@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.
- package/README.md +8 -8
- package/extensions/agent-workflow-audit/README.md +1 -1
- package/extensions/agent-workflow-audit/index.ts +277 -44
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/git.ts +0 -8
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/web/index.html +3 -3
- package/extensions/code-reviewer/README.md +5 -5
- package/extensions/code-reviewer/index.ts +258 -54
- 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/contrarian/README.md +4 -3
- package/extensions/contrarian/index.ts +47 -40
- 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 +5 -5
- package/extensions/dynamic-context-pruning/index.ts +114 -232
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +2 -2
- package/extensions/fast/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 +247 -39
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +3 -10
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/README.md +4 -3
- package/extensions/oracle/index.ts +45 -38
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +3 -0
- package/extensions/permission-gate/index.ts +151 -28
- 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/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 +349 -29
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +4 -4
|
@@ -143,13 +143,13 @@ Optional global config:
|
|
|
143
143
|
~/<pi-config-dir>/agent/extensions/fast.json
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
Optional project config:
|
|
146
|
+
Optional project config, from the nearest ancestor of the working directory (the working directory itself counts):
|
|
147
147
|
|
|
148
148
|
```text
|
|
149
149
|
<project>/<pi-config-dir>/fast.json
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
Here `<pi-config-dir>` is Pi's runtime config directory name (`CONFIG_DIR_NAME`; `.pi` by default).
|
|
152
|
+
Here `<pi-config-dir>` is Pi's runtime config directory name (`CONFIG_DIR_NAME`; `.pi` by default). The loader uses that nearest ancestor `fast.json`, which overrides global config after Pi reports that the project is trusted.
|
|
153
153
|
|
|
154
154
|
```json
|
|
155
155
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-fast",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "A pi extension that enables Fast mode for supported direct OpenAI API, OpenAI Codex, and Anthropic Claude models based on the currently selected model.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inline Bash Extension - expands inline bash commands in user prompts.
|
|
3
|
-
*
|
|
4
|
-
* Start pi with this extension:
|
|
5
|
-
* pi -e ./extensions/inline-bash/index.ts
|
|
6
|
-
*
|
|
7
|
-
* Then type prompts with inline bash:
|
|
8
|
-
* What's in !{pwd}?
|
|
9
|
-
* The current branch is !{git branch --show-current} and status: !{git status --short}
|
|
10
|
-
* My node version is !{node --version}
|
|
11
|
-
*
|
|
12
|
-
* The !{command} patterns are executed and replaced with their trimmed output
|
|
13
|
-
* before the prompt is sent to the agent. Very large expansions are truncated.
|
|
14
|
-
*
|
|
15
|
-
* Note: Regular !command syntax (whole-line bash) is preserved and works as before.
|
|
16
|
-
*/
|
|
17
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
2
|
|
|
19
3
|
export default function (pi: ExtensionAPI) {
|
|
@@ -35,7 +35,7 @@ Then reload pi:
|
|
|
35
35
|
## Behavior
|
|
36
36
|
|
|
37
37
|
- Tool name: `librarian`
|
|
38
|
-
- Uses a restricted subagent with `bash` and `
|
|
38
|
+
- Uses a restricted subagent with `bash`, `read`, and any configured MCP tools (MCP tools run without confirmation; may have side effects; use for evidence gathering only; requires host Pi >=1.0.4; older hosts degrade to no MCP, no error); the global `mcp.json` always applies; a project `.pi/mcp.json` is never loaded (the librarian subagent runs in a separate temp workspace)
|
|
39
39
|
- Uses `gh` for GitHub search/API access
|
|
40
40
|
- Uses cached local checkouts only when enabled
|
|
41
41
|
- Toggle cache behavior for future calls with `/librarian-cache on | off | toggle | status`
|
|
@@ -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/,
|
|
@@ -903,6 +982,82 @@ async function buildLibrarianCandidates(
|
|
|
903
982
|
return candidates;
|
|
904
983
|
}
|
|
905
984
|
|
|
985
|
+
const UNICODE_SPACES_PATTERN = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Mirrors normalizeWindowsShellPath from dist/utils/paths.js. No-op on
|
|
989
|
+
* non-win32 paths.
|
|
990
|
+
*/
|
|
991
|
+
function normalizeWindowsShellPathLocal(filePath: string): string {
|
|
992
|
+
if (!filePath.startsWith("/") || filePath.startsWith("//") || filePath.includes("\\"))
|
|
993
|
+
return filePath;
|
|
994
|
+
const match = filePath.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
|
|
995
|
+
if (!match) return filePath;
|
|
996
|
+
const suffix = match[2]?.replaceAll("/", "\\");
|
|
997
|
+
return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Mirrors Pi's normalizePath (dist/utils/paths.js). On win32, Git Bash / WSL
|
|
1002
|
+
* drive-path normalisation always runs; only tilde expansion is gated by
|
|
1003
|
+
* expandTilde.
|
|
1004
|
+
*/
|
|
1005
|
+
function normalizePath(
|
|
1006
|
+
input: string,
|
|
1007
|
+
opts: { normalizeUnicodeSpaces?: boolean; stripAtPrefix?: boolean; expandTilde?: boolean } = {},
|
|
1008
|
+
): string {
|
|
1009
|
+
const { normalizeUnicodeSpaces = false, stripAtPrefix = false, expandTilde = true } = opts;
|
|
1010
|
+
let p = input;
|
|
1011
|
+
if (normalizeUnicodeSpaces) p = p.replace(UNICODE_SPACES_PATTERN, " ");
|
|
1012
|
+
if (stripAtPrefix && p.startsWith("@")) p = p.slice(1);
|
|
1013
|
+
if (process.platform === "win32") p = normalizeWindowsShellPathLocal(p);
|
|
1014
|
+
if (expandTilde) {
|
|
1015
|
+
const home = os.homedir();
|
|
1016
|
+
if (p === "~") p = home;
|
|
1017
|
+
else if (p.startsWith("~/") || (process.platform === "win32" && p.startsWith("~\\")))
|
|
1018
|
+
p = path.join(home, p.slice(2));
|
|
1019
|
+
}
|
|
1020
|
+
if (/^file:\/\//.test(p)) p = fileURLToPath(p);
|
|
1021
|
+
return p;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Mirrors Pi's resolveToCwd (dist/core/tools/path-utils.js). Exposed via
|
|
1026
|
+
* __test__ for parity tests; not imported by other extensions.
|
|
1027
|
+
*/
|
|
1028
|
+
function resolveToCwd(rawPath: string, cwd: string): string {
|
|
1029
|
+
const normalized = normalizePath(rawPath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
|
|
1030
|
+
const base = normalizePath(cwd);
|
|
1031
|
+
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(base, normalized);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/** Mirrors tryMacOSScreenshotPath from dist/core/tools/path-utils.js */
|
|
1035
|
+
function tryMacOSScreenshotPathVariant(filePath: string): string {
|
|
1036
|
+
return filePath.replace(/ (AM|PM)\./gi, " $1.");
|
|
1037
|
+
}
|
|
1038
|
+
/** Mirrors tryNFDVariant from dist/core/tools/path-utils.js */
|
|
1039
|
+
function tryNFDPathVariant(filePath: string): string {
|
|
1040
|
+
return filePath.normalize("NFD");
|
|
1041
|
+
}
|
|
1042
|
+
/** Mirrors tryCurlyQuoteVariant from dist/core/tools/path-utils.js */
|
|
1043
|
+
function tryCurlyQuotePathVariant(filePath: string): string {
|
|
1044
|
+
return filePath.replace(/'/g, "’");
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Returns all read path candidates including Pi's resolveReadPathAsync fallback
|
|
1048
|
+
* variants. Used only for the read tool guard; grep/find/ls use resolveToCwd only.
|
|
1049
|
+
*/
|
|
1050
|
+
function getReadPathCandidates(resolved: string): string[] {
|
|
1051
|
+
const nfd = tryNFDPathVariant(resolved);
|
|
1052
|
+
return [
|
|
1053
|
+
resolved,
|
|
1054
|
+
tryMacOSScreenshotPathVariant(resolved),
|
|
1055
|
+
nfd,
|
|
1056
|
+
tryCurlyQuotePathVariant(resolved),
|
|
1057
|
+
tryCurlyQuotePathVariant(nfd),
|
|
1058
|
+
];
|
|
1059
|
+
}
|
|
1060
|
+
|
|
906
1061
|
function resolveToolPath(cwd: string, rawPath: string): string {
|
|
907
1062
|
const normalized = rawPath.startsWith("@") ? rawPath.slice(1) : rawPath;
|
|
908
1063
|
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
|
|
@@ -992,16 +1147,53 @@ function createLibrarianRuntimeGuardExtension(options: {
|
|
|
992
1147
|
if (event.toolName === "read") {
|
|
993
1148
|
const input = event.input as { path?: unknown };
|
|
994
1149
|
if (typeof input.path !== "string") return undefined;
|
|
1150
|
+
// Canonicalize allowed roots. A path is allowed if it is inside EITHER
|
|
1151
|
+
// the original root (options.workspace / options.cacheRoot) OR its
|
|
1152
|
+
// realpath'd form. This handles both: a realpath'd file under a
|
|
1153
|
+
// symlinked alias workspace (lexical match on original root) and an
|
|
1154
|
+
// existing file canonicalized beyond the alias (match on real root).
|
|
1155
|
+
const realWorkspace = await fs
|
|
1156
|
+
.realpath(options.workspace)
|
|
1157
|
+
.catch(() => path.resolve(options.workspace));
|
|
1158
|
+
const realCacheRoot = await fs
|
|
1159
|
+
.realpath(options.cacheRoot)
|
|
1160
|
+
.catch(() => path.resolve(options.cacheRoot));
|
|
1161
|
+
function isAllowedPath(p: string): boolean {
|
|
1162
|
+
return (
|
|
1163
|
+
isInside(options.workspace, p) ||
|
|
1164
|
+
isInside(realWorkspace, p) ||
|
|
1165
|
+
(options.cacheEnabled && (isInside(options.cacheRoot, p) || isInside(realCacheRoot, p)))
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
995
1168
|
const resolved = resolveToolPath(options.workspace, input.path);
|
|
996
1169
|
const realPath = await fs.realpath(resolved).catch(() => resolved);
|
|
997
|
-
|
|
998
|
-
isInside(options.workspace, realPath) ||
|
|
999
|
-
(options.cacheEnabled && isInside(options.cacheRoot, realPath));
|
|
1000
|
-
if (!allowed)
|
|
1170
|
+
if (!isAllowedPath(realPath))
|
|
1001
1171
|
return {
|
|
1002
1172
|
block: true,
|
|
1003
1173
|
reason: `Librarian read is limited to its workspace/cache: ${realPath}`,
|
|
1004
1174
|
};
|
|
1175
|
+
// Pi-effective path check (monotonic: block if either resolution is
|
|
1176
|
+
// outside). For the read tool, also check Pi's resolveReadPathAsync
|
|
1177
|
+
// fallback variants so a missing file whose variant resolves outside is
|
|
1178
|
+
// not silently allowed.
|
|
1179
|
+
try {
|
|
1180
|
+
const piResolved = resolveToCwd(input.path, options.workspace);
|
|
1181
|
+
const candidates = getReadPathCandidates(piResolved);
|
|
1182
|
+
// Deliberate policy: all candidates are checked regardless of existence (not
|
|
1183
|
+
// Pi's existence-ordered selection) so the guard cannot be raced by creating
|
|
1184
|
+
// a variant between the hook and the read. Trade-off: a read is blocked when
|
|
1185
|
+
// an escaping variant symlink sits next to a safe primary file.
|
|
1186
|
+
for (const candidate of candidates) {
|
|
1187
|
+
const candidateRealPath = await fs.realpath(candidate).catch(() => candidate);
|
|
1188
|
+
if (!isAllowedPath(candidateRealPath))
|
|
1189
|
+
return {
|
|
1190
|
+
block: true,
|
|
1191
|
+
reason: `Librarian read is limited to its workspace/cache: ${candidateRealPath}`,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
} catch {
|
|
1195
|
+
return { block: true, reason: "Librarian read path could not be resolved safely." };
|
|
1196
|
+
}
|
|
1005
1197
|
}
|
|
1006
1198
|
|
|
1007
1199
|
if (event.toolName === "bash") {
|
|
@@ -1045,7 +1237,7 @@ function buildSystemPrompt(options: {
|
|
|
1045
1237
|
? `\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
1238
|
: `\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
1239
|
|
|
1048
|
-
return `You are Librarian, an evidence-first GitHub code scout running inside pi.\n\nUse
|
|
1240
|
+
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
1241
|
- 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
1242
|
}
|
|
1051
1243
|
|
|
@@ -1277,6 +1469,13 @@ function renderAnswer(details: LibrarianDetails): string {
|
|
|
1277
1469
|
}
|
|
1278
1470
|
|
|
1279
1471
|
export const __test__ = {
|
|
1472
|
+
SUBAGENT_MCP_TOOLS,
|
|
1473
|
+
isMcpCompatibleVersion,
|
|
1474
|
+
resolveSubagentProjectTrusted,
|
|
1475
|
+
createMcpSubagentFactories,
|
|
1476
|
+
disposeSubagentSession,
|
|
1477
|
+
createLibrarianRuntimeGuardExtension,
|
|
1478
|
+
resolveToCwd,
|
|
1280
1479
|
buildLibrarianCandidates,
|
|
1281
1480
|
findAvailableModel,
|
|
1282
1481
|
isModelAvailabilityError,
|
|
@@ -1652,7 +1851,10 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1652
1851
|
// Keep the research session from inheriting user/project tool, shell,
|
|
1653
1852
|
// retry, or provider-request settings. The host model runtime is still
|
|
1654
1853
|
// passed explicitly below so the selected provider/auth policy is kept.
|
|
1655
|
-
|
|
1854
|
+
// workspace is a temp clone dir — always a different dir from ctx.cwd,
|
|
1855
|
+
// so projectTrusted is always false here (project .pi/mcp.json is never loaded).
|
|
1856
|
+
const projectTrusted = resolveSubagentProjectTrusted(ctx, workspace);
|
|
1857
|
+
const isolatedSettingsManager = SettingsManager.inMemory({}, { projectTrusted });
|
|
1656
1858
|
const resourceLoader = new DefaultResourceLoader({
|
|
1657
1859
|
cwd: workspace,
|
|
1658
1860
|
agentDir: getAgentDir(),
|
|
@@ -1669,6 +1871,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1669
1871
|
cacheRoot,
|
|
1670
1872
|
cacheEnabled: cacheDecision.enabled,
|
|
1671
1873
|
}),
|
|
1874
|
+
...createMcpSubagentFactories(),
|
|
1672
1875
|
],
|
|
1673
1876
|
systemPromptOverride: () => systemPrompt,
|
|
1674
1877
|
skillsOverride: () => ({ skills: [], diagnostics: [] }),
|
|
@@ -1693,43 +1896,48 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1693
1896
|
sessionManager: SessionManager.inMemory(workspace),
|
|
1694
1897
|
model: candidate.model,
|
|
1695
1898
|
thinkingLevel: candidate.details.thinkingLevel,
|
|
1696
|
-
tools: ["read", "bash"],
|
|
1899
|
+
tools: ["read", "bash", ...SUBAGENT_MCP_TOOLS],
|
|
1697
1900
|
});
|
|
1698
1901
|
|
|
1902
|
+
// Assign session BEFORE bindExtensions so the finally block can dispose it
|
|
1903
|
+
// even if binding fails partway through (e.g. a server starts then an
|
|
1904
|
+
// extension's session_start throws).
|
|
1699
1905
|
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
|
|
1906
|
+
try {
|
|
1907
|
+
// Connect extensions (MCP servers) before prompting. Feature-detected: Pi >=0.99.0.
|
|
1908
|
+
await (created.session as any).bindExtensions?.({ mode: "json" });
|
|
1909
|
+
if (abortController.callerAborted) abortController.abortForCleanup();
|
|
1910
|
+
unsubscribe = (created.session as any).subscribe((event: any) => {
|
|
1911
|
+
addSessionEventUsage(usage, event);
|
|
1912
|
+
switch (event.type) {
|
|
1913
|
+
case "turn_end":
|
|
1914
|
+
details.turns += 1;
|
|
1915
|
+
emit();
|
|
1916
|
+
break;
|
|
1917
|
+
case "tool_execution_start":
|
|
1918
|
+
details.toolCalls.push({
|
|
1919
|
+
id: event.toolCallId,
|
|
1920
|
+
name: event.toolName,
|
|
1921
|
+
args: event.args,
|
|
1922
|
+
startedAt: Date.now(),
|
|
1923
|
+
});
|
|
1924
|
+
if (details.toolCalls.length > MAX_TOOL_CALLS_TO_KEEP) {
|
|
1925
|
+
details.toolCalls.splice(0, details.toolCalls.length - MAX_TOOL_CALLS_TO_KEEP);
|
|
1926
|
+
}
|
|
1927
|
+
emit();
|
|
1928
|
+
break;
|
|
1929
|
+
case "tool_execution_end": {
|
|
1930
|
+
const call = details.toolCalls.find((item) => item.id === event.toolCallId);
|
|
1931
|
+
if (call) {
|
|
1932
|
+
call.endedAt = Date.now();
|
|
1933
|
+
call.isError = event.isError;
|
|
1934
|
+
}
|
|
1935
|
+
emit();
|
|
1936
|
+
break;
|
|
1725
1937
|
}
|
|
1726
|
-
emit();
|
|
1727
|
-
break;
|
|
1728
1938
|
}
|
|
1729
|
-
}
|
|
1730
|
-
});
|
|
1939
|
+
});
|
|
1731
1940
|
|
|
1732
|
-
try {
|
|
1733
1941
|
if (!abortController.callerAborted) {
|
|
1734
1942
|
const promptPromise = created.session.prompt(
|
|
1735
1943
|
buildUserPrompt(query, repos, owners, maxSearchResults, details.cache),
|
|
@@ -1758,7 +1966,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1758
1966
|
}
|
|
1759
1967
|
unsubscribe?.();
|
|
1760
1968
|
unsubscribe = undefined;
|
|
1761
|
-
session
|
|
1969
|
+
await disposeSubagentSession(session);
|
|
1762
1970
|
session = undefined;
|
|
1763
1971
|
}
|
|
1764
1972
|
};
|
|
@@ -1841,7 +2049,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1841
2049
|
if (runTimeout) clearTimeout(runTimeout);
|
|
1842
2050
|
if (signal && abortListenerAdded) signal.removeEventListener("abort", abortFromCaller);
|
|
1843
2051
|
unsubscribe?.();
|
|
1844
|
-
session
|
|
2052
|
+
await disposeSubagentSession(session);
|
|
1845
2053
|
}
|
|
1846
2054
|
},
|
|
1847
2055
|
|
|
@@ -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 300k 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 300k 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": 300000,
|
|
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 300k 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 300k 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: 300_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;
|
|
@@ -834,7 +833,6 @@ function clearUsageState(state: UsageSessionState): void {
|
|
|
834
833
|
state.snapshot = undefined;
|
|
835
834
|
state.lastFetchedAt = undefined;
|
|
836
835
|
state.loading = false;
|
|
837
|
-
state.error = undefined;
|
|
838
836
|
state.availability = undefined;
|
|
839
837
|
}
|
|
840
838
|
|
|
@@ -909,7 +907,6 @@ function invalidateUsageState(state: UsageSessionState, usageIdentity?: UsageIde
|
|
|
909
907
|
state.snapshot = undefined;
|
|
910
908
|
state.lastFetchedAt = undefined;
|
|
911
909
|
state.loading = false;
|
|
912
|
-
state.error = undefined;
|
|
913
910
|
state.availability = undefined;
|
|
914
911
|
state.usageIdentity = usageIdentity;
|
|
915
912
|
// The previous promise may still be resolving. Clearing the reference lets
|
|
@@ -961,7 +958,7 @@ async function refreshUsageIfNeeded(
|
|
|
961
958
|
|
|
962
959
|
const mode = getUsageMode(ctx, config, resolvedUsageIdentity);
|
|
963
960
|
if (!mode) {
|
|
964
|
-
if (state.snapshot || state.availability || state.loading
|
|
961
|
+
if (state.snapshot || state.availability || state.loading) {
|
|
965
962
|
clearUsageState(state);
|
|
966
963
|
state.requestRender?.();
|
|
967
964
|
}
|
|
@@ -1006,19 +1003,16 @@ async function refreshUsageIfNeeded(
|
|
|
1006
1003
|
if (snapshot) {
|
|
1007
1004
|
state.snapshot = snapshot;
|
|
1008
1005
|
state.lastFetchedAt = snapshot.fetchedAt;
|
|
1009
|
-
state.error = undefined;
|
|
1010
1006
|
} else {
|
|
1011
1007
|
state.snapshot = undefined;
|
|
1012
1008
|
state.lastFetchedAt = Date.now();
|
|
1013
|
-
state.error = undefined;
|
|
1014
1009
|
state.availability = "unavailable";
|
|
1015
1010
|
}
|
|
1016
|
-
} catch
|
|
1011
|
+
} catch {
|
|
1017
1012
|
if (!usageRequestStillCurrent(ctx, state, requestVersion, requestUsageKey)) return;
|
|
1018
1013
|
state.snapshot = undefined;
|
|
1019
1014
|
state.lastFetchedAt = Date.now();
|
|
1020
1015
|
state.availability = "unavailable";
|
|
1021
|
-
state.error = error instanceof Error ? error.message : String(error);
|
|
1022
1016
|
} finally {
|
|
1023
1017
|
if (
|
|
1024
1018
|
state.disposed ||
|
|
@@ -1085,7 +1079,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1085
1079
|
state.snapshot = undefined;
|
|
1086
1080
|
state.lastFetchedAt = undefined;
|
|
1087
1081
|
state.loading = false;
|
|
1088
|
-
state.error = undefined;
|
|
1089
1082
|
state.availability = undefined;
|
|
1090
1083
|
state.usageIdentity = undefined;
|
|
1091
1084
|
state.inflight = undefined;
|
|
@@ -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.
|