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