graph-indexer 3.0.0 → 3.1.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 (60) hide show
  1. package/CHANGELOG.md +456 -0
  2. package/README.md +72 -15
  3. package/SECURITY.md +21 -4
  4. package/bin/graph-indexer.mjs +46 -5
  5. package/integrations/claude-code/.claude-plugin/plugin.json +3 -2
  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 +12 -6
  13. package/integrations/opencode/graph-indexer.js +13 -8
  14. package/package.json +2 -2
  15. package/src/cli/helper.mjs +15 -1
  16. package/src/cli/hook.mjs +140 -48
  17. package/src/cli/init.mjs +49 -17
  18. package/src/cli/resident.mjs +15 -5
  19. package/src/index/discover.mjs +3 -5
  20. package/src/index/indexer.mjs +77 -41
  21. package/src/index/modules.mjs +82 -7
  22. package/src/index/parse-worker.mjs +12 -0
  23. package/src/index/parsepool.mjs +73 -0
  24. package/src/index/resolver.mjs +371 -25
  25. package/src/mcp/server.mjs +16 -12
  26. package/src/mcp/tools.mjs +419 -26
  27. package/src/parse/arity.mjs +41 -3
  28. package/src/parse/extract.mjs +277 -46
  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 +125 -0
  36. package/src/parse/languages.mjs +9 -2
  37. package/src/query/check.mjs +55 -17
  38. package/src/query/facts.mjs +3 -2
  39. package/src/query/injection-spring.mjs +405 -0
  40. package/src/query/injection.mjs +173 -0
  41. package/src/query/intel.mjs +87 -22
  42. package/src/query/issue.mjs +156 -0
  43. package/src/query/orm.mjs +286 -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/testcmd.mjs +25 -14
  49. package/src/query/textsearch.mjs +29 -11
  50. package/src/search/search.mjs +85 -17
  51. package/src/search/semantic.mjs +201 -0
  52. package/src/search/vectors.mjs +122 -0
  53. package/src/store/db.mjs +22 -4
  54. package/src/util/git.mjs +30 -0
  55. package/src/util/paths.mjs +40 -1
  56. package/src/util/regex.mjs +2 -0
  57. package/vendor/grammars/README.md +47 -0
  58. package/vendor/grammars/kotlin.wasm.gz +0 -0
  59. package/vendor/grammars/manifest.json +4 -4
  60. /package/integrations/{claude-code → claude-code-local}/hooks/hooks.json +0 -0
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  <a href="https://www.npmjs.com/package/graph-indexer"><img src="https://img.shields.io/npm/v/graph-indexer?color=007acc&style=for-the-badge" alt="npm version"></a>
11
11
  <a href="https://www.npmjs.com/package/graph-indexer"><img src="https://img.shields.io/npm/dt/graph-indexer?color=4caf50&style=for-the-badge" alt="NPM Downloads"></a>
12
- <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-%3E%3D22.5-brightgreen?style=for-the-badge&logo=nodedotjs" alt="Node.js 22.5+"></a>
12
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-22.23.4%2B%20%7C%2024.22.1%2B%20%7C%2026.12.1%2B-brightgreen?style=for-the-badge&logo=nodedotjs" alt="Node.js 22.23.4+, 24.22.1+ or 26.12.1+"></a>
13
13
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge" alt="License: MIT"></a>
14
14
  </div>
15
15
 
@@ -40,14 +40,18 @@ instead of reading whole files and grepping.
40
40
 
41
41
  ## Quick start
42
42
 
43
- Requires **Node.js 22.5+** (for the built-in `node:sqlite`).
43
+ Requires **Node.js 22.23.4+, 24.22.1+ or 26.12.1+** (the built-in `node:sqlite`, and the October 2026
44
+ Node.js security releases, which fix CVE-2026-111497).
44
45
 
45
46
  ```bash
46
47
  cd your-repo
47
- npx graph-indexer init # wires the MCP server into the agents it detects
48
- npx graph-indexer index # optional: build the index now (the server also does it on start)
48
+ npx -y --prefix / graph-indexer@3 init # wires the MCP server into the agents it detects
49
+ npx -y --prefix / graph-indexer@3 index # optional: build the index now (the server also does it on start)
49
50
  ```
50
51
 
