dual-brain 7.1.1 → 7.1.2
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 +381 -68
- package/package.json +1 -1
- package/src/profile.mjs +89 -0
- package/src/session.mjs +119 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
rememberPreference, forgetPreference, getActivePreferences,
|
|
13
13
|
getAvailableProviders, isSoloBrain, getHeadModel,
|
|
14
14
|
detectAuth, detectEnvironment, setupAuth,
|
|
15
|
+
autoSetup,
|
|
15
16
|
} from '../src/profile.mjs';
|
|
16
17
|
|
|
17
18
|
import { detectTask } from '../src/detect.mjs';
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
import { dispatch, detectRuntime, dispatchDualBrain } from '../src/dispatch.mjs';
|
|
28
29
|
|
|
29
30
|
import { loadRepoCache } from '../src/repo.mjs';
|
|
30
|
-
import { loadSession, saveSession, formatSessionCard } from '../src/session.mjs';
|
|
31
|
+
import { loadSession, saveSession, formatSessionCard, importReplitSessions } from '../src/session.mjs';
|
|
31
32
|
|
|
32
33
|
import { box, bar, badge, menu, separator } from '../src/tui.mjs';
|
|
33
34
|
|
|
@@ -485,11 +486,10 @@ function cmdForget(text) {
|
|
|
485
486
|
|
|
486
487
|
// ─── Screen helpers ───────────────────────────────────────────────────────────
|
|
487
488
|
|
|
488
|
-
function profileExists() {
|
|
489
|
-
const
|
|
490
|
-
const cwd = process.cwd();
|
|
489
|
+
function profileExists(cwd) {
|
|
490
|
+
const dir = cwd || process.cwd();
|
|
491
491
|
const globalPath = join(process.env.HOME || '/root', '.config', 'dual-brain', 'profile.json');
|
|
492
|
-
const projectPath = join(
|
|
492
|
+
const projectPath = join(dir, '.dualbrain', 'profile.json');
|
|
493
493
|
return existsSync(projectPath) || existsSync(globalPath);
|
|
494
494
|
}
|
|
495
495
|
|
|
@@ -497,12 +497,66 @@ function profileExists() {
|
|
|
497
497
|
|
|
498
498
|
async function welcomeScreen(rl, ask) {
|
|
499
499
|
const version = readVersion();
|
|
500
|
-
|
|
501
|
-
|
|
500
|
+
const cwd = process.cwd();
|
|
501
|
+
|
|
502
|
+
// --- Try auto-setup first ---
|
|
503
|
+
console.log(box(`🧠 Dual-Brain v${version} — Setup`, [
|
|
504
|
+
'Detecting environment...',
|
|
502
505
|
]));
|
|
503
506
|
console.log('');
|
|
504
507
|
|
|
505
|
-
|
|
508
|
+
const setup = await autoSetup(cwd);
|
|
509
|
+
|
|
510
|
+
if (setup.confident) {
|
|
511
|
+
// Build summary lines for the auto-detected state
|
|
512
|
+
const detectedLines = [
|
|
513
|
+
'Detecting environment...',
|
|
514
|
+
...setup.actions.map(a => `✓ ${a}`),
|
|
515
|
+
...setup.warnings.map(w => `⚠ ${w}`),
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
const modeLabel = setup.profile.mode === 'dual' ? 'dual mode, balanced'
|
|
519
|
+
: setup.profile.mode === 'solo-claude' ? 'Claude-only mode, balanced'
|
|
520
|
+
: setup.profile.mode === 'solo-openai' ? 'OpenAI-only mode, balanced'
|
|
521
|
+
: `${setup.profile.mode}, balanced`;
|
|
522
|
+
|
|
523
|
+
const readyBox = box(`🧠 Dual-Brain v${version} — Setup`, [
|
|
524
|
+
...detectedLines,
|
|
525
|
+
'',
|
|
526
|
+
`Ready to go! Auto-configured ${modeLabel}.`,
|
|
527
|
+
]);
|
|
528
|
+
console.log(readyBox);
|
|
529
|
+
console.log('');
|
|
530
|
+
console.log(' [Enter] Start coding →');
|
|
531
|
+
console.log(' [c] Customize setup');
|
|
532
|
+
console.log(' [a] Auth management');
|
|
533
|
+
console.log('');
|
|
534
|
+
|
|
535
|
+
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
536
|
+
|
|
537
|
+
if (choice === 'c') {
|
|
538
|
+
// Fall through to manual wizard below
|
|
539
|
+
} else if (choice === 'a') {
|
|
540
|
+
return { next: 'auth' };
|
|
541
|
+
} else {
|
|
542
|
+
// Enter or anything else → save and go to dashboard
|
|
543
|
+
saveProfile(setup.profile, { cwd });
|
|
544
|
+
return { next: 'dashboard' };
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
// Not confident — show what's missing before falling through to wizard
|
|
548
|
+
if (setup.warnings.length > 0) {
|
|
549
|
+
console.log(box(`🧠 Dual-Brain v${version} — Setup`, [
|
|
550
|
+
'Auto-detection incomplete:',
|
|
551
|
+
...setup.warnings.map(w => ` ✗ ${w}`),
|
|
552
|
+
'',
|
|
553
|
+
'Let\'s configure manually.',
|
|
554
|
+
]));
|
|
555
|
+
console.log('');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// --- Manual wizard (fallback or [c] Customize) ---
|
|
506
560
|
console.log(separator('Claude (Anthropic)'));
|
|
507
561
|
console.log(' (1) $20/mo Pro');
|
|
508
562
|
console.log(' (2) $100/mo Max 5x');
|
|
@@ -521,7 +575,6 @@ async function welcomeScreen(rl, ask) {
|
|
|
521
575
|
// Ask for API key immediately
|
|
522
576
|
const key = (await ask('Paste your Anthropic API key: ')).trim();
|
|
523
577
|
if (key) {
|
|
524
|
-
const { saveAuthKey } = await import('../src/profile.mjs').then(m => m).catch(() => ({}));
|
|
525
578
|
// Inline: set env var for this session, profile will persist
|
|
526
579
|
process.env.ANTHROPIC_API_KEY = key;
|
|
527
580
|
console.log('✓ Claude API key set for this session');
|
|
@@ -578,7 +631,6 @@ async function welcomeScreen(rl, ask) {
|
|
|
578
631
|
else if (modeChoice === '3') { mode = 'quality-first'; }
|
|
579
632
|
|
|
580
633
|
// --- Build and save profile ---
|
|
581
|
-
const cwd = process.cwd();
|
|
582
634
|
const existingProfile = loadProfile(cwd);
|
|
583
635
|
const profile = {
|
|
584
636
|
...existingProfile,
|
|
@@ -668,6 +720,19 @@ async function dashboardScreen(rl, ask) {
|
|
|
668
720
|
|
|
669
721
|
console.log(box(`🧠 Dual-Brain v${version}`, dashLines));
|
|
670
722
|
console.log('');
|
|
723
|
+
|
|
724
|
+
// ── Recent Sessions (replit-tools import) ──────────────────────────────────
|
|
725
|
+
const recentSessions = importReplitSessions(cwd).slice(0, 5);
|
|
726
|
+
if (recentSessions.length > 0) {
|
|
727
|
+
console.log(separator('Recent Sessions'));
|
|
728
|
+
recentSessions.forEach((sess, i) => {
|
|
729
|
+
const activeIndicator = sess.isActive ? '●' : ' ';
|
|
730
|
+
const promptsLabel = `(${sess.promptCount} prompt${sess.promptCount !== 1 ? 's' : ''})`;
|
|
731
|
+
console.log(` [${i + 1}] ${sess.age.padEnd(6)} ${activeIndicator} ${sess.name} ${promptsLabel}`);
|
|
732
|
+
});
|
|
733
|
+
console.log('');
|
|
734
|
+
}
|
|
735
|
+
|
|
671
736
|
console.log(menu([
|
|
672
737
|
{ key: 'g', label: 'Go — dispatch a task', section: 'Actions' },
|
|
673
738
|
{ key: 's', label: 'Status — detailed provider info', section: 'Actions' },
|
|
@@ -681,6 +746,12 @@ async function dashboardScreen(rl, ask) {
|
|
|
681
746
|
|
|
682
747
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
683
748
|
|
|
749
|
+
// Numeric choice → session detail
|
|
750
|
+
const numChoice = parseInt(choice, 10);
|
|
751
|
+
if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
|
|
752
|
+
return { next: 'session-detail', session: recentSessions[numChoice - 1] };
|
|
753
|
+
}
|
|
754
|
+
|
|
684
755
|
if (choice === 'g') {
|
|
685
756
|
const taskDesc = (await ask(' Task description: ')).trim();
|
|
686
757
|
if (taskDesc) {
|
|
@@ -827,77 +898,223 @@ async function profileScreen(rl, ask) {
|
|
|
827
898
|
|
|
828
899
|
async function diagnosticsScreen(rl, ask) {
|
|
829
900
|
const cwd = process.cwd();
|
|
901
|
+
const { spawnSync: _spawnSync } = await import('child_process');
|
|
902
|
+
const { readdirSync } = await import('node:fs');
|
|
903
|
+
const { detectPlans } = await import('../src/profile.mjs');
|
|
904
|
+
|
|
905
|
+
// ── Version info ──────────────────────────────────────────────────────────
|
|
830
906
|
const version = readVersion();
|
|
831
|
-
const
|
|
832
|
-
|
|
907
|
+
const nodeVersion = process.version;
|
|
908
|
+
|
|
909
|
+
// ── Provider health ───────────────────────────────────────────────────────
|
|
910
|
+
const auth = await detectAuth();
|
|
911
|
+
const plans = detectPlans();
|
|
912
|
+
const { states: healthStates } = getHealth(cwd);
|
|
913
|
+
|
|
914
|
+
function _providerBadge(name) {
|
|
915
|
+
const entries = Object.entries(healthStates).filter(([k]) => k.startsWith(`${name}:`));
|
|
916
|
+
if (entries.length === 0) return '✅ healthy';
|
|
917
|
+
const statuses = entries.map(([, v]) => v.status);
|
|
918
|
+
if (statuses.includes('hot')) return '🔴 hot';
|
|
919
|
+
if (statuses.includes('degraded')) return '⚠️ degraded';
|
|
920
|
+
if (statuses.includes('probing')) return '⚠️ probing';
|
|
921
|
+
return '✅ healthy';
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const claudeStatus = auth.claude.found ? _providerBadge('claude') : '❌ no auth';
|
|
925
|
+
const openaiStatus = auth.openai.found ? _providerBadge('openai') : '❌ no auth';
|
|
926
|
+
const claudePlanStr = plans.claude ? `Max ${plans.claude}` : (auth.claude.masked ?? 'unknown');
|
|
927
|
+
const openaiPlanStr = plans.openai ? `Pro ${plans.openai}` : (auth.openai.masked ?? 'unknown');
|
|
928
|
+
const claudeAuthStr = auth.claude.masked ?? 'not configured';
|
|
929
|
+
const openaiAuthStr = auth.openai.masked ?? 'not configured';
|
|
930
|
+
|
|
931
|
+
// ── Enforcement checks ────────────────────────────────────────────────────
|
|
932
|
+
const hooksDir = join(cwd, '.claude', 'hooks');
|
|
933
|
+
const headGuardExists = existsSync(join(hooksDir, 'head-guard.mjs'));
|
|
934
|
+
const enforceTierExists = existsSync(join(hooksDir, 'enforce-tier.mjs'));
|
|
833
935
|
|
|
834
|
-
// Enforcement check
|
|
835
936
|
let guardCount = 0;
|
|
836
|
-
let guardDetails = 'NOT INSTALLED';
|
|
837
937
|
try {
|
|
838
938
|
const settingsFile = join(cwd, '.claude', 'settings.json');
|
|
839
939
|
if (existsSync(settingsFile)) {
|
|
840
940
|
const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
|
|
841
941
|
const preToolUse = settings?.hooks?.PreToolUse ?? [];
|
|
842
|
-
const guardCmd
|
|
843
|
-
const tierCmd
|
|
844
|
-
const hasEdit
|
|
845
|
-
const hasWrite
|
|
846
|
-
const hasBash
|
|
847
|
-
const hasAgent
|
|
942
|
+
const guardCmd = 'node .claude/hooks/head-guard.mjs';
|
|
943
|
+
const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
|
|
944
|
+
const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
|
|
945
|
+
const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
|
|
946
|
+
const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
|
|
947
|
+
const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
|
|
848
948
|
guardCount = [hasEdit, hasWrite, hasBash, hasAgent].filter(Boolean).length;
|
|
849
|
-
guardDetails = guardCount === 4
|
|
850
|
-
? `${guardCount}/4 guards active (Edit, Write, Bash, Agent)`
|
|
851
|
-
: `${guardCount}/4 guards — run: dual-brain install`;
|
|
852
949
|
}
|
|
853
|
-
} catch {
|
|
950
|
+
} catch { /* ignore */ }
|
|
854
951
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
952
|
+
let hookifyCount = 0;
|
|
953
|
+
try {
|
|
954
|
+
const claudeDir = join(cwd, '.claude');
|
|
955
|
+
if (existsSync(claudeDir)) {
|
|
956
|
+
hookifyCount = readdirSync(claudeDir).filter(f => f.startsWith('hookify.') && f.endsWith('.md')).length;
|
|
957
|
+
}
|
|
958
|
+
} catch { /* ignore */ }
|
|
959
|
+
|
|
960
|
+
// ── Replit-tools integration ──────────────────────────────────────────────
|
|
961
|
+
const replitToolsDir = join(cwd, '.replit-tools');
|
|
962
|
+
const hasReplitTools = existsSync(replitToolsDir);
|
|
963
|
+
const persistentDir = join(replitToolsDir, '.claude-persistent');
|
|
964
|
+
const sessionManagerExists = existsSync(join(replitToolsDir, 'scripts', 'claude-session-manager.sh'));
|
|
965
|
+
const authRefreshScript = join(replitToolsDir, 'scripts', 'claude-auth-refresh.sh');
|
|
966
|
+
|
|
967
|
+
let credsFresh = null;
|
|
968
|
+
let credsExpiry = null;
|
|
969
|
+
let historyCount = 0;
|
|
970
|
+
|
|
971
|
+
if (hasReplitTools) {
|
|
972
|
+
try {
|
|
973
|
+
const credsFile = join(persistentDir, '.credentials.json');
|
|
974
|
+
const creds = JSON.parse(readFileSync(credsFile, 'utf8'));
|
|
975
|
+
const expiresAt = creds?.claudeAiOauth?.expiresAt;
|
|
976
|
+
if (expiresAt) {
|
|
977
|
+
const expiresMs = typeof expiresAt === 'number' ? expiresAt : Date.parse(expiresAt);
|
|
978
|
+
credsFresh = Date.now() < expiresMs;
|
|
979
|
+
credsExpiry = new Date(expiresMs).toISOString().slice(0, 10);
|
|
980
|
+
}
|
|
981
|
+
} catch { /* credentials missing or unreadable */ }
|
|
982
|
+
|
|
983
|
+
try {
|
|
984
|
+
const histFile = join(persistentDir, 'history.jsonl');
|
|
985
|
+
if (existsSync(histFile)) {
|
|
986
|
+
historyCount = readFileSync(histFile, 'utf8').split('\n').filter(Boolean).length;
|
|
987
|
+
}
|
|
988
|
+
} catch { /* ignore */ }
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// ── Quality checks ────────────────────────────────────────────────────────
|
|
992
|
+
let testPass = null; let testTotal = null; let testError = null;
|
|
993
|
+
try {
|
|
994
|
+
const r = _spawnSync('node', ['--test', 'src/test.mjs'], { cwd, encoding: 'utf8', timeout: 30000 });
|
|
995
|
+
const out = (r.stdout ?? '') + (r.stderr ?? '');
|
|
996
|
+
const pm = out.match(/# pass (\d+)/);
|
|
997
|
+
const tm = out.match(/# tests (\d+)/);
|
|
998
|
+
if (pm && tm) { testPass = parseInt(pm[1], 10); testTotal = parseInt(tm[1], 10); }
|
|
999
|
+
else { testError = 'could not parse output'; }
|
|
1000
|
+
} catch (e) { testError = e.message; }
|
|
1001
|
+
|
|
1002
|
+
let healthPass = null; let healthTotal = null; let healthError = null;
|
|
1003
|
+
try {
|
|
1004
|
+
const healthScript = join(hooksDir, 'health-check.mjs');
|
|
1005
|
+
if (existsSync(healthScript)) {
|
|
1006
|
+
const r = _spawnSync('node', [healthScript], { cwd, encoding: 'utf8', timeout: 15000 });
|
|
1007
|
+
const out = (r.stdout ?? '') + (r.stderr ?? '');
|
|
1008
|
+
// Try summary line first: "8 pass, 0 warn, 0 fail"
|
|
1009
|
+
const sm = out.match(/(\d+) pass,\s*(\d+) warn,\s*(\d+) fail/);
|
|
1010
|
+
if (sm) {
|
|
1011
|
+
healthPass = parseInt(sm[1], 10);
|
|
1012
|
+
healthTotal = parseInt(sm[1], 10) + parseInt(sm[2], 10) + parseInt(sm[3], 10);
|
|
1013
|
+
} else {
|
|
1014
|
+
// Fall back to JSON block
|
|
1015
|
+
const jm = out.match(/\{[\s\S]*?"healthy"[\s\S]*?\}/);
|
|
1016
|
+
if (jm) {
|
|
1017
|
+
try {
|
|
1018
|
+
const p = JSON.parse(jm[0]);
|
|
1019
|
+
healthPass = p.pass ?? 0;
|
|
1020
|
+
healthTotal = (p.pass ?? 0) + (p.warn ?? 0) + (p.fail ?? 0);
|
|
1021
|
+
} catch { healthError = 'could not parse output'; }
|
|
1022
|
+
} else { healthError = 'could not parse output'; }
|
|
1023
|
+
}
|
|
1024
|
+
} else { healthError = 'health-check.mjs not found'; }
|
|
1025
|
+
} catch (e) { healthError = e.message; }
|
|
1026
|
+
|
|
1027
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
1028
|
+
const W = 56;
|
|
1029
|
+
const hbar = '═'.repeat(W);
|
|
1030
|
+
// Pad a string to exactly W visible columns (for box rows without leading ║ prefix)
|
|
1031
|
+
const padRow = (s) => {
|
|
1032
|
+
const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
|
|
1033
|
+
let vlen = 0;
|
|
1034
|
+
for (const ch of plain) {
|
|
1035
|
+
const cp = ch.codePointAt(0);
|
|
1036
|
+
if ((cp >= 0x1f300 && cp <= 0x1faff) || (cp >= 0x2600 && cp <= 0x27bf) || cp === 0xfe0f || cp === 0x20e3) vlen += 2;
|
|
1037
|
+
else vlen += 1;
|
|
1038
|
+
}
|
|
1039
|
+
return s + ' '.repeat(Math.max(0, W - vlen));
|
|
1040
|
+
};
|
|
1041
|
+
const hrow = (s) => `║${padRow(' ' + s)}║`;
|
|
1042
|
+
|
|
1043
|
+
const output = [
|
|
1044
|
+
`╔${hbar}╗`,
|
|
1045
|
+
hrow('🔧 Diagnostics'),
|
|
1046
|
+
`╠${hbar}╣`,
|
|
1047
|
+
hrow(`dual-brain v${version}`),
|
|
1048
|
+
hrow(`Node.js ${nodeVersion}`),
|
|
1049
|
+
`╚${hbar}╝`,
|
|
869
1050
|
'',
|
|
870
|
-
'
|
|
871
|
-
`
|
|
872
|
-
`
|
|
873
|
-
` CI: ${env.isCI ? 'yes' : 'no'}`,
|
|
1051
|
+
separator('Provider Health'),
|
|
1052
|
+
` ${claudeStatus.padEnd(14)} Claude ${claudePlanStr.padEnd(16)} ${claudeAuthStr}`,
|
|
1053
|
+
` ${openaiStatus.padEnd(14)} OpenAI ${openaiPlanStr.padEnd(16)} ${openaiAuthStr}`,
|
|
874
1054
|
'',
|
|
875
|
-
'
|
|
876
|
-
`
|
|
877
|
-
`
|
|
878
|
-
`
|
|
1055
|
+
separator('Enforcement'),
|
|
1056
|
+
` ${headGuardExists ? '✅' : '❌'} head-guard.mjs ${headGuardExists ? 'installed' : 'missing — run: dual-brain install'}`,
|
|
1057
|
+
` ${enforceTierExists ? '✅' : '❌'} enforce-tier.mjs ${enforceTierExists ? 'installed' : 'missing — run: dual-brain install'}`,
|
|
1058
|
+
` ${guardCount === 4 ? '✅' : '⚠️ '} settings.json ${guardCount}/4 guards registered${guardCount < 4 ? ' — run: dual-brain install' : ''}`,
|
|
1059
|
+
` ${hookifyCount > 0 ? '✅' : '⚠️ '} hookify rules ${hookifyCount} rules${hookifyCount > 0 ? ' (check: ls .claude/hookify.*.md)' : ' — none found'}`,
|
|
879
1060
|
'',
|
|
880
|
-
'
|
|
881
|
-
|
|
1061
|
+
separator('Replit Tools'),
|
|
1062
|
+
` ${hasReplitTools ? '✅' : '❌'} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
|
|
882
1063
|
];
|
|
883
1064
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
])
|
|
1065
|
+
if (hasReplitTools) {
|
|
1066
|
+
if (credsFresh === null) {
|
|
1067
|
+
output.push(' ⚠️ Claude auth credentials file missing');
|
|
1068
|
+
} else if (credsFresh) {
|
|
1069
|
+
output.push(` ✅ Claude auth fresh (expires: ${credsExpiry})`);
|
|
1070
|
+
} else {
|
|
1071
|
+
output.push(` ❌ Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
|
|
1072
|
+
}
|
|
1073
|
+
output.push(` ✅ Session archive ${historyCount} entries`);
|
|
1074
|
+
output.push(` ${sessionManagerExists ? '✅' : '⚠️ '} Session manager ${sessionManagerExists ? 'available' : 'not found'}`);
|
|
1075
|
+
} else {
|
|
1076
|
+
output.push(' ─── (not available)');
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
output.push('');
|
|
1080
|
+
output.push(separator('Quality'));
|
|
1081
|
+
if (testError) {
|
|
1082
|
+
output.push(` ❌ Tests error: ${testError}`);
|
|
1083
|
+
} else if (testPass !== null) {
|
|
1084
|
+
output.push(` ${testPass === testTotal ? '✅' : '❌'} Tests ${testPass}/${testTotal} passing`);
|
|
1085
|
+
}
|
|
1086
|
+
if (healthError) {
|
|
1087
|
+
output.push(` ❌ Health check error: ${healthError}`);
|
|
1088
|
+
} else if (healthPass !== null) {
|
|
1089
|
+
output.push(` ${healthPass === healthTotal ? '✅' : '⚠️ '} Health check ${healthPass}/${healthTotal} passing`);
|
|
1090
|
+
}
|
|
1091
|
+
output.push('');
|
|
1092
|
+
|
|
1093
|
+
console.log(output.join('\n'));
|
|
1094
|
+
|
|
1095
|
+
// Actions menu
|
|
1096
|
+
const menuOpts = [
|
|
1097
|
+
{ key: 'h', label: 'Run health check', section: 'Actions' },
|
|
1098
|
+
{ key: 't', label: 'Run test suite', section: 'Actions' },
|
|
1099
|
+
];
|
|
1100
|
+
if (hasReplitTools && existsSync(authRefreshScript)) {
|
|
1101
|
+
menuOpts.push({ key: 'r', label: 'Refresh auth (replit-tools)', section: 'Actions' });
|
|
1102
|
+
}
|
|
1103
|
+
menuOpts.push({ key: 'i', label: 'Reinstall hooks', section: 'Actions' });
|
|
1104
|
+
menuOpts.push({ key: 'b', label: 'Back to dashboard', section: 'Actions' });
|
|
1105
|
+
console.log(menu(menuOpts));
|
|
891
1106
|
console.log('');
|
|
892
1107
|
|
|
893
1108
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
894
1109
|
|
|
895
1110
|
if (choice === 'h') {
|
|
896
|
-
const hookScript = join(
|
|
1111
|
+
const hookScript = join(hooksDir, 'health-check.mjs');
|
|
1112
|
+
console.log('');
|
|
897
1113
|
if (existsSync(hookScript)) {
|
|
898
1114
|
try {
|
|
899
|
-
|
|
900
|
-
|
|
1115
|
+
const r = _spawnSync('node', [hookScript], { stdio: 'inherit', cwd });
|
|
1116
|
+
if (r.error) console.log(` Error: ${r.error.message}`);
|
|
1117
|
+
} catch (e) { console.log(` Error: ${e.message}`); }
|
|
901
1118
|
} else {
|
|
902
1119
|
console.log(' health-check.mjs not found — run: dual-brain install');
|
|
903
1120
|
}
|
|
@@ -905,6 +1122,32 @@ async function diagnosticsScreen(rl, ask) {
|
|
|
905
1122
|
return { next: 'diagnostics' };
|
|
906
1123
|
}
|
|
907
1124
|
|
|
1125
|
+
if (choice === 't') {
|
|
1126
|
+
console.log('\n Running test suite...\n');
|
|
1127
|
+
try {
|
|
1128
|
+
const r = _spawnSync('node', ['--test', 'src/test.mjs'], { stdio: 'inherit', cwd, timeout: 60000 });
|
|
1129
|
+
if (r.error) console.log(` Error: ${r.error.message}`);
|
|
1130
|
+
} catch (e) { console.log(` Error: ${e.message}`); }
|
|
1131
|
+
await ask('\n Press Enter to continue...');
|
|
1132
|
+
return { next: 'diagnostics' };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (choice === 'r') {
|
|
1136
|
+
if (existsSync(authRefreshScript)) {
|
|
1137
|
+
console.log('\n Refreshing Claude auth...\n');
|
|
1138
|
+
try {
|
|
1139
|
+
const r = _spawnSync('bash', [authRefreshScript], { stdio: 'inherit', cwd, timeout: 30000 });
|
|
1140
|
+
if (r.error) console.log(` Error: ${r.error.message}`);
|
|
1141
|
+
else if (r.status === 0) console.log('\n Auth refresh complete.');
|
|
1142
|
+
else console.log(`\n Auth refresh exited with code ${r.status}.`);
|
|
1143
|
+
} catch (e) { console.log(` Error: ${e.message}`); }
|
|
1144
|
+
} else {
|
|
1145
|
+
console.log(' claude-auth-refresh.sh not found.');
|
|
1146
|
+
}
|
|
1147
|
+
await ask('\n Press Enter to continue...');
|
|
1148
|
+
return { next: 'diagnostics' };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
908
1151
|
if (choice === 'i') {
|
|
909
1152
|
await cmdInstall();
|
|
910
1153
|
return { next: 'diagnostics' };
|
|
@@ -963,15 +1206,75 @@ async function replScreen(rl, ask) {
|
|
|
963
1206
|
}
|
|
964
1207
|
}
|
|
965
1208
|
|
|
1209
|
+
// ─── Screen: sessionDetailScreen ─────────────────────────────────────────────
|
|
1210
|
+
|
|
1211
|
+
async function sessionDetailScreen(rl, ask, ctx = {}) {
|
|
1212
|
+
const sess = ctx.session;
|
|
1213
|
+
if (!sess) return { next: 'dashboard' };
|
|
1214
|
+
|
|
1215
|
+
const W = 56;
|
|
1216
|
+
const hbar = '═'.repeat(W + 2);
|
|
1217
|
+
const pad = (s) => {
|
|
1218
|
+
const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
|
|
1219
|
+
return s + ' '.repeat(Math.max(0, W - plain.length));
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
const statusLine = sess.isActive
|
|
1223
|
+
? `active`
|
|
1224
|
+
: `inactive`;
|
|
1225
|
+
|
|
1226
|
+
const detailLines = [
|
|
1227
|
+
` Session: ${sess.name}`,
|
|
1228
|
+
`╠${hbar}╣`,
|
|
1229
|
+
` ID: ${sess.id.slice(0, 8)}...`,
|
|
1230
|
+
` Status: ${statusLine}`,
|
|
1231
|
+
` Prompts: ${sess.promptCount}`,
|
|
1232
|
+
` Last active: ${sess.age}`,
|
|
1233
|
+
` Project: ${sess.project || process.cwd()}`,
|
|
1234
|
+
];
|
|
1235
|
+
|
|
1236
|
+
console.log(`╔${hbar}╗`);
|
|
1237
|
+
for (const line of detailLines) {
|
|
1238
|
+
console.log(`║ ${pad(line)}║`);
|
|
1239
|
+
}
|
|
1240
|
+
console.log(`╚${hbar}╝`);
|
|
1241
|
+
console.log('');
|
|
1242
|
+
|
|
1243
|
+
if (sess.isActive) {
|
|
1244
|
+
console.log(' [c] Continue this session (claude --continue)');
|
|
1245
|
+
} else {
|
|
1246
|
+
console.log(' [r] Resume this session (claude --resume)');
|
|
1247
|
+
}
|
|
1248
|
+
console.log(' [b] Back to dashboard');
|
|
1249
|
+
console.log('');
|
|
1250
|
+
|
|
1251
|
+
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
1252
|
+
|
|
1253
|
+
if (choice === 'c' || choice === 'r') {
|
|
1254
|
+
console.log(`\n Launching: claude --resume ${sess.id}\n`);
|
|
1255
|
+
try {
|
|
1256
|
+
const { spawnSync } = await import('node:child_process');
|
|
1257
|
+
spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
|
|
1258
|
+
} catch {
|
|
1259
|
+
console.log(' Could not launch claude CLI. Run manually:');
|
|
1260
|
+
console.log(` claude --resume ${sess.id}`);
|
|
1261
|
+
}
|
|
1262
|
+
return { next: 'dashboard' };
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return { next: 'dashboard' };
|
|
1266
|
+
}
|
|
1267
|
+
|
|
966
1268
|
// ─── Screen state machine ─────────────────────────────────────────────────────
|
|
967
1269
|
|
|
968
1270
|
const SCREENS = {
|
|
969
|
-
welcome:
|
|
970
|
-
dashboard:
|
|
971
|
-
auth:
|
|
972
|
-
profile:
|
|
973
|
-
diagnostics:
|
|
974
|
-
repl:
|
|
1271
|
+
welcome: welcomeScreen,
|
|
1272
|
+
dashboard: dashboardScreen,
|
|
1273
|
+
auth: authScreen,
|
|
1274
|
+
profile: profileScreen,
|
|
1275
|
+
diagnostics: diagnosticsScreen,
|
|
1276
|
+
repl: replScreen,
|
|
1277
|
+
'session-detail': sessionDetailScreen,
|
|
975
1278
|
};
|
|
976
1279
|
|
|
977
1280
|
async function runScreens(startScreen = 'dashboard') {
|
|
@@ -979,15 +1282,19 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
979
1282
|
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
980
1283
|
|
|
981
1284
|
let current = startScreen;
|
|
1285
|
+
let ctx = {};
|
|
982
1286
|
while (current && current !== 'exit') {
|
|
983
1287
|
const screen = SCREENS[current];
|
|
984
1288
|
if (!screen) break;
|
|
985
1289
|
try {
|
|
986
|
-
const result = await screen(rl, ask);
|
|
1290
|
+
const result = await screen(rl, ask, ctx);
|
|
987
1291
|
current = result?.next || 'exit';
|
|
1292
|
+
// Pass through context (e.g. selected session) to next screen
|
|
1293
|
+
ctx = result?.session ? { session: result.session } : {};
|
|
988
1294
|
} catch (e) {
|
|
989
1295
|
console.error(`Error: ${e.message}`);
|
|
990
1296
|
current = 'dashboard'; // recover to dashboard on error
|
|
1297
|
+
ctx = {};
|
|
991
1298
|
}
|
|
992
1299
|
}
|
|
993
1300
|
rl.close();
|
|
@@ -1007,9 +1314,15 @@ async function main() {
|
|
|
1007
1314
|
|
|
1008
1315
|
if (!cmd) {
|
|
1009
1316
|
if (isInteractive) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1317
|
+
const cwd = process.cwd();
|
|
1318
|
+
if (profileExists(cwd)) {
|
|
1319
|
+
// Profile already exists → go straight to dashboard
|
|
1320
|
+
await runScreens('dashboard');
|
|
1321
|
+
} else {
|
|
1322
|
+
// First run: welcomeScreen handles auto-setup detection internally,
|
|
1323
|
+
// then falls through to manual wizard if needed.
|
|
1324
|
+
await runScreens('welcome');
|
|
1325
|
+
}
|
|
1013
1326
|
} else {
|
|
1014
1327
|
// Non-TTY: print status card and exit
|
|
1015
1328
|
const cwd = process.cwd();
|
package/package.json
CHANGED
package/src/profile.mjs
CHANGED
|
@@ -876,6 +876,94 @@ if (isMain) main().catch(e => { process.stderr.write(e.message + '\n'); process.
|
|
|
876
876
|
// Exports
|
|
877
877
|
// ---------------------------------------------------------------------------
|
|
878
878
|
|
|
879
|
+
// ---------------------------------------------------------------------------
|
|
880
|
+
// Auto-setup (1-click, no user input required)
|
|
881
|
+
// ---------------------------------------------------------------------------
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Attempt to configure a profile entirely from detected state — no user input.
|
|
885
|
+
*
|
|
886
|
+
* Returns:
|
|
887
|
+
* {
|
|
888
|
+
* confident: boolean, // true when at least one provider was found
|
|
889
|
+
* profile: object|null, // fully-built profile ready to save, or null
|
|
890
|
+
* warnings: string[], // non-fatal issues (e.g. missing provider)
|
|
891
|
+
* actions: string[], // human-readable lines for the summary box
|
|
892
|
+
* }
|
|
893
|
+
*
|
|
894
|
+
* IMPORTANT: this function NEVER stores credentials — it only reads what's
|
|
895
|
+
* already present on disk / in environment variables.
|
|
896
|
+
*/
|
|
897
|
+
async function autoSetup(cwd) {
|
|
898
|
+
const env = detectEnvironment();
|
|
899
|
+
const auth = await detectAuth();
|
|
900
|
+
const plans = detectPlans();
|
|
901
|
+
|
|
902
|
+
const result = {
|
|
903
|
+
confident: false,
|
|
904
|
+
profile: null,
|
|
905
|
+
warnings: [],
|
|
906
|
+
actions: [],
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
// Need at least one provider authenticated
|
|
910
|
+
if (!auth.claude.found && !auth.openai.found) {
|
|
911
|
+
result.warnings.push('No provider credentials found');
|
|
912
|
+
return result;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Build profile from detected state
|
|
916
|
+
const profile = defaultProfile();
|
|
917
|
+
|
|
918
|
+
// Claude
|
|
919
|
+
if (auth.claude.found) {
|
|
920
|
+
profile.providers.claude.enabled = true;
|
|
921
|
+
profile.providers.claude.plan = plans.claude || '$20';
|
|
922
|
+
const planLabel = {
|
|
923
|
+
'$20': 'Claude Pro ($20)',
|
|
924
|
+
'$100': 'Claude Max x5 ($100)',
|
|
925
|
+
'$200': 'Claude Max x20 ($200)',
|
|
926
|
+
}[profile.providers.claude.plan] || profile.providers.claude.plan;
|
|
927
|
+
result.actions.push(`${planLabel} via ${auth.claude.source}`);
|
|
928
|
+
} else {
|
|
929
|
+
profile.providers.claude.enabled = false;
|
|
930
|
+
result.warnings.push('Claude not authenticated');
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// OpenAI
|
|
934
|
+
if (auth.openai.found) {
|
|
935
|
+
profile.providers.openai.enabled = true;
|
|
936
|
+
profile.providers.openai.plan = plans.openai || '$20';
|
|
937
|
+
const planLabel = {
|
|
938
|
+
'$20': 'ChatGPT Plus ($20)',
|
|
939
|
+
'$100': 'ChatGPT Pro ($100)',
|
|
940
|
+
'$200': 'ChatGPT Pro ($200)',
|
|
941
|
+
}[profile.providers.openai.plan] || profile.providers.openai.plan;
|
|
942
|
+
result.actions.push(`${planLabel} via ${auth.openai.source}`);
|
|
943
|
+
} else {
|
|
944
|
+
profile.providers.openai.enabled = false;
|
|
945
|
+
result.warnings.push('OpenAI not authenticated');
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Mode
|
|
949
|
+
const enabledCount = [auth.claude.found, auth.openai.found].filter(Boolean).length;
|
|
950
|
+
profile.mode = enabledCount >= 2 ? 'dual'
|
|
951
|
+
: auth.claude.found ? 'solo-claude'
|
|
952
|
+
: 'solo-openai';
|
|
953
|
+
profile.bias = 'balanced';
|
|
954
|
+
|
|
955
|
+
// Environment note
|
|
956
|
+
if (env.isReplit && env.hasReplitTools) {
|
|
957
|
+
result.actions.push('Replit + replit-tools detected');
|
|
958
|
+
} else if (env.isReplit) {
|
|
959
|
+
result.actions.push('Replit environment detected');
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
result.confident = true;
|
|
963
|
+
result.profile = profile;
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
966
|
+
|
|
879
967
|
export {
|
|
880
968
|
loadProfile, saveProfile, ensureProfile, runOnboarding,
|
|
881
969
|
rememberPreference, forgetPreference, getActivePreferences,
|
|
@@ -884,4 +972,5 @@ export {
|
|
|
884
972
|
detectAuth, detectEnvironment,
|
|
885
973
|
setupAuth, saveAuthKey, loadAuthKeys,
|
|
886
974
|
getActiveKey, removeAuthKey, disableKey, rotateToNextKey,
|
|
975
|
+
defaultProfile, autoSetup,
|
|
887
976
|
};
|
package/src/session.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* formatSessionCard(session, repo, health) → compact status card string (≤5 lines)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, renameSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, renameSync, readdirSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
|
|
16
16
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -197,6 +197,124 @@ export function formatSessionCard(session, repo, health) {
|
|
|
197
197
|
return lines.join('\n');
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// ─── Replit-tools session import ──────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Human-readable time-ago string from a Unix timestamp (ms).
|
|
204
|
+
* @param {number} timestamp
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
function timeAgo(timestamp) {
|
|
208
|
+
const diff = Date.now() - timestamp;
|
|
209
|
+
const mins = Math.floor(diff / 60000);
|
|
210
|
+
if (mins < 1) return 'just now';
|
|
211
|
+
if (mins < 60) return `${mins}m ago`;
|
|
212
|
+
const hours = Math.floor(mins / 60);
|
|
213
|
+
if (hours < 24) return `${hours}h ago`;
|
|
214
|
+
const days = Math.floor(hours / 24);
|
|
215
|
+
return `${days}d ago`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Import sessions from replit-tools history.jsonl.
|
|
220
|
+
* Returns an array of session summary objects, sorted most-recent first.
|
|
221
|
+
* Returns [] gracefully if replit-tools is not present.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} cwd
|
|
224
|
+
* @returns {Array<{
|
|
225
|
+
* id: string, name: string, project: string,
|
|
226
|
+
* promptCount: number, lastActive: string,
|
|
227
|
+
* isActive: boolean, source: string, age: string
|
|
228
|
+
* }>}
|
|
229
|
+
*/
|
|
230
|
+
export function importReplitSessions(cwd = process.cwd()) {
|
|
231
|
+
const sessions = [];
|
|
232
|
+
const replitBase = join(cwd, '.replit-tools', '.claude-persistent');
|
|
233
|
+
|
|
234
|
+
// Read history.jsonl
|
|
235
|
+
const historyPath = join(replitBase, 'history.jsonl');
|
|
236
|
+
if (!existsSync(historyPath)) return sessions;
|
|
237
|
+
|
|
238
|
+
let lines;
|
|
239
|
+
try {
|
|
240
|
+
lines = readFileSync(historyPath, 'utf8').split('\n').filter(Boolean);
|
|
241
|
+
} catch { return sessions; }
|
|
242
|
+
|
|
243
|
+
const bySession = new Map(); // sessionId → { entries, firstPrompt, lastTimestamp }
|
|
244
|
+
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
try {
|
|
247
|
+
const entry = JSON.parse(line);
|
|
248
|
+
if (!entry.sessionId) continue;
|
|
249
|
+
|
|
250
|
+
if (!bySession.has(entry.sessionId)) {
|
|
251
|
+
bySession.set(entry.sessionId, {
|
|
252
|
+
sessionId: entry.sessionId,
|
|
253
|
+
project: entry.project,
|
|
254
|
+
entries: [],
|
|
255
|
+
firstPrompt: null,
|
|
256
|
+
lastTimestamp: 0,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const sess = bySession.get(entry.sessionId);
|
|
261
|
+
sess.entries.push(entry);
|
|
262
|
+
if (entry.timestamp > sess.lastTimestamp) sess.lastTimestamp = entry.timestamp;
|
|
263
|
+
|
|
264
|
+
// Find first meaningful user prompt (not slash commands, not login, not pastes)
|
|
265
|
+
if (!sess.firstPrompt && entry.display
|
|
266
|
+
&& !entry.display.startsWith('/')
|
|
267
|
+
&& !entry.display.startsWith('login')
|
|
268
|
+
&& !entry.display.startsWith('[Pasted')) {
|
|
269
|
+
sess.firstPrompt = entry.display;
|
|
270
|
+
}
|
|
271
|
+
} catch { continue; }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Read active terminal sessions
|
|
275
|
+
const sessionsDir = join(cwd, '.replit-tools', '.claude-sessions');
|
|
276
|
+
const activeSessionIds = new Set();
|
|
277
|
+
if (existsSync(sessionsDir)) {
|
|
278
|
+
try {
|
|
279
|
+
for (const f of readdirSync(sessionsDir)) {
|
|
280
|
+
try {
|
|
281
|
+
const data = JSON.parse(readFileSync(join(sessionsDir, f), 'utf8'));
|
|
282
|
+
if (data.sessionId) activeSessionIds.add(data.sessionId);
|
|
283
|
+
} catch { continue; }
|
|
284
|
+
}
|
|
285
|
+
} catch { /* non-fatal */ }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Build session list
|
|
289
|
+
for (const [id, sess] of bySession) {
|
|
290
|
+
// Derive display name
|
|
291
|
+
let name = sess.firstPrompt;
|
|
292
|
+
if (!name) {
|
|
293
|
+
// Fallback: use first non-login display
|
|
294
|
+
const firstReal = sess.entries.find(e => e.display && e.display !== 'login');
|
|
295
|
+
name = firstReal?.display || `Session ${id.slice(0, 8)}`;
|
|
296
|
+
}
|
|
297
|
+
// Truncate long names
|
|
298
|
+
if (name.length > 60) name = name.slice(0, 57) + '...';
|
|
299
|
+
|
|
300
|
+
sessions.push({
|
|
301
|
+
id: sess.sessionId,
|
|
302
|
+
name,
|
|
303
|
+
project: sess.project,
|
|
304
|
+
promptCount: sess.entries.length,
|
|
305
|
+
lastActive: new Date(sess.lastTimestamp).toISOString(),
|
|
306
|
+
isActive: activeSessionIds.has(id),
|
|
307
|
+
source: 'replit-tools',
|
|
308
|
+
age: timeAgo(sess.lastTimestamp),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Sort by most recent first
|
|
313
|
+
sessions.sort((a, b) => new Date(b.lastActive) - new Date(a.lastActive));
|
|
314
|
+
|
|
315
|
+
return sessions;
|
|
316
|
+
}
|
|
317
|
+
|
|
200
318
|
// ─── CLI (direct invocation) ──────────────────────────────────────────────────
|
|
201
319
|
|
|
202
320
|
const isMain = process.argv[1]?.endsWith('session.mjs');
|