dual-brain 7.1.5 → 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,10 +84,12 @@ 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
- Shows dashboard screen with menu-driven navigation.
69
- [s] Status, [p] Profile, [a] Auth, [d] Diagnostics, [q] Exit
91
+ Session manager with recent sessions and routing.
92
+ [n] New session, [c] Continue last, [1-9] Resume, [s] Settings, [q] Exit
70
93
 
71
94
  Options:
72
95
  --version Print version
@@ -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;
95
116
 
96
- const claudeDisabledNote = claudeDisabled ? ' (auth ok, but disabled in profile)' : '';
97
- const openaiDisabledNote = openaiDisabled ? ' (auth ok, but disabled in profile)' : '';
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';
123
+
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,339 +532,643 @@ 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: 'dashboard' };
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);
666
+
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' };
684
+ }
685
+ }
630
686
 
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');
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' };
651
704
  }
652
- } else if (openaiChoice === '5') {
653
- openaiEnabled = false;
654
- openaiPlan = null;
655
- } else {
656
- openaiPlan = 'plus';
657
705
  }
658
706
 
707
+ // Mode picker
659
708
  console.log('');
660
-
661
- // --- Optimization mode ---
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
- return { next: 'dashboard' };
754
+ return { next: 'main' };
712
755
  }
713
756
 
714
- // ─── Screen: dashboardScreen ──────────────────────────────────────────────────
757
+ // ─── Screen: mainScreen ───────────────────────────────────────────────────────
715
758
 
716
- async function dashboardScreen(rl, ask) {
759
+ async function mainScreen(rl, ask) {
717
760
  const cwd = process.cwd();
718
761
  const version = readVersion();
719
762
  const profile = loadProfile(cwd);
720
763
  const auth = await detectAuth();
721
- const env = detectEnvironment();
722
764
 
723
- // Build status lines for box
724
- // If auth is found but provider is disabled in profile, show warning instead of green
725
- const claudeProviderEnabled = profile?.providers?.claude?.enabled !== false;
726
- const openaiProviderEnabled = profile?.providers?.openai?.enabled !== false;
727
- const claudeStatus = auth.claude.found
728
- ? (claudeProviderEnabled ? `🟢 Claude ${badge('connected')}` : `⚠️ Claude ${badge('warning')} disabled`)
729
- : `🔴 Claude ${badge('missing')}`;
730
- const openaiStatus = auth.openai.found
731
- ? (openaiProviderEnabled ? `🟢 OpenAI ${badge('connected')}` : `⚠️ OpenAI ${badge('warning')} disabled`)
732
- : `🔴 OpenAI ${badge('missing')}`;
733
- const envLabel = env.hasReplitTools ? 'Replit + replit-tools' : env.isReplit ? 'Replit' : 'local';
734
-
735
- // Enforcement check
736
- let guardCount = 0;
737
- try {
738
- const settingsFile = join(cwd, '.claude', 'settings.json');
739
- if (existsSync(settingsFile)) {
740
- const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
741
- const preToolUse = settings?.hooks?.PreToolUse ?? [];
742
- const guardCmd = 'node .claude/hooks/head-guard.mjs';
743
- const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
744
- const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
745
- const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
746
- const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
747
- const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
748
- guardCount = [hasEdit, hasWrite, hasBash, hasAgent].filter(Boolean).length;
749
- }
750
- } catch { /* ignore */ }
751
-
752
- const authSummary = (auth.claude.found && auth.openai.found)
753
- ? 'both providers connected'
754
- : auth.claude.found
755
- ? 'Claude connected, OpenAI missing'
756
- : auth.openai.found
757
- ? 'OpenAI connected, Claude missing'
758
- : 'no providers connected';
759
-
760
- const dashLines = [
761
- `${claudeStatus} ${openaiStatus}`,
762
- `🌀 ${envLabel}`,
763
- '',
764
- `✓ Profile: ${profile.mode} · ${profile.providers?.claude?.enabled && profile.providers?.openai?.enabled ? 'dual' : 'solo'} mode`,
765
- `✓ Enforcement: ${guardCount} guards active`,
766
- `✓ Auth: ${authSummary}`,
767
- ];
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
+ }
768
786
 
769
- console.log(box(`🧠 Dual-Brain v${version}`, dashLines));
787
+ let claudeStatus = subStatus('Claude', claudePlan, auth.claude.found, claudeExpired, claudeDays, claudeSub);
788
+ let openaiStatus = subStatus('OpenAI', openaiPlan, auth.openai.found, openaiExpired, openaiDays, openaiSub);
789
+
790
+ console.log(`\ndual-brain v${version}`);
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
+ }
770
817
  console.log('');