52
+ `--prefix /` makes npx run the published package even in a repository that ships its own
53
+ `node_modules/graph-indexer`.
54
+
51
55
  `init` writes project-level MCP configuration for the agents it finds (`.mcp.json` for Claude
52
56
  Code, `.cursor/mcp.json`, `.vscode/mcp.json`, `.gemini/settings.json`, `opencode.json` for
53
57
  OpenCode and Kilo Code, `.junie/mcp/mcp.json`, `.zed/settings.json`; it prints the Codex TOML
@@ -56,12 +60,18 @@ to `.gitignore`, and adds a short block to `CLAUDE.md` / `AGENTS.md` on how to l
56
60
  For Claude Code it also writes a structural helper, `.claude/agents/code-structure.md`: a
57
61
  read-only sub-agent on a small model that answers call-site, caller, subclass and impact
58
62
  questions from the index, so the main agent can hand them off and get back only the list (the
59
- Claude Code plugin ships it too). `--hooks` adds the Claude Code hooks to `.claude/settings.json`,
63
+ Claude Code plugin ships it too (`integrations/claude-code`; the full plugin with hooks is `integrations/claude-code-local`)). `--hooks` adds the Claude Code hooks to `.claude/settings.json`,
60
64
  which Devin, Copilot CLI and Cursor also run, and the OpenCode / Kilo Code plugin that appends the
61
65
  same context to their tool output. Flags: `--agents claude,cursor,vscode,gemini,codex,opencode,junie,zed,devin`,
62
66
  `--all`, `--hooks`, `--local` (use this checkout instead of `npx`), `--dry-run`,
63
67
  `--no-instructions`, `--no-helper`.
64
68
 
69
+ In Claude Code (2.1.275 or later), the plugin installs in one step:
70
+
71
+ ```text
72
+ /plugin install graph-indexer --marketplace MaquinaTech/graph-indexer
73
+ ```
74
+
65
75
  <details>
66
76
  <summary>Manual configuration</summary>
67
77
 
@@ -70,7 +80,7 @@ Any MCP client can run the server over stdio:
70
80
  ```json
71
81
  {
72
82
  "mcpServers": {
73
- "graph-indexer": { "command": "npx", "args": ["-y", "graph-indexer@3", "serve", "--repo", "/path/to/repo"] }
83
+ "graph-indexer": { "command": "npx", "args": ["-y", "--prefix", "/", "graph-indexer@3", "serve", "--repo", "/path/to/repo"] }
74
84
  }
75
85
  }
76
86
  ```
@@ -81,7 +91,7 @@ VS Code uses `"servers"` instead of `"mcpServers"` and `"type": "stdio"`. Codex
81
91
  ```toml
82
92
  [mcp_servers.graph-indexer]
83
93
  command = "npx"
84
- args = ["-y", "graph-indexer@3", "serve", "--repo", "/path/to/repo"]
94
+ args = ["-y", "--prefix", "/", "graph-indexer@3", "serve", "--repo", "/path/to/repo"]
85
95
  ```
86
96
 
87
97
  Without `--repo` the server indexes the git repository containing its working directory
@@ -90,11 +100,19 @@ Without `--repo` the server indexes the git repository containing its working di
90
100
 
91
101
  ## The tools
92
102
 
103
+ `npx graph-indexer init` ends with an example from your own repository: a method that shares its
104
+ name with other definitions, what grep returns for that name, and the calls the index binds to
105
+ that one definition (`npx graph-indexer demo` shows it again).
106
+
93
107
  | Tool | Use it to | Typical reply |
94
108
  |---|---|---|
109
+ | `explore` | start a task or bug in code you have not read, in one call | the most relevant definitions with their code, who calls them, overrides and subclasses, where the names they use are defined and the tests to run with the command — inside a token budget, naming what it left out |
110
+ | `explore` + routes | ask about an endpoint ("what happens on POST /owners/42/edit") | the handler the framework registers for it — FastAPI, Flask, NestJS, Spring, Express/Fastify/Koa/Hono, Gin/Echo/chi; `npx graph-indexer routes` lists them all |
111
+ | dependency injection | look up a NestJS or Spring class, interface or token (`find_references`, `explore`) | what an injection point receives — NestJS: the `provide: TOKEN, useClass/useFactory/…` or `ClientsModule` registration of an `@Inject(TOKEN)`; Spring: the bean of an `@Autowired`/constructor/`@Bean` parameter, chosen by `@Qualifier`, `@Primary` and name, Feign, Spring Data and MyBatis proxies named as such — and where a provider is injected, which neither the type checker nor grep for the type connects; `npx graph-indexer inject` lists them all |
112
+ | ORM models | look up a model (`find_references`, `explore`; `npx graph-indexer models`) | its table, the migrations that touch it, and for Prisma the client calls (`prisma.user.findMany`) that only the generated client types — Prisma, Django, SQLAlchemy/SQLModel, TypeORM |
95
113
  | `read_code` | read symbols, line ranges or files — several per call — without searching for what they use | code with line numbers, then where each name it uses is defined (file:line and signature); who uses a symbol and which tests reference it; a long file's outline |
