dual-brain 7.1.6 → 7.1.8

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
+ // Show detection results in a box
602
+ const detectedFormatted = detectedLines.map(line => {
603
+ const ok = !line.includes('not logged');
604
+ return `${ok ? '✅' : '⚠️ '} ${line.trim()}`;
605
+ });
606
+ console.log('');
607
+ console.log(box(`🧠 Dual-Brain v${version} — Setup`, detectedFormatted));
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,34 +762,87 @@ 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 subLine(name, plan, found, expired, days, sub) {
779
+ const label = sub?.label ? ` [${sub.label}]` : '';
780
+ if (!found) return `⚠️ ${name}: not logged in — run: ${name === 'Claude' ? 'claude login' : 'codex login'}`;
781
+ if (expired) return `🔴 ${name}: ${plan} expired${label} — will re-auth`;
782
+ const daysNote = (days !== null && days <= 7) ? ` (${days}d left)` : '';
783
+ return `✅ ${name}: ${plan}${label}${daysNote}`;
784
+ }
726
785
 
727
- console.log(`\ndual-brain v${version}`);
728
- console.log(`${claudeStatus} · ${openaiStatus}\n`);
786
+ const headerLines = [
787
+ subLine('Claude', claudePlan, auth.claude.found, claudeExpired, claudeDays, claudeSub),
788
+ subLine('OpenAI', openaiPlan, auth.openai.found, openaiExpired, openaiDays, openaiSub),
789
+ ];
729
790
 
730
- const recentSessions = importReplitSessions(cwd).slice(0, 5);
791
+ console.log('');
792
+ console.log(box(`🧠 dual-brain v${version}`, headerLines));
793
+
794
+ // Auto-refresh expired subscriptions
795
+ if (claudeExpired || openaiExpired) {
796
+ const { spawnSync } = await import('node:child_process');
797
+ const expired = [];
798
+ if (claudeExpired) expired.push('Claude');
799
+ if (openaiExpired) expired.push('OpenAI');
800
+ console.log(`\n ${expired.join(' & ')} subscription expired. Re-authenticating...`);
801
+ if (claudeExpired) {
802
+ const r = spawnSync('claude', ['login'], { stdio: 'inherit', timeout: 30000 });
803
+ if (r.status === 0) {
804
+ claudeSub.expiresAt = null;
805
+ saveProfile(profile, { cwd });
806
+ console.log(' ✓ Claude re-authenticated');
807
+ }
808
+ }
809
+ if (openaiExpired) {
810
+ const r = spawnSync('codex', ['login'], { stdio: 'inherit', timeout: 30000 });
811
+ if (r.status === 0) {
812
+ openaiSub.expiresAt = null;
813
+ saveProfile(profile, { cwd });
814
+ console.log(' ✓ OpenAI re-authenticated');
815
+ }
816
+ }
817
+ }
818
+ console.log('');
819
+
820
+ const recentSessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 7);
731
821
 
