@sdsrs/code-graph 0.81.1 → 0.82.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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.81.1",
7
+ "version": "0.82.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -756,10 +756,13 @@ function update() {
756
756
  manifest.updatedAt = new Date().toISOString();
757
757
  writeManifest(manifest);
758
758
 
759
- // 7. Clean up old cached versions (keep latest 3). Claude Code only fires
760
- // hooks from the active version (per installed_plugins.json), so older
761
- // cache dirs are inert disk clutter, not correctness risks.
762
- cleanupOldCacheVersions(3);
759
+ // 7. Clean up old cached versions (keep the newest few). NOTE: older cache
760
+ // dirs are NOT always inert a running MCP server's launcher path
761
+ // (<version>/scripts/mcp-launcher.js) is resolved + cached by Claude Code
762
+ // for the whole session, so pruning the version a live process is bound to
763
+ // breaks `/mcp` reconnect with -32000 (MODULE_NOT_FOUND). cleanupOldCacheVersions
764
+ // therefore skips any version still referenced by a live process cmdline.
765
+ cleanupOldCacheVersions(5);
763
766
 
764
767
  return { oldVersion, version, settingsChanged, hooksRegistered };
765
768
  }
@@ -768,8 +771,16 @@ function update() {
768
771
  * Remove old plugin cache versions, keeping the N most recent.
769
772
  * Cache layout: ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/
770
773
  */
771
- function cleanupOldCacheVersions(keep = 3) {
772
- const cacheParent = path.join(pluginsCacheDir(), MARKETPLACE_NAME);
774
+ function cleanupOldCacheVersions(
775
+ keep = 5,
776
+ getActiveCmdlines = readActiveProcessCmdlines,
777
+ cacheParent = path.join(pluginsCacheDir(), MARKETPLACE_NAME),
778
+ ) {
779
+ // Command lines of all live processes — used by the in-use guard below to
780
+ // avoid deleting a version a running MCP server is still bound to. Failure to
781
+ // enumerate ⇒ [] ⇒ pruning falls back to recency-only (pre-guard behavior).
782
+ let cmdlines;
783
+ try { cmdlines = getActiveCmdlines() || []; } catch { cmdlines = []; }
773
784
  try {
774
785
  // List all subdirectories under the marketplace cache
775
786
  const entries = fs.readdirSync(cacheParent, { withFileTypes: true });
@@ -788,17 +799,51 @@ function cleanupOldCacheVersions(keep = 3) {
788
799
 
789
800
  if (versions.length <= keep) continue;
790
801
 
791
- const toRemove = versions.slice(keep);
792
- for (const v of toRemove) {
802
+ for (const v of versions.slice(keep)) {
803
+ // In-use guard: never delete a version dir a live process is running
804
+ // from. Claude Code caches the resolved launcher path
805
+ // (<version>/scripts/mcp-launcher.js) for the session; deleting that
806
+ // dir breaks `/mcp` reconnect with -32000. Trailing separator stops
807
+ // `0.8` from matching `0.80.x`.
808
+ if (cmdlines.some(c => c.includes(v.path + path.sep))) continue;
793
809
  try {
794
810
  fs.rmSync(v.path, { recursive: true, force: true });
795
- } catch { /* permission error or in-use — skip */ }
811
+ } catch { /* permission error — skip */ }
796
812
  }
797
813
  } catch { /* can't read plugin dir — skip */ }
798
814
  }
799
815
  } catch { /* cache dir doesn't exist — nothing to clean */ }
800
816
  }
801
817
 
818
+ /**
819
+ * Best-effort list of running process command lines, for cleanupOldCacheVersions'
820
+ * in-use guard. Linux reads /proc/<pid>/cmdline; macOS/BSD shells out to `ps`;
821
+ * any other platform or failure returns [] (pruning then falls back to
822
+ * recency-only — the same behavior as before the guard existed).
823
+ */
824
+ function readActiveProcessCmdlines() {
825
+ try {
826
+ if (process.platform === 'linux' && fs.existsSync('/proc')) {
827
+ const out = [];
828
+ for (const pid of fs.readdirSync('/proc')) {
829
+ if (!/^\d+$/.test(pid)) continue;
830
+ try {
831
+ const raw = fs.readFileSync(path.join('/proc', pid, 'cmdline'), 'utf8');
832
+ if (raw) out.push(raw.replace(/\0/g, ' '));
833
+ } catch { /* pid exited or unreadable — skip */ }
834
+ }
835
+ return out;
836
+ }
837
+ } catch { /* fall through to ps */ }
838
+ try {
839
+ const { execFileSync } = require('child_process');
840
+ return execFileSync('ps', ['-axww', '-o', 'command='], {
841
+ encoding: 'utf8', maxBuffer: 8 * 1024 * 1024,
842
+ }).split('\n').filter(Boolean);
843
+ } catch { /* unsupported platform — caller falls back to recency-only */ }
844
+ return [];
845
+ }
846
+
802
847
  // --- Health Check ---
803
848
  // Validates all registered paths in settings.json point to existing scripts.
804
849
  // Returns { healthy, issues, repaired, remaining }.
@@ -733,3 +733,54 @@ test('isStaleRelicContext: relic in plugins cache defers to a different active i
733
733
  existsSync: () => false,
734
734
  }), false);
