u1s1-cli 1.14.6 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/agent-setup.d.ts +7 -0
  2. package/dist/agent-setup.js +14 -0
  3. package/dist/agents/agent-config.d.ts +83 -0
  4. package/dist/agents/agent-config.js +154 -0
  5. package/dist/agents/claude.d.ts +80 -0
  6. package/dist/agents/claude.js +233 -0
  7. package/dist/agents/codex.d.ts +111 -0
  8. package/dist/agents/codex.js +392 -0
  9. package/dist/agents/launch.d.ts +109 -0
  10. package/dist/agents/launch.js +444 -0
  11. package/dist/agents/pi.d.ts +9 -0
  12. package/dist/agents/pi.js +562 -0
  13. package/dist/agents/prefs.d.ts +35 -0
  14. package/dist/agents/prefs.js +96 -0
  15. package/dist/agents/runtime.d.ts +354 -0
  16. package/dist/agents/runtime.js +884 -0
  17. package/dist/agents/status.d.ts +20 -0
  18. package/dist/agents/status.js +241 -0
  19. package/dist/config-command.d.ts +18 -0
  20. package/dist/config-command.js +335 -0
  21. package/dist/config.d.ts +17 -0
  22. package/dist/config.js +35 -1
  23. package/dist/device-auth.d.ts +37 -1
  24. package/dist/device-auth.js +43 -8
  25. package/dist/embed.d.ts +13 -0
  26. package/dist/embed.js +15 -0
  27. package/dist/index.js +22 -556
  28. package/dist/launcher.d.ts +48 -0
  29. package/dist/launcher.js +235 -0
  30. package/dist/mcp-server.d.ts +105 -0
  31. package/dist/mcp-server.js +449 -0
  32. package/dist/pi-ui-text.d.ts +57 -0
  33. package/dist/pi-ui-text.js +148 -0
  34. package/dist/usage.js +2 -2
  35. package/package.json +1 -1
  36. package/scripts/p0-claude-code/README.md +39 -0
  37. package/scripts/p0-claude-code/launch-claude.ts +94 -0
  38. package/scripts/p0-claude-code/replay.ts +74 -0
  39. package/scripts/p0-claude-code/seed-local.mjs +145 -0
  40. package/scripts/p0-claude-code/tap.mjs +105 -0
  41. package/scripts/patch-pi.js +76 -55
  42. package/scripts/pi-patch-report.mjs +224 -0
  43. package/scripts/pi-upgrade.mjs +829 -0
  44. package/scripts/render-regression.mjs +67 -5
