@diegopetrucci/pi-extensions 0.1.57 → 0.1.59

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.
Files changed (102) hide show
  1. package/.pi-fleet-tested-version +1 -1
  2. package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
  3. package/extensions/agent-workflow-audit/index.ts +8 -1
  4. package/extensions/agent-workflow-audit/package.json +5 -1
  5. package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
  6. package/extensions/annotate-git-diff/package.json +5 -2
  7. package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
  8. package/extensions/annotate-last-message/package.json +5 -2
  9. package/extensions/brrr/.pi-fleet-tested-version +1 -1
  10. package/extensions/brrr/README.md +2 -2
  11. package/extensions/brrr/package.json +5 -1
  12. package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
  13. package/extensions/claude-fast/package.json +5 -1
  14. package/extensions/code-reviewer/.pi-fleet-tested-version +1 -1
  15. package/extensions/code-reviewer/index.ts +129 -39
  16. package/extensions/code-reviewer/package.json +5 -1
  17. package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
  18. package/extensions/confirm-destructive/README.md +1 -1
  19. package/extensions/confirm-destructive/index.ts +20 -5
  20. package/extensions/confirm-destructive/package.json +5 -1
  21. package/extensions/context-cap/.pi-fleet-tested-version +1 -1
  22. package/extensions/context-cap/package.json +5 -1
  23. package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
  24. package/extensions/context-inspector/package.json +5 -1
  25. package/extensions/contrarian/.pi-fleet-tested-version +1 -1
  26. package/extensions/contrarian/README.md +1 -1
  27. package/extensions/contrarian/index.ts +199 -40
  28. package/extensions/contrarian/package.json +5 -1
  29. package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
  30. package/extensions/dirty-repo-guard/README.md +1 -1
  31. package/extensions/dirty-repo-guard/package.json +5 -1
  32. package/extensions/dynamic-context-pruning/.pi-fleet-tested-version +1 -1
  33. package/extensions/dynamic-context-pruning/package.json +4 -1
  34. package/extensions/git-footer/.pi-fleet-tested-version +1 -1
  35. package/extensions/git-footer/package.json +5 -1
  36. package/extensions/gnosis/.pi-fleet-tested-version +1 -1
  37. package/extensions/gnosis/package.json +5 -1
  38. package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
  39. package/extensions/illustrations-to-explain-things/package.json +4 -1
  40. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.webp +0 -0
  41. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.webp +0 -0
  42. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.webp +0 -0
  43. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.webp +0 -0
  44. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.webp +0 -0
  45. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.webp +0 -0
  46. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.webp +0 -0
  47. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.webp +0 -0
  48. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.webp +0 -0
  49. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.webp +0 -0
  50. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.webp +0 -0
  51. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.webp +0 -0
  52. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.webp +0 -0
  53. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.webp +0 -0
  54. package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
  55. package/extensions/inline-bash/README.md +1 -1
  56. package/extensions/inline-bash/package.json +5 -1
  57. package/extensions/librarian/.pi-fleet-tested-version +1 -1
  58. package/extensions/librarian/index.ts +393 -107
  59. package/extensions/librarian/package.json +5 -1
  60. package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
  61. package/extensions/minimal-footer/index.ts +2 -5
  62. package/extensions/minimal-footer/openai-usage.ts +58 -11
  63. package/extensions/minimal-footer/package.json +5 -1
  64. package/extensions/notify/.pi-fleet-tested-version +1 -1
  65. package/extensions/notify/README.md +3 -3
  66. package/extensions/notify/package.json +5 -1
  67. package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
  68. package/extensions/openai-fast/package.json +5 -1
  69. package/extensions/oracle/.pi-fleet-tested-version +1 -1
  70. package/extensions/oracle/README.md +1 -1
  71. package/extensions/oracle/index.ts +200 -41
  72. package/extensions/oracle/package.json +5 -1
  73. package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
  74. package/extensions/permission-gate/README.md +1 -1
  75. package/extensions/permission-gate/index.ts +453 -2
  76. package/extensions/permission-gate/package.json +5 -1
  77. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  78. package/extensions/quiet-tools/package.json +5 -1
  79. package/extensions/review/.pi-fleet-tested-version +1 -1
  80. package/extensions/review/index.ts +53 -19
  81. package/extensions/review/package.json +5 -1
  82. package/extensions/todo/.pi-fleet-tested-version +1 -1
  83. package/extensions/todo/README.md +1 -1
  84. package/extensions/todo/package.json +5 -1
  85. package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
  86. package/extensions/triage-comments/index.ts +115 -34
  87. package/extensions/triage-comments/package.json +5 -1
  88. package/package.json +10 -6
  89. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.png +0 -0
  90. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.png +0 -0
  91. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.png +0 -0
  92. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.png +0 -0
  93. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.png +0 -0
  94. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.png +0 -0
  95. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.png +0 -0
  96. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.png +0 -0
  97. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.png +0 -0
  98. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.png +0 -0
  99. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.png +0 -0
  100. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.png +0 -0
  101. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.png +0 -0
  102. package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.png +0 -0
