agentel 0.2.4 → 0.2.6
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 +38 -25
- package/docs/code-reference.md +22 -13
- package/docs/history-source-handling.md +173 -60
- package/docs/release.md +1 -2
- package/package.json +1 -1
- package/src/archive.js +1 -1
- package/src/canonical-events.js +74 -25
- package/src/cli.js +818 -109
- package/src/config.js +4 -1
- package/src/doctor.js +15 -2
- package/src/importers/claude.js +309 -11
- package/src/importers/providers.js +47 -1
- package/src/importers.js +845 -63
- package/src/parser-versions.js +6 -0
- package/src/search.js +5 -1
- package/src/sources.js +8 -3
- package/src/web-export-instructions.js +77 -0
package/src/importers.js
CHANGED
|
@@ -19,9 +19,12 @@ 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 OPENCODE_SOURCE_KINDS = new Set(["cli", "desktop", "web"]);
|
|
27
|
+
const OPENCODE_SESSION_ID_RE = /\bses_[A-Za-z0-9]+\b/g;
|
|
25
28
|
|
|
26
29
|
function importCliHistory(options = {}, env = process.env) {
|
|
27
30
|
const source = options.source || "all";
|
|
@@ -44,6 +47,7 @@ function importProviderHelpers() {
|
|
|
44
47
|
importCursorProvider,
|
|
45
48
|
importJsonlProvider,
|
|
46
49
|
importStructuredProvider,
|
|
50
|
+
manualImportInstructionResult,
|
|
47
51
|
readAntigravitySessions,
|
|
48
52
|
readAiderSessions,
|
|
49
53
|
readClineSessions,
|
|
@@ -76,6 +80,7 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
76
80
|
const archived = archivedSessionKeys(env);
|
|
77
81
|
const files =
|
|
78
82
|
provider === "claude_code" ? claudeFiles(env) : provider === "claude_sdk" ? claudeSdkFiles(env) : jsonlFiles(roots);
|
|
83
|
+
const claudeCodeMetadata = provider === "claude_code" ? claudeCodeSessionMetadataByCliSessionId(env) : new Map();
|
|
79
84
|
|
|
80
85
|
const candidates = files
|
|
81
86
|
.map((file) => ({ file, stat: safeStat(file) }))
|
|
@@ -95,7 +100,11 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
95
100
|
for (let index = 0; index < candidates.length; index++) {
|
|
96
101
|
const item = candidates[index];
|
|
97
102
|
const sourceType = jsonlProviderSourceType(provider);
|
|
98
|
-
const
|
|
103
|
+
const baseFingerprint = `${fingerprintPrefix(sourceType)}:${fileFingerprint(item.file, item.stat)}`;
|
|
104
|
+
const preliminaryMetadata = provider === "claude_code"
|
|
105
|
+
? claudeCodeMetadata.get(claudeSessionIdFromFilename(item.file)) || null
|
|
106
|
+
: null;
|
|
107
|
+
let fingerprint = claudeCodeImportFingerprint(baseFingerprint, preliminaryMetadata);
|
|
99
108
|
if (alreadyImportedFile(state, fingerprint, archived, provider)) {
|
|
100
109
|
summary.skipped++;
|
|
101
110
|
reportProgress(options, summary, index + 1, item.file);
|
|
@@ -115,15 +124,17 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
115
124
|
reportProgress(options, summary, index + 1, item.file);
|
|
116
125
|
continue;
|
|
117
126
|
}
|
|
118
|
-
const
|
|
127
|
+
const sessionId = parsed.sessionId || stableSessionId(provider, item.file, parsed.startedAt, parsed.messages);
|
|
128
|
+
const sessionMetadata = preliminaryMetadata || claudeCodeMetadata.get(sessionId) || null;
|
|
129
|
+
fingerprint = claudeCodeImportFingerprint(baseFingerprint, sessionMetadata);
|
|
130
|
+
const cwd = parsed.cwd || sessionMetadata?.cwd || "";
|
|
119
131
|
const scopeCanonical = cwd ? "" : uncategorizedScope(provider);
|
|
120
|
-
const repo = cwd
|
|
132
|
+
const repo = repoInfoForImport(provider, cwd, sessionMetadata);
|
|
121
133
|
if (options.repos && options.repos.length && (!repo || !options.repos.includes(repo.key))) {
|
|
122
134
|
summary.skipped++;
|
|
123
135
|
reportProgress(options, summary, index + 1, item.file);
|
|
124
136
|
continue;
|
|
125
137
|
}
|
|
126
|
-
const sessionId = parsed.sessionId || stableSessionId(provider, item.file, parsed.startedAt, parsed.messages);
|
|
127
138
|
if (alreadyImported(state, sessionId, fingerprint, archived, provider)) {
|
|
128
139
|
state.files[fingerprint] = { sessionId, duplicate: true, at: new Date().toISOString() };
|
|
129
140
|
summary.skipped++;
|
|
@@ -145,9 +156,10 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
145
156
|
startedAt: parsed.startedAt,
|
|
146
157
|
endedAt: parsed.endedAt,
|
|
147
158
|
sourcePath: item.file,
|
|
148
|
-
sourceFiles: [item.file, ...auxiliaryFiles],
|
|
159
|
+
sourceFiles: [item.file, sessionMetadata?.sourcePath || "", ...auxiliaryFiles].filter(Boolean),
|
|
149
160
|
sourceType,
|
|
150
|
-
title: parsed
|
|
161
|
+
title: jsonlSessionTitleForImport(parsed, sessionMetadata),
|
|
162
|
+
sessionSummary: mergeSessionSummaries(claudeCodeSidecarSessionSummary(sessionMetadata), parsed.sessionSummary)
|
|
151
163
|
},
|
|
152
164
|
env
|
|
153
165
|
);
|
|
@@ -169,10 +181,16 @@ function jsonlProviderSourceType(provider) {
|
|
|
169
181
|
return "cli-history";
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
function codexThreadSourceType(thread) {
|
|
185
|
+
if (thread.source === "vscode") return "codex-desktop-history";
|
|
186
|
+
if (thread.source === "exec") return "codex-sdk-history";
|
|
187
|
+
return "codex-cli-history";
|
|
188
|
+
}
|
|
189
|
+
|
|
172
190
|
function importClaudeDesktopProvider(provider, since, options = {}, env = process.env) {
|
|
173
191
|
const state = loadImportState(env);
|
|
174
192
|
const archived = archivedSessionKeys(env);
|
|
175
|
-
const sessions = readClaudeDesktopSessions().filter((session) => {
|
|
193
|
+
const sessions = readClaudeDesktopSessions({}, env).filter((session) => {
|
|
176
194
|
return !options.claudeDesktopKind || session.kind === options.claudeDesktopKind;
|
|
177
195
|
});
|
|
178
196
|
const candidates = sessions
|
|
@@ -199,7 +217,7 @@ function importClaudeDesktopProvider(provider, since, options = {}, env = proces
|
|
|
199
217
|
continue;
|
|
200
218
|
}
|
|
201
219
|
const cwd = session.cwd || "";
|
|
202
|
-
const repo =
|
|
220
|
+
const repo = repoInfoForImport(provider, cwd);
|
|
203
221
|
if (options.repos && options.repos.length && !matchesImportedSessionRepo(session, repo, options.repos)) {
|
|
204
222
|
summary.skipped++;
|
|
205
223
|
reportProgress(options, summary, index + 1, session.sourcePath);
|
|
@@ -247,10 +265,12 @@ function matchesImportedSessionRepo(session, repo, wantedRepos) {
|
|
|
247
265
|
|
|
248
266
|
function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
249
267
|
const threads = readCodexSessionEntries(env).filter((thread) => {
|
|
250
|
-
if (options.codexSource
|
|
251
|
-
|
|
252
|
-
return true;
|
|
268
|
+
if (options.codexSource) return thread.source === options.codexSource;
|
|
269
|
+
return ["cli", "vscode"].includes(thread.source);
|
|
253
270
|
});
|
|
271
|
+
if (!threads.length && options.codexSource && options.codexSource !== "cli") {
|
|
272
|
+
return { provider, discovered: 0, candidates: 0, imported: 0, skipped: 0, errors: [], details: {} };
|
|
273
|
+
}
|
|
254
274
|
if (!threads.length) return importJsonlProvider(provider, codexRoots(env), since, options, env);
|
|
255
275
|
|
|
256
276
|
const state = loadImportState(env);
|
|
@@ -278,7 +298,7 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
|
|
|
278
298
|
reportProgress(options, summary, index + 1, thread.rolloutPath);
|
|
279
299
|
continue;
|
|
280
300
|
}
|
|
281
|
-
const sourceType = thread
|
|
301
|
+
const sourceType = codexThreadSourceType(thread);
|
|
282
302
|
const fingerprint = `${fingerprintPrefix(sourceType)}:${fileFingerprint(thread.rolloutPath, stat)}:${codexSupplementFingerprint(thread)}`;
|
|
283
303
|
if (alreadyImported(state, thread.id, fingerprint, archived, provider)) {
|
|
284
304
|
summary.skipped++;
|
|
@@ -564,15 +584,17 @@ function structuredSessionReplaceSourcePathCopies(provider, sourceType) {
|
|
|
564
584
|
}
|
|
565
585
|
|
|
566
586
|
function structuredSessionUsesSharedRawFiles(provider, sourceType) {
|
|
567
|
-
return provider === "opencode" &&
|
|
587
|
+
return provider === "opencode" && ["opencode-cli-sqlite-history", "opencode-web-sqlite-history", "opencode-sqlite-history", "opencode-desktop-sqlite-history"].includes(sourceType);
|
|
568
588
|
}
|
|
569
589
|
|
|
570
590
|
function parseAgentJsonl(file, provider) {
|
|
571
591
|
const text = readTextMaybeZstd(file);
|
|
592
|
+
const events = [];
|
|
572
593
|
const messages = [];
|
|
573
594
|
let cwd = "";
|
|
574
595
|
let sessionId = "";
|
|
575
596
|
let title = "";
|
|
597
|
+
let titleSource = "";
|
|
576
598
|
const context = { model: "", tokenUsage: { input: 0, output: 0 } };
|
|
577
599
|
for (const line of text.split(/\r?\n/)) {
|
|
578
600
|
if (!line.trim()) continue;
|
|
@@ -582,6 +604,7 @@ function parseAgentJsonl(file, provider) {
|
|
|
582
604
|
} catch {
|
|
583
605
|
continue;
|
|
584
606
|
}
|
|
607
|
+
events.push(event);
|
|
585
608
|
cwd ||= firstString(event.cwd, event.working_directory, event.workingDirectory, event.context?.cwd, event.payload?.cwd);
|
|
586
609
|
sessionId ||= firstString(
|
|
587
610
|
event.session_id,
|
|
@@ -593,7 +616,20 @@ function parseAgentJsonl(file, provider) {
|
|
|
593
616
|
event.payload?.session_id,
|
|
594
617
|
event.sessionId
|
|
595
618
|
);
|
|
596
|
-
|
|
619
|
+
if (!title) {
|
|
620
|
+
const sourceTitle = firstString(event.title, event.conversation_title, event.payload?.title);
|
|
621
|
+
const aiTitle = firstString(event.aiTitle, event.ai_title);
|
|
622
|
+
if (sourceTitle) {
|
|
623
|
+
title = sourceTitle;
|
|
624
|
+
titleSource = "source";
|
|
625
|
+
} else if (aiTitle) {
|
|
626
|
+
title = aiTitle;
|
|
627
|
+
titleSource = "ai-title";
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
collectClaudeJsonlSessionMetadata(event, provider, context);
|
|
631
|
+
}
|
|
632
|
+
for (const event of events) {
|
|
597
633
|
updateCodexParseContext(event, provider, context);
|
|
598
634
|
updateClaudeParseContext(event, provider, context);
|
|
599
635
|
if (applyCodexTokenCount(event, provider, messages, context)) continue;
|
|
@@ -602,16 +638,409 @@ function parseAgentJsonl(file, provider) {
|
|
|
602
638
|
}
|
|
603
639
|
messages.sort((a, b) => String(a.timestamp || "").localeCompare(String(b.timestamp || "")));
|
|
604
640
|
const deduped = dedupeAdjacentMessages(messages);
|
|
641
|
+
const inferredTitle = inferredJsonlSessionTitle(provider, deduped);
|
|
605
642
|
return {
|
|
606
643
|
cwd,
|
|
607
644
|
sessionId,
|
|
608
|
-
title,
|
|
645
|
+
title: title || inferredTitle,
|
|
646
|
+
titleSource: titleSource || (inferredTitle ? "first-user-prompt" : ""),
|
|
647
|
+
sessionSummary: claudeJsonlSessionSummary(provider, context),
|
|
609
648
|
messages: deduped,
|
|
610
649
|
startedAt: deduped[0]?.timestamp || "",
|
|
611
650
|
endedAt: deduped[deduped.length - 1]?.timestamp || ""
|
|
612
651
|
};
|
|
613
652
|
}
|
|
614
653
|
|
|
654
|
+
function collectClaudeJsonlSessionMetadata(event, provider, context = {}) {
|
|
655
|
+
if (!isClaudeJsonlProvider(provider) || !event || typeof event !== "object") return;
|
|
656
|
+
const metadata = context.claudeJsonl || (context.claudeJsonl = {
|
|
657
|
+
eventTypeCounts: {},
|
|
658
|
+
entrypoints: new Set(),
|
|
659
|
+
userTypes: new Set(),
|
|
660
|
+
versions: new Set(),
|
|
661
|
+
gitBranches: new Set(),
|
|
662
|
+
permissionModes: new Set(),
|
|
663
|
+
promptIds: new Set(),
|
|
664
|
+
requestIds: new Set(),
|
|
665
|
+
sourceToolAssistantUUIDs: new Set(),
|
|
666
|
+
attachmentTypes: new Set(),
|
|
667
|
+
remoteToolNames: new Set(),
|
|
668
|
+
mcpServerNames: new Set(),
|
|
669
|
+
toolNames: new Set(),
|
|
670
|
+
agentIds: new Set(),
|
|
671
|
+
slugs: new Set(),
|
|
672
|
+
sourceToolUseIDs: new Set(),
|
|
673
|
+
parentToolUseIDs: new Set(),
|
|
674
|
+
toolUseIDs: new Set(),
|
|
675
|
+
attributionSkills: new Set(),
|
|
676
|
+
queue: {},
|
|
677
|
+
attachmentDetails: {},
|
|
678
|
+
apiErrors: {},
|
|
679
|
+
mcpStructuredContentCount: 0,
|
|
680
|
+
aiTitles: new Set(),
|
|
681
|
+
lastPrompt: "",
|
|
682
|
+
sidechainMessageCount: 0,
|
|
683
|
+
messageEventCount: 0,
|
|
684
|
+
remoteControl: false
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
const type = firstString(event.type, event.kind);
|
|
688
|
+
if (type) metadata.eventTypeCounts[type] = (metadata.eventTypeCounts[type] || 0) + 1;
|
|
689
|
+
addSetValue(metadata.entrypoints, event.entrypoint);
|
|
690
|
+
addSetValue(metadata.userTypes, event.userType, event.user_type);
|
|
691
|
+
addSetValue(metadata.versions, event.version);
|
|
692
|
+
addSetValue(metadata.gitBranches, event.gitBranch, event.git_branch);
|
|
693
|
+
addSetValue(metadata.permissionModes, event.permissionMode, event.permission_mode);
|
|
694
|
+
addSetValue(metadata.promptIds, event.promptId, event.prompt_id);
|
|
695
|
+
addSetValue(metadata.requestIds, event.requestId, event.request_id);
|
|
696
|
+
addSetValue(metadata.sourceToolAssistantUUIDs, event.sourceToolAssistantUUID, event.source_tool_assistant_uuid);
|
|
697
|
+
addSetValue(metadata.agentIds, event.agentId, event.agent_id);
|
|
698
|
+
addSetValue(metadata.slugs, event.slug);
|
|
699
|
+
addSetValue(metadata.sourceToolUseIDs, event.sourceToolUseID, event.source_tool_use_id);
|
|
700
|
+
addSetValue(metadata.parentToolUseIDs, event.parentToolUseID, event.parent_tool_use_id);
|
|
701
|
+
addSetValue(metadata.toolUseIDs, event.toolUseID, event.tool_use_id);
|
|
702
|
+
addSetValue(metadata.attributionSkills, event.attributionSkill, event.attribution_skill);
|
|
703
|
+
if (event.mcpMeta?.structuredContent && typeof event.mcpMeta.structuredContent === "object") metadata.mcpStructuredContentCount++;
|
|
704
|
+
collectClaudeApiError(event, metadata);
|
|
705
|
+
if (event.isSidechain === true) metadata.sidechainMessageCount++;
|
|
706
|
+
if ((event.type === "user" || event.type === "assistant") && event.message) metadata.messageEventCount++;
|
|
707
|
+
|
|
708
|
+
const aiTitle = firstString(event.aiTitle, event.ai_title);
|
|
709
|
+
if (aiTitle) metadata.aiTitles.add(aiTitle);
|
|
710
|
+
const lastPrompt = firstString(event.lastPrompt, event.last_prompt);
|
|
711
|
+
if (lastPrompt) metadata.lastPrompt = lastPrompt;
|
|
712
|
+
|
|
713
|
+
if (event.type === "queue-operation") collectClaudeQueueOperation(event, metadata);
|
|
714
|
+
if (event.type === "attachment") collectClaudeAttachmentMetadata(event, metadata);
|
|
715
|
+
collectClaudeToolNames(event, metadata);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function collectClaudeQueueOperation(event, metadata) {
|
|
719
|
+
const op = firstString(event.operation).toLowerCase();
|
|
720
|
+
if (!op) return;
|
|
721
|
+
const key = op.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "unknown";
|
|
722
|
+
metadata.queue[key] = (metadata.queue[key] || 0) + 1;
|
|
723
|
+
const timestamp = toIso(event.timestamp || event.created_at || event.createdAt || event.time);
|
|
724
|
+
if (timestamp) {
|
|
725
|
+
if (!metadata.queue.firstAt || timestamp < metadata.queue.firstAt) metadata.queue.firstAt = timestamp;
|
|
726
|
+
if (!metadata.queue.lastAt || timestamp > metadata.queue.lastAt) metadata.queue.lastAt = timestamp;
|
|
727
|
+
const upper = key.charAt(0).toUpperCase() + key.slice(1);
|
|
728
|
+
if (!metadata.queue[`first${upper}At`] || timestamp < metadata.queue[`first${upper}At`]) metadata.queue[`first${upper}At`] = timestamp;
|
|
729
|
+
if (!metadata.queue[`last${upper}At`] || timestamp > metadata.queue[`last${upper}At`]) metadata.queue[`last${upper}At`] = timestamp;
|
|
730
|
+
}
|
|
731
|
+
if (typeof event.content === "string" && event.content.trim()) {
|
|
732
|
+
metadata.queue.lastContent = event.content.trim();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function collectClaudeApiError(event, metadata) {
|
|
737
|
+
if (!event || typeof event !== "object") return;
|
|
738
|
+
if (event.isApiErrorMessage !== true && !event.error && event.apiErrorStatus == null) return;
|
|
739
|
+
const status = Number(event.apiErrorStatus);
|
|
740
|
+
if (Number.isFinite(status)) {
|
|
741
|
+
const key = String(status);
|
|
742
|
+
metadata.apiErrors.statusCounts = metadata.apiErrors.statusCounts || {};
|
|
743
|
+
metadata.apiErrors.statusCounts[key] = (metadata.apiErrors.statusCounts[key] || 0) + 1;
|
|
744
|
+
}
|
|
745
|
+
const type = firstString(event.error);
|
|
746
|
+
if (type) {
|
|
747
|
+
metadata.apiErrors.typeCounts = metadata.apiErrors.typeCounts || {};
|
|
748
|
+
metadata.apiErrors.typeCounts[type] = (metadata.apiErrors.typeCounts[type] || 0) + 1;
|
|
749
|
+
}
|
|
750
|
+
metadata.apiErrors.count = (metadata.apiErrors.count || 0) + 1;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function collectClaudeAttachmentMetadata(event, metadata) {
|
|
754
|
+
const attachment = event.attachment && typeof event.attachment === "object" ? event.attachment : {};
|
|
755
|
+
addSetValue(metadata.attachmentTypes, attachment.type);
|
|
756
|
+
collectClaudeAttachmentDetails(attachment, metadata);
|
|
757
|
+
for (const name of [...asStringArray(attachment.addedNames), ...asStringArray(attachment.removedNames)]) {
|
|
758
|
+
if (typeof name !== "string" || !name.trim()) continue;
|
|
759
|
+
const value = name.trim();
|
|
760
|
+
metadata.remoteToolNames.add(value);
|
|
761
|
+
if (isClaudeRemoteControlToolName(value)) metadata.remoteControl = true;
|
|
762
|
+
const server = claudeMcpServerNameFromTool(value);
|
|
763
|
+
if (server) metadata.mcpServerNames.add(server);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function collectClaudeAttachmentDetails(attachment, metadata) {
|
|
768
|
+
const type = firstString(attachment.type);
|
|
769
|
+
if (!type) return;
|
|
770
|
+
const details = metadata.attachmentDetails;
|
|
771
|
+
details.countByType = details.countByType || {};
|
|
772
|
+
details.countByType[type] = (details.countByType[type] || 0) + 1;
|
|
773
|
+
if (type === "nested_memory") {
|
|
774
|
+
details.nestedMemoryPaths = details.nestedMemoryPaths || new Set();
|
|
775
|
+
addSetValue(details.nestedMemoryPaths, firstString(attachment.path, attachment.content?.path, attachment.displayPath));
|
|
776
|
+
} else if (type === "edited_text_file") {
|
|
777
|
+
details.editedTextFiles = details.editedTextFiles || new Set();
|
|
778
|
+
addSetValue(details.editedTextFiles, firstString(attachment.path, attachment.content?.path, attachment.displayPath));
|
|
779
|
+
} else if (type === "queued_command") {
|
|
780
|
+
details.queuedCommandCount = (details.queuedCommandCount || 0) + 1;
|
|
781
|
+
details.lastQueuedCommand = compactMetadata({
|
|
782
|
+
prompt: firstString(attachment.prompt),
|
|
783
|
+
commandMode: firstString(attachment.commandMode, attachment.command_mode)
|
|
784
|
+
});
|
|
785
|
+
} else if (type === "command_permissions") {
|
|
786
|
+
details.commandPermissionCount = (details.commandPermissionCount || 0) + 1;
|
|
787
|
+
details.lastAllowedTools = asStringArray(attachment.allowedTools);
|
|
788
|
+
} else if (type === "date_change") {
|
|
789
|
+
details.dateChanges = details.dateChanges || new Set();
|
|
790
|
+
addSetValue(details.dateChanges, attachment.newDate, attachment.new_date);
|
|
791
|
+
} else if (type === "max_turns_reached") {
|
|
792
|
+
details.maxTurnsReachedCount = (details.maxTurnsReachedCount || 0) + 1;
|
|
793
|
+
details.lastMaxTurnsReached = compactMetadata({
|
|
794
|
+
maxTurns: numericValue(attachment.maxTurns, attachment.max_turns),
|
|
795
|
+
turnCount: numericValue(attachment.turnCount, attachment.turn_count)
|
|
796
|
+
});
|
|
797
|
+
} else if (type === "compact_file_reference") {
|
|
798
|
+
details.compactFileReferences = details.compactFileReferences || new Set();
|
|
799
|
+
addSetValue(details.compactFileReferences, attachment.path, attachment.displayPath);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function collectClaudeToolNames(event, metadata) {
|
|
804
|
+
const parts = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
805
|
+
for (const part of parts) {
|
|
806
|
+
if (!part || typeof part !== "object") continue;
|
|
807
|
+
const type = String(part.type || part.kind || "").toLowerCase();
|
|
808
|
+
if (type !== "tool_use" && type !== "server_tool_use" && type !== "mcp_tool_use" && type !== "function_call") continue;
|
|
809
|
+
addSetValue(metadata.toolNames, part.name, part.tool_name, part.toolName, part.function?.name);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function claudeMcpServerNameFromTool(name) {
|
|
814
|
+
const match = String(name || "").match(/^mcp__(.+?)__/);
|
|
815
|
+
if (!match) return "";
|
|
816
|
+
return match[1];
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const CLAUDE_REMOTE_CONTROL_TOOL_NAMES = new Set([
|
|
820
|
+
"RemoteTrigger",
|
|
821
|
+
"TaskOutput",
|
|
822
|
+
"TaskStop",
|
|
823
|
+
"PushNotification",
|
|
824
|
+
"AskUserQuestion",
|
|
825
|
+
"Monitor",
|
|
826
|
+
"TaskCreate",
|
|
827
|
+
"TaskUpdate",
|
|
828
|
+
"TaskList",
|
|
829
|
+
"TaskGet",
|
|
830
|
+
"CronCreate",
|
|
831
|
+
"CronDelete",
|
|
832
|
+
"CronList"
|
|
833
|
+
]);
|
|
834
|
+
|
|
835
|
+
function isClaudeRemoteControlToolName(name) {
|
|
836
|
+
const value = String(name || "").trim();
|
|
837
|
+
return CLAUDE_REMOTE_CONTROL_TOOL_NAMES.has(value);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function claudeJsonlSessionSummary(provider, context = {}) {
|
|
841
|
+
if (!isClaudeJsonlProvider(provider)) return null;
|
|
842
|
+
const metadata = context.claudeJsonl;
|
|
843
|
+
if (!metadata) return null;
|
|
844
|
+
const remoteToolNames = sortedSet(metadata.remoteToolNames);
|
|
845
|
+
const mcpServerNames = sortedSet(metadata.mcpServerNames);
|
|
846
|
+
const attachmentDetails = claudeAttachmentDetailsSummary(metadata.attachmentDetails);
|
|
847
|
+
const result = compactMetadata({
|
|
848
|
+
claudeJsonl: compactMetadata({
|
|
849
|
+
entrypoint: singleOrArray(metadata.entrypoints),
|
|
850
|
+
userType: singleOrArray(metadata.userTypes),
|
|
851
|
+
version: singleOrArray(metadata.versions),
|
|
852
|
+
gitBranch: singleOrArray(metadata.gitBranches),
|
|
853
|
+
permissionModes: sortedSet(metadata.permissionModes),
|
|
854
|
+
eventTypeCounts: compactMetadata(metadata.eventTypeCounts),
|
|
855
|
+
messageEventCount: metadata.messageEventCount || undefined,
|
|
856
|
+
sidechainMessageCount: metadata.sidechainMessageCount || undefined,
|
|
857
|
+
promptIdCount: metadata.promptIds.size || undefined,
|
|
858
|
+
requestIdCount: metadata.requestIds.size || undefined,
|
|
859
|
+
sourceToolAssistantUUIDCount: metadata.sourceToolAssistantUUIDs.size || undefined,
|
|
860
|
+
agentIds: sortedSet(metadata.agentIds),
|
|
861
|
+
slugs: sortedSet(metadata.slugs),
|
|
862
|
+
sourceToolUseIDCount: metadata.sourceToolUseIDs.size || undefined,
|
|
863
|
+
parentToolUseIDCount: metadata.parentToolUseIDs.size || undefined,
|
|
864
|
+
toolUseIDCount: metadata.toolUseIDs.size || undefined,
|
|
865
|
+
attributionSkills: sortedSet(metadata.attributionSkills),
|
|
866
|
+
mcpStructuredContentCount: metadata.mcpStructuredContentCount || undefined,
|
|
867
|
+
apiErrors: compactMetadata(metadata.apiErrors),
|
|
868
|
+
attachmentTypes: sortedSet(metadata.attachmentTypes),
|
|
869
|
+
attachmentDetails,
|
|
870
|
+
toolNames: sortedSet(metadata.toolNames),
|
|
871
|
+
aiTitle: sortedSet(metadata.aiTitles)[0] || undefined,
|
|
872
|
+
lastPrompt: metadata.lastPrompt || undefined,
|
|
873
|
+
queue: compactMetadata(metadata.queue),
|
|
874
|
+
remoteControl: metadata.remoteControl ? compactMetadata({
|
|
875
|
+
detected: true,
|
|
876
|
+
toolCount: remoteToolNames.length || undefined,
|
|
877
|
+
toolNames: remoteToolNames,
|
|
878
|
+
mcpToolCount: remoteToolNames.filter((name) => name.startsWith("mcp__")).length || undefined,
|
|
879
|
+
mcpServerNames
|
|
880
|
+
}) : undefined
|
|
881
|
+
})
|
|
882
|
+
});
|
|
883
|
+
return result || null;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function claudeAttachmentDetailsSummary(details = {}) {
|
|
887
|
+
return compactMetadata({
|
|
888
|
+
countByType: compactMetadata(details.countByType),
|
|
889
|
+
nestedMemoryPaths: details.nestedMemoryPaths ? sortedSet(details.nestedMemoryPaths) : undefined,
|
|
890
|
+
editedTextFiles: details.editedTextFiles ? sortedSet(details.editedTextFiles) : undefined,
|
|
891
|
+
queuedCommandCount: details.queuedCommandCount || undefined,
|
|
892
|
+
lastQueuedCommand: details.lastQueuedCommand || undefined,
|
|
893
|
+
commandPermissionCount: details.commandPermissionCount || undefined,
|
|
894
|
+
lastAllowedTools: details.lastAllowedTools?.length ? details.lastAllowedTools : undefined,
|
|
895
|
+
dateChanges: details.dateChanges ? sortedSet(details.dateChanges) : undefined,
|
|
896
|
+
maxTurnsReachedCount: details.maxTurnsReachedCount || undefined,
|
|
897
|
+
lastMaxTurnsReached: details.lastMaxTurnsReached || undefined,
|
|
898
|
+
compactFileReferences: details.compactFileReferences ? sortedSet(details.compactFileReferences) : undefined
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function addSetValue(set, ...values) {
|
|
903
|
+
for (const value of values) {
|
|
904
|
+
if (typeof value === "string" && value.trim()) set.add(value.trim());
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function asStringArray(value) {
|
|
909
|
+
if (!Array.isArray(value)) return [];
|
|
910
|
+
return value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim());
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function numericValue(...values) {
|
|
914
|
+
for (const value of values) {
|
|
915
|
+
if (value == null || value === "") continue;
|
|
916
|
+
const number = Number(value);
|
|
917
|
+
if (Number.isFinite(number)) return number;
|
|
918
|
+
}
|
|
919
|
+
return undefined;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function sortedSet(set) {
|
|
923
|
+
return [...set].sort((a, b) => a.localeCompare(b));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function singleOrArray(set) {
|
|
927
|
+
const values = sortedSet(set);
|
|
928
|
+
if (!values.length) return undefined;
|
|
929
|
+
return values.length === 1 ? values[0] : values;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function repoInfoForImport(provider, cwd, metadata = null) {
|
|
933
|
+
const repoCwd = repoCwdForImport(provider, cwd, metadata);
|
|
934
|
+
if (!repoCwd) return null;
|
|
935
|
+
return canonicalRepo(repoCwd);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function repoCwdForImport(provider, cwd, metadata = null) {
|
|
939
|
+
const metadataCwd = firstExistingDirectory(metadata?.originCwd, metadata?.cwd);
|
|
940
|
+
if (metadataCwd) return metadataCwd;
|
|
941
|
+
return claudeWorktreeParentRepo(provider, cwd) || cwd;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function claudeWorktreeParentRepo(provider, cwd) {
|
|
945
|
+
if (!isClaudeJsonlProvider(provider)) return "";
|
|
946
|
+
const resolved = path.resolve(String(cwd || ""));
|
|
947
|
+
const marker = `${path.sep}.claude${path.sep}worktrees${path.sep}`;
|
|
948
|
+
const markerIndex = resolved.indexOf(marker);
|
|
949
|
+
if (markerIndex === -1) return "";
|
|
950
|
+
const parentRepo = resolved.slice(0, markerIndex);
|
|
951
|
+
const stat = safeStat(parentRepo);
|
|
952
|
+
return stat && stat.isDirectory() ? parentRepo : "";
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function inferredJsonlSessionTitle(provider, messages) {
|
|
956
|
+
if (!isClaudeJsonlProvider(provider)) return "";
|
|
957
|
+
const firstUser = (messages || []).find((message) => message.role === "user" && !message.metadata?.providerGenerated);
|
|
958
|
+
return titleFromPrompt(firstUser?.content);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function titleFromPrompt(value) {
|
|
962
|
+
const cleaned = firstLine(value).replace(/\s+/g, " ").trim();
|
|
963
|
+
if (!cleaned) return "";
|
|
964
|
+
const max = 96;
|
|
965
|
+
return cleaned.length > max ? `${cleaned.slice(0, max - 1).trimEnd()}…` : cleaned;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function isClaudeJsonlProvider(provider) {
|
|
969
|
+
return provider === "claude_code" || provider === "claude_sdk";
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function jsonlSessionTitleForImport(parsed, metadata = null) {
|
|
973
|
+
return firstString(metadata?.title, parsed?.title);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function claudeCodeSidecarSessionSummary(metadata = null) {
|
|
977
|
+
if (!metadata) return null;
|
|
978
|
+
const sidecar = compactMetadata({
|
|
979
|
+
appSessionId: metadata.sessionId || undefined,
|
|
980
|
+
cliSessionId: metadata.cliSessionId || undefined,
|
|
981
|
+
title: metadata.title || undefined,
|
|
982
|
+
titleSource: metadata.titleSource || undefined,
|
|
983
|
+
cwd: metadata.cwd || undefined,
|
|
984
|
+
originCwd: metadata.originCwd || undefined,
|
|
985
|
+
worktreePath: metadata.worktreePath || undefined,
|
|
986
|
+
worktreeName: metadata.worktreeName || undefined,
|
|
987
|
+
sourceBranch: metadata.sourceBranch || undefined,
|
|
988
|
+
branch: metadata.branch || undefined,
|
|
989
|
+
createdAt: metadata.createdAt || undefined,
|
|
990
|
+
lastActivityAt: metadata.lastActivityAt || undefined,
|
|
991
|
+
model: metadata.model || undefined,
|
|
992
|
+
effort: metadata.effort || undefined,
|
|
993
|
+
permissionMode: metadata.permissionMode || undefined,
|
|
994
|
+
chromePermissionMode: metadata.chromePermissionMode || undefined,
|
|
995
|
+
completedTurns: metadata.completedTurns,
|
|
996
|
+
isArchived: typeof metadata.isArchived === "boolean" ? metadata.isArchived : undefined,
|
|
997
|
+
enabledMcpToolCount: metadata.enabledMcpToolCount,
|
|
998
|
+
mcpServerNames: metadata.mcpServerNames?.length ? metadata.mcpServerNames : undefined,
|
|
999
|
+
sourcePath: metadata.sourcePath || undefined
|
|
1000
|
+
});
|
|
1001
|
+
if (!sidecar) return null;
|
|
1002
|
+
return compactMetadata({
|
|
1003
|
+
claudeCodeSidecar: sidecar,
|
|
1004
|
+
modelUsage: metadata.model ? [{ model: metadata.model, source: "claude-code-sidecar" }] : undefined
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function mergeSessionSummaries(...summaries) {
|
|
1009
|
+
const result = {};
|
|
1010
|
+
for (const summary of summaries) {
|
|
1011
|
+
if (!summary || typeof summary !== "object" || Array.isArray(summary)) continue;
|
|
1012
|
+
for (const [key, value] of Object.entries(summary)) {
|
|
1013
|
+
if (key === "modelUsage" && Array.isArray(value)) {
|
|
1014
|
+
result.modelUsage = [...(Array.isArray(result.modelUsage) ? result.modelUsage : []), ...value];
|
|
1015
|
+
} else if (value && typeof value === "object" && !Array.isArray(value) && result[key] && typeof result[key] === "object" && !Array.isArray(result[key])) {
|
|
1016
|
+
result[key] = compactMetadata({ ...result[key], ...value });
|
|
1017
|
+
} else {
|
|
1018
|
+
result[key] = value;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return compactMetadata(result);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function firstExistingDirectory(...values) {
|
|
1026
|
+
for (const value of values) {
|
|
1027
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
1028
|
+
const stat = safeStat(value.trim());
|
|
1029
|
+
if (stat && stat.isDirectory()) return value.trim();
|
|
1030
|
+
}
|
|
1031
|
+
return "";
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function claudeCodeImportFingerprint(baseFingerprint, metadata = null) {
|
|
1035
|
+
if (!metadata?.sourcePath) return baseFingerprint;
|
|
1036
|
+
return `${baseFingerprint}:claude-code-session:${fileFingerprint(metadata.sourcePath, safeStat(metadata.sourcePath))}`;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function claudeSessionIdFromFilename(file) {
|
|
1040
|
+
const base = path.basename(String(file || "")).replace(/\.jsonl(?:\.zst)?$/i, "");
|
|
1041
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(base) ? base : "";
|
|
1042
|
+
}
|
|
1043
|
+
|
|
615
1044
|
function extractMessages(event, provider, context = {}) {
|
|
616
1045
|
const claudeMessages = extractClaudeMessagesFromEvent(event, provider, context);
|
|
617
1046
|
if (claudeMessages.length) {
|
|
@@ -649,9 +1078,54 @@ function dedupeAdjacentMessages(messages) {
|
|
|
649
1078
|
}
|
|
650
1079
|
result.push(message);
|
|
651
1080
|
}
|
|
1081
|
+
return dedupeDuplicateToolResults(result);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function dedupeDuplicateToolResults(messages) {
|
|
1085
|
+
const result = [];
|
|
1086
|
+
const toolResultsByKey = new Map();
|
|
1087
|
+
for (const message of messages) {
|
|
1088
|
+
const key = duplicateToolResultKey(message);
|
|
1089
|
+
if (key && toolResultsByKey.has(key)) {
|
|
1090
|
+
const existingIndex = toolResultsByKey.get(key);
|
|
1091
|
+
const existing = result[existingIndex];
|
|
1092
|
+
if (timestampsNear(existing?.timestamp, message?.timestamp)) {
|
|
1093
|
+
result[existingIndex] = preferredToolResultMessage(existing, message);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (key) toolResultsByKey.set(key, result.length);
|
|
1098
|
+
result.push(message);
|
|
1099
|
+
}
|
|
652
1100
|
return result;
|
|
653
1101
|
}
|
|
654
1102
|
|
|
1103
|
+
function duplicateToolResultKey(message) {
|
|
1104
|
+
const result = message?.metadata?.toolResult;
|
|
1105
|
+
const id = firstString(result?.id, result?.callId, result?.call_id, result?.toolCallId, result?.tool_call_id, result?.toolUseId, result?.tool_use_id);
|
|
1106
|
+
if (!id || message?.role !== "tool") return "";
|
|
1107
|
+
return `${message?.metadata?.provider || result?.provider || ""}:${id}`;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function preferredToolResultMessage(left, right) {
|
|
1111
|
+
return toolResultMessageDisplayScore(right) > toolResultMessageDisplayScore(left) ? right : left;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function toolResultMessageDisplayScore(message) {
|
|
1115
|
+
const result = message?.metadata?.toolResult || {};
|
|
1116
|
+
const rawCategory = normalizeToolToken(result.rawCategory);
|
|
1117
|
+
const kind = normalizeToolToken(result.kind);
|
|
1118
|
+
const category = normalizeToolToken(result.category);
|
|
1119
|
+
let score = 0;
|
|
1120
|
+
if (["exec_command_end", "patch_apply_end", "mcp_tool_call_end", "web_search_end", "tool_result", "tool_output"].includes(rawCategory)) score += 2000;
|
|
1121
|
+
if (rawCategory === "function_call_output" || rawCategory === "custom_tool_call_output") score -= 1000;
|
|
1122
|
+
if (category && category !== "function") score += 250;
|
|
1123
|
+
if (kind && !kind.startsWith("call_")) score += 150;
|
|
1124
|
+
if (/^\$\s/.test(String(result.summary || result.output || message?.content || ""))) score += 250;
|
|
1125
|
+
score += Math.min(200, String(result.output || message?.content || "").length / 200);
|
|
1126
|
+
return score;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
655
1129
|
function hasStructuredTranscriptMetadata(message) {
|
|
656
1130
|
const metadata = message?.metadata || {};
|
|
657
1131
|
return Boolean(
|
|
@@ -2379,6 +2853,7 @@ function discoverCliHistory(env = process.env, options = {}) {
|
|
|
2379
2853
|
const codexThreads = readCodexSessionEntries(env);
|
|
2380
2854
|
publish("codexCli", "Codex CLI", summarizeCodexThreads(codexThreads, "cli"));
|
|
2381
2855
|
publish("codexDesktop", "Codex Desktop", summarizeCodexThreads(codexThreads, "vscode"));
|
|
2856
|
+
publish("codexSdk", "Codex SDK jobs", summarizeCodexThreads(codexThreads, "exec"));
|
|
2382
2857
|
result.codex = summarizeCodexThreads(codexThreads);
|
|
2383
2858
|
|
|
2384
2859
|
const claudeScan = scanClaudeProjectFiles({
|
|
@@ -2388,7 +2863,7 @@ function discoverCliHistory(env = process.env, options = {}) {
|
|
|
2388
2863
|
|
|
2389
2864
|
const claudeDesktopSessions = readClaudeDesktopSessions({
|
|
2390
2865
|
onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "Claude App" })
|
|
2391
|
-
});
|
|
2866
|
+
}, env);
|
|
2392
2867
|
publish("claudeCodeDesktop", "Claude Code Desktop", summarizeClaudeDesktopSessions(claudeDesktopSessions, "claude-code-desktop-metadata"));
|
|
2393
2868
|
publish("claudeWorkspace", "Claude Workspace", summarizeClaudeDesktopSessions(claudeDesktopSessions, "claude-workspace-desktop"));
|
|
2394
2869
|
publish("claudeSdk", "Claude SDK jobs", summarizeClaudeSdkScan(claudeScan));
|
|
@@ -2443,13 +2918,38 @@ function discoverCliHistory(env = process.env, options = {}) {
|
|
|
2443
2918
|
);
|
|
2444
2919
|
|
|
2445
2920
|
publish(
|
|
2446
|
-
"
|
|
2447
|
-
"OpenCode",
|
|
2921
|
+
"opencodeCli",
|
|
2922
|
+
"OpenCode CLI",
|
|
2448
2923
|
summarizeStructuredSessions(
|
|
2449
2924
|
readOpenCodeSessions(env, {
|
|
2450
|
-
|
|
2925
|
+
openCodeKind: "cli",
|
|
2926
|
+
onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "OpenCode CLI" })
|
|
2451
2927
|
}),
|
|
2452
|
-
"OpenCode SQLite database
|
|
2928
|
+
"OpenCode CLI/core SQLite database plus project JSON session/message/part storage"
|
|
2929
|
+
)
|
|
2930
|
+
);
|
|
2931
|
+
|
|
2932
|
+
publish(
|
|
2933
|
+
"opencodeDesktop",
|
|
2934
|
+
"OpenCode Desktop",
|
|
2935
|
+
summarizeStructuredSessions(
|
|
2936
|
+
readOpenCodeSessions(env, {
|
|
2937
|
+
openCodeKind: "desktop",
|
|
2938
|
+
onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "OpenCode Desktop" })
|
|
2939
|
+
}),
|
|
2940
|
+
"OpenCode Desktop app-specific SQLite database and JSON session/message/part storage"
|
|
2941
|
+
)
|
|
2942
|
+
);
|
|
2943
|
+
|
|
2944
|
+
publish(
|
|
2945
|
+
"opencodeWeb",
|
|
2946
|
+
"OpenCode Web",
|
|
2947
|
+
summarizeStructuredSessions(
|
|
2948
|
+
readOpenCodeSessions(env, {
|
|
2949
|
+
openCodeKind: "web",
|
|
2950
|
+
onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "OpenCode Web" })
|
|
2951
|
+
}),
|
|
2952
|
+
"OpenCode web sessions from the shared OpenCode SQLite store"
|
|
2453
2953
|
)
|
|
2454
2954
|
);
|
|
2455
2955
|
|
|
@@ -2480,8 +2980,8 @@ function summarizeCodex(env = process.env, source) {
|
|
|
2480
2980
|
|
|
2481
2981
|
function summarizeCodexThreads(allThreads, source) {
|
|
2482
2982
|
const threads = allThreads.filter((thread) => {
|
|
2483
|
-
if (
|
|
2484
|
-
return
|
|
2983
|
+
if (source) return thread.source === source;
|
|
2984
|
+
return ["cli", "vscode"].includes(thread.source);
|
|
2485
2985
|
});
|
|
2486
2986
|
if (threads.length) {
|
|
2487
2987
|
const oldest = threads.length
|
|
@@ -2496,10 +2996,24 @@ function summarizeCodexThreads(allThreads, source) {
|
|
|
2496
2996
|
? "terminal `codex` sessions"
|
|
2497
2997
|
: source === "vscode"
|
|
2498
2998
|
? "Codex desktop app sessions"
|
|
2999
|
+
: source === "exec"
|
|
3000
|
+
? "Codex exec/SDK batch jobs; opt-in"
|
|
2499
3001
|
: "includes Codex CLI and Codex Desktop top-level sessions"
|
|
2500
3002
|
};
|
|
2501
3003
|
}
|
|
2502
|
-
return {
|
|
3004
|
+
return {
|
|
3005
|
+
sessions: 0,
|
|
3006
|
+
oldest: "",
|
|
3007
|
+
details: {},
|
|
3008
|
+
note:
|
|
3009
|
+
source === "cli"
|
|
3010
|
+
? "terminal `codex` sessions"
|
|
3011
|
+
: source === "vscode"
|
|
3012
|
+
? "Codex desktop app sessions"
|
|
3013
|
+
: source === "exec"
|
|
3014
|
+
? "Codex exec/SDK batch jobs; opt-in"
|
|
3015
|
+
: ""
|
|
3016
|
+
};
|
|
2503
3017
|
}
|
|
2504
3018
|
|
|
2505
3019
|
function summarizeClaude() {
|
|
@@ -2524,7 +3038,7 @@ function summarizeClaudeSdkScan(scan) {
|
|
|
2524
3038
|
}
|
|
2525
3039
|
|
|
2526
3040
|
function summarizeClaudeDesktop(env = process.env, options = {}) {
|
|
2527
|
-
const sessions = readClaudeDesktopSessions(options);
|
|
3041
|
+
const sessions = readClaudeDesktopSessions(options, env);
|
|
2528
3042
|
return summarizeClaudeDesktopSessions(sessions);
|
|
2529
3043
|
}
|
|
2530
3044
|
|
|
@@ -2755,11 +3269,13 @@ function classifyClaudeFile(file) {
|
|
|
2755
3269
|
}
|
|
2756
3270
|
let inspected = 0;
|
|
2757
3271
|
let sawSdkMessage = false;
|
|
3272
|
+
let sawRemoteControlTools = false;
|
|
2758
3273
|
for (const line of lines) {
|
|
2759
3274
|
if (!line.trim()) continue;
|
|
2760
3275
|
inspected++;
|
|
2761
3276
|
try {
|
|
2762
3277
|
const event = JSON.parse(line);
|
|
3278
|
+
if (isClaudeRemoteControlToolDelta(event)) sawRemoteControlTools = true;
|
|
2763
3279
|
if ((event.type === "user" || event.type === "assistant") && event.message) {
|
|
2764
3280
|
if (event.entrypoint === "sdk-cli") sawSdkMessage = true;
|
|
2765
3281
|
else return "conversation";
|
|
@@ -2767,9 +3283,14 @@ function classifyClaudeFile(file) {
|
|
|
2767
3283
|
} catch {
|
|
2768
3284
|
return "other";
|
|
2769
3285
|
}
|
|
2770
|
-
if (inspected >= 80) return sawSdkMessage ? "sdk-job" : "other";
|
|
3286
|
+
if (inspected >= 80) return sawSdkMessage ? (sawRemoteControlTools ? "conversation" : "sdk-job") : "other";
|
|
2771
3287
|
}
|
|
2772
|
-
return sawSdkMessage ? "sdk-job" : "other";
|
|
3288
|
+
return sawSdkMessage ? (sawRemoteControlTools ? "conversation" : "sdk-job") : "other";
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
function isClaudeRemoteControlToolDelta(event) {
|
|
3292
|
+
if (!event || event.type !== "attachment" || !Array.isArray(event.attachment?.addedNames)) return false;
|
|
3293
|
+
return event.attachment.addedNames.some((name) => isClaudeRemoteControlToolName(name));
|
|
2773
3294
|
}
|
|
2774
3295
|
|
|
2775
3296
|
function readInitialLines(file, maxLines, maxBytes = 1024 * 1024) {
|
|
@@ -3040,15 +3561,16 @@ function codexSupplementFingerprint(thread) {
|
|
|
3040
3561
|
function summarizeCodexSources(threads) {
|
|
3041
3562
|
const cli = threads.filter((thread) => thread.source === "cli").length;
|
|
3042
3563
|
const desktop = threads.filter((thread) => thread.source === "vscode").length;
|
|
3564
|
+
const exec = threads.filter((thread) => thread.source === "exec").length;
|
|
3043
3565
|
const summaries = threads.filter((thread) => thread.rawMemory || thread.rolloutSummary).length;
|
|
3044
3566
|
const archived = threads.filter((thread) => thread.sourceDetail === "archived_sessions").length;
|
|
3045
|
-
return { cli, desktop, ...(archived ? { archived } : {}), ...(summaries ? { summaries } : {}) };
|
|
3567
|
+
return { cli, desktop, ...(exec ? { exec } : {}), ...(archived ? { archived } : {}), ...(summaries ? { summaries } : {}) };
|
|
3046
3568
|
}
|
|
3047
3569
|
|
|
3048
|
-
function readClaudeDesktopSessions(options = {}) {
|
|
3570
|
+
function readClaudeDesktopSessions(options = {}, env = process.env) {
|
|
3049
3571
|
const roots = [
|
|
3050
|
-
{ root:
|
|
3051
|
-
{ root:
|
|
3572
|
+
{ root: claudeCodeSessionsRoot(env), kind: "claude-code-desktop-metadata" },
|
|
3573
|
+
{ root: claudeWorkspaceSessionsRoot(env), kind: "claude-workspace-desktop" }
|
|
3052
3574
|
];
|
|
3053
3575
|
const candidates = [];
|
|
3054
3576
|
for (const { root, kind } of roots) {
|
|
@@ -3071,6 +3593,90 @@ function readClaudeDesktopSessions(options = {}) {
|
|
|
3071
3593
|
return sessions;
|
|
3072
3594
|
}
|
|
3073
3595
|
|
|
3596
|
+
function claudeCodeSessionMetadataByCliSessionId(env = process.env) {
|
|
3597
|
+
const byCliSessionId = new Map();
|
|
3598
|
+
collectFiles(claudeCodeSessionsRoot(env), (file) => {
|
|
3599
|
+
if (!path.basename(file).startsWith("local_") || !file.endsWith(".json")) return;
|
|
3600
|
+
const metadata = parseClaudeCodeSessionMetadataFile(file);
|
|
3601
|
+
if (metadata?.cliSessionId) byCliSessionId.set(metadata.cliSessionId, metadata);
|
|
3602
|
+
});
|
|
3603
|
+
return byCliSessionId;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
function parseClaudeCodeSessionMetadataFile(file) {
|
|
3607
|
+
let data;
|
|
3608
|
+
try {
|
|
3609
|
+
data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
3610
|
+
} catch {
|
|
3611
|
+
return null;
|
|
3612
|
+
}
|
|
3613
|
+
const cliSessionId = firstString(data.cliSessionId);
|
|
3614
|
+
if (!cliSessionId) return null;
|
|
3615
|
+
return {
|
|
3616
|
+
cliSessionId,
|
|
3617
|
+
sessionId: firstString(data.sessionId),
|
|
3618
|
+
title: firstString(data.title),
|
|
3619
|
+
titleSource: firstString(data.titleSource),
|
|
3620
|
+
cwd: firstString(data.cwd),
|
|
3621
|
+
originCwd: firstString(data.originCwd),
|
|
3622
|
+
worktreePath: firstString(data.worktreePath),
|
|
3623
|
+
worktreeName: firstString(data.worktreeName),
|
|
3624
|
+
sourceBranch: firstString(data.sourceBranch),
|
|
3625
|
+
branch: firstString(data.branch),
|
|
3626
|
+
createdAt: toIso(data.createdAt),
|
|
3627
|
+
lastActivityAt: toIso(data.lastActivityAt),
|
|
3628
|
+
model: firstString(data.model),
|
|
3629
|
+
effort: firstString(data.effort),
|
|
3630
|
+
permissionMode: firstString(data.permissionMode),
|
|
3631
|
+
chromePermissionMode: firstString(data.chromePermissionMode),
|
|
3632
|
+
completedTurns: numberValue(data.completedTurns),
|
|
3633
|
+
isArchived: typeof data.isArchived === "boolean" ? data.isArchived : undefined,
|
|
3634
|
+
enabledMcpToolCount: enabledMcpToolCount(data.enabledMcpTools),
|
|
3635
|
+
mcpServerNames: claudeMcpServerNames(data.remoteMcpServersConfig),
|
|
3636
|
+
sourcePath: file
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
function enabledMcpToolCount(value) {
|
|
3641
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
3642
|
+
return Object.values(value).filter(Boolean).length;
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
function claudeMcpServerNames(value) {
|
|
3646
|
+
if (!Array.isArray(value)) return [];
|
|
3647
|
+
return [...new Set(value.map((server) => firstString(server?.name)).filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
function compactMetadata(value) {
|
|
3651
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value || null;
|
|
3652
|
+
const result = {};
|
|
3653
|
+
for (const [key, item] of Object.entries(value)) {
|
|
3654
|
+
if (item === undefined || item === null || item === "") continue;
|
|
3655
|
+
if (Array.isArray(item) && !item.length) continue;
|
|
3656
|
+
if (item && typeof item === "object" && !Array.isArray(item) && !Object.keys(item).length) continue;
|
|
3657
|
+
result[key] = item;
|
|
3658
|
+
}
|
|
3659
|
+
return Object.keys(result).length ? result : null;
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
function numberValue(value) {
|
|
3663
|
+
const n = Number(value);
|
|
3664
|
+
return Number.isFinite(n) ? n : undefined;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
function claudeAppSupportRoot(env = process.env) {
|
|
3668
|
+
const home = env && env.HOME ? env.HOME : os.homedir();
|
|
3669
|
+
return env.CLAUDE_APP_SUPPORT || path.join(home, "Library", "Application Support", "Claude");
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
function claudeCodeSessionsRoot(env = process.env) {
|
|
3673
|
+
return path.join(claudeAppSupportRoot(env), "claude-code-sessions");
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
function claudeWorkspaceSessionsRoot(env = process.env) {
|
|
3677
|
+
return path.join(claudeAppSupportRoot(env), "local-agent-mode-sessions");
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3074
3680
|
function parseClaudeDesktopSessionFile(file, kind = "claude-workspace-desktop") {
|
|
3075
3681
|
let data;
|
|
3076
3682
|
try {
|
|
@@ -3337,7 +3943,10 @@ function cursorBuildComposerInfoLookup(env = process.env) {
|
|
|
3337
3943
|
"key,",
|
|
3338
3944
|
"json_extract(value, '$.modelId') as modelId,",
|
|
3339
3945
|
"json_extract(value, '$.modelName') as modelName,",
|
|
3340
|
-
"json_extract(value, '$.model') as model",
|
|
3946
|
+
"json_extract(value, '$.model') as model,",
|
|
3947
|
+
"json_extract(value, '$.modelInfo.modelName') as modelInfoModelName,",
|
|
3948
|
+
"json_extract(value, '$.modelInfo.modelId') as modelInfoModelId,",
|
|
3949
|
+
"json_extract(value, '$.modelInfo.model') as modelInfoModel",
|
|
3341
3950
|
"from cursorDiskKV where",
|
|
3342
3951
|
"json_valid(value) and",
|
|
3343
3952
|
cursorDiskKvPrefixRangeCondition("bubbleId:")
|
|
@@ -3348,7 +3957,14 @@ function cursorBuildComposerInfoLookup(env = process.env) {
|
|
|
3348
3957
|
const keyMatch = String(row.key || "").match(/^bubbleId:([^:]+):/);
|
|
3349
3958
|
if (!keyMatch) continue;
|
|
3350
3959
|
const composerId = keyMatch[1].toLowerCase();
|
|
3351
|
-
const model = firstString(
|
|
3960
|
+
const model = firstString(
|
|
3961
|
+
row.modelInfoModelName,
|
|
3962
|
+
row.modelInfoModelId,
|
|
3963
|
+
row.modelInfoModel,
|
|
3964
|
+
row.modelId,
|
|
3965
|
+
row.modelName,
|
|
3966
|
+
row.model
|
|
3967
|
+
);
|
|
3352
3968
|
if (!model) continue;
|
|
3353
3969
|
const entry = info.get(composerId) || { title: "", modelHist: new Map() };
|
|
3354
3970
|
entry.modelHist.set(model, (entry.modelHist.get(model) || 0) + 1);
|
|
@@ -3960,6 +4576,7 @@ function cursorGlobalBubbleSelectColumns(valueExpression = "value", keyExpressio
|
|
|
3960
4576
|
`json_extract(${valueExpression}, '$.modelName') as modelName`,
|
|
3961
4577
|
`json_extract(${valueExpression}, '$.modelSlug') as modelSlug`,
|
|
3962
4578
|
`json_extract(${valueExpression}, '$.modelConfig') as modelConfig`,
|
|
4579
|
+
`json_extract(${valueExpression}, '$.modelInfo') as modelInfo`,
|
|
3963
4580
|
`json_extract(${valueExpression}, '$.providerOptions') as providerOptions`,
|
|
3964
4581
|
`json_extract(${valueExpression}, '$.status') as status`,
|
|
3965
4582
|
`json_extract(${valueExpression}, '$.state') as state`,
|
|
@@ -4014,6 +4631,7 @@ function cursorGlobalBubbleDataFromRow(row) {
|
|
|
4014
4631
|
modelName: row.modelName,
|
|
4015
4632
|
modelSlug: row.modelSlug,
|
|
4016
4633
|
modelConfig: cursorParseSqliteJsonColumn(row.modelConfig),
|
|
4634
|
+
modelInfo: cursorParseSqliteJsonColumn(row.modelInfo),
|
|
4017
4635
|
providerOptions: cursorParseSqliteJsonColumn(row.providerOptions),
|
|
4018
4636
|
status: row.status,
|
|
4019
4637
|
state: cursorParseSqliteJsonColumn(row.state) || row.state,
|
|
@@ -5151,6 +5769,11 @@ function cursorMessageMetadata(record, source) {
|
|
|
5151
5769
|
|
|
5152
5770
|
function cursorModel(record) {
|
|
5153
5771
|
return firstCursorModel(
|
|
5772
|
+
record?.modelInfo?.modelName,
|
|
5773
|
+
record?.modelInfo?.modelId,
|
|
5774
|
+
record?.modelInfo?.model,
|
|
5775
|
+
record?.message?.modelInfo?.modelName,
|
|
5776
|
+
record?.message?.modelInfo?.modelId,
|
|
5154
5777
|
record?.model,
|
|
5155
5778
|
record?.modelId,
|
|
5156
5779
|
record?.modelID,
|
|
@@ -6539,15 +7162,15 @@ function clineTitle(messages) {
|
|
|
6539
7162
|
}
|
|
6540
7163
|
|
|
6541
7164
|
function readOpenCodeSessions(env = process.env, options = {}) {
|
|
6542
|
-
const dbs = openCodeDatabaseFiles(env);
|
|
6543
|
-
const roots = openCodeStorageRoots(env);
|
|
7165
|
+
const dbs = openCodeDatabaseFiles(env, options);
|
|
7166
|
+
const roots = openCodeStorageRoots(env, options);
|
|
6544
7167
|
const files = roots.flatMap((root) => openCodeSessionFiles(root).map((file) => ({ root, file })));
|
|
6545
7168
|
const sessions = [];
|
|
6546
7169
|
reportDiscoveryProgress(options, { current: 0, total: dbs.length, message: "reading OpenCode SQLite stores" });
|
|
6547
7170
|
for (let index = 0; index < dbs.length; index++) {
|
|
6548
7171
|
let dbSessions = [];
|
|
6549
7172
|
try {
|
|
6550
|
-
dbSessions = readOpenCodeSqliteSessionsFromDb(dbs[index], options);
|
|
7173
|
+
dbSessions = readOpenCodeSqliteSessionsFromDb(dbs[index], options, env);
|
|
6551
7174
|
} catch (error) {
|
|
6552
7175
|
reportDiscoveryProgress(options, {
|
|
6553
7176
|
current: index + 1,
|
|
@@ -6569,7 +7192,7 @@ function readOpenCodeSessions(env = process.env, options = {}) {
|
|
|
6569
7192
|
reportDiscoveryProgress(options, { current: 0, total: files.length, message: "reading OpenCode storage" });
|
|
6570
7193
|
for (let index = 0; index < files.length; index++) {
|
|
6571
7194
|
const item = files[index];
|
|
6572
|
-
const session = parseOpenCodeSessionFile(item.file, item.root);
|
|
7195
|
+
const session = parseOpenCodeSessionFile(item.file, item.root, env, options);
|
|
6573
7196
|
if (session) {
|
|
6574
7197
|
sessions.push(session);
|
|
6575
7198
|
seenSessionIds.add(session.sessionId.replace(/^opencode-/, ""));
|
|
@@ -6584,38 +7207,70 @@ function readOpenCodeSessions(env = process.env, options = {}) {
|
|
|
6584
7207
|
for (const root of roots) {
|
|
6585
7208
|
for (const sessionId of openCodeMessageSessionIds(root)) {
|
|
6586
7209
|
if (seenSessionIds.has(sessionId)) continue;
|
|
6587
|
-
const session = parseOpenCodeMessageOnlySession(root, sessionId);
|
|
7210
|
+
const session = parseOpenCodeMessageOnlySession(root, sessionId, env, options);
|
|
6588
7211
|
if (session) {
|
|
6589
7212
|
sessions.push(session);
|
|
6590
7213
|
seenSessionIds.add(sessionId);
|
|
6591
7214
|
}
|
|
6592
7215
|
}
|
|
6593
7216
|
}
|
|
6594
|
-
return dedupeStructuredSessions(sessions, "opencode");
|
|
7217
|
+
return filterOpenCodeSessionsForKind(dedupeStructuredSessions(sessions, "opencode"), options.openCodeKind);
|
|
7218
|
+
}
|
|
7219
|
+
|
|
7220
|
+
function filterOpenCodeSessionsForKind(sessions, kind) {
|
|
7221
|
+
if (!OPENCODE_SOURCE_KINDS.has(kind)) return sessions;
|
|
7222
|
+
return (sessions || []).filter((session) => openCodeSourceKindForType(session.sourceType) === kind);
|
|
6595
7223
|
}
|
|
6596
7224
|
|
|
6597
|
-
function openCodeDataRoots(env = process.env) {
|
|
7225
|
+
function openCodeDataRoots(env = process.env, options = {}) {
|
|
6598
7226
|
const configured = env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR;
|
|
6599
7227
|
if (configured) return existingUniquePaths([configured]);
|
|
6600
|
-
|
|
7228
|
+
if (options.openCodeKind === "cli") return existingUniquePaths(openCodeCliDataRoots(env));
|
|
7229
|
+
if (options.openCodeKind === "desktop") return existingUniquePaths(openCodeDesktopDataRoots(env));
|
|
7230
|
+
if (options.openCodeKind === "web") return [];
|
|
7231
|
+
return existingUniquePaths([...openCodeCliDataRoots(env), ...openCodeDesktopDataRoots(env)]);
|
|
7232
|
+
}
|
|
7233
|
+
|
|
7234
|
+
function openCodeCliDataRoots(env = process.env) {
|
|
7235
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
6601
7236
|
const roots = [
|
|
6602
|
-
path.join(home, ".local", "share", "opencode")
|
|
7237
|
+
path.join(home, ".local", "share", "opencode")
|
|
7238
|
+
];
|
|
7239
|
+
return roots;
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
function openCodeDesktopDataRoots(env = process.env) {
|
|
7243
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
7244
|
+
const roots = [
|
|
7245
|
+
path.join(home, "Library", "Application Support", "ai.opencode.desktop"),
|
|
6603
7246
|
path.join(home, "Library", "Application Support", "opencode"),
|
|
6604
7247
|
path.join(home, ".local", "share", "ai.opencode.app"),
|
|
6605
7248
|
path.join(home, "Library", "Application Support", "ai.opencode.app")
|
|
6606
7249
|
];
|
|
6607
7250
|
const appData = env.APPDATA || env.LOCALAPPDATA || env.LocalAppData;
|
|
6608
7251
|
if (appData) {
|
|
7252
|
+
roots.push(path.join(appData, "ai.opencode.desktop"));
|
|
6609
7253
|
roots.push(path.join(appData, "opencode"));
|
|
6610
7254
|
roots.push(path.join(appData, "ai.opencode.app"));
|
|
6611
7255
|
}
|
|
6612
7256
|
return existingUniquePaths(roots);
|
|
6613
7257
|
}
|
|
6614
7258
|
|
|
6615
|
-
function
|
|
7259
|
+
function openCodeSqliteDataRoots(env = process.env) {
|
|
7260
|
+
return existingUniquePaths([...openCodeCliDataRoots(env), ...openCodeDesktopDataRoots(env)]);
|
|
7261
|
+
}
|
|
7262
|
+
|
|
7263
|
+
function openCodeDatabaseRoots(env = process.env, options = {}) {
|
|
7264
|
+
const configured = env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR;
|
|
7265
|
+
if (configured) return existingUniquePaths([configured]);
|
|
7266
|
+
if (OPENCODE_SOURCE_KINDS.has(options.openCodeKind)) return openCodeSqliteDataRoots(env);
|
|
7267
|
+
return openCodeSqliteDataRoots(env);
|
|
7268
|
+
}
|
|
7269
|
+
|
|
7270
|
+
function openCodeStorageRoots(env = process.env, options = {}) {
|
|
6616
7271
|
const explicit = envPathList(env.AGENTLOG_OPENCODE_STORAGE_ROOTS || env.AGENTLOG_OPENCODE_STORAGE_DIR);
|
|
6617
7272
|
if (explicit.length) return existingUniquePaths(explicit);
|
|
6618
|
-
const dataRoots = openCodeDataRoots(env);
|
|
7273
|
+
const dataRoots = openCodeDataRoots(env, options);
|
|
6619
7274
|
const roots = [];
|
|
6620
7275
|
for (const dataRoot of dataRoots) {
|
|
6621
7276
|
roots.push(path.join(dataRoot, "storage"));
|
|
@@ -6634,11 +7289,11 @@ function openCodeStorageRoots(env = process.env) {
|
|
|
6634
7289
|
return existingUniquePaths(roots);
|
|
6635
7290
|
}
|
|
6636
7291
|
|
|
6637
|
-
function openCodeDatabaseFiles(env = process.env) {
|
|
7292
|
+
function openCodeDatabaseFiles(env = process.env, options = {}) {
|
|
6638
7293
|
const explicit = envPathList(env.AGENTLOG_OPENCODE_DB || env.AGENTLOG_OPENCODE_DATABASE || env.OPENCODE_DB);
|
|
6639
7294
|
if (explicit.length) return existingUniquePaths(explicit);
|
|
6640
7295
|
if ((env.AGENTLOG_OPENCODE_STORAGE_ROOTS || env.AGENTLOG_OPENCODE_STORAGE_DIR) && !(env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR)) return [];
|
|
6641
|
-
return existingUniquePaths(
|
|
7296
|
+
return existingUniquePaths(openCodeDatabaseRoots(env, options).flatMap((root) => [
|
|
6642
7297
|
path.join(root, "opencode.db"),
|
|
6643
7298
|
path.join(root, "storage", "opencode.db")
|
|
6644
7299
|
]));
|
|
@@ -6668,11 +7323,40 @@ function openCodeMessageSessionIds(root) {
|
|
|
6668
7323
|
.sort((a, b) => a.localeCompare(b));
|
|
6669
7324
|
}
|
|
6670
7325
|
|
|
6671
|
-
function
|
|
7326
|
+
function openCodeDesktopSessionHints(env = process.env) {
|
|
7327
|
+
const hints = new Map();
|
|
7328
|
+
for (const root of openCodeDesktopDataRoots(env)) {
|
|
7329
|
+
collectFilesLimited(root, (file) => {
|
|
7330
|
+
if (!openCodeDesktopHintFile(file)) return;
|
|
7331
|
+
const stat = safeStat(file);
|
|
7332
|
+
if (!stat || stat.size > 2 * 1024 * 1024) return;
|
|
7333
|
+
let text = "";
|
|
7334
|
+
try {
|
|
7335
|
+
text = fs.readFileSync(file, "utf8");
|
|
7336
|
+
} catch {
|
|
7337
|
+
return;
|
|
7338
|
+
}
|
|
7339
|
+
for (const id of text.match(OPENCODE_SESSION_ID_RE) || []) {
|
|
7340
|
+
const files = hints.get(id) || new Set();
|
|
7341
|
+
files.add(file);
|
|
7342
|
+
hints.set(id, files);
|
|
7343
|
+
}
|
|
7344
|
+
}, 2, { skipDirs: new Set(["Cache", "Cache_Data", "Code Cache", "GPUCache", "blob_storage", "logs"]) });
|
|
7345
|
+
}
|
|
7346
|
+
return hints;
|
|
7347
|
+
}
|
|
7348
|
+
|
|
7349
|
+
function openCodeDesktopHintFile(file) {
|
|
7350
|
+
const base = path.basename(String(file || ""));
|
|
7351
|
+
return base.endsWith(".dat") || base === "opencode.settings" || base === "settings.json";
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7354
|
+
function readOpenCodeSqliteSessionsFromDb(dbPath, options = {}, env = process.env) {
|
|
6672
7355
|
if (!safeStat(dbPath)) return [];
|
|
6673
7356
|
if (!sqliteTableExists(dbPath, "session") || !sqliteTableExists(dbPath, "message") || !sqliteTableExists(dbPath, "part")) return [];
|
|
6674
7357
|
const sessionRows = readOpenCodeSqliteSessionRows(dbPath, options);
|
|
6675
7358
|
if (!sessionRows.length) return [];
|
|
7359
|
+
const classifications = openCodeSqliteSessionClassifications(sessionRows, dbPath, env, options);
|
|
6676
7360
|
const sessionIds = sessionRows.map((row) => row.id).filter(Boolean);
|
|
6677
7361
|
const messageRows = sortOpenCodeSqliteRows(readOpenCodeSqliteMessageRows(dbPath, sessionIds), ["session_id", "time_created", "id"]);
|
|
6678
7362
|
const partRows = sortOpenCodeSqliteRows(readOpenCodeSqlitePartRows(dbPath, sessionIds), ["session_id", "message_id", "time_created", "id"]);
|
|
@@ -6681,20 +7365,22 @@ function readOpenCodeSqliteSessionsFromDb(dbPath, options = {}) {
|
|
|
6681
7365
|
const storageRoot = path.join(path.dirname(dbPath), "storage");
|
|
6682
7366
|
const sessions = [];
|
|
6683
7367
|
for (const row of sessionRows) {
|
|
7368
|
+
const sourceType = classifications.sourceTypes.get(String(row.id || "")) || openCodeSqliteSourceType(dbPath, env, options);
|
|
6684
7369
|
const rows = messagesBySession.get(row.id) || [];
|
|
6685
7370
|
const messages = stampMessages(
|
|
6686
7371
|
dedupeAdjacentMessages(rows.flatMap((messageRow, index) => openCodeSqliteMessagesFromRow(messageRow, partsByMessage.get(messageRow.id) || [], index)))
|
|
6687
7372
|
.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
|
|
6688
|
-
|
|
7373
|
+
sourceType
|
|
6689
7374
|
);
|
|
6690
7375
|
const diffFile = path.join(storageRoot, "session_diff", `${row.id}.json`);
|
|
6691
7376
|
const diffMessage = openCodeDiffMessage(diffFile, messages[messages.length - 1]?.timestamp || toIso(row.time_updated || row.time_created));
|
|
6692
|
-
const finalMessages = diffMessage ? messages.concat(stampMessages([diffMessage],
|
|
7377
|
+
const finalMessages = diffMessage ? messages.concat(stampMessages([diffMessage], sourceType)) : messages;
|
|
6693
7378
|
if (!finalMessages.length) continue;
|
|
6694
7379
|
const sourceFiles = [dbPath, safeStat(diffFile) ? diffFile : ""].filter(Boolean);
|
|
6695
7380
|
const startedAt = toIso(row.time_created) || finalMessages[0]?.timestamp || new Date(safeStat(dbPath)?.mtimeMs || Date.now()).toISOString();
|
|
6696
7381
|
const endedAt = toIso(row.time_updated) || finalMessages[finalMessages.length - 1]?.timestamp || startedAt;
|
|
6697
7382
|
const cwd = firstString(row.directory, row.path, row.project_worktree, openCodeCwdFromMessages(finalMessages));
|
|
7383
|
+
const hintFiles = classifications.hintFiles.get(String(row.id || "")) || [];
|
|
6698
7384
|
sessions.push({
|
|
6699
7385
|
sessionId: `opencode-${row.id}`,
|
|
6700
7386
|
title: firstString(row.title, row.slug, clineTitle(finalMessages), row.id),
|
|
@@ -6703,11 +7389,12 @@ function readOpenCodeSqliteSessionsFromDb(dbPath, options = {}) {
|
|
|
6703
7389
|
endedAt,
|
|
6704
7390
|
messages: finalMessages,
|
|
6705
7391
|
sourcePath: `${dbPath}#${row.id}`,
|
|
6706
|
-
sourceFiles,
|
|
6707
|
-
sourceType
|
|
6708
|
-
fingerprint: openCodeSqliteSessionFingerprint(dbPath, row, rows, sourceFiles),
|
|
7392
|
+
sourceFiles: existingUniquePaths([...sourceFiles, ...hintFiles]),
|
|
7393
|
+
sourceType,
|
|
7394
|
+
fingerprint: openCodeSqliteSessionFingerprint(dbPath, row, rows, existingUniquePaths([...sourceFiles, ...hintFiles]), sourceType),
|
|
6709
7395
|
detailKey: "sqliteSessions",
|
|
6710
7396
|
sessionSummary: {
|
|
7397
|
+
source: openCodeSourceKindForType(sourceType),
|
|
6711
7398
|
projectId: row.project_id || undefined,
|
|
6712
7399
|
parentId: row.parent_id || undefined,
|
|
6713
7400
|
workspaceId: row.workspace_id || undefined,
|
|
@@ -6763,6 +7450,92 @@ function readOpenCodeSqliteSessionRows(dbPath, options = {}) {
|
|
|
6763
7450
|
return readSqliteJson(dbPath, queryParts.join(" "), "OpenCode SQLite sessions");
|
|
6764
7451
|
}
|
|
6765
7452
|
|
|
7453
|
+
function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process.env, options = {}) {
|
|
7454
|
+
const baseSourceType = openCodeSqliteSourceType(dbPath, env, options);
|
|
7455
|
+
const rowsById = new Map((sessionRows || []).map((row) => [String(row.id || ""), row]).filter(([id]) => id));
|
|
7456
|
+
const desktopHints = openCodeDesktopSessionHints(env);
|
|
7457
|
+
const sourceTypes = new Map();
|
|
7458
|
+
const hintFiles = new Map();
|
|
7459
|
+
const classify = (row, visiting = new Set()) => {
|
|
7460
|
+
const id = String(row?.id || "");
|
|
7461
|
+
if (!id) return baseSourceType;
|
|
7462
|
+
if (sourceTypes.has(id)) return sourceTypes.get(id);
|
|
7463
|
+
if (desktopHints.has(id)) {
|
|
7464
|
+
sourceTypes.set(id, "opencode-desktop-sqlite-history");
|
|
7465
|
+
hintFiles.set(id, [...desktopHints.get(id)]);
|
|
7466
|
+
return "opencode-desktop-sqlite-history";
|
|
7467
|
+
}
|
|
7468
|
+
if (row?.parent_id && !visiting.has(id)) {
|
|
7469
|
+
visiting.add(id);
|
|
7470
|
+
const parentId = String(row.parent_id);
|
|
7471
|
+
const parent = rowsById.get(parentId);
|
|
7472
|
+
const parentSourceType = parent ? classify(parent, visiting) : "";
|
|
7473
|
+
if (parentSourceType === "opencode-desktop-sqlite-history") {
|
|
7474
|
+
sourceTypes.set(id, parentSourceType);
|
|
7475
|
+
const parentFiles = hintFiles.get(parentId);
|
|
7476
|
+
if (parentFiles?.length) hintFiles.set(id, parentFiles);
|
|
7477
|
+
return parentSourceType;
|
|
7478
|
+
}
|
|
7479
|
+
}
|
|
7480
|
+
const sourceType = openCodeSqliteRowSourceType(row, dbPath, env, options, baseSourceType);
|
|
7481
|
+
sourceTypes.set(id, sourceType);
|
|
7482
|
+
return sourceType;
|
|
7483
|
+
};
|
|
7484
|
+
for (const row of sessionRows || []) classify(row);
|
|
7485
|
+
return { sourceTypes, hintFiles };
|
|
7486
|
+
}
|
|
7487
|
+
|
|
7488
|
+
function openCodeSqliteRowSourceType(row, dbPath, env = process.env, options = {}, baseSourceType = openCodeSqliteSourceType(dbPath, env, options)) {
|
|
7489
|
+
if (baseSourceType === "opencode-desktop-sqlite-history") return baseSourceType;
|
|
7490
|
+
if (!pathInsideAny(dbPath, openCodeCliDataRoots(env)) && !OPENCODE_SOURCE_KINDS.has(options.openCodeKind)) return baseSourceType;
|
|
7491
|
+
if (openCodeSqliteRowHasCliMetadata(row)) return "opencode-cli-sqlite-history";
|
|
7492
|
+
if (openCodeSqliteRowLooksWeb(row, dbPath, env)) return "opencode-web-sqlite-history";
|
|
7493
|
+
return baseSourceType;
|
|
7494
|
+
}
|
|
7495
|
+
|
|
7496
|
+
function openCodeSqliteRowHasCliMetadata(row) {
|
|
7497
|
+
return Boolean(firstString(row?.agent, row?.model));
|
|
7498
|
+
}
|
|
7499
|
+
|
|
7500
|
+
function openCodeSqliteRowLooksWeb(row, dbPath, env = process.env) {
|
|
7501
|
+
const version = firstString(row?.version);
|
|
7502
|
+
if (!version || version === "local") return false;
|
|
7503
|
+
return pathInsideAny(dbPath, openCodeCliDataRoots(env));
|
|
7504
|
+
}
|
|
7505
|
+
|
|
7506
|
+
function openCodeSqliteSourceType(dbPath, env = process.env, options = {}) {
|
|
7507
|
+
if (pathInsideAny(dbPath, openCodeDesktopDataRoots(env))) return "opencode-desktop-sqlite-history";
|
|
7508
|
+
if (pathInsideAny(dbPath, openCodeCliDataRoots(env))) return "opencode-sqlite-history";
|
|
7509
|
+
if (options.openCodeKind === "cli") return "opencode-cli-sqlite-history";
|
|
7510
|
+
if (options.openCodeKind === "desktop") return "opencode-desktop-sqlite-history";
|
|
7511
|
+
if (options.openCodeKind === "web") return "opencode-web-sqlite-history";
|
|
7512
|
+
return "opencode-sqlite-history";
|
|
7513
|
+
}
|
|
7514
|
+
|
|
7515
|
+
function openCodeStorageSourceType(storageRoot, env = process.env, options = {}) {
|
|
7516
|
+
if (options.openCodeKind === "cli") return "opencode-cli-history";
|
|
7517
|
+
if (options.openCodeKind === "desktop") return "opencode-desktop-history";
|
|
7518
|
+
if (pathInsideAny(storageRoot, openCodeCliDataRoots(env))) return "opencode-cli-history";
|
|
7519
|
+
if (pathInsideAny(storageRoot, openCodeDesktopDataRoots(env))) return "opencode-desktop-history";
|
|
7520
|
+
return "opencode-history";
|
|
7521
|
+
}
|
|
7522
|
+
|
|
7523
|
+
function openCodeSourceKindForType(sourceType) {
|
|
7524
|
+
if (String(sourceType || "").includes("cli")) return "cli";
|
|
7525
|
+
if (String(sourceType || "").includes("desktop")) return "desktop";
|
|
7526
|
+
if (String(sourceType || "").includes("web")) return "web";
|
|
7527
|
+
return "unknown";
|
|
7528
|
+
}
|
|
7529
|
+
|
|
7530
|
+
function pathInsideAny(candidate, roots) {
|
|
7531
|
+
const resolved = path.resolve(String(candidate || ""));
|
|
7532
|
+
return (roots || []).some((root) => {
|
|
7533
|
+
if (!root) return false;
|
|
7534
|
+
const resolvedRoot = path.resolve(String(root));
|
|
7535
|
+
return resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}${path.sep}`);
|
|
7536
|
+
});
|
|
7537
|
+
}
|
|
7538
|
+
|
|
6766
7539
|
function openCodeSqliteSessionTimestampExpr(sessionColumns) {
|
|
6767
7540
|
const candidates = ["time_updated", "time_created"].filter((column) => sessionColumns.has(column)).map((column) => `s.${column}`);
|
|
6768
7541
|
if (!candidates.length) return "";
|
|
@@ -6915,19 +7688,20 @@ function openCodeUsageFromMessageData(data, parts = []) {
|
|
|
6915
7688
|
return Object.values(usage).some((value) => value !== undefined) ? usage : null;
|
|
6916
7689
|
}
|
|
6917
7690
|
|
|
6918
|
-
function openCodeSqliteSessionFingerprint(dbPath, row, messageRows, sourceFiles) {
|
|
7691
|
+
function openCodeSqliteSessionFingerprint(dbPath, row, messageRows, sourceFiles, sourceType = "opencode-sqlite-history") {
|
|
6919
7692
|
const sessionRevision = [
|
|
6920
7693
|
row.id,
|
|
6921
7694
|
row.time_updated || row.time_created || "",
|
|
6922
7695
|
messageRows.length,
|
|
6923
7696
|
messageRows.map((message) => `${message.id}:${message.time_updated || message.time_created || ""}`).join("|")
|
|
6924
7697
|
].join(":");
|
|
6925
|
-
return `${fingerprintPrefix(
|
|
7698
|
+
return `${fingerprintPrefix(sourceType)}:${structuredSessionFingerprint({ sourcePath: dbPath, sourceFiles })}:${hashId(sessionRevision)}`;
|
|
6926
7699
|
}
|
|
6927
7700
|
|
|
6928
|
-
function parseOpenCodeSessionFile(file, storageRoot) {
|
|
7701
|
+
function parseOpenCodeSessionFile(file, storageRoot, env = process.env, options = {}) {
|
|
6929
7702
|
const info = readJsonMaybe(file, null);
|
|
6930
7703
|
if (!info || typeof info !== "object") return null;
|
|
7704
|
+
const sourceType = openCodeStorageSourceType(storageRoot, env, options);
|
|
6931
7705
|
const sessionId = firstString(info.id, info.sessionID, info.sessionId, path.basename(file, ".json"));
|
|
6932
7706
|
if (!sessionId) return null;
|
|
6933
7707
|
const projectId = firstString(info.projectID, info.projectId, path.basename(path.dirname(file)));
|
|
@@ -6938,7 +7712,7 @@ function parseOpenCodeSessionFile(file, storageRoot) {
|
|
|
6938
7712
|
const diffMessage = openCodeDiffMessage(diffFile, parsedMessages[parsedMessages.length - 1]?.timestamp || toIso(info.time?.updated || info.updatedAt || info.createdAt));
|
|
6939
7713
|
const messages = stampMessages(
|
|
6940
7714
|
dedupeAdjacentMessages(parsedMessages.concat(diffMessage ? [diffMessage] : [])).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
|
|
6941
|
-
|
|
7715
|
+
sourceType
|
|
6942
7716
|
);
|
|
6943
7717
|
if (!messages.length) return null;
|
|
6944
7718
|
const sourceFiles = [
|
|
@@ -6960,21 +7734,23 @@ function parseOpenCodeSessionFile(file, storageRoot) {
|
|
|
6960
7734
|
messages,
|
|
6961
7735
|
sourcePath: file,
|
|
6962
7736
|
sourceFiles,
|
|
6963
|
-
sourceType
|
|
6964
|
-
fingerprint: `${fingerprintPrefix(
|
|
6965
|
-
detailKey: "sessions"
|
|
7737
|
+
sourceType,
|
|
7738
|
+
fingerprint: `${fingerprintPrefix(sourceType)}:${structuredSessionFingerprint({ sourcePath: file, sourceFiles })}`,
|
|
7739
|
+
detailKey: "sessions",
|
|
7740
|
+
sessionSummary: { source: openCodeSourceKindForType(sourceType) }
|
|
6966
7741
|
};
|
|
6967
7742
|
}
|
|
6968
7743
|
|
|
6969
|
-
function parseOpenCodeMessageOnlySession(storageRoot, sessionId) {
|
|
7744
|
+
function parseOpenCodeMessageOnlySession(storageRoot, sessionId, env = process.env, options = {}) {
|
|
6970
7745
|
if (!sessionId) return null;
|
|
7746
|
+
const sourceType = openCodeStorageSourceType(storageRoot, env, options);
|
|
6971
7747
|
const messageFiles = openCodeMessageFiles(storageRoot, sessionId);
|
|
6972
7748
|
const parsedMessages = messageFiles.flatMap((messageFile, index) => openCodeMessagesFromFile(messageFile, storageRoot, index));
|
|
6973
7749
|
const diffFile = path.join(storageRoot, "session_diff", `${sessionId}.json`);
|
|
6974
7750
|
const diffMessage = openCodeDiffMessage(diffFile, parsedMessages[parsedMessages.length - 1]?.timestamp);
|
|
6975
7751
|
const messages = stampMessages(
|
|
6976
7752
|
dedupeAdjacentMessages(parsedMessages.concat(diffMessage ? [diffMessage] : [])).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
|
|
6977
|
-
|
|
7753
|
+
sourceType
|
|
6978
7754
|
);
|
|
6979
7755
|
if (!messages.length) return null;
|
|
6980
7756
|
const sourceFiles = [
|
|
@@ -6994,9 +7770,10 @@ function parseOpenCodeMessageOnlySession(storageRoot, sessionId) {
|
|
|
6994
7770
|
messages,
|
|
6995
7771
|
sourcePath: path.join(storageRoot, "message", sessionId),
|
|
6996
7772
|
sourceFiles,
|
|
6997
|
-
sourceType
|
|
6998
|
-
fingerprint: `${fingerprintPrefix(
|
|
6999
|
-
detailKey: "sessions"
|
|
7773
|
+
sourceType,
|
|
7774
|
+
fingerprint: `${fingerprintPrefix(sourceType)}:${structuredSessionFingerprint({ sourcePath: path.join(storageRoot, "message", sessionId), sourceFiles })}`,
|
|
7775
|
+
detailKey: "sessions",
|
|
7776
|
+
sessionSummary: { source: openCodeSourceKindForType(sourceType), recoveredFromMessages: true }
|
|
7000
7777
|
};
|
|
7001
7778
|
}
|
|
7002
7779
|
|
|
@@ -7365,7 +8142,12 @@ function dedupeOpenCodeSessions(sessions) {
|
|
|
7365
8142
|
}
|
|
7366
8143
|
|
|
7367
8144
|
function openCodeSourceRank(sourceType) {
|
|
8145
|
+
if (sourceType === "opencode-cli-sqlite-history") return 3;
|
|
8146
|
+
if (sourceType === "opencode-web-sqlite-history") return 3;
|
|
8147
|
+
if (sourceType === "opencode-desktop-sqlite-history") return 3;
|
|
7368
8148
|
if (sourceType === "opencode-sqlite-history") return 3;
|
|
8149
|
+
if (sourceType === "opencode-cli-history") return 2;
|
|
8150
|
+
if (sourceType === "opencode-desktop-history") return 2;
|
|
7369
8151
|
if (sourceType === "opencode-history") return 2;
|
|
7370
8152
|
return 1;
|
|
7371
8153
|
}
|