@diegopetrucci/pi-extensions 0.1.76 → 0.1.79

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 (63) hide show
  1. package/README.md +8 -8
  2. package/extensions/agent-workflow-audit/README.md +1 -1
  3. package/extensions/agent-workflow-audit/index.ts +275 -59
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/git.ts +0 -8
  6. package/extensions/annotate-git-diff/glimpseui.d.ts +5 -65
  7. package/extensions/annotate-git-diff/package.json +1 -1
  8. package/extensions/annotate-last-message/glimpseui.d.ts +5 -65
  9. package/extensions/annotate-last-message/package.json +1 -1
  10. package/extensions/annotate-last-message/web/index.html +3 -30
  11. package/extensions/code-reviewer/README.md +5 -5
  12. package/extensions/code-reviewer/index.ts +252 -82
  13. package/extensions/code-reviewer/package.json +1 -1
  14. package/extensions/confirm-destructive/index.ts +2 -8
  15. package/extensions/confirm-destructive/package.json +1 -1
  16. package/extensions/context-cap/README.md +9 -9
  17. package/extensions/context-cap/index.ts +3 -3
  18. package/extensions/context-cap/package.json +2 -2
  19. package/extensions/context-inspector/index.ts +2 -46
  20. package/extensions/context-inspector/package.json +1 -1
  21. package/extensions/contrarian/README.md +4 -3
  22. package/extensions/contrarian/index.ts +48 -67
  23. package/extensions/contrarian/package.json +1 -1
  24. package/extensions/dirty-repo-guard/index.ts +0 -4
  25. package/extensions/dirty-repo-guard/package.json +1 -1
  26. package/extensions/dynamic-context-pruning/README.md +20 -11
  27. package/extensions/dynamic-context-pruning/index.ts +118 -341
  28. package/extensions/dynamic-context-pruning/package.json +1 -1
  29. package/extensions/fast/README.md +18 -11
  30. package/extensions/fast/index.ts +9 -5
  31. package/extensions/fast/package.json +1 -1
  32. package/extensions/git-footer/index.ts +2 -16
  33. package/extensions/git-footer/package.json +1 -1
  34. package/extensions/inline-bash/index.ts +0 -16
  35. package/extensions/inline-bash/package.json +1 -1
  36. package/extensions/librarian/README.md +1 -1
  37. package/extensions/librarian/index.ts +246 -70
  38. package/extensions/librarian/package.json +1 -1
  39. package/extensions/minimal-footer/README.md +5 -5
  40. package/extensions/minimal-footer/index.ts +5 -26
  41. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  42. package/extensions/minimal-footer/openai-usage.ts +2 -11
  43. package/extensions/minimal-footer/package.json +1 -1
  44. package/extensions/oracle/README.md +4 -3
  45. package/extensions/oracle/index.ts +46 -65
  46. package/extensions/oracle/package.json +1 -1
  47. package/extensions/permission-gate/README.md +3 -0
  48. package/extensions/permission-gate/index.ts +127 -36
  49. package/extensions/permission-gate/package.json +1 -1
  50. package/extensions/project-mcp-json/README.md +2 -2
  51. package/extensions/project-mcp-json/package.json +1 -1
  52. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  53. package/extensions/quiet-tools/README.md +30 -19
  54. package/extensions/quiet-tools/index.ts +233 -166
  55. package/extensions/quiet-tools/package.json +2 -2
  56. package/extensions/review/README.md +2 -0
  57. package/extensions/review/package.json +1 -1
  58. package/extensions/todo/index.ts +0 -19
  59. package/extensions/todo/package.json +1 -1
  60. package/extensions/triage-comments/README.md +4 -2
  61. package/extensions/triage-comments/index.ts +344 -39
  62. package/extensions/triage-comments/package.json +1 -1
  63. package/package.json +4 -4
@@ -15,7 +15,6 @@ export type UsageAvailability = "unsupported" | "unavailable";
15
15
 
16
16
  interface WhamUsageWindow {
17
17
  limit_window_seconds?: number;
18
- reset_at?: number;
19
18
  used_percent?: number;
20
19
  }
21
20
 
