@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 +4 -0
- package/README.zh-CN.md +4 -0
- package/package.json +1 -1
- package/src/features/autostart/autostart.test.ts +23 -0
- package/src/features/autostart/index.ts +9 -0
- package/src/features/permission/index.test.ts +16 -1
- package/src/features/permission/index.ts +12 -2
- package/src/features/permission/modes.ts +10 -0
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.
|
|
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("
|
|
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
|
|
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
|
+
}
|