u1s1-cli 1.14.6 → 1.15.1

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 +113 -0
  6. package/dist/agents/claude.js +411 -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,411 @@
1
+ /**
2
+ * `u1s1 claude`:以子进程启动用户机器上的官方原版客户端,经本机签名代理用设备凭证访问网关。
3
+ *
4
+ * 注入(规划 4.4):
5
+ * CLAUDE_CONFIG_DIR=~/.u1s1/agents/claude/config 与用户自己的 ~/.claude 互不影响
6
+ * ANTHROPIC_BASE_URL=http://127.0.0.1:<端口> 不带 /v1,客户端自己拼 /v1/messages
7
+ * ANTHROPIC_AUTH_TOKEN=<本地 token> 代理只认 Bearer;不能用 ANTHROPIC_API_KEY(发 x-api-key)
8
+ * DISABLE_AUTOUPDATER=1 更新由启动器管(runtime.ts)
9
+ * --settings <启动器写出的文件> 网关下发的模型阵容等;文件里没有任何凭证
10
+ * 调用方自己传的 --settings(文件或 JSON,可多个)不转给客户端,
11
+ * 按顺序合并进这份文件再清洗(Claude Code 只认最后一个 --settings),
12
+ * 见 extractSettingsArgs / mergeClaudeSettings
13
+ * --mcp-config <启动器写出的文件> u1s1 工具服务(搜索、读网页、生图,见 mcp-server.ts);
14
+ * 文件里只引用环境变量名,代理地址和本地 token 的值在子进程环境里
15
+ *
16
+ * 真实凭证(设备令牌、设备私钥)只留在启动器进程里:子进程环境从 stripSecretEnv 起步、
17
+ * 再显式写入上面几项;本地 token 只对本进程的签名代理有效,进程退出即作废。
18
+ * 不改二进制,不删除也不禁用它自带的登录方式。
19
+ */
20
+ import { readdirSync, readFileSync, rmSync } from "node:fs";
21
+ import { join, resolve } from "node:path";
22
+ import { writePrivateFile } from "../config.js";
23
+ import { lang, tr } from "../i18n.js";
24
+ import { MCP_ENV_NAMES, MCP_SERVER_NAME, MCP_TRACE_ENV, mcpInjectionDisabled, mcpLaunchEnv, mcpServerCommand, } from "../mcp-server.js";
25
+ import { AGENT_PROFILES, agentsRoot, prepareAgentLaunch, runPreparedAgent, } from "./launch.js";
26
+ import { runtimePaths } from "./runtime.js";
27
+ import { stripSecretEnv } from "../secret-env.js";
28
+ // 共用的启动流程与文案在 launch.ts;这里保留原来的导出名,调用方与测试不用改
29
+ export { agentsRoot, CLAUDE_TERMS_URL, cliInstallHooks, consentLines, notOpenText, overMaxTestedText, printRuntimeFailure, runtimeErrorText, spawnAgent, } from "./launch.js";
30
+ export const CLAUDE_GATEWAY_AGENT = "claude-code";
31
+ export function claudePaths(root = agentsRoot()) {
32
+ const base = runtimePaths(root, "claude");
33
+ return {
34
+ ...base,
35
+ configDir: join(base.root, "config"),
36
+ settingsFile: join(base.root, "settings.json"),
37
+ configCache: join(base.root, "agent-config.json"),
38
+ mcpConfigFile: join(base.root, "mcp.json"),
39
+ };
40
+ }
41
+ /**
42
+ * 子进程环境里要去掉的变量:会让它绕开本机代理(云厂商开关与地址)、换用别的凭证
43
+ * (API Key、OAuth token,其中 x-api-key 会被代理拒绝),或盖过网关下发设置的模型变量。
44
+ * 只影响这一次启动的子进程环境,用户自己的 shell 与配置不动。
45
+ */
46
+ export const CLAUDE_ENV_DROP = [
47
+ "ANTHROPIC_API_KEY",
48
+ "ANTHROPIC_AUTH_TOKEN",
49
+ "ANTHROPIC_BASE_URL",
50
+ "ANTHROPIC_CUSTOM_HEADERS",
51
+ "ANTHROPIC_MODEL",
52
+ "ANTHROPIC_SMALL_FAST_MODEL",
53
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
54
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
55
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
56
+ "CLAUDE_CODE_OAUTH_TOKEN",
57
+ "CLAUDE_CODE_USE_BEDROCK",
58
+ "CLAUDE_CODE_USE_VERTEX",
59
+ "CLAUDE_CODE_USE_FOUNDRY",
60
+ "ANTHROPIC_BEDROCK_BASE_URL",
61
+ "ANTHROPIC_VERTEX_BASE_URL",
62
+ "ANTHROPIC_FOUNDRY_BASE_URL",
63
+ "ANTHROPIC_FOUNDRY_API_KEY",
64
+ "AWS_BEARER_TOKEN_BEDROCK",
65
+ "CLAUDE_CONFIG_DIR",
66
+ // 下面两个会改掉网关客户端判定要核对的请求形状,判成中转嫌疑(只有赠送余额的账号直接 403):
67
+ // =0 时 system 里不再带计费头;
68
+ "CLAUDE_CODE_ATTRIBUTION_HEADER",
69
+ // 合并进请求体的 JSON,带 metadata 时会盖掉 metadata.user_id(会话标识核对不上)。
70
+ "CLAUDE_CODE_EXTRA_BODY",
71
+ // u1s1 工具服务的变量只由启动器设;关掉注入时也不留用户环境里的旧值
72
+ ...MCP_ENV_NAMES.filter((name) => name !== "U1S1_LANG"),
73
+ ];
74
+ export function claudeLaunchEnv(input) {
75
+ if (!/^http:\/\/127\.0\.0\.1:\d+$/.test(input.proxyOrigin)) {
76
+ throw new Error(`proxy origin must be a loopback origin without a path: ${input.proxyOrigin}`);
77
+ }
78
+ const env = stripSecretEnv(input.baseEnv);
79
+ for (const key of CLAUDE_ENV_DROP)
80
+ delete env[key];
81
+ // scrubForeignProviderEnv 会从启动器自己的环境里删 ANTHROPIC_AUTH_TOKEN,所以这里显式写进子进程环境
82
+ env["CLAUDE_CONFIG_DIR"] = input.configDir;
83
+ env["ANTHROPIC_BASE_URL"] = input.proxyOrigin;
84
+ env["ANTHROPIC_AUTH_TOKEN"] = input.localToken;
85
+ env["DISABLE_AUTOUPDATER"] = "1";
86
+ if (input.mcp)
87
+ Object.assign(env, input.mcp);
88
+ return env;
89
+ }
90
+ /**
91
+ * --mcp-config 是可变参数(后面不是选项的参数都会被当成配置文件),所以放在 --settings 前面,
92
+ * 让 --settings 把它截住;用户自己的参数(包括直接写的提示词)放最后。
93
+ *
94
+ * 例外:用户参数恰好只有一个 --version / -v / -V 时原样传,什么都不加。Claude Code 只在参数只有
95
+ * 版本开关时走快速返回;前面多了 --mcp-config / --settings 就会先跑完整初始化,断网时要等很久。
96
+ * 只打印版本用不到这两个配置。
97
+ */
98
+ export function claudeLaunchArgs(settingsFile, userArgs, mcpConfigFile) {
99
+ if (isBareVersionQuery(userArgs))
100
+ return [...userArgs];
101
+ return [...(mcpConfigFile ? ["--mcp-config", mcpConfigFile] : []), "--settings", settingsFile, ...userArgs];
102
+ }
103
+ const CLAUDE_VERSION_FLAGS = new Set(["--version", "-v", "-V"]);
104
+ /** `u1s1 claude --version`(或 -v / -V)且没有别的参数。 */
105
+ export function isBareVersionQuery(userArgs) {
106
+ return userArgs.length === 1 && CLAUDE_VERSION_FLAGS.has(userArgs[0]);
107
+ }
108
+ /**
109
+ * 写给 --mcp-config 的内容:一个 stdio 服务,env 里只有 `${变量名}` 引用,由 Claude Code 在拉起时
110
+ * 用它自己的环境展开(值是启动器放进子进程环境的,见 claudeLaunchEnv),文件里没有 token。
111
+ */
112
+ export function claudeMcpConfig(command = mcpServerCommand(), env = process.env) {
113
+ const names = [...MCP_ENV_NAMES];
114
+ if (env[MCP_TRACE_ENV])
115
+ names.push(MCP_TRACE_ENV);
116
+ return {
117
+ mcpServers: {
118
+ [MCP_SERVER_NAME]: {
119
+ type: "stdio",
120
+ command: command.command,
121
+ args: command.args,
122
+ env: Object.fromEntries(names.map((name) => [name, `\${${name}}`])),
123
+ },
124
+ },
125
+ };
126
+ }
127
+ /**
128
+ * 搜索与读网页预先允许(与 Pi 一致,不逐次确认;读网页直连不计费,搜索按次计费但单价低,
129
+ * 工具说明里要求模型别重复搜)。生图单价高:放进 ask,每次都问用户——不放 ask 的话,Claude Code
130
+ * 默认的 auto mode 会让它自己的安全检查替用户放行(2.1.322 实测),用户不知情就扣费。
131
+ */
132
+ export const CLAUDE_MCP_ALLOWED_TOOLS = [
133
+ `mcp__${MCP_SERVER_NAME}__web_search`,
134
+ `mcp__${MCP_SERVER_NAME}__web_fetch`,
135
+ ];
136
+ export const CLAUDE_MCP_ASK_TOOLS = [`mcp__${MCP_SERVER_NAME}__generate_image`];
137
+ function mergeRules(existing, rules) {
138
+ const out = Array.isArray(existing) ? [...existing] : [];
139
+ for (const rule of rules)
140
+ if (!out.includes(rule))
141
+ out.push(rule);
142
+ return out;
143
+ }
144
+ export function withMcpPermissions(settings) {
145
+ const raw = settings["permissions"];
146
+ const permissions = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...raw } : {};
147
+ permissions["allow"] = mergeRules(permissions["allow"], CLAUDE_MCP_ALLOWED_TOOLS);
148
+ permissions["ask"] = mergeRules(permissions["ask"], CLAUDE_MCP_ASK_TOOLS);
149
+ return { ...settings, permissions };
150
+ }
151
+ /**
152
+ * settings.env 里绝不能出现的键(契约上网关也不会发,这里再兜一层;调用方的 --settings 合并进来后同样过这一遍):
153
+ * CLAUDE_ENV_DROP 里换凭证、换地址、换云厂商、改 u1s1 工具服务连接的那些。模型变量可以由调用方改
154
+ * (等同于改 model);两个请求形状变量另由 CLAUDE_SETTINGS_ENV_PINNED 钉回默认值。
155
+ */
156
+ const SETTINGS_ENV_ALLOWED_FROM_DROP = new Set([
157
+ "ANTHROPIC_MODEL",
158
+ "ANTHROPIC_SMALL_FAST_MODEL",
159
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
160
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
161
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
162
+ ]);
163
+ /** 会让客户端去别处取凭证的设置项。 */
164
+ const SETTINGS_FORBIDDEN = new Set(["apiKeyHelper"]);
165
+ /**
166
+ * 启动器自己钉在 settings.env 里的值(盖过网关下发的同名项)。CLAUDE_ENV_DROP 只管得到 shell 环境;
167
+ * 用户仓库里的 .claude/settings.json / settings.local.json 也能用 env 设这两个变量,改掉网关判定要核对的
168
+ * 请求形状(=0 时 system 里不带计费头;EXTRA_BODY 带 metadata 时盖掉 metadata.user_id),判成中转嫌疑。
169
+ * --settings 的优先级高于项目设置,在这里写回默认值。
170
+ */
171
+ export const CLAUDE_SETTINGS_ENV_PINNED = {
172
+ CLAUDE_CODE_ATTRIBUTION_HEADER: "1",
173
+ CLAUDE_CODE_EXTRA_BODY: "{}",
174
+ };
175
+ const SETTINGS_ENV_FORBIDDEN = new Set(CLAUDE_ENV_DROP.filter((key) => !SETTINGS_ENV_ALLOWED_FROM_DROP.has(key) && !(key in CLAUDE_SETTINGS_ENV_PINNED)));
176
+ function isPlainObject(value) {
177
+ return !!value && typeof value === "object" && !Array.isArray(value);
178
+ }
179
+ /** 去掉连接/凭证相关项(SETTINGS_FORBIDDEN、env 里的 SETTINGS_ENV_FORBIDDEN),钉上 CLAUDE_SETTINGS_ENV_PINNED。 */
180
+ export function sanitizeClaudeSettings(settings) {
181
+ const out = {};
182
+ for (const [key, value] of Object.entries(settings)) {
183
+ if (!SETTINGS_FORBIDDEN.has(key))
184
+ out[key] = value;
185
+ }
186
+ const env = {};
187
+ if (isPlainObject(out["env"])) {
188
+ for (const [key, value] of Object.entries(out["env"])) {
189
+ if (!SETTINGS_ENV_FORBIDDEN.has(key))
190
+ env[key] = value;
191
+ }
192
+ }
193
+ out["env"] = { ...env, ...CLAUDE_SETTINGS_ENV_PINNED };
194
+ return out;
195
+ }
196
+ /**
197
+ * 网关的 settings → 写给 --settings 的对象:去掉连接/凭证相关项,钉上 CLAUDE_SETTINGS_ENV_PINNED;
198
+ * model 用调用方选好的(用户在 u1s1 config 里定的、仍在阵容里的模型),没给时保留网关的,
199
+ * 网关也没有就用 default_model。
200
+ */
201
+ export function claudeSettingsFromConfig(config, model) {
202
+ const out = sanitizeClaudeSettings(config.settings ?? {});
203
+ if (model)
204
+ out["model"] = model;
205
+ else if (out["model"] === undefined && config.defaultModel)
206
+ out["model"] = config.defaultModel;
207
+ return out;
208
+ }
209
+ // ---------------------------------------------------------------- 调用方的 --settings
210
+ /** 调用方的 --settings 有问题(缺值、读不了、不是 JSON 对象);runClaude 报出来后以非 0 退出。 */
211
+ export class ClaudeSettingsArgError extends Error {
212
+ name = "ClaudeSettingsArgError";
213
+ }
214
+ /**
215
+ * Claude Code 的 --settings 只认最后一个(2.1.323 实测),用户参数里再带一个就把启动器的整份设置盖掉。
216
+ * 所以从用户参数里把它们全抽出来(`--settings <值>` 与 `--settings=<值>`,按出现顺序;`--` 之后不动),
217
+ * 由启动器合并进自己写的那份。值可以是文件路径,也可以是 JSON 字符串(与 Claude Code 一样)。
218
+ */
219
+ export function extractSettingsArgs(userArgs) {
220
+ const rest = [];
221
+ const sources = [];
222
+ for (let i = 0; i < userArgs.length; i++) {
223
+ const arg = userArgs[i];
224
+ if (arg === "--") {
225
+ rest.push(...userArgs.slice(i));
226
+ break;
227
+ }
228
+ if (arg === "--settings") {
229
+ const value = userArgs[i + 1];
230
+ if (value === undefined || value === "")
231
+ throw new ClaudeSettingsArgError(tr("--settings 后面缺少文件路径或 JSON。", "--settings needs a file path or a JSON string."));
232
+ sources.push(value);
233
+ i++;
234
+ }
235
+ else if (arg.startsWith("--settings=")) {
236
+ const value = arg.slice("--settings=".length);
237
+ if (value === "")
238
+ throw new ClaudeSettingsArgError(tr("--settings= 后面缺少文件路径或 JSON。", "--settings= needs a file path or a JSON string."));
239
+ sources.push(value);
240
+ }
241
+ else {
242
+ rest.push(arg);
243
+ }
244
+ }
245
+ return { rest, sources };
246
+ }
247
+ /** 读一个 --settings 的值:以 `{` 开头按 JSON 字符串解析,否则当文件路径(相对 cwd)。必须是 JSON 对象。 */
248
+ export function loadCallerSettings(source, cwd = process.cwd()) {
249
+ const inline = source.trimStart().startsWith("{");
250
+ let text;
251
+ if (inline) {
252
+ text = source;
253
+ }
254
+ else {
255
+ const file = resolve(cwd, source);
256
+ try {
257
+ text = readFileSync(file, "utf8");
258
+ }
259
+ catch (e) {
260
+ const reason = e.code ?? e.message;
261
+ throw new ClaudeSettingsArgError(tr(`读不了 --settings 文件 ${file}(${reason})。`, `Couldn't read the --settings file ${file} (${reason}).`));
262
+ }
263
+ }
264
+ const what = inline ? tr("--settings 的 JSON", "The --settings JSON") : tr(`--settings 文件 ${resolve(cwd, source)}`, `The --settings file ${resolve(cwd, source)}`);
265
+ let parsed;
266
+ try {
267
+ parsed = JSON.parse(text);
268
+ }
269
+ catch (e) {
270
+ throw new ClaudeSettingsArgError(tr(`${what} 不是合法的 JSON:${e.message}`, `${what} is not valid JSON: ${e.message}`));
271
+ }
272
+ if (!isPlainObject(parsed))
273
+ throw new ClaudeSettingsArgError(tr(`${what} 必须是一个 JSON 对象。`, `${what} must be a JSON object.`));
274
+ return parsed;
275
+ }
276
+ function mergeArrays(base, extra, dedupe) {
277
+ if (!dedupe)
278
+ return [...base, ...extra];
279
+ const out = [...base];
280
+ const seen = new Set(out.map((item) => JSON.stringify(item)));
281
+ for (const item of extra) {
282
+ const key = JSON.stringify(item);
283
+ if (!seen.has(key)) {
284
+ seen.add(key);
285
+ out.push(item);
286
+ }
287
+ }
288
+ return out;
289
+ }
290
+ function mergeValue(base, extra, path) {
291
+ if (isPlainObject(base) && isPlainObject(extra)) {
292
+ const out = { ...base };
293
+ for (const [key, value] of Object.entries(extra)) {
294
+ if (key === "__proto__")
295
+ continue;
296
+ out[key] = Object.hasOwn(out, key) ? mergeValue(out[key], value, [...path, key]) : value;
297
+ }
298
+ return out;
299
+ }
300
+ // hooks.<事件> 按原样拼接(调用方的回调排在后面);其余数组(permissions.allow/ask/deny 等)拼接去重
301
+ if (Array.isArray(base) && Array.isArray(extra))
302
+ return mergeArrays(base, extra, !(path.length === 2 && path[0] === "hooks"));
303
+ return extra;
304
+ }
305
+ /**
306
+ * 把调用方的设置合并进启动器的:对象逐层合并;hooks 每个事件的数组拼接;其余数组拼接去重;
307
+ * 标量(含 model、statusLine 这类整项)以调用方为准。合并后调用方还要再过 sanitizeClaudeSettings 与
308
+ * withMcpPermissions,不能借此换凭证、换地址、改请求形状。
309
+ */
310
+ export function mergeClaudeSettings(base, caller) {
311
+ return mergeValue(base, caller, []);
312
+ }
313
+ /** 启动器的设置 + 调用方的设置(按顺序)→ 清洗 → MCP 权限:写进 --settings 文件的最终内容。 */
314
+ export function finalClaudeSettings(launcher, callers, withMcp) {
315
+ const merged = sanitizeClaudeSettings(callers.reduce(mergeClaudeSettings, launcher));
316
+ return withMcp ? withMcpPermissions(merged) : merged;
317
+ }
318
+ export function writeClaudeSettings(file, settings) {
319
+ writePrivateFile(file, JSON.stringify(settings, null, 2) + "\n");
320
+ }
321
+ /** 桌面端复用:同一份注入逻辑(embed.ts 导出)。 */
322
+ export function agentLaunchEnv(agent, input) {
323
+ if (agent !== "claude")
324
+ throw new Error(`unsupported agent: ${String(agent)}`);
325
+ return claudeLaunchEnv(input);
326
+ }
327
+ // ---------------------------------------------------------------- run
328
+ const PROCESS_SETTINGS = /^settings-(\d+)\.json$/;
329
+ /**
330
+ * 带调用方 --settings 时,合并结果写进这个进程自己的文件:Claude Code 会监视 --settings 文件、改了就重读,
331
+ * 几个进程共用一份的话,后起的会把先起的那个的 hooks(调用方按会话区分的回调)换成自己的。
332
+ * 没有调用方 --settings 时内容只取决于网关配置,仍用共用的 settingsFile。
333
+ */
334
+ export function processSettingsFile(paths, pid = process.pid) {
335
+ return join(paths.root, `settings-${pid}.json`);
336
+ }
337
+ function pidAlive(pid) {
338
+ try {
339
+ process.kill(pid, 0);
340
+ return true;
341
+ }
342
+ catch (e) {
343
+ return e.code === "EPERM";
344
+ }
345
+ }
346
+ /** 删掉上次没来得及清理(进程被强杀)的每进程设置文件;进程还在的不动。 */
347
+ export function cleanStaleSettingsFiles(root, alive = pidAlive) {
348
+ let names;
349
+ try {
350
+ names = readdirSync(root);
351
+ }
352
+ catch {
353
+ return;
354
+ }
355
+ for (const name of names) {
356
+ const m = PROCESS_SETTINGS.exec(name);
357
+ if (m && !alive(Number(m[1])))
358
+ rmSync(join(root, name), { force: true });
359
+ }
360
+ }
361
+ /**
362
+ * `u1s1 claude [参数…]`。返回子进程的退出码(启动前就失败返回 1)。
363
+ */
364
+ export async function runClaude(userArgs) {
365
+ // 调用方的 --settings 先读出来:有错在登录、联网之前就报。只查版本的快速路径原样不动。
366
+ let launchArgs = userArgs;
367
+ let callerSettings = [];
368
+ if (!isBareVersionQuery(userArgs)) {
369
+ try {
370
+ const { rest, sources } = extractSettingsArgs(userArgs);
371
+ launchArgs = rest;
372
+ callerSettings = sources.map((source) => loadCallerSettings(source));
373
+ }
374
+ catch (e) {
375
+ if (!(e instanceof ClaudeSettingsArgError))
376
+ throw e;
377
+ console.error(e.message);
378
+ return 1;
379
+ }
380
+ }
381
+ const paths = claudePaths();
382
+ const prepared = await prepareAgentLaunch(AGENT_PROFILES.claude, paths);
383
+ if (typeof prepared === "number")
384
+ return prepared;
385
+ const mcp = mcpInjectionDisabled()
386
+ ? undefined
387
+ : mcpLaunchEnv({ proxyBaseUrl: prepared.proxy.baseUrl, localToken: prepared.proxy.localKey, features: prepared.features, lang: lang() });
388
+ const settings = claudeSettingsFromConfig(prepared.config, prepared.model);
389
+ cleanStaleSettingsFiles(paths.root);
390
+ const settingsFile = callerSettings.length ? processSettingsFile(paths) : paths.settingsFile;
391
+ writeClaudeSettings(settingsFile, finalClaudeSettings(settings, callerSettings, !!mcp));
392
+ if (mcp)
393
+ writePrivateFile(paths.mcpConfigFile, JSON.stringify(claudeMcpConfig(), null, 2) + "\n");
394
+ else
395
+ rmSync(paths.mcpConfigFile, { force: true });
396
+ const env = claudeLaunchEnv({
397
+ baseEnv: process.env,
398
+ proxyOrigin: new URL(prepared.proxy.baseUrl).origin,
399
+ localToken: prepared.proxy.localKey,
400
+ configDir: paths.configDir,
401
+ mcp,
402
+ });
403
+ const args = claudeLaunchArgs(settingsFile, launchArgs, mcp ? paths.mcpConfigFile : undefined);
404
+ try {
405
+ return await runPreparedAgent({ agent: "claude", paths, prepared, args, env, userArgs });
406
+ }
407
+ finally {
408
+ if (settingsFile !== paths.settingsFile)
409
+ rmSync(settingsFile, { force: true });
410
+ }
411
+ }
@@ -0,0 +1,111 @@
1
+ import { type McpLaunchEnv } from "../mcp-server.js";
2
+ import type { AgentConfig } from "./agent-config.js";
3
+ import { type RuntimePaths } from "./runtime.js";
4
+ export declare const CODEX_GATEWAY_AGENT: "codex";
5
+ /** config.toml 里我们的 provider id(不能用 openai、ollama 等内置保留名)。 */
6
+ export declare const CODEX_PROVIDER_ID = "u1s1";
7
+ /** provider 的 env_key:子进程环境里放本地 token 的变量名。 */
8
+ export declare const CODEX_KEY_ENV = "U1S1_LOCAL_KEY";
9
+ export interface CodexPaths extends RuntimePaths {
10
+ /** CODEX_HOME */
11
+ configDir: string;
12
+ /** CODEX_HOME/config.toml */
13
+ configToml: string;
14
+ /** 网关下发的模型目录,原样写成文件给 model_catalog_json 用 */
15
+ catalogFile: string;
16
+ /** 上次成功拉到的 /v1/agents/codex/config */
17
+ configCache: string;
18
+ }
19
+ export declare function codexPaths(root?: string): CodexPaths;
20
+ /**
21
+ * 子进程环境里要去掉的变量(依据 Codex 0.167.2 源码):
22
+ * - 会换掉配置/状态目录的:CODEX_HOME(我们自己设)、CODEX_SQLITE_HOME(状态库改放别处,
23
+ * 可能是用户自己的目录);
24
+ * - 别的凭证:OPENAI_API_KEY、CODEX_API_KEY(`codex exec` 读它当 API Key 登录)、CODEX_ACCESS_TOKEN;
25
+ * 我们的 provider 只用 env_key,但有了它们 Codex 会以为已登录官方账号,去做相应的事;
26
+ * - 改官方 provider 地址的 OPENAI_BASE_URL(老版本读,0.167.2 只在它的网络代理里用);
27
+ * - 改请求形状的:CODEX_INTERNAL_ORIGINATOR_OVERRIDE 会换掉 originator 头和 User-Agent 前缀,
28
+ * 网关的客户端判定看这两样;
29
+ * - 「由某包管理器管理」的标记(CODEX_MANAGED_BY_*、CODEX_MANAGED_PACKAGE_ROOT):会让它提示用
30
+ * npm/bun 等升级,更新由启动器管。
31
+ * 只影响这一次启动的子进程环境,用户自己的 shell 与配置不动。
32
+ */
33
+ export declare const CODEX_ENV_DROP: readonly string[];
34
+ export interface CodexLaunchInput {
35
+ baseEnv: NodeJS.ProcessEnv;
36
+ /** 签名代理的一次性本地 token */
37
+ localToken: string;
38
+ /** CODEX_HOME(须已存在,否则 Codex 报错退出) */
39
+ home: string;
40
+ /** u1s1 工具服务要的变量(mcpLaunchEnv);不注入时省略 */
41
+ mcp?: McpLaunchEnv;
42
+ }
43
+ /**
44
+ * 本机代理地址不走用户的 HTTP(S)_PROXY:0.167.2 实测,设了 HTTP_PROXY / HTTPS_PROXY 又没有 NO_PROXY 时,
45
+ * Codex 把发往 http://127.0.0.1:<端口>/v1/responses 的请求也交给那个代理(连同本地 token),
46
+ * 代理不在本机就直接失败。用户原有的 NO_PROXY 条目保留,只补上 127.0.0.1 和 localhost。
47
+ */
48
+ export declare function withLoopbackNoProxy(value: string | undefined): string;
49
+ export declare function codexLaunchEnv(input: CodexLaunchInput): NodeJS.ProcessEnv;
50
+ /**
51
+ * 启动器的 -c 覆盖放在用户参数前面:用户自己传的 -c / -m / --profile 照样生效(同名 -c 以后出现的为准)。
52
+ * @param proxyOrigin 签名代理的 origin,形如 http://127.0.0.1:12345(不带 /v1)
53
+ */
54
+ export declare function codexLaunchArgs(proxyOrigin: string, userArgs: readonly string[]): string[];
55
+ /**
56
+ * 启动器管的顶层键:每次启动重写;不在这里的键(包括 Codex 自己写的项目信任)原样保留。
57
+ * model 每次都写成「用户在 u1s1 config 里定的模型(仍在阵容里时),否则网关默认」:Codex 里用 /model 换的
58
+ * 模型只管那一次会话,和 u1s1 claude 一致(那边 --settings 的 model 也盖过客户端里 /model 的选择)。
59
+ */
60
+ export declare const CODEX_MANAGED_KEYS: readonly ["model", "model_provider", "check_for_update_on_startup", "cli_auth_credentials_store", "model_catalog_json"];
61
+ /**
62
+ * 跟着模型走的顶层键:Codex 的 /model 把它们和 model 一起写进 config.toml。启动器把 model 换掉时
63
+ * 一并去掉(回到新模型自己的默认),免得为别的模型选的思考强度带到新模型上;model 没变就保留。
64
+ */
65
+ export declare const CODEX_MODEL_BOUND_KEYS: readonly ["model_reasoning_effort"];
66
+ export interface CodexManagedSettings {
67
+ model: string | null;
68
+ /** model_catalog_json 的绝对路径;null = 不写这一项 */
69
+ catalogFile: string | null;
70
+ /** u1s1 工具服务怎么拉起;null / 省略 = 不注入(原有的 [mcp_servers.u1s1] 也去掉) */
71
+ mcp?: CodexMcpServer | null;
72
+ }
73
+ export interface CodexMcpServer {
74
+ command: string;
75
+ args: string[];
76
+ /** 从 Codex 环境转给服务的变量名(Codex 默认只转 HOME、PATH 等少数几个) */
77
+ envVars: string[];
78
+ }
79
+ export interface RenderedCodexConfig {
80
+ text: string;
81
+ /** 原文件里有没法逐行安全改写的写法:原文件要先备份,再整份重写 */
82
+ replaced: boolean;
83
+ }
84
+ /**
85
+ * 生成新的 config.toml。保守做法:不解析整份 TOML,只逐行处理我们管的东西——
86
+ * - 顶层(第一个表头之前)的受管键整行去掉,换成新值,放在文件最前面;
87
+ * - `[model_providers.u1s1]`、`[mcp_servers.u1s1]` 及其子表整段去掉,换成新的放在最后
88
+ * (例外:用户对 u1s1 工具的审批选择 `[mcp_servers.u1s1.tools.*]`,注入时跟在新表后面保留);
89
+ * - 其余每一行(用户加的键、别的表、注释、Codex 自己写的项目信任等)原样保留,顺序不变。
90
+ * 遇到没法逐行安全改写的写法(受管键是跨行值、在顶层或 [model_providers] 表里用点号/内联表写我们的
91
+ * provider)就整份重写,由调用方先把原文件备份。
92
+ */
93
+ export declare function renderCodexConfig(existing: string | undefined, managed: CodexManagedSettings): RenderedCodexConfig;
94
+ /** 网关 settings.model_catalog:必须是对象才写;CLI 不解析、不改它的内容。 */
95
+ export declare function modelCatalogFromConfig(config: AgentConfig): Record<string, unknown> | undefined;
96
+ /**
97
+ * 写 model-catalog.json 与 config.toml。返回原 config.toml 被整份重写时的备份路径(给用户一句说明)。
98
+ */
99
+ export declare function writeCodexConfig(paths: CodexPaths, config: AgentConfig, now?: () => number, mcp?: CodexMcpServer | null,
100
+ /** 这次启动用的模型(launch.ts 按 u1s1 config 选好);undefined = 网关默认 */
101
+ model?: string | null): string | undefined;
102
+ /** [mcp_servers.u1s1] 的内容:同一个 node、同一份 CLI;只列要转发的变量名。 */
103
+ export declare function codexMcpServer(command?: {
104
+ command: string;
105
+ args: string[];
106
+ }, env?: NodeJS.ProcessEnv): CodexMcpServer;
107
+ /**
108
+ * `u1s1 codex [参数…]`。返回子进程的退出码(启动前就失败返回 1)。
109
+ * 是否可用完全由网关配置的 enabled 决定(prepareAgentLaunch)。
110
+ */
111
+ export declare function runCodex(userArgs: string[]): Promise<number>;