agentel 0.2.5 → 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/src/importers.js CHANGED
@@ -19,6 +19,7 @@ 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";
@@ -46,6 +47,7 @@ function importProviderHelpers() {
46
47
  importCursorProvider,
47
48
  importJsonlProvider,
48
49
  importStructuredProvider,
50
+ manualImportInstructionResult,
49
51
  readAntigravitySessions,
50
52
  readAiderSessions,
51
53
  readClineSessions,
@@ -157,7 +159,7 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
157
159
  sourceFiles: [item.file, sessionMetadata?.sourcePath || "", ...auxiliaryFiles].filter(Boolean),
158
160
  sourceType,
159
161
  title: jsonlSessionTitleForImport(parsed, sessionMetadata),
160
- sessionSummary: claudeCodeSidecarSessionSummary(sessionMetadata)
162
+ sessionSummary: mergeSessionSummaries(claudeCodeSidecarSessionSummary(sessionMetadata), parsed.sessionSummary)
161
163
  },
162
164
  env
163
165
  );
@@ -179,6 +181,12 @@ function jsonlProviderSourceType(provider) {
179
181
  return "cli-history";
180
182
  }
181
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
+
182
190
  function importClaudeDesktopProvider(provider, since, options = {}, env = process.env) {
183
191
  const state = loadImportState(env);
184
192
  const archived = archivedSessionKeys(env);
@@ -257,10 +265,12 @@ function matchesImportedSessionRepo(session, repo, wantedRepos) {
257
265
 
258
266
  function importCodexProvider(provider, since, options = {}, env = process.env) {
259
267
  const threads = readCodexSessionEntries(env).filter((thread) => {
260
- if (options.codexSource && thread.source !== options.codexSource) return false;
261
- if (!["cli", "vscode"].includes(thread.source)) return false;
262
- return true;
268
+ if (options.codexSource) return thread.source === options.codexSource;
269
+ return ["cli", "vscode"].includes(thread.source);
263
270
  });
271
+ if (!threads.length && options.codexSource && options.codexSource !== "cli") {
272
+ return { provider, discovered: 0, candidates: 0, imported: 0, skipped: 0, errors: [], details: {} };
273
+ }
264
274
  if (!threads.length) return importJsonlProvider(provider, codexRoots(env), since, options, env);
265
275
 
266
276
  const state = loadImportState(env);
@@ -288,7 +298,7 @@ function importCodexProvider(provider, since, options = {}, env = process.env) {
288
298
  reportProgress(options, summary, index + 1, thread.rolloutPath);
289
299
  continue;
290
300
  }
291
- const sourceType = thread.source === "vscode" ? "codex-desktop-history" : "codex-cli-history";
301
+ const sourceType = codexThreadSourceType(thread);
292
302
  const fingerprint = `${fingerprintPrefix(sourceType)}:${fileFingerprint(thread.rolloutPath, stat)}:${codexSupplementFingerprint(thread)}`;
293
303
  if (alreadyImported(state, thread.id, fingerprint, archived, provider)) {
294
304
  summary.skipped++;
@@ -579,10 +589,12 @@ function structuredSessionUsesSharedRawFiles(provider, sourceType) {
579
589
 
580
590
  function parseAgentJsonl(file, provider) {
581
591
  const text = readTextMaybeZstd(file);
592
+ const events = [];
582
593
  const messages = [];
583
594
  let cwd = "";
584
595
  let sessionId = "";
585
596
  let title = "";
597
+ let titleSource = "";
586
598
  const context = { model: "", tokenUsage: { input: 0, output: 0 } };
587
599
  for (const line of text.split(/\r?\n/)) {
588
600
  if (!line.trim()) continue;
@@ -592,6 +604,7 @@ function parseAgentJsonl(file, provider) {
592
604
  } catch {
593
605
  continue;
594
606
  }
607
+ events.push(event);
595
608
  cwd ||= firstString(event.cwd, event.working_directory, event.workingDirectory, event.context?.cwd, event.payload?.cwd);
596
609
  sessionId ||= firstString(
597
610
  event.session_id,
@@ -603,7 +616,20 @@ function parseAgentJsonl(file, provider) {
603
616
  event.payload?.session_id,
604
617
  event.sessionId
605
618
  );
606
- title ||= firstString(event.title, event.conversation_title, event.payload?.title);
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) {
607
633
  updateCodexParseContext(event, provider, context);
608
634
  updateClaudeParseContext(event, provider, context);
609
635
  if (applyCodexTokenCount(event, provider, messages, context)) continue;
@@ -617,13 +643,292 @@ function parseAgentJsonl(file, provider) {
617
643
  cwd,
618
644
  sessionId,
619
645
  title: title || inferredTitle,
620
- titleSource: title ? "source" : inferredTitle ? "first-user-prompt" : "",
646
+ titleSource: titleSource || (inferredTitle ? "first-user-prompt" : ""),
647
+ sessionSummary: claudeJsonlSessionSummary(provider, context),
621
648
  messages: deduped,
622
649
  startedAt: deduped[0]?.timestamp || "",
623
650
  endedAt: deduped[deduped.length - 1]?.timestamp || ""
624
651
  };
625
652
  }
626
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
+
627
932
  function repoInfoForImport(provider, cwd, metadata = null) {
628
933
  const repoCwd = repoCwdForImport(provider, cwd, metadata);
629
934
  if (!repoCwd) return null;
@@ -665,7 +970,6 @@ function isClaudeJsonlProvider(provider) {
665
970
  }
666
971
 
667
972
  function jsonlSessionTitleForImport(parsed, metadata = null) {
668
- if (parsed?.titleSource === "source") return parsed.title || "";
669
973
  return firstString(metadata?.title, parsed?.title);
670
974
  }
671
975
 
@@ -701,6 +1005,23 @@ function claudeCodeSidecarSessionSummary(metadata = null) {
701
1005
  });
702
1006
  }
703
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
+
704
1025
  function firstExistingDirectory(...values) {
705
1026
  for (const value of values) {
706
1027
  if (typeof value !== "string" || !value.trim()) continue;
@@ -757,9 +1078,54 @@ function dedupeAdjacentMessages(messages) {
757
1078
  }
758
1079
  result.push(message);
759
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
+ }
760
1100
  return result;
761
1101
  }
762
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
+
763
1129
  function hasStructuredTranscriptMetadata(message) {
764
1130
  const metadata = message?.metadata || {};
765
1131
  return Boolean(
@@ -2487,6 +2853,7 @@ function discoverCliHistory(env = process.env, options = {}) {
2487
2853
  const codexThreads = readCodexSessionEntries(env);
2488
2854
  publish("codexCli", "Codex CLI", summarizeCodexThreads(codexThreads, "cli"));
2489
2855
  publish("codexDesktop", "Codex Desktop", summarizeCodexThreads(codexThreads, "vscode"));
2856
+ publish("codexSdk", "Codex SDK jobs", summarizeCodexThreads(codexThreads, "exec"));
2490
2857
  result.codex = summarizeCodexThreads(codexThreads);
2491
2858
 
2492
2859
  const claudeScan = scanClaudeProjectFiles({
@@ -2613,8 +2980,8 @@ function summarizeCodex(env = process.env, source) {
2613
2980
 
2614
2981
  function summarizeCodexThreads(allThreads, source) {
2615
2982
  const threads = allThreads.filter((thread) => {
2616
- if (!["cli", "vscode"].includes(thread.source)) return false;
2617
- return source ? thread.source === source : true;
2983
+ if (source) return thread.source === source;
2984
+ return ["cli", "vscode"].includes(thread.source);
2618
2985
  });
2619
2986
  if (threads.length) {
2620
2987
  const oldest = threads.length
@@ -2629,10 +2996,24 @@ function summarizeCodexThreads(allThreads, source) {
2629
2996
  ? "terminal `codex` sessions"
2630
2997
  : source === "vscode"
2631
2998
  ? "Codex desktop app sessions"
2999
+ : source === "exec"
3000
+ ? "Codex exec/SDK batch jobs; opt-in"
2632
3001
  : "includes Codex CLI and Codex Desktop top-level sessions"
2633
3002
  };
2634
3003
  }
2635
- return { sessions: 0, oldest: "", details: {}, note: source === "cli" ? "terminal `codex` sessions" : source === "vscode" ? "Codex desktop app sessions" : "" };
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
+ };
2636
3017
  }
2637
3018
 
2638
3019
  function summarizeClaude() {
@@ -2888,11 +3269,13 @@ function classifyClaudeFile(file) {
2888
3269
  }
2889
3270
  let inspected = 0;
2890
3271
  let sawSdkMessage = false;
3272
+ let sawRemoteControlTools = false;
2891
3273
  for (const line of lines) {
2892
3274
  if (!line.trim()) continue;
2893
3275
  inspected++;
2894
3276
  try {
2895
3277
  const event = JSON.parse(line);
3278
+ if (isClaudeRemoteControlToolDelta(event)) sawRemoteControlTools = true;
2896
3279
  if ((event.type === "user" || event.type === "assistant") && event.message) {
2897
3280
  if (event.entrypoint === "sdk-cli") sawSdkMessage = true;
2898
3281
  else return "conversation";
@@ -2900,9 +3283,14 @@ function classifyClaudeFile(file) {
2900
3283
  } catch {
2901
3284
  return "other";
2902
3285
  }
2903
- if (inspected >= 80) return sawSdkMessage ? "sdk-job" : "other";
3286
+ if (inspected >= 80) return sawSdkMessage ? (sawRemoteControlTools ? "conversation" : "sdk-job") : "other";
2904
3287
  }
2905
- 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));
2906
3294
  }
2907
3295
 
2908
3296
  function readInitialLines(file, maxLines, maxBytes = 1024 * 1024) {
@@ -3173,9 +3561,10 @@ function codexSupplementFingerprint(thread) {
3173
3561
  function summarizeCodexSources(threads) {
3174
3562
  const cli = threads.filter((thread) => thread.source === "cli").length;
3175
3563
  const desktop = threads.filter((thread) => thread.source === "vscode").length;
3564
+ const exec = threads.filter((thread) => thread.source === "exec").length;
3176
3565
  const summaries = threads.filter((thread) => thread.rawMemory || thread.rolloutSummary).length;
3177
3566
  const archived = threads.filter((thread) => thread.sourceDetail === "archived_sessions").length;
3178
- return { cli, desktop, ...(archived ? { archived } : {}), ...(summaries ? { summaries } : {}) };
3567
+ return { cli, desktop, ...(exec ? { exec } : {}), ...(archived ? { archived } : {}), ...(summaries ? { summaries } : {}) };
3179
3568
  }
3180
3569
 
3181
3570
  function readClaudeDesktopSessions(options = {}, env = process.env) {
@@ -10,6 +10,7 @@ const SOURCE_TYPE_ALIASES = {
10
10
  const PARSER_VERSION_SUFFIXES = {
11
11
  "codex-cli-history": 0,
12
12
  "codex-desktop-history": 0,
13
+ "codex-sdk-history": 0,
13
14
  "cli-history": 0,
14
15
  "claude-sdk-history": 0,
15
16
  "claude-code-desktop-metadata": 0,
package/src/search.js CHANGED
@@ -1103,6 +1103,7 @@ function normalizeProviderFilter(value) {
1103
1103
  codex: { provider: "codex" },
1104
1104
  codex_cli: { provider: "codex", sourceType: "codex-cli-history", sourceTypes: ["codex-cli-history", "cli-history"] },
1105
1105
  codex_desktop: { provider: "codex", sourceType: "codex-desktop-history", sourceTypes: ["codex-desktop-history"] },
1106
+ codex_sdk: { provider: "codex", sourceType: "codex-sdk-history", sourceTypes: ["codex-sdk-history"] },
1106
1107
  cursor: { provider: "cursor" },
1107
1108
  cline: { provider: "cline", sourceType: "cline-task-history", sourceTypes: ["cline-task-history"] },
1108
1109
  opencode: { provider: "opencode", sourceTypes: ["opencode-cli-history", "opencode-cli-sqlite-history", "opencode-desktop-history", "opencode-desktop-sqlite-history", "opencode-web-sqlite-history", "opencode-history", "opencode-sqlite-history"] },
package/src/sources.js CHANGED
@@ -6,6 +6,7 @@ const SOURCE_GROUPS = [
6
6
  sources: [
7
7
  { source: "codex-cli", provider: "codex", sourceType: "codex-cli-history", label: "Codex CLI" },
8
8
  { source: "codex-desktop", provider: "codex", sourceType: "codex-desktop-history", label: "Codex Desktop" },
9
+ { source: "codex-sdk", provider: "codex", sourceType: "codex-sdk-history", label: "Codex SDK jobs" },
9
10
  { source: "chatgpt", provider: "chatgpt", label: "ChatGPT" }
10
11
  ]
11
12
  },
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+
3
+ const WEB_EXPORT_INSTRUCTIONS = {
4
+ chatgpt: {
5
+ source: "chatgpt",
6
+ provider: "chatgpt",
7
+ label: "ChatGPT",
8
+ requestUrl: "https://privacy.openai.com/policies/en/",
9
+ helpUrl: "https://help.openai.com/en/articles/7260999-how-do-i-export-my-chatgpt-history-and-data",
10
+ fileDescription: "official ChatGPT data export ZIP or extracted folder",
11
+ importCommand: "agentlog import chatgpt <path-to-export.zip> --username <email> --scope local",
12
+ steps: [
13
+ "Open the OpenAI Privacy Portal, choose Make a Privacy Request, select I have a consumer ChatGPT account, then select Download my data.",
14
+ "Or in ChatGPT, open your profile menu, choose Settings, open Data Controls, then use Export Data.",
15
+ "Complete account verification and wait for the export email.",
16
+ "Download the ZIP before the email link expires, then pass the ZIP or extracted folder to agentlog."
17
+ ],
18
+ notes: [
19
+ "OpenAI says ChatGPT Business and Enterprise chat exports are not available through the ChatGPT account export flow.",
20
+ "OpenAI export emails can take time to arrive, and the download link expires after 24 hours."
21
+ ]
22
+ },
23
+ "claude-web": {
24
+ source: "claude-web",
25
+ provider: "claude_web",
26
+ label: "Claude.ai",
27
+ requestUrl: "https://claude.ai/settings/privacy",
28
+ helpUrl: "https://support.claude.com/en/articles/9450526-how-can-i-export-my-claude-data",
29
+ fileDescription: "official Claude.ai data export ZIP or extracted folder",
30
+ importCommand: "agentlog import claude-web <path-to-export.zip> --username <email-or-name> --scope local",
31
+ steps: [
32
+ "Open Claude on the web or Claude Desktop and go to Settings > Privacy.",
33
+ "Click Export data.",
34
+ "Wait for Claude to email the download link to the address on the account.",
35
+ "Download the ZIP before the email link expires, then pass the ZIP or extracted folder to agentlog."
36
+ ],
37
+ notes: [
38
+ "Claude's mobile apps cannot start a data export.",
39
+ "Claude requires you to be signed in when using the emailed download link, and the link expires after 24 hours."
40
+ ]
41
+ }
42
+ };
43
+
44
+ function canonicalWebExportInstructionSource(source) {
45
+ const value = String(source || "").trim().toLowerCase().replace(/[_\s]+/g, "-");
46
+ if (value === "chatgpt" || value === "openai") return "chatgpt";
47
+ if (value === "claude-ai" || value === "claude-web") return "claude-web";
48
+ return "";
49
+ }
50
+
51
+ function webExportInstructions(source) {
52
+ const key = canonicalWebExportInstructionSource(source);
53
+ return key ? WEB_EXPORT_INSTRUCTIONS[key] : null;
54
+ }
55
+
56
+ function manualImportInstructionResult(source) {
57
+ const instructions = webExportInstructions(source);
58
+ if (!instructions) throw new Error(`unknown web export instruction source: ${source}`);
59
+ return {
60
+ provider: instructions.provider,
61
+ source: instructions.source,
62
+ manual: true,
63
+ instructions,
64
+ discovered: 0,
65
+ candidates: 0,
66
+ imported: 0,
67
+ skipped: 0,
68
+ errors: []
69
+ };
70
+ }
71
+
72
+ module.exports = {
73
+ WEB_EXPORT_INSTRUCTIONS,
74
+ canonicalWebExportInstructionSource,
75
+ manualImportInstructionResult,
76
+ webExportInstructions
77
+ };