@sdsrs/code-graph 0.119.0 → 0.120.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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/auto-update.js +17 -2
- package/claude-plugin/scripts/find-binary.js +40 -6
- package/claude-plugin/scripts/lifecycle.js +47 -3
- package/claude-plugin/scripts/session-init.js +51 -8
- package/claude-plugin/templates/code-graph-snapshot.yml +1 -1
- package/package.json +6 -6
|
@@ -1063,7 +1063,12 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
1063
1063
|
// that set it, and kept polling GitHub on the ordinary interval while
|
|
1064
1064
|
// already rate-limited. Dead code since the backoff was written.
|
|
1065
1065
|
saveState({ ...readState(), installedVersion, lastCheck: new Date().toISOString() });
|
|
1066
|
-
|
|
1066
|
+
// NOT null: null is the CLI's "nothing to do" and it prints "Up to date
|
|
1067
|
+
// (vX)". A failed fetch means the opposite — the update status is
|
|
1068
|
+
// UNKNOWN — and a user running this because they are stuck on an old
|
|
1069
|
+
// version was being told the old version is current (offline, captive
|
|
1070
|
+
// portal, proxy, GitHub 5xx all land here).
|
|
1071
|
+
return { noop: true, reason: 'fetch-failed', from: installedVersion };
|
|
1067
1072
|
}
|
|
1068
1073
|
|
|
1069
1074
|
// Compare versions
|
|
@@ -1079,7 +1084,9 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
|
|
|
1079
1084
|
// against our own parent.
|
|
1080
1085
|
if (process.env.CODE_GRAPH_INSTALL_LOCK_HELD !== '1') {
|
|
1081
1086
|
installLock = acquireLock(path.join(CACHE_DIR, 'install.lock'));
|
|
1082
|
-
|
|
1087
|
+
// Reason, not null — same misreport as the fetch failure above: the
|
|
1088
|
+
// holder is mid-update, so "Up to date (v<old>)" is exactly wrong.
|
|
1089
|
+
if (!installLock) return { noop: true, reason: 'install-lock-held', from: installedVersion };
|
|
1083
1090
|
}
|
|
1084
1091
|
|
|
1085
1092
|
if (hasUpdate) {
|
|
@@ -1266,6 +1273,14 @@ if (require.main === module) {
|
|
|
1266
1273
|
console.log(`Update available: v${result.to} (auto-install failed)`);
|
|
1267
1274
|
} else if (result && result.binaryUpdated) {
|
|
1268
1275
|
console.log(`Repaired binary cache (v${result.to})`);
|
|
1276
|
+
} else if (result && result.noop && result.reason === 'fetch-failed') {
|
|
1277
|
+
// Message only — the exit code stays 0 on purpose: an unreachable
|
|
1278
|
+
// GitHub is not a failure of this command, and the launcher's install
|
|
1279
|
+
// chain plus `doctor` both spawn it.
|
|
1280
|
+
console.log(`Could not reach GitHub — update status UNKNOWN (still on v${result.from}). ` +
|
|
1281
|
+
'Retried on the next session; run `code-graph-mcp doctor` if it persists.');
|
|
1282
|
+
} else if (result && result.noop && result.reason === 'install-lock-held') {
|
|
1283
|
+
console.log(`Another session is installing/updating right now — check skipped (on v${result.from}).`);
|
|
1269
1284
|
} else if (!installMissing && isAutoUpdateDisabled()) {
|
|
1270
1285
|
console.log('CODE_GRAPH_NO_AUTO_UPDATE=1 — auto-update skipped');
|
|
1271
1286
|
} else if (!installMissing && isDevMode()) {
|
|
@@ -230,6 +230,36 @@ function isPlausibleReleaseBinary(candidate) {
|
|
|
230
230
|
try { return fs.statSync(candidate).size > 1_000_000; } catch { return false; }
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Both places npm may put the platform optionalDependency inside a
|
|
235
|
+
* `node_modules` root, most-likely first:
|
|
236
|
+
*
|
|
237
|
+
* <root>/@sdsrs/code-graph-<plat>-<arch>/ (hoisted)
|
|
238
|
+
* <root>/@sdsrs/code-graph/node_modules/@sdsrs/code-graph-…/ (nested)
|
|
239
|
+
*
|
|
240
|
+
* Hoisting is an npm implementation detail, not a contract: npm 12 leaves the
|
|
241
|
+
* optionalDependency nested under the shell package on a plain
|
|
242
|
+
* `npm install -g @sdsrs/code-graph`, while older npm hoists it (both layouts
|
|
243
|
+
* were observed on one machine, 2026-08-17). Probing only the hoisted spelling
|
|
244
|
+
* made a SUCCESSFUL npm install read as "did not yield a binary": the launcher
|
|
245
|
+
* then re-downloaded ~41MB from GitHub, and — the part that outlives the
|
|
246
|
+
* session — never called `recordGlobalInstall()`, so `uninstall` had no marker
|
|
247
|
+
* proving the plugin owns those global packages and left them on PATH forever.
|
|
248
|
+
*
|
|
249
|
+
* Discovery only. The global-package INVENTORY scanners (staleGlobalPkgs,
|
|
250
|
+
* installedGlobalPkgs, doctor) stay top-level-on-purpose: a nested
|
|
251
|
+
* optionalDependency is the shell package's private dependency, so
|
|
252
|
+
* `npm install -g`/`npm uninstall -g` on its name would introduce or orphan a
|
|
253
|
+
* top-level global the user never asked for.
|
|
254
|
+
*/
|
|
255
|
+
function platformPkgBinaries(nodeModulesRoot) {
|
|
256
|
+
const pkg = `code-graph-${PLATFORM}-${ARCH}`;
|
|
257
|
+
return [
|
|
258
|
+
path.join(nodeModulesRoot, '@sdsrs', pkg, BINARY_NAME),
|
|
259
|
+
path.join(nodeModulesRoot, '@sdsrs', 'code-graph', 'node_modules', '@sdsrs', pkg, BINARY_NAME),
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
|
|
233
263
|
function platformBinaryCandidates() {
|
|
234
264
|
const out = [];
|
|
235
265
|
// Fast path: standard module resolution.
|
|
@@ -239,10 +269,11 @@ function platformBinaryCandidates() {
|
|
|
239
269
|
if (isNativeBinary(bin) && isPlausibleReleaseBinary(bin)) out.push(bin);
|
|
240
270
|
} catch { /* not in node_modules walk-up */ }
|
|
241
271
|
|
|
242
|
-
// Slow path: explicit global node_modules probe.
|
|
272
|
+
// Slow path: explicit global node_modules probe, both npm layouts.
|
|
243
273
|
for (const globalRoot of globalNodeModulesCandidates()) {
|
|
244
|
-
const bin
|
|
245
|
-
|
|
274
|
+
for (const bin of platformPkgBinaries(globalRoot)) {
|
|
275
|
+
if (isNativeBinary(bin) && isPlausibleReleaseBinary(bin)) out.push(bin);
|
|
276
|
+
}
|
|
246
277
|
}
|
|
247
278
|
|
|
248
279
|
return out;
|
|
@@ -363,9 +394,12 @@ function findBinaryUncached() {
|
|
|
363
394
|
const npxDir = path.join(os.homedir(), '.npm', '_npx');
|
|
364
395
|
try {
|
|
365
396
|
for (const entry of fs.readdirSync(npxDir)) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
397
|
+
// Same hoisted-or-nested question as the global prefix — an npx tree is
|
|
398
|
+
// just another node_modules root, and npm decides the layout there too.
|
|
399
|
+
for (const bin of platformPkgBinaries(path.join(npxDir, entry, 'node_modules'))) {
|
|
400
|
+
const hit = gate.consider(bin);
|
|
401
|
+
if (hit) return hit;
|
|
402
|
+
}
|
|
369
403
|
}
|
|
370
404
|
} catch { /* no npx cache */ }
|
|
371
405
|
|
|
@@ -588,10 +588,54 @@ function cleanupDisabledStatusline() {
|
|
|
588
588
|
// still run after `/plugin uninstall` — Claude Code stops loading the
|
|
589
589
|
// plugin's hooks.json, so the SessionStart teardown in session-init.js never
|
|
590
590
|
// fires post-uninstall. Without this, the ~40MB cached binary leaked forever.
|
|
591
|
+
//
|
|
592
|
+
// Adoption comes off in the SAME breath, and BEFORE the wipe: install's
|
|
593
|
+
// auto-adopt writes a managed block into every project's CLAUDE.md, and until
|
|
594
|
+
// now nothing reachable removed it. session-init.js owned that step, on the
|
|
595
|
+
// branch its own comment concedes "usually never runs again" after a real
|
|
596
|
+
// uninstall — so the block (steering Claude at a CLI that is being deleted two
|
|
597
|
+
// lines below) survived forever in every adopted repo. The registry that names
|
|
598
|
+
// those projects lives inside CACHE_DIR, which is exactly why this must run
|
|
599
|
+
// first: same capture-before-cleanup ordering the rest of this teardown
|
|
600
|
+
// already learned. This branch fires at most once — the write above removes
|
|
601
|
+
// our composite from settings.json, so Claude Code stops invoking us.
|
|
591
602
|
let cacheRemoved = false;
|
|
592
|
-
|
|
603
|
+
let unadopted = [];
|
|
604
|
+
if (uninstalled) {
|
|
605
|
+
unadopted = unadoptRegisteredProjects();
|
|
606
|
+
cacheRemoved = removeCacheResidue();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return { cleaned: true, settingsChanged, cacheRemoved, unadopted };
|
|
610
|
+
}
|
|
593
611
|
|
|
594
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Strip the managed CLAUDE.md block + generated `.claude/plugin_code_graph_mcp.md`
|
|
614
|
+
* from every project in the adopted-projects registry. `unadopt` is
|
|
615
|
+
* sentinel-guarded, so user prose outside the managed block is preserved and a
|
|
616
|
+
* project that was already cleaned is a no-op.
|
|
617
|
+
*
|
|
618
|
+
* Fully swallowed per project AND overall: this runs inside a statusline render
|
|
619
|
+
* (statusline.js / statusline-composite.js call the caller at their top), where
|
|
620
|
+
* an uncaught throw blanks the user's status line — and an unreadable project
|
|
621
|
+
* must not cost the remaining ones their cleanup.
|
|
622
|
+
*/
|
|
623
|
+
function unadoptRegisteredProjects() {
|
|
624
|
+
const out = [];
|
|
625
|
+
let readAdoptedProjects, unadopt;
|
|
626
|
+
try { ({ readAdoptedProjects, unadopt } = require('./adopt')); }
|
|
627
|
+
catch { return out; } // POSIX-only helper unavailable — teardown continues
|
|
628
|
+
let projects = [];
|
|
629
|
+
try { projects = readAdoptedProjects() || []; } catch { return out; }
|
|
630
|
+
for (const project of projects) {
|
|
631
|
+
try {
|
|
632
|
+
const r = unadopt({ cwd: project });
|
|
633
|
+
out.push({ project, cleaned: !!(r && (r.blockPruned || r.fileRemoved || r.claudeMdRemoved)) });
|
|
634
|
+
} catch (e) {
|
|
635
|
+
out.push({ project, cleaned: false, error: (e && e.message) || String(e) });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
595
639
|
}
|
|
596
640
|
|
|
597
641
|
// --- Scope Conflict Detection ---
|
|
@@ -1690,7 +1734,7 @@ function removeCacheResidue() {
|
|
|
1690
1734
|
module.exports = {
|
|
1691
1735
|
install, uninstall, update, healthCheck, scanForBrokenPaths, checkScopeConflict,
|
|
1692
1736
|
isPluginExplicitlyDisabled, isPluginInactive, isPluginUninstalled, removeCacheResidue,
|
|
1693
|
-
cleanupDisabledStatusline,
|
|
1737
|
+
cleanupDisabledStatusline, unadoptRegisteredProjects,
|
|
1694
1738
|
readManifest, readJson, readJsonResult, readSettingsForWrite, writeJsonAtomic,
|
|
1695
1739
|
readRegistry, readRegistryForWrite, writeRegistry,
|
|
1696
1740
|
getPluginVersion, cleanupOldCacheVersions,
|
|
@@ -406,6 +406,30 @@ function ensureIndexFresh() {
|
|
|
406
406
|
return 'refreshing';
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
/**
|
|
410
|
+
* What to tell a user whose binary is missing. A missing binary is the NORMAL
|
|
411
|
+
* state of the first session after `/plugin install` — nothing ships the ~40MB
|
|
412
|
+
* engine with the plugin — and both automatic install paths are already
|
|
413
|
+
* running by the time the user reads this: launchBackgroundAutoUpdate() below
|
|
414
|
+
* (a missing binary bypasses the check throttle) and the MCP launcher's own
|
|
415
|
+
* install chain. Telling that user "MCP server cannot start. Install: npm
|
|
416
|
+
* install -g" reads as a failed install and sends them to fix something that
|
|
417
|
+
* is already fixing itself.
|
|
418
|
+
*
|
|
419
|
+
* The manual instruction is still the right answer when the user has opted out
|
|
420
|
+
* of auto-update, because then nothing else will fetch it. Pure so both arms
|
|
421
|
+
* are testable without a binary-less machine.
|
|
422
|
+
*/
|
|
423
|
+
function missingBinaryMessage(env = process.env) {
|
|
424
|
+
if (env.CODE_GRAPH_NO_AUTO_UPDATE === '1') {
|
|
425
|
+
return '[code-graph] Binary not found, and auto-download is off (CODE_GRAPH_NO_AUTO_UPDATE=1).\n' +
|
|
426
|
+
' Install it yourself: npm install -g @sdsrs/code-graph\n';
|
|
427
|
+
}
|
|
428
|
+
return '[code-graph] Binary not found — fetching it in the background (~40MB, first run only).\n' +
|
|
429
|
+
' Tools appear as soon as it lands; no restart needed.\n' +
|
|
430
|
+
' Still missing next session? Run `code-graph-mcp doctor`.\n';
|
|
431
|
+
}
|
|
432
|
+
|
|
409
433
|
/**
|
|
410
434
|
* Verify binary is available and executable.
|
|
411
435
|
* On macOS, detect Gatekeeper quarantine (common after npm/GitHub download).
|
|
@@ -415,10 +439,7 @@ function verifyBinary() {
|
|
|
415
439
|
const { findBinary } = require('./find-binary');
|
|
416
440
|
const binary = findBinary();
|
|
417
441
|
if (!binary) {
|
|
418
|
-
process.stderr.write(
|
|
419
|
-
'[code-graph] Binary not found — MCP server cannot start.\n' +
|
|
420
|
-
'Install: npm install -g @sdsrs/code-graph\n'
|
|
421
|
-
);
|
|
442
|
+
process.stderr.write(missingBinaryMessage());
|
|
422
443
|
return { available: false, binary: null };
|
|
423
444
|
}
|
|
424
445
|
|
|
@@ -527,6 +548,18 @@ function consistencyCheck(binary) {
|
|
|
527
548
|
return issues;
|
|
528
549
|
}
|
|
529
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Do two paths name the same project? The registry stores what `adopt` was
|
|
553
|
+
* given and `process.cwd()` is what the shell resolved, so a symlinked repo
|
|
554
|
+
* path (`/tmp` → `/private/tmp` on macOS) compares unequal as raw strings.
|
|
555
|
+
* realpath both, fall back to the resolved literal when a side no longer exists.
|
|
556
|
+
*/
|
|
557
|
+
function samePath(a, b) {
|
|
558
|
+
if (!a || !b) return false;
|
|
559
|
+
const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
560
|
+
return real(a) === real(b);
|
|
561
|
+
}
|
|
562
|
+
|
|
530
563
|
function runSessionInit({ source } = {}) {
|
|
531
564
|
// GC the shared tmp dir before anything else, so it happens even on the
|
|
532
565
|
// inactive / non-project early returns below — those sessions still wrote
|
|
@@ -543,7 +576,8 @@ function runSessionInit({ source } = {}) {
|
|
|
543
576
|
// Third caller of the same unguarded teardown (statusline.js and
|
|
544
577
|
// statusline-composite.js are the others): a read-only ~/.claude turns this
|
|
545
578
|
// into an uncaught throw that takes down the whole SessionStart hook.
|
|
546
|
-
|
|
579
|
+
let cleanup = null;
|
|
580
|
+
try { cleanup = cleanupDisabledStatusline(); } catch { /* best-effort teardown */ }
|
|
547
581
|
// Genuine uninstall (not a temporary disable) leaves residue the settings-only
|
|
548
582
|
// self-heal can't reach: ~/.cache/code-graph (the ~40MB binary + state) and the
|
|
549
583
|
// current project's CLAUDE.md adoption block. CC fires no uninstall hook, AND it
|
|
@@ -570,9 +604,18 @@ function runSessionInit({ source } = {}) {
|
|
|
570
604
|
// other projects is the preservation inside removeCacheResidue(); this
|
|
571
605
|
// ordering is the belt to that pair of braces, and it is what keeps the
|
|
572
606
|
// single-project case from depending on the preservation at all.
|
|
573
|
-
|
|
607
|
+
//
|
|
608
|
+
// cleanupDisabledStatusline() now sweeps the WHOLE adopted-projects
|
|
609
|
+
// registry (it is the only teardown that still runs after a real
|
|
610
|
+
// uninstall), so this cwd is usually already clean by the time we get
|
|
611
|
+
// here. Fold that in rather than re-deriving it: with the work moved
|
|
612
|
+
// earlier, the local `unadopt` returns "nothing to clean" and reporting
|
|
613
|
+
// `unadopted:false` for a project whose block IS gone would be a false
|
|
614
|
+
// negative in the one field a caller could act on.
|
|
615
|
+
let unadopted = !!(cleanup && Array.isArray(cleanup.unadopted)
|
|
616
|
+
&& cleanup.unadopted.some((u) => u && u.cleaned && samePath(u.project, process.cwd())));
|
|
574
617
|
try {
|
|
575
|
-
if (!isNonProjectCwd(process.cwd())) {
|
|
618
|
+
if (!unadopted && !isNonProjectCwd(process.cwd())) {
|
|
576
619
|
const r = unadopt({ cwd: process.cwd() });
|
|
577
620
|
unadopted = !!(r && (r.blockPruned || r.fileRemoved || r.claudeMdRemoved));
|
|
578
621
|
}
|
|
@@ -924,7 +967,7 @@ module.exports = {
|
|
|
924
967
|
indexNeedsRevalidation,
|
|
925
968
|
injectProjectMap,
|
|
926
969
|
injectRecentImpact,
|
|
927
|
-
verifyBinary,
|
|
970
|
+
verifyBinary, missingBinaryMessage,
|
|
928
971
|
consistencyCheck,
|
|
929
972
|
runSessionInit,
|
|
930
973
|
computeQuietHooks,
|
|
@@ -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.120.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.120.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.120.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.120.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.120.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.120.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.120.0"
|
|
43
43
|
}
|
|
44
44
|
}
|