graph-indexer 2.1.0 → 3.0.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.
Files changed (129) hide show
  1. package/CHANGELOG.md +260 -1
  2. package/README.md +232 -312
  3. package/SECURITY.md +37 -59
  4. package/bin/graph-indexer.mjs +251 -0
  5. package/integrations/claude-code/.claude-plugin/plugin.json +8 -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 +99 -0
  9. package/integrations/claude-code/hooks/hooks.json +16 -0
  10. package/integrations/opencode/graph-indexer.js +124 -0
  11. package/package.json +31 -105
  12. package/src/cli/helper.mjs +67 -0
  13. package/src/cli/hook.mjs +356 -0
  14. package/src/cli/init.mjs +255 -0
  15. package/src/cli/resident.mjs +175 -0
  16. package/src/index/discover.mjs +130 -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 +331 -0
  21. package/src/index/resolver.mjs +954 -0
  22. package/src/mcp/render.mjs +60 -0
  23. package/src/mcp/server.mjs +147 -0
  24. package/src/mcp/tools.mjs +973 -0
  25. package/src/parse/arity.mjs +152 -0
  26. package/src/parse/extract.mjs +1031 -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 +343 -0
  40. package/src/query/facts.mjs +323 -0
  41. package/src/query/intel.mjs +680 -0
  42. package/src/query/read.mjs +81 -0
  43. package/src/query/testcmd.mjs +119 -0
  44. package/src/query/textsearch.mjs +160 -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/paths.mjs +24 -0
  49. package/vendor/grammars/bash.wasm.gz +0 -0
  50. package/vendor/grammars/c.wasm.gz +0 -0
  51. package/vendor/grammars/c_sharp.wasm.gz +0 -0
  52. package/vendor/grammars/cpp.wasm.gz +0 -0
  53. package/vendor/grammars/css.wasm.gz +0 -0
  54. package/vendor/grammars/go.wasm.gz +0 -0
  55. package/vendor/grammars/java.wasm.gz +0 -0
  56. package/vendor/grammars/javascript.wasm.gz +0 -0
  57. package/vendor/grammars/kotlin.wasm.gz +0 -0
  58. package/vendor/grammars/manifest.json +148 -0
  59. package/vendor/grammars/php.wasm.gz +0 -0
  60. package/vendor/grammars/python.wasm.gz +0 -0
  61. package/vendor/grammars/ruby.wasm.gz +0 -0
  62. package/vendor/grammars/rust.wasm.gz +0 -0
  63. package/vendor/grammars/scala.wasm.gz +0 -0
  64. package/vendor/grammars/tsx.wasm.gz +0 -0
  65. package/vendor/grammars/typescript.wasm.gz +0 -0
  66. package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
  67. package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  68. package/cli-ui.mjs +0 -78
  69. package/colbert.mjs +0 -168
  70. package/config.mjs +0 -494
  71. package/daemon-ctl.mjs +0 -175
  72. package/daemon-lock.mjs +0 -69
  73. package/embedders/python/mlx_embed_server.py +0 -61
  74. package/embedders/python/requirements-mlx.txt +0 -5
  75. package/embedders/setup-mlx.mjs +0 -110
  76. package/embeddings.mjs +0 -571
  77. package/engine/binary.mjs +0 -437
  78. package/engine/memory.mjs +0 -1003
  79. package/engine/sqlite.mjs +0 -1136
  80. package/enrichment.mjs +0 -521
  81. package/git-signals.mjs +0 -248
  82. package/indexer.mjs +0 -546
  83. package/init.mjs +0 -1809
  84. package/layout.mjs +0 -123
  85. package/mcp/centrality.mjs +0 -117
  86. package/mcp/format.mjs +0 -61
  87. package/mcp/resolver.mjs +0 -116
  88. package/mcp/symbolgraph.mjs +0 -137
  89. package/mcp/taint.mjs +0 -225
  90. package/mcp/tools.mjs +0 -1582
  91. package/mcp/topology.mjs +0 -676
  92. package/mcp-server.mjs +0 -197
  93. package/parse/extractor.mjs +0 -628
  94. package/parse/imports.mjs +0 -365
  95. package/parse/interprocedural.mjs +0 -134
  96. package/parse/languages.mjs +0 -253
  97. package/parse/metadata.mjs +0 -701
  98. package/parse/routes.mjs +0 -327
  99. package/parse/scip.mjs +0 -391
  100. package/parse/taint-patterns.mjs +0 -119
  101. package/prompts/CORE.md +0 -91
  102. package/prompts/DOMAIN_TEMPLATE.md +0 -46
  103. package/prompts/INTEGRATION.md +0 -123
  104. package/prompts/frameworks/ANDROID.md +0 -17
  105. package/prompts/frameworks/ASPNET_CORE.md +0 -19
  106. package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
  107. package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
  108. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
  109. package/prompts/frameworks/RAILS.md +0 -20
  110. package/prompts/frameworks/REACT.md +0 -20
  111. package/prompts/frameworks/SPRING_BOOT.md +0 -20
  112. package/prompts/languages/BASH.md +0 -22
  113. package/prompts/languages/C.md +0 -21
  114. package/prompts/languages/CSHARP.md +0 -24
  115. package/prompts/languages/CSS_SCSS.md +0 -22
  116. package/prompts/languages/GO.md +0 -24
  117. package/prompts/languages/JAVA.md +0 -23
  118. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
  119. package/prompts/languages/KOTLIN.md +0 -24
  120. package/prompts/languages/PHP.md +0 -23
  121. package/prompts/languages/PYTHON.md +0 -23
  122. package/prompts/languages/RUBY.md +0 -23
  123. package/prompts/languages/RUST.md +0 -21
  124. package/prompts/languages/SWIFT.md +0 -21
  125. package/seal.mjs +0 -400
  126. package/search-core.mjs +0 -777
  127. package/search-sparse.mjs +0 -183
  128. package/storage.mjs +0 -73
  129. package/watch-daemon.mjs +0 -289
