@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.
- package/README.md +8 -8
- package/extensions/agent-workflow-audit/README.md +1 -1
- package/extensions/agent-workflow-audit/index.ts +275 -59
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/git.ts +0 -8
- package/extensions/annotate-git-diff/glimpseui.d.ts +5 -65
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +5 -65
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/web/index.html +3 -30
- package/extensions/code-reviewer/README.md +5 -5
- package/extensions/code-reviewer/index.ts +252 -82
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/index.ts +2 -8
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +3 -3
- package/extensions/context-cap/package.json +2 -2
- package/extensions/context-inspector/index.ts +2 -46
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/README.md +4 -3
- package/extensions/contrarian/index.ts +48 -67
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +0 -4
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +20 -11
- package/extensions/dynamic-context-pruning/index.ts +118 -341
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +18 -11
- package/extensions/fast/index.ts +9 -5
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/index.ts +2 -16
- package/extensions/git-footer/package.json +1 -1
- package/extensions/inline-bash/index.ts +0 -16
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/README.md +1 -1
- package/extensions/librarian/index.ts +246 -70
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +5 -26
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/openai-usage.ts +2 -11
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/README.md +4 -3
- package/extensions/oracle/index.ts +46 -65
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +3 -0
- package/extensions/permission-gate/index.ts +127 -36
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/project-mcp-json/README.md +2 -2
- package/extensions/project-mcp-json/package.json +1 -1
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/README.md +30 -19
- package/extensions/quiet-tools/index.ts +233 -166
- package/extensions/quiet-tools/package.json +2 -2
- package/extensions/review/README.md +2 -0
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +0 -19
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/README.md +4 -2
- package/extensions/triage-comments/index.ts +344 -39
- package/extensions/triage-comments/package.json +1 -1
- 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
|
-
|
|
72
|
-
|
|
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"> {
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
106
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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);
|
|
@@ -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):
|
|
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
|
-
|
|
643
|
-
return { path: input.path, content: input.content };
|
|
724
|
+
return normalizeToolPath(input.path);
|
|
644
725
|
}
|
|
645
726
|
|
|
646
|
-
function validateEditInput(input: unknown):
|
|
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
|
-
|
|
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): ${
|
|
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 ${
|
|
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
|
|
738
|
-
|
|
739
|
-
|
|
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:
|
|
846
|
+
return { block: true, reason: `Malformed ${toolName} input blocked` };
|
|
756
847
|
}
|
|
757
|
-
return
|
|
848
|
+
return confirmLexicalOrResolvedPath(toolName, normalizedPath, ctx);
|
|
758
849
|
}
|
|
759
850
|
|
|
760
851
|
return undefined;
|
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
3
|
+
A pi extension that makes collapsed built-in tool rows and MCP tool rows quieter in the TUI.
|
|
4
4
|
|
|
5
|
-
When
|
|
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.
|
|
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
|
-
|
|
13
|
-
- `grep`
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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.
|