@@ -0,0 +1,20 @@
1
+ import { type DefaultAgent } from "../config.js";
2
+ import { type LaunchPaths } from "./launch.js";
3
+ import { dailyUpdateCheck, ensureAgentRuntime, type ManagedAgent } from "./runtime.js";
4
+ export declare function launchPaths(agent: ManagedAgent): LaunchPaths;
5
+ export declare function printAgentsStatus(defaultAgent: DefaultAgent | undefined): Promise<void>;
6
+ export declare function pinCommand(raw: string | undefined, agent?: ManagedAgent): Promise<number>;
7
+ export declare function unpinCommand(agent?: ManagedAgent): number;
8
+ export interface InstallCommandDeps {
9
+ /** GET /public/agents/:agent/runtime;测试注入 */
10
+ fetchPublicRuntime?: (url: URL) => Promise<Response>;
11
+ ensure?: typeof ensureAgentRuntime;
12
+ checkLatest?: typeof dailyUpdateCheck;
13
+ }
14
+ /**
15
+ * `u1s1 agents install claude|codex`:找到或下载原版,不启动。与 `u1s1 claude` / `u1s1 codex` 走同一套
16
+ * ensureAgentRuntime:首次从官方下载前同样要用户确认(没有终端就不下载)。
17
+ * 不需要登录。刹车用缓存的网关配置;没有缓存时问网关的公开刹车接口,问不到就不下载。
18
+ * 缓存里或公开接口明确停用时不下载。
19
+ */
20
+ export declare function installCommand(deps?: InstallCommandDeps, agent?: ManagedAgent): Promise<number>;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * `u1s1 agents`:各客户端的安装状态、版本、是否有更新;`pin` / `unpin` 锁定版本;
3
+ * `install` 只下载不启动(安装脚本预选客户端时用)。
4
+ * 刹车用上次成功拉到的网关配置(缓存);没有缓存时 status / pin 只按版本号格式校验,
5
+ * install 改问网关的公开刹车接口,问不到就不下载。
6
+ */
7
+ import { mkdirSync } from "node:fs";
8
+ import { loadConfig, VERSION } from "../config.js";
9
+ import { gatewayFetch } from "../gateway-route.js";
10
+ import { tr } from "../i18n.js";
11
+ import { siteOf } from "../site.js";
12
+ import { loadPublicAgentRuntime, parseAgentConfig, readConfigCache } from "./agent-config.js";
13
+ import { claudePaths } from "./claude.js";
14
+ import { codexPaths } from "./codex.js";
15
+ import { AGENT_PROFILES, agentsRoot, cliInstallHooks, notOpenText, printRuntimeFailure, printRuntimeNotes, } from "./launch.js";
16
+ import { readAgentPrefs } from "./prefs.js";
17
+ import { bestInstallTarget, checkBrake, compareVersions, dailyUpdateCheck, ensureAgentRuntime, fetchLatestVersion, findOnPath, installedVersions, MANAGED_AGENTS, NO_BRAKE, normalizeVersion, npmRegistryFor, probeVersion, readRuntimeState, runtimeSpec, updateRuntimeState, } from "./runtime.js";
18
+ export function launchPaths(agent) {
19
+ return agent === "codex" ? codexPaths() : claudePaths();
20
+ }
21
+ function cachedConfig(agent) {
22
+ const cfg = loadConfig();
23
+ const cached = readConfigCache(launchPaths(agent).configCache, new URL(cfg.baseUrl).origin);
24
+ return cached ? parseAgentConfig(cached.body, AGENT_PROFILES[agent].gatewayAgent) : undefined;
25
+ }
26
+ function cachedBrake(agent) {
27
+ return cachedConfig(agent)?.runtime ?? NO_BRAKE;
28
+ }
29
+ function verdictText(v, brake) {
30
+ if (v === "blocked")
31
+ return tr("该版本已被停用", "this version has been disabled");
32
+ if (v === "below_min")
33
+ return tr(`低于最低要求 ${brake.minVersion}`, `below the minimum ${brake.minVersion}`);
34
+ return tr(`高于已验证的最高版本 ${brake.maxTestedVersion}`, `above the highest tested version ${brake.maxTestedVersion}`);
35
+ }
36
+ export async function printAgentsStatus(defaultAgent) {
37
+ const mark = (agent) => (defaultAgent === agent ? tr(" [默认]", " [default]") : "");
38
+ console.log(tr("客户端", "Clients"));
39
+ console.log(` pi ${tr(`内置,随 u1s1 v${VERSION} 发布`, `built in, ships with u1s1 v${VERSION}`)}${mark("pi")}`);
40
+ for (const agent of MANAGED_AGENTS) {
41
+ const lines = await managedAgentLines(agent);
42
+ console.log(` ${agent.padEnd(6)} ${lines[0]}${mark(agent)}`);
43
+ for (const line of lines.slice(1))
44
+ console.log(` ${line}`);
45
+ }
46
+ }
47
+ async function managedAgentLines(agent) {
48
+ const cfg = loadConfig();
49
+ const site = siteOf(cfg);
50
+ const spec = runtimeSpec(agent);
51
+ const cached = cachedConfig(agent);
52
+ const brake = cached?.runtime ?? NO_BRAKE;
53
+ const paths = launchPaths(agent);
54
+ const state = readRuntimeState(paths);
55
+ const installed = installedVersions(paths, spec);
56
+ const managed = state.current && installed.includes(state.current) ? state.current : installed[0];
57
+ const envBin = process.env[spec.binEnv]?.trim();
58
+ const pathBin = findOnPath(spec.command, { env: process.env, platform: process.platform });
59
+ // 探测用户自己的命令时把配置目录指到启动器自己的目录(CODEX_HOME 指向不存在的目录时 Codex 会报错)
60
+ if (envBin || pathBin)
61
+ mkdirSync(paths.configDir, { recursive: true, mode: 0o700 });
62
+ const pathVersion = pathBin ? probeVersion(pathBin, paths.configDir, spec) : undefined;
63
+ const latest = await fetchLatestVersion(npmRegistryFor(site.id), spec.npmPackage);
64
+ const autoUpdate = readAgentPrefs(agent).autoUpdate;
65
+ const lines = [];
66
+ if (cached && !cached.enabled) {
67
+ lines.push(tr(`暂不可用:${cached.disabledReason ?? notOpenText()}`, `Not available: ${cached.disabledReason ?? notOpenText()}`));
68
+ }
69
+ if (envBin) {
70
+ const v = probeVersion(envBin, paths.configDir, spec);
71
+ lines.push(tr(`使用 ${spec.binEnv} 指定的 ${envBin}(${v ?? "无法运行"})`, `Using ${envBin} from ${spec.binEnv} (${v ?? "doesn't run"})`));
72
+ if (v && checkBrake(v, brake) === "above_max_tested") {
73
+ lines.push(tr(`它${verdictText("above_max_tested", brake)},运行时改用 u1s1 管理的版本`, `It's ${verdictText("above_max_tested", brake)}; u1s1 ${agent} uses a u1s1-managed copy instead`));
74
+ }
75
+ }
76
+ else if (managed) {
77
+ lines.push(tr(`已安装 ${managed}(由 u1s1 管理)`, `Installed ${managed} (managed by u1s1)`));
78
+ }
79
+ else if (pathVersion && checkBrake(pathVersion, brake) === "ok") {
80
+ lines.push(tr(`使用本机已有的 ${pathBin}(${pathVersion})`, `Using the existing ${pathBin} (${pathVersion})`));
81
+ }
82
+ else if (pathVersion && checkBrake(pathVersion, brake) === "above_max_tested") {
83
+ lines.push(tr(`本机已有的 ${pathBin}(${pathVersion})${verdictText("above_max_tested", brake)},不会使用;首次运行 u1s1 ${agent} 时另装一份`, `The existing ${pathBin} (${pathVersion}) is ${verdictText("above_max_tested", brake)} and won't be used; a separate copy is installed the first time you run u1s1 ${agent}`));
84
+ }
85
+ else {
86
+ lines.push(tr(`未安装,首次运行 u1s1 ${agent} 时从官方渠道下载`, `Not installed; downloaded from the official source the first time you run u1s1 ${agent}`));
87
+ }
88
+ if (managed) {
89
+ const verdict = checkBrake(managed, brake);
90
+ if (verdict !== "ok") {
91
+ lines.push(autoUpdate
92
+ ? tr(`当前版本不可用(${verdictText(verdict, brake)}),下次启动前会换到可用的版本`, `Current version can't be used (${verdictText(verdict, brake)}); it will be replaced with an allowed version before the next launch`)
93
+ : tr(`当前版本不可用(${verdictText(verdict, brake)}),自动更新已关闭:运行 u1s1 agents install ${agent} 换到可用的版本`, `Current version can't be used (${verdictText(verdict, brake)}). Automatic updates are off: run u1s1 agents install ${agent} to switch to an allowed version`));
94
+ }
95
+ }
96
+ const managedBraked = managed !== undefined && checkBrake(managed, brake) !== "ok";
97
+ if (!autoUpdate && !state.pinned && !managedBraked)
98
+ lines.push(tr(`自动更新已关闭(u1s1 config set ${agent}.auto-update on 打开)`, `Automatic updates are off (u1s1 config set ${agent}.auto-update on to turn them on)`));
99
+ if (state.pinned)
100
+ lines.push(tr(`已锁定 ${state.pinned}`, `Pinned to ${state.pinned}`));
101
+ if (!envBin) {
102
+ const target = state.pinned ?? bestInstallTarget([latest, brake.maxTestedVersion], brake);
103
+ if (!latest)
104
+ lines.push(tr("暂时查不到官方最新版(网络?)", "Couldn't check the latest official version (network?)"));
105
+ else if (managed && target && compareVersions(target, managed) > 0) {
106
+ lines.push(autoUpdate || state.pinned
107
+ ? tr(`有更新:${target},下次运行 u1s1 ${agent} 时安装`, `Update available: ${target}, installed the next time you run u1s1 ${agent}`)
108
+ : tr(`有更新:${target},运行 u1s1 agents install ${agent} 安装`, `Update available: ${target}; run u1s1 agents install ${agent} to install it`));
109
+ }
110
+ else if (managed) {
111
+ lines.push(tr(`已是可用的最新版(官方最新 ${latest})`, `Up to date (latest official: ${latest})`));
112
+ }
113
+ else {
114
+ lines.push(tr(`官方最新 ${latest}`, `Latest official: ${latest}`));
115
+ }
116
+ }
117
+ return lines;
118
+ }
119
+ const PIN_EXAMPLE = { claude: "2.1.322", codex: "0.167.2" };
120
+ export async function pinCommand(raw, agent = "claude") {
121
+ const version = normalizeVersion(raw);
122
+ if (!version) {
123
+ console.error(tr(`用法: u1s1 agents pin ${agent} <版本>,例如 ${PIN_EXAMPLE[agent]}`, `Usage: u1s1 agents pin ${agent} <version>, e.g. ${PIN_EXAMPLE[agent]}`));
124
+ return 1;
125
+ }
126
+ const brake = cachedBrake(agent);
127
+ const verdict = checkBrake(version, brake);
128
+ if (verdict !== "ok") {
129
+ console.error(tr(`不能锁定 ${version}:${verdictText(verdict, brake)}。`, `Can't pin ${version}: ${verdictText(verdict, brake)}.`));
130
+ return 1;
131
+ }
132
+ updateRuntimeState(launchPaths(agent), { pinned: version, pending: undefined });
133
+ console.log(tr(`已锁定 ${version},下次运行 u1s1 ${agent} 时生效(需要时会先下载)。解除:u1s1 agents unpin ${agent}`, `Pinned to ${version}; takes effect the next time you run u1s1 ${agent} (downloading it first if needed). To undo: u1s1 agents unpin ${agent}`));
134
+ return 0;
135
+ }
136
+ export function unpinCommand(agent = "claude") {
137
+ const state = readRuntimeState(launchPaths(agent));
138
+ if (!state.pinned) {
139
+ console.log(tr("没有锁定版本。", "No version is pinned."));
140
+ return 0;
141
+ }
142
+ updateRuntimeState(launchPaths(agent), { pinned: undefined, lastCheckAt: undefined });
143
+ console.log(readAgentPrefs(agent).autoUpdate
144
+ ? tr("已解除锁定,恢复自动更新。", "Pin removed; automatic updates resume.")
145
+ : tr(`已解除锁定。自动更新是关着的:继续用现在的版本,运行 u1s1 agents install ${agent} 手动更新。`, `Pin removed. Automatic updates are off: the current version stays; run u1s1 agents install ${agent} to update.`));
146
+ return 0;
147
+ }
148
+ function fetchPublicRuntime(url) {
149
+ return gatewayFetch(url, {
150
+ headers: { "x-u1s1-version": VERSION, accept: "application/json" },
151
+ signal: AbortSignal.timeout(15_000),
152
+ });
153
+ }
154
+ /**
155
+ * 没有配置缓存(多半还没登录)时的刹车来源:网关的公开刹车接口。拿不到(老网关 404、网络失败)
156
+ * 或明确停用时返回退出码,调用方不下载——宁可晚装,也不在没有版本上限的情况下装。
157
+ */
158
+ async function publicBrake(agent, baseUrl, deps) {
159
+ const fetchRuntime = deps.fetchPublicRuntime ?? fetchPublicRuntime;
160
+ const gatewayAgent = AGENT_PROFILES[agent].gatewayAgent;
161
+ const result = await loadPublicAgentRuntime({
162
+ agent: gatewayAgent,
163
+ fetchRuntime: () => fetchRuntime(new URL(`/public/agents/${gatewayAgent}/runtime`, baseUrl)),
164
+ });
165
+ if (result.status !== "ok") {
166
+ console.error(tr(` 暂时拿不到允许安装的版本范围,这次先不下载;第一次运行 u1s1 ${agent} 时会再安装。`, ` Couldn't get the range of versions allowed right now, so nothing was downloaded; it will be installed the first time you run u1s1 ${agent}.`));
167
+ return 1;
168
+ }
169
+ if (!result.brakes.enabled) {
170
+ console.error(result.brakes.disabledReason ?? notOpenText());
171
+ return 1;
172
+ }
173
+ return result.brakes.runtime;
174
+ }
175
+ /**
176
+ * `u1s1 agents install claude|codex`:找到或下载原版,不启动。与 `u1s1 claude` / `u1s1 codex` 走同一套
177
+ * ensureAgentRuntime:首次从官方下载前同样要用户确认(没有终端就不下载)。
178
+ * 不需要登录。刹车用缓存的网关配置;没有缓存时问网关的公开刹车接口,问不到就不下载。
179
+ * 缓存里或公开接口明确停用时不下载。
180
+ */
181
+ export async function installCommand(deps = {}, agent = "claude") {
182
+ const cfg = loadConfig();
183
+ const site = siteOf(cfg);
184
+ const cached = cachedConfig(agent);
185
+ let brake;
186
+ if (cached) {
187
+ if (!cached.enabled) {
188
+ console.error(cached.disabledReason ?? notOpenText());
189
+ return 1;
190
+ }
191
+ brake = cached.runtime;
192
+ }
193
+ else {
194
+ const got = await publicBrake(agent, cfg.baseUrl, deps);
195
+ if (typeof got === "number")
196
+ return got;
197
+ brake = got;
198
+ }
199
+ const paths = launchPaths(agent);
200
+ const spec = runtimeSpec(agent);
201
+ mkdirSync(paths.configDir, { recursive: true, mode: 0o700 });
202
+ // 自动更新关着时这就是「手动更新」:现在查一次官方最新版(记成 pending,下面照常安装)。
203
+ // 开着时行为不变:每天的检查已经记下了 pending。
204
+ if (!readAgentPrefs(agent).autoUpdate) {
205
+ await (deps.checkLatest ?? ((input) => dailyUpdateCheck(input)))({
206
+ paths,
207
+ brake,
208
+ now: Date.now(),
209
+ latest: () => fetchLatestVersion(npmRegistryFor(site.id), spec.npmPackage),
210
+ force: true,
211
+ }).catch(() => undefined);
212
+ }
213
+ let runtime;
214
+ try {
215
+ runtime = await (deps.ensure ?? ensureAgentRuntime)(agent, {
216
+ brake,
217
+ site: site.id,
218
+ agentsRoot: agentsRoot(),
219
+ hooks: cliInstallHooks(agent),
220
+ probeConfigDir: paths.configDir,
221
+ });
222
+ }
223
+ catch (e) {
224
+ if (!printRuntimeFailure(e, agent))
225
+ throw e;
226
+ return 1;
227
+ }
228
+ printRuntimeNotes(runtime, agent);
229
+ if (runtime.installError)
230
+ return 1;
231
+ if (runtime.source === "env") {
232
+ console.log(tr(`使用 ${spec.binEnv} 指定的 ${runtime.bin}(${runtime.version}),不用下载。`, `Using ${runtime.bin} from ${spec.binEnv} (${runtime.version}); nothing to download.`));
233
+ }
234
+ else if (runtime.source === "path") {
235
+ console.log(tr(`将使用本机已有的 ${runtime.bin}(${runtime.version}),不用下载。`, `The existing ${runtime.bin} (${runtime.version}) will be used; nothing to download.`));
236
+ }
237
+ else {
238
+ console.log(tr(`${agent} ${runtime.version} 已就绪,运行 u1s1 ${agent} 开始使用。`, `${agent} ${runtime.version} is ready. Run u1s1 ${agent} to start.`));
239
+ }
240
+ return 0;
241
+ }
@@ -0,0 +1,18 @@
1
+ declare const CLIENTS: readonly ["claude", "codex"];
2
+ type Client = (typeof CLIENTS)[number];
3
+ export type ConfigKey = "default-client" | "pi.model" | `${Client}.model` | `${Client}.auto-update` | `${Client}.pin`;
4
+ export declare const CONFIG_KEYS: readonly ConfigKey[];
5
+ export declare function isConfigKey(value: unknown): value is ConfigKey;
6
+ export interface ConfigValue {
7
+ /** 存下来的值;undefined = 没设置 */
8
+ value?: string;
9
+ /** 没设置时实际生效的说明(列表里显示) */
10
+ fallback: string;
11
+ }
12
+ /** 读当前值。deps 供测试注入。 */
13
+ export declare function readConfigValue(key: ConfigKey): Promise<ConfigValue>;
14
+ /** 写一项;返回退出码(已向用户说明)。value = undefined 表示 unset。 */
15
+ export declare function writeConfigValue(key: ConfigKey, value: string | undefined): Promise<number>;
16
+ export declare function configHelpLines(): string[];
17
+ export declare function configCommand(args: readonly string[]): Promise<number>;
18
+ export {};
@@ -0,0 +1,335 @@
1
+ /**
2
+ * `u1s1 config`:用户可调的启动器设置(规划 4.6)。已有命令(`u1s1 use`、`u1s1 model`、`u1s1 agents pin`)
3
+ * 行为不变,这里是它们的统一入口,另加「每个客户端的默认模型」和「是否自动更新」。
4
+ *
5
+ * u1s1 config 终端里是交互菜单;没有终端时同 list
6
+ * u1s1 config list 列出全部设置和当前值
7
+ * u1s1 config get <键> 只打印值(脚本用);没设置过、也没有默认值时退出码 1
8
+ * u1s1 config set <键> <值>
9
+ * u1s1 config unset <键> 恢复默认
10
+ *
11
+ * 键:
12
+ * default-client pi | claude | codex 同 u1s1 use
13
+ * pi.model 模型 id 或别名 同 u1s1 model <模型>
14
+ * claude.model 模型 id u1s1 claude 的默认模型(--settings 的 model)
15
+ * codex.model 模型 id u1s1 codex 的默认模型(config.toml 的 model)
16
+ * claude.auto-update on | off 每天查新、下次启动前更新;缺省 on
17
+ * codex.auto-update on | off
18
+ * claude.pin 版本号 同 u1s1 agents pin / unpin
19
+ * codex.pin 版本号
20
+ *
21
+ * 客户端的模型只按网关上次下发的阵容(启动配置缓存)核对:还没启动过、没有缓存时照收,启动时再核对,
22
+ * 不在阵容里就退回默认模型并提示一次(prefs.ts)。
23
+ */
24
+ import { existsSync } from "node:fs";
25
+ import { createInterface } from "node:readline/promises";
26
+ import { agentSettingsFile, isDefaultAgent, loadConfig, readRawConfigFile, saveDefaultAgent, } from "./config.js";
27
+ import { tr } from "./i18n.js";
28
+ const CLIENTS = ["claude", "codex"];
29
+ export const CONFIG_KEYS = [
30
+ "default-client",
31
+ "pi.model",
32
+ "claude.model",
33
+ "codex.model",
34
+ "claude.auto-update",
35
+ "codex.auto-update",
36
+ "claude.pin",
37
+ "codex.pin",
38
+ ];
39
+ export function isConfigKey(value) {
40
+ return typeof value === "string" && CONFIG_KEYS.includes(value);
41
+ }
42
+ function clientOf(key) {
43
+ const prefix = key.split(".")[0];
44
+ return prefix === "claude" || prefix === "codex" ? prefix : undefined;
45
+ }
46
+ function describe(key) {
47
+ const client = clientOf(key);
48
+ if (key === "default-client")
49
+ return tr("运行 u1s1 时进入的客户端", "Client that plain u1s1 opens");
50
+ if (key === "pi.model")
51
+ return tr("Pi 的默认模型(同 u1s1 model)", "Pi's default model (same as u1s1 model)");
52
+ if (key.endsWith(".model"))
53
+ return tr(`u1s1 ${client} 启动时用的模型`, `Model u1s1 ${client} starts with`);
54
+ if (key.endsWith(".auto-update"))
55
+ return tr(`每天查一次 ${client} 新版,下次启动前更新`, `Check for a new ${client} daily and update before the next launch`);
56
+ return tr(`把 ${client} 锁定在某个版本(同 u1s1 agents pin)`, `Pin ${client} to a version (same as u1s1 agents pin)`);
57
+ }
58
+ /** 读当前值。deps 供测试注入。 */
59
+ export async function readConfigValue(key) {
60
+ const client = clientOf(key);
61
+ if (key === "default-client") {
62
+ const raw = readRawConfigFile();
63
+ const stored = raw?.["defaultAgent"];
64
+ if (isDefaultAgent(stored))
65
+ return { value: stored, fallback: "" };
66
+ const { isExistingUser } = await import("./launcher.js");
67
+ return {
68
+ fallback: isExistingUser(raw, existsSync(agentSettingsFile))
69
+ ? tr("pi(未设置)", "pi (not set)")
70
+ : tr("未设置(首次运行 u1s1 时选择)", "not set (chosen the first time you run u1s1)"),
71
+ };
72
+ }
73
+ if (key === "pi.model") {
74
+ const model = loadConfig().model;
75
+ return model ? { value: model, fallback: "" } : { fallback: tr("站点默认", "site default") };
76
+ }
77
+ const { readAgentPrefs } = await import("./agents/prefs.js");
78
+ if (key.endsWith(".model")) {
79
+ const model = readAgentPrefs(client).model;
80
+ return model ? { value: model, fallback: "" } : { fallback: tr("跟随网关默认", "gateway default") };
81
+ }
82
+ if (key.endsWith(".auto-update")) {
83
+ return readAgentPrefs(client).autoUpdate ? { value: "on", fallback: "" } : { value: "off", fallback: "" };
84
+ }
85
+ const { readRuntimeState } = await import("./agents/runtime.js");
86
+ const { launchPaths } = await import("./agents/status.js");
87
+ const pinned = readRuntimeState(launchPaths(client)).pinned;
88
+ return pinned ? { value: pinned, fallback: "" } : { fallback: tr("未锁定", "not pinned") };
89
+ }
90
+ // ---------------------------------------------------------------- write
91
+ /** 客户端阵容:上次缓存的网关启动配置;没有缓存返回 undefined(不核对)。 */
92
+ async function cachedLineup(client) {
93
+ const { readConfigCache, parseAgentConfig } = await import("./agents/agent-config.js");
94
+ const { AGENT_PROFILES } = await import("./agents/launch.js");
95
+ const { launchPaths } = await import("./agents/status.js");
96
+ const cached = readConfigCache(launchPaths(client).configCache, new URL(loadConfig().baseUrl).origin);
97
+ const config = cached ? parseAgentConfig(cached.body, AGENT_PROFILES[client].gatewayAgent) : undefined;
98
+ if (!config || !config.enabled)
99
+ return undefined;
100
+ return { ids: config.models.map((m) => m.id), defaultModel: config.defaultModel };
101
+ }
102
+ function parseSwitch(value) {
103
+ const v = value.trim().toLowerCase();
104
+ if (["on", "true", "yes", "1", "开"].includes(v))
105
+ return true;
106
+ if (["off", "false", "no", "0", "关"].includes(v))
107
+ return false;
108
+ return undefined;
109
+ }
110
+ /** 写一项;返回退出码(已向用户说明)。value = undefined 表示 unset。 */
111
+ export async function writeConfigValue(key, value) {
112
+ const client = clientOf(key);
113
+ if (key === "default-client") {
114
+ if (value === undefined) {
115
+ saveDefaultAgent(undefined);
116
+ console.log(tr("已清除默认客户端。", "Default client cleared."));
117
+ return 0;
118
+ }
119
+ if (!isDefaultAgent(value)) {
120
+ console.error(tr("default-client 只能是 pi、claude 或 codex。", "default-client must be pi, claude or codex."));
121
+ return 1;
122
+ }
123
+ saveDefaultAgent(value);
124
+ console.log(tr(`已把默认客户端设为 ${value}:运行 u1s1 直接进入它。`, `Default client set to ${value}: running u1s1 now opens it.`));
125
+ return 0;
126
+ }
127
+ if (key === "pi.model") {
128
+ if (value === undefined) {
129
+ console.error(tr("Pi 总要有一个模型,没法清除;用 u1s1 config set pi.model <模型> 换一个(u1s1 model 可看列表)。", "Pi always needs a model, so this can't be cleared; pick one with u1s1 config set pi.model <model> (u1s1 model lists them)."));
130
+ return 1;
131
+ }
132
+ const { modelCommand } = await import("./model.js");
133
+ const before = process.exitCode;
134
+ process.exitCode = 0;
135
+ await modelCommand(value);
136
+ const code = typeof process.exitCode === "number" ? process.exitCode : 0;
137
+ process.exitCode = before;
138
+ return code;
139
+ }
140
+ const { saveAgentPrefs } = await import("./agents/prefs.js");
141
+ if (key.endsWith(".model")) {
142
+ if (value === undefined) {
143
+ saveAgentPrefs(client, { model: null });
144
+ console.log(tr(`u1s1 ${client} 改回跟随网关默认模型。`, `u1s1 ${client} is back on the gateway default model.`));
145
+ return 0;
146
+ }
147
+ const { isModelId } = await import("./agents/prefs.js");
148
+ const model = value.trim();
149
+ if (model === "default")
150
+ return writeConfigValue(key, undefined);
151
+ if (!isModelId(model)) {
152
+ console.error(tr(`不是有效的模型 id:${value}`, `Not a valid model id: ${value}`));
153
+ return 1;
154
+ }
155
+ const lineup = await cachedLineup(client);
156
+ if (lineup && !lineup.ids.includes(model)) {
157
+ console.error(tr(`${model} 不在 ${client} 当前可用的模型里。可选:`, `${model} isn't one of the models currently available for ${client}. Choose from:`));
158
+ for (const id of lineup.ids)
159
+ console.error(` ${id}`);
160
+ return 1;
161
+ }
162
+ saveAgentPrefs(client, { model });
163
+ console.log(lineup
164
+ ? tr(`u1s1 ${client} 的默认模型设为 ${model},下次启动生效。`, `u1s1 ${client} will start with ${model} from the next launch.`)
165
+ : tr(`u1s1 ${client} 的默认模型设为 ${model}。还没拿到过 ${client} 的模型列表,下次启动时核对;不可用时会用默认模型并提示。`, `u1s1 ${client} will start with ${model}. The ${client} model list hasn't been fetched yet, so it's checked at the next launch; if it isn't available, the default model is used and you'll be told.`));
166
+ return 0;
167
+ }
168
+ if (key.endsWith(".auto-update")) {
169
+ const on = value === undefined ? true : parseSwitch(value);
170
+ if (on === undefined) {
171
+ console.error(tr(`${key} 只能是 on 或 off。`, `${key} must be on or off.`));
172
+ return 1;
173
+ }
174
+ saveAgentPrefs(client, { autoUpdate: on });
175
+ console.log(on
176
+ ? tr(`已打开 ${client} 的自动更新:每天查一次新版,下次启动前更新。`, `Automatic updates for ${client} are on: checked daily, installed before the next launch.`)
177
+ : tr(`已关闭 ${client} 的自动更新:一直用现在的版本,运行 u1s1 agents install ${client} 手动更新。`
178
+ + `版本被停用或太旧时仍要先更新才能启动。`, `Automatic updates for ${client} are off: the current version stays; run u1s1 agents install ${client} to update. `
179
+ + `A version that has been disabled or is too old still has to be updated before it can start.`));
180
+ return 0;
181
+ }
182
+ const { pinCommand, unpinCommand } = await import("./agents/status.js");
183
+ return value === undefined ? unpinCommand(client) : await pinCommand(value, client);
184
+ }
185
+ // ---------------------------------------------------------------- command
186
+ export function configHelpLines() {
187
+ const rows = [
188
+ ["u1s1 config", tr("交互式修改(没有终端时同 list)", "Change settings interactively (same as list without a terminal)")],
189
+ ["u1s1 config list", tr("列出全部设置", "List all settings")],
190
+ ["u1s1 config get <键>", tr("打印一项的值", "Print one value")],
191
+ ["u1s1 config set <键> <值>", tr("修改一项", "Change one value")],
192
+ ["u1s1 config unset <键>", tr("恢复默认", "Restore the default")],
193
+ ];
194
+ const keyRows = [
195
+ ["default-client", "pi | claude | codex"],
196
+ ["pi.model", tr("模型 id 或别名", "model id or alias")],
197
+ ["claude.model, codex.model", tr("模型 id;default = 跟随网关默认", "model id; default = gateway default")],
198
+ ["claude.auto-update, codex.auto-update", "on | off"],
199
+ ["claude.pin, codex.pin", tr("版本号", "version")],
200
+ ];
201
+ const usage = rows.map(([a, b]) => ` ${tr(a, a.replace("<键>", "<key>").replace("<值>", "<value>")).padEnd(30)}${b}`);
202
+ return [
203
+ tr("用法:", "Usage:"),
204
+ ...usage,
205
+ "",
206
+ tr("键:", "Keys:"),
207
+ ...keyRows.map(([a, b]) => ` ${a.padEnd(38)}${b}`),
208
+ "",
209
+ tr(" 客户端里用 /model 换的模型只对那一次会话有效;想让 u1s1 claude / u1s1 codex 每次都用它,在这里设。", " A model picked with /model inside a client only lasts for that session; set it here to start with it every time."),
210
+ ];
211
+ }
212
+ export async function configCommand(args) {
213
+ const [sub, key, ...rest] = args;
214
+ if (sub === "--help" || sub === "-h" || sub === "help") {
215
+ for (const line of configHelpLines())
216
+ console.log(line);
217
+ return 0;
218
+ }
219
+ if (sub === undefined) {
220
+ if (process.stdin.isTTY && process.stdout.isTTY)
221
+ return interactiveConfig();
222
+ return listConfig();
223
+ }
224
+ if (sub === "list" && key === undefined)
225
+ return listConfig();
226
+ if ((sub === "get" || sub === "set" || sub === "unset") && key !== undefined && !isConfigKey(key)) {
227
+ console.error(tr(`不认识的设置项:${key}`, `Unknown setting: ${key}`));
228
+ console.error(tr(`可用的:${CONFIG_KEYS.join("、")}`, `Available: ${CONFIG_KEYS.join(", ")}`));
229
+ return 1;
230
+ }
231
+ if (sub === "get" && isConfigKey(key) && rest.length === 0) {
232
+ const v = await readConfigValue(key);
233
+ if (v.value === undefined)
234
+ return 1;
235
+ console.log(v.value);
236
+ return 0;
237
+ }
238
+ if (sub === "set" && isConfigKey(key) && rest.length === 1)
239
+ return writeConfigValue(key, rest[0]);
240
+ if (sub === "unset" && isConfigKey(key) && rest.length === 0)
241
+ return writeConfigValue(key, undefined);
242
+ for (const line of configHelpLines())
243
+ console.error(line);
244
+ return 1;
245
+ }
246
+ async function listConfig() {
247
+ for (const key of CONFIG_KEYS) {
248
+ const v = await readConfigValue(key);
249
+ console.log(`${key.padEnd(20)}${v.value ?? `(${v.fallback})`}`);
250
+ }
251
+ return 0;
252
+ }
253
+ // ---------------------------------------------------------------- interactive
254
+ /** 终端里的显示宽度:中日韩字符和全角标点按 2 列算。 */
255
+ function displayWidth(text) {
256
+ let width = 0;
257
+ for (const ch of text)
258
+ width += /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6]/.test(ch) ? 2 : 1;
259
+ return width;
260
+ }
261
+ function padDisplay(text, width) {
262
+ return text + " ".repeat(Math.max(1, width - displayWidth(text)));
263
+ }
264
+ /** 某一项可选的值(交互菜单用);undefined = 自由输入。 */
265
+ async function choicesFor(key) {
266
+ const client = clientOf(key);
267
+ if (key === "default-client") {
268
+ return ["pi", "claude", "codex"].map((a) => ({ label: a, value: a }));
269
+ }
270
+ if (key.endsWith(".auto-update")) {
271
+ return [
272
+ { label: tr("on 每天查新,下次启动前更新", "on check daily, update before the next launch"), value: "on" },
273
+ { label: tr("off 一直用现在的版本,手动更新", "off keep the current version, update by hand"), value: "off" },
274
+ ];
275
+ }
276
+ if (client && key.endsWith(".model")) {
277
+ const lineup = await cachedLineup(client);
278
+ if (!lineup)
279
+ return undefined;
280
+ return [
281
+ { label: tr(`跟随网关默认${lineup.defaultModel ? `(现在是 ${lineup.defaultModel})` : ""}`, `Gateway default${lineup.defaultModel ? ` (currently ${lineup.defaultModel})` : ""}`), value: undefined },
282
+ ...lineup.ids.map((id) => ({ label: id, value: id })),
283
+ ];
284
+ }
285
+ return undefined;
286
+ }
287
+ async function interactiveConfig() {
288
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
289
+ try {
290
+ for (;;) {
291
+ console.log("");
292
+ console.log(tr(" u1s1 设置(脚本里可用 u1s1 config get|set <键> [值])", " u1s1 settings (scripts can use u1s1 config get|set <key> [value])"));
293
+ for (const [i, key] of CONFIG_KEYS.entries()) {
294
+ const v = await readConfigValue(key);
295
+ console.log(` ${String(i + 1).padStart(2)}) ${key.padEnd(20)}${padDisplay(v.value ?? `(${v.fallback})`, 26)}${describe(key)}`);
296
+ }
297
+ const answer = (await rl.question(tr(" 要改哪一项?输入序号,直接回车退出: ", " Which one? Enter a number, or press Enter to quit: "))).trim();
298
+ if (answer === "" || answer === "q")
299
+ return 0;
300
+ const index = Number(answer);
301
+ const key = Number.isInteger(index) ? CONFIG_KEYS[index - 1] : undefined;
302
+ if (!key) {
303
+ console.log(tr(` 请输入 1-${CONFIG_KEYS.length}`, ` Please enter 1-${CONFIG_KEYS.length}`));
304
+ continue;
305
+ }
306
+ await editOne(rl, key);
307
+ }
308
+ }
309
+ finally {
310
+ rl.close();
311
+ }
312
+ }
313
+ async function editOne(rl, key) {
314
+ const choices = await choicesFor(key);
315
+ if (!choices) {
316
+ const hint = key.endsWith(".pin")
317
+ ? tr("输入版本号,输入 - 解除锁定", "Enter a version, or - to unpin")
318
+ : tr("输入模型 id,输入 - 恢复默认", "Enter a model id, or - for the default");
319
+ const answer = (await rl.question(` ${key}: ${hint}(${tr("回车取消", "Enter to cancel")}) `)).trim();
320
+ if (answer === "")
321
+ return;
322
+ await writeConfigValue(key, answer === "-" ? undefined : answer);
323
+ return;
324
+ }
325
+ choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
326
+ const answer = (await rl.question(tr(" 选哪个?(回车取消) ", " Which one? (Enter to cancel) "))).trim();
327
+ if (answer === "")
328
+ return;
329
+ const picked = choices[Number(answer) - 1];
330
+ if (!picked) {
331
+ console.log(tr(" 没有这一项。", " No such option."));
332
+ return;
333
+ }
334
+ await writeConfigValue(key, picked.value);
335
+ }
package/dist/config.d.ts CHANGED
@@ -176,8 +176,13 @@ export interface CliConfig {
176
176
  modelProvider?: string;
177
177
  /** 登录时记下的账号邮箱,只用于横幅「账号」展示(真相以 u1s1 whoami 为准)。 */
178
178
  email?: string;
179
+ /** 默认客户端(`u1s1 use`/首次选择器写入);缺省 = 存量用户,按 Pi 处理(见 launcher.ts)。 */
180
+ defaultAgent?: DefaultAgent;
179
181
  }
