agentel 0.2.0 → 0.2.3

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,8 @@ 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,
458
+ parentComposerId: session.parentComposerId || undefined,
454
459
  sharedRawFiles: cursorSessionUsesSharedRawFiles(sourceType),
455
460
  replaceSourcePathCopies: sourceType === "cursor-agent-transcripts"
456
461
  },
@@ -534,6 +539,10 @@ function importStructuredProvider(provider, sessions, since, options = {}, env =
534
539
  sourceFiles: session.sourceFiles || [session.sourcePath].filter(Boolean),
535
540
  sourceType,
536
541
  title: session.title,
542
+ sessionSummary: session.sessionSummary,
543
+ providerConversationId: session.providerConversationId,
544
+ rawReferences: session.rawReferences,
545
+ sharedRawFiles: structuredSessionUsesSharedRawFiles(provider, sourceType),
537
546
  replaceSourcePathCopies: structuredSessionReplaceSourcePathCopies(provider, sourceType)
538
547
  },
539
548
  env
@@ -554,6 +563,10 @@ function structuredSessionReplaceSourcePathCopies(provider, sourceType) {
554
563
  return undefined;
555
564
  }
556
565
 
566
+ function structuredSessionUsesSharedRawFiles(provider, sourceType) {
567
+ return provider === "opencode" && sourceType === "opencode-sqlite-history";
568
+ }
569
+
557
570
  function parseAgentJsonl(file, provider) {
558
571
  const text = readTextMaybeZstd(file);
559
572
  const messages = [];
@@ -1342,6 +1355,7 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
1342
1355
  sourceType,
1343
1356
  parserVersion: parserVersionForSource(sourceType),
1344
1357
  title: conversation.title,
1358
+ sessionSummary: conversation.sessionSummary,
1345
1359
  providerConversationId: conversation.id,
1346
1360
  chatAccountId: account.accountId,
1347
1361
  chatUsername: account.username,
@@ -1351,6 +1365,7 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
1351
1365
  chatDisplayPath: displayPath,
1352
1366
  conversationKind: conversation.kind || "conversation",
1353
1367
  pinned: Boolean(conversation.pinned),
1368
+ timeStatus: conversation.timeStatus || "",
1354
1369
  replaceSourcePathCopies: false
1355
1370
  },
1356
1371
  env
@@ -1372,6 +1387,17 @@ function importWebChat(providerInput, file, options = {}, env = process.env) {
1372
1387
  return summary;
1373
1388
  }
1374
1389
 
1390
+ function importWindsurfTrajectoryExport(target, options = {}, env = process.env) {
1391
+ const since = parseSince(options.since || "all");
1392
+ return importStructuredProvider(
1393
+ "windsurf",
1394
+ readWindsurfTrajectoryExport(target, options),
1395
+ since,
1396
+ { ...options, repos: options.repos || [] },
1397
+ env
1398
+ );
1399
+ }
1400
+
1375
1401
  function readExportJson(file) {
1376
1402
  const bundle = readExportBundle(file);
1377
1403
  const conversations = bundle.entries.find((entry) => /(^|\/)conversations\.json$/i.test(entry.name));
@@ -1458,11 +1484,13 @@ function readExportFile(file) {
1458
1484
 
1459
1485
  function exportEntry(name, text, sourcePath) {
1460
1486
  const data = parseExportText(text, name);
1487
+ const stat = safeStat(String(sourcePath || "").split("#")[0]);
1461
1488
  return {
1462
1489
  name,
1463
1490
  sourcePath,
1464
1491
  data,
1465
1492
  size: Buffer.byteLength(text),
1493
+ mtime: stat?.mtimeMs ? new Date(stat.mtimeMs).toISOString() : "",
1466
1494
  sha256: crypto.createHash("sha256").update(text).digest("hex")
1467
1495
  };
1468
1496
  }
@@ -1571,16 +1599,16 @@ function chatgptRawConversations(data) {
1571
1599
  function chatgptMessages(conversation) {
1572
1600
  if (conversation.mapping && typeof conversation.mapping === "object") {
1573
1601
  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
- }));
1602
+ return nodes.map((node) => node && node.message).filter(Boolean).map((message) => {
1603
+ const role = normalizeEventRole(message.author?.role) || "unknown";
1604
+ const content = extractChatGptContent(message.content);
1605
+ return {
1606
+ role,
1607
+ content,
1608
+ timestamp: toIso(message.create_time || message.update_time),
1609
+ metadata: chatgptMessageMetadata(message, role, content)
1610
+ };
1611
+ });
1584
1612
  }
1585
1613
  return genericConversationMessages(conversation, "chatgpt-export");
1586
1614
  }
@@ -1610,6 +1638,221 @@ function extractChatGptContent(content) {
1610
1638
  return extractText(content);
1611
1639
  }
1612
1640
 
1641
+ function chatgptMessageMetadata(message, role, content) {
1642
+ return webWithUsage({
1643
+ source: "chatgpt-export",
1644
+ messageId: message.id || undefined,
1645
+ model: firstString(message.metadata?.model_slug, message.metadata?.model, message.metadata?.default_model_slug) || undefined
1646
+ }, webMessageUsage(message, role, { inputText: content, outputText: content }));
1647
+ }
1648
+
1649
+ function webWithUsage(metadata, usage) {
1650
+ if (!usage) return metadata;
1651
+ return { ...metadata, usage };
1652
+ }
1653
+
1654
+ function webMessageUsage(message, role, parts = {}) {
1655
+ const providerUsage = webProviderUsage(...webUsageCandidates(message));
1656
+ if (providerUsage) return webUsageWithRoleDirection(providerUsage, role);
1657
+ const normalizedRole = String(role || "").toLowerCase();
1658
+ return webEstimatedMessageUsage({
1659
+ inputText: normalizedRole === "assistant" ? "" : parts.inputText,
1660
+ outputText: normalizedRole === "assistant" ? parts.outputText : "",
1661
+ reasoningText: normalizedRole === "assistant" ? parts.reasoningText : ""
1662
+ });
1663
+ }
1664
+
1665
+ function webUsageWithRoleDirection(usage, role) {
1666
+ if (!usage || usage.inputTokens || usage.outputTokens || usage.totalInputTokens || usage.totalOutputTokens) return usage;
1667
+ const totalTokens = webPositiveTokenNumber(usage.totalTokens);
1668
+ if (!totalTokens) return usage;
1669
+ const directionalTokens = Math.max(
1670
+ 0,
1671
+ totalTokens -
1672
+ webPositiveTokenNumber(usage.cacheInputTokens) -
1673
+ webPositiveTokenNumber(usage.reasoningOutputTokens) -
1674
+ webPositiveTokenNumber(usage.toolUsePromptTokens)
1675
+ );
1676
+ if (!directionalTokens) return usage;
1677
+ if (String(role || "").toLowerCase() === "assistant") return { ...usage, outputTokens: directionalTokens };
1678
+ return { ...usage, inputTokens: directionalTokens };
1679
+ }
1680
+
1681
+ function webUsageCandidates(message) {
1682
+ if (!message || typeof message !== "object") return [];
1683
+ const metadata = message.metadata && typeof message.metadata === "object" ? message.metadata : {};
1684
+ return [
1685
+ message.usage,
1686
+ message.token_usage,
1687
+ message.tokenUsage,
1688
+ message.token_counts,
1689
+ message.tokenCounts,
1690
+ message.token_count,
1691
+ message.tokenCount,
1692
+ message.tokens,
1693
+ metadata.usage,
1694
+ metadata.token_usage,
1695
+ metadata.tokenUsage,
1696
+ metadata.token_counts,
1697
+ metadata.tokenCounts,
1698
+ metadata.token_count,
1699
+ metadata.tokenCount,
1700
+ metadata.tokens,
1701
+ message
1702
+ ];
1703
+ }
1704
+
1705
+ function webProviderUsage(...candidates) {
1706
+ for (const candidate of candidates) {
1707
+ const usage = normalizeWebProviderUsage(candidate);
1708
+ if (usage) return usage;
1709
+ }
1710
+ return null;
1711
+ }
1712
+
1713
+ function normalizeWebProviderUsage(usage) {
1714
+ if (usage == null || usage === "") return null;
1715
+ if (typeof usage !== "object") {
1716
+ const totalTokens = webPositiveTokenNumber(usage);
1717
+ return totalTokens ? { totalTokens } : null;
1718
+ }
1719
+ const inputTokens = webPositiveTokenNumber(numericValue(
1720
+ usage.inputTokens,
1721
+ usage.input_tokens,
1722
+ usage.promptTokens,
1723
+ usage.prompt_tokens,
1724
+ usage.promptTokenCount,
1725
+ usage.prompt_token_count,
1726
+ usage.inputTokenCount,
1727
+ usage.input_token_count,
1728
+ usage.input,
1729
+ usage.prompt
1730
+ ));
1731
+ const outputTokens = webPositiveTokenNumber(numericValue(
1732
+ usage.outputTokens,
1733
+ usage.output_tokens,
1734
+ usage.completionTokens,
1735
+ usage.completion_tokens,
1736
+ usage.completionTokenCount,
1737
+ usage.completion_token_count,
1738
+ usage.outputTokenCount,
1739
+ usage.output_token_count,
1740
+ usage.output,
1741
+ usage.completion
1742
+ ));
1743
+ const totalInputTokens = webPositiveTokenNumber(numericValue(
1744
+ usage.totalInputTokens,
1745
+ usage.total_input_tokens,
1746
+ usage.totalPromptTokens,
1747
+ usage.total_prompt_tokens,
1748
+ usage.totalPromptTokenCount,
1749
+ usage.total_prompt_token_count
1750
+ ));
1751
+ const totalOutputTokens = webPositiveTokenNumber(numericValue(
1752
+ usage.totalOutputTokens,
1753
+ usage.total_output_tokens,
1754
+ usage.totalCompletionTokens,
1755
+ usage.total_completion_tokens,
1756
+ usage.totalCompletionTokenCount,
1757
+ usage.total_completion_token_count
1758
+ ));
1759
+ const cacheInputTokens = webSumTokenNumbers(
1760
+ usage.cacheInputTokens,
1761
+ usage.cache_input_tokens,
1762
+ usage.cacheCreationInputTokens,
1763
+ usage.cache_creation_input_tokens,
1764
+ usage.cacheReadInputTokens,
1765
+ usage.cache_read_input_tokens,
1766
+ usage.cacheReadTokens,
1767
+ usage.cache_read_tokens,
1768
+ usage.cachedContentTokenCount,
1769
+ usage.cached_content_token_count,
1770
+ usage.cachedTokens,
1771
+ usage.cached_tokens,
1772
+ usage.prompt_tokens_details?.cached_tokens,
1773
+ usage.promptTokensDetails?.cachedTokens
1774
+ );
1775
+ const reasoningOutputTokens = webSumTokenNumbers(
1776
+ usage.reasoningOutputTokens,
1777
+ usage.reasoning_output_tokens,
1778
+ usage.reasoningTokens,
1779
+ usage.reasoning_tokens,
1780
+ usage.reasoningTokenCount,
1781
+ usage.reasoning_token_count,
1782
+ usage.thoughtsTokens,
1783
+ usage.thoughts_tokens,
1784
+ usage.thoughtsTokenCount,
1785
+ usage.thoughts_token_count,
1786
+ usage.completion_tokens_details?.reasoning_tokens,
1787
+ usage.completionTokensDetails?.reasoningTokens,
1788
+ usage.output_tokens_details?.reasoning_tokens,
1789
+ usage.outputTokensDetails?.reasoningTokens
1790
+ );
1791
+ const toolUsePromptTokens = webSumTokenNumbers(
1792
+ usage.toolUsePromptTokens,
1793
+ usage.tool_use_prompt_tokens,
1794
+ usage.toolUsePromptTokenCount,
1795
+ usage.tool_use_prompt_token_count
1796
+ );
1797
+ const explicitTotalTokens = webPositiveTokenNumber(numericValue(
1798
+ usage.totalTokens,
1799
+ usage.total_tokens,
1800
+ usage.totalTokenCount,
1801
+ usage.total_token_count,
1802
+ usage.tokenCount,
1803
+ usage.token_count,
1804
+ usage.tokens,
1805
+ usage.total
1806
+ ));
1807
+ const totalTokens = explicitTotalTokens ||
1808
+ inputTokens + outputTokens + cacheInputTokens + reasoningOutputTokens + toolUsePromptTokens ||
1809
+ totalInputTokens + totalOutputTokens;
1810
+ if (!totalTokens) return null;
1811
+ const result = { totalTokens };
1812
+ if (inputTokens) result.inputTokens = inputTokens;
1813
+ if (outputTokens) result.outputTokens = outputTokens;
1814
+ if (totalInputTokens) result.totalInputTokens = totalInputTokens;
1815
+ if (totalOutputTokens) result.totalOutputTokens = totalOutputTokens;
1816
+ if (cacheInputTokens) result.cacheInputTokens = cacheInputTokens;
1817
+ if (reasoningOutputTokens) result.reasoningOutputTokens = reasoningOutputTokens;
1818
+ if (toolUsePromptTokens) result.toolUsePromptTokens = toolUsePromptTokens;
1819
+ return result;
1820
+ }
1821
+
1822
+ function webEstimatedMessageUsage(parts = {}) {
1823
+ const inputTokens = webEstimatedTokenCount(parts.inputText);
1824
+ const outputTokens = webEstimatedTokenCount(parts.outputText);
1825
+ const reasoningOutputTokens = webEstimatedTokenCount(parts.reasoningText);
1826
+ const totalTokens = inputTokens + outputTokens + reasoningOutputTokens;
1827
+ if (!totalTokens) return null;
1828
+ const usage = {
1829
+ totalTokens,
1830
+ estimated: true,
1831
+ estimationMethod: WEB_CHAT_TOKEN_ESTIMATION_METHOD,
1832
+ charsPerToken: WEB_TOKEN_ESTIMATE_CHARS
1833
+ };
1834
+ if (inputTokens) usage.inputTokens = inputTokens;
1835
+ if (outputTokens) usage.outputTokens = outputTokens;
1836
+ if (reasoningOutputTokens) usage.reasoningOutputTokens = reasoningOutputTokens;
1837
+ return usage;
1838
+ }
1839
+
1840
+ function webEstimatedTokenCount(value) {
1841
+ const text = String(value || "");
1842
+ if (!text.trim()) return 0;
1843
+ return Math.max(1, Math.ceil(text.length / WEB_TOKEN_ESTIMATE_CHARS));
1844
+ }
1845
+
1846
+ function webPositiveTokenNumber(value) {
1847
+ const number = Number(value);
1848
+ if (!Number.isFinite(number) || number <= 0) return 0;
1849
+ return Math.round(number);
1850
+ }
1851
+
1852
+ function webSumTokenNumbers(...values) {
1853
+ return values.reduce((sum, value) => sum + webPositiveTokenNumber(value), 0);
1854
+ }
1855
+
1613
1856
  function normalizeClaudeWebExport(source) {
1614
1857
  const projectMap = claudeProjectMap(source);
1615
1858
  const conversations = [];
@@ -1643,7 +1886,11 @@ function claudeConversationsFromEntry(entry, projectMap = new Map()) {
1643
1886
  return values.map((conversation, index) => {
1644
1887
  const id = firstString(conversation.uuid, conversation.id, conversation.conversation_uuid, conversation.conversation_id) || `claude-${hashId(`${entry.name}:${index}`)}`;
1645
1888
  const title = firstString(conversation.name, conversation.title, conversation.summary) || "Claude conversation";
1646
- const messages = claudeMessages(conversation).filter((message) => message.content.trim());
1889
+ const sessionSummary = claudeConversationSessionSummary(conversation);
1890
+ const messages = [
1891
+ ...claudeConversationSummaryMessages(conversation),
1892
+ ...claudeMessages(conversation)
1893
+ ].filter((message) => message.content.trim());
1647
1894
  const sorted = sortConversationMessages(messages);
1648
1895
  return {
1649
1896
  id,
@@ -1655,7 +1902,8 @@ function claudeConversationsFromEntry(entry, projectMap = new Map()) {
1655
1902
  projectPath: claudeConversationProjectPath(conversation, projectPath, projectMap),
1656
1903
  entryPath: entry.name,
1657
1904
  sourceType: "claude-web-export",
1658
- kind: "conversation"
1905
+ kind: "conversation",
1906
+ sessionSummary
1659
1907
  };
1660
1908
  });
1661
1909
  }
@@ -1701,16 +1949,158 @@ function claudeProjectPath(entry) {
1701
1949
 
1702
1950
  function claudeMessages(conversation) {
1703
1951
  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),
1952
+ return messages.flatMap(claudeMessageRows);
1953
+ }
1954
+
1955
+ function claudeConversationSessionSummary(conversation) {
1956
+ const summary = firstString(conversation.summary, conversation.conversation_summary, conversation.conversationSummary);
1957
+ if (!summary) return undefined;
1958
+ return {
1959
+ source: "claude-web-export",
1960
+ summary,
1961
+ updatedAt: toIso(conversation.updated_at || conversation.updatedAt) || undefined
1962
+ };
1963
+ }
1964
+
1965
+ function claudeConversationSummaryMessages(conversation) {
1966
+ const summary = firstString(conversation.summary, conversation.conversation_summary, conversation.conversationSummary);
1967
+ if (!summary) return [];
1968
+ return [{
1969
+ role: "assistant",
1970
+ content: claudeSupplementContent("Claude conversation summary", summary),
1971
+ timestamp: toIso(conversation.created_at || conversation.createdAt || conversation.updated_at || conversation.updatedAt) || "",
1708
1972
  metadata: {
1709
1973
  source: "claude-web-export",
1710
- messageId: firstString(message.uuid, message.id) || undefined,
1711
- model: firstString(message.model, message.model_name, message.modelName) || undefined
1974
+ eventType: "claude-conversation-summary",
1975
+ supplementary: true,
1976
+ summaryKind: "conversation_summary"
1712
1977
  }
1713
- }));
1978
+ }];
1979
+ }
1980
+
1981
+ function claudeMessageRows(message) {
1982
+ const role = normalizeEventRole(message.sender || message.role || message.author?.role || message.author) || "unknown";
1983
+ const fallbackTimestamp = toIso(message.created_at || message.createdAt || message.updated_at || message.updatedAt || message.timestamp);
1984
+ const metadata = claudeWebMessageMetadata(message);
1985
+ if (role === "assistant") return claudeAssistantMessageRows(message, fallbackTimestamp, metadata);
1986
+ const content = claudeVisibleMessageText(message);
1987
+ return [{
1988
+ role,
1989
+ content,
1990
+ timestamp: fallbackTimestamp,
1991
+ metadata: webWithUsage(metadata, webMessageUsage(message, role, { inputText: content }))
1992
+ }];
1993
+ }
1994
+
1995
+ function claudeAssistantMessageRows(message, fallbackTimestamp, metadata) {
1996
+ const parts = claudeContentParts(message.content);
1997
+ if (!parts.length) {
1998
+ const content = extractText(message.text || message.content || message.message);
1999
+ return [{
2000
+ role: "assistant",
2001
+ content,
2002
+ timestamp: fallbackTimestamp,
2003
+ metadata: webWithUsage(metadata, webMessageUsage(message, "assistant", { outputText: content }))
2004
+ }];
2005
+ }
2006
+ const thinkingParts = claudeThinkingParts(parts);
2007
+ const visibleParts = claudeVisibleParts(parts);
2008
+ const thinking = claudeThinkingText(thinkingParts);
2009
+ const visible = claudeVisibleText(visibleParts);
2010
+ const usage = webMessageUsage(message, "assistant", { outputText: visible, reasoningText: thinking });
2011
+ const rows = [];
2012
+ if (thinking) {
2013
+ const thinkingSummaries = claudeThinkingSummaries(thinkingParts);
2014
+ rows.push({
2015
+ role: "assistant",
2016
+ content: claudeSupplementContent("Claude thinking", thinking),
2017
+ timestamp: claudePartsTimestamp(thinkingParts, fallbackTimestamp),
2018
+ metadata: webWithUsage({
2019
+ ...metadata,
2020
+ eventType: "claude-thinking",
2021
+ supplementary: true,
2022
+ summaryKind: "thinking",
2023
+ thinkingSummaries: thinkingSummaries.length ? thinkingSummaries : undefined
2024
+ }, visible ? null : usage)
2025
+ });
2026
+ }
2027
+ if (visible) {
2028
+ rows.push({
2029
+ role: "assistant",
2030
+ content: visible,
2031
+ timestamp: claudePartsTimestamp(visibleParts, fallbackTimestamp),
2032
+ metadata: webWithUsage(metadata, usage)
2033
+ });
2034
+ }
2035
+ return rows;
2036
+ }
2037
+
2038
+ function claudeWebMessageMetadata(message) {
2039
+ return {
2040
+ source: "claude-web-export",
2041
+ messageId: firstString(message.uuid, message.id) || undefined,
2042
+ model: firstString(message.model, message.model_name, message.modelName) || undefined
2043
+ };
2044
+ }
2045
+
2046
+ function claudeContentParts(content) {
2047
+ return Array.isArray(content) ? content.filter((part) => part && typeof part === "object") : [];
2048
+ }
2049
+
2050
+ function claudeVisibleMessageText(message) {
2051
+ const parts = claudeContentParts(message.content);
2052
+ if (parts.length) return claudeVisibleText(claudeVisibleParts(parts));
2053
+ return extractText(message.text || message.content || message.message);
2054
+ }
2055
+
2056
+ function claudeVisibleParts(parts) {
2057
+ return parts.filter((part) => {
2058
+ const type = String(part.type || part.kind || "").toLowerCase();
2059
+ return !/(tool_use|tool_result|function_call|function_result|thinking|redacted_thinking)/.test(type);
2060
+ });
2061
+ }
2062
+
2063
+ function claudeThinkingParts(parts) {
2064
+ return parts.filter((part) => /thinking/.test(String(part.type || part.kind || "").toLowerCase()));
2065
+ }
2066
+
2067
+ function claudeVisibleText(parts) {
2068
+ return parts.map((part) => extractText(part)).filter(Boolean).join("\n").trim();
2069
+ }
2070
+
2071
+ function claudeThinkingText(parts) {
2072
+ return parts
2073
+ .map((part) => firstString(part.thinking, part.text, part.content, part.summary, extractText(part.content)))
2074
+ .filter(Boolean)
2075
+ .join("\n\n")
2076
+ .trim();
2077
+ }
2078
+
2079
+ function claudeThinkingSummaries(parts) {
2080
+ return parts.flatMap((part) => Array.isArray(part.summaries)
2081
+ ? part.summaries.map((summary) => firstString(summary.summary, summary.text, summary.content))
2082
+ : []
2083
+ ).filter(Boolean);
2084
+ }
2085
+
2086
+ function claudePartsTimestamp(parts, fallbackTimestamp) {
2087
+ const timestamps = parts
2088
+ .map((part) => toIso(
2089
+ part.stop_timestamp ||
2090
+ part.stopTimestamp ||
2091
+ part.end_timestamp ||
2092
+ part.endTimestamp ||
2093
+ part.start_timestamp ||
2094
+ part.startTimestamp ||
2095
+ part.created_at ||
2096
+ part.createdAt
2097
+ ))
2098
+ .filter(Boolean);
2099
+ return timestamps[timestamps.length - 1] || fallbackTimestamp || "";
2100
+ }
2101
+
2102
+ function claudeSupplementContent(title, content) {
2103
+ return `### ${title}\n\n${String(content || "").trim()}`;
1714
2104
  }
