dual-brain 7.1.6 → 7.1.7

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.
@@ -4,14 +4,15 @@
4
4
  import { existsSync, readFileSync } from 'node:fs';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { execSync } from 'node:child_process';
7
+ import { execSync, spawnSync as _spawnSyncTop } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
9
9
 
10
10
  import {
11
11
  ensureProfile, loadProfile, saveProfile, runOnboarding,
12
12
  rememberPreference, forgetPreference, getActivePreferences,
13
13
  getAvailableProviders, isSoloBrain, getHeadModel,
14
- detectAuth, detectEnvironment, setupAuth,
14
+ detectAuth, detectEnvironment, detectPlans,
15
+ saveSubscription, listSubscriptions,
15
16
  autoSetup,
16
17
  } from '../src/profile.mjs';
17
18
 
@@ -28,7 +29,7 @@ import {
28
29
  import { dispatch, detectRuntime, dispatchDualBrain } from '../src/dispatch.mjs';
29
30
 
30
31
  import { loadRepoCache } from '../src/repo.mjs';
31
- import { loadSession, saveSession, formatSessionCard, importReplitSessions } from '../src/session.mjs';
32
+ import { loadSession, saveSession, formatSessionCard, importReplitSessions, renameSession, pinSession, unpinSession, categorizeSession, enrichSessions } from '../src/session.mjs';
32
33
 
33
34
  import { box, bar, badge, menu, separator } from '../src/tui.mjs';
34
35
 
@@ -44,14 +45,34 @@ function flag(args, name) { const i = args.indexOf(name); return i !== -1 ? (arg
44
45
  function err(msg) { process.stderr.write(`Error: ${msg}\n`); process.exit(1); }
45
46
  function vtrace(msg) { process.stderr.write(`[verbose] ${msg}\n`); }
46
47
 
48
+ function daysUntil(isoDate) {
49
+ if (!isoDate) return null;
50
+ const ms = Date.parse(isoDate) - Date.now();
51
+ return Math.ceil(ms / 86400000);
52
+ }
53
+
54
+ async function askExpiry(ask, provLabel) {
55
+ console.log(` ${provLabel} — how long should this auth last?`);
56
+ console.log(' (1) 1 week (2) 2 weeks (3) 1 month (4) Custom date (Enter) No expiry');
57
+ const choice = (await ask(' > ')).trim();
58
+ const now = new Date();
59
+ if (choice === '1') { now.setDate(now.getDate() + 7); return now.toISOString(); }
60
+ if (choice === '2') { now.setDate(now.getDate() + 14); return now.toISOString(); }
61
+ if (choice === '3') { now.setMonth(now.getMonth() + 1); return now.toISOString(); }
62
+ if (choice === '4') {
63
+ const d = (await ask(' Date YYYY-MM-DD: ')).trim();
64
+ if (/^\d{4}-\d{2}-\d{2}$/.test(d)) return new Date(d).toISOString();
65
+ }
66
+ return null;
67
+ }
68
+
47
69
  function printHelp() {
48
70
  console.log(`
49
71
  dual-brain <command> [options]
50
72
 
51
73
  Commands:
52
74
  init First-time setup → flows into interactive REPL
53
- auth Show authentication status for all providers
54
- auth setup Paste API keys directly (recommended for Replit)
75
+ auth Show subscription and login status
55
76
  install Install Claude Code hooks into the current project
56
77
  go "task description" Detect → decide → dispatch a task
57
78
  --dry-run Show routing decision without executing
@@ -63,6 +84,8 @@ Commands:
63
84
  cool <provider> Manually clear hot state for a provider
64
85
  remember "preference" Save a project-scoped preference
65
86
  forget "preference" Remove a preference by fuzzy match
87
+ shell-hook Output bash snippet to add dual-brain to your shell
88
+ Usage: dual-brain shell-hook >> ~/.bashrc
66
89
 
67
90
  Interactive mode (entered with no args on a TTY):
68
91
  Session manager with recent sessions and routing.
@@ -75,43 +98,44 @@ Options:
75
98
  `.trim());
76
99
  }
77
100
 
78
- // ─── Auth helpers ─────────────────────────────────────────────────────────────
101
+ // ─── Subscription status table ────────────────────────────────────────────────
79
102
 
80
103
  /**
81
- * Print a compact auth status table to stdout.
82
- * @param {{ claude: object, openai: object }} auth Result from detectAuth()
83
- * @param {object} [profile] Optional loaded profile to cross-check enabled state
104
+ * Print a subscription status table to stdout.
84
105
  */
85
- function printAuthTable(auth, profile) {
86
- const W = 55; // inner width (wide enough for source labels)
106
+ function printSubscriptionTable(auth, profile) {
107
+ const W = 55;
87
108
  const hbar = '═'.repeat(W);
88
109
  const pad = (s) => {
89
- const visible = s.replace(/[̀-ͯ]/g, ''); // strip combining chars for length
110
+ const visible = s.replace(/[̀-ͯ]/g, '');
90
111
  return s + ' '.repeat(Math.max(0, W - visible.length));
91
112
  };
92
113
 
93
- const claudeDisabled = profile?.providers?.claude?.enabled === false;
94
- const openaiDisabled = profile?.providers?.openai?.enabled === false;
114
+ const claudeSub = profile?.providers?.claude;
115
+ const openaiSub = profile?.providers?.openai;
116
+
117
+ const claudePlanLabel = claudeSub?.enabled
118
+ ? ({ 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)
119
+ : 'disabled';
120
+ const openaiPlanLabel = openaiSub?.enabled
121
+ ? ({ 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)
122
+ : 'disabled';
95
123
 
96
- const claudeDisabledNote = claudeDisabled ? ' (auth ok, but disabled in profile)' : '';
97
- const openaiDisabledNote = openaiDisabled ? ' (auth ok, but disabled in profile)' : '';
124
+ const claudeLabel = claudeSub?.label ? ` [${claudeSub.label}]` : '';
125
+ const openaiLabel = openaiSub?.label ? ` [${openaiSub.label}]` : '';
98
126
 
99
127
  const claudeLine1 = auth.claude.found
100
- ? ` Claude: found via ${auth.claude.source}${claudeDisabledNote}`
101
- : ` Claude: not found`;
102
- const claudeLine2 = auth.claude.found
103
- ? ` ${auth.claude.masked}`
104
- : ` run: dual-brain auth setup`;
128
+ ? ` Claude: logged in (${auth.claude.source})`
129
+ : ` Claude: not logged in — run: claude login`;
130
+ const claudeLine2 = ` plan: ${claudePlanLabel}${claudeLabel}`;
105
131
 
106
132
  const openaiLine1 = auth.openai.found
107
- ? ` OpenAI: found via ${auth.openai.source}${openaiDisabledNote}`
108
- : ` OpenAI: not found`;
109
- const openaiLine2 = auth.openai.found
110
- ? ` ${auth.openai.masked}`
111
- : ` run: dual-brain auth setup`;
133
+ ? ` OpenAI: logged in (${auth.openai.source})`
134
+ : ` OpenAI: not logged in — run: codex login`;
135
+ const openaiLine2 = ` plan: ${openaiPlanLabel}${openaiLabel}`;
112
136
 
113
137
  console.log(`╔${hbar}╗`);
114
- console.log(`║${pad(' Auth Status')}║`);
138
+ console.log(`║${pad(' Subscription Status')}║`);
115
139
  console.log(`╠${hbar}╣`);
116
140
  console.log(`║${pad(claudeLine1)}║`);
117
141
  console.log(`║${pad(claudeLine2)}║`);
@@ -125,35 +149,24 @@ function printAuthTable(auth, profile) {
125
149
  async function cmdInit(rl) {
126
150
  const cwd = process.cwd();
127
151
 
128
- // --- Step 1: Auth preflight ---
152
+ // --- Step 1: Detect auth ---
129
153
  const auth = await detectAuth();
130
- printAuthTable(auth, loadProfile(cwd));
154
+ printSubscriptionTable(auth, loadProfile(cwd));
131
155
 
132
156
  const noneFound = !auth.claude.found && !auth.openai.found;
133
157
  if (noneFound) {
134
- console.log('\nNo AI provider credentials found. Let\'s set up at least one now.\n');
135
- // Use the provided rl (REPL instance) or create a temporary one
136
- const rlOwned = !rl;
137
- if (!rl) rl = createInterface({ input: process.stdin, output: process.stdout });
138
- try {
139
- await setupAuth(rl);
140
- } finally {
141
- if (rlOwned) rl.close();
142
- }
143
- // Re-check after setup
144
- const authAfter = await detectAuth();
145
- if (!authAfter.claude.found && !authAfter.openai.found) {
146
- console.log('\nNo credentials configured. You can run "auth setup" in the REPL anytime.');
147
- // Still flow into REPL — don't exit
148
- return;
149
- }
158
+ console.log('\nNo AI provider found. Log in first:');
159
+ console.log(' Claude: claude login');
160
+ console.log(' OpenAI: codex login\n');
161
+ console.log('Then re-run: dual-brain init');
162
+ return;
150
163
  }
151
164
 
152
- // --- Step 2: Run onboarding wizard (pass shared rl so it isn't closed) ---
165
+ // --- Step 2: Run onboarding wizard ---
153
166
  const profile = await runOnboarding({ interactive: true, detectedAuth: auth, rl });
154
167
  saveProfile(profile, { cwd });
155
168
 
156
- // --- Step 2b: Install hooks so enforcement is active from first run ---
169
+ // --- Step 2b: Install hooks ---
157
170
  await cmdInstall(cwd);
158
171
 
159
172
  // --- Step 3: Show dashboard ---
@@ -166,30 +179,18 @@ async function cmdInit(rl) {
166
179
  console.log('\nReady! Type a task below, or "help" for commands.\n');
167
180
  }
168
181
 
169
- async function cmdAuth(subArgs = [], rl) {
170
- const sub = subArgs[0];
171
-
172
- if (sub === 'setup') {
173
- return cmdAuthSetup(rl);
174
- }
175
-
176
- const auth = await detectAuth();
182
+ /**
183
+ * Show subscription status (replaces old API key auth display).
184
+ */
185
+ async function cmdAuth(subArgs = []) {
186
+ const auth = await detectAuth();
177
187
  const profile = loadProfile(process.cwd());
178
- printAuthTable(auth, profile);
188
+ printSubscriptionTable(auth, profile);
179
189
 
180
- // If anything is missing, point to setup command
181
190
  if (!auth.claude.found || !auth.openai.found) {
182
- console.log('\nRun "dual-brain auth setup" (or "auth setup" in REPL) to paste API keys.');
183
- }
184
- }
185
-
186
- async function cmdAuthSetup(rl) {
187
- const rlOwned = !rl;
188
- if (!rl) rl = createInterface({ input: process.stdin, output: process.stdout });
189
- try {
190
- await setupAuth(rl);
191
- } finally {
192
- if (rlOwned) rl.close();
191
+ console.log('');
192
+ if (!auth.claude.found) console.log(' Claude not logged in. Run: claude login');
193
+ if (!auth.openai.found) console.log(' OpenAI not logged in. Run: codex login');
193
194
  }
194
195
  }
195
196
 
@@ -531,183 +532,225 @@ function profileExists(cwd) {
531
532
  return existsSync(projectPath) || existsSync(globalPath);
532
533
  }
533
534
 
535
+ // ─── Plan label helpers ───────────────────────────────────────────────────────
536
+
537
+ const CLAUDE_PLAN_LABELS = {
538
+ pro: 'Pro ($20/mo)',
539
+ max5: 'Max x5 ($100/mo)',
540
+ max20: 'Max x20 ($200/mo)',
541
+ '$20': 'Pro ($20/mo)',
542
+ '$100': 'Max x5 ($100/mo)',
543
+ '$200': 'Max x20 ($200/mo)',
544
+ };
545
+ const OPENAI_PLAN_LABELS = {
546
+ plus: 'Plus ($20/mo)',
547
+ pro: 'Pro ($100/mo)',
548
+ pro100: 'Pro ($100/mo)',
549
+ pro200: 'Pro ($200/mo)',
550
+ '$20': 'Plus ($20/mo)',
551
+ '$100': 'Pro ($100/mo)',
552
+ '$200': 'Pro ($200/mo)',
553
+ };
554
+
534
555
  // ─── Screen: welcomeScreen ────────────────────────────────────────────────────
535
556
 
536
557
  async function welcomeScreen(rl, ask) {
537
558
  const version = readVersion();
538
559
  const cwd = process.cwd();
539
560
 
540
- // --- Try auto-setup first ---
541
- console.log(box(`🧠 Dual-Brain v${version} — Setup`, [
542
- 'Detecting environment...',
543
- ]));
561
+ // --- Detect CLI login status ---
562
+ process.stdout.write(`\ndual-brain v${version} — Setup\n\nDetecting your setup...\n`);
563
+
564
+ const auth = await detectAuth();
565
+ const plans = detectPlans();
566
+
567
+ const claudeReady = auth.claude.found;
568
+ const openaiReady = auth.openai.found;
569
+
570
+ const claudePlanLabel = claudeReady
571
+ ? (CLAUDE_PLAN_LABELS[plans.claude] ?? plans.claude ?? 'plan unknown')
572
+ : null;
573
+ const openaiPlanLabel = openaiReady
574
+ ? (OPENAI_PLAN_LABELS[plans.openai] ?? plans.openai ?? 'plan unknown')
575
+ : null;
576
+
577
+ const detectedLines = [];
578
+ if (claudeReady) detectedLines.push(` Claude CLI ready${claudePlanLabel ? ` (${claudePlanLabel})` : ''}`);
579
+ else detectedLines.push(` Claude CLI not logged in`);
580
+ if (openaiReady) detectedLines.push(` Codex CLI ready${openaiPlanLabel ? ` (${openaiPlanLabel})` : ''}`);
581
+ else detectedLines.push(` Codex CLI not logged in`);
582
+
583
+ console.log('');
584
+ console.log('Detected:');
585
+ for (const line of detectedLines) {
586
+ const ok = !line.includes('not logged');
587
+ console.log(` ${ok ? '' : ''}${line.trim()}`);
588
+ }
544
589
  console.log('');
545
590
 
546
- const setup = await autoSetup(cwd);
547
-
548
- if (setup.confident) {
549
- // Build summary lines for the auto-detected state
550
- const detectedLines = [
551
- 'Detecting environment...',
552
- ...setup.actions.map(a => `✓ ${a}`),
553
- ...setup.warnings.map(w => `⚠ ${w}`),
554
- ];
555
-
556
- const modeLabel = setup.profile.mode === 'dual' ? 'dual mode, balanced'
557
- : setup.profile.mode === 'solo-claude' ? 'Claude-only mode, balanced'
558
- : setup.profile.mode === 'solo-openai' ? 'OpenAI-only mode, balanced'
559
- : `${setup.profile.mode}, balanced`;
560
-
561
- const readyBox = box(`🧠 Dual-Brain v${version} — Setup`, [
562
- ...detectedLines,
563
- '',
564
- `Ready to go! Auto-configured ${modeLabel}.`,
565
- ]);
566
- console.log(readyBox);
567
- console.log('');
568
- console.log(' [Enter] Start coding →');
569
- console.log(' [c] Customize setup');
570
- console.log(' [a] Auth management');
571
- console.log('');
591
+ // --- Detect data-tools / replit-tools sessions ---
592
+ const env = detectEnvironment();
593
+ const existingSessions = importReplitSessions(cwd);
594
+ if (env.hasReplitTools) {
595
+ detectedLines.push(` data-tools detected`);
596
+ }
597
+ if (existingSessions.length > 0) {
598
+ detectedLines.push(` ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} found from data-tools`);
599
+ }
572
600
 
573
- const choice = (await ask(' Choice: ')).trim().toLowerCase();
601
+ // Re-print with full detection results
602
+ console.log('\r\x1b[K'); // clear the partial output
603
+ console.log('Detected:');
604
+ for (const line of detectedLines) {
605
+ const ok = !line.includes('not logged');
606
+ console.log(` ${ok ? '✓' : '✗'} ${line.trim()}`);
607
+ }
608
+ console.log('');
574
609
 
575
- if (choice === 'c') {
576
- // Fall through to manual wizard below
577
- } else if (choice === 'a') {
578
- return { next: 'auth' };
579
- } else {
580
- // Enter or anything else → save and go to dashboard
581
- saveProfile(setup.profile, { cwd });
582
- await cmdInstall(cwd);
583
- return { next: 'main' };
610
+ if (!claudeReady && !openaiReady) {
611
+ console.log('No CLI login found. Log in first:');
612
+ console.log(' claude login — for Claude');
613
+ console.log(' codex login — for OpenAI/Codex\n');
614
+ console.log('Then re-run: dual-brain init');
615
+ return { next: 'exit' };
616
+ }
617
+
618
+ console.log(' [Enter] Save and go');
619
+ console.log(' [c] Customize plan tier');
620
+ if (existingSessions.length > 0) {
621
+ console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from data-tools`);
622
+ }
623
+ console.log('');
624
+
625
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
626
+
627
+ if (choice === 'i' && existingSessions.length > 0) {
628
+ console.log(`\n Importing ${existingSessions.length} sessions from data-tools...\n`);
629
+ const recent = existingSessions.slice(0, 5);
630
+ for (const sess of recent) {
631
+ console.log(` ${sess.age.padEnd(6)} ${sess.name}`);
584
632
  }
585
- } else {
586
- // Not confident show what's missing before falling through to wizard
587
- if (setup.warnings.length > 0) {
588
- console.log(box(`🧠 Dual-Brain v${version} — Setup`, [
589
- 'Auto-detection incomplete:',
590
- ...setup.warnings.map(w => ` ✗ ${w}`),
591
- '',
592
- 'Let\'s configure manually.',
593
- ]));
594
- console.log('');
633
+ if (existingSessions.length > 5) {
634
+ console.log(` ... and ${existingSessions.length - 5} more`);
595
635
  }
636
+ console.log('\n Sessions imported! They\'ll appear in your Recent list.\n');
637
+ await ask(' Press Enter to continue...');
638
+ // Fall through to auto-save
596
639
  }
597
640
 
598
- // --- Manual wizard (fallback or [c] Customize) ---
599
- console.log(separator('Claude (Anthropic)'));
600
- console.log(' (1) $20/mo Pro');
601
- console.log(' (2) $100/mo Max 5x');
602
- console.log(' (3) $200/mo Max 20x');
603
- console.log(' (4) API key only');
604
- console.log(' (5) Skip don\'t use Claude');
605
- const claudeChoice = (await ask('> ')).trim();
606
-
607
- let claudePlan = null;
608
- let claudeEnabled = true;
609
- if (claudeChoice === '1') { claudePlan = 'pro'; }
610
- else if (claudeChoice === '2') { claudePlan = 'max5'; }
611
- else if (claudeChoice === '3') { claudePlan = 'max20'; }
612
- else if (claudeChoice === '4') {
613
- claudePlan = 'api';
614
- // Ask for API key immediately
615
- const key = (await ask('Paste your Anthropic API key: ')).trim();
616
- if (key) {
617
- // Inline: set env var for this session, profile will persist
618
- process.env.ANTHROPIC_API_KEY = key;
619
- console.log('✓ Claude API key set for this session');
641
+ if (choice !== 'c') {
642
+ // Auto-save detected plans and proceed
643
+ const setup = await autoSetup(cwd);
644
+ if (setup.confident && setup.profile) {
645
+ saveProfile(setup.profile, { cwd });
646
+ } else {
647
+ // Build profile from what we know
648
+ const existing = loadProfile(cwd);
649
+ if (claudeReady) {
650
+ existing.providers.claude = { enabled: true, plan: plans.claude || 'pro' };
651
+ }
652
+ if (openaiReady) {
653
+ existing.providers.openai = { enabled: true, plan: plans.openai || 'plus' };
654
+ }
655
+ const enabledCount = [claudeReady, openaiReady].filter(Boolean).length;
656
+ existing.mode = enabledCount >= 2 ? 'dual' : claudeReady ? 'solo-claude' : 'solo-openai';
657
+ saveProfile(existing, { cwd });
620
658
  }
621
- } else if (claudeChoice === '5') {
622
- claudeEnabled = false;
623
- claudePlan = null;
624
- } else {
625
- // Default: pro
626
- claudePlan = 'pro';
659
+ await cmdInstall(cwd);
660
+ return { next: 'main' };
627
661
  }
628
662
 
629
- console.log('');
663
+ // ── [c] Customize: plan picker ───────────────────────────────────────────
664
+
665
+ const existingProfile = loadProfile(cwd);
630
666
 
631
- // --- OpenAI provider selection ---
632
- console.log(separator('OpenAI (ChatGPT/Codex)'));
633
- console.log(' (1) $20/mo Plus');
634
- console.log(' (2) $100/mo Pro');
635
- console.log(' (3) $200/mo Pro (higher limits)');
636
- console.log(' (4) API key only');
637
- console.log(' (5) Skip don\'t use OpenAI');
638
- const openaiChoice = (await ask('> ')).trim();
639
-
640
- let openaiPlan = null;
641
- let openaiEnabled = true;
642
- if (openaiChoice === '1') { openaiPlan = 'plus'; }
643
- else if (openaiChoice === '2') { openaiPlan = 'pro'; }
644
- else if (openaiChoice === '3') { openaiPlan = 'pro200'; }
645
- else if (openaiChoice === '4') {
646
- openaiPlan = 'api';
647
- const key = (await ask('Paste your OpenAI API key: ')).trim();
648
- if (key) {
649
- process.env.OPENAI_API_KEY = key;
650
- console.log('✓ OpenAI API key set for this session');
667
+ // Claude plan picker
668
+ if (claudeReady) {
669
+ console.log('');
670
+ console.log(separator('Claude subscription'));
671
+ console.log(' (1) Pro ($20/mo)');
672
+ console.log(' (2) Max x5 ($100/mo)');
673
+ console.log(' (3) Max x20 ($200/mo)');
674
+ console.log(' (4) Skip');
675
+ const claudeChoice = (await ask('> ')).trim();
676
+ const claudePlanMap = { '1': 'pro', '2': 'max5', '3': 'max20' };
677
+ if (claudeChoice !== '4') {
678
+ existingProfile.providers.claude = {
679
+ enabled: true,
680
+ plan: claudePlanMap[claudeChoice] || plans.claude || 'pro',
681
+ };
682
+ } else {
683
+ existingProfile.providers.claude = { enabled: false, plan: plans.claude || 'pro' };
651
684
  }
652
- } else if (openaiChoice === '5') {
653
- openaiEnabled = false;
654
- openaiPlan = null;
655
- } else {
656
- openaiPlan = 'plus';
657
685
  }
658
686
 
659
- console.log('');
687
+ // OpenAI plan picker
688
+ if (openaiReady) {
689
+ console.log('');
690
+ console.log(separator('OpenAI subscription'));
691
+ console.log(' (1) Plus ($20/mo)');
692
+ console.log(' (2) Pro ($100/mo)');
693
+ console.log(' (3) Pro ($200/mo higher limits)');
694
+ console.log(' (4) Skip');
695
+ const openaiChoice = (await ask('> ')).trim();
696
+ const openaiPlanMap = { '1': 'plus', '2': 'pro', '3': 'pro200' };
697
+ if (openaiChoice !== '4') {
698
+ existingProfile.providers.openai = {
699
+ enabled: true,
700
+ plan: openaiPlanMap[openaiChoice] || plans.openai || 'plus',
701
+ };
702
+ } else {
703
+ existingProfile.providers.openai = { enabled: false, plan: plans.openai || 'plus' };
704
+ }
705
+ }
660
706
 
661
- // --- Optimization mode ---
707
+ // Mode picker
708
+ console.log('');
662
709
  console.log(separator('Optimization'));
663
710
  console.log(' (1) Save usage — prefer cheaper models');
664
711
  console.log(' (2) Balanced — best model per tier (recommended)');
665
712
  console.log(' (3) Quality first — always use best available');
666
713
  const modeChoice = (await ask('> ')).trim();
714
+ existingProfile.mode = ({ '1': 'cost-saver', '3': 'quality-first' })[modeChoice] || 'balanced';
667
715
 
668
- let mode = 'balanced';
669
- if (modeChoice === '1') { mode = 'cost-saver'; }
670
- else if (modeChoice === '3') { mode = 'quality-first'; }
716
+ // Team setup
717
+ console.log('');
718
+ console.log(' Team auth: label subscriptions and set expiry for auto-refresh.');
719
+ console.log(' When a subscription expires, dual-brain will prompt re-login automatically.');
720
+ console.log('');
721
+ console.log(' [Enter] Skip [t] Set up team auth');
722
+ const teamChoice = (await ask(' Choice: ')).trim().toLowerCase();
723
+ if (teamChoice === 't') {
724
+ for (const provider of ['claude', 'openai']) {
725
+ if (!existingProfile.providers[provider]?.enabled) continue;
726
+ const provLabel = provider === 'claude' ? 'Claude' : 'OpenAI';
727
+ const label = (await ask(` ${provLabel} label (e.g. "Josh's $100 sub"): `)).trim();
728
+ if (label) existingProfile.providers[provider].label = label;
729
+ const expiry = await askExpiry(ask, provLabel);
730
+ if (expiry) existingProfile.providers[provider].expiresAt = expiry;
731
+ }
732
+ }
671
733
 
672
- // --- Build and save profile ---
673
- const existingProfile = loadProfile(cwd);
674
- const profile = {
675
- ...existingProfile,
676
- mode,
677
- providers: {
678
- claude: {
679
- enabled: claudeEnabled,
680
- plan: claudePlan || 'pro',
681
- },
682
- openai: {
683
- enabled: openaiEnabled,
684
- plan: openaiPlan || 'plus',
685
- },
686
- },
687
- };
688
- saveProfile(profile, { cwd });
734
+ const enabledCount = Object.values(existingProfile.providers).filter(p => p.enabled).length;
735
+ existingProfile.mode = enabledCount >= 2 ? existingProfile.mode || 'auto' : claudeReady ? 'solo-claude' : 'solo-openai';
689
736
 
690
- // --- Detect environment for summary ---
691
- const env = detectEnvironment();
692
- const auth = await detectAuth();
737
+ saveProfile(existingProfile, { cwd });
693
738
 
694
- const summaryLines = [
695
- `Mode: ${mode}`,
696
- claudeEnabled
697
- ? `Claude: ${claudePlan} plan ${auth.claude.found ? badge('connected') : badge('missing')}`
698
- : 'Claude: disabled',
699
- openaiEnabled
700
- ? `OpenAI: ${openaiPlan} plan ${auth.openai.found ? badge('connected') : badge('missing')}`
701
- : 'OpenAI: disabled',
702
- env.isReplit ? '🌀 Replit environment detected' : '',
703
- ].filter(Boolean);
739
+ // Summary
740
+ const summaryLines = [];
741
+ for (const [key, prov] of Object.entries(existingProfile.providers)) {
742
+ const planLabel = key === 'claude'
743
+ ? (CLAUDE_PLAN_LABELS[prov.plan] ?? prov.plan)
744
+ : (OPENAI_PLAN_LABELS[prov.plan] ?? prov.plan);
745
+ summaryLines.push(`${key === 'claude' ? 'Claude' : 'OpenAI'}: ${prov.enabled ? planLabel : 'disabled'}${prov.label ? ` [${prov.label}]` : ''}`);
746
+ }
747
+ summaryLines.push(`Mode: ${existingProfile.mode}`);
704
748
 
705
749
  console.log('');
706
750
  console.log(box('Setup Complete', summaryLines));
707
751
  console.log('');
708
752
 
709
753
  await cmdInstall(cwd);
710
-
711
754
  return { next: 'main' };
712
755
  }
713
756
 
@@ -719,21 +762,69 @@ async function mainScreen(rl, ask) {
719
762
  const profile = loadProfile(cwd);
720
763
  const auth = await detectAuth();
721
764
 
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`;
765
+ const claudeSub = profile?.providers?.claude;
766
+ const openaiSub = profile?.providers?.openai;
767
+ const claudePlan = claudeSub?.plan ?? 'Pro';
768
+ const openaiPlan = openaiSub?.plan ?? 'Plus';
769
+
770
+ // Check subscription expiry
771
+ const now = Date.now();
772
+ const claudeExpired = claudeSub?.expiresAt && Date.parse(claudeSub.expiresAt) < now;
773
+ const openaiExpired = openaiSub?.expiresAt && Date.parse(openaiSub.expiresAt) < now;
774
+
775
+ const claudeDays = daysUntil(claudeSub?.expiresAt);
776
+ const openaiDays = daysUntil(openaiSub?.expiresAt);
777
+
778
+ function subStatus(name, plan, found, expired, days, sub) {
779
+ if (!found) return `${name}: not logged in`;
780
+ let s = `${name}: ${plan} ✓`;
781
+ if (sub?.label) s += ` [${sub.label}]`;
782
+ if (expired) return `${name}: ${plan} ⚠ expired${sub?.label ? ` [${sub.label}]` : ''}`;
783
+ if (days !== null && days <= 7) s += ` (${days}d left)`;
784
+ return s;
785
+ }
786
+
787
+ let claudeStatus = subStatus('Claude', claudePlan, auth.claude.found, claudeExpired, claudeDays, claudeSub);
788
+ let openaiStatus = subStatus('OpenAI', openaiPlan, auth.openai.found, openaiExpired, openaiDays, openaiSub);
726
789
 
727
790
  console.log(`\ndual-brain v${version}`);
728
- console.log(`${claudeStatus} · ${openaiStatus}\n`);
791
+ console.log(`${claudeStatus} · ${openaiStatus}`);
792
+
793
+ // Auto-refresh expired subscriptions
794
+ if (claudeExpired || openaiExpired) {
795
+ const { spawnSync } = await import('node:child_process');
796
+ const expired = [];
797
+ if (claudeExpired) expired.push('Claude');
798
+ if (openaiExpired) expired.push('OpenAI');
799
+ console.log(`\n ${expired.join(' & ')} subscription expired. Re-authenticating...`);
800
+ if (claudeExpired) {
801
+ const r = spawnSync('claude', ['login'], { stdio: 'inherit', timeout: 30000 });
802
+ if (r.status === 0) {
803
+ claudeSub.expiresAt = null;
804
+ saveProfile(profile, { cwd });
805
+ console.log(' ✓ Claude re-authenticated');
806
+ }
807
+ }
808
+ if (openaiExpired) {
809
+ const r = spawnSync('codex', ['login'], { stdio: 'inherit', timeout: 30000 });
810
+ if (r.status === 0) {
811
+ openaiSub.expiresAt = null;
812
+ saveProfile(profile, { cwd });
813
+ console.log(' ✓ OpenAI re-authenticated');
814
+ }
815
+ }
816
+ }
817
+ console.log('');
729
818
 
730
- const recentSessions = importReplitSessions(cwd).slice(0, 5);
819
+ const recentSessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 7);
731
820
 
732
821
  if (recentSessions.length > 0) {
733
822
  console.log('Recent:');
734
823
  recentSessions.forEach((sess, i) => {
735
- const activeIndicator = sess.isActive ? ' ' : '';
736
- console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${sess.name}${activeIndicator}`);
824
+ const pin = sess.pinned ? '📌 ' : ' ';
825
+ const active = sess.isActive ? ' ●' : '';
826
+ const cat = sess.category ? ` [${sess.category}]` : '';
827
+ console.log(` [${i + 1}] ${pin}${sess.age.padEnd(6)} ${sess.name}${active}${cat}`);
737
828
  });
738
829
  console.log('');
739
830
  }
@@ -743,6 +834,7 @@ async function mainScreen(rl, ask) {
743
834
  if (recentSessions.length > 0) {
744
835
  console.log(' [1-9] Resume numbered above');
745
836
  }
837
+ console.log(' [e] Manage sessions');
746
838
  console.log(' [d] Switch to data-tools');
747
839
  if (!auth.claude.found) console.log(' [j] Login to Claude');
748
840
  if (!auth.openai.found) console.log(' [k] Login to Codex');
@@ -775,6 +867,8 @@ async function mainScreen(rl, ask) {
775
867
  return { next: 'main' };
776
868
  }
777
869
 
870
+ if (choice === 'e') { return { next: 'sessions' }; }
871
+
778
872
  if (choice === 'd') {
779
873
  const { spawnSync } = await import('node:child_process');
780
874
  const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
@@ -854,15 +948,24 @@ async function settingsScreen(rl, ask) {
854
948
 
855
949
  const modeLabel = (m) => m === profile.mode ? `${m} (active)` : m;
856
950
 
951
+ const claudeSub = profile?.providers?.claude;
952
+ const openaiSub = profile?.providers?.openai;
953
+ const claudePlanLabel = claudeSub?.enabled
954
+ ? (CLAUDE_PLAN_LABELS[claudeSub.plan] ?? claudeSub.plan ?? 'n/a')
955
+ : 'disabled';
956
+ const openaiPlanLabel = openaiSub?.enabled
957
+ ? (OPENAI_PLAN_LABELS[openaiSub.plan] ?? openaiSub.plan ?? 'n/a')
958
+ : 'disabled';
959
+
857
960
  const settingsLines = [
858
961
  `Mode:`,
859
962
  ` [1] ${modeLabel('cost-saver')}`,
860
963
  ` [2] ${modeLabel('balanced')}`,
861
964
  ` [3] ${modeLabel('quality-first')}`,
862
965
  '',
863
- `Auth:`,
864
- ` Claude: ${auth.claude.found ? `connected (${auth.claude.source})` : 'missing'}`,
865
- ` OpenAI: ${auth.openai.found ? `connected (${auth.openai.source})` : 'missing'}`,
966
+ `Subscriptions:`,
967
+ ` Claude: ${auth.claude.found ? 'logged in' : 'not logged in'} — ${claudePlanLabel}${claudeSub?.label ? ` [${claudeSub.label}]` : ''}`,
968
+ ` OpenAI: ${auth.openai.found ? 'logged in' : 'not logged in'} — ${openaiPlanLabel}${openaiSub?.label ? ` [${openaiSub.label}]` : ''}`,
866
969
  '',
867
970
  `Enforcement: ${guardCount}/4 guards active`,
868
971
  ];
@@ -871,12 +974,12 @@ async function settingsScreen(rl, ask) {
871
974
  console.log(box('Settings', settingsLines));
872
975
  console.log('');
873
976
  console.log(menu([
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: '' },
977
+ { key: '1', label: 'Switch to cost-saver', section: 'Mode' },
978
+ { key: '2', label: 'Switch to balanced', section: 'Mode' },
979
+ { key: '3', label: 'Switch to quality-first', section: 'Mode' },
980
+ { key: 'a', label: 'Manage subscriptions', section: 'Subscriptions' },
981
+ { key: 'i', label: 'Reinstall hooks', section: 'Enforcement' },
982
+ { key: 'b', label: 'Back', section: '' },
880
983
  ]));
881
984
  console.log('');
882
985
 
@@ -891,8 +994,7 @@ async function settingsScreen(rl, ask) {
891
994
  }
892
995
 
893
996
  if (choice === 'a') {
894
- await setupAuth(rl);
895
- return { next: 'settings' };
997
+ return { next: 'subscriptions' };
896
998
  }
897
999
 
898
1000
  if (choice === 'i') {
@@ -905,63 +1007,168 @@ async function settingsScreen(rl, ask) {
905
1007
  return { next: 'settings' };
906
1008
  }
907
1009
 
1010
+ // ─── Screen: subscriptionsScreen ─────────────────────────────────────────────
1011
+
1012
+ async function subscriptionsScreen(rl, ask) {
1013
+ const cwd = process.cwd();
1014
+ const profile = loadProfile(cwd);
1015
+ const auth = await detectAuth();
1016
+ const plans = detectPlans();
1017
+
1018
+ const claudeSub = profile?.providers?.claude;
1019
+ const openaiSub = profile?.providers?.openai;
1020
+
1021
+ const claudePlanLabel = claudeSub?.enabled
1022
+ ? (CLAUDE_PLAN_LABELS[claudeSub.plan] ?? claudeSub.plan ?? 'n/a')
1023
+ : 'disabled';
1024
+ const openaiPlanLabel = openaiSub?.enabled
1025
+ ? (OPENAI_PLAN_LABELS[openaiSub.plan] ?? openaiSub.plan ?? 'n/a')
1026
+ : 'disabled';
1027
+
1028
+ const subLines = [
1029
+ `Claude: ${auth.claude.found ? 'logged in' : 'not logged in'} — ${claudePlanLabel}`,
1030
+ claudeSub?.label ? ` label: ${claudeSub.label}` : '',
1031
+ claudeSub?.expiresAt ? ` expires: ${claudeSub.expiresAt.slice(0, 10)}` : '',
1032
+ '',
1033
+ `OpenAI: ${auth.openai.found ? 'logged in' : 'not logged in'} — ${openaiPlanLabel}`,
1034
+ openaiSub?.label ? ` label: ${openaiSub.label}` : '',
1035
+ openaiSub?.expiresAt ? ` expires: ${openaiSub.expiresAt.slice(0, 10)}` : '',
1036
+ ].filter(line => line !== '');
1037
+
1038
+ console.log('');
1039
+ console.log(box('Subscriptions', subLines));
1040
+ console.log('');
1041
+ console.log(menu([
1042
+ { key: 'd', label: 'Re-detect from CLI', section: '' },
1043
+ { key: 'c', label: 'Set Claude plan tier', section: '' },
1044
+ { key: 'o', label: 'Set OpenAI plan tier', section: '' },
1045
+ { key: 't', label: 'Set team label/expiry',section: '' },
1046
+ { key: 'b', label: 'Back to settings', section: '' },
1047
+ ]));
1048
+ console.log('');
1049
+
1050
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1051
+
1052
+ if (choice === 'd') {
1053
+ // Re-detect from CLI config files
1054
+ if (plans.claude && claudeSub) {
1055
+ profile.providers.claude.plan = plans.claude;
1056
+ console.log(` Detected Claude: ${CLAUDE_PLAN_LABELS[plans.claude] ?? plans.claude}`);
1057
+ }
1058
+ if (plans.openai && openaiSub) {
1059
+ profile.providers.openai.plan = plans.openai;
1060
+ console.log(` Detected OpenAI: ${OPENAI_PLAN_LABELS[plans.openai] ?? plans.openai}`);
1061
+ }
1062
+ saveProfile(profile, { cwd });
1063
+ return { next: 'subscriptions' };
1064
+ }
1065
+
1066
+ if (choice === 'c') {
1067
+ console.log('');
1068
+ console.log(' Claude plan:');
1069
+ console.log(' (1) Pro ($20/mo)');
1070
+ console.log(' (2) Max x5 ($100/mo)');
1071
+ console.log(' (3) Max x20 ($200/mo)');
1072
+ const c = (await ask(' > ')).trim();
1073
+ const planMap = { '1': 'pro', '2': 'max5', '3': 'max20' };
1074
+ if (planMap[c]) {
1075
+ if (!profile.providers.claude) profile.providers.claude = { enabled: true };
1076
+ profile.providers.claude.plan = planMap[c];
1077
+ profile.providers.claude.enabled = true;
1078
+ saveProfile(profile, { cwd });
1079
+ console.log(` Claude plan set to: ${CLAUDE_PLAN_LABELS[planMap[c]]}`);
1080
+ }
1081
+ return { next: 'subscriptions' };
1082
+ }
1083
+
1084
+ if (choice === 'o') {
1085
+ console.log('');
1086
+ console.log(' OpenAI plan:');
1087
+ console.log(' (1) Plus ($20/mo)');
1088
+ console.log(' (2) Pro ($100/mo)');
1089
+ console.log(' (3) Pro ($200/mo higher limits)');
1090
+ const c = (await ask(' > ')).trim();
1091
+ const planMap = { '1': 'plus', '2': 'pro', '3': 'pro200' };
1092
+ if (planMap[c]) {
1093
+ if (!profile.providers.openai) profile.providers.openai = { enabled: true };
1094
+ profile.providers.openai.plan = planMap[c];
1095
+ profile.providers.openai.enabled = true;
1096
+ saveProfile(profile, { cwd });
1097
+ console.log(` OpenAI plan set to: ${OPENAI_PLAN_LABELS[planMap[c]]}`);
1098
+ }
1099
+ return { next: 'subscriptions' };
1100
+ }
1101
+
1102
+ if (choice === 't') {
1103
+ // Team label/expiry for each provider
1104
+ for (const provider of ['claude', 'openai']) {
1105
+ const prov = profile.providers[provider];
1106
+ if (!prov?.enabled) continue;
1107
+ const provLabel = provider === 'claude' ? 'Claude' : 'OpenAI';
1108
+ const currentLabel = prov.label || '';
1109
+ const label = (await ask(` ${provLabel} label [${currentLabel || 'none'}]: `)).trim();
1110
+ if (label === '-') { delete prov.label; }
1111
+ else if (label) { prov.label = label; }
1112
+ const expiry = await askExpiry(ask, provLabel);
1113
+ if (expiry) { prov.expiresAt = expiry; }
1114
+ }
1115
+ saveProfile(profile, { cwd });
1116
+ console.log(' Team config saved.');
1117
+ return { next: 'subscriptions' };
1118
+ }
1119
+
1120
+ if (choice === 'b' || choice === 'back') { return { next: 'settings' }; }
1121
+
1122
+ return { next: 'subscriptions' };
1123
+ }
1124
+
908
1125
  // ─── Screen: dashboardScreen (kept for internal reference, unreachable) ───────
909
1126
 
910
1127
  async function dashboardScreen(rl, ask) {
911
1128
  return { next: 'main' };
912
1129
  }
913
1130
 
914
- // ─── Screen: authScreen ───────────────────────────────────────────────────────
1131
+ // ─── Screen: authScreen — subscription status view ───────────────────────────
915
1132
 
916
1133
  async function authScreen(rl, ask) {
1134
+ const cwd = process.cwd();
917
1135
  const auth = await detectAuth();
1136
+ const profile = loadProfile(cwd);
1137
+
1138
+ const claudeSub = profile?.providers?.claude;
1139
+ const openaiSub = profile?.providers?.openai;
1140
+ const claudePlanLabel = claudeSub?.enabled
1141
+ ? (CLAUDE_PLAN_LABELS[claudeSub.plan] ?? claudeSub.plan ?? 'n/a')
1142
+ : 'disabled';
1143
+ const openaiPlanLabel = openaiSub?.enabled
1144
+ ? (OPENAI_PLAN_LABELS[openaiSub.plan] ?? openaiSub.plan ?? 'n/a')
1145
+ : 'disabled';
918
1146
 
919
1147
  const authLines = [
920
1148
  'Claude:',
1149
+ auth.claude.found
1150
+ ? ` logged in via ${auth.claude.source}`
1151
+ : ` not logged in — run: claude login`,
1152
+ ` plan: ${claudePlanLabel}${claudeSub?.label ? ` [${claudeSub.label}]` : ''}`,
1153
+ '',
1154
+ 'OpenAI:',
1155
+ auth.openai.found
1156
+ ? ` logged in via ${auth.openai.source}`
1157
+ : ` not logged in — run: codex login`,
1158
+ ` plan: ${openaiPlanLabel}${openaiSub?.label ? ` [${openaiSub.label}]` : ''}`,
921
1159
  ];
922
1160
 
923
- if (auth.claude.found) {
924
- authLines.push(` source: ${auth.claude.source} ${badge('connected')}`);
925
- authLines.push(` key: ${auth.claude.masked}`);
926
- } else {
927
- authLines.push(` not configured ${badge('missing')}`);
928
- }
929
-
930
- authLines.push('');
931
- authLines.push('OpenAI:');
932
-
933
- if (auth.openai.found) {
934
- authLines.push(` source: ${auth.openai.source} ${badge('connected')}`);
935
- authLines.push(` key: ${auth.openai.masked}`);
936
- } else {
937
- authLines.push(` not configured ${badge('missing')}`);
938
- }
939
-
940
- console.log(box('🔑 Auth Management', authLines));
1161
+ console.log(box('Subscription Status', authLines));
941
1162
  console.log('');
942
1163
  console.log(menu([
943
- { key: 'a', label: 'Add API key', section: '' },
944
- { key: 't', label: 'Test keys', section: '' },
945
- { key: 'b', label: 'Back to dashboard', section: '' },
1164
+ { key: 'a', label: 'Manage subscriptions', section: '' },
1165
+ { key: 'b', label: 'Back to dashboard', section: '' },
946
1166
  ]));
947
1167
  console.log('');
948
1168
 
949
1169
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
950
1170
 
951
- if (choice === 'a') {
952
- await setupAuth(rl);
953
- return { next: 'auth' }; // refresh
954
- }
955
-
956
- if (choice === 't') {
957
- console.log('\n Testing auth...');
958
- const authNow = await detectAuth();
959
- console.log(` Claude: ${authNow.claude.found ? 'OK — ' + authNow.claude.source : 'NOT FOUND'}`);
960
- console.log(` OpenAI: ${authNow.openai.found ? 'OK — ' + authNow.openai.source : 'NOT FOUND'}`);
961
- await ask('\n Press Enter to continue...');
962
- return { next: 'auth' };
963
- }
964
-
1171
+ if (choice === 'a') { return { next: 'subscriptions' }; }
965
1172
  if (choice === 'b' || choice === 'back') { return { next: 'dashboard' }; }
966
1173
 
967
1174
  return { next: 'auth' };
@@ -1031,7 +1238,6 @@ async function diagnosticsScreen(rl, ask) {
1031
1238
  const cwd = process.cwd();
1032
1239
  const { spawnSync: _spawnSync } = await import('child_process');
1033
1240
  const { readdirSync } = await import('node:fs');
1034
- const { detectPlans } = await import('../src/profile.mjs');
1035
1241
 
1036
1242
  // ── Version info ──────────────────────────────────────────────────────────
1037
1243
  const version = readVersion();
@@ -1044,20 +1250,18 @@ async function diagnosticsScreen(rl, ask) {
1044
1250
 
1045
1251
  function _providerBadge(name) {
1046
1252
  const entries = Object.entries(healthStates).filter(([k]) => k.startsWith(`${name}:`));
1047
- if (entries.length === 0) return 'healthy';
1253
+ if (entries.length === 0) return 'healthy';
1048
1254
  const statuses = entries.map(([, v]) => v.status);
1049
- if (statuses.includes('hot')) return '🔴 hot';
1050
- if (statuses.includes('degraded')) return '⚠️ degraded';
1051
- if (statuses.includes('probing')) return '⚠️ probing';
1052
- return 'healthy';
1255
+ if (statuses.includes('hot')) return 'hot';
1256
+ if (statuses.includes('degraded')) return 'degraded';
1257
+ if (statuses.includes('probing')) return 'probing';
1258
+ return 'healthy';
1053
1259
  }
1054
1260
 
1055
- const claudeStatus = auth.claude.found ? _providerBadge('claude') : ' no auth';
1056
- const openaiStatus = auth.openai.found ? _providerBadge('openai') : ' no auth';
1057
- const claudePlanStr = plans.claude ? `Max ${plans.claude}` : (auth.claude.masked ?? 'unknown');
1058
- const openaiPlanStr = plans.openai ? `Pro ${plans.openai}` : (auth.openai.masked ?? 'unknown');
1059
- const claudeAuthStr = auth.claude.masked ?? 'not configured';
1060
- const openaiAuthStr = auth.openai.masked ?? 'not configured';
1261
+ const claudeHealthBadge = auth.claude.found ? _providerBadge('claude') : 'not logged in';
1262
+ const openaiHealthBadge = auth.openai.found ? _providerBadge('openai') : 'not logged in';
1263
+ const claudePlanStr = plans.claude ? (CLAUDE_PLAN_LABELS[plans.claude] ?? plans.claude) : 'unknown';
1264
+ const openaiPlanStr = plans.openai ? (OPENAI_PLAN_LABELS[plans.openai] ?? plans.openai) : 'unknown';
1061
1265
 
1062
1266
  // ── Enforcement checks ────────────────────────────────────────────────────
1063
1267
  const hooksDir = join(cwd, '.claude', 'hooks');
@@ -1158,7 +1362,6 @@ async function diagnosticsScreen(rl, ask) {
1158
1362
  // ── Render ────────────────────────────────────────────────────────────────
1159
1363
  const W = 56;
1160
1364
  const hbar = '═'.repeat(W);
1161
- // Pad a string to exactly W visible columns (for box rows without leading ║ prefix)
1162
1365
  const padRow = (s) => {
1163
1366
  const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
1164
1367
  let vlen = 0;
@@ -1173,36 +1376,36 @@ async function diagnosticsScreen(rl, ask) {
1173
1376
 
1174
1377
  const output = [
1175
1378
  `╔${hbar}╗`,
1176
- hrow('🔧 Diagnostics'),
1379
+ hrow('Diagnostics'),
1177
1380
  `╠${hbar}╣`,
1178
1381
  hrow(`dual-brain v${version}`),
1179
1382
  hrow(`Node.js ${nodeVersion}`),
1180
1383
  `╚${hbar}╝`,
1181
1384
  '',
1182
- separator('Provider Health'),
1183
- ` ${claudeStatus.padEnd(14)} Claude ${claudePlanStr.padEnd(16)} ${claudeAuthStr}`,
1184
- ` ${openaiStatus.padEnd(14)} OpenAI ${openaiPlanStr.padEnd(16)} ${openaiAuthStr}`,
1385
+ separator('Provider Status'),
1386
+ ` Claude: ${claudeHealthBadge.padEnd(14)} ${claudePlanStr}`,
1387
+ ` OpenAI: ${openaiHealthBadge.padEnd(14)} ${openaiPlanStr}`,
1185
1388
  '',
1186
1389
  separator('Enforcement'),
1187
- ` ${headGuardExists ? '' : ''} head-guard.mjs ${headGuardExists ? 'installed' : 'missing — run: dual-brain install'}`,
1188
- ` ${enforceTierExists ? '' : ''} enforce-tier.mjs ${enforceTierExists ? 'installed' : 'missing — run: dual-brain install'}`,
1189
- ` ${guardCount === 4 ? '' : '⚠️ '} settings.json ${guardCount}/4 guards registered${guardCount < 4 ? ' — run: dual-brain install' : ''}`,
1190
- ` ${hookifyCount > 0 ? '' : '⚠️ '} hookify rules ${hookifyCount} rules${hookifyCount > 0 ? ' (check: ls .claude/hookify.*.md)' : ' — none found'}`,
1390
+ ` ${headGuardExists ? 'ok' : 'MISSING'} head-guard.mjs ${headGuardExists ? 'installed' : 'run: dual-brain install'}`,
1391
+ ` ${enforceTierExists ? 'ok' : 'MISSING'} enforce-tier.mjs ${enforceTierExists ? 'installed' : 'run: dual-brain install'}`,
1392
+ ` ${guardCount === 4 ? 'ok' : 'PARTIAL'} settings.json ${guardCount}/4 guards registered${guardCount < 4 ? ' — run: dual-brain install' : ''}`,
1393
+ ` ${hookifyCount > 0 ? 'ok' : 'WARN '} hookify rules ${hookifyCount} rules${hookifyCount > 0 ? '' : ' — none found'}`,
1191
1394
  '',
1192
1395
  separator('Replit Tools'),
1193
- ` ${hasReplitTools ? '' : ''} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
1396
+ ` ${hasReplitTools ? 'ok' : 'n/a'} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
1194
1397
  ];
1195
1398
 
1196
1399
  if (hasReplitTools) {
1197
1400
  if (credsFresh === null) {
1198
- output.push(' ⚠️ Claude auth credentials file missing');
1401
+ output.push(' WARN Claude auth credentials file missing');
1199
1402
  } else if (credsFresh) {
1200
- output.push(` Claude auth fresh (expires: ${credsExpiry})`);
1403
+ output.push(` ok Claude auth fresh (expires: ${credsExpiry})`);
1201
1404
  } else {
1202
- output.push(` Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
1405
+ output.push(` ERROR Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
1203
1406
  }
1204
- output.push(` Session archive ${historyCount} entries`);
1205
- output.push(` ${sessionManagerExists ? '' : '⚠️ '} Session manager ${sessionManagerExists ? 'available' : 'not found'}`);
1407
+ output.push(` ok Session archive ${historyCount} entries`);
1408
+ output.push(` ${sessionManagerExists ? 'ok' : 'WARN '} Session manager ${sessionManagerExists ? 'available' : 'not found'}`);
1206
1409
  } else {
1207
1410
  output.push(' ─── (not available)');
1208
1411
  }
@@ -1210,14 +1413,14 @@ async function diagnosticsScreen(rl, ask) {
1210
1413
  output.push('');
1211
1414
  output.push(separator('Quality'));
1212
1415
  if (testError) {
1213
- output.push(` Tests error: ${testError}`);
1416
+ output.push(` ERROR Tests error: ${testError}`);
1214
1417
  } else if (testPass !== null) {
1215
- output.push(` ${testPass === testTotal ? '' : ''} Tests ${testPass}/${testTotal} passing`);
1418
+ output.push(` ${testPass === testTotal ? 'ok ' : 'FAIL '} Tests ${testPass}/${testTotal} passing`);
1216
1419
  }
1217
1420
  if (healthError) {
1218
- output.push(` Health check error: ${healthError}`);
1421
+ output.push(` ERROR Health check error: ${healthError}`);
1219
1422
  } else if (healthPass !== null) {
1220
- output.push(` ${healthPass === healthTotal ? '' : '⚠️ '} Health check ${healthPass}/${healthTotal} passing`);
1423
+ output.push(` ${healthPass === healthTotal ? 'ok ' : 'WARN '} Health check ${healthPass}/${healthTotal} passing`);
1221
1424
  }
1222
1425
  output.push('');
1223
1426
 
@@ -1309,10 +1512,8 @@ async function replScreen(rl, ask) {
1309
1512
  printHelp();
1310
1513
  } else if (line === 'status') {
1311
1514
  await cmdStatus([]);
1312
- } else if (line === 'auth setup' || line === 'auth-setup') {
1313
- await cmdAuthSetup(rl);
1314
1515
  } else if (line === 'auth') {
1315
- await cmdAuth([], rl);
1516
+ await cmdAuth([]);
1316
1517
  } else if (line.startsWith('go ')) {
1317
1518
  await cmdGo(line.slice(3).trim().split(/\s+/));
1318
1519
  } else if (line.startsWith('remember ')) {
@@ -1396,6 +1597,131 @@ async function sessionDetailScreen(rl, ask, ctx = {}) {
1396
1597
  return { next: 'dashboard' };
1397
1598
  }
1398
1599
 
1600
+ // ─── Screen: sessionsScreen ───────────────────────────────────────────────────
1601
+
1602
+ const CATEGORIES = ['security', 'ui', 'refactor', 'bugfix', 'testing', 'devops', 'planning'];
1603
+
1604
+ async function sessionsScreen(rl, ask) {
1605
+ const cwd = process.cwd();
1606
+ const sessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 9);
1607
+
1608
+ console.log('');
1609
+ console.log(separator('Session Manager'));
1610
+ console.log('');
1611
+
1612
+ if (sessions.length === 0) {
1613
+ console.log(' No sessions found.\n');
1614
+ console.log(' [b] Back\n');
1615
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1616
+ if (choice === 'b' || choice === 'back') return { next: 'main' };
1617
+ return { next: 'sessions' };
1618
+ }
1619
+
1620
+ sessions.forEach((sess, i) => {
1621
+ const pin = sess.pinned ? '📌 ' : ' ';
1622
+ const active = sess.isActive ? ' ●' : '';
1623
+ const cat = sess.category ? ` [${sess.category}]` : '';
1624
+ console.log(` [${i + 1}] ${pin}${sess.age.padEnd(6)} ${sess.name}${active}${cat}`);
1625
+ });
1626
+
1627
+ console.log('');
1628
+ console.log(' [1-9] Select a session to manage');
1629
+ console.log(' [b] Back');
1630
+ console.log('');
1631
+
1632
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1633
+
1634
+ if (choice === 'b' || choice === 'back') return { next: 'main' };
1635
+
1636
+ const numChoice = parseInt(choice, 10);
1637
+ if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= sessions.length) {
1638
+ return { next: 'session-manage', session: sessions[numChoice - 1] };
1639
+ }
1640
+
1641
+ return { next: 'sessions' };
1642
+ }
1643
+
1644
+ async function sessionManageScreen(rl, ask, ctx = {}) {
1645
+ const sess = ctx.session;
1646
+ if (!sess) return { next: 'sessions' };
1647
+
1648
+ const cwd = process.cwd();
1649
+ const pinLabel = sess.pinned ? 'Unpin' : 'Pin';
1650
+ const catLabel = sess.category ? `[${sess.category}]` : '(none)';
1651
+
1652
+ console.log('');
1653
+ console.log(separator(`Session: ${sess.name}`));
1654
+ console.log('');
1655
+ console.log(` Age: ${sess.age}`);
1656
+ console.log(` Category: ${catLabel}`);
1657
+ console.log(` Pinned: ${sess.pinned ? 'yes' : 'no'}`);
1658
+ console.log('');
1659
+ console.log(menu([
1660
+ { key: 'r', label: 'Rename', section: '' },
1661
+ { key: 'p', label: pinLabel, section: '' },
1662
+ { key: 'c', label: 'Set category', section: '' },
1663
+ { key: 'o', label: 'Open (resume)', section: '' },
1664
+ { key: 'b', label: 'Back', section: '' },
1665
+ ]));
1666
+ console.log('');
1667
+
1668
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1669
+
1670
+ if (choice === 'r') {
1671
+ const name = (await ask(' New name: ')).trim();
1672
+ if (name) {
1673
+ renameSession(sess.id, name, cwd);
1674
+ console.log(` Renamed to: ${name}`);
1675
+ }
1676
+ return { next: 'session-manage', session: { ...sess, name: name || sess.name } };
1677
+ }
1678
+
1679
+ if (choice === 'p') {
1680
+ if (sess.pinned) {
1681
+ unpinSession(sess.id, cwd);
1682
+ console.log(' Unpinned.');
1683
+ return { next: 'session-manage', session: { ...sess, pinned: false } };
1684
+ } else {
1685
+ pinSession(sess.id, cwd);
1686
+ console.log(' Pinned.');
1687
+ return { next: 'session-manage', session: { ...sess, pinned: true } };
1688
+ }
1689
+ }
1690
+
1691
+ if (choice === 'c') {
1692
+ console.log('');
1693
+ CATEGORIES.forEach((cat, i) => console.log(` (${i + 1}) ${cat}`));
1694
+ console.log(` (${CATEGORIES.length + 1}) custom`);
1695
+ console.log('');
1696
+ const catChoice = (await ask(' Category: ')).trim();
1697
+ const catIndex = parseInt(catChoice, 10);
1698
+ let category = null;
1699
+ if (!isNaN(catIndex) && catIndex >= 1 && catIndex <= CATEGORIES.length) {
1700
+ category = CATEGORIES[catIndex - 1];
1701
+ } else if (catIndex === CATEGORIES.length + 1) {
1702
+ category = (await ask(' Custom category: ')).trim() || null;
1703
+ } else if (catChoice) {
1704
+ category = catChoice;
1705
+ }
1706
+ if (category) {
1707
+ categorizeSession(sess.id, category, cwd);
1708
+ console.log(` Category set to: ${category}`);
1709
+ }
1710
+ return { next: 'session-manage', session: { ...sess, category: category ?? sess.category } };
1711
+ }
1712
+
1713
+ if (choice === 'o') {
1714
+ const { spawnSync } = await import('node:child_process');
1715
+ console.log(`\n Launching: claude --resume ${sess.id}\n`);
1716
+ spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
1717
+ return { next: 'sessions' };
1718
+ }
1719
+
1720
+ if (choice === 'b' || choice === 'back') return { next: 'sessions' };
1721
+
1722
+ return { next: 'session-manage', session: sess };
1723
+ }
1724
+
1399
1725
  // ─── Screen state machine ─────────────────────────────────────────────────────
1400
1726
 
1401
1727
  const SCREENS = {
@@ -1403,12 +1729,15 @@ const SCREENS = {
1403
1729
  main: mainScreen,
1404
1730
  'new-session': newSessionScreen,
1405
1731
  settings: settingsScreen,
1732
+ subscriptions: subscriptionsScreen,
1406
1733
  dashboard: dashboardScreen,
1407
1734
  auth: authScreen,
1408
1735
  profile: profileScreen,
1409
1736
  diagnostics: diagnosticsScreen,
1410
1737
  repl: replScreen,
1411
1738
  'session-detail': sessionDetailScreen,
1739
+ sessions: sessionsScreen,
1740
+ 'session-manage': sessionManageScreen,
1412
1741
  };
1413
1742
 
1414
1743
  async function runScreens(startScreen = 'dashboard') {
@@ -1481,8 +1810,6 @@ async function main() {
1481
1810
  // One-shot commands — run and exit
1482
1811
  if (cmd === 'install') { await cmdInstall(); return; }
1483
1812
  if (cmd === 'auth') {
1484
- const sub = args[1];
1485
- if (sub === 'setup') { await cmdAuthSetup(); return; }
1486
1813
  await cmdAuth(args.slice(1));
1487
1814
  return;
1488
1815
  }
@@ -1493,6 +1820,21 @@ async function main() {
1493
1820
  if (cmd === 'remember') { cmdRemember(args[1]); return; }
1494
1821
  if (cmd === 'forget') { cmdForget(args[1]); return; }
1495
1822
 
1823
+ if (cmd === 'shell-hook') {
1824
+ // Output a bash snippet users can add to their .bashrc or source directly.
1825
+ const hook = `
1826
+ # dual-brain shell integration
1827
+ # Source this file or add to .bashrc
1828
+ if command -v dual-brain &>/dev/null; then
1829
+ alias db='dual-brain'
1830
+ alias dbgo='dual-brain go'
1831
+ alias dbstat='dual-brain status'
1832
+ fi
1833
+ `.trim();
1834
+ console.log(hook);
1835
+ return;
1836
+ }
1837
+
1496
1838
  process.stderr.write(`Unknown command: ${cmd}\nRun "dual-brain --help" for usage.\n`);
1497
1839
  process.exit(1);
1498
1840
  }