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.
- package/CHANGELOG.md +456 -0
- package/README.md +61 -8
- package/SECURITY.md +35 -20
- package/bin/graph-indexer.mjs +46 -5
- package/integrations/claude-code/.claude-plugin/plugin.json +1 -1
- package/integrations/claude-code/agents/code-structure.md +0 -3
- package/integrations/claude-code/skills/setup/SKILL.md +26 -0
- package/integrations/claude-code-local/.claude-plugin/plugin.json +9 -0
- package/integrations/{claude-code → claude-code-local}/.mcp.json +1 -1
- package/integrations/claude-code-local/README.md +10 -0
- package/integrations/claude-code-local/agents/code-structure.md +22 -0
- package/integrations/{claude-code → claude-code-local}/hooks/gi-hook.mjs +15 -4
- package/integrations/opencode/graph-indexer.js +10 -3
- package/package.json +1 -1
- package/src/cli/helper.mjs +15 -1
- package/src/cli/hook.mjs +124 -40
- package/src/cli/init.mjs +32 -5
- package/src/cli/resident.mjs +19 -4
- package/src/index/discover.mjs +52 -30
- package/src/index/indexer.mjs +78 -42
- package/src/index/modules.mjs +75 -2
- package/src/index/parse-worker.mjs +12 -0
- package/src/index/parsepool.mjs +73 -0
- package/src/index/resolver.mjs +362 -23
- package/src/mcp/server.mjs +43 -13
- package/src/mcp/tools.mjs +418 -26
- package/src/parse/arity.mjs +39 -2
- package/src/parse/extract.mjs +269 -45
- package/src/parse/lang/ecma.mjs +2 -0
- package/src/parse/lang/go.mjs +2 -0
- package/src/parse/lang/jvm.mjs +33 -0
- package/src/parse/lang/markdown.mjs +92 -0
- package/src/parse/lang/python.mjs +43 -0
- package/src/parse/lang/rust.mjs +215 -15
- package/src/parse/lang/sfc.mjs +126 -0
- package/src/parse/languages.mjs +9 -2
- package/src/query/check.mjs +49 -13
- package/src/query/facts.mjs +3 -2
- package/src/query/injection-spring.mjs +406 -0
- package/src/query/injection.mjs +174 -0
- package/src/query/intel.mjs +85 -15
- package/src/query/issue.mjs +156 -0
- package/src/query/orm.mjs +288 -0
- package/src/query/read.mjs +6 -1
- package/src/query/report.mjs +53 -0
- package/src/query/routes.mjs +142 -0
- package/src/query/showcase.mjs +59 -0
- package/src/query/textsearch.mjs +30 -9
- package/src/search/search.mjs +85 -17
- package/src/search/semantic.mjs +201 -0
- package/src/search/vectors.mjs +122 -0
- package/src/store/db.mjs +33 -4
- package/src/util/git.mjs +60 -11
- package/src/util/paths.mjs +10 -3
- package/vendor/grammars/README.md +47 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +4 -4
- /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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
37
|
-
with fixed arguments
|
|
38
|
-
|
|
39
|
-
repository's own git config could
|
|
40
|
-
clean/smudge/process filters, `diff.external`
|
|
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.
|
|
46
|
-
|
|
47
|
-
indexed or served. Paths given to the tools
|
|
48
|
-
`change_impact`) are resolved
|
|
49
|
-
`.git/` and `.graph-indexer/` are never served
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
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
|
|
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
|
|
package/bin/graph-indexer.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
package/src/cli/helper.mjs
CHANGED
|
@@ -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
|
|
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
|
+
}
|