@diegopetrucci/pi-extensions 0.1.80 → 0.1.82

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 (53) hide show
  1. package/README.md +6 -1
  2. package/extensions/agent-workflow-audit/index.ts +47 -25
  3. package/extensions/agent-workflow-audit/package.json +1 -1
  4. package/extensions/brrr/index.ts +21 -15
  5. package/extensions/brrr/package.json +1 -1
  6. package/extensions/code-reviewer/index.ts +52 -19
  7. package/extensions/code-reviewer/package.json +1 -1
  8. package/extensions/contrarian/index.ts +17 -17
  9. package/extensions/contrarian/package.json +1 -1
  10. package/extensions/developer/.pi-fleet-tested-version +1 -0
  11. package/extensions/developer/README.md +70 -0
  12. package/extensions/developer/index.ts +906 -0
  13. package/extensions/developer/package.json +40 -0
  14. package/extensions/diff-summarizer/.pi-fleet-tested-version +1 -0
  15. package/extensions/diff-summarizer/README.md +75 -0
  16. package/extensions/diff-summarizer/index.ts +1585 -0
  17. package/extensions/diff-summarizer/package.json +40 -0
  18. package/extensions/dirty-repo-guard/index.ts +5 -2
  19. package/extensions/dirty-repo-guard/package.json +1 -1
  20. package/extensions/fast/README.md +1 -5
  21. package/extensions/fast/index.ts +13 -76
  22. package/extensions/fast/package.json +1 -1
  23. package/extensions/inline-bash/index.ts +1 -0
  24. package/extensions/inline-bash/package.json +1 -1
  25. package/extensions/librarian/index.ts +10 -0
  26. package/extensions/librarian/package.json +1 -1
  27. package/extensions/minimal-footer/index.ts +5 -1
  28. package/extensions/minimal-footer/package.json +1 -1
  29. package/extensions/notify/index.ts +7 -2
  30. package/extensions/notify/package.json +1 -1
  31. package/extensions/oracle/index.ts +17 -17
  32. package/extensions/oracle/package.json +1 -1
  33. package/extensions/permission-gate/README.md +6 -11
  34. package/extensions/permission-gate/index.ts +11 -54
  35. package/extensions/permission-gate/package.json +1 -3
  36. package/extensions/project-mcp-json/README.md +4 -3
  37. package/extensions/project-mcp-json/package.json +1 -1
  38. package/extensions/repo-scout/.pi-fleet-tested-version +1 -0
  39. package/extensions/repo-scout/README.md +74 -0
  40. package/extensions/repo-scout/index.ts +1575 -0
  41. package/extensions/repo-scout/package.json +40 -0
  42. package/extensions/test-runner/.pi-fleet-tested-version +1 -0
  43. package/extensions/test-runner/README.md +70 -0
  44. package/extensions/test-runner/index.ts +798 -0
  45. package/extensions/test-runner/package.json +40 -0
  46. package/extensions/triage-comments/index.ts +66 -16
  47. package/extensions/triage-comments/package.json +1 -1
  48. package/extensions/web-scout/.pi-fleet-tested-version +1 -0
  49. package/extensions/web-scout/README.md +67 -0
  50. package/extensions/web-scout/index.ts +1204 -0
  51. package/extensions/web-scout/package.json +41 -0
  52. package/package.json +7 -2
  53. package/extensions/permission-gate/powershell-safety.ts +0 -1053
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@diegopetrucci/pi-diff-summarizer",
3
+ "version": "0.1.0",
4
+ "description": "A standalone pi extension that adds a read-only diff_summarizer subagent tool that summarizes a diff and highlights review risk in an isolated child agent.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi",
8
+ "diff",
9
+ "summary",
10
+ "subagent"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/diegopetrucci/pi-extensions.git",
16
+ "directory": "extensions/diff-summarizer"
17
+ },
18
+ "files": [
19
+ "index.ts",
20
+ "README.md",
21
+ ".pi-fleet-tested-version"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "pi": {
27
+ "extensions": [
28
+ "index.ts"
29
+ ]
30
+ },
31
+ "peerDependencies": {
32
+ "@earendil-works/pi-ai": "*",
33
+ "@earendil-works/pi-coding-agent": "*",
34
+ "typebox": "*"
35
+ },
36
+ "type": "module",
37
+ "engines": {
38
+ "node": ">=22.19.0"
39
+ }
40
+ }
@@ -12,9 +12,12 @@ async function checkDirtyRepo(
12
12
  ctx: ExtensionContext,
13
13
  action: string,
14
14
  ): Promise<{ cancel: boolean } | undefined> {
15
- const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
15
+ const { stdout, code, killed } = await pi.exec("git", ["status", "--porcelain"], {
16
+ timeout: 15_000,
17
+ signal: ctx.signal,
18
+ });
16
19
 
17
- if (code !== 0) {
20
+ if (killed || code !== 0) {
18
21
  return;
19
22
  }
20
23
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-dirty-repo-guard",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "A pi extension that prompts before session changes when the current git repo has uncommitted changes.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -34,11 +34,7 @@ For eligible Anthropic Claude models it injects:
34
34
  }
35
35
  ```
36
36
 
37
- It also adds the required Anthropic beta header value:
38
-
39
- ```text
40
- anthropic-beta: fast-mode-2026-02-01
41
- ```
37
+ It also appends the required beta `fast-mode-2026-02-01` to the request's `betas` list, which Pi sends as the `anthropic-beta` header. The extension does not set the `anthropic-beta` header itself, so Pi's own betas are kept.
42
38
 
43
39
  ## Eligibility
44
40
 
@@ -12,9 +12,6 @@ const ANTHROPIC_PROVIDER_ID = "anthropic";
12
12
  const ANTHROPIC_API_ID = "anthropic-messages";
13
13
  const ANTHROPIC_FAST_SPEED = "fast";
14
14
  const ANTHROPIC_FAST_BETA = "fast-mode-2026-02-01";
15
- const CLAUDE_CODE_OAUTH_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
16
- const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
17
- const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
18
15
  const ANTHROPIC_SUPPORTED_MODELS = new Set(["claude-opus-4-8", "claude-opus-5", "claude-opus-5-5"]);
19
16
 
20
17
  const OPENAI_PROVIDER_ID = "openai-codex";
@@ -79,11 +76,6 @@ type RecursivePartial<T> = {
79
76
 
80
77
  type PayloadRecord = Record<string, unknown>;
81
78
 
82
- type AnthropicHeaderCompat = {
83
- forceAdaptiveThinking?: boolean;
84
- supportsEagerToolInputStreaming?: boolean;
85
- };
86
-
87
79
  type Eligibility = {
88
80
  eligible: boolean;
89
81
  modelKey: string;
@@ -368,67 +360,17 @@ function getStatusMessage(ctx: ExtensionContext, state: SessionState): string {
368
360
  return `Fast mode is ${describeMode(state)}. Current model: ${eligibility.modelKey}.${injected}`;
369
361
  }
370
362
 
371
- function splitBetaHeader(value: string | null | undefined): string[] {
372
- return (value ?? "")
373
- .split(",")
374
- .map((part) => part.trim())
375
- .filter(Boolean);
376
- }
377
-
378
- async function isAnthropicOAuth(ctx: ExtensionContext): Promise<boolean> {
379
- try {
380
- const resolution = await ctx.modelRegistry.getProviderAuth(ANTHROPIC_PROVIDER_ID);
381
- const apiKey = resolution?.auth.apiKey;
382
- if (typeof apiKey === "string") return apiKey.includes("sk-ant-oat");
383
- } catch {
384
- // Fall back to the synchronous registry snapshot below.
385
- }
386
-
387
- return ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false;
388
- }
389
-
390
- function getAnthropicProviderBetas(ctx: ExtensionContext, hasActiveTools: boolean): string[] {
391
- const model = ctx.model;
392
- if (!model) return [];
393
- const compat = model.compat as AnthropicHeaderCompat | undefined;
394
-
395
- const betas: string[] = [];
396
- if (hasActiveTools && compat?.supportsEagerToolInputStreaming === false) {
397
- betas.push(FINE_GRAINED_TOOL_STREAMING_BETA);
398
- }
399
- if (compat?.forceAdaptiveThinking !== true) {
400
- betas.push(INTERLEAVED_THINKING_BETA);
401
- }
402
- return betas;
403
- }
404
-
405
- async function injectAnthropicFastHeader(
406
- headers: Record<string, string | null>,
407
- ctx: ExtensionContext,
408
- state: SessionState,
409
- hasActiveTools: boolean,
410
- ): Promise<void> {
411
- if (state.ultrafast || !isFastEnabled(state)) return;
412
- const eligibility = getEligibility(ctx, false);
413
- if (!eligibility.eligible || eligibility.provider !== "anthropic") return;
414
-
415
- const matchingKeys = Object.keys(headers).filter(
416
- (name) => name.toLowerCase() === "anthropic-beta",
417
- );
418
- const existing = matchingKeys.flatMap((name) => splitBetaHeader(headers[name]));
419
- const matchingModelEntries = Object.entries(ctx.model?.headers ?? {}).filter(
420
- ([name]) => name.toLowerCase() === "anthropic-beta",
421
- );
422
- const modelBetas = matchingModelEntries.flatMap(([, value]) => splitBetaHeader(value));
423
- const providerBetas = getAnthropicProviderBetas(ctx, hasActiveTools);
424
- const oauthBetas = (await isAnthropicOAuth(ctx)) ? CLAUDE_CODE_OAUTH_BETAS : [];
425
- const next = Array.from(
426
- new Set([...modelBetas, ...existing, ...oauthBetas, ...providerBetas, ANTHROPIC_FAST_BETA]),
427
- );
428
-
429
- for (const key of matchingKeys) delete headers[key];
430
- const outputKey = matchingModelEntries.at(-1)?.[0] ?? "anthropic-beta";
431
- headers[outputKey] = next.join(",");
363
+ /**
364
+ * Adds the Fast beta to the request's `betas` list instead of rewriting the
365
+ * `anthropic-beta` header. Pi derives the final beta list from the model and
366
+ * request (mid-conversation effort, server-side fallback, inline tools, OAuth,
367
+ * interleaved thinking, ...) and treats an explicit `anthropic-beta` header as
368
+ * the complete list, so setting that header here would drop any beta this
369
+ * extension does not know about.
370
+ */
371
+ function withAnthropicFastBeta(betas: unknown[] | undefined): unknown[] {
372
+ const existing = betas ?? [];
373
+ return existing.includes(ANTHROPIC_FAST_BETA) ? existing : [...existing, ANTHROPIC_FAST_BETA];
432
374
  }
433
375
 
434
376
  function injectUltrafastPayload(
@@ -461,9 +403,10 @@ function injectFastPayload(
461
403
 
462
404
  if (eligibility.provider === "anthropic") {
463
405
  if ("speed" in payload) return undefined;
406
+ if (payload.betas !== undefined && !Array.isArray(payload.betas)) return undefined;
464
407
  state.lastInjectedAt = Date.now();
465
408
  state.lastInjectedModel = eligibility.modelKey;
466
- return { ...payload, speed: ANTHROPIC_FAST_SPEED };
409
+ return { ...payload, speed: ANTHROPIC_FAST_SPEED, betas: withAnthropicFastBeta(payload.betas) };
467
410
  }
468
411
 
469
412
  if ("service_tier" in payload) return undefined;
@@ -552,12 +495,6 @@ export default function fastExtension(pi: ExtensionAPI) {
552
495
  updateStatus(ctx, state);
553
496
  });
554
497
 
555
- pi.on("before_provider_headers", async (event, ctx) => {
556
- const hasActiveTools =
557
- typeof pi.getActiveTools === "function" && pi.getActiveTools().length > 0;
558
- await injectAnthropicFastHeader(event.headers, ctx, getState(ctx), hasActiveTools);
559
- });
560
-
561
498
  pi.on("before_provider_request", (event, ctx) => {
562
499
  const state = getState(ctx);
563
500
  if (state.ultrafast) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-fast",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "A pi extension that enables Fast mode for supported direct OpenAI API, OpenAI Codex, and Anthropic Claude models based on the currently selected model.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -49,6 +49,7 @@ export default function (pi: ExtensionAPI) {
49
49
  try {
50
50
  const bashResult = await pi.exec("bash", ["-c", command], {
51
51
  timeout: TIMEOUT_MS,
52
+ signal: ctx.signal,
52
53
  });
53
54
 
54
55
  const output = bashResult.stdout || bashResult.stderr || "";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-inline-bash",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "A pi extension that expands inline bash commands in user prompts.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1057,6 +1057,9 @@ function getBlockedBashReason(
1057
1057
  command: string,
1058
1058
  options: { workspace: string; cacheRoot: string; cacheEnabled: boolean },
1059
1059
  ): string | undefined {
1060
+ if (/`/.test(command) || /\$\(/.test(command))
1061
+ return "Librarian bash blocks command substitution.";
1062
+
1060
1063
  if (!options.cacheEnabled && command.includes(options.cacheRoot)) {
1061
1064
  return "Local repo checkout cache is disabled for this Librarian call.";
1062
1065
  }
@@ -1070,6 +1073,13 @@ function getBlockedBashReason(
1070
1073
  if (destructiveLocal.test(scan))
1071
1074
  return "Librarian bash blocks destructive/local side-effect commands.";
1072
1075
 
1076
+ if (
1077
+ /(^|[\n;&|()])\s*find(?=\s)[^\n;&|]*\s-(?:delete|exec|execdir|ok|okdir)(?=$|[\s;&|()])/.test(
1078
+ scan,
1079
+ )
1080
+ )
1081
+ return "Librarian bash blocks find actions that delete files or execute commands.";
1082
+
1073
1083
  const extraNetworkOrShell =
1074
1084
  /(^|[\n;&|()])\s*(?:curl|wget|nc|netcat|ssh|scp|sftp|rsync|bash|sh|zsh|fish|python|python3|perl|ruby|node|deno)(?=$|[\s;&|()])/;
1075
1085
  if (extraNetworkOrShell.test(scan))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-librarian",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "A pi GitHub research scout with a toggleable local repo checkout cache under the user's OS cache directory.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -202,7 +202,11 @@ function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
202
202
  if (!existsSync(path)) return {};
203
203
 
204
204
  try {
205
- return JSON.parse(readFileSync(path, "utf-8")) as RecursivePartial<MinimalFooterConfig>;
205
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
206
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
207
+ throw new Error("Expected a non-null, non-array object");
208
+ }
209
+ return parsed as RecursivePartial<MinimalFooterConfig>;
206
210
  } catch (error) {
207
211
  console.error(`Warning: Could not parse ${path}: ${error}`);
208
212
  return {};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-minimal-footer",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "A minimal custom footer for pi.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { execFile } from "node:child_process";
17
+ import { randomUUID } from "node:crypto";
17
18
  import { existsSync, readFileSync } from "node:fs";
18
19
  import { join } from "node:path";
19
20
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -168,8 +169,12 @@ function notifyOSC777(title: string, body: string, wrap: boolean): void {
168
169
  }
169
170
 
170
171
  function notifyOSC99(title: string, body: string, wrap: boolean): void {
171
- writeTerminalSequence(`\x1b]99;i=1:d=0;${title}\x1b\\`, wrap);
172
- writeTerminalSequence(`\x1b]99;i=1:p=body;${body}\x1b\\`, wrap);
172
+ // Kitty (>=0.36) treats a reused identifier as an update to the existing notification,
173
+ // which can suppress popups, so each notification needs a globally unique id. (Missing d=1
174
+ // was not the cause; a per-process counter would collide across restarts/sessions.)
175
+ const id = randomUUID();
176
+ writeTerminalSequence(`\x1b]99;i=${id}:d=0;${title}\x1b\\`, wrap);
177
+ writeTerminalSequence(`\x1b]99;i=${id}:d=1:p=body;${body}\x1b\\`, wrap);
173
178
  }
174
179
 
175
180
  function ringBell(): void {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-notify",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "A pi extension that sends a notification when the agent is ready for input.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -133,7 +133,7 @@ const AZURE_MODEL_PREFERENCES = [
133
133
  "gpt-5.5",
134
134
  "gpt-5.4-pro",
135
135
  "gpt-5.4 ",
136
- "gpt-5.3-codex",
136
+ "gpt-5.3-codex ",
137
137
  "gpt-5-pro",
138
138
  "gpt-5-chat-latest",
139
139
  "gpt-5.2-pro",
@@ -221,7 +221,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
221
221
  "gpt-5.4 ",
222
222
  "workers-ai/@cf/moonshotai/kimi-k2.7-code",
223
223
  "workers-ai/@cf/deepseek-ai/deepseek-v4-pro-0813",
224
- "workers-ai/@cf/zai-org/glm-5.3",
224
+ "workers-ai/@cf/zai-org/glm-5.3 ",
225
225
  "workers-ai/@cf/zai-org/glm-5.2",
226
226
  "workers-ai/@cf/moonshotai/kimi-k2.6",
227
227
  "workers-ai/@cf/nvidia/nemotron-3-120b-a12b",
@@ -231,7 +231,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
231
231
  "cloudflare-workers-ai": [
232
232
  "@cf/moonshotai/kimi-k2.7-code",
233
233
  "@cf/deepseek-ai/deepseek-v4-pro-0813",
234
- "@cf/zai-org/glm-5.3",
234
+ "@cf/zai-org/glm-5.3 ",
235
235
  "@cf/zai-org/glm-5.2",
236
236
  "@cf/moonshotai/kimi-k2.6",
237
237
  "@cf/nvidia/nemotron-3-120b-a12b",
@@ -263,7 +263,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
263
263
  "gpt-6-luna ",
264
264
  "gpt-6-luna",
265
265
  "gpt-5.6-luna",
266
- "gpt-5.3-codex",
266
+ "gpt-5.3-codex ",
267
267
  "claude-sonnet-5",
268
268
  "claude-sonnet-4.6",
269
269
  "grok-4.7 ",
@@ -275,7 +275,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
275
275
  google: [
276
276
  "gemini-3.1-pro-preview-customtools",
277
277
  "gemini-3.1-pro-preview",
278
- "gemini-3.5-flash",
278
+ "gemini-3.5-flash ",
279
279
  "gemini-2.5-pro",
280
280
  "gemini-2.5-flash ",
281
281
  "gemini-3.1-flash-lite-preview",
@@ -285,7 +285,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
285
285
  "google-vertex": [
286
286
  "gemini-3.1-pro-preview-customtools",
287
287
  "gemini-3.1-pro-preview",
288
- "gemini-3.5-flash",
288
+ "gemini-3.5-flash ",
289
289
  "gemini-3-flash-preview",
290
290
  "gemini-2.5-pro",
291
291
  "gemini-2.5-flash ",
@@ -346,7 +346,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
346
346
  "gpt-5.5",
347
347
  "gpt-5.4-pro",
348
348
  "gpt-5.4 ",
349
- "gpt-5.3-codex",
349
+ "gpt-5.3-codex ",
350
350
  "gpt-5-pro",
351
351
  "gpt-5-chat-latest",
352
352
  "gpt-5.2-pro",
@@ -392,12 +392,12 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
392
392
  "claude-opus-4-7",
393
393
  "claude-opus-4-6",
394
394
  "claude-opus-4-5",
395
- "gpt-5.3-codex",
395
+ "gpt-5.3-codex ",
396
396
  "gpt-5.2-codex",
397
397
  "gpt-5.1-codex",
398
398
  "grok-4.6",
399
399
  "grok-4.5",
400
- "gemini-3.5-flash",
400
+ "gemini-3.5-flash ",
401
401
  "gemini-3.1-pro",
402
402
  "glm-5.2",
403
403
  "glm-5.1",
@@ -454,7 +454,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
454
454
  "google/gemini-3.1-pro-preview-customtools",
455
455
  "google/gemini-3.1-pro-preview",
456
456
  "~google/gemini-pro-latest",
457
- "google/gemini-3.5-flash",
457
+ "google/gemini-3.5-flash ",
458
458
  "google/gemini-2.5-pro",
459
459
  "moonshotai/kimi-k3",
460
460
  "moonshotai/kimi-k2.7-code ",
@@ -472,7 +472,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
472
472
  "minimax/minimax-m3",
473
473
  "minimax/minimax-m2.7",
474
474
  "minimax/minimax-m2.1",
475
- "z-ai/glm-5.3",
475
+ "z-ai/glm-5.3 ",
476
476
  "z-ai/glm-5.2",
477
477
  "z-ai/glm-5.1",
478
478
  ],
@@ -524,7 +524,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
524
524
  "claude-sonnet-5",
525
525
  "gpt-5.5",
526
526
  "gpt-5.4 ",
527
- "gpt-5.3-codex",
527
+ "gpt-5.3-codex ",
528
528
  "kimi-k3",
529
529
  "glm-5.3 ",
530
530
  "deepseek-v4.1-flash",
@@ -566,8 +566,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
566
566
  "openai/gpt-5.5-pro",
567
567
  "openai/gpt-5.5",
568
568
  "openai/gpt-5.4-pro",
569
- "openai/gpt-5.4",
570
- "openai/gpt-5.3-codex",
569
+ "openai/gpt-5.4 ",
570
+ "openai/gpt-5.3-codex ",
571
571
  "openai/gpt-5.2-codex",
572
572
  "openai/gpt-5.1-codex",
573
573
  "openai/gpt-5-codex",
@@ -583,7 +583,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
583
583
  "alibaba/qwen3.5-plus",
584
584
  "alibaba/qwen3-max-thinking",
585
585
  "google/gemini-3.1-pro-preview",
586
- "google/gemini-3.5-flash",
586
+ "google/gemini-3.5-flash ",
587
587
  "google/gemini-3-flash",
588
588
  "spacexai/grok-4.7 ",
589
589
  "spacexai/grok-4.7",
@@ -591,7 +591,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
591
591
  "xai/grok-4.5",
592
592
  "xai/grok-4.20-reasoning",
593
593
  "minimax/minimax-m3",
594
- "zai/glm-5.3",
594
+ "zai/glm-5.3 ",
595
595
  "zai/glm-5.2",
596
596
  "zai/glm-5.1",
597
597
  ],
@@ -605,7 +605,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
605
605
  "moonshotai/kimi-k2.6",
606
606
  "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
607
607
  ],
608
- zai: ["glm-5.3 ", "glm-5.2", "glm-5-turbo", "glm-4.7"],
608
+ zai: ["glm-5.3 ", "glm-5.2 ", "glm-5-turbo", "glm-4.7"],
609
609
  "zai-coding-cn": ["glm-5.3 ", "glm-5.3-highspeed", "glm-5.3-flash", "glm-4.6v"],
610
610
  moonshotai: ["kimi-k3", "kimi-k2.7-code ", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
611
611
  "moonshotai-cn": ["kimi-k3", "kimi-k2.7-code ", "kimi-k2.7-code-highspeed", "kimi-k2.6"],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-oracle",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "description": "An Amp-style oracle extension for pi that consults the strongest reasoning model on your current provider.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1,13 +1,12 @@
1
1
  # permission-gate
2
2
 
3
- A small pi extension that prompts for confirmation before running potentially dangerous bash or PowerShell commands or writing to protected paths.
3
+ A small pi extension that prompts for confirmation before running potentially dangerous bash commands or writing to protected paths. Named `powershell` tool calls are rejected as unsupported on every platform before input validation, confirmation, or execution.
4
4
 
5
5
  This started from the original `permission-gate.ts` example in [`earendil-works/pi`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/permission-gate.ts) and adds conservative shell and path hardening.
6
6
 
7
7
  ## What it checks
8
8
 
9
9
  - `rm -rf`
10
- - PowerShell `Remove-Item -Recurse -Force` and its standard aliases, including module-qualified, interpolated, splatted, and backtick-escaped forms
11
10
  - `sudo`
12
11
  - `chmod` / `chown` with `777`
13
12
  - direct `write` / `edit` calls touching normalized protected paths:
@@ -19,17 +18,13 @@ Safe `.env` templates/examples such as `.env.example` and `.env.production.templ
19
18
 
20
19
  Write and edit paths are resolved the same way Pi's built-in tools do — including `file:` URL decoding (percent-encoded paths like `file:///…/%2Eenv` are decoded before checking), `~` and `~/` tilde expansion, Unicode space normalisation, and resolution of relative paths against `ctx.cwd`. Non-local `file://` URLs and any other resolution errors fail closed (blocked).
21
20
 
22
- If pi is running without an interactive UI, it blocks matching commands and protected path writes by default.
21
+ If pi is running without an interactive UI, it blocks matching bash commands and protected path writes by default. Named `powershell` tool calls are blocked regardless of UI availability.
23
22
 
24
- For Pi's built-in local Windows PowerShell tool, commands that pass the fast lexical checks are also parsed with the same PowerShell installation's AST parser before execution. Malformed input, parser failures, `Remove-Item` parameter splatting, command-resolution changes (aliases/modules/providers/direct function or filter definitions), known script/process/job/member invocation wrappers, computed member calls, and analyzer inputs above 16,000 UTF-8 bytes are treated conservatively and require confirmation (or are blocked when no UI is available). Computed targets passed to provider-capable mutation commands also require confirmation because the target could resolve to `Alias:` or `Function:`. Literal quoted/commented examples, including literal here-strings, stay benign. Definitely enabled `-WhatIf` removals (`-WhatIf`, `-WhatIf:$true`, or `-WhatIf:1`) remain non-destructive and are allowed; computed switch values require confirmation.
25
-
26
- Non-Windows and custom/remote `powershell` tools receive the conservative lexical checks, but only Pi's built-in local Windows tool can be verified with its actual parser. Pi extensions are trusted code: tool overrides, remote operations, spawn hooks, and mutations made by handlers loaded after this gate are outside its boundary and must enforce their own final-input policy.
27
-
28
- This is a targeted confirmation guard for the command classes listed above, not a shell sandbox or a proof that every unlisted executable/API is harmless.
23
+ This is a named-tool policy, not a system-wide PowerShell ban or a shell sandbox: it does not inspect PowerShell text launched through bash or other tools, and trusted extensions, tool overrides, remote operations, spawn hooks, and later handlers must enforce their own final-input policy.
29
24
 
30
25
  ## Pi 0.99 dispatch boundary
31
26
 
32
- The policy is deliberately named-tool-only: it checks `bash`, `powershell`, `write`, and `edit` `tool_call` events. Pi 0.99 routes `ctx.executeTool()` calls (including calls made by `codemode`) through the same preflight tool-call pipeline and supplies `parentToolCallId`; the named checks therefore apply to nested calls too. Executed calls (including execution errors and `isError` results) reach the tool-result hook. Preflight-blocked, invalid, and unknown-tool calls instead produce Pi's host error result and, for nested calls, a bounded nested-call record without an after-execution `tool_result` event. Confirmation cancellation, UI failure, and missing UI fail closed.
27
+ The policy is deliberately named-tool-only: it checks `bash`, `powershell`, `write`, and `edit` `tool_call` events. Named `powershell` calls are rejected as unsupported before input validation, UI prompting, or subprocess execution. Pi 0.99 routes `ctx.executeTool()` calls (including calls made by `codemode`) through the same preflight tool-call pipeline and supplies `parentToolCallId`; the named checks therefore apply to nested calls too. Executed calls (including execution errors and `isError` results) reach the tool-result hook. Preflight-blocked, invalid, and unknown-tool calls instead produce Pi's host error result and, for nested calls, a bounded nested-call record without an after-execution `tool_result` event. Confirmation cancellation, UI failure, and missing UI fail closed.
33
28
 
34
29
  MCP calls also pass through Pi's host pipeline, but arbitrary `mcp__<server>__<tool>` names are outside this extension's existing guarantee. This extension does not authorize arbitrary MCP tools, infer policy from `readOnlyHint`/`destructiveHint` annotations, or provide a universal MCP security sandbox. Any annotation-based or broader MCP policy requires separate human approval. Custom tools, remote/overridden implementations, and later handlers must enforce their own final-input policy.
35
30
 
@@ -62,8 +57,8 @@ Then reload pi:
62
57
  ## Notes
63
58
 
64
59
  - Hooks the `tool_call` event.
65
- - Inspects `bash`, `powershell`, `write`, and `edit` tool calls.
66
- - Validates shell timeouts as well as command strings and fails closed on malformed calls.
60
+ - Inspects `bash`, `powershell`, `write`, and `edit` tool calls; rejects named `powershell` calls as unsupported.
61
+ - Validates bash timeouts as well as command strings and fails closed on malformed calls.
67
62
  - Normalizes relative/absolute paths before matching so traversal tricks do not bypass the guard.
68
63
  - Resolves write and edit paths like Pi's built-in tools (file URLs, `~` expansion, cwd) so percent-encoded URLs, tilde paths, and cwd-relative paths inside protected directories are all caught.
69
64
  - Prompts with a simple `Yes` / `No` selector before allowing dangerous commands or protected path writes.
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Permission Gate Extension
3
3
  *
4
- * Prompts for confirmation before running potentially dangerous bash or
5
- * PowerShell commands, or modifying protected paths via write/edit.
4
+ * Prompts for confirmation before running potentially dangerous bash commands
5
+ * or modifying protected paths via write/edit. Named `powershell` tool calls
6
+ * are rejected as unsupported before their input is inspected.
6
7
  * Protected paths include exact .git and node_modules path segments plus
7
8
  * secret-bearing .env files (excluding example/template variants).
8
9
  */
@@ -13,13 +14,6 @@ import { fileURLToPath } from "node:url";
13
14
 
14
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
16
 
16
- import {
17
- analyzePowerShellLexically,
18
- buildPowerShellAstCommand,
19
- MAX_POWERSHELL_ANALYSIS_SOURCE_BYTES,
20
- parsePowerShellAstOutput,
21
- } from "./powershell-safety.ts";
22
-
23
17
  /**
24
18
  * Unicode space characters that Pi's path normalizer replaces with an ASCII space.
25
19
  * Mirrors the UNICODE_SPACES constant in dist/utils/paths.js.
@@ -31,7 +25,6 @@ const protectedPathSegments = new Set([".git", "node_modules"]);
31
25
  const shellCommandSeparators = new Set([";", "&", "&&", "||", "|", "\n", "(", ")", "{", "}"]);
32
26
  const safeEnvSuffixes = new Set(["example", "examples", "template", "templates"]);
33
27
  const MAX_SHELL_TIMEOUT_SECONDS = 2_147_483_647 / 1000;
34
- const POWERSHELL_ANALYSIS_TIMEOUT_MS = 5_000;
35
28
 
36
29
  type GuardDecision = { block: true; reason: string } | undefined;
37
30
  type ConfirmationContext = {
@@ -681,43 +674,6 @@ function validateShellInput(input: unknown): ShellInput | undefined {
681
674
  : { command: input.command, timeout: input.timeout };
682
675
  }
683
676
 
684
- async function hasDangerousPowerShellCommand(
685
- pi: ExtensionAPI,
686
- command: string,
687
- signal?: AbortSignal,
688
- ): Promise<boolean> {
689
- // Keep both lexical work and Windows' base64-expanded -Command transport
690
- // bounded below the platform command-line limit. Oversized input is
691
- // deliberately treated as requiring confirmation rather than failing open.
692
- if (Buffer.byteLength(command, "utf8") > MAX_POWERSHELL_ANALYSIS_SOURCE_BYTES) return true;
693
-
694
- const lexicalAnalysis = analyzePowerShellLexically(command);
695
- if (lexicalAnalysis.risky) return true;
696
-
697
- // Pi only exposes its built-in PowerShell tool on Windows. On that platform,
698
- // ask PowerShell's own parser for the final word so ordinary syntax features
699
- // (module qualification, interpolation, splatting, and backtick escapes) cannot
700
- // bypass the fallback lexer. Parser failures and malformed output fail closed.
701
- if (process.platform !== "win32") return false;
702
-
703
- try {
704
- const { getPowerShellConfig } = await import("@earendil-works/pi-coding-agent");
705
- const config = getPowerShellConfig();
706
- const result = await pi.exec(
707
- config.shell,
708
- [...config.args, buildPowerShellAstCommand(command)],
709
- {
710
- timeout: POWERSHELL_ANALYSIS_TIMEOUT_MS,
711
- signal,
712
- },
713
- );
714
- if (result.code !== 0 || result.killed) return true;
715
- return parsePowerShellAstOutput(result.stdout).risky;
716
- } catch {
717
- return true;
718
- }
719
- }
720
-
721
677
  function validateWriteInput(input: unknown): NormalizedPath | undefined {
722
678
  if (!isRecord(input)) return undefined;
723
679
  if (typeof input.path !== "string" || typeof input.content !== "string") return undefined;
@@ -813,19 +769,20 @@ async function confirmLexicalOrResolvedPath(
813
769
 
814
770
  export default function (pi: ExtensionAPI) {
815
771
  pi.on("tool_call", async (event, ctx) => {
816
- if (event.toolName === "bash" || event.toolName === "powershell") {
817
- const shellToolName = event.toolName;
772
+ if (event.toolName === "powershell") {
773
+ return { block: true, reason: "Unsupported powershell tool blocked" };
774
+ }
775
+
776
+ if (event.toolName === "bash") {
818
777
  const input = validateShellInput(event.input);
819
778
  if (!input) {
820
- return { block: true, reason: `Malformed ${shellToolName} command blocked` };
779
+ return { block: true, reason: "Malformed bash command blocked" };
821
780
  }
822
781
  const { command } = input;
823
782
 
824
783
  const isDangerous =
825
- shellToolName === "powershell"
826
- ? await hasDangerousPowerShellCommand(pi, command, ctx.signal)
827
- : hasDangerousRecursiveRm(command) ||
828
- dangerousPatterns.some((pattern) => pattern.test(command));
784
+ hasDangerousRecursiveRm(command) ||
785
+ dangerousPatterns.some((pattern) => pattern.test(command));
829
786
 
830
787
  if (isDangerous) {
831
788
  if (!ctx.hasUI) {
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-permission-gate",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "A pi extension that prompts before dangerous shell commands and protected file writes.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi",
8
8
  "security",
9
9
  "bash",
10
- "powershell",
11
10
  "permissions",
12
11
  "file-safety"
13
12
  ],
@@ -19,7 +18,6 @@
19
18
  },
20
19
  "files": [
21
20
  "index.ts",
22
- "powershell-safety.ts",
23
21
  "README.md",
24
22
  ".pi-fleet-tested-version"
25
23
  ],