@yaosu/pi-path-guard 1.1.1 → 1.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
@@ -35,7 +35,7 @@ After installing, run `/reload` or restart pi. 安装后 `/reload` 或重启 pi
35
35
  - `/guard` — interactive mode picker (title shows the full decision matrix; choices are bilingual) 交互式选择防护模式(标题展示完整判定矩阵,选项中英双语)
36
36
  - `/guard <strict|normal|loose|trusted|naked>` — quick switch (trusted requires a warning; naked requires a double warning) 快捷切换(trusted 需警告确认;naked 需两级确认)
37
37
  - Invalid argument → falls back to the interactive picker 非法参数 → 兜底弹出交互选择
38
- - Every new session resets to `normal` 每次新会话自动回到 `normal`
38
+ - The active mode persists across sessions via settings.json (`pathGuard.mode`): project `.pi/settings.json` overrides global `~/.pi/agent/settings.json`, falling back to `normal`. `/guard <mode>` writes it back (project settings in a trusted project, else global). 模式跨会话持久化到 settings.json(`pathGuard.mode`):项目 `.pi/settings.json` 优先于全局 `~/.pi/agent/settings.json`,缺省回 `normal`;`/guard <mode>` 切换时回写(受信任项目写项目配置,否则写全局)
39
39
  - The active mode is shown in the footer status bar (`🛡 <mode>`, `🛡 NAKED` in warning color) 当前模式显示在底部状态栏(`🛡 <mode>`,naked 用警示色 `🛡 NAKED`)
40
40
 
41
41
  ### Guard mode matrix / 防护模式矩阵
@@ -73,13 +73,13 @@ After installing, run `/reload` or restart pi. 安装后 `/reload` 或重启 pi
73
73
 
74
74
  ## Development / 开发与测试
75
75
 
76
- Automated tests (99 assertions) load the real extension with a mocked pi API, covering the 5 modes × protected paths / dangerous commands / truncation / git destructive matrix, plus `/guard` command interaction, trusted-mode confirmation, naked-mode double confirmation, and the footer status indicator:
76
+ Automated tests (104 assertions) load the real extension with a mocked pi API, covering the 5 modes × protected paths / dangerous commands / truncation / git destructive matrix, plus `/guard` command interaction, trusted-mode confirmation, naked-mode double confirmation, the footer status indicator, and settings.json mode persistence:
77
77
 
78
78
  ```bash
79
79
  cd tests && node --experimental-strip-types test-pathguard.ts
80
80
  ```
81
81
 
82
- 自动化测试(99 断言)模拟 pi API 加载真实扩展,覆盖 5 种模式 × 受保护路径 / 危险命令 / 截断 / git 破坏性等判定矩阵,以及 `/guard` 命令交互、trusted 确认与 naked 两级确认、底部状态栏指示等流程:
82
+ 自动化测试(104 断言)模拟 pi API 加载真实扩展,覆盖 5 种模式 × 受保护路径 / 危险命令 / 截断 / git 破坏性等判定矩阵,以及 `/guard` 命令交互、trusted 确认与 naked 两级确认、底部状态栏指示、settings.json 模式持久化等流程:
83
83
 
