u1s1-cli 1.14.5 → 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 (49) 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/api.js +2 -1
  20. package/dist/config-command.d.ts +18 -0
  21. package/dist/config-command.js +335 -0
  22. package/dist/config.d.ts +17 -0
  23. package/dist/config.js +35 -1
  24. package/dist/device-auth.d.ts +39 -17
  25. package/dist/device-auth.js +61 -72
  26. package/dist/embed.d.ts +13 -0
  27. package/dist/embed.js +15 -0
  28. package/dist/gateway-route.d.ts +57 -0
  29. package/dist/gateway-route.js +157 -0
  30. package/dist/index.js +22 -556
  31. package/dist/launcher.d.ts +48 -0
  32. package/dist/launcher.js +235 -0
  33. package/dist/login.js +3 -2
  34. package/dist/mcp-server.d.ts +105 -0
  35. package/dist/mcp-server.js +449 -0
  36. package/dist/pi-ui-text.d.ts +57 -0
  37. package/dist/pi-ui-text.js +148 -0
  38. package/dist/search-tools.js +2 -1
  39. package/dist/usage.js +2 -2
  40. package/package.json +3 -2
  41. package/scripts/p0-claude-code/README.md +39 -0
  42. package/scripts/p0-claude-code/launch-claude.ts +94 -0
  43. package/scripts/p0-claude-code/replay.ts +74 -0
  44. package/scripts/p0-claude-code/seed-local.mjs +145 -0
  45. package/scripts/p0-claude-code/tap.mjs +105 -0
  46. package/scripts/patch-pi.js +76 -55
  47. package/scripts/pi-patch-report.mjs +224 -0
  48. package/scripts/pi-upgrade.mjs +829 -0
  49. package/scripts/render-regression.mjs +67 -5
