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/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 fingerprint = `${fingerprintPrefix(sourceType)}:${fileFingerprint(item.file, item.stat)}`;
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 cwd = parsed.cwd || "";
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 ? canonicalRepo(cwd) : null;
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.title
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 = cwd ? canonicalRepo(cwd) : null;
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" && sourceType === "opencode-sqlite-history";
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
- "opencode",
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
- onProgress: (event) => reportDiscoveryProgress(options, { ...event, provider: "OpenCode" })
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: path.join(os.homedir(), "Library", "Application Support", "Claude", "claude-code-sessions"), kind: "claude-code-desktop-metadata" },
3051
- { root: path.join(os.homedir(), "Library", "Application Support", "Claude", "local-agent-mode-sessions"), kind: "claude-workspace-desktop" }
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(row.modelId, row.modelName, row.model);
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
- const dbSessions = readOpenCodeSqliteSessionsFromDb(dbs[index]);
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 openCodeDataRoots(env = process.env) {
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
- const home = env.HOME || os.homedir();
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, ".local", "share", "opencode"),
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 openCodeStorageRoots(env = process.env) {
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(openCodeDataRoots(env).flatMap((root) => [
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 readOpenCodeSqliteSessionsFromDb(dbPath) {
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
- const messageRows = readOpenCodeSqliteMessageRows(dbPath);
6664
- const partRows = readOpenCodeSqlitePartRows(dbPath);
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
- "opencode-sqlite-history"
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], "opencode-sqlite-history")) : messages;
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: "opencode-sqlite-history",
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
- if (sessionColumns.has("time_archived")) queryParts.push("where coalesce(s.time_archived, 0) = 0");
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 readOpenCodeSqliteMessageRows(dbPath) {
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
- const orderColumns = ["session_id"];
6758
- if (columns.has("time_created")) orderColumns.push("time_created");
6759
- orderColumns.push("id");
6760
- return readSqliteJson(dbPath, `select ${selects.join(", ")} from message order by ${orderColumns.join(", ")}`, "OpenCode SQLite messages");
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
- const orderColumns = ["session_id", "message_id"];
6775
- if (columns.has("time_created")) orderColumns.push("time_created");
6776
- orderColumns.push("id");
6777
- return readSqliteJson(dbPath, `select ${selects.join(", ")} from part order by ${orderColumns.join(", ")}`, "OpenCode SQLite parts");
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("opencode-sqlite-history")}:${structuredSessionFingerprint({ sourcePath: dbPath, sourceFiles })}:${hashId(sessionRevision)}`;
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
- "opencode-history"
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: "opencode-history",
6890
- fingerprint: `${fingerprintPrefix("opencode-history")}:${structuredSessionFingerprint({ sourcePath: file, sourceFiles })}`,
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
- "opencode-history"
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: "opencode-history",
6924
- fingerprint: `${fingerprintPrefix("opencode-history")}:${structuredSessionFingerprint({ sourcePath: path.join(storageRoot, "message", sessionId), sourceFiles })}`,
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
  }