graph-indexer 3.0.1 → 3.1.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 (58) hide show
  1. package/CHANGELOG.md +456 -0
  2. package/README.md +61 -8
  3. package/SECURITY.md +35 -20
  4. package/bin/graph-indexer.mjs +46 -5
  5. package/integrations/claude-code/.claude-plugin/plugin.json +1 -1
  6. package/integrations/claude-code/agents/code-structure.md +0 -3
  7. package/integrations/claude-code/skills/setup/SKILL.md +26 -0
  8. package/integrations/claude-code-local/.claude-plugin/plugin.json +9 -0
  9. package/integrations/{claude-code → claude-code-local}/.mcp.json +1 -1
  10. package/integrations/claude-code-local/README.md +10 -0
  11. package/integrations/claude-code-local/agents/code-structure.md +22 -0
  12. package/integrations/{claude-code → claude-code-local}/hooks/gi-hook.mjs +15 -4
  13. package/integrations/opencode/graph-indexer.js +10 -3
  14. package/package.json +1 -1
  15. package/src/cli/helper.mjs +15 -1
  16. package/src/cli/hook.mjs +124 -40
  17. package/src/cli/init.mjs +32 -5
  18. package/src/cli/resident.mjs +19 -4
  19. package/src/index/discover.mjs +52 -30
  20. package/src/index/indexer.mjs +78 -42
  21. package/src/index/modules.mjs +75 -2
  22. package/src/index/parse-worker.mjs +12 -0
  23. package/src/index/parsepool.mjs +73 -0
  24. package/src/index/resolver.mjs +362 -23
  25. package/src/mcp/server.mjs +43 -13
  26. package/src/mcp/tools.mjs +418 -26
  27. package/src/parse/arity.mjs +39 -2
  28. package/src/parse/extract.mjs +269 -45
  29. package/src/parse/lang/ecma.mjs +2 -0
  30. package/src/parse/lang/go.mjs +2 -0
  31. package/src/parse/lang/jvm.mjs +33 -0
  32. package/src/parse/lang/markdown.mjs +92 -0
  33. package/src/parse/lang/python.mjs +43 -0
  34. package/src/parse/lang/rust.mjs +215 -15
  35. package/src/parse/lang/sfc.mjs +126 -0
  36. package/src/parse/languages.mjs +9 -2
  37. package/src/query/check.mjs +49 -13
  38. package/src/query/facts.mjs +3 -2
  39. package/src/query/injection-spring.mjs +406 -0
  40. package/src/query/injection.mjs +174 -0
  41. package/src/query/intel.mjs +85 -15
  42. package/src/query/issue.mjs +156 -0
  43. package/src/query/orm.mjs +288 -0
  44. package/src/query/read.mjs +6 -1
  45. package/src/query/report.mjs +53 -0
  46. package/src/query/routes.mjs +142 -0
  47. package/src/query/showcase.mjs +59 -0
  48. package/src/query/textsearch.mjs +30 -9
  49. package/src/search/search.mjs +85 -17
  50. package/src/search/semantic.mjs +201 -0
  51. package/src/search/vectors.mjs +122 -0
  52. package/src/store/db.mjs +33 -4
  53. package/src/util/git.mjs +60 -11
  54. package/src/util/paths.mjs +10 -3
  55. package/vendor/grammars/README.md +47 -0
  56. package/vendor/grammars/kotlin.wasm.gz +0 -0
  57. package/vendor/grammars/manifest.json +4 -4
  58. /package/integrations/{claude-code → claude-code-local}/hooks/hooks.json +0 -0
package/SECURITY.md CHANGED
@@ -25,32 +25,43 @@ are not maintained; please upgrade to the latest version.
25
25
 
26
26
  graph-indexer is a local developer tool. Its threat model follows from a few deliberate choices:
27
27
 
28
- - **No network.** graph-indexer makes no outbound connections: no telemetry, no model
29
- downloads, no grammar installs. The tree-sitter runtime and all grammars ship inside the
30
- package as WebAssembly (versions and SHA-256 checksums in `vendor/grammars/manifest.json`),
31
- so there is no install-time or first-run download and no native build step.
28
+ - **No network.** graph-indexer makes no outbound connections: no telemetry, no grammar
29
+ installs. The tree-sitter runtime and all grammars ship inside the package as WebAssembly
30
+ (versions and SHA-256 checksums in `vendor/grammars/manifest.json`), so there is no install-time
31
+ or first-run download and no native build step. The one exception is explicit and opt-in:
32
+ `graph-indexer semantic install` downloads the optional embedding model from huggingface.co at a
33
+ pinned revision and installs it only if every file's SHA-256 matches the digest in the source.
32
34
  - **No dependencies.** The package has no npm dependencies; it uses only Node.js built-ins
