repospend 0.0.6 → 0.0.8
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/CHANGELOG.md +22 -0
- package/README.md +146 -90
- package/dist/cli.js +952 -42
- package/docs/screenshots/agent-friction.png +0 -0
- package/docs/screenshots/dashboard-overview.png +0 -0
- package/docs/screenshots/models-view.png +0 -0
- package/docs/screenshots/repo-detail.png +0 -0
- package/docs/screenshots/repos-view.png +0 -0
- package/docs/screenshots/session-detail.png +0 -0
- package/docs/screenshots/sessions-view.png +0 -0
- package/package.json +2 -1
- package/web-dist/assets/index-CcjRLAsK.css +1 -0
- package/web-dist/assets/index-CruklDZf.js +306 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-BxuOiBnq.js +0 -265
- package/web-dist/assets/index-KkDIavlL.css +0 -1
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
|
|
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
|
-
|
|
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
|
|
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
|
|
2228
|
-
const breakdown = readClaudeBreakdown(file.filePath,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
2347
|
-
|
|
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
|
|
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
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
return
|
|
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
|
|
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 =
|
|
2670
|
-
|
|
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
|
|
2727
|
-
import
|
|
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 =
|
|
3568
|
-
const packagedWebDist =
|
|
3569
|
-
const workspaceWebDist =
|
|
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) &&
|
|
4479
|
+
return Boolean(inputPath) && path7.isAbsolute(inputPath) && fs8.existsSync(inputPath);
|
|
3591
4480
|
}
|
|
3592
4481
|
function readPackageVersion() {
|
|
3593
|
-
const serverDist =
|
|
3594
|
-
const packagePath =
|
|
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(
|
|
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";
|
|
@@ -3602,9 +4491,28 @@ function readPackageVersion() {
|
|
|
3602
4491
|
async function startServer(options = {}) {
|
|
3603
4492
|
const port = options.port ?? Number(process.env.REPOSPEND_PORT ?? 2005);
|
|
3604
4493
|
const host = options.host ?? "127.0.0.1";
|
|
3605
|
-
const
|
|
3606
|
-
|
|
3607
|
-
|
|
4494
|
+
const candidatePorts = portCandidates(port, options.portFallbackCount ?? 20);
|
|
4495
|
+
let lastError;
|
|
4496
|
+
for (const candidatePort of candidatePorts) {
|
|
4497
|
+
const app = createServer(options);
|
|
4498
|
+
try {
|
|
4499
|
+
await app.listen({ host, port: candidatePort });
|
|
4500
|
+
return { app, url: `http://localhost:${candidatePort}`, port: candidatePort };
|
|
4501
|
+
} catch (error) {
|
|
4502
|
+
await app.close().catch(() => void 0);
|
|
4503
|
+
lastError = error;
|
|
4504
|
+
if (!isAddressInUse(error)) throw error;
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
throw lastError;
|
|
4508
|
+
}
|
|
4509
|
+
function portCandidates(startPort, fallbackCount) {
|
|
4510
|
+
if (!Number.isInteger(startPort) || startPort <= 0 || startPort > 65535) return [2005];
|
|
4511
|
+
const count2 = Math.max(0, Math.min(fallbackCount, 100));
|
|
4512
|
+
return Array.from({ length: count2 + 1 }, (_value, index) => startPort + index).filter((port) => port <= 65535);
|
|
4513
|
+
}
|
|
4514
|
+
function isAddressInUse(error) {
|
|
4515
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE");
|
|
3608
4516
|
}
|
|
3609
4517
|
function validatePricingBody(body) {
|
|
3610
4518
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
@@ -3648,7 +4556,9 @@ try {
|
|
|
3648
4556
|
if (command === "--help" || command === "-h" || command === "help") {
|
|
3649
4557
|
printHelp();
|
|
3650
4558
|
} else if (command === "serve") {
|
|
3651
|
-
const
|
|
4559
|
+
const requestedPort = Number(process.env.REPOSPEND_PORT ?? 2005);
|
|
4560
|
+
const { url, port } = await startServer({ port: requestedPort, serveWeb: true });
|
|
4561
|
+
if (port !== requestedPort) console.log(`Port ${requestedPort} is already in use; using ${port} instead.`);
|
|
3652
4562
|
console.log(`RepoSpend dashboard: ${url}`);
|
|
3653
4563
|
openBrowser(url);
|
|
3654
4564
|
} else if (command === "scan") {
|
|
@@ -3689,7 +4599,7 @@ function openBrowser(url) {
|
|
|
3689
4599
|
}
|
|
3690
4600
|
function printGroups(groups) {
|
|
3691
4601
|
if (!groups.length) {
|
|
3692
|
-
console.log("No usage found. Scanned Codex
|
|
4602
|
+
console.log("No usage found. Scanned Codex, Claude Code, and Cursor local data paths.");
|
|
3693
4603
|
return;
|
|
3694
4604
|
}
|
|
3695
4605
|
const rows = groups.map((group) => ({
|
|
@@ -3722,5 +4632,5 @@ function printHelp() {
|
|
|
3722
4632
|
repospend export --format csv
|
|
3723
4633
|
|
|
3724
4634
|
Options:
|
|
3725
|
-
--source all|codex|claude`);
|
|
4635
|
+
--source all|codex|claude|cursor`);
|
|
3726
4636
|
}
|