@liuxincuit/pi-codegraph 0.1.1 → 0.1.3

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/CONTEXT.md CHANGED
@@ -1,21 +1,21 @@
1
- # pi-codegraph
2
-
3
- A pi extension that gives the agent access to a CodeGraph index of the current project.
4
-
5
- ## Language
6
-
7
- **Project**:
8
- The directory the pi session runs in (`ctx.cwd`). The unit that CodeGraph indexes and the default scope of every query.
9
- _Avoid_: workspace, repository, repo
10
-
11
- **Index**:
12
- The `.codegraph/` directory at the project root, holding the SQLite knowledge graph of the project's symbols, edges, and files. Built by `codegraph init`, updated by `codegraph sync`.
13
- _Avoid_: database, cache, graph (ambiguous with the data structure)
14
-
15
- **Explore**:
16
- The single agent-facing query operation: a natural-language or symbol question answered with the relevant symbols' verbatim source plus the call paths between them.
17
- _Avoid_: search, query, lookup
18
-
19
- **Sync**:
20
- An incremental update of the Index to match the files currently on disk. Cheap when nothing changed.
21
- _Avoid_: refresh, rebuild (that's a full re-index)
1
+ # pi-codegraph
2
+
3
+ A pi extension that gives the agent access to a CodeGraph index of the current project.
4
+
5
+ ## Language
6
+
7
+ **Project**:
8
+ The directory the pi session runs in (`ctx.cwd`). The unit that CodeGraph indexes and the default scope of every query.
9
+ _Avoid_: workspace, repository, repo
10
+
11
+ **Index**:
12
+ The `.codegraph/` directory at the project root, holding the SQLite knowledge graph of the project's symbols, edges, and files. Built by `codegraph init`, updated by `codegraph sync`.
13
+ _Avoid_: database, cache, graph (ambiguous with the data structure)
14
+
15
+ **Explore**:
16
+ The single agent-facing query operation: a natural-language or symbol question answered with the relevant symbols' verbatim source plus the call paths between them.
17
+ _Avoid_: search, query, lookup
18
+
19
+ **Sync**:
20
+ An incremental update of the Index to match the files currently on disk. Cheap when nothing changed.
21
+ _Avoid_: refresh, rebuild (that's a full re-index)
package/LICENSE CHANGED
@@ -1,22 +1,22 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 izhimu
4
- Copyright (c) 2026 liuxincuit
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 izhimu
4
+ Copyright (c) 2026 liuxincuit
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -49,7 +49,9 @@ pi -e ./extensions/codegraph.ts
49
49
 
50
50
  ## 功能一览
51
51
 
52
- - **`codegraph_explore` 工具**面向智能体的核心代码智能工具。输入符号名或自然语言问题即可查询;可选传 `path` 查询其他已建索引的项目,传 `maxFiles` 限制返回的源码行数。
52
+ - **`codegraph_*` 工具集**面向智能体的代码智能工具,均支持 `path` 参数查询其他已建索引的项目:
53
+ - `codegraph_explore`(**默认注册**)— 一揽子探索:相关符号逐字源码 + 调用路径 + 影响范围(`maxFiles` 限制返回行数)
54
+ - `codegraph_query` / `codegraph_node` / `codegraph_callers` / `codegraph_callees` / `codegraph_impact` / `codegraph_files`(**默认隐藏**,见下方配置)
53
55
  - **`/codegraph-init [path]`** — 为项目建立索引(`codegraph init`)。
54
56
  - **`/codegraph-sync [path]`** — 手动同步自上次索引以来的改动(`codegraph sync`)。
55
57
  - **`/codegraph-status [path]`** — 查看索引状态与统计信息(`codegraph status`)。
@@ -71,6 +73,18 @@ pi -e ./extensions/codegraph.ts
71
73
 
72
74
  索引构建始终由你显式触发——智能体自身不会运行 `codegraph init`(见 `docs/adr/0002`)。
73
75
 
76
+ ## 细粒度工具(可选开启)
77
+
78
+ `codegraph_explore` 能覆盖绝大多数结构化查询,因此另外 6 个细粒度工具默认**不注册**,避免过多工具增加智能体的决策负担(见 `docs/adr/0003`)。需要时在全局 `~/.pi/agent/extensions/pi-codegraph/config.json` 或项目 `.pi/extensions/pi-codegraph/config.json` 中配置 `extraTools` 开启(项目配置覆盖全局):
79
+
80
+ ```json
81
+ {
82
+ "extraTools": ["query", "node", "impact"]
83
+ }
84
+ ```
85
+
86
+ 短名(`node`、`impact`)与完整工具名(`codegraph_node`)均可用,`"all"` 开启全部。配置在下一个会话生效(`/reload` 或重启 pi)。
87
+
74
88
  ## 工作原理
75
89
 
76
90
  pi 原生不支持 MCP,因此本扩展通过执行 CLI 来桥接 CodeGraph——`codegraph explore` 产生与 `codegraph_explore` MCP 工具相同的输出(见 `docs/adr/0001`)。每次工具调用仅需一次 `pi.exec`:没有守护进程、没有 JSON-RPC,没有可泄漏或需要恢复的状态。
@@ -1,298 +1,596 @@
1
- // pi-codegraph — pi extension
2
- //
3
- // CodeGraph support for pi: codegraph_explore tool, /codegraph-init,
4
- // /codegraph-sync, /codegraph-status, and /codegraph-unlock commands.
5
- //
6
- // Bridges to CodeGraph by executing the CLI (see docs/adr/0001). Requires the
7
- // codegraph CLI on PATH: npm i -g @colbymchenry/codegraph
8
- // Upstream: https://github.com/colbymchenry/codegraph
9
-
10
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
- import { Type } from "typebox";
12
- import * as fs from "node:fs/promises";
13
- import * as path from "node:path";
14
- import { fileURLToPath } from "node:url";
15
-
16
- const INSTALL_HINT =
17
- "未检测到 codegraph CLI(PATH 中无 codegraph 命令),本插件未注入任何工具、命令或技能。" +
18
- "安装:npm i -g @colbymchenry/codegraph,然后 /reload 或重启 pi 生效。";
19
-
20
- // Whole-process dedup for the missing-CLI hint: extensions re-run per session and
21
- // per subagent (separate jiti module copies), so gate on globalThis.
22
- const MISSING_CLI_HINTED = Symbol.for("pi-codegraph.missing-cli-hinted");
23
-
24
- // Result of `codegraph version` this session — null until first check.
25
- let cliAvailable: boolean | null = null;
26
-
27
- async function execCg(
28
- pi: ExtensionAPI,
29
- args: string[],
30
- options: { signal?: AbortSignal; timeout?: number; cwd?: string } = {},
31
- ) {
32
- if (process.platform === "win32") {
33
- return await pi.exec("cmd.exe", ["/d", "/s", "/c", "codegraph", ...args], options);
34
- }
35
- return await pi.exec("codegraph", args, options);
36
- }
37
-
38
- async function ensureCli(pi: ExtensionAPI): Promise<boolean> {
39
- if (cliAvailable !== null) return cliAvailable;
40
- try {
41
- const result = await execCg(pi, ["version"], { timeout: 10_000 });
42
- cliAvailable = result.code === 0;
43
- } catch {
44
- cliAvailable = false;
45
- }
46
- return cliAvailable;
47
- }
48
-
49
- function textResult(text: string) {
50
- return { content: [{ type: "text" as const, text }], details: undefined };
51
- }
52
-
53
- function outputOf(result: { stdout: string; stderr: string; code: number }): string {
54
- return (result.stdout + result.stderr).trim() || `exit ${result.code}`;
55
- }
56
-
57
- async function isIndexed(cwd: string): Promise<boolean> {
58
- try {
59
- await fs.access(path.join(cwd, ".codegraph", "codegraph.db"));
60
- return true;
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- type StatusState = "index" | "sync" | "init" | boolean | undefined;
67
-
68
- function updateStatusBar(ctx: ExtensionContext, state: StatusState) {
69
- if (!ctx.hasUI) return;
70
- const label = state === true ? "index" : state === false ? undefined : state;
71
- if (label) {
72
- const text = ctx.ui.theme?.fg
73
- ? `${ctx.ui.theme.fg("accent", "⬡")} ${label}`
74
- : `⬡ ${label}`;
75
- ctx.ui.setStatus("codegraph", text);
76
- } else {
77
- ctx.ui.setStatus("codegraph", undefined);
78
- }
79
- }
80
-
81
- // Modeled on upstream's MCP SERVER_INSTRUCTIONS (src/mcp/server-instructions.ts):
82
- // lead the agent to codegraph_explore BEFORE grep/read, plus anti-patterns and staleness handling.
83
- const INDEX_HINT = `# CodeGraph — this project is indexed
84
-
85
- A \`.codegraph/\` index exists here: SQLite knowledge graph of every symbol, edge, and file (30+ languages). ONE \`codegraph_explore\` call returns the relevant symbols' verbatim line-numbered source (treat it as already Read — safe to Edit from) PLUS call paths between them and a blast-radius summary of what depends on them.
86
-
87
- - For structural questions (how does X work / where is X / who calls Y / what breaks if I change Z), call \`codegraph_explore\` INSTEAD of grep + read — usually ONE call answers the whole question.
88
- - Call it BEFORE and WHILE writing or editing code: it puts the blast radius in view before you touch a symbol you can name.
89
- - Flow tracing: name endpoint symbols (e.g. \`mutateElement renderScene\`) to surface the path across dynamic-dispatch hops.
90
- - Anti-patterns: don't grep or Read first; don't re-verify codegraph output with grep (AST-derived, more accurate than grep); don't reconstruct a flow by hand.
91
- - "Already sent earlier in this conversation": pointer means content is already in context — do not re-fetch or Read.
92
- - Staleness: if tool output contains "⚠️ Some files referenced below were edited since the last index sync", read only those flagged files directly.
93
- - Multi-project / Monorepo: pass \`path\` to query any indexed sub-project directory.
94
- - If a project has no \`.codegraph/\`, use built-in tools there; indexing is the user's decision suggest /codegraph-init if it comes up.`;
95
-
96
- // When the CLI is absent, register nothing but a one-shot hint handler: no
97
- // tools, no commands, no resources_discover (so no skill injection), no sync.
98
- export function registerMissingCliHint(pi: ExtensionAPI) {
99
- pi.on("session_start", async (_event, ctx) => {
100
- if ((globalThis as Record<symbol, boolean>)[MISSING_CLI_HINTED]) return;
101
- (globalThis as Record<symbol, boolean>)[MISSING_CLI_HINTED] = true;
102
- if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "warning");
103
- });
104
- }
105
-
106
- export default async function codegraphExtension(pi: ExtensionAPI) {
107
- if (!(await ensureCli(pi))) {
108
- registerMissingCliHint(pi);
109
- return;
110
- }
111
- // ── codegraph_explore tool ─────────────────────────────────────────────
112
- pi.registerTool({
113
- name: "codegraph_explore",
114
- label: "CodeGraph Explore",
115
- description:
116
- "PRIMARY tool for code questions — call it BEFORE grep/read when the project has a .codegraph/ index. " +
117
- "One query returns the relevant symbols' verbatim line-numbered source plus the call paths between them and a blast-radius summary. " +
118
- "If the project is not indexed the output says so: continue with built-in tools and suggest the user run /codegraph-init.",
119
- promptSnippet:
120
- "codegraph_explore: symbol source + call paths in one shot from the project's CodeGraph index",
121
- promptGuidelines: [
122
- "For structural code questions (how does X work, where is X, what breaks if I change X), prefer codegraph_explore over grep when the project has a .codegraph/ index.",
123
- "Indexing is the user's decision — never run codegraph init yourself; suggest /codegraph-init instead.",
124
- ],
125
- parameters: Type.Object({
126
- query: Type.String({
127
- description: "Symbol names or a natural-language question about the code",
128
- }),
129
- path: Type.Optional(
130
- Type.String({
131
- description: "Project path to query; defaults to the current working directory",
132
- }),
133
- ),
134
- maxFiles: Type.Optional(
135
- Type.Integer({
136
- description: "Maximum number of files to include source from",
137
- }),
138
- ),
139
- }),
140
- execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
141
- if (!(await ensureCli(pi))) return textResult(INSTALL_HINT);
142
- const cwd = params.path ?? ctx.cwd;
143
- const args = ["explore", params.query, "-p", cwd];
144
- if (typeof params.maxFiles === "number" && params.maxFiles > 0) {
145
- args.push("--max-files", String(params.maxFiles));
146
- }
147
- const result = await execCg(pi, args, {
148
- signal,
149
- timeout: 120_000,
150
- });
151
- if (result.killed) return textResult("codegraph explore timed out (120s)");
152
- // Non-zero exits carry upstream's agent-friendly guidance (e.g. the
153
- // "not initialized" message) pass it through verbatim.
154
- if (result.code !== 0) return textResult(outputOf(result));
155
- return textResult(result.stdout.trim());
156
- },
157
- });
158
-
159
- // ── /codegraph-init ────────────────────────────────────────────────────
160
- pi.registerCommand("codegraph-init", {
161
- description: "Build the CodeGraph index for the current project (codegraph init)",
162
- handler: async (args, ctx) => {
163
- if (!(await ensureCli(pi))) {
164
- if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "error");
165
- return;
166
- }
167
- const target = args?.trim() || ctx.cwd;
168
- if (ctx.hasUI) {
169
- ctx.ui.notify(`Indexing ${target} — can take minutes on a large repo…`, "info");
170
- }
171
- updateStatusBar(ctx, "init");
172
- try {
173
- const result = await execCg(pi, ["init", target], { timeout: 1_800_000 });
174
- const out = outputOf(result);
175
- const success = result.code === 0;
176
- if (ctx.hasUI) {
177
- ctx.ui.notify(
178
- success ? "CodeGraph index built." : `codegraph init failed (exit ${result.code})`,
179
- success ? "info" : "error",
180
- );
181
- }
182
- pi.sendMessage(
183
- { customType: "codegraph-init", content: out, display: true },
184
- { triggerTurn: false },
185
- );
186
- } finally {
187
- updateStatusBar(ctx, await isIndexed(ctx.cwd));
188
- }
189
- },
190
- });
191
-
192
- // ── /codegraph-sync ────────────────────────────────────────────────────
193
- pi.registerCommand("codegraph-sync", {
194
- description: "Sync CodeGraph changes since last index (codegraph sync)",
195
- handler: async (args, ctx) => {
196
- if (!(await ensureCli(pi))) {
197
- if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "error");
198
- return;
199
- }
200
- const target = args?.trim() || ctx.cwd;
201
- if (ctx.hasUI) {
202
- ctx.ui.notify(`Syncing CodeGraph for ${target}…`, "info");
203
- }
204
- updateStatusBar(ctx, "sync");
205
- try {
206
- const result = await execCg(pi, ["sync", target], { timeout: 300_000 });
207
- const out = outputOf(result);
208
- if (ctx.hasUI) {
209
- ctx.ui.notify(out, result.code === 0 ? "info" : "warning");
210
- }
211
- pi.sendMessage(
212
- { customType: "codegraph-sync", content: out, display: true },
213
- { triggerTurn: false },
214
- );
215
- } finally {
216
- updateStatusBar(ctx, await isIndexed(ctx.cwd));
217
- }
218
- },
219
- });
220
-
221
- // ── /codegraph-status ──────────────────────────────────────────────────
222
- pi.registerCommand("codegraph-status", {
223
- description: "Show CodeGraph index status and statistics",
224
- handler: async (args, ctx) => {
225
- if (!(await ensureCli(pi))) {
226
- if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "error");
227
- return;
228
- }
229
- const target = args?.trim() || ctx.cwd;
230
- const result = await execCg(pi, ["status", target], { timeout: 30_000 });
231
- const out = outputOf(result);
232
- if (ctx.hasUI) ctx.ui.notify(out, result.code === 0 ? "info" : "warning");
233
- updateStatusBar(ctx, await isIndexed(ctx.cwd));
234
- pi.sendMessage(
235
- { customType: "codegraph-status", content: out, display: true },
236
- { triggerTurn: false },
237
- );
238
- },
239
- });
240
-
241
- // ── /codegraph-unlock ──────────────────────────────────────────────────
242
- pi.registerCommand("codegraph-unlock", {
243
- description: "Release stale CodeGraph database lock (codegraph unlock)",
244
- handler: async (args, ctx) => {
245
- if (!(await ensureCli(pi))) {
246
- if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "error");
247
- return;
248
- }
249
- const target = args?.trim() || ctx.cwd;
250
- const result = await execCg(pi, ["unlock", target], { timeout: 10_000 });
251
- const out = outputOf(result);
252
- if (ctx.hasUI) ctx.ui.notify(out, result.code === 0 ? "info" : "warning");
253
- pi.sendMessage(
254
- { customType: "codegraph-unlock", content: out, display: true },
255
- { triggerTurn: false },
256
- );
257
- },
258
- });
259
-
260
- // ── resources_discover: contribute the skill only when the CLI exists ──
261
- pi.on("resources_discover", async () => {
262
- return {
263
- skillPaths: [fileURLToPath(new URL("../skills/codegraph/SKILL.md", import.meta.url))],
264
- };
265
- });
266
-
267
- // ── session_start: incremental sync + context hint ────────────────────
268
- pi.on("session_start", async (event, ctx) => {
269
- const indexed = await isIndexed(ctx.cwd);
270
- if (indexed) {
271
- updateStatusBar(ctx, "sync");
272
- // Incremental sync; near-zero cost when nothing changed.
273
- void execCg(pi, ["sync", "-q", ctx.cwd], { timeout: 300_000 })
274
- .catch(() => {})
275
- .finally(async () => {
276
- updateStatusBar(ctx, await isIndexed(ctx.cwd));
277
- });
278
- } else {
279
- updateStatusBar(ctx, false);
280
- }
281
-
282
- // Inject the agent playbook once per process when the project IS
283
- // indexed (upstream does this via MCP initialize instructions). Skip
284
- // "reload": extensions rebind in place and the message would duplicate.
285
- if (event.reason !== "reload" && indexed) {
286
- pi.sendMessage(
287
- { customType: "codegraph-context", content: INDEX_HINT, display: false },
288
- { triggerTurn: false },
289
- );
290
- }
291
- });
292
-
293
- pi.on("session_shutdown", async (_event, ctx) => {
294
- if (ctx.hasUI) {
295
- ctx.ui.setStatus("codegraph", undefined);
296
- }
297
- });
298
- }
1
+ // pi-codegraph — pi extension
2
+ //
3
+ // CodeGraph support for pi: codegraph_explore tool, /codegraph-init,
4
+ // /codegraph-sync, /codegraph-status, and /codegraph-unlock commands.
5
+ //
6
+ // Bridges to CodeGraph by executing the CLI (see docs/adr/0001). Requires the
7
+ // codegraph CLI on PATH: npm i -g @colbymchenry/codegraph
8
+ // Upstream: https://github.com/colbymchenry/codegraph
9
+
10
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
+ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
12
+ import { TSchema, Type } from "typebox";
13
+ import * as fs from "node:fs/promises";
14
+ import * as path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const INSTALL_HINT =
18
+ "未检测到 codegraph CLI(PATH 中无 codegraph 命令),本插件未注入任何工具、命令或技能。" +
19
+ "安装:npm i -g @colbymchenry/codegraph,然后 /reload 或重启 pi 生效。";
20
+
21
+ // Whole-process dedup for the missing-CLI hint: extensions re-run per session and
22
+ // per subagent (separate jiti module copies), so gate on globalThis.
23
+ const MISSING_CLI_HINTED = Symbol.for("pi-codegraph.missing-cli-hinted");
24
+
25
+ // Result of `codegraph version` this session — null until first check.
26
+ let cliAvailable: boolean | null = null;
27
+
28
+ async function execCg(
29
+ pi: ExtensionAPI,
30
+ args: string[],
31
+ options: { signal?: AbortSignal; timeout?: number; cwd?: string } = {},
32
+ ) {
33
+ if (process.platform === "win32") {
34
+ return await pi.exec("cmd.exe", ["/d", "/s", "/c", "codegraph", ...args], options);
35
+ }
36
+ return await pi.exec("codegraph", args, options);
37
+ }
38
+
39
+ async function ensureCli(pi: ExtensionAPI): Promise<boolean> {
40
+ if (cliAvailable !== null) return cliAvailable;
41
+ try {
42
+ const result = await execCg(pi, ["version"], { timeout: 10_000 });
43
+ cliAvailable = result.code === 0;
44
+ } catch {
45
+ cliAvailable = false;
46
+ }
47
+ return cliAvailable;
48
+ }
49
+
50
+ function textResult(text: string) {
51
+ return { content: [{ type: "text" as const, text }], details: undefined };
52
+ }
53
+
54
+ function outputOf(result: { stdout: string; stderr: string; code: number }): string {
55
+ return (result.stdout + result.stderr).trim() || `exit ${result.code}`;
56
+ }
57
+
58
+ async function isIndexed(cwd: string): Promise<boolean> {
59
+ try {
60
+ await fs.access(path.join(cwd, ".codegraph", "codegraph.db"));
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ type StatusState = "index" | "sync" | "init" | boolean | undefined;
68
+
69
+ function updateStatusBar(ctx: ExtensionContext, state: StatusState) {
70
+ if (!ctx.hasUI) return;
71
+ const label = state === true ? "index" : state === false ? undefined : state;
72
+ if (label) {
73
+ const text = ctx.ui.theme?.fg
74
+ ? `${ctx.ui.theme.fg("accent", "⬡")} ${label}`
75
+ : `⬡ ${label}`;
76
+ ctx.ui.setStatus("codegraph", text);
77
+ } else {
78
+ ctx.ui.setStatus("codegraph", undefined);
79
+ }
80
+ }
81
+
82
+ // 仿照上游 MCP SERVER_INSTRUCTIONS(src/mcp/server-instructions.ts)编写:引导智能体
83
+ // grep/read 之前优先使用 codegraph 工具,并给出反模式与过期处理。提示内容按实际
84
+ // 启用的工具集生成:未开启的工具绝不被宣传(见 docs/adr/0003),避免智能体调用
85
+ // 不存在的工具。
86
+ const EXPLORE_HINT_LINE =
87
+ "`codegraph_explore` 一次调用完成广泛探索:相关符号的源码 + 调用路径。" +
88
+ "可以点名端点符号(如 `mutateElement renderScene`)以跨越动态分派跳转揭示调用路径。";
89
+
90
+ const EXTRA_TOOL_HINT_LINES: Record<string, string> = {
91
+ codegraph_query: "`codegraph_query` 定位符号:位置 + 签名,不含源码。",
92
+ codegraph_node:
93
+ "`codegraph_node` 单个符号的源码 + 调用/被调轨迹(可链式追踪调用图)。",
94
+ codegraph_callers: "`codegraph_callers`谁调用了某符号。",
95
+ codegraph_callees: "`codegraph_callees` — 某符号调用了什么。",
96
+ codegraph_impact: "`codegraph_impact` 修改某符号的影响范围(编辑前调用)。",
97
+ codegraph_files: "`codegraph_files` 已建索引的文件树(tree/flat/grouped 按语言分组)。",
98
+ };
99
+
100
+ const HIDDEN_TOOLS_NOTE =
101
+ "细粒度工具(`codegraph_query` `codegraph_node` `codegraph_callers` `codegraph_callees` " +
102
+ "`codegraph_impact` `codegraph_files`)默认隐藏以保持工具列表精简,只有用户通过配置开启后才会注册" +
103
+ "(全局 `~/.pi/agent/extensions/pi-codegraph/config.json` 或项目 `.pi/extensions/pi-codegraph/config.json` 的 " +
104
+ "`extraTools`)——当前会话请勿调用它们。";
105
+
106
+ function buildIndexHint(enabled: Set<string>): string {
107
+ const lines = [
108
+ "# CodeGraph — 本项目已建立索引",
109
+ "",
110
+ "这里存在 `.codegraph/` 索引:项目每个符号、边、文件构成的 SQLite 知识图谱(支持 30+ 语言)。" +
111
+ "它可以回答结构性问题,并给出逐字、带行号的源码(把 codegraph 输出视为已经 Read 过的内容,可直接据此编辑)。",
112
+ "",
113
+ "- 对于结构性问题(X 如何工作 / X 在哪里 / 谁调用 Y / 修改 Z 会破坏什么),应使用 codegraph 工具" +
114
+ "而不是 grep + read——通常一次调用即可完整回答。",
115
+ `- ${EXPLORE_HINT_LINE}`,
116
+ ];
117
+ for (const name of Object.keys(EXTRA_TOOL_HINT_LINES)) {
118
+ if (enabled.has(name)) lines.push(`- ${EXTRA_TOOL_HINT_LINES[name]}`);
119
+ }
120
+ lines.push(
121
+ enabled.size === 0
122
+ ? `- ${HIDDEN_TOOLS_NOTE}`
123
+ : "- 以上细粒度工具已通过 `extraTools` 配置开启。",
124
+ );
125
+ lines.push(
126
+ "- 反模式:不要先用 grep 或 Read;不要用 grep 重复验证 codegraph 输出(基于 AST,比 grep 更准确);不要手工重建调用流程。",
127
+ '- "Already sent earlier in this conversation":该提示表示内容已在会话上下文中——不要重新获取或 Read。',
128
+ '- 过期提示:如果工具输出包含 "⚠️ Some files referenced below were edited since the last index sync",只直接读取其中被标记的文件。',
129
+ "- 多项目 / Monorepo:传入 `path` 查询任意已建索引的子项目目录。",
130
+ "- 项目没有 `.codegraph/` 时,在该项目使用内置工具;是否建索引由用户决定——必要时建议 /codegraph-init。",
131
+ );
132
+ return lines.join("\n");
133
+ }
134
+
135
+ // ── 配置:细粒度工具 opt-in(docs/adr/0003)───────────────────────────
136
+ // 全局配置在 ~/.pi/agent/extensions/pi-codegraph/config.json,项目配置在
137
+ // .pi/extensions/pi-codegraph/config.json(覆盖全局,仅对受信任项目生效)。
138
+ // 结构为扁平 JSON:{ "extraTools": ["node", "impact"] }。pi 没有第三方
139
+ // 扩展配置 API,故由扩展自行读取这两个文件。
140
+ type CgConfig = { extraTools?: string[] | "all" };
141
+
142
+ const CONFIG_REL_PATH = path.join("extensions", "pi-codegraph", "config.json");
143
+
144
+ async function readConfigFile<T>(file: string): Promise<T | undefined> {
145
+ try {
146
+ const raw = await fs.readFile(file, "utf8");
147
+ return JSON.parse(raw) as T;
148
+ } catch {
149
+ return undefined;
150
+ }
151
+ }
152
+
153
+ async function loadCgConfig(ctx: ExtensionContext): Promise<CgConfig> {
154
+ const global = await readConfigFile<CgConfig>(path.join(getAgentDir(), CONFIG_REL_PATH));
155
+ let project: CgConfig | undefined;
156
+ if (ctx.isProjectTrusted()) {
157
+ project = await readConfigFile<CgConfig>(path.join(ctx.cwd, CONFIG_DIR_NAME, CONFIG_REL_PATH));
158
+ }
159
+ return { ...(global ?? {}), ...(project ?? {}) };
160
+ }
161
+
162
+ // When the CLI is absent, register nothing but a one-shot hint handler: no
163
+ // tools, no commands, no resources_discover (so no skill injection), no sync.
164
+ export function registerMissingCliHint(pi: ExtensionAPI) {
165
+ pi.on("session_start", async (_event, ctx) => {
166
+ if ((globalThis as Record<symbol, boolean>)[MISSING_CLI_HINTED]) return;
167
+ (globalThis as Record<symbol, boolean>)[MISSING_CLI_HINTED] = true;
168
+ if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "warning");
169
+ });
170
+ }
171
+
172
+ export default async function codegraphExtension(pi: ExtensionAPI) {
173
+ if (!(await ensureCli(pi))) {
174
+ registerMissingCliHint(pi);
175
+ return;
176
+ }
177
+ // ── codegraph_explore tool ─────────────────────────────────────────────
178
+ pi.registerTool({
179
+ name: "codegraph_explore",
180
+ label: "CodeGraph Explore",
181
+ description:
182
+ "Broad code exploration in one shot: relevant symbols' verbatim line-numbered source, call paths between them, and a blast-radius summary.",
183
+ promptSnippet:
184
+ "codegraph_explore: symbol source + call paths in one shot from the project's CodeGraph index",
185
+ promptGuidelines: [
186
+ "For structural code questions (how does X work, where is X, what breaks if I change X), prefer codegraph_explore over grep when the project has a .codegraph/ index.",
187
+ "Indexing is the user's decision — never run codegraph init yourself; suggest /codegraph-init instead.",
188
+ ],
189
+ parameters: Type.Object({
190
+ query: Type.String({
191
+ description: "Symbol names or a natural-language question about the code",
192
+ }),
193
+ path: Type.Optional(
194
+ Type.String({
195
+ description: "Project path (default: cwd)",
196
+ }),
197
+ ),
198
+ maxFiles: Type.Optional(
199
+ Type.Integer({
200
+ description: "Maximum number of files to include source from",
201
+ }),
202
+ ),
203
+ }),
204
+ execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
205
+ if (!(await ensureCli(pi))) return textResult(INSTALL_HINT);
206
+ const cwd = params.path ?? ctx.cwd;
207
+ const args = ["explore", params.query, "-p", cwd];
208
+ if (typeof params.maxFiles === "number" && params.maxFiles > 0) {
209
+ args.push("--max-files", String(params.maxFiles));
210
+ }
211
+ const result = await execCg(pi, args, {
212
+ signal,
213
+ timeout: 120_000,
214
+ });
215
+ if (result.killed) return textResult("codegraph explore timed out (120s)");
216
+ // Non-zero exits carry upstream's agent-friendly guidance (e.g. the
217
+ // "not initialized" message) — pass it through verbatim.
218
+ if (result.code !== 0) return textResult(outputOf(result));
219
+ return textResult(result.stdout.trim());
220
+ },
221
+ });
222
+
223
+ // ── codegraph_* fine-grained tools (CLI parity with the MCP tools) ────
224
+ // Each maps 1:1 to a codegraph CLI subcommand; `path` selects the project,
225
+ // defaults to the session cwd (same convention as codegraph_explore).
226
+ const projectPath = () =>
227
+ Type.Optional(
228
+ Type.String({
229
+ description: "Project path (default: cwd)",
230
+ }),
231
+ );
232
+
233
+ type CliToolDef = {
234
+ name: string;
235
+ label: string;
236
+ description: string;
237
+ snippet: string;
238
+ guidelines: string[];
239
+ subcommand: string;
240
+ parameters: TSchema;
241
+ positional?: (p: Record<string, unknown>) => string[];
242
+ flags?: (p: Record<string, unknown>) => string[];
243
+ timeout?: number;
244
+ };
245
+
246
+ function registerCliTool(pi: ExtensionAPI, def: CliToolDef) {
247
+ pi.registerTool({
248
+ name: def.name,
249
+ label: def.label,
250
+ description: def.description,
251
+ promptSnippet: def.snippet,
252
+ promptGuidelines: def.guidelines,
253
+ parameters: def.parameters,
254
+ execute: async (_toolCallId, params: Record<string, unknown>, signal, _onUpdate, ctx) => {
255
+ const cwd = (params.path as string | undefined) ?? ctx.cwd;
256
+ const args = [
257
+ def.subcommand,
258
+ ...(def.positional?.(params) ?? []),
259
+ "-p",
260
+ cwd,
261
+ ...(def.flags?.(params) ?? []),
262
+ ];
263
+ const result = await execCg(pi, args, {
264
+ signal,
265
+ timeout: def.timeout ?? 60_000,
266
+ });
267
+ if (result.killed) return textResult(`codegraph ${def.subcommand} timed out`);
268
+ // Non-zero exits carry upstream's agent-friendly guidance — pass it through.
269
+ if (result.code !== 0) return textResult(outputOf(result));
270
+ return textResult(result.stdout.trim());
271
+ },
272
+ });
273
+ }
274
+
275
+ const cliTools: CliToolDef[] = [
276
+ {
277
+ name: "codegraph_query",
278
+ label: "CodeGraph Query",
279
+ description:
280
+ "Search symbols by name. Returns locations and signatures only (no source). Use to locate where a symbol is declared.",
281
+ snippet: "codegraph_query: symbol locations + signatures by name",
282
+ guidelines: [
283
+ "To locate where a symbol is declared (kind, file, line), use codegraph_query before grep.",
284
+ ],
285
+ subcommand: "query",
286
+ parameters: Type.Object({
287
+ search: Type.String({ description: "Symbol name or partial name to search" }),
288
+ kind: Type.Optional(
289
+ Type.String({
290
+ description:
291
+ "Filter by node kind: function, method, class, interface, type, variable, route, component",
292
+ }),
293
+ ),
294
+ limit: Type.Optional(Type.Integer({ description: "Maximum results (default 10)" })),
295
+ path: projectPath(),
296
+ }),
297
+ positional: (p) => [p.search as string],
298
+ flags: (p) => [
299
+ ...(p.kind ? ["--kind", p.kind as string] : []),
300
+ ...(typeof p.limit === "number" && p.limit > 0 ? ["--limit", String(p.limit)] : []),
301
+ ],
302
+ },
303
+ {
304
+ name: "codegraph_node",
305
+ label: "CodeGraph Node",
306
+ description:
307
+ "One symbol's source plus its caller/callee trail. Chain it to follow a call graph across files.",
308
+ snippet: "codegraph_node: a symbol's source + caller/callee trail",
309
+ guidelines: [
310
+ "To deep-dive one known symbol (verbatim source + who it calls / is called by), use codegraph_node.",
311
+ ],
312
+ subcommand: "node",
313
+ parameters: Type.Object({
314
+ name: Type.String({ description: "Symbol name to inspect" }),
315
+ path: projectPath(),
316
+ }),
317
+ positional: (p) => [p.name as string],
318
+ },
319
+ {
320
+ name: "codegraph_callers",
321
+ label: "CodeGraph Callers",
322
+ description: "Find all functions or methods that call a specific symbol.",
323
+ snippet: "codegraph_callers: who calls a symbol",
324
+ guidelines: [
325
+ "To find what calls a symbol (reverse dependencies), use codegraph_callers.",
326
+ ],
327
+ subcommand: "callers",
328
+ parameters: Type.Object({
329
+ symbol: Type.String({ description: "Symbol name whose callers to find" }),
330
+ limit: Type.Optional(Type.Integer({ description: "Maximum results (default 20)" })),
331
+ path: projectPath(),
332
+ }),
333
+ positional: (p) => [p.symbol as string],
334
+ flags: (p) => [
335
+ ...(typeof p.limit === "number" && p.limit > 0 ? ["--limit", String(p.limit)] : []),
336
+ ],
337
+ },
338
+ {
339
+ name: "codegraph_callees",
340
+ label: "CodeGraph Callees",
341
+ description: "Find all functions or methods that a specific symbol calls.",
342
+ snippet: "codegraph_callees: what a symbol calls",
343
+ guidelines: [
344
+ "To find what a symbol calls (its outgoing edges), use codegraph_callees.",
345
+ ],
346
+ subcommand: "callees",
347
+ parameters: Type.Object({
348
+ symbol: Type.String({ description: "Symbol name whose callees to find" }),
349
+ limit: Type.Optional(Type.Integer({ description: "Maximum results (default 20)" })),
350
+ path: projectPath(),
351
+ }),
352
+ positional: (p) => [p.symbol as string],
353
+ flags: (p) => [
354
+ ...(typeof p.limit === "number" && p.limit > 0 ? ["--limit", String(p.limit)] : []),
355
+ ],
356
+ },
357
+ {
358
+ name: "codegraph_impact",
359
+ label: "CodeGraph Impact",
360
+ description: "Analyze what code is affected by changing a symbol (blast radius).",
361
+ snippet: "codegraph_impact: blast radius of changing a symbol",
362
+ guidelines: [
363
+ "Before editing a symbol, use codegraph_impact to see what depends on it.",
364
+ ],
365
+ subcommand: "impact",
366
+ parameters: Type.Object({
367
+ symbol: Type.String({ description: "Symbol name to analyze impact of changing" }),
368
+ depth: Type.Optional(Type.Integer({ description: "Traversal depth (default 2)" })),
369
+ path: projectPath(),
370
+ }),
371
+ positional: (p) => [p.symbol as string],
372
+ flags: (p) => [
373
+ ...(typeof p.depth === "number" && p.depth > 0 ? ["--depth", String(p.depth)] : []),
374
+ ],
375
+ },
376
+ {
377
+ name: "codegraph_files",
378
+ label: "CodeGraph Files",
379
+ description:
380
+ "Show the indexed project's file structure (tree, flat, or grouped by language), with per-file symbol counts.",
381
+ snippet: "codegraph_files: indexed file tree with symbol counts",
382
+ guidelines: [
383
+ "To get a structured view of the project files, use codegraph_files.",
384
+ ],
385
+ subcommand: "files",
386
+ parameters: Type.Object({
387
+ dir: Type.Optional(Type.String({ description: "Subdirectory within the project to show" })),
388
+ pattern: Type.Optional(Type.String({ description: "Glob pattern to filter files" })),
389
+ format: Type.Optional(
390
+ Type.Union([Type.Literal("tree"), Type.Literal("flat"), Type.Literal("grouped")]),
391
+ ),
392
+ maxDepth: Type.Optional(Type.Integer({ description: "Maximum directory depth for tree format" })),
393
+ path: projectPath(),
394
+ }),
395
+ positional: (p) => (p.dir ? [p.dir as string] : []),
396
+ flags: (p) => [
397
+ ...(p.pattern ? ["--pattern", p.pattern as string] : []),
398
+ ...(p.format ? ["--format", p.format as string] : []),
399
+ ...(typeof p.maxDepth === "number" && p.maxDepth > 0
400
+ ? ["--max-depth", String(p.maxDepth)]
401
+ : []),
402
+ ],
403
+ },
404
+ ];
405
+
406
+ // 细粒度工具为 opt-in:仅当配置开启时才注册,保证默认工具列表精简(见
407
+ // docs/adr/0003)。`registeredExtraTools` 防止同一进程内重复注册;
408
+ // `enabledExtraTools` 同时驱动活动工具集与注入的提示内容。
409
+ const registeredExtraTools = new Set<string>();
410
+ let enabledExtraTools = new Set<string>();
411
+
412
+ const extraToolKey = (def: CliToolDef) => def.name.replace(/^codegraph_/, "");
413
+
414
+ function resolveExtraTools(cfg: CgConfig, ctx: ExtensionContext): Set<string> {
415
+ const enabled = new Set<string>();
416
+ const wanted = cfg.extraTools ?? [];
417
+ const names = wanted === "all" ? cliTools.map((d) => d.name) : wanted;
418
+ for (const item of names) {
419
+ const key = typeof item === "string" ? item.trim() : "";
420
+ const def = cliTools.find((d) => d.name === key || extraToolKey(d) === key);
421
+ if (def) {
422
+ enabled.add(def.name);
423
+ } else if (key && ctx.hasUI) {
424
+ ctx.ui.notify(`codegraph: unknown extra tool "${item}" (ignored)`, "warning");
425
+ }
426
+ }
427
+ return enabled;
428
+ }
429
+
430
+ function applyExtraTools(pi: ExtensionAPI, cfg: CgConfig, ctx: ExtensionContext) {
431
+ enabledExtraTools = resolveExtraTools(cfg, ctx);
432
+ for (const def of cliTools) {
433
+ if (enabledExtraTools.has(def.name) && !registeredExtraTools.has(def.name)) {
434
+ registerCliTool(pi, def);
435
+ registeredExtraTools.add(def.name);
436
+ }
437
+ }
438
+ // 将活动工具集与配置对齐:此前已注册但当前未启用的工具(例如切换项目后)
439
+ // 不得再出现在系统提示中。
440
+ const active = new Set(pi.getActiveTools());
441
+ const extraNames = new Set(cliTools.map((d) => d.name));
442
+ let changed = false;
443
+ for (const name of extraNames) {
444
+ if (enabledExtraTools.has(name) && !active.has(name)) {
445
+ active.add(name);
446
+ changed = true;
447
+ } else if (!enabledExtraTools.has(name) && active.has(name)) {
448
+ active.delete(name);
449
+ changed = true;
450
+ }
451
+ }
452
+ if (changed) pi.setActiveTools([...active]);
453
+ }
454
+
455
+ // ── /codegraph-init ────────────────────────────────────────────────────
456
+ pi.registerCommand("codegraph-init", {
457
+ description: "Build the CodeGraph index for the current project (codegraph init)",
458
+ handler: async (args, ctx) => {
459
+ if (!(await ensureCli(pi))) {
460
+ if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "error");
461
+ return;
462
+ }
463
+ const target = args?.trim() || ctx.cwd;
464
+ if (ctx.hasUI) {
465
+ ctx.ui.notify(`Indexing ${target} — can take minutes on a large repo…`, "info");
466
+ }
467
+ updateStatusBar(ctx, "init");
468
+ try {
469
+ const result = await execCg(pi, ["init", target], { timeout: 1_800_000 });
470
+ const out = outputOf(result);
471
+ const success = result.code === 0;
472
+ if (ctx.hasUI) {
473
+ ctx.ui.notify(
474
+ success ? "CodeGraph index built." : `codegraph init failed (exit ${result.code})`,
475
+ success ? "info" : "error",
476
+ );
477
+ }
478
+ pi.sendMessage(
479
+ { customType: "codegraph-init", content: out, display: true },
480
+ { triggerTurn: false },
481
+ );
482
+ } finally {
483
+ updateStatusBar(ctx, await isIndexed(ctx.cwd));
484
+ }
485
+ },
486
+ });
487
+
488
+ // ── /codegraph-sync ────────────────────────────────────────────────────
489
+ pi.registerCommand("codegraph-sync", {
490
+ description: "Sync CodeGraph changes since last index (codegraph sync)",
491
+ handler: async (args, ctx) => {
492
+ if (!(await ensureCli(pi))) {
493
+ if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "error");
494
+ return;
495
+ }
496
+ const target = args?.trim() || ctx.cwd;
497
+ if (ctx.hasUI) {
498
+ ctx.ui.notify(`Syncing CodeGraph for ${target}…`, "info");
499
+ }
500
+ updateStatusBar(ctx, "sync");
501
+ try {
502
+ const result = await execCg(pi, ["sync", target], { timeout: 300_000 });
503
+ const out = outputOf(result);
504
+ if (ctx.hasUI) {
505
+ ctx.ui.notify(out, result.code === 0 ? "info" : "warning");
506
+ }
507
+ pi.sendMessage(
508
+ { customType: "codegraph-sync", content: out, display: true },
509
+ { triggerTurn: false },
510
+ );
511
+ } finally {
512
+ updateStatusBar(ctx, await isIndexed(ctx.cwd));
513
+ }
514
+ },
515
+ });
516
+
517
+ // ── /codegraph-status ──────────────────────────────────────────────────
518
+ pi.registerCommand("codegraph-status", {
519
+ description: "Show CodeGraph index status and statistics",
520
+ handler: async (args, ctx) => {
521
+ if (!(await ensureCli(pi))) {
522
+ if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "error");
523
+ return;
524
+ }
525
+ const target = args?.trim() || ctx.cwd;
526
+ const result = await execCg(pi, ["status", target], { timeout: 30_000 });
527
+ const out = outputOf(result);
528
+ if (ctx.hasUI) ctx.ui.notify(out, result.code === 0 ? "info" : "warning");
529
+ updateStatusBar(ctx, await isIndexed(ctx.cwd));
530
+ pi.sendMessage(
531
+ { customType: "codegraph-status", content: out, display: true },
532
+ { triggerTurn: false },
533
+ );
534
+ },
535
+ });
536
+
537
+ // ── /codegraph-unlock ──────────────────────────────────────────────────
538
+ pi.registerCommand("codegraph-unlock", {
539
+ description: "Release stale CodeGraph database lock (codegraph unlock)",
540
+ handler: async (args, ctx) => {
541
+ if (!(await ensureCli(pi))) {
542
+ if (ctx.hasUI) ctx.ui.notify(INSTALL_HINT, "error");
543
+ return;
544
+ }
545
+ const target = args?.trim() || ctx.cwd;
546
+ const result = await execCg(pi, ["unlock", target], { timeout: 10_000 });
547
+ const out = outputOf(result);
548
+ if (ctx.hasUI) ctx.ui.notify(out, result.code === 0 ? "info" : "warning");
549
+ pi.sendMessage(
550
+ { customType: "codegraph-unlock", content: out, display: true },
551
+ { triggerTurn: false },
552
+ );
553
+ },
554
+ });
555
+
556
+ // ── resources_discover: contribute the skill only when the CLI exists ──
557
+ pi.on("resources_discover", async () => {
558
+ return {
559
+ skillPaths: [fileURLToPath(new URL("../skills/codegraph/SKILL.md", import.meta.url))],
560
+ };
561
+ });
562
+
563
+ // ── session_start:opt-in 工具 + 增量同步 + 上下文提示 ───────────────
564
+ pi.on("session_start", async (event, ctx) => {
565
+ const cfg = await loadCgConfig(ctx);
566
+ applyExtraTools(pi, cfg, ctx);
567
+ const indexed = await isIndexed(ctx.cwd);
568
+ if (indexed) {
569
+ updateStatusBar(ctx, "sync");
570
+ // Incremental sync; near-zero cost when nothing changed.
571
+ void execCg(pi, ["sync", "-q", ctx.cwd], { timeout: 300_000 })
572
+ .catch(() => {})
573
+ .finally(async () => {
574
+ updateStatusBar(ctx, await isIndexed(ctx.cwd));
575
+ });
576
+ } else {
577
+ updateStatusBar(ctx, false);
578
+ }
579
+
580
+ // Inject the agent playbook once per process when the project IS
581
+ // indexed (upstream does this via MCP initialize instructions). Skip
582
+ // "reload": extensions rebind in place and the message would duplicate.
583
+ if (event.reason !== "reload" && indexed) {
584
+ pi.sendMessage(
585
+ { customType: "codegraph-context", content: buildIndexHint(enabledExtraTools), display: false },
586
+ { triggerTurn: false },
587
+ );
588
+ }
589
+ });
590
+
591
+ pi.on("session_shutdown", async (_event, ctx) => {
592
+ if (ctx.hasUI) {
593
+ ctx.ui.setStatus("codegraph", undefined);
594
+ }
595
+ });
596
+ }
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
- {
2
- "name": "@liuxincuit/pi-codegraph",
3
- "version": "0.1.1",
4
- "description": "CodeGraph support for pi — symbol source + call paths via the codegraph CLI",
5
- "type": "module",
6
- "keywords": [
7
- "pi-package",
8
- "codegraph",
9
- "code-intelligence",
10
- "knowledge-graph"
11
- ],
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/liuxincuit/pi-codegraph.git"
15
- },
16
- "author": "liuxincuit",
17
- "license": "MIT",
18
- "peerDependencies": {
19
- "@earendil-works/pi-coding-agent": "*"
20
- },
21
- "devDependencies": {
22
- "@earendil-works/pi-coding-agent": "*",
23
- "@types/node": "^22.0.0",
24
- "typebox": "^1.3.7",
25
- "typescript": "^5.5.0"
26
- },
27
- "scripts": {
28
- "typecheck": "tsc --noEmit",
29
- "smoke": "tsc extensions/codegraph.ts --outDir .smoke-build --module esnext --target es2022 --moduleResolution bundler --skipLibCheck --noEmit false && node smoke.mjs"
30
- },
31
- "pi": {
32
- "extensions": [
33
- "./extensions"
34
- ]
35
- },
36
- "files": [
37
- "extensions/",
38
- "skills/",
39
- "CONTEXT.md",
40
- "README.md",
41
- "LICENSE"
42
- ]
43
- }
1
+ {
2
+ "name": "@liuxincuit/pi-codegraph",
3
+ "version": "0.1.3",
4
+ "description": "CodeGraph support for pi — symbol source + call paths via the codegraph CLI",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "codegraph",
9
+ "code-intelligence",
10
+ "knowledge-graph"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/liuxincuit/pi-codegraph.git"
15
+ },
16
+ "author": "liuxincuit",
17
+ "license": "MIT",
18
+ "peerDependencies": {
19
+ "@earendil-works/pi-coding-agent": "*"
20
+ },
21
+ "devDependencies": {
22
+ "@earendil-works/pi-coding-agent": "*",
23
+ "@types/node": "^22.0.0",
24
+ "typebox": "^1.3.7",
25
+ "typescript": "^5.5.0"
26
+ },
27
+ "scripts": {
28
+ "typecheck": "tsc --noEmit",
29
+ "smoke": "tsc extensions/codegraph.ts --outDir .smoke-build --module esnext --target es2022 --moduleResolution bundler --skipLibCheck --noEmit false && node smoke.mjs"
30
+ },
31
+ "pi": {
32
+ "extensions": [
33
+ "./extensions"
34
+ ]
35
+ },
36
+ "files": [
37
+ "extensions/",
38
+ "skills/",
39
+ "CONTEXT.md",
40
+ "README.md",
41
+ "LICENSE"
42
+ ]
43
+ }
@@ -1,27 +1,43 @@
1
- ---
2
- name: codegraph
3
- description: Query the project's CodeGraph index (symbol source + call paths) via the codegraph_explore tool. Use when answering structural code questions — how X works, where X is, what a change affects — in a project that has a .codegraph/ index.
4
- ---
5
-
6
- # CodeGraph
7
-
8
- The `codegraph_explore` tool answers structural code questions in one shot: the relevant symbols' verbatim line-numbered source, the call paths between them, and a blast-radius summary. It reads the project's CodeGraph index (`.codegraph/`), built and maintained by the `codegraph` CLI.
9
-
10
- ## When to use
11
-
12
- - "How does X work?" / "Where is X?" / "What calls Y?" / "What breaks if I change Z?"
13
- - Before an edit, to map the symbols you are about to touch and inspect the blast radius.
14
- - Prefer it over grep/read for structural exploration **when the project has a `.codegraph/` index**.
15
-
16
- ## How to query
17
-
18
- - `query`: symbol names (`CodeGraph open`, `MCPSession`), endpoint flows (`mutateElement renderScene`), or a natural-language question. Naming a file or symbol returns its current line-numbered source.
19
- - `path`: optional project path. Defaults to the current working directory. In a monorepo, pass the sub-project directory that contains `.codegraph/`.
20
- - `maxFiles`: optional integer to limit how many file sources are returned.
21
-
22
- ## Anti-patterns & Guidance
23
-
24
- - **Trust AST results.** Don't re-verify codegraph output with grep.
25
- - **Already sent earlier in this conversation.** When this pointer appears, the lines are already in your session context — scroll back instead of re-fetching or reading the file.
26
- - **Staleness banner.** If output warns `⚠️ Some files referenced below were edited since the last index sync`, read only those specific files directly; other files in the response remain fresh.
27
- - **No index, no tool.** If the output says the project isn't indexed, stop calling `codegraph_explore` for that project this session and use built-in tools. Indexing is the user's decision — suggest the user run `/codegraph-init` if appropriate.
1
+ ---
2
+ name: codegraph
3
+ description: 通过 codegraph_explore 工具查询项目的 CodeGraph 索引(符号源码、调用路径、影响范围)。当回答结构化代码问题——X 如何工作、X 在哪里、谁调用 Y、修改某处会影响什么——且项目存在 .codegraph/ 索引时使用。
4
+ ---
5
+
6
+ # CodeGraph
7
+
8
+ `codegraph_explore` 工具从项目的 CodeGraph 索引(`.codegraph/`,由 `codegraph` CLI 建立与维护)回答结构化代码问题。**当项目存在 `.codegraph/` 索引时**,应优先使用它而不是 grep/read。
9
+
10
+ ## 默认工具
11
+
12
+ - `codegraph_explore` 一次调用完成广泛探索:相关符号的源码 + 调用路径。可以点名端点符号(`mutateElement renderScene`)以跨越动态分派跳转揭示调用路径。参数:`query`(符号名或自然语言问题)、可选 `maxFiles`(限制返回源码行数)。
13
+
14
+ 绝大多数结构化问题只需 `codegraph_explore` 即可解决。该工具始终注册。
15
+
16
+ ## 细粒度工具(可选开启)
17
+
18
+ 以下细粒度工具**默认隐藏**,以保持工具列表精简。它们只有在用户通过配置 `extraTools` 开启后才会注册;开启之前调用会报 "tool not found"。除非能在 `Available tools` 中看到它们,否则不要调用。
19
+
20
+ - `codegraph_query` 定位符号:位置 + 签名,不含源码。
21
+ - `codegraph_node` — 单个已知符号的逐字源码 + 调用/被调轨迹,可链式追踪调用图。
22
+ - `codegraph_callers` / `codegraph_callees` — 谁调用了某符号 / 某符号调用了什么。
23
+ - `codegraph_impact` — 修改某符号的影响范围;编辑前调用。
24
+ - `codegraph_files` 已建索引的文件树(tree/flat/grouped 按语言分组)与符号数量。
25
+
26
+ 开启方式:在 `~/.pi/agent/extensions/pi-codegraph/config.json`(全局)或 `.pi/extensions/pi-codegraph/config.json`(项目,覆盖全局)中配置:
27
+
28
+ ```json
29
+ { "extraTools": ["query", "node", "impact"] }
30
+ ```
31
+
32
+ 短名(`node`、`impact`)与完整工具名(`codegraph_node`)均可,`"extraTools": "all"` 开启全部。配置在下一个会话生效(`/reload` 或重启 pi)。如果反复需要某个隐藏工具,可以向用户建议此配置。
33
+
34
+ ## 如何查询
35
+
36
+ - `codegraph_explore` 接受 `path` 参数:要查询的项目目录,默认为当前工作目录。在 monorepo 中,传入包含 `.codegraph/` 的子项目目录。
37
+
38
+ ## 反模式与指导
39
+
40
+ - **信任 AST 结果。** 不要用 grep 重复验证 codegraph 输出。
41
+ - **Already sent earlier in this conversation。** 当出现该提示时,内容已在会话上下文中——回看上下文,不要重新获取或 Read。
42
+ - **过期提示。** 如果输出警告 `⚠️ Some files referenced below were edited since the last index sync`,只直接读取其中被标记的文件;其余内容仍然新鲜。
43
+ - **无索引不可用。** 如果输出提示项目未建索引,本会话内停止对该项目调用 `codegraph_explore`,改用内置工具。是否建索引由用户决定——适当时建议用户运行 `/codegraph-init`。