33
35
  (`node:sqlite`, `node:fs`, `node:child_process`, …) and the vendored WebAssembly runtime.
34
36
  - **No code execution.** Source files are parsed into syntax trees; the indexed code is never
35
37
  imported, evaluated or run. The only subprocesses are read-only git commands
36
- (`git rev-parse`, `git ls-files`, `git diff`, `git log`, `git show`) run in the indexed repository
37
- with fixed arguments. The only tool input they take is the `base` revision of `check_changes`,
38
- which is refused when git could read it as an option. Every git call switches off what the
39
- repository's own git config could run: `core.fsmonitor`, hooks, signature verification,
40
- clean/smudge/process filters, `diff.external` and textconv drivers.
38
+ (`git rev-parse`, `git ls-files`, `git status`, `git diff`, `git log`, `git show`) run in the
39
+ indexed repository with fixed arguments, all through one function (`src/util/git.mjs`). The only
40
+ tool input they take is the `base` revision of `check_changes`, which is refused when git could
41
+ read it as an option. Every git call switches off what the repository's own git config could
42
+ run: `core.fsmonitor`, hooks, signature verification, clean/smudge/process filters, `diff.external`
43
+ and textconv drivers. The overrides are passed through git's environment (`GIT_CONFIG_COUNT`), which
44
+ holds any driver name verbatim, including one containing `=`; with a git older than 2.31 that
45
+ cannot, a repository naming such a driver gets no git call at all. git does not descend into
46
+ submodules (`--ignore-submodules=all`), whose own config the overrides do not cover. On Windows,
47
+ `git` and `npx` are looked up on PATH only, never in the repository (the working directory).
41
48
  - **No code from the repository through npx.** Every `npx` command graph-indexer runs or writes into
42
49
  an agent's configuration passes `--prefix`, so a `node_modules/graph-indexer` that the repository
43
50
  ships is never run in place of the published package.
44
51
  - **Stays inside the repository.** Only files listed by git (or found by a `.gitignore`-aware
45
- walk) under the repository root are indexed. Symlinks are followed only when their target
46
- resolves inside the repository, so a link to a file elsewhere on the machine is never read,
47
- indexed or served. Paths given to the tools (`read_code "file.ts:42"`, `outline`,
48
- `change_impact`) are resolved (`..` and symbolic links) and must stay inside the repository;
49
- `.git/` and `.graph-indexer/` are never served. Manifests (`package.json`, `tsconfig.json`,
50
- `go.mod`) are read only as small regular files inside the repository.
51
- - **Bounded work.** A regular expression given to `search_text` runs under a 5 s budget, and a file
52
- whose syntax makes the structural queries super-linear is given up after 15 s, so neither can
53
- block the server.
52
+ walk) under the repository root are indexed. Every component of a file's path is resolved, so
53
+ neither a symbolic link to a file elsewhere on the machine nor a file under a linked directory is
54
+ read, indexed or served; devices and FIFOs are skipped. Paths given to the tools
55
+ (`read_code "file.ts:42"`, `outline`, `change_impact`) are resolved the same way and must stay
56
+ inside the repository; `.git/` and `.graph-indexer/` are never served, in any letter case on a
57
+ case-insensitive file system. Manifests and other files read outside the index (`package.json`,
58
+ `tsconfig.json`, `go.mod`, `Cargo.toml`, `svelte.config.*`, `pom.xml`, `build.gradle`, `.gitignore`,
59
+ Prisma schemas, migrations) are read only as small regular files inside the repository.
60
+ - **Bounded work.** A regular expression given to `search_text` and a glob given to `find_files`
61
+ run under a 5 s budget, `.gitignore` patterns are matched without backtracking, `search_text`
62
+ annotates at most 50,000 matching lines, and a file whose syntax makes the structural queries
63
+ super-linear is given up after 15 s, so none of them can block the server. Tool arguments are held
64
+ to the bounds their schema declares (depths, limits, list lengths) whatever the client sends.
54
65
  - **Read-only tools.** All MCP tools are read-only (`readOnlyHint: true`) and never modify
