dual-brain 0.1.12 → 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.
@@ -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 subscription and login status
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 subscription status table to stdout.
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 ($20/mo)', max5: 'Max x5 ($100/mo)', max20: 'Max x20 ($200/mo)', '$20': 'Pro ($20/mo)', '$100': 'Max x5 ($100/mo)', '$200': 'Max x20 ($200/mo)' }[claudeSub.plan] ?? claudeSub.plan)
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 ($20/mo)', pro: 'Pro ($100/mo)', pro100: 'Pro ($100/mo)', pro200: 'Pro ($200/mo)', '$20': 'Plus ($20/mo)', '$100': 'Pro ($100/mo)', '$200': 'Pro ($200/mo)' }[openaiSub.plan] ?? openaiSub.plan)
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(' Subscription Status')}║`);
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 subscription status (replaces old API key auth display).
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 = process.cwd();
329
- const profile = await ensureProfile(cwd);
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
- // Verbose: emit model selection and dual-brain rationale
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
- // Print routing table (only in dry-run or verbose; silent in normal mode)
374
- if (dryRun || verbose) {
375
- console.log(` provider : ${decision.provider}`);
376
- console.log(` model : ${decision.model}${decision.effort ? ' (' + decision.effort + ')' : ''}`);
377
- console.log(` tier : ${decision.tier}`);
378
- console.log(` dual-brain : ${decision.dualBrain ? 'yes' : 'no'}`);
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 (verbose) console.log('\nDispatching...');
388
- let result;
389
- if (decision.dualBrain) {
390
- result = await dispatchDualBrain({ decision, prompt, files, cwd, verbose });
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: decision.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: decision.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 ($20/mo)',
721
- max5: 'Max x5 ($100/mo)',
722
- max20: 'Max x20 ($200/mo)',
723
- '$20': 'Pro ($20/mo)',
724
- '$100': 'Max x5 ($100/mo)',
725
- '$200': 'Max x20 ($200/mo)',
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 ($20/mo)',
729
- pro: 'Pro ($100/mo)',
730
- pro100: 'Pro ($100/mo)',
731
- pro200: 'Pro ($200/mo)',
732
- '$20': 'Plus ($20/mo)',
733
- '$100': 'Pro ($100/mo)',
734
- '$200': 'Pro ($200/mo)',
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 plan tier');
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 subscription'));
884
- console.log(' (1) Pro ($20/mo)');
885
- console.log(' (2) Max x5 ($100/mo)');
886
- console.log(' (3) Max x20 ($200/mo)');
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 subscription'));
904
- console.log(' (1) Plus ($20/mo)');
905
- console.log(' (2) Pro ($100/mo)');
906
- console.log(' (3) Pro ($200/mo higher limits)');
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 subscriptions and set expiry for auto-refresh.');
932
- console.log(' When a subscription expires, dual-brain will prompt re-login automatically.');
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 $100 sub"): `)).trim();
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 price = PLAN_PRICES[s.plan] || s.plan;
2436
- counts[price] = (counts[price] || 0) + 1;
2445
+ const label = s.plan || 'unknown';
2446
+ counts[label] = (counts[label] || 0) + 1;
2437
2447
  }
2438
2448
  return Object.entries(counts)
2439
- .sort((a, b) => parseInt(b[0].slice(1)) - parseInt(a[0].slice(1)))
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 subscription...');
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 $100 sub\", or Enter to skip): ")).trim();
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 subscription...');
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 subscriptions to remove.\n');
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 subscription:\n');
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(detection, cwd, rl) {
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
- const { auth, existingSessions } = detection;
2645
- const claudeReady = auth.claude.found;
2646
- const openaiReady = auth.openai.found;
2647
-
2648
- // ── Detect Codex CLI availability ─────────────────────────────────────────
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 user input)
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 — available`
2673
+ ? `${GREEN} Claude Code`
2673
2674
  : `${RED} Claude Code — not found`));
2674
2675
  console.log(wRow(openaiReady
2675
- ? `${GREEN} OpenAI API — available`
2676
- : `${RED} OpenAI API — not found`));
2677
- console.log(wRow(codexAvailable
2678
- ? `${GREEN} Codex CLI available`
2679
- : `${RED} Codex CLI — not found`));
2676
+ ? `${GREEN} OpenAI API`
2677
+ : codexAvailable
2678
+ ? `${GREEN} OpenAI / Codex CLI`
2679
+ : `${DIM} OpenAInot configured${RESET}`));
2680
2680
  console.log(wRow(rt.installed
2681
- ? `${GREEN} replit-tools — available`
2682
- : `${RED} replit-tools — not found`));
2681
+ ? `${GREEN} replit-tools`
2682
+ : `${DIM} replit-tools — not found${RESET}`));
2683
2683
  console.log(wBottom);
2684
2684
 
2685
- if (!claudeReady && !openaiReady) {
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(wSep);
2703
- console.log(wRow(' 1. ⚡ Fast quick answers, single model, skip reviews'));
2704
- console.log(wRow(' 2. ⚖️ Balanced — smart routing, reviews on important changes'));
2705
- console.log(wRow(' (recommended)'));
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 [1/2/3/Enter]: ')).trim();
2714
- const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
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 && !codexAvailable) {
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(`✓ Ready. Type a task or press Enter for dashboard.`));
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
- if (claudeReady) {
2734
- finalProfile.providers.claude = { enabled: true };
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 = enabledCount >= 2 ? chosenBias : claudeReady ? 'solo-claude' : 'solo-openai';
2742
- finalProfile.bias = chosenBias;
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('Subscription Status', authLines));
2781
+ console.log(box('Provider Status', authLines));
2784
2782
  console.log('');
2785
2783
  console.log(menu([
2786
- { key: 'a', label: 'Manage subscriptions', section: '' },
2787
- { key: 'b', label: 'Back to dashboard', section: '' },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
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
- * These replace subscription-tier-based routing the user picks a style
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 subscription price — that was fake knowledge.
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 subscription tier or price.
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/subscription tracking removed. Use provider enabled flag instead. */
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/subscription tracking removed. Use getAvailableProviders() instead. */
783
+ /** @deprecated Plan tracking removed. Use getAvailableProviders() instead. */
784
784
  function listSubscriptions(cwd) {
785
785
  const profile = loadProfile(cwd);
786
786
  return profile.providers || {};