@sdsrs/code-graph 0.124.0 → 0.125.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 +84 -13
- package/claude-plugin/scripts/cg-answer.js +8 -1
- package/claude-plugin/scripts/lifecycle.js +1 -0
- package/claude-plugin/scripts/statusline-composite.js +30 -15
- package/claude-plugin/templates/code-graph-snapshot.yml +1 -1
- package/package.json +6 -6
|
@@ -7,7 +7,7 @@ const http = require('http');
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
|
-
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, readJsonResult, writeJsonAtomic, installedPluginsPath, pluginsCacheDir } = require('./lifecycle');
|
|
10
|
+
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, readJsonResult, backupCorruptFile, writeJsonAtomic, installedPluginsPath, pluginsCacheDir } = require('./lifecycle');
|
|
11
11
|
const { claudeHome } = require('./claude-config');
|
|
12
12
|
const { clearCache: clearBinaryCache, globalNodeModulesCandidates, nvmNodeModulesDirs, PLATFORM_PKG, detectLibc } = require('./find-binary');
|
|
13
13
|
const { readBinaryVersion, compareVersions, isDevMode } = require('./version-utils');
|
|
@@ -785,25 +785,96 @@ async function downloadAndInstall(latest, {
|
|
|
785
785
|
// fragile), pluginDst was never created. Advancing installPath/manifest to it anyway
|
|
786
786
|
// pointed Claude Code at a nonexistent install dir while state read "up to date".
|
|
787
787
|
if (pluginUpdated) {
|
|
788
|
-
// Update installed_plugins.json to point to new version
|
|
788
|
+
// Update installed_plugins.json to point to new version.
|
|
789
|
+
//
|
|
790
|
+
// Through the same three-way read the lifecycle.js site uses. The lenient
|
|
791
|
+
// `readJson` returns null for ENOENT, EACCES and unparseable alike, and
|
|
792
|
+
// the `if (installed && …)` guard below then skipped the repoint in
|
|
793
|
+
// SILENCE — while the plugin copy had landed and the manifest below is
|
|
794
|
+
// about to be advanced. Claude Code keeps launching the old install dir
|
|
795
|
+
// with state reading "up to date": the split-brain shape the binary-pin
|
|
796
|
+
// incident was made of, and one this file cannot fix by guessing at bytes
|
|
797
|
+
// it could not read. So it says so instead, which keeps `/plugin update`
|
|
798
|
+
// reachable as the manual way out.
|
|
789
799
|
const installedPath = installedPluginsPath();
|
|
790
|
-
|
|
791
|
-
|
|
800
|
+
const installedRead = readJsonResult(installedPath);
|
|
801
|
+
// Whether the registry entry is STILL pointing at the old version when this
|
|
802
|
+
// block ends. It gates the manifest advance below, and that gate is the
|
|
803
|
+
// whole difference between a report and a fix: `checkForUpdate` reads
|
|
804
|
+
// `readManifest().version` as the authoritative installed version, so
|
|
805
|
+
// advancing it past a repoint that did not happen makes the next session
|
|
806
|
+
// compute "up to date" — the message below prints ONCE, into a SessionStart
|
|
807
|
+
// hook's stderr, and the split-brain then has nothing behind it. Left
|
|
808
|
+
// behind, the ordinary check interval retries the whole install and
|
|
809
|
+
// re-reports, and the repoint lands by itself the moment the file is
|
|
810
|
+
// repaired. `missing` is not blocked: no registry means nothing to repoint.
|
|
811
|
+
let repointBlocked = false;
|
|
812
|
+
if (installedRead.corrupt) {
|
|
813
|
+
// Value unusable — the bytes are not ours to guess at.
|
|
814
|
+
const why = installedRead.error
|
|
815
|
+
? (installedRead.error.code || installedRead.error.message)
|
|
816
|
+
: 'it does not contain a JSON object';
|
|
817
|
+
console.error(
|
|
818
|
+
`[code-graph] plugin ${latest.version} is installed, but ${installedPath} ` +
|
|
819
|
+
`could not be read (${why}) — its entry for this plugin still points at the ` +
|
|
820
|
+
'previous version. Run `/plugin update` or repair that file by hand.'
|
|
821
|
+
);
|
|
822
|
+
repointBlocked = true;
|
|
823
|
+
} else {
|
|
824
|
+
let installed = installedRead.value;
|
|
825
|
+
// `lossy` is NOT `corrupt`: the value parsed and is usable, it is the
|
|
826
|
+
// BYTES that will not survive our rewrite (a cp1252 byte inside a path,
|
|
827
|
+
// see readJsonResult). lifecycle.js's readSettingsForWrite route applies
|
|
828
|
+
// here for the same reason — preserve the true bytes, then proceed, since
|
|
829
|
+
// refusing outright strands the install over a byte we can work around.
|
|
830
|
+
// Collapsing this into the corrupt arm also misreported it: a lossy result
|
|
831
|
+
// carries no `error`, so the message called a parseable file unparseable.
|
|
832
|
+
if (installed && installedRead.lossy) {
|
|
833
|
+
const backup = backupCorruptFile(installedPath, installedRead.raw);
|
|
834
|
+
if (backup) {
|
|
835
|
+
console.error(
|
|
836
|
+
`[code-graph] ${installedPath} contains bytes that are not valid UTF-8; ` +
|
|
837
|
+
`repointing it at ${latest.version} will replace them. Saved the original ` +
|
|
838
|
+
`to ${backup} first.`
|
|
839
|
+
);
|
|
840
|
+
} else {
|
|
841
|
+
console.error(
|
|
842
|
+
`[code-graph] plugin ${latest.version} is installed, but ${installedPath} ` +
|
|
843
|
+
'contains bytes that are not valid UTF-8 and no backup copy could be made — ' +
|
|
844
|
+
'its entry for this plugin still points at the previous version. Rewriting it ' +
|
|
845
|
+
'would replace those bytes permanently. Run `/plugin update` after repairing it.'
|
|
846
|
+
);
|
|
847
|
+
installed = null;
|
|
848
|
+
repointBlocked = true;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
792
851
|
if (installed && installed.plugins && installed.plugins[PLUGIN_ID]) {
|
|
793
852
|
installed.plugins[PLUGIN_ID][0].installPath = pluginDst;
|
|
794
853
|
installed.plugins[PLUGIN_ID][0].version = latest.version;
|
|
795
854
|
installed.plugins[PLUGIN_ID][0].lastUpdated = new Date().toISOString();
|
|
796
|
-
|
|
855
|
+
try {
|
|
856
|
+
writeJsonAtomic(installedPath, installed);
|
|
857
|
+
} catch (err) {
|
|
858
|
+
console.error(
|
|
859
|
+
`[code-graph] plugin ${latest.version} is installed, but ${installedPath} ` +
|
|
860
|
+
`could not be written (${err.code || err.name}) — its entry for this plugin ` +
|
|
861
|
+
'still points at the previous version. Run `/plugin update`.'
|
|
862
|
+
);
|
|
863
|
+
repointBlocked = true;
|
|
864
|
+
}
|
|
797
865
|
}
|
|
798
|
-
}
|
|
866
|
+
}
|
|
799
867
|
|
|
800
|
-
// Update install manifest
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
868
|
+
// Update install manifest — only when nothing is left pointing at the old
|
|
869
|
+
// version. See `repointBlocked` above: this value IS the update gate.
|
|
870
|
+
if (!repointBlocked) {
|
|
871
|
+
try {
|
|
872
|
+
const manifest = readManifest();
|
|
873
|
+
manifest.version = latest.version;
|
|
874
|
+
manifest.updatedAt = new Date().toISOString();
|
|
875
|
+
writeJsonAtomic(path.join(CACHE_DIR, 'install-manifest.json'), manifest);
|
|
876
|
+
} catch { /* not fatal */ }
|
|
877
|
+
}
|
|
807
878
|
|
|
808
879
|
// Run the NEW lifecycle.js to update settings.json hooks with new paths.
|
|
809
880
|
// Without this, settings.json hooks still point to the old version directory
|
|
@@ -61,7 +61,14 @@ function truncateAtLine(text, maxBytes) {
|
|
|
61
61
|
if (lastNl > 0) {
|
|
62
62
|
return { text: head.slice(0, lastNl), truncated: true };
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
// Hard cut, when even the first line does not fit. Back the cut off to a
|
|
65
|
+
// UTF-8 character boundary instead of re-decoding the bytes: `latin1` maps
|
|
66
|
+
// each byte to its own character, so a CJK line came back as mojibake rather
|
|
67
|
+
// than as a shortened line, and `utf8` alone would leave a U+FFFD where the
|
|
68
|
+
// cut landed mid-character. A continuation byte is `10xxxxxx`.
|
|
69
|
+
let end = maxBytes;
|
|
70
|
+
while (end > 0 && (buf[end] & 0xc0) === 0x80) end--;
|
|
71
|
+
return { text: buf.subarray(0, end).toString('utf8'), truncated: true };
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/**
|
|
@@ -1817,6 +1817,7 @@ module.exports = {
|
|
|
1817
1817
|
isPluginExplicitlyDisabled, isPluginInactive, isPluginUninstalled, removeCacheResidue,
|
|
1818
1818
|
cleanupDisabledStatusline, unadoptRegisteredProjects,
|
|
1819
1819
|
readManifest, readJson, readJsonResult, readSettingsForWrite, writeJsonAtomic,
|
|
1820
|
+
backupCorruptFile, // auto-update.js repoints installed_plugins.json and owes the same preserve-then-proceed route
|
|
1820
1821
|
migrateOldPluginIds, // exported so its failure arms are testable (audit 2026-08-22 P2-10)
|
|
1821
1822
|
readRegistry, readRegistryForWrite, writeRegistry,
|
|
1822
1823
|
getPluginVersion, cleanupOldCacheVersions,
|
|
@@ -42,7 +42,7 @@ function run(stdin) {
|
|
|
42
42
|
const registry = readRegistry();
|
|
43
43
|
if (registry.length === 0) {
|
|
44
44
|
// Fallback: no registry, run code-graph only
|
|
45
|
-
const cg = runProvider(codeGraphCommand(), false, stdin);
|
|
45
|
+
const cg = runProvider(codeGraphCommand(), false, stdin, 'code-graph');
|
|
46
46
|
if (cg) process.stdout.write(cg);
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
@@ -57,7 +57,7 @@ function run(stdin) {
|
|
|
57
57
|
|
|
58
58
|
const outputs = [];
|
|
59
59
|
for (const provider of sorted) {
|
|
60
|
-
const out = runProvider(provider.command, provider.needsStdin, stdin);
|
|
60
|
+
const out = runProvider(provider.command, provider.needsStdin, stdin, provider.id);
|
|
61
61
|
if (out) outputs.push(out);
|
|
62
62
|
}
|
|
63
63
|
if (outputs.length > 0) {
|
|
@@ -65,26 +65,41 @@ function run(stdin) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/// True when the command needs a shell to mean what it says.
|
|
69
|
-
///
|
|
70
|
-
///
|
|
71
|
-
///
|
|
68
|
+
/// True when the command needs a shell to mean what it says.
|
|
69
|
+
///
|
|
70
|
+
/// Gated on the entry being `_previous`, and that gate is the whole point.
|
|
71
|
+
/// `_previous` IS the user's `statusLine.command`, which Claude Code runs
|
|
72
|
+
/// through a shell — so a captured pipeline is legitimate there, and a pipeline
|
|
73
|
+
/// cannot run through `execFileSync` under any splitting: it produces ENOENT and
|
|
74
|
+
/// a silently missing segment.
|
|
75
|
+
///
|
|
76
|
+
/// The other two registry classes were never shell strings. `codeGraphCommand()`
|
|
77
|
+
/// composes `node "<__dirname>/statusline.js"`, and third-party entries arrive
|
|
78
|
+
/// through `statusline-chain.js register`, whose only executor has ever been
|
|
79
|
+
/// `execFileSync`. Handing those to a shell imposes semantics they never had,
|
|
80
|
+
/// and OUR segment is the one that dies: measured, a plugin installed under a
|
|
81
|
+
/// directory named `dev$work` produced `node "…/dev$work/statusline.js"`, which
|
|
82
|
+
/// a shell reads as `…/dev/statusline.js` — segment gone, `catch` swallows it.
|
|
83
|
+
/// Inside double quotes only `$` and a backtick break, which is why this stayed
|
|
84
|
+
/// invisible until someone had one in an install path.
|
|
72
85
|
///
|
|
73
86
|
/// Trade-off, stated because it is a real one: through `sh -c`, the timeout's
|
|
74
87
|
/// SIGKILL reaches the SHELL, not necessarily a grandchild that traps signals
|
|
75
|
-
/// (the hazard the direct-exec path was hardened against).
|
|
76
|
-
///
|
|
77
|
-
///
|
|
78
|
-
///
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
/// (the hazard the direct-exec path was hardened against). Confining the shell
|
|
89
|
+
/// to `_previous` also confines that loss to the entry that cannot work without
|
|
90
|
+
/// it. Windows keeps everything on the direct path — note that Claude Code
|
|
91
|
+
/// itself runs statusline commands through Git Bash there, so a `_previous`
|
|
92
|
+
/// pipeline works in Claude Code and still dies here; the fix is half-applied by
|
|
93
|
+
/// platform, which is a gap rather than a regression (it never worked here).
|
|
94
|
+
function needsShell(command, id) {
|
|
95
|
+
return id === '_previous' && process.platform !== 'win32' && SHELL_METACHARS.test(command);
|
|
81
96
|
}
|
|
82
97
|
|
|
83
|
-
function runProvider(command, needsStdin, stdin) {
|
|
98
|
+
function runProvider(command, needsStdin, stdin, id) {
|
|
84
99
|
if (!command) return null;
|
|
85
100
|
try {
|
|
86
101
|
// Parse command into executable + args
|
|
87
|
-
const parts = needsShell(command) ? ['/bin/sh', '-c', command] : parseCommand(command);
|
|
102
|
+
const parts = needsShell(command, id) ? ['/bin/sh', '-c', command] : parseCommand(command);
|
|
88
103
|
if (!parts) return null;
|
|
89
104
|
|
|
90
105
|
// Claude Code runs statusLine.command through a shell, so a leading `~`
|
|
@@ -94,7 +109,7 @@ function runProvider(command, needsStdin, stdin) {
|
|
|
94
109
|
// swallowed below, silently dropping the user's original statusline.
|
|
95
110
|
// `sh -c` does its own tilde expansion; expanding our own would corrupt the
|
|
96
111
|
// script text (`~` inside a quoted string is not a home directory).
|
|
97
|
-
const argv = needsShell(command) ? parts : parts.map(expandTilde);
|
|
112
|
+
const argv = needsShell(command, id) ? parts : parts.map(expandTilde);
|
|
98
113
|
|
|
99
114
|
// Forward Claude Code's authoritative current dir (from the stdin payload) as
|
|
100
115
|
// a plugin-scoped env var. The code-graph provider gates on it instead of its
|
|
@@ -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.125.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.125.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.125.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.125.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.125.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.125.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.125.0"
|
|
43
43
|
}
|
|
44
44
|
}
|