@sdsrs/code-graph 0.127.0 → 0.129.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 +23 -9
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/agents/code-explorer.md +4 -1
- package/claude-plugin/hooks/hooks.json +1 -1
- package/claude-plugin/scripts/auto-update.js +53 -5
- package/claude-plugin/scripts/lifecycle.js +15 -3
- package/claude-plugin/scripts/pre-edit-guide.js +30 -4
- package/claude-plugin/scripts/pre-grep-guide.js +14 -0
- package/claude-plugin/scripts/session-init.js +18 -1
- package/claude-plugin/skills/index.md +13 -3
- package/claude-plugin/templates/code-graph-snapshot.yml +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -55,17 +55,18 @@ Every design decision — from token-aware compression to node_id-based snippet
|
|
|
55
55
|
## Performance
|
|
56
56
|
|
|
57
57
|
Every row below is a line `code-graph-mcp benchmark` prints, measured on this
|
|
58
|
-
repository (
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
repository (283 files, 5,311 nodes, 11,215 edges) with a release build — median
|
|
59
|
+
of three consecutive runs on an otherwise idle machine. Run the same command on
|
|
60
|
+
your own project: the numbers that matter are yours, and these scale with tree
|
|
61
|
+
size and machine.
|
|
61
62
|
|
|
62
|
-
| `benchmark` line | This repo, v0.
|
|
63
|
+
| `benchmark` line | This repo, v0.129.0 |
|
|
63
64
|
|--------|-------|
|
|
64
|
-
| Full index | **~
|
|
65
|
-
| Incremental (noop) | **~
|
|
66
|
-
| Query latency P50 / P99 | **~
|
|
67
|
-
| DB size | **~
|
|
68
|
-
| Avg tokens/node | **~
|
|
65
|
+
| Full index | **~2.0s** (≈139 files/second, single-threaded) |
|
|
66
|
+
| Incremental (noop) | **~28ms** no-change detection via BLAKE3 Merkle tree |
|
|
67
|
+
| Query latency P50 / P99 | **~655us / ~2.1ms** |
|
|
68
|
+
| DB size | **~22.4MB** (≈4.2MB per 1,000 nodes) |
|
|
69
|
+
| Avg tokens/node | **~242** |
|
|
69
70
|
|
|
70
71
|
## Efficiency: code-graph vs Traditional Tools
|
|
71
72
|
|
|
@@ -286,6 +287,18 @@ code-graph-mcp uninstall # restore statusline, strip hooks, drop the cache
|
|
|
286
287
|
npm uninstall -g @sdsrs/code-graph
|
|
287
288
|
```
|
|
288
289
|
|
|
290
|
+
### Built from source (cargo)
|
|
291
|
+
|
|
292
|
+
`uninstall` lives in the npm wrapper, so a `cargo install` / `cargo build` binary
|
|
293
|
+
does not carry it — that binary tells you as much and points here. Run the
|
|
294
|
+
teardown through npx without installing globally (npx still downloads the package
|
|
295
|
+
and its platform binary into the npm cache), then remove your binary:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npx -y @sdsrs/code-graph uninstall # statusline, hooks, cache
|
|
299
|
+
cargo uninstall code-graph-mcp # or delete the target/release binary
|
|
300
|
+
```
|
|
301
|
+
|
|
289
302
|
## MCP Tools
|
|
290
303
|
|
|
291
304
|
`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:
|
|
@@ -338,6 +351,7 @@ All tools are also available as CLI subcommands for shell scripts, hooks, and te
|
|
|
338
351
|
| `doctor` | — | Diagnose and repair environment issues |
|
|
339
352
|
| `adopt` | — | Install the steering block into the project `CLAUDE.md` + detail doc |
|
|
340
353
|
| `unadopt` | — | Remove the steering block + detail doc |
|
|
354
|
+
| `uninstall` | — | Full local teardown (statusline, hooks, cache). npm/npx installs only — a `cargo install` build prints where to get it |
|
|
341
355
|
| `serve` | — | Start the MCP JSON-RPC server on stdio (the default with no subcommand) |
|
|
342
356
|
|
|
343
357
|
Common options: `--json` (JSON output), `--compact` (compact output), `--limit N`, `--depth N`, `--file <path>`.
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-explorer
|
|
3
3
|
description: Deep code understanding expert using AST knowledge graph. Use when exploring unfamiliar code, tracing complex relationships, or understanding module architecture.
|
|
4
|
-
tools: ["Read", "Grep", "Glob", "Bash", "mcp__code-graph__semantic_code_search", "mcp__code-graph__get_call_graph", "mcp__code-graph__get_ast_node", "mcp__code-graph__project_map", "mcp__code-graph__module_overview", "mcp__code-graph__ast_search", "mcp__code-graph__find_references"]
|
|
4
|
+
tools: ["Read", "Grep", "Glob", "Bash", "mcp__code-graph__semantic_code_search", "mcp__code-graph__get_call_graph", "mcp__code-graph__get_ast_node", "mcp__code-graph__project_map", "mcp__code-graph__module_overview", "mcp__code-graph__ast_search", "mcp__code-graph__find_references", "mcp__plugin_code-graph-mcp_code-graph__semantic_code_search", "mcp__plugin_code-graph-mcp_code-graph__get_call_graph", "mcp__plugin_code-graph-mcp_code-graph__get_ast_node", "mcp__plugin_code-graph-mcp_code-graph__project_map", "mcp__plugin_code-graph-mcp_code-graph__module_overview", "mcp__plugin_code-graph-mcp_code-graph__ast_search", "mcp__plugin_code-graph-mcp_code-graph__find_references"]
|
|
5
5
|
model: sonnet
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
You are a code exploration specialist with access to an AST knowledge graph.
|
|
9
9
|
|
|
10
|
+
<!-- The tool allowlist carries both MCP namespace spellings on purpose (DOC-07);
|
|
11
|
+
rationale in CHANGELOG. Delete at most one half, never both. -->
|
|
12
|
+
|
|
10
13
|
## Strategy
|
|
11
14
|
|
|
12
15
|
1. **Start with semantic_code_search** to locate relevant code by meaning, or **module_overview** / **project_map** to map an unfamiliar directory or the whole repo
|
|
@@ -507,9 +507,23 @@ function cachedBinaryStaleVsState(state, { binaryPath = cachedBinaryPath(), read
|
|
|
507
507
|
* install dropped the platform package).
|
|
508
508
|
*
|
|
509
509
|
* Returns true on successful promote, false otherwise. Never throws.
|
|
510
|
+
*
|
|
511
|
+
* JS-03: gated on `cachedBinaryNeedsUpdate` so an already-current binary is
|
|
512
|
+
* never re-fetched. The gate lives HERE rather than at the call sites because
|
|
513
|
+
* all four of them wanted it: `downloadAndInstall` Step 2 called this
|
|
514
|
+
* unconditionally, and its two early-return arms (no plugin tarball / tarball
|
|
515
|
+
* integrity failure) call it again on the way out. On the normal update path
|
|
516
|
+
* the binary really is behind, so the gate is a no-op; it pays off on every
|
|
517
|
+
* REPEATED round — e.g. the JS-02 treadmill, where a blocked repoint replayed
|
|
518
|
+
* `downloadAndInstall` about every 30 minutes and each replay re-downloaded and
|
|
519
|
+
* re-promoted a ~40 MB binary that was already latest.
|
|
520
|
+
*
|
|
521
|
+
* `needsUpdate` is injectable for tests only; production always uses the real
|
|
522
|
+
* predicate. Returning false on a skip is accurate: nothing was updated.
|
|
510
523
|
*/
|
|
511
|
-
async function downloadBinary(latest) {
|
|
524
|
+
async function downloadBinary(latest, { needsUpdate = cachedBinaryNeedsUpdate } = {}) {
|
|
512
525
|
if (!latest || !latest.binaryUrl) return false;
|
|
526
|
+
if (!needsUpdate(latest)) return false; // already at latest.version — no fetch
|
|
513
527
|
if (!commandExists('curl')) {
|
|
514
528
|
console.error('[code-graph] Binary download skipped: curl not on PATH.');
|
|
515
529
|
return false;
|
|
@@ -700,6 +714,16 @@ async function downloadAndInstall(latest, {
|
|
|
700
714
|
let pluginUpdated = false;
|
|
701
715
|
let binaryUpdated = false;
|
|
702
716
|
let marketplaceRefreshed = false;
|
|
717
|
+
// JS-02 (audit 2026-08-29): declared out here so it can be RETURNED. It used
|
|
718
|
+
// to live inside the `if (pluginUpdated)` block, correctly stopping the
|
|
719
|
+
// manifest from advancing, and then evaporate — the caller saw
|
|
720
|
+
// `pluginUpdated: true`, wrote `updateAttempts: 0, updateAvailable: false,
|
|
721
|
+
// suspendedAt: null`, and re-read the un-advanced registry next round. With
|
|
722
|
+
// `updateAvailable:false` the recheck interval is 30 minutes, so a registry
|
|
723
|
+
// that stays broken drives ~48 full download-and-install rounds a day —
|
|
724
|
+
// exactly the treadmill MAX_UPDATE_ATTEMPTS and the suspension mechanism
|
|
725
|
+
// exist to stop, both of which stayed dormant because every round "succeeded".
|
|
726
|
+
let repointBlocked = false;
|
|
703
727
|
|
|
704
728
|
try {
|
|
705
729
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
@@ -808,7 +832,6 @@ async function downloadAndInstall(latest, {
|
|
|
808
832
|
// behind, the ordinary check interval retries the whole install and
|
|
809
833
|
// re-reports, and the repoint lands by itself the moment the file is
|
|
810
834
|
// repaired. `missing` is not blocked: no registry means nothing to repoint.
|
|
811
|
-
let repointBlocked = false;
|
|
812
835
|
if (installedRead.corrupt) {
|
|
813
836
|
// Value unusable — the bytes are not ours to guess at.
|
|
814
837
|
const why = installedRead.error
|
|
@@ -848,7 +871,28 @@ async function downloadAndInstall(latest, {
|
|
|
848
871
|
repointBlocked = true;
|
|
849
872
|
}
|
|
850
873
|
}
|
|
851
|
-
|
|
874
|
+
// JS-05 (audit 2026-08-29): `plugins[PLUGIN_ID]` was assumed to be a
|
|
875
|
+
// non-empty array. A registry holding `[]` — or any other truthy
|
|
876
|
+
// non-array — made `[0].installPath = …` throw a TypeError, which the
|
|
877
|
+
// outer catch reported as "Plugin download/extract failed": a diagnosis
|
|
878
|
+
// pointing at the download for a malformed local file. `activeInstallPath`
|
|
879
|
+
// in lifecycle.js already reads this same field the careful way; this is
|
|
880
|
+
// the sibling that did not.
|
|
881
|
+
const records = installed && installed.plugins && installed.plugins[PLUGIN_ID];
|
|
882
|
+
const repointable =
|
|
883
|
+
Array.isArray(records) && records[0] && typeof records[0] === 'object';
|
|
884
|
+
if (installed && !repointable && installed.plugins && installed.plugins[PLUGIN_ID]) {
|
|
885
|
+
// Present but not the shape we can write into (`[]`, or a truthy
|
|
886
|
+
// non-array). Blocked, NOT skipped: a silent skip here would feed the
|
|
887
|
+
// JS-02 treadmill through a new door.
|
|
888
|
+
console.error(
|
|
889
|
+
`[code-graph] plugin ${latest.version} is installed, but this plugin's entry in ` +
|
|
890
|
+
`${installedPath} is malformed (expected a non-empty array) — it still points at ` +
|
|
891
|
+
'the previous version. Run `/plugin update` or repair that file by hand.'
|
|
892
|
+
);
|
|
893
|
+
repointBlocked = true;
|
|
894
|
+
}
|
|
895
|
+
if (repointable) {
|
|
852
896
|
installed.plugins[PLUGIN_ID][0].installPath = pluginDst;
|
|
853
897
|
installed.plugins[PLUGIN_ID][0].version = latest.version;
|
|
854
898
|
installed.plugins[PLUGIN_ID][0].lastUpdated = new Date().toISOString();
|
|
@@ -900,7 +944,7 @@ async function downloadAndInstall(latest, {
|
|
|
900
944
|
binaryUpdated = true;
|
|
901
945
|
}
|
|
902
946
|
|
|
903
|
-
return { pluginUpdated, binaryUpdated, marketplaceRefreshed };
|
|
947
|
+
return { pluginUpdated, binaryUpdated, marketplaceRefreshed, repointBlocked };
|
|
904
948
|
} catch (e) {
|
|
905
949
|
console.error(`[code-graph] Plugin download/extract failed: ${e.message}`);
|
|
906
950
|
return { pluginUpdated: false, binaryUpdated: false, marketplaceRefreshed };
|
|
@@ -1278,7 +1322,11 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
1278
1322
|
return { updateAvailable: true, suspended: true, from: installedVersion, to: latest.version };
|
|
1279
1323
|
}
|
|
1280
1324
|
const result = await downloadAndInstall(latest);
|
|
1281
|
-
|
|
1325
|
+
// A refused repoint is NOT a success (JS-02): the bytes landed, but the
|
|
1326
|
+
// registry Claude Code reads still names the old version, so the next round
|
|
1327
|
+
// sees the same update available. Counting it as success reset the attempt
|
|
1328
|
+
// counter and the suspension stamp every time.
|
|
1329
|
+
const success = result.pluginUpdated && !result.repointBlocked;
|
|
1282
1330
|
// Suspension clock. It restarts when the daily retry is spent and fails,
|
|
1283
1331
|
// which is what keeps `retryDue` from staying true and turning the retry
|
|
1284
1332
|
// back into a per-session treadmill; it clears on success and on a new
|
|
@@ -639,9 +639,18 @@ function unadoptRegisteredProjects() {
|
|
|
639
639
|
for (const project of (res && res.list) || []) {
|
|
640
640
|
try {
|
|
641
641
|
const r = unadopt({ cwd: project });
|
|
642
|
-
|
|
642
|
+
// Three outcomes, not two (audit 2026-08-29 JS-06). A project whose block
|
|
643
|
+
// the user already removed by hand comes back with nothing pruned and NO
|
|
644
|
+
// error — which used to land in the "Could NOT clean" list, sending them
|
|
645
|
+
// to hand-edit a file that is already clean. `unadopt` reports real
|
|
646
|
+
// failure separately (`claudeMdUnreadable` / `claudeMdUnwritable`, the
|
|
647
|
+
// same pair adopt.js folds into its own `cleanupFailed`), so use it rather
|
|
648
|
+
// than inferring failure from "nothing happened".
|
|
649
|
+
const cleaned = !!(r && (r.blockPruned || r.fileRemoved || r.claudeMdRemoved));
|
|
650
|
+
const failed = !!(r && (r.claudeMdUnreadable || r.claudeMdUnwritable));
|
|
651
|
+
out.push({ project, cleaned, failed });
|
|
643
652
|
} catch (e) {
|
|
644
|
-
out.push({ project, cleaned: false, error: (e && e.message) || String(e) });
|
|
653
|
+
out.push({ project, cleaned: false, failed: true, error: (e && e.message) || String(e) });
|
|
645
654
|
}
|
|
646
655
|
}
|
|
647
656
|
reportUnadoptSweep(out);
|
|
@@ -659,7 +668,9 @@ function unadoptRegisteredProjects() {
|
|
|
659
668
|
function reportUnadoptSweep(entries) {
|
|
660
669
|
try {
|
|
661
670
|
const cleaned = entries.filter((e) => e && e.cleaned).map((e) => e.project);
|
|
662
|
-
|
|
671
|
+
// Only genuine failures. "Nothing to clean" is neither a success worth
|
|
672
|
+
// announcing nor a problem worth sending someone to fix (JS-06).
|
|
673
|
+
const failed = entries.filter((e) => e && !e.cleaned && e.failed).map((e) => e.project);
|
|
663
674
|
if (!cleaned.length && !failed.length) return;
|
|
664
675
|
const lines = [];
|
|
665
676
|
if (cleaned.length) {
|
|
@@ -1825,6 +1836,7 @@ module.exports = {
|
|
|
1825
1836
|
install, uninstall, update, healthCheck, scanForBrokenPaths, checkScopeConflict,
|
|
1826
1837
|
isPluginExplicitlyDisabled, isPluginInactive, isPluginUninstalled, removeCacheResidue,
|
|
1827
1838
|
cleanupDisabledStatusline, unadoptRegisteredProjects,
|
|
1839
|
+
reportUnadoptSweep, // exported so its three-way bucketing is testable (audit 2026-08-29 JS-06)
|
|
1828
1840
|
readManifest, readJson, readJsonResult, readSettingsForWrite, writeJsonAtomic,
|
|
1829
1841
|
backupCorruptFile, // auto-update.js repoints installed_plugins.json and owes the same preserve-then-proceed route
|
|
1830
1842
|
migrateOldPluginIds, // exported so its failure arms are testable (audit 2026-08-22 P2-10)
|
|
@@ -45,20 +45,46 @@ if (!oldStr || oldStr.length < 10) process.exit(0);
|
|
|
45
45
|
|
|
46
46
|
// --- Extract function/method signature from the edited text ---
|
|
47
47
|
// Match function definitions across languages: Rust, JS/TS, Python, Go, Java/C#/Kotlin, Ruby, PHP
|
|
48
|
+
//
|
|
49
|
+
// Every unbounded run that is FOLLOWED BY A REQUIRED LITERAL carries an explicit
|
|
50
|
+
// `{1,128}` cap — longer than any real identifier, short enough that the engine
|
|
51
|
+
// gives up after 128 steps per start position. Without it those three patterns
|
|
52
|
+
// are quadratic: on a long
|
|
53
|
+
// unbroken \w run with no `name(...)` construct in it, the greedy run swallows to
|
|
54
|
+
// the end at EVERY start position and then backtracks a character at a time.
|
|
55
|
+
// Measured at HEAD on this box, pattern 4 alone: 10 KB 28 ms, 100 KB 2.8 s,
|
|
56
|
+
// 200 KB 11.0 s, 400 KB 43.4 s — doubling the input quadrupled the time.
|
|
57
|
+
//
|
|
58
|
+
// This needs no malice to hit. `old_string` is whatever the model is editing, so
|
|
59
|
+
// one benign blob without brackets — a base64 asset, a hex dump, a minified
|
|
60
|
+
// bundle, a long snake_case table — stalls a BLOCKING PreToolUse hook for
|
|
61
|
+
// seconds. Real code is unaffected either way (225 KB of this repo's own
|
|
62
|
+
// source: 0.3 ms), because a bracket ends the run almost immediately.
|
|
63
|
+
//
|
|
64
|
+
// The runs that are NOT capped are the ones nothing is required after
|
|
65
|
+
// (`fn\s+(\w+)`, `def\s+(\w+)`, …): those anchor on a keyword first and their
|
|
66
|
+
// trailing capture cannot backtrack, so a cap there would only truncate a long
|
|
67
|
+
// symbol name.
|
|
48
68
|
const fnPatterns = [
|
|
49
69
|
/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/, // Rust
|
|
50
70
|
/(?:export\s+)?(?:async\s+)?function\s+(\w+)/, // JS/TS
|
|
51
|
-
/(?:const|let|var)\s+(\w
|
|
52
|
-
/(?:async\s+)?(\w
|
|
71
|
+
/(?:const|let|var)\s+(\w{1,128})\s*=\s*(?:async\s+)?(?:\([^)]*\)|_)\s*=>/, // JS arrow
|
|
72
|
+
/(?:async\s+)?(\w{1,128})\s*\([^)]*\)\s*\{/, // JS method / Go func
|
|
53
73
|
/def\s+(\w+)/, // Python/Ruby
|
|
54
74
|
/func\s+(\w+)/, // Go/Swift
|
|
55
|
-
/(?:public|private|protected|static|override|virtual|abstract|internal)\s+\S
|
|
75
|
+
/(?:public|private|protected|static|override|virtual|abstract|internal)\s+\S{1,128}\s+(\w{1,128})\s*\(/, // Java/C#/Kotlin
|
|
56
76
|
/(?:public\s+)?function\s+(\w+)/, // PHP
|
|
57
77
|
];
|
|
58
78
|
|
|
79
|
+
// Second bound, on the INPUT rather than the patterns: a signature sits at the
|
|
80
|
+
// head of the edited hunk, so matching past the first 8 KB buys nothing and
|
|
81
|
+
// costs linearly. Belt to the caps' braces — it also bounds whatever pattern a
|
|
82
|
+
// future author adds to the array without reading the note above.
|
|
83
|
+
const scanned = oldStr.length > 8192 ? oldStr.slice(0, 8192) : oldStr;
|
|
84
|
+
|
|
59
85
|
let symbol = null;
|
|
60
86
|
for (const pat of fnPatterns) {
|
|
61
|
-
const m =
|
|
87
|
+
const m = scanned.match(pat);
|
|
62
88
|
if (m) {
|
|
63
89
|
// Find the first captured group
|
|
64
90
|
symbol = m[1] || m[2];
|
|
@@ -291,6 +291,20 @@ const { resolveProjectRoot } = require('./project-root');
|
|
|
291
291
|
// (the exact shape that would re-create the answered:false glob failure).
|
|
292
292
|
function rebaseRelativePaths(cmd, relPrefix, rootDir, exists = fs.existsSync) {
|
|
293
293
|
if (!cmd || typeof cmd !== 'string' || !relPrefix || !rootDir) return cmd;
|
|
294
|
+
// SEC-05 (audit 2026-08-29): one `exists()` syscall per surviving token, and
|
|
295
|
+
// this runs BEFORE every length gate in the file — `shouldHint`'s 1000-char
|
|
296
|
+
// sanity check (:159) and the 2000-char ones on the sed/tail extractors are all
|
|
297
|
+
// downstream of it, so the guard sat below the thing it was guarding. Measured
|
|
298
|
+
// with a counting stub: 100k tokens is 100,001 probes, 2.2 s of real
|
|
299
|
+
// `fs.existsSync` on this box, paid inside a BLOCKING PreToolUse hook.
|
|
300
|
+
//
|
|
301
|
+
// The bound is the loosest one the file already uses, so nothing that any
|
|
302
|
+
// downstream gate would still have processed changes behavior: a command this
|
|
303
|
+
// long is past the sed/tail extractors' limit and twice past `shouldHint`'s.
|
|
304
|
+
// Placed here rather than at the two call sites because both of them
|
|
305
|
+
// (`pre-grep-guide` runMain, `post-grep-inject` runMain) need it, and a guard
|
|
306
|
+
// that lives in the callers is one refactor away from being dropped.
|
|
307
|
+
if (cmd.length > 2000) return cmd;
|
|
294
308
|
const prefix = relPrefix.split(path.sep).join('/');
|
|
295
309
|
// Shell sits outside any known source dir (docs/, target/, …) — don't guess.
|
|
296
310
|
if (!SRC_PATH_TOKEN.test(prefix + '/')) return cmd;
|
|
@@ -12,6 +12,10 @@ const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-util
|
|
|
12
12
|
const { maybeAutoAdopt, isAdopted, unadopt } = require('./adopt');
|
|
13
13
|
const { isNonProjectCwd } = require('./project-detect');
|
|
14
14
|
const { hidden } = require('./proc-opts');
|
|
15
|
+
// Module scope on purpose: `detectHookDark` reads it inside a try/catch that
|
|
16
|
+
// treats any throw as "nothing to conclude", so a lazy require in there would
|
|
17
|
+
// turn a resolution failure into a silent disable (pre-tag review, JS-08).
|
|
18
|
+
const { resolveProjectRoot } = require('./project-root');
|
|
15
19
|
|
|
16
20
|
// v0.17.0 — quietHooks: unconditional quiet 默认。
|
|
17
21
|
// 项目地图与 MEMORY.md plugin contract + on-demand `project_map` 工具高度重叠,
|
|
@@ -954,7 +958,20 @@ function analyzeHookDark(recText) {
|
|
|
954
958
|
|
|
955
959
|
function detectHookDark() {
|
|
956
960
|
try {
|
|
957
|
-
|
|
961
|
+
// JS-08 (audit 2026-08-29). Every WRITER of this file records into the
|
|
962
|
+
// RESOLVED root — subdir cwd walks up to the project root, a linked worktree
|
|
963
|
+
// reads the main checkout's index — while this reader used a bare
|
|
964
|
+
// `process.cwd()`. So in exactly the sessions the subdir-cwd fix exists for,
|
|
965
|
+
// the dark DETECTOR was itself dark: no file at that path, no claim made,
|
|
966
|
+
// nothing said. `resolveProjectRoot` returns null when nothing on the walk is
|
|
967
|
+
// indexed; cwd remains the fallback for that case (unchanged behaviour).
|
|
968
|
+
//
|
|
969
|
+
// Required at module scope (see top of file), not here: inside this
|
|
970
|
+
// try/catch a module-resolution failure would be indistinguishable from
|
|
971
|
+
// "no recommendations.jsonl", which is the silent-disable shape this whole
|
|
972
|
+
// finding is about. The sibling hooks all require it at top level too.
|
|
973
|
+
const root = resolveProjectRoot(process.cwd()) || process.cwd();
|
|
974
|
+
const recPath = path.join(root, '.code-graph', 'recommendations.jsonl');
|
|
958
975
|
return analyzeHookDark(fs.readFileSync(recPath, 'utf8'));
|
|
959
976
|
} catch { return null; } // no recommendations.jsonl → nothing to conclude
|
|
960
977
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
name: index
|
|
3
3
|
description: |
|
|
4
4
|
Diagnose and fix code-graph index issues. Use when: search returns unexpected/empty
|
|
5
|
-
results, or after major codebase restructuring.
|
|
6
|
-
|
|
5
|
+
results, or after major codebase restructuring. Covers the health check and both
|
|
6
|
+
rebuild paths from the CLI.
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Index Maintenance
|
|
@@ -20,5 +20,15 @@ code-graph-mcp incremental-index
|
|
|
20
20
|
|
|
21
21
|
## Full rebuild (when incremental isn't enough)
|
|
22
22
|
```bash
|
|
23
|
-
|
|
23
|
+
code-graph-mcp rebuild-index --confirm
|
|
24
24
|
```
|
|
25
|
+
|
|
26
|
+
This builds the new index in a temporary location and swaps it in with an atomic
|
|
27
|
+
rename, so a running MCP server (and its open WAL) never observes a half-built
|
|
28
|
+
index. Deleting the `.code-graph/` directory by hand skips that swap, which is
|
|
29
|
+
the exact situation the atomic path exists for.
|
|
30
|
+
|
|
31
|
+
`get_index_status` and `rebuild_index` do dispatch over JSON-RPC, but they are
|
|
32
|
+
deliberately kept out of `tools/list` to save tokens (`src/mcp/tools.rs`), so
|
|
33
|
+
they are not in your callable tool set — the CLI above is the surface you have.
|
|
34
|
+
It is also the only one that works with no server running.
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
node-version: '20'
|
|
36
36
|
- name: Build snapshot
|
|
37
37
|
run: |
|
|
38
|
-
npx -y -p @sdsrs/code-graph@0.
|
|
38
|
+
npx -y -p @sdsrs/code-graph@0.129.0 code-graph-mcp snapshot create --out snapshot.db
|
|
39
39
|
zstd -9 snapshot.db -o snapshot.db.zst
|
|
40
40
|
mv snapshot.db.zst "code-graph-snapshot-${GITHUB_SHA:0:7}.db.zst"
|
|
41
41
|
- name: Upload to release
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.129.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.129.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.129.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.129.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.129.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.129.0"
|
|
43
43
|
}
|
|
44
44
|
}
|