@nexus-cortex/cli 4.28.0 → 4.30.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 +133 -2
- package/package.json +2 -2
package/bin/cortex.js
CHANGED
|
@@ -112,6 +112,86 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
112
112
|
process.exit(0);
|
|
113
113
|
}
|
|
114
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
|
+
|
|
115
195
|
// ── Headless Commander delegation ─────────────────────────────────
|
|
116
196
|
// `cortex` is primarily the HTTP chat/PR client; the headless Commander program
|
|
117
197
|
// (autoresearch, models, message, mcp, config, …) lives in dist/index.js. If the
|
|
@@ -383,6 +463,42 @@ async function fetchJSON(path, options = {}, timeoutMs = 30000) {
|
|
|
383
463
|
|
|
384
464
|
// ── Commands ──────────────────────────────────────────────────────
|
|
385
465
|
|
|
466
|
+
// Interactive chat REPL — `cortex` with no message drops you here so you can talk
|
|
467
|
+
// line-by-line without the shell mangling ?/*/quotes. The server keeps the session,
|
|
468
|
+
// so it's multi-turn. Reuses the same /v1/messages send as the one-shot path.
|
|
469
|
+
async function runInteractiveChat() {
|
|
470
|
+
if (newSession) {
|
|
471
|
+
try { await fetchJSON('/sessions/new', { method: 'POST' }); } catch { /* fresh session is best-effort */ }
|
|
472
|
+
}
|
|
473
|
+
const { createInterface } = await import('node:readline/promises');
|
|
474
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
475
|
+
process.stdout.write(
|
|
476
|
+
'\n Nexus Cortex — interactive chat. Type a message and press Enter.\n' +
|
|
477
|
+
' The session persists across messages. Type "exit" (or press Ctrl-D) to quit.\n\n',
|
|
478
|
+
);
|
|
479
|
+
try {
|
|
480
|
+
for (;;) {
|
|
481
|
+
let line;
|
|
482
|
+
try { line = (await rl.question('cortex> ')).trim(); }
|
|
483
|
+
catch { break; } // Ctrl-D / closed input
|
|
484
|
+
if (!line) continue;
|
|
485
|
+
if (line === 'exit' || line === 'quit' || line === ':q') break;
|
|
486
|
+
const payload = { messages: [{ role: 'user', content: line }] };
|
|
487
|
+
if (modelId) payload.model = modelId;
|
|
488
|
+
try {
|
|
489
|
+
const data = await fetchJSON('/v1/messages', { method: 'POST', body: JSON.stringify(payload) }, messageTimeoutMs);
|
|
490
|
+
const text = (data.content || []).filter((b) => b.type === 'text').map((b) => b.text).join('\n');
|
|
491
|
+
process.stdout.write('\n' + (text || '(no text response)') + '\n\n');
|
|
492
|
+
} catch (err) {
|
|
493
|
+
process.stderr.write(`\n [error] ${err.message}\n\n`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} finally {
|
|
497
|
+
rl.close();
|
|
498
|
+
}
|
|
499
|
+
process.stdout.write(' Bye.\n');
|
|
500
|
+
}
|
|
501
|
+
|
|
386
502
|
async function run() {
|
|
387
503
|
// --shutdown: stop the server and exit
|
|
388
504
|
if (doShutdown) {
|
|
@@ -390,6 +506,13 @@ async function run() {
|
|
|
390
506
|
return;
|
|
391
507
|
}
|
|
392
508
|
|
|
509
|
+
// First-run onboarding: no API key anywhere → walk the user through setup before
|
|
510
|
+
// starting the server. Skipped for pure server-management invocations.
|
|
511
|
+
const SKIP_SETUP = ['--help', '-h', '--stats', '--sessions', '--tmux'].some((f) => args.includes(f));
|
|
512
|
+
if (!SKIP_SETUP && !hasApiKey()) {
|
|
513
|
+
await runSetupWizard();
|
|
514
|
+
}
|
|
515
|
+
|
|
393
516
|
await ensureServer();
|
|
394
517
|
|
|
395
518
|
// --tmux: list active tmux sessions (served by SandboxViewServer on port+1)
|
|
@@ -510,9 +633,17 @@ async function run() {
|
|
|
510
633
|
}
|
|
511
634
|
}
|
|
512
635
|
|
|
513
|
-
//
|
|
636
|
+
// No message provided. In an interactive terminal (and NOT agent/run mode), drop
|
|
637
|
+
// into the chat REPL so the shell never mangles ?/*/quotes. agent/run with no task,
|
|
638
|
+
// or any non-TTY (scripted) use, still gets the usage error — unchanged.
|
|
514
639
|
if (!prompt) {
|
|
515
|
-
|
|
640
|
+
if (!__agentMode && process.stdin.isTTY) {
|
|
641
|
+
await runInteractiveChat();
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
console.error(__agentMode
|
|
645
|
+
? 'Usage: cortex agent "<task>" (or cortex run "<task>")'
|
|
646
|
+
: 'Usage: cortex "your prompt here"');
|
|
516
647
|
console.error(' cortex --help for more options');
|
|
517
648
|
process.exit(1);
|
|
518
649
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexus-cortex/cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.30.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.30.0",
|
|
23
23
|
"chalk": "^4.1.2",
|
|
24
24
|
"cli-spinners": "^2.9.0",
|
|
25
25
|
"commander": "^11.0.0",
|