@nexus-cortex/cli 4.27.0 → 4.29.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.
Files changed (2) hide show
  1. package/bin/cortex.js +122 -1
  2. package/package.json +2 -2
package/bin/cortex.js CHANGED
@@ -25,7 +25,8 @@
25
25
  import { spawn, spawnSync } from 'child_process';
26
26
  import { fileURLToPath } from 'url';
27
27
  import { dirname, join, resolve } from 'path';
28
- import { existsSync, readFileSync, realpathSync } from 'fs';
28
+ import { existsSync, readFileSync, realpathSync, statSync, mkdirSync, writeFileSync } from 'fs';
29
+ import { homedir } from 'os';
29
30
  import { createRequire } from 'module';
30
31
 
31
32
  const __cortex_require = createRequire(import.meta.url);
@@ -61,6 +62,37 @@ if (!process.env.CORTEX_ROOT) {
61
62
  const BASE_PORT = process.env.PORT || '4000';
62
63
  const BASE_URL = process.env.CORTEX_URL || `http://localhost:${BASE_PORT}`;
63
64
 
65
+ // ── Background auto-update (opt-out) ──────────────────────────────
66
+ // On by default; set CORTEX_AUTO_UPDATE=false (or pass --no-auto-update) to disable.
67
+ // Fire-and-forget: spawns a DETACHED `npm i -g nexus-cortex@latest` that the current
68
+ // run does NOT wait on and that does NOT hot-swap the running process — the update
69
+ // lands on the NEXT launch. Throttled to once per 24h via a marker file. Skipped for
70
+ // dev/source checkouts (only updates an actual global npm install). Never blocks or
71
+ // throws into startup — any failure is swallowed.
72
+ function maybeAutoUpdate() {
73
+ try {
74
+ const optOut = String(process.env.CORTEX_AUTO_UPDATE).toLowerCase() === 'false'
75
+ || process.argv.includes('--no-auto-update');
76
+ if (optOut) return;
77
+ // Only auto-update a real installed package, never a git/source checkout.
78
+ if (!__cortex_dirname.includes('node_modules')) return;
79
+
80
+ const marker = join(homedir(), '.cortex', '.update-check');
81
+ const DAY_MS = 24 * 60 * 60 * 1000;
82
+ try {
83
+ if (Date.now() - statSync(marker).mtimeMs < DAY_MS) return; // throttled
84
+ } catch { /* no marker yet — proceed */ }
85
+ try { mkdirSync(dirname(marker), { recursive: true }); writeFileSync(marker, String(Date.now())); } catch {}
86
+
87
+ const child = spawn('npm', ['install', '-g', 'nexus-cortex@latest'], {
88
+ detached: true,
89
+ stdio: 'ignore',
90
+ });
91
+ child.unref();
92
+ } catch { /* never let auto-update affect the CLI */ }
93
+ }
94
+ maybeAutoUpdate();
95
+
64
96
  let serverProcess = null;
65
97
 
66
98
  // ── Argument parsing ──────────────────────────────────────────────
@@ -80,6 +112,86 @@ if (args.includes('--version') || args.includes('-v')) {
80
112
  process.exit(0);
81
113
  }
82
114
 
115
+ // ── Setup wizard (interactive API-key onboarding) ─────────────────
116
+ // Global config lives at ~/.cortex/.env so a global npm install works from ANY
117
+ // folder (the server loads it). Runs on `cortex config init` and on first run
118
+ // when no key is configured anywhere.
119
+ const PROVIDERS = [
120
+ { name: 'Anthropic (Claude)', keyVar: 'ANTHROPIC_API_KEY', model: 'claude-sonnet-4-6', hint: 'sk-ant-…' },
121
+ { name: 'OpenAI (GPT)', keyVar: 'OPENAI_API_KEY', model: 'gpt-5-mini', hint: 'sk-…' },
122
+ { name: 'Google (Gemini)', keyVar: 'GEMINI_API_KEY', model: 'gemini-2.5-flash', hint: 'AIza…' },
123
+ { name: 'DeepSeek', keyVar: 'DEEPSEEK_API_KEY', model: 'deepseek-v4-pro', hint: 'sk-…' },
124
+ { name: 'xAI (Grok)', keyVar: 'XAI_API_KEY', model: 'grok-4.3', hint: 'xai-…' },
125
+ ];
126
+ const KEY_VARS = [...PROVIDERS.map((p) => p.keyVar), 'GOOGLE_API_KEY'];
127
+ const GLOBAL_ENV = join(homedir(), '.cortex', '.env');
128
+
129
+ function hasApiKey() {
130
+ if (KEY_VARS.some((k) => (process.env[k] || '').trim())) return true;
131
+ for (const f of [GLOBAL_ENV, join(process.cwd(), '.env')]) {
132
+ try {
133
+ const txt = readFileSync(f, 'utf8');
134
+ if (KEY_VARS.some((k) => new RegExp('^' + k + '=\\S', 'm').test(txt))) return true;
135
+ } catch { /* file absent */ }
136
+ }
137
+ return false;
138
+ }
139
+
140
+ function writeGlobalEnv(kv) {
141
+ mkdirSync(dirname(GLOBAL_ENV), { recursive: true });
142
+ let lines = [];
143
+ try { lines = readFileSync(GLOBAL_ENV, 'utf8').split('\n').filter((l) => l.length); } catch { /* new file */ }
144
+ for (const [k, v] of Object.entries(kv)) {
145
+ const i = lines.findIndex((l) => l.startsWith(k + '='));
146
+ if (i >= 0) lines[i] = `${k}=${v}`; else lines.push(`${k}=${v}`);
147
+ }
148
+ writeFileSync(GLOBAL_ENV, lines.join('\n') + '\n', { mode: 0o600 }); // user-only — holds a secret
149
+ }
150
+
151
+ async function runSetupWizard() {
152
+ if (!process.stdin.isTTY) {
153
+ process.stderr.write(
154
+ '\n No API key configured. Run `cortex config init` for interactive setup, or set one:\n' +
155
+ ' export ANTHROPIC_API_KEY=sk-ant-…\n' +
156
+ ' export DEFAULT_MODEL_ID=claude-sonnet-4-6\n\n',
157
+ );
158
+ process.exit(1);
159
+ }
160
+ const { createInterface } = await import('node:readline/promises');
161
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
162
+ try {
163
+ process.stdout.write('\n Welcome to Nexus Cortex — quick setup (~30s).\n\n Which AI provider?\n');
164
+ PROVIDERS.forEach((p, i) => process.stdout.write(` ${i + 1}) ${p.name}\n`));
165
+ let choice;
166
+ for (;;) {
167
+ const n = parseInt((await rl.question(`\n Choose 1-${PROVIDERS.length}: `)).trim(), 10);
168
+ if (n >= 1 && n <= PROVIDERS.length) { choice = PROVIDERS[n - 1]; break; }
169
+ process.stdout.write(` Please enter a number 1-${PROVIDERS.length}.\n`);
170
+ }
171
+ let key = '';
172
+ while (!key) {
173
+ key = (await rl.question(`\n Paste your ${choice.name} API key (${choice.hint}): `)).trim();
174
+ if (!key) process.stdout.write(' An API key is required.\n');
175
+ }
176
+ const model = (await rl.question(`\n Default model [${choice.model}]: `)).trim() || choice.model;
177
+ writeGlobalEnv({ [choice.keyVar]: key, DEFAULT_MODEL_ID: model });
178
+ process.stdout.write(`\n ✓ Saved to ${GLOBAL_ENV}\n You're set — try: cortex "what is 2 + 2?"\n\n`);
179
+ } catch {
180
+ // Ctrl-C / Ctrl-D / closed input — exit cleanly instead of dumping a stack trace.
181
+ process.stdout.write('\n Setup cancelled. Run `cortex config init` any time to finish.\n');
182
+ rl.close();
183
+ process.exit(1);
184
+ } finally {
185
+ rl.close();
186
+ }
187
+ }
188
+
189
+ // `cortex config init` — interactive setup (intercept before the Commander handoff).
190
+ if (args[0] === 'config' && args[1] === 'init') {
191
+ await runSetupWizard();
192
+ process.exit(0);
193
+ }
194
+
83
195
  // ── Headless Commander delegation ─────────────────────────────────
84
196
  // `cortex` is primarily the HTTP chat/PR client; the headless Commander program
85
197
  // (autoresearch, models, message, mcp, config, …) lives in dist/index.js. If the
@@ -210,6 +322,8 @@ FLAGS:
210
322
  --timeout MS Request timeout in ms (default: 600000 = 10 min)
211
323
  --idle-timeout SECS Auto-shutdown server after N seconds of inactivity
212
324
  --cwd DIR (agent) Run in DIR — the agent's file tools operate there
325
+ --no-auto-update Skip the background update check for this run
326
+ (also: export CORTEX_AUTO_UPDATE=false to disable it entirely)
213
327
  --shutdown Stop the running server and exit
214
328
  --tmux List active tmux sessions with dashboard URLs
215
329
  --stats Show current session statistics
@@ -356,6 +470,13 @@ async function run() {
356
470
  return;
357
471
  }
358
472
 
473
+ // First-run onboarding: no API key anywhere → walk the user through setup before
474
+ // starting the server. Skipped for pure server-management invocations.
475
+ const SKIP_SETUP = ['--help', '-h', '--stats', '--sessions', '--tmux'].some((f) => args.includes(f));
476
+ if (!SKIP_SETUP && !hasApiKey()) {
477
+ await runSetupWizard();
478
+ }
479
+
359
480
  await ensureServer();
360
481
 
361
482
  // --tmux: list active tmux sessions (served by SandboxViewServer on port+1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexus-cortex/cli",
3
- "version": "4.27.0",
3
+ "version": "4.29.0",
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.27.0",
22
+ "@nexus-cortex/core": "^4.29.0",
23
23
  "chalk": "^4.1.2",
24
24
  "cli-spinners": "^2.9.0",
25
25
  "commander": "^11.0.0",