771
818
 
772
- // ── Recent Sessions (replit-tools import) ──────────────────────────────────
773
- const recentSessions = importReplitSessions(cwd).slice(0, 5);
819
+ const recentSessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 7);
820
+
774
821
  if (recentSessions.length > 0) {
775
- console.log(separator('Recent Sessions'));
822
+ console.log('Recent:');
776
823
  recentSessions.forEach((sess, i) => {
777
- const activeIndicator = sess.isActive ? '' : ' ';
778
- const promptsLabel = `(${sess.promptCount} prompt${sess.promptCount !== 1 ? 's' : ''})`;
779
- console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${activeIndicator} ${sess.name} ${promptsLabel}`);
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}`);
780
828
  });
781
829
  console.log('');
782
830
  }
783
831
 
784
- console.log(menu([
785
- { key: 's', label: 'Status — detailed provider info', section: 'Info' },
786
- { key: 'p', label: 'Profile & preferences', section: 'Settings' },
787
- { key: 'a', label: 'Auth management', section: 'Settings' },
788
- { key: 'd', label: 'Diagnostics & repair', section: 'Settings' },
789
- { key: 'q', label: 'Exit to shell', section: '' },
790
- ]));
832
+ console.log(' [c] Continue last session');
833
+ console.log(' [n] New session');
834
+ if (recentSessions.length > 0) {
835
+ console.log(' [1-9] Resume numbered above');
836
+ }
837
+ console.log(' [e] Manage sessions');
838
+ console.log(' [d] Switch to data-tools');
839
+ if (!auth.claude.found) console.log(' [j] Login to Claude');
840
+ if (!auth.openai.found) console.log(' [k] Login to Codex');
841
+ console.log(' [s] Settings [q] Exit');
791
842
  console.log('');
792
843
 
793
844
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
794
845
 
795
- // Numeric choice session detail
846
+ if (choice === 'n') { return { next: 'new-session' }; }
847
+
848
+ if (choice === 'c') {
849
+ const sessions = importReplitSessions(cwd);
850
+ if (sessions.length === 0) {
851
+ console.log('\n No recent sessions found.\n');
852
+ await ask(' Press Enter to continue...');
853
+ return { next: 'main' };
854
+ }
855
+ const { spawnSync } = await import('node:child_process');
856
+ console.log(`\n Launching: claude --resume ${sessions[0].id}\n`);
857
+ spawnSync('claude', ['--resume', sessions[0].id], { stdio: 'inherit' });
858
+ return { next: 'main' };
859
+ }
860
+
796
861
  const numChoice = parseInt(choice, 10);
797
862
  if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
798
- return { next: 'session-detail', session: recentSessions[numChoice - 1] };
863
+ const sess = recentSessions[numChoice - 1];
864
+ const { spawnSync } = await import('node:child_process');
865
+ console.log(`\n Launching: claude --resume ${sess.id}\n`);
866
+ spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
867
+ return { next: 'main' };
799
868
  }
800
869
 
801
- if (choice === 's') {
802
- await cmdStatus([]);
803
- await ask('\n Press Enter to return to dashboard...');
804
- return { next: 'dashboard' };
870
+ if (choice === 'e') { return { next: 'sessions' }; }
871
+
872
+ if (choice === 'd') {
873
+ const { spawnSync } = await import('node:child_process');
874
+ const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
875
+ if (which.status === 0) {
876
+ spawnSync('claude-menu', { stdio: 'inherit' });
877
+ } else {
878
+ console.log('\n data-tools not found — install with: npm i -g replit-tools\n');
879
+ await ask(' Press Enter to continue...');
880
+ }
881
+ return { next: 'main' };
882
+ }
883
+
884
+ if (choice === 'j') {
885
+ const { spawnSync } = await import('node:child_process');
886
+ spawnSync('claude', ['login'], { stdio: 'inherit' });
887
+ return { next: 'main' };
805
888
  }
806
889
 
807
- if (choice === 'p') { return { next: 'profile' }; }
808
- if (choice === 'a') { return { next: 'auth' }; }
809
- if (choice === 'd') { return { next: 'diagnostics' }; }
890
+ if (choice === 'k') {
891
+ const { spawnSync } = await import('node:child_process');
892
+ spawnSync('codex', ['login'], { stdio: 'inherit' });
893
+ return { next: 'main' };
894
+ }
895
+
896
+ if (choice === 's') { return { next: 'settings' }; }
810
897
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
811
898
 
812
- // Unknown choice stay on dashboard
813
- return { next: 'dashboard' };
899
+ return { next: 'main' };
814
900
  }