1715
2105
 
1716
2106
  function claudeMemoryConversations(entry, projectMap = new Map()) {
@@ -1718,7 +2108,7 @@ function claudeMemoryConversations(entry, projectMap = new Map()) {
1718
2108
  const conversations = [];
1719
2109
  for (const row of rows) {
1720
2110
  const rootContent = renderClaudeConversationMemory(row);
1721
- if (rootContent.trim()) conversations.push(claudeMemoryConversation("memory", "Claude Memory", rootContent, "memory", entry.name));
2111
+ if (rootContent.trim()) conversations.push(claudeMemoryConversation("memory", "Claude Memory", rootContent, "memory", entry));
1722
2112
  const projectMemories = row && typeof row === "object" && row.project_memories && typeof row.project_memories === "object"
1723
2113
  ? row.project_memories
1724
2114
  : {};
@@ -1727,14 +2117,14 @@ function claudeMemoryConversations(entry, projectMap = new Map()) {
1727
2117
  if (!content.trim()) continue;
1728
2118
  const project = projectMap.get(projectId);
1729
2119
  const projectTitle = project?.title || project?.path || sanitizeProjectPath(projectId) || projectId;
1730
- conversations.push(claudeMemoryConversation(`memory-${projectId}`, `Claude Project Memory: ${projectTitle}`, content, "memory", entry.name));
2120
+ conversations.push(claudeMemoryConversation(`memory-${projectId}`, `Claude Project Memory: ${projectTitle}`, content, "memory", entry));
1731
2121
  }
1732
2122
  }
1733
2123
  return conversations;
1734
2124
  }
1735
2125
 
1736
- function claudeMemoryConversation(id, title, content, projectPath, entryPath) {
1737
- const timestamp = new Date().toISOString();
2126
+ function claudeMemoryConversation(id, title, content, projectPath, entry) {
2127
+ const timestamp = claudeMemorySyntheticTimestamp(entry);
1738
2128
  return {
1739
2129
  id,
1740
2130
  title,
@@ -1743,13 +2133,23 @@ function claudeMemoryConversation(id, title, content, projectPath, entryPath) {
1743
2133
  endedAt: timestamp,
1744
2134
  updatedAt: timestamp,
1745
2135
  projectPath: projectPath || "",
1746
- entryPath,
2136
+ entryPath: entry?.name || "",
1747
2137
  sourceType: "claude-web-memory",
1748
2138
  kind: "memory",
1749
- pinned: true
2139
+ pinned: true,
2140
+ timeStatus: "recovered-time-unknown",
2141
+ sessionSummary: {
2142
+ source: "claude-web-memory",
2143
+ timeStatus: "recovered-time-unknown",
2144
+ note: "Claude memory exports do not include reliable memory creation or update timestamps."
2145
+ }
1750
2146
  };
1751
2147
  }
1752
2148
 
2149
+ function claudeMemorySyntheticTimestamp(entry) {
2150
+ return toIso(entry?.mtime) || "1970-01-01T00:00:00.000Z";
2151
+ }
2152
+
1753
2153
  function renderClaudeConversationMemory(data) {
1754
2154
  if (data && typeof data === "object" && typeof data.conversations_memory === "string") {
1755
2155
  return data.conversations_memory;
@@ -1781,12 +2181,16 @@ function renderClaudeMemoryItem(item) {
1781
2181
 
1782
2182
  function genericConversationMessages(conversation, source) {
1783
2183
  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
- }));
2184
+ return messages.map((message) => {
2185
+ const role = normalizeEventRole(message.role || message.sender || message.author?.role) || "unknown";
2186
+ const content = extractText(message.content || message.text || message.message);
2187
+ return {
2188
+ role,
2189
+ content,
2190
+ timestamp: toIso(message.created_at || message.create_time || message.timestamp),
2191
+ metadata: webWithUsage({ source }, webMessageUsage(message, role, { inputText: content, outputText: content }))
2192
+ };
2193
+ });
1790
2194
  }
1791
2195
 
1792
2196
  function sortConversationMessages(messages) {
@@ -1805,7 +2209,8 @@ function webConversationSessionId(provider, accountId, conversationId) {
1805
2209
 
1806
2210
  function webConversationFingerprint(sourceType, accountId, conversation) {
1807
2211
  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)}`;
2212
+ const summary = JSON.stringify(conversation.sessionSummary || {});
2213
+ return `${fingerprintPrefix(sourceType)}:${accountId}:${conversation.projectPath || ""}:${conversation.updatedAt || conversation.endedAt || ""}:${conversation.messages.length}:${hashId(`${body}\n${summary}`)}`;
1809
2214
  }
1810
2215
 
1811
2216
  function webConversationScope(provider, accountId, projectPath = "") {
@@ -2005,8 +2410,8 @@ function discoverCliHistory(env = process.env, options = {}) {
2005
2410
  summarizeStructuredSessions(
2006
2411
  readAntigravitySessions({
2007
2412
  onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "Antigravity" })
2008
- }),
2009
- "Antigravity task/plan/walkthrough artifacts; binary protobuf transcripts are counted separately"
2413
+ }, env),
2414
+ "Antigravity task/plan/walkthrough artifacts plus trajectory summaries; binary protobuf transcripts are counted separately"
2010
2415
  )
2011
2416
  );
2012
2417
 
@@ -2044,7 +2449,7 @@ function discoverCliHistory(env = process.env, options = {}) {
2044
2449
  readOpenCodeSessions(env, {
2045
2450
  onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "OpenCode" })
2046
2451
  }),
2047
- "OpenCode JSON session/message/part storage"
2452
+ "OpenCode SQLite database and JSON session/message/part storage"
2048
2453
  )
2049
2454
  );
2050
2455
 
@@ -2233,6 +2638,8 @@ function summarizeStructuredSessionDetails(sessions) {
2233
2638
  if (session.detailKey) acc[session.detailKey] = (acc[session.detailKey] || 0) + 1;
2234
2639
  if (session.artifactCount) acc.artifacts = (acc.artifacts || 0) + session.artifactCount;
2235
2640
  if (session.binaryCount) acc.binaryOnly = (acc.binaryOnly || 0) + session.binaryCount;
2641
+ if (session.partialSummary) acc.partialSummaries = (acc.partialSummaries || 0) + 1;
2642
+ if (session.stateDbCount) acc.stateDbs = (acc.stateDbs || 0) + session.stateDbCount;
2236
2643
  return acc;
2237
2644
  }, {});
2238
2645
  }
@@ -2413,7 +2820,7 @@ function readCodexThreads(env = process.env) {
2413
2820
  "where rollout_path != ''",
2414
2821
  "order by updated_at desc"
2415
2822
  ].join(" ");
2416
- const result = spawnSync("sqlite3", [db, "-json", query], { encoding: "utf8", maxBuffer: 1024 * 1024 * 50 });
2823
+ const result = spawnSync("sqlite3", [db, "-json", query], { argv0: "agentlog-sqlite", encoding: "utf8", maxBuffer: 1024 * 1024 * 50 });
2417
2824
  if (result.status !== 0 || !result.stdout.trim()) return [];
2418
2825
  try {
2419
2826
  return JSON.parse(result.stdout).map((row) => ({
@@ -2550,18 +2957,37 @@ function normalizeSourcePath(file) {
2550
2957
  }
2551
2958
 
2552
2959
  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
2960
  try {
2559
- return parseSqliteJson(result.stdout).length > 0;
2961
+ return readSqliteJson(
2962
+ dbPath,
2963
+ `select name from sqlite_master where type = 'table' and name = ${sqlQuote(tableName)} limit 1`,
2964
+ `${tableName} table check`
2965
+ ).length > 0;
2560
2966
  } catch {
2561
2967
  return false;
2562
2968
  }
2563
2969
  }
2564
2970
 
2971
+ function sqliteTableColumns(dbPath, tableName) {
2972
+ try {
2973
+ return new Set(
2974
+ readSqliteJson(dbPath, `select name from pragma_table_info(${sqlQuote(tableName)})`, `${tableName} column check`)
2975
+ .map((row) => String(row.name || ""))
2976
+ .filter(Boolean)
2977
+ );
2978
+ } catch {
2979
+ return new Set();
2980
+ }
2981
+ }
2982
+
2983
+ function sqliteSelectMaybe(columns, tableAlias, columnName, outputName = columnName) {
2984
+ if (columns.has(columnName)) {
2985
+ const value = `${tableAlias}.${columnName}`;
2986
+ return outputName === columnName ? value : `${value} as ${outputName}`;
2987
+ }
2988
+ return `null as ${outputName}`;
2989
+ }
2990
+
2565
2991
  function codexStateDb(env = process.env) {
2566
2992
  return env.CODEX_STATE_DB || path.join(codexHome(env), "state_5.sqlite");
2567
2993
  }
@@ -2848,11 +3274,13 @@ function readCursorProjectTranscriptSessions(options = {}) {
2848
3274
  return stat && stat.mtime >= options.modifiedSince;
2849
3275
  }));
2850
3276
  }
3277
+ const composerInfoLookup = options.composerInfoLookup
3278
+ || (options.composerTitleLookup ? (id) => ({ title: options.composerTitleLookup(id), model: "" }) : cursorBuildComposerInfoLookup(env));
2851
3279
  const sessions = [];
2852
3280
  reportDiscoveryProgress(options, { current: 0, total: groups.length, message: "reading Cursor agent transcripts" });
2853
3281
  for (let index = 0; index < groups.length; index++) {
2854
3282
  const group = groups[index];
2855
- const session = parseCursorTranscriptGroup(group);
3283
+ const session = parseCursorTranscriptGroup({ ...group, composerInfoLookup });
2856
3284
  if (session) sessions.push(session);
2857
3285
  reportDiscoveryProgress(options, {
2858
3286
  current: index + 1,
@@ -2864,6 +3292,87 @@ function readCursorProjectTranscriptSessions(options = {}) {
2864
3292
  return sessions;
2865
3293
  }
2866
3294
 
3295
+ function cursorBuildComposerTitleLookup(env = process.env) {
3296
+ const lookup = cursorBuildComposerInfoLookup(env);
3297
+ return (composerId) => lookup(composerId).title;
3298
+ }
3299
+
3300
+ function cursorBuildComposerInfoLookup(env = process.env) {
3301
+ const info = new Map();
3302
+ for (const db of cursorGlobalStorageDbs(env)) {
3303
+ try {
3304
+ const composerRows = readSqliteJson(
3305
+ db,
3306
+ [
3307
+ "select",
3308
+ "key,",
3309
+ "json_extract(value, '$.name') as name,",
3310
+ "json_extract(value, '$.title') as title,",
3311
+ "json_extract(value, '$.chatTitle') as chatTitle,",
3312
+ "json_extract(value, '$.modelConfig.modelName') as modelConfigModelName",
3313
+ "from cursorDiskKV where",
3314
+ "json_valid(value) and (",
3315
+ cursorDiskKvPrefixRangeCondition("composerData:"),
3316
+ "or",
3317
+ cursorDiskKvPrefixRangeCondition("_composerData:"),
3318
+ ")"
3319
+ ].join(" "),
3320
+ "Cursor global SQLite store"
3321
+ );
3322
+ for (const row of composerRows) {
3323
+ const match = String(row.key || "").match(/^_?composerData:(.+)$/);
3324
+ if (!match) continue;
3325
+ const composerId = match[1].toLowerCase();
3326
+ const title = firstString(row.name, row.title, row.chatTitle);
3327
+ const entry = info.get(composerId) || { title: "", modelHist: new Map() };
3328
+ if (title && !entry.title) entry.title = title;
3329
+ const configModel = firstString(row.modelConfigModelName);
3330
+ if (configModel) entry.configModel = (entry.configModel || configModel);
3331
+ info.set(composerId, entry);
3332
+ }
3333
+ const bubbleRows = readSqliteJson(
3334
+ db,
3335
+ [
3336
+ "select",
3337
+ "key,",
3338
+ "json_extract(value, '$.modelId') as modelId,",
3339
+ "json_extract(value, '$.modelName') as modelName,",
3340
+ "json_extract(value, '$.model') as model",
3341
+ "from cursorDiskKV where",
3342
+ "json_valid(value) and",
3343
+ cursorDiskKvPrefixRangeCondition("bubbleId:")
3344
+ ].join(" "),
3345
+ "Cursor global SQLite store"
3346
+ );
3347
+ for (const row of bubbleRows) {
3348
+ const keyMatch = String(row.key || "").match(/^bubbleId:([^:]+):/);
3349
+ if (!keyMatch) continue;
3350
+ const composerId = keyMatch[1].toLowerCase();
3351
+ const model = firstString(row.modelId, row.modelName, row.model);
3352
+ if (!model) continue;
3353
+ const entry = info.get(composerId) || { title: "", modelHist: new Map() };
3354
+ entry.modelHist.set(model, (entry.modelHist.get(model) || 0) + 1);
3355
+ info.set(composerId, entry);
3356
+ }
3357
+ } catch {
3358
+ // global store may be locked or absent; degrade silently.
3359
+ }
3360
+ }
3361
+ const result = new Map();
3362
+ for (const [id, entry] of info) {
3363
+ let dominantModel = "";
3364
+ let bestCount = 0;
3365
+ for (const [model, count] of entry.modelHist || []) {
3366
+ if (count > bestCount) { bestCount = count; dominantModel = model; }
3367
+ }
3368
+ result.set(id, {
3369
+ title: entry.title || "",
3370
+ model: dominantModel || entry.configModel || ""
3371
+ });
3372
+ }
3373
+ return (composerId) => result.get(String(composerId || "").toLowerCase()) || { title: "", model: "" };
3374
+ }
3375
+
2867
3376
  function cursorProjectTranscriptFiles(env = process.env) {
2868
3377
  const root = cursorProjectsRoot(env);
2869
3378
  const files = [];
@@ -2884,13 +3393,28 @@ function groupCursorTranscriptFiles(files, env = process.env) {
2884
3393
  if (agentIndex < 1) continue;
2885
3394
  const projectSlug = parts[0];
2886
3395
  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}`;
3396
+ const subagentIndex = rest.indexOf("subagents");
3397
+ let id;
3398
+ let parentId = "";
3399
+ let root;
3400
+ let key;
3401
+ if (subagentIndex >= 0 && rest.length > subagentIndex + 1) {
3402
+ // <projectSlug>/agent-transcripts/<parent>/subagents/<subagent>/...
3403
+ parentId = rest.slice(0, subagentIndex).filter(Boolean)[0] || "";
3404
+ const subagentRest = rest.slice(subagentIndex + 1);
3405
+ id = subagentRest.length > 1 ? subagentRest[0] : path.basename(file, path.extname(file));
3406
+ root = path.join(projectsRoot, projectSlug, "agent-transcripts", parentId, "subagents", id);
3407
+ key = `${projectSlug}:${parentId}/subagents/${id}`;
3408
+ } else {
3409
+ id = rest.length > 1 ? rest[0] : path.basename(file, path.extname(file));
3410
+ root = path.join(projectsRoot, projectSlug, "agent-transcripts", id);
3411
+ key = `${projectSlug}:${id}`;
3412
+ }
2890
3413
  if (!groups.has(key)) {
2891
3414
  groups.set(key, {
2892
3415
  key,
2893
- id: sessionPart,
3416
+ id,
3417
+ parentId: parentId || undefined,
2894
3418
  projectSlug,
2895
3419
  projectDir: path.join(projectsRoot, projectSlug),
2896
3420
  root,
@@ -2905,16 +3429,28 @@ function groupCursorTranscriptFiles(files, env = process.env) {
2905
3429
 
2906
3430
  function parseCursorTranscriptGroup(group) {
2907
3431
  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;
3432
+ const rawMessages = parsedFiles
3433
+ .flatMap((parsed) => parsed.messages || [])
3434
+ .sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
3435
+ if (!rawMessages.length) return null;
3436
+ const composerInfoLookup = group.composerInfoLookup
3437
+ || (group.composerLookup ? (id) => ({ title: group.composerLookup(id), model: "" }) : () => ({ title: "", model: "" }));
3438
+ const info = composerInfoLookup(group.id);
3439
+ const fallbackModel = info.model || "";
3440
+ const enrichedRaw = fallbackModel
3441
+ ? rawMessages.map((message) => cursorMessageWithFallbackModel(message, fallbackModel))
3442
+ : rawMessages;
3443
+ const messages = stampMessages(dedupeAdjacentMessages(enrichedRaw), "cursor-agent-transcripts");
2912
3444
  const cwd = firstString(...parsedFiles.map((parsed) => parsed.cwd), cursorSlugToPath(group.projectSlug, group.env));
3445
+ const fileTitle = firstString(...parsedFiles.map((parsed) => parsed.title));
3446
+ const userTitle = cursorTranscriptTitleFromMessages(messages);
3447
+ const title = firstString(fileTitle, info.title, userTitle, group.id.replace(/[-_]+/g, " "));
2913
3448
  return {
2914
3449
  sessionId: `cursor-${hashId(group.key)}`,
2915
3450
  id: group.id,
3451
+ parentComposerId: group.parentId || undefined,
2916
3452
  sourceKey: "cursor-agent-transcripts",
2917
- title: firstString(...parsedFiles.map((parsed) => parsed.title), group.id.replace(/[-_]+/g, " ")),
3453
+ title,
2918
3454
  cwd,
2919
3455
  startedAt: messages[0]?.timestamp || new Date().toISOString(),
2920
3456
  endedAt: messages[messages.length - 1]?.timestamp || messages[0]?.timestamp || new Date().toISOString(),
@@ -2928,6 +3464,19 @@ function parseCursorTranscriptGroup(group) {
2928
3464
  };
2929
3465
  }
2930
3466
 
3467
+ function cursorTranscriptTitleFromMessages(messages) {
3468
+ for (const message of messages || []) {
3469
+ if (message.role !== "user") continue;
3470
+ const text = String(message.content || "").trim();
3471
+ if (!text) continue;
3472
+ const firstLineText = text.split(/\r?\n/, 1)[0].trim();
3473
+ if (!firstLineText) continue;
3474
+ const truncated = firstLineText.length > 80 ? firstLineText.slice(0, 77).trimEnd() + "…" : firstLineText;
3475
+ return truncated;
3476
+ }
3477
+ return "";
3478
+ }
3479
+
2931
3480
  function parseCursorTranscriptFile(file) {
2932
3481
  const stat = safeStat(file);
2933
3482
  const fallbackTime = new Date(stat?.mtimeMs || Date.now()).toISOString();
@@ -3231,6 +3780,7 @@ function readCursorGlobalDiskKvSessionsFromDb(dbPath, options = {}) {
3231
3780
  "json_extract(value, '$.workspaceIdentifier') as workspaceIdentifier",
3232
3781
  "json_extract(value, '$.workspace') as workspace",
3233
3782
  "json_extract(value, '$.context') as context",
3783
+ "json_extract(value, '$.modelConfig') as modelConfig",
3234
3784
  "json_extract(value, '$.fullConversationHeadersOnly') as fullConversationHeadersOnly",
3235
3785
  "length(json_extract(value, '$.conversation')) as conversationBytes"
3236
3786
  ].join(", "),
@@ -3289,6 +3839,7 @@ function cursorGlobalComposerDataFromRow(row) {
3289
3839
  workspaceIdentifier: cursorParseSqliteJsonColumn(row.workspaceIdentifier),
3290
3840
  workspace: cursorParseSqliteJsonColumn(row.workspace),
3291
3841
  context: cursorParseSqliteJsonColumn(row.context),
3842
+ modelConfig: cursorParseSqliteJsonColumn(row.modelConfig),
3292
3843
  fullConversationHeadersOnly: cursorParseSqliteJsonColumn(row.fullConversationHeadersOnly),
3293
3844
  _hasConversation: Number(row.conversationBytes || 0) > 2
3294
3845
  };
@@ -3408,6 +3959,8 @@ function cursorGlobalBubbleSelectColumns(valueExpression = "value", keyExpressio
3408
3959
  `json_extract(${valueExpression}, '$.modelID') as modelID`,
3409
3960
  `json_extract(${valueExpression}, '$.modelName') as modelName`,
3410
3961
  `json_extract(${valueExpression}, '$.modelSlug') as modelSlug`,
3962
+ `json_extract(${valueExpression}, '$.modelConfig') as modelConfig`,
3963
+ `json_extract(${valueExpression}, '$.providerOptions') as providerOptions`,
3411
3964
  `json_extract(${valueExpression}, '$.status') as status`,
3412
3965
  `json_extract(${valueExpression}, '$.state') as state`,
3413
3966
  `json_extract(${valueExpression}, '$.phase') as phase`,
@@ -3422,6 +3975,8 @@ function cursorGlobalBubbleSelectColumns(valueExpression = "value", keyExpressio
3422
3975
  `json_extract(${valueExpression}, '$.usage') as usage`,
3423
3976
  `json_extract(${valueExpression}, '$.tokenUsage') as tokenUsage`,
3424
3977
  `json_extract(${valueExpression}, '$.token_usage') as token_usage`,
3978
+ `json_extract(${valueExpression}, '$.tokenCount') as tokenCount`,
3979
+ `json_extract(${valueExpression}, '$.token_count') as token_count`,
3425
3980
  `json_extract(${valueExpression}, '$.tokens') as tokens`,
3426
3981
  `json_extract(${valueExpression}, '$.terminalSelections') as terminalSelections`,
3427
3982
  `json_extract(${valueExpression}, '$.fileSelections') as fileSelections`,
@@ -3458,6 +4013,8 @@ function cursorGlobalBubbleDataFromRow(row) {
3458
4013
  modelID: row.modelID,
3459
4014
  modelName: row.modelName,
3460
4015
  modelSlug: row.modelSlug,
4016
+ modelConfig: cursorParseSqliteJsonColumn(row.modelConfig),
4017
+ providerOptions: cursorParseSqliteJsonColumn(row.providerOptions),
3461
4018
  status: row.status,
3462
4019
  state: cursorParseSqliteJsonColumn(row.state) || row.state,
3463
4020
  phase: row.phase,
@@ -3472,6 +4029,8 @@ function cursorGlobalBubbleDataFromRow(row) {
3472
4029
  usage: cursorParseSqliteJsonColumn(row.usage),
3473
4030
  tokenUsage: cursorParseSqliteJsonColumn(row.tokenUsage),
3474
4031
  token_usage: cursorParseSqliteJsonColumn(row.token_usage),
4032
+ tokenCount: cursorParseSqliteJsonColumn(row.tokenCount),
4033
+ token_count: cursorParseSqliteJsonColumn(row.token_count),
3475
4034
  tokens: cursorParseSqliteJsonColumn(row.tokens),
3476
4035
  terminalSelections: cursorParseSqliteJsonColumn(row.terminalSelections) || [],
3477
4036
  fileSelections: cursorParseSqliteJsonColumn(row.fileSelections) || [],
@@ -3500,6 +4059,7 @@ function cursorGlobalComposerSession(dbPath, composerId, composer, bubbleMap, op
3500
4059
  const headers = cursorGlobalComposerHeaders(composer);
3501
4060
  const startedAt = cursorIso(composer.createdAt || composer.created_at) || eventTimestamp(composer) || new Date().toISOString();
3502
4061
  const endedAt = cursorIso(composer.lastUpdatedAt || composer.updatedAt || composer.updated_at) || startedAt;
4062
+ const composerModel = cursorModel(composer);
3503
4063
  const messages = [];
3504
4064
  for (let index = 0; index < headers.length; index++) {
3505
4065
  const header = headers[index] || {};
@@ -3507,14 +4067,14 @@ function cursorGlobalComposerSession(dbPath, composerId, composer, bubbleMap, op
3507
4067
  const record = bubbleMap.get(bubbleId) || header;
3508
4068
  const timestamp = offsetTimestamp(startedAt, index);
3509
4069
  for (const message of cursorMessagesFromRecord(record, "cursor-global-sqlite", timestamp)) {
3510
- messages.push({ ...message, timestamp });
4070
+ messages.push(cursorMessageWithFallbackModel({ ...message, timestamp }, composerModel));
3511
4071
  }
3512
4072
  }
3513
4073
  if (!messages.length) {
3514
4074
  for (const [index, record] of [...bubbleMap.values()].sort(cursorGlobalBubbleRecordCompare).entries()) {
3515
4075
  const timestamp = offsetTimestamp(startedAt, index);
3516
4076
  for (const message of cursorMessagesFromRecord(record, "cursor-global-sqlite", timestamp)) {
3517
- messages.push({ ...message, timestamp });
4077
+ messages.push(cursorMessageWithFallbackModel({ ...message, timestamp }, composerModel));
3518
4078
  }
3519
4079
  }
3520
4080
  }
@@ -3540,6 +4100,17 @@ function cursorGlobalComposerSession(dbPath, composerId, composer, bubbleMap, op
3540
4100
  };
3541
4101
  }
3542
4102
 
4103
+ function cursorMessageWithFallbackModel(message, model) {
4104
+ if (!model || message?.role !== "assistant" || message?.metadata?.model) return message;
4105
+ return {
4106
+ ...message,
4107
+ metadata: {
4108
+ ...(message.metadata || {}),
4109
+ model
4110
+ }
4111
+ };
4112
+ }
4113
+
3543
4114
  function cursorGlobalComposerHeaders(composer) {
3544
4115
  if (Array.isArray(composer.fullConversationHeadersOnly) && composer.fullConversationHeadersOnly.length) {
3545
4116
  return composer.fullConversationHeadersOnly;
@@ -3894,24 +4465,46 @@ function cursorCwdFromObject(data) {
3894
4465
  );
3895
4466
  const workspaceCwd = cursorExistingCwdFromPath(workspacePath, true);
3896
4467
  if (workspaceCwd) return workspaceCwd;
4468
+ const candidates = cursorPathCandidatesFromValue(data);
4469
+ const mostFrequent = cursorMostFrequentExistingCwd(candidates);
4470
+ if (mostFrequent) return mostFrequent;
3897
4471
  const filePath = cursorFirstPathInObject(data);
3898
4472
  return cursorExistingCwdFromPath(filePath, false);
3899
4473
  }
3900
4474
 
3901
4475
  function cursorCwdFromMessages(messages) {
4476
+ const allCandidates = [];
3902
4477
  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
- }
4478
+ allCandidates.push(...cursorPathCandidatesFromValue(message?.metadata));
3908
4479
  }
3909
4480
  const text = (messages || []).map((message) => message?.content || "").join("\n");
3910
- for (const candidate of cursorPathCandidatesFromValue(text)) {
4481
+ allCandidates.push(...cursorPathCandidatesFromValue(text));
4482
+ const mostFrequent = cursorMostFrequentExistingCwd(allCandidates);
4483
+ if (mostFrequent) return mostFrequent;
4484
+ return cursorCwdFromTerminalPromptText(text);
4485
+ }
4486
+
4487
+ function cursorMostFrequentExistingCwd(candidates) {
4488
+ if (!candidates || !candidates.length) return "";
4489
+ const counts = new Map();
4490
+ const order = [];
4491
+ for (const candidate of candidates) {
3911
4492
  const cwd = cursorExistingCwdFromPath(candidate, false);
3912
- if (cwd) return cwd;
4493
+ if (!cwd) continue;
4494
+ if (!counts.has(cwd)) order.push(cwd);
4495
+ counts.set(cwd, (counts.get(cwd) || 0) + 1);
3913
4496
  }
3914
- return cursorCwdFromTerminalPromptText(text);
4497
+ if (!counts.size) return "";
4498
+ let bestCwd = "";
4499
+ let bestCount = -1;
4500
+ for (const cwd of order) {
4501
+ const count = counts.get(cwd) || 0;
4502
+ if (count > bestCount) {
4503
+ bestCount = count;
4504
+ bestCwd = cwd;
4505
+ }
4506
+ }
4507
+ return bestCwd;
3915
4508
  }
3916
4509
 
3917
4510
  function cursorPathCandidatesFromValue(value, depth = 0, candidates = []) {
@@ -4020,15 +4613,29 @@ function cursorNormalizePathCandidate(value) {
4020
4613
  function cursorAttributionCwd(value) {
4021
4614
  const candidate = cursorNormalizePathCandidate(value);
4022
4615
  if (!candidate || !path.isAbsolute(candidate)) return "";
4023
- const existing = cursorExistingCwdFromPath(candidate, true);
4616
+ if (cursorIsSystemRootPath(candidate)) return "";
4617
+ const climbed = cursorClimbOutOfDependencyDirs(candidate);
4618
+ const existing = cursorExistingCwdFromPath(climbed, true);
4024
4619
  if (existing) return existing;
4025
4620
  const appSupportCursorWorkspace = `${path.sep}Application Support${path.sep}Cursor${path.sep}Workspaces${path.sep}`;
4026
- if (candidate.includes(appSupportCursorWorkspace)) return "";
4621
+ if (climbed.includes(appSupportCursorWorkspace)) return "";
4622
+ return climbed;
4623
+ }
4624
+
4625
+ function cursorClimbOutOfDependencyDirs(candidate) {
4626
+ const segments = candidate.split(path.sep);
4627
+ // Strip trailing path beyond the first node_modules / .pnpm / vendor segment
4628
+ // so workspace folders that point inside a dependency resolve to the project root.
4629
+ for (const marker of ["node_modules", ".pnpm", "bower_components", "vendor"]) {
4630
+ const index = segments.indexOf(marker);
4631
+ if (index > 0) return segments.slice(0, index).join(path.sep) || candidate;
4632
+ }
4027
4633
  return candidate;
4028
4634
  }
4029
4635
 
4030
4636
  function cursorExistingCwdFromPath(candidate, assumeDirectory = false) {
4031
4637
  if (!candidate || !path.isAbsolute(candidate)) return "";
4638
+ if (cursorIsSystemRootPath(candidate)) return "";
4032
4639
  const stat = safeStat(candidate);
4033
4640
  let start = "";
4034
4641
  if (stat?.isDirectory()) start = candidate;
@@ -4042,13 +4649,34 @@ function cursorExistingCwdFromPath(candidate, assumeDirectory = false) {
4042
4649
  function cursorNearestProjectDir(start) {
4043
4650
  let current = path.resolve(start);
4044
4651
  for (;;) {
4045
- if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, "package.json"))) return current;
4652
+ if (cursorIsSystemRootPath(current)) return "";
4653
+ const insideDependency = cursorPathInsideDependencyDir(current);
4654
+ const hasMarker = fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, "package.json"));
4655
+ if (hasMarker && !insideDependency) return current;
4046
4656
  const parent = path.dirname(current);
4047
- if (parent === current) return fs.existsSync(start) ? start : "";
4657
+ if (parent === current) return cursorIsSystemRootPath(start) ? "" : (fs.existsSync(start) ? start : "");
4048
4658
  current = parent;
4049
4659
  }
4050
4660
  }
4051
4661
 
4662
+ function cursorPathInsideDependencyDir(candidate) {
4663
+ const segments = String(candidate || "").split(path.sep);
4664
+ return ["node_modules", ".pnpm", "bower_components", "vendor"].some((marker) => segments.includes(marker));
4665
+ }
4666
+
4667
+ function cursorIsSystemRootPath(candidate) {
4668
+ const normalized = String(candidate || "").replace(/\/+$/, "") || "/";
4669
+ if (normalized === "/" || normalized === path.parse(normalized).root) return true;
4670
+ const home = os.homedir();
4671
+ if (normalized === home) return true;
4672
+ // Top-level system directories that are never project roots on their own.
4673
+ const blocked = new Set([
4674
+ "/Users", "/home", "/Volumes", "/private", "/tmp", "/var",
4675
+ "/Applications", "/Library", "/System", "/opt", "/etc", "/usr"
4676
+ ]);
4677
+ return blocked.has(normalized);
4678
+ }
4679
+
4052
4680
  function readSqliteJson(dbPath, query, label = "SQLite store") {
4053
4681
  const result = runSqliteJson(dbPath, query);
4054
4682
  if (result.ok) return parseSqliteJson(result.stdout);
@@ -4058,6 +4686,7 @@ function readSqliteJson(dbPath, query, label = "SQLite store") {
4058
4686
 
4059
4687
  function runSqliteJson(dbPath, query) {
4060
4688
  const result = spawnSync("sqlite3", [dbPath, "-json", query], {
4689
+ argv0: "agentlog-sqlite",
4061
4690
  encoding: "utf8",
4062
4691
  maxBuffer: 1024 * 1024 * 200,
4063
4692
  timeout: SQLITE_QUERY_TIMEOUT_MS
@@ -4131,9 +4760,13 @@ function extractCursorConversations(data, sourceKey, fallbackTime) {
4131
4760
  const visit = (node) => {
4132
4761
  if (!node || typeof node !== "object") return;
4133
4762
  if (Array.isArray(node.bubbles)) {
4134
- const messages = node.bubbles
4763
+ const containerModel = cursorModel(node);
4764
+ const rawMessages = node.bubbles
4135
4765
  .flatMap((bubble, index) => cursorMessagesFromRecord(bubble, sourceKey, offsetTimestamp(fallbackTime, index)))
4136
4766
  .filter(Boolean);
4767
+ const messages = containerModel
4768
+ ? rawMessages.map((message) => cursorMessageWithFallbackModel(message, containerModel))
4769
+ : rawMessages;
4137
4770
  if (messages.length) {
4138
4771
  conversations.push({
4139
4772
  id: node.tabId || node.composerId || node.id || hashId(JSON.stringify(messages.slice(0, 2))),
@@ -4297,6 +4930,7 @@ function cursorAiServiceGenerationMessage(entry, fallbackTime, index) {
4297
4930
  const timestamp = cursorIso(entry.unixMs) || offsetTimestamp(fallbackTime, index);
4298
4931
  const role = type === "composer" ? "user" : "assistant";
4299
4932
  const content = type === "apply" ? `Applied changes: ${text}` : text;
4933
+ const commandType = cursorNormalizeCommandType(entry.commandType ?? entry.command_type);
4300
4934
  return {
4301
4935
  role,
4302
4936
  content,
@@ -4305,7 +4939,9 @@ function cursorAiServiceGenerationMessage(entry, fallbackTime, index) {
4305
4939
  provider: "cursor",
4306
4940
  source: "cursor-ai-service-history",
4307
4941
  eventType: firstString(type, "aiService.generation"),
4308
- requestId: firstString(entry.generationUUID, entry.generationId, entry.id) || undefined
4942
+ requestId: firstString(entry.generationUUID, entry.generationId, entry.id) || undefined,
4943
+ commandType: commandType || undefined,
4944
+ model: cursorModel(entry) || undefined
4309
4945
  }
4310
4946
  };
4311
4947
  }
@@ -4313,6 +4949,7 @@ function cursorAiServiceGenerationMessage(entry, fallbackTime, index) {
4313
4949
  function cursorAiServicePromptMessage(prompt, fallbackTime, index) {
4314
4950
  const text = firstString(prompt?.text, prompt?.prompt, prompt?.value);
4315
4951
  if (!text) return null;
4952
+ const commandType = cursorNormalizeCommandType(prompt?.commandType ?? prompt?.command_type);
4316
4953
  return {
4317
4954
  role: "user",
4318
4955
  content: text,
@@ -4320,11 +4957,31 @@ function cursorAiServicePromptMessage(prompt, fallbackTime, index) {
4320
4957
  metadata: {
4321
4958
  provider: "cursor",
4322
4959
  source: "cursor-ai-service-history",
4323
- eventType: "aiService.prompt"
4960
+ eventType: "aiService.prompt",
4961
+ commandType: commandType || undefined
4324
4962
  }
4325
4963
  };
4326
4964
  }
4327
4965
 
4966
+ function cursorNormalizeCommandType(value) {
4967
+ if (value == null || value === "") return "";
4968
+ const text = String(value).trim();
4969
+ if (!text) return "";
4970
+ // Cursor stores commandType as either an integer enum or a string. Normalize the
4971
+ // common integer mapping so downstream consumers see human-readable labels.
4972
+ const numeric = Number(text);
4973
+ if (Number.isInteger(numeric)) {
4974
+ switch (numeric) {
4975
+ case 1: return "chat";
4976
+ case 2: return "edit";
4977
+ case 3: return "agent";
4978
+ case 4: return "ask";
4979
+ default: return `command-${numeric}`;
4980
+ }
4981
+ }
4982
+ return text.toLowerCase();
4983
+ }
4984
+
4328
4985
  function cursorMs(...values) {
4329
4986
  for (const value of values) {
4330
4987
  if (value == null || value === "") continue;
@@ -4476,43 +5133,184 @@ function cursorStructuredText(value) {
4476
5133
 
4477
5134
  function cursorMessageMetadata(record, source) {
4478
5135
  const usage = cursorUsage(record);
5136
+ const commandType = cursorNormalizeCommandType(record?.commandType ?? record?.command_type ?? record?.message?.commandType);
4479
5137
  return {
4480
5138
  provider: "cursor",
4481
5139
  source,
4482
5140
  bubbleType: String(record?.type || "").trim() || undefined,
4483
5141
  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,
5142
+ model: cursorModel(record) || undefined,
4485
5143
  status: firstString(record?.status, record?.state, record?.phase) || undefined,
4486
5144
  requestId: firstString(record?.requestId, record?.request_id, record?.messageId, record?.messageID) || undefined,
4487
5145
  composerId: firstString(record?.composerId, record?.composerID, record?.conversationId, record?.conversation_id) || undefined,
5146
+ commandType: commandType || undefined,
4488
5147
  usage: usage || undefined
4489
5148
  };
4490
5149
  }
4491
5150
 
5151
+ function cursorModel(record) {
5152
+ return firstCursorModel(
5153
+ record?.model,
5154
+ record?.modelId,
5155
+ record?.modelID,
5156
+ record?.modelName,
5157
+ record?.modelSlug,
5158
+ record?.message?.model,
5159
+ record?.message?.modelId,
5160
+ record?.message?.modelName,
5161
+ record?.providerOptions?.cursor?.modelName,
5162
+ record?.providerOptions?.cursor?.modelId,
5163
+ record?.provider_options?.cursor?.modelName,
5164
+ record?.provider_options?.cursor?.modelId,
5165
+ record?.message?.providerOptions?.cursor?.modelName,
5166
+ record?.message?.providerOptions?.cursor?.modelId,
5167
+ cursorModelFromModelConfig(record?.modelConfig),
5168
+ cursorModelFromModelConfig(record?.message?.modelConfig),
5169
+ cursorModelFromParts(record?.content),
5170
+ cursorModelFromParts(record?.message?.content),
5171
+ cursorModelFromParts(record?.parts)
5172
+ );
5173
+ }
5174
+
5175
+ function cursorModelFromModelConfig(config) {
5176
+ if (!config || typeof config !== "object") return "";
5177
+ const direct = firstCursorModel(config.modelName, config.model, config.modelId, config.model_slug, config.modelSlug);
5178
+ if (direct) return direct;
5179
+ const selected = Array.isArray(config.selectedModels) ? config.selectedModels : [];
5180
+ for (const item of selected) {
5181
+ const model = firstCursorModel(item?.modelName, item?.modelId, item?.id, item?.model);
5182
+ if (model) return model;
5183
+ }
5184
+ return "";
5185
+ }
5186
+
5187
+ function cursorModelFromParts(value, depth = 0) {
5188
+ if (!value || depth > 5) return "";
5189
+ if (Array.isArray(value)) {
5190
+ for (const item of value) {
5191
+ const model = cursorModelFromParts(item, depth + 1);
5192
+ if (model) return model;
5193
+ }
5194
+ return "";
5195
+ }
5196
+ if (typeof value !== "object") return "";
5197
+ const direct = firstCursorModel(
5198
+ value.providerOptions?.cursor?.modelName,
5199
+ value.providerOptions?.cursor?.modelId,
5200
+ value.provider_options?.cursor?.modelName,
5201
+ value.provider_options?.cursor?.modelId
5202
+ );
5203
+ if (direct) return direct;
5204
+ const configured = cursorModelFromModelConfig(value.modelConfig);
5205
+ if (configured) return configured;
5206
+ for (const child of Object.values(value)) {
5207
+ if (child && typeof child === "object") {
5208
+ const model = cursorModelFromParts(child, depth + 1);
5209
+ if (model) return model;
5210
+ }
5211
+ }
5212
+ return "";
5213
+ }
5214
+
5215
+ function firstCursorModel(...values) {
5216
+ for (const value of values) {
5217
+ if (typeof value !== "string") continue;
5218
+ const trimmed = value.trim();
5219
+ if (!trimmed || /^default$/i.test(trimmed)) continue;
5220
+ return trimmed;
5221
+ }
5222
+ return "";
5223
+ }
5224
+
4492
5225
  function cursorUsage(record) {
4493
5226
  const candidates = [
5227
+ record?.tokenCount,
5228
+ record?.token_count,
4494
5229
  record?.usage,
4495
5230
  record?.tokenUsage,
4496
5231
  record?.token_usage,
4497
5232
  record?.tokens,
4498
5233
  record?.metrics?.usage,
5234
+ record?.message?.tokenCount,
5235
+ record?.message?.token_count,
4499
5236
  record?.message?.usage,
4500
5237
  record?.message?.tokenUsage
4501
5238
  ];
5239
+ let input = null;
5240
+ let output = null;
5241
+ let cacheInput = null;
5242
+ let total = null;
4502
5243
  for (const item of candidates) {
4503
5244
  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;
5245
+ input = preferredCursorTokenValue(input, numericValue(
5246
+ item.inputTokens,
5247
+ item.input_tokens,
5248
+ item.inputTokenCount,
5249
+ item.input_token_count,
5250
+ item.promptTokens,
5251
+ item.prompt_tokens,
5252
+ item.promptTokenCount,
5253
+ item.prompt_token_count,
5254
+ item.prompt
5255
+ ));
5256
+ output = preferredCursorTokenValue(output, numericValue(
5257
+ item.outputTokens,
5258
+ item.output_tokens,
5259
+ item.outputTokenCount,
5260
+ item.output_token_count,
5261
+ item.completionTokens,
5262
+ item.completion_tokens,
5263
+ item.completionTokenCount,
5264
+ item.completion_token_count,
5265
+ item.completion
5266
+ ));
5267
+ cacheInput = preferredCursorTokenValue(cacheInput, cursorCacheInputTokens(item));
5268
+ total = preferredCursorTokenValue(total, numericValue(item.totalTokens, item.total_tokens, item.totalTokenCount, item.total_token_count, item.total));
5269
+ }
5270
+ if (![input, output, cacheInput, total].some((value) => value != null && value > 0)) return null;
5271
+ const usage = {};
5272
+ if (input != null) usage.inputTokens = input;
5273
+ if (output != null) usage.outputTokens = output;
5274
+ if (cacheInput != null) usage.cacheInputTokens = cacheInput;
5275
+ if (total != null) usage.totalTokens = total;
5276
+ else if (input != null || output != null) usage.totalTokens = (input || 0) + (output || 0);
5277
+ return usage;
5278
+ }
5279
+
5280
+ function preferredCursorTokenValue(current, next) {
5281
+ if (next == null) return current;
5282
+ if (current == null) return next;
5283
+ if (current <= 0 && next > 0) return next;
5284
+ return current;
5285
+ }
5286
+
5287
+ function cursorCacheInputTokens(item) {
5288
+ const cacheInput = numericValue(item.cacheInputTokens, item.cache_input_tokens);
5289
+ const cacheCreation = numericValue(
5290
+ item.cacheCreationInputTokens,
5291
+ item.cache_creation_input_tokens,
5292
+ item.cacheCreationTokens,
5293
+ item.cache_creation_tokens
5294
+ );
5295
+ const cacheRead = numericValue(
5296
+ item.cacheReadInputTokens,
5297
+ item.cache_read_input_tokens,
5298
+ item.cacheReadTokens,
5299
+ item.cache_read_tokens
5300
+ );
5301
+ const cached = numericValue(
5302
+ item.cachedContentTokenCount,
5303
+ item.cached_content_token_count,
5304
+ item.cachedTokens,
5305
+ item.cached_tokens,
5306
+ item.cacheTokens,
5307
+ item.cache_tokens,
5308
+ item.cached
5309
+ );
5310
+ const total = [cacheInput, cacheCreation, cacheRead, cached]
5311
+ .filter((value) => value != null && value > 0)
5312
+ .reduce((sum, value) => sum + value, 0);
5313
+ return total > 0 ? total : null;
4516
5314
  }
4517
5315
 
4518
5316
  function cursorToolCallsFromRecord(record) {
@@ -4622,7 +5420,7 @@ function cursorToolName(node, keyToken, type) {
4622
5420
  if (node.command || node.cmd || node.terminalCommand) return "run_terminal_cmd";
4623
5421
  if (node.diff || node.patch || node.old_string || node.new_string || node.oldText || node.newText) return "edit";
4624
5422
  if (node.query || /search|grep/.test(type || keyToken)) return "search";
4625
- if (node.path || node.file || node.uri) {
5423
+ if (node.path || node.file || node.filename || node.filePath || node.fsPath || node.uri) {
4626
5424
  if (/read|open|view/.test(type || keyToken)) return "read_file";
4627
5425
  if (/write|edit|diff|patch/.test(type || keyToken)) return "edit_file";
4628
5426
  }
@@ -4634,16 +5432,22 @@ function cursorToolArguments(node, name, keyToken, type) {
4634
5432
  const value = node.input ?? node.args ?? node.arguments ?? node.params ?? node.parameters ?? node.function?.arguments ?? action.input;
4635
5433
  if (value != null) return value;
4636
5434
  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 };
5435
+ 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
5436
  if (node.old_string || node.new_string || node.oldText || node.newText) {
4639
5437
  return {
4640
- path: firstString(node.path, node.file, node.filename) || undefined,
5438
+ path: firstString(node.path, node.file, node.filename, node.filePath, node.fsPath) || undefined,
4641
5439
  old_string: firstString(node.old_string, node.oldText),
4642
5440
  new_string: firstString(node.new_string, node.newText)
4643
5441
  };
4644
5442
  }
4645
5443
  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 };
5444
+ if (node.path || node.file || node.filename || node.filePath || node.fsPath || node.uri) {
5445
+ return {
5446
+ path: firstString(node.path, node.file, node.filename, node.filePath, node.fsPath, cursorUriPath(node.uri)),
5447
+ instruction: firstString(node.instruction, node.text, node.content) || undefined,
5448
+ name: name || type || keyToken
5449
+ };
5450
+ }
4647
5451
  return null;
4648
5452
  }
4649
5453
 
@@ -4653,7 +5457,11 @@ function cursorDiffToolCalls(node) {
4653
5457
  .concat(Array.isArray(node.diffs) ? node.diffs : [])
4654
5458
  .concat(Array.isArray(node.fileDiffs) ? node.fileDiffs : [])
4655
5459
  .concat(Array.isArray(node.file_diffs) ? node.file_diffs : [])
4656
- .concat(Array.isArray(node.edits) ? node.edits : []);
5460
+ .concat(Array.isArray(node.edits) ? node.edits : [])
5461
+ .concat(Array.isArray(node.suggestedCodeBlocks) ? node.suggestedCodeBlocks : [])
5462
+ .concat(Array.isArray(node.suggested_code_blocks) ? node.suggested_code_blocks : [])
5463
+ .concat(Array.isArray(node.diffHistories) ? node.diffHistories : [])
5464
+ .concat(Array.isArray(node.diff_histories) ? node.diff_histories : []);
4657
5465
  return diffs.map((diff) => cursorNormalizeToolCall(diff, "fileDiffs")).filter(Boolean);
4658
5466
  }
4659
5467
 
@@ -4703,9 +5511,9 @@ function cursorLooksLikeToolResult(node, keyToken, type) {
4703
5511
 
4704
5512
  function cursorToolTarget(args, node) {
4705
5513
  if (args && typeof args === "object" && !Array.isArray(args)) {
4706
- return firstString(args.path, args.file, args.filename, args.target_file, args.targetFile, args.uri);
5514
+ return firstString(args.path, args.file, args.filename, args.filePath, args.fsPath, args.target_file, args.targetFile, args.uri);
4707
5515
  }
4708
- return firstString(node?.path, node?.file, node?.filename, node?.target_file, node?.targetFile, node?.uri);
5516
+ return firstString(node?.path, node?.file, node?.filename, node?.filePath, node?.fsPath, node?.target_file, node?.targetFile, node?.uri);
4709
5517
  }
4710
5518
 
4711
5519
  function dedupeCursorToolCalls(calls) {
@@ -4761,8 +5569,12 @@ function cursorBubbleContext(bubble) {
4761
5569
  if (value) parts.push(`Terminal selection:\n${value}`);
4762
5570
  }
4763
5571
  }
4764
- if (Array.isArray(bubble.fileSelections)) {
4765
- const files = bubble.fileSelections.map((selection) => selection.uri?.fsPath || selection.uri?.path).filter(Boolean);
5572
+ if (Array.isArray(bubble.fileSelections) || Array.isArray(bubble.selections)) {
5573
+ const files = []
5574
+ .concat(Array.isArray(bubble.fileSelections) ? bubble.fileSelections : [])
5575
+ .concat(Array.isArray(bubble.selections) ? bubble.selections : [])
5576
+ .map((selection) => cursorUriPath(selection?.uri) || cursorNormalizePathCandidate(firstString(selection?.fsPath, selection?.path, selection?.filePath)))
5577
+ .filter(Boolean);
4766
5578
  if (files.length) parts.push(`Files:\n${files.join("\n")}`);
4767
5579
  }
4768
5580
  const nestedPaths = [
@@ -5045,17 +5857,106 @@ function dedupeCursorSessions(sessions) {
5045
5857
  seen.add(key);
5046
5858
  exact.push(session);
5047
5859
  }
5048
- const contentDeduped = cursorDedupeSessionsByContent(exact);
5860
+ cursorPropagateCwdFromComposerSiblings(exact);
5861
+ const composerDeduped = cursorDedupeByComposerId(exact);
5862
+ const contentDeduped = cursorDedupeSessionsByContent(composerDeduped);
5049
5863
  cursorPropagateCwdFromFallbackSessions(contentDeduped);
5050
5864
  return contentDeduped.filter(
5051
5865
  (session) =>
5052
5866
  !cursorSessionExactDuplicateInBetterSession(session, contentDeduped) &&
5053
5867
  !cursorSessionNearDuplicateInBetterSession(session, contentDeduped) &&
5054
5868
  !cursorSessionCoveredByBetterSessions(session, contentDeduped) &&
5055
- !cursorSessionContainedInBetterSession(session, contentDeduped)
5869
+ !cursorSessionContainedInBetterSession(session, contentDeduped) &&
5870
+ !cursorSessionIsEmptyApplyStub(session, contentDeduped)
5056
5871
  );
5057
5872
  }
5058
5873
 
5874
+ function cursorPropagateCwdFromComposerSiblings(sessions) {
5875
+ const cwdByComposerId = new Map();
5876
+ for (const session of sessions) {
5877
+ const composerId = cursorSessionComposerId(session);
5878
+ if (!composerId || !session.cwd) continue;
5879
+ const existing = cwdByComposerId.get(composerId);
5880
+ if (!existing || cursorSourceRank(session) > existing.rank) {
5881
+ cwdByComposerId.set(composerId, { cwd: session.cwd, rank: cursorSourceRank(session) });
5882
+ }
5883
+ }
5884
+ for (const target of sessions) {
5885
+ if (!target || target.cwd) continue;
5886
+ const composerId = cursorSessionComposerId(target);
5887
+ if (!composerId) continue;
5888
+ const sibling = cwdByComposerId.get(composerId);
5889
+ if (!sibling?.cwd) continue;
5890
+ target.cwd = sibling.cwd;
5891
+ if (target.scopeCanonical === "cursor/uncategorized") target.scopeCanonical = "";
5892
+ }
5893
+ }
5894
+
5895
+ function cursorDedupeByComposerId(sessions) {
5896
+ const byComposerId = new Map();
5897
+ const result = [];
5898
+ for (const session of sessions) {
5899
+ const composerId = cursorSessionComposerId(session);
5900
+ if (!composerId) {
5901
+ result.push(session);
5902
+ continue;
5903
+ }
5904
+ const existing = byComposerId.get(composerId);
5905
+ if (!existing) {
5906
+ byComposerId.set(composerId, session);
5907
+ result.push(session);
5908
+ continue;
5909
+ }
5910
+ if (cursorPreferSession(session, existing) === session) {
5911
+ const index = result.indexOf(existing);
5912
+ if (index >= 0) result[index] = session;
5913
+ byComposerId.set(composerId, session);
5914
+ }
5915
+ }
5916
+ return result;
5917
+ }
5918
+
5919
+ function cursorSessionComposerId(session) {
5920
+ if (!session) return "";
5921
+ const direct = firstString(session.composerId, session.id);
5922
+ if (cursorLooksLikeComposerId(direct)) return String(direct).trim();
5923
+ const fromPath = cursorComposerIdFromSourcePath(session.sourcePath || "");
5924
+ if (fromPath) return fromPath;
5925
+ return "";
5926
+ }
5927
+
5928
+ function cursorComposerIdFromSourcePath(sourcePath) {
5929
+ const text = String(sourcePath || "");
5930
+ if (!text) return "";
5931
+ 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);
5932
+ if (!matches) return "";
5933
+ // Prefer the last UUID in the path — that's the composer/transcript id, not the workspace hash.
5934
+ return matches[matches.length - 1].toLowerCase();
5935
+ }
5936
+
5937
+ function cursorSessionIsEmptyApplyStub(session, sessions) {
5938
+ if (!session || !cursorLikelyCursorPromptHistoryFallback(session)) return false;
5939
+ if (!cursorSessionAssistantMessagesAreOnlyApplyStubs(session)) return false;
5940
+ const userProbes = cursorDedupeUserProbes(session);
5941
+ const rank = cursorSourceRank(session);
5942
+ for (const other of sessions) {
5943
+ if (other === session) continue;
5944
+ if (cursorSourceRank(other) <= rank) continue;
5945
+ if (!cursorSessionsComparable(session, other)) continue;
5946
+ if (!userProbes.length) return true;
5947
+ const text = cursorSessionSearchText(other);
5948
+ if (userProbes.some((probe) => text.includes(probe))) return true;
5949
+ }
5950
+ return false;
5951
+ }
5952
+
5953
+ function cursorSessionAssistantMessagesAreOnlyApplyStubs(session) {
5954
+ const messages = session?.messages || [];
5955
+ const assistantMessages = messages.filter((message) => message.role === "assistant");
5956
+ if (!assistantMessages.length) return true;
5957
+ return assistantMessages.every((message) => /^applied changes:/i.test(String(message.content || "").trim()));
5958
+ }
5959
+
5059
5960
  function cursorPropagateCwdFromFallbackSessions(sessions) {
5060
5961
  for (const target of sessions) {
5061
5962
  if (!target || target.cwd) continue;
@@ -5261,6 +6162,9 @@ function cursorSessionSearchText(session) {
5261
6162
  }
5262
6163
 
5263
6164
  function cursorSessionsComparable(left, right) {
6165
+ const leftId = cursorSessionComposerId(left);
6166
+ const rightId = cursorSessionComposerId(right);
6167
+ if (leftId && rightId && leftId === rightId) return true;
5264
6168
  if (left.cwd && right.cwd && left.cwd !== right.cwd) return false;
5265
6169
  return true;
5266
6170
  }
@@ -5286,7 +6190,11 @@ function pruneCursorArchivedDuplicates(env = process.env, state = null, options
5286
6190
  const archived = listSessions(env)
5287
6191
  .filter((session) => session.provider === "cursor")
5288
6192
  .filter((session) => !sourcePaths || sourcePaths.has(cursorSessionSourcePath(session)))
5289
- .map((session) => cursorSessionWithInferredCwd({ ...session, messages: readTranscript(session.transcriptPath) }));
6193
+ .map((session) => cursorSessionWithInferredCwd({
6194
+ ...session,
6195
+ id: session.composerId || cursorComposerIdFromSourcePath(session.sourcePath || ""),
6196
+ messages: readTranscript(session.transcriptPath)
6197
+ }));
5290
6198
  const kept = new Set(dedupeCursorSessions(archived).map((session) => session.sessionId));
5291
6199
  let pruned = 0;
5292
6200
  for (const session of archived) {
@@ -5630,14 +6538,30 @@ function clineTitle(messages) {
5630
6538
  }
5631
6539
 
5632
6540
  function readOpenCodeSessions(env = process.env, options = {}) {
6541
+ const dbs = openCodeDatabaseFiles(env);
5633
6542
  const roots = openCodeStorageRoots(env);
5634
6543
  const files = roots.flatMap((root) => openCodeSessionFiles(root).map((file) => ({ root, file })));
5635
6544
  const sessions = [];
6545
+ reportDiscoveryProgress(options, { current: 0, total: dbs.length, message: "reading OpenCode SQLite stores" });
6546
+ for (let index = 0; index < dbs.length; index++) {
6547
+ const dbSessions = readOpenCodeSqliteSessionsFromDb(dbs[index]);
6548
+ sessions.push(...dbSessions);
6549
+ reportDiscoveryProgress(options, {
6550
+ current: index + 1,
6551
+ total: dbs.length,
6552
+ message: `${dbSessions.length} SQLite sessions`,
6553
+ path: dbs[index]
6554
+ });
6555
+ }
6556
+ const seenSessionIds = new Set();
5636
6557
  reportDiscoveryProgress(options, { current: 0, total: files.length, message: "reading OpenCode storage" });
5637
6558
  for (let index = 0; index < files.length; index++) {
5638
6559
  const item = files[index];
5639
6560
  const session = parseOpenCodeSessionFile(item.file, item.root);
5640
- if (session) sessions.push(session);
6561
+ if (session) {
6562
+ sessions.push(session);
6563
+ seenSessionIds.add(session.sessionId.replace(/^opencode-/, ""));
6564
+ }
5641
6565
  reportDiscoveryProgress(options, {
5642
6566
  current: index + 1,
5643
6567
  total: files.length,
@@ -5645,28 +6569,69 @@ function readOpenCodeSessions(env = process.env, options = {}) {
5645
6569
  path: item.file
5646
6570
  });
5647
6571
  }
6572
+ for (const root of roots) {
6573
+ for (const sessionId of openCodeMessageSessionIds(root)) {
6574
+ if (seenSessionIds.has(sessionId)) continue;
6575
+ const session = parseOpenCodeMessageOnlySession(root, sessionId);
6576
+ if (session) {
6577
+ sessions.push(session);
6578
+ seenSessionIds.add(sessionId);
6579
+ }
6580
+ }
6581
+ }
5648
6582
  return dedupeStructuredSessions(sessions, "opencode");
5649
6583
  }
5650
6584
 
6585
+ function openCodeDataRoots(env = process.env) {
6586
+ const configured = env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR;
6587
+ if (configured) return existingUniquePaths([configured]);
6588
+ const home = env.HOME || os.homedir();
6589
+ const roots = [
6590
+ path.join(home, ".local", "share", "opencode"),
6591
+ path.join(home, "Library", "Application Support", "opencode"),
6592
+ path.join(home, ".local", "share", "ai.opencode.app"),
6593
+ path.join(home, "Library", "Application Support", "ai.opencode.app")
6594
+ ];
6595
+ const appData = env.APPDATA || env.LOCALAPPDATA || env.LocalAppData;
6596
+ if (appData) {
6597
+ roots.push(path.join(appData, "opencode"));
6598
+ roots.push(path.join(appData, "ai.opencode.app"));
6599
+ }
6600
+ return existingUniquePaths(roots);
6601
+ }
6602
+
5651
6603
  function openCodeStorageRoots(env = process.env) {
5652
6604
  const explicit = envPathList(env.AGENTLOG_OPENCODE_STORAGE_ROOTS || env.AGENTLOG_OPENCODE_STORAGE_DIR);
5653
6605
  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"));
6606
+ const dataRoots = openCodeDataRoots(env);
6607
+ const roots = [];
6608
+ for (const dataRoot of dataRoots) {
6609
+ roots.push(path.join(dataRoot, "storage"));
6610
+ const projectRoot = path.join(dataRoot, "project");
6611
+ let entries = [];
6612
+ try {
6613
+ entries = fs.readdirSync(projectRoot, { withFileTypes: true });
6614
+ } catch {
6615
+ entries = [];
6616
+ }
6617
+ for (const entry of entries) {
6618
+ if (entry.isDirectory()) roots.push(path.join(projectRoot, entry.name, "storage"));
6619
+ }
6620
+ if (path.basename(dataRoot) === "storage") roots.push(dataRoot);
5665
6621
  }
5666
- if (path.basename(dataRoot) === "storage") roots.push(dataRoot);
5667
6622
  return existingUniquePaths(roots);
5668
6623
  }
5669
6624
 
6625
+ function openCodeDatabaseFiles(env = process.env) {
6626
+ const explicit = envPathList(env.AGENTLOG_OPENCODE_DB || env.AGENTLOG_OPENCODE_DATABASE || env.OPENCODE_DB);
6627
+ if (explicit.length) return existingUniquePaths(explicit);
6628
+ if ((env.AGENTLOG_OPENCODE_STORAGE_ROOTS || env.AGENTLOG_OPENCODE_STORAGE_DIR) && !(env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR)) return [];
6629
+ return existingUniquePaths(openCodeDataRoots(env).flatMap((root) => [
6630
+ path.join(root, "opencode.db"),
6631
+ path.join(root, "storage", "opencode.db")
6632
+ ]));
6633
+ }
6634
+
5670
6635
  function openCodeSessionFiles(root) {
5671
6636
  const sessionRoot = path.join(root, "session");
5672
6637
  const files = [];
@@ -5676,6 +6641,216 @@ function openCodeSessionFiles(root) {
5676
6641
  return files.sort((a, b) => a.localeCompare(b));
5677
6642
  }
5678
6643
 
6644
+ function openCodeMessageSessionIds(root) {
6645
+ const messageRoot = path.join(root, "message");
6646
+ let entries = [];
6647
+ try {
6648
+ entries = fs.readdirSync(messageRoot, { withFileTypes: true });
6649
+ } catch {
6650
+ return [];
6651
+ }
6652
+ return entries
6653
+ .filter((entry) => entry.isDirectory())
6654
+ .map((entry) => entry.name)
6655
+ .filter(Boolean)
6656
+ .sort((a, b) => a.localeCompare(b));
6657
+ }
6658
+
6659
+ function readOpenCodeSqliteSessionsFromDb(dbPath) {
6660
+ if (!safeStat(dbPath)) return [];
6661
+ if (!sqliteTableExists(dbPath, "session") || !sqliteTableExists(dbPath, "message") || !sqliteTableExists(dbPath, "part")) return [];
6662
+ const sessionRows = readOpenCodeSqliteSessionRows(dbPath);
6663
+ const messageRows = readOpenCodeSqliteMessageRows(dbPath);
6664
+ const partRows = readOpenCodeSqlitePartRows(dbPath);
6665
+ const messagesBySession = groupRowsBy(messageRows, "session_id");
6666
+ const partsByMessage = groupRowsBy(partRows, "message_id");
6667
+ const storageRoot = path.join(path.dirname(dbPath), "storage");
6668
+ const sessions = [];
6669
+ for (const row of sessionRows) {
6670
+ const rows = messagesBySession.get(row.id) || [];
6671
+ const messages = stampMessages(
6672
+ dedupeAdjacentMessages(rows.flatMap((messageRow, index) => openCodeSqliteMessagesFromRow(messageRow, partsByMessage.get(messageRow.id) || [], index)))
6673
+ .sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
6674
+ "opencode-sqlite-history"
6675
+ );
6676
+ const diffFile = path.join(storageRoot, "session_diff", `${row.id}.json`);
6677
+ const diffMessage = openCodeDiffMessage(diffFile, messages[messages.length - 1]?.timestamp || toIso(row.time_updated || row.time_created));
6678
+ const finalMessages = diffMessage ? messages.concat(stampMessages([diffMessage], "opencode-sqlite-history")) : messages;
6679
+ if (!finalMessages.length) continue;
6680
+ const sourceFiles = [dbPath, safeStat(diffFile) ? diffFile : ""].filter(Boolean);
6681
+ const startedAt = toIso(row.time_created) || finalMessages[0]?.timestamp || new Date(safeStat(dbPath)?.mtimeMs || Date.now()).toISOString();
6682
+ const endedAt = toIso(row.time_updated) || finalMessages[finalMessages.length - 1]?.timestamp || startedAt;
6683
+ const cwd = firstString(row.directory, row.path, row.project_worktree, openCodeCwdFromMessages(finalMessages));
6684
+ sessions.push({
6685
+ sessionId: `opencode-${row.id}`,
6686
+ title: firstString(row.title, row.slug, clineTitle(finalMessages), row.id),
6687
+ cwd,
6688
+ startedAt,
6689
+ endedAt,
6690
+ messages: finalMessages,
6691
+ sourcePath: `${dbPath}#${row.id}`,
6692
+ sourceFiles,
6693
+ sourceType: "opencode-sqlite-history",
6694
+ fingerprint: openCodeSqliteSessionFingerprint(dbPath, row, rows, sourceFiles),
6695
+ detailKey: "sqliteSessions",
6696
+ sessionSummary: {
6697
+ projectId: row.project_id || undefined,
6698
+ parentId: row.parent_id || undefined,
6699
+ workspaceId: row.workspace_id || undefined,
6700
+ slug: row.slug || undefined,
6701
+ version: row.version || undefined,
6702
+ agent: row.agent || undefined,
6703
+ model: row.model || undefined,
6704
+ projectName: row.project_name || undefined,
6705
+ projectWorktree: row.project_worktree || undefined
6706
+ }
6707
+ });
6708
+ }
6709
+ return sessions;
6710
+ }
6711
+
6712
+ function readOpenCodeSqliteSessionRows(dbPath) {
6713
+ const sessionColumns = sqliteTableColumns(dbPath, "session");
6714
+ if (!sessionColumns.has("id")) return [];
6715
+ const projectColumns = sqliteTableExists(dbPath, "project") ? sqliteTableColumns(dbPath, "project") : new Set();
6716
+ const canJoinProject = sessionColumns.has("project_id") && projectColumns.has("id");
6717
+ const selects = [
6718
+ "s.id",
6719
+ sqliteSelectMaybe(sessionColumns, "s", "project_id"),
6720
+ sqliteSelectMaybe(sessionColumns, "s", "parent_id"),
6721
+ sqliteSelectMaybe(sessionColumns, "s", "slug"),
6722
+ sqliteSelectMaybe(sessionColumns, "s", "directory"),
6723
+ sqliteSelectMaybe(sessionColumns, "s", "title"),
6724
+ sqliteSelectMaybe(sessionColumns, "s", "version"),
6725
+ sqliteSelectMaybe(sessionColumns, "s", "share_url"),
6726
+ sqliteSelectMaybe(sessionColumns, "s", "time_created"),
6727
+ sqliteSelectMaybe(sessionColumns, "s", "time_updated"),
6728
+ sqliteSelectMaybe(sessionColumns, "s", "time_archived"),
6729
+ sqliteSelectMaybe(sessionColumns, "s", "workspace_id"),
6730
+ sqliteSelectMaybe(sessionColumns, "s", "path"),
6731
+ sqliteSelectMaybe(sessionColumns, "s", "agent"),
6732
+ sqliteSelectMaybe(sessionColumns, "s", "model"),
6733
+ canJoinProject && projectColumns.has("worktree") ? "p.worktree as project_worktree" : "null as project_worktree",
6734
+ canJoinProject && projectColumns.has("name") ? "p.name as project_name" : "null as project_name"
6735
+ ];
6736
+ const queryParts = [`select ${selects.join(", ")}`, "from session s"];
6737
+ if (canJoinProject) queryParts.push("left join project p on p.id = s.project_id");
6738
+ if (sessionColumns.has("time_archived")) queryParts.push("where coalesce(s.time_archived, 0) = 0");
6739
+ const orderColumns = [];
6740
+ if (sessionColumns.has("time_updated")) orderColumns.push("s.time_updated desc");
6741
+ if (sessionColumns.has("time_created")) orderColumns.push("s.time_created desc");
6742
+ orderColumns.push("s.id");
6743
+ queryParts.push(`order by ${orderColumns.join(", ")}`);
6744
+ return readSqliteJson(dbPath, queryParts.join(" "), "OpenCode SQLite sessions");
6745
+ }
6746
+
6747
+ function readOpenCodeSqliteMessageRows(dbPath) {
6748
+ const columns = sqliteTableColumns(dbPath, "message");
6749
+ if (!columns.has("id") || !columns.has("session_id")) return [];
6750
+ const selects = [
6751
+ "id",
6752
+ "session_id",
6753
+ sqliteSelectMaybe(columns, "message", "time_created"),
6754
+ sqliteSelectMaybe(columns, "message", "time_updated"),
6755
+ sqliteSelectMaybe(columns, "message", "data")
6756
+ ];
6757
+ const orderColumns = ["session_id"];
6758
+ if (columns.has("time_created")) orderColumns.push("time_created");
6759
+ orderColumns.push("id");
6760
+ return readSqliteJson(dbPath, `select ${selects.join(", ")} from message order by ${orderColumns.join(", ")}`, "OpenCode SQLite messages");
6761
+ }
6762
+
6763
+ function readOpenCodeSqlitePartRows(dbPath) {
6764
+ const columns = sqliteTableColumns(dbPath, "part");
6765
+ if (!columns.has("id") || !columns.has("message_id") || !columns.has("session_id")) return [];
6766
+ const selects = [
6767
+ "id",
6768
+ "message_id",
6769
+ "session_id",
6770
+ sqliteSelectMaybe(columns, "part", "time_created"),
6771
+ sqliteSelectMaybe(columns, "part", "time_updated"),
6772
+ sqliteSelectMaybe(columns, "part", "data")
6773
+ ];
6774
+ const orderColumns = ["session_id", "message_id"];
6775
+ if (columns.has("time_created")) orderColumns.push("time_created");
6776
+ orderColumns.push("id");
6777
+ return readSqliteJson(dbPath, `select ${selects.join(", ")} from part order by ${orderColumns.join(", ")}`, "OpenCode SQLite parts");
6778
+ }
6779
+
6780
+ function openCodeSqliteMessagesFromRow(row, partRows, index) {
6781
+ const data = parseJsonObject(row.data);
6782
+ const parts = (partRows || []).map((partRow) => ({
6783
+ id: partRow.id,
6784
+ messageID: partRow.message_id,
6785
+ messageId: partRow.message_id,
6786
+ sessionID: partRow.session_id,
6787
+ sessionId: partRow.session_id,
6788
+ timeCreated: partRow.time_created,
6789
+ timeUpdated: partRow.time_updated,
6790
+ ...parseJsonObject(partRow.data)
6791
+ }));
6792
+ const role = normalizeEventRole(data.role || data.type || data.actor) || openCodeRoleFromParts(parts);
6793
+ 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);
6794
+ const text = firstString(data.content, data.text, data.message, openCodePartText(parts));
6795
+ const toolCalls = parts.map(openCodeToolCall).filter(Boolean);
6796
+ const toolResults = parts.map(openCodeToolResult).filter(Boolean);
6797
+ const messageId = firstString(row.id, data.id, data.messageID, data.messageId);
6798
+ const usage = openCodeUsageFromMessageData(data, parts);
6799
+ const result = [];
6800
+ if (role && (text || toolCalls.length)) {
6801
+ result.push({
6802
+ role,
6803
+ content: text,
6804
+ timestamp,
6805
+ metadata: {
6806
+ provider: "opencode",
6807
+ messageId,
6808
+ parentMessageId: firstString(data.parentID, data.parentId) || undefined,
6809
+ requestId: usage ? messageId : undefined,
6810
+ model: firstString(data.modelID, data.modelId, data.model?.modelID, data.model?.modelId, data.model) || undefined,
6811
+ providerId: firstString(data.providerID, data.providerId, data.model?.providerID, data.model?.providerId) || undefined,
6812
+ mode: firstString(data.mode) || undefined,
6813
+ agent: firstString(data.agent) || undefined,
6814
+ finish: firstString(data.finish) || undefined,
6815
+ cwd: firstString(data.path?.cwd) || undefined,
6816
+ root: firstString(data.path?.root) || undefined,
6817
+ usage: usage || undefined,
6818
+ cost: Number.isFinite(Number(data.cost)) ? Number(data.cost) : undefined,
6819
+ toolCalls: toolCalls.length ? toolCalls : undefined
6820
+ }
6821
+ });
6822
+ }
6823
+ for (const toolResult of toolResults) {
6824
+ result.push({ role: "tool", content: toolResult.output, timestamp, metadata: { provider: "opencode", messageId, toolResult } });
6825
+ }
6826
+ return result;
6827
+ }
6828
+
6829
+ function openCodeUsageFromMessageData(data, parts = []) {
6830
+ const finishPart = (parts || []).find((part) => String(part?.type || "").toLowerCase() === "step-finish" && part.tokens && typeof part.tokens === "object");
6831
+ const tokens = data?.tokens && typeof data.tokens === "object" ? data.tokens : finishPart?.tokens;
6832
+ if (!tokens || typeof tokens !== "object") return null;
6833
+ const usage = {
6834
+ input_tokens: Number.isFinite(Number(tokens.input)) ? Number(tokens.input) : undefined,
6835
+ output_tokens: Number.isFinite(Number(tokens.output)) ? Number(tokens.output) : undefined,
6836
+ total_tokens: Number.isFinite(Number(tokens.total)) ? Number(tokens.total) : undefined,
6837
+ reasoning_tokens: Number.isFinite(Number(tokens.reasoning)) ? Number(tokens.reasoning) : undefined,
6838
+ cache_creation_input_tokens: Number.isFinite(Number(tokens.cache?.write)) ? Number(tokens.cache.write) : undefined,
6839
+ cache_read_input_tokens: Number.isFinite(Number(tokens.cache?.read)) ? Number(tokens.cache.read) : undefined
6840
+ };
6841
+ return Object.values(usage).some((value) => value !== undefined) ? usage : null;
6842
+ }
6843
+
6844
+ function openCodeSqliteSessionFingerprint(dbPath, row, messageRows, sourceFiles) {
6845
+ const sessionRevision = [
6846
+ row.id,
6847
+ row.time_updated || row.time_created || "",
6848
+ messageRows.length,
6849
+ messageRows.map((message) => `${message.id}:${message.time_updated || message.time_created || ""}`).join("|")
6850
+ ].join(":");
6851
+ return `${fingerprintPrefix("opencode-sqlite-history")}:${structuredSessionFingerprint({ sourcePath: dbPath, sourceFiles })}:${hashId(sessionRevision)}`;
6852
+ }
6853
+
5679
6854
  function parseOpenCodeSessionFile(file, storageRoot) {
5680
6855
  const info = readJsonMaybe(file, null);
5681
6856
  if (!info || typeof info !== "object") return null;
@@ -5717,6 +6892,63 @@ function parseOpenCodeSessionFile(file, storageRoot) {
5717
6892
  };
5718
6893
  }
5719
6894
 
6895
+ function parseOpenCodeMessageOnlySession(storageRoot, sessionId) {
6896
+ if (!sessionId) return null;
6897
+ const messageFiles = openCodeMessageFiles(storageRoot, sessionId);
6898
+ const parsedMessages = messageFiles.flatMap((messageFile, index) => openCodeMessagesFromFile(messageFile, storageRoot, index));
6899
+ const diffFile = path.join(storageRoot, "session_diff", `${sessionId}.json`);
6900
+ const diffMessage = openCodeDiffMessage(diffFile, parsedMessages[parsedMessages.length - 1]?.timestamp);
6901
+ const messages = stampMessages(
6902
+ dedupeAdjacentMessages(parsedMessages.concat(diffMessage ? [diffMessage] : [])).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
6903
+ "opencode-history"
6904
+ );
6905
+ if (!messages.length) return null;
6906
+ const sourceFiles = [
6907
+ ...messageFiles,
6908
+ ...messageFiles.flatMap((messageFile) => openCodePartFiles(storageRoot, firstString(readJsonMaybe(messageFile, {})?.id, path.basename(messageFile, ".json")))),
6909
+ safeStat(diffFile) ? diffFile : ""
6910
+ ].filter(Boolean);
6911
+ const startedAt = messages[0]?.timestamp || "";
6912
+ const endedAt = messages[messages.length - 1]?.timestamp || startedAt;
6913
+ const cwd = openCodeCwdFromMessages(messages) || openCodeProjectPathFromStorage(storageRoot);
6914
+ return {
6915
+ sessionId: `opencode-${sessionId}`,
6916
+ title: clineTitle(messages) || sessionId,
6917
+ cwd,
6918
+ startedAt,
6919
+ endedAt,
6920
+ messages,
6921
+ sourcePath: path.join(storageRoot, "message", sessionId),
6922
+ sourceFiles,
6923
+ sourceType: "opencode-history",
6924
+ fingerprint: `${fingerprintPrefix("opencode-history")}:${structuredSessionFingerprint({ sourcePath: path.join(storageRoot, "message", sessionId), sourceFiles })}`,
6925
+ detailKey: "sessions"
6926
+ };
6927
+ }
6928
+
6929
+ function openCodeCwdFromMessages(messages) {
6930
+ for (const message of messages || []) {
6931
+ const cwd = firstString(message?.metadata?.cwd, message?.metadata?.directory, message?.metadata?.projectPath);
6932
+ if (cwd) return cwd;
6933
+ const fromContent = openCodePathFromText(message?.content);
6934
+ if (fromContent) return fromContent;
6935
+ for (const toolCall of message?.metadata?.toolCalls || []) {
6936
+ const fromTool = openCodePathFromText(JSON.stringify(toolCall.arguments || toolCall.argument || ""));
6937
+ if (fromTool) return fromTool;
6938
+ }
6939
+ }
6940
+ return "";
6941
+ }
6942
+
6943
+ function openCodePathFromText(text) {
6944
+ const value = String(text || "");
6945
+ const cdMatch = value.match(/\bcd\s+["']?([^"'\s]+)["']?/);
6946
+ const candidate = cdMatch ? cdMatch[1] : value.match(/(?:working\s+)?directory[:=\s]+["']?([^"'\s]+)["']?/i)?.[1];
6947
+ if (!candidate) return "";
6948
+ const expanded = candidate.startsWith("~/") ? path.join(os.homedir(), candidate.slice(2)) : candidate;
6949
+ return path.isAbsolute(expanded) && fs.existsSync(expanded) ? expanded : "";
6950
+ }
6951
+
5720
6952
  function openCodeProjectInfo(storageRoot, projectId) {
5721
6953
  const file = projectId ? path.join(storageRoot, "project", `${projectId}.json`) : "";
5722
6954
  const data = file ? readJsonMaybe(file, {}) : {};
@@ -5887,10 +7119,14 @@ function openCodeDiffText(value) {
5887
7119
  if (typeof value === "string") return value.trim();
5888
7120
  if (Array.isArray(value)) return value.map(openCodeDiffText).filter(Boolean).join("\n");
5889
7121
  if (typeof value !== "object") return String(value);
7122
+ const file = firstString(value.path, value.file, value.filename);
5890
7123
  for (const key of ["diff", "patch", "text", "content"]) {
5891
- if (typeof value[key] === "string" && value[key].trim()) return value[key].trim();
7124
+ if (typeof value[key] === "string" && value[key].trim()) {
7125
+ const body = value[key].trim();
7126
+ if (file && !/^diff --git\s/m.test(body)) return `diff --git a/${file} b/${file}\n${body}`;
7127
+ return body;
7128
+ }
5892
7129
  }
5893
- const file = firstString(value.path, value.file, value.filename);
5894
7130
  const body = openCodeDiffText(value.hunks || value.changes || value.edits);
5895
7131
  if (file && body) return `diff --git a/${file} b/${file}\n${body}`;
5896
7132
  return "";
@@ -6024,6 +7260,7 @@ function aiderMarkdownMessages(text, fallbackTime) {
6024
7260
  }
6025
7261
 
6026
7262
  function dedupeStructuredSessions(sessions, provider) {
7263
+ if (provider === "opencode") return dedupeOpenCodeSessions(sessions);
6027
7264
  const seen = new Set();
6028
7265
  const result = [];
6029
7266
  for (const session of sessions) {
@@ -6035,6 +7272,30 @@ function dedupeStructuredSessions(sessions, provider) {
6035
7272
  return result;
6036
7273
  }
6037
7274
 
7275
+ function dedupeOpenCodeSessions(sessions) {
7276
+ const bySessionId = new Map();
7277
+ const order = [];
7278
+ for (const session of sessions || []) {
7279
+ const key = `opencode:${session.sessionId}`;
7280
+ const existing = bySessionId.get(key);
7281
+ if (!existing) {
7282
+ bySessionId.set(key, session);
7283
+ order.push(key);
7284
+ continue;
7285
+ }
7286
+ const preferred = openCodeSourceRank(session.sourceType) > openCodeSourceRank(existing.sourceType) ? session : existing;
7287
+ preferred.sourceFiles = existingUniquePaths([...(existing.sourceFiles || []), ...(session.sourceFiles || [])]);
7288
+ bySessionId.set(key, preferred);
7289
+ }
7290
+ return order.map((key) => bySessionId.get(key)).filter(Boolean);
7291
+ }
7292
+
7293
+ function openCodeSourceRank(sourceType) {
7294
+ if (sourceType === "opencode-sqlite-history") return 3;
7295
+ if (sourceType === "opencode-history") return 2;
7296
+ return 1;
7297
+ }
7298
+
6038
7299
  function readDevinSessions(env = process.env, options = {}) {
6039
7300
  const db = devinSessionsDb(env);
6040
7301
  reportDiscoveryProgress(options, { current: 0, total: fs.existsSync(db) ? 1 : 0, message: "opening Devin sessions database", path: db });
@@ -6150,6 +7411,9 @@ function devinMessagesFromNode(row) {
6150
7411
  if (role === "system" || devinIsContextUserMessage(role, content)) return [];
6151
7412
  const toolResult = role === "tool" ? devinNormalizeToolResult(content, data) : null;
6152
7413
  const body = role === "tool" ? content : devinVisibleContent(content, toolCalls);
7414
+ const usage = role === "assistant" ? devinUsage(data) : null;
7415
+ const model = role === "assistant" ? firstString(data.metadata?.generation_model, data.model, data.model_id, data.modelId) : "";
7416
+ const requestId = role === "assistant" ? firstString(data.metadata?.request_id, data.request_id, data.message_id) : "";
6153
7417
  if (!body && !toolCalls.length && !toolResult) return [];
6154
7418
  return [
6155
7419
  {
@@ -6162,6 +7426,9 @@ function devinMessagesFromNode(row) {
6162
7426
  parentNodeId: row.parent_node_id ?? undefined,
6163
7427
  toolCalls: toolCalls.length ? toolCalls.map(devinPublicToolCall) : undefined,
6164
7428
  toolResult: toolResult || undefined,
7429
+ usage: usage || undefined,
7430
+ model: model || undefined,
7431
+ requestId: requestId || undefined,
6165
7432
  sourceType: "devin-cli-history",
6166
7433
  parserVersion: parserVersionForSource("devin-cli-history")
6167
7434
  }
@@ -6169,6 +7436,25 @@ function devinMessagesFromNode(row) {
6169
7436
  ];
6170
7437
  }
6171
7438
 
7439
+ function devinUsage(message = {}) {
7440
+ const metrics = message?.metadata?.metrics;
7441
+ if (!metrics || typeof metrics !== "object") return null;
7442
+ const inputTokens = numericValue(metrics.input_tokens, metrics.inputTokens, metrics.prompt_tokens, metrics.promptTokens);
7443
+ const outputTokens = numericValue(metrics.output_tokens, metrics.outputTokens, metrics.completion_tokens, metrics.completionTokens, message?.metadata?.num_tokens);
7444
+ const cacheCreationInputTokens = numericValue(metrics.cache_creation_tokens, metrics.cache_creation_input_tokens, metrics.cacheCreationInputTokens);
7445
+ const cacheReadInputTokens = numericValue(metrics.cache_read_tokens, metrics.cache_read_input_tokens, metrics.cacheReadInputTokens);
7446
+ if ([inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens].every((value) => value == null)) return null;
7447
+ const usage = {
7448
+ inputTokens: inputTokens ?? undefined,
7449
+ outputTokens: outputTokens ?? undefined,
7450
+ cacheCreationInputTokens: cacheCreationInputTokens ?? undefined,
7451
+ cacheReadInputTokens: cacheReadInputTokens ?? undefined
7452
+ };
7453
+ const totalTokens = [inputTokens, outputTokens].reduce((sum, value) => sum + (value || 0), 0);
7454
+ if (totalTokens) usage.totalTokens = totalTokens;
7455
+ return usage;
7456
+ }
7457
+
6172
7458
  function devinVisibleContent(content, toolCalls) {
6173
7459
  const value = String(content || "").trim();
6174
7460
  if (toolCalls.length && /^none$/i.test(value)) return "";
@@ -6292,8 +7578,9 @@ function readGeminiCliSessions(options = {}, env = process.env) {
6292
7578
  reportDiscoveryProgress(options, { current: 0, total: files.length, message: "reading saved chats and checkpoints" });
6293
7579
  for (let index = 0; index < files.length; index++) {
6294
7580
  const file = files[index];
6295
- const parsed = parseGeminiCliHistoryFile(file);
6296
- if (parsed) sessions.push(parsed);
7581
+ for (const parsed of asArray(parseGeminiCliHistoryFile(file))) {
7582
+ if (parsed) sessions.push(parsed);
7583
+ }
6297
7584
  reportDiscoveryProgress(options, { current: index + 1, total: files.length, message: `${sessions.length} importable`, path: file });
6298
7585
  }
6299
7586
  return coalesceGeminiCliSessions(sessions);
@@ -6476,17 +7763,26 @@ function parseGeminiCliHistoryFile(file) {
6476
7763
  const stat = safeStat(file);
6477
7764
  const fallbackTime = new Date(stat?.mtimeMs || Date.now()).toISOString();
6478
7765
  const ext = path.extname(file).toLowerCase();
6479
- let parsed = null;
7766
+ let parsedItems = [];
6480
7767
  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 });
7768
+ if (ext === ".jsonl") parsedItems = parseGeminiCliJsonlSessions(fs.readFileSync(file, "utf8"), { fallbackTime });
7769
+ else if (ext === ".md" || ext === ".markdown") parsedItems = [parseMarkdownChatFile(file, "gemini-cli", fallbackTime)].filter(Boolean);
7770
+ else parsedItems = parseGeminiCliJsonSessions(JSON.parse(fs.readFileSync(file, "utf8")), { fallbackTime });
6484
7771
  } catch {
6485
7772
  return null;
6486
7773
  }
7774
+ const multiSessionSource = parsedItems.length > 1;
7775
+ return parsedItems
7776
+ .map((parsed, index) => geminiCliParsedSession(file, stat, fallbackTime, ext, parsed, { multiSessionSource, index }))
7777
+ .filter(Boolean);
7778
+ }
7779
+
7780
+ function geminiCliParsedSession(file, stat, fallbackTime, ext, parsed, options = {}) {
6487
7781
  if (!parsed || !parsed.messages.length) return null;
6488
7782
  const cwd = parsed.cwd || geminiProjectCwd(file);
6489
7783
  const sessionId = parsed.sessionId || stableSessionId("gemini_cli", file, parsed.startedAt || fallbackTime, parsed.messages);
7784
+ const sourceFingerprint = fileFingerprint(file, stat);
7785
+ const fingerprint = options.multiSessionSource ? `${sourceFingerprint}:session:${hashId(sessionId || options.index)}` : sourceFingerprint;
6490
7786
  return {
6491
7787
  sessionId,
6492
7788
  title: parsed.title || path.basename(file, ext),
@@ -6497,8 +7793,9 @@ function parseGeminiCliHistoryFile(file) {
6497
7793
  sourcePath: file,
6498
7794
  sourceFiles: [file],
6499
7795
  sourceType: "gemini-cli-history",
6500
- fingerprint: `${fingerprintPrefix("gemini-cli-history")}:${fileFingerprint(file, stat)}`,
6501
- detailKey: "files"
7796
+ fingerprint: `${fingerprintPrefix("gemini-cli-history")}:${fingerprint}`,
7797
+ detailKey: "files",
7798
+ sessionSummary: parsed.sessionSummary || undefined
6502
7799
  };
6503
7800
  }
6504
7801
 
@@ -6526,6 +7823,121 @@ function parseMarkdownChatFile(file, source, fallbackTime) {
6526
7823
  };
6527
7824
  }
6528
7825
 
7826
+ function readWindsurfTrajectoryExport(target, options = {}) {
7827
+ const files = windsurfTrajectoryFiles(target);
7828
+ const sessions = [];
7829
+ reportDiscoveryProgress(options, { current: 0, total: files.length, message: "reading Windsurf trajectory exports" });
7830
+ for (let index = 0; index < files.length; index++) {
7831
+ const file = files[index];
7832
+ const session = parseWindsurfTrajectoryFile(file);
7833
+ if (session) sessions.push(session);
7834
+ reportDiscoveryProgress(options, { current: index + 1, total: files.length, message: `${sessions.length} importable`, path: file });
7835
+ }
7836
+ return sessions;
7837
+ }
7838
+
7839
+ function windsurfTrajectoryFiles(target) {
7840
+ const resolved = path.resolve(target);
7841
+ const stat = safeStat(resolved);
7842
+ if (!stat) throw new Error(`Cannot read Windsurf trajectory export path ${target}`);
7843
+ if (stat.isFile()) return isMarkdownFile(resolved) ? [resolved] : [];
7844
+ if (!stat.isDirectory()) return [];
7845
+ const files = [];
7846
+ collectFiles(resolved, (file) => {
7847
+ if (isMarkdownFile(file)) files.push(file);
7848
+ });
7849
+ return files.sort((a, b) => a.localeCompare(b));
7850
+ }
7851
+
7852
+ function isMarkdownFile(file) {
7853
+ return [".md", ".markdown"].includes(path.extname(file).toLowerCase());
7854
+ }
7855
+
7856
+ function parseWindsurfTrajectoryFile(file) {
7857
+ const stat = safeStat(file);
7858
+ const fallbackTime = new Date(stat?.mtimeMs || Date.now()).toISOString();
7859
+ let text = "";
7860
+ try {
7861
+ text = fs.readFileSync(file, "utf8");
7862
+ } catch {
7863
+ return null;
7864
+ }
7865
+ if (!looksLikeWindsurfTrajectory(text)) return null;
7866
+ const messages = windsurfTrajectoryMessages(text, fallbackTime);
7867
+ if (!messages.length) return null;
7868
+ const stampedMessages = stampMessages(messages, "windsurf-trajectory-export");
7869
+ const startedAt = stampedMessages[0]?.timestamp || fallbackTime;
7870
+ const endedAt = stampedMessages[stampedMessages.length - 1]?.timestamp || fallbackTime;
7871
+ const cwd = inferCwdFromMarkdownText(text);
7872
+ return {
7873
+ sessionId: stableSessionId("windsurf", file, startedAt, stampedMessages),
7874
+ title: windsurfTrajectoryTitle(text, stampedMessages, file),
7875
+ cwd,
7876
+ scopeCanonical: cwd ? "" : uncategorizedScope("windsurf"),
7877
+ startedAt,
7878
+ endedAt,
7879
+ messages: stampedMessages,
7880
+ sourcePath: file,
7881
+ sourceFiles: [file],
7882
+ sourceType: "windsurf-trajectory-export",
7883
+ fingerprint: `${fingerprintPrefix("windsurf-trajectory-export")}:${fileFingerprint(file, stat)}:${hashId(stampedMessages.map((message) => `${message.role}:${message.content}`).join("\n"))}`,
7884
+ detailKey: "exports"
7885
+ };
7886
+ }
7887
+
7888
+ function looksLikeWindsurfTrajectory(text) {
7889
+ return /^#\s+Cascade Chat Conversation\s*$/im.test(String(text || "")) && /^###\s+(User Input|Planner Response|Assistant Response|Cascade Response)\s*$/im.test(String(text || ""));
7890
+ }
7891
+
7892
+ function windsurfTrajectoryMessages(text, fallbackTime) {
7893
+ const messages = [];
7894
+ let role = "";
7895
+ let heading = "";
7896
+ let current = [];
7897
+ const flush = () => {
7898
+ const content = current.join("\n").trim();
7899
+ if (role && content) {
7900
+ messages.push({
7901
+ role,
7902
+ content,
7903
+ timestamp: new Date(new Date(fallbackTime).getTime() + messages.length).toISOString(),
7904
+ metadata: { source: "windsurf-trajectory-export", heading }
7905
+ });
7906
+ }
7907
+ current = [];
7908
+ };
7909
+ for (const line of String(text || "").split(/\r?\n/)) {
7910
+ const match = line.match(/^###\s+(.+?)\s*$/);
7911
+ const nextRole = match ? windsurfTrajectoryRole(match[1]) : "";
7912
+ if (match && nextRole) {
7913
+ flush();
7914
+ role = nextRole;
7915
+ heading = match[1].trim();
7916
+ } else if (role) {
7917
+ current.push(line);
7918
+ }
7919
+ }
7920
+ flush();
7921
+ return messages;
7922
+ }
7923
+
7924
+ function windsurfTrajectoryRole(label) {
7925
+ const value = String(label || "").trim().toLowerCase();
7926
+ if (/(user|human).*(input|prompt|message)?/.test(value)) return "user";
7927
+ if (/(planner|assistant|cascade|agent|model).*(response|output|message)?/.test(value)) return "assistant";
7928
+ if (/system/.test(value)) return "system";
7929
+ if (/tool/.test(value)) return "tool";
7930
+ return "";
7931
+ }
7932
+
7933
+ function windsurfTrajectoryTitle(text, messages, file) {
7934
+ const title = markdownTitle(text);
7935
+ if (title && !/^Cascade Chat Conversation$/i.test(title)) return title;
7936
+ const firstUser = messages.find((message) => message.role === "user");
7937
+ const line = firstLine(firstUser?.content);
7938
+ return line ? line.slice(0, 120) : path.basename(file, path.extname(file));
7939
+ }
7940
+
6529
7941
  function readWindsurfSessions(options = {}) {
6530
7942
  // Kept for future decoder work only. Windsurf's current Cascade transcript
6531
7943
  // stores are encrypted binary files, so this source is disabled from public
@@ -6560,8 +7972,9 @@ function readWindsurfSessions(options = {}) {
6560
7972
  }
6561
7973
 
6562
7974
  function readAntigravitySessions(options = {}, env = process.env) {
6563
- const roots = [path.join(os.homedir(), ".gemini", "antigravity", "brain")];
6564
- const sessions = readMarkdownArtifactSessions({
7975
+ const home = antigravityHome(env);
7976
+ const roots = [path.join(home, "brain")];
7977
+ const artifactSessions = readMarkdownArtifactSessions({
6565
7978
  provider: "antigravity",
6566
7979
  roots,
6567
7980
  sourceType: "antigravity-brain",
@@ -6569,7 +7982,11 @@ function readAntigravitySessions(options = {}, env = process.env) {
6569
7982
  artifactNames: ["task.md", "implementation_plan.md", "walkthrough.md", "plan.md"],
6570
7983
  options
6571
7984
  });
6572
- const binaryCount = countFiles(path.join(os.homedir(), ".gemini", "antigravity", "conversations"), (file) => file.endsWith(".pb"));
7985
+ const artifactSessionIds = new Set(artifactSessions.map((session) => session.sessionId));
7986
+ const trajectorySessions = readAntigravityTrajectorySummarySessions(options, env)
7987
+ .filter((session) => !artifactSessionIds.has(session.sessionId));
7988
+ const sessions = [...artifactSessions, ...trajectorySessions];
7989
+ const binaryCount = countFiles(path.join(home, "conversations"), (file) => file.endsWith(".pb"));
6573
7990
  if (binaryCount && sessions.length) sessions[0].binaryCount = binaryCount;
6574
7991
  else if (binaryCount) {
6575
7992
  sessions.push({
@@ -6580,7 +7997,7 @@ function readAntigravitySessions(options = {}, env = process.env) {
6580
7997
  startedAt: new Date().toISOString(),
6581
7998
  endedAt: new Date().toISOString(),
6582
7999
  messages: [],
6583
- sourcePath: path.join(os.homedir(), ".gemini", "antigravity", "conversations"),
8000
+ sourcePath: path.join(home, "conversations"),
6584
8001
  sourceType: "antigravity-protobuf",
6585
8002
  binaryCount,
6586
8003
  detailKey: "tasks"
@@ -6589,6 +8006,205 @@ function readAntigravitySessions(options = {}, env = process.env) {
6589
8006
  return sessions;
6590
8007
  }
6591
8008
 
8009
+ function antigravityHome(env = process.env) {
8010
+ return env.AGENTLOG_ANTIGRAVITY_HOME_DIR || path.join(geminiHome(env), "antigravity");
8011
+ }
8012
+
8013
+ function antigravityGlobalStateDbs(env = process.env) {
8014
+ const explicit = envPathList(env.AGENTLOG_ANTIGRAVITY_GLOBAL_STORAGE_DB || env.AGENTLOG_ANTIGRAVITY_GLOBAL_STATE_DB);
8015
+ if (explicit.length) return existingUniquePaths(explicit);
8016
+ const appRoots = envPathList(env.AGENTLOG_ANTIGRAVITY_APP_SUPPORT_DIR);
8017
+ if (!appRoots.length) {
8018
+ appRoots.push(
8019
+ path.join(os.homedir(), "Library", "Application Support", "Antigravity"),
8020
+ path.join(os.homedir(), ".config", "Antigravity"),
8021
+ path.join(os.homedir(), "AppData", "Roaming", "Antigravity")
8022
+ );
8023
+ }
8024
+ return existingUniquePaths([
8025
+ ...explicit,
8026
+ ...appRoots.flatMap((root) => [
8027
+ path.join(root, "User", "globalStorage", "state.vscdb"),
8028
+ path.join(root, "User", "globalStorage", "state.vscdb.backup")
8029
+ ])
8030
+ ]);
8031
+ }
8032
+
8033
+ function readAntigravityTrajectorySummarySessions(options = {}, env = process.env) {
8034
+ const dbs = antigravityGlobalStateDbs(env);
8035
+ if (!dbs.length) return [];
8036
+ const sessions = [];
8037
+ const seen = new Set();
8038
+ reportDiscoveryProgress(options, { current: 0, total: dbs.length, message: "reading trajectory summaries" });
8039
+ for (let index = 0; index < dbs.length; index++) {
8040
+ const db = dbs[index];
8041
+ try {
8042
+ for (const session of antigravityTrajectorySummarySessionsFromDb(db, env)) {
8043
+ if (seen.has(session.sessionId)) continue;
8044
+ seen.add(session.sessionId);
8045
+ sessions.push(session);
8046
+ }
8047
+ } catch (error) {
8048
+ reportDiscoveryProgress(options, { current: index + 1, total: dbs.length, message: error.message, path: db });
8049
+ continue;
8050
+ }
8051
+ reportDiscoveryProgress(options, { current: index + 1, total: dbs.length, message: `${sessions.length} summaries`, path: db });
8052
+ }
8053
+ if (sessions.length) sessions[0].stateDbCount = dbs.length;
8054
+ return sessions.sort((a, b) => String(a.startedAt).localeCompare(String(b.startedAt)) || a.sessionId.localeCompare(b.sessionId));
8055
+ }
8056
+
8057
+ function antigravityTrajectorySummarySessionsFromDb(db, env = process.env) {
8058
+ if (!fs.existsSync(db)) return [];
8059
+ const rows = readSqliteJson(
8060
+ db,
8061
+ "SELECT value FROM ItemTable WHERE key='antigravityUnifiedStateSync.trajectorySummaries'",
8062
+ "Antigravity global state"
8063
+ );
8064
+ const encoded = firstString(...rows.map((row) => row.value));
8065
+ if (!encoded) return [];
8066
+ return parseAntigravityTrajectorySummaries(encoded, { db, env });
8067
+ }
8068
+
8069
+ function parseAntigravityTrajectorySummaries(encoded, context = {}) {
8070
+ const outer = decodeProtoMessage(bufferFromBase64(encoded));
8071
+ const sessions = [];
8072
+ for (const entry of outer.filter((field) => field.number === 1 && field.wireType === 2)) {
8073
+ const session = antigravityTrajectorySummarySession(entry.bytes, context);
8074
+ if (session) sessions.push(session);
8075
+ }
8076
+ return sessions;
8077
+ }
8078
+
8079
+ function antigravityTrajectorySummarySession(entryBytes, context = {}) {
8080
+ const entry = decodeProtoMessage(entryBytes);
8081
+ const id = protoString(firstProtoField(entry, 1));
8082
+ const wrapper = firstProtoField(entry, 2);
8083
+ const summaryEnvelope = wrapper?.bytes ? decodeProtoMessage(wrapper.bytes) : [];
8084
+ const summaryEncoded = protoString(firstProtoField(summaryEnvelope, 1));
8085
+ if (!id || !summaryEncoded) return null;
8086
+
8087
+ const sourceType = "antigravity-trajectory-summary";
8088
+ const summaryBytes = bufferFromBase64(summaryEncoded);
8089
+ const fields = decodeProtoMessage(summaryBytes);
8090
+ const prompt = cleanAntigravityText(protoString(firstProtoField(fields, 1)));
8091
+ if (!prompt) return null;
8092
+ const startedAt = antigravitySummaryTimestamp(fields, 7) || antigravitySummaryTimestamp(fields, 3) || antigravitySummaryTimestamp(fields, 10) || new Date().toISOString();
8093
+ const endedAt = antigravitySummaryTimestamp(fields, 10) || antigravitySummaryTimestamp(fields, 3) || startedAt;
8094
+ const cwd = antigravitySummaryCwd(fields);
8095
+ const assistantSummary = antigravityAssistantSummary(fields, prompt);
8096
+ const messages = [
8097
+ {
8098
+ role: "user",
8099
+ content: prompt,
8100
+ timestamp: startedAt,
8101
+ metadata: {
8102
+ provider: "antigravity",
8103
+ source: sourceType,
8104
+ providerConversationId: id,
8105
+ partialSummary: true
8106
+ }
8107
+ }
8108
+ ];
8109
+ if (assistantSummary) {
8110
+ messages.push({
8111
+ role: "assistant",
8112
+ content: `# Antigravity Trajectory Summary\n\n${assistantSummary}`,
8113
+ timestamp: endedAt,
8114
+ metadata: {
8115
+ provider: "antigravity",
8116
+ source: sourceType,
8117
+ providerConversationId: id,
8118
+ partialSummary: true
8119
+ }
8120
+ });
8121
+ }
8122
+ const db = context.db || "";
8123
+ return {
8124
+ sessionId: `antigravity-${id}`,
8125
+ providerConversationId: id,
8126
+ title: antigravitySummaryTitle(prompt),
8127
+ cwd,
8128
+ scopeCanonical: cwd ? "" : "antigravity/uncategorized",
8129
+ startedAt,
8130
+ endedAt,
8131
+ messages: stampMessages(messages, sourceType),
8132
+ sourcePath: `antigravity-state:${db || "globalStorage/state.vscdb"}#trajectorySummaries/${id}`,
8133
+ sourceFiles: [],
8134
+ sourceType,
8135
+ fingerprint: `${fingerprintPrefix(sourceType)}:${db}:${id}:${hashId(summaryEncoded)}`,
8136
+ detailKey: "trajectorySummaries",
8137
+ partialSummary: true,
8138
+ rawReferences: db
8139
+ ? [
8140
+ {
8141
+ originalPath: db,
8142
+ entryPath: `ItemTable/antigravityUnifiedStateSync.trajectorySummaries/${id}`,
8143
+ conversationId: id,
8144
+ note: "Antigravity trajectory summary state row. The global state DB is referenced but not copied because it can contain auth tokens."
8145
+ }
8146
+ ]
8147
+ : undefined,
8148
+ sessionSummary: {
8149
+ source: sourceType,
8150
+ partial: true,
8151
+ note: "Imported from Antigravity trajectory summary metadata. Full binary conversation bodies are not decoded."
8152
+ }
8153
+ };
8154
+ }
8155
+
8156
+ function antigravitySummaryTimestamp(fields, number) {
8157
+ const field = firstProtoField(fields, number);
8158
+ if (!field?.bytes) return "";
8159
+ const timestamp = decodeProtoMessage(field.bytes);
8160
+ const seconds = protoVarintValue(firstProtoField(timestamp, 1));
8161
+ const nanos = protoVarintValue(firstProtoField(timestamp, 2)) || 0;
8162
+ if (!Number.isFinite(seconds) || seconds < 946684800 || seconds > 4102444800) return "";
8163
+ return new Date((seconds * 1000) + Math.floor(nanos / 1e6)).toISOString();
8164
+ }
8165
+
8166
+ function antigravitySummaryCwd(fields) {
8167
+ const candidates = collectProtoStrings(fields)
8168
+ .filter((value) => value.startsWith("file://"))
8169
+ .map((value) => {
8170
+ try {
8171
+ return fileURLToPath(value);
8172
+ } catch {
8173
+ return "";
8174
+ }
8175
+ })
8176
+ .filter(Boolean)
8177
+ .map((candidate) => {
8178
+ const stat = safeStat(candidate);
8179
+ return stat?.isFile() ? path.dirname(candidate) : candidate;
8180
+ })
8181
+ .filter((candidate) => candidate && !candidate.includes(`${path.sep}.gemini${path.sep}antigravity${path.sep}`))
8182
+ .filter((candidate) => !cursorIsSystemRootPath(candidate));
8183
+ return candidates[0] || "";
8184
+ }
8185
+
8186
+ function antigravityAssistantSummary(fields, prompt) {
8187
+ const promptValue = cleanAntigravityText(prompt);
8188
+ return collectProtoStrings(fields)
8189
+ .map(cleanAntigravityText)
8190
+ .filter((value) => value && value !== promptValue)
8191
+ .filter((value) => value.length >= 80)
8192
+ .filter((value) => !value.startsWith("file://"))
8193
+ .filter((value) => !isLikelyBase64(value))
8194
+ .filter((value) => !/^[0-9a-f-]{32,}$/i.test(value))
8195
+ .sort((a, b) => b.length - a.length)[0] || "";
8196
+ }
8197
+
8198
+ function antigravitySummaryTitle(prompt) {
8199
+ const value = cleanAntigravityText(prompt);
8200
+ if (!value) return "Antigravity trajectory summary";
8201
+ return value.length > 80 ? `${value.slice(0, 77).trimEnd()}...` : value;
8202
+ }
8203
+
8204
+ function cleanAntigravityText(value) {
8205
+ return String(value || "").replace(/\s+/g, " ").trim();
8206
+ }
8207
+
6592
8208
  function readMarkdownArtifactSessions({ provider, roots, sourceType, detailKey, artifactNames, options = {} }) {
6593
8209
  const dirs = [];
6594
8210
  for (const root of roots) {
@@ -6738,16 +8354,22 @@ function inferCwdFromMarkdownFiles(files) {
6738
8354
  } catch {
6739
8355
  continue;
6740
8356
  }
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
- }
8357
+ const cwd = inferCwdFromMarkdownText(text);
8358
+ if (cwd) return cwd;
8359
+ }
8360
+ return "";
8361
+ }
8362
+
8363
+ function inferCwdFromMarkdownText(text) {
8364
+ const match = String(text || "").match(/file:\/\/([^)\]\s]+)/);
8365
+ if (!match) return "";
8366
+ try {
8367
+ const pathname = fileURLToPath(match[0]);
8368
+ if (fs.existsSync(pathname)) return fs.statSync(pathname).isDirectory() ? pathname : path.dirname(pathname);
8369
+ const existing = nearestExistingParent(pathname);
8370
+ if (existing) return existing;
8371
+ } catch {
8372
+ return "";
6751
8373
  }
6752
8374
  return "";
6753
8375
  }
@@ -6813,6 +8435,111 @@ function countFiles(root, predicate) {
6813
8435
  return count;
6814
8436
  }
6815
8437
 
8438
+ function bufferFromBase64(value) {
8439
+ try {
8440
+ return Buffer.from(String(value || ""), "base64");
8441
+ } catch {
8442
+ return Buffer.alloc(0);
8443
+ }
8444
+ }
8445
+
8446
+ function decodeProtoMessage(buffer) {
8447
+ const fields = [];
8448
+ const bytes = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer || []);
8449
+ let offset = 0;
8450
+ while (offset < bytes.length) {
8451
+ const key = readProtoVarint(bytes, offset);
8452
+ if (!key) break;
8453
+ offset = key.offset;
8454
+ const fieldNumber = Math.floor(key.value / 8);
8455
+ const wireType = key.value % 8;
8456
+ if (!fieldNumber) break;
8457
+ if (wireType === 0) {
8458
+ const value = readProtoVarint(bytes, offset);
8459
+ if (!value) break;
8460
+ offset = value.offset;
8461
+ fields.push({ number: fieldNumber, wireType, value: value.value });
8462
+ continue;
8463
+ }
8464
+ if (wireType === 1) {
8465
+ if (offset + 8 > bytes.length) break;
8466
+ fields.push({ number: fieldNumber, wireType, bytes: bytes.subarray(offset, offset + 8) });
8467
+ offset += 8;
8468
+ continue;
8469
+ }
8470
+ if (wireType === 2) {
8471
+ const length = readProtoVarint(bytes, offset);
8472
+ if (!length) break;
8473
+ offset = length.offset;
8474
+ if (length.value < 0 || offset + length.value > bytes.length) break;
8475
+ const value = bytes.subarray(offset, offset + length.value);
8476
+ offset += length.value;
8477
+ fields.push({ number: fieldNumber, wireType, bytes: value, text: printableUtf8(value) });
8478
+ continue;
8479
+ }
8480
+ if (wireType === 5) {
8481
+ if (offset + 4 > bytes.length) break;
8482
+ fields.push({ number: fieldNumber, wireType, bytes: bytes.subarray(offset, offset + 4) });
8483
+ offset += 4;
8484
+ continue;
8485
+ }
8486
+ break;
8487
+ }
8488
+ return fields;
8489
+ }
8490
+
8491
+ function readProtoVarint(buffer, offset) {
8492
+ let value = 0n;
8493
+ let shift = 0n;
8494
+ for (let index = offset; index < buffer.length; index++) {
8495
+ const byte = buffer[index];
8496
+ value |= BigInt(byte & 0x7f) << shift;
8497
+ if ((byte & 0x80) === 0) {
8498
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) return null;
8499
+ return { value: Number(value), offset: index + 1 };
8500
+ }
8501
+ shift += 7n;
8502
+ if (shift > 63n) return null;
8503
+ }
8504
+ return null;
8505
+ }
8506
+
8507
+ function printableUtf8(buffer) {
8508
+ if (!buffer?.length) return "";
8509
+ const text = buffer.toString("utf8");
8510
+ if (text.includes("\uFFFD")) return "";
8511
+ if (/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(text)) return "";
8512
+ return text;
8513
+ }
8514
+
8515
+ function firstProtoField(fields, number) {
8516
+ return (fields || []).find((field) => field.number === number) || null;
8517
+ }
8518
+
8519
+ function protoString(field) {
8520
+ return field?.wireType === 2 ? field.text || "" : "";
8521
+ }
8522
+
8523
+ function protoVarintValue(field) {
8524
+ return field?.wireType === 0 ? field.value : null;
8525
+ }
8526
+
8527
+ function collectProtoStrings(fields, depth = 0) {
8528
+ const strings = [];
8529
+ if (depth > 8) return strings;
8530
+ for (const field of fields || []) {
8531
+ if (field.wireType !== 2 || !field.bytes) continue;
8532
+ if (field.text) strings.push(field.text);
8533
+ strings.push(...collectProtoStrings(decodeProtoMessage(field.bytes), depth + 1));
8534
+ }
8535
+ return strings;
8536
+ }
8537
+
8538
+ function isLikelyBase64(value) {
8539
+ const text = String(value || "").trim();
8540
+ return text.length >= 80 && text.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(text);
8541
+ }
8542
+
6816
8543
  function envPathList(value) {
6817
8544
  return String(value || "")
6818
8545
  .split(new RegExp(`[${escapeRegExp(path.delimiter)},]`))
@@ -6843,6 +8570,30 @@ function readJsonMaybe(file, fallback = null) {
6843
8570
  }
6844
8571
  }
6845
8572
 
8573
+ function parseJsonObject(value, fallback = {}) {
8574
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
8575
+ if (typeof value !== "string" || !value.trim()) return fallback;
8576
+ try {
8577
+ const parsed = JSON.parse(value);
8578
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback;
8579
+ } catch {
8580
+ return fallback;
8581
+ }
8582
+ }
8583
+
8584
+ function groupRowsBy(rows, key) {
8585
+ const grouped = new Map();
8586
+ for (const row of rows || []) {
8587
+ const value = row?.[key];
8588
+ if (value === undefined || value === null || value === "") continue;
8589
+ const groupKey = String(value);
8590
+ const group = grouped.get(groupKey) || [];
8591
+ group.push(row);
8592
+ grouped.set(groupKey, group);
8593
+ }
8594
+ return grouped;
8595
+ }
8596
+
6846
8597
  function defaultSkipDirs() {
6847
8598
  return new Set([".git", "node_modules", "vendor", "dist", "build", ".next", ".cache", ".venv", "venv", "__pycache__"]);
6848
8599
  }
@@ -7037,6 +8788,7 @@ module.exports = {
7037
8788
  mergeCursorRawAssistantOnlySessions,
7038
8789
  mergeCursorRawCompanionSessions,
7039
8790
  importCliHistory,
8791
+ importWindsurfTrajectoryExport,
7040
8792
  importWebChat,
7041
8793
  normalizeEventRole,
7042
8794
  parseClaudeDesktopSessionFile,
@@ -7046,18 +8798,26 @@ module.exports = {
7046
8798
  providerAdapterForSource,
7047
8799
  providerAdapterSources,
7048
8800
  geminiCliHistoryFiles,
8801
+ openCodeDatabaseFiles,
7049
8802
  openCodeStorageRoots,
7050
8803
  readCursorProjectTranscriptSessions,
7051
8804
  readCursorGlobalDiskKvSessionsFromDb,
7052
8805
  readCursorRawSqliteSalvageSessionsFromDb,
7053
8806
  readAiderSessions,
8807
+ readAntigravitySessions,
7054
8808
  readClineSessions,
7055
8809
  readDevinSessionsFromDb,
7056
8810
  readGeminiCliSessions,
7057
8811
  readOpenCodeSessions,
8812
+ readWindsurfTrajectoryExport,
7058
8813
  readExportBundle,
7059
8814
  readExportJson,
7060
8815
  normalizeWebConversations,
7061
8816
  claudeFileHistoryFiles,
7062
- claudeFileHistoryRoot
8817
+ claudeFileHistoryRoot,
8818
+ __cursorTestables: {
8819
+ cursorAttributionCwd,
8820
+ cursorIsSystemRootPath,
8821
+ cursorMostFrequentExistingCwd
8822
+ }
7063
8823
  };