@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.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/auto-update.js +4 -4
- package/claude-plugin/scripts/cache-paths.js +39 -0
- package/claude-plugin/scripts/doctor.js +4 -3
- package/claude-plugin/scripts/find-binary.js +48 -5
- package/claude-plugin/scripts/lifecycle.js +29 -11
- package/claude-plugin/scripts/session-init.js +2 -1
- package/claude-plugin/scripts/statusline.js +2 -3
- package/claude-plugin/scripts/user-prompt-context.js +31 -5
- package/claude-plugin/templates/code-graph-snapshot.yml +1 -1
- package/package.json +6 -6
|
@@ -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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1952
|
-
//
|
|
1953
|
-
//
|
|
1954
|
-
//
|
|
1955
|
-
//
|
|
1956
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.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
|
}
|