@nexus-cortex/cli 4.137.0 → 4.137.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/bin/cortex.js CHANGED
@@ -29,6 +29,7 @@ import { existsSync, readFileSync, realpathSync, mkdirSync, openSync, writeFileS
29
29
  import { homedir } from 'os';
30
30
  import { createRequire } from 'module';
31
31
  import { Agent as UndiciAgent } from 'undici';
32
+ import { isStaleServer, staleServerPolicy, wrongProject } from './stale-server.js';
32
33
 
33
34
  // 🔴 ENV BOOTSTRAP FIRST (operator directive: seed+load .env "no matter how
34
35
  // invoked"). This bin historically NEVER ran bootstrapEnv — it read only the raw
@@ -505,10 +506,48 @@ async function ensureKeysOrExit() {
505
506
  process.exit(1);
506
507
  }
507
508
 
509
+ function cliVersion() {
510
+ try { return JSON.parse(readFileSync(join(__cortex_dirname, '..', 'package.json'), 'utf8')).version; } catch { return 'unknown'; }
511
+ }
512
+
508
513
  async function ensureServer() {
509
514
  if (!(await isServerUp())) {
510
515
  await startServer();
516
+ return;
511
517
  }
518
+ // 2026-11-14 (m2 battery C5/C7): a server started BEFORE `cortex update` keeps serving the OLD harness — no USER.md slot, no new guards —
519
+ // and nothing said so. Compare versions; by default restart a stale server (CORTEX_STALE_SERVER=warn|fail to change that).
520
+ let sv, sp;
521
+ try { const h = (await (await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(3000) })).json())?.server; sv = h?.version; sp = h?.projectPath; } catch { /* treat as unknown */ }
522
+ const cv = cliVersion();
523
+ const stale = isStaleServer(sv, cv);
524
+ // a server bound to ANOTHER project would put this turn's memory, files and session there (m2 battery C4) — same policy as a stale one
525
+ const other = !stale && wrongProject(sp, process.env.PROJECT_PATH || process.cwd());
526
+ if (!stale && !other) return;
527
+ const policy = staleServerPolicy(process.env);
528
+ const msg = stale
529
+ ? `[cortex] the server on ${BASE_URL} runs ${sv && sv !== '4.0.0' ? sv : 'an older release'}, this CLI is ${cv}`
530
+ : `[cortex] the server on ${BASE_URL} is bound to ${sp}, but you are in ${process.cwd()}`;
531
+ if (policy === 'warn') { process.stderr.write(`${msg} — continuing on the OLD server (CORTEX_STALE_SERVER=warn)\n`); return; }
532
+ if (policy === 'fail') { process.stderr.write(`${msg} — refusing (CORTEX_STALE_SERVER=fail). Run: cortex --shutdown\n`); process.exit(2); }
533
+ process.stderr.write(`${msg} — restarting it ${stale ? 'so this turn runs the installed version' : 'in this directory'}\n`);
534
+ try { await fetch(`${BASE_URL}/shutdown`, { method: 'POST', signal: AbortSignal.timeout(5000) }); } catch { /* it may already be going */ }
535
+ // wait until the PORT is free, not just until /health stops answering: a server started while the old one still holds the socket hops to
536
+ // port+1 (EADDRINUSE fallback) and this CLI would then talk to nothing (e2e 2026-11-14: new server on 4520, CLI said ready on 4519)
537
+ const { createConnection } = await import('net');
538
+ const portOf = () => Number(new URL(BASE_URL).port || 80);
539
+ const portBusy = () => new Promise((res) => { const c = createConnection({ port: portOf(), host: new URL(BASE_URL).hostname }); c.once('connect', () => { c.destroy(); res(true); }); c.once('error', () => res(false)); });
540
+ let free = 0;
541
+ for (let i = 0; i < 80 && free < 3; i++) { free = (await portBusy()) ? 0 : free + 1; await new Promise((r) => setTimeout(r, 250)); }
542
+ if (free < 3) { process.stderr.write(`[cortex] the old server did not release port ${portOf()} — run: cortex --shutdown\n`); process.exit(2); }
543
+ await startServer();
544
+ // verify the server that answers is the one we wanted (version + project)
545
+ try {
546
+ const h = (await (await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(3000) })).json())?.server;
547
+ if (isStaleServer(h?.version, cv) || wrongProject(h?.projectPath, process.env.PROJECT_PATH || process.cwd())) {
548
+ process.stderr.write(`[cortex] after the restart ${BASE_URL} still answers as ${h?.version} in ${h?.projectPath} — stop it by hand: cortex --shutdown\n`); process.exit(2);
549
+ }
550
+ } catch { /* startServer already verified it is up */ }
512
551
  }
513
552
 
514
553
  async function fetchJSON(path, options = {}, timeoutMs = 30000) {
@@ -0,0 +1,28 @@
1
+ import { realpathSync } from 'fs';
2
+ // Stale-server detection (2026-11-14, m2 battery C5/C7): the CLI attached to a server started before `cortex update` and the turn ran
3
+ // OLD code silently. Pure helpers so the policy is unit-tested (tests/unit/staleServer.test.ts); used by bin/cortex.js ensureServer().
4
+ const parse = (v) => String(v ?? '').split('-')[0].split('.').map((n) => Number.parseInt(n, 10));
5
+
6
+ /** True when the running server is older than this CLI (or reports no / the legacy hard-coded '4.0.0' version). */
7
+ export function isStaleServer(serverVersion, cliVersion) {
8
+ const c = parse(cliVersion);
9
+ if (c.length < 3 || c.some(Number.isNaN)) return false; // unknown CLI version: never force a restart
10
+ const s = parse(serverVersion);
11
+ if (s.length < 3 || s.some(Number.isNaN)) return true;
12
+ for (let i = 0; i < 3; i++) { if (s[i] !== c[i]) return s[i] < c[i]; }
13
+ return false;
14
+ }
15
+
16
+ /** CORTEX_STALE_SERVER = restart (default) | warn | fail. */
17
+ export function staleServerPolicy(env = process.env) {
18
+ const v = String(env.CORTEX_STALE_SERVER ?? '').trim().toLowerCase();
19
+ return v === 'warn' || v === 'fail' ? v : 'restart';
20
+ }
21
+
22
+ /** True when the running server is bound to a DIFFERENT project directory than the caller's cwd (memory, files and sessions would land in
23
+ * the server's project — m2 battery C4). Unknown server project → false (an old server is caught by the version check instead). */
24
+ export function wrongProject(serverProject, cwd) {
25
+ if (!serverProject || !cwd) return false;
26
+ const norm = (p) => { let r = String(p); try { r = realpathSync(r); } catch { /* keep as given */ } return r.replace(/\/+$/, '') || '/'; };
27
+ return norm(serverProject) !== norm(cwd);
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexus-cortex/cli",
3
- "version": "4.137.0",
3
+ "version": "4.137.1",
4
4
  "description": "Nexus Cortex CLI - Terminal interface for multi-provider LLM orchestration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "prepack": "node ../../scripts/copy-pkg-cortex-scaffold.mjs"
20
20
  },
21
21
  "dependencies": {
22
- "@nexus-cortex/core": "4.137.0",
22
+ "@nexus-cortex/core": "4.137.1",
23
23
  "chalk": "^4.1.2",
24
24
  "cli-spinners": "^2.9.0",
25
25
  "commander": "^11.0.0",