@sdsrs/code-graph 0.164.0 → 0.165.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/adopt.js +15 -2
- package/claude-plugin/scripts/recommendation-log.js +11 -9
- package/claude-plugin/scripts/session-init.js +55 -3
- package/claude-plugin/templates/code-graph-snapshot.yml +1 -1
- package/package.json +6 -6
|
@@ -667,6 +667,19 @@ function needsRefresh({ cwd, templatePath } = {}) {
|
|
|
667
667
|
} catch { return false; }
|
|
668
668
|
}
|
|
669
669
|
|
|
670
|
+
// What a stale notice is about: the shipped template plus the block this
|
|
671
|
+
// project would get now. SessionStart shows the notice once per value, so the
|
|
672
|
+
// user's own copy is deliberately not an input — a hand edit is not news, a
|
|
673
|
+
// newer shipped template is. null when the template cannot be read.
|
|
674
|
+
function shippedFingerprint({ cwd, templatePath } = {}) {
|
|
675
|
+
const effectiveCwd = cwd || process.cwd();
|
|
676
|
+
let tpl;
|
|
677
|
+
try { tpl = fs.readFileSync(templatePath || TEMPLATE_PATH); } catch { return null; }
|
|
678
|
+
return require('crypto').createHash('sha256')
|
|
679
|
+
.update(tpl).update('\0').update(buildBlock(detectProjectType(effectiveCwd)))
|
|
680
|
+
.digest('hex').slice(0, 16);
|
|
681
|
+
}
|
|
682
|
+
|
|
670
683
|
// 检测脚本是否从 Claude Code 插件 cache 运行。
|
|
671
684
|
// 走 __dirname 而非 CLAUDE_PLUGIN_ROOT — 后者在多插件共存时会互相污染
|
|
672
685
|
// (见 feedback_plugin_env_isolation.md)。
|
|
@@ -752,7 +765,7 @@ function maybeAutoAdopt({ cwd, home, env, scriptPath } = {}) {
|
|
|
752
765
|
// name is kept: SessionStart, tests and the uninstall paths all call it.
|
|
753
766
|
if (isAdopted({ cwd })) {
|
|
754
767
|
if (env.CODE_GRAPH_NO_TEMPLATE_REFRESH !== '1' && needsRefresh({ cwd })) {
|
|
755
|
-
return { attempted: false, reason: 'stale', migrated };
|
|
768
|
+
return { attempted: false, reason: 'stale', fingerprint: shippedFingerprint({ cwd }), migrated };
|
|
756
769
|
}
|
|
757
770
|
return { attempted: false, reason: 'already-adopted', migrated };
|
|
758
771
|
}
|
|
@@ -990,7 +1003,7 @@ if (require.main === module) {
|
|
|
990
1003
|
module.exports = {
|
|
991
1004
|
adopt, unadopt, memoryDir, formatResult, unadoptCommand, shellQuote, stripSentinelBlock,
|
|
992
1005
|
readAdoptedProjects, readAdoptedResult, recordAdopted, removeAdopted, adoptedRegistryFile,
|
|
993
|
-
isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot, adoptCommand,
|
|
1006
|
+
isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, shippedFingerprint, isProjectRoot, adoptCommand,
|
|
994
1007
|
detectProjectType, buildBlock, buildTriggerRows, migrateLegacyMemoryDir,
|
|
995
1008
|
claudeMdPath, detailDir, detailPath,
|
|
996
1009
|
extractCargoRuntimeDeps, extractPyRuntimeDeps, extractGoDirectRequires,
|
|
@@ -39,11 +39,11 @@ const O_NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
|
|
|
39
39
|
// this is ever required from something long-lived.
|
|
40
40
|
const warnedPaths = new Set();
|
|
41
41
|
|
|
42
|
-
function warnNotOwned(p, why) {
|
|
42
|
+
function warnNotOwned(p, why, what = 'adoption metrics') {
|
|
43
43
|
if (warnedPaths.has(p)) return;
|
|
44
44
|
warnedPaths.add(p);
|
|
45
45
|
try {
|
|
46
|
-
process.stderr.write(`[code-graph] skipping
|
|
46
|
+
process.stderr.write(`[code-graph] skipping ${what}: ${p} ${why}\n`);
|
|
47
47
|
} catch {
|
|
48
48
|
/* stderr closed → nothing to do */
|
|
49
49
|
}
|
|
@@ -67,9 +67,10 @@ function warnNotOwned(p, why) {
|
|
|
67
67
|
* @param {string} p absolute path
|
|
68
68
|
* @param {'file'|'dir'} kind
|
|
69
69
|
* @param {fs.Stats} [known] an lstat the caller already took, to avoid a second
|
|
70
|
+
* @param {string} [what] what is skipped, for the diagnostic
|
|
70
71
|
* @returns {boolean} true if `p` is safe to write
|
|
71
72
|
*/
|
|
72
|
-
function isOwnedPath(p, kind, known) {
|
|
73
|
+
function isOwnedPath(p, kind, known, what) {
|
|
73
74
|
let st = known;
|
|
74
75
|
if (!st) {
|
|
75
76
|
try {
|
|
@@ -79,11 +80,11 @@ function isOwnedPath(p, kind, known) {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
if (st.isSymbolicLink()) {
|
|
82
|
-
warnNotOwned(p, 'is a symlink (following it would write outside the project)');
|
|
83
|
+
warnNotOwned(p, 'is a symlink (following it would write outside the project)', what);
|
|
83
84
|
return false;
|
|
84
85
|
}
|
|
85
86
|
if (kind === 'dir' ? st.isDirectory() : st.isFile()) return true;
|
|
86
|
-
warnNotOwned(p, 'is not a regular file');
|
|
87
|
+
warnNotOwned(p, 'is not a regular file', what);
|
|
87
88
|
return false;
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -102,9 +103,10 @@ function isOwnedPath(p, kind, known) {
|
|
|
102
103
|
*
|
|
103
104
|
* Callers must NOT pass `O_TRUNC`: truncation would happen on the open, before
|
|
104
105
|
* any of this could look. Truncate through the returned descriptor instead.
|
|
106
|
+
* @param {string} [what] what is skipped, for the diagnostic
|
|
105
107
|
* @returns {number|null} an open fd the caller must close, or null if refused
|
|
106
108
|
*/
|
|
107
|
-
function openOwned(file, flags) {
|
|
109
|
+
function openOwned(file, flags, what) {
|
|
108
110
|
const fd = fs.openSync(file, flags | O_NOFOLLOW);
|
|
109
111
|
let st;
|
|
110
112
|
try {
|
|
@@ -114,12 +116,12 @@ function openOwned(file, flags) {
|
|
|
114
116
|
throw e;
|
|
115
117
|
}
|
|
116
118
|
if (!st.isFile()) {
|
|
117
|
-
warnNotOwned(file, 'is not a regular file');
|
|
119
|
+
warnNotOwned(file, 'is not a regular file', what);
|
|
118
120
|
fs.closeSync(fd);
|
|
119
121
|
return null;
|
|
120
122
|
}
|
|
121
123
|
if (st.nlink > 1) {
|
|
122
|
-
warnNotOwned(file, 'has more than one hard link (the write would reach another path)');
|
|
124
|
+
warnNotOwned(file, 'has more than one hard link (the write would reach another path)', what);
|
|
123
125
|
fs.closeSync(fd);
|
|
124
126
|
return null;
|
|
125
127
|
}
|
|
@@ -202,4 +204,4 @@ function recordRecommendation(cwd, event = {}) {
|
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
|
|
205
|
-
module.exports = { recordRecommendation, REC_FILE };
|
|
207
|
+
module.exports = { recordRecommendation, REC_FILE, isOwnedPath, openOwned };
|
|
@@ -19,6 +19,7 @@ const { installHookFailOpen, remainingMs } = require('./hook-fail-open');
|
|
|
19
19
|
// treats any throw as "nothing to conclude", so a lazy require in there would
|
|
20
20
|
// turn a resolution failure into a silent disable (pre-tag review, JS-08).
|
|
21
21
|
const { resolveProjectRoot } = require('./project-root');
|
|
22
|
+
const { isOwnedPath, openOwned } = require('./recommendation-log');
|
|
22
23
|
|
|
23
24
|
// ── SessionStart budget (audit 2026-09-05 NEW-05) ─────────────────────────
|
|
24
25
|
//
|
|
@@ -565,6 +566,50 @@ function samePath(a, b) {
|
|
|
565
566
|
return real(a) === real(b);
|
|
566
567
|
}
|
|
567
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Whether to show the out-of-date-block notice in this project, recording it
|
|
571
|
+
* as shown. Once per shipped-template fingerprint: 0.164.0 showed it at every
|
|
572
|
+
* session start. Recorded in `.code-graph/` only when that directory already
|
|
573
|
+
* exists — creating it would put an unexcluded directory in `git status`, the
|
|
574
|
+
* side effect D3/D4 removed. Anything unrecordable (no fingerprint, no
|
|
575
|
+
* directory, a failed write) shows the notice: a failure must never silence it.
|
|
576
|
+
* `record: false` reads the record and never creates or writes it, for a
|
|
577
|
+
* session nobody attends.
|
|
578
|
+
*/
|
|
579
|
+
function staleNoticeDue(cwd, fingerprint, { record = true } = {}) {
|
|
580
|
+
if (!fingerprint) return true;
|
|
581
|
+
const dir = path.join(cwd, '.code-graph');
|
|
582
|
+
const marker = path.join(dir, 'stale-block-notice');
|
|
583
|
+
// `.code-graph/` is repo content: a clone can put a symlink or a hard link
|
|
584
|
+
// where the record goes (tar a FIFO), and a plain read or write follows it
|
|
585
|
+
// out of the project. Only a single-link regular file in a real directory is
|
|
586
|
+
// read or written — recommendation-log.js's guards; anything else is
|
|
587
|
+
// unrecordable.
|
|
588
|
+
const what = 'the stale-block notice record';
|
|
589
|
+
let dirStat;
|
|
590
|
+
try { dirStat = fs.lstatSync(dir); } catch { return true; }
|
|
591
|
+
if (!isOwnedPath(dir, 'dir', dirStat, what) || !isOwnedPath(marker, 'file', undefined, what)) return true;
|
|
592
|
+
let fd;
|
|
593
|
+
try {
|
|
594
|
+
fd = openOwned(marker,
|
|
595
|
+
(record ? fs.constants.O_RDWR | fs.constants.O_CREAT : fs.constants.O_RDONLY)
|
|
596
|
+
| (fs.constants.O_NONBLOCK || 0), what);
|
|
597
|
+
} catch { return true; }
|
|
598
|
+
if (fd === null) return true;
|
|
599
|
+
try {
|
|
600
|
+
// A read that fills the buffer (64 bytes or more) is not a record this
|
|
601
|
+
// plugin wrote, whatever its first bytes.
|
|
602
|
+
const buf = Buffer.alloc(64);
|
|
603
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
604
|
+
if (n < buf.length && buf.toString('utf8', 0, n).trim() === fingerprint) return false;
|
|
605
|
+
if (!record) return true;
|
|
606
|
+
fs.ftruncateSync(fd, 0);
|
|
607
|
+
fs.writeSync(fd, fingerprint + '\n', 0);
|
|
608
|
+
} catch { /* unrecorded: shown again next session */ }
|
|
609
|
+
finally { fs.closeSync(fd); }
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
|
|
568
613
|
function runSessionInit({ source } = {}) {
|
|
569
614
|
// Fresh per run: this is module state, and the test suite calls this function
|
|
570
615
|
// many times in one process. A carried-over array would report last run's
|
|
@@ -684,12 +729,19 @@ function runSessionInit({ source } = {}) {
|
|
|
684
729
|
autoAdopt = { attempted: false, reason: 'threw', error: (e && e.message) || String(e) };
|
|
685
730
|
}
|
|
686
731
|
}
|
|
687
|
-
|
|
732
|
+
// `claude -p` and SDK sessions run this hook too, and nobody reads their
|
|
733
|
+
// notice. Claude Code sets CLAUDE_CODE_SESSION_ATTENDED to '0' for them ('1'
|
|
734
|
+
// in the terminal UI, the IDE and desktop apps), so they read the record but
|
|
735
|
+
// never write it. Unset (an older Claude Code): recorded, as before.
|
|
736
|
+
const attended = process.env.CLAUDE_CODE_SESSION_ATTENDED !== '0';
|
|
737
|
+
if (autoAdopt.reason === 'stale'
|
|
738
|
+
&& staleNoticeDue(process.cwd(), autoAdopt.fingerprint, { record: attended })) {
|
|
688
739
|
notices.push(
|
|
689
740
|
"[code-graph] This project's CLAUDE.md carries an out-of-date code-graph block.\n" +
|
|
690
741
|
` Refresh it: ${adoptCommand()}\n` +
|
|
691
742
|
` Remove it: ${unadoptCommand()}\n` +
|
|
692
|
-
' (SessionStart no longer edits CLAUDE.md itself
|
|
743
|
+
' (Shown once per shipped template. SessionStart no longer edits CLAUDE.md itself;\n' +
|
|
744
|
+
' CODE_GRAPH_NO_TEMPLATE_REFRESH=1 silences this.)'
|
|
693
745
|
);
|
|
694
746
|
}
|
|
695
747
|
|
|
@@ -855,7 +907,7 @@ module.exports = {
|
|
|
855
907
|
injectProjectMap,
|
|
856
908
|
verifyBinary, missingBinaryMessage, unadoptCommand,
|
|
857
909
|
consistencyCheck,
|
|
858
|
-
runSessionInit,
|
|
910
|
+
runSessionInit, staleNoticeDue,
|
|
859
911
|
computeQuietHooks,
|
|
860
912
|
shouldInjectMap,
|
|
861
913
|
hookFireWarning, checkHookFiring, analyzeHookDark, detectHookDark, // v0.67.0
|
|
@@ -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.165.1 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.165.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.165.1",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.165.1",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.165.1",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.165.1",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.165.1"
|
|
43
43
|
}
|
|
44
44
|
}
|