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.
- package/bin/dual-brain.mjs +800 -358
- 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,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
|
-
|
|
69
|
-
[
|
|
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
|
-
// ───
|
|
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;
|
|
95
116
|
|
|
96
|
-
const
|
|
97
|
-
|
|
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:
|
|
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,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
|
-
// ---
|
|
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
|
+
// 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
|
-
|
|
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);
|
|
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
|
-
//
|
|
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');
|
|
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
|
-
|
|
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
|
-
return { next: 'dashboard' };
|
|
754
|
+
return { next: 'main' };
|
|
712
755
|
}
|
|
713
756
|
|
|
714
|
-
// ─── Screen:
|
|
757
|
+
// ─── Screen: mainScreen ───────────────────────────────────────────────────────
|
|
715
758
|
|
|
716
|
-
async function
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
819
|
+
const recentSessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 7);
|
|
820
|
+
|
|
774
821
|
if (recentSessions.length > 0) {
|
|
775
|
-
console.log(
|
|
822
|
+
console.log('Recent:');
|
|
776
823
|
recentSessions.forEach((sess, i) => {
|
|
777
|
-
const
|
|
778
|
-
const
|
|
779
|
-
|
|
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(
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === '
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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 === '
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
813
|
-
return { next: 'dashboard' };
|
|
899
|
+
return { next: 'main' };
|
|
814
900
|
}
|
|
815
901
|
|
|
816
|
-
// ─── Screen:
|
|
902
|
+
// ─── Screen: newSessionScreen ─────────────────────────────────────────────────
|
|
817
903
|
|
|
818
|
-
async function
|
|
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
|
-
|
|
822
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
833
|
-
|
|
996
|
+
if (choice === 'a') {
|
|
997
|
+
return { next: 'subscriptions' };
|
|
998
|
+
}
|
|
834
999
|
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
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: '
|
|
846
|
-
{ key: '
|
|
847
|
-
{ key: '
|
|
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 === '
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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 '
|
|
1253
|
+
if (entries.length === 0) return 'healthy';
|
|
950
1254
|
const statuses = entries.map(([, v]) => v.status);
|
|
951
|
-
if (statuses.includes('hot')) return '
|
|
952
|
-
if (statuses.includes('degraded')) return '
|
|
953
|
-
if (statuses.includes('probing')) return '
|
|
954
|
-
return '
|
|
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
|
|
958
|
-
const
|
|
959
|
-
const claudePlanStr
|
|
960
|
-
const openaiPlanStr
|
|
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('
|
|
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
|
|
1085
|
-
` ${
|
|
1086
|
-
` ${
|
|
1385
|
+
separator('Provider Status'),
|
|
1386
|
+
` Claude: ${claudeHealthBadge.padEnd(14)} ${claudePlanStr}`,
|
|
1387
|
+
` OpenAI: ${openaiHealthBadge.padEnd(14)} ${openaiPlanStr}`,
|
|
1087
1388
|
'',
|
|
1088
1389
|
separator('Enforcement'),
|
|
1089
|
-
` ${headGuardExists ? '
|
|
1090
|
-
` ${enforceTierExists ? '
|
|
1091
|
-
` ${guardCount === 4 ? '
|
|
1092
|
-
` ${hookifyCount > 0 ? '
|
|
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 ? '
|
|
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('
|
|
1401
|
+
output.push(' WARN Claude auth credentials file missing');
|
|
1101
1402
|
} else if (credsFresh) {
|
|
1102
|
-
output.push(`
|
|
1403
|
+
output.push(` ok Claude auth fresh (expires: ${credsExpiry})`);
|
|
1103
1404
|
} else {
|
|
1104
|
-
output.push(`
|
|
1405
|
+
output.push(` ERROR Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
|
|
1105
1406
|
}
|
|
1106
|
-
output.push(`
|
|
1107
|
-
output.push(` ${sessionManagerExists ? '
|
|
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(`
|
|
1416
|
+
output.push(` ERROR Tests error: ${testError}`);
|
|
1116
1417
|
} else if (testPass !== null) {
|
|
1117
|
-
output.push(` ${testPass === testTotal ? '
|
|
1418
|
+
output.push(` ${testPass === testTotal ? 'ok ' : 'FAIL '} Tests ${testPass}/${testTotal} passing`);
|
|
1118
1419
|
}
|
|
1119
1420
|
if (healthError) {
|
|
1120
|
-
output.push(`
|
|
1421
|
+
output.push(` ERROR Health check error: ${healthError}`);
|
|
1121
1422
|
} else if (healthPass !== null) {
|
|
1122
|
-
output.push(` ${healthPass === healthTotal ? '
|
|
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([]
|
|
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:
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
}
|