@zhalker/gentle-shell-codegraph-subagents 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +45 -2
- package/extensions/index.ts +38 -0
- package/package.json +18 -4
- package/src/agents.ts +111 -0
- package/src/apply.ts +60 -0
- package/src/mcp.ts +25 -0
- package/src/prefetch.ts +142 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zhalker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zhalker/gentle-shell-codegraph-subagents
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pi package that keeps CodeGraph reachable from gentle-shell subagents. On every session start it checks the global agent definitions and `mcp.json` and fixes what gentle-ai or the gentle-pi package left behind. Nothing is rewritten when everything is already right.
|
|
4
|
+
|
|
5
|
+
> **This package edits files in your Pi configuration.** On every session start it rewrites the agent definitions in `~/.pi/agent/agents/` and `~/.pi/agent/subagents/` and the `codegraph` entry in `~/.pi/agent/mcp.json`. It keeps a one-time backup of each file it changes as `<file>.pre-codegraph-subagents.bak`. Read "What it does" before installing, and see "Uninstall" to go back.
|
|
6
|
+
|
|
7
|
+
Upstream issue: Gentleman-Programming/gentle-shell#2804. License: MIT.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
In `~/.pi/agent/agents/` and `~/.pi/agent/subagents/` (or `$PI_CODING_AGENT_DIR`):
|
|
12
|
+
|
|
13
|
+
- Removes the blocks gentle-ai writes between `<!-- gentle-ai:pi-codegraph-tool -->` / `<!-- gentle-ai:pi-codegraph-guidance -->` and `<!-- /gentle-ai:pi-codegraph -->`.
|
|
14
|
+
- Removes the dead `mcp` entry (the retired `pi-mcp-adapter` proxy) and the `"*": false` entry from every `tools:` list.
|
|
15
|
+
- Adds `mcp__codegraph__codegraph_explore` to `gentle-ai-explore`, `gentle-ai-verify`, `gentle-ai-worker`, `jd-fix-agent`, `jd-judge-a` and `jd-judge-b`, removes the `codegraph` extension tool from `gentle-ai-explore`, and rewrites that agent's own CodeGraph instructions to use the MCP tool.
|
|
16
|
+
- Leaves the `review-*` agents without CodeGraph.
|
|
17
|
+
|
|
18
|
+
In `mcp.json`: sets `"exposure": "direct"` on the `codegraph` server. Without it Pi never declares the server's tools to the model and the `tools:` entry above matches nothing.
|
|
19
|
+
|
|
20
|
+
In a subagent that only reads and holds `mcp__codegraph__codegraph_explore` (the explorer), a `before_agent_start` hook also runs `codegraph explore <task>` once before the first model call and hands the output to the model as a message, bounded to 20 s and 12,000 characters. If the project has no `.codegraph/` index, the CLI is missing or fails, nothing is added and the run continues as before.
|
|
21
|
+
|
|
22
|
+
Before the first change to a file it saves the original next to it as `<file>.pre-codegraph-subagents.bak`; later changes never overwrite that backup.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pi install npm:@zhalker/gentle-shell-codegraph-subagents
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Changes apply on the next session start. Reinstalling, or running `gentle-ai sync` again, is corrected the same way on the following start.
|
|
31
|
+
|
|
32
|
+
## Uninstall
|
|
33
|
+
|
|
34
|
+
Remove the package, then copy each `*.pre-codegraph-subagents.bak` over its original if you want the previous files back.
|
|
35
|
+
|
|
36
|
+
## Known limits
|
|
37
|
+
|
|
38
|
+
- Only the global agent directory is handled, not `<project>/.pi/agents/`.
|
|
39
|
+
- The explorer prefetch is recognised by the child's tool set (read-only plus the CodeGraph MCP tool), because gentle-pi does not tell the child which agent it is.
|
|
40
|
+
- The rewrite of the explorer's own instructions matches known sentences; if gentle-pi rewords them, they are left as they are.
|
|
41
|
+
- The `codegraph` MCP server has to exist in `mcp.json`; the package never creates it.
|
|
42
|
+
- Tested with Pi 1.4.0 and node 24.
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
`npm test` runs the unit tests with Node's built-in runner.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { applyCodegraphSubagents } from "../src/apply.ts";
|
|
4
|
+
import { exploreWithCodegraph, prefetchForExplorer } from "../src/prefetch.ts";
|
|
5
|
+
|
|
6
|
+
// Reapplied on every session start so a reinstall (pi install, gentle-shell install)
|
|
7
|
+
// or a gentle-ai sync that rewrote the agent files is corrected on the next launch.
|
|
8
|
+
export default function (pi: ExtensionAPI) {
|
|
9
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
10
|
+
try {
|
|
11
|
+
const { changed, errors } = applyCodegraphSubagents(getAgentDir());
|
|
12
|
+
if (!ctx.hasUI) return;
|
|
13
|
+
if (changed.length > 0) ctx.ui.notify(`gentle-shell-codegraph-subagents: updated ${changed.length} file(s)`, "info");
|
|
14
|
+
if (errors.length > 0) ctx.ui.notify(`gentle-shell-codegraph-subagents: ${errors.length} file(s) skipped: ${errors[0]}`, "warning");
|
|
15
|
+
} catch {
|
|
16
|
+
// Never block a session over this.
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Only the explorer child: hand it `codegraph explore` for its task before its first model call.
|
|
21
|
+
// Without .codegraph, the CLI, or in time, the run goes on exactly as before.
|
|
22
|
+
pi.on("before_agent_start", async (event) => {
|
|
23
|
+
try {
|
|
24
|
+
const prompt = event.messageType === "user" ? event.prompt : undefined;
|
|
25
|
+
const content = await prefetchForExplorer({
|
|
26
|
+
env: process.env,
|
|
27
|
+
prompt,
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
exists: existsSync,
|
|
30
|
+
explore: exploreWithCodegraph,
|
|
31
|
+
});
|
|
32
|
+
if (content === undefined) return undefined;
|
|
33
|
+
return { message: { customType: "codegraph-prefetch", content, display: false } };
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhalker/gentle-shell-codegraph-subagents",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi package that keeps CodeGraph reachable from gentle-shell subagents: normalizes agent tool lists and the codegraph MCP exposure on every session start, and pre-fetches CodeGraph for the explorer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "zhalker",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"keywords": ["pi-package", "pi", "codegraph", "subagents", "gentle-shell", "gentle-ai", "mcp"],
|
|
9
|
+
"files": ["extensions", "src", "README.md", "LICENSE"],
|
|
10
|
+
"pi": {
|
|
11
|
+
"extensions": ["./extensions/index.ts"]
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test \"test/*.test.ts\"",
|
|
15
|
+
"prepublishOnly": "npm test"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const CODEGRAPH_MCP_TOOL = "mcp__codegraph__codegraph_explore";
|
|
2
|
+
|
|
3
|
+
const EXPLORER = "gentle-ai-explore";
|
|
4
|
+
const AGENTS_WITH_MCP_TOOL = new Set([
|
|
5
|
+
"gentle-ai-explore",
|
|
6
|
+
"gentle-ai-verify",
|
|
7
|
+
"gentle-ai-worker",
|
|
8
|
+
"jd-fix-agent",
|
|
9
|
+
"jd-judge-a",
|
|
10
|
+
"jd-judge-b",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const FRONTMATTER = /^(---\r?\n)([\s\S]*?)(\r?\n---)([\s\S]*)$/;
|
|
14
|
+
const GENTLE_AI_BLOCK = /<!-- gentle-ai:pi-codegraph-(?:tool|guidance) -->[\s\S]*?<!-- \/gentle-ai:pi-codegraph -->/g;
|
|
15
|
+
const MARKER = "\u0000";
|
|
16
|
+
|
|
17
|
+
function unquote(value: string): string {
|
|
18
|
+
const v = value.trim();
|
|
19
|
+
return v.length >= 2 && (v[0] === '"' || v[0] === "'") && v[v.length - 1] === v[0] ? v.slice(1, -1) : v;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The gentle-pi agent files ship a `- "*": false` list entry; Pi reads it as a tool named `*": false`.
|
|
23
|
+
function isBrokenEntry(item: string): boolean {
|
|
24
|
+
return /\*["']?\s*:\s*false/.test(item);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const EXPLORER_STRUCTURAL_LINE = "- For structural questions, use the cwd-scoped `codegraph` tool";
|
|
28
|
+
const EXPLORER_INDEX_LINE = "- `codegraph` may create or update only the current workspace `.codegraph/` index";
|
|
29
|
+
const EXPLORER_READ_ONLY_PHRASE = "Other than the explicit `.codegraph/` index exception, read and search only.";
|
|
30
|
+
|
|
31
|
+
// The explorer loses its `codegraph` extension tool, so the lines that tell it to use or initialize that tool would send it nowhere.
|
|
32
|
+
function rewriteExplorerInstructions(body: string, eol: string): string {
|
|
33
|
+
if (!body.includes("codegraph")) return body;
|
|
34
|
+
const out: string[] = [];
|
|
35
|
+
for (const line of body.split(eol)) {
|
|
36
|
+
if (line.startsWith(EXPLORER_STRUCTURAL_LINE)) {
|
|
37
|
+
out.push(`- For structural questions, use the \`${CODEGRAPH_MCP_TOOL}\` tool before broad filesystem searches. It reads the existing \`.codegraph/\` index and cannot create one; when the project has none, use \`read\`, \`grep\`, and \`find\` and say so in the handoff.`);
|
|
38
|
+
} else if (line.startsWith(EXPLORER_INDEX_LINE)) {
|
|
39
|
+
continue;
|
|
40
|
+
} else {
|
|
41
|
+
out.push(line.replace(EXPLORER_READ_ONLY_PHRASE, "Read and search only."));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out.join(eol);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeBlocks(body: string, eol: string): string {
|
|
48
|
+
if (!body.includes("gentle-ai:pi-codegraph")) return body;
|
|
49
|
+
const marked = body.replace(GENTLE_AI_BLOCK, MARKER);
|
|
50
|
+
return marked.replace(/\s*\u0000(?:\s*\u0000)*\s*/g, (match, offset: number) => {
|
|
51
|
+
if (offset === 0) return "";
|
|
52
|
+
return offset + match.length >= marked.length ? eol : eol + eol;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function editTools(front: string, eol: string): string {
|
|
58
|
+
const lines = front.split(/\r?\n/);
|
|
59
|
+
const nameLine = lines.find((line) => /^name:/.test(line));
|
|
60
|
+
const name = nameLine ? unquote(nameLine.slice("name:".length)) : "";
|
|
61
|
+
const at = lines.findIndex((line) => /^tools:/.test(line));
|
|
62
|
+
if (at === -1) return front;
|
|
63
|
+
|
|
64
|
+
const rest = lines[at].slice("tools:".length).trim();
|
|
65
|
+
let style: "bracket" | "csv" | "list";
|
|
66
|
+
let items: string[];
|
|
67
|
+
let consumed = 1;
|
|
68
|
+
let indent = " ";
|
|
69
|
+
if (rest.startsWith("[")) {
|
|
70
|
+
style = "bracket";
|
|
71
|
+
items = rest.replace(/^\[/, "").replace(/\]$/, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
72
|
+
} else if (rest.length > 0) {
|
|
73
|
+
style = "csv";
|
|
74
|
+
items = rest.split(",").map((s) => s.trim()).filter(Boolean);
|
|
75
|
+
} else {
|
|
76
|
+
style = "list";
|
|
77
|
+
items = [];
|
|
78
|
+
for (let i = at + 1; i < lines.length; i++) {
|
|
79
|
+
const m = /^(\s*)-\s+(.*)$/.exec(lines[i]);
|
|
80
|
+
if (!m) break;
|
|
81
|
+
if (items.length === 0) indent = m[1];
|
|
82
|
+
items.push(m[2].trim());
|
|
83
|
+
consumed++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (items.length === 0) return front;
|
|
87
|
+
|
|
88
|
+
let next = items.filter((item) => unquote(item) !== "mcp" && !isBrokenEntry(item));
|
|
89
|
+
if (name === EXPLORER) next = next.filter((item) => unquote(item) !== "codegraph");
|
|
90
|
+
if (AGENTS_WITH_MCP_TOOL.has(name) && !next.some((item) => unquote(item) === CODEGRAPH_MCP_TOOL)) next.push(CODEGRAPH_MCP_TOOL);
|
|
91
|
+
// An empty list would hand the agent Pi's default tools; keep what was there instead.
|
|
92
|
+
if (next.length === 0 || next.join("\n") === items.join("\n")) return front;
|
|
93
|
+
|
|
94
|
+
const replacement =
|
|
95
|
+
style === "list" ? ["tools:", ...next.map((item) => `${indent}- ${item}`)]
|
|
96
|
+
: style === "bracket" ? [`tools: [${next.join(", ")}]`]
|
|
97
|
+
: [`tools: ${next.join(", ")}`];
|
|
98
|
+
return [...lines.slice(0, at), ...replacement, ...lines.slice(at + consumed)].join(eol);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Returns the agent definition with the CodeGraph wiring applied; identical text when nothing changes. */
|
|
102
|
+
export function normalizeAgent(text: string): string {
|
|
103
|
+
const match = FRONTMATTER.exec(text);
|
|
104
|
+
if (!match) return text;
|
|
105
|
+
const [, open, front, close, body] = match;
|
|
106
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
107
|
+
const name = /^name:\s*(.+)$/m.exec(front)?.[1];
|
|
108
|
+
const cleaned = removeBlocks(body, eol);
|
|
109
|
+
const rewritten = name !== undefined && unquote(name) === EXPLORER ? rewriteExplorerInstructions(cleaned, eol) : cleaned;
|
|
110
|
+
return open + editTools(front, eol) + close + rewritten;
|
|
111
|
+
}
|
package/src/apply.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { normalizeAgent } from "./agents.ts";
|
|
4
|
+
import { normalizeMcpConfig } from "./mcp.ts";
|
|
5
|
+
|
|
6
|
+
export interface ApplyResult {
|
|
7
|
+
/** Files that were rewritten. */
|
|
8
|
+
changed: string[];
|
|
9
|
+
/** One message per file that could not be processed. */
|
|
10
|
+
errors: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const BACKUP_SUFFIX = ".pre-codegraph-subagents.bak";
|
|
14
|
+
|
|
15
|
+
// Write beside the target and rename, so a reader never sees a half-written file.
|
|
16
|
+
function writeAtomic(file: string, text: string): void {
|
|
17
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
18
|
+
try {
|
|
19
|
+
writeFileSync(temp, text);
|
|
20
|
+
renameSync(temp, file);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
try { unlinkSync(temp); } catch { /* nothing to clean */ }
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function rewrite(file: string, normalize: (text: string) => string, result: ApplyResult): void {
|
|
28
|
+
try {
|
|
29
|
+
const before = readFileSync(file, "utf8");
|
|
30
|
+
const after = normalize(before);
|
|
31
|
+
if (after === before) return;
|
|
32
|
+
const backup = file + BACKUP_SUFFIX;
|
|
33
|
+
if (!existsSync(backup)) writeFileSync(backup, before, { flag: "wx" });
|
|
34
|
+
writeAtomic(file, after);
|
|
35
|
+
result.changed.push(file);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
result.errors.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Applies the CodeGraph wiring to the global agent definitions and to mcp.json
|
|
43
|
+
* under `agentDir`. Idempotent and never throws: failures are returned in `errors`.
|
|
44
|
+
*/
|
|
45
|
+
export function applyCodegraphSubagents(agentDir: string): ApplyResult {
|
|
46
|
+
const result: ApplyResult = { changed: [], errors: [] };
|
|
47
|
+
for (const folder of ["agents", "subagents"]) {
|
|
48
|
+
const dir = join(agentDir, folder);
|
|
49
|
+
let entries: string[];
|
|
50
|
+
try {
|
|
51
|
+
entries = readdirSync(dir).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
|
|
52
|
+
} catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) rewrite(join(dir, entry), normalizeAgent, result);
|
|
56
|
+
}
|
|
57
|
+
const mcp = join(agentDir, "mcp.json");
|
|
58
|
+
if (existsSync(mcp)) rewrite(mcp, normalizeMcpConfig, result);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi declares MCP tools to the model only for servers with `exposure: "direct"`;
|
|
3
|
+
* the default, `codemode`, makes them reachable from scripts but never callable
|
|
4
|
+
* as a tool, so an agent `tools:` entry like `mcp__codegraph__*` would match nothing.
|
|
5
|
+
* Returns the config with the codegraph server set to `direct`; identical text when nothing changes.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeMcpConfig(text: string): string {
|
|
8
|
+
let config: unknown;
|
|
9
|
+
try {
|
|
10
|
+
config = JSON.parse(text);
|
|
11
|
+
} catch {
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) return text;
|
|
15
|
+
const servers = (config as { mcpServers?: unknown }).mcpServers;
|
|
16
|
+
if (typeof servers !== "object" || servers === null || Array.isArray(servers)) return text;
|
|
17
|
+
const server = (servers as Record<string, unknown>).codegraph;
|
|
18
|
+
if (typeof server !== "object" || server === null || Array.isArray(server)) return text;
|
|
19
|
+
const entry = server as Record<string, unknown>;
|
|
20
|
+
if (entry.enabled === false || entry.exposure === "direct") return text;
|
|
21
|
+
|
|
22
|
+
entry.exposure = "direct";
|
|
23
|
+
const indent = /^([ \t]+)"/m.exec(text)?.[1] ?? " ";
|
|
24
|
+
return JSON.stringify(config, null, indent) + "\n";
|
|
25
|
+
}
|
package/src/prefetch.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CODEGRAPH_MCP_TOOL } from "./agents.ts";
|
|
4
|
+
|
|
5
|
+
export const PREFETCH_TIMEOUT_MS = 20_000;
|
|
6
|
+
export const PREFETCH_MAX_CHARS = 12_000;
|
|
7
|
+
export const QUERY_MAX_CHARS = 300;
|
|
8
|
+
const READ_LIMIT_BYTES = 200_000;
|
|
9
|
+
|
|
10
|
+
const REQUESTED_TOOLS_ENV = "GENTLE_PI_AGENTS_REQUESTED_TOOLS";
|
|
11
|
+
const CHILD_ENV = "GENTLE_PI_AGENTS_CHILD";
|
|
12
|
+
const WRITING_TOOLS = ["bash", "edit", "write", "bash_background"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* gentle-pi gives a subagent child no env var with the agent's name, only its tool list.
|
|
16
|
+
* The explorer is the one child that reads only and holds the CodeGraph MCP tool.
|
|
17
|
+
*/
|
|
18
|
+
export function isExplorerChild(env: Record<string, string | undefined>): boolean {
|
|
19
|
+
if (env[CHILD_ENV] !== "1") return false;
|
|
20
|
+
const tools = (env[REQUESTED_TOOLS_ENV] ?? "").split(",").map((tool) => tool.trim());
|
|
21
|
+
return tools.includes(CODEGRAPH_MCP_TOOL) && !tools.some((tool) => WRITING_TOOLS.includes(tool));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The task text becomes arguments of a command that Windows runs through cmd.exe, so only
|
|
26
|
+
* letters, digits and a few harmless marks survive, and no word can look like an option.
|
|
27
|
+
*/
|
|
28
|
+
export function sanitizeQuery(task: string): string {
|
|
29
|
+
const words = task
|
|
30
|
+
.replace(/[^\p{L}\p{N}_.,:/-]+/gu, " ")
|
|
31
|
+
.split(" ")
|
|
32
|
+
.map((word) => word.replace(/^-+/, ""))
|
|
33
|
+
.filter((word) => word.length > 0);
|
|
34
|
+
return words.join(" ").slice(0, QUERY_MAX_CHARS).trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function boundOutput(text: string, max = PREFETCH_MAX_CHARS): string {
|
|
38
|
+
return text.length <= max ? text : `${text.slice(0, max)}\n\n[truncated: output cut at ${max} characters]`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RunOptions {
|
|
42
|
+
command: string;
|
|
43
|
+
args: string[];
|
|
44
|
+
cwd: string;
|
|
45
|
+
timeoutMs: number;
|
|
46
|
+
maxBytes: number;
|
|
47
|
+
/** Windows needs a shell to run the .cmd shim that npm installs for codegraph. */
|
|
48
|
+
shell?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function killTree(pid: number | undefined, child: { kill(): boolean }): void {
|
|
52
|
+
if (process.platform === "win32" && pid !== undefined) {
|
|
53
|
+
try {
|
|
54
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }).on("error", () => {});
|
|
55
|
+
return;
|
|
56
|
+
} catch { /* fall through */ }
|
|
57
|
+
}
|
|
58
|
+
try { child.kill(); } catch { /* already gone */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Output of a command that exits 0 and prints something; undefined for anything else. Never rejects. */
|
|
62
|
+
export function runCommand(options: RunOptions): Promise<string | undefined> {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
let settled = false;
|
|
65
|
+
const finish = (value: string | undefined) => {
|
|
66
|
+
if (settled) return;
|
|
67
|
+
settled = true;
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve(value);
|
|
70
|
+
};
|
|
71
|
+
let child;
|
|
72
|
+
try {
|
|
73
|
+
child = options.shell
|
|
74
|
+
? spawn([options.command, ...options.args].join(" "), { cwd: options.cwd, shell: true, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] })
|
|
75
|
+
: spawn(options.command, options.args, { cwd: options.cwd, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
76
|
+
} catch {
|
|
77
|
+
resolve(undefined);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
killTree(child.pid, child);
|
|
82
|
+
finish(undefined);
|
|
83
|
+
}, options.timeoutMs);
|
|
84
|
+
const chunks: Buffer[] = [];
|
|
85
|
+
let size = 0;
|
|
86
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
87
|
+
size += chunk.length;
|
|
88
|
+
if (size > options.maxBytes) {
|
|
89
|
+
killTree(child.pid, child);
|
|
90
|
+
finish(Buffer.concat(chunks).toString("utf8").slice(0, options.maxBytes));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
chunks.push(chunk);
|
|
94
|
+
});
|
|
95
|
+
child.on("error", () => finish(undefined));
|
|
96
|
+
child.on("close", (code) => {
|
|
97
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
98
|
+
finish(code === 0 && text.trim().length > 0 ? text : undefined);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The CLI's explore output is the same as the codegraph_explore MCP tool. */
|
|
104
|
+
export function exploreWithCodegraph(query: string, cwd: string): Promise<string | undefined> {
|
|
105
|
+
const windows = process.platform === "win32";
|
|
106
|
+
return runCommand({
|
|
107
|
+
command: "codegraph",
|
|
108
|
+
args: windows ? ["explore", ...query.split(" ")] : ["explore", query],
|
|
109
|
+
cwd,
|
|
110
|
+
timeoutMs: PREFETCH_TIMEOUT_MS,
|
|
111
|
+
maxBytes: READ_LIMIT_BYTES,
|
|
112
|
+
shell: windows,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface PrefetchInput {
|
|
117
|
+
env: Record<string, string | undefined>;
|
|
118
|
+
prompt: string | undefined;
|
|
119
|
+
cwd: string;
|
|
120
|
+
exists(path: string): boolean;
|
|
121
|
+
explore(query: string, cwd: string): Promise<string | undefined>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Text to hand to the explorer before its first step; undefined means the run goes on untouched. */
|
|
125
|
+
export async function prefetchForExplorer(input: PrefetchInput): Promise<string | undefined> {
|
|
126
|
+
try {
|
|
127
|
+
if (!isExplorerChild(input.env) || input.prompt === undefined) return undefined;
|
|
128
|
+
const query = sanitizeQuery(input.prompt);
|
|
129
|
+
if (query.length === 0) return undefined;
|
|
130
|
+
if (!input.exists(join(input.cwd, ".codegraph"))) return undefined;
|
|
131
|
+
const output = await input.explore(query, input.cwd);
|
|
132
|
+
if (output === undefined || output.trim().length === 0) return undefined;
|
|
133
|
+
return [
|
|
134
|
+
"CodeGraph pre-fetch for your task, run for you before your first step with `codegraph explore` (bounded in size and time).",
|
|
135
|
+
`Use it as a starting point and call ${CODEGRAPH_MCP_TOOL} when you need more.`,
|
|
136
|
+
"",
|
|
137
|
+
boundOutput(output.trim()),
|
|
138
|
+
].join("\n");
|
|
139
|
+
} catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|