@epoch-agent/cli 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,4642 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ writeProviderKey
4
+ } from "./chunk-3SCZQI5W.js";
5
+
6
+ // src/index.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/commands/agents.ts
10
+ import { findRole, roleSourceLabel } from "@epoch-agent/core";
11
+
12
+ // src/exit-codes.ts
13
+ var EXIT_CODES = {
14
+ /** 任务完成 */
15
+ SUCCESS: 0,
16
+ /** 通用失败(provider 起不来、运行时报错……) */
17
+ FAILURE: 1,
18
+ /**
19
+ * 非交互下有操作因**缺少预授权**被拒。
20
+ *
21
+ * 只在非交互路径出现:交互模式下用户亲手点的「拒绝」是他的决定,
22
+ * 不算 agent 被卡住,仍然退 0。
23
+ */
24
+ PERMISSION_DENIED: 3,
25
+ /**
26
+ * 企业托管设置挡下了这次启动(方案 22)。
27
+ *
28
+ * 和 FAILURE 分开的理由同上:这不是「epoch 坏了」,是**策略生效了**。
29
+ * 管理员按机器批量铺开一条 `disableBypassPermissionsMode` 之后,
30
+ * 要能从退出码上一眼看出哪些机器是被自己的策略拦下的,
31
+ * 而不是去每台机器上读 stderr 猜。
32
+ */
33
+ MANAGED_POLICY: 4,
34
+ /**
35
+ * `--max-turns` / `--max-budget-usd` 触顶,任务**没做完**就停了(方案 28)。
36
+ *
37
+ * 和 FAILURE 分开:CI 里这不是「epoch 坏了」,是**限额生效了** ——
38
+ * 该做的是看一眼输出决定要不要加额重跑,而不是去查日志。
39
+ * 跨会话预算(`budget.maxCostUsd`)触顶**不走这个码**,仍然退 0:
40
+ * 那是长期配置,每次都退非 0 会让流水线天天红。
41
+ *
42
+ * ⚠️ 方案 28 立项时写的是 `4`,那是 2026-08-08 的现状(当时只有 0/1/3)。
43
+ * `4` 后来被企业托管策略(方案 22)占了,而那个码已经在根 README 和
44
+ * MDM 文档里对外承诺过,不能挪。所以这里往后排。
45
+ */
46
+ LIMIT_EXCEEDED: 5,
47
+ /**
48
+ * `--input-format stream-json` 的输入有坏行(方案 28)。
49
+ *
50
+ * **不是**用法错误(那是 1):命令敲对了,是宿主写进 stdin 的那一行不合协议。
51
+ * 坏行会被报到 stderr 并**跳过**(NDJSON 是按行分帧的,跳一行不会失步),
52
+ * 进程接着跑;这个码在收尾时才落。
53
+ */
54
+ INPUT_ERROR: 6
55
+ };
56
+
57
+ // src/errors.ts
58
+ var CliError = class extends Error {
59
+ constructor(message, exitCode = EXIT_CODES.FAILURE, hint) {
60
+ super(message);
61
+ this.exitCode = exitCode;
62
+ this.hint = hint;
63
+ this.name = "CliError";
64
+ }
65
+ exitCode;
66
+ hint;
67
+ };
68
+ function isCancellation(err2) {
69
+ if (!(err2 instanceof Error)) return false;
70
+ return err2.name === "ExitPromptError" || err2.name === "AbortError" || // AbortController.abort() 默认抛的就是这个
71
+ err2.name === "Error" && err2.message === "The operation was aborted.";
72
+ }
73
+ var EXPECTED_FAILURES = {
74
+ /** 企业托管策略挡下的启动(方案 22 §2.6)。不是故障,是策略生效了 */
75
+ ManagedPolicyError: EXIT_CODES.MANAGED_POLICY,
76
+ /** `--agent` 给了不认识的角色名(方案 29 验收 #13)。可用角色都在 message 里 */
77
+ AgentRoleError: EXIT_CODES.FAILURE,
78
+ /** `--settings` 指的文件不是合法 JSON(方案 29 验收 #16)。hint 里带行号 */
79
+ SettingsFileError: EXIT_CODES.FAILURE
80
+ };
81
+ function expectedFailureExitCode(err2) {
82
+ if (!(err2 instanceof Error)) return void 0;
83
+ return EXPECTED_FAILURES[err2.name];
84
+ }
85
+ function hintOf(err2) {
86
+ const hint = err2.hint;
87
+ return typeof hint === "string" && hint.length > 0 ? hint : void 0;
88
+ }
89
+ function reportFatal(err2) {
90
+ if (isCancellation(err2)) {
91
+ process.stderr.write("\n\u5DF2\u53D6\u6D88\n");
92
+ process.exit(130);
93
+ }
94
+ const exitCode = err2 instanceof CliError ? err2.exitCode : expectedFailureExitCode(err2);
95
+ if (exitCode !== void 0 && err2 instanceof Error) {
96
+ process.stderr.write(`\u9519\u8BEF: ${err2.message}
97
+ `);
98
+ const hint = hintOf(err2);
99
+ if (hint) process.stderr.write(`${hint}
100
+ `);
101
+ process.exit(exitCode);
102
+ }
103
+ process.stderr.write(`${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}
104
+ `);
105
+ process.exit(EXIT_CODES.FAILURE);
106
+ }
107
+
108
+ // src/commands/agents.ts
109
+ var log = (msg) => {
110
+ process.stdout.write(msg + "\n");
111
+ };
112
+ async function withRuntime(fn) {
113
+ const { buildRuntime: buildRuntime4 } = await import("@epoch-agent/runtime");
114
+ const rt = await buildRuntime4({ installSignalHandlers: true });
115
+ try {
116
+ fn(rt);
117
+ } finally {
118
+ await rt.dispose();
119
+ }
120
+ }
121
+ function renderRole(role) {
122
+ const lines = [];
123
+ lines.push(` ${role.name} [${roleSourceLabel(role.source)}]`);
124
+ lines.push(` ${role.description}`);
125
+ if (role.tools) {
126
+ lines.push(` \u5DE5\u5177\uFF08\u58F0\u660E\uFF09: ${role.tools.join(", ")}`);
127
+ } else {
128
+ lines.push(" \u5DE5\u5177\uFF08\u58F0\u660E\uFF09: \u7EE7\u627F\u4E3B agent \u7684\u5168\u90E8\u53EF\u7528\u5DE5\u5177");
129
+ }
130
+ if (role.maxTurns !== void 0) lines.push(` \u8F6E\u6B21\u4E0A\u9650: ${role.maxTurns}`);
131
+ return lines;
132
+ }
133
+ function renderRoleDetail(role) {
134
+ const lines = [...renderRole(role), ""];
135
+ if (role.prompt) {
136
+ lines.push(" \u89D2\u8272 prompt:");
137
+ for (const line of role.prompt.split("\n")) lines.push(` ${line}`);
138
+ } else {
139
+ lines.push(" \u89D2\u8272 prompt: \u65E0\uFF08\u53EA\u6362\u6389\u5B50 agent \u7684\u8EAB\u4EFD\u884C\uFF0C\u5176\u4F59\u6CBF\u7528\u901A\u7528 prompt\uFF09");
140
+ }
141
+ return lines;
142
+ }
143
+ function registerAgentsCommand(program2) {
144
+ const agents = program2.command("agents").description("\u5217\u51FA\u53EF\u6D3E\u7ED9\u5B50 agent \u7684\u89D2\u8272\uFF08delegate_task \u7684 role \u53D6\u503C\uFF09").action(async () => {
145
+ await withRuntime((rt) => {
146
+ if (rt.agentRoles.length === 0) {
147
+ log("\u6CA1\u6709\u53EF\u7528\u89D2\u8272\uFF08provider \u8D77\u4E0D\u6765\u65F6 delegate_task \u4E0D\u4F1A\u6CE8\u518C\uFF09");
148
+ log("\u8FD0\u884C epoch doctor \u770B\u5177\u4F53\u662F\u54EA\u4E2A\u6A21\u5757\u7684\u95EE\u9898\u3002");
149
+ return;
150
+ }
151
+ log(`\u53EF\u6D3E\u89D2\u8272\uFF08${rt.agentRoles.length} \u4E2A\uFF09:
152
+ `);
153
+ for (const role of rt.agentRoles) {
154
+ for (const line of renderRole(role)) log(line);
155
+ log("");
156
+ }
157
+ const notes = rt.diagnosticList.filter((d) => d.module === "Agent \u89D2\u8272");
158
+ if (notes.length > 0) {
159
+ log("\u52A0\u8F7D\u8BF4\u660E:");
160
+ for (const d of notes) log(` ${d.detail}`);
161
+ log("");
162
+ }
163
+ log("\u5B9A\u4E49\u81EA\u5DF1\u7684\u89D2\u8272\uFF1A~/.epoch/agents/<\u540D\u5B57>.md\uFF08\u9879\u76EE\u7EA7\u653E .epoch/agents/\uFF0C\u9700\u4FE1\u4EFB\u5DE5\u4F5C\u533A\uFF09");
164
+ log("\u26A0 \u5DE5\u5177\u767D\u540D\u5355\u5BF9\u542B terminal \u7684\u89D2\u8272\u4E0D\u662F\u5B89\u5168\u8FB9\u754C \u2014\u2014 \u6709 shell \u5C31\u80FD\u7ED5\u5F00\u300C\u6CA1\u6709\u5199\u5DE5\u5177\u300D\u3002");
165
+ });
166
+ });
167
+ agents.command("show <name>").description("\u770B\u67D0\u4E2A\u89D2\u8272\u7684\u5B8C\u6574\u5B9A\u4E49\uFF08\u542B\u89D2\u8272 prompt\uFF09").action(async (name) => {
168
+ await withRuntime((rt) => {
169
+ const role = findRole(rt.agentRoles, name);
170
+ if (!role) {
171
+ const available = rt.agentRoles.map((r) => r.name).join(", ");
172
+ throw new CliError(
173
+ `\u6CA1\u6709\u540D\u4E3A "${name}" \u7684 agent \u89D2\u8272`,
174
+ EXIT_CODES.FAILURE,
175
+ available ? `\u53EF\u7528\uFF1A${available}` : "\u4E00\u4E2A\u89D2\u8272\u90FD\u6CA1\u52A0\u8F7D\u51FA\u6765\uFF0C\u8FD0\u884C epoch doctor \u770B\u539F\u56E0"
176
+ );
177
+ }
178
+ for (const line of renderRoleDetail(role)) log(line);
179
+ });
180
+ });
181
+ }
182
+
183
+ // src/commands/completion.ts
184
+ var SHELLS = ["bash", "zsh", "fish", "powershell"];
185
+ function longFlags(cmd) {
186
+ return cmd.options.map((o) => o.long).filter((l) => !!l);
187
+ }
188
+ function visibleSubcommands(cmd) {
189
+ return cmd.createHelp().visibleCommands(cmd);
190
+ }
191
+ function toSpec(cmd) {
192
+ return {
193
+ name: cmd.name(),
194
+ description: cmd.description(),
195
+ // `--help` 要手工补:commander 把它存在 `_helpOption` 里单独处理,**不进**
196
+ // `cmd.options`,所以 longFlags 拿不到。`--version` 反过来是真选项
197
+ // (run.ts 注册的,或者 `program.version()` 注册的),已经在里面了。
198
+ // 过一次 Set 是防有人又显式声明了同名 flag —— 重复项在 bash 里表现为
199
+ // 同一个候选出现两遍
200
+ options: [.../* @__PURE__ */ new Set([...longFlags(cmd), "--help"])],
201
+ subcommands: visibleSubcommands(cmd).map((sub) => toSpec(sub))
202
+ };
203
+ }
204
+ function describeCommands(program2) {
205
+ return toSpec(program2);
206
+ }
207
+ function words(spec) {
208
+ return [...spec.subcommands.map((s) => s.name), ...spec.options].join(" ");
209
+ }
210
+ function q(text) {
211
+ return text.replace(/'/g, "'\\''");
212
+ }
213
+ function bashBranch(cmd) {
214
+ if (cmd.subcommands.length === 0) return ` ${cmd.name}) words='${words(cmd)}' ;;`;
215
+ const inner = cmd.subcommands.map((s) => ` ${s.name}) words='${words(s)}' ;;`);
216
+ return [
217
+ ` ${cmd.name})`,
218
+ ` if [[ $COMP_CWORD -eq 2 ]]; then`,
219
+ ` words='${words(cmd)}'`,
220
+ ` else`,
221
+ ` case "\${COMP_WORDS[2]}" in`,
222
+ ...inner,
223
+ ` *) words='' ;;`,
224
+ ` esac`,
225
+ ` fi ;;`
226
+ ].join("\n");
227
+ }
228
+ function bashScript(root) {
229
+ return `# epoch bash \u8865\u5168\u3002\u88C5\u6CD5\uFF1A
230
+ # epoch completion bash > /usr/local/etc/bash_completion.d/epoch
231
+ # \u6216\u76F4\u63A5 eval "$(epoch completion bash)"
232
+ _epoch_completion() {
233
+ local cur words
234
+ cur="\${COMP_WORDS[COMP_CWORD]}"
235
+
236
+ if [[ $COMP_CWORD -eq 1 ]]; then
237
+ COMPREPLY=($(compgen -W '${words(root)}' -- "$cur"))
238
+ return
239
+ fi
240
+
241
+ case "\${COMP_WORDS[1]}" in
242
+ ${root.subcommands.map(bashBranch).join("\n")}
243
+ *) words='' ;;
244
+ esac
245
+
246
+ # \u6CA1\u6709\u5019\u9009\u65F6\u4EA4\u8FD8\u7ED9\u9ED8\u8BA4\u7684\u6587\u4EF6\u540D\u8865\u5168\uFF1A\`epoch "\u5206\u6790 <\u6587\u4EF6>"\` \u8981\u80FD\u8865\u8DEF\u5F84
247
+ if [[ -z "$words" ]]; then
248
+ COMPREPLY=($(compgen -f -- "$cur"))
249
+ else
250
+ COMPREPLY=($(compgen -W "$words" -- "$cur"))
251
+ fi
252
+ }
253
+ complete -F _epoch_completion epoch
254
+ `;
255
+ }
256
+ function zshBranch(cmd) {
257
+ if (cmd.subcommands.length === 0) return ` ${cmd.name}) opts=(${words(cmd)}) ;;`;
258
+ const inner = cmd.subcommands.map((s) => ` ${s.name}) opts=(${words(s)}) ;;`);
259
+ return [
260
+ ` ${cmd.name})`,
261
+ ` if (( CURRENT == 3 )); then`,
262
+ ` opts=(${words(cmd)})`,
263
+ ` else`,
264
+ ` case "\${words[3]}" in`,
265
+ ...inner,
266
+ ` *) opts=() ;;`,
267
+ ` esac`,
268
+ ` fi ;;`
269
+ ].join("\n");
270
+ }
271
+ function zshScript(root) {
272
+ const cmds = root.subcommands.map((c) => ` '${c.name}:${q(c.description)}'`).join("\n");
273
+ return `#compdef epoch
274
+ # epoch zsh \u8865\u5168\u3002\u88C5\u6CD5\uFF1A\u628A\u672C\u6587\u4EF6\u5B58\u6210 fpath \u91CC\u7684 _epoch\uFF0C
275
+ # \u6216\u5728 .zshrc \u91CC eval "$(epoch completion zsh)"
276
+ _epoch_completion() {
277
+ local -a cmds opts
278
+ cmds=(
279
+ ${cmds}
280
+ )
281
+
282
+ if (( CURRENT == 2 )); then
283
+ _describe -t commands '\u547D\u4EE4' cmds
284
+ compadd -- ${root.options.join(" ")}
285
+ return
286
+ fi
287
+
288
+ case "\${words[2]}" in
289
+ ${root.subcommands.map(zshBranch).join("\n")}
290
+ *) opts=() ;;
291
+ esac
292
+
293
+ if (( \${#opts} )); then
294
+ compadd -a opts
295
+ else
296
+ _files
297
+ fi
298
+ }
299
+ compdef _epoch_completion epoch
300
+ `;
301
+ }
302
+ function fishLines(cmd, seen) {
303
+ const lines = [];
304
+ for (const sub of cmd.subcommands) {
305
+ const desc = sub.description || sub.name;
306
+ lines.push(`complete -c epoch -n '${seen}' -a '${sub.name}' -d '${q(desc)}'`);
307
+ lines.push(...fishLines(sub, `__fish_seen_subcommand_from ${sub.name}`));
308
+ }
309
+ for (const opt of cmd.options) {
310
+ lines.push(`complete -c epoch -n '${seen}' -l ${opt.slice(2)}`);
311
+ }
312
+ return lines;
313
+ }
314
+ function fishScript(root) {
315
+ return [
316
+ "# epoch fish \u8865\u5168\u3002\u88C5\u6CD5\uFF1A",
317
+ "# epoch completion fish > ~/.config/fish/completions/epoch.fish",
318
+ ...fishLines(root, "__fish_use_subcommand")
319
+ ].join("\n");
320
+ }
321
+ function powershellEntries(cmd, prefix) {
322
+ const lines = [` '${prefix}' = '${words(cmd)}'`];
323
+ for (const sub of cmd.subcommands) {
324
+ lines.push(...powershellEntries(sub, prefix ? `${prefix} ${sub.name}` : sub.name));
325
+ }
326
+ return lines;
327
+ }
328
+ function powershellScript(root) {
329
+ return `# epoch PowerShell completion.
330
+ # Install: epoch completion powershell >> $PROFILE
331
+ # Or for the current session only: epoch completion powershell | Out-String | Invoke-Expression
332
+ Register-ArgumentCompleter -Native -CommandName epoch -ScriptBlock {
333
+ param($wordToComplete, $commandAst, $cursorPosition)
334
+
335
+ $epochWords = @{
336
+ ${powershellEntries(root, "").join("\n")}
337
+ }
338
+
339
+ # Words already typed, minus the executable and the word being completed
340
+ $typed = @($commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.ToString() })
341
+ if ($wordToComplete -and $typed.Count -gt 0) {
342
+ $typed = @($typed | Select-Object -First ($typed.Count - 1))
343
+ }
344
+
345
+ # Longest known command path wins; stop at the first flag or unknown word
346
+ $key = ''
347
+ foreach ($word in $typed) {
348
+ if ($word.StartsWith('-')) { break }
349
+ $next = if ($key) { "$key $word" } else { $word }
350
+ if (-not $epochWords.ContainsKey($next)) { break }
351
+ $key = $next
352
+ }
353
+
354
+ $candidates = $epochWords[$key]
355
+ if (-not $candidates) { return }
356
+ $candidates -split ' ' | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object | ForEach-Object {
357
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
358
+ }
359
+ }
360
+ `;
361
+ }
362
+ function renderCompletion(program2, shell) {
363
+ const root = describeCommands(program2);
364
+ if (shell === "bash") return bashScript(root);
365
+ if (shell === "zsh") return zshScript(root);
366
+ if (shell === "powershell") return powershellScript(root);
367
+ return fishScript(root) + "\n";
368
+ }
369
+ function isShellName(value) {
370
+ return SHELLS.includes(value);
371
+ }
372
+ function registerCompletionCommand(program2) {
373
+ program2.command("completion").description(`\u751F\u6210 shell \u8865\u5168\u811A\u672C\uFF08${SHELLS.join(" / ")}\uFF09`).argument("<shell>", SHELLS.join(" / ")).action((shell) => {
374
+ if (!isShellName(shell)) {
375
+ throw new CliError(`\u4E0D\u652F\u6301\u7684 shell: ${shell}`, 1, `\u53EF\u9009: ${SHELLS.join(" / ")}`);
376
+ }
377
+ process.stdout.write(renderCompletion(program2, shell));
378
+ });
379
+ }
380
+
381
+ // src/commands/config.ts
382
+ import { existsSync as existsSync2, readFileSync } from "fs";
383
+ import { maskApiKey, t } from "@epoch-agent/infra";
384
+ import {
385
+ API_KEY_ENV_VARS,
386
+ isPermissionLevel,
387
+ isProviderType,
388
+ parseModelRef,
389
+ PERMISSION_LEVELS,
390
+ PROVIDER_TYPES,
391
+ SHELL_KINDS
392
+ } from "@epoch-agent/protocol";
393
+
394
+ // src/files/config-yaml.ts
395
+ import {
396
+ DEFAULT_YAML,
397
+ readConfigYaml,
398
+ readKey,
399
+ setScalar,
400
+ setSectionField,
401
+ unsetScalar,
402
+ unsetSectionField,
403
+ writeConfigYaml
404
+ } from "@epoch-agent/infra";
405
+
406
+ // src/paths.ts
407
+ import { existsSync, mkdirSync } from "fs";
408
+ import { hasProviderCredentials } from "@epoch-agent/core";
409
+ import { configPath, envPath, resolveHomeDir } from "@epoch-agent/infra";
410
+ var EPOCH_HOME = resolveHomeDir();
411
+ var CONFIG_PATH = configPath(EPOCH_HOME);
412
+ var ENV_PATH = envPath(EPOCH_HOME);
413
+ function ensureEpochHome() {
414
+ if (!existsSync(EPOCH_HOME)) mkdirSync(EPOCH_HOME, { recursive: true });
415
+ }
416
+ function hasCredentials() {
417
+ return hasProviderCredentials(EPOCH_HOME);
418
+ }
419
+
420
+ // src/commands/config.ts
421
+ var log2 = (msg) => {
422
+ process.stdout.write(msg + "\n");
423
+ };
424
+ function isPositiveNumber(v) {
425
+ const n = Number(v);
426
+ return Number.isFinite(n) && n > 0;
427
+ }
428
+ var BUDGET_FIELDS = {
429
+ maxCostUsd: isPositiveNumber,
430
+ maxTokens: isPositiveNumber,
431
+ warnAtPercent: (v) => isPositiveNumber(v) && Number(v) <= 100,
432
+ onUnknownPricing: (v) => v === "block" || v === "warn"
433
+ };
434
+ function knownKeys() {
435
+ return `provider, model, models.utility, permission, shell, ` + Object.keys(BUDGET_FIELDS).map((f) => `budget.${f}`).join(", ");
436
+ }
437
+ function maskSecrets(yaml) {
438
+ return yaml.replace(/^(\s*(?:apiKey|api_key|token|secret)\s*:\s*)(.+)$/gim, (_m, head, raw) => {
439
+ const value = String(raw).trim().replace(/^["']|["']$/g, "");
440
+ if (!value) return `${head}${raw}`;
441
+ return `${head}${maskApiKey(value)}`;
442
+ });
443
+ }
444
+ function registerConfigCommand(program2) {
445
+ const cmd = program2.command("config").description(t("cli.config.summary"));
446
+ cmd.command("show").description(t("cli.config.show")).option("--raw", t("cli.config.opt_raw")).action((opts) => {
447
+ if (!existsSync2(CONFIG_PATH)) {
448
+ log2("\u5C1A\u672A\u914D\u7F6E\u3002\u8FD0\u884C epoch model \u521D\u59CB\u5316\u3002");
449
+ } else {
450
+ const yaml = readFileSync(CONFIG_PATH, "utf-8").trim();
451
+ log2(opts.raw ? yaml : maskSecrets(yaml));
452
+ }
453
+ });
454
+ cmd.command("path").description(t("cli.config.path")).action(() => log2(CONFIG_PATH));
455
+ cmd.command("get").description(t("cli.config.get")).argument("<key>", t("cli.config.keys")).action((key) => {
456
+ const value = readKey(
457
+ readConfigYaml(CONFIG_PATH),
458
+ key === "provider" ? "provider.type" : key
459
+ );
460
+ if (value === void 0) throw new CliError(`${key} \u672A\u8BBE\u7F6E`);
461
+ log2(value);
462
+ });
463
+ cmd.command("set").description(t("cli.config.set")).argument("<key>", t("cli.config.keys")).argument("<value>", t("cli.config.value")).action((key, value) => {
464
+ ensureEpochHome();
465
+ writeConfigYaml(CONFIG_PATH, applySet(readConfigYaml(CONFIG_PATH), key, value));
466
+ log2(`\u2705 ${key} = ${value}`);
467
+ });
468
+ cmd.command("unset").description(t("cli.config.unset")).argument("<key>", t("cli.config.keys_unset")).action((key) => {
469
+ ensureEpochHome();
470
+ writeConfigYaml(CONFIG_PATH, applyUnset(readConfigYaml(CONFIG_PATH), key));
471
+ log2(`\u2705 \u5DF2\u5220\u9664 ${key}`);
472
+ });
473
+ registerSchemaCommand(cmd);
474
+ registerSecretCommand(cmd);
475
+ }
476
+ function registerSchemaCommand(cmd) {
477
+ cmd.command("schema").description(t("cli.config.schema")).action(async () => {
478
+ const { resolveProjectRoot: resolveProjectRoot4 } = await import("@epoch-agent/core");
479
+ const root = resolveProjectRoot4(process.cwd());
480
+ const { writeProjectSchemas } = await import("./schema-file-DH6X4N4K.js");
481
+ const written = await writeProjectSchemas(root);
482
+ for (const entry of written) log2(`\u2705 ${entry.path}`);
483
+ log2("");
484
+ log2(t("cli.config.schema_paste_hint"));
485
+ log2(` { "$schema": "${written[0]?.reference ?? ""}", \u2026 }`);
486
+ });
487
+ }
488
+ function registerSecretCommand(cmd) {
489
+ cmd.command("secret").description(t("cli.config.secret")).option("--export", t("cli.config.opt_export")).action(async (opts) => {
490
+ const { ensureSecretsReady } = await import("@epoch-agent/runtime");
491
+ await ensureSecretsReady();
492
+ const { getSecretStore } = await import("@epoch-agent/infra");
493
+ const store = getSecretStore();
494
+ if (!store) throw new CliError("\u51ED\u636E\u5B58\u50A8\u672A\u5C31\u7EEA");
495
+ if (opts.export) await exportSecrets(store);
496
+ else await printSecretStatus(store);
497
+ });
498
+ }
499
+ async function printSecretStatus(store) {
500
+ log2(`${store.encrypted ? "\u2713" : "\u26A0"} \u540E\u7AEF: ${store.backend} \u2014\u2014 ${store.detail}`);
501
+ const names = (await store.list()).filter((n) => API_KEY_ENV_VARS.includes(n));
502
+ if (names.length === 0) {
503
+ log2(" (\u8FD8\u6CA1\u5B58\u8FC7 provider \u51ED\u636E\uFF0C\u8DD1 epoch model \u914D\u4E00\u4E2A)");
504
+ return;
505
+ }
506
+ for (const name of names) log2(` ${name}`);
507
+ }
508
+ async function exportSecrets(store) {
509
+ const { isNonInteractive: isNonInteractive9 } = await import("@epoch-agent/core");
510
+ if (isNonInteractive9()) {
511
+ throw new CliError(
512
+ "epoch config secret --export \u9700\u8981\u4EA4\u4E92\u5F0F\u7EC8\u7AEF",
513
+ 1,
514
+ "\u5B83\u4F1A\u628A\u51ED\u636E\u5199\u6210\u660E\u6587\uFF0C\u5FC5\u987B\u7531\u4EBA\u786E\u8BA4\u4E24\u6B21\u3002CI / \u5BB9\u5668\u91CC\u8BF7\u6539\u7528\u73AF\u5883\u53D8\u91CF\uFF08\u4F18\u5148\u7EA7\u6700\u9AD8\uFF09\u3002"
515
+ );
516
+ }
517
+ const { confirm } = await import("@inquirer/prompts");
518
+ log2(`\u5373\u5C06\u628A\u51ED\u636E\u4EE5\u660E\u6587\u5199\u5165 ${ENV_PATH}\uFF08\u6743\u9650 0600\uFF09\u3002`);
519
+ if (!await confirm({ message: "\u786E\u8BA4\u8981\u628A\u51ED\u636E\u5012\u6210\u660E\u6587\u5417\uFF1F", default: false })) {
520
+ log2("\u5DF2\u53D6\u6D88");
521
+ return;
522
+ }
523
+ const again = await confirm({
524
+ message: `\u518D\u786E\u8BA4\u4E00\u6B21\uFF1A${ENV_PATH} \u5C06\u5305\u542B\u53EF\u76F4\u63A5\u4F7F\u7528\u7684 API key`,
525
+ default: false
526
+ });
527
+ if (!again) {
528
+ log2("\u5DF2\u53D6\u6D88");
529
+ return;
530
+ }
531
+ ensureEpochHome();
532
+ const { exportSecretsToEnv } = await import("./env-file-JUOFDESY.js");
533
+ const written = await exportSecretsToEnv(store, ENV_PATH, API_KEY_ENV_VARS);
534
+ if (written.length === 0) {
535
+ log2("\u6CA1\u6709\u53EF\u5012\u51FA\u7684\u51ED\u636E");
536
+ return;
537
+ }
538
+ log2(`\u2705 \u5DF2\u5199\u51FA ${written.length} \u4E2A\u51ED\u636E\u5230 ${ENV_PATH}\uFF080600\uFF09`);
539
+ log2(" \u94A5\u5319\u4E32\u91CC\u90A3\u4EFD\u6CA1\u6709\u5220 \u2014\u2014 \u89E3\u6790\u94FE\u91CC .env \u538B\u5728\u94A5\u5319\u4E32\u4E4B\u4E0A\uFF0C\u8FD9\u4EFD\u4F1A\u751F\u6548\u3002");
540
+ }
541
+ function applySet(yaml, key, value) {
542
+ switch (key) {
543
+ case "provider":
544
+ if (!isProviderType(value)) {
545
+ throw new CliError(`\u672A\u77E5 provider: ${value}`, 1, `\u652F\u6301: ${PROVIDER_TYPES.join(", ")}`);
546
+ }
547
+ return yaml.replace(/^(\s*)type:.*$/m, `$1type: ${value}`);
548
+ case "model":
549
+ return setScalar(yaml, "model", value);
550
+ case "permission":
551
+ if (!isPermissionLevel(value)) {
552
+ throw new CliError(`\u672A\u77E5\u6743\u9650\u7EA7\u522B: ${value}`, 1, `\u652F\u6301: ${PERMISSION_LEVELS.join(", ")}`);
553
+ }
554
+ return setScalar(yaml, "permission", value);
555
+ case "shell": {
556
+ if (!SHELL_KINDS.includes(value)) {
557
+ throw new CliError(`\u672A\u77E5 shell: ${value}`, 1, `\u652F\u6301: ${SHELL_KINDS.join(", ")}`);
558
+ }
559
+ return setScalar(yaml, "shell", value);
560
+ }
561
+ case "models.utility": {
562
+ if (!parseModelRef(value)) {
563
+ throw new CliError(
564
+ `models.utility \u4E0D\u80FD\u4E3A\u7A7A`,
565
+ 1,
566
+ "\u5F62\u5982 gpt-4o-mini \u6216 deepseek/deepseek-chat"
567
+ );
568
+ }
569
+ return setSectionField(yaml, "models", "utility", value);
570
+ }
571
+ default: {
572
+ const field = budgetField(key);
573
+ const validate = BUDGET_FIELDS[field];
574
+ if (!validate) throw new CliError(`\u672A\u77E5\u914D\u7F6E\u9879: ${key}`, 1, `\u652F\u6301: ${knownKeys()}`);
575
+ if (!validate(value)) throw new CliError(`${key} \u7684\u503C\u4E0D\u5408\u6CD5: ${value}`);
576
+ return setSectionField(yaml, "budget", field, value);
577
+ }
578
+ }
579
+ }
580
+ function applyUnset(yaml, key) {
581
+ if (key === "model" || key === "permission" || key === "shell") return unsetScalar(yaml, key);
582
+ if (key === "models.utility") return unsetSectionField(yaml, "models", "utility");
583
+ const field = budgetField(key);
584
+ if (BUDGET_FIELDS[field]) return unsetSectionField(yaml, "budget", field);
585
+ throw new CliError(
586
+ `\u4E0D\u652F\u6301\u5220\u9664: ${key}`,
587
+ 1,
588
+ `\u53EF\u5220\u9664: model, permission, shell, models.utility, budget.<\u5B57\u6BB5>`
589
+ );
590
+ }
591
+ function budgetField(key) {
592
+ return key.startsWith("budget.") ? key.slice("budget.".length) : "";
593
+ }
594
+
595
+ // src/commands/mcp.ts
596
+ import {
597
+ listMcpServers,
598
+ mcpLogin,
599
+ mcpLogout,
600
+ probeMcpServer
601
+ } from "@epoch-agent/runtime";
602
+ var log3 = (msg) => process.stdout.write(msg + "\n");
603
+ var warn = (msg) => process.stderr.write(msg + "\n");
604
+ var STATE_LABEL = {
605
+ "not-applicable": "\u4E0D\u9700\u8981\uFF08stdio\uFF09",
606
+ "bearer-header": "\u9759\u6001 Authorization header",
607
+ authorized: "\u5DF2\u767B\u5F55",
608
+ refreshable: "token \u8FC7\u671F\uFF0C\u4E0B\u6B21\u8FDE\u63A5\u81EA\u52A8\u7EED\u671F",
609
+ "logged-out": "\u672A\u767B\u5F55"
610
+ };
611
+ function registerMcpCommand(program2) {
612
+ const cmd = program2.command("mcp").description("\u7BA1\u7406 MCP server\uFF08\u5217\u8868 / \u63A2\u6D3B / OAuth \u767B\u5F55\uFF09");
613
+ cmd.command("list", { isDefault: true }).description("\u5217\u51FA\u5DF2\u914D\u7F6E\u7684 MCP server \u548C\u5404\u81EA\u7684\u767B\u5F55\u6001\uFF08\u4E0D\u8054\u7F51\uFF09").action(() => guardAsync(() => printList()));
614
+ cmd.command("status").description("\u8FDE\u4E00\u6B21\u6307\u5B9A server\uFF0C\u770B\u771F\u5B9E\u7684\u8FDE\u63A5\u7ED3\u679C\u548C\u5DE5\u5177\u8868").argument("<name>", "server \u540D").action(
615
+ (name) => guardAsync(async () => {
616
+ log3(`\u6B63\u5728\u8FDE\u63A5 ${name} \u2026\u2026`);
617
+ const result = await probeMcpServer(name, { homeDir: EPOCH_HOME });
618
+ if (!result.connected) {
619
+ warn(`\u2717 \u8FDE\u63A5\u5931\u8D25${result.error ? `\uFF1A${result.error}` : ""}`);
620
+ if (result.needsLogin) warn(` \u2192 \u8DD1 epoch mcp login ${name}`);
621
+ process.exit(1);
622
+ }
623
+ log3(`\u2713 \u5DF2\u8FDE\u63A5\uFF0C${result.toolNames.length} \u4E2A\u5DE5\u5177`);
624
+ for (const tool of result.toolNames) log3(` ${tool}`);
625
+ })
626
+ );
627
+ cmd.command("login").description("\u5BF9\u8FDC\u7A0B MCP server \u505A OAuth \u6388\u6743\uFF08\u4F1A\u6253\u5F00\u6D4F\u89C8\u5668\uFF09").argument("<name>", "server \u540D").option("--port <port>", "\u56FA\u5B9A\u56DE\u8C03\u7AEF\u53E3\uFF08\u6388\u6743\u670D\u52A1\u5668\u8981\u6C42\u767B\u8BB0 redirect_uri \u65F6\u7528\uFF09").option("--scope <scope>", "\u7533\u8BF7\u7684 scope\uFF0C\u7A7A\u683C\u5206\u9694").action(
628
+ (name, opts) => guardAsync(async () => {
629
+ const port = opts.port === void 0 ? void 0 : parsePort(opts.port);
630
+ const { refreshedOnly } = await mcpLogin(name, {
631
+ homeDir: EPOCH_HOME,
632
+ ...port === void 0 ? {} : { port },
633
+ ...opts.scope === void 0 ? {} : { scope: opts.scope },
634
+ print: log3
635
+ });
636
+ log3(refreshedOnly ? `\u2713 ${name} \u7684\u51ED\u636E\u5DF2\u7EED\u671F\uFF08\u65E0\u9700\u91CD\u65B0\u6388\u6743\uFF09` : `\u2713 ${name} \u6388\u6743\u5B8C\u6210`);
637
+ })
638
+ );
639
+ cmd.command("logout").description("\u5220\u6389\u672C\u5730\u4FDD\u5B58\u7684 OAuth \u51ED\u636E").argument("<name>", "server \u540D").action(
640
+ (name) => guardAsync(async () => {
641
+ const removed = await mcpLogout(name, { homeDir: EPOCH_HOME });
642
+ log3(removed ? `\u2713 \u5DF2\u5220\u9664 ${name} \u7684\u672C\u5730\u51ED\u636E` : `${name} \u672C\u6765\u5C31\u6CA1\u6709\u4FDD\u5B58\u51ED\u636E`);
643
+ })
644
+ );
645
+ }
646
+ async function printList() {
647
+ const { servers, issues, configPath: configPath2 } = await listMcpServers({ homeDir: EPOCH_HOME });
648
+ for (const issue of issues) warn(`\u26A0 \u914D\u7F6E\u95EE\u9898 ${issue.path}\uFF1A${issue.message}`);
649
+ log3(`MCP servers\uFF08${configPath2}\uFF09`);
650
+ if (servers.length === 0) {
651
+ log3(" (\u65E0)");
652
+ return;
653
+ }
654
+ for (const s of servers) log3(` ${describe(s)}`);
655
+ }
656
+ function describe(s) {
657
+ const target = s.config.transport === "stdio" ? s.config.command : s.config.url;
658
+ const parts = [
659
+ `${s.config.name} [${s.config.transport}] ${target ?? ""}`,
660
+ ` \u8BA4\u8BC1\uFF1A${STATE_LABEL[s.auth.state]}`
661
+ ];
662
+ if (s.auth.expiresAt) {
663
+ parts[1] += `\uFF08\u5230\u671F ${new Date(s.auth.expiresAt).toLocaleString("zh-CN")}\uFF09`;
664
+ }
665
+ return parts.join("\n");
666
+ }
667
+ function parsePort(raw) {
668
+ const port = Number(raw);
669
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
670
+ throw new Error(`--port \u5FC5\u987B\u662F 1024\u201365535 \u7684\u6574\u6570\uFF0C\u6536\u5230 "${raw}"`);
671
+ }
672
+ return port;
673
+ }
674
+ function guardAsync(fn) {
675
+ fn().catch((err2) => {
676
+ warn(`\u9519\u8BEF: ${err2 instanceof Error ? err2.message : String(err2)}`);
677
+ process.exit(1);
678
+ });
679
+ }
680
+
681
+ // src/commands/model.ts
682
+ import { input, password, select } from "@inquirer/prompts";
683
+ import { isNonInteractive, readProviderKeyEnv } from "@epoch-agent/core";
684
+ import {
685
+ apiKeyEnvVar,
686
+ PROVIDER_INFOS,
687
+ PROVIDER_TYPES as PROVIDER_TYPES2
688
+ } from "@epoch-agent/protocol";
689
+ var log4 = (msg) => process.stdout.write(msg + "\n");
690
+ var CUSTOM = "__custom__";
691
+ function registerModelCommand(program2) {
692
+ program2.command("model").description("\u4EA4\u4E92\u5F0F\u9009\u62E9 provider \u548C\u6A21\u578B").option("-r, --refresh", "\u6E05\u9664\u6A21\u578B\u7F13\u5B58").action(async (opts) => {
693
+ ensureEpochHome();
694
+ if (opts.refresh) return refreshCache();
695
+ if (isNonInteractive()) {
696
+ throw new CliError(
697
+ "epoch model \u9700\u8981\u4EA4\u4E92\u5F0F\u7EC8\u7AEF",
698
+ 1,
699
+ "\u5728 CI / \u7BA1\u9053\u91CC\u8BF7\u6539\u7528 epoch config set provider <\u540D\u5B57> \u548C epoch config set model <\u540D\u5B57>\uFF0CAPI key \u8D70\u73AF\u5883\u53D8\u91CF\u3002"
700
+ );
701
+ }
702
+ await runWizard();
703
+ });
704
+ }
705
+ async function refreshCache() {
706
+ const { clearModelCache } = await import("@epoch-agent/core");
707
+ for (const t14 of PROVIDER_TYPES2) clearModelCache(EPOCH_HOME, t14);
708
+ log4("\u5DF2\u6E05\u9664\u5168\u90E8\u6A21\u578B\u7F13\u5B58");
709
+ }
710
+ function readCurrentSelection() {
711
+ const yaml = readConfigYaml(CONFIG_PATH);
712
+ return { provider: readKey(yaml, "provider.type"), model: readKey(yaml, "model") };
713
+ }
714
+ async function runWizard() {
715
+ await ensureSecrets();
716
+ const current = readCurrentSelection();
717
+ const provider = await select({
718
+ message: "\u9009\u62E9 Provider:",
719
+ choices: providerChoices(current.provider),
720
+ pageSize: listPageSize(PROVIDER_INFOS.filter((p) => p.interactive).length),
721
+ ...isKnownProvider(current.provider) ? { default: current.provider } : {}
722
+ });
723
+ const baseUrl = provider === "openai-compatible" ? await askBaseUrl() : void 0;
724
+ const keyForDiscover = await resolveApiKey(provider);
725
+ const { discoverModels } = await import("@epoch-agent/core");
726
+ const result = await discoverModels(EPOCH_HOME, provider, keyForDiscover, baseUrl);
727
+ log4(describeDiscovery(result.source, result.models.length, keyForDiscover !== ""));
728
+ const model = await pickModel(result.models, result.source, carriedModel(provider, current));
729
+ let yaml = readConfigYaml(CONFIG_PATH);
730
+ yaml = setSectionField(yaml, "provider", "type", provider);
731
+ if (baseUrl) yaml = setSectionField(yaml, "provider", "baseUrl", baseUrl);
732
+ yaml = setScalar(yaml, "model", model);
733
+ writeConfigYaml(CONFIG_PATH, yaml);
734
+ log4(`
735
+ \u2705 ${provider} / ${model}
736
+ \u5DF2\u5C31\u7EEA\uFF0C\u8F93\u5165 epoch "\u4F60\u597D" \u8BD5\u8BD5`);
737
+ }
738
+ async function ensureSecrets() {
739
+ try {
740
+ const { ensureSecretsReady } = await import("@epoch-agent/runtime");
741
+ await ensureSecretsReady();
742
+ } catch (err2) {
743
+ log4(` \u26A0 \u51ED\u636E\u5B58\u50A8\u521D\u59CB\u5316\u5931\u8D25\uFF0C\u5DF2\u914D\u7F6E\u7684 key \u53EF\u80FD\u8BFB\u4E0D\u5230\uFF08${describeError(err2)}\uFF09`);
744
+ }
745
+ }
746
+ async function resolveApiKey(provider) {
747
+ const envKey = apiKeyEnvVar(provider);
748
+ if (!envKey) return "";
749
+ const existing = readProviderKeyEnv(EPOCH_HOME)[envKey];
750
+ const message = existing ? `API key (${envKey})\uFF08\u5DF2\u914D\u7F6E\uFF0C\u56DE\u8F66\u6CBF\u7528\uFF09:` : `API key (${envKey}):`;
751
+ const typed = (await password({ message, mask: "*" })).trim();
752
+ if (!typed) return existing ?? "";
753
+ await saveApiKey(envKey, typed);
754
+ return typed;
755
+ }
756
+ async function saveApiKey(envKey, apiKey) {
757
+ const result = await writeProviderKey(envKey, apiKey, ENV_PATH);
758
+ log4(result.encrypted ? ` \u5DF2\u5B58\u5165 ${result.detail}` : ` \u26A0 ${result.detail}`);
759
+ }
760
+ async function askBaseUrl() {
761
+ const answer = (await input({
762
+ message: "API base URL\uFF08\u5982 http://localhost:8080/v1\uFF09:",
763
+ default: "http://localhost:8080/v1"
764
+ })).trim();
765
+ return answer || void 0;
766
+ }
767
+ async function pickModel(models, source, currentModel) {
768
+ const choices = modelChoices(models, source, currentModel);
769
+ if (choices.length === 0) {
770
+ const fallback2 = currentModel ?? "gpt-4o-mini";
771
+ return await input({ message: "\u8F93\u5165\u6A21\u578B\u540D:", default: fallback2 }) || fallback2;
772
+ }
773
+ const picked = await select({
774
+ message: "\u9009\u62E9\u6A21\u578B:",
775
+ choices,
776
+ pageSize: listPageSize(choices.length),
777
+ ...currentModel && models.includes(currentModel) ? { default: currentModel } : {}
778
+ });
779
+ if (picked !== CUSTOM) return picked;
780
+ const fallback = currentModel ?? models[0] ?? "gpt-4o-mini";
781
+ return await input({ message: "\u8F93\u5165\u6A21\u578B\u540D:", default: fallback }) || fallback;
782
+ }
783
+ function isKnownProvider(value) {
784
+ return PROVIDER_INFOS.some((p) => p.interactive && p.type === value);
785
+ }
786
+ function carriedModel(chosenProvider, current) {
787
+ return chosenProvider === current.provider ? current.model : void 0;
788
+ }
789
+ function providerChoices(currentProvider) {
790
+ return PROVIDER_INFOS.filter((p) => p.interactive).map((p) => ({
791
+ name: p.type === currentProvider ? `${p.label}\uFF08\u5F53\u524D\uFF09` : p.label,
792
+ value: p.type
793
+ }));
794
+ }
795
+ function modelChoices(models, source, currentModel) {
796
+ if (models.length === 0) return [];
797
+ const choices = models.map((m) => ({
798
+ name: m === currentModel ? `${m}\uFF08\u5F53\u524D\uFF09` : m,
799
+ value: m
800
+ }));
801
+ if (currentModel && !models.includes(currentModel)) {
802
+ choices.unshift({ name: `${currentModel}\uFF08\u5F53\u524D\uFF0C\u4E0D\u5728\u6B64\u76EE\u5F55\u4E2D\uFF09`, value: currentModel });
803
+ }
804
+ if (source !== "live") choices.push({ name: "\u2500\u2500 \u81EA\u5B9A\u4E49\u8F93\u5165 \u2500\u2500", value: CUSTOM });
805
+ return choices;
806
+ }
807
+ function listPageSize(itemCount, rows = process.stdout.rows) {
808
+ const height = rows && rows > 0 ? rows : 24;
809
+ return Math.min(itemCount, Math.max(3, height - 3));
810
+ }
811
+ function describeDiscovery(source, count, hasKey) {
812
+ if (source === "live") return ` \u4ECE API \u62C9\u53D6\u5230 ${count} \u4E2A\u6A21\u578B`;
813
+ const why = hasKey ? "\u62C9\u53D6\u6A21\u578B\u5217\u8868\u5931\u8D25\uFF08\u7F51\u7EDC / key \u65E0\u6548 / \u8BE5 provider \u4E0D\u63D0\u4F9B\u5217\u8868\u63A5\u53E3\uFF09" : "\u6CA1\u6709\u53EF\u7528\u7684 API key";
814
+ const what = source === "cache" ? "\u4E0A\u6B21\u7F13\u5B58\u7684\u5217\u8868" : "\u5185\u7F6E\u7684\u9759\u6001\u76EE\u5F55\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\uFF09";
815
+ return ` \u26A0 ${why}\uFF0C\u6539\u7528${what}\u3002\u5217\u8868\u91CC\u6CA1\u6709\u4F60\u8981\u7684\u6A21\u578B\u5C31\u9009\u300C\u81EA\u5B9A\u4E49\u8F93\u5165\u300D`;
816
+ }
817
+ function describeError(err2) {
818
+ return err2 instanceof Error ? err2.message : String(err2);
819
+ }
820
+
821
+ // src/commands/plugin.ts
822
+ import {
823
+ addMarketplace,
824
+ currentEpochVersion,
825
+ installPlugin,
826
+ inventoryTotal,
827
+ isNonInteractive as isNonInteractive2,
828
+ loadPlugins,
829
+ readMarketplaces,
830
+ readPluginManifest,
831
+ removeMarketplace,
832
+ resolveMarketplaceRef,
833
+ scanPluginDir,
834
+ searchMarketplaces,
835
+ setPluginEnabled,
836
+ uninstallPlugin,
837
+ updateMarketplace,
838
+ updatePlugin
839
+ } from "@epoch-agent/core";
840
+ import {
841
+ issueDetails,
842
+ marketplacesPath,
843
+ pluginsDir,
844
+ pluginsStatePath,
845
+ t as t2
846
+ } from "@epoch-agent/infra";
847
+ var log5 = (msg) => {
848
+ process.stdout.write(msg + "\n");
849
+ };
850
+ function locations() {
851
+ return { pluginsDir: pluginsDir(EPOCH_HOME), statePath: pluginsStatePath(EPOCH_HOME) };
852
+ }
853
+ function registerPluginCommand(program2) {
854
+ const cmd = program2.command("plugin").description("\u7BA1\u7406\u63D2\u4EF6\uFF08\u4E00\u5305\u547D\u4EE4 / \u89D2\u8272 / \u6280\u80FD / hook / deny \u89C4\u5219 / MCP server\uFF09");
855
+ registerBasics(cmd);
856
+ registerLifecycle(cmd);
857
+ registerMarketplace(cmd);
858
+ }
859
+ function registerBasics(cmd) {
860
+ cmd.command("list", { isDefault: true }).alias("ls").description("\u5217\u51FA\u5DF2\u5B89\u88C5\u7684\u63D2\u4EF6").option("--verbose", "\u8FDE\u6BCF\u4E2A\u63D2\u4EF6\u5E26\u6765\u7684\u6269\u5C55\u7269\u4E00\u8D77\u5217\u51FA\u6765").action((opts) => {
861
+ printList2(opts.verbose === true);
862
+ });
863
+ cmd.command("install").alias("add").description(
864
+ "\u88C5\u4E00\u4E2A\u63D2\u4EF6\uFF08./\u672C\u5730\u76EE\u5F55 | github:owner/repo | npm:\u5305\u540D | https://\u2026.zip | <\u5E02\u573A>/<\u63D2\u4EF6>\uFF09"
865
+ ).argument("<source>", "\u63D2\u4EF6\u6765\u6E90").option("-y, --yes", "\u8DF3\u8FC7\u5B89\u88C5\u524D\u786E\u8BA4").action(async (source, opts) => {
866
+ await runInstall(source, opts.yes === true);
867
+ });
868
+ cmd.command("validate").description("\u81EA\u67E5\u4E00\u4E2A\u63D2\u4EF6\u76EE\u5F55\uFF08\u5199\u63D2\u4EF6\u65F6\u7528\uFF0C\u4E0D\u5B89\u88C5\uFF09").argument("[path]", "\u63D2\u4EF6\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09").action((path) => {
869
+ validateDir(path ?? process.cwd());
870
+ });
871
+ }
872
+ function registerLifecycle(cmd) {
873
+ cmd.command("uninstall").alias("rm").description("\u5378\u8F7D\u4E00\u4E2A\u63D2\u4EF6\uFF08\u672C\u5730\u8F6F\u94FE\u53EA\u89E3\u94FE\uFF0C\u4E0D\u5220\u4F60\u7684\u6E90\u7801\u76EE\u5F55\uFF09").argument("<name>", "\u63D2\u4EF6\u540D").action(async (name) => {
874
+ const outcome = await uninstallPlugin(name, { statePath: pluginsStatePath(EPOCH_HOME) });
875
+ if (!outcome.ok) throw new CliError(outcome.reason);
876
+ log5(`\u2713 ${outcome.message}`);
877
+ });
878
+ cmd.command("disable").description("\u505C\u7528\u4E00\u4E2A\u63D2\u4EF6\uFF08\u53EA\u6539\u8BB0\u5F55\uFF0C\u6587\u4EF6\u4E00\u4E2A\u5B57\u8282\u90FD\u4E0D\u52A8\uFF09").argument("<name>", "\u63D2\u4EF6\u540D").action(async (name) => {
879
+ await toggle(name, false);
880
+ });
881
+ cmd.command("enable").description("\u91CD\u65B0\u542F\u7528\u4E00\u4E2A\u63D2\u4EF6").argument("<name>", "\u63D2\u4EF6\u540D").action(async (name) => {
882
+ await toggle(name, true);
883
+ });
884
+ cmd.command("update").description("\u6309\u8BB0\u5F55\u91CC\u7684\u6765\u6E90\u91CD\u88C5\u4E00\u904D\uFF08\u8F6F\u94FE\u88C5\u7684\u4E0D\u7528\u66F4\u65B0\uFF09").argument("<name>", "\u63D2\u4EF6\u540D").option("-y, --yes", "\u8DF3\u8FC7\u786E\u8BA4").action(async (name, opts) => {
885
+ await runUpdate(name, opts.yes === true);
886
+ });
887
+ }
888
+ function registerMarketplace(cmd) {
889
+ const market = cmd.command("marketplace").alias("mp").description("\u7BA1\u7406\u63D2\u4EF6\u5E02\u573A");
890
+ market.command("add").description("\u52A0\u4E00\u4E2A\u5E02\u573A\uFF08github:owner/repo | https://\u2026/epoch-marketplace.json | \u672C\u5730\u76EE\u5F55\uFF09").argument("<source>", "\u5E02\u573A\u5730\u5740").action(async (source) => {
891
+ const outcome = await addMarketplace(source, { statePath: marketplacesPath(EPOCH_HOME) });
892
+ if (!outcome.ok) throw new CliError(outcome.reason);
893
+ const count = outcome.record.catalog.plugins.length;
894
+ log5(`\u2713 \u5DF2\u52A0\u5E02\u573A ${outcome.record.name}\uFF08${count} \u4E2A\u63D2\u4EF6\uFF09`);
895
+ log5(` \u641C\u4E00\u4E0B\uFF1Aepoch plugin search <\u5173\u952E\u8BCD>`);
896
+ });
897
+ market.command("list", { isDefault: true }).alias("ls").description("\u5217\u51FA\u5DF2\u52A0\u7684\u5E02\u573A").action(printMarketplaces);
898
+ market.command("remove").alias("rm").description("\u5220\u6389\u4E00\u4E2A\u5E02\u573A\uFF08\u5DF2\u7ECF\u4ECE\u5B83\u88C5\u7684\u63D2\u4EF6\u4E0D\u53D7\u5F71\u54CD\uFF09").argument("<name>", "\u5E02\u573A\u540D").action(async (name) => {
899
+ const outcome = await removeMarketplace(name, { statePath: marketplacesPath(EPOCH_HOME) });
900
+ if (!outcome.ok) throw new CliError(outcome.reason ?? `\u5220\u4E0D\u6389 ${name}`);
901
+ log5(`\u2713 \u5DF2\u5220\u6389\u5E02\u573A ${name}`);
902
+ });
903
+ market.command("update").description("\u91CD\u62C9\u5E02\u573A\u76EE\u5F55\uFF08\u76EE\u5F55\u662F\u52A0\u8FDB\u6765\u90A3\u4E00\u523B\u7F13\u5B58\u7684\uFF0C\u641C\u7D22\u8D70\u7F13\u5B58\uFF09").argument("<name>", "\u5E02\u573A\u540D").action(async (name) => {
904
+ const outcome = await updateMarketplace(name, { statePath: marketplacesPath(EPOCH_HOME) });
905
+ if (!outcome.ok) throw new CliError(outcome.reason);
906
+ log5(`\u2713 ${name} \u5DF2\u66F4\u65B0\uFF08${outcome.record.catalog.plugins.length} \u4E2A\u63D2\u4EF6\uFF09`);
907
+ });
908
+ cmd.command("search").description("\u5728\u5DF2\u52A0\u7684\u5E02\u573A\u91CC\u641C\u63D2\u4EF6\uFF08\u4E0D\u7ED9\u5173\u952E\u8BCD\u5C31\u5217\u5168\u90E8\uFF09").argument("[keyword]", "\u5173\u952E\u8BCD").action((keyword) => {
909
+ printSearch(keyword ?? "");
910
+ });
911
+ }
912
+ function printList2(verbose) {
913
+ const artifacts = loadPlugins({ statePath: pluginsStatePath(EPOCH_HOME) });
914
+ if (artifacts.loaded.length === 0 && artifacts.skipped.length === 0) {
915
+ log5("\u6CA1\u6709\u5DF2\u5B89\u88C5\u7684\u63D2\u4EF6\u3002");
916
+ log5("\u88C5\u4E00\u4E2A\uFF1Aepoch plugin install ./my-plugin \u6216 epoch plugin install github:owner/repo");
917
+ return;
918
+ }
919
+ log5(`\u5DF2\u5B89\u88C5\u63D2\u4EF6\uFF08${pluginsStatePath(EPOCH_HOME)}\uFF09
920
+ `);
921
+ for (const plugin of artifacts.loaded) {
922
+ log5(loadedLine(plugin));
923
+ if (!verbose) continue;
924
+ for (const line of renderInventory(scanPluginDir(plugin.dir, plugin.name))) {
925
+ log5(` ${line}`);
926
+ }
927
+ log5("");
928
+ }
929
+ for (const skipped of artifacts.skipped) log5(skippedLine(skipped));
930
+ if (artifacts.skipped.length > 0) log5(`
931
+ ${PLUGIN_MARK_LEGEND}`);
932
+ for (const detail of issueDetails(artifacts.issues)) log5(`
933
+ \u26A0 ${detail}`);
934
+ }
935
+ function loadedLine(plugin) {
936
+ return ` \u2713 ${plugin.name}@${plugin.version} \u2190 ${plugin.source} ${countsLine(plugin.counts)}`;
937
+ }
938
+ function countsLine(counts) {
939
+ const parts = [];
940
+ const add = (n, key) => {
941
+ if (n > 0) parts.push(t2(key, { count: n }));
942
+ };
943
+ add(counts.commands, "plugin.n_commands");
944
+ add(counts.roles, "plugin.n_roles");
945
+ add(counts.skills, "plugin.n_skills");
946
+ add(counts.hooks, "plugin.n_hooks");
947
+ add(counts.denyRules, "plugin.n_deny");
948
+ add(counts.mcpServers, "plugin.n_mcp");
949
+ return parts.length === 0 ? t2("plugin.brings_nothing") : parts.join(" \xB7 ");
950
+ }
951
+ var PLUGIN_MARK_LEGEND = "\u2713 \u5DF2\u52A0\u8F7D \xB7 \u25CB \u5DF2\u505C\u7528 \xB7 \u2717 \u542F\u7528\u4E86\u4F46\u8FD9\u6B21\u6CA1\u52A0\u8F7D";
952
+ function skippedLine(skipped) {
953
+ return ` ${skipped.disabled ? "\u25CB" : "\u2717"} ${skipped.name} ${skipped.reason}`;
954
+ }
955
+ async function runInstall(input2, yes) {
956
+ if (!yes && isNonInteractive2()) {
957
+ throw new CliError(
958
+ "epoch plugin install \u9700\u8981\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\u6765\u786E\u8BA4\u8FD9\u6B21\u5B89\u88C5",
959
+ 1,
960
+ "\u63D2\u4EF6\u53EF\u4EE5\u5E26 hook\uFF08\u4F1A\u6267\u884C shell \u547D\u4EE4\uFF09\u3002CI / \u811A\u672C\u91CC\u8BF7\u663E\u5F0F\u52A0 --yes \u8868\u793A\u4F60\u5DF2\u7ECF\u77E5\u9053\u88C5\u8FDB\u6765\u7684\u662F\u4EC0\u4E48\u3002"
961
+ );
962
+ }
963
+ const ref = resolveMarketplaceRef(input2, marketplacesPath(EPOCH_HOME));
964
+ if (ref && !ref.ok) throw new CliError(ref.reason);
965
+ const source = ref?.ok ? ref.ref.source : input2;
966
+ if (ref?.ok) log5(`${input2} \u2192 ${source}`);
967
+ const outcome = await installPlugin(source, {
968
+ ...locations(),
969
+ epochVersion: currentEpochVersion(),
970
+ onProgress: log5,
971
+ ...ref?.ok ? { marketplace: ref.ref.marketplace } : {},
972
+ ...yes ? {} : { confirm: askInstall }
973
+ });
974
+ if (!outcome.ok) {
975
+ if (outcome.reason === "\u5DF2\u53D6\u6D88") {
976
+ log5("\u5DF2\u53D6\u6D88\uFF0C\u4EC0\u4E48\u90FD\u6CA1\u88C5\u3002");
977
+ return;
978
+ }
979
+ throw new CliError(`\u88C5\u4E0D\u4E0A ${source}\uFF1A${outcome.reason}`);
980
+ }
981
+ log5(`
982
+ \u2713 \u5DF2\u5B89\u88C5 ${outcome.record.name}@${outcome.record.version} \u2192 ${outcome.record.path}`);
983
+ if (outcome.record.linked) {
984
+ log5(" \u8FD9\u662F\u4E00\u6761\u8F6F\u94FE\uFF1A\u6539\u63D2\u4EF6\u6E90\u7801\u3001\u91CD\u542F epoch \u5C31\u751F\u6548\u3002");
985
+ }
986
+ log5(" \u91CD\u542F epoch \u4E4B\u540E\u751F\u6548\uFF08\u6269\u5C55\u7269\u5728\u542F\u52A8\u65F6\u52A0\u8F7D\uFF09\u3002");
987
+ }
988
+ async function runUpdate(name, yes) {
989
+ if (!yes && isNonInteractive2()) {
990
+ throw new CliError(
991
+ "epoch plugin update \u9700\u8981\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\u6765\u786E\u8BA4\u65B0\u7248\u672C\u5E26\u6765\u7684\u4E1C\u897F",
992
+ 1,
993
+ "\u65B0\u7248\u672C\u53EF\u80FD\u52A0\u4E86 hook\uFF08\u4F1A\u6267\u884C shell \u547D\u4EE4\uFF09\u3002CI / \u811A\u672C\u91CC\u8BF7\u663E\u5F0F\u52A0 --yes\u3002"
994
+ );
995
+ }
996
+ const outcome = await updatePlugin(name, {
997
+ ...locations(),
998
+ epochVersion: currentEpochVersion(),
999
+ onProgress: log5,
1000
+ ...yes ? {} : { confirm: askInstall }
1001
+ });
1002
+ if (!outcome.ok) throw new CliError(outcome.reason);
1003
+ log5(`\u2713 ${"message" in outcome ? outcome.message : `\u5DF2\u66F4\u65B0\u5230 ${outcome.record.version}`}`);
1004
+ }
1005
+ async function askInstall(preview2) {
1006
+ for (const line of renderPreview(preview2)) log5(line);
1007
+ const { confirm } = await import("@inquirer/prompts");
1008
+ return confirm({ message: "\u786E\u8BA4\u5B89\u88C5\uFF1F", default: false });
1009
+ }
1010
+ function renderPreview(preview2) {
1011
+ const { manifest, source, inventory } = preview2;
1012
+ const lines = [""];
1013
+ lines.push(`\u5C06\u5B89\u88C5 ${manifest.name}@${manifest.version}`);
1014
+ if (manifest.description) lines.push(` ${manifest.description}`);
1015
+ lines.push(` \u6765\u6E90: ${source.raw}\uFF08${source.type}\uFF09`);
1016
+ if (source.sha256) lines.push(` \u6821\u9A8C\u548C: sha256:${source.sha256}\uFF08\u5DF2\u6838\u5BF9\uFF09`);
1017
+ if (manifest.author) lines.push(` \u4F5C\u8005: ${manifest.author.name}`);
1018
+ if (manifest.homepage) lines.push(` \u4E3B\u9875: ${manifest.homepage}`);
1019
+ lines.push("");
1020
+ lines.push(" \u5B83\u4F1A\u5E26\u6765:");
1021
+ for (const line of renderInventory(inventory)) lines.push(` ${line}`);
1022
+ lines.push("");
1023
+ return lines;
1024
+ }
1025
+ function renderInventory(inv) {
1026
+ const lines = [];
1027
+ if (inventoryTotal(inv) === 0) {
1028
+ lines.push("\uFF08\u6CA1\u6709\u4EFB\u4F55\u6269\u5C55\u7269 \u2014\u2014 \u88C5\u4E86\u7B49\u4E8E\u6CA1\u88C5\uFF0C\u786E\u8BA4\u4E00\u4E0B\u76EE\u5F55\u7ED3\u6784\u5BF9\u4E0D\u5BF9\uFF09");
1029
+ }
1030
+ if (inv.commands.length > 0) lines.push(`\u547D\u4EE4 ${inv.commands.length} \u6761: ${list(inv.commands)}`);
1031
+ if (inv.roles.length > 0) lines.push(`\u89D2\u8272 ${inv.roles.length} \u4E2A: ${list(inv.roles)}`);
1032
+ if (inv.skills.length > 0) lines.push(`\u6280\u80FD ${inv.skills.length} \u4E2A: ${list(inv.skills)}`);
1033
+ if (inv.hooks.length > 0) {
1034
+ const tally = inv.hooks.map((h) => `${h.type} \xD7 ${h.count}`).join("\uFF0C");
1035
+ lines.push(`\u26A0 hook ${tally} \u2014\u2014 \u6BCF\u4E00\u6761\u90FD\u662F\u4F1A\u5728\u4F60\u673A\u5668\u4E0A\u6267\u884C\u7684 shell \u547D\u4EE4`);
1036
+ }
1037
+ if (inv.denyRules > 0) lines.push(`deny \u89C4\u5219 ${inv.denyRules} \u6761\uFF08\u53EA\u4F1A\u8BA9 agent \u80FD\u505A\u7684\u66F4\u5C11\uFF09`);
1038
+ if (inv.mcpServers.length > 0) {
1039
+ lines.push(
1040
+ t2("plugin.preview_mcp", {
1041
+ count: inv.mcpServers.length,
1042
+ names: list(inv.mcpServers)
1043
+ })
1044
+ );
1045
+ }
1046
+ if (inv.ignoredBuckets.length > 0) {
1047
+ lines.push(
1048
+ `settings.json \u91CC\u7684 ${inv.ignoredBuckets.join(" / ")} \u4F1A\u88AB\u5FFD\u7565 \u2014\u2014 \u63D2\u4EF6\u4E0D\u80FD\u66FF\u4F60\u653E\u5F00\u6743\u9650`
1049
+ );
1050
+ }
1051
+ if (inv.jsTools) {
1052
+ lines.push("tools/ \u91CC\u7684 JS \u5DE5\u5177\u4E0D\u4F1A\u88AB\u52A0\u8F7D\uFF08\u7B2C\u4E00\u7248\u4E0D\u5728 epoch \u8FDB\u7A0B\u91CC\u8DD1\u7B2C\u4E09\u65B9\u4EE3\u7801\uFF09");
1053
+ }
1054
+ for (const detail of issueDetails(inv.issues)) lines.push(`\u26A0 ${detail}`);
1055
+ return lines;
1056
+ }
1057
+ function list(names) {
1058
+ const shown = names.slice(0, 8).join(", ");
1059
+ return names.length > 8 ? `${shown} \u2026\uFF08\u8FD8\u6709 ${names.length - 8} \u4E2A\uFF09` : shown;
1060
+ }
1061
+ function printMarketplaces() {
1062
+ const state = readMarketplaces(marketplacesPath(EPOCH_HOME));
1063
+ for (const detail of issueDetails(state.issues)) log5(`\u26A0 ${detail}`);
1064
+ if (state.records.length === 0) {
1065
+ log5("\u6CA1\u6709\u5DF2\u52A0\u7684\u5E02\u573A\u3002");
1066
+ log5("\u52A0\u4E00\u4E2A\uFF1Aepoch plugin marketplace add github:owner/repo");
1067
+ return;
1068
+ }
1069
+ log5(`\u5DF2\u52A0\u5E02\u573A\uFF08${marketplacesPath(EPOCH_HOME)}\uFF09
1070
+ `);
1071
+ for (const record of state.records) {
1072
+ const owner = record.catalog.owner ? ` by ${record.catalog.owner.name}` : "";
1073
+ log5(` ${record.name} ${record.catalog.plugins.length} \u4E2A\u63D2\u4EF6 \u2190 ${record.source}${owner}`);
1074
+ if (record.catalog.description) log5(` ${record.catalog.description}`);
1075
+ }
1076
+ }
1077
+ function printSearch(keyword) {
1078
+ const statePath = marketplacesPath(EPOCH_HOME);
1079
+ if (readMarketplaces(statePath).records.length === 0) {
1080
+ log5("\u8FD8\u6CA1\u6709\u52A0\u4EFB\u4F55\u5E02\u573A\uFF0C\u5148 epoch plugin marketplace add <\u5730\u5740>\u3002");
1081
+ return;
1082
+ }
1083
+ const hits = searchMarketplaces(keyword, statePath);
1084
+ if (hits.length === 0) {
1085
+ log5(keyword ? `\u6CA1\u6709\u5339\u914D "${keyword}" \u7684\u63D2\u4EF6\u3002` : "\u5DF2\u52A0\u7684\u5E02\u573A\u91CC\u4E00\u4E2A\u63D2\u4EF6\u90FD\u6CA1\u6709\u3002");
1086
+ return;
1087
+ }
1088
+ log5(`${hits.length} \u4E2A\u7ED3\u679C:
1089
+ `);
1090
+ for (const hit of hits) {
1091
+ const category = hit.entry.category ? ` [${hit.entry.category}]` : "";
1092
+ log5(` ${hit.ref}${category}`);
1093
+ if (hit.entry.description) log5(` ${hit.entry.description}`);
1094
+ }
1095
+ log5(`
1096
+ \u88C5\u4E00\u4E2A\uFF1Aepoch plugin install ${hits[0]?.ref ?? "<\u5E02\u573A>/<\u63D2\u4EF6>"}`);
1097
+ }
1098
+ async function toggle(name, enabled) {
1099
+ const outcome = await setPluginEnabled(name, enabled, {
1100
+ statePath: pluginsStatePath(EPOCH_HOME)
1101
+ });
1102
+ if (!outcome.ok) throw new CliError(outcome.reason);
1103
+ log5(`\u2713 ${outcome.message}`);
1104
+ }
1105
+ function validateDir(dir) {
1106
+ const read = readPluginManifest(dir);
1107
+ if (!read.manifest) {
1108
+ const detail = issueDetails(read.issues).map((d) => ` - ${d}`).join("\n");
1109
+ throw new CliError(`${dir} \u4E0D\u662F\u4E00\u4E2A\u53EF\u7528\u7684\u63D2\u4EF6:
1110
+ ${detail}`);
1111
+ }
1112
+ const manifest = read.manifest;
1113
+ log5(`\u2713 \u6E05\u5355\u53EF\u7528: ${manifest.name}@${manifest.version}`);
1114
+ if (manifest.epochVersion) log5(` epochVersion: ${manifest.epochVersion}`);
1115
+ for (const detail of issueDetails(read.issues)) log5(`\u26A0 ${detail}`);
1116
+ log5("\n\u5B83\u4F1A\u5E26\u6765:");
1117
+ const inventory = scanPluginDir(dir, manifest.name);
1118
+ for (const line of renderInventory(inventory)) log5(` ${line}`);
1119
+ if (inventory.issues.length > 0) {
1120
+ throw new CliError(`\u6709 ${inventory.issues.length} \u5904\u95EE\u9898\uFF0C\u4FEE\u5B8C\u518D\u88C5`);
1121
+ }
1122
+ }
1123
+
1124
+ // src/commands/run.ts
1125
+ import { existsSync as existsSync6 } from "fs";
1126
+ import { dirname as dirname3, join as join5 } from "path";
1127
+ import { fileURLToPath as fileURLToPath3 } from "url";
1128
+ import { isNonInteractive as isNonInteractive8, OPERATION_TYPES as OPERATION_TYPES3, PERMISSION_LEVELS as PERMISSION_LEVELS3 } from "@epoch-agent/core";
1129
+ import {
1130
+ HEADLESS_INPUT_FORMATS as HEADLESS_INPUT_FORMATS2,
1131
+ HEADLESS_OUTPUT_FORMATS as HEADLESS_OUTPUT_FORMATS2,
1132
+ PROVIDER_TYPES as PROVIDER_TYPES4
1133
+ } from "@epoch-agent/protocol";
1134
+
1135
+ // src/headless/session.ts
1136
+ import { OPERATION_TYPES } from "@epoch-agent/core";
1137
+ import { buildRuntime } from "@epoch-agent/runtime";
1138
+
1139
+ // src/commands/run-flags.ts
1140
+ import { existsSync as existsSync3 } from "fs";
1141
+ import { delimiter, resolve } from "path";
1142
+ import { isOperationType, resolveWorkspace } from "@epoch-agent/core";
1143
+ import {
1144
+ HEADLESS_INPUT_FORMATS,
1145
+ HEADLESS_OUTPUT_FORMATS,
1146
+ isHeadlessInputFormat,
1147
+ isHeadlessOutputFormat
1148
+ } from "@epoch-agent/protocol";
1149
+ function looksLikeSessionId(value) {
1150
+ return /^(ses_)?[0-9a-f][0-9a-f-]{3,}$/i.test(value);
1151
+ }
1152
+ function parseResumeArg(raw) {
1153
+ if (raw === void 0 || raw === false) return { kind: "off" };
1154
+ if (raw === true) return { kind: "pick" };
1155
+ const value = raw.trim();
1156
+ if (!value) return { kind: "pick" };
1157
+ if (looksLikeSessionId(value)) return { kind: "id", id: value };
1158
+ return { kind: "pick", query: value };
1159
+ }
1160
+ function assertResumeFlagsExclusive(opts) {
1161
+ if (opts.continue === true && opts.resume !== void 0) {
1162
+ throw new CliError(
1163
+ "--continue \u548C --resume \u4E0D\u80FD\u4E00\u8D77\u7528",
1164
+ 1,
1165
+ "--continue \u63A5\u672C\u9879\u76EE\u6700\u8FD1\u4E00\u4E2A\u4F1A\u8BDD\uFF1B--resume \u6307\u5B9A\u4E00\u4E2A\uFF08\u6216\u4EA4\u4E92\u5F0F\u9009\uFF09\u3002\u53EA\u7ED9\u4E00\u4E2A"
1166
+ );
1167
+ }
1168
+ }
1169
+ function applySettingsPath(path) {
1170
+ if (!path) return;
1171
+ const abs = resolve(path);
1172
+ if (!existsSync3(abs)) {
1173
+ throw new CliError(`--settings \u6307\u5411\u7684\u6587\u4EF6\u4E0D\u5B58\u5728: ${path}`, 1, `\u89E3\u6790\u6210: ${abs}`);
1174
+ }
1175
+ process.env.EPOCH_SETTINGS = abs;
1176
+ }
1177
+ function applyAddDirs(dirs) {
1178
+ if (!dirs || dirs.length === 0) return [];
1179
+ const abs = dirs.map((d) => resolve(d));
1180
+ const { issues } = resolveWorkspace(process.cwd(), abs);
1181
+ const fatal = issues.filter((i) => i.kind !== "redundant");
1182
+ if (fatal.length > 0) {
1183
+ throw new CliError(
1184
+ `--add-dir \u6307\u5411\u7684\u76EE\u5F55\u4E0D\u53EF\u7528: ${fatal.map((i) => i.input).join(", ")}`,
1185
+ 1,
1186
+ fatal.map((i) => i.detail).join("\uFF1B")
1187
+ );
1188
+ }
1189
+ process.env.EPOCH_ADD_DIR = abs.join(delimiter);
1190
+ return abs;
1191
+ }
1192
+ function applyAgentRole(role) {
1193
+ const name = role?.trim();
1194
+ if (!name) return;
1195
+ process.env.EPOCH_AGENT = name;
1196
+ }
1197
+ function collect(value, previous) {
1198
+ return [...previous, value];
1199
+ }
1200
+ function parseHeadlessFlags(opts) {
1201
+ const tools = opts.allowTool ?? [];
1202
+ const ops = opts.allowOperation ?? [];
1203
+ if (tools.length === 0 && ops.length === 0) return { invalid: [] };
1204
+ const invalid = ops.filter((o) => !isOperationType(o));
1205
+ if (invalid.length > 0) return { invalid };
1206
+ return {
1207
+ policy: {
1208
+ ...tools.length > 0 ? { allowTools: tools } : {},
1209
+ ...ops.length > 0 ? { allowOperations: ops.filter(isOperationType) } : {}
1210
+ },
1211
+ invalid: []
1212
+ };
1213
+ }
1214
+ function resolveOutputFormat(opts) {
1215
+ const explicit = opts.outputFormat;
1216
+ if (explicit !== void 0 && !isHeadlessOutputFormat(explicit)) {
1217
+ throw new CliError(
1218
+ `--output-format \u4E0D\u8BA4\u8BC6: ${explicit}`,
1219
+ 1,
1220
+ `\u53EF\u9009: ${HEADLESS_OUTPUT_FORMATS.join(" / ")}`
1221
+ );
1222
+ }
1223
+ if (explicit === void 0) return opts.json === true ? "json" : "text";
1224
+ if (opts.json === true && explicit !== "json") {
1225
+ throw new CliError(
1226
+ `--json \u548C --output-format ${explicit} \u77DB\u76FE`,
1227
+ 1,
1228
+ "--json \u5C31\u662F --output-format json \u7684\u522B\u540D\uFF0C\u53EA\u7ED9\u4E00\u4E2A"
1229
+ );
1230
+ }
1231
+ return explicit;
1232
+ }
1233
+ function resolveHeadlessFormats(opts) {
1234
+ const raw = opts.inputFormat;
1235
+ if (raw !== void 0 && !isHeadlessInputFormat(raw)) {
1236
+ throw new CliError(
1237
+ `--input-format \u4E0D\u8BA4\u8BC6: ${raw}`,
1238
+ 1,
1239
+ `\u53EF\u9009: ${HEADLESS_INPUT_FORMATS.join(" / ")}`
1240
+ );
1241
+ }
1242
+ const input2 = raw ?? "text";
1243
+ if (input2 === "text") return { input: input2, output: resolveOutputFormat(opts) };
1244
+ if (opts.outputFormat === void 0 && opts.json !== true) {
1245
+ return { input: input2, output: "stream-json" };
1246
+ }
1247
+ const output = resolveOutputFormat(opts);
1248
+ if (output !== "stream-json") {
1249
+ throw new CliError(
1250
+ `--input-format stream-json \u53EA\u80FD\u914D --output-format stream-json\uFF08\u73B0\u5728\u662F ${output}\uFF09`,
1251
+ 1,
1252
+ "\u957F\u9A7B\u6A21\u5F0F\u4E0B\u5BBF\u4E3B\u8981\u5728\u4E8B\u4EF6\u6D41\u91CC\u6536 approval-request \u5E76\u7B54\u590D\uFF0C\u53E6\u5916\u4E24\u79CD\u8F93\u51FA\u683C\u5F0F\u6CA1\u6709\u8FD9\u4E00\u5E27 \u2014\u2014 \u6536\u4E0D\u5230\u95EE\u9898\u5374\u8981\u56DE\u7B54\u3002\u4E0D\u7ED9 --output-format \u65F6\u4F1A\u81EA\u52A8\u8865\u4E0A"
1253
+ );
1254
+ }
1255
+ return { input: input2, output };
1256
+ }
1257
+ function resolveApproverProgram(raw) {
1258
+ const value = raw?.trim();
1259
+ if (!value) return void 0;
1260
+ if (/\s/.test(value) || !/[\\/]/.test(value)) return value;
1261
+ const abs = resolve(value);
1262
+ if (!existsSync3(abs)) {
1263
+ throw new CliError(
1264
+ `--permission-prompt-tool \u6307\u5411\u7684\u7A0B\u5E8F\u4E0D\u5B58\u5728: ${value}`,
1265
+ 1,
1266
+ `\u89E3\u6790\u6210: ${abs}\u3002\u8981\u8DD1\u4E00\u4E2A\u89E3\u91CA\u5668\u5C31\u8FDE\u7740\u5199\uFF0C\u4F8B\u5982 --permission-prompt-tool "node ./approver.js"`
1267
+ );
1268
+ }
1269
+ return `"${abs}"`;
1270
+ }
1271
+ function parseCallLimits(opts) {
1272
+ const turns = parsePositive(opts.maxTurns, "--max-turns", (n) => Number.isInteger(n));
1273
+ const usd = parsePositive(opts.maxBudgetUsd, "--max-budget-usd");
1274
+ return {
1275
+ ...turns !== void 0 ? { maxTurns: turns } : {},
1276
+ ...usd !== void 0 ? { maxCallCostUsd: usd } : {}
1277
+ };
1278
+ }
1279
+ function parsePositive(raw, flag, extra = () => true) {
1280
+ if (raw === void 0) return void 0;
1281
+ const n = Number(raw);
1282
+ if (!Number.isFinite(n) || n <= 0 || !extra(n)) {
1283
+ throw new CliError(
1284
+ `${flag} \u4E0D\u662F\u5408\u6CD5\u7684\u503C: ${raw}`,
1285
+ 1,
1286
+ flag === "--max-turns" ? "\u8981\u4E00\u4E2A\u6B63\u6574\u6570\uFF0C\u4F8B\u5982 --max-turns 5" : "\u8981\u4E00\u4E2A\u6B63\u6570\uFF0C\u4F8B\u5982 0.50"
1287
+ );
1288
+ }
1289
+ return n;
1290
+ }
1291
+
1292
+ // src/headless/emit.ts
1293
+ import {
1294
+ HEADLESS_PROTOCOL_VERSION
1295
+ } from "@epoch-agent/protocol";
1296
+ import { ApprovalRelay, QuestionRelay, toSerializable } from "@epoch-agent/runtime";
1297
+ var HeadlessEmitter = class {
1298
+ write;
1299
+ clock;
1300
+ /**
1301
+ * 审批闭包的登记表。
1302
+ *
1303
+ * 复用 runtime 那一份而不是自己写个 Map:它把两个很容易写错的点封住了
1304
+ * (答复一次就出账、**界面没了不等于用户拒绝**),而 headless 这边
1305
+ * 「宿主把 stdin 关了」和 web 那边「浏览器断连」是同一件事。
1306
+ */
1307
+ relay = new ApprovalRelay();
1308
+ /**
1309
+ * 提问闭包的登记表(方案 34)。同样复用 runtime 那一份 ——
1310
+ * 它比 `ApprovalRelay` 少一个「替用户答一个」的方法,而 headless 这边
1311
+ * 「宿主把 stdin 关了」正是最容易顺手编一个答案的地方。
1312
+ */
1313
+ questions = new QuestionRelay();
1314
+ /** 全局广播游标。0 是保留值(`init`),所以其余帧从 1 开始 */
1315
+ seq = 0;
1316
+ sessionId = "";
1317
+ constructor(opts = {}) {
1318
+ this.write = opts.write ?? ((line) => void process.stdout.write(line + "\n"));
1319
+ this.clock = opts.clock ?? Date.now;
1320
+ }
1321
+ /**
1322
+ * 第一帧。`seq: 0`、**不带 `sessionId`** —— 它描述的是这条流本身,
1323
+ * 会话 id 在它自己的载荷里(同 hub 的 `connected`)。
1324
+ */
1325
+ init(info) {
1326
+ this.sessionId = info.sessionId;
1327
+ const event = {
1328
+ type: "init",
1329
+ protocolVersion: HEADLESS_PROTOCOL_VERSION,
1330
+ ...info
1331
+ };
1332
+ this.write(JSON.stringify({ seq: 0, ts: this.clock(), event }));
1333
+ }
1334
+ /**
1335
+ * 发一个引擎事件。
1336
+ *
1337
+ * 返回**摘出 `requestId` 之后**的那份视图 —— 调用方(长驻模式的驱动)要靠
1338
+ * 它知道刚发出去的审批叫什么 id。非审批事件返回 undefined 那一格。
1339
+ */
1340
+ agentEvent(ev) {
1341
+ const serialized = toSerializable(ev);
1342
+ this.relay.track(serialized);
1343
+ this.questions.track(serialized);
1344
+ this.publish(stripArtifactData(serialized.event));
1345
+ return serialized.event;
1346
+ }
1347
+ /** 一轮的收尾帧 */
1348
+ result(result) {
1349
+ this.publish({ type: "result", ...result });
1350
+ }
1351
+ /**
1352
+ * 把宿主的答复接回引擎。
1353
+ *
1354
+ * @returns `false` = 这个 requestId 不认识(重复答复 / 上一轮的 id)。
1355
+ * 调用方该把它当成 stderr 上的一行日志而不是错误:宿主的用户
1356
+ * 在按钮上双击一下就会走到这里。
1357
+ */
1358
+ respondApproval(requestId, outcome, note) {
1359
+ return this.relay.respond(requestId, outcome, note);
1360
+ }
1361
+ /**
1362
+ * 把宿主的提问答复接回引擎(方案 34 验收 10)。
1363
+ *
1364
+ * @returns `false` = 这个 requestId 不认识。处置同 `respondApproval`:
1365
+ * stderr 上一行日志,不是错误。
1366
+ */
1367
+ respondQuestion(requestId, answer) {
1368
+ return this.questions.respond(requestId, answer);
1369
+ }
1370
+ /** 还挂着几条审批没答复 */
1371
+ get pendingApprovals() {
1372
+ return this.relay.pendingCount;
1373
+ }
1374
+ /**
1375
+ * 还挂着几条提问没答复。
1376
+ *
1377
+ * 和 `pendingApprovals` 分开数:驱动器要用它判「stdin 没了还有东西在等吗」,
1378
+ * 而合成一个数之后,日志说不清放弃的是审批还是提问 —— 排查时那正是第一个问题。
1379
+ */
1380
+ get pendingQuestions() {
1381
+ return this.questions.pendingCount;
1382
+ }
1383
+ /**
1384
+ * 宿主没了(stdin 关了 / 收到 `abort`):把挂起的审批**放弃**,返回放弃的个数。
1385
+ *
1386
+ * **一个 promise 都不 resolve** —— 逐个 `deny` 会让模型收到一串「用户拒绝」,
1387
+ * 于是它认为这是有意否决,换个方式再试一遍。正确做法是放弃 + 中止整轮,
1388
+ * 让 `AgentSession` 的 finally 去兜底。这条规矩写死在
1389
+ * [runtime/src/serialize.ts](../../../runtime/src/serialize.ts) 的类注释里,
1390
+ * hub 那边也守着同一条。
1391
+ */
1392
+ abandonApprovals() {
1393
+ return this.relay.abandon();
1394
+ }
1395
+ /**
1396
+ * 同上,提问那一半(方案 34 验收 8)。
1397
+ *
1398
+ * **一个 promise 都不 resolve。** 逐个塞一个 `skipped` 进去等于告诉模型
1399
+ * 「用户看到了、选择不答」,而真相是宿主已经不在了 —— 那个问题从没被人看到过。
1400
+ * 放弃 + 中止之后模型看到的是「这一轮被中止了」,那才是实话。
1401
+ */
1402
+ abandonQuestions() {
1403
+ return this.questions.abandon();
1404
+ }
1405
+ /** 广播一帧:占一个序号、带上 sessionId、写出去 */
1406
+ publish(event) {
1407
+ const frame = {
1408
+ seq: ++this.seq,
1409
+ sessionId: this.sessionId,
1410
+ ts: this.clock(),
1411
+ event
1412
+ };
1413
+ this.write(JSON.stringify(frame));
1414
+ }
1415
+ };
1416
+ function stripArtifactData(event) {
1417
+ if (event.type !== "tool-result" || !event.artifacts) return event;
1418
+ return { ...event, artifacts: event.artifacts.map(withoutData) };
1419
+ }
1420
+ function withoutData(artifact) {
1421
+ if (artifact.data === void 0) return artifact;
1422
+ const copy = { ...artifact };
1423
+ delete copy.data;
1424
+ return copy;
1425
+ }
1426
+
1427
+ // src/headless/outcome.ts
1428
+ function emptyOutcome(diagnostics) {
1429
+ return {
1430
+ text: "",
1431
+ reasoning: "",
1432
+ budgetExceeded: false,
1433
+ turns: 0,
1434
+ diagnostics,
1435
+ aborted: false
1436
+ };
1437
+ }
1438
+ function resultReason(out2) {
1439
+ if (out2.finishReason === void 0) return "no-finish";
1440
+ if (out2.finishReason === "budget-exceeded" && out2.breach === "call-cost") {
1441
+ return "call-budget-exceeded";
1442
+ }
1443
+ return out2.finishReason;
1444
+ }
1445
+ function toResult(out2) {
1446
+ const reason = resultReason(out2);
1447
+ return {
1448
+ ok: reason === "stop",
1449
+ reason,
1450
+ text: out2.text,
1451
+ turns: out2.turns,
1452
+ ...out2.usage ? { usage: out2.usage } : {}
1453
+ };
1454
+ }
1455
+ function limitExitCode(out2, limits) {
1456
+ if (limits.maxTurns !== void 0 && out2.finishReason === "max-turns") {
1457
+ return EXIT_CODES.LIMIT_EXCEEDED;
1458
+ }
1459
+ if (out2.breach === "call-cost") return EXIT_CODES.LIMIT_EXCEEDED;
1460
+ return void 0;
1461
+ }
1462
+ function initInfoOf(runtime, cwd) {
1463
+ const provider = runtime.providerInfo?.provider;
1464
+ return {
1465
+ sessionId: runtime.sessionId,
1466
+ cwd,
1467
+ model: runtime.config.model,
1468
+ ...provider ? { provider } : {},
1469
+ permissionLevel: runtime.permissions.level(),
1470
+ tools: runtime.tools.map((t14) => t14.name),
1471
+ usageScope: runtime.usageScope,
1472
+ diagnostics: runtime.diagnostics
1473
+ };
1474
+ }
1475
+
1476
+ // src/headless/read.ts
1477
+ import { createInterface } from "readline";
1478
+ import {
1479
+ APPROVAL_OUTCOMES,
1480
+ HEADLESS_INPUT_EVENT_TYPES,
1481
+ isApprovalOutcome,
1482
+ isQuestionAnswerMap
1483
+ } from "@epoch-agent/protocol";
1484
+ function parseInputLine(line) {
1485
+ const text = line.trim();
1486
+ if (!text) return void 0;
1487
+ let raw;
1488
+ try {
1489
+ raw = JSON.parse(text);
1490
+ } catch (err2) {
1491
+ return { ok: false, message: `\u4E0D\u662F\u5408\u6CD5 JSON\uFF08${describe2(err2)}\uFF09\uFF1A${preview(text)}` };
1492
+ }
1493
+ if (!isRecord(raw)) return { ok: false, message: `\u9876\u5C42\u5FC5\u987B\u662F\u5BF9\u8C61\uFF1A${preview(text)}` };
1494
+ const type = raw["type"];
1495
+ if (typeof type !== "string" || !HEADLESS_INPUT_EVENT_TYPES.has(type)) {
1496
+ return {
1497
+ ok: false,
1498
+ message: `\u4E0D\u8BA4\u8BC6\u7684 type: ${JSON.stringify(type)}\uFF0C\u53EF\u9009 ${[...HEADLESS_INPUT_EVENT_TYPES].join(" / ")}`
1499
+ };
1500
+ }
1501
+ if (type === "abort" || type === "close") return { ok: true, event: { type } };
1502
+ if (type === "user-message") return parseUserMessage(raw);
1503
+ if (type === "question-response") return parseQuestionResponse(raw);
1504
+ return parseApprovalResponse(raw);
1505
+ }
1506
+ function parseUserMessage(raw) {
1507
+ const content = raw["content"];
1508
+ if (typeof content === "string") {
1509
+ if (!content.trim()) return { ok: false, message: "user-message \u7684 content \u662F\u7A7A\u7684" };
1510
+ return { ok: true, event: { type: "user-message", content } };
1511
+ }
1512
+ if (!Array.isArray(content) || content.length === 0) {
1513
+ return { ok: false, message: "user-message \u7684 content \u8981\u4E48\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF0C\u8981\u4E48\u662F\u975E\u7A7A\u90E8\u4EF6\u6570\u7EC4" };
1514
+ }
1515
+ const bad = content.findIndex((p) => !isRecord(p) || typeof p["type"] !== "string");
1516
+ if (bad >= 0) {
1517
+ return { ok: false, message: `user-message \u7684 content[${bad}] \u4E0D\u662F\u5E26 type \u7684\u5BF9\u8C61` };
1518
+ }
1519
+ return {
1520
+ ok: true,
1521
+ event: { type: "user-message", content }
1522
+ };
1523
+ }
1524
+ function parseApprovalResponse(raw) {
1525
+ const requestId = raw["requestId"];
1526
+ if (typeof requestId !== "string" || !requestId) {
1527
+ return { ok: false, message: "approval-response \u7F3A requestId\uFF08\u8981\u4E00\u4E2A\u975E\u7A7A\u5B57\u7B26\u4E32\uFF09" };
1528
+ }
1529
+ const outcome = raw["outcome"];
1530
+ if (typeof outcome !== "string" || !isApprovalOutcome(outcome)) {
1531
+ return {
1532
+ ok: false,
1533
+ message: `approval-response \u7684 outcome \u4E0D\u8BA4\u8BC6: ${JSON.stringify(outcome)}\uFF0C\u53EF\u9009 ${APPROVAL_OUTCOMES.join(" / ")}`
1534
+ };
1535
+ }
1536
+ const note = raw["note"];
1537
+ if (note !== void 0 && typeof note !== "string") {
1538
+ return { ok: false, message: "approval-response \u7684 note \u8981\u662F\u5B57\u7B26\u4E32" };
1539
+ }
1540
+ return {
1541
+ ok: true,
1542
+ event: {
1543
+ type: "approval-response",
1544
+ requestId,
1545
+ outcome,
1546
+ ...note !== void 0 ? { note } : {}
1547
+ }
1548
+ };
1549
+ }
1550
+ function parseQuestionResponse(raw) {
1551
+ const requestId = raw["requestId"];
1552
+ if (typeof requestId !== "string" || !requestId) {
1553
+ return { ok: false, message: "question-response \u7F3A requestId\uFF08\u8981\u4E00\u4E2A\u975E\u7A7A\u5B57\u7B26\u4E32\uFF09" };
1554
+ }
1555
+ if (!isQuestionAnswerMap(raw["answers"])) {
1556
+ return {
1557
+ ok: false,
1558
+ message: 'question-response \u7684 answers \u8981\u662F\u300C\u95EE\u53E5 \u2192 \u5B57\u7B26\u4E32\u6216\u5B57\u7B26\u4E32\u6570\u7EC4\u300D\u7684\u5BF9\u8C61\uFF08\u7B54\u4E0D\u4E0A\u6765\u5C31\u53D1 {"answers":{},"skipped":true}\uFF09'
1559
+ };
1560
+ }
1561
+ const skipped = raw["skipped"];
1562
+ if (skipped !== void 0 && typeof skipped !== "boolean") {
1563
+ return { ok: false, message: "question-response \u7684 skipped \u8981\u662F\u5E03\u5C14" };
1564
+ }
1565
+ return {
1566
+ ok: true,
1567
+ event: {
1568
+ type: "question-response",
1569
+ requestId,
1570
+ answers: raw["answers"],
1571
+ ...skipped === true ? { skipped: true } : {}
1572
+ }
1573
+ };
1574
+ }
1575
+ async function* readInputLines(stream = process.stdin) {
1576
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
1577
+ try {
1578
+ for await (const line of rl) yield line;
1579
+ } finally {
1580
+ rl.close();
1581
+ }
1582
+ }
1583
+ function isRecord(v) {
1584
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1585
+ }
1586
+ function describe2(err2) {
1587
+ return err2 instanceof Error ? err2.message : String(err2);
1588
+ }
1589
+ function preview(text) {
1590
+ return text.length > 80 ? `${text.slice(0, 80)}\u2026` : text;
1591
+ }
1592
+
1593
+ // src/headless/driver.ts
1594
+ async function driveStreamJson(opts) {
1595
+ const { limits } = opts;
1596
+ const warn4 = opts.warn ?? ((line) => void process.stderr.write(line + "\n"));
1597
+ const emitter = new HeadlessEmitter({
1598
+ ...opts.write ? { write: opts.write } : {},
1599
+ ...opts.clock ? { clock: opts.clock } : {}
1600
+ });
1601
+ emitter.init(opts.init);
1602
+ const state = new DriverState(opts.session, emitter, limits, warn4);
1603
+ for await (const line of readInputLines(opts.input ?? process.stdin)) {
1604
+ const parsed = parseInputLine(line);
1605
+ if (parsed === void 0) continue;
1606
+ if (!parsed.ok) {
1607
+ warn4(`[\u8F93\u5165\u9519\u8BEF] ${parsed.message}`);
1608
+ state.sawBadInput = true;
1609
+ continue;
1610
+ }
1611
+ if (state.handle(parsed.event)) break;
1612
+ }
1613
+ await state.shutdown();
1614
+ return state.exitCode();
1615
+ }
1616
+ var DriverState = class {
1617
+ constructor(session, emitter, limits, warn4) {
1618
+ this.session = session;
1619
+ this.emitter = emitter;
1620
+ this.limits = limits;
1621
+ this.warn = warn4;
1622
+ }
1623
+ session;
1624
+ emitter;
1625
+ limits;
1626
+ warn;
1627
+ /** 当前这一轮,idle 时为 null —— 于是「空转时收到 abort」不会毒掉下一轮 */
1628
+ turn = null;
1629
+ controller = null;
1630
+ /**
1631
+ * 还没轮到的用户消息,先进先出。
1632
+ *
1633
+ * **排队而不是拒绝**:stdin 是一条有序的单写者通道,宿主完全可能一口气写完
1634
+ * 三条就不管了(`printf '…\n…\n' | epoch --input-format stream-json` 正是
1635
+ * 最容易上手的用法)。拒绝的话第二条起全部无声消失。
1636
+ *
1637
+ * 排队**不等于交织**:下一轮要等上一轮的 `result` 之后才开,历史累积的顺序
1638
+ * 仍然是确定的 —— hub 那边用 409 挡的是「两个标签页同时发」,
1639
+ * 那是并发,这里是排队,两回事。
1640
+ */
1641
+ queue = [];
1642
+ /**
1643
+ * stdin 已经没了(EOF 或宿主发了 `close`)。
1644
+ *
1645
+ * 之后再出现的审批请求没人答得了,得当场放弃 —— 否则那一轮会一直挂着。
1646
+ */
1647
+ inputClosed = false;
1648
+ /**
1649
+ * 累计的退出码,取**最先出现的非 0** 那个。
1650
+ *
1651
+ * 显式标 `number` 而不是让它推成 `0`:`EXIT_CODES` 是 `as const`,
1652
+ * 不标的话这个字段的类型就是字面量 `0`,`raise()` 一行都赋不进来。
1653
+ */
1654
+ code = EXIT_CODES.SUCCESS;
1655
+ /** 有过坏行。收尾时落 INPUT_ERROR,但不覆盖更主要的失败原因 */
1656
+ sawBadInput = false;
1657
+ /**
1658
+ * 处理一条入站事件。
1659
+ *
1660
+ * **同步返回,绝不 await 那一轮** —— 这是整个文件最容易写错的一行:
1661
+ * 在这里 await 就等于「跑的时候不读 stdin」,而审批答复正是从 stdin 来的,
1662
+ * 于是第一次需要确认的操作就把双方锁死。开轮之后立刻回去读下一行。
1663
+ *
1664
+ * @returns 是否该停止读 stdin(收到 `close`)
1665
+ */
1666
+ handle(event) {
1667
+ if (event.type === "close") return true;
1668
+ if (event.type === "abort") {
1669
+ this.queue.length = 0;
1670
+ this.controller?.abort();
1671
+ return false;
1672
+ }
1673
+ if (event.type === "approval-response") {
1674
+ if (!this.emitter.respondApproval(event.requestId, event.outcome, event.note)) {
1675
+ this.warn(`[\u5FFD\u7565] \u4E0D\u8BA4\u8BC6\u7684 requestId: ${event.requestId}\uFF08\u91CD\u590D\u7B54\u590D\uFF1F\u4E0A\u4E00\u8F6E\u7684\uFF1F\uFF09`);
1676
+ }
1677
+ return false;
1678
+ }
1679
+ if (event.type === "question-response") {
1680
+ const answer = {
1681
+ answers: event.answers,
1682
+ ...event.skipped ? { skipped: true } : {}
1683
+ };
1684
+ if (!this.emitter.respondQuestion(event.requestId, answer)) {
1685
+ this.warn(`[\u5FFD\u7565] \u4E0D\u8BA4\u8BC6\u7684 requestId: ${event.requestId}\uFF08\u91CD\u590D\u7B54\u590D\uFF1F\u4E0A\u4E00\u8F6E\u7684\uFF1F\uFF09`);
1686
+ }
1687
+ return false;
1688
+ }
1689
+ this.queue.push(event.content);
1690
+ this.pump();
1691
+ return false;
1692
+ }
1693
+ /**
1694
+ * 队首那条开跑。已经有一轮在跑就什么都不做 —— 它收尾时会回来再叫一次。
1695
+ *
1696
+ * **同步返回,绝不 await 那一轮**:这是整个文件最容易写错的一行。在这里 await
1697
+ * 就等于「跑的时候不读 stdin」,而审批答复正是从 stdin 来的,于是第一次需要
1698
+ * 确认的操作就把双方锁死。
1699
+ */
1700
+ pump() {
1701
+ if (this.turn) return;
1702
+ const next = this.queue.shift();
1703
+ if (next === void 0) return;
1704
+ const controller = new AbortController();
1705
+ this.controller = controller;
1706
+ this.turn = this.runTurn(next, controller).finally(() => {
1707
+ this.turn = null;
1708
+ this.controller = null;
1709
+ this.pump();
1710
+ });
1711
+ }
1712
+ /** 跑一轮,把事件逐个上线,收尾发 result */
1713
+ async runTurn(content, controller) {
1714
+ const out2 = emptyOutcome([]);
1715
+ try {
1716
+ const run = this.session.run(content, {
1717
+ signal: controller.signal,
1718
+ ...this.limits.maxTurns !== void 0 ? { maxTurns: this.limits.maxTurns } : {},
1719
+ ...this.limits.maxCallCostUsd !== void 0 ? { maxCallCostUsd: this.limits.maxCallCostUsd } : {}
1720
+ });
1721
+ for await (const ev of run) {
1722
+ this.emitter.agentEvent(ev);
1723
+ if ((ev.type === "approval-request" || ev.type === "question") && this.inputClosed) {
1724
+ this.reapLostHost();
1725
+ }
1726
+ if (ev.type === "text-delta") out2.text += ev.text;
1727
+ else if (ev.type === "turn-complete") out2.turns = ev.turn;
1728
+ else if (ev.type === "usage") out2.usage = ev.cumulative;
1729
+ else if (ev.type === "error") this.raise(EXIT_CODES.FAILURE);
1730
+ else if (ev.type === "finish") {
1731
+ out2.finishReason = ev.reason;
1732
+ if (ev.breach) out2.breach = ev.breach;
1733
+ }
1734
+ }
1735
+ } catch (err2) {
1736
+ this.emitter.agentEvent({ type: "error", message: describe3(err2) });
1737
+ out2.finishReason = "error";
1738
+ this.raise(EXIT_CODES.FAILURE);
1739
+ }
1740
+ if (controller.signal.aborted) out2.aborted = true;
1741
+ this.emitter.result(toResult(out2));
1742
+ const limited = limitExitCode(out2, this.limits);
1743
+ if (limited !== void 0) this.raise(limited);
1744
+ }
1745
+ /**
1746
+ * 收摊:stdin 关了,或者宿主发了 `close`。
1747
+ *
1748
+ * **默认让在跑的那一轮跑完**,不是一关就杀。EOF 的含义是「没有更多输入」,
1749
+ * 不是「把已经交代的活扔掉」—— 而 `echo '{"type":"user-message",…}' | epoch`
1750
+ * 这种一次性用法里,stdin 恰好在消息发出的下一刻就 EOF 了,杀掉的话
1751
+ * 这条命令永远只会吐一个 `aborted`。
1752
+ *
1753
+ * 唯一的例外是**有审批挂着**:那种情况下等下去是等一个永远不会来的答复。
1754
+ * 见 `reapLostHost()`。
1755
+ */
1756
+ async shutdown() {
1757
+ this.inputClosed = true;
1758
+ if (this.emitter.pendingApprovals > 0 || this.emitter.pendingQuestions > 0) {
1759
+ this.reapLostHost();
1760
+ }
1761
+ await this.drain();
1762
+ }
1763
+ /**
1764
+ * 宿主没了但还有审批 / 提问挂着:**放弃**它们 + 丢掉排队的 + 中止本轮。
1765
+ *
1766
+ * 逐个 `deny` 会让模型收到一串「用户拒绝」,于是它认为这是有意否决,
1767
+ * 换个方式再试一遍。提问那边逐个塞 `skipped` 是同一类错误的另一种形态:
1768
+ * 那等于说「用户看到了、选择不答」,而真相是宿主已经不在了。
1769
+ * 放弃 + 中止之后模型看到的是「这一轮被中止了」。
1770
+ * 这条规矩在 serialize.ts / hub.ts / 这里各守一遍,三处都不许改回去。
1771
+ *
1772
+ * 两个数分开报:排查「进程为什么退不掉」时,第一个问题就是「卡在哪种请求上」。
1773
+ */
1774
+ reapLostHost() {
1775
+ const approvals = this.emitter.abandonApprovals();
1776
+ if (approvals > 0) {
1777
+ this.warn(`[\u6536\u5C3E] \u653E\u5F03\u4E86 ${approvals} \u6761\u6CA1\u7B54\u590D\u7684\u5BA1\u6279\uFF0C\u672C\u8F6E\u4E2D\u6B62\uFF08\u4E0D\u6309\u62D2\u7EDD\u5904\u7406\uFF09`);
1778
+ }
1779
+ const questions = this.emitter.abandonQuestions();
1780
+ if (questions > 0) {
1781
+ this.warn(`[\u6536\u5C3E] \u653E\u5F03\u4E86 ${questions} \u6761\u6CA1\u7B54\u590D\u7684\u63D0\u95EE\uFF0C\u672C\u8F6E\u4E2D\u6B62\uFF08\u4E0D\u4F2A\u9020\u7B54\u6848\uFF09`);
1782
+ }
1783
+ this.queue.length = 0;
1784
+ this.controller?.abort();
1785
+ }
1786
+ /**
1787
+ * 等到一轮都不剩。
1788
+ *
1789
+ * 必须是循环:`pump()` 挂在上一轮的 `finally` 里,所以 `await this.turn` 回来时
1790
+ * 队列里的下一条可能已经开跑了。只 await 一次会漏掉后面全部。
1791
+ */
1792
+ async drain() {
1793
+ while (this.turn) await this.turn.catch(() => {
1794
+ });
1795
+ }
1796
+ exitCode() {
1797
+ if (this.sawBadInput) this.raise(EXIT_CODES.INPUT_ERROR);
1798
+ return this.code;
1799
+ }
1800
+ /** 记一个失败码。**先到先得** —— 后面那个通常是前面那个的后果 */
1801
+ raise(code) {
1802
+ if (this.code === EXIT_CODES.SUCCESS) this.code = code;
1803
+ }
1804
+ };
1805
+ function describe3(err2) {
1806
+ return err2 instanceof Error ? err2.message : String(err2);
1807
+ }
1808
+
1809
+ // src/headless/session.ts
1810
+ async function runStreamJsonSession(opts) {
1811
+ const { policy: headless, invalid } = parseHeadlessFlags(opts);
1812
+ if (invalid.length > 0) {
1813
+ throw new CliError(
1814
+ `--allow-operation \u4E0D\u8BA4\u8BC6: ${invalid.join(", ")}`,
1815
+ EXIT_CODES.FAILURE,
1816
+ `\u53EF\u9009\u503C: ${OPERATION_TYPES.join(" / ")}`
1817
+ );
1818
+ }
1819
+ const limits = parseCallLimits(opts);
1820
+ warnInertFlags(opts);
1821
+ const runtime = await buildRuntime({
1822
+ // 和 CLI 的其它入口一样显式传:Ctrl+C 要走 dispose() 关掉 SQLite 连接和
1823
+ // MCP 子进程,不能因为库的默认值是 false 就退回「硬杀进程」。
1824
+ // 守卫用例:cli/__tests__/signal-handlers.test.ts
1825
+ installSignalHandlers: true,
1826
+ // 见文件头第 3 条。**不传 onApprovalRequest** —— 让 runtime 用事件桥
1827
+ interactive: true,
1828
+ ...headless ? { headless } : {}
1829
+ });
1830
+ try {
1831
+ if (!runtime.session) return reportDeadRuntime(runtime);
1832
+ for (const d of runtime.diagnostics) process.stderr.write(`\xB7 ${d}
1833
+ `);
1834
+ return await driveStreamJson({
1835
+ session: runtime.session,
1836
+ init: initInfoOf(runtime, process.cwd()),
1837
+ limits
1838
+ });
1839
+ } finally {
1840
+ await runtime.dispose();
1841
+ }
1842
+ }
1843
+ function reportDeadRuntime(runtime) {
1844
+ const emitter = new HeadlessEmitter();
1845
+ emitter.init(initInfoOf(runtime, process.cwd()));
1846
+ const detail = runtime.diagnostics.join("; ");
1847
+ emitter.agentEvent({
1848
+ type: "error",
1849
+ message: `provider \u4E0D\u53EF\u7528\uFF0Cagent \u65E0\u6CD5\u542F\u52A8${detail ? `\uFF1A${detail}` : ""}`
1850
+ });
1851
+ emitter.result({ ok: false, reason: "error", text: "", turns: 0 });
1852
+ return EXIT_CODES.FAILURE;
1853
+ }
1854
+ function warnInertFlags(opts) {
1855
+ if ((opts.allowTool ?? []).length > 0 || (opts.allowOperation ?? []).length > 0) {
1856
+ process.stderr.write(
1857
+ '\xB7 --allow-tool / --allow-operation \u5728 --input-format stream-json \u4E0B\u4E0D\u53C2\u4E0E\u5224\u5B9A\uFF1A\u5BBF\u4E3B\u80FD\u7B54\u5BA1\u6279\uFF0C\u6240\u4EE5\u6BCF\u4E00\u6B21\u90FD\u4F1A\u95EE\u5B83\u3002\u8981\u7528\u9884\u6388\u6743\u5C31\u8D70 epoch "<\u4EFB\u52A1>" \u5355\u6B21\u95EE\u7B54\u90A3\u6761\u8DEF\n'
1858
+ );
1859
+ }
1860
+ if ((opts.image ?? []).length > 0) {
1861
+ process.stderr.write(
1862
+ "\xB7 -i/--image \u5728 --input-format stream-json \u4E0B\u88AB\u5FFD\u7565\uFF1A\u56FE\u7247\u8D70 user-message \u7684 content \u90E8\u4EF6\u6570\u7EC4\n"
1863
+ );
1864
+ }
1865
+ }
1866
+
1867
+ // src/update-check.ts
1868
+ import { spawn } from "child_process";
1869
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
1870
+ import { get } from "https";
1871
+ import { dirname as dirname2, join as join3 } from "path";
1872
+ import { fileURLToPath as fileURLToPath2 } from "url";
1873
+
1874
+ // src/version.ts
1875
+ import { readFileSync as readFileSync2 } from "fs";
1876
+ import { dirname, join as join2 } from "path";
1877
+ import { fileURLToPath } from "url";
1878
+
1879
+ // src/installation.ts
1880
+ import { execFileSync } from "child_process";
1881
+ import { existsSync as existsSync4, realpathSync } from "fs";
1882
+ import { join } from "path";
1883
+ import { normalizeForMatch } from "@epoch-agent/infra";
1884
+ var PATH_RULES = [
1885
+ {
1886
+ patterns: [
1887
+ "/.npm/_npx",
1888
+ "/npm/_npx",
1889
+ // Windows:npm 的 cache 默认在 `%LocalAppData%\npm-cache`(老文档写
1890
+ // `%AppData%\npm-cache`,两者都命中这一条),`_npx` 是它的子目录。
1891
+ // 缺了它,Windows 上 npx 临时运行会被兜底成「npm 全局安装」。
1892
+ "/npm-cache/_npx",
1893
+ "/.cache/pnpm/dlx",
1894
+ "/.pnpm/_pnpx",
1895
+ "/.bun/install/cache"
1896
+ ],
1897
+ build: () => ({
1898
+ packageManager: "npx",
1899
+ isGlobal: false,
1900
+ note: "\u901A\u8FC7 npx / pnpm dlx \u4E34\u65F6\u8FD0\u884C\uFF0C\u6BCF\u6B21\u90FD\u662F\u6700\u65B0\u7248\uFF0C\u65E0\u9700\u5347\u7EA7\u3002"
1901
+ })
1902
+ },
1903
+ {
1904
+ // Windows 上是 `%LocalAppData%\Volta`,小写化之后和 POSIX 的 `.volta` 同形
1905
+ patterns: ["/.volta/", "/volta/"],
1906
+ build: (pkg) => ({
1907
+ packageManager: "volta",
1908
+ isGlobal: true,
1909
+ updateCommand: `volta install ${pkg}@latest`,
1910
+ note: "\u901A\u8FC7 Volta \u5B89\u88C5\u3002"
1911
+ })
1912
+ },
1913
+ {
1914
+ patterns: [
1915
+ "/.pnpm/global",
1916
+ "/.local/share/pnpm",
1917
+ "/library/pnpm/global/",
1918
+ "/appdata/local/pnpm/global/"
1919
+ ],
1920
+ build: (pkg) => ({
1921
+ packageManager: "pnpm",
1922
+ isGlobal: true,
1923
+ updateCommand: `pnpm add -g ${pkg}@latest`,
1924
+ note: "\u901A\u8FC7 pnpm \u5168\u5C40\u5B89\u88C5\u3002"
1925
+ })
1926
+ },
1927
+ {
1928
+ patterns: [
1929
+ "/.yarn/global",
1930
+ "/yarn/global",
1931
+ // Windows:yarn 1.5.1 起全局目录多了一层,变成 `Yarn\Data\global`;
1932
+ // 更早的版本是 `Yarn\config\global`。两条都收,否则 yarn 全局装的用户
1933
+ // 会被告知去跑 `npm install -g`。
1934
+ "/yarn/data/global",
1935
+ "/yarn/config/global"
1936
+ ],
1937
+ build: (pkg) => ({
1938
+ packageManager: "yarn",
1939
+ isGlobal: true,
1940
+ updateCommand: `yarn global add ${pkg}@latest`,
1941
+ note: "\u901A\u8FC7 yarn \u5168\u5C40\u5B89\u88C5\u3002"
1942
+ })
1943
+ },
1944
+ {
1945
+ patterns: ["/.bun/install/global"],
1946
+ build: (pkg) => ({
1947
+ packageManager: "bun",
1948
+ isGlobal: true,
1949
+ updateCommand: `bun add -g ${pkg}@latest`,
1950
+ note: "\u901A\u8FC7 bun \u5168\u5C40\u5B89\u88C5\u3002"
1951
+ })
1952
+ }
1953
+ ];
1954
+ function detectHomebrew(realPath, formula) {
1955
+ if (process.platform !== "darwin") return null;
1956
+ const prefixes = [process.env.HOMEBREW_PREFIX, "/opt/homebrew", "/usr/local"].filter(
1957
+ Boolean
1958
+ );
1959
+ if (!prefixes.some((prefix) => realPath.startsWith(prefix))) return null;
1960
+ try {
1961
+ const prefix = execFileSync("brew", ["--prefix", formula], {
1962
+ encoding: "utf-8",
1963
+ stdio: ["ignore", "pipe", "ignore"]
1964
+ }).trim();
1965
+ if (prefix && realPath.startsWith(realpathSync(prefix))) {
1966
+ return {
1967
+ packageManager: "homebrew",
1968
+ isGlobal: true,
1969
+ updateCommand: `brew upgrade ${formula}`,
1970
+ note: "\u901A\u8FC7 Homebrew \u5B89\u88C5\u3002"
1971
+ };
1972
+ }
1973
+ } catch {
1974
+ }
1975
+ return null;
1976
+ }
1977
+ function isFromGitCheckout(realPath) {
1978
+ if (realPath.includes("/node_modules/")) return false;
1979
+ let dir = realPath;
1980
+ for (let depth = 0; depth < 8; depth += 1) {
1981
+ const parent = dir.slice(0, dir.lastIndexOf("/"));
1982
+ if (!parent || parent === dir) break;
1983
+ dir = parent;
1984
+ if (existsSync4(join(dir, ".git"))) return true;
1985
+ }
1986
+ return false;
1987
+ }
1988
+ function detectLocalInstall(matchPath, cwd) {
1989
+ const root = normalizeForMatch(cwd);
1990
+ if (!matchPath.startsWith(`${root}/node_modules`)) return null;
1991
+ const manager = existsSync4(join(cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync4(join(cwd, "yarn.lock")) ? "yarn" : existsSync4(join(cwd, "bun.lockb")) ? "bun" : "npm";
1992
+ return {
1993
+ packageManager: manager,
1994
+ isGlobal: false,
1995
+ note: "\u4F5C\u4E3A\u9879\u76EE\u4F9D\u8D56\u5B89\u88C5\uFF0C\u8BF7\u5728\u9879\u76EE\u7684 package.json \u91CC\u5347\u7EA7\u7248\u672C\u3002"
1996
+ };
1997
+ }
1998
+ function getInstallationInfo(packageName, cliPath = process.argv[1]) {
1999
+ if (!cliPath) return { packageManager: "unknown", isGlobal: false, note: "\u8BC6\u522B\u4E0D\u51FA\u5B89\u88C5\u65B9\u5F0F\u3002" };
2000
+ try {
2001
+ const realPath = realpathSync(cliPath).replace(/\\/g, "/");
2002
+ const matchPath = normalizeForMatch(realPath);
2003
+ for (const rule of PATH_RULES) {
2004
+ if (rule.patterns.some((pattern) => matchPath.includes(pattern)))
2005
+ return rule.build(packageName);
2006
+ }
2007
+ if (isFromGitCheckout(realPath)) {
2008
+ return {
2009
+ packageManager: "source",
2010
+ isGlobal: false,
2011
+ note: "\u76F4\u63A5\u4ECE\u4ED3\u5E93\u6E90\u7801\u8FD0\u884C\uFF0C\u7528 git pull \u66F4\u65B0\u3002"
2012
+ };
2013
+ }
2014
+ const brew = detectHomebrew(realPath, packageName.replace(/^@[^/]+\//, ""));
2015
+ if (brew) return brew;
2016
+ const local = detectLocalInstall(matchPath, process.cwd());
2017
+ if (local) return local;
2018
+ return {
2019
+ packageManager: "npm",
2020
+ isGlobal: true,
2021
+ updateCommand: `npm install -g ${packageName}@latest`,
2022
+ note: "\u901A\u8FC7 npm \u5168\u5C40\u5B89\u88C5\u3002"
2023
+ };
2024
+ } catch {
2025
+ return { packageManager: "unknown", isGlobal: false, note: "\u8BC6\u522B\u4E0D\u51FA\u5B89\u88C5\u65B9\u5F0F\u3002" };
2026
+ }
2027
+ }
2028
+
2029
+ // src/version.ts
2030
+ function readVersion() {
2031
+ const here = dirname(fileURLToPath(import.meta.url));
2032
+ for (const candidate of [
2033
+ join2(here, "..", "package.json"),
2034
+ join2(here, "..", "..", "package.json")
2035
+ ]) {
2036
+ try {
2037
+ const raw = JSON.parse(readFileSync2(candidate, "utf-8"));
2038
+ if (raw.version) return raw.version;
2039
+ } catch {
2040
+ }
2041
+ }
2042
+ return "0.0.0-unknown";
2043
+ }
2044
+ var VERSION = readVersion();
2045
+ function describeVersion(packageName) {
2046
+ const info = getInstallationInfo(packageName);
2047
+ return [
2048
+ VERSION,
2049
+ `Node: ${process.version} (${process.platform}/${process.arch})`,
2050
+ `\u5B89\u88C5: ${info.packageManager}${info.isGlobal ? "\uFF08\u5168\u5C40\uFF09" : ""} \u2014\u2014 ${info.note}`
2051
+ ].join("\n");
2052
+ }
2053
+
2054
+ // src/update-check.ts
2055
+ var UPDATE_CHECK_SUBCOMMAND = "__update-check";
2056
+ var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
2057
+ var REQUEST_TIMEOUT_MS = 5e3;
2058
+ var MAX_RESPONSE_BYTES = 64 * 1024;
2059
+ function parseVersion(raw) {
2060
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([\dA-Za-z.-]+))?(?:\+[\dA-Za-z.-]+)?$/.exec(raw.trim());
2061
+ if (!match) return null;
2062
+ return {
2063
+ parts: [Number(match[1]), Number(match[2]), Number(match[3])],
2064
+ pre: match[4] ?? null
2065
+ };
2066
+ }
2067
+ function comparePrerelease(a, b) {
2068
+ const left = a.split(".");
2069
+ const right = b.split(".");
2070
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
2071
+ const x = left[i];
2072
+ const y = right[i];
2073
+ if (x === void 0) return -1;
2074
+ if (y === void 0) return 1;
2075
+ const xn = /^\d+$/.test(x);
2076
+ const yn = /^\d+$/.test(y);
2077
+ if (xn && yn) {
2078
+ if (Number(x) !== Number(y)) return Number(x) - Number(y);
2079
+ } else if (xn !== yn) {
2080
+ return xn ? -1 : 1;
2081
+ } else if (x !== y) {
2082
+ return x < y ? -1 : 1;
2083
+ }
2084
+ }
2085
+ return 0;
2086
+ }
2087
+ function compareVersions(a, b) {
2088
+ const left = parseVersion(a);
2089
+ const right = parseVersion(b);
2090
+ if (!left || !right) return 0;
2091
+ for (let i = 0; i < 3; i += 1) {
2092
+ const x = left.parts[i] ?? 0;
2093
+ const y = right.parts[i] ?? 0;
2094
+ if (x !== y) return x - y;
2095
+ }
2096
+ if (left.pre === right.pre) return 0;
2097
+ if (left.pre === null) return 1;
2098
+ if (right.pre === null) return -1;
2099
+ return comparePrerelease(left.pre, right.pre);
2100
+ }
2101
+ function isUpdateCheckDisabled(env = process.env) {
2102
+ const flag = env.EPOCH_NO_UPDATE_CHECK ?? env.NO_UPDATE_NOTIFIER;
2103
+ if (flag !== void 0 && flag !== "" && flag !== "0" && flag !== "false") return true;
2104
+ return env.CI !== void 0 && env.CI !== "" && env.CI !== "false";
2105
+ }
2106
+ function defaultCachePath(home = EPOCH_HOME) {
2107
+ return join3(home, "update-check.json");
2108
+ }
2109
+ function selfPackage() {
2110
+ const here = dirname2(fileURLToPath2(import.meta.url));
2111
+ for (const candidate of [
2112
+ join3(here, "..", "package.json"),
2113
+ join3(here, "..", "..", "package.json")
2114
+ ]) {
2115
+ try {
2116
+ const raw = JSON.parse(readFileSync3(candidate, "utf-8"));
2117
+ if (raw.name) return { name: raw.name, version: raw.version ?? VERSION };
2118
+ } catch {
2119
+ }
2120
+ }
2121
+ return { name: "@epoch-agent/cli", version: VERSION };
2122
+ }
2123
+ function readCache(path) {
2124
+ try {
2125
+ const raw = JSON.parse(readFileSync3(path, "utf-8"));
2126
+ if (typeof raw.name !== "string" || typeof raw.checkedAt !== "number") return null;
2127
+ return {
2128
+ name: raw.name,
2129
+ latest: typeof raw.latest === "string" ? raw.latest : null,
2130
+ checkedAt: raw.checkedAt
2131
+ };
2132
+ } catch {
2133
+ return null;
2134
+ }
2135
+ }
2136
+ function writeCache(path, cache) {
2137
+ try {
2138
+ mkdirSync2(dirname2(path), { recursive: true });
2139
+ writeFileSync(path, `${JSON.stringify(cache)}
2140
+ `, "utf-8");
2141
+ } catch {
2142
+ }
2143
+ }
2144
+ function registryOf(env) {
2145
+ return (env.EPOCH_NPM_REGISTRY ?? env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/+$/, "");
2146
+ }
2147
+ function fetchLatestFromRegistry(name, registry) {
2148
+ const url = `${registry}/-/package/${name.replace("/", "%2F")}/dist-tags`;
2149
+ return new Promise((resolve4) => {
2150
+ const request = get(
2151
+ url,
2152
+ { timeout: REQUEST_TIMEOUT_MS, headers: { accept: "application/json" } },
2153
+ (response) => {
2154
+ if (response.statusCode !== 200) {
2155
+ response.resume();
2156
+ resolve4(null);
2157
+ return;
2158
+ }
2159
+ let body = "";
2160
+ response.setEncoding("utf-8");
2161
+ response.on("data", (chunk) => {
2162
+ body += chunk;
2163
+ if (body.length > MAX_RESPONSE_BYTES) request.destroy();
2164
+ });
2165
+ response.on("end", () => {
2166
+ try {
2167
+ const tags = JSON.parse(body);
2168
+ resolve4(typeof tags.latest === "string" ? tags.latest : null);
2169
+ } catch {
2170
+ resolve4(null);
2171
+ }
2172
+ });
2173
+ response.on("error", () => resolve4(null));
2174
+ }
2175
+ );
2176
+ request.on("timeout", () => request.destroy());
2177
+ request.on("error", () => resolve4(null));
2178
+ });
2179
+ }
2180
+ function readUpdateNotice(options = {}) {
2181
+ const env = options.env ?? process.env;
2182
+ if (isUpdateCheckDisabled(env)) return null;
2183
+ const self = selfPackage();
2184
+ const name = options.packageName ?? self.name;
2185
+ const version = options.currentVersion ?? self.version;
2186
+ const cache = readCache(options.cachePath ?? defaultCachePath());
2187
+ if (!cache || cache.name !== name || !cache.latest) return null;
2188
+ if (compareVersions(cache.latest, version) <= 0) return null;
2189
+ return {
2190
+ name,
2191
+ current: version,
2192
+ latest: cache.latest,
2193
+ message: `${name} \u6709\u65B0\u7248\u672C\uFF1A${version} \u2192 ${cache.latest}\uFF08\u8FD0\u884C epoch upgrade \u67E5\u770B\u5347\u7EA7\u65B9\u5F0F\uFF09`
2194
+ };
2195
+ }
2196
+ async function refreshUpdateCache(options = {}) {
2197
+ const env = options.env ?? process.env;
2198
+ if (isUpdateCheckDisabled(env)) return null;
2199
+ const self = selfPackage();
2200
+ const name = options.packageName ?? self.name;
2201
+ const version = options.currentVersion ?? self.version;
2202
+ const cachePath = options.cachePath ?? defaultCachePath();
2203
+ const now = options.now ?? Date.now();
2204
+ const ttl = options.ttlMs ?? UPDATE_CACHE_TTL_MS;
2205
+ const cache = readCache(cachePath);
2206
+ if (cache && cache.name === name && now - cache.checkedAt < ttl) {
2207
+ return readUpdateNotice({ ...options, cachePath, packageName: name, currentVersion: version });
2208
+ }
2209
+ const fetchLatest = options.fetchLatest ?? fetchLatestFromRegistry;
2210
+ let latest = null;
2211
+ try {
2212
+ latest = await fetchLatest(name, registryOf(env));
2213
+ } catch {
2214
+ latest = null;
2215
+ }
2216
+ writeCache(cachePath, { name, latest, checkedAt: now });
2217
+ if (!latest || compareVersions(latest, version) <= 0) return null;
2218
+ return {
2219
+ name,
2220
+ current: version,
2221
+ latest,
2222
+ message: `${name} \u6709\u65B0\u7248\u672C\uFF1A${version} \u2192 ${latest}\uFF08\u8FD0\u884C epoch upgrade \u67E5\u770B\u5347\u7EA7\u65B9\u5F0F\uFF09`
2223
+ };
2224
+ }
2225
+ function scheduleUpdateCheck(options = {}) {
2226
+ const env = options.env ?? process.env;
2227
+ if (isUpdateCheckDisabled(env)) return;
2228
+ if (process.argv.includes(UPDATE_CHECK_SUBCOMMAND)) return;
2229
+ const cachePath = options.cachePath ?? defaultCachePath();
2230
+ const now = options.now ?? Date.now();
2231
+ const cache = readCache(cachePath);
2232
+ if (cache && now - cache.checkedAt < (options.ttlMs ?? UPDATE_CACHE_TTL_MS)) return;
2233
+ const self = process.argv[1];
2234
+ if (!self || !existsSync5(self)) return;
2235
+ try {
2236
+ spawn(process.execPath, [self, UPDATE_CHECK_SUBCOMMAND], {
2237
+ detached: true,
2238
+ stdio: "ignore",
2239
+ env: { ...env, EPOCH_UPDATE_CHECK_CHILD: "1" }
2240
+ }).unref();
2241
+ } catch {
2242
+ }
2243
+ }
2244
+ function notifyUpdateOnExit(options = {}) {
2245
+ process.on("exit", () => {
2246
+ try {
2247
+ const notice = readUpdateNotice(options);
2248
+ if (notice) process.stderr.write(`
2249
+ ${notice.message}
2250
+ `);
2251
+ } catch {
2252
+ }
2253
+ });
2254
+ }
2255
+
2256
+ // src/worktree.ts
2257
+ import { execFile } from "child_process";
2258
+ import { randomBytes } from "crypto";
2259
+ import { mkdirSync as mkdirSync3 } from "fs";
2260
+ import { basename, join as join4, resolve as resolve2 } from "path";
2261
+ import { worktreesDir } from "@epoch-agent/infra";
2262
+ var TIMEOUT_MS = 6e4;
2263
+ function git(args, cwd) {
2264
+ return new Promise((done) => {
2265
+ execFile(
2266
+ "git",
2267
+ args,
2268
+ { cwd, timeout: TIMEOUT_MS, encoding: "utf-8", windowsHide: true },
2269
+ (err2, stdout, stderr) => {
2270
+ const code = err2?.code;
2271
+ done({
2272
+ ok: !err2,
2273
+ stdout,
2274
+ stderr: stderr || "",
2275
+ missing: code === "ENOENT"
2276
+ });
2277
+ }
2278
+ );
2279
+ });
2280
+ }
2281
+ function firstLine(text) {
2282
+ return text.trim().split("\n")[0]?.trim() ?? "\u672A\u77E5\u539F\u56E0";
2283
+ }
2284
+ async function createWorktree(cwd = process.cwd(), container = worktreesDir()) {
2285
+ const repoRoot = await resolveRepoRoot(cwd);
2286
+ const head = await git(["rev-parse", "HEAD"], repoRoot);
2287
+ if (!head.ok) {
2288
+ throw new CliError(
2289
+ "\u8FD9\u4E2A\u4ED3\u5E93\u8FD8\u6CA1\u6709\u4EFB\u4F55\u63D0\u4EA4\uFF0C\u5EFA\u4E0D\u51FA worktree",
2290
+ 1,
2291
+ "worktree \u5FC5\u987B\u4ECE\u67D0\u4E2A commit \u957F\u51FA\u6765\u3002\u5148 git commit \u4E00\u6B21\uFF0C\u6216\u8005\u53BB\u6389 --worktree"
2292
+ );
2293
+ }
2294
+ const baseRef = head.stdout.trim();
2295
+ const shortId = randomBytes(3).toString("hex");
2296
+ const branch = `epoch/${shortId}`;
2297
+ const path = join4(container, `${basename(repoRoot)}-${shortId}`);
2298
+ mkdirSync3(container, { recursive: true });
2299
+ const added = await git(["worktree", "add", "-b", branch, path, baseRef], repoRoot);
2300
+ if (!added.ok) {
2301
+ throw new CliError(`\u5EFA worktree \u5931\u8D25\uFF1A${path}`, 1, firstLine(added.stderr));
2302
+ }
2303
+ return { repoRoot, path, branch, baseRef };
2304
+ }
2305
+ async function resolveRepoRoot(cwd) {
2306
+ const root = await git(["rev-parse", "--show-toplevel"], cwd);
2307
+ if (root.missing) {
2308
+ throw new CliError(
2309
+ "--worktree \u8981\u7528 git\uFF0C\u4F46\u5B83\u4E0D\u5728 PATH \u91CC",
2310
+ 1,
2311
+ "\u88C5\u4E00\u4E2A git\uFF0C\u6216\u8005\u53BB\u6389 --worktree"
2312
+ );
2313
+ }
2314
+ if (!root.ok) {
2315
+ throw new CliError(
2316
+ "--worktree \u53EA\u80FD\u5728 git \u4ED3\u5E93\u91CC\u7528",
2317
+ 1,
2318
+ `\u5F53\u524D\u76EE\u5F55 ${cwd} \u4E0D\u5728\u4EFB\u4F55 git \u4ED3\u5E93\u91CC\uFF08${firstLine(root.stderr)}\uFF09`
2319
+ );
2320
+ }
2321
+ return resolve2(root.stdout.trim());
2322
+ }
2323
+ function countLines(text) {
2324
+ return text.split("\n").filter((line) => line.trim() !== "").length;
2325
+ }
2326
+ async function inspectWorktree(session) {
2327
+ const status = await git(["status", "--porcelain"], session.path);
2328
+ const ahead = await git(["rev-list", "--count", `${session.baseRef}..HEAD`], session.path);
2329
+ const count = Number(ahead.stdout.trim());
2330
+ return {
2331
+ dirty: status.ok ? countLines(status.stdout) : null,
2332
+ ahead: ahead.ok && Number.isFinite(count) ? count : null
2333
+ };
2334
+ }
2335
+ function worktreeKeepReason(state) {
2336
+ if (state.dirty === null || state.ahead === null) return "\u67E5\u4E0D\u51FA\u5B83\u5E72\u51C0\u4E0D\u5E72\u51C0";
2337
+ if (state.dirty > 0) return `\u8FD8\u6709 ${state.dirty} \u5904\u672A\u63D0\u4EA4\u6539\u52A8`;
2338
+ if (state.ahead > 0) return `\u6709 ${state.ahead} \u4E2A\u63D0\u4EA4\u8FD8\u6CA1\u5408\u56DE\u53BB`;
2339
+ return null;
2340
+ }
2341
+ function worktreeBanner(session) {
2342
+ return `
2343
+ \u{1F33F} \u5728\u9694\u79BB\u7684 worktree \u91CC\u8DD1\u8FD9\u4E00\u6B21
2344
+ \u76EE\u5F55 ${session.path}
2345
+ \u5206\u652F ${session.branch}
2346
+ `;
2347
+ }
2348
+ async function finishWorktree(session, opts) {
2349
+ const print = opts.print ?? ((msg) => process.stderr.write(msg + "\n"));
2350
+ const state = await inspectWorktree(session);
2351
+ const forced = worktreeKeepReason(state);
2352
+ if (forced !== null) {
2353
+ print(`
2354
+ \u{1F33F} worktree \u4FDD\u7559\uFF08${forced}\uFF09\uFF1A
2355
+ ${session.path}
2356
+ \u5206\u652F ${session.branch}`);
2357
+ return;
2358
+ }
2359
+ const remove = opts.interactive ? await (opts.ask ?? askRemove)(session) : false;
2360
+ if (!remove) {
2361
+ print(`
2362
+ \u{1F33F} worktree \u4FDD\u7559\u5728 ${session.path}\uFF08\u5206\u652F ${session.branch}\uFF09`);
2363
+ return;
2364
+ }
2365
+ const failure = await removeWorktree(session);
2366
+ print(
2367
+ failure === null ? `
2368
+ \u{1F33F} \u5DF2\u5220\u9664 worktree ${session.path}\uFF08\u5206\u652F ${session.branch}\uFF09` : `
2369
+ \u26A0 worktree \u6CA1\u5220\u5E72\u51C0\uFF0C\u7559\u7740\u4E86\uFF1A${session.path}
2370
+ ${failure}`
2371
+ );
2372
+ }
2373
+ async function removeWorktree(session) {
2374
+ const removed = await git(["worktree", "remove", session.path], session.repoRoot);
2375
+ if (!removed.ok) return firstLine(removed.stderr);
2376
+ const branch = await git(["branch", "-D", session.branch], session.repoRoot);
2377
+ return branch.ok ? null : `\u76EE\u5F55\u5220\u4E86\uFF0C\u5206\u652F ${session.branch} \u6CA1\u5220\u6389\uFF1A${firstLine(branch.stderr)}`;
2378
+ }
2379
+ async function askRemove(session) {
2380
+ const { confirm } = await import("@inquirer/prompts");
2381
+ try {
2382
+ return await confirm({
2383
+ message: `worktree ${session.path} \u6CA1\u6709\u4EFB\u4F55\u6539\u52A8\uFF0C\u5220\u6389\u5B83\u5417\uFF1F`,
2384
+ default: false
2385
+ });
2386
+ } catch {
2387
+ return false;
2388
+ }
2389
+ }
2390
+
2391
+ // src/commands/run-once.ts
2392
+ import { formatUsd, imageFromPath, OPERATION_TYPES as OPERATION_TYPES2 } from "@epoch-agent/core";
2393
+ import {
2394
+ promptTokens
2395
+ } from "@epoch-agent/protocol";
2396
+ import { buildRuntime as buildRuntime2, listBackgroundTasks } from "@epoch-agent/runtime";
2397
+
2398
+ // src/approval.ts
2399
+ import {
2400
+ isNonInteractive as isNonInteractive3,
2401
+ PLAN_OUTCOME_LABELS
2402
+ } from "@epoch-agent/core";
2403
+ var TYPE_LABEL = {
2404
+ file_read: "\u8BFB\u6587\u4EF6",
2405
+ file_write: "\u5199\u6587\u4EF6",
2406
+ command: "\u6267\u884C\u547D\u4EE4",
2407
+ network: "\u7F51\u7EDC\u8BF7\u6C42",
2408
+ code_exec: "\u5728\u6C99\u7BB1\u5185\u6267\u884C\u4EE3\u7801"
2409
+ };
2410
+ function formatApprovalRequest(req) {
2411
+ if (req.plan) {
2412
+ return [
2413
+ "\u25A3 \u6A21\u578B\u4EA4\u4E86\u4E00\u4EFD\u8BA1\u5212\uFF0C\u7B49\u4F60\u6279",
2414
+ ` \u8FDB\u5165 plan \u6A21\u5F0F\u524D\u7684\u6743\u9650\u7EA7\u522B: ${req.plan.previousLevel}`,
2415
+ "",
2416
+ ...req.plan.markdown.split("\n").map((l) => ` ${l}`)
2417
+ ];
2418
+ }
2419
+ const lines = [
2420
+ `\u26A0\uFE0F ${req.toolName} \u8BF7\u6C42${TYPE_LABEL[req.type]}`,
2421
+ ` \u76EE\u6807: ${req.target || "(\u65E0)"}`
2422
+ ];
2423
+ if (req.detail && req.detail !== req.target) {
2424
+ lines.push(` \u8BE6\u60C5: ${req.detail.slice(0, 300)}`);
2425
+ }
2426
+ if (req.reason) lines.push(` \u539F\u56E0: ${req.reason}`);
2427
+ return lines;
2428
+ }
2429
+ function createInteractiveApproval(opts = {}) {
2430
+ const interactive = opts.isInteractive ?? (() => !isNonInteractive3());
2431
+ return async (req) => {
2432
+ if (!interactive()) {
2433
+ if (opts.permission?.()?.isPreauthorized(req.toolName, req.type)) {
2434
+ console.error(`[\u5DF2\u653E\u884C] headless \u9884\u6388\u6743: ${req.toolName} ${req.target}`);
2435
+ return "allow-once";
2436
+ }
2437
+ console.error(
2438
+ `[\u5DF2\u62D2\u7EDD] \u975E\u4EA4\u4E92\u73AF\u5883\u65E0\u6CD5\u786E\u8BA4: ${req.toolName} ${req.target}
2439
+ \u5982\u9700\u653E\u884C\uFF0C\u52A0 --allow-tool ${req.toolName}\u3001\u5728 config.yaml \u91CC\u914D headless.allowTools\uFF0C\u6216\u5728 ~/.epoch/policies/ \u52A0\u7B56\u7565\u89C4\u5219\u3002`
2440
+ );
2441
+ return "deny";
2442
+ }
2443
+ console.log("\n" + formatApprovalRequest(req).join("\n"));
2444
+ if (req.plan) return await askPlan(req.plan);
2445
+ const { select: select2 } = await import("@inquirer/prompts");
2446
+ try {
2447
+ return await select2({
2448
+ message: "\u662F\u5426\u5141\u8BB8\uFF1F",
2449
+ choices: [
2450
+ { name: "\u5141\u8BB8\u4E00\u6B21", value: "allow-once" },
2451
+ { name: "\u672C\u4F1A\u8BDD\u5185\u59CB\u7EC8\u5141\u8BB8", value: "allow-session" },
2452
+ { name: "\u6C38\u4E45\u5141\u8BB8\uFF08\u5199\u5165 approvals.json\uFF09", value: "allow-always" },
2453
+ { name: "\u62D2\u7EDD", value: "deny" }
2454
+ ],
2455
+ default: "allow-once"
2456
+ });
2457
+ } catch {
2458
+ return "deny";
2459
+ }
2460
+ };
2461
+ }
2462
+ async function askPlan(proposal) {
2463
+ const { input: input2, select: select2 } = await import("@inquirer/prompts");
2464
+ const choices = [
2465
+ ...proposal.canExecute ? [
2466
+ {
2467
+ name: `${PLAN_OUTCOME_LABELS["plan-execute"]}\uFF08\u6743\u9650\u56DE\u5230 ${proposal.previousLevel}\uFF09`,
2468
+ value: "plan-execute"
2469
+ }
2470
+ ] : [],
2471
+ { name: PLAN_OUTCOME_LABELS["plan-readonly"], value: "plan-readonly" },
2472
+ { name: PLAN_OUTCOME_LABELS["plan-revise"], value: "plan-revise" },
2473
+ { name: PLAN_OUTCOME_LABELS.deny, value: "deny" }
2474
+ ];
2475
+ try {
2476
+ const outcome = await select2({ message: "\u8FD9\u4EFD\u8BA1\u5212\uFF1F", choices });
2477
+ if (outcome !== "plan-revise") return outcome;
2478
+ const note = (await input2({ message: "\u8981\u6539\u4EC0\u4E48\uFF1F" })).trim();
2479
+ return note ? { outcome, note } : outcome;
2480
+ } catch {
2481
+ return "deny";
2482
+ }
2483
+ }
2484
+
2485
+ // src/headless/approver.ts
2486
+ import { spawn as spawn2 } from "child_process";
2487
+ import {
2488
+ isNonInteractive as isNonInteractive4,
2489
+ matchPreauthorization,
2490
+ mergeHeadlessPolicy
2491
+ } from "@epoch-agent/core";
2492
+ import { APPROVAL_OUTCOMES as APPROVAL_OUTCOMES2, isApprovalOutcome as isApprovalOutcome2 } from "@epoch-agent/protocol";
2493
+ var APPROVER_TIMEOUT_MS = 6e4;
2494
+ var MAX_STDOUT_BYTES = 64 * 1024;
2495
+ var CLIP = 200;
2496
+ function clip(s) {
2497
+ return s.length > CLIP ? `${s.slice(0, CLIP)}\u2026` : s;
2498
+ }
2499
+ function parseApproverVerdict(stdout) {
2500
+ const text = stdout.trim();
2501
+ if (!text) return { ok: false, why: "\u5BA1\u6279\u7A0B\u5E8F\u6CA1\u6709\u8F93\u51FA" };
2502
+ let raw;
2503
+ try {
2504
+ raw = JSON.parse(text);
2505
+ } catch {
2506
+ return { ok: false, why: `\u5BA1\u6279\u7A0B\u5E8F\u7684\u8F93\u51FA\u4E0D\u662F JSON: ${clip(text)}` };
2507
+ }
2508
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2509
+ return { ok: false, why: `\u5BA1\u6279\u7A0B\u5E8F\u7684\u8F93\u51FA\u4E0D\u662F\u4E00\u4E2A JSON \u5BF9\u8C61: ${clip(text)}` };
2510
+ }
2511
+ const rec = raw;
2512
+ const outcome = rec["outcome"];
2513
+ if (typeof outcome !== "string" || !isApprovalOutcome2(outcome)) {
2514
+ return {
2515
+ ok: false,
2516
+ why: `\u5BA1\u6279\u7A0B\u5E8F\u7ED9\u7684 outcome \u4E0D\u8BA4\u8BC6: ${clip(String(outcome))}\uFF08\u53EF\u9009: ${APPROVAL_OUTCOMES2.join(" / ")}\uFF09`
2517
+ };
2518
+ }
2519
+ const note = rec["note"];
2520
+ return {
2521
+ ok: true,
2522
+ answer: { outcome, ...typeof note === "string" && note ? { note } : {} }
2523
+ };
2524
+ }
2525
+ function preauthorizedBy(configPolicy, cliPolicy, req) {
2526
+ if (!isNonInteractive4()) return null;
2527
+ return matchPreauthorization(mergeHeadlessPolicy(configPolicy, cliPolicy), {
2528
+ type: req.type,
2529
+ toolName: req.toolName
2530
+ });
2531
+ }
2532
+ function askExternalApprover(program2, payload, opts = {}) {
2533
+ const timeoutMs = opts.timeoutMs ?? APPROVER_TIMEOUT_MS;
2534
+ return new Promise((resolve4) => {
2535
+ let settled = false;
2536
+ let out2 = "";
2537
+ const settle = (v) => {
2538
+ if (settled) return;
2539
+ settled = true;
2540
+ resolve4(v);
2541
+ };
2542
+ const child = spawn2(program2, {
2543
+ shell: true,
2544
+ ...opts.cwd ? { cwd: opts.cwd } : {},
2545
+ // stdin/stdout 是这条问答的通道,stderr 让给它自己打日志
2546
+ stdio: ["pipe", "pipe", "inherit"]
2547
+ });
2548
+ const timer = setTimeout(() => {
2549
+ child.kill("SIGKILL");
2550
+ settle({ ok: false, why: `\u5BA1\u6279\u7A0B\u5E8F ${timeoutMs}ms \u5185\u6CA1\u6709\u7B54\u590D` });
2551
+ }, timeoutMs);
2552
+ const done = (v) => {
2553
+ clearTimeout(timer);
2554
+ settle(v);
2555
+ };
2556
+ child.stdout?.on("data", (d) => {
2557
+ out2 += d.toString("utf-8");
2558
+ if (out2.length > MAX_STDOUT_BYTES) {
2559
+ child.kill("SIGKILL");
2560
+ done({ ok: false, why: `\u5BA1\u6279\u7A0B\u5E8F\u8F93\u51FA\u8D85\u8FC7 ${MAX_STDOUT_BYTES} \u5B57\u8282\u8FD8\u6CA1\u6536\u5C3E` });
2561
+ }
2562
+ });
2563
+ child.on("error", (err2) => {
2564
+ done({ ok: false, why: `\u5BA1\u6279\u7A0B\u5E8F\u8D77\u4E0D\u6765: ${err2.message}` });
2565
+ });
2566
+ child.on("close", (code) => {
2567
+ if (code !== 0) {
2568
+ done({ ok: false, why: `\u5BA1\u6279\u7A0B\u5E8F\u9000\u51FA\u7801 ${code ?? -1}` });
2569
+ return;
2570
+ }
2571
+ done(parseApproverVerdict(out2));
2572
+ });
2573
+ child.stdin?.on("error", () => {
2574
+ });
2575
+ child.stdin?.end(JSON.stringify(payload));
2576
+ });
2577
+ }
2578
+ function payloadOf(req, cwd, sessionId) {
2579
+ return {
2580
+ version: 1,
2581
+ ...sessionId ? { sessionId } : {},
2582
+ cwd,
2583
+ toolName: req.toolName,
2584
+ type: req.type,
2585
+ target: req.target,
2586
+ detail: req.detail,
2587
+ ...req.reason ? { reason: req.reason } : {},
2588
+ ...req.plan ? { plan: req.plan } : {}
2589
+ };
2590
+ }
2591
+ function createExternalApproval(opts) {
2592
+ const write = opts.write ?? ((s) => void process.stderr.write(s));
2593
+ const cwd = opts.cwd ?? process.cwd();
2594
+ const ask = opts.ask ?? ((p) => askExternalApprover(opts.program, p, {
2595
+ cwd,
2596
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
2597
+ }));
2598
+ return async (req) => {
2599
+ const hit = req.plan ? null : opts.preauth?.(req) ?? null;
2600
+ if (hit) {
2601
+ write(`[\u9884\u6388\u6743\u653E\u884C] ${req.toolName} ${req.target} \u2014\u2014 ${hit}\uFF08\u6CA1\u8D77\u5BA1\u6279\u7A0B\u5E8F\uFF09
2602
+ `);
2603
+ return "allow-once";
2604
+ }
2605
+ const verdict = await ask(payloadOf(req, cwd, opts.sessionId?.()));
2606
+ if (!verdict.ok) {
2607
+ write(`[\u5916\u90E8\u5BA1\u6279\u62D2\u7EDD] ${req.toolName} ${req.target} \u2014\u2014 ${verdict.why}\uFF0C\u6309\u62D2\u7EDD\u5904\u7406
2608
+ `);
2609
+ return "deny";
2610
+ }
2611
+ const { outcome } = verdict.answer;
2612
+ const verb = outcome === "deny" ? "\u62D2\u7EDD" : "\u653E\u884C";
2613
+ write(`[\u5916\u90E8\u5BA1\u6279${verb}] ${req.toolName} ${req.target} \u2014\u2014 ${outcome}
2614
+ `);
2615
+ return verdict.answer;
2616
+ };
2617
+ }
2618
+
2619
+ // src/headless/tasks.ts
2620
+ import { describeBackgroundTask } from "@epoch-agent/core";
2621
+ function backgroundTaskSummary(tasks) {
2622
+ if (tasks.length === 0) return "";
2623
+ const running = tasks.filter((t14) => t14.status === "running").length;
2624
+ const head = running > 0 ? `\u2699 \u540E\u53F0\u4EFB\u52A1 ${tasks.length} \u4E2A\uFF0C\u5176\u4E2D ${running} \u4E2A\u8FD8\u5728\u8DD1 \u2014\u2014 epoch \u9000\u51FA\u65F6\u4F1A\u4E00\u5E76\u7EC8\u6B62` : `\u2699 \u540E\u53F0\u4EFB\u52A1 ${tasks.length} \u4E2A\uFF0C\u90FD\u5DF2\u7ECF\u7ED3\u675F`;
2625
+ return [head, ...tasks.map((t14) => ` ${describeBackgroundTask(t14)}`)].join("\n");
2626
+ }
2627
+
2628
+ // src/import-prompt.ts
2629
+ import {
2630
+ createImportGate,
2631
+ ImportTrustStore,
2632
+ isNonInteractive as isNonInteractive6,
2633
+ loadConfig as loadConfig2,
2634
+ resolveProjectRoot as resolveProjectRoot2,
2635
+ scanInstructions,
2636
+ setImportGate,
2637
+ TrustManager as TrustManager2
2638
+ } from "@epoch-agent/core";
2639
+ import { trustedImportsPath, trustPath as trustPath2 } from "@epoch-agent/infra";
2640
+
2641
+ // src/trust-prompt.ts
2642
+ import {
2643
+ findProjectInstructions,
2644
+ isNonInteractive as isNonInteractive5,
2645
+ loadConfig,
2646
+ resolveProjectRoot,
2647
+ TrustManager
2648
+ } from "@epoch-agent/core";
2649
+ import { trustPath } from "@epoch-agent/infra";
2650
+ function trustPromptTarget(deps) {
2651
+ if (!deps.gateEnabled) return null;
2652
+ if (!deps.interactive) return null;
2653
+ const root = resolveProjectRoot(deps.workDir);
2654
+ if (deps.store.check(root) !== "unknown") return null;
2655
+ const instructionsPath = findProjectInstructions(root, deps.workDir);
2656
+ if (!instructionsPath) return null;
2657
+ return { root, instructionsPath };
2658
+ }
2659
+ function applyTrustChoice(store, root, choice) {
2660
+ if (choice.kind === "trust") {
2661
+ store.record(root, "trusted", choice.scope);
2662
+ const suffix = choice.scope === "directory-tree" ? "\uFF08\u542B\u5B50\u76EE\u5F55\uFF09" : "";
2663
+ return `\u2713 \u5DF2\u4FE1\u4EFB ${root}${suffix}\uFF0C\u672C\u76EE\u5F55\u7684\u9879\u76EE\u6307\u4EE4\u4F1A\u52A0\u8F7D`;
2664
+ }
2665
+ if (choice.kind === "never") {
2666
+ store.record(root, "untrusted", "directory");
2667
+ return `\u2717 \u5DF2\u8BB0\u4E3A\u4E0D\u4FE1\u4EFB ${root}\uFF0C\u4E4B\u540E\u4E0D\u518D\u8BE2\u95EE\uFF08epoch trust rm ${root} \u53EF\u64A4\u9500\uFF09`;
2668
+ }
2669
+ return "\u672C\u6B21\u4E0D\u52A0\u8F7D\u9879\u76EE\u6307\u4EE4\u3002\u60F3\u8BA9\u5B83\u751F\u6548\u8FD0\u884C epoch trust add";
2670
+ }
2671
+ function trustPromptMessage(target) {
2672
+ return [
2673
+ `
2674
+ \u26A0 \u68C0\u6D4B\u5230\u9879\u76EE\u6307\u4EE4\u6587\u4EF6\uFF1A${target.instructionsPath}`,
2675
+ " \u5B83\u7531\u4ED3\u5E93\u4F5C\u8005\u7F16\u5199\uFF0C\u4E00\u65E6\u52A0\u8F7D\u5C31\u4F1A\u76F4\u63A5\u8FDB\u5165 system prompt\uFF0C\u7B49\u4E8E\u8BA9\u8FD9\u4E2A\u4ED3\u5E93",
2676
+ " \u6307\u6325\u4F60\u7684 agent\u3002\u6240\u4EE5\u672A\u7ECF\u786E\u8BA4\u4E0D\u4F1A\u52A0\u8F7D\u3002",
2677
+ ""
2678
+ ].join("\n");
2679
+ }
2680
+ var CHOICES = [
2681
+ { name: "\u672C\u6B21\u4E0D\u52A0\u8F7D\uFF08\u4E0B\u6B21\u8FD8\u4F1A\u95EE\uFF09", value: { kind: "skip" } },
2682
+ { name: `\u4FE1\u4EFB\u672C\u76EE\u5F55`, value: { kind: "trust", scope: "directory" } },
2683
+ { name: "\u4FE1\u4EFB\u672C\u76EE\u5F55\u53CA\u5176\u6240\u6709\u5B50\u76EE\u5F55", value: { kind: "trust", scope: "directory-tree" } },
2684
+ { name: "\u6C38\u4E0D\u4FE1\u4EFB\u672C\u76EE\u5F55\uFF08\u4E0D\u518D\u8BE2\u95EE\uFF09", value: { kind: "never" } }
2685
+ ];
2686
+ function restoreStdinDefault() {
2687
+ const stdin = process.stdin;
2688
+ if (stdin.isTTY) stdin.setRawMode?.(false);
2689
+ stdin.resume();
2690
+ }
2691
+ async function maybePromptForTrust(opts = {}) {
2692
+ const config = opts.config ?? loadConfig();
2693
+ const store = opts.store ?? new TrustManager(trustPath(config.homeDir));
2694
+ const print = opts.print ?? ((msg) => process.stdout.write(msg + "\n"));
2695
+ const target = trustPromptTarget({
2696
+ store,
2697
+ interactive: (opts.isInteractive ?? (() => !isNonInteractive5()))(),
2698
+ gateEnabled: config.trust?.enabled !== false,
2699
+ workDir: opts.workDir ?? process.cwd()
2700
+ });
2701
+ if (!target) return true;
2702
+ print(trustPromptMessage(target));
2703
+ let choice;
2704
+ try {
2705
+ choice = await (opts.ask ?? askTrustChoice)(target);
2706
+ } finally {
2707
+ (opts.restoreStdin ?? restoreStdinDefault)();
2708
+ }
2709
+ if (choice === null) return false;
2710
+ try {
2711
+ print(applyTrustChoice(store, target.root, choice));
2712
+ } catch (err2) {
2713
+ print(`\u26A0 \u4FE1\u4EFB\u8BB0\u5F55\u5199\u5165\u5931\u8D25\uFF0C\u672C\u6B21\u4ECD\u6309\u4E0D\u4FE1\u4EFB\u5904\u7406\uFF1A${err2 instanceof Error ? err2.message : err2}`);
2714
+ }
2715
+ return true;
2716
+ }
2717
+ async function askTrustChoice(_target) {
2718
+ const { select: select2 } = await import("@inquirer/prompts");
2719
+ try {
2720
+ return await select2({
2721
+ message: "\u662F\u5426\u4FE1\u4EFB\u672C\u76EE\u5F55\uFF1F",
2722
+ choices: [...CHOICES],
2723
+ default: CHOICES[0]?.value
2724
+ });
2725
+ } catch {
2726
+ return null;
2727
+ }
2728
+ }
2729
+
2730
+ // src/import-prompt.ts
2731
+ function externalImportTargets(deps) {
2732
+ if (!deps.gateEnabled) return [];
2733
+ if (!deps.interactive) return [];
2734
+ if (deps.store.deniedAll) return [];
2735
+ const root = resolveProjectRoot2(deps.workDir);
2736
+ if (deps.trust.check(root) !== "trusted") return [];
2737
+ const scan = scanInstructions(root, deps.workDir, {
2738
+ allowExternal: (p) => deps.store.isAllowed(p)
2739
+ });
2740
+ const out2 = [];
2741
+ const seen = /* @__PURE__ */ new Set();
2742
+ for (const src of scan.sources) {
2743
+ for (const node of src.imports) {
2744
+ if (node.skipped !== "external-denied") continue;
2745
+ if (seen.has(node.path)) continue;
2746
+ seen.add(node.path);
2747
+ out2.push({ path: node.path, from: src.path, spec: node.spec });
2748
+ }
2749
+ }
2750
+ return out2;
2751
+ }
2752
+ function importPromptMessage(target) {
2753
+ return [
2754
+ `
2755
+ \u26A0 ${target.from} \u5F15\u7528\u4E86\u9879\u76EE\u4E4B\u5916\u7684\u6587\u4EF6\uFF1A`,
2756
+ ` ${target.path}`,
2757
+ ` \uFF08\u6587\u4EF6\u91CC\u5199\u7684\u662F @${target.spec}\uFF09`,
2758
+ " \u5B83\u4E0D\u5728\u8FD9\u4E2A\u4ED3\u5E93\u91CC\uFF0C\u4E5F\u6CA1\u6709\u88AB\u300C\u4FE1\u4EFB\u8FD9\u4E2A\u76EE\u5F55\u300D\u90A3\u6B21\u5224\u5B9A\u8986\u76D6\u8FC7\u3002",
2759
+ " \u52A0\u8F7D\u5B83\u7B49\u4E8E\u628A\u4E00\u4EFD\u6CA1\u88AB\u5BA1\u8FC7\u7684\u5185\u5BB9\u62FC\u8FDB system prompt\u3002",
2760
+ ""
2761
+ ].join("\n");
2762
+ }
2763
+ function applyImportChoice(store, target, choice) {
2764
+ if (choice.kind === "once") {
2765
+ store.allowOnce(target.path);
2766
+ return `\u2713 \u672C\u6B21\u52A0\u8F7D ${target.path}\uFF08\u4E0B\u6B21\u8FD8\u4F1A\u95EE\uFF09`;
2767
+ }
2768
+ if (choice.kind === "remember") {
2769
+ store.remember(target.path);
2770
+ return `\u2713 \u5DF2\u8BB0\u4F4F ${target.path}\uFF0C\u4E4B\u540E\u4E0D\u518D\u8BE2\u95EE`;
2771
+ }
2772
+ if (choice.kind === "never") {
2773
+ store.denyAll();
2774
+ return "\u2717 \u4E4B\u540E\u4E00\u5F8B\u4E0D\u52A0\u8F7D\u9879\u76EE\u5916\u7684 import\uFF08\u6539\u8FD9\u4E2A\u51B3\u5B9A\uFF1A\u5220\u6389 ~/.epoch/trusted-imports.json\uFF09";
2775
+ }
2776
+ return `\u672C\u6B21\u4E0D\u52A0\u8F7D ${target.path}`;
2777
+ }
2778
+ var CHOICES2 = [
2779
+ { name: "\u672C\u6B21\u4E0D\u52A0\u8F7D\uFF08\u4E0B\u6B21\u8FD8\u4F1A\u95EE\uFF09", value: { kind: "skip" } },
2780
+ { name: "\u52A0\u8F7D\u8FD9\u4E00\u4E2A\u6587\u4EF6", value: { kind: "once" } },
2781
+ { name: "\u52A0\u8F7D\uFF0C\u5E76\u8BB0\u4F4F\u8FD9\u4E2A\u8DEF\u5F84\uFF08\u4E0B\u6B21\u4E0D\u95EE\uFF09", value: { kind: "remember" } },
2782
+ { name: "\u6C38\u4E0D\u52A0\u8F7D\u9879\u76EE\u5916\u7684 import", value: { kind: "never" } }
2783
+ ];
2784
+ async function maybePromptForExternalImports(opts = {}) {
2785
+ const config = opts.config ?? loadConfig2();
2786
+ const store = opts.store ?? new ImportTrustStore(trustedImportsPath(config.homeDir));
2787
+ const print = opts.print ?? ((msg) => process.stdout.write(msg + "\n"));
2788
+ const targets = externalImportTargets({
2789
+ store,
2790
+ trust: opts.trust ?? new TrustManager2(trustPath2(config.homeDir)),
2791
+ interactive: (opts.isInteractive ?? (() => !isNonInteractive6()))(),
2792
+ gateEnabled: config.trust?.enabled !== false,
2793
+ workDir: opts.workDir ?? process.cwd()
2794
+ });
2795
+ if (targets.length === 0) {
2796
+ setImportGate(createImportGate(store));
2797
+ return true;
2798
+ }
2799
+ try {
2800
+ for (const target of targets) {
2801
+ print(importPromptMessage(target));
2802
+ const choice = await (opts.ask ?? askImportChoice)(target);
2803
+ if (choice === null) return false;
2804
+ try {
2805
+ print(applyImportChoice(store, target, choice));
2806
+ } catch (err2) {
2807
+ print(
2808
+ `\u26A0 import \u653E\u884C\u8BB0\u5F55\u5199\u5165\u5931\u8D25\uFF0C\u672C\u6B21\u4ECD\u6309\u4E0D\u52A0\u8F7D\u5904\u7406\uFF1A${err2 instanceof Error ? err2.message : err2}`
2809
+ );
2810
+ }
2811
+ if (choice.kind === "never") break;
2812
+ }
2813
+ } finally {
2814
+ (opts.restoreStdin ?? restoreStdinDefault)();
2815
+ setImportGate(createImportGate(store));
2816
+ }
2817
+ return true;
2818
+ }
2819
+ async function askImportChoice(_target) {
2820
+ const { select: select2 } = await import("@inquirer/prompts");
2821
+ try {
2822
+ return await select2({
2823
+ message: "\u662F\u5426\u52A0\u8F7D\u5B83\uFF1F",
2824
+ choices: [...CHOICES2],
2825
+ default: CHOICES2[0]?.value
2826
+ });
2827
+ } catch {
2828
+ return null;
2829
+ }
2830
+ }
2831
+
2832
+ // src/commands/run-stream.ts
2833
+ var LIVE_PREFIX = "\u2502 ";
2834
+ function createStreamWriter(enabled, sink) {
2835
+ const out2 = sink ? sink.out : (s) => void process.stdout.write(s);
2836
+ const err2 = sink ? sink.err : (s) => void process.stderr.write(s);
2837
+ let wroteText = false;
2838
+ let midText = false;
2839
+ let midLive = false;
2840
+ return {
2841
+ text(chunk) {
2842
+ if (!enabled || !chunk) return;
2843
+ if (midLive) {
2844
+ err2("\n");
2845
+ midLive = false;
2846
+ }
2847
+ out2(chunk);
2848
+ wroteText = true;
2849
+ midText = !chunk.endsWith("\n");
2850
+ },
2851
+ live(chunk) {
2852
+ if (!enabled || !chunk) return;
2853
+ if (midText) {
2854
+ out2("\n");
2855
+ midText = false;
2856
+ }
2857
+ const endsNl = chunk.endsWith("\n");
2858
+ const segs = (endsNl ? chunk.slice(0, -1) : chunk).split("\n");
2859
+ const body = segs.map((s, i) => i === 0 && midLive ? s : LIVE_PREFIX + s).join("\n");
2860
+ err2(body + (endsNl ? "\n" : ""));
2861
+ midLive = !endsNl;
2862
+ },
2863
+ end() {
2864
+ if (!enabled) return;
2865
+ if (midLive) {
2866
+ err2("\n");
2867
+ midLive = false;
2868
+ }
2869
+ if (wroteText) out2("\n");
2870
+ midText = false;
2871
+ }
2872
+ };
2873
+ }
2874
+
2875
+ // src/commands/run-once.ts
2876
+ async function readStdin() {
2877
+ if (process.stdin.isTTY) return "";
2878
+ const chunks = [];
2879
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
2880
+ return Buffer.concat(chunks).toString("utf-8");
2881
+ }
2882
+ function composePrompt(query, stdin) {
2883
+ const piped = stdin.trim();
2884
+ if (!piped) return query ?? "";
2885
+ if (!query) return piped;
2886
+ return `${query}
2887
+
2888
+ --- \u4EE5\u4E0B\u662F\u6807\u51C6\u8F93\u5165\u7684\u5185\u5BB9 ---
2889
+ ${piped}`;
2890
+ }
2891
+ function composeContent(query, images) {
2892
+ if (images.length === 0) return query;
2893
+ try {
2894
+ return [
2895
+ ...query ? [{ type: "text", text: query }] : [],
2896
+ ...images.map((p) => imageFromPath(p))
2897
+ ];
2898
+ } catch (err2) {
2899
+ throw new CliError(
2900
+ err2 instanceof Error ? err2.message : String(err2),
2901
+ EXIT_CODES.FAILURE,
2902
+ "\u652F\u6301 png / jpeg / gif / webp\uFF0C\u4E5F\u53EF\u4EE5\u76F4\u63A5\u7ED9 http(s) \u56FE\u7247 URL"
2903
+ );
2904
+ }
2905
+ }
2906
+ var SHORT_ID_LEN = 8;
2907
+ async function runOnce(query, opts, resumeId) {
2908
+ const { policy: headless, invalid } = parseHeadlessFlags(opts);
2909
+ if (invalid.length > 0) {
2910
+ throw new CliError(
2911
+ `--allow-operation \u4E0D\u8BA4\u8BC6: ${invalid.join(", ")}`,
2912
+ EXIT_CODES.FAILURE,
2913
+ `\u53EF\u9009\u503C: ${OPERATION_TYPES2.join(" / ")}`
2914
+ );
2915
+ }
2916
+ const format = resolveOutputFormat(opts);
2917
+ const limits = parseCallLimits(opts);
2918
+ const approver = resolveApproverProgram(opts.permissionPromptTool);
2919
+ const content = composeContent(query, opts.image ?? []);
2920
+ if (!await maybePromptForTrust()) {
2921
+ process.exit(EXIT_CODES.FAILURE);
2922
+ }
2923
+ if (!await maybePromptForExternalImports()) {
2924
+ process.exit(EXIT_CODES.FAILURE);
2925
+ }
2926
+ let rt = null;
2927
+ const runtime = await buildRuntime2({
2928
+ onApprovalRequest: approver ? createExternalApproval({
2929
+ program: approver,
2930
+ sessionId: () => rt?.sessionId,
2931
+ preauth: (req) => preauthorizedBy(rt?.config.headless, headless, req)
2932
+ }) : createInteractiveApproval({ permission: () => rt?.permission }),
2933
+ // 必须显式传:`buildRuntime` 的默认值是 false(库不替宿主决定进程怎么退),
2934
+ // 而 CLI **就是**那个该被决定的宿主 —— Ctrl+C 要走 dispose() 关掉 SQLite
2935
+ // 连接和 MCP 子进程,不能因为默认值翻转就悄悄退回「硬杀进程」。
2936
+ // 守卫用例:cli/__tests__/signal-handlers.test.ts
2937
+ installSignalHandlers: true,
2938
+ // 有外部审批器 = **有人能答**,所以告诉权限层按交互处理(方案 28)。
2939
+ //
2940
+ // 不这么做的话这个参数压根不会被调到:非交互下权限层的第 3 层会把每一次
2941
+ // `ask_user` 直接收敛成 deny(见 manager.ts 的 applyPreauthorization),
2942
+ // 审批回调连请求都收不到。这和长驻模式那条路是同一个决定、同一个理由
2943
+ // (见 headless/session.ts 的文件头第 3 条)。
2944
+ //
2945
+ // 连带的三个后果,都是刻意的:
2946
+ // 1. `--allow-tool` 那套预授权不再由权限层判 —— 所以下面 `preauth` 把
2947
+ // 「预授权先命中就不起子进程」补在了审批回调里(验收 #18)
2948
+ // 2. 混淆构造(`$(echo rm) -rf /`)从「非交互无人确认,拒绝」变成「问审批
2949
+ // 程序」。那正是「有人能答」的含义,也是文档里要写明「等同于信任它」的
2950
+ // 原因之一
2951
+ // 3. 这条路上**不会出现退出码 3** —— 那个码的语义是「没人可问所以被卡住」,
2952
+ // 而这里每一次都真的问过了。审批程序答 deny 是它自己的决定,退 0
2953
+ ...approver ? { interactive: true } : {},
2954
+ ...headless ? { headless } : {},
2955
+ ...resumeId ? { resumeId } : {}
2956
+ });
2957
+ rt = runtime;
2958
+ if (!runtime.session) {
2959
+ const detail = runtime.diagnostics.join("\n ");
2960
+ await runtime.dispose();
2961
+ throw new CliError(
2962
+ "provider \u4E0D\u53EF\u7528\uFF0Cagent \u65E0\u6CD5\u542F\u52A8",
2963
+ EXIT_CODES.FAILURE,
2964
+ detail ? `\u8BCA\u65AD:
2965
+ ${detail}` : "\u8FD0\u884C epoch model \u914D\u7F6E"
2966
+ );
2967
+ }
2968
+ try {
2969
+ if (format !== "json") reportDiagnostics(runtime.diagnostics, opts.verbose === true);
2970
+ if (resumeId) {
2971
+ const restored = runtime.session.getHistory().length;
2972
+ process.stderr.write(
2973
+ `\u21BB \u5DF2\u6062\u590D ${resumeId.slice(0, SHORT_ID_LEN)}\uFF08${restored} \u6761\u5386\u53F2\u6D88\u606F\uFF09
2974
+ `
2975
+ );
2976
+ }
2977
+ const emitter = format === "stream-json" ? new HeadlessEmitter() : void 0;
2978
+ emitter?.init(initInfoOf(runtime, process.cwd()));
2979
+ const outcome = await consume(runtime, content, opts, format, limits, emitter);
2980
+ const tasks = listBackgroundTasks(runtime.session?.sessionId ?? runtime.sessionId);
2981
+ emitOutcome(outcome, opts, format, emitter, tasks);
2982
+ for (const notice of runtime.model?.drainNotices() ?? []) {
2983
+ process.stderr.write(`\xB7 ${notice}
2984
+ `);
2985
+ }
2986
+ const limited = limitExitCode(outcome, limits);
2987
+ if (limited !== void 0 && !process.exitCode) process.exitCode = limited;
2988
+ if (outcome.aborted && !process.exitCode) process.exitCode = 130;
2989
+ } finally {
2990
+ reportHeadlessAudit(runtime.permission);
2991
+ await runtime.dispose();
2992
+ }
2993
+ }
2994
+ async function consume(runtime, query, opts, format, limits, emitter) {
2995
+ const out2 = emptyOutcome(runtime.diagnostics);
2996
+ const streaming = opts.stream !== false && format === "text";
2997
+ const w = createStreamWriter(streaming);
2998
+ const controller = new AbortController();
2999
+ const onSigint = () => {
3000
+ out2.aborted = true;
3001
+ controller.abort();
3002
+ };
3003
+ process.on("SIGINT", onSigint);
3004
+ try {
3005
+ const run = runtime.session.run(query, {
3006
+ signal: controller.signal,
3007
+ ...limits.maxTurns !== void 0 ? { maxTurns: limits.maxTurns } : {},
3008
+ ...limits.maxCallCostUsd !== void 0 ? { maxCallCostUsd: limits.maxCallCostUsd } : {}
3009
+ });
3010
+ for await (const ev of run) {
3011
+ emitter?.agentEvent(ev);
3012
+ collect2(out2, ev, w);
3013
+ }
3014
+ } finally {
3015
+ process.off("SIGINT", onSigint);
3016
+ w.end();
3017
+ }
3018
+ return out2;
3019
+ }
3020
+ function collect2(out2, ev, w) {
3021
+ if (ev.type === "text-delta") {
3022
+ out2.text += ev.text;
3023
+ w.text(ev.text);
3024
+ } else if (ev.type === "tool-output-delta") {
3025
+ w.live(ev.text);
3026
+ } else if (ev.type === "reasoning-delta") {
3027
+ out2.reasoning += ev.text;
3028
+ } else if (ev.type === "turn-complete") {
3029
+ out2.turns = ev.turn;
3030
+ } else if (ev.type === "usage") {
3031
+ out2.usage = ev.cumulative;
3032
+ } else if (ev.type === "error") {
3033
+ out2.text = `\u9519\u8BEF: ${ev.message}`;
3034
+ process.exitCode = EXIT_CODES.FAILURE;
3035
+ } else if (ev.type === "finish") {
3036
+ out2.finishReason = ev.reason;
3037
+ if (ev.breach) out2.breach = ev.breach;
3038
+ if (ev.reason === "budget-exceeded") out2.budgetExceeded = true;
3039
+ }
3040
+ }
3041
+ function emitOutcome(out2, opts, format, emitter, tasks = []) {
3042
+ const taskLines = backgroundTaskSummary(tasks);
3043
+ if (format === "stream-json") {
3044
+ emitter?.result(toResult(out2));
3045
+ if (taskLines) process.stderr.write(taskLines + "\n");
3046
+ return;
3047
+ }
3048
+ if (format === "json") {
3049
+ process.stdout.write(
3050
+ JSON.stringify(
3051
+ {
3052
+ text: out2.text,
3053
+ ...out2.reasoning ? { reasoning: out2.reasoning } : {},
3054
+ ...out2.usage ? { usage: out2.usage } : {},
3055
+ ...out2.finishReason ? { finishReason: out2.finishReason } : {},
3056
+ budgetExceeded: out2.budgetExceeded,
3057
+ aborted: out2.aborted,
3058
+ diagnostics: out2.diagnostics,
3059
+ // 一个后台任务都没起时**整个字段都不出现** —— 绝大多数调用是这样,
3060
+ // 而验收 4.1 第 1 条要求 `--json` 的输出与改造前逐字节相同
3061
+ ...tasks.length > 0 ? { backgroundTasks: tasks } : {}
3062
+ },
3063
+ null,
3064
+ 2
3065
+ ) + "\n"
3066
+ );
3067
+ return;
3068
+ }
3069
+ if (out2.reasoning) process.stderr.write(`\u23FA \u601D\u8003\u8FC7\u7A0B:
3070
+ ${out2.reasoning}
3071
+
3072
+ `);
3073
+ if (opts.stream === false && out2.text) process.stdout.write(out2.text + "\n");
3074
+ if (out2.aborted) process.stderr.write("\n\u5DF2\u4E2D\u6B62\n");
3075
+ if (taskLines) process.stderr.write(taskLines + "\n");
3076
+ const summary = formatSummary(out2.usage, out2.budgetExceeded);
3077
+ if (summary) process.stderr.write(summary + "\n");
3078
+ }
3079
+ function reportDiagnostics(diagnostics, verbose, write) {
3080
+ const w = write ?? ((s) => void process.stderr.write(s));
3081
+ for (const d of noteworthy(diagnostics, verbose)) w(`\xB7 ${d}
3082
+ `);
3083
+ }
3084
+ function noteworthy(diagnostics, verbose) {
3085
+ if (verbose) return [...diagnostics];
3086
+ return diagnostics.filter((d) => !/:\s*OK$/.test(d));
3087
+ }
3088
+ function formatSummary(usage, budgetExceeded) {
3089
+ if (!usage) return "";
3090
+ const cached = usage.cacheHitTokens ? `\uFF08\u7F13\u5B58\u547D\u4E2D ${usage.cacheHitTokens}\uFF09` : "";
3091
+ const tokens = `${promptTokens(usage)} in${cached} / ${usage.outputTokens} out`;
3092
+ const cost = usage.costUsd === void 0 ? "\u6210\u672C\u672A\u77E5\uFF08\u65E0\u5B9A\u4EF7\u6570\u636E\uFF09" : formatUsd(usage.costUsd);
3093
+ const stopped = budgetExceeded ? " \u26A0 \u5DF2\u89E6\u8FBE\u9884\u7B97\u4E0A\u9650" : "";
3094
+ return `\u2500 ${tokens} \xB7 ${cost}${stopped}`;
3095
+ }
3096
+ function reportHeadlessAudit(permission) {
3097
+ const audit = permission?.getAudit("headless").entries ?? [];
3098
+ if (audit.length === 0) return;
3099
+ for (const e of audit) {
3100
+ process.stderr.write(
3101
+ e.outcome === "granted" ? `[\u9884\u6388\u6743\u653E\u884C] ${e.toolName} ${e.target} \u2014\u2014 ${e.reason}
3102
+ ` : `[\u672A\u9884\u6388\u6743\u62D2\u7EDD] ${e.toolName} ${e.target} \u2014\u2014 ${e.reason}
3103
+ `
3104
+ );
3105
+ }
3106
+ if (audit.some((e) => e.outcome === "denied") && !process.exitCode) {
3107
+ process.exitCode = EXIT_CODES.PERMISSION_DENIED;
3108
+ }
3109
+ }
3110
+
3111
+ // src/commands/run-overrides.ts
3112
+ import { isPermissionLevel as isPermissionLevel2, PERMISSION_LEVELS as PERMISSION_LEVELS2 } from "@epoch-agent/core";
3113
+ import { t as t3 } from "@epoch-agent/infra";
3114
+ import { isProviderType as isProviderType2, PROVIDER_TYPES as PROVIDER_TYPES3 } from "@epoch-agent/protocol";
3115
+ function applyRunFlags(opts) {
3116
+ applySettingsPath(opts.settings);
3117
+ applyAddDirs(opts.addDir);
3118
+ applyAgentRole(opts.agent);
3119
+ applyOverrides(opts);
3120
+ parseCallLimits(opts);
3121
+ resolveApproverProgram(opts.permissionPromptTool);
3122
+ }
3123
+ function applyOverrides(opts) {
3124
+ if (opts.model) process.env.EPOCH_MODEL = assertModelName(opts.model, "--model");
3125
+ if (opts.fallbackModel) {
3126
+ process.env.EPOCH_FALLBACK_MODEL = assertModelName(opts.fallbackModel, "--fallback-model");
3127
+ }
3128
+ if (opts.baseUrl) process.env.EPOCH_BASE_URL = assertHttpUrl(opts.baseUrl);
3129
+ if (opts.provider) {
3130
+ if (!isProviderType2(opts.provider)) {
3131
+ throw new CliError(
3132
+ t3("flags.provider_unknown", { value: opts.provider }),
3133
+ 1,
3134
+ t3("flags.choices", { choices: PROVIDER_TYPES3.join(", ") })
3135
+ );
3136
+ }
3137
+ process.env.EPOCH_PROVIDER = opts.provider;
3138
+ }
3139
+ if (opts.permission) {
3140
+ if (!isPermissionLevel2(opts.permission)) {
3141
+ throw new CliError(
3142
+ t3("flags.permission_unknown", { value: opts.permission }),
3143
+ 1,
3144
+ t3("flags.choices", { choices: PERMISSION_LEVELS2.join(", ") })
3145
+ );
3146
+ }
3147
+ process.env.EPOCH_PERMISSION = opts.permission;
3148
+ }
3149
+ }
3150
+ var MODEL_ID = /^[!-~]+$/;
3151
+ function assertModelName(value, flag) {
3152
+ const spec = value.trim();
3153
+ if (MODEL_ID.test(spec)) return value;
3154
+ throw new CliError(
3155
+ t3("flags.model_not_a_name", { flag, value }),
3156
+ 1,
3157
+ t3("flags.model_not_a_name_hint", { value: spec })
3158
+ );
3159
+ }
3160
+ function assertHttpUrl(value) {
3161
+ try {
3162
+ const url = new URL(value.trim());
3163
+ if (url.protocol === "http:" || url.protocol === "https:") return value;
3164
+ } catch {
3165
+ }
3166
+ throw new CliError(
3167
+ t3("flags.base_url_invalid", { value }),
3168
+ 1,
3169
+ t3("flags.base_url_invalid_hint", { value: value.trim() })
3170
+ );
3171
+ }
3172
+
3173
+ // src/commands/sessions.ts
3174
+ import { isNonInteractive as isNonInteractive7, resolveProjectRoot as resolveProjectRoot3, SessionManager } from "@epoch-agent/core";
3175
+ import { dbPath } from "@epoch-agent/infra";
3176
+ var log6 = (msg) => process.stdout.write(msg + "\n");
3177
+ var SHORT_ID_LEN2 = 8;
3178
+ var PICK_LIMIT = 20;
3179
+ function registerSessionsCommand(program2) {
3180
+ const cmd = program2.command("sessions").alias("session").description("\u7BA1\u7406\u5386\u53F2\u4F1A\u8BDD");
3181
+ cmd.command("list").description("\u5217\u51FA\u6700\u8FD1\u7684\u4F1A\u8BDD").option("-n, --limit <n>", "\u6570\u91CF", "10").action((opts) => {
3182
+ withSessions((sm) => {
3183
+ const r = sm.list({ limit: parseInt(opts.limit, 10) });
3184
+ if (r.sessions.length === 0) {
3185
+ log6(" (\u65E0)");
3186
+ return;
3187
+ }
3188
+ for (const s of r.sessions) {
3189
+ const when = s.startedAt ? new Date(s.startedAt).toLocaleString("zh-CN") : "\u672A\u77E5";
3190
+ log6(` ${s.id.slice(0, SHORT_ID_LEN2)} ${when} ${s.title || s.preview || "(\u65E0)"}`);
3191
+ }
3192
+ });
3193
+ });
3194
+ cmd.command("resume").description("\u6062\u590D\u4F1A\u8BDD\u7EE7\u7EED\u5BF9\u8BDD\uFF08id \u53EF\u4EE5\u53EA\u5199 list \u91CC\u663E\u793A\u7684\u524D 8 \u4F4D\uFF09").argument("[id]", "\u4F1A\u8BDD ID \u6216\u5176\u524D\u7F00\uFF08\u4E0D\u6307\u5B9A\u5219\u6062\u590D\u6700\u65B0\uFF09").argument("[query]", "\u8981\u53D1\u9001\u7684\u6D88\u606F\uFF08\u53EF\u9009\uFF09").action(async (id, query) => {
3195
+ const resolved = resolveSessionId(id);
3196
+ if (!resolved.ok) {
3197
+ throw new CliError(resolved.error, EXIT_CODES.FAILURE);
3198
+ }
3199
+ await runOnce(query || "\u7EE7\u7EED", {}, resolved.id);
3200
+ });
3201
+ }
3202
+ function resolveSessionId(input2) {
3203
+ return withSessions((sm) => {
3204
+ if (!input2) {
3205
+ const latest = sm.getLatest()?.meta.id;
3206
+ return latest ? { ok: true, id: latest } : { ok: false, error: "\u65E0\u53EF\u6062\u590D\u7684\u4F1A\u8BDD" };
3207
+ }
3208
+ if (sm.get(input2)) return { ok: true, id: input2 };
3209
+ const candidates = sm.list({ limit: 200 }).sessions.filter((s) => s.id.startsWith(input2)).map((s) => s.id);
3210
+ if (candidates.length === 1) return { ok: true, id: candidates[0] };
3211
+ if (candidates.length === 0) {
3212
+ return { ok: false, error: `\u627E\u4E0D\u5230\u4F1A\u8BDD: ${input2}\uFF08\u7528 epoch sessions list \u67E5\u770B\uFF09` };
3213
+ }
3214
+ return {
3215
+ ok: false,
3216
+ error: `\u4F1A\u8BDD\u524D\u7F00 ${input2} \u5339\u914D\u5230 ${candidates.length} \u4E2A\uFF0C\u8BF7\u5199\u66F4\u957F\u7684\u524D\u7F00`
3217
+ };
3218
+ });
3219
+ }
3220
+ function latestSessionIdForProject(cwd = process.cwd()) {
3221
+ const root = resolveProjectRoot3(cwd);
3222
+ return withSessions((sm) => sm.getLatestByCwd(root)?.meta.id);
3223
+ }
3224
+ async function pickSessionId() {
3225
+ if (isNonInteractive7()) {
3226
+ throw new CliError(
3227
+ "\u975E\u4EA4\u4E92\u73AF\u5883\u4E0B --resume \u5FC5\u987B\u5E26\u4F1A\u8BDD id",
3228
+ EXIT_CODES.FAILURE,
3229
+ "\u7528 epoch sessions list \u67E5 id\uFF0C\u6216\u7528 --continue \u63A5\u672C\u9879\u76EE\u6700\u8FD1\u4E00\u4E2A"
3230
+ );
3231
+ }
3232
+ const rows = withSessions(
3233
+ (sm) => sm.list({ limit: PICK_LIMIT }).sessions.map((s) => ({
3234
+ id: s.id,
3235
+ title: s.title || s.preview || "(\u65E0\u6807\u9898)",
3236
+ when: s.startedAt ? new Date(s.startedAt).toLocaleString("zh-CN") : "\u672A\u77E5",
3237
+ // 老会话的 cwd 是 NULL(方案 29 之前没人写它),如实显示成「未记录」
3238
+ where: s.cwd ?? "\u672A\u8BB0\u5F55",
3239
+ messageCount: s.messageCount
3240
+ }))
3241
+ );
3242
+ if (rows.length === 0) {
3243
+ log6("\u6CA1\u6709\u5386\u53F2\u4F1A\u8BDD\uFF0C\u5C06\u5F00\u59CB\u4E00\u6BB5\u65B0\u5BF9\u8BDD\u3002");
3244
+ return void 0;
3245
+ }
3246
+ const { select: select2 } = await import("@inquirer/prompts");
3247
+ try {
3248
+ return await select2({
3249
+ message: "\u6062\u590D\u54EA\u4E00\u6BB5\u4F1A\u8BDD\uFF1F",
3250
+ choices: rows.map((r) => ({
3251
+ name: `${r.id.slice(0, SHORT_ID_LEN2)} ${r.when} ${r.title}`,
3252
+ value: r.id,
3253
+ description: `${r.messageCount} \u6761\u6D88\u606F \xB7 ${r.where}`
3254
+ })),
3255
+ pageSize: 12
3256
+ });
3257
+ } catch {
3258
+ return void 0;
3259
+ }
3260
+ }
3261
+ function withSessions(fn) {
3262
+ const sm = new SessionManager(dbPath(EPOCH_HOME));
3263
+ try {
3264
+ return fn(sm);
3265
+ } finally {
3266
+ sm.close();
3267
+ }
3268
+ }
3269
+
3270
+ // src/commands/run.ts
3271
+ var log7 = (msg) => {
3272
+ process.stdout.write(msg + "\n");
3273
+ };
3274
+ function registerRunCommand(program2) {
3275
+ program2.argument("[query]", "\u8981\u6267\u884C\u7684\u4EFB\u52A1\uFF1B\u4E0D\u7ED9\u4E14 stdin \u6709\u5185\u5BB9\u65F6\u8BFB stdin").option("--allow-tool <name>", "\u975E\u4EA4\u4E92\uFF08CI / \u7BA1\u9053\uFF09\u4E0B\u9884\u6388\u6743\u7684\u5DE5\u5177\u540D\uFF0C\u53EF\u91CD\u590D", collect, []).option(
3276
+ "--allow-operation <type>",
3277
+ `\u975E\u4EA4\u4E92\u4E0B\u9884\u6388\u6743\u7684\u64CD\u4F5C\u7C7B\u578B\uFF08${OPERATION_TYPES3.join(" / ")}\uFF09\uFF0C\u53EF\u91CD\u590D`,
3278
+ collect,
3279
+ []
3280
+ ).option(
3281
+ "-i, --image <path>",
3282
+ "\u9644\u52A0\u56FE\u7247\uFF08\u8DEF\u5F84\u6216 http(s) URL\uFF09\uFF0C\u53EF\u91CD\u590D\u3002\u9700\u8981\u6A21\u578B\u652F\u6301\u89C6\u89C9",
3283
+ collect,
3284
+ []
3285
+ ).option("-c, --continue", "\u63A5\u4E0A\u672C\u9879\u76EE\u6700\u8FD1\u4E00\u4E2A\u4F1A\u8BDD\u7EE7\u7EED").option("-r, --resume [id]", "\u6062\u590D\u67D0\u4E2A\u4F1A\u8BDD\uFF1B\u4E0D\u5E26 id \u5219\u4EA4\u4E92\u5F0F\u9009").option("-m, --model <name>", "\u672C\u6B21\u4F7F\u7528\u7684\u6A21\u578B\uFF0C\u8986\u76D6\u914D\u7F6E\u6587\u4EF6").option("--fallback-model <name>", "\u4E3B\u6A21\u578B\u4E0D\u53EF\u7528\u65F6\uFF0C\u540C\u4E00\u4E2A provider \u5185\u5148\u964D\u7EA7\u5230\u5B83").option("-p, --provider <type>", `\u672C\u6B21\u4F7F\u7528\u7684 provider\uFF08${PROVIDER_TYPES4.join(" / ")}\uFF09`).option("--base-url <url>", "\u672C\u6B21\u4F7F\u7528\u7684 API base URL").option("--permission <level>", `\u672C\u6B21\u7684\u6743\u9650\u7EA7\u522B\uFF08${PERMISSION_LEVELS3.join(" / ")}\uFF09`).option("--settings <file>", "\u989D\u5916\u7684\u8BBE\u7F6E\u6587\u4EF6\uFF0C\u538B\u8FC7\u9879\u76EE\u7EA7\u548C\u7528\u6237\u7EA7\uFF08CI \u7528\uFF09").option(
3286
+ "--add-dir <dir>",
3287
+ "\u628A\u989D\u5916\u76EE\u5F55\u52A0\u8FDB\u5DE5\u4F5C\u533A\uFF08\u6587\u4EF6\u5DE5\u5177\u548C\u6743\u9650\u5224\u5B9A\u90FD\u653E\u884C\u5B83\uFF09\uFF0C\u53EF\u91CD\u590D",
3288
+ collect,
3289
+ []
3290
+ ).option("--agent <role>", "\u7528\u67D0\u4E2A\u89D2\u8272\u8DD1\u4E3B agent\uFF08\u5B83\u7684\u5DE5\u5177\u767D\u540D\u5355\u548C maxTurns \u751F\u6548\uFF09").option("--worktree", "\u5728\u4E00\u4E2A\u65B0\u5EFA\u7684 git worktree\uFF08\u65B0\u5206\u652F\uFF09\u91CC\u8DD1\u8FD9\u4E00\u6B21\uFF0C\u9000\u51FA\u65F6\u95EE\u662F\u5426\u5220\u9664").option("--json", "\u4EE5 JSON \u8F93\u51FA\uFF08stdout \u6070\u597D\u4E00\u4E2A\u6587\u6863\uFF09\uFF0C\u7ED9 CI \u7528").option(
3291
+ "--output-format <fmt>",
3292
+ `stdout \u7684\u683C\u5F0F\uFF08${HEADLESS_OUTPUT_FORMATS2.join(" / ")}\uFF09\u3002--json \u662F json \u7684\u522B\u540D`
3293
+ ).option(
3294
+ "--input-format <fmt>",
3295
+ `stdin \u7684\u683C\u5F0F\uFF08${HEADLESS_INPUT_FORMATS2.join(" / ")}\uFF09\u3002stream-json \u8FDB\u957F\u9A7B\u6A21\u5F0F`
3296
+ ).option(
3297
+ "--permission-prompt-tool <program>",
3298
+ "\u628A\u6BCF\u4E00\u6B21\u9700\u8981\u786E\u8BA4\u7684\u64CD\u4F5C\u4EA4\u7ED9\u8FD9\u4E2A\u5916\u90E8\u7A0B\u5E8F\u88C1\u51B3\uFF08\u7B49\u540C\u4E8E\u5B8C\u5168\u4FE1\u4EFB\u5B83\uFF09"
3299
+ ).option("--max-turns <n>", "\u672C\u6B21\u8C03\u7528\u6700\u591A\u51E0\u8F6E\u5DE5\u5177\u5FAA\u73AF\uFF0C\u538B\u8FC7\u914D\u7F6E\u91CC\u7684 maxTurns").option("--max-budget-usd <x>", "\u672C\u6B21\u8C03\u7528\u82B1\u8D85\u8FD9\u4E2A\u91D1\u989D\u5C31\u505C\uFF08\u4E0E\u8DE8\u4F1A\u8BDD\u9884\u7B97\u53E0\u52A0\uFF09").option("--no-stream", "\u4E0D\u6D41\u5F0F\u8F93\u51FA\uFF0C\u7B49\u8DD1\u5B8C\u4E00\u6B21\u6027\u6253\u5370").option("-v, --verbose", "\u6253\u5370\u5168\u90E8\u542F\u52A8\u8BCA\u65AD\uFF08\u9ED8\u8BA4\u53EA\u6253\u975E OK \u7684\uFF09").option("-V, --version", "\u6253\u5370\u7248\u672C\u3001Node\u3001\u5E73\u53F0\u4E0E\u5B89\u88C5\u65B9\u5F0F").action(async (query, opts) => {
3300
+ if (opts.version === true) {
3301
+ log7(describeVersion(selfPackage().name));
3302
+ return;
3303
+ }
3304
+ applyRunFlags(opts);
3305
+ assertResumeFlagsExclusive(opts);
3306
+ const resumeArg = parseResumeArg(opts.resume);
3307
+ const formats = resolveHeadlessFormats(opts);
3308
+ if (formats.input === "stream-json") {
3309
+ if (opts.continue === true || resumeArg.kind !== "off") {
3310
+ throw new CliError(
3311
+ "--continue / --resume \u4E0D\u80FD\u548C --input-format stream-json \u4E00\u8D77\u7528",
3312
+ EXIT_CODES.FAILURE,
3313
+ "\u957F\u9A7B\u6A21\u5F0F\u4E0B\u8981\u6062\u590D\u54EA\u6BB5\u4F1A\u8BDD\u7531\u5BBF\u4E3B\u5728 init \u5E27\u7684 sessionId \u91CC\u6307\u5B9A"
3314
+ );
3315
+ }
3316
+ if (opts.worktree === true) {
3317
+ throw new CliError(
3318
+ "--worktree \u4E0D\u80FD\u548C --input-format stream-json \u4E00\u8D77\u7528",
3319
+ EXIT_CODES.FAILURE,
3320
+ "\u957F\u9A7B\u6A21\u5F0F\u4E0B stdout \u662F\u534F\u8BAE\u901A\u9053\uFF0Cworktree \u7684\u63D0\u793A\u548C\u8BE2\u95EE\u6CA1\u6709\u843D\u70B9\uFF1B\u5BBF\u4E3B\u53EF\u4EE5\u81EA\u5DF1 git worktree add \u518D\u7528\u90A3\u4E2A\u76EE\u5F55\u5F53 cwd \u542F\u52A8"
3321
+ );
3322
+ }
3323
+ if (opts.permissionPromptTool !== void 0) {
3324
+ throw new CliError(
3325
+ "--permission-prompt-tool \u4E0D\u80FD\u548C --input-format stream-json \u4E00\u8D77\u7528",
3326
+ EXIT_CODES.FAILURE,
3327
+ "\u957F\u9A7B\u6A21\u5F0F\u4E0B\u5BA1\u6279\u5DF2\u7ECF\u662F\u6D41\u91CC\u7684 approval-request \u5E27\uFF0C\u4F60\u76F4\u63A5\u7B54\u5C31\u884C \u2014\u2014 \u90A3\u6761\u8DEF\u66F4\u5E72\u51C0\uFF08\u4E0D\u7528\u4E3A\u6BCF\u6B21\u5BA1\u6279\u8D77\u4E00\u4E2A\u5B50\u8FDB\u7A0B\uFF0C\u4E5F\u4E0D\u7528\u7F16\u89E3\u7801\uFF09"
3328
+ );
3329
+ }
3330
+ if (!hasCredentials()) {
3331
+ throw new CliError(
3332
+ "\u6CA1\u6709\u914D\u7F6E\u6A21\u578B\u51ED\u636E\uFF0C\u957F\u9A7B\u6A21\u5F0F\u8D77\u4E0D\u6765",
3333
+ EXIT_CODES.FAILURE,
3334
+ "\u5148\u8FD0\u884C epoch model \u914D\u7F6E provider \u548C API key"
3335
+ );
3336
+ }
3337
+ process.exitCode = await runStreamJsonSession(opts);
3338
+ return;
3339
+ }
3340
+ const carried = resumeArg.kind === "pick" ? resumeArg.query : void 0;
3341
+ const prompt = composePrompt(query ?? carried, await readStdin());
3342
+ if (!hasCredentials()) {
3343
+ log7("\u{1F44B} \u6B22\u8FCE\u4F7F\u7528 epoch-agent\uFF01\n\n\u8FD0\u884C epoch model \u914D\u7F6E\u6A21\u578B\u548C API key \u5373\u53EF\u5F00\u59CB\u5BF9\u8BDD\u3002");
3344
+ return;
3345
+ }
3346
+ const resumeId = await resolveResumeTarget(opts, resumeArg);
3347
+ const dispatch = async () => {
3348
+ if (!prompt && (opts.image ?? []).length === 0) return launchTui(resumeId);
3349
+ return runOnce(prompt, opts, resumeId);
3350
+ };
3351
+ if (opts.worktree !== true) return dispatch();
3352
+ await inWorktree(!isNonInteractive8() && formats.output === "text", dispatch);
3353
+ });
3354
+ }
3355
+ async function inWorktree(interactive, body) {
3356
+ const session = await createWorktree();
3357
+ process.stderr.write(worktreeBanner(session));
3358
+ process.chdir(session.path);
3359
+ try {
3360
+ await body();
3361
+ } finally {
3362
+ await finishWorktree(session, { interactive });
3363
+ }
3364
+ }
3365
+ async function resolveResumeTarget(opts, resumeArg) {
3366
+ if (opts.continue === true) {
3367
+ const latest = latestSessionIdForProject();
3368
+ if (!latest) {
3369
+ process.stderr.write("\u672C\u9879\u76EE\u8FD8\u6CA1\u6709\u5386\u53F2\u4F1A\u8BDD\uFF0C\u5F00\u59CB\u4E00\u6BB5\u65B0\u7684\u3002\n");
3370
+ }
3371
+ return latest;
3372
+ }
3373
+ if (resumeArg.kind === "id") {
3374
+ const resolved = resolveSessionId(resumeArg.id);
3375
+ if (!resolved.ok) {
3376
+ throw new CliError(
3377
+ resolved.error,
3378
+ EXIT_CODES.FAILURE,
3379
+ // `-r` 的值是纯十六进制的一句话时会走到这儿(见 looksLikeSessionId)
3380
+ '\u5982\u679C\u90A3\u4E0D\u662F id \u800C\u662F\u8981\u53D1\u7684\u6D88\u606F\uFF0C\u5199\u6210 epoch --resume "\u6D88\u606F" \u4E4B\u5916\u7684\u5F62\u5F0F\uFF1Aepoch -r -- "\u6D88\u606F" \u4F1A\u628A\u5B83\u5F53\u4F4D\u7F6E\u53C2\u6570\uFF0C\u6216\u5148 epoch sessions list \u67E5 id'
3381
+ );
3382
+ }
3383
+ return resolved.id;
3384
+ }
3385
+ if (resumeArg.kind === "pick") return pickSessionId();
3386
+ return void 0;
3387
+ }
3388
+ function resolveTuiEntry(baseDir, exists = existsSync6) {
3389
+ return [
3390
+ join5(baseDir, "tui-entry.js"),
3391
+ join5(baseDir, "tui-entry.ts"),
3392
+ join5(baseDir, "..", "tui-entry.js"),
3393
+ join5(baseDir, "..", "tui-entry.ts")
3394
+ ].find(exists);
3395
+ }
3396
+ async function launchTui(resumeId) {
3397
+ const target = resolveTuiEntry(dirname3(fileURLToPath3(import.meta.url)));
3398
+ if (!target) {
3399
+ process.stderr.write("\u542F\u52A8\u5931\u8D25\uFF1A\u627E\u4E0D\u5230 TUI \u5165\u53E3 tui-entry\uFF0C\u8BF7\u5148\u8FD0\u884C pnpm build\n");
3400
+ process.exitCode = EXIT_CODES.FAILURE;
3401
+ return;
3402
+ }
3403
+ const isSource = target.endsWith(".ts");
3404
+ const cmd = process.execPath;
3405
+ const args = isSource ? ["--import", "tsx", target] : [target];
3406
+ const { spawn: spawn3 } = await import("child_process");
3407
+ const child = spawn3(cmd, args, {
3408
+ stdio: "inherit",
3409
+ env: { ...process.env, ...resumeId ? { EPOCH_RESUME: resumeId } : {} }
3410
+ });
3411
+ await new Promise((done) => {
3412
+ child.on("error", (err2) => {
3413
+ process.stderr.write(`TUI \u542F\u52A8\u5931\u8D25: ${err2.message}
3414
+ `);
3415
+ process.exitCode = EXIT_CODES.FAILURE;
3416
+ done();
3417
+ });
3418
+ child.on("exit", (code, signal) => {
3419
+ process.exitCode = signal ? EXIT_CODES.FAILURE : code ?? EXIT_CODES.SUCCESS;
3420
+ done();
3421
+ });
3422
+ });
3423
+ }
3424
+
3425
+ // src/commands/schedule.ts
3426
+ import { t as t11 } from "@epoch-agent/infra";
3427
+
3428
+ // src/schedule/add.ts
3429
+ import {
3430
+ SCHEDULE_DEFAULTS,
3431
+ validateSchedule
3432
+ } from "@epoch-agent/core";
3433
+ import { automationWorkDir, t as t6 } from "@epoch-agent/infra";
3434
+ import {
3435
+ isOperationType as isOperationType2,
3436
+ isPermissionLevel as isPermissionLevel3,
3437
+ OPERATION_TYPES as OPERATION_TYPES4,
3438
+ PERMISSION_LEVELS as PERMISSION_LEVELS4,
3439
+ SCHEDULE_INTERVAL_MINUTES
3440
+ } from "@epoch-agent/protocol";
3441
+
3442
+ // src/schedule/labels.ts
3443
+ import { t as t4 } from "@epoch-agent/infra";
3444
+ var ISSUE_KEYS = {
3445
+ "name-empty": "schedule.issue.name_empty",
3446
+ "prompt-empty": "schedule.issue.prompt_empty",
3447
+ "permission-unknown": "schedule.issue.permission_unknown",
3448
+ "work-dir-missing": "schedule.issue.work_dir_missing",
3449
+ "budget-missing": "schedule.issue.budget_missing",
3450
+ "max-turns-invalid": "schedule.issue.max_turns_invalid",
3451
+ "timeout-invalid": "schedule.issue.timeout_invalid",
3452
+ "trigger-time-invalid": "schedule.issue.trigger_time_invalid",
3453
+ "trigger-weekdays-empty": "schedule.issue.trigger_weekdays_empty",
3454
+ "trigger-days-invalid": "schedule.issue.trigger_days_invalid",
3455
+ "trigger-interval-invalid": "schedule.issue.trigger_interval_invalid",
3456
+ "trigger-once-date-invalid": "schedule.issue.trigger_once_date_invalid",
3457
+ "trigger-once-past": "schedule.issue.trigger_once_past",
3458
+ "date-range-invalid": "schedule.issue.date_range_invalid",
3459
+ "date-range-on-once": "schedule.issue.date_range_on_once",
3460
+ "rule-invalid": "schedule.issue.rule_invalid",
3461
+ "bypass-needs-workdir": "schedule.issue.bypass_needs_workdir",
3462
+ "bypass-workdir-too-broad": "schedule.issue.bypass_workdir_too_broad",
3463
+ "bypass-needs-limits": "schedule.issue.bypass_needs_limits",
3464
+ "bypass-not-confirmed": "schedule.issue.bypass_not_confirmed"
3465
+ };
3466
+ var STATUS_KEYS = {
3467
+ ok: "schedule.status.ok",
3468
+ failed: "schedule.status.failed",
3469
+ denied: "schedule.status.denied",
3470
+ timeout: "schedule.status.timeout",
3471
+ budget: "schedule.status.budget",
3472
+ "skipped-overlap": "schedule.status.skipped_overlap",
3473
+ "skipped-window": "schedule.status.skipped_window",
3474
+ missed: "schedule.status.missed"
3475
+ };
3476
+ var WEEKDAY_KEYS = {
3477
+ 0: "schedule.weekday.sun",
3478
+ 1: "schedule.weekday.mon",
3479
+ 2: "schedule.weekday.tue",
3480
+ 3: "schedule.weekday.wed",
3481
+ 4: "schedule.weekday.thu",
3482
+ 5: "schedule.weekday.fri",
3483
+ 6: "schedule.weekday.sat"
3484
+ };
3485
+ var DRIFT_KEYS = {
3486
+ "missing-in-os": "schedule.drift.missing_in_os",
3487
+ "orphan-in-os": "schedule.drift.orphan_in_os",
3488
+ "spec-mismatch": "schedule.drift.spec_mismatch",
3489
+ "never-registered": "schedule.drift.never_registered"
3490
+ };
3491
+ function renderIssues(issues) {
3492
+ return issues.map((i) => ` \xB7 ${t4(ISSUE_KEYS[i.code], { detail: i.detail ?? "" })}`).join("\n");
3493
+ }
3494
+ function statusLabel(status) {
3495
+ const mark = status === "ok" ? "\u2705" : status === "failed" || status === "denied" ? "\u274C" : "\u26A0\uFE0F";
3496
+ return `${mark} ${t4(STATUS_KEYS[status])}`;
3497
+ }
3498
+ function driftLabel(kind) {
3499
+ return t4(DRIFT_KEYS[kind]);
3500
+ }
3501
+ function describeTrigger(trigger) {
3502
+ if (trigger.kind === "interval") {
3503
+ return trigger.everyMinutes < 60 ? t4("schedule.trigger.every_minutes", { n: trigger.everyMinutes }) : t4("schedule.trigger.every_hours", { n: trigger.everyMinutes / 60 });
3504
+ }
3505
+ if (trigger.kind === "once") {
3506
+ return t4("schedule.trigger.once", { date: trigger.date, at: trigger.at });
3507
+ }
3508
+ if (trigger.cycle === "daily") return t4("schedule.trigger.daily", { at: trigger.at });
3509
+ if (trigger.cycle === "weekly") {
3510
+ const days = trigger.weekdays.map((d) => t4(WEEKDAY_KEYS[d])).join("/");
3511
+ return t4("schedule.trigger.weekly", { days, at: trigger.at });
3512
+ }
3513
+ return t4("schedule.trigger.monthly", { days: trigger.days.join("/"), at: trigger.at });
3514
+ }
3515
+ function permissionLabel(level) {
3516
+ if (level === "plan") return t4("schedule.permission.plan");
3517
+ if (level === "acceptEdits") return t4("schedule.permission.accept_edits");
3518
+ if (level === "bypass") return t4("schedule.permission.bypass");
3519
+ if (level === "auto") return t4("schedule.permission.auto");
3520
+ return t4("schedule.permission.default");
3521
+ }
3522
+
3523
+ // src/schedule/shared.ts
3524
+ import { realpathSync as realpathSync2 } from "fs";
3525
+ import {
3526
+ resolveScheduleBackend,
3527
+ ScheduleRegistrar,
3528
+ ScheduleStore
3529
+ } from "@epoch-agent/core";
3530
+ import { dbPath as dbPath2, t as t5 } from "@epoch-agent/infra";
3531
+ var log8 = (msg) => void process.stdout.write(`${msg}
3532
+ `);
3533
+ var warn2 = (msg) => void process.stderr.write(`${msg}
3534
+ `);
3535
+ async function openSchedule(homeDir = EPOCH_HOME) {
3536
+ const store = new ScheduleStore(dbPath2(homeDir));
3537
+ const backend = await resolveScheduleBackend();
3538
+ const registrar = new ScheduleRegistrar({ store, backend, homeDir, cli: cliEntry() });
3539
+ return { store, registrar, backend, homeDir, close: () => store.close() };
3540
+ }
3541
+ function cliEntry() {
3542
+ const raw = process.argv[1] ?? "";
3543
+ let entry = raw;
3544
+ try {
3545
+ entry = realpathSync2(raw);
3546
+ } catch {
3547
+ }
3548
+ const info = getInstallationInfo("@epoch-agent/cli", raw);
3549
+ const form = info.packageManager === "npx" ? "npx" : info.packageManager === "source" ? "source" : "installed";
3550
+ return { execPath: process.execPath, entry, form, execArgv: process.execArgv };
3551
+ }
3552
+ function mustFind(store, idOrPrefix) {
3553
+ const exact = store.get(idOrPrefix);
3554
+ if (exact) return exact;
3555
+ const hits = store.findByPrefix(idOrPrefix);
3556
+ if (hits.length === 1) return hits[0];
3557
+ if (hits.length === 0) {
3558
+ throw new CliError(
3559
+ t5("schedule.err_not_found", { id: idOrPrefix }),
3560
+ EXIT_CODES.FAILURE,
3561
+ t5("schedule.err_not_found_hint")
3562
+ );
3563
+ }
3564
+ throw new CliError(
3565
+ t5("schedule.err_ambiguous", { id: idOrPrefix }),
3566
+ EXIT_CODES.FAILURE,
3567
+ hits.map((h) => ` ${h.id} ${h.name}`).join("\n")
3568
+ );
3569
+ }
3570
+ function reportRegistration(outcome, fatal) {
3571
+ if (outcome.ok) {
3572
+ if (outcome.warnings.includes("source")) warn2(t5("schedule.warn_source_entry"));
3573
+ return;
3574
+ }
3575
+ const message = outcome.reason === "unsupported-platform" ? t5("schedule.err_platform_unsupported") : outcome.reason === "backend-error" ? t5("schedule.err_backend", { detail: outcome.detail }) : outcome.refusal === "npx" ? t5("schedule.err_npx") : t5("schedule.err_no_host_runner");
3576
+ const hint = outcome.reason === "refused" && outcome.refusal === "npx" ? t5("schedule.err_npx_hint") : outcome.reason === "refused" ? t5("schedule.err_no_host_runner_hint") : void 0;
3577
+ if (!fatal) {
3578
+ warn2(message);
3579
+ if (hint) warn2(hint);
3580
+ return;
3581
+ }
3582
+ throw new CliError(message, EXIT_CODES.FAILURE, hint);
3583
+ }
3584
+ function formatWhen(ms) {
3585
+ if (ms === void 0) return "\u2014";
3586
+ return new Date(ms).toLocaleString();
3587
+ }
3588
+
3589
+ // src/schedule/add.ts
3590
+ var WEEKDAY_CODES = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
3591
+ async function runAdd(opts) {
3592
+ const filled = await fillInteractively(opts);
3593
+ const trigger = parseTrigger(filled);
3594
+ const permission = parsePermission(filled.permission);
3595
+ const ctx = await openSchedule();
3596
+ try {
3597
+ const input2 = buildInput(filled, trigger, permission);
3598
+ const validation = validateSchedule({
3599
+ name: input2.name,
3600
+ prompt: input2.prompt,
3601
+ permission,
3602
+ // 工作区留空时它会落到 `~/.epoch/automation/<id>/`,而那个目录要等 id
3603
+ // 才算得出来 —— 校验这一步先用一个必然存在的替身(数据目录本身)。
3604
+ // ⚠️ `bypass` 那一档下**不许**这么替:它的第 1 条硬约束就是「必须绑一个
3605
+ // 工作区」,拿数据目录顶上等于把那条约束绕过去
3606
+ workDir: input2.workDir ?? (permission === "bypass" ? "" : ctx.homeDir),
3607
+ maxTurns: input2.maxTurns ?? SCHEDULE_DEFAULTS.maxTurns,
3608
+ maxBudgetUsd: input2.maxBudgetUsd,
3609
+ timeoutMs: input2.timeoutMs ?? SCHEDULE_DEFAULTS.timeoutMs,
3610
+ trigger,
3611
+ ...input2.startDate !== void 0 ? { startDate: input2.startDate } : {},
3612
+ ...input2.endDate !== void 0 ? { endDate: input2.endDate } : {},
3613
+ allowRules: input2.allowRules ?? [],
3614
+ bypassAcknowledged: filled.iUnderstandBypass === true,
3615
+ homeDir: ctx.homeDir
3616
+ });
3617
+ if (validation.issues.length > 0) {
3618
+ throw new CliError(
3619
+ t6("schedule.err_invalid"),
3620
+ EXIT_CODES.FAILURE,
3621
+ renderIssues(validation.issues)
3622
+ );
3623
+ }
3624
+ for (const shadow of validation.shadows) warn2(`\u26A0 ${shadow.message}`);
3625
+ const def = createWithWorkDir(ctx.store, input2, ctx.homeDir);
3626
+ const withDefaults = applyDefaultAllowlist(ctx.store, def, filled);
3627
+ const outcome = await registerOrRollback(
3628
+ ctx.store,
3629
+ withDefaults,
3630
+ (d) => ctx.registrar.register(d)
3631
+ );
3632
+ reportRegistration(outcome, true);
3633
+ printSummary(ctx.store.get(withDefaults.id) ?? withDefaults);
3634
+ } finally {
3635
+ ctx.close();
3636
+ }
3637
+ }
3638
+ async function registerOrRollback(store, def, register) {
3639
+ const outcome = await register(def);
3640
+ if (!outcome.ok) store.remove(def.id);
3641
+ return outcome;
3642
+ }
3643
+ async function fillInteractively(opts) {
3644
+ const hasTrigger = Boolean(opts.daily ?? opts.weekly ?? opts.monthly ?? opts.every ?? opts.once);
3645
+ const missing = !opts.name || !opts.prompt || !hasTrigger || !opts.budget;
3646
+ if (!missing) return opts;
3647
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3648
+ throw new CliError(
3649
+ t6("schedule.err_missing_flags"),
3650
+ EXIT_CODES.FAILURE,
3651
+ t6("schedule.err_missing_flags_hint")
3652
+ );
3653
+ }
3654
+ const { input: input2, select: select2 } = await import("@inquirer/prompts");
3655
+ const next = { ...opts };
3656
+ next.name ||= await input2({ message: t6("schedule.ask_name") });
3657
+ next.prompt ||= await input2({ message: t6("schedule.ask_prompt") });
3658
+ if (!hasTrigger) {
3659
+ const at = await input2({ message: t6("schedule.ask_at"), default: "09:00" });
3660
+ next.daily = at;
3661
+ }
3662
+ next.budget ||= await input2({ message: t6("schedule.ask_budget"), default: "0.50" });
3663
+ next.permission ||= await select2({
3664
+ message: t6("schedule.ask_permission"),
3665
+ choices: [
3666
+ { name: t6("schedule.permission.default"), value: "default" },
3667
+ { name: t6("schedule.permission.plan"), value: "plan" },
3668
+ { name: t6("schedule.permission.accept_edits"), value: "acceptEdits" }
3669
+ ]
3670
+ });
3671
+ return next;
3672
+ }
3673
+ function parseTrigger(opts) {
3674
+ const given = [opts.daily, opts.weekly, opts.monthly, opts.every, opts.once].filter(
3675
+ (v) => v !== void 0
3676
+ );
3677
+ if (given.length === 0) throw new CliError(t6("schedule.err_no_trigger"), EXIT_CODES.FAILURE);
3678
+ if (given.length > 1) throw new CliError(t6("schedule.err_many_triggers"), EXIT_CODES.FAILURE);
3679
+ if (opts.daily !== void 0) return { kind: "cron", cycle: "daily", at: opts.daily };
3680
+ if (opts.weekly !== void 0) {
3681
+ return {
3682
+ kind: "cron",
3683
+ cycle: "weekly",
3684
+ at: requireAt(opts),
3685
+ weekdays: opts.weekly.split(",").map(parseWeekday)
3686
+ };
3687
+ }
3688
+ if (opts.monthly !== void 0) {
3689
+ return {
3690
+ kind: "cron",
3691
+ cycle: "monthly",
3692
+ at: requireAt(opts),
3693
+ days: opts.monthly.split(",").map((d) => Number(d.trim()))
3694
+ };
3695
+ }
3696
+ if (opts.once !== void 0) {
3697
+ return { kind: "once", date: opts.once, at: requireAt(opts) };
3698
+ }
3699
+ return { kind: "interval", everyMinutes: parseEvery(opts.every) };
3700
+ }
3701
+ function requireAt(opts) {
3702
+ if (!opts.at) throw new CliError(t6("schedule.err_at_required"), EXIT_CODES.FAILURE);
3703
+ return opts.at;
3704
+ }
3705
+ function parseWeekday(raw) {
3706
+ const idx = WEEKDAY_CODES.indexOf(raw.trim().toUpperCase());
3707
+ if (idx < 0) {
3708
+ throw new CliError(
3709
+ t6("schedule.err_weekday", { value: raw }),
3710
+ EXIT_CODES.FAILURE,
3711
+ WEEKDAY_CODES.join(",")
3712
+ );
3713
+ }
3714
+ return idx;
3715
+ }
3716
+ function parseEvery(raw) {
3717
+ const m = /^(\d+)\s*([mh])$/i.exec(raw.trim());
3718
+ const minutes = m ? Number(m[1]) * (m[2]?.toLowerCase() === "h" ? 60 : 1) : Number.NaN;
3719
+ if (!SCHEDULE_INTERVAL_MINUTES.includes(minutes)) {
3720
+ throw new CliError(
3721
+ t6("schedule.err_every", { value: raw }),
3722
+ EXIT_CODES.FAILURE,
3723
+ t6("schedule.err_every_hint", {
3724
+ values: SCHEDULE_INTERVAL_MINUTES.map((n) => n < 60 ? `${n}m` : `${n / 60}h`).join(" / ")
3725
+ })
3726
+ );
3727
+ }
3728
+ return minutes;
3729
+ }
3730
+ function parsePermission(raw) {
3731
+ if (raw === void 0) return "default";
3732
+ if (!isPermissionLevel3(raw)) {
3733
+ throw new CliError(
3734
+ t6("schedule.err_permission", { value: raw }),
3735
+ EXIT_CODES.FAILURE,
3736
+ PERMISSION_LEVELS4.join(" / ")
3737
+ );
3738
+ }
3739
+ return raw;
3740
+ }
3741
+ function parseOperations(raw) {
3742
+ const out2 = [];
3743
+ for (const value of raw ?? []) {
3744
+ if (!isOperationType2(value)) {
3745
+ throw new CliError(
3746
+ t6("schedule.err_operation", { value }),
3747
+ EXIT_CODES.FAILURE,
3748
+ OPERATION_TYPES4.join(" / ")
3749
+ );
3750
+ }
3751
+ out2.push(value);
3752
+ }
3753
+ return out2;
3754
+ }
3755
+ function buildInput(opts, trigger, permission) {
3756
+ const budget = Number(opts.budget);
3757
+ if (!Number.isFinite(budget) || budget <= 0) {
3758
+ throw new CliError(
3759
+ t6("schedule.err_budget", { value: opts.budget ?? "" }),
3760
+ EXIT_CODES.FAILURE,
3761
+ t6("schedule.err_budget_hint")
3762
+ );
3763
+ }
3764
+ return {
3765
+ name: opts.name ?? "",
3766
+ prompt: opts.prompt ?? "",
3767
+ ...opts.workdir ? { workDir: opts.workdir } : {},
3768
+ ...opts.model ? { model: opts.model } : {},
3769
+ permission,
3770
+ ...opts.allowTool ? { allowTools: opts.allowTool } : {},
3771
+ allowOperations: parseOperations(opts.allowOperation),
3772
+ ...opts.allowRule ? { allowRules: opts.allowRule } : {},
3773
+ ...opts.maxTurns ? { maxTurns: Number(opts.maxTurns) } : {},
3774
+ maxBudgetUsd: budget,
3775
+ ...opts.timeout ? { timeoutMs: Number(opts.timeout) * 6e4 } : {},
3776
+ trigger,
3777
+ ...opts.start ? { startDate: opts.start } : {},
3778
+ ...opts.end ? { endDate: opts.end } : {},
3779
+ ...opts.disabled ? { enabled: false } : {}
3780
+ };
3781
+ }
3782
+ function createWithWorkDir(store, input2, homeDir) {
3783
+ const created = store.create({ ...input2, workDir: input2.workDir ?? homeDir });
3784
+ if (input2.workDir) return created;
3785
+ const dir = automationWorkDir(created.id, homeDir);
3786
+ return store.update(created.id, { workDir: dir }) ?? created;
3787
+ }
3788
+ function applyDefaultAllowlist(store, def, opts) {
3789
+ const userGave = (opts.allowTool?.length ?? 0) > 0 || (opts.allowOperation?.length ?? 0) > 0 || (opts.allowRule?.length ?? 0) > 0;
3790
+ if (userGave || def.permission === "plan" || def.permission === "bypass") return def;
3791
+ return store.update(def.id, {
3792
+ allowOperations: ["file_read"],
3793
+ allowRules: [`file_write(${def.workDir}/**)`]
3794
+ }) ?? def;
3795
+ }
3796
+ function printSummary(def) {
3797
+ log8(t6("schedule.created", { id: def.id, name: def.name }));
3798
+ log8("");
3799
+ log8(t6("schedule.created_next", { id: def.id.slice(0, 12) }));
3800
+ }
3801
+
3802
+ // src/schedule/doctor.ts
3803
+ import { diagnoseSchedules, pruneSchedules, repairSchedules } from "@epoch-agent/core";
3804
+ import { t as t7 } from "@epoch-agent/infra";
3805
+ async function runDoctor(opts) {
3806
+ const ctx = await openSchedule();
3807
+ try {
3808
+ const deps = { store: ctx.store, backend: ctx.backend, registrar: ctx.registrar };
3809
+ const report = await diagnoseSchedules(deps);
3810
+ if (report.unsupported) {
3811
+ warn2(t7("schedule.doctor_unsupported"));
3812
+ return;
3813
+ }
3814
+ if (report.drifts.length === 0) {
3815
+ log8(t7("schedule.doctor_clean", { n: report.checked }));
3816
+ return;
3817
+ }
3818
+ log8(t7("schedule.doctor_found", { n: report.drifts.length }));
3819
+ for (const drift of report.drifts) {
3820
+ log8(
3821
+ ` \xB7 ${driftLabel(drift.kind)} ${drift.name ?? drift.scheduleId ?? ""} [${drift.osTaskId || "\u2014"}]${drift.detail ? `
3822
+ ${drift.detail}` : ""}`
3823
+ );
3824
+ }
3825
+ if (opts.repair !== true && opts.prune !== true) {
3826
+ log8("");
3827
+ log8(t7("schedule.doctor_hint"));
3828
+ return;
3829
+ }
3830
+ if (opts.repair === true) {
3831
+ for (const outcome of await repairSchedules(report.drifts, deps)) {
3832
+ const label = outcome.drift.name ?? outcome.drift.scheduleId ?? "";
3833
+ if (outcome.ok) log8(t7("schedule.doctor_repaired", { name: label }));
3834
+ else
3835
+ warn2(t7("schedule.doctor_repair_failed", { name: label, detail: outcome.detail ?? "" }));
3836
+ }
3837
+ }
3838
+ if (opts.prune === true) {
3839
+ for (const outcome of await pruneSchedules(report.drifts, deps)) {
3840
+ if (outcome.ok) log8(t7("schedule.doctor_pruned", { id: outcome.drift.osTaskId }));
3841
+ else {
3842
+ warn2(
3843
+ t7("schedule.doctor_prune_failed", {
3844
+ id: outcome.drift.osTaskId,
3845
+ detail: outcome.detail ?? ""
3846
+ })
3847
+ );
3848
+ process.exitCode = EXIT_CODES.FAILURE;
3849
+ }
3850
+ }
3851
+ }
3852
+ } finally {
3853
+ ctx.close();
3854
+ }
3855
+ }
3856
+
3857
+ // src/schedule/manage.ts
3858
+ import { automationLogsDir, t as t8 } from "@epoch-agent/infra";
3859
+ async function runSetEnabled(idOrPrefix, enabled) {
3860
+ const ctx = await openSchedule();
3861
+ try {
3862
+ const def = mustFind(ctx.store, idOrPrefix);
3863
+ const outcome = await ctx.registrar.setEnabled(def, enabled);
3864
+ reportRegistration(outcome, false);
3865
+ log8(
3866
+ enabled ? t8("schedule.enabled_done", { name: def.name }) : t8("schedule.disabled_done", { name: def.name })
3867
+ );
3868
+ } finally {
3869
+ ctx.close();
3870
+ }
3871
+ }
3872
+ async function runRemove(idOrPrefix) {
3873
+ const ctx = await openSchedule();
3874
+ try {
3875
+ const def = mustFind(ctx.store, idOrPrefix);
3876
+ if (def.osTaskId) reportRegistration(await ctx.registrar.unregister(def), false);
3877
+ ctx.store.remove(def.id);
3878
+ log8(t8("schedule.removed", { name: def.name }));
3879
+ log8(t8("schedule.removed_logs_kept", { dir: automationLogsDir(def.id, ctx.homeDir) }));
3880
+ } finally {
3881
+ ctx.close();
3882
+ }
3883
+ }
3884
+
3885
+ // src/schedule/run.ts
3886
+ import { t as t9 } from "@epoch-agent/infra";
3887
+ import { fireSchedule } from "@epoch-agent/runtime";
3888
+ async function runFire(scheduleId, homeDir) {
3889
+ const home = homeDir ?? EPOCH_HOME;
3890
+ const ctx = await openSchedule(home);
3891
+ let result;
3892
+ try {
3893
+ result = await fireSchedule({ scheduleId, homeDir: home, registrar: ctx.registrar });
3894
+ } finally {
3895
+ ctx.close();
3896
+ }
3897
+ if (result.missing) {
3898
+ warn2(t9("schedule.fire_missing", { id: scheduleId }));
3899
+ } else if (result.disabled) {
3900
+ warn2(t9("schedule.fire_disabled", { id: scheduleId }));
3901
+ } else {
3902
+ warn2(`${statusLabel(result.status)}${result.detail ? ` ${result.detail}` : ""}`);
3903
+ }
3904
+ process.exitCode = result.exitCode;
3905
+ }
3906
+ async function runManual(idOrPrefix, opts) {
3907
+ const ctx = await openSchedule();
3908
+ try {
3909
+ const def = mustFind(ctx.store, idOrPrefix);
3910
+ log8(t9("schedule.run_start", { name: def.name }));
3911
+ const result = await fireSchedule({
3912
+ scheduleId: def.id,
3913
+ homeDir: ctx.homeDir,
3914
+ manual: true,
3915
+ registrar: ctx.registrar
3916
+ });
3917
+ log8(`${statusLabel(result.status)}${result.detail ? ` ${result.detail}` : ""}`);
3918
+ const pending = result.run?.pendingApprovals ?? [];
3919
+ if (pending.length === 0) {
3920
+ process.exitCode = result.exitCode;
3921
+ return;
3922
+ }
3923
+ printPending(pending);
3924
+ if (opts.fix === true) {
3925
+ const added = applyFix(ctx.store, def.id, pending);
3926
+ log8(added.length > 0 ? t9("schedule.fix_done", { n: added.length }) : t9("schedule.fix_none"));
3927
+ return;
3928
+ }
3929
+ log8(t9("schedule.fix_hint", { id: def.id.slice(0, 12) }));
3930
+ process.exitCode = result.exitCode;
3931
+ } finally {
3932
+ ctx.close();
3933
+ }
3934
+ }
3935
+ function printPending(pending) {
3936
+ log8("");
3937
+ log8(t9("schedule.pending_header", { n: pending.length }));
3938
+ for (const item of pending) {
3939
+ log8(` \xB7 ${t9("schedule.pending_line", { tool: item.toolName, target: item.target })}`);
3940
+ if (item.suggestedRule) log8(` \u2192 ${item.suggestedRule}`);
3941
+ }
3942
+ }
3943
+ function applyFix(store, scheduleId, pending) {
3944
+ const def = store.get(scheduleId);
3945
+ if (!def) return [];
3946
+ const existing = new Set(def.allowRules);
3947
+ const added = [];
3948
+ for (const item of pending) {
3949
+ if (!item.suggestedRule || existing.has(item.suggestedRule)) continue;
3950
+ existing.add(item.suggestedRule);
3951
+ added.push(item.suggestedRule);
3952
+ }
3953
+ if (added.length > 0) store.update(scheduleId, { allowRules: [...existing] });
3954
+ return added;
3955
+ }
3956
+
3957
+ // src/schedule/view.ts
3958
+ import { nextRunAt, readRecording } from "@epoch-agent/core";
3959
+ import { t as t10 } from "@epoch-agent/infra";
3960
+ async function runList() {
3961
+ const ctx = await openSchedule();
3962
+ try {
3963
+ const defs = ctx.store.list();
3964
+ if (defs.length === 0) {
3965
+ log8(t10("schedule.list_empty"));
3966
+ log8(t10("schedule.list_empty_hint"));
3967
+ return;
3968
+ }
3969
+ log8(t10("schedule.list_header"));
3970
+ for (const def of defs) {
3971
+ const next = def.enabled ? formatWhen(nextRunAt(def.trigger, Date.now(), def.startDate, def.endDate) ?? void 0) : t10("schedule.disabled_mark");
3972
+ const danger = def.permission === "bypass" ? " \u{1F534}" : "";
3973
+ log8(
3974
+ ` ${def.id.slice(0, 12)} ${def.name}${danger}
3975
+ ${describeTrigger(def.trigger)} \xB7 ${t10("schedule.col_next")} ${next} \xB7 ${t10("schedule.col_last")} ${def.lastStatus ? statusLabel(def.lastStatus) : "\u2014"} \xB7 ${permissionLabel(def.permission)}`
3976
+ );
3977
+ }
3978
+ } finally {
3979
+ ctx.close();
3980
+ }
3981
+ }
3982
+ async function runShow(idOrPrefix) {
3983
+ const ctx = await openSchedule();
3984
+ try {
3985
+ const def = mustFind(ctx.store, idOrPrefix);
3986
+ log8(`${def.name} (${def.id})`);
3987
+ log8(` ${t10("schedule.field_enabled")}: ${def.enabled ? t10("schedule.yes") : t10("schedule.no")}`);
3988
+ log8(` ${t10("schedule.field_trigger")}: ${describeTrigger(def.trigger)}`);
3989
+ if (def.startDate ?? def.endDate) {
3990
+ log8(` ${t10("schedule.field_window")}: ${def.startDate ?? "\u2014"} .. ${def.endDate ?? "\u2014"}`);
3991
+ }
3992
+ log8(
3993
+ ` ${t10("schedule.field_next")}: ${formatWhen(
3994
+ nextRunAt(def.trigger, Date.now(), def.startDate, def.endDate) ?? void 0
3995
+ )}`
3996
+ );
3997
+ log8(` ${t10("schedule.field_workdir")}: ${def.workDir}`);
3998
+ if (def.model) log8(` ${t10("schedule.field_model")}: ${def.model}`);
3999
+ log8(` ${t10("schedule.field_permission")}: ${permissionLabel(def.permission)}`);
4000
+ log8(
4001
+ ` ${t10("schedule.field_limits")}: ` + t10("schedule.limits_value", {
4002
+ turns: def.maxTurns,
4003
+ budget: def.maxBudgetUsd.toFixed(2),
4004
+ minutes: Math.round(def.timeoutMs / 6e4)
4005
+ })
4006
+ );
4007
+ log8(` ${t10("schedule.field_backend")}: ${def.osBackend} ${def.osTaskId || "\u2014"}`);
4008
+ log8(` ${t10("schedule.field_prompt")}:`);
4009
+ for (const line of def.prompt.split("\n")) log8(` ${line}`);
4010
+ log8("");
4011
+ printAllowlist(def);
4012
+ log8("");
4013
+ log8(t10("schedule.field_last_run", { when: formatWhen(def.lastRunAt) }));
4014
+ } finally {
4015
+ ctx.close();
4016
+ }
4017
+ }
4018
+ function printAllowlist(def) {
4019
+ if (def.permission === "bypass") {
4020
+ log8(t10("schedule.allowlist_bypass"));
4021
+ return;
4022
+ }
4023
+ if (def.permission === "plan") {
4024
+ log8(t10("schedule.allowlist_readonly"));
4025
+ return;
4026
+ }
4027
+ const lines = [
4028
+ ...def.allowTools.map((v) => ` ${v}`),
4029
+ ...def.allowOperations.map((v) => ` ${v}`),
4030
+ ...def.allowRules.map((v) => ` ${v}`)
4031
+ ];
4032
+ log8(t10("schedule.allowlist_header"));
4033
+ log8(lines.length > 0 ? lines.join("\n") : ` ${t10("schedule.allowlist_empty")}`);
4034
+ log8(t10("schedule.allowlist_scope_note"));
4035
+ }
4036
+ async function runLogs(idOrPrefix, limit, tail) {
4037
+ const ctx = await openSchedule();
4038
+ try {
4039
+ const def = mustFind(ctx.store, idOrPrefix);
4040
+ const runs = ctx.store.listRuns(def.id, limit);
4041
+ if (runs.length === 0) {
4042
+ log8(t10("schedule.logs_empty"));
4043
+ return;
4044
+ }
4045
+ for (const run of runs) printRun(run);
4046
+ if (tail) printRecording(runs[0]);
4047
+ } finally {
4048
+ ctx.close();
4049
+ }
4050
+ }
4051
+ function printRun(run) {
4052
+ log8(
4053
+ ` ${formatWhen(run.startedAt)} ${statusLabel(run.status)} ` + t10("schedule.run_meta", {
4054
+ turns: run.turns,
4055
+ cost: run.costUsd.toFixed(4),
4056
+ manual: run.manual ? t10("schedule.run_manual") : ""
4057
+ })
4058
+ );
4059
+ if (run.reason) log8(` ${run.reason}`);
4060
+ for (const pending of run.pendingApprovals) {
4061
+ log8(
4062
+ ` \u26A0 ${t10("schedule.pending_line", { tool: pending.toolName, target: pending.target })}`
4063
+ );
4064
+ if (pending.suggestedRule) {
4065
+ log8(` ${t10("schedule.pending_fix", { rule: pending.suggestedRule })}`);
4066
+ }
4067
+ }
4068
+ }
4069
+ function printRecording(run) {
4070
+ if (!run.logPath) return;
4071
+ log8("");
4072
+ log8(t10("schedule.logs_recording", { path: run.logPath }));
4073
+ for (const envelope of readRecording(run.logPath)) {
4074
+ const event = envelope.event;
4075
+ if (event.type === "text-delta" && event.text) process.stdout.write(event.text);
4076
+ else if (event.type === "error" && event.message) log8(`
4077
+ [error] ${event.message}`);
4078
+ }
4079
+ process.stdout.write("\n");
4080
+ }
4081
+
4082
+ // src/commands/schedule.ts
4083
+ var DEFAULT_LOG_LIMIT = 10;
4084
+ function registerScheduleCommand(program2) {
4085
+ const cmd = program2.command("schedule").description(t11("cli.schedule.summary"));
4086
+ cmd.command("add").description(t11("cli.schedule.add")).option("--name <name>", t11("cli.schedule.opt_name")).option("--prompt <text>", t11("cli.schedule.opt_prompt")).option("--daily <HH:mm>", t11("cli.schedule.opt_daily")).option("--weekly <days>", t11("cli.schedule.opt_weekly")).option("--monthly <days>", t11("cli.schedule.opt_monthly")).option("--every <interval>", t11("cli.schedule.opt_every")).option("--once <YYYY-MM-DD>", t11("cli.schedule.opt_once")).option("--at <HH:mm>", t11("cli.schedule.opt_at")).option("--start <YYYY-MM-DD>", t11("cli.schedule.opt_start")).option("--end <YYYY-MM-DD>", t11("cli.schedule.opt_end")).option("--workdir <dir>", t11("cli.schedule.opt_workdir")).option("--model <model>", t11("cli.schedule.opt_model")).option("--permission <level>", t11("cli.schedule.opt_permission")).option("--allow-tool <name>", t11("cli.schedule.opt_allow_tool"), collect3, []).option("--allow-operation <type>", t11("cli.schedule.opt_allow_operation"), collect3, []).option("--allow-rule <rule>", t11("cli.schedule.opt_allow_rule"), collect3, []).option("--max-turns <n>", t11("cli.schedule.opt_max_turns")).option("--budget <usd>", t11("cli.schedule.opt_budget")).option("--timeout <minutes>", t11("cli.schedule.opt_timeout")).option("--disabled", t11("cli.schedule.opt_disabled")).option("--i-understand-bypass", t11("cli.schedule.opt_bypass_ack")).action((opts) => runAdd(opts));
4087
+ cmd.command("list", { isDefault: true }).description(t11("cli.schedule.list")).action(() => runList());
4088
+ cmd.command("show").description(t11("cli.schedule.show")).argument("<id>", t11("cli.schedule.arg_id")).action((id) => runShow(id));
4089
+ cmd.command("enable").description(t11("cli.schedule.enable")).argument("<id>", t11("cli.schedule.arg_id")).action((id) => runSetEnabled(id, true));
4090
+ cmd.command("disable").description(t11("cli.schedule.disable")).argument("<id>", t11("cli.schedule.arg_id")).action((id) => runSetEnabled(id, false));
4091
+ cmd.command("rm").alias("remove").description(t11("cli.schedule.rm")).argument("<id>", t11("cli.schedule.arg_id")).action((id) => runRemove(id));
4092
+ cmd.command("run").description(t11("cli.schedule.run")).argument("<id>", t11("cli.schedule.arg_id")).option("--fix", t11("cli.schedule.opt_fix")).action((id, opts) => runManual(id, opts));
4093
+ cmd.command("logs").description(t11("cli.schedule.logs")).argument("<id>", t11("cli.schedule.arg_id")).option("-n, --limit <n>", t11("cli.schedule.opt_limit"), String(DEFAULT_LOG_LIMIT)).option("--tail", t11("cli.schedule.opt_tail")).action(
4094
+ (id, opts) => runLogs(id, Number(opts.limit ?? DEFAULT_LOG_LIMIT), opts.tail === true)
4095
+ );
4096
+ cmd.command("fire").description(t11("cli.schedule.fire")).argument("<id>", t11("cli.schedule.arg_id")).option("--home <dir>", t11("cli.schedule.opt_home")).action((id, opts) => runFire(id, opts.home));
4097
+ cmd.command("doctor").description(t11("cli.schedule.doctor")).option("--repair", t11("cli.schedule.opt_repair")).option("--prune", t11("cli.schedule.opt_prune")).action((opts) => runDoctor(opts));
4098
+ }
4099
+ function collect3(value, previous) {
4100
+ return [...previous, value];
4101
+ }
4102
+
4103
+ // src/commands/status.ts
4104
+ import { existsSync as existsSync7 } from "fs";
4105
+ import { statfs } from "fs/promises";
4106
+ import {
4107
+ describeIsolation,
4108
+ getConfigIssues,
4109
+ loadConfig as loadConfig3,
4110
+ SANDBOX_COVERS,
4111
+ SANDBOX_EXCLUDES
4112
+ } from "@epoch-agent/core";
4113
+ import { confine, resolveProfile, t as t12 } from "@epoch-agent/infra";
4114
+ import { hasFailure } from "@epoch-agent/protocol";
4115
+
4116
+ // src/statusline.ts
4117
+ import { execFile as execFile2 } from "child_process";
4118
+ import { sanitizeToolOutput } from "@epoch-agent/core";
4119
+ import { shellSpawnArgs } from "@epoch-agent/infra";
4120
+ var TIMEOUT_MS2 = 2e3;
4121
+ var MAX_WIDTH = 60;
4122
+ var MAX_BUFFER = 64 * 1024;
4123
+ function formatStatusLine(stdout) {
4124
+ const first = sanitizeToolOutput(stdout).split("\n")[0]?.trim() ?? "";
4125
+ if (!first) return null;
4126
+ return first.length > MAX_WIDTH ? first.slice(0, MAX_WIDTH - 1) + "\u2026" : first;
4127
+ }
4128
+ function runStatusLineCommand(command, cwd) {
4129
+ const { file, args, options } = shellSpawnArgs(command);
4130
+ return new Promise((settle) => {
4131
+ execFile2(
4132
+ file,
4133
+ args,
4134
+ {
4135
+ ...options,
4136
+ cwd,
4137
+ timeout: TIMEOUT_MS2,
4138
+ maxBuffer: MAX_BUFFER,
4139
+ encoding: "utf-8",
4140
+ // 不弹黑框:Windows 上每 5 秒闪一个控制台窗口是没法用的
4141
+ windowsHide: true
4142
+ },
4143
+ (err2, stdout) => {
4144
+ if (err2) {
4145
+ const killed = err2.killed === true;
4146
+ settle({
4147
+ text: null,
4148
+ error: killed ? `\u8D85\u8FC7 ${TIMEOUT_MS2}ms \u6CA1\u8FD4\u56DE` : err2.message.trim()
4149
+ });
4150
+ return;
4151
+ }
4152
+ settle({ text: formatStatusLine(stdout) });
4153
+ }
4154
+ );
4155
+ });
4156
+ }
4157
+ async function probeStatusLine(config, cwd) {
4158
+ const command = config.statusLine?.command?.trim();
4159
+ if (!command) return null;
4160
+ const result = await runStatusLineCommand(command, cwd);
4161
+ if (result.text === null) {
4162
+ return { command, ok: false, detail: result.error ?? "\u6CA1\u6709\u8F93\u51FA" };
4163
+ }
4164
+ return { command, ok: true, detail: result.text };
4165
+ }
4166
+
4167
+ // src/commands/status.ts
4168
+ var log9 = (msg) => {
4169
+ process.stdout.write(msg + "\n");
4170
+ };
4171
+ var STATUS_MARK = {
4172
+ ok: "\u2713",
4173
+ warn: "\u26A0",
4174
+ failed: "\u2717",
4175
+ skipped: "\u25CB"
4176
+ };
4177
+ function formatBytes(bytes) {
4178
+ const units = ["B", "KB", "MB", "GB", "TB", "PB"];
4179
+ let n = bytes;
4180
+ let i = 0;
4181
+ while (n >= 1e3 && i < units.length - 1) {
4182
+ n /= 1e3;
4183
+ i += 1;
4184
+ }
4185
+ return `${n < 10 && i > 0 ? n.toFixed(1) : Math.round(n)}${units[i]}`;
4186
+ }
4187
+ async function describeDisk(path = ".") {
4188
+ try {
4189
+ const fs = await statfs(path);
4190
+ return formatBytes(fs.bavail * fs.bsize);
4191
+ } catch {
4192
+ return void 0;
4193
+ }
4194
+ }
4195
+ function describeConfig() {
4196
+ if (!existsSync7(CONFIG_PATH)) return "\u4E0D\u5B58\u5728";
4197
+ loadConfig3();
4198
+ const n = getConfigIssues().length;
4199
+ return n === 0 ? "\u5B58\u5728" : `\u5B58\u5728\uFF0C\u4F46\u6709 ${n} \u5904\u95EE\u9898\uFF08\u8DD1 epoch doctor \u770B\u662F\u54EA\u4E9B\u5B57\u6BB5\uFF09`;
4200
+ }
4201
+ async function describeSecretBackend() {
4202
+ try {
4203
+ const { createSecretStore } = await import("@epoch-agent/infra");
4204
+ const store = await createSecretStore();
4205
+ return store.encrypted ? store.detail : `\u26A0 ${store.detail}`;
4206
+ } catch (err2) {
4207
+ return `\u63A2\u6D4B\u5931\u8D25\uFF08${err2 instanceof Error ? err2.message : String(err2)}\uFF09`;
4208
+ }
4209
+ }
4210
+ function workspaceLines(rt) {
4211
+ const lines = [` \u9879\u76EE\u6839: ${rt.workspace.root}`];
4212
+ lines.push(
4213
+ rt.trusted ? " \u4FE1\u4EFB\u72B6\u6001: \u5DF2\u4FE1\u4EFB\uFF08\u9879\u76EE\u6307\u4EE4\u6587\u4EF6\u4F1A\u52A0\u8F7D\uFF09" : " \u4FE1\u4EFB\u72B6\u6001: \u672A\u4FE1\u4EFB\uFF08EPOCH.md / AGENTS.md / CLAUDE.md \u4E0D\u4F1A\u52A0\u8F7D\uFF0Cepoch trust add \u53EF\u6388\u6743\uFF09"
4214
+ );
4215
+ lines.push(
4216
+ rt.workspace.extra.length === 0 ? " \u989D\u5916\u76EE\u5F55: \u65E0\uFF08--add-dir \u53EF\u52A0\uFF09" : ` \u989D\u5916\u76EE\u5F55: ${rt.workspace.extra.length} \u4E2A`
4217
+ );
4218
+ for (const dir of rt.workspace.extra) lines.push(` \xB7 ${dir}`);
4219
+ return lines;
4220
+ }
4221
+ function sandboxLines(cwd) {
4222
+ const iso = describeIsolation();
4223
+ const lines = [`
4224
+ ${t12("cli.doctor.sandbox_head")}`];
4225
+ if (iso.backend === "none") {
4226
+ lines.push(` ${STATUS_MARK.skipped} ${iso.detail}`);
4227
+ lines.push(` ${STATUS_MARK.warn} ${t12("cli.doctor.sandbox_absent_tools")}`);
4228
+ return lines;
4229
+ }
4230
+ lines.push(
4231
+ ` ${STATUS_MARK.ok} ${t12("cli.doctor.sandbox_backend", { backend: iso.backend, platform: iso.platform })}`
4232
+ );
4233
+ lines.push(` \xB7 ${t12("cli.doctor.sandbox_code_exec", { detail: iso.detail })}`);
4234
+ const probe = (mode) => confine("/bin/sh", ["-c", "true"], { mode, workspaceRoot: cwd, allowNetwork: true });
4235
+ for (const mode of ["workspace-write", "read-only"]) {
4236
+ const c = probe(mode);
4237
+ if (!c.confined) continue;
4238
+ lines.push(` \xB7 ${t12("cli.doctor.sandbox_terminal", { mode, enforcement: c.enforcement })}`);
4239
+ if (c.enforcement === "partial") lines.push(` ${t12("cli.doctor.sandbox_partial_devnull")}`);
4240
+ }
4241
+ const writable = probe("workspace-write");
4242
+ if (writable.confined) {
4243
+ lines.push(` \xB7 ${t12("cli.doctor.sandbox_writable")}`);
4244
+ for (const dir of writable.writableDirs) lines.push(` \xB7 ${dir}`);
4245
+ }
4246
+ lines.push(` \xB7 ${t12("cli.doctor.sandbox_covers", { tools: SANDBOX_COVERS.join(", ") })}`);
4247
+ lines.push(
4248
+ SANDBOX_EXCLUDES.length === 0 ? ` ${STATUS_MARK.ok} ${t12("cli.doctor.sandbox_excludes_none")}` : ` ${STATUS_MARK.warn} ${t12("cli.doctor.sandbox_excludes", { tools: SANDBOX_EXCLUDES.join(", ") })}`
4249
+ );
4250
+ return lines;
4251
+ }
4252
+ async function statusLineLines(config, cwd) {
4253
+ const probe = await probeStatusLine(config, cwd);
4254
+ if (!probe) return [];
4255
+ return [
4256
+ "\n\u81EA\u5B9A\u4E49\u72B6\u6001\u680F:",
4257
+ ` \u547D\u4EE4: ${probe.command}`,
4258
+ probe.ok ? ` \u2713 \u663E\u793A: ${probe.detail}` : ` \u26A0 \u8DD1\u4E0D\u8D77\u6765\uFF08\u72B6\u6001\u680F\u4F1A\u7559\u7A7A\uFF09: ${probe.detail}`
4259
+ ];
4260
+ }
4261
+ function registerStatusCommands(program2) {
4262
+ program2.command("status").description("\u663E\u793A\u7CFB\u7EDF\u72B6\u6001").action(async () => {
4263
+ log9(
4264
+ [
4265
+ `\u7248\u672C: ${VERSION}`,
4266
+ `Node: ${process.version} (${process.platform}/${process.arch})`,
4267
+ `\u914D\u7F6E\u76EE\u5F55: ${EPOCH_HOME}`,
4268
+ `Profile: ${resolveProfile()}`,
4269
+ `\u51ED\u8BC1: ${hasCredentials() ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`,
4270
+ `\u51ED\u636E\u5B58\u50A8: ${await describeSecretBackend()}`,
4271
+ `config.yaml: ${describeConfig()}`,
4272
+ `.env: ${existsSync7(ENV_PATH) ? "\u5B58\u5728" : "\u4E0D\u5B58\u5728"}`
4273
+ ].join("\n")
4274
+ );
4275
+ });
4276
+ program2.command("doctor").description("\u8BCA\u65AD\u7CFB\u7EDF\u73AF\u5883").action(async () => {
4277
+ log9(`\u2713 Node.js: ${process.version}`);
4278
+ const avail = await describeDisk();
4279
+ if (avail) log9(`\u2713 \u78C1\u76D8: ${avail} \u53EF\u7528`);
4280
+ log9(`${hasCredentials() ? "\u2713" : "\u25CB"} \u51ED\u8BC1`);
4281
+ const configIssues = existsSync7(CONFIG_PATH) ? (loadConfig3(), getConfigIssues()) : [];
4282
+ const configMark = !existsSync7(CONFIG_PATH) ? "\u25CB" : configIssues.length > 0 ? "\u26A0" : "\u2713";
4283
+ log9(`${configMark} config: ${CONFIG_PATH}`);
4284
+ try {
4285
+ const { buildRuntime: buildRuntime4 } = await import("@epoch-agent/runtime");
4286
+ const rt = await buildRuntime4({ installSignalHandlers: true });
4287
+ log9("\n\u5DE5\u4F5C\u533A:");
4288
+ for (const line of workspaceLines(rt)) log9(line);
4289
+ for (const line of sandboxLines(rt.workspace.root)) log9(line);
4290
+ for (const line of await statusLineLines(rt.config, process.cwd())) log9(line);
4291
+ log9("\n\u6A21\u5757\u81EA\u68C0:");
4292
+ const order = { failed: 0, warn: 1, skipped: 2, ok: 3 };
4293
+ const sorted = [...rt.diagnosticList].sort(
4294
+ (a, b) => (order[a.status] ?? 9) - (order[b.status] ?? 9)
4295
+ );
4296
+ for (const d of sorted) log9(` ${STATUS_MARK[d.status]} ${d.module}: ${d.detail}`);
4297
+ await rt.dispose();
4298
+ if (hasFailure(rt.diagnosticList)) {
4299
+ const bad = rt.diagnosticList.filter((d) => d.status === "failed").length;
4300
+ log9(`
4301
+ \u2717 ${bad} \u4E2A\u6A21\u5757\u8D77\u4E0D\u6765`);
4302
+ process.exitCode = EXIT_CODES.FAILURE;
4303
+ }
4304
+ } catch (err2) {
4305
+ log9(`
4306
+ \u2717 \u88C5\u914D\u5931\u8D25: ${err2 instanceof Error ? err2.message : String(err2)}`);
4307
+ }
4308
+ });
4309
+ }
4310
+
4311
+ // src/commands/trust.ts
4312
+ import { existsSync as existsSync8, realpathSync as realpathSync3 } from "fs";
4313
+ import { resolve as resolve3 } from "path";
4314
+ import { loadConfig as loadConfig4, TrustManager as TrustManager3 } from "@epoch-agent/core";
4315
+ import { trustPath as trustPath3 } from "@epoch-agent/infra";
4316
+ var log10 = (msg) => process.stdout.write(msg + "\n");
4317
+ var warn3 = (msg) => process.stderr.write(msg + "\n");
4318
+ var SCOPE_LABEL = {
4319
+ directory: "\u4EC5\u6B64\u76EE\u5F55",
4320
+ "directory-tree": "\u542B\u5B50\u76EE\u5F55"
4321
+ };
4322
+ var LEVEL_LABEL = {
4323
+ trusted: "\u5DF2\u4FE1\u4EFB",
4324
+ untrusted: "\u5DF2\u62D2\u7EDD",
4325
+ unknown: "\u672A\u51B3\u5B9A"
4326
+ };
4327
+ function physicalPath(input2) {
4328
+ const abs = resolve3(input2);
4329
+ if (!existsSync8(abs)) return abs;
4330
+ try {
4331
+ return realpathSync3(abs);
4332
+ } catch {
4333
+ return abs;
4334
+ }
4335
+ }
4336
+ function registerTrustCommand(program2) {
4337
+ const cmd = program2.command("trust").description("\u7BA1\u7406\u5DE5\u4F5C\u533A\u4FE1\u4EFB\uFF08\u54EA\u4E9B\u76EE\u5F55\u53EF\u4EE5\u628A\u81EA\u5DF1\u7684\u9879\u76EE\u6307\u4EE4\u52A0\u8FDB system prompt\uFF09");
4338
+ cmd.command("list", { isDefault: true }).description("\u5217\u51FA\u6240\u6709\u4FE1\u4EFB\u8BB0\u5F55").action(() => withTrust((tm) => printList3(tm)));
4339
+ cmd.command("add").description("\u4FE1\u4EFB\u4E00\u4E2A\u76EE\u5F55").argument("[path]", "\u76EE\u5F55\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09").option("--tree", "\u8FDE\u540C\u6240\u6709\u5B50\u76EE\u5F55\u4E00\u8D77\u4FE1\u4EFB\uFF08\u542B\u4EE5\u540E\u65B0\u5EFA / clone \u8FDB\u6765\u7684\uFF09").action((path, opts) => {
4340
+ withTrust((tm) => {
4341
+ const dir = physicalPath(path ?? process.cwd());
4342
+ tm.record(dir, "trusted", opts.tree ? "directory-tree" : "directory");
4343
+ log10(
4344
+ `\u2713 \u5DF2\u4FE1\u4EFB ${dir}\uFF08${opts.tree ? SCOPE_LABEL["directory-tree"] : SCOPE_LABEL.directory}\uFF09`
4345
+ );
4346
+ });
4347
+ });
4348
+ cmd.command("deny").description("\u660E\u786E\u62D2\u7EDD\u4E00\u4E2A\u76EE\u5F55\uFF08\u4E4B\u540E\u4E0D\u518D\u8BE2\u95EE\uFF0C\u4E5F\u4E0D\u52A0\u8F7D\u5B83\u7684\u9879\u76EE\u6307\u4EE4\uFF09").argument("[path]", "\u76EE\u5F55\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09").option("--tree", "\u8FDE\u540C\u6240\u6709\u5B50\u76EE\u5F55\u4E00\u8D77\u62D2\u7EDD").action((path, opts) => {
4349
+ withTrust((tm) => {
4350
+ const dir = physicalPath(path ?? process.cwd());
4351
+ tm.record(dir, "untrusted", opts.tree ? "directory-tree" : "directory");
4352
+ log10(
4353
+ `\u2717 \u5DF2\u62D2\u7EDD ${dir}\uFF08${opts.tree ? SCOPE_LABEL["directory-tree"] : SCOPE_LABEL.directory}\uFF09`
4354
+ );
4355
+ });
4356
+ });
4357
+ cmd.command("remove").alias("rm").description("\u5220\u6389\u4E00\u6761\u8BB0\u5F55\uFF0C\u8BE5\u76EE\u5F55\u56DE\u5230\u300C\u672A\u51B3\u5B9A\u300D").argument("<path>", "\u76EE\u5F55\u8DEF\u5F84").action((path) => {
4358
+ withTrust((tm) => {
4359
+ const dir = physicalPath(path);
4360
+ if (!tm.list().some((r) => resolve3(r.path) === dir)) {
4361
+ warn3(`${dir} \u4E0A\u6CA1\u6709\u4FE1\u4EFB\u8BB0\u5F55\uFF08\u7528 epoch trust \u67E5\u770B\u73B0\u6709\u8BB0\u5F55\uFF09`);
4362
+ process.exit(1);
4363
+ }
4364
+ tm.revoke(dir);
4365
+ log10(`\u5DF2\u5220\u9664 ${dir} \u7684\u8BB0\u5F55\uFF0C\u73B0\u5728\u5224\u5B9A\u4E3A\uFF1A${LEVEL_LABEL[tm.check(dir)]}`);
4366
+ });
4367
+ });
4368
+ }
4369
+ function printList3(tm) {
4370
+ if (loadConfig4().trust?.enabled === false) {
4371
+ warn3(
4372
+ "\u26A0 config.trust.enabled = false\uFF1A\u95F8\u95E8\u5DF2\u5173\u95ED\uFF0C\u4E0B\u9762\u7684\u8BB0\u5F55\u4E0D\u751F\u6548\uFF0C\u4EFB\u4F55\u76EE\u5F55\u7684\u9879\u76EE\u6307\u4EE4\u90FD\u4F1A\u52A0\u8F7D"
4373
+ );
4374
+ }
4375
+ log10(`\u4FE1\u4EFB\u8BB0\u5F55\uFF08${trustPath3(EPOCH_HOME)}\uFF09`);
4376
+ const records = [...tm.list()].sort((a, b) => a.path.localeCompare(b.path));
4377
+ if (records.length === 0) {
4378
+ log10(" (\u65E0)");
4379
+ }
4380
+ for (const r of records) {
4381
+ const mark = r.level === "trusted" ? "\u2713" : "\u2717";
4382
+ const when = r.decidedAt ? new Date(r.decidedAt).toLocaleString("zh-CN") : "\u672A\u77E5";
4383
+ log10(` ${mark} ${r.path} [${SCOPE_LABEL[r.scope]}] ${when}`);
4384
+ }
4385
+ const cwd = process.cwd();
4386
+ log10(`
4387
+ \u5F53\u524D\u76EE\u5F55 ${cwd}
4388
+ \u5224\u5B9A\uFF1A${LEVEL_LABEL[tm.check(cwd)]}`);
4389
+ }
4390
+ function withTrust(fn) {
4391
+ const tm = new TrustManager3(trustPath3(EPOCH_HOME));
4392
+ for (const err2 of tm.loadErrors) warn3(`\u26A0 ${err2}`);
4393
+ try {
4394
+ fn(tm);
4395
+ } catch (err2) {
4396
+ warn3(`\u9519\u8BEF: ${err2 instanceof Error ? err2.message : String(err2)}`);
4397
+ process.exit(1);
4398
+ }
4399
+ }
4400
+
4401
+ // src/commands/upgrade.ts
4402
+ var log11 = (msg) => process.stdout.write(`${msg}
4403
+ `);
4404
+ function registerUpgradeCommand(program2) {
4405
+ program2.command("upgrade").description("\u68C0\u67E5\u65B0\u7248\u672C\u5E76\u7ED9\u51FA\u5347\u7EA7\u547D\u4EE4").action(async () => {
4406
+ const self = selfPackage();
4407
+ const info = getInstallationInfo(self.name);
4408
+ log11(`\u5F53\u524D\u7248\u672C: ${self.version}`);
4409
+ log11(`\u5B89\u88C5\u65B9\u5F0F: ${info.packageManager}${info.isGlobal ? "\uFF08\u5168\u5C40\uFF09" : ""} \u2014\u2014 ${info.note}`);
4410
+ const notice = isUpdateCheckDisabled() ? readUpdateNotice() : await refreshUpdateCache({ ttlMs: 0 }).catch(() => null);
4411
+ if (notice) log11(`
4412
+ \u6709\u65B0\u7248\u672C: ${notice.current} \u2192 ${notice.latest}`);
4413
+ else log11("\n\u6CA1\u6709\u67E5\u5230\u66F4\u65B0\uFF08\u5DF2\u662F\u6700\u65B0\uFF0C\u6216 registry \u6682\u65F6\u4E0D\u53EF\u8FBE\uFF09");
4414
+ if (info.updateCommand) {
4415
+ log11(`
4416
+ \u5347\u7EA7\u8BF7\u81EA\u5DF1\u6267\u884C:
4417
+ ${info.updateCommand}`);
4418
+ log11("\n\uFF08epoch \u4E0D\u4F1A\u81EA\u52A8\u66FF\u4F60\u5347\u7EA7\uFF1A\u5168\u5C40\u76EE\u5F55\u6743\u9650\u3001\u5305\u7BA1\u7406\u5668\u5DEE\u5F02\u3001\u8FDB\u7A0B\u81EA\u5DF1\u8986\u76D6\u81EA\u5DF1\u90FD\u662F\u5751\uFF09");
4419
+ }
4420
+ });
4421
+ program2.command(UPDATE_CHECK_SUBCOMMAND, { hidden: true }).description("\u5185\u90E8\u547D\u4EE4\uFF1A\u540E\u53F0\u5237\u65B0\u7248\u672C\u7F13\u5B58").action(async () => {
4422
+ await refreshUpdateCache().catch(() => null);
4423
+ });
4424
+ }
4425
+
4426
+ // src/commands/web.ts
4427
+ import { t as t13 } from "@epoch-agent/infra";
4428
+ import { buildRuntime as buildRuntime3 } from "@epoch-agent/runtime";
4429
+ import {
4430
+ createWebServer,
4431
+ decideBinding,
4432
+ DEFAULT_WEB_HOST,
4433
+ DEFAULT_WEB_PORT
4434
+ } from "@epoch-agent/server";
4435
+
4436
+ // src/commands/web-open.ts
4437
+ import { execFile as execFile3 } from "child_process";
4438
+ function isBrowsableUrl(url) {
4439
+ try {
4440
+ const { protocol } = new URL(url);
4441
+ return protocol === "http:" || protocol === "https:";
4442
+ } catch {
4443
+ return false;
4444
+ }
4445
+ }
4446
+ function browserLaunchArgv(url, platform = process.platform) {
4447
+ if (!isBrowsableUrl(url)) return null;
4448
+ if (platform === "darwin") return { command: "open", args: [url] };
4449
+ if (platform === "win32") {
4450
+ return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url] };
4451
+ }
4452
+ return null;
4453
+ }
4454
+ function openInBrowser(url) {
4455
+ const launch = browserLaunchArgv(url);
4456
+ if (!launch) return false;
4457
+ try {
4458
+ execFile3(launch.command, launch.args, { windowsHide: true }, () => {
4459
+ });
4460
+ return true;
4461
+ } catch {
4462
+ return false;
4463
+ }
4464
+ }
4465
+
4466
+ // src/commands/web.ts
4467
+ var out = (msg) => {
4468
+ process.stdout.write(`${msg}
4469
+ `);
4470
+ };
4471
+ var err = (msg) => {
4472
+ process.stderr.write(`${msg}
4473
+ `);
4474
+ };
4475
+ function registerWebCommand(program2) {
4476
+ program2.command("web").description(t13("cli.web.summary")).option("-p, --port <port>", t13("cli.web.opt_port", { port: DEFAULT_WEB_PORT })).option("--host <host>", t13("cli.web.opt_host", { host: DEFAULT_WEB_HOST })).option("--token <token>", t13("cli.web.opt_token")).option("--no-open", t13("cli.web.opt_no_open")).option("--json", t13("cli.web.opt_json")).action(async (opts, command) => {
4477
+ await runWeb({ ...opts, json: jsonRequested(opts.json, command.parent?.opts()["json"]) });
4478
+ });
4479
+ }
4480
+ function jsonRequested(own, fromProgram) {
4481
+ return own === true || fromProgram === true;
4482
+ }
4483
+ async function runWeb(opts) {
4484
+ const port = parsePort2(opts.port);
4485
+ if (port === null) return fail(`\u7AEF\u53E3\u5FC5\u987B\u662F\u6570\u5B57\uFF1A${String(opts.port)}`);
4486
+ const binding = decideBinding({
4487
+ ...opts.host === void 0 ? {} : { host: opts.host },
4488
+ ...port === void 0 ? {} : { port },
4489
+ ...opts.token === void 0 ? {} : { token: opts.token }
4490
+ });
4491
+ if (!binding.ok) return fail(binding.error);
4492
+ if (!await maybePromptForTrust()) process.exit(1);
4493
+ if (!await maybePromptForExternalImports()) process.exit(1);
4494
+ const server = await boot(binding);
4495
+ if (!server) return;
4496
+ const json = opts.json === true;
4497
+ const say = json ? err : out;
4498
+ const lines = announceLines({
4499
+ url: server.url,
4500
+ host: server.host,
4501
+ port: server.port,
4502
+ token: server.token,
4503
+ lanExposed: binding.lanExposed,
4504
+ placeholder: server.webRoot === void 0,
4505
+ json
4506
+ });
4507
+ if (lines.machine) out(lines.machine);
4508
+ for (const line of lines.human) say(line);
4509
+ if (opts.open && !openInBrowser(server.url)) say(BROWSER_FAILED);
4510
+ installShutdown(server, say);
4511
+ }
4512
+ var MISSING_WEB_ROOT = "\u524D\u7AEF\u4EA7\u7269\u6CA1\u627E\u5230\uFF0C\u672C\u6B21\u53EA\u63D0\u4F9B REST / SSE\uFF0C\u6253\u5F00\u662F\u5360\u4F4D\u9875\u3002\u8DD1\u4E00\u6B21 pnpm build \u518D\u8BD5";
4513
+ var BROWSER_FAILED = " \uFF08\u6CA1\u80FD\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u590D\u5236\u4E0A\u9762\u90A3\u6761\u5730\u5740\u5373\u53EF\uFF09";
4514
+ async function boot(binding) {
4515
+ const runtime = await buildRuntime3({
4516
+ // **显式 false,且不许改成 true。** runtime 装的那个处理器是
4517
+ // `dispose(); process.exit(0)`,它注册得比下面 installShutdown() 早,
4518
+ // 于是 Ctrl+C 会在 HTTP 服务关闭、挂起审批 abandon 之前就把进程打死。
4519
+ // web 的收尾顺序是 §4.3 定的:abandon + abort → 断连接 → dispose
4520
+ installSignalHandlers: false,
4521
+ // 浏览器**能**弹审批框,所以这里是交互式的。不显式给的话
4522
+ // `isNonInteractive()` 会去猜 TTY —— 从桌面启动器拉起时没有 TTY,
4523
+ // 于是 `config.headless.allowTools` 这份只该在 CI 里生效的预授权白名单
4524
+ // 会在一个完全能弹框的界面下悄悄生效,审批被跳过而没人知道
4525
+ interactive: true
4526
+ });
4527
+ if (!runtime.session) {
4528
+ await runtime.dispose();
4529
+ fail(`\u542F\u52A8\u5931\u8D25\uFF1Aprovider \u4E0D\u53EF\u7528\uFF0C\u8FD0\u884C epoch model \u914D\u7F6E
4530
+ ${runtime.diagnostics.join("\n")}`);
4531
+ return null;
4532
+ }
4533
+ const created = await createWebServer({
4534
+ runtime,
4535
+ version: VERSION,
4536
+ // **不再传 `artifactsRoot`**(方案 54 §二):缺省就是 `runtime.artifactsRoot`,
4537
+ // 也就是引擎这次真的往里写产物的那个目录。以前这里是
4538
+ // `artifactsDir(undefined, runtime.config.homeDir)` —— 算得对,但它是**第二处**
4539
+ // 在算同一个路径,而第二处存在本身就是那个 bug 的形状:嵌入宿主照抄这一行、
4540
+ // 少传一个 homeDir,就是产物 404 而全链路 200。
4541
+ // CLI 自己也走缺省,是为了让宿主走的那条路每天被跑到。
4542
+ //
4543
+ // **不再传 workDir**(决定 18):工作区变成逐会话绑定之后,「这个服务的工作
4544
+ // 目录」不再是一个能回答问题的事实。服务端问的是 `runtime.workspaces`,
4545
+ // 而引导会话那份绑定就是 `buildRuntime()` 里的 workDir(默认 `process.cwd()`)——
4546
+ // 同一个值,但只有一个真源
4547
+ host: binding.host,
4548
+ port: binding.port,
4549
+ // 用上面那一次判定产出的 token,不让 server 再生成一个 ——
4550
+ // 两个 token 里只有一个会被写进 URL,另一个就是纯粹的困惑来源
4551
+ token: binding.token
4552
+ // **不传 webRoot**:前端产物 2026-08-14 起随 server 一起发,由它自己定位
4553
+ // (`server/src/web-root.ts`)。cli 原来那份探测是同一段逻辑的第二个副本,
4554
+ // 而副本只有 cli 这一条路走得到 —— 嵌入宿主照样是占位页
4555
+ });
4556
+ if (!created.ok) {
4557
+ await runtime.dispose();
4558
+ fail(created.error);
4559
+ return null;
4560
+ }
4561
+ return created.server;
4562
+ }
4563
+ function announceLines(input2) {
4564
+ const human = [];
4565
+ if (!input2.json) {
4566
+ human.push(` Epoch Web http://${input2.host}:${input2.port}`);
4567
+ human.push(` \u6253\u5F00\u8FD9\u6761\uFF08\u542B\u4E00\u6B21\u6027 token\uFF09\uFF1A
4568
+ ${input2.url}`);
4569
+ }
4570
+ if (input2.lanExposed) {
4571
+ human.push(" \u26A0 \u5DF2\u66B4\u9732\u5230\u56DE\u73AF\u4E4B\u5916\uFF1A\u540C\u7F51\u6BB5\u7684\u4EFB\u4F55\u4EBA\u62FF\u5230\u8FD9\u6761 URL \u90FD\u80FD\u64CD\u4F5C\u4F60\u7684 agent");
4572
+ }
4573
+ if (input2.placeholder) human.push(` \u26A0 ${MISSING_WEB_ROOT}`);
4574
+ if (!input2.json) return { human };
4575
+ const machine = JSON.stringify({
4576
+ url: input2.url,
4577
+ host: input2.host,
4578
+ port: input2.port,
4579
+ token: input2.token
4580
+ });
4581
+ return { machine, human };
4582
+ }
4583
+ function installShutdown(server, say) {
4584
+ let shuttingDown = false;
4585
+ const onSignal = () => {
4586
+ if (shuttingDown) process.exit(1);
4587
+ shuttingDown = true;
4588
+ say("\n \u6536\u5C3E\u4E2D\u2026");
4589
+ server.close().then(
4590
+ () => process.exit(0),
4591
+ () => process.exit(1)
4592
+ );
4593
+ };
4594
+ process.on("SIGINT", onSignal);
4595
+ process.on("SIGTERM", onSignal);
4596
+ }
4597
+ function parsePort2(raw) {
4598
+ if (raw === void 0) return void 0;
4599
+ if (!/^\d+$/.test(raw.trim())) return null;
4600
+ return Number.parseInt(raw.trim(), 10);
4601
+ }
4602
+ function fail(message) {
4603
+ err(message);
4604
+ process.exit(1);
4605
+ }
4606
+
4607
+ // src/index.ts
4608
+ var program = new Command();
4609
+ program.name("epoch").description("Epoch Agent CLI \u667A\u80FD\u4F53");
4610
+ registerConfigCommand(program);
4611
+ registerMcpCommand(program);
4612
+ registerModelCommand(program);
4613
+ registerStatusCommands(program);
4614
+ registerSessionsCommand(program);
4615
+ registerAgentsCommand(program);
4616
+ registerPluginCommand(program);
4617
+ registerTrustCommand(program);
4618
+ registerScheduleCommand(program);
4619
+ registerUpgradeCommand(program);
4620
+ registerWebCommand(program);
4621
+ registerCompletionCommand(program);
4622
+ registerRunCommand(program);
4623
+ function isMetaInvocation(argv) {
4624
+ return argv.slice(2).some((a) => ["-h", "--help", "-V", "--version", "help"].includes(a));
4625
+ }
4626
+ if (!isMetaInvocation(process.argv)) {
4627
+ scheduleUpdateCheck();
4628
+ notifyUpdateOnExit();
4629
+ }
4630
+ program.parseAsync().catch(reportFatal);
4631
+ export {
4632
+ isMetaInvocation
4633
+ };
4634
+ /**
4635
+ * @license
4636
+ * Copyright 2025 Google LLC
4637
+ * SPDX-License-Identifier: Apache-2.0
4638
+ *
4639
+ * 改编自 gemini-cli `packages/cli/src/utils/installationInfo.ts`,
4640
+ * 按 epoch 的包名与目录形态调整,去掉了自动更新分支(epoch 只提示不代跑)。
4641
+ * 另修掉了原版同样存在的三处 Windows 误判(见下方「Windows 上的坑」)。
4642
+ */