repospend 0.0.6 → 0.0.7

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/cli.js CHANGED
@@ -71,6 +71,8 @@ function groupBy(sessions, keyFn, labelFn) {
71
71
  id,
72
72
  label: labelFn(session),
73
73
  estimatedCostUsd: void 0,
74
+ knownCostSessions: 0,
75
+ unknownCostSessions: 0,
74
76
  inputTokens: 0,
75
77
  cachedInputTokens: 0,
76
78
  outputTokens: 0,
@@ -90,6 +92,9 @@ function groupBy(sessions, keyFn, labelFn) {
90
92
  group.warnings = unique([...group.warnings, ...session.warnings]);
91
93
  if (session.estimatedCostUsd !== void 0) {
92
94
  group.estimatedCostUsd = roundCurrency((group.estimatedCostUsd ?? 0) + session.estimatedCostUsd);
95
+ group.knownCostSessions += 1;
96
+ } else {
97
+ group.unknownCostSessions += 1;
93
98
  }
94
99
  groups.set(id, group);
95
100
  }
@@ -852,7 +857,7 @@ function buildDashboardSnapshot(input) {
852
857
  };
853
858
  }
854
859
  function isDashboardVisibleSession(session) {
855
- return session.totalTokens > 0 || session.sourceClient === "claude";
860
+ return session.totalTokens > 0 || session.sourceClient === "claude" || session.sourceClient === "cursor";
856
861
  }
857
862
 
858
863
  // packages/core/src/repo.ts
@@ -1273,7 +1278,8 @@ function finalizeTokenAccumulator(accumulator) {
1273
1278
  }
1274
1279
  const direct = accumulator.directTokenUsages.filter((usage) => isValidTokenUsage(usage));
