@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.
Files changed (2) hide show
  1. package/bin/cortex.js +133 -2
  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
- // Need a prompt for message commands
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
- console.error('Usage: cortex "your prompt here"');
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.28.0",
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.28.0",
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",