@diegopetrucci/pi-extensions 0.1.76 → 0.1.78

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 +8 -8
  2. package/extensions/agent-workflow-audit/README.md +1 -1
  3. package/extensions/agent-workflow-audit/index.ts +277 -44
  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/package.json +1 -1
  7. package/extensions/annotate-last-message/package.json +1 -1
  8. package/extensions/annotate-last-message/web/index.html +3 -3
  9. package/extensions/code-reviewer/README.md +5 -5
  10. package/extensions/code-reviewer/index.ts +258 -54
  11. package/extensions/code-reviewer/package.json +1 -1
  12. package/extensions/confirm-destructive/index.ts +2 -8
  13. package/extensions/confirm-destructive/package.json +1 -1
  14. package/extensions/context-cap/README.md +9 -9
  15. package/extensions/context-cap/index.ts +3 -3
  16. package/extensions/context-cap/package.json +2 -2
  17. package/extensions/contrarian/README.md +4 -3
  18. package/extensions/contrarian/index.ts +47 -40
  19. package/extensions/contrarian/package.json +1 -1
  20. package/extensions/dirty-repo-guard/index.ts +0 -4
  21. package/extensions/dirty-repo-guard/package.json +1 -1
  22. package/extensions/dynamic-context-pruning/README.md +5 -5
  23. package/extensions/dynamic-context-pruning/index.ts +114 -232
  24. package/extensions/dynamic-context-pruning/package.json +1 -1
  25. package/extensions/fast/README.md +2 -2
  26. package/extensions/fast/package.json +1 -1
  27. package/extensions/inline-bash/index.ts +0 -16
  28. package/extensions/inline-bash/package.json +1 -1
  29. package/extensions/librarian/README.md +1 -1
  30. package/extensions/librarian/index.ts +247 -39
  31. package/extensions/librarian/package.json +1 -1
  32. package/extensions/minimal-footer/README.md +5 -5
  33. package/extensions/minimal-footer/index.ts +3 -10
  34. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  35. package/extensions/minimal-footer/package.json +1 -1
  36. package/extensions/oracle/README.md +4 -3
  37. package/extensions/oracle/index.ts +45 -38
  38. package/extensions/oracle/package.json +1 -1
  39. package/extensions/permission-gate/README.md +3 -0
  40. package/extensions/permission-gate/index.ts +151 -28
  41. package/extensions/permission-gate/package.json +1 -1
  42. package/extensions/project-mcp-json/README.md +2 -2
  43. package/extensions/project-mcp-json/package.json +1 -1
  44. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  45. package/extensions/quiet-tools/README.md +30 -19
  46. package/extensions/quiet-tools/index.ts +233 -166
  47. package/extensions/quiet-tools/package.json +2 -2
  48. package/extensions/todo/index.ts +0 -19
  49. package/extensions/todo/package.json +1 -1
  50. package/extensions/triage-comments/README.md +4 -2
  51. package/extensions/triage-comments/index.ts +349 -29
  52. package/extensions/triage-comments/package.json +1 -1
  53. package/package.json +4 -4