96
114
  | `search_text` | grep any text across the repository (code, config, docs) | grep lines with the enclosing definition and, for identifiers, the definition each match refers to; where the definition is when the search missed it |
97
- | `search_code` | find where a behaviour or identifier lives (natural language or names) | ranked symbols with location, signature, doc line and matching lines |
115
+ | `search_code` | find where a behaviour or identifier lives (natural language or names); `queries: [...]` asks for several phrasings at once | ranked symbols with location, signature, doc line and matching lines; for a question about docs, design or the reason for something, the Markdown sections that answer it |
98
116
  | `find_references` | see every use before renaming or changing a signature | uses grouped by file, enclosing function, confidence, calls through base types marked |
99
117
  | `call_graph` | trace callers and callees across files | a tree with locations and confidence |
100
118
  | `change_impact` | get the blast radius of a change, or of your uncommitted diff | transitive dependents by distance, tests to run, co-changed files, risk |
@@ -132,6 +150,12 @@ often changed together (git): context.go (14/42), context_test.go (10/42), …
132
150
  risk: medium (9 dependents, 1 test files)
133
151
  ```
134
152
 
153
+ **Search by meaning (optional).** `npx graph-indexer semantic install` downloads a 32 MB static code
154
+ embedding model (potion-code-16M-v2, MIT, pinned and checksummed); with `GRAPH_INDEXER_SEMANTIC=1`
155
+ search also ranks definitions by meaning, in plain JavaScript and without network access. It helps
156
+ queries written in words (top-5 success 0.674 → 0.728) and not the localization of real commits,
157
+ so it is off by default ([measurements](docs/BENCHMARKS.md#3-symbol-search)).
158
+
135
159
  Every reference carries a confidence: **exact/high** means it was bound through a scope,
136
160
  import or inferred receiver type; **likely/possible** means only the name matched. Footers
137
161
  report same-name call sites that could not be bound, so an agent knows when to double-check
@@ -157,8 +181,10 @@ own `findReferences` (import lines and declarations excluded):
157
181
  do not. Against the compiler's full rename set (sibling overrides included) recall is 0.925. The
158
182
  fixture has no `node_modules`, so library-typed values are invisible to the compiler: precision is
159
183
  a lower bound.* The same measurement against the Go type checker (gin, caddy, nats-server) gives
160
- precision 0.992–1.000 and recall 0.972–0.981, and against the Java compiler (spring-petclinic,
161
- jsoup, Commons Collections) 0.955–0.994 and 0.861–0.989; grep's precision there is 0.02–0.61.
184
+ precision 0.992–1.000 and recall 0.972–0.981, against the Java compiler (spring-petclinic,
185
+ jsoup, Commons Collections) 0.961–0.994 and 0.883–0.989, against pyright (six Python
186
+ repositories) 0.895–0.998 and 0.927–0.998, and against rust-analyzer (serde_json, ripgrep, tokio)
187
+ 0.968–0.990 and 0.799–0.942; grep's precision there is 0.01–0.61.
162
188
  In the server of Twenty, an open-source CRM monorepo (28,512 files), precision is 0.999 and
163
189
  recall 0.924; grep's precision is 0.009 and 2.x's 0.052.
164
190
 
@@ -168,7 +194,7 @@ rolled back to the parent commit first.
168
194
 
169
195
  | | file Acc@1 | file Acc@5 | function Acc@5 | function MRR@10 |
170
196
  |---|---|---|---|---|
171
- | graph-indexer `search_code` | **0.544** | **0.805** | **0.528** | **0.432** |
197
+ | graph-indexer `search_code` | **0.550** | **0.799** | **0.528** | **0.433** |
172
198
  | grep, files ranked by idf-weighted hits | 0.485 | 0.769 | 0.514 | 0.374 |
173
199
  | BM25 over the same index | 0.373 | 0.722 | 0.401 | 0.290 |
174
200
 
@@ -176,6 +202,14 @@ rolled back to the parent commit first.
176
202
  strictly at symbol level: rank-1 0.703 and MRR 0.760, against 0.552 and 0.646 for graph-indexer
177
203
  2.x on the same queries. On the 169 held-out queries (never used for tuning) MRR is 0.773 vs 0.632.
178
204
 
205
+ **Against zvec-grep 0.3.0** (hybrid BM25 + vector search, with its default embedding model,
206
+ Qwen3-Embedding-0.6B, run on the same Mac): on the same 377 queries its best setting, code only,
207
+ reaches rank-1 0.581 and MRR 0.639 (graph-indexer 0.703 and 0.761); on the 169 commits it puts
208
+ the changed file first in 0.420 of them (graph-indexer 0.550, ranked grep 0.485). It is ahead on
209
+ two repositories, gin and fastapi. It has no references or callers. In a one-repetition pilot of
210
+ agent sessions (Haiku 5.5, 24 tasks, 13 of them from zvec-grep's own benchmark) neither tool beat
211
+ Claude Code alone. [Details](docs/BENCHMARKS.md#zvec-grep-030-2026-11-06).
212
+
179
213
  **Against 2.x.** In real Claude Code sessions on nestjs and Twenty (40 tasks, 120 runs), 3.0
180
214
  cost 0.43 of 2.x on code questions and 0.46 on refactors, with a quarter to a third of the
181
215
  tokens spent looking code up; 2.x cost about twice as much as no index, as its tools and prompt
@@ -236,9 +270,32 @@ files are re-parsed. Warm tool calls take 1–4 ms for the graph tools, about 30
236
270
 
237
271
  ## Languages
238
272
 
239
- JavaScript, TypeScript/TSX, Python, Go, Java, Kotlin, Scala, C#, Rust, C, C++, Ruby, PHP, Bash
240
- and CSS/SCSS/Less. Reference binding and type inference are deepest for TypeScript/JavaScript,
241
- Python, Go, Java, C# and Rust.
273
+ Every language gets definitions, outlines, search and edit checks. How far references are bound
274
+ differs, and is stated rather than implied:
275
+
276
+ | Level | Languages | What it means |
277
+ |---|---|---|
278
+ | **Verified** | TypeScript/TSX, Go, Java, Python, Rust | references measured against the language's own compiler or type checker on three or more real repositories ([docs/BENCHMARKS.md](docs/BENCHMARKS.md)): TypeScript 0.997 precision / 0.962 recall (nestjs), Go ≥ 0.992 / ≥ 0.972 (gin, caddy, nats-server), Java 0.961–0.994 / 0.883–0.989 (spring-petclinic, jsoup, Commons Collections), Python 0.895–0.998 / 0.927–0.998 against pyright (fastapi, flask, requests, httpx, rich, click), Rust 0.974–0.990 / 0.838–0.947 against rust-analyzer (serde_json, ripgrep, tokio) |
279
+ | **Resolved** | JavaScript, C#, Vue and Svelte components (`<script>` as TypeScript; template tags as uses of the components imported) | scopes, imports and receiver types inferred from declarations, annotations and constructors; no compiler oracle yet |
280
+ | **Scoped** | Kotlin, Scala, C, C++, Ruby, PHP | scopes, imports/includes and packages; less type inference, so more calls stay unbound or by name |
281
+ | **Symbols** | Bash, CSS/SCSS/Less | definitions, outlines and search; references by name |
282
+
283
+ Each answer marks how a reference was bound (`exact`, `likely`, `possible`) and says when a list is
284
+ complete, whatever the level.
285
+
286
+ How much of the code the parser reads, measured on public repositories (68,000 files in 46
287
+ repositories: the fixtures, the agent benchmark's repositories, okhttp, kotlinx.coroutines,
288
+ nowinandroid, Exposed, spring-boot, cats, Play and Jellyfin):
289
+
290
+ | | files with a region the parser cannot read |
291
+ |---|---|
292
+ | TypeScript, TSX, JavaScript, Python, Java, Kotlin, Scala, C#, Rust, Go, Ruby, PHP | 0–0.2% (test data with deliberate syntax errors, `typeof import()` in type arguments, Python 3.15 unpacking comprehensions) |
293
+ | Bash | 4% (zsh completion scripts) |
294
+ | C, C++ | 39% of files, 15% of lines: macros inside declarations (`void (CJSON_CDECL *fn)(…)`); the functions themselves are kept |
295
+ | SCSS, Less | about half of the files: they are read with the CSS grammar, which does not know nesting, `#{…}` or mixins |
296
+
297
+ Where a symbol's name occurs in code the parser could not read, `find_references` and
298
+ `call_graph` say so and name the lines, instead of reporting the list as complete.
242
299
 
