dual-brain 7.1.1 → 7.1.3

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.
@@ -12,6 +12,7 @@ import {
12
12
  rememberPreference, forgetPreference, getActivePreferences,
13
13
  getAvailableProviders, isSoloBrain, getHeadModel,
14
14
  detectAuth, detectEnvironment, setupAuth,
15
+ autoSetup,
15
16
  } from '../src/profile.mjs';
16
17
 
17
18
  import { detectTask } from '../src/detect.mjs';
@@ -27,7 +28,7 @@ import {
27
28
  import { dispatch, detectRuntime, dispatchDualBrain } from '../src/dispatch.mjs';
28
29
 
29
30
  import { loadRepoCache } from '../src/repo.mjs';
30
- import { loadSession, saveSession, formatSessionCard } from '../src/session.mjs';
31
+ import { loadSession, saveSession, formatSessionCard, importReplitSessions } from '../src/session.mjs';
31
32
 
32
33
  import { box, bar, badge, menu, separator } from '../src/tui.mjs';
33
34
 
@@ -485,11 +486,10 @@ function cmdForget(text) {
485
486
 
486
487
  // ─── Screen helpers ───────────────────────────────────────────────────────────
487
488
 
488
- function profileExists() {
489
- const { homedir } = { homedir: () => process.env.HOME || '/root' };
490
- const cwd = process.cwd();
489
+ function profileExists(cwd) {
490
+ const dir = cwd || process.cwd();
491
491
  const globalPath = join(process.env.HOME || '/root', '.config', 'dual-brain', 'profile.json');
492
- const projectPath = join(cwd, '.dualbrain', 'profile.json');
492
+ const projectPath = join(dir, '.dualbrain', 'profile.json');
493
493
  return existsSync(projectPath) || existsSync(globalPath);
494
494
  }
495
495
 
@@ -497,12 +497,66 @@ function profileExists() {
497
497
 
498
498
  async function welcomeScreen(rl, ask) {
499
499
  const version = readVersion();
500
- console.log(box(`🧠 Dual-Brain v${version} — First-Time Setup`, [
501
- 'Let\'s configure your AI providers.',
500
+ const cwd = process.cwd();
501
+
502
+ // --- Try auto-setup first ---
503
+ console.log(box(`🧠 Dual-Brain v${version} — Setup`, [
504
+ 'Detecting environment...',
502
505
  ]));
503
506
  console.log('');
504
507
 
505
- // --- Claude provider selection ---
508
+ const setup = await autoSetup(cwd);
509
+
510
+ if (setup.confident) {
511
+ // Build summary lines for the auto-detected state
512
+ const detectedLines = [
513
+ 'Detecting environment...',
514
+ ...setup.actions.map(a => `✓ ${a}`),
515
+ ...setup.warnings.map(w => `⚠ ${w}`),
516
+ ];
517
+
518
+ const modeLabel = setup.profile.mode === 'dual' ? 'dual mode, balanced'
519
+ : setup.profile.mode === 'solo-claude' ? 'Claude-only mode, balanced'
520
+ : setup.profile.mode === 'solo-openai' ? 'OpenAI-only mode, balanced'
521
+ : `${setup.profile.mode}, balanced`;
522
+
523
+ const readyBox = box(`🧠 Dual-Brain v${version} — Setup`, [
524
+ ...detectedLines,
525
+ '',
526
+ `Ready to go! Auto-configured ${modeLabel}.`,
527
+ ]);
528
+ console.log(readyBox);
529
+ console.log('');
530
+ console.log(' [Enter] Start coding →');
531
+ console.log(' [c] Customize setup');
532
+ console.log(' [a] Auth management');
533
+ console.log('');
534
+
535
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
536
+
537
+ if (choice === 'c') {
538
+ // Fall through to manual wizard below
539
+ } else if (choice === 'a') {
540
+ return { next: 'auth' };
541
+ } else {
542
+ // Enter or anything else → save and go to dashboard
543
+ saveProfile(setup.profile, { cwd });
544
+ return { next: 'dashboard' };
545
+ }
546
+ } else {
547
+ // Not confident — show what's missing before falling through to wizard
548
+ if (setup.warnings.length > 0) {
549
+ console.log(box(`🧠 Dual-Brain v${version} — Setup`, [
550
+ 'Auto-detection incomplete:',
551
+ ...setup.warnings.map(w => ` ✗ ${w}`),
552
+ '',
553
+ 'Let\'s configure manually.',
554
+ ]));
555
+ console.log('');
556
+ }
557
+ }
558
+
559
+ // --- Manual wizard (fallback or [c] Customize) ---
506
560
  console.log(separator('Claude (Anthropic)'));
507
561
  console.log(' (1) $20/mo Pro');
508
562
  console.log(' (2) $100/mo Max 5x');
@@ -521,7 +575,6 @@ async function welcomeScreen(rl, ask) {
521
575
  // Ask for API key immediately
522
576
  const key = (await ask('Paste your Anthropic API key: ')).trim();
523
577
  if (key) {
524
- const { saveAuthKey } = await import('../src/profile.mjs').then(m => m).catch(() => ({}));
525
578
  // Inline: set env var for this session, profile will persist
526
579
  process.env.ANTHROPIC_API_KEY = key;
527
580
  console.log('✓ Claude API key set for this session');
@@ -578,7 +631,6 @@ async function welcomeScreen(rl, ask) {
578
631
  else if (modeChoice === '3') { mode = 'quality-first'; }
579
632
 
580
633
  // --- Build and save profile ---
581
- const cwd = process.cwd();
582
634
  const existingProfile = loadProfile(cwd);
583
635
  const profile = {
584
636
  ...existingProfile,
@@ -668,29 +720,34 @@ async function dashboardScreen(rl, ask) {
668
720
 
669
721
  console.log(box(`🧠 Dual-Brain v${version}`, dashLines));
670
722
  console.log('');
723
+
724
+ // ── Recent Sessions (replit-tools import) ──────────────────────────────────
725
+ const recentSessions = importReplitSessions(cwd).slice(0, 5);
726
+ if (recentSessions.length > 0) {
727
+ console.log(separator('Recent Sessions'));
728
+ recentSessions.forEach((sess, i) => {
729
+ const activeIndicator = sess.isActive ? '●' : ' ';
730
+ const promptsLabel = `(${sess.promptCount} prompt${sess.promptCount !== 1 ? 's' : ''})`;
731
+ console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${activeIndicator} ${sess.name} ${promptsLabel}`);
732
+ });
733
+ console.log('');
734
+ }
735
+
671
736
  console.log(menu([
672
- { key: 'g', label: 'Godispatch a task', section: 'Actions' },
673
- { key: 's', label: 'Status — detailed provider info', section: 'Actions' },
737
+ { key: 's', label: 'Statusdetailed provider info', section: 'Info' },
674
738
  { key: 'p', label: 'Profile & preferences', section: 'Settings' },
675
739
  { key: 'a', label: 'Auth management', section: 'Settings' },
676
- { key: 'd', label: 'Diagnostics', section: 'Settings' },
677
- { key: 'c', label: 'Command mode (REPL)', section: 'Session' },
678
- { key: 'q', label: 'Exit', section: 'Session' },
740
+ { key: 'd', label: 'Diagnostics & repair', section: 'Settings' },
741
+ { key: 'q', label: 'Exit to shell', section: '' },
679
742
  ]));
680
743
  console.log('');
681
744
 
682
745
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
683
746
 
684
- if (choice === 'g') {
685
- const taskDesc = (await ask(' Task description: ')).trim();
686
- if (taskDesc) {
687
- try {
688
- await cmdGo([taskDesc]);
689
- } catch (e) {
690
- console.error(`Error: ${e.message}`);
691
- }
692
- }
693
- return { next: 'dashboard' };
747
+ // Numeric choice session detail
748
+ const numChoice = parseInt(choice, 10);
749
+ if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
750
+ return { next: 'session-detail', session: recentSessions[numChoice - 1] };
694
751
  }
695
752
 
696
753
  if (choice === 's') {
@@ -702,7 +759,6 @@ async function dashboardScreen(rl, ask) {
702
759
  if (choice === 'p') { return { next: 'profile' }; }
703
760
  if (choice === 'a') { return { next: 'auth' }; }
704
761
  if (choice === 'd') { return { next: 'diagnostics' }; }
705
- if (choice === 'c') { return { next: 'repl' }; }
706
762
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
707
763
 
708
764
  // Unknown choice — stay on dashboard
@@ -827,77 +883,223 @@ async function profileScreen(rl, ask) {
827
883
 
828
884
  async function diagnosticsScreen(rl, ask) {
829
885
  const cwd = process.cwd();
886
+ const { spawnSync: _spawnSync } = await import('child_process');
887
+ const { readdirSync } = await import('node:fs');
888
+ const { detectPlans } = await import('../src/profile.mjs');
889
+
890
+ // ── Version info ──────────────────────────────────────────────────────────
830
891
  const version = readVersion();
831
- const env = detectEnvironment();
832
- const rt = await detectRuntime();
892
+ const nodeVersion = process.version;
893
+
894
+ // ── Provider health ───────────────────────────────────────────────────────
895
+ const auth = await detectAuth();
896
+ const plans = detectPlans();
897
+ const { states: healthStates } = getHealth(cwd);
898
+
899
+ function _providerBadge(name) {
900
+ const entries = Object.entries(healthStates).filter(([k]) => k.startsWith(`${name}:`));
901
+ if (entries.length === 0) return '✅ healthy';
902
+ const statuses = entries.map(([, v]) => v.status);
903
+ if (statuses.includes('hot')) return '🔴 hot';
904
+ if (statuses.includes('degraded')) return '⚠️ degraded';
905
+ if (statuses.includes('probing')) return '⚠️ probing';
906
+ return '✅ healthy';
907
+ }
908
+
909
+ const claudeStatus = auth.claude.found ? _providerBadge('claude') : '❌ no auth';
910
+ const openaiStatus = auth.openai.found ? _providerBadge('openai') : '❌ no auth';
911
+ const claudePlanStr = plans.claude ? `Max ${plans.claude}` : (auth.claude.masked ?? 'unknown');
912
+ const openaiPlanStr = plans.openai ? `Pro ${plans.openai}` : (auth.openai.masked ?? 'unknown');
913
+ const claudeAuthStr = auth.claude.masked ?? 'not configured';
914
+ const openaiAuthStr = auth.openai.masked ?? 'not configured';
915
+
916
+ // ── Enforcement checks ────────────────────────────────────────────────────
917
+ const hooksDir = join(cwd, '.claude', 'hooks');
918
+ const headGuardExists = existsSync(join(hooksDir, 'head-guard.mjs'));
919
+ const enforceTierExists = existsSync(join(hooksDir, 'enforce-tier.mjs'));
833
920
 
834
- // Enforcement check
835
921
  let guardCount = 0;
836
- let guardDetails = 'NOT INSTALLED';
837
922
  try {
838
923
  const settingsFile = join(cwd, '.claude', 'settings.json');
839
924
  if (existsSync(settingsFile)) {
840
925
  const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
841
926
  const preToolUse = settings?.hooks?.PreToolUse ?? [];
842
- const guardCmd = 'node .claude/hooks/head-guard.mjs';
843
- const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
844
- const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
845
- const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
846
- const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
847
- const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
927
+ const guardCmd = 'node .claude/hooks/head-guard.mjs';
928
+ const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
929
+ const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
930
+ const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
931
+ const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
932
+ const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
848
933
  guardCount = [hasEdit, hasWrite, hasBash, hasAgent].filter(Boolean).length;
849
- guardDetails = guardCount === 4
850
- ? `${guardCount}/4 guards active (Edit, Write, Bash, Agent)`
851
- : `${guardCount}/4 guards — run: dual-brain install`;
852
934
  }
853
- } catch { guardDetails = 'unknown (could not read settings)'; }
935
+ } catch { /* ignore */ }
854
936
 
855
- // Hook health: check if hook files exist
856
- const hooksDir = join(cwd, '.claude', 'hooks');
857
- const expectedHooks = [
858
- 'head-guard.mjs', 'enforce-tier.mjs', 'budget-balancer.mjs',
859
- 'session-report.mjs', 'quality-gate.mjs', 'health-check.mjs',
860
- ];
861
- const hookStatus = expectedHooks.map(h => {
862
- const present = existsSync(join(hooksDir, h));
863
- return ` ${present ? '✓' : '✗'} ${h}`;
864
- });
865
-
866
- const diagLines = [
867
- `Version: ${version}`,
868
- `Enforcement: ${guardDetails}`,
937
+ let hookifyCount = 0;
938
+ try {
939
+ const claudeDir = join(cwd, '.claude');
940
+ if (existsSync(claudeDir)) {
941
+ hookifyCount = readdirSync(claudeDir).filter(f => f.startsWith('hookify.') && f.endsWith('.md')).length;
942
+ }
943
+ } catch { /* ignore */ }
944
+
945
+ // ── Replit-tools integration ──────────────────────────────────────────────
946
+ const replitToolsDir = join(cwd, '.replit-tools');
947
+ const hasReplitTools = existsSync(replitToolsDir);
948
+ const persistentDir = join(replitToolsDir, '.claude-persistent');
949
+ const sessionManagerExists = existsSync(join(replitToolsDir, 'scripts', 'claude-session-manager.sh'));
950
+ const authRefreshScript = join(replitToolsDir, 'scripts', 'claude-auth-refresh.sh');
951
+
952
+ let credsFresh = null;
953
+ let credsExpiry = null;
954
+ let historyCount = 0;
955
+
956
+ if (hasReplitTools) {
957
+ try {
958
+ const credsFile = join(persistentDir, '.credentials.json');
959
+ const creds = JSON.parse(readFileSync(credsFile, 'utf8'));
960
+ const expiresAt = creds?.claudeAiOauth?.expiresAt;
961
+ if (expiresAt) {
962
+ const expiresMs = typeof expiresAt === 'number' ? expiresAt : Date.parse(expiresAt);
963
+ credsFresh = Date.now() < expiresMs;
964
+ credsExpiry = new Date(expiresMs).toISOString().slice(0, 10);
965
+ }
966
+ } catch { /* credentials missing or unreadable */ }
967
+
968
+ try {
969
+ const histFile = join(persistentDir, 'history.jsonl');
970
+ if (existsSync(histFile)) {
971
+ historyCount = readFileSync(histFile, 'utf8').split('\n').filter(Boolean).length;
972
+ }
973
+ } catch { /* ignore */ }
974
+ }
975
+
976
+ // ── Quality checks ────────────────────────────────────────────────────────
977
+ let testPass = null; let testTotal = null; let testError = null;
978
+ try {
979
+ const r = _spawnSync('node', ['--test', 'src/test.mjs'], { cwd, encoding: 'utf8', timeout: 30000 });
980
+ const out = (r.stdout ?? '') + (r.stderr ?? '');
981
+ const pm = out.match(/# pass (\d+)/);
982
+ const tm = out.match(/# tests (\d+)/);
983
+ if (pm && tm) { testPass = parseInt(pm[1], 10); testTotal = parseInt(tm[1], 10); }
984
+ else { testError = 'could not parse output'; }
985
+ } catch (e) { testError = e.message; }
986
+
987
+ let healthPass = null; let healthTotal = null; let healthError = null;
988
+ try {
989
+ const healthScript = join(hooksDir, 'health-check.mjs');
990
+ if (existsSync(healthScript)) {
991
+ const r = _spawnSync('node', [healthScript], { cwd, encoding: 'utf8', timeout: 15000 });
992
+ const out = (r.stdout ?? '') + (r.stderr ?? '');
993
+ // Try summary line first: "8 pass, 0 warn, 0 fail"
994
+ const sm = out.match(/(\d+) pass,\s*(\d+) warn,\s*(\d+) fail/);
995
+ if (sm) {
996
+ healthPass = parseInt(sm[1], 10);
997
+ healthTotal = parseInt(sm[1], 10) + parseInt(sm[2], 10) + parseInt(sm[3], 10);
998
+ } else {
999
+ // Fall back to JSON block
1000
+ const jm = out.match(/\{[\s\S]*?"healthy"[\s\S]*?\}/);
1001
+ if (jm) {
1002
+ try {
1003
+ const p = JSON.parse(jm[0]);
1004
+ healthPass = p.pass ?? 0;
1005
+ healthTotal = (p.pass ?? 0) + (p.warn ?? 0) + (p.fail ?? 0);
1006
+ } catch { healthError = 'could not parse output'; }
1007
+ } else { healthError = 'could not parse output'; }
1008
+ }
1009
+ } else { healthError = 'health-check.mjs not found'; }
1010
+ } catch (e) { healthError = e.message; }
1011
+
1012
+ // ── Render ────────────────────────────────────────────────────────────────
1013
+ const W = 56;
1014
+ const hbar = '═'.repeat(W);
1015
+ // Pad a string to exactly W visible columns (for box rows without leading ║ prefix)
1016
+ const padRow = (s) => {
1017
+ const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
1018
+ let vlen = 0;
1019
+ for (const ch of plain) {
1020
+ const cp = ch.codePointAt(0);
1021
+ if ((cp >= 0x1f300 && cp <= 0x1faff) || (cp >= 0x2600 && cp <= 0x27bf) || cp === 0xfe0f || cp === 0x20e3) vlen += 2;
1022
+ else vlen += 1;
1023
+ }
1024
+ return s + ' '.repeat(Math.max(0, W - vlen));
1025
+ };
1026
+ const hrow = (s) => `║${padRow(' ' + s)}║`;
1027
+
1028
+ const output = [
1029
+ `╔${hbar}╗`,
1030
+ hrow('🔧 Diagnostics'),
1031
+ `╠${hbar}╣`,
1032
+ hrow(`dual-brain v${version}`),
1033
+ hrow(`Node.js ${nodeVersion}`),
1034
+ `╚${hbar}╝`,
869
1035
  '',
870
- 'Environment:',
871
- ` Replit: ${env.isReplit ? 'yes' : 'no'}`,
872
- ` replit-tools:${env.hasReplitTools ? 'yes' : 'no'}`,
873
- ` CI: ${env.isCI ? 'yes' : 'no'}`,
1036
+ separator('Provider Health'),
1037
+ ` ${claudeStatus.padEnd(14)} Claude ${claudePlanStr.padEnd(16)} ${claudeAuthStr}`,
1038
+ ` ${openaiStatus.padEnd(14)} OpenAI ${openaiPlanStr.padEnd(16)} ${openaiAuthStr}`,
874
1039
  '',
875
- 'Runtime:',
876
- ` claude CLI: ${rt.claudeAvailable ? 'available' : 'not found'}`,
877
- ` codex CLI: ${rt.codexAvailable ? 'available' : 'not found'}`,
878
- ` runtime: ${rt.runtime}`,
1040
+ separator('Enforcement'),
1041
+ ` ${headGuardExists ? '✅' : '❌'} head-guard.mjs ${headGuardExists ? 'installed' : 'missing — run: dual-brain install'}`,
1042
+ ` ${enforceTierExists ? '✅' : '❌'} enforce-tier.mjs ${enforceTierExists ? 'installed' : 'missing — run: dual-brain install'}`,
1043
+ ` ${guardCount === 4 ? '✅' : '⚠️ '} settings.json ${guardCount}/4 guards registered${guardCount < 4 ? ' — run: dual-brain install' : ''}`,
1044
+ ` ${hookifyCount > 0 ? '✅' : '⚠️ '} hookify rules ${hookifyCount} rules${hookifyCount > 0 ? ' (check: ls .claude/hookify.*.md)' : ' — none found'}`,
879
1045
  '',
880
- 'Hook files:',
881
- ...hookStatus,
1046
+ separator('Replit Tools'),
1047
+ ` ${hasReplitTools ? '✅' : '❌'} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
882
1048
  ];
883
1049
 
884
- console.log(box('Diagnostics', diagLines));
885
- console.log('');
886
- console.log(menu([
887
- { key: 'h', label: 'Run health check', section: '' },
888
- { key: 'i', label: 'Install hooks', section: '' },
889
- { key: 'b', label: 'Back to dashboard', section: '' },
890
- ]));
1050
+ if (hasReplitTools) {
1051
+ if (credsFresh === null) {
1052
+ output.push(' ⚠️ Claude auth credentials file missing');
1053
+ } else if (credsFresh) {
1054
+ output.push(` ✅ Claude auth fresh (expires: ${credsExpiry})`);
1055
+ } else {
1056
+ output.push(` ❌ Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
1057
+ }
1058
+ output.push(` ✅ Session archive ${historyCount} entries`);
1059
+ output.push(` ${sessionManagerExists ? '✅' : '⚠️ '} Session manager ${sessionManagerExists ? 'available' : 'not found'}`);
1060
+ } else {
1061
+ output.push(' ─── (not available)');
1062
+ }
1063
+
1064
+ output.push('');
1065
+ output.push(separator('Quality'));
1066
+ if (testError) {
1067
+ output.push(` ❌ Tests error: ${testError}`);
1068
+ } else if (testPass !== null) {
1069
+ output.push(` ${testPass === testTotal ? '✅' : '❌'} Tests ${testPass}/${testTotal} passing`);
1070
+ }
1071
+ if (healthError) {
1072
+ output.push(` ❌ Health check error: ${healthError}`);
1073
+ } else if (healthPass !== null) {
1074
+ output.push(` ${healthPass === healthTotal ? '✅' : '⚠️ '} Health check ${healthPass}/${healthTotal} passing`);
1075
+ }
1076
+ output.push('');
1077
+
1078
+ console.log(output.join('\n'));
1079
+
1080
+ // Actions menu
1081
+ const menuOpts = [
1082
+ { key: 'h', label: 'Run health check', section: 'Actions' },
1083
+ { key: 't', label: 'Run test suite', section: 'Actions' },
1084
+ ];
1085
+ if (hasReplitTools && existsSync(authRefreshScript)) {
1086
+ menuOpts.push({ key: 'r', label: 'Refresh auth (replit-tools)', section: 'Actions' });
1087
+ }
1088
+ menuOpts.push({ key: 'i', label: 'Reinstall hooks', section: 'Actions' });
1089
+ menuOpts.push({ key: 'b', label: 'Back to dashboard', section: 'Actions' });
1090
+ console.log(menu(menuOpts));
891
1091
  console.log('');
892
1092
 
893
1093
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
894
1094
 
895
1095
  if (choice === 'h') {
896
- const hookScript = join(cwd, '.claude', 'hooks', 'health-check.mjs');
1096
+ const hookScript = join(hooksDir, 'health-check.mjs');
1097
+ console.log('');
897
1098
  if (existsSync(hookScript)) {
898
1099
  try {
899
- execSync(`node "${hookScript}"`, { stdio: 'inherit', cwd });
900
- } catch { /* hook exits non-zero on issues — already printed output */ }
1100
+ const r = _spawnSync('node', [hookScript], { stdio: 'inherit', cwd });
1101
+ if (r.error) console.log(` Error: ${r.error.message}`);
1102
+ } catch (e) { console.log(` Error: ${e.message}`); }
901
1103
  } else {
902
1104
  console.log(' health-check.mjs not found — run: dual-brain install');
903
1105
  }
@@ -905,6 +1107,32 @@ async function diagnosticsScreen(rl, ask) {
905
1107
  return { next: 'diagnostics' };
906
1108
  }
907
1109
 
1110
+ if (choice === 't') {
1111
+ console.log('\n Running test suite...\n');
1112
+ try {
1113
+ const r = _spawnSync('node', ['--test', 'src/test.mjs'], { stdio: 'inherit', cwd, timeout: 60000 });
1114
+ if (r.error) console.log(` Error: ${r.error.message}`);
1115
+ } catch (e) { console.log(` Error: ${e.message}`); }
1116
+ await ask('\n Press Enter to continue...');
1117
+ return { next: 'diagnostics' };
1118
+ }
1119
+
1120
+ if (choice === 'r') {
1121
+ if (existsSync(authRefreshScript)) {
1122
+ console.log('\n Refreshing Claude auth...\n');
1123
+ try {
1124
+ const r = _spawnSync('bash', [authRefreshScript], { stdio: 'inherit', cwd, timeout: 30000 });
1125
+ if (r.error) console.log(` Error: ${r.error.message}`);
1126
+ else if (r.status === 0) console.log('\n Auth refresh complete.');
1127
+ else console.log(`\n Auth refresh exited with code ${r.status}.`);
1128
+ } catch (e) { console.log(` Error: ${e.message}`); }
1129
+ } else {
1130
+ console.log(' claude-auth-refresh.sh not found.');
1131
+ }
1132
+ await ask('\n Press Enter to continue...');
1133
+ return { next: 'diagnostics' };
1134
+ }
1135
+
908
1136
  if (choice === 'i') {
909
1137
  await cmdInstall();
910
1138
  return { next: 'diagnostics' };
@@ -963,15 +1191,75 @@ async function replScreen(rl, ask) {
963
1191
  }
964
1192
  }
965
1193
 
1194
+ // ─── Screen: sessionDetailScreen ─────────────────────────────────────────────
1195
+
1196
+ async function sessionDetailScreen(rl, ask, ctx = {}) {
1197
+ const sess = ctx.session;
1198
+ if (!sess) return { next: 'dashboard' };
1199
+
1200
+ const W = 56;
1201
+ const hbar = '═'.repeat(W + 2);
1202
+ const pad = (s) => {
1203
+ const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
1204
+ return s + ' '.repeat(Math.max(0, W - plain.length));
1205
+ };
1206
+
1207
+ const statusLine = sess.isActive
1208
+ ? `active`
1209
+ : `inactive`;
1210
+
1211
+ const detailLines = [
1212
+ ` Session: ${sess.name}`,
1213
+ `╠${hbar}╣`,
1214
+ ` ID: ${sess.id.slice(0, 8)}...`,
1215
+ ` Status: ${statusLine}`,
1216
+ ` Prompts: ${sess.promptCount}`,
1217
+ ` Last active: ${sess.age}`,
1218
+ ` Project: ${sess.project || process.cwd()}`,
1219
+ ];
1220
+
1221
+ console.log(`╔${hbar}╗`);
1222
+ for (const line of detailLines) {
1223
+ console.log(`║ ${pad(line)}║`);
1224
+ }
1225
+ console.log(`╚${hbar}╝`);
1226
+ console.log('');
1227
+
1228
+ if (sess.isActive) {
1229
+ console.log(' [c] Continue this session (claude --continue)');
1230
+ } else {
1231
+ console.log(' [r] Resume this session (claude --resume)');
1232
+ }
1233
+ console.log(' [b] Back to dashboard');
1234
+ console.log('');
1235
+
1236
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1237
+
1238
+ if (choice === 'c' || choice === 'r') {
1239
+ console.log(`\n Launching: claude --resume ${sess.id}\n`);
1240
+ try {
1241
+ const { spawnSync } = await import('node:child_process');
1242
+ spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
1243
+ } catch {
1244
+ console.log(' Could not launch claude CLI. Run manually:');
1245
+ console.log(` claude --resume ${sess.id}`);
1246
+ }
1247
+ return { next: 'dashboard' };
1248
+ }
1249
+
1250
+ return { next: 'dashboard' };
1251
+ }
1252
+
966
1253
  // ─── Screen state machine ─────────────────────────────────────────────────────
967
1254
 
968
1255
  const SCREENS = {
969
- welcome: welcomeScreen,
970
- dashboard: dashboardScreen,
971
- auth: authScreen,
972
- profile: profileScreen,
973
- diagnostics: diagnosticsScreen,
974
- repl: replScreen,
1256
+ welcome: welcomeScreen,
1257
+ dashboard: dashboardScreen,
1258
+ auth: authScreen,
1259
+ profile: profileScreen,
1260
+ diagnostics: diagnosticsScreen,
1261
+ repl: replScreen,
1262
+ 'session-detail': sessionDetailScreen,
975
1263
  };
976
1264
 
977
1265
  async function runScreens(startScreen = 'dashboard') {
@@ -979,15 +1267,19 @@ async function runScreens(startScreen = 'dashboard') {
979
1267
  const ask = (q) => new Promise(res => rl.question(q, res));
980
1268
 
981
1269
  let current = startScreen;
1270
+ let ctx = {};
982
1271
  while (current && current !== 'exit') {
983
1272
  const screen = SCREENS[current];
984
1273
  if (!screen) break;
985
1274
  try {
986
- const result = await screen(rl, ask);
1275
+ const result = await screen(rl, ask, ctx);
987
1276
  current = result?.next || 'exit';
1277
+ // Pass through context (e.g. selected session) to next screen
1278
+ ctx = result?.session ? { session: result.session } : {};
988
1279
  } catch (e) {
989
1280
  console.error(`Error: ${e.message}`);
990
1281
  current = 'dashboard'; // recover to dashboard on error
1282
+ ctx = {};
991
1283
  }
992
1284
  }
993
1285
  rl.close();
@@ -1007,9 +1299,15 @@ async function main() {
1007
1299
 
1008
1300
  if (!cmd) {
1009
1301
  if (isInteractive) {
1010
- // Check if first-run (no profile) → welcome screen, else dashboard
1011
- const startScreen = profileExists() ? 'dashboard' : 'welcome';
1012
- await runScreens(startScreen);
1302
+ const cwd = process.cwd();
1303
+ if (profileExists(cwd)) {
1304
+ // Profile already exists → go straight to dashboard
1305
+ await runScreens('dashboard');
1306
+ } else {
1307
+ // First run: welcomeScreen handles auto-setup detection internally,
1308
+ // then falls through to manual wizard if needed.
1309
+ await runScreens('welcome');
1310
+ }
1013
1311
  } else {
1014
1312
  // Non-TTY: print status card and exit
1015
1313
  const cwd = process.cwd();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "7.1.1",
3
+ "version": "7.1.3",
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/profile.mjs CHANGED
@@ -876,6 +876,94 @@ if (isMain) main().catch(e => { process.stderr.write(e.message + '\n'); process.
876
876
  // Exports
877
877
  // ---------------------------------------------------------------------------
878
878
 
879
+ // ---------------------------------------------------------------------------
880
+ // Auto-setup (1-click, no user input required)
881
+ // ---------------------------------------------------------------------------
882
+
883
+ /**
884
+ * Attempt to configure a profile entirely from detected state — no user input.
885
+ *
886
+ * Returns:
887
+ * {
888
+ * confident: boolean, // true when at least one provider was found
889
+ * profile: object|null, // fully-built profile ready to save, or null
890
+ * warnings: string[], // non-fatal issues (e.g. missing provider)
891
+ * actions: string[], // human-readable lines for the summary box
892
+ * }
893
+ *
894
+ * IMPORTANT: this function NEVER stores credentials — it only reads what's
895
+ * already present on disk / in environment variables.
896
+ */
897
+ async function autoSetup(cwd) {
898
+ const env = detectEnvironment();
899
+ const auth = await detectAuth();
900
+ const plans = detectPlans();
901
+
902
+ const result = {
903
+ confident: false,
904
+ profile: null,
905
+ warnings: [],
906
+ actions: [],
907
+ };
908
+
909
+ // Need at least one provider authenticated
910
+ if (!auth.claude.found && !auth.openai.found) {
911
+ result.warnings.push('No provider credentials found');
912
+ return result;
913
+ }
914
+
915
+ // Build profile from detected state
916
+ const profile = defaultProfile();
917
+
918
+ // Claude
919
+ if (auth.claude.found) {
920
+ profile.providers.claude.enabled = true;
921
+ profile.providers.claude.plan = plans.claude || '$20';
922
+ const planLabel = {
923
+ '$20': 'Claude Pro ($20)',
924
+ '$100': 'Claude Max x5 ($100)',
925
+ '$200': 'Claude Max x20 ($200)',
926
+ }[profile.providers.claude.plan] || profile.providers.claude.plan;
927
+ result.actions.push(`${planLabel} via ${auth.claude.source}`);
928
+ } else {
929
+ profile.providers.claude.enabled = false;
930
+ result.warnings.push('Claude not authenticated');
931
+ }
932
+
933
+ // OpenAI
934
+ if (auth.openai.found) {
935
+ profile.providers.openai.enabled = true;
936
+ profile.providers.openai.plan = plans.openai || '$20';
937
+ const planLabel = {
938
+ '$20': 'ChatGPT Plus ($20)',
939
+ '$100': 'ChatGPT Pro ($100)',
940
+ '$200': 'ChatGPT Pro ($200)',
941
+ }[profile.providers.openai.plan] || profile.providers.openai.plan;
942
+ result.actions.push(`${planLabel} via ${auth.openai.source}`);
943
+ } else {
944
+ profile.providers.openai.enabled = false;
945
+ result.warnings.push('OpenAI not authenticated');
946
+ }
947
+
948
+ // Mode
949
+ const enabledCount = [auth.claude.found, auth.openai.found].filter(Boolean).length;
950
+ profile.mode = enabledCount >= 2 ? 'dual'
951
+ : auth.claude.found ? 'solo-claude'
952
+ : 'solo-openai';
953
+ profile.bias = 'balanced';
954
+
955
+ // Environment note
956
+ if (env.isReplit && env.hasReplitTools) {
957
+ result.actions.push('Replit + replit-tools detected');
958
+ } else if (env.isReplit) {
959
+ result.actions.push('Replit environment detected');
960
+ }
961
+
962
+ result.confident = true;
963
+ result.profile = profile;
964
+ return result;
965
+ }
966
+
879
967
  export {
880
968
  loadProfile, saveProfile, ensureProfile, runOnboarding,
881
969
  rememberPreference, forgetPreference, getActivePreferences,
@@ -884,4 +972,5 @@ export {
884
972
  detectAuth, detectEnvironment,
885
973
  setupAuth, saveAuthKey, loadAuthKeys,
886
974
  getActiveKey, removeAuthKey, disableKey, rotateToNextKey,
975
+ defaultProfile, autoSetup,
887
976
  };
package/src/session.mjs CHANGED
@@ -10,7 +10,7 @@
10
10
  * formatSessionCard(session, repo, health) → compact status card string (≤5 lines)
11
11
  */
12
12
 
13
- import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, renameSync } from 'node:fs';
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, renameSync, readdirSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
 
16
16
  // ─── Constants ────────────────────────────────────────────────────────────────
@@ -197,6 +197,147 @@ export function formatSessionCard(session, repo, health) {
197
197
  return lines.join('\n');
198
198
  }
199
199
 
200
+ // ─── Replit-tools session import ──────────────────────────────────────────────
201
+
202
+ /**
203
+ * Human-readable time-ago string from a Unix timestamp (ms).
204
+ * @param {number} timestamp
205
+ * @returns {string}
206
+ */
207
+ function timeAgo(timestamp) {
208
+ const diff = Date.now() - timestamp;
209
+ const mins = Math.floor(diff / 60000);
210
+ if (mins < 1) return 'just now';
211
+ if (mins < 60) return `${mins}m ago`;
212
+ const hours = Math.floor(mins / 60);
213
+ if (hours < 24) return `${hours}h ago`;
214
+ const days = Math.floor(hours / 24);
215
+ return `${days}d ago`;
216
+ }
217
+
218
+ /**
219
+ * Import sessions from replit-tools history.jsonl.
220
+ * Returns an array of session summary objects, sorted most-recent first.
221
+ * Returns [] gracefully if replit-tools is not present.
222
+ *
223
+ * @param {string} cwd
224
+ * @returns {Array<{
225
+ * id: string, name: string, project: string,
226
+ * promptCount: number, lastActive: string,
227
+ * isActive: boolean, source: string, age: string
228
+ * }>}
229
+ */
230
+ export function importReplitSessions(cwd = process.cwd()) {
231
+ const sessions = [];
232
+
233
+ // Check multiple possible locations for replit-tools
234
+ const candidates = [
235
+ join(cwd, '.replit-tools', '.claude-persistent'),
236
+ join('/home/runner/workspace', '.replit-tools', '.claude-persistent'),
237
+ ];
238
+ // Deduplicate
239
+ const seen = new Set();
240
+ const replitBases = candidates.filter(p => {
241
+ const norm = p.replace(/\/+$/, '');
242
+ if (seen.has(norm)) return false;
243
+ seen.add(norm);
244
+ return true;
245
+ });
246
+
247
+ let replitBase = null;
248
+ for (const candidate of replitBases) {
249
+ if (existsSync(join(candidate, 'history.jsonl'))) {
250
+ replitBase = candidate;
251
+ break;
252
+ }
253
+ }
254
+ if (!replitBase) return sessions;
255
+
256
+ // Read history.jsonl
257
+ const historyPath = join(replitBase, 'history.jsonl');
258
+
259
+ let lines;
260
+ try {
261
+ lines = readFileSync(historyPath, 'utf8').split('\n').filter(Boolean);
262
+ } catch { return sessions; }
263
+
264
+ const bySession = new Map(); // sessionId → { entries, firstPrompt, lastTimestamp }
265
+
266
+ for (const line of lines) {
267
+ try {
268
+ const entry = JSON.parse(line);
269
+ if (!entry.sessionId) continue;
270
+
271
+ if (!bySession.has(entry.sessionId)) {
272
+ bySession.set(entry.sessionId, {
273
+ sessionId: entry.sessionId,
274
+ project: entry.project,
275
+ entries: [],
276
+ firstPrompt: null,
277
+ lastTimestamp: 0,
278
+ });
279
+ }
280
+
281
+ const sess = bySession.get(entry.sessionId);
282
+ sess.entries.push(entry);
283
+ if (entry.timestamp > sess.lastTimestamp) sess.lastTimestamp = entry.timestamp;
284
+
285
+ // Find first meaningful user prompt (not slash commands, not login, not pastes)
286
+ if (!sess.firstPrompt && entry.display
287
+ && !entry.display.startsWith('/')
288
+ && !entry.display.startsWith('login')
289
+ && !entry.display.startsWith('[Pasted')) {
290
+ sess.firstPrompt = entry.display;
291
+ }
292
+ } catch { continue; }
293
+ }
294
+
295
+ // Read active terminal sessions
296
+ // Use the same root as replitBase (go up one level from .claude-persistent)
297
+ const replitRoot = join(replitBase, '..');
298
+ const sessionsDir = join(replitRoot, '..', '.claude-sessions');
299
+ const activeSessionIds = new Set();
300
+ if (existsSync(sessionsDir)) {
301
+ try {
302
+ for (const f of readdirSync(sessionsDir)) {
303
+ try {
304
+ const data = JSON.parse(readFileSync(join(sessionsDir, f), 'utf8'));
305
+ if (data.sessionId) activeSessionIds.add(data.sessionId);
306
+ } catch { continue; }
307
+ }
308
+ } catch { /* non-fatal */ }
309
+ }
310
+
311
+ // Build session list
312
+ for (const [id, sess] of bySession) {
313
+ // Derive display name
314
+ let name = sess.firstPrompt;
315
+ if (!name) {
316
+ // Fallback: use first non-login display
317
+ const firstReal = sess.entries.find(e => e.display && e.display !== 'login');
318
+ name = firstReal?.display || `Session ${id.slice(0, 8)}`;
319
+ }
320
+ // Truncate long names
321
+ if (name.length > 60) name = name.slice(0, 57) + '...';
322
+
323
+ sessions.push({
324
+ id: sess.sessionId,
325
+ name,
326
+ project: sess.project,
327
+ promptCount: sess.entries.length,
328
+ lastActive: new Date(sess.lastTimestamp).toISOString(),
329
+ isActive: activeSessionIds.has(id),
330
+ source: 'replit-tools',
331
+ age: timeAgo(sess.lastTimestamp),
332
+ });
333
+ }
334
+
335
+ // Sort by most recent first
336
+ sessions.sort((a, b) => new Date(b.lastActive) - new Date(a.lastActive));
337
+
338
+ return sessions;
339
+ }
340
+
200
341
  // ─── CLI (direct invocation) ──────────────────────────────────────────────────
201
342
 
202
343
  const isMain = process.argv[1]?.endsWith('session.mjs');