735
735
  });
736
+
737
+ test('cleanupOldCacheVersions keeps an in-use version even beyond the keep window', (t) => {
738
+ const { cleanupOldCacheVersions } = require('./lifecycle.js');
739
+ const cacheParent = fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-cache-'));
740
+ t.after(() => fs.rmSync(cacheParent, { recursive: true, force: true }));
741
+ const pluginDir = path.join(cacheParent, 'code-graph-mcp');
742
+ // Seven versions, oldest -> newest by mtime.
743
+ const vers = ['0.78.0', '0.80.2', '0.80.3', '0.81.0', '0.81.1', '0.81.2', '0.81.3'];
744
+ vers.forEach((v, i) => {
745
+ const scripts = path.join(pluginDir, v, 'scripts');
746
+ fs.mkdirSync(scripts, { recursive: true });
747
+ fs.writeFileSync(path.join(scripts, 'mcp-launcher.js'), '// stub');
748
+ const ts = (i + 1) * 3600; // distinct, increasing mtimes
749
+ fs.utimesSync(path.join(pluginDir, v), ts, ts);
750
+ });
751
+ // A live MCP server is running from the OLDEST version (beyond keep=5) — this
752
+ // is the v0.80.2 reconnect-(-32000) scenario.
753
+ const inUse = path.join(pluginDir, '0.78.0');
754
+ const fakeCmdlines = [`node ${path.join(inUse, 'scripts', 'mcp-launcher.js')} `];
755
+
756
+ cleanupOldCacheVersions(5, () => fakeCmdlines, cacheParent);
757
+
758
+ assert.equal(fs.existsSync(inUse), true,
759
+ 'in-use version must survive prune even when it is the oldest');
760
+ assert.equal(fs.existsSync(path.join(pluginDir, '0.80.2')), false,
761
+ 'a non-in-use version beyond the keep window is still pruned');
762
+ assert.equal(fs.existsSync(path.join(pluginDir, '0.81.3')), true,
763
+ 'newest version (within keep window) is kept');
764
+ });
765
+
766
+ test('cleanupOldCacheVersions prunes beyond keep when nothing is in use', (t) => {
767
+ const { cleanupOldCacheVersions } = require('./lifecycle.js');
768
+ const cacheParent = fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-cache-'));
769
+ t.after(() => fs.rmSync(cacheParent, { recursive: true, force: true }));
770
+ const pluginDir = path.join(cacheParent, 'code-graph-mcp');
771
+ const vers = ['0.78.0', '0.80.2', '0.80.3', '0.81.0', '0.81.1', '0.81.2', '0.81.3'];
772
+ vers.forEach((v, i) => {
773
+ fs.mkdirSync(path.join(pluginDir, v), { recursive: true });
774
+ const ts = (i + 1) * 3600;
775
+ fs.utimesSync(path.join(pluginDir, v), ts, ts);
776
+ });
777
+ // No live process references any version → recency-only pruning (pre-guard).
778
+ cleanupOldCacheVersions(5, () => [], cacheParent);
779
+
780
+ assert.equal(fs.existsSync(path.join(pluginDir, '0.78.0')), false, 'oldest pruned');
781
+ assert.equal(fs.existsSync(path.join(pluginDir, '0.80.2')), false, '2nd-oldest pruned');
782
+ assert.equal(fs.existsSync(path.join(pluginDir, '0.80.3')), true, 'within keep window kept');
783
+ assert.equal(
784
+ fs.readdirSync(pluginDir).filter(n => fs.statSync(path.join(pluginDir, n)).isDirectory()).length,
785
+ 5, 'exactly keep=5 versions remain');
786
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.81.1",
3
+ "version": "0.82.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.81.1",
39
- "@sdsrs/code-graph-linux-arm64": "0.81.1",
40
- "@sdsrs/code-graph-darwin-x64": "0.81.1",
41
- "@sdsrs/code-graph-darwin-arm64": "0.81.1",
42
- "@sdsrs/code-graph-win32-x64": "0.81.1"
38
+ "@sdsrs/code-graph-linux-x64": "0.82.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.82.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.82.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.82.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.82.0"
43
43
  }
44
44
  }