@isac322/pi-codegraph 0.2.0 → 0.3.1
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/.mcp.json +1 -2
- package/CONTRIBUTING.md +63 -0
- package/README.md +35 -2
- package/dist/bin/codegraph-mcp.d.ts +3 -0
- package/dist/bin/codegraph-mcp.d.ts.map +1 -0
- package/dist/bin/codegraph-mcp.js +199 -0
- package/dist/bin/codegraph-mcp.js.map +1 -0
- package/dist/extensions/omp.d.ts +14 -0
- package/dist/extensions/omp.d.ts.map +1 -0
- package/dist/extensions/omp.js +19 -0
- package/dist/extensions/omp.js.map +1 -0
- package/dist/extensions/pi.d.ts +3 -0
- package/dist/extensions/pi.d.ts.map +1 -0
- package/dist/extensions/pi.js +162 -0
- package/dist/extensions/pi.js.map +1 -0
- package/dist/lib/codegraph.d.ts +65 -0
- package/dist/lib/codegraph.d.ts.map +1 -0
- package/dist/lib/codegraph.js +567 -0
- package/dist/lib/codegraph.js.map +1 -0
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +131 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/jsonrpc.d.ts +19 -0
- package/dist/lib/jsonrpc.d.ts.map +1 -0
- package/dist/lib/jsonrpc.js +116 -0
- package/dist/lib/jsonrpc.js.map +1 -0
- package/dist/lib/pi-mcp-client.d.ts +15 -0
- package/dist/lib/pi-mcp-client.d.ts.map +1 -0
- package/dist/lib/pi-mcp-client.js +84 -0
- package/dist/lib/pi-mcp-client.js.map +1 -0
- package/dist/lib/prompt.d.ts +7 -0
- package/dist/lib/prompt.d.ts.map +1 -0
- package/dist/lib/prompt.js +16 -0
- package/dist/lib/prompt.js.map +1 -0
- package/dist/lib/tool-metadata.d.ts +10 -0
- package/dist/lib/tool-metadata.d.ts.map +1 -0
- package/dist/lib/tool-metadata.js +163 -0
- package/dist/lib/tool-metadata.js.map +1 -0
- package/dist/lib/types.d.ts +69 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/worker-pool.d.ts +19 -0
- package/dist/lib/worker-pool.d.ts.map +1 -0
- package/dist/lib/worker-pool.js +221 -0
- package/dist/lib/worker-pool.js.map +1 -0
- package/package.json +30 -9
- package/bin/codegraph-mcp.mjs +0 -146
- package/extensions/omp.mjs +0 -14
- package/extensions/pi.mjs +0 -127
- package/lib/codegraph.mjs +0 -415
- package/lib/config.mjs +0 -103
- package/lib/jsonrpc.mjs +0 -90
- package/lib/pi-mcp-client.mjs +0 -68
- package/lib/prompt.mjs +0 -15
- package/lib/tool-metadata.mjs +0 -97
- package/lib/worker-pool.mjs +0 -107
package/extensions/pi.mjs
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
-
import { loadSettings } from "../lib/config.mjs";
|
|
3
|
-
import { workspaceSummary } from "../lib/codegraph.mjs";
|
|
4
|
-
import { buildCodeGraphPrompt } from "../lib/prompt.mjs";
|
|
5
|
-
import { PiCodeGraphClient } from "../lib/pi-mcp-client.mjs";
|
|
6
|
-
import { codegraphTools, summarizeToolText, toolCallLabel } from "../lib/tool-metadata.mjs";
|
|
7
|
-
|
|
8
|
-
function textContent(result) {
|
|
9
|
-
return (result?.content || []).filter((item) => item?.type === "text").map((item) => item.text).join("\n");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function trusted(ctx) {
|
|
13
|
-
if (typeof ctx?.isProjectTrusted !== "function") return true;
|
|
14
|
-
return Boolean(await ctx.isProjectTrusted());
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export default async function piCodeGraphExtension(pi) {
|
|
18
|
-
const settings = await loadSettings();
|
|
19
|
-
let sessionCwd = process.cwd();
|
|
20
|
-
let client;
|
|
21
|
-
|
|
22
|
-
const getClient = async (cwd, ctx) => {
|
|
23
|
-
if (!(await trusted(ctx))) throw new Error("CodeGraph is disabled until this project is trusted.");
|
|
24
|
-
if (!client || sessionCwd !== cwd) {
|
|
25
|
-
await client?.close();
|
|
26
|
-
sessionCwd = cwd;
|
|
27
|
-
client = new PiCodeGraphClient(settings, cwd);
|
|
28
|
-
}
|
|
29
|
-
return client;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
for (const tool of codegraphTools) {
|
|
33
|
-
pi.registerTool({
|
|
34
|
-
name: tool.name,
|
|
35
|
-
label: tool.label,
|
|
36
|
-
description: tool.description,
|
|
37
|
-
promptSnippet: tool.promptSnippet,
|
|
38
|
-
promptGuidelines: tool.promptGuidelines,
|
|
39
|
-
parameters: tool.inputSchema,
|
|
40
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
41
|
-
const cwd = ctx?.cwd || sessionCwd || process.cwd();
|
|
42
|
-
const activeClient = await getClient(cwd, ctx);
|
|
43
|
-
const args = { ...(params || {}), projectPath: params?.projectPath || cwd };
|
|
44
|
-
const result = await activeClient.callTool(tool.name, args, signal);
|
|
45
|
-
const text = textContent(result);
|
|
46
|
-
if (result?.isError) throw new Error(text || `${tool.label} failed`);
|
|
47
|
-
const summary = summarizeToolText(text);
|
|
48
|
-
return {
|
|
49
|
-
content: result?.content?.length ? result.content : [{ type: "text", text: JSON.stringify(result) }],
|
|
50
|
-
details: { toolName: tool.name, projectPath: args.projectPath, ...summary },
|
|
51
|
-
};
|
|
52
|
-
},
|
|
53
|
-
renderCall(args, theme) {
|
|
54
|
-
return new Text(theme.fg("toolTitle", theme.bold(`${tool.label} `)) + theme.fg("muted", toolCallLabel(tool.name, args)), 0, 0);
|
|
55
|
-
},
|
|
56
|
-
renderResult(result, { expanded }, theme) {
|
|
57
|
-
const details = result.details || {};
|
|
58
|
-
const text = textContent(result);
|
|
59
|
-
if (!text) return new Text(theme.fg("dim", "No output"), 0, 0);
|
|
60
|
-
if (expanded) return new Text(text, 0, 0);
|
|
61
|
-
const lines = text.split("\n");
|
|
62
|
-
const preview = lines.slice(0, 6).join("\n");
|
|
63
|
-
const suffix = lines.length > 6 ? `\n${theme.fg("dim", `... ${lines.length - 6} more lines`)}` : "";
|
|
64
|
-
const marker = details.truncated ? theme.fg("warning", " [truncated]") : "";
|
|
65
|
-
return new Text(`${preview}${suffix}${marker}`, 0, 0);
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
71
|
-
sessionCwd = ctx.cwd || process.cwd();
|
|
72
|
-
if (!(await trusted(ctx))) return;
|
|
73
|
-
ctx.ui?.setStatus?.("codegraph", "CodeGraph indexing");
|
|
74
|
-
try {
|
|
75
|
-
const activeClient = await getClient(sessionCwd, ctx);
|
|
76
|
-
await activeClient.request("codegraph/workspace/prepare", { projectPath: sessionCwd });
|
|
77
|
-
} catch (error) {
|
|
78
|
-
ctx.ui?.notify?.(`CodeGraph initialization failed: ${error.message}`, "warning");
|
|
79
|
-
} finally {
|
|
80
|
-
ctx.ui?.setStatus?.("codegraph", undefined);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
85
|
-
if (!settings.promptInjection || !(await trusted(ctx))) return;
|
|
86
|
-
const cwd = ctx.cwd || sessionCwd || process.cwd();
|
|
87
|
-
const status = await workspaceSummary(cwd);
|
|
88
|
-
const guidance = buildCodeGraphPrompt({ runtime: "pi", cwd, status });
|
|
89
|
-
return { systemPrompt: event.systemPrompt ? `${event.systemPrompt}\n\n${guidance}` : guidance };
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
pi.on("session_shutdown", async () => {
|
|
93
|
-
await client?.close();
|
|
94
|
-
client = undefined;
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
pi.registerCommand("codegraph", {
|
|
98
|
-
description: "CodeGraph status, sync, doctor, or gc",
|
|
99
|
-
handler: async (input, ctx) => {
|
|
100
|
-
if (!(await trusted(ctx))) {
|
|
101
|
-
ctx.ui?.notify?.("Trust this project before using CodeGraph.", "error");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const action = String(input || "status").trim().split(/\s+/)[0] || "status";
|
|
105
|
-
const methods = {
|
|
106
|
-
status: "codegraph/workspace/status",
|
|
107
|
-
sync: "codegraph/workspace/sync",
|
|
108
|
-
doctor: "codegraph/workspace/doctor",
|
|
109
|
-
gc: "codegraph/workspace/gc",
|
|
110
|
-
};
|
|
111
|
-
if (!methods[action]) {
|
|
112
|
-
ctx.ui?.notify?.("Usage: /codegraph [status|sync|doctor|gc]", "error");
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
ctx.ui?.setStatus?.("codegraph", `CodeGraph ${action}`);
|
|
116
|
-
try {
|
|
117
|
-
const activeClient = await getClient(ctx.cwd || sessionCwd, ctx);
|
|
118
|
-
const result = await activeClient.request(methods[action], { projectPath: ctx.cwd || sessionCwd, force: action === "gc" });
|
|
119
|
-
ctx.ui?.notify?.(JSON.stringify(result, null, 2), "info");
|
|
120
|
-
} catch (error) {
|
|
121
|
-
ctx.ui?.notify?.(`CodeGraph ${action} failed: ${error.message}`, "error");
|
|
122
|
-
} finally {
|
|
123
|
-
ctx.ui?.setStatus?.("codegraph", undefined);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
}
|
package/lib/codegraph.mjs
DELETED
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { spawn, execFile } from "node:child_process";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
-
import { access, lstat, mkdir, readFile, realpath, readdir, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
7
|
-
import os from "node:os";
|
|
8
|
-
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
const require = createRequire(import.meta.url);
|
|
11
|
-
const metadataName = ".pi-codegraph.json";
|
|
12
|
-
const emptyFilesMarker = "No files found matching the criteria.";
|
|
13
|
-
|
|
14
|
-
export function sanitizeDiagnostic(value, maxLength = 2_000) {
|
|
15
|
-
const clean = String(value || "")
|
|
16
|
-
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
17
|
-
.replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|APIKEY|AUTH)[A-Z0-9_]*=)\S+/gi, "$1[redacted]")
|
|
18
|
-
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
|
|
19
|
-
.replace(/--(?:token|secret|password|api-key|apikey|otp)(?:=|\s+)\S+/gi, "--[redacted]");
|
|
20
|
-
return clean.length > maxLength ? `${clean.slice(0, maxLength)}...` : clean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function truncateText(value, maxChars) {
|
|
24
|
-
const text = String(value ?? "");
|
|
25
|
-
if (text.length <= maxChars) return { text, truncated: false };
|
|
26
|
-
const marker = "\n\n[pi-codegraph output truncated]\n\n";
|
|
27
|
-
const headLength = Math.floor((maxChars - marker.length) * 0.75);
|
|
28
|
-
const tailLength = Math.max(0, maxChars - marker.length - headLength);
|
|
29
|
-
return { text: `${text.slice(0, headLength)}${marker}${text.slice(-tailLength)}`, truncated: true };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function normalizeFilesPath(inputPath, projectCwd) {
|
|
33
|
-
if (typeof inputPath !== "string" || !inputPath.trim()) return undefined;
|
|
34
|
-
let expanded = inputPath.trim();
|
|
35
|
-
if (expanded === "~" || expanded.startsWith("~/")) expanded = join(os.homedir(), expanded.slice(1));
|
|
36
|
-
if (projectCwd && isAbsolute(expanded)) {
|
|
37
|
-
const rel = relative(projectCwd, expanded);
|
|
38
|
-
if (!rel) return undefined;
|
|
39
|
-
if (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel)) return rel.split(sep).join("/");
|
|
40
|
-
}
|
|
41
|
-
return expanded.split(sep).join("/");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function annotateFilesResult(text, originalPath) {
|
|
45
|
-
if (!originalPath || !String(text).includes(emptyFilesMarker)) return text;
|
|
46
|
-
return `${text}\n\nHint: codegraph_files expects a repo-relative POSIX prefix such as \"src/components\". The path \"${originalPath}\" did not match the index.`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function existingDirectory(input) {
|
|
50
|
-
const resolved = await realpath(resolve(input));
|
|
51
|
-
const info = await stat(resolved);
|
|
52
|
-
if (!info.isDirectory()) throw new Error(`Project path is not a directory: ${input}`);
|
|
53
|
-
return resolved;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function gitValue(cwd, args) {
|
|
57
|
-
try {
|
|
58
|
-
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], { timeout: 5_000 });
|
|
59
|
-
return stdout.trim();
|
|
60
|
-
} catch {
|
|
61
|
-
return "";
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function gitIdentity(input) {
|
|
66
|
-
const sourcePath = await existingDirectory(input);
|
|
67
|
-
const top = await gitValue(sourcePath, ["rev-parse", "--show-toplevel"]);
|
|
68
|
-
if (!top) {
|
|
69
|
-
const info = await stat(sourcePath);
|
|
70
|
-
return {
|
|
71
|
-
sourcePath,
|
|
72
|
-
repoRoot: sourcePath,
|
|
73
|
-
repoIdentity: `directory:${sourcePath}:${info.dev}:${info.ino}:${info.birthtimeMs}`,
|
|
74
|
-
worktreeIdentity: `directory:${sourcePath}:${info.dev}:${info.ino}`,
|
|
75
|
-
gitCommonDir: "",
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
const repoRoot = await realpath(top);
|
|
79
|
-
const commonRaw = await gitValue(repoRoot, ["rev-parse", "--git-common-dir"]);
|
|
80
|
-
const gitDirRaw = await gitValue(repoRoot, ["rev-parse", "--git-dir"]);
|
|
81
|
-
const common = await realpath(isAbsolute(commonRaw) ? commonRaw : resolve(repoRoot, commonRaw));
|
|
82
|
-
const gitDir = await realpath(isAbsolute(gitDirRaw) ? gitDirRaw : resolve(repoRoot, gitDirRaw));
|
|
83
|
-
const [rootCommits, remote, commonInfo, gitDirInfo, sourceInfo] = await Promise.all([
|
|
84
|
-
gitValue(repoRoot, ["rev-list", "--max-parents=0", "HEAD"]),
|
|
85
|
-
gitValue(repoRoot, ["config", "--get", "remote.origin.url"]),
|
|
86
|
-
stat(common),
|
|
87
|
-
stat(gitDir),
|
|
88
|
-
stat(repoRoot),
|
|
89
|
-
]);
|
|
90
|
-
const repoIdentity = createHash("sha256").update([common, rootCommits, remote, commonInfo.dev, commonInfo.ino, commonInfo.birthtimeMs].join("\0")).digest("hex");
|
|
91
|
-
const worktreeIdentity = createHash("sha256").update([gitDir, gitDirInfo.dev, gitDirInfo.ino, repoRoot, sourceInfo.dev, sourceInfo.ino].join("\0")).digest("hex");
|
|
92
|
-
return { sourcePath: repoRoot, repoRoot, repoIdentity, worktreeIdentity, gitCommonDir: common };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isWithin(child, parent) {
|
|
96
|
-
const rel = relative(parent, child);
|
|
97
|
-
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export class ProjectGuard {
|
|
101
|
-
static async create(baseRoot, settings) {
|
|
102
|
-
if (process.env.PI_CODEGRAPH_TRUSTED === "0") throw new Error("CodeGraph is disabled for an untrusted project.");
|
|
103
|
-
const base = await gitIdentity(baseRoot);
|
|
104
|
-
const allowedRoots = [];
|
|
105
|
-
for (const root of [base.sourcePath, ...settings.allowedProjectRoots]) {
|
|
106
|
-
try {
|
|
107
|
-
allowedRoots.push(await existingDirectory(root));
|
|
108
|
-
} catch {
|
|
109
|
-
// Ignore unavailable optional roots.
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return new ProjectGuard(base, allowedRoots);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
constructor(base, allowedRoots) {
|
|
116
|
-
this.base = base;
|
|
117
|
-
this.allowedRoots = allowedRoots;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async resolve(requestedPath) {
|
|
121
|
-
if (requestedPath && !isAbsolute(requestedPath)) throw new Error("CodeGraph projectPath must be absolute.");
|
|
122
|
-
const identity = await gitIdentity(requestedPath || this.base.sourcePath);
|
|
123
|
-
if (this.allowedRoots.some((root) => isWithin(identity.sourcePath, root))) return identity;
|
|
124
|
-
if (this.base.gitCommonDir && identity.gitCommonDir === this.base.gitCommonDir) return identity;
|
|
125
|
-
throw new Error(`CodeGraph projectPath is outside the trusted workspace and registered worktrees: ${identity.sourcePath}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function locatePackageJson(entry) {
|
|
130
|
-
let current = dirname(entry);
|
|
131
|
-
for (;;) {
|
|
132
|
-
const candidate = join(current, "package.json");
|
|
133
|
-
try {
|
|
134
|
-
const parsed = JSON.parse(await readFile(candidate, "utf8"));
|
|
135
|
-
if (parsed.name === "@colbymchenry/codegraph") return { path: candidate, parsed };
|
|
136
|
-
} catch {
|
|
137
|
-
// Continue toward the filesystem root.
|
|
138
|
-
}
|
|
139
|
-
const parent = dirname(current);
|
|
140
|
-
if (parent === current) return undefined;
|
|
141
|
-
current = parent;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function resolveCodeGraphLaunch(settings, args = []) {
|
|
146
|
-
if (settings.codegraphExecutable) return { command: settings.codegraphExecutable, args };
|
|
147
|
-
try {
|
|
148
|
-
let packageJson;
|
|
149
|
-
try {
|
|
150
|
-
const packagePath = require.resolve("@colbymchenry/codegraph/package.json");
|
|
151
|
-
packageJson = { path: packagePath, parsed: JSON.parse(await readFile(packagePath, "utf8")) };
|
|
152
|
-
} catch {
|
|
153
|
-
packageJson = await locatePackageJson(require.resolve("@colbymchenry/codegraph"));
|
|
154
|
-
}
|
|
155
|
-
const binValue = typeof packageJson?.parsed?.bin === "string" ? packageJson.parsed.bin : packageJson?.parsed?.bin?.codegraph;
|
|
156
|
-
if (binValue) return { command: process.execPath, args: [resolve(dirname(packageJson.path), binValue), ...args] };
|
|
157
|
-
} catch {
|
|
158
|
-
// Fall through to a global executable.
|
|
159
|
-
}
|
|
160
|
-
return { command: "codegraph", args };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function runCodeGraph(settings, cwd, args, options = {}) {
|
|
164
|
-
const launch = await resolveCodeGraphLaunch(settings, args);
|
|
165
|
-
return new Promise((resolvePromise, reject) => {
|
|
166
|
-
const child = spawn(launch.command, launch.args, { cwd, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
167
|
-
let stdout = "";
|
|
168
|
-
let stderr = "";
|
|
169
|
-
let settled = false;
|
|
170
|
-
const finish = (error, value) => {
|
|
171
|
-
if (settled) return;
|
|
172
|
-
settled = true;
|
|
173
|
-
clearTimeout(timer);
|
|
174
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
175
|
-
error ? reject(error) : resolvePromise(value);
|
|
176
|
-
};
|
|
177
|
-
const onAbort = () => {
|
|
178
|
-
child.kill();
|
|
179
|
-
const error = new Error(`codegraph ${args[0]} aborted`);
|
|
180
|
-
error.name = "AbortError";
|
|
181
|
-
finish(error);
|
|
182
|
-
};
|
|
183
|
-
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
184
|
-
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
185
|
-
child.on("error", finish);
|
|
186
|
-
child.on("exit", (code) => {
|
|
187
|
-
if (code === 0) finish(undefined, { stdout, stderr });
|
|
188
|
-
else finish(new Error(sanitizeDiagnostic(stderr) || `codegraph ${args[0]} exited with code ${code}`));
|
|
189
|
-
});
|
|
190
|
-
const timeoutMs = options.timeoutMs || settings.requestTimeoutMs;
|
|
191
|
-
const timer = setTimeout(() => {
|
|
192
|
-
child.kill();
|
|
193
|
-
const error = new Error(`codegraph ${args[0]} timed out after ${timeoutMs}ms`);
|
|
194
|
-
error.code = "ETIMEDOUT";
|
|
195
|
-
finish(error);
|
|
196
|
-
}, timeoutMs);
|
|
197
|
-
timer.unref?.();
|
|
198
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function exists(input) {
|
|
203
|
-
try {
|
|
204
|
-
await access(input);
|
|
205
|
-
return true;
|
|
206
|
-
} catch {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function readMetadata(indexPath) {
|
|
212
|
-
try {
|
|
213
|
-
return JSON.parse(await readFile(join(indexPath, metadataName), "utf8"));
|
|
214
|
-
} catch {
|
|
215
|
-
return undefined;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function writeMetadata(indexPath, metadata) {
|
|
220
|
-
const target = join(indexPath, metadataName);
|
|
221
|
-
const temporary = `${target}.${process.pid}.tmp`;
|
|
222
|
-
await writeFile(temporary, `${JSON.stringify(metadata, null, 2)}\n`, { mode: 0o600 });
|
|
223
|
-
await rename(temporary, target);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function acquireLock(lockPath, timeoutMs = 30_000) {
|
|
227
|
-
await mkdir(dirname(lockPath), { recursive: true });
|
|
228
|
-
const started = Date.now();
|
|
229
|
-
for (;;) {
|
|
230
|
-
try {
|
|
231
|
-
await mkdir(lockPath);
|
|
232
|
-
return async () => rm(lockPath, { recursive: true, force: true });
|
|
233
|
-
} catch (error) {
|
|
234
|
-
if (error?.code !== "EEXIST") throw error;
|
|
235
|
-
try {
|
|
236
|
-
const info = await stat(lockPath);
|
|
237
|
-
if (Date.now() - info.mtimeMs > timeoutMs * 2) await rm(lockPath, { recursive: true, force: true });
|
|
238
|
-
} catch {
|
|
239
|
-
// Another owner released the lock.
|
|
240
|
-
}
|
|
241
|
-
if (Date.now() - started >= timeoutMs) throw new Error(`Timed out waiting for CodeGraph index lock: ${lockPath}`);
|
|
242
|
-
await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export class WorkspaceManager {
|
|
248
|
-
constructor(settings) {
|
|
249
|
-
this.settings = settings;
|
|
250
|
-
this.lastSync = new Map();
|
|
251
|
-
this.lastGc = 0;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async prepare(identity, options = {}) {
|
|
255
|
-
await existingDirectory(identity.sourcePath);
|
|
256
|
-
const key = createHash("sha256").update(`${identity.repoIdentity}\0${identity.worktreeIdentity}\0${identity.sourcePath}`).digest("hex");
|
|
257
|
-
const managedIndex = join(this.settings.indexStore, "projects", key);
|
|
258
|
-
const lockPath = join(this.settings.indexStore, "locks", `${key}.lock`);
|
|
259
|
-
const release = await acquireLock(lockPath, this.settings.requestTimeoutMs);
|
|
260
|
-
try {
|
|
261
|
-
await existingDirectory(identity.sourcePath);
|
|
262
|
-
const binding = await this.#bind(identity, managedIndex);
|
|
263
|
-
const database = join(binding.indexPath, "codegraph.db");
|
|
264
|
-
if (!(await exists(database))) await runCodeGraph(this.settings, identity.sourcePath, ["init", "-i"], options);
|
|
265
|
-
const lastSync = this.lastSync.get(identity.sourcePath) || 0;
|
|
266
|
-
const shouldSync = (options.forceSync || this.settings.autoSync) && (options.forceSync || Date.now() - lastSync >= this.settings.syncMinIntervalMs);
|
|
267
|
-
if (shouldSync) {
|
|
268
|
-
await existingDirectory(identity.sourcePath);
|
|
269
|
-
await runCodeGraph(this.settings, identity.sourcePath, ["sync"], options);
|
|
270
|
-
this.lastSync.set(identity.sourcePath, Date.now());
|
|
271
|
-
}
|
|
272
|
-
const metadata = {
|
|
273
|
-
schemaVersion: 2,
|
|
274
|
-
sourcePath: identity.sourcePath,
|
|
275
|
-
repoIdentity: identity.repoIdentity,
|
|
276
|
-
worktreeIdentity: identity.worktreeIdentity,
|
|
277
|
-
managed: binding.managed,
|
|
278
|
-
lastPreparedAt: new Date().toISOString(),
|
|
279
|
-
lastSyncAt: this.lastSync.get(identity.sourcePath) || null,
|
|
280
|
-
};
|
|
281
|
-
await writeMetadata(binding.indexPath, metadata);
|
|
282
|
-
return { ...metadata, indexPath: binding.indexPath, state: "ready" };
|
|
283
|
-
} finally {
|
|
284
|
-
await release();
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async #bind(identity, managedIndex) {
|
|
289
|
-
const linkPath = join(identity.sourcePath, ".codegraph");
|
|
290
|
-
await mkdir(managedIndex, { recursive: true });
|
|
291
|
-
try {
|
|
292
|
-
const info = await lstat(linkPath);
|
|
293
|
-
if (info.isSymbolicLink()) {
|
|
294
|
-
let target;
|
|
295
|
-
try { target = await realpath(linkPath); } catch { target = ""; }
|
|
296
|
-
if (target === managedIndex) return { indexPath: managedIndex, managed: true };
|
|
297
|
-
const metadata = target ? await readMetadata(target) : undefined;
|
|
298
|
-
if (metadata?.managed && metadata.sourcePath === identity.sourcePath) {
|
|
299
|
-
await rm(linkPath, { force: true });
|
|
300
|
-
} else {
|
|
301
|
-
throw new Error(`Refusing to replace an unmanaged .codegraph symlink at ${identity.sourcePath}`);
|
|
302
|
-
}
|
|
303
|
-
} else if (info.isDirectory()) {
|
|
304
|
-
const metadata = await readMetadata(linkPath);
|
|
305
|
-
if (metadata && (metadata.repoIdentity !== identity.repoIdentity || metadata.worktreeIdentity !== identity.worktreeIdentity)) {
|
|
306
|
-
throw new Error(`Existing .codegraph belongs to a different repository identity: ${linkPath}`);
|
|
307
|
-
}
|
|
308
|
-
return { indexPath: linkPath, managed: false };
|
|
309
|
-
} else {
|
|
310
|
-
throw new Error(`Expected .codegraph to be a directory or symlink: ${linkPath}`);
|
|
311
|
-
}
|
|
312
|
-
} catch (error) {
|
|
313
|
-
if (error?.code !== "ENOENT") throw error;
|
|
314
|
-
}
|
|
315
|
-
await existingDirectory(identity.sourcePath);
|
|
316
|
-
const temporary = `${linkPath}.pi-codegraph-${process.pid}`;
|
|
317
|
-
await rm(temporary, { force: true });
|
|
318
|
-
await symlink(managedIndex, temporary, "dir");
|
|
319
|
-
try {
|
|
320
|
-
await rename(temporary, linkPath);
|
|
321
|
-
} catch (error) {
|
|
322
|
-
await rm(temporary, { force: true });
|
|
323
|
-
if (error?.code !== "EEXIST" && error?.code !== "ENOTEMPTY") throw error;
|
|
324
|
-
}
|
|
325
|
-
return { indexPath: await realpath(linkPath), managed: true };
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
async status(identity) {
|
|
329
|
-
const linkPath = join(identity.sourcePath, ".codegraph");
|
|
330
|
-
try {
|
|
331
|
-
const indexPath = await realpath(linkPath);
|
|
332
|
-
const metadata = await readMetadata(indexPath);
|
|
333
|
-
return {
|
|
334
|
-
state: (await exists(join(indexPath, "codegraph.db"))) ? "ready" : "uninitialized",
|
|
335
|
-
sourcePath: identity.sourcePath,
|
|
336
|
-
indexPath,
|
|
337
|
-
managed: metadata?.managed ?? false,
|
|
338
|
-
lastSyncAt: metadata?.lastSyncAt || null,
|
|
339
|
-
identityMatches: !metadata || (metadata.repoIdentity === identity.repoIdentity && metadata.worktreeIdentity === identity.worktreeIdentity),
|
|
340
|
-
};
|
|
341
|
-
} catch {
|
|
342
|
-
return { state: "missing", sourcePath: identity.sourcePath, indexPath: null, managed: false, lastSyncAt: null, identityMatches: true };
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async doctor(identity) {
|
|
347
|
-
const version = await runCodeGraph(this.settings, identity.sourcePath, ["--version"]);
|
|
348
|
-
return { executable: version.stdout.trim() || version.stderr.trim(), workspace: await this.status(identity), settings: publicSettings(this.settings) };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async gc(activeProjects = new Set(), force = false) {
|
|
352
|
-
if (!this.settings.autoGc && !force) return { removed: [] };
|
|
353
|
-
if (!force && Date.now() - this.lastGc < 60 * 60_000) return { removed: [] };
|
|
354
|
-
this.lastGc = Date.now();
|
|
355
|
-
const projectsRoot = join(this.settings.indexStore, "projects");
|
|
356
|
-
let entries = [];
|
|
357
|
-
try { entries = await readdir(projectsRoot, { withFileTypes: true }); } catch { return { removed: [] }; }
|
|
358
|
-
const removed = [];
|
|
359
|
-
for (const entry of entries) {
|
|
360
|
-
if (!entry.isDirectory()) continue;
|
|
361
|
-
const indexPath = join(projectsRoot, entry.name);
|
|
362
|
-
const metadata = await readMetadata(indexPath);
|
|
363
|
-
if (!metadata?.managed || activeProjects.has(metadata.sourcePath)) continue;
|
|
364
|
-
let stale = !(await exists(metadata.sourcePath));
|
|
365
|
-
if (!stale) {
|
|
366
|
-
try {
|
|
367
|
-
const identity = await gitIdentity(metadata.sourcePath);
|
|
368
|
-
stale = identity.repoIdentity !== metadata.repoIdentity || identity.worktreeIdentity !== metadata.worktreeIdentity;
|
|
369
|
-
} catch {
|
|
370
|
-
stale = true;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
if (stale) {
|
|
374
|
-
await rm(indexPath, { recursive: true, force: true });
|
|
375
|
-
removed.push(indexPath);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return { removed };
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
export async function workspaceSummary(cwd) {
|
|
383
|
-
try {
|
|
384
|
-
const identity = await gitIdentity(cwd);
|
|
385
|
-
const linkPath = join(identity.sourcePath, ".codegraph");
|
|
386
|
-
const indexPath = await realpath(linkPath);
|
|
387
|
-
const metadata = await readMetadata(indexPath);
|
|
388
|
-
return {
|
|
389
|
-
state: (await exists(join(indexPath, "codegraph.db"))) ? "ready" : "uninitialized",
|
|
390
|
-
sourcePath: identity.sourcePath,
|
|
391
|
-
indexPath,
|
|
392
|
-
lastSyncAt: metadata?.lastSyncAt || null,
|
|
393
|
-
identityMatches: !metadata || (metadata.repoIdentity === identity.repoIdentity && metadata.worktreeIdentity === identity.worktreeIdentity),
|
|
394
|
-
};
|
|
395
|
-
} catch {
|
|
396
|
-
return { state: "missing", sourcePath: resolve(cwd), indexPath: null, lastSyncAt: null, identityMatches: true };
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export function publicSettings(settings) {
|
|
401
|
-
return {
|
|
402
|
-
autoSync: settings.autoSync,
|
|
403
|
-
autoGc: settings.autoGc,
|
|
404
|
-
indexStore: settings.indexStore,
|
|
405
|
-
workerIdleTimeoutMs: settings.workerIdleTimeoutMs,
|
|
406
|
-
maxWorkers: settings.maxWorkers,
|
|
407
|
-
requestTimeoutMs: settings.requestTimeoutMs,
|
|
408
|
-
syncMinIntervalMs: settings.syncMinIntervalMs,
|
|
409
|
-
maxOutputChars: settings.maxOutputChars,
|
|
410
|
-
allowedProjectRoots: settings.allowedProjectRoots,
|
|
411
|
-
promptInjection: settings.promptInjection,
|
|
412
|
-
codegraphExecutable: settings.codegraphExecutable || "auto",
|
|
413
|
-
configFile: settings.configFile,
|
|
414
|
-
};
|
|
415
|
-
}
|
package/lib/config.mjs
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
|
|
5
|
-
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
6
|
-
const cacheHome = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
|
|
7
|
-
|
|
8
|
-
export const defaultSettings = Object.freeze({
|
|
9
|
-
autoSync: true,
|
|
10
|
-
autoGc: true,
|
|
11
|
-
indexStore: path.join(cacheHome, "pi-codegraph"),
|
|
12
|
-
workerIdleTimeoutMs: 5 * 60_000,
|
|
13
|
-
maxWorkers: 6,
|
|
14
|
-
requestTimeoutMs: 30_000,
|
|
15
|
-
syncMinIntervalMs: 15_000,
|
|
16
|
-
maxOutputChars: 60_000,
|
|
17
|
-
allowedProjectRoots: [],
|
|
18
|
-
promptInjection: true,
|
|
19
|
-
codegraphExecutable: "",
|
|
20
|
-
configFile: path.join(configHome, "pi-codegraph", "config.json"),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
function booleanValue(value, fallback) {
|
|
24
|
-
if (typeof value === "boolean") return value;
|
|
25
|
-
if (typeof value !== "string") return fallback;
|
|
26
|
-
if (["1", "true", "yes", "on"].includes(value.toLowerCase())) return true;
|
|
27
|
-
if (["0", "false", "no", "off"].includes(value.toLowerCase())) return false;
|
|
28
|
-
return fallback;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function integerValue(value, fallback, minimum = 0) {
|
|
32
|
-
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
33
|
-
return Number.isFinite(parsed) && parsed >= minimum ? parsed : fallback;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function stringArray(value, fallback = []) {
|
|
37
|
-
if (Array.isArray(value)) return value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim());
|
|
38
|
-
if (typeof value === "string" && value.trim()) return value.split(path.delimiter).map((item) => item.trim()).filter(Boolean);
|
|
39
|
-
return fallback;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function normalizeSettings(input) {
|
|
43
|
-
return {
|
|
44
|
-
autoSync: booleanValue(input.autoSync, defaultSettings.autoSync),
|
|
45
|
-
autoGc: booleanValue(input.autoGc, defaultSettings.autoGc),
|
|
46
|
-
indexStore: path.resolve(String(input.indexStore || defaultSettings.indexStore)),
|
|
47
|
-
workerIdleTimeoutMs: integerValue(input.workerIdleTimeoutMs, defaultSettings.workerIdleTimeoutMs, 1_000),
|
|
48
|
-
maxWorkers: integerValue(input.maxWorkers, defaultSettings.maxWorkers, 1),
|
|
49
|
-
requestTimeoutMs: integerValue(input.requestTimeoutMs, defaultSettings.requestTimeoutMs, 1_000),
|
|
50
|
-
syncMinIntervalMs: integerValue(input.syncMinIntervalMs, defaultSettings.syncMinIntervalMs, 0),
|
|
51
|
-
maxOutputChars: integerValue(input.maxOutputChars, defaultSettings.maxOutputChars, 4_000),
|
|
52
|
-
allowedProjectRoots: stringArray(input.allowedProjectRoots).map((root) => path.resolve(root)),
|
|
53
|
-
promptInjection: booleanValue(input.promptInjection, defaultSettings.promptInjection),
|
|
54
|
-
codegraphExecutable: typeof input.codegraphExecutable === "string" ? input.codegraphExecutable.trim() : "",
|
|
55
|
-
configFile: String(input.configFile || defaultSettings.configFile),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function loadSettings(overrides = {}) {
|
|
60
|
-
const configFile = process.env.PI_CODEGRAPH_CONFIG || overrides.configFile || defaultSettings.configFile;
|
|
61
|
-
let fileSettings = {};
|
|
62
|
-
try {
|
|
63
|
-
fileSettings = JSON.parse(await readFile(configFile, "utf8"));
|
|
64
|
-
} catch (error) {
|
|
65
|
-
if (error?.code !== "ENOENT") throw new Error(`Invalid pi-codegraph config at ${configFile}: ${error.message}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const environment = {
|
|
69
|
-
autoSync: process.env.PI_CODEGRAPH_AUTO_SYNC,
|
|
70
|
-
autoGc: process.env.PI_CODEGRAPH_AUTO_GC,
|
|
71
|
-
indexStore: process.env.PI_CODEGRAPH_INDEX_STORE,
|
|
72
|
-
workerIdleTimeoutMs: process.env.PI_CODEGRAPH_WORKER_IDLE_MS,
|
|
73
|
-
maxWorkers: process.env.PI_CODEGRAPH_MAX_WORKERS,
|
|
74
|
-
requestTimeoutMs: process.env.PI_CODEGRAPH_REQUEST_TIMEOUT_MS,
|
|
75
|
-
syncMinIntervalMs: process.env.PI_CODEGRAPH_SYNC_MIN_INTERVAL_MS,
|
|
76
|
-
maxOutputChars: process.env.PI_CODEGRAPH_MAX_OUTPUT_CHARS,
|
|
77
|
-
allowedProjectRoots: process.env.PI_CODEGRAPH_ALLOWED_ROOTS,
|
|
78
|
-
promptInjection: process.env.PI_CODEGRAPH_PROMPT_INJECTION,
|
|
79
|
-
codegraphExecutable: process.env.PI_CODEGRAPH_EXECUTABLE,
|
|
80
|
-
configFile,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const compactEnvironment = Object.fromEntries(Object.entries(environment).filter(([, value]) => value !== undefined));
|
|
84
|
-
return normalizeSettings({ ...defaultSettings, ...fileSettings, ...compactEnvironment, ...overrides, configFile });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function settingsEnvironment(settings, baseRoot, trusted = true) {
|
|
88
|
-
return {
|
|
89
|
-
PI_CODEGRAPH_BASE_ROOT: baseRoot,
|
|
90
|
-
PI_CODEGRAPH_TRUSTED: trusted ? "1" : "0",
|
|
91
|
-
PI_CODEGRAPH_AUTO_SYNC: String(settings.autoSync),
|
|
92
|
-
PI_CODEGRAPH_AUTO_GC: String(settings.autoGc),
|
|
93
|
-
PI_CODEGRAPH_INDEX_STORE: settings.indexStore,
|
|
94
|
-
PI_CODEGRAPH_WORKER_IDLE_MS: String(settings.workerIdleTimeoutMs),
|
|
95
|
-
PI_CODEGRAPH_MAX_WORKERS: String(settings.maxWorkers),
|
|
96
|
-
PI_CODEGRAPH_REQUEST_TIMEOUT_MS: String(settings.requestTimeoutMs),
|
|
97
|
-
PI_CODEGRAPH_SYNC_MIN_INTERVAL_MS: String(settings.syncMinIntervalMs),
|
|
98
|
-
PI_CODEGRAPH_MAX_OUTPUT_CHARS: String(settings.maxOutputChars),
|
|
99
|
-
PI_CODEGRAPH_ALLOWED_ROOTS: settings.allowedProjectRoots.join(path.delimiter),
|
|
100
|
-
PI_CODEGRAPH_PROMPT_INJECTION: String(settings.promptInjection),
|
|
101
|
-
...(settings.codegraphExecutable ? { PI_CODEGRAPH_EXECUTABLE: settings.codegraphExecutable } : {}),
|
|
102
|
-
};
|
|
103
|
-
}
|