dual-brain 7.1.1 → 7.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,6 +720,19 @@ 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
737
  { key: 'g', label: 'Go — dispatch a task', section: 'Actions' },
673
738
  { key: 's', label: 'Status — detailed provider info', section: 'Actions' },
@@ -681,6 +746,12 @@ async function dashboardScreen(rl, ask) {
681
746
 
682
747
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
683
748
 
749
+ // Numeric choice → session detail
750
+ const numChoice = parseInt(choice, 10);
751
+ if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
752
+ return { next: 'session-detail', session: recentSessions[numChoice - 1] };
753
+ }
754
+
684
755
  if (choice === 'g') {
685
756
  const taskDesc = (await ask(' Task description: ')).trim();
686
757
  if (taskDesc) {
@@ -827,77 +898,223 @@ async function profileScreen(rl, ask) {
827
898
 
828
899
  async function diagnosticsScreen(rl, ask) {
829
900
  const cwd = process.cwd();
901
+ const { spawnSync: _spawnSync } = await import('child_process');
902
+ const { readdirSync } = await import('node:fs');
903
+ const { detectPlans } = await import('../src/profile.mjs');
904
+
905
+ // ── Version info ──────────────────────────────────────────────────────────
830
906
  const version = readVersion();
831
- const env = detectEnvironment();
832
- const rt = await detectRuntime();
907
+ const nodeVersion = process.version;
908
+
909
+ // ── Provider health ───────────────────────────────────────────────────────
910
+ const auth = await detectAuth();
911
+ const plans = detectPlans();
912
+ const { states: healthStates } = getHealth(cwd);
913
+
914
+ function _providerBadge(name) {
915
+ const entries = Object.entries(healthStates).filter(([k]) => k.startsWith(`${name}:`));
916
+ if (entries.length === 0) return '✅ healthy';
917
+ const statuses = entries.map(([, v]) => v.status);
918
+ if (statuses.includes('hot')) return '🔴 hot';
919
+ if (statuses.includes('degraded')) return '⚠️ degraded';
920
+ if (statuses.includes('probing')) return '⚠️ probing';
921
+ return '✅ healthy';
922
+ }
923
+
924
+ const claudeStatus = auth.claude.found ? _providerBadge('claude') : '❌ no auth';
925
+ const openaiStatus = auth.openai.found ? _providerBadge('openai') : '❌ no auth';
926
+ const claudePlanStr = plans.claude ? `Max ${plans.claude}` : (auth.claude.masked ?? 'unknown');
927
+ const openaiPlanStr = plans.openai ? `Pro ${plans.openai}` : (auth.openai.masked ?? 'unknown');
928
+ const claudeAuthStr = auth.claude.masked ?? 'not configured';
929
+ const openaiAuthStr = auth.openai.masked ?? 'not configured';
930
+
931
+ // ── Enforcement checks ────────────────────────────────────────────────────
932
+ const hooksDir = join(cwd, '.claude', 'hooks');
933
+ const headGuardExists = existsSync(join(hooksDir, 'head-guard.mjs'));
934
+ const enforceTierExists = existsSync(join(hooksDir, 'enforce-tier.mjs'));
833
935
 
834
- // Enforcement check
835
936
  let guardCount = 0;
836
- let guardDetails = 'NOT INSTALLED';
837
937
  try {
838
938
  const settingsFile = join(cwd, '.claude', 'settings.json');
839
939
  if (existsSync(settingsFile)) {
840
940
  const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
841
941
  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));
942
+ const guardCmd = 'node .claude/hooks/head-guard.mjs';
943
+ const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
944
+ const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
945
+ const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
946
+ const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
947
+ const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
848
948
  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
949
  }
853
- } catch { guardDetails = 'unknown (could not read settings)'; }
950
+ } catch { /* ignore */ }
854
951
 
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}`,
952
+ let hookifyCount = 0;
953
+ try {
954
+ const claudeDir = join(cwd, '.claude');
955
+ if (existsSync(claudeDir)) {
956
+ hookifyCount = readdirSync(claudeDir).filter(f => f.startsWith('hookify.') && f.endsWith('.md')).length;
957
+ }
958
+ } catch { /* ignore */ }
959
+
960
+ // ── Replit-tools integration ──────────────────────────────────────────────
961
+ const replitToolsDir = join(cwd, '.replit-tools');
962
+ const hasReplitTools = existsSync(replitToolsDir);
963
+ const persistentDir = join(replitToolsDir, '.claude-persistent');
964
+ const sessionManagerExists = existsSync(join(replitToolsDir, 'scripts', 'claude-session-manager.sh'));
965
+ const authRefreshScript = join(replitToolsDir, 'scripts', 'claude-auth-refresh.sh');
966
+
967
+ let credsFresh = null;
968
+ let credsExpiry = null;
969
+ let historyCount = 0;
970
+
971
+ if (hasReplitTools) {
972
+ try {
973
+ const credsFile = join(persistentDir, '.credentials.json');
974
+ const creds = JSON.parse(readFileSync(credsFile, 'utf8'));
975
+ const expiresAt = creds?.claudeAiOauth?.expiresAt;
976
+ if (expiresAt) {
977
+ const expiresMs = typeof expiresAt === 'number' ? expiresAt : Date.parse(expiresAt);
978
+ credsFresh = Date.now() < expiresMs;
979
+ credsExpiry = new Date(expiresMs).toISOString().slice(0, 10);
980
+ }
981
+ } catch { /* credentials missing or unreadable */ }
982
+
983
+ try {
984
+ const histFile = join(persistentDir, 'history.jsonl');
985
+ if (existsSync(histFile)) {
986
+ historyCount = readFileSync(histFile, 'utf8').split('\n').filter(Boolean).length;
987
+ }
988
+ } catch { /* ignore */ }
989
+ }
990
+
991
+ // ── Quality checks ────────────────────────────────────────────────────────
992
+ let testPass = null; let testTotal = null; let testError = null;
993
+ try {
994
+ const r = _spawnSync('node', ['--test', 'src/test.mjs'], { cwd, encoding: 'utf8', timeout: 30000 });
995
+ const out = (r.stdout ?? '') + (r.stderr ?? '');
996
+ const pm = out.match(/# pass (\d+)/);
997
+ const tm = out.match(/# tests (\d+)/);
998
+ if (pm && tm) { testPass = parseInt(pm[1], 10); testTotal = parseInt(tm[1], 10); }
999
+ else { testError = 'could not parse output'; }
1000
+ } catch (e) { testError = e.message; }
1001
+
1002
+ let healthPass = null; let healthTotal = null; let healthError = null;
1003
+ try {
1004
+ const healthScript = join(hooksDir, 'health-check.mjs');
1005
+ if (existsSync(healthScript)) {
1006
+ const r = _spawnSync('node', [healthScript], { cwd, encoding: 'utf8', timeout: 15000 });
1007
+ const out = (r.stdout ?? '') + (r.stderr ?? '');
1008
+ // Try summary line first: "8 pass, 0 warn, 0 fail"
1009
+ const sm = out.match(/(\d+) pass,\s*(\d+) warn,\s*(\d+) fail/);
1010
+ if (sm) {
1011
+ healthPass = parseInt(sm[1], 10);
1012
+ healthTotal = parseInt(sm[1], 10) + parseInt(sm[2], 10) + parseInt(sm[3], 10);
1013
+ } else {
1014
+ // Fall back to JSON block
1015
+ const jm = out.match(/\{[\s\S]*?"healthy"[\s\S]*?\}/);
1016
+ if (jm) {
1017
+ try {
1018
+ const p = JSON.parse(jm[0]);
1019
+ healthPass = p.pass ?? 0;
1020
+ healthTotal = (p.pass ?? 0) + (p.warn ?? 0) + (p.fail ?? 0);
1021
+ } catch { healthError = 'could not parse output'; }
1022
+ } else { healthError = 'could not parse output'; }
1023
+ }
1024
+ } else { healthError = 'health-check.mjs not found'; }
1025
+ } catch (e) { healthError = e.message; }
1026
+
1027
+ // ── Render ────────────────────────────────────────────────────────────────
1028
+ const W = 56;
1029
+ const hbar = '═'.repeat(W);
1030
+ // Pad a string to exactly W visible columns (for box rows without leading ║ prefix)
1031
+ const padRow = (s) => {
1032
+ const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
1033
+ let vlen = 0;
1034
+ for (const ch of plain) {
1035
+ const cp = ch.codePointAt(0);
1036
+ if ((cp >= 0x1f300 && cp <= 0x1faff) || (cp >= 0x2600 && cp <= 0x27bf) || cp === 0xfe0f || cp === 0x20e3) vlen += 2;
1037
+ else vlen += 1;
1038
+ }
1039
+ return s + ' '.repeat(Math.max(0, W - vlen));
1040
+ };
1041
+ const hrow = (s) => `║${padRow(' ' + s)}║`;
1042
+
1043
+ const output = [
1044
+ `╔${hbar}╗`,
1045
+ hrow('🔧 Diagnostics'),
1046
+ `╠${hbar}╣`,
1047
+ hrow(`dual-brain v${version}`),
1048
+ hrow(`Node.js ${nodeVersion}`),
1049
+ `╚${hbar}╝`,
869
1050
  '',
870
- 'Environment:',
871
- ` Replit: ${env.isReplit ? 'yes' : 'no'}`,
872
- ` replit-tools:${env.hasReplitTools ? 'yes' : 'no'}`,
873
- ` CI: ${env.isCI ? 'yes' : 'no'}`,
1051
+ separator('Provider Health'),
1052
+ ` ${claudeStatus.padEnd(14)} Claude ${claudePlanStr.padEnd(16)} ${claudeAuthStr}`,
1053
+ ` ${openaiStatus.padEnd(14)} OpenAI ${openaiPlanStr.padEnd(16)} ${openaiAuthStr}`,
874
1054
  '',
875
- 'Runtime:',
876
- ` claude CLI: ${rt.claudeAvailable ? 'available' : 'not found'}`,
877
- ` codex CLI: ${rt.codexAvailable ? 'available' : 'not found'}`,
878
- ` runtime: ${rt.runtime}`,
1055
+ separator('Enforcement'),
1056
+ ` ${headGuardExists ? '✅' : '❌'} head-guard.mjs ${headGuardExists ? 'installed' : 'missing — run: dual-brain install'}`,
1057
+ ` ${enforceTierExists ? '✅' : '❌'} enforce-tier.mjs ${enforceTierExists ? 'installed' : 'missing — run: dual-brain install'}`,
1058
+ ` ${guardCount === 4 ? '✅' : '⚠️ '} settings.json ${guardCount}/4 guards registered${guardCount < 4 ? ' — run: dual-brain install' : ''}`,
1059
+ ` ${hookifyCount > 0 ? '✅' : '⚠️ '} hookify rules ${hookifyCount} rules${hookifyCount > 0 ? ' (check: ls .claude/hookify.*.md)' : ' — none found'}`,
879
1060
  '',
880
- 'Hook files:',
881
- ...hookStatus,
1061
+ separator('Replit Tools'),
1062
+ ` ${hasReplitTools ? '✅' : '❌'} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
882
1063
  ];
