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