agent-purge 0.2.0 → 0.2.2

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/lib/personas.mjs CHANGED
@@ -1,180 +1,207 @@
1
- // persona 管理:一个 markdown 文件 = 元数据(frontmatter) + 系统提示词正文。
2
- // 正文是唯一喂给 harness 的东西;元数据只用来做选择。
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { loadState, saveState } from "./state.mjs";
7
-
8
- const WILD = "*";
9
- // active.md 是派生物,不是状态:install 指向它、opencode 插件运行时读它 → 真热切。
10
- // 真正的指针在 state.json 的 persona 字段(旧布局的 personas/.active 在 loadState 里已迁移)。
11
- const ACTIVE_BODY = "active.md";
12
- const RESERVED_IDS = new Set([ACTIVE_BODY.slice(0, -3), ".active"]); // "active" / ".active"
13
-
14
- export function createStore(root) {
15
- const dir = join(root, "personas");
16
- const build = join(dir, ".build");
17
- mkdirSync(build, { recursive: true });
18
-
19
- // 首次运行(或 npm 安装后的空目录):从 lib/templates/personas 拷一份中性的起步 persona。
20
- {
21
- const has = existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(".md") && f !== "active.md");
22
- const tpl = join(dirname(fileURLToPath(import.meta.url)), "templates", "personas");
23
- if (!has && existsSync(tpl)) {
24
- for (const f of readdirSync(tpl)) writeFileSync(join(dir, f), readFileSync(join(tpl, f), "utf8"));
25
- }
26
- }
27
-
28
- const listFiles = () =>
29
- (existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".md") && !f.startsWith(".") && f !== "active.md") : []);
30
-
31
- function read(id) {
32
- const file = join(dir, `${id}.md`);
33
- if (!existsSync(file)) return null;
34
- const raw = readFileSync(file, "utf8");
35
- const { meta, body } = parse(raw);
36
- return { id, file, meta, body, raw };
37
- }
38
-
39
- function list() {
40
- return listFiles()
41
- .map((f) => read(f.slice(0, -3)))
42
- .filter(Boolean)
43
- .sort((a, b) => (a.meta.order ?? 99) - (b.meta.order ?? 99) || a.id.localeCompare(b.id));
44
- }
45
-
46
- function save(id, { meta = {}, body }) {
47
- if (isReservedId(id)) throw new Error(`${id} 是保留名(active 指针专用),换个 id`);
48
- const file = join(dir, `${id}.md`);
49
- writeFileSync(file, serialize({ id, meta, body }));
50
- return read(id);
51
- }
52
-
53
- function remove(id) {
54
- rmSync(join(dir, `${id}.md`), { force: true });
55
- rmSync(join(build, `${id}.md`), { force: true });
56
- // 删的就是 active 时必须改道:否则下次无参启动直接 “没有 persona”。
57
- // 落点不抓 list()[0](那是顺序序,等于静默换人),优先落到 neutral。
58
- if (activeId() === id) {
59
- const all = list();
60
- const next = all.find((p) => p.id === "neutral") ?? all[0] ?? null;
61
- if (next) setActive(next.id);
62
- else writeFileSync(join(dir, ACTIVE_BODY), "You are a helpful coding assistant.\n");
63
- }
64
- }
65
-
66
- // 把正文落到 .build,给需要文件路径的 harness 用(frontmatter 绝不能进 prompt)
67
- function materialize(id) {
68
- const p = read(id);
69
- if (!p) return null;
70
- const out = join(build, `${id}.md`);
71
- writeFileSync(out, p.body.trim() + "\n");
72
- return out;
73
- }
74
-
75
- function activeId() {
76
- const { state } = loadState(root);
77
- if (state.persona && read(state.persona)) return state.persona;
78
- return list()[0]?.id ?? null;
79
- }
80
-
81
- function setActive(id) {
82
- const p = read(id);
83
- if (!p) throw new Error(`no such persona: ${id}`);
84
- // 先写正文快照,再写指针:中途被杀也只会多一份一致的快照
85
- writeFileSync(join(dir, ACTIVE_BODY), p.body.trim() + "\n");
86
- const { state } = loadState(root);
87
- saveState(root, { ...state, persona: id });
88
- }
89
-
90
- // install 前调用:保证 active.md 是最新正文,返回它的路径(落盘型后端指向它)
91
- function refreshActive() {
92
- const id = activeId();
93
- if (id) setActive(id);
94
- return join(dir, ACTIVE_BODY);
95
- }
96
-
97
- // 选中的 persona:harness+model 精确度优先,其次 active,其次第一个
98
- function resolve({ harness, model } = {}) {
99
- const all = list();
100
- const scored = all.map((p) => {
101
- const h = asArray(p.meta.harness);
102
- const m = asArray(p.meta.model);
103
- const hHit = h.includes(WILD) ? 1 : h.some((g) => globHit(g, harness));
104
- const mHit = m.includes(WILD) ? 1 : m.some((g) => globHit(g, model));
105
- const explicitH = hHit && !h.includes(WILD) ? 1 : 0;
106
- const explicitM = mHit && !m.includes(WILD) ? 1 : 0;
107
- return { p, hit: hHit && mHit, score: explicitH * 2 + explicitM * 4 };
108
- });
109
- const eligible = scored.filter((s) => s.hit).sort((a, b) => b.score - a.score || a.p.id.localeCompare(b.p.id));
110
- const active = activeId();
111
- const fallback = all.find((p) => p.id === active) ?? all[0] ?? null;
112
- return { persona: eligible[0]?.p ?? fallback, matched: Boolean(eligible[0]), score: eligible[0]?.score ?? 0 };
113
- }
114
-
115
- return { dir, build, list, read, save, remove, materialize, activeId, setActive, refreshActive, resolve };
116
- }
117
-
118
- /** 保留 id:会被当成 persona 文件名,与 active 双文件冲突 */
119
- export function isReservedId(id) {
120
- return RESERVED_IDS.has(String(id || ""));
121
- }
122
-
123
- function parse(raw) {
124
- if (!raw.startsWith("---")) return { meta: {}, body: raw };
125
- const end = raw.indexOf("\n---", 3);
126
- if (end === -1) return { meta: {}, body: raw };
127
- const head = raw.slice(3, end).trim();
128
- const body = raw.slice(end + 4).replace(/^\r?\n/, "");
129
- return { meta: parseMeta(head), body };
130
- }
131
-
132
- function parseMeta(head) {
133
- const meta = {};
134
- let key = null;
135
- for (const line of head.split(/\r?\n/)) {
136
- if (!line.trim()) continue;
137
- const arr = line.match(/^\s*-\s+(.*)$/);
138
- if (arr && key) {
139
- if (!Array.isArray(meta[key])) meta[key] = [];
140
- meta[key].push(unquote(arr[1]));
141
- continue;
142
- }
143
- const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
144
- if (!kv) continue;
145
- key = kv[1];
146
- const v = kv[2].trim();
147
- if (!v) {
148
- meta[key] = [];
149
- continue;
150
- }
151
- if (v.startsWith("[")) {
152
- meta[key] = v
153
- .replace(/^\[|\]$/g, "")
154
- .split(",")
155
- .map((s) => unquote(s.trim()))
156
- .filter(Boolean);
157
- } else meta[key] = unquote(v);
158
- }
159
- return meta;
160
- }
161
-
162
- function serialize({ id, meta, body }) {
163
- const lines = ["---", `id: ${meta.id ?? id}`];
164
- for (const [k, v] of Object.entries(meta)) {
165
- if (k === "id") continue;
166
- if (Array.isArray(v)) lines.push(`${k}: [${v.map((x) => JSON.stringify(String(x))).join(", ")}]`);
167
- else if (v !== undefined && v !== null && v !== "") lines.push(`${k}: ${JSON.stringify(String(v))}`);
168
- }
169
- lines.push("---", "", body.trim(), "");
170
- return lines.join("\n");
171
- }
172
-
173
- const unquote = (s) => s.replace(/^["']|["']$/g, "");
174
- const asArray = (v) => (Array.isArray(v) ? v : v ? [String(v)] : [WILD]);
175
-
176
- function globHit(pattern, value) {
177
- if (!value) return pattern === WILD;
178
- const re = new RegExp("^" + String(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i");
179
- return re.test(value);
180
- }
1
+ // persona 管理:一个 markdown 文件 = 元数据(frontmatter) + 系统提示词正文。
2
+ // 正文是唯一喂给 harness 的东西;元数据只用来做选择。
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { loadState, saveState } from "./state.mjs";
7
+
8
+ const WILD = "*";
9
+ // active.md 是派生物,不是状态:install 指向它、opencode 插件运行时读它 → 真热切。
10
+ // 真正的指针在 state.json 的 persona 字段(旧布局的 personas/.active 在 loadState 里已迁移)。
11
+ const ACTIVE_BODY = "active.md";
12
+ const RESERVED_IDS = new Set([ACTIVE_BODY.slice(0, -3), ".active"]); // "active" / ".active"
13
+
14
+ export function createStore(root) {
15
+ const dir = join(root, "personas");
16
+ const build = join(dir, ".build");
17
+ mkdirSync(build, { recursive: true });
18
+
19
+ // 首次运行(或 npm 安装后的空目录):从 lib/templates/personas 拷一份中性的起步 persona。
20
+ {
21
+ const has = existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(".md") && f !== "active.md");
22
+ const tpl = join(dirname(fileURLToPath(import.meta.url)), "templates", "personas");
23
+ if (!has && existsSync(tpl)) {
24
+ for (const f of readdirSync(tpl)) writeFileSync(join(dir, f), readFileSync(join(tpl, f), "utf8"));
25
+ }
26
+ }
27
+
28
+ const listFiles = () =>
29
+ (existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".md") && !f.startsWith(".") && f !== "active.md") : []);
30
+
31
+ function read(id) {
32
+ const file = join(dir, `${id}.md`);
33
+ if (!existsSync(file)) return null;
34
+ const raw = readFileSync(file, "utf8");
35
+ const { meta, body } = parse(raw);
36
+ return { id, file, meta: normalizeMeta(meta), body, raw };
37
+ }
38
+
39
+ function list() {
40
+ return listFiles()
41
+ .map((f) => read(f.slice(0, -3)))
42
+ .filter(Boolean)
43
+ .sort((a, b) => (a.meta.order ?? 99) - (b.meta.order ?? 99) || a.id.localeCompare(b.id));
44
+ }
45
+
46
+ function save(id, { meta = {}, body }) {
47
+ if (isReservedId(id)) throw new Error(`${id} 是保留名(active 指针专用),换个 id`);
48
+ const file = join(dir, `${id}.md`);
49
+ writeFileSync(file, serialize({ id, meta, body }));
50
+ return read(id);
51
+ }
52
+
53
+ function remove(id) {
54
+ rmSync(join(dir, `${id}.md`), { force: true });
55
+ rmSync(join(build, `${id}.md`), { force: true });
56
+ // 删的就是 active 时必须改道:否则下次无参启动直接 “没有 persona”。
57
+ // 落点不抓 list()[0](那是顺序序,等于静默换人),优先落到 neutral。
58
+ if (activeId() === id) {
59
+ const all = list();
60
+ const next = all.find((p) => p.id === "neutral") ?? all[0] ?? null;
61
+ if (next) setActive(next.id);
62
+ else writeFileSync(join(dir, ACTIVE_BODY), "You are a helpful coding assistant.\n");
63
+ }
64
+ }
65
+
66
+ // 把正文落到 .build,给需要文件路径的 harness 用(frontmatter 绝不能进 prompt)
67
+ function materialize(id) {
68
+ const p = read(id);
69
+ if (!p) return null;
70
+ const out = join(build, `${id}.md`);
71
+ writeFileSync(out, p.body.trim() + "\n");
72
+ return out;
73
+ }
74
+
75
+ function activeId() {
76
+ const { state } = loadState(root);
77
+ if (state.persona && read(state.persona)) return state.persona;
78
+ return list()[0]?.id ?? null;
79
+ }
80
+
81
+ function setActive(id) {
82
+ const p = read(id);
83
+ if (!p) throw new Error(`no such persona: ${id}`);
84
+ // 先写正文快照,再写指针:中途被杀也只会多一份一致的快照
85
+ writeFileSync(join(dir, ACTIVE_BODY), p.body.trim() + "\n");
86
+ const { state } = loadState(root);
87
+ saveState(root, { ...state, persona: id });
88
+ }
89
+
90
+ // install 前调用:保证 active.md 是最新正文,返回它的路径(落盘型后端指向它)
91
+ function refreshActive() {
92
+ const id = activeId();
93
+ if (id) setActive(id);
94
+ return join(dir, ACTIVE_BODY);
95
+ }
96
+
97
+ // 选中的 persona:harness+model 精确度优先,其次 active,其次第一个
98
+ function resolve({ harness, model } = {}) {
99
+ const all = list();
100
+ const scored = all.map((p) => {
101
+ const h = asArray(p.meta.harness);
102
+ const m = asArray(p.meta.model);
103
+ const hHit = h.includes(WILD) ? 1 : h.some((g) => globHit(g, harness));
104
+ const mHit = m.includes(WILD) ? 1 : m.some((g) => globHit(g, model));
105
+ const explicitH = hHit && !h.includes(WILD) ? 1 : 0;
106
+ const explicitM = mHit && !m.includes(WILD) ? 1 : 0;
107
+ return { p, hit: hHit && mHit, score: explicitH * 2 + explicitM * 4 };
108
+ });
109
+ const eligible = scored.filter((s) => s.hit).sort((a, b) => b.score - a.score || a.p.id.localeCompare(b.p.id));
110
+ const active = activeId();
111
+ const fallback = all.find((p) => p.id === active) ?? all[0] ?? null;
112
+ return { persona: eligible[0]?.p ?? fallback, matched: Boolean(eligible[0]), score: eligible[0]?.score ?? 0 };
113
+ }
114
+
115
+ return { dir, build, list, read, save, remove, materialize, activeId, setActive, refreshActive, resolve };
116
+ }
117
+
118
+ /** 保留 id:会被当成 persona 文件名,与 active 双文件冲突 */
119
+ export function isReservedId(id) {
120
+ return RESERVED_IDS.has(String(id || ""));
121
+ }
122
+
123
+ function parse(raw) {
124
+ if (!raw.startsWith("---")) return { meta: {}, body: raw };
125
+ const end = raw.indexOf("\n---", 3);
126
+ if (end === -1) return { meta: {}, body: raw };
127
+ const head = raw.slice(3, end).trim();
128
+ const body = raw.slice(end + 4).replace(/^\r?\n/, "");
129
+ return { meta: parseMeta(head), body };
130
+ }
131
+
132
+ function parseMeta(head) {
133
+ const meta = {};
134
+ let key = null;
135
+ for (const line of head.split(/\r?\n/)) {
136
+ if (!line.trim()) continue;
137
+ const arr = line.match(/^\s*-\s+(.*)$/);
138
+ if (arr && key) {
139
+ if (!Array.isArray(meta[key])) meta[key] = [];
140
+ meta[key].push(unquote(arr[1]));
141
+ continue;
142
+ }
143
+ const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
144
+ if (!kv) continue;
145
+ key = kv[1];
146
+ const v = kv[2].trim();
147
+ if (!v) {
148
+ meta[key] = [];
149
+ continue;
150
+ }
151
+ if (v.startsWith("[")) {
152
+ meta[key] = v
153
+ .replace(/^\[|\]$/g, "")
154
+ .split(",")
155
+ .map((s) => unquote(s.trim()))
156
+ .filter(Boolean);
157
+ } else meta[key] = coerce(v);
158
+ }
159
+ return meta;
160
+ }
161
+
162
+ function serialize({ id, meta, body }) {
163
+ const lines = ["---", `id: ${meta.id ?? id}`];
164
+ const seen = new Set(["id"]);
165
+ for (const [k, v] of Object.entries(meta)) {
166
+ // desc 是早期 UI 的键名,README/模板写的是 description:写回时只留 description,避免同名两行
167
+ const key = k === "desc" ? "description" : k;
168
+ if (seen.has(key)) continue;
169
+ if (Array.isArray(v)) {
170
+ seen.add(key);
171
+ lines.push(`${key}: [${v.map((x) => JSON.stringify(String(x))).join(", ")}]`);
172
+ } else if (v !== undefined && v !== null && v !== "") {
173
+ seen.add(key);
174
+ // 数字/布尔裸写(`order: 1`、`enabled: true`),字符串才引号包——否则一次保存就把类型改成字符串
175
+ lines.push(`${key}: ${typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(String(v))}`);
176
+ }
177
+ }
178
+ lines.push("---", "", body.trim(), "");
179
+ return lines.join("\n");
180
+ }
181
+
182
+ /**
183
+ * 键名归一:磁盘上有两套写法(README/模板 `description`,0.2.x UI `desc`)。
184
+ * 统一按 description 存,读的时候把 desc 当别名补上 —— CLI 列表、Web 表单都能直接读到。
185
+ */
186
+ function normalizeMeta(meta) {
187
+ if (meta.description !== undefined && meta.desc === undefined) meta.desc = meta.description;
188
+ if (meta.desc !== undefined && meta.description === undefined) meta.description = meta.desc;
189
+ return meta;
190
+ }
191
+
192
+ const unquote = (s) => s.replace(/^["']|["']$/g, "");
193
+ /** 标量取值:数字/布尔还原成真类型,其余保持字符串(只收干净的写法,`01` 这种不动) */
194
+ function coerce(v) {
195
+ const s = unquote(v);
196
+ if (s === "true") return true;
197
+ if (s === "false") return false;
198
+ if (/^-?\d+(\.\d+)?$/.test(s) && String(Number(s)) === s) return Number(s);
199
+ return s;
200
+ }
201
+ const asArray = (v) => (Array.isArray(v) ? v : v ? [String(v)] : [WILD]);
202
+
203
+ function globHit(pattern, value) {
204
+ if (!value) return pattern === WILD;
205
+ const re = new RegExp("^" + String(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i");
206
+ return re.test(value);
207
+ }
package/lib/slashes.mjs CHANGED
@@ -186,7 +186,7 @@ function loadPersona(): { id: string | null; body: string } {
186
186
  }
187
187
  }
188
188
 
189
- function loadState(): { persona: string | null; mode: string; shim: boolean } {
189
+ function loadState(): { persona: string | null; mode: string; shim: boolean; piAuto: boolean; piUserLayer: boolean } {
190
190
  // 启动器会用环境变量告知本次运行用的 persona/mode(--persona / --mode 覆盖);
191
191
  // 否则读 state.json —— 注意 mode 存在 default.mode,不是顶层。
192
192
  const envPersona = process.env.AGENT_PURGE_PERSONA;
@@ -197,15 +197,17 @@ function loadState(): { persona: string | null; mode: string; shim: boolean } {
197
197
  default?: { harness?: string; mode?: string };
198
198
  armed?: Record<string, string[]>;
199
199
  piAuto?: boolean;
200
+ piUserLayer?: boolean;
200
201
  };
201
202
  return {
202
203
  persona: envPersona ?? st.persona ?? null,
203
204
  mode: envMode ?? st.default?.mode ?? "replace",
204
205
  shim: Array.isArray(st.armed?.pi) && st.armed.pi.includes("shim"),
205
206
  piAuto: st.piAuto === true,
207
+ piUserLayer: st.piUserLayer === true,
206
208
  };
207
209
  } catch {
208
- return { persona: envPersona ?? null, mode: envMode ?? "replace", shim: false, piAuto: process.env.AGENT_PURGE_FORCE === "1" };
210
+ return { persona: envPersona ?? null, mode: envMode ?? "replace", shim: false, piAuto: process.env.AGENT_PURGE_FORCE === "1", piUserLayer: false };
209
211
  }
210
212
  }
211
213
 
@@ -248,6 +250,55 @@ export default function agentPurge(pi: ExtensionAPI) {
248
250
  return { systemPrompt: host.length > 0 ? cache.body + "\\n\\n" + host : cache.body };
249
251
  });
250
252
 
253
+ // 真正保证“会话中途热切也生效”的是这个拦截面:每个模型请求前重写 transcript 里的
254
+ // system 消恵。before_agent_start 的返回值在部分 pi 版本上只对首轮生效(会话已有上下文时
255
+ // 会被 transcript 覆盖),所以这里再兜一层;两层同时生效时靠 body 前缀去重。
256
+ try {
257
+ pi.on("context_with_system", async (event: any) => {
258
+ const st = loadState();
259
+ const m = st.mode === "strict" || st.mode === "append" ? st.mode : "replace";
260
+ const on = st.mode === "off" ? "off" : (forced ?? (st.persona && autoOn(st) ? "on" : "off"));
261
+ if (on !== "on") return;
262
+ const p = loadPersona();
263
+ if (!p.body) return;
264
+ const messages: any[] = Array.isArray(event?.messages) ? event.messages : [];
265
+ if (messages.length === 0) return;
266
+ const textOf = (msg: any): string => {
267
+ const c = msg?.content;
268
+ if (typeof c === "string") return c;
269
+ if (Array.isArray(c)) return c.map((b: any) => (b && typeof b.text === "string" ? b.text : "")).join("");
270
+ return "";
271
+ };
272
+ // pi 1.4 约定:这个钩子的返回值会被忽略,必须原地改 event.messages
273
+ const out = messages;
274
+ const at = out.findIndex((msg: any) => msg?.role === "system");
275
+ const host = at >= 0 ? textOf(out[at]) : "";
276
+ if (!host.includes(p.body.slice(0, 40))) {
277
+ let next: string;
278
+ if (m === "strict") next = p.body;
279
+ else if (m === "append") next = (host ? host + "\\n\\n" : "") + p.body;
280
+ else next = host ? p.body + "\\n\\n" + cutHostIdentity(host) : p.body;
281
+ if (at >= 0) out[at].content = next;
282
+ else out.unshift({ role: "system", content: next });
283
+ }
284
+
285
+ // 有些模型(实测 deepseek-v4.1-flash 走 mg 网关)直接无视 system 角色:
286
+ // 再往最后一条用户消息前面贴一份,让指令一定被看到。
287
+ if (st.piUserLayer) {
288
+ for (let i = out.length - 1; i >= 0; i--) {
289
+ if (out[i]?.role !== "user") continue;
290
+ const shown = textOf(out[i]);
291
+ if (shown.includes(p.body.slice(0, 40))) break;
292
+ out[i].content = "<agent-purge>\\n" + p.body + "\\n</agent-purge>\\n\\n" + shown;
293
+ break;
294
+ }
295
+ }
296
+ return;
297
+ });
298
+ } catch {
299
+ // 老版本 pi 没有 context_with_system:退回 before_agent_start 的返回值
300
+ }
301
+
251
302
  pi.registerCommand("purge", {
252
303
  description: "会话级接管开关(人设跟随 agent-purge 的 state.json)",
253
304
  getArgumentCompletions: (prefix: string) =>
package/lib/state.mjs CHANGED
@@ -33,7 +33,7 @@ export function statePath(root) {
33
33
  }
34
34
 
35
35
  function emptyState() {
36
- return { v: 1, persona: null, default: null, armed: {}, piAuto: false };
36
+ return { v: 1, persona: null, default: null, armed: {}, piAuto: false, piUserLayer: false };
37
37
  }
38
38
 
39
39
  /**
@@ -115,6 +115,12 @@ export function setPiAuto(root, on) {
115
115
  return saveState(root, { ...state, piAuto: on === true }).piAuto;
116
116
  }
117
117
 
118
+ /** pi 的人设走哪一层:false = system(默认),true = 额外塞进用户消息(给忽略 system 的模型用) */
119
+ export function setPiUserLayer(root, on) {
120
+ const { state } = loadState(root);
121
+ return saveState(root, { ...state, piUserLayer: on === true }).piUserLayer;
122
+ }
123
+
118
124
  /** armed 变成 {codex: ["shim","install"]};空数组即删键 */
119
125
  export function setArmed(root, id, arm, on) {
120
126
  const { state } = loadState(root);
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "agent-purge",
3
- "version": "0.2.0",
4
- "description": "One persona, many coding agents: replace or inject system prompts across codex, claude, opencode, grok, omp, hermes, pi, zcode.",
5
- "type": "module",
6
- "bin": {
7
- "agent-purge": "bin/agent-purge.mjs"
8
- },
9
- "files": [
10
- "bin",
11
- "lib",
12
- "backends",
13
- "web",
14
- "bench",
15
- "matrix.json",
16
- "README.md",
17
- "LICENSE"
18
- ],
19
- "engines": {
20
- "node": ">=20"
21
- },
22
- "scripts": {
23
- "web": "node bin/agent-purge.mjs web",
24
- "matrix": "node bin/agent-purge.mjs matrix",
25
- "detect": "node bin/agent-purge.mjs detect",
26
- "build:exe": "node scripts/build-exe.mjs",
27
- "prepack": "node -e \"require('node:fs').rmSync('personas/.build',{recursive:true,force:true})\""
28
- },
29
- "keywords": [
30
- "system-prompt",
31
- "codex",
32
- "claude",
33
- "opencode",
34
- "coding-agent",
35
- "pi",
36
- "persona",
37
- "prompt-injection"
38
- ],
39
- "license": "MIT",
40
- "repository": {
41
- "type": "git",
42
- "url": "git+https://github.com/ZiChuanLan/agent-purge.git"
43
- },
44
- "homepage": "https://github.com/ZiChuanLan/agent-purge#readme",
45
- "bugs": {
46
- "url": "https://github.com/ZiChuanLan/agent-purge/issues"
47
- }
48
- }
1
+ {
2
+ "name": "agent-purge",
3
+ "version": "0.2.2",
4
+ "description": "One persona, many coding agents: replace or inject system prompts across codex, claude, opencode, grok, omp, hermes, pi, zcode.",
5
+ "type": "module",
6
+ "bin": {
7
+ "agent-purge": "bin/agent-purge.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "lib",
12
+ "backends",
13
+ "web",
14
+ "bench",
15
+ "matrix.json",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "scripts": {
23
+ "web": "node bin/agent-purge.mjs web",
24
+ "matrix": "node bin/agent-purge.mjs matrix",
25
+ "detect": "node bin/agent-purge.mjs detect",
26
+ "build:exe": "node scripts/build-exe.mjs",
27
+ "prepack": "node -e \"require('node:fs').rmSync('personas/.build',{recursive:true,force:true})\""
28
+ },
29
+ "keywords": [
30
+ "system-prompt",
31
+ "codex",
32
+ "claude",
33
+ "opencode",
34
+ "coding-agent",
35
+ "pi",
36
+ "persona",
37
+ "prompt-injection"
38
+ ],
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/ZiChuanLan/agent-purge.git"
43
+ },
44
+ "homepage": "https://github.com/ZiChuanLan/agent-purge#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/ZiChuanLan/agent-purge/issues"
47
+ }
48
+ }