agent-purge 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CONTRIBUTING.md +64 -0
  2. package/LICENSE +21 -21
  3. package/README.md +169 -138
  4. package/backends/index.mjs +204 -244
  5. package/bench/bench-armor.mjs +844 -832
  6. package/bench/capture-hot.mjs +58 -0
  7. package/bench/capture-provider.mjs +66 -0
  8. package/bench/capture-report.mjs +15 -0
  9. package/bench/capture-server-hot.mjs +55 -0
  10. package/bench/capture-system.mjs +36 -0
  11. package/bench/judge-armor.mjs +119 -119
  12. package/bin/agent-purge.mjs +956 -780
  13. package/docs/LEGAL.md +27 -0
  14. package/docs/architecture.md +39 -0
  15. package/docs/compatibility.md +78 -0
  16. package/docs/images/console-desktop.png +0 -0
  17. package/docs/images/console-mobile.png +0 -0
  18. package/docs/releasing.md +87 -0
  19. package/docs/usage.md +158 -0
  20. package/lib/activation.mjs +38 -0
  21. package/lib/apply.mjs +227 -0
  22. package/lib/armed.mjs +38 -38
  23. package/lib/armor.mjs +225 -222
  24. package/lib/caps.mjs +182 -173
  25. package/lib/capture.mjs +324 -0
  26. package/lib/clean.mjs +124 -0
  27. package/lib/control.mjs +60 -0
  28. package/lib/detect.mjs +74 -70
  29. package/lib/doctor.mjs +7 -7
  30. package/lib/dsh.mjs +495 -453
  31. package/lib/environment.mjs +36 -0
  32. package/lib/evidence.mjs +41 -0
  33. package/lib/extension.mjs +140 -0
  34. package/lib/footprints.mjs +59 -0
  35. package/lib/home.mjs +47 -47
  36. package/lib/launch-conflicts.mjs +24 -0
  37. package/lib/launch.mjs +57 -0
  38. package/lib/opencode.mjs +62 -0
  39. package/lib/paths.mjs +25 -0
  40. package/lib/persist.mjs +439 -280
  41. package/lib/persona-import.mjs +351 -0
  42. package/lib/personas.mjs +17 -7
  43. package/lib/private-server.mjs +54 -0
  44. package/lib/project.mjs +134 -0
  45. package/lib/restore.mjs +131 -114
  46. package/lib/rewrite.mjs +124 -0
  47. package/lib/root.mjs +37 -0
  48. package/lib/shim.mjs +203 -160
  49. package/lib/slashes.mjs +227 -628
  50. package/lib/spawn.mjs +182 -126
  51. package/lib/state.mjs +85 -137
  52. package/lib/templates/personas/neutral.md +30 -30
  53. package/lib/update.mjs +137 -0
  54. package/lib/verification.mjs +23 -0
  55. package/lib/window.mjs +58 -0
  56. package/lib/wrappers.mjs +49 -0
  57. package/magpie-plugin/README.md +44 -0
  58. package/matrix.json +325 -341
  59. package/package.json +30 -4
  60. package/web/app.js +590 -0
  61. package/web/assets/agent-purge.ico +0 -0
  62. package/web/assets/claude.ico +0 -0
  63. package/web/assets/codebuddy.png +0 -0
  64. package/web/assets/codex.webp +0 -0
  65. package/web/assets/dsh.ico +0 -0
  66. package/web/assets/grok.ico +0 -0
  67. package/web/assets/hermes.ico +0 -0
  68. package/web/assets/omp.png +0 -0
  69. package/web/assets/opencode.ico +0 -0
  70. package/web/assets/opencode2.ico +0 -0
  71. package/web/assets/pi.png +0 -0
  72. package/web/assets/zcode.png +0 -0
  73. package/web/model.mjs +49 -0
  74. package/web/server.mjs +330 -191
  75. package/web/styles.css +1072 -0
  76. package/web/ui.html +76 -1163
