@andares/pi-toolkits 0.9.1 → 0.9.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/README.md CHANGED
@@ -106,6 +106,8 @@ Enter with `/ask` (press it again to exit). Meant for the case where you only wa
106
106
  - `/ask-perm allow allow allow ask` — positional form (`in-read in-write out-read out-write`), enables immediately without a dialog. Missing arguments fall back to the defaults. This is the form autostart uses.
107
107
  - `/ask-perm` again — turns it off.
108
108
 
109
+ Repeating the argument form with the modes already in force is a **no-op**: no second "ON" notice and no loss of the session grants collected so far. Changing any mode notifies and clears those grants, because they were made under the old policy.
110
+
109
111
  **Defaults**: in-project read/write allowed, outside read allowed, outside write asks.
110
112
 
111
113
  | Scope | reads | writes / exec |
@@ -307,6 +309,8 @@ No file = feature disabled, zero overhead, silently skipped.
307
309
 
308
310
  The global list runs first, so a project can override an earlier setting (for example a later `/ask-perm` with different arguments). Use the global file only when you want the same commands in every project; per-project files remain the recommended default.
309
311
 
312
+ An **identical command line** declared by both sources runs once: a duplicate is skipped for that replay, so a global `/ask-perm ...` copied into a project does not run (or announce itself) twice. Different commands still both run, which is what keeps a project override working.
313
+
310
314
  **When it runs**:
311
315
 
312
316
  | session_start reason | Replayed | Why |
package/README.zh-CN.md CHANGED
@@ -106,6 +106,8 @@ pi -e ./src/index.ts
106
106
  - `/ask-perm allow allow allow ask` —— 位置参数形式(`in-read in-write out-read out-write`),不弹窗直接开启;没给全的参数回落到默认值。这也是 autostart 使用的形式
107
107
  - 再输入一次 `/ask-perm` —— 关闭
108
108
 
109
+ 参数形式重复执行、且四项模式与当前一致时是**空操作**:不会再出一条「ON」提示,也不会清掉已经攒下的会话授权。只有模式真的变了才会提示并清空授权——那些授权是按旧策略给的。
110
+
109
111
  **默认值**:项目内读/写全放行,项目外读放行,项目外写询问。
110
112
 
111
113
  | 作用域 | 读 | 写 / 执行 |
