agent-purge 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/bin/agent-purge.mjs +44 -40
- package/lib/home.mjs +47 -0
- package/lib/slashes.mjs +4 -2
- package/package.json +8 -3
- package/web/server.mjs +28 -25
package/README.md
CHANGED
|
@@ -28,6 +28,14 @@ npm i -g agent-purge # 或 npx agent-purge web
|
|
|
28
28
|
|
|
29
29
|
要求 Node ≥ 20,零依赖。
|
|
30
30
|
|
|
31
|
+
**数据独立存放**,升级不会被覆盖:默认 `~/.agent-purge/`(Windows `C:\Users\<你>\.agent-purge\`),里面是 `state.json` 和 `personas/`。想换位置就设环境变量:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export AGENT_PURGE_HOME=/path/to/your/data # Windows: setx AGENT_PURGE_HOME D:\ap-data
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
旧版本(0.1.x)把数据放在包目录里,首次运行会自动搬到新家,原文件不动。
|
|
38
|
+
|
|
31
39
|
## 人设
|
|
32
40
|
|
|
33
41
|
`personas/<id>.md` = frontmatter(`id` / `name` / `description` / `harness` / `model`)+ 正文。正文就是会被写进系统提示词的那一段。
|
package/bin/agent-purge.mjs
CHANGED
|
@@ -14,9 +14,12 @@ import { resolveBin, spawnBin } from "../lib/spawn.mjs";
|
|
|
14
14
|
import { installCrashHandlers, recover } from "../lib/restore.mjs";
|
|
15
15
|
import { capsFor } from "../lib/caps.mjs";
|
|
16
16
|
import { MODES, armedOf, loadState, patchState, setArmed, setDefault } from "../lib/state.mjs";
|
|
17
|
+
import { ensureDataHome } from "../lib/home.mjs";
|
|
17
18
|
|
|
18
19
|
// bun --compile 产物:资源目录 = exe 所在目录;普通 node:包根
|
|
19
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;
|
|
20
23
|
const matrix = JSON.parse(readFileSync(join(ROOT, "matrix.json"), "utf8"));
|
|
21
24
|
|
|
22
25
|
const argv = process.argv.slice(2);
|
|
@@ -82,7 +85,7 @@ switch (cmd) {
|
|
|
82
85
|
|
|
83
86
|
/** 无参:state.default 说跑谁就跑谁;没有默认就跑到 Web 去选 */
|
|
84
87
|
async function defaultHarness() {
|
|
85
|
-
const { state } = loadState(
|
|
88
|
+
const { state } = loadState(DATA);
|
|
86
89
|
const harness = state.default?.harness;
|
|
87
90
|
if (!harness) {
|
|
88
91
|
console.log("还没有默认 harness。两种办法:\n agent-purge web 在浏览器里选,再点“设为默认执行器”\n agent-purge <harness> 直接点名跑(如 agent-purge codex)");
|
|
@@ -97,7 +100,7 @@ async function launch(name) {
|
|
|
97
100
|
if (!spec) die(`未知 harness: ${name}(可用: ${Object.keys(matrix.harnesses).join(", ")};子命令见 --help)`);
|
|
98
101
|
if (spec.class === "unsupported") die(`${name} 做不到:${spec.reason}`);
|
|
99
102
|
|
|
100
|
-
const { state } = loadState(
|
|
103
|
+
const { state } = loadState(DATA);
|
|
101
104
|
// 由 state.json 决定注入方式;显式 --mode 永远压过它
|
|
102
105
|
const explicitMode = flag("--mode", null);
|
|
103
106
|
if (explicitMode !== null && !MODES.includes(explicitMode)) die(`--mode 只能是 ${MODES.join(" / ")}`);
|
|
@@ -108,7 +111,7 @@ async function launch(name) {
|
|
|
108
111
|
if (autoFlag !== null) {
|
|
109
112
|
if (autoFlag !== "on" && autoFlag !== "off") die("--auto 只能是 on / off");
|
|
110
113
|
const { setPiAuto } = await import("../lib/state.mjs");
|
|
111
|
-
setPiAuto(
|
|
114
|
+
setPiAuto(DATA, autoFlag === "on");
|
|
112
115
|
console.log(`[auto] 裸敲 pi ${autoFlag === "on" ? "会" : "不会"}自动注入(已写入 state.json)`);
|
|
113
116
|
}
|
|
114
117
|
|
|
@@ -131,7 +134,7 @@ async function launch(name) {
|
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
const { createStore } = await import("../lib/personas.mjs");
|
|
134
|
-
const store = createStore(
|
|
137
|
+
const store = createStore(DATA);
|
|
135
138
|
const personaName = flag("--persona", "active");
|
|
136
139
|
let pickedName = personaName === "active" ? store.activeId() : personaName;
|
|
137
140
|
let picked = store.read(pickedName);
|
|
@@ -216,7 +219,7 @@ async function launch(name) {
|
|
|
216
219
|
// 不装、不喊注入:扩展就算在盘上,也会因为 mode=off + 没 FORCE 而沉默
|
|
217
220
|
built.env = { ...built.env, AGENT_PURGE_MODE: "off" };
|
|
218
221
|
} else {
|
|
219
|
-
installSlash("pi",
|
|
222
|
+
installSlash("pi", DATA, { personaId: picked.id, personaBody: personaText });
|
|
220
223
|
built.env = { ...built.env, AGENT_PURGE_PERSONA: picked.id, AGENT_PURGE_MODE: mode, AGENT_PURGE_FORCE: "1" };
|
|
221
224
|
}
|
|
222
225
|
}
|
|
@@ -244,9 +247,9 @@ async function launch(name) {
|
|
|
244
247
|
|
|
245
248
|
async function useCmd() {
|
|
246
249
|
const { createStore } = await import("../lib/personas.mjs");
|
|
247
|
-
const store = createStore(
|
|
250
|
+
const store = createStore(DATA);
|
|
248
251
|
const arg = argv[1];
|
|
249
|
-
const { state, migrated } = loadState(
|
|
252
|
+
const { state, migrated } = loadState(DATA);
|
|
250
253
|
migrated.forEach((m) => console.log(`[migrate] ${m}`));
|
|
251
254
|
|
|
252
255
|
const id = arg && !arg.startsWith("-") ? arg : state.persona;
|
|
@@ -259,7 +262,7 @@ async function useCmd() {
|
|
|
259
262
|
|
|
260
263
|
// user 层的 /purge 命令文件是烘焙进去的,换人设必须重渲染
|
|
261
264
|
const { refreshSlashes } = await import("../lib/slashes.mjs");
|
|
262
|
-
const refreshed = refreshSlashes(
|
|
265
|
+
const refreshed = refreshSlashes(DATA, state.armed ? Object.entries(state.armed).filter(([, a]) => a.includes("slash")).map(([k]) => k) : [], {
|
|
263
266
|
personaId: id,
|
|
264
267
|
personaBody: store.read(id).body.trim(),
|
|
265
268
|
});
|
|
@@ -272,7 +275,7 @@ async function useCmd() {
|
|
|
272
275
|
|
|
273
276
|
function modeCmd() {
|
|
274
277
|
const want = argv[1];
|
|
275
|
-
const { state } = loadState(
|
|
278
|
+
const { state } = loadState(DATA);
|
|
276
279
|
const current = state.default?.mode ?? "replace";
|
|
277
280
|
if (!want) {
|
|
278
281
|
console.log(
|
|
@@ -287,7 +290,7 @@ function modeCmd() {
|
|
|
287
290
|
return;
|
|
288
291
|
}
|
|
289
292
|
if (!MODES.includes(want)) die(`mode 只能是 ${MODES.join(" / ")}`);
|
|
290
|
-
setDefault(
|
|
293
|
+
setDefault(DATA, { harness: state.default?.harness ?? "codex", mode: want });
|
|
291
294
|
console.log(`mode ← ${want}(state.json;下次启动生效,--mode 可临时覆盖)`);
|
|
292
295
|
}
|
|
293
296
|
|
|
@@ -305,12 +308,12 @@ async function installCmd(verb, rest) {
|
|
|
305
308
|
}
|
|
306
309
|
const spec = matrix.harnesses[h];
|
|
307
310
|
const { createStore } = await import("../lib/personas.mjs");
|
|
308
|
-
const store = createStore(
|
|
311
|
+
const store = createStore(DATA);
|
|
309
312
|
const pid = store.activeId();
|
|
310
|
-
const r = fn({ personaPath: store.materialize(pid), personaText: store.read(pid).body.trim(), root:
|
|
313
|
+
const r = fn({ personaPath: store.materialize(pid), personaText: store.read(pid).body.trim(), root: DATA, reason: spec.reason }) ?? {};
|
|
311
314
|
const ok = verb === "install" ? r.ok !== false : r.ok !== false;
|
|
312
315
|
console.log(`${ok ? "✓" : "✗"} ${verb} ${h}${r.file ? ` → ${r.file}` : ""}${r.note ? ` (${r.note})` : ""}`);
|
|
313
|
-
if (ok) setArmed(
|
|
316
|
+
if (ok) setArmed(DATA, h, "install", verb === "install");
|
|
314
317
|
}
|
|
315
318
|
}
|
|
316
319
|
|
|
@@ -320,7 +323,7 @@ async function armCmd(verb, rest) {
|
|
|
320
323
|
const { install, uninstall } = await import("../lib/persist.mjs");
|
|
321
324
|
const { capsFor } = await import("../lib/caps.mjs");
|
|
322
325
|
const { createStore } = await import("../lib/personas.mjs");
|
|
323
|
-
const store = createStore(
|
|
326
|
+
const store = createStore(DATA);
|
|
324
327
|
const pid = store.activeId();
|
|
325
328
|
if (!pid) die("还没有 persona:先 agent-purge use <id>");
|
|
326
329
|
|
|
@@ -332,15 +335,15 @@ async function armCmd(verb, rest) {
|
|
|
332
335
|
// 否则 `agent-purge <h>` 这条入口照样注入(实测:关了总开关还能接管,就是这里漏的)。
|
|
333
336
|
// 开的时候把上次的 mode 还回去。单家操作不动 mode。
|
|
334
337
|
if (rest.includes("--all")) {
|
|
335
|
-
const { state: cur } = loadState(
|
|
338
|
+
const { state: cur } = loadState(DATA);
|
|
336
339
|
const m = cur.default?.mode ?? "off";
|
|
337
340
|
if (verb === "disarm" && m !== "off" && cur.default?.harness) {
|
|
338
|
-
setDefault(
|
|
339
|
-
patchState(
|
|
341
|
+
setDefault(DATA, { harness: cur.default.harness, mode: "off" });
|
|
342
|
+
patchState(DATA, { prevMode: m });
|
|
340
343
|
}
|
|
341
344
|
if (verb === "arm" && m === "off" && cur.default?.harness) {
|
|
342
345
|
const back = MODES.includes(cur.prevMode) && cur.prevMode !== "off" ? cur.prevMode : "replace";
|
|
343
|
-
setDefault(
|
|
346
|
+
setDefault(DATA, { harness: cur.default.harness, mode: back });
|
|
344
347
|
}
|
|
345
348
|
}
|
|
346
349
|
|
|
@@ -364,29 +367,29 @@ async function armCmd(verb, rest) {
|
|
|
364
367
|
}
|
|
365
368
|
}
|
|
366
369
|
if (ch === "install") {
|
|
367
|
-
const r = verb === "arm" ? install(h, { root:
|
|
370
|
+
const r = verb === "arm" ? install(h, { root: DATA, active: store.refreshActive() }) : uninstall(h);
|
|
368
371
|
if (r.error) skip.push(`常驻: ${r.error}`);
|
|
369
372
|
else {
|
|
370
|
-
setArmed(
|
|
373
|
+
setArmed(DATA, h, "install", verb === "arm");
|
|
371
374
|
done.push(verb === "arm" ? `常驻 ${r.action}` : `撤常驻 ${r.action}`);
|
|
372
375
|
}
|
|
373
376
|
}
|
|
374
377
|
if (ch === "shim") {
|
|
375
|
-
const r = verb === "arm" ? installShim(
|
|
378
|
+
const r = verb === "arm" ? installShim(DATA, h) : uninstallShim(DATA, h);
|
|
376
379
|
if (r.error) skip.push(`shim: ${r.error}`);
|
|
377
380
|
else {
|
|
378
|
-
setArmed(
|
|
381
|
+
setArmed(DATA, h, "shim", verb === "arm");
|
|
379
382
|
done.push(verb === "arm" ? "裸命令接管" : "撤裸命令接管");
|
|
380
383
|
}
|
|
381
384
|
}
|
|
382
385
|
if (ch === "slash") {
|
|
383
386
|
const r =
|
|
384
387
|
verb === "arm"
|
|
385
|
-
? installSlash(h,
|
|
386
|
-
: uninstallSlash(h,
|
|
388
|
+
? installSlash(h, DATA, { personaId: pid, personaBody: store.read(pid).body.trim() })
|
|
389
|
+
: uninstallSlash(h, DATA);
|
|
387
390
|
if (r.error) skip.push(`/purge: ${r.error}`);
|
|
388
391
|
else {
|
|
389
|
-
setArmed(
|
|
392
|
+
setArmed(DATA, h, "slash", verb === "arm");
|
|
390
393
|
done.push(verb === "arm" ? `/purge ${r.layer} 层` : "撤 /purge");
|
|
391
394
|
}
|
|
392
395
|
}
|
|
@@ -423,10 +426,10 @@ async function armCmd(verb, rest) {
|
|
|
423
426
|
async function slashCmd(rest) {
|
|
424
427
|
const { installSlash, uninstallSlash, slashStatus } = await import("../lib/slashes.mjs");
|
|
425
428
|
const { createStore } = await import("../lib/personas.mjs");
|
|
426
|
-
const store = createStore(
|
|
429
|
+
const store = createStore(DATA);
|
|
427
430
|
const id = rest[0];
|
|
428
431
|
if (!id || id === "status") {
|
|
429
|
-
const rows = slashStatus(
|
|
432
|
+
const rows = slashStatus(DATA);
|
|
430
433
|
const live = rows.filter((s) => s.offered);
|
|
431
434
|
live.forEach((s) => {
|
|
432
435
|
console.log(`${s.installed ? "✓" : " "} ${s.id.padEnd(9)} ${s.layer.padEnd(4)} ${s.trigger.padEnd(22)} persona=${s.personaId ?? "-"}`);
|
|
@@ -446,47 +449,47 @@ async function slashCmd(rest) {
|
|
|
446
449
|
const ids =
|
|
447
450
|
target && target !== "--all"
|
|
448
451
|
? rest.slice(1).filter((x) => !x.startsWith("-"))
|
|
449
|
-
: slashStatus(
|
|
452
|
+
: slashStatus(DATA).filter((s) => s.installed).map((s) => s.id);
|
|
450
453
|
if (!ids.length) console.log("没有装着的 /purge");
|
|
451
454
|
ids.forEach((h) => {
|
|
452
|
-
const r = uninstallSlash(h,
|
|
455
|
+
const r = uninstallSlash(h, DATA);
|
|
453
456
|
if (!r.error) {
|
|
454
|
-
setArmed(
|
|
457
|
+
setArmed(DATA, h, "slash", false);
|
|
455
458
|
console.log(`✓ 已移除 /purge ${h}${r.removed?.length ? " → " + r.removed.join(", ") : "(本来就没有)"}`);
|
|
456
459
|
}
|
|
457
460
|
});
|
|
458
461
|
return;
|
|
459
462
|
}
|
|
460
|
-
const targets = id === "--all" ? slashStatus(
|
|
463
|
+
const targets = id === "--all" ? slashStatus(DATA).filter((s) => s.offered).map((s) => s.id) : [id];
|
|
461
464
|
const pid = store.activeId();
|
|
462
465
|
if (!pid) die("还没有 persona:先 agent-purge use <id>");
|
|
463
466
|
for (const h of targets) {
|
|
464
|
-
const r = installSlash(h,
|
|
467
|
+
const r = installSlash(h, DATA, { personaId: pid, personaBody: store.read(pid).body.trim() });
|
|
465
468
|
if (r.error) {
|
|
466
469
|
console.log(`- ${h}: ${r.error}`);
|
|
467
470
|
continue;
|
|
468
471
|
}
|
|
469
|
-
setArmed(
|
|
472
|
+
setArmed(DATA, h, "slash", true);
|
|
470
473
|
console.log(`✓ /purge → ${h}(${r.layer} 层)${r.file}`);
|
|
471
474
|
console.log(` ${r.note}`);
|
|
472
475
|
}
|
|
473
476
|
}
|
|
474
477
|
|
|
475
478
|
async function statusCmd() {
|
|
476
|
-
const { migrated } = loadState(
|
|
479
|
+
const { migrated } = loadState(DATA);
|
|
477
480
|
migrated.forEach((m) => console.log(`[migrate] ${m}`));
|
|
478
481
|
// armed 是缓存,磁盘才是真相:先把假记录摘掉,再报数
|
|
479
482
|
const { reconcileArmed } = await import("../lib/armed.mjs");
|
|
480
|
-
const fixed = await reconcileArmed(
|
|
483
|
+
const fixed = await reconcileArmed(DATA);
|
|
481
484
|
if (fixed.length) console.log(`[对账] armed 纠正:${fixed.join(", ")}`);
|
|
482
|
-
const { state } = loadState(
|
|
485
|
+
const { state } = loadState(DATA);
|
|
483
486
|
return Promise.all([import("../lib/shim.mjs"), import("../lib/slashes.mjs")]).then(([shimMod, slashMod]) => {
|
|
484
487
|
const shims = shimMod.shimStatus();
|
|
485
|
-
const slashes = slashMod.slashStatus(
|
|
488
|
+
const slashes = slashMod.slashStatus(DATA);
|
|
486
489
|
|
|
487
490
|
console.log(`persona ${state.persona ?? "(未设置)"}`);
|
|
488
491
|
console.log(`default ${state.default ? `${state.default.harness} · ${state.default.mode}` : "(未设置)"} 裸敲 agent-purge 用它`);
|
|
489
|
-
console.log(`state ${join(
|
|
492
|
+
console.log(`state ${join(DATA, "state.json")}`);
|
|
490
493
|
console.log("");
|
|
491
494
|
console.log("harness shim slash install /purge层 常驻落点");
|
|
492
495
|
for (const [h] of Object.entries(matrix.harnesses)) {
|
|
@@ -513,8 +516,8 @@ async function statusCmd() {
|
|
|
513
516
|
|
|
514
517
|
async function lsCmd() {
|
|
515
518
|
const { createStore } = await import("../lib/personas.mjs");
|
|
516
|
-
const store = createStore(
|
|
517
|
-
const { state } = loadState(
|
|
519
|
+
const store = createStore(DATA);
|
|
520
|
+
const { state } = loadState(DATA);
|
|
518
521
|
const fit = store.resolve({ harness: state.default?.harness, model: null });
|
|
519
522
|
for (const p of store.list()) {
|
|
520
523
|
const mark = p.id === store.activeId() ? "*" : " ";
|
|
@@ -532,6 +535,7 @@ function webCmd() {
|
|
|
532
535
|
detached: true,
|
|
533
536
|
stdio: "ignore",
|
|
534
537
|
shell: false,
|
|
538
|
+
env: { ...process.env, AGENT_PURGE_HOME: DATA },
|
|
535
539
|
});
|
|
536
540
|
child.unref();
|
|
537
541
|
console.log(`agent-purge web 已在后台启动 → http://127.0.0.1:${port}(pid ${child.pid})`);
|
package/lib/home.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// 用户数据目录:state.json 与 personas/ 不跟代码走。
|
|
2
|
+
//
|
|
3
|
+
// 原因:npm 升级是「整个包目录替换」,数据若留在包目录里会连人设一起被清掉。
|
|
4
|
+
// 所以代码认 ROOT(装在哪都行),数据认 HOME:
|
|
5
|
+
// AGENT_PURGE_HOME 显式指定 > ~/.agent-purge
|
|
6
|
+
//
|
|
7
|
+
// 老布局(数据躺在包目录里)在首次运行时整份拷过来,原件留着当备份,不删。
|
|
8
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
export function dataHome() {
|
|
13
|
+
const env = process.env.AGENT_PURGE_HOME?.trim();
|
|
14
|
+
if (!env) return join(homedir(), ".agent-purge");
|
|
15
|
+
return isAbsolute(env) ? env : resolve(env);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 确保数据目录存在,并把旧布局搬过来。
|
|
20
|
+
* @returns {{ home: string, migrated: string[] }}
|
|
21
|
+
*/
|
|
22
|
+
export function ensureDataHome(pkgRoot = null) {
|
|
23
|
+
const home = dataHome();
|
|
24
|
+
mkdirSync(home, { recursive: true });
|
|
25
|
+
const migrated = [];
|
|
26
|
+
// 显式指定 AGENT_PURGE_HOME = 用户自己挑的新家,不往里搬旧数据
|
|
27
|
+
if (!pkgRoot || process.env.AGENT_PURGE_HOME?.trim() || resolve(pkgRoot) === resolve(home)) {
|
|
28
|
+
return { home, migrated };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const state = [join(pkgRoot, "state.json"), join(home, "state.json")];
|
|
32
|
+
if (existsSync(state[0]) && !existsSync(state[1])) {
|
|
33
|
+
copyFileSync(state[0], state[1]);
|
|
34
|
+
migrated.push(`state: ${state[0]} → ${state[1]}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const from = join(pkgRoot, "personas");
|
|
38
|
+
const to = join(home, "personas");
|
|
39
|
+
if (existsSync(from) && !existsSync(to)) {
|
|
40
|
+
const md = readdirSync(from).filter((f) => f.endsWith(".md"));
|
|
41
|
+
if (md.length) {
|
|
42
|
+
cpSync(from, to, { recursive: true });
|
|
43
|
+
migrated.push(`personas: ${from} → ${to}(${md.length} 个)`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { home, migrated };
|
|
47
|
+
}
|
package/lib/slashes.mjs
CHANGED
|
@@ -161,8 +161,10 @@ function piExtension(root) {
|
|
|
161
161
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
162
162
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
const
|
|
164
|
+
// 数据目录跟安装目录解耦:AGENT_PURGE_HOME 优先,没有才用安装时记住的路径
|
|
165
|
+
const HOME = process.env.AGENT_PURGE_HOME ? String(process.env.AGENT_PURGE_HOME).replace(/\\\\/g, "/") : null;
|
|
166
|
+
const STATE = (HOME ? HOME + "/state.json" : ${JSON.stringify(statePath)});
|
|
167
|
+
const PERSONAS = (HOME ? HOME + "/personas" : ${JSON.stringify(personasDir)});
|
|
166
168
|
const HOST_IDENTITY = "You are an expert coding assistant operating inside pi";
|
|
167
169
|
|
|
168
170
|
function stripFrontmatter(raw: string): string {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-purge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "One persona, many coding agents: replace or inject system prompts across codex, claude, opencode, grok, omp, hermes, pi, zcode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,12 @@
|
|
|
37
37
|
"prompt-injection"
|
|
38
38
|
],
|
|
39
39
|
"license": "MIT",
|
|
40
|
-
"repository": {
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/ZiChuanLan/agent-purge.git"
|
|
43
|
+
},
|
|
41
44
|
"homepage": "https://github.com/ZiChuanLan/agent-purge#readme",
|
|
42
|
-
"bugs": {
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/ZiChuanLan/agent-purge/issues"
|
|
47
|
+
}
|
|
43
48
|
}
|
package/web/server.mjs
CHANGED
|
@@ -13,8 +13,11 @@ import { installShim, shimStatus, uninstallShim } from "../lib/shim.mjs";
|
|
|
13
13
|
import { installSlash, refreshSlashes, slashStatus, uninstallSlash } from "../lib/slashes.mjs";
|
|
14
14
|
import { reconcileArmed } from "../lib/armed.mjs";
|
|
15
15
|
import { capsFor } from "../lib/caps.mjs";
|
|
16
|
+
import { ensureDataHome } from "../lib/home.mjs";
|
|
16
17
|
|
|
17
18
|
const ROOT = process.versions.bun ? dirname(process.execPath) : dirname(dirname(fileURLToPath(import.meta.url)));
|
|
19
|
+
// 数据(state.json + personas/)在用户目录,不在安装目录
|
|
20
|
+
const DATA = ensureDataHome(ROOT).home;
|
|
18
21
|
// 运行期真正被改写的层(≠ bench 能测到的层):每家只有它自己暴露的那一个写入点。
|
|
19
22
|
// sys = 系统提示/配置文件/插件 splice;user = 只落在用户消息里(zcode 的 hook)。
|
|
20
23
|
const LAYER = {
|
|
@@ -24,7 +27,7 @@ const LAYER = {
|
|
|
24
27
|
zcode: "",
|
|
25
28
|
};
|
|
26
29
|
const matrix = JSON.parse(readFileSync(join(ROOT, "matrix.json"), "utf8"));
|
|
27
|
-
const store = createStore(
|
|
30
|
+
const store = createStore(DATA);
|
|
28
31
|
|
|
29
32
|
export function serveWeb(port = 7788) {
|
|
30
33
|
const server = createServer(handle);
|
|
@@ -84,12 +87,12 @@ function armorState() {
|
|
|
84
87
|
|
|
85
88
|
async function state(withVersions) {
|
|
86
89
|
// 先跟磁盘对账,再报数:armed 是缓存,磁盘才是真相
|
|
87
|
-
const reconciled = await reconcileArmed(
|
|
90
|
+
const reconciled = await reconcileArmed(DATA);
|
|
88
91
|
const found = detect({ versions: withVersions });
|
|
89
|
-
const persisted = persistStatus(
|
|
90
|
-
const { state: st } = loadState(
|
|
92
|
+
const persisted = persistStatus(DATA);
|
|
93
|
+
const { state: st } = loadState(DATA);
|
|
91
94
|
const shims = shimStatus();
|
|
92
|
-
const slashes = slashStatus(
|
|
95
|
+
const slashes = slashStatus(DATA);
|
|
93
96
|
const harnesses = Object.entries(matrix.harnesses).map(([id, m]) => {
|
|
94
97
|
const d = found[id] ?? {};
|
|
95
98
|
const inst = persisted.find((x) => x.name === id);
|
|
@@ -143,7 +146,7 @@ function saveDefaults(payload) {
|
|
|
143
146
|
if (spec.class === "unsupported") return { error: `${harness} 做不到:${spec.reason}` };
|
|
144
147
|
if (mode && !MODES.includes(mode)) return { error: `mode 只能是 ${MODES.join(" / ")}` };
|
|
145
148
|
try {
|
|
146
|
-
return { ok: true, defaults: setDefault(
|
|
149
|
+
return { ok: true, defaults: setDefault(DATA, { harness, mode: mode ?? "replace" }) };
|
|
147
150
|
} catch (e) {
|
|
148
151
|
return { error: e.message };
|
|
149
152
|
}
|
|
@@ -173,12 +176,12 @@ function usePersona(payload) {
|
|
|
173
176
|
try {
|
|
174
177
|
store.setActive(payload.id);
|
|
175
178
|
// 两个指针必须一起写,否则 UI 选的“当前人设”和 `agent-purge` 实际用的不是同一个
|
|
176
|
-
setPersona(
|
|
177
|
-
const { state: st } = loadState(
|
|
179
|
+
setPersona(DATA, payload.id);
|
|
180
|
+
const { state: st } = loadState(DATA);
|
|
178
181
|
const slashTargets = Object.entries(st.armed ?? {})
|
|
179
182
|
.filter(([, a]) => a.includes("slash"))
|
|
180
183
|
.map(([k]) => k);
|
|
181
|
-
const refreshed = refreshSlashes(
|
|
184
|
+
const refreshed = refreshSlashes(DATA, slashTargets, { personaId: payload.id, personaBody: store.read(payload.id).body.trim() });
|
|
182
185
|
return { ok: true, active: payload.id, refreshed };
|
|
183
186
|
} catch (e) {
|
|
184
187
|
return { error: e.message };
|
|
@@ -194,15 +197,15 @@ function applyArmAll(payload) {
|
|
|
194
197
|
// 全局总闸 = 电源,不只是注册表:撤的时候把 default.mode 置 off(记下 prevMode),
|
|
195
198
|
// 否则 `agent-purge <h>` 这条入口永远注入 —— 用户实测“关了也能直接接管”就出在这。
|
|
196
199
|
// 开的时候把上次的 mode 还回去。
|
|
197
|
-
const { state: cur } = loadState(
|
|
200
|
+
const { state: cur } = loadState(DATA);
|
|
198
201
|
const m = cur.default?.mode ?? "off";
|
|
199
202
|
if (verb === "off" && m !== "off" && cur.default?.harness) {
|
|
200
|
-
setDefault(
|
|
201
|
-
patchState(
|
|
203
|
+
setDefault(DATA, { harness: cur.default.harness, mode: "off" });
|
|
204
|
+
patchState(DATA, { prevMode: m });
|
|
202
205
|
}
|
|
203
206
|
if (verb === "on" && m === "off" && cur.default?.harness) {
|
|
204
207
|
const back = MODES.includes(cur.prevMode) && cur.prevMode !== "off" ? cur.prevMode : "replace";
|
|
205
|
-
setDefault(
|
|
208
|
+
setDefault(DATA, { harness: cur.default.harness, mode: back });
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
const perHarness = [];
|
|
@@ -217,7 +220,7 @@ function applyArmAll(payload) {
|
|
|
217
220
|
else perHarness.push({ harness: h, done: (r.done ?? []).map((d) => d.channel), skipped: r.skipped ?? [] });
|
|
218
221
|
}
|
|
219
222
|
if (!perHarness.length) return { error: failed.join(";") || "没有可注册的 harness" };
|
|
220
|
-
const after = loadState(
|
|
223
|
+
const after = loadState(DATA).state.default;
|
|
221
224
|
return { ok: true, action: verb, channel, mode: after?.mode ?? null, harnesses: perHarness, failed };
|
|
222
225
|
}
|
|
223
226
|
|
|
@@ -225,16 +228,16 @@ function applyInstall(payload) {
|
|
|
225
228
|
const { harness, action } = payload;
|
|
226
229
|
if (action === "uninstall") {
|
|
227
230
|
const r = uninstall(harness);
|
|
228
|
-
setArmed(
|
|
231
|
+
setArmed(DATA, harness, "install", false);
|
|
229
232
|
return { ok: true, result: r };
|
|
230
233
|
}
|
|
231
234
|
// 装谁就把谁设为 active:active.md 与 install 指向始终是同一份正文
|
|
232
235
|
const wanted = payload.persona ?? store.activeId();
|
|
233
236
|
if (!store.read(wanted)) return { error: "没有可用 persona" };
|
|
234
237
|
store.setActive(wanted);
|
|
235
|
-
const r = install(harness, { root:
|
|
238
|
+
const r = install(harness, { root: DATA, active: store.refreshActive() });
|
|
236
239
|
if (r.error) return { error: r.error };
|
|
237
|
-
setArmed(
|
|
240
|
+
setArmed(DATA, harness, "install", true);
|
|
238
241
|
return { ok: true, result: r };
|
|
239
242
|
}
|
|
240
243
|
|
|
@@ -274,26 +277,26 @@ function armChannel(harness, channel, verb, id) {
|
|
|
274
277
|
}
|
|
275
278
|
if (channel === "shim") {
|
|
276
279
|
if (verb === "off") {
|
|
277
|
-
const r = uninstallShim(
|
|
280
|
+
const r = uninstallShim(DATA, harness);
|
|
278
281
|
if (r.error) return { error: r.error };
|
|
279
|
-
setArmed(
|
|
282
|
+
setArmed(DATA, harness, "shim", false);
|
|
280
283
|
return { removed: r.removed };
|
|
281
284
|
}
|
|
282
|
-
const r = installShim(
|
|
285
|
+
const r = installShim(DATA, harness);
|
|
283
286
|
if (r.error) return { error: r.error };
|
|
284
|
-
setArmed(
|
|
287
|
+
setArmed(DATA, harness, "shim", true);
|
|
285
288
|
return { file: r.file, real: r.real };
|
|
286
289
|
}
|
|
287
290
|
if (channel === "slash") {
|
|
288
291
|
if (verb === "off") {
|
|
289
|
-
const r = uninstallSlash(harness,
|
|
292
|
+
const r = uninstallSlash(harness, DATA);
|
|
290
293
|
if (r.error) return { error: r.error };
|
|
291
|
-
setArmed(
|
|
294
|
+
setArmed(DATA, harness, "slash", false);
|
|
292
295
|
return { removed: r.removed };
|
|
293
296
|
}
|
|
294
|
-
const r = installSlash(harness,
|
|
297
|
+
const r = installSlash(harness, DATA, { personaId: id, personaBody: store.read(id).body.trim() });
|
|
295
298
|
if (r.error) return { error: r.error };
|
|
296
|
-
setArmed(
|
|
299
|
+
setArmed(DATA, harness, "slash", true);
|
|
297
300
|
return { file: r.file, layer: r.layer };
|
|
298
301
|
}
|
|
299
302
|
return { error: `未知通道: ${channel}` };
|