84
84
  ```bash
85
85
  cd tests && node --experimental-strip-types test-pathguard.ts
@@ -42,6 +42,11 @@
42
42
  * checks, git destructive, truncate, outside deletes/overwrites); only system-destructive
43
43
  * Block-group commands (mkfs/reboot/block-device writes/bulk delete) are still confirmed.
44
44
  * Switching to naked requires a double confirmation (stronger than trusted's single warning).
45
+ *
46
+ * v3.2 adds (upgrade from v3.1): mode persistence across sessions. The active mode is read from
47
+ * settings.json on session_start (project .pi/settings.json overrides the global
48
+ * ~/.pi/agent/settings.json, falling back to normal) and written back when /guard switches
49
+ * mode — project settings in a trusted project, otherwise global settings. Key: pathGuard.mode.
45
50
  */
46
51
 
47
52
  import type {
@@ -64,7 +69,13 @@ import {
64
69
  sep,
65
70
  } from "node:path";
66
71
  import { homedir } from "node:os";
67
- import { realpathSync, existsSync, statSync } from "node:fs";
72
+ import {
73
+ realpathSync,
74
+ existsSync,
75
+ statSync,
76
+ readFileSync,
77
+ writeFileSync,
78
+ } from "node:fs";
68
79
 
69
80
  // ─── Configuration ──────────────────────────────────────────────────────
70
81
 
@@ -189,6 +200,11 @@ const DEVICE_TARGETS = new Set([
189
200
 
190
201
  const HOME = homedir();
191
202
 
203
+ /** pi project config dir name (default CONFIG_DIR_NAME is `.pi`). */
204
+ const CONFIG_DIR = ".pi";
205
+ /** Global settings.json path (default pi agent dir). */
206
+ const GLOBAL_SETTINGS_PATH = join(HOME, ".pi", "agent", "settings.json");
207
+
192
208
  // ─── Guard Modes ─────────────────────────────────────────────────────
193
209
 
194
210
  /** Guard mode: strict (full) / normal (default) / loose (relaxed) / trusted (most permissive) / naked (no protection) */
@@ -268,16 +284,94 @@ function refreshModeStatus(ui: ExtensionUIContext) {
268
284
  ui.setStatus("path-guard", t.fg(color, label));
269
285
  }
270
286
 
287
+ // ─── Settings persistence (mode survives across sessions) ─────────────
288
+
289
+ /** Global settings.json path (~/.pi/agent/settings.json). */
290
+ function globalSettingsPath(): string {
291
+ return GLOBAL_SETTINGS_PATH;
292
+ }
293
+
294
+ /** Project settings.json path (cwd/.pi/settings.json), or undefined when no cwd. */
295
+ function projectSettingsPath(cwd: string | undefined): string | undefined {
296
+ return cwd ? join(cwd, CONFIG_DIR, "settings.json") : undefined;
297
+ }
298
+
299
+ /** Read the saved guard mode from a settings.json file, or undefined if absent/invalid. */
300
+ function readSettingsMode(filePath: string | undefined): GuardMode | undefined {
301
+ if (!filePath) return undefined;
302
+ try {
303
+ if (!existsSync(filePath)) return undefined;
304
+ const data = JSON.parse(readFileSync(filePath, "utf8")) as {
305
+ pathGuard?: { mode?: string };
306
+ };
307
+ const mode = data?.pathGuard?.mode;
308
+ return typeof mode === "string" && isGuardMode(mode) ? mode : undefined;
309
+ } catch {
310
+ return undefined;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Effective saved mode at session start: project settings override global,
316
+ * falling back to normal. Project-local config is only honored for trusted
317
+ * projects; otherwise only the global setting applies.
318
+ */
319
+ function readSavedMode(cwd: string | undefined, trusted: boolean): GuardMode {
320
+ const proj = projectSettingsPath(cwd);
321
+ if (proj && trusted) {
322
+ const pm = readSettingsMode(proj);
323
+ if (pm) return pm;
324
+ }
325
+ return readSettingsMode(globalSettingsPath()) ?? "normal";
326
+ }
327
+
328
+ /**
329
+ * Persist a mode to settings.json. In a trusted project it writes the project
330
+ * settings file (cwd/.pi/settings.json); otherwise the global settings file.
331
+ * Requires a cwd; returns "project" | "global" | "none" (none = not persisted).
332
+ */
333
+ function persistMode(
334
+ mode: GuardMode,
335
+ cwd: string | undefined,
336
+ trusted: boolean,
337
+ ): string {
338
+ if (!cwd) return "none"; // no context to persist to
339
+ const target = trusted ? projectSettingsPath(cwd) : globalSettingsPath();
340
+ if (!target) return "none";
341
+ try {
342
+ let data: Record<string, unknown> = {};
343
+ if (existsSync(target)) {
344
+ data = JSON.parse(readFileSync(target, "utf8")) as Record<string, unknown>;
345
+ }
346
+ const guard = (data.pathGuard as Record<string, unknown>) ?? {};
347
+ guard.mode = mode;
348
+ data.pathGuard = guard;
349
+ writeFileSync(target, JSON.stringify(data, null, 2) + "\n", "utf8");
350
+ return trusted ? "project" : "global";
351
+ } catch {
352
+ return "none";
353
+ }
354
+ }
355
+
356
+ /** Human-readable persistence note for notify messages. */
357
+ function persistNote(where: string): string {
358
+ if (where === "project")
359
+ return "saved to project settings (.pi/settings.json)";
360
+ if (where === "global") return "saved to global settings";
361
+ return "session-only (not persisted)";
362
+ }
363
+
271
364
  export default function (pi: ExtensionAPI) {
272
- // Reset to normal on every new session (startup, /new, /resume all fire session_start)
365
+ // Restore the persisted mode on every new session (startup, /new, /resume all
366
+ // fire session_start). Project settings override global; falls back to normal.
273
367
  pi.on("session_start", (_event, ctx) => {
274
- setMode("normal", ctx.ui);
368
+ setMode(readSavedMode(ctx.cwd, ctx.isProjectTrusted?.() === true), ctx.ui);
275
369
  });
276
370
 
277
371
  // /guard slash command: view / switch guard mode
278
372
  pi.registerCommand("guard", {
279
373
  description:
280
- "Path Guard modes: /guard shows the current mode, /guard <strict|normal|loose|trusted|naked> switches",
374
+ "Path Guard modes: /guard shows the current mode, /guard <strict|normal|loose|trusted|naked> switches (persisted to settings)",
281
375
  handler: async (args, ctx) => {
282
376
  const m = args?.trim().toLowerCase() ?? "";
283
377
 
@@ -292,8 +386,9 @@ export default function (pi: ExtensionAPI) {
292
386
  }
293
387
  currentMode = m;
294
388
  refreshModeStatus(ctx.ui);
389
+ const where = persistMode(m, ctx.cwd, ctx.isProjectTrusted?.() === true);
295
390
  ctx.ui.notify(
296
- `Path Guard switched to: ${m} (session-only; new sessions reset to normal)`,
391
+ `Path Guard switched to: ${m} (${persistNote(where)})`,
297
392
  "info",
298
393
  );
299
394
  return;
@@ -329,8 +424,13 @@ export default function (pi: ExtensionAPI) {
329
424
  }
330
425
  currentMode = picked;
331
426
  refreshModeStatus(ctx.ui);
427
+ const where = persistMode(
428
+ picked,
429
+ ctx.cwd,
430
+ ctx.isProjectTrusted?.() === true,
431
+ );
332
432
  ctx.ui.notify(
333
- `Path Guard switched to: ${picked} (session-only; new sessions reset to normal)`,
433
+ `Path Guard switched to: ${picked} (${persistNote(where)})`,
334
434
  "info",
335
435
  );
336
436
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaosu/pi-path-guard",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Path Guard for pi — blocks destructive commands & path overwrites, protects .env/keys, with strict/normal/loose/trusted/naked guard modes (/guard). pi 防误删/防误覆盖扩展,支持 5 种防护模式。",
6
6
  "keywords": [