vibe-replay 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -40,8 +40,15 @@ function normalizeTitle(value) {
40
40
  return cleaned || void 0;
41
41
  }
42
42
  function extractToolFilePath(input) {
43
- const fp = input?.file_path ?? input?.filePath ?? input?.path ?? input?.relativeWorkspacePath;
44
- return typeof fp === "string" && fp.trim() ? fp : void 0;
43
+ return extractToolFilePaths(input)[0];
44
+ }
45
+ function extractToolFilePaths(input) {
46
+ if (!input) return [];
47
+ const plural2 = input.file_paths ?? input.filePaths ?? input.paths;
48
+ const paths = Array.isArray(plural2) ? plural2.filter((fp) => typeof fp === "string" && fp.trim().length > 0) : [];
49
+ const singular = input.file_path ?? input.filePath ?? input.path ?? input.relativeWorkspacePath;
50
+ if (typeof singular === "string" && singular.trim()) paths.unshift(singular);
51
+ return [...new Set(paths)];
45
52
  }
46
53
  function localDayKey(input) {
47
54
  if (input == null || input === "") return void 0;
@@ -68,13 +75,13 @@ var init_utils = __esm({
68
75
  });
69
76
 
70
77
  // src/overlays.ts
71
- import { readFile as readFile12 } from "fs/promises";
72
- import { join as join12, resolve } from "path";
78
+ import { readFile as readFile14 } from "fs/promises";
79
+ import { join as join13, resolve } from "path";
73
80
  async function loadOverlays(baseDir, slug) {
74
- const dirs = [join12(baseDir, slug), resolve("./vibe-replay", slug)];
81
+ const dirs = [join13(baseDir, slug), resolve("./vibe-replay", slug)];
75
82
  for (const dir of dirs) {
76
83
  try {
77
- const raw = await readFile12(join12(dir, "overlays.json"), "utf-8");
84
+ const raw = await readFile14(join13(dir, "overlays.json"), "utf-8");
78
85
  const parsed = JSON.parse(raw);
79
86
  if (parsed && typeof parsed === "object" && Array.isArray(parsed.overlays)) {
80
87
  return parsed;
@@ -133,9 +140,9 @@ __export(cloud_exports, {
133
140
  saveAuthTokenSync: () => saveAuthTokenSync
134
141
  });
135
142
  import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
136
- import { mkdir as mkdir3, readFile as readFile13, unlink, writeFile as writeFile3 } from "fs/promises";
137
- import { homedir as homedir11 } from "os";
138
- import { basename as basename5, dirname as dirname3, join as join13 } from "path";
143
+ import { mkdir as mkdir3, readFile as readFile15, unlink, writeFile as writeFile3 } from "fs/promises";
144
+ import { homedir as homedir12 } from "os";
145
+ import { basename as basename5, dirname as dirname3, join as join14 } from "path";
139
146
  function authOrigin(url) {
140
147
  try {
141
148
  return new URL(url).origin;
@@ -225,7 +232,7 @@ function getAuthFilePath() {
225
232
  return AUTH_FILE;
226
233
  }
227
234
  function getApiUrl() {
228
- return process.env.VIBE_REPLAY_API_URL || DEFAULT_API_URL;
235
+ return (process.env.VIBE_REPLAY_API_URL || DEFAULT_API_URL).replace(/\/$/, "");
229
236
  }
230
237
  function getSessionCookieName(apiUrl) {
231
238
  const url = apiUrl || getApiUrl();
@@ -236,8 +243,8 @@ async function publishCloud(outputDir, opts) {
236
243
  if (!auth) {
237
244
  throw new Error("Not logged in. Run `vibe-replay auth login` first.");
238
245
  }
239
- const jsonPath = join13(outputDir, "replay.json");
240
- const content = await readFile13(jsonPath, "utf-8");
246
+ const jsonPath = join14(outputDir, "replay.json");
247
+ const content = await readFile15(jsonPath, "utf-8");
241
248
  const replay = JSON.parse(content);
242
249
  const sizeBytes = Buffer.byteLength(content, "utf-8");
243
250
  const MAX_SIZE = 10 * 1024 * 1024;
@@ -284,8 +291,8 @@ async function publishCloudWithOverlays(outputDir, opts) {
284
291
  if (overlays.overlays.length === 0) {
285
292
  return publishCloud(outputDir, opts);
286
293
  }
287
- const replayPath = join13(outputDir, "replay.json");
288
- const originalContent = await readFile13(replayPath, "utf-8");
294
+ const replayPath = join14(outputDir, "replay.json");
295
+ const originalContent = await readFile15(replayPath, "utf-8");
289
296
  const session = JSON.parse(originalContent);
290
297
  const merged = sessionWithEffectiveContent(session, overlays);
291
298
  await writeFile3(replayPath, JSON.stringify(merged), "utf-8");
@@ -297,7 +304,7 @@ async function publishCloudWithOverlays(outputDir, opts) {
297
304
  }
298
305
  async function loadSavedCloudInfo(outputDir) {
299
306
  try {
300
- const raw = await readFile13(join13(outputDir, CLOUD_META_FILE), "utf-8");
307
+ const raw = await readFile15(join14(outputDir, CLOUD_META_FILE), "utf-8");
301
308
  const parsed = JSON.parse(raw);
302
309
  if (!parsed?.id || !parsed?.url) return void 0;
303
310
  return parsed;
@@ -306,7 +313,7 @@ async function loadSavedCloudInfo(outputDir) {
306
313
  }
307
314
  }
308
315
  async function saveCloudInfo(outputDir, info) {
309
- await writeFile3(join13(outputDir, CLOUD_META_FILE), JSON.stringify(info, null, 2), "utf-8");
316
+ await writeFile3(join14(outputDir, CLOUD_META_FILE), JSON.stringify(info, null, 2), "utf-8");
310
317
  }
311
318
  var CLOUD_META_FILE, DEFAULT_API_URL, AUTH_DIR, AUTH_FILE;
312
319
  var init_cloud = __esm({
@@ -315,8 +322,8 @@ var init_cloud = __esm({
315
322
  init_overlays();
316
323
  CLOUD_META_FILE = ".vibe-replay-cloud.json";
317
324
  DEFAULT_API_URL = "https://vibe-replay.com";
318
- AUTH_DIR = join13(homedir11(), ".config", "vibe-replay");
319
- AUTH_FILE = join13(AUTH_DIR, "auth.json");
325
+ AUTH_DIR = join14(homedir12(), ".config", "vibe-replay");
326
+ AUTH_FILE = join14(AUTH_DIR, "auth.json");
320
327
  }
321
328
  });
322
329
 
@@ -399,12 +406,12 @@ __export(insights_exports, {
399
406
  scanResultToInsight: () => scanResultToInsight,
400
407
  writeInsightsStore: () => writeInsightsStore
401
408
  });
402
- import { mkdir as mkdir4, readFile as readFile15, rename, writeFile as writeFile5 } from "fs/promises";
403
- import { homedir as homedir12 } from "os";
404
- import { dirname as dirname4, join as join15 } from "path";
409
+ import { mkdir as mkdir4, readFile as readFile17, rename, writeFile as writeFile5 } from "fs/promises";
410
+ import { homedir as homedir13 } from "os";
411
+ import { dirname as dirname4, join as join16 } from "path";
405
412
  async function readInsightsStore() {
406
413
  try {
407
- const raw = await readFile15(STORE_PATH, "utf-8");
414
+ const raw = await readFile17(STORE_PATH, "utf-8");
408
415
  const parsed = JSON.parse(raw);
409
416
  return migrateInsightsStore(parsed);
410
417
  } catch {
@@ -600,8 +607,8 @@ var init_insights = __esm({
600
607
  init_machine_id();
601
608
  init_utils();
602
609
  init_version();
603
- INSIGHTS_DIR = join15(homedir12(), ".vibe-replay", "insights");
604
- STORE_PATH = join15(INSIGHTS_DIR, "store.json");
610
+ INSIGHTS_DIR = join16(homedir13(), ".vibe-replay", "insights");
611
+ STORE_PATH = join16(INSIGHTS_DIR, "store.json");
605
612
  }
606
613
  });
607
614
 
@@ -834,7 +841,7 @@ function generateGitHubMarkdown(session, opts = {}) {
834
841
  }
835
842
  lines.push("");
836
843
  }
837
- const risks = detectRiskSignals(session.scenes, filesChanged);
844
+ const risks = detectRiskSignals(session, filesChanged);
838
845
  if (risks.length > 0) {
839
846
  lines.push("**Signals:**");
840
847
  for (const risk of risks) {
@@ -967,9 +974,9 @@ function parseToolCall(scene) {
967
974
  case "View":
968
975
  return { kind: "read", file: baseName(input.file_path || input.path || "") };
969
976
  case "Edit":
970
- return { kind: "edit", file: baseName(input.file_path || "") };
977
+ return { kind: "edit", file: baseName(firstFilePath(input) || "") };
971
978
  case "Write":
972
- return { kind: "create", file: baseName(input.file_path || "") };
979
+ return { kind: "create", file: baseName(firstFilePath(input) || "") };
973
980
  case "Bash": {
974
981
  const cmd = (input.command || "").split("\n")[0];
975
982
  const lower = result.toLowerCase();
@@ -1040,8 +1047,9 @@ function groupActions(raw) {
1040
1047
  flushReads();
1041
1048
  return grouped;
1042
1049
  }
1043
- function detectRiskSignals(scenes, filesChanged) {
1050
+ function detectRiskSignals(session, filesChanged) {
1044
1051
  const risks = [];
1052
+ const { scenes } = session;
1045
1053
  for (const [file, count] of filesChanged) {
1046
1054
  if (count >= 4) {
1047
1055
  risks.push(`\`${file}\` modified ${count}x`);
@@ -1066,7 +1074,7 @@ function detectRiskSignals(scenes, filesChanged) {
1066
1074
  const resolved = testPasses > 0 ? " (resolved)" : "";
1067
1075
  risks.push(`${testFailures} test failure(s)${resolved}`);
1068
1076
  }
1069
- if (scenes.some((s) => s.type === "compaction-summary")) {
1077
+ if (scenes.some((s) => s.type === "compaction-summary") || (session.meta.compactions?.length || 0) > 0) {
1070
1078
  risks.push("Session was compacted (very long conversation)");
1071
1079
  }
1072
1080
  return risks;
@@ -1180,6 +1188,17 @@ function stripMarkdown(s) {
1180
1188
  function selectKeyPhases(phases, max) {
1181
1189
  return phases.slice(0, max);
1182
1190
  }
1191
+ function buildSvgFooter(session, opts) {
1192
+ const duration = formatDuration(session.meta.stats.durationMs);
1193
+ const tStats = computeToolStats(session.scenes);
1194
+ const parts = [duration];
1195
+ if (tStats.responses > 0) parts.push(`${tStats.responses} responses`);
1196
+ if (tStats.totalTools > 0) parts.push(`${tStats.totalTools} tools`);
1197
+ return {
1198
+ footerLeft: parts.filter(Boolean).join(" \xB7 "),
1199
+ footerRight: opts.replayUrl ? "View full replay \u2192" : "vibe-replay.com"
1200
+ };
1201
+ }
1183
1202
  function renderSvg(frames, session, opts = {}) {
1184
1203
  const N = frames.length;
1185
1204
  const secPerFrame = 5;
@@ -1193,13 +1212,7 @@ function renderSvg(frames, session, opts = {}) {
1193
1212
  const fill = i === 0 ? " animation-fill-mode: backwards;" : "";
1194
1213
  return ` .frame-${i} { animation: fadeInOut ${cycleDuration}s ${delay}s infinite;${fill} }`;
1195
1214
  }).join("\n");
1196
- const duration = formatDuration(session.meta.stats.durationMs);
1197
- const tStats = computeToolStats(session.scenes);
1198
- const footerParts = [duration];
1199
- if (tStats.responses > 0) footerParts.push(`${tStats.responses} responses`);
1200
- if (tStats.totalTools > 0) footerParts.push(`${tStats.totalTools} tools`);
1201
- const footerLeft = footerParts.filter(Boolean).join(" \xB7 ");
1202
- const footerRight = opts.replayUrl ? "View full replay \u2192" : "vibe-replay.com";
1215
+ const { footerLeft, footerRight } = buildSvgFooter(session, opts);
1203
1216
  const framesSvg = frames.map((f, i) => renderFrame(f, i)).join("\n\n");
1204
1217
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${SVG_W}" height="${SVG_H}" viewBox="0 0 ${SVG_W} ${SVG_H}">
1205
1218
  <style>
@@ -1383,13 +1396,7 @@ function renderTurnFrame(frame, _index, gAttr) {
1383
1396
  return parts.join("\n");
1384
1397
  }
1385
1398
  function renderStaticFrameSvg(frame, session, opts = {}) {
1386
- const duration = formatDuration(session.meta.stats.durationMs);
1387
- const tStats = computeToolStats(session.scenes);
1388
- const footerParts = [duration];
1389
- if (tStats.responses > 0) footerParts.push(`${tStats.responses} responses`);
1390
- if (tStats.totalTools > 0) footerParts.push(`${tStats.totalTools} tools`);
1391
- const footerLeft = footerParts.filter(Boolean).join(" \xB7 ");
1392
- const footerRight = opts.replayUrl ? "View full replay \u2192" : "vibe-replay.com";
1399
+ const { footerLeft, footerRight } = buildSvgFooter(session, opts);
1393
1400
  let frameContent;
1394
1401
  if (frame.userPrompt != null) {
1395
1402
  frameContent = renderTurnFrame(frame, 0, "");
@@ -1426,8 +1433,7 @@ function collectFilesChanged(scenes) {
1426
1433
  for (const scene of scenes) {
1427
1434
  if (scene.type !== "tool-call") continue;
1428
1435
  if (scene.toolName === "Edit" || scene.toolName === "Write") {
1429
- const fp = scene.input.file_path;
1430
- if (fp) {
1436
+ for (const fp of filePathsFromInput(scene.input)) {
1431
1437
  const name = shortPath(fp);
1432
1438
  files.set(name, (files.get(name) || 0) + 1);
1433
1439
  }
@@ -1435,6 +1441,16 @@ function collectFilesChanged(scenes) {
1435
1441
  }
1436
1442
  return files;
1437
1443
  }
1444
+ function firstFilePath(input) {
1445
+ return filePathsFromInput(input)[0];
1446
+ }
1447
+ function filePathsFromInput(input) {
1448
+ const plural2 = input.file_paths || input.filePaths || input.paths;
1449
+ const paths = Array.isArray(plural2) ? plural2.filter((fp) => typeof fp === "string" && fp.trim().length > 0) : [];
1450
+ const singular = input.file_path || input.filePath || input.path || input.relativeWorkspacePath;
1451
+ if (typeof singular === "string" && singular.trim()) paths.unshift(singular);
1452
+ return [...new Set(paths)];
1453
+ }
1438
1454
  function shortPath(fullPath) {
1439
1455
  const parts = fullPath.split("/");
1440
1456
  if (parts.length <= 2) return parts.join("/");
@@ -1852,13 +1868,37 @@ function estimateActiveDuration(timestamps, maxGapMs = DEFAULT_MAX_GAP_MS) {
1852
1868
  return active > 0 ? active : void 0;
1853
1869
  }
1854
1870
 
1871
+ // src/providers/warnings.ts
1872
+ function addParseWarning(warnings, warning) {
1873
+ const existing = warnings.find(
1874
+ (w) => w.kind === warning.kind && w.source === warning.source && w.message === warning.message
1875
+ );
1876
+ if (existing) {
1877
+ existing.count += warning.count ?? 1;
1878
+ return;
1879
+ }
1880
+ warnings.push({
1881
+ kind: warning.kind,
1882
+ count: warning.count ?? 1,
1883
+ message: warning.message,
1884
+ source: warning.source,
1885
+ firstLine: warning.firstLine,
1886
+ sample: warning.kind === "malformed-json" ? void 0 : warning.sample
1887
+ });
1888
+ }
1889
+ function compactWarningSample(value, max = 160) {
1890
+ const compacted = value.replace(/\s+/g, " ").trim();
1891
+ if (compacted.length <= max) return compacted;
1892
+ return `${compacted.slice(0, max)}...`;
1893
+ }
1894
+
1855
1895
  // src/providers/claude-code/parser.ts
1856
1896
  async function parseClaudeCodeSession(filePaths) {
1857
1897
  const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
1858
1898
  const allLines = [];
1859
1899
  for (const fp of paths) {
1860
1900
  const content = await readFile4(fp, "utf-8");
1861
- allLines.push(...content.split("\n").filter((l) => l.trim()));
1901
+ allLines.push(...content.split("\n"));
1862
1902
  }
1863
1903
  return parseClaudeCodeLines(allLines, { subagentsSourcePath: paths[0] });
1864
1904
  }
@@ -1902,11 +1942,21 @@ async function parseClaudeCodeLines(lines, options = {}) {
1902
1942
  const toolImages = /* @__PURE__ */ new Map();
1903
1943
  const userTurns = [];
1904
1944
  const allTimestamps = [];
1905
- for (const line of lines) {
1945
+ const parseWarnings = [];
1946
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
1947
+ const line = lines[lineIndex];
1948
+ if (!line.trim()) continue;
1906
1949
  let obj;
1907
1950
  try {
1908
1951
  obj = JSON.parse(line);
1909
1952
  } catch {
1953
+ addParseWarning(parseWarnings, {
1954
+ kind: "malformed-json",
1955
+ source: "claude-code JSONL",
1956
+ firstLine: lineIndex + 1,
1957
+ message: "Skipped malformed JSONL line",
1958
+ sample: line
1959
+ });
1910
1960
  continue;
1911
1961
  }
1912
1962
  if (obj.timestamp) allTimestamps.push(obj.timestamp);
@@ -2013,6 +2063,14 @@ async function parseClaudeCodeLines(lines, options = {}) {
2013
2063
  }
2014
2064
  if (!obj.message) continue;
2015
2065
  const { role, content: msgContent, id: msgId } = obj.message;
2066
+ if (obj.isApiErrorMessage && obj.timestamp) {
2067
+ const text = extractMessageText(msgContent);
2068
+ const errorType = inferApiErrorMessageType(text);
2069
+ apiErrors.push({
2070
+ timestamp: obj.timestamp,
2071
+ ...errorType ? { errorType } : {}
2072
+ });
2073
+ }
2016
2074
  if (obj.isMeta && role === "user") {
2017
2075
  const text = extractMessageText(msgContent);
2018
2076
  if (text.trim()) {
@@ -2123,7 +2181,7 @@ async function parseClaudeCodeLines(lines, options = {}) {
2123
2181
  }
2124
2182
  }
2125
2183
  }
2126
- const subAgentData = options.subagentsSourcePath ? await readSubagents(options.subagentsSourcePath, usageByMsgId) : /* @__PURE__ */ new Map();
2184
+ const subAgentData = options.subagentsSourcePath ? await readSubagents(options.subagentsSourcePath, usageByMsgId, parseWarnings) : /* @__PURE__ */ new Map();
2127
2185
  const assistantTurns = [];
2128
2186
  for (const msgId of assistantOrder) {
2129
2187
  const blocks = assistantBlocks.get(msgId);
@@ -2259,7 +2317,8 @@ async function parseClaudeCodeLines(lines, options = {}) {
2259
2317
  mcpServersUsed: mcpServersUsed.size > 0 ? [...mcpServersUsed].sort() : void 0,
2260
2318
  agentName,
2261
2319
  worktree,
2262
- queueOperationStats
2320
+ queueOperationStats,
2321
+ parseWarnings: parseWarnings.length > 0 ? parseWarnings : void 0
2263
2322
  };
2264
2323
  }
2265
2324
  function extractMessageText(content) {
@@ -2269,6 +2328,13 @@ function extractMessageText(content) {
2269
2328
  }
2270
2329
  return "";
2271
2330
  }
2331
+ function inferApiErrorMessageType(text) {
2332
+ const lower = text.toLowerCase();
2333
+ if (lower.includes("rate limit")) return "rate_limit_error";
2334
+ if (lower.includes("overloaded")) return "overloaded_error";
2335
+ if (lower.includes("api error")) return "api_error";
2336
+ return void 0;
2337
+ }
2272
2338
  function extractImages(block) {
2273
2339
  if (block.type !== "tool_result") return [];
2274
2340
  const { content } = block;
@@ -2367,16 +2433,16 @@ function buildTurnStats(finalTurns, usageByMsgId, turnDurations) {
2367
2433
  } else if (group.startTimestamp && group.endTimestamp) {
2368
2434
  durationMs = estimateActiveDuration([group.startTimestamp, group.endTimestamp]);
2369
2435
  }
2370
- const stat10 = { turnIndex: i };
2371
- if (turnModel) stat10.model = turnModel;
2372
- if (durationMs !== void 0) stat10.durationMs = durationMs;
2373
- if (turnTokens) stat10.tokenUsage = turnTokens;
2374
- if (maxContextTokens > 0) stat10.contextTokens = maxContextTokens;
2375
- stats.push(stat10);
2436
+ const stat11 = { turnIndex: i };
2437
+ if (turnModel) stat11.model = turnModel;
2438
+ if (durationMs !== void 0) stat11.durationMs = durationMs;
2439
+ if (turnTokens) stat11.tokenUsage = turnTokens;
2440
+ if (maxContextTokens > 0) stat11.contextTokens = maxContextTokens;
2441
+ stats.push(stat11);
2376
2442
  }
2377
2443
  return stats;
2378
2444
  }
2379
- async function readSubagents(mainFilePath, usageByMsgId) {
2445
+ async function readSubagents(mainFilePath, usageByMsgId, parseWarnings) {
2380
2446
  const result = /* @__PURE__ */ new Map();
2381
2447
  const sessionDir = mainFilePath.replace(/\.jsonl$/, "");
2382
2448
  const subagentsDir = join5(sessionDir, "subagents");
@@ -2418,12 +2484,21 @@ async function readSubagents(mainFilePath, usageByMsgId) {
2418
2484
  const saAssistantBlocks = /* @__PURE__ */ new Map();
2419
2485
  const saAssistantOrder = [];
2420
2486
  const saAssistantTimestamps = /* @__PURE__ */ new Map();
2487
+ let lineNumber = 0;
2421
2488
  for (const line of content.split("\n")) {
2489
+ lineNumber++;
2422
2490
  if (!line.trim()) continue;
2423
2491
  let obj;
2424
2492
  try {
2425
2493
  obj = JSON.parse(line);
2426
2494
  } catch {
2495
+ addParseWarning(parseWarnings, {
2496
+ kind: "malformed-json",
2497
+ source: "claude-code subagent JSONL",
2498
+ firstLine: lineNumber,
2499
+ message: "Skipped malformed subagent JSONL line",
2500
+ sample: line
2501
+ });
2427
2502
  continue;
2428
2503
  }
2429
2504
  const msg = obj?.message;
@@ -2641,6 +2716,7 @@ async function extractCoworkSessionInfo(jsonPath) {
2641
2716
  const timestamp = meta.lastActivityAt ? new Date(meta.lastActivityAt).toISOString() : meta.createdAt ? new Date(meta.createdAt).toISOString() : auditStat.mtime.toISOString();
2642
2717
  const fileSize = auditStat.size;
2643
2718
  const model = meta.model ? meta.model.replace(/\[[^\]]*\]$/, "") : void 0;
2719
+ const fsDetectedFiles = Array.isArray(meta.fsDetectedFiles) ? meta.fsDetectedFiles.filter((file) => typeof file === "string") : void 0;
2644
2720
  const project = "Cowork";
2645
2721
  return {
2646
2722
  provider: "claude-cowork",
@@ -2659,7 +2735,13 @@ async function extractCoworkSessionInfo(jsonPath) {
2659
2735
  prompts,
2660
2736
  promptCount,
2661
2737
  toolCallCount,
2662
- model
2738
+ model,
2739
+ isStarred: meta.isStarred,
2740
+ spaceId: meta.spaceId,
2741
+ spaceIdSetBy: meta.spaceIdSetBy,
2742
+ pluginsEnabled: meta.pluginsEnabled,
2743
+ skillsEnabled: meta.skillsEnabled,
2744
+ fsDetectedFiles
2663
2745
  };
2664
2746
  }
2665
2747
  function collectPromptsFromLines(lines, initialMessage) {
@@ -2694,11 +2776,12 @@ function collectPromptsFromLines(lines, initialMessage) {
2694
2776
 
2695
2777
  // src/providers/claude-cowork/parser.ts
2696
2778
  import { readFile as readFile6 } from "fs/promises";
2697
- function normalizeCoworkLine(line) {
2779
+ function normalizeCoworkLine(line, onMalformedJson) {
2698
2780
  let obj;
2699
2781
  try {
2700
2782
  obj = JSON.parse(line);
2701
2783
  } catch {
2784
+ onMalformedJson?.();
2702
2785
  return null;
2703
2786
  }
2704
2787
  if (!obj || typeof obj !== "object") return null;
@@ -2718,16 +2801,30 @@ function normalizeCoworkLine(line) {
2718
2801
  async function parseClaudeCoworkSession(filePaths, sessionInfo) {
2719
2802
  const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
2720
2803
  const normalized = [];
2804
+ const parseWarnings = [];
2721
2805
  for (const fp of paths) {
2722
2806
  const content = await readFile6(fp, "utf-8");
2723
- for (const raw of content.split("\n")) {
2807
+ const lines = content.split("\n");
2808
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
2809
+ const raw = lines[lineIndex];
2724
2810
  const t = raw.trim();
2725
2811
  if (!t) continue;
2726
- const line = normalizeCoworkLine(t);
2812
+ const line = normalizeCoworkLine(t, () => {
2813
+ addParseWarning(parseWarnings, {
2814
+ kind: "malformed-json",
2815
+ source: "claude-cowork audit JSONL",
2816
+ firstLine: lineIndex + 1,
2817
+ message: "Skipped malformed JSONL line",
2818
+ sample: t
2819
+ });
2820
+ });
2727
2821
  if (line) normalized.push(line);
2728
2822
  }
2729
2823
  }
2730
2824
  const result = await parseClaudeCodeLines(normalized);
2825
+ if (parseWarnings.length > 0) {
2826
+ result.parseWarnings = [...parseWarnings, ...result.parseWarnings || []];
2827
+ }
2731
2828
  if (sessionInfo) {
2732
2829
  if (!result.title && sessionInfo.title) result.title = sessionInfo.title;
2733
2830
  if (!result.model && sessionInfo.model) result.model = sessionInfo.model;
@@ -2833,7 +2930,7 @@ var claudeDesktopProvider = {
2833
2930
 
2834
2931
  // src/providers/codex/discover.ts
2835
2932
  import { createReadStream as createReadStream3 } from "fs";
2836
- import { readdir as readdir5, stat as stat4 } from "fs/promises";
2933
+ import { readFile as readFile8, readdir as readdir5, stat as stat4 } from "fs/promises";
2837
2934
  import { homedir as homedir7 } from "os";
2838
2935
  import { basename, join as join8 } from "path";
2839
2936
  import { createInterface as createInterface3 } from "readline";
@@ -2841,6 +2938,47 @@ init_utils();
2841
2938
 
2842
2939
  // src/providers/codex/constants.ts
2843
2940
  var USER_MESSAGE_BEGIN = "## My request for Codex:";
2941
+ var CODEX_CONTEXT_TAGS = [
2942
+ "environment_context",
2943
+ "permissions instructions",
2944
+ "app-context",
2945
+ "collaboration_mode",
2946
+ "apps_instructions",
2947
+ "skills_instructions",
2948
+ "plugins_instructions"
2949
+ ];
2950
+ function isCodexToolCallType(type) {
2951
+ return type === "function_call" || type === "local_shell_call" || type === "custom_tool_call" || type === "tool_search_call" || type === "web_search_call" || type === "image_generation_call";
2952
+ }
2953
+ function stripUserMessagePrefix(text) {
2954
+ const idx = text.indexOf(USER_MESSAGE_BEGIN);
2955
+ return idx === -1 ? text : text.slice(idx + USER_MESSAGE_BEGIN.length);
2956
+ }
2957
+ function stripLeadingCodexContextBlocks(text) {
2958
+ let remaining = text.trim();
2959
+ let stripped = true;
2960
+ while (stripped) {
2961
+ stripped = false;
2962
+ for (const tag of CODEX_CONTEXT_TAGS) {
2963
+ const open3 = `<${tag}>`;
2964
+ const close = `</${tag}>`;
2965
+ if (!remaining.startsWith(open3)) continue;
2966
+ const closeIndex = remaining.indexOf(close);
2967
+ if (closeIndex === -1) return "";
2968
+ remaining = remaining.slice(closeIndex + close.length).trim();
2969
+ stripped = true;
2970
+ break;
2971
+ }
2972
+ }
2973
+ return remaining;
2974
+ }
2975
+ function codexStripTwoPass(text) {
2976
+ let result = text;
2977
+ for (let i = 0; i < 2; i++) {
2978
+ result = stripUserMessagePrefix(stripLeadingCodexContextBlocks(result)).trim();
2979
+ }
2980
+ return result;
2981
+ }
2844
2982
 
2845
2983
  // src/providers/codex/discover.ts
2846
2984
  var STATE_DB_FILENAME = "state_5.sqlite";
@@ -2864,7 +3002,8 @@ async function sessionInfoFromThreadRow(row) {
2864
3002
  const fileStat = await stat4(row.rollout_path).catch(() => null);
2865
3003
  if (!fileStat?.isFile()) return null;
2866
3004
  const extracted = await extractCodexSessionInfo(row.rollout_path, fileStat.size);
2867
- const firstPrompt = cleanCodexUserMessage(row.first_user_message || extracted?.firstPrompt || "");
3005
+ const rowFirstPrompt = row.first_user_message ? normalizeDiscoveredUserMessage(row.first_user_message) : "";
3006
+ const firstPrompt = rowFirstPrompt || extracted?.firstPrompt || "";
2868
3007
  if (!firstPrompt) return extracted;
2869
3008
  return {
2870
3009
  provider: "codex",
@@ -2904,6 +3043,7 @@ async function extractCodexSessionInfo(filePath, fileSize) {
2904
3043
  let editCountEst = 0;
2905
3044
  let durationMsEst = 0;
2906
3045
  const prompts = [];
3046
+ const promptSeen = /* @__PURE__ */ new Map();
2907
3047
  let rl;
2908
3048
  try {
2909
3049
  const input = createReadStream3(filePath, { encoding: "utf-8" });
@@ -2937,13 +3077,11 @@ async function extractCodexSessionInfo(filePath, fileSize) {
2937
3077
  const p = obj.payload || {};
2938
3078
  if (p.type === "thread_name_updated" && p.thread_name) title = p.thread_name;
2939
3079
  if (p.type === "user_message") {
2940
- const cleaned = typeof p.message === "string" ? cleanCodexUserMessage(p.message) : "";
2941
- const hasImages = hasUserImages(p);
2942
- if (cleaned.length >= 10 || hasImages) {
3080
+ const rawText = typeof p.message === "string" ? p.message : "";
3081
+ const cleaned = normalizeDiscoveredUserMessage(rawText);
3082
+ const imageKey = userImageDedupeKey(p);
3083
+ if (recordDiscoveredPrompt(promptSeen, prompts, obj.timestamp, cleaned, imageKey)) {
2943
3084
  promptCount++;
2944
- if (prompts.length < 2) {
2945
- prompts.push((cleaned || "[Image]").slice(0, 200));
2946
- }
2947
3085
  }
2948
3086
  }
2949
3087
  if (p.type === "exec_command_end" && typeof p.duration?.secs === "number") {
@@ -2953,7 +3091,15 @@ async function extractCodexSessionInfo(filePath, fileSize) {
2953
3091
  }
2954
3092
  if (obj.type === "response_item") {
2955
3093
  const p = obj.payload || {};
2956
- if (p.type === "function_call") {
3094
+ if (p.type === "message" && p.role === "user") {
3095
+ const rawText = contentText(p.content);
3096
+ const cleaned = normalizeDiscoveredUserMessage(rawText);
3097
+ const imageKey = contentImageDedupeKey(p.content);
3098
+ if (recordDiscoveredPrompt(promptSeen, prompts, obj.timestamp, cleaned, imageKey)) {
3099
+ promptCount++;
3100
+ }
3101
+ }
3102
+ if (isCodexToolCallType(p.type)) {
2957
3103
  toolCallCount++;
2958
3104
  if (isEditTool(p.name)) editCountEst++;
2959
3105
  }
@@ -2999,7 +3145,7 @@ async function readThreadRowsFromStateDb() {
2999
3145
  try {
3000
3146
  const mod = await import("sql.js");
3001
3147
  const SQL = await mod.default();
3002
- const dbBuffer = await import("fs/promises").then((fs) => fs.readFile(stateDbPath));
3148
+ const dbBuffer = await readFile8(stateDbPath);
3003
3149
  const db = new SQL.Database(dbBuffer);
3004
3150
  try {
3005
3151
  const result = db.exec(`
@@ -3062,28 +3208,74 @@ function toIsoFlexible(value) {
3062
3208
  function isEditTool(name) {
3063
3209
  return !!name && ["apply_patch", "edit", "write_file"].includes(name);
3064
3210
  }
3065
- function cleanCodexUserMessage(text) {
3066
- const withoutPrefix = text.includes(USER_MESSAGE_BEGIN) ? text.slice(text.indexOf(USER_MESSAGE_BEGIN) + USER_MESSAGE_BEGIN.length) : text;
3067
- return cleanPromptText(withoutPrefix);
3211
+ function normalizeDiscoveredUserMessage(text) {
3212
+ return cleanPromptText(codexStripTwoPass(text));
3213
+ }
3214
+ function recordDiscoveredPrompt(promptSeen, prompts, timestamp, text, imageKey) {
3215
+ const hasImages = imageKey.length > 0;
3216
+ if (isCodexContextMessage(text)) return false;
3217
+ if (text.length < 10 && !hasImages) return false;
3218
+ const key = `${text}:images:${imageKey}`;
3219
+ const time = timestamp ? Date.parse(timestamp) : Number.NaN;
3220
+ const previous = promptSeen.get(key) || [];
3221
+ const isDuplicate = Number.isNaN(time) ? previous.length > 0 : previous.some((prev) => Math.abs(time - prev) <= 2e3);
3222
+ if (isDuplicate) return false;
3223
+ promptSeen.set(key, [...previous, Number.isNaN(time) ? Number.NEGATIVE_INFINITY : time]);
3224
+ if (prompts.length < 2) prompts.push((text || "[Image]").slice(0, 200));
3225
+ return true;
3068
3226
  }
3069
- function hasUserImages(payload) {
3070
- return Array.isArray(payload.images) && payload.images.length > 0 || Array.isArray(payload.local_images) && payload.local_images.length > 0;
3227
+ function contentText(content) {
3228
+ if (typeof content === "string") return content;
3229
+ if (!Array.isArray(content)) return "";
3230
+ return content.map((part) => {
3231
+ if (typeof part === "string") return part;
3232
+ if (part?.type === "output_text" || part?.type === "input_text" || part?.type === "text") {
3233
+ return part.text || "";
3234
+ }
3235
+ return "";
3236
+ }).filter(Boolean).join("\n");
3237
+ }
3238
+ function isCodexContextMessage(text) {
3239
+ const trimmed = text.trim();
3240
+ return CODEX_CONTEXT_TAGS.some((tag) => trimmed.startsWith(`<${tag}>`));
3241
+ }
3242
+ function userImageDedupeKey(payload) {
3243
+ return [
3244
+ ...Array.isArray(payload.images) ? payload.images : [],
3245
+ ...Array.isArray(payload.local_images) ? payload.local_images : []
3246
+ ].filter((image) => typeof image === "string" && image.length > 0).map(imageDedupeKey).join(",");
3247
+ }
3248
+ function contentImageDedupeKey(content) {
3249
+ if (!Array.isArray(content)) return "";
3250
+ return content.flatMap((part) => {
3251
+ if (part?.type !== "input_image" && part?.type !== "image" && part?.type !== "local_image") {
3252
+ return [];
3253
+ }
3254
+ const image = typeof part.image_url === "string" ? part.image_url : typeof part.image_url?.url === "string" ? part.image_url.url : typeof part.source?.data === "string" ? `data:${part.source.media_type || "image/png"};base64,${part.source.data}` : typeof part.path === "string" ? part.path : "";
3255
+ return image ? [image] : [];
3256
+ }).map(imageDedupeKey).join(",");
3257
+ }
3258
+ function imageDedupeKey(image) {
3259
+ return `${image.slice(0, 64)}:${image.length}:${image.slice(-32)}`;
3071
3260
  }
3072
3261
 
3073
3262
  // src/providers/codex/parser.ts
3074
3263
  import { readFileSync } from "fs";
3075
- import { readFile as readFile8 } from "fs/promises";
3264
+ import { readFile as readFile9 } from "fs/promises";
3076
3265
  import { extname } from "path";
3077
3266
  function asCodexTokenInfo(value) {
3078
3267
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3079
3268
  return value;
3080
3269
  }
3270
+ function asOptionalString(value) {
3271
+ return typeof value === "string" && value.trim() ? value : void 0;
3272
+ }
3081
3273
  async function parseCodexSession(filePaths, sessionInfo) {
3082
3274
  const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
3083
3275
  const lines = [];
3084
3276
  for (const fp of paths) {
3085
- const content = await readFile8(fp, "utf-8");
3086
- lines.push(...content.split("\n").filter((l) => l.trim()));
3277
+ const content = await readFile9(fp, "utf-8");
3278
+ lines.push(...content.split("\n"));
3087
3279
  }
3088
3280
  return parseCodexLines(lines, sessionInfo, paths);
3089
3281
  }
@@ -3099,20 +3291,34 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3099
3291
  let entrypoint;
3100
3292
  let permissionMode;
3101
3293
  let approvalPolicy;
3294
+ let memoryMode;
3102
3295
  const turns = [];
3103
3296
  const allTimestamps = [];
3104
3297
  const compactions = [];
3105
3298
  const tools = /* @__PURE__ */ new Map();
3106
3299
  const toolResults = /* @__PURE__ */ new Map();
3107
3300
  const tokenSnapshots = [];
3301
+ const taskDurations = [];
3108
3302
  const mcpServersUsed = /* @__PURE__ */ new Set();
3109
3303
  const skillsUsed = /* @__PURE__ */ new Set();
3110
3304
  const gitBranches = [];
3111
- for (const line of lines) {
3305
+ const seenUserMessages = /* @__PURE__ */ new Map();
3306
+ const seenAssistantMessages = /* @__PURE__ */ new Map();
3307
+ const parseWarnings = [];
3308
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
3309
+ const line = lines[lineIndex];
3310
+ if (!line.trim()) continue;
3112
3311
  let obj;
3113
3312
  try {
3114
3313
  obj = JSON.parse(line);
3115
3314
  } catch {
3315
+ addParseWarning(parseWarnings, {
3316
+ kind: "malformed-json",
3317
+ source: "codex JSONL",
3318
+ firstLine: lineIndex + 1,
3319
+ message: "Skipped malformed JSONL line",
3320
+ sample: line
3321
+ });
3116
3322
  continue;
3117
3323
  }
3118
3324
  if (obj.timestamp) {
@@ -3129,6 +3335,7 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3129
3335
  gitBranch = gitBranch || p2.git?.branch || p2.git_branch;
3130
3336
  if (gitBranch) pushUnique(gitBranches, gitBranch);
3131
3337
  entrypoint = entrypoint || p2.originator || p2.source;
3338
+ memoryMode = memoryMode || asOptionalString(p2.memory_mode);
3132
3339
  continue;
3133
3340
  }
3134
3341
  if (obj.type === "turn_context") {
@@ -3146,11 +3353,27 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3146
3353
  }
3147
3354
  if (p2.type === "user_message") {
3148
3355
  const blocks = userMessageBlocks(p2);
3149
- if (blocks.length > 0) {
3150
- turns.push({ role: "user", timestamp: obj.timestamp, blocks });
3356
+ const text = textFromBlocks(blocks);
3357
+ if (blocks.length > 0 && shouldRecordMessage(seenUserMessages, obj.timestamp, blocks)) {
3358
+ turns.push({
3359
+ role: "user",
3360
+ ...isCompactionSummaryText(text) ? { subtype: "compaction-summary" } : {},
3361
+ timestamp: obj.timestamp,
3362
+ blocks
3363
+ });
3151
3364
  }
3152
3365
  continue;
3153
3366
  }
3367
+ if (p2.type === "agent_message" && typeof p2.message === "string" && p2.message.trim().length > 0) {
3368
+ const blocks = [{ type: "text", text: p2.message }];
3369
+ if (!shouldRecordMessage(seenAssistantMessages, obj.timestamp, blocks)) continue;
3370
+ turns.push({
3371
+ role: "assistant",
3372
+ timestamp: obj.timestamp,
3373
+ blocks
3374
+ });
3375
+ continue;
3376
+ }
3154
3377
  if (p2.type === "agent_reasoning" && typeof p2.text === "string" && p2.text.trim()) {
3155
3378
  turns.push({
3156
3379
  role: "assistant",
@@ -3160,7 +3383,7 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3160
3383
  continue;
3161
3384
  }
3162
3385
  if (p2.type === "exec_command_end" && p2.call_id) {
3163
- toolResults.set(p2.call_id, {
3386
+ mergeToolResult(toolResults, p2.call_id, {
3164
3387
  result: formatExecResult(p2),
3165
3388
  isError: typeof p2.exit_code === "number" ? p2.exit_code !== 0 : void 0,
3166
3389
  timestamp: obj.timestamp,
@@ -3172,12 +3395,34 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3172
3395
  tokenSnapshots.push({
3173
3396
  timestamp: obj.timestamp,
3174
3397
  total: asCodexTokenInfo(p2.info?.total_token_usage),
3175
- last: asCodexTokenInfo(p2.info?.last_token_usage)
3398
+ last: asCodexTokenInfo(p2.info?.last_token_usage),
3399
+ contextLimit: typeof p2.info?.model_context_window === "number" ? p2.info.model_context_window : void 0
3400
+ });
3401
+ continue;
3402
+ }
3403
+ if (p2.type === "task_complete" && typeof p2.duration_ms === "number") {
3404
+ if (p2.duration_ms > 0) taskDurations.push(p2.duration_ms);
3405
+ continue;
3406
+ }
3407
+ if (p2.type === "context_compacted") {
3408
+ recordCompaction(compactions, obj.timestamp || "", "codex-context");
3409
+ continue;
3410
+ }
3411
+ if (p2.type === "patch_apply_end" && p2.call_id) {
3412
+ const tool = tools.get(p2.call_id);
3413
+ const changedFiles = patchApplyChangedFiles(p2);
3414
+ if (tool && changedFiles.length > 0) {
3415
+ tool.input = mergeToolFilePaths(tool.input, changedFiles);
3416
+ }
3417
+ mergeToolResult(toolResults, p2.call_id, {
3418
+ result: formatPatchApplyResult(p2),
3419
+ isError: typeof p2.success === "boolean" ? !p2.success : void 0,
3420
+ timestamp: obj.timestamp
3176
3421
  });
3177
3422
  continue;
3178
3423
  }
3179
- if (p2.type === "web_search_end" && p2.query) {
3180
- const callId = p2.call_id || `web_search:${obj.timestamp}`;
3424
+ if (p2.type === "web_search_end") {
3425
+ const callId = (p2.call_id && tools.has(p2.call_id) ? p2.call_id : void 0) || findWebSearchToolId(tools, toolResults, p2, obj.timestamp) || p2.call_id || `web_search:${obj.timestamp}`;
3181
3426
  if (!tools.has(callId)) {
3182
3427
  tools.set(callId, {
3183
3428
  id: callId,
@@ -3186,31 +3431,84 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3186
3431
  timestamp: obj.timestamp
3187
3432
  });
3188
3433
  }
3189
- toolResults.set(callId, {
3190
- result: `[Search: ${typeof p2.query === "string" ? p2.query : JSON.stringify(p2.query)}]`,
3434
+ mergeToolResult(toolResults, callId, {
3435
+ result: formatWebSearchResult(p2),
3191
3436
  timestamp: obj.timestamp
3192
3437
  });
3193
3438
  }
3439
+ if (p2.type === "mcp_tool_call_end" && p2.call_id) {
3440
+ const invocation = p2.invocation || {};
3441
+ const server = typeof invocation.server === "string" ? invocation.server : "";
3442
+ const toolName = typeof invocation.tool === "string" ? invocation.tool : "";
3443
+ const tool = tools.get(p2.call_id);
3444
+ if (tool && server && toolName) {
3445
+ tool.name = `mcp__${server}__${toolName}`;
3446
+ mcpServersUsed.add(server);
3447
+ }
3448
+ mergeToolResult(toolResults, p2.call_id, {
3449
+ result: formatMcpToolResult(p2),
3450
+ isError: isMcpToolError(p2),
3451
+ timestamp: obj.timestamp,
3452
+ durationMs: durationToMs(p2.duration)
3453
+ });
3454
+ }
3455
+ continue;
3456
+ }
3457
+ if (obj.type === "compacted") {
3458
+ recordCompaction(compactions, obj.timestamp || "", "codex");
3459
+ const text = compactedSummaryText(obj.payload);
3460
+ if (text) {
3461
+ turns.push({
3462
+ role: "user",
3463
+ subtype: "compaction-summary",
3464
+ timestamp: obj.timestamp,
3465
+ blocks: [{ type: "text", text }]
3466
+ });
3467
+ }
3194
3468
  continue;
3195
3469
  }
3196
3470
  if (obj.type !== "response_item") continue;
3197
3471
  const p = obj.payload || {};
3472
+ if (p.type === "message" && p.role === "developer") {
3473
+ const text = contentText2(p.content);
3474
+ if (text.trim()) {
3475
+ turns.push({
3476
+ role: "user",
3477
+ subtype: "context-injection",
3478
+ timestamp: obj.timestamp,
3479
+ blocks: [{ type: "text", text }]
3480
+ });
3481
+ }
3482
+ continue;
3483
+ }
3484
+ if (p.type === "message" && p.role === "user") {
3485
+ const blocks = userMessageBlocksFromContent(p.content);
3486
+ const text = textFromBlocks(blocks);
3487
+ if (blocks.length > 0 && shouldRecordMessage(seenUserMessages, obj.timestamp, blocks)) {
3488
+ turns.push({
3489
+ role: "user",
3490
+ ...isCompactionSummaryText(text) ? { subtype: "compaction-summary" } : {},
3491
+ timestamp: obj.timestamp,
3492
+ blocks
3493
+ });
3494
+ }
3495
+ continue;
3496
+ }
3198
3497
  if (p.type === "message" && p.role === "assistant") {
3199
- const text = contentText(p.content);
3498
+ const text = contentText2(p.content);
3200
3499
  if (text.trim()) {
3500
+ const blocks = [{ type: "text", text }];
3501
+ if (!shouldRecordMessage(seenAssistantMessages, obj.timestamp, blocks)) continue;
3201
3502
  turns.push({
3202
3503
  role: "assistant",
3203
3504
  timestamp: obj.timestamp,
3204
- blocks: [{ type: "text", text }]
3505
+ blocks
3205
3506
  });
3206
3507
  }
3207
3508
  continue;
3208
3509
  }
3209
3510
  if (p.type === "compaction") {
3210
- compactions.push({
3211
- timestamp: obj.timestamp || "",
3212
- trigger: "codex"
3213
- });
3511
+ recordCompaction(compactions, obj.timestamp || "", "codex");
3214
3512
  continue;
3215
3513
  }
3216
3514
  if (p.type === "reasoning") {
@@ -3224,8 +3522,9 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3224
3522
  }
3225
3523
  continue;
3226
3524
  }
3227
- if (p.type === "function_call" || p.type === "local_shell_call" || p.type === "custom_tool_call" || p.type === "tool_search_call" || p.type === "web_search_call" || p.type === "image_generation_call") {
3228
- const callId = p.call_id || p.id || `${p.type}:${obj.timestamp}:${tools.size}`;
3525
+ if (isCodexToolCallType(p.type)) {
3526
+ const existingWebSearchId = p.type === "web_search_call" && !p.call_id && !p.id ? findWebSearchToolId(tools, toolResults, p, obj.timestamp, { includeResolved: true }) : void 0;
3527
+ const callId = p.call_id || p.id || existingWebSearchId || `${p.type}:${obj.timestamp}:${tools.size}`;
3229
3528
  const name = p.name || normalizeCodexToolName(p.type);
3230
3529
  const input = inputForResponseItem(p);
3231
3530
  tools.set(callId, { id: callId, name, input, timestamp: obj.timestamp });
@@ -3238,10 +3537,15 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3238
3537
  }
3239
3538
  if (p.type === "function_call_output" || p.type === "custom_tool_call_output" || p.type === "tool_search_output") {
3240
3539
  if (p.call_id) {
3241
- toolResults.set(p.call_id, {
3242
- result: formatOutputPayload(p),
3243
- timestamp: obj.timestamp
3244
- });
3540
+ mergeToolResult(
3541
+ toolResults,
3542
+ p.call_id,
3543
+ {
3544
+ result: formatOutputPayload(p),
3545
+ timestamp: obj.timestamp
3546
+ },
3547
+ { preferExistingResult: true }
3548
+ );
3245
3549
  }
3246
3550
  continue;
3247
3551
  }
@@ -3266,10 +3570,14 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3266
3570
  }
3267
3571
  turns.sort((a, b) => (a.timestamp || "").localeCompare(b.timestamp || ""));
3268
3572
  const tokenUsage = tokenUsageFromSnapshots(tokenSnapshots);
3573
+ const contextLimit = [...tokenSnapshots].toReversed().find((s) => s.contextLimit)?.contextLimit;
3269
3574
  const tokenUsageByModel = tokenUsage && model ? {
3270
3575
  [model]: tokenUsage
3271
3576
  } : void 0;
3272
- const turnStats = buildCodexTurnStats(turns, tokenSnapshots, model);
3577
+ const turnStats = buildCodexTurnStats(turns, tokenSnapshots, taskDurations, model);
3578
+ const summedTaskDurationMs = taskDurations.reduce((sum, duration) => sum + duration, 0);
3579
+ const userPromptTurnCount = turns.filter((turn) => turn.role === "user" && !turn.subtype).length;
3580
+ const completeTaskDurationMs = userPromptTurnCount > 0 && taskDurations.length >= userPromptTurnCount ? summedTaskDurationMs : void 0;
3273
3581
  return {
3274
3582
  sessionId,
3275
3583
  slug: slug || sessionId.slice(0, 8),
@@ -3278,16 +3586,18 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3278
3586
  model,
3279
3587
  startTime,
3280
3588
  endTime,
3281
- totalDurationMs: estimateActiveDuration(allTimestamps),
3589
+ totalDurationMs: completeTaskDurationMs || estimateActiveDuration(allTimestamps),
3282
3590
  turns,
3283
3591
  tokenUsage,
3284
3592
  tokenUsageByModel,
3285
3593
  compactions: compactions.length > 0 ? compactions : void 0,
3286
3594
  turnStats: turnStats.length > 0 ? turnStats : void 0,
3595
+ contextLimit,
3287
3596
  gitBranch,
3288
3597
  gitBranches: gitBranches.length > 1 ? gitBranches : void 0,
3289
3598
  entrypoint,
3290
3599
  permissionMode: approvalPolicy || permissionMode,
3600
+ memoryMode,
3291
3601
  skillsUsed: skillsUsed.size > 0 ? [...skillsUsed].sort() : void 0,
3292
3602
  mcpServersUsed: mcpServersUsed.size > 0 ? [...mcpServersUsed].sort() : void 0,
3293
3603
  dataSource: "jsonl",
@@ -3296,12 +3606,13 @@ function parseCodexLines(lines, sessionInfo, sourcePaths = []) {
3296
3606
  sources: ["~/.codex/state_5.sqlite", "~/.codex/sessions"],
3297
3607
  supplements: sourcePaths,
3298
3608
  notes: ["Discovered from Codex state and parsed from rollout JSONL (local beta)."]
3299
- }
3609
+ },
3610
+ parseWarnings: parseWarnings.length > 0 ? parseWarnings : void 0
3300
3611
  };
3301
3612
  }
3302
3613
  function userMessageBlocks(payload) {
3303
3614
  const blocks = [];
3304
- const text = typeof payload.message === "string" ? stripUserMessagePrefix(payload.message).trim() : "";
3615
+ const text = typeof payload.message === "string" ? normalizeUserMessageText(payload.message) : "";
3305
3616
  if (text) blocks.push({ type: "text", text });
3306
3617
  const imageUrls = [...Array.isArray(payload.images) ? payload.images : []].filter(
3307
3618
  (v) => typeof v === "string" && v.trim()
@@ -3311,7 +3622,42 @@ function userMessageBlocks(payload) {
3311
3622
  if (imageUrls.length > 0) blocks.push({ type: "_user_images", images: imageUrls });
3312
3623
  return blocks;
3313
3624
  }
3314
- function contentText(content) {
3625
+ function userMessageBlocksFromContent(content) {
3626
+ const blocks = [];
3627
+ const text = normalizeUserMessageText(contentText2(content));
3628
+ if (text) blocks.push({ type: "text", text });
3629
+ const images = contentImages(content);
3630
+ if (images.length > 0) blocks.push({ type: "_user_images", images });
3631
+ return blocks;
3632
+ }
3633
+ function textFromBlocks(blocks) {
3634
+ return blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n").trim();
3635
+ }
3636
+ function shouldRecordMessage(seen, timestamp, blocks) {
3637
+ const key = messageDedupeKey(blocks);
3638
+ const time = timestamp ? Date.parse(timestamp) : Number.NaN;
3639
+ const previous = seen.get(key) || [];
3640
+ const isDuplicate = Number.isNaN(time) ? previous.length > 0 : previous.some((prev) => Math.abs(time - prev) <= 2e3);
3641
+ if (isDuplicate) return false;
3642
+ seen.set(key, [...previous, Number.isNaN(time) ? Number.NEGATIVE_INFINITY : time]);
3643
+ return true;
3644
+ }
3645
+ function messageDedupeKey(blocks) {
3646
+ return blocks.map((block) => {
3647
+ if (block.type === "text") return `text:${block.text || ""}`;
3648
+ if (block.type === "_user_images") {
3649
+ return `images:${block.images.map(imageDedupeKey2).join(",")}`;
3650
+ }
3651
+ return `block:${block.type}`;
3652
+ }).join("|");
3653
+ }
3654
+ function imageDedupeKey2(image) {
3655
+ return `${image.slice(0, 64)}:${image.length}:${image.slice(-32)}`;
3656
+ }
3657
+ function isCompactionSummaryText(text) {
3658
+ return text.startsWith("This session is being continued from a previous conversation");
3659
+ }
3660
+ function contentText2(content) {
3315
3661
  if (typeof content === "string") return content;
3316
3662
  if (!Array.isArray(content)) return "";
3317
3663
  return content.map((part) => {
@@ -3322,10 +3668,22 @@ function contentText(content) {
3322
3668
  return "";
3323
3669
  }).filter(Boolean).join("\n");
3324
3670
  }
3671
+ function contentImages(content) {
3672
+ if (!Array.isArray(content)) return [];
3673
+ const images = [];
3674
+ for (const part of content) {
3675
+ if (part?.type !== "input_image" && part?.type !== "image" && part?.type !== "local_image") {
3676
+ continue;
3677
+ }
3678
+ const imageUrl = typeof part.image_url === "string" ? part.image_url : typeof part.image_url?.url === "string" ? part.image_url.url : typeof part.source?.data === "string" ? `data:${part.source.media_type || "image/png"};base64,${part.source.data}` : typeof part.path === "string" ? localImageToDataUrl(part.path) || "" : "";
3679
+ if (imageUrl) images.push(imageUrl);
3680
+ }
3681
+ return images;
3682
+ }
3325
3683
  function reasoningText(payload) {
3326
3684
  if (typeof payload.content === "string") return payload.content;
3327
- if (Array.isArray(payload.content)) return contentText(payload.content);
3328
- if (Array.isArray(payload.summary)) return contentText(payload.summary);
3685
+ if (Array.isArray(payload.content)) return contentText2(payload.content);
3686
+ if (Array.isArray(payload.summary)) return contentText2(payload.summary);
3329
3687
  return "";
3330
3688
  }
3331
3689
  function parseArguments(value) {
@@ -3356,6 +3714,20 @@ function inputForResponseItem(payload) {
3356
3714
  }
3357
3715
  return parseArguments(payload.arguments ?? payload.action ?? {});
3358
3716
  }
3717
+ function patchApplyChangedFiles(payload) {
3718
+ const changes = payload?.changes;
3719
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return [];
3720
+ return Object.keys(changes).filter((file) => file.trim().length > 0);
3721
+ }
3722
+ function mergeToolFilePaths(input, filePaths) {
3723
+ const unique = [...new Set(filePaths)];
3724
+ if (unique.length === 0) return input;
3725
+ return {
3726
+ ...input,
3727
+ file_paths: unique,
3728
+ ...input.file_path || unique.length !== 1 ? {} : { file_path: unique[0] }
3729
+ };
3730
+ }
3359
3731
  function localShellActionInput(action) {
3360
3732
  const command = Array.isArray(action?.command) ? action.command.map(String).join(" ") : typeof action?.command === "string" ? action.command : "";
3361
3733
  return {
@@ -3393,6 +3765,27 @@ function formatExecResult(payload) {
3393
3765
  const exit = typeof payload.exit_code === "number" ? `Exit code: ${payload.exit_code}` : "";
3394
3766
  return [output, status, exit].filter(Boolean).join("\n");
3395
3767
  }
3768
+ function formatPatchApplyResult(payload) {
3769
+ const status = payload.status ? `Status: ${payload.status}` : "";
3770
+ const stdout = payload.stdout || "";
3771
+ const stderr = payload.stderr || "";
3772
+ const files = patchApplyChangedFiles(payload);
3773
+ const changed = files.length > 0 ? `Changed files:
3774
+ ${files.map((file) => `- ${file}`).join("\n")}` : "";
3775
+ return [stdout, stderr, status, changed].filter(Boolean).join("\n");
3776
+ }
3777
+ function formatWebSearchResult(payload) {
3778
+ if (typeof payload.query === "string" && payload.query.trim()) {
3779
+ return `[Search: ${payload.query}]`;
3780
+ }
3781
+ const action = payload.action || {};
3782
+ if (typeof action.url === "string" && action.url.trim()) return `[Open: ${action.url}]`;
3783
+ if (typeof action.pattern === "string" && action.pattern.trim())
3784
+ return `[Find: ${action.pattern}]`;
3785
+ if (typeof action.query === "string" && action.query.trim()) return `[Search: ${action.query}]`;
3786
+ if (typeof action.type === "string" && action.type.trim()) return `[Web search: ${action.type}]`;
3787
+ return "[Web search]";
3788
+ }
3396
3789
  function formatOutputPayload(payload) {
3397
3790
  if (typeof payload.output === "string") return payload.output;
3398
3791
  if (Array.isArray(payload.output)) return formatContentItems(payload.output);
@@ -3400,6 +3793,81 @@ function formatOutputPayload(payload) {
3400
3793
  if (payload.action) return JSON.stringify(payload.action, null, 2);
3401
3794
  return JSON.stringify(payload);
3402
3795
  }
3796
+ function mergeToolResult(toolResults, callId, next, options = {}) {
3797
+ const previous = toolResults.get(callId);
3798
+ if (!previous) {
3799
+ toolResults.set(callId, next);
3800
+ return;
3801
+ }
3802
+ toolResults.set(callId, {
3803
+ result: options.preferExistingResult && previous.result ? previous.result : next.result || previous.result,
3804
+ isError: next.isError ?? previous.isError,
3805
+ timestamp: next.timestamp || previous.timestamp,
3806
+ durationMs: next.durationMs ?? previous.durationMs
3807
+ });
3808
+ }
3809
+ function formatMcpToolResult(payload) {
3810
+ const result = payload.result?.Ok ?? payload.result?.Err ?? payload.result;
3811
+ if (result?.content) return formatContentItems(result.content);
3812
+ if (typeof result === "string") return result;
3813
+ return result ? JSON.stringify(result, null, 2) : "";
3814
+ }
3815
+ function isMcpToolError(payload) {
3816
+ return Boolean(
3817
+ payload.isError || payload.is_error || payload.result?.Err || payload.result?.isError
3818
+ );
3819
+ }
3820
+ function findWebSearchToolId(tools, toolResults, payload, timestamp, options = {}) {
3821
+ for (const tool of tools.values()) {
3822
+ if (tool.name !== "web_search") continue;
3823
+ if (!options.includeResolved && toolResults.has(tool.id)) continue;
3824
+ if (!sameWebSearchAction(tool.input, payload.action || { query: payload.query })) continue;
3825
+ if (!timestamp || !tool.timestamp) return tool.id;
3826
+ const diff = Math.abs(Date.parse(timestamp) - Date.parse(tool.timestamp));
3827
+ if (!Number.isFinite(diff) || diff <= 5e3) return tool.id;
3828
+ }
3829
+ return void 0;
3830
+ }
3831
+ function sameWebSearchAction(a, b) {
3832
+ try {
3833
+ return stableStringify(a || {}) === stableStringify(b || {});
3834
+ } catch {
3835
+ return false;
3836
+ }
3837
+ }
3838
+ function stableStringify(value) {
3839
+ return JSON.stringify(stableNormalize(value));
3840
+ }
3841
+ function stableNormalize(value) {
3842
+ if (Array.isArray(value)) return value.map(stableNormalize);
3843
+ if (!value || typeof value !== "object") return value;
3844
+ return Object.fromEntries(
3845
+ Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nested]) => [key, stableNormalize(nested)])
3846
+ );
3847
+ }
3848
+ function compactedSummaryText(payload) {
3849
+ if (typeof payload?.message === "string" && payload.message.trim()) return payload.message.trim();
3850
+ return "";
3851
+ }
3852
+ function recordCompaction(compactions, timestamp, trigger) {
3853
+ const time = Date.parse(timestamp);
3854
+ const duplicateIndex = compactions.findIndex((compaction) => {
3855
+ const previous = Date.parse(compaction.timestamp);
3856
+ return Number.isFinite(time) && Number.isFinite(previous) ? Math.abs(time - previous) <= 2e3 : compaction.timestamp === timestamp;
3857
+ });
3858
+ if (duplicateIndex >= 0) {
3859
+ if (compactionPriority(trigger) > compactionPriority(compactions[duplicateIndex].trigger)) {
3860
+ compactions[duplicateIndex] = { ...compactions[duplicateIndex], trigger };
3861
+ }
3862
+ return;
3863
+ }
3864
+ compactions.push({ timestamp, trigger });
3865
+ }
3866
+ function compactionPriority(trigger) {
3867
+ if (trigger === "codex-context") return 2;
3868
+ if (trigger === "codex") return 1;
3869
+ return 0;
3870
+ }
3403
3871
  function formatContentItems(items) {
3404
3872
  return items.map((item) => {
3405
3873
  if (item?.type === "input_text" || item?.type === "output_text" || item?.type === "text") {
@@ -3428,41 +3896,43 @@ function tokenUsageFromSnapshots(snapshots) {
3428
3896
  cacheReadTokens: cached
3429
3897
  };
3430
3898
  }
3431
- function buildCodexTurnStats(turns, snapshots, model) {
3432
- const userTurns = turns.filter((t) => t.role === "user");
3899
+ function buildCodexTurnStats(turns, snapshots, taskDurations, model) {
3900
+ const userTurns = turns.filter((t) => t.role === "user" && !t.subtype);
3433
3901
  const usable = snapshots.filter((s) => s.last);
3434
3902
  return userTurns.map((turn, i) => {
3435
3903
  const usage = usable[i]?.last;
3436
- const stat10 = { turnIndex: i };
3437
- if (model) stat10.model = model;
3904
+ const stat11 = { turnIndex: i };
3905
+ if (model) stat11.model = model;
3438
3906
  if (usage) {
3439
3907
  const input = usage.input_tokens || 0;
3440
3908
  const cached = usage.cached_input_tokens || 0;
3441
- stat10.tokenUsage = {
3909
+ stat11.tokenUsage = {
3442
3910
  inputTokens: Math.max(0, input - cached),
3443
3911
  outputTokens: usage.output_tokens || 0,
3444
3912
  cacheCreationTokens: 0,
3445
3913
  cacheReadTokens: cached
3446
3914
  };
3447
- stat10.contextTokens = input;
3915
+ stat11.contextTokens = input;
3448
3916
  }
3449
3917
  const nextUser = userTurns[i + 1]?.timestamp;
3450
- if (turn.timestamp) {
3918
+ const taskDuration = taskDurations[i];
3919
+ if (typeof taskDuration === "number" && taskDuration > 0) {
3920
+ stat11.durationMs = taskDuration;
3921
+ } else if (turn.timestamp) {
3451
3922
  const end = nextUser || usable[i]?.timestamp;
3452
3923
  if (end) {
3453
3924
  const diff = Date.parse(end) - Date.parse(turn.timestamp);
3454
- if (diff > 0 && diff < 12 * 60 * 60 * 1e3) stat10.durationMs = diff;
3925
+ if (diff > 0 && diff < 12 * 60 * 60 * 1e3) stat11.durationMs = diff;
3455
3926
  }
3456
3927
  }
3457
- return stat10;
3928
+ return stat11;
3458
3929
  });
3459
3930
  }
3460
3931
  function pushUnique(items, value) {
3461
3932
  if (items.length === 0 || items[items.length - 1] !== value) items.push(value);
3462
3933
  }
3463
- function stripUserMessagePrefix(text) {
3464
- const idx = text.indexOf(USER_MESSAGE_BEGIN);
3465
- return idx === -1 ? text : text.slice(idx + USER_MESSAGE_BEGIN.length);
3934
+ function normalizeUserMessageText(text) {
3935
+ return codexStripTwoPass(text);
3466
3936
  }
3467
3937
  function localImageToDataUrl(path) {
3468
3938
  try {
@@ -3499,16 +3969,16 @@ var codexProvider = {
3499
3969
 
3500
3970
  // src/providers/cursor/discover.ts
3501
3971
  init_utils();
3502
- import { readdir as readdir7, readFile as readFile10, stat as stat6 } from "fs/promises";
3503
- import { homedir as homedir9 } from "os";
3504
- import { basename as basename3, join as join10 } from "path";
3972
+ import { readdir as readdir8, readFile as readFile12, stat as stat7 } from "fs/promises";
3973
+ import { homedir as homedir10 } from "os";
3974
+ import { basename as basename3, join as join11 } from "path";
3505
3975
 
3506
3976
  // src/providers/cursor/sqlite-reader.ts
3507
3977
  import { createHash } from "crypto";
3508
3978
  import { execFile } from "child_process";
3509
- import { readdir as readdir6, readFile as readFile9, stat as stat5 } from "fs/promises";
3979
+ import { readdir as readdir6, readFile as readFile10, stat as stat5 } from "fs/promises";
3510
3980
  import { homedir as homedir8 } from "os";
3511
- import { basename as basename2, dirname as dirname2, join as join9 } from "path";
3981
+ import { basename as basename2, dirname as dirname2, join as join9, posix } from "path";
3512
3982
  import { promisify } from "util";
3513
3983
  init_utils();
3514
3984
 
@@ -3589,6 +4059,7 @@ var MAX_CURSOR_REQUEST_CONTEXT_ROWS = 500;
3589
4059
  var SQLITE_CLI_MAX_BUFFER = 256 * 1024 * 1024;
3590
4060
  var SQLITE_CLI_QUERY_TIMEOUT_MS = 12e4;
3591
4061
  var MAX_CURSOR_GLOBAL_STATE_TOOL_RESULT_CHARS = 1e4;
4062
+ var GLOBAL_STATE_BUBBLE_KEY_CHUNK_SIZE = 200;
3592
4063
  var execFileAsync = promisify(execFile);
3593
4064
  function createRetryableInit(factory) {
3594
4065
  let promise = null;
@@ -3616,7 +4087,7 @@ function closeCachedSqlJsDb() {
3616
4087
  }
3617
4088
  var cachedStoreDbIndex = null;
3618
4089
  var resolvedProjectRootCache = /* @__PURE__ */ new Map();
3619
- var GLOBAL_STATE_DISCOVERY_CACHE_PREFIX = "cursor-global-state-discovery-v3";
4090
+ var GLOBAL_STATE_DISCOVERY_CACHE_PREFIX = "cursor-global-state-discovery-v4";
3620
4091
  function workspaceHash(absolutePath) {
3621
4092
  return createHash("md5").update(absolutePath).digest("hex");
3622
4093
  }
@@ -3656,10 +4127,21 @@ async function globalStateWalFingerprint(dbPath) {
3656
4127
  function sqlString(value) {
3657
4128
  return `'${value.replaceAll("'", "''")}'`;
3658
4129
  }
3659
- function sqlLikePrefix(prefix) {
3660
- return sqlString(
3661
- `${prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`
3662
- );
4130
+ function nextStringPrefix(prefix) {
4131
+ if (!prefix) return null;
4132
+ const chars = [...prefix];
4133
+ const last = chars.pop();
4134
+ if (!last) return null;
4135
+ const codePoint = last.codePointAt(0);
4136
+ if (codePoint === void 0 || codePoint >= 1114111) return null;
4137
+ const next = codePoint + 1;
4138
+ if (next >= 55296 && next <= 57343) return null;
4139
+ return `${chars.join("")}${String.fromCodePoint(next)}`;
4140
+ }
4141
+ function sqlKeyPrefixRange(prefix, column = "key") {
4142
+ const upperBound = nextStringPrefix(prefix);
4143
+ if (!upperBound) return `${column} >= ${sqlString(prefix)}`;
4144
+ return `${column} >= ${sqlString(prefix)} AND ${column} < ${sqlString(upperBound)}`;
3663
4145
  }
3664
4146
  function projectedCursorBubbleSelectSql() {
3665
4147
  const value = "value";
@@ -3696,6 +4178,41 @@ function projectedCursorBubbleSelectSql() {
3696
4178
  `json_extract(${value}, '$.recentlyViewedFiles') AS recentlyViewedFiles`
3697
4179
  ].join(", ");
3698
4180
  }
4181
+ function replayableGlobalStateBubblePredicateSql(valueExpr) {
4182
+ const nonEmptyText = (path) => `(json_type(${valueExpr}, ${sqlString(path)}) = 'text' AND length(trim(COALESCE(json_extract(${valueExpr}, ${sqlString(
4183
+ path
4184
+ )}), ''))) > 0)`;
4185
+ return [
4186
+ nonEmptyText("$.text"),
4187
+ nonEmptyText("$.thinking"),
4188
+ nonEmptyText("$.thinking.text"),
4189
+ nonEmptyText("$.toolFormerData.name")
4190
+ ].join(" OR ");
4191
+ }
4192
+ function replayableGlobalStateBubbleCountSql(composerValueExpr, composerKeyExpr) {
4193
+ const inlineBubblePredicate = replayableGlobalStateBubblePredicateSql("conversation.value");
4194
+ const referencedBubblePredicate = replayableGlobalStateBubblePredicateSql("bubble.value");
4195
+ return [
4196
+ "COALESCE((",
4197
+ "SELECT COUNT(*) FROM json_each(",
4198
+ composerValueExpr,
4199
+ ", '$.conversation') AS conversation",
4200
+ " WHERE json_type(conversation.value) = 'object' AND (",
4201
+ inlineBubblePredicate,
4202
+ ")",
4203
+ "), 0) + COALESCE((",
4204
+ "SELECT COUNT(*) FROM json_each(",
4205
+ composerValueExpr,
4206
+ ", '$.fullConversationHeadersOnly') AS header",
4207
+ " JOIN cursorDiskKV AS bubble ON bubble.key = 'bubbleId:' || substr(",
4208
+ composerKeyExpr,
4209
+ ", length('composerData:') + 1) || ':' || json_extract(header.value, '$.bubbleId')",
4210
+ " WHERE json_valid(bubble.value) AND (",
4211
+ referencedBubblePredicate,
4212
+ ")",
4213
+ "), 0)"
4214
+ ].join("");
4215
+ }
3699
4216
  var SQLITE_CLI_NEGATIVE_CACHE_TTL_MS = 3e4;
3700
4217
  var sqliteCliUsabilityCache = /* @__PURE__ */ new Map();
3701
4218
  async function canUseSqliteCli(dbPath) {
@@ -3728,6 +4245,13 @@ async function querySqliteCli(dbPath, sql) {
3728
4245
  if (!trimmed) return [];
3729
4246
  return JSON.parse(trimmed);
3730
4247
  }
4248
+ async function querySqliteCliText(dbPath, sql) {
4249
+ const { stdout } = await execFileAsync("sqlite3", ["-readonly", dbPath, sql], {
4250
+ maxBuffer: SQLITE_CLI_MAX_BUFFER,
4251
+ timeout: SQLITE_CLI_QUERY_TIMEOUT_MS
4252
+ });
4253
+ return stdout.replace(/\r?\n$/, "");
4254
+ }
3731
4255
  function sqlJsRows(db, sql) {
3732
4256
  const result = db.exec(sql);
3733
4257
  if (!result.length) return [];
@@ -3746,6 +4270,21 @@ async function queryGlobalStateRows(globalStateDb, sql) {
3746
4270
  }
3747
4271
  return sqlJsRows(globalStateDb.db, sql);
3748
4272
  }
4273
+ async function queryGlobalStateTextValue(globalStateDb, key) {
4274
+ if (globalStateDb.backend === "sqlite-cli") {
4275
+ const value = await querySqliteCliText(
4276
+ globalStateDb.dbPath,
4277
+ `SELECT CAST(value AS TEXT) FROM cursorDiskKV WHERE key = ${sqlString(key)} LIMIT 1`
4278
+ );
4279
+ return value || null;
4280
+ }
4281
+ const rows = sqlJsRows(
4282
+ globalStateDb.db,
4283
+ `SELECT value FROM cursorDiskKV WHERE key = ${sqlString(key)} LIMIT 1`
4284
+ );
4285
+ if (rows.length === 0) return null;
4286
+ return valueToString(rows[0].value) || null;
4287
+ }
3749
4288
  async function openGlobalStateDb() {
3750
4289
  const dbPath = await findGlobalStateDb();
3751
4290
  if (!dbPath) return null;
@@ -3772,7 +4311,7 @@ async function openGlobalStateDb() {
3772
4311
  } catch {
3773
4312
  return null;
3774
4313
  }
3775
- const dbBuffer = await readFile9(dbPath).catch(() => null);
4314
+ const dbBuffer = await readFile10(dbPath).catch(() => null);
3776
4315
  if (!dbBuffer) return null;
3777
4316
  const db = new SQL.Database(dbBuffer);
3778
4317
  closeCachedSqlJsDb();
@@ -3867,6 +4406,110 @@ async function resolveCursorLiveWatchPaths(sessionId) {
3867
4406
  }
3868
4407
  return [...paths];
3869
4408
  }
4409
+ async function readCursorLiveDiagnostics(sessionId) {
4410
+ const globalStateDb = await openGlobalStateDb();
4411
+ if (!globalStateDb || !await hasGlobalStateSession(globalStateDb, sessionId)) return null;
4412
+ const rawComposer = await queryGlobalStateTextValue(globalStateDb, `composerData:${sessionId}`);
4413
+ const composer = parseJson(rawComposer || "");
4414
+ if (!composer) return null;
4415
+ const headerBubbleIds = Array.isArray(composer.fullConversationHeadersOnly) ? composer.fullConversationHeadersOnly.map(
4416
+ (header) => header && typeof header === "object" && typeof header.bubbleId === "string" ? header.bubbleId : ""
4417
+ ).filter(Boolean) : Array.isArray(composer.conversation) ? composer.conversation.map(
4418
+ (item) => item && typeof item === "object" && typeof item.bubbleId === "string" ? item.bubbleId : ""
4419
+ ).filter(Boolean) : [];
4420
+ const latestBubbleId = headerBubbleIds[headerBubbleIds.length - 1];
4421
+ const bubbleRows = [];
4422
+ const bubbleKeys = headerBubbleIds.map((bubbleId) => `bubbleId:${sessionId}:${bubbleId}`);
4423
+ for (let i = 0; i < bubbleKeys.length; i += GLOBAL_STATE_BUBBLE_KEY_CHUNK_SIZE) {
4424
+ const chunk = bubbleKeys.slice(i, i + GLOBAL_STATE_BUBBLE_KEY_CHUNK_SIZE);
4425
+ bubbleRows.push(
4426
+ ...await queryGlobalStateRows(
4427
+ globalStateDb,
4428
+ [
4429
+ "SELECT",
4430
+ "key,",
4431
+ "length(value) AS bytes,",
4432
+ "json_extract(value,'$.type') AS type,",
4433
+ "json_extract(value,'$.text') AS text,",
4434
+ "json_extract(value,'$.createdAt') AS createdAt,",
4435
+ "json_extract(value,'$.lastUpdatedAt') AS lastUpdatedAt,",
4436
+ "json_extract(value,'$.toolFormerData.name') AS toolName,",
4437
+ "json_extract(value,'$.toolFormerData.result') IS NOT NULL AS toolHasResult,",
4438
+ "length(json_extract(value,'$.toolFormerData.result')) AS toolResultLength",
4439
+ "FROM cursorDiskKV",
4440
+ `WHERE key IN (${chunk.map(sqlString).join(",")}) AND json_valid(value)`
4441
+ ].join(" ")
4442
+ )
4443
+ );
4444
+ }
4445
+ const bubbleRowsByKey = new Map(bubbleRows.map((row) => [valueToString(row.key), row]));
4446
+ const latest = latestBubbleId ? bubbleRowsByKey.get(`bubbleId:${sessionId}:${latestBubbleId}`) || {} : {};
4447
+ const latestToolName = valueToString(latest.toolName).trim();
4448
+ let toolCallCount = 0;
4449
+ let toolResultCount = 0;
4450
+ let pendingToolCount = 0;
4451
+ let maxBubbleBytes = 0;
4452
+ let totalBubbleBytes = 0;
4453
+ for (const row of bubbleRows) {
4454
+ const bytes = toNonNegativeInt(row.bytes);
4455
+ maxBubbleBytes = Math.max(maxBubbleBytes, bytes);
4456
+ totalBubbleBytes += bytes;
4457
+ if (!valueToString(row.toolName)) continue;
4458
+ toolCallCount++;
4459
+ if (Number(row.toolHasResult)) {
4460
+ toolResultCount++;
4461
+ } else {
4462
+ pendingToolCount++;
4463
+ }
4464
+ }
4465
+ const wal = await globalStateWalFingerprint(globalStateDb.dbPath);
4466
+ const diagnostics = {
4467
+ source: "global-state",
4468
+ probedAt: (/* @__PURE__ */ new Date()).toISOString(),
4469
+ dbPath: globalStateDb.dbPath,
4470
+ dbMtimeMs: Math.round(globalStateDb.mtimeMs),
4471
+ walMtimeMs: Math.round(wal.walMtimeMs),
4472
+ walSize: Math.round(wal.walSize),
4473
+ composerBytes: rawComposer ? Buffer.byteLength(rawComposer, "utf-8") : 0,
4474
+ headerCount: headerBubbleIds.length,
4475
+ ...toIsoTimestamp(composer.lastUpdatedAt) ? { composerLastUpdatedAt: toIsoTimestamp(composer.lastUpdatedAt) } : {},
4476
+ ...latestBubbleId ? { latestBubbleId } : {},
4477
+ ...toIsoTimestamp(latest.createdAt) ? { latestBubbleCreatedAt: toIsoTimestamp(latest.createdAt) } : {},
4478
+ ...toIsoTimestamp(latest.lastUpdatedAt) ? { latestBubbleUpdatedAt: toIsoTimestamp(latest.lastUpdatedAt) } : {},
4479
+ ...typeof latest.type === "number" ? { latestBubbleType: latest.type } : {},
4480
+ ...valueToString(latest.text).trim() ? { latestTextPreview: valueToString(latest.text).replace(/\s+/g, " ").trim().slice(0, 160) } : {},
4481
+ ...latestToolName ? { latestToolName } : {},
4482
+ ...latestToolName && latest.toolHasResult !== void 0 ? { latestToolHasResult: Number(latest.toolHasResult) !== 0 } : {},
4483
+ ...latest.toolResultLength !== null && latest.toolResultLength !== void 0 ? { latestToolResultLength: toNonNegativeInt(latest.toolResultLength) } : {},
4484
+ bubbleCount: bubbleRows.length,
4485
+ toolCallCount,
4486
+ toolResultCount,
4487
+ pendingToolCount,
4488
+ maxBubbleBytes,
4489
+ totalBubbleBytes,
4490
+ signature: ""
4491
+ };
4492
+ diagnostics.signature = cursorLiveDiagnosticsSignature(diagnostics);
4493
+ return diagnostics;
4494
+ }
4495
+ function cursorLiveDiagnosticsSignature(diagnostics) {
4496
+ return [
4497
+ diagnostics.headerCount,
4498
+ diagnostics.bubbleCount,
4499
+ diagnostics.toolCallCount,
4500
+ diagnostics.toolResultCount,
4501
+ diagnostics.pendingToolCount,
4502
+ diagnostics.composerBytes,
4503
+ diagnostics.composerLastUpdatedAt || "",
4504
+ diagnostics.latestBubbleId || "",
4505
+ diagnostics.latestBubbleCreatedAt || "",
4506
+ diagnostics.latestBubbleUpdatedAt || "",
4507
+ diagnostics.latestToolName || "",
4508
+ diagnostics.latestToolHasResult ? "1" : "0",
4509
+ diagnostics.latestToolResultLength ?? "",
4510
+ diagnostics.totalBubbleBytes
4511
+ ].join("|");
4512
+ }
3870
4513
  async function listStoreDbSessionIds(forceRefresh = false) {
3871
4514
  return new Set((await getStoreDbIndex(forceRefresh)).keys());
3872
4515
  }
@@ -4193,7 +4836,7 @@ async function readStoreDbMeta(dbPath) {
4193
4836
  } catch {
4194
4837
  return null;
4195
4838
  }
4196
- const dbBuffer = await readFile9(dbPath).catch(() => null);
4839
+ const dbBuffer = await readFile10(dbPath).catch(() => null);
4197
4840
  if (!dbBuffer) return null;
4198
4841
  const db = new SQL.Database(dbBuffer);
4199
4842
  try {
@@ -4252,6 +4895,32 @@ function countComposerConversationHeaders(composer) {
4252
4895
  const legacyConversation = Array.isArray(composer.conversation) ? composer.conversation.length : 0;
4253
4896
  return Math.max(fullHeaders, legacyConversation);
4254
4897
  }
4898
+ function composerHeaderBubbleIds(composer) {
4899
+ if (!Array.isArray(composer.fullConversationHeadersOnly)) return [];
4900
+ return composer.fullConversationHeadersOnly.map(
4901
+ (header) => header && typeof header === "object" && typeof header.bubbleId === "string" ? header.bubbleId : ""
4902
+ ).filter((bubbleId) => Boolean(bubbleId));
4903
+ }
4904
+ function isReplayableGlobalStateBubble(bubble) {
4905
+ return bubbleToTurn(bubble) !== null;
4906
+ }
4907
+ async function hasReplayableGlobalStateComposer(globalStateDb, sessionId, composer) {
4908
+ if (Array.isArray(composer.conversation) && composer.conversation.some(
4909
+ (item) => item && typeof item === "object" && isReplayableGlobalStateBubble(item)
4910
+ )) {
4911
+ return true;
4912
+ }
4913
+ const bubbleIds = composerHeaderBubbleIds(composer);
4914
+ if (bubbleIds.length === 0) return false;
4915
+ const rows = await loadProjectedBubbleRowsByKeys(
4916
+ globalStateDb,
4917
+ bubbleIds.map((bubbleId) => `bubbleId:${sessionId}:${bubbleId}`)
4918
+ );
4919
+ return rows.some((row) => {
4920
+ const bubble = projectedCursorBubbleRowToBubble(row);
4921
+ return bubble ? isReplayableGlobalStateBubble(bubble) : false;
4922
+ });
4923
+ }
4255
4924
  function firstUserTextSnippet(turns) {
4256
4925
  const firstUser = turns.find((t) => t.role === "user");
4257
4926
  const firstText = firstUser?.blocks.find(
@@ -4284,15 +4953,19 @@ async function discoverGlobalStateOnlySessions(knownSessionIds, decodedWorkspace
4284
4953
  const discoveredSessions = [];
4285
4954
  try {
4286
4955
  const composerDiscoverySql = globalStateDb.backend === "sqlite-cli" ? [
4287
- "SELECT key,",
4288
- "MAX(COALESCE(json_array_length(value,'$.fullConversationHeadersOnly'),0),",
4289
- "COALESCE(json_array_length(value,'$.conversation'),0)) AS headerCount,",
4290
- "json_extract(value,'$.lastUpdatedAt') AS lastUpdatedAt,",
4291
- "json_extract(value,'$.createdAt') AS createdAt,",
4292
- "json_extract(value,'$.name') AS title,",
4293
- "length(value) AS fileSize",
4294
- "FROM cursorDiskKV WHERE key LIKE 'composerData:%' AND json_valid(value)"
4295
- ].join(" ") : "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'";
4956
+ "SELECT kv.key,",
4957
+ "MAX(COALESCE(json_array_length(kv.value,'$.fullConversationHeadersOnly'),0),",
4958
+ "COALESCE(json_array_length(kv.value,'$.conversation'),0)) AS headerCount,",
4959
+ "json_extract(kv.value,'$.lastUpdatedAt') AS lastUpdatedAt,",
4960
+ "json_extract(kv.value,'$.createdAt') AS createdAt,",
4961
+ "json_extract(kv.value,'$.name') AS title,",
4962
+ "length(kv.value) AS fileSize,",
4963
+ `${replayableGlobalStateBubbleCountSql("kv.value", "kv.key")} AS replayableBubbleCount`,
4964
+ `FROM cursorDiskKV AS kv WHERE ${sqlKeyPrefixRange(
4965
+ "composerData:",
4966
+ "kv.key"
4967
+ )} AND json_valid(kv.value)`
4968
+ ].join(" ") : `SELECT key, value FROM cursorDiskKV WHERE ${sqlKeyPrefixRange("composerData:")}`;
4296
4969
  const rows = await queryGlobalStateRows(globalStateDb, composerDiscoverySql);
4297
4970
  if (rows.length === 0) return { sessions: [], sessionIds };
4298
4971
  for (const row of rows) {
@@ -4304,6 +4977,8 @@ async function discoverGlobalStateOnlySessions(knownSessionIds, decodedWorkspace
4304
4977
  if (rawComposer && !composer) continue;
4305
4978
  const headerCount = composer ? countComposerConversationHeaders(composer) : toNonNegativeInt(row.headerCount);
4306
4979
  if (headerCount === 0) continue;
4980
+ const hasReplayableBubble = composer ? await hasReplayableGlobalStateComposer(globalStateDb, sessionId, composer) : toNonNegativeInt(row.replayableBubbleCount) > 0;
4981
+ if (!hasReplayableBubble) continue;
4307
4982
  sessionIds.add(sessionId);
4308
4983
  const timestamp = toIsoTimestamp(composer?.lastUpdatedAt ?? row.lastUpdatedAt) || toIsoTimestamp(composer?.createdAt ?? row.createdAt) || (/* @__PURE__ */ new Date()).toISOString();
4309
4984
  const title = typeof (composer?.name ?? row.title) === "string" && valueToString(composer?.name ?? row.title).trim() ? valueToString(composer?.name ?? row.title).trim() : void 0;
@@ -4390,7 +5065,7 @@ async function parseCursorStoreDb(sessionId, workspacePath = "") {
4390
5065
  } catch {
4391
5066
  return null;
4392
5067
  }
4393
- const dbBuffer = await readFile9(dbPath);
5068
+ const dbBuffer = await readFile10(dbPath);
4394
5069
  const db = new SQL.Database(dbBuffer);
4395
5070
  try {
4396
5071
  const metaRows = db.exec("SELECT value FROM meta WHERE key = '0'");
@@ -4472,7 +5147,7 @@ function buildCursorStoreResult(sessionId, workspacePath, metaJson, messages) {
4472
5147
  const { turns, turnStats, totalDurationMs } = messagesToTurns(messages);
4473
5148
  const slug = sessionId.slice(0, 8);
4474
5149
  const title = metaJson.name || firstUserTextSnippet(turns);
4475
- const hasDurationStats = turnStats.some((stat10) => (stat10.durationMs || 0) > 0);
5150
+ const hasDurationStats = turnStats.some((stat11) => (stat11.durationMs || 0) > 0);
4476
5151
  const notes = [];
4477
5152
  if (hasDurationStats) {
4478
5153
  notes.push("Per-turn duration is estimated from Cursor tool execution metadata.");
@@ -4485,7 +5160,7 @@ function buildCursorStoreResult(sessionId, workspacePath, metaJson, messages) {
4485
5160
  slug,
4486
5161
  title,
4487
5162
  cwd: workspacePath,
4488
- model: normalizeCursorModelName(metaJson.lastUsedModel) || turnStats.find((stat10) => typeof stat10.model === "string" && stat10.model)?.model,
5163
+ model: normalizeCursorModelName(metaJson.lastUsedModel) || turnStats.find((stat11) => typeof stat11.model === "string" && stat11.model)?.model,
4489
5164
  startTime: metaJson.createdAt ? new Date(metaJson.createdAt).toISOString() : void 0,
4490
5165
  ...totalDurationMs !== void 0 ? { totalDurationMs } : {},
4491
5166
  ...turnStats.length > 0 ? { turnStats } : {},
@@ -4754,7 +5429,7 @@ function addContextFilesFromAttachedFolderResult(files, seen, value) {
4754
5429
  for (const file of parsed.files) {
4755
5430
  const name = file && typeof file === "object" ? normalizeCursorContextFile(file.name) : normalizeCursorContextFile(file);
4756
5431
  if (directory && name) {
4757
- addUniqueContextFile(files, seen, join9(directory, name));
5432
+ addUniqueContextFile(files, seen, posix.join(directory, name));
4758
5433
  continue;
4759
5434
  }
4760
5435
  const direct = file && typeof file === "object" ? normalizeCursorContextFile(file.filePath) || normalizeCursorContextFile(file.path) || normalizeCursorContextFile(file.fsPath) || normalizeCursorContextFile(file.uri) || normalizeCursorContextFile(file.relativeWorkspacePath) || normalizeCursorContextFile(file.relativePath) : void 0;
@@ -4825,9 +5500,9 @@ function buildBubbleProjectHintData(entries) {
4825
5500
  async function loadCursorRequestContexts(globalStateDb, sessionId) {
4826
5501
  const rows = await queryGlobalStateRows(
4827
5502
  globalStateDb,
4828
- `SELECT value FROM cursorDiskKV WHERE key LIKE ${sqlLikePrefix(
5503
+ `SELECT value FROM cursorDiskKV WHERE ${sqlKeyPrefixRange(
4829
5504
  `messageRequestContext:${sessionId}:`
4830
- )} ESCAPE '\\' LIMIT ${MAX_CURSOR_REQUEST_CONTEXT_ROWS}`
5505
+ )} LIMIT ${MAX_CURSOR_REQUEST_CONTEXT_ROWS}`
4831
5506
  );
4832
5507
  const contexts = [];
4833
5508
  for (const row of rows) {
@@ -4840,12 +5515,25 @@ async function loadCursorRequestContexts(globalStateDb, sessionId) {
4840
5515
  async function countCursorCheckpointEntries(globalStateDb, sessionId) {
4841
5516
  const rows = await queryGlobalStateRows(
4842
5517
  globalStateDb,
4843
- `SELECT COUNT(*) AS count FROM cursorDiskKV WHERE key LIKE ${sqlLikePrefix(
5518
+ `SELECT COUNT(*) AS count FROM cursorDiskKV WHERE ${sqlKeyPrefixRange(
4844
5519
  `checkpointId:${sessionId}:`
4845
- )} ESCAPE '\\'`
5520
+ )}`
4846
5521
  );
4847
5522
  return toNonNegativeInt(rows[0]?.count);
4848
5523
  }
5524
+ async function loadProjectedBubbleRowsByKeys(globalStateDb, keys) {
5525
+ const rows = [];
5526
+ for (let i = 0; i < keys.length; i += GLOBAL_STATE_BUBBLE_KEY_CHUNK_SIZE) {
5527
+ const chunk = keys.slice(i, i + GLOBAL_STATE_BUBBLE_KEY_CHUNK_SIZE);
5528
+ rows.push(
5529
+ ...await queryGlobalStateRows(
5530
+ globalStateDb,
5531
+ `SELECT ${projectedCursorBubbleSelectSql()} FROM cursorDiskKV WHERE key IN (${chunk.map(sqlString).join(",")}) AND json_valid(value)`
5532
+ )
5533
+ );
5534
+ }
5535
+ return rows;
5536
+ }
4849
5537
  function mergeUniqueStrings(...groups) {
4850
5538
  const merged = [];
4851
5539
  const seen = /* @__PURE__ */ new Set();
@@ -4861,13 +5549,13 @@ function mergeUniqueStrings(...groups) {
4861
5549
  }
4862
5550
  function mergeCursorParseResults(primary, enrichment) {
4863
5551
  const mergedModel = chooseMergedCursorModel(primary.model, enrichment.model);
4864
- const preferEnrichmentDuration = enrichment.dataSource === "global-state" && (enrichment.totalDurationMs !== void 0 || enrichment.turnStats?.some((stat10) => stat10.durationMs !== void 0) === true);
5552
+ const preferEnrichmentDuration = enrichment.dataSource === "global-state" && (enrichment.totalDurationMs !== void 0 || enrichment.turnStats?.some((stat11) => stat11.durationMs !== void 0) === true);
4865
5553
  const mergedTurnStats = mergeTurnStats(primary.turnStats, enrichment.turnStats, {
4866
5554
  preferEnrichmentDuration
4867
5555
  });
4868
5556
  const mergedTokenUsage = primary.tokenUsage || enrichment.tokenUsage;
4869
5557
  const mergedTokenUsageByModel = primary.tokenUsageByModel || enrichment.tokenUsageByModel;
4870
- const mergedTotalDurationMs = (preferEnrichmentDuration ? enrichment.totalDurationMs : void 0) || primary.totalDurationMs || (!preferEnrichmentDuration ? enrichment.totalDurationMs : void 0) || (mergedTurnStats && mergedTurnStats.length > 0 ? mergedTurnStats.reduce((sum, stat10) => sum + (stat10.durationMs || 0), 0) || void 0 : void 0);
5558
+ const mergedTotalDurationMs = (preferEnrichmentDuration ? enrichment.totalDurationMs : void 0) || primary.totalDurationMs || (!preferEnrichmentDuration ? enrichment.totalDurationMs : void 0) || (mergedTurnStats && mergedTurnStats.length > 0 ? mergedTurnStats.reduce((sum, stat11) => sum + (stat11.durationMs || 0), 0) || void 0 : void 0);
4871
5559
  const mergedDuration = mergedTotalDurationMs !== primary.totalDurationMs;
4872
5560
  const mergedTokens = !primary.tokenUsage && !!enrichment.tokenUsage || !primary.tokenUsageByModel && !!enrichment.tokenUsageByModel;
4873
5561
  const supplements = mergeUniqueStrings(
@@ -4937,11 +5625,11 @@ function mergeTurnStats(primary, enrichment, options) {
4937
5625
  if (!primary?.length) return enrichment;
4938
5626
  if (!enrichment?.length) return primary;
4939
5627
  const maxTurnIndex = Math.max(
4940
- ...primary.map((stat10) => stat10.turnIndex),
4941
- ...enrichment.map((stat10) => stat10.turnIndex)
5628
+ ...primary.map((stat11) => stat11.turnIndex),
5629
+ ...enrichment.map((stat11) => stat11.turnIndex)
4942
5630
  );
4943
- const primaryByIndex = new Map(primary.map((stat10) => [stat10.turnIndex, stat10]));
4944
- const enrichmentByIndex = new Map(enrichment.map((stat10) => [stat10.turnIndex, stat10]));
5631
+ const primaryByIndex = new Map(primary.map((stat11) => [stat11.turnIndex, stat11]));
5632
+ const enrichmentByIndex = new Map(enrichment.map((stat11) => [stat11.turnIndex, stat11]));
4945
5633
  const merged = [];
4946
5634
  for (let turnIndex = 0; turnIndex <= maxTurnIndex; turnIndex++) {
4947
5635
  const current = primaryByIndex.get(turnIndex);
@@ -5035,17 +5723,17 @@ function applyGlobalStateWallClockDurations(entries, turnStats) {
5035
5723
  if (durationsByTurn.size === 0) {
5036
5724
  return {
5037
5725
  turnStats,
5038
- totalDurationMs: turnStats.reduce((sum, stat10) => sum + (stat10.durationMs || 0), 0) || void 0,
5726
+ totalDurationMs: turnStats.reduce((sum, stat11) => sum + (stat11.durationMs || 0), 0) || void 0,
5039
5727
  usedWallClock: false
5040
5728
  };
5041
5729
  }
5042
- const mergedTurnStats = turnStats.map((stat10) => {
5043
- const wallClockDuration = durationsByTurn.get(stat10.turnIndex);
5044
- return wallClockDuration !== void 0 ? { ...stat10, durationMs: wallClockDuration } : stat10;
5730
+ const mergedTurnStats = turnStats.map((stat11) => {
5731
+ const wallClockDuration = durationsByTurn.get(stat11.turnIndex);
5732
+ return wallClockDuration !== void 0 ? { ...stat11, durationMs: wallClockDuration } : stat11;
5045
5733
  });
5046
5734
  return {
5047
5735
  turnStats: mergedTurnStats,
5048
- totalDurationMs: mergedTurnStats.reduce((sum, stat10) => sum + (stat10.durationMs || 0), 0) || void 0,
5736
+ totalDurationMs: mergedTurnStats.reduce((sum, stat11) => sum + (stat11.durationMs || 0), 0) || void 0,
5049
5737
  usedWallClock: true
5050
5738
  };
5051
5739
  }
@@ -5091,15 +5779,15 @@ function buildGlobalStateMetrics(entries, fallbackModel, sessionTokenUsage) {
5091
5779
  current.durationMs = (current.durationMs || 0) + bubbleDurationMs;
5092
5780
  }
5093
5781
  }
5094
- for (const stat10 of turnStats) {
5095
- if (stat10.tokenUsage && !hasAnyTokens(stat10.tokenUsage)) {
5096
- delete stat10.tokenUsage;
5782
+ for (const stat11 of turnStats) {
5783
+ if (stat11.tokenUsage && !hasAnyTokens(stat11.tokenUsage)) {
5784
+ delete stat11.tokenUsage;
5097
5785
  }
5098
- if ((stat10.durationMs || 0) <= 0) {
5099
- delete stat10.durationMs;
5786
+ if ((stat11.durationMs || 0) <= 0) {
5787
+ delete stat11.durationMs;
5100
5788
  }
5101
- if ((stat10.contextTokens || 0) <= 0) {
5102
- delete stat10.contextTokens;
5789
+ if ((stat11.contextTokens || 0) <= 0) {
5790
+ delete stat11.contextTokens;
5103
5791
  }
5104
5792
  }
5105
5793
  const {
@@ -5146,12 +5834,11 @@ async function parseCursorGlobalStateDb(sessionId, globalStateDb, preferredWorks
5146
5834
  const resolvedGlobalStateDb = globalStateDb ?? await openGlobalStateDb();
5147
5835
  if (!resolvedGlobalStateDb) return null;
5148
5836
  try {
5149
- const composerRows = await queryGlobalStateRows(
5837
+ const rawComposer = await queryGlobalStateTextValue(
5150
5838
  resolvedGlobalStateDb,
5151
- `SELECT value FROM cursorDiskKV WHERE key = ${sqlString(`composerData:${sessionId}`)}`
5839
+ `composerData:${sessionId}`
5152
5840
  );
5153
- if (composerRows.length === 0) return null;
5154
- const rawComposer = valueToString(composerRows[0].value);
5841
+ if (!rawComposer) return null;
5155
5842
  const composer = parseJson(rawComposer);
5156
5843
  if (!composer) return null;
5157
5844
  const entries = [];
@@ -5173,10 +5860,9 @@ async function parseCursorGlobalStateDb(sessionId, globalStateDb, preferredWorks
5173
5860
  bubbleIds.map((bubbleId) => `bubbleId:${sessionId}:${bubbleId}`)
5174
5861
  );
5175
5862
  const bubblesByKey = /* @__PURE__ */ new Map();
5176
- const rows = await queryGlobalStateRows(
5177
- resolvedGlobalStateDb,
5178
- `SELECT ${projectedCursorBubbleSelectSql()} FROM cursorDiskKV WHERE key LIKE ${sqlLikePrefix(`bubbleId:${sessionId}:`)} AND json_valid(value)`
5179
- );
5863
+ const rows = await loadProjectedBubbleRowsByKeys(resolvedGlobalStateDb, [
5864
+ ...expectedBubbleKeys
5865
+ ]);
5180
5866
  for (const row of rows) {
5181
5867
  const key = valueToString(row.key);
5182
5868
  if (!expectedBubbleKeys.has(key)) continue;
@@ -5233,7 +5919,7 @@ ${buildBubbleProjectHintData(bubbleEntries)}`,
5233
5919
  }
5234
5920
  const hasDetailedTurnStats = Boolean(
5235
5921
  metrics.turnStats?.some(
5236
- (stat10) => !!stat10.durationMs || !!stat10.contextTokens || !!stat10.tokenUsage
5922
+ (stat11) => !!stat11.durationMs || !!stat11.contextTokens || !!stat11.tokenUsage
5237
5923
  )
5238
5924
  );
5239
5925
  if (!hasDetailedTurnStats) {
@@ -5325,7 +6011,7 @@ function messagesToTurns(messages) {
5325
6011
  }
5326
6012
  }
5327
6013
  const turnStats = buildStoreTurnStats(turns);
5328
- const totalDurationMs = turnStats.length > 0 ? turnStats.reduce((sum, stat10) => sum + (stat10.durationMs || 0), 0) || void 0 : void 0;
6014
+ const totalDurationMs = turnStats.length > 0 ? turnStats.reduce((sum, stat11) => sum + (stat11.durationMs || 0), 0) || void 0 : void 0;
5329
6015
  return { turns, turnStats, totalDurationMs };
5330
6016
  }
5331
6017
  var CURSOR_SYSTEM_CONTEXT_RE = /^<(?:user_info|system_reminder|agent_transcripts|rules|git_status)>/;
@@ -5407,10 +6093,14 @@ function buildStoreTurnStats(turns) {
5407
6093
  function mapCursorToolName(name) {
5408
6094
  const mapping = {
5409
6095
  Shell: "Bash",
6096
+ shell: "Bash",
6097
+ // Cursor SDK lowercase tool name
5410
6098
  run_terminal_command_v2: "Bash",
5411
6099
  run_terminal_cmd: "Bash",
5412
6100
  Read: "Read",
5413
6101
  ReadFile: "Read",
6102
+ read: "Read",
6103
+ // Cursor SDK lowercase tool name
5414
6104
  read_file_v2: "Read",
5415
6105
  read_file: "Read",
5416
6106
  read_lints: "ReadLints",
@@ -5428,6 +6118,8 @@ function mapCursorToolName(name) {
5428
6118
  LS: "Glob",
5429
6119
  StrReplace: "Edit",
5430
6120
  EditFile: "Edit",
6121
+ edit: "Edit",
6122
+ // Cursor SDK lowercase tool name
5431
6123
  edit_file_v2: "Edit",
5432
6124
  edit_file: "Edit",
5433
6125
  search_replace: "Edit",
@@ -5690,42 +6382,426 @@ function extractModel(msg) {
5690
6382
  return void 0;
5691
6383
  }
5692
6384
 
6385
+ // src/providers/cursor/sdk-reader.ts
6386
+ import { execFile as execFile2 } from "child_process";
6387
+ import { readdir as readdir7, readFile as readFile11, stat as stat6 } from "fs/promises";
6388
+ import { homedir as homedir9 } from "os";
6389
+ import { join as join10 } from "path";
6390
+ import { promisify as promisify2 } from "util";
6391
+ var CURSOR_PROJECTS_ROOT = join10(homedir9(), ".cursor", "projects");
6392
+ var SDK_STORE_SUBDIR = "sdk-agent-store";
6393
+ var SDK_INDEX_DB_BASENAME = "index.db";
6394
+ var MIN_INDEX_DB_SIZE = 1024;
6395
+ var MAX_SQLJS_DB_BYTES = 128 * 1024 * 1024;
6396
+ var execFileAsync2 = promisify2(execFile2);
6397
+ var getSqlJs2 = /* @__PURE__ */ (() => {
6398
+ let promise = null;
6399
+ return async () => {
6400
+ if (!promise) {
6401
+ promise = (async () => {
6402
+ const mod = await import("sql.js");
6403
+ return mod.default();
6404
+ })().catch((err) => {
6405
+ promise = null;
6406
+ throw err;
6407
+ });
6408
+ }
6409
+ return promise;
6410
+ };
6411
+ })();
6412
+ var cachedAgentIndex = null;
6413
+ var cachedAgentIndexAt = 0;
6414
+ var AGENT_INDEX_TTL_MS = 5e3;
6415
+ async function discoverSdkAgents() {
6416
+ const index = await getSdkAgentIndex();
6417
+ return [...index.values()];
6418
+ }
6419
+ async function findSdkAgentById(agentId) {
6420
+ if (!agentId) return null;
6421
+ const index = await getSdkAgentIndex();
6422
+ return index.get(agentId) || null;
6423
+ }
6424
+ async function getSdkAgentIndex() {
6425
+ if (cachedAgentIndex && Date.now() - cachedAgentIndexAt < AGENT_INDEX_TTL_MS) {
6426
+ return cachedAgentIndex;
6427
+ }
6428
+ const dbPaths = await listSdkIndexDbPaths(CURSOR_PROJECTS_ROOT);
6429
+ const index = /* @__PURE__ */ new Map();
6430
+ for (const dbPath of dbPaths) {
6431
+ let handle = null;
6432
+ try {
6433
+ handle = await openIndexDb(dbPath);
6434
+ if (!handle) continue;
6435
+ const rows = await queryIndexDb(
6436
+ handle,
6437
+ "SELECT agent_id, workspace_ref, status, name, created_at, updated_at FROM agents"
6438
+ );
6439
+ for (const row of rows) {
6440
+ const agentId = stringOrEmpty(row.agent_id);
6441
+ if (!agentId) continue;
6442
+ index.set(agentId, {
6443
+ agentId,
6444
+ workspaceRef: stringOrEmpty(row.workspace_ref),
6445
+ status: stringOrEmpty(row.status) || "UNKNOWN",
6446
+ name: typeof row.name === "string" ? row.name : void 0,
6447
+ createdAt: stringOrEmpty(row.created_at),
6448
+ updatedAt: stringOrEmpty(row.updated_at),
6449
+ dbPath
6450
+ });
6451
+ }
6452
+ } catch {
6453
+ } finally {
6454
+ closeIndexDb(handle);
6455
+ }
6456
+ }
6457
+ cachedAgentIndex = index;
6458
+ cachedAgentIndexAt = Date.now();
6459
+ return index;
6460
+ }
6461
+ async function loadSdkAgentEnrichment(agent) {
6462
+ let handle = null;
6463
+ try {
6464
+ handle = await openIndexDb(agent.dbPath);
6465
+ if (!handle) return null;
6466
+ const runRows = await queryIndexDb(
6467
+ handle,
6468
+ [
6469
+ "SELECT run_id, agent_id, turn_number, status, model,",
6470
+ "started_at, finished_at, result",
6471
+ `FROM runs WHERE agent_id = ${sqlString2(agent.agentId)}`,
6472
+ "ORDER BY turn_number ASC, created_at ASC"
6473
+ ].join(" ")
6474
+ );
6475
+ if (runRows.length === 0) return null;
6476
+ const runs = runRows.map((row) => ({
6477
+ runId: stringOrEmpty(row.run_id),
6478
+ agentId: stringOrEmpty(row.agent_id),
6479
+ turnNumber: typeof row.turn_number === "number" ? row.turn_number : Number(row.turn_number),
6480
+ status: stringOrEmpty(row.status),
6481
+ model: row.model ? String(row.model) : void 0,
6482
+ startedAt: row.started_at ? String(row.started_at) : void 0,
6483
+ finishedAt: row.finished_at ? String(row.finished_at) : void 0,
6484
+ result: row.result ? String(row.result) : void 0
6485
+ }));
6486
+ const runIds = runs.map((r) => r.runId).filter(Boolean);
6487
+ if (runIds.length === 0) return { agent, runs, toolCallsByRun: /* @__PURE__ */ new Map() };
6488
+ const inList = runIds.map(sqlString2).join(", ");
6489
+ const eventRows = await queryIndexDb(
6490
+ handle,
6491
+ [
6492
+ "SELECT run_id, seq, payload_json FROM run_events",
6493
+ `WHERE run_id IN (${inList}) ORDER BY run_id, seq`
6494
+ ].join(" ")
6495
+ );
6496
+ const toolCallsByRun = collectToolCalls(eventRows);
6497
+ return finalizeEnrichment(agent, runs, toolCallsByRun);
6498
+ } catch {
6499
+ return null;
6500
+ } finally {
6501
+ closeIndexDb(handle);
6502
+ }
6503
+ }
6504
+ function finalizeEnrichment(agent, runs, toolCallsByRun) {
6505
+ let startedAt;
6506
+ let finishedAt;
6507
+ let totalDurationMs = 0;
6508
+ let totalDurationSeen = false;
6509
+ let latestModel;
6510
+ for (const run of runs) {
6511
+ if (run.startedAt && (!startedAt || run.startedAt < startedAt)) startedAt = run.startedAt;
6512
+ if (run.finishedAt && (!finishedAt || run.finishedAt > finishedAt)) finishedAt = run.finishedAt;
6513
+ if (run.startedAt && run.finishedAt) {
6514
+ const startMs = Date.parse(run.startedAt);
6515
+ const endMs = Date.parse(run.finishedAt);
6516
+ if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
6517
+ totalDurationMs += endMs - startMs;
6518
+ totalDurationSeen = true;
6519
+ }
6520
+ }
6521
+ if (run.model) latestModel = run.model;
6522
+ }
6523
+ return {
6524
+ agent,
6525
+ runs,
6526
+ toolCallsByRun,
6527
+ startedAt,
6528
+ finishedAt,
6529
+ totalDurationMs: totalDurationSeen ? totalDurationMs : void 0,
6530
+ latestModel
6531
+ };
6532
+ }
6533
+ function collectToolCalls(rows) {
6534
+ const out = /* @__PURE__ */ new Map();
6535
+ const inProgress = /* @__PURE__ */ new Map();
6536
+ for (const rawRow of rows) {
6537
+ const row = rawRow;
6538
+ const payload = row.payload_json;
6539
+ if (typeof payload !== "string" || !payload) continue;
6540
+ const parsed = safeJsonParse(payload);
6541
+ const message = parsed?.message;
6542
+ if (!message || message.type !== "tool_call") continue;
6543
+ const runId = stringOrEmpty(row.run_id);
6544
+ const callId = stringOrEmpty(message.call_id);
6545
+ if (!runId || !callId) continue;
6546
+ const seq = typeof row.seq === "number" ? row.seq : Number(row.seq);
6547
+ if (!Number.isFinite(seq)) continue;
6548
+ const key = `${runId}::${callId}`;
6549
+ const rawName = typeof message.name === "string" ? message.name : "Tool";
6550
+ const mappedName = mapCursorToolName(rawName);
6551
+ const argsObj = isPlainObject(message.args) ? message.args : {};
6552
+ const status = typeof message.status === "string" ? message.status : "unknown";
6553
+ let entry = inProgress.get(key);
6554
+ if (!entry) {
6555
+ entry = {
6556
+ callId,
6557
+ runId,
6558
+ firstSeq: seq,
6559
+ lastSeq: seq,
6560
+ name: mappedName,
6561
+ args: argsObj,
6562
+ status
6563
+ };
6564
+ inProgress.set(key, entry);
6565
+ const list = out.get(runId) || [];
6566
+ list.push(entry);
6567
+ out.set(runId, list);
6568
+ } else {
6569
+ entry.lastSeq = seq;
6570
+ entry.status = status;
6571
+ if (Object.keys(argsObj).length > 0) {
6572
+ entry.args = argsObj;
6573
+ }
6574
+ }
6575
+ if (message.result !== void 0) {
6576
+ const formatted = formatSdkToolResult(rawName, message.result);
6577
+ entry.result = formatted.text;
6578
+ entry.isError = formatted.isError || status === "error";
6579
+ }
6580
+ }
6581
+ for (const [runId, list] of out) {
6582
+ list.sort((a, b) => a.firstSeq - b.firstSeq);
6583
+ out.set(runId, list);
6584
+ }
6585
+ return out;
6586
+ }
6587
+ function formatSdkToolResult(rawToolName, result) {
6588
+ if (result === null || result === void 0) return { text: "", isError: false };
6589
+ if (typeof result === "string") return { text: result, isError: false };
6590
+ if (!isPlainObject(result)) {
6591
+ return { text: safeJsonStringify(result), isError: false };
6592
+ }
6593
+ const status = typeof result.status === "string" ? result.status : "";
6594
+ const isError = status === "error" || status === "failed" || result.error !== void 0;
6595
+ const value = result.value !== void 0 ? result.value : result;
6596
+ const mapped = mapCursorToolName(rawToolName);
6597
+ if (isError && typeof result.error === "string") {
6598
+ return { text: result.error, isError: true };
6599
+ }
6600
+ if (mapped === "Bash" && isPlainObject(value)) {
6601
+ const stdout = typeof value.stdout === "string" ? value.stdout : "";
6602
+ const stderr = typeof value.stderr === "string" ? value.stderr : "";
6603
+ const exitCode = typeof value.exitCode === "number" ? value.exitCode : void 0;
6604
+ const parts = [];
6605
+ if (stdout) parts.push(stdout.endsWith("\n") ? stdout.slice(0, -1) : stdout);
6606
+ if (stderr) parts.push(`[stderr]
6607
+ ${stderr}`);
6608
+ if (parts.length === 0 && typeof exitCode === "number") parts.push(`exit ${exitCode}`);
6609
+ return {
6610
+ text: parts.join("\n"),
6611
+ isError: isError || exitCode !== void 0 && exitCode !== 0
6612
+ };
6613
+ }
6614
+ if (mapped === "Read" && isPlainObject(value) && typeof value.content === "string") {
6615
+ return { text: value.content, isError };
6616
+ }
6617
+ if ((mapped === "Edit" || mapped === "Write" || mapped === "MultiEdit") && isPlainObject(value)) {
6618
+ const diffString = typeof value.diffString === "string" ? value.diffString : "";
6619
+ if (diffString) {
6620
+ const wrapped = {
6621
+ ...value,
6622
+ diff: { chunks: [{ diffString }] }
6623
+ };
6624
+ return { text: safeJsonStringify(wrapped), isError };
6625
+ }
6626
+ }
6627
+ return { text: safeJsonStringify(value), isError };
6628
+ }
6629
+ function applySdkEnrichmentToTurns(turns, enrichment) {
6630
+ const sortedRuns = [...enrichment.runs].sort((a, b) => a.turnNumber - b.turnNumber);
6631
+ if (sortedRuns.length === 0) {
6632
+ return { turns, toolCallsEnriched: 0, assistantTurnsModelTagged: 0 };
6633
+ }
6634
+ let runIdx = -1;
6635
+ let toolUseIdx = 0;
6636
+ let toolCallsEnriched = 0;
6637
+ let assistantTurnsModelTagged = 0;
6638
+ for (const turn of turns) {
6639
+ if (turn.role === "user") {
6640
+ runIdx++;
6641
+ toolUseIdx = 0;
6642
+ continue;
6643
+ }
6644
+ if (turn.role !== "assistant") continue;
6645
+ if (runIdx < 0) {
6646
+ continue;
6647
+ }
6648
+ const run = sortedRuns[runIdx];
6649
+ if (!run) continue;
6650
+ if (run.model && !turn.model) {
6651
+ turn.model = run.model;
6652
+ assistantTurnsModelTagged++;
6653
+ }
6654
+ const sdkCalls = enrichment.toolCallsByRun.get(run.runId) || [];
6655
+ for (const block of turn.blocks) {
6656
+ if (block.type !== "tool_use") continue;
6657
+ if (typeof block._result === "string") continue;
6658
+ const sdkCall = sdkCalls[toolUseIdx];
6659
+ toolUseIdx++;
6660
+ if (!sdkCall) continue;
6661
+ if (sdkCall.result === void 0) continue;
6662
+ block._result = sdkCall.result;
6663
+ if (sdkCall.isError) block._isError = true;
6664
+ try {
6665
+ block.input = mapToolArgs(block.name, block.input, block._result);
6666
+ } catch {
6667
+ }
6668
+ toolCallsEnriched++;
6669
+ }
6670
+ }
6671
+ return { turns, toolCallsEnriched, assistantTurnsModelTagged };
6672
+ }
6673
+ async function openIndexDb(dbPath) {
6674
+ const st = await stat6(dbPath).catch(() => null);
6675
+ if (!st?.isFile() || st.size < MIN_INDEX_DB_SIZE) return null;
6676
+ if (await canUseSqliteCliFor(dbPath)) {
6677
+ return { dbPath, backend: "sqlite-cli" };
6678
+ }
6679
+ let SQL;
6680
+ try {
6681
+ SQL = await getSqlJs2();
6682
+ } catch {
6683
+ return null;
6684
+ }
6685
+ if (st.size > MAX_SQLJS_DB_BYTES) return null;
6686
+ const buffer = await readFile11(dbPath).catch(() => null);
6687
+ if (!buffer) return null;
6688
+ const db = new SQL.Database(buffer);
6689
+ return { dbPath, backend: "sqljs", db };
6690
+ }
6691
+ function closeIndexDb(handle) {
6692
+ if (!handle || handle.backend !== "sqljs" || !handle.db) return;
6693
+ try {
6694
+ handle.db.close();
6695
+ } catch {
6696
+ }
6697
+ }
6698
+ async function queryIndexDb(handle, sql) {
6699
+ if (handle.backend === "sqlite-cli") {
6700
+ const { stdout } = await execFileAsync2("sqlite3", ["-readonly", "-json", handle.dbPath, sql], {
6701
+ maxBuffer: 64 * 1024 * 1024,
6702
+ timeout: 6e4
6703
+ });
6704
+ const trimmed = stdout.trim();
6705
+ if (!trimmed) return [];
6706
+ const parsed = JSON.parse(trimmed);
6707
+ return Array.isArray(parsed) ? parsed : [];
6708
+ }
6709
+ const result = handle.db.exec(sql);
6710
+ if (!Array.isArray(result) || result.length === 0) return [];
6711
+ const [{ columns, values }] = result;
6712
+ return values.map((row) => {
6713
+ const record = {};
6714
+ columns.forEach((column, index) => {
6715
+ record[column] = row[index];
6716
+ });
6717
+ return record;
6718
+ });
6719
+ }
6720
+ var sqliteCliCheckCache = /* @__PURE__ */ new Map();
6721
+ var SQLITE_CLI_CHECK_TTL_MS = 3e4;
6722
+ async function canUseSqliteCliFor(dbPath) {
6723
+ const cached = sqliteCliCheckCache.get(dbPath);
6724
+ if (cached && Date.now() - cached.checkedAt < SQLITE_CLI_CHECK_TTL_MS) return cached.canUse;
6725
+ let canUse = false;
6726
+ try {
6727
+ const { stdout } = await execFileAsync2(
6728
+ "sqlite3",
6729
+ ["-readonly", "-json", dbPath, "SELECT json_valid('{}') AS ok;"],
6730
+ { maxBuffer: 1024 * 1024 }
6731
+ );
6732
+ const rows = JSON.parse(stdout.trim());
6733
+ canUse = rows[0]?.ok === 1;
6734
+ } catch {
6735
+ canUse = false;
6736
+ }
6737
+ sqliteCliCheckCache.set(dbPath, { canUse, checkedAt: Date.now() });
6738
+ return canUse;
6739
+ }
6740
+ async function listSdkIndexDbPaths(projectsRoot) {
6741
+ const projects = await readdir7(projectsRoot).catch(() => []);
6742
+ const perProjectPaths = await Promise.all(
6743
+ projects.map(async (project) => {
6744
+ const out = [];
6745
+ const sdkRoot = join10(projectsRoot, project, SDK_STORE_SUBDIR);
6746
+ const sdkStat = await stat6(sdkRoot).catch(() => null);
6747
+ if (!sdkStat?.isDirectory()) return out;
6748
+ const projectHashes = await readdir7(sdkRoot).catch(() => []);
6749
+ for (const hash of projectHashes) {
6750
+ const dbPath = join10(sdkRoot, hash, SDK_INDEX_DB_BASENAME);
6751
+ const dbStat = await stat6(dbPath).catch(() => null);
6752
+ if (!dbStat?.isFile() || dbStat.size < MIN_INDEX_DB_SIZE) continue;
6753
+ out.push(dbPath);
6754
+ }
6755
+ return out;
6756
+ })
6757
+ );
6758
+ return perProjectPaths.flat();
6759
+ }
6760
+ function sqlString2(value) {
6761
+ return `'${value.replaceAll("'", "''")}'`;
6762
+ }
6763
+ function stringOrEmpty(value) {
6764
+ return typeof value === "string" ? value : "";
6765
+ }
6766
+ function isPlainObject(value) {
6767
+ return !!value && typeof value === "object" && !Array.isArray(value);
6768
+ }
6769
+ function safeJsonParse(text) {
6770
+ try {
6771
+ return JSON.parse(text);
6772
+ } catch {
6773
+ return null;
6774
+ }
6775
+ }
6776
+ function safeJsonStringify(value) {
6777
+ try {
6778
+ return JSON.stringify(value);
6779
+ } catch {
6780
+ return String(value);
6781
+ }
6782
+ }
6783
+
5693
6784
  // src/providers/cursor/discover.ts
5694
- var CURSOR_DIR = join10(homedir9(), ".cursor", "projects");
6785
+ var CURSOR_DIR = join11(homedir10(), ".cursor", "projects");
6786
+ var PROJECT_DISCOVERY_CONCURRENCY = 6;
6787
+ var TRANSCRIPT_INFO_CONCURRENCY = 6;
6788
+ var ENTRY_STAT_CONCURRENCY = 32;
6789
+ var decodedProjectDirCache = /* @__PURE__ */ new Map();
5695
6790
  async function discoverCursorSessions() {
5696
6791
  const sessions = [];
5697
6792
  let projectDirs;
5698
6793
  try {
5699
- projectDirs = await readdir7(CURSOR_DIR);
6794
+ projectDirs = await readdir8(CURSOR_DIR);
5700
6795
  } catch {
5701
6796
  return sessions;
5702
6797
  }
5703
- for (const projDir of projectDirs) {
5704
- const transcriptsDir = join10(CURSOR_DIR, projDir, "agent-transcripts");
5705
- const dirStat = await stat6(transcriptsDir).catch(() => null);
5706
- if (!dirStat?.isDirectory()) continue;
5707
- const project = await decodeProjectDir2(projDir);
5708
- const transcriptEntries = await collectTranscriptEntries(transcriptsDir);
5709
- if (transcriptEntries.length === 0) continue;
5710
- transcriptEntries.sort((a, b) => a.mtimeMs - b.mtimeMs);
5711
- const toolEntries = await collectToolEntries(join10(CURSOR_DIR, projDir, "agent-tools"));
5712
- toolEntries.sort((a, b) => a.mtimeMs - b.mtimeMs);
5713
- for (let i = 0; i < transcriptEntries.length; i++) {
5714
- const transcript = transcriptEntries[i];
5715
- const prevMtimeMs = i === 0 ? Number.NEGATIVE_INFINITY : transcriptEntries[i - 1].mtimeMs;
5716
- const toolPaths = toolEntries.filter((t) => t.mtimeMs > prevMtimeMs && t.mtimeMs <= transcript.mtimeMs).map((t) => t.path);
5717
- const info = await extractSessionInfo2(
5718
- transcript.path,
5719
- transcript.fileSize,
5720
- transcript.mtimeMs,
5721
- project,
5722
- toolPaths
5723
- );
5724
- if (!info) continue;
5725
- info.workspacePath = project;
5726
- info.hasSqlite = false;
5727
- sessions.push(info);
5728
- }
6798
+ const projectSessions = await mapLimit(
6799
+ projectDirs,
6800
+ PROJECT_DISCOVERY_CONCURRENCY,
6801
+ (projDir) => discoverProjectSessions(projDir)
6802
+ );
6803
+ for (const projectSessionList of projectSessions) {
6804
+ sessions.push(...projectSessionList);
5729
6805
  }
5730
6806
  const transcriptSessions = sessions.slice();
5731
6807
  const knownIds = new Set(transcriptSessions.map((s) => s.sessionId));
@@ -5740,54 +6816,159 @@ async function discoverCursorSessions() {
5740
6816
  const hasStoreDb = storeDbSessionIds.has(session.sessionId);
5741
6817
  session.hasSqlite = hasStoreDb || globalState.sessionIds.has(session.sessionId);
5742
6818
  }
6819
+ await enrichWithSdkAgents(sessions);
5743
6820
  sessions.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
5744
6821
  return sessions;
5745
6822
  }
6823
+ async function discoverProjectSessions(projDir) {
6824
+ const transcriptsDir = join11(CURSOR_DIR, projDir, "agent-transcripts");
6825
+ const dirStat = await stat7(transcriptsDir).catch(() => null);
6826
+ if (!dirStat?.isDirectory()) return [];
6827
+ const project = await decodeProjectDir2(projDir);
6828
+ const transcriptEntries = await collectTranscriptEntries(transcriptsDir);
6829
+ if (transcriptEntries.length === 0) return [];
6830
+ transcriptEntries.sort((a, b) => a.mtimeMs - b.mtimeMs);
6831
+ const toolEntries = await collectToolEntries(join11(CURSOR_DIR, projDir, "agent-tools"));
6832
+ toolEntries.sort((a, b) => a.mtimeMs - b.mtimeMs);
6833
+ const jobs = [];
6834
+ let toolStart = 0;
6835
+ let toolEnd = 0;
6836
+ for (let i = 0; i < transcriptEntries.length; i++) {
6837
+ const transcript = transcriptEntries[i];
6838
+ const prevMtimeMs = i === 0 ? Number.NEGATIVE_INFINITY : transcriptEntries[i - 1].mtimeMs;
6839
+ while (toolStart < toolEntries.length && toolEntries[toolStart].mtimeMs <= prevMtimeMs) {
6840
+ toolStart++;
6841
+ }
6842
+ while (toolEnd < toolEntries.length && toolEntries[toolEnd].mtimeMs <= transcript.mtimeMs) {
6843
+ toolEnd++;
6844
+ }
6845
+ jobs.push({
6846
+ ...transcript,
6847
+ toolPaths: toolEntries.slice(toolStart, toolEnd).map((tool) => tool.path)
6848
+ });
6849
+ }
6850
+ const infos = await mapLimit(jobs, TRANSCRIPT_INFO_CONCURRENCY, async (job) => {
6851
+ const info = await extractSessionInfo2(
6852
+ job.path,
6853
+ job.fileSize,
6854
+ job.mtimeMs,
6855
+ project,
6856
+ job.toolPaths
6857
+ );
6858
+ if (!info) return null;
6859
+ info.workspacePath = project;
6860
+ info.hasSqlite = false;
6861
+ return info;
6862
+ });
6863
+ return infos.filter((info) => info !== null);
6864
+ }
6865
+ async function enrichWithSdkAgents(sessions) {
6866
+ let sdkAgents = [];
6867
+ try {
6868
+ sdkAgents = await discoverSdkAgents();
6869
+ } catch {
6870
+ return;
6871
+ }
6872
+ if (sdkAgents.length === 0) return;
6873
+ const sessionsByAgentId = /* @__PURE__ */ new Map();
6874
+ for (const session of sessions) {
6875
+ sessionsByAgentId.set(session.sessionId, session);
6876
+ }
6877
+ for (const agent of sdkAgents) {
6878
+ const existing = sessionsByAgentId.get(agent.agentId);
6879
+ if (!existing) continue;
6880
+ existing.hasSdk = true;
6881
+ if (agent.workspaceRef && (!existing.cwd || existing.cwd.startsWith("/-"))) {
6882
+ existing.cwd = agent.workspaceRef;
6883
+ existing.workspacePath = agent.workspaceRef;
6884
+ }
6885
+ }
6886
+ }
5746
6887
  async function decodeProjectDir2(encoded) {
6888
+ let cached = decodedProjectDirCache.get(encoded);
6889
+ if (!cached) {
6890
+ cached = decodeProjectDirUncached(encoded).catch((err) => {
6891
+ decodedProjectDirCache.delete(encoded);
6892
+ throw err;
6893
+ });
6894
+ decodedProjectDirCache.set(encoded, cached);
6895
+ }
6896
+ return cached;
6897
+ }
6898
+ async function decodeProjectDirUncached(encoded) {
6899
+ if (process.platform === "win32") return decodeWindowsProjectDir(encoded);
5747
6900
  const parts = encoded.split("-");
5748
- async function resolve3(idx, current) {
5749
- if (idx >= parts.length) {
5750
- const s = await stat6(current).catch(() => null);
5751
- return s?.isDirectory() ? current : null;
6901
+ const startIdx = parts[0] ? 0 : 1;
6902
+ const resolved = await resolveEncodedProjectParts(parts, startIdx, "/");
6903
+ const fallbackEncoded = encoded.startsWith("-") ? encoded.slice(1) : encoded;
6904
+ return `/${resolved ? resolved.slice(1) : fallbackEncoded.replace(/-/g, "/")}`;
6905
+ }
6906
+ async function decodeWindowsProjectDir(encoded) {
6907
+ const parts = encoded.split("-").filter((part, idx) => part !== "" || idx !== 0);
6908
+ if (parts.length === 0) return encoded;
6909
+ const drive = parts[0];
6910
+ if (!/^[a-zA-Z]$/.test(drive)) {
6911
+ return parts.join("\\");
6912
+ }
6913
+ const root = `${drive.toUpperCase()}:\\`;
6914
+ const resolved = await resolveEncodedProjectParts(parts, 1, root);
6915
+ if (resolved) return resolved;
6916
+ return parts.length === 1 ? root : `${root}${parts.slice(1).join("\\")}`;
6917
+ }
6918
+ async function resolveEncodedProjectParts(parts, idx, current) {
6919
+ if (idx >= parts.length) {
6920
+ const currentStat = await stat7(current).catch(() => null);
6921
+ return currentStat?.isDirectory() ? current : null;
6922
+ }
6923
+ const entries = await readdir8(current, { withFileTypes: true }).catch(() => []);
6924
+ if (entries.length === 0) return null;
6925
+ const dirNames = new Set(
6926
+ entries.filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => entry.name)
6927
+ );
6928
+ for (let end = idx + 1; end <= parts.length; end++) {
6929
+ const candidate = parts.slice(idx, end).join("-");
6930
+ const candidatePath = join11(current, candidate);
6931
+ if (!dirNames.has(candidate)) {
6932
+ if (process.platform !== "win32") continue;
6933
+ const candidateStat = await stat7(candidatePath).catch(() => null);
6934
+ if (!candidateStat?.isDirectory()) continue;
5752
6935
  }
5753
- const withSlash = `${current}/${parts[idx]}`;
5754
- const slashResult = await resolve3(idx + 1, withSlash);
5755
- if (slashResult) return slashResult;
5756
- const withHyphen = `${current}-${parts[idx]}`;
5757
- return resolve3(idx + 1, withHyphen);
6936
+ const resolved = await resolveEncodedProjectParts(parts, end, candidatePath);
6937
+ if (resolved) return resolved;
5758
6938
  }
5759
- const result = await resolve3(1, `/${parts[0]}`);
5760
- return result || `/${encoded.replace(/-/g, "/")}`;
6939
+ return null;
5761
6940
  }
5762
6941
  async function extractSessionInfo2(filePath, fileSize, mtimeMs, project, toolPaths) {
5763
6942
  try {
5764
- const content = await readFile10(filePath, "utf-8");
5765
- const lines = content.split("\n").filter((l) => l.trim());
5766
- if (lines.length < 2) return null;
6943
+ const content = await readFile12(filePath, "utf-8");
5767
6944
  const sessionId = basename3(filePath, ".jsonl");
5768
6945
  let firstPrompt = "";
5769
- for (const line of lines.slice(0, 10)) {
5770
- try {
5771
- const obj = JSON.parse(line);
5772
- if (obj.role === "user") {
5773
- const textBlock = obj.message?.content?.find?.((b) => b.type === "text");
5774
- if (textBlock?.text) {
5775
- firstPrompt = sanitizeCursorUserText(textBlock.text).slice(0, 200);
5776
- break;
5777
- }
5778
- }
5779
- } catch {
5780
- }
5781
- }
5782
- if (!firstPrompt) return null;
5783
6946
  let promptCount = 0;
5784
6947
  let toolCallCount = 0;
5785
6948
  let editCountEst = 0;
5786
6949
  let model;
6950
+ let lineCount = 0;
6951
+ let promptScanCount = 0;
5787
6952
  const toolUseRe = /"type"\s*:\s*"tool_use"/g;
5788
6953
  const editToolRe = /"name"\s*:\s*"(edit_file|file_edit|create_file)"/;
5789
6954
  const modelRe = /"model(?:Id)?"\s*:\s*"([^"]+)"/;
5790
- for (const line of lines) {
6955
+ for (const rawLine of content.split("\n")) {
6956
+ const line = rawLine.trim();
6957
+ if (!line) continue;
6958
+ lineCount++;
6959
+ if (!firstPrompt && promptScanCount < 10) {
6960
+ promptScanCount++;
6961
+ try {
6962
+ const obj = JSON.parse(line);
6963
+ if (obj.role === "user") {
6964
+ const textBlock = obj.message?.content?.find?.((b) => b.type === "text");
6965
+ if (textBlock?.text) {
6966
+ firstPrompt = sanitizeCursorUserText(textBlock.text).slice(0, 200);
6967
+ }
6968
+ }
6969
+ } catch {
6970
+ }
6971
+ }
5791
6972
  if ((line.includes('"role":"user"') || line.includes('"role": "user"')) && !line.includes('"tool_result"')) {
5792
6973
  promptCount++;
5793
6974
  }
@@ -5801,6 +6982,8 @@ async function extractSessionInfo2(filePath, fileSize, mtimeMs, project, toolPat
5801
6982
  if (m) model = m[1];
5802
6983
  }
5803
6984
  }
6985
+ if (lineCount < 2) return null;
6986
+ if (!firstPrompt) return null;
5804
6987
  toolCallCount = Math.max(toolCallCount, toolPaths.length);
5805
6988
  const timestamp = new Date(mtimeMs).toISOString();
5806
6989
  return {
@@ -5811,7 +6994,7 @@ async function extractSessionInfo2(filePath, fileSize, mtimeMs, project, toolPat
5811
6994
  cwd: project,
5812
6995
  version: "",
5813
6996
  timestamp,
5814
- lineCount: lines.length,
6997
+ lineCount,
5815
6998
  fileSize,
5816
6999
  filePath,
5817
7000
  filePaths: [filePath],
@@ -5829,59 +7012,73 @@ async function extractSessionInfo2(filePath, fileSize, mtimeMs, project, toolPat
5829
7012
  async function collectTranscriptEntries(transcriptsDir) {
5830
7013
  let entries;
5831
7014
  try {
5832
- entries = await readdir7(transcriptsDir);
7015
+ entries = await readdir8(transcriptsDir);
5833
7016
  } catch {
5834
7017
  return [];
5835
7018
  }
5836
- const transcripts = [];
5837
- for (const entry of entries) {
5838
- const entryPath = join10(transcriptsDir, entry);
5839
- const entryStat = await stat6(entryPath).catch(() => null);
5840
- if (!entryStat) continue;
7019
+ const transcripts = await mapLimit(entries, ENTRY_STAT_CONCURRENCY, async (entry) => {
7020
+ const entryPath = join11(transcriptsDir, entry);
7021
+ const entryStat = await stat7(entryPath).catch(() => null);
7022
+ if (!entryStat) return null;
5841
7023
  if (entry.endsWith(".jsonl") && entryStat.isFile()) {
5842
- transcripts.push({
7024
+ return {
5843
7025
  path: entryPath,
5844
7026
  fileSize: entryStat.size,
5845
7027
  mtimeMs: entryStat.mtimeMs
5846
- });
5847
- continue;
7028
+ };
5848
7029
  }
5849
- if (!entryStat.isDirectory()) continue;
5850
- const innerPath = join10(entryPath, `${entry}.jsonl`);
5851
- const innerStat = await stat6(innerPath).catch(() => null);
5852
- if (!innerStat?.isFile()) continue;
5853
- transcripts.push({
7030
+ if (!entryStat.isDirectory()) return null;
7031
+ const innerPath = join11(entryPath, `${entry}.jsonl`);
7032
+ const innerStat = await stat7(innerPath).catch(() => null);
7033
+ if (!innerStat?.isFile()) return null;
7034
+ return {
5854
7035
  path: innerPath,
5855
7036
  fileSize: innerStat.size,
5856
7037
  mtimeMs: innerStat.mtimeMs
5857
- });
5858
- }
5859
- return transcripts;
7038
+ };
7039
+ });
7040
+ return transcripts.filter((entry) => entry !== null);
5860
7041
  }
5861
7042
  async function collectToolEntries(toolDir) {
5862
- const dirStat = await stat6(toolDir).catch(() => null);
7043
+ const dirStat = await stat7(toolDir).catch(() => null);
5863
7044
  if (!dirStat?.isDirectory()) return [];
5864
7045
  let entries;
5865
7046
  try {
5866
- entries = await readdir7(toolDir);
7047
+ entries = await readdir8(toolDir);
5867
7048
  } catch {
5868
7049
  return [];
5869
7050
  }
5870
- const tools = [];
5871
- for (const entry of entries) {
5872
- if (!entry.endsWith(".txt")) continue;
5873
- const entryPath = join10(toolDir, entry);
5874
- const entryStat = await stat6(entryPath).catch(() => null);
5875
- if (!entryStat?.isFile()) continue;
5876
- tools.push({ path: entryPath, mtimeMs: entryStat.mtimeMs });
5877
- }
5878
- return tools;
7051
+ const toolFiles = entries.filter((entry) => entry.endsWith(".txt"));
7052
+ const tools = await mapLimit(toolFiles, ENTRY_STAT_CONCURRENCY, async (entry) => {
7053
+ const entryPath = join11(toolDir, entry);
7054
+ const entryStat = await stat7(entryPath).catch(() => null);
7055
+ if (!entryStat?.isFile()) return null;
7056
+ return { path: entryPath, mtimeMs: entryStat.mtimeMs };
7057
+ });
7058
+ return tools.filter((entry) => entry !== null);
7059
+ }
7060
+ async function mapLimit(items, concurrency, worker) {
7061
+ if (items.length === 0) return [];
7062
+ const results = [];
7063
+ results.length = items.length;
7064
+ let nextIndex = 0;
7065
+ const workerCount = Math.min(Math.max(1, concurrency), items.length);
7066
+ await Promise.all(
7067
+ Array.from({ length: workerCount }, async () => {
7068
+ while (nextIndex < items.length) {
7069
+ const index = nextIndex++;
7070
+ results[index] = await worker(items[index], index);
7071
+ }
7072
+ })
7073
+ );
7074
+ return results;
5879
7075
  }
5880
7076
 
5881
7077
  // src/providers/cursor/parser.ts
5882
- import { readdir as readdir8, readFile as readFile11, stat as stat7 } from "fs/promises";
5883
- import { homedir as homedir10 } from "os";
5884
- import { basename as basename4, extname as extname2, join as join11 } from "path";
7078
+ import { readdir as readdir9, readFile as readFile13, stat as stat8 } from "fs/promises";
7079
+ import { homedir as homedir11 } from "os";
7080
+ import { basename as basename4, extname as extname2, join as join12 } from "path";
7081
+ var CURSOR_UUID_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5885
7082
  function toErrorMessage(err) {
5886
7083
  if (err instanceof Error && err.message) return err.message;
5887
7084
  return String(err);
@@ -5895,15 +7092,17 @@ async function parseCursorSession(filePaths, sessionInfo) {
5895
7092
  const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
5896
7093
  const transcriptPaths = paths.filter((p) => p.endsWith(".jsonl"));
5897
7094
  const explicitToolPaths = paths.filter((p) => p.endsWith(".txt"));
7095
+ const inferredSqliteSession = inferCursorSqliteSession(paths);
5898
7096
  let sqliteError;
5899
7097
  let sqliteFallbackNote;
5900
7098
  let sqliteAttempted = false;
5901
- if (sessionInfo?.sessionId) {
7099
+ const sqliteSessionId = sessionInfo?.sessionId || inferredSqliteSession?.sessionId;
7100
+ if (sqliteSessionId) {
5902
7101
  sqliteAttempted = true;
5903
7102
  let sqliteResult = null;
5904
7103
  try {
5905
- const preferredWorkspacePath2 = sessionInfo.workspacePath || sessionInfo.cwd || "";
5906
- sqliteResult = await parseCursorSqlite(preferredWorkspacePath2, sessionInfo.sessionId);
7104
+ const preferredWorkspacePath2 = sessionInfo?.workspacePath || sessionInfo?.cwd || "";
7105
+ sqliteResult = await parseCursorSqlite(preferredWorkspacePath2, sqliteSessionId);
5907
7106
  } catch (err) {
5908
7107
  sqliteError = compactErrorMessage(err);
5909
7108
  sqliteFallbackNote = `cursor SQLite parse failed (${sqliteError}); fell back to JSONL transcript`;
@@ -5916,6 +7115,10 @@ async function parseCursorSession(filePaths, sessionInfo) {
5916
7115
  const jsonlThinking = await parseCursorJsonl(transcriptPaths, [], {
5917
7116
  inferToolPaths: false
5918
7117
  });
7118
+ sqliteResult.parseWarnings = mergeParseWarnings(
7119
+ sqliteResult.parseWarnings,
7120
+ jsonlThinking.parseWarnings
7121
+ );
5919
7122
  sqliteResult.turns = mergeJsonlSupplementsIntoCursorTurns(
5920
7123
  sqliteResult.turns,
5921
7124
  jsonlThinking.turns
@@ -5958,8 +7161,83 @@ async function parseCursorSession(filePaths, sessionInfo) {
5958
7161
  sqliteFallbackNote
5959
7162
  );
5960
7163
  }
7164
+ const sdkSessionId = sessionInfo?.sessionId || deriveSessionIdFromTranscript(transcriptPaths);
7165
+ if (sdkSessionId) {
7166
+ const enrichment = await tryLoadSdkEnrichment(sdkSessionId);
7167
+ if (enrichment) {
7168
+ const { toolCallsEnriched, assistantTurnsModelTagged } = applySdkEnrichmentToTurns(
7169
+ jsonlResult.turns,
7170
+ enrichment
7171
+ );
7172
+ if (toolCallsEnriched > 0 || assistantTurnsModelTagged > 0) {
7173
+ jsonlResult.dataSourceInfo = withSupplement(
7174
+ jsonlResult.dataSourceInfo || defaultDataSourceInfo(jsonlResult.dataSource),
7175
+ `cursor-sdk index.db (tool results +${toolCallsEnriched}, model tags +${assistantTurnsModelTagged})`
7176
+ );
7177
+ }
7178
+ if (enrichment.latestModel && !jsonlResult.model) {
7179
+ jsonlResult.model = enrichment.latestModel;
7180
+ }
7181
+ if (enrichment.startedAt && !jsonlResult.startTime) {
7182
+ jsonlResult.startTime = enrichment.startedAt;
7183
+ }
7184
+ if (enrichment.finishedAt && !jsonlResult.endTime) {
7185
+ jsonlResult.endTime = enrichment.finishedAt;
7186
+ }
7187
+ if (enrichment.totalDurationMs && !jsonlResult.totalDurationMs) {
7188
+ jsonlResult.totalDurationMs = enrichment.totalDurationMs;
7189
+ }
7190
+ }
7191
+ }
5961
7192
  return jsonlResult;
5962
7193
  }
7194
+ function inferCursorSqliteSession(paths) {
7195
+ for (const path of paths) {
7196
+ const sessionId = sessionIdFromGlobalStatePath(path) || sessionIdFromStoreDbPath(path);
7197
+ if (sessionId) return { sessionId };
7198
+ }
7199
+ return void 0;
7200
+ }
7201
+ function sessionIdFromGlobalStatePath(path) {
7202
+ const marker = "#composerData:";
7203
+ const markerIndex = path.indexOf(marker);
7204
+ if (markerIndex < 0) return void 0;
7205
+ const rawSessionId = path.slice(markerIndex + marker.length).split(/[/?#]/, 1)[0]?.trim();
7206
+ return rawSessionId || void 0;
7207
+ }
7208
+ function sessionIdFromStoreDbPath(path) {
7209
+ const normalized = path.replaceAll("\\", "/");
7210
+ const parts = normalized.split("/");
7211
+ if (parts.at(-1) !== "store.db") return void 0;
7212
+ const rawSessionId = parts.at(-2)?.trim();
7213
+ if (!rawSessionId || !CURSOR_UUID_SESSION_ID_RE.test(rawSessionId)) return void 0;
7214
+ return rawSessionId;
7215
+ }
7216
+ async function tryLoadSdkEnrichment(sessionId) {
7217
+ if (!sessionId.startsWith("agent-")) return null;
7218
+ try {
7219
+ const agent = await findSdkAgentById(sessionId);
7220
+ if (!agent) return null;
7221
+ return await loadSdkAgentEnrichment(agent);
7222
+ } catch {
7223
+ return null;
7224
+ }
7225
+ }
7226
+ function deriveSessionIdFromTranscript(transcriptPaths) {
7227
+ for (let i = transcriptPaths.length - 1; i >= 0; i--) {
7228
+ const p = transcriptPaths[i];
7229
+ const base = basename4(p, ".jsonl");
7230
+ if (base.startsWith("agent-")) return base;
7231
+ }
7232
+ return null;
7233
+ }
7234
+ function mergeParseWarnings(base, extra) {
7235
+ const merged = [];
7236
+ for (const warning of [...base || [], ...extra || []]) {
7237
+ addParseWarning(merged, warning);
7238
+ }
7239
+ return merged.length > 0 ? merged : void 0;
7240
+ }
5963
7241
  function defaultDataSourceInfo(dataSource) {
5964
7242
  if (!dataSource) return void 0;
5965
7243
  return {
@@ -5985,16 +7263,26 @@ async function parseCursorJsonl(transcriptPaths, explicitToolPaths, options) {
5985
7263
  const toolResults = /* @__PURE__ */ new Map();
5986
7264
  const toolErrors = /* @__PURE__ */ new Map();
5987
7265
  const toolImages = /* @__PURE__ */ new Map();
7266
+ const parseWarnings = [];
5988
7267
  const sortedTranscriptPaths = await sortByMtime(transcriptPaths);
5989
7268
  const sessionId = basename4(sortedTranscriptPaths[sortedTranscriptPaths.length - 1], ".jsonl");
5990
7269
  for (const filePath of sortedTranscriptPaths) {
5991
- const content = await readFile11(filePath, "utf-8");
5992
- const lines = content.split("\n").filter((l) => l.trim());
5993
- for (const line of lines) {
7270
+ const content = await readFile13(filePath, "utf-8");
7271
+ const lines = content.split("\n");
7272
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
7273
+ const line = lines[lineIndex];
7274
+ if (!line.trim()) continue;
5994
7275
  let obj;
5995
7276
  try {
5996
7277
  obj = JSON.parse(line);
5997
7278
  } catch {
7279
+ addParseWarning(parseWarnings, {
7280
+ kind: "malformed-json",
7281
+ source: "cursor transcript JSONL",
7282
+ firstLine: lineIndex + 1,
7283
+ message: "Skipped malformed JSONL line",
7284
+ sample: line
7285
+ });
5998
7286
  continue;
5999
7287
  }
6000
7288
  const role = obj.role;
@@ -6031,7 +7319,17 @@ async function parseCursorJsonl(transcriptPaths, explicitToolPaths, options) {
6031
7319
  }
6032
7320
  for (const imagePath of imageFilePaths) {
6033
7321
  const dataUrl = await readImageFileAsDataUrl(imagePath);
6034
- if (dataUrl) userImages.push(dataUrl);
7322
+ if (dataUrl) {
7323
+ userImages.push(dataUrl);
7324
+ } else {
7325
+ addParseWarning(parseWarnings, {
7326
+ kind: "missing-image",
7327
+ source: "cursor transcript image reference",
7328
+ firstLine: lineIndex + 1,
7329
+ message: "Skipped image reference because the file could not be read",
7330
+ sample: imagePath
7331
+ });
7332
+ }
6035
7333
  }
6036
7334
  if (role === "user" && textParts.length === 0 && userImages.length === 0) continue;
6037
7335
  if (role === "user") {
@@ -6118,7 +7416,8 @@ async function parseCursorJsonl(transcriptPaths, explicitToolPaths, options) {
6118
7416
  "cursor/projects/agent-transcripts/*.jsonl",
6119
7417
  ...hasToolData ? ["cursor/projects/agent-tools/*.txt"] : []
6120
7418
  ]
6121
- }
7419
+ },
7420
+ parseWarnings: parseWarnings.length > 0 ? parseWarnings : void 0
6122
7421
  };
6123
7422
  }
6124
7423
  function stripUserQueryWrapper(text) {
@@ -6133,7 +7432,9 @@ function normalizeImagePlaceholderLines(text) {
6133
7432
  return filtered.join("\n").trim();
6134
7433
  }
6135
7434
  function resolveImagePath(pathValue) {
6136
- if (pathValue.startsWith("~/")) return join11(homedir10(), pathValue.slice(2));
7435
+ if (pathValue.startsWith("~/") || pathValue.startsWith("~\\")) {
7436
+ return join12(homedir11(), pathValue.slice(2));
7437
+ }
6137
7438
  return pathValue;
6138
7439
  }
6139
7440
  function imageMediaType(pathValue) {
@@ -6149,7 +7450,7 @@ function extractImageFilePathsFromText(text) {
6149
7450
  let match;
6150
7451
  while ((match = blockPattern.exec(text)) !== null) {
6151
7452
  const blockContent = match[1];
6152
- const pathRegex = /(?:^|\n)\s*(?:\d+\.\s*)?((?:~\/|\/)[^\n]+\.(?:png|jpe?g|gif|webp))/gi;
7453
+ const pathRegex = /(?:^|\n)\s*(?:\d+\.\s*)?((?:~[\\/]|[\\/]|[A-Za-z]:[\\/])[^\n]+\.(?:png|jpe?g|gif|webp))/gi;
6153
7454
  let pathMatch;
6154
7455
  while ((pathMatch = pathRegex.exec(blockContent)) !== null) {
6155
7456
  const pathValue = pathMatch[1].trim();
@@ -6161,7 +7462,7 @@ function extractImageFilePathsFromText(text) {
6161
7462
  }
6162
7463
  async function readImageFileAsDataUrl(pathValue) {
6163
7464
  try {
6164
- const data = await readFile11(pathValue);
7465
+ const data = await readFile13(pathValue);
6165
7466
  return `data:${imageMediaType(pathValue)};base64,${data.toString("base64")}`;
6166
7467
  } catch {
6167
7468
  return null;
@@ -6298,7 +7599,7 @@ function mergeJsonlSupplementsIntoCursorTurns(primaryTurns, jsonlTurns) {
6298
7599
  async function sortByMtime(paths) {
6299
7600
  const entries = [];
6300
7601
  for (const path of paths) {
6301
- const st = await stat7(path).catch(() => null);
7602
+ const st = await stat8(path).catch(() => null);
6302
7603
  if (!st?.isFile()) continue;
6303
7604
  entries.push({ path, mtimeMs: st.mtimeMs });
6304
7605
  }
@@ -6316,8 +7617,8 @@ async function inferToolPaths(transcriptPaths) {
6316
7617
  projects.get(projectRoot)?.add(transcriptPath);
6317
7618
  }
6318
7619
  for (const [projectRoot, selectedPaths] of projects.entries()) {
6319
- const transcriptsDir = join11(projectRoot, "agent-transcripts");
6320
- const toolDir = join11(projectRoot, "agent-tools");
7620
+ const transcriptsDir = join12(projectRoot, "agent-transcripts");
7621
+ const toolDir = join12(projectRoot, "agent-tools");
6321
7622
  const transcriptEntries = await collectTranscriptEntries2(transcriptsDir);
6322
7623
  const toolEntries = await collectToolEntries2(toolDir);
6323
7624
  if (transcriptEntries.length === 0 || toolEntries.length === 0) continue;
@@ -6340,22 +7641,22 @@ async function inferToolPaths(transcriptPaths) {
6340
7641
  async function collectTranscriptEntries2(transcriptsDir) {
6341
7642
  let entries;
6342
7643
  try {
6343
- entries = await readdir8(transcriptsDir);
7644
+ entries = await readdir9(transcriptsDir);
6344
7645
  } catch {
6345
7646
  return [];
6346
7647
  }
6347
7648
  const transcripts = [];
6348
7649
  for (const entry of entries) {
6349
- const entryPath = join11(transcriptsDir, entry);
6350
- const st = await stat7(entryPath).catch(() => null);
7650
+ const entryPath = join12(transcriptsDir, entry);
7651
+ const st = await stat8(entryPath).catch(() => null);
6351
7652
  if (!st) continue;
6352
7653
  if (entry.endsWith(".jsonl") && st.isFile()) {
6353
7654
  transcripts.push({ path: entryPath, mtimeMs: st.mtimeMs });
6354
7655
  continue;
6355
7656
  }
6356
7657
  if (!st.isDirectory()) continue;
6357
- const nested = join11(entryPath, `${entry}.jsonl`);
6358
- const nestedStat = await stat7(nested).catch(() => null);
7658
+ const nested = join12(entryPath, `${entry}.jsonl`);
7659
+ const nestedStat = await stat8(nested).catch(() => null);
6359
7660
  if (!nestedStat?.isFile()) continue;
6360
7661
  transcripts.push({ path: nested, mtimeMs: nestedStat.mtimeMs });
6361
7662
  }
@@ -6364,15 +7665,15 @@ async function collectTranscriptEntries2(transcriptsDir) {
6364
7665
  async function collectToolEntries2(toolDir) {
6365
7666
  let entries;
6366
7667
  try {
6367
- entries = await readdir8(toolDir);
7668
+ entries = await readdir9(toolDir);
6368
7669
  } catch {
6369
7670
  return [];
6370
7671
  }
6371
7672
  const tools = [];
6372
7673
  for (const entry of entries) {
6373
7674
  if (!entry.endsWith(".txt")) continue;
6374
- const entryPath = join11(toolDir, entry);
6375
- const st = await stat7(entryPath).catch(() => null);
7675
+ const entryPath = join12(toolDir, entry);
7676
+ const st = await stat8(entryPath).catch(() => null);
6376
7677
  if (!st?.isFile()) continue;
6377
7678
  tools.push({ path: entryPath, mtimeMs: st.mtimeMs });
6378
7679
  }
@@ -6386,10 +7687,10 @@ function getCursorProjectRoot(transcriptPath) {
6386
7687
  async function loadToolEvents(toolPaths) {
6387
7688
  const events = [];
6388
7689
  for (const path of toolPaths) {
6389
- const content = await readFile11(path, "utf-8").catch(() => "");
7690
+ const content = await readFile13(path, "utf-8").catch(() => "");
6390
7691
  const result = content.trim();
6391
7692
  if (!result) continue;
6392
- const st = await stat7(path).catch(() => null);
7693
+ const st = await stat8(path).catch(() => null);
6393
7694
  const id = basename4(path, extname2(path));
6394
7695
  events.push({
6395
7696
  id,
@@ -6532,8 +7833,8 @@ init_cloud();
6532
7833
  // src/publishers/gist.ts
6533
7834
  init_cloud();
6534
7835
  import { createHash as createHash2 } from "crypto";
6535
- import { readFile as readFile14, writeFile as writeFile4 } from "fs/promises";
6536
- import { join as join14 } from "path";
7836
+ import { readFile as readFile16, writeFile as writeFile4 } from "fs/promises";
7837
+ import { join as join15 } from "path";
6537
7838
  var GIST_META_FILE = ".vibe-replay-gist.json";
6538
7839
  function checkPublishStatus() {
6539
7840
  const auth = loadAuthToken();
@@ -6545,12 +7846,16 @@ async function publishGist(outputDir, title, opts) {
6545
7846
  if (!auth) {
6546
7847
  throw new Error("Not logged in. Run `vibe-replay auth login` first.");
6547
7848
  }
6548
- const jsonPath = join14(outputDir, "replay.json");
6549
- const content = await readFile14(jsonPath, "utf-8");
7849
+ const jsonPath = join15(outputDir, "replay.json");
7850
+ const content = await readFile16(jsonPath, "utf-8");
6550
7851
  const overwrite = opts?.overwrite;
6551
7852
  const filename = overwrite?.filename || `${sanitizeFilename(title)}.json`;
6552
7853
  const description = `vibe-replay: ${title}`;
6553
7854
  const apiUrl = getApiUrl();
7855
+ const headers = {
7856
+ "Content-Type": "application/json",
7857
+ Cookie: `${getSessionCookieName(apiUrl)}=${auth.token}`
7858
+ };
6554
7859
  let gistId;
6555
7860
  let gistUrl;
6556
7861
  let viewerUrl;
@@ -6558,10 +7863,7 @@ async function publishGist(outputDir, title, opts) {
6558
7863
  if (overwrite) {
6559
7864
  const resp = await fetch(`${apiUrl}/api/gists/${overwrite.gistId}`, {
6560
7865
  method: "PATCH",
6561
- headers: {
6562
- "Content-Type": "application/json",
6563
- Cookie: `${getSessionCookieName(apiUrl)}=${auth.token}`
6564
- },
7866
+ headers,
6565
7867
  body: JSON.stringify({ filename, content, description })
6566
7868
  });
6567
7869
  if (!resp.ok) {
@@ -6578,10 +7880,7 @@ async function publishGist(outputDir, title, opts) {
6578
7880
  } else {
6579
7881
  const resp = await fetch(`${apiUrl}/api/gists`, {
6580
7882
  method: "POST",
6581
- headers: {
6582
- "Content-Type": "application/json",
6583
- Cookie: `${getSessionCookieName(apiUrl)}=${auth.token}`
6584
- },
7883
+ headers,
6585
7884
  body: JSON.stringify({ filename, content, description, public: true })
6586
7885
  });
6587
7886
  if (!resp.ok) {
@@ -6612,7 +7911,7 @@ async function publishGist(outputDir, title, opts) {
6612
7911
  }
6613
7912
  async function loadSavedGistInfo(outputDir) {
6614
7913
  try {
6615
- const raw = await readFile14(join14(outputDir, GIST_META_FILE), "utf-8");
7914
+ const raw = await readFile16(join15(outputDir, GIST_META_FILE), "utf-8");
6616
7915
  const parsed = JSON.parse(raw);
6617
7916
  if (!parsed?.gistId || !parsed?.filename) return void 0;
6618
7917
  return parsed;
@@ -6621,7 +7920,7 @@ async function loadSavedGistInfo(outputDir) {
6621
7920
  }
6622
7921
  }
6623
7922
  async function saveGistInfo(outputDir, info) {
6624
- await writeFile4(join14(outputDir, GIST_META_FILE), JSON.stringify(info, null, 2), "utf-8");
7923
+ await writeFile4(join15(outputDir, GIST_META_FILE), JSON.stringify(info, null, 2), "utf-8");
6625
7924
  }
6626
7925
  function sanitizeFilename(s) {
6627
7926
  return s.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").slice(0, 60);
@@ -6732,21 +8031,21 @@ function scanForSecrets(content) {
6732
8031
  }
6733
8032
 
6734
8033
  // src/server.ts
6735
- import { execFile as execFile2 } from "child_process";
8034
+ import { execFile as execFile3 } from "child_process";
6736
8035
  import { createHash as createHash4 } from "crypto";
6737
8036
  import { watch as fsWatch } from "fs";
6738
8037
  import {
6739
8038
  mkdir as mkdir5,
6740
8039
  open as fsOpen,
6741
- readdir as readdir10,
6742
- readFile as readFile17,
6743
- stat as stat9,
8040
+ readdir as readdir11,
8041
+ readFile as readFile19,
8042
+ stat as stat10,
6744
8043
  unlink as unlink2,
6745
8044
  writeFile as writeFile6
6746
8045
  } from "fs/promises";
6747
- import { homedir as homedir15 } from "os";
6748
- import { basename as basename6, dirname as dirname5, join as join17, resolve as resolve2 } from "path";
6749
- import { promisify as promisify2 } from "util";
8046
+ import { homedir as homedir16 } from "os";
8047
+ import { basename as basename6, dirname as dirname5, join as join18, resolve as resolve2 } from "path";
8048
+ import { promisify as promisify3 } from "util";
6750
8049
  import { serve } from "@hono/node-server";
6751
8050
  import chalk from "chalk";
6752
8051
  import { Hono } from "hono";
@@ -6756,6 +8055,14 @@ import open2 from "open";
6756
8055
  // src/feedback.ts
6757
8056
  import { spawn } from "child_process";
6758
8057
  import { randomUUID } from "crypto";
8058
+ var IS_WINDOWS = process.platform === "win32";
8059
+ function spawnTool(command, args, options) {
8060
+ if (IS_WINDOWS) {
8061
+ const quoted = /\s/.test(command) ? `"${command}"` : command;
8062
+ return spawn(quoted, args, { ...options, shell: true });
8063
+ }
8064
+ return spawn(command, args, options);
8065
+ }
6759
8066
  var TOOL_PRIORITY = ["claude", "agent", "opencode"];
6760
8067
  async function detectFeedbackTools() {
6761
8068
  const insideClaude = !!process.env.CLAUDECODE;
@@ -6767,8 +8074,8 @@ async function detectFeedbackTools() {
6767
8074
  const tools = [];
6768
8075
  for (const tool of candidates) {
6769
8076
  try {
6770
- const path = await shell(`which ${tool.cmd}`);
6771
- if (path.trim()) tools.push({ name: tool.name, command: path.trim() });
8077
+ const located = await locateCommand(tool.cmd);
8078
+ if (located) tools.push({ name: tool.name, command: located });
6772
8079
  } catch {
6773
8080
  }
6774
8081
  }
@@ -6994,7 +8301,7 @@ async function executeFeedback(prompt, tool) {
6994
8301
  }
6995
8302
  function runClaude(prompt, cmd) {
6996
8303
  return new Promise((resolve3, reject) => {
6997
- const proc = spawn(cmd, ["-p", "--output-format", "json"], {
8304
+ const proc = spawnTool(cmd, ["-p", "--output-format", "json"], {
6998
8305
  env: { ...process.env, NO_COLOR: "1" },
6999
8306
  timeout: 6e5,
7000
8307
  stdio: ["pipe", "pipe", "pipe"]
@@ -7022,7 +8329,7 @@ function runClaude(prompt, cmd) {
7022
8329
  }
7023
8330
  function runOpencode(prompt, cmd) {
7024
8331
  return new Promise((resolve3, reject) => {
7025
- const proc = spawn(cmd, ["run"], {
8332
+ const proc = spawnTool(cmd, ["run"], {
7026
8333
  env: { ...process.env, NO_COLOR: "1", TERM: "dumb" },
7027
8334
  timeout: 6e5,
7028
8335
  stdio: ["pipe", "pipe", "pipe"]
@@ -7045,7 +8352,7 @@ function runOpencode(prompt, cmd) {
7045
8352
  }
7046
8353
  function runAgent(prompt, cmd) {
7047
8354
  return new Promise((resolve3, reject) => {
7048
- const proc = spawn(cmd, ["-p", "--output-format", "json", "--mode", "ask", "--trust"], {
8355
+ const proc = spawnTool(cmd, ["-p", "--output-format", "json", "--mode", "ask", "--trust"], {
7049
8356
  env: { ...process.env, NO_COLOR: "1" },
7050
8357
  timeout: 6e5,
7051
8358
  stdio: ["pipe", "pipe", "pipe"]
@@ -7398,10 +8705,13 @@ async function generateFeedback(session, tool, { debug = false } = {}) {
7398
8705
  }
7399
8706
  return { annotations: feedbackToAnnotations(result), result };
7400
8707
  }
7401
- function buildTranslationPrompt(session, opts) {
7402
- const translatableScenes = session.scenes.map(
8708
+ function collectTranslatableScenes(scenes) {
8709
+ return scenes.map(
7403
8710
  (s, i) => s.type === "user-prompt" || s.type === "text-response" ? { index: i, content: s.content } : null
7404
8711
  ).filter((s) => s !== null);
8712
+ }
8713
+ function buildTranslationPrompt(session, opts) {
8714
+ const translatableScenes = collectTranslatableScenes(session.scenes);
7405
8715
  const scenesBlock = translatableScenes.map((s) => `--- SCENE ${s.index} ---
7406
8716
  ${s.content}`).join("\n\n");
7407
8717
  const sourcePart = opts.sourceLang ? `from ${opts.sourceLang} ` : "";
@@ -7483,9 +8793,7 @@ function parseTranslationBatch(output, batchScenes, opts, now) {
7483
8793
  }
7484
8794
  async function generateTranslation(session, tool, opts) {
7485
8795
  if (session.scenes.length === 0) return null;
7486
- const allScenes = session.scenes.map(
7487
- (s, i) => s.type === "user-prompt" || s.type === "text-response" ? { index: i, content: s.content } : null
7488
- ).filter((s) => s !== null);
8796
+ const allScenes = collectTranslatableScenes(session.scenes);
7489
8797
  if (allScenes.length === 0) return null;
7490
8798
  const batches = [];
7491
8799
  for (let i = 0; i < allScenes.length; i += TRANSLATE_BATCH_SIZE) {
@@ -7654,14 +8962,19 @@ async function generateToneAdjustment(session, tool, opts) {
7654
8962
  function stripAnsi(str) {
7655
8963
  return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1B\][^\x07]*\x07/g, "");
7656
8964
  }
7657
- function shell(cmd) {
8965
+ function locateCommand(cmd) {
7658
8966
  return new Promise((resolve3, reject) => {
7659
- const proc = spawn("sh", ["-c", cmd], { timeout: 5e3 });
8967
+ const locator = IS_WINDOWS ? "where" : "which";
8968
+ const proc = spawn(locator, [cmd], { timeout: 5e3 });
7660
8969
  let out = "";
7661
8970
  proc.stdout.on("data", (d) => out += d.toString());
7662
8971
  proc.on("close", (code) => {
7663
- if (code === 0) resolve3(out);
7664
- else reject(new Error(`${cmd} failed`));
8972
+ if (code !== 0) {
8973
+ resolve3(null);
8974
+ return;
8975
+ }
8976
+ const first = out.split(/\r?\n/).find((line) => line.trim());
8977
+ resolve3(first ? first.trim() : null);
7665
8978
  });
7666
8979
  proc.on("error", reject);
7667
8980
  });
@@ -7673,12 +8986,30 @@ init_overlays();
7673
8986
  init_cloud();
7674
8987
 
7675
8988
  // src/scanner.ts
7676
- import { readdir as readdir9, readFile as readFile16, stat as stat8 } from "fs/promises";
7677
- import { homedir as homedir13 } from "os";
7678
- import { join as join16 } from "path";
8989
+ import { readdir as readdir10, readFile as readFile18, stat as stat9 } from "fs/promises";
8990
+ import { homedir as homedir14 } from "os";
8991
+ import { join as join17 } from "path";
7679
8992
 
7680
8993
  // src/pricing.ts
7681
8994
  var MODEL_PRICING = {
8995
+ "gpt-5.5": {
8996
+ inputRate: 5,
8997
+ outputRate: 30,
8998
+ cacheCreateRate: 5,
8999
+ cacheReadRate: 0.5
9000
+ },
9001
+ "gpt-5.4": {
9002
+ inputRate: 2.5,
9003
+ outputRate: 15,
9004
+ cacheCreateRate: 2.5,
9005
+ cacheReadRate: 0.25
9006
+ },
9007
+ "gpt-5.4-mini": {
9008
+ inputRate: 0.75,
9009
+ outputRate: 4.5,
9010
+ cacheCreateRate: 0.75,
9011
+ cacheReadRate: 0.075
9012
+ },
7682
9013
  // Opus 4.6/4.5
7683
9014
  "opus-4-new": {
7684
9015
  inputRate: 5,
@@ -7732,6 +9063,9 @@ var MODEL_PRICING = {
7732
9063
  var DEFAULT_PRICING = MODEL_PRICING.sonnet;
7733
9064
  function getModelPricing(model) {
7734
9065
  const lower = model.toLowerCase();
9066
+ if (lower.includes("gpt-5.4-mini")) return MODEL_PRICING["gpt-5.4-mini"];
9067
+ if (lower.includes("gpt-5.5")) return MODEL_PRICING["gpt-5.5"];
9068
+ if (lower.includes("gpt-5.4")) return MODEL_PRICING["gpt-5.4"];
7735
9069
  if (lower.includes("opus-4-6") || lower.includes("opus-4-5")) return MODEL_PRICING["opus-4-new"];
7736
9070
  if (lower.includes("opus")) return MODEL_PRICING.opus;
7737
9071
  if (lower.includes("sonnet-4-6") || lower.includes("sonnet-4-5"))
@@ -7743,6 +9077,9 @@ function getModelPricing(model) {
7743
9077
  return DEFAULT_PRICING;
7744
9078
  }
7745
9079
  var MODEL_CONTEXT_LIMITS = {
9080
+ "gpt-5.5": 27e4,
9081
+ "gpt-5.4-mini": 27e4,
9082
+ "gpt-5.4": 27e4,
7746
9083
  "gpt-4o": 128e3,
7747
9084
  "gpt-4-turbo": 128e3,
7748
9085
  "gpt-4": 8192,
@@ -7856,7 +9193,7 @@ async function scanSession(input) {
7856
9193
  for (const filePath of input.filePaths) {
7857
9194
  let content;
7858
9195
  try {
7859
- content = await readFile16(filePath, "utf-8");
9196
+ content = await readFile18(filePath, "utf-8");
7860
9197
  } catch {
7861
9198
  continue;
7862
9199
  }
@@ -7911,6 +9248,9 @@ async function scanSession(input) {
7911
9248
  if (obj.subtype === "api_error") apiErrorCount++;
7912
9249
  continue;
7913
9250
  }
9251
+ if (obj.type === "assistant" && obj.isApiErrorMessage) {
9252
+ apiErrorCount++;
9253
+ }
7914
9254
  if (obj.isMeta) {
7915
9255
  const text = extractMetaText(obj.message?.content);
7916
9256
  if (text.startsWith("Base directory for this skill:")) {
@@ -7975,15 +9315,15 @@ async function scanSession(input) {
7975
9315
  if (input.filePaths.length > 0) {
7976
9316
  const mainFile = input.filePaths[0];
7977
9317
  const sessionDir = mainFile.replace(/\.jsonl$/, "");
7978
- const subagentsDir = join16(sessionDir, "subagents");
9318
+ const subagentsDir = join17(sessionDir, "subagents");
7979
9319
  try {
7980
- const files = await readdir9(subagentsDir);
9320
+ const files = await readdir10(subagentsDir);
7981
9321
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
7982
9322
  subAgentCount = jsonlFiles.length;
7983
9323
  for (const saFile of jsonlFiles) {
7984
9324
  let saContent;
7985
9325
  try {
7986
- saContent = await readFile16(join16(subagentsDir, saFile), "utf-8");
9326
+ saContent = await readFile18(join17(subagentsDir, saFile), "utf-8");
7987
9327
  } catch {
7988
9328
  continue;
7989
9329
  }
@@ -8170,7 +9510,7 @@ function buildScanResultFromParsed(input, parsed) {
8170
9510
  let derivedSubAgentCount = 0;
8171
9511
  const fileEditCounts = /* @__PURE__ */ new Map();
8172
9512
  for (const turn of parsed.turns) {
8173
- if (turn.role === "user" && turn.subtype !== "compaction-summary") {
9513
+ if (turn.role === "user" && !turn.subtype) {
8174
9514
  const hasText = turn.blocks.some(
8175
9515
  (block) => block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0
8176
9516
  );
@@ -8193,16 +9533,18 @@ function buildScanResultFromParsed(input, parsed) {
8193
9533
  const saPath = extractToolFilePath(saScene.input);
8194
9534
  if (!saPath) continue;
8195
9535
  editCount++;
8196
- const short2 = shortenPath(saPath);
8197
- fileEditCounts.set(short2, (fileEditCounts.get(short2) || 0) + 1);
9536
+ const short = shortenPath(saPath);
9537
+ fileEditCounts.set(short, (fileEditCounts.get(short) || 0) + 1);
8198
9538
  }
8199
9539
  }
8200
9540
  if (!FILE_EDIT_TOOLS.has(block.name)) continue;
8201
- const rawPath = extractToolFilePath(block.input);
8202
- if (!rawPath) continue;
9541
+ const rawPaths = extractToolFilePaths(block.input);
9542
+ if (rawPaths.length === 0) continue;
8203
9543
  editCount++;
8204
- const short = shortenPath(rawPath);
8205
- fileEditCounts.set(short, (fileEditCounts.get(short) || 0) + 1);
9544
+ for (const rawPath of rawPaths) {
9545
+ const short = shortenPath(rawPath);
9546
+ fileEditCounts.set(short, (fileEditCounts.get(short) || 0) + 1);
9547
+ }
8206
9548
  }
8207
9549
  }
8208
9550
  const costEstimate = estimateParsedCost(parsed);
@@ -8244,7 +9586,7 @@ function buildScanResultFromParsed(input, parsed) {
8244
9586
  }
8245
9587
  function firstUserPrompt(turns) {
8246
9588
  for (const turn of turns) {
8247
- if (turn.role !== "user" || turn.subtype === "compaction-summary") continue;
9589
+ if (turn.role !== "user" || turn.subtype) continue;
8248
9590
  for (const block of turn.blocks) {
8249
9591
  if (block.type !== "text") continue;
8250
9592
  const text = block.text.replace(/\s+/g, " ").trim();
@@ -8274,7 +9616,7 @@ async function getFileMeta(filePaths) {
8274
9616
  let maxMtime = 0;
8275
9617
  for (const fp of filePaths) {
8276
9618
  try {
8277
- const s = await stat8(fp);
9619
+ const s = await stat9(fp);
8278
9620
  totalSize += s.size;
8279
9621
  if (s.mtimeMs > maxMtime) maxMtime = s.mtimeMs;
8280
9622
  } catch {
@@ -8627,33 +9969,33 @@ function buildAggregateDataQuality(scans) {
8627
9969
  }
8628
9970
  return notes.length > 0 ? { notes } : void 0;
8629
9971
  }
8630
- var CLAUDE_DIR2 = join16(homedir13(), ".claude", "projects");
9972
+ var CLAUDE_DIR2 = join17(homedir14(), ".claude", "projects");
8631
9973
  function encodeProjectDir(project) {
8632
9974
  let resolved = project;
8633
9975
  if (resolved.startsWith("~/")) {
8634
- resolved = join16(homedir13(), resolved.slice(2));
9976
+ resolved = join17(homedir14(), resolved.slice(2));
8635
9977
  } else if (resolved === "~") {
8636
- resolved = homedir13();
9978
+ resolved = homedir14();
8637
9979
  }
8638
9980
  return resolved.replace(/\//g, "-");
8639
9981
  }
8640
9982
  async function readProjectMemory(project) {
8641
9983
  const encoded = encodeProjectDir(project);
8642
- const projectDir = join16(CLAUDE_DIR2, encoded);
8643
- const memoryDir = join16(projectDir, "memory");
9984
+ const projectDir = join17(CLAUDE_DIR2, encoded);
9985
+ const memoryDir = join17(projectDir, "memory");
8644
9986
  const memoryFiles = [];
8645
9987
  let claudeMd;
8646
9988
  try {
8647
- const content = await readFile16(join16(projectDir, "CLAUDE.md"), "utf-8");
9989
+ const content = await readFile18(join17(projectDir, "CLAUDE.md"), "utf-8");
8648
9990
  if (content.trim()) claudeMd = content.slice(0, 5e3);
8649
9991
  } catch {
8650
9992
  }
8651
9993
  try {
8652
- const files = await readdir9(memoryDir);
9994
+ const files = await readdir10(memoryDir);
8653
9995
  for (const file of files) {
8654
9996
  if (!file.endsWith(".md") || file === "MEMORY.md") continue;
8655
9997
  try {
8656
- const content = await readFile16(join16(memoryDir, file), "utf-8");
9998
+ const content = await readFile18(join17(memoryDir, file), "utf-8");
8657
9999
  const fm = parseFrontmatter(content);
8658
10000
  memoryFiles.push({
8659
10001
  name: fm.name || file.replace(/\.md$/, ""),
@@ -8694,7 +10036,7 @@ function extractMetaText(content) {
8694
10036
  }
8695
10037
 
8696
10038
  // src/transform.ts
8697
- import { homedir as homedir14 } from "os";
10039
+ import { homedir as homedir15 } from "os";
8698
10040
 
8699
10041
  // src/utils/tokenEstimate.ts
8700
10042
  function estimateTokens(s) {
@@ -8704,11 +10046,15 @@ function estimateTokens(s) {
8704
10046
  }
8705
10047
 
8706
10048
  // src/transform.ts
8707
- var HOME = homedir14();
10049
+ var HOME = homedir15();
8708
10050
  function redactPath(s) {
8709
10051
  if (!HOME) return s;
8710
10052
  return s.replaceAll(HOME, "~");
8711
10053
  }
10054
+ function redactFilePath(s) {
10055
+ const redacted = redactPath(s);
10056
+ return process.platform === "win32" ? redacted.replaceAll("\\", "/") : redacted;
10057
+ }
8712
10058
  function transformToReplay(parsed, provider, project, options) {
8713
10059
  const scenes = [];
8714
10060
  let userPrompts = 0;
@@ -8834,7 +10180,7 @@ function transformToReplay(parsed, provider, project, options) {
8834
10180
  startTime: parsed.startTime || (/* @__PURE__ */ new Date()).toISOString(),
8835
10181
  endTime: parsed.endTime,
8836
10182
  model: parsed.model,
8837
- cwd: redactPath(parsed.cwd),
10183
+ cwd: redactFilePath(parsed.cwd),
8838
10184
  project,
8839
10185
  ...options?.generator ? { generator: options.generator } : {},
8840
10186
  stats: {
@@ -8847,7 +10193,7 @@ function transformToReplay(parsed, provider, project, options) {
8847
10193
  costEstimate,
8848
10194
  ...parsed.turnStats ? { turnStats: parsed.turnStats } : {}
8849
10195
  },
8850
- ...parsed.model ? { contextLimit: getModelContextLimit(parsed.model) } : {},
10196
+ ...parsed.contextLimit || parsed.model ? { contextLimit: parsed.contextLimit || getModelContextLimit(parsed.model || "") } : {},
8851
10197
  ...parsed.tokenUsageByModel ? { tokenUsageByModel: parsed.tokenUsageByModel } : {},
8852
10198
  ...parsed.prLinks && parsed.prLinks.length > 0 ? { prLinks: parsed.prLinks } : {},
8853
10199
  compactions: parsed.compactions,
@@ -8856,10 +10202,19 @@ function transformToReplay(parsed, provider, project, options) {
8856
10202
  ...parsed.gitBranches ? { gitBranches: parsed.gitBranches } : {},
8857
10203
  ...parsed.entrypoint ? { entrypoint: parsed.entrypoint } : {},
8858
10204
  ...parsed.permissionMode ? { permissionMode: parsed.permissionMode } : {},
10205
+ ...parsed.memoryMode ? { memoryMode: parsed.memoryMode } : {},
8859
10206
  ...parsed.apiErrors && parsed.apiErrors.length > 0 ? { apiErrors: parsed.apiErrors } : {},
8860
- ...parsed.trackedFiles && parsed.trackedFiles.length > 0 ? { trackedFiles: parsed.trackedFiles.map(redactPath) } : {},
8861
- ...parsed.contextFiles && parsed.contextFiles.length > 0 ? { contextFiles: parsed.contextFiles.map(redactPath) } : {},
10207
+ ...parsed.trackedFiles && parsed.trackedFiles.length > 0 ? { trackedFiles: parsed.trackedFiles.map(redactFilePath) } : {},
10208
+ ...parsed.contextFiles && parsed.contextFiles.length > 0 ? { contextFiles: parsed.contextFiles.map(redactFilePath) } : {},
8862
10209
  ...parsed.cursorSidecars ? { cursorSidecars: parsed.cursorSidecars } : {},
10210
+ ...parsed.parseWarnings && parsed.parseWarnings.length > 0 ? {
10211
+ parseWarnings: parsed.parseWarnings.map((warning) => ({
10212
+ ...warning,
10213
+ message: redactWarningText(warning.message),
10214
+ ...warning.source ? { source: redactWarningText(warning.source) } : {},
10215
+ sample: warning.kind !== "malformed-json" && warning.sample ? compactWarningSample(redactWarningText(warning.sample)) : void 0
10216
+ }))
10217
+ } : {},
8863
10218
  ...parsed.serviceTier ? { serviceTier: parsed.serviceTier } : {},
8864
10219
  ...parsed.skillsUsed ? { skillsUsed: parsed.skillsUsed } : {},
8865
10220
  ...parsed.mcpServersUsed ? { mcpServersUsed: parsed.mcpServersUsed } : {},
@@ -8874,9 +10229,9 @@ function transformToReplay(parsed, provider, project, options) {
8874
10229
  function buildFileDiff(toolName, input) {
8875
10230
  if (toolName === "Edit" && input.file_path) {
8876
10231
  return {
8877
- filePath: redactPath(input.file_path),
8878
- oldContent: input.old_string ?? "",
8879
- newContent: input.new_string ?? ""
10232
+ filePath: redactFilePath(input.file_path),
10233
+ oldContent: redactDiffContent(input.old_string),
10234
+ newContent: redactDiffContent(input.new_string)
8880
10235
  };
8881
10236
  }
8882
10237
  if (toolName === "MultiEdit" && input.file_path && Array.isArray(input.edits)) {
@@ -8885,28 +10240,49 @@ function buildFileDiff(toolName, input) {
8885
10240
  });
8886
10241
  if (edits.length > 0) {
8887
10242
  return {
8888
- filePath: redactPath(input.file_path),
8889
- oldContent: edits.map((edit) => edit.old_string ?? "").join("\n\n"),
8890
- newContent: edits.map((edit) => edit.new_string ?? "").join("\n\n")
10243
+ filePath: redactFilePath(input.file_path),
10244
+ oldContent: edits.map((edit) => redactDiffContent(edit.old_string)).join("\n\n"),
10245
+ newContent: edits.map((edit) => redactDiffContent(edit.new_string)).join("\n\n")
8891
10246
  };
8892
10247
  }
8893
10248
  }
8894
10249
  if (toolName === "Write" && input.file_path) {
8895
10250
  return {
8896
- filePath: redactPath(input.file_path),
10251
+ filePath: redactFilePath(input.file_path),
8897
10252
  oldContent: "",
8898
- newContent: truncate(input.content || "", 3e3)
10253
+ newContent: truncate(redactDiffContent(input.content), 3e3)
8899
10254
  };
8900
10255
  }
8901
10256
  if (toolName === "Delete" && input.file_path) {
8902
10257
  return {
8903
- filePath: redactPath(input.file_path),
8904
- oldContent: input.old_string ?? "(file deleted)",
10258
+ filePath: redactFilePath(input.file_path),
10259
+ oldContent: typeof input.old_string === "string" ? redactDiffContent(input.old_string) : "(file deleted)",
8905
10260
  newContent: ""
8906
10261
  };
8907
10262
  }
8908
10263
  return void 0;
8909
10264
  }
10265
+ function redactDiffContent(value) {
10266
+ return typeof value === "string" ? redactSecrets(redactPath(value)) : "";
10267
+ }
10268
+ function redactWarningText(value) {
10269
+ return redactSecrets(redactWarningPaths(redactPath(value))).replace(
10270
+ /[A-Za-z0-9_-]{4,}\.\.\.\[REDACTED\]/g,
10271
+ "[REDACTED]"
10272
+ );
10273
+ }
10274
+ function redactWarningPaths(value) {
10275
+ let result = value;
10276
+ if (HOME) {
10277
+ const slashEscapedHome = HOME.replaceAll("/", "\\/");
10278
+ result = result.replaceAll(slashEscapedHome, "~");
10279
+ const backslashEscapedHome = HOME.replaceAll("\\", "\\\\");
10280
+ if (backslashEscapedHome !== HOME) {
10281
+ result = result.replaceAll(backslashEscapedHome, "~");
10282
+ }
10283
+ }
10284
+ return result.replace(/[A-Za-z]:(?:\\\\|\\)Users(?:\\\\|\\)[^\\/"\s]+/g, "~");
10285
+ }
8910
10286
  function buildToolScene(toolName, input, result, images) {
8911
10287
  const resultTokens = estimateTokens(result);
8912
10288
  const scene = {
@@ -9123,7 +10499,7 @@ function getErrorMessage(err) {
9123
10499
  return "Unknown error";
9124
10500
  }
9125
10501
  function normalizeProjectPath(project) {
9126
- const home = homedir15();
10502
+ const home = homedir16();
9127
10503
  return project.startsWith(home) ? `~${project.slice(home.length)}` : project;
9128
10504
  }
9129
10505
  var MAX_INSIGHTS_SYNC_DAYS_PER_REQUEST = 90;
@@ -9194,20 +10570,20 @@ function resolveGenerateInputs(body, discoveredSessions) {
9194
10570
  var ARCHIVE_DIR = ".archive";
9195
10571
  async function getArchivedSlugs(baseDir) {
9196
10572
  try {
9197
- const entries = await readdir10(join17(baseDir, ARCHIVE_DIR));
10573
+ const entries = await readdir11(join18(baseDir, ARCHIVE_DIR));
9198
10574
  return new Set(entries);
9199
10575
  } catch {
9200
10576
  return /* @__PURE__ */ new Set();
9201
10577
  }
9202
10578
  }
9203
10579
  async function archiveSlug(baseDir, slug) {
9204
- const dir = join17(baseDir, ARCHIVE_DIR);
10580
+ const dir = join18(baseDir, ARCHIVE_DIR);
9205
10581
  await mkdir5(dir, { recursive: true });
9206
- await writeFile6(join17(dir, slug), "");
10582
+ await writeFile6(join18(dir, slug), "");
9207
10583
  }
9208
10584
  async function unarchiveSlug(baseDir, slug) {
9209
10585
  try {
9210
- await unlink2(join17(baseDir, ARCHIVE_DIR, slug));
10586
+ await unlink2(join18(baseDir, ARCHIVE_DIR, slug));
9211
10587
  } catch {
9212
10588
  }
9213
10589
  }
@@ -9215,29 +10591,29 @@ async function scanSessionsFromDir(baseDir) {
9215
10591
  const results = [];
9216
10592
  let entries;
9217
10593
  try {
9218
- entries = await readdir10(baseDir);
10594
+ entries = await readdir11(baseDir);
9219
10595
  } catch {
9220
10596
  return results;
9221
10597
  }
9222
10598
  for (const entry of entries) {
9223
- const replayPath = join17(baseDir, entry, "replay.json");
10599
+ const replayPath = join18(baseDir, entry, "replay.json");
9224
10600
  try {
9225
- const raw = await readFile17(replayPath, "utf-8");
10601
+ const raw = await readFile19(replayPath, "utf-8");
9226
10602
  const session = JSON.parse(raw);
9227
- const annotationsPath = join17(baseDir, entry, "annotations.json");
10603
+ const annotationsPath = join18(baseDir, entry, "annotations.json");
9228
10604
  let annotationCount = 0;
9229
10605
  try {
9230
- const annRaw = await readFile17(annotationsPath, "utf-8");
10606
+ const annRaw = await readFile19(annotationsPath, "utf-8");
9231
10607
  const anns = JSON.parse(annRaw);
9232
10608
  annotationCount = Array.isArray(anns) ? anns.length : 0;
9233
10609
  } catch {
9234
10610
  }
9235
10611
  let gist;
9236
10612
  try {
9237
- gist = await loadSavedGistInfo(join17(baseDir, entry));
10613
+ gist = await loadSavedGistInfo(join18(baseDir, entry));
9238
10614
  } catch {
9239
10615
  }
9240
- const cloudInfo = await loadSavedCloudInfo(join17(baseDir, entry));
10616
+ const cloudInfo = await loadSavedCloudInfo(join18(baseDir, entry));
9241
10617
  const userPrompts = (session.scenes || []).filter((sc) => sc.type === "user-prompt").map((sc) => previewPrompt(sc.content)).filter((m) => m.length >= 10);
9242
10618
  const firstMessage = userPrompts[0] || void 0;
9243
10619
  const messages = userPrompts.length > 0 ? userPrompts.slice(0, 2) : void 0;
@@ -9265,7 +10641,7 @@ async function scanSessionsFromDir(baseDir) {
9265
10641
  let outdated = false;
9266
10642
  if (gist?.contentHash) {
9267
10643
  try {
9268
- const content = await readFile17(replayPath, "utf-8");
10644
+ const content = await readFile19(replayPath, "utf-8");
9269
10645
  const currentHash = createHash4("sha256").update(content).digest("hex").slice(0, 16);
9270
10646
  outdated = currentHash !== gist?.contentHash;
9271
10647
  } catch {
@@ -9311,20 +10687,20 @@ async function scanSessions(baseDir) {
9311
10687
  return allResults;
9312
10688
  }
9313
10689
  async function loadSessionFromDisk(baseDir, slug) {
9314
- let replayPath = join17(baseDir, slug, "replay.json");
10690
+ let replayPath = join18(baseDir, slug, "replay.json");
9315
10691
  try {
9316
- await stat9(replayPath);
10692
+ await stat10(replayPath);
9317
10693
  } catch {
9318
10694
  const fallback = resolve2("./vibe-replay", slug, "replay.json");
9319
- await stat9(fallback);
10695
+ await stat10(fallback);
9320
10696
  replayPath = fallback;
9321
10697
  }
9322
- const raw = await readFile17(replayPath, "utf-8");
10698
+ const raw = await readFile19(replayPath, "utf-8");
9323
10699
  const session = JSON.parse(raw);
9324
10700
  const sessionDir = dirname5(replayPath);
9325
- const annotationsPath = join17(sessionDir, "annotations.json");
10701
+ const annotationsPath = join18(sessionDir, "annotations.json");
9326
10702
  try {
9327
- const annRaw = await readFile17(annotationsPath, "utf-8");
10703
+ const annRaw = await readFile19(annotationsPath, "utf-8");
9328
10704
  const anns = JSON.parse(annRaw);
9329
10705
  if (Array.isArray(anns) && anns.length > 0) {
9330
10706
  session.annotations = anns;
@@ -9340,7 +10716,12 @@ function pickSourceRecordForSession(session, bySessionId, byKey) {
9340
10716
  const byIdMatch = bySessionId.get(session.sessionId);
9341
10717
  return (byIdMatch?.provider === session.provider ? byIdMatch : void 0) ?? byKey.get(sourceSessionKey(session.provider, session.project, session.slug));
9342
10718
  }
9343
- function selectCursorEnrichmentCandidates(merged, baseSources, limit = 30) {
10719
+ function selectCursorEnrichmentCandidates(merged, baseSources, limitOrHints = 30) {
10720
+ const hints = typeof limitOrHints === "number" ? { limit: limitOrHints } : limitOrHints;
10721
+ const limit = Math.max(1, Math.min(hints.limit ?? 30, 100));
10722
+ const preferredSessionIds = new Set(hints.sessionIds || []);
10723
+ const preferredSlugs = new Set(hints.slugs || []);
10724
+ const preferredProjects = new Set(hints.projects || []);
9344
10725
  const mergedBySessionId = /* @__PURE__ */ new Map();
9345
10726
  const mergedByKey = /* @__PURE__ */ new Map();
9346
10727
  for (const session of merged) {
@@ -9352,7 +10733,91 @@ function selectCursorEnrichmentCandidates(merged, baseSources, limit = 30) {
9352
10733
  ).map((s) => {
9353
10734
  const byId = s.sessionId ? mergedBySessionId.get(s.sessionId) : void 0;
9354
10735
  return byId || mergedByKey.get(sourceSessionKey(s.provider, s.project, s.slug));
9355
- }).filter((s) => Boolean(s)).sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, limit);
10736
+ }).filter((s) => Boolean(s)).sort((a, b) => {
10737
+ const scoreDelta = enrichmentPriorityScore(b, preferredSessionIds, preferredSlugs, preferredProjects) - enrichmentPriorityScore(a, preferredSessionIds, preferredSlugs, preferredProjects);
10738
+ return scoreDelta || b.timestamp.localeCompare(a.timestamp);
10739
+ }).slice(0, limit);
10740
+ }
10741
+ function enrichmentPriorityScore(session, preferredSessionIds, preferredSlugs, preferredProjects) {
10742
+ let score = 0;
10743
+ if (preferredSessionIds.has(session.sessionId)) score += 1e3;
10744
+ if (preferredSlugs.has(session.slug)) score += 800;
10745
+ if (preferredProjects.has(session.project)) score += 400;
10746
+ if (session.timestamp && Date.now() - Date.parse(session.timestamp) <= 24 * 60 * 60 * 1e3) {
10747
+ score += 100;
10748
+ }
10749
+ if (session.hasPR) score += 25;
10750
+ if (session.hasSqlite) score += 10;
10751
+ return score;
10752
+ }
10753
+ function prioritizeScanInputs(inputs, previousResults, hints = {}) {
10754
+ const previousSessionIds = new Set(previousResults.map((result) => result.sessionId));
10755
+ const preferredSessionIds = new Set(hints.sessionIds || []);
10756
+ const preferredSlugs = new Set(hints.slugs || []);
10757
+ const preferredProjects = new Set(hints.projects || []);
10758
+ return [...inputs].sort((a, b) => {
10759
+ const scoreDelta = scanInputPriorityScore(
10760
+ b,
10761
+ previousSessionIds,
10762
+ preferredSessionIds,
10763
+ preferredSlugs,
10764
+ preferredProjects
10765
+ ) - scanInputPriorityScore(
10766
+ a,
10767
+ previousSessionIds,
10768
+ preferredSessionIds,
10769
+ preferredSlugs,
10770
+ preferredProjects
10771
+ );
10772
+ return scoreDelta || (b.timestamp || "").localeCompare(a.timestamp || "");
10773
+ });
10774
+ }
10775
+ function scanInputPriorityScore(input, previousSessionIds, preferredSessionIds, preferredSlugs, preferredProjects) {
10776
+ let score = 0;
10777
+ if (!previousSessionIds.has(input.sessionId)) score += 1e3;
10778
+ if (preferredSessionIds.has(input.sessionId)) score += 2e3;
10779
+ if (preferredSlugs.has(input.slug)) score += 1600;
10780
+ if (preferredProjects.has(input.project)) score += 400;
10781
+ if (input.timestamp && Date.now() - Date.parse(input.timestamp) <= 24 * 60 * 60 * 1e3) {
10782
+ score += 100;
10783
+ }
10784
+ return score;
10785
+ }
10786
+ function mergeEnrichmentHints(existing, next) {
10787
+ return {
10788
+ sessionIds: uniqueStrings([...existing?.sessionIds || [], ...next.sessionIds || []]),
10789
+ slugs: uniqueStrings([...existing?.slugs || [], ...next.slugs || []]),
10790
+ projects: uniqueStrings([...existing?.projects || [], ...next.projects || []]),
10791
+ limit: Math.max(existing?.limit || 0, next.limit || 0) || void 0
10792
+ };
10793
+ }
10794
+ function uniqueStrings(values) {
10795
+ return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))].slice(
10796
+ 0,
10797
+ 200
10798
+ );
10799
+ }
10800
+ function enrichmentHintsFromBody(value) {
10801
+ if (!value || typeof value !== "object") return {};
10802
+ const body = value;
10803
+ return {
10804
+ sessionIds: stringArrayFromUnknown(body.sessionIds),
10805
+ slugs: stringArrayFromUnknown(body.slugs),
10806
+ projects: stringArrayFromUnknown(body.projects),
10807
+ limit: typeof body.limit === "number" ? body.limit : void 0
10808
+ };
10809
+ }
10810
+ function stringArrayFromUnknown(value) {
10811
+ if (!Array.isArray(value)) return void 0;
10812
+ const strings = value.filter(
10813
+ (item) => typeof item === "string" && item.trim().length > 0
10814
+ );
10815
+ return strings.length > 0 ? [...new Set(strings)] : void 0;
10816
+ }
10817
+ function normalizeSessionProjectsForHome(sessions, home) {
10818
+ return sessions.map(
10819
+ (session) => session.project.startsWith(home) ? { ...session, project: `~${session.project.slice(home.length)}` } : { ...session }
10820
+ );
9356
10821
  }
9357
10822
  function countSessionStats(turns) {
9358
10823
  let promptCount = 0;
@@ -9408,13 +10873,13 @@ async function buildSourcesResult(merged, baseDir, home, previousSources = [], c
9408
10873
  const projectExistsMap = /* @__PURE__ */ new Map();
9409
10874
  const projectIsGitMap = /* @__PURE__ */ new Map();
9410
10875
  for (const p of uniqueProjects) {
9411
- const resolved = p.startsWith("~/") ? join17(home, p.slice(2)) : p === "~" ? home : p;
10876
+ const resolved = p === "~" ? home : p.startsWith("~/") || p.startsWith("~\\") ? join18(home, p.slice(2)) : p;
9412
10877
  try {
9413
- const s = await stat9(resolved);
10878
+ const s = await stat10(resolved);
9414
10879
  projectExistsMap.set(p, s.isDirectory());
9415
10880
  if (s.isDirectory()) {
9416
10881
  try {
9417
- await stat9(join17(resolved, ".git"));
10882
+ await stat10(join18(resolved, ".git"));
9418
10883
  projectIsGitMap.set(p, true);
9419
10884
  } catch {
9420
10885
  projectIsGitMap.set(p, false);
@@ -9456,11 +10921,18 @@ async function buildSourcesResult(merged, baseDir, home, previousSources = [], c
9456
10921
  filePaths: s.filePaths,
9457
10922
  toolPaths: s.toolPaths,
9458
10923
  hasSqlite: s.hasSqlite,
10924
+ hasSdk: s.hasSdk,
9459
10925
  gitBranch: s.gitBranch,
9460
10926
  model: s.model,
9461
10927
  durationMsEst: s.durationMsEst,
9462
10928
  editCountEst: s.editCountEst,
9463
10929
  hasPR: s.hasPR,
10930
+ isStarred: s.isStarred,
10931
+ spaceId: s.spaceId,
10932
+ spaceIdSetBy: s.spaceIdSetBy,
10933
+ pluginsEnabled: s.pluginsEnabled,
10934
+ skillsEnabled: s.skillsEnabled,
10935
+ fsDetectedFiles: s.fsDetectedFiles,
9464
10936
  expiresInDays: s.provider === "claude-code" && cleanupPeriodDays > 0 ? computeDaysUntilCleanup(s.timestamp, cleanupPeriodDays) : void 0,
9465
10937
  existingReplay: replay ? replay.slug : null,
9466
10938
  projectExists: projectExistsMap.get(s.project) ?? false,
@@ -9487,10 +10959,10 @@ async function buildSourcesResult(merged, baseDir, home, previousSources = [], c
9487
10959
  });
9488
10960
  }
9489
10961
  async function readClaudeSessionState(sessionId) {
9490
- const sessionsDir = join17(homedir15(), ".claude", "sessions");
10962
+ const sessionsDir = join18(homedir16(), ".claude", "sessions");
9491
10963
  let files;
9492
10964
  try {
9493
- files = await readdir10(sessionsDir);
10965
+ files = await readdir11(sessionsDir);
9494
10966
  } catch {
9495
10967
  return "stopped";
9496
10968
  }
@@ -9498,7 +10970,7 @@ async function readClaudeSessionState(sessionId) {
9498
10970
  if (!file.endsWith(".json")) continue;
9499
10971
  let data;
9500
10972
  try {
9501
- const content = await readFile17(join17(sessionsDir, file), "utf-8");
10973
+ const content = await readFile19(join18(sessionsDir, file), "utf-8");
9502
10974
  data = JSON.parse(content);
9503
10975
  } catch {
9504
10976
  continue;
@@ -9548,10 +11020,10 @@ function mergeSameSessions(sessions) {
9548
11020
  return result;
9549
11021
  }
9550
11022
  async function loadAnnotations(baseDir, slug) {
9551
- const dirs = [join17(baseDir, slug), resolve2("./vibe-replay", slug)];
11023
+ const dirs = [join18(baseDir, slug), resolve2("./vibe-replay", slug)];
9552
11024
  for (const dir of dirs) {
9553
11025
  try {
9554
- const raw = await readFile17(join17(dir, "annotations.json"), "utf-8");
11026
+ const raw = await readFile19(join18(dir, "annotations.json"), "utf-8");
9555
11027
  const anns = JSON.parse(raw);
9556
11028
  if (Array.isArray(anns)) return anns;
9557
11029
  } catch {
@@ -9560,11 +11032,11 @@ async function loadAnnotations(baseDir, slug) {
9560
11032
  return [];
9561
11033
  }
9562
11034
  async function saveAnnotations(baseDir, slug, annotations) {
9563
- const annPath = join17(baseDir, slug, "annotations.json");
11035
+ const annPath = join18(baseDir, slug, "annotations.json");
9564
11036
  await writeFile6(annPath, JSON.stringify(annotations, null, 2), "utf-8");
9565
11037
  }
9566
11038
  async function saveOverlays(baseDir, slug, overlays) {
9567
- const overlayPath = join17(baseDir, slug, "overlays.json");
11039
+ const overlayPath = join18(baseDir, slug, "overlays.json");
9568
11040
  await writeFile6(overlayPath, JSON.stringify(overlays, null, 2), "utf-8");
9569
11041
  }
9570
11042
  async function startServer(baseDir, opts) {
@@ -9573,7 +11045,7 @@ async function startServer(baseDir, opts) {
9573
11045
  const viewerHtml = isDevMode ? "" : await loadViewerHtml();
9574
11046
  const cleanupPeriodDays = await getClaudeCodeCleanupPeriod();
9575
11047
  const cacheKeySuffix = createHash4("sha1").update(baseDir).digest("hex").slice(0, 12);
9576
- const sourcesCacheKey = `dashboard-sources-v3-${cacheKeySuffix}`;
11048
+ const sourcesCacheKey = `dashboard-sources-v4-${cacheKeySuffix}`;
9577
11049
  const replaysCacheKey = `dashboard-replays-v1-${cacheKeySuffix}`;
9578
11050
  const scanResultsCacheKey = `dashboard-scan-results-v1-${cacheKeySuffix}`;
9579
11051
  const insightsCacheKey = `dashboard-insights-v1-${cacheKeySuffix}`;
@@ -9634,11 +11106,20 @@ async function startServer(baseDir, opts) {
9634
11106
  total: 0,
9635
11107
  updated: 0
9636
11108
  };
9637
- const enrichCursorStatsInBackground = (merged, baseSources) => {
9638
- if (sourcesEnrichmentStatus.running) return;
11109
+ let pendingSourcesEnrichment = null;
11110
+ let lastDiscoveredMergedSessions = [];
11111
+ const enrichCursorStatsInBackground = (merged, baseSources, hints = {}) => {
11112
+ if (sourcesEnrichmentStatus.running) {
11113
+ pendingSourcesEnrichment = {
11114
+ merged,
11115
+ baseSources,
11116
+ hints: mergeEnrichmentHints(pendingSourcesEnrichment?.hints, hints)
11117
+ };
11118
+ return;
11119
+ }
9639
11120
  const cursorProvider2 = getProvider("cursor");
9640
11121
  if (!cursorProvider2) return;
9641
- const candidates = selectCursorEnrichmentCandidates(merged, baseSources);
11122
+ const candidates = selectCursorEnrichmentCandidates(merged, baseSources, hints);
9642
11123
  sourcesEnrichmentStatus = {
9643
11124
  running: true,
9644
11125
  processed: 0,
@@ -9655,9 +11136,9 @@ async function startServer(baseDir, opts) {
9655
11136
  };
9656
11137
  return;
9657
11138
  }
11139
+ const enrichedSources = baseSources.map((s) => ({ ...s }));
9658
11140
  void (async () => {
9659
11141
  let changed = false;
9660
- const enrichedSources = baseSources.map((s) => ({ ...s }));
9661
11142
  const bySessionId = /* @__PURE__ */ new Map();
9662
11143
  const byKey = /* @__PURE__ */ new Map();
9663
11144
  for (const source of enrichedSources) {
@@ -9733,6 +11214,11 @@ async function startServer(baseDir, opts) {
9733
11214
  running: false,
9734
11215
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
9735
11216
  };
11217
+ const pending = pendingSourcesEnrichment;
11218
+ pendingSourcesEnrichment = null;
11219
+ if (pending) {
11220
+ enrichCursorStatsInBackground(pending.merged, enrichedSources, pending.hints);
11221
+ }
9736
11222
  })().catch(() => {
9737
11223
  sourcesEnrichmentStatus = {
9738
11224
  ...sourcesEnrichmentStatus,
@@ -9740,6 +11226,11 @@ async function startServer(baseDir, opts) {
9740
11226
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
9741
11227
  message: "Cursor stat backfill failed"
9742
11228
  };
11229
+ const pending = pendingSourcesEnrichment;
11230
+ pendingSourcesEnrichment = null;
11231
+ if (pending) {
11232
+ enrichCursorStatsInBackground(pending.merged, enrichedSources, pending.hints);
11233
+ }
9743
11234
  });
9744
11235
  };
9745
11236
  const persistedScanResults = await readFileCache(scanResultsCacheKey);
@@ -9871,7 +11362,7 @@ async function startServer(baseDir, opts) {
9871
11362
  computedAt: insightsCache.computedAt
9872
11363
  });
9873
11364
  };
9874
- const startBackgroundScan = () => {
11365
+ const startBackgroundScan = (hints = {}) => {
9875
11366
  if (scanState.running) return;
9876
11367
  const previousResults = scanState.results;
9877
11368
  const previousFinishedAt = scanState.finishedAt;
@@ -9893,29 +11384,34 @@ async function startServer(baseDir, opts) {
9893
11384
  allSessions.push(...sessions);
9894
11385
  }
9895
11386
  const merged = mergeSameSessions(allSessions);
9896
- const home = homedir15();
11387
+ const home = homedir16();
9897
11388
  for (const s of merged) {
9898
11389
  if (s.project.startsWith(home)) {
9899
11390
  s.project = `~${s.project.slice(home.length)}`;
9900
11391
  }
9901
11392
  }
9902
- const scanInputs = merged.map((s) => ({
9903
- sessionId: s.sessionId,
9904
- provider: s.provider,
9905
- project: s.project,
9906
- slug: s.slug,
9907
- filePaths: s.filePaths,
9908
- toolPaths: s.toolPaths,
9909
- sourceFilePath: s.filePath,
9910
- sourceFileSize: s.fileSize,
9911
- sourceLineCount: s.lineCount,
9912
- workspacePath: s.workspacePath,
9913
- hasSqlite: s.hasSqlite,
9914
- deferRichCursorParse: s.provider === "cursor" && !!s.hasSqlite,
9915
- timestamp: s.timestamp,
9916
- title: s.title,
9917
- firstPrompt: s.firstPrompt
9918
- }));
11393
+ lastDiscoveredMergedSessions = merged.map((session) => ({ ...session }));
11394
+ const scanInputs = prioritizeScanInputs(
11395
+ merged.map((s) => ({
11396
+ sessionId: s.sessionId,
11397
+ provider: s.provider,
11398
+ project: s.project,
11399
+ slug: s.slug,
11400
+ filePaths: s.filePaths,
11401
+ toolPaths: s.toolPaths,
11402
+ sourceFilePath: s.filePath,
11403
+ sourceFileSize: s.fileSize,
11404
+ sourceLineCount: s.lineCount,
11405
+ workspacePath: s.workspacePath,
11406
+ hasSqlite: s.hasSqlite,
11407
+ deferRichCursorParse: s.provider === "cursor" && !!s.hasSqlite,
11408
+ timestamp: s.timestamp,
11409
+ title: s.title,
11410
+ firstPrompt: s.firstPrompt
11411
+ })),
11412
+ previousResults,
11413
+ hints
11414
+ );
9919
11415
  scanState.total = scanInputs.length;
9920
11416
  const results = await runBackgroundScan(scanInputs, (progress) => {
9921
11417
  scanState = {
@@ -10002,7 +11498,7 @@ async function startServer(baseDir, opts) {
10002
11498
  await sendError(`Session not found: ${sessionId}`);
10003
11499
  return;
10004
11500
  }
10005
- const home = homedir15();
11501
+ const home = homedir16();
10006
11502
  const projectFor = (info) => info.project.startsWith(home) ? `~${info.project.slice(home.length)}` : info.project;
10007
11503
  const watchers = [];
10008
11504
  const watchedPaths = /* @__PURE__ */ new Set();
@@ -10011,6 +11507,7 @@ async function startServer(baseDir, opts) {
10011
11507
  let inFlight = false;
10012
11508
  let dirty = false;
10013
11509
  let lastSignature = null;
11510
+ let lastCursorDiagnosticsSignature = null;
10014
11511
  const isClaudeProvider = providerName === "claude-code";
10015
11512
  const isCursorProvider = providerName === "cursor";
10016
11513
  const isCodexProvider = providerName === "codex";
@@ -10068,13 +11565,13 @@ async function startServer(baseDir, opts) {
10068
11565
  const cached = jsonlTail.get(filePath);
10069
11566
  let size;
10070
11567
  try {
10071
- size = (await stat9(filePath)).size;
11568
+ size = (await stat10(filePath)).size;
10072
11569
  } catch {
10073
11570
  jsonlTail.delete(filePath);
10074
11571
  return cached?.lines ?? [];
10075
11572
  }
10076
11573
  if (!cached || size < cached.offset) {
10077
- const content = await readFile17(filePath);
11574
+ const content = await readFile19(filePath);
10078
11575
  const { lines, partial } = splitDecodedLines(content);
10079
11576
  jsonlTail.set(filePath, { offset: content.length, partial, lines });
10080
11577
  return lines;
@@ -10114,6 +11611,17 @@ async function startServer(baseDir, opts) {
10114
11611
  const paths = [...info.filePaths, ...info.toolPaths || []];
10115
11612
  ensureWatchersFor(paths);
10116
11613
  await ensureCursorDbWatchersFor(info);
11614
+ const cursorDiagnostics = isCursorProvider && info.hasSqlite ? await readCursorLiveDiagnostics(info.sessionId).catch(() => null) : null;
11615
+ if (cursorDiagnostics && lastCursorDiagnosticsSignature === cursorDiagnostics.signature && lastSignature !== null) {
11616
+ await stream.writeSSE({
11617
+ data: JSON.stringify({
11618
+ type: "diagnostics",
11619
+ cursorDiagnostics,
11620
+ cursorRowsChanged: false
11621
+ })
11622
+ });
11623
+ return;
11624
+ }
10117
11625
  let parsed;
10118
11626
  if (isJsonlLiveProvider) {
10119
11627
  const allLines = [];
@@ -10136,13 +11644,25 @@ async function startServer(baseDir, opts) {
10136
11644
  lastLiveState = await readClaudeSessionState(sessionId);
10137
11645
  }
10138
11646
  const signature = JSON.stringify(replay.scenes);
11647
+ if (cursorDiagnostics) {
11648
+ lastCursorDiagnosticsSignature = cursorDiagnostics.signature;
11649
+ }
10139
11650
  if (signature !== lastSignature) {
10140
11651
  lastSignature = signature;
10141
11652
  await stream.writeSSE({
10142
11653
  data: JSON.stringify({
10143
11654
  type: "session",
10144
11655
  session: replay,
10145
- state: lastLiveState
11656
+ state: lastLiveState,
11657
+ ...cursorDiagnostics ? { cursorDiagnostics, cursorRowsChanged: true } : {}
11658
+ })
11659
+ });
11660
+ } else if (cursorDiagnostics) {
11661
+ await stream.writeSSE({
11662
+ data: JSON.stringify({
11663
+ type: "diagnostics",
11664
+ cursorDiagnostics,
11665
+ cursorRowsChanged: true
10146
11666
  })
10147
11667
  });
10148
11668
  }
@@ -10243,8 +11763,8 @@ async function startServer(baseDir, opts) {
10243
11763
  try {
10244
11764
  const target = await loadSessionFromDisk(baseDir, slug);
10245
11765
  target.meta.title = normalizeTitle(body.title);
10246
- const targetDir = join17(baseDir, slug);
10247
- await writeFile6(join17(targetDir, "replay.json"), JSON.stringify(target), "utf-8");
11766
+ const targetDir = join18(baseDir, slug);
11767
+ await writeFile6(join18(targetDir, "replay.json"), JSON.stringify(target), "utf-8");
10248
11768
  await generateOutput(target, targetDir);
10249
11769
  const updatedReplays = await refreshReplaysCache();
10250
11770
  if (updatedReplays) await syncSourcesCacheWithReplays(updatedReplays);
@@ -10258,7 +11778,7 @@ async function startServer(baseDir, opts) {
10258
11778
  if (!slug) return c.json({ error: "invalid slug" }, 400);
10259
11779
  try {
10260
11780
  const { rm } = await import("fs/promises");
10261
- await rm(join17(baseDir, slug), { recursive: true });
11781
+ await rm(join18(baseDir, slug), { recursive: true });
10262
11782
  const updatedReplays = await refreshReplaysCache();
10263
11783
  if (updatedReplays) await syncSourcesCacheWithReplays(updatedReplays);
10264
11784
  return c.json({ ok: true });
@@ -10292,6 +11812,36 @@ async function startServer(baseDir, opts) {
10292
11812
  app.get("/api/sources/enrichment-status", async (c) => {
10293
11813
  return c.json(sourcesEnrichmentStatus);
10294
11814
  });
11815
+ app.post("/api/sources/enrich", async (c) => {
11816
+ const hints = enrichmentHintsFromBody(await c.req.json().catch(() => void 0));
11817
+ const cached = await readFileCache(sourcesCacheKey);
11818
+ if (!cached?.data?.length) {
11819
+ return c.json({ ok: false, message: "No sources cache available" }, 404);
11820
+ }
11821
+ const cursorProvider2 = getProvider("cursor");
11822
+ if (!cursorProvider2) return c.json({ ok: false, message: "Cursor provider unavailable" }, 404);
11823
+ const home = homedir16();
11824
+ let cursorSessions = lastDiscoveredMergedSessions.filter(
11825
+ (session) => session.provider === "cursor"
11826
+ );
11827
+ if (cursorSessions.length === 0) {
11828
+ cursorSessions = normalizeSessionProjectsForHome(
11829
+ mergeSameSessions(await cursorProvider2.discover()),
11830
+ home
11831
+ );
11832
+ lastDiscoveredMergedSessions = [
11833
+ ...lastDiscoveredMergedSessions.filter((session) => session.provider !== "cursor"),
11834
+ ...cursorSessions
11835
+ ];
11836
+ }
11837
+ const wasRunning = sourcesEnrichmentStatus.running;
11838
+ enrichCursorStatsInBackground(cursorSessions, cached.data, hints);
11839
+ return c.json({
11840
+ ok: true,
11841
+ running: sourcesEnrichmentStatus.running,
11842
+ queued: wasRunning
11843
+ });
11844
+ });
10295
11845
  app.get("/api/sources", async (c) => {
10296
11846
  try {
10297
11847
  const providers2 = getAllProviders();
@@ -10301,11 +11851,12 @@ async function startServer(baseDir, opts) {
10301
11851
  allSessions.push(...sessions);
10302
11852
  }
10303
11853
  const merged = mergeSameSessions(allSessions);
11854
+ lastDiscoveredMergedSessions = normalizeSessionProjectsForHome(merged, homedir16());
10304
11855
  const previous = await readFileCache(sourcesCacheKey);
10305
11856
  const result = await buildSourcesResult(
10306
11857
  merged,
10307
11858
  baseDir,
10308
- homedir15(),
11859
+ homedir16(),
10309
11860
  previous?.data || [],
10310
11861
  cleanupPeriodDays
10311
11862
  );
@@ -10335,11 +11886,12 @@ async function startServer(baseDir, opts) {
10335
11886
  }
10336
11887
  }
10337
11888
  const merged = mergeSameSessions(allSessions);
11889
+ lastDiscoveredMergedSessions = normalizeSessionProjectsForHome(merged, homedir16());
10338
11890
  const previous = await readFileCache(sourcesCacheKey);
10339
11891
  const result = await buildSourcesResult(
10340
11892
  merged,
10341
11893
  baseDir,
10342
- homedir15(),
11894
+ homedir16(),
10343
11895
  previous?.data || [],
10344
11896
  cleanupPeriodDays
10345
11897
  );
@@ -10374,7 +11926,7 @@ async function startServer(baseDir, opts) {
10374
11926
  return c.json({ error: "title must be a string" }, 400);
10375
11927
  }
10376
11928
  const parsed = await provider.parse(resolved.value.paths, resolved.value.sessionInfo);
10377
- const home = homedir15();
11929
+ const home = homedir16();
10378
11930
  const rawProject = body.sessionProject || parsed.cwd;
10379
11931
  const project = rawProject.startsWith(home) ? `~${rawProject.slice(home.length)}` : rawProject;
10380
11932
  const replay = transformToReplay(parsed, body.provider, project, {
@@ -10392,7 +11944,7 @@ async function startServer(baseDir, opts) {
10392
11944
  }
10393
11945
  const rawSlug = replay.meta.slug || replay.meta.sessionId.slice(0, 8);
10394
11946
  const slug = rawSlug.replace(/[^a-zA-Z0-9_-]/g, "-");
10395
- const outputDir = join17(baseDir, slug);
11947
+ const outputDir = join18(baseDir, slug);
10396
11948
  await generateOutput(replay, outputDir);
10397
11949
  const updatedReplays = await refreshReplaysCache();
10398
11950
  if (updatedReplays) await syncSourcesCacheWithReplays(updatedReplays);
@@ -10415,7 +11967,7 @@ async function startServer(baseDir, opts) {
10415
11967
  });
10416
11968
  app.post("/api/regenerate-all", async (c) => {
10417
11969
  const replaysDir = baseDir;
10418
- const { readdir: readdir11, readFile: readF } = await import("fs/promises");
11970
+ const { readdir: readdir12, readFile: readF } = await import("fs/promises");
10419
11971
  const results = [];
10420
11972
  const allProviders = getAllProviders();
10421
11973
  const allSessions = [];
@@ -10428,14 +11980,14 @@ async function startServer(baseDir, opts) {
10428
11980
  }
10429
11981
  let entries;
10430
11982
  try {
10431
- entries = await readdir11(replaysDir);
11983
+ entries = await readdir12(replaysDir);
10432
11984
  } catch {
10433
11985
  return c.json({ error: "No replays directory" }, 404);
10434
11986
  }
10435
11987
  for (const slug of entries) {
10436
11988
  if (slug.startsWith(".") || slug === "cache") continue;
10437
11989
  try {
10438
- const replayPath = join17(replaysDir, slug, "replay.json");
11990
+ const replayPath = join18(replaysDir, slug, "replay.json");
10439
11991
  const raw = await readF(replayPath, "utf-8").catch(() => null);
10440
11992
  if (!raw) continue;
10441
11993
  const oldReplay = JSON.parse(raw);
@@ -10457,7 +12009,7 @@ async function startServer(baseDir, opts) {
10457
12009
  }
10458
12010
  const paths = [...sessionInfo.filePaths, ...sessionInfo.toolPaths || []];
10459
12011
  const parsed = await provider.parse(paths, sessionInfo);
10460
- const home = homedir15();
12012
+ const home = homedir16();
10461
12013
  const project = sessionInfo.project.startsWith(home) ? `~${sessionInfo.project.slice(home.length)}` : sessionInfo.project;
10462
12014
  const replay = transformToReplay(parsed, providerName, project, {
10463
12015
  generator: {
@@ -10467,7 +12019,7 @@ async function startServer(baseDir, opts) {
10467
12019
  }
10468
12020
  });
10469
12021
  if (oldReplay.meta?.title) replay.meta.title = oldReplay.meta.title;
10470
- const outputDir = join17(replaysDir, slug);
12022
+ const outputDir = join18(replaysDir, slug);
10471
12023
  await generateOutput(replay, outputDir);
10472
12024
  results.push({ slug, status: "regenerated", scenes: replay.scenes.length });
10473
12025
  } catch (err) {
@@ -10483,7 +12035,8 @@ async function startServer(baseDir, opts) {
10483
12035
  });
10484
12036
  });
10485
12037
  app.post("/api/scan/start", async (c) => {
10486
- startBackgroundScan();
12038
+ const hints = enrichmentHintsFromBody(await c.req.json().catch(() => void 0));
12039
+ startBackgroundScan(hints);
10487
12040
  return c.json({ ok: true, message: "Background scan started" });
10488
12041
  });
10489
12042
  app.get("/api/scan/status", async (c) => {
@@ -10596,7 +12149,7 @@ async function startServer(baseDir, opts) {
10596
12149
  app.get("/api/gh-status", (c) => {
10597
12150
  return c.json(checkPublishStatus());
10598
12151
  });
10599
- const cloudApiBaseUrl = getApiUrl().replace(/\/$/, "");
12152
+ const cloudApiBaseUrl = getApiUrl();
10600
12153
  function readLocalAuthSession() {
10601
12154
  const exact = loadAuthToken(cloudApiBaseUrl);
10602
12155
  if (exact) {
@@ -10611,7 +12164,7 @@ async function startServer(baseDir, opts) {
10611
12164
  return {
10612
12165
  token: fallback.token,
10613
12166
  user: fallback.user,
10614
- targetApi: fallback.origin.replace(/\/$/, "")
12167
+ targetApi: fallback.origin
10615
12168
  };
10616
12169
  }
10617
12170
  return null;
@@ -10628,7 +12181,7 @@ async function startServer(baseDir, opts) {
10628
12181
  if (exact) candidates.push({ token: exact.token, apiUrl: cloudApiBaseUrl });
10629
12182
  const fallback = loadAnyAuthToken();
10630
12183
  if (fallback) {
10631
- const fallbackApi = fallback.origin.replace(/\/$/, "");
12184
+ const fallbackApi = fallback.origin;
10632
12185
  if (fallbackApi !== cloudApiBaseUrl) {
10633
12186
  candidates.push({ token: fallback.token, apiUrl: fallbackApi });
10634
12187
  }
@@ -10880,7 +12433,7 @@ async function startServer(baseDir, opts) {
10880
12433
  });
10881
12434
  });
10882
12435
  app.get("/api/system-checks", async (c) => {
10883
- const exec = promisify2(execFile2);
12436
+ const exec = promisify3(execFile3);
10884
12437
  const TOOL_CHECK_TIMEOUT_MS = 3e3;
10885
12438
  const CHECK_TIMEOUT_MARKER = "__check_timeout__";
10886
12439
  const CHECK_TIMEOUT_DETAIL = "check timeout";
@@ -10901,7 +12454,8 @@ async function startServer(baseDir, opts) {
10901
12454
  }
10902
12455
  };
10903
12456
  async function checkCli(name, label, purpose, cmd, versionArgs = ["--version"], extraCheck) {
10904
- const whichResult = await runCommand("which", [cmd]);
12457
+ const locator = process.platform === "win32" ? "where" : "which";
12458
+ const whichResult = await runCommand(locator, [cmd]);
10905
12459
  if (!whichResult.ok) {
10906
12460
  if (whichResult.timedOut) {
10907
12461
  return { name, label, purpose, installed: false, detail: CHECK_TIMEOUT_DETAIL };
@@ -10972,7 +12526,7 @@ async function startServer(baseDir, opts) {
10972
12526
  app.get("/api/gist-info", async (c) => {
10973
12527
  const result = requireSlug(c.req.query("slug"));
10974
12528
  if ("error" in result) return c.json({ error: result.error }, 400);
10975
- const targetDir = join17(baseDir, result.slug);
12529
+ const targetDir = join18(baseDir, result.slug);
10976
12530
  const gist = await loadSavedGistInfo(targetDir);
10977
12531
  if (!gist) return c.json({ gist: null });
10978
12532
  return c.json({ gist });
@@ -10980,7 +12534,7 @@ async function startServer(baseDir, opts) {
10980
12534
  app.delete("/api/gist-info", async (c) => {
10981
12535
  const result = requireSlug(c.req.query("slug"));
10982
12536
  if ("error" in result) return c.json({ error: result.error }, 400);
10983
- const metaPath = join17(baseDir, result.slug, ".vibe-replay-gist.json");
12537
+ const metaPath = join18(baseDir, result.slug, ".vibe-replay-gist.json");
10984
12538
  await unlink2(metaPath).catch(() => {
10985
12539
  });
10986
12540
  return c.json({ ok: true });
@@ -10988,7 +12542,7 @@ async function startServer(baseDir, opts) {
10988
12542
  app.get("/api/cloud-info", async (c) => {
10989
12543
  const result = requireSlug(c.req.query("slug"));
10990
12544
  if ("error" in result) return c.json({ error: result.error }, 400);
10991
- const targetDir = join17(baseDir, result.slug);
12545
+ const targetDir = join18(baseDir, result.slug);
10992
12546
  const cloud = await loadSavedCloudInfo(targetDir);
10993
12547
  if (!cloud) return c.json({ cloud: null });
10994
12548
  return c.json({ cloud });
@@ -10996,10 +12550,10 @@ async function startServer(baseDir, opts) {
10996
12550
  app.post("/api/cloud-info", async (c) => {
10997
12551
  const result = requireSlug(c.req.query("slug"));
10998
12552
  if ("error" in result) return c.json({ error: result.error }, 400);
10999
- const targetDir = join17(baseDir, result.slug);
12553
+ const targetDir = join18(baseDir, result.slug);
11000
12554
  const body = await c.req.json();
11001
12555
  if (!body.id || !body.url) return c.json({ error: "Missing id/url" }, 400);
11002
- const metaPath = join17(targetDir, ".vibe-replay-cloud.json");
12556
+ const metaPath = join18(targetDir, ".vibe-replay-cloud.json");
11003
12557
  await writeFile6(
11004
12558
  metaPath,
11005
12559
  JSON.stringify(
@@ -11019,7 +12573,7 @@ async function startServer(baseDir, opts) {
11019
12573
  app.delete("/api/cloud-info", async (c) => {
11020
12574
  const result = requireSlug(c.req.query("slug"));
11021
12575
  if ("error" in result) return c.json({ error: result.error }, 400);
11022
- const metaPath = join17(baseDir, result.slug, ".vibe-replay-cloud.json");
12576
+ const metaPath = join18(baseDir, result.slug, ".vibe-replay-cloud.json");
11023
12577
  await unlink2(metaPath).catch(() => {
11024
12578
  });
11025
12579
  return c.json({ ok: true });
@@ -11027,13 +12581,13 @@ async function startServer(baseDir, opts) {
11027
12581
  app.post("/api/publish/gist", async (c) => {
11028
12582
  const result = requireSlug(c.req.query("slug"));
11029
12583
  if ("error" in result) return c.json({ error: result.error }, 400);
11030
- const targetDir = join17(baseDir, result.slug);
12584
+ const targetDir = join18(baseDir, result.slug);
11031
12585
  try {
11032
12586
  const rawSession = await loadSessionFromDisk(baseDir, result.slug);
11033
12587
  const overlaysData = await loadOverlays(baseDir, result.slug);
11034
12588
  const targetSession = sessionWithEffectiveContent(rawSession, overlaysData);
11035
- const replayPath = join17(targetDir, "replay.json");
11036
- const originalContent = await readFile17(replayPath, "utf-8");
12589
+ const replayPath = join18(targetDir, "replay.json");
12590
+ const originalContent = await readFile19(replayPath, "utf-8");
11037
12591
  await writeFile6(replayPath, JSON.stringify(targetSession), "utf-8");
11038
12592
  try {
11039
12593
  const title = targetSession.meta.title || targetSession.meta.slug;
@@ -11052,7 +12606,7 @@ async function startServer(baseDir, opts) {
11052
12606
  app.post("/api/publish/cloud", async (c) => {
11053
12607
  const result = requireSlug(c.req.query("slug"));
11054
12608
  if ("error" in result) return c.json({ error: result.error }, 400);
11055
- const targetDir = join17(baseDir, result.slug);
12609
+ const targetDir = join18(baseDir, result.slug);
11056
12610
  try {
11057
12611
  const body = await c.req.json().catch(() => ({}));
11058
12612
  const cloudResult = await publishCloudWithOverlays(targetDir, {
@@ -11066,13 +12620,13 @@ async function startServer(baseDir, opts) {
11066
12620
  app.post("/api/export/html", async (c) => {
11067
12621
  const result = requireSlug(c.req.query("slug"));
11068
12622
  if ("error" in result) return c.json({ error: result.error }, 400);
11069
- const targetDir = join17(baseDir, result.slug);
12623
+ const targetDir = join18(baseDir, result.slug);
11070
12624
  try {
11071
12625
  const rawSession = await loadSessionFromDisk(baseDir, result.slug);
11072
12626
  const overlaysData = await loadOverlays(baseDir, result.slug);
11073
12627
  const targetSession = sessionWithEffectiveContent(rawSession, overlaysData);
11074
- const replayPath = join17(targetDir, "replay.json");
11075
- const originalContent = await readFile17(replayPath, "utf-8");
12628
+ const replayPath = join18(targetDir, "replay.json");
12629
+ const originalContent = await readFile19(replayPath, "utf-8");
11076
12630
  try {
11077
12631
  const outputPath = await generateOutput(targetSession, targetDir);
11078
12632
  return c.json({ path: outputPath });
@@ -11086,23 +12640,23 @@ async function startServer(baseDir, opts) {
11086
12640
  app.get("/api/export/github/status", async (c) => {
11087
12641
  const result = requireSlug(c.req.query("slug"));
11088
12642
  if ("error" in result) return c.json({ error: result.error }, 400);
11089
- const targetDir = join17(baseDir, result.slug);
12643
+ const targetDir = join18(baseDir, result.slug);
11090
12644
  try {
11091
- const svgPath = join17(targetDir, "session-preview.svg");
11092
- const mdPath = join17(targetDir, "github-summary.md");
11093
- const gifPath = join17(targetDir, "session-preview.gif");
12645
+ const svgPath = join18(targetDir, "session-preview.svg");
12646
+ const mdPath = join18(targetDir, "github-summary.md");
12647
+ const gifPath = join18(targetDir, "session-preview.gif");
11094
12648
  const [svgContent, markdown, gifBuf] = await Promise.all([
11095
- readFile17(svgPath, "utf-8").catch(() => null),
11096
- readFile17(mdPath, "utf-8").catch(() => null),
11097
- readFile17(gifPath).catch(() => null)
12649
+ readFile19(svgPath, "utf-8").catch(() => null),
12650
+ readFile19(mdPath, "utf-8").catch(() => null),
12651
+ readFile19(gifPath).catch(() => null)
11098
12652
  ]);
11099
12653
  if (!svgContent && !markdown && !gifBuf) return c.json({ exists: false });
11100
12654
  const gist = await loadSavedGistInfo(targetDir);
11101
12655
  const gifContent = gifBuf ? gifBuf.toString("base64") : null;
11102
12656
  const [gifMtime, svgMtime, mdMtime] = await Promise.all([
11103
- stat9(gifPath).then((s) => s.mtime.toISOString()).catch(() => null),
11104
- stat9(svgPath).then((s) => s.mtime.toISOString()).catch(() => null),
11105
- stat9(mdPath).then((s) => s.mtime.toISOString()).catch(() => null)
12657
+ stat10(gifPath).then((s) => s.mtime.toISOString()).catch(() => null),
12658
+ stat10(svgPath).then((s) => s.mtime.toISOString()).catch(() => null),
12659
+ stat10(mdPath).then((s) => s.mtime.toISOString()).catch(() => null)
11106
12660
  ]);
11107
12661
  return c.json({
11108
12662
  exists: true,
@@ -11124,7 +12678,7 @@ async function startServer(baseDir, opts) {
11124
12678
  app.post("/api/export/github", async (c) => {
11125
12679
  const result = requireSlug(c.req.query("slug"));
11126
12680
  if ("error" in result) return c.json({ error: result.error }, 400);
11127
- const targetDir = join17(baseDir, result.slug);
12681
+ const targetDir = join18(baseDir, result.slug);
11128
12682
  try {
11129
12683
  const rawSession = await loadSessionFromDisk(baseDir, result.slug);
11130
12684
  const overlaysData = await loadOverlays(baseDir, result.slug);
@@ -11132,14 +12686,14 @@ async function startServer(baseDir, opts) {
11132
12686
  const gist = await loadSavedGistInfo(targetDir);
11133
12687
  const replayUrl = gist?.viewerUrl || void 0;
11134
12688
  const svgContent = generateGitHubSvg(targetSession, { replayUrl });
11135
- const svgFilePath = join17(targetDir, "session-preview.svg");
12689
+ const svgFilePath = join18(targetDir, "session-preview.svg");
11136
12690
  await writeFile6(svgFilePath, svgContent, "utf-8");
11137
12691
  let gifContent = null;
11138
12692
  let gifFilePath = null;
11139
12693
  let gifWarning;
11140
12694
  try {
11141
12695
  const gifBuffer = await generateGitHubGif(targetSession, { replayUrl });
11142
- gifFilePath = join17(targetDir, "session-preview.gif");
12696
+ gifFilePath = join18(targetDir, "session-preview.gif");
11143
12697
  await writeFile6(gifFilePath, gifBuffer);
11144
12698
  gifContent = gifBuffer.toString("base64");
11145
12699
  } catch (err) {
@@ -11150,7 +12704,7 @@ async function startServer(baseDir, opts) {
11150
12704
  svgPath: "./session-preview.svg",
11151
12705
  gifPath: gifContent ? "./session-preview.gif" : void 0
11152
12706
  });
11153
- const mdFilePath = join17(targetDir, "github-summary.md");
12707
+ const mdFilePath = join18(targetDir, "github-summary.md");
11154
12708
  await writeFile6(mdFilePath, markdown, "utf-8");
11155
12709
  const findings = scanForSecrets(JSON.stringify(targetSession));
11156
12710
  const warnings = findings.map((f) => `[${f.rule}] ${f.match}`);
@@ -11395,24 +12949,370 @@ async function startDashboard(baseDir, opts) {
11395
12949
  await startServer(baseDir, { openDashboard: true, externalViewerUrl: opts?.externalViewerUrl });
11396
12950
  }
11397
12951
 
12952
+ // src/session-query.ts
12953
+ init_utils();
12954
+ var HIGH_TOOL_DENSITY_THRESHOLD = 20;
12955
+ async function queryLocalSessions(sessions, options = {}) {
12956
+ const limit = normalizeLimit(options.limit);
12957
+ const terms = splitTerms(options.query);
12958
+ const filtered = filterScoredSessionInfos(sessions, options).slice(0, limit);
12959
+ const matches = filtered.map(({ session, query }) => sessionInfoToMatch(session, terms, query));
12960
+ if (!options.scan && !options.brief) return matches;
12961
+ const scanned = await Promise.all(
12962
+ filtered.map(async ({ session }) => {
12963
+ try {
12964
+ return await scanSession(scanInputFromSession(session));
12965
+ } catch {
12966
+ return void 0;
12967
+ }
12968
+ })
12969
+ );
12970
+ return matches.map((match, index) => {
12971
+ const scan = scanned[index];
12972
+ const summary = scan ? scanSummary(scan) : void 0;
12973
+ return {
12974
+ ...match,
12975
+ ...summary ? { scan: summary } : {},
12976
+ ...options.brief ? { brief: buildSessionBrief(match, summary) } : {}
12977
+ };
12978
+ });
12979
+ }
12980
+ function filterScoredSessionInfos(sessions, options = {}) {
12981
+ const terms = splitTerms(options.query);
12982
+ const projectTerm = normalizeSearch(options.project);
12983
+ const provider = normalizeSearch(options.provider);
12984
+ const wideMatch = Boolean(options.any);
12985
+ const filtered = [...sessions].map((session) => ({
12986
+ session,
12987
+ query: scoreSession(session, terms)
12988
+ })).filter(({ session, query }) => {
12989
+ if (provider && normalizeSearch(session.provider) !== provider) return false;
12990
+ if (projectTerm && !sessionProjectHaystack(session).includes(projectTerm)) return false;
12991
+ if (terms.length > 0) {
12992
+ if (wideMatch) return query.matchedTerms.length > 0;
12993
+ if (query.matchedTerms.length !== terms.length) return false;
12994
+ }
12995
+ return true;
12996
+ }).sort((a, b) => {
12997
+ if (wideMatch || terms.length > 0) {
12998
+ const scoreDelta = b.query.score - a.query.score;
12999
+ if (scoreDelta) return scoreDelta;
13000
+ }
13001
+ return b.session.timestamp.localeCompare(a.session.timestamp);
13002
+ });
13003
+ return options.dedupe ? dedupeSimilarScoredSessions(filtered) : filtered;
13004
+ }
13005
+ function scanInputFromSession(session) {
13006
+ return {
13007
+ sessionId: session.sessionId,
13008
+ provider: session.provider,
13009
+ project: shortenPath(session.project),
13010
+ slug: session.slug,
13011
+ filePaths: session.filePaths,
13012
+ toolPaths: session.toolPaths,
13013
+ sourceFilePath: session.filePath,
13014
+ sourceFileSize: session.fileSize,
13015
+ sourceLineCount: session.lineCount,
13016
+ workspacePath: session.workspacePath,
13017
+ hasSqlite: session.hasSqlite,
13018
+ timestamp: session.timestamp,
13019
+ title: session.title,
13020
+ firstPrompt: session.firstPrompt
13021
+ };
13022
+ }
13023
+ function formatSessionQueryText(matches) {
13024
+ if (matches.length === 0) return "No matching sessions found.";
13025
+ return matches.map((match, index) => {
13026
+ const title = match.title || previewPrompt(match.firstPrompt) || match.slug;
13027
+ const lines = [
13028
+ `${index + 1}. ${title}`,
13029
+ ` ${match.provider} | ${match.timestamp} | ${match.project}`,
13030
+ ` session: ${match.sessionId}`,
13031
+ ` file: ${match.filePath}`
13032
+ ];
13033
+ if (match.gitBranch) lines.push(` branch: ${match.gitBranch}`);
13034
+ if (match.model) lines.push(` model: ${match.model}`);
13035
+ if (match.matchedTerms?.length) {
13036
+ const quality = match.matchQuality ? `${match.matchQuality}, ` : "";
13037
+ lines.push(
13038
+ ` matched: ${quality}${match.matchedTerms.length}/${(match.matchedTerms.length || 0) + (match.unmatchedTerms?.length || 0)} terms (${match.matchedTerms.join(", ")})`
13039
+ );
13040
+ }
13041
+ if (match.unmatchedTerms?.length) {
13042
+ lines.push(` unmatched: ${match.unmatchedTerms.join(", ")}`);
13043
+ }
13044
+ if (match.whyMatched?.length) lines.push(` why: ${match.whyMatched.join("; ")}`);
13045
+ if (match.firstPrompt) lines.push(` first prompt: ${previewPrompt(match.firstPrompt)}`);
13046
+ if (match.scan) lines.push(` efficiency: ${formatScanSummary(match.scan)}`);
13047
+ if (match.brief) {
13048
+ lines.push(` brief: ${match.brief.summary}`);
13049
+ if (match.brief.signals.length > 0) {
13050
+ lines.push(` signals: ${match.brief.signals.join("; ")}`);
13051
+ }
13052
+ lines.push(` next: ${match.brief.suggestedNextAction}`);
13053
+ }
13054
+ return lines.join("\n");
13055
+ }).join("\n\n");
13056
+ }
13057
+ function sessionInfoToMatch(session, terms = [], precomputedScore) {
13058
+ const query = precomputedScore || scoreSession(session, terms);
13059
+ return {
13060
+ provider: session.provider,
13061
+ sessionId: session.sessionId,
13062
+ slug: session.slug,
13063
+ title: cleanPromptText(session.title || "") || void 0,
13064
+ project: shortenPath(session.project),
13065
+ cwd: shortenPath(session.cwd),
13066
+ timestamp: session.timestamp,
13067
+ gitBranch: session.gitBranch,
13068
+ model: session.model,
13069
+ firstPrompt: cleanPromptText(session.firstPrompt),
13070
+ prompts: session.prompts?.map(cleanPromptText).filter(Boolean),
13071
+ filePath: session.filePath,
13072
+ filePaths: session.filePaths,
13073
+ toolPaths: session.toolPaths,
13074
+ promptCount: session.promptCount,
13075
+ toolCallCount: session.toolCallCount,
13076
+ editCount: session.editCountEst,
13077
+ durationMs: session.durationMsEst,
13078
+ hasPR: session.hasPR,
13079
+ score: query.score,
13080
+ matchedTerms: query.matchedTerms,
13081
+ unmatchedTerms: query.unmatchedTerms,
13082
+ matchQuality: query.matchQuality,
13083
+ whyMatched: query.whyMatched
13084
+ };
13085
+ }
13086
+ function scanSummary(scan) {
13087
+ const promptCount = scan.promptCount;
13088
+ return {
13089
+ promptCount,
13090
+ toolCallCount: scan.toolCallCount,
13091
+ editCount: scan.editCount,
13092
+ filesModified: scan.filesModified.slice(0, 20),
13093
+ durationMs: scan.durationMs,
13094
+ costEstimate: scan.costEstimate,
13095
+ apiErrorCount: scan.apiErrorCount,
13096
+ compactionCount: scan.compactionCount,
13097
+ subAgentCount: scan.subAgentCount,
13098
+ toolCallsPerPrompt: ratio(scan.toolCallCount, promptCount),
13099
+ editsPerPrompt: ratio(scan.editCount, promptCount),
13100
+ medianTurnDurationMs: median(scan.turnDurations),
13101
+ dataQualityNotes: scan.dataQualityNotes
13102
+ };
13103
+ }
13104
+ function formatScanSummary(scan) {
13105
+ const parts = [
13106
+ `${scan.promptCount} prompts`,
13107
+ `${scan.toolCallCount} tools`,
13108
+ `${scan.editCount} edits`
13109
+ ];
13110
+ if (scan.durationMs) parts.push(formatDuration2(scan.durationMs));
13111
+ if (scan.costEstimate) parts.push(`$${scan.costEstimate.toFixed(2)}`);
13112
+ if (scan.toolCallsPerPrompt !== void 0) {
13113
+ parts.push(`${scan.toolCallsPerPrompt.toFixed(1)} tools/prompt`);
13114
+ }
13115
+ if (scan.apiErrorCount > 0) parts.push(`${scan.apiErrorCount} API errors`);
13116
+ if (scan.compactionCount > 0) parts.push(`${scan.compactionCount} compactions`);
13117
+ return parts.join(", ");
13118
+ }
13119
+ function scoreSession(session, terms) {
13120
+ if (terms.length === 0) {
13121
+ return { score: 0, matchedTerms: [], unmatchedTerms: [], whyMatched: [] };
13122
+ }
13123
+ const fields = [
13124
+ { name: "title", value: session.title, weight: 8 },
13125
+ { name: "first prompt", value: session.firstPrompt, weight: 7 },
13126
+ { name: "prompts", value: (session.prompts || []).join(" "), weight: 5 },
13127
+ { name: "project", value: session.project, weight: 4 },
13128
+ { name: "branch", value: session.gitBranch, weight: 4 },
13129
+ { name: "model", value: session.model, weight: 2 },
13130
+ { name: "provider", value: session.provider, weight: 2 },
13131
+ { name: "slug", value: session.slug, weight: 2 },
13132
+ { name: "files", value: (session.fsDetectedFiles || []).join(" "), weight: 3 }
13133
+ ].map((field) => ({ ...field, normalized: normalizeSearch(field.value) }));
13134
+ const matchedTerms = [];
13135
+ const unmatchedTerms = [];
13136
+ const whyMatched = [];
13137
+ let score = 0;
13138
+ for (const term of terms) {
13139
+ const hits = fields.filter((field) => textMatchesTerm(field.normalized, term));
13140
+ if (hits.length === 0) {
13141
+ unmatchedTerms.push(term);
13142
+ continue;
13143
+ }
13144
+ matchedTerms.push(term);
13145
+ const best = hits.sort((a, b) => b.weight - a.weight)[0];
13146
+ score += best.weight;
13147
+ whyMatched.push(`${term} in ${best.name}`);
13148
+ }
13149
+ if (matchedTerms.length === terms.length && terms.length > 1) score += 5;
13150
+ if (session.hasPR && terms.some((term) => ["pr", "review", "pull", "merge"].includes(term))) {
13151
+ score += 2;
13152
+ }
13153
+ if (session.promptCount && session.promptCount > 10) score += 1;
13154
+ return {
13155
+ score,
13156
+ matchedTerms,
13157
+ unmatchedTerms,
13158
+ matchQuality: matchQuality(matchedTerms.length, terms.length),
13159
+ whyMatched
13160
+ };
13161
+ }
13162
+ function matchQuality(matchedTermCount, queryTermCount) {
13163
+ if (queryTermCount === 0 || matchedTermCount === 0) return void 0;
13164
+ if (matchedTermCount === queryTermCount) return "all-terms";
13165
+ if (matchedTermCount >= 2) return "strong";
13166
+ return "weak";
13167
+ }
13168
+ function buildSessionBrief(match, scan) {
13169
+ const text = normalizeSearch(
13170
+ [match.title, match.firstPrompt, match.project, match.gitBranch].filter(Boolean).join(" ")
13171
+ );
13172
+ const taskType = classifyTaskType(text, match, scan);
13173
+ const signals = sessionSignals(match, scan);
13174
+ return {
13175
+ taskType,
13176
+ summary: briefSummary(taskType, match, scan),
13177
+ signals,
13178
+ suggestedNextAction: suggestedNextAction(taskType, signals, scan)
13179
+ };
13180
+ }
13181
+ function classifyTaskType(text, match, scan) {
13182
+ if (/(review|pr|pull request|merge|ai review)/i.test(text) || match.hasPR) return "PR/review";
13183
+ if (/(latest main|branch|current feature|what.*working)/i.test(text)) {
13184
+ return "context recovery";
13185
+ }
13186
+ if (/(skill|dev space|devspaces)/i.test(text)) return "skill workflow";
13187
+ if (/(auth|oauth|login)/i.test(text)) return "auth/debugging";
13188
+ if (/(ci|test|lint|build|e2e)/i.test(text)) return "CI/test";
13189
+ if ((scan?.editCount || 0) > 0 || (match.editCount || 0) > 0) return "implementation";
13190
+ return "research";
13191
+ }
13192
+ function sessionSignals(match, scan) {
13193
+ const signals = [];
13194
+ const promptCount = scan?.promptCount ?? match.promptCount ?? 0;
13195
+ const toolCount = scan?.toolCallCount ?? match.toolCallCount ?? 0;
13196
+ const toolsPerPrompt = scan?.toolCallsPerPrompt ?? ratio(toolCount, promptCount);
13197
+ if (toolsPerPrompt !== void 0 && toolsPerPrompt >= HIGH_TOOL_DENSITY_THRESHOLD) {
13198
+ signals.push(`high tool density (${toolsPerPrompt.toFixed(1)} tools/prompt)`);
13199
+ }
13200
+ if (promptCount >= 40) signals.push(`long conversation (${promptCount} prompts)`);
13201
+ if ((scan?.editCount ?? match.editCount ?? 0) >= 50) {
13202
+ signals.push(`heavy editing (${scan?.editCount ?? match.editCount} edits)`);
13203
+ }
13204
+ if ((scan?.filesModified?.length || 0) >= 10) {
13205
+ signals.push(`broad file surface (${scan?.filesModified.length} files in scan summary)`);
13206
+ }
13207
+ if ((scan?.apiErrorCount || 0) > 0) signals.push(`${scan?.apiErrorCount} API errors`);
13208
+ if ((scan?.compactionCount || 0) > 0) signals.push(`${scan?.compactionCount} compactions`);
13209
+ if ((scan?.subAgentCount || 0) > 0) signals.push(`${scan?.subAgentCount} sub-agents`);
13210
+ if (match.matchedTerms?.length) {
13211
+ const quality = match.matchQuality ? `${match.matchQuality} match` : "matched";
13212
+ signals.push(`${quality}: ${match.matchedTerms.join(", ")}`);
13213
+ }
13214
+ if (match.unmatchedTerms?.length) {
13215
+ signals.push(`unmatched: ${match.unmatchedTerms.join(", ")}`);
13216
+ }
13217
+ return signals;
13218
+ }
13219
+ function briefSummary(taskType, match, scan) {
13220
+ const title = previewPrompt(match.title || match.firstPrompt || match.slug);
13221
+ if (!scan) return `${taskType} session: ${title}`;
13222
+ return `${taskType} session with ${plural(scan.promptCount, "prompt")}, ${plural(scan.toolCallCount, "tool")}, and ${plural(scan.editCount, "edit")}: ${title}`;
13223
+ }
13224
+ function suggestedNextAction(taskType, signals, scan) {
13225
+ if (!scan) return "Run again with --scan or --brief before doing a retro.";
13226
+ if (signals.some((signal) => signal.includes("high tool density") || signal.includes("long"))) {
13227
+ return "Generate a replay or inspect chapters; raw metrics suggest a dense session.";
13228
+ }
13229
+ if (taskType === "PR/review")
13230
+ return "Inspect PR links/review comments or generate a PR-focused replay.";
13231
+ if (taskType === "context recovery")
13232
+ return "Use this as evidence for a session-start snapshot feature.";
13233
+ return "Use filePaths for deeper transcript/replay inspection if this is the right candidate.";
13234
+ }
13235
+ function splitTerms(query) {
13236
+ return normalizeSearch(query).split(/\s+/).map((term) => term.trim()).filter(Boolean);
13237
+ }
13238
+ function textMatchesTerm(text, term) {
13239
+ if (!term) return false;
13240
+ if (/^[a-z0-9]{1,3}$/.test(term)) {
13241
+ return text.split(/[^a-z0-9]+/).filter(Boolean).includes(term);
13242
+ }
13243
+ return text.includes(term);
13244
+ }
13245
+ function dedupeSimilarScoredSessions(scoredSessions) {
13246
+ const seen = /* @__PURE__ */ new Set();
13247
+ const deduped = [];
13248
+ for (const scored of scoredSessions) {
13249
+ const { session } = scored;
13250
+ const key = duplicateSessionKey(session);
13251
+ if (key && seen.has(key)) continue;
13252
+ if (key) seen.add(key);
13253
+ deduped.push(scored);
13254
+ }
13255
+ return deduped;
13256
+ }
13257
+ function duplicateSessionKey(session) {
13258
+ const prompt = normalizeSearch(cleanPromptText(session.firstPrompt));
13259
+ if (prompt.length >= 60) return `prompt:${prompt.slice(0, 240)}`;
13260
+ const title = normalizeSearch(cleanPromptText(session.title || ""));
13261
+ if (title.length >= 60) return `title:${title.slice(0, 240)}`;
13262
+ return void 0;
13263
+ }
13264
+ function sessionProjectHaystack(session) {
13265
+ return normalizeSearch(
13266
+ [session.project, session.cwd, session.workspacePath].filter(Boolean).join(" ")
13267
+ );
13268
+ }
13269
+ function normalizeSearch(value) {
13270
+ return (value || "").toLowerCase().replace(/\s+/g, " ").trim();
13271
+ }
13272
+ function plural(count, label) {
13273
+ return `${count} ${label}${count === 1 ? "" : "s"}`;
13274
+ }
13275
+ function normalizeLimit(limit) {
13276
+ if (!Number.isFinite(limit) || !limit || limit <= 0) return 10;
13277
+ return Math.min(Math.floor(limit), 100);
13278
+ }
13279
+ function ratio(value, denominator) {
13280
+ if (denominator <= 0) return void 0;
13281
+ return Math.round(value / denominator * 10) / 10;
13282
+ }
13283
+ function median(values) {
13284
+ if (!values?.length) return void 0;
13285
+ const sorted = [...values].sort((a, b) => a - b);
13286
+ const mid = Math.floor(sorted.length / 2);
13287
+ if (sorted.length % 2 === 1) return sorted[mid];
13288
+ return Math.round((sorted[mid - 1] + sorted[mid]) / 2);
13289
+ }
13290
+ function formatDuration2(ms) {
13291
+ const minutes = Math.round(ms / 6e4);
13292
+ if (minutes < 60) return `${minutes}m`;
13293
+ const hours = Math.floor(minutes / 60);
13294
+ const remainder = minutes % 60;
13295
+ return remainder ? `${hours}h ${remainder}m` : `${hours}h`;
13296
+ }
13297
+
11398
13298
  // src/index.ts
11399
13299
  init_utils();
11400
13300
  init_version();
11401
13301
  async function runGitHubExport(replay, outputDir) {
11402
- const { join: join18 } = await import("path");
13302
+ const { join: join19 } = await import("path");
11403
13303
  const { writeFile: writeFile7 } = await import("fs/promises");
11404
13304
  const savedGist = await loadSavedGistInfo(outputDir);
11405
13305
  const replayUrl = savedGist?.viewerUrl;
11406
13306
  const svgSpinner = ora("Generating animated SVG...").start();
11407
13307
  const svgContent = generateGitHubSvg(replay, { replayUrl });
11408
- const svgFilePath = join18(outputDir, "session-preview.svg");
13308
+ const svgFilePath = join19(outputDir, "session-preview.svg");
11409
13309
  await writeFile7(svgFilePath, svgContent, "utf-8");
11410
13310
  svgSpinner.succeed(`SVG: ${svgFilePath}`);
11411
13311
  let gifGenerated = false;
11412
13312
  const gifSpinner = ora("Generating animated GIF...").start();
11413
13313
  try {
11414
13314
  const gifBuffer = await generateGitHubGif(replay, { replayUrl });
11415
- const gifFilePath = join18(outputDir, "session-preview.gif");
13315
+ const gifFilePath = join19(outputDir, "session-preview.gif");
11416
13316
  await writeFile7(gifFilePath, gifBuffer);
11417
13317
  const gifSizeKB = Math.round(gifBuffer.length / 1024);
11418
13318
  gifSpinner.succeed(`GIF: ${gifFilePath} (${gifSizeKB} KB)`);
@@ -11426,7 +13326,7 @@ async function runGitHubExport(replay, outputDir) {
11426
13326
  svgPath: "./session-preview.svg",
11427
13327
  gifPath: gifGenerated ? "./session-preview.gif" : void 0
11428
13328
  });
11429
- const mdFilePath = join18(outputDir, "github-summary.md");
13329
+ const mdFilePath = join19(outputDir, "github-summary.md");
11430
13330
  await writeFile7(mdFilePath, markdown, "utf-8");
11431
13331
  mdSpinner.succeed(`Markdown: ${mdFilePath}`);
11432
13332
  const redactionsSpinner = ora("Generating redaction report...").start();
@@ -11442,7 +13342,7 @@ async function runGitHubExport(replay, outputDir) {
11442
13342
  "leftoverFindings: regex matches in the final markdown \u2014 review these before sharing."
11443
13343
  ]
11444
13344
  };
11445
- const redactionsPath = join18(outputDir, "redactions.json");
13345
+ const redactionsPath = join19(outputDir, "redactions.json");
11446
13346
  await writeFile7(redactionsPath, `${JSON.stringify(redactionsReport, null, 2)}
11447
13347
  `, "utf-8");
11448
13348
  const leftoverNote = leftoverFindings.length > 0 ? chalk2.yellow(`(${leftoverFindings.length} leftover finding(s) \u2014 review)`) : chalk2.dim("(no leftover findings)");
@@ -11451,12 +13351,16 @@ async function runGitHubExport(replay, outputDir) {
11451
13351
  markdown,
11452
13352
  outputDir,
11453
13353
  svgPath: svgFilePath,
11454
- gifPath: gifGenerated ? join18(outputDir, "session-preview.gif") : void 0,
13354
+ gifPath: gifGenerated ? join19(outputDir, "session-preview.gif") : void 0,
11455
13355
  mdPath: mdFilePath,
11456
13356
  redactionsPath
11457
13357
  };
11458
13358
  }
11459
13359
  var DEV_MENU_ENABLED = process.env.VIBE_REPLAY_DEV_MENU === "1";
13360
+ function getDevViewerOpts() {
13361
+ if (!DEV_MENU_ENABLED) return void 0;
13362
+ return { externalViewerUrl: `http://localhost:${process.env.VIBE_VIEWER_PORT || "5173"}` };
13363
+ }
11460
13364
  var SESSION_DISCOVERY_CACHE_KEY = "session-discovery-v3";
11461
13365
  function normalizePromptTitle(value) {
11462
13366
  return normalizeTitle(cleanPromptText(value || "")) || "";
@@ -11475,6 +13379,35 @@ function suggestedReplayTitle(replayTitle, replaySlug, sessionInfo) {
11475
13379
  if (firstPromptTitle) return firstPromptTitle;
11476
13380
  return replayCandidate || slug || replaySlug;
11477
13381
  }
13382
+ function formatParseWarningSummary(warnings) {
13383
+ if (!warnings?.length) return [];
13384
+ return warnings.map((warning) => {
13385
+ const label = warning.kind === "malformed-json" ? "malformed JSONL line" : warning.kind === "missing-image" ? "image file" : warning.kind === "unreadable-source" ? "unreadable source" : "item";
13386
+ const plural2 = warning.count === 1 ? label : `${label}s`;
13387
+ const location = warning.firstLine ? ` first at line ${warning.firstLine}` : "";
13388
+ const source = warning.source ? ` in ${warning.source}` : "";
13389
+ const sample = warning.sample ? ` Sample: ${warning.sample}` : "";
13390
+ return `${warning.count} ${plural2} skipped${source}${location}.${sample}`;
13391
+ });
13392
+ }
13393
+ function printParseWarnings(warnings) {
13394
+ const lines = formatParseWarningSummary(warnings);
13395
+ if (lines.length === 0) return;
13396
+ const total = warnings?.reduce((sum, warning) => sum + warning.count, 0) ?? 0;
13397
+ process.stderr.write(
13398
+ `${chalk2.yellow(` \u26A0 ${total} parser warning${total === 1 ? "" : "s"} detected`)}
13399
+ `
13400
+ );
13401
+ for (const line of lines.slice(0, 4)) {
13402
+ process.stderr.write(`${chalk2.dim(" - ")}${chalk2.yellow(line)}
13403
+ `);
13404
+ }
13405
+ if (lines.length > 4) {
13406
+ process.stderr.write(chalk2.dim(` ... ${lines.length - 4} more warning group(s)
13407
+ `));
13408
+ }
13409
+ process.stderr.write("\n");
13410
+ }
11478
13411
  async function discoverAllSessions() {
11479
13412
  const providers2 = getAllProviders();
11480
13413
  const allSessions = [];
@@ -11495,27 +13428,15 @@ program.name("vibe-replay").description("AI Coding Session Replay & Sharing Tool
11495
13428
  ).action(async (opts) => {
11496
13429
  console.log(chalk2.bold.cyan("\n vibe-replay") + chalk2.dim(` v${CLI_VERSION}
11497
13430
  `));
11498
- if (process.platform === "win32") {
11499
- console.log(chalk2.yellow(" \u26A0 Windows is not supported yet.\n"));
11500
- console.log(chalk2.dim(" We're working on it! Follow progress and updates:"));
11501
- console.log(
11502
- chalk2.dim(" \u2192 ") + chalk2.white("https://github.com/tuo-lei/vibe-replay/issues/26")
11503
- );
11504
- console.log(chalk2.dim(" \u2192 ") + chalk2.white("https://vibe-replay.com\n"));
11505
- process.exit(1);
11506
- }
11507
13431
  const { join: pathJoin } = await import("path");
11508
- const { homedir: homedir16 } = await import("os");
11509
- const replayBaseDir = pathJoin(homedir16(), ".vibe-replay");
13432
+ const { homedir: homedir17 } = await import("os");
13433
+ const replayBaseDir = pathJoin(homedir17(), ".vibe-replay");
11510
13434
  void discoverAllSessions().then(async (sessions) => {
11511
13435
  await writeFileCache(SESSION_DISCOVERY_CACHE_KEY, sessions);
11512
13436
  }).catch(() => {
11513
13437
  });
11514
13438
  if (opts.dashboard) {
11515
- await startDashboard(
11516
- replayBaseDir,
11517
- DEV_MENU_ENABLED ? { externalViewerUrl: `http://localhost:${process.env.VIBE_VIEWER_PORT || "5173"}` } : void 0
11518
- );
13439
+ await startDashboard(replayBaseDir, getDevViewerOpts());
11519
13440
  return;
11520
13441
  }
11521
13442
  if ((opts.open || opts.github) && !opts.session) {
@@ -11547,21 +13468,18 @@ program.name("vibe-replay").description("AI Coding Session Replay & Sharing Tool
11547
13468
  ]
11548
13469
  });
11549
13470
  if (topChoice === "dashboard") {
11550
- await startDashboard(
11551
- replayBaseDir,
11552
- DEV_MENU_ENABLED ? { externalViewerUrl: `http://localhost:${process.env.VIBE_VIEWER_PORT || "5173"}` } : void 0
11553
- );
13471
+ await startDashboard(replayBaseDir, getDevViewerOpts());
11554
13472
  return;
11555
13473
  }
11556
13474
  if (topChoice === "replays") {
11557
- const { readdir: readdir11, readFile: readFile18 } = await import("fs/promises");
13475
+ const { readdir: readdir12, readFile: readFile20 } = await import("fs/promises");
11558
13476
  const replayEntries = [];
11559
13477
  try {
11560
- const entries = await readdir11(replayBaseDir);
13478
+ const entries = await readdir12(replayBaseDir);
11561
13479
  for (const slug2 of entries) {
11562
13480
  if (slug2.startsWith(".") || slug2 === "cache") continue;
11563
13481
  try {
11564
- const raw = await readFile18(pathJoin(replayBaseDir, slug2, "replay.json"), "utf-8");
13482
+ const raw = await readFile20(pathJoin(replayBaseDir, slug2, "replay.json"), "utf-8");
11565
13483
  const replay2 = JSON.parse(raw);
11566
13484
  const title = replay2.meta?.title || slug2;
11567
13485
  const provider2 = replay2.meta?.provider || "";
@@ -11574,7 +13492,7 @@ program.name("vibe-replay").description("AI Coding Session Replay & Sharing Tool
11574
13492
  minute: "2-digit",
11575
13493
  hour12: false
11576
13494
  }) : "";
11577
- const providerBadge = provider2 === "claude-code" ? chalk2.hex("#D97706")("claude") : provider2 === "claude-desktop" ? chalk2.hex("#C084FC")("desktop") : provider2 === "claude-cowork" ? chalk2.hex("#F472B6")("cowork") : provider2 === "cursor" ? chalk2.hex("#0096FF")("cursor") : chalk2.yellow(provider2);
13495
+ const providerBadge = provider2 === "claude-code" ? chalk2.hex("#D97706")("claude") : provider2 === "claude-desktop" ? chalk2.hex("#C084FC")("desktop") : provider2 === "claude-cowork" ? chalk2.hex("#F472B6")("cowork") : provider2 === "cursor" ? chalk2.hex("#0096FF")("cursor") : provider2 === "codex" ? chalk2.hex("#B392F0")("codex") : chalk2.yellow(provider2);
11578
13496
  replayEntries.push({
11579
13497
  name: `${providerBadge} ${chalk2.dim(`[${time}]`)} ${chalk2.white(title)} ${chalk2.dim(`(${scenes} scenes)`)}`,
11580
13498
  value: slug2,
@@ -11695,7 +13613,8 @@ program.name("vibe-replay").description("AI Coding Session Replay & Sharing Tool
11695
13613
  }
11696
13614
  const info = displayedSessions.find((s) => s.filePath === chosen);
11697
13615
  sessionInfo = info;
11698
- sessionPaths = info ? [...info.filePaths, ...info.toolPaths || []] : [chosen];
13616
+ const sourcePaths = info ? info.filePaths.length > 0 ? info.filePaths : [info.filePath] : [chosen];
13617
+ sessionPaths = [...sourcePaths, ...info?.toolPaths || []];
11699
13618
  providerName = info?.provider || opts.provider;
11700
13619
  }
11701
13620
  const provider = getProvider(providerName);
@@ -11746,10 +13665,10 @@ program.name("vibe-replay").description("AI Coding Session Replay & Sharing Tool
11746
13665
  replay.meta.title = normalizedUserTitle;
11747
13666
  }
11748
13667
  }
11749
- const { join: join18 } = await import("path");
13668
+ const { join: join19 } = await import("path");
11750
13669
  const rawSlug = replay.meta.slug || replay.meta.sessionId.slice(0, 8);
11751
13670
  const slug = rawSlug.replace(/[^a-zA-Z0-9_-]/g, "-");
11752
- const outputDir = join18(home, ".vibe-replay", slug);
13671
+ const outputDir = join19(home, ".vibe-replay", slug);
11753
13672
  const genSpinner = ora("Generating replay...").start();
11754
13673
  const outputPath = await generateOutput(replay, outputDir);
11755
13674
  const { stat: fsStat } = await import("fs/promises");
@@ -11787,6 +13706,7 @@ program.name("vibe-replay").description("AI Coding Session Replay & Sharing Tool
11787
13706
  console.log(chalk2.dim(" Continuing \u2014 user confirmed findings are safe.\n"));
11788
13707
  }
11789
13708
  }
13709
+ printParseWarnings(replay.meta.parseWarnings);
11790
13710
  if (opts.open) {
11791
13711
  await publishLocal(outputPath);
11792
13712
  console.log();
@@ -11840,9 +13760,9 @@ ${chalk2.bold.green(" Done!")}
11840
13760
  if (target === "local") {
11841
13761
  await publishLocal(outputPath);
11842
13762
  } else if (target === "editor") {
11843
- await startServer(join18(home, ".vibe-replay"), {
13763
+ await startServer(join19(home, ".vibe-replay"), {
11844
13764
  openSlug: slug,
11845
- externalViewerUrl: DEV_MENU_ENABLED ? `http://localhost:${process.env.VIBE_VIEWER_PORT || "5173"}` : void 0
13765
+ ...getDevViewerOpts()
11846
13766
  });
11847
13767
  return;
11848
13768
  } else if (target === "cloud") {
@@ -11944,8 +13864,36 @@ ${chalk2.bold.green(" Done!")}
11944
13864
  console.log(chalk2.dim(" File: ") + chalk2.white(outputPath));
11945
13865
  console.log();
11946
13866
  });
13867
+ program.command("sessions").description("Search local AI coding sessions (agent-friendly)").option("-q, --query <text>", "Search title, prompt, project, branch, model, or slug").option("--project <text>", "Filter by project path substring").option("-p, --provider <name>", "Filter by provider (claude-code, cursor, codex, ...)").option("-l, --limit <number>", "Maximum sessions to return", (value) => Number(value), 10).option("--scan", "Run richer per-session scan for efficiency metrics").option("--any", "Match any query term instead of requiring all terms").option("--brief", "Include scan-backed session briefs and match evidence").option("--dedupe", "Collapse near-duplicate sessions with the same long prompt/title").option("--json", "Print machine-readable JSON").action(async (opts) => {
13868
+ const discoverSpinner = opts.json ? void 0 : ora("Discovering local sessions...").start();
13869
+ try {
13870
+ const sessions = mergeSameSessions2(await discoverAllSessions());
13871
+ discoverSpinner?.succeed(`Found ${sessions.length} sessions`);
13872
+ const scanSpinner = opts.scan && !opts.json ? ora("Scanning matching sessions...").start() : void 0;
13873
+ const matches = await queryLocalSessions(sessions, opts);
13874
+ scanSpinner?.succeed(`Prepared ${matches.length} session result(s)`);
13875
+ if (opts.json) {
13876
+ console.log(JSON.stringify({ sessions: matches }, null, 2));
13877
+ } else {
13878
+ console.log();
13879
+ console.log(formatSessionQueryText(matches));
13880
+ console.log();
13881
+ }
13882
+ } catch (err) {
13883
+ discoverSpinner?.fail("Session search failed");
13884
+ const message = err instanceof Error ? err.message : String(err);
13885
+ if (opts.json) {
13886
+ console.error(message);
13887
+ } else {
13888
+ console.error(chalk2.red(`
13889
+ \u2717 ${message}
13890
+ `));
13891
+ }
13892
+ process.exit(1);
13893
+ }
13894
+ });
11947
13895
  var authCmd = program.command("auth").description("Manage authentication");
11948
- authCmd.command("login").description("Log in to vibe-replay with GitHub").option("--api-url <url>", "API base URL", "https://vibe-replay.com").action(async (opts) => {
13896
+ authCmd.command("login").description("Log in to vibe-replay with GitHub").option("--api-url <url>", "API base URL", DEFAULT_API_URL).action(async (opts) => {
11949
13897
  const crypto = await import("crypto");
11950
13898
  const apiUrl = opts.apiUrl.replace(/\/$/, "");
11951
13899
  const parsed = new URL(apiUrl);
@@ -12037,7 +13985,7 @@ authCmd.command("login").description("Log in to vibe-replay with GitHub").option
12037
13985
  5 * 60 * 1e3
12038
13986
  );
12039
13987
  });
12040
- authCmd.command("logout").description("Log out of vibe-replay").option("--api-url <url>", "API base URL", "https://vibe-replay.com").action(async (opts) => {
13988
+ authCmd.command("logout").description("Log out of vibe-replay").option("--api-url <url>", "API base URL", DEFAULT_API_URL).action(async (opts) => {
12041
13989
  const apiUrl = opts.apiUrl.replace(/\/$/, "");
12042
13990
  const auth = loadAuthToken(apiUrl);
12043
13991
  if (!auth) {
@@ -12047,7 +13995,7 @@ authCmd.command("logout").description("Log out of vibe-replay").option("--api-ur
12047
13995
  removeAuthTokenSync(apiUrl);
12048
13996
  console.log(chalk2.bold.green("\n \u2713 Logged out successfully\n"));
12049
13997
  });
12050
- authCmd.command("status").description("Show current authentication status").option("--api-url <url>", "API base URL", "https://vibe-replay.com").action(async (opts) => {
13998
+ authCmd.command("status").description("Show current authentication status").option("--api-url <url>", "API base URL", DEFAULT_API_URL).action(async (opts) => {
12051
13999
  const apiUrl = opts.apiUrl.replace(/\/$/, "");
12052
14000
  const auth = loadAuthToken(apiUrl);
12053
14001
  if (!auth) {
@@ -12071,9 +14019,9 @@ authCmd.command("status").description("Show current authentication status").opti
12071
14019
  var VISIBILITIES = ["public", "unlisted", "private"];
12072
14020
  program.command("share").description("Share an existing replay via cloud (unlisted link, expires in 7 days)").argument("[path]", "Path to replay directory or replay.json").option("--visibility <type>", `Visibility: ${VISIBILITIES.join(", ")}`, "unlisted").option("--api-url <url>", `API base URL (default: ${DEFAULT_API_URL})`).action(async (pathArg, opts) => {
12073
14021
  const { existsSync: existsSync2, statSync } = await import("fs");
12074
- const { readFile: readFile18, readdir: readdir11 } = await import("fs/promises");
12075
- const { join: join18, dirname: dirname6, resolve: resolve3 } = await import("path");
12076
- const { homedir: homedir16 } = await import("os");
14022
+ const { readFile: readFile20, readdir: readdir12 } = await import("fs/promises");
14023
+ const { join: join19, dirname: dirname6, resolve: resolve3 } = await import("path");
14024
+ const { homedir: homedir17 } = await import("os");
12077
14025
  if (opts.apiUrl) {
12078
14026
  process.env.VIBE_REPLAY_API_URL = opts.apiUrl.replace(/\/$/, "");
12079
14027
  }
@@ -12102,14 +14050,14 @@ program.command("share").description("Share an existing replay via cloud (unlist
12102
14050
  const s = statSync(abs);
12103
14051
  outputDir = s.isDirectory() ? abs : dirname6(abs);
12104
14052
  } else {
12105
- const replayBaseDir = join18(homedir16(), ".vibe-replay");
12106
- const entries = await readdir11(replayBaseDir).catch(() => []);
14053
+ const replayBaseDir = join19(homedir17(), ".vibe-replay");
14054
+ const entries = await readdir12(replayBaseDir).catch(() => []);
12107
14055
  const replays = [];
12108
14056
  for (const slug of entries) {
12109
14057
  if (slug.startsWith(".") || slug === "cache") continue;
12110
- const jsonPath2 = join18(replayBaseDir, slug, "replay.json");
14058
+ const jsonPath2 = join19(replayBaseDir, slug, "replay.json");
12111
14059
  try {
12112
- const raw = await readFile18(jsonPath2, "utf-8");
14060
+ const raw = await readFile20(jsonPath2, "utf-8");
12113
14061
  const replay = JSON.parse(raw);
12114
14062
  const title = replay.meta?.title || slug;
12115
14063
  const startTime = replay.meta?.startTime || "";
@@ -12122,7 +14070,7 @@ program.command("share").description("Share an existing replay via cloud (unlist
12122
14070
  }) : "";
12123
14071
  replays.push({
12124
14072
  name: time ? `${chalk2.dim(`[${time}]`)} ${chalk2.white(title)}` : chalk2.white(title),
12125
- value: join18(replayBaseDir, slug),
14073
+ value: join19(replayBaseDir, slug),
12126
14074
  time: startTime
12127
14075
  });
12128
14076
  } catch {
@@ -12138,7 +14086,7 @@ program.command("share").description("Share an existing replay via cloud (unlist
12138
14086
  choices: replays
12139
14087
  });
12140
14088
  }
12141
- const jsonPath = join18(outputDir, "replay.json");
14089
+ const jsonPath = join19(outputDir, "replay.json");
12142
14090
  if (!existsSync2(jsonPath)) {
12143
14091
  console.error(chalk2.red(`
12144
14092
  \u2717 No replay.json found in ${outputDir}
@@ -12162,13 +14110,9 @@ program.command("share").description("Share an existing replay via cloud (unlist
12162
14110
  });
12163
14111
  var LIVE_STALENESS_WARNING_MS = 5 * 6e4;
12164
14112
  program.command("live").description("Watch a running AI coding session live in the browser").option("-p, --provider <name>", "Provider name (default: auto-detect)").option("-s, --session <sessionId>", "Specific session ID to watch").action(async (opts) => {
12165
- if (process.platform === "win32") {
12166
- console.log(chalk2.yellow("\n \u26A0 Windows is not supported yet.\n"));
12167
- process.exit(1);
12168
- }
12169
14113
  const { join: pathJoin } = await import("path");
12170
- const { homedir: homedir16 } = await import("os");
12171
- const replayBaseDir = pathJoin(homedir16(), ".vibe-replay");
14114
+ const { homedir: homedir17 } = await import("os");
14115
+ const replayBaseDir = pathJoin(homedir17(), ".vibe-replay");
12172
14116
  console.log(chalk2.bold.cyan("\n vibe-replay live") + chalk2.dim(` v${CLI_VERSION}
12173
14117
  `));
12174
14118
  let target = null;
@@ -12227,7 +14171,7 @@ program.command("live").description("Watch a running AI coding session live in t
12227
14171
  openLive: { provider: target.provider, sessionId: target.sessionId }
12228
14172
  });
12229
14173
  });
12230
- program.command("login", { hidden: true }).description("Log in to vibe-replay (alias for auth login)").option("--api-url <url>", "API base URL", "https://vibe-replay.com").action(async () => {
14174
+ program.command("login", { hidden: true }).description("Log in to vibe-replay (alias for auth login)").option("--api-url <url>", "API base URL", DEFAULT_API_URL).action(async () => {
12231
14175
  await authCmd.commands.find((c) => c.name() === "login")?.parseAsync(["login", ...process.argv.slice(3)], { from: "user" });
12232
14176
  });
12233
14177
  program.parse();
@@ -12256,7 +14200,7 @@ function formatSessionChoices(sessions, cleanupPeriodDays) {
12256
14200
  });
12257
14201
  const sizeKB = Math.round(s.fileSize / 1024);
12258
14202
  const prompt = s.firstPrompt.replace(/\n/g, " ").slice(0, 50);
12259
- const providerBadge = s.provider === "claude-code" ? chalk2.hex("#D97706")("claude") : s.provider === "claude-desktop" ? chalk2.hex("#C084FC")("desktop") : s.provider === "claude-cowork" ? chalk2.hex("#F472B6")("cowork") : s.provider === "cursor" ? chalk2.hex("#0096FF")("cursor") : chalk2.yellow(s.provider);
14203
+ const providerBadge = s.provider === "claude-code" ? chalk2.hex("#D97706")("claude") : s.provider === "claude-desktop" ? chalk2.hex("#C084FC")("desktop") : s.provider === "claude-cowork" ? chalk2.hex("#F472B6")("cowork") : s.provider === "cursor" ? chalk2.hex("#0096FF")("cursor") : s.provider === "codex" ? chalk2.hex("#B392F0")("codex") : chalk2.yellow(s.provider);
12260
14204
  const titleStr = s.title ? chalk2.white(` "${s.title}"`) : "";
12261
14205
  const fileCount = s.filePaths.length > 1 ? chalk2.dim(` [${s.filePaths.length} parts]`) : "";
12262
14206
  const sqliteBadge = s.hasSqlite ? chalk2.green(" db") : "";