@@ -11,8 +11,9 @@ import * as path from "node:path";
11
11
 
12
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
 
14
- const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
14
+ const dangerousPatterns = [/\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
15
15
  const protectedPathSegments = new Set([".git", "node_modules"]);
16
+ const shellCommandSeparators = new Set([";", "&", "&&", "||", "|", "\n", "(", ")", "{", "}"]);
16
17
  const safeEnvSuffixes = new Set(["example", "examples", "template", "templates"]);
17
18
 
18
19
  type GuardDecision = { block: true; reason: string } | undefined;
@@ -22,6 +23,10 @@ type NormalizedPath = {
22
23
  segments: string[];
23
24
  basename: string;
24
25
  };
26
+ type ShellToken = {
27
+ text: string;
28
+ quote?: '"' | "'";
29
+ };
25
30
 
26
31
  type WriteInput = { path: string; content: string };
27
32
  type EditInput = { path: string; edits: Array<{ oldText: string; newText: string }> };
@@ -73,6 +78,452 @@ function isProtectedPath(normalizedPath: NormalizedPath): boolean {
73
78
  return isProtectedEnvFile(normalizedPath.basename);
74
79
  }
75
80
 
81
+ function findClosingDelimiter(command: string, start: number, delimiter: ')' | '`'): number {
82
+ let quote: '"' | "'" | undefined;
83
+ let escaping = false;
84
+ let depth = delimiter === ')' ? 1 : 0;
85
+
86
+ for (let index = start; index < command.length; index += 1) {
87
+ const char = command[index];
88
+ const next = command[index + 1];
89
+
90
+ if (escaping) {
91
+ escaping = false;
92
+ continue;
93
+ }
94
+
95
+ if (quote) {
96
+ if (char === "\\" && quote === '"') {
97
+ escaping = true;
98
+ continue;
99
+ }
100
+ if (char === quote) {
101
+ quote = undefined;
102
+ }
103
+ continue;
104
+ }
105
+
106
+ if (char === "\\") {
107
+ escaping = true;
108
+ continue;
109
+ }
110
+
111
+ if (char === '"' || char === "'") {
112
+ quote = char;
113
+ continue;
114
+ }
115
+
116
+ if (delimiter === ')' && char === '$' && next === '(') {
117
+ depth += 1;
118
+ index += 1;
119
+ continue;
120
+ }
121
+
122
+ if (char === delimiter) {
123
+ if (delimiter === ')') {
124
+ depth -= 1;
125
+ if (depth === 0) return index;
126
+ continue;
127
+ }
128
+ return index;
129
+ }
130
+ }
131
+
132
+ return -1;
133
+ }
134
+
135
+ function tokenizeShellWords(command: string): ShellToken[] {
136
+ // Intentionally shallow tokenization for reviewable safety checks. This handles
137
+ // simple quoting, command separators, and nested command substitutions, but it does
138
+ // not attempt full shell grammar such as heredocs, arrays, or parameter expansion.
139
+ const tokens: ShellToken[] = [];
140
+ let current = "";
141
+ let currentQuote: '"' | "'" | undefined;
142
+ let escaping = false;
143
+
144
+ const flushCurrent = () => {
145
+ if (!current) return;
146
+ tokens.push({ text: current, quote: currentQuote });
147
+ current = "";
148
+ currentQuote = undefined;
149
+ };
150
+
151
+ for (let index = 0; index < command.length; index += 1) {
152
+ const char = command[index];
153
+ const next = command[index + 1];
154
+
155
+ if (escaping) {
156
+ current += char;
157
+ escaping = false;
158
+ continue;
159
+ }
160
+
161
+ if (currentQuote) {
162
+ if (char === "\\" && currentQuote === '"') {
163
+ escaping = true;
164
+ continue;
165
+ }
166
+ if (char === currentQuote) {
167
+ flushCurrent();
168
+ continue;
169
+ }
170
+ current += char;
171
+ continue;
172
+ }
173
+
174
+ if (char === "\\") {
175
+ escaping = true;
176
+ continue;
177
+ }
178
+
179
+ if (char === '"' || char === "'") {
180
+ flushCurrent();
181
+ currentQuote = char;
182
+ continue;
183
+ }
184
+
185
+ if (char === "$" && next === "(") {
186
+ const closingIndex = findClosingDelimiter(command, index + 2, ')');
187
+ if (closingIndex !== -1) {
188
+ flushCurrent();
189
+ tokens.push({ text: command.slice(index, closingIndex + 1) });
190
+ index = closingIndex;
191
+ continue;
192
+ }
193
+ }
194
+
195
+ if (char === "`") {
196
+ const closingIndex = findClosingDelimiter(command, index + 1, '`');
197
+ if (closingIndex !== -1) {
198
+ flushCurrent();
199
+ tokens.push({ text: command.slice(index, closingIndex + 1) });
200
+ index = closingIndex;
201
+ continue;
202
+ }
203
+ }
204
+
205
+ if (char === "&" && next === "&") {
206
+ flushCurrent();
207
+ tokens.push({ text: "&&" });
208
+ index += 1;
209
+ continue;
210
+ }
211
+
212
+ if (char === "|" && next === "|") {
213
+ flushCurrent();
214
+ tokens.push({ text: "||" });
215
+ index += 1;
216
+ continue;
217
+ }
218
+
219
+ if (char === ";" || char === "&" || char === "|" || char === "\n" || char === "(" || char === ")" || char === "{" || char === "}") {
220
+ flushCurrent();
221
+ tokens.push({ text: char });
222
+ continue;
223
+ }
224
+
225
+ if (/\s/.test(char)) {
226
+ flushCurrent();
227
+ continue;
228
+ }
229
+
230
+ current += char;
231
+ }
232
+
233
+ flushCurrent();
234
+ return tokens;
235
+ }
236
+
237
+ function isShellAssignmentToken(token: string): boolean {
238
+ return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
239
+ }
240
+
241
+ function hasDangerousSubstitution(command: string): boolean {
242
+ let quote: '"' | "'" | undefined;
243
+ let escaping = false;
244
+
245
+ for (let index = 0; index < command.length; index += 1) {
246
+ const char = command[index];
247
+ const next = command[index + 1];
248
+
249
+ if (escaping) {
250
+ escaping = false;
251
+ continue;
252
+ }
253
+
254
+ if (quote === "'") {
255
+ if (char === "'") quote = undefined;
256
+ continue;
257
+ }
258
+
259
+ if (char === "\\") {
260
+ escaping = true;
261
+ continue;
262
+ }
263
+
264
+ if (char === "'") {
265
+ quote = "'";
266
+ continue;
267
+ }
268
+
269
+ if (char === '"') {
270
+ quote = quote === '"' ? undefined : '"';
271
+ continue;
272
+ }
273
+
274
+ if (char === '$' && next === '(') {
275
+ const closingIndex = findClosingDelimiter(command, index + 2, ')');
276
+ if (closingIndex !== -1 && hasDangerousRecursiveRm(command.slice(index + 2, closingIndex))) return true;
277
+ if (closingIndex !== -1) index = closingIndex;
278
+ continue;
279
+ }
280
+
281
+ if (char === '`') {
282
+ const closingIndex = findClosingDelimiter(command, index + 1, '`');
283
+ if (closingIndex !== -1 && hasDangerousRecursiveRm(command.slice(index + 1, closingIndex))) return true;
284
+ if (closingIndex !== -1) index = closingIndex;
285
+ }
286
+ }
287
+
288
+ return false;
289
+ }
290
+
291
+ function basenameToken(token: string): string {
292
+ return path.posix.basename(token);
293
+ }
294
+
295
+ function unwrapLeadingWrappers(
296
+ tokens: ShellToken[],
297
+ startIndex: number,
298
+ ): { headIndex: number; nonExecuting: boolean; consumed: number } {
299
+ let index = startIndex;
300
+
301
+ const consumeOptionValue = (inlinePrefix: string, separateOptions: string[], longOption: string): boolean => {
302
+ const token = tokens[index]?.text;
303
+ if (!token) return false;
304
+ if (token === longOption || separateOptions.includes(token)) {
305
+ index += 1;
306
+ if (index < tokens.length) index += 1;
307
+ return true;
308
+ }
309
+ if (token.startsWith(`${longOption}=`) || (inlinePrefix && token.startsWith(inlinePrefix) && token.length > inlinePrefix.length)) {
310
+ index += 1;
311
+ return true;
312
+ }
313
+ return false;
314
+ };
315
+
316
+ const consumeOptionalLongOption = (inlinePrefix: string, shortOption: string, longOption: string): boolean => {
317
+ const token = tokens[index]?.text;
318
+ if (!token) return false;
319
+ if (token === longOption) {
320
+ index += 1;
321
+ return true;
322
+ }
323
+ if (token === shortOption) {
324
+ index += 1;
325
+ if (index < tokens.length) index += 1;
326
+ return true;
327
+ }
328
+ if (token.startsWith(`${longOption}=`) || token.startsWith(inlinePrefix) && token.length > inlinePrefix.length) {
329
+ index += 1;
330
+ return true;
331
+ }
332
+ return false;
333
+ };
334
+
335
+ while (index < tokens.length && isShellAssignmentToken(tokens[index].text)) index += 1;
336
+ while (tokens[index]?.text === "sudo") index += 1;
337
+
338
+ while (index < tokens.length) {
339
+ const current = tokens[index]?.text;
340
+ const currentBase = current ? basenameToken(current) : undefined;
341
+ if (!current) break;
342
+
343
+ if (current === "command") {
344
+ index += 1;
345
+ while (index < tokens.length) {
346
+ const option = tokens[index]?.text;
347
+ if (!option?.startsWith("-")) break;
348
+ if (option === "--") {
349
+ index += 1;
350
+ break;
351
+ }
352
+ if (option === "-v" || option === "-V") {
353
+ return { headIndex: tokens.length, nonExecuting: true, consumed: index + 1 - startIndex };
354
+ }
355
+ index += 1;
356
+ }
357
+ continue;
358
+ }
359
+
360
+ if (currentBase === "env") {
361
+ index += 1;
362
+ while (index < tokens.length) {
363
+ const option = tokens[index]?.text;
364
+ if (!option) break;
365
+ if (option === "--") {
366
+ index += 1;
367
+ break;
368
+ }
369
+ if (option === "--help" || option === "--version") {
370
+ return { headIndex: tokens.length, nonExecuting: true, consumed: index + 1 - startIndex };
371
+ }
372
+ if (isShellAssignmentToken(option)) {
373
+ index += 1;
374
+ continue;
375
+ }
376
+ if (consumeOptionValue("-u", ["-u"], "--unset")) continue;
377
+ if (consumeOptionValue("-C", ["-C"], "--chdir")) continue;
378
+ if (!option.startsWith("-")) break;
379
+ index += 1;
380
+ }
381
+ continue;
382
+ }
383
+
384
+ if (currentBase === "xargs") {
385
+ index += 1;
386
+ while (index < tokens.length) {
387
+ const option = tokens[index]?.text;
388
+ if (!option) break;
389
+ if (option === "--") {
390
+ index += 1;
391
+ break;
392
+ }
393
+ if (option === "--help" || option === "--version") {
394
+ return { headIndex: tokens.length, nonExecuting: true, consumed: index + 1 - startIndex };
395
+ }
396
+ if (consumeOptionValue("-n", ["-n"], "--max-args")) continue;
397
+ if (consumeOptionValue("-P", ["-P"], "--max-procs")) continue;
398
+ if (consumeOptionalLongOption("-I", "-I", "--replace")) continue;
399
+ if (consumeOptionValue("-a", ["-a"], "--arg-file")) continue;
400
+ if (consumeOptionalLongOption("-E", "-E", "--eof")) continue;
401
+ if (consumeOptionValue("-s", ["-s"], "--max-chars")) continue;
402
+ if (consumeOptionValue("-d", ["-d"], "--delimiter")) continue;
403
+ if (consumeOptionalLongOption("-L", "-L", "--max-lines")) continue;
404
+ if (!option.startsWith("-")) break;
405
+ index += 1;
406
+ }
407
+ continue;
408
+ }
409
+
410
+ break;
411
+ }
412
+
413
+ return { headIndex: index, nonExecuting: false, consumed: index - startIndex };
414
+ }
415
+
416
+ function detectRecursiveForceRm(tokens: ShellToken[], headIndex: number): boolean {
417
+ const commandName = tokens[headIndex];
418
+ if (!commandName || basenameToken(commandName.text) !== "rm") return false;
419
+
420
+ let sawRecursive = false;
421
+ let sawForce = false;
422
+
423
+ for (let index = headIndex + 1; index < tokens.length; index += 1) {
424
+ const token = tokens[index]?.text;
425
+ if (!token) continue;
426
+ if (token === "--") break;
427
+ if (!token.startsWith("-") || token === "-") continue;
428
+ if (token === "--recursive") sawRecursive = true;
429
+ if (token === "--force") sawForce = true;
430
+ if (/^-[^-]+$/.test(token)) {
431
+ const flags = token.slice(1);
432
+ if (flags.includes("r") || flags.includes("R")) sawRecursive = true;
433
+ if (flags.includes("f")) sawForce = true;
434
+ }
435
+ }
436
+
437
+ return sawRecursive && sawForce;
438
+ }
439
+
440
+ function hasDangerousCommandHead(tokens: ShellToken[], headIndex: number): boolean {
441
+ if (detectRecursiveForceRm(tokens, headIndex)) return true;
442
+
443
+ const firstToken = tokens[headIndex];
444
+ if (!firstToken) return false;
445
+
446
+ if (["sh", "bash", "zsh", "dash", "ksh"].includes(basenameToken(firstToken.text))) {
447
+ for (let index = headIndex + 1; index < tokens.length; index += 1) {
448
+ const token = tokens[index]?.text;
449
+ if (!token) break;
450
+ if (token === "--") break;
451
+ if (token === "-c" || (/^-[A-Za-z]+$/.test(token) && token.includes("c"))) {
452
+ const script = tokens[index + 1]?.text;
453
+ return typeof script === "string" && hasDangerousRecursiveRm(script);
454
+ }
455
+ if (!token.startsWith("-")) break;
456
+ }
457
+ }
458
+
459
+ if (firstToken.text === "eval") {
460
+ return headIndex + 1 < tokens.length && hasDangerousRecursiveRm(tokens.slice(headIndex + 1).map(({ text }) => text).join(" "));
461
+ }
462
+
463
+ return false;
464
+ }
465
+
466
+ function hasDangerousCommandSuffix(tokens: ShellToken[]): boolean {
467
+ let index = 0;
468
+
469
+ while (index < tokens.length) {
470
+ const unwrapped = unwrapLeadingWrappers(tokens, index);
471
+ if (unwrapped.nonExecuting) return false;
472
+ if (hasDangerousCommandHead(tokens, unwrapped.headIndex)) return true;
473
+
474
+ // Check every possible executable head once, while skipping known-wrapper
475
+ // option values so data such as an xargs EOF marker is never treated as a command.
476
+ index += unwrapped.consumed + 1;
477
+ }
478
+
479
+ return false;
480
+ }
481
+
482
+ function hasDangerousRecursiveRm(command: string): boolean {
483
+ // Conservative detector: recurse through obvious command-substitution and shell-wrapper
484
+ // surfaces, then inspect argv-like tokens for rm plus recursive+force semantics.
485
+ if (hasDangerousSubstitution(command)) return true;
486
+
487
+ const tokens = tokenizeShellWords(command);
488
+ let currentCommand: ShellToken[] = [];
489
+ let findExecCommand: ShellToken[] | undefined;
490
+
491
+ const evaluateTokens = (commandTokens: ShellToken[]): boolean => hasDangerousCommandSuffix(commandTokens);
492
+
493
+ const resetCurrentCommand = () => {
494
+ currentCommand = [];
495
+ };
496
+
497
+ for (const token of tokens) {
498
+ if (findExecCommand) {
499
+ if (token.text === ";" || token.text === "+") {
500
+ if (evaluateTokens(findExecCommand)) return true;
501
+ findExecCommand = undefined;
502
+ resetCurrentCommand();
503
+ continue;
504
+ }
505
+ findExecCommand.push(token);
506
+ continue;
507
+ }
508
+
509
+ if (token.text === "-exec" || token.text === "-execdir") {
510
+ findExecCommand = [];
511
+ continue;
512
+ }
513
+
514
+ if (shellCommandSeparators.has(token.text)) {
515
+ if (evaluateTokens(currentCommand)) return true;
516
+ resetCurrentCommand();
517
+ continue;
518
+ }
519
+
520
+ currentCommand.push(token);
521
+ }
522
+
523
+ if (findExecCommand && evaluateTokens(findExecCommand)) return true;
524
+ return evaluateTokens(currentCommand);
525
+ }
526
+
76
527
  function validateWriteInput(input: unknown): WriteInput | undefined {
77
528
  if (!isRecord(input)) return undefined;
78
529
  if (typeof input.path !== "string" || typeof input.content !== "string") return undefined;
@@ -131,7 +582,7 @@ export default function (pi: ExtensionAPI) {
131
582
  return { block: true, reason: "Malformed bash command blocked" };
132
583
  }
133
584
 
134
- const isDangerous = dangerousPatterns.some((p) => p.test(command));
585
+ const isDangerous = hasDangerousRecursiveRm(command) || dangerousPatterns.some((p) => p.test(command));
135
586
 
136
587
  if (isDangerous) {
137
588
  if (!ctx.hasUI) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-permission-gate",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "A pi extension that prompts before dangerous bash commands and protected file writes.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -31,5 +31,9 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@earendil-works/pi-coding-agent": "*"
34
+ },
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=22.19.0"
34
38
  }
35
39
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-quiet-tools",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A pi extension that visually compacts collapsed built-in tool rows in the TUI without changing tool results sent to the model.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -31,5 +31,9 @@
31
31
  "peerDependencies": {
32
32
  "@earendil-works/pi-coding-agent": "*",
33
33
  "@earendil-works/pi-tui": "*"
34
+ },
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=22.19.0"
34
38
  }
35
39
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -1996,36 +1996,71 @@ Instructions:
1996
1996
  pi.appendEntry(REVIEW_STATE_TYPE, { active: false });
1997
1997
  }
1998
1998
 
1999
+ type SummaryNavigationResult =
2000
+ | { status: "success" }
2001
+ | { status: "cancelled" }
2002
+ | { status: "late-success" }
2003
+ | { status: "error"; error: string };
2004
+
1999
2005
  async function navigateWithSummary(
2000
2006
  ctx: ExtensionCommandContext,
2001
2007
  originId: string,
2002
2008
  showLoader: boolean,
2003
- ): Promise<{ cancelled: boolean; error?: string } | null> {
2009
+ ): Promise<SummaryNavigationResult> {
2004
2010
  if (showLoader && ctx.mode === "tui") {
2005
- return ctx.ui.custom<{ cancelled: boolean; error?: string } | null>((tui, theme, _kb, done) => {
2011
+ return ctx.ui.custom<SummaryNavigationResult>((tui, theme, _kb, done) => {
2006
2012
  const loader = new BorderedLoader(tui, theme, "Returning and summarizing review branch...");
2007
- loader.onAbort = () => done(null);
2013
+ let finished = false;
2014
+ let abortRequested = false;
2015
+
2016
+ const finish = (result: SummaryNavigationResult) => {
2017
+ if (finished) return;
2018
+ finished = true;
2019
+ done(result);
2020
+ };
2008
2021
 
2009
- ctx.navigateTree(originId, {
2010
- summarize: true,
2011
- customInstructions: REVIEW_SUMMARY_PROMPT,
2012
- replaceInstructions: true,
2013
- })
2014
- .then(done)
2015
- .catch((err) => done({ cancelled: false, error: err instanceof Error ? err.message : String(err) }));
2022
+ const navigationPromise = ctx
2023
+ .navigateTree(originId, {
2024
+ summarize: true,
2025
+ customInstructions: REVIEW_SUMMARY_PROMPT,
2026
+ replaceInstructions: true,
2027
+ })
2028
+ .then((result) => {
2029
+ finish(
2030
+ result.cancelled
2031
+ ? { status: "cancelled" }
2032
+ : abortRequested
2033
+ ? { status: "late-success" }
2034
+ : { status: "success" },
2035
+ );
2036
+ })
2037
+ .catch((err) => {
2038
+ finish({ status: "error", error: err instanceof Error ? err.message : String(err) });
2039
+ });
2040
+
2041
+ loader.onAbort = () => {
2042
+ if (abortRequested) return;
2043
+ abortRequested = true;
2044
+ void Promise.resolve()
2045
+ .then(() => ctx.abort())
2046
+ .catch(() => undefined)
2047
+ .then(() => navigationPromise)
2048
+ .catch(() => undefined);
2049
+ };
2016
2050
 
2017
2051
  return loader;
2018
2052
  });
2019
2053
  }
2020
2054
 
2021
2055
  try {
2022
- return await ctx.navigateTree(originId, {
2056
+ const result = await ctx.navigateTree(originId, {
2023
2057
  summarize: true,
2024
2058
  customInstructions: REVIEW_SUMMARY_PROMPT,
2025
2059
  replaceInstructions: true,
2026
2060
  });
2061
+ return result.cancelled ? { status: "cancelled" } : { status: "success" };
2027
2062
  } catch (error) {
2028
- return { cancelled: false, error: error instanceof Error ? error.message : String(error) };
2063
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
2029
2064
  }
2030
2065
  }
2031
2066
 
@@ -2064,22 +2099,21 @@ Instructions:
2064
2099
  }
2065
2100
 
2066
2101
  const summaryResult = await navigateWithSummary(ctx, originId, options.showSummaryLoader ?? false);
2067
- if (summaryResult === null) {
2068
- ctx.ui.notify("Summarization cancelled. Use /end-review to try again.", "info");
2069
- return "cancelled";
2070
- }
2071
-
2072
- if (summaryResult.error) {
2102
+ if (summaryResult.status === "error") {
2073
2103
  ctx.ui.notify(`Summarization failed: ${summaryResult.error}`, "error");
2074
2104
  return "error";
2075
2105
  }
2076
2106
 
2077
- if (summaryResult.cancelled) {
2107
+ if (summaryResult.status === "cancelled") {
2078
2108
  ctx.ui.notify("Navigation cancelled. Use /end-review to try again.", "info");
2079
2109
  return "cancelled";
2080
2110
  }
2081
2111
 
2082
2112
  clearReviewState(ctx);
2113
+ if (summaryResult.status === "late-success") {
2114
+ ctx.ui.notify("Cancellation arrived too late; review already completed, and no follow-up was queued.", "warning");
2115
+ return "ok";
2116
+ }
2083
2117
 
2084
2118
  if (action === "returnAndSummarize") {
2085
2119
  if (!ctx.ui.getEditorText().trim()) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-review",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "A standalone pi extension that adds /review and /end-review commands adapted from mitsuhiko/agent-stuff.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -33,5 +33,9 @@
33
33
  "peerDependencies": {
34
34
  "@earendil-works/pi-coding-agent": "*",
35
35
  "@earendil-works/pi-tui": "*"
36
+ },
37
+ "type": "module",
38
+ "engines": {
39
+ "node": ">=22.19.0"
36
40
  }
37
41
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1
@@ -2,7 +2,7 @@
2
2
 
3
3
  A pi extension that adds a branch-aware todo list managed by the agent.
4
4
 
5
- This started from the original `todo.ts` example in [`earendil-works/pi-mono`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/examples/extensions/todo.ts), with small packaging and snapshot-safety tweaks.
5
+ This started from the original `todo.ts` example in [`earendil-works/pi`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/todo.ts), with small packaging and snapshot-safety tweaks.
6
6
 
7
7
  ## What it adds
8
8
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-todo",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A pi extension that adds a branch-aware todo tool and /todos viewer.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -33,5 +33,9 @@
33
33
  "@earendil-works/pi-coding-agent": "*",
34
34
  "@earendil-works/pi-tui": "*",
35
35
  "typebox": "*"
36
+ },
37
+ "type": "module",
38
+ "engines": {
39
+ "node": ">=22.19.0"
36
40
  }
37
41
  }
@@ -1 +1 @@
1
- 0.80.6
1
+ 0.81.1