claude-simple 0.2.9 → 0.2.11

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.
@@ -757,8 +757,18 @@ async function loadGrokContext(sessionDir) {
757
757
  }
758
758
 
759
759
  /** Public helper: load usage/context for any agent session. */
760
- export async function loadSessionUsage(sessionId, agent, { claudeConfigDirs, codexHome, grokHome }) {
760
+ export async function loadSessionUsage(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }) {
761
761
  const a = (agent || 'claude').toLowerCase();
762
+ if (a === 'cursor') {
763
+ const sessions = await scanCursorSessions(cursorHome);
764
+ if (!sessions.some(s => s.sessionId === sessionId)) return null;
765
+ return { agent: 'cursor', sessionId, context: await loadCursorContext(sessionId, cursorHome) };
766
+ }
767
+ if (a === 'gemini') {
768
+ const sessions = await scanGeminiSessions(geminiHome);
769
+ if (!sessions.some(s => s.sessionId === sessionId)) return null;
770
+ return { agent: 'gemini', sessionId, context: await loadGeminiContext(sessionId, geminiHome) };
771
+ }
762
772
  if (a === 'grok') {
763
773
  const dir = await findGrokSessionDir(sessionId, grokHome);
764
774
  if (!dir) return null;
@@ -890,8 +900,242 @@ export async function loadGrokSessionBundle(sessionDir) {
890
900
  return { messages, context };
891
901
  }
892
902
 
903
+ // ─── Gemini / agy ────────────────────────────────────────────────────────────
904
+
905
+ function geminiUiSessionsRoot(geminiHome) {
906
+ return path.join(geminiHome || path.join(os.homedir(), '.gemini'), 'claude-simple-ui', 'sessions');
907
+ }
908
+
909
+ function geminiJournalPath(sessionId, geminiHome) {
910
+ return path.join(geminiUiSessionsRoot(geminiHome), `${sessionId}.jsonl`);
911
+ }
912
+
913
+ // agy keeps the actual native transcript here. This is plain JSONL (unlike
914
+ // its conversation SQLite database) and is available for conversations made
915
+ // in the Antigravity app as well as print-mode conversations created by us.
916
+ function geminiNativeTranscriptPath(sessionId, geminiHome) {
917
+ const home = geminiHome || path.join(os.homedir(), '.gemini');
918
+ return path.join(home, 'antigravity-cli', 'brain', sessionId, '.system_generated', 'logs', 'transcript.jsonl');
919
+ }
920
+
921
+ function geminiPathFromUri(uri) {
922
+ if (!uri || typeof uri !== 'string') return null;
923
+ try { return decodeURIComponent(uri.replace(/^file:\/\//, '')); } catch { return null; }
924
+ }
925
+
926
+ async function readGeminiJournalMeta(filePath) {
927
+ let meta = null;
928
+ let firstUser = null;
929
+ let turnCount = 0;
930
+ let totalTokens = 0;
931
+ try {
932
+ const stream = fsSync.createReadStream(filePath);
933
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
934
+ for await (const line of rl) {
935
+ let d; try { d = JSON.parse(line); } catch { continue; }
936
+ if (d.type === 'session_meta') meta = { ...(meta || {}), ...d };
937
+ if (d.type === 'user' && d.content?.trim()) {
938
+ turnCount++;
939
+ if (!firstUser) firstUser = d.content.trim();
940
+ }
941
+ if (d.type === 'result' && d.usage) totalTokens += Number(d.usage.total_tokens) || 0;
942
+ }
943
+ } catch {}
944
+ return { meta, firstUser, turnCount, totalTokens };
945
+ }
946
+
947
+ /** agy keeps public conversation metadata plus a UI-owned readable journal. */
948
+ export async function scanGeminiSessions(geminiHome, filterCwd = null) {
949
+ const home = geminiHome || path.join(os.homedir(), '.gemini');
950
+ const records = new Map();
951
+ const metadataPath = path.join(home, 'antigravity-cli', 'cache', 'conversation_metadata.json');
952
+ try {
953
+ const raw = JSON.parse(await fs.readFile(metadataPath, 'utf8'));
954
+ for (const [sessionId, item] of Object.entries(raw?.conversations || {})) {
955
+ const summary = item?.summary || {};
956
+ const cwd = geminiPathFromUri(summary.WorkspaceURIs?.[0]) || null;
957
+ records.set(sessionId, {
958
+ sessionId, agent: 'gemini', cwd, configDir: home,
959
+ display: summary.Title || summary.Preview || sessionId.slice(0, 8),
960
+ updatedAt: Date.parse(item?.last_modified_time || summary.UpdatedAt || '') || 0,
961
+ turnCount: Number(summary.NumSteps) || 0,
962
+ hasMemory: false,
963
+ });
964
+ }
965
+ } catch {}
966
+ // history.jsonl catches fresh print-mode conversations before metadata cache
967
+ // catches up, and provides a useful title for older conversations.
968
+ try {
969
+ const stream = fsSync.createReadStream(path.join(home, 'antigravity-cli', 'history.jsonl'));
970
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
971
+ for await (const line of rl) {
972
+ let d; try { d = JSON.parse(line); } catch { continue; }
973
+ const sessionId = d.conversationId;
974
+ if (!sessionId) continue;
975
+ const existing = records.get(sessionId) || {
976
+ sessionId, agent: 'gemini', cwd: d.workspace || null, configDir: home,
977
+ display: sessionId.slice(0, 8), updatedAt: 0, turnCount: 0, hasMemory: false,
978
+ };
979
+ existing.cwd ||= d.workspace || null;
980
+ existing.display = existing.display === sessionId.slice(0, 8) ? (d.display || existing.display) : existing.display;
981
+ existing.updatedAt = Math.max(existing.updatedAt || 0, Number(d.timestamp) || 0);
982
+ if (d.type !== 'slash_command') existing.turnCount = Math.max(existing.turnCount || 0, 1);
983
+ records.set(sessionId, existing);
984
+ }
985
+ } catch {}
986
+ try {
987
+ const entries = await fs.readdir(geminiUiSessionsRoot(home), { withFileTypes: true });
988
+ await Promise.all(entries.filter(e => e.isFile() && e.name.endsWith('.jsonl')).map(async e => {
989
+ const filePath = path.join(geminiUiSessionsRoot(home), e.name);
990
+ const sessionId = e.name.slice(0, -6);
991
+ const [stat, parsed] = await Promise.all([fs.stat(filePath), readGeminiJournalMeta(filePath)]);
992
+ const existing = records.get(sessionId) || { sessionId, agent: 'gemini', configDir: home, hasMemory: false };
993
+ existing.cwd = parsed.meta?.cwd || existing.cwd || null;
994
+ existing.display = parsed.firstUser || existing.display || sessionId.slice(0, 8);
995
+ existing.updatedAt = Math.max(existing.updatedAt || 0, stat.mtimeMs);
996
+ existing.turnCount = parsed.turnCount || existing.turnCount || 0;
997
+ existing.totalTokens = parsed.totalTokens || existing.totalTokens || 0;
998
+ existing.journalPath = filePath;
999
+ records.set(sessionId, existing);
1000
+ }));
1001
+ } catch {}
1002
+ return [...records.values()].filter(s => !filterCwd || s.cwd === filterCwd);
1003
+ }
1004
+
1005
+ export async function parseGeminiMessages(filePath) {
1006
+ const messages = [];
1007
+ let pendingAssistant = null;
1008
+ const flushAssistant = () => {
1009
+ if (pendingAssistant?.content) messages.push(pendingAssistant);
1010
+ pendingAssistant = null;
1011
+ };
1012
+ try {
1013
+ const stream = fsSync.createReadStream(filePath);
1014
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
1015
+ for await (const line of rl) {
1016
+ let d; try { d = JSON.parse(line); } catch { continue; }
1017
+ if (d.type === 'user' && d.content?.trim()) {
1018
+ flushAssistant();
1019
+ messages.push({ role: 'user', type: 'text', content: d.content.trim(), ts: d.ts });
1020
+ } else if (d.type === 'assistant_delta' && d.data) {
1021
+ if (!pendingAssistant) pendingAssistant = { role: 'assistant', type: 'text', content: '', ts: d.ts };
1022
+ pendingAssistant.content += d.data;
1023
+ } else if (d.type === 'tool') {
1024
+ flushAssistant();
1025
+ messages.push({ role: 'tool', type: 'tool_use', name: d.name || 'tool', input: d.input || {}, ts: d.ts });
1026
+ } else if (d.type === 'result' && d.usage) {
1027
+ flushAssistant();
1028
+ messages.push({ type: 'token_usage', usage: d.usage, ts: d.ts });
1029
+ }
1030
+ }
1031
+ } catch {}
1032
+ flushAssistant();
1033
+ return messages;
1034
+ }
1035
+
1036
+ async function findGeminiJournal(sessionId, geminiHome) {
1037
+ const file = geminiJournalPath(sessionId, geminiHome);
1038
+ try { await fs.access(file); return file; } catch { return null; }
1039
+ }
1040
+
1041
+ async function findGeminiNativeTranscript(sessionId, geminiHome) {
1042
+ const file = geminiNativeTranscriptPath(sessionId, geminiHome);
1043
+ try { await fs.access(file); return file; } catch { return null; }
1044
+ }
1045
+
1046
+ function geminiUserText(content) {
1047
+ const text = String(content || '').trim();
1048
+ // Native rows wrap the visible user request in transport metadata. Keep
1049
+ // the chat transcript readable while retaining a safe fallback for old rows.
1050
+ const request = text.match(/<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/i);
1051
+ return (request?.[1] || text).trim();
1052
+ }
1053
+
1054
+ /** Parse agy's native brain transcript into the common UI history shape. */
1055
+ export async function parseGeminiNativeMessages(filePath) {
1056
+ const messages = [];
1057
+ try {
1058
+ const stream = fsSync.createReadStream(filePath);
1059
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
1060
+ for await (const line of rl) {
1061
+ let d; try { d = JSON.parse(line); } catch { continue; }
1062
+ if (d.status && d.status !== 'DONE') continue;
1063
+ const ts = Date.parse(d.created_at || '') || undefined;
1064
+ if (d.source === 'USER_EXPLICIT' && d.type === 'USER_INPUT') {
1065
+ const content = geminiUserText(d.content);
1066
+ if (content) messages.push({ role: 'user', type: 'text', content, ts });
1067
+ } else if (d.source === 'MODEL' && ['GENERIC', 'PLANNER_RESPONSE'].includes(d.type)) {
1068
+ const content = String(d.content || '').trim();
1069
+ if (content) messages.push({ role: 'assistant', type: 'text', content, ts });
1070
+ }
1071
+ }
1072
+ } catch {}
1073
+ return messages;
1074
+ }
1075
+
1076
+ async function loadGeminiContext(sessionId, geminiHome) {
1077
+ const native = await findGeminiNativeTranscript(sessionId, geminiHome);
1078
+ const file = native || await findGeminiJournal(sessionId, geminiHome);
1079
+ if (!file) return { note: 'This Gemini conversation has no readable transcript on this machine.' };
1080
+ const messages = native ? await parseGeminiNativeMessages(native) : await parseGeminiMessages(file);
1081
+ const usages = messages.filter(m => m.type === 'token_usage').map(m => m.usage || {});
1082
+ const total = { input_tokens: 0, output_tokens: 0, reasoning_tokens: 0, cache_read_input_tokens: 0, total_tokens: 0 };
1083
+ for (const u of usages) for (const key of Object.keys(total)) total[key] += Number(u[key]) || 0;
1084
+ return {
1085
+ turnCount: messages.filter(m => m.role === 'user').length,
1086
+ toolCallCount: messages.filter(m => m.type === 'tool_use').length,
1087
+ lastTurn: usages.at(-1) || null,
1088
+ sessionTotals: total,
1089
+ };
1090
+ }
1091
+
893
1092
  // ─── Unified ──────────────────────────────────────────────────────────────────
894
1093
 
1094
+ // ─── Cursor Agent ────────────────────────────────────────────────────────────
1095
+ // Cursor's native transcript format is private. Sessions created through this
1096
+ // UI have a durable, UI-owned journal at ~/.cursor/claude-simple-ui/sessions.
1097
+ function cursorUiSessionsRoot(cursorHome) {
1098
+ return path.join(cursorHome || path.join(os.homedir(), '.cursor'), 'claude-simple-ui', 'sessions');
1099
+ }
1100
+
1101
+ function cursorJournalPath(sessionId, cursorHome) {
1102
+ return path.join(cursorUiSessionsRoot(cursorHome), `${sessionId}.jsonl`);
1103
+ }
1104
+
1105
+ async function findCursorJournal(sessionId, cursorHome) {
1106
+ const file = cursorJournalPath(sessionId, cursorHome);
1107
+ try { await fs.access(file); return file; } catch { return null; }
1108
+ }
1109
+
1110
+ export async function scanCursorSessions(cursorHome, filterCwd = null) {
1111
+ const home = cursorHome || path.join(os.homedir(), '.cursor');
1112
+ const sessions = [];
1113
+ try {
1114
+ const entries = await fs.readdir(cursorUiSessionsRoot(home), { withFileTypes: true });
1115
+ await Promise.all(entries.filter(e => e.isFile() && e.name.endsWith('.jsonl')).map(async e => {
1116
+ const filePath = path.join(cursorUiSessionsRoot(home), e.name);
1117
+ const [stat, parsed] = await Promise.all([fs.stat(filePath), readGeminiJournalMeta(filePath)]);
1118
+ sessions.push({
1119
+ sessionId: e.name.slice(0, -6), agent: 'cursor', cwd: parsed.meta?.cwd || null,
1120
+ configDir: home, display: parsed.firstUser || e.name.slice(0, 8), updatedAt: stat.mtimeMs,
1121
+ turnCount: parsed.turnCount || 0, totalTokens: parsed.totalTokens || 0,
1122
+ hasMemory: false, journalPath: filePath,
1123
+ });
1124
+ }));
1125
+ } catch {}
1126
+ return sessions.filter(s => !filterCwd || s.cwd === filterCwd);
1127
+ }
1128
+
1129
+ async function loadCursorContext(sessionId, cursorHome) {
1130
+ const file = await findCursorJournal(sessionId, cursorHome);
1131
+ if (!file) return { note: 'This Cursor conversation predates UI journaling; its native transcript is not exposed by cursor-agent.' };
1132
+ const messages = await parseGeminiMessages(file);
1133
+ const usages = messages.filter(m => m.type === 'token_usage').map(m => m.usage || {});
1134
+ const total = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_write_input_tokens: 0 };
1135
+ for (const u of usages) for (const key of Object.keys(total)) total[key] += Number(u[key]) || 0;
1136
+ return { turnCount: messages.filter(m => m.role === 'user').length, toolCallCount: messages.filter(m => m.type === 'tool_use').length, lastTurn: usages.at(-1) || null, sessionTotals: total };
1137
+ }
1138
+
895
1139
  /** Normalize cwd so /private/tmp and /tmp (etc.) group together on macOS. */
896
1140
  export function normalizeCwd(cwd) {
897
1141
  if (!cwd) return null;
@@ -924,15 +1168,18 @@ function publicSession(s) {
924
1168
  // keep for deep search only when needed — not sent if stripped below
925
1169
  _filePath: s.filePath || null,
926
1170
  _sessionDir: s.sessionDir || null,
1171
+ _journalPath: s.journalPath || null,
927
1172
  };
928
1173
  }
929
1174
 
930
- export async function scanAllSessions({ claudeConfigDirs, codexHome, grokHome, filterCwd, agents }) {
931
- const want = new Set(agents || ['claude', 'codex', 'grok']);
1175
+ export async function scanAllSessions({ claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome, filterCwd, agents }) {
1176
+ const want = new Set(agents || ['claude', 'codex', 'grok', 'gemini', 'cursor']);
932
1177
  const tasks = [];
933
1178
  if (want.has('claude')) tasks.push(scanClaudeSessions(claudeConfigDirs, filterCwd));
934
1179
  if (want.has('codex')) tasks.push(scanCodexSessions(codexHome, filterCwd));
935
1180
  if (want.has('grok')) tasks.push(scanGrokSessions(grokHome, filterCwd));
1181
+ if (want.has('gemini')) tasks.push(scanGeminiSessions(geminiHome, filterCwd));
1182
+ if (want.has('cursor')) tasks.push(scanCursorSessions(cursorHome, filterCwd));
936
1183
 
937
1184
  const results = await Promise.all(tasks);
938
1185
  const seen = new Set();
@@ -949,7 +1196,7 @@ export async function scanAllSessions({ claudeConfigDirs, codexHome, grokHome, f
949
1196
 
950
1197
  /** Strip internal-only fields before sending to clients. */
951
1198
  export function toClientSession(s) {
952
- const { _filePath, _sessionDir, ...rest } = s;
1199
+ const { _filePath, _sessionDir, _journalPath, ...rest } = s;
953
1200
  return rest;
954
1201
  }
955
1202
 
@@ -1060,6 +1307,14 @@ async function peekSessionText(s, query) {
1060
1307
  },
1061
1308
  });
1062
1309
  }
1310
+ if (s.agent === 'gemini') {
1311
+ const file = s._journalPath || await findGeminiJournal(s.sessionId, s.configDir);
1312
+ if (!file) return null;
1313
+ return await scanFileForQuery(file, query, {
1314
+ maxLines: 160,
1315
+ extract: d => (d.type === 'user' ? d.content : d.type === 'assistant_delta' ? d.data : null),
1316
+ });
1317
+ }
1063
1318
  // claude
1064
1319
  const file = await findClaudeSessionFile(s.sessionId, null);
1065
1320
  // findClaudeSessionFile needs config dirs — skip if we don't have path
@@ -1165,8 +1420,26 @@ export function groupByProject(sessions) {
1165
1420
  * Load session history. Returns { messages, context } or null.
1166
1421
  * `context` carries window / token usage when the agent exposes it (Grok signals.json).
1167
1422
  */
1168
- export async function loadSessionMessages(sessionId, agent, { claudeConfigDirs, codexHome, grokHome }) {
1423
+ export async function loadSessionMessages(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }) {
1169
1424
  const a = agent || 'claude';
1425
+ if (a === 'cursor') {
1426
+ const file = await findCursorJournal(sessionId, cursorHome);
1427
+ if (!file) return null;
1428
+ return { messages: await parseGeminiMessages(file), context: await loadCursorContext(sessionId, cursorHome), agent: 'cursor' };
1429
+ }
1430
+ if (a === 'gemini') {
1431
+ const native = await findGeminiNativeTranscript(sessionId, geminiHome);
1432
+ const file = native || await findGeminiJournal(sessionId, geminiHome);
1433
+ if (!file) {
1434
+ const native = (await scanGeminiSessions(geminiHome)).some(s => s.sessionId === sessionId);
1435
+ if (!native) return null;
1436
+ return { messages: [], context: await loadGeminiContext(sessionId, geminiHome), agent: 'gemini' };
1437
+ }
1438
+ return {
1439
+ messages: native ? await parseGeminiNativeMessages(native) : await parseGeminiMessages(file),
1440
+ context: await loadGeminiContext(sessionId, geminiHome), agent: 'gemini',
1441
+ };
1442
+ }
1170
1443
  if (a === 'codex') {
1171
1444
  const file = await findCodexSessionFile(sessionId, codexHome);
1172
1445
  if (!file) return null;
@@ -1183,7 +1456,8 @@ export async function loadSessionMessages(sessionId, agent, { claudeConfigDirs,
1183
1456
  return { messages: await parseClaudeMessages(file), context: null, agent: 'claude' };
1184
1457
  }
1185
1458
 
1186
- export async function loadSessionMemory(sessionId, agent, { claudeConfigDirs, codexHome, grokHome }) {
1459
+ export async function loadSessionMemory(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }) {
1460
+ if (agent === 'gemini' || agent === 'cursor') return { agent, files: [] };
1187
1461
  if (agent === 'grok') {
1188
1462
  const dir = await findGrokSessionDir(sessionId, grokHome);
1189
1463
  if (!dir) return null;
@@ -1248,6 +1522,7 @@ export async function readSessionMemoryFile(sessionId, agent, fileId, opts) {
1248
1522
  const id = String(fileId || '').replaceAll('\\', '/');
1249
1523
  if (!id || id.startsWith('/') || id.split('/').some(p => p === '..' || !p)) return null;
1250
1524
  let root;
1525
+ if (agent === 'gemini' || agent === 'cursor') return null;
1251
1526
  if (agent === 'codex') root = path.join(opts.codexHome || path.join(os.homedir(), '.codex'), 'memories');
1252
1527
  else if (agent === 'grok') {
1253
1528
  const dir = await findGrokSessionDir(sessionId, opts.grokHome);
@@ -1272,10 +1547,10 @@ export async function readSessionMemoryFile(sessionId, agent, fileId, opts) {
1272
1547
  }
1273
1548
 
1274
1549
  /** Return bounded raw JSONL for a particular agent session, for Meta Agent analysis. */
1275
- export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, codexHome, grokHome }, { offset = 0, maxLines = 500 } = {}) {
1550
+ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset = 0, maxLines = 500 } = {}) {
1276
1551
  if (!agent) {
1277
- for (const candidate of ['claude', 'codex', 'grok']) {
1278
- const found = await loadSessionJsonl(sessionId, candidate, { claudeConfigDirs, codexHome, grokHome }, { offset, maxLines });
1552
+ for (const candidate of ['claude', 'codex', 'grok', 'gemini', 'cursor']) {
1553
+ const found = await loadSessionJsonl(sessionId, candidate, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { offset, maxLines });
1279
1554
  if (found) return found;
1280
1555
  }
1281
1556
  return null;
@@ -1283,6 +1558,8 @@ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, cod
1283
1558
  const a = (agent || 'claude').toLowerCase();
1284
1559
  let file;
1285
1560
  if (a === 'codex') file = await findCodexSessionFile(sessionId, codexHome);
1561
+ else if (a === 'cursor') file = await findCursorJournal(sessionId, cursorHome);
1562
+ else if (a === 'gemini') file = await findGeminiJournal(sessionId, geminiHome);
1286
1563
  else if (a === 'grok') {
1287
1564
  const dir = await findGrokSessionDir(sessionId, grokHome);
1288
1565
  file = dir ? path.join(dir, 'chat_history.jsonl') : null;
@@ -1304,11 +1581,20 @@ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, cod
1304
1581
  return { agent: a, fileName: path.basename(file), offset: start, lines, nextOffset: start + lines.length, truncated: hasMore, size: stat?.size || 0 };
1305
1582
  }
1306
1583
 
1307
- export async function getSessionMeta(sessionId, agent, { claudeConfigDirs, codexHome, grokHome }) {
1584
+ export async function getSessionMeta(sessionId, agent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }) {
1308
1585
  const a = (agent || 'claude').toLowerCase();
1309
1586
  let cwd = null;
1310
1587
  let display = null;
1311
- if (a === 'grok') {
1588
+ if (a === 'cursor') {
1589
+ const found = (await scanCursorSessions(cursorHome)).find(s => s.sessionId === sessionId);
1590
+ cwd = found?.cwd || null;
1591
+ display = found?.display || null;
1592
+ } else if (a === 'gemini') {
1593
+ const sessions = await scanGeminiSessions(geminiHome);
1594
+ const found = sessions.find(s => s.sessionId === sessionId);
1595
+ cwd = found?.cwd || null;
1596
+ display = found?.display || null;
1597
+ } else if (a === 'grok') {
1312
1598
  const dir = await findGrokSessionDir(sessionId, grokHome);
1313
1599
  if (dir) {
1314
1600
  try {
@@ -1339,13 +1625,13 @@ export async function getSessionMeta(sessionId, agent, { claudeConfigDirs, codex
1339
1625
  return { cwd, display };
1340
1626
  }
1341
1627
 
1342
- export async function convertSession(sourceSessionId, sourceAgent, targetAgent, { claudeConfigDirs, codexHome, grokHome }) {
1628
+ export async function convertSession(sourceSessionId, sourceAgent, targetAgent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }) {
1343
1629
  // 1. Get meta and messages from source session
1344
- const meta = await getSessionMeta(sourceSessionId, sourceAgent, { claudeConfigDirs, codexHome, grokHome });
1630
+ const meta = await getSessionMeta(sourceSessionId, sourceAgent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome });
1345
1631
  const cwd = meta.cwd || null;
1346
1632
  const display = meta.display || 'Converted Session';
1347
1633
 
1348
- const bundle = await loadSessionMessages(sourceSessionId, sourceAgent, { claudeConfigDirs, codexHome, grokHome });
1634
+ const bundle = await loadSessionMessages(sourceSessionId, sourceAgent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome });
1349
1635
  const messages = bundle.messages || [];
1350
1636
 
1351
1637
  // 2. Generate target session ID
@@ -1353,6 +1639,9 @@ export async function convertSession(sourceSessionId, sourceAgent, targetAgent,
1353
1639
 
1354
1640
  // 3. Serialize messages to the target agent
1355
1641
  const ta = targetAgent.toLowerCase();
1642
+ if (ta === 'gemini' || ta === 'cursor') {
1643
+ throw new Error(`${ta === 'cursor' ? 'Cursor' : 'Gemini'} sessions use native conversation storage and cannot be cloned safely. Start a new session instead.`);
1644
+ }
1356
1645
 
1357
1646
  if (ta === 'claude') {
1358
1647
  const configDir = claudeConfigDirs?.[0] || path.join(os.homedir(), '.claude');