@@ -0,0 +1,444 @@
1
+ /**
2
+ * 官方原版客户端(`u1s1 claude` / `u1s1 codex`)共用的启动流程与文案:
3
+ * 登录 → 经签名代理拉网关配置(开关、刹车、阵容)→ 找到/下载原版 → 起声明了客户端的签名代理 →
4
+ * 由各客户端模块写配置、拼环境 → 子进程继承终端 → 透传退出码;顺带每天查一次新版。
5
+ *
6
+ * 各客户端只提供 AgentProfile(名字、条款、路径)和「怎么注入」,下载、刹车、文案都在这里一份。
7
+ */
8
+ import { spawn } from "node:child_process";
9
+ import { mkdirSync, readdirSync, statSync } from "node:fs";
10
+ import { constants as osConstants } from "node:os";
11
+ import { join } from "node:path";
12
+ import { createInterface } from "node:readline/promises";
13
+ import { Agent, fetch as undiciFetch } from "undici";
14
+ import { AccessDeniedError, AuthError, fetchModels } from "../api.js";
15
+ import { u1s1Dir } from "../config.js";
16
+ import { ensureSigningProxy } from "../device-auth.js";
17
+ import { tr } from "../i18n.js";
18
+ import { siteLinks, siteOf } from "../site.js";
19
+ import { loadAgentConfig } from "./agent-config.js";
20
+ import { readAgentPrefs, resolveLaunchModel } from "./prefs.js";
21
+ import { dailyUpdateCheck, ensureAgentRuntime, fetchLatestVersion, npmRegistryFor, RuntimeInstallError, RuntimePlanError, runtimeSpec, } from "./runtime.js";
22
+ export function agentsRoot() {
23
+ return join(u1s1Dir, "agents");
24
+ }
25
+ export const CLAUDE_TERMS_URL = "https://code.claude.com/docs/en/legal-and-compliance";
26
+ export const CODEX_LICENSE_URL = "https://github.com/openai/codex/blob/main/LICENSE";
27
+ export const AGENT_PROFILES = {
28
+ claude: {
29
+ agent: "claude",
30
+ gatewayAgent: "claude-code",
31
+ product: "Claude Code",
32
+ termsLine: () => tr(` Claude Code 的使用受 Anthropic 的条款约束:${CLAUDE_TERMS_URL}`, ` Use of Claude Code is subject to Anthropic's terms: ${CLAUDE_TERMS_URL}`),
33
+ locationLine: () => tr(" 位置:~/.u1s1/agents/claude(不影响你自己装的版本和 ~/.claude)", " Location: ~/.u1s1/agents/claude (your own install and ~/.claude are left alone)"),
34
+ },
35
+ codex: {
36
+ agent: "codex",
37
+ gatewayAgent: "codex",
38
+ product: "Codex CLI",
39
+ termsLine: () => tr(` Codex CLI 以 Apache-2.0 许可发布:${CODEX_LICENSE_URL}`, ` Codex CLI is released under the Apache-2.0 license: ${CODEX_LICENSE_URL}`),
40
+ locationLine: () => tr(" 位置:~/.u1s1/agents/codex(不影响你自己装的版本和 ~/.codex)", " Location: ~/.u1s1/agents/codex (your own install and ~/.codex are left alone)"),
41
+ },
42
+ };
43
+ // ---------------------------------------------------------------- loopback
44
+ let loopbackAgent;
45
+ /**
46
+ * 请求本机签名代理(http://127.0.0.1:<端口>)专用:直连,不走用户的 HTTP(S)_PROXY。
47
+ * 全局 dispatcher 认 HTTP_PROXY,用户没在 NO_PROXY 里写 127.0.0.1 时,连本机代理的请求也会交给
48
+ * 那个代理;远端代理连不到用户本机,首次启动(还没有配置缓存)就报「拿不到启动配置」。
49
+ * 签名代理到网关那一跳不受影响,照旧按用户的代理设置走。
50
+ */
51
+ export function loopbackFetch(url, init) {
52
+ const host = new URL(url).hostname;
53
+ if (host !== "127.0.0.1" && host !== "localhost" && host !== "[::1]") {
54
+ throw new Error(`loopbackFetch is only for the local signing proxy: ${url}`);
55
+ }
56
+ loopbackAgent ??= new Agent();
57
+ return undiciFetch(url, { ...init, dispatcher: loopbackAgent });
58
+ }
59
+ // ---------------------------------------------------------------- child process
60
+ /**
61
+ * 起子进程、继承终端、等它退出、透传退出码。
62
+ *
63
+ * 信号:Ctrl+C / Ctrl+\ 由终端直接发给整个前台进程组,子进程自己会收到;启动器只要不被它们
64
+ * 打死就行,不再转发(转发会让子进程收到两次,交互界面里「按两次 Ctrl+C 退出」就成了按一次)。
65
+ * SIGTERM / SIGHUP 通常只发给启动器(kill、关窗口),转发给子进程。
66
+ * 签名代理的监听是 unref 的:必须等子进程退出后再返回。
67
+ */
68
+ export function spawnAgent(bin, args, env, deps = {}) {
69
+ const proc = deps.proc ?? process;
70
+ const child = (deps.spawn ?? spawn)(bin, [...args], { env, stdio: "inherit" });
71
+ const swallow = () => { };
72
+ const forwarders = new Map();
73
+ for (const sig of ["SIGTERM", "SIGHUP"]) {
74
+ forwarders.set(sig, () => {
75
+ if (child.exitCode === null && child.signalCode === null)
76
+ child.kill(sig);
77
+ });
78
+ }
79
+ const ignored = process.platform === "win32" ? ["SIGINT"] : ["SIGINT", "SIGQUIT"];
80
+ for (const sig of ignored)
81
+ proc.on(sig, swallow);
82
+ for (const [sig, fn] of forwarders)
83
+ proc.on(sig, fn);
84
+ const cleanup = () => {
85
+ for (const sig of ignored)
86
+ proc.off(sig, swallow);
87
+ for (const [sig, fn] of forwarders)
88
+ proc.off(sig, fn);
89
+ };
90
+ return new Promise((resolve) => {
91
+ let settled = false;
92
+ const done = (code) => {
93
+ if (settled)
94
+ return;
95
+ settled = true;
96
+ cleanup();
97
+ resolve(code);
98
+ };
99
+ child.once("error", (e) => {
100
+ console.error(tr(`无法启动 ${bin}:${e.message}`, `Couldn't start ${bin}: ${e.message}`));
101
+ done(127);
102
+ });
103
+ child.once("exit", (code, signal) => {
104
+ if (code !== null)
105
+ return done(code);
106
+ const num = signal ? osConstants.signals[signal] : undefined;
107
+ done(128 + (num ?? 1));
108
+ });
109
+ });
110
+ }
111
+ // ---------------------------------------------------------------- CLI hooks (文案)
112
+ function mb(bytes) {
113
+ return (bytes / 1024 / 1024).toFixed(bytes >= 100 * 1024 * 1024 ? 0 : 1);
114
+ }
115
+ export function consentLines(info) {
116
+ const profile = AGENT_PROFILES[info.agent];
117
+ const host = new URL(info.registry).host;
118
+ // 措辞按实际下载的源定(中国站默认镜像;U1S1_NPM_REGISTRY 可能改成官方源或别的镜像)
119
+ const source = host === "registry.npmjs.org"
120
+ ? tr(`官方 npm 包 ${info.npmPackage}(${host})`, `the official npm package ${info.npmPackage} (${host})`)
121
+ : tr(`官方 npm 包 ${info.npmPackage}(经 npm 镜像 ${host} 下载,同一份包,带完整性校验)`, `the official npm package ${info.npmPackage} (via the npm mirror ${host}; same package, integrity-checked)`);
122
+ const size = info.downloadBytes !== undefined || info.unpackedBytes !== undefined
123
+ ? [
124
+ info.downloadBytes !== undefined ? tr(`下载约 ${mb(info.downloadBytes)} MB`, `about ${mb(info.downloadBytes)} MB to download`) : undefined,
125
+ info.unpackedBytes !== undefined ? tr(`安装后约 ${mb(info.unpackedBytes)} MB`, `about ${mb(info.unpackedBytes)} MB installed`) : undefined,
126
+ ].filter(Boolean).join(tr(",", ", "))
127
+ : tr("几百 MB(暂时拿不到确切大小)", "a few hundred MB (exact size unavailable)");
128
+ return [
129
+ "",
130
+ tr(` 首次使用需要下载官方 ${profile.product} ${info.version}:`, ` The official ${profile.product} ${info.version} needs to be downloaded first:`),
131
+ tr(` 来源:${source}`, ` Source: ${source}`),
132
+ tr(` 大小:${size}`, ` Size: ${size}`),
133
+ profile.locationLine(),
134
+ profile.termsLine(),
135
+ ];
136
+ }
137
+ async function askConsent(info) {
138
+ for (const line of consentLines(info))
139
+ console.error(line);
140
+ const preset = process.env["U1S1_AGENT_INSTALL"]?.trim().toLowerCase();
141
+ if (preset === "yes" || preset === "y" || preset === "1")
142
+ return true;
143
+ if (!process.stdin.isTTY) {
144
+ console.error(tr(` 当前没有可交互的终端。请在终端里运行一次 u1s1 ${info.agent} 确认下载,或设置 U1S1_AGENT_INSTALL=yes。`, ` No interactive terminal. Run u1s1 ${info.agent} in a terminal once to confirm, or set U1S1_AGENT_INSTALL=yes.`));
145
+ return false;
146
+ }
147
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
148
+ try {
149
+ const answer = (await rl.question(tr(" 现在下载吗?[Y/n] ", " Download now? [Y/n] "))).trim().toLowerCase();
150
+ return answer === "" || answer === "y" || answer === "yes" || answer === "是";
151
+ }
152
+ finally {
153
+ rl.close();
154
+ }
155
+ }
156
+ export function cliInstallHooks(agent = "claude") {
157
+ let progressShown = false;
158
+ const endProgress = () => {
159
+ if (progressShown)
160
+ process.stderr.write("\n");
161
+ progressShown = false;
162
+ };
163
+ return {
164
+ consent: (info) => askConsent(info),
165
+ log: (line) => {
166
+ endProgress();
167
+ console.error(line);
168
+ },
169
+ progress: process.stderr.isTTY
170
+ ? (done, total) => {
171
+ progressShown = true;
172
+ const text = total
173
+ ? tr(` 已下载 ${mb(Math.min(done, total))} / 约 ${mb(total)} MB`, ` Downloaded ${mb(Math.min(done, total))} / ~${mb(total)} MB`)
174
+ : tr(` 已下载 ${mb(done)} MB`, ` Downloaded ${mb(done)} MB`);
175
+ process.stderr.write(`\r${text}\x1b[K`);
176
+ }
177
+ : undefined,
178
+ text: {
179
+ noNpm: () => tr(`需要 npm 才能下载,但这台机器上没找到。请安装 Node.js 22 或更新版本(自带 npm,https://nodejs.org),或重新运行官网安装脚本后再试。`, `npm is required for the download but wasn't found. Install Node.js 22 or newer (includes npm, https://nodejs.org), or rerun the official install script, then try again.`),
180
+ npmFailed: (attempt, max) => tr(` 下载失败(第 ${attempt}/${max} 次)`, ` Download failed (attempt ${attempt}/${max})`),
181
+ retrying: (s) => tr(` ${s} 秒后重试…`, ` Retrying in ${s}s…`),
182
+ verifyFailed: (detail) => tr(`下载的文件没通过校验(${detail}),已丢弃。`, `The download failed verification (${detail}) and was discarded.`),
183
+ declined: () => tr("已取消下载。", "Download cancelled."),
184
+ unsupported: () => tr(`这台机器的系统/架构(${process.platform}-${process.arch})没有官方版本。`, `There's no official build for this system (${process.platform}-${process.arch}).`),
185
+ installing: (v) => tr(` 正在下载并安装 ${v} …`, ` Downloading and installing ${v}…`),
186
+ installed: (v) => tr(` 已安装 ${v}`, ` Installed ${v}`),
187
+ overMaxTested: (s) => overMaxTestedText(s, agent),
188
+ },
189
+ };
190
+ }
191
+ /** 已有的某份客户端高于 max_tested、这次不用它。 */
192
+ export function overMaxTestedText(s, agent = "claude") {
193
+ const max = s.maxTestedVersion;
194
+ const binEnv = runtimeSpec(agent).binEnv;
195
+ if (s.source === "env") {
196
+ return tr(` ${binEnv} 指向的 ${s.bin} 是 ${s.version},高于目前验证过的最高版本 ${max}。不改动它,这次改用 u1s1 管理的、不超过 ${max} 的版本。`, ` ${s.bin} from ${binEnv} is ${s.version}, newer than the highest version verified so far (${max}). It's left as is; this run uses a u1s1-managed copy no newer than ${max} instead.`);
197
+ }
198
+ if (s.source === "path") {
199
+ return tr(` 本机已有的 ${s.bin} 是 ${s.version},高于目前验证过的最高版本 ${max}。不改动它,由 u1s1 另装一份不超过 ${max} 的版本来用。`, ` Your existing ${s.bin} is ${s.version}, newer than the highest version verified so far (${max}). It's left as is; u1s1 uses its own copy no newer than ${max} instead.`);
200
+ }
201
+ return tr(` u1s1 装的 ${s.version} 高于目前验证过的最高版本 ${max},换到不超过 ${max} 的版本。`, ` The u1s1-managed ${s.version} is newer than the highest version verified so far (${max}); switching to a version no newer than ${max}.`);
202
+ }
203
+ /** ensureAgentRuntime 抛出的错误 → 给用户的几行说明。不认识的错误返回 false(调用方再抛)。 */
204
+ export function printRuntimeFailure(e, agent = "claude") {
205
+ if (e instanceof RuntimePlanError) {
206
+ console.error(runtimeErrorText(e, agent));
207
+ return true;
208
+ }
209
+ if (!(e instanceof RuntimeInstallError))
210
+ return false;
211
+ console.error(e.message);
212
+ if (e.detail)
213
+ console.error(e.detail);
214
+ const max = e.skipped?.[0]?.maxTestedVersion;
215
+ if (max) {
216
+ console.error(tr(`没换成不超过 ${max} 的版本。高于 ${max} 的版本还没验证过能和 u1s1 正常配合,不会拿来用;稍后重新运行即可再试。`, `Couldn't switch to a version no newer than ${max}. Versions above ${max} haven't been verified to work with u1s1 yet, so they won't be used; run this again later to retry.`));
217
+ }
218
+ return true;
219
+ }
220
+ export function notOpenText() {
221
+ return tr("这个客户端暂未开放。现在可以用 u1s1 pi(u1s1 内置的编程助手)。", "This client isn't available yet. You can use u1s1 pi (u1s1's built-in coding assistant) for now.");
222
+ }
223
+ export function runtimeErrorText(e, agent = "claude") {
224
+ const p = e.plan;
225
+ const binEnv = runtimeSpec(agent).binEnv;
226
+ if (p.code === "env_bin_unusable") {
227
+ return tr(`${binEnv} 指向的 ${p.bin} 无法运行(取不到版本号)。`, `${binEnv} points to ${p.bin}, which doesn't run (no version reported).`);
228
+ }
229
+ if (p.code === "update_required") {
230
+ const why = p.verdict === "blocked"
231
+ ? tr("已被停用", "has been disabled")
232
+ : p.verdict === "below_min"
233
+ ? tr("版本太旧", "is too old")
234
+ : tr("高于目前验证过的最高版本", "is newer than the highest version verified so far");
235
+ const product = AGENT_PROFILES[agent].product;
236
+ return tr(`${product} ${p.version} ${why},要换到可用的版本才能启动。自动更新已关闭:运行 u1s1 agents install ${agent} 手动更新,`
237
+ + `或 u1s1 config set ${agent}.auto-update on 打开自动更新。`, `${product} ${p.version} ${why} and has to be replaced with an allowed version before it can start. Automatic updates are off: `
238
+ + `run u1s1 agents install ${agent} to update now, or u1s1 config set ${agent}.auto-update on to turn them back on.`);
239
+ }
240
+ if (p.code === "env_bin_brake") {
241
+ return p.verdict === "blocked"
242
+ ? tr(`${binEnv} 指向的版本 ${p.version} 已被停用,请换一个版本。`, `Version ${p.version} (${binEnv}) has been disabled; use a different version.`)
243
+ : tr(`${binEnv} 指向的版本 ${p.version} 太旧,请升级。`, `Version ${p.version} (${binEnv}) is too old; please upgrade.`);
244
+ }
245
+ return tr("暂时拿不到可安装的版本(可能连不上 npm 源),请稍后再试。", "No installable version is available right now (the npm registry may be unreachable). Please try again later.");
246
+ }
247
+ /** 运行时的两条附带提示:pin 被忽略、更新没装上。 */
248
+ export function printRuntimeNotes(runtime, agent) {
249
+ if (runtime.pinIgnored) {
250
+ console.error(tr(` 锁定的版本 ${runtime.pinIgnored} 不在允许范围内,已忽略(u1s1 agents unpin ${agent} 可解除)。`, ` The pinned version ${runtime.pinIgnored} isn't allowed right now and was ignored (u1s1 agents unpin ${agent} to clear it).`));
251
+ }
252
+ if (runtime.installError) {
253
+ console.error(tr(` 更新没装上,继续使用 ${runtime.version}。`, ` The update didn't install; continuing with ${runtime.version}.`));
254
+ }
255
+ }
256
+ // ---------------------------------------------------------------- orchestration
257
+ function attestationSource(cfg, models) {
258
+ return {
259
+ token: models?.clientAttestation,
260
+ expiresInSeconds: models?.clientAttestationExpiresInSeconds,
261
+ refresh: async () => {
262
+ const refreshed = await fetchModels(cfg);
263
+ return { token: refreshed.clientAttestation, expiresInSeconds: refreshed.clientAttestationExpiresInSeconds };
264
+ },
265
+ };
266
+ }
267
+ /**
268
+ * 子进程启动前的公共部分。返回 number = 启动前就失败的退出码(已向用户说明)。
269
+ * 是否可用完全由网关配置的 enabled 决定:停用时显示 disabled_reason,不下载、不启动。
270
+ */
271
+ export async function prepareAgentLaunch(profile, paths) {
272
+ const { ensureAuth, login } = await import("../login.js");
273
+ let cfg = await ensureAuth();
274
+ const site = siteOf(cfg);
275
+ // 先拉一次 /v1/models:拿客户端证明(签名代理附在每个请求上),顺带确认登录有效
276
+ let models;
277
+ try {
278
+ models = await fetchModels(cfg);
279
+ }
280
+ catch (e) {
281
+ if (e instanceof AuthError) {
282
+ console.error(tr(" 登录已失效(这台设备可能被移除,或账号更换过钥匙),需要重新登录。", " Your session has expired (this device may have been removed, or the account's keys changed). Please sign in again."));
283
+ cfg = await login();
284
+ models = await fetchModels(cfg).catch(() => undefined);
285
+ }
286
+ else if (e instanceof AccessDeniedError) {
287
+ console.error(` ${e.message}`);
288
+ console.error(tr(` 这个账号当前无法使用 u1s1。如有疑问可发邮件到 ${site.contactEmail},或在 ${siteLinks(site).dashboard} 提交工单。`, ` This account can't use u1s1 right now. Questions? Email ${site.contactEmail} or open a support ticket at ${siteLinks(site).dashboard}.`));
289
+ return 1;
290
+ }
291
+ // 其他(多半是网络):往下走,配置可以用缓存,真连不上时子进程自己会报错
292
+ }
293
+ const attestation = attestationSource(cfg, models);
294
+ // 配置经签名代理拉(与客户端请求同一条签名通路)
295
+ const configProxy = await ensureSigningProxy(cfg, "terminal", attestation);
296
+ const result = await loadAgentConfig({
297
+ agent: profile.gatewayAgent,
298
+ cacheFile: paths.configCache,
299
+ site: new URL(cfg.baseUrl).origin,
300
+ fetchConfig: () => loopbackFetch(`${configProxy.baseUrl}/agents/${profile.gatewayAgent}/config`, {
301
+ headers: { authorization: `Bearer ${configProxy.localKey}`, accept: "application/json" },
302
+ signal: AbortSignal.timeout(15_000),
303
+ }),
304
+ });
305
+ if (result.status === "unavailable") {
306
+ console.error(notOpenText());
307
+ return 1;
308
+ }
309
+ if (result.status === "disabled") {
310
+ console.error(result.reason ?? notOpenText());
311
+ return 1;
312
+ }
313
+ if (result.status === "auth") {
314
+ console.error(tr("登录已失效,请运行 u1s1 login 重新登录。", "Your session has expired. Run u1s1 login to sign in again."));
315
+ return 1;
316
+ }
317
+ if (result.status === "error") {
318
+ console.error(tr(`拿不到启动配置(${result.detail}),检查一下网络后再试。`, `Couldn't fetch the launch configuration (${result.detail}). Check your network and try again.`));
319
+ return 1;
320
+ }
321
+ const config = result.config;
322
+ const prefs = readAgentPrefs(profile.agent);
323
+ const model = resolveLaunchModel(profile.agent, config, {
324
+ prefs,
325
+ notify: (missing, fallback) => console.error(modelMissingText(profile.agent, missing, fallback)),
326
+ });
327
+ mkdirSync(paths.configDir, { recursive: true, mode: 0o700 });
328
+ let runtime;
329
+ try {
330
+ runtime = await ensureAgentRuntime(profile.agent, {
331
+ brake: config.runtime,
332
+ site: site.id,
333
+ agentsRoot: agentsRoot(),
334
+ hooks: cliInstallHooks(profile.agent),
335
+ probeConfigDir: paths.configDir,
336
+ autoUpdate: prefs.autoUpdate,
337
+ });
338
+ }
339
+ catch (e) {
340
+ if (!printRuntimeFailure(e, profile.agent))
341
+ throw e;
342
+ return 1;
343
+ }
344
+ printRuntimeNotes(runtime, profile.agent);
345
+ // 子进程用的代理:带上客户端名与版本,网关据此核对请求形状
346
+ const proxy = await ensureSigningProxy(cfg, "terminal", attestation, { name: profile.gatewayAgent, version: runtime.version });
347
+ return { config, model, autoUpdate: prefs.autoUpdate, runtime, proxy, site: site.id, features: models?.features };
348
+ }
349
+ /** 用户在 u1s1 config 里选的模型不在当前阵容里(只提示一次,见 prefs.ts)。 */
350
+ export function modelMissingText(agent, missing, fallback) {
351
+ const using = fallback ? tr(`这次用默认模型 ${fallback}`, `using the default model ${fallback} instead`) : tr("这次用默认模型", "using the default model instead");
352
+ return tr(` 你为 ${agent} 设的默认模型 ${missing} 现在不可用,${using}。换一个:u1s1 config set ${agent}.model <模型>`, ` The default model you set for ${agent}, ${missing}, isn't available right now; ${using}. To pick another: u1s1 config set ${agent}.model <model>`);
353
+ }
354
+ // ---------------------------------------------------------------- resume hint
355
+ const UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
356
+ /**
357
+ * 退出时客户端自己打印「用 codex resume <id> / claude --resume <id> 继续」,但用户要经 u1s1 启动才连得上
358
+ * 网关。这里在它下面补一行带 u1s1 的写法。会话 id 取这次运行期间写过的最新会话文件(客户端自己的
359
+ * 配置目录下,启动器专用,不是用户的 ~/.codex、~/.claude):
360
+ * codex CODEX_HOME/sessions/<年>/<月>/<日>/rollout-<时间>-<id>.jsonl
361
+ * claude CLAUDE_CONFIG_DIR/projects/<目录名>/<id>.jsonl
362
+ */
363
+ const SESSION_LAYOUT = {
364
+ codex: { dir: "sessions", file: new RegExp(`^rollout-.*-(${UUID})\\.jsonl$`), depth: 4 },
365
+ claude: { dir: "projects", file: new RegExp(`^(${UUID})\\.jsonl$`), depth: 2 },
366
+ };
367
+ /** configDir 下这次运行(since 之后)写过的最新会话的 id。 */
368
+ export function latestSessionId(agent, configDir, since) {
369
+ const layout = SESSION_LAYOUT[agent];
370
+ let best;
371
+ const walk = (dir, depth) => {
372
+ let names;
373
+ try {
374
+ names = readdirSync(dir);
375
+ }
376
+ catch {
377
+ return;
378
+ }
379
+ for (const name of names) {
380
+ const path = join(dir, name);
381
+ let st;
382
+ try {
383
+ st = statSync(path);
384
+ }
385
+ catch {
386
+ continue;
387
+ }
388
+ if (st.isDirectory()) {
389
+ if (depth > 1)
390
+ walk(path, depth - 1);
391
+ continue;
392
+ }
393
+ const m = layout.file.exec(name);
394
+ if (m && st.mtimeMs >= since && (!best || st.mtimeMs > best.mtime))
395
+ best = { id: m[1], mtime: st.mtimeMs };
396
+ }
397
+ };
398
+ walk(join(configDir, layout.dir), layout.depth);
399
+ return best?.id;
400
+ }
401
+ /**
402
+ * 只在交互界面正常退出后提示:脚本调用(codex exec、claude -p)客户端自己也不打印续接提示,
403
+ * 我们也不加。
404
+ */
405
+ export function wantsResumeHint(agent, userArgs, interactive) {
406
+ if (!interactive)
407
+ return false;
408
+ if (agent === "codex")
409
+ return !["exec", "e", "review", "login", "logout", "mcp", "mcp-server", "app-server", "completion", "apply", "a", "cloud", "sandbox", "debug", "features"].includes(userArgs[0] ?? "")
410
+ && !userArgs.some((a) => a === "--help" || a === "-h" || a === "--version" || a === "-V");
411
+ return !userArgs.some((a) => a === "-p" || a === "--print" || a === "--help" || a === "-h" || a === "--version" || a === "-v");
412
+ }
413
+ export function resumeHintText(agent, id) {
414
+ const command = agent === "codex" ? `u1s1 codex resume ${id}` : `u1s1 claude --resume ${id}`;
415
+ return tr(`经 u1s1 继续这次会话:${command}`, `To continue this session through u1s1, run: ${command}`);
416
+ }
417
+ /**
418
+ * 起子进程并等它退出;同时每天最多查一次新版(只管启动器自己装的那份),下次启动前更新,不挡启动。
419
+ * 用户关了自动更新(u1s1 config)就不查。
420
+ */
421
+ export async function runPreparedAgent(input) {
422
+ const { prepared } = input;
423
+ const spec = runtimeSpec(input.agent);
424
+ const registry = npmRegistryFor(prepared.site);
425
+ const check = prepared.runtime.source === "managed" && prepared.autoUpdate
426
+ ? dailyUpdateCheck({
427
+ paths: input.paths,
428
+ brake: prepared.config.runtime,
429
+ now: Date.now(),
430
+ latest: () => fetchLatestVersion(registry, spec.npmPackage),
431
+ }).catch(() => undefined)
432
+ : Promise.resolve(undefined);
433
+ const startedAt = Date.now();
434
+ const code = await spawnAgent(prepared.runtime.bin, input.args, input.env);
435
+ if (code === 0 && input.paths.configDir && input.userArgs
436
+ && wantsResumeHint(input.agent, input.userArgs, !!process.stdin.isTTY && !!process.stdout.isTTY)) {
437
+ // 文件时间的精度与时钟差:往前放 2 秒
438
+ const id = latestSessionId(input.agent, input.paths.configDir, startedAt - 2_000);
439
+ if (id)
440
+ console.log(resumeHintText(input.agent, id));
441
+ }
442
+ await Promise.race([check, new Promise((r) => setTimeout(r, 1_500).unref())]);
443
+ return code;
444
+ }
@@ -0,0 +1,9 @@
1
+ import { type CliConfig } from "../config.js";
2
+ /**
3
+ * TUI 退出后前台安装新版:此刻不再有模型调用和懒加载,重铺 node_modules 才安全。
4
+ * 不设 timeout——半路杀掉 npm 会留下残缺的全局安装,之后连启动都起不来;
5
+ * stdio 直通,装多久用户看得见。(同机另开的 u1s1 实例仍会被重铺波及,属残余
6
+ * 风险,但远好于更新自己脚下这棵树。)
7
+ */
8
+ export declare function installPendingUpdate(): void;
9
+ export declare function runAgent(cfg: CliConfig, args: string[]): Promise<void>;