883
1064
 
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
- ]));
1065
+ if (hasReplitTools) {
1066
+ if (credsFresh === null) {
1067
+ output.push(' ⚠️ Claude auth credentials file missing');
1068
+ } else if (credsFresh) {
1069
+ output.push(` ✅ Claude auth fresh (expires: ${credsExpiry})`);
1070
+ } else {
1071
+ output.push(` ❌ Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
1072
+ }
1073
+ output.push(` ✅ Session archive ${historyCount} entries`);
1074
+ output.push(` ${sessionManagerExists ? '✅' : '⚠️ '} Session manager ${sessionManagerExists ? 'available' : 'not found'}`);
1075
+ } else {
1076
+ output.push(' ─── (not available)');
1077
+ }
1078
+
1079
+ output.push('');
1080
+ output.push(separator('Quality'));
1081
+ if (testError) {
1082
+ output.push(` ❌ Tests error: ${testError}`);
1083
+ } else if (testPass !== null) {
1084
+ output.push(` ${testPass === testTotal ? '✅' : '❌'} Tests ${testPass}/${testTotal} passing`);
1085
+ }
1086
+ if (healthError) {
1087
+ output.push(` ❌ Health check error: ${healthError}`);
1088
+ } else if (healthPass !== null) {
1089
+ output.push(` ${healthPass === healthTotal ? '✅' : '⚠️ '} Health check ${healthPass}/${healthTotal} passing`);
1090
+ }
1091
+ output.push('');
1092
+
1093
+ console.log(output.join('\n'));
1094
+
1095
+ // Actions menu
1096
+ const menuOpts = [
1097
+ { key: 'h', label: 'Run health check', section: 'Actions' },
1098
+ { key: 't', label: 'Run test suite', section: 'Actions' },
1099
+ ];
1100
+ if (hasReplitTools && existsSync(authRefreshScript)) {
1101
+ menuOpts.push({ key: 'r', label: 'Refresh auth (replit-tools)', section: 'Actions' });
1102
+ }
1103
+ menuOpts.push({ key: 'i', label: 'Reinstall hooks', section: 'Actions' });
1104
+ menuOpts.push({ key: 'b', label: 'Back to dashboard', section: 'Actions' });
1105
+ console.log(menu(menuOpts));
891
1106
  console.log('');
892
1107
 
893
1108
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
894
1109
 
895
1110
  if (choice === 'h') {
896
- const hookScript = join(cwd, '.claude', 'hooks', 'health-check.mjs');
1111
+ const hookScript = join(hooksDir, 'health-check.mjs');
1112
+ console.log('');
897
1113
  if (existsSync(hookScript)) {
898
1114
  try {
899
- execSync(`node "${hookScript}"`, { stdio: 'inherit', cwd });
900
- } catch { /* hook exits non-zero on issues — already printed output */ }
1115
+ const r = _spawnSync('node', [hookScript], { stdio: 'inherit', cwd });
1116
+ if (r.error) console.log(` Error: ${r.error.message}`);
1117
+ } catch (e) { console.log(` Error: ${e.message}`); }
901
1118
  } else {
902
1119
  console.log(' health-check.mjs not found — run: dual-brain install');
903
1120
  }
@@ -905,6 +1122,32 @@ async function diagnosticsScreen(rl, ask) {
905
1122
  return { next: 'diagnostics' };
906
1123
  }
907
1124
 
1125
+ if (choice === 't') {
1126
+ console.log('\n Running test suite...\n');
1127
+ try {
1128
+ const r = _spawnSync('node', ['--test', 'src/test.mjs'], { stdio: 'inherit', cwd, timeout: 60000 });
1129
+ if (r.error) console.log(` Error: ${r.error.message}`);
1130
+ } catch (e) { console.log(` Error: ${e.message}`); }
1131
+ await ask('\n Press Enter to continue...');
1132
+ return { next: 'diagnostics' };
1133
+ }
1134
+
1135
+ if (choice === 'r') {
1136
+ if (existsSync(authRefreshScript)) {
1137
+ console.log('\n Refreshing Claude auth...\n');
1138
+ try {
1139
+ const r = _spawnSync('bash', [authRefreshScript], { stdio: 'inherit', cwd, timeout: 30000 });
1140
+ if (r.error) console.log(` Error: ${r.error.message}`);
1141
+ else if (r.status === 0) console.log('\n Auth refresh complete.');
1142
+ else console.log(`\n Auth refresh exited with code ${r.status}.`);
1143
+ } catch (e) { console.log(` Error: ${e.message}`); }
1144
+ } else {
1145
+ console.log(' claude-auth-refresh.sh not found.');
1146
+ }
1147
+ await ask('\n Press Enter to continue...');
1148
+ return { next: 'diagnostics' };
1149
+ }
1150
+
908
1151
  if (choice === 'i') {
909
1152
  await cmdInstall();
910
1153
  return { next: 'diagnostics' };
@@ -963,15 +1206,75 @@ async function replScreen(rl, ask) {
963
1206
  }
964
1207
  }
965
1208
 
1209
+ // ─── Screen: sessionDetailScreen ─────────────────────────────────────────────
1210
+
1211
+ async function sessionDetailScreen(rl, ask, ctx = {}) {
1212
+ const sess = ctx.session;
1213
+ if (!sess) return { next: 'dashboard' };
1214
+
1215
+ const W = 56;
1216
+ const hbar = '═'.repeat(W + 2);
1217
+ const pad = (s) => {
1218
+ const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
1219
+ return s + ' '.repeat(Math.max(0, W - plain.length));
1220
+ };
1221
+
1222
+ const statusLine = sess.isActive
1223
+ ? `active`
1224
+ : `inactive`;
1225
+
1226
+ const detailLines = [
1227
+ ` Session: ${sess.name}`,
1228
+ `╠${hbar}╣`,
1229
+ ` ID: ${sess.id.slice(0, 8)}...`,
1230
+ ` Status: ${statusLine}`,
1231
+ ` Prompts: ${sess.promptCount}`,
1232
+ ` Last active: ${sess.age}`,
1233
+ ` Project: ${sess.project || process.cwd()}`,
1234
+ ];
1235
+
1236
+ console.log(`╔${hbar}╗`);
1237
+ for (const line of detailLines) {
1238
+ console.log(`║ ${pad(line)}║`);
1239
+ }
1240
+ console.log(`╚${hbar}╝`);
1241
+ console.log('');
1242
+
1243
+ if (sess.isActive) {
1244
+ console.log(' [c] Continue this session (claude --continue)');
1245
+ } else {
1246
+ console.log(' [r] Resume this session (claude --resume)');
1247
+ }
1248
+ console.log(' [b] Back to dashboard');
1249
+ console.log('');
1250
+
1251
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1252
+
1253
+ if (choice === 'c' || choice === 'r') {
1254
+ console.log(`\n Launching: claude --resume ${sess.id}\n`);
1255
+ try {
1256
+ const { spawnSync } = await import('node:child_process');
1257
+ spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
1258
+ } catch {
1259
+ console.log(' Could not launch claude CLI. Run manually:');
1260
+ console.log(` claude --resume ${sess.id}`);
1261
+ }
1262
+ return { next: 'dashboard' };
1263
+ }
1264
+
1265
+ return { next: 'dashboard' };
1266
+ }
1267
+
966
1268
  // ─── Screen state machine ─────────────────────────────────────────────────────
967
1269
 
968
1270
  const SCREENS = {
969
- welcome: welcomeScreen,
970
- dashboard: dashboardScreen,
971
- auth: authScreen,
972
- profile: profileScreen,
973
- diagnostics: diagnosticsScreen,
974
- repl: replScreen,
1271
+ welcome: welcomeScreen,
1272
+ dashboard: dashboardScreen,
1273
+ auth: authScreen,
1274
+ profile: profileScreen,
1275
+ diagnostics: diagnosticsScreen,
1276
+ repl: replScreen,
1277
+ 'session-detail': sessionDetailScreen,
975
1278
  };
976
1279
 
977
1280
  async function runScreens(startScreen = 'dashboard') {
@@ -979,15 +1282,19 @@ async function runScreens(startScreen = 'dashboard') {
979
1282
  const ask = (q) => new Promise(res => rl.question(q, res));
980
1283
 
981
1284
  let current = startScreen;
1285
+ let ctx = {};
982
1286
  while (current && current !== 'exit') {
983
1287
  const screen = SCREENS[current];
984
1288
  if (!screen) break;
985
1289
  try {
986
- const result = await screen(rl, ask);
1290
+ const result = await screen(rl, ask, ctx);
987
1291
  current = result?.next || 'exit';
1292
+ // Pass through context (e.g. selected session) to next screen
1293
+ ctx = result?.session ? { session: result.session } : {};
988
1294
  } catch (e) {
989
1295
  console.error(`Error: ${e.message}`);
990
1296
  current = 'dashboard'; // recover to dashboard on error
1297
+ ctx = {};
991
1298
  }
992
1299
  }
993
1300
  rl.close();
@@ -1007,9 +1314,15 @@ async function main() {
1007
1314
 
1008
1315
  if (!cmd) {
1009
1316
  if (isInteractive) {
1010
- // Check if first-run (no profile) → welcome screen, else dashboard
1011
- const startScreen = profileExists() ? 'dashboard' : 'welcome';
1012
- await runScreens(startScreen);
1317
+ const cwd = process.cwd();
1318
+ if (profileExists(cwd)) {
1319
+ // Profile already exists → go straight to dashboard
1320
+ await runScreens('dashboard');
1321
+ } else {
1322
+ // First run: welcomeScreen handles auto-setup detection internally,
1323
+ // then falls through to manual wizard if needed.
1324
+ await runScreens('welcome');
1325
+ }
1013
1326
  } else {
1014
1327
  // Non-TTY: print status card and exit
1015
1328
  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.2",
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,124 @@ 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
+ const replitBase = join(cwd, '.replit-tools', '.claude-persistent');
233
+
234
+ // Read history.jsonl
235
+ const historyPath = join(replitBase, 'history.jsonl');
236
+ if (!existsSync(historyPath)) return sessions;
237
+
238
+ let lines;
239
+ try {
240
+ lines = readFileSync(historyPath, 'utf8').split('\n').filter(Boolean);
241
+ } catch { return sessions; }
242
+
243
+ const bySession = new Map(); // sessionId → { entries, firstPrompt, lastTimestamp }
244
+
245
+ for (const line of lines) {
246
+ try {
247
+ const entry = JSON.parse(line);
248
+ if (!entry.sessionId) continue;
249
+
250
+ if (!bySession.has(entry.sessionId)) {
251
+ bySession.set(entry.sessionId, {
252
+ sessionId: entry.sessionId,
253
+ project: entry.project,
254
+ entries: [],
255
+ firstPrompt: null,
256
+ lastTimestamp: 0,
257
+ });
258
+ }
259
+
260
+ const sess = bySession.get(entry.sessionId);
261
+ sess.entries.push(entry);
262
+ if (entry.timestamp > sess.lastTimestamp) sess.lastTimestamp = entry.timestamp;
263
+
264
+ // Find first meaningful user prompt (not slash commands, not login, not pastes)
265
+ if (!sess.firstPrompt && entry.display
266
+ && !entry.display.startsWith('/')
267
+ && !entry.display.startsWith('login')
268
+ && !entry.display.startsWith('[Pasted')) {
269
+ sess.firstPrompt = entry.display;
270
+ }
271
+ } catch { continue; }
272
+ }
273
+
274
+ // Read active terminal sessions
275
+ const sessionsDir = join(cwd, '.replit-tools', '.claude-sessions');
276
+ const activeSessionIds = new Set();
277
+ if (existsSync(sessionsDir)) {
278
+ try {
279
+ for (const f of readdirSync(sessionsDir)) {
280
+ try {
281
+ const data = JSON.parse(readFileSync(join(sessionsDir, f), 'utf8'));
282
+ if (data.sessionId) activeSessionIds.add(data.sessionId);
283
+ } catch { continue; }
284
+ }
285
+ } catch { /* non-fatal */ }
286
+ }
287
+
288
+ // Build session list
289
+ for (const [id, sess] of bySession) {
290
+ // Derive display name
291
+ let name = sess.firstPrompt;
292
+ if (!name) {
293
+ // Fallback: use first non-login display
294
+ const firstReal = sess.entries.find(e => e.display && e.display !== 'login');
295
+ name = firstReal?.display || `Session ${id.slice(0, 8)}`;
296
+ }
297
+ // Truncate long names
298
+ if (name.length > 60) name = name.slice(0, 57) + '...';
299
+
300
+ sessions.push({
301
+ id: sess.sessionId,
302
+ name,
303
+ project: sess.project,
304
+ promptCount: sess.entries.length,
305
+ lastActive: new Date(sess.lastTimestamp).toISOString(),
306
+ isActive: activeSessionIds.has(id),
307
+ source: 'replit-tools',
308
+ age: timeAgo(sess.lastTimestamp),
309
+ });
310
+ }
311
+
312
+ // Sort by most recent first
313
+ sessions.sort((a, b) => new Date(b.lastActive) - new Date(a.lastActive));
314
+
315
+ return sessions;
316
+ }
317
+
200
318
  // ─── CLI (direct invocation) ──────────────────────────────────────────────────
201
319
 
202
320
  const isMain = process.argv[1]?.endsWith('session.mjs');