pi-graphnav 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 +95 -2
- package/extensions/command/parse.ts +75 -0
- package/extensions/core/config.ts +103 -0
- package/extensions/core/gate.ts +317 -0
- package/extensions/core/mcp.ts +183 -0
- package/extensions/core/project.ts +126 -0
- package/extensions/core/tools.ts +142 -0
- package/extensions/graphnav.ts +329 -0
- package/package.json +48 -4
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A long-lived `codegraph serve --mcp` process per project, spoken to over stdio (JSON-RPC 2.0,
|
|
3
|
+
* one message per line).
|
|
4
|
+
*
|
|
5
|
+
* Long-lived on purpose: the server's file watcher keeps the index in sync while pi runs,
|
|
6
|
+
* and no call pays for a process start. A call that times out kills the process; the next
|
|
7
|
+
* call starts a fresh one.
|
|
8
|
+
*/
|
|
9
|
+
import { type ChildProcessWithoutNullStreams, spawn as nodeSpawn } from "node:child_process";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
|
|
12
|
+
/** The part of a child process the client uses (so tests can pass a fake). */
|
|
13
|
+
export interface ServerProcess {
|
|
14
|
+
stdin: { write(chunk: string): unknown };
|
|
15
|
+
stdout: { on(event: "data", fn: (chunk: Buffer | string) => void): unknown };
|
|
16
|
+
stderr: { on(event: "data", fn: (chunk: Buffer | string) => void): unknown };
|
|
17
|
+
on(event: "exit", fn: (code: number | null) => void): unknown;
|
|
18
|
+
on(event: "error", fn: (err: Error) => void): unknown;
|
|
19
|
+
kill(): unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Spawner = (args: string[], cwd: string) => ServerProcess;
|
|
23
|
+
|
|
24
|
+
export const defaultSpawner: Spawner = (args, cwd) =>
|
|
25
|
+
nodeSpawn("codegraph", args, {
|
|
26
|
+
cwd,
|
|
27
|
+
env: process.env,
|
|
28
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
// npm installs a .cmd shim on Windows, which only a shell resolves.
|
|
31
|
+
shell: process.platform === "win32",
|
|
32
|
+
}) as ChildProcessWithoutNullStreams;
|
|
33
|
+
|
|
34
|
+
export interface McpResult {
|
|
35
|
+
content?: { type: string; text?: string }[];
|
|
36
|
+
isError?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const MAX_STDERR = 2000;
|
|
40
|
+
|
|
41
|
+
/** Remove ANSI colours and anything that looks like a secret from server diagnostics. */
|
|
42
|
+
export function sanitize(text: string): string {
|
|
43
|
+
return text
|
|
44
|
+
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
45
|
+
.replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|APIKEY)[A-Z0-9_]*=)\S+/gi, "$1[redacted]")
|
|
46
|
+
.replace(/\bBearer\s+\S+/gi, "Bearer [redacted]")
|
|
47
|
+
.trim()
|
|
48
|
+
.slice(-MAX_STDERR);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Pending {
|
|
52
|
+
resolve: (v: any) => void;
|
|
53
|
+
reject: (e: Error) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class CodeGraphServer {
|
|
57
|
+
private proc?: ServerProcess;
|
|
58
|
+
private ready?: Promise<void>;
|
|
59
|
+
private pending = new Map<number, Pending>();
|
|
60
|
+
private nextId = 1;
|
|
61
|
+
private buffer = "";
|
|
62
|
+
private stderr = "";
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
readonly root: string,
|
|
66
|
+
private readonly opts: { watch: boolean; timeoutMs: number; spawn?: Spawner },
|
|
67
|
+
) {}
|
|
68
|
+
|
|
69
|
+
get running(): boolean {
|
|
70
|
+
return this.proc !== undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private start(): Promise<void> {
|
|
74
|
+
const args = ["serve", "--mcp", "--path", this.root, ...(this.opts.watch ? [] : ["--no-watch"])];
|
|
75
|
+
const proc = (this.opts.spawn ?? defaultSpawner)(args, this.root);
|
|
76
|
+
this.proc = proc;
|
|
77
|
+
this.buffer = "";
|
|
78
|
+
this.stderr = "";
|
|
79
|
+
proc.stdout.on("data", (chunk) => this.onData(String(chunk)));
|
|
80
|
+
proc.stderr.on("data", (chunk) => {
|
|
81
|
+
this.stderr = (this.stderr + String(chunk)).slice(-MAX_STDERR);
|
|
82
|
+
});
|
|
83
|
+
proc.on("error", (err) => this.fail(proc, err));
|
|
84
|
+
proc.on("exit", (code) => this.fail(proc, new Error(sanitize(this.stderr) || `codegraph serve exited with code ${code}`)));
|
|
85
|
+
const rootUri = pathToFileURL(this.root).href;
|
|
86
|
+
return this.request("initialize", {
|
|
87
|
+
protocolVersion: "2024-11-05",
|
|
88
|
+
rootUri,
|
|
89
|
+
workspaceFolders: [{ uri: rootUri, name: this.root.split(/[\\/]/).pop() || this.root }],
|
|
90
|
+
capabilities: {},
|
|
91
|
+
clientInfo: { name: "pi-graphnav", version: "1" },
|
|
92
|
+
}).then(() => {
|
|
93
|
+
this.send({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The process died or could not start: fail what waits on it and forget it. */
|
|
98
|
+
private fail(proc: ServerProcess, err: Error) {
|
|
99
|
+
if (this.proc !== proc) return;
|
|
100
|
+
this.proc = undefined;
|
|
101
|
+
this.ready = undefined;
|
|
102
|
+
for (const p of this.pending.values()) p.reject(err);
|
|
103
|
+
this.pending.clear();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private onData(chunk: string) {
|
|
107
|
+
this.buffer += chunk;
|
|
108
|
+
let nl: number;
|
|
109
|
+
while ((nl = this.buffer.indexOf("\n")) !== -1) {
|
|
110
|
+
const line = this.buffer.slice(0, nl).trim();
|
|
111
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
112
|
+
if (!line) continue;
|
|
113
|
+
let msg: any;
|
|
114
|
+
try {
|
|
115
|
+
msg = JSON.parse(line);
|
|
116
|
+
} catch {
|
|
117
|
+
continue; // a log line on stdout
|
|
118
|
+
}
|
|
119
|
+
const p = typeof msg?.id === "number" ? this.pending.get(msg.id) : undefined;
|
|
120
|
+
if (!p) continue;
|
|
121
|
+
this.pending.delete(msg.id);
|
|
122
|
+
if (msg.error) p.reject(new Error(String(msg.error.message ?? JSON.stringify(msg.error))));
|
|
123
|
+
else p.resolve(msg.result);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private send(payload: unknown) {
|
|
128
|
+
this.proc?.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private request(method: string, params: unknown): Promise<any> {
|
|
132
|
+
const id = this.nextId++;
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
this.pending.set(id, { resolve, reject });
|
|
135
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Call a CodeGraph tool. Times out after `timeoutMs` (the process is then restarted on the next call). */
|
|
140
|
+
async call(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<McpResult> {
|
|
141
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
142
|
+
this.ready ??= this.start().catch((e: unknown) => {
|
|
143
|
+
this.close();
|
|
144
|
+
throw e;
|
|
145
|
+
});
|
|
146
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
147
|
+
let onAbort: (() => void) | undefined;
|
|
148
|
+
let id: number | undefined;
|
|
149
|
+
const guard = new Promise<never>((_, reject) => {
|
|
150
|
+
timer = setTimeout(() => {
|
|
151
|
+
this.close();
|
|
152
|
+
reject(new Error(`CodeGraph did not answer within ${Math.round(this.opts.timeoutMs / 1000)}s. If this repeats, run "codegraph unlock" in ${this.root}.`));
|
|
153
|
+
}, this.opts.timeoutMs);
|
|
154
|
+
onAbort = () => reject(new Error("Aborted"));
|
|
155
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
156
|
+
});
|
|
157
|
+
try {
|
|
158
|
+
const answer = this.ready.then(() => {
|
|
159
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
160
|
+
id = this.nextId;
|
|
161
|
+
return this.request("tools/call", { name, arguments: args });
|
|
162
|
+
});
|
|
163
|
+
return await Promise.race([answer, guard]);
|
|
164
|
+
} finally {
|
|
165
|
+
// An abandoned request (timeout, abort) must not wait in `pending` for an answer nobody reads.
|
|
166
|
+
if (id !== undefined) this.pending.delete(id);
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Stop the process. Idempotent. */
|
|
173
|
+
close() {
|
|
174
|
+
const proc = this.proc;
|
|
175
|
+
if (!proc) return;
|
|
176
|
+
this.fail(proc, new Error("CodeGraph server closed"));
|
|
177
|
+
try {
|
|
178
|
+
proc.kill();
|
|
179
|
+
} catch {
|
|
180
|
+
// already gone
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which project CodeGraph serves, and what keeping its index current takes.
|
|
3
|
+
*
|
|
4
|
+
* Pure, except where a function takes the file system or a process runner as a parameter.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
export interface Project {
|
|
11
|
+
root: string;
|
|
12
|
+
/** A `.codegraph/` directory exists at the root. */
|
|
13
|
+
hasIndexDir: boolean;
|
|
14
|
+
/** The root is a git work tree (so `init` there is expected, not a surprise). */
|
|
15
|
+
isRepo: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The project for `cwd`: the nearest directory with `.codegraph/`, else the nearest git
|
|
20
|
+
* work tree. Never the home directory or a file-system root: indexing those is never wanted.
|
|
21
|
+
*/
|
|
22
|
+
export function findProject(cwd: string, exists: (p: string) => boolean = existsSync, home: string = homedir()): Project | undefined {
|
|
23
|
+
let gitRoot: string | undefined;
|
|
24
|
+
let dir = resolve(cwd);
|
|
25
|
+
for (;;) {
|
|
26
|
+
if (dir === home || dir === parse(dir).root) break;
|
|
27
|
+
if (exists(join(dir, ".codegraph"))) return { root: dir, hasIndexDir: true, isRepo: exists(join(dir, ".git")) };
|
|
28
|
+
if (gitRoot === undefined && exists(join(dir, ".git"))) gitRoot = dir;
|
|
29
|
+
const up = dirname(dir);
|
|
30
|
+
if (up === dir) break;
|
|
31
|
+
dir = up;
|
|
32
|
+
}
|
|
33
|
+
return gitRoot === undefined ? undefined : { root: gitRoot, hasIndexDir: false, isRepo: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What `codegraph status -j` says, reduced to what matters here. */
|
|
37
|
+
export interface IndexStatus {
|
|
38
|
+
initialized: boolean;
|
|
39
|
+
/** `complete`, `indexing`, … (`unknown` when the field is missing). */
|
|
40
|
+
state: string;
|
|
41
|
+
files: number;
|
|
42
|
+
pending: number;
|
|
43
|
+
reindexRecommended: boolean;
|
|
44
|
+
/** The index was built by another CodeGraph version than the one installed. */
|
|
45
|
+
versionMismatch: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
49
|
+
|
|
50
|
+
/** Parse `codegraph status -j`. Garbage (an old CodeGraph, a crash) reads as "not initialized". */
|
|
51
|
+
export function parseStatus(json: string): IndexStatus {
|
|
52
|
+
let raw: any;
|
|
53
|
+
try {
|
|
54
|
+
raw = JSON.parse(json);
|
|
55
|
+
} catch {
|
|
56
|
+
raw = undefined;
|
|
57
|
+
}
|
|
58
|
+
const r = typeof raw === "object" && raw !== null ? raw : {};
|
|
59
|
+
const index = typeof r.index === "object" && r.index !== null ? r.index : {};
|
|
60
|
+
const pending = typeof r.pendingChanges === "object" && r.pendingChanges !== null ? r.pendingChanges : {};
|
|
61
|
+
return {
|
|
62
|
+
initialized: r.initialized === true,
|
|
63
|
+
state: typeof index.state === "string" ? index.state : "unknown",
|
|
64
|
+
files: num(r.fileCount),
|
|
65
|
+
pending: num(pending.added) + num(pending.modified) + num(pending.removed),
|
|
66
|
+
reindexRecommended: index.reindexRecommended === true,
|
|
67
|
+
versionMismatch:
|
|
68
|
+
typeof index.builtWithVersion === "string" && typeof r.version === "string" && index.builtWithVersion !== r.version,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type IndexAction = "init" | "index" | "sync" | "none" | "skip";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What brings the index up to date: `init` when there is none (only in a git work tree, and
|
|
76
|
+
* only when allowed), a full `index` when it is stale or broken, `sync` for pending changes.
|
|
77
|
+
*/
|
|
78
|
+
export function planIndex(status: IndexStatus, project: Project, opts: { autoInit: boolean }): IndexAction {
|
|
79
|
+
if (!status.initialized) return opts.autoInit && project.isRepo ? "init" : "skip";
|
|
80
|
+
if (status.reindexRecommended || status.versionMismatch || (status.state !== "complete" && status.state !== "indexing")) return "index";
|
|
81
|
+
if (status.pending > 0) return "sync";
|
|
82
|
+
return "none";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The CLI arguments for an action. */
|
|
86
|
+
export function actionArgs(action: Exclude<IndexAction, "none" | "skip">): string[] {
|
|
87
|
+
switch (action) {
|
|
88
|
+
case "init":
|
|
89
|
+
return ["init", "-i", "-y"];
|
|
90
|
+
case "index":
|
|
91
|
+
return ["index", "-q"];
|
|
92
|
+
case "sync":
|
|
93
|
+
return ["sync", "-q"];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Runs `codegraph <args>` in `cwd`; resolves with the exit code and output, never rejects. */
|
|
98
|
+
export type Runner = (args: string[], cwd: string) => Promise<{ code: number; stdout: string; stderr: string }>;
|
|
99
|
+
|
|
100
|
+
export interface EnsureResult {
|
|
101
|
+
action: IndexAction;
|
|
102
|
+
status: IndexStatus;
|
|
103
|
+
/** Set when CodeGraph is missing or a step failed. */
|
|
104
|
+
error?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Bring the index of `project` up to date: status, then init/index/sync as planned, then status again. */
|
|
108
|
+
export async function ensureIndex(project: Project, run: Runner, opts: { autoInit: boolean }): Promise<EnsureResult> {
|
|
109
|
+
const first = await run(["status", "-j"], project.root);
|
|
110
|
+
if (first.code === 127) return { action: "skip", status: parseStatus(""), error: "the codegraph CLI is not installed (npm i -g @colbymchenry/codegraph)" };
|
|
111
|
+
const status = parseStatus(first.stdout);
|
|
112
|
+
const action = planIndex(status, project, opts);
|
|
113
|
+
if (action === "none" || action === "skip") return { action, status };
|
|
114
|
+
const res = await run(actionArgs(action), project.root);
|
|
115
|
+
const after = parseStatus((await run(["status", "-j"], project.root)).stdout);
|
|
116
|
+
if (res.code !== 0) return { action, status: after, error: `codegraph ${action} failed (exit ${res.code}): ${lastLine(res.stderr || res.stdout)}` };
|
|
117
|
+
return { action, status: after };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lastLine = (s: string): string =>
|
|
121
|
+
s
|
|
122
|
+
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
123
|
+
.trim()
|
|
124
|
+
.split("\n")
|
|
125
|
+
.pop()!
|
|
126
|
+
.slice(0, 300);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `codegraph_*` tools: names, parameters and the guidance the model sees.
|
|
3
|
+
*
|
|
4
|
+
* The names match the CodeGraph MCP server's tools, so calls pass straight through.
|
|
5
|
+
*/
|
|
6
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
7
|
+
|
|
8
|
+
const projectPath = Type.Optional(
|
|
9
|
+
Type.String({ description: "Absolute path of another indexed project. Defaults to the current project." }),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const kind = Type.Optional(
|
|
13
|
+
Type.Union(
|
|
14
|
+
["function", "method", "class", "interface", "type", "variable", "route", "component"].map((k) => Type.Literal(k)),
|
|
15
|
+
{ description: "Only symbols of this kind." },
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const TOOLS = [
|
|
20
|
+
{
|
|
21
|
+
name: "codegraph_explore",
|
|
22
|
+
label: "CodeGraph Explore",
|
|
23
|
+
description:
|
|
24
|
+
"Start here for any question about how code works, where something lives or what to change: returns the source of the relevant symbols, grouped by file, with the call paths between them. Accepts symbol names, file names or plain code terms.",
|
|
25
|
+
snippet: "Explore an area of the codebase: relevant symbols' source and call paths in one call",
|
|
26
|
+
parameters: Type.Object({
|
|
27
|
+
query: Type.String({ description: "Symbols, files or code terms, e.g. \"parseConfig loadSettings\"." }),
|
|
28
|
+
maxFiles: Type.Optional(Type.Number({ description: "Most files to return (default 12)." })),
|
|
29
|
+
projectPath,
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "codegraph_search",
|
|
34
|
+
label: "CodeGraph Search",
|
|
35
|
+
description: "Find symbols by name (or part of one): kind, file and line of each definition. Use instead of grep for a symbol.",
|
|
36
|
+
snippet: "Find where a symbol is defined (instead of rg/grep)",
|
|
37
|
+
parameters: Type.Object({
|
|
38
|
+
query: Type.String({ description: "Symbol name or part of one." }),
|
|
39
|
+
kind,
|
|
40
|
+
limit: Type.Optional(Type.Number({ description: "Most results (default 10)." })),
|
|
41
|
+
projectPath,
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "codegraph_node",
|
|
46
|
+
label: "CodeGraph Node",
|
|
47
|
+
description: "One symbol in detail: signature, location, callers and callees; with includeCode its source.",
|
|
48
|
+
snippet: "One symbol's details, callers and callees",
|
|
49
|
+
parameters: Type.Object({
|
|
50
|
+
symbol: Type.String({ description: "Symbol name." }),
|
|
51
|
+
includeCode: Type.Optional(Type.Boolean({ description: "Include the source (default false)." })),
|
|
52
|
+
projectPath,
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "codegraph_callers",
|
|
57
|
+
label: "CodeGraph Callers",
|
|
58
|
+
description: "Every function or method that calls a symbol: its usages, without grepping.",
|
|
59
|
+
snippet: "Who calls a symbol (its usages)",
|
|
60
|
+
parameters: Type.Object({
|
|
61
|
+
symbol: Type.String({ description: "Symbol name." }),
|
|
62
|
+
limit: Type.Optional(Type.Number({ description: "Most results (default 20)." })),
|
|
63
|
+
projectPath,
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "codegraph_callees",
|
|
68
|
+
label: "CodeGraph Callees",
|
|
69
|
+
description: "Every function or method a symbol calls.",
|
|
70
|
+
snippet: "What a symbol calls",
|
|
71
|
+
parameters: Type.Object({
|
|
72
|
+
symbol: Type.String({ description: "Symbol name." }),
|
|
73
|
+
limit: Type.Optional(Type.Number({ description: "Most results (default 20)." })),
|
|
74
|
+
projectPath,
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "codegraph_impact",
|
|
79
|
+
label: "CodeGraph Impact",
|
|
80
|
+
description: "What a change to a symbol affects, transitively. Call it before changing a signature or behaviour.",
|
|
81
|
+
snippet: "What breaks if a symbol changes",
|
|
82
|
+
parameters: Type.Object({
|
|
83
|
+
symbol: Type.String({ description: "Symbol name." }),
|
|
84
|
+
depth: Type.Optional(Type.Number({ description: "How many levels to follow (default 2)." })),
|
|
85
|
+
projectPath,
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "codegraph_files",
|
|
90
|
+
label: "CodeGraph Files",
|
|
91
|
+
description: "The project's indexed files as a tree, flat list or grouped by language, with symbol counts.",
|
|
92
|
+
snippet: "Project file structure from the index",
|
|
93
|
+
parameters: Type.Object({
|
|
94
|
+
path: Type.Optional(Type.String({ description: "Only files under this root-relative prefix, e.g. \"src/core\"." })),
|
|
95
|
+
pattern: Type.Optional(Type.String({ description: "Glob on file paths, e.g. \"*.test.ts\"." })),
|
|
96
|
+
format: Type.Optional(Type.Union([Type.Literal("tree"), Type.Literal("flat"), Type.Literal("grouped")])),
|
|
97
|
+
includeMetadata: Type.Optional(Type.Boolean()),
|
|
98
|
+
maxDepth: Type.Optional(Type.Number()),
|
|
99
|
+
projectPath,
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "codegraph_status",
|
|
104
|
+
label: "CodeGraph Status",
|
|
105
|
+
description: "Index statistics for the project: files, symbols, last update.",
|
|
106
|
+
snippet: "Index status",
|
|
107
|
+
parameters: Type.Object({ projectPath }),
|
|
108
|
+
},
|
|
109
|
+
] as const;
|
|
110
|
+
|
|
111
|
+
export type ToolName = (typeof TOOLS)[number]["name"];
|
|
112
|
+
export const TOOL_NAMES: readonly string[] = TOOLS.map((t) => t.name);
|
|
113
|
+
export const isCodeGraphTool = (name: string): boolean => TOOL_NAMES.includes(name);
|
|
114
|
+
|
|
115
|
+
/** Guidelines added to the system prompt while the tools are active (static: they never break the prompt cache). */
|
|
116
|
+
export const GUIDELINES = [
|
|
117
|
+
"Understand code through CodeGraph first: codegraph_explore for how something works or where to change it, codegraph_search to find a definition, codegraph_callers/codegraph_callees for relations, codegraph_impact before changing a signature, codegraph_files for structure.",
|
|
118
|
+
"Do not grep (rg, grep, git grep) for symbol names: such searches are redirected to CodeGraph. Text search is for string literals, comments, config and docs, or after CodeGraph came up empty.",
|
|
119
|
+
"Read a file once CodeGraph has told you where to look, with offset/limit around the reported lines.",
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/** Largest result handed to the model; the rest is cut with a note. */
|
|
123
|
+
export const MAX_RESULT_CHARS = 60_000;
|
|
124
|
+
|
|
125
|
+
export function resultText(result: { content?: { type: string; text?: string }[] } | undefined): string {
|
|
126
|
+
const text = (result?.content ?? [])
|
|
127
|
+
.filter((p) => p?.type === "text" && typeof p.text === "string")
|
|
128
|
+
.map((p) => p.text)
|
|
129
|
+
.join("\n");
|
|
130
|
+
if (text.length <= MAX_RESULT_CHARS) return text || "(no result)";
|
|
131
|
+
return `${text.slice(0, MAX_RESULT_CHARS)}\n\n[cut at ${MAX_RESULT_CHARS} characters: narrow the query, lower maxFiles/limit or pass a path]`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `codegraph_files` wants a root-relative POSIX prefix; accept an absolute path inside the project too. */
|
|
135
|
+
export function normalizeFilesPath(path: unknown, root: string): string | undefined {
|
|
136
|
+
if (typeof path !== "string" || path.trim() === "") return undefined;
|
|
137
|
+
let p = path.trim().replace(/\\/g, "/");
|
|
138
|
+
const r = root.replace(/\\/g, "/").replace(/\/$/, "");
|
|
139
|
+
if (p === r) return undefined;
|
|
140
|
+
if (p.startsWith(`${r}/`)) p = p.slice(r.length + 1).replace(/^(?:\.?\/)+/, "");
|
|
141
|
+
return p.replace(/^\.\/(?:\.?\/)*/, "").replace(/\/$/, "") || undefined;
|
|
142
|
+
}
|