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 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`)+ 正文。正文就是会被写进系统提示词的那一段。
@@ -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(ROOT);
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(ROOT);
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(ROOT, autoFlag === "on");
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(ROOT);
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", ROOT, { personaId: picked.id, personaBody: personaText });
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(ROOT);
250
+ const store = createStore(DATA);
248
251
  const arg = argv[1];
249
- const { state, migrated } = loadState(ROOT);
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(ROOT, state.armed ? Object.entries(state.armed).filter(([, a]) => a.includes("slash")).map(([k]) => k) : [], {
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(ROOT);
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(ROOT, { harness: state.default?.harness ?? "codex", mode: want });
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(ROOT);
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: ROOT, reason: spec.reason }) ?? {};
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(ROOT, h, "install", verb === "install");
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(ROOT);
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(ROOT);
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(ROOT, { harness: cur.default.harness, mode: "off" });
339
- patchState(ROOT, { prevMode: m });
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(ROOT, { harness: cur.default.harness, mode: back });
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: ROOT, active: store.refreshActive() }) : uninstall(h);
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(ROOT, h, "install", verb === "arm");
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(ROOT, h) : uninstallShim(ROOT, h);
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(ROOT, h, "shim", verb === "arm");
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, ROOT, { personaId: pid, personaBody: store.read(pid).body.trim() })
386
- : uninstallSlash(h, ROOT);
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(ROOT, h, "slash", verb === "arm");
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(ROOT);
429
+ const store = createStore(DATA);
427
430
  const id = rest[0];
428
431
  if (!id || id === "status") {
429
- const rows = slashStatus(ROOT);
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(ROOT).filter((s) => s.installed).map((s) => s.id);
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, ROOT);
455
+ const r = uninstallSlash(h, DATA);
453
456
  if (!r.error) {
454
- setArmed(ROOT, h, "slash", false);
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(ROOT).filter((s) => s.offered).map((s) => s.id) : [id];
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, ROOT, { personaId: pid, personaBody: store.read(pid).body.trim() });
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(ROOT, h, "slash", true);
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(ROOT);
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(ROOT);
483
+ const fixed = await reconcileArmed(DATA);
481
484
  if (fixed.length) console.log(`[对账] armed 纠正:${fixed.join(", ")}`);
482
- const { state } = loadState(ROOT);
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(ROOT);
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(ROOT, "state.json")}`);
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(ROOT);
517
- const { state } = loadState(ROOT);
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
- const STATE = ${JSON.stringify(statePath)};
165
- const PERSONAS = ${JSON.stringify(personasDir)};
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.1.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": { "type": "git", "url": "git+https://github.com/ZiChuanLan/agent-purge.git" },
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": { "url": "https://github.com/ZiChuanLan/agent-purge/issues" }
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(ROOT);
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(ROOT);
90
+ const reconciled = await reconcileArmed(DATA);
88
91
  const found = detect({ versions: withVersions });
89
- const persisted = persistStatus(ROOT);
90
- const { state: st } = loadState(ROOT);
92
+ const persisted = persistStatus(DATA);
93
+ const { state: st } = loadState(DATA);
91
94
  const shims = shimStatus();
92
- const slashes = slashStatus(ROOT);
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(ROOT, { harness, mode: mode ?? "replace" }) };
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(ROOT, payload.id);
177
- const { state: st } = loadState(ROOT);
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(ROOT, slashTargets, { personaId: payload.id, personaBody: store.read(payload.id).body.trim() });
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(ROOT);
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(ROOT, { harness: cur.default.harness, mode: "off" });
201
- patchState(ROOT, { prevMode: m });
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(ROOT, { harness: cur.default.harness, mode: back });
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(ROOT).state.default;
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(ROOT, harness, "install", false);
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: ROOT, active: store.refreshActive() });
238
+ const r = install(harness, { root: DATA, active: store.refreshActive() });
236
239
  if (r.error) return { error: r.error };
237
- setArmed(ROOT, harness, "install", true);
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(ROOT, harness);
280
+ const r = uninstallShim(DATA, harness);
278
281
  if (r.error) return { error: r.error };
279
- setArmed(ROOT, harness, "shim", false);
282
+ setArmed(DATA, harness, "shim", false);
280
283
  return { removed: r.removed };
281
284
  }
282
- const r = installShim(ROOT, harness);
285
+ const r = installShim(DATA, harness);
283
286
  if (r.error) return { error: r.error };
284
- setArmed(ROOT, harness, "shim", true);
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, ROOT);
292
+ const r = uninstallSlash(harness, DATA);
290
293
  if (r.error) return { error: r.error };
291
- setArmed(ROOT, harness, "slash", false);
294
+ setArmed(DATA, harness, "slash", false);
292
295
  return { removed: r.removed };
293
296
  }
294
- const r = installSlash(harness, ROOT, { personaId: id, personaBody: store.read(id).body.trim() });
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(ROOT, harness, "slash", true);
299
+ setArmed(DATA, harness, "slash", true);
297
300
  return { file: r.file, layer: r.layer };
298
301
  }
299
302
  return { error: `未知通道: ${channel}` };