@sdsrs/code-graph 0.138.0 → 0.140.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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.138.0",
7
+ "version": "0.140.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -35,7 +35,7 @@ function commandExists(cmd) {
35
35
 
36
36
  // ── Configuration ──────────────────────────────────────────
37
37
  const GITHUB_REPO = 'sdsrss/code-graph-mcp';
38
- const STATE_FILE = path.join(CACHE_DIR, 'update-state.json');
38
+ const { UPDATE_STATE_FILE: STATE_FILE, MANIFEST_FILE, INSTALL_LOCK_FILE } = require('./cache-paths');
39
39
  const BINARY_CACHE_DIR = path.join(CACHE_DIR, 'bin');
40
40
  const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h — steady-state re-check
41
41
  const UP_TO_DATE_RECHECK_MS = 30 * 60 * 1000; // 30min — re-verify an "up to date" result (release-race guard)
@@ -956,7 +956,7 @@ async function downloadAndInstall(latest, {
956
956
  const manifest = readManifest();
957
957
  manifest.version = latest.version;
958
958
  manifest.updatedAt = new Date().toISOString();
959
- writeJsonAtomic(path.join(CACHE_DIR, 'install-manifest.json'), manifest);
959
+ writeJsonAtomic(MANIFEST_FILE, manifest);
960
960
  } catch { /* not fatal */ }
961
961
  }
962
962
 
@@ -1272,7 +1272,7 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
1272
1272
  // shouldHealGlobalsOnThrottle). Cheap local check first; only the actually-
1273
1273
  // stale case takes the slow, lock-guarded npm path.
