dual-brain 7.0.1 → 7.0.2
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/dual-brain.mjs +144 -28
- package/package.json +1 -1
- package/src/profile.mjs +135 -3
package/bin/dual-brain.mjs
CHANGED
|
@@ -5,12 +5,13 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
11
|
ensureProfile, loadProfile, saveProfile, runOnboarding,
|
|
11
12
|
rememberPreference, forgetPreference, getActivePreferences,
|
|
12
13
|
getAvailableProviders, isSoloBrain, getHeadModel,
|
|
13
|
-
detectAuth, detectEnvironment,
|
|
14
|
+
detectAuth, detectEnvironment, setupAuth,
|
|
14
15
|
} from '../src/profile.mjs';
|
|
15
16
|
|
|
16
17
|
import { detectTask } from '../src/detect.mjs';
|
|
@@ -45,8 +46,9 @@ function printHelp() {
|
|
|
45
46
|
dual-brain <command> [options]
|
|
46
47
|
|
|
47
48
|
Commands:
|
|
48
|
-
init First-time setup
|
|
49
|
+
init First-time setup → flows into interactive REPL
|
|
49
50
|
auth Show authentication status for all providers
|
|
51
|
+
auth setup Paste API keys directly (recommended for Replit)
|
|
50
52
|
install Install Claude Code hooks into the current project
|
|
51
53
|
go "task description" Detect → decide → dispatch a task
|
|
52
54
|
--dry-run Show routing decision without executing
|
|
@@ -59,6 +61,14 @@ Commands:
|
|
|
59
61
|
remember "preference" Save a project-scoped preference
|
|
60
62
|
forget "preference" Remove a preference by fuzzy match
|
|
61
63
|
|
|
64
|
+
Interactive REPL (entered after init or npx dual-brain with no args):
|
|
65
|
+
<task description> Dispatch a task directly
|
|
66
|
+
go <task> Same as dual-brain go
|
|
67
|
+
status / auth / init Run commands without exiting
|
|
68
|
+
auth setup Re-run API key setup
|
|
69
|
+
help Show this help
|
|
70
|
+
exit / quit / q Exit the REPL
|
|
71
|
+
|
|
62
72
|
Options:
|
|
63
73
|
--version Print version
|
|
64
74
|
--help Show this help
|
|
@@ -85,14 +95,14 @@ function printAuthTable(auth) {
|
|
|
85
95
|
: ` Claude: ✗ not found`;
|
|
86
96
|
const claudeLine2 = auth.claude.found
|
|
87
97
|
? ` ${auth.claude.masked}`
|
|
88
|
-
: ` run:
|
|
98
|
+
: ` run: dual-brain auth setup`;
|
|
89
99
|
|
|
90
100
|
const openaiLine1 = auth.openai.found
|
|
91
101
|
? ` OpenAI: ✓ found via ${auth.openai.source}`
|
|
92
102
|
: ` OpenAI: ✗ not found`;
|
|
93
103
|
const openaiLine2 = auth.openai.found
|
|
94
104
|
? ` ${auth.openai.masked}`
|
|
95
|
-
: ` run:
|
|
105
|
+
: ` run: dual-brain auth setup`;
|
|
96
106
|
|
|
97
107
|
console.log(`╔${bar}╗`);
|
|
98
108
|
console.log(`║${pad(' Auth Status')}║`);
|
|
@@ -127,8 +137,8 @@ async function cmdCard() {
|
|
|
127
137
|
// Auth status warnings (non-blocking)
|
|
128
138
|
const auth = await detectAuth();
|
|
129
139
|
const warnings = [];
|
|
130
|
-
if (!auth.claude.found) warnings.push('Claude auth not found — run:
|
|
131
|
-
if (!auth.openai.found) warnings.push('OpenAI auth not found — run:
|
|
140
|
+
if (!auth.claude.found) warnings.push('Claude auth not found — run: dual-brain auth setup');
|
|
141
|
+
if (!auth.openai.found) warnings.push('OpenAI auth not found — run: dual-brain auth setup');
|
|
132
142
|
if (warnings.length > 0) {
|
|
133
143
|
console.log('\nAuth warnings:');
|
|
134
144
|
for (const w of warnings) console.log(` ⚠ ${w}`);
|
|
@@ -144,7 +154,7 @@ async function cmdCard() {
|
|
|
144
154
|
|
|
145
155
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
146
156
|
|
|
147
|
-
async function cmdInit() {
|
|
157
|
+
async function cmdInit(rl) {
|
|
148
158
|
const cwd = process.cwd();
|
|
149
159
|
|
|
150
160
|
// --- Step 1: Auth preflight ---
|
|
@@ -153,39 +163,61 @@ async function cmdInit() {
|
|
|
153
163
|
|
|
154
164
|
const noneFound = !auth.claude.found && !auth.openai.found;
|
|
155
165
|
if (noneFound) {
|
|
156
|
-
console.log('\nNo AI provider credentials found.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
console.log('\nNo AI provider credentials found. Let\'s set up at least one now.\n');
|
|
167
|
+
// Use the provided rl (REPL instance) or create a temporary one
|
|
168
|
+
const rlOwned = !rl;
|
|
169
|
+
if (!rl) rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
170
|
+
try {
|
|
171
|
+
await setupAuth(rl);
|
|
172
|
+
} finally {
|
|
173
|
+
if (rlOwned) rl.close();
|
|
174
|
+
}
|
|
175
|
+
// Re-check after setup
|
|
176
|
+
const authAfter = await detectAuth();
|
|
177
|
+
if (!authAfter.claude.found && !authAfter.openai.found) {
|
|
178
|
+
console.log('\nNo credentials configured. You can run "auth setup" in the REPL anytime.');
|
|
179
|
+
// Still flow into REPL — don't exit
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
161
182
|
}
|
|
162
183
|
|
|
163
|
-
// --- Step 2: Run onboarding wizard
|
|
164
|
-
const profile = await runOnboarding({ interactive: true, detectedAuth: auth });
|
|
184
|
+
// --- Step 2: Run onboarding wizard (pass shared rl so it isn't closed) ---
|
|
185
|
+
const profile = await runOnboarding({ interactive: true, detectedAuth: auth, rl });
|
|
165
186
|
saveProfile(profile, { cwd });
|
|
166
187
|
|
|
167
|
-
// --- Step 3: Show dashboard
|
|
188
|
+
// --- Step 3: Show dashboard ---
|
|
168
189
|
console.log('');
|
|
169
190
|
const repo = loadRepoCache(cwd);
|
|
170
191
|
const session = loadSession(cwd);
|
|
171
192
|
const health = getHealth(cwd);
|
|
172
193
|
const card = formatSessionCard(session, repo, health);
|
|
173
194
|
console.log(card);
|
|
174
|
-
console.log('\nReady!
|
|
195
|
+
console.log('\nReady! Type a task below, or "help" for commands.\n');
|
|
175
196
|
}
|
|
176
197
|
|
|
177
|
-
async function cmdAuth() {
|
|
198
|
+
async function cmdAuth(subArgs = [], rl) {
|
|
199
|
+
const sub = subArgs[0];
|
|
200
|
+
|
|
201
|
+
if (sub === 'setup') {
|
|
202
|
+
return cmdAuthSetup(rl);
|
|
203
|
+
}
|
|
204
|
+
|
|
178
205
|
const auth = await detectAuth();
|
|
179
206
|
printAuthTable(auth);
|
|
180
207
|
|
|
181
|
-
// If anything is missing,
|
|
182
|
-
if (!auth.claude.found) {
|
|
183
|
-
console.log('\
|
|
184
|
-
console.log(' claude auth login');
|
|
208
|
+
// If anything is missing, point to setup command
|
|
209
|
+
if (!auth.claude.found || !auth.openai.found) {
|
|
210
|
+
console.log('\nRun "dual-brain auth setup" (or "auth setup" in REPL) to paste API keys.');
|
|
185
211
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function cmdAuthSetup(rl) {
|
|
215
|
+
const rlOwned = !rl;
|
|
216
|
+
if (!rl) rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
217
|
+
try {
|
|
218
|
+
await setupAuth(rl);
|
|
219
|
+
} finally {
|
|
220
|
+
if (rlOwned) rl.close();
|
|
189
221
|
}
|
|
190
222
|
}
|
|
191
223
|
|
|
@@ -489,6 +521,65 @@ function cmdForget(text) {
|
|
|
489
521
|
console.log('Preference removed (if matched).');
|
|
490
522
|
}
|
|
491
523
|
|
|
524
|
+
// ─── Interactive REPL ────────────────────────────────────────────────────────
|
|
525
|
+
|
|
526
|
+
async function startRepl(rl) {
|
|
527
|
+
// rl may have been created by cmdCard/cmdInit — reuse it.
|
|
528
|
+
// If not provided, create a fresh one.
|
|
529
|
+
const rlOwned = !rl;
|
|
530
|
+
if (!rl) rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
531
|
+
|
|
532
|
+
console.log('\nType a task or command. Type "help" for commands, "exit" to quit.\n');
|
|
533
|
+
|
|
534
|
+
const prompt = () => {
|
|
535
|
+
rl.question('dual-brain> ', async (input) => {
|
|
536
|
+
const line = input.trim();
|
|
537
|
+
if (!line) { prompt(); return; }
|
|
538
|
+
|
|
539
|
+
if (line === 'exit' || line === 'quit' || line === 'q') {
|
|
540
|
+
if (rlOwned) rl.close();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
if (line === 'help') {
|
|
546
|
+
printHelp();
|
|
547
|
+
} else if (line === 'status') {
|
|
548
|
+
await cmdStatus([]);
|
|
549
|
+
} else if (line === 'auth setup' || line === 'auth-setup') {
|
|
550
|
+
await cmdAuthSetup(rl);
|
|
551
|
+
} else if (line === 'auth') {
|
|
552
|
+
await cmdAuth([], rl);
|
|
553
|
+
} else if (line.startsWith('go ')) {
|
|
554
|
+
await cmdGo(line.slice(3).trim().split(/\s+/));
|
|
555
|
+
} else if (line.startsWith('remember ')) {
|
|
556
|
+
cmdRemember(line.slice(9).trim());
|
|
557
|
+
} else if (line.startsWith('forget ')) {
|
|
558
|
+
cmdForget(line.slice(7).trim());
|
|
559
|
+
} else if (line.startsWith('hot ')) {
|
|
560
|
+
cmdHot(line.slice(4).trim());
|
|
561
|
+
} else if (line.startsWith('cool ')) {
|
|
562
|
+
cmdCool(line.slice(5).trim());
|
|
563
|
+
} else if (line === 'init') {
|
|
564
|
+
await cmdInit(rl);
|
|
565
|
+
} else {
|
|
566
|
+
// Treat as a task description → go
|
|
567
|
+
await cmdGo([line]);
|
|
568
|
+
}
|
|
569
|
+
} catch (e) {
|
|
570
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
prompt(); // loop back
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
prompt();
|
|
578
|
+
|
|
579
|
+
// Return a promise that resolves when rl closes (exit/quit)
|
|
580
|
+
return new Promise(resolve => rl.on('close', resolve));
|
|
581
|
+
}
|
|
582
|
+
|
|
492
583
|
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
493
584
|
|
|
494
585
|
async function main() {
|
|
@@ -496,12 +587,37 @@ async function main() {
|
|
|
496
587
|
const cmd = args[0];
|
|
497
588
|
|
|
498
589
|
if (cmd === '--help' || cmd === '-h') { printHelp(); return; }
|
|
499
|
-
if (
|
|
500
|
-
|
|
590
|
+
if (cmd === '--version' || cmd === '-v') { console.log(readVersion()); return; }
|
|
591
|
+
|
|
592
|
+
// Interactive-only commands: enter REPL after completing (only when TTY)
|
|
593
|
+
const isInteractive = process.stdin.isTTY;
|
|
501
594
|
|
|
502
|
-
if (cmd
|
|
595
|
+
if (!cmd) {
|
|
596
|
+
await cmdCard();
|
|
597
|
+
if (isInteractive) await startRepl();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (cmd === 'init') {
|
|
602
|
+
if (isInteractive) {
|
|
603
|
+
// Create the shared rl upfront so init wizard and REPL share it
|
|
604
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
605
|
+
await cmdInit(rl);
|
|
606
|
+
await startRepl(rl);
|
|
607
|
+
} else {
|
|
608
|
+
await cmdInit();
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// One-shot commands — run and exit
|
|
503
614
|
if (cmd === 'install') { await cmdInstall(); return; }
|
|
504
|
-
if (cmd === 'auth')
|
|
615
|
+
if (cmd === 'auth') {
|
|
616
|
+
const sub = args[1];
|
|
617
|
+
if (sub === 'setup') { await cmdAuthSetup(); return; }
|
|
618
|
+
await cmdAuth(args.slice(1));
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
505
621
|
if (cmd === 'go') { await cmdGo(args.slice(1)); return; }
|
|
506
622
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
507
623
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
package/package.json
CHANGED
package/src/profile.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { createInterface } from 'readline';
|
|
26
26
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
27
27
|
import { homedir } from 'os';
|
|
28
|
-
import { join } from 'path';
|
|
28
|
+
import { dirname, join } from 'path';
|
|
29
29
|
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
// Claude Code memory integration
|
|
@@ -174,6 +174,20 @@ async function detectAuth() {
|
|
|
174
174
|
} catch { continue; }
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// --- Claude: check .dualbrain/auth.json (before env var) ---
|
|
178
|
+
if (!results.claude.found) {
|
|
179
|
+
const storedAuth = loadAuthKeys();
|
|
180
|
+
if (storedAuth.claude?.key) {
|
|
181
|
+
const expired = storedAuth.claude.expiresAt && new Date(storedAuth.claude.expiresAt) <= new Date();
|
|
182
|
+
if (!expired) {
|
|
183
|
+
results.claude.found = true;
|
|
184
|
+
results.claude.source = '.dualbrain/auth.json';
|
|
185
|
+
results.claude.masked = _maskCredential(storedAuth.claude.key);
|
|
186
|
+
process.env.ANTHROPIC_API_KEY = storedAuth.claude.key;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
177
191
|
// --- Claude: fallback to ANTHROPIC_API_KEY env var ---
|
|
178
192
|
if (!results.claude.found && process.env.ANTHROPIC_API_KEY) {
|
|
179
193
|
results.claude.found = true;
|
|
@@ -208,6 +222,20 @@ async function detectAuth() {
|
|
|
208
222
|
} catch { continue; }
|
|
209
223
|
}
|
|
210
224
|
|
|
225
|
+
// --- OpenAI: check .dualbrain/auth.json (before env var) ---
|
|
226
|
+
if (!results.openai.found) {
|
|
227
|
+
const storedAuth = loadAuthKeys();
|
|
228
|
+
if (storedAuth.openai?.key) {
|
|
229
|
+
const expired = storedAuth.openai.expiresAt && new Date(storedAuth.openai.expiresAt) <= new Date();
|
|
230
|
+
if (!expired) {
|
|
231
|
+
results.openai.found = true;
|
|
232
|
+
results.openai.source = '.dualbrain/auth.json';
|
|
233
|
+
results.openai.masked = _maskCredential(storedAuth.openai.key);
|
|
234
|
+
process.env.OPENAI_API_KEY = storedAuth.openai.key;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
211
239
|
// --- OpenAI: fallback to OPENAI_API_KEY env var ---
|
|
212
240
|
if (!results.openai.found && process.env.OPENAI_API_KEY) {
|
|
213
241
|
results.openai.found = true;
|
|
@@ -218,6 +246,105 @@ async function detectAuth() {
|
|
|
218
246
|
return results;
|
|
219
247
|
}
|
|
220
248
|
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// API key storage (.dualbrain/auth.json)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
const AUTH_FILE = (cwd) => join(cwd || process.cwd(), '.dualbrain', 'auth.json');
|
|
254
|
+
|
|
255
|
+
function loadAuthKeys(cwd) {
|
|
256
|
+
try {
|
|
257
|
+
return JSON.parse(readFileSync(AUTH_FILE(cwd), 'utf8'));
|
|
258
|
+
} catch {
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function saveAuthKey(provider, key, opts = {}) {
|
|
264
|
+
const cwd = opts.cwd || process.cwd();
|
|
265
|
+
const authFile = AUTH_FILE(cwd);
|
|
266
|
+
const dir = dirname(authFile);
|
|
267
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
268
|
+
|
|
269
|
+
const auth = loadAuthKeys(cwd);
|
|
270
|
+
auth[provider] = {
|
|
271
|
+
key,
|
|
272
|
+
savedAt: new Date().toISOString(),
|
|
273
|
+
expiresAt: opts.expiresAt || null,
|
|
274
|
+
};
|
|
275
|
+
writeFileSync(authFile, JSON.stringify(auth, null, 2));
|
|
276
|
+
|
|
277
|
+
// Inject into process.env for this session so dispatch can use it
|
|
278
|
+
if (provider === 'claude') process.env.ANTHROPIC_API_KEY = key;
|
|
279
|
+
if (provider === 'openai') process.env.OPENAI_API_KEY = key;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Interactive setup flow: walks user through entering API keys for missing providers.
|
|
284
|
+
* Accepts an existing readline Interface (rl) — does NOT close it.
|
|
285
|
+
* @param {import('readline').Interface} rl
|
|
286
|
+
*/
|
|
287
|
+
async function setupAuth(rl) {
|
|
288
|
+
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
289
|
+
const auth = await detectAuth();
|
|
290
|
+
|
|
291
|
+
// Claude setup
|
|
292
|
+
if (!auth.claude.found) {
|
|
293
|
+
console.log('\n— Claude Setup —');
|
|
294
|
+
console.log('Options:');
|
|
295
|
+
console.log(' (1) Paste API key (recommended for Replit)');
|
|
296
|
+
console.log(' (2) Skip for now');
|
|
297
|
+
const choice = (await ask('> ')).trim();
|
|
298
|
+
if (choice === '1') {
|
|
299
|
+
const key = (await ask('Paste your Anthropic API key: ')).trim();
|
|
300
|
+
if (key && (key.startsWith('sk-ant-') || key.startsWith('sk-'))) {
|
|
301
|
+
const expiryStr = (await ask('Set key expiry? (enter days, or press Enter to skip)\n> ')).trim();
|
|
302
|
+
let expiresAt = null;
|
|
303
|
+
if (expiryStr && /^\d+$/.test(expiryStr)) {
|
|
304
|
+
const d = new Date();
|
|
305
|
+
d.setDate(d.getDate() + parseInt(expiryStr, 10));
|
|
306
|
+
expiresAt = d.toISOString();
|
|
307
|
+
console.log(`✓ Key expires in ${expiryStr} days (${d.toISOString().slice(0, 10)})`);
|
|
308
|
+
}
|
|
309
|
+
saveAuthKey('claude', key, { expiresAt });
|
|
310
|
+
console.log('✓ Claude API key saved');
|
|
311
|
+
} else {
|
|
312
|
+
console.log('Invalid key format. Expected sk-ant-... or sk-...');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
console.log(`\n✓ Claude: already configured via ${auth.claude.source}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// OpenAI setup
|
|
320
|
+
if (!auth.openai.found) {
|
|
321
|
+
console.log('\n— OpenAI Setup —');
|
|
322
|
+
console.log('Options:');
|
|
323
|
+
console.log(' (1) Paste API key (recommended for Replit)');
|
|
324
|
+
console.log(' (2) Skip for now');
|
|
325
|
+
const choice = (await ask('> ')).trim();
|
|
326
|
+
if (choice === '1') {
|
|
327
|
+
const key = (await ask('Paste your OpenAI API key: ')).trim();
|
|
328
|
+
if (key && key.startsWith('sk-')) {
|
|
329
|
+
const expiryStr = (await ask('Set key expiry? (enter days, or press Enter to skip)\n> ')).trim();
|
|
330
|
+
let expiresAt = null;
|
|
331
|
+
if (expiryStr && /^\d+$/.test(expiryStr)) {
|
|
332
|
+
const d = new Date();
|
|
333
|
+
d.setDate(d.getDate() + parseInt(expiryStr, 10));
|
|
334
|
+
expiresAt = d.toISOString();
|
|
335
|
+
console.log(`✓ Key expires in ${expiryStr} days (${d.toISOString().slice(0, 10)})`);
|
|
336
|
+
}
|
|
337
|
+
saveAuthKey('openai', key, { expiresAt });
|
|
338
|
+
console.log('✓ OpenAI API key saved');
|
|
339
|
+
} else {
|
|
340
|
+
console.log('Invalid key format. Expected sk-...');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
console.log(`\n✓ OpenAI: already configured via ${auth.openai.source}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
221
348
|
// ---------------------------------------------------------------------------
|
|
222
349
|
// Auto-detect subscription plans from provider config files
|
|
223
350
|
// ---------------------------------------------------------------------------
|
|
@@ -414,7 +541,10 @@ function saveProfile(profile, opts = {}) {
|
|
|
414
541
|
async function runOnboarding(opts = {}) {
|
|
415
542
|
if (!opts.interactive) return defaultProfile();
|
|
416
543
|
|
|
417
|
-
|
|
544
|
+
// Accept an externally-provided readline instance (shared with REPL/auth setup)
|
|
545
|
+
// or create one internally if not provided. Only close if we created it.
|
|
546
|
+
const rlProvided = !!opts.rl;
|
|
547
|
+
const rl = opts.rl || createInterface({ input: process.stdin, output: process.stdout });
|
|
418
548
|
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
419
549
|
const profile = defaultProfile();
|
|
420
550
|
|
|
@@ -440,7 +570,8 @@ async function runOnboarding(opts = {}) {
|
|
|
440
570
|
profile.mode = n >= 2 ? 'dual' : profile.providers.claude.enabled ? 'solo-claude' : 'solo-openai';
|
|
441
571
|
process.stdout.write('\nProfile saved.\n');
|
|
442
572
|
} finally {
|
|
443
|
-
rl
|
|
573
|
+
// Only close if we created the rl instance (not if it was passed in)
|
|
574
|
+
if (!rlProvided) rl.close();
|
|
444
575
|
}
|
|
445
576
|
return profile;
|
|
446
577
|
}
|
|
@@ -582,4 +713,5 @@ export {
|
|
|
582
713
|
getAvailableProviders, isSoloBrain, getHeadModel,
|
|
583
714
|
detectPlans, syncPreferencesToMemory,
|
|
584
715
|
detectAuth, detectEnvironment,
|
|
716
|
+
setupAuth, saveAuthKey, loadAuthKeys,
|
|
585
717
|
};
|