732
822
  if (recentSessions.length > 0) {
733
- console.log('Recent:');
823
+ console.log(separator('Recent Sessions'));
734
824
  recentSessions.forEach((sess, i) => {
735
- const activeIndicator = sess.isActive ? ' ' : '';
736
- console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${sess.name}${activeIndicator}`);
825
+ const pin = sess.pinned ? '📌 ' : ' ';
826
+ const active = sess.isActive ? ' ●' : '';
827
+ const cat = sess.category ? ` [${sess.category}]` : '';
828
+ console.log(` [${i + 1}] ${pin}${sess.age.padEnd(6)} ${sess.name}${active}${cat}`);
737
829
  });
738
830
  console.log('');
739
831
  }
740
832
 
741
- console.log(' [c] Continue last session');
742
- console.log(' [n] New session');
833
+ const menuOpts = [];
834
+ menuOpts.push({ key: 'c', label: 'Continue last session', section: 'Sessions' });
835
+ menuOpts.push({ key: 'n', label: 'New session', section: 'Sessions' });
743
836
  if (recentSessions.length > 0) {
744
- console.log(' [1-9] Resume numbered above');
837
+ menuOpts.push({ key: '1-9', label: 'Resume numbered above', section: 'Sessions' });
745
838
  }
746
- console.log(' [d] Switch to data-tools');
747
- if (!auth.claude.found) console.log(' [j] Login to Claude');
748
- if (!auth.openai.found) console.log(' [k] Login to Codex');
749
- console.log(' [s] Settings [q] Exit');
839
+ menuOpts.push({ key: 'e', label: 'Manage sessions', section: 'Sessions' });
840
+ menuOpts.push({ key: 'd', label: 'Switch to data-tools', section: 'Tools' });
841
+ menuOpts.push({ key: 'j', label: 'Login to Claude', section: 'Auth' });
842
+ menuOpts.push({ key: 'k', label: 'Login to Codex', section: 'Auth' });
843
+ menuOpts.push({ key: 's', label: 'Settings', section: '' });
844
+ menuOpts.push({ key: 'q', label: 'Exit', section: '' });
845
+ console.log(menu(menuOpts));
750
846
  console.log('');
751
847
 
752
848
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
@@ -775,6 +871,8 @@ async function mainScreen(rl, ask) {
775
871
  return { next: 'main' };
776
872
  }
777
873
 
874
+ if (choice === 'e') { return { next: 'sessions' }; }
875
+
778
876
  if (choice === 'd') {
779
877
  const { spawnSync } = await import('node:child_process');
780
878
  const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
@@ -854,15 +952,24 @@ async function settingsScreen(rl, ask) {
854
952
 
855
953
  const modeLabel = (m) => m === profile.mode ? `${m} (active)` : m;
856
954
 
955
+ const claudeSub = profile?.providers?.claude;
956
+ const openaiSub = profile?.providers?.openai;
957
+ const claudePlanLabel = claudeSub?.enabled
958
+ ? (CLAUDE_PLAN_LABELS[claudeSub.plan] ?? claudeSub.plan ?? 'n/a')
959
+ : 'disabled';
960
+ const openaiPlanLabel = openaiSub?.enabled
961
+ ? (OPENAI_PLAN_LABELS[openaiSub.plan] ?? openaiSub.plan ?? 'n/a')
962
+ : 'disabled';
963
+
857
964
  const settingsLines = [
858
965
  `Mode:`,
859
966
  ` [1] ${modeLabel('cost-saver')}`,
860
967
  ` [2] ${modeLabel('balanced')}`,
861
968
  ` [3] ${modeLabel('quality-first')}`,
862
969
  '',
863
- `Auth:`,
864
- ` Claude: ${auth.claude.found ? `connected (${auth.claude.source})` : 'missing'}`,
865
- ` OpenAI: ${auth.openai.found ? `connected (${auth.openai.source})` : 'missing'}`,
970
+ `Subscriptions:`,
971
+ ` Claude: ${auth.claude.found ? 'logged in' : 'not logged in'} — ${claudePlanLabel}${claudeSub?.label ? ` [${claudeSub.label}]` : ''}`,
972
+ ` OpenAI: ${auth.openai.found ? 'logged in' : 'not logged in'} — ${openaiPlanLabel}${openaiSub?.label ? ` [${openaiSub.label}]` : ''}`,
866
973
  '',
867
974
  `Enforcement: ${guardCount}/4 guards active`,
868
975
  ];
@@ -871,12 +978,12 @@ async function settingsScreen(rl, ask) {
871
978
  console.log(box('Settings', settingsLines));
872
979
  console.log('');
873
980
  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: '' },
981
+ { key: '1', label: 'Switch to cost-saver', section: 'Mode' },
982
+ { key: '2', label: 'Switch to balanced', section: 'Mode' },
983
+ { key: '3', label: 'Switch to quality-first', section: 'Mode' },
984
+ { key: 'a', label: 'Manage subscriptions', section: 'Subscriptions' },
985
+ { key: 'i', label: 'Reinstall hooks', section: 'Enforcement' },
986
+ { key: 'b', label: 'Back', section: '' },
880
987
  ]));
881
988
  console.log('');
882
989
 
@@ -891,8 +998,7 @@ async function settingsScreen(rl, ask) {
891
998
  }
892
999
 
893
1000
  if (choice === 'a') {
894
- await setupAuth(rl);
895
- return { next: 'settings' };
1001
+ return { next: 'subscriptions' };
896
1002
  }
897
1003
 
898
1004
  if (choice === 'i') {
@@ -905,63 +1011,168 @@ async function settingsScreen(rl, ask) {
905
1011
  return { next: 'settings' };
906
1012
  }
907
1013
 
1014
+ // ─── Screen: subscriptionsScreen ─────────────────────────────────────────────
1015
+
1016
+ async function subscriptionsScreen(rl, ask) {
1017
+ const cwd = process.cwd();
1018
+ const profile = loadProfile(cwd);
1019
+ const auth = await detectAuth();
1020
+ const plans = detectPlans();
1021
+
1022
+ const claudeSub = profile?.providers?.claude;
1023
+ const openaiSub = profile?.providers?.openai;
1024
+
1025
+ const claudePlanLabel = claudeSub?.enabled
1026
+ ? (CLAUDE_PLAN_LABELS[claudeSub.plan] ?? claudeSub.plan ?? 'n/a')
1027
+ : 'disabled';
1028
+ const openaiPlanLabel = openaiSub?.enabled
1029
+ ? (OPENAI_PLAN_LABELS[openaiSub.plan] ?? openaiSub.plan ?? 'n/a')
1030
+ : 'disabled';
1031
+
1032
+ const subLines = [
1033
+ `Claude: ${auth.claude.found ? 'logged in' : 'not logged in'} — ${claudePlanLabel}`,
1034
+ claudeSub?.label ? ` label: ${claudeSub.label}` : '',
1035
+ claudeSub?.expiresAt ? ` expires: ${claudeSub.expiresAt.slice(0, 10)}` : '',
1036
+ '',
1037
+ `OpenAI: ${auth.openai.found ? 'logged in' : 'not logged in'} — ${openaiPlanLabel}`,
1038
+ openaiSub?.label ? ` label: ${openaiSub.label}` : '',
1039
+ openaiSub?.expiresAt ? ` expires: ${openaiSub.expiresAt.slice(0, 10)}` : '',
1040
+ ].filter(line => line !== '');
1041
+
1042
+ console.log('');
1043
+ console.log(box('Subscriptions', subLines));
1044
+ console.log('');
1045
+ console.log(menu([
1046
+ { key: 'd', label: 'Re-detect from CLI', section: '' },
1047
+ { key: 'c', label: 'Set Claude plan tier', section: '' },
1048
+ { key: 'o', label: 'Set OpenAI plan tier', section: '' },
1049
+ { key: 't', label: 'Set team label/expiry',section: '' },
1050
+ { key: 'b', label: 'Back to settings', section: '' },
1051
+ ]));
1052
+ console.log('');
1053
+
1054
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1055
+
1056
+ if (choice === 'd') {
1057
+ // Re-detect from CLI config files
1058
+ if (plans.claude && claudeSub) {
1059
+ profile.providers.claude.plan = plans.claude;
1060
+ console.log(` Detected Claude: ${CLAUDE_PLAN_LABELS[plans.claude] ?? plans.claude}`);
1061
+ }
1062
+ if (plans.openai && openaiSub) {
1063
+ profile.providers.openai.plan = plans.openai;
1064
+ console.log(` Detected OpenAI: ${OPENAI_PLAN_LABELS[plans.openai] ?? plans.openai}`);
1065
+ }
1066
+ saveProfile(profile, { cwd });
1067
+ return { next: 'subscriptions' };
1068
+ }
1069
+
1070
+ if (choice === 'c') {
1071
+ console.log('');
1072
+ console.log(' Claude plan:');
1073
+ console.log(' (1) Pro ($20/mo)');
1074
+ console.log(' (2) Max x5 ($100/mo)');
1075
+ console.log(' (3) Max x20 ($200/mo)');
1076
+ const c = (await ask(' > ')).trim();
1077
+ const planMap = { '1': 'pro', '2': 'max5', '3': 'max20' };
1078
+ if (planMap[c]) {
1079
+ if (!profile.providers.claude) profile.providers.claude = { enabled: true };
1080
+ profile.providers.claude.plan = planMap[c];
1081
+ profile.providers.claude.enabled = true;
1082
+ saveProfile(profile, { cwd });
1083
+ console.log(` Claude plan set to: ${CLAUDE_PLAN_LABELS[planMap[c]]}`);
1084
+ }
1085
+ return { next: 'subscriptions' };
1086
+ }
1087
+
1088
+ if (choice === 'o') {
1089
+ console.log('');
1090
+ console.log(' OpenAI plan:');
1091
+ console.log(' (1) Plus ($20/mo)');
1092
+ console.log(' (2) Pro ($100/mo)');
1093
+ console.log(' (3) Pro ($200/mo higher limits)');
1094
+ const c = (await ask(' > ')).trim();
1095
+ const planMap = { '1': 'plus', '2': 'pro', '3': 'pro200' };
1096
+ if (planMap[c]) {
1097
+ if (!profile.providers.openai) profile.providers.openai = { enabled: true };
1098
+ profile.providers.openai.plan = planMap[c];
1099
+ profile.providers.openai.enabled = true;
1100
+ saveProfile(profile, { cwd });
1101
+ console.log(` OpenAI plan set to: ${OPENAI_PLAN_LABELS[planMap[c]]}`);
1102
+ }
1103
+ return { next: 'subscriptions' };
1104
+ }
1105
+
1106
+ if (choice === 't') {
1107
+ // Team label/expiry for each provider
1108
+ for (const provider of ['claude', 'openai']) {
1109
+ const prov = profile.providers[provider];
1110
+ if (!prov?.enabled) continue;
1111
+ const provLabel = provider === 'claude' ? 'Claude' : 'OpenAI';
1112
+ const currentLabel = prov.label || '';
1113
+ const label = (await ask(` ${provLabel} label [${currentLabel || 'none'}]: `)).trim();
1114
+ if (label === '-') { delete prov.label; }
1115
+ else if (label) { prov.label = label; }
1116
+ const expiry = await askExpiry(ask, provLabel);
1117
+ if (expiry) { prov.expiresAt = expiry; }
1118
+ }
1119
+ saveProfile(profile, { cwd });
1120
+ console.log(' Team config saved.');
1121
+ return { next: 'subscriptions' };
1122
+ }
1123
+
1124
+ if (choice === 'b' || choice === 'back') { return { next: 'settings' }; }
1125
+
1126
+ return { next: 'subscriptions' };
1127
+ }
1128
+
908
1129
  // ─── Screen: dashboardScreen (kept for internal reference, unreachable) ───────
909
1130
 
910
1131
  async function dashboardScreen(rl, ask) {
911
1132
  return { next: 'main' };
912
1133
  }
913
1134
 
914
- // ─── Screen: authScreen ───────────────────────────────────────────────────────
1135
+ // ─── Screen: authScreen — subscription status view ───────────────────────────
915
1136
 
916
1137
  async function authScreen(rl, ask) {
1138
+ const cwd = process.cwd();
917
1139
  const auth = await detectAuth();
1140
+ const profile = loadProfile(cwd);
1141
+
1142
+ const claudeSub = profile?.providers?.claude;
1143
+ const openaiSub = profile?.providers?.openai;
1144
+ const claudePlanLabel = claudeSub?.enabled
1145
+ ? (CLAUDE_PLAN_LABELS[claudeSub.plan] ?? claudeSub.plan ?? 'n/a')
1146
+ : 'disabled';
1147
+ const openaiPlanLabel = openaiSub?.enabled
1148
+ ? (OPENAI_PLAN_LABELS[openaiSub.plan] ?? openaiSub.plan ?? 'n/a')
1149
+ : 'disabled';
918
1150
 
919
1151
  const authLines = [
920
1152
  'Claude:',
1153
+ auth.claude.found
1154
+ ? ` logged in via ${auth.claude.source}`
1155
+ : ` not logged in — run: claude login`,
1156
+ ` plan: ${claudePlanLabel}${claudeSub?.label ? ` [${claudeSub.label}]` : ''}`,
1157
+ '',
1158
+ 'OpenAI:',
1159
+ auth.openai.found
1160
+ ? ` logged in via ${auth.openai.source}`
1161
+ : ` not logged in — run: codex login`,
1162
+ ` plan: ${openaiPlanLabel}${openaiSub?.label ? ` [${openaiSub.label}]` : ''}`,
921
1163
  ];
922
1164
 
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));
1165
+ console.log(box('Subscription Status', authLines));
941
1166
  console.log('');
942
1167
  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: '' },
1168
+ { key: 'a', label: 'Manage subscriptions', section: '' },
1169
+ { key: 'b', label: 'Back to dashboard', section: '' },
946
1170
  ]));
947
1171
  console.log('');
948
1172
 
949
1173
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
950
1174
 
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
-
1175
+ if (choice === 'a') { return { next: 'subscriptions' }; }
965
1176
  if (choice === 'b' || choice === 'back') { return { next: 'dashboard' }; }
966
1177
 
967
1178
  return { next: 'auth' };
@@ -1031,7 +1242,6 @@ async function diagnosticsScreen(rl, ask) {
1031
1242
  const cwd = process.cwd();
1032
1243
  const { spawnSync: _spawnSync } = await import('child_process');
1033
1244
  const { readdirSync } = await import('node:fs');
1034
- const { detectPlans } = await import('../src/profile.mjs');
1035
1245
 
1036
1246
  // ── Version info ──────────────────────────────────────────────────────────
1037
1247
  const version = readVersion();
@@ -1044,20 +1254,18 @@ async function diagnosticsScreen(rl, ask) {
1044
1254
 
1045
1255
  function _providerBadge(name) {
1046
1256
  const entries = Object.entries(healthStates).filter(([k]) => k.startsWith(`${name}:`));
1047
- if (entries.length === 0) return 'healthy';
1257
+ if (entries.length === 0) return 'healthy';
1048
1258
  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';
1259
+ if (statuses.includes('hot')) return 'hot';
1260
+ if (statuses.includes('degraded')) return 'degraded';
1261
+ if (statuses.includes('probing')) return 'probing';
1262
+ return 'healthy';
1053
1263
  }
1054
1264
 
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';
1265
+ const claudeHealthBadge = auth.claude.found ? _providerBadge('claude') : 'not logged in';
1266
+ const openaiHealthBadge = auth.openai.found ? _providerBadge('openai') : 'not logged in';
1267
+ const claudePlanStr = plans.claude ? (CLAUDE_PLAN_LABELS[plans.claude] ?? plans.claude) : 'unknown';
1268
+ const openaiPlanStr = plans.openai ? (OPENAI_PLAN_LABELS[plans.openai] ?? plans.openai) : 'unknown';
1061
1269
 
1062
1270
  // ── Enforcement checks ────────────────────────────────────────────────────
1063
1271
  const hooksDir = join(cwd, '.claude', 'hooks');
@@ -1158,7 +1366,6 @@ async function diagnosticsScreen(rl, ask) {
1158
1366
  // ── Render ────────────────────────────────────────────────────────────────
1159
1367
  const W = 56;
1160
1368
  const hbar = '═'.repeat(W);
1161
- // Pad a string to exactly W visible columns (for box rows without leading ║ prefix)
1162
1369
  const padRow = (s) => {
1163
1370
  const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
1164
1371
  let vlen = 0;
@@ -1173,36 +1380,36 @@ async function diagnosticsScreen(rl, ask) {
1173
1380
 
1174
1381
  const output = [
1175
1382
  `╔${hbar}╗`,
1176
- hrow('🔧 Diagnostics'),
1383
+ hrow('Diagnostics'),
1177
1384
  `╠${hbar}╣`,
1178
1385
  hrow(`dual-brain v${version}`),
1179
1386
  hrow(`Node.js ${nodeVersion}`),
1180
1387
  `╚${hbar}╝`,
1181
1388
  '',
1182
- separator('Provider Health'),
1183
- ` ${claudeStatus.padEnd(14)} Claude ${claudePlanStr.padEnd(16)} ${claudeAuthStr}`,
1184
- ` ${openaiStatus.padEnd(14)} OpenAI ${openaiPlanStr.padEnd(16)} ${openaiAuthStr}`,
1389
+ separator('Provider Status'),
1390
+ ` Claude: ${claudeHealthBadge.padEnd(14)} ${claudePlanStr}`,
1391
+ ` OpenAI: ${openaiHealthBadge.padEnd(14)} ${openaiPlanStr}`,
1185
1392
  '',
1186
1393
  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'}`,
1394
+ ` ${headGuardExists ? 'ok' : 'MISSING'} head-guard.mjs ${headGuardExists ? 'installed' : 'run: dual-brain install'}`,
1395
+ ` ${enforceTierExists ? 'ok' : 'MISSING'} enforce-tier.mjs ${enforceTierExists ? 'installed' : 'run: dual-brain install'}`,
1396
+ ` ${guardCount === 4 ? 'ok' : 'PARTIAL'} settings.json ${guardCount}/4 guards registered${guardCount < 4 ? ' — run: dual-brain install' : ''}`,
1397
+ ` ${hookifyCount > 0 ? 'ok' : 'WARN '} hookify rules ${hookifyCount} rules${hookifyCount > 0 ? '' : ' — none found'}`,
1191
1398
  '',
1192
1399
  separator('Replit Tools'),
1193
- ` ${hasReplitTools ? '' : ''} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
1400
+ ` ${hasReplitTools ? 'ok' : 'n/a'} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
1194
1401
  ];
1195
1402
 
1196
1403
  if (hasReplitTools) {
1197
1404
  if (credsFresh === null) {
1198
- output.push(' ⚠️ Claude auth credentials file missing');
1405
+ output.push(' WARN Claude auth credentials file missing');
1199
1406
  } else if (credsFresh) {
1200
- output.push(` Claude auth fresh (expires: ${credsExpiry})`);
1407
+ output.push(` ok Claude auth fresh (expires: ${credsExpiry})`);
1201
1408
  } else {
1202
- output.push(` Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
1409
+ output.push(` ERROR Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
1203
1410
  }
1204
- output.push(` Session archive ${historyCount} entries`);
1205
- output.push(` ${sessionManagerExists ? '' : '⚠️ '} Session manager ${sessionManagerExists ? 'available' : 'not found'}`);
1411
+ output.push(` ok Session archive ${historyCount} entries`);
1412
+ output.push(` ${sessionManagerExists ? 'ok' : 'WARN '} Session manager ${sessionManagerExists ? 'available' : 'not found'}`);
1206
1413
  } else {
1207
1414
  output.push(' ─── (not available)');
1208
1415
  }
@@ -1210,14 +1417,14 @@ async function diagnosticsScreen(rl, ask) {
1210
1417
  output.push('');
1211
1418
  output.push(separator('Quality'));
1212
1419
  if (testError) {
1213
- output.push(` Tests error: ${testError}`);
1420
+ output.push(` ERROR Tests error: ${testError}`);
1214
1421
  } else if (testPass !== null) {
1215
- output.push(` ${testPass === testTotal ? '' : ''} Tests ${testPass}/${testTotal} passing`);
1422
+ output.push(` ${testPass === testTotal ? 'ok ' : 'FAIL '} Tests ${testPass}/${testTotal} passing`);
1216
1423
  }
1217
1424
  if (healthError) {
1218
- output.push(` Health check error: ${healthError}`);
1425
+ output.push(` ERROR Health check error: ${healthError}`);
1219
1426
  } else if (healthPass !== null) {
1220
- output.push(` ${healthPass === healthTotal ? '' : '⚠️ '} Health check ${healthPass}/${healthTotal} passing`);
1427
+ output.push(` ${healthPass === healthTotal ? 'ok ' : 'WARN '} Health check ${healthPass}/${healthTotal} passing`);
1221
1428
  }
1222
1429
  output.push('');
1223
1430
 
@@ -1309,10 +1516,8 @@ async function replScreen(rl, ask) {
1309
1516
  printHelp();
1310
1517
  } else if (line === 'status') {
1311
1518
  await cmdStatus([]);
1312
- } else if (line === 'auth setup' || line === 'auth-setup') {
1313
- await cmdAuthSetup(rl);
1314
1519
  } else if (line === 'auth') {
1315
- await cmdAuth([], rl);
1520
+ await cmdAuth([]);
1316
1521
  } else if (line.startsWith('go ')) {
1317
1522
  await cmdGo(line.slice(3).trim().split(/\s+/));
1318
1523
  } else if (line.startsWith('remember ')) {
@@ -1396,6 +1601,131 @@ async function sessionDetailScreen(rl, ask, ctx = {}) {
1396
1601
  return { next: 'dashboard' };
1397
1602
  }
1398
1603
 
1604
+ // ─── Screen: sessionsScreen ───────────────────────────────────────────────────
1605
+
1606
+ const CATEGORIES = ['security', 'ui', 'refactor', 'bugfix', 'testing', 'devops', 'planning'];
1607
+
1608
+ async function sessionsScreen(rl, ask) {
1609
+ const cwd = process.cwd();
1610
+ const sessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 9);
1611
+
1612
+ console.log('');
1613
+ console.log(separator('Session Manager'));
1614
+ console.log('');
1615
+
1616
+ if (sessions.length === 0) {
1617
+ console.log(' No sessions found.\n');
1618
+ console.log(' [b] Back\n');
1619
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1620
+ if (choice === 'b' || choice === 'back') return { next: 'main' };
1621
+ return { next: 'sessions' };
1622
+ }
1623
+
1624
+ sessions.forEach((sess, i) => {
1625
+ const pin = sess.pinned ? '📌 ' : ' ';
1626
+ const active = sess.isActive ? ' ●' : '';
1627
+ const cat = sess.category ? ` [${sess.category}]` : '';
1628
+ console.log(` [${i + 1}] ${pin}${sess.age.padEnd(6)} ${sess.name}${active}${cat}`);
1629
+ });
1630
+
1631
+ console.log('');
1632
+ console.log(' [1-9] Select a session to manage');
1633
+ console.log(' [b] Back');
1634
+ console.log('');
1635
+
1636
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1637
+
1638
+ if (choice === 'b' || choice === 'back') return { next: 'main' };
1639
+
1640
+ const numChoice = parseInt(choice, 10);
1641
+ if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= sessions.length) {
1642
+ return { next: 'session-manage', session: sessions[numChoice - 1] };
1643
+ }
1644
+
1645
+ return { next: 'sessions' };
1646
+ }
1647
+
1648
+ async function sessionManageScreen(rl, ask, ctx = {}) {
1649
+ const sess = ctx.session;
1650
+ if (!sess) return { next: 'sessions' };
1651
+
1652
+ const cwd = process.cwd();
1653
+ const pinLabel = sess.pinned ? 'Unpin' : 'Pin';
1654
+ const catLabel = sess.category ? `[${sess.category}]` : '(none)';
1655
+
1656
+ console.log('');
1657
+ console.log(separator(`Session: ${sess.name}`));
1658
+ console.log('');
1659
+ console.log(` Age: ${sess.age}`);
1660
+ console.log(` Category: ${catLabel}`);
1661
+ console.log(` Pinned: ${sess.pinned ? 'yes' : 'no'}`);
1662
+ console.log('');
1663
+ console.log(menu([
1664
+ { key: 'r', label: 'Rename', section: '' },
1665
+ { key: 'p', label: pinLabel, section: '' },
1666
+ { key: 'c', label: 'Set category', section: '' },
1667
+ { key: 'o', label: 'Open (resume)', section: '' },
1668
+ { key: 'b', label: 'Back', section: '' },
1669
+ ]));
1670
+ console.log('');
1671
+
1672
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1673
+
1674
+ if (choice === 'r') {
1675
+ const name = (await ask(' New name: ')).trim();
1676
+ if (name) {
1677
+ renameSession(sess.id, name, cwd);
1678
+ console.log(` Renamed to: ${name}`);
1679
+ }
1680
+ return { next: 'session-manage', session: { ...sess, name: name || sess.name } };
1681
+ }
1682
+
1683
+ if (choice === 'p') {
1684
+ if (sess.pinned) {
1685
+ unpinSession(sess.id, cwd);
1686
+ console.log(' Unpinned.');
1687
+ return { next: 'session-manage', session: { ...sess, pinned: false } };
1688
+ } else {
1689
+ pinSession(sess.id, cwd);
1690
+ console.log(' Pinned.');
1691
+ return { next: 'session-manage', session: { ...sess, pinned: true } };
1692
+ }
1693
+ }
1694
+
1695
+ if (choice === 'c') {
1696
+ console.log('');
1697
+ CATEGORIES.forEach((cat, i) => console.log(` (${i + 1}) ${cat}`));
1698
+ console.log(` (${CATEGORIES.length + 1}) custom`);
1699
+ console.log('');
1700
+ const catChoice = (await ask(' Category: ')).trim();
1701
+ const catIndex = parseInt(catChoice, 10);
1702
+ let category = null;
1703
+ if (!isNaN(catIndex) && catIndex >= 1 && catIndex <= CATEGORIES.length) {
1704
+ category = CATEGORIES[catIndex - 1];
1705
+ } else if (catIndex === CATEGORIES.length + 1) {
1706
+ category = (await ask(' Custom category: ')).trim() || null;
1707
+ } else if (catChoice) {
1708
+ category = catChoice;
1709
+ }
1710
+ if (category) {
1711
+ categorizeSession(sess.id, category, cwd);
1712
+ console.log(` Category set to: ${category}`);
1713
+ }
1714
+ return { next: 'session-manage', session: { ...sess, category: category ?? sess.category } };
1715
+ }
1716
+
1717
+ if (choice === 'o') {
1718
+ const { spawnSync } = await import('node:child_process');
1719
+ console.log(`\n Launching: claude --resume ${sess.id}\n`);
1720
+ spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
1721
+ return { next: 'sessions' };
1722
+ }
1723
+
1724
+ if (choice === 'b' || choice === 'back') return { next: 'sessions' };
1725
+
1726
+ return { next: 'session-manage', session: sess };
1727
+ }
1728
+
1399
1729
  // ─── Screen state machine ─────────────────────────────────────────────────────
1400
1730
 
1401
1731
  const SCREENS = {
@@ -1403,12 +1733,15 @@ const SCREENS = {
1403
1733
  main: mainScreen,
1404
1734
  'new-session': newSessionScreen,
1405
1735
  settings: settingsScreen,
1736
+ subscriptions: subscriptionsScreen,
1406
1737
  dashboard: dashboardScreen,
1407
1738
  auth: authScreen,
1408
1739
  profile: profileScreen,
1409
1740
  diagnostics: diagnosticsScreen,
1410
1741
  repl: replScreen,
1411
1742
  'session-detail': sessionDetailScreen,
1743
+ sessions: sessionsScreen,
1744
+ 'session-manage': sessionManageScreen,
1412
1745
  };
1413
1746
 
1414
1747
  async function runScreens(startScreen = 'dashboard') {
@@ -1481,8 +1814,6 @@ async function main() {
1481
1814
  // One-shot commands — run and exit
1482
1815
  if (cmd === 'install') { await cmdInstall(); return; }
1483
1816
  if (cmd === 'auth') {
1484
- const sub = args[1];
1485
- if (sub === 'setup') { await cmdAuthSetup(); return; }
1486
1817
  await cmdAuth(args.slice(1));
1487
1818
  return;
1488
1819
  }
@@ -1493,6 +1824,21 @@ async function main() {
1493
1824
  if (cmd === 'remember') { cmdRemember(args[1]); return; }
1494
1825
  if (cmd === 'forget') { cmdForget(args[1]); return; }
1495
1826
 
1827
+ if (cmd === 'shell-hook') {
1828
+ // Output a bash snippet users can add to their .bashrc or source directly.
1829
+ const hook = `
1830
+ # dual-brain shell integration
1831
+ # Source this file or add to .bashrc
1832
+ if command -v dual-brain &>/dev/null; then
1833
+ alias db='dual-brain'
1834
+ alias dbgo='dual-brain go'
1835
+ alias dbstat='dual-brain status'
1836
+ fi
1837
+ `.trim();
1838
+ console.log(hook);
1839
+ return;
1840
+ }
1841
+
1496
1842
  process.stderr.write(`Unknown command: ${cmd}\nRun "dual-brain --help" for usage.\n`);
1497
1843
  process.exit(1);
1498
1844
  }