dual-brain 0.1.11 → 0.1.13
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 +167 -167
- package/package.json +1 -1
- package/src/decide.mjs +2 -3
- package/src/profile.mjs +4 -4
package/bin/dual-brain.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// dual-brain — CLI entry point. Commands: init, go, status, remember, forget
|
|
2
|
+
// dual-brain — CLI entry point. Commands: init, go, think, review, status, remember, forget
|
|
3
3
|
|
|
4
4
|
import { appendFileSync, existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync, unlinkSync, watch as fsWatch } from 'node:fs';
|
|
5
5
|
import { join, dirname, basename, extname } from 'node:path';
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
rememberPreference, forgetPreference, getActivePreferences,
|
|
13
13
|
getAvailableProviders, isSoloBrain, getHeadModel,
|
|
14
14
|
detectAuth, detectEnvironment, detectPlans,
|
|
15
|
+
detectCapabilities,
|
|
15
16
|
saveSubscription, listSubscriptions,
|
|
16
17
|
autoSetup,
|
|
17
18
|
} from '../src/profile.mjs';
|
|
@@ -28,6 +29,8 @@ import {
|
|
|
28
29
|
|
|
29
30
|
import { dispatch, detectRuntime, dispatchDualBrain } from '../src/dispatch.mjs';
|
|
30
31
|
|
|
32
|
+
import { runPipeline, buildExecutionPlan, formatExecutionPlan } from '../src/pipeline.mjs';
|
|
33
|
+
|
|
31
34
|
import { loadRepoCache } from '../src/repo.mjs';
|
|
32
35
|
import { loadSession, saveSession, formatSessionCard, importReplitSessions, getSessionMeta, saveSessionMeta, renameSession, pinSession, unpinSession, categorizeSession, enrichSessions, archiveSession, getArchivedSessions } from '../src/session.mjs';
|
|
33
36
|
|
|
@@ -156,7 +159,7 @@ dual-brain <command> [options]
|
|
|
156
159
|
|
|
157
160
|
Commands:
|
|
158
161
|
init First-time setup → flows into interactive REPL
|
|
159
|
-
auth Show
|
|
162
|
+
auth Show provider login and plan status
|
|
160
163
|
install Install Claude Code hooks into the current project
|
|
161
164
|
go "task description" Detect → decide → dispatch a task
|
|
162
165
|
--dry-run Show routing decision without executing
|
|
@@ -220,7 +223,7 @@ function detectReplitTools(cwd) {
|
|
|
220
223
|
// ─── Subscription status table ────────────────────────────────────────────────
|
|
221
224
|
|
|
222
225
|
/**
|
|
223
|
-
* Print a
|
|
226
|
+
* Print a provider status table to stdout.
|
|
224
227
|
*/
|
|
225
228
|
function printSubscriptionTable(auth, profile) {
|
|
226
229
|
const W = 55;
|
|
@@ -234,10 +237,10 @@ function printSubscriptionTable(auth, profile) {
|
|
|
234
237
|
const openaiSub = profile?.providers?.openai;
|
|
235
238
|
|
|
236
239
|
const claudePlanLabel = claudeSub?.enabled
|
|
237
|
-
? ({ pro: 'Pro
|
|
240
|
+
? ({ pro: 'Pro', max5: 'Max x5', max20: 'Max x20', '$20': 'Pro', '$100': 'Max x5', '$200': 'Max x20' }[claudeSub.plan] ?? claudeSub.plan) // doctor:verified — config value lookup
|
|
238
241
|
: 'disabled';
|
|
239
242
|
const openaiPlanLabel = openaiSub?.enabled
|
|
240
|
-
? ({ plus: 'Plus
|
|
243
|
+
? ({ plus: 'Plus', pro: 'Pro', pro100: 'Pro', pro200: 'Pro (higher limits)', '$20': 'Plus', '$100': 'Pro', '$200': 'Pro (higher limits)' }[openaiSub.plan] ?? openaiSub.plan) // doctor:verified — config value lookup
|
|
241
244
|
: 'disabled';
|
|
242
245
|
|
|
243
246
|
const claudeLabel = claudeSub?.label ? ` [${claudeSub.label}]` : '';
|
|
@@ -254,7 +257,7 @@ function printSubscriptionTable(auth, profile) {
|
|
|
254
257
|
const openaiLine2 = ` plan: ${openaiPlanLabel}${openaiLabel}`;
|
|
255
258
|
|
|
256
259
|
console.log(`╔${hbar}╗`);
|
|
257
|
-
console.log(`║${pad('
|
|
260
|
+
console.log(`║${pad(' Provider Status')}║`);
|
|
258
261
|
console.log(`╠${hbar}╣`);
|
|
259
262
|
console.log(`║${pad(claudeLine1)}║`);
|
|
260
263
|
console.log(`║${pad(claudeLine2)}║`);
|
|
@@ -299,7 +302,7 @@ async function cmdInit(rl) {
|
|
|
299
302
|
}
|
|
300
303
|
|
|
301
304
|
/**
|
|
302
|
-
* Show
|
|
305
|
+
* Show provider login and plan status.
|
|
303
306
|
*/
|
|
304
307
|
async function cmdAuth(subArgs = []) {
|
|
305
308
|
const auth = await detectAuth();
|
|
@@ -325,89 +328,45 @@ async function cmdGo(args) {
|
|
|
325
328
|
const prompt = args.find(a => !a.startsWith('--') && !a.startsWith('-') && a !== (filesRaw ?? ''));
|
|
326
329
|
if (!prompt) err('Usage: dual-brain go "task description" [--dry-run] [--files a,b] [--verbose]');
|
|
327
330
|
|
|
328
|
-
const cwd
|
|
329
|
-
|
|
330
|
-
const detection = detectTask({ prompt, files });
|
|
331
|
-
|
|
332
|
-
// Print the one-sentence classification
|
|
333
|
-
console.log(detection.explanation);
|
|
334
|
-
|
|
335
|
-
// Verbose: emit detection trace before routing decision
|
|
336
|
-
if (verbose) {
|
|
337
|
-
vtrace(`Intent: ${detection.intent} | Risk: ${detection.risk} | Complexity: ${detection.complexity} | Effort: ${detection.effort ?? 'n/a'}`);
|
|
338
|
-
vtrace(`Tier: ${detection.tier} | Files: ${detection.fileCount ?? files.length} | Requires write: ${detection.requiresWrite}`);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Verbose: emit provider health scores before dispatch
|
|
342
|
-
if (verbose) {
|
|
343
|
-
const providers = getAvailableProviders(profile);
|
|
344
|
-
const { states } = getHealth(cwd);
|
|
345
|
-
const providerScores = ['claude', 'openai'].map(name => {
|
|
346
|
-
const enabled = providers.some(p => p.name === name);
|
|
347
|
-
if (!enabled) return `${name}=unavailable`;
|
|
348
|
-
// Find any state entry for this provider
|
|
349
|
-
const statuses = Object.entries(states)
|
|
350
|
-
.filter(([k]) => k.startsWith(`${name}:`))
|
|
351
|
-
.map(([, v]) => v.status);
|
|
352
|
-
const worst = statuses.includes('hot') ? 'hot'
|
|
353
|
-
: statuses.includes('probing') ? 'probing'
|
|
354
|
-
: statuses.includes('degraded') ? 'degraded'
|
|
355
|
-
: 'healthy';
|
|
356
|
-
return `${name}=${worst}`;
|
|
357
|
-
}).join(' ');
|
|
358
|
-
vtrace(`Provider health: ${providerScores}`);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const decision = decideRoute({ profile, detection, cwd });
|
|
331
|
+
const cwd = process.cwd();
|
|
332
|
+
await ensureProfile(cwd);
|
|
362
333
|
|
|
363
|
-
|
|
364
|
-
if (verbose) {
|
|
365
|
-
const modelLabel = decision.effort ? `${decision.model} (${decision.effort})` : decision.model;
|
|
366
|
-
const modelStatus = getAvailableModels(profile)[decision.provider]?.includes(decision.model)
|
|
367
|
-
? 'available, matches tier'
|
|
368
|
-
: 'selected';
|
|
369
|
-
vtrace(`Model selection: ${modelLabel} (${modelStatus})`);
|
|
370
|
-
vtrace(`Dual-brain: ${decision.dualBrain ? 'yes' : 'no'} (${isSoloBrain(profile) ? 'solo provider' : 'dual provider'}, ${detection.risk} risk)`);
|
|
371
|
-
}
|
|
334
|
+
if (verbose) console.log('\nDispatching...');
|
|
372
335
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
console.log(` reason : ${decision.explanation}`);
|
|
380
|
-
}
|
|
336
|
+
const { plan, result } = await runPipeline('go', prompt, {
|
|
337
|
+
files,
|
|
338
|
+
cwd,
|
|
339
|
+
verbose,
|
|
340
|
+
dryRun,
|
|
341
|
+
});
|
|
381
342
|
|
|
382
343
|
if (dryRun) {
|
|
344
|
+
// formatExecutionPlan already printed by pipeline when verbose/dryRun=true
|
|
383
345
|
console.log('\n(dry-run — not executing)');
|
|
384
346
|
return;
|
|
385
347
|
}
|
|
386
348
|
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
349
|
+
if (!result) return;
|
|
350
|
+
|
|
351
|
+
// Display result — dual-brain vs single-provider
|
|
352
|
+
if (result.consensus) {
|
|
391
353
|
console.log(`\nConsensus: ${result.consensus}`);
|
|
392
354
|
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
393
355
|
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
394
|
-
// Save session state
|
|
395
356
|
saveSession({
|
|
396
357
|
objective: prompt,
|
|
397
358
|
branch: null,
|
|
398
359
|
filesChanged: files,
|
|
399
360
|
commandsRun: [`dual-brain go "${prompt}"`],
|
|
400
361
|
lastResult: { status: 'success', summary: result.consensus || 'dual-brain complete' },
|
|
401
|
-
provider:
|
|
362
|
+
provider: plan?._decision?.provider ?? 'claude',
|
|
402
363
|
nextAction: null,
|
|
403
364
|
}, cwd);
|
|
404
365
|
} else {
|
|
405
|
-
result = await dispatch({ decision, prompt, files, cwd, verbose });
|
|
406
366
|
const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
|
|
407
|
-
console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
367
|
+
console.log(`\n${statusLine}${result.durationMs != null ? ` in ${(result.durationMs / 1000).toFixed(1)}s` : ''}`);
|
|
408
368
|
if (result.summary) console.log(result.summary);
|
|
409
369
|
if (result.error) process.stderr.write(`${result.error}\n`);
|
|
410
|
-
// Save session state regardless of success/failure
|
|
411
370
|
saveSession({
|
|
412
371
|
objective: prompt,
|
|
413
372
|
branch: null,
|
|
@@ -417,7 +376,7 @@ async function cmdGo(args) {
|
|
|
417
376
|
status: result.status === 'completed' ? 'success' : 'failure',
|
|
418
377
|
summary: result.summary || (result.status === 'completed' ? 'completed' : `exit ${result.exitCode}`),
|
|
419
378
|
},
|
|
420
|
-
provider:
|
|
379
|
+
provider: plan?._decision?.provider ?? 'claude',
|
|
421
380
|
nextAction: null,
|
|
422
381
|
}, cwd);
|
|
423
382
|
if (result.status !== 'completed') process.exit(1);
|
|
@@ -425,6 +384,62 @@ async function cmdGo(args) {
|
|
|
425
384
|
}
|
|
426
385
|
}
|
|
427
386
|
|
|
387
|
+
async function cmdThink(args) {
|
|
388
|
+
const question = args.find(a => !a.startsWith('--') && !a.startsWith('-'));
|
|
389
|
+
if (!question) err('Usage: dual-brain think "architecture question or design decision"');
|
|
390
|
+
|
|
391
|
+
const cwd = process.cwd();
|
|
392
|
+
await ensureProfile(cwd);
|
|
393
|
+
|
|
394
|
+
const { result, verification } = await runPipeline('think', question, {
|
|
395
|
+
cwd,
|
|
396
|
+
verbose: true,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
if (!result) return;
|
|
400
|
+
|
|
401
|
+
if (result.consensus) {
|
|
402
|
+
console.log(`\nConsensus: ${result.consensus}`);
|
|
403
|
+
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
404
|
+
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
405
|
+
} else {
|
|
406
|
+
if (result.summary) console.log(`\n${result.summary}`);
|
|
407
|
+
if (result.error) process.stderr.write(`${result.error}\n`);
|
|
408
|
+
if (result.status && result.status !== 'completed') process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (verification && !verification.ok) {
|
|
412
|
+
for (const note of verification.notes) process.stderr.write(` note: ${note}\n`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function cmdReview(_args) {
|
|
417
|
+
const cwd = process.cwd();
|
|
418
|
+
await ensureProfile(cwd);
|
|
419
|
+
|
|
420
|
+
const { result, verification } = await runPipeline('review', 'review current diff', {
|
|
421
|
+
cwd,
|
|
422
|
+
verbose: true,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
if (!result) return;
|
|
426
|
+
|
|
427
|
+
if (result.consensus) {
|
|
428
|
+
console.log(`\nConsensus: ${result.consensus}`);
|
|
429
|
+
if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
|
|
430
|
+
if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
|
|
431
|
+
} else {
|
|
432
|
+
if (result.summary) console.log(`\n${result.summary}`);
|
|
433
|
+
if (result.error) process.stderr.write(`${result.error}\n`);
|
|
434
|
+
if (result.status && result.status !== 'completed') process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (verification && !verification.ok) {
|
|
438
|
+
for (const note of verification.notes) process.stderr.write(` note: ${note}\n`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
|
|
428
443
|
async function cmdStatus(args = []) {
|
|
429
444
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
430
445
|
const cwd = process.cwd();
|
|
@@ -717,21 +732,21 @@ function profileExists(cwd) {
|
|
|
717
732
|
// ─── Plan label helpers ───────────────────────────────────────────────────────
|
|
718
733
|
|
|
719
734
|
const CLAUDE_PLAN_LABELS = {
|
|
720
|
-
pro: 'Pro
|
|
721
|
-
max5: 'Max x5
|
|
722
|
-
max20: 'Max x20
|
|
723
|
-
'$20': 'Pro
|
|
724
|
-
'$100': 'Max x5
|
|
725
|
-
'$200': 'Max x20
|
|
735
|
+
pro: 'Pro',
|
|
736
|
+
max5: 'Max x5',
|
|
737
|
+
max20: 'Max x20',
|
|
738
|
+
'$20': 'Pro', // doctor:verified — backward-compat key for legacy stored plan value
|
|
739
|
+
'$100': 'Max x5', // doctor:verified — backward-compat key for legacy stored plan value
|
|
740
|
+
'$200': 'Max x20', // doctor:verified — backward-compat key for legacy stored plan value
|
|
726
741
|
};
|
|
727
742
|
const OPENAI_PLAN_LABELS = {
|
|
728
|
-
plus: 'Plus
|
|
729
|
-
pro: 'Pro
|
|
730
|
-
pro100: 'Pro
|
|
731
|
-
pro200: 'Pro (
|
|
732
|
-
'$20': 'Plus
|
|
733
|
-
'$100': 'Pro
|
|
734
|
-
'$200': 'Pro (
|
|
743
|
+
plus: 'Plus',
|
|
744
|
+
pro: 'Pro',
|
|
745
|
+
pro100: 'Pro',
|
|
746
|
+
pro200: 'Pro (higher limits)',
|
|
747
|
+
'$20': 'Plus', // doctor:verified — backward-compat key for legacy stored plan value
|
|
748
|
+
'$100': 'Pro', // doctor:verified — backward-compat key for legacy stored plan value
|
|
749
|
+
'$200': 'Pro (higher limits)', // doctor:verified — backward-compat key for legacy stored plan value
|
|
735
750
|
};
|
|
736
751
|
|
|
737
752
|
// ─── Screen: welcomeScreen ────────────────────────────────────────────────────
|
|
@@ -817,7 +832,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
817
832
|
}
|
|
818
833
|
|
|
819
834
|
console.log(' [Enter] Save and go');
|
|
820
|
-
console.log(' [c] Customize
|
|
835
|
+
console.log(' [c] Customize work style');
|
|
821
836
|
if (existingSessions.length > 0) {
|
|
822
837
|
console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from data-tools`);
|
|
823
838
|
}
|
|
@@ -880,10 +895,10 @@ async function welcomeScreen(rl, ask) {
|
|
|
880
895
|
// Claude plan picker
|
|
881
896
|
if (claudeReady) {
|
|
882
897
|
console.log('');
|
|
883
|
-
console.log(separator('Claude
|
|
884
|
-
console.log(' (1) Pro
|
|
885
|
-
console.log(' (2) Max x5
|
|
886
|
-
console.log(' (3) Max x20
|
|
898
|
+
console.log(separator('Claude plan'));
|
|
899
|
+
console.log(' (1) Pro');
|
|
900
|
+
console.log(' (2) Max x5');
|
|
901
|
+
console.log(' (3) Max x20');
|
|
887
902
|
console.log(' (4) Skip');
|
|
888
903
|
const claudeChoice = (await ask('> ')).trim();
|
|
889
904
|
const claudePlanMap = { '1': 'pro', '2': 'max5', '3': 'max20' };
|
|
@@ -900,10 +915,10 @@ async function welcomeScreen(rl, ask) {
|
|
|
900
915
|
// OpenAI plan picker
|
|
901
916
|
if (openaiReady) {
|
|
902
917
|
console.log('');
|
|
903
|
-
console.log(separator('OpenAI
|
|
904
|
-
console.log(' (1) Plus
|
|
905
|
-
console.log(' (2) Pro
|
|
906
|
-
console.log(' (3) Pro (
|
|
918
|
+
console.log(separator('OpenAI plan'));
|
|
919
|
+
console.log(' (1) Plus');
|
|
920
|
+
console.log(' (2) Pro');
|
|
921
|
+
console.log(' (3) Pro (higher limits)');
|
|
907
922
|
console.log(' (4) Skip');
|
|
908
923
|
const openaiChoice = (await ask('> ')).trim();
|
|
909
924
|
const openaiPlanMap = { '1': 'plus', '2': 'pro', '3': 'pro200' };
|
|
@@ -928,8 +943,8 @@ async function welcomeScreen(rl, ask) {
|
|
|
928
943
|
|
|
929
944
|
// Team setup
|
|
930
945
|
console.log('');
|
|
931
|
-
console.log(' Team auth: label
|
|
932
|
-
console.log(' When a
|
|
946
|
+
console.log(' Team auth: label providers and set expiry for auto-refresh.');
|
|
947
|
+
console.log(' When a provider link expires, dual-brain will prompt re-login automatically.');
|
|
933
948
|
console.log('');
|
|
934
949
|
console.log(' [Enter] Skip [t] Set up team auth');
|
|
935
950
|
const teamChoice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
@@ -937,7 +952,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
937
952
|
for (const provider of ['claude', 'openai']) {
|
|
938
953
|
if (!existingProfile.providers[provider]?.enabled) continue;
|
|
939
954
|
const provLabel = provider === 'claude' ? 'Claude' : 'OpenAI';
|
|
940
|
-
const label = (await ask(` ${provLabel} label (e.g. "Josh's
|
|
955
|
+
const label = (await ask(` ${provLabel} label (e.g. "Josh's work account"): `)).trim();
|
|
941
956
|
if (label) existingProfile.providers[provider].label = label;
|
|
942
957
|
const expiry = await askExpiry(ask, provLabel);
|
|
943
958
|
if (expiry) existingProfile.providers[provider].expiresAt = expiry;
|
|
@@ -2423,21 +2438,15 @@ async function settingsScreen(rl, ask) {
|
|
|
2423
2438
|
|
|
2424
2439
|
// ─── Helper: aggregatePlans ───────────────────────────────────────────────────
|
|
2425
2440
|
|
|
2426
|
-
const PLAN_PRICES = {
|
|
2427
|
-
pro: '$20', max5: '$100', max20: '$200',
|
|
2428
|
-
plus: '$20', pro100: '$100', pro200: '$200',
|
|
2429
|
-
};
|
|
2430
|
-
|
|
2431
2441
|
function aggregatePlans(subs) {
|
|
2432
2442
|
if (!subs || subs.length === 0) return '';
|
|
2433
2443
|
const counts = {};
|
|
2434
2444
|
for (const s of subs) {
|
|
2435
|
-
const
|
|
2436
|
-
counts[
|
|
2445
|
+
const label = s.plan || 'unknown';
|
|
2446
|
+
counts[label] = (counts[label] || 0) + 1;
|
|
2437
2447
|
}
|
|
2438
2448
|
return Object.entries(counts)
|
|
2439
|
-
.
|
|
2440
|
-
.map(([price, count]) => `${price}×${count}`)
|
|
2449
|
+
.map(([label, count]) => count > 1 ? `${label}×${count}` : label)
|
|
2441
2450
|
.join(' ');
|
|
2442
2451
|
}
|
|
2443
2452
|
|
|
@@ -2499,13 +2508,13 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2499
2508
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
2500
2509
|
|
|
2501
2510
|
if (choice === '1') {
|
|
2502
|
-
console.log('\n Linking Claude
|
|
2511
|
+
console.log('\n Linking Claude account...');
|
|
2503
2512
|
console.log(' A browser window will open — paste the code below when prompted.\n');
|
|
2504
2513
|
const { spawnSync } = await import('node:child_process');
|
|
2505
2514
|
const r = spawnSync('claude', ['auth', 'login'], { stdio: 'inherit', timeout: 60000 });
|
|
2506
2515
|
if (r.status === 0) {
|
|
2507
2516
|
console.log('\n ✅ Claude linked successfully!\n');
|
|
2508
|
-
const label = (await ask(" Label (e.g. \"Josh's
|
|
2517
|
+
const label = (await ask(" Label (e.g. \"Josh's work account\", or Enter to skip): ")).trim();
|
|
2509
2518
|
const expiry = await askExpiry(ask, 'Claude');
|
|
2510
2519
|
const newPlans = detectPlans();
|
|
2511
2520
|
const plan = newPlans.claude?.plan || 'pro';
|
|
@@ -2527,7 +2536,7 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2527
2536
|
}
|
|
2528
2537
|
|
|
2529
2538
|
if (choice === '2') {
|
|
2530
|
-
console.log('\n Linking Codex
|
|
2539
|
+
console.log('\n Linking Codex account...');
|
|
2531
2540
|
console.log(' A browser window will open — paste the code below when prompted.\n');
|
|
2532
2541
|
const { spawnSync } = await import('node:child_process');
|
|
2533
2542
|
const r = spawnSync('codex', ['login'], { stdio: 'inherit', timeout: 60000 });
|
|
@@ -2565,12 +2574,12 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2565
2574
|
}
|
|
2566
2575
|
|
|
2567
2576
|
if (allSubs.length === 0) {
|
|
2568
|
-
console.log('\n No
|
|
2577
|
+
console.log('\n No linked accounts to remove.\n');
|
|
2569
2578
|
await ask(' Press Enter to continue...');
|
|
2570
2579
|
return { next: 'subscriptions' };
|
|
2571
2580
|
}
|
|
2572
2581
|
|
|
2573
|
-
console.log('\n Remove a
|
|
2582
|
+
console.log('\n Remove a linked account:\n');
|
|
2574
2583
|
allSubs.forEach(({ displayName, sub }, i) => {
|
|
2575
2584
|
const planLabels = displayName === 'Claude' ? CLAUDE_PLAN_LABELS : OPENAI_PLAN_LABELS;
|
|
2576
2585
|
const planLabel = planLabels[sub.plan] ?? sub.plan ?? 'unknown';
|
|
@@ -2617,14 +2626,13 @@ async function subscriptionsScreen(rl, ask) {
|
|
|
2617
2626
|
* @param {object} rl readline interface
|
|
2618
2627
|
* @returns {object|null} profile object to save, or null if cancelled/skipped
|
|
2619
2628
|
*/
|
|
2620
|
-
async function runOnboardingWizard(
|
|
2629
|
+
async function runOnboardingWizard(_detection, cwd, rl) {
|
|
2621
2630
|
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
2622
2631
|
const version = readVersion();
|
|
2623
2632
|
|
|
2624
2633
|
// ── Rounded box helpers (matching mainScreen style) ────────────────────────
|
|
2625
2634
|
const W = 51;
|
|
2626
2635
|
const wTop = ` ┌${'─'.repeat(W)}┐`;
|
|
2627
|
-
const wSep = ` ├${'─'.repeat(W)}┤`;
|
|
2628
2636
|
const wBottom = ` └${'─'.repeat(W)}┘`;
|
|
2629
2637
|
const wPad = (s) => {
|
|
2630
2638
|
const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
@@ -2641,105 +2649,95 @@ async function runOnboardingWizard(detection, cwd, rl) {
|
|
|
2641
2649
|
};
|
|
2642
2650
|
const wRow = (s) => ` │ ${wPad(s)}│`;
|
|
2643
2651
|
|
|
2644
|
-
|
|
2645
|
-
const
|
|
2646
|
-
const
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
let codexAvailable = false;
|
|
2650
|
-
try {
|
|
2651
|
-
const { spawnSync } = await import('node:child_process');
|
|
2652
|
-
const r = spawnSync('which', ['codex'], { encoding: 'utf8' });
|
|
2653
|
-
codexAvailable = r.status === 0;
|
|
2654
|
-
} catch {}
|
|
2652
|
+
// ── Use detectCapabilities for broad detection (env vars, ~/.claude, CLI) ──
|
|
2653
|
+
const caps = await detectCapabilities(cwd);
|
|
2654
|
+
const claudeReady = caps.claude.available;
|
|
2655
|
+
const openaiReady = caps.openai.available;
|
|
2656
|
+
const codexAvailable = caps.codex.available;
|
|
2655
2657
|
|
|
2656
2658
|
// ── Detect replit-tools ────────────────────────────────────────────────────
|
|
2657
2659
|
const rt = detectReplitTools(cwd);
|
|
2658
2660
|
|
|
2659
2661
|
const GREEN = '\x1b[32m✓\x1b[0m';
|
|
2660
2662
|
const RED = '\x1b[31m✗\x1b[0m';
|
|
2663
|
+
const DIM = '\x1b[2m';
|
|
2664
|
+
const RESET = '\x1b[0m';
|
|
2661
2665
|
|
|
2662
2666
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2663
|
-
// Step 1 — Auto-detect capabilities (no
|
|
2667
|
+
// Step 1 — Auto-detect capabilities (instant, no spinner)
|
|
2664
2668
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2665
2669
|
console.log('');
|
|
2666
2670
|
console.log(wTop);
|
|
2667
2671
|
console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
|
|
2668
|
-
console.log(wSep);
|
|
2669
|
-
console.log(wRow('Checking your setup...'));
|
|
2670
|
-
console.log(wSep);
|
|
2671
2672
|
console.log(wRow(claudeReady
|
|
2672
|
-
? `${GREEN} Claude Code
|
|
2673
|
+
? `${GREEN} Claude Code`
|
|
2673
2674
|
: `${RED} Claude Code — not found`));
|
|
2674
2675
|
console.log(wRow(openaiReady
|
|
2675
|
-
? `${GREEN} OpenAI API
|
|
2676
|
-
:
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
: `${RED} Codex CLI — not found`));
|
|
2676
|
+
? `${GREEN} OpenAI API`
|
|
2677
|
+
: codexAvailable
|
|
2678
|
+
? `${GREEN} OpenAI / Codex CLI`
|
|
2679
|
+
: `${DIM}○ OpenAI — not configured${RESET}`));
|
|
2680
2680
|
console.log(wRow(rt.installed
|
|
2681
|
-
? `${GREEN} replit-tools
|
|
2682
|
-
: `${
|
|
2681
|
+
? `${GREEN} replit-tools`
|
|
2682
|
+
: `${DIM}○ replit-tools — not found${RESET}`));
|
|
2683
2683
|
console.log(wBottom);
|
|
2684
2684
|
|
|
2685
|
-
|
|
2685
|
+
// ── Edge cases: communicate honestly, but always let them proceed ──────────
|
|
2686
|
+
console.log('');
|
|
2687
|
+
if (!claudeReady && !openaiReady && !codexAvailable) {
|
|
2688
|
+
console.log(' No AI providers detected — configure OPENAI_API_KEY or use');
|
|
2689
|
+
console.log(' within Claude Code. You can still continue and set up later.');
|
|
2690
|
+
console.log('');
|
|
2691
|
+
} else if (claudeReady && !openaiReady && !codexAvailable) {
|
|
2692
|
+
console.log(` ${DIM}Tip: Add OPENAI_API_KEY for dual-brain collaboration${RESET}`);
|
|
2693
|
+
console.log('');
|
|
2694
|
+
} else if (!claudeReady && (openaiReady || codexAvailable)) {
|
|
2695
|
+
console.log(` ${DIM}Note: Use within Claude Code for full dual-brain${RESET}`);
|
|
2686
2696
|
console.log('');
|
|
2687
|
-
console.log(' No AI provider found. Log in first:');
|
|
2688
|
-
console.log(' claude auth login — for Claude');
|
|
2689
|
-
console.log(' codex login — for OpenAI/Codex');
|
|
2690
|
-
console.log(' Then re-run: dual-brain init\n');
|
|
2691
|
-
return null;
|
|
2692
2697
|
}
|
|
2693
2698
|
|
|
2694
2699
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2695
2700
|
// Step 2 — ONE question: work style
|
|
2696
2701
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2697
|
-
console.log('');
|
|
2698
2702
|
console.log(wTop);
|
|
2699
|
-
console.log(wRow(`🧠 Dual-Brain v${version} — Work Style`));
|
|
2700
|
-
console.log(wSep);
|
|
2701
2703
|
console.log(wRow('How do you want to work?'));
|
|
2702
|
-
console.log(
|
|
2703
|
-
console.log(wRow(' 1
|
|
2704
|
-
console.log(wRow(' 2
|
|
2705
|
-
console.log(wRow('
|
|
2706
|
-
console.log(wRow(' 3. 🔥 Full Power — deep reasoning, dual-brain on everything'));
|
|
2707
|
-
console.log(wRow(' that matters'));
|
|
2708
|
-
console.log(wSep);
|
|
2709
|
-
console.log(wRow('[Enter] Balanced'));
|
|
2704
|
+
console.log(wRow(''));
|
|
2705
|
+
console.log(wRow(' 1 ⚡ Fast — single model, quick tasks, skip reviews'));
|
|
2706
|
+
console.log(wRow(' 2 ⚖️ Balanced — smart routing, reviews on important changes'));
|
|
2707
|
+
console.log(wRow(' 3 🔥 Full Power — deep reasoning, dual-brain when it matters'));
|
|
2710
2708
|
console.log(wBottom);
|
|
2711
2709
|
console.log('');
|
|
2712
2710
|
|
|
2713
|
-
const styleChoice = (await ask(' Choice [
|
|
2714
|
-
const styleMap
|
|
2711
|
+
const styleChoice = (await ask(' Choice [2]: ')).trim();
|
|
2712
|
+
const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
2713
|
+
const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Full Power' };
|
|
2715
2714
|
const chosenBias = styleMap[styleChoice] || 'balanced';
|
|
2715
|
+
const chosenName = styleNames[chosenBias];
|
|
2716
2716
|
|
|
2717
2717
|
// ── Non-blocking note if metered API detected ──────────────────────────────
|
|
2718
|
-
if (openaiReady &&
|
|
2718
|
+
if (openaiReady && caps.openai.metered) {
|
|
2719
|
+
console.log(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}`);
|
|
2719
2720
|
console.log('');
|
|
2720
|
-
console.log(' 💡 OpenAI API detected — will confirm before expensive operations');
|
|
2721
2721
|
}
|
|
2722
2722
|
|
|
2723
2723
|
// ── Done ───────────────────────────────────────────────────────────────────
|
|
2724
|
-
console.log('');
|
|
2725
2724
|
console.log(wTop);
|
|
2726
|
-
console.log(wRow(
|
|
2725
|
+
console.log(wRow(`${GREEN} Ready — ${chosenName} mode`));
|
|
2726
|
+
console.log(wRow(` Type a task to start, or press Enter for dashboard`));
|
|
2727
2727
|
console.log(wBottom);
|
|
2728
2728
|
console.log('');
|
|
2729
2729
|
|
|
2730
2730
|
// ── Build and return the profile object ────────────────────────────────────
|
|
2731
2731
|
const finalProfile = loadProfile(cwd);
|
|
2732
2732
|
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
if (openaiReady) {
|
|
2737
|
-
finalProfile.providers.openai = { enabled: true };
|
|
2738
|
-
}
|
|
2733
|
+
finalProfile.providers.claude = { enabled: claudeReady };
|
|
2734
|
+
finalProfile.providers.openai = { enabled: openaiReady || codexAvailable };
|
|
2735
|
+
finalProfile.apiGuardrail = caps.openai.metered;
|
|
2739
2736
|
|
|
2740
|
-
const enabledCount = [claudeReady, openaiReady].filter(Boolean).length;
|
|
2741
|
-
finalProfile.mode
|
|
2742
|
-
finalProfile.bias
|
|
2737
|
+
const enabledCount = [claudeReady, openaiReady || codexAvailable].filter(Boolean).length;
|
|
2738
|
+
finalProfile.mode = enabledCount >= 2 ? 'dual' : claudeReady ? 'solo-claude' : 'solo-openai';
|
|
2739
|
+
finalProfile.bias = chosenBias;
|
|
2740
|
+
finalProfile.workStyle = chosenBias;
|
|
2743
2741
|
|
|
2744
2742
|
return finalProfile;
|
|
2745
2743
|
}
|
|
@@ -2780,11 +2778,11 @@ async function authScreen(rl, ask) {
|
|
|
2780
2778
|
` plan: ${openaiPlanLabel}${openaiSub?.label ? ` [${openaiSub.label}]` : ''}`,
|
|
2781
2779
|
];
|
|
2782
2780
|
|
|
2783
|
-
console.log(box('
|
|
2781
|
+
console.log(box('Provider Status', authLines));
|
|
2784
2782
|
console.log('');
|
|
2785
2783
|
console.log(menu([
|
|
2786
|
-
{ key: 'a', label: 'Manage
|
|
2787
|
-
{ key: 'b', label: 'Back to dashboard',
|
|
2784
|
+
{ key: 'a', label: 'Manage linked accounts', section: '' },
|
|
2785
|
+
{ key: 'b', label: 'Back to dashboard', section: '' },
|
|
2788
2786
|
]));
|
|
2789
2787
|
console.log('');
|
|
2790
2788
|
|
|
@@ -4383,6 +4381,8 @@ async function main() {
|
|
|
4383
4381
|
return;
|
|
4384
4382
|
}
|
|
4385
4383
|
if (cmd === 'go') { await cmdGo(args.slice(1)); return; }
|
|
4384
|
+
if (cmd === 'think') { await cmdThink(args.slice(1)); return; }
|
|
4385
|
+
if (cmd === 'review') { await cmdReview(args.slice(1)); return; }
|
|
4386
4386
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
4387
4387
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
|
4388
4388
|
if (cmd === 'cool') { cmdCool(args[1]); return; }
|
package/package.json
CHANGED
package/src/decide.mjs
CHANGED
|
@@ -26,8 +26,7 @@ const WORKSPACE = join(__dirname, '..');
|
|
|
26
26
|
/**
|
|
27
27
|
* Work styles control how aggressively the router uses stronger models,
|
|
28
28
|
* challenger (dual-brain) reviews, and checkpoints.
|
|
29
|
-
*
|
|
30
|
-
* regardless of what plan they are on.
|
|
29
|
+
* The user picks a style regardless of provider or plan — no price gating.
|
|
31
30
|
*/
|
|
32
31
|
export const WORK_STYLES = {
|
|
33
32
|
fast: {
|
|
@@ -193,7 +192,7 @@ export function getModelCapabilities(model) {
|
|
|
193
192
|
* Return which models the user can access.
|
|
194
193
|
* All known models are available by default; providers can explicitly restrict
|
|
195
194
|
* via profile.providers.<provider>.models (array of allowed model short names).
|
|
196
|
-
* This does NOT gate on
|
|
195
|
+
* This does NOT gate on price or configured plan — we cannot verify those from here.
|
|
197
196
|
* @param {{ providers?: { claude?: { enabled?: boolean, models?: string[] }, openai?: { enabled?: boolean, models?: string[] } } }} profile
|
|
198
197
|
* @returns {{ claude: string[], openai: string[] }}
|
|
199
198
|
*/
|
package/src/profile.mjs
CHANGED
|
@@ -131,7 +131,7 @@ function detectEnvironment() {
|
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Detect what providers and tools are actually available.
|
|
134
|
-
* Never makes network calls, never claims to know
|
|
134
|
+
* Never makes network calls, never claims to know configured plan or price.
|
|
135
135
|
*
|
|
136
136
|
* @param {string} [cwd]
|
|
137
137
|
* @returns {Promise<{
|
|
@@ -338,7 +338,7 @@ function migrateProfile(profile) {
|
|
|
338
338
|
}
|
|
339
339
|
delete profile.plan;
|
|
340
340
|
delete profile.price;
|
|
341
|
-
delete profile.subscription;
|
|
341
|
+
delete profile.subscription; // doctor:verified — removing legacy field from stored config
|
|
342
342
|
delete profile.budget;
|
|
343
343
|
delete profile.detectedPlan;
|
|
344
344
|
}
|
|
@@ -771,7 +771,7 @@ function detectPlans() {
|
|
|
771
771
|
return { claude: null, openai: null };
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
-
/** @deprecated Plan
|
|
774
|
+
/** @deprecated Plan tracking removed. Use provider enabled flag instead. */
|
|
775
775
|
function saveSubscription(provider, config, cwd) {
|
|
776
776
|
const profile = loadProfile(cwd);
|
|
777
777
|
if (!profile.providers[provider]) profile.providers[provider] = { enabled: true };
|
|
@@ -780,7 +780,7 @@ function saveSubscription(provider, config, cwd) {
|
|
|
780
780
|
return profile;
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
-
/** @deprecated Plan
|
|
783
|
+
/** @deprecated Plan tracking removed. Use getAvailableProviders() instead. */
|
|
784
784
|
function listSubscriptions(cwd) {
|
|
785
785
|
const profile = loadProfile(cwd);
|
|
786
786
|
return profile.providers || {};
|