@sdsrs/code-graph 0.164.0 → 0.165.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/adopt.js +15 -2
- package/claude-plugin/scripts/recommendation-log.js +11 -9
- package/claude-plugin/scripts/session-init.js +43 -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,44 @@ 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
|
+
*/
|
|
577
|
+
function staleNoticeDue(cwd, fingerprint) {
|
|
578
|
+
if (!fingerprint) return true;
|
|
579
|
+
const dir = path.join(cwd, '.code-graph');
|
|
580
|
+
const marker = path.join(dir, 'stale-block-notice');
|
|
581
|
+
// `.code-graph/` is repo content: a clone can put a symlink or a hard link
|
|
582
|
+
// where the record goes (tar a FIFO), and a plain read or write follows it
|
|
583
|
+
// out of the project. Only a single-link regular file in a real directory is
|
|
584
|
+
// read or written — recommendation-log.js's guards; anything else is
|
|
585
|
+
// unrecordable.
|
|
586
|
+
const what = 'the stale-block notice record';
|
|
587
|
+
let dirStat;
|
|
588
|
+
try { dirStat = fs.lstatSync(dir); } catch { return true; }
|
|
589
|
+
if (!isOwnedPath(dir, 'dir', dirStat, what) || !isOwnedPath(marker, 'file', undefined, what)) return true;
|
|
590
|
+
let fd;
|
|
591
|
+
try {
|
|
592
|
+
fd = openOwned(marker,
|
|
593
|
+
fs.constants.O_RDWR | fs.constants.O_CREAT | (fs.constants.O_NONBLOCK || 0), what);
|
|
594
|
+
} catch { return true; }
|
|
595
|
+
if (fd === null) return true;
|
|
596
|
+
try {
|
|
597
|
+
const buf = Buffer.alloc(64);
|
|
598
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
599
|
+
if (buf.toString('utf8', 0, n).trim() === fingerprint) return false;
|
|
600
|
+
fs.ftruncateSync(fd, 0);
|
|
601
|
+
fs.writeSync(fd, fingerprint + '\n', 0);
|
|
602
|
+
} catch { /* unrecorded: shown again next session */ }
|
|
603
|
+
finally { fs.closeSync(fd); }
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
568
607
|
function runSessionInit({ source } = {}) {
|
|
569
608
|
// Fresh per run: this is module state, and the test suite calls this function
|
|
570
609
|
// many times in one process. A carried-over array would report last run's
|
|
@@ -684,12 +723,13 @@ function runSessionInit({ source } = {}) {
|
|
|
684
723
|
autoAdopt = { attempted: false, reason: 'threw', error: (e && e.message) || String(e) };
|
|
685
724
|
}
|
|
686
725
|
}
|
|
687
|
-
if (autoAdopt.reason === 'stale') {
|
|
726
|
+
if (autoAdopt.reason === 'stale' && staleNoticeDue(process.cwd(), autoAdopt.fingerprint)) {
|
|
688
727
|
notices.push(
|
|
689
728
|
"[code-graph] This project's CLAUDE.md carries an out-of-date code-graph block.\n" +
|
|
690
729
|
` Refresh it: ${adoptCommand()}\n` +
|
|
691
730
|
` Remove it: ${unadoptCommand()}\n` +
|
|
692
|
-
' (SessionStart no longer edits CLAUDE.md itself
|
|
731
|
+
' (Shown once per shipped template. SessionStart no longer edits CLAUDE.md itself;\n' +
|
|
732
|
+
' CODE_GRAPH_NO_TEMPLATE_REFRESH=1 silences this.)'
|
|
693
733
|
);
|
|
694
734
|
}
|
|
695
735
|
|
|
@@ -855,7 +895,7 @@ module.exports = {
|
|
|
855
895
|
injectProjectMap,
|
|
856
896
|
verifyBinary, missingBinaryMessage, unadoptCommand,
|
|
857
897
|
consistencyCheck,
|
|
858
|
-
runSessionInit,
|
|
898
|
+
runSessionInit, staleNoticeDue,
|
|
859
899
|
computeQuietHooks,
|
|
860
900
|
shouldInjectMap,
|
|
861
901
|
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.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.165.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.165.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.165.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.165.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.165.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.165.0"
|
|
43
43
|
}
|
|
44
44
|
}
|