@@ -307,6 +309,8 @@ run = [
307
309
 
308
310
  全局列表先执行,项目列表后执行,因此项目可以覆盖此前的设置(例如后面再执行一条不同参数的 `/ask-perm`)。只有希望所有项目都用同一套命令时才用全局文件;仍然建议以项目级配置为主。
309
311
 
312
+ **完全相同的命令行**若同时出现在两个来源,一次重放里只执行一次:重复项跳过,所以把全局的 `/ask-perm ...` 复制到项目里不会再跑一遍、也不会再提示一遍。不同的命令仍然各跑一次——这正是「项目覆盖全局」得以成立的原因。
313
+
310
314
  **执行时机**:
311
315
 
312
316
  | session_start reason | 是否重放 | 原因 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andares/pi-toolkits",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), session-scoped permission checks, model favorites, prompt stash, third-party code review, session autostart, and a dedicated compaction model",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -418,6 +418,29 @@ describe("registerAutostart", () => {
418
418
  expect(h.notifications[0]?.type).toBe("warning");
419
419
  });
420
420
 
421
+ it("runs an identical command only once across sources", async () => {
422
+ const h = await harness(
423
+ "[command]\nrun = [\"/ask on\"]",
424
+ false,
425
+ "[command]\nrun = [\"/ask on\"]",
426
+ );
427
+ await h.start("startup");
428
+ expect(h.sent.map((entry) => entry.command)).toEqual(["/ask on"]);
429
+ });
430
+
431
+ it("still runs different commands from both sources", async () => {
432
+ const h = await harness(
433
+ "[command]\nrun = [\"/ask on\", \"/add-dir ../p\"]",
434
+ false,
435
+ "[command]\nrun = [\"/ask on\"]",
436
+ );
437
+ await h.start("startup");
438
+ expect(h.sent.map((entry) => entry.command)).toEqual([
439
+ "/ask on",
440
+ "/add-dir ../p",
441
+ ]);
442
+ });
443
+
421
444
  it("keeps running the project config when the global one is absent", async () => {
422
445
  const h = await harness('[command]\nrun = ["/ask on"]', false, null);
423
446
  await h.start("startup");
@@ -163,6 +163,13 @@ export function registerAutostart(
163
163
  ),
164
164
  );
165
165
 
166
+ // The same line may be declared by both sources (global defaults plus a
167
+ // project copy). Running an identical command twice in one replay is
168
+ // never meaningful — a repeated `/ask-perm` would re-notify, and a
169
+ // repeated `/add-dir` is pure noise — so identical text runs once.
170
+ // Different commands still both run, which keeps the project's override.
171
+ const dispatched = new Set<string>();
172
+
166
173
  for (const source of sources) {
167
174
  const commands = await readCommands(ctx, source);
168
175
  if (commands === undefined || commands.length === 0) continue;
@@ -184,6 +191,8 @@ export function registerAutostart(
184
191
  );
185
192
  continue;
186
193
  }
194
+ if (dispatched.has(command)) continue;
195
+ dispatched.add(command);
187
196
  // One failing command must not cancel the rest of the list.
188
197
  try {
189
198
  await pi.sendUserMessage(command, { expandPromptTemplates: true });
@@ -269,15 +269,30 @@ describe("/ask-perm", () => {
269
269
  expect(await h.evaluate(customEvent("mcp"))).toBeUndefined();
270
270
  });
271
271
 
272
- it("clears session grants when the mode is reconfigured", async () => {
272
+ it("is idempotent: repeating the same command notifies once and keeps grants", async () => {
273
273
  const h = makeHarness();
274
274
  await h.command("allow allow allow ask");
275
+ expect(h.notes).toHaveLength(1);
276
+
275
277
  asAsk(await h.evaluate(bashEvent("rm -rf /tmp/outside/x"))).request.apply(
276
278
  "allow-session",
277
279
  );
280
+ await h.command("allow allow allow ask");
281
+ expect(h.notes).toHaveLength(1);
282
+ // The grant collected before the repeat is still in force.
278
283
  expect(await h.evaluate(bashEvent("rm -rf /tmp/outside/y"))).toBeUndefined();
284
+ });
279
285
 
286
+ it("notifies and clears session grants when the modes actually change", async () => {
287
+ const h = makeHarness();
280
288
  await h.command("allow allow allow ask");
289
+ asAsk(await h.evaluate(bashEvent("rm -rf /tmp/outside/x"))).request.apply(
290
+ "allow-session",
291
+ );
292
+ expect(await h.evaluate(bashEvent("rm -rf /tmp/outside/y"))).toBeUndefined();
293
+
294
+ await h.command("ask allow allow ask");
295
+ expect(h.notes).toHaveLength(2);
281
296
  expect(asAsk(await h.evaluate(bashEvent("rm -rf /tmp/outside/y"))).kind).toBe("ask");
282
297
  });
283
298
  });
@@ -31,6 +31,7 @@ import {
31
31
  import {
32
32
  formatModes,
33
33
  parsePermissionArgs,
34
+ sameModes,
34
35
  shortMode,
35
36
  } from "./modes.js";
36
37
  import { openPermissionPanel } from "./panel.js";
@@ -253,9 +254,16 @@ export function registerPermission(pi: ExtensionAPI, gates: GateHub): void {
253
254
  ctx.ui.notify(`ask-perm: ${parsed.error}`, "error");
254
255
  return;
255
256
  }
257
+ // Idempotent: the same command may arrive twice in one session start
258
+ // (both autostart sources declaring it, a duplicated config line, …).
259
+ // Re-applying an unchanged policy is a no-op — no second "ON" notice,
260
+ // and no loss of the session grants collected so far.
261
+ if (enabled && sameModes(modes, parsed.modes)) return;
262
+
263
+ const changed = !sameModes(modes, parsed.modes);
256
264
  modes = parsed.modes;
257
- // A new policy invalidates grants made under the old one.
258
- rules.clear();
265
+ // A changed policy invalidates grants made under the old one.
266
+ if (changed) rules.clear();
259
267
  enabled = true;
260
268
  syncStatus(ctx);
261
269
  ctx.ui.notify(
@@ -284,7 +292,9 @@ export function registerPermission(pi: ExtensionAPI, gates: GateHub): void {
284
292
  );
285
293
  return;
286
294
  }
295
+ // The panel only opens while off, so this always transitions on.
287
296
  modes = chosen;
297
+ rules.clear();
288
298
  enabled = true;
289
299
  syncStatus(ctx);
290
300
  ctx.ui.notify(
@@ -80,3 +80,13 @@ export function shortMode(mode: TriMode | "allow"): string {
80
80
  return "D";
81
81
  }
82
82
  }
83
+
84
+ /** Structural equality for the four modes. */
85
+ export function sameModes(a: PermissionModes, b: PermissionModes): boolean {
86
+ return (
87
+ a.inRead === b.inRead &&
88
+ a.inWrite === b.inWrite &&
89
+ a.outRead === b.outRead &&
90
+ a.outWrite === b.outWrite
91
+ );
92
+ }