@sdsrs/code-graph 0.79.1 → 0.80.1
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 +6 -0
- package/claude-plugin/scripts/project-detect.js +27 -5
- package/claude-plugin/scripts/project-detect.test.js +23 -1
- package/claude-plugin/scripts/statusline.js +12 -1
- package/claude-plugin/scripts/statusline.test.js +25 -0
- package/claude-plugin/scripts/user-prompt-context.js +9 -3
- package/package.json +6 -6
|
@@ -520,6 +520,12 @@ async function checkForUpdate({ installMissing = false } = {}) {
|
|
|
520
520
|
installedVersion: success ? latest.version : installedVersion,
|
|
521
521
|
latestVersion: latest.version,
|
|
522
522
|
updateAvailable: !success,
|
|
523
|
+
// Consecutive failed-download counter. The statusline shows "↻ updating"
|
|
524
|
+
// while updateAvailable is set; without a bound, a persistently-failing
|
|
525
|
+
// update (missing tar/curl, full disk, blocked network) pins "updating"
|
|
526
|
+
// forever, asserting a self-heal that never happens. The statusline stops
|
|
527
|
+
// trusting it past STUCK_UPDATE_ATTEMPTS; success resets to 0.
|
|
528
|
+
updateAttempts: success ? 0 : (state.updateAttempts || 0) + 1,
|
|
523
529
|
lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
|
|
524
530
|
rateLimited: false,
|
|
525
531
|
binaryUpdated: result.binaryUpdated,
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// this tool, so counting it would let a once-polluted /tmp self-certify as a
|
|
23
23
|
// project on the next session (circular).
|
|
24
24
|
const fs = require('fs');
|
|
25
|
+
const os = require('os');
|
|
25
26
|
const path = require('path');
|
|
26
27
|
|
|
27
28
|
const PROJECT_MARKERS = [
|
|
@@ -33,11 +34,32 @@ function isProjectRoot(cwd) {
|
|
|
33
34
|
return PROJECT_MARKERS.some(m => fs.existsSync(path.join(cwd, m)));
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
37
|
+
// Walk up from `cwd` to the nearest ancestor carrying a project marker, bounded
|
|
38
|
+
// by $HOME (exclusive) and the filesystem root. Returns the marker dir, or null
|
|
39
|
+
// if none. Mirrors the Rust binary's `resolve_project_root_bounded` so the JS
|
|
40
|
+
// activation gate and the binary AGREE: before this, the gate checked ONLY the
|
|
41
|
+
// literal cwd, so launching from a marker-less monorepo SUBDIR (e.g.
|
|
42
|
+
// `repo/backend/` with `.git` only at `repo/`) classified it non-project and
|
|
43
|
+
// served the 0-tool stub — even though the binary would have resolved `repo/`
|
|
44
|
+
// and answered queries. The $HOME-exclusive bound keeps every /tmp / headless
|
|
45
|
+
// cwd non-project (a stray marker in $HOME must not certify them).
|
|
46
|
+
function findProjectRoot(cwd = process.cwd()) {
|
|
47
|
+
const home = os.homedir();
|
|
48
|
+
let dir = path.resolve(cwd);
|
|
49
|
+
for (;;) {
|
|
50
|
+
if (dir === home) return null; // $HOME-exclusive: don't certify $HOME or above
|
|
51
|
+
if (isProjectRoot(dir)) return dir;
|
|
52
|
+
const parent = path.dirname(dir);
|
|
53
|
+
if (parent === dir) return null; // filesystem root
|
|
54
|
+
dir = parent;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A cwd is "non-project" when neither it NOR any ancestor (up to $HOME) carries a
|
|
59
|
+
// recognized project marker. The plugin's activation gates short-circuit there:
|
|
60
|
+
// no MCP tools, no index creation, no SessionStart map injection, no auto-adoption.
|
|
39
61
|
function isNonProjectCwd(cwd = process.cwd()) {
|
|
40
|
-
return
|
|
62
|
+
return findProjectRoot(cwd) === null;
|
|
41
63
|
}
|
|
42
64
|
|
|
43
|
-
module.exports = { PROJECT_MARKERS, isProjectRoot, isNonProjectCwd };
|
|
65
|
+
module.exports = { PROJECT_MARKERS, isProjectRoot, findProjectRoot, isNonProjectCwd };
|
|
@@ -7,7 +7,7 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
|
|
10
|
-
const { PROJECT_MARKERS, isProjectRoot, isNonProjectCwd } = require('./project-detect');
|
|
10
|
+
const { PROJECT_MARKERS, isProjectRoot, findProjectRoot, isNonProjectCwd } = require('./project-detect');
|
|
11
11
|
|
|
12
12
|
function mkTmp(t) {
|
|
13
13
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-pd-'));
|
|
@@ -71,3 +71,25 @@ test('isProjectRoot detects each marker', (t) => {
|
|
|
71
71
|
assert.equal(isProjectRoot(dir), true, `${marker} should make cwd a project`);
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
test('isNonProjectCwd: a marker-less SUBDIR of a project resolves to the project (walk-up, monorepo fix)', (t) => {
|
|
76
|
+
// Regression (v0.79.1 audit #7): the gate checked ONLY the literal cwd, so a
|
|
77
|
+
// monorepo subdir (`.git` only at the repo root) served the 0-tool stub even
|
|
78
|
+
// though the Rust binary's resolver walks up and would answer queries. The
|
|
79
|
+
// gate now walks up too.
|
|
80
|
+
const root = mkTmp(t);
|
|
81
|
+
fs.mkdirSync(path.join(root, '.git'));
|
|
82
|
+
const sub = path.join(root, 'backend', 'src');
|
|
83
|
+
fs.mkdirSync(sub, { recursive: true });
|
|
84
|
+
assert.equal(isProjectRoot(sub), false, 'the subdir itself has no marker');
|
|
85
|
+
assert.equal(isNonProjectCwd(sub), false, 'but it is INSIDE a project → not non-project');
|
|
86
|
+
assert.equal(findProjectRoot(sub), root, 'walk-up returns the repo root');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('findProjectRoot: a marker-less tree with no ancestor marker → null (tmp/headless stays gated)', (t) => {
|
|
90
|
+
const dir = mkTmp(t);
|
|
91
|
+
const sub = path.join(dir, 'a', 'b');
|
|
92
|
+
fs.mkdirSync(sub, { recursive: true });
|
|
93
|
+
assert.equal(findProjectRoot(sub), null);
|
|
94
|
+
assert.equal(isNonProjectCwd(sub), true);
|
|
95
|
+
});
|
|
@@ -12,10 +12,17 @@ const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() =>
|
|
|
12
12
|
// True when auto-update has a newer release queued or in flight (the background
|
|
13
13
|
// downloader in session-init.js hasn't promoted the new binary yet). Used to show
|
|
14
14
|
// a transient "updating" state instead of the alarming "offline" during that window.
|
|
15
|
+
// After this many consecutive failed download attempts (auto-update.js tracks
|
|
16
|
+
// updateAttempts), a pending update is treated as STUCK: the statusline stops
|
|
17
|
+
// showing "↻ updating" (which asserts an in-progress self-heal) and surfaces the
|
|
18
|
+
// real status instead. Without this, a persistently-failing update (missing
|
|
19
|
+
// tar/curl, full disk, blocked network) pins "updating" forever.
|
|
20
|
+
const STUCK_UPDATE_ATTEMPTS = 5;
|
|
15
21
|
function updatePending() {
|
|
16
22
|
try {
|
|
17
23
|
const st = JSON.parse(fs.readFileSync(
|
|
18
24
|
path.join(os.homedir(), '.cache', 'code-graph', 'update-state.json'), 'utf8'));
|
|
25
|
+
if ((st.updateAttempts || 0) >= STUCK_UPDATE_ATTEMPTS) return false;
|
|
19
26
|
if (st.updateAvailable) return true;
|
|
20
27
|
if (st.latestVersion && st.installedVersion && st.latestVersion !== st.installedVersion) return true;
|
|
21
28
|
} catch { /* no state file or unreadable — treat as no pending update */ }
|
|
@@ -113,7 +120,11 @@ function parseReport(text) {
|
|
|
113
120
|
// still downloading. That, or any pending update, is transient: show "updating"
|
|
114
121
|
// so the user knows it self-heals, rather than the misleading "offline".
|
|
115
122
|
function statusUnavailable(errText) {
|
|
116
|
-
|
|
123
|
+
// Primary signal: the binary's STABLE schema-too-new marker (Rust
|
|
124
|
+
// domain::SCHEMA_TOO_NEW_MARKER) \u2014 not reword-able prose. Fallback to the legacy
|
|
125
|
+
// phrase so a cached binary predating the marker still reads as "updating".
|
|
126
|
+
const errStr = errText || '';
|
|
127
|
+
const binaryOutdated = errStr.includes('code-graph:schema-too-new') || /schema version/i.test(errStr);
|
|
117
128
|
return (binaryOutdated || updatePending()) ? 'code-graph: \u21bb updating' : 'code-graph: offline';
|
|
118
129
|
}
|
|
119
130
|
|
|
@@ -117,6 +117,17 @@ test('no report + update pending → updating', (t) => {
|
|
|
117
117
|
assert.equal(runStatusline(home, project), 'code-graph: ↻ updating');
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
test('stuck update (updateAttempts exhausted) → offline, not updating', (t) => {
|
|
121
|
+
// A persistently-failing update must not pin "↻ updating" forever. Past
|
|
122
|
+
// STUCK_UPDATE_ATTEMPTS the statusline drops the optimistic state and surfaces
|
|
123
|
+
// the real one (here: no report → offline).
|
|
124
|
+
const home = mkHome(t);
|
|
125
|
+
const project = mkProject(home);
|
|
126
|
+
installStubBinary(home, { report: 'boom', exitCode: 1 });
|
|
127
|
+
setUpdateState(home, { updateAvailable: true, updateAttempts: 5 });
|
|
128
|
+
assert.equal(runStatusline(home, project), 'code-graph: offline');
|
|
129
|
+
});
|
|
130
|
+
|
|
120
131
|
test('schema-version error on stderr (no report) → updating', (t) => {
|
|
121
132
|
const home = mkHome(t);
|
|
122
133
|
const project = mkProject(home);
|
|
@@ -129,6 +140,20 @@ test('schema-version error on stderr (no report) → updating', (t) => {
|
|
|
129
140
|
assert.equal(runStatusline(home, project), 'code-graph: ↻ updating');
|
|
130
141
|
});
|
|
131
142
|
|
|
143
|
+
test('schema-too-new MARKER on stderr → updating (keys on the stable token, not prose)', (t) => {
|
|
144
|
+
// The binary appends domain::SCHEMA_TOO_NEW_MARKER; the statusline must detect
|
|
145
|
+
// the post-update window via that token even when the surrounding prose is
|
|
146
|
+
// reworded/translated (the old `/schema version/i` regex would miss this).
|
|
147
|
+
const home = mkHome(t);
|
|
148
|
+
const project = mkProject(home);
|
|
149
|
+
installStubBinary(home, {
|
|
150
|
+
report: '',
|
|
151
|
+
stderr: 'Error: totally reworded wording here [code-graph:schema-too-new]',
|
|
152
|
+
exitCode: 1,
|
|
153
|
+
});
|
|
154
|
+
assert.equal(runStatusline(home, project), 'code-graph: ↻ updating');
|
|
155
|
+
});
|
|
156
|
+
|
|
132
157
|
test('genuine crash (no report, nothing pending) → offline', (t) => {
|
|
133
158
|
const home = mkHome(t);
|
|
134
159
|
const project = mkProject(home);
|
|
@@ -7,6 +7,12 @@ const { execFileSync } = require('child_process');
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
|
+
// Hook cooldown flags + the restart notice go under cgTmpDir() (a code-graph-mcp/
|
|
11
|
+
// subdir of os.tmpdir()), NOT bare os.tmpdir(): Claude Code overrides $TMPDIR to
|
|
12
|
+
// ~/.claude/tmp/, so bare-tmp flags interleave with transcript captures —
|
|
13
|
+
// diagnostic blindness + the §8 recursive-grep footgun (see tmp-dir.js). The
|
|
14
|
+
// other hook scripts already route through here; this one was the lone holdout.
|
|
15
|
+
const { cgTmpDir } = require('./tmp-dir');
|
|
10
16
|
|
|
11
17
|
// Mid-session install detection: hook fires but no manifest yet.
|
|
12
18
|
const MANIFEST_PATH = path.join(os.homedir(), '.cache', 'code-graph', 'install-manifest.json');
|
|
@@ -22,7 +28,7 @@ const COOLDOWNS = {
|
|
|
22
28
|
|
|
23
29
|
function isCoolingDown(type) {
|
|
24
30
|
try {
|
|
25
|
-
const flag = path.join(
|
|
31
|
+
const flag = path.join(cgTmpDir(), `.code-graph-ctx-${type}`);
|
|
26
32
|
const stat = fs.statSync(flag);
|
|
27
33
|
return Date.now() - stat.mtimeMs < (COOLDOWNS[type] || 60000);
|
|
28
34
|
} catch { return false; }
|
|
@@ -30,7 +36,7 @@ function isCoolingDown(type) {
|
|
|
30
36
|
|
|
31
37
|
function markCooldown(type) {
|
|
32
38
|
try {
|
|
33
|
-
fs.writeFileSync(path.join(
|
|
39
|
+
fs.writeFileSync(path.join(cgTmpDir(), `.code-graph-ctx-${type}`), '');
|
|
34
40
|
} catch { /* ok */ }
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -398,7 +404,7 @@ function runMain() {
|
|
|
398
404
|
// Mid-session install: lifecycle.js install() hasn't run yet (no manifest).
|
|
399
405
|
// MCP server only starts at session startup — tell the user to restart.
|
|
400
406
|
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
401
|
-
const noticeFile = path.join(
|
|
407
|
+
const noticeFile = path.join(cgTmpDir(), '.code-graph-mcp-restart-notice');
|
|
402
408
|
try {
|
|
403
409
|
// Show once per hour to avoid spam
|
|
404
410
|
if (Date.now() - fs.statSync(noticeFile).mtimeMs < 3600000) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.1",
|
|
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.80.1",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.80.1",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.80.1",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.80.1",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.80.1"
|
|
43
43
|
}
|
|
44
44
|
}
|