dual-brain 0.1.1 → 0.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.
Files changed (2) hide show
  1. package/bin/dual-brain.mjs +217 -321
  2. package/package.json +1 -1
@@ -998,112 +998,94 @@ function loadTerminalState(cwd, terminalId) {
998
998
  } catch { return null; }
999
999
  }
1000
1000
 
1001
- // ─── Screen: mainScreen ───────────────────────────────────────────────────────
1002
-
1003
- async function mainScreen(rl, ask) {
1004
- const cwd = process.cwd();
1005
- const version = readVersion();
1006
- const profile = loadProfile(cwd);
1007
- const auth = await detectAuth();
1001
+ // ─── Dashboard box helpers ────────────────────────────────────────────────────
1008
1002
 
1009
- const claudeSub = profile?.providers?.claude;
1010
- const openaiSub = profile?.providers?.openai;
1011
- const claudePlan = claudeSub?.plan ?? 'Pro';
1012
- const openaiPlan = openaiSub?.plan ?? 'Plus';
1003
+ /**
1004
+ * Build a provider status string for the dashboard status line.
1005
+ * Returns a string like: "🟢 Claude $100×2 $20×1 🟢 OpenAI $100"
1006
+ * Uses ANSI color codes for the dots (no emoji width issues).
1007
+ */
1008
+ function buildProviderStatusLine(profile, auth) {
1009
+ const GREEN = '\x1b[32m●\x1b[0m';
1010
+ const RED = '\x1b[31m●\x1b[0m';
1011
+ const now = Date.now();
1013
1012
 
1014
- // Check subscription expiry
1015
- const now = Date.now();
1016
- const claudeExpired = claudeSub?.expiresAt && Date.parse(claudeSub.expiresAt) < now;
1017
- const openaiExpired = openaiSub?.expiresAt && Date.parse(openaiSub.expiresAt) < now;
1013
+ function providerSegment(provKey, displayName) {
1014
+ const sub = profile?.providers?.[provKey];
1015
+ const found = provKey === 'claude' ? auth.claude.found : auth.openai.found;
1016
+ if (!found) return `${RED} ${displayName}: not connected`;
1018
1017
 
1019
- const claudeDays = daysUntil(claudeSub?.expiresAt);
1020
- const openaiDays = daysUntil(openaiSub?.expiresAt);
1018
+ const expired = sub?.expiresAt && Date.parse(sub.expiresAt) < now;
1019
+ if (expired) return `${RED} ${displayName}: expired`;
1021
1020
 
1022
- function subLine(name, plan, found, expired, days, sub) {
1023
- const label = sub?.label ? ` [${sub.label}]` : '';
1024
- if (!found) return `⚠️ ${name}: not logged in — run: ${name === 'Claude' ? 'claude auth login' : 'codex login'}`;
1025
- // Multi-sub: show aggregated counts when more than one sub exists
1021
+ const dot = GREEN;
1022
+ // Multi-sub: show aggregated plan amounts
1026
1023
  const subs = sub?.subs;
1027
- if (subs && subs.length > 1) {
1028
- const aggregate = aggregatePlans(subs);
1029
- return `✅ ${name}: ${aggregate} [${subs.length} subs]`;
1024
+ if (subs && subs.length > 0) {
1025
+ const agg = aggregatePlans(subs);
1026
+ return `${dot} ${displayName} ${agg}`;
1030
1027
  }
1031
- if (expired) return `🔴 ${name}: ${plan} expired${label} — will re-auth`;
1032
- const daysNote = (days !== null && days <= 7) ? ` (${days}d left)` : '';
1033
- return `✅ ${name}: ${plan}${label}${daysNote}`;
1028
+ // Single plan
1029
+ const planPrice = PLAN_PRICES[sub?.plan] || sub?.plan || 'connected';
1030
+ return `${dot} ${displayName} ${planPrice}`;
1034
1031
  }
1035
1032
 
1036
- const headerLines = [
1037
- subLine('Claude', claudePlan, auth.claude.found, claudeExpired, claudeDays, claudeSub),
1038
- subLine('OpenAI', openaiPlan, auth.openai.found, openaiExpired, openaiDays, openaiSub),
1039
- ];
1033
+ const parts = [];
1034
+ parts.push(providerSegment('claude', 'Claude'));
1035
+ parts.push(providerSegment('openai', 'OpenAI'));
1036
+ return parts.join(' ');
1037
+ }
1040
1038
 
1041
- const rtMain = detectReplitTools(cwd);
1042
- const dtVersion = (rtMain.installed && rtMain.version) ? rtMain.version : null;
1043
- console.log(`🧠 Dual Brain v${version}`);
1044
- const latestVersion = await checkForUpdates(version);
1045
- if (latestVersion) {
1046
- console.log(` ⬆️ Update available: v${version} v${latestVersion}`);
1047
- console.log(` Run: npx -y dual-brain@latest`);
1048
- }
1049
- console.log('');
1039
+ /**
1040
+ * Render a box row padded to inner width W (stripping ANSI for length calculation).
1041
+ * Returns a string like: "│ content padded to W │"
1042
+ */
1043
+ function makeBoxRow(content, W) {
1044
+ const plain = content.replace(/\x1b\[[0-9;]*m/g, '');
1045
+ const padding = Math.max(0, W - plain.length);
1046
+ return `│ ${content}${' '.repeat(padding)} │`;
1047
+ }
1050
1048
 
1051
- // Provider status (outside the box)
1052
- for (const line of headerLines) {
1053
- console.log(` ${line}`);
1054
- }
1055
- if (dtVersion) {
1056
- console.log(` 📦 data-tools v${dtVersion} detected`);
1057
- }
1049
+ // ─── Screen: mainScreen ───────────────────────────────────────────────────────
1058
1050
 
1059
- const sparkline = buildSparkline(cwd);
1060
- if (sparkline) {
1061
- console.log(` Activity: ${sparkline}`);
1062
- }
1051
+ async function mainScreen(rl, ask) {
1052
+ const cwd = process.cwd();
1053
+ const version = readVersion();
1054
+ const profile = loadProfile(cwd);
1055
+ const auth = await detectAuth();
1063
1056
 
1064
- // Silent OAuth token auto-refresh (like data-tools)
1057
+ const claudeSub = profile?.providers?.claude;
1058
+ const openaiSub = profile?.providers?.openai;
1059
+
1060
+ // Check subscription expiry for auto-refresh
1061
+ const now = Date.now();
1062
+ const claudeExpired = claudeSub?.expiresAt && Date.parse(claudeSub.expiresAt) < now;
1063
+ const openaiExpired = openaiSub?.expiresAt && Date.parse(openaiSub.expiresAt) < now;
1064
+
1065
+ // Silent OAuth token auto-refresh
1065
1066
  try {
1066
1067
  const { autoRefreshToken } = await import('../src/profile.mjs');
1067
- const refreshResult = await autoRefreshToken(cwd);
1068
- if (refreshResult.status === 'refreshed') {
1069
- console.log(` 🔄 Token auto-refreshed (${refreshResult.hoursRemaining}h remaining)`);
1070
- }
1068
+ await autoRefreshToken(cwd);
1071
1069
  } catch {}
1072
1070
 
1073
- // Append-only session archive sync (like data-tools)
1071
+ // Append-only session archive sync
1074
1072
  try {
1075
1073
  const { syncSessionMirror } = await import('../src/session.mjs');
1076
- const mirror = syncSessionMirror(cwd);
1077
- if (mirror.copied > 0 || mirror.grew > 0) {
1078
- console.log(` ✅ Archive mirror: +${mirror.copied} new, ${mirror.grew} updated`);
1079
- }
1074
+ syncSessionMirror(cwd);
1080
1075
  } catch {}
1081
1076
 
1082
1077
  // Auto-refresh expired subscriptions
1083
1078
  if (claudeExpired || openaiExpired) {
1084
1079
  const { spawnSync } = await import('node:child_process');
1085
- const expired = [];
1086
- if (claudeExpired) expired.push('Claude');
1087
- if (openaiExpired) expired.push('OpenAI');
1088
- console.log(`\n ${expired.join(' & ')} subscription expired. Re-authenticating...`);
1089
1080
  if (claudeExpired) {
1090
1081
  const r = spawnSync('claude', ['auth', 'login'], { stdio: 'inherit', timeout: 30000 });
1091
- if (r.status === 0) {
1092
- claudeSub.expiresAt = null;
1093
- saveProfile(profile, { cwd });
1094
- console.log(' ✓ Claude re-authenticated');
1095
- }
1082
+ if (r.status === 0) { claudeSub.expiresAt = null; saveProfile(profile, { cwd }); }
1096
1083
  }
1097
1084
  if (openaiExpired) {
1098
1085
  const r = spawnSync('codex', ['login'], { stdio: 'inherit', timeout: 30000 });
1099
- if (r.status === 0) {
1100
- openaiSub.expiresAt = null;
1101
- saveProfile(profile, { cwd });
1102
- console.log(' ✓ OpenAI re-authenticated');
1103
- }
1086
+ if (r.status === 0) { openaiSub.expiresAt = null; saveProfile(profile, { cwd }); }
1104
1087
  }
1105
1088
  }
1106
- console.log('');
1107
1089
 
1108
1090
  // Build session index in background (powers search + smart resume)
1109
1091
  try {
@@ -1111,245 +1093,153 @@ async function mainScreen(rl, ask) {
1111
1093
  buildSessionIndex(cwd);
1112
1094
  } catch {}
1113
1095
 
1114
- const recentSessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 7);
1096
+ // Gather recent sessions
1097
+ const recentSessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 3);
1115
1098
 
1116
- if (recentSessions.length > 0) {
1117
- console.log(' Recent Sessions:');
1118
- recentSessions.forEach((sess, i) => {
1119
- const pin = sess.pinned ? '📌 ' : ' ';
1120
- const active = sess.isActive ? ' ●' : '';
1121
- const cat = sess.category ? ` [${sess.category}]` : '';
1122
- const tool = (sess.tool === 'codex') ? 'cdx' : 'cld';
1123
- // If the name is still the "Session XXXXXXXX" fallback, try the project path instead
1124
- let rawName = sess.name || '';
1125
- if (/^Session [0-9a-f]{8,}$/i.test(rawName)) {
1126
- rawName = sess.project ? sess.project.replace(/^-/, '/').replace(/-/g, '/') : sess.id.slice(0, 8);
1127
- }
1128
- const displayName = rawName.length > 40 ? rawName.slice(0, 37) + '...' : (rawName || sess.id.slice(0, 8));
1129
- console.log(` [${i + 1}] ${pin}${tool} ${sess.age.padEnd(8)} ${displayName}${active}${cat}`);
1130
- });
1131
- console.log('');
1132
- }
1099
+ // Detect data-tools version
1100
+ const rtMain = detectReplitTools(cwd);
1101
+ const dtVersion = (rtMain.installed && rtMain.version) ? rtMain.version : null;
1133
1102
 
1134
- const brandW = 37;
1135
- const brandTop = ` ┌${'─'.repeat(brandW)}┐`;
1136
- const brandBottom = ` └${'─'.repeat(brandW)}┘`;
1137
- const brandPad = (s) => {
1138
- const leftPad = Math.floor((brandW - s.length) / 2);
1139
- const rightPad = brandW - s.length - leftPad;
1140
- return ' '.repeat(leftPad) + s + ' '.repeat(rightPad);
1141
- };
1142
- console.log(brandTop);
1143
- console.log(` │ ${brandPad('Dual Brain Session Manager')}│`);
1144
- console.log(` │ ${brandPad('Built on data-tools by Steve Moraco')}│`);
1145
- console.log(brandBottom);
1146
- console.log('');
1103
+ // ── Box layout ────────────────────────────────────────────────────────────
1104
+ const termW = process.stdout.columns || 60;
1105
+ const boxW = Math.min(termW - 2, 60); // outer width (including │ │)
1106
+ const W = boxW - 4; // inner content width ( {content} │)
1147
1107
 
1148
- const running = countRunningInstances();
1149
- const runningParts = [];
1150
- if (running.claude > 0) runningParts.push(`${running.claude} claude`);
1151
- if (running.codex > 0) runningParts.push(`${running.codex} codex`);
1152
- if (runningParts.length > 0) {
1153
- console.log(` (${runningParts.join(', ')} running)`);
1154
- console.log('');
1155
- }
1108
+ const top = `┌${'─'.repeat(boxW - 2)}┐`;
1109
+ const sep = `├${'─'.repeat(boxW - 2)}┤`;
1110
+ const bot = `└${'─'.repeat(boxW - 2)}┘`;
1156
1111
 
1157
- console.log(' [c] Continue last session');
1158
- console.log(' [n] New session');
1159
- console.log('');
1160
- if (recentSessions.length > 0) {
1161
- console.log(' [1-9] Resume numbered above');
1162
- }
1163
- console.log(' [r] Resume (full list)');
1164
- console.log(' [/] Search sessions');
1165
- console.log(' [e] Manage sessions');
1166
- console.log(' [m] Manage subscriptions');
1167
- console.log(' [s] Settings');
1168
- console.log(' [?] Help & shortcuts');
1169
- console.log('');
1170
- console.log(' \x1b[2mreplit-tools:\x1b[0m');
1171
- console.log(' [i] Import sessions');
1172
- console.log(' [d] Switch to data-tools');
1173
- console.log('');
1174
- console.log(' [q] Exit');
1175
- console.log('');
1112
+ const row = (content) => makeBoxRow(content, W);
1176
1113
 
1177
- const choice = (await ask(' Choice: ')).trim().toLowerCase();
1114
+ // ── Header: one line above the box ────────────────────────────────────────
1115
+ process.stdout.write(`\n🧠 dual-brain v${version}\n`);
1178
1116
 
1179
- if (choice === '?') {
1180
- const W = 37;
1181
- const helpTop = ` ┌${'─'.repeat(W)}┐`;
1182
- const helpSep = ` ├${'─'.repeat(W)}┤`;
1183
- const helpBottom = ` └${'─'.repeat(W)}┘`;
1184
- const helpPad = (s) => s + ' '.repeat(Math.max(0, W - s.length));
1185
- console.log('');
1186
- console.log(helpTop);
1187
- console.log(` │ ${helpPad('At ~/workspace$ prompt:')}│`);
1188
- console.log(` │ ${helpPad('db = show this menu')}│`);
1189
- console.log(` │ ${helpPad('j = login to claude')}│`);
1190
- console.log(` │ ${helpPad('k = login to codex')}│`);
1191
- console.log(helpSep);
1192
- console.log(` │ ${helpPad('In Claude:')}│`);
1193
- console.log(` │ ${helpPad('Ctrl+C x2 = back to menu')}│`);
1194
- console.log(` │ ${helpPad('Ctrl+C x3 = exit to shell')}│`);
1195
- console.log(helpBottom);
1196
- console.log('');
1197
- await ask(' Press Enter to continue...');
1198
- return { next: 'main' };
1199
- }
1117
+ // ── Status section ────────────────────────────────────────────────────────
1118
+ const providerLine = buildProviderStatusLine(profile, auth);
1200
1119
 
1201
- if (choice === 'n') { return { next: 'new-session' }; }
1120
+ const statusRows = [row(providerLine)];
1121
+ if (dtVersion) {
1122
+ statusRows.push(row(`\x1b[2m📦 data-tools v${dtVersion}\x1b[0m`));
1123
+ }
1202
1124
 
1203
- if (choice === 'c') {
1204
- const termId = getTerminalId();
1205
- const termState = loadTerminalState(cwd, termId);
1206
- const sessions = importReplitSessions(cwd);
1125
+ // ── Sessions section ──────────────────────────────────────────────────────
1126
+ const sessionRows = [];
1127
+ if (recentSessions.length === 0) {
1128
+ const noSessMsg = 'No sessions yet. Press n to start.';
1129
+ sessionRows.push(row(noSessMsg));
1130
+ } else {
1131
+ recentSessions.forEach((sess, i) => {
1132
+ // Normalize name: strip "Session XXXXXXXX" fallbacks
1133
+ let rawName = sess.name || '';
1134
+ if (/^Session [0-9a-f]{8,}$/i.test(rawName)) {
1135
+ rawName = sess.project
1136
+ ? sess.project.replace(/^-/, '/').replace(/-/g, '/')
1137
+ : sess.id.slice(0, 8);
1138
+ }
1139
+ // Layout: "{num} {name...} {age}"
1140
+ const numStr = String(i + 1);
1141
+ const ageStr = sess.age || '';
1142
+ // Available for name: W - numStr.length - 2 spaces - 2 spaces before age - ageStr.length
1143
+ const nameMax = W - numStr.length - 2 - 2 - ageStr.length;
1144
+ const name = rawName.length > nameMax
1145
+ ? rawName.slice(0, nameMax - 3) + '...'
1146
+ : rawName.padEnd(nameMax);
1147
+ const content = `${numStr} ${name} ${ageStr}`;
1148
+ sessionRows.push(row(content));
1149
+ });
1150
+ }
1207
1151
 
1208
- // Priority: terminal-specific last session, then global last session
1209
- const targetId = termState?.sessionId || (sessions.length > 0 ? sessions[0].id : null);
1152
+ // ── Actions bar ───────────────────────────────────────────────────────────
1153
+ const actionsContent = '↵ Resume n New / Search s Settings q Quit';
1154
+ const actionsRow = row(actionsContent);
1210
1155
 
1211
- if (!targetId) {
1212
- console.log('\n No recent sessions found.\n');
1213
- await ask(' Press Enter to continue...');
1214
- return { next: 'main' };
1215
- }
1156
+ // ── Print the full box ────────────────────────────────────────────────────
1157
+ const lines = [
1158
+ top,
1159
+ ...statusRows,
1160
+ sep,
1161
+ ...sessionRows,
1162
+ sep,
1163
+ actionsRow,
1164
+ bot,
1165
+ ];
1166
+ process.stdout.write(lines.join('\n') + '\n');
1167
+ process.stdout.write(`\x1b[2mBuilt on data-tools by Steve Moraco\x1b[0m\n\n`);
1216
1168
 
1217
- // Smart resume preview
1218
- try {
1219
- const { getSessionContext } = await import('../src/session.mjs');
1220
- const ctx = getSessionContext(targetId, cwd);
1221
- if (ctx) {
1222
- console.log('');
1223
- if (ctx.lastPrompt) console.log(` Last working on: ${ctx.lastPrompt}`);
1224
- if (ctx.filesTouched.length > 0) console.log(` Files touched: ${ctx.filesTouched.join(', ')}`);
1225
- }
1226
- } catch {}
1169
+ // ── Key handling ──────────────────────────────────────────────────────────
1170
+ const raw = (await ask('')).trim();
1171
+ const choice = raw.toLowerCase();
1227
1172
 
1173
+ // Enter (empty) → resume most recent session
1174
+ if (raw === '' || choice === '\r') {
1175
+ if (recentSessions.length === 0) {
1176
+ return { next: 'new-session' };
1177
+ }
1178
+ const sess = recentSessions[0];
1228
1179
  const { spawnSync } = await import('node:child_process');
1229
- const tool = termState?.tool || 'claude';
1230
- console.log(`\n Resuming: ${tool} --resume ${targetId}\n`);
1231
- spawnSync(tool === 'codex' ? 'codex' : 'claude', ['--resume', targetId], { stdio: 'inherit' });
1232
- saveTerminalState(cwd, termId, targetId, tool);
1180
+ process.stdout.write(`\n Launching: claude --resume ${sess.id}\n\n`);
1181
+ spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
1182
+ saveTerminalState(cwd, getTerminalId(), sess.id, sess.tool || 'claude');
1233
1183
  return { next: 'main' };
1234
1184
  }
1235
1185
 
1236
- const numChoice = parseInt(choice, 10);
1186
+ // Number 1-3 resume that session
1187
+ const numChoice = parseInt(raw, 10);
1237
1188
  if (!isNaN(numChoice) && numChoice >= 1 && numChoice <= recentSessions.length) {
1238
1189
  const sess = recentSessions[numChoice - 1];
1239
-
1240
- // Smart resume preview
1241
1190
  try {
1242
1191
  const { getSessionContext } = await import('../src/session.mjs');
1243
1192
  const ctx = getSessionContext(sess.id, cwd);
1244
1193
  if (ctx) {
1245
- console.log('');
1246
- if (ctx.lastPrompt) console.log(` Last working on: ${ctx.lastPrompt}`);
1247
- if (ctx.filesTouched.length > 0) console.log(` Files touched: ${ctx.filesTouched.join(', ')}`);
1194
+ if (ctx.lastPrompt) process.stdout.write(`\n Last working on: ${ctx.lastPrompt}\n`);
1195
+ if (ctx.filesTouched.length > 0) process.stdout.write(` Files touched: ${ctx.filesTouched.join(', ')}\n`);
1248
1196
  }
1249
1197
  } catch {}
1250
-
1251
1198
  const { spawnSync } = await import('node:child_process');
1252
- console.log(`\n Launching: claude --resume ${sess.id}\n`);
1199
+ process.stdout.write(`\n Launching: claude --resume ${sess.id}\n\n`);
1253
1200
  spawnSync('claude', ['--resume', sess.id], { stdio: 'inherit' });
1254
1201
  saveTerminalState(cwd, getTerminalId(), sess.id, sess.tool || 'claude');
1255
1202
  return { next: 'main' };
1256
1203
  }
1257
1204
 
1258
- if (choice === 'r') {
1259
- const allSessions = enrichSessions(importReplitSessions(cwd), cwd);
1260
- if (allSessions.length === 0) {
1261
- console.log('\n No sessions found.\n');
1262
- await ask(' Press Enter to continue...');
1263
- return { next: 'main' };
1264
- }
1265
-
1266
- console.log('\n All Sessions:');
1267
- allSessions.forEach((sess, i) => {
1268
- const pin = sess.pinned ? '📌 ' : ' ';
1269
- const active = sess.isActive ? ' ●' : '';
1270
- const cat = sess.category ? ` [${sess.category}]` : '';
1271
- const tool = (sess.tool === 'codex') ? 'cdx' : 'cld';
1272
- console.log(` [${String(i + 1).padStart(2)}] ${pin}${tool} ${sess.age.padEnd(8)} ${sess.name}${active}${cat}`);
1273
- });
1274
- console.log('');
1275
-
1276
- const pick = (await ask(' Enter number to resume (or Enter to cancel): ')).trim();
1277
- const num = parseInt(pick, 10);
1278
- if (!isNaN(num) && num >= 1 && num <= allSessions.length) {
1279
- const sess = allSessions[num - 1];
1280
- const { spawnSync } = await import('node:child_process');
1281
- const tool = sess.tool === 'codex' ? 'codex' : 'claude';
1282
- console.log(`\n Launching: ${tool} --resume ${sess.id}\n`);
1283
- spawnSync(tool, ['--resume', sess.id], { stdio: 'inherit' });
1284
- }
1285
- return { next: 'main' };
1286
- }
1205
+ if (choice === 'n') { return { next: 'new-session' }; }
1287
1206
 
1288
1207
  if (choice === '/') {
1289
1208
  const query = (await ask(' Search: ')).trim();
1290
1209
  if (!query) return { next: 'main' };
1291
1210
 
1292
1211
  const { searchSessions, buildSessionIndex } = await import('../src/session.mjs');
1293
- // Build index if needed (silent)
1294
1212
  try { buildSessionIndex(cwd); } catch {}
1295
1213
 
1296
1214
  const results = searchSessions(query, cwd);
1297
1215
  if (results.length === 0) {
1298
- console.log(`\n No sessions matching "${query}"\n`);
1216
+ process.stdout.write(`\n No sessions matching "${query}"\n\n`);
1299
1217
  await ask(' Press Enter to continue...');
1300
1218
  return { next: 'main' };
1301
1219
  }
1302
1220
 
1303
- console.log(`\n Found ${results.length} session${results.length === 1 ? '' : 's'}:`);
1221
+ process.stdout.write(`\n Found ${results.length} session${results.length === 1 ? '' : 's'}:\n`);
1304
1222
  results.slice(0, 9).forEach((sess, i) => {
1305
- const tool = sess.tool === 'codex' ? 'cdx' : 'cld';
1306
- const date = sess.date ? new Date(sess.date).toLocaleDateString() : '?';
1223
+ const tool = sess.tool === 'codex' ? 'cdx' : 'cld';
1224
+ const date = sess.date ? new Date(sess.date).toLocaleDateString() : '?';
1307
1225
  const topics = sess.topics.slice(0, 3).join(', ');
1308
- console.log(` [${i + 1}] ${tool} ${date} ${sess.prompts.first || sess.id.slice(0, 8)}`);
1309
- if (topics) console.log(` topics: ${topics}`);
1226
+ process.stdout.write(` [${i + 1}] ${tool} ${date} ${sess.prompts.first || sess.id.slice(0, 8)}\n`);
1227
+ if (topics) process.stdout.write(` topics: ${topics}\n`);
1310
1228
  });
1311
- console.log('');
1229
+ process.stdout.write('\n');
1312
1230
 
1313
1231
  const pick = (await ask(' Enter number to resume (or Enter to cancel): ')).trim();
1314
- const num = parseInt(pick, 10);
1232
+ const num = parseInt(pick, 10);
1315
1233
  if (!isNaN(num) && num >= 1 && num <= Math.min(results.length, 9)) {
1316
1234
  const sess = results[num - 1];
1317
1235
  const { spawnSync } = await import('node:child_process');
1318
1236
  const tool = sess.tool === 'codex' ? 'codex' : 'claude';
1319
- console.log(`\n Launching: ${tool} --resume ${sess.id}\n`);
1237
+ process.stdout.write(`\n Launching: ${tool} --resume ${sess.id}\n\n`);
1320
1238
  spawnSync(tool, ['--resume', sess.id], { stdio: 'inherit' });
1321
1239
  }
1322
1240
  return { next: 'main' };
1323
1241
  }
1324
1242
 
1325
- if (choice === 'e') { return { next: 'sessions' }; }
1326
-
1327
- if (choice === 'i') {
1328
- const sessions = importReplitSessions(cwd);
1329
- if (sessions.length === 0) {
1330
- console.log('\n No replit-tools sessions found to import.\n');
1331
- } else {
1332
- console.log(`\n ✅ Found ${sessions.length} sessions from replit-tools.`);
1333
- console.log(' Sessions are automatically available in the list above.\n');
1334
- }
1335
- await ask(' Press Enter to continue...');
1336
- return { next: 'main' };
1337
- }
1338
-
1339
- if (choice === 'd') {
1340
- const { spawnSync } = await import('node:child_process');
1341
- const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
1342
- if (which.status === 0) {
1343
- spawnSync('claude-menu', { stdio: 'inherit' });
1344
- } else {
1345
- console.log('\n data-tools not found — install with: npm i -g replit-tools\n');
1346
- await ask(' Press Enter to continue...');
1347
- }
1348
- return { next: 'main' };
1349
- }
1350
-
1351
- if (choice === 'm') { return { next: 'subscriptions' }; }
1352
-
1353
1243
  if (choice === 's') { return { next: 'settings' }; }
1354
1244
  if (choice === 'q' || choice === 'exit') { return { next: 'exit' }; }
1355
1245
 
@@ -1391,84 +1281,90 @@ async function newSessionScreen(rl, ask) {
1391
1281
 
1392
1282
  async function settingsScreen(rl, ask) {
1393
1283
  const cwd = process.cwd();
1394
- const profile = loadProfile(cwd);
1395
- const auth = await detectAuth();
1396
-
1397
- let guardCount = 0;
1398
- try {
1399
- const settingsFile = join(cwd, '.claude', 'settings.json');
1400
- if (existsSync(settingsFile)) {
1401
- const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
1402
- const preToolUse = settings?.hooks?.PreToolUse ?? [];
1403
- const guardCmd = 'node .claude/hooks/head-guard.mjs';
1404
- const tierCmd = 'node .claude/hooks/enforce-tier.mjs';
1405
- const hasEdit = preToolUse.some(e => e.matcher === 'Edit' && e.hooks?.some(h => h.command === guardCmd));
1406
- const hasWrite = preToolUse.some(e => e.matcher === 'Write' && e.hooks?.some(h => h.command === guardCmd));
1407
- const hasBash = preToolUse.some(e => e.matcher === 'Bash' && e.hooks?.some(h => h.command === guardCmd));
1408
- const hasAgent = preToolUse.some(e => e.matcher === 'Agent' && e.hooks?.some(h => h.command === tierCmd));
1409
- guardCount = [hasEdit, hasWrite, hasBash, hasAgent].filter(Boolean).length;
1410
- }
1411
- } catch { /* ignore */ }
1412
-
1413
- const modeLabel = (m) => m === profile.mode ? `${m} (active)` : m;
1414
-
1415
- const claudeSub = profile?.providers?.claude;
1416
- const openaiSub = profile?.providers?.openai;
1417
- const claudePlanLabel = claudeSub?.enabled
1418
- ? (CLAUDE_PLAN_LABELS[claudeSub.plan] ?? claudeSub.plan ?? 'n/a')
1419
- : 'disabled';
1420
- const openaiPlanLabel = openaiSub?.enabled
1421
- ? (OPENAI_PLAN_LABELS[openaiSub.plan] ?? openaiSub.plan ?? 'n/a')
1422
- : 'disabled';
1423
1284
 
1424
- const settingsLines = [
1425
- `Mode:`,
1426
- ` [1] ${modeLabel('cost-saver')}`,
1427
- ` [2] ${modeLabel('balanced')}`,
1428
- ` [3] ${modeLabel('quality-first')}`,
1429
- '',
1430
- `Subscriptions:`,
1431
- ` Claude: ${auth.claude.found ? 'logged in' : 'not logged in'} — ${claudePlanLabel}${claudeSub?.label ? ` [${claudeSub.label}]` : ''}`,
1432
- ` OpenAI: ${auth.openai.found ? 'logged in' : 'not logged in'} — ${openaiPlanLabel}${openaiSub?.label ? ` [${openaiSub.label}]` : ''}`,
1433
- '',
1434
- `Enforcement: ${guardCount}/4 guards active`,
1285
+ // Box layout matching dashboard
1286
+ const termW = process.stdout.columns || 60;
1287
+ const boxW = Math.min(termW - 2, 60);
1288
+ const W = boxW - 4;
1289
+
1290
+ const top = `┌${''.repeat(boxW - 2)}┐`;
1291
+ const sep = `├${'─'.repeat(boxW - 2)}┤`;
1292
+ const bot = `└${''.repeat(boxW - 2)}┘`;
1293
+ const row = (content) => makeBoxRow(content, W);
1294
+
1295
+ const lines = [
1296
+ top,
1297
+ row('Settings'),
1298
+ sep,
1299
+ row('[m] Manage subscriptions'),
1300
+ row('[e] Manage sessions'),
1301
+ row('[i] Import from replit-tools'),
1302
+ row('[d] Switch to data-tools'),
1303
+ row('[?] Help & shortcuts'),
1304
+ row('[x] Diagnostics'),
1305
+ row(''),
1306
+ row('[Esc/b] Back to dashboard'),
1307
+ bot,
1435
1308
  ];
1309
+ process.stdout.write('\n' + lines.join('\n') + '\n\n');
1436
1310
 
1437
- console.log('');
1438
- console.log(box('Settings', settingsLines));
1439
- console.log('');
1440
- console.log(menu([
1441
- { key: '1', label: 'Switch to cost-saver', section: 'Mode' },
1442
- { key: '2', label: 'Switch to balanced', section: 'Mode' },
1443
- { key: '3', label: 'Switch to quality-first', section: 'Mode' },
1444
- { key: 'a', label: 'Manage subscriptions', section: 'Subscriptions' },
1445
- { key: 'i', label: 'Reinstall hooks', section: 'Enforcement' },
1446
- { key: 'b', label: 'Back', section: '' },
1447
- ]));
1448
- console.log('');
1311
+ const raw = (await ask(' Choice: ')).trim();
1312
+ const choice = raw.toLowerCase();
1449
1313
 
1450
- const choice = (await ask(' Choice: ')).trim().toLowerCase();
1314
+ if (choice === 'm') { return { next: 'subscriptions' }; }
1451
1315
 
1452
- if (choice === '1' || choice === '2' || choice === '3') {
1453
- const modeMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
1454
- profile.mode = modeMap[choice];
1455
- saveProfile(profile, { cwd });
1456
- console.log(` Mode set to: ${profile.mode}`);
1316
+ if (choice === 'e') { return { next: 'sessions' }; }
1317
+
1318
+ if (choice === 'i') {
1319
+ const sessions = importReplitSessions(cwd);
1320
+ if (sessions.length === 0) {
1321
+ process.stdout.write('\n No replit-tools sessions found to import.\n\n');
1322
+ } else {
1323
+ process.stdout.write(`\n Found ${sessions.length} sessions from replit-tools.\n`);
1324
+ process.stdout.write(' Sessions are automatically available in the Recent list.\n\n');
1325
+ }
1326
+ await ask(' Press Enter to continue...');
1457
1327
  return { next: 'settings' };
1458
1328
  }
1459
1329
 
1460
- if (choice === 'a') {
1461
- return { next: 'subscriptions' };
1330
+ if (choice === 'd') {
1331
+ const { spawnSync } = await import('node:child_process');
1332
+ const which = spawnSync('which', ['claude-menu'], { encoding: 'utf8' });
1333
+ if (which.status === 0) {
1334
+ spawnSync('claude-menu', { stdio: 'inherit' });
1335
+ } else {
1336
+ process.stdout.write('\n data-tools not found — install with: npm i -g replit-tools\n\n');
1337
+ await ask(' Press Enter to continue...');
1338
+ }
1339
+ return { next: 'settings' };
1462
1340
  }
1463
1341
 
1464
- if (choice === 'i') {
1465
- await cmdInstall();
1342
+ if (choice === '?') {
1343
+ const W2 = 37;
1344
+ const helpTop = ` ┌${'─'.repeat(W2)}┐`;
1345
+ const helpSep = ` ├${'─'.repeat(W2)}┤`;
1346
+ const helpBottom = ` └${'─'.repeat(W2)}┘`;
1347
+ const helpPad = (s) => s + ' '.repeat(Math.max(0, W2 - s.length));
1348
+ process.stdout.write('\n');
1349
+ process.stdout.write(helpTop + '\n');
1350
+ process.stdout.write(` │ ${helpPad('At ~/workspace$ prompt:')}│\n`);
1351
+ process.stdout.write(` │ ${helpPad('db = show this menu')}│\n`);
1352
+ process.stdout.write(` │ ${helpPad('j = login to claude')}│\n`);
1353
+ process.stdout.write(` │ ${helpPad('k = login to codex')}│\n`);
1354
+ process.stdout.write(helpSep + '\n');
1355
+ process.stdout.write(` │ ${helpPad('In Claude:')}│\n`);
1356
+ process.stdout.write(` │ ${helpPad('Ctrl+C x2 = back to menu')}│\n`);
1357
+ process.stdout.write(` │ ${helpPad('Ctrl+C x3 = exit to shell')}│\n`);
1358
+ process.stdout.write(helpBottom + '\n\n');
1359
+ await ask(' Press Enter to continue...');
1466
1360
  return { next: 'settings' };
1467
1361
  }
1468
1362
 
1469
- if (choice === 'b' || choice === 'back') { return { next: 'main' }; }
1363
+ if (choice === 'x') { return { next: 'diagnostics' }; }
1364
+
1365
+ if (choice === 'b' || choice === 'back' || raw === '\x1b') { return { next: 'main' }; }
1470
1366
 
1471
- return { next: 'settings' };
1367
+ return { next: 'main' };
1472
1368
  }
1473
1369
 
1474
1370
  // ─── Helper: aggregatePlans ───────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {