@sdsrs/code-graph 0.111.0 → 0.112.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.
- package/README.md +39 -35
- package/bin/cli.js +5 -0
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/auto-update.js +62 -3
- package/claude-plugin/scripts/doctor.js +3 -3
- package/claude-plugin/scripts/launcher-install.js +14 -1
- package/claude-plugin/scripts/lifecycle.js +70 -16
- package/claude-plugin/scripts/npm-exec.js +7 -1
- package/claude-plugin/scripts/post-grep-inject.js +11 -9
- package/claude-plugin/scripts/pre-edit-guide.js +6 -2
- package/claude-plugin/scripts/pre-grep-guide.js +11 -9
- package/claude-plugin/scripts/session-init.js +20 -7
- package/claude-plugin/scripts/statusline-composite.js +8 -1
- package/claude-plugin/scripts/statusline.js +38 -9
- package/claude-plugin/scripts/tmp-dir.js +58 -1
- package/claude-plugin/scripts/user-prompt-context.js +46 -17
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -5,14 +5,16 @@ A high-performance code knowledge graph server implementing the [Model Context P
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Multi-language parsing** — Tree-sitter AST extraction across tiers of depth:
|
|
8
|
-
- **Full** (calls + imports + inheritance
|
|
8
|
+
- **Full** (calls + imports + inheritance): TypeScript/TSX, JavaScript, Go, Python, Rust, Java. HTTP route extraction additionally covers TypeScript/TSX + JavaScript (Express/Connect), Go (`net/http`), Python (Flask/FastAPI), and Rust axum (`.route()` chains with inline `.nest()` prefixes; named handlers only) — actix/rocket and Java Spring are not yet route-extracted
|
|
9
9
|
- **Smoke-tested** (calls + imports + inheritance): C#, Kotlin, Ruby, PHP, Swift, Dart
|
|
10
|
-
- **Limited** (functions + calls + `#include` imports + gtest test markers + C++ base-class inheritance
|
|
10
|
+
- **Limited** (functions + calls + `#include` imports + gtest test markers + C++ base-class inheritance + `Class::method` scope qualification): C, C++
|
|
11
11
|
- **Scripting**: Bash (functions + commands + `source`/`.` imports), Markdown (headings)
|
|
12
12
|
- **File-FTS only** (no AST symbol extraction): HTML, CSS, JSON
|
|
13
|
+
- **Test markers** — detected from the AST for Rust (`#[test]` / `#[cfg(test)]`), JavaScript/TypeScript/TSX (`describe`/`it`/`test` blocks and their callbacks) and C/C++ (gtest `TEST` macros). Go, Python, Java and the rest fall back to path/name heuristics (`tests/`, `src/test/java/`, `*_test.*`, `*.test.js`, `test_*`, `*Test`)
|
|
14
|
+
- **Value/type references** — a `references` relation for symbols that are used without being called, imported or inherited (path-qualified constants, type-position uses, functions passed as values): Rust, TypeScript/TSX, JavaScript, Python, Go, Java, C, C++
|
|
13
15
|
- **Semantic code search** — Hybrid BM25 full-text + vector semantic search with Reciprocal Rank Fusion (RRF), powered by sqlite-vec
|
|
14
16
|
- **Call graph traversal** — Recursive CTE queries to trace callers/callees with cycle detection
|
|
15
|
-
- **HTTP route tracing** — Map route paths to backend handler functions (Express, Flask/FastAPI, Go `net/http
|
|
17
|
+
- **HTTP route tracing** — Map route paths to backend handler functions (Express, Flask/FastAPI, Go `net/http`, Rust axum)
|
|
16
18
|
- **Dead code detection** — Find unreferenced symbols with smart Orphan/Exported-Unused classification
|
|
17
19
|
- **Impact analysis** — Determine the blast radius of code changes by tracing all dependents
|
|
18
20
|
- **Incremental indexing** — Merkle tree change detection with file system watcher for real-time updates. Smart event filtering skips metadata-only changes (chmod, xattr)
|
|
@@ -36,11 +38,11 @@ Combines BM25 full-text ranking (FTS5) with vector semantic similarity (sqlite-v
|
|
|
36
38
|
|
|
37
39
|
### Scope-Aware Relation Extraction
|
|
38
40
|
|
|
39
|
-
The parser doesn't just find function calls — it tracks them within their proper scope context. Extracts calls, imports, inheritance, interface implementations, exports, and HTTP route bindings. Same-file targets are preferred over cross-file matches to minimize false-positive edges.
|
|
41
|
+
The parser doesn't just find function calls — it tracks them within their proper scope context. Extracts calls, imports, inheritance, interface implementations, exports (ESM `export` **and** CommonJS `module.exports = { … }` / `exports.name = …`), value/type references, and HTTP route bindings. Same-file targets are preferred over cross-file matches to minimize false-positive edges.
|
|
40
42
|
|
|
41
43
|
### HTTP Request Flow Tracing
|
|
42
44
|
|
|
43
|
-
Unique to code-graph-mcp: trace from `GET /api/users` → route handler → service layer → database call in a single query. Supports Express, Flask/FastAPI, and
|
|
45
|
+
Unique to code-graph-mcp: trace from `GET /api/users` → route handler → service layer → database call in a single query. Supports Express/Connect, Flask/FastAPI, Go `net/http`, and Rust axum.
|
|
44
46
|
|
|
45
47
|
### Zero External Dependencies at Runtime
|
|
46
48
|
|
|
@@ -73,10 +75,10 @@ Real-world benchmarks comparing code-graph-mcp tools against traditional approac
|
|
|
73
75
|
| Project architecture overview | 5-8 calls | 1 call (`project_map`) | **~85%** |
|
|
74
76
|
| Find function by concept | 3-5 calls | 1 call (`semantic_code_search`) | **~75%** |
|
|
75
77
|
| Trace 2-level call chain | 8-15 calls | 1 call (`get_call_graph`) | **~90%** |
|
|
76
|
-
| Pre-change impact analysis | 10-20+ calls | 1 call (`
|
|
78
|
+
| Pre-change impact analysis | 10-20+ calls | 1 call (`get_ast_node` + `include_impact`) | **~95%** |
|
|
77
79
|
| Module structure & exports | 5+ calls | 1 call (`module_overview`) | **~80%** |
|
|
78
|
-
| File dependency mapping | 3-5 calls | 1 call (`
|
|
79
|
-
| Similar code detection | N/A | 1 call (`
|
|
80
|
+
| File dependency mapping | 3-5 calls | 1 call (`module_overview` + `include_deps`) | **~75%** |
|
|
81
|
+
| Similar code detection | N/A | 1 call (`get_ast_node` + `include_similar`) | **unique** |
|
|
80
82
|
|
|
81
83
|
### Overall Session Efficiency
|
|
82
84
|
|
|
@@ -160,9 +162,11 @@ Then reconnect the MCP server in Claude Code with `/mcp`.
|
|
|
160
162
|
Set it in the `env` block of `~/.claude/settings.json` (the environment of the
|
|
161
163
|
process hosting the MCP server), then reconnect with `/mcp`.
|
|
162
164
|
|
|
163
|
-
An update that keeps failing also stops
|
|
164
|
-
|
|
165
|
-
release is published
|
|
165
|
+
An update that keeps failing also stops hammering: after 5 failed attempts at
|
|
166
|
+
the *same* release the updater drops to one retry per day (and retries
|
|
167
|
+
immediately when a newer release is published), instead of re-downloading on
|
|
168
|
+
every session. While it is in that state the statusline shows `⚠ update stuck`
|
|
169
|
+
and `code-graph-mcp doctor` prints the manual update command.
|
|
166
170
|
|
|
167
171
|
#### Invited-memory mode (quieter prompts)
|
|
168
172
|
|
|
@@ -261,20 +265,19 @@ npm uninstall -g @sdsrs/code-graph
|
|
|
261
265
|
|
|
262
266
|
## MCP Tools
|
|
263
267
|
|
|
268
|
+
`tools/list` advertises exactly these seven. Several older niche tools were folded into flags on them, so one call now covers what used to take a separate tool:
|
|
269
|
+
|
|
264
270
|
| Tool | Description |
|
|
265
271
|
|------|-------------|
|
|
266
|
-
| `project_map` | Full project architecture: modules, dependencies, entry points, hot functions |
|
|
267
|
-
| `semantic_code_search` | Hybrid BM25 + vector
|
|
268
|
-
| `get_call_graph` | Trace upstream/downstream call chains for a function |
|
|
269
|
-
| `
|
|
270
|
-
| `
|
|
271
|
-
| `module_overview` | High-level overview of a module's structure and exports |
|
|
272
|
-
| `dependency_graph` | Visualize dependency relationships between modules. Supports `compact` mode |
|
|
273
|
-
| `find_similar_code` | Find semantically similar code via embeddings. Requires `symbol_name` or `node_id` |
|
|
274
|
-
| `get_ast_node` | Extract a specific code symbol with signature, body, and relations. Supports `compact` mode |
|
|
272
|
+
| `project_map` | Full project architecture: modules, dependencies, entry points, hot functions. `include_centrality` adds architectural chokepoints |
|
|
273
|
+
| `semantic_code_search` | Hybrid BM25 + vector search (RRF) for AST nodes. Supports `compact` mode |
|
|
274
|
+
| `get_call_graph` | Trace upstream/downstream call chains for a function. Pass `route_path='GET /api/x'` to trace an HTTP route → handler → downstream instead |
|
|
275
|
+
| `get_ast_node` | One symbol with signature, body and relations. `include_impact` adds the blast radius, `include_similar` embedding-similar nodes, `include_references` callers/callees. Supports `compact` mode |
|
|
276
|
+
| `module_overview` | Symbols in a directory or file, grouped by type and caller count. `include_dead` lists unreferenced symbols under the path; `include_deps` adds the file dependency graph (single-file paths only) |
|
|
275
277
|
| `ast_search` | Search AST nodes by text and/or structural filters (type, return type, params) |
|
|
276
|
-
| `find_references` | Find all references to a symbol (callers, importers, inheritors). Supports `compact` mode |
|
|
277
|
-
|
|
278
|
+
| `find_references` | Find all references to a symbol (callers, importers, inheritors, implementors, value/type references). Supports `compact` mode |
|
|
279
|
+
|
|
280
|
+
**Hidden aliases.** These names are not in `tools/list` but still dispatch via `tools/call`, so existing clients keep working: `trace_http_chain` / `find_http_route` (→ `get_call_graph` with `route_path`), `read_snippet` (→ `get_ast_node`), `dependency_graph`, `find_similar_code`, `find_dead_code`, plus the management tools `start_watch`, `stop_watch`, `get_index_status` and `rebuild_index`. `impact_analysis` is **removed** — calling it returns `Unknown tool`; use `get_ast_node` with `include_impact=true`, or the CLI's `impact --json` for the full report.
|
|
278
281
|
|
|
279
282
|
## CLI Commands
|
|
280
283
|
|
|
@@ -285,15 +288,15 @@ All tools are also available as CLI subcommands for shell scripts, hooks, and te
|
|
|
285
288
|
| `search <query>` | `semantic_code_search` | FTS5 search by concept |
|
|
286
289
|
| `ast-search [query]` | `ast_search` | Structural search with `--type`/`--returns`/`--params` filters |
|
|
287
290
|
| `callgraph <symbol>` | `get_call_graph` | Show call graph (callers/callees) |
|
|
288
|
-
| `impact <symbol>` | `
|
|
291
|
+
| `impact <symbol>` | `get_ast_node` (`include_impact=true`) | Impact analysis (callers, routes, risk level) |
|
|
289
292
|
| `show <symbol>` | `get_ast_node` | Show symbol details (code, type, signature) |
|
|
290
293
|
| `map` | `project_map` | Project architecture map |
|
|
291
294
|
| `overview <path>` | `module_overview` | Module symbols grouped by file and type |
|
|
292
|
-
| `deps <file>` | `
|
|
293
|
-
| `trace <route>` | `
|
|
294
|
-
| `similar <symbol>` | `
|
|
295
|
+
| `deps <file>` | `module_overview` (`include_deps=true`) | File-level dependency graph |
|
|
296
|
+
| `trace <route>` | `get_call_graph` (`route_path=…`) | Trace HTTP route → handler → downstream calls |
|
|
297
|
+
| `similar <symbol>` | `get_ast_node` (`include_similar=true`) | Find semantically similar code (requires embeddings) |
|
|
295
298
|
| `refs <symbol>` | `find_references` | Find all references to a symbol |
|
|
296
|
-
| `dead-code [path]` | `
|
|
299
|
+
| `dead-code [path]` | `module_overview` (`include_dead=true`) | Find unused code (orphans and exported-unused) |
|
|
297
300
|
| `grep <pattern>` | — | AST-context grep (ripgrep + containing function/class) |
|
|
298
301
|
| `incremental-index` | — | Run incremental index update (auto-creates DB if needed) |
|
|
299
302
|
| `health-check` | `get_index_status` | Query index status and freshness |
|
|
@@ -319,20 +322,20 @@ Available when installed as a Claude Code plugin:
|
|
|
319
322
|
|
|
320
323
|
| Language | Extensions | Relations Extracted |
|
|
321
324
|
|----------|-----------|-------------------|
|
|
322
|
-
| TypeScript | .ts, .tsx | calls, imports, exports, inherits, implements, routes_to |
|
|
323
|
-
| JavaScript | .js, .jsx, .mjs, .cjs | calls, imports, exports, inherits, routes_to |
|
|
324
|
-
| Go | .go | calls, imports, inherits, routes_to |
|
|
325
|
-
| Python | .py, .pyi | calls, imports, inherits, routes_to |
|
|
326
|
-
| Rust | .rs | calls, imports,
|
|
327
|
-
| Java | .java | calls, imports, inherits, implements |
|
|
325
|
+
| TypeScript | .ts, .tsx | calls, imports, exports, inherits, implements, routes_to, references |
|
|
326
|
+
| JavaScript | .js, .jsx, .mjs, .cjs | calls, imports, exports (ESM + CommonJS), inherits, routes_to, references |
|
|
327
|
+
| Go | .go | calls, imports, inherits, routes_to, references |
|
|
328
|
+
| Python | .py, .pyi | calls, imports, inherits, routes_to, references |
|
|
329
|
+
| Rust | .rs | calls, imports, implements, routes_to (axum), references |
|
|
330
|
+
| Java | .java | calls, imports, inherits, implements, references |
|
|
328
331
|
| C# | .cs | calls, imports, inherits, implements |
|
|
329
332
|
| Kotlin | .kt, .kts | calls, imports, inherits |
|
|
330
333
|
| Ruby | .rb | calls, imports, inherits |
|
|
331
334
|
| PHP | .php | calls, imports, inherits, implements |
|
|
332
335
|
| Swift | .swift | calls, imports, inherits |
|
|
333
336
|
| Dart | .dart | calls, imports, inherits, implements |
|
|
334
|
-
| C | .c, .h | calls, imports |
|
|
335
|
-
| C++ | .cpp, .cc, .cxx, .hpp | calls, imports, inherits |
|
|
337
|
+
| C | .c, .h | calls, imports, references |
|
|
338
|
+
| C++ | .cpp, .cc, .cxx, .hpp, .hh, .hxx | calls, imports, inherits, references |
|
|
336
339
|
| Bash | .sh, .bash | functions, commands, `source`/`.` imports |
|
|
337
340
|
| Markdown | .md, .mdx, .markdown | headings |
|
|
338
341
|
| HTML | .html, .htm | file-FTS only (no AST symbols) |
|
|
@@ -340,6 +343,7 @@ Available when installed as a Claude Code plugin:
|
|
|
340
343
|
| JSON | .json | file-FTS only (no AST symbols) |
|
|
341
344
|
|
|
342
345
|
**Known limitations:**
|
|
346
|
+
- **Rust has no `inherits` edges** — the language has no class inheritance, so `impl Trait for Type` is recorded as `implements` and an `inherits`-filtered query returns empty for Rust.
|
|
343
347
|
- **Kotlin/Swift interface conformance** is recorded as `inherits` (both use a single `: Type` grammar for base classes and protocols/interfaces), so `implements`-filtered queries return empty for these two languages.
|
|
344
348
|
- **Cross-file dead-code detection** may false-positive a type whose only cross-file reference sits beyond the 4096-byte stored-content cap per node (documented accepted limitation, v0.97.1).
|
|
345
349
|
|
package/bin/cli.js
CHANGED
|
@@ -131,9 +131,14 @@ if (!binary) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
// Spawn the binary, forwarding stdio for MCP JSON-RPC communication
|
|
134
|
+
// windowsHide as a literal rather than via claude-plugin/scripts/proc-opts:
|
|
135
|
+
// this file is the npm package's bin entry and ships in tarballs that may not
|
|
136
|
+
// carry claude-plugin/. Same meaning — CREATE_NO_WINDOW, so a console-less
|
|
137
|
+
// parent (the MCP client) does not flash a window; inherited stdio still works.
|
|
134
138
|
const child = spawn(binary, process.argv.slice(2), {
|
|
135
139
|
stdio: "inherit",
|
|
136
140
|
env: process.env,
|
|
141
|
+
windowsHide: true,
|
|
137
142
|
});
|
|
138
143
|
|
|
139
144
|
child.on("error", (err) => {
|
|
@@ -57,6 +57,20 @@ const FETCH_TIMEOUT_MS = 3000;
|
|
|
57
57
|
// STUCK_UPDATE_ATTEMPTS (drift-guarded in statusline.test.js) so the moment the
|
|
58
58
|
// updater gives up is the moment the statusline stops promising "↻ updating".
|
|
59
59
|
const MAX_UPDATE_ATTEMPTS = 5;
|
|
60
|
+
// ...but suspension is not permanent. The cap alone assumed every repeated
|
|
61
|
+
// failure is permanent, and the causes are not distinguishable at the failure
|
|
62
|
+
// site: a briefly-missing `.sha256` sidecar, a captive portal, a temporarily
|
|
63
|
+
// full disk burn the budget just as fast as a broken tar — and SessionStart
|
|
64
|
+
// forces a check with only a 2-minute floor, so ~5 Claude Code restarts in ~10
|
|
65
|
+
// minutes exhaust it. Before this, recovery required a NEWER release (or
|
|
66
|
+
// hand-deleting update-state.json, which nothing tells the user to do), so a
|
|
67
|
+
// ten-minute outage could park the updater for days. One retry per day keeps
|
|
68
|
+
// the per-session treadmill dead while guaranteeing self-heal.
|
|
69
|
+
//
|
|
70
|
+
// Note the retry can NOT be keyed to `--force`: session-init passes --force on
|
|
71
|
+
// every session start, so re-arming there would restore the exact treadmill
|
|
72
|
+
// this cap exists to stop.
|
|
73
|
+
const SUSPENSION_RETRY_MS = 24 * 60 * 60 * 1000;
|
|
60
74
|
|
|
61
75
|
function isSilentMode(argv = process.argv.slice(2), env = process.env) {
|
|
62
76
|
return argv.includes('--silent') || env.CODE_GRAPH_AUTO_UPDATE_SILENT === '1';
|
|
@@ -880,8 +894,17 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
880
894
|
// always starts with a full budget. The counter used to be unscoped, which
|
|
881
895
|
// only mattered because nothing ever read it: the download chain re-ran on
|
|
882
896
|
// every single session no matter how many times it had already failed.
|
|
883
|
-
const
|
|
884
|
-
|
|
897
|
+
const sameTarget = state.latestVersion === latest.version;
|
|
898
|
+
const attempts = sameTarget ? (state.updateAttempts || 0) : 0;
|
|
899
|
+
// A suspended release gets one retry per day (SUSPENSION_RETRY_MS), so a
|
|
900
|
+
// transient cause — sidecar blip, captive portal, briefly-full disk —
|
|
901
|
+
// heals itself instead of parking the updater until the next release.
|
|
902
|
+
// Spend the retry by entering the attempt path with the budget one short:
|
|
903
|
+
// if it fails again it re-suspends immediately (and re-stamps the clock),
|
|
904
|
+
// costing at most one download per day rather than one per session.
|
|
905
|
+
const suspendedAt = sameTarget && state.suspendedAt ? Date.parse(state.suspendedAt) : NaN;
|
|
906
|
+
const retryDue = Number.isFinite(suspendedAt) && (Date.now() - suspendedAt) >= SUSPENSION_RETRY_MS;
|
|
907
|
+
if (attempts >= MAX_UPDATE_ATTEMPTS && !retryDue) {
|
|
885
908
|
// Suspended — check-only from here until a newer release moves the
|
|
886
909
|
// target. The one thing still worth attempting is a MISSING cached
|
|
887
910
|
// binary: without it the MCP server has no engine at all, so that
|
|
@@ -895,19 +918,44 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
895
918
|
latestVersion: latest.version,
|
|
896
919
|
updateAvailable: true,
|
|
897
920
|
updateAttempts: attempts,
|
|
921
|
+
// Stamp on ENTRY to suspension, then leave it alone: the retry clock
|
|
922
|
+
// must measure time since we gave up, not time since the last check
|
|
923
|
+
// (which every session would reset, making the retry unreachable).
|
|
924
|
+
suspendedAt: (sameTarget && state.suspendedAt) || new Date().toISOString(),
|
|
898
925
|
rateLimited: false,
|
|
899
926
|
binaryUpdated: healedMissing || state.binaryUpdated,
|
|
900
927
|
});
|
|
901
928
|
console.error(
|
|
902
929
|
`[code-graph] Auto-update to v${latest.version} suspended after ${attempts} failed attempts on this machine. ` +
|
|
903
930
|
'Update manually (`/plugin update code-graph-mcp`, or `npm install -g @sdsrs/code-graph`) or run `code-graph-mcp doctor`. ' +
|
|
904
|
-
'
|
|
931
|
+
'Retried automatically once a day, and immediately when a newer release is published.'
|
|
905
932
|
);
|
|
906
933
|
return { updateAvailable: true, suspended: true, from: installedVersion, to: latest.version };
|
|
907
934
|
}
|
|
908
935
|
const result = await downloadAndInstall(latest);
|
|
909
936
|
const success = result.pluginUpdated;
|
|
937
|
+
// Suspension clock. It restarts when the daily retry is spent and fails,
|
|
938
|
+
// which is what keeps `retryDue` from staying true and turning the retry
|
|
939
|
+
// back into a per-session treadmill; it clears on success and on a new
|
|
940
|
+
// target version, so a stale stamp from the previous release cannot make
|
|
941
|
+
// the next one look instantly retry-due.
|
|
942
|
+
let nextSuspendedAt;
|
|
943
|
+
if (success) nextSuspendedAt = null;
|
|
944
|
+
else if (retryDue) nextSuspendedAt = new Date().toISOString();
|
|
945
|
+
else if (!sameTarget) nextSuspendedAt = null;
|
|
946
|
+
else nextSuspendedAt = state.suspendedAt || null;
|
|
910
947
|
const newState = {
|
|
948
|
+
// Carry the prior state forward. Every OTHER saveState in this function
|
|
949
|
+
// spreads `...state`; this one rebuilt from scratch, so any key it does
|
|
950
|
+
// not name was dropped on every update check that found a new release.
|
|
951
|
+
// The keys that matter are selfHealGlobalPkgs' — globalPkgHealAttempts /
|
|
952
|
+
// globalPkgHealVersion — because that function returns `{}` once the
|
|
953
|
+
// attempt cap is hit, meaning the spread below contributes nothing and
|
|
954
|
+
// the counter reset to zero. A capped-out global heal therefore got a
|
|
955
|
+
// fresh budget on every release, which is precisely the retry treadmill
|
|
956
|
+
// the cap exists to stop. Keys this object sets are overwritten below;
|
|
957
|
+
// nothing stale leaks through.
|
|
958
|
+
...state,
|
|
911
959
|
lastCheck: new Date().toISOString(),
|
|
912
960
|
installedVersion: success ? latest.version : installedVersion,
|
|
913
961
|
latestVersion: latest.version,
|
|
@@ -918,6 +966,7 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
918
966
|
// forever, asserting a self-heal that never happens. The statusline stops
|
|
919
967
|
// trusting it past STUCK_UPDATE_ATTEMPTS; success resets to 0.
|
|
920
968
|
updateAttempts: success ? 0 : attempts + 1,
|
|
969
|
+
suspendedAt: nextSuspendedAt,
|
|
921
970
|
lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
|
|
922
971
|
rateLimited: false,
|
|
923
972
|
binaryUpdated: result.binaryUpdated,
|
|
@@ -955,6 +1004,16 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
955
1004
|
lastCheck: new Date().toISOString(),
|
|
956
1005
|
latestVersion: latest.version,
|
|
957
1006
|
updateAvailable: false,
|
|
1007
|
+
// Reaching here means the installed shell IS the latest release, so any
|
|
1008
|
+
// failure record describes an update that is no longer pending. Leaving it
|
|
1009
|
+
// set kept `doctor` warning "vX failed to install 5× — auto-retry
|
|
1010
|
+
// throttled" about a version already installed, and left a suspension
|
|
1011
|
+
// stamp that the next release then had to age out. The common way to get
|
|
1012
|
+
// here from a suspended state is the manual route the suspension notice
|
|
1013
|
+
// itself recommends (`npm install -g` / `/plugin update`) — the updater
|
|
1014
|
+
// has to notice that its advice was taken.
|
|
1015
|
+
updateAttempts: 0,
|
|
1016
|
+
suspendedAt: null,
|
|
958
1017
|
rateLimited: false,
|
|
959
1018
|
binaryUpdated: selfHealedBinary || state.binaryUpdated,
|
|
960
1019
|
...globalHeal,
|
|
@@ -214,7 +214,7 @@ function runDiagnostics({ checkOnly = false } = {}) {
|
|
|
214
214
|
results.push({
|
|
215
215
|
name: 'Auto-update',
|
|
216
216
|
status: 'warn',
|
|
217
|
-
detail: `v${state.latestVersion} failed to install ${attempts}× — auto-retry
|
|
217
|
+
detail: `v${state.latestVersion} failed to install ${attempts}× — auto-retry throttled to once a day. `
|
|
218
218
|
+ 'Update manually: `npm install -g @sdsrs/code-graph` (or `/plugin update code-graph-mcp`)',
|
|
219
219
|
});
|
|
220
220
|
} else if (state && state.updateAvailable && state.binaryUpdated === false) {
|
|
@@ -482,9 +482,9 @@ function relicRepairGuard({ log = console.log, relic = undefined } = {}) {
|
|
|
482
482
|
function detectEmbedModel(binary, run = execFileSync) {
|
|
483
483
|
if (!binary) return null;
|
|
484
484
|
try {
|
|
485
|
-
const out = run(binary, ['health-check', '--json'], {
|
|
485
|
+
const out = run(binary, ['health-check', '--json'], hidden({
|
|
486
486
|
timeout: 10000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
487
|
-
});
|
|
487
|
+
}));
|
|
488
488
|
return JSON.parse(out).model_available === true;
|
|
489
489
|
} catch { return null; }
|
|
490
490
|
}
|
|
@@ -129,7 +129,20 @@ function installBinaryInBackground({
|
|
|
129
129
|
// `npm.cmd` (needs a shell) and passing `args` alongside `shell: true` is
|
|
130
130
|
// DEP0190 — runtime-deprecated in Node 24, and unescaped. It pre-quotes the
|
|
131
131
|
// whole command into `file` with empty `args`, and carries windowsHide.
|
|
132
|
-
|
|
132
|
+
// try/catch because quoteCmdArg THROWS on an argument it cannot quote, and
|
|
133
|
+
// `version` is read from plugin.json — our own file, but a parse of somebody
|
|
134
|
+
// else's disk. Uncaught, the throw escapes mcp-launcher.js and kills the MCP
|
|
135
|
+
// server AFTER the 0-tool stub is already serving, turning a bad version
|
|
136
|
+
// string into a dead server instead of a failed install. Every sibling call
|
|
137
|
+
// in auto-update.js is already inside a try.
|
|
138
|
+
let npm;
|
|
139
|
+
try {
|
|
140
|
+
npm = npmInvocation(['install', '-g', `@sdsrs/code-graph@${version}`]);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
process.stderr.write(`[code-graph] cannot build the npm install command: ${e.message}\n`);
|
|
143
|
+
finish(onFailed);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
133
146
|
runStep(npm.file, npm.args, npmTimeoutMs, '[code-graph][npm]', spawnFn, (npmExit) => {
|
|
134
147
|
if (resolved()) {
|
|
135
148
|
if (npmExit === 0 && recordGlobalInstall) {
|
|
@@ -114,7 +114,13 @@ function readJsonResult(filePath) {
|
|
|
114
114
|
|
|
115
115
|
// Lenient reader kept exactly as-is for its 20+ callers (manifests, registries,
|
|
116
116
|
// plugin.json …) where "absent" and "unreadable" genuinely mean the same thing.
|
|
117
|
-
//
|
|
117
|
+
// A caller that will WRITE settings.json back must use readSettingsForWrite()
|
|
118
|
+
// + tryWriteSettings() instead — the pair that detects the lossy/corrupt cases
|
|
119
|
+
// and preserves the original bytes. Reading settings.json with THIS function is
|
|
120
|
+
// fine as long as nothing is written back (isPluginInactive, syncLifecycleConfig's
|
|
121
|
+
// self-heal probe); the destructive combination is `readJson(settingsPath())`
|
|
122
|
+
// followed by a write, which is how cleanupDisabledStatusline and uninstall
|
|
123
|
+
// destroyed non-UTF-8 bytes for four releases after the detector landed.
|
|
118
124
|
function readJson(filePath) {
|
|
119
125
|
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
|
|
120
126
|
}
|
|
@@ -158,9 +164,18 @@ function backupCorruptFile(filePath, raw) {
|
|
|
158
164
|
// printing "Hooks ✅ 1 issue(s) auto-repaired" for a run that moved the user's
|
|
159
165
|
// model / env / permissions into a `.corrupt-*` file it never mentioned. A
|
|
160
166
|
// destructive repair has to be reported as one, with the path to get it back.
|
|
161
|
-
|
|
167
|
+
//
|
|
168
|
+
// `pre` lets a caller that ALREADY probed the file with readJsonResult reuse
|
|
169
|
+
// that result instead of reading twice. It matters for more than speed: the
|
|
170
|
+
// backup this function may take is a side effect, and a caller which only
|
|
171
|
+
// writes conditionally (cleanupDisabledStatusline runs on every statusline
|
|
172
|
+
// render) must be able to probe first and pay the backup only on the write
|
|
173
|
+
// path — otherwise a lossy settings.json accumulates one `.corrupt-*` copy per
|
|
174
|
+
// prompt. Passing `pre` also keeps the returned `settings` object IDENTICAL to
|
|
175
|
+
// the one the caller already mutated.
|
|
176
|
+
function readSettingsForWrite(pre) {
|
|
162
177
|
const p = settingsPath();
|
|
163
|
-
const res = readJsonResult(p);
|
|
178
|
+
const res = pre || readJsonResult(p);
|
|
164
179
|
if (res.value && res.lossy) {
|
|
165
180
|
// Usable JSON whose bytes will not survive our rewrite (see readJsonResult).
|
|
166
181
|
// Preserve the true bytes, then proceed with the parsed value — refusing
|
|
@@ -413,7 +428,11 @@ function detachStatuslineIntegration(settings, { compositeDoomed = true } = {})
|
|
|
413
428
|
}
|
|
414
429
|
|
|
415
430
|
function cleanupDisabledStatusline() {
|
|
416
|
-
|
|
431
|
+
// Probe without side effects first — this runs on every statusline render and
|
|
432
|
+
// usually finds nothing to do. A corrupt/unreadable file yields no value and
|
|
433
|
+
// is left strictly alone (same outcome the old lenient readJson produced).
|
|
434
|
+
const probe = readJsonResult(settingsPath());
|
|
435
|
+
const settings = probe.value;
|
|
417
436
|
if (!settings || !isPluginInactive(settings)) {
|
|
418
437
|
return { cleaned: false, settingsChanged: false };
|
|
419
438
|
}
|
|
@@ -425,7 +444,15 @@ function cleanupDisabledStatusline() {
|
|
|
425
444
|
let settingsChanged = detachStatuslineIntegration(settings, { compositeDoomed: uninstalled });
|
|
426
445
|
if (removeHooksFromSettings(settings)) settingsChanged = true;
|
|
427
446
|
if (settingsChanged) {
|
|
428
|
-
|
|
447
|
+
// Now that a write is certain, take the guarded path: readSettingsForWrite
|
|
448
|
+
// preserves bytes JSON.stringify would destroy (the lossy-UTF8 case) and
|
|
449
|
+
// returns null when it could not, and tryWriteSettings turns a read-only
|
|
450
|
+
// ~/.claude into a diagnosed no-op instead of an uncaught EACCES — this
|
|
451
|
+
// function is called at the top of statusline.js, where a throw blanks the
|
|
452
|
+
// user's status line. `guarded` is the same object we just mutated.
|
|
453
|
+
const { settings: guarded } = readSettingsForWrite(probe);
|
|
454
|
+
if (!guarded) return { cleaned: false, settingsChanged: false };
|
|
455
|
+
if (tryWriteSettings(guarded)) settingsChanged = false;
|
|
429
456
|
}
|
|
430
457
|
|
|
431
458
|
// Genuine uninstall (not a temporary disable): reclaim ~/.cache/code-graph
|
|
@@ -1071,7 +1098,13 @@ function defaultRunNpm(args) {
|
|
|
1071
1098
|
}
|
|
1072
1099
|
|
|
1073
1100
|
function uninstall({ purgeGlobal = false, unadoptAll = false, runNpm = defaultRunNpm, scanGlobalPkgs = installedGlobalPkgs } = {}) {
|
|
1074
|
-
|
|
1101
|
+
// Same guarded pair as install()/update(): probe without side effects, and
|
|
1102
|
+
// only take the backup + write path once there is something to write. Reading
|
|
1103
|
+
// this file with the lenient readJson and writing it back raw is what
|
|
1104
|
+
// destroyed non-UTF-8 bytes here — a teardown has even less license to lose
|
|
1105
|
+
// the user's model / env / permissions than an install does.
|
|
1106
|
+
const probe = readJsonResult(settingsPath());
|
|
1107
|
+
const settings = probe.value;
|
|
1075
1108
|
let settingsChanged = false;
|
|
1076
1109
|
|
|
1077
1110
|
if (settings) {
|
|
@@ -1097,7 +1130,8 @@ function uninstall({ purgeGlobal = false, unadoptAll = false, runNpm = defaultRu
|
|
|
1097
1130
|
|
|
1098
1131
|
// 4. Write settings if changed
|
|
1099
1132
|
if (settingsChanged) {
|
|
1100
|
-
|
|
1133
|
+
const { settings: guarded } = readSettingsForWrite(probe);
|
|
1134
|
+
if (!guarded || tryWriteSettings(guarded)) settingsChanged = false;
|
|
1101
1135
|
}
|
|
1102
1136
|
}
|
|
1103
1137
|
|
|
@@ -1155,6 +1189,14 @@ function uninstall({ purgeGlobal = false, unadoptAll = false, runNpm = defaultRu
|
|
|
1155
1189
|
// 6. Remove cache directory
|
|
1156
1190
|
try { fs.rmSync(CACHE_DIR, { recursive: true, force: true }); } catch { /* ok */ }
|
|
1157
1191
|
|
|
1192
|
+
// 6.5. The shared tmp dir (cooldown flags, read-fanout state, interrupted
|
|
1193
|
+
// `update-*` staging). Nothing in it outlives an uninstall, and the periodic
|
|
1194
|
+
// prune that keeps it bounded stops running the moment the hooks are gone —
|
|
1195
|
+
// so without this it is residue with no remaining owner.
|
|
1196
|
+
try {
|
|
1197
|
+
fs.rmSync(require('./tmp-dir').CG_TMP_DIR, { recursive: true, force: true });
|
|
1198
|
+
} catch { /* ok */ }
|
|
1199
|
+
|
|
1158
1200
|
// 7. Remove plugin files from cache (all known paths, including parent dirs)
|
|
1159
1201
|
const cacheRoot = pluginsCacheDir();
|
|
1160
1202
|
const pluginCacheDirs = [
|
|
@@ -1354,11 +1396,22 @@ function scanForBrokenPaths() {
|
|
|
1354
1396
|
const settings = settingsRead.value || {};
|
|
1355
1397
|
const issues = [];
|
|
1356
1398
|
|
|
1399
|
+
// Every path extraction below goes through hookCmdScript, the same parser
|
|
1400
|
+
// compositeSlotIsStale and surveyHookCoverage use. These three sites each had
|
|
1401
|
+
// their own inline `/node\s+"([^"]+)"/`, which requires the literal word
|
|
1402
|
+
// `node` followed by whitespace — so a command naming the interpreter by
|
|
1403
|
+
// absolute path, the spelling a Windows install produces
|
|
1404
|
+
// (`"C:\Program Files\nodejs\node.exe" "C:\…\script.js"`), matched nothing.
|
|
1405
|
+
// A path this scan cannot READ is reported as no path at all, i.e. as healthy,
|
|
1406
|
+
// which is the failure mode the whole function exists to prevent. Six sites,
|
|
1407
|
+
// two spellings, one of them blind: collapse to one.
|
|
1408
|
+
const scriptOf = (cmd) => hookCmdScript(cmd);
|
|
1409
|
+
|
|
1357
1410
|
// Check statusLine path
|
|
1358
1411
|
if (isOurComposite(settings)) {
|
|
1359
|
-
const
|
|
1360
|
-
if (
|
|
1361
|
-
issues.push({ type: 'statusLine', path:
|
|
1412
|
+
const script = scriptOf(settings.statusLine.command);
|
|
1413
|
+
if (script && !fs.existsSync(script)) {
|
|
1414
|
+
issues.push({ type: 'statusLine', path: script });
|
|
1362
1415
|
}
|
|
1363
1416
|
}
|
|
1364
1417
|
|
|
@@ -1369,9 +1422,9 @@ function scanForBrokenPaths() {
|
|
|
1369
1422
|
for (const entry of entries) {
|
|
1370
1423
|
if (!isOurHookEntry(entry) || !entry.hooks) continue;
|
|
1371
1424
|
for (const h of entry.hooks) {
|
|
1372
|
-
const
|
|
1373
|
-
if (
|
|
1374
|
-
issues.push({ type: 'hook', event, path:
|
|
1425
|
+
const script = h.command && scriptOf(h.command);
|
|
1426
|
+
if (script && !fs.existsSync(script)) {
|
|
1427
|
+
issues.push({ type: 'hook', event, path: script });
|
|
1375
1428
|
}
|
|
1376
1429
|
}
|
|
1377
1430
|
}
|
|
@@ -1382,9 +1435,9 @@ function scanForBrokenPaths() {
|
|
|
1382
1435
|
const registry = readRegistry();
|
|
1383
1436
|
for (const provider of registry) {
|
|
1384
1437
|
if (provider.id === '_previous') continue;
|
|
1385
|
-
const
|
|
1386
|
-
if (
|
|
1387
|
-
issues.push({ type: 'registry', id: provider.id, path:
|
|
1438
|
+
const script = provider.command && scriptOf(provider.command);
|
|
1439
|
+
if (script && !fs.existsSync(script)) {
|
|
1440
|
+
issues.push({ type: 'registry', id: provider.id, path: script });
|
|
1388
1441
|
}
|
|
1389
1442
|
}
|
|
1390
1443
|
|
|
@@ -1480,6 +1533,7 @@ module.exports = {
|
|
|
1480
1533
|
removeHooksFromSettings, isOurHookEntry,
|
|
1481
1534
|
registerHooksToSettings, buildSettingsHookEntries, // v0.32.0
|
|
1482
1535
|
surveyHookCoverage, compositeCommand, compositeSlotIsStale, // v0.49.1 — version-aware self-heal
|
|
1536
|
+
hookCmdScript, // the ONE hook-command path parser (session-init reuses it)
|
|
1483
1537
|
cacheDirVersion, // exported for the separator-agnostic test
|
|
1484
1538
|
|
|
1485
1539
|
verifyHooksFire, defaultHookFireProbes, // v0.67.0 — firing self-test
|
|
@@ -30,7 +30,13 @@ function quoteCmdArg(arg) {
|
|
|
30
30
|
if (/["%!\r\n]/.test(s)) {
|
|
31
31
|
throw new Error(`npm argument cannot be safely quoted for cmd.exe: ${JSON.stringify(s)}`);
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// Double a trailing run of backslashes. cmd.exe itself does not treat `\` as
|
|
34
|
+
// an escape, but the RECEIVING program's MSVCRT argv parser does: `"C:\x\"`
|
|
35
|
+
// reads the final `\"` as an escaped quote and swallows the rest of the
|
|
36
|
+
// command line into that argument. Only reachable for path-shaped args, which
|
|
37
|
+
// no current call site passes — but this helper reads as general-purpose.
|
|
38
|
+
const trailing = /\\+$/.exec(s);
|
|
39
|
+
return `"${trailing ? s + trailing[0] : s}"`;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
const fs = require('fs');
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const crypto = require('crypto');
|
|
26
|
-
const { cgTmpDir } = require('./tmp-dir');
|
|
26
|
+
const { cgTmpDir, cwdHash } = require('./tmp-dir');
|
|
27
27
|
const { recordRecommendation } = require('./recommendation-log');
|
|
28
28
|
const { runGrepAnswer, runShowAnswer, runCallgraphAnswer, sanitizeSearchPath } = require('./cg-answer');
|
|
29
29
|
const { emitPostToolContext } = require('./hook-emit');
|
|
@@ -217,18 +217,20 @@ function commandHash(cmd) {
|
|
|
217
217
|
return crypto.createHash('sha1').update(String(cmd)).digest('hex').slice(0, 12);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
// Project-scoped for the same reason as pre-grep-guide's (see cwdHash in
|
|
221
|
+
// tmp-dir.js): one shared tmp dir means an un-scoped flag is machine-global.
|
|
222
|
+
function flagPath(cmd, cwd = process.cwd()) {
|
|
223
|
+
return path.join(cgTmpDir(), `.code-graph-postinject-${cwdHash(cwd)}-${commandHash(cmd)}`);
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
function isOnCooldown(cmd, now = Date.now(), windowMs = 60000) {
|
|
226
|
+
function isOnCooldown(cmd, now = Date.now(), windowMs = 60000, cwd = process.cwd()) {
|
|
225
227
|
try {
|
|
226
|
-
return now - fs.statSync(flagPath(cmd)).mtimeMs < windowMs;
|
|
228
|
+
return now - fs.statSync(flagPath(cmd, cwd)).mtimeMs < windowMs;
|
|
227
229
|
} catch { return false; }
|
|
228
230
|
}
|
|
229
231
|
|
|
230
|
-
function markCooldown(cmd) {
|
|
231
|
-
try { fs.writeFileSync(flagPath(cmd), ''); } catch { /* ok */ }
|
|
232
|
+
function markCooldown(cmd, cwd = process.cwd()) {
|
|
233
|
+
try { fs.writeFileSync(flagPath(cmd, cwd), ''); } catch { /* ok */ }
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
// --- Main execution ---
|
|
@@ -259,8 +261,8 @@ function runMain() {
|
|
|
259
261
|
const found = findFoldableGrepSegment(cmd);
|
|
260
262
|
if (!found) return;
|
|
261
263
|
|
|
262
|
-
if (isOnCooldown(rawCmd)) return;
|
|
263
|
-
markCooldown(rawCmd);
|
|
264
|
+
if (isOnCooldown(rawCmd, Date.now(), 60000, root)) return;
|
|
265
|
+
markCooldown(rawCmd, root);
|
|
264
266
|
|
|
265
267
|
const { segment, block } = found;
|
|
266
268
|
// Run the answer exactly like the deny path.
|
|
@@ -10,7 +10,7 @@ const { execFileSync } = require('child_process');
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { findBinary } = require('./find-binary');
|
|
13
|
-
const { cgTmpDir } = require('./tmp-dir');
|
|
13
|
+
const { cgTmpDir, cwdHash } = require('./tmp-dir');
|
|
14
14
|
const { resolveProjectRoot } = require('./project-root');
|
|
15
15
|
const { recordRecommendation } = require('./recommendation-log');
|
|
16
16
|
const { formatCoveringTests } = require('./covering-tests');
|
|
@@ -118,7 +118,11 @@ function isCommonKeyword(s) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// --- Per-symbol cooldown: 2 minutes ---
|
|
121
|
-
|
|
121
|
+
// Project-scoped (see cwdHash in tmp-dir.js). A symbol name is the LEAST
|
|
122
|
+
// project-unique key there is — `main`, `run`, `new`, `parse` collide across
|
|
123
|
+
// every repo on the machine, so editing `parse` in one project suppressed the
|
|
124
|
+
// impact push for a completely different `parse` in another for two minutes.
|
|
125
|
+
const cooldownFile = path.join(cgTmpDir(), `.cg-impact-${cwdHash(cwd)}-${symbol}`);
|
|
122
126
|
try {
|
|
123
127
|
if (Date.now() - fs.statSync(cooldownFile).mtimeMs < 120000) process.exit(0);
|
|
124
128
|
} catch { /* first time for this symbol */ }
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
const fs = require('fs');
|
|
40
40
|
const path = require('path');
|
|
41
41
|
const crypto = require('crypto');
|
|
42
|
-
const { cgTmpDir } = require('./tmp-dir');
|
|
42
|
+
const { cgTmpDir, cwdHash } = require('./tmp-dir');
|
|
43
43
|
const { recordRecommendation } = require('./recommendation-log');
|
|
44
44
|
const { runGrepAnswer, runShowAnswer, sanitizeSearchPath } = require('./cg-answer');
|
|
45
45
|
|
|
@@ -513,18 +513,20 @@ function commandHash(cmd) {
|
|
|
513
513
|
return crypto.createHash('sha1').update(cmd).digest('hex').slice(0, 12);
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
|
|
517
|
-
|
|
516
|
+
// Project-scoped: the same `grep -rn "foo" src/` in two repos is two different
|
|
517
|
+
// questions and must not share one 60s cooldown (see cwdHash in tmp-dir.js).
|
|
518
|
+
function flagPath(cmd, cwd = process.cwd()) {
|
|
519
|
+
return path.join(cgTmpDir(), `.code-graph-bash-${cwdHash(cwd)}-${commandHash(cmd)}`);
|
|
518
520
|
}
|
|
519
521
|
|
|
520
|
-
function isOnCooldown(cmd, now = Date.now(), windowMs = 60000) {
|
|
522
|
+
function isOnCooldown(cmd, now = Date.now(), windowMs = 60000, cwd = process.cwd()) {
|
|
521
523
|
try {
|
|
522
|
-
return now - fs.statSync(flagPath(cmd)).mtimeMs < windowMs;
|
|
524
|
+
return now - fs.statSync(flagPath(cmd, cwd)).mtimeMs < windowMs;
|
|
523
525
|
} catch { return false; }
|
|
524
526
|
}
|
|
525
527
|
|
|
526
|
-
function markCooldown(cmd) {
|
|
527
|
-
try { fs.writeFileSync(flagPath(cmd), ''); } catch { /* ok */ }
|
|
528
|
+
function markCooldown(cmd, cwd = process.cwd()) {
|
|
529
|
+
try { fs.writeFileSync(flagPath(cmd, cwd), ''); } catch { /* ok */ }
|
|
528
530
|
}
|
|
529
531
|
|
|
530
532
|
function buildHint() {
|
|
@@ -720,7 +722,7 @@ function runMain() {
|
|
|
720
722
|
return;
|
|
721
723
|
}
|
|
722
724
|
|
|
723
|
-
if (isOnCooldown(rawCmd)) {
|
|
725
|
+
if (isOnCooldown(rawCmd, Date.now(), 60000, root)) {
|
|
724
726
|
// Outcome proxy: a source grep re-issued within the cooldown window runs
|
|
725
727
|
// silently (no deny/hint). Record it so `stats` sees the model's grep
|
|
726
728
|
// fan-out — especially a re-grep right after cg answered the same query.
|
|
@@ -728,7 +730,7 @@ function runMain() {
|
|
|
728
730
|
return;
|
|
729
731
|
}
|
|
730
732
|
|
|
731
|
-
markCooldown(rawCmd);
|
|
733
|
+
markCooldown(rawCmd, root);
|
|
732
734
|
|
|
733
735
|
const block = isBlockDisabled() ? null : classifyBlock(cmd);
|
|
734
736
|
if (block) {
|
|
@@ -6,7 +6,7 @@ const fs = require('fs');
|
|
|
6
6
|
const {
|
|
7
7
|
install, update, readManifest, getPluginVersion, checkScopeConflict,
|
|
8
8
|
cleanupDisabledStatusline, isPluginInactive, isPluginUninstalled, removeCacheResidue,
|
|
9
|
-
readJson, CACHE_DIR, settingsPath, isStaleRelicContext,
|
|
9
|
+
readJson, CACHE_DIR, settingsPath, isStaleRelicContext, hookCmdScript,
|
|
10
10
|
} = require('./lifecycle');
|
|
11
11
|
const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
|
|
12
12
|
const { maybeAutoAdopt, isAdopted, unadopt } = require('./adopt');
|
|
@@ -251,9 +251,13 @@ function syncLifecycleConfig() {
|
|
|
251
251
|
installReporting();
|
|
252
252
|
return 'self-healed';
|
|
253
253
|
}
|
|
254
|
-
// Also self-heal if composite path points to a non-existent script (path
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
// Also self-heal if composite path points to a non-existent script (path
|
|
255
|
+
// pollution). hookCmdScript, not another inline `/node\s+"…"/`: that spelling
|
|
256
|
+
// cannot read a command whose interpreter is an absolute path
|
|
257
|
+
// (`"C:\Program Files\nodejs\node.exe" "…\statusline-composite.js"`), and an
|
|
258
|
+
// unreadable command silently reads as a healthy one.
|
|
259
|
+
const compositeScript = hookCmdScript(settings.statusLine.command);
|
|
260
|
+
if (compositeScript && !fs.existsSync(compositeScript)) {
|
|
257
261
|
installReporting();
|
|
258
262
|
return 'self-healed-bad-path';
|
|
259
263
|
}
|
|
@@ -282,8 +286,8 @@ function syncLifecycleConfig() {
|
|
|
282
286
|
for (const entry of entries) {
|
|
283
287
|
if (!entry.hooks) continue;
|
|
284
288
|
for (const h of entry.hooks) {
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
289
|
+
const script = h.command && hookCmdScript(h.command);
|
|
290
|
+
if (script && script.includes('code-graph') && !fs.existsSync(script)) {
|
|
287
291
|
installReporting();
|
|
288
292
|
return 'self-healed-bad-hook';
|
|
289
293
|
}
|
|
@@ -509,13 +513,22 @@ function consistencyCheck(binary) {
|
|
|
509
513
|
}
|
|
510
514
|
|
|
511
515
|
function runSessionInit({ source } = {}) {
|
|
516
|
+
// GC the shared tmp dir before anything else, so it happens even on the
|
|
517
|
+
// inactive / non-project early returns below — those sessions still wrote
|
|
518
|
+
// cooldown flags on the way in. Cheap (one readdir + a stat per entry) and
|
|
519
|
+
// fully swallowed: reclaiming disk must never be able to fail a SessionStart.
|
|
520
|
+
try { require('./tmp-dir').pruneCgTmp(); } catch { /* best-effort GC */ }
|
|
521
|
+
|
|
512
522
|
if (isPluginInactive()) {
|
|
513
523
|
// Capture the uninstalled-vs-disabled verdict BEFORE cleanupDisabledStatusline()
|
|
514
524
|
// runs — it removes our composite + registry entry, which are the very signals
|
|
515
525
|
// isPluginUninstalled()/isPluginInactive() read, so calling it afterwards would
|
|
516
526
|
// always see "no composite/registry" and report not-uninstalled (teardown skipped).
|
|
517
527
|
const uninstalled = isPluginUninstalled();
|
|
518
|
-
|
|
528
|
+
// Third caller of the same unguarded teardown (statusline.js and
|
|
529
|
+
// statusline-composite.js are the others): a read-only ~/.claude turns this
|
|
530
|
+
// into an uncaught throw that takes down the whole SessionStart hook.
|
|
531
|
+
try { cleanupDisabledStatusline(); } catch { /* best-effort teardown */ }
|
|
519
532
|
// Genuine uninstall (not a temporary disable) leaves residue the settings-only
|
|
520
533
|
// self-heal can't reach: ~/.cache/code-graph (the ~40MB binary + state) and the
|
|
521
534
|
// current project's CLAUDE.md adoption block. CC fires no uninstall hook, AND it
|
|
@@ -16,7 +16,14 @@ const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() =>
|
|
|
16
16
|
const SEPARATOR = ' \x1b[2m|\x1b[0m ';
|
|
17
17
|
|
|
18
18
|
function main() {
|
|
19
|
-
|
|
19
|
+
// Same reasoning as statusline.js: the teardown writes settings.json and the
|
|
20
|
+
// registry, both of which throw on a read-only config dir, and this is THE
|
|
21
|
+
// command Claude Code runs for the status line — an uncaught throw here blanks
|
|
22
|
+
// every provider's segment, not just ours.
|
|
23
|
+
let disabledCleanup = { cleaned: false };
|
|
24
|
+
try {
|
|
25
|
+
disabledCleanup = cleanupDisabledStatusline();
|
|
26
|
+
} catch { /* teardown is optional; rendering is not */ }
|
|
20
27
|
if (disabledCleanup.cleaned) process.exit(0);
|
|
21
28
|
|
|
22
29
|
// Collect stdin (Claude Code pipes JSON context)
|
|
@@ -19,18 +19,41 @@ const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() =>
|
|
|
19
19
|
// real status instead. Without this, a persistently-failing update (missing
|
|
20
20
|
// tar/curl, full disk, blocked network) pins "updating" forever.
|
|
21
21
|
const STUCK_UPDATE_ATTEMPTS = 5;
|
|
22
|
-
function
|
|
22
|
+
function readUpdateState() {
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
return JSON.parse(fs.readFileSync(
|
|
25
25
|
path.join(os.homedir(), '.cache', 'code-graph', 'update-state.json'), 'utf8'));
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
} catch { return null; /* no state file or unreadable */ }
|
|
27
|
+
}
|
|
28
|
+
function updatePending(st = readUpdateState()) {
|
|
29
|
+
if (!st) return false;
|
|
30
|
+
if ((st.updateAttempts || 0) >= STUCK_UPDATE_ATTEMPTS) return false;
|
|
31
|
+
if (st.updateAvailable) return true;
|
|
32
|
+
if (st.latestVersion && st.installedVersion && st.latestVersion !== st.installedVersion) return true;
|
|
30
33
|
return false;
|
|
31
34
|
}
|
|
35
|
+
// The updater has given up on this release (auto-update.js MAX_UPDATE_ATTEMPTS).
|
|
36
|
+
// This has to be SHOWN, not merely not-lied-about: the updater's own stderr
|
|
37
|
+
// notice is written by a process session-init spawns `detached` with
|
|
38
|
+
// `stdio: 'ignore'`, so nobody ever sees it, and updatePending() going quiet
|
|
39
|
+
// above means the only remaining signal was running `doctor` by hand. A user
|
|
40
|
+
// who never runs doctor would sit on a permanently parked updater with no way
|
|
41
|
+
// to know (found by pre-release review of v0.111.0, fixed in v0.111.1).
|
|
42
|
+
function updateStuck(st = readUpdateState()) {
|
|
43
|
+
return !!(st && st.updateAvailable && (st.updateAttempts || 0) >= STUCK_UPDATE_ATTEMPTS);
|
|
44
|
+
}
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
// The teardown is best-effort housekeeping, never a precondition for rendering.
|
|
47
|
+
// It writes ~/.claude/settings.json and ~/.cache/code-graph/statusline-registry.json,
|
|
48
|
+
// and on a read-only config dir (EROFS container mount, a `sudo` that left root
|
|
49
|
+
// ownership behind, restrictive umask) those writes throw — from module scope,
|
|
50
|
+
// where nothing catches them. The user's whole status line then goes blank plus a
|
|
51
|
+
// node stack trace, for a cleanup they never asked for. Swallow it: the next run
|
|
52
|
+
// with a writable dir does the work.
|
|
53
|
+
let disabledCleanup = { cleaned: false };
|
|
54
|
+
try {
|
|
55
|
+
disabledCleanup = cleanupDisabledStatusline();
|
|
56
|
+
} catch { /* teardown is optional; rendering is not */ }
|
|
34
57
|
if (disabledCleanup.cleaned) process.exit(0);
|
|
35
58
|
|
|
36
59
|
// Only show status in projects that have a code-graph directory. The statusLine
|
|
@@ -94,7 +117,10 @@ const bin = findBinary();
|
|
|
94
117
|
if (!bin) {
|
|
95
118
|
// No usable binary yet. If an update is queued, the background downloader is
|
|
96
119
|
// still fetching it \u2014 that is "updating", not a broken "offline" state.
|
|
97
|
-
process.stdout.write(
|
|
120
|
+
process.stdout.write(
|
|
121
|
+
updateStuck() ? 'code-graph: \u26a0 update stuck'
|
|
122
|
+
: updatePending() ? 'code-graph: \u21bb updating'
|
|
123
|
+
: 'code-graph: offline');
|
|
98
124
|
process.exit(0);
|
|
99
125
|
}
|
|
100
126
|
|
|
@@ -121,6 +147,8 @@ function renderHealth(s) {
|
|
|
121
147
|
// index doesn't masquerade as fully current.
|
|
122
148
|
if (s.index_version_stale) line += ' | \u21bb rebuilding';
|
|
123
149
|
if (s.watching) line += ' | watching';
|
|
150
|
+
// A parked updater is otherwise invisible in normal use — see updateStuck().
|
|
151
|
+
if (updateStuck()) line += ' | \u26a0 update stuck';
|
|
124
152
|
return line;
|
|
125
153
|
}
|
|
126
154
|
|
|
@@ -144,7 +172,8 @@ function statusUnavailable(errText) {
|
|
|
144
172
|
// phrase so a cached binary predating the marker still reads as "updating".
|
|
145
173
|
const errStr = errText || '';
|
|
146
174
|
const binaryOutdated = errStr.includes('code-graph:schema-too-new') || /schema version/i.test(errStr);
|
|
147
|
-
|
|
175
|
+
if (binaryOutdated || updatePending()) return 'code-graph: \u21bb updating';
|
|
176
|
+
return updateStuck() ? 'code-graph: \u26a0 update stuck' : 'code-graph: offline';
|
|
148
177
|
}
|
|
149
178
|
|
|
150
179
|
let report = null;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Fix: pin all hook + auto-update artifacts to a `code-graph-mcp/` subdir of
|
|
20
20
|
// whatever `os.tmpdir()` resolves to. Contained, deterministic, easy to GC.
|
|
21
|
+
const crypto = require('crypto');
|
|
21
22
|
const fs = require('fs');
|
|
22
23
|
const os = require('os');
|
|
23
24
|
const path = require('path');
|
|
@@ -29,4 +30,60 @@ function cgTmpDir() {
|
|
|
29
30
|
return CG_TMP_DIR;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// Short, filename-safe digest of a project root. Cooldown flags live in ONE
|
|
34
|
+
// shared tmp dir for the whole machine, so a flag named only after its subject
|
|
35
|
+
// (a command, a symbol, a context type) is global: an `impact` push in project A
|
|
36
|
+
// silenced the identical prompt in project B for the next 30s, and a grep
|
|
37
|
+
// cooldown followed the developer across every repo they had open. Folding the
|
|
38
|
+
// cwd in makes the flag mean what it always claimed to — "recently done HERE".
|
|
39
|
+
// pre-read-guide.js already did this; the other four hooks did not.
|
|
40
|
+
function cwdHash(cwd) {
|
|
41
|
+
return crypto.createHash('sha1').update(String(cwd)).digest('hex').slice(0, 12);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Nothing ever deleted what cgTmpDir() collects. Every cooldown flag, read-fanout
|
|
45
|
+
// state file and interrupted `update-*` download stayed forever: measured 281
|
|
46
|
+
// entries on a working dev box, 232 of them older than a day. They are 0-byte
|
|
47
|
+
// flags, so the cost is inode churn and a directory that makes `ls` in the shared
|
|
48
|
+
// tmp useless — but the `update-*` dirs hold a partly-extracted release tarball,
|
|
49
|
+
// which is megabytes each.
|
|
50
|
+
//
|
|
51
|
+
// The window has to exceed the longest cooldown that reads these files, or the
|
|
52
|
+
// prune would silently shorten it. The longest is user-prompt-context's
|
|
53
|
+
// `overview` at 5 minutes, so 24h is ~288× the margin — this is garbage
|
|
54
|
+
// collection, not expiry.
|
|
55
|
+
const PRUNE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Delete cgTmpDir() entries older than `maxAgeMs`. Best-effort and total: any
|
|
59
|
+
* failure (missing dir, a file another process just removed, a permission
|
|
60
|
+
* problem) is skipped rather than raised — this runs from a SessionStart hook,
|
|
61
|
+
* where a throw is a visible failure and unreclaimed disk is not.
|
|
62
|
+
* @returns {number} entries removed
|
|
63
|
+
*/
|
|
64
|
+
function pruneCgTmp({ now = Date.now(), maxAgeMs = PRUNE_MAX_AGE_MS, dir = CG_TMP_DIR } = {}) {
|
|
65
|
+
// The recursive delete below is the reason for this check. `dir` is a
|
|
66
|
+
// parameter only so tests can point it at a sandbox, and a future caller
|
|
67
|
+
// passing the wrong path must not be able to turn this into `rm -rf` on
|
|
68
|
+
// something that isn't ours.
|
|
69
|
+
if (path.basename(dir) !== 'code-graph-mcp') return 0;
|
|
70
|
+
let removed = 0;
|
|
71
|
+
let entries;
|
|
72
|
+
try {
|
|
73
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
74
|
+
} catch { return 0; }
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const target = path.join(dir, entry.name);
|
|
77
|
+
try {
|
|
78
|
+
if (now - fs.statSync(target).mtimeMs <= maxAgeMs) continue;
|
|
79
|
+
// `update-*` staging dirs from an interrupted auto-update are the only
|
|
80
|
+
// subdirectories here, and they are the ones that actually hold bytes.
|
|
81
|
+
if (entry.isDirectory()) fs.rmSync(target, { recursive: true, force: true });
|
|
82
|
+
else fs.unlinkSync(target);
|
|
83
|
+
removed++;
|
|
84
|
+
} catch { /* raced, unreadable, or already gone — the next run retries */ }
|
|
85
|
+
}
|
|
86
|
+
return removed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { cgTmpDir, CG_TMP_DIR, cwdHash, pruneCgTmp, PRUNE_MAX_AGE_MS };
|
|
@@ -12,7 +12,7 @@ const os = require('os');
|
|
|
12
12
|
// ~/.claude/tmp/, so bare-tmp flags interleave with transcript captures —
|
|
13
13
|
// diagnostic blindness + the §8 recursive-grep footgun (see tmp-dir.js). The
|
|
14
14
|
// other hook scripts already route through here; this one was the lone holdout.
|
|
15
|
-
const { cgTmpDir } = require('./tmp-dir');
|
|
15
|
+
const { cgTmpDir, cwdHash } = require('./tmp-dir');
|
|
16
16
|
const { hidden } = require('./proc-opts');
|
|
17
17
|
|
|
18
18
|
// Mid-session install detection: hook fires but no manifest yet.
|
|
@@ -27,17 +27,24 @@ const COOLDOWNS = {
|
|
|
27
27
|
symptom: 10 * 60 * 1000, // 10min — Phase E meta-advisory hint, low value to repeat
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Project-scoped (see cwdHash in tmp-dir.js). There are only five flag names
|
|
31
|
+
// here, so an un-scoped flag was effectively a machine-wide mute: one `impact`
|
|
32
|
+
// push in any repo silenced the next 30s of impact pushes in every other repo,
|
|
33
|
+
// and `overview` did it for five minutes.
|
|
34
|
+
function ctxFlagPath(type, cwd) {
|
|
35
|
+
return path.join(cgTmpDir(), `.code-graph-ctx-${cwdHash(cwd)}-${type}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isCoolingDown(type, cwd = process.cwd()) {
|
|
31
39
|
try {
|
|
32
|
-
const
|
|
33
|
-
const stat = fs.statSync(flag);
|
|
40
|
+
const stat = fs.statSync(ctxFlagPath(type, cwd));
|
|
34
41
|
return Date.now() - stat.mtimeMs < (COOLDOWNS[type] || 60000);
|
|
35
42
|
} catch { return false; }
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
function markCooldown(type) {
|
|
45
|
+
function markCooldown(type, cwd = process.cwd()) {
|
|
39
46
|
try {
|
|
40
|
-
fs.writeFileSync(
|
|
47
|
+
fs.writeFileSync(ctxFlagPath(type, cwd), '');
|
|
41
48
|
} catch { /* ok */ }
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -424,7 +431,12 @@ function runMain() {
|
|
|
424
431
|
// --- Read user message ---
|
|
425
432
|
let message;
|
|
426
433
|
try {
|
|
427
|
-
|
|
434
|
+
// fd 0, not '/dev/stdin': the path form open(2)s the symlink target, which
|
|
435
|
+
// fails with ENXIO when stdin is a socketpair (e.g. spawnSync {input}).
|
|
436
|
+
// Reading the fd directly works for pipes, sockets, and files alike. Five
|
|
437
|
+
// sibling hooks already did this; this one was the holdout, so it read
|
|
438
|
+
// nothing under exactly the conditions verifyHooksFire spawns it with.
|
|
439
|
+
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
428
440
|
message = (input && input.message) || '';
|
|
429
441
|
} catch {
|
|
430
442
|
return;
|
|
@@ -445,14 +457,16 @@ function runMain() {
|
|
|
445
457
|
const filePaths = extractFilePaths(message);
|
|
446
458
|
const symbols = extractSymbols(message);
|
|
447
459
|
const intents = detectIntents(message);
|
|
448
|
-
|
|
460
|
+
// Key the cooldowns on the resolved project root, not the raw shell cwd, so a
|
|
461
|
+
// `cd` into a subdir does not read as a different project and re-fire.
|
|
462
|
+
const query = determineQueryType(intents, symbols, filePaths, (type) => isCoolingDown(type, cwd), message);
|
|
449
463
|
|
|
450
464
|
if (!query) return;
|
|
451
465
|
|
|
452
466
|
// Phase E: symptom-hint is prose-only (no CLI execution). Emit + cooldown
|
|
453
467
|
// before the result-fetching paths so it can short-circuit cleanly.
|
|
454
468
|
if (query.type === 'symptom-hint') {
|
|
455
|
-
markCooldown('symptom');
|
|
469
|
+
markCooldown('symptom', cwd);
|
|
456
470
|
process.stdout.write(
|
|
457
471
|
'[code-graph:hint] indexed repo — for vague-symptom prompts, try `semantic_code_search "<symptom>"` ' +
|
|
458
472
|
'or `module_overview <suspected-dir>` to surface candidate code structurally. Skip if not searching code.\n'
|
|
@@ -467,8 +481,17 @@ function runMain() {
|
|
|
467
481
|
search: '[code-graph:search] Relevant code:',
|
|
468
482
|
};
|
|
469
483
|
|
|
470
|
-
|
|
471
|
-
|
|
484
|
+
// Resolve the binary the way every other hook does. The bare name relied on
|
|
485
|
+
// `code-graph-mcp` being on PATH, which it is not for plugin-only installs
|
|
486
|
+
// (the binary lives in ~/.cache/code-graph/bin), and when it IS on PATH it may
|
|
487
|
+
// be an unrelated or years-stale global shim — the same finding cg-answer.js
|
|
488
|
+
// and session-init.js were already fixed for. Null means no engine: nothing to
|
|
489
|
+
// run, and nothing to say about it.
|
|
490
|
+
const binary = require('./find-binary').findBinary();
|
|
491
|
+
if (!binary) return;
|
|
492
|
+
|
|
493
|
+
function run(args) {
|
|
494
|
+
return execFileSync(binary, args, hidden({
|
|
472
495
|
cwd,
|
|
473
496
|
timeout: 3000,
|
|
474
497
|
encoding: 'utf8',
|
|
@@ -477,15 +500,21 @@ function runMain() {
|
|
|
477
500
|
}));
|
|
478
501
|
}
|
|
479
502
|
|
|
503
|
+
// Stamp the cooldown on ATTEMPT, not on success. Keyed on the outcome, a
|
|
504
|
+
// binary that fails or times out produced no flag, so the very next prompt of
|
|
505
|
+
// the same shape re-ran it — a 3s execFileSync timeout on EVERY turn, which is
|
|
506
|
+
// the worst case wearing the cheapest disguise. The cooldown's job is rate
|
|
507
|
+
// limiting; whether the run had something to say is a separate question.
|
|
508
|
+
markCooldown(query.type, cwd);
|
|
509
|
+
|
|
480
510
|
try {
|
|
481
511
|
let result = '';
|
|
482
|
-
if (query.type === 'impact') result = run(
|
|
483
|
-
else if (query.type === 'callgraph') result = run(
|
|
484
|
-
else if (query.type === 'overview') result = run(
|
|
485
|
-
else if (query.type === 'search') result = run(
|
|
512
|
+
if (query.type === 'impact') result = run(['impact', query.symbol]);
|
|
513
|
+
else if (query.type === 'callgraph') result = run(['callgraph', query.symbol, '--depth', '2']);
|
|
514
|
+
else if (query.type === 'overview') result = run(['overview', query.path]);
|
|
515
|
+
else if (query.type === 'search') result = run(['search', query.symbol, '--limit', '8']);
|
|
486
516
|
|
|
487
517
|
if (result && result.trim()) {
|
|
488
|
-
markCooldown(query.type);
|
|
489
518
|
process.stdout.write(`${PREFIXES[query.type]}\n${result.trim()}\n`);
|
|
490
519
|
}
|
|
491
520
|
} catch {
|
|
@@ -497,4 +526,4 @@ if (require.main === module) {
|
|
|
497
526
|
runMain();
|
|
498
527
|
}
|
|
499
528
|
|
|
500
|
-
module.exports = { shouldSkip, extractFilePaths, extractSymbols, detectIntents, scoreIntent, INTENT_PATTERNS, INTENT_THRESHOLD, determineQueryType, computeQuietHooks, STOP_WORDS, PLAIN_WORD_EXCLUDE, hasSymptom, SYMPTOM_PATTERNS, buildRunEnv };
|
|
529
|
+
module.exports = { COOLDOWNS, shouldSkip, extractFilePaths, extractSymbols, detectIntents, scoreIntent, INTENT_PATTERNS, INTENT_THRESHOLD, determineQueryType, computeQuietHooks, STOP_WORDS, PLAIN_WORD_EXCLUDE, hasSymptom, SYMPTOM_PATTERNS, buildRunEnv };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.112.0",
|
|
4
4
|
"description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"node": ">=16"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@sdsrs/code-graph-linux-x64": "0.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
38
|
+
"@sdsrs/code-graph-linux-x64": "0.112.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.112.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.112.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.112.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.112.0"
|
|
43
43
|
}
|
|
44
44
|
}
|