55
66
  the repository.
56
67
  - **stdio transport.** The MCP server talks to the local client over stdin/stdout. It does not
@@ -59,10 +70,14 @@ graph-indexer is a local developer tool. Its threat model follows from a few del
59
70
  hooks, also answers hook and CLI queries over a Unix domain socket (a named pipe on Windows),
60
71
  never TCP. The socket lives in `<repo>/.graph-indexer/` with mode 0600; when that path is too
61
72
  long for a socket, in a per-user directory in the temp dir with mode 0700 whose owner and mode
62
- both sides check before using it. It answers the same read-only queries as the CLI.
73
+ both sides check before using it. On Windows the pipe's name carries a random id that each
74
+ resident writes to `.graph-indexer/`, so another user of the machine cannot create the pipe first
75
+ and answer in its place. It answers the same read-only queries as the CLI.
63
76
  `GRAPH_INDEXER_RESIDENT=0` disables it.
64
77
  - **Local artifacts.** The index is a SQLite database in `<repo>/.graph-indexer/` (git-ignored by
65
- `init`, mode 0700; a `.graph-indexer` that is a symbolic link is refused). It contains excerpts of your source code — treat it with the same sensitivity as the
78
+ `init`, mode 0700; a `.graph-indexer` that is a symbolic link is refused, symbolic links inside it
79
+ are removed before anything is written there, and an index committed to the repository is not
80
+ opened). It contains excerpts of your source code — treat it with the same sensitivity as the
66
81
  repository itself and do not commit or share it. Deleting the directory is always safe; it is
67
82
  rebuilt on the next run.
68
83
 
@@ -6,7 +6,8 @@
6
6
  * graph-indexer index [--repo DIR] build / update the index and print stats
7
7
  * graph-indexer init [--repo DIR] [--agents …] wire the MCP server into your coding agents
8
8
  * graph-indexer status [--repo DIR]
9
- * graph-indexer search <query> [--path P] [--kind K] [--limit N]
9
+ * graph-indexer explore <task in words> [--symbols a,b] [--path P] [--max-tokens N]
10
+ * graph-indexer search <query> [--or QUERY]… [--path P] [--kind K] [--limit N]
10
11
  * graph-indexer read <target>… [--full] symbols, file:START-END ranges or files, with the
11
12
  * definitions each one uses
12
13
  * graph-indexer symbol <name>… [--no-code]
@@ -15,6 +16,7 @@
15
16
  * graph-indexer impact [--symbols a,b] [--files x,y] [--diff] [--depth N]
16
17
  * graph-indexer outline [path] [--focus TEXT] [--max-tokens N]
17
18
  * graph-indexer files <text|glob> [--path P]
19
+ * graph-indexer demo grep versus exact uses, on this repository
18
20
  *
19
21
  * The query commands print exactly what the MCP tools return, so humans (and agents limited to a
20
22
  * shell) get the same answers.
@@ -33,6 +35,8 @@ let cmd = aliasOf[invokedAs] ?? null;
33
35
  if (!cmd && argv.length && !argv[0].startsWith('-')) { const c = argv.shift(); cmd = aliasOf[c] ?? c; }
34
36
 
35
37
  function flag(name) { const i = argv.indexOf(name); if (i < 0) return false; argv.splice(i, 1); return true; }