1274
1274
  if (shouldHealGlobalsOnThrottle(state)) {
1275
- installLock = acquireLock(path.join(CACHE_DIR, 'install.lock'));
1275
+ installLock = acquireLock(INSTALL_LOCK_FILE);
1276
1276
  if (installLock) {
1277
1277
  const globalHeal = await selfHealGlobalPkgs({ version: state.latestVersion }, state);
1278
1278
  saveState({ ...readState(), ...globalHeal });
@@ -1317,7 +1317,7 @@ async function checkForUpdate({ installMissing = false, force = false, requestJs
1317
1317
  // it marks that with CODE_GRAPH_INSTALL_LOCK_HELD so we don't deadlock
1318
1318
  // against our own parent.
1319
1319
  if (process.env.CODE_GRAPH_INSTALL_LOCK_HELD !== '1') {
1320
- installLock = acquireLock(path.join(CACHE_DIR, 'install.lock'));
1320
+ installLock = acquireLock(INSTALL_LOCK_FILE);
1321
1321
  // Reason, not null — same misreport as the fetch failure above: the
1322
1322
  // holder is mid-update, so "Up to date (v<old>)" is exactly wrong.
1323
1323
  if (!installLock) return { noop: true, reason: 'install-lock-held', from: installedVersion };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+ // Single home for the `~/.cache/code-graph` file names.
3
+ //
4
+ // JS-05 (audit 2026-09-05): `update-state.json` was spelled in five modules,
5
+ // `install.lock` in two, `install-manifest.json` in three — and two of those
6
+ // rebuilt the whole path out of `os.homedir()` rather than joining `CACHE_DIR`,
7
+ // so the modules that would break first if the cache layout moved were also the
8
+ // two that never load the module where it was defined. A rename had to be found
9
+ // by grep. `tests/hardening.rs::cache_file_names_have_exactly_one_spelling`
10
+ // keeps this the only file that may spell them.
11
+ //
12
+ // Deliberately tiny and dependency-free beyond node builtins, for ONE consumer:
13
+ // `user-prompt-context.js` runs on the UserPromptSubmit hook path and does not
14
+ // load `lifecycle.js`, so taking a path string from there would cost it the
15
+ // whole module. Measured at 30.1 ms as shipped versus 34.5 ms if it also loaded
16
+ // `lifecycle.js` (+14%).
17
+ //
18
+ // `statusline.js` was the other hardcoded spelling and is NOT a reason for this
19
+ // file — it already does `require('./lifecycle')`, so its copy of the three path
20
+ // segments bought nothing. An earlier version of this comment claimed both
21
+ // modules needed the split (pre-ship review 2026-09-07).
22
+ const os = require('os');
23
+ const path = require('path');
24
+
25
+ const CACHE_DIR = path.join(os.homedir(), '.cache', 'code-graph');
26
+
27
+ /** Auto-update bookkeeping: available version, attempt counters, last error. */
28
+ const UPDATE_STATE_FILE = path.join(CACHE_DIR, 'update-state.json');
29
+ /** What the last install wrote, and where. Absent = never installed. */
30
+ const MANIFEST_FILE = path.join(CACHE_DIR, 'install-manifest.json');
31
+ /** Inter-process install lock (see install-lock.js). */
32
+ const INSTALL_LOCK_FILE = path.join(CACHE_DIR, 'install.lock');
33
+
34
+ module.exports = {
35
+ CACHE_DIR,
36
+ UPDATE_STATE_FILE,
37
+ MANIFEST_FILE,
38
+ INSTALL_LOCK_FILE,
39
+ };
@@ -6,10 +6,11 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
8
8
  const {
9
- getPluginVersion, readJson, readJsonResult, healthCheck, scanForBrokenPaths, CACHE_DIR,
9
+ getPluginVersion, readJson, readJsonResult, healthCheck, scanForBrokenPaths,
10
10
  settingsPath, surveyHookCoverage,
11
11
  installedGlobalPkgs, GLOBAL_INSTALL_MARKER, SHELL_PKG,
12
12
  } = require('./lifecycle');
13
+ const { UPDATE_STATE_FILE } = require('./cache-paths');
13
14
  const { findBinary, clearCache: clearBinaryCache } = require('./find-binary');
14
15
  const { hidden } = require('./proc-opts');
15
16
  const { MAX_UPDATE_ATTEMPTS, GLOBAL_PKG_HEAL_MAX_ATTEMPTS, isBinaryHealExhausted, readState } = require('./auto-update');
@@ -589,7 +590,7 @@ function runDiagnostics({ checkOnly = false } = {}) {
589
590
  // this line used to hardcode 3 while the comment named the constant, so
590
591
  // raising the cap in auto-update.js would have left this diagnosis
591
592
  // reporting the old one (audit 2026-08-29 JS-07).
592
- const state = readJson(path.join(CACHE_DIR, 'update-state.json')) || {};
593
+ const state = readJson(UPDATE_STATE_FILE) || {};
593
594
  const healGaveUp = (state.globalPkgHealAttempts || 0) >= GLOBAL_PKG_HEAL_MAX_ATTEMPTS;
594
595
  results.push({
595
596
  name: 'Global npm packages',
@@ -858,7 +859,7 @@ function probeHealth({ find = findBinary } = {}) {
858
859
  }
859
860
 
860
861
  function readUpdateState() {
861
- try { return readJson(path.join(CACHE_DIR, 'update-state.json')); } catch { return null; }
862
+ try { return readJson(UPDATE_STATE_FILE); } catch { return null; }
862
863
  }
863
864
 
864
865
  /**
@@ -33,6 +33,34 @@ function pathProbeTimeoutMs(remaining = require('./hook-fail-open').remainingMs)
33
33
  const ms = left === null ? PATH_PROBE_MIN_MS : Math.floor(left);
34
34
  return Math.max(ms, PATH_PROBE_MIN_MS);
35
35
  }
36
+
37
+ // `readBinaryVersion`'s own default. Kept here as the number this module asks
38
+ // for so the clamp below is expressed against the same figure the callee would
39
+ // have used on its own.
40
+ const VERSION_PROBE_TIMEOUT_MS = 5000;
41
+
42
+ /**
43
+ * How long ONE `--version` probe in the discovery chain may run (JS-23, audit
44
+ * 2026-09-07).
45
+ *
46
+ * The version gate execs the candidate binary, and `findBinary()` walks up to
47
+ * six candidates — so a chain of default 5 s probes is 30 s of children on the
48
+ * first thing every hook does, all of it BEFORE the hook has consulted its
49
+ * budget even once. Cold cache (first install, a version bump,
50
+ * `auto-update.clearCache()`) is exactly when the chain is longest and every
51
+ * candidate is a cold page-in.
52
+ *
53
+ * Same shape and the same two edges as `pathProbeTimeoutMs`: never 0 (node
54
+ * reads `timeout: 0` as no timeout), never fractional (ERR_OUT_OF_RANGE, which
55
+ * `readBinaryVersion`'s catch would report as "no version" → a good binary
56
+ * judged stale). With no deadline armed — doctor, statusline, the launcher —
57
+ * it returns the unchanged 5 s default.
58
+ */
59
+ function versionProbeTimeoutMs(remaining = require('./hook-fail-open').remainingMs) {
60
+ const left = remaining(VERSION_PROBE_TIMEOUT_MS);
61
+ const ms = left === null ? PATH_PROBE_MIN_MS : Math.floor(left);
62
+ return Math.max(ms, PATH_PROBE_MIN_MS);
63
+ }
36
64
  const PLATFORM_PKG = `@sdsrs/code-graph-${PLATFORM}-${ARCH}`;
37
65
 
38
66
  /**
@@ -129,9 +157,13 @@ function globalNodeModulesCandidates() {
129
157
 
130
158
  // 4. Last resort: ask npm directly. Slow (~50-200ms) but most accurate when
131
159
  // user has a non-standard prefix. Cached at the disk-cache layer above.
160
+ // Budget-aware like the sibling probes in this file (JS-23 / pre-ship review
161
+ // 2026-09-07): this runs on the same `findBinaryUncached()` chain, and every
162
+ // hook calls `findBinary()` before it consults its own budget. `remainingMs`
163
+ // returns the default unchanged when nothing armed a deadline.
132
164
  try {
133
165
  const npm = npmInvocation(['root', '-g'], {
134
- timeout: 2000,
166
+ timeout: pathProbeTimeoutMs(),
135
167
  stdio: ['pipe', 'pipe', 'pipe'],
136
168
  encoding: 'utf8',
137
169
  });
@@ -186,7 +218,9 @@ function isCachedBinaryFresh(cachedPath, pkgVersion, knownVersion) {
186
218
  // `knownVersion` lets the caller skip the `--version` spawn when the cache
187
219
  // entry already recorded it AND the file has not changed since — see
188
220
  // `readCacheEntry`. Absent, behaviour is exactly as before.
189
- const cacheVer = knownVersion || readBinaryVersion(cachedPath);
221
+ // Clamped: this fires whenever the cache stamp mismatches — i.e. right after
222
+ // an auto-update, on the hook path (JS-23 / pre-ship review 2026-09-07).
223
+ const cacheVer = knownVersion || readBinaryVersion(cachedPath, { timeoutMs: versionProbeTimeoutMs() });
190
224
  if (!cacheVer) return true;
191
225
  return compareVersions(cacheVer, pkgVersion) >= 0;
192
226
  }
@@ -243,7 +277,9 @@ function writeCacheEntry(binPath) {
243
277
  CACHE_FILE,
244
278
  JSON.stringify({
245
279
  path: binPath,
246
- version: readBinaryVersion(binPath) || null,
280
+ // Clamped: this is on the COLD-CACHE path, which is exactly the case
281
+ // JS-23 is about (pre-ship review 2026-09-07).
282
+ version: readBinaryVersion(binPath, { timeoutMs: versionProbeTimeoutMs() }) || null,
247
283
  stamp: binaryStamp(binPath),
248
284
  })
249
285
  );
@@ -396,13 +432,19 @@ function findPlatformBinary() {
396
432
  * gracefully on a slightly-old binary than on "offline", and the stale-binary
397
433
  * self-heal in auto-update.js re-downloads shortly anyway.
398
434
  */
399
- function createVersionGate(pkgVersion, { readVersion = readBinaryVersion } = {}) {
435
+ function createVersionGate(
436
+ pkgVersion,
437
+ { readVersion = readBinaryVersion, probeTimeoutMs = versionProbeTimeoutMs } = {},
438
+ ) {
400
439
  let bestStale = null;
401
440
  return {
402
441
  consider(bin) {
403
442
  if (!isNativeBinary(bin)) return null;
404
443
  if (!pkgVersion) return bin;
405
- const ver = readVersion(bin);
444
+ // Re-read per candidate, not once for the gate: the budget shrinks as the
445
+ // chain walks, and the point is that candidate six cannot still ask for a
446
+ // fresh 5 s (JS-23).
447
+ const ver = readVersion(bin, { timeoutMs: probeTimeoutMs() });
406
448
  if (!ver || compareVersions(ver, pkgVersion) >= 0) return bin;
407
449
  if (!bestStale || compareVersions(ver, bestStale.ver) > 0) bestStale = { bin, ver };
408
450
  return null;
@@ -554,6 +596,7 @@ module.exports = {
554
596
  detectLibc, unsupportedPlatformHint,
555
597
  CACHE_FILE, BINARY_NAME, PLATFORM_PKG,
556
598
  pathProbeTimeoutMs, PATH_PROBE_TIMEOUT_MS, PATH_PROBE_MIN_MS,
599
+ versionProbeTimeoutMs, VERSION_PROBE_TIMEOUT_MS,
557
600
  };
558
601
 
559
602
  // Allow direct invocation for testing
@@ -12,7 +12,15 @@ const OLD_PLUGIN_IDS = [
12
12
  'code-graph@sdsrss-code-graph', // v2 legacy ID (pre-rename)
13
13
  ];
14
14
  const MARKETPLACE_NAME = 'code-graph-mcp';
15
- const CACHE_DIR = path.join(os.homedir(), '.cache', 'code-graph');
15
+ // Imported, not defined here: five other modules needed these three names and
16
+ // one of them — `user-prompt-context.js`, on the UserPromptSubmit hook path —
17
+ // cannot afford to load this file for them (see cache-paths.js, JS-05).
18
+ //
19
+ // The export surface below is unchanged. Measured rather than asserted, after
20
+ // the first version of this comment claimed `MANIFEST_FILE` was exported from
21
+ // here: `CACHE_DIR` and `INSTALL_LOCK_FILE` are exported and still are;
22
+ // `MANIFEST_FILE` is module-private and always was, so nothing importable moved.
23
+ const { CACHE_DIR, MANIFEST_FILE, INSTALL_LOCK_FILE } = require('./cache-paths');
16
24
  // Bound for the `ps` fallback in getActiveCmdlines (pre-ship review 2026-09-06).
17
25
  // 2 s matches the other hook-path probes; the floor is what a budget-exhausted
18
26
  // hook still gives it, since an empty list degrades to recency-only.
@@ -22,14 +30,12 @@ const PS_PROBE_MIN_MS = 250;
22
30
  // plugins when hooks run in shared process context (e.g. claude-mem-lite sets it
23
31
  // to its own marketplace path, polluting all subsequent settings.json hook processes).
24
32
  const PLUGIN_ROOT = path.resolve(__dirname, '..');
25
- const MANIFEST_FILE = path.join(CACHE_DIR, 'install-manifest.json');
26
33
  const REGISTRY_FILE = path.join(CACHE_DIR, 'statusline-registry.json');
27
34
  // Written by the launcher's background install when ITS `npm install -g` step
28
35
  // introduced the global shell + platform packages. Uninstall only removes
29
36
  // global packages it can prove the plugin installed (marker present) or when
30
37
  // the user passes --purge-global — a deliberate user install is never yanked.
31
38
  const GLOBAL_INSTALL_MARKER = path.join(CACHE_DIR, 'global-install-marker.json');
32
- const INSTALL_LOCK_FILE = path.join(CACHE_DIR, 'install.lock');
33
39
  const SHELL_PKG = '@sdsrs/code-graph';
34
40
 
35
41
  // Lazy resolvers — Claude Code's config dir can be overridden by CLAUDE_CONFIG_DIR
@@ -1610,8 +1616,18 @@ function uninstall({ purgeGlobal = false, unadoptAll = false, runNpm = defaultRu
1610
1616
  }
1611
1617
  }
1612
1618
 
1613
- // 6. Remove cache directory
1614
- try { fs.rmSync(CACHE_DIR, { recursive: true, force: true }); } catch { /* ok */ }
1619
+ // 6. Remove cache directory — through `removeCacheResidue()`, not a bare
1620
+ // rmSync (JS-17, audit 2026-09-07). Without `--unadopt-all` the sweep above
1621
+ // does not run, so the registry is still NON-EMPTY here and a wholesale wipe
1622
+ // strands every managed block — while step 8's own guidance, and `cli.js`,
1623
+ // tell the user their next move is `--unadopt-all`, which then reads an
1624
+ // absent registry and reports nothing to clean. `adoptedProjects` was
1625
+ // captured at 5.5 for the RETURN value; that capture never protected the
1626
+ // file. With `--unadopt-all` the sweep leaves the registry empty (or holding
1627
+ // only projects whose CLAUDE.md could not be rewritten, which is exactly what
1628
+ // must survive), and `removeCacheResidue` preserves only a non-empty one, so
1629
+ // the clean case still leaves nothing behind.
1630
+ try { removeCacheResidue(); } catch { /* ok */ }
1615
1631
 
1616
1632
  // 6.5. The shared tmp dir (cooldown flags, read-fanout state, interrupted
1617
1633
  // `update-*` staging). Nothing in it outlives an uninstall, and the periodic
@@ -1948,12 +1964,14 @@ function isPluginUninstalled(settings = readJson(settingsPath()) || {}) {
1948
1964
  // of which repos carry a managed CLAUDE.md block. Wiping it strands every block
1949
1965
  // — `uninstall({unadoptAll:true})` afterwards reads an empty registry, reports
1950
1966
  // `unadopted: []`, and the blocks stay in the user's repos with nothing left
1951
- // that knows where they are. `uninstall()` captures the list before calling
1952
- // here; `cleanupDisabledStatusline` does not, and by this function's own comment
1953
- // that is the ONE path guaranteed to run after `/plugin uninstall`. So the
1954
- // preservation belongs here, at the wipe, rather than at each caller — the same
1955
- // "fix it at the shared layer, not per surface" the <external> query filter
1956
- // needed.
1967
+ // that knows where they are. No caller can be trusted to protect it first:
1968
+ // `cleanupDisabledStatusline` cannot (it is the ONE path guaranteed to run after
1969
+ // a CC `/plugin uninstall`, and it has no list), and `uninstall()` did not —
1970
+ // it captured the list for its RETURN value and then wiped the file with a bare
1971
+ // rmSync, which is the JS-17 data loss (audit 2026-09-07); it routes through
1972
+ // here now. So the preservation belongs here, at the wipe, rather than at each
1973
+ // caller — the same "fix it at the shared layer, not per surface" the
1974
+ // <external> query filter needed.
1957
1975
  function removeCacheResidue() {
1958
1976
  // Path comes from adopt.js rather than a second spelling of the basename —
1959
1977
  // a literal here would silently stop matching the day adopt.js renames it,
@@ -8,6 +8,7 @@ const {
8
8
  cleanupDisabledStatusline, isPluginInactive, isPluginUninstalled, removeCacheResidue,
9
9
  readJson, CACHE_DIR, settingsPath, isStaleRelicContext, hookCmdScript,
10
10
  } = require('./lifecycle');
11
+ const { UPDATE_STATE_FILE } = require('./cache-paths');
11
12
  const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
12
13
  const { maybeAutoAdopt, isAdopted, unadopt } = require('./adopt');
13
14
  const { isNonProjectCwd } = require('./project-detect');
@@ -604,7 +605,7 @@ function consistencyCheck(binary) {
604
605
 
605
606
  // Check 3: Auto-update incomplete
606
607
  try {
607
- const statePath = path.join(CACHE_DIR, 'update-state.json');
608
+ const statePath = UPDATE_STATE_FILE;
608
609
  const state = readJson(statePath);
609
610
  if (state && state.updateAvailable && state.binaryUpdated === false) {
610
611
  issues.push({
@@ -10,11 +10,11 @@ if (require.main === module) require('./hook-fail-open').installHookFailOpen('st
10
10
 
11
11
  const { execFileSync } = require('child_process');
12
12
  const fs = require('fs');
13
- const os = require('os');
14
13
  const path = require('path');
15
14
  const { findBinary } = require('./find-binary');
16
15
  const { resolveProjectRoot } = require('./project-root');
17
16
  const lifecycle = require('./lifecycle');
17
+ const { UPDATE_STATE_FILE } = require('./cache-paths');
18
18
  const { hidden } = require('./proc-opts');
19
19
  const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() => ({ cleaned: false }));
20
20
 
@@ -29,8 +29,7 @@ const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() =>
29
29
  const STUCK_UPDATE_ATTEMPTS = 5;
30
30
  function readUpdateState() {
31
31
  try {
32
- return JSON.parse(fs.readFileSync(
33
- path.join(os.homedir(), '.cache', 'code-graph', 'update-state.json'), 'utf8'));
32
+ return JSON.parse(fs.readFileSync(UPDATE_STATE_FILE, 'utf8'));
34
33
  } catch { return null; /* no state file or unreadable */ }
35
34
  }
36
35
  function updatePending(st = readUpdateState()) {
@@ -8,13 +8,27 @@
8
8
  // would swallow the test's own failures.
9
9
  if (require.main === module) require('./hook-fail-open').installHookFailOpen('UserPromptSubmit');
10
10
 
11
+ // Default per-child allowance, spent through the hook budget below.
12
+ const CHILD_TIMEOUT_MS = 3000;
13
+
14
+ /**
15
+ * How long ONE child of this hook may run: the default when nothing armed a
16
+ * deadline (a test importing this module), or whatever is LEFT of the
17
+ * UserPromptSubmit budget. `null` means the budget is exhausted — do not run.
18
+ *
19
+ * Exported so the three states are testable; `run()` below is a closure over
20
+ * the resolved binary and cannot be reached from a test (JS-18).
21
+ */
22
+ function childBudgetMs() {
23
+ return require('./hook-fail-open').remainingMs(CHILD_TIMEOUT_MS);
24
+ }
25
+
11
26
  // UserPromptSubmit hook: inject relevant code-graph RESULTS based on user's intent.
12
27
  // Strategy: PUSH structural context (not suggestions) that Grep/Read cannot provide.
13
28
  // This is a CODE INDEX — only inject structural code context (impact, overview, callgraph).
14
29
  const { execFileSync } = require('child_process');
15
30
  const fs = require('fs');
16
31
  const path = require('path');
17
- const os = require('os');
18
32
  // Hook cooldown flags + the restart notice go under cgTmpDir() (a code-graph-mcp/
19
33
  // subdir of os.tmpdir()), NOT bare os.tmpdir(): Claude Code overrides $TMPDIR to
20
34
  // ~/.claude/tmp/, so bare-tmp flags interleave with transcript captures —
@@ -23,8 +37,11 @@ const os = require('os');
23
37
  const { cgTmpDir, cwdHash } = require('./tmp-dir');
24
38
  const { hidden } = require('./proc-opts');
25
39
 
26
- // Mid-session install detection: hook fires but no manifest yet.
27
- const MANIFEST_PATH = path.join(os.homedir(), '.cache', 'code-graph', 'install-manifest.json');
40
+ // Mid-session install detection: hook fires but no manifest yet. The path comes
41
+ // from `cache-paths.js` — deliberately NOT from `lifecycle.js`, which this hook
42
+ // must not load (startup budget); it used to be a third hand-spelled copy of the
43
+ // same three path segments (JS-05).
44
+ const { MANIFEST_FILE: MANIFEST_PATH } = require('./cache-paths');
28
45
 
29
46
  // --- Per-type rate limiting (replaces single global cooldown) ---
30
47
  const COOLDOWNS = {
@@ -516,9 +533,18 @@ function runMain() {
516
533
  if (!binary) return;
517
534
 
518
535
  function run(args) {
536
+ // JS-18 (audit 2026-09-07): spend the UserPromptSubmit budget, don't ask
537
+ // for a fresh 3 s. This file armed a deadline at line 1 and then never
538
+ // consulted it — the literal here plus the `findBinary()` chain above ran
539
+ // outside it, so a cold cache had the hook killed by Claude Code and the
540
+ // user saw a hook error on THEIR prompt. `null` means the budget is gone:
541
+ // do not run (node reads `timeout: 0` as no timeout at all). Returning ''
542
+ // lands on the same "nothing to say" path as an empty result.
543
+ const budget = childBudgetMs();
544
+ if (budget === null) return '';
519
545
  return execFileSync(binary, args, hidden({
520
546
  cwd,
521
- timeout: 3000,
547
+ timeout: budget,
522
548
  encoding: 'utf8',
523
549
  stdio: ['pipe', 'pipe', 'pipe'],
524
550
  env: buildRunEnv(),
@@ -551,4 +577,4 @@ if (require.main === module) {
551
577
  runMain();
552
578
  }
553
579
 
554
- module.exports = { COOLDOWNS, shouldSkip, extractFilePaths, extractSymbols, detectIntents, scoreIntent, INTENT_PATTERNS, INTENT_THRESHOLD, determineQueryType, computeQuietHooks, STOP_WORDS, PLAIN_WORD_EXCLUDE, hasSymptom, SYMPTOM_PATTERNS, buildRunEnv };
580
+ module.exports = { COOLDOWNS, shouldSkip, extractFilePaths, extractSymbols, detectIntents, scoreIntent, INTENT_PATTERNS, INTENT_THRESHOLD, determineQueryType, computeQuietHooks, STOP_WORDS, PLAIN_WORD_EXCLUDE, hasSymptom, SYMPTOM_PATTERNS, buildRunEnv, childBudgetMs, CHILD_TIMEOUT_MS };
@@ -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.138.0 code-graph-mcp snapshot create --out snapshot.db
38
+ npx -y -p @sdsrs/code-graph@0.140.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.138.0",
3
+ "version": "0.140.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.138.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.138.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.138.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.138.0",
42
- "@sdsrs/code-graph-win32-x64": "0.138.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.140.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.140.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.140.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.140.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.140.0"
43
43
  }
44
44
  }