182
+ export type DefaultAgent = "pi" | "claude" | "codex";
183
+ export declare function isDefaultAgent(value: unknown): value is DefaultAgent;
180
184
  export declare const u1s1Dir: string;
185
+ export declare const configFile: string;
181
186
  /** pi keeps auth/models/settings/sessions under this dir — isolated from any real pi install. */
182
187
  export declare const agentDir: string;
183
188
  export declare const agentSettingsFile: string;
@@ -246,6 +251,18 @@ export declare function writePrivateFile(path: string, content: string): void;
246
251
  export declare function stripEnvOverrides(cfg: CliConfig, file: Partial<CliConfig>, env?: NodeJS.ProcessEnv): Partial<CliConfig>;
247
252
  export declare function saveConfig(cfg: CliConfig): void;
248
253
  /** Persist the user's preferred model to both stores. */
254
+ /**
255
+ * 只改 config.json 里的 defaultAgent 一个键,其余原样保留(还没登录、文件不存在时也能写)。
256
+ * 不经 saveConfig:那会把默认 baseUrl 等生效值一并写死进文件。
257
+ */
258
+ export declare function saveDefaultAgent(agent: DefaultAgent | undefined): void;
259
+ /**
260
+ * 只改 config.json 里 mutate 碰到的键,其余原样保留(不经 saveConfig,理由同 saveDefaultAgent)。
261
+ * 文件在但读不出来时不覆盖(里面可能有登录凭据),抛错让用户先修好。
262
+ */
263
+ export declare function updateRawConfigFile(mutate: (file: Record<string, unknown>) => void): void;
264
+ /** config.json 的原始内容(不叠加环境变量);不存在或解析失败为 undefined。 */
265
+ export declare function readRawConfigFile(): Record<string, unknown> | undefined;
249
266
  export declare function persistPreferredModel(cfg: CliConfig, provider: string, modelId: string): CliConfig;
250
267
  export declare function saveEndpointsCache(endpoints: CustomEndpoint[]): void;
251
268
  export declare function loadEndpointsCache(): CustomEndpoint[];