@@ -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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-oracle",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
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,96 @@ 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).
81
+ *
82
+ * Options (all default false/true as noted):
83
+ * normalizeUnicodeSpaces — replace Unicode space characters with ASCII space.
84
+ * stripAtPrefix — strip a leading "@" path sigil.
85
+ * expandTilde — expand "~"/"~/" to homedir (default true).
86
+ *
87
+ * On win32, Git Bash / WSL drive-path normalisation always runs (not gated
88
+ * by expandTilde); only the tilde expansion itself is gated by expandTilde.
89
+ * Always resolves "file://" URLs via fileURLToPath; throws for non-local hosts
90
+ * (e.g. file://hostname/…) or invalid URLs.
91
+ */
92
+ function normalizePath(
93
+ input: string,
94
+ opts: { normalizeUnicodeSpaces?: boolean; stripAtPrefix?: boolean; expandTilde?: boolean } = {},
95
+ ): string {
96
+ const { normalizeUnicodeSpaces = false, stripAtPrefix = false, expandTilde = true } = opts;
97
+ let p = input;
98
+ if (normalizeUnicodeSpaces) p = p.replace(UNICODE_SPACES_PATTERN, " ");
99
+ if (stripAtPrefix && p.startsWith("@")) p = p.slice(1);
100
+ if (process.platform === "win32") p = normalizeWindowsShellPathLocal(p);
101
+ if (expandTilde) {
102
+ const home = os.homedir();
103
+ if (p === "~") p = home;
104
+ else if (p.startsWith("~/") || (process.platform === "win32" && p.startsWith("~\\")))
105
+ p = path.join(home, p.slice(2));
106
+ }
107
+ if (/^file:\/\//.test(p)) p = fileURLToPath(p);
108
+ return p;
109
+ }
110
+
111
+ /**
112
+ * Mirrors Pi's resolveToCwd (dist/core/tools/path-utils.js): normalises the
113
+ * raw path with { normalizeUnicodeSpaces: true, stripAtPrefix: true }, normalises
114
+ * cwd with default options (expandTilde: true, so win32 shell-path handling
115
+ * also applies to cwd as Pi does), then resolves absolute paths via
116
+ * path.resolve and relative paths against cwd.
117
+ *
118
+ * Exported for tests that check parity with Pi's resolveToCwd. Note: other
119
+ * extensions will not import this file; each extension keeps its own local copy.
120
+ */
121
+ export function resolveToCwd(rawPath: string, cwd: string): string {
122
+ const normalized = normalizePath(rawPath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
123
+ const base = normalizePath(cwd);
124
+ return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(base, normalized);
125
+ }
126
+
127
+ type PiCheckResult =
128
+ | { status: "protected"; resolvedPath: string }
129
+ | { status: "error" }
130
+ | { status: "allowed" };
131
+
132
+ /**
133
+ * Resolves rawPath via the Pi-mirroring resolveToCwd, then checks whether the
134
+ * resulting absolute path is a protected path.
135
+ */
136
+ function checkPiResolvedPath(rawPath: string, cwd: string): PiCheckResult {
137
+ try {
138
+ const resolved = resolveToCwd(rawPath, cwd);
139
+ const normalized = normalizeToolPath(resolved);
140
+ if (normalized !== undefined && isProtectedPath(normalized)) {
141
+ return { status: "protected", resolvedPath: resolved };
142
+ }
143
+ return { status: "allowed" };
144
+ } catch {
145
+ return { status: "error" };
146
+ }
147
+ }
148
+
59
149
  function normalizeToolPath(rawPath: string): NormalizedPath | undefined {
60
150
  const trimmed = rawPath.trim();
61
151
  if (!trimmed) return undefined;
@@ -636,21 +726,20 @@ async function hasDangerousPowerShellCommand(
636
726
  }
637
727
  }
638
728
 
639
- function validateWriteInput(input: unknown): WriteInput | undefined {
729
+ function validateWriteInput(input: unknown): NormalizedPath | undefined {
640
730
  if (!isRecord(input)) return undefined;
641
731
  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 };
732
+ return normalizeToolPath(input.path);
644
733
  }
645
734
 
646
- function validateEditInput(input: unknown): EditInput | undefined {
735
+ function validateEditInput(input: unknown): NormalizedPath | undefined {
647
736
  if (!isRecord(input)) return undefined;
648
737
  if (typeof input.path !== "string" || !Array.isArray(input.edits)) return undefined;
649
- if (!normalizeToolPath(input.path)) return undefined;
738
+ const normalizedPath = normalizeToolPath(input.path);
739
+ if (!normalizedPath) return undefined;
650
740
 
651
- const edits = input.edits;
652
741
  if (
653
- edits.some(
742
+ input.edits.some(
654
743
  (edit) =>
655
744
  !isRecord(edit) || typeof edit.oldText !== "string" || typeof edit.newText !== "string",
656
745
  )
@@ -658,10 +747,7 @@ function validateEditInput(input: unknown): EditInput | undefined {
658
747
  return undefined;
659
748
  }
660
749
 
661
- return {
662
- path: input.path,
663
- edits: edits as Array<{ oldText: string; newText: string }>,
664
- };
750
+ return normalizedPath;
665
751
  }
666
752
 
667
753
  async function requestConfirmation(
@@ -691,17 +777,22 @@ async function confirmProtectedPathAction(
691
777
  toolName: "write" | "edit",
692
778
  normalizedPath: NormalizedPath,
693
779
  ctx: ConfirmationContext,
780
+ alsoProtected = false,
781
+ displaySuffix?: string,
694
782
  ): Promise<GuardDecision> {
695
- if (!isProtectedPath(normalizedPath)) return undefined;
783
+ if (!alsoProtected && !isProtectedPath(normalizedPath)) return undefined;
784
+ const displayLabel = displaySuffix
785
+ ? `${normalizedPath.displayPath}${displaySuffix}`
786
+ : normalizedPath.displayPath;
696
787
  if (!ctx.hasUI || !ctx.ui) {
697
788
  return {
698
789
  block: true,
699
- reason: `Protected path blocked (${toolName} without UI confirmation): ${normalizedPath.displayPath}`,
790
+ reason: `Protected path blocked (${toolName} without UI confirmation): ${displayLabel}`,
700
791
  };
701
792
  }
702
793
 
703
794
  return requestConfirmation(
704
- `⚠️ Protected path ${toolName} request:\n\n ${normalizedPath.displayPath}\n\nAllow?`,
795
+ `⚠️ Protected path ${toolName} request:\n\n ${displayLabel}\n\nAllow?`,
705
796
  ctx,
706
797
  );
707
798
  }
@@ -734,27 +825,59 @@ export default function (pi: ExtensionAPI) {
734
825
  }
735
826
 
736
827
  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);
828
+ const normalizedPath = validateWriteInput(event.input);
742
829
  if (!normalizedPath) {
743
830
  return { block: true, reason: "Malformed write input blocked" };
744
831
  }
745
- return confirmProtectedPathAction("write", normalizedPath, ctx);
832
+ // Monotonic hardening: also check via Pi-style path resolution against ctx.cwd.
833
+ // OR logic: protected if EITHER the lexical check OR the Pi-resolved check matches.
834
+ // When only the Pi-resolved check catches a path, show the resolved absolute path in
835
+ // the reason so the user understands why the plain-looking path is protected.
836
+ const lexicalProtected = isProtectedPath(normalizedPath);
837
+ const piResult = checkPiResolvedPath(normalizedPath.original, ctx.cwd ?? process.cwd());
838
+ const piAlsoProtected = piResult.status === "protected" || piResult.status === "error";
839
+ const displaySuffix = lexicalProtected
840
+ ? undefined
841
+ : piResult.status === "protected"
842
+ ? ` (resolves to ${piResult.resolvedPath})`
843
+ : piResult.status === "error"
844
+ ? " (path could not be resolved)"
845
+ : undefined;
846
+ return confirmProtectedPathAction(
847
+ "write",
848
+ normalizedPath,
849
+ ctx,
850
+ piAlsoProtected,
851
+ displaySuffix,
852
+ );
746
853
  }
747
854
 
748
855
  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);
856
+ const normalizedPath = validateEditInput(event.input);
754
857
  if (!normalizedPath) {
755
858
  return { block: true, reason: "Malformed edit input blocked" };
756
859
  }
757
- return confirmProtectedPathAction("edit", normalizedPath, ctx);
860
+ // Monotonic hardening: also check via Pi-style path resolution against ctx.cwd.
861
+ // OR logic: protected if EITHER the lexical check OR the Pi-resolved check matches.
862
+ // When only the Pi-resolved check catches a path, show the resolved absolute path in
863
+ // the reason so the user understands why the plain-looking path is protected.
864
+ const lexicalProtected = isProtectedPath(normalizedPath);
865
+ const piResult = checkPiResolvedPath(normalizedPath.original, ctx.cwd ?? process.cwd());
866
+ const piAlsoProtected = piResult.status === "protected" || piResult.status === "error";
867
+ const displaySuffix = lexicalProtected
868
+ ? undefined
869
+ : piResult.status === "protected"
870
+ ? ` (resolves to ${piResult.resolvedPath})`
871
+ : piResult.status === "error"
872
+ ? " (path could not be resolved)"
873
+ : undefined;
874
+ return confirmProtectedPathAction(
875
+ "edit",
876
+ normalizedPath,
877
+ ctx,
878
+ piAlsoProtected,
879
+ displaySuffix,
880
+ );
758
881
  }
759
882
 
760
883
  return undefined;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-permission-gate",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
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.