815
901
 
816
- // ─── Screen: authScreen ───────────────────────────────────────────────────────
902
+ // ─── Screen: newSessionScreen ─────────────────────────────────────────────────
817
903
 
818
- async function authScreen(rl, ask) {
904
+ async function newSessionScreen(rl, ask) {
905
+ const cwd = process.cwd();
906
+ const input = (await ask('\n What do you want to do? ')).trim();
907
+ if (!input) { return { next: 'main' }; }
908
+
909
+ const profile = loadProfile(cwd);
910
+ const detection = detectTask({ prompt: input });
911
+ const decision = decideRoute({ profile, detection, cwd });
912
+
913
+ console.log(`\n Routing: ${decision.provider}/${decision.model} (${decision.tier})`);
914
+ console.log(` Reason: ${decision.explanation}\n`);
915
+
916
+ const { spawnSync } = await import('node:child_process');
917
+ if (decision.provider === 'openai') {
918
+ spawnSync('codex', [input], { stdio: 'inherit' });
919
+ } else {
920
+ spawnSync('claude', ['-p', input], { stdio: 'inherit' });
921
+ }
922
+
923
+ return { next: 'main' };
924
+ }
925
+
926
+ // ─── Screen: settingsScreen ───────────────────────────────────────────────────
927
+
928
+ async function settingsScreen(rl, ask) {
929
+ const cwd = process.cwd();
930
+ const profile = loadProfile(cwd);
819
931
  const auth = await detectAuth();
820
932
 
821
- const authLines = [
822
- 'Claude:',
933
+ let guardCount = 0;
934
+ try {
935
+ const settingsFile = join(cwd, '.claude', 'settings.json');
936
+ if (existsSync(settingsFile)) {
937
+ const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
938
+ const preToolUse = settings?.hooks?.PreToolUse ?? [];
939
+ const guardCmd = 'node .claude/hooks/head-guard.mjs';
940
+ const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
941
+ const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
942
+ const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
943
+ const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
944
+ const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
945
+ guardCount = [hasEdit, hasWrite, hasBash, hasAgent].filter(Boolean).length;
946
+ }
947
+ } catch { /* ignore */ }
948
+
949
+ const modeLabel = (m) => m === profile.mode ? `${m} (active)` : m;
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
+
960
+ const settingsLines = [
961
+ `Mode:`,
962
+ ` [1] ${modeLabel('cost-saver')}`,
963
+ ` [2] ${modeLabel('balanced')}`,
964
+ ` [3] ${modeLabel('quality-first')}`,
965
+ '',
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}]` : ''}`,
969
+ '',
970
+ `Enforcement: ${guardCount}/4 guards active`,
823
971
  ];
824
972
 
825
- if (auth.claude.found) {
826
- authLines.push(` source: ${auth.claude.source} ${badge('connected')}`);
827
- authLines.push(` key: ${auth.claude.masked}`);
828
- } else {
829
- authLines.push(` not configured ${badge('missing')}`);
973
+ console.log('');
974
+ console.log(box('Settings', settingsLines));
975
+ console.log('');
976
+ console.log(menu([
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: '' },
983
+ ]));
984
+ console.log('');
985
+
986
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
987
+
988
+ if (choice === '1' || choice === '2' || choice === '3') {
989
+ const modeMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
990
+ profile.mode = modeMap[choice];
991
+ saveProfile(profile, { cwd });
992
+ console.log(` Mode set to: ${profile.mode}`);
993
+ return { next: 'settings' };
830
994
  }
831
995
 
832
- authLines.push('');
833
- authLines.push('OpenAI:');
996
+ if (choice === 'a') {
997
+ return { next: 'subscriptions' };
998
+ }
834
999
 
835
- if (auth.openai.found) {
836
- authLines.push(` source: ${auth.openai.source} ${badge('connected')}`);
837
- authLines.push(` key: ${auth.openai.masked}`);
838
- } else {
839
- authLines.push(` not configured ${badge('missing')}`);
1000
+ if (choice === 'i') {
1001
+ await cmdInstall();
1002
+ return { next: 'settings' };
840
1003
  }
841
1004
 
842
- console.log(box('🔑 Auth Management', authLines));
1005
+ if (choice === 'b' || choice === 'back') { return { next: 'main' }; }
1006
+
1007
+ return { next: 'settings' };
1008
+ }
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));
843
1040
  console.log('');
