agent-purge 0.2.5 → 0.2.6
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/bin/agent-purge.mjs +738 -736
- package/lib/caps.mjs +4 -4
- package/lib/dsh.mjs +346 -316
- package/lib/slashes.mjs +23 -2
- package/package.json +1 -1
package/bin/agent-purge.mjs
CHANGED
|
@@ -1,737 +1,739 @@
|
|
|
1
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
|
-
const i = argv.indexOf(name);
|
|
32
|
-
return i === -1 ? fallback : argv[i + 1];
|
|
33
|
-
};
|
|
34
|
-
let cmd = argv[0];
|
|
35
|
-
|
|
36
|
-
if (!cmd || cmd.startsWith("-")) cmd = await defaultHarness();
|
|
37
|
-
|
|
38
|
-
// ── 选东西 ────────────────────────────────────────────────────────────────
|
|
39
|
-
switch (cmd) {
|
|
40
|
-
case "use":
|
|
41
|
-
await useCmd();
|
|
42
|
-
break;
|
|
43
|
-
case "mode":
|
|
44
|
-
modeCmd();
|
|
45
|
-
break;
|
|
46
|
-
case "install":
|
|
47
|
-
case "uninstall":
|
|
48
|
-
await installCmd(cmd, argv.slice(1));
|
|
49
|
-
break;
|
|
50
|
-
case "arm":
|
|
51
|
-
case "disarm":
|
|
52
|
-
case "register":
|
|
53
|
-
case "注册":
|
|
54
|
-
await armCmd(cmd === "arm" || cmd === "register" || cmd === "注册" ? "arm" : "disarm", argv.slice(1));
|
|
55
|
-
break;
|
|
56
|
-
case "slash":
|
|
57
|
-
await slashCmd(argv.slice(1));
|
|
58
|
-
break;
|
|
59
|
-
case "status":
|
|
60
|
-
statusCmd();
|
|
61
|
-
break;
|
|
62
|
-
case "ls":
|
|
63
|
-
await lsCmd();
|
|
64
|
-
break;
|
|
65
|
-
case "doctor":
|
|
66
|
-
case "detect": {
|
|
67
|
-
const { doctor } = await import("../lib/doctor.mjs");
|
|
68
|
-
await doctor(argv);
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
case "matrix":
|
|
72
|
-
printMatrix();
|
|
73
|
-
break;
|
|
74
|
-
case "armor":
|
|
75
|
-
await armorCli(argv.slice(1));
|
|
76
|
-
break;
|
|
77
|
-
case "web":
|
|
78
|
-
webCmd();
|
|
79
|
-
break;
|
|
80
|
-
default:
|
|
81
|
-
await launch(cmd);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ── 用东西 ────────────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
/** 无参:state.default 说跑谁就跑谁;没有默认就跑到 Web 去选 */
|
|
87
|
-
async function defaultHarness() {
|
|
88
|
-
const { state } = loadState(DATA);
|
|
89
|
-
const harness = state.default?.harness;
|
|
90
|
-
if (!harness) {
|
|
91
|
-
console.log("还没有默认 harness。两种办法:\n agent-purge web 在浏览器里选,再点“设为默认执行器”\n agent-purge <harness> 直接点名跑(如 agent-purge codex)");
|
|
92
|
-
process.exit(0);
|
|
93
|
-
}
|
|
94
|
-
console.log(`[default] ${harness} · ${state.default.mode} · persona=${state.persona ?? "-"}(state.json)`);
|
|
95
|
-
return harness;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function launch(name) {
|
|
99
|
-
const spec = matrix.harnesses[name];
|
|
100
|
-
if (!spec) die(`未知 harness: ${name}(可用: ${Object.keys(matrix.harnesses).join(", ")};子命令见 --help)`);
|
|
101
|
-
if (spec.class === "unsupported") die(`${name} 做不到:${spec.reason}`);
|
|
102
|
-
|
|
103
|
-
const { state } = loadState(DATA);
|
|
104
|
-
// 由 state.json 决定注入方式;显式 --mode 永远压过它
|
|
105
|
-
const explicitMode = flag("--mode", null);
|
|
106
|
-
if (explicitMode !== null && !MODES.includes(explicitMode)) die(`--mode 只能是 ${MODES.join(" / ")}`);
|
|
107
|
-
const mode = explicitMode ?? state.default?.mode ?? "replace";
|
|
108
|
-
|
|
109
|
-
// 裸敲 pi 的持久位(写 state.json,扩展下一会话读它)
|
|
110
|
-
const autoFlag = name === "pi" ? flag("--auto", null) : null;
|
|
111
|
-
if (autoFlag !== null) {
|
|
112
|
-
if (autoFlag !== "on" && autoFlag !== "off") die("--auto 只能是 on / off");
|
|
113
|
-
const { setPiAuto } = await import("../lib/state.mjs");
|
|
114
|
-
setPiAuto(DATA, autoFlag === "on");
|
|
115
|
-
console.log(`[auto] 裸敲 pi ${autoFlag === "on" ? "会" : "不会"}自动注入(已写入 state.json)`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// pi 的人设落在哪一层:sys(默认,真系统提示词)或 user(塞进用户消息,给忽略 system 的模型)
|
|
119
|
-
const layerFlag = name === "pi" ? flag("--layer", null) : null;
|
|
120
|
-
if (layerFlag !== null) {
|
|
121
|
-
if (layerFlag !== "sys" && layerFlag !== "user") die("--layer 只能是 sys / user");
|
|
122
|
-
const { setPiUserLayer } = await import("../lib/state.mjs");
|
|
123
|
-
setPiUserLayer(DATA, layerFlag === "user");
|
|
124
|
-
console.log(
|
|
125
|
-
layerFlag === "user"
|
|
126
|
-
? "[layer] 人设额外塞进用户消息(给忽略 system 的模型用;代价是每条消息多一段)"
|
|
127
|
-
: "[layer] 人设走 system 层(默认)",
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 实测策略:该 harness 的哪一层真的能注入(数据来自 matrix.json armor 段)
|
|
132
|
-
const { plan } = await import("../lib/armor.mjs");
|
|
133
|
-
const strategy = plan(matrix, name); if (!strategy.error) {
|
|
134
|
-
if (strategy.requiresSeed) console.log(`[策略] ${name}:裸注入 sys 层实测 ${strategy.bare.sys}%,带人设词种子 ${strategy.persona.sys}% —— 需要 seed 层`);
|
|
135
|
-
const noSys = strategy.blocked.find((b) => b.layer === "sys");
|
|
136
|
-
if (noSys && mode !== "off") console.log(`[策略] ${name}:sys 层不可用(${noSys.reason}),本次只能靠 user 层`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 通道全废的家(闭源桌面 / 提示词在服务端)说清楚:这次只是原样启动,没注入任何东西
|
|
140
|
-
{
|
|
141
|
-
const c = capsFor(name);
|
|
142
|
-
if (!c.shim.ok && !c.install.ok && !c.slash.ok && mode !== "off") {
|
|
143
|
-
const why = [c.shim, c.install, c.slash].map((x) => x.why).filter(Boolean)[0];
|
|
144
|
-
console.log(`[提示] ${name} 注入不进去,本次是原样启动:${why}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const { createStore } = await import("../lib/personas.mjs");
|
|
149
|
-
const store = createStore(DATA);
|
|
150
|
-
const personaName = flag("--persona", "active");
|
|
151
|
-
let pickedName = personaName === "active" ? store.activeId() : personaName;
|
|
152
|
-
let picked = store.read(pickedName);
|
|
153
|
-
// 指针可能指向已删的 persona:回退到第一个,不让启动器直接死
|
|
154
|
-
if (!picked && personaName === "active") {
|
|
155
|
-
const first = store.list()[0];
|
|
156
|
-
if (first) {
|
|
157
|
-
console.warn(`[warn] active 指向不存在的 persona: ${pickedName ?? "(空)"},本次回退到 ${first.id}(agent-purge use ${first.id} 可写回指针)`);
|
|
158
|
-
pickedName = first.id;
|
|
159
|
-
picked = first;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (!picked) die(`没有 persona: ${pickedName}(可用: ${store.list().map((p) => p.id).join(", ")})`);
|
|
163
|
-
const personaPath = store.materialize(picked.id);
|
|
164
|
-
const personaText = picked.body.trim();
|
|
165
|
-
const cwd = resolve(flag("--cwd", process.cwd()));
|
|
166
|
-
|
|
167
|
-
// --ask:一次性问答,与 Web UI 共用 oneshots 规格;不给才启动交互式 harness
|
|
168
|
-
// 透传:有 -- 就只收 -- 后面的;没有就把“不是 agent-purge 自己的 flag”全交给 harness
|
|
169
|
-
// (这样裸命令 `pi -p "…"` / `codex exec "…"` 经 shim 进来也能把参数带到,--ask 也一样)
|
|
170
|
-
const dd = argv.indexOf("--");
|
|
171
|
-
let passthrough = [];
|
|
172
|
-
if (dd !== -1) {
|
|
173
|
-
passthrough = argv.slice(dd + 1);
|
|
174
|
-
} else {
|
|
175
|
-
const ownWithValue = new Set(["--ask", "--mode", "--persona", "--cwd", "--auto"]);
|
|
176
|
-
const tail = argv.slice(1);
|
|
177
|
-
for (let i = 0; i < tail.length; i++) {
|
|
178
|
-
const t = tail[i];
|
|
179
|
-
if (ownWithValue.has(t)) {
|
|
180
|
-
i++;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (t === "--dry-run" || ownWithValue.has(t.split("=")[0])) continue;
|
|
184
|
-
passthrough.push(t);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const askIdx = argv.indexOf("--ask");
|
|
189
|
-
let built;
|
|
190
|
-
if (askIdx !== -1) {
|
|
191
|
-
const raw = argv[askIdx + 1];
|
|
192
|
-
if (!raw || raw.startsWith("--")) die('--ask 需要一个 prompt,例如: agent-purge codex --ask "解释这段代码"');
|
|
193
|
-
const prompt = raw.trim();
|
|
194
|
-
const one = oneshots[name];
|
|
195
|
-
if (!one) die(`${name} 没有一次性运行模式(可用: ${Object.keys(oneshots).join(", ")})`);
|
|
196
|
-
built = one({ prompt, personaPath, personaText, mode, cwd });
|
|
197
|
-
if (passthrough.length) built.args = [...built.args, ...passthrough];
|
|
198
|
-
} else {
|
|
199
|
-
built = backends[name]?.({ personaPath, personaText, mode, args: passthrough, cwd, reason: spec.reason }) ?? {
|
|
200
|
-
unsupported: "该 harness 还没有后端实现",
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (built.manual) {
|
|
205
|
-
console.log(`${name}: 需要人工介入`);
|
|
206
|
-
built.manual.forEach((line) => console.log(" - " + line));
|
|
207
|
-
process.exit(0);
|
|
208
|
-
}
|
|
209
|
-
if (built.unsupported) die(`${name} 做不到:${built.unsupported}`);
|
|
210
|
-
|
|
211
|
-
if (built.note) console.log(built.note);
|
|
212
|
-
if (built.residueHint) console.log(`[residue] ${built.residueHint}`);
|
|
213
|
-
// off 有两种来源:全局总电源关了,或者这一轮手动指定。措辞不能混,否则看日志的人会以为总开关坏了
|
|
214
|
-
if (mode === "off") {
|
|
215
|
-
const globalOff = (state.default?.mode ?? "") === "off";
|
|
216
|
-
console.log(
|
|
217
|
-
globalOff
|
|
218
|
-
? "[off] 总电源关着(state.default.mode=off):本轮原样启动,不装扩展、不注入;要注入就先去 UI 把总开关拧开,或 --mode replace/strict/append"
|
|
219
|
-
: "[off] 本轮不注入(--mode off):原样启动;全局默认仍是 " + (state.default?.mode ?? "?") + ",想改默认用 agent-purge mode <模式>",
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
console.log(`[cmd] ${[built.cmd, ...built.args.map((a) => (a.includes(" ") ? JSON.stringify(a) : a))].join(" ")}`);
|
|
223
|
-
if (argv.includes("--dry-run")) process.exit(0);
|
|
224
|
-
|
|
225
|
-
// pi:注入活在扩展里(命令行装不下 10K 人设)。
|
|
226
|
-
// mode ≠ off:确保扩展在位(没有就装、有就重写成人设最新版),并用 AGENT_PURGE_FORCE=1 明确告诉扩展“这次是启动器要注入”。
|
|
227
|
-
// mode = off:不装、也不喊注入。扩展就算挂在盘上,也会因为 mode=off + 没 FORCE 而保持沉默(裸敲 pi 原样跑)。
|
|
228
|
-
if (name === "pi" && !built.unsupported && !built.manual) {
|
|
229
|
-
const { installSlash } = await import("../lib/slashes.mjs");
|
|
230
|
-
if (mode === "off") {
|
|
231
|
-
// 不装、不喊注入:扩展就算在盘上,也会因为 mode=off + 没 FORCE 而沉默
|
|
232
|
-
built.env = { ...built.env, AGENT_PURGE_MODE: "off" };
|
|
233
|
-
} else {
|
|
234
|
-
installSlash("pi", DATA, { personaId: picked.id, personaBody: personaText });
|
|
235
|
-
built.env = { ...built.env, AGENT_PURGE_PERSONA: picked.id, AGENT_PURGE_MODE: mode, AGENT_PURGE_FORCE: "1" };
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (!resolveBin(built.cmd)) {
|
|
240
|
-
die(`${name}: 找不到可执行文件 ${built.cmd}(PATH 里没有它;agent-purge doctor 可看本机探测结果)`);
|
|
241
|
-
}
|
|
242
|
-
built.setup?.();
|
|
243
|
-
// 一次性问答:stdin 直接给 NUL。否则 codex 这类会先“Reading additional input from stdin...”
|
|
244
|
-
// 并阻塞到 EOF(非 TTY 环境必现);stdin 关了它就直接跑。
|
|
245
|
-
const stdio = askIdx !== -1 ? ["ignore", "inherit", "inherit"] : "inherit";
|
|
246
|
-
const child = spawnBin(built.cmd, built.args, { cwd, stdio, env: { ...process.env, ...built.env } });
|
|
247
|
-
const finish = (code) => {
|
|
248
|
-
built.cleanup?.();
|
|
249
|
-
process.exit(code ?? 1);
|
|
250
|
-
};
|
|
251
|
-
child.on("error", (e) => {
|
|
252
|
-
console.error(`${name}: ${e.message}`);
|
|
253
|
-
finish(1);
|
|
254
|
-
});
|
|
255
|
-
child.on("close", (code) => finish(code));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── 选东西的实现 ───────────────────────────────────────────────────────────
|
|
259
|
-
|
|
260
|
-
async function useCmd() {
|
|
261
|
-
const { createStore } = await import("../lib/personas.mjs");
|
|
262
|
-
const store = createStore(DATA);
|
|
263
|
-
const arg = argv[1];
|
|
264
|
-
const { state, migrated } = loadState(DATA);
|
|
265
|
-
migrated.forEach((m) => console.log(`[migrate] ${m}`));
|
|
266
|
-
|
|
267
|
-
const id = arg && !arg.startsWith("-") ? arg : state.persona;
|
|
268
|
-
if (!id) {
|
|
269
|
-
console.log(`当前 persona: (未设置)\n可用: ${store.list().map((p) => p.id).join(", ")}`);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
if (!store.read(id)) die(`没有 persona: ${id}(可用: ${store.list().map((p) => p.id).join(", ")})`);
|
|
273
|
-
store.setActive(id); // 写 active.md 快照 + state.persona 指针
|
|
274
|
-
|
|
275
|
-
// user 层的 /purge 命令文件是烘焙进去的,换人设必须重渲染
|
|
276
|
-
const { refreshSlashes } = await import("../lib/slashes.mjs");
|
|
277
|
-
const refreshed = refreshSlashes(DATA, state.armed ? Object.entries(state.armed).filter(([, a]) => a.includes("slash")).map(([k]) => k) : [], {
|
|
278
|
-
personaId: id,
|
|
279
|
-
personaBody: store.read(id).body.trim(),
|
|
280
|
-
});
|
|
281
|
-
console.log(`persona ← ${id}(active.md 已同步)`);
|
|
282
|
-
refreshed.forEach((f) => console.log(`[slash] 重新写入 ${f}`));
|
|
283
|
-
if (Object.keys(state.armed ?? {}).length) {
|
|
284
|
-
console.log("已武装的 harness 下一轮/下次启动自动跟随;落盘型(install)若想立刻同步:agent-purge install <h>");
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function modeCmd() {
|
|
289
|
-
const want = argv[1];
|
|
290
|
-
const { state } = loadState(DATA);
|
|
291
|
-
const current = state.default?.mode ?? "replace";
|
|
292
|
-
if (!want) {
|
|
293
|
-
console.log(
|
|
294
|
-
[
|
|
295
|
-
`当前注入方式: ${current}`,
|
|
296
|
-
" strict 只发人设(宿主提示词全丢,最干净)",
|
|
297
|
-
" replace 人设在前 + 宿主提示词剔除身份句(默认,官方仍认得你)",
|
|
298
|
-
" append 人设只挂在最前,宿主原样保留(最保守)",
|
|
299
|
-
" off 不注入(受管 harness 也能干净地裸用)",
|
|
300
|
-
].join("\n"),
|
|
301
|
-
);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (!MODES.includes(want)) die(`mode 只能是 ${MODES.join(" / ")}`);
|
|
305
|
-
setDefault(DATA, { harness: state.default?.harness ?? "codex", mode: want });
|
|
306
|
-
console.log(`mode ← ${want}(state.json;下次启动生效,--mode 可临时覆盖)`);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async function installCmd(verb, rest) {
|
|
310
|
-
const id = rest[0];
|
|
311
|
-
const ids = id === "--all" || !id ? Object.keys(matrix.harnesses).filter((h) => matrix.harnesses[h].class !== "unsupported") : [id];
|
|
312
|
-
// 常驻只走 lib/persist.mjs 这一个控制面(caps / Web 开关 / status 都读它的 targets)
|
|
313
|
-
const { install, uninstall, targets } = await import("../lib/persist.mjs");
|
|
314
|
-
const { createStore } = await import("../lib/personas.mjs");
|
|
315
|
-
const store = createStore(DATA);
|
|
316
|
-
for (const h of ids) {
|
|
317
|
-
if (!matrix.harnesses[h]) die(`未知 harness: ${h}`);
|
|
318
|
-
if (!targets[h]) {
|
|
319
|
-
console.log(`${h}: 没有常驻模式(用 arm 走裸命令接管 / slash 走会话内)`);
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
let r;
|
|
323
|
-
if (verb === "install") {
|
|
324
|
-
const pid = store.activeId();
|
|
325
|
-
if (!pid) die("还没有 persona:先 agent-purge use <id>");
|
|
326
|
-
r = install(h, { root: DATA, active: store.refreshActive() }) ?? {};
|
|
327
|
-
} else {
|
|
328
|
-
// 卸载不能依赖 persona 还在:人设删了也必须能关
|
|
329
|
-
r = uninstall(h) ?? {};
|
|
330
|
-
}
|
|
331
|
-
const ok = !r.error;
|
|
332
|
-
console.log(`${ok ? "✓" : "✗"} ${verb} ${h}${r.file ? ` → ${r.file}` : ""}${r.note ? ` (${r.note})` : ""}${r.error ? ` ${r.error}` : ""}`);
|
|
333
|
-
if (ok) setArmed(DATA, h, "install", verb === "install");
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async function armCmd(verb, rest) {
|
|
338
|
-
const { installShim, uninstallShim, ensureUserPath } = await import("../lib/shim.mjs");
|
|
339
|
-
const { installSlash, uninstallSlash } = await import("../lib/slashes.mjs");
|
|
340
|
-
const { install, uninstall } = await import("../lib/persist.mjs");
|
|
341
|
-
const { capsFor } = await import("../lib/caps.mjs");
|
|
342
|
-
const { createStore } = await import("../lib/personas.mjs");
|
|
343
|
-
const store = createStore(DATA);
|
|
344
|
-
const pid = store.activeId();
|
|
345
|
-
if (!pid) die("还没有 persona:先 agent-purge use <id>");
|
|
346
|
-
|
|
347
|
-
const supported = Object.keys(matrix.harnesses).filter((h) => matrix.harnesses[h].class !== "unsupported");
|
|
348
|
-
const namedTarget = rest.filter((x) => !x.startsWith("-"));
|
|
349
|
-
const targets = rest.includes("--all") || !namedTarget.length ? supported : namedTarget;
|
|
350
|
-
|
|
351
|
-
// 全局 --all 就是总电源:撤的时候必须把 default.mode 置 off,
|
|
352
|
-
// 否则 `agent-purge <h>` 这条入口照样注入(实测:关了总开关还能接管,就是这里漏的)。
|
|
353
|
-
// 开的时候把上次的 mode 还回去。单家操作不动 mode。
|
|
354
|
-
if (rest.includes("--all")) {
|
|
355
|
-
const { state: cur } = loadState(DATA);
|
|
356
|
-
const m = cur.default?.mode ?? "off";
|
|
357
|
-
if (verb === "disarm" && m !== "off" && cur.default?.harness) {
|
|
358
|
-
setDefault(DATA, { harness: cur.default.harness, mode: "off" });
|
|
359
|
-
patchState(DATA, { prevMode: m });
|
|
360
|
-
}
|
|
361
|
-
if (verb === "arm" && m === "off" && cur.default?.harness) {
|
|
362
|
-
const back = MODES.includes(cur.prevMode) && cur.prevMode !== "off" ? cur.prevMode : "replace";
|
|
363
|
-
setDefault(DATA, { harness: cur.default.harness, mode: back });
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// 通道默认全开(常驻 + 裸命令接管 + /purge);只想做其中一个就显式点名
|
|
368
|
-
const named = ["--install", "--shim", "--slash"].filter((f) => rest.includes(f));
|
|
369
|
-
const channels = named.length ? named.map((f) => f.slice(2)) : ["install", "shim", "slash"];
|
|
370
|
-
let armedAny = 0;
|
|
371
|
-
|
|
372
|
-
for (const h of targets) {
|
|
373
|
-
if (!matrix.harnesses[h]) die(`未知 harness: ${h}`);
|
|
374
|
-
const done = [];
|
|
375
|
-
const skip = [];
|
|
376
|
-
|
|
377
|
-
for (const ch of channels) {
|
|
378
|
-
// 先过能力表:这家形态上做不到的通道(如 zcode 桌面 + shim),不写文件,直接说明原因
|
|
379
|
-
if (verb === "arm") {
|
|
380
|
-
const cap = capsFor(h)[ch];
|
|
381
|
-
if (cap && !cap.ok) {
|
|
382
|
-
skip.push(`${ch}: ${cap.why}`);
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
if (ch === "install") {
|
|
387
|
-
const r = verb === "arm" ? install(h, { root: DATA, active: store.refreshActive() }) : uninstall(h);
|
|
388
|
-
if (r.error) skip.push(`常驻: ${r.error}`);
|
|
389
|
-
else {
|
|
390
|
-
setArmed(DATA, h, "install", verb === "arm");
|
|
391
|
-
done.push(verb === "arm" ? `常驻 ${r.action}` : `撤常驻 ${r.action}`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
if (ch === "shim") {
|
|
395
|
-
const r = verb === "arm" ? installShim(DATA, h) : uninstallShim(DATA, h);
|
|
396
|
-
if (r.error) skip.push(`shim: ${r.error}`);
|
|
397
|
-
else {
|
|
398
|
-
setArmed(DATA, h, "shim", verb === "arm");
|
|
399
|
-
done.push(verb === "arm" ? "裸命令接管" : "撤裸命令接管");
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if (ch === "slash") {
|
|
403
|
-
const r =
|
|
404
|
-
verb === "arm"
|
|
405
|
-
? installSlash(h, DATA, { personaId: pid, personaBody: store.read(pid).body.trim() })
|
|
406
|
-
: uninstallSlash(h, DATA);
|
|
407
|
-
if (r.error) skip.push(`/purge: ${r.error}`);
|
|
408
|
-
else {
|
|
409
|
-
setArmed(DATA, h, "slash", verb === "arm");
|
|
410
|
-
done.push(verb === "arm" ? `/purge ${r.layer} 层` : "撤 /purge");
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
const icon = done.length ? "✓" : "·";
|
|
415
|
-
if (done.length) armedAny += 1;
|
|
416
|
-
console.log(`${icon} ${verb} ${h.padEnd(11)} ${done.join(" · ") || "无可用通道"}${skip.length ? " 跳过: " + skip.join(";") : ""}`);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (verb === "arm" && armedAny && channels.includes("shim")) {
|
|
420
|
-
const r = ensureUserPath();
|
|
421
|
-
console.log(r.changed ? `[path] ${r.dir} → 用户 PATH 最前(${r.how})` : `[path] ${r.dir} 就位(${r.how})`);
|
|
422
|
-
}
|
|
423
|
-
if (verb === "arm") {
|
|
424
|
-
if (!armedAny) {
|
|
425
|
-
console.log(`\n一个通道都没装上(${targets.join(", ")}):上面“跳过”里写了每家为什么不行。`);
|
|
426
|
-
} else {
|
|
427
|
-
const h0 = targets[0];
|
|
428
|
-
const lines = [];
|
|
429
|
-
if (channels.includes("shim")) lines.push(`新开终端敲 ${h0} → 走 PATH 前置壳,注入生效`);
|
|
430
|
-
if (channels.includes("slash")) {
|
|
431
|
-
lines.push(
|
|
432
|
-
h0 === "pi"
|
|
433
|
-
? "裸敲 pi 不注入(只装了热切);会话里打 /purge on 才改写,/purge status 看状态"
|
|
434
|
-
:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
console.log(`
|
|
510
|
-
console.log("");
|
|
511
|
-
console.log("
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const {
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (sub === "
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
r
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
--
|
|
624
|
-
--
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
agent-purge
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
agent-purge arm <h
|
|
710
|
-
agent-purge
|
|
711
|
-
agent-purge arm <h> --
|
|
712
|
-
agent-purge
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
agent-purge
|
|
718
|
-
agent-purge
|
|
719
|
-
agent-purge
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
+
const i = argv.indexOf(name);
|
|
32
|
+
return i === -1 ? fallback : argv[i + 1];
|
|
33
|
+
};
|
|
34
|
+
let cmd = argv[0];
|
|
35
|
+
|
|
36
|
+
if (!cmd || cmd.startsWith("-")) cmd = await defaultHarness();
|
|
37
|
+
|
|
38
|
+
// ── 选东西 ────────────────────────────────────────────────────────────────
|
|
39
|
+
switch (cmd) {
|
|
40
|
+
case "use":
|
|
41
|
+
await useCmd();
|
|
42
|
+
break;
|
|
43
|
+
case "mode":
|
|
44
|
+
modeCmd();
|
|
45
|
+
break;
|
|
46
|
+
case "install":
|
|
47
|
+
case "uninstall":
|
|
48
|
+
await installCmd(cmd, argv.slice(1));
|
|
49
|
+
break;
|
|
50
|
+
case "arm":
|
|
51
|
+
case "disarm":
|
|
52
|
+
case "register":
|
|
53
|
+
case "注册":
|
|
54
|
+
await armCmd(cmd === "arm" || cmd === "register" || cmd === "注册" ? "arm" : "disarm", argv.slice(1));
|
|
55
|
+
break;
|
|
56
|
+
case "slash":
|
|
57
|
+
await slashCmd(argv.slice(1));
|
|
58
|
+
break;
|
|
59
|
+
case "status":
|
|
60
|
+
statusCmd();
|
|
61
|
+
break;
|
|
62
|
+
case "ls":
|
|
63
|
+
await lsCmd();
|
|
64
|
+
break;
|
|
65
|
+
case "doctor":
|
|
66
|
+
case "detect": {
|
|
67
|
+
const { doctor } = await import("../lib/doctor.mjs");
|
|
68
|
+
await doctor(argv);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "matrix":
|
|
72
|
+
printMatrix();
|
|
73
|
+
break;
|
|
74
|
+
case "armor":
|
|
75
|
+
await armorCli(argv.slice(1));
|
|
76
|
+
break;
|
|
77
|
+
case "web":
|
|
78
|
+
webCmd();
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
await launch(cmd);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── 用东西 ────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/** 无参:state.default 说跑谁就跑谁;没有默认就跑到 Web 去选 */
|
|
87
|
+
async function defaultHarness() {
|
|
88
|
+
const { state } = loadState(DATA);
|
|
89
|
+
const harness = state.default?.harness;
|
|
90
|
+
if (!harness) {
|
|
91
|
+
console.log("还没有默认 harness。两种办法:\n agent-purge web 在浏览器里选,再点“设为默认执行器”\n agent-purge <harness> 直接点名跑(如 agent-purge codex)");
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
console.log(`[default] ${harness} · ${state.default.mode} · persona=${state.persona ?? "-"}(state.json)`);
|
|
95
|
+
return harness;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function launch(name) {
|
|
99
|
+
const spec = matrix.harnesses[name];
|
|
100
|
+
if (!spec) die(`未知 harness: ${name}(可用: ${Object.keys(matrix.harnesses).join(", ")};子命令见 --help)`);
|
|
101
|
+
if (spec.class === "unsupported") die(`${name} 做不到:${spec.reason}`);
|
|
102
|
+
|
|
103
|
+
const { state } = loadState(DATA);
|
|
104
|
+
// 由 state.json 决定注入方式;显式 --mode 永远压过它
|
|
105
|
+
const explicitMode = flag("--mode", null);
|
|
106
|
+
if (explicitMode !== null && !MODES.includes(explicitMode)) die(`--mode 只能是 ${MODES.join(" / ")}`);
|
|
107
|
+
const mode = explicitMode ?? state.default?.mode ?? "replace";
|
|
108
|
+
|
|
109
|
+
// 裸敲 pi 的持久位(写 state.json,扩展下一会话读它)
|
|
110
|
+
const autoFlag = name === "pi" ? flag("--auto", null) : null;
|
|
111
|
+
if (autoFlag !== null) {
|
|
112
|
+
if (autoFlag !== "on" && autoFlag !== "off") die("--auto 只能是 on / off");
|
|
113
|
+
const { setPiAuto } = await import("../lib/state.mjs");
|
|
114
|
+
setPiAuto(DATA, autoFlag === "on");
|
|
115
|
+
console.log(`[auto] 裸敲 pi ${autoFlag === "on" ? "会" : "不会"}自动注入(已写入 state.json)`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// pi 的人设落在哪一层:sys(默认,真系统提示词)或 user(塞进用户消息,给忽略 system 的模型)
|
|
119
|
+
const layerFlag = name === "pi" ? flag("--layer", null) : null;
|
|
120
|
+
if (layerFlag !== null) {
|
|
121
|
+
if (layerFlag !== "sys" && layerFlag !== "user") die("--layer 只能是 sys / user");
|
|
122
|
+
const { setPiUserLayer } = await import("../lib/state.mjs");
|
|
123
|
+
setPiUserLayer(DATA, layerFlag === "user");
|
|
124
|
+
console.log(
|
|
125
|
+
layerFlag === "user"
|
|
126
|
+
? "[layer] 人设额外塞进用户消息(给忽略 system 的模型用;代价是每条消息多一段)"
|
|
127
|
+
: "[layer] 人设走 system 层(默认)",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 实测策略:该 harness 的哪一层真的能注入(数据来自 matrix.json armor 段)
|
|
132
|
+
const { plan } = await import("../lib/armor.mjs");
|
|
133
|
+
const strategy = plan(matrix, name); if (!strategy.error) {
|
|
134
|
+
if (strategy.requiresSeed) console.log(`[策略] ${name}:裸注入 sys 层实测 ${strategy.bare.sys}%,带人设词种子 ${strategy.persona.sys}% —— 需要 seed 层`);
|
|
135
|
+
const noSys = strategy.blocked.find((b) => b.layer === "sys");
|
|
136
|
+
if (noSys && mode !== "off") console.log(`[策略] ${name}:sys 层不可用(${noSys.reason}),本次只能靠 user 层`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 通道全废的家(闭源桌面 / 提示词在服务端)说清楚:这次只是原样启动,没注入任何东西
|
|
140
|
+
{
|
|
141
|
+
const c = capsFor(name);
|
|
142
|
+
if (!c.shim.ok && !c.install.ok && !c.slash.ok && mode !== "off") {
|
|
143
|
+
const why = [c.shim, c.install, c.slash].map((x) => x.why).filter(Boolean)[0];
|
|
144
|
+
console.log(`[提示] ${name} 注入不进去,本次是原样启动:${why}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { createStore } = await import("../lib/personas.mjs");
|
|
149
|
+
const store = createStore(DATA);
|
|
150
|
+
const personaName = flag("--persona", "active");
|
|
151
|
+
let pickedName = personaName === "active" ? store.activeId() : personaName;
|
|
152
|
+
let picked = store.read(pickedName);
|
|
153
|
+
// 指针可能指向已删的 persona:回退到第一个,不让启动器直接死
|
|
154
|
+
if (!picked && personaName === "active") {
|
|
155
|
+
const first = store.list()[0];
|
|
156
|
+
if (first) {
|
|
157
|
+
console.warn(`[warn] active 指向不存在的 persona: ${pickedName ?? "(空)"},本次回退到 ${first.id}(agent-purge use ${first.id} 可写回指针)`);
|
|
158
|
+
pickedName = first.id;
|
|
159
|
+
picked = first;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!picked) die(`没有 persona: ${pickedName}(可用: ${store.list().map((p) => p.id).join(", ")})`);
|
|
163
|
+
const personaPath = store.materialize(picked.id);
|
|
164
|
+
const personaText = picked.body.trim();
|
|
165
|
+
const cwd = resolve(flag("--cwd", process.cwd()));
|
|
166
|
+
|
|
167
|
+
// --ask:一次性问答,与 Web UI 共用 oneshots 规格;不给才启动交互式 harness
|
|
168
|
+
// 透传:有 -- 就只收 -- 后面的;没有就把“不是 agent-purge 自己的 flag”全交给 harness
|
|
169
|
+
// (这样裸命令 `pi -p "…"` / `codex exec "…"` 经 shim 进来也能把参数带到,--ask 也一样)
|
|
170
|
+
const dd = argv.indexOf("--");
|
|
171
|
+
let passthrough = [];
|
|
172
|
+
if (dd !== -1) {
|
|
173
|
+
passthrough = argv.slice(dd + 1);
|
|
174
|
+
} else {
|
|
175
|
+
const ownWithValue = new Set(["--ask", "--mode", "--persona", "--cwd", "--auto"]);
|
|
176
|
+
const tail = argv.slice(1);
|
|
177
|
+
for (let i = 0; i < tail.length; i++) {
|
|
178
|
+
const t = tail[i];
|
|
179
|
+
if (ownWithValue.has(t)) {
|
|
180
|
+
i++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (t === "--dry-run" || ownWithValue.has(t.split("=")[0])) continue;
|
|
184
|
+
passthrough.push(t);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const askIdx = argv.indexOf("--ask");
|
|
189
|
+
let built;
|
|
190
|
+
if (askIdx !== -1) {
|
|
191
|
+
const raw = argv[askIdx + 1];
|
|
192
|
+
if (!raw || raw.startsWith("--")) die('--ask 需要一个 prompt,例如: agent-purge codex --ask "解释这段代码"');
|
|
193
|
+
const prompt = raw.trim();
|
|
194
|
+
const one = oneshots[name];
|
|
195
|
+
if (!one) die(`${name} 没有一次性运行模式(可用: ${Object.keys(oneshots).join(", ")})`);
|
|
196
|
+
built = one({ prompt, personaPath, personaText, mode, cwd });
|
|
197
|
+
if (passthrough.length) built.args = [...built.args, ...passthrough];
|
|
198
|
+
} else {
|
|
199
|
+
built = backends[name]?.({ personaPath, personaText, mode, args: passthrough, cwd, reason: spec.reason }) ?? {
|
|
200
|
+
unsupported: "该 harness 还没有后端实现",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (built.manual) {
|
|
205
|
+
console.log(`${name}: 需要人工介入`);
|
|
206
|
+
built.manual.forEach((line) => console.log(" - " + line));
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
if (built.unsupported) die(`${name} 做不到:${built.unsupported}`);
|
|
210
|
+
|
|
211
|
+
if (built.note) console.log(built.note);
|
|
212
|
+
if (built.residueHint) console.log(`[residue] ${built.residueHint}`);
|
|
213
|
+
// off 有两种来源:全局总电源关了,或者这一轮手动指定。措辞不能混,否则看日志的人会以为总开关坏了
|
|
214
|
+
if (mode === "off") {
|
|
215
|
+
const globalOff = (state.default?.mode ?? "") === "off";
|
|
216
|
+
console.log(
|
|
217
|
+
globalOff
|
|
218
|
+
? "[off] 总电源关着(state.default.mode=off):本轮原样启动,不装扩展、不注入;要注入就先去 UI 把总开关拧开,或 --mode replace/strict/append"
|
|
219
|
+
: "[off] 本轮不注入(--mode off):原样启动;全局默认仍是 " + (state.default?.mode ?? "?") + ",想改默认用 agent-purge mode <模式>",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
console.log(`[cmd] ${[built.cmd, ...built.args.map((a) => (a.includes(" ") ? JSON.stringify(a) : a))].join(" ")}`);
|
|
223
|
+
if (argv.includes("--dry-run")) process.exit(0);
|
|
224
|
+
|
|
225
|
+
// pi:注入活在扩展里(命令行装不下 10K 人设)。
|
|
226
|
+
// mode ≠ off:确保扩展在位(没有就装、有就重写成人设最新版),并用 AGENT_PURGE_FORCE=1 明确告诉扩展“这次是启动器要注入”。
|
|
227
|
+
// mode = off:不装、也不喊注入。扩展就算挂在盘上,也会因为 mode=off + 没 FORCE 而保持沉默(裸敲 pi 原样跑)。
|
|
228
|
+
if (name === "pi" && !built.unsupported && !built.manual) {
|
|
229
|
+
const { installSlash } = await import("../lib/slashes.mjs");
|
|
230
|
+
if (mode === "off") {
|
|
231
|
+
// 不装、不喊注入:扩展就算在盘上,也会因为 mode=off + 没 FORCE 而沉默
|
|
232
|
+
built.env = { ...built.env, AGENT_PURGE_MODE: "off" };
|
|
233
|
+
} else {
|
|
234
|
+
installSlash("pi", DATA, { personaId: picked.id, personaBody: personaText });
|
|
235
|
+
built.env = { ...built.env, AGENT_PURGE_PERSONA: picked.id, AGENT_PURGE_MODE: mode, AGENT_PURGE_FORCE: "1" };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!resolveBin(built.cmd)) {
|
|
240
|
+
die(`${name}: 找不到可执行文件 ${built.cmd}(PATH 里没有它;agent-purge doctor 可看本机探测结果)`);
|
|
241
|
+
}
|
|
242
|
+
built.setup?.();
|
|
243
|
+
// 一次性问答:stdin 直接给 NUL。否则 codex 这类会先“Reading additional input from stdin...”
|
|
244
|
+
// 并阻塞到 EOF(非 TTY 环境必现);stdin 关了它就直接跑。
|
|
245
|
+
const stdio = askIdx !== -1 ? ["ignore", "inherit", "inherit"] : "inherit";
|
|
246
|
+
const child = spawnBin(built.cmd, built.args, { cwd, stdio, env: { ...process.env, ...built.env } });
|
|
247
|
+
const finish = (code) => {
|
|
248
|
+
built.cleanup?.();
|
|
249
|
+
process.exit(code ?? 1);
|
|
250
|
+
};
|
|
251
|
+
child.on("error", (e) => {
|
|
252
|
+
console.error(`${name}: ${e.message}`);
|
|
253
|
+
finish(1);
|
|
254
|
+
});
|
|
255
|
+
child.on("close", (code) => finish(code));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── 选东西的实现 ───────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
async function useCmd() {
|
|
261
|
+
const { createStore } = await import("../lib/personas.mjs");
|
|
262
|
+
const store = createStore(DATA);
|
|
263
|
+
const arg = argv[1];
|
|
264
|
+
const { state, migrated } = loadState(DATA);
|
|
265
|
+
migrated.forEach((m) => console.log(`[migrate] ${m}`));
|
|
266
|
+
|
|
267
|
+
const id = arg && !arg.startsWith("-") ? arg : state.persona;
|
|
268
|
+
if (!id) {
|
|
269
|
+
console.log(`当前 persona: (未设置)\n可用: ${store.list().map((p) => p.id).join(", ")}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (!store.read(id)) die(`没有 persona: ${id}(可用: ${store.list().map((p) => p.id).join(", ")})`);
|
|
273
|
+
store.setActive(id); // 写 active.md 快照 + state.persona 指针
|
|
274
|
+
|
|
275
|
+
// user 层的 /purge 命令文件是烘焙进去的,换人设必须重渲染
|
|
276
|
+
const { refreshSlashes } = await import("../lib/slashes.mjs");
|
|
277
|
+
const refreshed = refreshSlashes(DATA, state.armed ? Object.entries(state.armed).filter(([, a]) => a.includes("slash")).map(([k]) => k) : [], {
|
|
278
|
+
personaId: id,
|
|
279
|
+
personaBody: store.read(id).body.trim(),
|
|
280
|
+
});
|
|
281
|
+
console.log(`persona ← ${id}(active.md 已同步)`);
|
|
282
|
+
refreshed.forEach((f) => console.log(`[slash] 重新写入 ${f}`));
|
|
283
|
+
if (Object.keys(state.armed ?? {}).length) {
|
|
284
|
+
console.log("已武装的 harness 下一轮/下次启动自动跟随;落盘型(install)若想立刻同步:agent-purge install <h>");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function modeCmd() {
|
|
289
|
+
const want = argv[1];
|
|
290
|
+
const { state } = loadState(DATA);
|
|
291
|
+
const current = state.default?.mode ?? "replace";
|
|
292
|
+
if (!want) {
|
|
293
|
+
console.log(
|
|
294
|
+
[
|
|
295
|
+
`当前注入方式: ${current}`,
|
|
296
|
+
" strict 只发人设(宿主提示词全丢,最干净)",
|
|
297
|
+
" replace 人设在前 + 宿主提示词剔除身份句(默认,官方仍认得你)",
|
|
298
|
+
" append 人设只挂在最前,宿主原样保留(最保守)",
|
|
299
|
+
" off 不注入(受管 harness 也能干净地裸用)",
|
|
300
|
+
].join("\n"),
|
|
301
|
+
);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!MODES.includes(want)) die(`mode 只能是 ${MODES.join(" / ")}`);
|
|
305
|
+
setDefault(DATA, { harness: state.default?.harness ?? "codex", mode: want });
|
|
306
|
+
console.log(`mode ← ${want}(state.json;下次启动生效,--mode 可临时覆盖)`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function installCmd(verb, rest) {
|
|
310
|
+
const id = rest[0];
|
|
311
|
+
const ids = id === "--all" || !id ? Object.keys(matrix.harnesses).filter((h) => matrix.harnesses[h].class !== "unsupported") : [id];
|
|
312
|
+
// 常驻只走 lib/persist.mjs 这一个控制面(caps / Web 开关 / status 都读它的 targets)
|
|
313
|
+
const { install, uninstall, targets } = await import("../lib/persist.mjs");
|
|
314
|
+
const { createStore } = await import("../lib/personas.mjs");
|
|
315
|
+
const store = createStore(DATA);
|
|
316
|
+
for (const h of ids) {
|
|
317
|
+
if (!matrix.harnesses[h]) die(`未知 harness: ${h}`);
|
|
318
|
+
if (!targets[h]) {
|
|
319
|
+
console.log(`${h}: 没有常驻模式(用 arm 走裸命令接管 / slash 走会话内)`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
let r;
|
|
323
|
+
if (verb === "install") {
|
|
324
|
+
const pid = store.activeId();
|
|
325
|
+
if (!pid) die("还没有 persona:先 agent-purge use <id>");
|
|
326
|
+
r = install(h, { root: DATA, active: store.refreshActive() }) ?? {};
|
|
327
|
+
} else {
|
|
328
|
+
// 卸载不能依赖 persona 还在:人设删了也必须能关
|
|
329
|
+
r = uninstall(h) ?? {};
|
|
330
|
+
}
|
|
331
|
+
const ok = !r.error;
|
|
332
|
+
console.log(`${ok ? "✓" : "✗"} ${verb} ${h}${r.file ? ` → ${r.file}` : ""}${r.note ? ` (${r.note})` : ""}${r.error ? ` ${r.error}` : ""}`);
|
|
333
|
+
if (ok) setArmed(DATA, h, "install", verb === "install");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function armCmd(verb, rest) {
|
|
338
|
+
const { installShim, uninstallShim, ensureUserPath } = await import("../lib/shim.mjs");
|
|
339
|
+
const { installSlash, uninstallSlash } = await import("../lib/slashes.mjs");
|
|
340
|
+
const { install, uninstall } = await import("../lib/persist.mjs");
|
|
341
|
+
const { capsFor } = await import("../lib/caps.mjs");
|
|
342
|
+
const { createStore } = await import("../lib/personas.mjs");
|
|
343
|
+
const store = createStore(DATA);
|
|
344
|
+
const pid = store.activeId();
|
|
345
|
+
if (!pid) die("还没有 persona:先 agent-purge use <id>");
|
|
346
|
+
|
|
347
|
+
const supported = Object.keys(matrix.harnesses).filter((h) => matrix.harnesses[h].class !== "unsupported");
|
|
348
|
+
const namedTarget = rest.filter((x) => !x.startsWith("-"));
|
|
349
|
+
const targets = rest.includes("--all") || !namedTarget.length ? supported : namedTarget;
|
|
350
|
+
|
|
351
|
+
// 全局 --all 就是总电源:撤的时候必须把 default.mode 置 off,
|
|
352
|
+
// 否则 `agent-purge <h>` 这条入口照样注入(实测:关了总开关还能接管,就是这里漏的)。
|
|
353
|
+
// 开的时候把上次的 mode 还回去。单家操作不动 mode。
|
|
354
|
+
if (rest.includes("--all")) {
|
|
355
|
+
const { state: cur } = loadState(DATA);
|
|
356
|
+
const m = cur.default?.mode ?? "off";
|
|
357
|
+
if (verb === "disarm" && m !== "off" && cur.default?.harness) {
|
|
358
|
+
setDefault(DATA, { harness: cur.default.harness, mode: "off" });
|
|
359
|
+
patchState(DATA, { prevMode: m });
|
|
360
|
+
}
|
|
361
|
+
if (verb === "arm" && m === "off" && cur.default?.harness) {
|
|
362
|
+
const back = MODES.includes(cur.prevMode) && cur.prevMode !== "off" ? cur.prevMode : "replace";
|
|
363
|
+
setDefault(DATA, { harness: cur.default.harness, mode: back });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 通道默认全开(常驻 + 裸命令接管 + /purge);只想做其中一个就显式点名
|
|
368
|
+
const named = ["--install", "--shim", "--slash"].filter((f) => rest.includes(f));
|
|
369
|
+
const channels = named.length ? named.map((f) => f.slice(2)) : ["install", "shim", "slash"];
|
|
370
|
+
let armedAny = 0;
|
|
371
|
+
|
|
372
|
+
for (const h of targets) {
|
|
373
|
+
if (!matrix.harnesses[h]) die(`未知 harness: ${h}`);
|
|
374
|
+
const done = [];
|
|
375
|
+
const skip = [];
|
|
376
|
+
|
|
377
|
+
for (const ch of channels) {
|
|
378
|
+
// 先过能力表:这家形态上做不到的通道(如 zcode 桌面 + shim),不写文件,直接说明原因
|
|
379
|
+
if (verb === "arm") {
|
|
380
|
+
const cap = capsFor(h)[ch];
|
|
381
|
+
if (cap && !cap.ok) {
|
|
382
|
+
skip.push(`${ch}: ${cap.why}`);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (ch === "install") {
|
|
387
|
+
const r = verb === "arm" ? install(h, { root: DATA, active: store.refreshActive() }) : uninstall(h);
|
|
388
|
+
if (r.error) skip.push(`常驻: ${r.error}`);
|
|
389
|
+
else {
|
|
390
|
+
setArmed(DATA, h, "install", verb === "arm");
|
|
391
|
+
done.push(verb === "arm" ? `常驻 ${r.action}` : `撤常驻 ${r.action}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (ch === "shim") {
|
|
395
|
+
const r = verb === "arm" ? installShim(DATA, h) : uninstallShim(DATA, h);
|
|
396
|
+
if (r.error) skip.push(`shim: ${r.error}`);
|
|
397
|
+
else {
|
|
398
|
+
setArmed(DATA, h, "shim", verb === "arm");
|
|
399
|
+
done.push(verb === "arm" ? "裸命令接管" : "撤裸命令接管");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (ch === "slash") {
|
|
403
|
+
const r =
|
|
404
|
+
verb === "arm"
|
|
405
|
+
? installSlash(h, DATA, { personaId: pid, personaBody: store.read(pid).body.trim() })
|
|
406
|
+
: uninstallSlash(h, DATA);
|
|
407
|
+
if (r.error) skip.push(`/purge: ${r.error}`);
|
|
408
|
+
else {
|
|
409
|
+
setArmed(DATA, h, "slash", verb === "arm");
|
|
410
|
+
done.push(verb === "arm" ? `/purge ${r.layer} 层` : "撤 /purge");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const icon = done.length ? "✓" : "·";
|
|
415
|
+
if (done.length) armedAny += 1;
|
|
416
|
+
console.log(`${icon} ${verb} ${h.padEnd(11)} ${done.join(" · ") || "无可用通道"}${skip.length ? " 跳过: " + skip.join(";") : ""}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (verb === "arm" && armedAny && channels.includes("shim")) {
|
|
420
|
+
const r = ensureUserPath();
|
|
421
|
+
console.log(r.changed ? `[path] ${r.dir} → 用户 PATH 最前(${r.how})` : `[path] ${r.dir} 就位(${r.how})`);
|
|
422
|
+
}
|
|
423
|
+
if (verb === "arm") {
|
|
424
|
+
if (!armedAny) {
|
|
425
|
+
console.log(`\n一个通道都没装上(${targets.join(", ")}):上面“跳过”里写了每家为什么不行。`);
|
|
426
|
+
} else {
|
|
427
|
+
const h0 = targets[0];
|
|
428
|
+
const lines = [];
|
|
429
|
+
if (channels.includes("shim")) lines.push(`新开终端敲 ${h0} → 走 PATH 前置壳,注入生效`);
|
|
430
|
+
if (channels.includes("slash")) {
|
|
431
|
+
lines.push(
|
|
432
|
+
h0 === "pi"
|
|
433
|
+
? "裸敲 pi 不注入(只装了热切);会话里打 /purge on 才改写,/purge status 看状态"
|
|
434
|
+
: h0 === "dsh"
|
|
435
|
+
? "dsh 会话里打 /purge 切开关(改 state.json,不产生模型消息);三形态共用一份状态"
|
|
436
|
+
: "会话里打 /purge 才注入(它展开成用户消息)",
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
if (channels.includes("install")) lines.push("改了配置文件:连别人启动的、GUI 里点开的也生效(那家重启后生效)");
|
|
440
|
+
console.log("\n下一步:\n" + lines.map((l) => " - " + l).join("\n") + "\n复核:agent-purge status");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function slashCmd(rest) {
|
|
446
|
+
const { installSlash, uninstallSlash, slashStatus } = await import("../lib/slashes.mjs");
|
|
447
|
+
const { createStore } = await import("../lib/personas.mjs");
|
|
448
|
+
const store = createStore(DATA);
|
|
449
|
+
const id = rest[0];
|
|
450
|
+
if (!id || id === "status") {
|
|
451
|
+
const rows = slashStatus(DATA);
|
|
452
|
+
const live = rows.filter((s) => s.offered);
|
|
453
|
+
live.forEach((s) => {
|
|
454
|
+
console.log(`${s.installed ? "✓" : " "} ${s.id.padEnd(9)} ${s.layer.padEnd(4)} ${s.trigger.padEnd(22)} persona=${s.personaId ?? "-"}`);
|
|
455
|
+
console.log(` ${s.note}`);
|
|
456
|
+
});
|
|
457
|
+
// 只列“能把内容写进 system 数组”的;user 层不要再诱人点
|
|
458
|
+
const dead = rows.filter((s) => !s.offered);
|
|
459
|
+
if (dead.length) {
|
|
460
|
+
const still = dead.filter((s) => s.installed).map((s) => s.id);
|
|
461
|
+
console.log(`\n已下线(user 层,压不住系统提示词):${dead.map((s) => s.id).join(", ")}`);
|
|
462
|
+
if (still.length) console.log(`旧文件还在盘上:agent-purge slash off ${still.join(" ")}`);
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (id === "off") {
|
|
467
|
+
const target = rest[1];
|
|
468
|
+
const ids =
|
|
469
|
+
target && target !== "--all"
|
|
470
|
+
? rest.slice(1).filter((x) => !x.startsWith("-"))
|
|
471
|
+
: slashStatus(DATA).filter((s) => s.installed).map((s) => s.id);
|
|
472
|
+
if (!ids.length) console.log("没有装着的 /purge");
|
|
473
|
+
ids.forEach((h) => {
|
|
474
|
+
const r = uninstallSlash(h, DATA);
|
|
475
|
+
if (!r.error) {
|
|
476
|
+
setArmed(DATA, h, "slash", false);
|
|
477
|
+
console.log(`✓ 已移除 /purge ${h}${r.removed?.length ? " → " + r.removed.join(", ") : "(本来就没有)"}`);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const targets = id === "--all" ? slashStatus(DATA).filter((s) => s.offered).map((s) => s.id) : [id];
|
|
483
|
+
const pid = store.activeId();
|
|
484
|
+
if (!pid) die("还没有 persona:先 agent-purge use <id>");
|
|
485
|
+
for (const h of targets) {
|
|
486
|
+
const r = installSlash(h, DATA, { personaId: pid, personaBody: store.read(pid).body.trim() });
|
|
487
|
+
if (r.error) {
|
|
488
|
+
console.log(`- ${h}: ${r.error}`);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
setArmed(DATA, h, "slash", true);
|
|
492
|
+
console.log(`✓ /purge → ${h}(${r.layer} 层)${r.file}`);
|
|
493
|
+
console.log(` ${r.note}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function statusCmd() {
|
|
498
|
+
const { migrated } = loadState(DATA);
|
|
499
|
+
migrated.forEach((m) => console.log(`[migrate] ${m}`));
|
|
500
|
+
// armed 是缓存,磁盘才是真相:先把假记录摘掉,再报数
|
|
501
|
+
const { reconcileArmed } = await import("../lib/armed.mjs");
|
|
502
|
+
const fixed = await reconcileArmed(DATA);
|
|
503
|
+
if (fixed.length) console.log(`[对账] armed 纠正:${fixed.join(", ")}`);
|
|
504
|
+
const { state } = loadState(DATA);
|
|
505
|
+
return Promise.all([import("../lib/shim.mjs"), import("../lib/slashes.mjs")]).then(([shimMod, slashMod]) => {
|
|
506
|
+
const shims = shimMod.shimStatus();
|
|
507
|
+
const slashes = slashMod.slashStatus(DATA);
|
|
508
|
+
|
|
509
|
+
console.log(`persona ${state.persona ?? "(未设置)"}`);
|
|
510
|
+
console.log(`default ${state.default ? `${state.default.harness} · ${state.default.mode}` : "(未设置)"} 裸敲 agent-purge 用它`);
|
|
511
|
+
console.log(`state ${join(DATA, "state.json")}`);
|
|
512
|
+
console.log("");
|
|
513
|
+
console.log("harness shim slash install /purge层 常驻落点");
|
|
514
|
+
for (const [h] of Object.entries(matrix.harnesses)) {
|
|
515
|
+
const armed = armedOf(state, h);
|
|
516
|
+
const sh = shims.find((s) => s.id === h);
|
|
517
|
+
const sl = slashes.find((s) => s.id === h);
|
|
518
|
+
const cap = capsFor(h, { bin: sh?.target ?? null });
|
|
519
|
+
const mark = (on) => (on ? " ✓ " : " · ");
|
|
520
|
+
const slashCol = sl && sl.offered ? (sl.layer === "sys" ? "sys(热)" : "user") : "—";
|
|
521
|
+
// install 列不写“注册没注册”,写“能不能装”:装了是 ✓,能装没装是「可用」,没落点才是 —
|
|
522
|
+
const installCol = armed.includes("install") ? " ✓ " : cap.install.ok ? "可用" : " — ";
|
|
523
|
+
const landing = cap.install.ok ? `${cap.install.layer || "?"}${cap.install.verified === false ? "(未实测)" : ""}` : (cap.install.why || "");
|
|
524
|
+
console.log(
|
|
525
|
+
`${h.padEnd(11)}${mark(sh?.installed).padEnd(7)}${mark(sl?.installed).padEnd(7)}${installCol.padEnd(10)}${slashCol.padEnd(10)}${landing}`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const dir = shimMod.shimDir();
|
|
529
|
+
const ps = shimMod.pathState();
|
|
530
|
+
const where = !ps.present ? "不在 PATH" : ps.first ? "已在 PATH 最前" : "在 PATH 但不最前(真身可能先被命中)";
|
|
531
|
+
console.log(`\nshim 目录 ${dir}(${where})`);
|
|
532
|
+
if (!ps.first) console.log(`修好它:agent-purge arm <h> --shim(会自动修正 PATH)`);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function lsCmd() {
|
|
537
|
+
const { createStore } = await import("../lib/personas.mjs");
|
|
538
|
+
const store = createStore(DATA);
|
|
539
|
+
const { state } = loadState(DATA);
|
|
540
|
+
const fit = store.resolve({ harness: state.default?.harness, model: null });
|
|
541
|
+
for (const p of store.list()) {
|
|
542
|
+
const mark = p.id === store.activeId() ? "*" : " ";
|
|
543
|
+
const best = fit?.persona?.id === p.id && fit?.matched ? " ← 最匹配当前 harness" : "";
|
|
544
|
+
const desc = p.meta?.description ?? p.meta?.desc ?? "";
|
|
545
|
+
console.log(`${mark} ${p.id.padEnd(14)} ${String(p.meta?.name ?? "").padEnd(12)} ${desc.slice(0, 44)}${best}`);
|
|
546
|
+
}
|
|
547
|
+
console.log(`\n* = active(agent-purge use <id> 切换)`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function webCmd() {
|
|
551
|
+
const port = Number(flag("--port", "7788"));
|
|
552
|
+
// --detach:脱离当前终端后台存活,日志丢弃
|
|
553
|
+
if (argv.includes("--detach")) {
|
|
554
|
+
const child = spawn(process.execPath, [join(ROOT, "web", "server.mjs"), "--serve", String(port)], {
|
|
555
|
+
detached: true,
|
|
556
|
+
stdio: "ignore",
|
|
557
|
+
shell: false,
|
|
558
|
+
env: { ...process.env, AGENT_PURGE_HOME: DATA },
|
|
559
|
+
});
|
|
560
|
+
child.unref();
|
|
561
|
+
console.log(`agent-purge web 已在后台启动 → http://127.0.0.1:${port}(pid ${child.pid})`);
|
|
562
|
+
process.exit(0);
|
|
563
|
+
}
|
|
564
|
+
return import("../web/server.mjs").then(({ serveWeb }) => serveWeb(port));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ── 其余 ──────────────────────────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
async function armorCli(rest) {
|
|
570
|
+
const { renderMatrix, renderPlan, plans, plan, preflight, runBench, runJudge } = await import("../lib/armor.mjs");
|
|
571
|
+
const color = !argv.includes("--no-color") && Boolean(process.stdout.isTTY);
|
|
572
|
+
const sub = rest[0]?.startsWith("-") ? "matrix" : rest[0] ?? "matrix";
|
|
573
|
+
const opts = (name, fallback) => {
|
|
574
|
+
const i = rest.indexOf(name);
|
|
575
|
+
return i === -1 ? fallback : rest[i + 1];
|
|
576
|
+
};
|
|
577
|
+
const flags = rest.filter((x) => x.startsWith("--"));
|
|
578
|
+
// 位置参数 ≠ 非 - 开头的 token:`--base https://x` 的值不是 harness 名
|
|
579
|
+
const VALUE_FLAGS = new Set(["--base", "--key", "--model", "--task", "--layers", "--only", "--repeat", "--concurrency", "--timeout", "--out", "--dir", "--probe"]);
|
|
580
|
+
const positionals = (from = 1) => {
|
|
581
|
+
const out = [];
|
|
582
|
+
for (let i = from; i < rest.length; i++) {
|
|
583
|
+
const a = rest[i];
|
|
584
|
+
if (a.startsWith("--")) {
|
|
585
|
+
if (VALUE_FLAGS.has(a)) i++;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (!a.startsWith("-")) out.push(a);
|
|
589
|
+
}
|
|
590
|
+
return out;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
if (sub === "matrix") return renderMatrix(matrix, { color });
|
|
594
|
+
|
|
595
|
+
if (sub === "plan") {
|
|
596
|
+
const ids = positionals();
|
|
597
|
+
const list = ids.length ? ids.map((id) => plan(matrix, id)) : plans(matrix);
|
|
598
|
+
list.forEach((p, i) => {
|
|
599
|
+
if (i) console.log();
|
|
600
|
+
renderPlan(p, { color });
|
|
601
|
+
});
|
|
602
|
+
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(",")}`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (sub === "preflight" || sub === "check") {
|
|
607
|
+
const ids = positionals();
|
|
608
|
+
const env = { base: opts("--base", process.env.MG_BASE), key: opts("--key", process.env.MG_KEY), model: opts("--model", process.env.MG_MODEL) };
|
|
609
|
+
let bad = 0;
|
|
610
|
+
for (const id of ids.length ? ids : ["pi"]) {
|
|
611
|
+
const r = preflight(matrix, id, env);
|
|
612
|
+
console.log(`${r.ok ? "✓" : "✗"} ${id}`);
|
|
613
|
+
r.checks.forEach((c) => console.log(` ${c.ok ? "ok " : "MISS"} ${c.name}${c.detail ? ` — ${c.detail}` : ""}`));
|
|
614
|
+
if (!r.ok) bad++;
|
|
615
|
+
}
|
|
616
|
+
if (bad) process.exitCode = 1;
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (sub === "run") {
|
|
621
|
+
if (flags.includes("--help")) {
|
|
622
|
+
console.log(`agent-purge armor run --task crack-bare --layers off,sys,user --only pi,codex --repeat 2 --concurrency 3
|
|
623
|
+
--base <url> --key <key> --model <id> 上游(默认读 MG_BASE / MG_KEY / MG_MODEL)
|
|
624
|
+
--task crack-bare|crack-persona 问法(裸问法 / 带人设词种子)
|
|
625
|
+
--layers off,sys,user|off,sys
|
|
626
|
+
--out <dir>`);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const env = {
|
|
630
|
+
MG_BASE: opts("--base", process.env.MG_BASE),
|
|
631
|
+
MG_KEY: opts("--key", process.env.MG_KEY),
|
|
632
|
+
MG_MODEL: opts("--model", process.env.MG_MODEL),
|
|
633
|
+
};
|
|
634
|
+
if (!env.MG_BASE || !env.MG_KEY) {
|
|
635
|
+
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");
|
|
636
|
+
process.exit(2);
|
|
637
|
+
}
|
|
638
|
+
const pass = [];
|
|
639
|
+
for (let i = 0; i < rest.length; i++) {
|
|
640
|
+
const a = rest[i];
|
|
641
|
+
if (["--base", "--key", "--model"].includes(a)) { i++; continue; }
|
|
642
|
+
pass.push(a);
|
|
643
|
+
}
|
|
644
|
+
const { code } = runBench(pass, env);
|
|
645
|
+
process.exitCode = code;
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (sub === "judge") {
|
|
650
|
+
const pass = [];
|
|
651
|
+
for (let i = 0; i < rest.length; i++) {
|
|
652
|
+
const a = rest[i];
|
|
653
|
+
if (["--base", "--key", "--model"].includes(a)) { i++; continue; }
|
|
654
|
+
pass.push(a);
|
|
655
|
+
}
|
|
656
|
+
const { code } = runJudge(pass, {
|
|
657
|
+
MG_BASE: opts("--base", process.env.MG_BASE),
|
|
658
|
+
MG_KEY: opts("--key", process.env.MG_KEY),
|
|
659
|
+
MG_MODEL: opts("--model", process.env.MG_MODEL),
|
|
660
|
+
});
|
|
661
|
+
process.exitCode = code;
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
console.error(`未知子命令: armor ${sub}(可用: matrix | plan | preflight | run | judge)`);
|
|
666
|
+
process.exit(2);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function printMatrix() {
|
|
670
|
+
const rows = Object.entries(matrix.harnesses).map(([name, s]) => ({
|
|
671
|
+
harness: name,
|
|
672
|
+
class: s.class,
|
|
673
|
+
override: s.override === true ? "yes" : s.override ? String(s.override) : "no",
|
|
674
|
+
inject: s.inject ? "yes" : "no",
|
|
675
|
+
hot: s.hotSwitch ? "yes" : "no",
|
|
676
|
+
chars: s.defaultSystemChars != null ? `${s.defaultSystemChars} → ${s.purgedSystemChars ?? "?"}` : "—",
|
|
677
|
+
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 ? "" : "%"}` : "未测",
|
|
678
|
+
version: s.measuredVersion ?? "—",
|
|
679
|
+
}));
|
|
680
|
+
const head = ["harness", "class", "override", "inject", "hot", "system chars", "sys/user 注入", "version"];
|
|
681
|
+
const width = head.map((h, i) => Math.max(h.length, ...rows.map((r) => String(Object.values(r)[i]).length)));
|
|
682
|
+
const line = (cells) => cells.map((c, i) => String(c).padEnd(width[i])).join(" ");
|
|
683
|
+
console.log(line(head));
|
|
684
|
+
console.log(width.map((w) => "-".repeat(w)).join(" "));
|
|
685
|
+
rows.forEach((r) => console.log(line(Object.values(r))));
|
|
686
|
+
const residues = Object.entries(matrix.harnesses).filter(([, s]) => s.residue);
|
|
687
|
+
if (residues.length) {
|
|
688
|
+
console.log("\n残留:");
|
|
689
|
+
residues.forEach(([n, s]) => console.log(` ${n}: ${s.residue}`));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function help() {
|
|
694
|
+
console.log(`agent-purge —— 把任意 harness 的系统提示词换成你的 persona
|
|
695
|
+
|
|
696
|
+
一句话:先「选」,再「用」。选一次,之后所有入口都跟着变。
|
|
697
|
+
|
|
698
|
+
① 选(写 state.json,全工具共用,改完立刻生效)
|
|
699
|
+
agent-purge use <id> 换 persona(ls 看有哪些,* = 当前)
|
|
700
|
+
agent-purge mode <m> strict(只发人设) / replace(默认) / append / off
|
|
701
|
+
agent-purge web 浏览器里点选 harness · persona · mode
|
|
702
|
+
|
|
703
|
+
② 用(三种入口,效果一样)
|
|
704
|
+
agent-purge 裸敲 = 跑 default 里那家(web 选过就用选过的)
|
|
705
|
+
agent-purge <h> [--ask "…"] 指定一家启动;--dry-run 只看命令,-- 后面透传
|
|
706
|
+
<h> web / arm 之后直接敲命令名,就已经注入
|
|
707
|
+
|
|
708
|
+
③ 装(让「别的入口」也带人设;一条命令全装,想只做一项就点名)
|
|
709
|
+
agent-purge arm <h>|--all 常驻 + 裸命令接管 + /purge 全开(= 注册)
|
|
710
|
+
agent-purge 注册 [--all] 中文别名,同上
|
|
711
|
+
agent-purge arm <h> --shim 只做裸命令接管(写 PATH 最前)
|
|
712
|
+
agent-purge arm <h> --install 只做配置文件常驻
|
|
713
|
+
agent-purge arm <h> --slash 只做会话内 /purge
|
|
714
|
+
agent-purge disarm <h>|--all 以上全部撤掉
|
|
715
|
+
|
|
716
|
+
④ 看
|
|
717
|
+
agent-purge status 一张表:每家的 shim / slash / install / 能做什么
|
|
718
|
+
agent-purge ls persona 列表
|
|
719
|
+
agent-purge matrix 能力矩阵(matrix.json)
|
|
720
|
+
agent-purge armor [...] plan|preflight|run|judge 实测热力图
|
|
721
|
+
agent-purge doctor 本机探测(哪家装了、版本)
|
|
722
|
+
|
|
723
|
+
通道语义
|
|
724
|
+
shim 在 PATH 最前放同名 cmd,裸敲即经 agent-purge 启动(sys 层注入)
|
|
725
|
+
install 改 harness 自己的配置/插件文件(连别人启动的它也会带人设)
|
|
726
|
+
slash 会话里打 /purge 换提示词:**pi 和 dsh 都能做**(pi 的扩展钩子 / dsh 的 cordis 插件,真 sys 层、热切换)
|
|
727
|
+
其他家没有这个能力:/purge 展开成用户消息,bench 实测 0/2 无效,所以不提供
|
|
728
|
+
注入强度:只有 sys 层算。user 层(上下文块/用户消息)在 codex/claude/opencode/grok/hermes 实测都是 0/2,一律不提供。
|
|
729
|
+
|
|
730
|
+
harness: pi, opencode, opencode2, dsh, codex, claude, grok, omp, hermes, zcode, antigravity, qoder, workbuddy, codebuddy
|
|
731
|
+
opencode2 = @opencode/cli 2.x(v2 插件 API),dsh = DeepSeek Harness(cordis 插件,desktop/web/headless 三形态)。
|
|
732
|
+
qoder / workbuddy 无 prompt 组装入口(提示词在服务端),只能改上下文规则文件。
|
|
733
|
+
`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function die(message) {
|
|
737
|
+
console.error(message);
|
|
738
|
+
process.exit(2);
|
|
739
|
+
}
|