dual-brain 7.1.5 → 7.1.6

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/dual-brain.mjs +182 -82
  2. package/package.json +1 -1
@@ -65,8 +65,8 @@ Commands:
65
65
  forget "preference" Remove a preference by fuzzy match
66
66
 
67
67
  Interactive mode (entered with no args on a TTY):
68
- Shows dashboard screen with menu-driven navigation.
69
- [s] Status, [p] Profile, [a] Auth, [d] Diagnostics, [q] Exit
68
+ Session manager with recent sessions and routing.
69
+ [n] New session, [c] Continue last, [1-9] Resume, [s] Settings, [q] Exit
70
70
 
71
71
  Options:
72
72
  --version Print version
@@ -580,7 +580,7 @@ async function welcomeScreen(rl, ask) {
580
580
  // Enter or anything else → save and go to dashboard
581
581
  saveProfile(setup.profile, { cwd });
582
582
  await cmdInstall(cwd);
583
- return { next: 'dashboard' };
583
+ return { next: 'main' };
584
584
  }
585
585
  } else {
586
586
  // Not confident — show what's missing before falling through to wizard
@@ -708,109 +708,207 @@ async function welcomeScreen(rl, ask) {
708
708
 
709
709
  await cmdInstall(cwd);
710
710
 
711
- return { next: 'dashboard' };
711
+ return { next: 'main' };
712
712
  }
713
713
 
714
- // ─── Screen: dashboardScreen ──────────────────────────────────────────────────
714
+ // ─── Screen: mainScreen ───────────────────────────────────────────────────────
715
715
 
716
- async function dashboardScreen(rl, ask) {
716
+ async function mainScreen(rl, ask) {
717
717
  const cwd = process.cwd();
718
718
  const version = readVersion();
719
719
  const profile = loadProfile(cwd);
720
720
  const auth = await detectAuth();
721
- const env = detectEnvironment();
722
721
 
723
- // Build status lines for box
724
- // If auth is found but provider is disabled in profile, show warning instead of green
725
- const claudeProviderEnabled = profile?.providers?.claude?.enabled !== false;
726
- const openaiProviderEnabled = profile?.providers?.openai?.enabled !== false;
727
- const claudeStatus = auth.claude.found
728
- ? (claudeProviderEnabled ? `🟢 Claude ${badge('connected')}` : `⚠️ Claude ${badge('warning')} disabled`)
729
- : `🔴 Claude ${badge('missing')}`;
730
- const openaiStatus = auth.openai.found
731
- ? (openaiProviderEnabled ? `🟢 OpenAI ${badge('connected')}` : `⚠️ OpenAI ${badge('warning')} disabled`)
732
- : `🔴 OpenAI ${badge('missing')}`;
733
- const envLabel = env.hasReplitTools ? 'Replit + replit-tools' : env.isReplit ? 'Replit' : 'local';
734
-
735
- // Enforcement check
722
+ const claudePlan = profile?.providers?.claude?.plan ?? 'Pro';
723
+ const openaiPlan = profile?.providers?.openai?.plan ?? 'Plus';
724
+ const claudeStatus = auth.claude.found ? `Claude: ${claudePlan} ✓` : `Claude: missing`;
725
+ const openaiStatus = auth.openai.found ? `OpenAI: ${openaiPlan} ✓` : `OpenAI: missing`;
726
+
727
+ console.log(`\ndual-brain v${version}`);
728
+ console.log(`${claudeStatus} · ${openaiStatus}\n`);
729
+
730
+ const recentSessions = importReplitSessions(cwd).slice(0, 5);
731
+
732
+ if (recentSessions.length > 0) {
733
+ console.log('Recent:');
734
+ recentSessions.forEach((sess, i) => {
735
+ const activeIndicator = sess.isActive ? ' ●' : '';
736
+ console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${sess.name}${activeIndicator}`);
737
+ });
738
+ console.log('');
739
+ }
740
+
741
+ console.log(' [c] Continue last session');
742
+ console.log(' [n] New session');
743
+ if (recentSessions.length > 0) {
744
+ console.log(' [1-9] Resume numbered above');
745
+ }
746
+ console.log(' [d] Switch to data-tools');
747
+ if (!auth.claude.found) console.log(' [j] Login to Claude');
748
+ if (!auth.openai.found) console.log(' [k] Login to Codex');
749
+ console.log(' [s] Settings [q] Exit');
750
+ console.log('');
751
+
752
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
753
+
754
+ if (choice === 'n') { return { next: 'new-session' }; }
755
+
756
+ if (choice === 'c') {
757
+ const sessions = importReplitSessions(cwd);
758
+ if (sessions.length === 0) {
759
+ console.log('\n No recent sessions found.\n');
760
+ await ask(' Press Enter to continue...');
761
+ return { next: 'main' };
762
+ }
763
+ const { spawnSync } = await import('node:child_process');
764
+ console.log(`\n Launching: claude --resume ${sessions[0].id}\n`);
765
+ spawnSync('claude', ['--resume', sessions[0].id], { stdio: 'inherit' });
766
+ return { next: 'main' };
767
+ }
768
+
769
+ const numChoice = parseInt(choice, 10);
770
+ if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
771
+ const sess = recentSessions[numChoice - 1];
772
+ const { spawnSync } = await import('node:child_process');
773
+ console.log(`\n Launching: claude --resume ${sess.id}\n`);
774
+ spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
775
+ return { next: 'main' };
776
+ }
777
+
778
+ if (choice === 'd') {
779
+ const { spawnSync } = await import('node:child_process');
780
+ const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
781
+ if (which.status === 0) {
782
+ spawnSync('claude-menu', { stdio: 'inherit' });
783
+ } else {
784
+ console.log('\n data-tools not found — install with: npm i -g replit-tools\n');
785
+ await ask(' Press Enter to continue...');
786
+ }
787
+ return { next: 'main' };
788
+ }
789
+
790
+ if (choice === 'j') {
791
+ const { spawnSync } = await import('node:child_process');
792
+ spawnSync('claude', ['login'], { stdio: 'inherit' });
793
+ return { next: 'main' };
794
+ }
795
+
796
+ if (choice === 'k') {
797
+ const { spawnSync } = await import('node:child_process');
798
+ spawnSync('codex', ['login'], { stdio: 'inherit' });
799
+ return { next: 'main' };
800
+ }
801
+
802
+ if (choice === 's') { return { next: 'settings' }; }
803
+ if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
804
+
805
+ return { next: 'main' };
806
+ }
807
+
808
+ // ─── Screen: newSessionScreen ─────────────────────────────────────────────────
809
+
810
+ async function newSessionScreen(rl, ask) {
811
+ const cwd = process.cwd();
812
+ const input = (await ask('\n What do you want to do? ')).trim();
813
+ if (!input) { return { next: 'main' }; }
814
+
815
+ const profile = loadProfile(cwd);
816
+ const detection = detectTask({ prompt: input });
817
+ const decision = decideRoute({ profile, detection, cwd });
818
+
819
+ console.log(`\n Routing: ${decision.provider}/${decision.model} (${decision.tier})`);
820
+ console.log(` Reason: ${decision.explanation}\n`);
821
+
822
+ const { spawnSync } = await import('node:child_process');
823
+ if (decision.provider === 'openai') {
824
+ spawnSync('codex', [input], { stdio: 'inherit' });
825
+ } else {
826
+ spawnSync('claude', ['-p', input], { stdio: 'inherit' });
827
+ }
828
+
829
+ return { next: 'main' };
830
+ }
831
+
832
+ // ─── Screen: settingsScreen ───────────────────────────────────────────────────
833
+
834
+ async function settingsScreen(rl, ask) {
835
+ const cwd = process.cwd();
836
+ const profile = loadProfile(cwd);
837
+ const auth = await detectAuth();
838
+
736
839
  let guardCount = 0;
737
840
  try {
738
841
  const settingsFile = join(cwd, '.claude', 'settings.json');
739
842
  if (existsSync(settingsFile)) {
740
843
  const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
741
844
  const preToolUse = settings?.hooks?.PreToolUse ?? [];
742
- const guardCmd = 'node .claude/hooks/head-guard.mjs';
743
- const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
744
- const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
745
- const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
746
- const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
747
- const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
845
+ const guardCmd = 'node .claude/hooks/head-guard.mjs';
846
+ const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
847
+ const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
848
+ const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
849
+ const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
850
+ const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
748
851
  guardCount = [hasEdit, hasWrite, hasBash, hasAgent].filter(Boolean).length;
749
852
  }
750
853
  } catch { /* ignore */ }
751
854
 
752
- const authSummary = (auth.claude.found && auth.openai.found)
753
- ? 'both providers connected'
754
- : auth.claude.found
755
- ? 'Claude connected, OpenAI missing'
756
- : auth.openai.found
757
- ? 'OpenAI connected, Claude missing'
758
- : 'no providers connected';
759
-
760
- const dashLines = [
761
- `${claudeStatus} ${openaiStatus}`,
762
- `🌀 ${envLabel}`,
855
+ const modeLabel = (m) => m === profile.mode ? `${m} (active)` : m;
856
+
857
+ const settingsLines = [
858
+ `Mode:`,
859
+ ` [1] ${modeLabel('cost-saver')}`,
860
+ ` [2] ${modeLabel('balanced')}`,
861
+ ` [3] ${modeLabel('quality-first')}`,
862
+ '',
863
+ `Auth:`,
864
+ ` Claude: ${auth.claude.found ? `connected (${auth.claude.source})` : 'missing'}`,
865
+ ` OpenAI: ${auth.openai.found ? `connected (${auth.openai.source})` : 'missing'}`,
763
866
  '',
764
- `✓ Profile: ${profile.mode} · ${profile.providers?.claude?.enabled && profile.providers?.openai?.enabled ? 'dual' : 'solo'} mode`,
765
- `✓ Enforcement: ${guardCount} guards active`,
766
- `✓ Auth: ${authSummary}`,
867
+ `Enforcement: ${guardCount}/4 guards active`,
767
868
  ];
768
869
 
769
- console.log(box(`🧠 Dual-Brain v${version}`, dashLines));
770
870
  console.log('');
771
-
772
- // ── Recent Sessions (replit-tools import) ──────────────────────────────────
773
- const recentSessions = importReplitSessions(cwd).slice(0, 5);
774
- if (recentSessions.length > 0) {
775
- console.log(separator('Recent Sessions'));
776
- recentSessions.forEach((sess, i) => {
777
- const activeIndicator = sess.isActive ? '●' : ' ';
778
- const promptsLabel = `(${sess.promptCount} prompt${sess.promptCount !== 1 ? 's' : ''})`;
779
- console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${activeIndicator} ${sess.name} ${promptsLabel}`);
780
- });
781
- console.log('');
782
- }
783
-
871
+ console.log(box('Settings', settingsLines));
872
+ console.log('');
784
873
  console.log(menu([
785
- { key: 's', label: 'Status detailed provider info', section: 'Info' },
786
- { key: 'p', label: 'Profile & preferences', section: 'Settings' },
787
- { key: 'a', label: 'Auth management', section: 'Settings' },
788
- { key: 'd', label: 'Diagnostics & repair', section: 'Settings' },
789
- { key: 'q', label: 'Exit to shell', section: '' },
874
+ { key: '1', label: 'Switch to cost-saver', section: 'Mode' },
875
+ { key: '2', label: 'Switch to balanced', section: 'Mode' },
876
+ { key: '3', label: 'Switch to quality-first', section: 'Mode' },
877
+ { key: 'a', label: 'Add API key', section: 'Auth' },
878
+ { key: 'i', label: 'Reinstall hooks', section: 'Enforcement' },
879
+ { key: 'b', label: 'Back', section: '' },
790
880
  ]));
791
881
  console.log('');
792
882
 
793
883
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
794
884
 
795
- // Numeric choice session detail
796
- const numChoice = parseInt(choice, 10);
797
- if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
798
- return { next: 'session-detail', session: recentSessions[numChoice - 1] };
885
+ if (choice === '1' || choice === '2' || choice === '3') {
886
+ const modeMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
887
+ profile.mode = modeMap[choice];
888
+ saveProfile(profile, { cwd });
889
+ console.log(` Mode set to: ${profile.mode}`);
890
+ return { next: 'settings' };
799
891
  }
800
892
 
801
- if (choice === 's') {
802
- await cmdStatus([]);
803
- await ask('\n Press Enter to return to dashboard...');
804
- return { next: 'dashboard' };
893
+ if (choice === 'a') {
894
+ await setupAuth(rl);
895
+ return { next: 'settings' };
805
896
  }
806
897
 
807
- if (choice === 'p') { return { next: 'profile' }; }
808
- if (choice === 'a') { return { next: 'auth' }; }
809
- if (choice === 'd') { return { next: 'diagnostics' }; }
810
- if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
898
+ if (choice === 'i') {
899
+ await cmdInstall();
900
+ return { next: 'settings' };
901
+ }
811
902
 
812
- // Unknown choice stay on dashboard
813
- return { next: 'dashboard' };
903
+ if (choice === 'b' || choice === 'back') { return { next: 'main' }; }
904
+
905
+ return { next: 'settings' };
906
+ }
907
+
908
+ // ─── Screen: dashboardScreen (kept for internal reference, unreachable) ───────
909
+
910
+ async function dashboardScreen(rl, ask) {
911
+ return { next: 'main' };
814
912
  }
815
913
 
816
914
  // ─── Screen: authScreen ───────────────────────────────────────────────────────
@@ -1301,12 +1399,15 @@ async function sessionDetailScreen(rl, ask, ctx = {}) {
1301
1399
  // ─── Screen state machine ─────────────────────────────────────────────────────
1302
1400
 
1303
1401
  const SCREENS = {
1304
- welcome: welcomeScreen,
1305
- dashboard: dashboardScreen,
1306
- auth: authScreen,
1307
- profile: profileScreen,
1308
- diagnostics: diagnosticsScreen,
1309
- repl: replScreen,
1402
+ welcome: welcomeScreen,
1403
+ main: mainScreen,
1404
+ 'new-session': newSessionScreen,
1405
+ settings: settingsScreen,
1406
+ dashboard: dashboardScreen,
1407
+ auth: authScreen,
1408
+ profile: profileScreen,
1409
+ diagnostics: diagnosticsScreen,
1410
+ repl: replScreen,
1310
1411
  'session-detail': sessionDetailScreen,
1311
1412
  };
1312
1413
 
@@ -1326,7 +1427,7 @@ async function runScreens(startScreen = 'dashboard') {
1326
1427
  ctx = result?.session ? { session: result.session } : {};
1327
1428
  } catch (e) {
1328
1429
  console.error(`Error: ${e.message}`);
1329
- current = 'dashboard'; // recover to dashboard on error
1430
+ current = 'main';
1330
1431
  ctx = {};
1331
1432
  }
1332
1433
  }
@@ -1349,8 +1450,7 @@ async function main() {
1349
1450
  if (isInteractive) {
1350
1451
  const cwd = process.cwd();
1351
1452
  if (profileExists(cwd)) {
1352
- // Profile already exists → go straight to dashboard
1353
- await runScreens('dashboard');
1453
+ await runScreens('main');
1354
1454
  } else {
1355
1455
  // First run: welcomeScreen handles auto-setup detection internally,
1356
1456
  // then falls through to manual wizard if needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "7.1.5",
3
+ "version": "7.1.6",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {