@isac322/pi-codegraph 0.2.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/.mcp.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "codegraph": {
4
+ "command": "node",
5
+ "args": ["./bin/codegraph-mcp.mjs"]
6
+ }
7
+ }
8
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 isac322
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 ADDED
@@ -0,0 +1,95 @@
1
+ # @isac322/pi-codegraph
2
+
3
+ CodeGraph tools for Pi and OMP, with worktree-aware index lifecycle management.
4
+
5
+ ## Why this package
6
+
7
+ The extension exposes CodeGraph's structural tools with native Pi metadata and rendering while keeping OMP on its native MCP path. Indexes are separated per worktree, stored centrally, synchronized automatically, identity-checked, and garbage-collected after worktrees disappear.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pi install npm:@isac322/pi-codegraph
13
+ ```
14
+
15
+ OMP loads the package-local `.mcp.json` and `omp.extensions` entry. Pi loads `pi.extensions` and starts the internal MCP facade at session start, never during extension discovery.
16
+
17
+ The package installs `@colbymchenry/codegraph` as an optional dependency and falls back to a `codegraph` executable on `PATH`.
18
+
19
+ ## Tools
20
+
21
+ - `codegraph_explore`: broad architecture and flow exploration
22
+ - `codegraph_search`: symbol-name lookup
23
+ - `codegraph_node`: one known symbol and its relationships
24
+ - `codegraph_files`: indexed project structure
25
+ - `codegraph_callers`: inbound calls
26
+ - `codegraph_callees`: outbound calls
27
+ - `codegraph_impact`: transitive change impact
28
+ - `codegraph_status`: CodeGraph index health
29
+
30
+ Pi adds compact call/result rendering and `/codegraph status|sync|doctor|gc`.
31
+
32
+ ## Worktrees
33
+
34
+ Each worktree gets a distinct database. New indexes are stored under the configured central index store and exposed to CodeGraph through the worktree's `.codegraph` symlink. Existing real `.codegraph` directories remain in place and receive identity metadata.
35
+
36
+ A project path is accepted only when it is inside an allowed root or resolves to a worktree with the same Git common directory as the session root. Repository and worktree identities are checked before an index is reused. Missing or replaced worktrees fail closed.
37
+
38
+ ## Configuration
39
+
40
+ Global configuration is read from `~/.config/pi-codegraph/config.json`, or from `PI_CODEGRAPH_CONFIG`.
41
+
42
+ ```json
43
+ {
44
+ "autoSync": true,
45
+ "autoGc": true,
46
+ "indexStore": "/home/me/.cache/pi-codegraph",
47
+ "workerIdleTimeoutMs": 300000,
48
+ "maxWorkers": 6,
49
+ "requestTimeoutMs": 30000,
50
+ "syncMinIntervalMs": 15000,
51
+ "maxOutputChars": 60000,
52
+ "allowedProjectRoots": ["/work/company"],
53
+ "promptInjection": true,
54
+ "codegraphExecutable": ""
55
+ }
56
+ ```
57
+
58
+ Environment overrides:
59
+
60
+ - `PI_CODEGRAPH_AUTO_SYNC`
61
+ - `PI_CODEGRAPH_AUTO_GC`
62
+ - `PI_CODEGRAPH_INDEX_STORE`
63
+ - `PI_CODEGRAPH_WORKER_IDLE_MS`
64
+ - `PI_CODEGRAPH_MAX_WORKERS`
65
+ - `PI_CODEGRAPH_REQUEST_TIMEOUT_MS`
66
+ - `PI_CODEGRAPH_SYNC_MIN_INTERVAL_MS`
67
+ - `PI_CODEGRAPH_MAX_OUTPUT_CHARS`
68
+ - `PI_CODEGRAPH_ALLOWED_ROOTS`
69
+ - `PI_CODEGRAPH_PROMPT_INJECTION`
70
+ - `PI_CODEGRAPH_EXECUTABLE`
71
+
72
+ `PI_CODEGRAPH_ALLOWED_ROOTS` uses the platform path delimiter.
73
+
74
+ ## Runtime behavior
75
+
76
+ - Pi defers process startup until `session_start` and closes all resources on `session_shutdown`.
77
+ - OMP uses one package-local MCP facade and project-scoped CodeGraph workers.
78
+ - Workers are capped, evicted by least-recently-used idle order, and terminated after the idle timeout.
79
+ - Tool cancellation and timeout propagate to the worker. Diagnostics are ANSI-stripped, size-limited, and redact common token and secret forms.
80
+ - `codegraph_files` accepts absolute in-project paths and `~`, normalizing them to repo-relative POSIX prefixes.
81
+ - Large results are bounded and retain both their beginning and end with an explicit truncation marker.
82
+
83
+ ## Security
84
+
85
+ Pi checks project trust before initialization or tool execution. The MCP facade resolves real paths and restricts access to the active workspace, sibling worktrees of the same repository, and explicitly configured roots.
86
+
87
+ ## Pi package gallery
88
+
89
+ The npm package declares the `pi-package` keyword and an explicit `pi.extensions` manifest. After npm publication, Pi's package gallery can index it at [pi.dev/packages](https://pi.dev/packages).
90
+
91
+ Releases use npm trusted publishing from `.github/workflows/publish.yml`. Configure the npm trusted publisher for repository `isac322/pi-codegraph` and workflow `publish.yml`, then publish a GitHub Release.
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ import { loadSettings } from "../lib/config.mjs";
3
+ import { ProjectGuard, WorkspaceManager, annotateFilesResult, normalizeFilesPath, publicSettings, sanitizeDiagnostic, truncateText } from "../lib/codegraph.mjs";
4
+ import { CodeGraphWorkerPool } from "../lib/worker-pool.mjs";
5
+ import { codegraphTools, codegraphToolNames } from "../lib/tool-metadata.mjs";
6
+
7
+ const settings = await loadSettings();
8
+ const baseRoot = process.env.PI_CODEGRAPH_BASE_ROOT || process.cwd();
9
+ const guard = await ProjectGuard.create(baseRoot, settings);
10
+ const manager = new WorkspaceManager(settings);
11
+ const pool = new CodeGraphWorkerPool(settings);
12
+ const controllers = new Map();
13
+ let buffer = "";
14
+ let gcTimer;
15
+
16
+ function write(message) {
17
+ process.stdout.write(`${JSON.stringify(message)}\n`);
18
+ }
19
+
20
+ function response(id, result) {
21
+ write({ jsonrpc: "2.0", id, result });
22
+ }
23
+
24
+ function errorResponse(id, error, code = -32603) {
25
+ write({ jsonrpc: "2.0", id, error: { code, message: sanitizeDiagnostic(error?.message || error) } });
26
+ }
27
+
28
+ async function resolveIdentity(params = {}) {
29
+ return guard.resolve(params.projectPath || baseRoot);
30
+ }
31
+
32
+ function extractText(result) {
33
+ return (result?.content || []).filter((part) => part?.type === "text").map((part) => part.text).join("\n") || JSON.stringify(result);
34
+ }
35
+
36
+ async function callTool(params, signal) {
37
+ const name = params?.name;
38
+ if (!codegraphToolNames.includes(name)) throw new Error(`Unknown CodeGraph tool: ${name}`);
39
+ const supplied = { ...(params?.arguments || {}) };
40
+ const identity = await resolveIdentity(supplied);
41
+ await manager.prepare(identity, { signal });
42
+ delete supplied.projectPath;
43
+ const originalFilesPath = name === "codegraph_files" && typeof supplied.path === "string" ? supplied.path : undefined;
44
+ if (name === "codegraph_files") {
45
+ const normalized = normalizeFilesPath(originalFilesPath, identity.sourcePath);
46
+ if (normalized === undefined) delete supplied.path;
47
+ else supplied.path = normalized;
48
+ }
49
+ const result = await pool.call(identity.sourcePath, name, supplied, signal);
50
+ if (result?.isError) return result;
51
+ let text = extractText(result);
52
+ if (name === "codegraph_files") text = annotateFilesResult(text, originalFilesPath);
53
+ const limited = truncateText(text, settings.maxOutputChars);
54
+ return {
55
+ ...result,
56
+ content: [{ type: "text", text: limited.text }],
57
+ _meta: { projectPath: identity.sourcePath, truncated: limited.truncated },
58
+ };
59
+ }
60
+
61
+ async function handleRequest(message, signal) {
62
+ switch (message.method) {
63
+ case "initialize":
64
+ return {
65
+ protocolVersion: "2024-11-05",
66
+ capabilities: { tools: { listChanged: false } },
67
+ serverInfo: { name: "pi-codegraph", version: "0.2.0" },
68
+ };
69
+ case "tools/list":
70
+ return { tools: codegraphTools.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })) };
71
+ case "tools/call":
72
+ return callTool(message.params, signal);
73
+ case "codegraph/workspace/prepare": {
74
+ const identity = await resolveIdentity(message.params);
75
+ const result = await manager.prepare(identity, { signal });
76
+ await manager.gc(pool.activeProjects());
77
+ return result;
78
+ }
79
+ case "codegraph/workspace/status": {
80
+ const identity = await resolveIdentity(message.params);
81
+ return manager.status(identity);
82
+ }
83
+ case "codegraph/workspace/sync": {
84
+ const identity = await resolveIdentity(message.params);
85
+ return manager.prepare(identity, { signal, forceSync: true });
86
+ }
87
+ case "codegraph/workspace/doctor": {
88
+ const identity = await resolveIdentity(message.params);
89
+ return manager.doctor(identity);
90
+ }
91
+ case "codegraph/workspace/gc":
92
+ return manager.gc(pool.activeProjects(), Boolean(message.params?.force));
93
+ case "codegraph/settings":
94
+ return publicSettings(settings);
95
+ case "shutdown":
96
+ return null;
97
+ default:
98
+ throw Object.assign(new Error(`Method not found: ${message.method}`), { rpcCode: -32601 });
99
+ }
100
+ }
101
+
102
+ async function receive(message) {
103
+ if (message.method === "notifications/cancelled") {
104
+ controllers.get(message.params?.requestId)?.abort();
105
+ return;
106
+ }
107
+ if (message.id === undefined) return;
108
+ const controller = new AbortController();
109
+ controllers.set(message.id, controller);
110
+ try {
111
+ response(message.id, await handleRequest(message, controller.signal));
112
+ } catch (error) {
113
+ if (message.method === "tools/call") {
114
+ response(message.id, { content: [{ type: "text", text: sanitizeDiagnostic(error.message) }], isError: true });
115
+ } else {
116
+ errorResponse(message.id, error, error.rpcCode);
117
+ }
118
+ } finally {
119
+ controllers.delete(message.id);
120
+ }
121
+ }
122
+
123
+ process.stdin.on("data", (chunk) => {
124
+ buffer += chunk.toString("utf8");
125
+ let newline;
126
+ while ((newline = buffer.indexOf("\n")) !== -1) {
127
+ const line = buffer.slice(0, newline).trim();
128
+ buffer = buffer.slice(newline + 1);
129
+ if (!line) continue;
130
+ try { void receive(JSON.parse(line)); } catch { errorResponse(null, new Error("Invalid JSON"), -32700); }
131
+ }
132
+ });
133
+
134
+ async function shutdown() {
135
+ clearInterval(gcTimer);
136
+ for (const controller of controllers.values()) controller.abort();
137
+ await pool.close();
138
+ }
139
+
140
+ if (settings.autoGc) {
141
+ gcTimer = setInterval(() => { void manager.gc(pool.activeProjects()); }, 60 * 60_000);
142
+ gcTimer.unref?.();
143
+ }
144
+ process.on("SIGTERM", () => { void shutdown().finally(() => process.exit(0)); });
145
+ process.on("SIGINT", () => { void shutdown().finally(() => process.exit(0)); });
146
+ process.stdin.on("end", () => { void shutdown().finally(() => process.exit(0)); });
@@ -0,0 +1,14 @@
1
+ import { loadSettings } from "../lib/config.mjs";
2
+ import { workspaceSummary } from "../lib/codegraph.mjs";
3
+ import { buildCodeGraphPrompt } from "../lib/prompt.mjs";
4
+
5
+ export default async function ompCodeGraphExtension(omp) {
6
+ const settings = await loadSettings();
7
+ if (!settings.promptInjection || typeof omp?.on !== "function") return;
8
+ omp.on("before_agent_start", async (event, ctx) => {
9
+ const cwd = ctx?.cwd || process.cwd();
10
+ const status = await workspaceSummary(cwd);
11
+ const guidance = buildCodeGraphPrompt({ runtime: "omp", cwd, status });
12
+ return { systemPrompt: event.systemPrompt ? `${event.systemPrompt}\n\n${guidance}` : guidance };
13
+ });
14
+ }
@@ -0,0 +1,127 @@
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
+ }