agentel 0.2.0 → 0.2.2

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
@@ -15,11 +15,14 @@ const { DISABLED_IMPORT_SOURCE_MESSAGES, IMPORT_SOURCE_ORDER } = require("./sour
15
15
  const { parseAiderHistoryFile } = require("./importers/aider");
16
16
  const { extractClaudeMessagesFromEvent, updateClaudeParseContext } = require("./importers/claude");
17
17
  const { readClineCheckpointDiffs } = require("./importers/cline");
18
- const { parseGeminiCliJson, parseGeminiCliJsonl } = require("./importers/gemini");
18
+ const { parseGeminiCliJsonSessions, parseGeminiCliJsonlSessions } = require("./importers/gemini");
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
22
 
23
+ const WEB_TOKEN_ESTIMATE_CHARS = 4;
24
+ const WEB_CHAT_TOKEN_ESTIMATION_METHOD = "web-message-parts-chars-v1";
25
+
23
26
  function importCliHistory(options = {}, env = process.env) {
24
27
  const source = options.source || "all";
25
28
  const since = parseSince(options.since || "30d");
@@ -451,6 +454,7 @@ function importCursorProvider(provider, since, options = {}, env = process.env)
451
454
  sourceFiles: session.sourceFiles || [session.sourcePath, session.dbPath].filter(Boolean),
452
455
  sourceType,
453
456
  title: session.title,
457
+ composerId: cursorSessionComposerId(session) || undefined,
454
458
  sharedRawFiles: cursorSessionUsesSharedRawFiles(sourceType),
455
459
  replaceSourcePathCopies: sourceType === "cursor-agent-transcripts"
456
460
  },
@@ -534,6 +538,10 @@ function importStructuredProvider(provider, sessions, since, options = {}, env =
534
538
  sourceFiles: session.sourceFiles || [session.sourcePath].filter(Boolean),
535
539
  sourceType,
536
540
  title: session.title,
541
+ sessionSummary: session.sessionSummary,
542
+ providerConversationId: session.providerConversationId,
543
+ rawReferences: session.rawReferences,
544
+ sharedRawFiles: structuredSessionUsesSharedRawFiles(provider, sourceType),
537
545
  replaceSourcePathCopies: structuredSessionReplaceSourcePathCopies(provider, sourceType)
538
546
  },
539
547
  env
@@ -554,6 +562,10 @@ function structuredSessionReplaceSourcePathCopies(provider, sourceType) {
554
562
  return undefined;
555
563
  }
556
564
 
565
+ function structuredSessionUsesSharedRawFiles(provider, sourceType) {
566
+ return provider === "opencode" && sourceType === "opencode-sqlite-history";
567
+ }
568
+
557
569
  function parseAgentJsonl(file, provider) {
558
570
  const text = readTextMaybeZstd(file);
559
571
  const messages = [];
@@ -1342,6 +1354,7 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
1342
1354
  sourceType,
1343
1355
  parserVersion: parserVersionForSource(sourceType),
1344
1356
  title: conversation.title,
1357
+ sessionSummary: conversation.sessionSummary,
1345
1358
  providerConversationId: conversation.id,
1346
1359
  chatAccountId: account.accountId,
1347
1360
  chatUsername: account.username,
@@ -1351,6 +1364,7 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
1351
1364
  chatDisplayPath: displayPath,
1352
1365
  conversationKind: conversation.kind || "conversation",
1353
1366
  pinned: Boolean(conversation.pinned),
1367
+ timeStatus: conversation.timeStatus || "",
1354
1368
  replaceSourcePathCopies: false
1355
1369
  },
1356
1370
  env
@@ -1372,6 +1386,17 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
1372
1386
  return summary;
1373
1387
  }
1374
1388
 
