dual-brain 7.1.5 → 7.1.6
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 +182 -82
- package/package.json +1 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -65,8 +65,8 @@ Commands:
|
|
|
65
65
|
forget "preference" Remove a preference by fuzzy match
|
|
66
66
|
|
|
67
67
|
Interactive mode (entered with no args on a TTY):
|
|
68
|
-
|
|
69
|
-
[
|
|
68
|
+
Session manager with recent sessions and routing.
|
|
69
|
+
[n] New session, [c] Continue last, [1-9] Resume, [s] Settings, [q] Exit
|
|
70
70
|
|
|
71
71
|
Options:
|
|
72
72
|
--version Print version
|
|
@@ -580,7 +580,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
580
580
|
// Enter or anything else → save and go to dashboard
|
|
581
581
|
saveProfile(setup.profile, { cwd });
|
|
582
582
|
await cmdInstall(cwd);
|
|
583
|
-
return { next: '
|
|
583
|
+
return { next: 'main' };
|
|
584
584
|
}
|
|
585
585
|
} else {
|
|
586
586
|
// Not confident — show what's missing before falling through to wizard
|
|
@@ -708,109 +708,207 @@ async function welcomeScreen(rl, ask) {
|
|
|
708
708
|
|
|
709
709
|
await cmdInstall(cwd);
|
|
710
710
|
|
|
711
|
-
return { next: '
|
|
711
|
+
return { next: 'main' };
|
|
712
712
|
}
|
|
713
713
|
|
|
714
|
-
// ─── Screen:
|
|
714
|
+
// ─── Screen: mainScreen ───────────────────────────────────────────────────────
|
|
715
715
|
|
|
716
|
-
async function
|
|
716
|
+
async function mainScreen(rl, ask) {
|
|
717
717
|
const cwd = process.cwd();
|
|
718
718
|
const version = readVersion();
|
|
719
719
|
const profile = loadProfile(cwd);
|
|
720
720
|
const auth = await detectAuth();
|
|
721
|
-
const env = detectEnvironment();
|
|
722
721
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
722
|
+
const claudePlan = profile?.providers?.claude?.plan ?? 'Pro';
|
|
723
|
+
const openaiPlan = profile?.providers?.openai?.plan ?? 'Plus';
|
|
724
|
+
const claudeStatus = auth.claude.found ? `Claude: ${claudePlan} ✓` : `Claude: missing`;
|
|
725
|
+
const openaiStatus = auth.openai.found ? `OpenAI: ${openaiPlan} ✓` : `OpenAI: missing`;
|
|
726
|
+
|
|
727
|
+
console.log(`\ndual-brain v${version}`);
|
|
728
|
+
console.log(`${claudeStatus} · ${openaiStatus}\n`);
|
|
729
|
+
|
|
730
|
+
const recentSessions = importReplitSessions(cwd).slice(0, 5);
|
|
731
|
+
|
|
732
|
+
if (recentSessions.length > 0) {
|
|
733
|
+
console.log('Recent:');
|
|
734
|
+
recentSessions.forEach((sess, i) => {
|
|
735
|
+
const activeIndicator = sess.isActive ? ' ●' : '';
|
|
736
|
+
console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${sess.name}${activeIndicator}`);
|
|
737
|
+
});
|
|
738
|
+
console.log('');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
console.log(' [c] Continue last session');
|
|
742
|
+
console.log(' [n] New session');
|
|
743
|
+
if (recentSessions.length > 0) {
|
|
744
|
+
console.log(' [1-9] Resume numbered above');
|
|
745
|
+
}
|
|
746
|
+
console.log(' [d] Switch to data-tools');
|
|
747
|
+
if (!auth.claude.found) console.log(' [j] Login to Claude');
|
|
748
|
+
if (!auth.openai.found) console.log(' [k] Login to Codex');
|
|
749
|
+
console.log(' [s] Settings [q] Exit');
|
|
750
|
+
console.log('');
|
|
751
|
+
|
|
752
|
+
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
753
|
+
|
|
754
|
+
if (choice === 'n') { return { next: 'new-session' }; }
|
|
755
|
+
|
|
756
|
+
if (choice === 'c') {
|
|
757
|
+
const sessions = importReplitSessions(cwd);
|
|
758
|
+
if (sessions.length === 0) {
|
|
759
|
+
console.log('\n No recent sessions found.\n');
|
|
760
|
+
await ask(' Press Enter to continue...');
|
|
761
|
+
return { next: 'main' };
|
|
762
|
+
}
|
|
763
|
+
const { spawnSync } = await import('node:child_process');
|
|
764
|
+
console.log(`\n Launching: claude --resume ${sessions[0].id}\n`);
|
|
765
|
+
spawnSync('claude', ['--resume', sessions[0].id], { stdio: 'inherit' });
|
|
766
|
+
return { next: 'main' };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const numChoice = parseInt(choice, 10);
|
|
770
|
+
if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
|
|
771
|
+
const sess = recentSessions[numChoice - 1];
|
|
772
|
+
const { spawnSync } = await import('node:child_process');
|
|
773
|
+
console.log(`\n Launching: claude --resume ${sess.id}\n`);
|
|
774
|
+
spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
|
|
775
|
+
return { next: 'main' };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (choice === 'd') {
|
|
779
|
+
const { spawnSync } = await import('node:child_process');
|
|
780
|
+
const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
|
|
781
|
+
if (which.status === 0) {
|
|
782
|
+
spawnSync('claude-menu', { stdio: 'inherit' });
|
|
783
|
+
} else {
|
|
784
|
+
console.log('\n data-tools not found — install with: npm i -g replit-tools\n');
|
|
785
|
+
await ask(' Press Enter to continue...');
|
|
786
|
+
}
|
|
787
|
+
return { next: 'main' };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (choice === 'j') {
|
|
791
|
+
const { spawnSync } = await import('node:child_process');
|
|
792
|
+
spawnSync('claude', ['login'], { stdio: 'inherit' });
|
|
793
|
+
return { next: 'main' };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (choice === 'k') {
|
|
797
|
+
const { spawnSync } = await import('node:child_process');
|
|
798
|
+
spawnSync('codex', ['login'], { stdio: 'inherit' });
|
|
799
|
+
return { next: 'main' };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (choice === 's') { return { next: 'settings' }; }
|
|
803
|
+
if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
|
|
804
|
+
|
|
805
|
+
return { next: 'main' };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ─── Screen: newSessionScreen ─────────────────────────────────────────────────
|
|
809
|
+
|
|
810
|
+
async function newSessionScreen(rl, ask) {
|
|
811
|
+
const cwd = process.cwd();
|
|
812
|
+
const input = (await ask('\n What do you want to do? ')).trim();
|
|
813
|
+
if (!input) { return { next: 'main' }; }
|
|
814
|
+
|
|
815
|
+
const profile = loadProfile(cwd);
|
|
816
|
+
const detection = detectTask({ prompt: input });
|
|
817
|
+
const decision = decideRoute({ profile, detection, cwd });
|
|
818
|
+
|
|
819
|
+
console.log(`\n Routing: ${decision.provider}/${decision.model} (${decision.tier})`);
|
|
820
|
+
console.log(` Reason: ${decision.explanation}\n`);
|
|
821
|
+
|
|
822
|
+
const { spawnSync } = await import('node:child_process');
|
|
823
|
+
if (decision.provider === 'openai') {
|
|
824
|
+
spawnSync('codex', [input], { stdio: 'inherit' });
|
|
825
|
+
} else {
|
|
826
|
+
spawnSync('claude', ['-p', input], { stdio: 'inherit' });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return { next: 'main' };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ─── Screen: settingsScreen ───────────────────────────────────────────────────
|
|
833
|
+
|
|
834
|
+
async function settingsScreen(rl, ask) {
|
|
835
|
+
const cwd = process.cwd();
|
|
836
|
+
const profile = loadProfile(cwd);
|
|
837
|
+
const auth = await detectAuth();
|
|
838
|
+
|
|
736
839
|
let guardCount = 0;
|
|
737
840
|
try {
|
|
738
841
|
const settingsFile = join(cwd, '.claude', 'settings.json');
|
|
739
842
|
if (existsSync(settingsFile)) {
|
|
740
843
|
const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
|
|
741
844
|
const preToolUse = settings?.hooks?.PreToolUse ?? [];
|
|
742
|
-
const guardCmd
|
|
743
|
-
const tierCmd
|
|
744
|
-
const hasEdit
|
|
745
|
-
const hasWrite
|
|
746
|
-
const hasBash
|
|
747
|
-
const hasAgent
|
|
845
|
+
const guardCmd = 'node .claude/hooks/head-guard.mjs';
|
|
846
|
+
const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
|
|
847
|
+
const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
|
|
848
|
+
const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
|
|
849
|
+
const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
|
|
850
|
+
const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
|
|
748
851
|
guardCount = [hasEdit, hasWrite, hasBash, hasAgent].filter(Boolean).length;
|
|
749
852
|
}
|
|
750
853
|
} catch { /* ignore */ }
|
|
751
854
|
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
855
|
+
const modeLabel = (m) => m === profile.mode ? `${m} (active)` : m;
|
|
856
|
+
|
|
857
|
+
const settingsLines = [
|
|
858
|
+
`Mode:`,
|
|
859
|
+
` [1] ${modeLabel('cost-saver')}`,
|
|
860
|
+
` [2] ${modeLabel('balanced')}`,
|
|
861
|
+
` [3] ${modeLabel('quality-first')}`,
|
|
862
|
+
'',
|
|
863
|
+
`Auth:`,
|
|
864
|
+
` Claude: ${auth.claude.found ? `connected (${auth.claude.source})` : 'missing'}`,
|
|
865
|
+
` OpenAI: ${auth.openai.found ? `connected (${auth.openai.source})` : 'missing'}`,
|
|
763
866
|
'',
|
|
764
|
-
|
|
765
|
-
`✓ Enforcement: ${guardCount} guards active`,
|
|
766
|
-
`✓ Auth: ${authSummary}`,
|
|
867
|
+
`Enforcement: ${guardCount}/4 guards active`,
|
|
767
868
|
];
|
|
768
869
|
|
|
769
|
-
console.log(box(`🧠 Dual-Brain v${version}`, dashLines));
|
|
770
870
|
console.log('');
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const recentSessions = importReplitSessions(cwd).slice(0, 5);
|
|
774
|
-
if (recentSessions.length > 0) {
|
|
775
|
-
console.log(separator('Recent Sessions'));
|
|
776
|
-
recentSessions.forEach((sess, i) => {
|
|
777
|
-
const activeIndicator = sess.isActive ? '●' : ' ';
|
|
778
|
-
const promptsLabel = `(${sess.promptCount} prompt${sess.promptCount !== 1 ? 's' : ''})`;
|
|
779
|
-
console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${activeIndicator} ${sess.name} ${promptsLabel}`);
|
|
780
|
-
});
|
|
781
|
-
console.log('');
|
|
782
|
-
}
|
|
783
|
-
|
|
871
|
+
console.log(box('Settings', settingsLines));
|
|
872
|
+
console.log('');
|
|
784
873
|
console.log(menu([
|
|
785
|
-
{ key: '
|
|
786
|
-
{ key: '
|
|
787
|
-
{ key: '
|
|
788
|
-
{ key: '
|
|
789
|
-
{ key: '
|
|
874
|
+
{ key: '1', label: 'Switch to cost-saver', section: 'Mode' },
|
|
875
|
+
{ key: '2', label: 'Switch to balanced', section: 'Mode' },
|
|
876
|
+
{ key: '3', label: 'Switch to quality-first', section: 'Mode' },
|
|
877
|
+
{ key: 'a', label: 'Add API key', section: 'Auth' },
|
|
878
|
+
{ key: 'i', label: 'Reinstall hooks', section: 'Enforcement' },
|
|
879
|
+
{ key: 'b', label: 'Back', section: '' },
|
|
790
880
|
]));
|
|
791
881
|
console.log('');
|
|
792
882
|
|
|
793
883
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
794
884
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
885
|
+
if (choice === '1' || choice === '2' || choice === '3') {
|
|
886
|
+
const modeMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
|
|
887
|
+
profile.mode = modeMap[choice];
|
|
888
|
+
saveProfile(profile, { cwd });
|
|
889
|
+
console.log(` Mode set to: ${profile.mode}`);
|
|
890
|
+
return { next: 'settings' };
|
|
799
891
|
}
|
|
800
892
|
|
|
801
|
-
if (choice === '
|
|
802
|
-
await
|
|
803
|
-
|
|
804
|
-
return { next: 'dashboard' };
|
|
893
|
+
if (choice === 'a') {
|
|
894
|
+
await setupAuth(rl);
|
|
895
|
+
return { next: 'settings' };
|
|
805
896
|
}
|
|
806
897
|
|
|
807
|
-
if (choice === '
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
898
|
+
if (choice === 'i') {
|
|
899
|
+
await cmdInstall();
|
|
900
|
+
return { next: 'settings' };
|
|
901
|
+
}
|
|
811
902
|
|
|
812
|
-
|
|
813
|
-
|
|
903
|
+
if (choice === 'b' || choice === 'back') { return { next: 'main' }; }
|
|
904
|
+
|
|
905
|
+
return { next: 'settings' };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// ─── Screen: dashboardScreen (kept for internal reference, unreachable) ───────
|
|
909
|
+
|
|
910
|
+
async function dashboardScreen(rl, ask) {
|
|
911
|
+
return { next: 'main' };
|
|
814
912
|
}
|
|
815
913
|
|
|
816
914
|
// ─── Screen: authScreen ───────────────────────────────────────────────────────
|
|
@@ -1301,12 +1399,15 @@ async function sessionDetailScreen(rl, ask, ctx = {}) {
|
|
|
1301
1399
|
// ─── Screen state machine ─────────────────────────────────────────────────────
|
|
1302
1400
|
|
|
1303
1401
|
const SCREENS = {
|
|
1304
|
-
welcome:
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1402
|
+
welcome: welcomeScreen,
|
|
1403
|
+
main: mainScreen,
|
|
1404
|
+
'new-session': newSessionScreen,
|
|
1405
|
+
settings: settingsScreen,
|
|
1406
|
+
dashboard: dashboardScreen,
|
|
1407
|
+
auth: authScreen,
|
|
1408
|
+
profile: profileScreen,
|
|
1409
|
+
diagnostics: diagnosticsScreen,
|
|
1410
|
+
repl: replScreen,
|
|
1310
1411
|
'session-detail': sessionDetailScreen,
|
|
1311
1412
|
};
|
|
1312
1413
|
|
|
@@ -1326,7 +1427,7 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
1326
1427
|
ctx = result?.session ? { session: result.session } : {};
|
|
1327
1428
|
} catch (e) {
|
|
1328
1429
|
console.error(`Error: ${e.message}`);
|
|
1329
|
-
current = '
|
|
1430
|
+
current = 'main';
|
|
1330
1431
|
ctx = {};
|
|
1331
1432
|
}
|
|
1332
1433
|
}
|
|
@@ -1349,8 +1450,7 @@ async function main() {
|
|
|
1349
1450
|
if (isInteractive) {
|
|
1350
1451
|
const cwd = process.cwd();
|
|
1351
1452
|
if (profileExists(cwd)) {
|
|
1352
|
-
|
|
1353
|
-
await runScreens('dashboard');
|
|
1453
|
+
await runScreens('main');
|
|
1354
1454
|
} else {
|
|
1355
1455
|
// First run: welcomeScreen handles auto-setup detection internally,
|
|
1356
1456
|
// then falls through to manual wizard if needed.
|