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.
- package/README.md +13 -5
- package/dist/app.0.2.9.min.js +143 -139
- package/dist/style.0.2.9.min.css +1 -1
- package/package.json +11 -3
- package/public/chat.js +5 -5
- package/public/files.js +5 -5
- package/public/index.html +6 -7
- package/public/lib.js +1 -1
- package/public/router.js +36 -8
- package/public/settings.js +54 -2
- package/public/shell/boot.js +7 -7
- package/public/shell/layout.js +2 -3
- package/public/shell/session-list.js +5 -12
- package/public/shell/templates.js +34 -28
- package/public/shell/util.js +3 -1
- package/public/shell.js +54 -13
- package/public/state.js +36 -5
- package/public/style.css +13 -0
- package/public/sw.js +6 -2
- package/public/ui.min.css +2 -0
- package/public/vendor/highlight-dark.min.css +10 -0
- package/public/vendor/highlight.min.js +1262 -0
- package/public/vendor/jszip.min.js +13 -0
- package/public/vendor/mammoth.browser.min.js +21 -0
- package/public/vendor/marked.min.js +6 -0
- package/public/vendor/mini-react.min.js +4 -0
- package/public/vendor/pdf.min.js +22 -0
- package/public/vendor/pdf.worker.min.js +22 -0
- package/public/vendor/xlsx.full.min.js +22 -0
- package/src/agents/cursor.js +189 -0
- package/src/agents/gemini.js +182 -0
- package/src/agents/meta-agent.js +9 -5
- package/src/agents/models.js +112 -2
- package/src/agents/sessions.js +303 -14
- package/src/app.js +119 -7
- package/src/mcp.js +275 -0
package/src/agents/sessions.js
CHANGED
|
@@ -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 === '
|
|
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');
|