dual-brain 7.1.10 → 7.1.12

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.
@@ -126,7 +126,7 @@ function printSubscriptionTable(auth, profile) {
126
126
 
127
127
  const claudeLine1 = auth.claude.found
128
128
  ? ` Claude: logged in (${auth.claude.source})`
129
- : ` Claude: not logged in — run: claude login`;
129
+ : ` Claude: not logged in — run: claude auth login`;
130
130
  const claudeLine2 = ` plan: ${claudePlanLabel}${claudeLabel}`;
131
131
 
132
132
  const openaiLine1 = auth.openai.found
@@ -156,7 +156,7 @@ async function cmdInit(rl) {
156
156
  const noneFound = !auth.claude.found && !auth.openai.found;
157
157
  if (noneFound) {
158
158
  console.log('\nNo AI provider found. Log in first:');
159
- console.log(' Claude: claude login');
159
+ console.log(' Claude: claude auth login');
160
160
  console.log(' OpenAI: codex login\n');
161
161
  console.log('Then re-run: dual-brain init');
162
162
  return;
@@ -189,7 +189,7 @@ async function cmdAuth(subArgs = []) {
189
189
 
190
190
  if (!auth.claude.found || !auth.openai.found) {
191
191
  console.log('');
192
- if (!auth.claude.found) console.log(' Claude not logged in. Run: claude login');
192
+ if (!auth.claude.found) console.log(' Claude not logged in. Run: claude auth login');
193
193
  if (!auth.openai.found) console.log(' OpenAI not logged in. Run: codex login');
194
194
  }
195
195
  }
@@ -525,6 +525,35 @@ function cmdForget(text) {
525
525
 
526
526
  // ─── Screen helpers ───────────────────────────────────────────────────────────
527
527
 
528
+ /**
529
+ * Render the data-tools-style rounded header box for the main screen.
530
+ * Inner width is 39 chars. Lines are padded with spaces to fill the box.
531
+ */
532
+ function renderHeader(version, providerLines) {
533
+ const W = 39; // inner width
534
+ const pad = (s) => {
535
+ // Strip ANSI codes for length calculation
536
+ const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
537
+ return s + ' '.repeat(Math.max(0, W - visible.length));
538
+ };
539
+ const top = ` ┌${'─'.repeat(W)}┐`;
540
+ const sep = ` ├${'─'.repeat(W)}┤`;
541
+ const bottom = ` └${'─'.repeat(W)}┘`;
542
+
543
+ const title = `DATA Tools - Dual Brain v${version}`;
544
+ const credit = `by Steve Moraco + dual-brain`;
545
+
546
+ const lines = [top];
547
+ lines.push(` │ ${pad(title)}│`);
548
+ lines.push(` │ ${pad(credit)}│`);
549
+ lines.push(sep);
550
+ for (const pl of providerLines) {
551
+ lines.push(` │ ${pad(pl)}│`);
552
+ }
553
+ lines.push(bottom);
554
+ return lines.join('\n');
555
+ }
556
+
528
557
  function profileExists(cwd) {
529
558
  const dir = cwd || process.cwd();
530
559
  const globalPath = join(process.env.HOME || '/root', '.config', 'dual-brain', 'profile.json');
@@ -609,8 +638,8 @@ async function welcomeScreen(rl, ask) {
609
638
 
610
639
  if (!claudeReady && !openaiReady) {
611
640
  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');
641
+ console.log(' claude auth login — for Claude');
642
+ console.log(' codex login — for OpenAI/Codex\n');
614
643
  console.log('Then re-run: dual-brain init');
615
644
  return { next: 'exit' };
616
645
  }
@@ -777,7 +806,13 @@ async function mainScreen(rl, ask) {
777
806
 
778
807
  function subLine(name, plan, found, expired, days, sub) {
779
808
  const label = sub?.label ? ` [${sub.label}]` : '';
780
- if (!found) return `⚠️ ${name}: not logged in — run: ${name === 'Claude' ? 'claude login' : 'codex login'}`;
809
+ if (!found) return `⚠️ ${name}: not logged in — run: ${name === 'Claude' ? 'claude auth login' : 'codex login'}`;
810
+ // Multi-sub: show aggregated counts when more than one sub exists
811
+ const subs = sub?.subs;
812
+ if (subs && subs.length > 1) {
813
+ const aggregate = aggregatePlans(subs);
814
+ return `✅ ${name}: ${aggregate} [${subs.length} subs]`;
815
+ }
781
816
  if (expired) return `🔴 ${name}: ${plan} expired${label} — will re-auth`;
782
817
  const daysNote = (days !== null && days <= 7) ? ` (${days}d left)` : '';
783
818
  return `✅ ${name}: ${plan}${label}${daysNote}`;
@@ -788,8 +823,32 @@ async function mainScreen(rl, ask) {
788
823
  subLine('OpenAI', openaiPlan, auth.openai.found, openaiExpired, openaiDays, openaiSub),
789
824
  ];
790
825
 
826
+ console.log(`📦 DATA Tools - Dual Brain v${version}`);
791
827
  console.log('');
792
- console.log(box(`🧠 dual-brain v${version}`, headerLines));
828
+
829
+ // Help shortcuts box (matching data-tools style)
830
+ const W = 37;
831
+ const helpTop = ` ┌${'─'.repeat(W)}┐`;
832
+ const helpSep = ` ├${'─'.repeat(W)}┤`;
833
+ const helpBottom = ` └${'─'.repeat(W)}┘`;
834
+ const helpPad = (s) => s + ' '.repeat(Math.max(0, W - s.length));
835
+
836
+ console.log(helpTop);
837
+ console.log(` │ ${helpPad('At ~/workspace$ prompt:')}│`);
838
+ console.log(` │ ${helpPad('db = show this menu')}│`);
839
+ console.log(` │ ${helpPad('j = login to claude')}│`);
840
+ console.log(` │ ${helpPad('k = login to codex')}│`);
841
+ console.log(helpSep);
842
+ console.log(` │ ${helpPad('In Claude:')}│`);
843
+ console.log(` │ ${helpPad('Ctrl+C x2 = back to menu')}│`);
844
+ console.log(` │ ${helpPad('Ctrl+C x3 = exit to shell')}│`);
845
+ console.log(helpBottom);
846
+ console.log('');
847
+
848
+ // Provider status (outside the box)
849
+ for (const line of headerLines) {
850
+ console.log(` ${line}`);
851
+ }
793
852
 
794
853
  // Auto-refresh expired subscriptions
795
854
  if (claudeExpired || openaiExpired) {
@@ -799,7 +858,7 @@ async function mainScreen(rl, ask) {
799
858
  if (openaiExpired) expired.push('OpenAI');
800
859
  console.log(`\n ${expired.join(' & ')} subscription expired. Re-authenticating...`);
801
860
  if (claudeExpired) {
802
- const r = spawnSync('claude', ['login'], { stdio: 'inherit', timeout: 30000 });
861
+ const r = spawnSync('claude', ['auth', 'login'], { stdio: 'inherit', timeout: 30000 });
803
862
  if (r.status === 0) {
804
863
  claudeSub.expiresAt = null;
805
864
  saveProfile(profile, { cwd });
@@ -820,28 +879,42 @@ async function mainScreen(rl, ask) {
820
879
  const recentSessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 7);
821
880
 
822
881
  if (recentSessions.length > 0) {
823
- console.log(separator('Recent Sessions'));
882
+ console.log(' Recent Sessions:');
824
883
  recentSessions.forEach((sess, i) => {
825
884
  const pin = sess.pinned ? '📌 ' : ' ';
826
885
  const active = sess.isActive ? ' ●' : '';
827
886
  const cat = sess.category ? ` [${sess.category}]` : '';
828
- console.log(` [${i + 1}] ${pin}${sess.age.padEnd(6)} ${sess.name}${active}${cat}`);
887
+ const tool = (sess.tool === 'codex') ? 'cdx' : 'cld';
888
+ console.log(` [${i + 1}] ${pin}${tool} ${sess.age.padEnd(8)} ${sess.name}${active}${cat}`);
829
889
  });
830
890
  console.log('');
831
891
  }
832
892
 
833
- const menuOpts = [];
834
- menuOpts.push({ key: 'c', label: 'Continue last session', section: 'Sessions' });
835
- menuOpts.push({ key: 'n', label: 'New session', section: 'Sessions' });
893
+ const brandW = 37;
894
+ const brandTop = ` ┌${''.repeat(brandW)}┐`;
895
+ const brandBottom = ` └${''.repeat(brandW)}┘`;
896
+ const brandPad = (s) => {
897
+ const leftPad = Math.floor((brandW - s.length) / 2);
898
+ const rightPad = brandW - s.length - leftPad;
899
+ return ' '.repeat(leftPad) + s + ' '.repeat(rightPad);
900
+ };
901
+ console.log(brandTop);
902
+ console.log(` │ ${brandPad('Dual Brain Session Manager')}│`);
903
+ console.log(` │ ${brandPad('by Steve Moraco + dual-brain')}│`);
904
+ console.log(brandBottom);
905
+ console.log('');
906
+
907
+ console.log(' [c] Continue last session');
908
+ console.log(' [n] New session');
836
909
  if (recentSessions.length > 0) {
837
- menuOpts.push({ key: '1-9', label: 'Resume numbered above', section: 'Sessions' });
838
- }
839
- menuOpts.push({ key: 'e', label: 'Manage sessions', section: 'Sessions' });
840
- menuOpts.push({ key: 'd', label: 'Switch to data-tools', section: 'Tools' });
841
- menuOpts.push({ key: 'm', label: 'Manage subscriptions', section: 'Subscriptions' });
842
- menuOpts.push({ key: 's', label: 'Settings', section: '' });
843
- menuOpts.push({ key: 'q', label: 'Exit', section: '' });
844
- console.log(menu(menuOpts));
910
+ console.log(' [1-9] Resume numbered above');
911
+ }
912
+ console.log(' [e] Manage sessions');
913
+ console.log(' [i] Import from replit-tools');
914
+ console.log(' [m] Manage subscriptions');
915
+ console.log(' [d] Switch to data-tools');
916
+ console.log(' [s] Settings');
917
+ console.log(' [q] Exit');
845
918
  console.log('');
846
919
 
847
920
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
@@ -872,6 +945,18 @@ async function mainScreen(rl, ask) {
872
945
 
873
946
  if (choice === 'e') { return { next: 'sessions' }; }
874
947
 
948
+ if (choice === 'i') {
949
+ const sessions = importReplitSessions(cwd);
950
+ if (sessions.length === 0) {
951
+ console.log('\n No replit-tools sessions found to import.\n');
952
+ } else {
953
+ console.log(`\n ✅ Found ${sessions.length} sessions from replit-tools.`);
954
+ console.log(' Sessions are automatically available in the list above.\n');
955
+ }
956
+ await ask(' Press Enter to continue...');
957
+ return { next: 'main' };
958
+ }
959
+
875
960
  if (choice === 'd') {
876
961
  const { spawnSync } = await import('node:child_process');
877
962
  const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
@@ -1000,6 +1085,26 @@ async function settingsScreen(rl, ask) {
1000
1085
  return { next: 'settings' };
1001
1086
  }
1002
1087
 
1088
+ // ─── Helper: aggregatePlans ───────────────────────────────────────────────────
1089
+
1090
+ const PLAN_PRICES = {
1091
+ pro: '$20', max5: '$100', max20: '$200',
1092
+ plus: '$20', pro100: '$100', pro200: '$200',
1093
+ };
1094
+
1095
+ function aggregatePlans(subs) {
1096
+ if (!subs || subs.length === 0) return '';
1097
+ const counts = {};
1098
+ for (const s of subs) {
1099
+ const price = PLAN_PRICES[s.plan] || s.plan;
1100
+ counts[price] = (counts[price] || 0) + 1;
1101
+ }
1102
+ return Object.entries(counts)
1103
+ .sort((a, b) => parseInt(b[0].slice(1)) - parseInt(a[0].slice(1)))
1104
+ .map(([price, count]) => `${price}×${count}`)
1105
+ .join(' ');
1106
+ }
1107
+
1003
1108
  // ─── Screen: subscriptionsScreen ─────────────────────────────────────────────
1004
1109
 
1005
1110
  async function subscriptionsScreen(rl, ask) {
@@ -1007,37 +1112,49 @@ async function subscriptionsScreen(rl, ask) {
1007
1112
  const cwd = process.cwd();
1008
1113
  const profile = loadProfile(cwd);
1009
1114
  const auth = await detectAuth();
1010
- const plans = detectPlans();
1011
1115
 
1012
- // Build status lines
1013
- const lines = [];
1014
- if (auth.claude.found) {
1015
- const plan = plans.claude?.label || plans.claude?.plan || 'unknown plan';
1016
- const sub = profile?.providers?.claude;
1017
- const label = sub?.label ? ` [${sub.label}]` : '';
1018
- const d = sub?.expiresAt ? daysUntil(sub.expiresAt) : null;
1019
- const expiry = d !== null ? ` (${d < 0 ? 'expired' : d === 0 ? 'today' : `${d}d left`})` : '';
1020
- lines.push(` ✅ Claude: ${plan}${label}${expiry}`);
1021
- } else {
1022
- lines.push(` ⚠️ Claude: not linked`);
1116
+ // Backward compat: migrate old single-sub format to subs array
1117
+ for (const prov of ['claude', 'openai']) {
1118
+ const p = profile?.providers?.[prov];
1119
+ if (p && !p.subs && p.plan) {
1120
+ p.subs = [{ plan: p.plan, label: p.label || null, expiresAt: p.expiresAt || null }];
1121
+ }
1023
1122
  }
1024
- if (auth.openai.found) {
1025
- const plan = plans.openai?.label || plans.openai?.plan || 'unknown plan';
1026
- const sub = profile?.providers?.openai;
1027
- const label = sub?.label ? ` [${sub.label}]` : '';
1028
- const d = sub?.expiresAt ? daysUntil(sub.expiresAt) : null;
1029
- const expiry = d !== null ? ` (${d < 0 ? 'expired' : d === 0 ? 'today' : `${d}d left`})` : '';
1030
- lines.push(` ✅ OpenAI: ${plan}${label}${expiry}`);
1031
- } else {
1032
- lines.push(` ⚠️ OpenAI: not linked`);
1123
+
1124
+ // Build status lines roster format
1125
+ const lines = [];
1126
+
1127
+ function buildProviderLines(provKey, displayName, authFound) {
1128
+ const sub = profile?.providers?.[provKey];
1129
+ const subs = sub?.subs || [];
1130
+ if (!authFound && subs.length === 0) {
1131
+ lines.push(` ⚠️ ${displayName}: not linked`);
1132
+ return;
1133
+ }
1134
+ const aggregate = aggregatePlans(subs);
1135
+ const prefix = authFound ? '✅' : '⚠️ ';
1136
+ lines.push(` ${prefix} ${displayName}:${aggregate ? ' ' + aggregate : ' (no subs)'}`);
1137
+ subs.forEach((s, i) => {
1138
+ const planLabels = provKey === 'claude' ? CLAUDE_PLAN_LABELS : OPENAI_PLAN_LABELS;
1139
+ const planLabel = planLabels[s.plan] ?? s.plan ?? 'unknown';
1140
+ const nameStr = (s.label || '(no label)').padEnd(22);
1141
+ const d = s.expiresAt ? daysUntil(s.expiresAt) : null;
1142
+ const expiry = d === null ? '' : d < 0 ? ' (expired)' : d === 0 ? ' (today)' : ` (${d}d left)`;
1143
+ lines.push(` ${i + 1}. ${nameStr} ${planLabel}${expiry}`);
1144
+ });
1033
1145
  }
1034
1146
 
1147
+ buildProviderLines('claude', 'Claude', auth.claude.found);
1148
+ lines.push('');
1149
+ buildProviderLines('openai', 'OpenAI', auth.openai.found);
1150
+
1035
1151
  console.log(box('Subscriptions', lines));
1036
1152
  console.log('');
1037
1153
 
1038
1154
  const menuOpts = [
1039
1155
  { key: '1', label: 'Add Claude sub', section: 'Link' },
1040
1156
  { key: '2', label: 'Add Codex sub', section: 'Link' },
1157
+ { key: 'r', label: 'Remove a sub', section: 'Link' },
1041
1158
  { key: 'b', label: 'Back to home', section: '' },
1042
1159
  ];
1043
1160
  console.log(menu(menuOpts));
@@ -1049,7 +1166,7 @@ async function subscriptionsScreen(rl, ask) {
1049
1166
  console.log('\n Linking Claude subscription...');
1050
1167
  console.log(' A browser window will open — paste the code below when prompted.\n');
1051
1168
  const { spawnSync } = await import('node:child_process');
1052
- const r = spawnSync('claude', ['login'], { stdio: 'inherit', timeout: 60000 });
1169
+ const r = spawnSync('claude', ['auth', 'login'], { stdio: 'inherit', timeout: 60000 });
1053
1170
  if (r.status === 0) {
1054
1171
  console.log('\n ✅ Claude linked successfully!\n');
1055
1172
  const label = (await ask(" Label (e.g. \"Josh's $100 sub\", or Enter to skip): ")).trim();
@@ -1060,8 +1177,9 @@ async function subscriptionsScreen(rl, ask) {
1060
1177
  if (!profile.providers.claude) profile.providers.claude = { enabled: true };
1061
1178
  profile.providers.claude.plan = plan;
1062
1179
  profile.providers.claude.enabled = true;
1063
- if (label) profile.providers.claude.label = label;
1064
- if (expiry) profile.providers.claude.expiresAt = expiry;
1180
+ // Push to subs array instead of overwriting
1181
+ if (!profile.providers.claude.subs) profile.providers.claude.subs = [];
1182
+ profile.providers.claude.subs.push({ plan, label: label || null, expiresAt: expiry || null });
1065
1183
  saveProfile(profile, { cwd });
1066
1184
  console.log(' ✓ Saved\n');
1067
1185
  await ask(' Press Enter to continue...');
@@ -1087,8 +1205,9 @@ async function subscriptionsScreen(rl, ask) {
1087
1205
  if (!profile.providers.openai) profile.providers.openai = { enabled: true };
1088
1206
  profile.providers.openai.plan = plan;
1089
1207
  profile.providers.openai.enabled = true;
1090
- if (label) profile.providers.openai.label = label;
1091
- if (expiry) profile.providers.openai.expiresAt = expiry;
1208
+ // Push to subs array instead of overwriting
1209
+ if (!profile.providers.openai.subs) profile.providers.openai.subs = [];
1210
+ profile.providers.openai.subs.push({ plan, label: label || null, expiresAt: expiry || null });
1092
1211
  saveProfile(profile, { cwd });
1093
1212
  console.log(' ✓ Saved\n');
1094
1213
  await ask(' Press Enter to continue...');
@@ -1099,6 +1218,56 @@ async function subscriptionsScreen(rl, ask) {
1099
1218
  return { next: 'subscriptions' };
1100
1219
  }
1101
1220
 
1221
+ if (choice === 'r') {
1222
+ // Build a flat numbered list of all subs across both providers
1223
+ const allSubs = [];
1224
+ for (const [provKey, displayName] of [['claude', 'Claude'], ['openai', 'OpenAI']]) {
1225
+ const subs = profile?.providers?.[provKey]?.subs || [];
1226
+ for (const s of subs) {
1227
+ allSubs.push({ provKey, displayName, sub: s });
1228
+ }
1229
+ }
1230
+
1231
+ if (allSubs.length === 0) {
1232
+ console.log('\n No subscriptions to remove.\n');
1233
+ await ask(' Press Enter to continue...');
1234
+ return { next: 'subscriptions' };
1235
+ }
1236
+
1237
+ console.log('\n Remove a subscription:\n');
1238
+ allSubs.forEach(({ displayName, sub }, i) => {
1239
+ const planLabels = displayName === 'Claude' ? CLAUDE_PLAN_LABELS : OPENAI_PLAN_LABELS;
1240
+ const planLabel = planLabels[sub.plan] ?? sub.plan ?? 'unknown';
1241
+ const labelStr = sub.label ? ` [${sub.label}]` : '';
1242
+ console.log(` (${i + 1}) ${displayName}: ${planLabel}${labelStr}`);
1243
+ });
1244
+ console.log(' (Enter) Cancel\n');
1245
+
1246
+ const numStr = (await ask(' Remove #: ')).trim();
1247
+ const numChoice = parseInt(numStr, 10);
1248
+ if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= allSubs.length) {
1249
+ const { provKey, sub } = allSubs[numChoice - 1];
1250
+ const confirm = (await ask(` Remove "${sub.label || sub.plan}" from ${provKey}? (y/N): `)).trim().toLowerCase();
1251
+ if (confirm === 'y') {
1252
+ const subs = profile.providers[provKey].subs;
1253
+ const idx = subs.indexOf(sub);
1254
+ if (idx !== -1) subs.splice(idx, 1);
1255
+ // Update top-level plan to first remaining sub (or keep as-is)
1256
+ if (subs.length > 0) {
1257
+ profile.providers[provKey].plan = subs[0].plan;
1258
+ }
1259
+ saveProfile(profile, { cwd });
1260
+ console.log(' ✓ Removed\n');
1261
+ } else {
1262
+ console.log(' Cancelled.\n');
1263
+ }
1264
+ } else {
1265
+ console.log(' Cancelled.\n');
1266
+ }
1267
+ await ask(' Press Enter to continue...');
1268
+ return { next: 'subscriptions' };
1269
+ }
1270
+
1102
1271
  return { next: 'main' };
1103
1272
  }
1104
1273
 
@@ -1128,7 +1297,7 @@ async function authScreen(rl, ask) {
1128
1297
  'Claude:',
1129
1298
  auth.claude.found
1130
1299
  ? ` logged in via ${auth.claude.source}`
1131
- : ` not logged in — run: claude login`,
1300
+ : ` not logged in — run: claude auth login`,
1132
1301
  ` plan: ${claudePlanLabel}${claudeSub?.label ? ` [${claudeSub.label}]` : ''}`,
1133
1302
  '',
1134
1303
  'OpenAI:',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "7.1.10",
3
+ "version": "7.1.12",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
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, readdirSync } from 'node:fs';
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, renameSync, readdirSync, statSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
 
16
16
  // ─── Constants ────────────────────────────────────────────────────────────────
@@ -309,6 +309,112 @@ export function importReplitSessions(cwd = process.cwd()) {
309
309
  } catch { continue; }
310
310
  }
311
311
 
312
+ // Scan ~/.claude/projects/-home-runner-workspace/ for JSONL files not already in bySession
313
+ const projectsDir = join(process.env.HOME || '/root', '.claude', 'projects', '-home-runner-workspace');
314
+ if (existsSync(projectsDir)) {
315
+ try {
316
+ for (const f of readdirSync(projectsDir)) {
317
+ if (!f.endsWith('.jsonl') || f.startsWith('agent-')) continue;
318
+ const sessionId = f.replace('.jsonl', '');
319
+ if (bySession.has(sessionId)) continue;
320
+
321
+ try {
322
+ const content = readFileSync(join(projectsDir, f), 'utf8');
323
+ const lines = content.split('\n').filter(Boolean).slice(0, 50);
324
+ let firstPrompt = null;
325
+ let lastTimestamp = 0;
326
+
327
+ for (const line of lines) {
328
+ try {
329
+ const entry = JSON.parse(line);
330
+ if (entry.timestamp && entry.timestamp > lastTimestamp) lastTimestamp = entry.timestamp;
331
+ if (!firstPrompt && entry.type === 'user' && entry.message?.content) {
332
+ const text = typeof entry.message.content === 'string'
333
+ ? entry.message.content
334
+ : entry.message.content?.[0]?.text;
335
+ if (text && !text.startsWith('/') && text.length < 200) {
336
+ firstPrompt = text;
337
+ }
338
+ }
339
+ } catch { continue; }
340
+ }
341
+
342
+ if (lastTimestamp === 0) {
343
+ const stat = statSync(join(projectsDir, f));
344
+ lastTimestamp = Math.floor(stat.mtimeMs / 1000);
345
+ }
346
+
347
+ bySession.set(sessionId, {
348
+ sessionId,
349
+ project: '-home-runner-workspace',
350
+ entries: [],
351
+ firstPrompt: firstPrompt || sessionId.slice(0, 8) + '...',
352
+ lastTimestamp,
353
+ });
354
+ } catch { continue; }
355
+ }
356
+ } catch { /* non-fatal */ }
357
+ }
358
+
359
+ // Scan ~/.codex/sessions/ for codex session JSONLs (YYYY/MM/DD tree)
360
+ const codexSessionsDir = join(process.env.HOME || '/root', '.codex', 'sessions');
361
+ if (existsSync(codexSessionsDir)) {
362
+ try {
363
+ const walk = (dir) => {
364
+ let results = [];
365
+ try {
366
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
367
+ const full = join(dir, entry.name);
368
+ if (entry.isDirectory()) results = results.concat(walk(full));
369
+ else if (entry.isFile() && entry.name.endsWith('.jsonl')) results.push(full);
370
+ }
371
+ } catch {}
372
+ return results;
373
+ };
374
+
375
+ for (const f of walk(codexSessionsDir)) {
376
+ try {
377
+ const content = readFileSync(f, 'utf8');
378
+ const lines = content.split('\n').filter(Boolean);
379
+ if (!lines.length) continue;
380
+
381
+ const meta = JSON.parse(lines[0]);
382
+ if (meta.type !== 'session_meta' || !meta.payload) continue;
383
+ if (meta.payload.cwd !== cwd && meta.payload.cwd !== '/home/runner/workspace') continue;
384
+
385
+ const id = meta.payload.id;
386
+ if (bySession.has(id)) continue;
387
+
388
+ let firstPrompt = null;
389
+ let lastTimestamp = Date.parse(meta.payload.timestamp || meta.timestamp) / 1000;
390
+
391
+ for (const ln of lines) {
392
+ try {
393
+ const j = JSON.parse(ln);
394
+ if (j.timestamp) {
395
+ const ts = Date.parse(j.timestamp) / 1000;
396
+ if (ts > lastTimestamp) lastTimestamp = ts;
397
+ }
398
+ if (!firstPrompt && j.type === 'event_msg' && j.payload?.type === 'user_message') {
399
+ const text = (j.payload.message || '').trim();
400
+ if (text) firstPrompt = text;
401
+ }
402
+ } catch { continue; }
403
+ }
404
+
405
+ bySession.set(id, {
406
+ sessionId: id,
407
+ project: '-home-runner-workspace',
408
+ entries: [],
409
+ firstPrompt: firstPrompt || id.slice(0, 8) + '...',
410
+ lastTimestamp,
411
+ tool: 'codex',
412
+ });
413
+ } catch { continue; }
414
+ }
415
+ } catch { /* non-fatal */ }
416
+ }
417
+
312
418
  // Read active terminal sessions
313
419
  // Use the same root as replitBase (go up one level from .claude-persistent)
314
420
  const replitRoot = join(replitBase, '..');
@@ -346,6 +452,7 @@ export function importReplitSessions(cwd = process.cwd()) {
346
452
  isActive: activeSessionIds.has(id),
347
453
  source: 'replit-tools',
348
454
  age: timeAgo(sess.lastTimestamp),
455
+ tool: sess.tool || 'claude',
349
456
  });
350
457
  }
351
458