agentel 0.2.6 → 0.2.8
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 +45 -18
- package/docs/code-reference.md +9 -5
- package/docs/history-source-handling.md +166 -65
- package/docs/release.md +1 -1
- package/package.json +5 -2
- package/src/archive.js +200 -17
- package/src/cli.js +1794 -104
- package/src/config.js +11 -0
- package/src/importers/gemini.js +2 -1
- package/src/importers.js +1675 -134
- package/src/search.js +416 -176
- package/src/web-export-instructions.js +6 -4
package/src/importers.js
CHANGED
|
@@ -7,7 +7,7 @@ const path = require("path");
|
|
|
7
7
|
const zlib = require("zlib");
|
|
8
8
|
const { execFileSync, spawnSync } = require("child_process");
|
|
9
9
|
const { fileURLToPath } = require("url");
|
|
10
|
-
const { archiveRoot, deleteSessionArchive, listSessions, readTranscript, writeSession, stableSessionId, toIso } = require("./archive");
|
|
10
|
+
const { archiveRoot, computeSessionUsage, deleteSessionArchive, listSessions, readTranscript, writeSession, stableSessionId, toIso } = require("./archive");
|
|
11
11
|
const { fingerprintPrefix, parserVersionForSource } = require("./parser-versions");
|
|
12
12
|
const { canonicalRepo } = require("./repo");
|
|
13
13
|
const { ensureDir, paths, readJson, writeJson } = require("./paths");
|
|
@@ -23,6 +23,7 @@ const { manualImportInstructionResult } = require("./web-export-instructions");
|
|
|
23
23
|
|
|
24
24
|
const WEB_TOKEN_ESTIMATE_CHARS = 4;
|
|
25
25
|
const WEB_CHAT_TOKEN_ESTIMATION_METHOD = "web-message-parts-chars-v1";
|
|
26
|
+
const EXPORT_ZIP_ENTRY_MAX_BUFFER = 1024 * 1024 * 512;
|
|
26
27
|
const OPENCODE_SOURCE_KINDS = new Set(["cli", "desktop", "web"]);
|
|
27
28
|
const OPENCODE_SESSION_ID_RE = /\bses_[A-Za-z0-9]+\b/g;
|
|
28
29
|
|
|
@@ -81,10 +82,22 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
81
82
|
const files =
|
|
82
83
|
provider === "claude_code" ? claudeFiles(env) : provider === "claude_sdk" ? claudeSdkFiles(env) : jsonlFiles(roots);
|
|
83
84
|
const claudeCodeMetadata = provider === "claude_code" ? claudeCodeSessionMetadataByCliSessionId(env) : new Map();
|
|
85
|
+
const claudeSubagentCache = provider === "claude_code" ? new Map() : null;
|
|
84
86
|
|
|
85
87
|
const candidates = files
|
|
86
|
-
.map((file) =>
|
|
87
|
-
|
|
88
|
+
.map((file) => {
|
|
89
|
+
const stat = safeStat(file);
|
|
90
|
+
const claudeSubagentRunSourceFiles = provider === "claude_code"
|
|
91
|
+
? claudeSubagentRunSourceFilesForSession(claudeSessionIdFromFilename(file), file, env)
|
|
92
|
+
: [];
|
|
93
|
+
return {
|
|
94
|
+
file,
|
|
95
|
+
stat,
|
|
96
|
+
claudeSubagentRunSourceFiles,
|
|
97
|
+
importMtimeMs: latestFileMtimeMs([file, ...claudeSubagentRunSourceFiles], stat)
|
|
98
|
+
};
|
|
99
|
+
})
|
|
100
|
+
.filter((item) => item.stat && (!since || item.importMtimeMs >= since.getTime()))
|
|
88
101
|
.sort((a, b) => a.file.localeCompare(b.file));
|
|
89
102
|
|
|
90
103
|
const summary = {
|
|
@@ -100,7 +113,11 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
100
113
|
for (let index = 0; index < candidates.length; index++) {
|
|
101
114
|
const item = candidates[index];
|
|
102
115
|
const sourceType = jsonlProviderSourceType(provider);
|
|
103
|
-
const
|
|
116
|
+
const subagentRunFingerprint = provider === "claude_code" ? filesFingerprint(item.claudeSubagentRunSourceFiles) : "";
|
|
117
|
+
const baseFingerprint = [
|
|
118
|
+
`${fingerprintPrefix(sourceType)}:${fileFingerprint(item.file, item.stat)}`,
|
|
119
|
+
subagentRunFingerprint ? `claude-subagent-runs:${subagentRunFingerprint}` : ""
|
|
120
|
+
].filter(Boolean).join(":");
|
|
104
121
|
const preliminaryMetadata = provider === "claude_code"
|
|
105
122
|
? claudeCodeMetadata.get(claudeSessionIdFromFilename(item.file)) || null
|
|
106
123
|
: null;
|
|
@@ -130,6 +147,12 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
130
147
|
const cwd = parsed.cwd || sessionMetadata?.cwd || "";
|
|
131
148
|
const scopeCanonical = cwd ? "" : uncategorizedScope(provider);
|
|
132
149
|
const repo = repoInfoForImport(provider, cwd, sessionMetadata);
|
|
150
|
+
const claudeSubagents = provider === "claude_code"
|
|
151
|
+
? claudeSubagentImportContext(repoCwdForImport(provider, cwd, sessionMetadata), env, claudeSubagentCache)
|
|
152
|
+
: null;
|
|
153
|
+
const claudeSubagentRuns = provider === "claude_code"
|
|
154
|
+
? claudeSubagentRunImportContext(sessionId, item.file, env, item.claudeSubagentRunSourceFiles)
|
|
155
|
+
: null;
|
|
133
156
|
if (options.repos && options.repos.length && (!repo || !options.repos.includes(repo.key))) {
|
|
134
157
|
summary.skipped++;
|
|
135
158
|
reportProgress(options, summary, index + 1, item.file);
|
|
@@ -156,18 +179,48 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
156
179
|
startedAt: parsed.startedAt,
|
|
157
180
|
endedAt: parsed.endedAt,
|
|
158
181
|
sourcePath: item.file,
|
|
159
|
-
sourceFiles: [
|
|
182
|
+
sourceFiles: [
|
|
183
|
+
item.file,
|
|
184
|
+
sessionMetadata?.sourcePath || "",
|
|
185
|
+
...auxiliaryFiles,
|
|
186
|
+
...(claudeSubagents?.sourceFiles || []),
|
|
187
|
+
...(claudeSubagentRuns?.sourceFiles || [])
|
|
188
|
+
].filter(Boolean),
|
|
160
189
|
sourceType,
|
|
161
190
|
title: jsonlSessionTitleForImport(parsed, sessionMetadata),
|
|
162
|
-
sessionSummary: mergeSessionSummaries(
|
|
191
|
+
sessionSummary: mergeSessionSummaries(
|
|
192
|
+
claudeCodeSidecarSessionSummary(sessionMetadata),
|
|
193
|
+
parsed.sessionSummary,
|
|
194
|
+
claudeSubagents?.sessionSummary,
|
|
195
|
+
claudeSubagentRuns?.sessionSummary
|
|
196
|
+
)
|
|
163
197
|
},
|
|
164
198
|
env
|
|
165
199
|
);
|
|
166
200
|
state.files[fingerprint] = { sessionId, at: new Date().toISOString() };
|
|
167
201
|
state.sessions[sessionId] = { provider, sourcePath: item.file, fingerprint, auxiliary: auxiliaryFiles.length || undefined, at: new Date().toISOString() };
|
|
168
202
|
archived.add(archiveSessionKey(provider, sessionId));
|
|
203
|
+
for (const subagentSession of claudeSubagentRuns?.sessions || []) {
|
|
204
|
+
writeSession(
|
|
205
|
+
{
|
|
206
|
+
...subagentSession,
|
|
207
|
+
repoInfo: repo || undefined,
|
|
208
|
+
scopeCanonical,
|
|
209
|
+
sourceType
|
|
210
|
+
},
|
|
211
|
+
env
|
|
212
|
+
);
|
|
213
|
+
state.sessions[subagentSession.sessionId] = {
|
|
214
|
+
provider,
|
|
215
|
+
sourcePath: subagentSession.sourcePath,
|
|
216
|
+
fingerprint: `${fingerprintPrefix(sourceType)}:${filesFingerprint(subagentSession.sourceFiles || [subagentSession.sourcePath])}`,
|
|
217
|
+
parentSessionId: sessionId,
|
|
218
|
+
at: new Date().toISOString()
|
|
219
|
+
};
|
|
220
|
+
archived.add(archiveSessionKey(provider, subagentSession.sessionId));
|
|
221
|
+
}
|
|
169
222
|
}
|
|
170
|
-
summary.imported
|
|
223
|
+
summary.imported += 1 + (claudeSubagentRuns?.sessions?.length || 0);
|
|
171
224
|
reportProgress(options, summary, index + 1, item.file);
|
|
172
225
|
}
|
|
173
226
|
|
|
@@ -187,6 +240,46 @@ function codexThreadSourceType(thread) {
|
|
|
187
240
|
return "codex-cli-history";
|
|
188
241
|
}
|
|
189
242
|
|
|
243
|
+
function finalizeCodexParsedThread(thread, parsed) {
|
|
244
|
+
const result = { ...(parsed || {}) };
|
|
245
|
+
result.messages = Array.isArray(parsed?.messages) ? [...parsed.messages] : [];
|
|
246
|
+
result.messages.push(...codexSupplementaryMessages(thread, result.endedAt || thread?.updatedAt));
|
|
247
|
+
result.messages = dedupeAdjacentMessages(result.messages)
|
|
248
|
+
.sort((a, b) => String(a.timestamp || "").localeCompare(String(b.timestamp || "")));
|
|
249
|
+
result.startedAt = result.messages[0]?.timestamp || result.startedAt;
|
|
250
|
+
result.endedAt = result.messages[result.messages.length - 1]?.timestamp || result.endedAt;
|
|
251
|
+
result.sessionSummary = mergeSessionSummaries(result.sessionSummary, codexThreadStateSessionSummary(thread));
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function codexThreadStateSessionSummary(thread) {
|
|
256
|
+
const rawTotalTokens = Number(thread?.tokensUsed);
|
|
257
|
+
const totalTokens = Number.isFinite(rawTotalTokens) && rawTotalTokens > 0 ? rawTotalTokens : 0;
|
|
258
|
+
const model = firstString(thread?.model);
|
|
259
|
+
if (!totalTokens && !model) return null;
|
|
260
|
+
return {
|
|
261
|
+
usage: totalTokens ? { totalTokens, authoritativeTotalTokens: true, source: "codex-state-tokens-used" } : undefined,
|
|
262
|
+
modelUsage: model ? [{ model, source: "codex-state" }] : undefined
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function codexSubagentChildrenByParent(threads) {
|
|
267
|
+
const byParent = new Map();
|
|
268
|
+
for (const thread of threads || []) {
|
|
269
|
+
if (!thread?.parentThreadId) continue;
|
|
270
|
+
const children = byParent.get(thread.parentThreadId) || [];
|
|
271
|
+
children.push(thread);
|
|
272
|
+
byParent.set(thread.parentThreadId, children);
|
|
273
|
+
}
|
|
274
|
+
for (const children of byParent.values()) {
|
|
275
|
+
children.sort((a, b) => {
|
|
276
|
+
const time = String(a.createdAt || a.updatedAt || "").localeCompare(String(b.createdAt || b.updatedAt || ""));
|
|
277
|
+
return time || String(a.id || "").localeCompare(String(b.id || ""));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return byParent;
|
|
281
|
+
}
|
|
282
|
+
|
|
190
283
|
function importClaudeDesktopProvider(provider, since, options = {}, env = process.env) {
|
|
191
284
|
const state = loadImportState(env);
|
|
192
285
|
const archived = archivedSessionKeys(env);
|
|
@@ -268,6 +361,7 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
268
361
|
if (options.codexSource) return thread.source === options.codexSource;
|
|
269
362
|
return ["cli", "vscode"].includes(thread.source);
|
|
270
363
|
});
|
|
364
|
+
const codexSubagentChildren = codexSubagentChildrenByParent(threads);
|
|
271
365
|
if (!threads.length && options.codexSource && options.codexSource !== "cli") {
|
|
272
366
|
return { provider, discovered: 0, candidates: 0, imported: 0, skipped: 0, errors: [], details: {} };
|
|
273
367
|
}
|
|
@@ -277,7 +371,7 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
277
371
|
const archived = archivedSessionKeys(env);
|
|
278
372
|
const candidates = threads
|
|
279
373
|
.filter((thread) => thread.rolloutPath)
|
|
280
|
-
.filter((thread) => !since || new Date(thread.
|
|
374
|
+
.filter((thread) => !since || new Date(codexThreadImportUpdatedAt(thread, codexSubagentChildren.get(thread.id) || [])) >= since)
|
|
281
375
|
.sort((a, b) => a.rolloutPath.localeCompare(b.rolloutPath));
|
|
282
376
|
const summary = {
|
|
283
377
|
provider,
|
|
@@ -299,7 +393,13 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
299
393
|
continue;
|
|
300
394
|
}
|
|
301
395
|
const sourceType = codexThreadSourceType(thread);
|
|
302
|
-
const
|
|
396
|
+
const children = codexSubagentChildren.get(thread.id) || [];
|
|
397
|
+
const fingerprint = [
|
|
398
|
+
`${fingerprintPrefix(sourceType)}:${fileFingerprint(thread.rolloutPath, stat)}`,
|
|
399
|
+
codexSupplementFingerprint(thread),
|
|
400
|
+
codexThreadMetadataFingerprint(thread),
|
|
401
|
+
codexSubagentRunsFingerprint(children)
|
|
402
|
+
].filter(Boolean).join(":");
|
|
303
403
|
if (alreadyImported(state, thread.id, fingerprint, archived, provider)) {
|
|
304
404
|
summary.skipped++;
|
|
305
405
|
reportProgress(options, summary, index + 1, thread.rolloutPath);
|
|
@@ -313,10 +413,7 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
313
413
|
reportProgress(options, summary, index + 1, thread.rolloutPath);
|
|
314
414
|
continue;
|
|
315
415
|
}
|
|
316
|
-
parsed
|
|
317
|
-
parsed.messages = dedupeAdjacentMessages(parsed.messages).sort((a, b) => String(a.timestamp || "").localeCompare(String(b.timestamp || "")));
|
|
318
|
-
parsed.startedAt = parsed.messages[0]?.timestamp || parsed.startedAt;
|
|
319
|
-
parsed.endedAt = parsed.messages[parsed.messages.length - 1]?.timestamp || parsed.endedAt;
|
|
416
|
+
parsed = finalizeCodexParsedThread(thread, parsed);
|
|
320
417
|
if (!parsed.messages.length) {
|
|
321
418
|
state.files[fingerprint] = { skipped: true, reason: "no messages", at: new Date().toISOString() };
|
|
322
419
|
summary.skipped++;
|
|
@@ -338,6 +435,8 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
338
435
|
reportProgress(options, summary, index + 1, thread.rolloutPath);
|
|
339
436
|
continue;
|
|
340
437
|
}
|
|
438
|
+
const codexSubagentRuns = codexSubagentRunImportContext(thread, children, env);
|
|
439
|
+
const codexSubagentRun = codexSubagentSessionSummary(thread, parsed, env);
|
|
341
440
|
if (!options.dryRun) {
|
|
342
441
|
writeSession(
|
|
343
442
|
{
|
|
@@ -350,9 +449,12 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
350
449
|
startedAt: parsed.startedAt || thread.createdAt,
|
|
351
450
|
endedAt: parsed.endedAt || thread.updatedAt,
|
|
352
451
|
sourcePath: thread.rolloutPath,
|
|
353
|
-
sourceFiles: codexSourceFiles(thread, env),
|
|
452
|
+
sourceFiles: codexSourceFiles(thread, env, children),
|
|
354
453
|
sourceType,
|
|
355
|
-
title: thread
|
|
454
|
+
title: codexSessionTitleForImport(thread, parsed),
|
|
455
|
+
sessionSummary: mergeSessionSummaries(parsed.sessionSummary, codexSubagentRuns?.sessionSummary, codexSubagentRun?.sessionSummary),
|
|
456
|
+
conversationKind: thread.isCodexSubagent ? "codex_subagent" : undefined,
|
|
457
|
+
parentComposerId: thread.parentThreadId || undefined
|
|
356
458
|
},
|
|
357
459
|
env
|
|
358
460
|
);
|
|
@@ -616,7 +718,11 @@ function parseAgentJsonl(file, provider) {
|
|
|
616
718
|
event.payload?.session_id,
|
|
617
719
|
event.sessionId
|
|
618
720
|
);
|
|
619
|
-
|
|
721
|
+
const codexThreadTitle = provider === "codex" ? codexThreadNameFromEvent(event) : "";
|
|
722
|
+
if (codexThreadTitle) {
|
|
723
|
+
title = codexThreadTitle;
|
|
724
|
+
titleSource = "thread-name";
|
|
725
|
+
} else if (!title) {
|
|
620
726
|
const sourceTitle = firstString(event.title, event.conversation_title, event.payload?.title);
|
|
621
727
|
const aiTitle = firstString(event.aiTitle, event.ai_title);
|
|
622
728
|
if (sourceTitle) {
|
|
@@ -953,18 +1059,48 @@ function claudeWorktreeParentRepo(provider, cwd) {
|
|
|
953
1059
|
}
|
|
954
1060
|
|
|
955
1061
|
function inferredJsonlSessionTitle(provider, messages) {
|
|
956
|
-
if (!isClaudeJsonlProvider(provider)) return "";
|
|
1062
|
+
if (!isClaudeJsonlProvider(provider) && provider !== "codex") return "";
|
|
957
1063
|
const firstUser = (messages || []).find((message) => message.role === "user" && !message.metadata?.providerGenerated);
|
|
958
1064
|
return titleFromPrompt(firstUser?.content);
|
|
959
1065
|
}
|
|
960
1066
|
|
|
961
1067
|
function titleFromPrompt(value) {
|
|
962
|
-
const cleaned =
|
|
1068
|
+
const cleaned = cleanPromptTitleLine(promptTitleLine(value));
|
|
963
1069
|
if (!cleaned) return "";
|
|
964
1070
|
const max = 96;
|
|
965
1071
|
return cleaned.length > max ? `${cleaned.slice(0, max - 1).trimEnd()}…` : cleaned;
|
|
966
1072
|
}
|
|
967
1073
|
|
|
1074
|
+
function promptTitleLine(value) {
|
|
1075
|
+
const lines = String(value || "")
|
|
1076
|
+
.split(/\r?\n/)
|
|
1077
|
+
.map((line) => line.trim())
|
|
1078
|
+
.filter(Boolean);
|
|
1079
|
+
if (!lines.length) return "";
|
|
1080
|
+
if (isAgentlogRecallSkillLine(lines[0]) && lines.length > 1) {
|
|
1081
|
+
const candidate = lines.slice(1).find((line) => cleanPromptTitleLine(line) && !lowSignalPromptTitleLine(line));
|
|
1082
|
+
if (candidate) return candidate;
|
|
1083
|
+
}
|
|
1084
|
+
return lines[0];
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function cleanPromptTitleLine(value) {
|
|
1088
|
+
return String(value || "")
|
|
1089
|
+
.replace(/\[\$[^\]\n]+\]\([^)]+\)\s*/g, "")
|
|
1090
|
+
.replace(/\[([^\]\n]+)\]\([^)]+\)/g, "$1")
|
|
1091
|
+
.replace(/\s+/g, " ")
|
|
1092
|
+
.trim();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function isAgentlogRecallSkillLine(value) {
|
|
1096
|
+
return /^\[\$agentlog-recall\]\([^)]+\)/i.test(String(value || "").trim());
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function lowSignalPromptTitleLine(value) {
|
|
1100
|
+
const cleaned = cleanPromptTitleLine(value);
|
|
1101
|
+
return /^#/.test(cleaned) || /^<[^>]+>/.test(cleaned) || /^['"]?\/[^'"]+['"]?$/.test(cleaned);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
968
1104
|
function isClaudeJsonlProvider(provider) {
|
|
969
1105
|
return provider === "claude_code" || provider === "claude_sdk";
|
|
970
1106
|
}
|
|
@@ -1230,21 +1366,37 @@ function applyCodexTokenCount(event, provider, messages, context) {
|
|
|
1230
1366
|
if (String(payload.type || event.type || "").toLowerCase() !== "token_count") return false;
|
|
1231
1367
|
const usage = codexTokenUsage(payload);
|
|
1232
1368
|
if (!usage) return true;
|
|
1233
|
-
const previous = context.tokenUsage || { input: 0, output: 0 };
|
|
1234
|
-
const
|
|
1235
|
-
const
|
|
1369
|
+
const previous = context.tokenUsage || { input: 0, output: 0, cachedInput: 0, reasoningOutput: 0, total: 0 };
|
|
1370
|
+
const hasPrevious = codexTokenUsageHasProgress(previous);
|
|
1371
|
+
const inputDelta = hasPrevious ? Math.max(0, usage.input - previous.input) : usage.input;
|
|
1372
|
+
const outputDelta = hasPrevious ? Math.max(0, usage.output - previous.output) : usage.output;
|
|
1373
|
+
const cacheInputDelta = hasPrevious ? Math.max(0, usage.cachedInput - previous.cachedInput) : usage.cachedInput;
|
|
1374
|
+
const reasoningOutputDelta = hasPrevious ? Math.max(0, usage.reasoningOutput - previous.reasoningOutput) : usage.reasoningOutput;
|
|
1375
|
+
const totalDelta = usage.total
|
|
1376
|
+
? (hasPrevious ? Math.max(0, usage.total - previous.total) : usage.total)
|
|
1377
|
+
: inputDelta + outputDelta;
|
|
1236
1378
|
context.tokenUsage = usage;
|
|
1237
1379
|
const target = [...messages].reverse().find((message) => message.role === "assistant");
|
|
1238
1380
|
if (!target) return true;
|
|
1381
|
+
const existingUsage = target.metadata?.usage && typeof target.metadata.usage === "object" ? target.metadata.usage : {};
|
|
1382
|
+
const freshInputDelta = Math.max(0, inputDelta - cacheInputDelta);
|
|
1383
|
+
const nextUsage = compactMetadata({
|
|
1384
|
+
...existingUsage,
|
|
1385
|
+
inputTokens: addTokenNumbers(existingUsage.inputTokens, freshInputDelta),
|
|
1386
|
+
outputTokens: addTokenNumbers(existingUsage.outputTokens, outputDelta),
|
|
1387
|
+
cacheInputTokens: addTokenNumbers(existingUsage.cacheInputTokens, cacheInputDelta),
|
|
1388
|
+
reasoningOutputTokens: addTokenNumbers(existingUsage.reasoningOutputTokens, reasoningOutputDelta),
|
|
1389
|
+
reasoningOutputTokensIncludedInOutput: reasoningOutputDelta || existingUsage.reasoningOutputTokensIncludedInOutput ? true : undefined,
|
|
1390
|
+
totalTokens: addTokenNumbers(existingUsage.totalTokens, totalDelta || freshInputDelta + cacheInputDelta + outputDelta),
|
|
1391
|
+
totalInputTokens: usage.input,
|
|
1392
|
+
totalOutputTokens: usage.output,
|
|
1393
|
+
totalCacheInputTokens: usage.cachedInput || undefined,
|
|
1394
|
+
totalReasoningOutputTokens: usage.reasoningOutput || undefined
|
|
1395
|
+
});
|
|
1239
1396
|
target.metadata = {
|
|
1240
1397
|
...(target.metadata || {}),
|
|
1241
1398
|
provider,
|
|
1242
|
-
usage:
|
|
1243
|
-
inputTokens: inputDelta,
|
|
1244
|
-
outputTokens: outputDelta,
|
|
1245
|
-
totalInputTokens: usage.input,
|
|
1246
|
-
totalOutputTokens: usage.output
|
|
1247
|
-
}
|
|
1399
|
+
usage: nextUsage
|
|
1248
1400
|
};
|
|
1249
1401
|
return true;
|
|
1250
1402
|
}
|
|
@@ -1260,11 +1412,50 @@ function codexTokenUsage(payload) {
|
|
|
1260
1412
|
for (const item of candidates) {
|
|
1261
1413
|
const input = Number(item?.input_tokens ?? item?.inputTokens ?? item?.prompt_tokens ?? item?.promptTokens);
|
|
1262
1414
|
const output = Number(item?.output_tokens ?? item?.outputTokens ?? item?.completion_tokens ?? item?.completionTokens);
|
|
1263
|
-
if (Number.isFinite(input) && Number.isFinite(output))
|
|
1415
|
+
if (Number.isFinite(input) && Number.isFinite(output)) {
|
|
1416
|
+
const cachedInput = Number(
|
|
1417
|
+
item?.cached_input_tokens ??
|
|
1418
|
+
item?.cachedInputTokens ??
|
|
1419
|
+
item?.cache_read_input_tokens ??
|
|
1420
|
+
item?.cacheReadInputTokens ??
|
|
1421
|
+
item?.cacheInputTokens ??
|
|
1422
|
+
item?.prompt_tokens_details?.cached_tokens ??
|
|
1423
|
+
item?.promptTokensDetails?.cachedTokens
|
|
1424
|
+
);
|
|
1425
|
+
const reasoningOutput = Number(
|
|
1426
|
+
item?.reasoning_output_tokens ??
|
|
1427
|
+
item?.reasoningOutputTokens ??
|
|
1428
|
+
item?.reasoning_tokens ??
|
|
1429
|
+
item?.reasoningTokens ??
|
|
1430
|
+
item?.completion_tokens_details?.reasoning_tokens ??
|
|
1431
|
+
item?.completionTokensDetails?.reasoningTokens ??
|
|
1432
|
+
item?.output_tokens_details?.reasoning_tokens ??
|
|
1433
|
+
item?.outputTokensDetails?.reasoningTokens
|
|
1434
|
+
);
|
|
1435
|
+
const total = Number(item?.total_tokens ?? item?.totalTokens ?? item?.totalTokenCount ?? item?.total_token_count ?? item?.total);
|
|
1436
|
+
return {
|
|
1437
|
+
input,
|
|
1438
|
+
output,
|
|
1439
|
+
cachedInput: Number.isFinite(cachedInput) && cachedInput > 0 ? cachedInput : 0,
|
|
1440
|
+
reasoningOutput: Number.isFinite(reasoningOutput) && reasoningOutput > 0 ? reasoningOutput : 0,
|
|
1441
|
+
total: Number.isFinite(total) && total > 0 ? total : input + output
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1264
1444
|
}
|
|
1265
1445
|
return null;
|
|
1266
1446
|
}
|
|
1267
1447
|
|
|
1448
|
+
function codexTokenUsageHasProgress(usage) {
|
|
1449
|
+
return Boolean((usage?.input || 0) || (usage?.output || 0) || (usage?.cachedInput || 0) || (usage?.reasoningOutput || 0) || (usage?.total || 0));
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function addTokenNumbers(left, right) {
|
|
1453
|
+
const a = Number(left || 0);
|
|
1454
|
+
const b = Number(right || 0);
|
|
1455
|
+
const sum = (Number.isFinite(a) && a > 0 ? a : 0) + (Number.isFinite(b) && b > 0 ? b : 0);
|
|
1456
|
+
return sum || undefined;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1268
1459
|
function extractCodexSpecialMessage(event, provider, context = {}) {
|
|
1269
1460
|
if (provider !== "codex") return null;
|
|
1270
1461
|
const item = event?.payload || event?.item || event?.data || event;
|
|
@@ -1764,7 +1955,17 @@ function extractText(value, depth = 0) {
|
|
|
1764
1955
|
|
|
1765
1956
|
function importWebChat(providerInput, file, options = {}, env = process.env) {
|
|
1766
1957
|
const provider = canonicalWebProvider(providerInput);
|
|
1767
|
-
|
|
1958
|
+
reportWebImportProgress(options, provider, {
|
|
1959
|
+
current: 0,
|
|
1960
|
+
total: 0,
|
|
1961
|
+
message: "reading export"
|
|
1962
|
+
});
|
|
1963
|
+
const source = readExportBundle(file, provider);
|
|
1964
|
+
reportWebImportProgress(options, provider, {
|
|
1965
|
+
current: 0,
|
|
1966
|
+
total: source.entries.length || 0,
|
|
1967
|
+
message: source.entries.length ? `parsed ${source.entries.length} export files` : "parsed export"
|
|
1968
|
+
});
|
|
1768
1969
|
const sourceAccountId = options.accountId || inferWebSourceAccountId(provider, source);
|
|
1769
1970
|
let accountId = options.accountId || sourceAccountId;
|
|
1770
1971
|
const existing = accountId ? getWebAccount(provider, accountId, env) : null;
|
|
@@ -1782,6 +1983,11 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
|
|
|
1782
1983
|
displayName: options.displayName || existing?.displayName || inferredUsername,
|
|
1783
1984
|
sourceAccountId
|
|
1784
1985
|
}, env);
|
|
1986
|
+
reportWebImportProgress(options, provider, {
|
|
1987
|
+
current: 0,
|
|
1988
|
+
total: source.entries.length || 0,
|
|
1989
|
+
message: "normalizing conversations"
|
|
1990
|
+
});
|
|
1785
1991
|
const normalized = normalizeWebConversations(provider, source, account);
|
|
1786
1992
|
const conversations = normalized.conversations;
|
|
1787
1993
|
const summary = {
|
|
@@ -1797,19 +2003,39 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
|
|
|
1797
2003
|
};
|
|
1798
2004
|
const state = loadImportState(env);
|
|
1799
2005
|
const archived = archivedSessionKeys(env);
|
|
1800
|
-
|
|
2006
|
+
reportWebImportProgress(options, provider, {
|
|
2007
|
+
current: 0,
|
|
2008
|
+
total: conversations.length,
|
|
2009
|
+
message: `found ${conversations.length} conversations`
|
|
2010
|
+
});
|
|
2011
|
+
const sharedRaw = options.dryRun ? null : ensureSharedWebExportRaw(provider, source, account, env, options);
|
|
1801
2012
|
|
|
1802
|
-
|
|
2013
|
+
conversations.forEach((conversation, index) => {
|
|
2014
|
+
const current = index + 1;
|
|
1803
2015
|
if (!conversation.messages.length) {
|
|
1804
2016
|
summary.skipped++;
|
|
1805
|
-
|
|
2017
|
+
reportWebImportProgress(options, provider, {
|
|
2018
|
+
current,
|
|
2019
|
+
total: conversations.length,
|
|
2020
|
+
imported: summary.imported,
|
|
2021
|
+
skipped: summary.skipped,
|
|
2022
|
+
errors: summary.errors.length
|
|
2023
|
+
});
|
|
2024
|
+
return;
|
|
1806
2025
|
}
|
|
1807
2026
|
const sourceType = conversation.sourceType || webConversationSourceType(provider, conversation);
|
|
1808
2027
|
const sessionId = webConversationSessionId(provider, account.accountId, conversation.id);
|
|
1809
2028
|
const fingerprint = webConversationFingerprint(sourceType, account.accountId, conversation);
|
|
1810
2029
|
if (alreadyImported(state, sessionId, fingerprint, archived, provider)) {
|
|
1811
2030
|
summary.skipped++;
|
|
1812
|
-
|
|
2031
|
+
reportWebImportProgress(options, provider, {
|
|
2032
|
+
current,
|
|
2033
|
+
total: conversations.length,
|
|
2034
|
+
imported: summary.imported,
|
|
2035
|
+
skipped: summary.skipped,
|
|
2036
|
+
errors: summary.errors.length
|
|
2037
|
+
});
|
|
2038
|
+
return;
|
|
1813
2039
|
}
|
|
1814
2040
|
const scopeCanonical = webConversationScope(provider, account.accountId, conversation.projectPath);
|
|
1815
2041
|
const displayPath = webConversationDisplayPath(account.displayName, conversation.projectPath);
|
|
@@ -1855,12 +2081,36 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
|
|
|
1855
2081
|
archived.add(archiveSessionKey(provider, sessionId));
|
|
1856
2082
|
}
|
|
1857
2083
|
summary.imported++;
|
|
1858
|
-
|
|
2084
|
+
reportWebImportProgress(options, provider, {
|
|
2085
|
+
current,
|
|
2086
|
+
total: conversations.length,
|
|
2087
|
+
imported: summary.imported,
|
|
2088
|
+
skipped: summary.skipped,
|
|
2089
|
+
errors: summary.errors.length
|
|
2090
|
+
});
|
|
2091
|
+
});
|
|
1859
2092
|
|
|
1860
2093
|
if (!options.dryRun) saveImportState(state, env);
|
|
2094
|
+
reportWebImportProgress(options, provider, {
|
|
2095
|
+
current: conversations.length,
|
|
2096
|
+
total: conversations.length,
|
|
2097
|
+
imported: summary.imported,
|
|
2098
|
+
skipped: summary.skipped,
|
|
2099
|
+
errors: summary.errors.length,
|
|
2100
|
+
message: `${options.dryRun ? "dry run complete" : "import complete"}: imported=${summary.imported} skipped=${summary.skipped}`
|
|
2101
|
+
});
|
|
1861
2102
|
return summary;
|
|
1862
2103
|
}
|
|
1863
2104
|
|
|
2105
|
+
function reportWebImportProgress(options, provider, event) {
|
|
2106
|
+
if (typeof options.onProgress !== "function") return;
|
|
2107
|
+
options.onProgress({
|
|
2108
|
+
kind: "import",
|
|
2109
|
+
provider: providerLabelForWeb(provider),
|
|
2110
|
+
...event
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
|
|
1864
2114
|
function importWindsurfTrajectoryExport(target, options = {}, env = process.env) {
|
|
1865
2115
|
const since = parseSince(options.since || "all");
|
|
1866
2116
|
return importStructuredProvider(
|
|
@@ -1880,7 +2130,8 @@ function readExportJson(file) {
|
|
|
1880
2130
|
return bundle.entries.map((entry) => entry.data);
|
|
1881
2131
|
}
|
|
1882
2132
|
|
|
1883
|
-
function readExportBundle(file) {
|
|
2133
|
+
function readExportBundle(file, provider = "") {
|
|
2134
|
+
if (Array.isArray(file)) return readExportBundleList(file, provider);
|
|
1884
2135
|
const resolved = path.resolve(file);
|
|
1885
2136
|
let stat;
|
|
1886
2137
|
try {
|
|
@@ -1888,31 +2139,96 @@ function readExportBundle(file) {
|
|
|
1888
2139
|
} catch (error) {
|
|
1889
2140
|
throw exportAccessError(resolved, error);
|
|
1890
2141
|
}
|
|
1891
|
-
const
|
|
2142
|
+
const bundle = stat.isDirectory()
|
|
2143
|
+
? readExportFolder(resolved, provider)
|
|
2144
|
+
: { entries: readExportFile(resolved, provider), rawFiles: [resolved] };
|
|
2145
|
+
const entries = bundle.entries;
|
|
1892
2146
|
if (!entries.length) {
|
|
1893
|
-
throw new Error(`${file} did not contain readable JSON, JSONL, or
|
|
2147
|
+
throw new Error(`${compactExportPath(file)} did not contain readable JSON, JSONL, NDJSON, or recognized nested export ZIP files. If this is an external path such as Downloads, Desktop, Documents, iCloud Drive, or an attached volume, make sure the terminal app running agentlog has permission to read it, unzip the relevant conversation part ZIPs, or copy the export into a readable folder and rerun.`);
|
|
1894
2148
|
}
|
|
1895
2149
|
return {
|
|
1896
2150
|
root: resolved,
|
|
1897
2151
|
kind: stat.isDirectory() ? "folder" : path.extname(resolved).toLowerCase() === ".zip" ? "zip" : "json",
|
|
1898
2152
|
entries,
|
|
2153
|
+
rawFiles: bundle.rawFiles || [],
|
|
2154
|
+
fingerprint: hashId(entries.map((entry) => `${entry.name}:${entry.sha256}`).sort().join("\n"))
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function readExportBundleList(files, provider = "") {
|
|
2159
|
+
const resolved = files.map((file) => path.resolve(file)).filter(Boolean);
|
|
2160
|
+
if (!resolved.length) throw new Error("no export paths provided");
|
|
2161
|
+
const bundles = resolved.map((file) => readExportBundle(file, provider));
|
|
2162
|
+
const entries = [];
|
|
2163
|
+
const rawFiles = [];
|
|
2164
|
+
for (const bundle of bundles) {
|
|
2165
|
+
const prefix = exportBundleMergePrefix(bundle.root, resolved);
|
|
2166
|
+
for (const entry of bundle.entries || []) entries.push(prefixExportEntry(entry, prefix));
|
|
2167
|
+
for (const rawFile of bundle.rawFiles || []) rawFiles.push(prefixExportRawFile(rawFile, prefix));
|
|
2168
|
+
}
|
|
2169
|
+
return {
|
|
2170
|
+
root: resolved.join(path.delimiter),
|
|
2171
|
+
kind: "multi",
|
|
2172
|
+
entries: entries.sort((a, b) => a.name.localeCompare(b.name)),
|
|
2173
|
+
rawFiles: rawFiles.sort((a, b) => a.name.localeCompare(b.name)),
|
|
1899
2174
|
fingerprint: hashId(entries.map((entry) => `${entry.name}:${entry.sha256}`).sort().join("\n"))
|
|
1900
2175
|
};
|
|
1901
2176
|
}
|
|
1902
2177
|
|
|
1903
|
-
function
|
|
2178
|
+
function exportBundleMergePrefix(root, roots) {
|
|
2179
|
+
if (roots.length <= 1) return "";
|
|
2180
|
+
const base = safeArchiveRelativePath(path.basename(root)) || hashId(root);
|
|
2181
|
+
return `${base}/`;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function prefixExportEntry(entry, prefix) {
|
|
2185
|
+
if (!prefix) return entry;
|
|
2186
|
+
return {
|
|
2187
|
+
...entry,
|
|
2188
|
+
name: `${prefix}${entry.name}`
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
function prefixExportRawFile(rawFile, prefix) {
|
|
2193
|
+
if (!prefix) return rawFile;
|
|
2194
|
+
return {
|
|
2195
|
+
...rawFile,
|
|
2196
|
+
name: `${prefix}${rawFile.name}`
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
function readExportFolder(root, provider = "") {
|
|
1904
2201
|
const entries = [];
|
|
2202
|
+
const rawFiles = [];
|
|
1905
2203
|
collectExportFiles(root, (file) => {
|
|
1906
|
-
|
|
2204
|
+
const relativeName = path.relative(root, file).split(path.sep).join("/");
|
|
2205
|
+
if (!ignoredExportRawFile(relativeName)) rawFiles.push(exportRawFile(root, file, relativeName));
|
|
2206
|
+
if (nestedExportZipEntryName(relativeName, provider)) {
|
|
2207
|
+
const zipPrefix = `${relativeName.replace(/\.zip$/i, "")}/`;
|
|
2208
|
+
entries.push(...readZipExportEntries(file, provider, {
|
|
2209
|
+
entryPrefix: zipPrefix,
|
|
2210
|
+
sourcePrefix: `${file}#`
|
|
2211
|
+
}));
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const namedJson = isExportEntryName(file);
|
|
2215
|
+
if (!namedJson && !looksLikeJsonPayload(file)) return;
|
|
1907
2216
|
let text;
|
|
1908
2217
|
try {
|
|
1909
2218
|
text = readExportText(file);
|
|
1910
2219
|
} catch (error) {
|
|
1911
2220
|
throw exportAccessError(root, error, file);
|
|
1912
2221
|
}
|
|
1913
|
-
|
|
2222
|
+
try {
|
|
2223
|
+
entries.push(exportEntry(relativeName, text, file));
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
if (namedJson) throw error;
|
|
2226
|
+
}
|
|
1914
2227
|
});
|
|
1915
|
-
return
|
|
2228
|
+
return {
|
|
2229
|
+
entries: entries.sort((a, b) => a.name.localeCompare(b.name)),
|
|
2230
|
+
rawFiles: rawFiles.sort((a, b) => a.name.localeCompare(b.name))
|
|
2231
|
+
};
|
|
1916
2232
|
}
|
|
1917
2233
|
|
|
1918
2234
|
function collectExportFiles(root, visit) {
|
|
@@ -1934,26 +2250,88 @@ function collectExportFiles(root, visit) {
|
|
|
1934
2250
|
|
|
1935
2251
|
function exportAccessError(root, error, target = root) {
|
|
1936
2252
|
const code = error?.code ? ` (${error.code})` : "";
|
|
1937
|
-
const message = error?.message || String(error || "unknown error");
|
|
2253
|
+
const message = compactExportPath(error?.message || String(error || "unknown error"));
|
|
1938
2254
|
const hint = "Grant Full Disk Access to Terminal/iTerm or the Node process running agentlog, import the original .zip file, or copy the export into a readable folder and rerun.";
|
|
1939
|
-
const wrapped = new Error(`Cannot read web export path ${target}${code}: ${message}. ${hint}`);
|
|
2255
|
+
const wrapped = new Error(`Cannot read web export path ${compactExportPath(target)}${code}: ${message}. ${hint}`);
|
|
1940
2256
|
wrapped.code = "AGENTLOG_WEB_EXPORT_ACCESS";
|
|
1941
2257
|
wrapped.causeCode = error?.code || "";
|
|
1942
2258
|
return wrapped;
|
|
1943
2259
|
}
|
|
1944
2260
|
|
|
1945
|
-
function
|
|
2261
|
+
function compactExportPath(value) {
|
|
2262
|
+
let text = String(value || "");
|
|
2263
|
+
const home = os.homedir();
|
|
2264
|
+
if (home) {
|
|
2265
|
+
const escaped = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2266
|
+
text = text.replace(new RegExp(escaped, "g"), "~");
|
|
2267
|
+
}
|
|
2268
|
+
return text.replace(/\/Users\/[^/\s'"]+/g, "~");
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
function readExportFile(file, provider = "") {
|
|
1946
2272
|
if (path.extname(file).toLowerCase() !== ".zip") {
|
|
1947
2273
|
return [exportEntry(path.basename(file), readExportText(file), file)];
|
|
1948
2274
|
}
|
|
2275
|
+
return readZipExportEntries(file, provider);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function readZipExportEntries(file, provider = "", context = {}) {
|
|
1949
2279
|
const list = spawnSync("unzip", ["-Z1", file], { encoding: "utf8" });
|
|
1950
2280
|
if (list.status !== 0) throw new Error("reading zip exports requires the `unzip` command");
|
|
1951
|
-
const names = list.stdout.split(/\r?\n/).filter(
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2281
|
+
const names = list.stdout.split(/\r?\n/).filter(Boolean);
|
|
2282
|
+
const entryPrefix = context.entryPrefix || "";
|
|
2283
|
+
const sourcePrefix = context.sourcePrefix || `${file}#`;
|
|
2284
|
+
const depth = context.depth || 0;
|
|
2285
|
+
const entries = [];
|
|
2286
|
+
for (const name of names.filter(isExportEntryName)) {
|
|
2287
|
+
const content = readZipEntryText(file, name);
|
|
2288
|
+
entries.push(exportEntry(`${entryPrefix}${name}`, content, `${sourcePrefix}${name}`));
|
|
2289
|
+
}
|
|
2290
|
+
if (depth < 2) {
|
|
2291
|
+
for (const name of names.filter((entry) => nestedExportZipEntryName(entry, provider))) {
|
|
2292
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentlog-nested-export-"));
|
|
2293
|
+
const tempZip = path.join(tempDir, safeArchiveRelativePath(path.basename(name)) || "nested.zip");
|
|
2294
|
+
try {
|
|
2295
|
+
extractZipEntryToFile(file, name, tempZip);
|
|
2296
|
+
const nestedPrefix = `${entryPrefix}${name.replace(/\.zip$/i, "")}/`;
|
|
2297
|
+
entries.push(...readZipExportEntries(tempZip, provider, {
|
|
2298
|
+
entryPrefix: nestedPrefix,
|
|
2299
|
+
sourcePrefix: `${sourcePrefix}${name}#`,
|
|
2300
|
+
depth: depth + 1
|
|
2301
|
+
}));
|
|
2302
|
+
} finally {
|
|
2303
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
function readZipEntryText(file, name) {
|
|
2311
|
+
const content = spawnSync("unzip", ["-p", file, name], { encoding: "utf8", maxBuffer: EXPORT_ZIP_ENTRY_MAX_BUFFER });
|
|
2312
|
+
if (content.status !== 0) throw new Error(`failed to read ${name} from zip`);
|
|
2313
|
+
return content.stdout;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
function extractZipEntryToFile(file, name, target) {
|
|
2317
|
+
ensureDir(path.dirname(target));
|
|
2318
|
+
const fd = fs.openSync(target, "w", 0o600);
|
|
2319
|
+
try {
|
|
2320
|
+
const result = spawnSync("unzip", ["-p", file, name], { stdio: ["ignore", fd, "pipe"] });
|
|
2321
|
+
if (result.status !== 0) {
|
|
2322
|
+
const message = result.stderr ? result.stderr.toString("utf8").trim() : "";
|
|
2323
|
+
throw new Error(`failed to extract ${name} from zip${message ? `: ${message}` : ""}`);
|
|
2324
|
+
}
|
|
2325
|
+
} finally {
|
|
2326
|
+
fs.closeSync(fd);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
function nestedExportZipEntryName(name, provider = "") {
|
|
2331
|
+
const text = String(name || "");
|
|
2332
|
+
if (!/\.zip$/i.test(text) || ignoredExportRawFile(text)) return false;
|
|
2333
|
+
if (provider === "chatgpt") return openAiConversationExportPath(text);
|
|
2334
|
+
return /(^|\/)Conversations__[^/]*\.zip$/i.test(text);
|
|
1957
2335
|
}
|
|
1958
2336
|
|
|
1959
2337
|
function exportEntry(name, text, sourcePath) {
|
|
@@ -1969,6 +2347,16 @@ function exportEntry(name, text, sourcePath) {
|
|
|
1969
2347
|
};
|
|
1970
2348
|
}
|
|
1971
2349
|
|
|
2350
|
+
function exportRawFile(root, file, name = path.relative(root, file).split(path.sep).join("/")) {
|
|
2351
|
+
const stat = safeStat(file);
|
|
2352
|
+
return {
|
|
2353
|
+
name,
|
|
2354
|
+
sourcePath: file,
|
|
2355
|
+
size: stat?.size || 0,
|
|
2356
|
+
mtime: stat?.mtimeMs ? new Date(stat.mtimeMs).toISOString() : ""
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
|
|
1972
2360
|
function readExportText(file) {
|
|
1973
2361
|
const buffer = fs.readFileSync(file);
|
|
1974
2362
|
const bytes = file.toLowerCase().endsWith(".gz") ? zlib.gunzipSync(buffer) : buffer;
|
|
@@ -1999,11 +2387,6 @@ function parseJsonLines(text, name = "", options = {}) {
|
|
|
1999
2387
|
return rows;
|
|
2000
2388
|
}
|
|
2001
2389
|
|
|
2002
|
-
function isExportDataFile(file) {
|
|
2003
|
-
if (isExportEntryName(file)) return true;
|
|
2004
|
-
return looksLikeJsonPayload(file);
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
2390
|
function isExportEntryName(name) {
|
|
2008
2391
|
const lower = String(name || "").toLowerCase();
|
|
2009
2392
|
return /\.(json|jsonl|ndjson)(\.gz)?$/.test(lower);
|
|
@@ -2031,6 +2414,12 @@ function looksLikeJsonPayload(file) {
|
|
|
2031
2414
|
}
|
|
2032
2415
|
}
|
|
2033
2416
|
|
|
2417
|
+
function ignoredExportRawFile(name) {
|
|
2418
|
+
return String(name || "")
|
|
2419
|
+
.split(/[\\/]+/)
|
|
2420
|
+
.some((part) => part === ".DS_Store" || part === "__MACOSX" || part.startsWith("._"));
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2034
2423
|
function normalizeWebConversations(provider, source, account) {
|
|
2035
2424
|
if (provider === "chatgpt") return { conversations: normalizeChatGptExport(source, account) };
|
|
2036
2425
|
return { conversations: normalizeClaudeWebExport(source, account) };
|
|
@@ -2041,7 +2430,7 @@ function normalizeChatGptExport(source) {
|
|
|
2041
2430
|
return entries.flatMap((entry) => chatgptRawConversations(entry.data).map((conversation, index) => {
|
|
2042
2431
|
const id = firstString(conversation.id, conversation.uuid, conversation.conversation_id) || `chatgpt-${hashId(`${entry.name}:${index}`)}`;
|
|
2043
2432
|
const title = firstString(conversation.title, conversation.name, conversation.summary) || "ChatGPT conversation";
|
|
2044
|
-
const messages = chatgptMessages(conversation).filter(
|
|
2433
|
+
const messages = chatgptMessages(conversation).filter(chatgptMessageHasDisplayContent);
|
|
2045
2434
|
const sorted = sortConversationMessages(messages);
|
|
2046
2435
|
return {
|
|
2047
2436
|
id,
|
|
@@ -2053,34 +2442,41 @@ function normalizeChatGptExport(source) {
|
|
|
2053
2442
|
projectPath: "",
|
|
2054
2443
|
entryPath: entry.name,
|
|
2055
2444
|
sourceType: "chatgpt-export",
|
|
2056
|
-
kind: "conversation"
|
|
2445
|
+
kind: "conversation",
|
|
2446
|
+
sessionSummary: chatgptSessionSummary(conversation)
|
|
2057
2447
|
};
|
|
2058
2448
|
}));
|
|
2059
2449
|
}
|
|
2060
2450
|
|
|
2061
2451
|
function chatConversationEntries(source) {
|
|
2062
|
-
const preferred = source.entries.filter((entry) => /(^|\/)conversations
|
|
2063
|
-
return preferred.length ? preferred : source.entries.filter((entry) =>
|
|
2452
|
+
const preferred = source.entries.filter((entry) => /(^|\/)conversations(?:-\d+)?\.json$/i.test(entry.name));
|
|
2453
|
+
return preferred.length ? preferred : source.entries.filter((entry) => chatgptRawConversations(entry.data).length);
|
|
2064
2454
|
}
|
|
2065
2455
|
|
|
2066
2456
|
function chatgptRawConversations(data) {
|
|
2067
|
-
if (Array.isArray(data)) return data;
|
|
2068
|
-
if (Array.isArray(data?.conversations)) return data.conversations;
|
|
2457
|
+
if (Array.isArray(data)) return data.filter(chatgptLooksLikeConversation);
|
|
2458
|
+
if (Array.isArray(data?.conversations)) return data.conversations.filter(chatgptLooksLikeConversation);
|
|
2069
2459
|
if (data?.mapping || Array.isArray(data?.messages)) return [data];
|
|
2070
2460
|
return [];
|
|
2071
2461
|
}
|
|
2072
2462
|
|
|
2463
|
+
function chatgptLooksLikeConversation(value) {
|
|
2464
|
+
return Boolean(value && typeof value === "object" && (value.mapping || Array.isArray(value.messages) || value.conversation_id || value.current_node));
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2073
2467
|
function chatgptMessages(conversation) {
|
|
2074
2468
|
if (conversation.mapping && typeof conversation.mapping === "object") {
|
|
2075
2469
|
const nodes = chatgptMainPathNodes(conversation);
|
|
2076
2470
|
return nodes.map((node) => node && node.message).filter(Boolean).map((message) => {
|
|
2077
2471
|
const role = normalizeEventRole(message.author?.role) || "unknown";
|
|
2078
|
-
const content = extractChatGptContent(message
|
|
2472
|
+
const content = extractChatGptContent(message);
|
|
2473
|
+
const toolCall = chatgptToolCallFromMessage(message, content);
|
|
2474
|
+
const toolResult = role === "tool" ? chatgptToolResultFromMessage(message, content) : null;
|
|
2079
2475
|
return {
|
|
2080
2476
|
role,
|
|
2081
|
-
content,
|
|
2477
|
+
content: toolCall ? "" : content,
|
|
2082
2478
|
timestamp: toIso(message.create_time || message.update_time),
|
|
2083
|
-
metadata: chatgptMessageMetadata(message, role, content)
|
|
2479
|
+
metadata: chatgptMessageMetadata(message, role, content, { toolCall, toolResult })
|
|
2084
2480
|
};
|
|
2085
2481
|
});
|
|
2086
2482
|
}
|
|
@@ -2104,22 +2500,266 @@ function chatgptMainPathNodes(conversation) {
|
|
|
2104
2500
|
);
|
|
2105
2501
|
}
|
|
2106
2502
|
|
|
2107
|
-
function extractChatGptContent(
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
if (Array.isArray(content.
|
|
2111
|
-
|
|
2112
|
-
|
|
2503
|
+
function extractChatGptContent(message) {
|
|
2504
|
+
const content = message?.content || {};
|
|
2505
|
+
const parts = [];
|
|
2506
|
+
if (Array.isArray(content.parts)) {
|
|
2507
|
+
for (const part of content.parts) {
|
|
2508
|
+
const text = chatgptContentPartText(part);
|
|
2509
|
+
if (text) parts.push(text);
|
|
2510
|
+
}
|
|
2511
|
+
} else if (Array.isArray(content.text)) {
|
|
2512
|
+
parts.push(extractText(content.text));
|
|
2513
|
+
} else if (typeof content.text === "string") {
|
|
2514
|
+
parts.push(content.text);
|
|
2515
|
+
} else {
|
|
2516
|
+
parts.push(extractText(content));
|
|
2517
|
+
}
|
|
2518
|
+
return parts.filter((part) => String(part || "").trim()).join("\n").trim();
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function chatgptContentPartText(part) {
|
|
2522
|
+
if (typeof part === "string") return part;
|
|
2523
|
+
if (!part || typeof part !== "object") return extractText(part);
|
|
2524
|
+
const text = extractText(part);
|
|
2525
|
+
if (text) return text;
|
|
2526
|
+
const contentType = firstString(part.content_type, part.type, "asset");
|
|
2527
|
+
const pointer = firstString(part.asset_pointer, part.assetPointer, part.file_id, part.fileId);
|
|
2528
|
+
if (pointer || /image|file|asset/i.test(contentType)) {
|
|
2529
|
+
return "";
|
|
2530
|
+
}
|
|
2531
|
+
return "";
|
|
2113
2532
|
}
|
|
2114
2533
|
|
|
2115
|
-
function chatgptMessageMetadata(message, role, content) {
|
|
2534
|
+
function chatgptMessageMetadata(message, role, content, options = {}) {
|
|
2535
|
+
const assetPointers = chatgptAssetPointers(message);
|
|
2536
|
+
const attachments = chatgptNormalizedAttachments(message);
|
|
2116
2537
|
return webWithUsage({
|
|
2117
2538
|
source: "chatgpt-export",
|
|
2118
2539
|
messageId: message.id || undefined,
|
|
2119
|
-
model: firstString(message.metadata?.model_slug, message.metadata?.model, message.metadata?.default_model_slug) || undefined
|
|
2540
|
+
model: firstString(message.metadata?.model_slug, message.metadata?.model, message.metadata?.default_model_slug) || undefined,
|
|
2541
|
+
contentType: firstString(message.content?.content_type) || undefined,
|
|
2542
|
+
channel: firstString(message.channel) || undefined,
|
|
2543
|
+
recipient: firstString(message.recipient) || undefined,
|
|
2544
|
+
status: firstString(message.status) || undefined,
|
|
2545
|
+
attachments: chatgptAttachAssetPointers(attachments, assetPointers),
|
|
2546
|
+
assetPointers,
|
|
2547
|
+
toolCalls: options.toolCall ? [options.toolCall] : undefined,
|
|
2548
|
+
toolResult: options.toolResult || undefined
|
|
2120
2549
|
}, webMessageUsage(message, role, { inputText: content, outputText: content }));
|
|
2121
2550
|
}
|
|
2122
2551
|
|
|
2552
|
+
function chatgptSessionSummary(conversation) {
|
|
2553
|
+
const summary = firstString(conversation.summary);
|
|
2554
|
+
if (!summary) return undefined;
|
|
2555
|
+
return {
|
|
2556
|
+
summary,
|
|
2557
|
+
source: "chatgpt-export",
|
|
2558
|
+
summaryKind: "conversation_summary"
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
function chatgptMessageHasDisplayContent(message) {
|
|
2563
|
+
if (!message || typeof message !== "object") return false;
|
|
2564
|
+
if (String(message.content || "").trim()) return true;
|
|
2565
|
+
const metadata = message.metadata || {};
|
|
2566
|
+
return Boolean(
|
|
2567
|
+
(Array.isArray(metadata.attachments) && metadata.attachments.length) ||
|
|
2568
|
+
(Array.isArray(metadata.assetPointers) && metadata.assetPointers.length) ||
|
|
2569
|
+
(Array.isArray(metadata.toolCalls) && metadata.toolCalls.length) ||
|
|
2570
|
+
metadata.toolResult
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function chatgptToolCallFromMessage(message, content) {
|
|
2575
|
+
const recipient = firstString(message?.recipient);
|
|
2576
|
+
if (!recipient || recipient === "all" || recipient === "assistant") return null;
|
|
2577
|
+
const rawInput = String(content || "").trim();
|
|
2578
|
+
if (!rawInput) return null;
|
|
2579
|
+
const args = parseToolArgsValue(rawInput);
|
|
2580
|
+
const name = chatgptToolName(recipient, args);
|
|
2581
|
+
const summary = summarizeToolArguments(args) || rawInput.slice(0, 240);
|
|
2582
|
+
return compactObject({
|
|
2583
|
+
provider: "chatgpt",
|
|
2584
|
+
name,
|
|
2585
|
+
displayName: toolDisplayName(name),
|
|
2586
|
+
category: chatgptToolCategory(recipient, name),
|
|
2587
|
+
rawCategory: "chatgpt_tool_call",
|
|
2588
|
+
title: toolDisplayName(name),
|
|
2589
|
+
status: "tool_call",
|
|
2590
|
+
argument: summary,
|
|
2591
|
+
rawInputSummary: summary,
|
|
2592
|
+
inputPreview: chatgptToolInputPreview(args, rawInput),
|
|
2593
|
+
arguments: args && typeof args === "object" && !Array.isArray(args) ? args : undefined
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
function chatgptToolName(recipient, args) {
|
|
2598
|
+
const base = String(recipient || "tool").trim();
|
|
2599
|
+
if (base === "web.run" && args && typeof args === "object" && !Array.isArray(args)) {
|
|
2600
|
+
if (args.search_query || args.searchQuery) return "web.search";
|
|
2601
|
+
if (args.open) return "web.open";
|
|
2602
|
+
if (args.image_query || args.imageQuery) return "web.image_search";
|
|
2603
|
+
if (args.finance) return "web.finance";
|
|
2604
|
+
if (args.weather) return "web.weather";
|
|
2605
|
+
if (args.sports) return "web.sports";
|
|
2606
|
+
if (args.time) return "web.time";
|
|
2607
|
+
}
|
|
2608
|
+
return base || "tool";
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
function chatgptToolCategory(recipient, name) {
|
|
2612
|
+
const text = `${recipient || ""} ${name || ""}`.toLowerCase();
|
|
2613
|
+
if (text.includes("web.")) return "web";
|
|
2614
|
+
return "";
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
function chatgptToolInputPreview(args, rawInput) {
|
|
2618
|
+
if (!args || typeof args !== "object") return rawInput.slice(0, 2000);
|
|
2619
|
+
try {
|
|
2620
|
+
return JSON.stringify(args, null, 2).slice(0, 2000);
|
|
2621
|
+
} catch {
|
|
2622
|
+
return rawInput.slice(0, 2000);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
function chatgptToolResultFromMessage(message, content) {
|
|
2627
|
+
const text = String(content || "").trim();
|
|
2628
|
+
if (!text) return null;
|
|
2629
|
+
const parsedFile = chatgptParsedFileToolResult(text);
|
|
2630
|
+
if (parsedFile) return parsedFile;
|
|
2631
|
+
if (/^All the files uploaded by the user have been fully loaded\./i.test(text)) {
|
|
2632
|
+
return compactObject({
|
|
2633
|
+
provider: "chatgpt",
|
|
2634
|
+
kind: "Uploaded files loaded",
|
|
2635
|
+
title: "Uploaded files loaded",
|
|
2636
|
+
category: "read",
|
|
2637
|
+
categoryLabel: "Files",
|
|
2638
|
+
rawCategory: "chatgpt_file_tool",
|
|
2639
|
+
summary: firstLine(text),
|
|
2640
|
+
output: text,
|
|
2641
|
+
lineCount: text.split("\n").length,
|
|
2642
|
+
collapsed: false,
|
|
2643
|
+
status: "completed"
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
return null;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
function chatgptParsedFileToolResult(text) {
|
|
2650
|
+
if (!/<PARSED TEXT FOR PAGE:\s*\d+\s*\/\s*\d+>/i.test(text)) return null;
|
|
2651
|
+
const pageMatches = [...text.matchAll(/<PARSED TEXT FOR PAGE:\s*(\d+)\s*\/\s*(\d+)>/gi)];
|
|
2652
|
+
const pageCount = pageMatches.reduce((max, match) => Math.max(max, Number(match[2]) || 0), 0);
|
|
2653
|
+
const citationMatch = text.match(/\uE200filecite\uE202([^\uE201]+)\uE201/i);
|
|
2654
|
+
const citation = citationMatch ? chatgptCitationText(citationMatch[1]) : "";
|
|
2655
|
+
const detail = [citation, pageCount ? `${pageCount} page${pageCount === 1 ? "" : "s"}` : ""].filter(Boolean).join(" · ");
|
|
2656
|
+
return compactObject({
|
|
2657
|
+
provider: "chatgpt",
|
|
2658
|
+
kind: "Parsed uploaded file",
|
|
2659
|
+
title: "Parsed uploaded file",
|
|
2660
|
+
category: "read",
|
|
2661
|
+
categoryLabel: "Files",
|
|
2662
|
+
rawCategory: "chatgpt_file_tool",
|
|
2663
|
+
summary: detail ? `Parsed uploaded file text · ${detail}` : "Parsed uploaded file text",
|
|
2664
|
+
output: text,
|
|
2665
|
+
lineCount: text.split("\n").length,
|
|
2666
|
+
collapsed: true,
|
|
2667
|
+
status: "completed"
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
function chatgptCitationText(value) {
|
|
2672
|
+
return String(value || "").split("\uE202").map((part) => part.trim()).filter(Boolean).join(" ");
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
function chatgptNormalizedAttachments(message) {
|
|
2676
|
+
const attachments = Array.isArray(message?.metadata?.attachments) ? message.metadata.attachments : [];
|
|
2677
|
+
const normalized = attachments.map((attachment) => {
|
|
2678
|
+
if (!attachment || typeof attachment !== "object") return null;
|
|
2679
|
+
return compactObject({
|
|
2680
|
+
id: firstString(attachment.id, attachment.file_id, attachment.fileId),
|
|
2681
|
+
name: firstString(attachment.name, attachment.filename, attachment.file_name),
|
|
2682
|
+
mimeType: firstString(attachment.mime_type, attachment.mimeType),
|
|
2683
|
+
size: finiteNumber(attachment.size, attachment.size_bytes, attachment.sizeBytes),
|
|
2684
|
+
width: finiteNumber(attachment.width),
|
|
2685
|
+
height: finiteNumber(attachment.height),
|
|
2686
|
+
source: firstString(attachment.source),
|
|
2687
|
+
libraryFileId: firstString(attachment.library_file_id, attachment.libraryFileId)
|
|
2688
|
+
});
|
|
2689
|
+
}).filter((attachment) => attachment && Object.keys(attachment).length);
|
|
2690
|
+
return normalized.length ? normalized : undefined;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
function chatgptAssetPointers(message) {
|
|
2694
|
+
const parts = Array.isArray(message?.content?.parts) ? message.content.parts : [];
|
|
2695
|
+
const pointers = parts
|
|
2696
|
+
.filter((part) => part && typeof part === "object" && (part.asset_pointer || part.assetPointer))
|
|
2697
|
+
.map((part) => compactObject({
|
|
2698
|
+
assetPointer: firstString(part.asset_pointer, part.assetPointer),
|
|
2699
|
+
contentType: firstString(part.content_type, part.type),
|
|
2700
|
+
mimeType: firstString(part.mime_type, part.mimeType),
|
|
2701
|
+
width: finiteNumber(part.width),
|
|
2702
|
+
height: finiteNumber(part.height),
|
|
2703
|
+
size: finiteNumber(part.size_bytes, part.sizeBytes)
|
|
2704
|
+
}))
|
|
2705
|
+
.filter((pointer) => Object.keys(pointer).length);
|
|
2706
|
+
return pointers.length ? pointers : undefined;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
function chatgptAttachAssetPointers(attachments, assetPointers) {
|
|
2710
|
+
if (!Array.isArray(attachments) || !attachments.length) return undefined;
|
|
2711
|
+
if (!Array.isArray(assetPointers) || !assetPointers.length) return attachments;
|
|
2712
|
+
return attachments.map((attachment, index) => {
|
|
2713
|
+
const pointer = chatgptAssetPointerForAttachment(attachment, assetPointers, index, attachments.length);
|
|
2714
|
+
if (!pointer) return attachment;
|
|
2715
|
+
return compactObject({
|
|
2716
|
+
...attachment,
|
|
2717
|
+
assetPointer: pointer.assetPointer,
|
|
2718
|
+
pointerContentType: pointer.contentType,
|
|
2719
|
+
pointerMimeType: pointer.mimeType
|
|
2720
|
+
});
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
function chatgptAssetPointerForAttachment(attachment, assetPointers, index, attachmentCount) {
|
|
2725
|
+
const id = normalizeAttachmentToken(attachment?.id || "");
|
|
2726
|
+
if (id) {
|
|
2727
|
+
const match = assetPointers.find((pointer) => normalizeAttachmentToken(pointer.assetPointer || "").endsWith(id));
|
|
2728
|
+
if (match) return match;
|
|
2729
|
+
}
|
|
2730
|
+
if (attachmentCount === assetPointers.length) return assetPointers[index] || null;
|
|
2731
|
+
if (attachmentCount === 1 && assetPointers.length === 1) return assetPointers[0];
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
function normalizeAttachmentToken(value) {
|
|
2736
|
+
return String(value || "")
|
|
2737
|
+
.trim()
|
|
2738
|
+
.toLowerCase()
|
|
2739
|
+
.replace(/^file-service:\/\//, "")
|
|
2740
|
+
.replace(/^sandbox:\/\//, "")
|
|
2741
|
+
.replace(/^attachment:\/\//, "")
|
|
2742
|
+
.replace(/[?#].*$/, "")
|
|
2743
|
+
.split(/[\\/]+/)
|
|
2744
|
+
.filter(Boolean)
|
|
2745
|
+
.pop() || "";
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
function compactObject(input) {
|
|
2749
|
+
const output = {};
|
|
2750
|
+
for (const [key, value] of Object.entries(input || {})) {
|
|
2751
|
+
if (value === undefined || value === null || value === "") continue;
|
|
2752
|
+
if (Array.isArray(value) && !value.length) continue;
|
|
2753
|
+
output[key] = value;
|
|
2754
|
+
}
|
|
2755
|
+
return output;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
function finiteNumber(...values) {
|
|
2759
|
+
const value = numericValue(...values);
|
|
2760
|
+
return Number.isFinite(value) ? value : undefined;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2123
2763
|
function webWithUsage(metadata, usage) {
|
|
2124
2764
|
if (!usage) return metadata;
|
|
2125
2765
|
return { ...metadata, usage };
|
|
@@ -2711,12 +3351,19 @@ function webConversationSourcePath(source, conversation) {
|
|
|
2711
3351
|
}
|
|
2712
3352
|
|
|
2713
3353
|
function inferWebSourceAccountId(provider, source) {
|
|
3354
|
+
const accountMetadataName = provider === "chatgpt"
|
|
3355
|
+
? /(user|account|profile)/
|
|
3356
|
+
: /(user|account|profile|organization|export|memories)/;
|
|
2714
3357
|
for (const entry of source.entries) {
|
|
2715
3358
|
const name = entry.name.toLowerCase();
|
|
2716
|
-
if (
|
|
3359
|
+
if (!accountMetadataName.test(name) || /(^|\/)conversations(?:-\d+)?\.json$/i.test(name)) continue;
|
|
2717
3360
|
const id = webAccountIdFromData(entry.data);
|
|
2718
3361
|
if (id) return id;
|
|
2719
3362
|
}
|
|
3363
|
+
if (provider === "chatgpt") {
|
|
3364
|
+
const id = openAiConversationExportAccountId(source);
|
|
3365
|
+
if (id) return id;
|
|
3366
|
+
}
|
|
2720
3367
|
if (provider === "claude_web") {
|
|
2721
3368
|
const match = path.basename(source.root).match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
|
|
2722
3369
|
if (match) return match[0].toLowerCase();
|
|
@@ -2724,6 +3371,19 @@ function inferWebSourceAccountId(provider, source) {
|
|
|
2724
3371
|
return "";
|
|
2725
3372
|
}
|
|
2726
3373
|
|
|
3374
|
+
function openAiConversationExportAccountId(source) {
|
|
3375
|
+
const values = [
|
|
3376
|
+
source.root,
|
|
3377
|
+
...(Array.isArray(source.entries) ? source.entries.map((entry) => entry.name) : []),
|
|
3378
|
+
...(Array.isArray(source.rawFiles) ? source.rawFiles.map((file) => file.name) : [])
|
|
3379
|
+
];
|
|
3380
|
+
for (const value of values) {
|
|
3381
|
+
const match = String(value || "").match(/Conversations__([A-Za-z0-9]+)-chatgpt-\d+/i);
|
|
3382
|
+
if (match) return match[1];
|
|
3383
|
+
}
|
|
3384
|
+
return "";
|
|
3385
|
+
}
|
|
3386
|
+
|
|
2727
3387
|
function inferWebUsername(provider, source) {
|
|
2728
3388
|
for (const entry of source.entries) {
|
|
2729
3389
|
const name = entry.name.toLowerCase();
|
|
@@ -2768,10 +3428,15 @@ function providerLabelForWeb(provider) {
|
|
|
2768
3428
|
return provider === "chatgpt" ? "ChatGPT" : "Claude.ai";
|
|
2769
3429
|
}
|
|
2770
3430
|
|
|
2771
|
-
function ensureSharedWebExportRaw(provider, source, account, env = process.env) {
|
|
3431
|
+
function ensureSharedWebExportRaw(provider, source, account, env = process.env, options = {}) {
|
|
2772
3432
|
const root = path.join(archiveRoot(env), "raw", "web-exports", provider, account.accountId, source.fingerprint);
|
|
2773
3433
|
const manifestPath = path.join(root, "manifest.json");
|
|
2774
3434
|
if (fs.existsSync(manifestPath)) {
|
|
3435
|
+
reportWebImportProgress(options, provider, {
|
|
3436
|
+
current: 1,
|
|
3437
|
+
total: 1,
|
|
3438
|
+
message: "raw export already preserved"
|
|
3439
|
+
});
|
|
2775
3440
|
return { root, manifestPath, sha256: source.fingerprint };
|
|
2776
3441
|
}
|
|
2777
3442
|
ensureDir(root);
|
|
@@ -2779,6 +3444,11 @@ function ensureSharedWebExportRaw(provider, source, account, env = process.env)
|
|
|
2779
3444
|
const records = [];
|
|
2780
3445
|
let containerPath = "";
|
|
2781
3446
|
if (source.kind === "zip" || source.kind === "json") {
|
|
3447
|
+
reportWebImportProgress(options, provider, {
|
|
3448
|
+
current: 0,
|
|
3449
|
+
total: 1,
|
|
3450
|
+
message: "preserving raw export"
|
|
3451
|
+
});
|
|
2782
3452
|
const extension = source.kind === "zip" ? ".zip" : path.extname(source.root) || ".json";
|
|
2783
3453
|
containerPath = path.join(root, `source${extension}`);
|
|
2784
3454
|
fs.copyFileSync(source.root, containerPath);
|
|
@@ -2789,22 +3459,55 @@ function ensureSharedWebExportRaw(provider, source, account, env = process.env)
|
|
|
2789
3459
|
size: stat?.size || 0,
|
|
2790
3460
|
sha256: fileSha256(containerPath)
|
|
2791
3461
|
});
|
|
3462
|
+
reportWebImportProgress(options, provider, {
|
|
3463
|
+
current: 1,
|
|
3464
|
+
total: 1,
|
|
3465
|
+
message: "preserved raw export"
|
|
3466
|
+
});
|
|
2792
3467
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
3468
|
+
const rawFiles = webExportRawFilesForProvider(source, provider);
|
|
3469
|
+
const entryBySourcePath = new Map(source.entries.map((entry) => [path.resolve(entry.sourcePath), entry]));
|
|
3470
|
+
const copiedRaw = new Set();
|
|
3471
|
+
if (source.kind === "folder" || source.kind === "multi") {
|
|
3472
|
+
let rawIndex = 0;
|
|
3473
|
+
for (const rawFile of rawFiles) {
|
|
3474
|
+
rawIndex++;
|
|
3475
|
+
if (!rawFile?.sourcePath || !fs.existsSync(rawFile.sourcePath)) continue;
|
|
3476
|
+
const resolved = path.resolve(rawFile.sourcePath);
|
|
3477
|
+
if (copiedRaw.has(resolved)) continue;
|
|
3478
|
+
copiedRaw.add(resolved);
|
|
3479
|
+
const archivedPath = path.join(root, "entries", safeArchiveRelativePath(rawFile.name));
|
|
2797
3480
|
ensureDir(path.dirname(archivedPath));
|
|
2798
|
-
fs.copyFileSync(
|
|
3481
|
+
fs.copyFileSync(rawFile.sourcePath, archivedPath);
|
|
3482
|
+
const stat = safeStat(archivedPath);
|
|
3483
|
+
const parsedEntry = entryBySourcePath.get(resolved);
|
|
3484
|
+
records.push({
|
|
3485
|
+
entryPath: rawFile.name,
|
|
3486
|
+
originalPath: rawFile.sourcePath,
|
|
3487
|
+
archivedPath,
|
|
3488
|
+
size: stat?.size || rawFile.size || 0,
|
|
3489
|
+
mtime: rawFile.mtime || (stat?.mtimeMs ? new Date(stat.mtimeMs).toISOString() : undefined),
|
|
3490
|
+
sha256: parsedEntry?.sha256 || fileSha256(archivedPath),
|
|
3491
|
+
parsed: Boolean(parsedEntry) || undefined
|
|
3492
|
+
});
|
|
3493
|
+
reportWebImportProgress(options, provider, {
|
|
3494
|
+
current: rawIndex,
|
|
3495
|
+
total: rawFiles.length,
|
|
3496
|
+
message: `preserving raw export files: ${rawIndex}/${rawFiles.length}`
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
} else {
|
|
3500
|
+
for (const entry of source.entries) {
|
|
3501
|
+
records.push({
|
|
3502
|
+
entryPath: entry.name,
|
|
3503
|
+
originalPath: entry.sourcePath,
|
|
3504
|
+
archivedPath: containerPath,
|
|
3505
|
+
containerPath: containerPath || undefined,
|
|
3506
|
+
size: entry.size,
|
|
3507
|
+
sha256: entry.sha256,
|
|
3508
|
+
parsed: true
|
|
3509
|
+
});
|
|
2799
3510
|
}
|
|
2800
|
-
records.push({
|
|
2801
|
-
entryPath: entry.name,
|
|
2802
|
-
originalPath: entry.sourcePath,
|
|
2803
|
-
archivedPath,
|
|
2804
|
-
containerPath: containerPath || undefined,
|
|
2805
|
-
size: entry.size,
|
|
2806
|
-
sha256: entry.sha256
|
|
2807
|
-
});
|
|
2808
3511
|
}
|
|
2809
3512
|
writeJson(manifestPath, {
|
|
2810
3513
|
version: 1,
|
|
@@ -2820,6 +3523,20 @@ function ensureSharedWebExportRaw(provider, source, account, env = process.env)
|
|
|
2820
3523
|
return { root, manifestPath, sha256: source.fingerprint };
|
|
2821
3524
|
}
|
|
2822
3525
|
|
|
3526
|
+
function webExportRawFilesForProvider(source, provider) {
|
|
3527
|
+
const rawFiles = Array.isArray(source.rawFiles) ? source.rawFiles : [];
|
|
3528
|
+
if (!rawFiles.length) return [];
|
|
3529
|
+
if (provider !== "chatgpt") return rawFiles;
|
|
3530
|
+
const hasOpenAiConversationParts = rawFiles.some((file) => openAiConversationExportPath(file.name));
|
|
3531
|
+
if (!hasOpenAiConversationParts) return rawFiles;
|
|
3532
|
+
return rawFiles.filter((file) => openAiConversationExportPath(file.name));
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
function openAiConversationExportPath(name) {
|
|
3536
|
+
const text = String(name || "");
|
|
3537
|
+
return /(^|\/)Conversations__[^/]*chatgpt[^/]*(?:\/|$)/i.test(text) || /(^|\/)Conversations__[^/]*chatgpt[^/]*\.zip$/i.test(text);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
2823
3540
|
function webRawReference(sharedRaw, conversation) {
|
|
2824
3541
|
return {
|
|
2825
3542
|
filename: "web-export-reference",
|
|
@@ -3191,6 +3908,450 @@ function claudeFileHistoryFiles(sessionId, env = process.env) {
|
|
|
3191
3908
|
return files;
|
|
3192
3909
|
}
|
|
3193
3910
|
|
|
3911
|
+
function claudeSubagentImportContext(cwd, env = process.env, cache = null) {
|
|
3912
|
+
const key = `${claudeSubagentUserRoot(env)}\0${path.resolve(String(cwd || ""))}`;
|
|
3913
|
+
if (cache?.has(key)) return cache.get(key);
|
|
3914
|
+
const definitions = readClaudeSubagentDefinitions(cwd, env);
|
|
3915
|
+
const effective = effectiveClaudeSubagentDefinitions(definitions);
|
|
3916
|
+
const sourceFiles = definitions.map((definition) => definition.sourcePath).filter(Boolean);
|
|
3917
|
+
const projectCount = effective.filter((definition) => definition.scope === "project").length;
|
|
3918
|
+
const userCount = effective.filter((definition) => definition.scope === "user").length;
|
|
3919
|
+
const names = effective.map((definition) => definition.name).sort((a, b) => a.localeCompare(b));
|
|
3920
|
+
const shadowedNames = shadowedClaudeSubagentNames(definitions);
|
|
3921
|
+
const sessionSummary = effective.length
|
|
3922
|
+
? {
|
|
3923
|
+
claudeSubagents: compactMetadata({
|
|
3924
|
+
count: effective.length,
|
|
3925
|
+
parsedCount: definitions.length !== effective.length ? definitions.length : undefined,
|
|
3926
|
+
projectCount: projectCount || undefined,
|
|
3927
|
+
userCount: userCount || undefined,
|
|
3928
|
+
shadowedNames: shadowedNames.length ? shadowedNames : undefined,
|
|
3929
|
+
names,
|
|
3930
|
+
definitions: effective.map(claudeSubagentDefinitionSummary)
|
|
3931
|
+
})
|
|
3932
|
+
}
|
|
3933
|
+
: null;
|
|
3934
|
+
const result = { sessionSummary, sourceFiles };
|
|
3935
|
+
if (cache) cache.set(key, result);
|
|
3936
|
+
return result;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
function readClaudeSubagentDefinitions(cwd, env = process.env) {
|
|
3940
|
+
return claudeSubagentDefinitionFiles(cwd, env)
|
|
3941
|
+
.map((entry) => parseClaudeSubagentDefinitionFile(entry.file, entry))
|
|
3942
|
+
.filter(Boolean)
|
|
3943
|
+
.sort((a, b) => {
|
|
3944
|
+
const scope = subagentScopePriority(a.scope) - subagentScopePriority(b.scope);
|
|
3945
|
+
return scope || a.name.localeCompare(b.name) || a.sourcePath.localeCompare(b.sourcePath);
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
function claudeSubagentDefinitionFiles(cwd, env = process.env) {
|
|
3950
|
+
const roots = [];
|
|
3951
|
+
const userRoot = claudeSubagentUserRoot(env);
|
|
3952
|
+
if (safeStat(userRoot)?.isDirectory()) roots.push({ root: userRoot, scope: "user" });
|
|
3953
|
+
const projectRoot = findClaudeProjectSubagentRoot(cwd);
|
|
3954
|
+
if (projectRoot && path.resolve(projectRoot) !== path.resolve(userRoot)) {
|
|
3955
|
+
roots.push({ root: projectRoot, scope: "project" });
|
|
3956
|
+
}
|
|
3957
|
+
const seen = new Set();
|
|
3958
|
+
const files = [];
|
|
3959
|
+
for (const root of roots) {
|
|
3960
|
+
collectFiles(root.root, (file) => {
|
|
3961
|
+
if (!/\.md$/i.test(file)) return;
|
|
3962
|
+
const resolved = path.resolve(file);
|
|
3963
|
+
if (seen.has(resolved)) return;
|
|
3964
|
+
seen.add(resolved);
|
|
3965
|
+
files.push({ ...root, file: resolved });
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
return files.sort((a, b) => {
|
|
3969
|
+
const scope = subagentScopePriority(a.scope) - subagentScopePriority(b.scope);
|
|
3970
|
+
return scope || a.file.localeCompare(b.file);
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
function parseClaudeSubagentDefinitionFile(file, context = {}) {
|
|
3975
|
+
let text;
|
|
3976
|
+
try {
|
|
3977
|
+
text = fs.readFileSync(file, "utf8");
|
|
3978
|
+
} catch {
|
|
3979
|
+
return null;
|
|
3980
|
+
}
|
|
3981
|
+
const document = splitMarkdownFrontmatter(text);
|
|
3982
|
+
if (!document.frontmatter) return null;
|
|
3983
|
+
const metadata = parseSimpleYamlFrontmatter(document.frontmatter);
|
|
3984
|
+
const name = firstString(metadata.name, path.basename(file, path.extname(file)));
|
|
3985
|
+
if (!name) return null;
|
|
3986
|
+
const sourceRoot = context.root || path.dirname(file);
|
|
3987
|
+
const body = String(document.body || "").trim();
|
|
3988
|
+
const stat = safeStat(file);
|
|
3989
|
+
return compactMetadata({
|
|
3990
|
+
name,
|
|
3991
|
+
description: firstString(metadata.description),
|
|
3992
|
+
tools: normalizeClaudeSubagentTools(metadata.tools),
|
|
3993
|
+
model: firstString(metadata.model),
|
|
3994
|
+
scope: context.scope || "unknown",
|
|
3995
|
+
sourcePath: path.resolve(file),
|
|
3996
|
+
relativePath: relativePathWithin(sourceRoot, file),
|
|
3997
|
+
frontmatterKeys: Object.keys(metadata).sort((a, b) => a.localeCompare(b)),
|
|
3998
|
+
instructionPreview: previewString(body, 600),
|
|
3999
|
+
instructionLineCount: body ? body.split(/\r?\n/).length : undefined,
|
|
4000
|
+
mtime: stat?.mtimeMs ? new Date(stat.mtimeMs).toISOString() : undefined
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
function effectiveClaudeSubagentDefinitions(definitions) {
|
|
4005
|
+
const byName = new Map();
|
|
4006
|
+
for (const definition of definitions) {
|
|
4007
|
+
const key = definition.name.toLowerCase();
|
|
4008
|
+
const existing = byName.get(key);
|
|
4009
|
+
if (!existing || subagentScopePriority(definition.scope) >= subagentScopePriority(existing.scope)) {
|
|
4010
|
+
byName.set(key, definition);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
function shadowedClaudeSubagentNames(definitions) {
|
|
4017
|
+
const seen = new Map();
|
|
4018
|
+
const shadowed = new Set();
|
|
4019
|
+
for (const definition of definitions) {
|
|
4020
|
+
const key = definition.name.toLowerCase();
|
|
4021
|
+
if (seen.has(key)) shadowed.add(definition.name);
|
|
4022
|
+
seen.set(key, definition);
|
|
4023
|
+
}
|
|
4024
|
+
return [...shadowed].sort((a, b) => a.localeCompare(b));
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
function claudeSubagentDefinitionSummary(definition) {
|
|
4028
|
+
return compactMetadata({
|
|
4029
|
+
name: definition.name,
|
|
4030
|
+
description: definition.description,
|
|
4031
|
+
tools: definition.tools,
|
|
4032
|
+
model: definition.model,
|
|
4033
|
+
scope: definition.scope,
|
|
4034
|
+
sourcePath: definition.sourcePath,
|
|
4035
|
+
relativePath: definition.relativePath,
|
|
4036
|
+
instructionPreview: definition.instructionPreview,
|
|
4037
|
+
instructionLineCount: definition.instructionLineCount
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
function claudeSubagentRunImportContext(parentSessionId, conversationFile, env = process.env, sourceFiles = null) {
|
|
4042
|
+
const files = Array.isArray(sourceFiles)
|
|
4043
|
+
? sourceFiles
|
|
4044
|
+
: claudeSubagentRunSourceFilesForSession(parentSessionId, conversationFile, env);
|
|
4045
|
+
const transcriptFiles = files.filter((file) => /\.jsonl$/i.test(file));
|
|
4046
|
+
const sourceRoot = conversationFile ? path.dirname(conversationFile) : "";
|
|
4047
|
+
const sessions = [];
|
|
4048
|
+
const runs = [];
|
|
4049
|
+
for (const file of transcriptFiles) {
|
|
4050
|
+
const parsed = parseClaudeSubagentRunFile(file, parentSessionId, sourceRoot);
|
|
4051
|
+
if (!parsed) continue;
|
|
4052
|
+
sessions.push(parsed.session);
|
|
4053
|
+
runs.push(parsed.summary);
|
|
4054
|
+
}
|
|
4055
|
+
const sourceFileList = [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
4056
|
+
return {
|
|
4057
|
+
sourceFiles: sourceFileList,
|
|
4058
|
+
sessions,
|
|
4059
|
+
sessionSummary: runs.length
|
|
4060
|
+
? {
|
|
4061
|
+
claudeSubagentRuns: compactMetadata({
|
|
4062
|
+
count: runs.length,
|
|
4063
|
+
agentIds: [...new Set(runs.map((run) => run.agentId).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
|
|
4064
|
+
agentTypes: [...new Set(runs.map((run) => run.agentType).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
|
|
4065
|
+
messageCount: runs.reduce((sum, run) => sum + (run.messageCount || 0), 0),
|
|
4066
|
+
userMessageCount: runs.reduce((sum, run) => sum + (run.userMessageCount || 0), 0),
|
|
4067
|
+
assistantMessageCount: runs.reduce((sum, run) => sum + (run.assistantMessageCount || 0), 0),
|
|
4068
|
+
toolCallCount: runs.reduce((sum, run) => sum + (run.toolCallCount || 0), 0),
|
|
4069
|
+
toolResultCount: runs.reduce((sum, run) => sum + (run.toolResultCount || 0), 0),
|
|
4070
|
+
runs
|
|
4071
|
+
})
|
|
4072
|
+
}
|
|
4073
|
+
: null
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
function parseClaudeSubagentRunFile(file, parentSessionId, sourceRoot = "") {
|
|
4078
|
+
let parsed;
|
|
4079
|
+
try {
|
|
4080
|
+
parsed = parseAgentJsonl(file, "claude_code");
|
|
4081
|
+
} catch {
|
|
4082
|
+
return null;
|
|
4083
|
+
}
|
|
4084
|
+
if (!parsed.messages.length) return null;
|
|
4085
|
+
const meta = readClaudeSubagentRunMeta(file);
|
|
4086
|
+
const agentId = claudeSubagentRunAgentId(parsed, file);
|
|
4087
|
+
const agentType = firstString(meta?.agentType, meta?.type, meta?.name);
|
|
4088
|
+
const models = sessionMessageModels(parsed.messages);
|
|
4089
|
+
const usage = computeSessionUsage(parsed.messages);
|
|
4090
|
+
const promptPreview = previewString(firstClaudeSubagentUserPrompt(parsed.messages), 360);
|
|
4091
|
+
const resultPreview = previewString(lastClaudeSubagentAssistantText(parsed.messages), 360);
|
|
4092
|
+
const toolCallCount = parsed.messages.reduce((sum, message) => sum + (Array.isArray(message.metadata?.toolCalls) ? message.metadata.toolCalls.length : 0), 0);
|
|
4093
|
+
const toolResultCount = parsed.messages.reduce((sum, message) => sum + (message.metadata?.toolResult ? 1 : 0), 0);
|
|
4094
|
+
const sourceFiles = claudeSubagentRunSourceFilesForTranscript(file);
|
|
4095
|
+
const sessionId = `claude-subagent-${hashId(`${parentSessionId}:${agentId}:${file}`)}`;
|
|
4096
|
+
const title = claudeSubagentRunTitle(agentType, parsed.title, promptPreview, agentId);
|
|
4097
|
+
const relativePath = relativePathWithin(sourceRoot || path.dirname(file), file);
|
|
4098
|
+
const summary = compactMetadata({
|
|
4099
|
+
sessionId,
|
|
4100
|
+
parentSessionId,
|
|
4101
|
+
agentId,
|
|
4102
|
+
agentType,
|
|
4103
|
+
title,
|
|
4104
|
+
startedAt: parsed.startedAt,
|
|
4105
|
+
endedAt: parsed.endedAt,
|
|
4106
|
+
messageCount: parsed.messages.length,
|
|
4107
|
+
userMessageCount: parsed.messages.filter((message) => message.role === "user").length,
|
|
4108
|
+
assistantMessageCount: parsed.messages.filter((message) => message.role === "assistant").length,
|
|
4109
|
+
toolCallCount,
|
|
4110
|
+
toolResultCount,
|
|
4111
|
+
models,
|
|
4112
|
+
usage,
|
|
4113
|
+
promptPreview,
|
|
4114
|
+
resultPreview,
|
|
4115
|
+
sourcePath: file,
|
|
4116
|
+
relativePath
|
|
4117
|
+
});
|
|
4118
|
+
return {
|
|
4119
|
+
summary,
|
|
4120
|
+
session: {
|
|
4121
|
+
provider: "claude_code",
|
|
4122
|
+
sessionId,
|
|
4123
|
+
cwd: parsed.cwd,
|
|
4124
|
+
messages: parsed.messages,
|
|
4125
|
+
startedAt: parsed.startedAt,
|
|
4126
|
+
endedAt: parsed.endedAt,
|
|
4127
|
+
sourcePath: file,
|
|
4128
|
+
sourceFiles,
|
|
4129
|
+
title,
|
|
4130
|
+
conversationKind: "claude_subagent",
|
|
4131
|
+
parentComposerId: parentSessionId,
|
|
4132
|
+
sessionSummary: mergeSessionSummaries(parsed.sessionSummary, {
|
|
4133
|
+
claudeSubagentRun: compactMetadata({
|
|
4134
|
+
parentSessionId,
|
|
4135
|
+
agentId,
|
|
4136
|
+
agentType,
|
|
4137
|
+
title,
|
|
4138
|
+
promptPreview,
|
|
4139
|
+
resultPreview,
|
|
4140
|
+
sourcePath: file,
|
|
4141
|
+
relativePath
|
|
4142
|
+
})
|
|
4143
|
+
})
|
|
4144
|
+
}
|
|
4145
|
+
};
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
function claudeSubagentRunSourceFilesForSession(parentSessionId, conversationFile, env = process.env) {
|
|
4149
|
+
const files = [];
|
|
4150
|
+
for (const dir of claudeSubagentRunDirs(parentSessionId, conversationFile, env)) {
|
|
4151
|
+
let entries;
|
|
4152
|
+
try {
|
|
4153
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
4154
|
+
} catch (error) {
|
|
4155
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES") continue;
|
|
4156
|
+
throw error;
|
|
4157
|
+
}
|
|
4158
|
+
for (const entry of entries) {
|
|
4159
|
+
if (!entry.isFile()) continue;
|
|
4160
|
+
if (!/\.jsonl$/i.test(entry.name) && !/\.meta\.json$/i.test(entry.name)) continue;
|
|
4161
|
+
files.push(path.join(dir, entry.name));
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
function claudeSubagentRunDirs(parentSessionId, conversationFile, env = process.env) {
|
|
4168
|
+
if (!parentSessionId || !conversationFile) return [];
|
|
4169
|
+
const dirs = [
|
|
4170
|
+
path.join(path.dirname(conversationFile), String(parentSessionId), "subagents"),
|
|
4171
|
+
path.join(path.dirname(conversationFile), "subagents")
|
|
4172
|
+
];
|
|
4173
|
+
const root = claudeRoots(env)[0];
|
|
4174
|
+
const directParent = path.join(root, path.basename(path.dirname(conversationFile)), String(parentSessionId), "subagents");
|
|
4175
|
+
dirs.push(directParent);
|
|
4176
|
+
const seen = new Set();
|
|
4177
|
+
return dirs.filter((dir) => {
|
|
4178
|
+
const resolved = path.resolve(dir);
|
|
4179
|
+
if (seen.has(resolved)) return false;
|
|
4180
|
+
seen.add(resolved);
|
|
4181
|
+
return safeStat(resolved)?.isDirectory();
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
function claudeSubagentRunSourceFilesForTranscript(file) {
|
|
4186
|
+
const files = [file];
|
|
4187
|
+
const meta = claudeSubagentRunMetaFile(file);
|
|
4188
|
+
if (safeStat(meta)?.isFile()) files.push(meta);
|
|
4189
|
+
return files;
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
function claudeSubagentRunMetaFile(file) {
|
|
4193
|
+
return String(file || "").replace(/\.jsonl$/i, ".meta.json");
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
function readClaudeSubagentRunMeta(file) {
|
|
4197
|
+
const metaFile = claudeSubagentRunMetaFile(file);
|
|
4198
|
+
try {
|
|
4199
|
+
return readJson(metaFile, null);
|
|
4200
|
+
} catch {
|
|
4201
|
+
return null;
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
function claudeSubagentRunAgentId(parsed, file) {
|
|
4206
|
+
const agentIds = parsed?.sessionSummary?.claudeJsonl?.agentIds;
|
|
4207
|
+
if (Array.isArray(agentIds) && agentIds.length) return agentIds[0];
|
|
4208
|
+
if (typeof agentIds === "string" && agentIds.trim()) return agentIds.trim();
|
|
4209
|
+
return path.basename(String(file || ""), path.extname(String(file || ""))).replace(/^agent-/, "");
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
function claudeSubagentRunTitle(agentType, parsedTitle, promptPreview, agentId) {
|
|
4213
|
+
const promptTitle = titleFromPrompt(promptPreview) || titleFromPrompt(parsedTitle) || firstString(parsedTitle);
|
|
4214
|
+
const prefix = firstString(agentType);
|
|
4215
|
+
const title = prefix && promptTitle ? `${prefix}: ${promptTitle}` : firstString(promptTitle, prefix, agentId, "Claude subagent");
|
|
4216
|
+
return titleFromPrompt(title) || title;
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
function firstClaudeSubagentUserPrompt(messages) {
|
|
4220
|
+
return (messages || []).find((message) => message.role === "user" && String(message.content || "").trim())?.content || "";
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
function lastClaudeSubagentAssistantText(messages) {
|
|
4224
|
+
return [...(messages || [])].reverse().find((message) => message.role === "assistant" && String(message.content || "").trim())?.content || "";
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
function sessionMessageModels(messages) {
|
|
4228
|
+
return [...new Set((messages || []).map((message) => message.metadata?.model).filter((model) => typeof model === "string" && model.trim()).map((model) => model.trim()))]
|
|
4229
|
+
.sort((a, b) => a.localeCompare(b));
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
function claudeSubagentUserRoot(env = process.env) {
|
|
4233
|
+
const home = env && env.HOME ? env.HOME : os.homedir();
|
|
4234
|
+
return path.join(home, ".claude", "agents");
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
function findClaudeProjectSubagentRoot(cwd) {
|
|
4238
|
+
if (!cwd) return "";
|
|
4239
|
+
let dir = path.resolve(cwd);
|
|
4240
|
+
if (!safeStat(dir)?.isDirectory()) return "";
|
|
4241
|
+
for (;;) {
|
|
4242
|
+
const candidate = path.join(dir, ".claude", "agents");
|
|
4243
|
+
if (safeStat(candidate)?.isDirectory()) return candidate;
|
|
4244
|
+
const parent = path.dirname(dir);
|
|
4245
|
+
if (parent === dir || fs.existsSync(path.join(dir, ".git"))) return "";
|
|
4246
|
+
dir = parent;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
function subagentScopePriority(scope) {
|
|
4251
|
+
if (scope === "project") return 2;
|
|
4252
|
+
if (scope === "user") return 1;
|
|
4253
|
+
return 0;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
function splitMarkdownFrontmatter(text) {
|
|
4257
|
+
const normalized = String(text || "").replace(/^\uFEFF/, "");
|
|
4258
|
+
const match = normalized.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/);
|
|
4259
|
+
if (!match) return { frontmatter: "", body: normalized };
|
|
4260
|
+
return { frontmatter: match[1], body: match[2] || "" };
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
function parseSimpleYamlFrontmatter(text) {
|
|
4264
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
4265
|
+
const result = {};
|
|
4266
|
+
for (let index = 0; index < lines.length; index++) {
|
|
4267
|
+
const line = lines[index];
|
|
4268
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
4269
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
4270
|
+
if (!match) continue;
|
|
4271
|
+
const key = match[1];
|
|
4272
|
+
const raw = match[2] || "";
|
|
4273
|
+
if (/^[>|]/.test(raw.trim())) {
|
|
4274
|
+
const block = [];
|
|
4275
|
+
while (index + 1 < lines.length && (/^(?:\s+|$)/.test(lines[index + 1]))) {
|
|
4276
|
+
index++;
|
|
4277
|
+
block.push(lines[index].replace(/^\s{2}/, ""));
|
|
4278
|
+
}
|
|
4279
|
+
result[key] = raw.trim().startsWith(">") ? block.join(" ").replace(/\s+/g, " ").trim() : block.join("\n").trim();
|
|
4280
|
+
} else if (!raw.trim()) {
|
|
4281
|
+
const values = [];
|
|
4282
|
+
while (index + 1 < lines.length) {
|
|
4283
|
+
const item = lines[index + 1].match(/^\s*-\s*(.*)$/);
|
|
4284
|
+
if (!item) break;
|
|
4285
|
+
index++;
|
|
4286
|
+
values.push(parseYamlScalar(item[1]));
|
|
4287
|
+
}
|
|
4288
|
+
result[key] = values.length ? values : "";
|
|
4289
|
+
} else {
|
|
4290
|
+
result[key] = parseYamlScalar(raw);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
return result;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
function parseYamlScalar(value) {
|
|
4297
|
+
const text = stripYamlComment(String(value || "").trim());
|
|
4298
|
+
if (!text) return "";
|
|
4299
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
4300
|
+
return text
|
|
4301
|
+
.slice(1, -1)
|
|
4302
|
+
.split(",")
|
|
4303
|
+
.map((item) => stripYamlString(item.trim()))
|
|
4304
|
+
.filter(Boolean);
|
|
4305
|
+
}
|
|
4306
|
+
return stripYamlString(text);
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
function stripYamlComment(value) {
|
|
4310
|
+
let quote = "";
|
|
4311
|
+
for (let index = 0; index < value.length; index++) {
|
|
4312
|
+
const char = value[index];
|
|
4313
|
+
if ((char === '"' || char === "'") && value[index - 1] !== "\\") {
|
|
4314
|
+
quote = quote === char ? "" : quote || char;
|
|
4315
|
+
}
|
|
4316
|
+
if (char === "#" && !quote && (index === 0 || /\s/.test(value[index - 1]))) {
|
|
4317
|
+
return value.slice(0, index).trimEnd();
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
return value;
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
function stripYamlString(value) {
|
|
4324
|
+
const text = String(value || "").trim();
|
|
4325
|
+
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
4326
|
+
return text.slice(1, -1);
|
|
4327
|
+
}
|
|
4328
|
+
return text;
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
function normalizeClaudeSubagentTools(value) {
|
|
4332
|
+
const values = Array.isArray(value)
|
|
4333
|
+
? value
|
|
4334
|
+
: typeof value === "string"
|
|
4335
|
+
? value.split(",")
|
|
4336
|
+
: [];
|
|
4337
|
+
return values
|
|
4338
|
+
.map((item) => String(item || "").trim())
|
|
4339
|
+
.filter(Boolean)
|
|
4340
|
+
.filter((item) => !/^none$/i.test(item))
|
|
4341
|
+
.sort((a, b) => a.localeCompare(b));
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
function relativePathWithin(root, file) {
|
|
4345
|
+
const relative = path.relative(root || path.dirname(file), file);
|
|
4346
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : path.basename(file);
|
|
4347
|
+
}
|
|
4348
|
+
|
|
4349
|
+
function previewString(value, max = 600) {
|
|
4350
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
4351
|
+
if (!text) return "";
|
|
4352
|
+
return text.length > max ? `${text.slice(0, max - 3).trimEnd()}...` : text;
|
|
4353
|
+
}
|
|
4354
|
+
|
|
3194
4355
|
function jsonlFiles(roots) {
|
|
3195
4356
|
const files = [];
|
|
3196
4357
|
for (const root of roots) {
|
|
@@ -3324,59 +4485,196 @@ function readInitialLines(file, maxLines, maxBytes = 1024 * 1024) {
|
|
|
3324
4485
|
function readCodexThreads(env = process.env) {
|
|
3325
4486
|
const db = codexStateDb(env);
|
|
3326
4487
|
if (!fs.existsSync(db)) return [];
|
|
4488
|
+
const sessionIndex = readCodexSessionIndex(env);
|
|
4489
|
+
const threadColumns = sqliteTableColumns(db, "threads");
|
|
3327
4490
|
const hasStage1Outputs = sqliteTableExists(db, "stage1_outputs");
|
|
3328
|
-
const
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
4491
|
+
const hasSpawnEdges = sqliteTableExists(db, "thread_spawn_edges");
|
|
4492
|
+
const select = [
|
|
4493
|
+
"t.id",
|
|
4494
|
+
"t.rollout_path",
|
|
4495
|
+
"t.created_at",
|
|
4496
|
+
"t.updated_at",
|
|
4497
|
+
"t.source",
|
|
4498
|
+
"t.cwd",
|
|
4499
|
+
"t.title",
|
|
4500
|
+
sqliteSelectMaybe(threadColumns, "t", "created_at_ms"),
|
|
4501
|
+
sqliteSelectMaybe(threadColumns, "t", "updated_at_ms"),
|
|
4502
|
+
sqliteSelectMaybe(threadColumns, "t", "agent_nickname"),
|
|
4503
|
+
sqliteSelectMaybe(threadColumns, "t", "agent_role"),
|
|
4504
|
+
sqliteSelectMaybe(threadColumns, "t", "agent_path"),
|
|
4505
|
+
sqliteSelectMaybe(threadColumns, "t", "thread_source"),
|
|
4506
|
+
sqliteSelectMaybe(threadColumns, "t", "tokens_used"),
|
|
4507
|
+
sqliteSelectMaybe(threadColumns, "t", "model"),
|
|
4508
|
+
sqliteSelectMaybe(threadColumns, "t", "model_provider", "model_provider"),
|
|
4509
|
+
hasStage1Outputs ? "s.raw_memory" : "null as raw_memory",
|
|
4510
|
+
hasStage1Outputs ? "s.rollout_summary" : "null as rollout_summary",
|
|
4511
|
+
hasStage1Outputs ? "s.generated_at as summary_generated_at" : "null as summary_generated_at",
|
|
4512
|
+
hasStage1Outputs ? "s.source_updated_at as summary_source_updated_at" : "null as summary_source_updated_at",
|
|
4513
|
+
hasStage1Outputs ? "s.rollout_slug" : "null as rollout_slug",
|
|
4514
|
+
hasSpawnEdges ? "e.parent_thread_id as spawn_parent_thread_id" : "null as spawn_parent_thread_id",
|
|
4515
|
+
hasSpawnEdges ? "e.status as spawn_status" : "null as spawn_status"
|
|
4516
|
+
];
|
|
4517
|
+
const joins = [
|
|
4518
|
+
hasStage1Outputs ? "left join stage1_outputs s on s.thread_id = t.id" : "",
|
|
4519
|
+
hasSpawnEdges ? "left join thread_spawn_edges e on e.child_thread_id = t.id" : ""
|
|
4520
|
+
].filter(Boolean).join(" ");
|
|
4521
|
+
const query = [
|
|
4522
|
+
`select ${select.join(", ")}`,
|
|
4523
|
+
"from threads t",
|
|
4524
|
+
joins,
|
|
4525
|
+
"where t.rollout_path != ''",
|
|
4526
|
+
"order by t.updated_at desc"
|
|
4527
|
+
].filter(Boolean).join(" ");
|
|
3344
4528
|
const result = spawnSync("sqlite3", [db, "-json", query], { argv0: "agentlog-sqlite", encoding: "utf8", maxBuffer: 1024 * 1024 * 50 });
|
|
3345
4529
|
if (result.status !== 0 || !result.stdout.trim()) return [];
|
|
3346
4530
|
try {
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
4531
|
+
const rows = JSON.parse(result.stdout).map((row) => {
|
|
4532
|
+
const sourceInfo = codexThreadSourceInfo(row.source);
|
|
4533
|
+
return {
|
|
4534
|
+
id: row.id,
|
|
4535
|
+
rolloutPath: row.rollout_path,
|
|
4536
|
+
createdAt: toIso(row.created_at_ms || row.created_at),
|
|
4537
|
+
updatedAt: toIso(row.updated_at_ms || row.updated_at),
|
|
4538
|
+
source: row.source,
|
|
4539
|
+
rawSource: row.source,
|
|
4540
|
+
threadSource: row.thread_source || "",
|
|
4541
|
+
tokensUsed: numberValue(row.tokens_used),
|
|
4542
|
+
model: firstString(row.model),
|
|
4543
|
+
modelProvider: firstString(row.model_provider),
|
|
4544
|
+
cwd: row.cwd,
|
|
4545
|
+
stateTitle: row.title,
|
|
4546
|
+
rawMemory: row.raw_memory || "",
|
|
4547
|
+
rolloutSummary: row.rollout_summary || "",
|
|
4548
|
+
summaryGeneratedAt: toIso(row.summary_generated_at),
|
|
4549
|
+
summarySourceUpdatedAt: toIso(row.summary_source_updated_at),
|
|
4550
|
+
rolloutSlug: row.rollout_slug || "",
|
|
4551
|
+
parentThreadId: firstString(row.spawn_parent_thread_id, sourceInfo.parentThreadId),
|
|
4552
|
+
spawnStatus: firstString(row.spawn_status, sourceInfo.status),
|
|
4553
|
+
agentNickname: firstString(row.agent_nickname, sourceInfo.agentNickname),
|
|
4554
|
+
agentRole: firstString(row.agent_role, sourceInfo.agentRole),
|
|
4555
|
+
agentPath: firstString(row.agent_path, sourceInfo.agentPath),
|
|
4556
|
+
subagentDepth: numberValue(sourceInfo.depth),
|
|
4557
|
+
isCodexSubagent: Boolean(row.spawn_parent_thread_id || sourceInfo.isSubagent || row.agent_nickname || row.agent_role || row.agent_path)
|
|
4558
|
+
};
|
|
4559
|
+
});
|
|
4560
|
+
return resolveCodexThreadSources(rows).map((thread) => applyCodexSessionIndexTitle({ ...thread, title: thread.stateTitle || "" }, sessionIndex));
|
|
3361
4561
|
} catch {
|
|
3362
4562
|
return [];
|
|
3363
4563
|
}
|
|
3364
4564
|
}
|
|
3365
4565
|
|
|
4566
|
+
function resolveCodexThreadSources(threads) {
|
|
4567
|
+
const byId = new Map((threads || []).map((thread) => [thread.id, thread]));
|
|
4568
|
+
const resolved = new Map();
|
|
4569
|
+
const sourceForThread = (thread, seen = new Set()) => {
|
|
4570
|
+
if (!thread) return "cli";
|
|
4571
|
+
const cached = resolved.get(thread.id);
|
|
4572
|
+
if (cached) return cached;
|
|
4573
|
+
const direct = codexSourceKind(thread.rawSource || thread.source) || codexSourceKind(thread.threadSource);
|
|
4574
|
+
if (direct) {
|
|
4575
|
+
resolved.set(thread.id, direct);
|
|
4576
|
+
return direct;
|
|
4577
|
+
}
|
|
4578
|
+
if (thread.parentThreadId && !seen.has(thread.id)) {
|
|
4579
|
+
seen.add(thread.id);
|
|
4580
|
+
const parentSource = sourceForThread(byId.get(thread.parentThreadId), seen);
|
|
4581
|
+
resolved.set(thread.id, parentSource);
|
|
4582
|
+
return parentSource;
|
|
4583
|
+
}
|
|
4584
|
+
resolved.set(thread.id, "cli");
|
|
4585
|
+
return "cli";
|
|
4586
|
+
};
|
|
4587
|
+
return (threads || []).map((thread) => ({
|
|
4588
|
+
...thread,
|
|
4589
|
+
source: sourceForThread(thread),
|
|
4590
|
+
isCodexSubagent: Boolean(thread.isCodexSubagent || thread.parentThreadId)
|
|
4591
|
+
}));
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
function codexSourceKind(value) {
|
|
4595
|
+
const text = String(value || "").trim();
|
|
4596
|
+
return ["cli", "vscode", "exec"].includes(text) ? text : "";
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
function codexThreadSourceInfo(value) {
|
|
4600
|
+
let parsed = null;
|
|
4601
|
+
try {
|
|
4602
|
+
parsed = typeof value === "string" && value.trim().startsWith("{") ? JSON.parse(value) : null;
|
|
4603
|
+
} catch {
|
|
4604
|
+
parsed = null;
|
|
4605
|
+
}
|
|
4606
|
+
const spawn = parsed?.subagent?.thread_spawn || parsed?.thread_spawn || null;
|
|
4607
|
+
if (!spawn || typeof spawn !== "object") return {};
|
|
4608
|
+
return {
|
|
4609
|
+
isSubagent: true,
|
|
4610
|
+
parentThreadId: firstString(spawn.parent_thread_id, spawn.parentThreadId, spawn.parent_id, spawn.parentId),
|
|
4611
|
+
agentNickname: firstString(spawn.agent_nickname, spawn.agentNickname, spawn.nickname),
|
|
4612
|
+
agentRole: firstString(spawn.agent_role, spawn.agentRole, spawn.role),
|
|
4613
|
+
agentPath: firstString(spawn.agent_path, spawn.agentPath, spawn.path),
|
|
4614
|
+
status: firstString(spawn.status),
|
|
4615
|
+
depth: spawn.depth
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4619
|
+
function applyCodexSessionIndexTitle(thread, sessionIndex) {
|
|
4620
|
+
if (!thread) return thread;
|
|
4621
|
+
const indexedTitle = sessionIndex.get(thread.id);
|
|
4622
|
+
if (!indexedTitle?.title) return thread;
|
|
4623
|
+
return {
|
|
4624
|
+
...thread,
|
|
4625
|
+
title: indexedTitle.title,
|
|
4626
|
+
titleSource: "session-index",
|
|
4627
|
+
sessionIndexPath: indexedTitle.path,
|
|
4628
|
+
sessionIndexUpdatedAt: indexedTitle.updatedAt,
|
|
4629
|
+
sessionIndexTitle: indexedTitle.title
|
|
4630
|
+
};
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
function readCodexSessionIndex(env = process.env) {
|
|
4634
|
+
const file = codexSessionIndexPath(env);
|
|
4635
|
+
if (!fs.existsSync(file)) return new Map();
|
|
4636
|
+
const byId = new Map();
|
|
4637
|
+
let lines = [];
|
|
4638
|
+
try {
|
|
4639
|
+
lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
|
4640
|
+
} catch {
|
|
4641
|
+
return byId;
|
|
4642
|
+
}
|
|
4643
|
+
for (const line of lines) {
|
|
4644
|
+
if (!line.trim()) continue;
|
|
4645
|
+
let entry;
|
|
4646
|
+
try {
|
|
4647
|
+
entry = JSON.parse(line);
|
|
4648
|
+
} catch {
|
|
4649
|
+
continue;
|
|
4650
|
+
}
|
|
4651
|
+
const id = firstString(entry.id, entry.thread_id, entry.threadId, entry.session_id, entry.sessionId);
|
|
4652
|
+
const title = titleFromPrompt(firstString(entry.thread_name, entry.threadName, entry.title, entry.name));
|
|
4653
|
+
if (!id || !title) continue;
|
|
4654
|
+
const updatedAt = toIso(entry.updated_at || entry.updatedAt || entry.time || entry.timestamp);
|
|
4655
|
+
const previous = byId.get(id);
|
|
4656
|
+
if (!previous || String(updatedAt || "").localeCompare(String(previous.updatedAt || "")) >= 0) {
|
|
4657
|
+
byId.set(id, { title, updatedAt, path: file });
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
return byId;
|
|
4661
|
+
}
|
|
4662
|
+
|
|
3366
4663
|
function readCodexSessionEntries(env = process.env) {
|
|
4664
|
+
const sessionIndex = readCodexSessionIndex(env);
|
|
3367
4665
|
const indexed = readCodexThreads(env).map((thread) => ({ ...thread, indexed: true }));
|
|
3368
4666
|
const seen = new Set(indexed.map((thread) => normalizeSourcePath(thread.rolloutPath)));
|
|
3369
4667
|
const discovered = [];
|
|
3370
4668
|
for (const file of codexRolloutFiles(env)) {
|
|
3371
4669
|
const key = normalizeSourcePath(file);
|
|
3372
4670
|
if (seen.has(key)) continue;
|
|
3373
|
-
const thread = codexRolloutFileThread(file);
|
|
4671
|
+
const thread = applyCodexSessionIndexTitle(codexRolloutFileThread(file), sessionIndex);
|
|
3374
4672
|
if (thread) {
|
|
3375
4673
|
discovered.push(thread);
|
|
3376
4674
|
seen.add(key);
|
|
3377
4675
|
}
|
|
3378
4676
|
}
|
|
3379
|
-
return indexed.concat(discovered).sort((a, b) => String(b
|
|
4677
|
+
return indexed.concat(discovered).sort((a, b) => String(codexThreadImportUpdatedAt(b)).localeCompare(String(codexThreadImportUpdatedAt(a))));
|
|
3380
4678
|
}
|
|
3381
4679
|
|
|
3382
4680
|
function codexRolloutFiles(env = process.env) {
|
|
@@ -3448,18 +4746,174 @@ function readCodexRolloutInfo(file) {
|
|
|
3448
4746
|
info.cwd ||= firstString(payload.cwd, event.cwd);
|
|
3449
4747
|
}
|
|
3450
4748
|
info.cwd ||= firstString(event.cwd, payload.cwd);
|
|
3451
|
-
|
|
3452
|
-
if (
|
|
4749
|
+
const threadTitle = codexThreadNameFromEvent(event);
|
|
4750
|
+
if (threadTitle) {
|
|
4751
|
+
info.title = threadTitle;
|
|
4752
|
+
info.titleSource = "thread-name";
|
|
4753
|
+
} else if (!info.title) {
|
|
4754
|
+
const promptTitle = titleFromPrompt(firstUserTextFromCodexEvent(event));
|
|
4755
|
+
if (promptTitle) {
|
|
4756
|
+
info.title = promptTitle;
|
|
4757
|
+
info.titleSource = "prompt";
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
if (info.id && info.cwd && info.title && lastTimestamp && info.titleSource === "thread-name") break;
|
|
3453
4761
|
}
|
|
3454
4762
|
if (lastTimestamp) info.updatedAt = lastTimestamp;
|
|
3455
4763
|
return info;
|
|
3456
4764
|
}
|
|
3457
4765
|
|
|
4766
|
+
function codexSessionTitleForImport(thread, parsed) {
|
|
4767
|
+
const indexedTitle = firstString(thread?.sessionIndexTitle);
|
|
4768
|
+
if (indexedTitle) return indexedTitle;
|
|
4769
|
+
const parsedTitle = firstString(parsed?.title);
|
|
4770
|
+
if (parsedTitle && (parsed?.titleSource === "thread-name" || codexPromptLikeThreadTitle(thread?.title))) return parsedTitle;
|
|
4771
|
+
return firstString(thread?.title, parsedTitle);
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
function codexSubagentRunImportContext(parentThread, childThreads = [], env = process.env) {
|
|
4775
|
+
if (!parentThread?.id || !Array.isArray(childThreads) || !childThreads.length) return null;
|
|
4776
|
+
const runs = [];
|
|
4777
|
+
for (const childThread of childThreads) {
|
|
4778
|
+
const parsed = parseCodexSubagentThread(childThread);
|
|
4779
|
+
const summary = codexSubagentRunSummary(parentThread, childThread, parsed, env);
|
|
4780
|
+
if (summary) runs.push(summary);
|
|
4781
|
+
}
|
|
4782
|
+
runs.sort((a, b) => {
|
|
4783
|
+
const time = String(a.startedAt || a.endedAt || "").localeCompare(String(b.startedAt || b.endedAt || ""));
|
|
4784
|
+
return time || String(a.sessionId || "").localeCompare(String(b.sessionId || ""));
|
|
4785
|
+
});
|
|
4786
|
+
if (!runs.length) return null;
|
|
4787
|
+
return {
|
|
4788
|
+
sessionSummary: {
|
|
4789
|
+
codexSubagentRuns: compactMetadata({
|
|
4790
|
+
count: runs.length,
|
|
4791
|
+
agentIds: [...new Set(runs.map((run) => run.agentId).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
|
|
4792
|
+
agentNicknames: [...new Set(runs.map((run) => run.agentNickname).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
|
|
4793
|
+
agentRoles: [...new Set(runs.map((run) => run.agentRole).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
|
|
4794
|
+
statuses: [...new Set(runs.map((run) => run.status).filter(Boolean))].sort((a, b) => a.localeCompare(b)),
|
|
4795
|
+
messageCount: runs.reduce((sum, run) => sum + (run.messageCount || 0), 0),
|
|
4796
|
+
userMessageCount: runs.reduce((sum, run) => sum + (run.userMessageCount || 0), 0),
|
|
4797
|
+
assistantMessageCount: runs.reduce((sum, run) => sum + (run.assistantMessageCount || 0), 0),
|
|
4798
|
+
toolCallCount: runs.reduce((sum, run) => sum + (run.toolCallCount || 0), 0),
|
|
4799
|
+
toolResultCount: runs.reduce((sum, run) => sum + (run.toolResultCount || 0), 0),
|
|
4800
|
+
runs
|
|
4801
|
+
})
|
|
4802
|
+
}
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
|
|
4806
|
+
function codexSubagentSessionSummary(thread, parsed, env = process.env) {
|
|
4807
|
+
if (!thread?.isCodexSubagent && !thread?.parentThreadId) return null;
|
|
4808
|
+
const summary = codexSubagentRunSummary(null, thread, parsed, env);
|
|
4809
|
+
return summary ? { sessionSummary: { codexSubagentRun: summary } } : null;
|
|
4810
|
+
}
|
|
4811
|
+
|
|
4812
|
+
function parseCodexSubagentThread(thread) {
|
|
4813
|
+
if (!thread?.rolloutPath) return null;
|
|
4814
|
+
try {
|
|
4815
|
+
return finalizeCodexParsedThread(thread, parseAgentJsonl(thread.rolloutPath, "codex"));
|
|
4816
|
+
} catch {
|
|
4817
|
+
return null;
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
function codexSubagentRunSummary(parentThread, childThread, parsed, env = process.env) {
|
|
4822
|
+
if (!childThread?.id && !parsed?.sessionId) return null;
|
|
4823
|
+
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
4824
|
+
const sessionId = parsed?.sessionId || childThread.id;
|
|
4825
|
+
const parentSessionId = firstString(parentThread?.id, childThread.parentThreadId);
|
|
4826
|
+
const promptPreview = previewString(firstCodexSubagentUserPrompt(messages) || childThread.title, 360);
|
|
4827
|
+
const resultPreview = previewString(lastCodexSubagentAssistantText(messages), 360);
|
|
4828
|
+
const toolCallCount = messages.reduce((sum, message) => sum + (Array.isArray(message.metadata?.toolCalls) ? message.metadata.toolCalls.length : 0), 0);
|
|
4829
|
+
const toolResultCount = messages.reduce((sum, message) => sum + (message.metadata?.toolResult ? 1 : 0), 0);
|
|
4830
|
+
const usage = messages.length ? computeSessionUsage(messages) : null;
|
|
4831
|
+
const sourceRoot = codexHome(env);
|
|
4832
|
+
return compactMetadata({
|
|
4833
|
+
sessionId,
|
|
4834
|
+
parentSessionId,
|
|
4835
|
+
agentId: childThread.id || sessionId,
|
|
4836
|
+
agentNickname: childThread.agentNickname,
|
|
4837
|
+
agentRole: childThread.agentRole,
|
|
4838
|
+
agentType: childThread.agentRole,
|
|
4839
|
+
agentPath: childThread.agentPath,
|
|
4840
|
+
status: childThread.spawnStatus,
|
|
4841
|
+
depth: childThread.subagentDepth,
|
|
4842
|
+
provider: "codex",
|
|
4843
|
+
providerLabel: "Codex",
|
|
4844
|
+
title: codexSubagentRunTitle(childThread, parsed?.title, promptPreview),
|
|
4845
|
+
startedAt: parsed?.startedAt || childThread.createdAt,
|
|
4846
|
+
endedAt: parsed?.endedAt || childThread.updatedAt,
|
|
4847
|
+
messageCount: messages.length || undefined,
|
|
4848
|
+
userMessageCount: messages.filter((message) => message.role === "user").length || undefined,
|
|
4849
|
+
assistantMessageCount: messages.filter((message) => message.role === "assistant").length || undefined,
|
|
4850
|
+
toolCallCount: toolCallCount || undefined,
|
|
4851
|
+
toolResultCount: toolResultCount || undefined,
|
|
4852
|
+
models: sessionMessageModels(messages),
|
|
4853
|
+
usage,
|
|
4854
|
+
promptPreview,
|
|
4855
|
+
resultPreview,
|
|
4856
|
+
sourcePath: childThread.rolloutPath,
|
|
4857
|
+
relativePath: childThread.rolloutPath ? relativePathWithin(sourceRoot, childThread.rolloutPath) : ""
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
function codexSubagentRunTitle(thread, parsedTitle, promptPreview) {
|
|
4862
|
+
const prefix = firstString(thread?.agentNickname, thread?.agentRole);
|
|
4863
|
+
const promptTitle = titleFromPrompt(firstString(parsedTitle, thread?.title, promptPreview));
|
|
4864
|
+
if (prefix && promptTitle) return `${prefix}: ${promptTitle}`;
|
|
4865
|
+
return firstString(promptTitle, prefix, thread?.id, "Codex subagent");
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
function firstCodexSubagentUserPrompt(messages) {
|
|
4869
|
+
return (messages || []).find((message) => message.role === "user" && String(message.content || "").trim())?.content || "";
|
|
4870
|
+
}
|
|
4871
|
+
|
|
4872
|
+
function lastCodexSubagentAssistantText(messages) {
|
|
4873
|
+
return [...(messages || [])].reverse().find((message) => message.role === "assistant" && String(message.content || "").trim())?.content || "";
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
function codexThreadImportUpdatedAt(thread, childThreads = []) {
|
|
4877
|
+
return latestIso(thread?.updatedAt, thread?.sessionIndexUpdatedAt, thread?.createdAt, ...(childThreads || []).map((child) => codexThreadImportUpdatedAt(child)));
|
|
4878
|
+
}
|
|
4879
|
+
|
|
4880
|
+
function latestIso(...values) {
|
|
4881
|
+
let latest = 0;
|
|
4882
|
+
for (const value of values) {
|
|
4883
|
+
const iso = toIso(value);
|
|
4884
|
+
if (!iso) continue;
|
|
4885
|
+
const time = new Date(iso).getTime();
|
|
4886
|
+
if (Number.isFinite(time) && time > latest) latest = time;
|
|
4887
|
+
}
|
|
4888
|
+
return latest ? new Date(latest).toISOString() : "";
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
function codexPromptLikeThreadTitle(value) {
|
|
4892
|
+
const text = String(value || "").trim();
|
|
4893
|
+
return text.length > 160 || /^# Files mentioned by the user:/m.test(text) || /\[\$[^\]\n]+\]\([^)]+SKILL\.md\)/i.test(text);
|
|
4894
|
+
}
|
|
4895
|
+
|
|
4896
|
+
function codexThreadNameFromEvent(event) {
|
|
4897
|
+
const item = event?.payload || event?.item || event?.data || event;
|
|
4898
|
+
const type = String(item?.type || event?.type || item?.kind || "").toLowerCase();
|
|
4899
|
+
if (!["thread_name_updated", "thread_title_updated", "conversation_title_updated"].includes(type)) return "";
|
|
4900
|
+
return titleFromPrompt(firstString(item.thread_name, item.threadName, item.title, item.name));
|
|
4901
|
+
}
|
|
4902
|
+
|
|
3458
4903
|
function firstUserTextFromCodexEvent(event) {
|
|
3459
4904
|
const payload = event.payload || event;
|
|
3460
|
-
|
|
3461
|
-
if (payload.type === "
|
|
3462
|
-
|
|
4905
|
+
let content = "";
|
|
4906
|
+
if (payload.type === "message" && payload.role === "user") content = extractText(payload.content);
|
|
4907
|
+
else if (payload.type === "user_message") content = firstString(payload.message, payload.text, extractText(payload.content));
|
|
4908
|
+
if (!content) return "";
|
|
4909
|
+
const classification = providerGeneratedContext(content, "codex", event);
|
|
4910
|
+
if (!classification) return content;
|
|
4911
|
+
return classification.kind === "attachment_context" ? codexAttachmentRequestText(content) : "";
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
function codexAttachmentRequestText(value) {
|
|
4915
|
+
const match = String(value || "").match(/(?:^|\n)## My request for Codex:\s*\n([\s\S]*)$/i);
|
|
4916
|
+
return match ? match[1].trim() : "";
|
|
3463
4917
|
}
|
|
3464
4918
|
|
|
3465
4919
|
function codexTimestampFromFilename(file) {
|
|
@@ -3513,12 +4967,20 @@ function codexStateDb(env = process.env) {
|
|
|
3513
4967
|
return env.CODEX_STATE_DB || path.join(codexHome(env), "state_5.sqlite");
|
|
3514
4968
|
}
|
|
3515
4969
|
|
|
4970
|
+
function codexSessionIndexPath(env = process.env) {
|
|
4971
|
+
return env.CODEX_SESSION_INDEX || path.join(codexHome(env), "session_index.jsonl");
|
|
4972
|
+
}
|
|
4973
|
+
|
|
3516
4974
|
function codexHome(env = process.env) {
|
|
3517
4975
|
return env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
3518
4976
|
}
|
|
3519
4977
|
|
|
3520
|
-
function codexSourceFiles(thread, env = process.env) {
|
|
3521
|
-
|
|
4978
|
+
function codexSourceFiles(thread, env = process.env, childThreads = []) {
|
|
4979
|
+
const files = [thread.rolloutPath, thread.indexed ? codexStateDb(env) : "", thread.sessionIndexPath || ""].filter(Boolean);
|
|
4980
|
+
for (const child of childThreads || []) {
|
|
4981
|
+
files.push(...codexSourceFiles(child, env));
|
|
4982
|
+
}
|
|
4983
|
+
return [...new Set(files)].sort((a, b) => a.localeCompare(b));
|
|
3522
4984
|
}
|
|
3523
4985
|
|
|
3524
4986
|
function codexSupplementaryMessages(thread, fallbackTimestamp = "") {
|
|
@@ -3551,20 +5013,59 @@ function codexSupplementFingerprint(thread) {
|
|
|
3551
5013
|
thread.rolloutSummary || "",
|
|
3552
5014
|
thread.summaryGeneratedAt || "",
|
|
3553
5015
|
thread.summarySourceUpdatedAt || "",
|
|
3554
|
-
thread.rolloutSlug || ""
|
|
5016
|
+
thread.rolloutSlug || "",
|
|
5017
|
+
thread.sessionIndexTitle || "",
|
|
5018
|
+
thread.sessionIndexUpdatedAt || ""
|
|
3555
5019
|
].join("\0");
|
|
3556
5020
|
if (!payload.replace(/\0/g, "")) return "summaries:none";
|
|
3557
5021
|
const hash = crypto.createHash("sha256").update(payload).digest("hex").slice(0, 24);
|
|
3558
5022
|
return `summaries:${hash}`;
|
|
3559
5023
|
}
|
|
3560
5024
|
|
|
5025
|
+
function codexThreadMetadataFingerprint(thread) {
|
|
5026
|
+
const payload = [
|
|
5027
|
+
thread?.rawSource || "",
|
|
5028
|
+
thread?.threadSource || "",
|
|
5029
|
+
thread?.parentThreadId || "",
|
|
5030
|
+
thread?.spawnStatus || "",
|
|
5031
|
+
thread?.agentNickname || "",
|
|
5032
|
+
thread?.agentRole || "",
|
|
5033
|
+
thread?.agentPath || "",
|
|
5034
|
+
thread?.subagentDepth || "",
|
|
5035
|
+
thread?.tokensUsed || ""
|
|
5036
|
+
].join("\0");
|
|
5037
|
+
if (!payload.replace(/\0/g, "")) return "";
|
|
5038
|
+
const hash = crypto.createHash("sha256").update(payload).digest("hex").slice(0, 24);
|
|
5039
|
+
return `thread-meta:${hash}`;
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
function codexSubagentRunsFingerprint(childThreads = []) {
|
|
5043
|
+
const payload = (childThreads || [])
|
|
5044
|
+
.map((thread) => [
|
|
5045
|
+
thread.id || "",
|
|
5046
|
+
thread.rolloutPath || "",
|
|
5047
|
+
thread.parentThreadId || "",
|
|
5048
|
+
thread.spawnStatus || "",
|
|
5049
|
+
thread.agentNickname || "",
|
|
5050
|
+
thread.agentRole || "",
|
|
5051
|
+
thread.agentPath || "",
|
|
5052
|
+
thread.tokensUsed || "",
|
|
5053
|
+
thread.rolloutPath ? fileFingerprint(thread.rolloutPath, safeStat(thread.rolloutPath)) : ""
|
|
5054
|
+
].join("\t"))
|
|
5055
|
+
.join("\n");
|
|
5056
|
+
if (!payload) return "";
|
|
5057
|
+
const hash = crypto.createHash("sha256").update(payload).digest("hex").slice(0, 24);
|
|
5058
|
+
return `codex-subagent-runs:${hash}`;
|
|
5059
|
+
}
|
|
5060
|
+
|
|
3561
5061
|
function summarizeCodexSources(threads) {
|
|
3562
5062
|
const cli = threads.filter((thread) => thread.source === "cli").length;
|
|
3563
5063
|
const desktop = threads.filter((thread) => thread.source === "vscode").length;
|
|
3564
5064
|
const exec = threads.filter((thread) => thread.source === "exec").length;
|
|
3565
5065
|
const summaries = threads.filter((thread) => thread.rawMemory || thread.rolloutSummary).length;
|
|
3566
5066
|
const archived = threads.filter((thread) => thread.sourceDetail === "archived_sessions").length;
|
|
3567
|
-
|
|
5067
|
+
const subagents = threads.filter((thread) => thread.isCodexSubagent).length;
|
|
5068
|
+
return { cli, desktop, ...(exec ? { exec } : {}), ...(subagents ? { subagents } : {}), ...(archived ? { archived } : {}), ...(summaries ? { summaries } : {}) };
|
|
3568
5069
|
}
|
|
3569
5070
|
|
|
3570
5071
|
function readClaudeDesktopSessions(options = {}, env = process.env) {
|
|
@@ -7356,10 +8857,10 @@ function readOpenCodeSqliteSessionsFromDb(dbPath, options = {}, env = process.en
|
|
|
7356
8857
|
if (!sqliteTableExists(dbPath, "session") || !sqliteTableExists(dbPath, "message") || !sqliteTableExists(dbPath, "part")) return [];
|
|
7357
8858
|
const sessionRows = readOpenCodeSqliteSessionRows(dbPath, options);
|
|
7358
8859
|
if (!sessionRows.length) return [];
|
|
7359
|
-
const classifications = openCodeSqliteSessionClassifications(sessionRows, dbPath, env, options);
|
|
7360
8860
|
const sessionIds = sessionRows.map((row) => row.id).filter(Boolean);
|
|
7361
8861
|
const messageRows = sortOpenCodeSqliteRows(readOpenCodeSqliteMessageRows(dbPath, sessionIds), ["session_id", "time_created", "id"]);
|
|
7362
8862
|
const partRows = sortOpenCodeSqliteRows(readOpenCodeSqlitePartRows(dbPath, sessionIds), ["session_id", "message_id", "time_created", "id"]);
|
|
8863
|
+
const classifications = openCodeSqliteSessionClassifications(sessionRows, dbPath, env, options, messageRows);
|
|
7363
8864
|
const messagesBySession = groupRowsBy(messageRows, "session_id");
|
|
7364
8865
|
const partsByMessage = groupRowsBy(partRows, "message_id");
|
|
7365
8866
|
const storageRoot = path.join(path.dirname(dbPath), "storage");
|
|
@@ -7450,9 +8951,10 @@ function readOpenCodeSqliteSessionRows(dbPath, options = {}) {
|
|
|
7450
8951
|
return readSqliteJson(dbPath, queryParts.join(" "), "OpenCode SQLite sessions");
|
|
7451
8952
|
}
|
|
7452
8953
|
|
|
7453
|
-
function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process.env, options = {}) {
|
|
8954
|
+
function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process.env, options = {}, messageRows = []) {
|
|
7454
8955
|
const baseSourceType = openCodeSqliteSourceType(dbPath, env, options);
|
|
7455
8956
|
const rowsById = new Map((sessionRows || []).map((row) => [String(row.id || ""), row]).filter(([id]) => id));
|
|
8957
|
+
const messagesBySession = groupRowsBy(messageRows, "session_id");
|
|
7456
8958
|
const desktopHints = openCodeDesktopSessionHints(env);
|
|
7457
8959
|
const sourceTypes = new Map();
|
|
7458
8960
|
const hintFiles = new Map();
|
|
@@ -7477,7 +8979,7 @@ function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process
|
|
|
7477
8979
|
return parentSourceType;
|
|
7478
8980
|
}
|
|
7479
8981
|
}
|
|
7480
|
-
const sourceType = openCodeSqliteRowSourceType(row, dbPath, env, options, baseSourceType);
|
|
8982
|
+
const sourceType = openCodeSqliteRowSourceType(row, dbPath, env, options, baseSourceType, messagesBySession.get(id) || []);
|
|
7481
8983
|
sourceTypes.set(id, sourceType);
|
|
7482
8984
|
return sourceType;
|
|
7483
8985
|
};
|
|
@@ -7485,10 +8987,10 @@ function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process
|
|
|
7485
8987
|
return { sourceTypes, hintFiles };
|
|
7486
8988
|
}
|
|
7487
8989
|
|
|
7488
|
-
function openCodeSqliteRowSourceType(row, dbPath, env = process.env, options = {}, baseSourceType = openCodeSqliteSourceType(dbPath, env, options)) {
|
|
8990
|
+
function openCodeSqliteRowSourceType(row, dbPath, env = process.env, options = {}, baseSourceType = openCodeSqliteSourceType(dbPath, env, options), messageRows = []) {
|
|
7489
8991
|
if (baseSourceType === "opencode-desktop-sqlite-history") return baseSourceType;
|
|
7490
8992
|
if (!pathInsideAny(dbPath, openCodeCliDataRoots(env)) && !OPENCODE_SOURCE_KINDS.has(options.openCodeKind)) return baseSourceType;
|
|
7491
|
-
if (openCodeSqliteRowHasCliMetadata(row)) return "opencode-cli-sqlite-history";
|
|
8993
|
+
if (openCodeSqliteRowHasCliMetadata(row) || openCodeSqliteMessagesHaveCliMetadata(messageRows)) return "opencode-cli-sqlite-history";
|
|
7492
8994
|
if (openCodeSqliteRowLooksWeb(row, dbPath, env)) return "opencode-web-sqlite-history";
|
|
7493
8995
|
return baseSourceType;
|
|
7494
8996
|
}
|
|
@@ -7497,6 +8999,13 @@ function openCodeSqliteRowHasCliMetadata(row) {
|
|
|
7497
8999
|
return Boolean(firstString(row?.agent, row?.model));
|
|
7498
9000
|
}
|
|
7499
9001
|
|
|
9002
|
+
function openCodeSqliteMessagesHaveCliMetadata(rows = []) {
|
|
9003
|
+
return rows.some((row) => {
|
|
9004
|
+
const data = parseJsonObject(row?.data);
|
|
9005
|
+
return Boolean(firstString(data.agent, data.mode, data.path?.cwd, data.path?.root));
|
|
9006
|
+
});
|
|
9007
|
+
}
|
|
9008
|
+
|
|
7500
9009
|
function openCodeSqliteRowLooksWeb(row, dbPath, env = process.env) {
|
|
7501
9010
|
const version = firstString(row?.version);
|
|
7502
9011
|
if (!version || version === "local") return false;
|
|
@@ -7560,7 +9069,7 @@ function readOpenCodeSqliteMessageRows(dbPath, sessionIds = []) {
|
|
|
7560
9069
|
"session_id",
|
|
7561
9070
|
sqliteSelectMaybe(columns, "message", "time_created"),
|
|
7562
9071
|
sqliteSelectMaybe(columns, "message", "time_updated"),
|
|
7563
|
-
|
|
9072
|
+
openCodeSqliteMessageDataSelect(dbPath, columns)
|
|
7564
9073
|
];
|
|
7565
9074
|
return readOpenCodeSqliteRowsForSessionIds(
|
|
7566
9075
|
dbPath,
|
|
@@ -7571,6 +9080,20 @@ function readOpenCodeSqliteMessageRows(dbPath, sessionIds = []) {
|
|
|
7571
9080
|
);
|
|
7572
9081
|
}
|
|
7573
9082
|
|
|
9083
|
+
function openCodeSqliteMessageDataSelect(dbPath, columns) {
|
|
9084
|
+
if (!columns.has("data")) return "null as data";
|
|
9085
|
+
if (!sqliteJsonFunctionsAvailable(dbPath)) return "message.data";
|
|
9086
|
+
return "case when json_valid(message.data) then json_remove(message.data, '$.summary') else message.data end as data";
|
|
9087
|
+
}
|
|
9088
|
+
|
|
9089
|
+
function sqliteJsonFunctionsAvailable(dbPath) {
|
|
9090
|
+
try {
|
|
9091
|
+
return readSqliteJson(dbPath, "select json_valid('{\"ok\":true}') as ok", "SQLite JSON function check")[0]?.ok === 1;
|
|
9092
|
+
} catch {
|
|
9093
|
+
return false;
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
9096
|
+
|
|
7574
9097
|
function readOpenCodeSqlitePartRows(dbPath, sessionIds = []) {
|
|
7575
9098
|
const columns = sqliteTableColumns(dbPath, "part");
|
|
7576
9099
|
if (!columns.has("id") || !columns.has("message_id") || !columns.has("session_id")) return [];
|
|
@@ -9525,6 +11048,22 @@ function fileFingerprint(file, stat = safeStat(file)) {
|
|
|
9525
11048
|
return `${file}:${stat?.size || 0}:${Math.floor(stat?.mtimeMs || 0)}`;
|
|
9526
11049
|
}
|
|
9527
11050
|
|
|
11051
|
+
function filesFingerprint(files) {
|
|
11052
|
+
return (files || [])
|
|
11053
|
+
.map((file) => fileFingerprint(file, safeStat(file)))
|
|
11054
|
+
.sort((a, b) => a.localeCompare(b))
|
|
11055
|
+
.join("|");
|
|
11056
|
+
}
|
|
11057
|
+
|
|
11058
|
+
function latestFileMtimeMs(files, primaryStat = null) {
|
|
11059
|
+
let latest = primaryStat?.mtimeMs || 0;
|
|
11060
|
+
for (const file of files || []) {
|
|
11061
|
+
const stat = safeStat(file);
|
|
11062
|
+
if (stat?.mtimeMs && stat.mtimeMs > latest) latest = stat.mtimeMs;
|
|
11063
|
+
}
|
|
11064
|
+
return latest;
|
|
11065
|
+
}
|
|
11066
|
+
|
|
9528
11067
|
function fileSha256(file) {
|
|
9529
11068
|
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
9530
11069
|
}
|
|
@@ -9648,6 +11187,7 @@ module.exports = {
|
|
|
9648
11187
|
importWebChat,
|
|
9649
11188
|
normalizeEventRole,
|
|
9650
11189
|
parseClaudeDesktopSessionFile,
|
|
11190
|
+
parseClaudeSubagentDefinitionFile,
|
|
9651
11191
|
parseAgentJsonl,
|
|
9652
11192
|
parseJsonlHistoryFile,
|
|
9653
11193
|
parseSince,
|
|
@@ -9662,6 +11202,7 @@ module.exports = {
|
|
|
9662
11202
|
readAiderSessions,
|
|
9663
11203
|
readAntigravitySessions,
|
|
9664
11204
|
readClineSessions,
|
|
11205
|
+
readClaudeSubagentDefinitions,
|
|
9665
11206
|
readDevinSessionsFromDb,
|
|
9666
11207
|
readGeminiCliSessions,
|
|
9667
11208
|
readOpenCodeSessions,
|