@@ -1,780 +1,956 @@
1
- #!/usr/bin/env node
2
- // agent-purge — 一个入口,把各家 harness 的系统提示词替换收进来。
3
- // 能力与实测数字见 matrix.json。
4
- //
5
- // 命令只有两类,别再分第三类:
6
- // 选东西(写 state.json) use / mode / install / arm / slash
7
- // 用东西(跑起来) <harness> / 裸敲已 arm 的命令名 / web / ls / status
8
- import { spawn } from "node:child_process";
9
- import { readFileSync } from "node:fs";
10
- import { dirname, join, resolve } from "node:path";
11
- import { fileURLToPath } from "node:url";
12
- import { backends, oneshots } from "../backends/index.mjs";
13
- import { resolveBin, spawnBin } from "../lib/spawn.mjs";
14
- import { installCrashHandlers, recover } from "../lib/restore.mjs";
15
- import { capsFor } from "../lib/caps.mjs";
16
- import { MODES, armedOf, loadState, patchState, setArmed, setDefault } from "../lib/state.mjs";
17
- import { ensureDataHome } from "../lib/home.mjs";
18
-
19
- // bun --compile 产物:资源目录 = exe 所在目录;普通 node:包根
20
- const ROOT = process.versions.bun ? dirname(process.execPath) : dirname(dirname(fileURLToPath(import.meta.url)));
21
- // 数据(state.json + personas/)放用户目录,npm 升级不会连人设一起清掉
22
- const DATA = ensureDataHome(ROOT).home;
23
- const matrix = JSON.parse(readFileSync(join(ROOT, "matrix.json"), "utf8"));
24
-
25
- const argv = process.argv.slice(2);
26
- installCrashHandlers();
27
- const recovered = recover(); // 上次进程被杀时留下的半改配置,开机先回滚
28
- if (recovered > 0) console.log(`[restore] 回滚了 ${recovered} 个上次残留的配置文件`);
29
- if (argv.includes("--help") || argv.includes("-h")) help(); // 永远优先于默认值
30
- const flag = (name, fallback) => {
31
- // 支持 --mode=append 这种写法(以前只认空格分隔,等号形式会被静默忽略)
32
- const inline = argv.find((a) => a.startsWith(name + "="));
33
- if (inline !== undefined) return inline.slice(name.length + 1);
34
- const i = argv.indexOf(name);
35
- return i === -1 ? fallback : argv[i + 1];
36
- };
37
- let cmd = argv[0];
38
-
39
- if (!cmd || cmd.startsWith("-")) cmd = await defaultHarness();
40
-
41
- // ── 选东西 ────────────────────────────────────────────────────────────────
42
- switch (cmd) {
43
- case "use":
44
- await useCmd();
45
- break;
46
- case "mode":
47
- modeCmd();
48
- break;
49
- case "install":
50
- case "uninstall":
51
- await installCmd(cmd, argv.slice(1));
52
- break;
53
- case "arm":
54
- case "disarm":
55
- case "register":
56
- case "注册":
57
- await armCmd(cmd === "arm" || cmd === "register" || cmd === "注册" ? "arm" : "disarm", argv.slice(1));
58
- break;
59
- case "slash":
60
- await slashCmd(argv.slice(1));
61
- break;
62
- case "status":
63
- statusCmd();
64
- break;
65
- case "ls":
66
- await lsCmd();
67
- break;
68
- case "doctor":
69
- case "detect": {
70
- const { doctor } = await import("../lib/doctor.mjs");
71
- await doctor(argv);
72
- break;
73
- }
74
- case "matrix":
75
- printMatrix();
76
- break;
77
- case "armor":
78
- await armorCli(argv.slice(1));
79
- break;
80
- case "web":
81
- webCmd();
82
- break;
83
- default:
84
- await launch(cmd);
85
- }
86
-
87
- // ── 用东西 ────────────────────────────────────────────────────────────────
88
-
89
- /** 无参:state.default 说跑谁就跑谁;没有默认就跑到 Web 去选 */
90
- async function defaultHarness() {
91
- const { state } = loadState(DATA);
92
- const harness = state.default?.harness;
93
- if (!harness) {
94
- console.log("还没有默认 harness。两种办法:\n agent-purge web 在浏览器里选,再点“设为默认执行器”\n agent-purge <harness> 直接点名跑(如 agent-purge codex)");
95
- process.exit(0);
96
- }
97
- console.log(`[default] ${harness} · ${state.default.mode} · persona=${state.persona ?? "-"}(state.json)`);
98
- return harness;
99
- }
100
-
101
- async function launch(name) {
102
- const spec = matrix.harnesses[name];
103
- if (!spec) die(`未知 harness: ${name}(可用: ${Object.keys(matrix.harnesses).join(", ")};子命令见 --help)`);
104
- if (spec.class === "unsupported") die(`${name} 做不到:${spec.reason}`);
105
-
106
- const { state } = loadState(DATA);
107
- // 由 state.json 决定注入方式;显式 --mode 永远压过它
108
- const explicitMode = flag("--mode", null);
109
- if (explicitMode !== null && !MODES.includes(explicitMode)) die(`--mode 只能是 ${MODES.join(" / ")}`);
110
- const mode = explicitMode ?? state.default?.mode ?? "replace";
111
-
112
- // 裸敲 pi 的持久位(写 state.json,扩展下一会话读它)
113
- const autoFlag = name === "pi" || name === "omp" ? flag("--auto", null) : null;
114
- if (autoFlag !== null) {
115
- if (autoFlag !== "on" && autoFlag !== "off") die("--auto 只能是 on / off");
116
- const { setPiAuto, setOmpAuto } = await import("../lib/state.mjs");
117
- if (name === "pi") setPiAuto(DATA, autoFlag === "on");
118
- else setOmpAuto(DATA, autoFlag === "on");
119
- console.log(`[auto] 裸敲 ${name} ${autoFlag === "on" ? "会" : "不会"}自动注入(已写入 state.json)`);
120
- }
121
-
122
- // pi 的人设落在哪一层:sys(默认,真系统提示词)或 user(塞进用户消息,给忽略 system 的模型)
123
- const layerFlag = name === "pi" ? flag("--layer", null) : null;
124
- if (layerFlag !== null) {
125
- if (layerFlag !== "sys" && layerFlag !== "user") die("--layer 只能是 sys / user");
126
- const { setPiUserLayer } = await import("../lib/state.mjs");
127
- setPiUserLayer(DATA, layerFlag === "user");
128
- console.log(
129
- layerFlag === "user"
130
- ? "[layer] 人设额外塞进用户消息(给忽略 system 的模型用;代价是每条消息多一段)"
131
- : "[layer] 人设走 system 层(默认)",
132
- );
133
- }
134
-
135
- // 实测策略:该 harness 的哪一层真的能注入(数据来自 matrix.json armor 段)
136
- const { plan } = await import("../lib/armor.mjs");
137
- const strategy = plan(matrix, name);
138
- if (!strategy.error) {
139
- if (strategy.requiresSeed) console.log(`[策略] ${name}:裸注入 sys 层实测 ${strategy.bare.sys}%,带人设词种子 ${strategy.persona.sys}% —— 需要 seed 层`);
140
- const noSys = strategy.blocked.find((b) => b.layer === "sys");
141
- if (noSys && mode !== "off") console.log(`[策略] ${name}:sys 层不可用(${noSys.reason}),本次只能靠 user 层`);
142
- }
143
-
144
- // 通道全废的家(闭源桌面 / 提示词在服务端)说清楚:这次只是原样启动,没注入任何东西
145
- {
146
- const c = capsFor(name);
147
- if (!c.shim.ok && !c.install.ok && !c.slash.ok && mode !== "off") {
148
- const why = [c.shim, c.install, c.slash].map((x) => x.why).filter(Boolean)[0];
149
- console.log(`[提示] ${name} 注入不进去,本次是原样启动:${why}`);
150
- }
151
- }
152
-
153
- const { createStore } = await import("../lib/personas.mjs");
154
- const store = createStore(DATA);
155
- const personaName = flag("--persona", "active");
156
- let pickedName = personaName === "active" ? store.activeId() : personaName;
157
- let picked = store.read(pickedName);
158
- // 指针可能指向已删的 persona:回退到第一个,不让启动器直接死
159
- if (!picked && personaName === "active") {
160
- const first = store.list()[0];
161
- if (first) {
162
- console.warn(`[warn] active 指向不存在的 persona: ${pickedName ?? "(空)"},本次回退到 ${first.id}(agent-purge use ${first.id} 可写回指针)`);
163
- pickedName = first.id;
164
- picked = first;
165
- }
166
- }
167
- if (!picked) die(`没有 persona: ${pickedName}(可用: ${store.list().map((p) => p.id).join(", ")})`);
168
- const personaPath = store.materialize(picked.id);
169
- const personaText = picked.body.trim();
170
- const cwd = resolve(flag("--cwd", process.cwd()));
171
-
172
- // --ask:一次性问答,与 Web UI 共用 oneshots 规格;不给才启动交互式 harness
173
- // 透传:有 -- 就只收 -- 后面的;没有就把“不是 agent-purge 自己的 flag”全交给 harness
174
- // (这样裸命令 `pi -p "…"` / `codex exec "…"` 经 shim 进来也能把参数带到,--ask 也一样)
175
- const dd = argv.indexOf("--");
176
- let passthrough = [];
177
- if (dd !== -1) {
178
- passthrough = argv.slice(dd + 1);
179
- } else {
180
- const ownWithValue = new Set(["--ask", "--mode", "--persona", "--cwd", "--auto", "--layer"]);
181
- const tail = argv.slice(1);
182
- for (let i = 0; i < tail.length; i++) {
183
- const t = tail[i];
184
- if (ownWithValue.has(t)) {
185
- i++;
186
- continue;
187
- }
188
- if (t === "--dry-run" || ownWithValue.has(t.split("=")[0])) continue;
189
- passthrough.push(t);
190
- }
191
- }
192
-
193
- const askEq = argv.find((a) => a.startsWith("--ask="));
194
- const askIdx = argv.indexOf("--ask");
195
- const askPrompt = askEq !== undefined ? askEq.slice(6) : askIdx !== -1 ? argv[askIdx + 1] : null;
196
- let built;
197
- if (askPrompt !== null) {
198
- const raw = askPrompt;
199
- if (!raw || raw.startsWith("--")) die('--ask 需要一个 prompt,例如: agent-purge codex --ask "解释这段代码"');
200
- const prompt = raw.trim();
201
- const one = oneshots[name];
202
- if (!one) die(`${name} 没有一次性运行模式(可用: ${Object.keys(oneshots).join(", ")})`);
203
- built = one({ prompt, personaPath, personaText, mode, cwd });
204
- if (passthrough.length) built.args = [...built.args, ...passthrough];
205
- } else {
206
- built = backends[name]?.({ personaPath, personaText, mode, args: passthrough, cwd, reason: spec.reason }) ?? {
207
- unsupported: "该 harness 还没有后端实现",
208
- };
209
- }
210
-
211
- if (built.manual) {
212
- console.log(`${name}: 需要人工介入`);
213
- built.manual.forEach((line) => console.log(" - " + line));
214
- process.exit(0);
215
- }
216
- if (built.unsupported) die(`${name} 做不到:${built.unsupported}`);
217
-
218
- if (built.note) console.log(built.note);
219
- if (built.residueHint) console.log(`[residue] ${built.residueHint}`);
220
- const dryRun = argv.includes("--dry-run");
221
- // 启动时把人设塞进扩展(omp / pi):命令行走不通 10K 人设,扩展也没有长度上限。
222
- // omp 交互式启动时把后端的 --system-prompt 摘掉,否则扩展和命令行会各注入一次。
223
- // 但 --profile / --no-extensions 下默认扩展目录不会加载:这两个情况保留命令行注入。
224
- const ompProfile = name === "omp" ? (flag("--profile", null) ?? process.env.OMP_PROFILE ?? null) : null;
225
- const ompNoExtensions = name === "omp" && (argv.includes("--no-extensions") || argv.some((a) => a.startsWith("--no-extensions=")));
226
- const useExtension = name !== "omp" || (askPrompt === null && !ompProfile && !ompNoExtensions);
227
- let extFile = null;
228
- if ((name === "pi" || name === "omp") && !built.unsupported && !built.manual) {
229
- const isOmp = name === "omp";
230
- if (mode === "off") {
231
- // 不装、也不喊注入:扩展就算在盘上,也会因为 mode=off + 没 FORCE 而沉默
232
- built.env = { ...built.env, AGENT_PURGE_MODE: "off" };
233
- } else if (useExtension) {
234
- if (isOmp && built.args[0] === "--system-prompt") built.args.splice(0, 2);
235
- if (!dryRun) {
236
- const { installSlash } = await import("../lib/slashes.mjs");
237
- const r = installSlash(name, DATA, { personaId: picked.id, personaBody: personaText });
238
- if (r.error) die(`${name}: ${r.error}`);
239
- extFile = r.file;
240
- }
241
- built.env = { ...built.env, AGENT_PURGE_PERSONA: picked.id, AGENT_PURGE_MODE: mode, AGENT_PURGE_FORCE: "1" };
242
- } else if (isOmp && askPrompt === null) {
243
- console.log("[ext] --profile/--no-extensions:本次保留命令行注入,/purge 热切不可用");
244
- } else if (isOmp) {
245
- // --ask:命令行已经注入;扩展若在盘上且 ompAuto 开着会再注一遍,这里按 off 静音
246
- built.env = { ...built.env, AGENT_PURGE_MODE: "off" };
247
- }
248
- }
249
- // off 有两种来源:全局总电源关了,或者这一轮手动指定。措辞不能混,否则看日志的人会以为总开关坏了
250
- if (mode === "off") {
251
- const globalOff = (state.default?.mode ?? "") === "off";
252
- console.log(
253
- globalOff
254
- ? "[off] 总电源关着(state.default.mode=off):本轮原样启动,不装扩展、不注入;要注入就先去 UI 把总开关拧开,或 --mode replace/strict/append"
255
- : "[off] 本轮不注入(--mode off):原样启动;全局默认仍是 " + (state.default?.mode ?? "?") + ",想改默认用 agent-purge mode <模式>",
256
- );
257
- }
258
- console.log(`[cmd] ${[built.cmd, ...built.args.map((a) => (a.includes(" ") ? JSON.stringify(a) : a))].join(" ")}`);
259
- if (extFile) console.log(`[ext] ${extFile}(/purge 热切换)`);
260
- if (dryRun) process.exit(0);
261
-
262
- if (!resolveBin(built.cmd)) {
263
- die(`${name}: 找不到可执行文件 ${built.cmd}(PATH 里没有它;agent-purge doctor 可看本机探测结果)`);
264
- }
265
- built.setup?.();
266
- // Ctrl-C 时也要清掉一次性插件:process.exit 会触发 exit 监听器,不依赖 child close
267
- let cleaned = false;
268
- const cleanup = () => {
269
- if (cleaned) return;
270
- cleaned = true;
271
- try {
272
- built.cleanup?.();
273
- } catch {}
274
- };
275
- process.once("exit", cleanup);
276
- // 一次性问答:stdin 直接给 NUL。否则 codex 这类会先“Reading additional input from stdin...”
277
- // 并阻塞到 EOF(非 TTY 环境必现);stdin 关了它就直接跑。
278
- const stdio = askPrompt !== null ? ["ignore", "inherit", "inherit"] : "inherit";
279
- const child = spawnBin(built.cmd, built.args, { cwd, stdio, env: { ...process.env, ...built.env } });
280
- const finish = (code) => {
281
- cleanup();
282
- process.exit(code ?? 1);
283
- };
284
- child.on("error", (e) => {
285
- console.error(`${name}: ${e.message}`);
286
- finish(1);
287
- });
288
- child.on("close", (code) => finish(code));
289
- }
290
-
291
- // ── 选东西的实现 ───────────────────────────────────────────────────────────
292
-
293
- async function useCmd() {
294
- const { createStore } = await import("../lib/personas.mjs");
295
- const store = createStore(DATA);
296
- const arg = argv[1];
297
- const { state, migrated } = loadState(DATA);
298
- migrated.forEach((m) => console.log(`[migrate] ${m}`));
299
-
300
- const id = arg && !arg.startsWith("-") ? arg : state.persona;
301
- if (!id) {
302
- console.log(`当前 persona: (未设置)\n可用: ${store.list().map((p) => p.id).join(", ")}`);
303
- return;
304
- }
305
- if (!store.read(id)) die(`没有 persona: ${id}(可用: ${store.list().map((p) => p.id).join(", ")})`);
306
- store.setActive(id); // 写 active.md 快照 + state.persona 指针
307
-
308
- // user 层的 /purge 命令文件是烘焙进去的,换人设必须重渲染
309
- const { refreshSlashes } = await import("../lib/slashes.mjs");
310
- const refreshed = refreshSlashes(DATA, state.armed ? Object.entries(state.armed).filter(([, a]) => a.includes("slash")).map(([k]) => k) : [], {
311
- personaId: id,
312
- personaBody: store.read(id).body.trim(),
313
- });
314
- console.log(`persona ← ${id}(active.md 已同步)`);
315
- refreshed.forEach((f) => console.log(`[slash] 重新写入 ${f}`));
316
- if (Object.keys(state.armed ?? {}).length) {
317
- console.log("已武装的 harness 下一轮/下次启动自动跟随;落盘型(install)若想立刻同步:agent-purge install <h>");
318
- }
319
- }
320
-
321
- function modeCmd() {
322
- const want = argv[1];
323
- const { state } = loadState(DATA);
324
- const current = state.default?.mode ?? "replace";
325
- if (!want) {
326
- console.log(
327
- [
328
- `当前注入方式: ${current}`,
329
- " strict 只发人设(宿主提示词全丢,最干净)",
330
- " replace 人设在前 + 宿主提示词剔除身份句(默认,官方仍认得你)",
331
- " append 人设只挂在最前,宿主原样保留(最保守)",
332
- " off 不注入(受管 harness 也能干净地裸用)",
333
- ].join("\n"),
334
- );
335
- return;
336
- }
337
- if (!MODES.includes(want)) die(`mode 只能是 ${MODES.join(" / ")}`);
338
- setDefault(DATA, { harness: state.default?.harness ?? "codex", mode: want });
339
- console.log(`mode ← ${want}(state.json;下次启动生效,--mode 可临时覆盖)`);
340
- }
341
-
342
- async function installCmd(verb, rest) {
343
- const id = rest[0];
344
- const ids = id === "--all" || !id ? Object.keys(matrix.harnesses).filter((h) => matrix.harnesses[h].class !== "unsupported") : [id];
345
- // 常驻只走 lib/persist.mjs 这一个控制面(caps / Web 开关 / status 都读它的 targets)
346
- const { install, uninstall, targets } = await import("../lib/persist.mjs");
347
- const { createStore } = await import("../lib/personas.mjs");
348
- const store = createStore(DATA);
349
- for (const h of ids) {
350
- if (!matrix.harnesses[h]) die(`未知 harness: ${h}`);
351
- if (!targets[h]) {
352
- console.log(`${h}: 没有常驻模式(用 arm 走裸命令接管 / slash 走会话内)`);
353
- continue;
354
- }
355
- let r;
356
- if (verb === "install") {
357
- const pid = store.activeId();
358
- if (!pid) die("还没有 persona:先 agent-purge use <id>");
359
- r = install(h, { root: DATA, active: store.refreshActive() }) ?? {};
360
- } else {
361
- // 卸载不能依赖 persona 还在:人设删了也必须能关
362
- r = uninstall(h) ?? {};
363
- }
364
- const ok = !r.error;
365
- console.log(`${ok ? "✓" : "✗"} ${verb} ${h}${r.file ? ` → ${r.file}` : ""}${r.note ? ` (${r.note})` : ""}${r.error ? ` ${r.error}` : ""}`);
366
- if (ok) setArmed(DATA, h, "install", verb === "install");
367
- }
368
- }
369
-
370
- async function armCmd(verb, rest) {
371
- const { installShim, uninstallShim, ensureUserPath } = await import("../lib/shim.mjs");
372
- const { installSlash, uninstallSlash } = await import("../lib/slashes.mjs");
373
- const { install, uninstall } = await import("../lib/persist.mjs");
374
- const { capsFor } = await import("../lib/caps.mjs");
375
- const { createStore } = await import("../lib/personas.mjs");
376
- const store = createStore(DATA);
377
- const pid = store.activeId();
378
- if (!pid) die("还没有 persona:先 agent-purge use <id>");
379
-
380
- const supported = Object.keys(matrix.harnesses).filter((h) => matrix.harnesses[h].class !== "unsupported");
381
- const namedTarget = rest.filter((x) => !x.startsWith("-"));
382
- // 点了名的只动那家:`--all` 只管总电源,不再悄悄把别的 harness 一起撤(issue #5)
383
- const targets = namedTarget.length ? namedTarget : supported;
384
-
385
- // 全局 --all 就是总电源:撤的时候必须把 default.mode 置 off,
386
- // 否则 `agent-purge <h>` 这条入口照样注入(实测:关了总开关还能接管,就是这里漏的)。
387
- // 开的时候把上次的 mode 还回去。单家操作不动 mode。
388
- // 没设过默认 harness 时也要能关:launch() 在 default 为空时会回落成 replace。
389
- if (rest.includes("--all")) {
390
- const { state: cur } = loadState(DATA);
391
- const m = cur.default?.mode ?? null; // null = 从没设过,launch() 会回落成 replace
392
- const h = cur.default?.harness ?? null;
393
- if (verb === "disarm" && m !== "off") {
394
- patchState(DATA, { default: { harness: h, mode: "off" }, prevMode: m ?? "replace" });
395
- console.log(`总电源:mode ${m ?? "(未设,实际按 replace)"} → off(裸敲 agent-purge / agent-purge <h> 都不再注入;arm --all 还回去)`);
396
- }
397
- if (verb === "arm" && cur.default && m === "off") {
398
- const back = MODES.includes(cur.prevMode) && cur.prevMode !== "off" ? cur.prevMode : "replace";
399
- patchState(DATA, { default: { harness: h, mode: back } });
400
- console.log(`总电源:mode off → ${back}`);
401
- }
402
- }
403
-
404
- // 通道默认全开(常驻 + 裸命令接管 + /purge);只想做其中一个就显式点名
405
- const named = ["--install", "--shim", "--slash"].filter((f) => rest.includes(f));
406
- const channels = named.length ? named.map((f) => f.slice(2)) : ["install", "shim", "slash"];
407
- let armedAny = 0;
408
-
409
- for (const h of targets) {
410
- if (!matrix.harnesses[h]) die(`未知 harness: ${h}`);
411
- const done = [];
412
- const skip = [];
413
-
414
- for (const ch of channels) {
415
- // 先过能力表:这家形态上做不到的通道(如 zcode 桌面 + shim),不写文件,直接说明原因
416
- if (verb === "arm") {
417
- const cap = capsFor(h)[ch];
418
- if (cap && !cap.ok) {
419
- skip.push(`${ch}: ${cap.why}`);
420
- continue;
421
- }
422
- }
423
- if (ch === "install") {
424
- const r = verb === "arm" ? install(h, { root: DATA, active: store.refreshActive() }) : uninstall(h);
425
- if (r.error) skip.push(`常驻: ${r.error}`);
426
- else {
427
- setArmed(DATA, h, "install", verb === "arm");
428
- done.push(verb === "arm" ? `常驻 ${r.action}` : `撤常驻 ${r.action}`);
429
- }
430
- }
431
- if (ch === "shim") {
432
- const r = verb === "arm" ? installShim(DATA, h) : uninstallShim(DATA, h);
433
- if (r.error) skip.push(`shim: ${r.error}`);
434
- else {
435
- setArmed(DATA, h, "shim", verb === "arm");
436
- done.push(verb === "arm" ? "裸命令接管" : "撤裸命令接管");
437
- }
438
- }
439
- if (ch === "slash") {
440
- const r =
441
- verb === "arm"
442
- ? installSlash(h, DATA, { personaId: pid, personaBody: store.read(pid).body.trim() })
443
- : uninstallSlash(h, DATA);
444
- if (r.error) skip.push(`/purge: ${r.error}`);
445
- else {
446
- setArmed(DATA, h, "slash", verb === "arm");
447
- done.push(verb === "arm" ? `/purge ${r.layer} 层` : "撤 /purge");
448
- }
449
- }
450
- }
451
- const icon = done.length ? "✓" : "·";
452
- if (done.length) armedAny += 1;
453
- console.log(`${icon} ${verb} ${h.padEnd(11)} ${done.join(" · ") || "无可用通道"}${skip.length ? " 跳过: " + skip.join(";") : ""}`);
454
- }
455
-
456
- if (verb === "arm" && armedAny && channels.includes("shim")) {
457
- const r = ensureUserPath();
458
- console.log(r.changed ? `[path] ${r.dir} → 用户 PATH 最前(${r.how})` : `[path] ${r.dir} 就位(${r.how})`);
459
- }
460
- if (verb === "arm") {
461
- if (!armedAny) {
462
- console.log(`\n一个通道都没装上(${targets.join(", ")}):上面“跳过”里写了每家为什么不行。`);
463
- } else {
464
- const h0 = targets[0];
465
- const lines = [];
466
- if (channels.includes("shim")) lines.push(`新开终端敲 ${h0} → 走 PATH 前置壳,注入生效`);
467
- if (channels.includes("slash")) {
468
- lines.push(
469
- h0 === "pi"
470
- ? "裸敲 pi 不注入(只装了热切);会话里打 /purge on 才改写,/purge status 看状态"
471
- : h0 === "dsh"
472
- ? "dsh 会话里打 /purge 切开关(改 state.json,不产生模型消息);三形态共用一份状态"
473
- : "会话里打 /purge 才注入(它展开成用户消息)",
474
- );
475
- }
476
- if (channels.includes("install")) lines.push("改了配置文件:连别人启动的、GUI 里点开的也生效(那家重启后生效)");
477
- console.log("\n下一步:\n" + lines.map((l) => " - " + l).join("\n") + "\n复核:agent-purge status");
478
- }
479
- }
480
- }
481
-
482
- async function slashCmd(rest) {
483
- const { installSlash, uninstallSlash, slashStatus } = await import("../lib/slashes.mjs");
484
- const { createStore } = await import("../lib/personas.mjs");
485
- const store = createStore(DATA);
486
- const id = rest[0];
487
- if (!id || id === "status") {
488
- const rows = slashStatus(DATA);
489
- const live = rows.filter((s) => s.offered);
490
- live.forEach((s) => {
491
- console.log(`${s.installed ? "✓" : " "} ${s.id.padEnd(9)} ${s.layer.padEnd(4)} ${s.trigger.padEnd(22)} persona=${s.personaId ?? "-"}`);
492
- console.log(` ${s.note}`);
493
- });
494
- // 只列“能把内容写进 system 数组”的;user 层不要再诱人点
495
- const dead = rows.filter((s) => !s.offered);
496
- if (dead.length) {
497
- const still = dead.filter((s) => s.installed).map((s) => s.id);
498
- console.log(`\n已下线(user 层,压不住系统提示词):${dead.map((s) => s.id).join(", ")}`);
499
- if (still.length) console.log(`旧文件还在盘上:agent-purge slash off ${still.join(" ")}`);
500
- }
501
- return;
502
- }
503
- if (id === "off") {
504
- const target = rest[1];
505
- const ids =
506
- target && target !== "--all"
507
- ? rest.slice(1).filter((x) => !x.startsWith("-"))
508
- : slashStatus(DATA).filter((s) => s.installed).map((s) => s.id);
509
- if (!ids.length) console.log("没有装着的 /purge");
510
- ids.forEach((h) => {
511
- const r = uninstallSlash(h, DATA);
512
- if (!r.error) {
513
- setArmed(DATA, h, "slash", false);
514
- console.log(`✓ 已移除 /purge ${h}${r.removed?.length ? " → " + r.removed.join(", ") : "(本来就没有)"}`);
515
- }
516
- });
517
- return;
518
- }
519
- const targets = id === "--all" ? slashStatus(DATA).filter((s) => s.offered).map((s) => s.id) : [id];
520
- const pid = store.activeId();
521
- if (!pid) die("还没有 persona:先 agent-purge use <id>");
522
- for (const h of targets) {
523
- const r = installSlash(h, DATA, { personaId: pid, personaBody: store.read(pid).body.trim() });
524
- if (r.error) {
525
- console.log(`- ${h}: ${r.error}`);
526
- continue;
527
- }
528
- setArmed(DATA, h, "slash", true);
529
- console.log(`✓ /purge → ${h}(${r.layer} 层)${r.file}`);
530
- console.log(` ${r.note}`);
531
- }
532
- }
533
-
534
- async function statusCmd() {
535
- const { migrated } = loadState(DATA);
536
- migrated.forEach((m) => console.log(`[migrate] ${m}`));
537
- // armed 是缓存,磁盘才是真相:先把假记录摘掉,再报数
538
- const { reconcileArmed } = await import("../lib/armed.mjs");
539
- const fixed = await reconcileArmed(DATA);
540
- if (fixed.length) console.log(`[对账] armed 纠正:${fixed.join(", ")}`);
541
- const { state } = loadState(DATA);
542
- return Promise.all([import("../lib/shim.mjs"), import("../lib/slashes.mjs")]).then(([shimMod, slashMod]) => {
543
- const shims = shimMod.shimStatus();
544
- const slashes = slashMod.slashStatus(DATA);
545
-
546
- console.log(`persona ${state.persona ?? "(未设置)"}`);
547
- console.log(`default ${state.default ? `${state.default.harness ?? "(未选默认)"} · ${state.default.mode}` : "(未设置)"} 裸敲 agent-purge 用它`);
548
- console.log(`state ${join(DATA, "state.json")}`);
549
- console.log("");
550
- console.log("harness shim slash install /purge层 常驻落点");
551
- for (const [h] of Object.entries(matrix.harnesses)) {
552
- const armed = armedOf(state, h);
553
- const sh = shims.find((s) => s.id === h);
554
- const sl = slashes.find((s) => s.id === h);
555
- const cap = capsFor(h, { bin: sh?.target ?? null });
556
- const mark = (on) => (on ? " ✓ " : " · ");
557
- const slashCol = sl && sl.offered ? (sl.layer === "sys" ? "sys(热)" : "user") : "—";
558
- // install 列不写“注册没注册”,写“能不能装”:装了是 ✓,能装没装是「可用」,没落点才是 —
559
- const installCol = armed.includes("install") ? " ✓ " : cap.install.ok ? "可用" : " — ";
560
- const landing = cap.install.ok ? `${cap.install.layer || "?"}${cap.install.verified === false ? "(未实测)" : ""}` : (cap.install.why || "");
561
- console.log(
562
- `${h.padEnd(11)}${mark(sh?.installed).padEnd(7)}${mark(sl?.installed).padEnd(7)}${installCol.padEnd(10)}${slashCol.padEnd(10)}${landing}`,
563
- );
564
- }
565
- const dir = shimMod.shimDir();
566
- const ps = shimMod.pathState();
567
- const where = !ps.present ? "不在 PATH" : ps.first ? "已在 PATH 最前" : "在 PATH 但不最前(真身可能先被命中)";
568
- console.log(`\nshim 目录 ${dir}(${where})`);
569
- if (!ps.first) console.log(`修好它:agent-purge arm <h> --shim(会自动修正 PATH)`);
570
- });
571
- }
572
-
573
- async function lsCmd() {
574
- const { createStore } = await import("../lib/personas.mjs");
575
- const store = createStore(DATA);
576
- const { state } = loadState(DATA);
577
- const fit = store.resolve({ harness: state.default?.harness, model: null });
578
- for (const p of store.list()) {
579
- const mark = p.id === store.activeId() ? "*" : " ";
580
- const best = fit?.persona?.id === p.id && fit?.matched ? " ← 最匹配当前 harness" : "";
581
- const desc = p.meta?.description ?? p.meta?.desc ?? "";
582
- console.log(`${mark} ${p.id.padEnd(14)} ${String(p.meta?.name ?? "").padEnd(12)} ${desc.slice(0, 44)}${best}`);
583
- }
584
- console.log(`\n* = active(agent-purge use <id> 切换)`);
585
- }
586
-
587
- function webCmd() {
588
- const port = Number(flag("--port", "7788"));
589
- // --detach:脱离当前终端后台存活,日志丢弃
590
- if (argv.includes("--detach")) {
591
- const child = spawn(process.execPath, [join(ROOT, "web", "server.mjs"), "--serve", String(port)], {
592
- detached: true,
593
- stdio: "ignore",
594
- shell: false,
595
- env: { ...process.env, AGENT_PURGE_HOME: DATA },
596
- });
597
- child.unref();
598
- console.log(`agent-purge web 已在后台启动 → http://127.0.0.1:${port}(pid ${child.pid})`);
599
- process.exit(0);
600
- }
601
- return import("../web/server.mjs").then(({ serveWeb }) => serveWeb(port));
602
- }
603
-
604
- // ── 其余 ──────────────────────────────────────────────────────────────────
605
-
606
- async function armorCli(rest) {
607
- const { renderMatrix, renderPlan, plans, plan, preflight, runBench, runJudge } = await import("../lib/armor.mjs");
608
- const color = !argv.includes("--no-color") && Boolean(process.stdout.isTTY);
609
- const sub = rest[0]?.startsWith("-") ? "matrix" : rest[0] ?? "matrix";
610
- const opts = (name, fallback) => {
611
- const i = rest.indexOf(name);
612
- return i === -1 ? fallback : rest[i + 1];
613
- };
614
- const flags = rest.filter((x) => x.startsWith("--"));
615
- // 位置参数 ≠ 非 - 开头的 token:`--base https://x` 的值不是 harness 名
616
- const VALUE_FLAGS = new Set(["--base", "--key", "--model", "--task", "--layers", "--only", "--repeat", "--concurrency", "--timeout", "--out", "--dir", "--probe"]);
617
- const positionals = (from = 1) => {
618
- const out = [];
619
- for (let i = from; i < rest.length; i++) {
620
- const a = rest[i];
621
- if (a.startsWith("--")) {
622
- if (VALUE_FLAGS.has(a)) i++;
623
- continue;
624
- }
625
- if (!a.startsWith("-")) out.push(a);
626
- }
627
- return out;
628
- };
629
-
630
- if (sub === "matrix") return renderMatrix(matrix, { color });
631
-
632
- if (sub === "plan") {
633
- const ids = positionals();
634
- const list = ids.length ? ids.map((id) => plan(matrix, id)) : plans(matrix);
635
- list.forEach((p, i) => {
636
- if (i) console.log();
637
- renderPlan(p, { color });
638
- });
639
- if (!ids.length) console.log(`\n${"─".repeat(48)}\n批量跑:agent-purge armor run --task crack-bare --layers off,sys,user --only ${list.map((p) => p.id).join(",")}`);
640
- return;
641
- }
642
-
643
- if (sub === "preflight" || sub === "check") {
644
- const ids = positionals();
645
- const env = { base: opts("--base", process.env.MG_BASE), key: opts("--key", process.env.MG_KEY), model: opts("--model", process.env.MG_MODEL) };
646
- let bad = 0;
647
- for (const id of ids.length ? ids : ["pi"]) {
648
- const r = preflight(matrix, id, env);
649
- console.log(`${r.ok ? "✓" : "✗"} ${id}`);
650
- r.checks.forEach((c) => console.log(` ${c.ok ? "ok " : "MISS"} ${c.name}${c.detail ? ` — ${c.detail}` : ""}`));
651
- if (!r.ok) bad++;
652
- }
653
- if (bad) process.exitCode = 1;
654
- return;
655
- }
656
-
657
- if (sub === "run") {
658
- if (flags.includes("--help")) {
659
- console.log(`agent-purge armor run --task crack-bare --layers off,sys,user --only pi,codex --repeat 2 --concurrency 3
660
- --base <url> --key <key> --model <id> 上游(默认读 MG_BASE / MG_KEY / MG_MODEL)
661
- --task crack-bare|crack-persona 问法(裸问法 / 带人设词种子)
662
- --layers off,sys,user|off,sys
663
- --out <dir>`);
664
- return;
665
- }
666
- const env = {
667
- MG_BASE: opts("--base", process.env.MG_BASE),
668
- MG_KEY: opts("--key", process.env.MG_KEY),
669
- MG_MODEL: opts("--model", process.env.MG_MODEL),
670
- };
671
- if (!env.MG_BASE || !env.MG_KEY) {
672
- console.error("缺少上游:--base / --key(或环境变量 MG_BASE / MG_KEY)。例:\n agent-purge armor run --base https://api.deepseek.com/v1 --key $env:MG_KEY --model deepseek-flash --task crack-bare --layers off,sys --only pi");
673
- process.exit(2);
674
- }
675
- const pass = [];
676
- for (let i = 0; i < rest.length; i++) {
677
- const a = rest[i];
678
- if (["--base", "--key", "--model"].includes(a)) { i++; continue; }
679
- pass.push(a);
680
- }
681
- const { code } = runBench(pass, env);
682
- process.exitCode = code;
683
- return;
684
- }
685
-
686
- if (sub === "judge") {
687
- const pass = [];
688
- for (let i = 0; i < rest.length; i++) {
689
- const a = rest[i];
690
- if (["--base", "--key", "--model"].includes(a)) { i++; continue; }
691
- pass.push(a);
692
- }
693
- const { code } = runJudge(pass, {
694
- MG_BASE: opts("--base", process.env.MG_BASE),
695
- MG_KEY: opts("--key", process.env.MG_KEY),
696
- MG_MODEL: opts("--model", process.env.MG_MODEL),
697
- });
698
- process.exitCode = code;
699
- return;
700
- }
701
-
702
- console.error(`未知子命令: armor ${sub}(可用: matrix | plan | preflight | run | judge)`);
703
- process.exit(2);
704
- }
705
-
706
- function printMatrix() {
707
- const rows = Object.entries(matrix.harnesses).map(([name, s]) => ({
708
- harness: name,
709
- class: s.class,
710
- override: s.override === true ? "yes" : s.override ? String(s.override) : "no",
711
- inject: s.inject ? "yes" : "no",
712
- hot: s.hotSwitch ? "yes" : "no",
713
- chars: s.defaultSystemChars != null ? `${s.defaultSystemChars} → ${s.purgedSystemChars ?? "?"}` : "—",
714
- armor: s.armor ? `${s.armor.probe.bare.sys ?? "—"}${s.armor.probe.bare.sys == null ? "" : "%"} / ${s.armor.probe.bare.user ?? "—"}${s.armor.probe.bare.user == null ? "" : "%"}` : "未测",
715
- version: s.measuredVersion ?? "—",
716
- }));
717
- const head = ["harness", "class", "override", "inject", "hot", "system chars", "sys/user 注入", "version"];
718
- const width = head.map((h, i) => Math.max(h.length, ...rows.map((r) => String(Object.values(r)[i]).length)));
719
- const line = (cells) => cells.map((c, i) => String(c).padEnd(width[i])).join(" ");
720
- console.log(line(head));
721
- console.log(width.map((w) => "-".repeat(w)).join(" "));
722
- rows.forEach((r) => console.log(line(Object.values(r))));
723
- const residues = Object.entries(matrix.harnesses).filter(([, s]) => s.residue);
724
- if (residues.length) {
725
- console.log("\n残留:");
726
- residues.forEach(([n, s]) => console.log(` ${n}: ${s.residue}`));
727
- }
728
- }
729
-
730
- function help() {
731
- console.log(`agent-purge —— 把任意 harness 的系统提示词换成你的 persona
732
-
733
- 一句话:先「选」,再「用」。选一次,之后所有入口都跟着变。
734
-
735
- ① 选(写 state.json,全工具共用,改完立刻生效)
736
- agent-purge use <id> 换 persona(ls 看有哪些,* = 当前)
737
- agent-purge mode <m> strict(只发人设) / replace(默认) / append / off
738
- agent-purge web 浏览器里点选 harness · persona · mode
739
-
740
- ② 用(三种入口,效果一样)
741
- agent-purge 裸敲 = 跑 default 里那家(web 选过就用选过的)
742
- agent-purge <h> [--ask "…"] 指定一家启动;--dry-run 只看命令,-- 后面透传
743
- <h> web / arm 之后直接敲命令名,就已经注入
744
-
745
- ③ 装(让「别的入口」也带人设;一条命令全装,想只做一项就点名)
746
- agent-purge arm <h>|--all 常驻 + 裸命令接管 + /purge 全开(= 注册)
747
- agent-purge 注册 [--all] 中文别名,同上
748
- agent-purge arm <h> --shim 只做裸命令接管(写 PATH 最前)
749
- agent-purge arm <h> --install 只做配置文件常驻
750
- agent-purge arm <h> --slash 只做会话内 /purge
751
- agent-purge disarm <h>|--all 以上全部撤掉;不带 <h> 就是所有 harness
752
-
753
- 注:--all 是总电源(state.json 的 default.mode)。带 <h> 时只撤那家,
754
- 但同样把总电源关掉——不然裸敲 agent-purge、agent-purge <h> 照样注入。
755
- 彻底别管我:agent-purge disarm --all
756
-
757
- ④ 看
758
- agent-purge status 一张表:每家的 shim / slash / install / 能做什么
759
- agent-purge ls persona 列表
760
- agent-purge matrix 能力矩阵(matrix.json)
761
- agent-purge armor [...] plan|preflight|run|judge 实测热力图
762
- agent-purge doctor 本机探测(哪家装了、版本)
763
-
764
- 通道语义
765
- shim 在 PATH 最前放同名 cmd,裸敲即经 agent-purge 启动(sys 层注入)
766
- install 改 harness 自己的配置/插件文件(连别人启动的它也会带人设)
767
- slash 会话里打 /purge 换提示词:**pi 和 dsh 都能做**(pi 的扩展钩子 / dsh 的 cordis 插件,真 sys 层、热切换)
768
- 其他家没有这个能力:/purge 展开成用户消息,bench 实测 0/2 无效,所以不提供
769
- 注入强度:只有 sys 层算。user 层(上下文块/用户消息)在 codex/claude/opencode/grok/hermes 实测都是 0/2,一律不提供。
770
-
771
- harness: pi, opencode, opencode2, dsh, codex, claude, grok, omp, hermes, zcode, antigravity, qoder, workbuddy, codebuddy
772
- opencode2 = @opencode/cli 2.x(v2 插件 API),dsh = DeepSeek Harness(cordis 插件,desktop/web/headless 三形态)。
773
- qoder / workbuddy 无 prompt 组装入口(提示词在服务端),只能改上下文规则文件。
774
- `);
775
- }
776
-
777
- function die(message) {
778
- console.error(message);
779
- process.exit(2);
780
- }
1
+ #!/usr/bin/env node
2
+ // agent-purge — 一个入口,把各家 harness 的系统提示词替换收进来。
3
+ // 能力与实测数字见 matrix.json。
4
+ //
5
+ // 命令只有两类,别再分第三类:
6
+ // 选东西(写 state.json) use / mode / install / arm / slash
7
+ // 用东西(跑起来) <harness> / 裸敲已 arm 的命令名 / web / ls / status
8
+ import { spawn } from "node:child_process";
9
+ import { join, resolve } from "node:path";
10
+ import { backends, oneshots } from "../backends/index.mjs";
11
+ import { resolveBin, launchConfig, prepareCommand, spawnPrepared } from "../lib/spawn.mjs";
12
+ import { launchEnvironment } from "../lib/environment.mjs";
13
+ import { createLaunch } from "../lib/launch.mjs";
14
+ import { installCrashHandlers, recover, swapFile } from "../lib/restore.mjs";
15
+ import { capsFor } from "../lib/caps.mjs";
16
+ import { MODES, armedOf, loadState, resolveFor } from "../lib/state.mjs";
17
+ import { dataHome, ensureDataHome } from "../lib/home.mjs";
18
+ import { applyConfiguration, changeChannel, configureHarness, disableHarness } from "../lib/apply.mjs";
19
+ import { ROOT, isCompiled, readResource } from "../lib/root.mjs";
20
+ // 静态导入:bun 单文件产物里,被动态 import 过的模块会被拆成独立 chunk,命名空间就空了
21
+ import { serveWeb } from "../web/server.mjs";
22
+
23
+ // 数据(state.json + personas/)放用户目录,npm 升级不会连人设一起清掉
24
+ // 编译态没有「包目录」可搬旧数据,传 null 跳过 legacy 迁移
25
+ const DATA = dataHome();
26
+ const matrix = (() => {
27
+ try { return JSON.parse(readResource("matrix.json").toString("utf8")); }
28
+ catch (error) { throw new Error(`无法加载 matrix.json:${error.message}`, { cause: error }); }
29
+ })();
30
+ // 这些家只有“整段替换/追加”两个启动口,没有宿主提示词可读:replace 实际按 strict 执行。
31
+ const STRICT_REPLACE = new Set(["codex", "claude", "grok", "codebuddy", "hermes"]);
32
+
33
+ const argv = process.argv.slice(2);
34
+ const separator = argv.indexOf("--");
35
+ const controlArgs = separator < 0 ? argv : argv.slice(0, separator);
36
+ if (controlArgs.includes("--help") || controlArgs.includes("-h")) { help(); process.exit(0); }
37
+ if (argv[0] === "self-uninstall" && !argv.includes("--yes") && !argv.includes("--dry-run")) {
38
+ console.error("self-uninstall 会永久删除用户人设、设置和恢复资料。请先备份;仅撤销接管并保留数据请用 agent-purge clean。\n先预览:agent-purge self-uninstall --dry-run\n确认删除:agent-purge self-uninstall --yes");
39
+ process.exit(2);
40
+ }
41
+ ensureDataHome(isCompiled() ? null : ROOT);
42
+
43
+ installCrashHandlers();
44
+ const recovered = recover(); // 只在真实命令入口恢复;--help 不读写 journal
45
+ if (recovered > 0) console.log(`[restore] 回滚了 ${recovered} 个上次残留的配置文件`);
46
+
47
+ // 编译态与脚本态使用同一个 Web 入口;0 表示系统分配空闲端口,供隔离 smoke 使用。
48
+ if (argv[0] === "--serve") {
49
+ const port = argv[1] === undefined ? 7788 : Number(argv[1]);
50
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error("端口必须为 0..65535 的整数");
51
+ serveWeb(port);
52
+ await new Promise(() => {});
53
+ }
54
+ const flag = (name, fallback) => {
55
+ // 支持 --mode=append 这种写法(以前只认空格分隔,等号形式会被静默忽略)
56
+ const inline = controlArgs.find((a) => a.startsWith(name + "="));
57
+ if (inline !== undefined) return inline.slice(name.length + 1);
58
+ const i = controlArgs.indexOf(name);
59
+ return i === -1 ? fallback : controlArgs[i + 1];
60
+ };
61
+ let cmd = argv[0];
62
+
63
+ // 便携版 exe 双击无参数启动时,默认直接打开原生桌面窗口应用
64
+ if (!cmd && isCompiled()) {
65
+ await webCmd();
66
+ // 挂住事件循环
67
+ await new Promise(() => {});
68
+ }
69
+
70
+ if (!cmd || cmd.startsWith("-")) cmd = await defaultHarness();
71
+
72
+ // ── 选东西 ────────────────────────────────────────────────────────────────
73
+ switch (cmd) {
74
+ case "use":
75
+ await useCmd();
76
+ break;
77
+ case "sync":
78
+ await syncCmd();
79
+ break;
80
+ case "mode":
81
+ modeCmd();
82
+ break;
83
+ case "pause":
84
+ case "resume":
85
+ printApply(applyConfiguration(DATA, { enabled: cmd === "resume" }));
86
+ break;
87
+ case "install":
88
+ case "uninstall":
89
+ await installCmd(cmd, argv.slice(1));
90
+ break;
91
+ case "arm":
92
+ case "disarm":
93
+ case "register":
94
+ case "注册":
95
+ await armCmd(cmd === "arm" || cmd === "register" || cmd === "注册" ? "arm" : "disarm", argv.slice(1));
96
+ break;
97
+ case "slash":
98
+ await slashCmd(argv.slice(1));
99
+ break;
100
+ case "project":
101
+ await projectCmd(argv.slice(1));
102
+ break;
103
+ case "status":
104
+ await statusCmd();
105
+ break;
106
+ case "ls":
107
+ await lsCmd();
108
+ break;
109
+ case "pull":
110
+ await personaImportCmd(argv.slice(1));
111
+ break;
112
+ case "persona":
113
+ await personaImportCmd(argv.slice(1));
114
+ break;
115
+ case "doctor":
116
+ case "detect": {
117
+ const { doctor } = await import("../lib/doctor.mjs");
118
+ await doctor(argv);
119
+ break;
120
+ }
121
+ case "matrix":
122
+ printMatrix();
123
+ break;
124
+ case "armor":
125
+ await armorCli(argv.slice(1));
126
+ break;
127
+ case "update":
128
+ case "upgrade": {
129
+ const { checkForUpdate, downloadFile, replaceSelfExecutable, canReplaceSelf, RELEASES_PAGE } = await import("../lib/update.mjs");
130
+ const doDownload = argv.includes("--download") || argv.includes("-d") || argv.includes("--yes") || argv.includes("-y");
131
+ console.log("正在检查 GitHub Release 最新版本...");
132
+ const info = await checkForUpdate({ force: true });
133
+ if (!info.ok) {
134
+ console.log(`✗ 检查更新失败:${info.error}`);
135
+ console.log(` 可手动访问发布页查看:${RELEASES_PAGE}`);
136
+ process.exitCode = 1;
137
+ break;
138
+ }
139
+ console.log(`当前运行版本:v${info.current}`);
140
+ if (info.published === false) {
141
+ console.log("尚无公开 Release;这不表示 npm 已是最新版。npm 安装可运行 npm outdated -g agent-purge。");
142
+ break;
143
+ }
144
+ console.log(`最新发布版本:v${info.latest} (${info.tagName || info.latest})`);
145
+ if (!info.hasUpdate) {
146
+ console.log("\n✓ 当前已是最新版本!");
147
+ break;
148
+ }
149
+ console.log(`\n⚡ 发现新版本:v${info.latest}!`);
150
+ if (info.body) {
151
+ console.log(`\n【更新说明】\n${info.body.trim().slice(0, 500)}${info.body.length > 500 ? "..." : ""}`);
152
+ }
153
+
154
+ const isExe = canReplaceSelf();
155
+ const targetExeAsset = info.assets?.find((a) => a.name.toLowerCase() === "agent-purge.exe");
156
+ const targetSetupAsset = info.assets?.find((a) => a.name.toLowerCase().endsWith("-setup.exe"));
157
+
158
+ if (isExe && targetExeAsset) {
159
+ console.log(`\n检测到当前为独立 exe 运行环境,可直接原地升级。`);
160
+ if (doDownload) {
161
+ const { tmpdir } = await import("node:os");
162
+ const tempTarget = join(tmpdir(), `agent-purge-new-${Date.now()}.exe`);
163
+ console.log(`正在下载 ${targetExeAsset.name} (${(targetExeAsset.size / 1024 / 1024).toFixed(1)} MB)...`);
164
+ await downloadFile(targetExeAsset.downloadUrl, tempTarget, (rec, tot) => {
165
+ if (tot > 0) process.stdout.write(`\r下载进度: ${((rec / tot) * 100).toFixed(1)}%`);
166
+ }, { expectedSize: targetExeAsset.size, digest: targetExeAsset.digest });
167
+ console.log("\n下载完成,正在安全替换当前可执行文件...");
168
+ try { replaceSelfExecutable(tempTarget); } finally { const { rmSync } = await import("node:fs"); rmSync(tempTarget, { force: true }); }
169
+ console.log(`✓ 升级成功!当前 exe 已就绪为 v${info.latest}。重启 agent-purge 即可生效。`);
170
+ break;
171
+ }
172
+ console.log(` · 自动下载并就地升级当前 exe:\n agent-purge update --download`);
173
+ } else if (process.platform === "win32" && targetSetupAsset && doDownload) {
174
+ const { tmpdir } = await import("node:os");
175
+ const tempSetup = join(tmpdir(), targetSetupAsset.name);
176
+ console.log(`正在下载安装包 ${targetSetupAsset.name}...`);
177
+ await downloadFile(targetSetupAsset.downloadUrl, tempSetup, undefined, { expectedSize: targetSetupAsset.size, digest: targetSetupAsset.digest });
178
+ console.log(`\n下载完成,正在启动安装程序:${tempSetup}`);
179
+ const { spawn } = await import("node:child_process");
180
+ spawn(tempSetup, [], { detached: true, stdio: "ignore" }).unref();
181
+ process.exit(0);
182
+ }
183
+
184
+ console.log("\n【下载与升级方式】");
185
+ console.log(` 1. 访问 GitHub Release 页面直接下载:\n ${info.htmlUrl}`);
186
+ if (info.assets?.length) {
187
+ console.log(" 2. Release 包含的安装/便携产物:");
188
+ for (const a of info.assets) {
189
+ const sizeMb = (a.size / 1024 / 1024).toFixed(1);
190
+ console.log(` · ${a.name} (${sizeMb} MB) → ${a.downloadUrl}`);
191
+ }
192
+ }
193
+ console.log(" 3. 若通过 npm 全局安装,可直接执行:\n npm install -g agent-purge@latest");
194
+ break;
195
+ }
196
+ case "web":
197
+ await webCmd();
198
+ break;
199
+ case "clean":
200
+ case "self-uninstall": {
201
+ const { clean } = await import("../lib/clean.mjs");
202
+ const dryRun = argv.includes("--dry-run");
203
+ if (dryRun) console.log("dry-run:只报会做什么,一条也不写\n");
204
+ const r = clean(DATA, { all: cmd === "self-uninstall", dryRun });
205
+ for (const s of r.steps) {
206
+ const detail = typeof s.detail === "string" ? s.detail : JSON.stringify(s.detail);
207
+ console.log(`${s.ok ? "✓" : "✗"} ${s.what}${s.detail === undefined ? "" : ` — ${detail}`}`);
208
+ }
209
+ if (r.leftovers.length) console.log(`\n还剩:\n${r.leftovers.map((l) => ` · ${l}`).join("\n")}`);
210
+ console.log(r.ok && !r.leftovers.length ? "\n干净了:没有残留" : "\n有残留,见上");
211
+ if (!r.ok) process.exitCode = 1;
212
+ if (cmd === "self-uninstall") console.log("\n代码本身用包管理器卸:npm uninstall -g agent-purge");
213
+ break;
214
+ }
215
+ case "verify": {
216
+ const { verifyHarnesses, cleanupRoots } = await import("../lib/capture.mjs");
217
+ const wanted = argv.slice(1).filter((a) => !a.startsWith("-"));
218
+ const hs = wanted.length ? wanted : Object.keys(matrix.harnesses ?? {});
219
+ const mode = loadState(DATA).state?.default?.mode ?? "replace";
220
+ const report = await verifyHarnesses(hs, { root: DATA, mode });
221
+ for (const r of report.harnesses) {
222
+ if (r.status === "captured") {
223
+ const m = r.modes[0];
224
+ const where = ["sys", "user"].filter((k) => m?.injected?.[k]).join("+") || "-";
225
+ console.log(`✓ ${r.harness} 实测:命中 ${where} · ${m?.chars ?? 0} 字 · ${m?.requests?.length ?? 0} 个请求`);
226
+ } else if (r.status === "missing") console.log(`· ${r.harness} 未安装,跳过`);
227
+ else console.log(`✗ ${r.harness} ${r.status}${r.reason ? ` — ${r.reason}` : ""}`);
228
+ }
229
+ console.log(`\n证据:${join(DATA, "verification", "latest.json")}`);
230
+ cleanupRoots();
231
+ if (report.harnesses.some((r) => ["mismatch", "error"].includes(r.status))) process.exitCode = 1;
232
+ break;
233
+ }
234
+ default:
235
+ await launch(cmd);
236
+ }
237
+
238
+ // ── 用东西 ────────────────────────────────────────────────────────────────
239
+
240
+ /** 无参:state.default 说跑谁就跑谁;没有默认就跑到 Web 去选 */
241
+ async function defaultHarness() {
242
+ const { state } = loadState(DATA);
243
+ const harness = state.default?.harness;
244
+ if (!harness) {
245
+ console.log("还没有默认 harness。两种办法:\n agent-purge web 在浏览器里选,再点“设为默认执行器”\n agent-purge <harness> 直接点名跑(如 agent-purge codex)");
246
+ process.exit(0);
247
+ }
248
+ const eff = resolveFor(state, harness);
249
+ const mark = (from) => (from === "override" ? "(单独指定)" : "(跟随全局)");
250
+ console.log(`[default] ${harness} · ${eff.mode}${mark(eff.modeFrom)} · persona=${eff.persona ?? "-"}${mark(eff.personaFrom)}(state.json)`);
251
+ return harness;
252
+ }
253
+
254
+ async function launch(name) {
255
+ const spec = matrix.harnesses[name];
256
+ if (!spec) die(`未知 harness: ${name}(可用: ${Object.keys(matrix.harnesses).join(", ")};子命令见 --help)`);
257
+ if (spec.class === "unsupported") die(`${name} 做不到:${spec.reason}`);
258
+
259
+ const { state } = loadState(DATA);
260
+ // 由 state.json 决定注入方式;显式 --mode 永远压过它
261
+ const explicitMode = flag("--mode", null);
262
+ if (explicitMode !== null && !MODES.includes(explicitMode)) die(`--mode 只能是 ${MODES.join(" / ")}`);
263
+ const config = launchConfig(state, name, { mode: explicitMode, persona: flag("--persona", null) === "active" ? state.persona : flag("--persona", null) });
264
+ const mode = config.mode;
265
+
266
+ // 裸敲 pi 的持久位(写 state.json,扩展下一会话读它)
267
+ const autoFlag = name === "pi" || name === "omp" ? flag("--auto", null) : null;
268
+ if (autoFlag !== null) {
269
+ if (autoFlag !== "on" && autoFlag !== "off") die("--auto 只能是 on / off");
270
+ const { setPiAuto, setOmpAuto } = await import("../lib/state.mjs");
271
+ if (name === "pi") setPiAuto(DATA, autoFlag === "on");
272
+ else setOmpAuto(DATA, autoFlag === "on");
273
+ console.log(`[auto] 裸敲 ${name} ${autoFlag === "on" ? "会" : "不会"}自动注入(已写入 state.json)`);
274
+ }
275
+
276
+ // pi 的人设落在哪一层:sys(默认,真系统提示词)或 user(塞进用户消息,给忽略 system 的模型)
277
+ const layerFlag = name === "pi" ? flag("--layer", null) : null;
278
+ if (layerFlag !== null) {
279
+ if (layerFlag !== "sys" && layerFlag !== "user") die("--layer 只能是 sys / user");
280
+ const { setPiUserLayer } = await import("../lib/state.mjs");
281
+ setPiUserLayer(DATA, layerFlag === "user");
282
+ console.log(
283
+ layerFlag === "user"
284
+ ? "[layer] 人设额外塞进用户消息(给忽略 system 的模型用;代价是每条消息多一段)"
285
+ : "[layer] 人设走 system 层(默认)",
286
+ );
287
+ }
288
+
289
+ // 实测策略:该 harness 的哪一层真的能注入(数据来自 matrix.json armor 段)
290
+ const { plan } = await import("../lib/armor.mjs");
291
+ const strategy = plan(matrix, name);
292
+ if (!strategy.error) {
293
+ if (strategy.requiresSeed) console.log(`[策略] ${name}:裸注入 sys 层实测 ${strategy.bare.sys}%,带人设词种子 ${strategy.persona.sys}% —— 需要 seed 层`);
294
+ const noSys = strategy.blocked.find((b) => b.layer === "sys");
295
+ if (noSys && mode !== "off") console.log(`[策略] ${name}:sys 层不可用(${noSys.reason}),本次只能靠 user 层`);
296
+ }
297
+ if (mode === "replace" && STRICT_REPLACE.has(name)) {
298
+ console.log(`[mode] ${name} 没有宿主提示词读取口:replace 实际按 strict 执行;要保留宿主请用 --mode append`);
299
+ }
300
+
301
+ // 通道全废的家(闭源桌面 / 提示词在服务端)说清楚:这次只是原样启动,没注入任何东西
302
+ {
303
+ const c = capsFor(name);
304
+ if (!c.shim.ok && !c.install.ok && !c.slash.ok && mode !== "off") {
305
+ const why = [c.shim, c.install, c.slash].map((x) => x.why).filter(Boolean)[0];
306
+ console.log(`[提示] ${name} 注入不进去,本次是原样启动:${why}`);
307
+ }
308
+ }
309
+
310
+ let picked = null, personaPath = null, personaText = "";
311
+ if (mode !== "off") {
312
+ const { createStore } = await import("../lib/personas.mjs");
313
+ const store = createStore(DATA);
314
+ const id = config.persona ?? store.activeId();
315
+ picked = store.read(id);
316
+ if (!picked) die(`没有 persona: ${id}`);
317
+ personaPath = picked.id === store.activeId() ? store.refreshActive() : store.materialize(picked.id);
318
+ personaText = picked.body.trim();
319
+ }
320
+ const cwd = resolve(flag("--cwd", process.cwd()));
321
+
322
+ // --ask:一次性问答,与 Web UI 共用 oneshots 规格;不给才启动交互式 harness
323
+ // 透传:有 -- 就只收 -- 后面的;没有就把“不是 agent-purge 自己的 flag”全交给 harness
324
+ // (这样裸命令 `pi -p "…"` / `codex exec "…"` 经 shim 进来也能把参数带到,--ask 也一样)
325
+ const dd = argv.indexOf("--");
326
+ let passthrough = [];
327
+ if (dd !== -1) {
328
+ passthrough = argv.slice(dd + 1);
329
+ } else {
330
+ const ownWithValue = new Set(["--ask", "--mode", "--persona", "--cwd", "--auto", "--layer"]);
331
+ const tail = argv.slice(1);
332
+ for (let i = 0; i < tail.length; i++) {
333
+ const t = tail[i];
334
+ if (ownWithValue.has(t)) {
335
+ i++;
336
+ continue;
337
+ }
338
+ if (t === "--dry-run" || ownWithValue.has(t.split("=")[0])) continue;
339
+ passthrough.push(t);
340
+ }
341
+ }
342
+
343
+ const askEq = controlArgs.find((a) => a.startsWith("--ask="));
344
+ const askIdx = controlArgs.indexOf("--ask");
345
+ const askPrompt = askEq !== undefined ? askEq.slice(6) : askIdx !== -1 ? controlArgs[askIdx + 1] : null;
346
+ let built;
347
+ if (askPrompt !== null) {
348
+ const raw = askPrompt;
349
+ if (!raw || raw.startsWith("--")) die('--ask 需要一个 prompt,例如: agent-purge codex --ask "解释这段代码"');
350
+ const prompt = raw.trim();
351
+ const one = oneshots[name];
352
+ if (!one) die(`${name} 没有一次性运行模式(可用: ${Object.keys(oneshots).join(", ")})`);
353
+ built = one({ prompt, personaPath, personaText, mode, cwd, root: DATA });
354
+ if (passthrough.length) built.args = [...built.args, ...passthrough];
355
+ } else {
356
+ built = backends[name]?.({ personaPath, personaText, mode, args: passthrough, cwd, root: DATA, reason: spec.reason }) ?? {
357
+ unsupported: "该 harness 还没有后端实现",
358
+ };
359
+ }
360
+
361
+ if (built.manual) {
362
+ console.log(`${name}: 需要人工介入`);
363
+ built.manual.forEach((line) => console.log(" - " + line));
364
+ process.exit(0);
365
+ }
366
+ if (built.unsupported) die(`${name} 做不到:${built.unsupported}`);
367
+
368
+ if (built.note) console.log(built.note);
369
+ const dryRun = controlArgs.includes("--dry-run");
370
+ const ompProfile = name === "omp" ? (flag("--profile", null) ?? process.env.OMP_PROFILE ?? null) : null;
371
+ const ompNoExtensions = name === "omp" && argv.includes("--no-extensions");
372
+ const useExtension = mode !== "off" && (name === "pi" || (name === "omp" && askPrompt === null && !ompProfile && !ompNoExtensions));
373
+ let extFile = null;
374
+ if (useExtension) {
375
+ if (name === "omp" && ["--system-prompt", "--append-system-prompt"].includes(built.args[0])) built.args.splice(0, 2);
376
+ const { resolveSlash } = await import("../lib/slashes.mjs");
377
+ extFile = resolveSlash(name, DATA)?.file;
378
+ if (!extFile) die(`${name}: 没有扩展目录`);
379
+ // 临时 -e 只供本次进程;不会悄悄选择常驻 /purge 或覆盖用户已有扩展。
380
+ if (!state.armed?.[name]?.includes("slash")) {
381
+ extFile = join(DATA, "runtime", "extension-" + name + ".mjs");
382
+ built.args = ["-e", extFile, ...built.args];
383
+ }
384
+ }
385
+ if (mode === "off") {
386
+ const { launchConflicts } = await import("../lib/launch-conflicts.mjs");
387
+ const conflicts = launchConflicts(DATA, name, cwd);
388
+ if (conflicts.length) die("本次 off 与仍在磁盘上的常驻/项目内容冲突:\n" + conflicts.join("\n") + "\n请先 agent-purge pause 撤下登记内容,再重试。全局选择保持不变。");
389
+ console.log(state.default?.mode === "off" ? "[off] 总电源关着:本次不安装、不 materialize、不注入;恢复请用 agent-purge resume" : "[off] 本次透传;全局选择保持不变");
390
+ }
391
+ console.log(`[cmd] ${[built.cmd, ...previewArgs(built.args)].join(" ")}`);
392
+ if (extFile) console.log(`[ext] ${extFile}`);
393
+ if (dryRun) return;
394
+ if (!resolveBin(built.cmd)) die(`${name}: 找不到可执行文件 ${built.cmd}`);
395
+ if (name === "opencode" && built.args.some(v => v === "--attach" || v.startsWith("--attach="))) die("一次性启用使用独立宿主;--attach 指向已有服务,请使用该服务已登记的常驻通道");
396
+ const needsLease = useExtension || built.runtime || ["pi", "omp", "dsh", "opencode", "opencode2"].includes(name);
397
+ const lease = needsLease ? createLaunch(DATA, name, { persona: picked?.id, mode, writer: useExtension ? "extension" : built.runtime ? "temporary" : "cli" }) : null;
398
+ let undoExtension = null, cleaned = false, child = null, server = null;
399
+ const cleanup = () => {
400
+ if (cleaned) return true;
401
+ try {
402
+ lease?.close();
403
+ if (server) { server.close(); server = null; }
404
+ built.cleanup?.();
405
+ if (undoExtension) { undoExtension(); undoExtension = null; }
406
+ cleaned = true; return true;
407
+ } catch (error) { console.error("[cleanup] " + error.message + ";恢复凭据已保留,可重试"); return false; }
408
+ };
409
+ process.once("exit", cleanup);
410
+ try {
411
+ const env = { ...launchEnvironment(process.env), ...built.env, ...lease?.env };
412
+ if (useExtension) {
413
+ const { extensionSource } = await import("../lib/extension.mjs");
414
+ const { mkdirSync } = await import("node:fs");
415
+ mkdirSync(join(extFile, ".."), { recursive: true });
416
+ undoExtension = swapFile(extFile, extensionSource(DATA, name));
417
+ }
418
+ built.setup?.();
419
+ if (built.privateServer) {
420
+ const { startPrivateServer } = await import("../lib/private-server.mjs");
421
+ server = await startPrivateServer(built.cmd, { env, cwd, lease });
422
+ built.args = server.clientArgs(built.args);
423
+ }
424
+ const stdio = askPrompt !== null ? ["ignore", "inherit", "inherit"] : "inherit";
425
+ const descriptor = prepareCommand(built.cmd, built.args, { cwd, stdio, env });
426
+ if (lease && !server && !descriptor.hostDirect) throw new Error("临时启用尚未验证这个启动器的宿主归属,请指向原生 exe 或标准 Node 入口");
427
+ const startedAt = Date.now(); child = spawnPrepared(descriptor);
428
+ if (lease && !server && child.pid) lease.publish(child.pid, startedAt);
429
+ let finished = false;
430
+ const finish = code => { if (finished) return; finished = true; process.exitCode = cleanup() ? code ?? 1 : 1; };
431
+ child.once("error", error => { console.error(`${name}: ${error.message}`); finish(1); });
432
+ child.once("close", finish);
433
+ } catch (error) {
434
+ if (child) child.kill(); cleanup(); process.exitCode = 1; console.error(`${name}: ${error.message}`);
435
+ }
436
+
437
+ }
438
+
439
+ // ── 选东西的实现 ───────────────────────────────────────────────────────────
440
+
441
+ function printApply(r) {
442
+ console.log(`persona ${r.persona ?? "(未设置)"} · mode ${r.mode} · revision ${r.revision}`);
443
+ for (const row of r.results) {
444
+ // 被例外覆盖的那家写进去的不是全局值,直接标出来,否则看不出来它是怎么生效的
445
+ const odd = row.mode && row.mode !== r.mode ? ` [${row.persona ?? "-"} · ${row.mode}]` : "";
446
+ console.log(`${row.status === "failed" ? "✗" : "✓"} ${row.harness}:${row.channel}${odd}${row.channel === "project" && row.file ? ` → ${row.file}` : ""} — ${row.error ?? row.message}`);
447
+ }
448
+ if (!r.ok) process.exitCode = 1;
449
+ }
450
+
451
+ /** 只列跟全局不一样的几家;没例外就什么都不打印 */
452
+ function printOverrides(state) {
453
+ for (const [h, o] of Object.entries(state.overrides ?? {})) {
454
+ const bits = [o.persona ? `人设=${o.persona}` : "", o.mode ? `模式=${o.mode}` : ""].filter(Boolean);
455
+ console.log(` ${h}: ${bits.join(" ")}(其余跟随全局;--for ${h} follow 恢复)`);
456
+ }
457
+ }
458
+
459
+ async function useCmd() {
460
+ const id = argv[1];
461
+ const forHarness = flag("--for", null);
462
+ if (forHarness) {
463
+ if (!id) die("用法:agent-purge use <persona|follow> --for <harness>");
464
+ const follow = id === "follow";
465
+ const { createStore } = await import("../lib/personas.mjs");
466
+ const store = createStore(DATA);
467
+ if (!follow && !store.read(id)) die(`没有人设: ${id}(可用: ${store.list().map((p) => p.id).join(", ")})`);
468
+ const report = configureHarness(DATA, forHarness, { persona: follow ? null : id });
469
+ const eff = resolveFor(loadState(DATA).state, forHarness);
470
+ console.log(`${forHarness} 人设 → ${eff.persona ?? "(未设置)"}(${eff.personaFrom === "override" ? "单独指定" : "跟随全局"})`);
471
+ printApply(report);
472
+ return;
473
+ }
474
+ if (!id) {
475
+ console.log(`当前 persona: ${loadState(DATA).state.persona ?? "(未设置)"}`);
476
+ printOverrides(loadState(DATA).state);
477
+ return;
478
+ }
479
+ printApply(applyConfiguration(DATA, { persona: id }));
480
+ }
481
+
482
+ async function syncCmd() {
483
+ printApply(applyConfiguration(DATA, { onlyFailed: argv.includes("--failed") }));
484
+ }
485
+
486
+ function modeCmd() {
487
+ const mode = argv[1];
488
+ const forHarness = flag("--for", null);
489
+ if (forHarness) {
490
+ if (!mode) die("用法:agent-purge mode <strict|replace|append|off|follow> --for <harness>");
491
+ const follow = mode === "follow";
492
+ if (!follow && !MODES.includes(mode)) die(`未知模式: ${mode}(可用: ${MODES.join("/")}/follow)`);
493
+ const report = configureHarness(DATA, forHarness, { mode: follow ? null : mode });
494
+ const eff = resolveFor(loadState(DATA).state, forHarness);
495
+ console.log(`${forHarness} 模式 → ${eff.mode}(${eff.modeFrom === "override" ? "单独指定" : "跟随全局"})`);
496
+ printApply(report);
497
+ return;
498
+ }
499
+ if (!mode) {
500
+ console.log(`当前模式: ${loadState(DATA).state.default?.mode ?? "replace"}\nstrict=只发人设;replace=移除宿主身份句;append=宿主后追加;off=暂停并撤下静态内容`);
501
+ printOverrides(loadState(DATA).state);
502
+ return;
503
+ }
504
+ printApply(applyConfiguration(DATA, { mode }));
505
+ }
506
+
507
+ async function installCmd(verb, rest) {
508
+ const { targets } = await import("../lib/persist.mjs");
509
+ const named = rest.filter(value => !value.startsWith("-"));
510
+ const ids = named.length ? named : Object.keys(targets);
511
+ for (const harness of ids) {
512
+ const r = changeChannel(DATA, { harness, channel: "install", enabled: verb === "install", force: rest.includes("--force") });
513
+ console.log(`${r.ok ? "✓" : "✗"} ${verb} ${harness} — ${r.error ?? r.action ?? r.message}${r.file ? " → " + r.file : ""}`);
514
+ if (r.profiles) console.log(` 实际 profile:${r.profiles.join(", ") || "没有已安装的插件"}`);
515
+ for (const file of r.preserved ?? []) console.log(` 保留用户修改/旧版文件:${file}`);
516
+ if (verb === "uninstall" && harness === "dsh") console.log(" uninstall 仅撤常驻通道;撤销包含 /purge 在内的全部入口请用 agent-purge disarm dsh。");
517
+ if (!r.ok) process.exitCode = 1;
518
+ }
519
+ }
520
+
521
+ async function armCmd(verb, rest) {
522
+ const named = rest.filter(value => !value.startsWith("-"));
523
+ const ids = named.length ? named : Object.keys(matrix.harnesses).filter(h => matrix.harnesses[h].class !== "unsupported");
524
+ for (const h of ids) if (!matrix.harnesses[h]) die(`未知执行器: ${h}`);
525
+ if (rest.includes("--all")) {
526
+ console.log("总电源:" + (verb === "arm" ? "恢复" : "暂停"));
527
+ printApply(applyConfiguration(DATA, { enabled: verb === "arm" }));
528
+ }
529
+ const channels = ["install", "shim", "slash", "project"].filter(c => rest.includes("--" + c));
530
+ let shim = false;
531
+ for (const harness of ids) {
532
+ if (verb === "disarm" && !channels.length) {
533
+ const r = disableHarness(DATA, harness);
534
+ console.log(`${r.ok ? "✓" : "✗"} disarm ${harness}`);
535
+ for (const row of r.results) if (!row.ok) console.log(" " + row.channel + ": " + row.error);
536
+ if (!r.ok) process.exitCode = 1;
537
+ continue;
538
+ }
539
+ for (const channel of channels.length ? channels : ["install", "shim", "slash"]) {
540
+ if (verb === "arm" && !capsFor(harness)[channel]?.ok) { console.log(`- ${harness}:${channel} — ${capsFor(harness)[channel]?.why}`); continue; }
541
+ const r = changeChannel(DATA, { harness, channel, enabled: verb === "arm", force: rest.includes("--force") });
542
+ console.log(`${r.ok ? "✓" : "✗"} ${verb} ${harness}:${channel} — ${r.error ?? r.action ?? r.message}`);
543
+ if (!r.ok) process.exitCode = 1;
544
+ if (r.ok && verb === "arm" && channel === "shim") shim = true;
545
+ }
546
+ }
547
+ if (shim) { const { ensureUserPath } = await import("../lib/shim.mjs"); const r = ensureUserPath(); console.log("[path] " + r.how); }
548
+ }
549
+
550
+ async function slashCmd(rest) {
551
+ const { slashStatus } = await import("../lib/slashes.mjs");
552
+ const verb = rest[0];
553
+ if (!verb || verb === "status") {
554
+ for (const row of slashStatus(DATA)) console.log(`${row.installed ? "✓" : "·"} ${row.id}: ${row.note} → ${row.file}`);
555
+ return;
556
+ }
557
+ const enabled = verb !== "off";
558
+ const named = (enabled ? rest : rest.slice(1)).filter(v => !v.startsWith("-"));
559
+ const ids = named.length ? named : slashStatus(DATA).filter(s => enabled ? s.offered : s.installed).map(s => s.id);
560
+ for (const harness of ids) {
561
+ const r = changeChannel(DATA, { harness, channel: "slash", enabled });
562
+ console.log(`${r.ok ? "✓" : "✗"} /purge ${harness} — ${r.error ?? r.action ?? r.message}`);
563
+ if (!r.ok) process.exitCode = 1;
564
+ }
565
+ }
566
+
567
+ async function projectCmd(rest) {
568
+ const { projectStatus, projectTargets, gitHint } = await import("../lib/project.mjs");
569
+ const dir = resolve(flag("--dir", process.cwd()));
570
+ const verb = ["on", "off", "status"].includes(rest[0]) ? rest[0] : "status";
571
+ const named = [];
572
+ for (let i = rest[0] === verb ? 1 : 0; i < rest.length; i++) {
573
+ if (rest[i] === "--dir") { i++; continue; }
574
+ if (!rest[i].startsWith("-")) named.push(rest[i]);
575
+ }
576
+ const capable = Object.keys(projectTargets);
577
+ if (verb === "status") {
578
+ const rows = projectStatus(dir, DATA);
579
+ console.log(`项目目录:${dir}`);
580
+ for (const r of rows) console.log(`${r.on || r.selected ? "✓" : " "} ${r.id.padEnd(9)} ${r.file}${r.selected && !r.on ? "(已选,尚未写入)" : r.hasBlock && !r.on ? "(有块但不是我们记的)" : ""}`);
581
+ return;
582
+ }
583
+ if (verb !== "on" && verb !== "off") die(`用法:agent-purge project on|off [harness…] [--dir X]`);
584
+ const ids = named.length
585
+ ? named
586
+ : verb === "on"
587
+ ? Object.keys(loadState(DATA).state.armed ?? {}).filter((h) => capable.includes(h))
588
+ : projectStatus(dir, DATA).filter((r) => r.on || r.selected).map((r) => r.id);
589
+ if (!ids.length) {
590
+ console.log(verb === "on" ? `没有已接管的项目级 harness(可用: ${capable.join(", ")});点名写:agent-purge project on pi` : "这个目录里没有托管块");
591
+ return;
592
+ }
593
+ for (const h of ids) {
594
+ if (!capable.includes(h)) {
595
+ console.log(`- ${h}: 没有项目级规则文件`);
596
+ continue;
597
+ }
598
+ const r = changeChannel(DATA, { harness: h, channel: "project", enabled: verb === "on", dir });
599
+ if (!r.ok) {
600
+ console.log(`✗ ${h}: ${r.error ?? "写入失败"}`);
601
+ continue;
602
+ }
603
+ console.log(`${verb === "on" ? "✓" : "-"} ${h} ${r.file}${r.action ? `(${r.action})` : ""}`);
604
+ if (verb === "on") r.hint = gitHint(h, dir);
605
+ if (r.hint) console.log(` 注意:${r.hint}`);
606
+ }
607
+ }
608
+
609
+ async function statusCmd() {
610
+ const { migrated } = loadState(DATA);
611
+ migrated.forEach((m) => console.log(`[migrate] ${m}`));
612
+ // armed 是缓存,磁盘才是真相:先把假记录摘掉,再报数
613
+ const { reconcileArmed } = await import("../lib/armed.mjs");
614
+ const fixed = await reconcileArmed(DATA);
615
+ if (fixed.length) console.log(`[对账] armed 纠正:${fixed.join(", ")}`);
616
+ const { state } = loadState(DATA);
617
+ return Promise.all([import("../lib/shim.mjs"), import("../lib/slashes.mjs")]).then(([shimMod, slashMod]) => {
618
+ const shims = shimMod.shimStatus();
619
+ const slashes = slashMod.slashStatus(DATA);
620
+
621
+ console.log(`persona ${state.persona ?? "(未设置)"}`);
622
+ console.log(`default ${state.default ? `${state.default.harness ?? "(未选默认)"} · ${state.default.mode}` : "(未设置)"} 裸敲 agent-purge 用它`);
623
+ console.log(`state ${join(DATA, "state.json")}`);
624
+ console.log("");
625
+ console.log("harness shim slash install /purge层 常驻落点");
626
+ for (const [h] of Object.entries(matrix.harnesses)) {
627
+ const armed = armedOf(state, h);
628
+ const sh = shims.find((s) => s.id === h);
629
+ const sl = slashes.find((s) => s.id === h);
630
+ const cap = capsFor(h, { bin: sh?.target ?? null });
631
+ const mark = (on) => (on ? " ✓ " : " · ");
632
+ const slashCol = sl && sl.offered ? (sl.layer === "sys" ? "sys(热)" : "user") : "—";
633
+ // install 列不写“注册没注册”,写“能不能装”:装了是 ✓,能装没装是「可用」,没落点才是 —
634
+ const installCol = armed.includes("install") ? " ✓ " : cap.install.ok ? "可用" : " — ";
635
+ const landing = cap.install.ok ? `${cap.install.layer || "?"}${cap.install.verified === false ? "(未实测)" : ""}` : (cap.install.why || "");
636
+ console.log(
637
+ `${h.padEnd(11)}${mark(sh?.installed).padEnd(7)}${mark(sl?.installed).padEnd(7)}${installCol.padEnd(10)}${slashCol.padEnd(10)}${landing}`,
638
+ );
639
+ }
640
+ const dir = shimMod.shimDir();
641
+ const ps = shimMod.pathState();
642
+ const where = !ps.present ? "不在 PATH" : ps.first ? "已在 PATH 最前" : "在 PATH 但不最前(真身可能先被命中)";
643
+ console.log(`\nshim 目录 ${dir}(${where})`);
644
+ if (!ps.first) console.log(`修好它:agent-purge arm <h> --shim(会自动修正 PATH)`);
645
+ });
646
+ }
647
+
648
+ async function personaImportCmd(args = []) {
649
+ let sub = args[0];
650
+ let source = args[1];
651
+ if (sub !== "pull" && sub !== "import") {
652
+ source = sub;
653
+ sub = "pull";
654
+ }
655
+ if (!source && sub === "pull") {
656
+ source = "ZiChuanLan/some-magic";
657
+ }
658
+ if (!source) {
659
+ die("用法:agent-purge pull [owner/repo 或 本地目录] 或 agent-purge persona import <dir>");
660
+ }
661
+
662
+ const noOverwrite = argv.includes("--no-overwrite");
663
+ const { importPersonas } = await import("../lib/persona-import.mjs");
664
+ console.log(`正在从 ${source} 拉取人设...`);
665
+ try {
666
+ const res = await importPersonas(source, DATA, { overwrite: !noOverwrite });
667
+ console.log(`\n拉取完成!共导入 ${res.total} 个人设:`);
668
+ for (const item of res.imported) {
669
+ console.log(` ✓ ${item.id.padEnd(14)} ${item.name} (${item.chars} 字, ${item.action === "created" ? "新增" : "更新"})`);
670
+ }
671
+ if (res.skipped?.length) {
672
+ console.log(`\n跳过 ${res.skipped.length} 个文件:`);
673
+ for (const item of res.skipped) {
674
+ console.log(` - ${item.file || item.id}: ${item.reason}`);
675
+ }
676
+ }
677
+ if (res.errors?.length) {
678
+ console.log(`\n错误 ${res.errors.length} 项:`);
679
+ for (const item of res.errors) {
680
+ console.log(` ✗ ${item.file}: ${item.error}`);
681
+ }
682
+ }
683
+ console.log(`\n查看已安装人设:agent-purge ls`);
684
+ console.log(`切换当前人设:agent-purge use <id>`);
685
+ } catch (e) {
686
+ die(`拉取失败:${e.message}`);
687
+ }
688
+ }
689
+
690
+ async function lsCmd() {
691
+ const { createStore } = await import("../lib/personas.mjs");
692
+ const store = createStore(DATA);
693
+ const { state } = loadState(DATA);
694
+ const fit = store.resolve({ harness: state.default?.harness, model: null });
695
+ for (const p of store.list()) {
696
+ const mark = p.id === store.activeId() ? "*" : " ";
697
+ const best = fit?.persona?.id === p.id && fit?.matched ? " ← 最匹配当前 harness" : "";
698
+ const desc = p.meta?.description ?? p.meta?.desc ?? "";
699
+ console.log(`${mark} ${p.id.padEnd(14)} ${String(p.meta?.name ?? "").padEnd(12)} ${desc.slice(0, 44)}${best}`);
700
+ }
701
+ console.log(`\n* = active(agent-purge use <id> 切换)`);
702
+ }
703
+
704
+ async function webCmd() {
705
+ const port = Number(flag("--port", "7788"));
706
+ if (!Number.isInteger(port) || port < 0 || port > 65535) die("端口必须为 0..65535 的整数");
707
+ const noOpen = argv.includes("--no-open");
708
+ const url = `http://127.0.0.1:${port}`;
709
+ const { openAppWindow } = await import("../lib/window.mjs");
710
+
711
+ // --detach:脱离当前终端后台存活,日志丢弃
712
+ if (argv.includes("--detach")) {
713
+ if (port === 0) die("--detach 需要明确端口;随机端口请用前台 web --port 0 --no-open");
714
+ // 源码态 execPath 是 node,得连脚本路径一起给;编译态 execPath 就是 exe 本体
715
+ const serve = isCompiled() ? ["--serve", String(port)] : [join(ROOT, "web", "server.mjs"), "--serve", String(port)];
716
+ const child = spawn(process.execPath, serve, {
717
+ detached: true,
718
+ stdio: "ignore",
719
+ shell: false,
720
+ windowsHide: true,
721
+ env: { ...process.env, AGENT_PURGE_HOME: DATA },
722
+ });
723
+ child.unref();
724
+ console.log(`agent-purge 后台启动请求已发送 → ${url}(pid ${child.pid};请确认服务可连接)`);
725
+ if (!noOpen) {
726
+ const res = openAppWindow(url);
727
+ if (res.mode === "app") console.log(`已启动独立桌面窗口(${res.browser})`);
728
+ }
729
+ process.exit(0);
730
+ }
731
+
732
+ const server = serveWeb(port);
733
+ if (!noOpen) {
734
+ server.once("listening", () => {
735
+ const res = openAppWindow(`http://127.0.0.1:${server.address().port}`);
736
+ if (res.mode === "app") console.log(`✓ 已作为独立桌面窗口打开(无标签/地址栏,原生客户端体验)`);
737
+ });
738
+ }
739
+ return server;
740
+ }
741
+
742
+ // ── 其余 ──────────────────────────────────────────────────────────────────
743
+
744
+ async function armorCli(rest) {
745
+ const { renderMatrix, renderPlan, plans, plan, preflight, runBench, runJudge } = await import("../lib/armor.mjs");
746
+ const color = !argv.includes("--no-color") && Boolean(process.stdout.isTTY);
747
+ const sub = rest[0]?.startsWith("-") ? "matrix" : rest[0] ?? "matrix";
748
+ const opts = (name, fallback) => {
749
+ const i = rest.indexOf(name);
750
+ return i === -1 ? fallback : rest[i + 1];
751
+ };
752
+ const flags = rest.filter((x) => x.startsWith("--"));
753
+ // 位置参数 ≠ 非 - 开头的 token:`--base https://x` 的值不是 harness 名
754
+ const VALUE_FLAGS = new Set(["--base", "--key", "--model", "--task", "--layers", "--only", "--repeat", "--concurrency", "--timeout", "--out", "--dir", "--probe"]);
755
+ const positionals = (from = 1) => {
756
+ const out = [];
757
+ for (let i = from; i < rest.length; i++) {
758
+ const a = rest[i];
759
+ if (a.startsWith("--")) {
760
+ if (VALUE_FLAGS.has(a)) i++;
761
+ continue;
762
+ }
763
+ if (!a.startsWith("-")) out.push(a);
764
+ }
765
+ return out;
766
+ };
767
+
768
+ if (sub === "matrix") return renderMatrix(matrix, { color });
769
+
770
+ if (sub === "plan") {
771
+ const ids = positionals();
772
+ const list = ids.length ? ids.map((id) => plan(matrix, id)) : plans(matrix);
773
+ list.forEach((p, i) => {
774
+ if (i) console.log();
775
+ renderPlan(p, { color });
776
+ });
777
+ if (!ids.length) console.log(`\n${"─".repeat(48)}\n批量跑:agent-purge armor run --task crack-bare --layers off,sys,user --only ${list.map((p) => p.id).join(",")}`);
778
+ return;
779
+ }
780
+
781
+ if (sub === "preflight" || sub === "check") {
782
+ const ids = positionals();
783
+ const env = { base: opts("--base", process.env.MG_BASE), key: opts("--key", process.env.MG_KEY), model: opts("--model", process.env.MG_MODEL) };
784
+ let bad = 0;
785
+ for (const id of ids.length ? ids : ["pi"]) {
786
+ const r = preflight(matrix, id, env);
787
+ console.log(`${r.ok ? "✓" : "✗"} ${id}`);
788
+ r.checks.forEach((c) => console.log(` ${c.ok ? "ok " : "MISS"} ${c.name}${c.detail ? ` — ${c.detail}` : ""}`));
789
+ if (!r.ok) bad++;
790
+ }
791
+ if (bad) process.exitCode = 1;
792
+ return;
793
+ }
794
+
795
+ if (sub === "run") {
796
+ if (flags.includes("--help")) {
797
+ console.log(`agent-purge armor run --task crack-bare --layers off,sys,user --only pi,codex --repeat 2 --concurrency 3
798
+ --base <url> --key <key> --model <id> 上游(默认读 MG_BASE / MG_KEY / MG_MODEL)
799
+ --task crack-bare|crack-persona 问法(裸问法 / 带人设词种子)
800
+ --layers off,sys,user|off,sys
801
+ --out <dir>`);
802
+ return;
803
+ }
804
+ const env = {
805
+ MG_BASE: opts("--base", process.env.MG_BASE),
806
+ MG_KEY: opts("--key", process.env.MG_KEY),
807
+ MG_MODEL: opts("--model", process.env.MG_MODEL),
808
+ };
809
+ if (!env.MG_BASE || !env.MG_KEY) {
810
+ console.error("缺少上游:--base / --key(或环境变量 MG_BASE / MG_KEY)。例:\n agent-purge armor run --base https://api.deepseek.com/v1 --key $env:MG_KEY --model deepseek-flash --task crack-bare --layers off,sys --only pi");
811
+ process.exit(2);
812
+ }
813
+ const pass = [];
814
+ for (let i = 0; i < rest.length; i++) {
815
+ const a = rest[i];
816
+ if (["--base", "--key", "--model"].includes(a)) { i++; continue; }
817
+ pass.push(a);
818
+ }
819
+ const { code } = runBench(pass, env);
820
+ process.exitCode = code;
821
+ return;
822
+ }
823
+
824
+ if (sub === "judge") {
825
+ const pass = [];
826
+ for (let i = 0; i < rest.length; i++) {
827
+ const a = rest[i];
828
+ if (["--base", "--key", "--model"].includes(a)) { i++; continue; }
829
+ pass.push(a);
830
+ }
831
+ const { code } = runJudge(pass, {
832
+ MG_BASE: opts("--base", process.env.MG_BASE),
833
+ MG_KEY: opts("--key", process.env.MG_KEY),
834
+ MG_MODEL: opts("--model", process.env.MG_MODEL),
835
+ });
836
+ process.exitCode = code;
837
+ return;
838
+ }
839
+
840
+ console.error(`未知子命令: armor ${sub}(可用: matrix | plan | preflight | run | judge)`);
841
+ process.exit(2);
842
+ }
843
+
844
+ function printMatrix() {
845
+ const rows = Object.entries(matrix.harnesses).map(([name, s]) => ({
846
+ harness: name,
847
+ class: s.class,
848
+ override: s.override === true ? "yes" : s.override ? String(s.override) : "no",
849
+ inject: s.inject ? "yes" : "no",
850
+ hot: s.hotSwitch ? "yes" : "no",
851
+ chars: s.defaultSystemChars != null ? `${s.defaultSystemChars} → ${s.purgedSystemChars ?? "?"}` : "—",
852
+ armor: s.armor ? `${s.armor.probe.bare.sys ?? "—"}${s.armor.probe.bare.sys == null ? "" : "%"} / ${s.armor.probe.bare.user ?? "—"}${s.armor.probe.bare.user == null ? "" : "%"}` : "未测",
853
+ version: s.measuredVersion ?? "—",
854
+ }));
855
+ const head = ["harness", "class", "override", "inject", "hot", "system chars", "sys/user 注入", "version"];
856
+ const width = head.map((h, i) => Math.max(h.length, ...rows.map((r) => String(Object.values(r)[i]).length)));
857
+ const line = (cells) => cells.map((c, i) => String(c).padEnd(width[i])).join(" ");
858
+ console.log(line(head));
859
+ console.log(width.map((w) => "-".repeat(w)).join(" "));
860
+ rows.forEach((r) => console.log(line(Object.values(r))));
861
+ const residues = Object.entries(matrix.harnesses).filter(([, s]) => s.residue);
862
+ if (residues.length) {
863
+ console.log("\n残留:");
864
+ residues.forEach(([n, s]) => console.log(` ${n}: ${s.residue}`));
865
+ }
866
+ }
867
+
868
+ function help() {
869
+ console.log(`agent-purge —— 把任意 harness 的系统提示词换成你的 persona
870
+
871
+ 一句话:先「选」,再「用」。选一次,之后所有入口都跟着变。
872
+
873
+ ① 选(写共享配置并同步;已加载动态插件下一轮读取,静态入口需新会话)
874
+ agent-purge use <id> 换 persona(ls 看有哪些,* = 当前;自动刷新已武装的常驻落点)
875
+ agent-purge use <id> --for <h> 只给某一家换 persona;--for <h> follow 恢复跟随全局
876
+ agent-purge pull [repo] 从 GitHub(默认 ZiChuanLan/some-magic)拉取人设
877
+ agent-purge persona import <dir> 从本地目录批量导入人设 (.md)
878
+ agent-purge sync 只刷新常驻落点/命令文件,不换 persona
879
+ agent-purge mode <m> replace(换身份, 默认) / strict(只留人设) / append(追加) / off
880
+ agent-purge mode <m> --for <h> 只给某一家设模式;--for <h> follow 恢复跟随全局
881
+ agent-purge pause 暂停并撤下静态托管内容,保留通道选择
882
+ agent-purge resume 恢复上次模式与原选择范围
883
+ agent-purge clean 从所有 harness 彻底拔出插件与接管命令并还原配置
884
+ agent-purge update 检查 GitHub Release 最新版本与下载更新
885
+ agent-purge web 浏览器里点选 harness · persona · mode
886
+
887
+ ② 用(命令入口与常驻配置的生效时机不同)
888
+ agent-purge 裸敲 = 跑 default 里那家(web 选过就用选过的)
889
+ agent-purge <h> [--ask "…"] 指定一家启动;--dry-run 只看命令,-- 后面透传
890
+ <h> web / arm 之后直接敲命令名,就已经注入
891
+
892
+ ③ 装(让「别的入口」也带人设;一条命令全装,想只做一项就点名)
893
+ agent-purge arm <h>|--all 常驻 + 裸命令接管 + /purge 全开(= 注册)
894
+ agent-purge 注册 [--all] 中文别名,同上
895
+ agent-purge arm <h> --shim 只做裸命令接管(写 PATH 最前)
896
+ agent-purge arm <h> --install 只做配置文件常驻
897
+ agent-purge arm <h> --slash 只做会话内 /purge
898
+ agent-purge disarm <h>|--all 以上全部撤掉;不带 <h> 就是所有 harness
899
+ agent-purge clean 按账本撒回所有写入(保留 state 与人设)
900
+ agent-purge self-uninstall --yes 撤回写入 + 永久删除数据目录(先备份人设)
901
+ agent-purge self-uninstall --dry-run 只预览将撤销和删除的项目
902
+ agent-purge project on|off [<h>…] 项目级:写当前目录的 AGENTS.md / CLAUDE.md
903
+ [--dir X] 托管块;只影响这个项目,删块即逐字节还原
904
+
905
+ 注:--all 是总电源(state.json 的 default.mode)。带 <h> 时只撤那家,
906
+ 但同样把总电源关掉——不然裸敲 agent-purge、agent-purge <h> 照样注入。
907
+ 彻底别管我:agent-purge disarm --all
908
+
909
+ ④ 看
910
+ agent-purge status 一张表:每家的 shim / slash / install / 能做什么
911
+ agent-purge ls persona 列表
912
+ agent-purge verify [harness…] 实测:本机真跑一次,看人设到底进没进 system(写 verification/latest.json)
913
+ agent-purge matrix 能力矩阵(matrix.json)
914
+ agent-purge armor [...] plan|preflight|run|judge 实测热力图
915
+ agent-purge doctor 本机探测(哪家装了、版本)
916
+
917
+ 通道语义
918
+ shim 在 PATH 最前放同名 cmd,裸敲即经 agent-purge 启动(sys 层注入)
919
+ install 改 harness 自己的配置/插件文件(连别人启动的它也会带人设)
920
+ slash pi / omp / dsh 通过扩展提供 /purge,会话下一轮读取
921
+ project 写当前项目根的 AGENTS.md / CLAUDE.md(和全局块同时开会两份都进)
922
+ 热切 pi / omp / dsh / opencode / opencode2 已验证同会话 A→B→strict→off
923
+ 首次安装或更新插件代码需重新加载;其他启动参数入口需新会话
924
+ 证据 npm run test:harness;Web「验证与诊断」展示请求角色与宿主残留
925
+ 配置写入不代表当前会话已读取;Codex 实际请求角色为 developer。
926
+
927
+ harness: pi, opencode, opencode2, dsh, codex, claude, grok, omp, hermes, zcode, antigravity, codebuddy
928
+ opencode2 = @opencode/cli 2.x(v2 插件 API),dsh = DeepSeek Harness(cordis 插件,desktop/web/headless 三形态)。
929
+ `);
930
+ }
931
+
932
+ /** [cmd] 只用于复现启动参数;人设正文不能整段落进终端/CI 日志。 */
933
+ function previewArgs(args) {
934
+ const secret = new Set(["--system-prompt", "--append-system-prompt", "--system-prompt-override", "--rules"]);
935
+ const out = [];
936
+ for (let i = 0; i < args.length; i++) {
937
+ const a = String(args[i]);
938
+ if (a === "-c" && typeof args[i + 1] === "string" && args[i + 1].startsWith("developer_instructions=")) {
939
+ out.push(a, `developer_instructions=<persona ${args[i + 1].length} 字>`);
940
+ i++;
941
+ continue;
942
+ }
943
+ if (secret.has(a) && args[i + 1] !== undefined) {
944
+ out.push(a, `<persona ${String(args[i + 1]).length} 字>`);
945
+ i++;
946
+ continue;
947
+ }
948
+ out.push(a.includes(" ") ? JSON.stringify(a) : a);
949
+ }
950
+ return out;
951
+ }
952
+
953
+ function die(message) {
954
+ console.error(message);
955
+ process.exit(2);
956
+ }