@@ -29,7 +28,6 @@ interface WhamUsageResponse {
29
28
  export interface UsageWindow {
30
29
  usedPercent?: number;
31
30
  windowSeconds?: number;
32
- resetAt?: number;
33
31
  }
34
32
 
35
33
  export interface UsageSnapshot {
@@ -59,19 +57,12 @@ function normalizeWindowSeconds(value?: number): number | undefined {
59
57
  return value;
60
58
  }
61
59
 
62
- function normalizeResetAt(value?: number): number | undefined {
63
- if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
64
- return value * 1000;
65
- }
66
-
67
60
  function parseUsageWindow(window?: WhamUsageWindow): UsageWindow | undefined {
68
61
  if (!window) return undefined;
69
62
  const usedPercent = normalizeUsedPercent(window.used_percent);
70
63
  const windowSeconds = normalizeWindowSeconds(window.limit_window_seconds);
71
- const resetAt = normalizeResetAt(window.reset_at);
72
- if (usedPercent === undefined && windowSeconds === undefined && resetAt === undefined)
73
- return undefined;
74
- return { usedPercent, windowSeconds, resetAt };
64
+ if (usedPercent === undefined && windowSeconds === undefined) return undefined;
65
+ return { usedPercent, windowSeconds };
75
66
  }
76
67
 
77
68
  function parseUsageSnapshot(data: WhamUsageResponse): Omit<UsageSnapshot, "fetchedAt"> {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-minimal-footer",
3
- "version": "0.1.22",
3
+ "version": "0.1.25",
4
4
  "description": "A minimal custom footer for pi.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -4,11 +4,11 @@
4
4
 
5
5
  An Amp-style oracle for [pi](https://github.com/earendil-works/pi).
6
6
 
7
- It adds an `oracle` tool that spins up a separate read-only pi subprocess and sends it to the strongest reasoning model available on the **same provider/subscription** the user is currently using.
7
+ It adds an `oracle` tool that spins up a separate pi subprocess with read-only built-in tools and any configured MCP tools, and sends it to the strongest reasoning model available on the **same provider/subscription** the user is currently using.
8
8
 
9
9
  ## What it does
10
10
 
11
- - creates an isolated read-only subprocess
11
+ - creates an isolated subprocess with read-only built-in tools (read, grep, find, ls) and any configured MCP tools
12
12
  - auto-picks the strongest reasoning model on the current provider
13
13
  - uses provider-specific hardcoded rankings first, then a heuristic fallback
14
14
  - requests `xhigh` by default for reasoning models, then clamps to the model-supported thinking level
@@ -110,7 +110,8 @@ When the current Pi session has a non-empty model scope, Oracle limits automatic
110
110
 
111
111
  ## Notes
112
112
 
113
- - The oracle is intentionally **read-only by default**.
113
+ - The oracle subprocess uses read-only built-in tools (read, grep, find, ls) by default, plus any configured MCP tools. It can optionally include bash for non-mutating inspection.
114
+ - Configured MCP tools run without confirmation inside the subprocess and may have side effects — use them only for gathering evidence. MCP access requires host Pi >=1.0.4; older hosts degrade to no MCP, no error.
114
115
  - It is best for review, analysis, planning, debugging, and second opinions.
115
116
  - It is slower than using the main model directly, so it should be used selectively.
116
117
  - While it runs in interactive mode, it adds a footer status line and a widget below the editor.
@@ -102,8 +102,15 @@ interface OraclePreferences {
102
102
  thinkingLevel?: ThinkingLevel;
103
103
  }
104
104
 
105
- const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
106
- const READ_ONLY_PLUS_BASH_TOOLS = [...READ_ONLY_TOOLS, "bash"];
105
+ const SUBAGENT_MCP_TOOLS = [
106
+ "mcp__*",
107
+ "tool_search",
108
+ "list_mcp_resources",
109
+ "list_mcp_resource_templates",
110
+ "read_mcp_resource",
111
+ ];
112
+ const READ_ONLY_TOOLS = ["read", "grep", "find", "ls", ...SUBAGENT_MCP_TOOLS];
113
+ const READ_ONLY_PLUS_BASH_TOOLS = ["read", "grep", "find", "ls", "bash", ...SUBAGENT_MCP_TOOLS];
107
114
  const DEFAULT_THINKING_LEVEL: ThinkingLevel = "xhigh";
108
115
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
109
116
  const COLLAPSED_LINE_LIMIT = 8;
@@ -111,6 +118,33 @@ const ORACLE_STATUS_ID = "oracle";
111
118
  const ORACLE_WIDGET_ID = "oracle";
112
119
  const ORACLE_CONFIG_FILE = "oracle.json";
113
120
 
121
+ const AZURE_MODEL_PREFERENCES = [
122
+ "gpt-6-astra ",
123
+ "gpt-6-astra",
124
+ "gpt-6.1-sol ",
125
+ "gpt-6-sol ",
126
+ "gpt-6-sol",
127
+ "gpt-5.6-sol",
128
+ "gpt-5.6-terra",
129
+ "gpt-6-luna ",
130
+ "gpt-6-luna",
131
+ "gpt-5.6-luna",
132
+ "gpt-5.5-pro",
133
+ "gpt-5.5",
134
+ "gpt-5.4-pro",
135
+ "gpt-5.4 ",
136
+ "gpt-5.3-codex",
137
+ "gpt-5-pro",
138
+ "gpt-5-chat-latest",
139
+ "gpt-5.2-pro",
140
+ "gpt-5.2",
141
+ "gpt-5.1",
142
+ "o3-pro",
143
+ "o1-pro",
144
+ "gpt-5.4-mini",
145
+ "gpt-5-mini",
146
+ ];
147
+
114
148
  const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
115
149
  "amazon-bedrock": [
116
150
  "global.anthropic.claude-opus-5-5 ",
@@ -163,32 +197,9 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
163
197
  "claude-sonnet-4-5",
164
198
  ],
165
199
  "ant-ling": ["Ling-2.6-1T", "Ling-2.6-flash"],
166
- "azure-openai-responses": [
167
- "gpt-6-astra ",
168
- "gpt-6-astra",
169
- "gpt-6.1-sol ",
170
- "gpt-6-sol ",
171
- "gpt-6-sol",
172
- "gpt-5.6-sol",
173
- "gpt-5.6-terra",
174
- "gpt-6-luna ",
175
- "gpt-6-luna",
176
- "gpt-5.6-luna",
177
- "gpt-5.5-pro",
178
- "gpt-5.5",
179
- "gpt-5.4-pro",
180
- "gpt-5.4 ",
181
- "gpt-5.3-codex",
182
- "gpt-5-pro",
183
- "gpt-5-chat-latest",
184
- "gpt-5.2-pro",
185
- "gpt-5.2",
186
- "gpt-5.1",
187
- "o3-pro",
188
- "o1-pro",
189
- "gpt-5.4-mini",
190
- "gpt-5-mini",
191
- ],
200
+ azure: AZURE_MODEL_PREFERENCES,
201
+ // Legacy alias for Pi 1.0.0–1.0.2 hosts (renamed to `azure` in Pi 1.0.3).
202
+ "azure-openai-responses": AZURE_MODEL_PREFERENCES,
192
203
  baseten: [
193
204
  "moonshotai/Kimi-K3",
194
205
  "moonshotai/Kimi-K2.7-Code ",
@@ -213,10 +224,6 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
213
224
  "cloudflare-ai-gateway": [
214
225
  "claude-fable-5",
215
226
  "claude-opus-5",
216
- "claude-opus-4.8",
217
- "claude-opus-4.7",
218
- "claude-opus-4.6",
219
- "claude-opus-4.5",
220
227
  "gpt-6-astra ",
221
228
  "gpt-6-astra",
222
229
  "gpt-5.5",
@@ -553,9 +560,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
553
560
  "MiniMaxAI/MiniMax-M3",
554
561
  "MiniMaxAI/MiniMax-M2.7",
555
562
  "openai/gpt-oss-120b",
556
- "openai/gpt-oss-20b",
557
563
  "nvidia/nemotron-3-ultra-550b-a55b",
558
- "google/gemma-4-31B-it",
559
564
  ],
560
565
  typesafe: [],
561
566
  "vercel-ai-gateway": [
@@ -631,11 +636,12 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
631
636
  };
632
637
 
633
638
  const ORACLE_SYSTEM_PROMPT = [
634
- "You are Oracle, a read-only high-reasoning coding assistant.",
639
+ "You are Oracle, a high-reasoning coding assistant with access to read-only built-in tools and any configured MCP tools.",
635
640
  "Your job is analysis, debugging, planning, review, and second opinions.",
636
641
  "Use the available tools to inspect the repository and gather evidence.",
637
642
  "Never claim to have changed files or run mutating actions.",
638
643
  "If bash is available, use it only for non-mutating inspection commands.",
644
+ "Configured MCP tools are available and run without confirmation inside this subagent. Use them only to gather evidence or context. Avoid side-effecting MCP actions unless the task explicitly asks for them.",
639
645
  "Be concrete. Reference file paths, symbols, commands, and risks when helpful.",
640
646
  "Prefer short sections and finish with a concise 'Bottom line' summary.",
641
647
  ].join("\n");
@@ -1681,14 +1687,15 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
1681
1687
  name: "oracle",
1682
1688
  label: "Oracle",
1683
1689
  description:
1684
- "Consult a separate read-only oracle subprocess for deep analysis, code review, debugging, planning, and second opinions.",
1690
+ "Consult a separate oracle subprocess with read-only built-in tools and configured MCP tools for deep analysis, code review, debugging, planning, and second opinions.",
1685
1691
  promptSnippet:
1686
- "Consult a read-only oracle that auto-selects the strongest reasoning model on the current provider/subscription.",
1692
+ "Consult an oracle subprocess that auto-selects the strongest reasoning model on the current provider/subscription; exposes read-only built-in tools plus any configured MCP tools.",
1687
1693
  promptGuidelines: [
1688
1694
  "Use oracle sparingly when you want a second opinion, deeper analysis, code review, debugging help, or a higher-reasoning pass.",
1689
1695
  "Do not use oracle for routine low-value work; oracle is slower than the main agent.",
1690
- "The oracle tool is read-only by default and only exposes read, grep, find, and ls unless oracle includeBash is enabled.",
1696
+ "The oracle tool exposes read-only built-in tools (read, grep, find, and ls) plus any configured MCP tools; oracle includeBash adds the bash inspection tool.",
1691
1697
  "Set oracle includeBash only when the extra bash inspection tool is genuinely useful; keep oracle relying on read, grep, find, and ls otherwise.",
1698
+ "Configured MCP tools are available to oracle and run without confirmation inside the subagent; they may have side effects — use them only for gathering evidence, not for state-changing actions unless the task explicitly asks. MCP access requires host Pi >=1.0.4; older hosts degrade to no MCP, no error.",
1692
1699
  "The oracle tool requests xhigh by default for reasoning models; oracle defaults and explicit thinkingLevel overrides are clamped to the effective model-supported level when the model is matched.",
1693
1700
  ],
1694
1701
  parameters: OracleParams,
@@ -1803,8 +1810,7 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
1803
1810
  onUpdate?.(partial);
1804
1811
  };
1805
1812
 
1806
- let lastErrorResult: { ok: false; error: string; details: OracleDetails } | undefined;
1807
- for (let index = 0; index < attempts.length; index++) {
1813
+ for (let index = 0; ; index++) {
1808
1814
  const previous = index > 0 ? attempts[index - 1] : undefined;
1809
1815
  const attempt = previous
1810
1816
  ? withFallbackReason(attempts[index], previous)
@@ -1828,7 +1834,6 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
1828
1834
  });
1829
1835
  }
1830
1836
 
1831
- lastErrorResult = result;
1832
1837
  const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
1833
1838
  if (!canFallBack) {
1834
1839
  return finalizeResult({
@@ -1837,30 +1842,6 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
1837
1842
  });
1838
1843
  }
1839
1844
  }
1840
-
1841
- return finalizeResult({
1842
- content: [
1843
- {
1844
- type: "text",
1845
- text: lastErrorResult?.error ?? "Oracle could not select an available model.",
1846
- },
1847
- ],
1848
- details: lastErrorResult?.details ?? {
1849
- modelRef: "",
1850
- provider: ctx.model?.provider ?? "unknown",
1851
- modelId: "",
1852
- modelName: undefined,
1853
- thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
1854
- autoSelected: true,
1855
- selectionReason: "No authenticated models were available to run the oracle.",
1856
- includeBash: params.includeBash ?? false,
1857
- usage: createEmptyUsage(),
1858
- stderr: "",
1859
- exitCode: 1,
1860
- durationMs: 0,
1861
- cwd: params.cwd ?? ctx.cwd,
1862
- },
1863
- });
1864
1845
  } finally {
1865
1846
  activeRuns.delete(toolCallId);
1866
1847
  updateOracleUi(ctx, activeRuns);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-oracle",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
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",
@@ -17,6 +17,8 @@ This started from the original `permission-gate.ts` example in [`earendil-works/
17
17
 
18
18
  Safe `.env` templates/examples such as `.env.example` and `.env.production.template` are allowed.
19
19
 
20
+ 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
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.
@@ -63,4 +65,5 @@ Then reload pi:
63
65
  - Inspects `bash`, `powershell`, `write`, and `edit` tool calls.
64
66
  - Validates shell timeouts as well as command strings and fails closed on malformed calls.
65
67
  - Normalizes relative/absolute paths before matching so traversal tricks do not bypass the guard.
68
+ - 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.
66
69
  - Prompts with a simple `Yes` / `No` selector before allowing dangerous commands or protected path writes.
@@ -7,7 +7,9 @@
7
7
  * secret-bearing .env files (excluding example/template variants).
8
8
  */
9
9
 
10
+ import * as os from "node:os";
10
11
  import * as path from "node:path";
12
+ import { fileURLToPath } from "node:url";
11
13
 
12
14
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
15
 
@@ -18,6 +20,12 @@ import {
18
20
  parsePowerShellAstOutput,
19
21
  } from "./powershell-safety.ts";
20
22
 
23
+ /**
24
+ * Unicode space characters that Pi's path normalizer replaces with an ASCII space.
25
+ * Mirrors the UNICODE_SPACES constant in dist/utils/paths.js.
26
+ */
27
+ const UNICODE_SPACES_PATTERN = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
28
+
21
29
  const dangerousPatterns = [/\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
22
30
  const protectedPathSegments = new Set([".git", "node_modules"]);
23
31
  const shellCommandSeparators = new Set([";", "&", "&&", "||", "|", "\n", "(", ")", "{", "}"]);
@@ -48,14 +56,88 @@ type ShellToken = {
48
56
  quote?: '"' | "'";
49
57
  };
50
58
 
51
- type WriteInput = { path: string; content: string };
52
- type EditInput = { path: string; edits: Array<{ oldText: string; newText: string }> };
53
59
  type ShellInput = { command: string; timeout?: number };
54
60
 
55
61
  function isRecord(value: unknown): value is Record<string, unknown> {
56
62
  return !!value && typeof value === "object" && !Array.isArray(value);
57
63
  }
58
64
 
65
+ /**
66
+ * Converts Git Bash / MSYS / Cygwin / WSL drive paths to a form native Windows
67
+ * APIs accept. Mirrors normalizeWindowsShellPath from
68
+ * dist/utils/paths.js. No-op on non-win32 paths.
69
+ */
70
+ function normalizeWindowsShellPathLocal(filePath: string): string {
71
+ if (!filePath.startsWith("/") || filePath.startsWith("//") || filePath.includes("\\"))
72
+ return filePath;
73
+ const match = filePath.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
74
+ if (!match) return filePath;
75
+ const suffix = match[2]?.replaceAll("/", "\\");
76
+ return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
77
+ }
78
+
79
+ /**
80
+ * Mirrors Pi's normalizePath (dist/utils/paths.js). On win32, Git Bash / WSL
81
+ * drive-path normalisation always runs; only tilde expansion is gated by
82
+ * expandTilde.
83
+ */
84
+ function normalizePath(
85
+ input: string,
86
+ opts: { normalizeUnicodeSpaces?: boolean; stripAtPrefix?: boolean; expandTilde?: boolean } = {},
87
+ ): string {
88
+ const { normalizeUnicodeSpaces = false, stripAtPrefix = false, expandTilde = true } = opts;
89
+ let p = input;
90
+ if (normalizeUnicodeSpaces) p = p.replace(UNICODE_SPACES_PATTERN, " ");
91
+ if (stripAtPrefix && p.startsWith("@")) p = p.slice(1);
92
+ if (process.platform === "win32") p = normalizeWindowsShellPathLocal(p);
93
+ if (expandTilde) {
94
+ const home = os.homedir();
95
+ if (p === "~") p = home;
96
+ else if (p.startsWith("~/") || (process.platform === "win32" && p.startsWith("~\\")))
97
+ p = path.join(home, p.slice(2));
98
+ }
99
+ if (/^file:\/\//.test(p)) p = fileURLToPath(p);
100
+ return p;
101
+ }
102
+
103
+ /**
104
+ * Mirrors Pi's resolveToCwd (dist/core/tools/path-utils.js): normalises the
105
+ * raw path with { normalizeUnicodeSpaces: true, stripAtPrefix: true }, normalises
106
+ * cwd with default options (expandTilde: true, so win32 shell-path handling
107
+ * also applies to cwd as Pi does), then resolves absolute paths via
108
+ * path.resolve and relative paths against cwd.
109
+ *
110
+ * Exported for tests that check parity with Pi's resolveToCwd. Note: other
111
+ * extensions will not import this file; each extension keeps its own local copy.
112
+ */
113
+ export function resolveToCwd(rawPath: string, cwd: string): string {
114
+ const normalized = normalizePath(rawPath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
115
+ const base = normalizePath(cwd);
116
+ return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(base, normalized);
117
+ }
118
+
119
+ type PiCheckResult =
120
+ | { status: "protected"; resolvedPath: string }
121
+ | { status: "error" }
122
+ | { status: "allowed" };
123
+
124
+ /**
125
+ * Resolves rawPath via the Pi-mirroring resolveToCwd, then checks whether the
126
+ * resulting absolute path is a protected path.
127
+ */
128
+ function checkPiResolvedPath(rawPath: string, cwd: string): PiCheckResult {
129
+ try {
130
+ const resolved = resolveToCwd(rawPath, cwd);
131
+ const normalized = normalizeToolPath(resolved);
132
+ if (normalized !== undefined && isProtectedPath(normalized)) {
133
+ return { status: "protected", resolvedPath: resolved };
134
+ }
135
+ return { status: "allowed" };
136
+ } catch {
137
+ return { status: "error" };
138
+ }
139
+ }
140
+
59
141
  function normalizeToolPath(rawPath: string): NormalizedPath | undefined {
60
142
  const trimmed = rawPath.trim();
61
143
  if (!trimmed) return undefined;
@@ -636,21 +718,20 @@ async function hasDangerousPowerShellCommand(
636
718
  }
637
719
  }
638
720
 
639
- function validateWriteInput(input: unknown): WriteInput | undefined {
721
+ function validateWriteInput(input: unknown): NormalizedPath | undefined {
640
722
  if (!isRecord(input)) return undefined;
641
723
  if (typeof input.path !== "string" || typeof input.content !== "string") return undefined;
642
- if (!normalizeToolPath(input.path)) return undefined;
643
- return { path: input.path, content: input.content };
724
+ return normalizeToolPath(input.path);
644
725
  }
645
726
 
646
- function validateEditInput(input: unknown): EditInput | undefined {
727
+ function validateEditInput(input: unknown): NormalizedPath | undefined {
647
728
  if (!isRecord(input)) return undefined;
648
729
  if (typeof input.path !== "string" || !Array.isArray(input.edits)) return undefined;
649
- if (!normalizeToolPath(input.path)) return undefined;
730
+ const normalizedPath = normalizeToolPath(input.path);
731
+ if (!normalizedPath) return undefined;
650
732
 
651
- const edits = input.edits;
652
733
  if (
653
- edits.some(
734
+ input.edits.some(
654
735
  (edit) =>
655
736
  !isRecord(edit) || typeof edit.oldText !== "string" || typeof edit.newText !== "string",
656
737
  )
@@ -658,10 +739,7 @@ function validateEditInput(input: unknown): EditInput | undefined {
658
739
  return undefined;
659
740
  }
660
741
 
661
- return {
662
- path: input.path,
663
- edits: edits as Array<{ oldText: string; newText: string }>,
664
- };
742
+ return normalizedPath;
665
743
  }
666
744
 
667
745
  async function requestConfirmation(
@@ -691,21 +769,48 @@ async function confirmProtectedPathAction(
691
769
  toolName: "write" | "edit",
692
770
  normalizedPath: NormalizedPath,
693
771
  ctx: ConfirmationContext,
772
+ alsoProtected = false,
773
+ displaySuffix?: string,
694
774
  ): Promise<GuardDecision> {
695
- if (!isProtectedPath(normalizedPath)) return undefined;
775
+ if (!alsoProtected && !isProtectedPath(normalizedPath)) return undefined;
776
+ const displayLabel = displaySuffix
777
+ ? `${normalizedPath.displayPath}${displaySuffix}`
778
+ : normalizedPath.displayPath;
696
779
  if (!ctx.hasUI || !ctx.ui) {
697
780
  return {
698
781
  block: true,
699
- reason: `Protected path blocked (${toolName} without UI confirmation): ${normalizedPath.displayPath}`,
782
+ reason: `Protected path blocked (${toolName} without UI confirmation): ${displayLabel}`,
700
783
  };
701
784
  }
702
785
 
703
786
  return requestConfirmation(
704
- `⚠️ Protected path ${toolName} request:\n\n ${normalizedPath.displayPath}\n\nAllow?`,
787
+ `⚠️ Protected path ${toolName} request:\n\n ${displayLabel}\n\nAllow?`,
705
788
  ctx,
706
789
  );
707
790
  }
708
791
 
792
+ async function confirmLexicalOrResolvedPath(
793
+ toolName: "write" | "edit",
794
+ normalizedPath: NormalizedPath,
795
+ ctx: ConfirmationContext & { cwd?: string },
796
+ ): Promise<GuardDecision> {
797
+ // Monotonic hardening: also check via Pi-style path resolution against ctx.cwd.
798
+ // OR logic: protected if EITHER the lexical check OR the Pi-resolved check matches.
799
+ // When only the Pi-resolved check catches a path, show the resolved absolute path in
800
+ // the reason so the user understands why the plain-looking path is protected.
801
+ const lexicalProtected = isProtectedPath(normalizedPath);
802
+ const piResult = checkPiResolvedPath(normalizedPath.original, ctx.cwd ?? process.cwd());
803
+ const piAlsoProtected = piResult.status === "protected" || piResult.status === "error";
804
+ const displaySuffix = lexicalProtected
805
+ ? undefined
806
+ : piResult.status === "protected"
807
+ ? ` (resolves to ${piResult.resolvedPath})`
808
+ : piResult.status === "error"
809
+ ? " (path could not be resolved)"
810
+ : undefined;
811
+ return confirmProtectedPathAction(toolName, normalizedPath, ctx, piAlsoProtected, displaySuffix);
812
+ }
813
+
709
814
  export default function (pi: ExtensionAPI) {
710
815
  pi.on("tool_call", async (event, ctx) => {
711
816
  if (event.toolName === "bash" || event.toolName === "powershell") {
@@ -733,28 +838,14 @@ export default function (pi: ExtensionAPI) {
733
838
  return undefined;
734
839
  }
735
840
 
736
- if (event.toolName === "write") {
737
- const input = validateWriteInput(event.input);
738
- if (!input) {
739
- return { block: true, reason: "Malformed write input blocked" };
740
- }
741
- const normalizedPath = normalizeToolPath(input.path);
742
- if (!normalizedPath) {
743
- return { block: true, reason: "Malformed write input blocked" };
744
- }
745
- return confirmProtectedPathAction("write", normalizedPath, ctx);
746
- }
747
-
748
- if (event.toolName === "edit") {
749
- const input = validateEditInput(event.input);
750
- if (!input) {
751
- return { block: true, reason: "Malformed edit input blocked" };
752
- }
753
- const normalizedPath = normalizeToolPath(input.path);
841
+ if (event.toolName === "write" || event.toolName === "edit") {
842
+ const toolName = event.toolName;
843
+ const normalizedPath =
844
+ toolName === "write" ? validateWriteInput(event.input) : validateEditInput(event.input);
754
845
  if (!normalizedPath) {
755
- return { block: true, reason: "Malformed edit input blocked" };
846
+ return { block: true, reason: `Malformed ${toolName} input blocked` };
756
847
  }
757
- return confirmProtectedPathAction("edit", normalizedPath, ctx);
848
+ return confirmLexicalOrResolvedPath(toolName, normalizedPath, ctx);
758
849
  }
759
850
 
760
851
  return undefined;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-permission-gate",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "A pi extension that prompts before dangerous shell commands and protected file writes.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -89,10 +89,10 @@ if a parent directory is trusted.
89
89
  Other approaches and their limitations:
90
90
 
91
91
  - **Delete the entry (or set it to `null`)** in `trust.json`: this resumes
92
- nearest-ancestor lookup (step 3b), *not* a direct fallback to
92
+ nearest-ancestor lookup (step 3), *not* a direct fallback to
93
93
  `defaultProjectTrust`. If any ancestor directory has a saved `true`, that
94
94
  decision is found first and loading is re-enabled. `defaultProjectTrust`
95
- (step 3c) only applies when the nearest-ancestor walk finds no decision at
95
+ (step 4) only applies when the nearest-ancestor walk finds no decision at
96
96
  all. Deleting/nulling an entry is therefore *not* a reliable denial when
97
97
  any ancestor path is trusted.
98
98
  - **Set `defaultProjectTrust` to `"never"`** in `~/.pi/agent/settings.json`:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-project-mcp-json",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A pi extension that reads Claude Code-style .mcp.json files and registers the configured servers with pi.registerMcpServer.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1 +1 @@
1
- 0.84.4
1
+ 1.1.0
@@ -1,33 +1,45 @@
1
1
  # quiet-tools
2
2
 
3
- A pi extension that makes collapsed built-in tool rows much quieter in the TUI.
3
+ A pi extension that makes collapsed built-in tool rows and MCP tool rows quieter in the TUI.
4
4
 
5
- When enabled, each collapsed tool row renders as one invocation line plus a separate `(Ctrl+O to expand)` hint line. Tool output is hidden until expanded. Expanding with `Ctrl+O` still shows pi's full rendered output.
5
+ When loaded on a supported pi host, quiet rendering is always on: each collapsed tool row shows one invocation line plus a separate `(Ctrl+O to expand)` hint line. Tool output is hidden until expanded. The global `Ctrl+O` binding, per-row expansion, and native expanded result rendering remain available.
6
6
 
7
- `quiet-tools` only changes the visual renderer. It does not truncate, summarize, or rewrite the actual tool results sent to the model. On Pi 0.99, its replacement definitions retain the upstream schemas, labels, descriptions, exposure/namespace metadata, `outputSchema`, execution function, abort signal, update callback, execution context, details, structured content, error status, and bounded output behavior. Expanded rendering delegates back to Pi's renderer.
7
+ `quiet-tools` only changes the visual renderer. It does not truncate, summarize, or rewrite the actual tool results sent to the model. Expanded rendering delegates back to pi's renderer, preserving pi's default output for every tool.
8
+
9
+ ## How it works
10
+
11
+ On pi >=1.0.1, `quiet-tools` registers a single `pi.registerToolRenderer` resolver. The resolver intercepts collapsed rows for the 7 built-in tools and any `mcp__*` tool and renders them quietly. All other tools are passed through to pi's default rendering. There is no opt-out setting, environment variable, or persistence mechanism.
12
+
13
+ On pi <1.0.1 (where `registerToolRenderer` is not available), the extension registers no renderers and shows a one-time warning notification on session start. The read-only `/quiet-tools status` command remains available, but rendering is inactive.
8
14
 
9
15
  ## Covered tools
10
16
 
11
- - `bash`
12
- - `read`
13
- - `grep`
14
- - `find`
15
- - `ls`
16
- - `edit`
17
- - `write`
17
+ **Built-in tools:** `bash`, `edit`, `find`, `grep`, `ls`, `read`, `write`
18
+
19
+ Each built-in has a compact one-line summary (for example, `$ echo hello`, `read ~/project/notes.txt:1-20`, or `grep /TODO/ in src/`). Collapsed error and aborted rows retain a terse `[failed]` marker; the full native result remains available when expanded.
20
+
21
+ **MCP tools:** any tool whose name starts with `mcp__`
18
22
 
19
- For every covered tool, the collapsed invocation is truncated to a single visual line so long paths, commands, diffs, or file contents do not fill the TUI. Expanding restores pi's normal renderer.
23
+ MCP collapsed call lines show the tool name followed by compact JSON arguments (sanitized to a single line and truncated to the available TUI width). MCP collapsed results render empty, like built-in collapsed results.
20
24
 
21
- ## Commands
25
+ For every covered tool, the collapsed invocation is truncated to a single visual line. Expanding restores pi's normal renderer.
26
+
27
+ ## Command
22
28
 
23
29
  ```text
30
+ /quiet-tools
24
31
  /quiet-tools status
25
- /quiet-tools off
26
- /quiet-tools on
27
- /quiet-tools toggle
28
32
  ```
29
33
 
30
- The extension starts enabled by default. Disabling is temporary for the current extension runtime/session; after `/reload`, `/new`, `/resume`, or `/fork`, it starts enabled again.
34
+ Both forms report the always-on status. `status` is the only completion and valid action; other arguments show `Usage: /quiet-tools status` and cannot change rendering.
35
+
36
+ ## Export-style contexts
37
+
38
+ Quiet call headers are also used in export-style renderer contexts. As a result, an export may omit a native write-call preview; this is an accepted tradeoff. Native expanded result rendering remains available in the TUI, including native error text, timing, and image handling.
39
+
40
+ ## Requirements
41
+
42
+ Pi >=1.0.1 is required for quiet rendering. On older pi hosts, the extension loads without error and the status command remains available, but rendering is inactive.
31
43
 
32
44
  ## Install
33
45
 
@@ -57,8 +69,7 @@ Then reload pi:
57
69
 
58
70
  ## Notes
59
71
 
60
- - This extension overrides pi's built-in tool definitions so it can customize only their TUI renderers.
61
- - It reuses pi's built-in implementations and preserves `shellPath`, `shellCommandPrefix`, and image autoresize settings when they are available from settings files.
62
- - If another extension also overrides built-in tool execution, pi's extension load order determines which override wins.
72
+ - `quiet-tools` uses pi's `registerToolRenderer` API to wrap only visual renderers, without touching tool execution, metadata, active-tool selection, or model-visible results.
63
73
  - It affects assistant-invoked tool rows. User `!`/`!!` bash commands are rendered by a separate pi component and keep pi's default preview behavior.
64
74
  - Pi renders image attachments outside tool result renderers, so inline image display for image reads is still controlled by pi's image settings.
75
+ - MCP tool argument lines are sanitized (ANSI and control characters) and truncated to fit the TUI width. Expand to see full output.