243
300
  ## Command line
244
301
 
@@ -276,7 +333,7 @@ Version 3 is a rewrite; see the [changelog](CHANGELOG.md). In short: the tools c
276
333
  `check_changes`, `outline`), the
277
334
  embedding, Ollama, reranker and daemon options are gone (search is lexical plus graph and needs
278
335
  no model), grammars are bundled instead of installed on first use, and Node.js 22.5+ is required.
279
- Re-run `npx graph-indexer init` to update agent configuration; old `.graph-indexer/` contents are
336
+ Re-run `npx -y --prefix / graph-indexer@3 init` to update agent configuration; old `.graph-indexer/` contents are
280
337
  replaced automatically.
281
338
 
282
339
  ## License
package/SECURITY.md CHANGED
@@ -33,13 +33,24 @@ graph-indexer is a local developer tool. Its threat model follows from a few del
33
33
  (`node:sqlite`, `node:fs`, `node:child_process`, …) and the vendored WebAssembly runtime.
34
34
  - **No code execution.** Source files are parsed into syntax trees; the indexed code is never
35
35
  imported, evaluated or run. The only subprocesses are read-only git commands
36
- (`git rev-parse`, `git ls-files`, `git diff`, `git log`) run in the indexed repository with
37
- fixed arguments; no tool input is passed to them.
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.
41
+ - **No code from the repository through npx.** Every `npx` command graph-indexer runs or writes into
42
+ an agent's configuration passes `--prefix`, so a `node_modules/graph-indexer` that the repository
43
+ ships is never run in place of the published package.
38
44
  - **Stays inside the repository.** Only files listed by git (or found by a `.gitignore`-aware
39
45
  walk) under the repository root are indexed. Symlinks are followed only when their target
40
46
  resolves inside the repository, so a link to a file elsewhere on the machine is never read,
41
47
  indexed or served. Paths given to the tools (`read_code "file.ts:42"`, `outline`,
42
- `change_impact`) are looked up in the index, never opened directly from disk.
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.
43
54
  - **Read-only tools.** All MCP tools are read-only (`readOnlyHint: true`) and never modify
44
55
  the repository.
45
56
  - **stdio transport.** The MCP server talks to the local client over stdin/stdout. It does not
@@ -51,10 +62,16 @@ graph-indexer is a local developer tool. Its threat model follows from a few del
51
62
  both sides check before using it. It answers the same read-only queries as the CLI.
52
63
  `GRAPH_INDEXER_RESIDENT=0` disables it.
53
64
  - **Local artifacts.** The index is a SQLite database in `<repo>/.graph-indexer/` (git-ignored by
54
- `init`). It contains excerpts of your source code — treat it with the same sensitivity as the
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
55
66
  repository itself and do not commit or share it. Deleting the directory is always safe; it is
56
67
  rebuilt on the next run.
57
68
 
69
+ ## Runtime
70
+
71
+ graph-indexer runs in the Node.js the user installs. It requires a release that includes the
72
+ October 2026 Node.js security fixes (22.23.4+, 24.22.1+ or 26.12.1+; CVE-2026-111497 and others).
73
+ Keep Node.js updated to the latest patch release of a supported line.
74
+
58
75
  ## Operational guidance
59
76
 
60
77
  - Run the server only on repositories you trust and intend to index.
@@ -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,8 +1,9 @@
1
1
  {
2
2
  "name": "graph-indexer",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
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",
7
- "license": "MIT"
7
+ "license": "MIT",
8
+ "icon": "https://raw.githubusercontent.com/MaquinaTech/graph-indexer/main/assets/logo.jpg"
8
9
  }
@@ -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.0 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.0 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.0",
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", "graph-indexer@3", "serve"]
5
+ "args": ["-y", "--prefix", "/", "graph-indexer@3.1.0", "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.0 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.0';
23
25
  const WIN = process.platform === 'win32';
24
26
 
25
27
  export function findRepoRoot(start) {
@@ -43,11 +45,12 @@ export function socketPath(root) {
43
45
 
44
46
  function trusted(file, root) {
45
47
  if (WIN) return true;
46
- const dir = path.dirname(file);
47
- if (dir === path.join(path.resolve(root), '.graph-indexer')) return fs.existsSync(file);
48
+ const mine = (st) => !process.getuid || st.uid === process.getuid();
48
49
  try {
49
- const st = fs.lstatSync(dir);
50
- return fs.existsSync(file) && st.isDirectory() && (!process.getuid || st.uid === process.getuid()) && (st.mode & 0o077) === 0;
50
+ const sock = fs.lstatSync(file), dir = fs.lstatSync(path.dirname(file));
51
+ const inRepo = path.dirname(file) === path.join(path.resolve(root), '.graph-indexer');
52
+ // the socket and its directory are ours, not links or listeners planted by the repository or another user
53
+ return sock.isSocket() && mine(sock) && dir.isDirectory() && mine(dir) && (inRepo || (dir.mode & 0o077) === 0);
51
54
  } catch { return false; }
52
55
  }
53
56
 
@@ -85,8 +88,11 @@ async function main() {
85
88
  const root = findRepoRoot(input.cwd || process.cwd());
86
89
  const reply = await ask(root, { op: 'hook', event, input }, 2500);
87
90
  if (reply) { if (reply.out) process.stdout.write(reply.out + '\n'); return; }
88
- // no resident yet: the full hook through npx (it starts one for the next calls)
89
- const child = spawn(WIN ? 'npx.cmd' : 'npx', ['-y', 'graph-indexer@3', 'hook', event], { stdio: ['pipe', 'inherit', 'ignore'], shell: WIN });
91
+ // no resident yet: the full hook through npx (it starts one for the next calls); --prefix keeps
92
+ // npx from running a graph-indexer that the repository ships in its own node_modules
93
+ // (on Windows npx.cmd needs a shell, so the home directory is quoted for cmd.exe)
94
+ const home = WIN ? `"${os.homedir()}"` : os.homedir();
95
+ const child = spawn(WIN ? 'npx.cmd' : 'npx', ['-y', '--prefix', home, `graph-indexer@${VERSION}`, 'hook', event], { stdio: ['pipe', 'inherit', 'ignore'], shell: WIN });
90
96
  child.on('error', () => { /* fail open */ });
91
97
  child.stdin.on('error', () => { /* fail open */ });
92
98
  child.stdin.end(raw);
@@ -19,8 +19,9 @@ import { spawn } from 'node:child_process';
19
19
 
20
20
  const PROTOCOL = 1;
21
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'];
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'];
24
25
  // OpenCode tool ids → the tool names graph-indexer's hook understands
25
26
  const TOOLS = { read: 'Read', grep: 'Grep', bash: 'Bash', edit: 'Edit', write: 'Write' };
26
27
 
@@ -46,12 +47,12 @@ function socketPath(root) {
46
47
 
47
48
  function trusted(file, root) {
48
49
  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;
50
+ const mine = (st) => !process.getuid || st.uid === process.getuid();
52
51
  try {
53
- const st = fs.lstatSync(dir);
54
- return st.isDirectory() && (!process.getuid || st.uid === process.getuid()) && (st.mode & 0o077) === 0;
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);
55
56
  } catch { return false; }
56
57
  }
57
58
 
@@ -107,7 +108,11 @@ export const GraphIndexer = async ({ directory, worktree } = {}) => {
107
108
  if (!reply) {
108
109
  if (!started) {
109
110
  started = true;
110
- const child = spawn(DAEMON[0], [...DAEMON.slice(1), '--repo', root], { detached: true, stdio: 'ignore', windowsHide: true, shell: WIN });
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 });
111
116
  child.on('error', () => { /* optional */ });
112
117
  child.unref();
113
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graph-indexer",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
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",
@@ -34,7 +34,7 @@
34
34
  "index": "node bin/graph-indexer.mjs index"
35
35
  },
36
36
  "engines": {
37
- "node": ">=22.5.0"
37
+ "node": "^22.23.4 || ^24.22.1 || >=26.12.1"
38
38
  },
39
39
  "repository": {
40
40
  "type": "git",
@@ -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
+ }