1275
1280
  if (direct.length > 0) {
1276
- const total = direct.reduce(
1281
+ const filteredDirect = filterDuplicateDirectUsages(direct);
1282
+ const total = filteredDirect.reduce(
1277
1283
  (sum2, usage) => ({
1278
1284
  inputTokens: sum2.inputTokens + usage.inputTokens,
1279
1285
  cachedInputTokens: sum2.cachedInputTokens + usage.cachedInputTokens,
@@ -1286,6 +1292,8 @@ function finalizeTokenAccumulator(accumulator) {
1286
1292
  applyFinalUsage(accumulator, total);
1287
1293
  accumulator.tokenAggregationMethod = "delta_sum";
1288
1294
  accumulator.tokenConfidence = "high";
1295
+ const skipped = direct.length - filteredDirect.length;
1296
+ if (skipped > 0) accumulator.warnings.push("duplicate_or_stale_token_snapshots_skipped");
1289
1297
  return;
1290
1298
  }
1291
1299
  accumulator.tokenAggregationMethod = "unknown";
@@ -1298,7 +1306,10 @@ function extractRecordTokens(record) {
1298
1306
  const payloadInfo = firstObject2(payload?.info);
1299
1307
  const codexLastUsage = firstObject2(payloadInfo?.last_token_usage);
1300
1308
  if (codexLastUsage) {
1301
- return tokenUsageFromObject(codexLastUsage, false);
1309
+ const usage2 = tokenUsageFromObject(codexLastUsage, false);
1310
+ const cumulativeSnapshot = firstObject2(payloadInfo?.total_token_usage);
1311
+ const cumulativeUsage = cumulativeSnapshot ? tokenUsageFromObject(cumulativeSnapshot, true) : void 0;
1312
+ return usage2 ? { ...usage2, cumulativeSnapshot: cumulativeUsage ? stripSnapshot(cumulativeUsage) : void 0 } : void 0;
1302
1313
  }
1303
1314
  const codexTotalUsage = firstObject2(payloadInfo?.total_token_usage);
1304
1315
  if (codexTotalUsage) {
@@ -1308,6 +1319,45 @@ function extractRecordTokens(record) {
1308
1319
  const usage = firstObject2(object.usage, object.token_usage, object.tokens, response?.usage, payload?.usage, payload?.token_usage, payload?.tokens);
1309
1320
  return tokenUsageFromObject(usage ?? object, false);
1310
1321
  }
1322
+ function filterDuplicateDirectUsages(usages) {
1323
+ const filtered = [];
1324
+ let previous;
1325
+ for (const usage of usages) {
1326
+ const snapshot = usage.cumulativeSnapshot;
1327
+ if (snapshot && previous) {
1328
+ if (sameTokenUsage(snapshot, previous)) continue;
1329
+ if (!isMonotonicFrom(snapshot, previous) && looksLikeStaleRegression(snapshot, previous, usage)) continue;
1330
+ }
1331
+ filtered.push(usage);
1332
+ if (snapshot) previous = snapshot;
1333
+ }
1334
+ return filtered;
1335
+ }
1336
+ function sameTokenUsage(a, b) {
1337
+ return a.inputTokens === b.inputTokens && a.cachedInputTokens === b.cachedInputTokens && a.outputTokens === b.outputTokens && a.reasoningTokens === b.reasoningTokens && a.totalTokens === b.totalTokens;
1338
+ }
1339
+ function isMonotonicFrom(current, previous) {
1340
+ return current.inputTokens >= previous.inputTokens && current.cachedInputTokens >= previous.cachedInputTokens && current.outputTokens >= previous.outputTokens && current.reasoningTokens >= previous.reasoningTokens;
1341
+ }
1342
+ function looksLikeStaleRegression(current, previous, last) {
1343
+ const previousTotal = tokenBucketTotal(previous);
1344
+ const currentTotal = tokenBucketTotal(current);
1345
+ const lastTotal = tokenBucketTotal(last);
1346
+ if (previousTotal <= 0 || currentTotal <= 0 || lastTotal <= 0) return false;
1347
+ return currentTotal * 100 >= previousTotal * 98 || currentTotal * 4 >= previousTotal * 3 && currentTotal + lastTotal * 2 >= previousTotal;
1348
+ }
1349
+ function tokenBucketTotal(usage) {
1350
+ return usage.inputTokens + usage.cachedInputTokens + usage.outputTokens + usage.reasoningTokens;
1351
+ }
1352
+ function stripSnapshot(usage) {
1353
+ return {
1354
+ inputTokens: usage.inputTokens,
1355
+ cachedInputTokens: usage.cachedInputTokens,
1356
+ outputTokens: usage.outputTokens,
1357
+ reasoningTokens: usage.reasoningTokens,
1358
+ totalTokens: usage.totalTokens
1359
+ };
1360
+ }
1311
1361
  function tokenUsageFromObject(target, cumulative) {
1312
1362
  const input = numberFromKeys(target, ["input_tokens", "inputTokens", "prompt_tokens", "promptTokens"]);
1313
1363
  const cached = numberFromKeys(target, ["cached_input_tokens", "cachedInputTokens", "cached_tokens", "cachedTokens", "cache_read_input_tokens"]);
@@ -2116,9 +2166,9 @@ function scanClaude(options) {
2116
2166
  const warnings = [];
2117
2167
  const discovery = discoverClaudeSessionFiles(claudeHome, warnings, Boolean(options.claudeHome));
2118
2168
  const historyExists = fs5.existsSync(historyPath);
2119
- const seenUsageKeys = /* @__PURE__ */ new Set();
2120
2169
  const orderedFiles = discovery.files.map((file) => ({ file, mtimeMs: safeMtimeMs(file.filePath) })).sort((a, b) => a.mtimeMs - b.mtimeMs);
2121
- const sessions = orderedFiles.map(({ file }, index) => claudeFileToUsage(file, index, options, seenUsageKeys));
2170
+ const usageSelection = selectClaudeUsageOccurrences(orderedFiles.map(({ file }) => file));
2171
+ const sessions = orderedFiles.flatMap(({ file }, index) => claudeFileToUsages(file, index, options, usageSelection));
2122
2172
  const historyEntryCount = countHistoryEntries(historyPath);
2123
2173
  const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
2124
2174
  const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
@@ -2224,8 +2274,12 @@ function walk2(root, maxDepth, onFile, depth = 0) {
2224
2274
  }
2225
2275
  }
2226
2276
  }
2227
- function claudeFileToUsage(file, index, options, seenUsageKeys) {
2228
- const breakdown = readClaudeBreakdown(file.filePath, seenUsageKeys);
2277
+ function claudeFileToUsages(file, index, options, usageSelection) {
2278
+ const breakdown = readClaudeBreakdown(file.filePath, usageSelection);
2279
+ const segments = segmentClaudeBreakdownByDayAndModel(breakdown);
2280
+ return segments.map(({ breakdown: segment, suffix }) => claudeBreakdownToUsage(file, index, options, segment, suffix));
2281
+ }
2282
+ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "") {
2229
2283
  const id = breakdown.id ?? (path5.basename(file.filePath).replace(/\.jsonl$/i, "") || `claude-session-${index}`);
2230
2284
  const fallbackCwd = inferProjectPath(file.projectDir, file.projectsPath);
2231
2285
  const cwd = breakdown.cwd ?? fallbackCwd;
@@ -2235,7 +2289,7 @@ function claudeFileToUsage(file, index, options, seenUsageKeys) {
2235
2289
  if (!breakdown.cwd) warnings.push("repo_inferred_from_claude_project_dir");
2236
2290
  const hasTokenBreakdown = breakdown.totalTokens > 0 && breakdown.usageCount > 0;
2237
2291
  const usage = {
2238
- id,
2292
+ id: `${id}${idSuffix}`,
2239
2293
  sourceClient: "claude",
2240
2294
  sourceApp: claudeSourceAppLabel(entrypoint),
2241
2295
  sourceAppRaw: entrypoint,
@@ -2300,7 +2354,7 @@ function claudeFileToUsage(file, index, options, seenUsageKeys) {
2300
2354
  function inferClaudeEntrypointFromPath(filePath) {
2301
2355
  return filePath.split(path5.sep).includes("local-agent-mode-sessions") ? "local-agent" : void 0;
2302
2356
  }
2303
- function readClaudeBreakdown(filePath, seenUsageKeys) {
2357
+ function readClaudeBreakdown(filePath, usageSelection) {
2304
2358
  const breakdown = emptyBreakdown2();
2305
2359
  try {
2306
2360
  const text = fs5.readFileSync(filePath, "utf8");
@@ -2308,8 +2362,8 @@ function readClaudeBreakdown(filePath, seenUsageKeys) {
2308
2362
  breakdown.rawEventCount = parse.records.length;
2309
2363
  breakdown.parseErrors = parse.errors;
2310
2364
  breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
2311
- for (const record of parse.records) {
2312
- applyClaudeRecord(breakdown, record, seenUsageKeys);
2365
+ for (const [recordIndex, record] of parse.records.entries()) {
2366
+ applyClaudeRecord(breakdown, record, usageSelection, occurrenceKey(filePath, recordIndex));
2313
2367
  }
2314
2368
  breakdown.totalTokens = breakdown.inputTokens + breakdown.outputTokens;
2315
2369
  } catch (error) {
@@ -2320,7 +2374,7 @@ function readClaudeBreakdown(filePath, seenUsageKeys) {
2320
2374
  }
2321
2375
  return breakdown;
2322
2376
  }
2323
- function applyClaudeRecord(breakdown, record, seenUsageKeys) {
2377
+ function applyClaudeRecord(breakdown, record, usageSelection, usageOccurrenceKey) {
2324
2378
  const timestamp3 = dateValue3(record.timestamp);
2325
2379
  if (timestamp3) {
2326
2380
  breakdown.startedAt = minDate(breakdown.startedAt, timestamp3);
@@ -2343,21 +2397,70 @@ function applyClaudeRecord(breakdown, record, seenUsageKeys) {
2343
2397
  const usageTokens = usage ? usageTotal(usage) : 0;
2344
2398
  const model = normalizeClaudeModel(message.model);
2345
2399
  if (model && (usageTokens > 0 || !breakdown.model)) breakdown.model = model;
2346
- if (usage && usageTokens > 0 && !alreadySeenUsage(record, message, seenUsageKeys)) {
2347
- applyUsage(breakdown, usage);
2400
+ const selectedUsage = usageTokens > 0 ? usageSelection.usageByOccurrence.get(usageOccurrenceKey) : void 0;
2401
+ if (selectedUsage) {
2402
+ applyUsage(breakdown, selectedUsage);
2403
+ breakdown.usageEvents.push({ timestamp: timestamp3, model, usage: selectedUsage });
2348
2404
  }
2349
2405
  applyContentSignals(breakdown, message.content);
2350
2406
  }
2351
2407
  if (looksFailedToolResult(record.toolUseResult)) breakdown.failedToolCallCount += 1;
2352
2408
  }
2353
- function alreadySeenUsage(record, message, seenUsageKeys) {
2409
+ function selectClaudeUsageOccurrences(files) {
2410
+ const usageByOccurrence = /* @__PURE__ */ new Map();
2411
+ const selectedByDedupKey = /* @__PURE__ */ new Map();
2412
+ for (const file of files) {
2413
+ let parse;
2414
+ try {
2415
+ parse = parseJsonLines2(fs5.readFileSync(file.filePath, "utf8"));
2416
+ } catch {
2417
+ continue;
2418
+ }
2419
+ for (const [recordIndex, record] of parse.records.entries()) {
2420
+ const message = firstObject4(record.message);
2421
+ const usage = firstObject4(message?.usage);
2422
+ if (!message || !usage || usageTotal(usage) <= 0) continue;
2423
+ const occurrence = occurrenceKey(file.filePath, recordIndex);
2424
+ const dedupKey = usageDedupKey(record, message);
2425
+ if (!dedupKey) {
2426
+ usageByOccurrence.set(occurrence, usage);
2427
+ continue;
2428
+ }
2429
+ const selected = selectedByDedupKey.get(dedupKey);
2430
+ if (selected) {
2431
+ mergeUsageMax(selected.usage, usage);
2432
+ } else {
2433
+ selectedByDedupKey.set(dedupKey, { occurrence, usage: cloneUsage(usage) });
2434
+ }
2435
+ }
2436
+ }
2437
+ for (const selected of selectedByDedupKey.values()) {
2438
+ usageByOccurrence.set(selected.occurrence, selected.usage);
2439
+ }
2440
+ return { usageByOccurrence };
2441
+ }
2442
+ function usageDedupKey(record, message) {
2354
2443
  const messageId = stringValue3(message.id);
2355
2444
  const requestId = stringValue3(record.requestId);
2356
- if (!messageId || !requestId) return false;
2357
- const key = `${requestId}:${messageId}`;
2358
- if (seenUsageKeys.has(key)) return true;
2359
- seenUsageKeys.add(key);
2360
- return false;
2445
+ if (!messageId || !requestId) return void 0;
2446
+ return `${requestId}:${messageId}`;
2447
+ }
2448
+ function occurrenceKey(filePath, recordIndex) {
2449
+ return `${path5.resolve(filePath)}:${recordIndex}`;
2450
+ }
2451
+ function cloneUsage(usage) {
2452
+ return {
2453
+ input_tokens: numberValue(usage.input_tokens),
2454
+ cache_read_input_tokens: numberValue(usage.cache_read_input_tokens),
2455
+ cache_creation_input_tokens: numberValue(usage.cache_creation_input_tokens),
2456
+ output_tokens: numberValue(usage.output_tokens)
2457
+ };
2458
+ }
2459
+ function mergeUsageMax(target, usage) {
2460
+ target.input_tokens = Math.max(numberValue(target.input_tokens), numberValue(usage.input_tokens));
2461
+ target.cache_read_input_tokens = Math.max(numberValue(target.cache_read_input_tokens), numberValue(usage.cache_read_input_tokens));
2462
+ target.cache_creation_input_tokens = Math.max(numberValue(target.cache_creation_input_tokens), numberValue(usage.cache_creation_input_tokens));
2463
+ target.output_tokens = Math.max(numberValue(target.output_tokens), numberValue(usage.output_tokens));
2361
2464
  }
2362
2465
  function applyUsage(breakdown, usage) {
2363
2466
  const input = numberValue(usage.input_tokens);
@@ -2370,6 +2473,54 @@ function applyUsage(breakdown, usage) {
2370
2473
  breakdown.outputTokens += output;
2371
2474
  breakdown.usageCount += 1;
2372
2475
  }
2476
+ function segmentClaudeBreakdownByDayAndModel(breakdown) {
2477
+ const events = breakdown.usageEvents.filter((event) => event.timestamp);
2478
+ const keys = new Set(events.map((event) => segmentKey(event.timestamp, event.model)));
2479
+ if (keys.size <= 1) return [{ breakdown, suffix: "" }];
2480
+ const groups = /* @__PURE__ */ new Map();
2481
+ for (const event of events) {
2482
+ const key = segmentKey(event.timestamp, event.model);
2483
+ const day = dateKey2(event.timestamp);
2484
+ const existing = groups.get(key) ?? { day, model: event.model, events: [] };
2485
+ existing.events.push(event);
2486
+ groups.set(key, existing);
2487
+ }
2488
+ return [...groups.values()].sort((a, b) => a.day.localeCompare(b.day) || (a.model ?? "").localeCompare(b.model ?? "")).map((group) => {
2489
+ const segment = cloneBreakdownMetadata(breakdown);
2490
+ segment.model = group.model ?? breakdown.model;
2491
+ segment.startedAt = group.events.map((event) => event.timestamp).filter(Boolean).sort()[0];
2492
+ segment.endedAt = group.events.map((event) => event.timestamp).filter(Boolean).sort().at(-1);
2493
+ segment.assistantMessageCount = group.events.length;
2494
+ segment.messageCount = group.events.length;
2495
+ segment.rawEventCount = group.events.length;
2496
+ for (const event of group.events) {
2497
+ applyUsage(segment, event.usage);
2498
+ segment.usageEvents.push(event);
2499
+ }
2500
+ segment.totalTokens = segment.inputTokens + segment.outputTokens;
2501
+ return { breakdown: segment, suffix: `#${group.day}${group.model ? `#${group.model}` : ""}` };
2502
+ });
2503
+ }
2504
+ function cloneBreakdownMetadata(breakdown) {
2505
+ return {
2506
+ ...emptyBreakdown2(),
2507
+ id: breakdown.id,
2508
+ title: breakdown.title,
2509
+ cwd: breakdown.cwd,
2510
+ gitBranch: breakdown.gitBranch,
2511
+ entrypoint: breakdown.entrypoint,
2512
+ version: breakdown.version,
2513
+ parseStatus: breakdown.parseStatus,
2514
+ parseErrors: [...breakdown.parseErrors],
2515
+ warnings: [...breakdown.warnings, "claude_session_split_by_activity_day"]
2516
+ };
2517
+ }
2518
+ function segmentKey(timestamp3, model) {
2519
+ return `${dateKey2(timestamp3)}\0${model ?? ""}`;
2520
+ }
2521
+ function dateKey2(timestamp3) {
2522
+ return timestamp3?.slice(0, 10) ?? "unknown-date";
2523
+ }
2373
2524
  function usageTotal(usage) {
2374
2525
  return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + numberValue(usage.cache_creation_input_tokens) + numberValue(usage.output_tokens);
2375
2526
  }
@@ -2429,7 +2580,8 @@ function emptyBreakdown2() {
2429
2580
  parseStatus: "ok",
2430
2581
  parseErrors: [],
2431
2582
  promptTimeline: [],
2432
- warnings: []
2583
+ warnings: [],
2584
+ usageEvents: []
2433
2585
  };
2434
2586
  }
2435
2587
  function applyPromptTimeline2(breakdown, role, content, timestamp3) {
@@ -2579,6 +2731,738 @@ function safeMtimeMs(filePath) {
2579
2731
  }
2580
2732
  }
2581
2733
 
2734
+ // packages/core/src/sources/cursor.ts
2735
+ import fs6 from "node:fs";
2736
+ import os4 from "node:os";
2737
+ import path6 from "node:path";
2738
+ import Database2 from "better-sqlite3";
2739
+ var parserVersion = "cursor-v1";
2740
+ var maxJsonlBytes = 20 * 1024 * 1024;
2741
+ var maxDatabaseBytes = 100 * 1024 * 1024;
2742
+ var cursorStoragePrefixes = ["cursor.", "anysphere."];
2743
+ var interestingTerms = ["cursor", "composer", "aichat", "ai_chat", "aiservice", "agent-transcript", "prompttokenbreakdown"];
2744
+ function scanCursor(options) {
2745
+ const cursorHome = options.cursorHome ?? path6.join(os4.homedir(), ".cursor");
2746
+ const discovery = discoverCursorFiles(cursorHome, Boolean(options.cursorHome));
2747
+ const jsonlCandidates = discovery.jsonlFiles.map((filePath) => cursorJsonlToCandidate(filePath, cursorHome));
2748
+ const databaseCandidates = discovery.databaseFiles.flatMap((filePath) => cursorDatabaseToCandidates(filePath, cursorHome, discovery.warnings));
2749
+ const candidates = dedupeCandidates([...jsonlCandidates, ...databaseCandidates]);
2750
+ const sessions = candidates.map((candidate, index) => candidateToUsage(candidate, index, options));
2751
+ const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
2752
+ const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_"))).length;
2753
+ const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_") || error.startsWith("sqlite_json_parse:"))).length;
2754
+ if (!fs6.existsSync(cursorHome)) {
2755
+ discovery.warnings.push(`Cursor home directory not found at ${cursorHome}`);
2756
+ }
2757
+ if (!fs6.existsSync(discovery.statePath)) {
2758
+ discovery.warnings.push(`Cursor global state database not found at ${discovery.statePath}`);
2759
+ }
2760
+ return {
2761
+ source: {
2762
+ id: "cursor",
2763
+ label: "Cursor",
2764
+ available: discovery.jsonlFiles.length > 0 || discovery.databaseFiles.length > 0,
2765
+ paths: unique4([cursorHome, discovery.projectsPath, discovery.statePath, discovery.workspaceStoragePath, ...discovery.roots]),
2766
+ warnings: discovery.warnings
2767
+ },
2768
+ sessions,
2769
+ stats: {
2770
+ sourceId: "cursor",
2771
+ sourceLabel: "Cursor",
2772
+ homePath: cursorHome,
2773
+ cursorHome,
2774
+ globalStoragePath: discovery.globalStoragePath,
2775
+ workspaceStoragePath: discovery.workspaceStoragePath,
2776
+ projectsPath: discovery.projectsPath,
2777
+ statePath: discovery.statePath,
2778
+ projectsExists: fs6.existsSync(discovery.projectsPath),
2779
+ stateExists: fs6.existsSync(discovery.statePath),
2780
+ sessionsExists: discovery.jsonlFiles.length > 0,
2781
+ sessionFileCount: discovery.jsonlFiles.length,
2782
+ databaseFileCount: discovery.databaseFiles.length,
2783
+ skippedFileCount: discovery.skippedFileCount,
2784
+ sessionsImported: sessions.length,
2785
+ parseFailureCount,
2786
+ unreadableFileCount,
2787
+ malformedFileCount,
2788
+ lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
2789
+ }
2790
+ };
2791
+ }
2792
+ function discoverCursorFiles(cursorHome, explicitHome) {
2793
+ const warnings = [];
2794
+ const appPaths = defaultCursorAppPaths();
2795
+ const roots = explicitHome ? [cursorHome] : unique4([cursorHome, appPaths.statePath, ...cursorOwnedGlobalStorageRoots(appPaths.globalStoragePath), ...cursorOwnedWorkspaceStorageRoots(appPaths.workspaceStoragePath)]);
2796
+ const jsonlFiles = [];
2797
+ const databaseFiles = [];
2798
+ let skippedFileCount = 0;
2799
+ for (const root of roots) {
2800
+ if (!fs6.existsSync(root)) continue;
2801
+ try {
2802
+ if (isReadableRegularFile(root, maxDatabaseBytes)) {
2803
+ databaseFiles.push(root);
2804
+ continue;
2805
+ }
2806
+ walk3(root, 10, (filePath) => {
2807
+ const lower = filePath.toLowerCase();
2808
+ if (lower.endsWith(".jsonl")) {
2809
+ if (isReadableRegularFile(filePath, maxJsonlBytes)) jsonlFiles.push(filePath);
2810
+ else skippedFileCount += 1;
2811
+ return;
2812
+ }
2813
+ if (/\.(sqlite|db|vscdb)$/i.test(filePath)) {
2814
+ if (isReadableRegularFile(filePath, maxDatabaseBytes)) databaseFiles.push(filePath);
2815
+ else skippedFileCount += 1;
2816
+ }
2817
+ });
2818
+ } catch (error) {
2819
+ warnings.push(`Unable to inspect Cursor path ${root}: ${errorMessage3(error)}`);
2820
+ }
2821
+ }
2822
+ return {
2823
+ roots,
2824
+ projectsPath: path6.join(cursorHome, "projects"),
2825
+ globalStoragePath: appPaths.globalStoragePath,
2826
+ workspaceStoragePath: appPaths.workspaceStoragePath,
2827
+ statePath: appPaths.statePath,
2828
+ jsonlFiles: unique4(jsonlFiles).sort(cursorJsonlSort),
2829
+ databaseFiles: unique4(databaseFiles).sort(),
2830
+ skippedFileCount,
2831
+ warnings
2832
+ };
2833
+ }
2834
+ function cursorOwnedGlobalStorageRoots(globalStoragePath) {
2835
+ return cursorOwnedChildDirs(globalStoragePath);
2836
+ }
2837
+ function cursorOwnedWorkspaceStorageRoots(workspaceStoragePath) {
2838
+ if (!fs6.existsSync(workspaceStoragePath)) return [];
2839
+ const roots = [];
2840
+ try {
2841
+ for (const workspace of fs6.readdirSync(workspaceStoragePath, { withFileTypes: true })) {
2842
+ if (!workspace.isDirectory()) continue;
2843
+ roots.push(...cursorOwnedChildDirs(path6.join(workspaceStoragePath, workspace.name)));
2844
+ }
2845
+ } catch {
2846
+ return [];
2847
+ }
2848
+ return roots;
2849
+ }
2850
+ function cursorOwnedChildDirs(root) {
2851
+ if (!fs6.existsSync(root)) return [];
2852
+ try {
2853
+ return fs6.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && cursorStoragePrefixes.some((prefix) => entry.name.toLowerCase().startsWith(prefix))).map((entry) => path6.join(root, entry.name));
2854
+ } catch {
2855
+ return [];
2856
+ }
2857
+ }
2858
+ function defaultCursorAppPaths() {
2859
+ const home = os4.homedir();
2860
+ if (process.platform === "win32") {
2861
+ const appData = process.env.APPDATA ?? path6.join(home, "AppData", "Roaming");
2862
+ const globalStoragePath2 = path6.join(appData, "Cursor", "User", "globalStorage");
2863
+ return {
2864
+ globalStoragePath: globalStoragePath2,
2865
+ workspaceStoragePath: path6.join(appData, "Cursor", "User", "workspaceStorage"),
2866
+ statePath: path6.join(globalStoragePath2, "state.vscdb")
2867
+ };
2868
+ }
2869
+ if (process.platform === "darwin") {
2870
+ const globalStoragePath2 = path6.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
2871
+ return {
2872
+ globalStoragePath: globalStoragePath2,
2873
+ workspaceStoragePath: path6.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"),
2874
+ statePath: path6.join(globalStoragePath2, "state.vscdb")
2875
+ };
2876
+ }
2877
+ const xdg = process.env.XDG_CONFIG_HOME || path6.join(home, ".config");
2878
+ const globalStoragePath = path6.join(xdg, "Cursor", "User", "globalStorage");
2879
+ return {
2880
+ globalStoragePath,
2881
+ workspaceStoragePath: path6.join(xdg, "Cursor", "User", "workspaceStorage"),
2882
+ statePath: path6.join(globalStoragePath, "state.vscdb")
2883
+ };
2884
+ }
2885
+ function walk3(root, maxDepth, onFile, depth = 0) {
2886
+ if (depth > maxDepth) return;
2887
+ for (const entry of fs6.readdirSync(root, { withFileTypes: true })) {
2888
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "CachedData" || entry.name === "Cache") continue;
2889
+ const fullPath = path6.join(root, entry.name);
2890
+ if (entry.isDirectory()) {
2891
+ walk3(fullPath, maxDepth, onFile, depth + 1);
2892
+ } else if (entry.isFile()) {
2893
+ onFile(fullPath);
2894
+ }
2895
+ }
2896
+ }
2897
+ function isReadableRegularFile(filePath, maxBytes) {
2898
+ try {
2899
+ fs6.accessSync(filePath, fs6.constants.R_OK);
2900
+ const stat = fs6.statSync(filePath);
2901
+ return stat.isFile() && stat.size <= maxBytes;
2902
+ } catch {
2903
+ return false;
2904
+ }
2905
+ }
2906
+ function cursorJsonlSort(a, b) {
2907
+ const aTranscript = a.includes(`${path6.sep}agent-transcripts${path6.sep}`) ? 0 : 1;
2908
+ const bTranscript = b.includes(`${path6.sep}agent-transcripts${path6.sep}`) ? 0 : 1;
2909
+ return aTranscript - bTranscript || a.localeCompare(b);
2910
+ }
2911
+ function cursorJsonlToCandidate(filePath, cursorHome) {
2912
+ const breakdown = emptyBreakdown3();
2913
+ try {
2914
+ const parse = parseJsonLines3(fs6.readFileSync(filePath, "utf8"));
2915
+ breakdown.rawEventCount = parse.records.length;
2916
+ breakdown.parseErrors = parse.errors;
2917
+ breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
2918
+ for (const record of parse.records) applyCursorRecord(breakdown, record);
2919
+ finalizeBreakdown(breakdown);
2920
+ } catch (error) {
2921
+ const message = `unable_to_read_cursor_jsonl:${errorMessage3(error)}`;
2922
+ breakdown.parseStatus = "failed";
2923
+ breakdown.parseErrors.push(message);
2924
+ breakdown.warnings.push(message);
2925
+ }
2926
+ if (!breakdown.id) breakdown.id = path6.basename(filePath).replace(/\.jsonl$/i, "") || void 0;
2927
+ return {
2928
+ sourcePath: filePath,
2929
+ surface: inferSurfaceFromPath(filePath),
2930
+ fallbackCwd: inferProjectPathFromCursorPath(filePath, cursorHome),
2931
+ breakdown
2932
+ };
2933
+ }
2934
+ function cursorDatabaseToCandidates(filePath, cursorHome, warnings) {
2935
+ const candidates = [];
2936
+ let db;
2937
+ try {
2938
+ db = new Database2(filePath, { readonly: true, fileMustExist: true });
2939
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
2940
+ for (const table of tables) {
2941
+ const columns = safeColumns(db, table.name, warnings);
2942
+ if (!columns.length) continue;
2943
+ const selectedColumns = columns.filter((column) => isLikelyTextColumn(column.name, column.type));
2944
+ if (!selectedColumns.length) continue;
2945
+ const rows = safeRows(db, table.name, selectedColumns.map((column) => column.name), warnings);
2946
+ for (const row of rows) {
2947
+ const match = candidateFromDatabaseRow(filePath, cursorHome, table.name, row);
2948
+ if (match) candidates.push(match);
2949
+ }
2950
+ }
2951
+ } catch (error) {
2952
+ warnings.push(`Unable to read Cursor SQLite database at ${filePath}: ${errorMessage3(error)}`);
2953
+ } finally {
2954
+ db?.close();
2955
+ }
2956
+ return candidates;
2957
+ }
2958
+ function safeColumns(db, table, warnings) {
2959
+ try {
2960
+ return db.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all();
2961
+ } catch (error) {
2962
+ warnings.push(`Unable to inspect Cursor SQLite table ${table}: ${errorMessage3(error)}`);
2963
+ return [];
2964
+ }
2965
+ }
2966
+ function safeRows(db, table, columns, warnings) {
2967
+ try {
2968
+ const select = columns.map(quoteIdentifier).join(", ");
2969
+ return db.prepare(`SELECT ${select} FROM ${quoteIdentifier(table)} LIMIT 500`).all();
2970
+ } catch (error) {
2971
+ warnings.push(`Unable to scan Cursor SQLite table ${table}: ${errorMessage3(error)}`);
2972
+ return [];
2973
+ }
2974
+ }
2975
+ function quoteIdentifier(value) {
2976
+ return `"${value.replaceAll('"', '""')}"`;
2977
+ }
2978
+ function isLikelyTextColumn(name, type) {
2979
+ const normalized = `${name} ${type}`.toLowerCase();
2980
+ return type === "" || normalized.includes("text") || normalized.includes("char") || normalized.includes("json") || normalized.includes("blob") || interestingTerms.some((term) => normalized.includes(term));
2981
+ }
2982
+ function candidateFromDatabaseRow(filePath, cursorHome, table, row) {
2983
+ const records = [];
2984
+ const matchedKeys = [];
2985
+ const parseErrors = [];
2986
+ const textEntries = Object.entries(row).map(([key, value]) => ({ key, text: textValue(value) })).filter((entry) => Boolean(entry.text));
2987
+ const rowLooksInteresting = textEntries.some(({ key, text }) => looksCursorInteresting(key, text));
2988
+ if (!rowLooksInteresting) return void 0;
2989
+ for (const { key, text } of textEntries) {
2990
+ if (!looksCursorInteresting(key, text) && !text.trim().startsWith("{") && !text.trim().startsWith("[")) continue;
2991
+ matchedKeys.push(`${table}.${key}`);
2992
+ const parsed = parseJsonBlob(text);
2993
+ if (parsed.ok && Array.isArray(parsed.value)) records.push(...parsed.value);
2994
+ else if (parsed.ok) records.push(parsed.value);
2995
+ else records.push({ key, value: text.slice(0, 2e3) });
2996
+ if (!parsed.ok && text.trim().startsWith("{")) parseErrors.push(`sqlite_json_parse:${table}.${key}:${parsed.error}`);
2997
+ }
2998
+ if (!records.length) return void 0;
2999
+ const breakdown = emptyBreakdown3();
3000
+ breakdown.parseErrors = parseErrors;
3001
+ breakdown.parseStatus = parseErrors.length ? "partial" : "ok";
3002
+ breakdown.rawEventCount = records.length;
3003
+ breakdown.matchedKeys = unique4(matchedKeys);
3004
+ for (const record of records) applyCursorRecord(breakdown, record);
3005
+ finalizeBreakdown(breakdown);
3006
+ if (isCursorMetadataOnlyBreakdown(breakdown)) return void 0;
3007
+ return {
3008
+ sourcePath: `${filePath}#${table}`,
3009
+ surface: inferSurfaceFromPath(filePath),
3010
+ fallbackCwd: inferProjectPathFromCursorPath(filePath, cursorHome),
3011
+ breakdown
3012
+ };
3013
+ }
3014
+ function isCursorMetadataOnlyBreakdown(breakdown) {
3015
+ return breakdown.messageCount === 0 && breakdown.usageCount === 0 && !breakdown.hasEstimatedPromptTokens && breakdown.costCount === 0;
3016
+ }
3017
+ function looksCursorInteresting(key, text) {
3018
+ const haystack = `${key}
3019
+ ${text.slice(0, 4e3)}`.toLowerCase();
3020
+ return interestingTerms.some((term) => haystack.includes(term));
3021
+ }
3022
+ function parseJsonBlob(text) {
3023
+ const trimmed = text.trim();
3024
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return { ok: false, error: "not_json" };
3025
+ try {
3026
+ return { ok: true, value: JSON.parse(trimmed) };
3027
+ } catch (error) {
3028
+ return { ok: false, error: errorMessage3(error) };
3029
+ }
3030
+ }
3031
+ function parseJsonLines3(text) {
3032
+ const records = [];
3033
+ const errors = [];
3034
+ text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index })).filter(({ line }) => Boolean(line)).forEach(({ line, index }) => {
3035
+ try {
3036
+ records.push(JSON.parse(line));
3037
+ } catch (error) {
3038
+ errors.push(`jsonl_line_${index + 1}:${errorMessage3(error)}`);
3039
+ }
3040
+ });
3041
+ return { records, errors };
3042
+ }
3043
+ function applyCursorRecord(breakdown, record) {
3044
+ const objects = collectObjects(record, 6, 150);
3045
+ const root = firstObject5(record);
3046
+ const timestamp3 = firstValueByKeys(objects, ["timestamp", "createdAt", "created_at", "lastUpdatedAt", "conversationCheckpointLastUpdatedAt", "time", "date"]);
3047
+ const date = dateValue4(timestamp3);
3048
+ if (date) {
3049
+ breakdown.startedAt = minDate2(breakdown.startedAt, date);
3050
+ breakdown.endedAt = maxDate2(breakdown.endedAt, date);
3051
+ }
3052
+ breakdown.id ??= firstStringByKeys(objects, ["sessionId", "session_id", "conversationId", "conversation_id", "composerId", "composer_id", "chatId", "chat_id"]);
3053
+ breakdown.title ??= firstStringByKeys(objects, ["title", "name", "summary"]);
3054
+ breakdown.cwd ??= firstPathByKeys(objects, ["cwd", "workspacePath", "workspace_path", "projectPath", "project_path", "repoPath", "repo_path", "rootPath", "root_path"]);
3055
+ breakdown.model ??= firstStringByKeys(objects, ["model", "modelName", "model_name", "modelId", "model_id"]);
3056
+ const role = inferRole(objects, root);
3057
+ const text = extractMessageText3(objects, role);
3058
+ if (role === "user") breakdown.userPromptCount += 1;
3059
+ if (role === "assistant") breakdown.assistantMessageCount += 1;
3060
+ if (role === "user" || role === "assistant") {
3061
+ breakdown.messageCount += 1;
3062
+ if (text && breakdown.promptTimeline.length < 80) {
3063
+ breakdown.promptTimeline.push({ role, text: normalizeTimelineText3(text), timestamp: date });
3064
+ }
3065
+ }
3066
+ const usage = extractUsage(objects);
3067
+ if (usage.inputTokens + usage.cachedInputTokens + usage.outputTokens + usage.reasoningTokens + usage.totalTokens > 0) {
3068
+ breakdown.inputTokens += usage.inputTokens;
3069
+ breakdown.cachedInputTokens += usage.cachedInputTokens;
3070
+ breakdown.outputTokens += usage.outputTokens;
3071
+ breakdown.reasoningTokens += usage.reasoningTokens;
3072
+ breakdown.totalTokens += usage.totalTokens;
3073
+ breakdown.usageCount += 1;
3074
+ if (usage.estimatedPromptTokens) breakdown.hasEstimatedPromptTokens = true;
3075
+ }
3076
+ const cost = firstNumberByKeys(objects, ["costUsd", "cost_usd", "estimatedCostUsd", "estimated_cost_usd", "cost"]);
3077
+ if (cost !== void 0 && cost >= 0) {
3078
+ breakdown.estimatedCostUsd = roundCurrency2((breakdown.estimatedCostUsd ?? 0) + cost);
3079
+ breakdown.costCount += 1;
3080
+ }
3081
+ applyToolSignals(breakdown, objects);
3082
+ }
3083
+ function collectObjects(value, maxDepth, maxObjects, objects = []) {
3084
+ if (objects.length >= maxObjects || maxDepth < 0) return objects;
3085
+ if (!value || typeof value !== "object") return objects;
3086
+ if (Array.isArray(value)) {
3087
+ for (const item of value.slice(0, 80)) collectObjects(item, maxDepth - 1, maxObjects, objects);
3088
+ return objects;
3089
+ }
3090
+ const object = value;
3091
+ objects.push(object);
3092
+ for (const child of Object.values(object)) collectObjects(child, maxDepth - 1, maxObjects, objects);
3093
+ return objects;
3094
+ }
3095
+ function firstObject5(value) {
3096
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3097
+ }
3098
+ function firstStringByKeys(objects, keys) {
3099
+ for (const object of objects) {
3100
+ for (const key of keys) {
3101
+ const value = stringValue4(object[key]);
3102
+ if (value) return value;
3103
+ }
3104
+ }
3105
+ return void 0;
3106
+ }
3107
+ function firstValueByKeys(objects, keys) {
3108
+ for (const object of objects) {
3109
+ for (const key of keys) {
3110
+ if (object[key] !== void 0 && object[key] !== null && object[key] !== "") return object[key];
3111
+ }
3112
+ }
3113
+ return void 0;
3114
+ }
3115
+ function firstPathByKeys(objects, keys) {
3116
+ const value = firstStringByKeys(objects, keys);
3117
+ if (!value) return void 0;
3118
+ if (value.startsWith("file://")) return value.replace(/^file:\/\//, "");
3119
+ return value;
3120
+ }
3121
+ function firstNumberByKeys(objects, keys) {
3122
+ for (const object of objects) {
3123
+ for (const key of keys) {
3124
+ const value = numberValue2(object[key]);
3125
+ if (value !== void 0) return value;
3126
+ }
3127
+ }
3128
+ return void 0;
3129
+ }
3130
+ function inferRole(objects, root) {
3131
+ const role = firstStringByKeys(objects, ["role", "author", "speaker", "from"])?.toLowerCase();
3132
+ if (role === "user" || role === "human") return "user";
3133
+ if (role === "assistant" || role === "ai" || role === "agent") return "assistant";
3134
+ const type = (stringValue4(root?.type) ?? stringValue4(root?.event) ?? "").toLowerCase();
3135
+ if (type.includes("user") || type.includes("prompt")) return "user";
3136
+ if (type.includes("assistant") || type.includes("response") || type.includes("completion")) return "assistant";
3137
+ return void 0;
3138
+ }
3139
+ function extractMessageText3(objects, role) {
3140
+ if (!role) return void 0;
3141
+ for (const object of objects) {
3142
+ for (const key of ["text", "content", "message", "prompt", "response", "answer"]) {
3143
+ const text = textFromUnknown(object[key]);
3144
+ if (text) return text;
3145
+ }
3146
+ }
3147
+ return void 0;
3148
+ }
3149
+ function textFromUnknown(value) {
3150
+ if (typeof value === "string" && value.trim()) return value;
3151
+ if (Array.isArray(value)) {
3152
+ const parts = value.map(textFromUnknown).filter((item) => Boolean(item));
3153
+ return parts.length ? parts.join("\n") : void 0;
3154
+ }
3155
+ const object = firstObject5(value);
3156
+ if (!object) return void 0;
3157
+ return stringValue4(object.text) ?? stringValue4(object.content) ?? stringValue4(object.value);
3158
+ }
3159
+ function extractUsage(objects) {
3160
+ const usageObjects = objects.filter((object) => Object.keys(object).some((key) => key.toLowerCase().includes("token") || key.toLowerCase() === "usage"));
3161
+ const candidates = usageObjects.length ? usageObjects : objects;
3162
+ return {
3163
+ inputTokens: firstNumberByKeys(candidates, ["inputTokens", "input_tokens", "promptTokens", "prompt_tokens", "totalUsedTokens"]) ?? 0,
3164
+ cachedInputTokens: firstNumberByKeys(candidates, ["cachedInputTokens", "cached_input_tokens", "cacheReadInputTokens", "cache_read_input_tokens"]) ?? 0,
3165
+ outputTokens: firstNumberByKeys(candidates, ["outputTokens", "output_tokens", "completionTokens", "completion_tokens"]) ?? 0,
3166
+ reasoningTokens: firstNumberByKeys(candidates, ["reasoningTokens", "reasoning_tokens", "reasoningOutputTokens", "reasoning_output_tokens"]) ?? 0,
3167
+ totalTokens: firstNumberByKeys(candidates, ["totalTokens", "total_tokens", "tokens", "tokenCount", "token_count"]) ?? 0,
3168
+ estimatedPromptTokens: firstNumberByKeys(candidates, ["totalUsedTokens"]) !== void 0
3169
+ };
3170
+ }
3171
+ function applyToolSignals(breakdown, objects) {
3172
+ const typeText = objects.flatMap((object) => [stringValue4(object.type), stringValue4(object.event), stringValue4(object.toolName), stringValue4(object.tool_name), stringValue4(object.name)]).filter((item) => Boolean(item)).join(" ").toLowerCase();
3173
+ if (typeText.includes("tool")) breakdown.toolCallCount += 1;
3174
+ if (typeText.includes("terminal") || typeText.includes("shell") || typeText.includes("command")) breakdown.shellCommandCount += 1;
3175
+ if (typeText.includes("read")) breakdown.fileReadCount += 1;
3176
+ if (typeText.includes("edit") || typeText.includes("write") || typeText.includes("apply")) breakdown.fileEditCount += 1;
3177
+ if (objects.some(looksFailed)) breakdown.failedToolCallCount += 1;
3178
+ }
3179
+ function looksFailed(object) {
3180
+ if (object.isError === true || object.is_error === true || object.error === true) return true;
3181
+ const status = stringValue4(object.status)?.toLowerCase();
3182
+ if (status === "failed" || status === "error") return true;
3183
+ const exitCode = numberValue2(object.exitCode ?? object.exit_code ?? object.statusCode ?? object.status_code);
3184
+ return exitCode !== void 0 && exitCode !== 0;
3185
+ }
3186
+ function finalizeBreakdown(breakdown) {
3187
+ if (breakdown.totalTokens === 0) {
3188
+ breakdown.totalTokens = breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens;
3189
+ }
3190
+ if (breakdown.messageCount === 0) {
3191
+ breakdown.messageCount = breakdown.userPromptCount + breakdown.assistantMessageCount;
3192
+ }
3193
+ }
3194
+ function candidateToUsage(candidate, index, options) {
3195
+ const breakdown = candidate.breakdown;
3196
+ const id = breakdown.id ?? `${path6.basename(candidate.sourcePath).replace(/\.(jsonl|sqlite|db|vscdb)$/i, "") || "cursor-session"}-${index}`;
3197
+ const cwd = breakdown.cwd ?? candidate.fallbackCwd ?? process.cwd();
3198
+ const repo = resolveRepoInfo(cwd, options.config);
3199
+ const hasTokenBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
3200
+ const tokenAggregationMethod = hasTokenBreakdown ? breakdown.hasEstimatedPromptTokens ? "estimated" : "direct_usage" : "unknown";
3201
+ const tokenWarnings = breakdown.hasEstimatedPromptTokens ? ["cursor_prompt_token_breakdown_estimate"] : [];
3202
+ const usage = {
3203
+ id,
3204
+ sourceClient: "cursor",
3205
+ sourceApp: cursorSourceAppLabel(candidate.surface),
3206
+ sourceAppRaw: candidate.surface,
3207
+ sourcePath: candidate.sourcePath,
3208
+ repoRoot: repo.repoRoot,
3209
+ repoName: repo.repoName,
3210
+ cwd,
3211
+ gitRemoteUrl: repo.gitRemoteUrl,
3212
+ gitBranch: repo.gitBranch,
3213
+ title: breakdown.title,
3214
+ startedAt: breakdown.startedAt,
3215
+ endedAt: breakdown.endedAt,
3216
+ model: breakdown.model,
3217
+ provider: providerForModel2(breakdown.model ?? ""),
3218
+ inputTokens: breakdown.inputTokens,
3219
+ cachedInputTokens: breakdown.cachedInputTokens,
3220
+ outputTokens: breakdown.outputTokens,
3221
+ reasoningTokens: breakdown.reasoningTokens,
3222
+ reasoningOutputTokens: breakdown.reasoningTokens,
3223
+ totalTokens: breakdown.totalTokens,
3224
+ tokenAggregationMethod,
3225
+ tokenConfidence: breakdown.hasEstimatedPromptTokens ? "low" : hasTokenBreakdown ? "medium" : "low",
3226
+ tokenSnapshotCount: breakdown.usageCount,
3227
+ estimatedCostUsd: void 0,
3228
+ messageCount: breakdown.messageCount,
3229
+ rawTokenTotal: breakdown.totalTokens || void 0,
3230
+ warnings: [...repo.warnings, ...breakdown.warnings, ...tokenWarnings, ...hasTokenBreakdown ? [] : ["missing_token_breakdown"]],
3231
+ durationMs: durationMs3(breakdown.startedAt, breakdown.endedAt),
3232
+ rawEventCount: breakdown.rawEventCount,
3233
+ parseStatus: breakdown.parseStatus,
3234
+ parseErrors: breakdown.parseErrors,
3235
+ detectedSurface: detectedSurface(candidate.surface),
3236
+ surfaceConfidence: candidate.surface === "unknown" ? "low" : "medium",
3237
+ surfaceReason: cursorSurfaceReason(candidate.surface),
3238
+ userPromptCount: breakdown.userPromptCount,
3239
+ assistantMessageCount: breakdown.assistantMessageCount,
3240
+ toolCallCount: breakdown.toolCallCount,
3241
+ shellCommandCount: breakdown.shellCommandCount,
3242
+ failedToolCallCount: breakdown.failedToolCallCount,
3243
+ nonZeroCommandEvents: 0,
3244
+ importantCommandFailures: breakdown.failedToolCallCount,
3245
+ harmlessNonZeroEvents: 0,
3246
+ exploratoryMisses: 0,
3247
+ repeatedFailureClusters: 0,
3248
+ commandIssueSeverity: breakdown.failedToolCallCount > 0 ? "warning" : "none",
3249
+ commandIssueImpact: breakdown.failedToolCallCount > 0 ? "medium" : "none",
3250
+ topFailureType: breakdown.failedToolCallCount > 0 ? "unknown" : void 0,
3251
+ commandIssueSamples: [],
3252
+ fileReadCount: breakdown.fileReadCount,
3253
+ fileEditCount: breakdown.fileEditCount,
3254
+ promptTimeline: breakdown.promptTimeline,
3255
+ sessionOutcome: inferOutcome3(breakdown),
3256
+ sourceMetadata: {
3257
+ parserVersion,
3258
+ cursor: {
3259
+ surface: candidate.surface,
3260
+ matchedKeys: breakdown.matchedKeys,
3261
+ costRecords: breakdown.costCount,
3262
+ estimatedPromptTokens: breakdown.hasEstimatedPromptTokens
3263
+ }
3264
+ }
3265
+ };
3266
+ const pricedCost = hasTokenBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
3267
+ const estimatedCostUsd = pricedCost ?? breakdown.estimatedCostUsd;
3268
+ return {
3269
+ ...usage,
3270
+ estimatedCostUsd,
3271
+ warnings: estimatedCostUsd === void 0 ? [...usage.warnings, breakdown.hasEstimatedPromptTokens ? "cursor_cost_not_estimated_from_prompt_tokens" : hasTokenBreakdown ? "unknown_pricing" : "unknown_cost"] : usage.warnings
3272
+ };
3273
+ }
3274
+ function inferProjectPathFromCursorPath(filePath, cursorHome) {
3275
+ const parts = filePath.split(path6.sep);
3276
+ const projectsIndex = parts.lastIndexOf("projects");
3277
+ if (projectsIndex >= 0 && projectsIndex < parts.length - 1) {
3278
+ const projectId = parts[projectsIndex + 1];
3279
+ if (projectId?.startsWith("-")) {
3280
+ return `${path6.sep}${projectId.slice(1).split("-").filter(Boolean).join(path6.sep)}`;
3281
+ }
3282
+ if (projectId) return path6.join(cursorHome, "projects", projectId);
3283
+ }
3284
+ return void 0;
3285
+ }
3286
+ function inferSurfaceFromPath(filePath) {
3287
+ const lower = filePath.toLowerCase();
3288
+ if (lower.includes(`${path6.sep}agent-transcripts${path6.sep}`) || lower.endsWith(".jsonl")) return "cli";
3289
+ if (lower.includes(`${path6.sep}workspaceStorage${path6.sep}`.toLowerCase()) || lower.includes(`${path6.sep}globalStorage${path6.sep}`.toLowerCase()) || lower.endsWith(".vscdb")) return "desktop";
3290
+ return "unknown";
3291
+ }
3292
+ function cursorSourceAppLabel(surface) {
3293
+ if (surface === "cli") return "Cursor CLI";
3294
+ if (surface === "desktop") return "Cursor Desktop";
3295
+ return "Cursor";
3296
+ }
3297
+ function detectedSurface(surface) {
3298
+ if (surface === "cli") return "terminal_cli";
3299
+ if (surface === "desktop") return "vscode_extension";
3300
+ return "unknown";
3301
+ }
3302
+ function cursorSurfaceReason(surface) {
3303
+ if (surface === "cli") return "Cursor local JSONL transcript";
3304
+ if (surface === "desktop") return "Cursor local SQLite/vscdb storage";
3305
+ return "Cursor local data";
3306
+ }
3307
+ function inferOutcome3(breakdown) {
3308
+ if (breakdown.parseStatus === "failed") return "failed";
3309
+ if (breakdown.failedToolCallCount > 0) return "partial";
3310
+ if (breakdown.fileEditCount > 0) return "completed";
3311
+ if (breakdown.shellCommandCount > 0) return "setup_debugging";
3312
+ if (breakdown.userPromptCount > 0 || breakdown.assistantMessageCount > 0) return "research_only";
3313
+ return "unknown";
3314
+ }
3315
+ function emptyBreakdown3() {
3316
+ return {
3317
+ id: void 0,
3318
+ title: void 0,
3319
+ cwd: void 0,
3320
+ startedAt: void 0,
3321
+ endedAt: void 0,
3322
+ model: void 0,
3323
+ inputTokens: 0,
3324
+ cachedInputTokens: 0,
3325
+ outputTokens: 0,
3326
+ reasoningTokens: 0,
3327
+ totalTokens: 0,
3328
+ usageCount: 0,
3329
+ estimatedCostUsd: void 0,
3330
+ costCount: 0,
3331
+ hasEstimatedPromptTokens: false,
3332
+ messageCount: 0,
3333
+ userPromptCount: 0,
3334
+ assistantMessageCount: 0,
3335
+ toolCallCount: 0,
3336
+ shellCommandCount: 0,
3337
+ failedToolCallCount: 0,
3338
+ fileReadCount: 0,
3339
+ fileEditCount: 0,
3340
+ rawEventCount: 0,
3341
+ parseStatus: "ok",
3342
+ parseErrors: [],
3343
+ promptTimeline: [],
3344
+ warnings: [],
3345
+ matchedKeys: []
3346
+ };
3347
+ }
3348
+ function dedupeCandidates(candidates) {
3349
+ const byId = /* @__PURE__ */ new Map();
3350
+ const pathOnly = [];
3351
+ for (const candidate of candidates) {
3352
+ const id = candidate.breakdown.id;
3353
+ if (!id) {
3354
+ pathOnly.push(candidate);
3355
+ continue;
3356
+ }
3357
+ const existing = byId.get(id);
3358
+ byId.set(id, existing ? mergeCursorCandidates(existing, candidate) : candidate);
3359
+ }
3360
+ return [...byId.values(), ...pathOnly];
3361
+ }
3362
+ function mergeCursorCandidates(a, b) {
3363
+ const primary = cursorCandidateScore(b) > cursorCandidateScore(a) ? b : a;
3364
+ const secondary = primary === a ? b : a;
3365
+ return {
3366
+ ...primary,
3367
+ surface: primary.surface === "unknown" ? secondary.surface : primary.surface,
3368
+ fallbackCwd: primary.fallbackCwd ?? secondary.fallbackCwd,
3369
+ breakdown: mergeCursorBreakdowns(primary.breakdown, secondary.breakdown)
3370
+ };
3371
+ }
3372
+ function cursorCandidateScore(candidate) {
3373
+ const breakdown = candidate.breakdown;
3374
+ const hasTokenBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
3375
+ return (hasTokenBreakdown ? 1e9 : 0) + breakdown.totalTokens + breakdown.messageCount * 1e3 + breakdown.rawEventCount;
3376
+ }
3377
+ function mergeCursorBreakdowns(primary, secondary) {
3378
+ return {
3379
+ ...primary,
3380
+ title: primary.title ?? secondary.title,
3381
+ cwd: primary.cwd ?? secondary.cwd,
3382
+ startedAt: optionalMinDate(primary.startedAt, secondary.startedAt),
3383
+ endedAt: optionalMaxDate(primary.endedAt, secondary.endedAt),
3384
+ model: primary.model ?? secondary.model,
3385
+ estimatedCostUsd: primary.estimatedCostUsd ?? secondary.estimatedCostUsd,
3386
+ costCount: Math.max(primary.costCount, secondary.costCount),
3387
+ hasEstimatedPromptTokens: primary.hasEstimatedPromptTokens || secondary.hasEstimatedPromptTokens,
3388
+ messageCount: Math.max(primary.messageCount, secondary.messageCount),
3389
+ userPromptCount: Math.max(primary.userPromptCount, secondary.userPromptCount),
3390
+ assistantMessageCount: Math.max(primary.assistantMessageCount, secondary.assistantMessageCount),
3391
+ toolCallCount: Math.max(primary.toolCallCount, secondary.toolCallCount),
3392
+ shellCommandCount: Math.max(primary.shellCommandCount, secondary.shellCommandCount),
3393
+ failedToolCallCount: Math.max(primary.failedToolCallCount, secondary.failedToolCallCount),
3394
+ fileReadCount: Math.max(primary.fileReadCount, secondary.fileReadCount),
3395
+ fileEditCount: Math.max(primary.fileEditCount, secondary.fileEditCount),
3396
+ rawEventCount: Math.max(primary.rawEventCount, secondary.rawEventCount),
3397
+ parseStatus: mergeParseStatus(primary.parseStatus, secondary.parseStatus),
3398
+ parseErrors: unique4([...primary.parseErrors, ...secondary.parseErrors]),
3399
+ promptTimeline: primary.promptTimeline.length >= secondary.promptTimeline.length ? primary.promptTimeline : secondary.promptTimeline,
3400
+ warnings: unique4([...primary.warnings, ...secondary.warnings, "cursor_duplicate_session_merged"]),
3401
+ matchedKeys: unique4([...primary.matchedKeys, ...secondary.matchedKeys])
3402
+ };
3403
+ }
3404
+ function mergeParseStatus(a, b) {
3405
+ if (a === "failed" || b === "failed") return "failed";
3406
+ if (a === "partial" || b === "partial") return "partial";
3407
+ return "ok";
3408
+ }
3409
+ function optionalMinDate(current, candidate) {
3410
+ return candidate ? minDate2(current, candidate) : current;
3411
+ }
3412
+ function optionalMaxDate(current, candidate) {
3413
+ return candidate ? maxDate2(current, candidate) : current;
3414
+ }
3415
+ function providerForModel2(model) {
3416
+ const normalized = model.toLowerCase();
3417
+ if (!normalized) return void 0;
3418
+ if (normalized.includes("claude")) return "anthropic";
3419
+ if (normalized.startsWith("gpt") || normalized.startsWith("o")) return "openai";
3420
+ return void 0;
3421
+ }
3422
+ function stringValue4(value) {
3423
+ return typeof value === "string" && value.trim() ? value : void 0;
3424
+ }
3425
+ function textValue(value) {
3426
+ if (typeof value === "string") return value;
3427
+ if (Buffer.isBuffer(value)) return value.toString("utf8");
3428
+ return void 0;
3429
+ }
3430
+ function numberValue2(value) {
3431
+ if (typeof value === "number" && Number.isFinite(value)) return value;
3432
+ if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
3433
+ return void 0;
3434
+ }
3435
+ function dateValue4(value) {
3436
+ if (typeof value === "number" && Number.isFinite(value)) return new Date(value < 1e10 ? value * 1e3 : value).toISOString();
3437
+ if (typeof value !== "string") return void 0;
3438
+ const date = new Date(value);
3439
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
3440
+ }
3441
+ function minDate2(current, candidate) {
3442
+ return !current || candidate < current ? candidate : current;
3443
+ }
3444
+ function maxDate2(current, candidate) {
3445
+ return !current || candidate > current ? candidate : current;
3446
+ }
3447
+ function durationMs3(startedAt, endedAt) {
3448
+ if (!startedAt || !endedAt) return void 0;
3449
+ const start = new Date(startedAt).getTime();
3450
+ const end = new Date(endedAt).getTime();
3451
+ return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : void 0;
3452
+ }
3453
+ function normalizeTimelineText3(text) {
3454
+ return text.replace(/\s+/g, " ").trim().slice(0, 1e3);
3455
+ }
3456
+ function roundCurrency2(value) {
3457
+ return Number(value.toFixed(6));
3458
+ }
3459
+ function unique4(values) {
3460
+ return [...new Set(values.filter(Boolean))];
3461
+ }
3462
+ function errorMessage3(error) {
3463
+ return error instanceof Error ? error.message : String(error);
3464
+ }
3465
+
2582
3466
  // packages/core/src/sources/index.ts
2583
3467
  function scanUsageSources(options) {
2584
3468
  const codex = scanCodex({
@@ -2591,15 +3475,20 @@ function scanUsageSources(options) {
2591
3475
  ...options.config ? { config: options.config } : {},
2592
3476
  pricing: options.pricing
2593
3477
  });
3478
+ const cursor = scanCursor({
3479
+ ...options.cursorHome ? { cursorHome: options.cursorHome } : {},
3480
+ ...options.config ? { config: options.config } : {},
3481
+ pricing: options.pricing
3482
+ });
2594
3483
  return {
2595
- sources: [codex.source, claude.source],
2596
- sourceStats: [codex.stats, claude.stats],
2597
- sessions: [...codex.sessions, ...claude.sessions]
3484
+ sources: [codex.source, claude.source, cursor.source],
3485
+ sourceStats: [codex.stats, claude.stats, cursor.stats],
3486
+ sessions: [...codex.sessions, ...claude.sessions, ...cursor.sessions]
2598
3487
  };
2599
3488
  }
2600
3489
 
2601
3490
  // apps/server/src/data.ts
2602
- import fs6 from "node:fs";
3491
+ import fs7 from "node:fs";
2603
3492
  var scanCacheTtlMs = 1e4;
2604
3493
  var scanCache;
2605
3494
  function readDashboardData(filters = {}, options = {}) {
@@ -2666,8 +3555,8 @@ function writePricingData(models) {
2666
3555
  }
2667
3556
  function clearRepoSpendLocalData() {
2668
3557
  const target = repospendHome();
2669
- const removed = fs6.existsSync(target);
2670
- fs6.rmSync(target, { recursive: true, force: true });
3558
+ const removed = fs7.existsSync(target);
3559
+ fs7.rmSync(target, { recursive: true, force: true });
2671
3560
  clearScanCache();
2672
3561
  return { path: target, removed };
2673
3562
  }
@@ -2723,8 +3612,8 @@ function runtimeCwd() {
2723
3612
  }
2724
3613
 
2725
3614
  // apps/server/src/server.ts
2726
- import fs7 from "node:fs";
2727
- import path6 from "node:path";
3615
+ import fs8 from "node:fs";
3616
+ import path7 from "node:path";
2728
3617
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2729
3618
  import fastifyStatic from "@fastify/static";
2730
3619
  import Fastify from "fastify";
@@ -3564,9 +4453,9 @@ function createServer(options = {}) {
3564
4453
  return exportCsv();
3565
4454
  });
3566
4455
  if (options.serveWeb !== false) {
3567
- const serverDist = path6.dirname(fileURLToPath2(import.meta.url));
3568
- const packagedWebDist = path6.resolve(serverDist, "../web-dist");
3569
- const workspaceWebDist = path6.resolve(serverDist, "../../web/dist");
4456
+ const serverDist = path7.dirname(fileURLToPath2(import.meta.url));
4457
+ const packagedWebDist = path7.resolve(serverDist, "../web-dist");
4458
+ const workspaceWebDist = path7.resolve(serverDist, "../../web/dist");
3570
4459
  const webDist = fsExists(packagedWebDist) ? packagedWebDist : workspaceWebDist;
3571
4460
  if (!fsExists(webDist)) {
3572
4461
  throw new Error(`RepoSpend web assets are missing. Expected ${packagedWebDist} in the published package, or ${workspaceWebDist} during local development. Run "pnpm pack:prepare" before packaging.`);
@@ -3587,13 +4476,13 @@ function createServer(options = {}) {
3587
4476
  return app;
3588
4477
  }
3589
4478
  function fsExists(inputPath) {
3590
- return Boolean(inputPath) && path6.isAbsolute(inputPath) && fs7.existsSync(inputPath);
4479
+ return Boolean(inputPath) && path7.isAbsolute(inputPath) && fs8.existsSync(inputPath);
3591
4480
  }
3592
4481
  function readPackageVersion() {
3593
- const serverDist = path6.dirname(fileURLToPath2(import.meta.url));
3594
- const packagePath = path6.resolve(serverDist, "../package.json");
4482
+ const serverDist = path7.dirname(fileURLToPath2(import.meta.url));
4483
+ const packagePath = path7.resolve(serverDist, "../package.json");
3595
4484
  try {
3596
- const parsed = JSON.parse(fs7.readFileSync(packagePath, "utf8"));
4485
+ const parsed = JSON.parse(fs8.readFileSync(packagePath, "utf8"));
3597
4486
  return typeof parsed.version === "string" ? parsed.version : "unknown";
3598
4487
  } catch {
3599
4488
  return "unknown";
@@ -3689,7 +4578,7 @@ function openBrowser(url) {
3689
4578
  }
3690
4579
  function printGroups(groups) {
3691
4580
  if (!groups.length) {
3692
- console.log("No usage found. Scanned Codex and Claude Code local data paths.");
4581
+ console.log("No usage found. Scanned Codex, Claude Code, and Cursor local data paths.");
3693
4582
  return;
3694
4583
  }
3695
4584
  const rows = groups.map((group) => ({
@@ -3722,5 +4611,5 @@ function printHelp() {
3722
4611
  repospend export --format csv
3723
4612
 
3724
4613
  Options:
3725
- --source all|codex|claude`);
4614
+ --source all|codex|claude|cursor`);
3726
4615
  }