synabun 2026.4.20-beta.5 → 2026.4.20-beta.7

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.
@@ -9818,6 +9818,66 @@ function buildUpdateCommand(toolKey, installSource) {
9818
9818
  return `npm install -g ${npmPkg}@latest`;
9819
9819
  }
9820
9820
 
9821
+ // Resolve SynaBun's own install roots so we can reject any tool binary that
9822
+ // lives inside our bundled node_modules — bulletproof guard for the recurring
9823
+ // phantom-version bug (e.g. stale @anthropic-ai/claude-code left over from
9824
+ // prior SynaBun installs that shipped it as a dependency, which `npm install`
9825
+ // does not auto-prune when removed from package.json).
9826
+ const SYNABUN_ROOTS = (() => {
9827
+ const roots = new Set();
9828
+ try { roots.add(realpathSync(__dirname).replace(/\\/g, '/').toLowerCase()); } catch {}
9829
+ try { roots.add(realpathSync(resolve(__dirname, '..')).replace(/\\/g, '/').toLowerCase()); } catch {}
9830
+ return [...roots];
9831
+ })();
9832
+
9833
+ function isInsideSynabun(binPath) {
9834
+ if (!binPath) return false;
9835
+ try {
9836
+ const resolved = realpathSync(binPath).replace(/\\/g, '/').toLowerCase();
9837
+ return SYNABUN_ROOTS.some(root => resolved.startsWith(root + '/'));
9838
+ } catch {
9839
+ const norm = binPath.replace(/\\/g, '/').toLowerCase();
9840
+ return SYNABUN_ROOTS.some(root => norm.startsWith(root + '/'));
9841
+ }
9842
+ }
9843
+
9844
+ // Versions of @anthropic-ai/claude-code that SynaBun has historically shipped
9845
+ // as a bundled dependency. If a `claude --version` probe returns one of these,
9846
+ // it's almost certainly the stale bundle (a real user-global install would be
9847
+ // the current 2.1.116+). Reject hard regardless of where the binary path looks
9848
+ // to come from. Add new versions here if SynaBun ever ships claude-code again.
9849
+ const KNOWN_STALE_CLAUDE_VERSIONS = new Set(['2.1.71', '2.1.89', '2.1.91']);
9850
+
9851
+ // Aggressively delete any stale bundled CLI tool packages left over in our
9852
+ // own node_modules. Idempotent — runs on every startup. Belt-and-suspenders
9853
+ // against `npm install` not auto-pruning packages removed from package.json.
9854
+ // Logs every found package (deleted or not) so Windows users can confirm the
9855
+ // prune is actually firing on their box.
9856
+ function pruneBundledCliTools() {
9857
+ const stalePkgs = ['@anthropic-ai/claude-code', '@openai/codex', 'opencode-ai', '@google/gemini-cli'];
9858
+ const moduleRoots = [
9859
+ join(__dirname, 'node_modules'),
9860
+ join(__dirname, '..', 'node_modules'),
9861
+ ];
9862
+ for (const root of moduleRoots) {
9863
+ for (const pkg of stalePkgs) {
9864
+ const target = join(root, pkg);
9865
+ try {
9866
+ if (existsSync(target)) {
9867
+ rmSync(target, { recursive: true, force: true });
9868
+ console.log(` Pruned stale bundled CLI: ${target}`);
9869
+ }
9870
+ } catch { /* permission or in-use — ignore */ }
9871
+ // Also remove .bin shims that point at the deleted package
9872
+ const binBase = pkg.split('/').pop().replace(/-cli$|-ai$/, '');
9873
+ for (const ext of ['', '.cmd', '.ps1', '.exe']) {
9874
+ const shim = join(root, '.bin', binBase + ext);
9875
+ try { if (existsSync(shim)) rmSync(shim, { force: true }); } catch {}
9876
+ }
9877
+ }
9878
+ }
9879
+ }
9880
+
9821
9881
  async function checkToolVersions() {
9822
9882
  // Strip node_modules/.bin entries so the version check reflects the user's
9823
9883
  // global install rather than any copy bundled with SynaBun itself.
@@ -9831,11 +9891,34 @@ async function checkToolVersions() {
9831
9891
  const results = {};
9832
9892
 
9833
9893
  for (const tool of TOOL_DEFS) {
9894
+ // Resolve binary path FIRST. If it lives inside SynaBun's own install
9895
+ // dir (stale bundled copy), treat the tool as not installed — do NOT
9896
+ // run --version against it, since that would report the bundled
9897
+ // version and trigger a false-positive update badge.
9898
+ const probedBinPath = resolveBinPath(tool.cmd, cleanEnv);
9899
+ const isStaleBundle = isInsideSynabun(probedBinPath);
9900
+
9834
9901
  let installed = null;
9835
- try {
9836
- const raw = execSync(`${tool.cmd} ${tool.versionArg}`, execOpts).trim();
9837
- installed = parseVersion(raw);
9838
- } catch { /* not installed or errored */ }
9902
+ if (!isStaleBundle) {
9903
+ try {
9904
+ const raw = execSync(`${tool.cmd} ${tool.versionArg}`, execOpts).trim();
9905
+ installed = parseVersion(raw);
9906
+ } catch { /* not installed or errored */ }
9907
+ }
9908
+
9909
+ // Nuclear safety net for the recurring claude phantom-version bug:
9910
+ // if the probe returned a known historical bundled version of claude-code,
9911
+ // reject it even if the binary-path detection failed. A real user-global
9912
+ // install of claude is always current (2.1.116+), so seeing 2.1.89 etc.
9913
+ // means we're reading a stale bundle SynaBun shipped in a prior release.
9914
+ if (tool.key === 'claude-code' && installed && KNOWN_STALE_CLAUDE_VERSIONS.has(installed)) {
9915
+ console.warn(
9916
+ ` [version-guard] Rejected claude v${installed} from ${probedBinPath || '(unknown path)'}` +
9917
+ ` — matches known-stale SynaBun bundle. Treating as not installed. ` +
9918
+ `If you actually have this version installed, remove it from KNOWN_STALE_CLAUDE_VERSIONS.`
9919
+ );
9920
+ installed = null;
9921
+ }
9839
9922
 
9840
9923
  let latest = null;
9841
9924
  try {
@@ -9852,7 +9935,7 @@ async function checkToolVersions() {
9852
9935
  }
9853
9936
  } catch { /* network error */ }
9854
9937
 
9855
- const binPath = installed ? resolveBinPath(tool.cmd, cleanEnv) : null;
9938
+ const binPath = installed ? probedBinPath : null;
9856
9939
  const installSource = detectInstallSource(binPath);
9857
9940
  const updateCommand = installed ? buildUpdateCommand(tool.key, installSource) : null;
9858
9941
 
@@ -21082,6 +21165,12 @@ const httpServer = app.listen(PORT, async () => {
21082
21165
  // Check for npm updates
21083
21166
  try { await checkNpmUpdate(); } catch {}
21084
21167
 
21168
+ // Aggressively prune any stale bundled CLI tools left in our node_modules
21169
+ // before the version check runs — defends against `npm install` not auto-
21170
+ // pruning packages that were removed from package.json (recurring source of
21171
+ // phantom-version badges, e.g. v2.1.89 from a stale @anthropic-ai/claude-code).
21172
+ try { pruneBundledCliTools(); } catch {}
21173
+
21085
21174
  // Check for CLI tool updates
21086
21175
  try { await checkToolVersions(); } catch {}
21087
21176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.4.20-beta.5",
3
+ "version": "2026.4.20-beta.7",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",