dual-brain 0.1.2 → 0.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 +163 -5
- package/package.json +1 -1
- package/src/session.mjs +181 -9
package/bin/dual-brain.mjs
CHANGED
|
@@ -1113,6 +1113,22 @@ async function mainScreen(rl, ask) {
|
|
|
1113
1113
|
|
|
1114
1114
|
// ── Header: one line above the box ────────────────────────────────────────
|
|
1115
1115
|
process.stdout.write(`\n🧠 dual-brain v${version}\n`);
|
|
1116
|
+
{
|
|
1117
|
+
let gitName = '';
|
|
1118
|
+
try {
|
|
1119
|
+
const { execSync } = await import('node:child_process');
|
|
1120
|
+
gitName = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
1121
|
+
} catch { /* ignore */ }
|
|
1122
|
+
if (gitName) {
|
|
1123
|
+
const hour = new Date().getHours();
|
|
1124
|
+
let greet;
|
|
1125
|
+
if (hour >= 5 && hour <= 11) greet = 'Good morning';
|
|
1126
|
+
else if (hour >= 12 && hour <= 16) greet = 'Good afternoon';
|
|
1127
|
+
else if (hour >= 17 && hour <= 21) greet = 'Good evening';
|
|
1128
|
+
else greet = 'Late night';
|
|
1129
|
+
process.stdout.write(`\x1b[2m${greet}, ${gitName}\x1b[0m\n`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1116
1132
|
|
|
1117
1133
|
// ── Status section ────────────────────────────────────────────────────────
|
|
1118
1134
|
const providerLine = buildProviderStatusLine(profile, auth);
|
|
@@ -1164,11 +1180,126 @@ async function mainScreen(rl, ask) {
|
|
|
1164
1180
|
bot,
|
|
1165
1181
|
];
|
|
1166
1182
|
process.stdout.write(lines.join('\n') + '\n');
|
|
1167
|
-
process.stdout.write(`\x1b[
|
|
1183
|
+
process.stdout.write(`\x1b[2mPowered by data-tools · Steve Moraco\x1b[0m\n\n`);
|
|
1168
1184
|
|
|
1169
1185
|
// ── Key handling ──────────────────────────────────────────────────────────
|
|
1170
|
-
|
|
1171
|
-
|
|
1186
|
+
// Use raw keypress mode so we can show a live type-to-start buffer.
|
|
1187
|
+
// Single-key commands (n, s, q, /, 1-9, Enter) only fire when buffer is empty.
|
|
1188
|
+
let taskBuffer = '';
|
|
1189
|
+
|
|
1190
|
+
const readline = await import('node:readline');
|
|
1191
|
+
|
|
1192
|
+
// Render the type-ahead line below the box (overwrites the current cursor line)
|
|
1193
|
+
const renderBuffer = (buf) => {
|
|
1194
|
+
// Move to the prompt line (we're already at it after printing the box + footer)
|
|
1195
|
+
// Use carriage return + clear-to-end-of-line to overwrite
|
|
1196
|
+
if (buf.length === 0) {
|
|
1197
|
+
process.stdout.write('\r\x1b[K');
|
|
1198
|
+
} else {
|
|
1199
|
+
const display = buf.length > W - 4 ? buf.slice(-(W - 4)) : buf;
|
|
1200
|
+
process.stdout.write(`\r\x1b[K> ${display}\x1b[7m \x1b[0m`);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
// Enable keypress events on stdin (safe to call multiple times)
|
|
1205
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
1206
|
+
|
|
1207
|
+
const raw = await new Promise((resolve) => {
|
|
1208
|
+
// Switch to raw mode if possible (TTY only)
|
|
1209
|
+
const wasRaw = process.stdin.isRaw;
|
|
1210
|
+
const canRaw = process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
1211
|
+
if (canRaw) process.stdin.setRawMode(true);
|
|
1212
|
+
|
|
1213
|
+
const cleanup = () => {
|
|
1214
|
+
process.stdin.removeListener('keypress', onKey);
|
|
1215
|
+
if (canRaw) {
|
|
1216
|
+
try { process.stdin.setRawMode(wasRaw || false); } catch {}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
const onKey = (str, key) => {
|
|
1221
|
+
if (!key) return;
|
|
1222
|
+
|
|
1223
|
+
const name = key.name || '';
|
|
1224
|
+
const seq = key.sequence || str || '';
|
|
1225
|
+
|
|
1226
|
+
// Ctrl-C / Ctrl-D → exit
|
|
1227
|
+
if (key.ctrl && (name === 'c' || name === 'd')) {
|
|
1228
|
+
cleanup();
|
|
1229
|
+
process.stdout.write('\n');
|
|
1230
|
+
resolve('q');
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Enter key
|
|
1235
|
+
if (name === 'return' || name === 'enter' || seq === '\r' || seq === '\n') {
|
|
1236
|
+
cleanup();
|
|
1237
|
+
if (taskBuffer.length > 0) {
|
|
1238
|
+
process.stdout.write('\n');
|
|
1239
|
+
resolve(`__task__:${taskBuffer}`);
|
|
1240
|
+
} else {
|
|
1241
|
+
resolve('');
|
|
1242
|
+
}
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Escape → clear buffer
|
|
1247
|
+
if (name === 'escape') {
|
|
1248
|
+
taskBuffer = '';
|
|
1249
|
+
renderBuffer('');
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Backspace / delete
|
|
1254
|
+
if (name === 'backspace' || name === 'delete') {
|
|
1255
|
+
if (taskBuffer.length > 0) {
|
|
1256
|
+
taskBuffer = taskBuffer.slice(0, -1);
|
|
1257
|
+
renderBuffer(taskBuffer);
|
|
1258
|
+
}
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Ignore non-printable / control keys
|
|
1263
|
+
if (key.ctrl || key.meta || !str || str.length === 0) return;
|
|
1264
|
+
const code = str.codePointAt(0);
|
|
1265
|
+
if (code < 32 || code === 127) return;
|
|
1266
|
+
|
|
1267
|
+
// Single-key commands only fire when buffer is empty
|
|
1268
|
+
if (taskBuffer.length === 0) {
|
|
1269
|
+
const lower = str.toLowerCase();
|
|
1270
|
+
if (lower === 'n' || lower === 's' || lower === 'q' || lower === '/') {
|
|
1271
|
+
cleanup();
|
|
1272
|
+
process.stdout.write('\n');
|
|
1273
|
+
resolve(lower);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const digit = parseInt(str, 10);
|
|
1277
|
+
if (!isNaN(digit) && digit >= 1 && digit <= 9) {
|
|
1278
|
+
cleanup();
|
|
1279
|
+
process.stdout.write('\n');
|
|
1280
|
+
resolve(str);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Accumulate into buffer
|
|
1286
|
+
taskBuffer += str;
|
|
1287
|
+
renderBuffer(taskBuffer);
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
process.stdin.on('keypress', onKey);
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
const choice = typeof raw === 'string' ? raw.toLowerCase() : '';
|
|
1294
|
+
|
|
1295
|
+
// Typed task → dispatch as "dual-brain go"
|
|
1296
|
+
if (raw.startsWith('__task__:')) {
|
|
1297
|
+
const prompt = raw.slice('__task__:'.length).trim();
|
|
1298
|
+
if (prompt) {
|
|
1299
|
+
return { next: 'go', prompt };
|
|
1300
|
+
}
|
|
1301
|
+
return { next: 'main' };
|
|
1302
|
+
}
|
|
1172
1303
|
|
|
1173
1304
|
// Enter (empty) → resume most recent session
|
|
1174
1305
|
if (raw === '' || choice === '\r') {
|
|
@@ -2455,13 +2586,40 @@ async function runScreens(startScreen = 'dashboard') {
|
|
|
2455
2586
|
let current = startScreen;
|
|
2456
2587
|
let ctx = {};
|
|
2457
2588
|
while (current && current !== 'exit') {
|
|
2589
|
+
// Handle type-to-start dispatch from mainScreen
|
|
2590
|
+
if (current === 'go' && ctx.prompt) {
|
|
2591
|
+
const prompt = ctx.prompt;
|
|
2592
|
+
const cwd = process.cwd();
|
|
2593
|
+
const profile = loadProfile(cwd);
|
|
2594
|
+
const detection = detectTask({ prompt });
|
|
2595
|
+
const decision = decideRoute({ profile, detection, cwd });
|
|
2596
|
+
process.stdout.write(`\n Routing: ${decision.provider}/${decision.model} (${decision.tier})\n`);
|
|
2597
|
+
process.stdout.write(` Reason: ${decision.explanation}\n\n`);
|
|
2598
|
+
const { spawnSync } = await import('node:child_process');
|
|
2599
|
+
const launchTool = decision.provider === 'openai' ? 'codex' : 'claude';
|
|
2600
|
+
if (launchTool === 'codex') {
|
|
2601
|
+
spawnSync('codex', [prompt], { stdio: 'inherit' });
|
|
2602
|
+
} else {
|
|
2603
|
+
spawnSync('claude', ['-p', prompt], { stdio: 'inherit' });
|
|
2604
|
+
}
|
|
2605
|
+
const freshSessions = importReplitSessions(cwd);
|
|
2606
|
+
if (freshSessions.length > 0) {
|
|
2607
|
+
saveTerminalState(cwd, getTerminalId(), freshSessions[0].id, launchTool);
|
|
2608
|
+
}
|
|
2609
|
+
current = 'main';
|
|
2610
|
+
ctx = {};
|
|
2611
|
+
continue;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2458
2614
|
const screen = SCREENS[current];
|
|
2459
2615
|
if (!screen) break;
|
|
2460
2616
|
try {
|
|
2461
2617
|
const result = await screen(rl, ask, ctx);
|
|
2462
2618
|
current = result?.next || 'exit';
|
|
2463
|
-
// Pass through context (e.g. selected session) to next screen
|
|
2464
|
-
ctx = result?.session ? { session: result.session }
|
|
2619
|
+
// Pass through context (e.g. selected session, typed prompt) to next screen
|
|
2620
|
+
ctx = result?.session ? { session: result.session }
|
|
2621
|
+
: result?.prompt ? { prompt: result.prompt }
|
|
2622
|
+
: {};
|
|
2465
2623
|
} catch (e) {
|
|
2466
2624
|
console.error(`Error: ${e.message}`);
|
|
2467
2625
|
current = 'main';
|
package/package.json
CHANGED
package/src/session.mjs
CHANGED
|
@@ -461,23 +461,38 @@ export function importReplitSessions(cwd = process.cwd()) {
|
|
|
461
461
|
const windowMs = windowHours * 60 * 60 * 1000;
|
|
462
462
|
const cutoff = Date.now() - windowMs;
|
|
463
463
|
|
|
464
|
+
// Load existing session index for smartName lookup (best-effort, non-fatal)
|
|
465
|
+
let sessionIndex = {};
|
|
466
|
+
try {
|
|
467
|
+
const indexPath = join(cwd, '.dualbrain', 'session-index.json');
|
|
468
|
+
if (existsSync(indexPath)) {
|
|
469
|
+
sessionIndex = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
470
|
+
}
|
|
471
|
+
} catch { /* non-fatal */ }
|
|
472
|
+
|
|
464
473
|
// Build session list
|
|
465
474
|
for (const [id, sess] of bySession) {
|
|
466
475
|
// Skip sessions outside the recency window (timestamps are in ms)
|
|
467
476
|
if (sess.lastTimestamp < cutoff) continue;
|
|
468
|
-
|
|
469
|
-
|
|
477
|
+
|
|
478
|
+
// Use smartName from index if available, otherwise fall back to first prompt
|
|
479
|
+
let name = sessionIndex[id]?.smartName || null;
|
|
480
|
+
|
|
470
481
|
if (!name) {
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
name
|
|
482
|
+
// Classic fallback: first meaningful prompt
|
|
483
|
+
name = sess.firstPrompt;
|
|
484
|
+
if (!name) {
|
|
485
|
+
const firstReal = sess.entries.find(e => e.display && e.display !== 'login');
|
|
486
|
+
name = firstReal?.display || `Session ${id.slice(0, 8)}`;
|
|
487
|
+
}
|
|
488
|
+
// Truncate long names that came from raw prompts
|
|
489
|
+
if (name.length > 60) name = name.slice(0, 57) + '...';
|
|
474
490
|
}
|
|
475
|
-
// Truncate long names
|
|
476
|
-
if (name.length > 60) name = name.slice(0, 57) + '...';
|
|
477
491
|
|
|
478
492
|
sessions.push({
|
|
479
493
|
id: sess.sessionId,
|
|
480
494
|
name,
|
|
495
|
+
smartName: sessionIndex[id]?.smartName || null,
|
|
481
496
|
project: sess.project,
|
|
482
497
|
promptCount: sess.entries.length,
|
|
483
498
|
lastActive: new Date(sess.lastTimestamp).toISOString(),
|
|
@@ -741,6 +756,159 @@ export function syncSessionMirror(cwd = process.cwd()) {
|
|
|
741
756
|
return { copied: totalCopied, grew: totalGrew };
|
|
742
757
|
}
|
|
743
758
|
|
|
759
|
+
// ─── Smart session naming ─────────────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* File pattern → human label mapping (checked in order, first match wins).
|
|
763
|
+
* Each entry: { pattern: RegExp, label: string, action?: string }
|
|
764
|
+
*/
|
|
765
|
+
const FILE_PATTERN_RULES = [
|
|
766
|
+
{ pattern: /auth/i, label: 'Auth', action: 'Refactor' },
|
|
767
|
+
{ pattern: /test|spec/i, label: 'Tests', action: 'Fix' },
|
|
768
|
+
{ pattern: /dispatch/i, label: 'Dispatch', action: 'Update' },
|
|
769
|
+
{ pattern: /session/i, label: 'Session', action: 'Update' },
|
|
770
|
+
{ pattern: /profile/i, label: 'Profile', action: 'Update' },
|
|
771
|
+
{ pattern: /detect/i, label: 'Detection', action: 'Update' },
|
|
772
|
+
{ pattern: /decide/i, label: 'Routing', action: 'Update' },
|
|
773
|
+
{ pattern: /budget/i, label: 'Budget', action: 'Update' },
|
|
774
|
+
{ pattern: /hook/i, label: 'Hooks', action: 'Update' },
|
|
775
|
+
{ pattern: /install/i, label: 'Install', action: 'Update' },
|
|
776
|
+
{ pattern: /config/i, label: 'Config', action: 'Update' },
|
|
777
|
+
{ pattern: /migrate/i, label: 'Migration', action: 'Add' },
|
|
778
|
+
];
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Topic words that suggest a dominant action verb.
|
|
782
|
+
*/
|
|
783
|
+
const TOPIC_ACTION_MAP = [
|
|
784
|
+
{ words: ['fix', 'bug', 'error', 'crash', 'broken', 'fail'], action: 'Fix' },
|
|
785
|
+
{ words: ['refactor', 'cleanup', 'clean', 'reorganize'], action: 'Refactor' },
|
|
786
|
+
{ words: ['add', 'implement', 'create', 'build', 'write'], action: 'Add' },
|
|
787
|
+
{ words: ['update', 'upgrade', 'bump', 'patch'], action: 'Update' },
|
|
788
|
+
{ words: ['test', 'spec', 'coverage'], action: 'Fix' },
|
|
789
|
+
{ words: ['deploy', 'release', 'publish'], action: 'Deploy' },
|
|
790
|
+
{ words: ['audit', 'review', 'check'], action: 'Review' },
|
|
791
|
+
];
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Convert a string to Title Case.
|
|
795
|
+
* @param {string} str
|
|
796
|
+
* @returns {string}
|
|
797
|
+
*/
|
|
798
|
+
function toTitleCase(str) {
|
|
799
|
+
return str.replace(/\b\w/g, c => c.toUpperCase());
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Strip file extensions from a name candidate.
|
|
804
|
+
* @param {string} name
|
|
805
|
+
* @returns {string}
|
|
806
|
+
*/
|
|
807
|
+
function stripExtensions(name) {
|
|
808
|
+
return name.replace(/\.(mjs|js|ts|tsx|jsx|json|md|css|html|py|sh|sql|toml|yaml|yml)\b/gi, '');
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Truncate a string to maxLen characters, preserving whole words where possible.
|
|
813
|
+
* @param {string} str
|
|
814
|
+
* @param {number} maxLen
|
|
815
|
+
* @returns {string}
|
|
816
|
+
*/
|
|
817
|
+
function truncate(str, maxLen = 40) {
|
|
818
|
+
if (str.length <= maxLen) return str;
|
|
819
|
+
const cut = str.slice(0, maxLen).replace(/\s+\S*$/, '');
|
|
820
|
+
return cut || str.slice(0, maxLen);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Generate a smart human-readable session name from session index data.
|
|
825
|
+
*
|
|
826
|
+
* Priority:
|
|
827
|
+
* 1. Dominant file pattern (e.g. auth*.mjs → "Refactor Auth Module")
|
|
828
|
+
* 2. Top topics (e.g. ['auth','token','refresh'] → "Auth Token Refresh")
|
|
829
|
+
* 3. Fallback: first prompt truncated to 40 chars
|
|
830
|
+
*
|
|
831
|
+
* Rules: ≤40 chars, Title Case, no file extensions, action-prefixed when detectable.
|
|
832
|
+
*
|
|
833
|
+
* @param {{ topics?: string[], files?: string[], prompts?: { first?: string } }} sessionData
|
|
834
|
+
* @returns {string}
|
|
835
|
+
*/
|
|
836
|
+
export function generateSmartName(sessionData) {
|
|
837
|
+
const topics = sessionData.topics || [];
|
|
838
|
+
const files = sessionData.files || [];
|
|
839
|
+
const firstPrompt = sessionData.prompts?.first || '';
|
|
840
|
+
|
|
841
|
+
// ── Step 1: Detect dominant action from topics ─────────────────────────────
|
|
842
|
+
let detectedAction = null;
|
|
843
|
+
for (const { words, action } of TOPIC_ACTION_MAP) {
|
|
844
|
+
if (topics.some(t => words.includes(t))) {
|
|
845
|
+
detectedAction = action;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ── Step 2: Try file pattern match ─────────────────────────────────────────
|
|
851
|
+
if (files.length > 0) {
|
|
852
|
+
// Flatten all filenames for pattern matching
|
|
853
|
+
const fileNames = files.map(f => f.split('/').pop()).join(' ');
|
|
854
|
+
|
|
855
|
+
for (const { pattern, label, action } of FILE_PATTERN_RULES) {
|
|
856
|
+
if (pattern.test(fileNames)) {
|
|
857
|
+
const actionWord = detectedAction || action || 'Update';
|
|
858
|
+
const candidate = `${actionWord} ${label}`;
|
|
859
|
+
return truncate(toTitleCase(candidate));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// No named pattern — derive a label from the most common directory or base name
|
|
864
|
+
const basenames = files.map(f => {
|
|
865
|
+
const base = f.split('/').pop() || f;
|
|
866
|
+
// Strip extension and convert camelCase/kebab to words
|
|
867
|
+
return stripExtensions(base)
|
|
868
|
+
.replace(/[-_]/g, ' ')
|
|
869
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
870
|
+
.trim();
|
|
871
|
+
}).filter(Boolean);
|
|
872
|
+
|
|
873
|
+
if (basenames.length > 0) {
|
|
874
|
+
// Use the most common prefix or first significant basename
|
|
875
|
+
const label = basenames[0];
|
|
876
|
+
const actionWord = detectedAction || 'Update';
|
|
877
|
+
const candidate = `${actionWord} ${label}`;
|
|
878
|
+
return truncate(toTitleCase(stripExtensions(candidate)));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ── Step 3: Try top topics ─────────────────────────────────────────────────
|
|
883
|
+
if (topics.length >= 2) {
|
|
884
|
+
// Take top 3 topics and compose a name
|
|
885
|
+
const topTopics = topics.slice(0, 3);
|
|
886
|
+
const actionWord = detectedAction || null;
|
|
887
|
+
|
|
888
|
+
let candidate;
|
|
889
|
+
if (actionWord) {
|
|
890
|
+
// Use action + remaining topics
|
|
891
|
+
candidate = [actionWord, ...topTopics.filter(t => t !== actionWord.toLowerCase())].slice(0, 3).join(' ');
|
|
892
|
+
} else {
|
|
893
|
+
candidate = topTopics.join(' ');
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return truncate(toTitleCase(candidate));
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (topics.length === 1) {
|
|
900
|
+
const actionWord = detectedAction || 'Work on';
|
|
901
|
+
return truncate(toTitleCase(`${actionWord} ${topics[0]}`));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ── Step 4: Fallback — first prompt truncated ──────────────────────────────
|
|
905
|
+
if (firstPrompt) {
|
|
906
|
+
return truncate(firstPrompt);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return 'Session';
|
|
910
|
+
}
|
|
911
|
+
|
|
744
912
|
// ─── Session index ────────────────────────────────────────────────────────────
|
|
745
913
|
|
|
746
914
|
/**
|
|
@@ -841,7 +1009,7 @@ export function buildSessionIndex(cwd = process.cwd()) {
|
|
|
841
1009
|
.slice(0, 10)
|
|
842
1010
|
.map(([w]) => w);
|
|
843
1011
|
|
|
844
|
-
|
|
1012
|
+
const sessionEntry = {
|
|
845
1013
|
id: sessionId,
|
|
846
1014
|
topics,
|
|
847
1015
|
files: [...fileSet].slice(0, 20),
|
|
@@ -851,6 +1019,8 @@ export function buildSessionIndex(cwd = process.cwd()) {
|
|
|
851
1019
|
tool: 'claude',
|
|
852
1020
|
_fileSize: fileSize,
|
|
853
1021
|
};
|
|
1022
|
+
sessionEntry.smartName = generateSmartName(sessionEntry);
|
|
1023
|
+
index[sessionId] = sessionEntry;
|
|
854
1024
|
} catch { continue; }
|
|
855
1025
|
}
|
|
856
1026
|
}
|
|
@@ -904,12 +1074,14 @@ export function buildSessionIndex(cwd = process.cwd()) {
|
|
|
904
1074
|
} catch { continue; }
|
|
905
1075
|
}
|
|
906
1076
|
|
|
907
|
-
|
|
1077
|
+
const codexEntry = {
|
|
908
1078
|
id, topics: [], files: [],
|
|
909
1079
|
prompts: { first: firstPrompt || '', last: lastPrompt || '' },
|
|
910
1080
|
date: lastTimestamp ? new Date(lastTimestamp * 1000).toISOString() : null,
|
|
911
1081
|
messageCount, tool: 'codex', _fileSize: fileSize,
|
|
912
1082
|
};
|
|
1083
|
+
codexEntry.smartName = generateSmartName(codexEntry);
|
|
1084
|
+
index[id] = codexEntry;
|
|
913
1085
|
} catch { continue; }
|
|
914
1086
|
}
|
|
915
1087
|
}
|