@@ -0,0 +1,124 @@
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
+ const DAEMON = ['npx', '-y', 'graph-indexer@3', 'daemon'];
24
+ // OpenCode tool ids → the tool names graph-indexer's hook understands
25
+ const TOOLS = { read: 'Read', grep: 'Grep', bash: 'Bash', edit: 'Edit', write: 'Write' };
26
+
27
+ function findRepoRoot(start) {
28
+ let dir = path.resolve(start);
29
+ for (;;) {
30
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
31
+ const parent = path.dirname(dir);
32
+ if (parent === dir) return path.resolve(start);
33
+ dir = parent;
34
+ }
35
+ }
36
+
37
+ // must match socketPath() in graph-indexer's src/cli/resident.mjs
38
+ function socketPath(root) {
39
+ const abs = path.resolve(root);
40
+ const hash = crypto.createHash('sha256').update(abs).digest('hex').slice(0, 16);
41
+ if (WIN) return `\\\\.\\pipe\\graph-indexer-${hash}`;
42
+ const inRepo = path.join(abs, '.graph-indexer', 'resident.sock');
43
+ if (Buffer.byteLength(inRepo) <= 100) return inRepo;
44
+ return path.join(os.tmpdir(), `graph-indexer-${process.getuid?.() ?? 'user'}`, `${hash}.sock`);
45
+ }
46
+
47
+ function trusted(file, root) {
48
+ if (WIN) return true;
49
+ if (!fs.existsSync(file)) return false;
50
+ const dir = path.dirname(file);
51
+ if (dir === path.join(path.resolve(root), '.graph-indexer')) return true;
52
+ try {
53
+ const st = fs.lstatSync(dir);
54
+ return st.isDirectory() && (!process.getuid || st.uid === process.getuid()) && (st.mode & 0o077) === 0;
55
+ } catch { return false; }
56
+ }
57
+
58
+ function ask(root, request, totalMs) {
59
+ const file = socketPath(root);
60
+ if (!trusted(file, root)) return Promise.resolve(null);
61
+ return new Promise((resolve) => {
62
+ let done = false, buf = '';
63
+ const sock = net.createConnection(file);
64
+ const finish = (v) => { if (!done) { done = true; clearTimeout(timer); sock.destroy(); resolve(v); } };
65
+ const timer = setTimeout(() => finish(null), totalMs);
66
+ sock.setEncoding('utf8');
67
+ sock.on('connect', () => sock.write(JSON.stringify({ v: PROTOCOL, any: true, ...request }) + '\n'));
68
+ sock.on('data', (d) => {
69
+ buf += d;
70
+ const i = buf.indexOf('\n');
71
+ if (i < 0) return;
72
+ let r = null;
73
+ try { r = JSON.parse(buf.slice(0, i)); } catch { /* garbled */ }
74
+ finish(r?.ok ? r : null);
75
+ });
76
+ sock.on('error', () => finish(null));
77
+ sock.on('close', () => finish(null));
78
+ });
79
+ }
80
+
81
+ /** The Claude Code-style PostToolUse payload for an OpenCode tool call, or null. */
82
+ function hookPayload(input, output, root) {
83
+ const name = TOOLS[input?.tool];
84
+ if (!name) return null;
85
+ const a = input.args ?? {};
86
+ let text = typeof output?.output === 'string' ? output.output : '';
87
+ if (name === 'Grep' && /^No files found/.test(text.trim())) text = ''; // a search that found nothing
88
+ const toolInput = name === 'Read' ? { file_path: a.filePath, offset: a.offset, limit: a.limit }
89
+ : name === 'Grep' ? { pattern: a.pattern, path: a.path }
90
+ : name === 'Bash' ? { command: a.command }
91
+ : { file_path: a.filePath };
92
+ return { session_id: `opencode-${input.sessionID ?? 'default'}`, cwd: root, hook_event_name: 'PostToolUse', tool_name: name, tool_input: toolInput, tool_response: text };
93
+ }
94
+
95
+ export const GraphIndexer = async ({ directory, worktree } = {}) => {
96
+ // Kilo Code may load the plugin from both .kilo/ and .opencode/: register once
97
+ if (globalThis.__graphIndexerPlugin) return {};
98
+ globalThis.__graphIndexerPlugin = true;
99
+ const root = findRepoRoot(worktree || directory || process.cwd());
100
+ let started = false;
101
+ return {
102
+ 'tool.execute.after': async (input, output) => {
103
+ try {
104
+ const payload = hookPayload(input, output, root);
105
+ if (!payload) return;
106
+ const reply = await ask(root, { op: 'hook', event: 'post-tool', input: payload }, 2000);
107
+ if (!reply) {
108
+ if (!started) {
109
+ started = true;
110
+ const child = spawn(DAEMON[0], [...DAEMON.slice(1), '--repo', root], { detached: true, stdio: 'ignore', windowsHide: true, shell: WIN });
111
+ child.on('error', () => { /* optional */ });
112
+ child.unref();
113
+ }
114
+ return;
115
+ }
116
+ const text = reply.out ? JSON.parse(reply.out)?.hookSpecificOutput?.additionalContext : null;
117
+ if (text) output.output = `${output.output ?? ''}\n\n${text}`;
118
+ } catch { /* fail open: the tool's own output is untouched */ }
119
+ },
120
+ };
121
+ };
122
+
123
+ // for tests only; a property, not an export, so OpenCode does not call it as a plugin
124
+ GraphIndexer.hookPayload = hookPayload;
package/package.json CHANGED
@@ -1,123 +1,56 @@
1
1
  {
2
2
  "name": "graph-indexer",
3
- "version": "2.1.0",
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.0",
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"
36
+ "engines": {
37
+ "node": ">=22.5.0"
90
38
  },
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"
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/MaquinaTech/graph-indexer.git"
109
42
  },
110
43
  "keywords": [
111
44
  "mcp",
112
45
  "model-context-protocol",
113
- "indexer",
114
- "ast",
115
- "tree-sitter",
116
- "rag",
117
- "hybrid-search",
118
- "rrf",
46
+ "code-search",
119
47
  "call-graph",
120
- "ollama"
48
+ "tree-sitter",
49
+ "code-intelligence",
50
+ "ai-agents",
51
+ "claude-code",
52
+ "find-references",
53
+ "impact-analysis"
121
54
  ],
122
55
  "author": {
123
56
  "name": "MaquinaTech",
@@ -125,17 +58,10 @@
125
58
  "email": "nicolopezdelerma@gmail.com"
126
59
  },
127
60
  "license": "MIT",
128
- "engines": {
129
- "node": ">=18.0.0"
130
- },
131
61
  "bugs": {
132
62
  "url": "https://github.com/MaquinaTech/graph-indexer/issues"
133
63
  },
134
64
  "homepage": "https://github.com/MaquinaTech/graph-indexer",
135
- "funding": {
136
- "type": "github",
137
- "url": "https://github.com/sponsors/MaquinaTech"
138
- },
139
65
  "publishConfig": {
140
66
  "access": "public"
141
67
  }
@@ -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
+ }