1389
+ function importWindsurfTrajectoryExport(target, options = {}, env = process.env) {
1390
+ const since = parseSince(options.since || "all");
1391
+ return importStructuredProvider(
1392
+ "windsurf",
1393
+ readWindsurfTrajectoryExport(target, options),
1394
+ since,
1395
+ { ...options, repos: options.repos || [] },
1396
+ env
1397
+ );
1398
+ }
1399
+
1375
1400
  function readExportJson(file) {
1376
1401
  const bundle = readExportBundle(file);
1377
1402
  const conversations = bundle.entries.find((entry) => /(^|\/)conversations\.json$/i.test(entry.name));
@@ -1458,11 +1483,13 @@ function readExportFile(file) {
1458
1483
 
1459
1484
  function exportEntry(name, text, sourcePath) {
1460
1485
  const data = parseExportText(text, name);
1486
+ const stat = safeStat(String(sourcePath || "").split("#")[0]);
1461
1487
  return {
1462
1488
  name,
1463
1489
  sourcePath,
1464
1490
  data,
1465
1491
  size: Buffer.byteLength(text),
1492
+ mtime: stat?.mtimeMs ? new Date(stat.mtimeMs).toISOString() : "",
1466
1493
  sha256: crypto.createHash("sha256").update(text).digest("hex")
1467
1494
  };
1468
1495
  }
@@ -1571,16 +1598,16 @@ function chatgptRawConversations(data) {
1571
1598
  function chatgptMessages(conversation) {
1572
1599
  if (conversation.mapping && typeof conversation.mapping === "object") {
1573
1600
  const nodes = chatgptMainPathNodes(conversation);
1574
- return nodes.map((node) => node && node.message).filter(Boolean).map((message) => ({
1575
- role: normalizeEventRole(message.author?.role) || "unknown",
1576
- content: extractChatGptContent(message.content),
1577
- timestamp: toIso(message.create_time || message.update_time),
1578
- metadata: {
1579
- source: "chatgpt-export",
1580
- messageId: message.id || undefined,
1581
- model: firstString(message.metadata?.model_slug, message.metadata?.model, message.metadata?.default_model_slug) || undefined
1582
- }
1583
- }));
1601
+ return nodes.map((node) => node && node.message).filter(Boolean).map((message) => {
1602
+ const role = normalizeEventRole(message.author?.role) || "unknown";
1603
+ const content = extractChatGptContent(message.content);
1604
+ return {
1605
+ role,
1606
+ content,
1607
+ timestamp: toIso(message.create_time || message.update_time),
1608
+ metadata: chatgptMessageMetadata(message, role, content)
1609
+ };
1610
+ });
1584
1611
  }
1585
1612
  return genericConversationMessages(conversation, "chatgpt-export");
1586
1613
  }
@@ -1610,6 +1637,221 @@ function extractChatGptContent(content) {
1610
1637
  return extractText(content);
1611
1638
  }
1612
1639
 
1640
+ function chatgptMessageMetadata(message, role, content) {
1641
+ return webWithUsage({
1642
+ source: "chatgpt-export",
1643
+ messageId: message.id || undefined,
1644
+ model: firstString(message.metadata?.model_slug, message.metadata?.model, message.metadata?.default_model_slug) || undefined
1645
+ }, webMessageUsage(message, role, { inputText: content, outputText: content }));
1646
+ }
1647
+
1648
+ function webWithUsage(metadata, usage) {
1649
+ if (!usage) return metadata;
1650
+ return { ...metadata, usage };
1651
+ }
1652
+
1653
+ function webMessageUsage(message, role, parts = {}) {
1654
+ const providerUsage = webProviderUsage(...webUsageCandidates(message));
1655
+ if (providerUsage) return webUsageWithRoleDirection(providerUsage, role);
1656
+ const normalizedRole = String(role || "").toLowerCase();
1657
+ return webEstimatedMessageUsage({
1658
+ inputText: normalizedRole === "assistant" ? "" : parts.inputText,
1659
+ outputText: normalizedRole === "assistant" ? parts.outputText : "",
1660
+ reasoningText: normalizedRole === "assistant" ? parts.reasoningText : ""
1661
+ });
1662
+ }
1663
+
1664
+ function webUsageWithRoleDirection(usage, role) {
1665
+ if (!usage || usage.inputTokens || usage.outputTokens || usage.totalInputTokens || usage.totalOutputTokens) return usage;
1666
+ const totalTokens = webPositiveTokenNumber(usage.totalTokens);
1667
+ if (!totalTokens) return usage;
1668
+ const directionalTokens = Math.max(
1669
+ 0,
1670
+ totalTokens -
1671
+ webPositiveTokenNumber(usage.cacheInputTokens) -
1672
+ webPositiveTokenNumber(usage.reasoningOutputTokens) -
1673
+ webPositiveTokenNumber(usage.toolUsePromptTokens)
1674
+ );
1675
+ if (!directionalTokens) return usage;
1676
+ if (String(role || "").toLowerCase() === "assistant") return { ...usage, outputTokens: directionalTokens };
1677
+ return { ...usage, inputTokens: directionalTokens };
1678
+ }
1679
+
1680
+ function webUsageCandidates(message) {
1681
+ if (!message || typeof message !== "object") return [];
1682
+ const metadata = message.metadata && typeof message.metadata === "object" ? message.metadata : {};
1683
+ return [
1684
+ message.usage,
1685
+ message.token_usage,
1686
+ message.tokenUsage,
1687
+ message.token_counts,
1688
+ message.tokenCounts,
1689
+ message.token_count,
1690
+ message.tokenCount,
1691
+ message.tokens,
1692
+ metadata.usage,
1693
+ metadata.token_usage,
1694
+ metadata.tokenUsage,
1695
+ metadata.token_counts,
1696
+ metadata.tokenCounts,
1697
+ metadata.token_count,
1698
+ metadata.tokenCount,
1699
+ metadata.tokens,
1700
+ message
1701
+ ];
1702
+ }
1703
+
1704
+ function webProviderUsage(...candidates) {
1705
+ for (const candidate of candidates) {
1706
+ const usage = normalizeWebProviderUsage(candidate);
1707
+ if (usage) return usage;
1708
+ }
1709
+ return null;
1710
+ }
1711
+
1712
+ function normalizeWebProviderUsage(usage) {
1713
+ if (usage == null || usage === "") return null;
1714
+ if (typeof usage !== "object") {
1715
+ const totalTokens = webPositiveTokenNumber(usage);
1716
+ return totalTokens ? { totalTokens } : null;
1717
+ }
1718
+ const inputTokens = webPositiveTokenNumber(numericValue(
1719
+ usage.inputTokens,
1720
+ usage.input_tokens,
1721
+ usage.promptTokens,
1722
+ usage.prompt_tokens,
1723
+ usage.promptTokenCount,
1724
+ usage.prompt_token_count,
1725
+ usage.inputTokenCount,
1726
+ usage.input_token_count,
1727
+ usage.input,
1728
+ usage.prompt
1729
+ ));
1730
+ const outputTokens = webPositiveTokenNumber(numericValue(
1731
+ usage.outputTokens,
1732
+ usage.output_tokens,
1733
+ usage.completionTokens,
1734
+ usage.completion_tokens,
1735
+ usage.completionTokenCount,
1736
+ usage.completion_token_count,
1737
+ usage.outputTokenCount,
1738
+ usage.output_token_count,
1739
+ usage.output,
1740
+ usage.completion
1741
+ ));
1742
+ const totalInputTokens = webPositiveTokenNumber(numericValue(
1743
+ usage.totalInputTokens,
1744
+ usage.total_input_tokens,
1745
+ usage.totalPromptTokens,
1746
+ usage.total_prompt_tokens,
1747
+ usage.totalPromptTokenCount,
1748
+ usage.total_prompt_token_count
1749
+ ));
1750
+ const totalOutputTokens = webPositiveTokenNumber(numericValue(
1751
+ usage.totalOutputTokens,
1752
+ usage.total_output_tokens,
1753
+ usage.totalCompletionTokens,
1754
+ usage.total_completion_tokens,
1755
+ usage.totalCompletionTokenCount,
1756
+ usage.total_completion_token_count
1757
+ ));
1758
+ const cacheInputTokens = webSumTokenNumbers(
1759
+ usage.cacheInputTokens,
1760
+ usage.cache_input_tokens,
1761
+ usage.cacheCreationInputTokens,
1762
+ usage.cache_creation_input_tokens,
1763
+ usage.cacheReadInputTokens,
1764
+ usage.cache_read_input_tokens,
1765
+ usage.cacheReadTokens,
1766
+ usage.cache_read_tokens,
1767
+ usage.cachedContentTokenCount,
1768
+ usage.cached_content_token_count,
1769
+ usage.cachedTokens,
1770
+ usage.cached_tokens,
1771
+ usage.prompt_tokens_details?.cached_tokens,
1772
+ usage.promptTokensDetails?.cachedTokens
1773
+ );
1774
+ const reasoningOutputTokens = webSumTokenNumbers(
1775
+ usage.reasoningOutputTokens,
1776
+ usage.reasoning_output_tokens,
1777
+ usage.reasoningTokens,
1778
+ usage.reasoning_tokens,
1779
+ usage.reasoningTokenCount,
1780
+ usage.reasoning_token_count,
1781
+ usage.thoughtsTokens,
1782
+ usage.thoughts_tokens,
1783
+ usage.thoughtsTokenCount,
1784
+ usage.thoughts_token_count,
1785
+ usage.completion_tokens_details?.reasoning_tokens,
1786
+ usage.completionTokensDetails?.reasoningTokens,
1787
+ usage.output_tokens_details?.reasoning_tokens,
1788
+ usage.outputTokensDetails?.reasoningTokens
1789
+ );
1790
+ const toolUsePromptTokens = webSumTokenNumbers(
1791
+ usage.toolUsePromptTokens,
1792
+ usage.tool_use_prompt_tokens,
1793
+ usage.toolUsePromptTokenCount,
1794
+ usage.tool_use_prompt_token_count
1795
+ );
1796
+ const explicitTotalTokens = webPositiveTokenNumber(numericValue(
1797
+ usage.totalTokens,
1798
+ usage.total_tokens,
1799
+ usage.totalTokenCount,
1800
+ usage.total_token_count,
1801
+ usage.tokenCount,
1802
+ usage.token_count,
1803
+ usage.tokens,
1804
+ usage.total
1805
+ ));
1806
+ const totalTokens = explicitTotalTokens ||
1807
+ inputTokens + outputTokens + cacheInputTokens + reasoningOutputTokens + toolUsePromptTokens ||
1808
+ totalInputTokens + totalOutputTokens;
1809
+ if (!totalTokens) return null;
1810
+ const result = { totalTokens };
1811
+ if (inputTokens) result.inputTokens = inputTokens;
1812
+ if (outputTokens) result.outputTokens = outputTokens;
1813
+ if (totalInputTokens) result.totalInputTokens = totalInputTokens;
1814
+ if (totalOutputTokens) result.totalOutputTokens = totalOutputTokens;
1815
+ if (cacheInputTokens) result.cacheInputTokens = cacheInputTokens;
1816
+ if (reasoningOutputTokens) result.reasoningOutputTokens = reasoningOutputTokens;
1817
+ if (toolUsePromptTokens) result.toolUsePromptTokens = toolUsePromptTokens;
1818
+ return result;
1819
+ }
1820
+
1821
+ function webEstimatedMessageUsage(parts = {}) {
1822
+ const inputTokens = webEstimatedTokenCount(parts.inputText);
1823
+ const outputTokens = webEstimatedTokenCount(parts.outputText);
1824
+ const reasoningOutputTokens = webEstimatedTokenCount(parts.reasoningText);
1825
+ const totalTokens = inputTokens + outputTokens + reasoningOutputTokens;
1826
+ if (!totalTokens) return null;
1827
+ const usage = {
1828
+ totalTokens,
1829
+ estimated: true,
1830
+ estimationMethod: WEB_CHAT_TOKEN_ESTIMATION_METHOD,
1831
+ charsPerToken: WEB_TOKEN_ESTIMATE_CHARS
1832
+ };
1833
+ if (inputTokens) usage.inputTokens = inputTokens;
1834
+ if (outputTokens) usage.outputTokens = outputTokens;
1835
+ if (reasoningOutputTokens) usage.reasoningOutputTokens = reasoningOutputTokens;
1836
+ return usage;
1837
+ }
1838
+
1839
+ function webEstimatedTokenCount(value) {
1840
+ const text = String(value || "");
1841
+ if (!text.trim()) return 0;
1842
+ return Math.max(1, Math.ceil(text.length / WEB_TOKEN_ESTIMATE_CHARS));
1843
+ }
1844
+
1845
+ function webPositiveTokenNumber(value) {
1846
+ const number = Number(value);
1847
+ if (!Number.isFinite(number) || number <= 0) return 0;
1848
+ return Math.round(number);
1849
+ }
1850
+
1851
+ function webSumTokenNumbers(...values) {
1852
+ return values.reduce((sum, value) => sum + webPositiveTokenNumber(value), 0);
1853
+ }
1854
+
1613
1855
  function normalizeClaudeWebExport(source) {
1614
1856
  const projectMap = claudeProjectMap(source);
1615
1857
  const conversations = [];
@@ -1643,7 +1885,11 @@ function claudeConversationsFromEntry(entry, projectMap = new Map()) {
1643
1885
  return values.map((conversation, index) => {
1644
1886
  const id = firstString(conversation.uuid, conversation.id, conversation.conversation_uuid, conversation.conversation_id) || `claude-${hashId(`${entry.name}:${index}`)}`;
1645
1887
  const title = firstString(conversation.name, conversation.title, conversation.summary) || "Claude conversation";
1646
- const messages = claudeMessages(conversation).filter((message) => message.content.trim());
1888
+ const sessionSummary = claudeConversationSessionSummary(conversation);
1889
+ const messages = [
1890
+ ...claudeConversationSummaryMessages(conversation),
1891
+ ...claudeMessages(conversation)
1892
+ ].filter((message) => message.content.trim());
1647
1893
  const sorted = sortConversationMessages(messages);
1648
1894
  return {
1649
1895
  id,
@@ -1655,7 +1901,8 @@ function claudeConversationsFromEntry(entry, projectMap = new Map()) {
1655
1901
  projectPath: claudeConversationProjectPath(conversation, projectPath, projectMap),
1656
1902
  entryPath: entry.name,
1657
1903
  sourceType: "claude-web-export",
1658
- kind: "conversation"
1904
+ kind: "conversation",
1905
+ sessionSummary
1659
1906
  };
1660
1907
  });
1661
1908
  }
@@ -1701,16 +1948,158 @@ function claudeProjectPath(entry) {
1701
1948
 
1702
1949
  function claudeMessages(conversation) {
1703
1950
  const messages = conversation.chat_messages || conversation.messages || conversation.children || [];
1704
- return messages.map((message) => ({
1705
- role: normalizeEventRole(message.sender || message.role || message.author?.role || message.author) || "unknown",
1706
- content: extractText(message.text || message.content || message.message),
1707
- timestamp: toIso(message.created_at || message.createdAt || message.updated_at || message.updatedAt || message.timestamp),
1951
+ return messages.flatMap(claudeMessageRows);
1952
+ }
1953
+
1954
+ function claudeConversationSessionSummary(conversation) {
1955
+ const summary = firstString(conversation.summary, conversation.conversation_summary, conversation.conversationSummary);
1956
+ if (!summary) return undefined;
1957
+ return {
1958
+ source: "claude-web-export",
1959
+ summary,
1960
+ updatedAt: toIso(conversation.updated_at || conversation.updatedAt) || undefined
1961
+ };
1962
+ }
1963
+
1964
+ function claudeConversationSummaryMessages(conversation) {
1965
+ const summary = firstString(conversation.summary, conversation.conversation_summary, conversation.conversationSummary);
1966
+ if (!summary) return [];
1967
+ return [{
1968
+ role: "assistant",
1969
+ content: claudeSupplementContent("Claude conversation summary", summary),
1970
+ timestamp: toIso(conversation.created_at || conversation.createdAt || conversation.updated_at || conversation.updatedAt) || "",
1708
1971
  metadata: {
1709
1972
  source: "claude-web-export",
1710
- messageId: firstString(message.uuid, message.id) || undefined,
1711
- model: firstString(message.model, message.model_name, message.modelName) || undefined
1973
+ eventType: "claude-conversation-summary",
1974
+ supplementary: true,
1975
+ summaryKind: "conversation_summary"
1712
1976
  }
1713
- }));
1977
+ }];
1978
+ }
1979
+
1980
+ function claudeMessageRows(message) {
1981
+ const role = normalizeEventRole(message.sender || message.role || message.author?.role || message.author) || "unknown";
1982
+ const fallbackTimestamp = toIso(message.created_at || message.createdAt || message.updated_at || message.updatedAt || message.timestamp);
1983
+ const metadata = claudeWebMessageMetadata(message);
1984
+ if (role === "assistant") return claudeAssistantMessageRows(message, fallbackTimestamp, metadata);
1985
+ const content = claudeVisibleMessageText(message);
1986
+ return [{
1987
+ role,
1988
+ content,
1989
+ timestamp: fallbackTimestamp,
1990
+ metadata: webWithUsage(metadata, webMessageUsage(message, role, { inputText: content }))
1991
+ }];
1992
+ }
1993
+
1994
+ function claudeAssistantMessageRows(message, fallbackTimestamp, metadata) {
1995
+ const parts = claudeContentParts(message.content);
1996
+ if (!parts.length) {
1997
+ const content = extractText(message.text || message.content || message.message);
1998
+ return [{
1999
+ role: "assistant",
2000
+ content,
2001
+ timestamp: fallbackTimestamp,
2002
+ metadata: webWithUsage(metadata, webMessageUsage(message, "assistant", { outputText: content }))
2003
+ }];
2004
+ }
2005
+ const thinkingParts = claudeThinkingParts(parts);
2006
+ const visibleParts = claudeVisibleParts(parts);
2007
+ const thinking = claudeThinkingText(thinkingParts);
2008
+ const visible = claudeVisibleText(visibleParts);
2009
+ const usage = webMessageUsage(message, "assistant", { outputText: visible, reasoningText: thinking });
2010
+ const rows = [];
2011
+ if (thinking) {
2012
+ const thinkingSummaries = claudeThinkingSummaries(thinkingParts);
2013
+ rows.push({
2014
+ role: "assistant",
2015
+ content: claudeSupplementContent("Claude thinking", thinking),
2016
+ timestamp: claudePartsTimestamp(thinkingParts, fallbackTimestamp),
2017
+ metadata: webWithUsage({
2018
+ ...metadata,
2019
+ eventType: "claude-thinking",
2020
+ supplementary: true,
2021
+ summaryKind: "thinking",
2022
+ thinkingSummaries: thinkingSummaries.length ? thinkingSummaries : undefined
2023
+ }, visible ? null : usage)
2024
+ });
2025
+ }
2026
+ if (visible) {
2027
+ rows.push({
2028
+ role: "assistant",
2029
+ content: visible,
2030
+ timestamp: claudePartsTimestamp(visibleParts, fallbackTimestamp),
2031
+ metadata: webWithUsage(metadata, usage)
2032
+ });
2033
+ }
2034
+ return rows;
2035
+ }
2036
+
2037
+ function claudeWebMessageMetadata(message) {
2038
+ return {
2039
+ source: "claude-web-export",
2040
+ messageId: firstString(message.uuid, message.id) || undefined,
2041
+ model: firstString(message.model, message.model_name, message.modelName) || undefined
2042
+ };
2043
+ }
2044
+
2045
+ function claudeContentParts(content) {
2046
+ return Array.isArray(content) ? content.filter((part) => part && typeof part === "object") : [];
2047
+ }
2048
+
2049
+ function claudeVisibleMessageText(message) {
2050
+ const parts = claudeContentParts(message.content);
2051
+ if (parts.length) return claudeVisibleText(claudeVisibleParts(parts));
2052
+ return extractText(message.text || message.content || message.message);
2053
+ }
2054
+
2055
+ function claudeVisibleParts(parts) {
2056
+ return parts.filter((part) => {
2057
+ const type = String(part.type || part.kind || "").toLowerCase();
2058
+ return !/(tool_use|tool_result|function_call|function_result|thinking|redacted_thinking)/.test(type);
2059
+ });
2060
+ }
2061
+
2062
+ function claudeThinkingParts(parts) {
2063
+ return parts.filter((part) => /thinking/.test(String(part.type || part.kind || "").toLowerCase()));
2064
+ }
2065
+
2066
+ function claudeVisibleText(parts) {
2067
+ return parts.map((part) => extractText(part)).filter(Boolean).join("\n").trim();
2068
+ }
2069
+
2070
+ function claudeThinkingText(parts) {
2071
+ return parts
2072
+ .map((part) => firstString(part.thinking, part.text, part.content, part.summary, extractText(part.content)))
2073
+ .filter(Boolean)
2074
+ .join("\n\n")
2075
+ .trim();
2076
+ }
2077
+
2078
+ function claudeThinkingSummaries(parts) {
2079
+ return parts.flatMap((part) => Array.isArray(part.summaries)
2080
+ ? part.summaries.map((summary) => firstString(summary.summary, summary.text, summary.content))
2081
+ : []
2082
+ ).filter(Boolean);
2083
+ }
2084
+
2085
+ function claudePartsTimestamp(parts, fallbackTimestamp) {
2086
+ const timestamps = parts
2087
+ .map((part) => toIso(
2088
+ part.stop_timestamp ||
2089
+ part.stopTimestamp ||
2090
+ part.end_timestamp ||
2091
+ part.endTimestamp ||
2092
+ part.start_timestamp ||
2093
+ part.startTimestamp ||
2094
+ part.created_at ||
2095
+ part.createdAt
2096
+ ))
2097
+ .filter(Boolean);
2098
+ return timestamps[timestamps.length - 1] || fallbackTimestamp || "";
2099
+ }
2100
+
2101
+ function claudeSupplementContent(title, content) {
2102
+ return `### ${title}\n\n${String(content || "").trim()}`;
1714
2103
  }
1715
2104
 
1716
2105
  function claudeMemoryConversations(entry, projectMap = new Map()) {
@@ -1718,7 +2107,7 @@ function claudeMemoryConversations(entry, projectMap = new Map()) {
1718
2107
  const conversations = [];
1719
2108
  for (const row of rows) {
1720
2109
  const rootContent = renderClaudeConversationMemory(row);
1721
- if (rootContent.trim()) conversations.push(claudeMemoryConversation("memory", "Claude Memory", rootContent, "memory", entry.name));
2110
+ if (rootContent.trim()) conversations.push(claudeMemoryConversation("memory", "Claude Memory", rootContent, "memory", entry));
1722
2111
  const projectMemories = row && typeof row === "object" && row.project_memories && typeof row.project_memories === "object"
1723
2112
  ? row.project_memories
1724
2113
  : {};
@@ -1727,14 +2116,14 @@ function claudeMemoryConversations(entry, projectMap = new Map()) {
1727
2116
  if (!content.trim()) continue;
1728
2117
  const project = projectMap.get(projectId);
1729
2118
  const projectTitle = project?.title || project?.path || sanitizeProjectPath(projectId) || projectId;
1730
- conversations.push(claudeMemoryConversation(`memory-${projectId}`, `Claude Project Memory: ${projectTitle}`, content, "memory", entry.name));
2119
+ conversations.push(claudeMemoryConversation(`memory-${projectId}`, `Claude Project Memory: ${projectTitle}`, content, "memory", entry));
1731
2120
  }
1732
2121
  }
1733
2122
  return conversations;
1734
2123
  }
1735
2124
 
1736
- function claudeMemoryConversation(id, title, content, projectPath, entryPath) {
1737
- const timestamp = new Date().toISOString();
2125
+ function claudeMemoryConversation(id, title, content, projectPath, entry) {
2126
+ const timestamp = claudeMemorySyntheticTimestamp(entry);
1738
2127
  return {
1739
2128
  id,
1740
2129
  title,
@@ -1743,13 +2132,23 @@ function claudeMemoryConversation(id, title, content, projectPath, entryPath) {
1743
2132
  endedAt: timestamp,
1744
2133
  updatedAt: timestamp,
1745
2134
  projectPath: projectPath || "",
1746
- entryPath,
2135
+ entryPath: entry?.name || "",
1747
2136
  sourceType: "claude-web-memory",
1748
2137
  kind: "memory",
1749
- pinned: true
2138
+ pinned: true,
2139
+ timeStatus: "recovered-time-unknown",
2140
+ sessionSummary: {
2141
+ source: "claude-web-memory",
2142
+ timeStatus: "recovered-time-unknown",
2143
+ note: "Claude memory exports do not include reliable memory creation or update timestamps."
2144
+ }
1750
2145
  };
1751
2146
  }
1752
2147
 
2148
+ function claudeMemorySyntheticTimestamp(entry) {
2149
+ return toIso(entry?.mtime) || "1970-01-01T00:00:00.000Z";
2150
+ }
2151
+
1753
2152
  function renderClaudeConversationMemory(data) {
1754
2153
  if (data && typeof data === "object" && typeof data.conversations_memory === "string") {
1755
2154
  return data.conversations_memory;
@@ -1781,12 +2180,16 @@ function renderClaudeMemoryItem(item) {
1781
2180
 
1782
2181
  function genericConversationMessages(conversation, source) {
1783
2182
  const messages = conversation.messages || conversation.chat_messages || [];
1784
- return messages.map((message) => ({
1785
- role: normalizeEventRole(message.role || message.sender || message.author?.role) || "unknown",
1786
- content: extractText(message.content || message.text || message.message),
1787
- timestamp: toIso(message.created_at || message.create_time || message.timestamp),
1788
- metadata: { source }
1789
- }));
2183
+ return messages.map((message) => {
2184
+ const role = normalizeEventRole(message.role || message.sender || message.author?.role) || "unknown";
2185
+ const content = extractText(message.content || message.text || message.message);
2186
+ return {
2187
+ role,
2188
+ content,
2189
+ timestamp: toIso(message.created_at || message.create_time || message.timestamp),
2190
+ metadata: webWithUsage({ source }, webMessageUsage(message, role, { inputText: content, outputText: content }))
2191
+ };
2192
+ });
1790
2193
  }
1791
2194
 
1792
2195
  function sortConversationMessages(messages) {
@@ -1805,7 +2208,8 @@ function webConversationSessionId(provider, accountId, conversationId) {
1805
2208
 
1806
2209
  function webConversationFingerprint(sourceType, accountId, conversation) {
1807
2210
  const body = conversation.messages.map((message) => `${message.role}:${message.timestamp}:${message.content}`).join("\n");
1808
- return `${fingerprintPrefix(sourceType)}:${accountId}:${conversation.projectPath || ""}:${conversation.updatedAt || conversation.endedAt || ""}:${conversation.messages.length}:${hashId(body)}`;
2211
+ const summary = JSON.stringify(conversation.sessionSummary || {});
2212
+ return `${fingerprintPrefix(sourceType)}:${accountId}:${conversation.projectPath || ""}:${conversation.updatedAt || conversation.endedAt || ""}:${conversation.messages.length}:${hashId(`${body}\n${summary}`)}`;
1809
2213
  }
1810
2214
 
1811
2215
  function webConversationScope(provider, accountId, projectPath = "") {
@@ -2005,8 +2409,8 @@ function discoverCliHistory(env = process.env, options = {}) {
2005
2409
  summarizeStructuredSessions(
2006
2410
  readAntigravitySessions({
2007
2411
  onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "Antigravity" })
2008
- }),
2009
- "Antigravity task/plan/walkthrough artifacts; binary protobuf transcripts are counted separately"
2412
+ }, env),
2413
+ "Antigravity task/plan/walkthrough artifacts plus trajectory summaries; binary protobuf transcripts are counted separately"
2010
2414
  )
2011
2415
  );
2012
2416
 
@@ -2044,7 +2448,7 @@ function discoverCliHistory(env = process.env, options = {}) {
2044
2448
  readOpenCodeSessions(env, {
2045
2449
  onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "OpenCode" })
2046
2450
  }),
2047
- "OpenCode JSON session/message/part storage"
2451
+ "OpenCode SQLite database and JSON session/message/part storage"
2048
2452
  )
2049
2453
  );
2050
2454
 
@@ -2233,6 +2637,8 @@ function summarizeStructuredSessionDetails(sessions) {
2233
2637
  if (session.detailKey) acc[session.detailKey] = (acc[session.detailKey] || 0) + 1;
2234
2638
  if (session.artifactCount) acc.artifacts = (acc.artifacts || 0) + session.artifactCount;
2235
2639
  if (session.binaryCount) acc.binaryOnly = (acc.binaryOnly || 0) + session.binaryCount;
2640
+ if (session.partialSummary) acc.partialSummaries = (acc.partialSummaries || 0) + 1;
2641
+ if (session.stateDbCount) acc.stateDbs = (acc.stateDbs || 0) + session.stateDbCount;
2236
2642
  return acc;
2237
2643
  }, {});
2238
2644
  }
@@ -2413,7 +2819,7 @@ function readCodexThreads(env = process.env) {
2413
2819
  "where rollout_path != ''",
2414
2820
  "order by updated_at desc"
2415
2821
  ].join(" ");
2416
- const result = spawnSync("sqlite3", [db, "-json", query], { encoding: "utf8", maxBuffer: 1024 * 1024 * 50 });
2822
+ const result = spawnSync("sqlite3", [db, "-json", query], { argv0: "agentlog-sqlite", encoding: "utf8", maxBuffer: 1024 * 1024 * 50 });
2417
2823
  if (result.status !== 0 || !result.stdout.trim()) return [];
2418
2824
  try {
2419
2825
  return JSON.parse(result.stdout).map((row) => ({
@@ -2550,13 +2956,12 @@ function normalizeSourcePath(file) {
2550
2956
  }
2551
2957
 
2552
2958
  function sqliteTableExists(dbPath, tableName) {
2553
- const result = runSqliteJson(
2554
- dbPath,
2555
- `select name from sqlite_master where type = 'table' and name = ${sqlQuote(tableName)} limit 1`
2556
- );
2557
- if (!result.ok) return false;
2558
2959
  try {
2559
- return parseSqliteJson(result.stdout).length > 0;
2960
+ return readSqliteJson(
2961
+ dbPath,
2962
+ `select name from sqlite_master where type = 'table' and name = ${sqlQuote(tableName)} limit 1`,
2963
+ `${tableName} table check`
2964
+ ).length > 0;
2560
2965
  } catch {
2561
2966
  return false;
2562
2967
  }
@@ -2848,11 +3253,13 @@ function readCursorProjectTranscriptSessions(options = {}) {
2848
3253
  return stat && stat.mtime >= options.modifiedSince;
2849
3254
  }));
2850
3255
  }
3256
+ const composerInfoLookup = options.composerInfoLookup
3257
+ || (options.composerTitleLookup ? (id) => ({ title: options.composerTitleLookup(id), model: "" }) : cursorBuildComposerInfoLookup(env));
2851
3258
  const sessions = [];
2852
3259
  reportDiscoveryProgress(options, { current: 0, total: groups.length, message: "reading Cursor agent transcripts" });
2853
3260
  for (let index = 0; index < groups.length; index++) {
2854
3261
  const group = groups[index];
2855
- const session = parseCursorTranscriptGroup(group);
3262
+ const session = parseCursorTranscriptGroup({ ...group, composerInfoLookup });
2856
3263
  if (session) sessions.push(session);
2857
3264
  reportDiscoveryProgress(options, {
2858
3265
  current: index + 1,
@@ -2864,6 +3271,87 @@ function readCursorProjectTranscriptSessions(options = {}) {
2864
3271
  return sessions;
2865
3272
  }
2866
3273
 
3274
+ function cursorBuildComposerTitleLookup(env = process.env) {
3275
+ const lookup = cursorBuildComposerInfoLookup(env);
3276
+ return (composerId) => lookup(composerId).title;
3277
+ }
3278
+
3279
+ function cursorBuildComposerInfoLookup(env = process.env) {
3280
+ const info = new Map();
3281
+ for (const db of cursorGlobalStorageDbs(env)) {
3282
+ try {
3283
+ const composerRows = readSqliteJson(
3284
+ db,
3285
+ [
3286
+ "select",
3287
+ "key,",
3288
+ "json_extract(value, '$.name') as name,",
3289
+ "json_extract(value, '$.title') as title,",
3290
+ "json_extract(value, '$.chatTitle') as chatTitle,",
3291
+ "json_extract(value, '$.modelConfig.modelName') as modelConfigModelName",
3292
+ "from cursorDiskKV where",
3293
+ "json_valid(value) and (",
3294
+ cursorDiskKvPrefixRangeCondition("composerData:"),
3295
+ "or",
3296
+ cursorDiskKvPrefixRangeCondition("_composerData:"),
3297
+ ")"
3298
+ ].join(" "),
3299
+ "Cursor global SQLite store"
3300
+ );
3301
+ for (const row of composerRows) {
3302
+ const match = String(row.key || "").match(/^_?composerData:(.+)$/);
3303
+ if (!match) continue;
3304
+ const composerId = match[1].toLowerCase();
3305
+ const title = firstString(row.name, row.title, row.chatTitle);
3306
+ const entry = info.get(composerId) || { title: "", modelHist: new Map() };
3307
+ if (title && !entry.title) entry.title = title;
3308
+ const configModel = firstString(row.modelConfigModelName);
3309
+ if (configModel) entry.configModel = (entry.configModel || configModel);
3310
+ info.set(composerId, entry);
3311
+ }
3312
+ const bubbleRows = readSqliteJson(
3313
+ db,
3314
+ [
3315
+ "select",
3316
+ "key,",
3317
+ "json_extract(value, '$.modelId') as modelId,",
3318
+ "json_extract(value, '$.modelName') as modelName,",
3319
+ "json_extract(value, '$.model') as model",
3320
+ "from cursorDiskKV where",
3321
+ "json_valid(value) and",
3322
+ cursorDiskKvPrefixRangeCondition("bubbleId:")
3323
+ ].join(" "),
3324
+ "Cursor global SQLite store"
3325
+ );
3326
+ for (const row of bubbleRows) {
3327
+ const keyMatch = String(row.key || "").match(/^bubbleId:([^:]+):/);
3328
+ if (!keyMatch) continue;
3329
+ const composerId = keyMatch[1].toLowerCase();
3330
+ const model = firstString(row.modelId, row.modelName, row.model);
3331
+ if (!model) continue;
3332
+ const entry = info.get(composerId) || { title: "", modelHist: new Map() };
3333
+ entry.modelHist.set(model, (entry.modelHist.get(model) || 0) + 1);
3334
+ info.set(composerId, entry);
3335
+ }
3336
+ } catch {
3337
+ // global store may be locked or absent; degrade silently.
3338
+ }
3339
+ }
3340
+ const result = new Map();
3341
+ for (const [id, entry] of info) {
3342
+ let dominantModel = "";
3343
+ let bestCount = 0;
3344
+ for (const [model, count] of entry.modelHist || []) {
3345
+ if (count > bestCount) { bestCount = count; dominantModel = model; }
3346
+ }
3347
+ result.set(id, {
3348
+ title: entry.title || "",
3349
+ model: dominantModel || entry.configModel || ""
3350
+ });
3351
+ }
3352
+ return (composerId) => result.get(String(composerId || "").toLowerCase()) || { title: "", model: "" };
3353
+ }
3354
+
2867
3355
  function cursorProjectTranscriptFiles(env = process.env) {
2868
3356
  const root = cursorProjectsRoot(env);
2869
3357
  const files = [];
@@ -2884,13 +3372,28 @@ function groupCursorTranscriptFiles(files, env = process.env) {
2884
3372
  if (agentIndex < 1) continue;
2885
3373
  const projectSlug = parts[0];
2886
3374
  const rest = parts.slice(agentIndex + 1);
2887
- const sessionPart = rest.length > 1 ? rest[0] : path.basename(file, path.extname(file));
2888
- const root = path.join(projectsRoot, projectSlug, "agent-transcripts", sessionPart);
2889
- const key = `${projectSlug}:${sessionPart}`;
3375
+ const subagentIndex = rest.indexOf("subagents");
3376
+ let id;
3377
+ let parentId = "";
3378
+ let root;
3379
+ let key;
3380
+ if (subagentIndex >= 0 && rest.length > subagentIndex + 1) {
3381
+ // <projectSlug>/agent-transcripts/<parent>/subagents/<subagent>/...
3382
+ parentId = rest.slice(0, subagentIndex).filter(Boolean)[0] || "";
3383
+ const subagentRest = rest.slice(subagentIndex + 1);
3384
+ id = subagentRest.length > 1 ? subagentRest[0] : path.basename(file, path.extname(file));
3385
+ root = path.join(projectsRoot, projectSlug, "agent-transcripts", parentId, "subagents", id);
3386
+ key = `${projectSlug}:${parentId}/subagents/${id}`;
3387
+ } else {
3388
+ id = rest.length > 1 ? rest[0] : path.basename(file, path.extname(file));
3389
+ root = path.join(projectsRoot, projectSlug, "agent-transcripts", id);
3390
+ key = `${projectSlug}:${id}`;
3391
+ }
2890
3392
  if (!groups.has(key)) {
2891
3393
  groups.set(key, {
2892
3394
  key,
2893
- id: sessionPart,
3395
+ id,
3396
+ parentId: parentId || undefined,
2894
3397
  projectSlug,
2895
3398
  projectDir: path.join(projectsRoot, projectSlug),
2896
3399
  root,
@@ -2905,16 +3408,28 @@ function groupCursorTranscriptFiles(files, env = process.env) {
2905
3408
 
2906
3409
  function parseCursorTranscriptGroup(group) {
2907
3410
  const parsedFiles = group.files.map(parseCursorTranscriptFile).filter(Boolean);
2908
- const messages = stampMessages(dedupeAdjacentMessages(
2909
- parsedFiles.flatMap((parsed) => parsed.messages || []).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)))
2910
- ), "cursor-agent-transcripts");
2911
- if (!messages.length) return null;
3411
+ const rawMessages = parsedFiles
3412
+ .flatMap((parsed) => parsed.messages || [])
3413
+ .sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
3414
+ if (!rawMessages.length) return null;
3415
+ const composerInfoLookup = group.composerInfoLookup
3416
+ || (group.composerLookup ? (id) => ({ title: group.composerLookup(id), model: "" }) : () => ({ title: "", model: "" }));
3417
+ const info = composerInfoLookup(group.id);
3418
+ const fallbackModel = info.model || "";
3419
+ const enrichedRaw = fallbackModel
3420
+ ? rawMessages.map((message) => cursorMessageWithFallbackModel(message, fallbackModel))
3421
+ : rawMessages;
3422
+ const messages = stampMessages(dedupeAdjacentMessages(enrichedRaw), "cursor-agent-transcripts");
2912
3423
  const cwd = firstString(...parsedFiles.map((parsed) => parsed.cwd), cursorSlugToPath(group.projectSlug, group.env));
3424
+ const fileTitle = firstString(...parsedFiles.map((parsed) => parsed.title));
3425
+ const userTitle = cursorTranscriptTitleFromMessages(messages);
3426
+ const title = firstString(fileTitle, info.title, userTitle, group.id.replace(/[-_]+/g, " "));
2913
3427
  return {
2914
3428
  sessionId: `cursor-${hashId(group.key)}`,
2915
3429
  id: group.id,
3430
+ parentComposerId: group.parentId || undefined,
2916
3431
  sourceKey: "cursor-agent-transcripts",
2917
- title: firstString(...parsedFiles.map((parsed) => parsed.title), group.id.replace(/[-_]+/g, " ")),
3432
+ title,
2918
3433
  cwd,
2919
3434
  startedAt: messages[0]?.timestamp || new Date().toISOString(),
2920
3435
  endedAt: messages[messages.length - 1]?.timestamp || messages[0]?.timestamp || new Date().toISOString(),
@@ -2928,6 +3443,19 @@ function parseCursorTranscriptGroup(group) {
2928
3443
  };
2929
3444
  }
2930
3445
 
3446
+ function cursorTranscriptTitleFromMessages(messages) {
3447
+ for (const message of messages || []) {
3448
+ if (message.role !== "user") continue;
3449
+ const text = String(message.content || "").trim();
3450
+ if (!text) continue;
3451
+ const firstLineText = text.split(/\r?\n/, 1)[0].trim();
3452
+ if (!firstLineText) continue;
3453
+ const truncated = firstLineText.length > 80 ? firstLineText.slice(0, 77).trimEnd() + "…" : firstLineText;
3454
+ return truncated;
3455
+ }
3456
+ return "";
3457
+ }
3458
+
2931
3459
  function parseCursorTranscriptFile(file) {
2932
3460
  const stat = safeStat(file);
2933
3461
  const fallbackTime = new Date(stat?.mtimeMs || Date.now()).toISOString();
@@ -3231,6 +3759,7 @@ function readCursorGlobalDiskKvSessionsFromDb(dbPath, options = {}) {
3231
3759
  "json_extract(value, '$.workspaceIdentifier') as workspaceIdentifier",
3232
3760
  "json_extract(value, '$.workspace') as workspace",
3233
3761
  "json_extract(value, '$.context') as context",
3762
+ "json_extract(value, '$.modelConfig') as modelConfig",
3234
3763
  "json_extract(value, '$.fullConversationHeadersOnly') as fullConversationHeadersOnly",
3235
3764
  "length(json_extract(value, '$.conversation')) as conversationBytes"
3236
3765
  ].join(", "),
@@ -3289,6 +3818,7 @@ function cursorGlobalComposerDataFromRow(row) {
3289
3818
  workspaceIdentifier: cursorParseSqliteJsonColumn(row.workspaceIdentifier),
3290
3819
  workspace: cursorParseSqliteJsonColumn(row.workspace),
3291
3820
  context: cursorParseSqliteJsonColumn(row.context),
3821
+ modelConfig: cursorParseSqliteJsonColumn(row.modelConfig),
3292
3822
  fullConversationHeadersOnly: cursorParseSqliteJsonColumn(row.fullConversationHeadersOnly),
3293
3823
  _hasConversation: Number(row.conversationBytes || 0) > 2
3294
3824
  };
@@ -3408,6 +3938,8 @@ function cursorGlobalBubbleSelectColumns(valueExpression = "value", keyExpressio
3408
3938
  `json_extract(${valueExpression}, '$.modelID') as modelID`,
3409
3939
  `json_extract(${valueExpression}, '$.modelName') as modelName`,
3410
3940
  `json_extract(${valueExpression}, '$.modelSlug') as modelSlug`,
3941
+ `json_extract(${valueExpression}, '$.modelConfig') as modelConfig`,
3942
+ `json_extract(${valueExpression}, '$.providerOptions') as providerOptions`,
3411
3943
  `json_extract(${valueExpression}, '$.status') as status`,
3412
3944
  `json_extract(${valueExpression}, '$.state') as state`,
3413
3945
  `json_extract(${valueExpression}, '$.phase') as phase`,
@@ -3422,6 +3954,8 @@ function cursorGlobalBubbleSelectColumns(valueExpression = "value", keyExpressio
3422
3954
  `json_extract(${valueExpression}, '$.usage') as usage`,
3423
3955
  `json_extract(${valueExpression}, '$.tokenUsage') as tokenUsage`,
3424
3956
  `json_extract(${valueExpression}, '$.token_usage') as token_usage`,
3957
+ `json_extract(${valueExpression}, '$.tokenCount') as tokenCount`,
3958
+ `json_extract(${valueExpression}, '$.token_count') as token_count`,
3425
3959
  `json_extract(${valueExpression}, '$.tokens') as tokens`,
3426
3960
  `json_extract(${valueExpression}, '$.terminalSelections') as terminalSelections`,
3427
3961
  `json_extract(${valueExpression}, '$.fileSelections') as fileSelections`,
@@ -3458,6 +3992,8 @@ function cursorGlobalBubbleDataFromRow(row) {
3458
3992
  modelID: row.modelID,
3459
3993
  modelName: row.modelName,
3460
3994
  modelSlug: row.modelSlug,
3995
+ modelConfig: cursorParseSqliteJsonColumn(row.modelConfig),
3996
+ providerOptions: cursorParseSqliteJsonColumn(row.providerOptions),
3461
3997
  status: row.status,
3462
3998
  state: cursorParseSqliteJsonColumn(row.state) || row.state,
3463
3999
  phase: row.phase,
@@ -3472,6 +4008,8 @@ function cursorGlobalBubbleDataFromRow(row) {
3472
4008
  usage: cursorParseSqliteJsonColumn(row.usage),
3473
4009
  tokenUsage: cursorParseSqliteJsonColumn(row.tokenUsage),
3474
4010
  token_usage: cursorParseSqliteJsonColumn(row.token_usage),
4011
+ tokenCount: cursorParseSqliteJsonColumn(row.tokenCount),
4012
+ token_count: cursorParseSqliteJsonColumn(row.token_count),
3475
4013
  tokens: cursorParseSqliteJsonColumn(row.tokens),
3476
4014
  terminalSelections: cursorParseSqliteJsonColumn(row.terminalSelections) || [],
3477
4015
  fileSelections: cursorParseSqliteJsonColumn(row.fileSelections) || [],
@@ -3500,6 +4038,7 @@ function cursorGlobalComposerSession(dbPath, composerId, composer, bubbleMap, op
3500
4038
  const headers = cursorGlobalComposerHeaders(composer);
3501
4039
  const startedAt = cursorIso(composer.createdAt || composer.created_at) || eventTimestamp(composer) || new Date().toISOString();
3502
4040
  const endedAt = cursorIso(composer.lastUpdatedAt || composer.updatedAt || composer.updated_at) || startedAt;
4041
+ const composerModel = cursorModel(composer);
3503
4042
  const messages = [];
3504
4043
  for (let index = 0; index < headers.length; index++) {
3505
4044
  const header = headers[index] || {};
@@ -3507,14 +4046,14 @@ function cursorGlobalComposerSession(dbPath, composerId, composer, bubbleMap, op
3507
4046
  const record = bubbleMap.get(bubbleId) || header;
3508
4047
  const timestamp = offsetTimestamp(startedAt, index);
3509
4048
  for (const message of cursorMessagesFromRecord(record, "cursor-global-sqlite", timestamp)) {
3510
- messages.push({ ...message, timestamp });
4049
+ messages.push(cursorMessageWithFallbackModel({ ...message, timestamp }, composerModel));
3511
4050
  }
3512
4051
  }
3513
4052
  if (!messages.length) {
3514
4053
  for (const [index, record] of [...bubbleMap.values()].sort(cursorGlobalBubbleRecordCompare).entries()) {
3515
4054
  const timestamp = offsetTimestamp(startedAt, index);
3516
4055
  for (const message of cursorMessagesFromRecord(record, "cursor-global-sqlite", timestamp)) {
3517
- messages.push({ ...message, timestamp });
4056
+ messages.push(cursorMessageWithFallbackModel({ ...message, timestamp }, composerModel));
3518
4057
  }
3519
4058
  }
3520
4059
  }
@@ -3540,6 +4079,17 @@ function cursorGlobalComposerSession(dbPath, composerId, composer, bubbleMap, op
3540
4079
  };
3541
4080
  }
3542
4081
 
4082
+ function cursorMessageWithFallbackModel(message, model) {
4083
+ if (!model || message?.role !== "assistant" || message?.metadata?.model) return message;
4084
+ return {
4085
+ ...message,
4086
+ metadata: {
4087
+ ...(message.metadata || {}),
4088
+ model
4089
+ }
4090
+ };
4091
+ }
4092
+
3543
4093
  function cursorGlobalComposerHeaders(composer) {
3544
4094
  if (Array.isArray(composer.fullConversationHeadersOnly) && composer.fullConversationHeadersOnly.length) {
3545
4095
  return composer.fullConversationHeadersOnly;
@@ -3894,24 +4444,46 @@ function cursorCwdFromObject(data) {
3894
4444
  );
3895
4445
  const workspaceCwd = cursorExistingCwdFromPath(workspacePath, true);
3896
4446
  if (workspaceCwd) return workspaceCwd;
4447
+ const candidates = cursorPathCandidatesFromValue(data);
4448
+ const mostFrequent = cursorMostFrequentExistingCwd(candidates);
4449
+ if (mostFrequent) return mostFrequent;
3897
4450
  const filePath = cursorFirstPathInObject(data);
3898
4451
  return cursorExistingCwdFromPath(filePath, false);
3899
4452
  }
3900
4453
 
3901
4454
  function cursorCwdFromMessages(messages) {
4455
+ const allCandidates = [];
3902
4456
  for (const message of messages || []) {
3903
- const metadataPaths = cursorPathCandidatesFromValue(message?.metadata);
3904
- for (const candidate of metadataPaths) {
3905
- const cwd = cursorExistingCwdFromPath(candidate, false);
3906
- if (cwd) return cwd;
3907
- }
4457
+ allCandidates.push(...cursorPathCandidatesFromValue(message?.metadata));
3908
4458
  }
3909
4459
  const text = (messages || []).map((message) => message?.content || "").join("\n");
3910
- for (const candidate of cursorPathCandidatesFromValue(text)) {
4460
+ allCandidates.push(...cursorPathCandidatesFromValue(text));
4461
+ const mostFrequent = cursorMostFrequentExistingCwd(allCandidates);
4462
+ if (mostFrequent) return mostFrequent;
4463
+ return cursorCwdFromTerminalPromptText(text);
4464
+ }
4465
+
4466
+ function cursorMostFrequentExistingCwd(candidates) {
4467
+ if (!candidates || !candidates.length) return "";
4468
+ const counts = new Map();
4469
+ const order = [];
4470
+ for (const candidate of candidates) {
3911
4471
  const cwd = cursorExistingCwdFromPath(candidate, false);
3912
- if (cwd) return cwd;
4472
+ if (!cwd) continue;
4473
+ if (!counts.has(cwd)) order.push(cwd);
4474
+ counts.set(cwd, (counts.get(cwd) || 0) + 1);
3913
4475
  }
3914
- return cursorCwdFromTerminalPromptText(text);
4476
+ if (!counts.size) return "";
4477
+ let bestCwd = "";
4478
+ let bestCount = -1;
4479
+ for (const cwd of order) {
4480
+ const count = counts.get(cwd) || 0;
4481
+ if (count > bestCount) {
4482
+ bestCount = count;
4483
+ bestCwd = cwd;
4484
+ }
4485
+ }
4486
+ return bestCwd;
3915
4487
  }
3916
4488
 
3917
4489
  function cursorPathCandidatesFromValue(value, depth = 0, candidates = []) {
@@ -4020,15 +4592,29 @@ function cursorNormalizePathCandidate(value) {
4020
4592
  function cursorAttributionCwd(value) {
4021
4593
  const candidate = cursorNormalizePathCandidate(value);
4022
4594
  if (!candidate || !path.isAbsolute(candidate)) return "";
4023
- const existing = cursorExistingCwdFromPath(candidate, true);
4595
+ if (cursorIsSystemRootPath(candidate)) return "";
4596
+ const climbed = cursorClimbOutOfDependencyDirs(candidate);
4597
+ const existing = cursorExistingCwdFromPath(climbed, true);
4024
4598
  if (existing) return existing;
4025
4599
  const appSupportCursorWorkspace = `${path.sep}Application Support${path.sep}Cursor${path.sep}Workspaces${path.sep}`;
4026
- if (candidate.includes(appSupportCursorWorkspace)) return "";
4600
+ if (climbed.includes(appSupportCursorWorkspace)) return "";
4601
+ return climbed;
4602
+ }
4603
+
4604
+ function cursorClimbOutOfDependencyDirs(candidate) {
4605
+ const segments = candidate.split(path.sep);
4606
+ // Strip trailing path beyond the first node_modules / .pnpm / vendor segment
4607
+ // so workspace folders that point inside a dependency resolve to the project root.
4608
+ for (const marker of ["node_modules", ".pnpm", "bower_components", "vendor"]) {
4609
+ const index = segments.indexOf(marker);
4610
+ if (index > 0) return segments.slice(0, index).join(path.sep) || candidate;
4611
+ }
4027
4612
  return candidate;
4028
4613
  }
4029
4614
 
4030
4615
  function cursorExistingCwdFromPath(candidate, assumeDirectory = false) {
4031
4616
  if (!candidate || !path.isAbsolute(candidate)) return "";
4617
+ if (cursorIsSystemRootPath(candidate)) return "";
4032
4618
  const stat = safeStat(candidate);
4033
4619
  let start = "";
4034
4620
  if (stat?.isDirectory()) start = candidate;
@@ -4042,13 +4628,34 @@ function cursorExistingCwdFromPath(candidate, assumeDirectory = false) {
4042
4628
  function cursorNearestProjectDir(start) {
4043
4629
  let current = path.resolve(start);
4044
4630
  for (;;) {
4045
- if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, "package.json"))) return current;
4631
+ if (cursorIsSystemRootPath(current)) return "";
4632
+ const insideDependency = cursorPathInsideDependencyDir(current);
4633
+ const hasMarker = fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, "package.json"));
4634
+ if (hasMarker && !insideDependency) return current;
4046
4635
  const parent = path.dirname(current);
4047
- if (parent === current) return fs.existsSync(start) ? start : "";
4636
+ if (parent === current) return cursorIsSystemRootPath(start) ? "" : (fs.existsSync(start) ? start : "");
4048
4637
  current = parent;
4049
4638
  }
4050
4639
  }
4051
4640
 
4641
+ function cursorPathInsideDependencyDir(candidate) {
4642
+ const segments = String(candidate || "").split(path.sep);
4643
+ return ["node_modules", ".pnpm", "bower_components", "vendor"].some((marker) => segments.includes(marker));
4644
+ }
4645
+
4646
+ function cursorIsSystemRootPath(candidate) {
4647
+ const normalized = String(candidate || "").replace(/\/+$/, "") || "/";
4648
+ if (normalized === "/" || normalized === path.parse(normalized).root) return true;
4649
+ const home = os.homedir();
4650
+ if (normalized === home) return true;
4651
+ // Top-level system directories that are never project roots on their own.
4652
+ const blocked = new Set([
4653
+ "/Users", "/home", "/Volumes", "/private", "/tmp", "/var",
4654
+ "/Applications", "/Library", "/System", "/opt", "/etc", "/usr"
4655
+ ]);
4656
+ return blocked.has(normalized);
4657
+ }
4658
+
4052
4659
  function readSqliteJson(dbPath, query, label = "SQLite store") {
4053
4660
  const result = runSqliteJson(dbPath, query);
4054
4661
  if (result.ok) return parseSqliteJson(result.stdout);
@@ -4058,6 +4665,7 @@ function readSqliteJson(dbPath, query, label = "SQLite store") {
4058
4665
 
4059
4666
  function runSqliteJson(dbPath, query) {
4060
4667
  const result = spawnSync("sqlite3", [dbPath, "-json", query], {
4668
+ argv0: "agentlog-sqlite",
4061
4669
  encoding: "utf8",
4062
4670
  maxBuffer: 1024 * 1024 * 200,
4063
4671
  timeout: SQLITE_QUERY_TIMEOUT_MS
@@ -4131,9 +4739,13 @@ function extractCursorConversations(data, sourceKey, fallbackTime) {
4131
4739
  const visit = (node) => {
4132
4740
  if (!node || typeof node !== "object") return;
4133
4741
  if (Array.isArray(node.bubbles)) {
4134
- const messages = node.bubbles
4742
+ const containerModel = cursorModel(node);
4743
+ const rawMessages = node.bubbles
4135
4744
  .flatMap((bubble, index) => cursorMessagesFromRecord(bubble, sourceKey, offsetTimestamp(fallbackTime, index)))
4136
4745
  .filter(Boolean);
4746
+ const messages = containerModel
4747
+ ? rawMessages.map((message) => cursorMessageWithFallbackModel(message, containerModel))
4748
+ : rawMessages;
4137
4749
  if (messages.length) {
4138
4750
  conversations.push({
4139
4751
  id: node.tabId || node.composerId || node.id || hashId(JSON.stringify(messages.slice(0, 2))),
@@ -4297,6 +4909,7 @@ function cursorAiServiceGenerationMessage(entry, fallbackTime, index) {
4297
4909
  const timestamp = cursorIso(entry.unixMs) || offsetTimestamp(fallbackTime, index);
4298
4910
  const role = type === "composer" ? "user" : "assistant";
4299
4911
  const content = type === "apply" ? `Applied changes: ${text}` : text;
4912
+ const commandType = cursorNormalizeCommandType(entry.commandType ?? entry.command_type);
4300
4913
  return {
4301
4914
  role,
4302
4915
  content,
@@ -4305,7 +4918,9 @@ function cursorAiServiceGenerationMessage(entry, fallbackTime, index) {
4305
4918
  provider: "cursor",
4306
4919
  source: "cursor-ai-service-history",
4307
4920
  eventType: firstString(type, "aiService.generation"),
4308
- requestId: firstString(entry.generationUUID, entry.generationId, entry.id) || undefined
4921
+ requestId: firstString(entry.generationUUID, entry.generationId, entry.id) || undefined,
4922
+ commandType: commandType || undefined,
4923
+ model: cursorModel(entry) || undefined
4309
4924
  }
4310
4925
  };
4311
4926
  }
@@ -4313,6 +4928,7 @@ function cursorAiServiceGenerationMessage(entry, fallbackTime, index) {
4313
4928
  function cursorAiServicePromptMessage(prompt, fallbackTime, index) {
4314
4929
  const text = firstString(prompt?.text, prompt?.prompt, prompt?.value);
4315
4930
  if (!text) return null;
4931
+ const commandType = cursorNormalizeCommandType(prompt?.commandType ?? prompt?.command_type);
4316
4932
  return {
4317
4933
  role: "user",
4318
4934
  content: text,
@@ -4320,11 +4936,31 @@ function cursorAiServicePromptMessage(prompt, fallbackTime, index) {
4320
4936
  metadata: {
4321
4937
  provider: "cursor",
4322
4938
  source: "cursor-ai-service-history",
4323
- eventType: "aiService.prompt"
4939
+ eventType: "aiService.prompt",
4940
+ commandType: commandType || undefined
4324
4941
  }
4325
4942
  };
4326
4943
  }
4327
4944
 
4945
+ function cursorNormalizeCommandType(value) {
4946
+ if (value == null || value === "") return "";
4947
+ const text = String(value).trim();
4948
+ if (!text) return "";
4949
+ // Cursor stores commandType as either an integer enum or a string. Normalize the
4950
+ // common integer mapping so downstream consumers see human-readable labels.
4951
+ const numeric = Number(text);
4952
+ if (Number.isInteger(numeric)) {
4953
+ switch (numeric) {
4954
+ case 1: return "chat";
4955
+ case 2: return "edit";
4956
+ case 3: return "agent";
4957
+ case 4: return "ask";
4958
+ default: return `command-${numeric}`;
4959
+ }
4960
+ }
4961
+ return text.toLowerCase();
4962
+ }
4963
+
4328
4964
  function cursorMs(...values) {
4329
4965
  for (const value of values) {
4330
4966
  if (value == null || value === "") continue;
@@ -4476,43 +5112,184 @@ function cursorStructuredText(value) {
4476
5112
 
4477
5113
  function cursorMessageMetadata(record, source) {
4478
5114
  const usage = cursorUsage(record);
5115
+ const commandType = cursorNormalizeCommandType(record?.commandType ?? record?.command_type ?? record?.message?.commandType);
4479
5116
  return {
4480
5117
  provider: "cursor",
4481
5118
  source,
4482
5119
  bubbleType: String(record?.type || "").trim() || undefined,
4483
5120
  eventType: firstString(record?.eventType, record?.event_type, record?.kind, record?.type) || undefined,
4484
- model: firstString(record?.model, record?.modelId, record?.modelID, record?.modelName, record?.modelSlug, record?.message?.model) || undefined,
5121
+ model: cursorModel(record) || undefined,
4485
5122
  status: firstString(record?.status, record?.state, record?.phase) || undefined,
4486
5123
  requestId: firstString(record?.requestId, record?.request_id, record?.messageId, record?.messageID) || undefined,
4487
5124
  composerId: firstString(record?.composerId, record?.composerID, record?.conversationId, record?.conversation_id) || undefined,
5125
+ commandType: commandType || undefined,
4488
5126
  usage: usage || undefined
4489
5127
  };
4490
5128
  }
4491
5129
 
5130
+ function cursorModel(record) {
5131
+ return firstCursorModel(
5132
+ record?.model,
5133
+ record?.modelId,
5134
+ record?.modelID,
5135
+ record?.modelName,
5136
+ record?.modelSlug,
5137
+ record?.message?.model,
5138
+ record?.message?.modelId,
5139
+ record?.message?.modelName,
5140
+ record?.providerOptions?.cursor?.modelName,
5141
+ record?.providerOptions?.cursor?.modelId,
5142
+ record?.provider_options?.cursor?.modelName,
5143
+ record?.provider_options?.cursor?.modelId,
5144
+ record?.message?.providerOptions?.cursor?.modelName,
5145
+ record?.message?.providerOptions?.cursor?.modelId,
5146
+ cursorModelFromModelConfig(record?.modelConfig),
5147
+ cursorModelFromModelConfig(record?.message?.modelConfig),
5148
+ cursorModelFromParts(record?.content),
5149
+ cursorModelFromParts(record?.message?.content),
5150
+ cursorModelFromParts(record?.parts)
5151
+ );
5152
+ }
5153
+
5154
+ function cursorModelFromModelConfig(config) {
5155
+ if (!config || typeof config !== "object") return "";
5156
+ const direct = firstCursorModel(config.modelName, config.model, config.modelId, config.model_slug, config.modelSlug);
5157
+ if (direct) return direct;
5158
+ const selected = Array.isArray(config.selectedModels) ? config.selectedModels : [];
5159
+ for (const item of selected) {
5160
+ const model = firstCursorModel(item?.modelName, item?.modelId, item?.id, item?.model);
5161
+ if (model) return model;
5162
+ }
5163
+ return "";
5164
+ }
5165
+
5166
+ function cursorModelFromParts(value, depth = 0) {
5167
+ if (!value || depth > 5) return "";
5168
+ if (Array.isArray(value)) {
5169
+ for (const item of value) {
5170
+ const model = cursorModelFromParts(item, depth + 1);
5171
+ if (model) return model;
5172
+ }
5173
+ return "";
5174
+ }
5175
+ if (typeof value !== "object") return "";
5176
+ const direct = firstCursorModel(
5177
+ value.providerOptions?.cursor?.modelName,
5178
+ value.providerOptions?.cursor?.modelId,
5179
+ value.provider_options?.cursor?.modelName,
5180
+ value.provider_options?.cursor?.modelId
5181
+ );
5182
+ if (direct) return direct;
5183
+ const configured = cursorModelFromModelConfig(value.modelConfig);
5184
+ if (configured) return configured;
5185
+ for (const child of Object.values(value)) {
5186
+ if (child && typeof child === "object") {
5187
+ const model = cursorModelFromParts(child, depth + 1);
5188
+ if (model) return model;
5189
+ }
5190
+ }
5191
+ return "";
5192
+ }
5193
+
5194
+ function firstCursorModel(...values) {
5195
+ for (const value of values) {
5196
+ if (typeof value !== "string") continue;
5197
+ const trimmed = value.trim();
5198
+ if (!trimmed || /^default$/i.test(trimmed)) continue;
5199
+ return trimmed;
5200
+ }
5201
+ return "";
5202
+ }
5203
+
4492
5204
  function cursorUsage(record) {
4493
5205
  const candidates = [
5206
+ record?.tokenCount,
5207
+ record?.token_count,
4494
5208
  record?.usage,
4495
5209
  record?.tokenUsage,
4496
5210
  record?.token_usage,
4497
5211
  record?.tokens,
4498
5212
  record?.metrics?.usage,
5213
+ record?.message?.tokenCount,
5214
+ record?.message?.token_count,
4499
5215
  record?.message?.usage,
4500
5216
  record?.message?.tokenUsage
4501
5217
  ];
5218
+ let input = null;
5219
+ let output = null;
5220
+ let cacheInput = null;
5221
+ let total = null;
4502
5222
  for (const item of candidates) {
4503
5223
  if (!item || typeof item !== "object") continue;
4504
- const input = numericValue(item.inputTokens, item.input_tokens, item.promptTokens, item.prompt_tokens, item.prompt);
4505
- const output = numericValue(item.outputTokens, item.output_tokens, item.completionTokens, item.completion_tokens, item.completion);
4506
- const total = numericValue(item.totalTokens, item.total_tokens, item.total);
4507
- if (input != null || output != null || total != null) {
4508
- return {
4509
- inputTokens: input ?? undefined,
4510
- outputTokens: output ?? undefined,
4511
- totalTokens: total ?? (input != null || output != null ? (input || 0) + (output || 0) : undefined)
4512
- };
4513
- }
4514
- }
4515
- return null;
5224
+ input = preferredCursorTokenValue(input, numericValue(
5225
+ item.inputTokens,
5226
+ item.input_tokens,
5227
+ item.inputTokenCount,
5228
+ item.input_token_count,
5229
+ item.promptTokens,
5230
+ item.prompt_tokens,
5231
+ item.promptTokenCount,
5232
+ item.prompt_token_count,
5233
+ item.prompt
5234
+ ));
5235
+ output = preferredCursorTokenValue(output, numericValue(
5236
+ item.outputTokens,
5237
+ item.output_tokens,
5238
+ item.outputTokenCount,
5239
+ item.output_token_count,
5240
+ item.completionTokens,
5241
+ item.completion_tokens,
5242
+ item.completionTokenCount,
5243
+ item.completion_token_count,
5244
+ item.completion
5245
+ ));
5246
+ cacheInput = preferredCursorTokenValue(cacheInput, cursorCacheInputTokens(item));
5247
+ total = preferredCursorTokenValue(total, numericValue(item.totalTokens, item.total_tokens, item.totalTokenCount, item.total_token_count, item.total));
5248
+ }
5249
+ if (![input, output, cacheInput, total].some((value) => value != null && value > 0)) return null;
5250
+ const usage = {};
5251
+ if (input != null) usage.inputTokens = input;
5252
+ if (output != null) usage.outputTokens = output;
5253
+ if (cacheInput != null) usage.cacheInputTokens = cacheInput;
5254
+ if (total != null) usage.totalTokens = total;
5255
+ else if (input != null || output != null) usage.totalTokens = (input || 0) + (output || 0);
5256
+ return usage;
5257
+ }
5258
+
5259
+ function preferredCursorTokenValue(current, next) {
5260
+ if (next == null) return current;
5261
+ if (current == null) return next;
5262
+ if (current <= 0 && next > 0) return next;
5263
+ return current;
5264
+ }
5265
+
5266
+ function cursorCacheInputTokens(item) {
5267
+ const cacheInput = numericValue(item.cacheInputTokens, item.cache_input_tokens);
5268
+ const cacheCreation = numericValue(
5269
+ item.cacheCreationInputTokens,
5270
+ item.cache_creation_input_tokens,
5271
+ item.cacheCreationTokens,
5272
+ item.cache_creation_tokens
5273
+ );
5274
+ const cacheRead = numericValue(
5275
+ item.cacheReadInputTokens,
5276
+ item.cache_read_input_tokens,
5277
+ item.cacheReadTokens,
5278
+ item.cache_read_tokens
5279
+ );
5280
+ const cached = numericValue(
5281
+ item.cachedContentTokenCount,
5282
+ item.cached_content_token_count,
5283
+ item.cachedTokens,
5284
+ item.cached_tokens,
5285
+ item.cacheTokens,
5286
+ item.cache_tokens,
5287
+ item.cached
5288
+ );
5289
+ const total = [cacheInput, cacheCreation, cacheRead, cached]
5290
+ .filter((value) => value != null && value > 0)
5291
+ .reduce((sum, value) => sum + value, 0);
5292
+ return total > 0 ? total : null;
4516
5293
  }
4517
5294
 
4518
5295
  function cursorToolCallsFromRecord(record) {
@@ -4622,7 +5399,7 @@ function cursorToolName(node, keyToken, type) {
4622
5399
  if (node.command || node.cmd || node.terminalCommand) return "run_terminal_cmd";
4623
5400
  if (node.diff || node.patch || node.old_string || node.new_string || node.oldText || node.newText) return "edit";
4624
5401
  if (node.query || /search|grep/.test(type || keyToken)) return "search";
4625
- if (node.path || node.file || node.uri) {
5402
+ if (node.path || node.file || node.filename || node.filePath || node.fsPath || node.uri) {
4626
5403
  if (/read|open|view/.test(type || keyToken)) return "read_file";
4627
5404
  if (/write|edit|diff|patch/.test(type || keyToken)) return "edit_file";
4628
5405
  }
@@ -4634,16 +5411,22 @@ function cursorToolArguments(node, name, keyToken, type) {
4634
5411
  const value = node.input ?? node.args ?? node.arguments ?? node.params ?? node.parameters ?? node.function?.arguments ?? action.input;
4635
5412
  if (value != null) return value;
4636
5413
  if (node.command || node.cmd || node.terminalCommand) return { command: node.command || node.cmd || node.terminalCommand };
4637
- if (node.diff || node.patch) return { diff: node.diff || node.patch, path: firstString(node.path, node.file, node.filename) || undefined };
5414
+ if (node.diff || node.patch) return { diff: node.diff || node.patch, path: firstString(node.path, node.file, node.filename, node.filePath, node.fsPath) || undefined };
4638
5415
  if (node.old_string || node.new_string || node.oldText || node.newText) {
4639
5416
  return {
4640
- path: firstString(node.path, node.file, node.filename) || undefined,
5417
+ path: firstString(node.path, node.file, node.filename, node.filePath, node.fsPath) || undefined,
4641
5418
  old_string: firstString(node.old_string, node.oldText),
4642
5419
  new_string: firstString(node.new_string, node.newText)
4643
5420
  };
4644
5421
  }
4645
5422
  if (node.query) return { query: node.query };
4646
- if (node.path || node.file || node.uri) return { path: firstString(node.path, node.file, node.uri), name: name || type || keyToken };
5423
+ if (node.path || node.file || node.filename || node.filePath || node.fsPath || node.uri) {
5424
+ return {
5425
+ path: firstString(node.path, node.file, node.filename, node.filePath, node.fsPath, cursorUriPath(node.uri)),
5426
+ instruction: firstString(node.instruction, node.text, node.content) || undefined,
5427
+ name: name || type || keyToken
5428
+ };
5429
+ }
4647
5430
  return null;
4648
5431
  }
4649
5432
 
@@ -4653,7 +5436,11 @@ function cursorDiffToolCalls(node) {
4653
5436
  .concat(Array.isArray(node.diffs) ? node.diffs : [])
4654
5437
  .concat(Array.isArray(node.fileDiffs) ? node.fileDiffs : [])
4655
5438
  .concat(Array.isArray(node.file_diffs) ? node.file_diffs : [])
4656
- .concat(Array.isArray(node.edits) ? node.edits : []);
5439
+ .concat(Array.isArray(node.edits) ? node.edits : [])
5440
+ .concat(Array.isArray(node.suggestedCodeBlocks) ? node.suggestedCodeBlocks : [])
5441
+ .concat(Array.isArray(node.suggested_code_blocks) ? node.suggested_code_blocks : [])
5442
+ .concat(Array.isArray(node.diffHistories) ? node.diffHistories : [])
5443
+ .concat(Array.isArray(node.diff_histories) ? node.diff_histories : []);
4657
5444
  return diffs.map((diff) => cursorNormalizeToolCall(diff, "fileDiffs")).filter(Boolean);
4658
5445
  }
4659
5446
 
@@ -4703,9 +5490,9 @@ function cursorLooksLikeToolResult(node, keyToken, type) {
4703
5490
 
4704
5491
  function cursorToolTarget(args, node) {
4705
5492
  if (args && typeof args === "object" && !Array.isArray(args)) {
4706
- return firstString(args.path, args.file, args.filename, args.target_file, args.targetFile, args.uri);
5493
+ return firstString(args.path, args.file, args.filename, args.filePath, args.fsPath, args.target_file, args.targetFile, args.uri);
4707
5494
  }
4708
- return firstString(node?.path, node?.file, node?.filename, node?.target_file, node?.targetFile, node?.uri);
5495
+ return firstString(node?.path, node?.file, node?.filename, node?.filePath, node?.fsPath, node?.target_file, node?.targetFile, node?.uri);
4709
5496
  }
4710
5497
 
4711
5498
  function dedupeCursorToolCalls(calls) {
@@ -4761,8 +5548,12 @@ function cursorBubbleContext(bubble) {
4761
5548
  if (value) parts.push(`Terminal selection:\n${value}`);
4762
5549
  }
4763
5550
  }
4764
- if (Array.isArray(bubble.fileSelections)) {
4765
- const files = bubble.fileSelections.map((selection) => selection.uri?.fsPath || selection.uri?.path).filter(Boolean);
5551
+ if (Array.isArray(bubble.fileSelections) || Array.isArray(bubble.selections)) {
5552
+ const files = []
5553
+ .concat(Array.isArray(bubble.fileSelections) ? bubble.fileSelections : [])
5554
+ .concat(Array.isArray(bubble.selections) ? bubble.selections : [])
5555
+ .map((selection) => cursorUriPath(selection?.uri) || cursorNormalizePathCandidate(firstString(selection?.fsPath, selection?.path, selection?.filePath)))
5556
+ .filter(Boolean);
4766
5557
  if (files.length) parts.push(`Files:\n${files.join("\n")}`);
4767
5558
  }
4768
5559
  const nestedPaths = [
@@ -5045,17 +5836,106 @@ function dedupeCursorSessions(sessions) {
5045
5836
  seen.add(key);
5046
5837
  exact.push(session);
5047
5838
  }
5048
- const contentDeduped = cursorDedupeSessionsByContent(exact);
5839
+ cursorPropagateCwdFromComposerSiblings(exact);
5840
+ const composerDeduped = cursorDedupeByComposerId(exact);
5841
+ const contentDeduped = cursorDedupeSessionsByContent(composerDeduped);
5049
5842
  cursorPropagateCwdFromFallbackSessions(contentDeduped);
5050
5843
  return contentDeduped.filter(
5051
5844
  (session) =>
5052
5845
  !cursorSessionExactDuplicateInBetterSession(session, contentDeduped) &&
5053
5846
  !cursorSessionNearDuplicateInBetterSession(session, contentDeduped) &&
5054
5847
  !cursorSessionCoveredByBetterSessions(session, contentDeduped) &&
5055
- !cursorSessionContainedInBetterSession(session, contentDeduped)
5848
+ !cursorSessionContainedInBetterSession(session, contentDeduped) &&
5849
+ !cursorSessionIsEmptyApplyStub(session, contentDeduped)
5056
5850
  );
5057
5851
  }
5058
5852
 
5853
+ function cursorPropagateCwdFromComposerSiblings(sessions) {
5854
+ const cwdByComposerId = new Map();
5855
+ for (const session of sessions) {
5856
+ const composerId = cursorSessionComposerId(session);
5857
+ if (!composerId || !session.cwd) continue;
5858
+ const existing = cwdByComposerId.get(composerId);
5859
+ if (!existing || cursorSourceRank(session) > existing.rank) {
5860
+ cwdByComposerId.set(composerId, { cwd: session.cwd, rank: cursorSourceRank(session) });
5861
+ }
5862
+ }
5863
+ for (const target of sessions) {
5864
+ if (!target || target.cwd) continue;
5865
+ const composerId = cursorSessionComposerId(target);
5866
+ if (!composerId) continue;
5867
+ const sibling = cwdByComposerId.get(composerId);
5868
+ if (!sibling?.cwd) continue;
5869
+ target.cwd = sibling.cwd;
5870
+ if (target.scopeCanonical === "cursor/uncategorized") target.scopeCanonical = "";
5871
+ }
5872
+ }
5873
+
5874
+ function cursorDedupeByComposerId(sessions) {
5875
+ const byComposerId = new Map();
5876
+ const result = [];
5877
+ for (const session of sessions) {
5878
+ const composerId = cursorSessionComposerId(session);
5879
+ if (!composerId) {
5880
+ result.push(session);
5881
+ continue;
5882
+ }
5883
+ const existing = byComposerId.get(composerId);
5884
+ if (!existing) {
5885
+ byComposerId.set(composerId, session);
5886
+ result.push(session);
5887
+ continue;
5888
+ }
5889
+ if (cursorPreferSession(session, existing) === session) {
5890
+ const index = result.indexOf(existing);
5891
+ if (index >= 0) result[index] = session;
5892
+ byComposerId.set(composerId, session);
5893
+ }
5894
+ }
5895
+ return result;
5896
+ }
5897
+
5898
+ function cursorSessionComposerId(session) {
5899
+ if (!session) return "";
5900
+ const direct = firstString(session.composerId, session.id);
5901
+ if (cursorLooksLikeComposerId(direct)) return String(direct).trim();
5902
+ const fromPath = cursorComposerIdFromSourcePath(session.sourcePath || "");
5903
+ if (fromPath) return fromPath;
5904
+ return "";
5905
+ }
5906
+
5907
+ function cursorComposerIdFromSourcePath(sourcePath) {
5908
+ const text = String(sourcePath || "");
5909
+ if (!text) return "";
5910
+ const matches = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi);
5911
+ if (!matches) return "";
5912
+ // Prefer the last UUID in the path — that's the composer/transcript id, not the workspace hash.
5913
+ return matches[matches.length - 1].toLowerCase();
5914
+ }
5915
+
5916
+ function cursorSessionIsEmptyApplyStub(session, sessions) {
5917
+ if (!session || !cursorLikelyCursorPromptHistoryFallback(session)) return false;
5918
+ if (!cursorSessionAssistantMessagesAreOnlyApplyStubs(session)) return false;
5919
+ const userProbes = cursorDedupeUserProbes(session);
5920
+ const rank = cursorSourceRank(session);
5921
+ for (const other of sessions) {
5922
+ if (other === session) continue;
5923
+ if (cursorSourceRank(other) <= rank) continue;
5924
+ if (!cursorSessionsComparable(session, other)) continue;
5925
+ if (!userProbes.length) return true;
5926
+ const text = cursorSessionSearchText(other);
5927
+ if (userProbes.some((probe) => text.includes(probe))) return true;
5928
+ }
5929
+ return false;
5930
+ }
5931
+
5932
+ function cursorSessionAssistantMessagesAreOnlyApplyStubs(session) {
5933
+ const messages = session?.messages || [];
5934
+ const assistantMessages = messages.filter((message) => message.role === "assistant");
5935
+ if (!assistantMessages.length) return true;
5936
+ return assistantMessages.every((message) => /^applied changes:/i.test(String(message.content || "").trim()));
5937
+ }
5938
+
5059
5939
  function cursorPropagateCwdFromFallbackSessions(sessions) {
5060
5940
  for (const target of sessions) {
5061
5941
  if (!target || target.cwd) continue;
@@ -5261,6 +6141,9 @@ function cursorSessionSearchText(session) {
5261
6141
  }
5262
6142
 
5263
6143
  function cursorSessionsComparable(left, right) {
6144
+ const leftId = cursorSessionComposerId(left);
6145
+ const rightId = cursorSessionComposerId(right);
6146
+ if (leftId && rightId && leftId === rightId) return true;
5264
6147
  if (left.cwd && right.cwd && left.cwd !== right.cwd) return false;
5265
6148
  return true;
5266
6149
  }
@@ -5286,7 +6169,11 @@ function pruneCursorArchivedDuplicates(env = process.env, state = null, options
5286
6169
  const archived = listSessions(env)
5287
6170
  .filter((session) => session.provider === "cursor")
5288
6171
  .filter((session) => !sourcePaths || sourcePaths.has(cursorSessionSourcePath(session)))
5289
- .map((session) => cursorSessionWithInferredCwd({ ...session, messages: readTranscript(session.transcriptPath) }));
6172
+ .map((session) => cursorSessionWithInferredCwd({
6173
+ ...session,
6174
+ id: session.composerId || cursorComposerIdFromSourcePath(session.sourcePath || ""),
6175
+ messages: readTranscript(session.transcriptPath)
6176
+ }));
5290
6177
  const kept = new Set(dedupeCursorSessions(archived).map((session) => session.sessionId));
5291
6178
  let pruned = 0;
5292
6179
  for (const session of archived) {
@@ -5630,14 +6517,30 @@ function clineTitle(messages) {
5630
6517
  }
5631
6518
 
5632
6519
  function readOpenCodeSessions(env = process.env, options = {}) {
6520
+ const dbs = openCodeDatabaseFiles(env);
5633
6521
  const roots = openCodeStorageRoots(env);
5634
6522
  const files = roots.flatMap((root) => openCodeSessionFiles(root).map((file) => ({ root, file })));
5635
6523
  const sessions = [];
6524
+ reportDiscoveryProgress(options, { current: 0, total: dbs.length, message: "reading OpenCode SQLite stores" });
6525
+ for (let index = 0; index < dbs.length; index++) {
6526
+ const dbSessions = readOpenCodeSqliteSessionsFromDb(dbs[index]);
6527
+ sessions.push(...dbSessions);
6528
+ reportDiscoveryProgress(options, {
6529
+ current: index + 1,
6530
+ total: dbs.length,
6531
+ message: `${dbSessions.length} SQLite sessions`,
6532
+ path: dbs[index]
6533
+ });
6534
+ }
6535
+ const seenSessionIds = new Set();
5636
6536
  reportDiscoveryProgress(options, { current: 0, total: files.length, message: "reading OpenCode storage" });
5637
6537
  for (let index = 0; index < files.length; index++) {
5638
6538
  const item = files[index];
5639
6539
  const session = parseOpenCodeSessionFile(item.file, item.root);
5640
- if (session) sessions.push(session);
6540
+ if (session) {
6541
+ sessions.push(session);
6542
+ seenSessionIds.add(session.sessionId.replace(/^opencode-/, ""));
6543
+ }
5641
6544
  reportDiscoveryProgress(options, {
5642
6545
  current: index + 1,
5643
6546
  total: files.length,
@@ -5645,28 +6548,69 @@ function readOpenCodeSessions(env = process.env, options = {}) {
5645
6548
  path: item.file
5646
6549
  });
5647
6550
  }
5648
- return dedupeStructuredSessions(sessions, "opencode");
5649
- }
6551
+ for (const root of roots) {
6552
+ for (const sessionId of openCodeMessageSessionIds(root)) {
6553
+ if (seenSessionIds.has(sessionId)) continue;
6554
+ const session = parseOpenCodeMessageOnlySession(root, sessionId);
6555
+ if (session) {
6556
+ sessions.push(session);
6557
+ seenSessionIds.add(sessionId);
6558
+ }
6559
+ }
6560
+ }
6561
+ return dedupeStructuredSessions(sessions, "opencode");
6562
+ }
6563
+
6564
+ function openCodeDataRoots(env = process.env) {
6565
+ const configured = env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR;
6566
+ if (configured) return existingUniquePaths([configured]);
6567
+ const home = env.HOME || os.homedir();
6568
+ const roots = [
6569
+ path.join(home, ".local", "share", "opencode"),
6570
+ path.join(home, "Library", "Application Support", "opencode"),
6571
+ path.join(home, ".local", "share", "ai.opencode.app"),
6572
+ path.join(home, "Library", "Application Support", "ai.opencode.app")
6573
+ ];
6574
+ const appData = env.APPDATA || env.LOCALAPPDATA || env.LocalAppData;
6575
+ if (appData) {
6576
+ roots.push(path.join(appData, "opencode"));
6577
+ roots.push(path.join(appData, "ai.opencode.app"));
6578
+ }
6579
+ return existingUniquePaths(roots);
6580
+ }
5650
6581
 
5651
6582
  function openCodeStorageRoots(env = process.env) {
5652
6583
  const explicit = envPathList(env.AGENTLOG_OPENCODE_STORAGE_ROOTS || env.AGENTLOG_OPENCODE_STORAGE_DIR);
5653
6584
  if (explicit.length) return existingUniquePaths(explicit);
5654
- const dataRoot = env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR || path.join(os.homedir(), ".local", "share", "opencode");
5655
- const roots = [path.join(dataRoot, "storage")];
5656
- const projectRoot = path.join(dataRoot, "project");
5657
- let entries = [];
5658
- try {
5659
- entries = fs.readdirSync(projectRoot, { withFileTypes: true });
5660
- } catch {
5661
- entries = [];
5662
- }
5663
- for (const entry of entries) {
5664
- if (entry.isDirectory()) roots.push(path.join(projectRoot, entry.name, "storage"));
6585
+ const dataRoots = openCodeDataRoots(env);
6586
+ const roots = [];
6587
+ for (const dataRoot of dataRoots) {
6588
+ roots.push(path.join(dataRoot, "storage"));
6589
+ const projectRoot = path.join(dataRoot, "project");
6590
+ let entries = [];
6591
+ try {
6592
+ entries = fs.readdirSync(projectRoot, { withFileTypes: true });
6593
+ } catch {
6594
+ entries = [];
6595
+ }
6596
+ for (const entry of entries) {
6597
+ if (entry.isDirectory()) roots.push(path.join(projectRoot, entry.name, "storage"));
6598
+ }
6599
+ if (path.basename(dataRoot) === "storage") roots.push(dataRoot);
5665
6600
  }
5666
- if (path.basename(dataRoot) === "storage") roots.push(dataRoot);
5667
6601
  return existingUniquePaths(roots);
5668
6602
  }
5669
6603
 
6604
+ function openCodeDatabaseFiles(env = process.env) {
6605
+ const explicit = envPathList(env.AGENTLOG_OPENCODE_DB || env.AGENTLOG_OPENCODE_DATABASE || env.OPENCODE_DB);
6606
+ if (explicit.length) return existingUniquePaths(explicit);
6607
+ if ((env.AGENTLOG_OPENCODE_STORAGE_ROOTS || env.AGENTLOG_OPENCODE_STORAGE_DIR) && !(env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR)) return [];
6608
+ return existingUniquePaths(openCodeDataRoots(env).flatMap((root) => [
6609
+ path.join(root, "opencode.db"),
6610
+ path.join(root, "storage", "opencode.db")
6611
+ ]));
6612
+ }
6613
+
5670
6614
  function openCodeSessionFiles(root) {
5671
6615
  const sessionRoot = path.join(root, "session");
5672
6616
  const files = [];
@@ -5676,6 +6620,179 @@ function openCodeSessionFiles(root) {
5676
6620
  return files.sort((a, b) => a.localeCompare(b));
5677
6621
  }
5678
6622
 
6623
+ function openCodeMessageSessionIds(root) {
6624
+ const messageRoot = path.join(root, "message");
6625
+ let entries = [];
6626
+ try {
6627
+ entries = fs.readdirSync(messageRoot, { withFileTypes: true });
6628
+ } catch {
6629
+ return [];
6630
+ }
6631
+ return entries
6632
+ .filter((entry) => entry.isDirectory())
6633
+ .map((entry) => entry.name)
6634
+ .filter(Boolean)
6635
+ .sort((a, b) => a.localeCompare(b));
6636
+ }
6637
+
6638
+ function readOpenCodeSqliteSessionsFromDb(dbPath) {
6639
+ if (!safeStat(dbPath)) return [];
6640
+ if (!sqliteTableExists(dbPath, "session") || !sqliteTableExists(dbPath, "message") || !sqliteTableExists(dbPath, "part")) return [];
6641
+ const sessionRows = readOpenCodeSqliteSessionRows(dbPath);
6642
+ const messageRows = readOpenCodeSqliteMessageRows(dbPath);
6643
+ const partRows = readOpenCodeSqlitePartRows(dbPath);
6644
+ const messagesBySession = groupRowsBy(messageRows, "session_id");
6645
+ const partsByMessage = groupRowsBy(partRows, "message_id");
6646
+ const storageRoot = path.join(path.dirname(dbPath), "storage");
6647
+ const sessions = [];
6648
+ for (const row of sessionRows) {
6649
+ const rows = messagesBySession.get(row.id) || [];
6650
+ const messages = stampMessages(
6651
+ dedupeAdjacentMessages(rows.flatMap((messageRow, index) => openCodeSqliteMessagesFromRow(messageRow, partsByMessage.get(messageRow.id) || [], index)))
6652
+ .sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
6653
+ "opencode-sqlite-history"
6654
+ );
6655
+ const diffFile = path.join(storageRoot, "session_diff", `${row.id}.json`);
6656
+ const diffMessage = openCodeDiffMessage(diffFile, messages[messages.length - 1]?.timestamp || toIso(row.time_updated || row.time_created));
6657
+ const finalMessages = diffMessage ? messages.concat(stampMessages([diffMessage], "opencode-sqlite-history")) : messages;
6658
+ if (!finalMessages.length) continue;
6659
+ const sourceFiles = [dbPath, safeStat(diffFile) ? diffFile : ""].filter(Boolean);
6660
+ const startedAt = toIso(row.time_created) || finalMessages[0]?.timestamp || new Date(safeStat(dbPath)?.mtimeMs || Date.now()).toISOString();
6661
+ const endedAt = toIso(row.time_updated) || finalMessages[finalMessages.length - 1]?.timestamp || startedAt;
6662
+ const cwd = firstString(row.directory, row.path, row.project_worktree, openCodeCwdFromMessages(finalMessages));
6663
+ sessions.push({
6664
+ sessionId: `opencode-${row.id}`,
6665
+ title: firstString(row.title, row.slug, clineTitle(finalMessages), row.id),
6666
+ cwd,
6667
+ startedAt,
6668
+ endedAt,
6669
+ messages: finalMessages,
6670
+ sourcePath: `${dbPath}#${row.id}`,
6671
+ sourceFiles,
6672
+ sourceType: "opencode-sqlite-history",
6673
+ fingerprint: openCodeSqliteSessionFingerprint(dbPath, row, rows, sourceFiles),
6674
+ detailKey: "sqliteSessions",
6675
+ sessionSummary: {
6676
+ projectId: row.project_id || undefined,
6677
+ parentId: row.parent_id || undefined,
6678
+ workspaceId: row.workspace_id || undefined,
6679
+ slug: row.slug || undefined,
6680
+ version: row.version || undefined,
6681
+ agent: row.agent || undefined,
6682
+ model: row.model || undefined,
6683
+ projectName: row.project_name || undefined,
6684
+ projectWorktree: row.project_worktree || undefined
6685
+ }
6686
+ });
6687
+ }
6688
+ return sessions;
6689
+ }
6690
+
6691
+ function readOpenCodeSqliteSessionRows(dbPath) {
6692
+ return readSqliteJson(
6693
+ dbPath,
6694
+ [
6695
+ "select s.id, s.project_id, s.parent_id, s.slug, s.directory, s.title, s.version,",
6696
+ "s.share_url, s.time_created, s.time_updated, s.time_archived, s.workspace_id, s.path, s.agent, s.model,",
6697
+ "p.worktree as project_worktree, p.name as project_name",
6698
+ "from session s left join project p on p.id = s.project_id",
6699
+ "where coalesce(s.time_archived, 0) = 0",
6700
+ "order by s.time_updated desc, s.id"
6701
+ ].join(" "),
6702
+ "OpenCode SQLite sessions"
6703
+ );
6704
+ }
6705
+
6706
+ function readOpenCodeSqliteMessageRows(dbPath) {
6707
+ return readSqliteJson(
6708
+ dbPath,
6709
+ "select id, session_id, time_created, time_updated, data from message order by session_id, time_created, id",
6710
+ "OpenCode SQLite messages"
6711
+ );
6712
+ }
6713
+
6714
+ function readOpenCodeSqlitePartRows(dbPath) {
6715
+ return readSqliteJson(
6716
+ dbPath,
6717
+ "select id, message_id, session_id, time_created, time_updated, data from part order by session_id, message_id, time_created, id",
6718
+ "OpenCode SQLite parts"
6719
+ );
6720
+ }
6721
+
6722
+ function openCodeSqliteMessagesFromRow(row, partRows, index) {
6723
+ const data = parseJsonObject(row.data);
6724
+ const parts = (partRows || []).map((partRow) => ({
6725
+ id: partRow.id,
6726
+ messageID: partRow.message_id,
6727
+ messageId: partRow.message_id,
6728
+ sessionID: partRow.session_id,
6729
+ sessionId: partRow.session_id,
6730
+ timeCreated: partRow.time_created,
6731
+ timeUpdated: partRow.time_updated,
6732
+ ...parseJsonObject(partRow.data)
6733
+ }));
6734
+ const role = normalizeEventRole(data.role || data.type || data.actor) || openCodeRoleFromParts(parts);
6735
+ const timestamp = toIso(data.time?.created || data.createdAt || data.created_at || row.time_created) || offsetTimestamp(new Date(Number(row.time_created) || Date.now()).toISOString(), index);
6736
+ const text = firstString(data.content, data.text, data.message, openCodePartText(parts));
6737
+ const toolCalls = parts.map(openCodeToolCall).filter(Boolean);
6738
+ const toolResults = parts.map(openCodeToolResult).filter(Boolean);
6739
+ const messageId = firstString(row.id, data.id, data.messageID, data.messageId);
6740
+ const usage = openCodeUsageFromMessageData(data, parts);
6741
+ const result = [];
6742
+ if (role && (text || toolCalls.length)) {
6743
+ result.push({
6744
+ role,
6745
+ content: text,
6746
+ timestamp,
6747
+ metadata: {
6748
+ provider: "opencode",
6749
+ messageId,
6750
+ parentMessageId: firstString(data.parentID, data.parentId) || undefined,
6751
+ requestId: usage ? messageId : undefined,
6752
+ model: firstString(data.modelID, data.modelId, data.model?.modelID, data.model?.modelId, data.model) || undefined,
6753
+ providerId: firstString(data.providerID, data.providerId, data.model?.providerID, data.model?.providerId) || undefined,
6754
+ mode: firstString(data.mode) || undefined,
6755
+ agent: firstString(data.agent) || undefined,
6756
+ finish: firstString(data.finish) || undefined,
6757
+ cwd: firstString(data.path?.cwd) || undefined,
6758
+ root: firstString(data.path?.root) || undefined,
6759
+ usage: usage || undefined,
6760
+ cost: Number.isFinite(Number(data.cost)) ? Number(data.cost) : undefined,
6761
+ toolCalls: toolCalls.length ? toolCalls : undefined
6762
+ }
6763
+ });
6764
+ }
6765
+ for (const toolResult of toolResults) {
6766
+ result.push({ role: "tool", content: toolResult.output, timestamp, metadata: { provider: "opencode", messageId, toolResult } });
6767
+ }
6768
+ return result;
6769
+ }
6770
+
6771
+ function openCodeUsageFromMessageData(data, parts = []) {
6772
+ const finishPart = (parts || []).find((part) => String(part?.type || "").toLowerCase() === "step-finish" && part.tokens && typeof part.tokens === "object");
6773
+ const tokens = data?.tokens && typeof data.tokens === "object" ? data.tokens : finishPart?.tokens;
6774
+ if (!tokens || typeof tokens !== "object") return null;
6775
+ const usage = {
6776
+ input_tokens: Number.isFinite(Number(tokens.input)) ? Number(tokens.input) : undefined,
6777
+ output_tokens: Number.isFinite(Number(tokens.output)) ? Number(tokens.output) : undefined,
6778
+ total_tokens: Number.isFinite(Number(tokens.total)) ? Number(tokens.total) : undefined,
6779
+ reasoning_tokens: Number.isFinite(Number(tokens.reasoning)) ? Number(tokens.reasoning) : undefined,
6780
+ cache_creation_input_tokens: Number.isFinite(Number(tokens.cache?.write)) ? Number(tokens.cache.write) : undefined,
6781
+ cache_read_input_tokens: Number.isFinite(Number(tokens.cache?.read)) ? Number(tokens.cache.read) : undefined
6782
+ };
6783
+ return Object.values(usage).some((value) => value !== undefined) ? usage : null;
6784
+ }
6785
+
6786
+ function openCodeSqliteSessionFingerprint(dbPath, row, messageRows, sourceFiles) {
6787
+ const sessionRevision = [
6788
+ row.id,
6789
+ row.time_updated || row.time_created || "",
6790
+ messageRows.length,
6791
+ messageRows.map((message) => `${message.id}:${message.time_updated || message.time_created || ""}`).join("|")
6792
+ ].join(":");
6793
+ return `${fingerprintPrefix("opencode-sqlite-history")}:${structuredSessionFingerprint({ sourcePath: dbPath, sourceFiles })}:${hashId(sessionRevision)}`;
6794
+ }
6795
+
5679
6796
  function parseOpenCodeSessionFile(file, storageRoot) {
5680
6797
  const info = readJsonMaybe(file, null);
5681
6798
  if (!info || typeof info !== "object") return null;
@@ -5717,6 +6834,63 @@ function parseOpenCodeSessionFile(file, storageRoot) {
5717
6834
  };
5718
6835
  }
5719
6836
 
6837
+ function parseOpenCodeMessageOnlySession(storageRoot, sessionId) {
6838
+ if (!sessionId) return null;
6839
+ const messageFiles = openCodeMessageFiles(storageRoot, sessionId);
6840
+ const parsedMessages = messageFiles.flatMap((messageFile, index) => openCodeMessagesFromFile(messageFile, storageRoot, index));
6841
+ const diffFile = path.join(storageRoot, "session_diff", `${sessionId}.json`);
6842
+ const diffMessage = openCodeDiffMessage(diffFile, parsedMessages[parsedMessages.length - 1]?.timestamp);
6843
+ const messages = stampMessages(
6844
+ dedupeAdjacentMessages(parsedMessages.concat(diffMessage ? [diffMessage] : [])).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
6845
+ "opencode-history"
6846
+ );
6847
+ if (!messages.length) return null;
6848
+ const sourceFiles = [
6849
+ ...messageFiles,
6850
+ ...messageFiles.flatMap((messageFile) => openCodePartFiles(storageRoot, firstString(readJsonMaybe(messageFile, {})?.id, path.basename(messageFile, ".json")))),
6851
+ safeStat(diffFile) ? diffFile : ""
6852
+ ].filter(Boolean);
6853
+ const startedAt = messages[0]?.timestamp || "";
6854
+ const endedAt = messages[messages.length - 1]?.timestamp || startedAt;
6855
+ const cwd = openCodeCwdFromMessages(messages) || openCodeProjectPathFromStorage(storageRoot);
6856
+ return {
6857
+ sessionId: `opencode-${sessionId}`,
6858
+ title: clineTitle(messages) || sessionId,
6859
+ cwd,
6860
+ startedAt,
6861
+ endedAt,
6862
+ messages,
6863
+ sourcePath: path.join(storageRoot, "message", sessionId),
6864
+ sourceFiles,
6865
+ sourceType: "opencode-history",
6866
+ fingerprint: `${fingerprintPrefix("opencode-history")}:${structuredSessionFingerprint({ sourcePath: path.join(storageRoot, "message", sessionId), sourceFiles })}`,
6867
+ detailKey: "sessions"
6868
+ };
6869
+ }
6870
+
6871
+ function openCodeCwdFromMessages(messages) {
6872
+ for (const message of messages || []) {
6873
+ const cwd = firstString(message?.metadata?.cwd, message?.metadata?.directory, message?.metadata?.projectPath);
6874
+ if (cwd) return cwd;
6875
+ const fromContent = openCodePathFromText(message?.content);
6876
+ if (fromContent) return fromContent;
6877
+ for (const toolCall of message?.metadata?.toolCalls || []) {
6878
+ const fromTool = openCodePathFromText(JSON.stringify(toolCall.arguments || toolCall.argument || ""));
6879
+ if (fromTool) return fromTool;
6880
+ }
6881
+ }
6882
+ return "";
6883
+ }
6884
+
6885
+ function openCodePathFromText(text) {
6886
+ const value = String(text || "");
6887
+ const cdMatch = value.match(/\bcd\s+["']?([^"'\s]+)["']?/);
6888
+ const candidate = cdMatch ? cdMatch[1] : value.match(/(?:working\s+)?directory[:=\s]+["']?([^"'\s]+)["']?/i)?.[1];
6889
+ if (!candidate) return "";
6890
+ const expanded = candidate.startsWith("~/") ? path.join(os.homedir(), candidate.slice(2)) : candidate;
6891
+ return path.isAbsolute(expanded) && fs.existsSync(expanded) ? expanded : "";
6892
+ }
6893
+
5720
6894
  function openCodeProjectInfo(storageRoot, projectId) {
5721
6895
  const file = projectId ? path.join(storageRoot, "project", `${projectId}.json`) : "";
5722
6896
  const data = file ? readJsonMaybe(file, {}) : {};
@@ -5887,10 +7061,14 @@ function openCodeDiffText(value) {
5887
7061
  if (typeof value === "string") return value.trim();
5888
7062
  if (Array.isArray(value)) return value.map(openCodeDiffText).filter(Boolean).join("\n");
5889
7063
  if (typeof value !== "object") return String(value);
7064
+ const file = firstString(value.path, value.file, value.filename);
5890
7065
  for (const key of ["diff", "patch", "text", "content"]) {
5891
- if (typeof value[key] === "string" && value[key].trim()) return value[key].trim();
7066
+ if (typeof value[key] === "string" && value[key].trim()) {
7067
+ const body = value[key].trim();
7068
+ if (file && !/^diff --git\s/m.test(body)) return `diff --git a/${file} b/${file}\n${body}`;
7069
+ return body;
7070
+ }
5892
7071
  }
5893
- const file = firstString(value.path, value.file, value.filename);
5894
7072
  const body = openCodeDiffText(value.hunks || value.changes || value.edits);
5895
7073
  if (file && body) return `diff --git a/${file} b/${file}\n${body}`;
5896
7074
  return "";
@@ -6024,6 +7202,7 @@ function aiderMarkdownMessages(text, fallbackTime) {
6024
7202
  }
6025
7203
 
6026
7204
  function dedupeStructuredSessions(sessions, provider) {
7205
+ if (provider === "opencode") return dedupeOpenCodeSessions(sessions);
6027
7206
  const seen = new Set();
6028
7207
  const result = [];
6029
7208
  for (const session of sessions) {
@@ -6035,6 +7214,30 @@ function dedupeStructuredSessions(sessions, provider) {
6035
7214
  return result;
6036
7215
  }
6037
7216
 
7217
+ function dedupeOpenCodeSessions(sessions) {
7218
+ const bySessionId = new Map();
7219
+ const order = [];
7220
+ for (const session of sessions || []) {
7221
+ const key = `opencode:${session.sessionId}`;
7222
+ const existing = bySessionId.get(key);
7223
+ if (!existing) {
7224
+ bySessionId.set(key, session);
7225
+ order.push(key);
7226
+ continue;
7227
+ }
7228
+ const preferred = openCodeSourceRank(session.sourceType) > openCodeSourceRank(existing.sourceType) ? session : existing;
7229
+ preferred.sourceFiles = existingUniquePaths([...(existing.sourceFiles || []), ...(session.sourceFiles || [])]);
7230
+ bySessionId.set(key, preferred);
7231
+ }
7232
+ return order.map((key) => bySessionId.get(key)).filter(Boolean);
7233
+ }
7234
+
7235
+ function openCodeSourceRank(sourceType) {
7236
+ if (sourceType === "opencode-sqlite-history") return 3;
7237
+ if (sourceType === "opencode-history") return 2;
7238
+ return 1;
7239
+ }
7240
+
6038
7241
  function readDevinSessions(env = process.env, options = {}) {
6039
7242
  const db = devinSessionsDb(env);
6040
7243
  reportDiscoveryProgress(options, { current: 0, total: fs.existsSync(db) ? 1 : 0, message: "opening Devin sessions database", path: db });
@@ -6150,6 +7353,9 @@ function devinMessagesFromNode(row) {
6150
7353
  if (role === "system" || devinIsContextUserMessage(role, content)) return [];
6151
7354
  const toolResult = role === "tool" ? devinNormalizeToolResult(content, data) : null;
6152
7355
  const body = role === "tool" ? content : devinVisibleContent(content, toolCalls);
7356
+ const usage = role === "assistant" ? devinUsage(data) : null;
7357
+ const model = role === "assistant" ? firstString(data.metadata?.generation_model, data.model, data.model_id, data.modelId) : "";
7358
+ const requestId = role === "assistant" ? firstString(data.metadata?.request_id, data.request_id, data.message_id) : "";
6153
7359
  if (!body && !toolCalls.length && !toolResult) return [];
6154
7360
  return [
6155
7361
  {
@@ -6162,6 +7368,9 @@ function devinMessagesFromNode(row) {
6162
7368
  parentNodeId: row.parent_node_id ?? undefined,
6163
7369
  toolCalls: toolCalls.length ? toolCalls.map(devinPublicToolCall) : undefined,
6164
7370
  toolResult: toolResult || undefined,
7371
+ usage: usage || undefined,
7372
+ model: model || undefined,
7373
+ requestId: requestId || undefined,
6165
7374
  sourceType: "devin-cli-history",
6166
7375
  parserVersion: parserVersionForSource("devin-cli-history")
6167
7376
  }
@@ -6169,6 +7378,25 @@ function devinMessagesFromNode(row) {
6169
7378
  ];
6170
7379
  }
6171
7380
 
7381
+ function devinUsage(message = {}) {
7382
+ const metrics = message?.metadata?.metrics;
7383
+ if (!metrics || typeof metrics !== "object") return null;
7384
+ const inputTokens = numericValue(metrics.input_tokens, metrics.inputTokens, metrics.prompt_tokens, metrics.promptTokens);
7385
+ const outputTokens = numericValue(metrics.output_tokens, metrics.outputTokens, metrics.completion_tokens, metrics.completionTokens, message?.metadata?.num_tokens);
7386
+ const cacheCreationInputTokens = numericValue(metrics.cache_creation_tokens, metrics.cache_creation_input_tokens, metrics.cacheCreationInputTokens);
7387
+ const cacheReadInputTokens = numericValue(metrics.cache_read_tokens, metrics.cache_read_input_tokens, metrics.cacheReadInputTokens);
7388
+ if ([inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens].every((value) => value == null)) return null;
7389
+ const usage = {
7390
+ inputTokens: inputTokens ?? undefined,
7391
+ outputTokens: outputTokens ?? undefined,
7392
+ cacheCreationInputTokens: cacheCreationInputTokens ?? undefined,
7393
+ cacheReadInputTokens: cacheReadInputTokens ?? undefined
7394
+ };
7395
+ const totalTokens = [inputTokens, outputTokens].reduce((sum, value) => sum + (value || 0), 0);
7396
+ if (totalTokens) usage.totalTokens = totalTokens;
7397
+ return usage;
7398
+ }
7399
+
6172
7400
  function devinVisibleContent(content, toolCalls) {
6173
7401
  const value = String(content || "").trim();
6174
7402
  if (toolCalls.length && /^none$/i.test(value)) return "";
@@ -6292,8 +7520,9 @@ function readGeminiCliSessions(options = {}, env = process.env) {
6292
7520
  reportDiscoveryProgress(options, { current: 0, total: files.length, message: "reading saved chats and checkpoints" });
6293
7521
  for (let index = 0; index < files.length; index++) {
6294
7522
  const file = files[index];
6295
- const parsed = parseGeminiCliHistoryFile(file);
6296
- if (parsed) sessions.push(parsed);
7523
+ for (const parsed of asArray(parseGeminiCliHistoryFile(file))) {
7524
+ if (parsed) sessions.push(parsed);
7525
+ }
6297
7526
  reportDiscoveryProgress(options, { current: index + 1, total: files.length, message: `${sessions.length} importable`, path: file });
6298
7527
  }
6299
7528
  return coalesceGeminiCliSessions(sessions);
@@ -6476,17 +7705,26 @@ function parseGeminiCliHistoryFile(file) {
6476
7705
  const stat = safeStat(file);
6477
7706
  const fallbackTime = new Date(stat?.mtimeMs || Date.now()).toISOString();
6478
7707
  const ext = path.extname(file).toLowerCase();
6479
- let parsed = null;
7708
+ let parsedItems = [];
6480
7709
  try {
6481
- if (ext === ".jsonl") parsed = parseGeminiCliJsonl(fs.readFileSync(file, "utf8"), { fallbackTime });
6482
- else if (ext === ".md" || ext === ".markdown") parsed = parseMarkdownChatFile(file, "gemini-cli", fallbackTime);
6483
- else parsed = parseGeminiCliJson(JSON.parse(fs.readFileSync(file, "utf8")), { fallbackTime });
7710
+ if (ext === ".jsonl") parsedItems = parseGeminiCliJsonlSessions(fs.readFileSync(file, "utf8"), { fallbackTime });
7711
+ else if (ext === ".md" || ext === ".markdown") parsedItems = [parseMarkdownChatFile(file, "gemini-cli", fallbackTime)].filter(Boolean);
7712
+ else parsedItems = parseGeminiCliJsonSessions(JSON.parse(fs.readFileSync(file, "utf8")), { fallbackTime });
6484
7713
  } catch {
6485
7714
  return null;
6486
7715
  }
7716
+ const multiSessionSource = parsedItems.length > 1;
7717
+ return parsedItems
7718
+ .map((parsed, index) => geminiCliParsedSession(file, stat, fallbackTime, ext, parsed, { multiSessionSource, index }))
7719
+ .filter(Boolean);
7720
+ }
7721
+
7722
+ function geminiCliParsedSession(file, stat, fallbackTime, ext, parsed, options = {}) {
6487
7723
  if (!parsed || !parsed.messages.length) return null;
6488
7724
  const cwd = parsed.cwd || geminiProjectCwd(file);
6489
7725
  const sessionId = parsed.sessionId || stableSessionId("gemini_cli", file, parsed.startedAt || fallbackTime, parsed.messages);
7726
+ const sourceFingerprint = fileFingerprint(file, stat);
7727
+ const fingerprint = options.multiSessionSource ? `${sourceFingerprint}:session:${hashId(sessionId || options.index)}` : sourceFingerprint;
6490
7728
  return {
6491
7729
  sessionId,
6492
7730
  title: parsed.title || path.basename(file, ext),
@@ -6497,8 +7735,9 @@ function parseGeminiCliHistoryFile(file) {
6497
7735
  sourcePath: file,
6498
7736
  sourceFiles: [file],
6499
7737
  sourceType: "gemini-cli-history",
6500
- fingerprint: `${fingerprintPrefix("gemini-cli-history")}:${fileFingerprint(file, stat)}`,
6501
- detailKey: "files"
7738
+ fingerprint: `${fingerprintPrefix("gemini-cli-history")}:${fingerprint}`,
7739
+ detailKey: "files",
7740
+ sessionSummary: parsed.sessionSummary || undefined
6502
7741
  };
6503
7742
  }
6504
7743
 
@@ -6526,6 +7765,121 @@ function parseMarkdownChatFile(file, source, fallbackTime) {
6526
7765
  };
6527
7766
  }
6528
7767
 
7768
+ function readWindsurfTrajectoryExport(target, options = {}) {
7769
+ const files = windsurfTrajectoryFiles(target);
7770
+ const sessions = [];
7771
+ reportDiscoveryProgress(options, { current: 0, total: files.length, message: "reading Windsurf trajectory exports" });
7772
+ for (let index = 0; index < files.length; index++) {
7773
+ const file = files[index];
7774
+ const session = parseWindsurfTrajectoryFile(file);
7775
+ if (session) sessions.push(session);
7776
+ reportDiscoveryProgress(options, { current: index + 1, total: files.length, message: `${sessions.length} importable`, path: file });
7777
+ }
7778
+ return sessions;
7779
+ }
7780
+
7781
+ function windsurfTrajectoryFiles(target) {
7782
+ const resolved = path.resolve(target);
7783
+ const stat = safeStat(resolved);
7784
+ if (!stat) throw new Error(`Cannot read Windsurf trajectory export path ${target}`);
7785
+ if (stat.isFile()) return isMarkdownFile(resolved) ? [resolved] : [];
7786
+ if (!stat.isDirectory()) return [];
7787
+ const files = [];
7788
+ collectFiles(resolved, (file) => {
7789
+ if (isMarkdownFile(file)) files.push(file);
7790
+ });
7791
+ return files.sort((a, b) => a.localeCompare(b));
7792
+ }
7793
+
7794
+ function isMarkdownFile(file) {
7795
+ return [".md", ".markdown"].includes(path.extname(file).toLowerCase());
7796
+ }
7797
+
7798
+ function parseWindsurfTrajectoryFile(file) {
7799
+ const stat = safeStat(file);
7800
+ const fallbackTime = new Date(stat?.mtimeMs || Date.now()).toISOString();
7801
+ let text = "";
7802
+ try {
7803
+ text = fs.readFileSync(file, "utf8");
7804
+ } catch {
7805
+ return null;
7806
+ }
7807
+ if (!looksLikeWindsurfTrajectory(text)) return null;
7808
+ const messages = windsurfTrajectoryMessages(text, fallbackTime);
7809
+ if (!messages.length) return null;
7810
+ const stampedMessages = stampMessages(messages, "windsurf-trajectory-export");
7811
+ const startedAt = stampedMessages[0]?.timestamp || fallbackTime;
7812
+ const endedAt = stampedMessages[stampedMessages.length - 1]?.timestamp || fallbackTime;
7813
+ const cwd = inferCwdFromMarkdownText(text);
7814
+ return {
7815
+ sessionId: stableSessionId("windsurf", file, startedAt, stampedMessages),
7816
+ title: windsurfTrajectoryTitle(text, stampedMessages, file),
7817
+ cwd,
7818
+ scopeCanonical: cwd ? "" : uncategorizedScope("windsurf"),
7819
+ startedAt,
7820
+ endedAt,
7821
+ messages: stampedMessages,
7822
+ sourcePath: file,
7823
+ sourceFiles: [file],
7824
+ sourceType: "windsurf-trajectory-export",
7825
+ fingerprint: `${fingerprintPrefix("windsurf-trajectory-export")}:${fileFingerprint(file, stat)}:${hashId(stampedMessages.map((message) => `${message.role}:${message.content}`).join("\n"))}`,
7826
+ detailKey: "exports"
7827
+ };
7828
+ }
7829
+
7830
+ function looksLikeWindsurfTrajectory(text) {
7831
+ return /^#\s+Cascade Chat Conversation\s*$/im.test(String(text || "")) && /^###\s+(User Input|Planner Response|Assistant Response|Cascade Response)\s*$/im.test(String(text || ""));
7832
+ }
7833
+
7834
+ function windsurfTrajectoryMessages(text, fallbackTime) {
7835
+ const messages = [];
7836
+ let role = "";
7837
+ let heading = "";
7838
+ let current = [];
7839
+ const flush = () => {
7840
+ const content = current.join("\n").trim();
7841
+ if (role && content) {
7842
+ messages.push({
7843
+ role,
7844
+ content,
7845
+ timestamp: new Date(new Date(fallbackTime).getTime() + messages.length).toISOString(),
7846
+ metadata: { source: "windsurf-trajectory-export", heading }
7847
+ });
7848
+ }
7849
+ current = [];
7850
+ };
7851
+ for (const line of String(text || "").split(/\r?\n/)) {
7852
+ const match = line.match(/^###\s+(.+?)\s*$/);
7853
+ const nextRole = match ? windsurfTrajectoryRole(match[1]) : "";
7854
+ if (match && nextRole) {
7855
+ flush();
7856
+ role = nextRole;
7857
+ heading = match[1].trim();
7858
+ } else if (role) {
7859
+ current.push(line);
7860
+ }
7861
+ }
7862
+ flush();
7863
+ return messages;
7864
+ }
7865
+
7866
+ function windsurfTrajectoryRole(label) {
7867
+ const value = String(label || "").trim().toLowerCase();
7868
+ if (/(user|human).*(input|prompt|message)?/.test(value)) return "user";
7869
+ if (/(planner|assistant|cascade|agent|model).*(response|output|message)?/.test(value)) return "assistant";
7870
+ if (/system/.test(value)) return "system";
7871
+ if (/tool/.test(value)) return "tool";
7872
+ return "";
7873
+ }
7874
+
7875
+ function windsurfTrajectoryTitle(text, messages, file) {
7876
+ const title = markdownTitle(text);
7877
+ if (title && !/^Cascade Chat Conversation$/i.test(title)) return title;
7878
+ const firstUser = messages.find((message) => message.role === "user");
7879
+ const line = firstLine(firstUser?.content);
7880
+ return line ? line.slice(0, 120) : path.basename(file, path.extname(file));
7881
+ }
7882
+
6529
7883
  function readWindsurfSessions(options = {}) {
6530
7884
  // Kept for future decoder work only. Windsurf's current Cascade transcript
6531
7885
  // stores are encrypted binary files, so this source is disabled from public
@@ -6560,8 +7914,9 @@ function readWindsurfSessions(options = {}) {
6560
7914
  }
6561
7915
 
6562
7916
  function readAntigravitySessions(options = {}, env = process.env) {
6563
- const roots = [path.join(os.homedir(), ".gemini", "antigravity", "brain")];
6564
- const sessions = readMarkdownArtifactSessions({
7917
+ const home = antigravityHome(env);
7918
+ const roots = [path.join(home, "brain")];
7919
+ const artifactSessions = readMarkdownArtifactSessions({
6565
7920
  provider: "antigravity",
6566
7921
  roots,
6567
7922
  sourceType: "antigravity-brain",
@@ -6569,7 +7924,11 @@ function readAntigravitySessions(options = {}, env = process.env) {
6569
7924
  artifactNames: ["task.md", "implementation_plan.md", "walkthrough.md", "plan.md"],
6570
7925
  options
6571
7926
  });
6572
- const binaryCount = countFiles(path.join(os.homedir(), ".gemini", "antigravity", "conversations"), (file) => file.endsWith(".pb"));
7927
+ const artifactSessionIds = new Set(artifactSessions.map((session) => session.sessionId));
7928
+ const trajectorySessions = readAntigravityTrajectorySummarySessions(options, env)
7929
+ .filter((session) => !artifactSessionIds.has(session.sessionId));
7930
+ const sessions = [...artifactSessions, ...trajectorySessions];
7931
+ const binaryCount = countFiles(path.join(home, "conversations"), (file) => file.endsWith(".pb"));
6573
7932
  if (binaryCount && sessions.length) sessions[0].binaryCount = binaryCount;
6574
7933
  else if (binaryCount) {
6575
7934
  sessions.push({
@@ -6580,7 +7939,7 @@ function readAntigravitySessions(options = {}, env = process.env) {
6580
7939
  startedAt: new Date().toISOString(),
6581
7940
  endedAt: new Date().toISOString(),
6582
7941
  messages: [],
6583
- sourcePath: path.join(os.homedir(), ".gemini", "antigravity", "conversations"),
7942
+ sourcePath: path.join(home, "conversations"),
6584
7943
  sourceType: "antigravity-protobuf",
6585
7944
  binaryCount,
6586
7945
  detailKey: "tasks"
@@ -6589,6 +7948,205 @@ function readAntigravitySessions(options = {}, env = process.env) {
6589
7948
  return sessions;
6590
7949
  }
6591
7950
 
7951
+ function antigravityHome(env = process.env) {
7952
+ return env.AGENTLOG_ANTIGRAVITY_HOME_DIR || path.join(geminiHome(env), "antigravity");
7953
+ }
7954
+
7955
+ function antigravityGlobalStateDbs(env = process.env) {
7956
+ const explicit = envPathList(env.AGENTLOG_ANTIGRAVITY_GLOBAL_STORAGE_DB || env.AGENTLOG_ANTIGRAVITY_GLOBAL_STATE_DB);
7957
+ if (explicit.length) return existingUniquePaths(explicit);
7958
+ const appRoots = envPathList(env.AGENTLOG_ANTIGRAVITY_APP_SUPPORT_DIR);
7959
+ if (!appRoots.length) {
7960
+ appRoots.push(
7961
+ path.join(os.homedir(), "Library", "Application Support", "Antigravity"),
7962
+ path.join(os.homedir(), ".config", "Antigravity"),
7963
+ path.join(os.homedir(), "AppData", "Roaming", "Antigravity")
7964
+ );
7965
+ }
7966
+ return existingUniquePaths([
7967
+ ...explicit,
7968
+ ...appRoots.flatMap((root) => [
7969
+ path.join(root, "User", "globalStorage", "state.vscdb"),
7970
+ path.join(root, "User", "globalStorage", "state.vscdb.backup")
7971
+ ])
7972
+ ]);
7973
+ }
7974
+
7975
+ function readAntigravityTrajectorySummarySessions(options = {}, env = process.env) {
7976
+ const dbs = antigravityGlobalStateDbs(env);
7977
+ if (!dbs.length) return [];
7978
+ const sessions = [];
7979
+ const seen = new Set();
7980
+ reportDiscoveryProgress(options, { current: 0, total: dbs.length, message: "reading trajectory summaries" });
7981
+ for (let index = 0; index < dbs.length; index++) {
7982
+ const db = dbs[index];
7983
+ try {
7984
+ for (const session of antigravityTrajectorySummarySessionsFromDb(db, env)) {
7985
+ if (seen.has(session.sessionId)) continue;
7986
+ seen.add(session.sessionId);
7987
+ sessions.push(session);
7988
+ }
7989
+ } catch (error) {
7990
+ reportDiscoveryProgress(options, { current: index + 1, total: dbs.length, message: error.message, path: db });
7991
+ continue;
7992
+ }
7993
+ reportDiscoveryProgress(options, { current: index + 1, total: dbs.length, message: `${sessions.length} summaries`, path: db });
7994
+ }
7995
+ if (sessions.length) sessions[0].stateDbCount = dbs.length;
7996
+ return sessions.sort((a, b) => String(a.startedAt).localeCompare(String(b.startedAt)) || a.sessionId.localeCompare(b.sessionId));
7997
+ }
7998
+
7999
+ function antigravityTrajectorySummarySessionsFromDb(db, env = process.env) {
8000
+ if (!fs.existsSync(db)) return [];
8001
+ const rows = readSqliteJson(
8002
+ db,
8003
+ "SELECT value FROM ItemTable WHERE key='antigravityUnifiedStateSync.trajectorySummaries'",
8004
+ "Antigravity global state"
8005
+ );
8006
+ const encoded = firstString(...rows.map((row) => row.value));
8007
+ if (!encoded) return [];
8008
+ return parseAntigravityTrajectorySummaries(encoded, { db, env });
8009
+ }
8010
+
8011
+ function parseAntigravityTrajectorySummaries(encoded, context = {}) {
8012
+ const outer = decodeProtoMessage(bufferFromBase64(encoded));
8013
+ const sessions = [];
8014
+ for (const entry of outer.filter((field) => field.number === 1 && field.wireType === 2)) {
8015
+ const session = antigravityTrajectorySummarySession(entry.bytes, context);
8016
+ if (session) sessions.push(session);
8017
+ }
8018
+ return sessions;
8019
+ }
8020
+
8021
+ function antigravityTrajectorySummarySession(entryBytes, context = {}) {
8022
+ const entry = decodeProtoMessage(entryBytes);
8023
+ const id = protoString(firstProtoField(entry, 1));
8024
+ const wrapper = firstProtoField(entry, 2);
8025
+ const summaryEnvelope = wrapper?.bytes ? decodeProtoMessage(wrapper.bytes) : [];
8026
+ const summaryEncoded = protoString(firstProtoField(summaryEnvelope, 1));
8027
+ if (!id || !summaryEncoded) return null;
8028
+
8029
+ const sourceType = "antigravity-trajectory-summary";
8030
+ const summaryBytes = bufferFromBase64(summaryEncoded);
8031
+ const fields = decodeProtoMessage(summaryBytes);
8032
+ const prompt = cleanAntigravityText(protoString(firstProtoField(fields, 1)));
8033
+ if (!prompt) return null;
8034
+ const startedAt = antigravitySummaryTimestamp(fields, 7) || antigravitySummaryTimestamp(fields, 3) || antigravitySummaryTimestamp(fields, 10) || new Date().toISOString();
8035
+ const endedAt = antigravitySummaryTimestamp(fields, 10) || antigravitySummaryTimestamp(fields, 3) || startedAt;
8036
+ const cwd = antigravitySummaryCwd(fields);
8037
+ const assistantSummary = antigravityAssistantSummary(fields, prompt);
8038
+ const messages = [
8039
+ {
8040
+ role: "user",
8041
+ content: prompt,
8042
+ timestamp: startedAt,
8043
+ metadata: {
8044
+ provider: "antigravity",
8045
+ source: sourceType,
8046
+ providerConversationId: id,
8047
+ partialSummary: true
8048
+ }
8049
+ }
8050
+ ];
8051
+ if (assistantSummary) {
8052
+ messages.push({
8053
+ role: "assistant",
8054
+ content: `# Antigravity Trajectory Summary\n\n${assistantSummary}`,
8055
+ timestamp: endedAt,
8056
+ metadata: {
8057
+ provider: "antigravity",
8058
+ source: sourceType,
8059
+ providerConversationId: id,
8060
+ partialSummary: true
8061
+ }
8062
+ });
8063
+ }
8064
+ const db = context.db || "";
8065
+ return {
8066
+ sessionId: `antigravity-${id}`,
8067
+ providerConversationId: id,
8068
+ title: antigravitySummaryTitle(prompt),
8069
+ cwd,
8070
+ scopeCanonical: cwd ? "" : "antigravity/uncategorized",
8071
+ startedAt,
8072
+ endedAt,
8073
+ messages: stampMessages(messages, sourceType),
8074
+ sourcePath: `antigravity-state:${db || "globalStorage/state.vscdb"}#trajectorySummaries/${id}`,
8075
+ sourceFiles: [],
8076
+ sourceType,
8077
+ fingerprint: `${fingerprintPrefix(sourceType)}:${db}:${id}:${hashId(summaryEncoded)}`,
8078
+ detailKey: "trajectorySummaries",
8079
+ partialSummary: true,
8080
+ rawReferences: db
8081
+ ? [
8082
+ {
8083
+ originalPath: db,
8084
+ entryPath: `ItemTable/antigravityUnifiedStateSync.trajectorySummaries/${id}`,
8085
+ conversationId: id,
8086
+ note: "Antigravity trajectory summary state row. The global state DB is referenced but not copied because it can contain auth tokens."
8087
+ }
8088
+ ]
8089
+ : undefined,
8090
+ sessionSummary: {
8091
+ source: sourceType,
8092
+ partial: true,
8093
+ note: "Imported from Antigravity trajectory summary metadata. Full binary conversation bodies are not decoded."
8094
+ }
8095
+ };
8096
+ }
8097
+
8098
+ function antigravitySummaryTimestamp(fields, number) {
8099
+ const field = firstProtoField(fields, number);
8100
+ if (!field?.bytes) return "";
8101
+ const timestamp = decodeProtoMessage(field.bytes);
8102
+ const seconds = protoVarintValue(firstProtoField(timestamp, 1));
8103
+ const nanos = protoVarintValue(firstProtoField(timestamp, 2)) || 0;
8104
+ if (!Number.isFinite(seconds) || seconds < 946684800 || seconds > 4102444800) return "";
8105
+ return new Date((seconds * 1000) + Math.floor(nanos / 1e6)).toISOString();
8106
+ }
8107
+
8108
+ function antigravitySummaryCwd(fields) {
8109
+ const candidates = collectProtoStrings(fields)
8110
+ .filter((value) => value.startsWith("file://"))
8111
+ .map((value) => {
8112
+ try {
8113
+ return fileURLToPath(value);
8114
+ } catch {
8115
+ return "";
8116
+ }
8117
+ })
8118
+ .filter(Boolean)
8119
+ .map((candidate) => {
8120
+ const stat = safeStat(candidate);
8121
+ return stat?.isFile() ? path.dirname(candidate) : candidate;
8122
+ })
8123
+ .filter((candidate) => candidate && !candidate.includes(`${path.sep}.gemini${path.sep}antigravity${path.sep}`))
8124
+ .filter((candidate) => !cursorIsSystemRootPath(candidate));
8125
+ return candidates[0] || "";
8126
+ }
8127
+
8128
+ function antigravityAssistantSummary(fields, prompt) {
8129
+ const promptValue = cleanAntigravityText(prompt);
8130
+ return collectProtoStrings(fields)
8131
+ .map(cleanAntigravityText)
8132
+ .filter((value) => value && value !== promptValue)
8133
+ .filter((value) => value.length >= 80)
8134
+ .filter((value) => !value.startsWith("file://"))
8135
+ .filter((value) => !isLikelyBase64(value))
8136
+ .filter((value) => !/^[0-9a-f-]{32,}$/i.test(value))
8137
+ .sort((a, b) => b.length - a.length)[0] || "";
8138
+ }
8139
+
8140
+ function antigravitySummaryTitle(prompt) {
8141
+ const value = cleanAntigravityText(prompt);
8142
+ if (!value) return "Antigravity trajectory summary";
8143
+ return value.length > 80 ? `${value.slice(0, 77).trimEnd()}...` : value;
8144
+ }
8145
+
8146
+ function cleanAntigravityText(value) {
8147
+ return String(value || "").replace(/\s+/g, " ").trim();
8148
+ }
8149
+
6592
8150
  function readMarkdownArtifactSessions({ provider, roots, sourceType, detailKey, artifactNames, options = {} }) {
6593
8151
  const dirs = [];
6594
8152
  for (const root of roots) {
@@ -6738,16 +8296,22 @@ function inferCwdFromMarkdownFiles(files) {
6738
8296
  } catch {
6739
8297
  continue;
6740
8298
  }
6741
- const match = text.match(/file:\/\/([^)\]\s]+)/);
6742
- if (!match) continue;
6743
- try {
6744
- const pathname = fileURLToPath(match[0]);
6745
- if (fs.existsSync(pathname)) return fs.statSync(pathname).isDirectory() ? pathname : path.dirname(pathname);
6746
- const existing = nearestExistingParent(pathname);
6747
- if (existing) return existing;
6748
- } catch {
6749
- // Try the next candidate.
6750
- }
8299
+ const cwd = inferCwdFromMarkdownText(text);
8300
+ if (cwd) return cwd;
8301
+ }
8302
+ return "";
8303
+ }
8304
+
8305
+ function inferCwdFromMarkdownText(text) {
8306
+ const match = String(text || "").match(/file:\/\/([^)\]\s]+)/);
8307
+ if (!match) return "";
8308
+ try {
8309
+ const pathname = fileURLToPath(match[0]);
8310
+ if (fs.existsSync(pathname)) return fs.statSync(pathname).isDirectory() ? pathname : path.dirname(pathname);
8311
+ const existing = nearestExistingParent(pathname);
8312
+ if (existing) return existing;
8313
+ } catch {
8314
+ return "";
6751
8315
  }
6752
8316
  return "";
6753
8317
  }
@@ -6813,6 +8377,111 @@ function countFiles(root, predicate) {
6813
8377
  return count;
6814
8378
  }
6815
8379
 
8380
+ function bufferFromBase64(value) {
8381
+ try {
8382
+ return Buffer.from(String(value || ""), "base64");
8383
+ } catch {
8384
+ return Buffer.alloc(0);
8385
+ }
8386
+ }
8387
+
8388
+ function decodeProtoMessage(buffer) {
8389
+ const fields = [];
8390
+ const bytes = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer || []);
8391
+ let offset = 0;
8392
+ while (offset < bytes.length) {
8393
+ const key = readProtoVarint(bytes, offset);
8394
+ if (!key) break;
8395
+ offset = key.offset;
8396
+ const fieldNumber = Math.floor(key.value / 8);
8397
+ const wireType = key.value % 8;
8398
+ if (!fieldNumber) break;
8399
+ if (wireType === 0) {
8400
+ const value = readProtoVarint(bytes, offset);
8401
+ if (!value) break;
8402
+ offset = value.offset;
8403
+ fields.push({ number: fieldNumber, wireType, value: value.value });
8404
+ continue;
8405
+ }
8406
+ if (wireType === 1) {
8407
+ if (offset + 8 > bytes.length) break;
8408
+ fields.push({ number: fieldNumber, wireType, bytes: bytes.subarray(offset, offset + 8) });
8409
+ offset += 8;
8410
+ continue;
8411
+ }
8412
+ if (wireType === 2) {
8413
+ const length = readProtoVarint(bytes, offset);
8414
+ if (!length) break;
8415
+ offset = length.offset;
8416
+ if (length.value < 0 || offset + length.value > bytes.length) break;
8417
+ const value = bytes.subarray(offset, offset + length.value);
8418
+ offset += length.value;
8419
+ fields.push({ number: fieldNumber, wireType, bytes: value, text: printableUtf8(value) });
8420
+ continue;
8421
+ }
8422
+ if (wireType === 5) {
8423
+ if (offset + 4 > bytes.length) break;
8424
+ fields.push({ number: fieldNumber, wireType, bytes: bytes.subarray(offset, offset + 4) });
8425
+ offset += 4;
8426
+ continue;
8427
+ }
8428
+ break;
8429
+ }
8430
+ return fields;
8431
+ }
8432
+
8433
+ function readProtoVarint(buffer, offset) {
8434
+ let value = 0n;
8435
+ let shift = 0n;
8436
+ for (let index = offset; index < buffer.length; index++) {
8437
+ const byte = buffer[index];
8438
+ value |= BigInt(byte & 0x7f) << shift;
8439
+ if ((byte & 0x80) === 0) {
8440
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) return null;
8441
+ return { value: Number(value), offset: index + 1 };
8442
+ }
8443
+ shift += 7n;
8444
+ if (shift > 63n) return null;
8445
+ }
8446
+ return null;
8447
+ }
8448
+
8449
+ function printableUtf8(buffer) {
8450
+ if (!buffer?.length) return "";
8451
+ const text = buffer.toString("utf8");
8452
+ if (text.includes("\uFFFD")) return "";
8453
+ if (/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(text)) return "";
8454
+ return text;
8455
+ }
8456
+
8457
+ function firstProtoField(fields, number) {
8458
+ return (fields || []).find((field) => field.number === number) || null;
8459
+ }
8460
+
8461
+ function protoString(field) {
8462
+ return field?.wireType === 2 ? field.text || "" : "";
8463
+ }
8464
+
8465
+ function protoVarintValue(field) {
8466
+ return field?.wireType === 0 ? field.value : null;
8467
+ }
8468
+
8469
+ function collectProtoStrings(fields, depth = 0) {
8470
+ const strings = [];
8471
+ if (depth > 8) return strings;
8472
+ for (const field of fields || []) {
8473
+ if (field.wireType !== 2 || !field.bytes) continue;
8474
+ if (field.text) strings.push(field.text);
8475
+ strings.push(...collectProtoStrings(decodeProtoMessage(field.bytes), depth + 1));
8476
+ }
8477
+ return strings;
8478
+ }
8479
+
8480
+ function isLikelyBase64(value) {
8481
+ const text = String(value || "").trim();
8482
+ return text.length >= 80 && text.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(text);
8483
+ }
8484
+
6816
8485
  function envPathList(value) {
6817
8486
  return String(value || "")
6818
8487
  .split(new RegExp(`[${escapeRegExp(path.delimiter)},]`))
@@ -6843,6 +8512,30 @@ function readJsonMaybe(file, fallback = null) {
6843
8512
  }
6844
8513
  }
6845
8514
 
8515
+ function parseJsonObject(value, fallback = {}) {
8516
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
8517
+ if (typeof value !== "string" || !value.trim()) return fallback;
8518
+ try {
8519
+ const parsed = JSON.parse(value);
8520
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback;
8521
+ } catch {
8522
+ return fallback;
8523
+ }
8524
+ }
8525
+
8526
+ function groupRowsBy(rows, key) {
8527
+ const grouped = new Map();
8528
+ for (const row of rows || []) {
8529
+ const value = row?.[key];
8530
+ if (value === undefined || value === null || value === "") continue;
8531
+ const groupKey = String(value);
8532
+ const group = grouped.get(groupKey) || [];
8533
+ group.push(row);
8534
+ grouped.set(groupKey, group);
8535
+ }
8536
+ return grouped;
8537
+ }
8538
+
6846
8539
  function defaultSkipDirs() {
6847
8540
  return new Set([".git", "node_modules", "vendor", "dist", "build", ".next", ".cache", ".venv", "venv", "__pycache__"]);
6848
8541
  }
@@ -7037,6 +8730,7 @@ module.exports = {
7037
8730
  mergeCursorRawAssistantOnlySessions,
7038
8731
  mergeCursorRawCompanionSessions,
7039
8732
  importCliHistory,
8733
+ importWindsurfTrajectoryExport,
7040
8734
  importWebChat,
7041
8735
  normalizeEventRole,
7042
8736
  parseClaudeDesktopSessionFile,
@@ -7046,18 +8740,26 @@ module.exports = {
7046
8740
  providerAdapterForSource,
7047
8741
  providerAdapterSources,
7048
8742
  geminiCliHistoryFiles,
8743
+ openCodeDatabaseFiles,
7049
8744
  openCodeStorageRoots,
7050
8745
  readCursorProjectTranscriptSessions,
7051
8746
  readCursorGlobalDiskKvSessionsFromDb,
7052
8747
  readCursorRawSqliteSalvageSessionsFromDb,
7053
8748
  readAiderSessions,
8749
+ readAntigravitySessions,
7054
8750
  readClineSessions,
7055
8751
  readDevinSessionsFromDb,
7056
8752
  readGeminiCliSessions,
7057
8753
  readOpenCodeSessions,
8754
+ readWindsurfTrajectoryExport,
7058
8755
  readExportBundle,
7059
8756
  readExportJson,
7060
8757
  normalizeWebConversations,
7061
8758
  claudeFileHistoryFiles,
7062
- claudeFileHistoryRoot
8759
+ claudeFileHistoryRoot,
8760
+ __cursorTestables: {
8761
+ cursorAttributionCwd,
8762
+ cursorIsSystemRootPath,
8763
+ cursorMostFrequentExistingCwd
8764
+ }
7063
8765
  };