@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.
- package/bin/cortex.js +122 -1
- 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.
|
|
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.
|
|
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",
|