graph-indexer 2.1.1 → 3.0.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +312 -1
  2. package/README.md +236 -312
  3. package/SECURITY.md +53 -58
  4. package/bin/graph-indexer.mjs +251 -0
  5. package/integrations/claude-code/.claude-plugin/plugin.json +9 -0
  6. package/integrations/claude-code/.mcp.json +8 -0
  7. package/integrations/claude-code/agents/code-structure.md +22 -0
  8. package/integrations/claude-code/hooks/gi-hook.mjs +103 -0
  9. package/integrations/claude-code/hooks/hooks.json +16 -0
  10. package/integrations/opencode/graph-indexer.js +129 -0
  11. package/package.json +32 -109
  12. package/src/cli/helper.mjs +67 -0
  13. package/src/cli/hook.mjs +366 -0
  14. package/src/cli/init.mjs +272 -0
  15. package/src/cli/resident.mjs +185 -0
  16. package/src/index/discover.mjs +128 -0
  17. package/src/index/fingerprint.mjs +30 -0
  18. package/src/index/graph.mjs +61 -0
  19. package/src/index/indexer.mjs +476 -0
  20. package/src/index/modules.mjs +332 -0
  21. package/src/index/resolver.mjs +961 -0
  22. package/src/mcp/render.mjs +60 -0
  23. package/src/mcp/server.mjs +147 -0
  24. package/src/mcp/tools.mjs +974 -0
  25. package/src/parse/arity.mjs +153 -0
  26. package/src/parse/extract.mjs +1038 -0
  27. package/src/parse/lang/csharp.mjs +106 -0
  28. package/src/parse/lang/ecma.mjs +438 -0
  29. package/src/parse/lang/go.mjs +194 -0
  30. package/src/parse/lang/jvm.mjs +118 -0
  31. package/src/parse/lang/kotlin.mjs +97 -0
  32. package/src/parse/lang/misc.mjs +115 -0
  33. package/src/parse/lang/native.mjs +126 -0
  34. package/src/parse/lang/python.mjs +153 -0
  35. package/src/parse/lang/rust.mjs +157 -0
  36. package/src/parse/lang/scripting.mjs +219 -0
  37. package/src/parse/languages.mjs +48 -0
  38. package/src/parse/runtime.mjs +56 -0
  39. package/src/query/check.mjs +345 -0
  40. package/src/query/facts.mjs +323 -0
  41. package/src/query/intel.mjs +683 -0
  42. package/src/query/read.mjs +81 -0
  43. package/src/query/testcmd.mjs +130 -0
  44. package/src/query/textsearch.mjs +178 -0
  45. package/src/search/search.mjs +210 -0
  46. package/src/search/tokenize.mjs +188 -0
  47. package/src/store/db.mjs +169 -0
  48. package/src/util/git.mjs +30 -0
  49. package/src/util/paths.mjs +63 -0
  50. package/src/util/regex.mjs +2 -0
  51. package/vendor/grammars/bash.wasm.gz +0 -0
  52. package/vendor/grammars/c.wasm.gz +0 -0
  53. package/vendor/grammars/c_sharp.wasm.gz +0 -0
  54. package/vendor/grammars/cpp.wasm.gz +0 -0
  55. package/vendor/grammars/css.wasm.gz +0 -0
  56. package/vendor/grammars/go.wasm.gz +0 -0
  57. package/vendor/grammars/java.wasm.gz +0 -0
  58. package/vendor/grammars/javascript.wasm.gz +0 -0
  59. package/vendor/grammars/kotlin.wasm.gz +0 -0
  60. package/vendor/grammars/manifest.json +148 -0
  61. package/vendor/grammars/php.wasm.gz +0 -0
  62. package/vendor/grammars/python.wasm.gz +0 -0
  63. package/vendor/grammars/ruby.wasm.gz +0 -0
  64. package/vendor/grammars/rust.wasm.gz +0 -0
  65. package/vendor/grammars/scala.wasm.gz +0 -0
  66. package/vendor/grammars/tsx.wasm.gz +0 -0
  67. package/vendor/grammars/typescript.wasm.gz +0 -0
  68. package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
  69. package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  70. package/cli-ui.mjs +0 -78
  71. package/colbert.mjs +0 -168
  72. package/config.mjs +0 -494
  73. package/daemon-ctl.mjs +0 -175
  74. package/daemon-lock.mjs +0 -69
  75. package/embedders/python/mlx_embed_server.py +0 -61
  76. package/embedders/python/requirements-mlx.txt +0 -5
  77. package/embedders/setup-mlx.mjs +0 -110
  78. package/embeddings.mjs +0 -571
  79. package/engine/binary.mjs +0 -437
  80. package/engine/memory.mjs +0 -1003
  81. package/engine/sqlite.mjs +0 -1136
  82. package/enrichment.mjs +0 -521
  83. package/git-signals.mjs +0 -248
  84. package/indexer.mjs +0 -546
  85. package/init.mjs +0 -1809
  86. package/layout.mjs +0 -123
  87. package/mcp/centrality.mjs +0 -117
  88. package/mcp/format.mjs +0 -61
  89. package/mcp/resolver.mjs +0 -116
  90. package/mcp/symbolgraph.mjs +0 -137
  91. package/mcp/taint.mjs +0 -225
  92. package/mcp/tools.mjs +0 -1582
  93. package/mcp/topology.mjs +0 -676
  94. package/mcp-server.mjs +0 -197
  95. package/parse/extractor.mjs +0 -628
  96. package/parse/imports.mjs +0 -365
  97. package/parse/interprocedural.mjs +0 -134
  98. package/parse/languages.mjs +0 -253
  99. package/parse/metadata.mjs +0 -701
  100. package/parse/routes.mjs +0 -327
  101. package/parse/scip.mjs +0 -391
  102. package/parse/taint-patterns.mjs +0 -119
  103. package/prompts/CORE.md +0 -91
  104. package/prompts/DOMAIN_TEMPLATE.md +0 -46
  105. package/prompts/INTEGRATION.md +0 -123
  106. package/prompts/frameworks/ANDROID.md +0 -17
  107. package/prompts/frameworks/ASPNET_CORE.md +0 -19
  108. package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
  109. package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
  110. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
  111. package/prompts/frameworks/RAILS.md +0 -20
  112. package/prompts/frameworks/REACT.md +0 -20
  113. package/prompts/frameworks/SPRING_BOOT.md +0 -20
  114. package/prompts/languages/BASH.md +0 -22
  115. package/prompts/languages/C.md +0 -21
  116. package/prompts/languages/CSHARP.md +0 -24
  117. package/prompts/languages/CSS_SCSS.md +0 -22
  118. package/prompts/languages/GO.md +0 -24
  119. package/prompts/languages/JAVA.md +0 -23
  120. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
  121. package/prompts/languages/KOTLIN.md +0 -24
  122. package/prompts/languages/PHP.md +0 -23
  123. package/prompts/languages/PYTHON.md +0 -23
  124. package/prompts/languages/RUBY.md +0 -23
  125. package/prompts/languages/RUST.md +0 -21
  126. package/prompts/languages/SWIFT.md +0 -21
  127. package/seal.mjs +0 -400
  128. package/search-core.mjs +0 -777
  129. package/search-sparse.mjs +0 -183
  130. package/storage.mjs +0 -73
  131. package/watch-daemon.mjs +0 -289
@@ -0,0 +1,129 @@
1
+ /**
2
+ * graph-indexer plugin for OpenCode and Kilo Code (installed by `graph-indexer init --hooks`).
3
+ *
4
+ * After a read, a search or an edit it appends to the tool's output what the Claude Code hooks add
5
+ * as context: where the names used in the lines just read are defined (once the agent is
6
+ * crawling), where a definition that a search missed is, and what an edit broke. It asks the
7
+ * repository's graph-indexer resident process over its local socket (the MCP server `init`
8
+ * configures keeps one running) and adds nothing when none answers; the first miss starts
9
+ * `graph-indexer daemon` in the background for the next calls.
10
+ *
11
+ * Only the plugin function is exported: OpenCode calls every export of a plugin module.
12
+ */
13
+ import fs from 'node:fs';
14
+ import net from 'node:net';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+ import crypto from 'node:crypto';
18
+ import { spawn } from 'node:child_process';
19
+
20
+ const PROTOCOL = 1;
21
+ const WIN = process.platform === 'win32';
22
+ // how to start a resident when none answers (rewritten by `init --local` and installed binaries);
23
+ // --prefix keeps npx from running a graph-indexer that the repository ships in its own node_modules
24
+ const DAEMON = ['npx', '-y', '--prefix', os.homedir(), 'graph-indexer@3', 'daemon'];
25
+ // OpenCode tool ids → the tool names graph-indexer's hook understands
26
+ const TOOLS = { read: 'Read', grep: 'Grep', bash: 'Bash', edit: 'Edit', write: 'Write' };
27
+
28
+ function findRepoRoot(start) {
29
+ let dir = path.resolve(start);
30
+ for (;;) {
31
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
32
+ const parent = path.dirname(dir);
33
+ if (parent === dir) return path.resolve(start);
34
+ dir = parent;
35
+ }
36
+ }
37
+
38
+ // must match socketPath() in graph-indexer's src/cli/resident.mjs
39
+ function socketPath(root) {
40
+ const abs = path.resolve(root);
41
+ const hash = crypto.createHash('sha256').update(abs).digest('hex').slice(0, 16);
42
+ if (WIN) return `\\\\.\\pipe\\graph-indexer-${hash}`;
43
+ const inRepo = path.join(abs, '.graph-indexer', 'resident.sock');
44
+ if (Buffer.byteLength(inRepo) <= 100) return inRepo;
45
+ return path.join(os.tmpdir(), `graph-indexer-${process.getuid?.() ?? 'user'}`, `${hash}.sock`);
46
+ }
47
+
48
+ function trusted(file, root) {
49
+ if (WIN) return true;
50
+ const mine = (st) => !process.getuid || st.uid === process.getuid();
51
+ try {
52
+ const sock = fs.lstatSync(file), dir = fs.lstatSync(path.dirname(file));
53
+ const inRepo = path.dirname(file) === path.join(path.resolve(root), '.graph-indexer');
54
+ // the socket and its directory are ours, not links or listeners planted by the repository or another user
55
+ return sock.isSocket() && mine(sock) && dir.isDirectory() && mine(dir) && (inRepo || (dir.mode & 0o077) === 0);
56
+ } catch { return false; }
57
+ }
58
+
59
+ function ask(root, request, totalMs) {
60
+ const file = socketPath(root);
61
+ if (!trusted(file, root)) return Promise.resolve(null);
62
+ return new Promise((resolve) => {
63
+ let done = false, buf = '';
64
+ const sock = net.createConnection(file);
65
+ const finish = (v) => { if (!done) { done = true; clearTimeout(timer); sock.destroy(); resolve(v); } };
66
+ const timer = setTimeout(() => finish(null), totalMs);
67
+ sock.setEncoding('utf8');
68
+ sock.on('connect', () => sock.write(JSON.stringify({ v: PROTOCOL, any: true, ...request }) + '\n'));
69
+ sock.on('data', (d) => {
70
+ buf += d;
71
+ const i = buf.indexOf('\n');
72
+ if (i < 0) return;
73
+ let r = null;
74
+ try { r = JSON.parse(buf.slice(0, i)); } catch { /* garbled */ }
75
+ finish(r?.ok ? r : null);
76
+ });
77
+ sock.on('error', () => finish(null));
78
+ sock.on('close', () => finish(null));
79
+ });
80
+ }
81
+
82
+ /** The Claude Code-style PostToolUse payload for an OpenCode tool call, or null. */
83
+ function hookPayload(input, output, root) {
84
+ const name = TOOLS[input?.tool];
85
+ if (!name) return null;
86
+ const a = input.args ?? {};
87
+ let text = typeof output?.output === 'string' ? output.output : '';
88
+ if (name === 'Grep' && /^No files found/.test(text.trim())) text = ''; // a search that found nothing
89
+ const toolInput = name === 'Read' ? { file_path: a.filePath, offset: a.offset, limit: a.limit }
90
+ : name === 'Grep' ? { pattern: a.pattern, path: a.path }
91
+ : name === 'Bash' ? { command: a.command }
92
+ : { file_path: a.filePath };
93
+ return { session_id: `opencode-${input.sessionID ?? 'default'}`, cwd: root, hook_event_name: 'PostToolUse', tool_name: name, tool_input: toolInput, tool_response: text };
94
+ }
95
+
96
+ export const GraphIndexer = async ({ directory, worktree } = {}) => {
97
+ // Kilo Code may load the plugin from both .kilo/ and .opencode/: register once
98
+ if (globalThis.__graphIndexerPlugin) return {};
99
+ globalThis.__graphIndexerPlugin = true;
100
+ const root = findRepoRoot(worktree || directory || process.cwd());
101
+ let started = false;
102
+ return {
103
+ 'tool.execute.after': async (input, output) => {
104
+ try {
105
+ const payload = hookPayload(input, output, root);
106
+ if (!payload) return;
107
+ const reply = await ask(root, { op: 'hook', event: 'post-tool', input: payload }, 2000);
108
+ if (!reply) {
109
+ if (!started) {
110
+ started = true;
111
+ // cmd.exe (npx.cmd needs a shell on Windows) would run `&`, `|`… in a path: quote
112
+ // every argument, and give up on one that a quote cannot hold
113
+ const args = [...DAEMON.slice(1), '--repo', root];
114
+ if (WIN && args.some(a => /["%\r\n]/.test(a))) return;
115
+ const child = spawn(DAEMON[0], WIN ? args.map(a => `"${a}"`) : args, { detached: true, stdio: 'ignore', windowsHide: true, shell: WIN });
116
+ child.on('error', () => { /* optional */ });
117
+ child.unref();
118
+ }
119
+ return;
120
+ }
121
+ const text = reply.out ? JSON.parse(reply.out)?.hookSpecificOutput?.additionalContext : null;
122
+ if (text) output.output = `${output.output ?? ''}\n\n${text}`;
123
+ } catch { /* fail open: the tool's own output is untouched */ }
124
+ },
125
+ };
126
+ };
127
+
128
+ // for tests only; a property, not an export, so OpenCode does not call it as a plugin
129
+ GraphIndexer.hookPayload = hookPayload;
package/package.json CHANGED
@@ -1,126 +1,56 @@
1
1
  {
2
2
  "name": "graph-indexer",
3
- "version": "2.1.1",
4
- "description": "Air-gapped code-retrieval MCP server: AST chunking, hybrid RRF search and cross-language call graph, with an optional zero-dependency SQLite backend for monorepo scale and optional local-LLM semantic enrichment.",
3
+ "version": "3.0.1",
4
+ "description": "Live code graph and search for AI coding agents over MCP: tree-sitter (WASM) symbols, scope/import/type-aware references, call graphs, change impact and hybrid search — local, zero-dependency, no native builds.",
5
5
  "type": "module",
6
- "main": "engine/memory.mjs",
6
+ "main": "src/query/intel.mjs",
7
7
  "exports": {
8
- ".": "./engine/memory.mjs"
8
+ ".": "./src/query/intel.mjs",
9
+ "./mcp": "./src/mcp/server.mjs"
9
10
  },
10
11
  "bin": {
11
- "graph-indexer": "init.mjs",
12
- "idx-index": "indexer.mjs",
13
- "idx-watch": "watch-daemon.mjs",
14
- "idx-daemon": "daemon-ctl.mjs",
15
- "idx-mcp": "mcp-server.mjs"
12
+ "graph-indexer": "bin/graph-indexer.mjs",
13
+ "idx-mcp": "bin/graph-indexer.mjs",
14
+ "idx-index": "bin/graph-indexer.mjs"
16
15
  },
17
16
  "files": [
18
- "*.mjs",
19
- "engine/",
20
- "mcp/",
21
- "parse/",
22
- "embedders/setup-mlx.mjs",
23
- "embedders/python/mlx_embed_server.py",
24
- "embedders/python/requirements-mlx.txt",
25
- "!embedders/python/test_servers.mjs",
26
- "prompts/",
17
+ "bin/",
18
+ "src/",
19
+ "integrations/",
20
+ "vendor/",
27
21
  "README.md",
28
22
  "CHANGELOG.md",
29
23
  "SECURITY.md",
30
24
  "TRADEMARK.md",
31
25
  "LICENSE"
32
26
  ],
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/MaquinaTech/graph-indexer.git"
36
- },
37
27
  "scripts": {
38
- "start": "node ./mcp-server.mjs",
39
- "test": "node test/run.mjs",
40
- "test:unit": "node test/unit.mjs",
41
- "test:sqlite": "node test/sqlite.mjs",
42
- "test:enrich": "node test/enrichment.mjs",
43
- "test:mcp": "node test/mcp.mjs",
44
- "test:scale": "node test/scale.mjs",
45
- "test:callgraph": "node test/callgraph.mjs",
46
- "test:edges": "node test/edges.mjs",
47
- "test:references": "node test/references.mjs",
48
- "test:routes": "node test/routes.mjs",
49
- "test:jsonout": "node test/json-output.mjs",
50
- "test:gitsignals": "node test/git-signals.mjs",
51
- "test:security": "node test/security.mjs",
52
- "test:seal": "node test/seal.mjs",
53
- "test:taint": "node test/taint.mjs",
54
- "test:scip": "node test/scip.mjs",
55
- "test:ranker": "node test/ranker.mjs",
56
- "test:sparse": "node test/sparse.mjs",
57
- "test:colbert": "node test/colbert.mjs",
58
- "test:embed": "node test/embeddings.mjs",
59
- "test:languages": "node test/languages.mjs",
60
- "test:all": "node test/unit.mjs && node test/sqlite.mjs && node test/enrichment.mjs && node test/mcp.mjs && node test/scale.mjs && node test/callgraph.mjs && node test/edges.mjs && node test/references.mjs && node test/routes.mjs && node test/json-output.mjs && node test/git-signals.mjs && node test/security.mjs && node test/seal.mjs && node test/taint.mjs && node test/scip.mjs && node test/ranker.mjs && node test/sparse.mjs && node test/colbert.mjs && node test/embeddings.mjs && node test/languages.mjs",
61
- "test:eval": "node test/evaluate.mjs",
62
- "test:eval:embeddings": "OLLAMA_HOST=http://localhost:11434 node test/evaluate.mjs --embeddings",
63
- "test:setup": "node test/setup.mjs",
64
- "test:run": "node test/run.mjs",
65
- "test:embeddings": "OLLAMA_HOST=http://localhost:11434 node test/run-embeddings.mjs",
66
- "test:embeddings:skip": "OLLAMA_HOST=http://localhost:11434 node test/run-embeddings.mjs --skip-indexing",
67
- "test:suite": "node test/run.mjs --suite",
68
- "test:json": "node test/run.mjs --json",
69
- "test:reset": "rm -rf test/fixtures && node test/setup.mjs",
70
- "embed:setup:mlx": "node embedders/setup-mlx.mjs",
71
- "embed:setup:local": "npm install @huggingface/transformers",
72
- "mcp:index": "idx-index --repo .",
73
- "mcp:index:sqlite": "node indexer.mjs --repo . --use-sqlite",
74
- "mcp:index:enriched": "node indexer.mjs --repo . --enrichment",
75
- "mcp:watch": "node watch-daemon.mjs",
76
- "mcp:start": "idx-mcp",
77
- "mcp:daemon": "node daemon-ctl.mjs status",
78
- "mcp:daemon:start": "node daemon-ctl.mjs start",
79
- "mcp:daemon:stop": "node daemon-ctl.mjs stop",
80
- "mcp:daemon:restart": "node daemon-ctl.mjs restart",
81
- "mcp:daemon:status": "node daemon-ctl.mjs status",
82
- "mcp:daemon:logs": "node daemon-ctl.mjs logs"
28
+ "test": "node --test test/*.test.mjs",
29
+ "bench": "node bench/eval-search.mjs",
30
+ "bench:graph": "node bench/eval-graph.mjs",
31
+ "bench:localize": "node bench/eval-localize.mjs",
32
+ "fixtures": "node bench/fixtures.mjs",
33
+ "start": "node bin/graph-indexer.mjs serve",
34
+ "index": "node bin/graph-indexer.mjs index"
83
35
  },
84
- "dependencies": {
85
- "@modelcontextprotocol/sdk": "1.29.0",
86
- "chokidar": "5.0.0",
87
- "ignore": "5.3.2",
88
- "tree-sitter": "0.21.1",
89
- "zod": "^3.25.76"
90
- },
91
- "devDependencies": {
92
- "@huggingface/transformers": "3.8.1",
93
- "hnswlib-node": "3.0.0",
94
- "tree-sitter-bash": "0.23.3",
95
- "tree-sitter-c": "0.21.4",
96
- "tree-sitter-c-sharp": "0.21.3",
97
- "tree-sitter-css": "0.21.1",
98
- "tree-sitter-go": "0.21.0",
99
- "tree-sitter-java": "0.21.0",
100
- "tree-sitter-javascript": "0.21.4",
101
- "tree-sitter-kotlin": "0.3.8",
102
- "tree-sitter-php": "0.23.1",
103
- "tree-sitter-python": "0.21.0",
104
- "tree-sitter-ruby": "0.23.1",
105
- "tree-sitter-rust": "0.21.0",
106
- "tree-sitter-scss": "1.0.0",
107
- "tree-sitter-swift": "0.5.0",
108
- "tree-sitter-typescript": "0.21.2"
36
+ "engines": {
37
+ "node": "^22.23.4 || ^24.22.1 || >=26.12.1"
109
38
  },
110
- "overrides": {
111
- "sharp": "^0.35.4"
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/MaquinaTech/graph-indexer.git"
112
42
  },
113
43
  "keywords": [
114
44
  "mcp",
115
45
  "model-context-protocol",
116
- "indexer",
117
- "ast",
118
- "tree-sitter",
119
- "rag",
120
- "hybrid-search",
121
- "rrf",
46
+ "code-search",
122
47
  "call-graph",
123
- "ollama"
48
+ "tree-sitter",
49
+ "code-intelligence",
50
+ "ai-agents",
51
+ "claude-code",
52
+ "find-references",
53
+ "impact-analysis"
124
54
  ],
125
55
  "author": {
126
56
  "name": "MaquinaTech",
@@ -128,18 +58,11 @@
128
58
  "email": "nicolopezdelerma@gmail.com"
129
59
  },
130
60
  "license": "MIT",
131
- "engines": {
132
- "node": ">=18.0.0"
133
- },
134
61
  "bugs": {
135
62
  "url": "https://github.com/MaquinaTech/graph-indexer/issues"
136
63
  },
137
64
  "homepage": "https://github.com/MaquinaTech/graph-indexer",
138
- "funding": {
139
- "type": "github",
140
- "url": "https://github.com/sponsors/MaquinaTech"
141
- },
142
65
  "publishConfig": {
143
66
  "access": "public"
144
67
  }
145
- }
68
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The structural helper: a sub-agent on a small model that answers structural questions — call
3
+ * sites, callers of callers, subclasses and implementations, what a change affects — from the
4
+ * index, and replies with the answer alone. The main agent delegates the question and gets back a
5
+ * path:line list instead of the searches and reads behind it, and the lookup runs on a model that
6
+ * costs a fraction per token. `init` writes it for Claude Code (.claude/agents/) and the Claude Code
7
+ * plugin ships the same file (integrations/claude-code/agents/).
8
+ */
9
+
10
+ export const HELPER_NAME = 'code-structure';
11
+
12
+ // what the main agent sees when it decides whether to delegate
13
+ const DESCRIPTION = 'Exact answers to structural questions about this repository\'s code, from graph-indexer\'s index, on a small fast model: every call site of a function or method (told apart from same-name methods of other classes), callers of callers, classes that extend or implement a type, and what a change would affect with the tests that cover it. Give it the symbol (Class.method, with its file when you know it) and the scope (directories, whether tests count, how many levels); it replies with path:line lists and says whether they are complete. Delegate these instead of grepping for names and reading files yourself.';
14
+
15
+ /** How to answer each kind of question, with the MCP tools or, given its path, the CLI. */
16
+ function howTo(cli) {
17
+ if (!cli) return [
18
+ '- Call sites or other uses of a function, method or class: `find_references` on it (`Class.method`, or `path:Name` when the name is ambiguous), with kind `call` for calls only, `include_tests: false` when tests are excluded and `path` for one directory. It says when the list is complete.',
19
+ '- Callers of callers: `call_graph` with direction `callers` and the depth asked for (two levels up is depth 2), with `include_tests: false` when tests are excluded.',
20
+ '- Classes that extend a class or implement an interface, directly or through other classes: `find_references` with kind `inherit`.',
21
+ '- What a change affects (call sites to update, overrides, subclasses that inherit it, tests to run): `change_impact` with the symbols.',
22
+ '- A definition or a signature: `read_code` with its name.',
23
+ ];
24
+ return [
25
+ `- Call sites or other uses of a function, method or class: \`${cli} refs <Class.method>\` (or \`path:Name\` when the name is ambiguous), with \`--kind call\` for calls only, \`--no-tests\` when tests are excluded and \`--path DIR\` for one directory. It says when the list is complete.`,
26
+ `- Callers of callers: \`${cli} callgraph <Class.method> --direction callers --depth N\` with the depth asked for (two levels up is 2), and \`--no-tests\` when tests are excluded.`,
27
+ `- Classes that extend a class or implement an interface, directly or through other classes: \`${cli} refs <Name> --kind inherit\`.`,
28
+ `- What a change affects (call sites to update, overrides, subclasses that inherit it, tests to run): \`${cli} impact --symbols A,B\`.`,
29
+ `- A definition or a signature: \`${cli} read <name>\`.`,
30
+ ];
31
+ }
32
+
33
+ /**
34
+ * The helper's instructions (its system prompt). `cli` is the path of the graph-indexer executable
35
+ * when the helper reaches the index through a shell instead of the MCP server.
36
+ */
37
+ export function helperInstructions({ cli = null } = {}) {
38
+ return [
39
+ `You answer structural questions about the code in this repository — where something is used, who calls it, what extends or implements it, what a change would affect — exactly and briefly, from graph-indexer's index${cli ? ` (run it through the shell with the executable \`${cli}\`)` : ' (the graph-indexer tools)'}. The index binds every reference through scopes, imports and receiver types, so methods that only share a name are kept apart, and it re-syncs with the files before every answer.`,
40
+ '',
41
+ 'How to answer',
42
+ ...howTo(cli),
43
+ '- Answer from these results. Read code or search text only to settle what they report as unresolved or incomplete (a call through a name built at run time, a receiver of unknown type), not to redo a list they give as complete.',
44
+ '- Keep to the scope asked: the directories, whether tests count, how many levels.',
45
+ '',
46
+ 'Reply',
47
+ '- When the request gives an answer format, follow it exactly.',
48
+ '- Otherwise one item per line as `path:LINE`, relative to the repository root, with a few words after it when they help (the enclosing function, the kind of use), then one line saying whether the list is complete or what could not be resolved.',
49
+ '- Nothing else: no account of how you found it. You do not change files.',
50
+ ].join('\n');
51
+ }
52
+
53
+ /**
54
+ * The Claude Code sub-agent file. It inherits the session's tools (so the graph-indexer MCP tools
55
+ * whatever the server is called: project .mcp.json or plugin) minus the editing ones.
56
+ */
57
+ export function claudeAgentFile() {
58
+ return `---
59
+ name: ${HELPER_NAME}
60
+ description: ${JSON.stringify(DESCRIPTION)}
61
+ model: haiku
62
+ disallowedTools: Edit, Write, MultiEdit, NotebookEdit
63
+ omitClaudeMd: true
64
+ ---
65
+ ${helperInstructions()}
66
+ `;
67
+ }