dual-brain 7.1.1 → 7.1.3
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 +382 -84
- package/package.json +1 -1
- package/src/profile.mjs +89 -0
- package/src/session.mjs +142 -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,29 +720,34 @@ 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
|
-
{ key: '
|
|
673
|
-
{ key: 's', label: 'Status — detailed provider info', section: 'Actions' },
|
|
737
|
+
{ key: 's', label: 'Status — detailed provider info', section: 'Info' },
|
|
674
738
|
{ key: 'p', label: 'Profile & preferences', section: 'Settings' },
|
|
675
739
|
{ key: 'a', label: 'Auth management', section: 'Settings' },
|
|
676
|
-
{ key: 'd', label: 'Diagnostics',
|
|
677
|
-
{ key: '
|
|
678
|
-
{ key: 'q', label: 'Exit', section: 'Session' },
|
|
740
|
+
{ key: 'd', label: 'Diagnostics & repair', section: 'Settings' },
|
|
741
|
+
{ key: 'q', label: 'Exit to shell', section: '' },
|
|
679
742
|
]));
|
|
680
743
|
console.log('');
|
|
681
744
|
|
|
682
745
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
683
746
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
await cmdGo([taskDesc]);
|
|
689
|
-
} catch (e) {
|
|
690
|
-
console.error(`Error: ${e.message}`);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
return { next: 'dashboard' };
|
|
747
|
+
// Numeric choice → session detail
|
|
748
|
+
const numChoice = parseInt(choice, 10);
|
|
749
|
+
if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
|
|
750
|
+
return { next: 'session-detail', session: recentSessions[numChoice - 1] };
|
|
694
751
|
}
|
|
695
752
|
|
|
696
753
|
if (choice === 's') {
|
|
@@ -702,7 +759,6 @@ async function dashboardScreen(rl, ask) {
|
|
|
702
759
|
if (choice === 'p') { return { next: 'profile' }; }
|
|
703
760
|
if (choice === 'a') { return { next: 'auth' }; }
|
|
704
761
|
if (choice === 'd') { return { next: 'diagnostics' }; }
|
|
705
|
-
if (choice === 'c') { return { next: 'repl' }; }
|
|
706
762
|
if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
|
|
707
763
|
|
|
708
764
|
// Unknown choice — stay on dashboard
|
|
@@ -827,77 +883,223 @@ async function profileScreen(rl, ask) {
|
|
|
827
883
|
|
|
828
884
|
async function diagnosticsScreen(rl, ask) {
|
|
829
885
|
const cwd = process.cwd();
|
|
886
|
+
const { spawnSync: _spawnSync } = await import('child_process');
|
|
887
|
+
const { readdirSync } = await import('node:fs');
|
|
888
|
+
const { detectPlans } = await import('../src/profile.mjs');
|
|
889
|
+
|
|
890
|
+
// ── Version info ──────────────────────────────────────────────────────────
|
|
830
891
|
const version = readVersion();
|
|
831
|
-
const
|
|
832
|
-
|
|
892
|
+
const nodeVersion = process.version;
|
|
893
|
+
|
|
894
|
+
// ── Provider health ───────────────────────────────────────────────────────
|
|
895
|
+
const auth = await detectAuth();
|
|
896
|
+
const plans = detectPlans();
|
|
897
|
+
const { states: healthStates } = getHealth(cwd);
|
|
898
|
+
|
|
899
|
+
function _providerBadge(name) {
|
|
900
|
+
const entries = Object.entries(healthStates).filter(([k]) => k.startsWith(`${name}:`));
|
|
901
|
+
if (entries.length === 0) return '✅ healthy';
|
|
902
|
+
const statuses = entries.map(([, v]) => v.status);
|
|
903
|
+
if (statuses.includes('hot')) return '🔴 hot';
|
|
904
|
+
if (statuses.includes('degraded')) return '⚠️ degraded';
|
|
905
|
+
if (statuses.includes('probing')) return '⚠️ probing';
|
|
906
|
+
return '✅ healthy';
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const claudeStatus = auth.claude.found ? _providerBadge('claude') : '❌ no auth';
|
|
910
|
+
const openaiStatus = auth.openai.found ? _providerBadge('openai') : '❌ no auth';
|
|
911
|
+
const claudePlanStr = plans.claude ? `Max ${plans.claude}` : (auth.claude.masked ?? 'unknown');
|
|
912
|
+
const openaiPlanStr = plans.openai ? `Pro ${plans.openai}` : (auth.openai.masked ?? 'unknown');
|
|
913
|
+
const claudeAuthStr = auth.claude.masked ?? 'not configured';
|
|
914
|
+
const openaiAuthStr = auth.openai.masked ?? 'not configured';
|
|
915
|
+
|
|
916
|
+
// ── Enforcement checks ────────────────────────────────────────────────────
|
|
917
|
+
const hooksDir = join(cwd, '.claude', 'hooks');
|
|
918
|
+
const headGuardExists = existsSync(join(hooksDir, 'head-guard.mjs'));
|
|
919
|
+
const enforceTierExists = existsSync(join(hooksDir, 'enforce-tier.mjs'));
|
|
833
920
|
|
|
834
|
-
// Enforcement check
|
|
835
921
|
let guardCount = 0;
|
|
836
|
-
let guardDetails = 'NOT INSTALLED';
|
|
837
922
|
try {
|
|
838
923
|
const settingsFile = join(cwd, '.claude', 'settings.json');
|
|
839
924
|
if (existsSync(settingsFile)) {
|
|
840
925
|
const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
|
|
841
926
|
const preToolUse = settings?.hooks?.PreToolUse ?? [];
|
|
842
|
-
const guardCmd
|
|
843
|
-
const tierCmd
|
|
844
|
-
const hasEdit
|
|
845
|
-
const hasWrite
|
|
846
|
-
const hasBash
|
|
847
|
-
const hasAgent
|
|
927
|
+
const guardCmd = 'node .claude/hooks/head-guard.mjs';
|
|
928
|
+
const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
|
|
929
|
+
const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
|
|
930
|
+
const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
|
|
931
|
+
const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
|
|
932
|
+
const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
|
|
848
933
|
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
934
|
}
|
|
853
|
-
} catch {
|
|
935
|
+
} catch { /* ignore */ }
|
|
854
936
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
937
|
+
let hookifyCount = 0;
|
|
938
|
+
try {
|
|
939
|
+
const claudeDir = join(cwd, '.claude');
|
|
940
|
+
if (existsSync(claudeDir)) {
|
|
941
|
+
hookifyCount = readdirSync(claudeDir).filter(f => f.startsWith('hookify.') && f.endsWith('.md')).length;
|
|
942
|
+
}
|
|
943
|
+
} catch { /* ignore */ }
|
|
944
|
+
|
|
945
|
+
// ── Replit-tools integration ──────────────────────────────────────────────
|
|
946
|
+
const replitToolsDir = join(cwd, '.replit-tools');
|
|
947
|
+
const hasReplitTools = existsSync(replitToolsDir);
|
|
948
|
+
const persistentDir = join(replitToolsDir, '.claude-persistent');
|
|
949
|
+
const sessionManagerExists = existsSync(join(replitToolsDir, 'scripts', 'claude-session-manager.sh'));
|
|
950
|
+
const authRefreshScript = join(replitToolsDir, 'scripts', 'claude-auth-refresh.sh');
|
|
951
|
+
|
|
952
|
+
let credsFresh = null;
|
|
953
|
+
let credsExpiry = null;
|
|
954
|
+
let historyCount = 0;
|
|
955
|
+
|
|
956
|
+
if (hasReplitTools) {
|
|
957
|
+
try {
|
|
958
|
+
const credsFile = join(persistentDir, '.credentials.json');
|
|
959
|
+
const creds = JSON.parse(readFileSync(credsFile, 'utf8'));
|
|
960
|
+
const expiresAt = creds?.claudeAiOauth?.expiresAt;
|
|
961
|
+
if (expiresAt) {
|
|
962
|
+
const expiresMs = typeof expiresAt === 'number' ? expiresAt : Date.parse(expiresAt);
|
|
963
|
+
credsFresh = Date.now() < expiresMs;
|
|
964
|
+
credsExpiry = new Date(expiresMs).toISOString().slice(0, 10);
|
|
965
|
+
}
|
|
966
|
+
} catch { /* credentials missing or unreadable */ }
|
|
967
|
+
|
|
968
|
+
try {
|
|
969
|
+
const histFile = join(persistentDir, 'history.jsonl');
|
|
970
|
+
if (existsSync(histFile)) {
|
|
971
|
+
historyCount = readFileSync(histFile, 'utf8').split('\n').filter(Boolean).length;
|
|
972
|
+
}
|
|
973
|
+
} catch { /* ignore */ }
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// ── Quality checks ────────────────────────────────────────────────────────
|
|
977
|
+
let testPass = null; let testTotal = null; let testError = null;
|
|
978
|
+
try {
|
|
979
|
+
const r = _spawnSync('node', ['--test', 'src/test.mjs'], { cwd, encoding: 'utf8', timeout: 30000 });
|
|
980
|
+
const out = (r.stdout ?? '') + (r.stderr ?? '');
|
|
981
|
+
const pm = out.match(/# pass (\d+)/);
|
|
982
|
+
const tm = out.match(/# tests (\d+)/);
|
|
983
|
+
if (pm && tm) { testPass = parseInt(pm[1], 10); testTotal = parseInt(tm[1], 10); }
|
|
984
|
+
else { testError = 'could not parse output'; }
|
|
985
|
+
} catch (e) { testError = e.message; }
|
|
986
|
+
|
|
987
|
+
let healthPass = null; let healthTotal = null; let healthError = null;
|
|
988
|
+
try {
|
|
989
|
+
const healthScript = join(hooksDir, 'health-check.mjs');
|
|
990
|
+
if (existsSync(healthScript)) {
|
|
991
|
+
const r = _spawnSync('node', [healthScript], { cwd, encoding: 'utf8', timeout: 15000 });
|
|
992
|
+
const out = (r.stdout ?? '') + (r.stderr ?? '');
|
|
993
|
+
// Try summary line first: "8 pass, 0 warn, 0 fail"
|
|
994
|
+
const sm = out.match(/(\d+) pass,\s*(\d+) warn,\s*(\d+) fail/);
|
|
995
|
+
if (sm) {
|
|
996
|
+
healthPass = parseInt(sm[1], 10);
|
|
997
|
+
healthTotal = parseInt(sm[1], 10) + parseInt(sm[2], 10) + parseInt(sm[3], 10);
|
|
998
|
+
} else {
|
|
999
|
+
// Fall back to JSON block
|
|
1000
|
+
const jm = out.match(/\{[\s\S]*?"healthy"[\s\S]*?\}/);
|
|
1001
|
+
if (jm) {
|
|
1002
|
+
try {
|
|
1003
|
+
const p = JSON.parse(jm[0]);
|
|
1004
|
+
healthPass = p.pass ?? 0;
|
|
1005
|
+
healthTotal = (p.pass ?? 0) + (p.warn ?? 0) + (p.fail ?? 0);
|
|
1006
|
+
} catch { healthError = 'could not parse output'; }
|
|
1007
|
+
} else { healthError = 'could not parse output'; }
|
|
1008
|
+
}
|
|
1009
|
+
} else { healthError = 'health-check.mjs not found'; }
|
|
1010
|
+
} catch (e) { healthError = e.message; }
|
|
1011
|
+
|
|
1012
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
1013
|
+
const W = 56;
|
|
1014
|
+
const hbar = '═'.repeat(W);
|
|
1015
|
+
// Pad a string to exactly W visible columns (for box rows without leading ║ prefix)
|
|
1016
|
+
const padRow = (s) => {
|
|
1017
|
+
const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
|
|
1018
|
+
let vlen = 0;
|
|
1019
|
+
for (const ch of plain) {
|
|
1020
|
+
const cp = ch.codePointAt(0);
|
|
1021
|
+
if ((cp >= 0x1f300 && cp <= 0x1faff) || (cp >= 0x2600 && cp <= 0x27bf) || cp === 0xfe0f || cp === 0x20e3) vlen += 2;
|
|
1022
|
+
else vlen += 1;
|
|
1023
|
+
}
|
|
1024
|
+
return s + ' '.repeat(Math.max(0, W - vlen));
|
|
1025
|
+
};
|
|
1026
|
+
const hrow = (s) => `║${padRow(' ' + s)}║`;
|
|
1027
|
+
|
|
1028
|
+
const output = [
|
|
1029
|
+
`╔${hbar}╗`,
|
|
1030
|
+
hrow('🔧 Diagnostics'),
|
|
1031
|
+
`╠${hbar}╣`,
|
|
1032
|
+
hrow(`dual-brain v${version}`),
|
|
1033
|
+
hrow(`Node.js ${nodeVersion}`),
|
|
1034
|
+
`╚${hbar}╝`,
|
|
869
1035
|
'',
|
|
870
|
-
'
|
|
871
|
-
`
|
|
872
|
-
`
|
|
873
|
-
` CI: ${env.isCI ? 'yes' : 'no'}`,
|
|
1036
|
+
separator('Provider Health'),
|
|
1037
|
+
` ${claudeStatus.padEnd(14)} Claude ${claudePlanStr.padEnd(16)} ${claudeAuthStr}`,
|
|
1038
|
+
` ${openaiStatus.padEnd(14)} OpenAI ${openaiPlanStr.padEnd(16)} ${openaiAuthStr}`,
|
|
874
1039
|
'',
|
|
875
|
-
'
|
|
876
|
-
`
|
|
877
|
-
`
|
|
878
|
-
`
|
|
1040
|
+
separator('Enforcement'),
|
|
1041
|
+
` ${headGuardExists ? '✅' : '❌'} head-guard.mjs ${headGuardExists ? 'installed' : 'missing — run: dual-brain install'}`,
|
|
1042
|
+
` ${enforceTierExists ? '✅' : '❌'} enforce-tier.mjs ${enforceTierExists ? 'installed' : 'missing — run: dual-brain install'}`,
|
|
1043
|
+
` ${guardCount === 4 ? '✅' : '⚠️ '} settings.json ${guardCount}/4 guards registered${guardCount < 4 ? ' — run: dual-brain install' : ''}`,
|
|
1044
|
+
` ${hookifyCount > 0 ? '✅' : '⚠️ '} hookify rules ${hookifyCount} rules${hookifyCount > 0 ? ' (check: ls .claude/hookify.*.md)' : ' — none found'}`,
|
|
879
1045
|
'',
|
|
880
|
-
'
|
|
881
|
-
|
|
1046
|
+
separator('Replit Tools'),
|
|
1047
|
+
` ${hasReplitTools ? '✅' : '❌'} replit-tools ${hasReplitTools ? 'detected' : 'not detected'}`,
|
|
882
1048
|
];
|
|
883
1049
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
])
|
|
1050
|
+
if (hasReplitTools) {
|
|
1051
|
+
if (credsFresh === null) {
|
|
1052
|
+
output.push(' ⚠️ Claude auth credentials file missing');
|
|
1053
|
+
} else if (credsFresh) {
|
|
1054
|
+
output.push(` ✅ Claude auth fresh (expires: ${credsExpiry})`);
|
|
1055
|
+
} else {
|
|
1056
|
+
output.push(` ❌ Claude auth expired (${credsExpiry}) — run [r] Refresh auth`);
|
|
1057
|
+
}
|
|
1058
|
+
output.push(` ✅ Session archive ${historyCount} entries`);
|
|
1059
|
+
output.push(` ${sessionManagerExists ? '✅' : '⚠️ '} Session manager ${sessionManagerExists ? 'available' : 'not found'}`);
|
|
1060
|
+
} else {
|
|
1061
|
+
output.push(' ─── (not available)');
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
output.push('');
|
|
1065
|
+
output.push(separator('Quality'));
|
|
1066
|
+
if (testError) {
|
|
1067
|
+
output.push(` ❌ Tests error: ${testError}`);
|
|
1068
|
+
} else if (testPass !== null) {
|
|
1069
|
+
output.push(` ${testPass === testTotal ? '✅' : '❌'} Tests ${testPass}/${testTotal} passing`);
|
|
1070
|
+
}
|
|
1071
|
+
if (healthError) {
|
|
1072
|
+
output.push(` ❌ Health check error: ${healthError}`);
|
|
1073
|
+
} else if (healthPass !== null) {
|
|
1074
|
+
output.push(` ${healthPass === healthTotal ? '✅' : '⚠️ '} Health check ${healthPass}/${healthTotal} passing`);
|
|
1075
|
+
}
|
|
1076
|
+
output.push('');
|
|
1077
|
+
|
|
1078
|
+
console.log(output.join('\n'));
|
|
1079
|
+
|
|
1080
|
+
// Actions menu
|
|
1081
|
+
const menuOpts = [
|
|
1082
|
+
{ key: 'h', label: 'Run health check', section: 'Actions' },
|
|
1083
|
+
{ key: 't', label: 'Run test suite', section: 'Actions' },
|
|
1084
|
+
];
|
|
1085
|
+
if (hasReplitTools && existsSync(authRefreshScript)) {
|
|
1086
|
+
menuOpts.push({ key: 'r', label: 'Refresh auth (replit-tools)', section: 'Actions' });
|
|
1087
|
+
}
|
|
1088
|
+
menuOpts.push({ key: 'i', label: 'Reinstall hooks', section: 'Actions' });
|
|
1089
|
+
menuOpts.push({ key: 'b', label: 'Back to dashboard', section: 'Actions' });
|
|
1090
|
+
console.log(menu(menuOpts));
|
|
891
1091
|
console.log('');
|
|
892
1092
|
|
|
893
1093
|
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
894
1094
|
|
|
895
1095
|
if (choice === 'h') {
|
|
896
|
-
const hookScript = join(
|
|
1096
|
+
const hookScript = join(hooksDir, 'health-check.mjs');
|
|
1097
|
+
console.log('');
|
|
897
1098
|
if (existsSync(hookScript)) {
|
|
898
1099
|
try {
|
|
899
|
-
|
|
900
|
-
|
|
1100
|
+
const r = _spawnSync('node', [hookScript], { stdio: 'inherit', cwd });
|
|
1101
|
+
if (r.error) console.log(` Error: ${r.error.message}`);
|
|
1102
|
+
} catch (e) { console.log(` Error: ${e.message}`); }
|
|
901
1103
|
} else {
|
|
902
1104
|
console.log(' health-check.mjs not found — run: dual-brain install');
|
|
903
1105
|
}
|
|
@@ -905,6 +1107,32 @@ async function diagnosticsScreen(rl, ask) {
|
|
|
905
1107
|
return { next: 'diagnostics' };
|
|
906
1108
|
}
|
|
907
1109
|
|
|
1110
|
+
if (choice === 't') {
|
|
1111
|
+
console.log('\n Running test suite...\n');
|
|
1112
|
+
try {
|
|
1113
|
+
const r = _spawnSync('node', ['--test', 'src/test.mjs'], { stdio: 'inherit', cwd, timeout: 60000 });
|
|
1114
|
+
if (r.error) console.log(` Error: ${r.error.message}`);
|
|
1115
|
+
} catch (e) { console.log(` Error: ${e.message}`); }
|
|
1116
|
+
await ask('\n Press Enter to continue...');
|
|
1117
|
+
return { next: 'diagnostics' };
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (choice === 'r') {
|
|
1121
|
+
if (existsSync(authRefreshScript)) {
|
|
1122
|
+
console.log('\n Refreshing Claude auth...\n');
|
|
1123
|
+
try {
|
|
1124
|
+
const r = _spawnSync('bash', [authRefreshScript], { stdio: 'inherit', cwd, timeout: 30000 });
|
|
1125
|
+
if (r.error) console.log(` Error: ${r.error.message}`);
|
|
1126
|
+
else if (r.status === 0) console.log('\n Auth refresh complete.');
|
|
1127
|
+
else console.log(`\n Auth refresh exited with code ${r.status}.`);
|
|
1128
|
+
} catch (e) { console.log(` Error: ${e.message}`); }
|
|
1129
|
+
} else {
|
|
1130
|
+
console.log(' claude-auth-refresh.sh not found.');
|
|
1131
|
+
}
|
|
1132
|
+
await ask('\n Press Enter to continue...');
|
|
1133
|
+
return { next: 'diagnostics' };
|
|
1134
|
+
}
|
|
1135
|
+
|
|
908
1136
|
if (choice === 'i') {
|
|
909
1137
|
await cmdInstall();
|
|
910
1138
|
return { next: 'diagnostics' };
|
|
@@ -963,15 +1191,75 @@ async function replScreen(rl, ask) {
|
|
|
963
1191
|
}
|
|
964
1192
|
}
|
|
965
1193
|
|
|
1194
|
+
// ─── Screen: sessionDetailScreen ─────────────────────────────────────────────
|
|
1195
|
+
|
|
1196
|
+
async function sessionDetailScreen(rl, ask, ctx = {}) {
|
|
1197
|
+
const sess = ctx.session;
|
|
1198
|
+
if (!sess) return { next: 'dashboard' };
|
|
1199
|
+
|
|
1200
|
+
const W = 56;
|
|
1201
|
+
const hbar = '═'.repeat(W + 2);
|
|
1202
|
+
const pad = (s) => {
|
|
1203
|
+
const plain = s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
|
|
1204
|
+
return s + ' '.repeat(Math.max(0, W - plain.length));
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
const statusLine = sess.isActive
|
|
1208
|
+
? `active`
|
|
1209
|
+
: `inactive`;
|
|
1210
|
+
|
|
1211
|
+
const detailLines = [
|
|
1212
|
+
` Session: ${sess.name}`,
|
|
1213
|
+
`╠${hbar}╣`,
|
|
1214
|
+
` ID: ${sess.id.slice(0, 8)}...`,
|
|
1215
|
+
` Status: ${statusLine}`,
|
|
1216
|
+
` Prompts: ${sess.promptCount}`,
|
|
1217
|
+
` Last active: ${sess.age}`,
|
|
1218
|
+
` Project: ${sess.project || process.cwd()}`,
|
|
1219
|
+
];
|
|
1220
|
+
|
|
1221
|
+
console.log(`╔${hbar}╗`);
|
|
1222
|
+
for (const line of detailLines) {
|
|
1223
|
+
console.log(`║ ${pad(line)}║`);
|
|
1224
|
+
}
|
|
1225
|
+
console.log(`╚${hbar}╝`);
|
|
1226
|
+
console.log('');
|
|
1227
|
+
|
|
1228
|
+
if (sess.isActive) {
|
|
1229
|
+
console.log(' [c] Continue this session (claude --continue)');
|
|
1230
|
+
} else {
|
|
1231
|
+
console.log(' [r] Resume this session (claude --resume)');
|
|
1232
|
+
}
|
|
1233
|
+
console.log(' [b] Back to dashboard');
|
|
1234
|
+
console.log('');
|
|
1235
|
+
|
|
1236
|
+
const choice = (await ask(' Choice: ')).trim().toLowerCase();
|
|
1237
|
+
|
|
1238
|
+
if (choice === 'c' || choice === 'r') {
|
|
1239
|
+
console.log(`\n Launching: claude --resume ${sess.id}\n`);
|
|
1240
|
+
try {
|
|
1241
|
+
const { spawnSync } = await import('node:child_process');
|
|
1242
|
+
spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
|
|
1243
|
+
} catch {
|
|
1244
|
+
console.log(' Could not launch claude CLI. Run manually:');
|
|
1245
|
+
console.log(` claude --resume ${sess.id}`);
|
|
1246
|
+
}
|
|
1247
|
+
return { next: 'dashboard' };
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return { next: 'dashboard' };
|
|
1251
|
+
}
|
|
1252
|
+
|
|
966
1253
|
// ─── Screen state machine ─────────────────────────────────────────────────────
|
|
967
1254
|
|
|
968
1255
|
const SCREENS = {
|
|
969
|
-
welcome:
|
|
970
|
-
dashboard:
|
|
971
|
-
auth:
|
|
972
|
-
profile:
|
|
973
|
-
diagnostics:
|
|
974
|
-
repl:
|
|
1256
|
+
welcome: welcomeScreen,
|
|
1257
|
+
dashboard: dashboardScreen,
|
|
1258
|
+
auth: authScreen,
|
|
1259
|
+
profile: profileScreen,
|
|
1260
|
+
diagnostics: diagnosticsScreen,
|
|
1261
|
+
repl: replScreen,
|
|
1262
|
+
'session-detail': sessionDetailScreen,
|
|
975
1263
|
};
|
|
976
1264
|
|
|
977
1265
|
async function runScreens(startScreen = 'dashboard') {
|
|
@@ -979,15 +1267,19 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
979
1267
|
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
980
1268
|
|
|
981
1269
|
let current = startScreen;
|
|
1270
|
+
let ctx = {};
|
|
982
1271
|
while (current && current !== 'exit') {
|
|
983
1272
|
const screen = SCREENS[current];
|
|
984
1273
|
if (!screen) break;
|
|
985
1274
|
try {
|
|
986
|
-
const result = await screen(rl, ask);
|
|
1275
|
+
const result = await screen(rl, ask, ctx);
|
|
987
1276
|
current = result?.next || 'exit';
|
|
1277
|
+
// Pass through context (e.g. selected session) to next screen
|
|
1278
|
+
ctx = result?.session ? { session: result.session } : {};
|
|
988
1279
|
} catch (e) {
|
|
989
1280
|
console.error(`Error: ${e.message}`);
|
|
990
1281
|
current = 'dashboard'; // recover to dashboard on error
|
|
1282
|
+
ctx = {};
|
|
991
1283
|
}
|
|
992
1284
|
}
|
|
993
1285
|
rl.close();
|
|
@@ -1007,9 +1299,15 @@ async function main() {
|
|
|
1007
1299
|
|
|
1008
1300
|
if (!cmd) {
|
|
1009
1301
|
if (isInteractive) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1302
|
+
const cwd = process.cwd();
|
|
1303
|
+
if (profileExists(cwd)) {
|
|
1304
|
+
// Profile already exists → go straight to dashboard
|
|
1305
|
+
await runScreens('dashboard');
|
|
1306
|
+
} else {
|
|
1307
|
+
// First run: welcomeScreen handles auto-setup detection internally,
|
|
1308
|
+
// then falls through to manual wizard if needed.
|
|
1309
|
+
await runScreens('welcome');
|
|
1310
|
+
}
|
|
1013
1311
|
} else {
|
|
1014
1312
|
// Non-TTY: print status card and exit
|
|
1015
1313
|
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,147 @@ 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
|
+
|
|
233
|
+
// Check multiple possible locations for replit-tools
|
|
234
|
+
const candidates = [
|
|
235
|
+
join(cwd, '.replit-tools', '.claude-persistent'),
|
|
236
|
+
join('/home/runner/workspace', '.replit-tools', '.claude-persistent'),
|
|
237
|
+
];
|
|
238
|
+
// Deduplicate
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
const replitBases = candidates.filter(p => {
|
|
241
|
+
const norm = p.replace(/\/+$/, '');
|
|
242
|
+
if (seen.has(norm)) return false;
|
|
243
|
+
seen.add(norm);
|
|
244
|
+
return true;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
let replitBase = null;
|
|
248
|
+
for (const candidate of replitBases) {
|
|
249
|
+
if (existsSync(join(candidate, 'history.jsonl'))) {
|
|
250
|
+
replitBase = candidate;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!replitBase) return sessions;
|
|
255
|
+
|
|
256
|
+
// Read history.jsonl
|
|
257
|
+
const historyPath = join(replitBase, 'history.jsonl');
|
|
258
|
+
|
|
259
|
+
let lines;
|
|
260
|
+
try {
|
|
261
|
+
lines = readFileSync(historyPath, 'utf8').split('\n').filter(Boolean);
|
|
262
|
+
} catch { return sessions; }
|
|
263
|
+
|
|
264
|
+
const bySession = new Map(); // sessionId → { entries, firstPrompt, lastTimestamp }
|
|
265
|
+
|
|
266
|
+
for (const line of lines) {
|
|
267
|
+
try {
|
|
268
|
+
const entry = JSON.parse(line);
|
|
269
|
+
if (!entry.sessionId) continue;
|
|
270
|
+
|
|
271
|
+
if (!bySession.has(entry.sessionId)) {
|
|
272
|
+
bySession.set(entry.sessionId, {
|
|
273
|
+
sessionId: entry.sessionId,
|
|
274
|
+
project: entry.project,
|
|
275
|
+
entries: [],
|
|
276
|
+
firstPrompt: null,
|
|
277
|
+
lastTimestamp: 0,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const sess = bySession.get(entry.sessionId);
|
|
282
|
+
sess.entries.push(entry);
|
|
283
|
+
if (entry.timestamp > sess.lastTimestamp) sess.lastTimestamp = entry.timestamp;
|
|
284
|
+
|
|
285
|
+
// Find first meaningful user prompt (not slash commands, not login, not pastes)
|
|
286
|
+
if (!sess.firstPrompt && entry.display
|
|
287
|
+
&& !entry.display.startsWith('/')
|
|
288
|
+
&& !entry.display.startsWith('login')
|
|
289
|
+
&& !entry.display.startsWith('[Pasted')) {
|
|
290
|
+
sess.firstPrompt = entry.display;
|
|
291
|
+
}
|
|
292
|
+
} catch { continue; }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Read active terminal sessions
|
|
296
|
+
// Use the same root as replitBase (go up one level from .claude-persistent)
|
|
297
|
+
const replitRoot = join(replitBase, '..');
|
|
298
|
+
const sessionsDir = join(replitRoot, '..', '.claude-sessions');
|
|
299
|
+
const activeSessionIds = new Set();
|
|
300
|
+
if (existsSync(sessionsDir)) {
|
|
301
|
+
try {
|
|
302
|
+
for (const f of readdirSync(sessionsDir)) {
|
|
303
|
+
try {
|
|
304
|
+
const data = JSON.parse(readFileSync(join(sessionsDir, f), 'utf8'));
|
|
305
|
+
if (data.sessionId) activeSessionIds.add(data.sessionId);
|
|
306
|
+
} catch { continue; }
|
|
307
|
+
}
|
|
308
|
+
} catch { /* non-fatal */ }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Build session list
|
|
312
|
+
for (const [id, sess] of bySession) {
|
|
313
|
+
// Derive display name
|
|
314
|
+
let name = sess.firstPrompt;
|
|
315
|
+
if (!name) {
|
|
316
|
+
// Fallback: use first non-login display
|
|
317
|
+
const firstReal = sess.entries.find(e => e.display && e.display !== 'login');
|
|
318
|
+
name = firstReal?.display || `Session ${id.slice(0, 8)}`;
|
|
319
|
+
}
|
|
320
|
+
// Truncate long names
|
|
321
|
+
if (name.length > 60) name = name.slice(0, 57) + '...';
|
|
322
|
+
|
|
323
|
+
sessions.push({
|
|
324
|
+
id: sess.sessionId,
|
|
325
|
+
name,
|
|
326
|
+
project: sess.project,
|
|
327
|
+
promptCount: sess.entries.length,
|
|
328
|
+
lastActive: new Date(sess.lastTimestamp).toISOString(),
|
|
329
|
+
isActive: activeSessionIds.has(id),
|
|
330
|
+
source: 'replit-tools',
|
|
331
|
+
age: timeAgo(sess.lastTimestamp),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Sort by most recent first
|
|
336
|
+
sessions.sort((a, b) => new Date(b.lastActive) - new Date(a.lastActive));
|
|
337
|
+
|
|
338
|
+
return sessions;
|
|
339
|
+
}
|
|
340
|
+
|
|
200
341
|
// ─── CLI (direct invocation) ──────────────────────────────────────────────────
|
|
201
342
|
|
|
202
343
|
const isMain = process.argv[1]?.endsWith('session.mjs');
|