@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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { Dirent, Stats } from "node:fs";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
import type { FileHandle } from "node:fs/promises";
|
|
4
5
|
import * as fs from "node:fs/promises";
|
|
5
6
|
import * as os from "node:os";
|
|
6
7
|
import * as path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
7
9
|
|
|
8
10
|
import type {
|
|
9
11
|
ExtensionAPI,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
getAgentDir,
|
|
20
22
|
getMarkdownTheme,
|
|
21
23
|
} from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
22
25
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
23
26
|
import { Type } from "typebox";
|
|
24
27
|
|
|
@@ -90,6 +93,82 @@ function getModelRuntimeOption(ctx: {
|
|
|
90
93
|
const DEFAULT_CACHE_MODE: CacheMode = "disabled";
|
|
91
94
|
const DEFAULT_THINKING_LEVEL: ThinkingLevel = "low";
|
|
92
95
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
96
|
+
const SUBAGENT_MCP_TOOLS = [
|
|
97
|
+
"mcp__*",
|
|
98
|
+
"tool_search",
|
|
99
|
+
"list_mcp_resources",
|
|
100
|
+
"list_mcp_resource_templates",
|
|
101
|
+
"read_mcp_resource",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Derive project-trust for the isolated subagent session from the host tool context.
|
|
105
|
+
// Fails closed: returns false if the host has not trusted the project, or if the subagent
|
|
106
|
+
// runs in a different directory (e.g. a temp workspace), preventing an untrusted project's
|
|
107
|
+
// .pi/mcp.json from being loaded by the MCP extension.
|
|
108
|
+
function resolveSubagentProjectTrusted(
|
|
109
|
+
ctx: { cwd: string; isProjectTrusted(): boolean },
|
|
110
|
+
subagentCwd: string,
|
|
111
|
+
): boolean {
|
|
112
|
+
// Feature-detected for older Pi hosts that may lack isProjectTrusted.
|
|
113
|
+
const hostTrusted =
|
|
114
|
+
typeof (ctx as { isProjectTrusted?: unknown }).isProjectTrusted === "function"
|
|
115
|
+
? ctx.isProjectTrusted()
|
|
116
|
+
: false;
|
|
117
|
+
if (!hostTrusted) return false;
|
|
118
|
+
try {
|
|
119
|
+
return realpathSync(subagentCwd) === realpathSync(ctx.cwd);
|
|
120
|
+
} catch {
|
|
121
|
+
return false; // fail closed if realpath resolution fails
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Returns true when v is a semver string >=1.0.4 (ignores prerelease; missing/garbage → false). */
|
|
126
|
+
function isMcpCompatibleVersion(v: string | undefined): boolean {
|
|
127
|
+
if (!v) return false;
|
|
128
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
129
|
+
if (!m) return false;
|
|
130
|
+
const major = Number(m[1]),
|
|
131
|
+
minor = Number(m[2]),
|
|
132
|
+
patch = Number(m[3]);
|
|
133
|
+
if (major !== 1) return major > 1;
|
|
134
|
+
if (minor !== 0) return minor > 0;
|
|
135
|
+
return patch >= 4;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Safe runtime access to MCP/tool-search factories — feature-detected for Pi >=0.99.0 hosts.
|
|
139
|
+
// Additionally gated on VERSION >=1.0.4: on older hosts mcp__* allowlist entries are exact-name
|
|
140
|
+
// matches that resolve nothing, so spawning MCP servers provides no benefit.
|
|
141
|
+
function createMcpSubagentFactories(): ExtensionFactory[] {
|
|
142
|
+
const ns = piCodingAgent as Record<string, unknown>;
|
|
143
|
+
if (!isMcpCompatibleVersion(ns["VERSION"] as string | undefined)) return [];
|
|
144
|
+
const factories: ExtensionFactory[] = [];
|
|
145
|
+
const mcp = ns["createMcpExtension"];
|
|
146
|
+
const toolSearch = ns["createToolSearchExtension"];
|
|
147
|
+
if (typeof mcp === "function") factories.push((mcp as () => ExtensionFactory)());
|
|
148
|
+
if (typeof toolSearch === "function") factories.push((toolSearch as () => ExtensionFactory)());
|
|
149
|
+
return factories;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Emit session_shutdown before dispose so MCP closes server connections (no leaked child processes).
|
|
153
|
+
async function disposeSubagentSession(
|
|
154
|
+
session: { extensionRunner?: unknown; dispose(): void } | undefined,
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
if (!session) return;
|
|
157
|
+
try {
|
|
158
|
+
const runner = session.extensionRunner as
|
|
159
|
+
| {
|
|
160
|
+
hasHandlers(n: string): boolean;
|
|
161
|
+
emit(e: { type: string; reason: string }): Promise<unknown>;
|
|
162
|
+
}
|
|
163
|
+
| undefined;
|
|
164
|
+
if (runner?.hasHandlers("session_shutdown")) {
|
|
165
|
+
await runner.emit({ type: "session_shutdown", reason: "quit" });
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// Shutdown errors must not prevent dispose.
|
|
169
|
+
}
|
|
170
|
+
session.dispose();
|
|
171
|
+
}
|
|
93
172
|
|
|
94
173
|
const PREFERRED_FAST_MODEL_PATTERNS = [
|
|
95
174
|
/\bgpt[-_. ]?5\.5(?:[-_. ].*)?\b(?:mini|nano|fast|lite)\b/,
|
|
@@ -687,11 +766,6 @@ async function writeLibrarianPreferences(preferences: LibrarianPreferences): Pro
|
|
|
687
766
|
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
688
767
|
}
|
|
689
768
|
|
|
690
|
-
async function writeCachePreference(preference: CacheMode): Promise<void> {
|
|
691
|
-
const preferences = await readLibrarianPreferences();
|
|
692
|
-
await writeLibrarianPreferences({ ...preferences, cacheMode: preference });
|
|
693
|
-
}
|
|
694
|
-
|
|
695
769
|
function resolveCacheDecision(preference: CacheMode): { enabled: boolean; reason: string } {
|
|
696
770
|
if (preference === "enabled") {
|
|
697
771
|
return { enabled: true, reason: "cache preference enabled; using cached local checkouts" };
|
|
@@ -903,9 +977,80 @@ async function buildLibrarianCandidates(
|
|
|
903
977
|
return candidates;
|
|
904
978
|
}
|
|
905
979
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
980
|
+
const UNICODE_SPACES_PATTERN = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Mirrors normalizeWindowsShellPath from dist/utils/paths.js. No-op on
|
|
984
|
+
* non-win32 paths.
|
|
985
|
+
*/
|
|
986
|
+
function normalizeWindowsShellPathLocal(filePath: string): string {
|
|
987
|
+
if (!filePath.startsWith("/") || filePath.startsWith("//") || filePath.includes("\\"))
|
|
988
|
+
return filePath;
|
|
989
|
+
const match = filePath.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
|
|
990
|
+
if (!match) return filePath;
|
|
991
|
+
const suffix = match[2]?.replaceAll("/", "\\");
|
|
992
|
+
return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Mirrors Pi's normalizePath (dist/utils/paths.js). On win32, Git Bash / WSL
|
|
997
|
+
* drive-path normalisation always runs; only tilde expansion is gated by
|
|
998
|
+
* expandTilde.
|
|
999
|
+
*/
|
|
1000
|
+
function normalizePath(
|
|
1001
|
+
input: string,
|
|
1002
|
+
opts: { normalizeUnicodeSpaces?: boolean; stripAtPrefix?: boolean; expandTilde?: boolean } = {},
|
|
1003
|
+
): string {
|
|
1004
|
+
const { normalizeUnicodeSpaces = false, stripAtPrefix = false, expandTilde = true } = opts;
|
|
1005
|
+
let p = input;
|
|
1006
|
+
if (normalizeUnicodeSpaces) p = p.replace(UNICODE_SPACES_PATTERN, " ");
|
|
1007
|
+
if (stripAtPrefix && p.startsWith("@")) p = p.slice(1);
|
|
1008
|
+
if (process.platform === "win32") p = normalizeWindowsShellPathLocal(p);
|
|
1009
|
+
if (expandTilde) {
|
|
1010
|
+
const home = os.homedir();
|
|
1011
|
+
if (p === "~") p = home;
|
|
1012
|
+
else if (p.startsWith("~/") || (process.platform === "win32" && p.startsWith("~\\")))
|
|
1013
|
+
p = path.join(home, p.slice(2));
|
|
1014
|
+
}
|
|
1015
|
+
if (/^file:\/\//.test(p)) p = fileURLToPath(p);
|
|
1016
|
+
return p;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Mirrors Pi's resolveToCwd (dist/core/tools/path-utils.js). Exposed via
|
|
1021
|
+
* __test__ for parity tests; not imported by other extensions.
|
|
1022
|
+
*/
|
|
1023
|
+
function resolveToCwd(rawPath: string, cwd: string): string {
|
|
1024
|
+
const normalized = normalizePath(rawPath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
|
|
1025
|
+
const base = normalizePath(cwd);
|
|
1026
|
+
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(base, normalized);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/** Mirrors tryMacOSScreenshotPath from dist/core/tools/path-utils.js */
|
|
1030
|
+
function tryMacOSScreenshotPathVariant(filePath: string): string {
|
|
1031
|
+
return filePath.replace(/ (AM|PM)\./gi, " $1.");
|
|
1032
|
+
}
|
|
1033
|
+
/** Mirrors tryNFDVariant from dist/core/tools/path-utils.js */
|
|
1034
|
+
function tryNFDPathVariant(filePath: string): string {
|
|
1035
|
+
return filePath.normalize("NFD");
|
|
1036
|
+
}
|
|
1037
|
+
/** Mirrors tryCurlyQuoteVariant from dist/core/tools/path-utils.js */
|
|
1038
|
+
function tryCurlyQuotePathVariant(filePath: string): string {
|
|
1039
|
+
return filePath.replace(/'/g, "’");
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Returns all read path candidates including Pi's resolveReadPathAsync fallback
|
|
1043
|
+
* variants.
|
|
1044
|
+
*/
|
|
1045
|
+
function getReadPathCandidates(resolved: string): string[] {
|
|
1046
|
+
const nfd = tryNFDPathVariant(resolved);
|
|
1047
|
+
return [
|
|
1048
|
+
resolved,
|
|
1049
|
+
tryMacOSScreenshotPathVariant(resolved),
|
|
1050
|
+
nfd,
|
|
1051
|
+
tryCurlyQuotePathVariant(resolved),
|
|
1052
|
+
tryCurlyQuotePathVariant(nfd),
|
|
1053
|
+
];
|
|
909
1054
|
}
|
|
910
1055
|
|
|
911
1056
|
function getBlockedBashReason(
|
|
@@ -992,16 +1137,45 @@ function createLibrarianRuntimeGuardExtension(options: {
|
|
|
992
1137
|
if (event.toolName === "read") {
|
|
993
1138
|
const input = event.input as { path?: unknown };
|
|
994
1139
|
if (typeof input.path !== "string") return undefined;
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1140
|
+
// Canonicalize allowed roots. A path is allowed if it is inside EITHER
|
|
1141
|
+
// the original root (options.workspace / options.cacheRoot) OR its
|
|
1142
|
+
// realpath'd form. This handles both: a realpath'd file under a
|
|
1143
|
+
// symlinked alias workspace (lexical match on original root) and an
|
|
1144
|
+
// existing file canonicalized beyond the alias (match on real root).
|
|
1145
|
+
const realWorkspace = await fs
|
|
1146
|
+
.realpath(options.workspace)
|
|
1147
|
+
.catch(() => path.resolve(options.workspace));
|
|
1148
|
+
const realCacheRoot = await fs
|
|
1149
|
+
.realpath(options.cacheRoot)
|
|
1150
|
+
.catch(() => path.resolve(options.cacheRoot));
|
|
1151
|
+
function isAllowedPath(p: string): boolean {
|
|
1152
|
+
return (
|
|
1153
|
+
isInside(options.workspace, p) ||
|
|
1154
|
+
isInside(realWorkspace, p) ||
|
|
1155
|
+
(options.cacheEnabled && (isInside(options.cacheRoot, p) || isInside(realCacheRoot, p)))
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
// For the read tool, also check Pi's resolveReadPathAsync fallback
|
|
1159
|
+
// variants so a missing file whose variant resolves outside is not
|
|
1160
|
+
// silently allowed.
|
|
1161
|
+
try {
|
|
1162
|
+
const piResolved = resolveToCwd(input.path, options.workspace);
|
|
1163
|
+
const candidates = getReadPathCandidates(piResolved);
|
|
1164
|
+
// Deliberate policy: all candidates are checked regardless of existence (not
|
|
1165
|
+
// Pi's existence-ordered selection) so the guard cannot be raced by creating
|
|
1166
|
+
// a variant between the hook and the read. Trade-off: a read is blocked when
|
|
1167
|
+
// an escaping variant symlink sits next to a safe primary file.
|
|
1168
|
+
for (const candidate of candidates) {
|
|
1169
|
+
const candidateRealPath = await fs.realpath(candidate).catch(() => candidate);
|
|
1170
|
+
if (!isAllowedPath(candidateRealPath))
|
|
1171
|
+
return {
|
|
1172
|
+
block: true,
|
|
1173
|
+
reason: `Librarian read is limited to its workspace/cache: ${candidateRealPath}`,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
} catch {
|
|
1177
|
+
return { block: true, reason: "Librarian read path could not be resolved safely." };
|
|
1178
|
+
}
|
|
1005
1179
|
}
|
|
1006
1180
|
|
|
1007
1181
|
if (event.toolName === "bash") {
|
|
@@ -1045,7 +1219,7 @@ function buildSystemPrompt(options: {
|
|
|
1045
1219
|
? `\nLocal checkout cache is ENABLED for this call.\n- Cache root: ${options.cacheRoot}\n- Use checkout path pattern: ${options.cacheRoot}/github.com/<owner>/<repo>\n- Reuse an existing checkout when it has a .git directory. Fetch/prune before relying on it: git -C "$DIR" fetch --all --prune --tags --quiet\n- If missing, clone with gh repo clone "$REPO" "$DIR" (or git clone https://github.com/$REPO.git "$DIR").\n- If a ref/branch/SHA is requested, fetch it and check it out locally before citing files from that ref.\n- After using a checkout, update its cache marker: touch "$DIR/${CACHE_MARKER_FILE}"\n- Prefer local rg/read inside cached checkouts once a repo is cloned, and cite absolute cached paths with line ranges.\n- Clone only repositories that are relevant to the query; do not bulk-clone broad owner/org scopes unless necessary.`
|
|
1046
1220
|
: `\nLocal checkout cache is DISABLED for this call.\n- Do not clone repositories.\n- Use gh search/API/tree/contents calls and cache only necessary proof files under ${options.workspace}/repos/<owner>/<repo>/<path>.`;
|
|
1047
1221
|
|
|
1048
|
-
return `You are Librarian, an evidence-first GitHub code scout running inside pi.\n\nUse
|
|
1222
|
+
return `You are Librarian, an evidence-first GitHub code scout running inside pi.\n\nUse the available bash and read tools for local inspection, and any configured MCP tools for additional context (MCP tools run without confirmation inside this subagent; use them only to gather 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). Use gh, jq, rg, find/fd, ls, stat, mkdir, base64, and nl -ba for GitHub reconnaissance and numbered evidence. Use read for focused local file inspection.\n\nWorkspace: ${options.workspace}\nDefault gh search limit: ${options.maxSearchResults}\nTurn budget: ${MAX_TURNS} turns total, including your final answer. Stop searching once you have enough evidence.\n${cacheSection}\n\nPerformance guidance:\n- Prefer fast, parallel exploration. When probes are independent, issue multiple separate tool calls in the same assistant turn instead of waiting for each result one-by-one.\n- Aim for 4-8 independent bash/read calls in an exploration turn when useful: parallel gh searches, tree probes, contents fetches, and targeted file reads.\n- Keep dependent work sequential, and avoid concurrent writes to the same file, checkout, or cache directory.\n\nNon-negotiable constraints:\n- Never treat gh search snippets as proof by themselves. Use fetched files or local checkouts for code-content claims.\n- Keep temporary workspace writes under ${options.workspace}/repos unless local checkout cache is enabled, in which case writes under the cache root are also allowed.
|
|
1049
1223
|
- A runtime guard blocks destructive shell commands, credential/environment inspection, and reads outside the workspace/cache.\n- Never paste whole files. Use short snippets only when they clarify the evidence.\n- If evidence is partial or access fails (404/403), state the limitation clearly.\n- Do not present anything as fact unless it appeared in tool output or in a file you read.\n\nRecommended search flow:\n1. If symbols/text are known, start with gh search code and the provided repo/owner filters.\n2. If a repo is known but paths are unclear, resolve the default branch and inspect the git tree or contents API.\n3. Fetch or clone only the files/repos required to prove the answer.\n4. Use rg/read/nl -ba locally to produce stable path and line evidence.\n\nUseful gh patterns:\n- gh repo view "$REPO" --json defaultBranchRef --jq '.defaultBranchRef.name'\n- gh search code '<terms>' --json path,repository,sha,url,textMatches --limit ${options.maxSearchResults}\n- gh api "repos/$REPO/git/trees/$REF?recursive=1" > tree.json\n- gh api "repos/$REPO/contents/$FILE?ref=$REF" --jq .content | tr -d '\\n' | base64 --decode > "repos/$REPO/$FILE"\n- rg -n '<pattern>' '<local path>'\n- nl -ba '<local file>' | sed -n '10,30p'\n\nOutput format, exact order:\n## Summary\n1-3 concise sentences.\n## Locations\n- \`path\` or \`path:lineStart-lineEnd\` — what is here and why it matters; include GitHub URL when useful. If nothing relevant is found, write \`- (none)\`.\n## Evidence\n- \`path\` or \`path:lineStart-lineEnd\` — what this proves. Include concise snippets only if useful.\n## Searched\nOnly include when incomplete/not found or when the search path matters. List queries, filters, and probes used.\n## Next steps\nOptional: 1-3 narrow follow-up checks for remaining ambiguity.`;
|
|
1050
1224
|
}
|
|
1051
1225
|
|
|
@@ -1277,13 +1451,19 @@ function renderAnswer(details: LibrarianDetails): string {
|
|
|
1277
1451
|
}
|
|
1278
1452
|
|
|
1279
1453
|
export const __test__ = {
|
|
1454
|
+
SUBAGENT_MCP_TOOLS,
|
|
1455
|
+
isMcpCompatibleVersion,
|
|
1456
|
+
resolveSubagentProjectTrusted,
|
|
1457
|
+
createMcpSubagentFactories,
|
|
1458
|
+
disposeSubagentSession,
|
|
1459
|
+
createLibrarianRuntimeGuardExtension,
|
|
1460
|
+
resolveToCwd,
|
|
1280
1461
|
buildLibrarianCandidates,
|
|
1281
1462
|
findAvailableModel,
|
|
1282
1463
|
isModelAvailabilityError,
|
|
1283
1464
|
parseModelPreference,
|
|
1284
1465
|
resolveThinkingLevel,
|
|
1285
1466
|
aggregateAssistantUsage,
|
|
1286
|
-
addAssistantMessageUsage,
|
|
1287
1467
|
addSessionEventUsage,
|
|
1288
1468
|
inspectFinalAssistant,
|
|
1289
1469
|
classifyRunFailure,
|
|
@@ -1291,14 +1471,12 @@ export const __test__ = {
|
|
|
1291
1471
|
createLibrarianAbortController,
|
|
1292
1472
|
cleanupExpiredCache,
|
|
1293
1473
|
cleanupExpiredRepos,
|
|
1294
|
-
cleanupLockOwnershipError,
|
|
1295
1474
|
tryAcquireCleanupLock,
|
|
1296
1475
|
heartbeatCleanupLock,
|
|
1297
1476
|
releaseCleanupLock,
|
|
1298
1477
|
CLEANUP_LOCK_FILE,
|
|
1299
1478
|
CLEANUP_LOCK_STALE_MS,
|
|
1300
1479
|
CLEANUP_LOCK_MAX_LEASE_MS,
|
|
1301
|
-
isCleanupLockLeaseExpired,
|
|
1302
1480
|
};
|
|
1303
1481
|
|
|
1304
1482
|
export default function librarianExtension(pi: ExtensionAPI) {
|
|
@@ -1469,17 +1647,6 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1469
1647
|
const cacheRoot = getUserCacheReposRoot();
|
|
1470
1648
|
const configPath = getCacheConfigPath();
|
|
1471
1649
|
|
|
1472
|
-
const setPreference = async (mode: CacheMode): Promise<string | undefined> => {
|
|
1473
|
-
cachePreference = mode;
|
|
1474
|
-
try {
|
|
1475
|
-
await writeCachePreference(mode);
|
|
1476
|
-
return undefined;
|
|
1477
|
-
} catch (error) {
|
|
1478
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1479
|
-
return `Preference changed for this process, but could not save ${configPath}: ${message}`;
|
|
1480
|
-
}
|
|
1481
|
-
};
|
|
1482
|
-
|
|
1483
1650
|
const formatSetMessage = (mode: CacheMode, warning?: string): string => {
|
|
1484
1651
|
const main =
|
|
1485
1652
|
mode === "enabled"
|
|
@@ -1489,20 +1656,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1489
1656
|
};
|
|
1490
1657
|
|
|
1491
1658
|
if (action === "on" || action === "enable") {
|
|
1492
|
-
const warning = await
|
|
1659
|
+
const warning = await savePreferences({ ...currentPreferences(), cacheMode: "enabled" });
|
|
1493
1660
|
notifyCommand(ctx, formatSetMessage("enabled", warning), warning ? "warning" : "info");
|
|
1494
1661
|
return;
|
|
1495
1662
|
}
|
|
1496
1663
|
|
|
1497
1664
|
if (action === "off" || action === "disable") {
|
|
1498
|
-
const warning = await
|
|
1665
|
+
const warning = await savePreferences({ ...currentPreferences(), cacheMode: "disabled" });
|
|
1499
1666
|
notifyCommand(ctx, formatSetMessage("disabled", warning), warning ? "warning" : "info");
|
|
1500
1667
|
return;
|
|
1501
1668
|
}
|
|
1502
1669
|
|
|
1503
1670
|
if (action === "toggle") {
|
|
1504
1671
|
const next = cachePreference === "enabled" ? "disabled" : "enabled";
|
|
1505
|
-
const warning = await
|
|
1672
|
+
const warning = await savePreferences({ ...currentPreferences(), cacheMode: next });
|
|
1506
1673
|
notifyCommand(ctx, formatSetMessage(next, warning), warning ? "warning" : "info");
|
|
1507
1674
|
return;
|
|
1508
1675
|
}
|
|
@@ -1652,7 +1819,10 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1652
1819
|
// Keep the research session from inheriting user/project tool, shell,
|
|
1653
1820
|
// retry, or provider-request settings. The host model runtime is still
|
|
1654
1821
|
// passed explicitly below so the selected provider/auth policy is kept.
|
|
1655
|
-
|
|
1822
|
+
// workspace is a temp clone dir — always a different dir from ctx.cwd,
|
|
1823
|
+
// so projectTrusted is always false here (project .pi/mcp.json is never loaded).
|
|
1824
|
+
const projectTrusted = resolveSubagentProjectTrusted(ctx, workspace);
|
|
1825
|
+
const isolatedSettingsManager = SettingsManager.inMemory({}, { projectTrusted });
|
|
1656
1826
|
const resourceLoader = new DefaultResourceLoader({
|
|
1657
1827
|
cwd: workspace,
|
|
1658
1828
|
agentDir: getAgentDir(),
|
|
@@ -1669,6 +1839,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1669
1839
|
cacheRoot,
|
|
1670
1840
|
cacheEnabled: cacheDecision.enabled,
|
|
1671
1841
|
}),
|
|
1842
|
+
...createMcpSubagentFactories(),
|
|
1672
1843
|
],
|
|
1673
1844
|
systemPromptOverride: () => systemPrompt,
|
|
1674
1845
|
skillsOverride: () => ({ skills: [], diagnostics: [] }),
|
|
@@ -1693,43 +1864,48 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1693
1864
|
sessionManager: SessionManager.inMemory(workspace),
|
|
1694
1865
|
model: candidate.model,
|
|
1695
1866
|
thinkingLevel: candidate.details.thinkingLevel,
|
|
1696
|
-
tools: ["read", "bash"],
|
|
1867
|
+
tools: ["read", "bash", ...SUBAGENT_MCP_TOOLS],
|
|
1697
1868
|
});
|
|
1698
1869
|
|
|
1870
|
+
// Assign session BEFORE bindExtensions so the finally block can dispose it
|
|
1871
|
+
// even if binding fails partway through (e.g. a server starts then an
|
|
1872
|
+
// extension's session_start throws).
|
|
1699
1873
|
session = created.session as typeof session;
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
call
|
|
1874
|
+
try {
|
|
1875
|
+
// Connect extensions (MCP servers) before prompting. Feature-detected: Pi >=0.99.0.
|
|
1876
|
+
await (created.session as any).bindExtensions?.({ mode: "json" });
|
|
1877
|
+
if (abortController.callerAborted) abortController.abortForCleanup();
|
|
1878
|
+
unsubscribe = (created.session as any).subscribe((event: any) => {
|
|
1879
|
+
addSessionEventUsage(usage, event);
|
|
1880
|
+
switch (event.type) {
|
|
1881
|
+
case "turn_end":
|
|
1882
|
+
details.turns += 1;
|
|
1883
|
+
emit();
|
|
1884
|
+
break;
|
|
1885
|
+
case "tool_execution_start":
|
|
1886
|
+
details.toolCalls.push({
|
|
1887
|
+
id: event.toolCallId,
|
|
1888
|
+
name: event.toolName,
|
|
1889
|
+
args: event.args,
|
|
1890
|
+
startedAt: Date.now(),
|
|
1891
|
+
});
|
|
1892
|
+
if (details.toolCalls.length > MAX_TOOL_CALLS_TO_KEEP) {
|
|
1893
|
+
details.toolCalls.splice(0, details.toolCalls.length - MAX_TOOL_CALLS_TO_KEEP);
|
|
1894
|
+
}
|
|
1895
|
+
emit();
|
|
1896
|
+
break;
|
|
1897
|
+
case "tool_execution_end": {
|
|
1898
|
+
const call = details.toolCalls.find((item) => item.id === event.toolCallId);
|
|
1899
|
+
if (call) {
|
|
1900
|
+
call.endedAt = Date.now();
|
|
1901
|
+
call.isError = event.isError;
|
|
1902
|
+
}
|
|
1903
|
+
emit();
|
|
1904
|
+
break;
|
|
1725
1905
|
}
|
|
1726
|
-
emit();
|
|
1727
|
-
break;
|
|
1728
1906
|
}
|
|
1729
|
-
}
|
|
1730
|
-
});
|
|
1907
|
+
});
|
|
1731
1908
|
|
|
1732
|
-
try {
|
|
1733
1909
|
if (!abortController.callerAborted) {
|
|
1734
1910
|
const promptPromise = created.session.prompt(
|
|
1735
1911
|
buildUserPrompt(query, repos, owners, maxSearchResults, details.cache),
|
|
@@ -1758,7 +1934,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1758
1934
|
}
|
|
1759
1935
|
unsubscribe?.();
|
|
1760
1936
|
unsubscribe = undefined;
|
|
1761
|
-
session
|
|
1937
|
+
await disposeSubagentSession(session);
|
|
1762
1938
|
session = undefined;
|
|
1763
1939
|
}
|
|
1764
1940
|
};
|
|
@@ -1841,7 +2017,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1841
2017
|
if (runTimeout) clearTimeout(runTimeout);
|
|
1842
2018
|
if (signal && abortListenerAdded) signal.removeEventListener("abort", abortFromCaller);
|
|
1843
2019
|
unsubscribe?.();
|
|
1844
|
-
session
|
|
2020
|
+
await disposeSubagentSession(session);
|
|
1845
2021
|
}
|
|
1846
2022
|
},
|
|
1847
2023
|
|
|
@@ -10,7 +10,7 @@ It replaces pi's built-in footer with a cleaner two-line layout that focuses on
|
|
|
10
10
|
- TLH-style git dirty counts, ahead/behind counts, and optional PR number
|
|
11
11
|
- current repo name
|
|
12
12
|
- current context percentage
|
|
13
|
-
- red `DUMB ZONE` indicator when context usage is above
|
|
13
|
+
- red `DUMB ZONE` indicator when context usage is above 350k tokens
|
|
14
14
|
- current model and thinking level
|
|
15
15
|
- OpenAI Codex usage windows, labeled from the durations reported by OpenAI
|
|
16
16
|
- `xp` marker when Pi experimental features are enabled
|
|
@@ -39,7 +39,7 @@ main · +1 ~2 ?3 ↑1 • PR #42
|
|
|
39
39
|
|
|
40
40
|
The markers are `!` conflicts, `+` staged, `~` unstaged, `?` untracked, `↑` ahead, and `↓` behind. PR numbers are fetched best-effort with `gh pr view`. Git polling is skipped until Pi reports that the project is trusted, and `git status` is run with fsmonitor disabled.
|
|
41
41
|
|
|
42
|
-
When context usage is above
|
|
42
|
+
When context usage is above 350k tokens, the bottom-left line includes a red warning:
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
45
|
44.1% · DUMB ZONE
|
|
@@ -115,7 +115,7 @@ Example:
|
|
|
115
115
|
"showPercent": true,
|
|
116
116
|
"dumbZone": {
|
|
117
117
|
"enabled": true,
|
|
118
|
-
"thresholdTokens":
|
|
118
|
+
"thresholdTokens": 350000,
|
|
119
119
|
"label": "DUMB ZONE",
|
|
120
120
|
"color": "error"
|
|
121
121
|
}
|
|
@@ -231,7 +231,7 @@ Disable git dirty/ahead/PR status:
|
|
|
231
231
|
|
|
232
232
|
- **Top left:** current git branch plus dirty/ahead/PR status when available
|
|
233
233
|
- **Top right:** current repo directory name
|
|
234
|
-
- **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above
|
|
234
|
+
- **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above 350k context tokens
|
|
235
235
|
- **Bottom left on `openai-codex`:** current context usage percentage plus the Codex usage windows reported for the account
|
|
236
236
|
- **Bottom left on `openai` ChatGPT OAuth:** current context usage percentage plus `usage unsupported`; no legacy WHAM request is attempted
|
|
237
237
|
- **Bottom left with `PI_EXPERIMENTAL=1`:** current context usage percentage plus `xp`
|
|
@@ -246,7 +246,7 @@ This extension also lives inside the broader [`pi-extensions`](../../README.md)
|
|
|
246
246
|
- Replaces pi's built-in footer entirely.
|
|
247
247
|
- Uses pi footer data for git branch updates, plus background cached git/gh checks for dirty counts and PR number after project trust is granted.
|
|
248
248
|
- Shows only context percentage, not context window size.
|
|
249
|
-
- Shows `DUMB ZONE` only while context usage is above
|
|
249
|
+
- Shows `DUMB ZONE` only while context usage is above 350k tokens.
|
|
250
250
|
- Shows the model id rather than a provider-specific display label.
|
|
251
251
|
- Shows `xp` when `PI_EXPERIMENTAL=1`.
|
|
252
252
|
- For `openai-codex`, reads pi's stored OAuth login and fetches usage from ChatGPT's backend usage endpoint. Window labels are inferred from each window's reported duration; missing or unrecognized durations use neutral `usage` labels rather than guessing from window position.
|
|
@@ -24,7 +24,7 @@ const DEFAULT_CONFIG: MinimalFooterConfig = {
|
|
|
24
24
|
showPercent: true,
|
|
25
25
|
dumbZone: {
|
|
26
26
|
enabled: true,
|
|
27
|
-
thresholdTokens:
|
|
27
|
+
thresholdTokens: 350_000,
|
|
28
28
|
label: "DUMB ZONE",
|
|
29
29
|
color: "error",
|
|
30
30
|
},
|
|
@@ -121,7 +121,6 @@ type UsageSessionState = {
|
|
|
121
121
|
snapshot?: UsageSnapshot;
|
|
122
122
|
lastFetchedAt?: number;
|
|
123
123
|
loading: boolean;
|
|
124
|
-
error?: string;
|
|
125
124
|
availability?: UsageAvailability;
|
|
126
125
|
usageIdentity?: UsageIdentity;
|
|
127
126
|
requestVersion: number;
|
|
@@ -149,10 +148,6 @@ type GitStatusSnapshot = {
|
|
|
149
148
|
|
|
150
149
|
type PullRequestSnapshot = {
|
|
151
150
|
number?: number | string;
|
|
152
|
-
state?: string;
|
|
153
|
-
isDraft?: boolean;
|
|
154
|
-
url?: string;
|
|
155
|
-
title?: string;
|
|
156
151
|
};
|
|
157
152
|
|
|
158
153
|
type CommandResult = {
|
|
@@ -201,7 +196,7 @@ const GIT_STATUS_ARGS = [
|
|
|
201
196
|
"--porcelain=v2",
|
|
202
197
|
"--branch",
|
|
203
198
|
] as const;
|
|
204
|
-
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number
|
|
199
|
+
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number"] as const;
|
|
205
200
|
|
|
206
201
|
function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
|
|
207
202
|
if (!existsSync(path)) return {};
|
|
@@ -469,10 +464,6 @@ function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
|
|
|
469
464
|
if (typeof record.number === "number" || typeof record.number === "string") {
|
|
470
465
|
snapshot.number = record.number;
|
|
471
466
|
}
|
|
472
|
-
if (typeof record.state === "string") snapshot.state = record.state;
|
|
473
|
-
if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
|
|
474
|
-
if (typeof record.url === "string") snapshot.url = record.url;
|
|
475
|
-
if (typeof record.title === "string") snapshot.title = record.title;
|
|
476
467
|
return snapshot;
|
|
477
468
|
}
|
|
478
469
|
|
|
@@ -495,13 +486,7 @@ function pullRequestSnapshotsEqual(
|
|
|
495
486
|
left: PullRequestSnapshot | undefined,
|
|
496
487
|
right: PullRequestSnapshot | undefined,
|
|
497
488
|
): boolean {
|
|
498
|
-
return
|
|
499
|
-
left?.number === right?.number &&
|
|
500
|
-
left?.state === right?.state &&
|
|
501
|
-
left?.isDraft === right?.isDraft &&
|
|
502
|
-
left?.url === right?.url &&
|
|
503
|
-
left?.title === right?.title
|
|
504
|
-
);
|
|
489
|
+
return left?.number === right?.number;
|
|
505
490
|
}
|
|
506
491
|
|
|
507
492
|
function defaultRunner(
|
|
@@ -834,7 +819,6 @@ function clearUsageState(state: UsageSessionState): void {
|
|
|
834
819
|
state.snapshot = undefined;
|
|
835
820
|
state.lastFetchedAt = undefined;
|
|
836
821
|
state.loading = false;
|
|
837
|
-
state.error = undefined;
|
|
838
822
|
state.availability = undefined;
|
|
839
823
|
}
|
|
840
824
|
|
|
@@ -909,7 +893,6 @@ function invalidateUsageState(state: UsageSessionState, usageIdentity?: UsageIde
|
|
|
909
893
|
state.snapshot = undefined;
|
|
910
894
|
state.lastFetchedAt = undefined;
|
|
911
895
|
state.loading = false;
|
|
912
|
-
state.error = undefined;
|
|
913
896
|
state.availability = undefined;
|
|
914
897
|
state.usageIdentity = usageIdentity;
|
|
915
898
|
// The previous promise may still be resolving. Clearing the reference lets
|
|
@@ -961,7 +944,7 @@ async function refreshUsageIfNeeded(
|
|
|
961
944
|
|
|
962
945
|
const mode = getUsageMode(ctx, config, resolvedUsageIdentity);
|
|
963
946
|
if (!mode) {
|
|
964
|
-
if (state.snapshot || state.availability || state.loading
|
|
947
|
+
if (state.snapshot || state.availability || state.loading) {
|
|
965
948
|
clearUsageState(state);
|
|
966
949
|
state.requestRender?.();
|
|
967
950
|
}
|
|
@@ -1006,19 +989,16 @@ async function refreshUsageIfNeeded(
|
|
|
1006
989
|
if (snapshot) {
|
|
1007
990
|
state.snapshot = snapshot;
|
|
1008
991
|
state.lastFetchedAt = snapshot.fetchedAt;
|
|
1009
|
-
state.error = undefined;
|
|
1010
992
|
} else {
|
|
1011
993
|
state.snapshot = undefined;
|
|
1012
994
|
state.lastFetchedAt = Date.now();
|
|
1013
|
-
state.error = undefined;
|
|
1014
995
|
state.availability = "unavailable";
|
|
1015
996
|
}
|
|
1016
|
-
} catch
|
|
997
|
+
} catch {
|
|
1017
998
|
if (!usageRequestStillCurrent(ctx, state, requestVersion, requestUsageKey)) return;
|
|
1018
999
|
state.snapshot = undefined;
|
|
1019
1000
|
state.lastFetchedAt = Date.now();
|
|
1020
1001
|
state.availability = "unavailable";
|
|
1021
|
-
state.error = error instanceof Error ? error.message : String(error);
|
|
1022
1002
|
} finally {
|
|
1023
1003
|
if (
|
|
1024
1004
|
state.disposed ||
|
|
@@ -1085,7 +1065,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1085
1065
|
state.snapshot = undefined;
|
|
1086
1066
|
state.lastFetchedAt = undefined;
|
|
1087
1067
|
state.loading = false;
|
|
1088
|
-
state.error = undefined;
|
|
1089
1068
|
state.availability = undefined;
|
|
1090
1069
|
state.usageIdentity = undefined;
|
|
1091
1070
|
state.inflight = undefined;
|