844
1041
  console.log(menu([
845
- { key: 'a', label: 'Add API key', section: '' },
846
- { key: 't', label: 'Test keys', section: '' },
847
- { key: 'b', label: 'Back to dashboard', section: '' },
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: '' },
848
1047
  ]));
849
1048
  console.log('');
850
1049
 
851
1050
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
852
1051
 
853
- if (choice === 'a') {
854
- await setupAuth(rl);
855
- return { next: 'auth' }; // refresh
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' };
856
1100
  }
857
1101
 
858
1102
  if (choice === 't') {
859
- console.log('\n Testing auth...');
860
- const authNow = await detectAuth();
861
- console.log(` Claude: ${authNow.claude.found ? 'OK — ' + authNow.claude.source : 'NOT FOUND'}`);
862
- console.log(` OpenAI: ${authNow.openai.found ? 'OK — ' + authNow.openai.source : 'NOT FOUND'}`);
863
- await ask('\n Press Enter to continue...');
864
- return { next: 'auth' };
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' };
865
1118
  }
866
1119
 
1120
+ if (choice === 'b' || choice === 'back') { return { next: 'settings' }; }
1121
+
1122
+ return { next: 'subscriptions' };
1123
+ }
1124
+
1125
+ // ─── Screen: dashboardScreen (kept for internal reference, unreachable) ───────
1126
+
1127
+ async function dashboardScreen(rl, ask) {
1128
+ return { next: 'main' };
1129
+ }
1130
+
1131
+ // ─── Screen: authScreen — subscription status view ───────────────────────────
1132
+
1133
+ async function authScreen(rl, ask) {
1134
+ const cwd = process.cwd();
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';
1146
+
1147
+ const authLines = [
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}]` : ''}`,
1159
+ ];
1160
+
1161
+ console.log(box('Subscription Status', authLines));
1162
+ console.log('');
1163
+ console.log(menu([
1164
+ { key: 'a', label: 'Manage subscriptions', section: '' },
1165
+ { key: 'b', label: 'Back to dashboard', section: '' },
1166
+ ]));
1167
+ console.log('');
1168
+
1169
+ const choice = (await ask(' Choice: ')).trim().toLowerCase();
1170
+
1171
+ if (choice === 'a') { return { next: 'subscriptions' }; }
867
1172
  if (choice === 'b' || choice === 'back') { return { next: 'dashboard' }; }
868
1173
 
869
1174
  return { next: 'auth' };
