agentel 0.2.3 → 0.2.5
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 +4 -4
- package/docs/code-reference.md +3 -3
- package/docs/history-source-handling.md +88 -39
- package/docs/release.md +1 -2
- package/package.json +1 -2
- package/src/cli.js +58 -16
- package/src/config.js +4 -1
- package/src/doctor.js +13 -2
- package/src/importers/providers.js +26 -2
- package/src/importers.js +534 -67
- package/src/parser-versions.js +5 -0
- package/src/search.js +55 -26
- package/src/sources.js +7 -3
- package/agentlog-spec.md +0 -558
package/src/importers.js
CHANGED
|
@@ -22,6 +22,8 @@ const { canonicalWebProvider, derivedAccountId, getWebAccount, upsertWebAccount
|
|
|
22
22
|
|
|
23
23
|
const WEB_TOKEN_ESTIMATE_CHARS = 4;
|
|
24
24
|
const WEB_CHAT_TOKEN_ESTIMATION_METHOD = "web-message-parts-chars-v1";
|
|
25
|
+
const OPENCODE_SOURCE_KINDS = new Set(["cli", "desktop", "web"]);
|
|
26
|
+
const OPENCODE_SESSION_ID_RE = /\bses_[A-Za-z0-9]+\b/g;
|
|
25
27
|
|
|
26
28
|
function importCliHistory(options = {}, env = process.env) {
|
|
27
29
|
const source = options.source || "all";
|
|
@@ -76,6 +78,7 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
76
78
|
const archived = archivedSessionKeys(env);
|
|
77
79
|
const files =
|
|
78
80
|
provider === "claude_code" ? claudeFiles(env) : provider === "claude_sdk" ? claudeSdkFiles(env) : jsonlFiles(roots);
|
|
81
|
+
const claudeCodeMetadata = provider === "claude_code" ? claudeCodeSessionMetadataByCliSessionId(env) : new Map();
|
|
79
82
|
|
|
80
83
|
const candidates = files
|
|
81
84
|
.map((file) => ({ file, stat: safeStat(file) }))
|
|
@@ -95,7 +98,11 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
95
98
|
for (let index = 0; index < candidates.length; index++) {
|
|
96
99
|
const item = candidates[index];
|
|
97
100
|
const sourceType = jsonlProviderSourceType(provider);
|
|
98
|
-
const
|
|
101
|
+
const baseFingerprint = `${fingerprintPrefix(sourceType)}:${fileFingerprint(item.file, item.stat)}`;
|
|
102
|
+
const preliminaryMetadata = provider === "claude_code"
|
|
103
|
+
? claudeCodeMetadata.get(claudeSessionIdFromFilename(item.file)) || null
|
|
104
|
+
: null;
|
|
105
|
+
let fingerprint = claudeCodeImportFingerprint(baseFingerprint, preliminaryMetadata);
|
|
99
106
|
if (alreadyImportedFile(state, fingerprint, archived, provider)) {
|
|
100
107
|
summary.skipped++;
|
|
101
108
|
reportProgress(options, summary, index + 1, item.file);
|
|
@@ -115,15 +122,17 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
115
122
|
reportProgress(options, summary, index + 1, item.file);
|
|
116
123
|
continue;
|
|
117
124
|
}
|
|
118
|
-
const
|
|
125
|
+
const sessionId = parsed.sessionId || stableSessionId(provider, item.file, parsed.startedAt, parsed.messages);
|
|
126
|
+
const sessionMetadata = preliminaryMetadata || claudeCodeMetadata.get(sessionId) || null;
|
|
127
|
+
fingerprint = claudeCodeImportFingerprint(baseFingerprint, sessionMetadata);
|
|
128
|
+
const cwd = parsed.cwd || sessionMetadata?.cwd || "";
|
|
119
129
|
const scopeCanonical = cwd ? "" : uncategorizedScope(provider);
|
|
120
|
-
const repo = cwd
|
|
130
|
+
const repo = repoInfoForImport(provider, cwd, sessionMetadata);
|
|
121
131
|
if (options.repos && options.repos.length && (!repo || !options.repos.includes(repo.key))) {
|
|
122
132
|
summary.skipped++;
|
|
123
133
|
reportProgress(options, summary, index + 1, item.file);
|
|
124
134
|
continue;
|
|
125
135
|
}
|
|
126
|
-
const sessionId = parsed.sessionId || stableSessionId(provider, item.file, parsed.startedAt, parsed.messages);
|
|
127
136
|
if (alreadyImported(state, sessionId, fingerprint, archived, provider)) {
|
|
128
137
|
state.files[fingerprint] = { sessionId, duplicate: true, at: new Date().toISOString() };
|
|
129
138
|
summary.skipped++;
|
|
@@ -145,9 +154,10 @@ function importJsonlProvider(provider, roots, since, options = {}, env = process
|
|
|
145
154
|
startedAt: parsed.startedAt,
|
|
146
155
|
endedAt: parsed.endedAt,
|
|
147
156
|
sourcePath: item.file,
|
|
148
|
-
sourceFiles: [item.file, ...auxiliaryFiles],
|
|
157
|
+
sourceFiles: [item.file, sessionMetadata?.sourcePath || "", ...auxiliaryFiles].filter(Boolean),
|
|
149
158
|
sourceType,
|
|
150
|
-
title: parsed
|
|
159
|
+
title: jsonlSessionTitleForImport(parsed, sessionMetadata),
|
|
160
|
+
sessionSummary: claudeCodeSidecarSessionSummary(sessionMetadata)
|
|
151
161
|
},
|
|
152
162
|
env
|
|
153
163
|
);
|
|
@@ -172,7 +182,7 @@ function jsonlProviderSourceType(provider) {
|
|
|
172
182
|
function importClaudeDesktopProvider(provider, since, options = {}, env = process.env) {
|
|
173
183
|
const state = loadImportState(env);
|
|
174
184
|
const archived = archivedSessionKeys(env);
|
|
175
|
-
const sessions = readClaudeDesktopSessions().filter((session) => {
|
|
185
|
+
const sessions = readClaudeDesktopSessions({}, env).filter((session) => {
|
|
176
186
|
return !options.claudeDesktopKind || session.kind === options.claudeDesktopKind;
|
|
177
187
|
});
|
|
178
188
|
const candidates = sessions
|
|
@@ -199,7 +209,7 @@ function importClaudeDesktopProvider(provider, since, options = {}, env = proces
|
|
|
199
209
|
continue;
|
|
200
210
|
}
|
|
201
211
|
const cwd = session.cwd || "";
|
|
202
|
-
const repo =
|
|
212
|
+
const repo = repoInfoForImport(provider, cwd);
|
|
203
213
|
if (options.repos && options.repos.length && !matchesImportedSessionRepo(session, repo, options.repos)) {
|
|
204
214
|
summary.skipped++;
|
|
205
215
|
reportProgress(options, summary, index + 1, session.sourcePath);
|
|
@@ -564,7 +574,7 @@ function structuredSessionReplaceSourcePathCopies(provider, sourceType) {
|
|
|
564
574
|
}
|
|
565
575
|
|
|
566
576
|
function structuredSessionUsesSharedRawFiles(provider, sourceType) {
|
|
567
|
-
return provider === "opencode" &&
|
|
577
|
+
return provider === "opencode" && ["opencode-cli-sqlite-history", "opencode-web-sqlite-history", "opencode-sqlite-history", "opencode-desktop-sqlite-history"].includes(sourceType);
|
|
568
578
|
}
|
|
569
579
|
|
|
570
580
|
function parseAgentJsonl(file, provider) {
|
|
@@ -602,16 +612,114 @@ function parseAgentJsonl(file, provider) {
|
|
|
602
612
|
}
|
|
603
613
|
messages.sort((a, b) => String(a.timestamp || "").localeCompare(String(b.timestamp || "")));
|
|
604
614
|
const deduped = dedupeAdjacentMessages(messages);
|
|
615
|
+
const inferredTitle = inferredJsonlSessionTitle(provider, deduped);
|
|
605
616
|
return {
|
|
606
617
|
cwd,
|
|
607
618
|
sessionId,
|
|
608
|
-
title,
|
|
619
|
+
title: title || inferredTitle,
|
|
620
|
+
titleSource: title ? "source" : inferredTitle ? "first-user-prompt" : "",
|
|
609
621
|
messages: deduped,
|
|
610
622
|
startedAt: deduped[0]?.timestamp || "",
|
|
611
623
|
endedAt: deduped[deduped.length - 1]?.timestamp || ""
|
|
612
624
|
};
|
|
613
625
|
}
|
|
614
626
|
|
|
627
|
+
function repoInfoForImport(provider, cwd, metadata = null) {
|
|
628
|
+
const repoCwd = repoCwdForImport(provider, cwd, metadata);
|
|
629
|
+
if (!repoCwd) return null;
|
|
630
|
+
return canonicalRepo(repoCwd);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function repoCwdForImport(provider, cwd, metadata = null) {
|
|
634
|
+
const metadataCwd = firstExistingDirectory(metadata?.originCwd, metadata?.cwd);
|
|
635
|
+
if (metadataCwd) return metadataCwd;
|
|
636
|
+
return claudeWorktreeParentRepo(provider, cwd) || cwd;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function claudeWorktreeParentRepo(provider, cwd) {
|
|
640
|
+
if (!isClaudeJsonlProvider(provider)) return "";
|
|
641
|
+
const resolved = path.resolve(String(cwd || ""));
|
|
642
|
+
const marker = `${path.sep}.claude${path.sep}worktrees${path.sep}`;
|
|
643
|
+
const markerIndex = resolved.indexOf(marker);
|
|
644
|
+
if (markerIndex === -1) return "";
|
|
645
|
+
const parentRepo = resolved.slice(0, markerIndex);
|
|
646
|
+
const stat = safeStat(parentRepo);
|
|
647
|
+
return stat && stat.isDirectory() ? parentRepo : "";
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function inferredJsonlSessionTitle(provider, messages) {
|
|
651
|
+
if (!isClaudeJsonlProvider(provider)) return "";
|
|
652
|
+
const firstUser = (messages || []).find((message) => message.role === "user" && !message.metadata?.providerGenerated);
|
|
653
|
+
return titleFromPrompt(firstUser?.content);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function titleFromPrompt(value) {
|
|
657
|
+
const cleaned = firstLine(value).replace(/\s+/g, " ").trim();
|
|
658
|
+
if (!cleaned) return "";
|
|
659
|
+
const max = 96;
|
|
660
|
+
return cleaned.length > max ? `${cleaned.slice(0, max - 1).trimEnd()}…` : cleaned;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function isClaudeJsonlProvider(provider) {
|
|
664
|
+
return provider === "claude_code" || provider === "claude_sdk";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function jsonlSessionTitleForImport(parsed, metadata = null) {
|
|
668
|
+
if (parsed?.titleSource === "source") return parsed.title || "";
|
|
669
|
+
return firstString(metadata?.title, parsed?.title);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function claudeCodeSidecarSessionSummary(metadata = null) {
|
|
673
|
+
if (!metadata) return null;
|
|
674
|
+
const sidecar = compactMetadata({
|
|
675
|
+
appSessionId: metadata.sessionId || undefined,
|
|
676
|
+
cliSessionId: metadata.cliSessionId || undefined,
|
|
677
|
+
title: metadata.title || undefined,
|
|
678
|
+
titleSource: metadata.titleSource || undefined,
|
|
679
|
+
cwd: metadata.cwd || undefined,
|
|
680
|
+
originCwd: metadata.originCwd || undefined,
|
|
681
|
+
worktreePath: metadata.worktreePath || undefined,
|
|
682
|
+
worktreeName: metadata.worktreeName || undefined,
|
|
683
|
+
sourceBranch: metadata.sourceBranch || undefined,
|
|
684
|
+
branch: metadata.branch || undefined,
|
|
685
|
+
createdAt: metadata.createdAt || undefined,
|
|
686
|
+
lastActivityAt: metadata.lastActivityAt || undefined,
|
|
687
|
+
model: metadata.model || undefined,
|
|
688
|
+
effort: metadata.effort || undefined,
|
|
689
|
+
permissionMode: metadata.permissionMode || undefined,
|
|
690
|
+
chromePermissionMode: metadata.chromePermissionMode || undefined,
|
|
691
|
+
completedTurns: metadata.completedTurns,
|
|
692
|
+
isArchived: typeof metadata.isArchived === "boolean" ? metadata.isArchived : undefined,
|
|
693
|
+
enabledMcpToolCount: metadata.enabledMcpToolCount,
|
|
694
|
+
mcpServerNames: metadata.mcpServerNames?.length ? metadata.mcpServerNames : undefined,
|
|
695
|
+
sourcePath: metadata.sourcePath || undefined
|
|
696
|
+
});
|
|
697
|
+
if (!sidecar) return null;
|
|
698
|
+
return compactMetadata({
|
|
699
|
+
claudeCodeSidecar: sidecar,
|
|
700
|
+
modelUsage: metadata.model ? [{ model: metadata.model, source: "claude-code-sidecar" }] : undefined
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function firstExistingDirectory(...values) {
|
|
705
|
+
for (const value of values) {
|
|
706
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
707
|
+
const stat = safeStat(value.trim());
|
|
708
|
+
if (stat && stat.isDirectory()) return value.trim();
|
|
709
|
+
}
|
|
710
|
+
return "";
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function claudeCodeImportFingerprint(baseFingerprint, metadata = null) {
|
|
714
|
+
if (!metadata?.sourcePath) return baseFingerprint;
|
|
715
|
+
return `${baseFingerprint}:claude-code-session:${fileFingerprint(metadata.sourcePath, safeStat(metadata.sourcePath))}`;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function claudeSessionIdFromFilename(file) {
|
|
719
|
+
const base = path.basename(String(file || "")).replace(/\.jsonl(?:\.zst)?$/i, "");
|
|
720
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(base) ? base : "";
|
|
721
|
+
}
|
|
722
|
+
|
|
615
723
|
function extractMessages(event, provider, context = {}) {
|
|
616
724
|
const claudeMessages = extractClaudeMessagesFromEvent(event, provider, context);
|
|
617
725
|
if (claudeMessages.length) {
|
|
@@ -2388,7 +2496,7 @@ function discoverCliHistory(env = process.env, options = {}) {
|
|
|
2388
2496
|
|
|
2389
2497
|
const claudeDesktopSessions = readClaudeDesktopSessions({
|
|
2390
2498
|
onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "Claude App" })
|
|
2391
|
-
});
|
|
2499
|
+
}, env);
|
|
2392
2500
|
publish("claudeCodeDesktop", "Claude Code Desktop", summarizeClaudeDesktopSessions(claudeDesktopSessions, "claude-code-desktop-metadata"));
|
|
2393
2501
|
publish("claudeWorkspace", "Claude Workspace", summarizeClaudeDesktopSessions(claudeDesktopSessions, "claude-workspace-desktop"));
|
|
2394
2502
|
publish("claudeSdk", "Claude SDK jobs", summarizeClaudeSdkScan(claudeScan));
|
|
@@ -2443,13 +2551,38 @@ function discoverCliHistory(env = process.env, options = {}) {
|
|
|
2443
2551
|
);
|
|
2444
2552
|
|
|
2445
2553
|
publish(
|
|
2446
|
-
"
|
|
2447
|
-
"OpenCode",
|
|
2554
|
+
"opencodeCli",
|
|
2555
|
+
"OpenCode CLI",
|
|
2556
|
+
summarizeStructuredSessions(
|
|
2557
|
+
readOpenCodeSessions(env, {
|
|
2558
|
+
openCodeKind: "cli",
|
|
2559
|
+
onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "OpenCode CLI" })
|
|
2560
|
+
}),
|
|
2561
|
+
"OpenCode CLI/core SQLite database plus project JSON session/message/part storage"
|
|
2562
|
+
)
|
|
2563
|
+
);
|
|
2564
|
+
|
|
2565
|
+
publish(
|
|
2566
|
+
"opencodeDesktop",
|
|
2567
|
+
"OpenCode Desktop",
|
|
2448
2568
|
summarizeStructuredSessions(
|
|
2449
2569
|
readOpenCodeSessions(env, {
|
|
2450
|
-
|
|
2570
|
+
openCodeKind: "desktop",
|
|
2571
|
+
onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "OpenCode Desktop" })
|
|
2451
2572
|
}),
|
|
2452
|
-
"OpenCode SQLite database and JSON session/message/part storage"
|
|
2573
|
+
"OpenCode Desktop app-specific SQLite database and JSON session/message/part storage"
|
|
2574
|
+
)
|
|
2575
|
+
);
|
|
2576
|
+
|
|
2577
|
+
publish(
|
|
2578
|
+
"opencodeWeb",
|
|
2579
|
+
"OpenCode Web",
|
|
2580
|
+
summarizeStructuredSessions(
|
|
2581
|
+
readOpenCodeSessions(env, {
|
|
2582
|
+
openCodeKind: "web",
|
|
2583
|
+
onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "OpenCode Web" })
|
|
2584
|
+
}),
|
|
2585
|
+
"OpenCode web sessions from the shared OpenCode SQLite store"
|
|
2453
2586
|
)
|
|
2454
2587
|
);
|
|
2455
2588
|
|
|
@@ -2524,7 +2657,7 @@ function summarizeClaudeSdkScan(scan) {
|
|
|
2524
2657
|
}
|
|
2525
2658
|
|
|
2526
2659
|
function summarizeClaudeDesktop(env = process.env, options = {}) {
|
|
2527
|
-
const sessions = readClaudeDesktopSessions(options);
|
|
2660
|
+
const sessions = readClaudeDesktopSessions(options, env);
|
|
2528
2661
|
return summarizeClaudeDesktopSessions(sessions);
|
|
2529
2662
|
}
|
|
2530
2663
|
|
|
@@ -3045,10 +3178,10 @@ function summarizeCodexSources(threads) {
|
|
|
3045
3178
|
return { cli, desktop, ...(archived ? { archived } : {}), ...(summaries ? { summaries } : {}) };
|
|
3046
3179
|
}
|
|
3047
3180
|
|
|
3048
|
-
function readClaudeDesktopSessions(options = {}) {
|
|
3181
|
+
function readClaudeDesktopSessions(options = {}, env = process.env) {
|
|
3049
3182
|
const roots = [
|
|
3050
|
-
{ root:
|
|
3051
|
-
{ root:
|
|
3183
|
+
{ root: claudeCodeSessionsRoot(env), kind: "claude-code-desktop-metadata" },
|
|
3184
|
+
{ root: claudeWorkspaceSessionsRoot(env), kind: "claude-workspace-desktop" }
|
|
3052
3185
|
];
|
|
3053
3186
|
const candidates = [];
|
|
3054
3187
|
for (const { root, kind } of roots) {
|
|
@@ -3071,6 +3204,90 @@ function readClaudeDesktopSessions(options = {}) {
|
|
|
3071
3204
|
return sessions;
|
|
3072
3205
|
}
|
|
3073
3206
|
|
|
3207
|
+
function claudeCodeSessionMetadataByCliSessionId(env = process.env) {
|
|
3208
|
+
const byCliSessionId = new Map();
|
|
3209
|
+
collectFiles(claudeCodeSessionsRoot(env), (file) => {
|
|
3210
|
+
if (!path.basename(file).startsWith("local_") || !file.endsWith(".json")) return;
|
|
3211
|
+
const metadata = parseClaudeCodeSessionMetadataFile(file);
|
|
3212
|
+
if (metadata?.cliSessionId) byCliSessionId.set(metadata.cliSessionId, metadata);
|
|
3213
|
+
});
|
|
3214
|
+
return byCliSessionId;
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function parseClaudeCodeSessionMetadataFile(file) {
|
|
3218
|
+
let data;
|
|
3219
|
+
try {
|
|
3220
|
+
data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
3221
|
+
} catch {
|
|
3222
|
+
return null;
|
|
3223
|
+
}
|
|
3224
|
+
const cliSessionId = firstString(data.cliSessionId);
|
|
3225
|
+
if (!cliSessionId) return null;
|
|
3226
|
+
return {
|
|
3227
|
+
cliSessionId,
|
|
3228
|
+
sessionId: firstString(data.sessionId),
|
|
3229
|
+
title: firstString(data.title),
|
|
3230
|
+
titleSource: firstString(data.titleSource),
|
|
3231
|
+
cwd: firstString(data.cwd),
|
|
3232
|
+
originCwd: firstString(data.originCwd),
|
|
3233
|
+
worktreePath: firstString(data.worktreePath),
|
|
3234
|
+
worktreeName: firstString(data.worktreeName),
|
|
3235
|
+
sourceBranch: firstString(data.sourceBranch),
|
|
3236
|
+
branch: firstString(data.branch),
|
|
3237
|
+
createdAt: toIso(data.createdAt),
|
|
3238
|
+
lastActivityAt: toIso(data.lastActivityAt),
|
|
3239
|
+
model: firstString(data.model),
|
|
3240
|
+
effort: firstString(data.effort),
|
|
3241
|
+
permissionMode: firstString(data.permissionMode),
|
|
3242
|
+
chromePermissionMode: firstString(data.chromePermissionMode),
|
|
3243
|
+
completedTurns: numberValue(data.completedTurns),
|
|
3244
|
+
isArchived: typeof data.isArchived === "boolean" ? data.isArchived : undefined,
|
|
3245
|
+
enabledMcpToolCount: enabledMcpToolCount(data.enabledMcpTools),
|
|
3246
|
+
mcpServerNames: claudeMcpServerNames(data.remoteMcpServersConfig),
|
|
3247
|
+
sourcePath: file
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
function enabledMcpToolCount(value) {
|
|
3252
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
3253
|
+
return Object.values(value).filter(Boolean).length;
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
function claudeMcpServerNames(value) {
|
|
3257
|
+
if (!Array.isArray(value)) return [];
|
|
3258
|
+
return [...new Set(value.map((server) => firstString(server?.name)).filter(Boolean))].sort((a, b) => a.localeCompare(b));
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
function compactMetadata(value) {
|
|
3262
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value || null;
|
|
3263
|
+
const result = {};
|
|
3264
|
+
for (const [key, item] of Object.entries(value)) {
|
|
3265
|
+
if (item === undefined || item === null || item === "") continue;
|
|
3266
|
+
if (Array.isArray(item) && !item.length) continue;
|
|
3267
|
+
if (item && typeof item === "object" && !Array.isArray(item) && !Object.keys(item).length) continue;
|
|
3268
|
+
result[key] = item;
|
|
3269
|
+
}
|
|
3270
|
+
return Object.keys(result).length ? result : null;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
function numberValue(value) {
|
|
3274
|
+
const n = Number(value);
|
|
3275
|
+
return Number.isFinite(n) ? n : undefined;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function claudeAppSupportRoot(env = process.env) {
|
|
3279
|
+
const home = env && env.HOME ? env.HOME : os.homedir();
|
|
3280
|
+
return env.CLAUDE_APP_SUPPORT || path.join(home, "Library", "Application Support", "Claude");
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
function claudeCodeSessionsRoot(env = process.env) {
|
|
3284
|
+
return path.join(claudeAppSupportRoot(env), "claude-code-sessions");
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
function claudeWorkspaceSessionsRoot(env = process.env) {
|
|
3288
|
+
return path.join(claudeAppSupportRoot(env), "local-agent-mode-sessions");
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3074
3291
|
function parseClaudeDesktopSessionFile(file, kind = "claude-workspace-desktop") {
|
|
3075
3292
|
let data;
|
|
3076
3293
|
try {
|
|
@@ -3337,7 +3554,10 @@ function cursorBuildComposerInfoLookup(env = process.env) {
|
|
|
3337
3554
|
"key,",
|
|
3338
3555
|
"json_extract(value, '$.modelId') as modelId,",
|
|
3339
3556
|
"json_extract(value, '$.modelName') as modelName,",
|
|
3340
|
-
"json_extract(value, '$.model') as model",
|
|
3557
|
+
"json_extract(value, '$.model') as model,",
|
|
3558
|
+
"json_extract(value, '$.modelInfo.modelName') as modelInfoModelName,",
|
|
3559
|
+
"json_extract(value, '$.modelInfo.modelId') as modelInfoModelId,",
|
|
3560
|
+
"json_extract(value, '$.modelInfo.model') as modelInfoModel",
|
|
3341
3561
|
"from cursorDiskKV where",
|
|
3342
3562
|
"json_valid(value) and",
|
|
3343
3563
|
cursorDiskKvPrefixRangeCondition("bubbleId:")
|
|
@@ -3348,7 +3568,14 @@ function cursorBuildComposerInfoLookup(env = process.env) {
|
|
|
3348
3568
|
const keyMatch = String(row.key || "").match(/^bubbleId:([^:]+):/);
|
|
3349
3569
|
if (!keyMatch) continue;
|
|
3350
3570
|
const composerId = keyMatch[1].toLowerCase();
|
|
3351
|
-
const model = firstString(
|
|
3571
|
+
const model = firstString(
|
|
3572
|
+
row.modelInfoModelName,
|
|
3573
|
+
row.modelInfoModelId,
|
|
3574
|
+
row.modelInfoModel,
|
|
3575
|
+
row.modelId,
|
|
3576
|
+
row.modelName,
|
|
3577
|
+
row.model
|
|
3578
|
+
);
|
|
3352
3579
|
if (!model) continue;
|
|
3353
3580
|
const entry = info.get(composerId) || { title: "", modelHist: new Map() };
|
|
3354
3581
|
entry.modelHist.set(model, (entry.modelHist.get(model) || 0) + 1);
|
|
@@ -3960,6 +4187,7 @@ function cursorGlobalBubbleSelectColumns(valueExpression = "value", keyExpressio
|
|
|
3960
4187
|
`json_extract(${valueExpression}, '$.modelName') as modelName`,
|
|
3961
4188
|
`json_extract(${valueExpression}, '$.modelSlug') as modelSlug`,
|
|
3962
4189
|
`json_extract(${valueExpression}, '$.modelConfig') as modelConfig`,
|
|
4190
|
+
`json_extract(${valueExpression}, '$.modelInfo') as modelInfo`,
|
|
3963
4191
|
`json_extract(${valueExpression}, '$.providerOptions') as providerOptions`,
|
|
3964
4192
|
`json_extract(${valueExpression}, '$.status') as status`,
|
|
3965
4193
|
`json_extract(${valueExpression}, '$.state') as state`,
|
|
@@ -4014,6 +4242,7 @@ function cursorGlobalBubbleDataFromRow(row) {
|
|
|
4014
4242
|
modelName: row.modelName,
|
|
4015
4243
|
modelSlug: row.modelSlug,
|
|
4016
4244
|
modelConfig: cursorParseSqliteJsonColumn(row.modelConfig),
|
|
4245
|
+
modelInfo: cursorParseSqliteJsonColumn(row.modelInfo),
|
|
4017
4246
|
providerOptions: cursorParseSqliteJsonColumn(row.providerOptions),
|
|
4018
4247
|
status: row.status,
|
|
4019
4248
|
state: cursorParseSqliteJsonColumn(row.state) || row.state,
|
|
@@ -4149,6 +4378,7 @@ const CURSOR_RAW_ASSISTANT_MERGE_MIN_SCORE = 32;
|
|
|
4149
4378
|
const CURSOR_RAW_ASSISTANT_MERGE_MIN_OVERLAP = 2;
|
|
4150
4379
|
const CURSOR_ABSOLUTE_PATH_RE = /(?:file:\/\/)?\/(?:Users|home|Volumes|private|tmp|var)\/[^\s"'`<>{}|]+/g;
|
|
4151
4380
|
const SQLITE_QUERY_TIMEOUT_MS = 30 * 1000;
|
|
4381
|
+
const OPENCODE_SQLITE_BATCH_SIZE = 100;
|
|
4152
4382
|
|
|
4153
4383
|
function readCursorRawSqliteSalvageSessionsFromDb(dbPath, options = {}) {
|
|
4154
4384
|
const files = cursorRawSqliteFilesForDb(dbPath);
|
|
@@ -5150,6 +5380,11 @@ function cursorMessageMetadata(record, source) {
|
|
|
5150
5380
|
|
|
5151
5381
|
function cursorModel(record) {
|
|
5152
5382
|
return firstCursorModel(
|
|
5383
|
+
record?.modelInfo?.modelName,
|
|
5384
|
+
record?.modelInfo?.modelId,
|
|
5385
|
+
record?.modelInfo?.model,
|
|
5386
|
+
record?.message?.modelInfo?.modelName,
|
|
5387
|
+
record?.message?.modelInfo?.modelId,
|
|
5153
5388
|
record?.model,
|
|
5154
5389
|
record?.modelId,
|
|
5155
5390
|
record?.modelID,
|
|
@@ -6538,13 +6773,24 @@ function clineTitle(messages) {
|
|
|
6538
6773
|
}
|
|
6539
6774
|
|
|
6540
6775
|
function readOpenCodeSessions(env = process.env, options = {}) {
|
|
6541
|
-
const dbs = openCodeDatabaseFiles(env);
|
|
6542
|
-
const roots = openCodeStorageRoots(env);
|
|
6776
|
+
const dbs = openCodeDatabaseFiles(env, options);
|
|
6777
|
+
const roots = openCodeStorageRoots(env, options);
|
|
6543
6778
|
const files = roots.flatMap((root) => openCodeSessionFiles(root).map((file) => ({ root, file })));
|
|
6544
6779
|
const sessions = [];
|
|
6545
6780
|
reportDiscoveryProgress(options, { current: 0, total: dbs.length, message: "reading OpenCode SQLite stores" });
|
|
6546
6781
|
for (let index = 0; index < dbs.length; index++) {
|
|
6547
|
-
|
|
6782
|
+
let dbSessions = [];
|
|
6783
|
+
try {
|
|
6784
|
+
dbSessions = readOpenCodeSqliteSessionsFromDb(dbs[index], options, env);
|
|
6785
|
+
} catch (error) {
|
|
6786
|
+
reportDiscoveryProgress(options, {
|
|
6787
|
+
current: index + 1,
|
|
6788
|
+
total: dbs.length,
|
|
6789
|
+
message: `SQLite skipped: ${error.message}`,
|
|
6790
|
+
path: dbs[index]
|
|
6791
|
+
});
|
|
6792
|
+
continue;
|
|
6793
|
+
}
|
|
6548
6794
|
sessions.push(...dbSessions);
|
|
6549
6795
|
reportDiscoveryProgress(options, {
|
|
6550
6796
|
current: index + 1,
|
|
@@ -6557,7 +6803,7 @@ function readOpenCodeSessions(env = process.env, options = {}) {
|
|
|
6557
6803
|
reportDiscoveryProgress(options, { current: 0, total: files.length, message: "reading OpenCode storage" });
|
|
6558
6804
|
for (let index = 0; index < files.length; index++) {
|
|
6559
6805
|
const item = files[index];
|
|
6560
|
-
const session = parseOpenCodeSessionFile(item.file, item.root);
|
|
6806
|
+
const session = parseOpenCodeSessionFile(item.file, item.root, env, options);
|
|
6561
6807
|
if (session) {
|
|
6562
6808
|
sessions.push(session);
|
|
6563
6809
|
seenSessionIds.add(session.sessionId.replace(/^opencode-/, ""));
|
|
@@ -6572,38 +6818,70 @@ function readOpenCodeSessions(env = process.env, options = {}) {
|
|
|
6572
6818
|
for (const root of roots) {
|
|
6573
6819
|
for (const sessionId of openCodeMessageSessionIds(root)) {
|
|
6574
6820
|
if (seenSessionIds.has(sessionId)) continue;
|
|
6575
|
-
const session = parseOpenCodeMessageOnlySession(root, sessionId);
|
|
6821
|
+
const session = parseOpenCodeMessageOnlySession(root, sessionId, env, options);
|
|
6576
6822
|
if (session) {
|
|
6577
6823
|
sessions.push(session);
|
|
6578
6824
|
seenSessionIds.add(sessionId);
|
|
6579
6825
|
}
|
|
6580
6826
|
}
|
|
6581
6827
|
}
|
|
6582
|
-
return dedupeStructuredSessions(sessions, "opencode");
|
|
6828
|
+
return filterOpenCodeSessionsForKind(dedupeStructuredSessions(sessions, "opencode"), options.openCodeKind);
|
|
6583
6829
|
}
|
|
6584
6830
|
|
|
6585
|
-
function
|
|
6831
|
+
function filterOpenCodeSessionsForKind(sessions, kind) {
|
|
6832
|
+
if (!OPENCODE_SOURCE_KINDS.has(kind)) return sessions;
|
|
6833
|
+
return (sessions || []).filter((session) => openCodeSourceKindForType(session.sourceType) === kind);
|
|
6834
|
+
}
|
|
6835
|
+
|
|
6836
|
+
function openCodeDataRoots(env = process.env, options = {}) {
|
|
6586
6837
|
const configured = env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR;
|
|
6587
6838
|
if (configured) return existingUniquePaths([configured]);
|
|
6588
|
-
|
|
6839
|
+
if (options.openCodeKind === "cli") return existingUniquePaths(openCodeCliDataRoots(env));
|
|
6840
|
+
if (options.openCodeKind === "desktop") return existingUniquePaths(openCodeDesktopDataRoots(env));
|
|
6841
|
+
if (options.openCodeKind === "web") return [];
|
|
6842
|
+
return existingUniquePaths([...openCodeCliDataRoots(env), ...openCodeDesktopDataRoots(env)]);
|
|
6843
|
+
}
|
|
6844
|
+
|
|
6845
|
+
function openCodeCliDataRoots(env = process.env) {
|
|
6846
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
6847
|
+
const roots = [
|
|
6848
|
+
path.join(home, ".local", "share", "opencode")
|
|
6849
|
+
];
|
|
6850
|
+
return roots;
|
|
6851
|
+
}
|
|
6852
|
+
|
|
6853
|
+
function openCodeDesktopDataRoots(env = process.env) {
|
|
6854
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
6589
6855
|
const roots = [
|
|
6590
|
-
path.join(home, "
|
|
6856
|
+
path.join(home, "Library", "Application Support", "ai.opencode.desktop"),
|
|
6591
6857
|
path.join(home, "Library", "Application Support", "opencode"),
|
|
6592
6858
|
path.join(home, ".local", "share", "ai.opencode.app"),
|
|
6593
6859
|
path.join(home, "Library", "Application Support", "ai.opencode.app")
|
|
6594
6860
|
];
|
|
6595
6861
|
const appData = env.APPDATA || env.LOCALAPPDATA || env.LocalAppData;
|
|
6596
6862
|
if (appData) {
|
|
6863
|
+
roots.push(path.join(appData, "ai.opencode.desktop"));
|
|
6597
6864
|
roots.push(path.join(appData, "opencode"));
|
|
6598
6865
|
roots.push(path.join(appData, "ai.opencode.app"));
|
|
6599
6866
|
}
|
|
6600
6867
|
return existingUniquePaths(roots);
|
|
6601
6868
|
}
|
|
6602
6869
|
|
|
6603
|
-
function
|
|
6870
|
+
function openCodeSqliteDataRoots(env = process.env) {
|
|
6871
|
+
return existingUniquePaths([...openCodeCliDataRoots(env), ...openCodeDesktopDataRoots(env)]);
|
|
6872
|
+
}
|
|
6873
|
+
|
|
6874
|
+
function openCodeDatabaseRoots(env = process.env, options = {}) {
|
|
6875
|
+
const configured = env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR;
|
|
6876
|
+
if (configured) return existingUniquePaths([configured]);
|
|
6877
|
+
if (OPENCODE_SOURCE_KINDS.has(options.openCodeKind)) return openCodeSqliteDataRoots(env);
|
|
6878
|
+
return openCodeSqliteDataRoots(env);
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6881
|
+
function openCodeStorageRoots(env = process.env, options = {}) {
|
|
6604
6882
|
const explicit = envPathList(env.AGENTLOG_OPENCODE_STORAGE_ROOTS || env.AGENTLOG_OPENCODE_STORAGE_DIR);
|
|
6605
6883
|
if (explicit.length) return existingUniquePaths(explicit);
|
|
6606
|
-
const dataRoots = openCodeDataRoots(env);
|
|
6884
|
+
const dataRoots = openCodeDataRoots(env, options);
|
|
6607
6885
|
const roots = [];
|
|
6608
6886
|
for (const dataRoot of dataRoots) {
|
|
6609
6887
|
roots.push(path.join(dataRoot, "storage"));
|
|
@@ -6622,11 +6900,11 @@ function openCodeStorageRoots(env = process.env) {
|
|
|
6622
6900
|
return existingUniquePaths(roots);
|
|
6623
6901
|
}
|
|
6624
6902
|
|
|
6625
|
-
function openCodeDatabaseFiles(env = process.env) {
|
|
6903
|
+
function openCodeDatabaseFiles(env = process.env, options = {}) {
|
|
6626
6904
|
const explicit = envPathList(env.AGENTLOG_OPENCODE_DB || env.AGENTLOG_OPENCODE_DATABASE || env.OPENCODE_DB);
|
|
6627
6905
|
if (explicit.length) return existingUniquePaths(explicit);
|
|
6628
6906
|
if ((env.AGENTLOG_OPENCODE_STORAGE_ROOTS || env.AGENTLOG_OPENCODE_STORAGE_DIR) && !(env.AGENTLOG_OPENCODE_DATA_DIR || env.OPENCODE_DATA_DIR)) return [];
|
|
6629
|
-
return existingUniquePaths(
|
|
6907
|
+
return existingUniquePaths(openCodeDatabaseRoots(env, options).flatMap((root) => [
|
|
6630
6908
|
path.join(root, "opencode.db"),
|
|
6631
6909
|
path.join(root, "storage", "opencode.db")
|
|
6632
6910
|
]));
|
|
@@ -6656,31 +6934,64 @@ function openCodeMessageSessionIds(root) {
|
|
|
6656
6934
|
.sort((a, b) => a.localeCompare(b));
|
|
6657
6935
|
}
|
|
6658
6936
|
|
|
6659
|
-
function
|
|
6937
|
+
function openCodeDesktopSessionHints(env = process.env) {
|
|
6938
|
+
const hints = new Map();
|
|
6939
|
+
for (const root of openCodeDesktopDataRoots(env)) {
|
|
6940
|
+
collectFilesLimited(root, (file) => {
|
|
6941
|
+
if (!openCodeDesktopHintFile(file)) return;
|
|
6942
|
+
const stat = safeStat(file);
|
|
6943
|
+
if (!stat || stat.size > 2 * 1024 * 1024) return;
|
|
6944
|
+
let text = "";
|
|
6945
|
+
try {
|
|
6946
|
+
text = fs.readFileSync(file, "utf8");
|
|
6947
|
+
} catch {
|
|
6948
|
+
return;
|
|
6949
|
+
}
|
|
6950
|
+
for (const id of text.match(OPENCODE_SESSION_ID_RE) || []) {
|
|
6951
|
+
const files = hints.get(id) || new Set();
|
|
6952
|
+
files.add(file);
|
|
6953
|
+
hints.set(id, files);
|
|
6954
|
+
}
|
|
6955
|
+
}, 2, { skipDirs: new Set(["Cache", "Cache_Data", "Code Cache", "GPUCache", "blob_storage", "logs"]) });
|
|
6956
|
+
}
|
|
6957
|
+
return hints;
|
|
6958
|
+
}
|
|
6959
|
+
|
|
6960
|
+
function openCodeDesktopHintFile(file) {
|
|
6961
|
+
const base = path.basename(String(file || ""));
|
|
6962
|
+
return base.endsWith(".dat") || base === "opencode.settings" || base === "settings.json";
|
|
6963
|
+
}
|
|
6964
|
+
|
|
6965
|
+
function readOpenCodeSqliteSessionsFromDb(dbPath, options = {}, env = process.env) {
|
|
6660
6966
|
if (!safeStat(dbPath)) return [];
|
|
6661
6967
|
if (!sqliteTableExists(dbPath, "session") || !sqliteTableExists(dbPath, "message") || !sqliteTableExists(dbPath, "part")) return [];
|
|
6662
|
-
const sessionRows = readOpenCodeSqliteSessionRows(dbPath);
|
|
6663
|
-
|
|
6664
|
-
const
|
|
6968
|
+
const sessionRows = readOpenCodeSqliteSessionRows(dbPath, options);
|
|
6969
|
+
if (!sessionRows.length) return [];
|
|
6970
|
+
const classifications = openCodeSqliteSessionClassifications(sessionRows, dbPath, env, options);
|
|
6971
|
+
const sessionIds = sessionRows.map((row) => row.id).filter(Boolean);
|
|
6972
|
+
const messageRows = sortOpenCodeSqliteRows(readOpenCodeSqliteMessageRows(dbPath, sessionIds), ["session_id", "time_created", "id"]);
|
|
6973
|
+
const partRows = sortOpenCodeSqliteRows(readOpenCodeSqlitePartRows(dbPath, sessionIds), ["session_id", "message_id", "time_created", "id"]);
|
|
6665
6974
|
const messagesBySession = groupRowsBy(messageRows, "session_id");
|
|
6666
6975
|
const partsByMessage = groupRowsBy(partRows, "message_id");
|
|
6667
6976
|
const storageRoot = path.join(path.dirname(dbPath), "storage");
|
|
6668
6977
|
const sessions = [];
|
|
6669
6978
|
for (const row of sessionRows) {
|
|
6979
|
+
const sourceType = classifications.sourceTypes.get(String(row.id || "")) || openCodeSqliteSourceType(dbPath, env, options);
|
|
6670
6980
|
const rows = messagesBySession.get(row.id) || [];
|
|
6671
6981
|
const messages = stampMessages(
|
|
6672
6982
|
dedupeAdjacentMessages(rows.flatMap((messageRow, index) => openCodeSqliteMessagesFromRow(messageRow, partsByMessage.get(messageRow.id) || [], index)))
|
|
6673
6983
|
.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
|
|
6674
|
-
|
|
6984
|
+
sourceType
|
|
6675
6985
|
);
|
|
6676
6986
|
const diffFile = path.join(storageRoot, "session_diff", `${row.id}.json`);
|
|
6677
6987
|
const diffMessage = openCodeDiffMessage(diffFile, messages[messages.length - 1]?.timestamp || toIso(row.time_updated || row.time_created));
|
|
6678
|
-
const finalMessages = diffMessage ? messages.concat(stampMessages([diffMessage],
|
|
6988
|
+
const finalMessages = diffMessage ? messages.concat(stampMessages([diffMessage], sourceType)) : messages;
|
|
6679
6989
|
if (!finalMessages.length) continue;
|
|
6680
6990
|
const sourceFiles = [dbPath, safeStat(diffFile) ? diffFile : ""].filter(Boolean);
|
|
6681
6991
|
const startedAt = toIso(row.time_created) || finalMessages[0]?.timestamp || new Date(safeStat(dbPath)?.mtimeMs || Date.now()).toISOString();
|
|
6682
6992
|
const endedAt = toIso(row.time_updated) || finalMessages[finalMessages.length - 1]?.timestamp || startedAt;
|
|
6683
6993
|
const cwd = firstString(row.directory, row.path, row.project_worktree, openCodeCwdFromMessages(finalMessages));
|
|
6994
|
+
const hintFiles = classifications.hintFiles.get(String(row.id || "")) || [];
|
|
6684
6995
|
sessions.push({
|
|
6685
6996
|
sessionId: `opencode-${row.id}`,
|
|
6686
6997
|
title: firstString(row.title, row.slug, clineTitle(finalMessages), row.id),
|
|
@@ -6689,11 +7000,12 @@ function readOpenCodeSqliteSessionsFromDb(dbPath) {
|
|
|
6689
7000
|
endedAt,
|
|
6690
7001
|
messages: finalMessages,
|
|
6691
7002
|
sourcePath: `${dbPath}#${row.id}`,
|
|
6692
|
-
sourceFiles,
|
|
6693
|
-
sourceType
|
|
6694
|
-
fingerprint: openCodeSqliteSessionFingerprint(dbPath, row, rows, sourceFiles),
|
|
7003
|
+
sourceFiles: existingUniquePaths([...sourceFiles, ...hintFiles]),
|
|
7004
|
+
sourceType,
|
|
7005
|
+
fingerprint: openCodeSqliteSessionFingerprint(dbPath, row, rows, existingUniquePaths([...sourceFiles, ...hintFiles]), sourceType),
|
|
6695
7006
|
detailKey: "sqliteSessions",
|
|
6696
7007
|
sessionSummary: {
|
|
7008
|
+
source: openCodeSourceKindForType(sourceType),
|
|
6697
7009
|
projectId: row.project_id || undefined,
|
|
6698
7010
|
parentId: row.parent_id || undefined,
|
|
6699
7011
|
workspaceId: row.workspace_id || undefined,
|
|
@@ -6709,11 +7021,12 @@ function readOpenCodeSqliteSessionsFromDb(dbPath) {
|
|
|
6709
7021
|
return sessions;
|
|
6710
7022
|
}
|
|
6711
7023
|
|
|
6712
|
-
function readOpenCodeSqliteSessionRows(dbPath) {
|
|
7024
|
+
function readOpenCodeSqliteSessionRows(dbPath, options = {}) {
|
|
6713
7025
|
const sessionColumns = sqliteTableColumns(dbPath, "session");
|
|
6714
7026
|
if (!sessionColumns.has("id")) return [];
|
|
6715
7027
|
const projectColumns = sqliteTableExists(dbPath, "project") ? sqliteTableColumns(dbPath, "project") : new Set();
|
|
6716
7028
|
const canJoinProject = sessionColumns.has("project_id") && projectColumns.has("id");
|
|
7029
|
+
const timestampExpr = openCodeSqliteSessionTimestampExpr(sessionColumns);
|
|
6717
7030
|
const selects = [
|
|
6718
7031
|
"s.id",
|
|
6719
7032
|
sqliteSelectMaybe(sessionColumns, "s", "project_id"),
|
|
@@ -6735,7 +7048,11 @@ function readOpenCodeSqliteSessionRows(dbPath) {
|
|
|
6735
7048
|
];
|
|
6736
7049
|
const queryParts = [`select ${selects.join(", ")}`, "from session s"];
|
|
6737
7050
|
if (canJoinProject) queryParts.push("left join project p on p.id = s.project_id");
|
|
6738
|
-
|
|
7051
|
+
const where = [];
|
|
7052
|
+
if (sessionColumns.has("time_archived")) where.push("coalesce(s.time_archived, 0) = 0");
|
|
7053
|
+
const sinceCondition = openCodeSqliteSinceCondition(timestampExpr, options.since);
|
|
7054
|
+
if (sinceCondition) where.push(sinceCondition);
|
|
7055
|
+
if (where.length) queryParts.push(`where ${where.join(" and ")}`);
|
|
6739
7056
|
const orderColumns = [];
|
|
6740
7057
|
if (sessionColumns.has("time_updated")) orderColumns.push("s.time_updated desc");
|
|
6741
7058
|
if (sessionColumns.has("time_created")) orderColumns.push("s.time_created desc");
|
|
@@ -6744,7 +7061,109 @@ function readOpenCodeSqliteSessionRows(dbPath) {
|
|
|
6744
7061
|
return readSqliteJson(dbPath, queryParts.join(" "), "OpenCode SQLite sessions");
|
|
6745
7062
|
}
|
|
6746
7063
|
|
|
6747
|
-
function
|
|
7064
|
+
function openCodeSqliteSessionClassifications(sessionRows, dbPath, env = process.env, options = {}) {
|
|
7065
|
+
const baseSourceType = openCodeSqliteSourceType(dbPath, env, options);
|
|
7066
|
+
const rowsById = new Map((sessionRows || []).map((row) => [String(row.id || ""), row]).filter(([id]) => id));
|
|
7067
|
+
const desktopHints = openCodeDesktopSessionHints(env);
|
|
7068
|
+
const sourceTypes = new Map();
|
|
7069
|
+
const hintFiles = new Map();
|
|
7070
|
+
const classify = (row, visiting = new Set()) => {
|
|
7071
|
+
const id = String(row?.id || "");
|
|
7072
|
+
if (!id) return baseSourceType;
|
|
7073
|
+
if (sourceTypes.has(id)) return sourceTypes.get(id);
|
|
7074
|
+
if (desktopHints.has(id)) {
|
|
7075
|
+
sourceTypes.set(id, "opencode-desktop-sqlite-history");
|
|
7076
|
+
hintFiles.set(id, [...desktopHints.get(id)]);
|
|
7077
|
+
return "opencode-desktop-sqlite-history";
|
|
7078
|
+
}
|
|
7079
|
+
if (row?.parent_id && !visiting.has(id)) {
|
|
7080
|
+
visiting.add(id);
|
|
7081
|
+
const parentId = String(row.parent_id);
|
|
7082
|
+
const parent = rowsById.get(parentId);
|
|
7083
|
+
const parentSourceType = parent ? classify(parent, visiting) : "";
|
|
7084
|
+
if (parentSourceType === "opencode-desktop-sqlite-history") {
|
|
7085
|
+
sourceTypes.set(id, parentSourceType);
|
|
7086
|
+
const parentFiles = hintFiles.get(parentId);
|
|
7087
|
+
if (parentFiles?.length) hintFiles.set(id, parentFiles);
|
|
7088
|
+
return parentSourceType;
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
const sourceType = openCodeSqliteRowSourceType(row, dbPath, env, options, baseSourceType);
|
|
7092
|
+
sourceTypes.set(id, sourceType);
|
|
7093
|
+
return sourceType;
|
|
7094
|
+
};
|
|
7095
|
+
for (const row of sessionRows || []) classify(row);
|
|
7096
|
+
return { sourceTypes, hintFiles };
|
|
7097
|
+
}
|
|
7098
|
+
|
|
7099
|
+
function openCodeSqliteRowSourceType(row, dbPath, env = process.env, options = {}, baseSourceType = openCodeSqliteSourceType(dbPath, env, options)) {
|
|
7100
|
+
if (baseSourceType === "opencode-desktop-sqlite-history") return baseSourceType;
|
|
7101
|
+
if (!pathInsideAny(dbPath, openCodeCliDataRoots(env)) && !OPENCODE_SOURCE_KINDS.has(options.openCodeKind)) return baseSourceType;
|
|
7102
|
+
if (openCodeSqliteRowHasCliMetadata(row)) return "opencode-cli-sqlite-history";
|
|
7103
|
+
if (openCodeSqliteRowLooksWeb(row, dbPath, env)) return "opencode-web-sqlite-history";
|
|
7104
|
+
return baseSourceType;
|
|
7105
|
+
}
|
|
7106
|
+
|
|
7107
|
+
function openCodeSqliteRowHasCliMetadata(row) {
|
|
7108
|
+
return Boolean(firstString(row?.agent, row?.model));
|
|
7109
|
+
}
|
|
7110
|
+
|
|
7111
|
+
function openCodeSqliteRowLooksWeb(row, dbPath, env = process.env) {
|
|
7112
|
+
const version = firstString(row?.version);
|
|
7113
|
+
if (!version || version === "local") return false;
|
|
7114
|
+
return pathInsideAny(dbPath, openCodeCliDataRoots(env));
|
|
7115
|
+
}
|
|
7116
|
+
|
|
7117
|
+
function openCodeSqliteSourceType(dbPath, env = process.env, options = {}) {
|
|
7118
|
+
if (pathInsideAny(dbPath, openCodeDesktopDataRoots(env))) return "opencode-desktop-sqlite-history";
|
|
7119
|
+
if (pathInsideAny(dbPath, openCodeCliDataRoots(env))) return "opencode-sqlite-history";
|
|
7120
|
+
if (options.openCodeKind === "cli") return "opencode-cli-sqlite-history";
|
|
7121
|
+
if (options.openCodeKind === "desktop") return "opencode-desktop-sqlite-history";
|
|
7122
|
+
if (options.openCodeKind === "web") return "opencode-web-sqlite-history";
|
|
7123
|
+
return "opencode-sqlite-history";
|
|
7124
|
+
}
|
|
7125
|
+
|
|
7126
|
+
function openCodeStorageSourceType(storageRoot, env = process.env, options = {}) {
|
|
7127
|
+
if (options.openCodeKind === "cli") return "opencode-cli-history";
|
|
7128
|
+
if (options.openCodeKind === "desktop") return "opencode-desktop-history";
|
|
7129
|
+
if (pathInsideAny(storageRoot, openCodeCliDataRoots(env))) return "opencode-cli-history";
|
|
7130
|
+
if (pathInsideAny(storageRoot, openCodeDesktopDataRoots(env))) return "opencode-desktop-history";
|
|
7131
|
+
return "opencode-history";
|
|
7132
|
+
}
|
|
7133
|
+
|
|
7134
|
+
function openCodeSourceKindForType(sourceType) {
|
|
7135
|
+
if (String(sourceType || "").includes("cli")) return "cli";
|
|
7136
|
+
if (String(sourceType || "").includes("desktop")) return "desktop";
|
|
7137
|
+
if (String(sourceType || "").includes("web")) return "web";
|
|
7138
|
+
return "unknown";
|
|
7139
|
+
}
|
|
7140
|
+
|
|
7141
|
+
function pathInsideAny(candidate, roots) {
|
|
7142
|
+
const resolved = path.resolve(String(candidate || ""));
|
|
7143
|
+
return (roots || []).some((root) => {
|
|
7144
|
+
if (!root) return false;
|
|
7145
|
+
const resolvedRoot = path.resolve(String(root));
|
|
7146
|
+
return resolved === resolvedRoot || resolved.startsWith(`${resolvedRoot}${path.sep}`);
|
|
7147
|
+
});
|
|
7148
|
+
}
|
|
7149
|
+
|
|
7150
|
+
function openCodeSqliteSessionTimestampExpr(sessionColumns) {
|
|
7151
|
+
const candidates = ["time_updated", "time_created"].filter((column) => sessionColumns.has(column)).map((column) => `s.${column}`);
|
|
7152
|
+
if (!candidates.length) return "";
|
|
7153
|
+
return candidates.length === 1 ? candidates[0] : `coalesce(${candidates.join(", ")})`;
|
|
7154
|
+
}
|
|
7155
|
+
|
|
7156
|
+
function openCodeSqliteSinceCondition(timestampExpr, since) {
|
|
7157
|
+
if (!timestampExpr || !since) return "";
|
|
7158
|
+
const sinceTime = since instanceof Date ? since.getTime() : Date.parse(since);
|
|
7159
|
+
if (!Number.isFinite(sinceTime)) return "";
|
|
7160
|
+
const sinceMs = Math.floor(sinceTime);
|
|
7161
|
+
const sinceSeconds = Math.floor(sinceTime / 1000);
|
|
7162
|
+
const sinceIso = new Date(sinceTime).toISOString();
|
|
7163
|
+
return `((${timestampExpr} is not null) and ((abs(${timestampExpr}) > 1000000000000 and ${timestampExpr} >= ${sinceMs}) or (abs(${timestampExpr}) <= 1000000000000 and ${timestampExpr} >= ${sinceSeconds}) or (typeof(${timestampExpr}) = 'text' and datetime(${timestampExpr}) >= datetime(${sqlQuote(sinceIso)}))))`;
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
function readOpenCodeSqliteMessageRows(dbPath, sessionIds = []) {
|
|
6748
7167
|
const columns = sqliteTableColumns(dbPath, "message");
|
|
6749
7168
|
if (!columns.has("id") || !columns.has("session_id")) return [];
|
|
6750
7169
|
const selects = [
|
|
@@ -6754,13 +7173,16 @@ function readOpenCodeSqliteMessageRows(dbPath) {
|
|
|
6754
7173
|
sqliteSelectMaybe(columns, "message", "time_updated"),
|
|
6755
7174
|
sqliteSelectMaybe(columns, "message", "data")
|
|
6756
7175
|
];
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
7176
|
+
return readOpenCodeSqliteRowsForSessionIds(
|
|
7177
|
+
dbPath,
|
|
7178
|
+
"message",
|
|
7179
|
+
selects,
|
|
7180
|
+
sessionIds,
|
|
7181
|
+
"OpenCode SQLite messages"
|
|
7182
|
+
);
|
|
6761
7183
|
}
|
|
6762
7184
|
|
|
6763
|
-
function readOpenCodeSqlitePartRows(dbPath) {
|
|
7185
|
+
function readOpenCodeSqlitePartRows(dbPath, sessionIds = []) {
|
|
6764
7186
|
const columns = sqliteTableColumns(dbPath, "part");
|
|
6765
7187
|
if (!columns.has("id") || !columns.has("message_id") || !columns.has("session_id")) return [];
|
|
6766
7188
|
const selects = [
|
|
@@ -6771,10 +7193,46 @@ function readOpenCodeSqlitePartRows(dbPath) {
|
|
|
6771
7193
|
sqliteSelectMaybe(columns, "part", "time_updated"),
|
|
6772
7194
|
sqliteSelectMaybe(columns, "part", "data")
|
|
6773
7195
|
];
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
7196
|
+
return readOpenCodeSqliteRowsForSessionIds(
|
|
7197
|
+
dbPath,
|
|
7198
|
+
"part",
|
|
7199
|
+
selects,
|
|
7200
|
+
sessionIds,
|
|
7201
|
+
"OpenCode SQLite parts"
|
|
7202
|
+
);
|
|
7203
|
+
}
|
|
7204
|
+
|
|
7205
|
+
function readOpenCodeSqliteRowsForSessionIds(dbPath, tableName, selects, sessionIds, label) {
|
|
7206
|
+
const ids = [...new Set((sessionIds || []).map((id) => String(id || "")).filter(Boolean))];
|
|
7207
|
+
if (!ids.length) return [];
|
|
7208
|
+
const rows = [];
|
|
7209
|
+
for (let index = 0; index < ids.length; index += OPENCODE_SQLITE_BATCH_SIZE) {
|
|
7210
|
+
const batch = ids.slice(index, index + OPENCODE_SQLITE_BATCH_SIZE);
|
|
7211
|
+
const query = [
|
|
7212
|
+
`select ${selects.join(", ")}`,
|
|
7213
|
+
`from ${tableName}`,
|
|
7214
|
+
`where session_id in (${batch.map(sqlQuote).join(",")})`
|
|
7215
|
+
].join(" ");
|
|
7216
|
+
rows.push(...readSqliteJson(dbPath, query, label));
|
|
7217
|
+
}
|
|
7218
|
+
return rows;
|
|
7219
|
+
}
|
|
7220
|
+
|
|
7221
|
+
function sortOpenCodeSqliteRows(rows, keys) {
|
|
7222
|
+
return rows.sort((left, right) => {
|
|
7223
|
+
for (const key of keys) {
|
|
7224
|
+
const result = compareOpenCodeSqliteValues(left?.[key], right?.[key]);
|
|
7225
|
+
if (result) return result;
|
|
7226
|
+
}
|
|
7227
|
+
return 0;
|
|
7228
|
+
});
|
|
7229
|
+
}
|
|
7230
|
+
|
|
7231
|
+
function compareOpenCodeSqliteValues(left, right) {
|
|
7232
|
+
const leftNumber = Number(left);
|
|
7233
|
+
const rightNumber = Number(right);
|
|
7234
|
+
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber) && leftNumber !== rightNumber) return leftNumber - rightNumber;
|
|
7235
|
+
return String(left || "").localeCompare(String(right || ""));
|
|
6778
7236
|
}
|
|
6779
7237
|
|
|
6780
7238
|
function openCodeSqliteMessagesFromRow(row, partRows, index) {
|
|
@@ -6841,19 +7299,20 @@ function openCodeUsageFromMessageData(data, parts = []) {
|
|
|
6841
7299
|
return Object.values(usage).some((value) => value !== undefined) ? usage : null;
|
|
6842
7300
|
}
|
|
6843
7301
|
|
|
6844
|
-
function openCodeSqliteSessionFingerprint(dbPath, row, messageRows, sourceFiles) {
|
|
7302
|
+
function openCodeSqliteSessionFingerprint(dbPath, row, messageRows, sourceFiles, sourceType = "opencode-sqlite-history") {
|
|
6845
7303
|
const sessionRevision = [
|
|
6846
7304
|
row.id,
|
|
6847
7305
|
row.time_updated || row.time_created || "",
|
|
6848
7306
|
messageRows.length,
|
|
6849
7307
|
messageRows.map((message) => `${message.id}:${message.time_updated || message.time_created || ""}`).join("|")
|
|
6850
7308
|
].join(":");
|
|
6851
|
-
return `${fingerprintPrefix(
|
|
7309
|
+
return `${fingerprintPrefix(sourceType)}:${structuredSessionFingerprint({ sourcePath: dbPath, sourceFiles })}:${hashId(sessionRevision)}`;
|
|
6852
7310
|
}
|
|
6853
7311
|
|
|
6854
|
-
function parseOpenCodeSessionFile(file, storageRoot) {
|
|
7312
|
+
function parseOpenCodeSessionFile(file, storageRoot, env = process.env, options = {}) {
|
|
6855
7313
|
const info = readJsonMaybe(file, null);
|
|
6856
7314
|
if (!info || typeof info !== "object") return null;
|
|
7315
|
+
const sourceType = openCodeStorageSourceType(storageRoot, env, options);
|
|
6857
7316
|
const sessionId = firstString(info.id, info.sessionID, info.sessionId, path.basename(file, ".json"));
|
|
6858
7317
|
if (!sessionId) return null;
|
|
6859
7318
|
const projectId = firstString(info.projectID, info.projectId, path.basename(path.dirname(file)));
|
|
@@ -6864,7 +7323,7 @@ function parseOpenCodeSessionFile(file, storageRoot) {
|
|
|
6864
7323
|
const diffMessage = openCodeDiffMessage(diffFile, parsedMessages[parsedMessages.length - 1]?.timestamp || toIso(info.time?.updated || info.updatedAt || info.createdAt));
|
|
6865
7324
|
const messages = stampMessages(
|
|
6866
7325
|
dedupeAdjacentMessages(parsedMessages.concat(diffMessage ? [diffMessage] : [])).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
|
|
6867
|
-
|
|
7326
|
+
sourceType
|
|
6868
7327
|
);
|
|
6869
7328
|
if (!messages.length) return null;
|
|
6870
7329
|
const sourceFiles = [
|
|
@@ -6886,21 +7345,23 @@ function parseOpenCodeSessionFile(file, storageRoot) {
|
|
|
6886
7345
|
messages,
|
|
6887
7346
|
sourcePath: file,
|
|
6888
7347
|
sourceFiles,
|
|
6889
|
-
sourceType
|
|
6890
|
-
fingerprint: `${fingerprintPrefix(
|
|
6891
|
-
detailKey: "sessions"
|
|
7348
|
+
sourceType,
|
|
7349
|
+
fingerprint: `${fingerprintPrefix(sourceType)}:${structuredSessionFingerprint({ sourcePath: file, sourceFiles })}`,
|
|
7350
|
+
detailKey: "sessions",
|
|
7351
|
+
sessionSummary: { source: openCodeSourceKindForType(sourceType) }
|
|
6892
7352
|
};
|
|
6893
7353
|
}
|
|
6894
7354
|
|
|
6895
|
-
function parseOpenCodeMessageOnlySession(storageRoot, sessionId) {
|
|
7355
|
+
function parseOpenCodeMessageOnlySession(storageRoot, sessionId, env = process.env, options = {}) {
|
|
6896
7356
|
if (!sessionId) return null;
|
|
7357
|
+
const sourceType = openCodeStorageSourceType(storageRoot, env, options);
|
|
6897
7358
|
const messageFiles = openCodeMessageFiles(storageRoot, sessionId);
|
|
6898
7359
|
const parsedMessages = messageFiles.flatMap((messageFile, index) => openCodeMessagesFromFile(messageFile, storageRoot, index));
|
|
6899
7360
|
const diffFile = path.join(storageRoot, "session_diff", `${sessionId}.json`);
|
|
6900
7361
|
const diffMessage = openCodeDiffMessage(diffFile, parsedMessages[parsedMessages.length - 1]?.timestamp);
|
|
6901
7362
|
const messages = stampMessages(
|
|
6902
7363
|
dedupeAdjacentMessages(parsedMessages.concat(diffMessage ? [diffMessage] : [])).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp))),
|
|
6903
|
-
|
|
7364
|
+
sourceType
|
|
6904
7365
|
);
|
|
6905
7366
|
if (!messages.length) return null;
|
|
6906
7367
|
const sourceFiles = [
|
|
@@ -6920,9 +7381,10 @@ function parseOpenCodeMessageOnlySession(storageRoot, sessionId) {
|
|
|
6920
7381
|
messages,
|
|
6921
7382
|
sourcePath: path.join(storageRoot, "message", sessionId),
|
|
6922
7383
|
sourceFiles,
|
|
6923
|
-
sourceType
|
|
6924
|
-
fingerprint: `${fingerprintPrefix(
|
|
6925
|
-
detailKey: "sessions"
|
|
7384
|
+
sourceType,
|
|
7385
|
+
fingerprint: `${fingerprintPrefix(sourceType)}:${structuredSessionFingerprint({ sourcePath: path.join(storageRoot, "message", sessionId), sourceFiles })}`,
|
|
7386
|
+
detailKey: "sessions",
|
|
7387
|
+
sessionSummary: { source: openCodeSourceKindForType(sourceType), recoveredFromMessages: true }
|
|
6926
7388
|
};
|
|
6927
7389
|
}
|
|
6928
7390
|
|
|
@@ -7291,7 +7753,12 @@ function dedupeOpenCodeSessions(sessions) {
|
|
|
7291
7753
|
}
|
|
7292
7754
|
|
|
7293
7755
|
function openCodeSourceRank(sourceType) {
|
|
7756
|
+
if (sourceType === "opencode-cli-sqlite-history") return 3;
|
|
7757
|
+
if (sourceType === "opencode-web-sqlite-history") return 3;
|
|
7758
|
+
if (sourceType === "opencode-desktop-sqlite-history") return 3;
|
|
7294
7759
|
if (sourceType === "opencode-sqlite-history") return 3;
|
|
7760
|
+
if (sourceType === "opencode-cli-history") return 2;
|
|
7761
|
+
if (sourceType === "opencode-desktop-history") return 2;
|
|
7295
7762
|
if (sourceType === "opencode-history") return 2;
|
|
7296
7763
|
return 1;
|
|
7297
7764
|
}
|