agentel 0.2.5 → 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 +77 -37
- package/docs/code-reference.md +26 -13
- package/docs/history-source-handling.md +247 -82
- package/docs/release.md +1 -1
- package/package.json +5 -2
- package/src/archive.js +200 -17
- package/src/canonical-events.js +74 -25
- package/src/cli.js +2561 -204
- package/src/config.js +11 -0
- package/src/doctor.js +2 -0
- package/src/importers/claude.js +309 -11
- package/src/importers/gemini.js +2 -1
- package/src/importers/providers.js +22 -0
- package/src/importers.js +2142 -212
- package/src/parser-versions.js +1 -0
- package/src/search.js +417 -176
- package/src/sources.js +1 -0
- package/src/web-export-instructions.js +79 -0
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");
|
|
@@ -19,9 +19,11 @@ const { parseGeminiCliJsonSessions, parseGeminiCliJsonlSessions } = require("./i
|
|
|
19
19
|
const { importSourceWithAdapter, providerAdapterForSource, providerAdapterSources } = require("./importers/providers");
|
|
20
20
|
const { parseSince } = require("./importers/shared");
|
|
21
21
|
const { canonicalWebProvider, derivedAccountId, getWebAccount, upsertWebAccount } = require("./web-accounts");
|
|
22
|
+
const { manualImportInstructionResult } = require("./web-export-instructions");
|
|
22
23
|
|
|
23
24
|
const WEB_TOKEN_ESTIMATE_CHARS = 4;
|
|
24
25
|
const WEB_CHAT_TOKEN_ESTIMATION_METHOD = "web-message-parts-chars-v1";
|
|
26
|
+
const EXPORT_ZIP_ENTRY_MAX_BUFFER = 1024 * 1024 * 512;
|
|
25
27
|
const OPENCODE_SOURCE_KINDS = new Set(["cli", "desktop", "web"]);
|
|
26
28
|
const OPENCODE_SESSION_ID_RE = /\bses_[A-Za-z0-9]+\b/g;
|
|
27
29
|
|
|
@@ -46,6 +48,7 @@ function importProviderHelpers() {
|
|
|
46
48
|
importCursorProvider,
|
|
47
49
|
importJsonlProvider,
|
|
48
50
|
importStructuredProvider,
|
|
51
|
+
manualImportInstructionResult,
|
|
49
52
|
readAntigravitySessions,
|
|
50
53
|
readAiderSessions,
|
|
51
54
|
readClineSessions,
|
|
@@ -79,10 +82,22 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
79
82
|
const files =
|
|
80
83
|
provider === "claude_code" ? claudeFiles(env) : provider === "claude_sdk" ? claudeSdkFiles(env) : jsonlFiles(roots);
|
|
81
84
|
const claudeCodeMetadata = provider === "claude_code" ? claudeCodeSessionMetadataByCliSessionId(env) : new Map();
|
|
85
|
+
const claudeSubagentCache = provider === "claude_code" ? new Map() : null;
|
|
82
86
|
|
|
83
87
|
const candidates = files
|
|
84
|
-
.map((file) =>
|
|
85
|
-
|
|
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()))
|
|
86
101
|
.sort((a, b) => a.file.localeCompare(b.file));
|
|
87
102
|
|
|
88
103
|
const summary = {
|
|
@@ -98,7 +113,11 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
98
113
|
for (let index = 0; index < candidates.length; index++) {
|
|
99
114
|
const item = candidates[index];
|
|
100
115
|
const sourceType = jsonlProviderSourceType(provider);
|
|
101
|
-
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(":");
|
|
102
121
|
const preliminaryMetadata = provider === "claude_code"
|
|
103
122
|
? claudeCodeMetadata.get(claudeSessionIdFromFilename(item.file)) || null
|
|
104
123
|
: null;
|
|
@@ -128,6 +147,12 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
128
147
|
const cwd = parsed.cwd || sessionMetadata?.cwd || "";
|
|
129
148
|
const scopeCanonical = cwd ? "" : uncategorizedScope(provider);
|
|
130
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;
|
|
131
156
|
if (options.repos && options.repos.length && (!repo || !options.repos.includes(repo.key))) {
|
|
132
157
|
summary.skipped++;
|
|
133
158
|
reportProgress(options, summary, index + 1, item.file);
|
|
@@ -154,18 +179,48 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
154
179
|
startedAt: parsed.startedAt,
|
|
155
180
|
endedAt: parsed.endedAt,
|
|
156
181
|
sourcePath: item.file,
|
|
157
|
-
sourceFiles: [
|
|
182
|
+
sourceFiles: [
|
|
183
|
+
item.file,
|
|
184
|
+
sessionMetadata?.sourcePath || "",
|
|
185
|
+
...auxiliaryFiles,
|
|
186
|
+
...(claudeSubagents?.sourceFiles || []),
|
|
187
|
+
...(claudeSubagentRuns?.sourceFiles || [])
|
|
188
|
+
].filter(Boolean),
|
|
158
189
|
sourceType,
|
|
159
190
|
title: jsonlSessionTitleForImport(parsed, sessionMetadata),
|
|
160
|
-
sessionSummary:
|
|
191
|
+
sessionSummary: mergeSessionSummaries(
|
|
192
|
+
claudeCodeSidecarSessionSummary(sessionMetadata),
|
|
193
|
+
parsed.sessionSummary,
|
|
194
|
+
claudeSubagents?.sessionSummary,
|
|
195
|
+
claudeSubagentRuns?.sessionSummary
|
|
196
|
+
)
|
|
161
197
|
},
|
|
162
198
|
env
|
|
163
199
|
);
|
|
164
200
|
state.files[fingerprint] = { sessionId, at: new Date().toISOString() };
|
|
165
201
|
state.sessions[sessionId] = { provider, sourcePath: item.file, fingerprint, auxiliary: auxiliaryFiles.length || undefined, at: new Date().toISOString() };
|
|
166
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
|
+
}
|
|
167
222
|
}
|
|
168
|
-
summary.imported
|
|
223
|
+
summary.imported += 1 + (claudeSubagentRuns?.sessions?.length || 0);
|
|
169
224
|
reportProgress(options, summary, index + 1, item.file);
|
|
170
225
|
}
|
|
171
226
|
|
|
@@ -179,6 +234,52 @@ function jsonlProviderSourceType(provider) {
|
|
|
179
234
|
return "cli-history";
|
|
180
235
|
}
|
|
181
236
|
|
|
237
|
+
function codexThreadSourceType(thread) {
|
|
238
|
+
if (thread.source === "vscode") return "codex-desktop-history";
|
|
239
|
+
if (thread.source === "exec") return "codex-sdk-history";
|
|
240
|
+
return "codex-cli-history";
|
|
241
|
+
}
|
|
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
|
+
|
|
182
283
|
function importClaudeDesktopProvider(provider, since, options = {}, env = process.env) {
|
|
183
284
|
const state = loadImportState(env);
|
|
184
285
|
const archived = archivedSessionKeys(env);
|
|
@@ -257,17 +358,20 @@ function matchesImportedSessionRepo(session, repo, wantedRepos) {
|
|
|
257
358
|
|
|
258
359
|
function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
259
360
|
const threads = readCodexSessionEntries(env).filter((thread) => {
|
|
260
|
-
if (options.codexSource
|
|
261
|
-
|
|
262
|
-
return true;
|
|
361
|
+
if (options.codexSource) return thread.source === options.codexSource;
|
|
362
|
+
return ["cli", "vscode"].includes(thread.source);
|
|
263
363
|
});
|
|
364
|
+
const codexSubagentChildren = codexSubagentChildrenByParent(threads);
|
|
365
|
+
if (!threads.length && options.codexSource && options.codexSource !== "cli") {
|
|
366
|
+
return { provider, discovered: 0, candidates: 0, imported: 0, skipped: 0, errors: [], details: {} };
|
|
367
|
+
}
|
|
264
368
|
if (!threads.length) return importJsonlProvider(provider, codexRoots(env), since, options, env);
|
|
265
369
|
|
|
266
370
|
const state = loadImportState(env);
|
|
267
371
|
const archived = archivedSessionKeys(env);
|
|
268
372
|
const candidates = threads
|
|
269
373
|
.filter((thread) => thread.rolloutPath)
|
|
270
|
-
.filter((thread) => !since || new Date(thread.
|
|
374
|
+
.filter((thread) => !since || new Date(codexThreadImportUpdatedAt(thread, codexSubagentChildren.get(thread.id) || [])) >= since)
|
|
271
375
|
.sort((a, b) => a.rolloutPath.localeCompare(b.rolloutPath));
|
|
272
376
|
const summary = {
|
|
273
377
|
provider,
|
|
@@ -288,8 +392,14 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
288
392
|
reportProgress(options, summary, index + 1, thread.rolloutPath);
|
|
289
393
|
continue;
|
|
290
394
|
}
|
|
291
|
-
const sourceType = thread
|
|
292
|
-
const
|
|
395
|
+
const sourceType = codexThreadSourceType(thread);
|
|
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(":");
|
|
293
403
|
if (alreadyImported(state, thread.id, fingerprint, archived, provider)) {
|
|
294
404
|
summary.skipped++;
|
|
295
405
|
reportProgress(options, summary, index + 1, thread.rolloutPath);
|
|
@@ -303,10 +413,7 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
303
413
|
reportProgress(options, summary, index + 1, thread.rolloutPath);
|
|
304
414
|
continue;
|
|
305
415
|
}
|
|
306
|
-
parsed
|
|
307
|
-
parsed.messages = dedupeAdjacentMessages(parsed.messages).sort((a, b) => String(a.timestamp || "").localeCompare(String(b.timestamp || "")));
|
|
308
|
-
parsed.startedAt = parsed.messages[0]?.timestamp || parsed.startedAt;
|
|
309
|
-
parsed.endedAt = parsed.messages[parsed.messages.length - 1]?.timestamp || parsed.endedAt;
|
|
416
|
+
parsed = finalizeCodexParsedThread(thread, parsed);
|
|
310
417
|
if (!parsed.messages.length) {
|
|
311
418
|
state.files[fingerprint] = { skipped: true, reason: "no messages", at: new Date().toISOString() };
|
|
312
419
|
summary.skipped++;
|
|
@@ -328,6 +435,8 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
328
435
|
reportProgress(options, summary, index + 1, thread.rolloutPath);
|
|
329
436
|
continue;
|
|
330
437
|
}
|
|
438
|
+
const codexSubagentRuns = codexSubagentRunImportContext(thread, children, env);
|
|
439
|
+
const codexSubagentRun = codexSubagentSessionSummary(thread, parsed, env);
|
|
331
440
|
if (!options.dryRun) {
|
|
332
441
|
writeSession(
|
|
333
442
|
{
|
|
@@ -340,9 +449,12 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
340
449
|
startedAt: parsed.startedAt || thread.createdAt,
|
|
341
450
|
endedAt: parsed.endedAt || thread.updatedAt,
|
|
342
451
|
sourcePath: thread.rolloutPath,
|
|
343
|
-
sourceFiles: codexSourceFiles(thread, env),
|
|
452
|
+
sourceFiles: codexSourceFiles(thread, env, children),
|
|
344
453
|
sourceType,
|
|
345
|
-
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
|
|
346
458
|
},
|
|
347
459
|
env
|
|
348
460
|
);
|
|
@@ -579,10 +691,12 @@ function structuredSessionUsesSharedRawFiles(provider, sourceType) {
|
|
|
579
691
|
|
|
580
692
|
function parseAgentJsonl(file, provider) {
|
|
581
693
|
const text = readTextMaybeZstd(file);
|
|
694
|
+
const events = [];
|
|
582
695
|
const messages = [];
|
|
583
696
|
let cwd = "";
|
|
584
697
|
let sessionId = "";
|
|
585
698
|
let title = "";
|
|
699
|
+
let titleSource = "";
|
|
586
700
|
const context = { model: "", tokenUsage: { input: 0, output: 0 } };
|
|
587
701
|
for (const line of text.split(/\r?\n/)) {
|
|
588
702
|
if (!line.trim()) continue;
|
|
@@ -592,6 +706,7 @@ function parseAgentJsonl(file, provider) {
|
|
|
592
706
|
} catch {
|
|
593
707
|
continue;
|
|
594
708
|
}
|
|
709
|
+
events.push(event);
|
|
595
710
|
cwd ||= firstString(event.cwd, event.working_directory, event.workingDirectory, event.context?.cwd, event.payload?.cwd);
|
|
596
711
|
sessionId ||= firstString(
|
|
597
712
|
event.session_id,
|
|
@@ -603,7 +718,24 @@ function parseAgentJsonl(file, provider) {
|
|
|
603
718
|
event.payload?.session_id,
|
|
604
719
|
event.sessionId
|
|
605
720
|
);
|
|
606
|
-
|
|
721
|
+
const codexThreadTitle = provider === "codex" ? codexThreadNameFromEvent(event) : "";
|
|
722
|
+
if (codexThreadTitle) {
|
|
723
|
+
title = codexThreadTitle;
|
|
724
|
+
titleSource = "thread-name";
|
|
725
|
+
} else if (!title) {
|
|
726
|
+
const sourceTitle = firstString(event.title, event.conversation_title, event.payload?.title);
|
|
727
|
+
const aiTitle = firstString(event.aiTitle, event.ai_title);
|
|
728
|
+
if (sourceTitle) {
|
|
729
|
+
title = sourceTitle;
|
|
730
|
+
titleSource = "source";
|
|
731
|
+
} else if (aiTitle) {
|
|
732
|
+
title = aiTitle;
|
|
733
|
+
titleSource = "ai-title";
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
collectClaudeJsonlSessionMetadata(event, provider, context);
|
|
737
|
+
}
|
|
738
|
+
for (const event of events) {
|
|
607
739
|
updateCodexParseContext(event, provider, context);
|
|
608
740
|
updateClaudeParseContext(event, provider, context);
|
|
609
741
|
if (applyCodexTokenCount(event, provider, messages, context)) continue;
|
|
@@ -617,13 +749,292 @@ function parseAgentJsonl(file, provider) {
|
|
|
617
749
|
cwd,
|
|
618
750
|
sessionId,
|
|
619
751
|
title: title || inferredTitle,
|
|
620
|
-
titleSource:
|
|
752
|
+
titleSource: titleSource || (inferredTitle ? "first-user-prompt" : ""),
|
|
753
|
+
sessionSummary: claudeJsonlSessionSummary(provider, context),
|
|
621
754
|
messages: deduped,
|
|
622
755
|
startedAt: deduped[0]?.timestamp || "",
|
|
623
756
|
endedAt: deduped[deduped.length - 1]?.timestamp || ""
|
|
624
757
|
};
|
|
625
758
|
}
|
|
626
759
|
|
|
760
|
+
function collectClaudeJsonlSessionMetadata(event, provider, context = {}) {
|
|
761
|
+
if (!isClaudeJsonlProvider(provider) || !event || typeof event !== "object") return;
|
|
762
|
+
const metadata = context.claudeJsonl || (context.claudeJsonl = {
|
|
763
|
+
eventTypeCounts: {},
|
|
764
|
+
entrypoints: new Set(),
|
|
765
|
+
userTypes: new Set(),
|
|
766
|
+
versions: new Set(),
|
|
767
|
+
gitBranches: new Set(),
|
|
768
|
+
permissionModes: new Set(),
|
|
769
|
+
promptIds: new Set(),
|
|
770
|
+
requestIds: new Set(),
|
|
771
|
+
sourceToolAssistantUUIDs: new Set(),
|
|
772
|
+
attachmentTypes: new Set(),
|
|
773
|
+
remoteToolNames: new Set(),
|
|
774
|
+
mcpServerNames: new Set(),
|
|
775
|
+
toolNames: new Set(),
|
|
776
|
+
agentIds: new Set(),
|
|
777
|
+
slugs: new Set(),
|
|
778
|
+
sourceToolUseIDs: new Set(),
|
|
779
|
+
parentToolUseIDs: new Set(),
|
|
780
|
+
toolUseIDs: new Set(),
|
|
781
|
+
attributionSkills: new Set(),
|
|
782
|
+
queue: {},
|
|
783
|
+
attachmentDetails: {},
|
|
784
|
+
apiErrors: {},
|
|
785
|
+
mcpStructuredContentCount: 0,
|
|
786
|
+
aiTitles: new Set(),
|
|
787
|
+
lastPrompt: "",
|
|
788
|
+
sidechainMessageCount: 0,
|
|
789
|
+
messageEventCount: 0,
|
|
790
|
+
remoteControl: false
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const type = firstString(event.type, event.kind);
|
|
794
|
+
if (type) metadata.eventTypeCounts[type] = (metadata.eventTypeCounts[type] || 0) + 1;
|
|
795
|
+
addSetValue(metadata.entrypoints, event.entrypoint);
|
|
796
|
+
addSetValue(metadata.userTypes, event.userType, event.user_type);
|
|
797
|
+
addSetValue(metadata.versions, event.version);
|
|
798
|
+
addSetValue(metadata.gitBranches, event.gitBranch, event.git_branch);
|
|
799
|
+
addSetValue(metadata.permissionModes, event.permissionMode, event.permission_mode);
|
|
800
|
+
addSetValue(metadata.promptIds, event.promptId, event.prompt_id);
|
|
801
|
+
addSetValue(metadata.requestIds, event.requestId, event.request_id);
|
|
802
|
+
addSetValue(metadata.sourceToolAssistantUUIDs, event.sourceToolAssistantUUID, event.source_tool_assistant_uuid);
|
|
803
|
+
addSetValue(metadata.agentIds, event.agentId, event.agent_id);
|
|
804
|
+
addSetValue(metadata.slugs, event.slug);
|
|
805
|
+
addSetValue(metadata.sourceToolUseIDs, event.sourceToolUseID, event.source_tool_use_id);
|
|
806
|
+
addSetValue(metadata.parentToolUseIDs, event.parentToolUseID, event.parent_tool_use_id);
|
|
807
|
+
addSetValue(metadata.toolUseIDs, event.toolUseID, event.tool_use_id);
|
|
808
|
+
addSetValue(metadata.attributionSkills, event.attributionSkill, event.attribution_skill);
|
|
809
|
+
if (event.mcpMeta?.structuredContent && typeof event.mcpMeta.structuredContent === "object") metadata.mcpStructuredContentCount++;
|
|
810
|
+
collectClaudeApiError(event, metadata);
|
|
811
|
+
if (event.isSidechain === true) metadata.sidechainMessageCount++;
|
|
812
|
+
if ((event.type === "user" || event.type === "assistant") && event.message) metadata.messageEventCount++;
|
|
813
|
+
|
|
814
|
+
const aiTitle = firstString(event.aiTitle, event.ai_title);
|
|
815
|
+
if (aiTitle) metadata.aiTitles.add(aiTitle);
|
|
816
|
+
const lastPrompt = firstString(event.lastPrompt, event.last_prompt);
|
|
817
|
+
if (lastPrompt) metadata.lastPrompt = lastPrompt;
|
|
818
|
+
|
|
819
|
+
if (event.type === "queue-operation") collectClaudeQueueOperation(event, metadata);
|
|
820
|
+
if (event.type === "attachment") collectClaudeAttachmentMetadata(event, metadata);
|
|
821
|
+
collectClaudeToolNames(event, metadata);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function collectClaudeQueueOperation(event, metadata) {
|
|
825
|
+
const op = firstString(event.operation).toLowerCase();
|
|
826
|
+
if (!op) return;
|
|
827
|
+
const key = op.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "unknown";
|
|
828
|
+
metadata.queue[key] = (metadata.queue[key] || 0) + 1;
|
|
829
|
+
const timestamp = toIso(event.timestamp || event.created_at || event.createdAt || event.time);
|
|
830
|
+
if (timestamp) {
|
|
831
|
+
if (!metadata.queue.firstAt || timestamp < metadata.queue.firstAt) metadata.queue.firstAt = timestamp;
|
|
832
|
+
if (!metadata.queue.lastAt || timestamp > metadata.queue.lastAt) metadata.queue.lastAt = timestamp;
|
|
833
|
+
const upper = key.charAt(0).toUpperCase() + key.slice(1);
|
|
834
|
+
if (!metadata.queue[`first${upper}At`] || timestamp < metadata.queue[`first${upper}At`]) metadata.queue[`first${upper}At`] = timestamp;
|
|
835
|
+
if (!metadata.queue[`last${upper}At`] || timestamp > metadata.queue[`last${upper}At`]) metadata.queue[`last${upper}At`] = timestamp;
|
|
836
|
+
}
|
|
837
|
+
if (typeof event.content === "string" && event.content.trim()) {
|
|
838
|
+
metadata.queue.lastContent = event.content.trim();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function collectClaudeApiError(event, metadata) {
|
|
843
|
+
if (!event || typeof event !== "object") return;
|
|
844
|
+
if (event.isApiErrorMessage !== true && !event.error && event.apiErrorStatus == null) return;
|
|
845
|
+
const status = Number(event.apiErrorStatus);
|
|
846
|
+
if (Number.isFinite(status)) {
|
|
847
|
+
const key = String(status);
|
|
848
|
+
metadata.apiErrors.statusCounts = metadata.apiErrors.statusCounts || {};
|
|
849
|
+
metadata.apiErrors.statusCounts[key] = (metadata.apiErrors.statusCounts[key] || 0) + 1;
|
|
850
|
+
}
|
|
851
|
+
const type = firstString(event.error);
|
|
852
|
+
if (type) {
|
|
853
|
+
metadata.apiErrors.typeCounts = metadata.apiErrors.typeCounts || {};
|
|
854
|
+
metadata.apiErrors.typeCounts[type] = (metadata.apiErrors.typeCounts[type] || 0) + 1;
|
|
855
|
+
}
|
|
856
|
+
metadata.apiErrors.count = (metadata.apiErrors.count || 0) + 1;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function collectClaudeAttachmentMetadata(event, metadata) {
|
|
860
|
+
const attachment = event.attachment && typeof event.attachment === "object" ? event.attachment : {};
|
|
861
|
+
addSetValue(metadata.attachmentTypes, attachment.type);
|
|
862
|
+
collectClaudeAttachmentDetails(attachment, metadata);
|
|
863
|
+
for (const name of [...asStringArray(attachment.addedNames), ...asStringArray(attachment.removedNames)]) {
|
|
864
|
+
if (typeof name !== "string" || !name.trim()) continue;
|
|
865
|
+
const value = name.trim();
|
|
866
|
+
metadata.remoteToolNames.add(value);
|
|
867
|
+
if (isClaudeRemoteControlToolName(value)) metadata.remoteControl = true;
|
|
868
|
+
const server = claudeMcpServerNameFromTool(value);
|
|
869
|
+
if (server) metadata.mcpServerNames.add(server);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function collectClaudeAttachmentDetails(attachment, metadata) {
|
|
874
|
+
const type = firstString(attachment.type);
|
|
875
|
+
if (!type) return;
|
|
876
|
+
const details = metadata.attachmentDetails;
|
|
877
|
+
details.countByType = details.countByType || {};
|
|
878
|
+
details.countByType[type] = (details.countByType[type] || 0) + 1;
|
|
879
|
+
if (type === "nested_memory") {
|
|
880
|
+
details.nestedMemoryPaths = details.nestedMemoryPaths || new Set();
|
|
881
|
+
addSetValue(details.nestedMemoryPaths, firstString(attachment.path, attachment.content?.path, attachment.displayPath));
|
|
882
|
+
} else if (type === "edited_text_file") {
|
|
883
|
+
details.editedTextFiles = details.editedTextFiles || new Set();
|
|
884
|
+
addSetValue(details.editedTextFiles, firstString(attachment.path, attachment.content?.path, attachment.displayPath));
|
|
885
|
+
} else if (type === "queued_command") {
|
|
886
|
+
details.queuedCommandCount = (details.queuedCommandCount || 0) + 1;
|
|
887
|
+
details.lastQueuedCommand = compactMetadata({
|
|
888
|
+
prompt: firstString(attachment.prompt),
|
|
889
|
+
commandMode: firstString(attachment.commandMode, attachment.command_mode)
|
|
890
|
+
});
|
|
891
|
+
} else if (type === "command_permissions") {
|
|
892
|
+
details.commandPermissionCount = (details.commandPermissionCount || 0) + 1;
|
|
893
|
+
details.lastAllowedTools = asStringArray(attachment.allowedTools);
|
|
894
|
+
} else if (type === "date_change") {
|
|
895
|
+
details.dateChanges = details.dateChanges || new Set();
|
|
896
|
+
addSetValue(details.dateChanges, attachment.newDate, attachment.new_date);
|
|
897
|
+
} else if (type === "max_turns_reached") {
|
|
898
|
+
details.maxTurnsReachedCount = (details.maxTurnsReachedCount || 0) + 1;
|
|
899
|
+
details.lastMaxTurnsReached = compactMetadata({
|
|
900
|
+
maxTurns: numericValue(attachment.maxTurns, attachment.max_turns),
|
|
901
|
+
turnCount: numericValue(attachment.turnCount, attachment.turn_count)
|
|
902
|
+
});
|
|
903
|
+
} else if (type === "compact_file_reference") {
|
|
904
|
+
details.compactFileReferences = details.compactFileReferences || new Set();
|
|
905
|
+
addSetValue(details.compactFileReferences, attachment.path, attachment.displayPath);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function collectClaudeToolNames(event, metadata) {
|
|
910
|
+
const parts = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
911
|
+
for (const part of parts) {
|
|
912
|
+
if (!part || typeof part !== "object") continue;
|
|
913
|
+
const type = String(part.type || part.kind || "").toLowerCase();
|
|
914
|
+
if (type !== "tool_use" && type !== "server_tool_use" && type !== "mcp_tool_use" && type !== "function_call") continue;
|
|
915
|
+
addSetValue(metadata.toolNames, part.name, part.tool_name, part.toolName, part.function?.name);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function claudeMcpServerNameFromTool(name) {
|
|
920
|
+
const match = String(name || "").match(/^mcp__(.+?)__/);
|
|
921
|
+
if (!match) return "";
|
|
922
|
+
return match[1];
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const CLAUDE_REMOTE_CONTROL_TOOL_NAMES = new Set([
|
|
926
|
+
"RemoteTrigger",
|
|
927
|
+
"TaskOutput",
|
|
928
|
+
"TaskStop",
|
|
929
|
+
"PushNotification",
|
|
930
|
+
"AskUserQuestion",
|
|
931
|
+
"Monitor",
|
|
932
|
+
"TaskCreate",
|
|
933
|
+
"TaskUpdate",
|
|
934
|
+
"TaskList",
|
|
935
|
+
"TaskGet",
|
|
936
|
+
"CronCreate",
|
|
937
|
+
"CronDelete",
|
|
938
|
+
"CronList"
|
|
939
|
+
]);
|
|
940
|
+
|
|
941
|
+
function isClaudeRemoteControlToolName(name) {
|
|
942
|
+
const value = String(name || "").trim();
|
|
943
|
+
return CLAUDE_REMOTE_CONTROL_TOOL_NAMES.has(value);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function claudeJsonlSessionSummary(provider, context = {}) {
|
|
947
|
+
if (!isClaudeJsonlProvider(provider)) return null;
|
|
948
|
+
const metadata = context.claudeJsonl;
|
|
949
|
+
if (!metadata) return null;
|
|
950
|
+
const remoteToolNames = sortedSet(metadata.remoteToolNames);
|
|
951
|
+
const mcpServerNames = sortedSet(metadata.mcpServerNames);
|
|
952
|
+
const attachmentDetails = claudeAttachmentDetailsSummary(metadata.attachmentDetails);
|
|
953
|
+
const result = compactMetadata({
|
|
954
|
+
claudeJsonl: compactMetadata({
|
|
955
|
+
entrypoint: singleOrArray(metadata.entrypoints),
|
|
956
|
+
userType: singleOrArray(metadata.userTypes),
|
|
957
|
+
version: singleOrArray(metadata.versions),
|
|
958
|
+
gitBranch: singleOrArray(metadata.gitBranches),
|
|
959
|
+
permissionModes: sortedSet(metadata.permissionModes),
|
|
960
|
+
eventTypeCounts: compactMetadata(metadata.eventTypeCounts),
|
|
961
|
+
messageEventCount: metadata.messageEventCount || undefined,
|
|
962
|
+
sidechainMessageCount: metadata.sidechainMessageCount || undefined,
|
|
963
|
+
promptIdCount: metadata.promptIds.size || undefined,
|
|
964
|
+
requestIdCount: metadata.requestIds.size || undefined,
|
|
965
|
+
sourceToolAssistantUUIDCount: metadata.sourceToolAssistantUUIDs.size || undefined,
|
|
966
|
+
agentIds: sortedSet(metadata.agentIds),
|
|
967
|
+
slugs: sortedSet(metadata.slugs),
|
|
968
|
+
sourceToolUseIDCount: metadata.sourceToolUseIDs.size || undefined,
|
|
969
|
+
parentToolUseIDCount: metadata.parentToolUseIDs.size || undefined,
|
|
970
|
+
toolUseIDCount: metadata.toolUseIDs.size || undefined,
|
|
971
|
+
attributionSkills: sortedSet(metadata.attributionSkills),
|
|
972
|
+
mcpStructuredContentCount: metadata.mcpStructuredContentCount || undefined,
|
|
973
|
+
apiErrors: compactMetadata(metadata.apiErrors),
|
|
974
|
+
attachmentTypes: sortedSet(metadata.attachmentTypes),
|
|
975
|
+
attachmentDetails,
|
|
976
|
+
toolNames: sortedSet(metadata.toolNames),
|
|
977
|
+
aiTitle: sortedSet(metadata.aiTitles)[0] || undefined,
|
|
978
|
+
lastPrompt: metadata.lastPrompt || undefined,
|
|
979
|
+
queue: compactMetadata(metadata.queue),
|
|
980
|
+
remoteControl: metadata.remoteControl ? compactMetadata({
|
|
981
|
+
detected: true,
|
|
982
|
+
toolCount: remoteToolNames.length || undefined,
|
|
983
|
+
toolNames: remoteToolNames,
|
|
984
|
+
mcpToolCount: remoteToolNames.filter((name) => name.startsWith("mcp__")).length || undefined,
|
|
985
|
+
mcpServerNames
|
|
986
|
+
}) : undefined
|
|
987
|
+
})
|
|
988
|
+
});
|
|
989
|
+
return result || null;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function claudeAttachmentDetailsSummary(details = {}) {
|
|
993
|
+
return compactMetadata({
|
|
994
|
+
countByType: compactMetadata(details.countByType),
|
|
995
|
+
nestedMemoryPaths: details.nestedMemoryPaths ? sortedSet(details.nestedMemoryPaths) : undefined,
|
|
996
|
+
editedTextFiles: details.editedTextFiles ? sortedSet(details.editedTextFiles) : undefined,
|
|
997
|
+
queuedCommandCount: details.queuedCommandCount || undefined,
|
|
998
|
+
lastQueuedCommand: details.lastQueuedCommand || undefined,
|
|
999
|
+
commandPermissionCount: details.commandPermissionCount || undefined,
|
|
1000
|
+
lastAllowedTools: details.lastAllowedTools?.length ? details.lastAllowedTools : undefined,
|
|
1001
|
+
dateChanges: details.dateChanges ? sortedSet(details.dateChanges) : undefined,
|
|
1002
|
+
maxTurnsReachedCount: details.maxTurnsReachedCount || undefined,
|
|
1003
|
+
lastMaxTurnsReached: details.lastMaxTurnsReached || undefined,
|
|
1004
|
+
compactFileReferences: details.compactFileReferences ? sortedSet(details.compactFileReferences) : undefined
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function addSetValue(set, ...values) {
|
|
1009
|
+
for (const value of values) {
|
|
1010
|
+
if (typeof value === "string" && value.trim()) set.add(value.trim());
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function asStringArray(value) {
|
|
1015
|
+
if (!Array.isArray(value)) return [];
|
|
1016
|
+
return value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim());
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function numericValue(...values) {
|
|
1020
|
+
for (const value of values) {
|
|
1021
|
+
if (value == null || value === "") continue;
|
|
1022
|
+
const number = Number(value);
|
|
1023
|
+
if (Number.isFinite(number)) return number;
|
|
1024
|
+
}
|
|
1025
|
+
return undefined;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function sortedSet(set) {
|
|
1029
|
+
return [...set].sort((a, b) => a.localeCompare(b));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function singleOrArray(set) {
|
|
1033
|
+
const values = sortedSet(set);
|
|
1034
|
+
if (!values.length) return undefined;
|
|
1035
|
+
return values.length === 1 ? values[0] : values;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
627
1038
|
function repoInfoForImport(provider, cwd, metadata = null) {
|
|
628
1039
|
const repoCwd = repoCwdForImport(provider, cwd, metadata);
|
|
629
1040
|
if (!repoCwd) return null;
|
|
@@ -648,24 +1059,53 @@ function claudeWorktreeParentRepo(provider, cwd) {
|
|
|
648
1059
|
}
|
|
649
1060
|
|
|
650
1061
|
function inferredJsonlSessionTitle(provider, messages) {
|
|
651
|
-
if (!isClaudeJsonlProvider(provider)) return "";
|
|
1062
|
+
if (!isClaudeJsonlProvider(provider) && provider !== "codex") return "";
|
|
652
1063
|
const firstUser = (messages || []).find((message) => message.role === "user" && !message.metadata?.providerGenerated);
|
|
653
1064
|
return titleFromPrompt(firstUser?.content);
|
|
654
1065
|
}
|
|
655
1066
|
|
|
656
1067
|
function titleFromPrompt(value) {
|
|
657
|
-
const cleaned =
|
|
1068
|
+
const cleaned = cleanPromptTitleLine(promptTitleLine(value));
|
|
658
1069
|
if (!cleaned) return "";
|
|
659
1070
|
const max = 96;
|
|
660
1071
|
return cleaned.length > max ? `${cleaned.slice(0, max - 1).trimEnd()}…` : cleaned;
|
|
661
1072
|
}
|
|
662
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
|
+
|
|
663
1104
|
function isClaudeJsonlProvider(provider) {
|
|
664
1105
|
return provider === "claude_code" || provider === "claude_sdk";
|
|
665
1106
|
}
|
|
666
1107
|
|
|
667
1108
|
function jsonlSessionTitleForImport(parsed, metadata = null) {
|
|
668
|
-
if (parsed?.titleSource === "source") return parsed.title || "";
|
|
669
1109
|
return firstString(metadata?.title, parsed?.title);
|
|
670
1110
|
}
|
|
671
1111
|
|
|
@@ -701,6 +1141,23 @@ function claudeCodeSidecarSessionSummary(metadata = null) {
|
|
|
701
1141
|
});
|
|
702
1142
|
}
|
|
703
1143
|
|
|
1144
|
+
function mergeSessionSummaries(...summaries) {
|
|
1145
|
+
const result = {};
|
|
1146
|
+
for (const summary of summaries) {
|
|
1147
|
+
if (!summary || typeof summary !== "object" || Array.isArray(summary)) continue;
|
|
1148
|
+
for (const [key, value] of Object.entries(summary)) {
|
|
1149
|
+
if (key === "modelUsage" && Array.isArray(value)) {
|
|
1150
|
+
result.modelUsage = [...(Array.isArray(result.modelUsage) ? result.modelUsage : []), ...value];
|
|
1151
|
+
} else if (value && typeof value === "object" && !Array.isArray(value) && result[key] && typeof result[key] === "object" && !Array.isArray(result[key])) {
|
|
1152
|
+
result[key] = compactMetadata({ ...result[key], ...value });
|
|
1153
|
+
} else {
|
|
1154
|
+
result[key] = value;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return compactMetadata(result);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
704
1161
|
function firstExistingDirectory(...values) {
|
|
705
1162
|
for (const value of values) {
|
|
706
1163
|
if (typeof value !== "string" || !value.trim()) continue;
|
|
@@ -757,9 +1214,54 @@ function dedupeAdjacentMessages(messages) {
|
|
|
757
1214
|
}
|
|
758
1215
|
result.push(message);
|
|
759
1216
|
}
|
|
1217
|
+
return dedupeDuplicateToolResults(result);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function dedupeDuplicateToolResults(messages) {
|
|
1221
|
+
const result = [];
|
|
1222
|
+
const toolResultsByKey = new Map();
|
|
1223
|
+
for (const message of messages) {
|
|
1224
|
+
const key = duplicateToolResultKey(message);
|
|
1225
|
+
if (key && toolResultsByKey.has(key)) {
|
|
1226
|
+
const existingIndex = toolResultsByKey.get(key);
|
|
1227
|
+
const existing = result[existingIndex];
|
|
1228
|
+
if (timestampsNear(existing?.timestamp, message?.timestamp)) {
|
|
1229
|
+
result[existingIndex] = preferredToolResultMessage(existing, message);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (key) toolResultsByKey.set(key, result.length);
|
|
1234
|
+
result.push(message);
|
|
1235
|
+
}
|
|
760
1236
|
return result;
|
|
761
1237
|
}
|
|
762
1238
|
|
|
1239
|
+
function duplicateToolResultKey(message) {
|
|
1240
|
+
const result = message?.metadata?.toolResult;
|
|
1241
|
+
const id = firstString(result?.id, result?.callId, result?.call_id, result?.toolCallId, result?.tool_call_id, result?.toolUseId, result?.tool_use_id);
|
|
1242
|
+
if (!id || message?.role !== "tool") return "";
|
|
1243
|
+
return `${message?.metadata?.provider || result?.provider || ""}:${id}`;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function preferredToolResultMessage(left, right) {
|
|
1247
|
+
return toolResultMessageDisplayScore(right) > toolResultMessageDisplayScore(left) ? right : left;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function toolResultMessageDisplayScore(message) {
|
|
1251
|
+
const result = message?.metadata?.toolResult || {};
|
|
1252
|
+
const rawCategory = normalizeToolToken(result.rawCategory);
|
|
1253
|
+
const kind = normalizeToolToken(result.kind);
|
|
1254
|
+
const category = normalizeToolToken(result.category);
|
|
1255
|
+
let score = 0;
|
|
1256
|
+
if (["exec_command_end", "patch_apply_end", "mcp_tool_call_end", "web_search_end", "tool_result", "tool_output"].includes(rawCategory)) score += 2000;
|
|
1257
|
+
if (rawCategory === "function_call_output" || rawCategory === "custom_tool_call_output") score -= 1000;
|
|
1258
|
+
if (category && category !== "function") score += 250;
|
|
1259
|
+
if (kind && !kind.startsWith("call_")) score += 150;
|
|
1260
|
+
if (/^\$\s/.test(String(result.summary || result.output || message?.content || ""))) score += 250;
|
|
1261
|
+
score += Math.min(200, String(result.output || message?.content || "").length / 200);
|
|
1262
|
+
return score;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
763
1265
|
function hasStructuredTranscriptMetadata(message) {
|
|
764
1266
|
const metadata = message?.metadata || {};
|
|
765
1267
|
return Boolean(
|
|
@@ -864,21 +1366,37 @@ function applyCodexTokenCount(event, provider, messages, context) {
|
|
|
864
1366
|
if (String(payload.type || event.type || "").toLowerCase() !== "token_count") return false;
|
|
865
1367
|
const usage = codexTokenUsage(payload);
|
|
866
1368
|
if (!usage) return true;
|
|
867
|
-
const previous = context.tokenUsage || { input: 0, output: 0 };
|
|
868
|
-
const
|
|
869
|
-
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;
|
|
870
1378
|
context.tokenUsage = usage;
|
|
871
1379
|
const target = [...messages].reverse().find((message) => message.role === "assistant");
|
|
872
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
|
+
});
|
|
873
1396
|
target.metadata = {
|
|
874
1397
|
...(target.metadata || {}),
|
|
875
1398
|
provider,
|
|
876
|
-
usage:
|
|
877
|
-
inputTokens: inputDelta,
|
|
878
|
-
outputTokens: outputDelta,
|
|
879
|
-
totalInputTokens: usage.input,
|
|
880
|
-
totalOutputTokens: usage.output
|
|
881
|
-
}
|
|
1399
|
+
usage: nextUsage
|
|
882
1400
|
};
|
|
883
1401
|
return true;
|
|
884
1402
|
}
|
|
@@ -894,11 +1412,50 @@ function codexTokenUsage(payload) {
|
|
|
894
1412
|
for (const item of candidates) {
|
|
895
1413
|
const input = Number(item?.input_tokens ?? item?.inputTokens ?? item?.prompt_tokens ?? item?.promptTokens);
|
|
896
1414
|
const output = Number(item?.output_tokens ?? item?.outputTokens ?? item?.completion_tokens ?? item?.completionTokens);
|
|
897
|
-
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
|
+
}
|
|
898
1444
|
}
|
|
899
1445
|
return null;
|
|
900
1446
|
}
|
|
901
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
|
+
|
|
902
1459
|
function extractCodexSpecialMessage(event, provider, context = {}) {
|
|
903
1460
|
if (provider !== "codex") return null;
|
|
904
1461
|
const item = event?.payload || event?.item || event?.data || event;
|
|
@@ -1398,7 +1955,17 @@ function extractText(value, depth = 0) {
|
|
|
1398
1955
|
|
|
1399
1956
|
function importWebChat(providerInput, file, options = {}, env = process.env) {
|
|
1400
1957
|
const provider = canonicalWebProvider(providerInput);
|
|
1401
|
-
|
|
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
|
+
});
|
|
1402
1969
|
const sourceAccountId = options.accountId || inferWebSourceAccountId(provider, source);
|
|
1403
1970
|
let accountId = options.accountId || sourceAccountId;
|
|
1404
1971
|
const existing = accountId ? getWebAccount(provider, accountId, env) : null;
|
|
@@ -1416,6 +1983,11 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
|
|
|
1416
1983
|
displayName: options.displayName || existing?.displayName || inferredUsername,
|
|
1417
1984
|
sourceAccountId
|
|
1418
1985
|
}, env);
|
|
1986
|
+
reportWebImportProgress(options, provider, {
|
|
1987
|
+
current: 0,
|
|
1988
|
+
total: source.entries.length || 0,
|
|
1989
|
+
message: "normalizing conversations"
|
|
1990
|
+
});
|
|
1419
1991
|
const normalized = normalizeWebConversations(provider, source, account);
|
|
1420
1992
|
const conversations = normalized.conversations;
|
|
1421
1993
|
const summary = {
|
|
@@ -1431,19 +2003,39 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
|
|
|
1431
2003
|
};
|
|
1432
2004
|
const state = loadImportState(env);
|
|
1433
2005
|
const archived = archivedSessionKeys(env);
|
|
1434
|
-
|
|
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);
|
|
1435
2012
|
|
|
1436
|
-
|
|
2013
|
+
conversations.forEach((conversation, index) => {
|
|
2014
|
+
const current = index + 1;
|
|
1437
2015
|
if (!conversation.messages.length) {
|
|
1438
2016
|
summary.skipped++;
|
|
1439
|
-
|
|
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;
|
|
1440
2025
|
}
|
|
1441
2026
|
const sourceType = conversation.sourceType || webConversationSourceType(provider, conversation);
|
|
1442
2027
|
const sessionId = webConversationSessionId(provider, account.accountId, conversation.id);
|
|
1443
2028
|
const fingerprint = webConversationFingerprint(sourceType, account.accountId, conversation);
|
|
1444
2029
|
if (alreadyImported(state, sessionId, fingerprint, archived, provider)) {
|
|
1445
2030
|
summary.skipped++;
|
|
1446
|
-
|
|
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;
|
|
1447
2039
|
}
|
|
1448
2040
|
const scopeCanonical = webConversationScope(provider, account.accountId, conversation.projectPath);
|
|
1449
2041
|
const displayPath = webConversationDisplayPath(account.displayName, conversation.projectPath);
|
|
@@ -1489,12 +2081,36 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
|
|
|
1489
2081
|
archived.add(archiveSessionKey(provider, sessionId));
|
|
1490
2082
|
}
|
|
1491
2083
|
summary.imported++;
|
|
1492
|
-
|
|
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
|
+
});
|
|
1493
2092
|
|
|
1494
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
|
+
});
|
|
1495
2102
|
return summary;
|
|
1496
2103
|
}
|
|
1497
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
|
+
|
|
1498
2114
|
function importWindsurfTrajectoryExport(target, options = {}, env = process.env) {
|
|
1499
2115
|
const since = parseSince(options.since || "all");
|
|
1500
2116
|
return importStructuredProvider(
|
|
@@ -1514,7 +2130,8 @@ function readExportJson(file) {
|
|
|
1514
2130
|
return bundle.entries.map((entry) => entry.data);
|
|
1515
2131
|
}
|
|
1516
2132
|
|
|
1517
|
-
function readExportBundle(file) {
|
|
2133
|
+
function readExportBundle(file, provider = "") {
|
|
2134
|
+
if (Array.isArray(file)) return readExportBundleList(file, provider);
|
|
1518
2135
|
const resolved = path.resolve(file);
|
|
1519
2136
|
let stat;
|
|
1520
2137
|
try {
|
|
@@ -1522,31 +2139,96 @@ function readExportBundle(file) {
|
|
|
1522
2139
|
} catch (error) {
|
|
1523
2140
|
throw exportAccessError(resolved, error);
|
|
1524
2141
|
}
|
|
1525
|
-
const
|
|
2142
|
+
const bundle = stat.isDirectory()
|
|
2143
|
+
? readExportFolder(resolved, provider)
|
|
2144
|
+
: { entries: readExportFile(resolved, provider), rawFiles: [resolved] };
|
|
2145
|
+
const entries = bundle.entries;
|
|
1526
2146
|
if (!entries.length) {
|
|
1527
|
-
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.`);
|
|
1528
2148
|
}
|
|
1529
2149
|
return {
|
|
1530
2150
|
root: resolved,
|
|
1531
2151
|
kind: stat.isDirectory() ? "folder" : path.extname(resolved).toLowerCase() === ".zip" ? "zip" : "json",
|
|
1532
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)),
|
|
1533
2174
|
fingerprint: hashId(entries.map((entry) => `${entry.name}:${entry.sha256}`).sort().join("\n"))
|
|
1534
2175
|
};
|
|
1535
2176
|
}
|
|
1536
2177
|
|
|
1537
|
-
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 = "") {
|
|
1538
2201
|
const entries = [];
|
|
2202
|
+
const rawFiles = [];
|
|
1539
2203
|
collectExportFiles(root, (file) => {
|
|
1540
|
-
|
|
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;
|
|
1541
2216
|
let text;
|
|
1542
2217
|
try {
|
|
1543
2218
|
text = readExportText(file);
|
|
1544
2219
|
} catch (error) {
|
|
1545
2220
|
throw exportAccessError(root, error, file);
|
|
1546
2221
|
}
|
|
1547
|
-
|
|
2222
|
+
try {
|
|
2223
|
+
entries.push(exportEntry(relativeName, text, file));
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
if (namedJson) throw error;
|
|
2226
|
+
}
|
|
1548
2227
|
});
|
|
1549
|
-
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
|
+
};
|
|
1550
2232
|
}
|
|
1551
2233
|
|
|
1552
2234
|
function collectExportFiles(root, visit) {
|
|
@@ -1568,26 +2250,88 @@ function collectExportFiles(root, visit) {
|
|
|
1568
2250
|
|
|
1569
2251
|
function exportAccessError(root, error, target = root) {
|
|
1570
2252
|
const code = error?.code ? ` (${error.code})` : "";
|
|
1571
|
-
const message = error?.message || String(error || "unknown error");
|
|
2253
|
+
const message = compactExportPath(error?.message || String(error || "unknown error"));
|
|
1572
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.";
|
|
1573
|
-
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}`);
|
|
1574
2256
|
wrapped.code = "AGENTLOG_WEB_EXPORT_ACCESS";
|
|
1575
2257
|
wrapped.causeCode = error?.code || "";
|
|
1576
2258
|
return wrapped;
|
|
1577
2259
|
}
|
|
1578
2260
|
|
|
1579
|
-
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 = "") {
|
|
1580
2272
|
if (path.extname(file).toLowerCase() !== ".zip") {
|
|
1581
2273
|
return [exportEntry(path.basename(file), readExportText(file), file)];
|
|
1582
2274
|
}
|
|
2275
|
+
return readZipExportEntries(file, provider);
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function readZipExportEntries(file, provider = "", context = {}) {
|
|
1583
2279
|
const list = spawnSync("unzip", ["-Z1", file], { encoding: "utf8" });
|
|
1584
2280
|
if (list.status !== 0) throw new Error("reading zip exports requires the `unzip` command");
|
|
1585
|
-
const names = list.stdout.split(/\r?\n/).filter(
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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);
|
|
1591
2335
|
}
|
|
1592
2336
|
|
|
1593
2337
|
function exportEntry(name, text, sourcePath) {
|
|
@@ -1603,6 +2347,16 @@ function exportEntry(name, text, sourcePath) {
|
|
|
1603
2347
|
};
|
|
1604
2348
|
}
|
|
1605
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
|
+
|
|
1606
2360
|
function readExportText(file) {
|
|
1607
2361
|
const buffer = fs.readFileSync(file);
|
|
1608
2362
|
const bytes = file.toLowerCase().endsWith(".gz") ? zlib.gunzipSync(buffer) : buffer;
|
|
@@ -1633,11 +2387,6 @@ function parseJsonLines(text, name = "", options = {}) {
|
|
|
1633
2387
|
return rows;
|
|
1634
2388
|
}
|
|
1635
2389
|
|
|
1636
|
-
function isExportDataFile(file) {
|
|
1637
|
-
if (isExportEntryName(file)) return true;
|
|
1638
|
-
return looksLikeJsonPayload(file);
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
2390
|
function isExportEntryName(name) {
|
|
1642
2391
|
const lower = String(name || "").toLowerCase();
|
|
1643
2392
|
return /\.(json|jsonl|ndjson)(\.gz)?$/.test(lower);
|
|
@@ -1665,6 +2414,12 @@ function looksLikeJsonPayload(file) {
|
|
|
1665
2414
|
}
|
|
1666
2415
|
}
|
|
1667
2416
|
|
|
2417
|
+
function ignoredExportRawFile(name) {
|
|
2418
|
+
return String(name || "")
|
|
2419
|
+
.split(/[\\/]+/)
|
|
2420
|
+
.some((part) => part === ".DS_Store" || part === "__MACOSX" || part.startsWith("._"));
|
|
2421
|
+
}
|
|
2422
|
+
|
|
1668
2423
|
function normalizeWebConversations(provider, source, account) {
|
|
1669
2424
|
if (provider === "chatgpt") return { conversations: normalizeChatGptExport(source, account) };
|
|
1670
2425
|
return { conversations: normalizeClaudeWebExport(source, account) };
|
|
@@ -1675,7 +2430,7 @@ function normalizeChatGptExport(source) {
|
|
|
1675
2430
|
return entries.flatMap((entry) => chatgptRawConversations(entry.data).map((conversation, index) => {
|
|
1676
2431
|
const id = firstString(conversation.id, conversation.uuid, conversation.conversation_id) || `chatgpt-${hashId(`${entry.name}:${index}`)}`;
|
|
1677
2432
|
const title = firstString(conversation.title, conversation.name, conversation.summary) || "ChatGPT conversation";
|
|
1678
|
-
const messages = chatgptMessages(conversation).filter(
|
|
2433
|
+
const messages = chatgptMessages(conversation).filter(chatgptMessageHasDisplayContent);
|
|
1679
2434
|
const sorted = sortConversationMessages(messages);
|
|
1680
2435
|
return {
|
|
1681
2436
|
id,
|
|
@@ -1687,34 +2442,41 @@ function normalizeChatGptExport(source) {
|
|
|
1687
2442
|
projectPath: "",
|
|
1688
2443
|
entryPath: entry.name,
|
|
1689
2444
|
sourceType: "chatgpt-export",
|
|
1690
|
-
kind: "conversation"
|
|
2445
|
+
kind: "conversation",
|
|
2446
|
+
sessionSummary: chatgptSessionSummary(conversation)
|
|
1691
2447
|
};
|
|
1692
2448
|
}));
|
|
1693
2449
|
}
|
|
1694
2450
|
|
|
1695
2451
|
function chatConversationEntries(source) {
|
|
1696
|
-
const preferred = source.entries.filter((entry) => /(^|\/)conversations
|
|
1697
|
-
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);
|
|
1698
2454
|
}
|
|
1699
2455
|
|
|
1700
2456
|
function chatgptRawConversations(data) {
|
|
1701
|
-
if (Array.isArray(data)) return data;
|
|
1702
|
-
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);
|
|
1703
2459
|
if (data?.mapping || Array.isArray(data?.messages)) return [data];
|
|
1704
2460
|
return [];
|
|
1705
2461
|
}
|
|
1706
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
|
+
|
|
1707
2467
|
function chatgptMessages(conversation) {
|
|
1708
2468
|
if (conversation.mapping && typeof conversation.mapping === "object") {
|
|
1709
2469
|
const nodes = chatgptMainPathNodes(conversation);
|
|
1710
2470
|
return nodes.map((node) => node && node.message).filter(Boolean).map((message) => {
|
|
1711
2471
|
const role = normalizeEventRole(message.author?.role) || "unknown";
|
|
1712
|
-
const content = extractChatGptContent(message
|
|
2472
|
+
const content = extractChatGptContent(message);
|
|
2473
|
+
const toolCall = chatgptToolCallFromMessage(message, content);
|
|
2474
|
+
const toolResult = role === "tool" ? chatgptToolResultFromMessage(message, content) : null;
|
|
1713
2475
|
return {
|
|
1714
2476
|
role,
|
|
1715
|
-
content,
|
|
2477
|
+
content: toolCall ? "" : content,
|
|
1716
2478
|
timestamp: toIso(message.create_time || message.update_time),
|
|
1717
|
-
metadata: chatgptMessageMetadata(message, role, content)
|
|
2479
|
+
metadata: chatgptMessageMetadata(message, role, content, { toolCall, toolResult })
|
|
1718
2480
|
};
|
|
1719
2481
|
});
|
|
1720
2482
|
}
|
|
@@ -1738,22 +2500,266 @@ function chatgptMainPathNodes(conversation) {
|
|
|
1738
2500
|
);
|
|
1739
2501
|
}
|
|
1740
2502
|
|
|
1741
|
-
function extractChatGptContent(
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
if (Array.isArray(content.
|
|
1745
|
-
|
|
1746
|
-
|
|
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 "";
|
|
1747
2532
|
}
|
|
1748
2533
|
|
|
1749
|
-
function chatgptMessageMetadata(message, role, content) {
|
|
2534
|
+
function chatgptMessageMetadata(message, role, content, options = {}) {
|
|
2535
|
+
const assetPointers = chatgptAssetPointers(message);
|
|
2536
|
+
const attachments = chatgptNormalizedAttachments(message);
|
|
1750
2537
|
return webWithUsage({
|
|
1751
2538
|
source: "chatgpt-export",
|
|
1752
2539
|
messageId: message.id || undefined,
|
|
1753
|
-
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
|
|
1754
2549
|
}, webMessageUsage(message, role, { inputText: content, outputText: content }));
|
|
1755
2550
|
}
|
|
1756
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
|
+
|
|
1757
2763
|
function webWithUsage(metadata, usage) {
|
|
1758
2764
|
if (!usage) return metadata;
|
|
1759
2765
|
return { ...metadata, usage };
|
|
@@ -2345,12 +3351,19 @@ function webConversationSourcePath(source, conversation) {
|
|
|
2345
3351
|
}
|
|
2346
3352
|
|
|
2347
3353
|
function inferWebSourceAccountId(provider, source) {
|
|
3354
|
+
const accountMetadataName = provider === "chatgpt"
|
|
3355
|
+
? /(user|account|profile)/
|
|
3356
|
+
: /(user|account|profile|organization|export|memories)/;
|
|
2348
3357
|
for (const entry of source.entries) {
|
|
2349
3358
|
const name = entry.name.toLowerCase();
|
|
2350
|
-
if (
|
|
3359
|
+
if (!accountMetadataName.test(name) || /(^|\/)conversations(?:-\d+)?\.json$/i.test(name)) continue;
|
|
2351
3360
|
const id = webAccountIdFromData(entry.data);
|
|
2352
3361
|
if (id) return id;
|
|
2353
3362
|
}
|
|
3363
|
+
if (provider === "chatgpt") {
|
|
3364
|
+
const id = openAiConversationExportAccountId(source);
|
|
3365
|
+
if (id) return id;
|
|
3366
|
+
}
|
|
2354
3367
|
if (provider === "claude_web") {
|
|
2355
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);
|
|
2356
3369
|
if (match) return match[0].toLowerCase();
|
|
@@ -2358,6 +3371,19 @@ function inferWebSourceAccountId(provider, source) {
|
|
|
2358
3371
|
return "";
|
|
2359
3372
|
}
|
|
2360
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
|
+
|
|
2361
3387
|
function inferWebUsername(provider, source) {
|
|
2362
3388
|
for (const entry of source.entries) {
|
|
2363
3389
|
const name = entry.name.toLowerCase();
|
|
@@ -2402,10 +3428,15 @@ function providerLabelForWeb(provider) {
|
|
|
2402
3428
|
return provider === "chatgpt" ? "ChatGPT" : "Claude.ai";
|
|
2403
3429
|
}
|
|
2404
3430
|
|
|
2405
|
-
function ensureSharedWebExportRaw(provider, source, account, env = process.env) {
|
|
3431
|
+
function ensureSharedWebExportRaw(provider, source, account, env = process.env, options = {}) {
|
|
2406
3432
|
const root = path.join(archiveRoot(env), "raw", "web-exports", provider, account.accountId, source.fingerprint);
|
|
2407
3433
|
const manifestPath = path.join(root, "manifest.json");
|
|
2408
3434
|
if (fs.existsSync(manifestPath)) {
|
|
3435
|
+
reportWebImportProgress(options, provider, {
|
|
3436
|
+
current: 1,
|
|
3437
|
+
total: 1,
|
|
3438
|
+
message: "raw export already preserved"
|
|
3439
|
+
});
|
|
2409
3440
|
return { root, manifestPath, sha256: source.fingerprint };
|
|
2410
3441
|
}
|
|
2411
3442
|
ensureDir(root);
|
|
@@ -2413,6 +3444,11 @@ function ensureSharedWebExportRaw(provider, source, account, env = process.env)
|
|
|
2413
3444
|
const records = [];
|
|
2414
3445
|
let containerPath = "";
|
|
2415
3446
|
if (source.kind === "zip" || source.kind === "json") {
|
|
3447
|
+
reportWebImportProgress(options, provider, {
|
|
3448
|
+
current: 0,
|
|
3449
|
+
total: 1,
|
|
3450
|
+
message: "preserving raw export"
|
|
3451
|
+
});
|
|
2416
3452
|
const extension = source.kind === "zip" ? ".zip" : path.extname(source.root) || ".json";
|
|
2417
3453
|
containerPath = path.join(root, `source${extension}`);
|
|
2418
3454
|
fs.copyFileSync(source.root, containerPath);
|
|
@@ -2423,22 +3459,55 @@ function ensureSharedWebExportRaw(provider, source, account, env = process.env)
|
|
|
2423
3459
|
size: stat?.size || 0,
|
|
2424
3460
|
sha256: fileSha256(containerPath)
|
|
2425
3461
|
});
|
|
3462
|
+
reportWebImportProgress(options, provider, {
|
|
3463
|
+
current: 1,
|
|
3464
|
+
total: 1,
|
|
3465
|
+
message: "preserved raw export"
|
|
3466
|
+
});
|
|
2426
3467
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
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));
|
|
2431
3480
|
ensureDir(path.dirname(archivedPath));
|
|
2432
|
-
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
|
+
});
|
|
2433
3510
|
}
|
|
2434
|
-
records.push({
|
|
2435
|
-
entryPath: entry.name,
|
|
2436
|
-
originalPath: entry.sourcePath,
|
|
2437
|
-
archivedPath,
|
|
2438
|
-
containerPath: containerPath || undefined,
|
|
2439
|
-
size: entry.size,
|
|
2440
|
-
sha256: entry.sha256
|
|
2441
|
-
});
|
|
2442
3511
|
}
|
|
2443
3512
|
writeJson(manifestPath, {
|
|
2444
3513
|
version: 1,
|
|
@@ -2454,6 +3523,20 @@ function ensureSharedWebExportRaw(provider, source, account, env = process.env)
|
|
|
2454
3523
|
return { root, manifestPath, sha256: source.fingerprint };
|
|
2455
3524
|
}
|
|
2456
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
|
+
|
|
2457
3540
|
function webRawReference(sharedRaw, conversation) {
|
|
2458
3541
|
return {
|
|
2459
3542
|
filename: "web-export-reference",
|
|
@@ -2487,6 +3570,7 @@ function discoverCliHistory(env = process.env, options = {}) {
|
|
|
2487
3570
|
const codexThreads = readCodexSessionEntries(env);
|
|
2488
3571
|
publish("codexCli", "Codex CLI", summarizeCodexThreads(codexThreads, "cli"));
|
|
2489
3572
|
publish("codexDesktop", "Codex Desktop", summarizeCodexThreads(codexThreads, "vscode"));
|
|
3573
|
+
publish("codexSdk", "Codex SDK jobs", summarizeCodexThreads(codexThreads, "exec"));
|
|
2490
3574
|
result.codex = summarizeCodexThreads(codexThreads);
|
|
2491
3575
|
|
|
2492
3576
|
const claudeScan = scanClaudeProjectFiles({
|
|
@@ -2613,8 +3697,8 @@ function summarizeCodex(env = process.env, source) {
|
|
|
2613
3697
|
|
|
2614
3698
|
function summarizeCodexThreads(allThreads, source) {
|
|
2615
3699
|
const threads = allThreads.filter((thread) => {
|
|
2616
|
-
if (
|
|
2617
|
-
return
|
|
3700
|
+
if (source) return thread.source === source;
|
|
3701
|
+
return ["cli", "vscode"].includes(thread.source);
|
|
2618
3702
|
});
|
|
2619
3703
|
if (threads.length) {
|
|
2620
3704
|
const oldest = threads.length
|
|
@@ -2629,10 +3713,24 @@ function summarizeCodexThreads(allThreads, source) {
|
|
|
2629
3713
|
? "terminal `codex` sessions"
|
|
2630
3714
|
: source === "vscode"
|
|
2631
3715
|
? "Codex desktop app sessions"
|
|
3716
|
+
: source === "exec"
|
|
3717
|
+
? "Codex exec/SDK batch jobs; opt-in"
|
|
2632
3718
|
: "includes Codex CLI and Codex Desktop top-level sessions"
|
|
2633
3719
|
};
|
|
2634
3720
|
}
|
|
2635
|
-
return {
|
|
3721
|
+
return {
|
|
3722
|
+
sessions: 0,
|
|
3723
|
+
oldest: "",
|
|
3724
|
+
details: {},
|
|
3725
|
+
note:
|
|
3726
|
+
source === "cli"
|
|
3727
|
+
? "terminal `codex` sessions"
|
|
3728
|
+
: source === "vscode"
|
|
3729
|
+
? "Codex desktop app sessions"
|
|
3730
|
+
: source === "exec"
|
|
3731
|
+
? "Codex exec/SDK batch jobs; opt-in"
|
|
3732
|
+
: ""
|
|
3733
|
+
};
|
|
2636
3734
|
}
|
|
2637
3735
|
|
|
2638
3736
|
function summarizeClaude() {
|
|
@@ -2724,90 +3822,534 @@ function summarizeCursor(env = process.env, options = {}) {
|
|
|
2724
3822
|
errors++;
|
|
2725
3823
|
}
|
|
2726
3824
|
}
|
|
2727
|
-
const transcriptSessions = readCursorProjectTranscriptSessions({ env });
|
|
2728
|
-
const rawCompanionMerge = mergeCursorRawCompanionSessions(sessions.concat(transcriptSessions), { withStats: true });
|
|
2729
|
-
sessions = dedupeCursorSessions(rawCompanionMerge.sessions);
|
|
2730
|
-
const oldest = sessions.length ? sessions.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
|
|
2731
|
-
return {
|
|
2732
|
-
sessions: sessions.length,
|
|
2733
|
-
oldest,
|
|
2734
|
-
details: {
|
|
2735
|
-
workspaceDbs: dbs.length,
|
|
2736
|
-
globalDbs: globalDbs.length,
|
|
2737
|
-
projectTranscripts: transcriptSessions.length,
|
|
2738
|
-
rawCompanionFragmentsMerged: rawCompanionMerge.merged,
|
|
2739
|
-
rawCompanionFragmentsDropped: rawCompanionMerge.dropped,
|
|
2740
|
-
unreadable: errors
|
|
2741
|
-
},
|
|
2742
|
-
note: "Cursor workspace/global SQLite, raw SQLite salvage, plus ~/.cursor/projects agent transcripts"
|
|
2743
|
-
};
|
|
3825
|
+
const transcriptSessions = readCursorProjectTranscriptSessions({ env });
|
|
3826
|
+
const rawCompanionMerge = mergeCursorRawCompanionSessions(sessions.concat(transcriptSessions), { withStats: true });
|
|
3827
|
+
sessions = dedupeCursorSessions(rawCompanionMerge.sessions);
|
|
3828
|
+
const oldest = sessions.length ? sessions.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
|
|
3829
|
+
return {
|
|
3830
|
+
sessions: sessions.length,
|
|
3831
|
+
oldest,
|
|
3832
|
+
details: {
|
|
3833
|
+
workspaceDbs: dbs.length,
|
|
3834
|
+
globalDbs: globalDbs.length,
|
|
3835
|
+
projectTranscripts: transcriptSessions.length,
|
|
3836
|
+
rawCompanionFragmentsMerged: rawCompanionMerge.merged,
|
|
3837
|
+
rawCompanionFragmentsDropped: rawCompanionMerge.dropped,
|
|
3838
|
+
unreadable: errors
|
|
3839
|
+
},
|
|
3840
|
+
note: "Cursor workspace/global SQLite, raw SQLite salvage, plus ~/.cursor/projects agent transcripts"
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
function summarizeDevin(env = process.env, options = {}) {
|
|
3845
|
+
const sessions = readDevinSessions(env, options);
|
|
3846
|
+
const oldest = sessions.length ? sessions.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
|
|
3847
|
+
const db = devinSessionsDb(env);
|
|
3848
|
+
return {
|
|
3849
|
+
sessions: sessions.length,
|
|
3850
|
+
oldest,
|
|
3851
|
+
details: { database: fs.existsSync(db) ? 1 : 0, messages: sessions.reduce((sum, session) => sum + session.messages.length, 0) },
|
|
3852
|
+
note: "Devin for Terminal SQLite sessions.db"
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
function summarizeStructuredSessions(sessions, note = "") {
|
|
3857
|
+
const importable = sessions.filter((session) => session.messages.length);
|
|
3858
|
+
const oldest = importable.length ? importable.map((session) => session.startedAt).sort()[0]?.slice(0, 10) || "" : "";
|
|
3859
|
+
return {
|
|
3860
|
+
sessions: importable.length,
|
|
3861
|
+
oldest,
|
|
3862
|
+
details: summarizeStructuredSessionDetails(sessions),
|
|
3863
|
+
note
|
|
3864
|
+
};
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
function summarizeStructuredSessionDetails(sessions) {
|
|
3868
|
+
return sessions.reduce((acc, session) => {
|
|
3869
|
+
if (session.detailKey) acc[session.detailKey] = (acc[session.detailKey] || 0) + 1;
|
|
3870
|
+
if (session.artifactCount) acc.artifacts = (acc.artifacts || 0) + session.artifactCount;
|
|
3871
|
+
if (session.binaryCount) acc.binaryOnly = (acc.binaryOnly || 0) + session.binaryCount;
|
|
3872
|
+
if (session.partialSummary) acc.partialSummaries = (acc.partialSummaries || 0) + 1;
|
|
3873
|
+
if (session.stateDbCount) acc.stateDbs = (acc.stateDbs || 0) + session.stateDbCount;
|
|
3874
|
+
return acc;
|
|
3875
|
+
}, {});
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
function codexRoots(env = process.env) {
|
|
3879
|
+
const home = codexHome(env);
|
|
3880
|
+
return [path.join(home, "sessions"), path.join(home, "archived_sessions")];
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
function claudeRoots(env = process.env) {
|
|
3884
|
+
const home = env && env.HOME ? env.HOME : os.homedir();
|
|
3885
|
+
return [path.join(home, ".claude", "projects")];
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
function claudeFileHistoryRoot(env = process.env) {
|
|
3889
|
+
const home = env && env.HOME ? env.HOME : os.homedir();
|
|
3890
|
+
return path.join(home, ".claude", "file-history");
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
function claudeFileHistoryFiles(sessionId, env = process.env) {
|
|
3894
|
+
if (!sessionId) return [];
|
|
3895
|
+
const dir = path.join(claudeFileHistoryRoot(env), String(sessionId));
|
|
3896
|
+
let entries;
|
|
3897
|
+
try {
|
|
3898
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3899
|
+
} catch (error) {
|
|
3900
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES") return [];
|
|
3901
|
+
throw error;
|
|
3902
|
+
}
|
|
3903
|
+
const files = [];
|
|
3904
|
+
for (const entry of entries) {
|
|
3905
|
+
if (entry.isFile()) files.push(path.join(dir, entry.name));
|
|
3906
|
+
}
|
|
3907
|
+
files.sort((a, b) => a.localeCompare(b));
|
|
3908
|
+
return files;
|
|
3909
|
+
}
|
|
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));
|
|
2744
4165
|
}
|
|
2745
4166
|
|
|
2746
|
-
function
|
|
2747
|
-
|
|
2748
|
-
const
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
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
|
+
});
|
|
2756
4183
|
}
|
|
2757
4184
|
|
|
2758
|
-
function
|
|
2759
|
-
const
|
|
2760
|
-
const
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
oldest,
|
|
2764
|
-
details: summarizeStructuredSessionDetails(sessions),
|
|
2765
|
-
note
|
|
2766
|
-
};
|
|
4185
|
+
function claudeSubagentRunSourceFilesForTranscript(file) {
|
|
4186
|
+
const files = [file];
|
|
4187
|
+
const meta = claudeSubagentRunMetaFile(file);
|
|
4188
|
+
if (safeStat(meta)?.isFile()) files.push(meta);
|
|
4189
|
+
return files;
|
|
2767
4190
|
}
|
|
2768
4191
|
|
|
2769
|
-
function
|
|
2770
|
-
return
|
|
2771
|
-
if (session.detailKey) acc[session.detailKey] = (acc[session.detailKey] || 0) + 1;
|
|
2772
|
-
if (session.artifactCount) acc.artifacts = (acc.artifacts || 0) + session.artifactCount;
|
|
2773
|
-
if (session.binaryCount) acc.binaryOnly = (acc.binaryOnly || 0) + session.binaryCount;
|
|
2774
|
-
if (session.partialSummary) acc.partialSummaries = (acc.partialSummaries || 0) + 1;
|
|
2775
|
-
if (session.stateDbCount) acc.stateDbs = (acc.stateDbs || 0) + session.stateDbCount;
|
|
2776
|
-
return acc;
|
|
2777
|
-
}, {});
|
|
4192
|
+
function claudeSubagentRunMetaFile(file) {
|
|
4193
|
+
return String(file || "").replace(/\.jsonl$/i, ".meta.json");
|
|
2778
4194
|
}
|
|
2779
4195
|
|
|
2780
|
-
function
|
|
2781
|
-
const
|
|
2782
|
-
|
|
4196
|
+
function readClaudeSubagentRunMeta(file) {
|
|
4197
|
+
const metaFile = claudeSubagentRunMetaFile(file);
|
|
4198
|
+
try {
|
|
4199
|
+
return readJson(metaFile, null);
|
|
4200
|
+
} catch {
|
|
4201
|
+
return null;
|
|
4202
|
+
}
|
|
2783
4203
|
}
|
|
2784
4204
|
|
|
2785
|
-
function
|
|
2786
|
-
const
|
|
2787
|
-
|
|
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-/, "");
|
|
2788
4210
|
}
|
|
2789
4211
|
|
|
2790
|
-
function
|
|
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) {
|
|
2791
4233
|
const home = env && env.HOME ? env.HOME : os.homedir();
|
|
2792
|
-
return path.join(home, ".claude", "
|
|
4234
|
+
return path.join(home, ".claude", "agents");
|
|
2793
4235
|
}
|
|
2794
4236
|
|
|
2795
|
-
function
|
|
2796
|
-
if (!
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
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;
|
|
2804
4247
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
+
}
|
|
2808
4292
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
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;
|
|
2811
4353
|
}
|
|
2812
4354
|
|
|
2813
4355
|
function jsonlFiles(roots) {
|
|
@@ -2888,11 +4430,13 @@ function classifyClaudeFile(file) {
|
|
|
2888
4430
|
}
|
|
2889
4431
|
let inspected = 0;
|
|
2890
4432
|
let sawSdkMessage = false;
|
|
4433
|
+
let sawRemoteControlTools = false;
|
|
2891
4434
|
for (const line of lines) {
|
|
2892
4435
|
if (!line.trim()) continue;
|
|
2893
4436
|
inspected++;
|
|
2894
4437
|
try {
|
|
2895
4438
|
const event = JSON.parse(line);
|
|
4439
|
+
if (isClaudeRemoteControlToolDelta(event)) sawRemoteControlTools = true;
|
|
2896
4440
|
if ((event.type === "user" || event.type === "assistant") && event.message) {
|
|
2897
4441
|
if (event.entrypoint === "sdk-cli") sawSdkMessage = true;
|
|
2898
4442
|
else return "conversation";
|
|
@@ -2900,9 +4444,14 @@ function classifyClaudeFile(file) {
|
|
|
2900
4444
|
} catch {
|
|
2901
4445
|
return "other";
|
|
2902
4446
|
}
|
|
2903
|
-
if (inspected >= 80) return sawSdkMessage ? "sdk-job" : "other";
|
|
4447
|
+
if (inspected >= 80) return sawSdkMessage ? (sawRemoteControlTools ? "conversation" : "sdk-job") : "other";
|
|
2904
4448
|
}
|
|
2905
|
-
return sawSdkMessage ? "sdk-job" : "other";
|
|
4449
|
+
return sawSdkMessage ? (sawRemoteControlTools ? "conversation" : "sdk-job") : "other";
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
function isClaudeRemoteControlToolDelta(event) {
|
|
4453
|
+
if (!event || event.type !== "attachment" || !Array.isArray(event.attachment?.addedNames)) return false;
|
|
4454
|
+
return event.attachment.addedNames.some((name) => isClaudeRemoteControlToolName(name));
|
|
2906
4455
|
}
|
|
2907
4456
|
|
|
2908
4457
|
function readInitialLines(file, maxLines, maxBytes = 1024 * 1024) {
|
|
@@ -2936,59 +4485,196 @@ function readInitialLines(file, maxLines, maxBytes = 1024 * 1024) {
|
|
|
2936
4485
|
function readCodexThreads(env = process.env) {
|
|
2937
4486
|
const db = codexStateDb(env);
|
|
2938
4487
|
if (!fs.existsSync(db)) return [];
|
|
4488
|
+
const sessionIndex = readCodexSessionIndex(env);
|
|
4489
|
+
const threadColumns = sqliteTableColumns(db, "threads");
|
|
2939
4490
|
const hasStage1Outputs = sqliteTableExists(db, "stage1_outputs");
|
|
2940
|
-
const
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
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(" ");
|
|
2956
4528
|
const result = spawnSync("sqlite3", [db, "-json", query], { argv0: "agentlog-sqlite", encoding: "utf8", maxBuffer: 1024 * 1024 * 50 });
|
|
2957
4529
|
if (result.status !== 0 || !result.stdout.trim()) return [];
|
|
2958
4530
|
try {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
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));
|
|
2973
4561
|
} catch {
|
|
2974
4562
|
return [];
|
|
2975
4563
|
}
|
|
2976
4564
|
}
|
|
2977
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
|
+
|
|
2978
4663
|
function readCodexSessionEntries(env = process.env) {
|
|
4664
|
+
const sessionIndex = readCodexSessionIndex(env);
|
|
2979
4665
|
const indexed = readCodexThreads(env).map((thread) => ({ ...thread, indexed: true }));
|
|
2980
4666
|
const seen = new Set(indexed.map((thread) => normalizeSourcePath(thread.rolloutPath)));
|
|
2981
4667
|
const discovered = [];
|
|
2982
4668
|
for (const file of codexRolloutFiles(env)) {
|
|
2983
4669
|
const key = normalizeSourcePath(file);
|
|
2984
4670
|
if (seen.has(key)) continue;
|
|
2985
|
-
const thread = codexRolloutFileThread(file);
|
|
4671
|
+
const thread = applyCodexSessionIndexTitle(codexRolloutFileThread(file), sessionIndex);
|
|
2986
4672
|
if (thread) {
|
|
2987
4673
|
discovered.push(thread);
|
|
2988
4674
|
seen.add(key);
|
|
2989
4675
|
}
|
|
2990
4676
|
}
|
|
2991
|
-
return indexed.concat(discovered).sort((a, b) => String(b
|
|
4677
|
+
return indexed.concat(discovered).sort((a, b) => String(codexThreadImportUpdatedAt(b)).localeCompare(String(codexThreadImportUpdatedAt(a))));
|
|
2992
4678
|
}
|
|
2993
4679
|
|
|
2994
4680
|
function codexRolloutFiles(env = process.env) {
|
|
@@ -3060,18 +4746,174 @@ function readCodexRolloutInfo(file) {
|
|
|
3060
4746
|
info.cwd ||= firstString(payload.cwd, event.cwd);
|
|
3061
4747
|
}
|
|
3062
4748
|
info.cwd ||= firstString(event.cwd, payload.cwd);
|
|
3063
|
-
|
|
3064
|
-
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;
|
|
3065
4761
|
}
|
|
3066
4762
|
if (lastTimestamp) info.updatedAt = lastTimestamp;
|
|
3067
4763
|
return info;
|
|
3068
4764
|
}
|
|
3069
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
|
+
|
|
3070
4903
|
function firstUserTextFromCodexEvent(event) {
|
|
3071
4904
|
const payload = event.payload || event;
|
|
3072
|
-
|
|
3073
|
-
if (payload.type === "
|
|
3074
|
-
|
|
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() : "";
|
|
3075
4917
|
}
|
|
3076
4918
|
|
|
3077
4919
|
function codexTimestampFromFilename(file) {
|
|
@@ -3125,12 +4967,20 @@ function codexStateDb(env = process.env) {
|
|
|
3125
4967
|
return env.CODEX_STATE_DB || path.join(codexHome(env), "state_5.sqlite");
|
|
3126
4968
|
}
|
|
3127
4969
|
|
|
4970
|
+
function codexSessionIndexPath(env = process.env) {
|
|
4971
|
+
return env.CODEX_SESSION_INDEX || path.join(codexHome(env), "session_index.jsonl");
|
|
4972
|
+
}
|
|
4973
|
+
|
|
3128
4974
|
function codexHome(env = process.env) {
|
|
3129
4975
|
return env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
3130
4976
|
}
|
|
3131
4977
|
|
|
3132
|
-
function codexSourceFiles(thread, env = process.env) {
|
|
3133
|
-
|
|
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));
|
|
3134
4984
|
}
|
|
3135
4985
|
|
|
3136
4986
|
function codexSupplementaryMessages(thread, fallbackTimestamp = "") {
|
|
@@ -3163,19 +5013,59 @@ function codexSupplementFingerprint(thread) {
|
|
|
3163
5013
|
thread.rolloutSummary || "",
|
|
3164
5014
|
thread.summaryGeneratedAt || "",
|
|
3165
5015
|
thread.summarySourceUpdatedAt || "",
|
|
3166
|
-
thread.rolloutSlug || ""
|
|
5016
|
+
thread.rolloutSlug || "",
|
|
5017
|
+
thread.sessionIndexTitle || "",
|
|
5018
|
+
thread.sessionIndexUpdatedAt || ""
|
|
3167
5019
|
].join("\0");
|
|
3168
5020
|
if (!payload.replace(/\0/g, "")) return "summaries:none";
|
|
3169
5021
|
const hash = crypto.createHash("sha256").update(payload).digest("hex").slice(0, 24);
|
|
3170
5022
|
return `summaries:${hash}`;
|
|
3171
5023
|
}
|
|
3172
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
|
+
|
|
3173
5061
|
function summarizeCodexSources(threads) {
|
|
3174
5062
|
const cli = threads.filter((thread) => thread.source === "cli").length;
|
|
3175
5063
|
const desktop = threads.filter((thread) => thread.source === "vscode").length;
|
|
5064
|
+
const exec = threads.filter((thread) => thread.source === "exec").length;
|
|
3176
5065
|
const summaries = threads.filter((thread) => thread.rawMemory || thread.rolloutSummary).length;
|
|
3177
5066
|
const archived = threads.filter((thread) => thread.sourceDetail === "archived_sessions").length;
|
|
3178
|
-
|
|
5067
|
+
const subagents = threads.filter((thread) => thread.isCodexSubagent).length;
|
|
5068
|
+
return { cli, desktop, ...(exec ? { exec } : {}), ...(subagents ? { subagents } : {}), ...(archived ? { archived } : {}), ...(summaries ? { summaries } : {}) };
|
|
3179
5069
|
}
|
|
3180
5070
|
|
|
3181
5071
|
function readClaudeDesktopSessions(options = {}, env = process.env) {
|
|
@@ -6967,10 +8857,10 @@ function readOpenCodeSqliteSessionsFromDb(dbPath, options = {}, env = process.en
|
|
|
6967
8857
|
if (!sqliteTableExists(dbPath, "session") || !sqliteTableExists(dbPath, "message") || !sqliteTableExists(dbPath, "part")) return [];
|
|
6968
8858
|
const sessionRows = readOpenCodeSqliteSessionRows(dbPath, options);
|
|
6969
8859
|
if (!sessionRows.length) return [];
|
|
6970
|
-
const classifications = openCodeSqliteSessionClassifications(sessionRows, dbPath, env, options);
|
|
6971
8860
|
const sessionIds = sessionRows.map((row) => row.id).filter(Boolean);
|
|
6972
8861
|
const messageRows = sortOpenCodeSqliteRows(readOpenCodeSqliteMessageRows(dbPath, sessionIds), ["session_id", "time_created", "id"]);
|
|
6973
8862
|
const partRows = sortOpenCodeSqliteRows(readOpenCodeSqlitePartRows(dbPath, sessionIds), ["session_id", "message_id", "time_created", "id"]);
|
|
8863
|
+
const classifications = openCodeSqliteSessionClassifications(sessionRows, dbPath, env, options, messageRows);
|
|
6974
8864
|
const messagesBySession = groupRowsBy(messageRows, "session_id");
|
|
6975
8865
|
const partsByMessage = groupRowsBy(partRows, "message_id");
|
|
6976
8866
|
const storageRoot = path.join(path.dirname(dbPath), "storage");
|
|
@@ -7061,9 +8951,10 @@ function readOpenCodeSqliteSessionRows(dbPath, options = {}) {
|
|
|
7061
8951
|
return readSqliteJson(dbPath, queryParts.join(" "), "OpenCode SQLite sessions");
|
|
7062
8952
|
}
|
|
7063
8953
|
|
|
7064
|
-
function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process.env, options = {}) {
|
|
8954
|
+
function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process.env, options = {}, messageRows = []) {
|
|
7065
8955
|
const baseSourceType = openCodeSqliteSourceType(dbPath, env, options);
|
|
7066
8956
|
const rowsById = new Map((sessionRows || []).map((row) => [String(row.id || ""), row]).filter(([id]) => id));
|
|
8957
|
+
const messagesBySession = groupRowsBy(messageRows, "session_id");
|
|
7067
8958
|
const desktopHints = openCodeDesktopSessionHints(env);
|
|
7068
8959
|
const sourceTypes = new Map();
|
|
7069
8960
|
const hintFiles = new Map();
|
|
@@ -7088,7 +8979,7 @@ function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process
|
|
|
7088
8979
|
return parentSourceType;
|
|
7089
8980
|
}
|
|
7090
8981
|
}
|
|
7091
|
-
const sourceType = openCodeSqliteRowSourceType(row, dbPath, env, options, baseSourceType);
|
|
8982
|
+
const sourceType = openCodeSqliteRowSourceType(row, dbPath, env, options, baseSourceType, messagesBySession.get(id) || []);
|
|
7092
8983
|
sourceTypes.set(id, sourceType);
|
|
7093
8984
|
return sourceType;
|
|
7094
8985
|
};
|
|
@@ -7096,10 +8987,10 @@ function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process
|
|
|
7096
8987
|
return { sourceTypes, hintFiles };
|
|
7097
8988
|
}
|
|
7098
8989
|
|
|
7099
|
-
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 = []) {
|
|
7100
8991
|
if (baseSourceType === "opencode-desktop-sqlite-history") return baseSourceType;
|
|
7101
8992
|
if (!pathInsideAny(dbPath, openCodeCliDataRoots(env)) && !OPENCODE_SOURCE_KINDS.has(options.openCodeKind)) return baseSourceType;
|
|
7102
|
-
if (openCodeSqliteRowHasCliMetadata(row)) return "opencode-cli-sqlite-history";
|
|
8993
|
+
if (openCodeSqliteRowHasCliMetadata(row) || openCodeSqliteMessagesHaveCliMetadata(messageRows)) return "opencode-cli-sqlite-history";
|
|
7103
8994
|
if (openCodeSqliteRowLooksWeb(row, dbPath, env)) return "opencode-web-sqlite-history";
|
|
7104
8995
|
return baseSourceType;
|
|
7105
8996
|
}
|
|
@@ -7108,6 +8999,13 @@ function openCodeSqliteRowHasCliMetadata(row) {
|
|
|
7108
8999
|
return Boolean(firstString(row?.agent, row?.model));
|
|
7109
9000
|
}
|
|
7110
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
|
+
|
|
7111
9009
|
function openCodeSqliteRowLooksWeb(row, dbPath, env = process.env) {
|
|
7112
9010
|
const version = firstString(row?.version);
|
|
7113
9011
|
if (!version || version === "local") return false;
|
|
@@ -7171,7 +9069,7 @@ function readOpenCodeSqliteMessageRows(dbPath, sessionIds = []) {
|
|
|
7171
9069
|
"session_id",
|
|
7172
9070
|
sqliteSelectMaybe(columns, "message", "time_created"),
|
|
7173
9071
|
sqliteSelectMaybe(columns, "message", "time_updated"),
|
|
7174
|
-
|
|
9072
|
+
openCodeSqliteMessageDataSelect(dbPath, columns)
|
|
7175
9073
|
];
|
|
7176
9074
|
return readOpenCodeSqliteRowsForSessionIds(
|
|
7177
9075
|
dbPath,
|
|
@@ -7182,6 +9080,20 @@ function readOpenCodeSqliteMessageRows(dbPath, sessionIds = []) {
|
|
|
7182
9080
|
);
|
|
7183
9081
|
}
|
|
7184
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
|
+
|
|
7185
9097
|
function readOpenCodeSqlitePartRows(dbPath, sessionIds = []) {
|
|
7186
9098
|
const columns = sqliteTableColumns(dbPath, "part");
|
|
7187
9099
|
if (!columns.has("id") || !columns.has("message_id") || !columns.has("session_id")) return [];
|
|
@@ -9136,6 +11048,22 @@ function fileFingerprint(file, stat = safeStat(file)) {
|
|
|
9136
11048
|
return `${file}:${stat?.size || 0}:${Math.floor(stat?.mtimeMs || 0)}`;
|
|
9137
11049
|
}
|
|
9138
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
|
+
|
|
9139
11067
|
function fileSha256(file) {
|
|
9140
11068
|
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
9141
11069
|
}
|
|
@@ -9259,6 +11187,7 @@ module.exports = {
|
|
|
9259
11187
|
importWebChat,
|
|
9260
11188
|
normalizeEventRole,
|
|
9261
11189
|
parseClaudeDesktopSessionFile,
|
|
11190
|
+
parseClaudeSubagentDefinitionFile,
|
|
9262
11191
|
parseAgentJsonl,
|
|
9263
11192
|
parseJsonlHistoryFile,
|
|
9264
11193
|
parseSince,
|
|
@@ -9273,6 +11202,7 @@ module.exports = {
|
|
|
9273
11202
|
readAiderSessions,
|
|
9274
11203
|
readAntigravitySessions,
|
|
9275
11204
|
readClineSessions,
|
|
11205
|
+
readClaudeSubagentDefinitions,
|
|
9276
11206
|
readDevinSessionsFromDb,
|
|
9277
11207
|
readGeminiCliSessions,
|
|
9278
11208
|
readOpenCodeSessions,
|