@@ -933,7 +1238,6 @@ async function diagnosticsScreen(rl, ask) {
933
1238
  const cwd = process.cwd();
934
1239
  const { spawnSync: _spawnSync } = await import('child_process');
935
1240
  const { readdirSync } = await import('node:fs');
936
- const { detectPlans } = await import('../src/profile.mjs');
937
1241
 
938
1242
  // ── Version info ──────────────────────────────────────────────────────────
939
1243
  const version = readVersion();
@@ -946,20 +1250,18 @@ async function diagnosticsScreen(rl, ask) {
946
1250
 
947
1251
  function _providerBadge(name) {
948
1252
  const entries = Object.entries(healthStates).filter(([k]) => k.startsWith(`${name}:`));
949
- if (entries.length === 0) return 'healthy';
1253
+ if (entries.length === 0) return 'healthy';
950
1254
  const statuses = entries.map(([, v]) => v.status);
951
- if (statuses.includes('hot')) return '🔴 hot';
952
- if (statuses.includes('degraded')) return '⚠️ degraded';
953
- if (statuses.includes('probing')) return '⚠️ probing';
954
- 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';
955
1259
  }
956
1260
 
957
- const claudeStatus = auth.claude.found ? _providerBadge('claude') : ' no auth';
958
- const openaiStatus = auth.openai.found ? _providerBadge('openai') : ' no auth';
959
- const claudePlanStr = plans.claude ? `Max ${plans.claude}` : (auth.claude.masked ?? 'unknown');
960
- const openaiPlanStr = plans.openai ? `Pro ${plans.openai}` : (auth.openai.masked ?? 'unknown');
961
- const claudeAuthStr = auth.claude.masked ?? 'not configured';
962
- 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';
963
1265
 
964
1266
  // ── Enforcement checks ────────────────────────────────────────────────────
965
1267
  const hooksDir = join(cwd, '.claude', 'hooks');
@@ -1060,7 +1362,6 @@ async function diagnosticsScreen(rl, ask) {
1060
1362
  // ── Render ────────────────────────────────────────────────────────────────
1061
1363
  const W = 56;
1062
1364
  const hbar = '═'.repeat(W);
1063
- // Pad a string to exactly W visible columns (for box rows without leading ║ prefix)
1064
1365
  const padRow = (s) => {
1065
1366
  const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
1066
1367
  let vlen = 0;
@@ -1075,36 +1376,36 @@ async function diagnosticsScreen(rl, ask) {
1075
1376
 
1076
1377
  const output = [
1077
1378
  `╔${hbar}╗`,
1078
- hrow('🔧 Diagnostics'),
1379
+ hrow('Diagnostics'),
1079
1380
  `╠${hbar}╣`,
1080
1381
  hrow(`dual-brain v${version}`),
1081
1382
  hrow(`Node.js ${nodeVersion}`),
1082
1383
  `╚${hbar}╝`,
1083
1384
  '',
1084
- separator('Provider Health'),
1085
- ` ${claudeStatus.padEnd(14)} Claude ${claudePlanStr.padEnd(16)} ${claudeAuthStr}`,
1086
- ` ${openaiStatus.padEnd(14)} OpenAI ${openaiPlanStr.padEnd(16)} ${openaiAuthStr}`,
1385
+ separator('Provider Status'),
1386
+ ` Claude: ${claudeHealthBadge.padEnd(14)} ${claudePlanStr}`,
1387
+ ` OpenAI: ${openaiHealthBadge.padEnd(14)} ${openaiPlanStr}`,
1087
1388
  '',
1088
1389
  separator('Enforcement'),
1089
- ` ${headGuardExists ? '' : ''} head-guard.mjs ${headGuardExists ? 'installed' : 'missing — run: dual-brain install'}`,
1090
- ` ${enforceTierExists ? '' : ''} enforce-tier.mjs ${enforceTierExists ? 'installed' : 'missing — run: dual-brain install'}`,
1091
- ` ${guardCount === 4 ? '' : '⚠️ '} settings.json ${guardCount}/4 guards registered${guardCount < 4 ? ' — run: dual-brain install' : ''}`,
1092
- ` ${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'}`,
1093
1394
  '',
1094
1395
  separator('Replit Tools'),
1095
- ` ${hasReplitTools ? '' : ''} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
1396
+ ` ${hasReplitTools ? 'ok' : 'n/a'} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
1096
1397
  ];
1097
1398
 
1098
1399
  if (hasReplitTools) {
1099
1400
  if (credsFresh === null) {
1100
- output.push(' ⚠️ Claude auth credentials file missing');
1401
+ output.push(' WARN Claude auth credentials file missing');
1101
1402
  } else if (credsFresh) {
1102
- output.push(` Claude auth fresh (expires: ${credsExpiry})`);
1403
+ output.push(` ok Claude auth fresh (expires: ${credsExpiry})`);
1103
1404
  } else {
1104
- output.push(` Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
1405
+ output.push(` ERROR Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
1105
1406
  }
1106
- output.push(` Session archive ${historyCount} entries`);
1107
- 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'}`);
1108
1409
  } else {
1109
1410
  output.push(' ─── (not available)');
1110
1411
  }
@@ -1112,14 +1413,14 @@ async function diagnosticsScreen(rl, ask) {
1112
1413
  output.push('');
1113
1414
  output.push(separator('Quality'));
1114
1415
  if (testError) {
1115
- output.push(` Tests error: ${testError}`);
1416
+ output.push(` ERROR Tests error: ${testError}`);
1116
1417
  } else if (testPass !== null) {
1117
- output.push(` ${testPass === testTotal ? '' : ''} Tests ${testPass}/${testTotal} passing`);
1418
+ output.push(` ${testPass === testTotal ? 'ok ' : 'FAIL '} Tests ${testPass}/${testTotal} passing`);
1118
1419
  }
1119
1420
  if (healthError) {
1120
- output.push(` Health check error: ${healthError}`);
1421
+ output.push(` ERROR Health check error: ${healthError}`);
1121
1422
  } else if (healthPass !== null) {
1122
- output.push(` ${healthPass === healthTotal ? '' : '⚠️ '} Health check ${healthPass}/${healthTotal} passing`);
1423
+ output.push(` ${healthPass === healthTotal ? 'ok ' : 'WARN '} Health check ${healthPass}/${healthTotal} passing`);
1123
1424
  }
1124
1425
  output.push('');
1125
1426
 
@@ -1211,10 +1512,8 @@ async function replScreen(rl, ask) {
1211
1512
  printHelp();
1212
1513
  } else if (line === 'status') {
1213
1514
  await cmdStatus([]);
1214
- } else if (line === 'auth setup' || line === 'auth-setup') {
1215
- await cmdAuthSetup(rl);
1216
1515
  } else if (line === 'auth') {
1217
- await cmdAuth([], rl);
1516
+ await cmdAuth([]);
1218
1517
  } else if (line.startsWith('go ')) {
1219
1518
  await cmdGo(line.slice(3).trim().split(/\s+/));
1220
1519
  } else if (line.startsWith('remember ')) {
@@ -1298,16 +1597,147 @@ async function sessionDetailScreen(rl, ask, ctx = {}) {
1298
1597
  return { next: 'dashboard' };
1299
1598
  }
1300
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
+
1301
1725
  // ─── Screen state machine ─────────────────────────────────────────────────────
1302
1726
 
1303
1727
  const SCREENS = {
1304
- welcome: welcomeScreen,
1305
- dashboard: dashboardScreen,
1306
- auth: authScreen,
1307
- profile: profileScreen,
1308
- diagnostics: diagnosticsScreen,
1309
- repl: replScreen,
1728
+ welcome: welcomeScreen,
1729
+ main: mainScreen,
1730
+ 'new-session': newSessionScreen,
1731
+ settings: settingsScreen,
1732
+ subscriptions: subscriptionsScreen,
1733
+ dashboard: dashboardScreen,
1734
+ auth: authScreen,
1735
+ profile: profileScreen,
1736
+ diagnostics: diagnosticsScreen,
1737
+ repl: replScreen,
1310
1738
  'session-detail': sessionDetailScreen,
1739
+ sessions: sessionsScreen,
1740
+ 'session-manage': sessionManageScreen,
1311
1741
  };
1312
1742
 
1313
1743
  async function runScreens(startScreen = 'dashboard') {
@@ -1326,7 +1756,7 @@ async function runScreens(startScreen = 'dashboard') {
1326
1756
  ctx = result?.session ? { session: result.session } : {};
1327
1757
  } catch (e) {
1328
1758
  console.error(`Error: ${e.message}`);
1329
- current = 'dashboard'; // recover to dashboard on error
1759
+ current = 'main';
1330
1760
  ctx = {};
1331
1761
  }
1332
1762
  }
@@ -1349,8 +1779,7 @@ async function main() {
1349
1779
  if (isInteractive) {
1350
1780
  const cwd = process.cwd();
1351
1781
  if (profileExists(cwd)) {
1352
- // Profile already exists → go straight to dashboard
1353
- await runScreens('dashboard');
1782
+ await runScreens('main');
1354
1783
  } else {
1355
1784
  // First run: welcomeScreen handles auto-setup detection internally,
1356
1785
  // then falls through to manual wizard if needed.
@@ -1381,8 +1810,6 @@ async function main() {
1381
1810
  // One-shot commands — run and exit
1382
1811
  if (cmd === 'install') { await cmdInstall(); return; }
1383
1812
  if (cmd === 'auth') {
1384
- const sub = args[1];
1385
- if (sub === 'setup') { await cmdAuthSetup(); return; }
1386
1813
  await cmdAuth(args.slice(1));
1387
1814
  return;
1388
1815
  }
@@ -1393,6 +1820,21 @@ async function main() {
1393
1820
  if (cmd === 'remember') { cmdRemember(args[1]); return; }
1394
1821
  if (cmd === 'forget') { cmdForget(args[1]); return; }
1395
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
+
1396
1838
  process.stderr.write(`Unknown command: ${cmd}\nRun "dual-brain --help" for usage.\n`);
1397
1839
  process.exit(1);
1398
1840
  }