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.
- package/bin/dual-brain.mjs +217 -321
- package/package.json +1 -1
package/bin/dual-brain.mjs
CHANGED
|
@@ -998,112 +998,94 @@ function loadTerminalState(cwd, terminalId) {
|
|
|
998
998
|
} catch { return null; }
|
|
999
999
|
}
|
|
1000
1000
|
|
|
1001
|
-
// ───
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
1018
|
+
const expired = sub?.expiresAt && Date.parse(sub.expiresAt) < now;
|
|
1019
|
+
if (expired) return `${RED} ${displayName}: expired`;
|
|
1021
1020
|
|
|
1022
|
-
|
|
1023
|
-
|
|
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 >
|
|
1028
|
-
const
|
|
1029
|
-
return
|
|
1024
|
+
if (subs && subs.length > 0) {
|
|
1025
|
+
const agg = aggregatePlans(subs);
|
|
1026
|
+
return `${dot} ${displayName} ${agg}`;
|
|
1030
1027
|
}
|
|
1031
|
-
|
|
1032
|
-
const
|
|
1033
|
-
return
|
|
1028
|
+
// Single plan
|
|
1029
|
+
const planPrice = PLAN_PRICES[sub?.plan] || sub?.plan || 'connected';
|
|
1030
|
+
return `${dot} ${displayName} ${planPrice}`;
|
|
1034
1031
|
}
|
|
1035
1032
|
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1033
|
+
const parts = [];
|
|
1034
|
+
parts.push(providerSegment('claude', 'Claude'));
|
|
1035
|
+
parts.push(providerSegment('openai', 'OpenAI'));
|
|
1036
|
+
return parts.join(' ');
|
|
1037
|
+
}
|
|
1040
1038
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1071
|
+
// Append-only session archive sync
|
|
1074
1072
|
try {
|
|
1075
1073
|
const { syncSessionMirror } = await import('../src/session.mjs');
|
|
1076
|
-
|
|
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
|
-
|
|
1096
|
+
// Gather recent sessions
|
|
1097
|
+
const recentSessions = enrichSessions(importReplitSessions(cwd), cwd).slice(0, 3);
|
|
1115
1098
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
-
|
|
1135
|
-
const
|
|
1136
|
-
const
|
|
1137
|
-
const
|
|
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
|
|
1149
|
-
const
|
|
1150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1114
|
+
// ── Header: one line above the box ────────────────────────────────────────
|
|
1115
|
+
process.stdout.write(`\n🧠 dual-brain v${version}\n`);
|
|
1178
1116
|
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
1120
|
+
const statusRows = [row(providerLine)];
|
|
1121
|
+
if (dtVersion) {
|
|
1122
|
+
statusRows.push(row(`\x1b[2m📦 data-tools v${dtVersion}\x1b[0m`));
|
|
1123
|
+
}
|
|
1202
1124
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const
|
|
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
|
-
|
|
1209
|
-
|
|
1152
|
+
// ── Actions bar ───────────────────────────────────────────────────────────
|
|
1153
|
+
const actionsContent = '↵ Resume n New / Search s Settings q Quit';
|
|
1154
|
+
const actionsRow = row(actionsContent);
|
|
1210
1155
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1246
|
-
if (ctx.
|
|
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
|
-
|
|
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 === '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1306
|
-
const date
|
|
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
|
-
|
|
1309
|
-
if (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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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
|
-
|
|
1438
|
-
|
|
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
|
-
|
|
1314
|
+
if (choice === 'm') { return { next: 'subscriptions' }; }
|
|
1451
1315
|
|
|
1452
|
-
if (choice === '
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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 === '
|
|
1461
|
-
|
|
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 === '
|
|
1465
|
-
|
|
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 === '
|
|
1363
|
+
if (choice === 'x') { return { next: 'diagnostics' }; }
|
|
1364
|
+
|
|
1365
|
+
if (choice === 'b' || choice === 'back' || raw === '\x1b') { return { next: 'main' }; }
|
|
1470
1366
|
|
|
1471
|
-
return { next: '
|
|
1367
|
+
return { next: 'main' };
|
|
1472
1368
|
}
|
|
1473
1369
|
|
|
1474
1370
|
// ─── Helper: aggregatePlans ───────────────────────────────────────────────────
|