@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.
- package/.pi-fleet-tested-version +1 -1
- package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
- package/extensions/agent-workflow-audit/index.ts +8 -1
- package/extensions/agent-workflow-audit/package.json +5 -1
- package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-git-diff/package.json +5 -2
- package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-last-message/package.json +5 -2
- package/extensions/brrr/.pi-fleet-tested-version +1 -1
- package/extensions/brrr/README.md +2 -2
- package/extensions/brrr/package.json +5 -1
- package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
- package/extensions/claude-fast/package.json +5 -1
- package/extensions/code-reviewer/.pi-fleet-tested-version +1 -1
- package/extensions/code-reviewer/index.ts +129 -39
- package/extensions/code-reviewer/package.json +5 -1
- package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +20 -5
- package/extensions/confirm-destructive/package.json +5 -1
- package/extensions/context-cap/.pi-fleet-tested-version +1 -1
- package/extensions/context-cap/package.json +5 -1
- package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
- package/extensions/context-inspector/package.json +5 -1
- package/extensions/contrarian/.pi-fleet-tested-version +1 -1
- package/extensions/contrarian/README.md +1 -1
- package/extensions/contrarian/index.ts +199 -40
- package/extensions/contrarian/package.json +5 -1
- package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
- package/extensions/dirty-repo-guard/README.md +1 -1
- package/extensions/dirty-repo-guard/package.json +5 -1
- package/extensions/dynamic-context-pruning/.pi-fleet-tested-version +1 -1
- package/extensions/dynamic-context-pruning/package.json +4 -1
- package/extensions/git-footer/.pi-fleet-tested-version +1 -1
- package/extensions/git-footer/package.json +5 -1
- package/extensions/gnosis/.pi-fleet-tested-version +1 -1
- package/extensions/gnosis/package.json +5 -1
- package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
- package/extensions/illustrations-to-explain-things/package.json +4 -1
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.webp +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.webp +0 -0
- package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
- package/extensions/inline-bash/README.md +1 -1
- package/extensions/inline-bash/package.json +5 -1
- package/extensions/librarian/.pi-fleet-tested-version +1 -1
- package/extensions/librarian/index.ts +393 -107
- package/extensions/librarian/package.json +5 -1
- package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
- package/extensions/minimal-footer/index.ts +2 -5
- package/extensions/minimal-footer/openai-usage.ts +58 -11
- package/extensions/minimal-footer/package.json +5 -1
- package/extensions/notify/.pi-fleet-tested-version +1 -1
- package/extensions/notify/README.md +3 -3
- package/extensions/notify/package.json +5 -1
- package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
- package/extensions/openai-fast/package.json +5 -1
- package/extensions/oracle/.pi-fleet-tested-version +1 -1
- package/extensions/oracle/README.md +1 -1
- package/extensions/oracle/index.ts +200 -41
- package/extensions/oracle/package.json +5 -1
- package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
- package/extensions/permission-gate/README.md +1 -1
- package/extensions/permission-gate/index.ts +453 -2
- package/extensions/permission-gate/package.json +5 -1
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/package.json +5 -1
- package/extensions/review/.pi-fleet-tested-version +1 -1
- package/extensions/review/index.ts +53 -19
- package/extensions/review/package.json +5 -1
- package/extensions/todo/.pi-fleet-tested-version +1 -1
- package/extensions/todo/README.md +1 -1
- package/extensions/todo/package.json +5 -1
- package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
- package/extensions/triage-comments/index.ts +115 -34
- package/extensions/triage-comments/package.json +5 -1
- package/package.json +10 -6
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.png +0 -0
- 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 = [/\
|
|
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.
|
|
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.
|
|
1
|
+
0.81.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-quiet-tools",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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<
|
|
2009
|
+
): Promise<SummaryNavigationResult> {
|
|
2004
2010
|
if (showLoader && ctx.mode === "tui") {
|
|
2005
|
-
return ctx.ui.custom<
|
|
2011
|
+
return ctx.ui.custom<SummaryNavigationResult>((tui, theme, _kb, done) => {
|
|
2006
2012
|
const loader = new BorderedLoader(tui, theme, "Returning and summarizing review branch...");
|
|
2007
|
-
|
|
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
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
.
|
|
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
|
-
|
|
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 {
|
|
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 ===
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
1
|
+
0.81.1
|