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.
- package/bin/dual-brain.mjs +654 -308
- package/install.mjs +30 -0
- package/package.json +3 -2
- package/shell-hook.sh +26 -0
- package/src/index.mjs +2 -2
- package/src/profile.mjs +42 -244
- package/src/session.mjs +84 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
// ───
|
|
101
|
+
// ─── Subscription status table ────────────────────────────────────────────────
|
|
79
102
|
|
|
80
103
|
/**
|
|
81
|
-
* Print a
|
|
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
|
|
86
|
-
const W = 55;
|
|
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, '');
|
|
110
|
+
const visible = s.replace(/[̀-ͯ]/g, '');
|
|
90
111
|
return s + ' '.repeat(Math.max(0, W - visible.length));
|
|
91
112
|
};
|
|
92
113
|
|
|
93
|
-
const
|
|
94
|
-
const
|
|
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
|
|
97
|
-
const
|
|
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:
|
|
101
|
-
: ` Claude:
|
|
102
|
-
const claudeLine2 =
|
|
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:
|
|
108
|
-
: ` OpenAI:
|
|
109
|
-
const openaiLine2 =
|
|
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('
|
|
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:
|
|
152
|
+
// --- Step 1: Detect auth ---
|
|
129
153
|
const auth = await detectAuth();
|
|
130
|
-
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
// ---
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
586
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
claudePlan = null;
|
|
624
|
-
} else {
|
|
625
|
-
// Default: pro
|
|
626
|
-
claudePlan = 'pro';
|
|
659
|
+
await cmdInstall(cwd);
|
|
660
|
+
return { next: 'main' };
|
|
627
661
|
}
|
|
628
662
|
|
|
629
|
-
|
|
663
|
+
// ── [c] Customize: plan picker ───────────────────────────────────────────
|
|
664
|
+
|
|
665
|
+
const existingProfile = loadProfile(cwd);
|
|
630
666
|
|
|
631
|
-
//
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
673
|
-
|
|
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
|
-
|
|
691
|
-
const env = detectEnvironment();
|
|
692
|
-
const auth = await detectAuth();
|
|
737
|
+
saveProfile(existingProfile, { cwd });
|
|
693
738
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
|
723
|
-
const
|
|
724
|
-
const
|
|
725
|
-
const
|
|
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
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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
|
|
736
|
-
|
|
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
|
-
|
|
742
|
-
|
|
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
|
-
|
|
837
|
+
menuOpts.push({ key: '1-9', label: 'Resume numbered above', section: 'Sessions' });
|
|
745
838
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
`
|
|
864
|
-
` Claude: ${auth.claude.found ? `
|
|
865
|
-
` OpenAI: ${auth.openai.found ? `
|
|
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',
|
|
875
|
-
{ key: '2', label: 'Switch to balanced',
|
|
876
|
-
{ key: '3', label: 'Switch to quality-first',
|
|
877
|
-
{ key: 'a', label: '
|
|
878
|
-
{ key: 'i', label: 'Reinstall hooks',
|
|
879
|
-
{ key: 'b', label: 'Back',
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
944
|
-
{ key: '
|
|
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 '
|
|
1257
|
+
if (entries.length === 0) return 'healthy';
|
|
1048
1258
|
const statuses = entries.map(([, v]) => v.status);
|
|
1049
|
-
if (statuses.includes('hot')) return '
|
|
1050
|
-
if (statuses.includes('degraded')) return '
|
|
1051
|
-
if (statuses.includes('probing')) return '
|
|
1052
|
-
return '
|
|
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
|
|
1056
|
-
const
|
|
1057
|
-
const claudePlanStr
|
|
1058
|
-
const openaiPlanStr
|
|
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('
|
|
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
|
|
1183
|
-
` ${
|
|
1184
|
-
` ${
|
|
1389
|
+
separator('Provider Status'),
|
|
1390
|
+
` Claude: ${claudeHealthBadge.padEnd(14)} ${claudePlanStr}`,
|
|
1391
|
+
` OpenAI: ${openaiHealthBadge.padEnd(14)} ${openaiPlanStr}`,
|
|
1185
1392
|
'',
|
|
1186
1393
|
separator('Enforcement'),
|
|
1187
|
-
` ${headGuardExists ? '
|
|
1188
|
-
` ${enforceTierExists ? '
|
|
1189
|
-
` ${guardCount === 4 ? '
|
|
1190
|
-
` ${hookifyCount > 0 ? '
|
|
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 ? '
|
|
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('
|
|
1405
|
+
output.push(' WARN Claude auth credentials file missing');
|
|
1199
1406
|
} else if (credsFresh) {
|
|
1200
|
-
output.push(`
|
|
1407
|
+
output.push(` ok Claude auth fresh (expires: ${credsExpiry})`);
|
|
1201
1408
|
} else {
|
|
1202
|
-
output.push(`
|
|
1409
|
+
output.push(` ERROR Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
|
|
1203
1410
|
}
|
|
1204
|
-
output.push(`
|
|
1205
|
-
output.push(` ${sessionManagerExists ? '
|
|
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(`
|
|
1420
|
+
output.push(` ERROR Tests error: ${testError}`);
|
|
1214
1421
|
} else if (testPass !== null) {
|
|
1215
|
-
output.push(` ${testPass === testTotal ? '
|
|
1422
|
+
output.push(` ${testPass === testTotal ? 'ok ' : 'FAIL '} Tests ${testPass}/${testTotal} passing`);
|
|
1216
1423
|
}
|
|
1217
1424
|
if (healthError) {
|
|
1218
|
-
output.push(`
|
|
1425
|
+
output.push(` ERROR Health check error: ${healthError}`);
|
|
1219
1426
|
} else if (healthPass !== null) {
|
|
1220
|
-
output.push(` ${healthPass === healthTotal ? '
|
|
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([]
|
|
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
|
}
|