repospend 0.0.9 → 0.1.0
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 +17 -0
- package/README.md +129 -124
- package/dist/cli.js +1893 -409
- package/docs/data-sources.md +121 -0
- package/docs/llms.txt +59 -0
- package/docs/pricing.md +44 -0
- 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/docs/token-accounting.md +66 -0
- package/package.json +12 -2
- package/web-dist/assets/index-CdTTbCdO.css +1 -0
- package/web-dist/assets/index-DxlpLT8B.js +308 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-D9bkUUxw.css +0 -1
- package/web-dist/assets/index-L2zVlSNd.js +0 -306
package/dist/cli.js
CHANGED
|
@@ -51,7 +51,12 @@ function groupByRepo(sessions) {
|
|
|
51
51
|
}));
|
|
52
52
|
}
|
|
53
53
|
function groupByModel(sessions) {
|
|
54
|
-
|
|
54
|
+
const groups = groupBy(sessions, (session) => session.model ?? "unknown-model", (session) => session.model ?? "Model not recorded");
|
|
55
|
+
const unknownModelSessions = sessions.filter((session) => !session.model);
|
|
56
|
+
if (unknownModelSessions.length > 0 && unknownModelSessions.every(isClaudeSyntheticZeroUsageSession)) {
|
|
57
|
+
return groups.map((group) => group.id === "unknown-model" ? { ...group, label: "Claude synthetic / no usage" } : group);
|
|
58
|
+
}
|
|
59
|
+
return groups;
|
|
55
60
|
}
|
|
56
61
|
function groupBySourceApp(sessions) {
|
|
57
62
|
return groupBy(sessions, (session) => session.sourceApp, (session) => session.sourceApp);
|
|
@@ -100,6 +105,9 @@ function groupBy(sessions, keyFn, labelFn) {
|
|
|
100
105
|
}
|
|
101
106
|
return [...groups.values()].sort((a, b) => (b.estimatedCostUsd ?? 0) - (a.estimatedCostUsd ?? 0) || b.totalTokens - a.totalTokens);
|
|
102
107
|
}
|
|
108
|
+
function isClaudeSyntheticZeroUsageSession(session) {
|
|
109
|
+
return session.warnings.includes("claude_synthetic_zero_usage");
|
|
110
|
+
}
|
|
103
111
|
function toCsv(sessions) {
|
|
104
112
|
const columns = [
|
|
105
113
|
"id",
|
|
@@ -204,12 +212,99 @@ import path2 from "node:path";
|
|
|
204
212
|
import fs from "node:fs";
|
|
205
213
|
import os from "node:os";
|
|
206
214
|
import path from "node:path";
|
|
215
|
+
|
|
216
|
+
// packages/types/src/index.ts
|
|
217
|
+
function normalizePricingModelId(model) {
|
|
218
|
+
return model.toLowerCase().replace(/^github_copilot\//, "").replace(/^github-copilot\//, "").replace(/^copilot\//, "").replace(/claude-(opus|sonnet|haiku)-(\d+)\.(\d+)(?=$|[-.])/, "claude-$1-$2-$3");
|
|
219
|
+
}
|
|
220
|
+
function claudePricingFamilyModel(model, isUsable = () => true) {
|
|
221
|
+
const families = [
|
|
222
|
+
"claude-opus-4-7",
|
|
223
|
+
"claude-opus-4-6",
|
|
224
|
+
"claude-opus-4-5",
|
|
225
|
+
"claude-opus-4-1",
|
|
226
|
+
"claude-opus-4",
|
|
227
|
+
"claude-sonnet-4-6",
|
|
228
|
+
"claude-sonnet-4-5",
|
|
229
|
+
"claude-sonnet-4",
|
|
230
|
+
"claude-haiku-4-5",
|
|
231
|
+
"claude-3-5-haiku"
|
|
232
|
+
];
|
|
233
|
+
const normalized = normalizePricingModelId(model);
|
|
234
|
+
if (normalized === "claude-opus-4-6-fast-mode") return void 0;
|
|
235
|
+
return families.find((candidate) => (normalized === candidate || normalized.startsWith(`${candidate}-`) || normalized.startsWith(`${candidate}.`)) && isUsable(candidate));
|
|
236
|
+
}
|
|
237
|
+
function copilotPricingFamilyModel(model, isUsable = () => true) {
|
|
238
|
+
const normalized = normalizePricingModelId(model);
|
|
239
|
+
const claudeFamily = claudePricingFamilyModel(normalized, isUsable);
|
|
240
|
+
if (claudeFamily) return claudeFamily;
|
|
241
|
+
if ((normalized === "gpt-4.1" || normalized.startsWith("gpt-4.1-") || normalized.startsWith("gpt-41")) && isUsable("gpt-4.1")) return "gpt-4.1";
|
|
242
|
+
if ((normalized === "gemini-2.5-pro" || normalized.startsWith("gemini-2.5-pro-")) && isUsable("gemini-2.5-pro")) return "gemini-2.5-pro";
|
|
243
|
+
if ((normalized === "gemini-3-flash" || normalized.startsWith("gemini-3-flash-")) && isUsable("gemini-3-flash")) return "gemini-3-flash";
|
|
244
|
+
if ((normalized === "gemini-3.1-pro" || normalized.startsWith("gemini-3.1-pro-")) && isUsable("gemini-3.1-pro")) return "gemini-3.1-pro";
|
|
245
|
+
if ((normalized === "gemini-3.5-flash" || normalized.startsWith("gemini-3.5-flash-")) && isUsable("gemini-3.5-flash")) return "gemini-3.5-flash";
|
|
246
|
+
if ((normalized === "raptor-mini" || normalized.startsWith("raptor-mini-") || normalized.startsWith("oswe-vscode")) && isUsable("raptor-mini")) return "raptor-mini";
|
|
247
|
+
if ((normalized === "lark" || normalized.startsWith("lark-")) && isUsable("lark")) return "lark";
|
|
248
|
+
if ((normalized === "goldeneye" || normalized.startsWith("goldeneye-")) && isUsable("goldeneye")) return "goldeneye";
|
|
249
|
+
return void 0;
|
|
250
|
+
}
|
|
251
|
+
function parseVersionedModel(normalized) {
|
|
252
|
+
if (normalized.includes("fast-mode")) return void 0;
|
|
253
|
+
const claude = normalized.match(/^(claude-(?:opus|sonnet|haiku))-(\d+)(?:-(\d+))?/);
|
|
254
|
+
if (claude) {
|
|
255
|
+
const minor = claude[3] ? Number(claude[3]) : 0;
|
|
256
|
+
return { tier: claude[1], version: Number(claude[2]) + minor / 100 };
|
|
257
|
+
}
|
|
258
|
+
const gpt = normalized.match(/^(gpt)-(\d)(?:\.(\d+))?(-[a-z][a-z0-9-]*)?$/);
|
|
259
|
+
if (gpt) {
|
|
260
|
+
const minor = gpt[3] ? Number(gpt[3]) : 0;
|
|
261
|
+
return { tier: `gpt${gpt[4] ?? ""}`, version: Number(gpt[2]) + minor / 100 };
|
|
262
|
+
}
|
|
263
|
+
return void 0;
|
|
264
|
+
}
|
|
265
|
+
function versionedFallbackModel(model, knownModels, isUsable = () => true) {
|
|
266
|
+
const target = parseVersionedModel(normalizePricingModelId(model));
|
|
267
|
+
if (!target) return void 0;
|
|
268
|
+
let best;
|
|
269
|
+
for (const candidate of knownModels) {
|
|
270
|
+
const parsed = parseVersionedModel(normalizePricingModelId(candidate));
|
|
271
|
+
if (!parsed || parsed.tier !== target.tier || parsed.version > target.version) continue;
|
|
272
|
+
if (!isUsable(candidate)) continue;
|
|
273
|
+
if (!best || parsed.version > best.version) best = { id: candidate, version: parsed.version };
|
|
274
|
+
}
|
|
275
|
+
return best?.id;
|
|
276
|
+
}
|
|
277
|
+
function isCopilotAliasModel(model) {
|
|
278
|
+
const normalized = normalizePricingModelId(model);
|
|
279
|
+
return normalized.startsWith("raptor-mini") || normalized.startsWith("oswe-vscode") || normalized === "lark" || normalized.startsWith("lark-") || normalized.startsWith("goldeneye");
|
|
280
|
+
}
|
|
281
|
+
function positiveRate(value) {
|
|
282
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
283
|
+
}
|
|
284
|
+
function isUsableModelPricing(pricing) {
|
|
285
|
+
return positiveRate(pricing?.inputPerMillion) && positiveRate(pricing?.outputPerMillion);
|
|
286
|
+
}
|
|
287
|
+
function findModelPricing(model, pricing) {
|
|
288
|
+
const lower = normalizePricingModelId(model);
|
|
289
|
+
const exact = pricing[model] ?? pricing[lower];
|
|
290
|
+
if (isUsableModelPricing(exact)) return exact;
|
|
291
|
+
const versioned = versionedFallbackModel(lower, Object.keys(pricing), (candidate) => isUsableModelPricing(pricing[candidate]));
|
|
292
|
+
if (versioned) return pricing[versioned];
|
|
293
|
+
const claudeFamily = claudePricingFamilyModel(lower, (candidate) => isUsableModelPricing(pricing[candidate]));
|
|
294
|
+
if (claudeFamily) return pricing[claudeFamily];
|
|
295
|
+
const copilotFamily = copilotPricingFamilyModel(lower, (candidate) => isUsableModelPricing(pricing[candidate]));
|
|
296
|
+
if (copilotFamily) return pricing[copilotFamily];
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// packages/core/src/pricing.ts
|
|
207
301
|
var pricingInfo = {
|
|
208
|
-
sourceName: "Bundled
|
|
302
|
+
sourceName: "Bundled API-equivalent model pricing",
|
|
209
303
|
sourceUrl: "https://developers.openai.com/api/docs/pricing",
|
|
210
304
|
sourceUrls: [
|
|
211
305
|
{ label: "OpenAI pricing reference", url: "https://developers.openai.com/api/docs/pricing" },
|
|
212
|
-
{ label: "Claude pricing reference", url: "https://platform.claude.com/docs/en/about-claude/pricing" }
|
|
306
|
+
{ label: "Claude pricing reference", url: "https://platform.claude.com/docs/en/about-claude/pricing" },
|
|
307
|
+
{ label: "GitHub Copilot model pricing reference", url: "https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing" }
|
|
213
308
|
],
|
|
214
309
|
unit: "USD per 1M tokens",
|
|
215
310
|
updatedAt: "2026-05-18",
|
|
@@ -238,8 +333,19 @@ var defaultPricing = {
|
|
|
238
333
|
"gpt-5-pro": { inputPerMillion: 15, outputPerMillion: 120, reasoningOutputPerMillion: 120 },
|
|
239
334
|
"gpt-5-mini": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, reasoningOutputPerMillion: 2 },
|
|
240
335
|
"gpt-5-nano": { inputPerMillion: 0.05, cachedInputPerMillion: 5e-3, outputPerMillion: 0.4, reasoningOutputPerMillion: 0.4 },
|
|
336
|
+
"gpt-4.1": { inputPerMillion: 2, cachedInputPerMillion: 0.5, outputPerMillion: 8 },
|
|
337
|
+
"gemini-2.5-pro": { inputPerMillion: 1.25, cachedInputPerMillion: 0.125, outputPerMillion: 10, note: "GitHub Copilot supported Google-hosted model; API-equivalent estimate, not a Copilot bill." },
|
|
338
|
+
"gemini-3-flash": { inputPerMillion: 0.5, cachedInputPerMillion: 0.05, outputPerMillion: 3, note: "GitHub Copilot supported Google-hosted model; API-equivalent estimate, not a Copilot bill." },
|
|
339
|
+
"gemini-3.1-pro": { inputPerMillion: 2, cachedInputPerMillion: 0.2, outputPerMillion: 12, note: "GitHub Copilot supported Google-hosted model; API-equivalent estimate, not a Copilot bill." },
|
|
340
|
+
"gemini-3.5-flash": { inputPerMillion: 1.5, cachedInputPerMillion: 0.15, outputPerMillion: 9, note: "GitHub Copilot supported Google-hosted model; API-equivalent estimate, not a Copilot bill." },
|
|
341
|
+
"raptor-mini": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot fine-tuned model using GPT-5 mini pricing." },
|
|
342
|
+
"oswe-vscode-prime": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot Raptor mini internal model id." },
|
|
343
|
+
"lark": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot preview model; estimated with lightweight Copilot pricing." },
|
|
344
|
+
"goldeneye": { inputPerMillion: 1.25, cachedInputPerMillion: 0.125, outputPerMillion: 10, note: "GitHub Copilot fine-tuned model using GPT-5.1-Codex pricing." },
|
|
345
|
+
"claude-opus-4-8": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
241
346
|
"claude-opus-4-7": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
242
347
|
"claude-opus-4-6": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
348
|
+
"claude-opus-4-6-fast-mode": { inputPerMillion: 0, cacheCreationInputPerMillion: 0, cachedInputPerMillion: 0, outputPerMillion: 0, note: "GitHub Copilot supported model, but public per-token pricing is not listed separately. RepoSpend leaves API-equivalent pricing unset." },
|
|
243
349
|
"claude-opus-4-5": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
244
350
|
"claude-opus-4-1": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
|
|
245
351
|
"claude-opus-4": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
|
|
@@ -287,33 +393,6 @@ function calculateCostUsd(usage, pricing) {
|
|
|
287
393
|
const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate + cacheCreationInput / 1e6 * cacheCreationRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
|
|
288
394
|
return Number(cost.toFixed(6));
|
|
289
395
|
}
|
|
290
|
-
function findModelPricing(model, pricing) {
|
|
291
|
-
const lower = model.toLowerCase();
|
|
292
|
-
const exactPricing = pricing[model] ?? pricing[lower];
|
|
293
|
-
if (isUsablePricing(exactPricing)) return exactPricing;
|
|
294
|
-
const familyPricing = claudeFamilyPricing(lower, pricing);
|
|
295
|
-
if (isUsablePricing(familyPricing)) return familyPricing;
|
|
296
|
-
return void 0;
|
|
297
|
-
}
|
|
298
|
-
function claudeFamilyPricing(model, pricing) {
|
|
299
|
-
const families = [
|
|
300
|
-
"claude-opus-4-7",
|
|
301
|
-
"claude-opus-4-6",
|
|
302
|
-
"claude-opus-4-5",
|
|
303
|
-
"claude-opus-4-1",
|
|
304
|
-
"claude-opus-4",
|
|
305
|
-
"claude-sonnet-4-6",
|
|
306
|
-
"claude-sonnet-4-5",
|
|
307
|
-
"claude-sonnet-4",
|
|
308
|
-
"claude-haiku-4-5",
|
|
309
|
-
"claude-3-5-haiku"
|
|
310
|
-
];
|
|
311
|
-
const family = families.find((candidate) => model === candidate || model.startsWith(`${candidate}-`));
|
|
312
|
-
return family ? pricing[family] : void 0;
|
|
313
|
-
}
|
|
314
|
-
function isUsablePricing(pricing) {
|
|
315
|
-
return typeof pricing?.inputPerMillion === "number" && pricing.inputPerMillion > 0 && typeof pricing.outputPerMillion === "number" && pricing.outputPerMillion > 0;
|
|
316
|
-
}
|
|
317
396
|
|
|
318
397
|
// packages/core/src/cache.ts
|
|
319
398
|
var activeCacheVersion = "parse-v1";
|
|
@@ -411,6 +490,169 @@ function aliasForPath(repoRoot, config) {
|
|
|
411
490
|
return match?.name;
|
|
412
491
|
}
|
|
413
492
|
|
|
493
|
+
// packages/core/src/data-confidence.ts
|
|
494
|
+
function buildDataConfidence(input) {
|
|
495
|
+
const sessions = input.sessions;
|
|
496
|
+
const availableSources = input.sources.filter((source) => source.available);
|
|
497
|
+
const importedSessions = input.sourceStats.reduce((sum2, source) => sum2 + (source.sessionsImported ?? 0), 0);
|
|
498
|
+
const tokenDataSessions = sessions.filter((session) => session.totalTokens > 0);
|
|
499
|
+
const missingTokenSessions = sessions.filter((session) => session.totalTokens === 0 || session.warnings.includes("missing_token_breakdown"));
|
|
500
|
+
const pricedTokenSessions = tokenDataSessions.filter((session) => session.estimatedCostUsd !== void 0);
|
|
501
|
+
const unpricedTokenSessions = tokenDataSessions.filter((session) => session.estimatedCostUsd === void 0);
|
|
502
|
+
const highConfidenceTokenSessions = tokenDataSessions.filter((session) => session.tokenConfidence === "high");
|
|
503
|
+
const unknownRepoSessions = sessions.filter((session) => session.warnings.includes("repo_unverified_no_git_root"));
|
|
504
|
+
const verifiedRepoSessions = sessions.filter((session) => !unknownRepoSessions.includes(session));
|
|
505
|
+
const unknownSurfaceSessions = sessions.filter((session) => (session.detectedSurface ?? "unknown") === "unknown");
|
|
506
|
+
const parseIssueCount = Math.max(
|
|
507
|
+
input.scan.parseFailureCount ?? 0,
|
|
508
|
+
input.sourceStats.reduce((sum2, source) => sum2 + (source.parseFailureCount ?? 0), 0)
|
|
509
|
+
);
|
|
510
|
+
const sourceWarningCount = input.sources.reduce((sum2, source) => sum2 + source.warnings.length, 0);
|
|
511
|
+
const tokenDataPct = ratio(tokenDataSessions.length, sessions.length);
|
|
512
|
+
const pricingCoveragePct = tokenDataSessions.length > 0 ? ratio(pricedTokenSessions.length, tokenDataSessions.length) : sessions.length > 0 ? 0 : 1;
|
|
513
|
+
const highConfidenceTokenPct = tokenDataSessions.length > 0 ? ratio(highConfidenceTokenSessions.length, tokenDataSessions.length) : sessions.length > 0 ? 0 : 1;
|
|
514
|
+
const verifiedRepoPct = ratio(verifiedRepoSessions.length, sessions.length);
|
|
515
|
+
const parseHealthPct = parseIssueCount === 0 ? 1 : 1 - ratio(parseIssueCount, Math.max(input.scan.rawSessionCount ?? sessions.length, sessions.length, 1));
|
|
516
|
+
const rawScore = Math.round(100 * (tokenDataPct * 0.3 + pricingCoveragePct * 0.25 + verifiedRepoPct * 0.18 + highConfidenceTokenPct * 0.12 + Math.max(parseHealthPct, 0) * 0.15));
|
|
517
|
+
const score = emptyDataScore(availableSources.length, importedSessions) ?? rawScore;
|
|
518
|
+
const issues = dataConfidenceIssues({
|
|
519
|
+
sessions,
|
|
520
|
+
availableSources,
|
|
521
|
+
importedSessions,
|
|
522
|
+
tokenDataSessions,
|
|
523
|
+
missingTokenSessions,
|
|
524
|
+
unpricedTokenSessions,
|
|
525
|
+
unknownRepoSessions,
|
|
526
|
+
unknownSurfaceSessions,
|
|
527
|
+
parseIssueCount,
|
|
528
|
+
sourceWarningCount
|
|
529
|
+
});
|
|
530
|
+
return {
|
|
531
|
+
score,
|
|
532
|
+
label: score >= 80 ? "High" : score >= 55 ? "Medium" : "Low",
|
|
533
|
+
sessionCount: sessions.length,
|
|
534
|
+
sourceCount: availableSources.length,
|
|
535
|
+
tokenDataSessions: tokenDataSessions.length,
|
|
536
|
+
missingTokenSessions: missingTokenSessions.length,
|
|
537
|
+
pricedTokenSessions: pricedTokenSessions.length,
|
|
538
|
+
unpricedTokenSessions: unpricedTokenSessions.length,
|
|
539
|
+
highConfidenceTokenSessions: highConfidenceTokenSessions.length,
|
|
540
|
+
verifiedRepoSessions: verifiedRepoSessions.length,
|
|
541
|
+
unknownRepoSessions: unknownRepoSessions.length,
|
|
542
|
+
unknownSurfaceSessions: unknownSurfaceSessions.length,
|
|
543
|
+
parseIssueCount,
|
|
544
|
+
sourceWarningCount,
|
|
545
|
+
tokenDataPct,
|
|
546
|
+
pricingCoveragePct,
|
|
547
|
+
highConfidenceTokenPct,
|
|
548
|
+
verifiedRepoPct,
|
|
549
|
+
issues
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function dataConfidenceIssues({
|
|
553
|
+
sessions,
|
|
554
|
+
availableSources,
|
|
555
|
+
importedSessions,
|
|
556
|
+
tokenDataSessions,
|
|
557
|
+
missingTokenSessions,
|
|
558
|
+
unpricedTokenSessions,
|
|
559
|
+
unknownRepoSessions,
|
|
560
|
+
unknownSurfaceSessions,
|
|
561
|
+
parseIssueCount,
|
|
562
|
+
sourceWarningCount
|
|
563
|
+
}) {
|
|
564
|
+
const issues = [];
|
|
565
|
+
if (availableSources.length === 0) {
|
|
566
|
+
issues.push({
|
|
567
|
+
id: "no-local-sources",
|
|
568
|
+
title: "No local source data found",
|
|
569
|
+
detail: "RepoSpend did not find enabled local Codex, Claude Code, GitHub Copilot, or experimental Cursor data paths.",
|
|
570
|
+
tone: "critical",
|
|
571
|
+
affectedSessionIds: []
|
|
572
|
+
});
|
|
573
|
+
} else if (sessions.length === 0 && importedSessions === 0) {
|
|
574
|
+
issues.push({
|
|
575
|
+
id: "no-sessions-imported",
|
|
576
|
+
title: "No sessions imported",
|
|
577
|
+
detail: "Source paths exist, but no local sessions were imported for the current scan window.",
|
|
578
|
+
tone: "attention",
|
|
579
|
+
affectedSessionIds: []
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (tokenDataSessions.length === 0 && sessions.length > 0) {
|
|
583
|
+
issues.push({
|
|
584
|
+
id: "no-token-data",
|
|
585
|
+
title: "No token-bearing sessions",
|
|
586
|
+
detail: "Sessions were imported, but local files did not include token counts that RepoSpend can price or roll up.",
|
|
587
|
+
tone: "critical",
|
|
588
|
+
affectedSessionIds: sessions.map((session) => session.id)
|
|
589
|
+
});
|
|
590
|
+
} else if (missingTokenSessions.length > 0) {
|
|
591
|
+
issues.push({
|
|
592
|
+
id: "missing-token-data",
|
|
593
|
+
title: "Some sessions are missing token data",
|
|
594
|
+
detail: `${missingTokenSessions.length} session${missingTokenSessions.length === 1 ? "" : "s"} have no token total or are missing a full token breakdown.`,
|
|
595
|
+
tone: "attention",
|
|
596
|
+
affectedSessionIds: missingTokenSessions.map((session) => session.id)
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
if (unpricedTokenSessions.length > 0) {
|
|
600
|
+
issues.push({
|
|
601
|
+
id: "unpriced-token-sessions",
|
|
602
|
+
title: "Some token-bearing sessions are unpriced",
|
|
603
|
+
detail: `${unpricedTokenSessions.length} token-bearing session${unpricedTokenSessions.length === 1 ? "" : "s"} cannot show API-equivalent cost because pricing or token split coverage is missing.`,
|
|
604
|
+
tone: "attention",
|
|
605
|
+
affectedSessionIds: unpricedTokenSessions.map((session) => session.id)
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
if (parseIssueCount > 0) {
|
|
609
|
+
issues.push({
|
|
610
|
+
id: "parse-issues",
|
|
611
|
+
title: "Parser issues detected",
|
|
612
|
+
detail: `${parseIssueCount} local file${parseIssueCount === 1 ? "" : "s"} reported parse issues or skipped records.`,
|
|
613
|
+
tone: "attention",
|
|
614
|
+
affectedSessionIds: sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).map((session) => session.id)
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (unknownRepoSessions.length > 0) {
|
|
618
|
+
issues.push({
|
|
619
|
+
id: "unknown-repo-grouping",
|
|
620
|
+
title: "Some repo grouping is unverified",
|
|
621
|
+
detail: `${unknownRepoSessions.length} session${unknownRepoSessions.length === 1 ? "" : "s"} could not be tied to a verified Git root.`,
|
|
622
|
+
tone: "attention",
|
|
623
|
+
affectedSessionIds: unknownRepoSessions.map((session) => session.id)
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (unknownSurfaceSessions.length > 0) {
|
|
627
|
+
issues.push({
|
|
628
|
+
id: "unknown-surfaces",
|
|
629
|
+
title: "Some app surfaces are unknown",
|
|
630
|
+
detail: `${unknownSurfaceSessions.length} session${unknownSurfaceSessions.length === 1 ? "" : "s"} did not include enough metadata to identify the app or surface.`,
|
|
631
|
+
tone: "attention",
|
|
632
|
+
affectedSessionIds: unknownSurfaceSessions.map((session) => session.id)
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (sourceWarningCount > 0) {
|
|
636
|
+
issues.push({
|
|
637
|
+
id: "source-warnings",
|
|
638
|
+
title: "Source warnings were reported",
|
|
639
|
+
detail: `${sourceWarningCount} source warning${sourceWarningCount === 1 ? " was" : "s were"} reported during local discovery.`,
|
|
640
|
+
tone: "attention",
|
|
641
|
+
affectedSessionIds: []
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return issues;
|
|
645
|
+
}
|
|
646
|
+
function ratio(numerator, denominator) {
|
|
647
|
+
if (denominator <= 0) return 1;
|
|
648
|
+
return numerator / denominator;
|
|
649
|
+
}
|
|
650
|
+
function emptyDataScore(availableSourceCount, importedSessions) {
|
|
651
|
+
if (availableSourceCount === 0) return 0;
|
|
652
|
+
if (importedSessions === 0) return 50;
|
|
653
|
+
return void 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
414
656
|
// packages/core/src/filters.ts
|
|
415
657
|
function filterUsage(sessions, filters) {
|
|
416
658
|
return sessions.filter((session) => {
|
|
@@ -491,7 +733,20 @@ function repoWarnings(group, sessions) {
|
|
|
491
733
|
return warnings;
|
|
492
734
|
}
|
|
493
735
|
function compactNumber(value) {
|
|
494
|
-
|
|
736
|
+
if (!Number.isFinite(value)) return "0";
|
|
737
|
+
const sign = value < 0 ? "-" : "";
|
|
738
|
+
const absolute = Math.abs(value);
|
|
739
|
+
const units = [
|
|
740
|
+
{ threshold: 1e12, suffix: "T" },
|
|
741
|
+
{ threshold: 1e9, suffix: "B" },
|
|
742
|
+
{ threshold: 1e6, suffix: "M" },
|
|
743
|
+
{ threshold: 1e3, suffix: "K" }
|
|
744
|
+
];
|
|
745
|
+
const unit = units.find((item) => absolute >= item.threshold);
|
|
746
|
+
if (!unit) return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value);
|
|
747
|
+
const scaled = absolute / unit.threshold;
|
|
748
|
+
const formatted = new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(scaled);
|
|
749
|
+
return `${sign}${formatted}${unit.suffix}`;
|
|
495
750
|
}
|
|
496
751
|
function unique2(values) {
|
|
497
752
|
return [...new Set(values)].sort();
|
|
@@ -655,7 +910,7 @@ var healthSignalDetectors = [
|
|
|
655
910
|
return {
|
|
656
911
|
id: "command-friction",
|
|
657
912
|
tone: "attention",
|
|
658
|
-
title: "
|
|
913
|
+
title: "Possible failed command rate looks high",
|
|
659
914
|
detail: `${percent(commandIssueRate)} of detected shell commands look like blocking, repeated, or token-expensive issues in this filtered view.`,
|
|
660
915
|
action: "Look for repeated setup, dependency, permission, test, or build issues in the Sessions page.",
|
|
661
916
|
actionTarget: "sessions",
|
|
@@ -726,9 +981,9 @@ var healthSignalDetectors = [
|
|
|
726
981
|
tone: "attention",
|
|
727
982
|
title: "Some sessions cannot be priced",
|
|
728
983
|
detail: `${count(unknownCostSessions.length, "session")} are missing a model price or detailed token split.`,
|
|
729
|
-
action: "
|
|
730
|
-
actionTarget: "
|
|
731
|
-
actionLabel: "
|
|
984
|
+
action: "Review the affected sessions first. Add a model rate only when the session has token details but no usable price.",
|
|
985
|
+
actionTarget: "sessions",
|
|
986
|
+
actionLabel: "Review unpriced sessions",
|
|
732
987
|
metric: count(unknownCostSessions.length, "session"),
|
|
733
988
|
critical: true,
|
|
734
989
|
affectedSessionIds: unknownCostSessions.map((session) => session.id)
|
|
@@ -822,7 +1077,7 @@ function buildWasteSignals(sessions) {
|
|
|
822
1077
|
var wasteSignalDetectors = [
|
|
823
1078
|
wasteSignalDetector("high-token-no-edit", "High-token no-edit sessions", "Large sessions where local events did not show file edits.", (session) => (session.fileEditCount ?? 0) === 0 && session.totalTokens >= 1e6),
|
|
824
1079
|
wasteSignalDetector("high-token-low-output", "High-token low-output sessions", "Large sessions where most tokens were input or cached context.", (session) => session.totalTokens >= 1e6 && session.outputTokens / Math.max(session.totalTokens, 1) < 0.03),
|
|
825
|
-
wasteSignalDetector("command-issue-sessions", "
|
|
1080
|
+
wasteSignalDetector("command-issue-sessions", "Possible failed command sessions", "Sessions with blocking, repeated, or token-expensive command failures.", (session) => importantCommandFailures(session) > 0),
|
|
826
1081
|
wasteSignalDetector("unknown-repos", "Unknown repo sessions", "Repo root could not be verified from local Git metadata.", (session) => session.warnings.includes("repo_unverified_no_git_root")),
|
|
827
1082
|
wasteSignalDetector("unknown-surfaces", "Unknown surface sessions", "Local logs did not identify VS Code, terminal, exec, or app surface.", (session) => (session.detectedSurface ?? "unknown") === "unknown"),
|
|
828
1083
|
wasteSignalDetector("repeated-errors", "Repeated error signals", "Repeated-error hints found in session warnings.", (session) => session.warnings.some((warning) => warning.includes("repeated_error")))
|
|
@@ -874,7 +1129,20 @@ function percent(value) {
|
|
|
874
1129
|
return `${new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value * 100)}%`;
|
|
875
1130
|
}
|
|
876
1131
|
function compactNumber2(value) {
|
|
877
|
-
|
|
1132
|
+
if (!Number.isFinite(value)) return "0";
|
|
1133
|
+
const sign = value < 0 ? "-" : "";
|
|
1134
|
+
const absolute = Math.abs(value);
|
|
1135
|
+
const units = [
|
|
1136
|
+
{ threshold: 1e12, suffix: "T" },
|
|
1137
|
+
{ threshold: 1e9, suffix: "B" },
|
|
1138
|
+
{ threshold: 1e6, suffix: "M" },
|
|
1139
|
+
{ threshold: 1e3, suffix: "K" }
|
|
1140
|
+
];
|
|
1141
|
+
const unit = units.find((item) => absolute >= item.threshold);
|
|
1142
|
+
if (!unit) return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value);
|
|
1143
|
+
const scaled = absolute / unit.threshold;
|
|
1144
|
+
const formatted = new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(scaled);
|
|
1145
|
+
return `${sign}${formatted}${unit.suffix}`;
|
|
878
1146
|
}
|
|
879
1147
|
function currency(value, minimumFractionDigits, maximumFractionDigits) {
|
|
880
1148
|
return `$${new Intl.NumberFormat(void 0, { maximumFractionDigits, minimumFractionDigits }).format(value)}`;
|
|
@@ -917,11 +1185,12 @@ function buildDashboardSnapshot(input) {
|
|
|
917
1185
|
hours: groupByHour(sessions),
|
|
918
1186
|
models: groupByModel(sessions),
|
|
919
1187
|
sourceApps,
|
|
920
|
-
health: buildUsageHealth({ sessions, summary, repos, sourceApps, scan })
|
|
1188
|
+
health: buildUsageHealth({ sessions, summary, repos, sourceApps, scan }),
|
|
1189
|
+
confidence: buildDataConfidence({ sources: input.sources, sourceStats: input.sourceStats, sessions, scan })
|
|
921
1190
|
};
|
|
922
1191
|
}
|
|
923
1192
|
function isDashboardVisibleSession(session) {
|
|
924
|
-
return session.totalTokens > 0 || session.sourceClient === "claude" || session.sourceClient === "cursor";
|
|
1193
|
+
return session.totalTokens > 0 || session.sourceClient === "claude" || session.sourceClient === "cursor" || session.sourceClient === "copilot";
|
|
925
1194
|
}
|
|
926
1195
|
|
|
927
1196
|
// packages/core/src/repo.ts
|
|
@@ -1031,8 +1300,9 @@ import Database from "better-sqlite3";
|
|
|
1031
1300
|
function analyzeAgentFriction(records, totalTokens) {
|
|
1032
1301
|
const analysis = emptyAgentFrictionAnalysis();
|
|
1033
1302
|
const messages = [];
|
|
1303
|
+
const toolCallsById = indexToolCallsById(records);
|
|
1034
1304
|
for (const record of records) {
|
|
1035
|
-
applyActionMetadata(analysis, record, messages);
|
|
1305
|
+
applyActionMetadata(analysis, record, messages, toolCallsById);
|
|
1036
1306
|
}
|
|
1037
1307
|
finalizeCommandIssues(analysis, totalTokens);
|
|
1038
1308
|
return analysis;
|
|
@@ -1058,7 +1328,7 @@ function emptyAgentFrictionAnalysis() {
|
|
|
1058
1328
|
assistantMessageCount: 0
|
|
1059
1329
|
};
|
|
1060
1330
|
}
|
|
1061
|
-
function applyActionMetadata(analysis, record, messages) {
|
|
1331
|
+
function applyActionMetadata(analysis, record, messages, toolCallsById) {
|
|
1062
1332
|
if (!record || typeof record !== "object") return;
|
|
1063
1333
|
const object = record;
|
|
1064
1334
|
const payload = firstObject(object.payload) ?? object;
|
|
@@ -1069,12 +1339,73 @@ function applyActionMetadata(analysis, record, messages) {
|
|
|
1069
1339
|
const text = JSON.stringify(record).toLowerCase();
|
|
1070
1340
|
const isTool = type?.includes("tool") || type?.includes("function_call") || text.includes('"tool_call"') || text.includes('"function_call"');
|
|
1071
1341
|
if (isTool) analysis.toolCallCount += 1;
|
|
1072
|
-
const isNonZeroCommand =
|
|
1073
|
-
if (isNonZeroCommand) analysis.commandIssues.push(classifyCommandIssue(record, analysis.commandIssues));
|
|
1342
|
+
const isNonZeroCommand = hasNonZeroCommandExit(record);
|
|
1343
|
+
if (isNonZeroCommand) analysis.commandIssues.push(classifyCommandIssue(record, analysis.commandIssues, commandTextForToolOutput(record, toolCallsById)));
|
|
1074
1344
|
if (/\b(exec_command|shell|bash|zsh|command)\b/.test(text)) analysis.shellCommandCount += 1;
|
|
1075
1345
|
if (/\b(read_file|view_image|cat |sed |rg |grep |open_file)\b/.test(text)) analysis.fileReadCount += 1;
|
|
1076
1346
|
if (/\b(apply_patch|write_file|edit|patch|created|deleted)\b/.test(text)) analysis.fileEditCount += 1;
|
|
1077
1347
|
}
|
|
1348
|
+
function indexToolCallsById(records) {
|
|
1349
|
+
const calls = /* @__PURE__ */ new Map();
|
|
1350
|
+
const sessionCommands = /* @__PURE__ */ new Map();
|
|
1351
|
+
for (const record of records) {
|
|
1352
|
+
if (!record || typeof record !== "object") continue;
|
|
1353
|
+
const object = record;
|
|
1354
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1355
|
+
const type = `${stringValue(object.type) ?? ""} ${stringValue(payload.type) ?? ""}`.toLowerCase();
|
|
1356
|
+
const callId = stringValue(payload.call_id ?? object.call_id ?? payload.callId ?? object.callId);
|
|
1357
|
+
if (type.includes("function_call_output") || type.includes("custom_tool_call_output") || type.includes("tool_result")) {
|
|
1358
|
+
const output = stringValue(payload.output ?? object.output);
|
|
1359
|
+
const sessionId = output?.match(/\bProcess running with session ID\s+(\d+)\b/i)?.[1];
|
|
1360
|
+
const command3 = callId ? calls.get(callId) : void 0;
|
|
1361
|
+
if (sessionId && command3) sessionCommands.set(sessionId, command3);
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
if (!type.includes("function_call") && !type.includes("custom_tool_call") && !type.includes("tool_call")) continue;
|
|
1365
|
+
const command2 = toolCallCommandText(payload, sessionCommands) ?? toolCallCommandText(object, sessionCommands);
|
|
1366
|
+
if (callId && command2) calls.set(callId, command2);
|
|
1367
|
+
}
|
|
1368
|
+
return calls;
|
|
1369
|
+
}
|
|
1370
|
+
function commandTextForToolOutput(record, toolCallsById) {
|
|
1371
|
+
if (!record || typeof record !== "object") return void 0;
|
|
1372
|
+
const object = record;
|
|
1373
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1374
|
+
const callId = stringValue(payload.call_id ?? object.call_id ?? payload.callId ?? object.callId);
|
|
1375
|
+
return callId ? toolCallsById.get(callId) : void 0;
|
|
1376
|
+
}
|
|
1377
|
+
function toolCallCommandText(object, sessionCommands) {
|
|
1378
|
+
const name = `${stringValue(object.name) ?? ""} ${stringValue(object.recipient_name) ?? ""}`.toLowerCase();
|
|
1379
|
+
const args2 = object.arguments ?? object.args ?? object.input;
|
|
1380
|
+
const parsedArgs = typeof args2 === "string" ? parseJsonObject(args2) : firstObject(args2);
|
|
1381
|
+
const sessionId = parsedArgs ? idValue(parsedArgs.session_id) : void 0;
|
|
1382
|
+
if (sessionId && sessionCommands.has(sessionId)) return sessionCommands.get(sessionId);
|
|
1383
|
+
const command2 = parsedArgs ? extractCommandText(parsedArgs) : void 0;
|
|
1384
|
+
if (command2) return command2;
|
|
1385
|
+
if ((name.includes("exec_command") || name.includes("shell")) && typeof args2 === "string" && args2.trim()) return args2;
|
|
1386
|
+
const toolName = stringValue(object.name) ?? stringValue(object.recipient_name);
|
|
1387
|
+
if (!toolName) return void 0;
|
|
1388
|
+
return sessionId ? `${toolName} session ${sessionId}` : toolName;
|
|
1389
|
+
}
|
|
1390
|
+
function hasNonZeroCommandExit(record) {
|
|
1391
|
+
if (!record || typeof record !== "object") return false;
|
|
1392
|
+
const object = record;
|
|
1393
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1394
|
+
const type = `${stringValue(object.type) ?? ""} ${stringValue(payload.type) ?? ""}`.toLowerCase();
|
|
1395
|
+
const isCommandRecord = /(function_call_output|custom_tool_call_output|tool_result|exec_command|shell|command)/.test(type) || Boolean(extractCommandText(payload) ?? extractCommandText(object));
|
|
1396
|
+
const explicitExitCode = exitCodeValue(payload) ?? exitCodeValue(object);
|
|
1397
|
+
if (explicitExitCode !== void 0) return isCommandRecord && explicitExitCode !== 0;
|
|
1398
|
+
const output = stringValue(payload.output) ?? stringValue(object.output);
|
|
1399
|
+
if (!output || !/(function_call_output|custom_tool_call_output|tool_result|exec_command)/.test(type)) return false;
|
|
1400
|
+
const processExitCode = output.match(/\bProcess exited with code\s+(-?\d+)\b/i)?.[1];
|
|
1401
|
+
return processExitCode !== void 0 && Number(processExitCode) !== 0;
|
|
1402
|
+
}
|
|
1403
|
+
function exitCodeValue(object) {
|
|
1404
|
+
const value = object.exit_code ?? object.exitCode;
|
|
1405
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1406
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number(value);
|
|
1407
|
+
return void 0;
|
|
1408
|
+
}
|
|
1078
1409
|
function finalizeCommandIssues(analysis, totalTokens) {
|
|
1079
1410
|
const repeatedCommands = /* @__PURE__ */ new Map();
|
|
1080
1411
|
const repeatableIssues = analysis.commandIssues.filter((issue2) => issue2.classification !== "harmless_nonzero" && issue2.classification !== "exploratory_miss");
|
|
@@ -1119,30 +1450,38 @@ function issuePriority(issue2) {
|
|
|
1119
1450
|
return severityScore * 10 + impactScore;
|
|
1120
1451
|
}
|
|
1121
1452
|
function commandIssueReason(issue2) {
|
|
1122
|
-
if (issue2.severity === "critical") return "Repeated or blocking command
|
|
1123
|
-
if (issue2.severity === "warning") return "Non-zero command event classified as
|
|
1453
|
+
if (issue2.severity === "critical") return "Repeated or blocking command failure that may have stopped progress.";
|
|
1454
|
+
if (issue2.severity === "warning") return "Non-zero command event classified as worth reviewing for this session.";
|
|
1124
1455
|
if (issue2.classification === "exploratory_miss") return "Likely exploratory command that found no result.";
|
|
1125
1456
|
if (issue2.classification === "harmless_nonzero") return "Likely harmless non-zero exit during normal shell exploration.";
|
|
1126
1457
|
return "Non-zero command event with limited local context.";
|
|
1127
1458
|
}
|
|
1128
|
-
function classifyCommandIssue(record, previousIssues) {
|
|
1459
|
+
function classifyCommandIssue(record, previousIssues, commandHint) {
|
|
1129
1460
|
const raw = JSON.stringify(record);
|
|
1130
1461
|
const text = raw.toLowerCase();
|
|
1131
|
-
const command2 = extractCommandText(record) ?? compactCommandText(raw);
|
|
1132
|
-
const
|
|
1462
|
+
const command2 = commandHint ?? extractCommandText(record) ?? compactCommandText(raw);
|
|
1463
|
+
const commandText = command2.toLowerCase();
|
|
1464
|
+
const classificationText = commandHint ? `${commandText} ${commandOutputText(record) ?? ""}` : text;
|
|
1465
|
+
const category = commandCategory(commandHint ? commandText : text);
|
|
1133
1466
|
const commandKey = normalizeCommand(command2);
|
|
1134
1467
|
const repeated = previousIssues.filter((issue2) => issue2.command === commandKey).length + 1 >= 3;
|
|
1135
|
-
if (isHarmlessNonZero(
|
|
1136
|
-
return { command: commandKey, category, classification: isExploratoryMiss(
|
|
1468
|
+
if (isHarmlessNonZero(classificationText)) {
|
|
1469
|
+
return { command: commandKey, category, classification: isExploratoryMiss(classificationText) ? "exploratory_miss" : "harmless_nonzero", severity: "ignored", impact: "low" };
|
|
1137
1470
|
}
|
|
1138
|
-
if (isImportantFailure(
|
|
1139
|
-
return { command: commandKey, category, classification: "blocking_failure", severity: repeated ? "critical" : importantSeverity(
|
|
1471
|
+
if (isImportantFailure(classificationText, category) || repeated) {
|
|
1472
|
+
return { command: commandKey, category, classification: "blocking_failure", severity: repeated ? "critical" : importantSeverity(classificationText, category), impact: repeated ? "high" : importantImpact(category) };
|
|
1140
1473
|
}
|
|
1141
1474
|
if (category === "search" || category === "filesystem") {
|
|
1142
1475
|
return { command: commandKey, category, classification: "exploratory_miss", severity: "ignored", impact: "low" };
|
|
1143
1476
|
}
|
|
1144
1477
|
return { command: commandKey, category, classification: "unknown", severity: "info", impact: "low" };
|
|
1145
1478
|
}
|
|
1479
|
+
function commandOutputText(record) {
|
|
1480
|
+
if (!record || typeof record !== "object") return void 0;
|
|
1481
|
+
const object = record;
|
|
1482
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1483
|
+
return stringValue(payload.output ?? object.output)?.toLowerCase().slice(0, 2e3);
|
|
1484
|
+
}
|
|
1146
1485
|
function isHarmlessNonZero(text) {
|
|
1147
1486
|
return /\b(rg|grep)\b/.test(text) || /git\s+(diff|status)/.test(text) || /\bfind\b/.test(text) || /\btest\s+(-f|-d|-e)\b/.test(text) || /\b(ls|cat)\b/.test(text) && /(optional|exists|existence|not found|no such file)/.test(text);
|
|
1148
1487
|
}
|
|
@@ -1222,9 +1561,21 @@ function compactCommandText(text) {
|
|
|
1222
1561
|
function firstObject(...values) {
|
|
1223
1562
|
return values.find((value) => Boolean(value) && typeof value === "object" && !Array.isArray(value));
|
|
1224
1563
|
}
|
|
1564
|
+
function parseJsonObject(value) {
|
|
1565
|
+
try {
|
|
1566
|
+
return firstObject(JSON.parse(value));
|
|
1567
|
+
} catch {
|
|
1568
|
+
return void 0;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1225
1571
|
function stringValue(value) {
|
|
1226
1572
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1227
1573
|
}
|
|
1574
|
+
function idValue(value) {
|
|
1575
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
1576
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1577
|
+
return void 0;
|
|
1578
|
+
}
|
|
1228
1579
|
function applyMessageCount(analysis, messages, role, payload, object) {
|
|
1229
1580
|
const message = extractMessageText(payload) ?? extractMessageText(object);
|
|
1230
1581
|
if (!message) return;
|
|
@@ -1468,6 +1819,9 @@ function scanCodex(options) {
|
|
|
1468
1819
|
const codexHome = options.codexHome ?? path5.join(os2.homedir(), ".codex");
|
|
1469
1820
|
const statePath = path5.join(codexHome, "state_5.sqlite");
|
|
1470
1821
|
const sessionsPath = path5.join(codexHome, "sessions");
|
|
1822
|
+
const configPath2 = path5.join(codexHome, "config.toml");
|
|
1823
|
+
const configExists = fs5.existsSync(configPath2);
|
|
1824
|
+
const serviceTier = readCodexServiceTier(configPath2);
|
|
1471
1825
|
const warnings = [];
|
|
1472
1826
|
const sessions = [];
|
|
1473
1827
|
const stateExists = fs5.existsSync(statePath);
|
|
@@ -1491,8 +1845,12 @@ function scanCodex(options) {
|
|
|
1491
1845
|
id: "codex",
|
|
1492
1846
|
label: "Codex",
|
|
1493
1847
|
available: stateExists || sessionsExists,
|
|
1494
|
-
paths: [statePath, sessionsPath],
|
|
1495
|
-
warnings
|
|
1848
|
+
paths: configExists ? [statePath, sessionsPath, configPath2] : [statePath, sessionsPath],
|
|
1849
|
+
warnings,
|
|
1850
|
+
serviceTier,
|
|
1851
|
+
serviceTierSource: serviceTier ? "current_config" : void 0,
|
|
1852
|
+
serviceTierConfidence: serviceTier ? "medium" : void 0,
|
|
1853
|
+
serviceTierDetail: serviceTier ? `Current Codex config service_tier: ${serviceTier}` : void 0
|
|
1496
1854
|
},
|
|
1497
1855
|
sessions,
|
|
1498
1856
|
stats: {
|
|
@@ -1508,10 +1866,23 @@ function scanCodex(options) {
|
|
|
1508
1866
|
sessionsImported: sessions.length,
|
|
1509
1867
|
parseFailureCount: sessions.filter((session) => session.parseStatus === "failed").length,
|
|
1510
1868
|
unreadableFileCount: sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length,
|
|
1869
|
+
serviceTier,
|
|
1870
|
+
serviceTierSource: serviceTier ? "current_config" : void 0,
|
|
1871
|
+
serviceTierConfidence: serviceTier ? "medium" : void 0,
|
|
1872
|
+
serviceTierDetail: serviceTier ? `Current Codex config service_tier: ${serviceTier}` : void 0,
|
|
1511
1873
|
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1512
1874
|
}
|
|
1513
1875
|
};
|
|
1514
1876
|
}
|
|
1877
|
+
function readCodexServiceTier(configPath2) {
|
|
1878
|
+
try {
|
|
1879
|
+
const text = fs5.readFileSync(configPath2, "utf8");
|
|
1880
|
+
const match = text.match(/^\s*service_tier\s*=\s*["']?([^"'\s#]+)["']?/m);
|
|
1881
|
+
return match?.[1]?.trim().toLowerCase() || void 0;
|
|
1882
|
+
} catch {
|
|
1883
|
+
return void 0;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1515
1886
|
function readThreads(statePath, sessionFiles, options, warnings) {
|
|
1516
1887
|
let db;
|
|
1517
1888
|
try {
|
|
@@ -2280,6 +2651,7 @@ function scanClaude(options) {
|
|
|
2280
2651
|
const orderedFiles = discovery.files.map((file) => ({ file, mtimeMs: safeMtimeMs(file.filePath) })).filter(({ mtimeMs }) => !options.scanWindow?.fromMs || mtimeMs >= options.scanWindow.fromMs).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2281
2652
|
const usageSelection = selectClaudeUsageOccurrences(orderedFiles.map(({ file }) => file));
|
|
2282
2653
|
const sessions = orderedFiles.flatMap(({ file }, index) => claudeFileToUsages(file, index, options, usageSelection));
|
|
2654
|
+
const serviceTierSummary = summarizeServiceTiers(sessions.map((session) => session.serviceTier), "Claude session usage");
|
|
2283
2655
|
const historyEntryCount = countHistoryEntries(historyPath);
|
|
2284
2656
|
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
2285
2657
|
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
|
|
@@ -2294,7 +2666,8 @@ function scanClaude(options) {
|
|
|
2294
2666
|
label: "Claude Code",
|
|
2295
2667
|
available: discovery.files.length > 0 || historyExists,
|
|
2296
2668
|
paths: unique3([...discovery.projectsPaths, historyPath, ...discovery.desktopSessionRoots]),
|
|
2297
|
-
warnings
|
|
2669
|
+
warnings,
|
|
2670
|
+
...serviceTierSummary
|
|
2298
2671
|
},
|
|
2299
2672
|
sessions,
|
|
2300
2673
|
stats: {
|
|
@@ -2313,6 +2686,7 @@ function scanClaude(options) {
|
|
|
2313
2686
|
unreadableFileCount,
|
|
2314
2687
|
malformedFileCount,
|
|
2315
2688
|
historyEntryCount,
|
|
2689
|
+
...serviceTierSummary,
|
|
2316
2690
|
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2317
2691
|
}
|
|
2318
2692
|
};
|
|
@@ -2398,7 +2772,10 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
|
|
|
2398
2772
|
const warnings = [...repo.warnings, ...breakdown.warnings];
|
|
2399
2773
|
const entrypoint = breakdown.entrypoint ?? inferClaudeEntrypointFromPath(file.filePath);
|
|
2400
2774
|
if (!breakdown.cwd) warnings.push("repo_inferred_from_claude_project_dir");
|
|
2775
|
+
if (!breakdown.model && breakdown.usageCount === 0 && breakdown.syntheticZeroUsageCount > 0) warnings.push("claude_synthetic_zero_usage");
|
|
2401
2776
|
const hasTokenBreakdown = breakdown.totalTokens > 0 && breakdown.usageCount > 0;
|
|
2777
|
+
const serviceTierSummary = summarizeClaudeServiceTier(breakdown.serviceTiers, breakdown.serviceSpeeds);
|
|
2778
|
+
const serviceSpeedSummary = summarizeServiceTiers(breakdown.serviceSpeeds, "Claude usage.speed");
|
|
2402
2779
|
const usage = {
|
|
2403
2780
|
id: `${id}${idSuffix}`,
|
|
2404
2781
|
sourceClient: "claude",
|
|
@@ -2436,6 +2813,18 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
|
|
|
2436
2813
|
detectedSurface: claudeSurface(entrypoint),
|
|
2437
2814
|
surfaceConfidence: "medium",
|
|
2438
2815
|
surfaceReason: entrypoint ? `Claude Code entrypoint: ${entrypoint}` : "Claude Code transcript",
|
|
2816
|
+
serviceTier: serviceTierSummary.serviceTier,
|
|
2817
|
+
serviceTierSource: serviceTierSummary.serviceTier ? "session_usage" : void 0,
|
|
2818
|
+
serviceTierConfidence: serviceTierSummary.serviceTier ? "high" : void 0,
|
|
2819
|
+
serviceTierDetail: serviceTierSummary.serviceTierDetail,
|
|
2820
|
+
sourceMetadata: breakdown.serviceTiers.length || breakdown.serviceSpeeds.length ? {
|
|
2821
|
+
claude: {
|
|
2822
|
+
serviceTiers: breakdown.serviceTiers,
|
|
2823
|
+
speeds: breakdown.serviceSpeeds,
|
|
2824
|
+
serviceTier: serviceTierSummary.serviceTier,
|
|
2825
|
+
speed: serviceSpeedSummary.serviceTier
|
|
2826
|
+
}
|
|
2827
|
+
} : void 0,
|
|
2439
2828
|
userPromptCount: breakdown.userPromptCount,
|
|
2440
2829
|
assistantMessageCount: breakdown.assistantMessageCount,
|
|
2441
2830
|
toolCallCount: breakdown.toolCallCount,
|
|
@@ -2506,6 +2895,8 @@ function applyClaudeRecord(breakdown, record, usageSelection, usageOccurrenceKey
|
|
|
2506
2895
|
applyPromptTimeline2(breakdown, role, message.content, timestamp3);
|
|
2507
2896
|
const usage = firstObject4(message.usage);
|
|
2508
2897
|
const usageTokens = usage ? usageTotal(usage) : 0;
|
|
2898
|
+
const rawModel = stringValue3(message.model);
|
|
2899
|
+
if (rawModel === "<synthetic>" && usage && usageTokens === 0) breakdown.syntheticZeroUsageCount += 1;
|
|
2509
2900
|
const model = normalizeClaudeModel(message.model);
|
|
2510
2901
|
if (model && (usageTokens > 0 || !breakdown.model)) breakdown.model = model;
|
|
2511
2902
|
const selectedUsage = usageTokens > 0 ? usageSelection.usageByOccurrence.get(usageOccurrenceKey) : void 0;
|
|
@@ -2564,7 +2955,9 @@ function cloneUsage(usage) {
|
|
|
2564
2955
|
input_tokens: numberValue(usage.input_tokens),
|
|
2565
2956
|
cache_read_input_tokens: numberValue(usage.cache_read_input_tokens),
|
|
2566
2957
|
cache_creation_input_tokens: numberValue(usage.cache_creation_input_tokens),
|
|
2567
|
-
output_tokens: numberValue(usage.output_tokens)
|
|
2958
|
+
output_tokens: numberValue(usage.output_tokens),
|
|
2959
|
+
service_tier: stringValue3(usage.service_tier),
|
|
2960
|
+
speed: stringValue3(usage.speed)
|
|
2568
2961
|
};
|
|
2569
2962
|
}
|
|
2570
2963
|
function mergeUsageMax(target, usage) {
|
|
@@ -2572,6 +2965,8 @@ function mergeUsageMax(target, usage) {
|
|
|
2572
2965
|
target.cache_read_input_tokens = Math.max(numberValue(target.cache_read_input_tokens), numberValue(usage.cache_read_input_tokens));
|
|
2573
2966
|
target.cache_creation_input_tokens = Math.max(numberValue(target.cache_creation_input_tokens), numberValue(usage.cache_creation_input_tokens));
|
|
2574
2967
|
target.output_tokens = Math.max(numberValue(target.output_tokens), numberValue(usage.output_tokens));
|
|
2968
|
+
target.service_tier ??= stringValue3(usage.service_tier);
|
|
2969
|
+
target.speed ??= stringValue3(usage.speed);
|
|
2575
2970
|
}
|
|
2576
2971
|
function applyUsage(breakdown, usage) {
|
|
2577
2972
|
const input = numberValue(usage.input_tokens);
|
|
@@ -2583,6 +2978,8 @@ function applyUsage(breakdown, usage) {
|
|
|
2583
2978
|
breakdown.cacheCreationInputTokens += cacheCreation;
|
|
2584
2979
|
breakdown.outputTokens += output;
|
|
2585
2980
|
breakdown.usageCount += 1;
|
|
2981
|
+
pushUnique(breakdown.serviceTiers, normalizedServiceTier(usage.service_tier));
|
|
2982
|
+
pushUnique(breakdown.serviceSpeeds, normalizedServiceTier(usage.speed));
|
|
2586
2983
|
}
|
|
2587
2984
|
function segmentClaudeBreakdownByDayAndModel(breakdown) {
|
|
2588
2985
|
const events = breakdown.usageEvents.filter((event) => event.timestamp);
|
|
@@ -2692,9 +3089,46 @@ function emptyBreakdown2() {
|
|
|
2692
3089
|
parseErrors: [],
|
|
2693
3090
|
promptTimeline: [],
|
|
2694
3091
|
warnings: [],
|
|
2695
|
-
usageEvents: []
|
|
3092
|
+
usageEvents: [],
|
|
3093
|
+
serviceTiers: [],
|
|
3094
|
+
serviceSpeeds: [],
|
|
3095
|
+
syntheticZeroUsageCount: 0
|
|
2696
3096
|
};
|
|
2697
3097
|
}
|
|
3098
|
+
function summarizeClaudeServiceTier(serviceTiers, speeds) {
|
|
3099
|
+
const tierSummary = summarizeServiceTiers(serviceTiers, "Claude usage.service_tier");
|
|
3100
|
+
return tierSummary.serviceTier ? tierSummary : summarizeServiceTiers(speeds, "Claude usage.speed");
|
|
3101
|
+
}
|
|
3102
|
+
function summarizeServiceTiers(values, source) {
|
|
3103
|
+
const uniqueValues = unique3(values.filter((value) => Boolean(value)));
|
|
3104
|
+
if (uniqueValues.length === 0) return {};
|
|
3105
|
+
if (uniqueValues.length === 1) {
|
|
3106
|
+
const serviceTier = uniqueValues[0];
|
|
3107
|
+
if (!serviceTier) return {};
|
|
3108
|
+
return {
|
|
3109
|
+
serviceTier,
|
|
3110
|
+
serviceTierSource: "session_usage",
|
|
3111
|
+
serviceTierConfidence: "high",
|
|
3112
|
+
serviceTierDetail: serviceTierDetail(source, uniqueValues)
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
return {
|
|
3116
|
+
serviceTier: "mixed",
|
|
3117
|
+
serviceTierSource: "session_usage",
|
|
3118
|
+
serviceTierConfidence: "high",
|
|
3119
|
+
serviceTierDetail: serviceTierDetail(source, uniqueValues)
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
function serviceTierDetail(source, values) {
|
|
3123
|
+
return `${source}: ${unique3(values).join(", ")}`;
|
|
3124
|
+
}
|
|
3125
|
+
function normalizedServiceTier(value) {
|
|
3126
|
+
const tier = stringValue3(value)?.trim().toLowerCase();
|
|
3127
|
+
return tier || void 0;
|
|
3128
|
+
}
|
|
3129
|
+
function pushUnique(target, value) {
|
|
3130
|
+
if (value && !target.includes(value)) target.push(value);
|
|
3131
|
+
}
|
|
2698
3132
|
function applyPromptTimeline2(breakdown, role, content, timestamp3) {
|
|
2699
3133
|
if (breakdown.promptTimeline.length >= 80 || role !== "user" && role !== "assistant") return;
|
|
2700
3134
|
const text = extractClaudeMessageText(content);
|
|
@@ -2842,58 +3276,59 @@ function safeMtimeMs(filePath) {
|
|
|
2842
3276
|
}
|
|
2843
3277
|
}
|
|
2844
3278
|
|
|
2845
|
-
// packages/core/src/sources/
|
|
3279
|
+
// packages/core/src/sources/copilot.ts
|
|
2846
3280
|
import fs7 from "node:fs";
|
|
2847
3281
|
import os4 from "node:os";
|
|
2848
3282
|
import path7 from "node:path";
|
|
2849
|
-
|
|
2850
|
-
var
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
const
|
|
2857
|
-
const
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
const sessions = candidates.map((candidate, index) => candidateToUsage(candidate, index, options));
|
|
2864
|
-
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
2865
|
-
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_"))).length;
|
|
2866
|
-
const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_") || error.startsWith("sqlite_json_parse:"))).length;
|
|
2867
|
-
if (!fs7.existsSync(cursorHome)) {
|
|
2868
|
-
discovery.warnings.push(`Cursor home directory not found at ${cursorHome}`);
|
|
2869
|
-
}
|
|
2870
|
-
if (!fs7.existsSync(discovery.statePath)) {
|
|
2871
|
-
discovery.warnings.push(`Cursor global state database not found at ${discovery.statePath}`);
|
|
3283
|
+
var maxJsonlBytes = 50 * 1024 * 1024;
|
|
3284
|
+
var copilotOtelEnv = "COPILOT_OTEL_FILE_EXPORTER_PATH";
|
|
3285
|
+
function scanCopilot(options) {
|
|
3286
|
+
const copilotHome = options.copilotHome ?? path7.join(os4.homedir(), ".copilot");
|
|
3287
|
+
const discovery = discoverCopilotFiles(copilotHome, Boolean(options.copilotHome), options.codeUserRoots);
|
|
3288
|
+
const files = discovery.files.filter((file) => fileMayOverlapScanWindow2(file.filePath, options.scanWindow));
|
|
3289
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
3290
|
+
const fileStats = [];
|
|
3291
|
+
for (const [index, file] of files.entries()) {
|
|
3292
|
+
const read = readCopilotFile(file, index);
|
|
3293
|
+
fileStats.push(read.stats);
|
|
3294
|
+
for (const breakdown of read.breakdowns) {
|
|
3295
|
+
mergeBreakdown(sessionMap, breakdown);
|
|
3296
|
+
}
|
|
2872
3297
|
}
|
|
3298
|
+
applyModelHints(sessionMap, discovery.modelHints);
|
|
3299
|
+
const sessions = [...sessionMap.values()].sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? "")).map((breakdown, index) => breakdownToUsage(breakdown, index, options));
|
|
3300
|
+
const parseFailureCount = fileStats.filter((stats) => stats.parseFailed).length;
|
|
3301
|
+
const unreadableFileCount = fileStats.filter((stats) => stats.unreadable).length;
|
|
3302
|
+
const malformedFileCount = fileStats.filter((stats) => stats.malformed).length;
|
|
3303
|
+
if (!fs7.existsSync(copilotHome)) discovery.warnings.push(`GitHub Copilot home directory not found at ${copilotHome}`);
|
|
3304
|
+
if (!fs7.existsSync(discovery.otelPath)) discovery.warnings.push(`GitHub Copilot OpenTelemetry directory not found at ${discovery.otelPath}`);
|
|
3305
|
+
const otelFiles = files.filter((file) => file.kind === "otel").length;
|
|
3306
|
+
const debugLogFiles = files.filter((file) => file.kind === "debug").length;
|
|
3307
|
+
const transcriptFiles = files.filter((file) => file.kind === "transcript").length;
|
|
3308
|
+
const sessionStateFiles = files.filter((file) => file.kind === "session_state").length;
|
|
2873
3309
|
return {
|
|
2874
3310
|
source: {
|
|
2875
|
-
id: "
|
|
2876
|
-
label: "
|
|
2877
|
-
available:
|
|
2878
|
-
paths: unique4([
|
|
3311
|
+
id: "copilot",
|
|
3312
|
+
label: "GitHub Copilot",
|
|
3313
|
+
available: files.length > 0,
|
|
3314
|
+
paths: unique4([copilotHome, discovery.otelPath, discovery.workspaceStoragePath, ...discovery.codeUserRoots, ...files.map((file) => file.filePath), ...[...discovery.modelHints.values()].map((hint) => hint.sourcePath)]),
|
|
2879
3315
|
warnings: discovery.warnings
|
|
2880
3316
|
},
|
|
2881
3317
|
sessions,
|
|
2882
3318
|
stats: {
|
|
2883
|
-
sourceId: "
|
|
2884
|
-
sourceLabel: "
|
|
2885
|
-
homePath:
|
|
2886
|
-
|
|
2887
|
-
|
|
3319
|
+
sourceId: "copilot",
|
|
3320
|
+
sourceLabel: "GitHub Copilot",
|
|
3321
|
+
homePath: copilotHome,
|
|
3322
|
+
copilotHome,
|
|
3323
|
+
otelPath: discovery.otelPath,
|
|
2888
3324
|
workspaceStoragePath: discovery.workspaceStoragePath,
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
skippedFileCount: discovery.skippedFileCount,
|
|
3325
|
+
otelExists: fs7.existsSync(discovery.otelPath),
|
|
3326
|
+
sessionsExists: files.length > 0,
|
|
3327
|
+
sessionFileCount: files.length,
|
|
3328
|
+
otelFileCount: otelFiles,
|
|
3329
|
+
debugLogFileCount: debugLogFiles,
|
|
3330
|
+
transcriptFileCount: transcriptFiles,
|
|
3331
|
+
sessionStateFileCount: sessionStateFiles,
|
|
2897
3332
|
sessionsImported: sessions.length,
|
|
2898
3333
|
parseFailureCount,
|
|
2899
3334
|
unreadableFileCount,
|
|
@@ -2902,154 +3337,962 @@ function scanCursor(options) {
|
|
|
2902
3337
|
}
|
|
2903
3338
|
};
|
|
2904
3339
|
}
|
|
2905
|
-
function
|
|
3340
|
+
function discoverCopilotFiles(copilotHome, explicitHome, explicitCodeUserRoots) {
|
|
2906
3341
|
const warnings = [];
|
|
2907
|
-
const
|
|
2908
|
-
const
|
|
2909
|
-
const
|
|
2910
|
-
const
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
const lower = filePath.toLowerCase();
|
|
2921
|
-
if (lower.endsWith(".jsonl")) {
|
|
2922
|
-
if (isReadableRegularFile(filePath, maxJsonlBytes)) jsonlFiles.push(filePath);
|
|
2923
|
-
else skippedFileCount += 1;
|
|
2924
|
-
return;
|
|
2925
|
-
}
|
|
2926
|
-
if (/\.(sqlite|db|vscdb)$/i.test(filePath)) {
|
|
2927
|
-
if (isReadableRegularFile(filePath, maxDatabaseBytes)) databaseFiles.push(filePath);
|
|
2928
|
-
else skippedFileCount += 1;
|
|
2929
|
-
}
|
|
2930
|
-
});
|
|
2931
|
-
} catch (error) {
|
|
2932
|
-
warnings.push(`Unable to inspect Cursor path ${root}: ${errorMessage3(error)}`);
|
|
2933
|
-
}
|
|
3342
|
+
const otelPath = path7.join(copilotHome, "otel");
|
|
3343
|
+
const codeUserRoots = explicitCodeUserRoots ?? (explicitHome ? [] : defaultCodeUserRoots());
|
|
3344
|
+
const workspaceStoragePath = codeUserRoots[0] ? path7.join(codeUserRoots[0], "workspaceStorage") : "";
|
|
3345
|
+
const files = [];
|
|
3346
|
+
const modelHints = /* @__PURE__ */ new Map();
|
|
3347
|
+
collectJsonlFiles(otelPath, "otel", "cli", void 0, files, warnings);
|
|
3348
|
+
collectCopilotSessionStateFiles(copilotHome, files, warnings);
|
|
3349
|
+
const explicitOtelPath = process.env[copilotOtelEnv]?.trim();
|
|
3350
|
+
if (explicitOtelPath && isReadableRegularFile(explicitOtelPath, maxJsonlBytes)) {
|
|
3351
|
+
files.push({ filePath: path7.resolve(explicitOtelPath), kind: "otel", surface: "cli", fallbackCwd: void 0 });
|
|
3352
|
+
}
|
|
3353
|
+
for (const userRoot of codeUserRoots) {
|
|
3354
|
+
collectCodeCopilotFiles(userRoot, files, modelHints, warnings);
|
|
2934
3355
|
}
|
|
2935
3356
|
return {
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
jsonlFiles: unique4(jsonlFiles).sort(cursorJsonlSort),
|
|
2942
|
-
databaseFiles: unique4(databaseFiles).sort(),
|
|
2943
|
-
skippedFileCount,
|
|
3357
|
+
otelPath,
|
|
3358
|
+
workspaceStoragePath,
|
|
3359
|
+
files: uniqueFiles(files),
|
|
3360
|
+
codeUserRoots,
|
|
3361
|
+
modelHints,
|
|
2944
3362
|
warnings
|
|
2945
3363
|
};
|
|
2946
3364
|
}
|
|
2947
|
-
function
|
|
2948
|
-
|
|
3365
|
+
function collectCopilotSessionStateFiles(copilotHome, files, warnings) {
|
|
3366
|
+
const sessionStateRoot = path7.join(copilotHome, "session-state");
|
|
3367
|
+
if (!fs7.existsSync(sessionStateRoot)) return;
|
|
3368
|
+
try {
|
|
3369
|
+
for (const entry of fs7.readdirSync(sessionStateRoot, { withFileTypes: true })) {
|
|
3370
|
+
if (!entry.isDirectory()) continue;
|
|
3371
|
+
const sessionDir = path7.join(sessionStateRoot, entry.name);
|
|
3372
|
+
const eventsPath = path7.join(sessionDir, "events.jsonl");
|
|
3373
|
+
if (isReadableRegularFile(eventsPath, maxJsonlBytes)) {
|
|
3374
|
+
files.push({ filePath: eventsPath, kind: "session_state", surface: "cli", fallbackCwd: cwdFromWorkspaceYaml(path7.join(sessionDir, "workspace.yaml")) });
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
} catch (error) {
|
|
3378
|
+
warnings.push(`Unable to read GitHub Copilot session state at ${sessionStateRoot}: ${errorMessage3(error)}`);
|
|
3379
|
+
}
|
|
2949
3380
|
}
|
|
2950
|
-
function
|
|
2951
|
-
|
|
2952
|
-
|
|
3381
|
+
function collectCodeCopilotFiles(userRoot, files, modelHints, warnings) {
|
|
3382
|
+
const workspaceStorage = path7.join(userRoot, "workspaceStorage");
|
|
3383
|
+
if (!fs7.existsSync(workspaceStorage)) return;
|
|
2953
3384
|
try {
|
|
2954
|
-
for (const workspace of fs7.readdirSync(
|
|
3385
|
+
for (const workspace of fs7.readdirSync(workspaceStorage, { withFileTypes: true })) {
|
|
2955
3386
|
if (!workspace.isDirectory()) continue;
|
|
2956
|
-
|
|
3387
|
+
const workspaceRoot = path7.join(workspaceStorage, workspace.name);
|
|
3388
|
+
const copilotRoot = path7.join(workspaceRoot, "GitHub.copilot-chat");
|
|
3389
|
+
if (!fs7.existsSync(copilotRoot)) continue;
|
|
3390
|
+
const fallbackCwd = workspaceFolderFromStorageJson(path7.join(workspaceRoot, "workspace.json"));
|
|
3391
|
+
collectJsonlFiles(path7.join(copilotRoot, "debug-logs"), "debug", "vscode", fallbackCwd, files, warnings);
|
|
3392
|
+
collectJsonlFiles(path7.join(copilotRoot, "transcripts"), "transcript", "vscode", fallbackCwd, files, warnings);
|
|
3393
|
+
collectCodeChatSessionModelHints(path7.join(workspaceRoot, "chatSessions"), modelHints, warnings);
|
|
2957
3394
|
}
|
|
2958
|
-
} catch {
|
|
2959
|
-
|
|
3395
|
+
} catch (error) {
|
|
3396
|
+
warnings.push(`Unable to read GitHub Copilot VS Code storage at ${workspaceStorage}: ${errorMessage3(error)}`);
|
|
2960
3397
|
}
|
|
2961
|
-
return roots;
|
|
2962
3398
|
}
|
|
2963
|
-
function
|
|
2964
|
-
if (!fs7.existsSync(root)) return
|
|
3399
|
+
function collectCodeChatSessionModelHints(root, modelHints, warnings) {
|
|
3400
|
+
if (!fs7.existsSync(root)) return;
|
|
2965
3401
|
try {
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3402
|
+
walk3(root, 2, (filePath) => {
|
|
3403
|
+
if (!filePath.endsWith(".jsonl") || !isReadableRegularFile(filePath, maxJsonlBytes)) return;
|
|
3404
|
+
const hint = readCodeChatSessionModelHint(filePath);
|
|
3405
|
+
if (hint) modelHints.set(hint.sessionId, { model: hint.model, sourcePath: filePath });
|
|
3406
|
+
});
|
|
3407
|
+
} catch (error) {
|
|
3408
|
+
warnings.push(`Unable to read VS Code chat session metadata at ${root}: ${errorMessage3(error)}`);
|
|
2969
3409
|
}
|
|
2970
3410
|
}
|
|
2971
|
-
function
|
|
2972
|
-
const
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3411
|
+
function readCodeChatSessionModelHint(filePath) {
|
|
3412
|
+
const fallbackSessionId = path7.basename(filePath).replace(/\.jsonl$/i, "");
|
|
3413
|
+
let sessionId = fallbackSessionId;
|
|
3414
|
+
let model;
|
|
3415
|
+
try {
|
|
3416
|
+
for (const record of parseJsonLines3(fs7.readFileSync(filePath, "utf8")).records) {
|
|
3417
|
+
const body = firstObject5(record.v) ?? firstObject5(record.data) ?? record;
|
|
3418
|
+
const metadata = firstObject5(body.metadata);
|
|
3419
|
+
sessionId = stringValue4(body.sessionId) ?? stringValue4(metadata?.sessionId) ?? sessionId;
|
|
3420
|
+
model = chatSessionSelectedModel(body) ?? chatSessionResolvedModel(body) ?? model;
|
|
3421
|
+
}
|
|
3422
|
+
} catch {
|
|
3423
|
+
return void 0;
|
|
2981
3424
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3425
|
+
return sessionId && model ? { sessionId, model } : void 0;
|
|
3426
|
+
}
|
|
3427
|
+
function chatSessionSelectedModel(body) {
|
|
3428
|
+
const inputState = firstObject5(body.inputState);
|
|
3429
|
+
const selectedModel = firstObject5(inputState?.selectedModel) ?? firstObject5(body.selectedModel);
|
|
3430
|
+
const metadata = firstObject5(selectedModel?.metadata);
|
|
3431
|
+
return firstModelCandidate([
|
|
3432
|
+
stringValue4(metadata?.version),
|
|
3433
|
+
stringValue4(metadata?.family),
|
|
3434
|
+
stringValue4(selectedModel?.id),
|
|
3435
|
+
stringValue4(selectedModel?.identifier)
|
|
3436
|
+
]);
|
|
3437
|
+
}
|
|
3438
|
+
function chatSessionResolvedModel(body) {
|
|
3439
|
+
const metadata = firstObject5(body.metadata);
|
|
3440
|
+
const response = firstObject5(body.response);
|
|
3441
|
+
const responseMetadata = firstObject5(response?.metadata);
|
|
3442
|
+
return firstModelCandidate([
|
|
3443
|
+
stringValue4(metadata?.resolvedModel),
|
|
3444
|
+
stringValue4(responseMetadata?.resolvedModel)
|
|
3445
|
+
]);
|
|
3446
|
+
}
|
|
3447
|
+
function firstModelCandidate(values) {
|
|
3448
|
+
for (const value of values) {
|
|
3449
|
+
const normalized = value?.replace(/^github[-_]?copilot\//i, "").replace(/^copilot\//i, "").trim();
|
|
3450
|
+
if (normalized && normalized.toLowerCase() !== "auto") return normalized;
|
|
2989
3451
|
}
|
|
2990
|
-
|
|
2991
|
-
const globalStoragePath = path7.join(xdg, "Cursor", "User", "globalStorage");
|
|
2992
|
-
return {
|
|
2993
|
-
globalStoragePath,
|
|
2994
|
-
workspaceStoragePath: path7.join(xdg, "Cursor", "User", "workspaceStorage"),
|
|
2995
|
-
statePath: path7.join(globalStoragePath, "state.vscdb")
|
|
2996
|
-
};
|
|
3452
|
+
return void 0;
|
|
2997
3453
|
}
|
|
2998
|
-
function
|
|
2999
|
-
if (
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
}
|
|
3454
|
+
function collectJsonlFiles(root, kind, surface, fallbackCwd, files, warnings) {
|
|
3455
|
+
if (!fs7.existsSync(root)) return;
|
|
3456
|
+
try {
|
|
3457
|
+
walk3(root, 6, (filePath) => {
|
|
3458
|
+
if (filePath.endsWith(".jsonl") && isReadableRegularFile(filePath, maxJsonlBytes)) {
|
|
3459
|
+
files.push({ filePath, kind, surface, fallbackCwd });
|
|
3460
|
+
}
|
|
3461
|
+
});
|
|
3462
|
+
} catch (error) {
|
|
3463
|
+
warnings.push(`Unable to read GitHub Copilot path ${root}: ${errorMessage3(error)}`);
|
|
3008
3464
|
}
|
|
3009
3465
|
}
|
|
3010
|
-
function
|
|
3466
|
+
function readCopilotFile(file, index) {
|
|
3467
|
+
const fallbackId = path7.basename(file.filePath).replace(/\.jsonl$/i, "") || `copilot-session-${index}`;
|
|
3468
|
+
const fallbackTimestamp = fileModifiedDate(file.filePath);
|
|
3469
|
+
const fallback = emptyBreakdown3(file, fallbackId);
|
|
3011
3470
|
try {
|
|
3012
|
-
fs7.
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3471
|
+
const parse = parseJsonLines3(fs7.readFileSync(file.filePath, "utf8"));
|
|
3472
|
+
const traceContexts = collectTraceContexts(parse.records);
|
|
3473
|
+
const candidates = parse.records.map((record, recordIndex) => usageCandidateFromRecord(record, recordIndex, fallbackTimestamp, traceContexts)).filter((candidate) => Boolean(candidate));
|
|
3474
|
+
const selectedCandidates = uniqueCandidates(candidates.filter((candidate) => shouldEmitCandidate(candidate, candidates)));
|
|
3475
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3476
|
+
fallback.rawEventCount = parse.records.length;
|
|
3477
|
+
fallback.parseErrors = parse.errors;
|
|
3478
|
+
fallback.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
3479
|
+
if (file.kind === "transcript" || file.kind === "session_state") {
|
|
3480
|
+
for (const record of parse.records) applyTranscriptRecord(fallback, record, file, file.kind === "transcript" ? fallbackTimestamp : void 0);
|
|
3481
|
+
}
|
|
3482
|
+
for (const candidate of selectedCandidates) {
|
|
3483
|
+
const group = groups.get(candidate.sessionId) ?? emptyBreakdown3(file, candidate.sessionId);
|
|
3484
|
+
group.rawEventCount = parse.records.length;
|
|
3485
|
+
group.parseErrors = parse.errors;
|
|
3486
|
+
group.parseStatus = parse.errors.length ? "partial" : "ok";
|
|
3487
|
+
applyCandidate(group, candidate);
|
|
3488
|
+
groups.set(candidate.sessionId, group);
|
|
3489
|
+
}
|
|
3490
|
+
const stats = statsFromParseErrors(parse.errors);
|
|
3491
|
+
if (groups.size > 0) {
|
|
3492
|
+
return { breakdowns: [...groups.values()].map((group) => mergeActivity(group, fallback)), stats };
|
|
3493
|
+
}
|
|
3494
|
+
if (fallback.messageCount > 0 || fallback.rawEventCount > 0 || fallback.parseErrors.length > 0) return { breakdowns: [fallbackWithMissingTokenWarning(fallback)], stats };
|
|
3495
|
+
return { breakdowns: [], stats };
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
const breakdown = emptyBreakdown3(file, fallbackId);
|
|
3498
|
+
const message = `unable_to_read_copilot_file:${errorMessage3(error)}`;
|
|
3499
|
+
breakdown.parseStatus = "failed";
|
|
3500
|
+
breakdown.parseErrors.push(message);
|
|
3501
|
+
breakdown.warnings.push(message);
|
|
3502
|
+
return { breakdowns: [breakdown], stats: { parseFailed: true, unreadable: true, malformed: false } };
|
|
3017
3503
|
}
|
|
3018
3504
|
}
|
|
3019
|
-
function
|
|
3020
|
-
|
|
3021
|
-
const bTranscript = b.includes(`${path7.sep}agent-transcripts${path7.sep}`) ? 0 : 1;
|
|
3022
|
-
return aTranscript - bTranscript || a.localeCompare(b);
|
|
3505
|
+
function statsFromParseErrors(errors) {
|
|
3506
|
+
return { parseFailed: errors.length > 0, unreadable: false, malformed: errors.some((error) => error.startsWith("jsonl_line_")) };
|
|
3023
3507
|
}
|
|
3024
|
-
function
|
|
3025
|
-
|
|
3026
|
-
const
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3508
|
+
function collectTraceContexts(records) {
|
|
3509
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
3510
|
+
for (const record of records) {
|
|
3511
|
+
const traceId = traceIdFromRecord(record);
|
|
3512
|
+
const attributes = firstObject5(record.attributes);
|
|
3513
|
+
if (!traceId || !attributes) continue;
|
|
3514
|
+
const context = contexts.get(traceId) ?? { model: void 0, sessionId: void 0, sessionPriority: 0 };
|
|
3515
|
+
context.model ??= firstNonEmptyAttr(attributes, modelAttrs);
|
|
3516
|
+
const session = bestSessionAttr(attributes);
|
|
3517
|
+
if (session && session.priority > context.sessionPriority) {
|
|
3518
|
+
context.sessionId = session.value;
|
|
3519
|
+
context.sessionPriority = session.priority;
|
|
3520
|
+
}
|
|
3521
|
+
contexts.set(traceId, context);
|
|
3522
|
+
}
|
|
3523
|
+
return contexts;
|
|
3524
|
+
}
|
|
3525
|
+
function usageCandidateFromRecord(record, index, fallbackTimestamp, traceContexts) {
|
|
3526
|
+
const attributes = firstObject5(record.attributes);
|
|
3527
|
+
if (!attributes) return void 0;
|
|
3528
|
+
const source = usageSource(record, attributes);
|
|
3529
|
+
if (!source) return void 0;
|
|
3530
|
+
const input = numberValue2(attributes["gen_ai.usage.input_tokens"]);
|
|
3531
|
+
const output = numberValue2(attributes["gen_ai.usage.output_tokens"]);
|
|
3532
|
+
const cached = numberValue2(attributes["gen_ai.usage.cache_read.input_tokens"]);
|
|
3533
|
+
const cacheCreation = firstPositiveNumber(attributes, ["gen_ai.usage.cache_write.input_tokens", "gen_ai.usage.cache_creation.input_tokens"]);
|
|
3534
|
+
const reasoning = firstPositiveNumber(attributes, ["gen_ai.usage.reasoning.output_tokens", "gen_ai.usage.reasoning_tokens"]);
|
|
3535
|
+
const total = firstPositiveNumber(attributes, ["gen_ai.usage.total_tokens", "gen_ai.usage.total.token_count"]);
|
|
3536
|
+
const outputWithFallback = output || outputTokensFromTotal(total, input, reasoning);
|
|
3537
|
+
if (input + outputWithFallback + cached + cacheCreation + reasoning <= 0) return void 0;
|
|
3538
|
+
const traceId = traceIdFromRecord(record);
|
|
3539
|
+
const traceContext = traceId ? traceContexts.get(traceId) : void 0;
|
|
3540
|
+
const session = bestSessionAttr(attributes)?.value ?? traceContext?.sessionId ?? traceId ?? "unknown-session";
|
|
3541
|
+
const timestamp3 = timestampFromRecord(record) ?? fallbackTimestamp;
|
|
3542
|
+
return {
|
|
3543
|
+
source,
|
|
3544
|
+
traceId,
|
|
3545
|
+
responseId: stringValue4(attributes["gen_ai.response.id"]),
|
|
3546
|
+
sessionId: session,
|
|
3547
|
+
model: firstNonEmptyAttr(attributes, modelAttrs) ?? traceContext?.model,
|
|
3548
|
+
timestamp: timestamp3,
|
|
3549
|
+
durationMs: durationMsFromRecord(record),
|
|
3550
|
+
inputTokens: input,
|
|
3551
|
+
cachedInputTokens: cached,
|
|
3552
|
+
cacheCreationInputTokens: cacheCreation,
|
|
3553
|
+
outputTokens: outputWithFallback,
|
|
3554
|
+
reasoningTokens: reasoning,
|
|
3555
|
+
dedupKey: dedupKeyForRecord(source, record, attributes, traceId, session, timestamp3, index)
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
function outputTokensFromTotal(total, input, reasoning) {
|
|
3559
|
+
return total > 0 && total >= input + reasoning ? total - input - reasoning : 0;
|
|
3560
|
+
}
|
|
3561
|
+
function usageSource(record, attributes) {
|
|
3562
|
+
const operation = stringValue4(attributes["gen_ai.operation.name"]);
|
|
3563
|
+
const eventName = stringValue4(attributes["event.name"]);
|
|
3564
|
+
const name = stringValue4(record.name);
|
|
3565
|
+
const body = stringValue4(record.body) ?? stringValue4(record._body);
|
|
3566
|
+
const span = isSpanRecord(record);
|
|
3567
|
+
if (span && (operation === "chat" || name?.startsWith("chat "))) return "chat_span";
|
|
3568
|
+
if (!span && (eventName === "gen_ai.client.inference.operation.details" || body?.startsWith("GenAI inference:"))) return "inference_log";
|
|
3569
|
+
if (!span && (eventName === "copilot_chat.agent.turn" || body?.startsWith("copilot_chat.agent.turn"))) return "agent_turn_log";
|
|
3570
|
+
if (span && (operation === "invoke_agent" || name?.startsWith("invoke_agent "))) return "agent_summary_span";
|
|
3571
|
+
return void 0;
|
|
3572
|
+
}
|
|
3573
|
+
function isSpanRecord(record) {
|
|
3574
|
+
const type = stringValue4(record.type);
|
|
3575
|
+
if (type) return type === "span";
|
|
3576
|
+
return Boolean(stringValue4(record.name) && (stringValue4(record.spanId) || stringValue4(record.traceId) || record.startTime || record.endTime || record.duration || record.kind));
|
|
3577
|
+
}
|
|
3578
|
+
function shouldEmitCandidate(candidate, candidates) {
|
|
3579
|
+
const has = (source, field, value) => Boolean(value && candidates.some((item) => item.source === source && item[field] === value));
|
|
3580
|
+
if (candidate.source === "chat_span") return true;
|
|
3581
|
+
if (candidate.source === "inference_log") return !has("chat_span", "traceId", candidate.traceId) && !has("chat_span", "responseId", candidate.responseId);
|
|
3582
|
+
if (candidate.source === "agent_turn_log") {
|
|
3583
|
+
return !has("chat_span", "traceId", candidate.traceId) && !has("inference_log", "traceId", candidate.traceId) && !has("chat_span", "responseId", candidate.responseId) && !has("inference_log", "responseId", candidate.responseId);
|
|
3584
|
+
}
|
|
3585
|
+
return !has("chat_span", "traceId", candidate.traceId) && !has("inference_log", "traceId", candidate.traceId) && !has("agent_turn_log", "traceId", candidate.traceId) && !has("chat_span", "responseId", candidate.responseId) && !has("inference_log", "responseId", candidate.responseId) && !has("agent_turn_log", "responseId", candidate.responseId);
|
|
3586
|
+
}
|
|
3587
|
+
function uniqueCandidates(candidates) {
|
|
3588
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3589
|
+
return candidates.filter((candidate) => {
|
|
3590
|
+
if (seen.has(candidate.dedupKey)) return false;
|
|
3591
|
+
seen.add(candidate.dedupKey);
|
|
3592
|
+
return true;
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
var modelAttrs = ["gen_ai.response.model", "gen_ai.request.model"];
|
|
3596
|
+
var sessionAttrs = [
|
|
3597
|
+
{ key: "gen_ai.conversation.id", priority: 3 },
|
|
3598
|
+
{ key: "copilot_chat.session_id", priority: 3 },
|
|
3599
|
+
{ key: "copilot_chat.chat_session_id", priority: 3 },
|
|
3600
|
+
{ key: "session.id", priority: 3 },
|
|
3601
|
+
{ key: "github.copilot.interaction_id", priority: 2 },
|
|
3602
|
+
{ key: "gen_ai.response.id", priority: 1 }
|
|
3603
|
+
];
|
|
3604
|
+
function applyCandidate(breakdown, candidate) {
|
|
3605
|
+
breakdown.id = candidate.sessionId;
|
|
3606
|
+
breakdown.model = candidate.model ?? breakdown.model;
|
|
3607
|
+
const candidateStartedAt = candidate.timestamp && candidate.durationMs ? new Date(Date.parse(candidate.timestamp) - candidate.durationMs).toISOString() : candidate.timestamp;
|
|
3608
|
+
breakdown.startedAt = minDate2(breakdown.startedAt, candidateStartedAt);
|
|
3609
|
+
breakdown.endedAt = maxDate2(breakdown.endedAt, candidate.timestamp);
|
|
3610
|
+
applyTokenEvent(breakdown, {
|
|
3611
|
+
dedupKey: candidate.dedupKey,
|
|
3612
|
+
detailed: candidate.inputTokens > 0 || candidate.cachedInputTokens > 0 || candidate.cacheCreationInputTokens > 0 || candidate.reasoningTokens > 0,
|
|
3613
|
+
inputTokens: candidate.inputTokens,
|
|
3614
|
+
cachedInputTokens: candidate.cachedInputTokens,
|
|
3615
|
+
cacheCreationInputTokens: candidate.cacheCreationInputTokens,
|
|
3616
|
+
outputTokens: candidate.outputTokens,
|
|
3617
|
+
reasoningTokens: candidate.reasoningTokens
|
|
3618
|
+
});
|
|
3619
|
+
breakdown.messageCount += 1;
|
|
3620
|
+
breakdown.assistantMessageCount += 1;
|
|
3621
|
+
}
|
|
3622
|
+
function applyTranscriptRecord(breakdown, record, file, fallbackTimestamp) {
|
|
3623
|
+
const data = firstObject5(record.data);
|
|
3624
|
+
const timestamp3 = dateValue4(record.timestamp) ?? dateValue4(record.ts) ?? dateValue4(data?.startTime) ?? fallbackTimestamp;
|
|
3625
|
+
const type = stringValue4(record.type);
|
|
3626
|
+
const role = type?.startsWith("user.") ? "user" : type?.startsWith("assistant.") ? "assistant" : stringValue4(data?.role);
|
|
3627
|
+
const sessionId = stringValue4(data?.sessionId) ?? stringValue4(record.sessionId) ?? stringValue4(record.sid);
|
|
3628
|
+
if (sessionId) breakdown.id = sessionId;
|
|
3629
|
+
if (timestamp3) {
|
|
3630
|
+
breakdown.startedAt = minDate2(breakdown.startedAt, timestamp3);
|
|
3631
|
+
breakdown.endedAt = maxDate2(breakdown.endedAt, timestamp3);
|
|
3632
|
+
}
|
|
3633
|
+
breakdown.cwd ??= stringValue4(data?.cwd) ?? stringValue4(record.cwd) ?? file.fallbackCwd;
|
|
3634
|
+
const context = firstObject5(data?.context);
|
|
3635
|
+
breakdown.cwd ??= stringValue4(context?.cwd) ?? stringValue4(context?.gitRoot);
|
|
3636
|
+
const modelCandidate = stringValue4(data?.newModel) ?? stringValue4(data?.currentModel) ?? stringValue4(data?.model) ?? stringValue4(record.model);
|
|
3637
|
+
if (modelCandidate && (modelCandidate !== "auto" || !breakdown.model)) breakdown.model = modelCandidate;
|
|
3638
|
+
if (type === "session.start") breakdown.title ??= stringValue4(data?.title);
|
|
3639
|
+
const outputTokens = numberValue2(data?.outputTokens);
|
|
3640
|
+
if (outputTokens > 0) {
|
|
3641
|
+
const partialSessionId = sessionId ?? breakdown.id ?? path7.basename(file.filePath);
|
|
3642
|
+
applyTokenEvent(breakdown, {
|
|
3643
|
+
dedupKey: `transcript-output:${file.filePath}:${partialSessionId}:${timestamp3 ?? "unknown"}:${breakdown.usageCount}`,
|
|
3644
|
+
detailed: false,
|
|
3645
|
+
inputTokens: 0,
|
|
3646
|
+
cachedInputTokens: 0,
|
|
3647
|
+
cacheCreationInputTokens: 0,
|
|
3648
|
+
outputTokens,
|
|
3649
|
+
reasoningTokens: 0
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
if (role === "user" || role === "assistant") {
|
|
3653
|
+
breakdown.messageCount += 1;
|
|
3654
|
+
if (role === "user") breakdown.userPromptCount += 1;
|
|
3655
|
+
if (role === "assistant") breakdown.assistantMessageCount += 1;
|
|
3656
|
+
applyTimeline(breakdown, role, data?.content ?? record.content, timestamp3);
|
|
3657
|
+
}
|
|
3658
|
+
const toolRequests = Array.isArray(data?.toolRequests) ? data.toolRequests : [];
|
|
3659
|
+
breakdown.toolCallCount += toolRequests.length;
|
|
3660
|
+
for (const request of toolRequests) {
|
|
3661
|
+
const object = firstObject5(request);
|
|
3662
|
+
const name = stringValue4(object?.name)?.toLowerCase() ?? "";
|
|
3663
|
+
const args2 = stringValue4(object?.arguments)?.toLowerCase() ?? "";
|
|
3664
|
+
if (name.includes("terminal") || args2.includes('"command"')) breakdown.shellCommandCount += 1;
|
|
3665
|
+
if (name.includes("edit") || args2.includes("edit")) breakdown.fileEditCount += 1;
|
|
3666
|
+
if (name.includes("read") || args2.includes("read_file")) breakdown.fileReadCount += 1;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
function breakdownToUsage(breakdown, index, options) {
|
|
3670
|
+
const id = breakdown.id ?? `copilot-session-${index}`;
|
|
3671
|
+
const cwd = breakdown.cwd ?? "";
|
|
3672
|
+
const repo = breakdown.cwd ? resolveRepoInfo(breakdown.cwd, options.config) : unknownCopilotRepoInfo();
|
|
3673
|
+
const hasTokenBreakdown = breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0 && breakdown.usageCount > 0;
|
|
3674
|
+
const hasPricedBreakdown = hasTokenBreakdown && breakdown.inputTokens > 0;
|
|
3675
|
+
const warnings = unique4([...repo.warnings, ...breakdown.warnings]);
|
|
3676
|
+
if (hasTokenBreakdown && !hasPricedBreakdown) warnings.push("copilot_partial_token_breakdown");
|
|
3677
|
+
const usage = {
|
|
3678
|
+
id,
|
|
3679
|
+
sourceClient: "copilot",
|
|
3680
|
+
sourceApp: breakdown.surface === "cli" ? "Copilot CLI" : breakdown.surface === "vscode" ? "VS Code" : "GitHub Copilot",
|
|
3681
|
+
sourceAppRaw: breakdown.surface,
|
|
3682
|
+
sourcePath: breakdown.sourcePaths[0] ?? "",
|
|
3683
|
+
repoRoot: repo.repoRoot,
|
|
3684
|
+
repoName: repo.repoName,
|
|
3685
|
+
cwd,
|
|
3686
|
+
gitRemoteUrl: repo.gitRemoteUrl,
|
|
3687
|
+
gitBranch: repo.gitBranch,
|
|
3688
|
+
title: breakdown.title,
|
|
3689
|
+
startedAt: breakdown.startedAt,
|
|
3690
|
+
endedAt: breakdown.endedAt,
|
|
3691
|
+
model: normalizeCopilotModel(breakdown.model),
|
|
3692
|
+
provider: providerFromModel(breakdown.model),
|
|
3693
|
+
inputTokens: breakdown.inputTokens,
|
|
3694
|
+
cachedInputTokens: breakdown.cachedInputTokens,
|
|
3695
|
+
cacheCreationInputTokens: breakdown.cacheCreationInputTokens,
|
|
3696
|
+
outputTokens: breakdown.outputTokens,
|
|
3697
|
+
reasoningTokens: breakdown.reasoningTokens,
|
|
3698
|
+
reasoningOutputTokens: breakdown.reasoningTokens,
|
|
3699
|
+
totalTokens: breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens,
|
|
3700
|
+
tokenAggregationMethod: hasTokenBreakdown ? "direct_usage" : "unknown",
|
|
3701
|
+
tokenConfidence: hasTokenBreakdown ? "high" : "low",
|
|
3702
|
+
tokenSnapshotCount: breakdown.usageCount,
|
|
3703
|
+
estimatedCostUsd: void 0,
|
|
3704
|
+
messageCount: breakdown.messageCount,
|
|
3705
|
+
rawTokenTotal: hasTokenBreakdown ? breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens : void 0,
|
|
3706
|
+
warnings: hasTokenBreakdown ? warnings : unique4([...warnings, "missing_token_breakdown"]),
|
|
3707
|
+
durationMs: durationMs3(breakdown.startedAt, breakdown.endedAt),
|
|
3708
|
+
rawEventCount: breakdown.rawEventCount,
|
|
3709
|
+
parseStatus: breakdown.parseStatus,
|
|
3710
|
+
parseErrors: breakdown.parseErrors,
|
|
3711
|
+
detectedSurface: breakdown.surface === "vscode" ? "vscode_extension" : "terminal_cli",
|
|
3712
|
+
surfaceConfidence: breakdown.surface === "unknown" ? "low" : "high",
|
|
3713
|
+
surfaceReason: breakdown.surface === "vscode" ? "GitHub Copilot VS Code workspace storage" : "GitHub Copilot OpenTelemetry export",
|
|
3714
|
+
promptTimeline: breakdown.promptTimeline,
|
|
3715
|
+
userPromptCount: breakdown.userPromptCount,
|
|
3716
|
+
assistantMessageCount: breakdown.assistantMessageCount,
|
|
3717
|
+
toolCallCount: breakdown.toolCallCount,
|
|
3718
|
+
shellCommandCount: breakdown.shellCommandCount,
|
|
3719
|
+
failedToolCallCount: breakdown.failedToolCallCount,
|
|
3720
|
+
nonZeroCommandEvents: 0,
|
|
3721
|
+
importantCommandFailures: 0,
|
|
3722
|
+
harmlessNonZeroEvents: 0,
|
|
3723
|
+
exploratoryMisses: 0,
|
|
3724
|
+
repeatedFailureClusters: 0,
|
|
3725
|
+
commandIssueSeverity: breakdown.failedToolCallCount > 0 ? "warning" : "none",
|
|
3726
|
+
commandIssueImpact: breakdown.failedToolCallCount > 0 ? "medium" : "none",
|
|
3727
|
+
topFailureType: breakdown.failedToolCallCount > 0 ? "unknown" : void 0,
|
|
3728
|
+
commandIssueSamples: [],
|
|
3729
|
+
fileReadCount: breakdown.fileReadCount,
|
|
3730
|
+
fileEditCount: breakdown.fileEditCount,
|
|
3731
|
+
sourceMetadata: { copilot: { sourcePaths: breakdown.sourcePaths, surface: breakdown.surface } }
|
|
3732
|
+
};
|
|
3733
|
+
const cost = hasPricedBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
|
|
3734
|
+
return { ...usage, estimatedCostUsd: cost, warnings: hasPricedBreakdown && cost === void 0 ? [...usage.warnings, "unknown_pricing"] : usage.warnings };
|
|
3735
|
+
}
|
|
3736
|
+
function unknownCopilotRepoInfo() {
|
|
3737
|
+
return {
|
|
3738
|
+
repoRoot: "unknown-copilot-repo",
|
|
3739
|
+
repoName: "Unknown repo/folder",
|
|
3740
|
+
gitRemoteUrl: void 0,
|
|
3741
|
+
gitBranch: void 0,
|
|
3742
|
+
warnings: ["repo_unverified_no_git_root", "copilot_missing_cwd"]
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
function emptyBreakdown3(file, id) {
|
|
3746
|
+
return {
|
|
3747
|
+
id,
|
|
3748
|
+
title: void 0,
|
|
3749
|
+
cwd: file.fallbackCwd,
|
|
3750
|
+
startedAt: void 0,
|
|
3751
|
+
endedAt: void 0,
|
|
3752
|
+
model: void 0,
|
|
3753
|
+
inputTokens: 0,
|
|
3754
|
+
cachedInputTokens: 0,
|
|
3755
|
+
cacheCreationInputTokens: 0,
|
|
3756
|
+
outputTokens: 0,
|
|
3757
|
+
reasoningTokens: 0,
|
|
3758
|
+
usageCount: 0,
|
|
3759
|
+
messageCount: 0,
|
|
3760
|
+
userPromptCount: 0,
|
|
3761
|
+
assistantMessageCount: 0,
|
|
3762
|
+
toolCallCount: 0,
|
|
3763
|
+
shellCommandCount: 0,
|
|
3764
|
+
failedToolCallCount: 0,
|
|
3765
|
+
fileReadCount: 0,
|
|
3766
|
+
fileEditCount: 0,
|
|
3767
|
+
rawEventCount: 0,
|
|
3768
|
+
parseStatus: "ok",
|
|
3769
|
+
parseErrors: [],
|
|
3770
|
+
promptTimeline: [],
|
|
3771
|
+
warnings: [],
|
|
3772
|
+
sourcePaths: [file.filePath],
|
|
3773
|
+
surface: file.surface,
|
|
3774
|
+
tokenEvents: []
|
|
3775
|
+
};
|
|
3776
|
+
}
|
|
3777
|
+
function mergeBreakdown(sessions, breakdown) {
|
|
3778
|
+
const key = breakdown.id ?? breakdown.sourcePaths[0] ?? "unknown-session";
|
|
3779
|
+
const existing = sessions.get(key);
|
|
3780
|
+
if (!existing) {
|
|
3781
|
+
sessions.set(key, breakdown);
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3784
|
+
mergeActivity(existing, breakdown);
|
|
3785
|
+
mergeTokenData(existing, breakdown);
|
|
3786
|
+
existing.sourcePaths = unique4([...existing.sourcePaths, ...breakdown.sourcePaths]);
|
|
3787
|
+
}
|
|
3788
|
+
function mergeTokenData(target, source) {
|
|
3789
|
+
if (source.usageCount <= 0) return;
|
|
3790
|
+
const targetHadUsage = target.usageCount > 0;
|
|
3791
|
+
const targetHasDetailed = hasDetailedTokenBreakdown(target);
|
|
3792
|
+
const sourceHasDetailed = hasDetailedTokenBreakdown(source);
|
|
3793
|
+
if (targetHadUsage) target.warnings = unique4([...target.warnings, "copilot_duplicate_session_merged"]);
|
|
3794
|
+
if (targetHadUsage && targetHasDetailed && !sourceHasDetailed) return;
|
|
3795
|
+
if (targetHadUsage && !targetHasDetailed && sourceHasDetailed) {
|
|
3796
|
+
replaceTokenData(target, source);
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
if (source.tokenEvents.length > 0) {
|
|
3800
|
+
const existingKeys = new Set(target.tokenEvents.map((event) => event.dedupKey));
|
|
3801
|
+
for (const event of source.tokenEvents) {
|
|
3802
|
+
if (existingKeys.has(event.dedupKey)) continue;
|
|
3803
|
+
applyTokenEvent(target, event);
|
|
3804
|
+
existingKeys.add(event.dedupKey);
|
|
3805
|
+
}
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
applyTokenEvent(target, {
|
|
3809
|
+
dedupKey: `merged-total:${source.sourcePaths.join("|")}:${source.usageCount}`,
|
|
3810
|
+
detailed: sourceHasDetailed,
|
|
3811
|
+
inputTokens: source.inputTokens,
|
|
3812
|
+
cachedInputTokens: source.cachedInputTokens,
|
|
3813
|
+
cacheCreationInputTokens: source.cacheCreationInputTokens,
|
|
3814
|
+
outputTokens: source.outputTokens,
|
|
3815
|
+
reasoningTokens: source.reasoningTokens
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
function replaceTokenData(target, source) {
|
|
3819
|
+
target.inputTokens = source.inputTokens;
|
|
3820
|
+
target.cachedInputTokens = source.cachedInputTokens;
|
|
3821
|
+
target.cacheCreationInputTokens = source.cacheCreationInputTokens;
|
|
3822
|
+
target.outputTokens = source.outputTokens;
|
|
3823
|
+
target.reasoningTokens = source.reasoningTokens;
|
|
3824
|
+
target.usageCount = source.usageCount;
|
|
3825
|
+
target.tokenEvents = source.tokenEvents.map((event) => ({ ...event }));
|
|
3826
|
+
}
|
|
3827
|
+
function hasDetailedTokenBreakdown(breakdown) {
|
|
3828
|
+
return breakdown.inputTokens > 0 || breakdown.cachedInputTokens > 0 || breakdown.cacheCreationInputTokens > 0 || breakdown.reasoningTokens > 0;
|
|
3829
|
+
}
|
|
3830
|
+
function applyTokenEvent(breakdown, event) {
|
|
3831
|
+
if (breakdown.tokenEvents.some((existing) => existing.dedupKey === event.dedupKey)) return;
|
|
3832
|
+
breakdown.inputTokens += event.inputTokens;
|
|
3833
|
+
breakdown.cachedInputTokens += event.cachedInputTokens;
|
|
3834
|
+
breakdown.cacheCreationInputTokens += event.cacheCreationInputTokens;
|
|
3835
|
+
breakdown.outputTokens += event.outputTokens;
|
|
3836
|
+
breakdown.reasoningTokens += event.reasoningTokens;
|
|
3837
|
+
breakdown.usageCount += 1;
|
|
3838
|
+
breakdown.tokenEvents.push({ ...event });
|
|
3839
|
+
}
|
|
3840
|
+
function applyModelHints(sessions, modelHints) {
|
|
3841
|
+
for (const breakdown of sessions.values()) {
|
|
3842
|
+
if (!breakdown.id) continue;
|
|
3843
|
+
const hint = modelHints.get(breakdown.id);
|
|
3844
|
+
if (!hint || hasKnownModel(breakdown.model)) continue;
|
|
3845
|
+
breakdown.model = hint.model;
|
|
3846
|
+
breakdown.sourcePaths = unique4([...breakdown.sourcePaths, hint.sourcePath]);
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
function hasKnownModel(model) {
|
|
3850
|
+
const normalized = model?.trim().toLowerCase().replace(/^github[-_]?copilot\//, "").replace(/^copilot\//, "");
|
|
3851
|
+
return Boolean(normalized && normalized !== "auto");
|
|
3852
|
+
}
|
|
3853
|
+
function mergeActivity(target, source) {
|
|
3854
|
+
target.title ??= source.title;
|
|
3855
|
+
target.cwd ??= source.cwd;
|
|
3856
|
+
target.model ??= source.model;
|
|
3857
|
+
target.startedAt = minDate2(target.startedAt, source.startedAt);
|
|
3858
|
+
target.endedAt = maxDate2(target.endedAt, source.endedAt);
|
|
3859
|
+
target.messageCount = Math.max(target.messageCount, source.messageCount);
|
|
3860
|
+
target.userPromptCount = Math.max(target.userPromptCount, source.userPromptCount);
|
|
3861
|
+
target.assistantMessageCount = Math.max(target.assistantMessageCount, source.assistantMessageCount);
|
|
3862
|
+
target.toolCallCount = Math.max(target.toolCallCount, source.toolCallCount);
|
|
3863
|
+
target.shellCommandCount = Math.max(target.shellCommandCount, source.shellCommandCount);
|
|
3864
|
+
target.fileReadCount = Math.max(target.fileReadCount, source.fileReadCount);
|
|
3865
|
+
target.fileEditCount = Math.max(target.fileEditCount, source.fileEditCount);
|
|
3866
|
+
target.rawEventCount = Math.max(target.rawEventCount, source.rawEventCount);
|
|
3867
|
+
target.parseErrors = unique4([...target.parseErrors, ...source.parseErrors]);
|
|
3868
|
+
target.parseStatus = target.parseErrors.length ? "partial" : target.parseStatus;
|
|
3869
|
+
target.promptTimeline = [...target.promptTimeline, ...source.promptTimeline].slice(0, 80);
|
|
3870
|
+
target.warnings = unique4([...target.warnings, ...source.warnings]);
|
|
3871
|
+
return target;
|
|
3872
|
+
}
|
|
3873
|
+
function fallbackWithMissingTokenWarning(breakdown) {
|
|
3874
|
+
if (breakdown.usageCount === 0) breakdown.warnings.push("missing_token_breakdown");
|
|
3875
|
+
return breakdown;
|
|
3876
|
+
}
|
|
3877
|
+
function parseJsonLines3(text) {
|
|
3878
|
+
const records = [];
|
|
3879
|
+
const errors = [];
|
|
3880
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
3881
|
+
const trimmed = line.trim();
|
|
3882
|
+
if (!trimmed) return;
|
|
3883
|
+
try {
|
|
3884
|
+
const parsed = JSON.parse(trimmed);
|
|
3885
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) records.push(parsed);
|
|
3886
|
+
} catch (error) {
|
|
3887
|
+
errors.push(`jsonl_line_${index + 1}:${errorMessage3(error)}`);
|
|
3888
|
+
}
|
|
3889
|
+
});
|
|
3890
|
+
return { records, errors };
|
|
3891
|
+
}
|
|
3892
|
+
function applyTimeline(breakdown, role, content, timestamp3) {
|
|
3893
|
+
if (breakdown.promptTimeline.length >= 80) return;
|
|
3894
|
+
const text = typeof content === "string" ? content : void 0;
|
|
3895
|
+
if (!text?.trim()) return;
|
|
3896
|
+
breakdown.promptTimeline.push({ role, text: text.replace(/\s+/g, " ").trim().slice(0, 1e3), timestamp: timestamp3 });
|
|
3897
|
+
}
|
|
3898
|
+
function firstNonEmptyAttr(attributes, keys) {
|
|
3899
|
+
return keys.map((key) => stringValue4(attributes[key])).find(Boolean);
|
|
3900
|
+
}
|
|
3901
|
+
function bestSessionAttr(attributes) {
|
|
3902
|
+
return sessionAttrs.map((attr) => ({ value: stringValue4(attributes[attr.key]), priority: attr.priority })).filter((item) => Boolean(item.value)).sort((a, b) => b.priority - a.priority)[0];
|
|
3903
|
+
}
|
|
3904
|
+
function traceIdFromRecord(record) {
|
|
3905
|
+
const spanContext = firstObject5(record.spanContext);
|
|
3906
|
+
return stringValue4(record.traceId) ?? stringValue4(spanContext?.traceId);
|
|
3907
|
+
}
|
|
3908
|
+
function spanIdFromRecord(record) {
|
|
3909
|
+
const spanContext = firstObject5(record.spanContext);
|
|
3910
|
+
return stringValue4(record.spanId) ?? stringValue4(spanContext?.spanId);
|
|
3911
|
+
}
|
|
3912
|
+
function dedupKeyForRecord(source, record, attributes, traceId, sessionId, timestamp3, index) {
|
|
3913
|
+
const spanId = spanIdFromRecord(record);
|
|
3914
|
+
if ((source === "chat_span" || source === "agent_summary_span") && traceId && spanId) return `${traceId}:${spanId}`;
|
|
3915
|
+
if (source === "inference_log" && traceId && spanId) return `log:${traceId}:${spanId}`;
|
|
3916
|
+
if (source === "agent_turn_log") {
|
|
3917
|
+
const turn = stringValue4(attributes["turn.index"]) ?? stringValue4(attributes["copilot_chat.turn.index"]) ?? `idx-${index}`;
|
|
3918
|
+
return traceId ? `agent-turn:${traceId}:${turn}` : `agent-turn:${sessionId}:${turn}:${index}`;
|
|
3919
|
+
}
|
|
3920
|
+
return `${source}:${sessionId}:${timestamp3 ?? "unknown"}:${index}`;
|
|
3921
|
+
}
|
|
3922
|
+
function timestampFromRecord(record) {
|
|
3923
|
+
return timestampMsFromParts(record.endTime) ?? timestampMsFromParts(record.startTime) ?? timestampMsFromParts(record.hrTime) ?? timestampMsFromParts(record._hrTime) ?? timestampMsFromScalar(record.timestamp) ?? timestampMsFromScalar(record.observedTimestamp) ?? timestampMsFromUnixNanos(record.timeUnixNano);
|
|
3924
|
+
}
|
|
3925
|
+
function timestampMsFromParts(value) {
|
|
3926
|
+
if (!Array.isArray(value)) return void 0;
|
|
3927
|
+
const seconds = numberValue2(value[0]);
|
|
3928
|
+
const nanos = numberValue2(value[1]);
|
|
3929
|
+
if (seconds <= 0) return void 0;
|
|
3930
|
+
return new Date(seconds * 1e3 + Math.floor(nanos / 1e6)).toISOString();
|
|
3931
|
+
}
|
|
3932
|
+
function timestampMsFromScalar(value) {
|
|
3933
|
+
const raw = numberValue2(value);
|
|
3934
|
+
if (raw <= 0) return void 0;
|
|
3935
|
+
const absolute = Math.abs(raw);
|
|
3936
|
+
const ms = absolute >= 1e17 ? raw / 1e6 : absolute >= 1e14 ? raw / 1e3 : absolute >= 1e11 ? raw : raw * 1e3;
|
|
3937
|
+
return new Date(ms).toISOString();
|
|
3938
|
+
}
|
|
3939
|
+
function timestampMsFromUnixNanos(value) {
|
|
3940
|
+
const raw = numberValue2(value);
|
|
3941
|
+
return raw > 0 ? new Date(Math.floor(raw / 1e6)).toISOString() : void 0;
|
|
3942
|
+
}
|
|
3943
|
+
function durationMsFromRecord(record) {
|
|
3944
|
+
const start = timestampMs2(record.startTime);
|
|
3945
|
+
const end = timestampMs2(record.endTime);
|
|
3946
|
+
if (start !== void 0 && end !== void 0 && end > start) return end - start;
|
|
3947
|
+
const duration = numberValue2(record.duration);
|
|
3948
|
+
return duration > 0 ? duration >= 1e6 ? Math.floor(duration / 1e6) : duration : void 0;
|
|
3949
|
+
}
|
|
3950
|
+
function timestampMs2(value) {
|
|
3951
|
+
const iso = timestampMsFromParts(value) ?? timestampMsFromScalar(value);
|
|
3952
|
+
return iso ? Date.parse(iso) : void 0;
|
|
3953
|
+
}
|
|
3954
|
+
function defaultCodeUserRoots() {
|
|
3955
|
+
const home = os4.homedir();
|
|
3956
|
+
if (process.platform === "win32") {
|
|
3957
|
+
const appData = process.env.APPDATA ?? path7.join(home, "AppData", "Roaming");
|
|
3958
|
+
return ["Code", "Code - Insiders", "VSCodium"].map((name) => path7.join(appData, name, "User"));
|
|
3959
|
+
}
|
|
3960
|
+
if (process.platform === "darwin") {
|
|
3961
|
+
return ["Code", "Code - Insiders", "VSCodium"].map((name) => path7.join(home, "Library", "Application Support", name, "User"));
|
|
3962
|
+
}
|
|
3963
|
+
const xdg = process.env.XDG_CONFIG_HOME || path7.join(home, ".config");
|
|
3964
|
+
return ["Code", "Code - Insiders", "VSCodium"].map((name) => path7.join(xdg, name, "User"));
|
|
3965
|
+
}
|
|
3966
|
+
function workspaceFolderFromStorageJson(filePath) {
|
|
3967
|
+
try {
|
|
3968
|
+
const parsed = JSON.parse(fs7.readFileSync(filePath, "utf8"));
|
|
3969
|
+
const folder = stringValue4(parsed.folder);
|
|
3970
|
+
if (!folder) return void 0;
|
|
3971
|
+
return folder.startsWith("file://") ? decodeURIComponent(new URL(folder).pathname) : folder;
|
|
3972
|
+
} catch {
|
|
3973
|
+
return void 0;
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
function cwdFromWorkspaceYaml(filePath) {
|
|
3977
|
+
try {
|
|
3978
|
+
const text = fs7.readFileSync(filePath, "utf8");
|
|
3979
|
+
const match = /^cwd:\s*(.+)$/m.exec(text);
|
|
3980
|
+
return match?.[1]?.trim();
|
|
3981
|
+
} catch {
|
|
3982
|
+
return void 0;
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
function fileMayOverlapScanWindow2(filePath, scanWindow) {
|
|
3986
|
+
if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
|
|
3987
|
+
try {
|
|
3988
|
+
const mtime = fs7.statSync(filePath).mtimeMs;
|
|
3989
|
+
if (scanWindow.fromMs !== void 0 && mtime < scanWindow.fromMs) return false;
|
|
3990
|
+
if (scanWindow.toMs !== void 0 && mtime > scanWindow.toMs) return false;
|
|
3991
|
+
} catch {
|
|
3992
|
+
return true;
|
|
3993
|
+
}
|
|
3994
|
+
return true;
|
|
3995
|
+
}
|
|
3996
|
+
function walk3(root, maxDepth, onFile, depth = 0) {
|
|
3997
|
+
if (depth > maxDepth) return;
|
|
3998
|
+
for (const entry of fs7.readdirSync(root, { withFileTypes: true })) {
|
|
3999
|
+
const fullPath = path7.join(root, entry.name);
|
|
4000
|
+
if (entry.isDirectory()) walk3(fullPath, maxDepth, onFile, depth + 1);
|
|
4001
|
+
else if (entry.isFile()) onFile(fullPath);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
function isReadableRegularFile(filePath, maxBytes) {
|
|
4005
|
+
try {
|
|
4006
|
+
const stat = fs7.statSync(filePath);
|
|
4007
|
+
return stat.isFile() && stat.size <= maxBytes;
|
|
4008
|
+
} catch {
|
|
4009
|
+
return false;
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
function normalizeCopilotModel(model) {
|
|
4013
|
+
if (!model) return void 0;
|
|
4014
|
+
return normalizePricingModelId(model);
|
|
4015
|
+
}
|
|
4016
|
+
function providerFromModel(model) {
|
|
4017
|
+
const normalized = normalizeCopilotModel(model);
|
|
4018
|
+
if (!normalized) return "github-copilot";
|
|
4019
|
+
if (normalized.includes("claude")) return "anthropic";
|
|
4020
|
+
if (normalized.includes("gemini")) return "google";
|
|
4021
|
+
if (isCopilotAliasModel(normalized)) return "github-copilot";
|
|
4022
|
+
if (normalized.includes("gpt")) return "openai";
|
|
4023
|
+
return "github-copilot";
|
|
4024
|
+
}
|
|
4025
|
+
function firstPositiveNumber(object, keys) {
|
|
4026
|
+
return keys.map((key) => numberValue2(object[key])).find((value) => value > 0) ?? 0;
|
|
4027
|
+
}
|
|
4028
|
+
function firstObject5(value) {
|
|
4029
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4030
|
+
}
|
|
4031
|
+
function stringValue4(value) {
|
|
4032
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
4033
|
+
}
|
|
4034
|
+
function numberValue2(value) {
|
|
4035
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.max(Math.trunc(value), 0);
|
|
4036
|
+
if (typeof value === "string" && value.trim()) {
|
|
4037
|
+
const parsed = Number(value);
|
|
4038
|
+
return Number.isFinite(parsed) ? Math.max(Math.trunc(parsed), 0) : 0;
|
|
4039
|
+
}
|
|
4040
|
+
return 0;
|
|
4041
|
+
}
|
|
4042
|
+
function dateValue4(value) {
|
|
4043
|
+
if (typeof value === "string") {
|
|
4044
|
+
const parsed = Date.parse(value);
|
|
4045
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : void 0;
|
|
4046
|
+
}
|
|
4047
|
+
return timestampMsFromScalar(value);
|
|
4048
|
+
}
|
|
4049
|
+
function fileModifiedDate(filePath) {
|
|
4050
|
+
try {
|
|
4051
|
+
return new Date(fs7.statSync(filePath).mtimeMs).toISOString();
|
|
4052
|
+
} catch {
|
|
4053
|
+
return void 0;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
function minDate2(a, b) {
|
|
4057
|
+
if (!a) return b;
|
|
4058
|
+
if (!b) return a;
|
|
4059
|
+
return a < b ? a : b;
|
|
4060
|
+
}
|
|
4061
|
+
function maxDate2(a, b) {
|
|
4062
|
+
if (!a) return b;
|
|
4063
|
+
if (!b) return a;
|
|
4064
|
+
return a > b ? a : b;
|
|
4065
|
+
}
|
|
4066
|
+
function durationMs3(startedAt, endedAt) {
|
|
4067
|
+
if (!startedAt || !endedAt) return void 0;
|
|
4068
|
+
const start = Date.parse(startedAt);
|
|
4069
|
+
const end = Date.parse(endedAt);
|
|
4070
|
+
return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : void 0;
|
|
4071
|
+
}
|
|
4072
|
+
function unique4(items) {
|
|
4073
|
+
return [...new Set(items)];
|
|
4074
|
+
}
|
|
4075
|
+
function uniqueFiles(files) {
|
|
4076
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4077
|
+
return files.filter((file) => {
|
|
4078
|
+
const key = path7.resolve(file.filePath);
|
|
4079
|
+
if (seen.has(key)) return false;
|
|
4080
|
+
seen.add(key);
|
|
4081
|
+
return true;
|
|
4082
|
+
}).sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
4083
|
+
}
|
|
4084
|
+
function errorMessage3(error) {
|
|
4085
|
+
return error instanceof Error ? error.message : String(error);
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
// packages/core/src/sources/cursor.ts
|
|
4089
|
+
import fs8 from "node:fs";
|
|
4090
|
+
import os5 from "node:os";
|
|
4091
|
+
import path8 from "node:path";
|
|
4092
|
+
import Database2 from "better-sqlite3";
|
|
4093
|
+
var parserVersion = "cursor-v1";
|
|
4094
|
+
var maxJsonlBytes2 = 20 * 1024 * 1024;
|
|
4095
|
+
var maxDatabaseBytes = 100 * 1024 * 1024;
|
|
4096
|
+
var cursorStoragePrefixes = ["cursor.", "anysphere."];
|
|
4097
|
+
var interestingTerms = ["cursor", "composer", "aichat", "ai_chat", "aiservice", "agent-transcript", "prompttokenbreakdown"];
|
|
4098
|
+
function scanCursor(options) {
|
|
4099
|
+
const cursorHome = options.cursorHome ?? path8.join(os5.homedir(), ".cursor");
|
|
4100
|
+
const discovery = discoverCursorFiles(cursorHome, Boolean(options.cursorHome));
|
|
4101
|
+
const jsonlFiles = discovery.jsonlFiles.filter((filePath) => fileMayOverlapScanWindow3(filePath, options.scanWindow));
|
|
4102
|
+
const databaseFiles = discovery.databaseFiles.filter((filePath) => fileMayOverlapScanWindow3(filePath, options.scanWindow));
|
|
4103
|
+
const jsonlCandidates = jsonlFiles.map((filePath) => cursorJsonlToCandidate(filePath, cursorHome));
|
|
4104
|
+
const databaseCandidates = databaseFiles.flatMap((filePath) => cursorDatabaseToCandidates(filePath, cursorHome, discovery.warnings));
|
|
4105
|
+
const candidates = dedupeCandidates([...jsonlCandidates, ...databaseCandidates]);
|
|
4106
|
+
const sessions = candidates.map((candidate, index) => candidateToUsage(candidate, index, options));
|
|
4107
|
+
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
4108
|
+
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_"))).length;
|
|
4109
|
+
const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_") || error.startsWith("sqlite_json_parse:"))).length;
|
|
4110
|
+
if (!fs8.existsSync(cursorHome)) {
|
|
4111
|
+
discovery.warnings.push(`Cursor home directory not found at ${cursorHome}`);
|
|
4112
|
+
}
|
|
4113
|
+
if (!fs8.existsSync(discovery.statePath)) {
|
|
4114
|
+
discovery.warnings.push(`Cursor global state database not found at ${discovery.statePath}`);
|
|
4115
|
+
}
|
|
4116
|
+
return {
|
|
4117
|
+
source: {
|
|
4118
|
+
id: "cursor",
|
|
4119
|
+
label: "Cursor",
|
|
4120
|
+
available: discovery.jsonlFiles.length > 0 || discovery.databaseFiles.length > 0,
|
|
4121
|
+
paths: unique5([cursorHome, discovery.projectsPath, discovery.statePath, discovery.workspaceStoragePath, ...discovery.roots]),
|
|
4122
|
+
warnings: discovery.warnings
|
|
4123
|
+
},
|
|
4124
|
+
sessions,
|
|
4125
|
+
stats: {
|
|
4126
|
+
sourceId: "cursor",
|
|
4127
|
+
sourceLabel: "Cursor",
|
|
4128
|
+
homePath: cursorHome,
|
|
4129
|
+
cursorHome,
|
|
4130
|
+
globalStoragePath: discovery.globalStoragePath,
|
|
4131
|
+
workspaceStoragePath: discovery.workspaceStoragePath,
|
|
4132
|
+
projectsPath: discovery.projectsPath,
|
|
4133
|
+
statePath: discovery.statePath,
|
|
4134
|
+
projectsExists: fs8.existsSync(discovery.projectsPath),
|
|
4135
|
+
stateExists: fs8.existsSync(discovery.statePath),
|
|
4136
|
+
sessionsExists: jsonlFiles.length > 0,
|
|
4137
|
+
sessionFileCount: jsonlFiles.length,
|
|
4138
|
+
databaseFileCount: databaseFiles.length,
|
|
4139
|
+
skippedFileCount: discovery.skippedFileCount,
|
|
4140
|
+
sessionsImported: sessions.length,
|
|
4141
|
+
parseFailureCount,
|
|
4142
|
+
unreadableFileCount,
|
|
4143
|
+
malformedFileCount,
|
|
4144
|
+
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
function discoverCursorFiles(cursorHome, explicitHome) {
|
|
4149
|
+
const warnings = [];
|
|
4150
|
+
const appPaths = defaultCursorAppPaths();
|
|
4151
|
+
const roots = explicitHome ? [cursorHome] : unique5([cursorHome, appPaths.statePath, ...cursorOwnedGlobalStorageRoots(appPaths.globalStoragePath), ...cursorOwnedWorkspaceStorageRoots(appPaths.workspaceStoragePath)]);
|
|
4152
|
+
const jsonlFiles = [];
|
|
4153
|
+
const databaseFiles = [];
|
|
4154
|
+
let skippedFileCount = 0;
|
|
4155
|
+
for (const root of roots) {
|
|
4156
|
+
if (!fs8.existsSync(root)) continue;
|
|
4157
|
+
try {
|
|
4158
|
+
if (isReadableRegularFile2(root, maxDatabaseBytes)) {
|
|
4159
|
+
databaseFiles.push(root);
|
|
4160
|
+
continue;
|
|
4161
|
+
}
|
|
4162
|
+
walk4(root, 10, (filePath) => {
|
|
4163
|
+
const lower = filePath.toLowerCase();
|
|
4164
|
+
if (lower.endsWith(".jsonl")) {
|
|
4165
|
+
if (isReadableRegularFile2(filePath, maxJsonlBytes2)) jsonlFiles.push(filePath);
|
|
4166
|
+
else skippedFileCount += 1;
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
if (/\.(sqlite|db|vscdb)$/i.test(filePath)) {
|
|
4170
|
+
if (isReadableRegularFile2(filePath, maxDatabaseBytes)) databaseFiles.push(filePath);
|
|
4171
|
+
else skippedFileCount += 1;
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4174
|
+
} catch (error) {
|
|
4175
|
+
warnings.push(`Unable to inspect Cursor path ${root}: ${errorMessage4(error)}`);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
return {
|
|
4179
|
+
roots,
|
|
4180
|
+
projectsPath: path8.join(cursorHome, "projects"),
|
|
4181
|
+
globalStoragePath: appPaths.globalStoragePath,
|
|
4182
|
+
workspaceStoragePath: appPaths.workspaceStoragePath,
|
|
4183
|
+
statePath: appPaths.statePath,
|
|
4184
|
+
jsonlFiles: unique5(jsonlFiles).sort(cursorJsonlSort),
|
|
4185
|
+
databaseFiles: unique5(databaseFiles).sort(),
|
|
4186
|
+
skippedFileCount,
|
|
4187
|
+
warnings
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
function cursorOwnedGlobalStorageRoots(globalStoragePath) {
|
|
4191
|
+
return cursorOwnedChildDirs(globalStoragePath);
|
|
4192
|
+
}
|
|
4193
|
+
function cursorOwnedWorkspaceStorageRoots(workspaceStoragePath) {
|
|
4194
|
+
if (!fs8.existsSync(workspaceStoragePath)) return [];
|
|
4195
|
+
const roots = [];
|
|
4196
|
+
try {
|
|
4197
|
+
for (const workspace of fs8.readdirSync(workspaceStoragePath, { withFileTypes: true })) {
|
|
4198
|
+
if (!workspace.isDirectory()) continue;
|
|
4199
|
+
roots.push(...cursorOwnedChildDirs(path8.join(workspaceStoragePath, workspace.name)));
|
|
4200
|
+
}
|
|
4201
|
+
} catch {
|
|
4202
|
+
return [];
|
|
4203
|
+
}
|
|
4204
|
+
return roots;
|
|
4205
|
+
}
|
|
4206
|
+
function cursorOwnedChildDirs(root) {
|
|
4207
|
+
if (!fs8.existsSync(root)) return [];
|
|
4208
|
+
try {
|
|
4209
|
+
return fs8.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && cursorStoragePrefixes.some((prefix) => entry.name.toLowerCase().startsWith(prefix))).map((entry) => path8.join(root, entry.name));
|
|
4210
|
+
} catch {
|
|
4211
|
+
return [];
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
function defaultCursorAppPaths() {
|
|
4215
|
+
const home = os5.homedir();
|
|
4216
|
+
if (process.platform === "win32") {
|
|
4217
|
+
const appData = process.env.APPDATA ?? path8.join(home, "AppData", "Roaming");
|
|
4218
|
+
const globalStoragePath2 = path8.join(appData, "Cursor", "User", "globalStorage");
|
|
4219
|
+
return {
|
|
4220
|
+
globalStoragePath: globalStoragePath2,
|
|
4221
|
+
workspaceStoragePath: path8.join(appData, "Cursor", "User", "workspaceStorage"),
|
|
4222
|
+
statePath: path8.join(globalStoragePath2, "state.vscdb")
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
if (process.platform === "darwin") {
|
|
4226
|
+
const globalStoragePath2 = path8.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
|
|
4227
|
+
return {
|
|
4228
|
+
globalStoragePath: globalStoragePath2,
|
|
4229
|
+
workspaceStoragePath: path8.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"),
|
|
4230
|
+
statePath: path8.join(globalStoragePath2, "state.vscdb")
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
const xdg = process.env.XDG_CONFIG_HOME || path8.join(home, ".config");
|
|
4234
|
+
const globalStoragePath = path8.join(xdg, "Cursor", "User", "globalStorage");
|
|
4235
|
+
return {
|
|
4236
|
+
globalStoragePath,
|
|
4237
|
+
workspaceStoragePath: path8.join(xdg, "Cursor", "User", "workspaceStorage"),
|
|
4238
|
+
statePath: path8.join(globalStoragePath, "state.vscdb")
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
function walk4(root, maxDepth, onFile, depth = 0) {
|
|
4242
|
+
if (depth > maxDepth) return;
|
|
4243
|
+
for (const entry of fs8.readdirSync(root, { withFileTypes: true })) {
|
|
4244
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "CachedData" || entry.name === "Cache") continue;
|
|
4245
|
+
const fullPath = path8.join(root, entry.name);
|
|
4246
|
+
if (entry.isDirectory()) {
|
|
4247
|
+
walk4(fullPath, maxDepth, onFile, depth + 1);
|
|
4248
|
+
} else if (entry.isFile()) {
|
|
4249
|
+
onFile(fullPath);
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
function isReadableRegularFile2(filePath, maxBytes) {
|
|
4254
|
+
try {
|
|
4255
|
+
fs8.accessSync(filePath, fs8.constants.R_OK);
|
|
4256
|
+
const stat = fs8.statSync(filePath);
|
|
4257
|
+
return stat.isFile() && stat.size <= maxBytes;
|
|
4258
|
+
} catch {
|
|
4259
|
+
return false;
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
function cursorJsonlSort(a, b) {
|
|
4263
|
+
const aTranscript = a.includes(`${path8.sep}agent-transcripts${path8.sep}`) ? 0 : 1;
|
|
4264
|
+
const bTranscript = b.includes(`${path8.sep}agent-transcripts${path8.sep}`) ? 0 : 1;
|
|
4265
|
+
return aTranscript - bTranscript || a.localeCompare(b);
|
|
4266
|
+
}
|
|
4267
|
+
function fileMayOverlapScanWindow3(filePath, scanWindow) {
|
|
4268
|
+
if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
|
|
4269
|
+
const inferredTimestamp = timestampMs3(inferDateFromCursorPath(filePath));
|
|
4270
|
+
if (scanWindow.fromMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp < scanWindow.fromMs) return false;
|
|
4271
|
+
if (scanWindow.toMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp > scanWindow.toMs) return false;
|
|
4272
|
+
if (scanWindow.fromMs === void 0) return true;
|
|
4273
|
+
try {
|
|
4274
|
+
return fs8.statSync(filePath).mtimeMs >= scanWindow.fromMs;
|
|
4275
|
+
} catch {
|
|
3033
4276
|
return true;
|
|
3034
4277
|
}
|
|
3035
4278
|
}
|
|
3036
4279
|
function cursorJsonlToCandidate(filePath, cursorHome) {
|
|
3037
|
-
const breakdown =
|
|
4280
|
+
const breakdown = emptyBreakdown4();
|
|
3038
4281
|
try {
|
|
3039
|
-
const parse =
|
|
4282
|
+
const parse = parseJsonLines4(fs8.readFileSync(filePath, "utf8"));
|
|
3040
4283
|
breakdown.rawEventCount = parse.records.length;
|
|
3041
4284
|
breakdown.parseErrors = parse.errors;
|
|
3042
4285
|
breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
3043
4286
|
for (const record of parse.records) applyCursorRecord(breakdown, record);
|
|
3044
4287
|
finalizeBreakdown(breakdown);
|
|
3045
4288
|
} catch (error) {
|
|
3046
|
-
const message = `unable_to_read_cursor_jsonl:${
|
|
4289
|
+
const message = `unable_to_read_cursor_jsonl:${errorMessage4(error)}`;
|
|
3047
4290
|
breakdown.parseStatus = "failed";
|
|
3048
4291
|
breakdown.parseErrors.push(message);
|
|
3049
4292
|
breakdown.warnings.push(message);
|
|
3050
4293
|
}
|
|
3051
|
-
if (!breakdown.id) breakdown.id =
|
|
3052
|
-
const fallbackDate = inferDateFromCursorPath(filePath) ??
|
|
4294
|
+
if (!breakdown.id) breakdown.id = path8.basename(filePath).replace(/\.jsonl$/i, "") || void 0;
|
|
4295
|
+
const fallbackDate = inferDateFromCursorPath(filePath) ?? fileModifiedDate2(filePath);
|
|
3053
4296
|
if (fallbackDate) {
|
|
3054
4297
|
breakdown.startedAt ??= fallbackDate;
|
|
3055
4298
|
breakdown.endedAt ??= fallbackDate;
|
|
@@ -3079,7 +4322,7 @@ function cursorDatabaseToCandidates(filePath, cursorHome, warnings) {
|
|
|
3079
4322
|
}
|
|
3080
4323
|
}
|
|
3081
4324
|
} catch (error) {
|
|
3082
|
-
warnings.push(`Unable to read Cursor SQLite database at ${filePath}: ${
|
|
4325
|
+
warnings.push(`Unable to read Cursor SQLite database at ${filePath}: ${errorMessage4(error)}`);
|
|
3083
4326
|
} finally {
|
|
3084
4327
|
db?.close();
|
|
3085
4328
|
}
|
|
@@ -3089,7 +4332,7 @@ function safeColumns(db, table, warnings) {
|
|
|
3089
4332
|
try {
|
|
3090
4333
|
return db.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all();
|
|
3091
4334
|
} catch (error) {
|
|
3092
|
-
warnings.push(`Unable to inspect Cursor SQLite table ${table}: ${
|
|
4335
|
+
warnings.push(`Unable to inspect Cursor SQLite table ${table}: ${errorMessage4(error)}`);
|
|
3093
4336
|
return [];
|
|
3094
4337
|
}
|
|
3095
4338
|
}
|
|
@@ -3098,7 +4341,7 @@ function safeRows(db, table, columns, warnings) {
|
|
|
3098
4341
|
const select = columns.map(quoteIdentifier).join(", ");
|
|
3099
4342
|
return db.prepare(`SELECT ${select} FROM ${quoteIdentifier(table)} LIMIT 500`).all();
|
|
3100
4343
|
} catch (error) {
|
|
3101
|
-
warnings.push(`Unable to scan Cursor SQLite table ${table}: ${
|
|
4344
|
+
warnings.push(`Unable to scan Cursor SQLite table ${table}: ${errorMessage4(error)}`);
|
|
3102
4345
|
return [];
|
|
3103
4346
|
}
|
|
3104
4347
|
}
|
|
@@ -3126,11 +4369,11 @@ function candidateFromDatabaseRow(filePath, cursorHome, table, row) {
|
|
|
3126
4369
|
if (!parsed.ok && text.trim().startsWith("{")) parseErrors.push(`sqlite_json_parse:${table}.${key}:${parsed.error}`);
|
|
3127
4370
|
}
|
|
3128
4371
|
if (!records.length) return void 0;
|
|
3129
|
-
const breakdown =
|
|
4372
|
+
const breakdown = emptyBreakdown4();
|
|
3130
4373
|
breakdown.parseErrors = parseErrors;
|
|
3131
4374
|
breakdown.parseStatus = parseErrors.length ? "partial" : "ok";
|
|
3132
4375
|
breakdown.rawEventCount = records.length;
|
|
3133
|
-
breakdown.matchedKeys =
|
|
4376
|
+
breakdown.matchedKeys = unique5(matchedKeys);
|
|
3134
4377
|
for (const record of records) applyCursorRecord(breakdown, record);
|
|
3135
4378
|
finalizeBreakdown(breakdown);
|
|
3136
4379
|
if (isCursorMetadataOnlyBreakdown(breakdown)) return void 0;
|
|
@@ -3155,29 +4398,29 @@ function parseJsonBlob(text) {
|
|
|
3155
4398
|
try {
|
|
3156
4399
|
return { ok: true, value: JSON.parse(trimmed) };
|
|
3157
4400
|
} catch (error) {
|
|
3158
|
-
return { ok: false, error:
|
|
4401
|
+
return { ok: false, error: errorMessage4(error) };
|
|
3159
4402
|
}
|
|
3160
4403
|
}
|
|
3161
|
-
function
|
|
4404
|
+
function parseJsonLines4(text) {
|
|
3162
4405
|
const records = [];
|
|
3163
4406
|
const errors = [];
|
|
3164
4407
|
text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index })).filter(({ line }) => Boolean(line)).forEach(({ line, index }) => {
|
|
3165
4408
|
try {
|
|
3166
4409
|
records.push(JSON.parse(line));
|
|
3167
4410
|
} catch (error) {
|
|
3168
|
-
errors.push(`jsonl_line_${index + 1}:${
|
|
4411
|
+
errors.push(`jsonl_line_${index + 1}:${errorMessage4(error)}`);
|
|
3169
4412
|
}
|
|
3170
4413
|
});
|
|
3171
4414
|
return { records, errors };
|
|
3172
4415
|
}
|
|
3173
4416
|
function applyCursorRecord(breakdown, record) {
|
|
3174
4417
|
const objects = collectObjects(record, 6, 150);
|
|
3175
|
-
const root =
|
|
4418
|
+
const root = firstObject6(record);
|
|
3176
4419
|
const timestamp3 = firstValueByKeys(objects, ["timestamp", "createdAt", "created_at", "lastUpdatedAt", "conversationCheckpointLastUpdatedAt", "time", "date"]);
|
|
3177
|
-
const date =
|
|
4420
|
+
const date = dateValue5(timestamp3);
|
|
3178
4421
|
if (date) {
|
|
3179
|
-
breakdown.startedAt =
|
|
3180
|
-
breakdown.endedAt =
|
|
4422
|
+
breakdown.startedAt = minDate3(breakdown.startedAt, date);
|
|
4423
|
+
breakdown.endedAt = maxDate3(breakdown.endedAt, date);
|
|
3181
4424
|
}
|
|
3182
4425
|
breakdown.id ??= firstStringByKeys(objects, ["sessionId", "session_id", "conversationId", "conversation_id", "composerId", "composer_id", "chatId", "chat_id"]);
|
|
3183
4426
|
breakdown.title ??= firstStringByKeys(objects, ["title", "name", "summary"]);
|
|
@@ -3222,13 +4465,13 @@ function collectObjects(value, maxDepth, maxObjects, objects = []) {
|
|
|
3222
4465
|
for (const child of Object.values(object)) collectObjects(child, maxDepth - 1, maxObjects, objects);
|
|
3223
4466
|
return objects;
|
|
3224
4467
|
}
|
|
3225
|
-
function
|
|
4468
|
+
function firstObject6(value) {
|
|
3226
4469
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3227
4470
|
}
|
|
3228
4471
|
function firstStringByKeys(objects, keys) {
|
|
3229
4472
|
for (const object of objects) {
|
|
3230
4473
|
for (const key of keys) {
|
|
3231
|
-
const value =
|
|
4474
|
+
const value = stringValue5(object[key]);
|
|
3232
4475
|
if (value) return value;
|
|
3233
4476
|
}
|
|
3234
4477
|
}
|
|
@@ -3251,7 +4494,7 @@ function firstPathByKeys(objects, keys) {
|
|
|
3251
4494
|
function firstNumberByKeys(objects, keys) {
|
|
3252
4495
|
for (const object of objects) {
|
|
3253
4496
|
for (const key of keys) {
|
|
3254
|
-
const value =
|
|
4497
|
+
const value = numberValue3(object[key]);
|
|
3255
4498
|
if (value !== void 0) return value;
|
|
3256
4499
|
}
|
|
3257
4500
|
}
|
|
@@ -3261,7 +4504,7 @@ function inferRole(objects, root) {
|
|
|
3261
4504
|
const role = firstStringByKeys(objects, ["role", "author", "speaker", "from"])?.toLowerCase();
|
|
3262
4505
|
if (role === "user" || role === "human") return "user";
|
|
3263
4506
|
if (role === "assistant" || role === "ai" || role === "agent") return "assistant";
|
|
3264
|
-
const type = (
|
|
4507
|
+
const type = (stringValue5(root?.type) ?? stringValue5(root?.event) ?? "").toLowerCase();
|
|
3265
4508
|
if (type.includes("user") || type.includes("prompt")) return "user";
|
|
3266
4509
|
if (type.includes("assistant") || type.includes("response") || type.includes("completion")) return "assistant";
|
|
3267
4510
|
return void 0;
|
|
@@ -3282,9 +4525,9 @@ function textFromUnknown(value) {
|
|
|
3282
4525
|
const parts = value.map(textFromUnknown).filter((item) => Boolean(item));
|
|
3283
4526
|
return parts.length ? parts.join("\n") : void 0;
|
|
3284
4527
|
}
|
|
3285
|
-
const object =
|
|
4528
|
+
const object = firstObject6(value);
|
|
3286
4529
|
if (!object) return void 0;
|
|
3287
|
-
return
|
|
4530
|
+
return stringValue5(object.text) ?? stringValue5(object.content) ?? stringValue5(object.value);
|
|
3288
4531
|
}
|
|
3289
4532
|
function extractUsage(objects) {
|
|
3290
4533
|
const usageObjects = objects.filter((object) => Object.keys(object).some((key) => key.toLowerCase().includes("token") || key.toLowerCase() === "usage"));
|
|
@@ -3299,7 +4542,7 @@ function extractUsage(objects) {
|
|
|
3299
4542
|
};
|
|
3300
4543
|
}
|
|
3301
4544
|
function applyToolSignals(breakdown, objects) {
|
|
3302
|
-
const typeText = objects.flatMap((object) => [
|
|
4545
|
+
const typeText = objects.flatMap((object) => [stringValue5(object.type), stringValue5(object.event), stringValue5(object.toolName), stringValue5(object.tool_name), stringValue5(object.name)]).filter((item) => Boolean(item)).join(" ").toLowerCase();
|
|
3303
4546
|
if (typeText.includes("tool")) breakdown.toolCallCount += 1;
|
|
3304
4547
|
if (typeText.includes("terminal") || typeText.includes("shell") || typeText.includes("command")) breakdown.shellCommandCount += 1;
|
|
3305
4548
|
if (typeText.includes("read")) breakdown.fileReadCount += 1;
|
|
@@ -3308,9 +4551,9 @@ function applyToolSignals(breakdown, objects) {
|
|
|
3308
4551
|
}
|
|
3309
4552
|
function looksFailed(object) {
|
|
3310
4553
|
if (object.isError === true || object.is_error === true || object.error === true) return true;
|
|
3311
|
-
const status =
|
|
4554
|
+
const status = stringValue5(object.status)?.toLowerCase();
|
|
3312
4555
|
if (status === "failed" || status === "error") return true;
|
|
3313
|
-
const exitCode =
|
|
4556
|
+
const exitCode = numberValue3(object.exitCode ?? object.exit_code ?? object.statusCode ?? object.status_code);
|
|
3314
4557
|
return exitCode !== void 0 && exitCode !== 0;
|
|
3315
4558
|
}
|
|
3316
4559
|
function finalizeBreakdown(breakdown) {
|
|
@@ -3323,7 +4566,7 @@ function finalizeBreakdown(breakdown) {
|
|
|
3323
4566
|
}
|
|
3324
4567
|
function candidateToUsage(candidate, index, options) {
|
|
3325
4568
|
const breakdown = candidate.breakdown;
|
|
3326
|
-
const id = breakdown.id ?? `${
|
|
4569
|
+
const id = breakdown.id ?? `${path8.basename(candidate.sourcePath).replace(/\.(jsonl|sqlite|db|vscdb)$/i, "") || "cursor-session"}-${index}`;
|
|
3327
4570
|
const cwd = breakdown.cwd ?? candidate.fallbackCwd ?? process.cwd();
|
|
3328
4571
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
3329
4572
|
const hasTokenBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
|
|
@@ -3358,7 +4601,7 @@ function candidateToUsage(candidate, index, options) {
|
|
|
3358
4601
|
messageCount: breakdown.messageCount,
|
|
3359
4602
|
rawTokenTotal: breakdown.totalTokens || void 0,
|
|
3360
4603
|
warnings: [...repo.warnings, ...breakdown.warnings, ...tokenWarnings, ...hasTokenBreakdown ? [] : ["missing_token_breakdown"]],
|
|
3361
|
-
durationMs:
|
|
4604
|
+
durationMs: durationMs4(breakdown.startedAt, breakdown.endedAt),
|
|
3362
4605
|
rawEventCount: breakdown.rawEventCount,
|
|
3363
4606
|
parseStatus: breakdown.parseStatus,
|
|
3364
4607
|
parseErrors: breakdown.parseErrors,
|
|
@@ -3402,36 +4645,36 @@ function candidateToUsage(candidate, index, options) {
|
|
|
3402
4645
|
};
|
|
3403
4646
|
}
|
|
3404
4647
|
function inferProjectPathFromCursorPath(filePath, cursorHome) {
|
|
3405
|
-
const parts = filePath.split(
|
|
4648
|
+
const parts = filePath.split(path8.sep);
|
|
3406
4649
|
const projectsIndex = parts.lastIndexOf("projects");
|
|
3407
4650
|
if (projectsIndex >= 0 && projectsIndex < parts.length - 1) {
|
|
3408
4651
|
const projectId = parts[projectsIndex + 1];
|
|
3409
4652
|
const decodedProjectPath = decodeCursorProjectId(projectId);
|
|
3410
4653
|
if (decodedProjectPath) return decodedProjectPath;
|
|
3411
4654
|
if (projectId?.startsWith("-")) {
|
|
3412
|
-
return `${
|
|
4655
|
+
return `${path8.sep}${projectId.slice(1).split("-").filter(Boolean).join(path8.sep)}`;
|
|
3413
4656
|
}
|
|
3414
|
-
if (projectId) return
|
|
4657
|
+
if (projectId) return path8.join(cursorHome, "projects", projectId);
|
|
3415
4658
|
}
|
|
3416
4659
|
return void 0;
|
|
3417
4660
|
}
|
|
3418
4661
|
function inferDateFromCursorPath(filePath) {
|
|
3419
|
-
const parts = filePath.split(
|
|
4662
|
+
const parts = filePath.split(path8.sep);
|
|
3420
4663
|
for (const part of parts) {
|
|
3421
4664
|
if (!/^\d{10,13}$/.test(part)) continue;
|
|
3422
|
-
const date =
|
|
4665
|
+
const date = dateValue5(Number(part));
|
|
3423
4666
|
if (date) return date;
|
|
3424
4667
|
}
|
|
3425
4668
|
return void 0;
|
|
3426
4669
|
}
|
|
3427
|
-
function
|
|
4670
|
+
function timestampMs3(value) {
|
|
3428
4671
|
if (!value) return void 0;
|
|
3429
4672
|
const timestamp3 = new Date(value).getTime();
|
|
3430
4673
|
return Number.isFinite(timestamp3) ? timestamp3 : void 0;
|
|
3431
4674
|
}
|
|
3432
|
-
function
|
|
4675
|
+
function fileModifiedDate2(filePath) {
|
|
3433
4676
|
try {
|
|
3434
|
-
return
|
|
4677
|
+
return fs8.statSync(filePath).mtime.toISOString();
|
|
3435
4678
|
} catch {
|
|
3436
4679
|
return void 0;
|
|
3437
4680
|
}
|
|
@@ -3445,24 +4688,24 @@ function decodeCursorProjectId(projectId) {
|
|
|
3445
4688
|
if (!root) return void 0;
|
|
3446
4689
|
const matched = matchEncodedPath(root.path, tokens2.slice(root.consumed));
|
|
3447
4690
|
if (matched) return matched;
|
|
3448
|
-
return
|
|
4691
|
+
return path8.join(root.path, ...tokens2.slice(root.consumed));
|
|
3449
4692
|
}
|
|
3450
4693
|
function cursorProjectRoot(tokens2) {
|
|
3451
|
-
if (tokens2[0] === "Users" && tokens2[1]) return { path:
|
|
3452
|
-
if (tokens2[0] === "home" && tokens2[1]) return { path:
|
|
3453
|
-
if (tokens2[0] === "tmp") return { path:
|
|
4694
|
+
if (tokens2[0] === "Users" && tokens2[1]) return { path: path8.join(path8.sep, "Users", tokens2[1]), consumed: 2 };
|
|
4695
|
+
if (tokens2[0] === "home" && tokens2[1]) return { path: path8.join(path8.sep, "home", tokens2[1]), consumed: 2 };
|
|
4696
|
+
if (tokens2[0] === "tmp") return { path: path8.join(path8.sep, "tmp"), consumed: 1 };
|
|
3454
4697
|
if (/^[A-Za-z]$/.test(tokens2[0] ?? "") && tokens2[1]) return { path: `${tokens2[0]?.toUpperCase()}:\\`, consumed: 1 };
|
|
3455
4698
|
return void 0;
|
|
3456
4699
|
}
|
|
3457
4700
|
function matchEncodedPath(root, tokens2) {
|
|
3458
|
-
if (!tokens2.length) return
|
|
3459
|
-
if (!
|
|
4701
|
+
if (!tokens2.length) return fs8.existsSync(root) ? root : void 0;
|
|
4702
|
+
if (!fs8.existsSync(root)) return void 0;
|
|
3460
4703
|
try {
|
|
3461
|
-
for (const entry of
|
|
4704
|
+
for (const entry of fs8.readdirSync(root, { withFileTypes: true })) {
|
|
3462
4705
|
if (!entry.isDirectory()) continue;
|
|
3463
4706
|
const entryTokens = entry.name.split("-").filter(Boolean);
|
|
3464
4707
|
if (!startsWithTokens(tokens2, entryTokens)) continue;
|
|
3465
|
-
const matched = matchEncodedPath(
|
|
4708
|
+
const matched = matchEncodedPath(path8.join(root, entry.name), tokens2.slice(entryTokens.length));
|
|
3466
4709
|
if (matched) return matched;
|
|
3467
4710
|
}
|
|
3468
4711
|
} catch {
|
|
@@ -3475,8 +4718,8 @@ function startsWithTokens(tokens2, prefix) {
|
|
|
3475
4718
|
}
|
|
3476
4719
|
function inferSurfaceFromPath(filePath) {
|
|
3477
4720
|
const lower = filePath.toLowerCase();
|
|
3478
|
-
if (lower.includes(`${
|
|
3479
|
-
if (lower.includes(`${
|
|
4721
|
+
if (lower.includes(`${path8.sep}agent-transcripts${path8.sep}`) || lower.endsWith(".jsonl")) return "cli";
|
|
4722
|
+
if (lower.includes(`${path8.sep}workspaceStorage${path8.sep}`.toLowerCase()) || lower.includes(`${path8.sep}globalStorage${path8.sep}`.toLowerCase()) || lower.endsWith(".vscdb")) return "desktop";
|
|
3480
4723
|
return "unknown";
|
|
3481
4724
|
}
|
|
3482
4725
|
function cursorSourceAppLabel(surface) {
|
|
@@ -3502,7 +4745,7 @@ function inferOutcome3(breakdown) {
|
|
|
3502
4745
|
if (breakdown.userPromptCount > 0 || breakdown.assistantMessageCount > 0) return "research_only";
|
|
3503
4746
|
return "unknown";
|
|
3504
4747
|
}
|
|
3505
|
-
function
|
|
4748
|
+
function emptyBreakdown4() {
|
|
3506
4749
|
return {
|
|
3507
4750
|
id: void 0,
|
|
3508
4751
|
title: void 0,
|
|
@@ -3585,10 +4828,10 @@ function mergeCursorBreakdowns(primary, secondary) {
|
|
|
3585
4828
|
fileEditCount: Math.max(primary.fileEditCount, secondary.fileEditCount),
|
|
3586
4829
|
rawEventCount: Math.max(primary.rawEventCount, secondary.rawEventCount),
|
|
3587
4830
|
parseStatus: mergeParseStatus(primary.parseStatus, secondary.parseStatus),
|
|
3588
|
-
parseErrors:
|
|
4831
|
+
parseErrors: unique5([...primary.parseErrors, ...secondary.parseErrors]),
|
|
3589
4832
|
promptTimeline: primary.promptTimeline.length >= secondary.promptTimeline.length ? primary.promptTimeline : secondary.promptTimeline,
|
|
3590
|
-
warnings:
|
|
3591
|
-
matchedKeys:
|
|
4833
|
+
warnings: unique5([...primary.warnings, ...secondary.warnings, "cursor_duplicate_session_merged"]),
|
|
4834
|
+
matchedKeys: unique5([...primary.matchedKeys, ...secondary.matchedKeys])
|
|
3592
4835
|
};
|
|
3593
4836
|
}
|
|
3594
4837
|
function mergeParseStatus(a, b) {
|
|
@@ -3597,10 +4840,10 @@ function mergeParseStatus(a, b) {
|
|
|
3597
4840
|
return "ok";
|
|
3598
4841
|
}
|
|
3599
4842
|
function optionalMinDate(current, candidate) {
|
|
3600
|
-
return candidate ?
|
|
4843
|
+
return candidate ? minDate3(current, candidate) : current;
|
|
3601
4844
|
}
|
|
3602
4845
|
function optionalMaxDate(current, candidate) {
|
|
3603
|
-
return candidate ?
|
|
4846
|
+
return candidate ? maxDate3(current, candidate) : current;
|
|
3604
4847
|
}
|
|
3605
4848
|
function providerForModel2(model) {
|
|
3606
4849
|
const normalized = model.toLowerCase();
|
|
@@ -3609,7 +4852,7 @@ function providerForModel2(model) {
|
|
|
3609
4852
|
if (normalized.startsWith("gpt") || normalized.startsWith("o")) return "openai";
|
|
3610
4853
|
return void 0;
|
|
3611
4854
|
}
|
|
3612
|
-
function
|
|
4855
|
+
function stringValue5(value) {
|
|
3613
4856
|
return typeof value === "string" && value.trim() ? value : void 0;
|
|
3614
4857
|
}
|
|
3615
4858
|
function textValue(value) {
|
|
@@ -3617,24 +4860,24 @@ function textValue(value) {
|
|
|
3617
4860
|
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
3618
4861
|
return void 0;
|
|
3619
4862
|
}
|
|
3620
|
-
function
|
|
4863
|
+
function numberValue3(value) {
|
|
3621
4864
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3622
4865
|
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
|
|
3623
4866
|
return void 0;
|
|
3624
4867
|
}
|
|
3625
|
-
function
|
|
4868
|
+
function dateValue5(value) {
|
|
3626
4869
|
if (typeof value === "number" && Number.isFinite(value)) return new Date(value < 1e10 ? value * 1e3 : value).toISOString();
|
|
3627
4870
|
if (typeof value !== "string") return void 0;
|
|
3628
4871
|
const date = new Date(value);
|
|
3629
4872
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
3630
4873
|
}
|
|
3631
|
-
function
|
|
4874
|
+
function minDate3(current, candidate) {
|
|
3632
4875
|
return !current || candidate < current ? candidate : current;
|
|
3633
4876
|
}
|
|
3634
|
-
function
|
|
4877
|
+
function maxDate3(current, candidate) {
|
|
3635
4878
|
return !current || candidate > current ? candidate : current;
|
|
3636
4879
|
}
|
|
3637
|
-
function
|
|
4880
|
+
function durationMs4(startedAt, endedAt) {
|
|
3638
4881
|
if (!startedAt || !endedAt) return void 0;
|
|
3639
4882
|
const start = new Date(startedAt).getTime();
|
|
3640
4883
|
const end = new Date(endedAt).getTime();
|
|
@@ -3646,44 +4889,52 @@ function normalizeTimelineText3(text) {
|
|
|
3646
4889
|
function roundCurrency2(value) {
|
|
3647
4890
|
return Number(value.toFixed(6));
|
|
3648
4891
|
}
|
|
3649
|
-
function
|
|
4892
|
+
function unique5(values) {
|
|
3650
4893
|
return [...new Set(values.filter(Boolean))];
|
|
3651
4894
|
}
|
|
3652
|
-
function
|
|
4895
|
+
function errorMessage4(error) {
|
|
3653
4896
|
return error instanceof Error ? error.message : String(error);
|
|
3654
4897
|
}
|
|
3655
4898
|
|
|
3656
4899
|
// packages/core/src/sources/index.ts
|
|
4900
|
+
var codexAdapter = { id: "codex", scan: scanCodex };
|
|
4901
|
+
var claudeAdapter = { id: "claude", scan: scanClaude };
|
|
4902
|
+
var copilotAdapter = { id: "copilot", scan: scanCopilot };
|
|
4903
|
+
var cursorAdapter = { id: "cursor", scan: scanCursor };
|
|
4904
|
+
var adapters = [
|
|
4905
|
+
{ adapter: codexAdapter, homeKey: "codexHome", homeOption: "codexHome" },
|
|
4906
|
+
{ adapter: claudeAdapter, homeKey: "claudeHome", homeOption: "claudeHome" },
|
|
4907
|
+
{ adapter: copilotAdapter, homeKey: "copilotHome", homeOption: "copilotHome" },
|
|
4908
|
+
{
|
|
4909
|
+
adapter: cursorAdapter,
|
|
4910
|
+
homeKey: "cursorHome",
|
|
4911
|
+
homeOption: "cursorHome",
|
|
4912
|
+
enabled: (options) => options.config?.experimentalSources?.cursor === true || Boolean(options.cursorHome)
|
|
4913
|
+
}
|
|
4914
|
+
];
|
|
3657
4915
|
function scanUsageSources(options) {
|
|
3658
|
-
const
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
...
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
pricing: options.pricing
|
|
3676
|
-
}) : void 0;
|
|
3677
|
-
return {
|
|
3678
|
-
sources: [codex.source, claude.source, ...cursor ? [cursor.source] : []],
|
|
3679
|
-
sourceStats: [codex.stats, claude.stats, ...cursor ? [cursor.stats] : []],
|
|
3680
|
-
sessions: [...codex.sessions, ...claude.sessions, ...cursor?.sessions ?? []]
|
|
3681
|
-
};
|
|
4916
|
+
const sources = [];
|
|
4917
|
+
const sourceStats = [];
|
|
4918
|
+
const sessions = [];
|
|
4919
|
+
for (const entry of adapters) {
|
|
4920
|
+
if (entry.enabled && !entry.enabled(options)) continue;
|
|
4921
|
+
const home = options[entry.homeKey];
|
|
4922
|
+
const result = entry.adapter.scan({
|
|
4923
|
+
...home ? { [entry.homeOption]: home } : {},
|
|
4924
|
+
...options.config ? { config: options.config } : {},
|
|
4925
|
+
...options.scanWindow ? { scanWindow: options.scanWindow } : {},
|
|
4926
|
+
pricing: options.pricing
|
|
4927
|
+
});
|
|
4928
|
+
sources.push(result.source);
|
|
4929
|
+
sourceStats.push(result.stats);
|
|
4930
|
+
sessions.push(...result.sessions);
|
|
4931
|
+
}
|
|
4932
|
+
return { sources, sourceStats, sessions };
|
|
3682
4933
|
}
|
|
3683
4934
|
|
|
3684
4935
|
// apps/server/src/data.ts
|
|
3685
|
-
import
|
|
3686
|
-
import
|
|
4936
|
+
import fs9 from "node:fs";
|
|
4937
|
+
import path9 from "node:path";
|
|
3687
4938
|
import { spawn } from "node:child_process";
|
|
3688
4939
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3689
4940
|
var scanCacheTtlMs = 1e4;
|
|
@@ -3810,8 +5061,8 @@ function writePricingData(models) {
|
|
|
3810
5061
|
}
|
|
3811
5062
|
function clearRepoSpendLocalData() {
|
|
3812
5063
|
const target = repospendHome();
|
|
3813
|
-
const removed =
|
|
3814
|
-
|
|
5064
|
+
const removed = fs9.existsSync(target);
|
|
5065
|
+
fs9.rmSync(target, { recursive: true, force: true });
|
|
3815
5066
|
clearScanCache();
|
|
3816
5067
|
return { path: target, removed };
|
|
3817
5068
|
}
|
|
@@ -3820,11 +5071,11 @@ function clearRepoSpendParseCache() {
|
|
|
3820
5071
|
clearScanCache();
|
|
3821
5072
|
return result;
|
|
3822
5073
|
}
|
|
3823
|
-
function exportJson() {
|
|
3824
|
-
return JSON.stringify(readDashboardData().sessions, null, 2);
|
|
5074
|
+
function exportJson(filters = {}, options = {}) {
|
|
5075
|
+
return JSON.stringify(readDashboardData(filters, options).sessions, null, 2);
|
|
3825
5076
|
}
|
|
3826
|
-
function exportCsv() {
|
|
3827
|
-
return toCsv(readDashboardData().sessions);
|
|
5077
|
+
function exportCsv(filters = {}, options = {}) {
|
|
5078
|
+
return toCsv(readDashboardData(filters, options).sessions);
|
|
3828
5079
|
}
|
|
3829
5080
|
function stringQuery(value) {
|
|
3830
5081
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -3844,6 +5095,7 @@ function displaySessions(sessions, options) {
|
|
|
3844
5095
|
function sourceScopedAppLabel(session) {
|
|
3845
5096
|
const app = session.sourceApp || fallbackSurfaceLabel(session.detectedSurface);
|
|
3846
5097
|
if (!app || app === "Unknown") return sourceClientLabel(session.sourceClient);
|
|
5098
|
+
if (session.sourceClient === "copilot" && app === "Copilot CLI") return "GitHub Copilot CLI";
|
|
3847
5099
|
if (!shouldScopeApp(app)) return app;
|
|
3848
5100
|
return `${sourceClientLabel(session.sourceClient)} on ${app}`;
|
|
3849
5101
|
}
|
|
@@ -3871,10 +5123,10 @@ function warmCacheArgs(filters, options) {
|
|
|
3871
5123
|
return args2;
|
|
3872
5124
|
}
|
|
3873
5125
|
function cliEntryPath() {
|
|
3874
|
-
const currentDir =
|
|
3875
|
-
const sourceCli =
|
|
3876
|
-
if (
|
|
3877
|
-
return
|
|
5126
|
+
const currentDir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
5127
|
+
const sourceCli = path9.join(currentDir, "cli.ts");
|
|
5128
|
+
if (fs9.existsSync(sourceCli)) return sourceCli;
|
|
5129
|
+
return path9.join(currentDir, "cli.js");
|
|
3878
5130
|
}
|
|
3879
5131
|
function shouldScopeApp(app) {
|
|
3880
5132
|
const normalized = app.toLowerCase();
|
|
@@ -3883,6 +5135,7 @@ function shouldScopeApp(app) {
|
|
|
3883
5135
|
function sourceClientLabel(source) {
|
|
3884
5136
|
if (source === "codex") return "Codex";
|
|
3885
5137
|
if (source === "claude") return "Claude Code";
|
|
5138
|
+
if (source === "copilot") return "GitHub Copilot";
|
|
3886
5139
|
if (source === "gemini-cli") return "Gemini CLI";
|
|
3887
5140
|
if (source === "opencode") return "OpenCode";
|
|
3888
5141
|
if (source === "cursor") return "Cursor";
|
|
@@ -3900,14 +5153,64 @@ function runtimeCwd() {
|
|
|
3900
5153
|
return process.env.INIT_CWD || process.cwd();
|
|
3901
5154
|
}
|
|
3902
5155
|
|
|
5156
|
+
// apps/server/src/cli-options.ts
|
|
5157
|
+
function parseCliFilters(args2) {
|
|
5158
|
+
const filters = {};
|
|
5159
|
+
const source = listOption(args2, ["--source"]).filter((value) => value !== "all");
|
|
5160
|
+
const sourceApp = listOption(args2, ["--sourceApp", "--source-app", "--app"]);
|
|
5161
|
+
const repo = listOption(args2, ["--repo"]);
|
|
5162
|
+
const model = listOption(args2, ["--model"]);
|
|
5163
|
+
const from = valueAfter(args2, "--from");
|
|
5164
|
+
const to = valueAfter(args2, "--to");
|
|
5165
|
+
if (source.length) filters.source = source;
|
|
5166
|
+
if (sourceApp.length) filters.sourceApp = sourceApp;
|
|
5167
|
+
if (repo.length) filters.repo = repo;
|
|
5168
|
+
if (model.length) filters.model = model;
|
|
5169
|
+
if (from) filters.from = from;
|
|
5170
|
+
if (to) filters.to = to;
|
|
5171
|
+
return filters;
|
|
5172
|
+
}
|
|
5173
|
+
function parseCliDateFilters(args2) {
|
|
5174
|
+
const filters = {};
|
|
5175
|
+
const from = valueAfter(args2, "--from");
|
|
5176
|
+
const to = valueAfter(args2, "--to");
|
|
5177
|
+
if (from) filters.from = from;
|
|
5178
|
+
if (to) filters.to = to;
|
|
5179
|
+
return filters;
|
|
5180
|
+
}
|
|
5181
|
+
function parseCliDashboardOptions(args2) {
|
|
5182
|
+
return {
|
|
5183
|
+
splitSourceApps: args2.includes("--splitSourceApps") || args2.includes("--split-source-apps")
|
|
5184
|
+
};
|
|
5185
|
+
}
|
|
5186
|
+
function valueAfter(args2, flag) {
|
|
5187
|
+
const index = args2.indexOf(flag);
|
|
5188
|
+
const value = index >= 0 ? args2[index + 1] : void 0;
|
|
5189
|
+
return value && !value.startsWith("--") ? value : void 0;
|
|
5190
|
+
}
|
|
5191
|
+
function listOption(args2, flags) {
|
|
5192
|
+
const values = [];
|
|
5193
|
+
for (const flag of flags) {
|
|
5194
|
+
args2.forEach((arg, index) => {
|
|
5195
|
+
if (arg === flag) {
|
|
5196
|
+
const value = args2[index + 1];
|
|
5197
|
+
if (value && !value.startsWith("--")) values.push(value);
|
|
5198
|
+
} else if (arg.startsWith(`${flag}=`)) {
|
|
5199
|
+
values.push(arg.slice(flag.length + 1));
|
|
5200
|
+
}
|
|
5201
|
+
});
|
|
5202
|
+
}
|
|
5203
|
+
return [...new Set(values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean))];
|
|
5204
|
+
}
|
|
5205
|
+
|
|
3903
5206
|
// apps/server/src/server.ts
|
|
3904
|
-
import
|
|
3905
|
-
import
|
|
5207
|
+
import fs10 from "node:fs";
|
|
5208
|
+
import path10 from "node:path";
|
|
3906
5209
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3907
5210
|
import fastifyStatic from "@fastify/static";
|
|
3908
5211
|
import Fastify from "fastify";
|
|
3909
5212
|
|
|
3910
|
-
// apps/server/src/demo
|
|
5213
|
+
// apps/server/src/demo/fixtures.ts
|
|
3911
5214
|
var baseDate = "2026-05-18";
|
|
3912
5215
|
var demoPricing = {
|
|
3913
5216
|
...defaultPricing,
|
|
@@ -3915,68 +5218,14 @@ var demoPricing = {
|
|
|
3915
5218
|
"gpt-5.3-codex-spark": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
3916
5219
|
"claude-haiku-4-5-20251001": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 }
|
|
3917
5220
|
};
|
|
3918
|
-
function
|
|
3919
|
-
return process.env.REPOSPEND_DEMO_DATA === "lotr";
|
|
3920
|
-
}
|
|
3921
|
-
function readDemoDashboardData(filters = {}, options = {}) {
|
|
3922
|
-
return buildDashboardSnapshot({
|
|
3923
|
-
sources: demoSources,
|
|
3924
|
-
sourceStats: demoSourceStats,
|
|
3925
|
-
sessions: displaySessions(demoSessions, options),
|
|
3926
|
-
filters
|
|
3927
|
-
});
|
|
3928
|
-
}
|
|
3929
|
-
function readDemoRtkGain() {
|
|
5221
|
+
function issue(command2, category, severity, impact, reason) {
|
|
3930
5222
|
return {
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
totalCommands: 248,
|
|
3938
|
-
inputTokens: "18.4M",
|
|
3939
|
-
outputTokens: "2.6M",
|
|
3940
|
-
tokensSaved: "5.8M",
|
|
3941
|
-
savedPercent: 23.9,
|
|
3942
|
-
totalExecTime: "41m",
|
|
3943
|
-
averageExecTime: "9.9s",
|
|
3944
|
-
rtkTokensSaved: "5.8M",
|
|
3945
|
-
rtkSavingsRate: 23.9,
|
|
3946
|
-
rtkCommandsProcessed: 248,
|
|
3947
|
-
rtkAverageCommandRuntime: "9.9s",
|
|
3948
|
-
rtkTopSavingsCommands: [
|
|
3949
|
-
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
3950
|
-
{ command: "pnpm test", count: 18, saved: "840K", averageSavedPercent: 17.4, time: "12m" },
|
|
3951
|
-
{ command: "git diff", count: 39, saved: "620K", averageSavedPercent: 26.8, time: "2m" }
|
|
3952
|
-
],
|
|
3953
|
-
rtkRecentActivity: [
|
|
3954
|
-
"rg council-of-elrond planner",
|
|
3955
|
-
"pnpm test --filter gondor-api",
|
|
3956
|
-
"git diff -- shire-mobile"
|
|
3957
|
-
],
|
|
3958
|
-
rtkDiscoverAvailable: true,
|
|
3959
|
-
rtkDiscoverError: void 0,
|
|
3960
|
-
rtkDiscoverSummary: "Total: 248 fictional command events scanned.",
|
|
3961
|
-
rtkCoverageGaps: [
|
|
3962
|
-
{ command: "cat", count: 21, rtkEquivalent: "rtk sed", status: "easy win", estimatedSavings: "410K" },
|
|
3963
|
-
{ command: "grep", count: 13, rtkEquivalent: "rtk rg", status: "easy win", estimatedSavings: "360K" }
|
|
3964
|
-
],
|
|
3965
|
-
rtkUnhandledCommands: [
|
|
3966
|
-
{ command: "palantirctl inspect", count: 5, example: "palantirctl inspect eye-of-sauron-alerts" }
|
|
3967
|
-
],
|
|
3968
|
-
topCommands: [
|
|
3969
|
-
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
3970
|
-
{ command: "git", count: 46, saved: "720K", averageSavedPercent: 18.3, time: "3m" },
|
|
3971
|
-
{ command: "pnpm", count: 31, saved: "1.1M", averageSavedPercent: 16.8, time: "17m" }
|
|
3972
|
-
],
|
|
3973
|
-
recentCommands: [
|
|
3974
|
-
"rtk rg auth gates-of-moria",
|
|
3975
|
-
"rtk pnpm test --filter palantir-observability",
|
|
3976
|
-
"rtk git diff -- one-ring-infra"
|
|
3977
|
-
],
|
|
3978
|
-
raw: "RepoSpend LOTR demo RTK output",
|
|
3979
|
-
error: void 0
|
|
5223
|
+
command: command2,
|
|
5224
|
+
category,
|
|
5225
|
+
classification: severity === "critical" ? "blocking_failure" : "warning",
|
|
5226
|
+
severity,
|
|
5227
|
+
impact,
|
|
5228
|
+
reason
|
|
3980
5229
|
};
|
|
3981
5230
|
}
|
|
3982
5231
|
var demoSources = [
|
|
@@ -3993,6 +5242,17 @@ var demoSources = [
|
|
|
3993
5242
|
available: true,
|
|
3994
5243
|
paths: ["/demo/middle-earth/claude/projects"],
|
|
3995
5244
|
warnings: []
|
|
5245
|
+
},
|
|
5246
|
+
{
|
|
5247
|
+
id: "copilot",
|
|
5248
|
+
label: "GitHub Copilot",
|
|
5249
|
+
available: true,
|
|
5250
|
+
paths: [
|
|
5251
|
+
"/demo/middle-earth/copilot/otel",
|
|
5252
|
+
"/demo/middle-earth/copilot/session-state",
|
|
5253
|
+
"/demo/middle-earth/vscode/User/workspaceStorage"
|
|
5254
|
+
],
|
|
5255
|
+
warnings: []
|
|
3996
5256
|
}
|
|
3997
5257
|
];
|
|
3998
5258
|
var demoSourceStats = [
|
|
@@ -4024,6 +5284,26 @@ var demoSourceStats = [
|
|
|
4024
5284
|
malformedFileCount: 0,
|
|
4025
5285
|
projectDirCount: 4,
|
|
4026
5286
|
lastScannedAt: "2026-05-18T19:01:00.000Z"
|
|
5287
|
+
},
|
|
5288
|
+
{
|
|
5289
|
+
sourceId: "copilot",
|
|
5290
|
+
sourceLabel: "GitHub Copilot",
|
|
5291
|
+
homePath: "/demo/middle-earth/copilot",
|
|
5292
|
+
copilotHome: "/demo/middle-earth/copilot",
|
|
5293
|
+
otelPath: "/demo/middle-earth/copilot/otel",
|
|
5294
|
+
workspaceStoragePath: "/demo/middle-earth/vscode/User/workspaceStorage",
|
|
5295
|
+
otelExists: true,
|
|
5296
|
+
sessionsExists: true,
|
|
5297
|
+
otelFileCount: 5,
|
|
5298
|
+
transcriptFileCount: 3,
|
|
5299
|
+
debugLogFileCount: 2,
|
|
5300
|
+
sessionStateFileCount: 2,
|
|
5301
|
+
sessionFileCount: 10,
|
|
5302
|
+
sessionsImported: 3,
|
|
5303
|
+
parseFailureCount: 0,
|
|
5304
|
+
unreadableFileCount: 0,
|
|
5305
|
+
malformedFileCount: 0,
|
|
5306
|
+
lastScannedAt: "2026-05-18T19:02:00.000Z"
|
|
4027
5307
|
}
|
|
4028
5308
|
];
|
|
4029
5309
|
var demoInputs = [
|
|
@@ -4200,6 +5480,32 @@ var demoInputs = [
|
|
|
4200
5480
|
outcome: "completed",
|
|
4201
5481
|
branch: "planner/elrond-v2"
|
|
4202
5482
|
},
|
|
5483
|
+
{
|
|
5484
|
+
id: "lotr-session-015",
|
|
5485
|
+
repo: "rivendell-dashboard",
|
|
5486
|
+
title: "Use Copilot to draft council agenda actions",
|
|
5487
|
+
sourceClient: "copilot",
|
|
5488
|
+
sourceApp: "VS Code",
|
|
5489
|
+
sourceAppRaw: "vscode.copilot-chat",
|
|
5490
|
+
model: "lark-vscode-preview",
|
|
5491
|
+
provider: "github-copilot",
|
|
5492
|
+
hour: 14,
|
|
5493
|
+
minute: 55,
|
|
5494
|
+
durationMinutes: 36,
|
|
5495
|
+
inputTokens: 51e4,
|
|
5496
|
+
cachedInputTokens: 18e4,
|
|
5497
|
+
outputTokens: 74e3,
|
|
5498
|
+
reasoningTokens: 0,
|
|
5499
|
+
prompts: 4,
|
|
5500
|
+
replies: 7,
|
|
5501
|
+
tools: 24,
|
|
5502
|
+
commands: 6,
|
|
5503
|
+
edits: 6,
|
|
5504
|
+
reads: 29,
|
|
5505
|
+
outcome: "completed",
|
|
5506
|
+
branch: "planner/copilot-actions",
|
|
5507
|
+
detectedSurface: "vscode_extension"
|
|
5508
|
+
},
|
|
4203
5509
|
{
|
|
4204
5510
|
id: "lotr-session-007",
|
|
4205
5511
|
repo: "palantir-observability",
|
|
@@ -4226,6 +5532,39 @@ var demoInputs = [
|
|
|
4226
5532
|
harmlessNonZero: 2,
|
|
4227
5533
|
exploratoryMisses: 3
|
|
4228
5534
|
},
|
|
5535
|
+
{
|
|
5536
|
+
id: "lotr-session-016",
|
|
5537
|
+
repo: "palantir-observability",
|
|
5538
|
+
title: "Trace Copilot alert summaries through the seeing-stone feed",
|
|
5539
|
+
sourceClient: "copilot",
|
|
5540
|
+
sourceApp: "Copilot CLI",
|
|
5541
|
+
sourceAppRaw: "copilot-cli",
|
|
5542
|
+
model: "copilot/gpt-5-mini",
|
|
5543
|
+
provider: "openai",
|
|
5544
|
+
hour: 16,
|
|
5545
|
+
minute: 35,
|
|
5546
|
+
durationMinutes: 43,
|
|
5547
|
+
inputTokens: 69e4,
|
|
5548
|
+
cachedInputTokens: 21e4,
|
|
5549
|
+
outputTokens: 88e3,
|
|
5550
|
+
reasoningTokens: 0,
|
|
5551
|
+
prompts: 5,
|
|
5552
|
+
replies: 8,
|
|
5553
|
+
tools: 30,
|
|
5554
|
+
commands: 13,
|
|
5555
|
+
edits: 3,
|
|
5556
|
+
reads: 34,
|
|
5557
|
+
outcome: "partial",
|
|
5558
|
+
branch: "alerts/copilot-summaries",
|
|
5559
|
+
warnings: ["copilot_partial_token_breakdown"],
|
|
5560
|
+
importantFailures: 1,
|
|
5561
|
+
topFailureType: "test",
|
|
5562
|
+
detectedSurface: "terminal_cli",
|
|
5563
|
+
tokenConfidence: "medium",
|
|
5564
|
+
issueSamples: [
|
|
5565
|
+
issue("pnpm test --filter palantir-alerts", "test", "warning", "medium", "A generated summary snapshot changed before the matcher was tightened.")
|
|
5566
|
+
]
|
|
5567
|
+
},
|
|
4229
5568
|
{
|
|
4230
5569
|
id: "lotr-session-008",
|
|
4231
5570
|
repo: "mordor-ci",
|
|
@@ -4301,6 +5640,32 @@ var demoInputs = [
|
|
|
4301
5640
|
outcome: "completed",
|
|
4302
5641
|
branch: "docs/ring-config"
|
|
4303
5642
|
},
|
|
5643
|
+
{
|
|
5644
|
+
id: "lotr-session-017",
|
|
5645
|
+
repo: "fellowship-docs",
|
|
5646
|
+
title: "Ask Copilot to review the ring-config redaction guide",
|
|
5647
|
+
sourceClient: "copilot",
|
|
5648
|
+
sourceApp: "VS Code",
|
|
5649
|
+
sourceAppRaw: "vscode.copilot-chat",
|
|
5650
|
+
model: "copilot/claude-sonnet-4.5",
|
|
5651
|
+
provider: "anthropic",
|
|
5652
|
+
hour: 18,
|
|
5653
|
+
minute: 42,
|
|
5654
|
+
durationMinutes: 18,
|
|
5655
|
+
inputTokens: 24e4,
|
|
5656
|
+
cachedInputTokens: 96e3,
|
|
5657
|
+
outputTokens: 31e3,
|
|
5658
|
+
reasoningTokens: 0,
|
|
5659
|
+
prompts: 2,
|
|
5660
|
+
replies: 4,
|
|
5661
|
+
tools: 12,
|
|
5662
|
+
commands: 3,
|
|
5663
|
+
edits: 2,
|
|
5664
|
+
reads: 15,
|
|
5665
|
+
outcome: "completed",
|
|
5666
|
+
branch: "docs/copilot-redaction",
|
|
5667
|
+
detectedSurface: "vscode_extension"
|
|
5668
|
+
},
|
|
4304
5669
|
{
|
|
4305
5670
|
id: "lotr-session-011",
|
|
4306
5671
|
repo: "one-ring-infra",
|
|
@@ -4408,6 +5773,74 @@ var demoInputs = [
|
|
|
4408
5773
|
branch: "offline/bag-end"
|
|
4409
5774
|
}
|
|
4410
5775
|
];
|
|
5776
|
+
|
|
5777
|
+
// apps/server/src/demo/rtk.ts
|
|
5778
|
+
function readDemoRtkGain() {
|
|
5779
|
+
return {
|
|
5780
|
+
available: true,
|
|
5781
|
+
rtkDetected: true,
|
|
5782
|
+
rtkVersion: "rtk-demo 1.0.0",
|
|
5783
|
+
rtkCodexHookStatus: "active",
|
|
5784
|
+
rtkLastActivityAt: "2026-05-18T18:42:00.000Z",
|
|
5785
|
+
rtkDataSource: "fictional demo data",
|
|
5786
|
+
totalCommands: 248,
|
|
5787
|
+
inputTokens: "18.4M",
|
|
5788
|
+
outputTokens: "2.6M",
|
|
5789
|
+
tokensSaved: "5.8M",
|
|
5790
|
+
savedPercent: 23.9,
|
|
5791
|
+
totalExecTime: "41m",
|
|
5792
|
+
averageExecTime: "9.9s",
|
|
5793
|
+
rtkTokensSaved: "5.8M",
|
|
5794
|
+
rtkSavingsRate: 23.9,
|
|
5795
|
+
rtkCommandsProcessed: 248,
|
|
5796
|
+
rtkAverageCommandRuntime: "9.9s",
|
|
5797
|
+
rtkTopSavingsCommands: [
|
|
5798
|
+
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
5799
|
+
{ command: "pnpm test", count: 18, saved: "840K", averageSavedPercent: 17.4, time: "12m" },
|
|
5800
|
+
{ command: "git diff", count: 39, saved: "620K", averageSavedPercent: 26.8, time: "2m" }
|
|
5801
|
+
],
|
|
5802
|
+
rtkRecentActivity: [
|
|
5803
|
+
"rg council-of-elrond planner",
|
|
5804
|
+
"pnpm test --filter gondor-api",
|
|
5805
|
+
"git diff -- shire-mobile"
|
|
5806
|
+
],
|
|
5807
|
+
rtkDiscoverAvailable: true,
|
|
5808
|
+
rtkDiscoverError: void 0,
|
|
5809
|
+
rtkDiscoverSummary: "Total: 248 fictional command events scanned.",
|
|
5810
|
+
rtkCoverageGaps: [
|
|
5811
|
+
{ command: "cat", count: 21, rtkEquivalent: "rtk sed", status: "easy win", estimatedSavings: "410K" },
|
|
5812
|
+
{ command: "grep", count: 13, rtkEquivalent: "rtk rg", status: "easy win", estimatedSavings: "360K" }
|
|
5813
|
+
],
|
|
5814
|
+
rtkUnhandledCommands: [
|
|
5815
|
+
{ command: "palantirctl inspect", count: 5, example: "palantirctl inspect eye-of-sauron-alerts" }
|
|
5816
|
+
],
|
|
5817
|
+
topCommands: [
|
|
5818
|
+
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
5819
|
+
{ command: "git", count: 46, saved: "720K", averageSavedPercent: 18.3, time: "3m" },
|
|
5820
|
+
{ command: "pnpm", count: 31, saved: "1.1M", averageSavedPercent: 16.8, time: "17m" }
|
|
5821
|
+
],
|
|
5822
|
+
recentCommands: [
|
|
5823
|
+
"rtk rg auth gates-of-moria",
|
|
5824
|
+
"rtk pnpm test --filter palantir-observability",
|
|
5825
|
+
"rtk git diff -- one-ring-infra"
|
|
5826
|
+
],
|
|
5827
|
+
raw: "RepoSpend LOTR demo RTK output",
|
|
5828
|
+
error: void 0
|
|
5829
|
+
};
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
// apps/server/src/demo/index.ts
|
|
5833
|
+
function demoModeEnabled() {
|
|
5834
|
+
return process.env.REPOSPEND_DEMO_DATA === "lotr";
|
|
5835
|
+
}
|
|
5836
|
+
function readDemoDashboardData(filters = {}, options = {}) {
|
|
5837
|
+
return buildDashboardSnapshot({
|
|
5838
|
+
sources: demoSources,
|
|
5839
|
+
sourceStats: demoSourceStats,
|
|
5840
|
+
sessions: displaySessions(demoSessions, options),
|
|
5841
|
+
filters
|
|
5842
|
+
});
|
|
5843
|
+
}
|
|
4411
5844
|
var demoSessions = demoInputs.map(toSession);
|
|
4412
5845
|
function toSession(input) {
|
|
4413
5846
|
const start = timestamp2(input.hour, input.minute);
|
|
@@ -4496,16 +5929,6 @@ function detectedSurfaceForApp(sourceApp) {
|
|
|
4496
5929
|
function timestamp2(hour, minute) {
|
|
4497
5930
|
return `${baseDate}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00.000Z`;
|
|
4498
5931
|
}
|
|
4499
|
-
function issue(command2, category, severity, impact, reason) {
|
|
4500
|
-
return {
|
|
4501
|
-
command: command2,
|
|
4502
|
-
category,
|
|
4503
|
-
classification: severity === "critical" ? "blocking_failure" : "warning",
|
|
4504
|
-
severity,
|
|
4505
|
-
impact,
|
|
4506
|
-
reason
|
|
4507
|
-
};
|
|
4508
|
-
}
|
|
4509
5932
|
function demoReply(repo, edits, importantFailures) {
|
|
4510
5933
|
if (importantFailures > 0) {
|
|
4511
5934
|
return `Found the noisy path in ${repo}, separated the risky failures from ordinary exploration, and left a short list of follow-up checks.`;
|
|
@@ -4708,6 +6131,15 @@ function recentCommandLines(lines) {
|
|
|
4708
6131
|
// apps/server/src/server.ts
|
|
4709
6132
|
function createServer(options = {}) {
|
|
4710
6133
|
const app = Fastify({ logger: false });
|
|
6134
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
6135
|
+
if (!isProtectedLocalMutation(request.method, request.url)) return;
|
|
6136
|
+
if (!isAllowedLocalHost(request.headers.host)) {
|
|
6137
|
+
return reply.code(403).send({ error: "Mutating RepoSpend local settings requires a localhost Host header." });
|
|
6138
|
+
}
|
|
6139
|
+
if (!isAllowedLocalOrigin(request.headers.origin)) {
|
|
6140
|
+
return reply.code(403).send({ error: "Mutating RepoSpend local settings requires a localhost Origin." });
|
|
6141
|
+
}
|
|
6142
|
+
});
|
|
4711
6143
|
app.get("/api/health", async () => ({ ok: true, app: "RepoSpend", version: readPackageVersion() }));
|
|
4712
6144
|
app.get("/api/dashboard", async (request) => {
|
|
4713
6145
|
const query = request.query;
|
|
@@ -4745,19 +6177,21 @@ function createServer(options = {}) {
|
|
|
4745
6177
|
const query = request.query;
|
|
4746
6178
|
return warmDashboardCache(warmCacheFilters(), parseDashboardOptions(query));
|
|
4747
6179
|
});
|
|
4748
|
-
app.get("/api/export.json", async (
|
|
6180
|
+
app.get("/api/export.json", async (request, reply) => {
|
|
6181
|
+
const query = request.query;
|
|
4749
6182
|
reply.header("content-type", "application/json; charset=utf-8");
|
|
4750
|
-
return exportJson();
|
|
6183
|
+
return exportJson(parseFilters(query), parseDashboardOptions(query));
|
|
4751
6184
|
});
|
|
4752
|
-
app.get("/api/export.csv", async (
|
|
6185
|
+
app.get("/api/export.csv", async (request, reply) => {
|
|
6186
|
+
const query = request.query;
|
|
4753
6187
|
reply.header("content-type", "text/csv; charset=utf-8");
|
|
4754
6188
|
reply.header("content-disposition", 'attachment; filename="repospend-export.csv"');
|
|
4755
|
-
return exportCsv();
|
|
6189
|
+
return exportCsv(parseFilters(query), parseDashboardOptions(query));
|
|
4756
6190
|
});
|
|
4757
6191
|
if (options.serveWeb !== false) {
|
|
4758
|
-
const serverDist =
|
|
4759
|
-
const packagedWebDist =
|
|
4760
|
-
const workspaceWebDist =
|
|
6192
|
+
const serverDist = path10.dirname(fileURLToPath3(import.meta.url));
|
|
6193
|
+
const packagedWebDist = path10.resolve(serverDist, "../web-dist");
|
|
6194
|
+
const workspaceWebDist = path10.resolve(serverDist, "../../web/dist");
|
|
4761
6195
|
const webDist = fsExists(packagedWebDist) ? packagedWebDist : workspaceWebDist;
|
|
4762
6196
|
if (!fsExists(webDist)) {
|
|
4763
6197
|
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.`);
|
|
@@ -4778,13 +6212,13 @@ function createServer(options = {}) {
|
|
|
4778
6212
|
return app;
|
|
4779
6213
|
}
|
|
4780
6214
|
function fsExists(inputPath) {
|
|
4781
|
-
return Boolean(inputPath) &&
|
|
6215
|
+
return Boolean(inputPath) && path10.isAbsolute(inputPath) && fs10.existsSync(inputPath);
|
|
4782
6216
|
}
|
|
4783
6217
|
function readPackageVersion() {
|
|
4784
|
-
const serverDist =
|
|
4785
|
-
const packagePath =
|
|
6218
|
+
const serverDist = path10.dirname(fileURLToPath3(import.meta.url));
|
|
6219
|
+
const packagePath = path10.resolve(serverDist, "../package.json");
|
|
4786
6220
|
try {
|
|
4787
|
-
const parsed = JSON.parse(
|
|
6221
|
+
const parsed = JSON.parse(fs10.readFileSync(packagePath, "utf8"));
|
|
4788
6222
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
4789
6223
|
} catch {
|
|
4790
6224
|
return "unknown";
|
|
@@ -4816,6 +6250,33 @@ function portCandidates(startPort, fallbackCount) {
|
|
|
4816
6250
|
function isAddressInUse(error) {
|
|
4817
6251
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE");
|
|
4818
6252
|
}
|
|
6253
|
+
function isProtectedLocalMutation(method, url) {
|
|
6254
|
+
if (method !== "PUT" && method !== "POST" && method !== "DELETE") return false;
|
|
6255
|
+
return url.startsWith("/api/settings/") || url.startsWith("/api/cache/");
|
|
6256
|
+
}
|
|
6257
|
+
function isAllowedLocalHost(hostHeader) {
|
|
6258
|
+
if (!hostHeader) return false;
|
|
6259
|
+
const host = hostFromHeader(hostHeader).toLowerCase();
|
|
6260
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
6261
|
+
}
|
|
6262
|
+
function isAllowedLocalOrigin(originHeader) {
|
|
6263
|
+
if (!originHeader) return true;
|
|
6264
|
+
try {
|
|
6265
|
+
return isAllowedLocalHost(new URL(originHeader).host);
|
|
6266
|
+
} catch {
|
|
6267
|
+
return false;
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
function hostFromHeader(hostHeader) {
|
|
6271
|
+
const value = hostHeader.trim();
|
|
6272
|
+
if (value.startsWith("[")) {
|
|
6273
|
+
const end = value.indexOf("]");
|
|
6274
|
+
return end >= 0 ? value.slice(1, end) : value;
|
|
6275
|
+
}
|
|
6276
|
+
const colonCount = (value.match(/:/g) ?? []).length;
|
|
6277
|
+
if (colonCount === 1) return value.slice(0, value.indexOf(":"));
|
|
6278
|
+
return value;
|
|
6279
|
+
}
|
|
4819
6280
|
function validatePricingBody(body) {
|
|
4820
6281
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
4821
6282
|
throw new Error("Expected JSON body with a models object.");
|
|
@@ -4851,7 +6312,6 @@ function validateConfigBody(body) {
|
|
|
4851
6312
|
const input = body;
|
|
4852
6313
|
const config = {};
|
|
4853
6314
|
if (Array.isArray(input.repos)) config.repos = input.repos;
|
|
4854
|
-
if (Array.isArray(input.budgets)) config.budgets = input.budgets;
|
|
4855
6315
|
if (typeof input.pricingPath === "string") config.pricingPath = input.pricingPath;
|
|
4856
6316
|
const experimentalSources = input.experimentalSources;
|
|
4857
6317
|
if (experimentalSources && typeof experimentalSources === "object" && !Array.isArray(experimentalSources)) {
|
|
@@ -4890,24 +6350,26 @@ try {
|
|
|
4890
6350
|
console.log(`RepoSpend dashboard: ${url}`);
|
|
4891
6351
|
openBrowser(url);
|
|
4892
6352
|
} else if (command === "__warm-cache") {
|
|
4893
|
-
readDashboardData(
|
|
6353
|
+
readDashboardData(parseCliDateFilters(args), parseCliDashboardOptions(args));
|
|
4894
6354
|
} else if (command === "scan") {
|
|
4895
|
-
const data = readDashboardData(
|
|
6355
|
+
const data = readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args));
|
|
4896
6356
|
console.log(JSON.stringify({ sources: data.sources, sessionCount: data.sessions.length, summary: data.summary }, null, 2));
|
|
4897
6357
|
} else if (command === "by-repo") {
|
|
4898
|
-
printGroups(groupByRepo(readDashboardData(
|
|
6358
|
+
printGroups(groupByRepo(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4899
6359
|
} else if (command === "by-day") {
|
|
4900
|
-
printGroups(groupByDay(readDashboardData(
|
|
6360
|
+
printGroups(groupByDay(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4901
6361
|
} else if (command === "by-hour") {
|
|
4902
|
-
printGroups(groupByHour(readDashboardData(
|
|
6362
|
+
printGroups(groupByHour(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4903
6363
|
} else if (command === "by-model") {
|
|
4904
|
-
printGroups(groupByModel(readDashboardData(
|
|
6364
|
+
printGroups(groupByModel(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4905
6365
|
} else if (command === "by-app") {
|
|
4906
|
-
printGroups(groupBySourceApp(readDashboardData(
|
|
6366
|
+
printGroups(groupBySourceApp(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
6367
|
+
} else if (command === "doctor") {
|
|
6368
|
+
printDoctor(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)));
|
|
4907
6369
|
} else if (command === "export") {
|
|
4908
6370
|
const format = valueAfter(args, "--format") ?? "json";
|
|
4909
|
-
const filters =
|
|
4910
|
-
const sessions = readDashboardData(filters).sessions;
|
|
6371
|
+
const filters = parseCliFilters(args);
|
|
6372
|
+
const sessions = readDashboardData(filters, parseCliDashboardOptions(args)).sessions;
|
|
4911
6373
|
console.log(format === "csv" ? toCsv(sessions) : JSON.stringify(sessions, null, 2));
|
|
4912
6374
|
} else {
|
|
4913
6375
|
console.error(`Unknown command: ${command}`);
|
|
@@ -4940,26 +6402,42 @@ function printGroups(groups) {
|
|
|
4940
6402
|
}));
|
|
4941
6403
|
console.table(rows);
|
|
4942
6404
|
}
|
|
4943
|
-
function
|
|
4944
|
-
const
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
}
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
6405
|
+
function printDoctor(data) {
|
|
6406
|
+
const confidence = data.confidence;
|
|
6407
|
+
console.log(`RepoSpend data confidence: ${confidence.label} (${confidence.score}/100)`);
|
|
6408
|
+
console.table([
|
|
6409
|
+
{ metric: "Sessions", value: confidence.sessionCount },
|
|
6410
|
+
{ metric: "Token data coverage", value: `${Math.round(confidence.tokenDataPct * 100)}% (${confidence.tokenDataSessions}/${confidence.sessionCount})` },
|
|
6411
|
+
{ metric: "Pricing coverage", value: `${Math.round(confidence.pricingCoveragePct * 100)}% (${confidence.pricedTokenSessions}/${confidence.tokenDataSessions})` },
|
|
6412
|
+
{ metric: "Repo grouping", value: `${Math.round(confidence.verifiedRepoPct * 100)}% verified (${confidence.verifiedRepoSessions}/${confidence.sessionCount})` },
|
|
6413
|
+
{ metric: "Parse issues", value: confidence.parseIssueCount },
|
|
6414
|
+
{ metric: "Source warnings", value: confidence.sourceWarningCount }
|
|
6415
|
+
]);
|
|
6416
|
+
if (confidence.issues.length) {
|
|
6417
|
+
console.table(confidence.issues.map((issue2) => ({
|
|
6418
|
+
severity: issue2.tone,
|
|
6419
|
+
issue: issue2.title,
|
|
6420
|
+
detail: issue2.detail,
|
|
6421
|
+
sessions: issue2.affectedSessionIds.length
|
|
6422
|
+
})));
|
|
6423
|
+
} else {
|
|
6424
|
+
console.log("No data-confidence issues detected.");
|
|
6425
|
+
}
|
|
6426
|
+
console.table(data.sourceStats.map((source) => ({
|
|
6427
|
+
source: source.sourceLabel ?? source.sourceId ?? "unknown",
|
|
6428
|
+
imported: source.sessionsImported ?? 0,
|
|
6429
|
+
files: source.sessionFileCount,
|
|
6430
|
+
parseIssues: source.parseFailureCount ?? 0,
|
|
6431
|
+
primaryData: sourcePrimaryDataFound(source) ? "found" : "missing"
|
|
6432
|
+
})));
|
|
4955
6433
|
}
|
|
4956
|
-
function
|
|
4957
|
-
|
|
4958
|
-
return index >= 0 ? args2[index + 1] : void 0;
|
|
6434
|
+
function sourcePrimaryDataFound(source) {
|
|
6435
|
+
return Boolean(source.stateExists || source.sessionsExists || source.projectsExists || source.historyExists || source.otelExists || (source.otelFileCount ?? 0) > 0 || (source.databaseFileCount ?? 0) > 0);
|
|
4959
6436
|
}
|
|
4960
6437
|
function printHelp() {
|
|
4961
6438
|
console.log(`RepoSpend commands:
|
|
4962
6439
|
repospend serve
|
|
6440
|
+
repospend doctor
|
|
4963
6441
|
repospend scan
|
|
4964
6442
|
repospend by-repo
|
|
4965
6443
|
repospend by-day
|
|
@@ -4970,7 +6448,13 @@ function printHelp() {
|
|
|
4970
6448
|
repospend export --format csv
|
|
4971
6449
|
|
|
4972
6450
|
Options:
|
|
4973
|
-
--source all|codex|claude|cursor
|
|
6451
|
+
--source all|codex|claude|copilot|cursor
|
|
6452
|
+
--sourceApp "VS Code" | --source-app "VS Code" | --app "VS Code"
|
|
6453
|
+
--repo <repo name or path>
|
|
6454
|
+
--model <model id>
|
|
6455
|
+
--from YYYY-MM-DD
|
|
6456
|
+
--to YYYY-MM-DD
|
|
6457
|
+
--split-source-apps
|
|
4974
6458
|
|
|
4975
6459
|
Cursor is experimental and only scans when enabled in ~/.repospend/config.json.`);
|
|
4976
6460
|
}
|