38
+ /** Every value of a repeatable option (`--or a --or b`). */
39
+ function opts(name) { const out = []; let v; while ((v = opt(name)) != null) out.push(v); return out; }
36
40
  function opt(name, dflt = null) {
37
41
  const i = argv.indexOf(name);
38
42
  if (i < 0) return dflt;
@@ -120,13 +124,21 @@ async function serveResident(intel, { idleMs = 0, onIdle = null } = {}) {
120
124
  const HELP = `graph-indexer ${pkg.version} — live code graph & search for AI coding agents (MCP)
121
125
 
122
126
  Usage:
123
- graph-indexer init [--repo DIR] [--agents claude,cursor,vscode,gemini,codex,opencode,junie,zed,devin] [--all] [--hooks] [--local] [--dry-run] [--no-instructions] [--no-helper]
127
+ graph-indexer init [--repo DIR] [--agents claude,cursor,vscode,gemini,codex,opencode,junie,zed,devin] [--all] [--hooks] [--local] [--dry-run] [--no-instructions] [--no-helper] [--no-demo]
124
128
  graph-indexer serve [--repo DIR] start the MCP server on stdio
125
129
  graph-indexer index [--repo DIR] build or update the index
126
130
  graph-indexer status [--repo DIR]
127
- graph-indexer search <query> [--path P] [--kind K] [--limit N]
131
+ graph-indexer explore <task in words> [--or QUERY]… [--symbols a,b] [--path P] [--max-tokens N]
132
+ relevant code, callers, definitions used and tests
133
+ graph-indexer search <query> [--or QUERY]… [--path P] [--kind K] [--limit N]
134
+ --or: other phrasings of the same need, fused
128
135
  graph-indexer grep <regex> [--path P] [--literal|-F] [-i] [--limit N]
129
136
  graph-indexer files <text|glob> [--path P] [--limit N] find files by path or name
137
+ graph-indexer demo a method that shares its name: grep versus its exact uses
138
+ graph-indexer report summary to paste into an issue (no paths, names or code)
139
+ graph-indexer routes [path | METHOD /path] HTTP routes and their handlers
140
+ graph-indexer inject [token | class] dependency injection: tokens, providers, injection points
141
+ graph-indexer models [name | table] ORM models: table, migrations, Prisma client calls
130
142
  graph-indexer read <target>… [--full] [--max-lines N] symbols (Class.method), ranges (file:10-80) or files,
131
143
  with where each name they use is defined
132
144
  graph-indexer symbol <name>… [--no-code] one or several definitions
@@ -136,6 +148,8 @@ Usage:
136
148
  graph-indexer outline [path] [--focus TEXT] [--max-tokens N]
137
149
  graph-indexer check [--files x,y] [--base REV] verify uncommitted edits
138
150
  graph-indexer hook post-tool|session-start|subagent-start agent hook (JSON on stdin)
151
+ graph-indexer semantic install|status optional static code-embedding model (32 MB, MIT) for
152
+ search in words; used with GRAPH_INDEXER_SEMANTIC=1
139
153
  graph-indexer daemon [--idle-minutes N] keep the index open for hooks and CLI queries
140
154
  (the hooks start it; the MCP server does the same)
141
155
 
@@ -170,6 +184,17 @@ async function main() {
170
184
  for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, stop);
171
185
  return;
172
186
  }
187
+ case 'semantic': {
188
+ const { installModel, modelDir, semanticEnabled, MODEL } = await import('../src/search/semantic.mjs');
189
+ const sub = argv.shift() ?? 'status';
190
+ if (sub === 'install') {
191
+ const dir = await installModel({ log });
192
+ process.stdout.write(`${MODEL.repo}@${MODEL.revision.slice(0, 12)} (${MODEL.license}) installed in ${dir}\nTurn the semantic channel on with GRAPH_INDEXER_SEMANTIC=1.\n`);
193
+ } else {
194
+ process.stdout.write(JSON.stringify({ model: MODEL.repo, revision: MODEL.revision, dir: modelDir(), installed: fs.existsSync(path.join(modelDir(), 'model.safetensors')), enabled: semanticEnabled() }, null, 2) + '\n');
195
+ }
196
+ return;
197
+ }
173
198
  case 'index': {
174
199
  const t0 = Date.now();
175
200
  const intel = await openIntel();
@@ -191,9 +216,13 @@ async function main() {
191
216
  await runInit({ argv, opt, flag, log, version: pkg.version, repo: repoArg });
192
217
  return;
193
218
  }
219
+ case 'explore': {
220
+ const symbols = (opt('--symbols') ?? '').split(',').filter(Boolean); const pathF = opt('--path'); const maxTokens = Number(opt('--max-tokens', 2000)); const queries = opts('--or');
221
+ return runTool('explore', { query: argv.join(' '), queries, symbols, path: pathF, max_tokens: maxTokens });
222
+ }
194
223
  case 'search': {
195
- const pathF = opt('--path'); const kind = opt('--kind'); const limit = Number(opt('--limit', 8));
196
- return runTool('search_code', { query: argv.join(' '), path: pathF, kind, limit });
224
+ const pathF = opt('--path'); const kind = opt('--kind'); const limit = Number(opt('--limit', 8)); const queries = opts('--or');
225
+ return runTool('search_code', { query: argv.join(' '), queries, path: pathF, kind, limit });
197
226
  }
198
227
  case 'grep': {
199
228
  const pathF = opt('--path'); const literal = flag('--literal') || flag('-F'); const ic = flag('-i') || flag('--ignore-case'); const limit = Number(opt('--limit', 60));
@@ -234,6 +263,18 @@ async function main() {
234
263
  const files = (opt('--files') ?? '').split(',').filter(Boolean); const base = opt('--base', 'HEAD');
235
264
  return runTool('check_changes', { files, base });
236
265
  }
266
+ case 'demo': return runTool('demo', {});
267
+ case 'routes': return runTool('routes', { filter: argv.join(' ') });
268
+ case 'inject': return runTool('inject', { filter: argv.join(' ') });
269
+ case 'models': return runTool('models', { filter: argv.join(' ') });
270
+ case 'report': {
271
+ // for bug reports: sizes, languages, binding rates and latencies — no paths, names or code
272
+ const intel = await openIntel({ quiet: true });
273
+ const { reportText } = await import('../src/query/report.mjs');
274
+ process.stdout.write(await reportText(intel, { version: pkg.version }) + '\n');
275
+ intel.close();
276
+ return;
277
+ }
237
278
  case 'files': {
238
279
  const pathF = opt('--path'); const limit = Number(opt('--limit', 100));
239
280
  return runTool('find_files', { pattern: argv.join(' '), path: pathF, limit });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graph-indexer",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "description": "Live code graph for Claude Code: exact references, structural grep, change impact and a post-edit check, from a local index with no model or network.",
5
5
  "author": { "name": "MaquinaTech", "url": "https://github.com/MaquinaTech" },
6
6
  "homepage": "https://github.com/MaquinaTech/graph-indexer",
@@ -1,9 +1,6 @@
1
1
  ---
2
2
  name: code-structure
3
3
  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."
4
- model: haiku
5
- disallowedTools: Edit, Write, MultiEdit, NotebookEdit
6
- omitClaudeMd: true
7
4
  ---
8
5
  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 (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.
9
6
 
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: setup
3
+ description: Connect graph-indexer's MCP server to this repository so Claude Code can use exact references, call graphs and change impact. Use when the user installs this plugin, or when the graph-indexer tools (find_references, call_graph, change_impact) are not available.
4
+ ---
5
+ # Connect graph-indexer
6
+
7
+ This plugin brings the `code-structure` sub-agent and these instructions. The graph-indexer tools
8
+ come from a local MCP server that the user adds to the repository once. The plugin directory does not
9
+ allow plugins to start local programs, so the user must approve this step.
10
+
11
+ 1. Check that Node.js 22.23.4+, 24.22.1+ or 26.12.1+ is installed (`node --version`).
12
+ 2. Ask the user to confirm, then run from the repository root:
13
+
14
+ ```bash
15
+ claude mcp add graph-indexer -- npx -y --prefix / graph-indexer@3.1.1 serve
16
+ ```
17
+
18
+ `--prefix /` makes npx run only the published package, never a copy shipped inside the repository.
19
+ The version is pinned on purpose: update it by hand when a newer graph-indexer is released.
20
+
21
+ 3. Optional, with the user's consent: `npx -y --prefix / graph-indexer@3.1.1 init --hooks` also writes the
22
+ Claude Code hooks (an edit check after each edit, grep disambiguation) to `.claude/settings.json`.
23
+ 4. Restart Claude Code, or run `/mcp`, and confirm `graph-indexer` is connected.
24
+
25
+ Then use `find_references`, `call_graph`, `change_impact`, `read_code` and `check_changes`, or delegate
26
+ structural questions to the `code-structure` sub-agent.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "graph-indexer",
3
+ "version": "3.1.1",
4
+ "description": "Live code graph for Claude Code: exact references, structural grep, change impact and a post-edit check, from a local index with no model or network.",
5
+ "author": { "name": "MaquinaTech", "url": "https://github.com/MaquinaTech" },
6
+ "homepage": "https://github.com/MaquinaTech/graph-indexer",
7
+ "license": "MIT",
8
+ "icon": "https://raw.githubusercontent.com/MaquinaTech/graph-indexer/main/assets/logo.jpg"
9
+ }
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "graph-indexer": {
4
4
  "command": "npx",
5
- "args": ["-y", "--prefix", "/", "graph-indexer@3.0.1", "serve"]
5
+ "args": ["-y", "--prefix", "/", "graph-indexer@3.1.1", "serve"]
6
6
  }
7
7
  }
8
8
  }
@@ -0,0 +1,10 @@
1
+ # graph-indexer: full Claude Code plugin (manual install)
2
+
3
+ This folder is the plugin with everything wired in: the MCP server (`.mcp.json`), the hooks
4
+ (`hooks/`) and the `code-structure` sub-agent. The plugin directory lists only plugins made of
5
+ Markdown and JSON (see `../claude-code`), so this one is not listed there. To use it, copy the
6
+ folder or point a local marketplace at it. `npx -y --prefix / graph-indexer@3.1.1 init --hooks`
7
+ gives the same result without a plugin.
8
+
9
+ Each release must update the version in `.mcp.json`, `hooks/gi-hook.mjs` and
10
+ `.claude-plugin/plugin.json`; `test/plugin-pins.test.mjs` checks that they equal `package.json`.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: code-structure
3
+ 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."
4
+ model: haiku
5
+ disallowedTools: Edit, Write, MultiEdit, NotebookEdit
6
+ omitClaudeMd: true
7
+ ---
8
+ 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 (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.
9
+
10
+ How to answer
11
+ - 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.
12
+ - 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.
13
+ - Classes that extend a class or implement an interface, directly or through other classes: `find_references` with kind `inherit`.
14
+ - What a change affects (call sites to update, overrides, subclasses that inherit it, tests to run): `change_impact` with the symbols.
15
+ - A definition or a signature: `read_code` with its name.
16
+ - 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.
17
+ - Keep to the scope asked: the directories, whether tests count, how many levels.
18
+
19
+ Reply
20
+ - When the request gives an answer format, follow it exactly.
21
+ - 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.
22
+ - Nothing else: no account of how you found it. You do not change files.
@@ -20,6 +20,8 @@ import { spawn } from 'node:child_process';
20
20
  import { pathToFileURL } from 'node:url';
21
21
 
22
22
  const PROTOCOL = 1;
23
+ // the exact release this plugin version runs through npx (never a range): keep it equal to package.json's version
24
+ export const VERSION = '3.1.1';
23
25
  const WIN = process.platform === 'win32';
24
26
 
25
27
  export function findRepoRoot(start) {
@@ -35,7 +37,13 @@ export function findRepoRoot(start) {
35
37
  export function socketPath(root) {
36
38
  const abs = path.resolve(root);
37
39
  const hash = crypto.createHash('sha256').update(abs).digest('hex').slice(0, 16);
38
- if (WIN) return `\\\\.\\pipe\\graph-indexer-${hash}`;
40
+ // a pipe name anyone could compute would let another user of the machine create it first and
41
+ // answer as the resident: the name carries a random id kept in the private data directory
42
+ if (WIN) {
43
+ let id = '';
44
+ try { id = fs.readFileSync(path.join(abs, '.graph-indexer', 'resident.pipe-id'), 'utf8').trim(); } catch { /* no resident yet */ }
45
+ return /^[0-9a-f]{32}$/.test(id) ? `\\\\.\\pipe\\graph-indexer-${hash}-${id}` : null;
46
+ }
39
47
  const inRepo = path.join(abs, '.graph-indexer', 'resident.sock');
40
48
  if (Buffer.byteLength(inRepo) <= 100) return inRepo;
41
49
  return path.join(os.tmpdir(), `graph-indexer-${process.getuid?.() ?? 'user'}`, `${hash}.sock`);
@@ -55,7 +63,7 @@ function trusted(file, root) {
55
63
  /** The resident's answer ({ out }) or null. `any: true`: this client has no logic of its own to match. */
56
64
  function ask(root, request, totalMs) {
57
65
  const file = socketPath(root);
58
- if (!trusted(file, root)) return Promise.resolve(null);
66
+ if (!file || !trusted(file, root)) return Promise.resolve(null);
59
67
  return new Promise((resolve) => {
60
68
  let done = false, buf = '';
61
69
  const sock = net.createConnection(file);
@@ -89,8 +97,11 @@ async function main() {
89
97
  // no resident yet: the full hook through npx (it starts one for the next calls); --prefix keeps
90
98
  // npx from running a graph-indexer that the repository ships in its own node_modules
91
99
  // (on Windows npx.cmd needs a shell, so the home directory is quoted for cmd.exe)
92
- const home = WIN ? `"${os.homedir()}"` : os.homedir();
93
- const child = spawn(WIN ? 'npx.cmd' : 'npx', ['-y', '--prefix', home, 'graph-indexer@3', 'hook', event], { stdio: ['pipe', 'inherit', 'ignore'], shell: WIN });
100
+ // It runs from the home directory with the repository passed explicitly: cmd.exe looks for
101
+ // npx.cmd in the working directory first, which here would be the repository's.
102
+ if (WIN && /["%\r\n]/.test(root)) return;
103
+ const q = (a) => (WIN ? `"${a}"` : a);
104
+ const child = spawn(WIN ? 'npx.cmd' : 'npx', ['-y', '--prefix', q(os.homedir()), `graph-indexer@${VERSION}`, 'hook', event, '--repo', q(root)], { cwd: os.homedir(), stdio: ['pipe', 'inherit', 'ignore'], shell: WIN });
94
105
  child.on('error', () => { /* fail open */ });
95
106
  child.stdin.on('error', () => { /* fail open */ });
96
107
  child.stdin.end(raw);
@@ -39,7 +39,13 @@ function findRepoRoot(start) {
39
39
  function socketPath(root) {
40
40
  const abs = path.resolve(root);
41
41
  const hash = crypto.createHash('sha256').update(abs).digest('hex').slice(0, 16);
42
- if (WIN) return `\\\\.\\pipe\\graph-indexer-${hash}`;
42
+ // a pipe name anyone could compute would let another user of the machine create it first and
43
+ // answer as the resident: the name carries a random id kept in the private data directory
44
+ if (WIN) {
45
+ let id = '';
46
+ try { id = fs.readFileSync(path.join(abs, '.graph-indexer', 'resident.pipe-id'), 'utf8').trim(); } catch { /* no resident yet */ }
47
+ return /^[0-9a-f]{32}$/.test(id) ? `\\\\.\\pipe\\graph-indexer-${hash}-${id}` : null;
48
+ }
43
49
  const inRepo = path.join(abs, '.graph-indexer', 'resident.sock');
44
50
  if (Buffer.byteLength(inRepo) <= 100) return inRepo;
45
51
  return path.join(os.tmpdir(), `graph-indexer-${process.getuid?.() ?? 'user'}`, `${hash}.sock`);
@@ -58,7 +64,7 @@ function trusted(file, root) {
58
64
 
59
65
  function ask(root, request, totalMs) {
60
66
  const file = socketPath(root);
61
- if (!trusted(file, root)) return Promise.resolve(null);
67
+ if (!file || !trusted(file, root)) return Promise.resolve(null);
62
68
  return new Promise((resolve) => {
63
69
  let done = false, buf = '';
64
70
  const sock = net.createConnection(file);
@@ -112,7 +118,8 @@ export const GraphIndexer = async ({ directory, worktree } = {}) => {
112
118
  // every argument, and give up on one that a quote cannot hold
113
119
  const args = [...DAEMON.slice(1), '--repo', root];
114
120
  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 });
121
+ // from the home directory: cmd.exe would look for npx.cmd in the repository first
122
+ const child = spawn(DAEMON[0], WIN ? args.map(a => `"${a}"`) : args, { cwd: os.homedir(), detached: true, stdio: 'ignore', windowsHide: true, shell: WIN });
116
123
  child.on('error', () => { /* optional */ });
117
124
  child.unref();
118
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graph-indexer",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
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
6
  "main": "src/query/intel.mjs",
@@ -4,7 +4,7 @@
4
4
  * index, and replies with the answer alone. The main agent delegates the question and gets back a
5
5
  * path:line list instead of the searches and reads behind it, and the lookup runs on a model that
6
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/).
7
+ * plugin ships it too (integrations/claude-code/agents/, with only the frontmatter keys the plugin directory accepts).
8
8
  */
9
9
 
10
10
  export const HELPER_NAME = 'code-structure';
@@ -65,3 +65,17 @@ omitClaudeMd: true
65
65
  ${helperInstructions()}
66
66
  `;
67
67
  }
68
+
69
+ /**
70
+ * The same sub-agent for the Claude Code plugin listed in the plugin directory, which accepts only
71
+ * a short list of frontmatter keys (name and description): the model and the tool limits are left
72
+ * to the session, and the instructions already say it does not change files.
73
+ */
74
+ export function pluginAgentFile() {
75
+ return `---
76
+ name: ${HELPER_NAME}
77
+ description: ${JSON.stringify(DESCRIPTION)}
78
+ ---
79
+ ${helperInstructions()}
80
+ `;
81
+ }