repospend 0.0.9 → 0.1.1
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 +28 -0
- package/README.md +150 -131
- package/dist/cli.js +1973 -439
- package/docs/data-sources.md +128 -0
- package/docs/llms.txt +60 -0
- package/docs/pricing.md +66 -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 +100 -0
- package/package.json +17 -3
- package/web-dist/assets/index-BBw375PL.js +308 -0
- package/web-dist/assets/index-Bzdfbi5w.css +1 -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
|
@@ -9,11 +9,17 @@ function summarize(sessions) {
|
|
|
9
9
|
const cost = sumKnownCost(sessions);
|
|
10
10
|
const inputTokens = sum(sessions, "inputTokens");
|
|
11
11
|
const cachedInputTokens = sum(sessions, "cachedInputTokens");
|
|
12
|
+
const cacheCreationInputTokens = optionalSum(sessions, "cacheCreationInputTokens");
|
|
13
|
+
const cacheCreationInputTokens5m = optionalSum(sessions, "cacheCreationInputTokens5m");
|
|
14
|
+
const cacheCreationInputTokens1h = optionalSum(sessions, "cacheCreationInputTokens1h");
|
|
12
15
|
return {
|
|
13
16
|
estimatedCostUsd: cost.knownCount > 0 ? cost.value : void 0,
|
|
14
17
|
knownCostSessions: cost.knownCount,
|
|
15
18
|
totalTokens: sum(sessions, "totalTokens"),
|
|
16
19
|
cachedInputTokens,
|
|
20
|
+
cacheCreationInputTokens,
|
|
21
|
+
cacheCreationInputTokens5m,
|
|
22
|
+
cacheCreationInputTokens1h,
|
|
17
23
|
inputTokens,
|
|
18
24
|
outputTokens: sum(sessions, "outputTokens"),
|
|
19
25
|
reasoningTokens: sum(sessions, "reasoningTokens"),
|
|
@@ -51,7 +57,12 @@ function groupByRepo(sessions) {
|
|
|
51
57
|
}));
|
|
52
58
|
}
|
|
53
59
|
function groupByModel(sessions) {
|
|
54
|
-
|
|
60
|
+
const groups = groupBy(sessions, (session) => session.model ?? "unknown-model", (session) => session.model ?? "Model not recorded");
|
|
61
|
+
const unknownModelSessions = sessions.filter((session) => !session.model);
|
|
62
|
+
if (unknownModelSessions.length > 0 && unknownModelSessions.every(isClaudeSyntheticZeroUsageSession)) {
|
|
63
|
+
return groups.map((group) => group.id === "unknown-model" ? { ...group, label: "Claude synthetic / no usage" } : group);
|
|
64
|
+
}
|
|
65
|
+
return groups;
|
|
55
66
|
}
|
|
56
67
|
function groupBySourceApp(sessions) {
|
|
57
68
|
return groupBy(sessions, (session) => session.sourceApp, (session) => session.sourceApp);
|
|
@@ -75,6 +86,9 @@ function groupBy(sessions, keyFn, labelFn) {
|
|
|
75
86
|
unknownCostSessions: 0,
|
|
76
87
|
inputTokens: 0,
|
|
77
88
|
cachedInputTokens: 0,
|
|
89
|
+
cacheCreationInputTokens: 0,
|
|
90
|
+
cacheCreationInputTokens5m: 0,
|
|
91
|
+
cacheCreationInputTokens1h: 0,
|
|
78
92
|
outputTokens: 0,
|
|
79
93
|
reasoningTokens: 0,
|
|
80
94
|
totalTokens: 0,
|
|
@@ -84,6 +98,9 @@ function groupBy(sessions, keyFn, labelFn) {
|
|
|
84
98
|
};
|
|
85
99
|
group.inputTokens += session.inputTokens;
|
|
86
100
|
group.cachedInputTokens += session.cachedInputTokens;
|
|
101
|
+
group.cacheCreationInputTokens = (group.cacheCreationInputTokens ?? 0) + (session.cacheCreationInputTokens ?? 0);
|
|
102
|
+
group.cacheCreationInputTokens5m = (group.cacheCreationInputTokens5m ?? 0) + (session.cacheCreationInputTokens5m ?? 0);
|
|
103
|
+
group.cacheCreationInputTokens1h = (group.cacheCreationInputTokens1h ?? 0) + (session.cacheCreationInputTokens1h ?? 0);
|
|
87
104
|
group.outputTokens += session.outputTokens;
|
|
88
105
|
group.reasoningTokens += session.reasoningTokens;
|
|
89
106
|
group.totalTokens += session.totalTokens;
|
|
@@ -100,6 +117,9 @@ function groupBy(sessions, keyFn, labelFn) {
|
|
|
100
117
|
}
|
|
101
118
|
return [...groups.values()].sort((a, b) => (b.estimatedCostUsd ?? 0) - (a.estimatedCostUsd ?? 0) || b.totalTokens - a.totalTokens);
|
|
102
119
|
}
|
|
120
|
+
function isClaudeSyntheticZeroUsageSession(session) {
|
|
121
|
+
return session.warnings.includes("claude_synthetic_zero_usage");
|
|
122
|
+
}
|
|
103
123
|
function toCsv(sessions) {
|
|
104
124
|
const columns = [
|
|
105
125
|
"id",
|
|
@@ -120,6 +140,8 @@ function toCsv(sessions) {
|
|
|
120
140
|
"inputTokens",
|
|
121
141
|
"cachedInputTokens",
|
|
122
142
|
"cacheCreationInputTokens",
|
|
143
|
+
"cacheCreationInputTokens5m",
|
|
144
|
+
"cacheCreationInputTokens1h",
|
|
123
145
|
"outputTokens",
|
|
124
146
|
"reasoningTokens",
|
|
125
147
|
"reasoningOutputTokens",
|
|
@@ -204,15 +226,104 @@ import path2 from "node:path";
|
|
|
204
226
|
import fs from "node:fs";
|
|
205
227
|
import os from "node:os";
|
|
206
228
|
import path from "node:path";
|
|
229
|
+
|
|
230
|
+
// packages/types/src/index.ts
|
|
231
|
+
function normalizePricingModelId(model) {
|
|
232
|
+
return model.toLowerCase().replace(/^github_copilot\//, "").replace(/^github-copilot\//, "").replace(/^copilot\//, "").replace(/claude-(opus|sonnet|haiku)-(\d+)\.(\d+)(?=$|[-.])/, "claude-$1-$2-$3");
|
|
233
|
+
}
|
|
234
|
+
function claudePricingFamilyModel(model, isUsable = () => true) {
|
|
235
|
+
const families = [
|
|
236
|
+
"claude-fable-5",
|
|
237
|
+
"claude-mythos-5",
|
|
238
|
+
"claude-opus-4-7",
|
|
239
|
+
"claude-opus-4-6",
|
|
240
|
+
"claude-opus-4-5",
|
|
241
|
+
"claude-opus-4-1",
|
|
242
|
+
"claude-opus-4",
|
|
243
|
+
"claude-sonnet-4-6",
|
|
244
|
+
"claude-sonnet-4-5",
|
|
245
|
+
"claude-sonnet-4",
|
|
246
|
+
"claude-haiku-4-5",
|
|
247
|
+
"claude-3-5-haiku"
|
|
248
|
+
];
|
|
249
|
+
const normalized = normalizePricingModelId(model);
|
|
250
|
+
if (normalized === "claude-opus-4-6-fast-mode") return void 0;
|
|
251
|
+
return families.find((candidate) => (normalized === candidate || normalized.startsWith(`${candidate}-`) || normalized.startsWith(`${candidate}.`)) && isUsable(candidate));
|
|
252
|
+
}
|
|
253
|
+
function copilotPricingFamilyModel(model, isUsable = () => true) {
|
|
254
|
+
const normalized = normalizePricingModelId(model);
|
|
255
|
+
const claudeFamily = claudePricingFamilyModel(normalized, isUsable);
|
|
256
|
+
if (claudeFamily) return claudeFamily;
|
|
257
|
+
if ((normalized === "gpt-4.1" || normalized.startsWith("gpt-4.1-") || normalized.startsWith("gpt-41")) && isUsable("gpt-4.1")) return "gpt-4.1";
|
|
258
|
+
if ((normalized === "gemini-2.5-pro" || normalized.startsWith("gemini-2.5-pro-")) && isUsable("gemini-2.5-pro")) return "gemini-2.5-pro";
|
|
259
|
+
if ((normalized === "gemini-3-flash" || normalized.startsWith("gemini-3-flash-")) && isUsable("gemini-3-flash")) return "gemini-3-flash";
|
|
260
|
+
if ((normalized === "gemini-3.1-pro" || normalized.startsWith("gemini-3.1-pro-")) && isUsable("gemini-3.1-pro")) return "gemini-3.1-pro";
|
|
261
|
+
if ((normalized === "gemini-3.5-flash" || normalized.startsWith("gemini-3.5-flash-")) && isUsable("gemini-3.5-flash")) return "gemini-3.5-flash";
|
|
262
|
+
if ((normalized === "raptor-mini" || normalized.startsWith("raptor-mini-") || normalized.startsWith("oswe-vscode")) && isUsable("raptor-mini")) return "raptor-mini";
|
|
263
|
+
if ((normalized === "lark" || normalized.startsWith("lark-")) && isUsable("lark")) return "lark";
|
|
264
|
+
if ((normalized === "goldeneye" || normalized.startsWith("goldeneye-")) && isUsable("goldeneye")) return "goldeneye";
|
|
265
|
+
return void 0;
|
|
266
|
+
}
|
|
267
|
+
function parseVersionedModel(normalized) {
|
|
268
|
+
if (normalized.includes("fast-mode")) return void 0;
|
|
269
|
+
const claude = normalized.match(/^(claude-(?:fable|mythos|opus|sonnet|haiku))-(\d+)(?:-(\d+))?/);
|
|
270
|
+
if (claude) {
|
|
271
|
+
const minor = claude[3] ? Number(claude[3]) : 0;
|
|
272
|
+
return { tier: claude[1], version: Number(claude[2]) + minor / 100 };
|
|
273
|
+
}
|
|
274
|
+
const gpt = normalized.match(/^(gpt)-(\d)(?:\.(\d+))?(-[a-z][a-z0-9-]*)?$/);
|
|
275
|
+
if (gpt) {
|
|
276
|
+
const minor = gpt[3] ? Number(gpt[3]) : 0;
|
|
277
|
+
return { tier: `gpt${gpt[4] ?? ""}`, version: Number(gpt[2]) + minor / 100 };
|
|
278
|
+
}
|
|
279
|
+
return void 0;
|
|
280
|
+
}
|
|
281
|
+
function versionedFallbackModel(model, knownModels, isUsable = () => true) {
|
|
282
|
+
const target = parseVersionedModel(normalizePricingModelId(model));
|
|
283
|
+
if (!target) return void 0;
|
|
284
|
+
let best;
|
|
285
|
+
for (const candidate of knownModels) {
|
|
286
|
+
const parsed = parseVersionedModel(normalizePricingModelId(candidate));
|
|
287
|
+
if (!parsed || parsed.tier !== target.tier || parsed.version > target.version) continue;
|
|
288
|
+
if (!isUsable(candidate)) continue;
|
|
289
|
+
if (!best || parsed.version > best.version) best = { id: candidate, version: parsed.version };
|
|
290
|
+
}
|
|
291
|
+
return best?.id;
|
|
292
|
+
}
|
|
293
|
+
function isCopilotAliasModel(model) {
|
|
294
|
+
const normalized = normalizePricingModelId(model);
|
|
295
|
+
return normalized.startsWith("raptor-mini") || normalized.startsWith("oswe-vscode") || normalized === "lark" || normalized.startsWith("lark-") || normalized.startsWith("goldeneye");
|
|
296
|
+
}
|
|
297
|
+
function positiveRate(value) {
|
|
298
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
299
|
+
}
|
|
300
|
+
function isUsableModelPricing(pricing) {
|
|
301
|
+
return positiveRate(pricing?.inputPerMillion) && positiveRate(pricing?.outputPerMillion);
|
|
302
|
+
}
|
|
303
|
+
function findModelPricing(model, pricing) {
|
|
304
|
+
const lower = normalizePricingModelId(model);
|
|
305
|
+
const exact = pricing[model] ?? pricing[lower];
|
|
306
|
+
if (isUsableModelPricing(exact)) return exact;
|
|
307
|
+
const versioned = versionedFallbackModel(lower, Object.keys(pricing), (candidate) => isUsableModelPricing(pricing[candidate]));
|
|
308
|
+
if (versioned) return pricing[versioned];
|
|
309
|
+
const claudeFamily = claudePricingFamilyModel(lower, (candidate) => isUsableModelPricing(pricing[candidate]));
|
|
310
|
+
if (claudeFamily) return pricing[claudeFamily];
|
|
311
|
+
const copilotFamily = copilotPricingFamilyModel(lower, (candidate) => isUsableModelPricing(pricing[candidate]));
|
|
312
|
+
if (copilotFamily) return pricing[copilotFamily];
|
|
313
|
+
return void 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// packages/core/src/pricing.ts
|
|
207
317
|
var pricingInfo = {
|
|
208
|
-
sourceName: "Bundled
|
|
318
|
+
sourceName: "Bundled API-equivalent model pricing",
|
|
209
319
|
sourceUrl: "https://developers.openai.com/api/docs/pricing",
|
|
210
320
|
sourceUrls: [
|
|
211
321
|
{ 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" }
|
|
322
|
+
{ label: "Claude pricing reference", url: "https://platform.claude.com/docs/en/about-claude/pricing" },
|
|
323
|
+
{ label: "GitHub Copilot model pricing reference", url: "https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing" }
|
|
213
324
|
],
|
|
214
325
|
unit: "USD per 1M tokens",
|
|
215
|
-
updatedAt: "2026-
|
|
326
|
+
updatedAt: "2026-06-10",
|
|
216
327
|
note: "RepoSpend estimates API-equivalent cost from local token counts and public API-style Standard pricing. This is not your actual bill; subscriptions, credits, provider terms, cache behavior, regional processing, or other billing factors can make your real cost different."
|
|
217
328
|
};
|
|
218
329
|
var defaultPricing = {
|
|
@@ -238,16 +349,29 @@ var defaultPricing = {
|
|
|
238
349
|
"gpt-5-pro": { inputPerMillion: 15, outputPerMillion: 120, reasoningOutputPerMillion: 120 },
|
|
239
350
|
"gpt-5-mini": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, reasoningOutputPerMillion: 2 },
|
|
240
351
|
"gpt-5-nano": { inputPerMillion: 0.05, cachedInputPerMillion: 5e-3, outputPerMillion: 0.4, reasoningOutputPerMillion: 0.4 },
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
"
|
|
250
|
-
"claude-
|
|
352
|
+
"gpt-4.1": { inputPerMillion: 2, cachedInputPerMillion: 0.5, outputPerMillion: 8 },
|
|
353
|
+
"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." },
|
|
354
|
+
"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." },
|
|
355
|
+
"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." },
|
|
356
|
+
"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." },
|
|
357
|
+
"raptor-mini": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot fine-tuned model using GPT-5 mini pricing." },
|
|
358
|
+
"oswe-vscode-prime": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot Raptor mini internal model id." },
|
|
359
|
+
"lark": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot preview model; estimated with lightweight Copilot pricing." },
|
|
360
|
+
"goldeneye": { inputPerMillion: 1.25, cachedInputPerMillion: 0.125, outputPerMillion: 10, note: "GitHub Copilot fine-tuned model using GPT-5.1-Codex pricing." },
|
|
361
|
+
"claude-fable-5": { inputPerMillion: 10, cacheCreationInput5mPerMillion: 12.5, cacheCreationInput1hPerMillion: 20, cacheCreationInputPerMillion: 20, cachedInputPerMillion: 1, outputPerMillion: 50 },
|
|
362
|
+
"claude-mythos-5": { inputPerMillion: 10, cacheCreationInput5mPerMillion: 12.5, cacheCreationInput1hPerMillion: 20, cacheCreationInputPerMillion: 20, cachedInputPerMillion: 1, outputPerMillion: 50, note: "Limited availability Anthropic model; same public API-equivalent pricing as Claude Fable 5." },
|
|
363
|
+
"claude-opus-4-8": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
364
|
+
"claude-opus-4-7": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
365
|
+
"claude-opus-4-6": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
366
|
+
"claude-opus-4-6-fast-mode": { inputPerMillion: 0, cacheCreationInput5mPerMillion: 0, cacheCreationInput1hPerMillion: 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." },
|
|
367
|
+
"claude-opus-4-5": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
|
|
368
|
+
"claude-opus-4-1": { inputPerMillion: 15, cacheCreationInput5mPerMillion: 18.75, cacheCreationInput1hPerMillion: 30, cacheCreationInputPerMillion: 30, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
|
|
369
|
+
"claude-opus-4": { inputPerMillion: 15, cacheCreationInput5mPerMillion: 18.75, cacheCreationInput1hPerMillion: 30, cacheCreationInputPerMillion: 30, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
|
|
370
|
+
"claude-sonnet-4-6": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
|
|
371
|
+
"claude-sonnet-4-5": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
|
|
372
|
+
"claude-sonnet-4": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
|
|
373
|
+
"claude-haiku-4-5": { inputPerMillion: 1, cacheCreationInput5mPerMillion: 1.25, cacheCreationInput1hPerMillion: 2, cacheCreationInputPerMillion: 2, cachedInputPerMillion: 0.1, outputPerMillion: 5 },
|
|
374
|
+
"claude-3-5-haiku": { inputPerMillion: 0.8, cacheCreationInput5mPerMillion: 1, cacheCreationInput1hPerMillion: 1.6, cacheCreationInputPerMillion: 1.6, cachedInputPerMillion: 0.08, outputPerMillion: 4 }
|
|
251
375
|
};
|
|
252
376
|
function loadPricingTable(pricingPath) {
|
|
253
377
|
if (!pricingPath) {
|
|
@@ -279,40 +403,22 @@ function calculateCostUsd(usage, pricing) {
|
|
|
279
403
|
if (!modelPricing) {
|
|
280
404
|
return void 0;
|
|
281
405
|
}
|
|
282
|
-
const
|
|
406
|
+
const cacheCreationInput5m = usage.cacheCreationInputTokens5m ?? 0;
|
|
407
|
+
const cacheCreationInput1h = usage.cacheCreationInputTokens1h ?? 0;
|
|
408
|
+
const splitCacheCreationInput = cacheCreationInput5m + cacheCreationInput1h;
|
|
409
|
+
const cacheCreationInput = usage.cacheCreationInputTokens ?? splitCacheCreationInput;
|
|
410
|
+
const unclassifiedCacheCreationInput = Math.max(cacheCreationInput - splitCacheCreationInput, 0);
|
|
283
411
|
const billableInput = Math.max(usage.inputTokens - usage.cachedInputTokens - cacheCreationInput, 0);
|
|
284
412
|
const cachedRate = modelPricing.cachedInputPerMillion ?? modelPricing.inputPerMillion;
|
|
285
|
-
const cacheCreationRate = modelPricing.cacheCreationInputPerMillion
|
|
413
|
+
const cacheCreationRate = firstPositiveRate(modelPricing.cacheCreationInputPerMillion, modelPricing.cacheCreationInput1hPerMillion, modelPricing.cacheCreationInput5mPerMillion, modelPricing.inputPerMillion);
|
|
414
|
+
const cacheCreation5mRate = firstPositiveRate(modelPricing.cacheCreationInput5mPerMillion, modelPricing.cacheCreationInputPerMillion, modelPricing.inputPerMillion);
|
|
415
|
+
const cacheCreation1hRate = firstPositiveRate(modelPricing.cacheCreationInput1hPerMillion, modelPricing.cacheCreationInputPerMillion, modelPricing.inputPerMillion);
|
|
286
416
|
const reasoningRate = modelPricing.reasoningOutputPerMillion ?? modelPricing.outputPerMillion;
|
|
287
|
-
const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate +
|
|
417
|
+
const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate + cacheCreationInput5m / 1e6 * cacheCreation5mRate + cacheCreationInput1h / 1e6 * cacheCreation1hRate + unclassifiedCacheCreationInput / 1e6 * cacheCreationRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
|
|
288
418
|
return Number(cost.toFixed(6));
|
|
289
419
|
}
|
|
290
|
-
function
|
|
291
|
-
|
|
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;
|
|
420
|
+
function firstPositiveRate(...rates) {
|
|
421
|
+
return rates.find((rate) => typeof rate === "number" && Number.isFinite(rate) && rate > 0) ?? 0;
|
|
316
422
|
}
|
|
317
423
|
|
|
318
424
|
// packages/core/src/cache.ts
|
|
@@ -411,6 +517,169 @@ function aliasForPath(repoRoot, config) {
|
|
|
411
517
|
return match?.name;
|
|
412
518
|
}
|
|
413
519
|
|
|
520
|
+
// packages/core/src/data-confidence.ts
|
|
521
|
+
function buildDataConfidence(input) {
|
|
522
|
+
const sessions = input.sessions;
|
|
523
|
+
const availableSources = input.sources.filter((source) => source.available);
|
|
524
|
+
const importedSessions = input.sourceStats.reduce((sum2, source) => sum2 + (source.sessionsImported ?? 0), 0);
|
|
525
|
+
const tokenDataSessions = sessions.filter((session) => session.totalTokens > 0);
|
|
526
|
+
const missingTokenSessions = sessions.filter((session) => session.totalTokens === 0 || session.warnings.includes("missing_token_breakdown"));
|
|
527
|
+
const pricedTokenSessions = tokenDataSessions.filter((session) => session.estimatedCostUsd !== void 0);
|
|
528
|
+
const unpricedTokenSessions = tokenDataSessions.filter((session) => session.estimatedCostUsd === void 0);
|
|
529
|
+
const highConfidenceTokenSessions = tokenDataSessions.filter((session) => session.tokenConfidence === "high");
|
|
530
|
+
const unknownRepoSessions = sessions.filter((session) => session.warnings.includes("repo_unverified_no_git_root"));
|
|
531
|
+
const verifiedRepoSessions = sessions.filter((session) => !unknownRepoSessions.includes(session));
|
|
532
|
+
const unknownSurfaceSessions = sessions.filter((session) => (session.detectedSurface ?? "unknown") === "unknown");
|
|
533
|
+
const parseIssueCount = Math.max(
|
|
534
|
+
input.scan.parseFailureCount ?? 0,
|
|
535
|
+
input.sourceStats.reduce((sum2, source) => sum2 + (source.parseFailureCount ?? 0), 0)
|
|
536
|
+
);
|
|
537
|
+
const sourceWarningCount = input.sources.reduce((sum2, source) => sum2 + source.warnings.length, 0);
|
|
538
|
+
const tokenDataPct = ratio(tokenDataSessions.length, sessions.length);
|
|
539
|
+
const pricingCoveragePct = tokenDataSessions.length > 0 ? ratio(pricedTokenSessions.length, tokenDataSessions.length) : sessions.length > 0 ? 0 : 1;
|
|
540
|
+
const highConfidenceTokenPct = tokenDataSessions.length > 0 ? ratio(highConfidenceTokenSessions.length, tokenDataSessions.length) : sessions.length > 0 ? 0 : 1;
|
|
541
|
+
const verifiedRepoPct = ratio(verifiedRepoSessions.length, sessions.length);
|
|
542
|
+
const parseHealthPct = parseIssueCount === 0 ? 1 : 1 - ratio(parseIssueCount, Math.max(input.scan.rawSessionCount ?? sessions.length, sessions.length, 1));
|
|
543
|
+
const rawScore = Math.round(100 * (tokenDataPct * 0.3 + pricingCoveragePct * 0.25 + verifiedRepoPct * 0.18 + highConfidenceTokenPct * 0.12 + Math.max(parseHealthPct, 0) * 0.15));
|
|
544
|
+
const score = emptyDataScore(availableSources.length, importedSessions) ?? rawScore;
|
|
545
|
+
const issues = dataConfidenceIssues({
|
|
546
|
+
sessions,
|
|
547
|
+
availableSources,
|
|
548
|
+
importedSessions,
|
|
549
|
+
tokenDataSessions,
|
|
550
|
+
missingTokenSessions,
|
|
551
|
+
unpricedTokenSessions,
|
|
552
|
+
unknownRepoSessions,
|
|
553
|
+
unknownSurfaceSessions,
|
|
554
|
+
parseIssueCount,
|
|
555
|
+
sourceWarningCount
|
|
556
|
+
});
|
|
557
|
+
return {
|
|
558
|
+
score,
|
|
559
|
+
label: score >= 80 ? "High" : score >= 55 ? "Medium" : "Low",
|
|
560
|
+
sessionCount: sessions.length,
|
|
561
|
+
sourceCount: availableSources.length,
|
|
562
|
+
tokenDataSessions: tokenDataSessions.length,
|
|
563
|
+
missingTokenSessions: missingTokenSessions.length,
|
|
564
|
+
pricedTokenSessions: pricedTokenSessions.length,
|
|
565
|
+
unpricedTokenSessions: unpricedTokenSessions.length,
|
|
566
|
+
highConfidenceTokenSessions: highConfidenceTokenSessions.length,
|
|
567
|
+
verifiedRepoSessions: verifiedRepoSessions.length,
|
|
568
|
+
unknownRepoSessions: unknownRepoSessions.length,
|
|
569
|
+
unknownSurfaceSessions: unknownSurfaceSessions.length,
|
|
570
|
+
parseIssueCount,
|
|
571
|
+
sourceWarningCount,
|
|
572
|
+
tokenDataPct,
|
|
573
|
+
pricingCoveragePct,
|
|
574
|
+
highConfidenceTokenPct,
|
|
575
|
+
verifiedRepoPct,
|
|
576
|
+
issues
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function dataConfidenceIssues({
|
|
580
|
+
sessions,
|
|
581
|
+
availableSources,
|
|
582
|
+
importedSessions,
|
|
583
|
+
tokenDataSessions,
|
|
584
|
+
missingTokenSessions,
|
|
585
|
+
unpricedTokenSessions,
|
|
586
|
+
unknownRepoSessions,
|
|
587
|
+
unknownSurfaceSessions,
|
|
588
|
+
parseIssueCount,
|
|
589
|
+
sourceWarningCount
|
|
590
|
+
}) {
|
|
591
|
+
const issues = [];
|
|
592
|
+
if (availableSources.length === 0) {
|
|
593
|
+
issues.push({
|
|
594
|
+
id: "no-local-sources",
|
|
595
|
+
title: "No local source data found",
|
|
596
|
+
detail: "RepoSpend did not find enabled local Codex, Claude Code, GitHub Copilot, or experimental Cursor data paths.",
|
|
597
|
+
tone: "critical",
|
|
598
|
+
affectedSessionIds: []
|
|
599
|
+
});
|
|
600
|
+
} else if (sessions.length === 0 && importedSessions === 0) {
|
|
601
|
+
issues.push({
|
|
602
|
+
id: "no-sessions-imported",
|
|
603
|
+
title: "No sessions imported",
|
|
604
|
+
detail: "Source paths exist, but no local sessions were imported for the current scan window.",
|
|
605
|
+
tone: "attention",
|
|
606
|
+
affectedSessionIds: []
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
if (tokenDataSessions.length === 0 && sessions.length > 0) {
|
|
610
|
+
issues.push({
|
|
611
|
+
id: "no-token-data",
|
|
612
|
+
title: "No token-bearing sessions",
|
|
613
|
+
detail: "Sessions were imported, but local files did not include token counts that RepoSpend can price or roll up.",
|
|
614
|
+
tone: "critical",
|
|
615
|
+
affectedSessionIds: sessions.map((session) => session.id)
|
|
616
|
+
});
|
|
617
|
+
} else if (missingTokenSessions.length > 0) {
|
|
618
|
+
issues.push({
|
|
619
|
+
id: "missing-token-data",
|
|
620
|
+
title: "Some sessions are missing token data",
|
|
621
|
+
detail: `${missingTokenSessions.length} session${missingTokenSessions.length === 1 ? "" : "s"} have no token total or are missing a full token breakdown.`,
|
|
622
|
+
tone: "attention",
|
|
623
|
+
affectedSessionIds: missingTokenSessions.map((session) => session.id)
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (unpricedTokenSessions.length > 0) {
|
|
627
|
+
issues.push({
|
|
628
|
+
id: "unpriced-token-sessions",
|
|
629
|
+
title: "Some token-bearing sessions are unpriced",
|
|
630
|
+
detail: `${unpricedTokenSessions.length} token-bearing session${unpricedTokenSessions.length === 1 ? "" : "s"} cannot show API-equivalent cost because pricing or token split coverage is missing.`,
|
|
631
|
+
tone: "attention",
|
|
632
|
+
affectedSessionIds: unpricedTokenSessions.map((session) => session.id)
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (parseIssueCount > 0) {
|
|
636
|
+
issues.push({
|
|
637
|
+
id: "parse-issues",
|
|
638
|
+
title: "Parser issues detected",
|
|
639
|
+
detail: `${parseIssueCount} local file${parseIssueCount === 1 ? "" : "s"} reported parse issues or skipped records.`,
|
|
640
|
+
tone: "attention",
|
|
641
|
+
affectedSessionIds: sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).map((session) => session.id)
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (unknownRepoSessions.length > 0) {
|
|
645
|
+
issues.push({
|
|
646
|
+
id: "unknown-repo-grouping",
|
|
647
|
+
title: "Some repo grouping is unverified",
|
|
648
|
+
detail: `${unknownRepoSessions.length} session${unknownRepoSessions.length === 1 ? "" : "s"} could not be tied to a verified Git root.`,
|
|
649
|
+
tone: "attention",
|
|
650
|
+
affectedSessionIds: unknownRepoSessions.map((session) => session.id)
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (unknownSurfaceSessions.length > 0) {
|
|
654
|
+
issues.push({
|
|
655
|
+
id: "unknown-surfaces",
|
|
656
|
+
title: "Some app surfaces are unknown",
|
|
657
|
+
detail: `${unknownSurfaceSessions.length} session${unknownSurfaceSessions.length === 1 ? "" : "s"} did not include enough metadata to identify the app or surface.`,
|
|
658
|
+
tone: "attention",
|
|
659
|
+
affectedSessionIds: unknownSurfaceSessions.map((session) => session.id)
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
if (sourceWarningCount > 0) {
|
|
663
|
+
issues.push({
|
|
664
|
+
id: "source-warnings",
|
|
665
|
+
title: "Source warnings were reported",
|
|
666
|
+
detail: `${sourceWarningCount} source warning${sourceWarningCount === 1 ? " was" : "s were"} reported during local discovery.`,
|
|
667
|
+
tone: "attention",
|
|
668
|
+
affectedSessionIds: []
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
return issues;
|
|
672
|
+
}
|
|
673
|
+
function ratio(numerator, denominator) {
|
|
674
|
+
if (denominator <= 0) return 1;
|
|
675
|
+
return numerator / denominator;
|
|
676
|
+
}
|
|
677
|
+
function emptyDataScore(availableSourceCount, importedSessions) {
|
|
678
|
+
if (availableSourceCount === 0) return 0;
|
|
679
|
+
if (importedSessions === 0) return 50;
|
|
680
|
+
return void 0;
|
|
681
|
+
}
|
|
682
|
+
|
|
414
683
|
// packages/core/src/filters.ts
|
|
415
684
|
function filterUsage(sessions, filters) {
|
|
416
685
|
return sessions.filter((session) => {
|
|
@@ -491,7 +760,20 @@ function repoWarnings(group, sessions) {
|
|
|
491
760
|
return warnings;
|
|
492
761
|
}
|
|
493
762
|
function compactNumber(value) {
|
|
494
|
-
|
|
763
|
+
if (!Number.isFinite(value)) return "0";
|
|
764
|
+
const sign = value < 0 ? "-" : "";
|
|
765
|
+
const absolute = Math.abs(value);
|
|
766
|
+
const units = [
|
|
767
|
+
{ threshold: 1e12, suffix: "T" },
|
|
768
|
+
{ threshold: 1e9, suffix: "B" },
|
|
769
|
+
{ threshold: 1e6, suffix: "M" },
|
|
770
|
+
{ threshold: 1e3, suffix: "K" }
|
|
771
|
+
];
|
|
772
|
+
const unit = units.find((item) => absolute >= item.threshold);
|
|
773
|
+
if (!unit) return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value);
|
|
774
|
+
const scaled = absolute / unit.threshold;
|
|
775
|
+
const formatted = new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(scaled);
|
|
776
|
+
return `${sign}${formatted}${unit.suffix}`;
|
|
495
777
|
}
|
|
496
778
|
function unique2(values) {
|
|
497
779
|
return [...new Set(values)].sort();
|
|
@@ -655,7 +937,7 @@ var healthSignalDetectors = [
|
|
|
655
937
|
return {
|
|
656
938
|
id: "command-friction",
|
|
657
939
|
tone: "attention",
|
|
658
|
-
title: "
|
|
940
|
+
title: "Possible failed command rate looks high",
|
|
659
941
|
detail: `${percent(commandIssueRate)} of detected shell commands look like blocking, repeated, or token-expensive issues in this filtered view.`,
|
|
660
942
|
action: "Look for repeated setup, dependency, permission, test, or build issues in the Sessions page.",
|
|
661
943
|
actionTarget: "sessions",
|
|
@@ -726,9 +1008,9 @@ var healthSignalDetectors = [
|
|
|
726
1008
|
tone: "attention",
|
|
727
1009
|
title: "Some sessions cannot be priced",
|
|
728
1010
|
detail: `${count(unknownCostSessions.length, "session")} are missing a model price or detailed token split.`,
|
|
729
|
-
action: "
|
|
730
|
-
actionTarget: "
|
|
731
|
-
actionLabel: "
|
|
1011
|
+
action: "Review the affected sessions first. Add a model rate only when the session has token details but no usable price.",
|
|
1012
|
+
actionTarget: "sessions",
|
|
1013
|
+
actionLabel: "Review unpriced sessions",
|
|
732
1014
|
metric: count(unknownCostSessions.length, "session"),
|
|
733
1015
|
critical: true,
|
|
734
1016
|
affectedSessionIds: unknownCostSessions.map((session) => session.id)
|
|
@@ -822,7 +1104,7 @@ function buildWasteSignals(sessions) {
|
|
|
822
1104
|
var wasteSignalDetectors = [
|
|
823
1105
|
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
1106
|
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", "
|
|
1107
|
+
wasteSignalDetector("command-issue-sessions", "Possible failed command sessions", "Sessions with blocking, repeated, or token-expensive command failures.", (session) => importantCommandFailures(session) > 0),
|
|
826
1108
|
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
1109
|
wasteSignalDetector("unknown-surfaces", "Unknown surface sessions", "Local logs did not identify VS Code, terminal, exec, or app surface.", (session) => (session.detectedSurface ?? "unknown") === "unknown"),
|
|
828
1110
|
wasteSignalDetector("repeated-errors", "Repeated error signals", "Repeated-error hints found in session warnings.", (session) => session.warnings.some((warning) => warning.includes("repeated_error")))
|
|
@@ -874,7 +1156,20 @@ function percent(value) {
|
|
|
874
1156
|
return `${new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value * 100)}%`;
|
|
875
1157
|
}
|
|
876
1158
|
function compactNumber2(value) {
|
|
877
|
-
|
|
1159
|
+
if (!Number.isFinite(value)) return "0";
|
|
1160
|
+
const sign = value < 0 ? "-" : "";
|
|
1161
|
+
const absolute = Math.abs(value);
|
|
1162
|
+
const units = [
|
|
1163
|
+
{ threshold: 1e12, suffix: "T" },
|
|
1164
|
+
{ threshold: 1e9, suffix: "B" },
|
|
1165
|
+
{ threshold: 1e6, suffix: "M" },
|
|
1166
|
+
{ threshold: 1e3, suffix: "K" }
|
|
1167
|
+
];
|
|
1168
|
+
const unit = units.find((item) => absolute >= item.threshold);
|
|
1169
|
+
if (!unit) return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value);
|
|
1170
|
+
const scaled = absolute / unit.threshold;
|
|
1171
|
+
const formatted = new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(scaled);
|
|
1172
|
+
return `${sign}${formatted}${unit.suffix}`;
|
|
878
1173
|
}
|
|
879
1174
|
function currency(value, minimumFractionDigits, maximumFractionDigits) {
|
|
880
1175
|
return `$${new Intl.NumberFormat(void 0, { maximumFractionDigits, minimumFractionDigits }).format(value)}`;
|
|
@@ -917,11 +1212,12 @@ function buildDashboardSnapshot(input) {
|
|
|
917
1212
|
hours: groupByHour(sessions),
|
|
918
1213
|
models: groupByModel(sessions),
|
|
919
1214
|
sourceApps,
|
|
920
|
-
health: buildUsageHealth({ sessions, summary, repos, sourceApps, scan })
|
|
1215
|
+
health: buildUsageHealth({ sessions, summary, repos, sourceApps, scan }),
|
|
1216
|
+
confidence: buildDataConfidence({ sources: input.sources, sourceStats: input.sourceStats, sessions, scan })
|
|
921
1217
|
};
|
|
922
1218
|
}
|
|
923
1219
|
function isDashboardVisibleSession(session) {
|
|
924
|
-
return session.totalTokens > 0 || session.sourceClient === "claude" || session.sourceClient === "cursor";
|
|
1220
|
+
return session.totalTokens > 0 || session.sourceClient === "claude" || session.sourceClient === "cursor" || session.sourceClient === "copilot";
|
|
925
1221
|
}
|
|
926
1222
|
|
|
927
1223
|
// packages/core/src/repo.ts
|
|
@@ -1031,8 +1327,9 @@ import Database from "better-sqlite3";
|
|
|
1031
1327
|
function analyzeAgentFriction(records, totalTokens) {
|
|
1032
1328
|
const analysis = emptyAgentFrictionAnalysis();
|
|
1033
1329
|
const messages = [];
|
|
1330
|
+
const toolCallsById = indexToolCallsById(records);
|
|
1034
1331
|
for (const record of records) {
|
|
1035
|
-
applyActionMetadata(analysis, record, messages);
|
|
1332
|
+
applyActionMetadata(analysis, record, messages, toolCallsById);
|
|
1036
1333
|
}
|
|
1037
1334
|
finalizeCommandIssues(analysis, totalTokens);
|
|
1038
1335
|
return analysis;
|
|
@@ -1058,7 +1355,7 @@ function emptyAgentFrictionAnalysis() {
|
|
|
1058
1355
|
assistantMessageCount: 0
|
|
1059
1356
|
};
|
|
1060
1357
|
}
|
|
1061
|
-
function applyActionMetadata(analysis, record, messages) {
|
|
1358
|
+
function applyActionMetadata(analysis, record, messages, toolCallsById) {
|
|
1062
1359
|
if (!record || typeof record !== "object") return;
|
|
1063
1360
|
const object = record;
|
|
1064
1361
|
const payload = firstObject(object.payload) ?? object;
|
|
@@ -1069,12 +1366,73 @@ function applyActionMetadata(analysis, record, messages) {
|
|
|
1069
1366
|
const text = JSON.stringify(record).toLowerCase();
|
|
1070
1367
|
const isTool = type?.includes("tool") || type?.includes("function_call") || text.includes('"tool_call"') || text.includes('"function_call"');
|
|
1071
1368
|
if (isTool) analysis.toolCallCount += 1;
|
|
1072
|
-
const isNonZeroCommand =
|
|
1073
|
-
if (isNonZeroCommand) analysis.commandIssues.push(classifyCommandIssue(record, analysis.commandIssues));
|
|
1369
|
+
const isNonZeroCommand = hasNonZeroCommandExit(record);
|
|
1370
|
+
if (isNonZeroCommand) analysis.commandIssues.push(classifyCommandIssue(record, analysis.commandIssues, commandTextForToolOutput(record, toolCallsById)));
|
|
1074
1371
|
if (/\b(exec_command|shell|bash|zsh|command)\b/.test(text)) analysis.shellCommandCount += 1;
|
|
1075
1372
|
if (/\b(read_file|view_image|cat |sed |rg |grep |open_file)\b/.test(text)) analysis.fileReadCount += 1;
|
|
1076
1373
|
if (/\b(apply_patch|write_file|edit|patch|created|deleted)\b/.test(text)) analysis.fileEditCount += 1;
|
|
1077
1374
|
}
|
|
1375
|
+
function indexToolCallsById(records) {
|
|
1376
|
+
const calls = /* @__PURE__ */ new Map();
|
|
1377
|
+
const sessionCommands = /* @__PURE__ */ new Map();
|
|
1378
|
+
for (const record of records) {
|
|
1379
|
+
if (!record || typeof record !== "object") continue;
|
|
1380
|
+
const object = record;
|
|
1381
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1382
|
+
const type = `${stringValue(object.type) ?? ""} ${stringValue(payload.type) ?? ""}`.toLowerCase();
|
|
1383
|
+
const callId = stringValue(payload.call_id ?? object.call_id ?? payload.callId ?? object.callId);
|
|
1384
|
+
if (type.includes("function_call_output") || type.includes("custom_tool_call_output") || type.includes("tool_result")) {
|
|
1385
|
+
const output = stringValue(payload.output ?? object.output);
|
|
1386
|
+
const sessionId = output?.match(/\bProcess running with session ID\s+(\d+)\b/i)?.[1];
|
|
1387
|
+
const command3 = callId ? calls.get(callId) : void 0;
|
|
1388
|
+
if (sessionId && command3) sessionCommands.set(sessionId, command3);
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (!type.includes("function_call") && !type.includes("custom_tool_call") && !type.includes("tool_call")) continue;
|
|
1392
|
+
const command2 = toolCallCommandText(payload, sessionCommands) ?? toolCallCommandText(object, sessionCommands);
|
|
1393
|
+
if (callId && command2) calls.set(callId, command2);
|
|
1394
|
+
}
|
|
1395
|
+
return calls;
|
|
1396
|
+
}
|
|
1397
|
+
function commandTextForToolOutput(record, toolCallsById) {
|
|
1398
|
+
if (!record || typeof record !== "object") return void 0;
|
|
1399
|
+
const object = record;
|
|
1400
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1401
|
+
const callId = stringValue(payload.call_id ?? object.call_id ?? payload.callId ?? object.callId);
|
|
1402
|
+
return callId ? toolCallsById.get(callId) : void 0;
|
|
1403
|
+
}
|
|
1404
|
+
function toolCallCommandText(object, sessionCommands) {
|
|
1405
|
+
const name = `${stringValue(object.name) ?? ""} ${stringValue(object.recipient_name) ?? ""}`.toLowerCase();
|
|
1406
|
+
const args2 = object.arguments ?? object.args ?? object.input;
|
|
1407
|
+
const parsedArgs = typeof args2 === "string" ? parseJsonObject(args2) : firstObject(args2);
|
|
1408
|
+
const sessionId = parsedArgs ? idValue(parsedArgs.session_id) : void 0;
|
|
1409
|
+
if (sessionId && sessionCommands.has(sessionId)) return sessionCommands.get(sessionId);
|
|
1410
|
+
const command2 = parsedArgs ? extractCommandText(parsedArgs) : void 0;
|
|
1411
|
+
if (command2) return command2;
|
|
1412
|
+
if ((name.includes("exec_command") || name.includes("shell")) && typeof args2 === "string" && args2.trim()) return args2;
|
|
1413
|
+
const toolName = stringValue(object.name) ?? stringValue(object.recipient_name);
|
|
1414
|
+
if (!toolName) return void 0;
|
|
1415
|
+
return sessionId ? `${toolName} session ${sessionId}` : toolName;
|
|
1416
|
+
}
|
|
1417
|
+
function hasNonZeroCommandExit(record) {
|
|
1418
|
+
if (!record || typeof record !== "object") return false;
|
|
1419
|
+
const object = record;
|
|
1420
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1421
|
+
const type = `${stringValue(object.type) ?? ""} ${stringValue(payload.type) ?? ""}`.toLowerCase();
|
|
1422
|
+
const isCommandRecord = /(function_call_output|custom_tool_call_output|tool_result|exec_command|shell|command)/.test(type) || Boolean(extractCommandText(payload) ?? extractCommandText(object));
|
|
1423
|
+
const explicitExitCode = exitCodeValue(payload) ?? exitCodeValue(object);
|
|
1424
|
+
if (explicitExitCode !== void 0) return isCommandRecord && explicitExitCode !== 0;
|
|
1425
|
+
const output = stringValue(payload.output) ?? stringValue(object.output);
|
|
1426
|
+
if (!output || !/(function_call_output|custom_tool_call_output|tool_result|exec_command)/.test(type)) return false;
|
|
1427
|
+
const processExitCode = output.match(/\bProcess exited with code\s+(-?\d+)\b/i)?.[1];
|
|
1428
|
+
return processExitCode !== void 0 && Number(processExitCode) !== 0;
|
|
1429
|
+
}
|
|
1430
|
+
function exitCodeValue(object) {
|
|
1431
|
+
const value = object.exit_code ?? object.exitCode;
|
|
1432
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1433
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number(value);
|
|
1434
|
+
return void 0;
|
|
1435
|
+
}
|
|
1078
1436
|
function finalizeCommandIssues(analysis, totalTokens) {
|
|
1079
1437
|
const repeatedCommands = /* @__PURE__ */ new Map();
|
|
1080
1438
|
const repeatableIssues = analysis.commandIssues.filter((issue2) => issue2.classification !== "harmless_nonzero" && issue2.classification !== "exploratory_miss");
|
|
@@ -1119,30 +1477,38 @@ function issuePriority(issue2) {
|
|
|
1119
1477
|
return severityScore * 10 + impactScore;
|
|
1120
1478
|
}
|
|
1121
1479
|
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
|
|
1480
|
+
if (issue2.severity === "critical") return "Repeated or blocking command failure that may have stopped progress.";
|
|
1481
|
+
if (issue2.severity === "warning") return "Non-zero command event classified as worth reviewing for this session.";
|
|
1124
1482
|
if (issue2.classification === "exploratory_miss") return "Likely exploratory command that found no result.";
|
|
1125
1483
|
if (issue2.classification === "harmless_nonzero") return "Likely harmless non-zero exit during normal shell exploration.";
|
|
1126
1484
|
return "Non-zero command event with limited local context.";
|
|
1127
1485
|
}
|
|
1128
|
-
function classifyCommandIssue(record, previousIssues) {
|
|
1486
|
+
function classifyCommandIssue(record, previousIssues, commandHint) {
|
|
1129
1487
|
const raw = JSON.stringify(record);
|
|
1130
1488
|
const text = raw.toLowerCase();
|
|
1131
|
-
const command2 = extractCommandText(record) ?? compactCommandText(raw);
|
|
1132
|
-
const
|
|
1489
|
+
const command2 = commandHint ?? extractCommandText(record) ?? compactCommandText(raw);
|
|
1490
|
+
const commandText = command2.toLowerCase();
|
|
1491
|
+
const classificationText = commandHint ? `${commandText} ${commandOutputText(record) ?? ""}` : text;
|
|
1492
|
+
const category = commandCategory(commandHint ? commandText : text);
|
|
1133
1493
|
const commandKey = normalizeCommand(command2);
|
|
1134
1494
|
const repeated = previousIssues.filter((issue2) => issue2.command === commandKey).length + 1 >= 3;
|
|
1135
|
-
if (isHarmlessNonZero(
|
|
1136
|
-
return { command: commandKey, category, classification: isExploratoryMiss(
|
|
1495
|
+
if (isHarmlessNonZero(classificationText)) {
|
|
1496
|
+
return { command: commandKey, category, classification: isExploratoryMiss(classificationText) ? "exploratory_miss" : "harmless_nonzero", severity: "ignored", impact: "low" };
|
|
1137
1497
|
}
|
|
1138
|
-
if (isImportantFailure(
|
|
1139
|
-
return { command: commandKey, category, classification: "blocking_failure", severity: repeated ? "critical" : importantSeverity(
|
|
1498
|
+
if (isImportantFailure(classificationText, category) || repeated) {
|
|
1499
|
+
return { command: commandKey, category, classification: "blocking_failure", severity: repeated ? "critical" : importantSeverity(classificationText, category), impact: repeated ? "high" : importantImpact(category) };
|
|
1140
1500
|
}
|
|
1141
1501
|
if (category === "search" || category === "filesystem") {
|
|
1142
1502
|
return { command: commandKey, category, classification: "exploratory_miss", severity: "ignored", impact: "low" };
|
|
1143
1503
|
}
|
|
1144
1504
|
return { command: commandKey, category, classification: "unknown", severity: "info", impact: "low" };
|
|
1145
1505
|
}
|
|
1506
|
+
function commandOutputText(record) {
|
|
1507
|
+
if (!record || typeof record !== "object") return void 0;
|
|
1508
|
+
const object = record;
|
|
1509
|
+
const payload = firstObject(object.payload) ?? object;
|
|
1510
|
+
return stringValue(payload.output ?? object.output)?.toLowerCase().slice(0, 2e3);
|
|
1511
|
+
}
|
|
1146
1512
|
function isHarmlessNonZero(text) {
|
|
1147
1513
|
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
1514
|
}
|
|
@@ -1222,9 +1588,21 @@ function compactCommandText(text) {
|
|
|
1222
1588
|
function firstObject(...values) {
|
|
1223
1589
|
return values.find((value) => Boolean(value) && typeof value === "object" && !Array.isArray(value));
|
|
1224
1590
|
}
|
|
1591
|
+
function parseJsonObject(value) {
|
|
1592
|
+
try {
|
|
1593
|
+
return firstObject(JSON.parse(value));
|
|
1594
|
+
} catch {
|
|
1595
|
+
return void 0;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1225
1598
|
function stringValue(value) {
|
|
1226
1599
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1227
1600
|
}
|
|
1601
|
+
function idValue(value) {
|
|
1602
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
1603
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1604
|
+
return void 0;
|
|
1605
|
+
}
|
|
1228
1606
|
function applyMessageCount(analysis, messages, role, payload, object) {
|
|
1229
1607
|
const message = extractMessageText(payload) ?? extractMessageText(object);
|
|
1230
1608
|
if (!message) return;
|
|
@@ -1468,6 +1846,9 @@ function scanCodex(options) {
|
|
|
1468
1846
|
const codexHome = options.codexHome ?? path5.join(os2.homedir(), ".codex");
|
|
1469
1847
|
const statePath = path5.join(codexHome, "state_5.sqlite");
|
|
1470
1848
|
const sessionsPath = path5.join(codexHome, "sessions");
|
|
1849
|
+
const configPath2 = path5.join(codexHome, "config.toml");
|
|
1850
|
+
const configExists = fs5.existsSync(configPath2);
|
|
1851
|
+
const serviceTier = readCodexServiceTier(configPath2);
|
|
1471
1852
|
const warnings = [];
|
|
1472
1853
|
const sessions = [];
|
|
1473
1854
|
const stateExists = fs5.existsSync(statePath);
|
|
@@ -1491,8 +1872,12 @@ function scanCodex(options) {
|
|
|
1491
1872
|
id: "codex",
|
|
1492
1873
|
label: "Codex",
|
|
1493
1874
|
available: stateExists || sessionsExists,
|
|
1494
|
-
paths: [statePath, sessionsPath],
|
|
1495
|
-
warnings
|
|
1875
|
+
paths: configExists ? [statePath, sessionsPath, configPath2] : [statePath, sessionsPath],
|
|
1876
|
+
warnings,
|
|
1877
|
+
serviceTier,
|
|
1878
|
+
serviceTierSource: serviceTier ? "current_config" : void 0,
|
|
1879
|
+
serviceTierConfidence: serviceTier ? "medium" : void 0,
|
|
1880
|
+
serviceTierDetail: serviceTier ? `Current Codex config service_tier: ${serviceTier}` : void 0
|
|
1496
1881
|
},
|
|
1497
1882
|
sessions,
|
|
1498
1883
|
stats: {
|
|
@@ -1508,10 +1893,23 @@ function scanCodex(options) {
|
|
|
1508
1893
|
sessionsImported: sessions.length,
|
|
1509
1894
|
parseFailureCount: sessions.filter((session) => session.parseStatus === "failed").length,
|
|
1510
1895
|
unreadableFileCount: sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length,
|
|
1896
|
+
serviceTier,
|
|
1897
|
+
serviceTierSource: serviceTier ? "current_config" : void 0,
|
|
1898
|
+
serviceTierConfidence: serviceTier ? "medium" : void 0,
|
|
1899
|
+
serviceTierDetail: serviceTier ? `Current Codex config service_tier: ${serviceTier}` : void 0,
|
|
1511
1900
|
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1512
1901
|
}
|
|
1513
1902
|
};
|
|
1514
1903
|
}
|
|
1904
|
+
function readCodexServiceTier(configPath2) {
|
|
1905
|
+
try {
|
|
1906
|
+
const text = fs5.readFileSync(configPath2, "utf8");
|
|
1907
|
+
const match = text.match(/^\s*service_tier\s*=\s*["']?([^"'\s#]+)["']?/m);
|
|
1908
|
+
return match?.[1]?.trim().toLowerCase() || void 0;
|
|
1909
|
+
} catch {
|
|
1910
|
+
return void 0;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1515
1913
|
function readThreads(statePath, sessionFiles, options, warnings) {
|
|
1516
1914
|
let db;
|
|
1517
1915
|
try {
|
|
@@ -2280,6 +2678,7 @@ function scanClaude(options) {
|
|
|
2280
2678
|
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
2679
|
const usageSelection = selectClaudeUsageOccurrences(orderedFiles.map(({ file }) => file));
|
|
2282
2680
|
const sessions = orderedFiles.flatMap(({ file }, index) => claudeFileToUsages(file, index, options, usageSelection));
|
|
2681
|
+
const serviceTierSummary = summarizeServiceTiers(sessions.map((session) => session.serviceTier), "Claude session usage");
|
|
2283
2682
|
const historyEntryCount = countHistoryEntries(historyPath);
|
|
2284
2683
|
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
2285
2684
|
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
|
|
@@ -2294,7 +2693,8 @@ function scanClaude(options) {
|
|
|
2294
2693
|
label: "Claude Code",
|
|
2295
2694
|
available: discovery.files.length > 0 || historyExists,
|
|
2296
2695
|
paths: unique3([...discovery.projectsPaths, historyPath, ...discovery.desktopSessionRoots]),
|
|
2297
|
-
warnings
|
|
2696
|
+
warnings,
|
|
2697
|
+
...serviceTierSummary
|
|
2298
2698
|
},
|
|
2299
2699
|
sessions,
|
|
2300
2700
|
stats: {
|
|
@@ -2313,6 +2713,7 @@ function scanClaude(options) {
|
|
|
2313
2713
|
unreadableFileCount,
|
|
2314
2714
|
malformedFileCount,
|
|
2315
2715
|
historyEntryCount,
|
|
2716
|
+
...serviceTierSummary,
|
|
2316
2717
|
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2317
2718
|
}
|
|
2318
2719
|
};
|
|
@@ -2398,7 +2799,10 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
|
|
|
2398
2799
|
const warnings = [...repo.warnings, ...breakdown.warnings];
|
|
2399
2800
|
const entrypoint = breakdown.entrypoint ?? inferClaudeEntrypointFromPath(file.filePath);
|
|
2400
2801
|
if (!breakdown.cwd) warnings.push("repo_inferred_from_claude_project_dir");
|
|
2802
|
+
if (!breakdown.model && breakdown.usageCount === 0 && breakdown.syntheticZeroUsageCount > 0) warnings.push("claude_synthetic_zero_usage");
|
|
2401
2803
|
const hasTokenBreakdown = breakdown.totalTokens > 0 && breakdown.usageCount > 0;
|
|
2804
|
+
const serviceTierSummary = summarizeClaudeServiceTier(breakdown.serviceTiers, breakdown.serviceSpeeds);
|
|
2805
|
+
const serviceSpeedSummary = summarizeServiceTiers(breakdown.serviceSpeeds, "Claude usage.speed");
|
|
2402
2806
|
const usage = {
|
|
2403
2807
|
id: `${id}${idSuffix}`,
|
|
2404
2808
|
sourceClient: "claude",
|
|
@@ -2418,6 +2822,8 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
|
|
|
2418
2822
|
inputTokens: breakdown.inputTokens,
|
|
2419
2823
|
cachedInputTokens: breakdown.cachedInputTokens,
|
|
2420
2824
|
cacheCreationInputTokens: breakdown.cacheCreationInputTokens,
|
|
2825
|
+
cacheCreationInputTokens5m: breakdown.cacheCreationInputTokens5m,
|
|
2826
|
+
cacheCreationInputTokens1h: breakdown.cacheCreationInputTokens1h,
|
|
2421
2827
|
outputTokens: breakdown.outputTokens,
|
|
2422
2828
|
reasoningTokens: 0,
|
|
2423
2829
|
reasoningOutputTokens: 0,
|
|
@@ -2436,6 +2842,20 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
|
|
|
2436
2842
|
detectedSurface: claudeSurface(entrypoint),
|
|
2437
2843
|
surfaceConfidence: "medium",
|
|
2438
2844
|
surfaceReason: entrypoint ? `Claude Code entrypoint: ${entrypoint}` : "Claude Code transcript",
|
|
2845
|
+
serviceTier: serviceTierSummary.serviceTier,
|
|
2846
|
+
serviceTierSource: serviceTierSummary.serviceTier ? "session_usage" : void 0,
|
|
2847
|
+
serviceTierConfidence: serviceTierSummary.serviceTier ? "high" : void 0,
|
|
2848
|
+
serviceTierDetail: serviceTierSummary.serviceTierDetail,
|
|
2849
|
+
sourceMetadata: breakdown.serviceTiers.length || breakdown.serviceSpeeds.length || breakdown.cacheCreationInputTokens5m > 0 || breakdown.cacheCreationInputTokens1h > 0 ? {
|
|
2850
|
+
claude: {
|
|
2851
|
+
serviceTiers: breakdown.serviceTiers,
|
|
2852
|
+
speeds: breakdown.serviceSpeeds,
|
|
2853
|
+
serviceTier: serviceTierSummary.serviceTier,
|
|
2854
|
+
speed: serviceSpeedSummary.serviceTier,
|
|
2855
|
+
cacheCreationInputTokens5m: breakdown.cacheCreationInputTokens5m,
|
|
2856
|
+
cacheCreationInputTokens1h: breakdown.cacheCreationInputTokens1h
|
|
2857
|
+
}
|
|
2858
|
+
} : void 0,
|
|
2439
2859
|
userPromptCount: breakdown.userPromptCount,
|
|
2440
2860
|
assistantMessageCount: breakdown.assistantMessageCount,
|
|
2441
2861
|
toolCallCount: breakdown.toolCallCount,
|
|
@@ -2506,6 +2926,8 @@ function applyClaudeRecord(breakdown, record, usageSelection, usageOccurrenceKey
|
|
|
2506
2926
|
applyPromptTimeline2(breakdown, role, message.content, timestamp3);
|
|
2507
2927
|
const usage = firstObject4(message.usage);
|
|
2508
2928
|
const usageTokens = usage ? usageTotal(usage) : 0;
|
|
2929
|
+
const rawModel = stringValue3(message.model);
|
|
2930
|
+
if (rawModel === "<synthetic>" && usage && usageTokens === 0) breakdown.syntheticZeroUsageCount += 1;
|
|
2509
2931
|
const model = normalizeClaudeModel(message.model);
|
|
2510
2932
|
if (model && (usageTokens > 0 || !breakdown.model)) breakdown.model = model;
|
|
2511
2933
|
const selectedUsage = usageTokens > 0 ? usageSelection.usageByOccurrence.get(usageOccurrenceKey) : void 0;
|
|
@@ -2560,29 +2982,52 @@ function occurrenceKey(filePath, recordIndex) {
|
|
|
2560
2982
|
return `${path6.resolve(filePath)}:${recordIndex}`;
|
|
2561
2983
|
}
|
|
2562
2984
|
function cloneUsage(usage) {
|
|
2985
|
+
const cacheCreation = firstObject4(usage.cache_creation);
|
|
2563
2986
|
return {
|
|
2564
2987
|
input_tokens: numberValue(usage.input_tokens),
|
|
2565
2988
|
cache_read_input_tokens: numberValue(usage.cache_read_input_tokens),
|
|
2566
2989
|
cache_creation_input_tokens: numberValue(usage.cache_creation_input_tokens),
|
|
2567
|
-
|
|
2990
|
+
cache_creation: cacheCreation ? {
|
|
2991
|
+
ephemeral_5m_input_tokens: numberValue(cacheCreation.ephemeral_5m_input_tokens),
|
|
2992
|
+
ephemeral_1h_input_tokens: numberValue(cacheCreation.ephemeral_1h_input_tokens)
|
|
2993
|
+
} : void 0,
|
|
2994
|
+
output_tokens: numberValue(usage.output_tokens),
|
|
2995
|
+
service_tier: stringValue3(usage.service_tier),
|
|
2996
|
+
speed: stringValue3(usage.speed)
|
|
2568
2997
|
};
|
|
2569
2998
|
}
|
|
2570
2999
|
function mergeUsageMax(target, usage) {
|
|
3000
|
+
const targetCacheCreation = firstObject4(target.cache_creation) ?? {};
|
|
3001
|
+
const usageCacheCreation = firstObject4(usage.cache_creation) ?? {};
|
|
2571
3002
|
target.input_tokens = Math.max(numberValue(target.input_tokens), numberValue(usage.input_tokens));
|
|
2572
3003
|
target.cache_read_input_tokens = Math.max(numberValue(target.cache_read_input_tokens), numberValue(usage.cache_read_input_tokens));
|
|
2573
3004
|
target.cache_creation_input_tokens = Math.max(numberValue(target.cache_creation_input_tokens), numberValue(usage.cache_creation_input_tokens));
|
|
3005
|
+
target.cache_creation = {
|
|
3006
|
+
ephemeral_5m_input_tokens: Math.max(numberValue(targetCacheCreation.ephemeral_5m_input_tokens), numberValue(usageCacheCreation.ephemeral_5m_input_tokens)),
|
|
3007
|
+
ephemeral_1h_input_tokens: Math.max(numberValue(targetCacheCreation.ephemeral_1h_input_tokens), numberValue(usageCacheCreation.ephemeral_1h_input_tokens))
|
|
3008
|
+
};
|
|
2574
3009
|
target.output_tokens = Math.max(numberValue(target.output_tokens), numberValue(usage.output_tokens));
|
|
3010
|
+
target.service_tier ??= stringValue3(usage.service_tier);
|
|
3011
|
+
target.speed ??= stringValue3(usage.speed);
|
|
2575
3012
|
}
|
|
2576
3013
|
function applyUsage(breakdown, usage) {
|
|
2577
3014
|
const input = numberValue(usage.input_tokens);
|
|
2578
3015
|
const cacheRead = numberValue(usage.cache_read_input_tokens);
|
|
2579
|
-
const
|
|
3016
|
+
const cacheCreationBreakdown = firstObject4(usage.cache_creation);
|
|
3017
|
+
const cacheCreation5m = numberValue(cacheCreationBreakdown?.ephemeral_5m_input_tokens);
|
|
3018
|
+
const cacheCreation1h = numberValue(cacheCreationBreakdown?.ephemeral_1h_input_tokens);
|
|
3019
|
+
const splitCacheCreation = cacheCreation5m + cacheCreation1h;
|
|
3020
|
+
const cacheCreation = Math.max(numberValue(usage.cache_creation_input_tokens), splitCacheCreation);
|
|
2580
3021
|
const output = numberValue(usage.output_tokens);
|
|
2581
3022
|
breakdown.inputTokens += input + cacheRead + cacheCreation;
|
|
2582
3023
|
breakdown.cachedInputTokens += cacheRead;
|
|
2583
3024
|
breakdown.cacheCreationInputTokens += cacheCreation;
|
|
3025
|
+
breakdown.cacheCreationInputTokens5m += cacheCreation5m;
|
|
3026
|
+
breakdown.cacheCreationInputTokens1h += cacheCreation1h;
|
|
2584
3027
|
breakdown.outputTokens += output;
|
|
2585
3028
|
breakdown.usageCount += 1;
|
|
3029
|
+
pushUnique(breakdown.serviceTiers, normalizedServiceTier(usage.service_tier));
|
|
3030
|
+
pushUnique(breakdown.serviceSpeeds, normalizedServiceTier(usage.speed));
|
|
2586
3031
|
}
|
|
2587
3032
|
function segmentClaudeBreakdownByDayAndModel(breakdown) {
|
|
2588
3033
|
const events = breakdown.usageEvents.filter((event) => event.timestamp);
|
|
@@ -2633,7 +3078,8 @@ function dateKey2(timestamp3) {
|
|
|
2633
3078
|
return timestamp3?.slice(0, 10) ?? "unknown-date";
|
|
2634
3079
|
}
|
|
2635
3080
|
function usageTotal(usage) {
|
|
2636
|
-
|
|
3081
|
+
const cacheCreation = firstObject4(usage.cache_creation);
|
|
3082
|
+
return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + Math.max(numberValue(usage.cache_creation_input_tokens), numberValue(cacheCreation?.ephemeral_5m_input_tokens) + numberValue(cacheCreation?.ephemeral_1h_input_tokens)) + numberValue(usage.output_tokens);
|
|
2637
3083
|
}
|
|
2638
3084
|
function applyContentSignals(breakdown, content) {
|
|
2639
3085
|
if (!Array.isArray(content)) return;
|
|
@@ -2676,6 +3122,8 @@ function emptyBreakdown2() {
|
|
|
2676
3122
|
inputTokens: 0,
|
|
2677
3123
|
cachedInputTokens: 0,
|
|
2678
3124
|
cacheCreationInputTokens: 0,
|
|
3125
|
+
cacheCreationInputTokens5m: 0,
|
|
3126
|
+
cacheCreationInputTokens1h: 0,
|
|
2679
3127
|
outputTokens: 0,
|
|
2680
3128
|
totalTokens: 0,
|
|
2681
3129
|
usageCount: 0,
|
|
@@ -2692,9 +3140,46 @@ function emptyBreakdown2() {
|
|
|
2692
3140
|
parseErrors: [],
|
|
2693
3141
|
promptTimeline: [],
|
|
2694
3142
|
warnings: [],
|
|
2695
|
-
usageEvents: []
|
|
3143
|
+
usageEvents: [],
|
|
3144
|
+
serviceTiers: [],
|
|
3145
|
+
serviceSpeeds: [],
|
|
3146
|
+
syntheticZeroUsageCount: 0
|
|
2696
3147
|
};
|
|
2697
3148
|
}
|
|
3149
|
+
function summarizeClaudeServiceTier(serviceTiers, speeds) {
|
|
3150
|
+
const tierSummary = summarizeServiceTiers(serviceTiers, "Claude usage.service_tier");
|
|
3151
|
+
return tierSummary.serviceTier ? tierSummary : summarizeServiceTiers(speeds, "Claude usage.speed");
|
|
3152
|
+
}
|
|
3153
|
+
function summarizeServiceTiers(values, source) {
|
|
3154
|
+
const uniqueValues = unique3(values.filter((value) => Boolean(value)));
|
|
3155
|
+
if (uniqueValues.length === 0) return {};
|
|
3156
|
+
if (uniqueValues.length === 1) {
|
|
3157
|
+
const serviceTier = uniqueValues[0];
|
|
3158
|
+
if (!serviceTier) return {};
|
|
3159
|
+
return {
|
|
3160
|
+
serviceTier,
|
|
3161
|
+
serviceTierSource: "session_usage",
|
|
3162
|
+
serviceTierConfidence: "high",
|
|
3163
|
+
serviceTierDetail: serviceTierDetail(source, uniqueValues)
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
return {
|
|
3167
|
+
serviceTier: "mixed",
|
|
3168
|
+
serviceTierSource: "session_usage",
|
|
3169
|
+
serviceTierConfidence: "high",
|
|
3170
|
+
serviceTierDetail: serviceTierDetail(source, uniqueValues)
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
function serviceTierDetail(source, values) {
|
|
3174
|
+
return `${source}: ${unique3(values).join(", ")}`;
|
|
3175
|
+
}
|
|
3176
|
+
function normalizedServiceTier(value) {
|
|
3177
|
+
const tier = stringValue3(value)?.trim().toLowerCase();
|
|
3178
|
+
return tier || void 0;
|
|
3179
|
+
}
|
|
3180
|
+
function pushUnique(target, value) {
|
|
3181
|
+
if (value && !target.includes(value)) target.push(value);
|
|
3182
|
+
}
|
|
2698
3183
|
function applyPromptTimeline2(breakdown, role, content, timestamp3) {
|
|
2699
3184
|
if (breakdown.promptTimeline.length >= 80 || role !== "user" && role !== "assistant") return;
|
|
2700
3185
|
const text = extractClaudeMessageText(content);
|
|
@@ -2842,58 +3327,59 @@ function safeMtimeMs(filePath) {
|
|
|
2842
3327
|
}
|
|
2843
3328
|
}
|
|
2844
3329
|
|
|
2845
|
-
// packages/core/src/sources/
|
|
3330
|
+
// packages/core/src/sources/copilot.ts
|
|
2846
3331
|
import fs7 from "node:fs";
|
|
2847
3332
|
import os4 from "node:os";
|
|
2848
3333
|
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}`);
|
|
3334
|
+
var maxJsonlBytes = 50 * 1024 * 1024;
|
|
3335
|
+
var copilotOtelEnv = "COPILOT_OTEL_FILE_EXPORTER_PATH";
|
|
3336
|
+
function scanCopilot(options) {
|
|
3337
|
+
const copilotHome = options.copilotHome ?? path7.join(os4.homedir(), ".copilot");
|
|
3338
|
+
const discovery = discoverCopilotFiles(copilotHome, Boolean(options.copilotHome), options.codeUserRoots);
|
|
3339
|
+
const files = discovery.files.filter((file) => fileMayOverlapScanWindow2(file.filePath, options.scanWindow));
|
|
3340
|
+
const sessionMap = /* @__PURE__ */ new Map();
|
|
3341
|
+
const fileStats = [];
|
|
3342
|
+
for (const [index, file] of files.entries()) {
|
|
3343
|
+
const read = readCopilotFile(file, index);
|
|
3344
|
+
fileStats.push(read.stats);
|
|
3345
|
+
for (const breakdown of read.breakdowns) {
|
|
3346
|
+
mergeBreakdown(sessionMap, breakdown);
|
|
3347
|
+
}
|
|
2872
3348
|
}
|
|
3349
|
+
applyModelHints(sessionMap, discovery.modelHints);
|
|
3350
|
+
const sessions = [...sessionMap.values()].sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? "")).map((breakdown, index) => breakdownToUsage(breakdown, index, options));
|
|
3351
|
+
const parseFailureCount = fileStats.filter((stats) => stats.parseFailed).length;
|
|
3352
|
+
const unreadableFileCount = fileStats.filter((stats) => stats.unreadable).length;
|
|
3353
|
+
const malformedFileCount = fileStats.filter((stats) => stats.malformed).length;
|
|
3354
|
+
if (!fs7.existsSync(copilotHome)) discovery.warnings.push(`GitHub Copilot home directory not found at ${copilotHome}`);
|
|
3355
|
+
if (!fs7.existsSync(discovery.otelPath)) discovery.warnings.push(`GitHub Copilot OpenTelemetry directory not found at ${discovery.otelPath}`);
|
|
3356
|
+
const otelFiles = files.filter((file) => file.kind === "otel").length;
|
|
3357
|
+
const debugLogFiles = files.filter((file) => file.kind === "debug").length;
|
|
3358
|
+
const transcriptFiles = files.filter((file) => file.kind === "transcript").length;
|
|
3359
|
+
const sessionStateFiles = files.filter((file) => file.kind === "session_state").length;
|
|
2873
3360
|
return {
|
|
2874
3361
|
source: {
|
|
2875
|
-
id: "
|
|
2876
|
-
label: "
|
|
2877
|
-
available:
|
|
2878
|
-
paths: unique4([
|
|
3362
|
+
id: "copilot",
|
|
3363
|
+
label: "GitHub Copilot",
|
|
3364
|
+
available: files.length > 0,
|
|
3365
|
+
paths: unique4([copilotHome, discovery.otelPath, discovery.workspaceStoragePath, ...discovery.codeUserRoots, ...files.map((file) => file.filePath), ...[...discovery.modelHints.values()].map((hint) => hint.sourcePath)]),
|
|
2879
3366
|
warnings: discovery.warnings
|
|
2880
3367
|
},
|
|
2881
3368
|
sessions,
|
|
2882
3369
|
stats: {
|
|
2883
|
-
sourceId: "
|
|
2884
|
-
sourceLabel: "
|
|
2885
|
-
homePath:
|
|
2886
|
-
|
|
2887
|
-
|
|
3370
|
+
sourceId: "copilot",
|
|
3371
|
+
sourceLabel: "GitHub Copilot",
|
|
3372
|
+
homePath: copilotHome,
|
|
3373
|
+
copilotHome,
|
|
3374
|
+
otelPath: discovery.otelPath,
|
|
2888
3375
|
workspaceStoragePath: discovery.workspaceStoragePath,
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
skippedFileCount: discovery.skippedFileCount,
|
|
3376
|
+
otelExists: fs7.existsSync(discovery.otelPath),
|
|
3377
|
+
sessionsExists: files.length > 0,
|
|
3378
|
+
sessionFileCount: files.length,
|
|
3379
|
+
otelFileCount: otelFiles,
|
|
3380
|
+
debugLogFileCount: debugLogFiles,
|
|
3381
|
+
transcriptFileCount: transcriptFiles,
|
|
3382
|
+
sessionStateFileCount: sessionStateFiles,
|
|
2897
3383
|
sessionsImported: sessions.length,
|
|
2898
3384
|
parseFailureCount,
|
|
2899
3385
|
unreadableFileCount,
|
|
@@ -2902,154 +3388,962 @@ function scanCursor(options) {
|
|
|
2902
3388
|
}
|
|
2903
3389
|
};
|
|
2904
3390
|
}
|
|
2905
|
-
function
|
|
3391
|
+
function discoverCopilotFiles(copilotHome, explicitHome, explicitCodeUserRoots) {
|
|
2906
3392
|
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
|
-
}
|
|
3393
|
+
const otelPath = path7.join(copilotHome, "otel");
|
|
3394
|
+
const codeUserRoots = explicitCodeUserRoots ?? (explicitHome ? [] : defaultCodeUserRoots());
|
|
3395
|
+
const workspaceStoragePath = codeUserRoots[0] ? path7.join(codeUserRoots[0], "workspaceStorage") : "";
|
|
3396
|
+
const files = [];
|
|
3397
|
+
const modelHints = /* @__PURE__ */ new Map();
|
|
3398
|
+
collectJsonlFiles(otelPath, "otel", "cli", void 0, files, warnings);
|
|
3399
|
+
collectCopilotSessionStateFiles(copilotHome, files, warnings);
|
|
3400
|
+
const explicitOtelPath = process.env[copilotOtelEnv]?.trim();
|
|
3401
|
+
if (explicitOtelPath && isReadableRegularFile(explicitOtelPath, maxJsonlBytes)) {
|
|
3402
|
+
files.push({ filePath: path7.resolve(explicitOtelPath), kind: "otel", surface: "cli", fallbackCwd: void 0 });
|
|
3403
|
+
}
|
|
3404
|
+
for (const userRoot of codeUserRoots) {
|
|
3405
|
+
collectCodeCopilotFiles(userRoot, files, modelHints, warnings);
|
|
2934
3406
|
}
|
|
2935
3407
|
return {
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
jsonlFiles: unique4(jsonlFiles).sort(cursorJsonlSort),
|
|
2942
|
-
databaseFiles: unique4(databaseFiles).sort(),
|
|
2943
|
-
skippedFileCount,
|
|
3408
|
+
otelPath,
|
|
3409
|
+
workspaceStoragePath,
|
|
3410
|
+
files: uniqueFiles(files),
|
|
3411
|
+
codeUserRoots,
|
|
3412
|
+
modelHints,
|
|
2944
3413
|
warnings
|
|
2945
3414
|
};
|
|
2946
3415
|
}
|
|
2947
|
-
function
|
|
2948
|
-
|
|
3416
|
+
function collectCopilotSessionStateFiles(copilotHome, files, warnings) {
|
|
3417
|
+
const sessionStateRoot = path7.join(copilotHome, "session-state");
|
|
3418
|
+
if (!fs7.existsSync(sessionStateRoot)) return;
|
|
3419
|
+
try {
|
|
3420
|
+
for (const entry of fs7.readdirSync(sessionStateRoot, { withFileTypes: true })) {
|
|
3421
|
+
if (!entry.isDirectory()) continue;
|
|
3422
|
+
const sessionDir = path7.join(sessionStateRoot, entry.name);
|
|
3423
|
+
const eventsPath = path7.join(sessionDir, "events.jsonl");
|
|
3424
|
+
if (isReadableRegularFile(eventsPath, maxJsonlBytes)) {
|
|
3425
|
+
files.push({ filePath: eventsPath, kind: "session_state", surface: "cli", fallbackCwd: cwdFromWorkspaceYaml(path7.join(sessionDir, "workspace.yaml")) });
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
} catch (error) {
|
|
3429
|
+
warnings.push(`Unable to read GitHub Copilot session state at ${sessionStateRoot}: ${errorMessage3(error)}`);
|
|
3430
|
+
}
|
|
2949
3431
|
}
|
|
2950
|
-
function
|
|
2951
|
-
|
|
2952
|
-
|
|
3432
|
+
function collectCodeCopilotFiles(userRoot, files, modelHints, warnings) {
|
|
3433
|
+
const workspaceStorage = path7.join(userRoot, "workspaceStorage");
|
|
3434
|
+
if (!fs7.existsSync(workspaceStorage)) return;
|
|
2953
3435
|
try {
|
|
2954
|
-
for (const workspace of fs7.readdirSync(
|
|
3436
|
+
for (const workspace of fs7.readdirSync(workspaceStorage, { withFileTypes: true })) {
|
|
2955
3437
|
if (!workspace.isDirectory()) continue;
|
|
2956
|
-
|
|
3438
|
+
const workspaceRoot = path7.join(workspaceStorage, workspace.name);
|
|
3439
|
+
const copilotRoot = path7.join(workspaceRoot, "GitHub.copilot-chat");
|
|
3440
|
+
if (!fs7.existsSync(copilotRoot)) continue;
|
|
3441
|
+
const fallbackCwd = workspaceFolderFromStorageJson(path7.join(workspaceRoot, "workspace.json"));
|
|
3442
|
+
collectJsonlFiles(path7.join(copilotRoot, "debug-logs"), "debug", "vscode", fallbackCwd, files, warnings);
|
|
3443
|
+
collectJsonlFiles(path7.join(copilotRoot, "transcripts"), "transcript", "vscode", fallbackCwd, files, warnings);
|
|
3444
|
+
collectCodeChatSessionModelHints(path7.join(workspaceRoot, "chatSessions"), modelHints, warnings);
|
|
2957
3445
|
}
|
|
2958
|
-
} catch {
|
|
2959
|
-
|
|
3446
|
+
} catch (error) {
|
|
3447
|
+
warnings.push(`Unable to read GitHub Copilot VS Code storage at ${workspaceStorage}: ${errorMessage3(error)}`);
|
|
2960
3448
|
}
|
|
2961
|
-
return roots;
|
|
2962
3449
|
}
|
|
2963
|
-
function
|
|
2964
|
-
if (!fs7.existsSync(root)) return
|
|
3450
|
+
function collectCodeChatSessionModelHints(root, modelHints, warnings) {
|
|
3451
|
+
if (!fs7.existsSync(root)) return;
|
|
2965
3452
|
try {
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3453
|
+
walk3(root, 2, (filePath) => {
|
|
3454
|
+
if (!filePath.endsWith(".jsonl") || !isReadableRegularFile(filePath, maxJsonlBytes)) return;
|
|
3455
|
+
const hint = readCodeChatSessionModelHint(filePath);
|
|
3456
|
+
if (hint) modelHints.set(hint.sessionId, { model: hint.model, sourcePath: filePath });
|
|
3457
|
+
});
|
|
3458
|
+
} catch (error) {
|
|
3459
|
+
warnings.push(`Unable to read VS Code chat session metadata at ${root}: ${errorMessage3(error)}`);
|
|
2969
3460
|
}
|
|
2970
3461
|
}
|
|
2971
|
-
function
|
|
2972
|
-
const
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3462
|
+
function readCodeChatSessionModelHint(filePath) {
|
|
3463
|
+
const fallbackSessionId = path7.basename(filePath).replace(/\.jsonl$/i, "");
|
|
3464
|
+
let sessionId = fallbackSessionId;
|
|
3465
|
+
let model;
|
|
3466
|
+
try {
|
|
3467
|
+
for (const record of parseJsonLines3(fs7.readFileSync(filePath, "utf8")).records) {
|
|
3468
|
+
const body = firstObject5(record.v) ?? firstObject5(record.data) ?? record;
|
|
3469
|
+
const metadata = firstObject5(body.metadata);
|
|
3470
|
+
sessionId = stringValue4(body.sessionId) ?? stringValue4(metadata?.sessionId) ?? sessionId;
|
|
3471
|
+
model = chatSessionSelectedModel(body) ?? chatSessionResolvedModel(body) ?? model;
|
|
3472
|
+
}
|
|
3473
|
+
} catch {
|
|
3474
|
+
return void 0;
|
|
2981
3475
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3476
|
+
return sessionId && model ? { sessionId, model } : void 0;
|
|
3477
|
+
}
|
|
3478
|
+
function chatSessionSelectedModel(body) {
|
|
3479
|
+
const inputState = firstObject5(body.inputState);
|
|
3480
|
+
const selectedModel = firstObject5(inputState?.selectedModel) ?? firstObject5(body.selectedModel);
|
|
3481
|
+
const metadata = firstObject5(selectedModel?.metadata);
|
|
3482
|
+
return firstModelCandidate([
|
|
3483
|
+
stringValue4(metadata?.version),
|
|
3484
|
+
stringValue4(metadata?.family),
|
|
3485
|
+
stringValue4(selectedModel?.id),
|
|
3486
|
+
stringValue4(selectedModel?.identifier)
|
|
3487
|
+
]);
|
|
3488
|
+
}
|
|
3489
|
+
function chatSessionResolvedModel(body) {
|
|
3490
|
+
const metadata = firstObject5(body.metadata);
|
|
3491
|
+
const response = firstObject5(body.response);
|
|
3492
|
+
const responseMetadata = firstObject5(response?.metadata);
|
|
3493
|
+
return firstModelCandidate([
|
|
3494
|
+
stringValue4(metadata?.resolvedModel),
|
|
3495
|
+
stringValue4(responseMetadata?.resolvedModel)
|
|
3496
|
+
]);
|
|
3497
|
+
}
|
|
3498
|
+
function firstModelCandidate(values) {
|
|
3499
|
+
for (const value of values) {
|
|
3500
|
+
const normalized = value?.replace(/^github[-_]?copilot\//i, "").replace(/^copilot\//i, "").trim();
|
|
3501
|
+
if (normalized && normalized.toLowerCase() !== "auto") return normalized;
|
|
2989
3502
|
}
|
|
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
|
-
};
|
|
3503
|
+
return void 0;
|
|
2997
3504
|
}
|
|
2998
|
-
function
|
|
2999
|
-
if (
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
}
|
|
3505
|
+
function collectJsonlFiles(root, kind, surface, fallbackCwd, files, warnings) {
|
|
3506
|
+
if (!fs7.existsSync(root)) return;
|
|
3507
|
+
try {
|
|
3508
|
+
walk3(root, 6, (filePath) => {
|
|
3509
|
+
if (filePath.endsWith(".jsonl") && isReadableRegularFile(filePath, maxJsonlBytes)) {
|
|
3510
|
+
files.push({ filePath, kind, surface, fallbackCwd });
|
|
3511
|
+
}
|
|
3512
|
+
});
|
|
3513
|
+
} catch (error) {
|
|
3514
|
+
warnings.push(`Unable to read GitHub Copilot path ${root}: ${errorMessage3(error)}`);
|
|
3008
3515
|
}
|
|
3009
3516
|
}
|
|
3010
|
-
function
|
|
3517
|
+
function readCopilotFile(file, index) {
|
|
3518
|
+
const fallbackId = path7.basename(file.filePath).replace(/\.jsonl$/i, "") || `copilot-session-${index}`;
|
|
3519
|
+
const fallbackTimestamp = fileModifiedDate(file.filePath);
|
|
3520
|
+
const fallback = emptyBreakdown3(file, fallbackId);
|
|
3011
3521
|
try {
|
|
3012
|
-
fs7.
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3522
|
+
const parse = parseJsonLines3(fs7.readFileSync(file.filePath, "utf8"));
|
|
3523
|
+
const traceContexts = collectTraceContexts(parse.records);
|
|
3524
|
+
const candidates = parse.records.map((record, recordIndex) => usageCandidateFromRecord(record, recordIndex, fallbackTimestamp, traceContexts)).filter((candidate) => Boolean(candidate));
|
|
3525
|
+
const selectedCandidates = uniqueCandidates(candidates.filter((candidate) => shouldEmitCandidate(candidate, candidates)));
|
|
3526
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3527
|
+
fallback.rawEventCount = parse.records.length;
|
|
3528
|
+
fallback.parseErrors = parse.errors;
|
|
3529
|
+
fallback.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
3530
|
+
if (file.kind === "transcript" || file.kind === "session_state") {
|
|
3531
|
+
for (const record of parse.records) applyTranscriptRecord(fallback, record, file, file.kind === "transcript" ? fallbackTimestamp : void 0);
|
|
3532
|
+
}
|
|
3533
|
+
for (const candidate of selectedCandidates) {
|
|
3534
|
+
const group = groups.get(candidate.sessionId) ?? emptyBreakdown3(file, candidate.sessionId);
|
|
3535
|
+
group.rawEventCount = parse.records.length;
|
|
3536
|
+
group.parseErrors = parse.errors;
|
|
3537
|
+
group.parseStatus = parse.errors.length ? "partial" : "ok";
|
|
3538
|
+
applyCandidate(group, candidate);
|
|
3539
|
+
groups.set(candidate.sessionId, group);
|
|
3540
|
+
}
|
|
3541
|
+
const stats = statsFromParseErrors(parse.errors);
|
|
3542
|
+
if (groups.size > 0) {
|
|
3543
|
+
return { breakdowns: [...groups.values()].map((group) => mergeActivity(group, fallback)), stats };
|
|
3544
|
+
}
|
|
3545
|
+
if (fallback.messageCount > 0 || fallback.rawEventCount > 0 || fallback.parseErrors.length > 0) return { breakdowns: [fallbackWithMissingTokenWarning(fallback)], stats };
|
|
3546
|
+
return { breakdowns: [], stats };
|
|
3547
|
+
} catch (error) {
|
|
3548
|
+
const breakdown = emptyBreakdown3(file, fallbackId);
|
|
3549
|
+
const message = `unable_to_read_copilot_file:${errorMessage3(error)}`;
|
|
3550
|
+
breakdown.parseStatus = "failed";
|
|
3551
|
+
breakdown.parseErrors.push(message);
|
|
3552
|
+
breakdown.warnings.push(message);
|
|
3553
|
+
return { breakdowns: [breakdown], stats: { parseFailed: true, unreadable: true, malformed: false } };
|
|
3017
3554
|
}
|
|
3018
3555
|
}
|
|
3019
|
-
function
|
|
3020
|
-
|
|
3021
|
-
const bTranscript = b.includes(`${path7.sep}agent-transcripts${path7.sep}`) ? 0 : 1;
|
|
3022
|
-
return aTranscript - bTranscript || a.localeCompare(b);
|
|
3556
|
+
function statsFromParseErrors(errors) {
|
|
3557
|
+
return { parseFailed: errors.length > 0, unreadable: false, malformed: errors.some((error) => error.startsWith("jsonl_line_")) };
|
|
3023
3558
|
}
|
|
3024
|
-
function
|
|
3025
|
-
|
|
3026
|
-
const
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3559
|
+
function collectTraceContexts(records) {
|
|
3560
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
3561
|
+
for (const record of records) {
|
|
3562
|
+
const traceId = traceIdFromRecord(record);
|
|
3563
|
+
const attributes = firstObject5(record.attributes);
|
|
3564
|
+
if (!traceId || !attributes) continue;
|
|
3565
|
+
const context = contexts.get(traceId) ?? { model: void 0, sessionId: void 0, sessionPriority: 0 };
|
|
3566
|
+
context.model ??= firstNonEmptyAttr(attributes, modelAttrs);
|
|
3567
|
+
const session = bestSessionAttr(attributes);
|
|
3568
|
+
if (session && session.priority > context.sessionPriority) {
|
|
3569
|
+
context.sessionId = session.value;
|
|
3570
|
+
context.sessionPriority = session.priority;
|
|
3571
|
+
}
|
|
3572
|
+
contexts.set(traceId, context);
|
|
3573
|
+
}
|
|
3574
|
+
return contexts;
|
|
3575
|
+
}
|
|
3576
|
+
function usageCandidateFromRecord(record, index, fallbackTimestamp, traceContexts) {
|
|
3577
|
+
const attributes = firstObject5(record.attributes);
|
|
3578
|
+
if (!attributes) return void 0;
|
|
3579
|
+
const source = usageSource(record, attributes);
|
|
3580
|
+
if (!source) return void 0;
|
|
3581
|
+
const input = numberValue2(attributes["gen_ai.usage.input_tokens"]);
|
|
3582
|
+
const output = numberValue2(attributes["gen_ai.usage.output_tokens"]);
|
|
3583
|
+
const cached = numberValue2(attributes["gen_ai.usage.cache_read.input_tokens"]);
|
|
3584
|
+
const cacheCreation = firstPositiveNumber(attributes, ["gen_ai.usage.cache_write.input_tokens", "gen_ai.usage.cache_creation.input_tokens"]);
|
|
3585
|
+
const reasoning = firstPositiveNumber(attributes, ["gen_ai.usage.reasoning.output_tokens", "gen_ai.usage.reasoning_tokens"]);
|
|
3586
|
+
const total = firstPositiveNumber(attributes, ["gen_ai.usage.total_tokens", "gen_ai.usage.total.token_count"]);
|
|
3587
|
+
const outputWithFallback = output || outputTokensFromTotal(total, input, reasoning);
|
|
3588
|
+
if (input + outputWithFallback + cached + cacheCreation + reasoning <= 0) return void 0;
|
|
3589
|
+
const traceId = traceIdFromRecord(record);
|
|
3590
|
+
const traceContext = traceId ? traceContexts.get(traceId) : void 0;
|
|
3591
|
+
const session = bestSessionAttr(attributes)?.value ?? traceContext?.sessionId ?? traceId ?? "unknown-session";
|
|
3592
|
+
const timestamp3 = timestampFromRecord(record) ?? fallbackTimestamp;
|
|
3593
|
+
return {
|
|
3594
|
+
source,
|
|
3595
|
+
traceId,
|
|
3596
|
+
responseId: stringValue4(attributes["gen_ai.response.id"]),
|
|
3597
|
+
sessionId: session,
|
|
3598
|
+
model: firstNonEmptyAttr(attributes, modelAttrs) ?? traceContext?.model,
|
|
3599
|
+
timestamp: timestamp3,
|
|
3600
|
+
durationMs: durationMsFromRecord(record),
|
|
3601
|
+
inputTokens: input,
|
|
3602
|
+
cachedInputTokens: cached,
|
|
3603
|
+
cacheCreationInputTokens: cacheCreation,
|
|
3604
|
+
outputTokens: outputWithFallback,
|
|
3605
|
+
reasoningTokens: reasoning,
|
|
3606
|
+
dedupKey: dedupKeyForRecord(source, record, attributes, traceId, session, timestamp3, index)
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
function outputTokensFromTotal(total, input, reasoning) {
|
|
3610
|
+
return total > 0 && total >= input + reasoning ? total - input - reasoning : 0;
|
|
3611
|
+
}
|
|
3612
|
+
function usageSource(record, attributes) {
|
|
3613
|
+
const operation = stringValue4(attributes["gen_ai.operation.name"]);
|
|
3614
|
+
const eventName = stringValue4(attributes["event.name"]);
|
|
3615
|
+
const name = stringValue4(record.name);
|
|
3616
|
+
const body = stringValue4(record.body) ?? stringValue4(record._body);
|
|
3617
|
+
const span = isSpanRecord(record);
|
|
3618
|
+
if (span && (operation === "chat" || name?.startsWith("chat "))) return "chat_span";
|
|
3619
|
+
if (!span && (eventName === "gen_ai.client.inference.operation.details" || body?.startsWith("GenAI inference:"))) return "inference_log";
|
|
3620
|
+
if (!span && (eventName === "copilot_chat.agent.turn" || body?.startsWith("copilot_chat.agent.turn"))) return "agent_turn_log";
|
|
3621
|
+
if (span && (operation === "invoke_agent" || name?.startsWith("invoke_agent "))) return "agent_summary_span";
|
|
3622
|
+
return void 0;
|
|
3623
|
+
}
|
|
3624
|
+
function isSpanRecord(record) {
|
|
3625
|
+
const type = stringValue4(record.type);
|
|
3626
|
+
if (type) return type === "span";
|
|
3627
|
+
return Boolean(stringValue4(record.name) && (stringValue4(record.spanId) || stringValue4(record.traceId) || record.startTime || record.endTime || record.duration || record.kind));
|
|
3628
|
+
}
|
|
3629
|
+
function shouldEmitCandidate(candidate, candidates) {
|
|
3630
|
+
const has = (source, field, value) => Boolean(value && candidates.some((item) => item.source === source && item[field] === value));
|
|
3631
|
+
if (candidate.source === "chat_span") return true;
|
|
3632
|
+
if (candidate.source === "inference_log") return !has("chat_span", "traceId", candidate.traceId) && !has("chat_span", "responseId", candidate.responseId);
|
|
3633
|
+
if (candidate.source === "agent_turn_log") {
|
|
3634
|
+
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);
|
|
3635
|
+
}
|
|
3636
|
+
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);
|
|
3637
|
+
}
|
|
3638
|
+
function uniqueCandidates(candidates) {
|
|
3639
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3640
|
+
return candidates.filter((candidate) => {
|
|
3641
|
+
if (seen.has(candidate.dedupKey)) return false;
|
|
3642
|
+
seen.add(candidate.dedupKey);
|
|
3643
|
+
return true;
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
var modelAttrs = ["gen_ai.response.model", "gen_ai.request.model"];
|
|
3647
|
+
var sessionAttrs = [
|
|
3648
|
+
{ key: "gen_ai.conversation.id", priority: 3 },
|
|
3649
|
+
{ key: "copilot_chat.session_id", priority: 3 },
|
|
3650
|
+
{ key: "copilot_chat.chat_session_id", priority: 3 },
|
|
3651
|
+
{ key: "session.id", priority: 3 },
|
|
3652
|
+
{ key: "github.copilot.interaction_id", priority: 2 },
|
|
3653
|
+
{ key: "gen_ai.response.id", priority: 1 }
|
|
3654
|
+
];
|
|
3655
|
+
function applyCandidate(breakdown, candidate) {
|
|
3656
|
+
breakdown.id = candidate.sessionId;
|
|
3657
|
+
breakdown.model = candidate.model ?? breakdown.model;
|
|
3658
|
+
const candidateStartedAt = candidate.timestamp && candidate.durationMs ? new Date(Date.parse(candidate.timestamp) - candidate.durationMs).toISOString() : candidate.timestamp;
|
|
3659
|
+
breakdown.startedAt = minDate2(breakdown.startedAt, candidateStartedAt);
|
|
3660
|
+
breakdown.endedAt = maxDate2(breakdown.endedAt, candidate.timestamp);
|
|
3661
|
+
applyTokenEvent(breakdown, {
|
|
3662
|
+
dedupKey: candidate.dedupKey,
|
|
3663
|
+
detailed: candidate.inputTokens > 0 || candidate.cachedInputTokens > 0 || candidate.cacheCreationInputTokens > 0 || candidate.reasoningTokens > 0,
|
|
3664
|
+
inputTokens: candidate.inputTokens,
|
|
3665
|
+
cachedInputTokens: candidate.cachedInputTokens,
|
|
3666
|
+
cacheCreationInputTokens: candidate.cacheCreationInputTokens,
|
|
3667
|
+
outputTokens: candidate.outputTokens,
|
|
3668
|
+
reasoningTokens: candidate.reasoningTokens
|
|
3669
|
+
});
|
|
3670
|
+
breakdown.messageCount += 1;
|
|
3671
|
+
breakdown.assistantMessageCount += 1;
|
|
3672
|
+
}
|
|
3673
|
+
function applyTranscriptRecord(breakdown, record, file, fallbackTimestamp) {
|
|
3674
|
+
const data = firstObject5(record.data);
|
|
3675
|
+
const timestamp3 = dateValue4(record.timestamp) ?? dateValue4(record.ts) ?? dateValue4(data?.startTime) ?? fallbackTimestamp;
|
|
3676
|
+
const type = stringValue4(record.type);
|
|
3677
|
+
const role = type?.startsWith("user.") ? "user" : type?.startsWith("assistant.") ? "assistant" : stringValue4(data?.role);
|
|
3678
|
+
const sessionId = stringValue4(data?.sessionId) ?? stringValue4(record.sessionId) ?? stringValue4(record.sid);
|
|
3679
|
+
if (sessionId) breakdown.id = sessionId;
|
|
3680
|
+
if (timestamp3) {
|
|
3681
|
+
breakdown.startedAt = minDate2(breakdown.startedAt, timestamp3);
|
|
3682
|
+
breakdown.endedAt = maxDate2(breakdown.endedAt, timestamp3);
|
|
3683
|
+
}
|
|
3684
|
+
breakdown.cwd ??= stringValue4(data?.cwd) ?? stringValue4(record.cwd) ?? file.fallbackCwd;
|
|
3685
|
+
const context = firstObject5(data?.context);
|
|
3686
|
+
breakdown.cwd ??= stringValue4(context?.cwd) ?? stringValue4(context?.gitRoot);
|
|
3687
|
+
const modelCandidate = stringValue4(data?.newModel) ?? stringValue4(data?.currentModel) ?? stringValue4(data?.model) ?? stringValue4(record.model);
|
|
3688
|
+
if (modelCandidate && (modelCandidate !== "auto" || !breakdown.model)) breakdown.model = modelCandidate;
|
|
3689
|
+
if (type === "session.start") breakdown.title ??= stringValue4(data?.title);
|
|
3690
|
+
const outputTokens = numberValue2(data?.outputTokens);
|
|
3691
|
+
if (outputTokens > 0) {
|
|
3692
|
+
const partialSessionId = sessionId ?? breakdown.id ?? path7.basename(file.filePath);
|
|
3693
|
+
applyTokenEvent(breakdown, {
|
|
3694
|
+
dedupKey: `transcript-output:${file.filePath}:${partialSessionId}:${timestamp3 ?? "unknown"}:${breakdown.usageCount}`,
|
|
3695
|
+
detailed: false,
|
|
3696
|
+
inputTokens: 0,
|
|
3697
|
+
cachedInputTokens: 0,
|
|
3698
|
+
cacheCreationInputTokens: 0,
|
|
3699
|
+
outputTokens,
|
|
3700
|
+
reasoningTokens: 0
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
if (role === "user" || role === "assistant") {
|
|
3704
|
+
breakdown.messageCount += 1;
|
|
3705
|
+
if (role === "user") breakdown.userPromptCount += 1;
|
|
3706
|
+
if (role === "assistant") breakdown.assistantMessageCount += 1;
|
|
3707
|
+
applyTimeline(breakdown, role, data?.content ?? record.content, timestamp3);
|
|
3708
|
+
}
|
|
3709
|
+
const toolRequests = Array.isArray(data?.toolRequests) ? data.toolRequests : [];
|
|
3710
|
+
breakdown.toolCallCount += toolRequests.length;
|
|
3711
|
+
for (const request of toolRequests) {
|
|
3712
|
+
const object = firstObject5(request);
|
|
3713
|
+
const name = stringValue4(object?.name)?.toLowerCase() ?? "";
|
|
3714
|
+
const args2 = stringValue4(object?.arguments)?.toLowerCase() ?? "";
|
|
3715
|
+
if (name.includes("terminal") || args2.includes('"command"')) breakdown.shellCommandCount += 1;
|
|
3716
|
+
if (name.includes("edit") || args2.includes("edit")) breakdown.fileEditCount += 1;
|
|
3717
|
+
if (name.includes("read") || args2.includes("read_file")) breakdown.fileReadCount += 1;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
function breakdownToUsage(breakdown, index, options) {
|
|
3721
|
+
const id = breakdown.id ?? `copilot-session-${index}`;
|
|
3722
|
+
const cwd = breakdown.cwd ?? "";
|
|
3723
|
+
const repo = breakdown.cwd ? resolveRepoInfo(breakdown.cwd, options.config) : unknownCopilotRepoInfo();
|
|
3724
|
+
const hasTokenBreakdown = breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0 && breakdown.usageCount > 0;
|
|
3725
|
+
const hasPricedBreakdown = hasTokenBreakdown && breakdown.inputTokens > 0;
|
|
3726
|
+
const warnings = unique4([...repo.warnings, ...breakdown.warnings]);
|
|
3727
|
+
if (hasTokenBreakdown && !hasPricedBreakdown) warnings.push("copilot_partial_token_breakdown");
|
|
3728
|
+
const usage = {
|
|
3729
|
+
id,
|
|
3730
|
+
sourceClient: "copilot",
|
|
3731
|
+
sourceApp: breakdown.surface === "cli" ? "Copilot CLI" : breakdown.surface === "vscode" ? "VS Code" : "GitHub Copilot",
|
|
3732
|
+
sourceAppRaw: breakdown.surface,
|
|
3733
|
+
sourcePath: breakdown.sourcePaths[0] ?? "",
|
|
3734
|
+
repoRoot: repo.repoRoot,
|
|
3735
|
+
repoName: repo.repoName,
|
|
3736
|
+
cwd,
|
|
3737
|
+
gitRemoteUrl: repo.gitRemoteUrl,
|
|
3738
|
+
gitBranch: repo.gitBranch,
|
|
3739
|
+
title: breakdown.title,
|
|
3740
|
+
startedAt: breakdown.startedAt,
|
|
3741
|
+
endedAt: breakdown.endedAt,
|
|
3742
|
+
model: normalizeCopilotModel(breakdown.model),
|
|
3743
|
+
provider: providerFromModel(breakdown.model),
|
|
3744
|
+
inputTokens: breakdown.inputTokens,
|
|
3745
|
+
cachedInputTokens: breakdown.cachedInputTokens,
|
|
3746
|
+
cacheCreationInputTokens: breakdown.cacheCreationInputTokens,
|
|
3747
|
+
outputTokens: breakdown.outputTokens,
|
|
3748
|
+
reasoningTokens: breakdown.reasoningTokens,
|
|
3749
|
+
reasoningOutputTokens: breakdown.reasoningTokens,
|
|
3750
|
+
totalTokens: breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens,
|
|
3751
|
+
tokenAggregationMethod: hasTokenBreakdown ? "direct_usage" : "unknown",
|
|
3752
|
+
tokenConfidence: hasTokenBreakdown ? "high" : "low",
|
|
3753
|
+
tokenSnapshotCount: breakdown.usageCount,
|
|
3754
|
+
estimatedCostUsd: void 0,
|
|
3755
|
+
messageCount: breakdown.messageCount,
|
|
3756
|
+
rawTokenTotal: hasTokenBreakdown ? breakdown.inputTokens + breakdown.outputTokens + breakdown.reasoningTokens : void 0,
|
|
3757
|
+
warnings: hasTokenBreakdown ? warnings : unique4([...warnings, "missing_token_breakdown"]),
|
|
3758
|
+
durationMs: durationMs3(breakdown.startedAt, breakdown.endedAt),
|
|
3759
|
+
rawEventCount: breakdown.rawEventCount,
|
|
3760
|
+
parseStatus: breakdown.parseStatus,
|
|
3761
|
+
parseErrors: breakdown.parseErrors,
|
|
3762
|
+
detectedSurface: breakdown.surface === "vscode" ? "vscode_extension" : "terminal_cli",
|
|
3763
|
+
surfaceConfidence: breakdown.surface === "unknown" ? "low" : "high",
|
|
3764
|
+
surfaceReason: breakdown.surface === "vscode" ? "GitHub Copilot VS Code workspace storage" : "GitHub Copilot OpenTelemetry export",
|
|
3765
|
+
promptTimeline: breakdown.promptTimeline,
|
|
3766
|
+
userPromptCount: breakdown.userPromptCount,
|
|
3767
|
+
assistantMessageCount: breakdown.assistantMessageCount,
|
|
3768
|
+
toolCallCount: breakdown.toolCallCount,
|
|
3769
|
+
shellCommandCount: breakdown.shellCommandCount,
|
|
3770
|
+
failedToolCallCount: breakdown.failedToolCallCount,
|
|
3771
|
+
nonZeroCommandEvents: 0,
|
|
3772
|
+
importantCommandFailures: 0,
|
|
3773
|
+
harmlessNonZeroEvents: 0,
|
|
3774
|
+
exploratoryMisses: 0,
|
|
3775
|
+
repeatedFailureClusters: 0,
|
|
3776
|
+
commandIssueSeverity: breakdown.failedToolCallCount > 0 ? "warning" : "none",
|
|
3777
|
+
commandIssueImpact: breakdown.failedToolCallCount > 0 ? "medium" : "none",
|
|
3778
|
+
topFailureType: breakdown.failedToolCallCount > 0 ? "unknown" : void 0,
|
|
3779
|
+
commandIssueSamples: [],
|
|
3780
|
+
fileReadCount: breakdown.fileReadCount,
|
|
3781
|
+
fileEditCount: breakdown.fileEditCount,
|
|
3782
|
+
sourceMetadata: { copilot: { sourcePaths: breakdown.sourcePaths, surface: breakdown.surface } }
|
|
3783
|
+
};
|
|
3784
|
+
const cost = hasPricedBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
|
|
3785
|
+
return { ...usage, estimatedCostUsd: cost, warnings: hasPricedBreakdown && cost === void 0 ? [...usage.warnings, "unknown_pricing"] : usage.warnings };
|
|
3786
|
+
}
|
|
3787
|
+
function unknownCopilotRepoInfo() {
|
|
3788
|
+
return {
|
|
3789
|
+
repoRoot: "unknown-copilot-repo",
|
|
3790
|
+
repoName: "Unknown repo/folder",
|
|
3791
|
+
gitRemoteUrl: void 0,
|
|
3792
|
+
gitBranch: void 0,
|
|
3793
|
+
warnings: ["repo_unverified_no_git_root", "copilot_missing_cwd"]
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
function emptyBreakdown3(file, id) {
|
|
3797
|
+
return {
|
|
3798
|
+
id,
|
|
3799
|
+
title: void 0,
|
|
3800
|
+
cwd: file.fallbackCwd,
|
|
3801
|
+
startedAt: void 0,
|
|
3802
|
+
endedAt: void 0,
|
|
3803
|
+
model: void 0,
|
|
3804
|
+
inputTokens: 0,
|
|
3805
|
+
cachedInputTokens: 0,
|
|
3806
|
+
cacheCreationInputTokens: 0,
|
|
3807
|
+
outputTokens: 0,
|
|
3808
|
+
reasoningTokens: 0,
|
|
3809
|
+
usageCount: 0,
|
|
3810
|
+
messageCount: 0,
|
|
3811
|
+
userPromptCount: 0,
|
|
3812
|
+
assistantMessageCount: 0,
|
|
3813
|
+
toolCallCount: 0,
|
|
3814
|
+
shellCommandCount: 0,
|
|
3815
|
+
failedToolCallCount: 0,
|
|
3816
|
+
fileReadCount: 0,
|
|
3817
|
+
fileEditCount: 0,
|
|
3818
|
+
rawEventCount: 0,
|
|
3819
|
+
parseStatus: "ok",
|
|
3820
|
+
parseErrors: [],
|
|
3821
|
+
promptTimeline: [],
|
|
3822
|
+
warnings: [],
|
|
3823
|
+
sourcePaths: [file.filePath],
|
|
3824
|
+
surface: file.surface,
|
|
3825
|
+
tokenEvents: []
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
function mergeBreakdown(sessions, breakdown) {
|
|
3829
|
+
const key = breakdown.id ?? breakdown.sourcePaths[0] ?? "unknown-session";
|
|
3830
|
+
const existing = sessions.get(key);
|
|
3831
|
+
if (!existing) {
|
|
3832
|
+
sessions.set(key, breakdown);
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
mergeActivity(existing, breakdown);
|
|
3836
|
+
mergeTokenData(existing, breakdown);
|
|
3837
|
+
existing.sourcePaths = unique4([...existing.sourcePaths, ...breakdown.sourcePaths]);
|
|
3838
|
+
}
|
|
3839
|
+
function mergeTokenData(target, source) {
|
|
3840
|
+
if (source.usageCount <= 0) return;
|
|
3841
|
+
const targetHadUsage = target.usageCount > 0;
|
|
3842
|
+
const targetHasDetailed = hasDetailedTokenBreakdown(target);
|
|
3843
|
+
const sourceHasDetailed = hasDetailedTokenBreakdown(source);
|
|
3844
|
+
if (targetHadUsage) target.warnings = unique4([...target.warnings, "copilot_duplicate_session_merged"]);
|
|
3845
|
+
if (targetHadUsage && targetHasDetailed && !sourceHasDetailed) return;
|
|
3846
|
+
if (targetHadUsage && !targetHasDetailed && sourceHasDetailed) {
|
|
3847
|
+
replaceTokenData(target, source);
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
if (source.tokenEvents.length > 0) {
|
|
3851
|
+
const existingKeys = new Set(target.tokenEvents.map((event) => event.dedupKey));
|
|
3852
|
+
for (const event of source.tokenEvents) {
|
|
3853
|
+
if (existingKeys.has(event.dedupKey)) continue;
|
|
3854
|
+
applyTokenEvent(target, event);
|
|
3855
|
+
existingKeys.add(event.dedupKey);
|
|
3856
|
+
}
|
|
3857
|
+
return;
|
|
3858
|
+
}
|
|
3859
|
+
applyTokenEvent(target, {
|
|
3860
|
+
dedupKey: `merged-total:${source.sourcePaths.join("|")}:${source.usageCount}`,
|
|
3861
|
+
detailed: sourceHasDetailed,
|
|
3862
|
+
inputTokens: source.inputTokens,
|
|
3863
|
+
cachedInputTokens: source.cachedInputTokens,
|
|
3864
|
+
cacheCreationInputTokens: source.cacheCreationInputTokens,
|
|
3865
|
+
outputTokens: source.outputTokens,
|
|
3866
|
+
reasoningTokens: source.reasoningTokens
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
function replaceTokenData(target, source) {
|
|
3870
|
+
target.inputTokens = source.inputTokens;
|
|
3871
|
+
target.cachedInputTokens = source.cachedInputTokens;
|
|
3872
|
+
target.cacheCreationInputTokens = source.cacheCreationInputTokens;
|
|
3873
|
+
target.outputTokens = source.outputTokens;
|
|
3874
|
+
target.reasoningTokens = source.reasoningTokens;
|
|
3875
|
+
target.usageCount = source.usageCount;
|
|
3876
|
+
target.tokenEvents = source.tokenEvents.map((event) => ({ ...event }));
|
|
3877
|
+
}
|
|
3878
|
+
function hasDetailedTokenBreakdown(breakdown) {
|
|
3879
|
+
return breakdown.inputTokens > 0 || breakdown.cachedInputTokens > 0 || breakdown.cacheCreationInputTokens > 0 || breakdown.reasoningTokens > 0;
|
|
3880
|
+
}
|
|
3881
|
+
function applyTokenEvent(breakdown, event) {
|
|
3882
|
+
if (breakdown.tokenEvents.some((existing) => existing.dedupKey === event.dedupKey)) return;
|
|
3883
|
+
breakdown.inputTokens += event.inputTokens;
|
|
3884
|
+
breakdown.cachedInputTokens += event.cachedInputTokens;
|
|
3885
|
+
breakdown.cacheCreationInputTokens += event.cacheCreationInputTokens;
|
|
3886
|
+
breakdown.outputTokens += event.outputTokens;
|
|
3887
|
+
breakdown.reasoningTokens += event.reasoningTokens;
|
|
3888
|
+
breakdown.usageCount += 1;
|
|
3889
|
+
breakdown.tokenEvents.push({ ...event });
|
|
3890
|
+
}
|
|
3891
|
+
function applyModelHints(sessions, modelHints) {
|
|
3892
|
+
for (const breakdown of sessions.values()) {
|
|
3893
|
+
if (!breakdown.id) continue;
|
|
3894
|
+
const hint = modelHints.get(breakdown.id);
|
|
3895
|
+
if (!hint || hasKnownModel(breakdown.model)) continue;
|
|
3896
|
+
breakdown.model = hint.model;
|
|
3897
|
+
breakdown.sourcePaths = unique4([...breakdown.sourcePaths, hint.sourcePath]);
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
function hasKnownModel(model) {
|
|
3901
|
+
const normalized = model?.trim().toLowerCase().replace(/^github[-_]?copilot\//, "").replace(/^copilot\//, "");
|
|
3902
|
+
return Boolean(normalized && normalized !== "auto");
|
|
3903
|
+
}
|
|
3904
|
+
function mergeActivity(target, source) {
|
|
3905
|
+
target.title ??= source.title;
|
|
3906
|
+
target.cwd ??= source.cwd;
|
|
3907
|
+
target.model ??= source.model;
|
|
3908
|
+
target.startedAt = minDate2(target.startedAt, source.startedAt);
|
|
3909
|
+
target.endedAt = maxDate2(target.endedAt, source.endedAt);
|
|
3910
|
+
target.messageCount = Math.max(target.messageCount, source.messageCount);
|
|
3911
|
+
target.userPromptCount = Math.max(target.userPromptCount, source.userPromptCount);
|
|
3912
|
+
target.assistantMessageCount = Math.max(target.assistantMessageCount, source.assistantMessageCount);
|
|
3913
|
+
target.toolCallCount = Math.max(target.toolCallCount, source.toolCallCount);
|
|
3914
|
+
target.shellCommandCount = Math.max(target.shellCommandCount, source.shellCommandCount);
|
|
3915
|
+
target.fileReadCount = Math.max(target.fileReadCount, source.fileReadCount);
|
|
3916
|
+
target.fileEditCount = Math.max(target.fileEditCount, source.fileEditCount);
|
|
3917
|
+
target.rawEventCount = Math.max(target.rawEventCount, source.rawEventCount);
|
|
3918
|
+
target.parseErrors = unique4([...target.parseErrors, ...source.parseErrors]);
|
|
3919
|
+
target.parseStatus = target.parseErrors.length ? "partial" : target.parseStatus;
|
|
3920
|
+
target.promptTimeline = [...target.promptTimeline, ...source.promptTimeline].slice(0, 80);
|
|
3921
|
+
target.warnings = unique4([...target.warnings, ...source.warnings]);
|
|
3922
|
+
return target;
|
|
3923
|
+
}
|
|
3924
|
+
function fallbackWithMissingTokenWarning(breakdown) {
|
|
3925
|
+
if (breakdown.usageCount === 0) breakdown.warnings.push("missing_token_breakdown");
|
|
3926
|
+
return breakdown;
|
|
3927
|
+
}
|
|
3928
|
+
function parseJsonLines3(text) {
|
|
3929
|
+
const records = [];
|
|
3930
|
+
const errors = [];
|
|
3931
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
3932
|
+
const trimmed = line.trim();
|
|
3933
|
+
if (!trimmed) return;
|
|
3934
|
+
try {
|
|
3935
|
+
const parsed = JSON.parse(trimmed);
|
|
3936
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) records.push(parsed);
|
|
3937
|
+
} catch (error) {
|
|
3938
|
+
errors.push(`jsonl_line_${index + 1}:${errorMessage3(error)}`);
|
|
3939
|
+
}
|
|
3940
|
+
});
|
|
3941
|
+
return { records, errors };
|
|
3942
|
+
}
|
|
3943
|
+
function applyTimeline(breakdown, role, content, timestamp3) {
|
|
3944
|
+
if (breakdown.promptTimeline.length >= 80) return;
|
|
3945
|
+
const text = typeof content === "string" ? content : void 0;
|
|
3946
|
+
if (!text?.trim()) return;
|
|
3947
|
+
breakdown.promptTimeline.push({ role, text: text.replace(/\s+/g, " ").trim().slice(0, 1e3), timestamp: timestamp3 });
|
|
3948
|
+
}
|
|
3949
|
+
function firstNonEmptyAttr(attributes, keys) {
|
|
3950
|
+
return keys.map((key) => stringValue4(attributes[key])).find(Boolean);
|
|
3951
|
+
}
|
|
3952
|
+
function bestSessionAttr(attributes) {
|
|
3953
|
+
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];
|
|
3954
|
+
}
|
|
3955
|
+
function traceIdFromRecord(record) {
|
|
3956
|
+
const spanContext = firstObject5(record.spanContext);
|
|
3957
|
+
return stringValue4(record.traceId) ?? stringValue4(spanContext?.traceId);
|
|
3958
|
+
}
|
|
3959
|
+
function spanIdFromRecord(record) {
|
|
3960
|
+
const spanContext = firstObject5(record.spanContext);
|
|
3961
|
+
return stringValue4(record.spanId) ?? stringValue4(spanContext?.spanId);
|
|
3962
|
+
}
|
|
3963
|
+
function dedupKeyForRecord(source, record, attributes, traceId, sessionId, timestamp3, index) {
|
|
3964
|
+
const spanId = spanIdFromRecord(record);
|
|
3965
|
+
if ((source === "chat_span" || source === "agent_summary_span") && traceId && spanId) return `${traceId}:${spanId}`;
|
|
3966
|
+
if (source === "inference_log" && traceId && spanId) return `log:${traceId}:${spanId}`;
|
|
3967
|
+
if (source === "agent_turn_log") {
|
|
3968
|
+
const turn = stringValue4(attributes["turn.index"]) ?? stringValue4(attributes["copilot_chat.turn.index"]) ?? `idx-${index}`;
|
|
3969
|
+
return traceId ? `agent-turn:${traceId}:${turn}` : `agent-turn:${sessionId}:${turn}:${index}`;
|
|
3970
|
+
}
|
|
3971
|
+
return `${source}:${sessionId}:${timestamp3 ?? "unknown"}:${index}`;
|
|
3972
|
+
}
|
|
3973
|
+
function timestampFromRecord(record) {
|
|
3974
|
+
return timestampMsFromParts(record.endTime) ?? timestampMsFromParts(record.startTime) ?? timestampMsFromParts(record.hrTime) ?? timestampMsFromParts(record._hrTime) ?? timestampMsFromScalar(record.timestamp) ?? timestampMsFromScalar(record.observedTimestamp) ?? timestampMsFromUnixNanos(record.timeUnixNano);
|
|
3975
|
+
}
|
|
3976
|
+
function timestampMsFromParts(value) {
|
|
3977
|
+
if (!Array.isArray(value)) return void 0;
|
|
3978
|
+
const seconds = numberValue2(value[0]);
|
|
3979
|
+
const nanos = numberValue2(value[1]);
|
|
3980
|
+
if (seconds <= 0) return void 0;
|
|
3981
|
+
return new Date(seconds * 1e3 + Math.floor(nanos / 1e6)).toISOString();
|
|
3982
|
+
}
|
|
3983
|
+
function timestampMsFromScalar(value) {
|
|
3984
|
+
const raw = numberValue2(value);
|
|
3985
|
+
if (raw <= 0) return void 0;
|
|
3986
|
+
const absolute = Math.abs(raw);
|
|
3987
|
+
const ms = absolute >= 1e17 ? raw / 1e6 : absolute >= 1e14 ? raw / 1e3 : absolute >= 1e11 ? raw : raw * 1e3;
|
|
3988
|
+
return new Date(ms).toISOString();
|
|
3989
|
+
}
|
|
3990
|
+
function timestampMsFromUnixNanos(value) {
|
|
3991
|
+
const raw = numberValue2(value);
|
|
3992
|
+
return raw > 0 ? new Date(Math.floor(raw / 1e6)).toISOString() : void 0;
|
|
3993
|
+
}
|
|
3994
|
+
function durationMsFromRecord(record) {
|
|
3995
|
+
const start = timestampMs2(record.startTime);
|
|
3996
|
+
const end = timestampMs2(record.endTime);
|
|
3997
|
+
if (start !== void 0 && end !== void 0 && end > start) return end - start;
|
|
3998
|
+
const duration = numberValue2(record.duration);
|
|
3999
|
+
return duration > 0 ? duration >= 1e6 ? Math.floor(duration / 1e6) : duration : void 0;
|
|
4000
|
+
}
|
|
4001
|
+
function timestampMs2(value) {
|
|
4002
|
+
const iso = timestampMsFromParts(value) ?? timestampMsFromScalar(value);
|
|
4003
|
+
return iso ? Date.parse(iso) : void 0;
|
|
4004
|
+
}
|
|
4005
|
+
function defaultCodeUserRoots() {
|
|
4006
|
+
const home = os4.homedir();
|
|
4007
|
+
if (process.platform === "win32") {
|
|
4008
|
+
const appData = process.env.APPDATA ?? path7.join(home, "AppData", "Roaming");
|
|
4009
|
+
return ["Code", "Code - Insiders", "VSCodium"].map((name) => path7.join(appData, name, "User"));
|
|
4010
|
+
}
|
|
4011
|
+
if (process.platform === "darwin") {
|
|
4012
|
+
return ["Code", "Code - Insiders", "VSCodium"].map((name) => path7.join(home, "Library", "Application Support", name, "User"));
|
|
4013
|
+
}
|
|
4014
|
+
const xdg = process.env.XDG_CONFIG_HOME || path7.join(home, ".config");
|
|
4015
|
+
return ["Code", "Code - Insiders", "VSCodium"].map((name) => path7.join(xdg, name, "User"));
|
|
4016
|
+
}
|
|
4017
|
+
function workspaceFolderFromStorageJson(filePath) {
|
|
4018
|
+
try {
|
|
4019
|
+
const parsed = JSON.parse(fs7.readFileSync(filePath, "utf8"));
|
|
4020
|
+
const folder = stringValue4(parsed.folder);
|
|
4021
|
+
if (!folder) return void 0;
|
|
4022
|
+
return folder.startsWith("file://") ? decodeURIComponent(new URL(folder).pathname) : folder;
|
|
4023
|
+
} catch {
|
|
4024
|
+
return void 0;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
function cwdFromWorkspaceYaml(filePath) {
|
|
4028
|
+
try {
|
|
4029
|
+
const text = fs7.readFileSync(filePath, "utf8");
|
|
4030
|
+
const match = /^cwd:\s*(.+)$/m.exec(text);
|
|
4031
|
+
return match?.[1]?.trim();
|
|
4032
|
+
} catch {
|
|
4033
|
+
return void 0;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
function fileMayOverlapScanWindow2(filePath, scanWindow) {
|
|
4037
|
+
if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
|
|
4038
|
+
try {
|
|
4039
|
+
const mtime = fs7.statSync(filePath).mtimeMs;
|
|
4040
|
+
if (scanWindow.fromMs !== void 0 && mtime < scanWindow.fromMs) return false;
|
|
4041
|
+
if (scanWindow.toMs !== void 0 && mtime > scanWindow.toMs) return false;
|
|
4042
|
+
} catch {
|
|
4043
|
+
return true;
|
|
4044
|
+
}
|
|
4045
|
+
return true;
|
|
4046
|
+
}
|
|
4047
|
+
function walk3(root, maxDepth, onFile, depth = 0) {
|
|
4048
|
+
if (depth > maxDepth) return;
|
|
4049
|
+
for (const entry of fs7.readdirSync(root, { withFileTypes: true })) {
|
|
4050
|
+
const fullPath = path7.join(root, entry.name);
|
|
4051
|
+
if (entry.isDirectory()) walk3(fullPath, maxDepth, onFile, depth + 1);
|
|
4052
|
+
else if (entry.isFile()) onFile(fullPath);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
function isReadableRegularFile(filePath, maxBytes) {
|
|
4056
|
+
try {
|
|
4057
|
+
const stat = fs7.statSync(filePath);
|
|
4058
|
+
return stat.isFile() && stat.size <= maxBytes;
|
|
4059
|
+
} catch {
|
|
4060
|
+
return false;
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
function normalizeCopilotModel(model) {
|
|
4064
|
+
if (!model) return void 0;
|
|
4065
|
+
return normalizePricingModelId(model);
|
|
4066
|
+
}
|
|
4067
|
+
function providerFromModel(model) {
|
|
4068
|
+
const normalized = normalizeCopilotModel(model);
|
|
4069
|
+
if (!normalized) return "github-copilot";
|
|
4070
|
+
if (normalized.includes("claude")) return "anthropic";
|
|
4071
|
+
if (normalized.includes("gemini")) return "google";
|
|
4072
|
+
if (isCopilotAliasModel(normalized)) return "github-copilot";
|
|
4073
|
+
if (normalized.includes("gpt")) return "openai";
|
|
4074
|
+
return "github-copilot";
|
|
4075
|
+
}
|
|
4076
|
+
function firstPositiveNumber(object, keys) {
|
|
4077
|
+
return keys.map((key) => numberValue2(object[key])).find((value) => value > 0) ?? 0;
|
|
4078
|
+
}
|
|
4079
|
+
function firstObject5(value) {
|
|
4080
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4081
|
+
}
|
|
4082
|
+
function stringValue4(value) {
|
|
4083
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
4084
|
+
}
|
|
4085
|
+
function numberValue2(value) {
|
|
4086
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.max(Math.trunc(value), 0);
|
|
4087
|
+
if (typeof value === "string" && value.trim()) {
|
|
4088
|
+
const parsed = Number(value);
|
|
4089
|
+
return Number.isFinite(parsed) ? Math.max(Math.trunc(parsed), 0) : 0;
|
|
4090
|
+
}
|
|
4091
|
+
return 0;
|
|
4092
|
+
}
|
|
4093
|
+
function dateValue4(value) {
|
|
4094
|
+
if (typeof value === "string") {
|
|
4095
|
+
const parsed = Date.parse(value);
|
|
4096
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : void 0;
|
|
4097
|
+
}
|
|
4098
|
+
return timestampMsFromScalar(value);
|
|
4099
|
+
}
|
|
4100
|
+
function fileModifiedDate(filePath) {
|
|
4101
|
+
try {
|
|
4102
|
+
return new Date(fs7.statSync(filePath).mtimeMs).toISOString();
|
|
4103
|
+
} catch {
|
|
4104
|
+
return void 0;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
function minDate2(a, b) {
|
|
4108
|
+
if (!a) return b;
|
|
4109
|
+
if (!b) return a;
|
|
4110
|
+
return a < b ? a : b;
|
|
4111
|
+
}
|
|
4112
|
+
function maxDate2(a, b) {
|
|
4113
|
+
if (!a) return b;
|
|
4114
|
+
if (!b) return a;
|
|
4115
|
+
return a > b ? a : b;
|
|
4116
|
+
}
|
|
4117
|
+
function durationMs3(startedAt, endedAt) {
|
|
4118
|
+
if (!startedAt || !endedAt) return void 0;
|
|
4119
|
+
const start = Date.parse(startedAt);
|
|
4120
|
+
const end = Date.parse(endedAt);
|
|
4121
|
+
return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : void 0;
|
|
4122
|
+
}
|
|
4123
|
+
function unique4(items) {
|
|
4124
|
+
return [...new Set(items)];
|
|
4125
|
+
}
|
|
4126
|
+
function uniqueFiles(files) {
|
|
4127
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4128
|
+
return files.filter((file) => {
|
|
4129
|
+
const key = path7.resolve(file.filePath);
|
|
4130
|
+
if (seen.has(key)) return false;
|
|
4131
|
+
seen.add(key);
|
|
4132
|
+
return true;
|
|
4133
|
+
}).sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
4134
|
+
}
|
|
4135
|
+
function errorMessage3(error) {
|
|
4136
|
+
return error instanceof Error ? error.message : String(error);
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
// packages/core/src/sources/cursor.ts
|
|
4140
|
+
import fs8 from "node:fs";
|
|
4141
|
+
import os5 from "node:os";
|
|
4142
|
+
import path8 from "node:path";
|
|
4143
|
+
import Database2 from "better-sqlite3";
|
|
4144
|
+
var parserVersion = "cursor-v1";
|
|
4145
|
+
var maxJsonlBytes2 = 20 * 1024 * 1024;
|
|
4146
|
+
var maxDatabaseBytes = 100 * 1024 * 1024;
|
|
4147
|
+
var cursorStoragePrefixes = ["cursor.", "anysphere."];
|
|
4148
|
+
var interestingTerms = ["cursor", "composer", "aichat", "ai_chat", "aiservice", "agent-transcript", "prompttokenbreakdown"];
|
|
4149
|
+
function scanCursor(options) {
|
|
4150
|
+
const cursorHome = options.cursorHome ?? path8.join(os5.homedir(), ".cursor");
|
|
4151
|
+
const discovery = discoverCursorFiles(cursorHome, Boolean(options.cursorHome));
|
|
4152
|
+
const jsonlFiles = discovery.jsonlFiles.filter((filePath) => fileMayOverlapScanWindow3(filePath, options.scanWindow));
|
|
4153
|
+
const databaseFiles = discovery.databaseFiles.filter((filePath) => fileMayOverlapScanWindow3(filePath, options.scanWindow));
|
|
4154
|
+
const jsonlCandidates = jsonlFiles.map((filePath) => cursorJsonlToCandidate(filePath, cursorHome));
|
|
4155
|
+
const databaseCandidates = databaseFiles.flatMap((filePath) => cursorDatabaseToCandidates(filePath, cursorHome, discovery.warnings));
|
|
4156
|
+
const candidates = dedupeCandidates([...jsonlCandidates, ...databaseCandidates]);
|
|
4157
|
+
const sessions = candidates.map((candidate, index) => candidateToUsage(candidate, index, options));
|
|
4158
|
+
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
4159
|
+
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_"))).length;
|
|
4160
|
+
const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_") || error.startsWith("sqlite_json_parse:"))).length;
|
|
4161
|
+
if (!fs8.existsSync(cursorHome)) {
|
|
4162
|
+
discovery.warnings.push(`Cursor home directory not found at ${cursorHome}`);
|
|
4163
|
+
}
|
|
4164
|
+
if (!fs8.existsSync(discovery.statePath)) {
|
|
4165
|
+
discovery.warnings.push(`Cursor global state database not found at ${discovery.statePath}`);
|
|
4166
|
+
}
|
|
4167
|
+
return {
|
|
4168
|
+
source: {
|
|
4169
|
+
id: "cursor",
|
|
4170
|
+
label: "Cursor",
|
|
4171
|
+
available: discovery.jsonlFiles.length > 0 || discovery.databaseFiles.length > 0,
|
|
4172
|
+
paths: unique5([cursorHome, discovery.projectsPath, discovery.statePath, discovery.workspaceStoragePath, ...discovery.roots]),
|
|
4173
|
+
warnings: discovery.warnings
|
|
4174
|
+
},
|
|
4175
|
+
sessions,
|
|
4176
|
+
stats: {
|
|
4177
|
+
sourceId: "cursor",
|
|
4178
|
+
sourceLabel: "Cursor",
|
|
4179
|
+
homePath: cursorHome,
|
|
4180
|
+
cursorHome,
|
|
4181
|
+
globalStoragePath: discovery.globalStoragePath,
|
|
4182
|
+
workspaceStoragePath: discovery.workspaceStoragePath,
|
|
4183
|
+
projectsPath: discovery.projectsPath,
|
|
4184
|
+
statePath: discovery.statePath,
|
|
4185
|
+
projectsExists: fs8.existsSync(discovery.projectsPath),
|
|
4186
|
+
stateExists: fs8.existsSync(discovery.statePath),
|
|
4187
|
+
sessionsExists: jsonlFiles.length > 0,
|
|
4188
|
+
sessionFileCount: jsonlFiles.length,
|
|
4189
|
+
databaseFileCount: databaseFiles.length,
|
|
4190
|
+
skippedFileCount: discovery.skippedFileCount,
|
|
4191
|
+
sessionsImported: sessions.length,
|
|
4192
|
+
parseFailureCount,
|
|
4193
|
+
unreadableFileCount,
|
|
4194
|
+
malformedFileCount,
|
|
4195
|
+
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4196
|
+
}
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
function discoverCursorFiles(cursorHome, explicitHome) {
|
|
4200
|
+
const warnings = [];
|
|
4201
|
+
const appPaths = defaultCursorAppPaths();
|
|
4202
|
+
const roots = explicitHome ? [cursorHome] : unique5([cursorHome, appPaths.statePath, ...cursorOwnedGlobalStorageRoots(appPaths.globalStoragePath), ...cursorOwnedWorkspaceStorageRoots(appPaths.workspaceStoragePath)]);
|
|
4203
|
+
const jsonlFiles = [];
|
|
4204
|
+
const databaseFiles = [];
|
|
4205
|
+
let skippedFileCount = 0;
|
|
4206
|
+
for (const root of roots) {
|
|
4207
|
+
if (!fs8.existsSync(root)) continue;
|
|
4208
|
+
try {
|
|
4209
|
+
if (isReadableRegularFile2(root, maxDatabaseBytes)) {
|
|
4210
|
+
databaseFiles.push(root);
|
|
4211
|
+
continue;
|
|
4212
|
+
}
|
|
4213
|
+
walk4(root, 10, (filePath) => {
|
|
4214
|
+
const lower = filePath.toLowerCase();
|
|
4215
|
+
if (lower.endsWith(".jsonl")) {
|
|
4216
|
+
if (isReadableRegularFile2(filePath, maxJsonlBytes2)) jsonlFiles.push(filePath);
|
|
4217
|
+
else skippedFileCount += 1;
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
if (/\.(sqlite|db|vscdb)$/i.test(filePath)) {
|
|
4221
|
+
if (isReadableRegularFile2(filePath, maxDatabaseBytes)) databaseFiles.push(filePath);
|
|
4222
|
+
else skippedFileCount += 1;
|
|
4223
|
+
}
|
|
4224
|
+
});
|
|
4225
|
+
} catch (error) {
|
|
4226
|
+
warnings.push(`Unable to inspect Cursor path ${root}: ${errorMessage4(error)}`);
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
return {
|
|
4230
|
+
roots,
|
|
4231
|
+
projectsPath: path8.join(cursorHome, "projects"),
|
|
4232
|
+
globalStoragePath: appPaths.globalStoragePath,
|
|
4233
|
+
workspaceStoragePath: appPaths.workspaceStoragePath,
|
|
4234
|
+
statePath: appPaths.statePath,
|
|
4235
|
+
jsonlFiles: unique5(jsonlFiles).sort(cursorJsonlSort),
|
|
4236
|
+
databaseFiles: unique5(databaseFiles).sort(),
|
|
4237
|
+
skippedFileCount,
|
|
4238
|
+
warnings
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
function cursorOwnedGlobalStorageRoots(globalStoragePath) {
|
|
4242
|
+
return cursorOwnedChildDirs(globalStoragePath);
|
|
4243
|
+
}
|
|
4244
|
+
function cursorOwnedWorkspaceStorageRoots(workspaceStoragePath) {
|
|
4245
|
+
if (!fs8.existsSync(workspaceStoragePath)) return [];
|
|
4246
|
+
const roots = [];
|
|
4247
|
+
try {
|
|
4248
|
+
for (const workspace of fs8.readdirSync(workspaceStoragePath, { withFileTypes: true })) {
|
|
4249
|
+
if (!workspace.isDirectory()) continue;
|
|
4250
|
+
roots.push(...cursorOwnedChildDirs(path8.join(workspaceStoragePath, workspace.name)));
|
|
4251
|
+
}
|
|
4252
|
+
} catch {
|
|
4253
|
+
return [];
|
|
4254
|
+
}
|
|
4255
|
+
return roots;
|
|
4256
|
+
}
|
|
4257
|
+
function cursorOwnedChildDirs(root) {
|
|
4258
|
+
if (!fs8.existsSync(root)) return [];
|
|
4259
|
+
try {
|
|
4260
|
+
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));
|
|
4261
|
+
} catch {
|
|
4262
|
+
return [];
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
function defaultCursorAppPaths() {
|
|
4266
|
+
const home = os5.homedir();
|
|
4267
|
+
if (process.platform === "win32") {
|
|
4268
|
+
const appData = process.env.APPDATA ?? path8.join(home, "AppData", "Roaming");
|
|
4269
|
+
const globalStoragePath2 = path8.join(appData, "Cursor", "User", "globalStorage");
|
|
4270
|
+
return {
|
|
4271
|
+
globalStoragePath: globalStoragePath2,
|
|
4272
|
+
workspaceStoragePath: path8.join(appData, "Cursor", "User", "workspaceStorage"),
|
|
4273
|
+
statePath: path8.join(globalStoragePath2, "state.vscdb")
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
if (process.platform === "darwin") {
|
|
4277
|
+
const globalStoragePath2 = path8.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
|
|
4278
|
+
return {
|
|
4279
|
+
globalStoragePath: globalStoragePath2,
|
|
4280
|
+
workspaceStoragePath: path8.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"),
|
|
4281
|
+
statePath: path8.join(globalStoragePath2, "state.vscdb")
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
const xdg = process.env.XDG_CONFIG_HOME || path8.join(home, ".config");
|
|
4285
|
+
const globalStoragePath = path8.join(xdg, "Cursor", "User", "globalStorage");
|
|
4286
|
+
return {
|
|
4287
|
+
globalStoragePath,
|
|
4288
|
+
workspaceStoragePath: path8.join(xdg, "Cursor", "User", "workspaceStorage"),
|
|
4289
|
+
statePath: path8.join(globalStoragePath, "state.vscdb")
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
function walk4(root, maxDepth, onFile, depth = 0) {
|
|
4293
|
+
if (depth > maxDepth) return;
|
|
4294
|
+
for (const entry of fs8.readdirSync(root, { withFileTypes: true })) {
|
|
4295
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "CachedData" || entry.name === "Cache") continue;
|
|
4296
|
+
const fullPath = path8.join(root, entry.name);
|
|
4297
|
+
if (entry.isDirectory()) {
|
|
4298
|
+
walk4(fullPath, maxDepth, onFile, depth + 1);
|
|
4299
|
+
} else if (entry.isFile()) {
|
|
4300
|
+
onFile(fullPath);
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
function isReadableRegularFile2(filePath, maxBytes) {
|
|
4305
|
+
try {
|
|
4306
|
+
fs8.accessSync(filePath, fs8.constants.R_OK);
|
|
4307
|
+
const stat = fs8.statSync(filePath);
|
|
4308
|
+
return stat.isFile() && stat.size <= maxBytes;
|
|
4309
|
+
} catch {
|
|
4310
|
+
return false;
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
function cursorJsonlSort(a, b) {
|
|
4314
|
+
const aTranscript = a.includes(`${path8.sep}agent-transcripts${path8.sep}`) ? 0 : 1;
|
|
4315
|
+
const bTranscript = b.includes(`${path8.sep}agent-transcripts${path8.sep}`) ? 0 : 1;
|
|
4316
|
+
return aTranscript - bTranscript || a.localeCompare(b);
|
|
4317
|
+
}
|
|
4318
|
+
function fileMayOverlapScanWindow3(filePath, scanWindow) {
|
|
4319
|
+
if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
|
|
4320
|
+
const inferredTimestamp = timestampMs3(inferDateFromCursorPath(filePath));
|
|
4321
|
+
if (scanWindow.fromMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp < scanWindow.fromMs) return false;
|
|
4322
|
+
if (scanWindow.toMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp > scanWindow.toMs) return false;
|
|
4323
|
+
if (scanWindow.fromMs === void 0) return true;
|
|
4324
|
+
try {
|
|
4325
|
+
return fs8.statSync(filePath).mtimeMs >= scanWindow.fromMs;
|
|
4326
|
+
} catch {
|
|
3033
4327
|
return true;
|
|
3034
4328
|
}
|
|
3035
4329
|
}
|
|
3036
4330
|
function cursorJsonlToCandidate(filePath, cursorHome) {
|
|
3037
|
-
const breakdown =
|
|
4331
|
+
const breakdown = emptyBreakdown4();
|
|
3038
4332
|
try {
|
|
3039
|
-
const parse =
|
|
4333
|
+
const parse = parseJsonLines4(fs8.readFileSync(filePath, "utf8"));
|
|
3040
4334
|
breakdown.rawEventCount = parse.records.length;
|
|
3041
4335
|
breakdown.parseErrors = parse.errors;
|
|
3042
4336
|
breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
3043
4337
|
for (const record of parse.records) applyCursorRecord(breakdown, record);
|
|
3044
4338
|
finalizeBreakdown(breakdown);
|
|
3045
4339
|
} catch (error) {
|
|
3046
|
-
const message = `unable_to_read_cursor_jsonl:${
|
|
4340
|
+
const message = `unable_to_read_cursor_jsonl:${errorMessage4(error)}`;
|
|
3047
4341
|
breakdown.parseStatus = "failed";
|
|
3048
4342
|
breakdown.parseErrors.push(message);
|
|
3049
4343
|
breakdown.warnings.push(message);
|
|
3050
4344
|
}
|
|
3051
|
-
if (!breakdown.id) breakdown.id =
|
|
3052
|
-
const fallbackDate = inferDateFromCursorPath(filePath) ??
|
|
4345
|
+
if (!breakdown.id) breakdown.id = path8.basename(filePath).replace(/\.jsonl$/i, "") || void 0;
|
|
4346
|
+
const fallbackDate = inferDateFromCursorPath(filePath) ?? fileModifiedDate2(filePath);
|
|
3053
4347
|
if (fallbackDate) {
|
|
3054
4348
|
breakdown.startedAt ??= fallbackDate;
|
|
3055
4349
|
breakdown.endedAt ??= fallbackDate;
|
|
@@ -3079,7 +4373,7 @@ function cursorDatabaseToCandidates(filePath, cursorHome, warnings) {
|
|
|
3079
4373
|
}
|
|
3080
4374
|
}
|
|
3081
4375
|
} catch (error) {
|
|
3082
|
-
warnings.push(`Unable to read Cursor SQLite database at ${filePath}: ${
|
|
4376
|
+
warnings.push(`Unable to read Cursor SQLite database at ${filePath}: ${errorMessage4(error)}`);
|
|
3083
4377
|
} finally {
|
|
3084
4378
|
db?.close();
|
|
3085
4379
|
}
|
|
@@ -3089,7 +4383,7 @@ function safeColumns(db, table, warnings) {
|
|
|
3089
4383
|
try {
|
|
3090
4384
|
return db.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all();
|
|
3091
4385
|
} catch (error) {
|
|
3092
|
-
warnings.push(`Unable to inspect Cursor SQLite table ${table}: ${
|
|
4386
|
+
warnings.push(`Unable to inspect Cursor SQLite table ${table}: ${errorMessage4(error)}`);
|
|
3093
4387
|
return [];
|
|
3094
4388
|
}
|
|
3095
4389
|
}
|
|
@@ -3098,7 +4392,7 @@ function safeRows(db, table, columns, warnings) {
|
|
|
3098
4392
|
const select = columns.map(quoteIdentifier).join(", ");
|
|
3099
4393
|
return db.prepare(`SELECT ${select} FROM ${quoteIdentifier(table)} LIMIT 500`).all();
|
|
3100
4394
|
} catch (error) {
|
|
3101
|
-
warnings.push(`Unable to scan Cursor SQLite table ${table}: ${
|
|
4395
|
+
warnings.push(`Unable to scan Cursor SQLite table ${table}: ${errorMessage4(error)}`);
|
|
3102
4396
|
return [];
|
|
3103
4397
|
}
|
|
3104
4398
|
}
|
|
@@ -3126,11 +4420,11 @@ function candidateFromDatabaseRow(filePath, cursorHome, table, row) {
|
|
|
3126
4420
|
if (!parsed.ok && text.trim().startsWith("{")) parseErrors.push(`sqlite_json_parse:${table}.${key}:${parsed.error}`);
|
|
3127
4421
|
}
|
|
3128
4422
|
if (!records.length) return void 0;
|
|
3129
|
-
const breakdown =
|
|
4423
|
+
const breakdown = emptyBreakdown4();
|
|
3130
4424
|
breakdown.parseErrors = parseErrors;
|
|
3131
4425
|
breakdown.parseStatus = parseErrors.length ? "partial" : "ok";
|
|
3132
4426
|
breakdown.rawEventCount = records.length;
|
|
3133
|
-
breakdown.matchedKeys =
|
|
4427
|
+
breakdown.matchedKeys = unique5(matchedKeys);
|
|
3134
4428
|
for (const record of records) applyCursorRecord(breakdown, record);
|
|
3135
4429
|
finalizeBreakdown(breakdown);
|
|
3136
4430
|
if (isCursorMetadataOnlyBreakdown(breakdown)) return void 0;
|
|
@@ -3155,29 +4449,29 @@ function parseJsonBlob(text) {
|
|
|
3155
4449
|
try {
|
|
3156
4450
|
return { ok: true, value: JSON.parse(trimmed) };
|
|
3157
4451
|
} catch (error) {
|
|
3158
|
-
return { ok: false, error:
|
|
4452
|
+
return { ok: false, error: errorMessage4(error) };
|
|
3159
4453
|
}
|
|
3160
4454
|
}
|
|
3161
|
-
function
|
|
4455
|
+
function parseJsonLines4(text) {
|
|
3162
4456
|
const records = [];
|
|
3163
4457
|
const errors = [];
|
|
3164
4458
|
text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index })).filter(({ line }) => Boolean(line)).forEach(({ line, index }) => {
|
|
3165
4459
|
try {
|
|
3166
4460
|
records.push(JSON.parse(line));
|
|
3167
4461
|
} catch (error) {
|
|
3168
|
-
errors.push(`jsonl_line_${index + 1}:${
|
|
4462
|
+
errors.push(`jsonl_line_${index + 1}:${errorMessage4(error)}`);
|
|
3169
4463
|
}
|
|
3170
4464
|
});
|
|
3171
4465
|
return { records, errors };
|
|
3172
4466
|
}
|
|
3173
4467
|
function applyCursorRecord(breakdown, record) {
|
|
3174
4468
|
const objects = collectObjects(record, 6, 150);
|
|
3175
|
-
const root =
|
|
4469
|
+
const root = firstObject6(record);
|
|
3176
4470
|
const timestamp3 = firstValueByKeys(objects, ["timestamp", "createdAt", "created_at", "lastUpdatedAt", "conversationCheckpointLastUpdatedAt", "time", "date"]);
|
|
3177
|
-
const date =
|
|
4471
|
+
const date = dateValue5(timestamp3);
|
|
3178
4472
|
if (date) {
|
|
3179
|
-
breakdown.startedAt =
|
|
3180
|
-
breakdown.endedAt =
|
|
4473
|
+
breakdown.startedAt = minDate3(breakdown.startedAt, date);
|
|
4474
|
+
breakdown.endedAt = maxDate3(breakdown.endedAt, date);
|
|
3181
4475
|
}
|
|
3182
4476
|
breakdown.id ??= firstStringByKeys(objects, ["sessionId", "session_id", "conversationId", "conversation_id", "composerId", "composer_id", "chatId", "chat_id"]);
|
|
3183
4477
|
breakdown.title ??= firstStringByKeys(objects, ["title", "name", "summary"]);
|
|
@@ -3222,13 +4516,13 @@ function collectObjects(value, maxDepth, maxObjects, objects = []) {
|
|
|
3222
4516
|
for (const child of Object.values(object)) collectObjects(child, maxDepth - 1, maxObjects, objects);
|
|
3223
4517
|
return objects;
|
|
3224
4518
|
}
|
|
3225
|
-
function
|
|
4519
|
+
function firstObject6(value) {
|
|
3226
4520
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3227
4521
|
}
|
|
3228
4522
|
function firstStringByKeys(objects, keys) {
|
|
3229
4523
|
for (const object of objects) {
|
|
3230
4524
|
for (const key of keys) {
|
|
3231
|
-
const value =
|
|
4525
|
+
const value = stringValue5(object[key]);
|
|
3232
4526
|
if (value) return value;
|
|
3233
4527
|
}
|
|
3234
4528
|
}
|
|
@@ -3251,7 +4545,7 @@ function firstPathByKeys(objects, keys) {
|
|
|
3251
4545
|
function firstNumberByKeys(objects, keys) {
|
|
3252
4546
|
for (const object of objects) {
|
|
3253
4547
|
for (const key of keys) {
|
|
3254
|
-
const value =
|
|
4548
|
+
const value = numberValue3(object[key]);
|
|
3255
4549
|
if (value !== void 0) return value;
|
|
3256
4550
|
}
|
|
3257
4551
|
}
|
|
@@ -3261,7 +4555,7 @@ function inferRole(objects, root) {
|
|
|
3261
4555
|
const role = firstStringByKeys(objects, ["role", "author", "speaker", "from"])?.toLowerCase();
|
|
3262
4556
|
if (role === "user" || role === "human") return "user";
|
|
3263
4557
|
if (role === "assistant" || role === "ai" || role === "agent") return "assistant";
|
|
3264
|
-
const type = (
|
|
4558
|
+
const type = (stringValue5(root?.type) ?? stringValue5(root?.event) ?? "").toLowerCase();
|
|
3265
4559
|
if (type.includes("user") || type.includes("prompt")) return "user";
|
|
3266
4560
|
if (type.includes("assistant") || type.includes("response") || type.includes("completion")) return "assistant";
|
|
3267
4561
|
return void 0;
|
|
@@ -3282,9 +4576,9 @@ function textFromUnknown(value) {
|
|
|
3282
4576
|
const parts = value.map(textFromUnknown).filter((item) => Boolean(item));
|
|
3283
4577
|
return parts.length ? parts.join("\n") : void 0;
|
|
3284
4578
|
}
|
|
3285
|
-
const object =
|
|
4579
|
+
const object = firstObject6(value);
|
|
3286
4580
|
if (!object) return void 0;
|
|
3287
|
-
return
|
|
4581
|
+
return stringValue5(object.text) ?? stringValue5(object.content) ?? stringValue5(object.value);
|
|
3288
4582
|
}
|
|
3289
4583
|
function extractUsage(objects) {
|
|
3290
4584
|
const usageObjects = objects.filter((object) => Object.keys(object).some((key) => key.toLowerCase().includes("token") || key.toLowerCase() === "usage"));
|
|
@@ -3299,7 +4593,7 @@ function extractUsage(objects) {
|
|
|
3299
4593
|
};
|
|
3300
4594
|
}
|
|
3301
4595
|
function applyToolSignals(breakdown, objects) {
|
|
3302
|
-
const typeText = objects.flatMap((object) => [
|
|
4596
|
+
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
4597
|
if (typeText.includes("tool")) breakdown.toolCallCount += 1;
|
|
3304
4598
|
if (typeText.includes("terminal") || typeText.includes("shell") || typeText.includes("command")) breakdown.shellCommandCount += 1;
|
|
3305
4599
|
if (typeText.includes("read")) breakdown.fileReadCount += 1;
|
|
@@ -3308,9 +4602,9 @@ function applyToolSignals(breakdown, objects) {
|
|
|
3308
4602
|
}
|
|
3309
4603
|
function looksFailed(object) {
|
|
3310
4604
|
if (object.isError === true || object.is_error === true || object.error === true) return true;
|
|
3311
|
-
const status =
|
|
4605
|
+
const status = stringValue5(object.status)?.toLowerCase();
|
|
3312
4606
|
if (status === "failed" || status === "error") return true;
|
|
3313
|
-
const exitCode =
|
|
4607
|
+
const exitCode = numberValue3(object.exitCode ?? object.exit_code ?? object.statusCode ?? object.status_code);
|
|
3314
4608
|
return exitCode !== void 0 && exitCode !== 0;
|
|
3315
4609
|
}
|
|
3316
4610
|
function finalizeBreakdown(breakdown) {
|
|
@@ -3323,7 +4617,7 @@ function finalizeBreakdown(breakdown) {
|
|
|
3323
4617
|
}
|
|
3324
4618
|
function candidateToUsage(candidate, index, options) {
|
|
3325
4619
|
const breakdown = candidate.breakdown;
|
|
3326
|
-
const id = breakdown.id ?? `${
|
|
4620
|
+
const id = breakdown.id ?? `${path8.basename(candidate.sourcePath).replace(/\.(jsonl|sqlite|db|vscdb)$/i, "") || "cursor-session"}-${index}`;
|
|
3327
4621
|
const cwd = breakdown.cwd ?? candidate.fallbackCwd ?? process.cwd();
|
|
3328
4622
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
3329
4623
|
const hasTokenBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
|
|
@@ -3358,7 +4652,7 @@ function candidateToUsage(candidate, index, options) {
|
|
|
3358
4652
|
messageCount: breakdown.messageCount,
|
|
3359
4653
|
rawTokenTotal: breakdown.totalTokens || void 0,
|
|
3360
4654
|
warnings: [...repo.warnings, ...breakdown.warnings, ...tokenWarnings, ...hasTokenBreakdown ? [] : ["missing_token_breakdown"]],
|
|
3361
|
-
durationMs:
|
|
4655
|
+
durationMs: durationMs4(breakdown.startedAt, breakdown.endedAt),
|
|
3362
4656
|
rawEventCount: breakdown.rawEventCount,
|
|
3363
4657
|
parseStatus: breakdown.parseStatus,
|
|
3364
4658
|
parseErrors: breakdown.parseErrors,
|
|
@@ -3402,36 +4696,36 @@ function candidateToUsage(candidate, index, options) {
|
|
|
3402
4696
|
};
|
|
3403
4697
|
}
|
|
3404
4698
|
function inferProjectPathFromCursorPath(filePath, cursorHome) {
|
|
3405
|
-
const parts = filePath.split(
|
|
4699
|
+
const parts = filePath.split(path8.sep);
|
|
3406
4700
|
const projectsIndex = parts.lastIndexOf("projects");
|
|
3407
4701
|
if (projectsIndex >= 0 && projectsIndex < parts.length - 1) {
|
|
3408
4702
|
const projectId = parts[projectsIndex + 1];
|
|
3409
4703
|
const decodedProjectPath = decodeCursorProjectId(projectId);
|
|
3410
4704
|
if (decodedProjectPath) return decodedProjectPath;
|
|
3411
4705
|
if (projectId?.startsWith("-")) {
|
|
3412
|
-
return `${
|
|
4706
|
+
return `${path8.sep}${projectId.slice(1).split("-").filter(Boolean).join(path8.sep)}`;
|
|
3413
4707
|
}
|
|
3414
|
-
if (projectId) return
|
|
4708
|
+
if (projectId) return path8.join(cursorHome, "projects", projectId);
|
|
3415
4709
|
}
|
|
3416
4710
|
return void 0;
|
|
3417
4711
|
}
|
|
3418
4712
|
function inferDateFromCursorPath(filePath) {
|
|
3419
|
-
const parts = filePath.split(
|
|
4713
|
+
const parts = filePath.split(path8.sep);
|
|
3420
4714
|
for (const part of parts) {
|
|
3421
4715
|
if (!/^\d{10,13}$/.test(part)) continue;
|
|
3422
|
-
const date =
|
|
4716
|
+
const date = dateValue5(Number(part));
|
|
3423
4717
|
if (date) return date;
|
|
3424
4718
|
}
|
|
3425
4719
|
return void 0;
|
|
3426
4720
|
}
|
|
3427
|
-
function
|
|
4721
|
+
function timestampMs3(value) {
|
|
3428
4722
|
if (!value) return void 0;
|
|
3429
4723
|
const timestamp3 = new Date(value).getTime();
|
|
3430
4724
|
return Number.isFinite(timestamp3) ? timestamp3 : void 0;
|
|
3431
4725
|
}
|
|
3432
|
-
function
|
|
4726
|
+
function fileModifiedDate2(filePath) {
|
|
3433
4727
|
try {
|
|
3434
|
-
return
|
|
4728
|
+
return fs8.statSync(filePath).mtime.toISOString();
|
|
3435
4729
|
} catch {
|
|
3436
4730
|
return void 0;
|
|
3437
4731
|
}
|
|
@@ -3445,24 +4739,24 @@ function decodeCursorProjectId(projectId) {
|
|
|
3445
4739
|
if (!root) return void 0;
|
|
3446
4740
|
const matched = matchEncodedPath(root.path, tokens2.slice(root.consumed));
|
|
3447
4741
|
if (matched) return matched;
|
|
3448
|
-
return
|
|
4742
|
+
return path8.join(root.path, ...tokens2.slice(root.consumed));
|
|
3449
4743
|
}
|
|
3450
4744
|
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:
|
|
4745
|
+
if (tokens2[0] === "Users" && tokens2[1]) return { path: path8.join(path8.sep, "Users", tokens2[1]), consumed: 2 };
|
|
4746
|
+
if (tokens2[0] === "home" && tokens2[1]) return { path: path8.join(path8.sep, "home", tokens2[1]), consumed: 2 };
|
|
4747
|
+
if (tokens2[0] === "tmp") return { path: path8.join(path8.sep, "tmp"), consumed: 1 };
|
|
3454
4748
|
if (/^[A-Za-z]$/.test(tokens2[0] ?? "") && tokens2[1]) return { path: `${tokens2[0]?.toUpperCase()}:\\`, consumed: 1 };
|
|
3455
4749
|
return void 0;
|
|
3456
4750
|
}
|
|
3457
4751
|
function matchEncodedPath(root, tokens2) {
|
|
3458
|
-
if (!tokens2.length) return
|
|
3459
|
-
if (!
|
|
4752
|
+
if (!tokens2.length) return fs8.existsSync(root) ? root : void 0;
|
|
4753
|
+
if (!fs8.existsSync(root)) return void 0;
|
|
3460
4754
|
try {
|
|
3461
|
-
for (const entry of
|
|
4755
|
+
for (const entry of fs8.readdirSync(root, { withFileTypes: true })) {
|
|
3462
4756
|
if (!entry.isDirectory()) continue;
|
|
3463
4757
|
const entryTokens = entry.name.split("-").filter(Boolean);
|
|
3464
4758
|
if (!startsWithTokens(tokens2, entryTokens)) continue;
|
|
3465
|
-
const matched = matchEncodedPath(
|
|
4759
|
+
const matched = matchEncodedPath(path8.join(root, entry.name), tokens2.slice(entryTokens.length));
|
|
3466
4760
|
if (matched) return matched;
|
|
3467
4761
|
}
|
|
3468
4762
|
} catch {
|
|
@@ -3475,8 +4769,8 @@ function startsWithTokens(tokens2, prefix) {
|
|
|
3475
4769
|
}
|
|
3476
4770
|
function inferSurfaceFromPath(filePath) {
|
|
3477
4771
|
const lower = filePath.toLowerCase();
|
|
3478
|
-
if (lower.includes(`${
|
|
3479
|
-
if (lower.includes(`${
|
|
4772
|
+
if (lower.includes(`${path8.sep}agent-transcripts${path8.sep}`) || lower.endsWith(".jsonl")) return "cli";
|
|
4773
|
+
if (lower.includes(`${path8.sep}workspaceStorage${path8.sep}`.toLowerCase()) || lower.includes(`${path8.sep}globalStorage${path8.sep}`.toLowerCase()) || lower.endsWith(".vscdb")) return "desktop";
|
|
3480
4774
|
return "unknown";
|
|
3481
4775
|
}
|
|
3482
4776
|
function cursorSourceAppLabel(surface) {
|
|
@@ -3502,7 +4796,7 @@ function inferOutcome3(breakdown) {
|
|
|
3502
4796
|
if (breakdown.userPromptCount > 0 || breakdown.assistantMessageCount > 0) return "research_only";
|
|
3503
4797
|
return "unknown";
|
|
3504
4798
|
}
|
|
3505
|
-
function
|
|
4799
|
+
function emptyBreakdown4() {
|
|
3506
4800
|
return {
|
|
3507
4801
|
id: void 0,
|
|
3508
4802
|
title: void 0,
|
|
@@ -3585,10 +4879,10 @@ function mergeCursorBreakdowns(primary, secondary) {
|
|
|
3585
4879
|
fileEditCount: Math.max(primary.fileEditCount, secondary.fileEditCount),
|
|
3586
4880
|
rawEventCount: Math.max(primary.rawEventCount, secondary.rawEventCount),
|
|
3587
4881
|
parseStatus: mergeParseStatus(primary.parseStatus, secondary.parseStatus),
|
|
3588
|
-
parseErrors:
|
|
4882
|
+
parseErrors: unique5([...primary.parseErrors, ...secondary.parseErrors]),
|
|
3589
4883
|
promptTimeline: primary.promptTimeline.length >= secondary.promptTimeline.length ? primary.promptTimeline : secondary.promptTimeline,
|
|
3590
|
-
warnings:
|
|
3591
|
-
matchedKeys:
|
|
4884
|
+
warnings: unique5([...primary.warnings, ...secondary.warnings, "cursor_duplicate_session_merged"]),
|
|
4885
|
+
matchedKeys: unique5([...primary.matchedKeys, ...secondary.matchedKeys])
|
|
3592
4886
|
};
|
|
3593
4887
|
}
|
|
3594
4888
|
function mergeParseStatus(a, b) {
|
|
@@ -3597,10 +4891,10 @@ function mergeParseStatus(a, b) {
|
|
|
3597
4891
|
return "ok";
|
|
3598
4892
|
}
|
|
3599
4893
|
function optionalMinDate(current, candidate) {
|
|
3600
|
-
return candidate ?
|
|
4894
|
+
return candidate ? minDate3(current, candidate) : current;
|
|
3601
4895
|
}
|
|
3602
4896
|
function optionalMaxDate(current, candidate) {
|
|
3603
|
-
return candidate ?
|
|
4897
|
+
return candidate ? maxDate3(current, candidate) : current;
|
|
3604
4898
|
}
|
|
3605
4899
|
function providerForModel2(model) {
|
|
3606
4900
|
const normalized = model.toLowerCase();
|
|
@@ -3609,7 +4903,7 @@ function providerForModel2(model) {
|
|
|
3609
4903
|
if (normalized.startsWith("gpt") || normalized.startsWith("o")) return "openai";
|
|
3610
4904
|
return void 0;
|
|
3611
4905
|
}
|
|
3612
|
-
function
|
|
4906
|
+
function stringValue5(value) {
|
|
3613
4907
|
return typeof value === "string" && value.trim() ? value : void 0;
|
|
3614
4908
|
}
|
|
3615
4909
|
function textValue(value) {
|
|
@@ -3617,24 +4911,24 @@ function textValue(value) {
|
|
|
3617
4911
|
if (Buffer.isBuffer(value)) return value.toString("utf8");
|
|
3618
4912
|
return void 0;
|
|
3619
4913
|
}
|
|
3620
|
-
function
|
|
4914
|
+
function numberValue3(value) {
|
|
3621
4915
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3622
4916
|
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
|
|
3623
4917
|
return void 0;
|
|
3624
4918
|
}
|
|
3625
|
-
function
|
|
4919
|
+
function dateValue5(value) {
|
|
3626
4920
|
if (typeof value === "number" && Number.isFinite(value)) return new Date(value < 1e10 ? value * 1e3 : value).toISOString();
|
|
3627
4921
|
if (typeof value !== "string") return void 0;
|
|
3628
4922
|
const date = new Date(value);
|
|
3629
4923
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
3630
4924
|
}
|
|
3631
|
-
function
|
|
4925
|
+
function minDate3(current, candidate) {
|
|
3632
4926
|
return !current || candidate < current ? candidate : current;
|
|
3633
4927
|
}
|
|
3634
|
-
function
|
|
4928
|
+
function maxDate3(current, candidate) {
|
|
3635
4929
|
return !current || candidate > current ? candidate : current;
|
|
3636
4930
|
}
|
|
3637
|
-
function
|
|
4931
|
+
function durationMs4(startedAt, endedAt) {
|
|
3638
4932
|
if (!startedAt || !endedAt) return void 0;
|
|
3639
4933
|
const start = new Date(startedAt).getTime();
|
|
3640
4934
|
const end = new Date(endedAt).getTime();
|
|
@@ -3646,44 +4940,52 @@ function normalizeTimelineText3(text) {
|
|
|
3646
4940
|
function roundCurrency2(value) {
|
|
3647
4941
|
return Number(value.toFixed(6));
|
|
3648
4942
|
}
|
|
3649
|
-
function
|
|
4943
|
+
function unique5(values) {
|
|
3650
4944
|
return [...new Set(values.filter(Boolean))];
|
|
3651
4945
|
}
|
|
3652
|
-
function
|
|
4946
|
+
function errorMessage4(error) {
|
|
3653
4947
|
return error instanceof Error ? error.message : String(error);
|
|
3654
4948
|
}
|
|
3655
4949
|
|
|
3656
4950
|
// packages/core/src/sources/index.ts
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
}
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
4951
|
+
var codexAdapter = { id: "codex", scan: scanCodex };
|
|
4952
|
+
var claudeAdapter = { id: "claude", scan: scanClaude };
|
|
4953
|
+
var copilotAdapter = { id: "copilot", scan: scanCopilot };
|
|
4954
|
+
var cursorAdapter = { id: "cursor", scan: scanCursor };
|
|
4955
|
+
var adapters = [
|
|
4956
|
+
{ adapter: codexAdapter, homeKey: "codexHome", homeOption: "codexHome" },
|
|
4957
|
+
{ adapter: claudeAdapter, homeKey: "claudeHome", homeOption: "claudeHome" },
|
|
4958
|
+
{ adapter: copilotAdapter, homeKey: "copilotHome", homeOption: "copilotHome" },
|
|
4959
|
+
{
|
|
4960
|
+
adapter: cursorAdapter,
|
|
4961
|
+
homeKey: "cursorHome",
|
|
4962
|
+
homeOption: "cursorHome",
|
|
4963
|
+
enabled: (options) => options.config?.experimentalSources?.cursor === true || Boolean(options.cursorHome)
|
|
4964
|
+
}
|
|
4965
|
+
];
|
|
4966
|
+
function scanUsageSources(options) {
|
|
4967
|
+
const sources = [];
|
|
4968
|
+
const sourceStats = [];
|
|
4969
|
+
const sessions = [];
|
|
4970
|
+
for (const entry of adapters) {
|
|
4971
|
+
if (entry.enabled && !entry.enabled(options)) continue;
|
|
4972
|
+
const home = options[entry.homeKey];
|
|
4973
|
+
const result = entry.adapter.scan({
|
|
4974
|
+
...home ? { [entry.homeOption]: home } : {},
|
|
4975
|
+
...options.config ? { config: options.config } : {},
|
|
4976
|
+
...options.scanWindow ? { scanWindow: options.scanWindow } : {},
|
|
4977
|
+
pricing: options.pricing
|
|
4978
|
+
});
|
|
4979
|
+
sources.push(result.source);
|
|
4980
|
+
sourceStats.push(result.stats);
|
|
4981
|
+
sessions.push(...result.sessions);
|
|
4982
|
+
}
|
|
4983
|
+
return { sources, sourceStats, sessions };
|
|
3682
4984
|
}
|
|
3683
4985
|
|
|
3684
4986
|
// apps/server/src/data.ts
|
|
3685
|
-
import
|
|
3686
|
-
import
|
|
4987
|
+
import fs9 from "node:fs";
|
|
4988
|
+
import path9 from "node:path";
|
|
3687
4989
|
import { spawn } from "node:child_process";
|
|
3688
4990
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3689
4991
|
var scanCacheTtlMs = 1e4;
|
|
@@ -3810,8 +5112,8 @@ function writePricingData(models) {
|
|
|
3810
5112
|
}
|
|
3811
5113
|
function clearRepoSpendLocalData() {
|
|
3812
5114
|
const target = repospendHome();
|
|
3813
|
-
const removed =
|
|
3814
|
-
|
|
5115
|
+
const removed = fs9.existsSync(target);
|
|
5116
|
+
fs9.rmSync(target, { recursive: true, force: true });
|
|
3815
5117
|
clearScanCache();
|
|
3816
5118
|
return { path: target, removed };
|
|
3817
5119
|
}
|
|
@@ -3820,11 +5122,11 @@ function clearRepoSpendParseCache() {
|
|
|
3820
5122
|
clearScanCache();
|
|
3821
5123
|
return result;
|
|
3822
5124
|
}
|
|
3823
|
-
function exportJson() {
|
|
3824
|
-
return JSON.stringify(readDashboardData().sessions, null, 2);
|
|
5125
|
+
function exportJson(filters = {}, options = {}) {
|
|
5126
|
+
return JSON.stringify(readDashboardData(filters, options).sessions, null, 2);
|
|
3825
5127
|
}
|
|
3826
|
-
function exportCsv() {
|
|
3827
|
-
return toCsv(readDashboardData().sessions);
|
|
5128
|
+
function exportCsv(filters = {}, options = {}) {
|
|
5129
|
+
return toCsv(readDashboardData(filters, options).sessions);
|
|
3828
5130
|
}
|
|
3829
5131
|
function stringQuery(value) {
|
|
3830
5132
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -3844,6 +5146,7 @@ function displaySessions(sessions, options) {
|
|
|
3844
5146
|
function sourceScopedAppLabel(session) {
|
|
3845
5147
|
const app = session.sourceApp || fallbackSurfaceLabel(session.detectedSurface);
|
|
3846
5148
|
if (!app || app === "Unknown") return sourceClientLabel(session.sourceClient);
|
|
5149
|
+
if (session.sourceClient === "copilot" && app === "Copilot CLI") return "GitHub Copilot CLI";
|
|
3847
5150
|
if (!shouldScopeApp(app)) return app;
|
|
3848
5151
|
return `${sourceClientLabel(session.sourceClient)} on ${app}`;
|
|
3849
5152
|
}
|
|
@@ -3871,10 +5174,10 @@ function warmCacheArgs(filters, options) {
|
|
|
3871
5174
|
return args2;
|
|
3872
5175
|
}
|
|
3873
5176
|
function cliEntryPath() {
|
|
3874
|
-
const currentDir =
|
|
3875
|
-
const sourceCli =
|
|
3876
|
-
if (
|
|
3877
|
-
return
|
|
5177
|
+
const currentDir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
5178
|
+
const sourceCli = path9.join(currentDir, "cli.ts");
|
|
5179
|
+
if (fs9.existsSync(sourceCli)) return sourceCli;
|
|
5180
|
+
return path9.join(currentDir, "cli.js");
|
|
3878
5181
|
}
|
|
3879
5182
|
function shouldScopeApp(app) {
|
|
3880
5183
|
const normalized = app.toLowerCase();
|
|
@@ -3883,6 +5186,7 @@ function shouldScopeApp(app) {
|
|
|
3883
5186
|
function sourceClientLabel(source) {
|
|
3884
5187
|
if (source === "codex") return "Codex";
|
|
3885
5188
|
if (source === "claude") return "Claude Code";
|
|
5189
|
+
if (source === "copilot") return "GitHub Copilot";
|
|
3886
5190
|
if (source === "gemini-cli") return "Gemini CLI";
|
|
3887
5191
|
if (source === "opencode") return "OpenCode";
|
|
3888
5192
|
if (source === "cursor") return "Cursor";
|
|
@@ -3900,83 +5204,74 @@ function runtimeCwd() {
|
|
|
3900
5204
|
return process.env.INIT_CWD || process.cwd();
|
|
3901
5205
|
}
|
|
3902
5206
|
|
|
5207
|
+
// apps/server/src/cli-options.ts
|
|
5208
|
+
function parseCliFilters(args2) {
|
|
5209
|
+
const filters = {};
|
|
5210
|
+
const source = listOption(args2, ["--source"]).filter((value) => value !== "all");
|
|
5211
|
+
const sourceApp = listOption(args2, ["--sourceApp", "--source-app", "--app"]);
|
|
5212
|
+
const repo = listOption(args2, ["--repo"]);
|
|
5213
|
+
const model = listOption(args2, ["--model"]);
|
|
5214
|
+
const from = valueAfter(args2, "--from");
|
|
5215
|
+
const to = valueAfter(args2, "--to");
|
|
5216
|
+
if (source.length) filters.source = source;
|
|
5217
|
+
if (sourceApp.length) filters.sourceApp = sourceApp;
|
|
5218
|
+
if (repo.length) filters.repo = repo;
|
|
5219
|
+
if (model.length) filters.model = model;
|
|
5220
|
+
if (from) filters.from = from;
|
|
5221
|
+
if (to) filters.to = to;
|
|
5222
|
+
return filters;
|
|
5223
|
+
}
|
|
5224
|
+
function parseCliDateFilters(args2) {
|
|
5225
|
+
const filters = {};
|
|
5226
|
+
const from = valueAfter(args2, "--from");
|
|
5227
|
+
const to = valueAfter(args2, "--to");
|
|
5228
|
+
if (from) filters.from = from;
|
|
5229
|
+
if (to) filters.to = to;
|
|
5230
|
+
return filters;
|
|
5231
|
+
}
|
|
5232
|
+
function parseCliDashboardOptions(args2) {
|
|
5233
|
+
return {
|
|
5234
|
+
splitSourceApps: args2.includes("--splitSourceApps") || args2.includes("--split-source-apps")
|
|
5235
|
+
};
|
|
5236
|
+
}
|
|
5237
|
+
function valueAfter(args2, flag) {
|
|
5238
|
+
const index = args2.indexOf(flag);
|
|
5239
|
+
const value = index >= 0 ? args2[index + 1] : void 0;
|
|
5240
|
+
return value && !value.startsWith("--") ? value : void 0;
|
|
5241
|
+
}
|
|
5242
|
+
function listOption(args2, flags) {
|
|
5243
|
+
const values = [];
|
|
5244
|
+
for (const flag of flags) {
|
|
5245
|
+
args2.forEach((arg, index) => {
|
|
5246
|
+
if (arg === flag) {
|
|
5247
|
+
const value = args2[index + 1];
|
|
5248
|
+
if (value && !value.startsWith("--")) values.push(value);
|
|
5249
|
+
} else if (arg.startsWith(`${flag}=`)) {
|
|
5250
|
+
values.push(arg.slice(flag.length + 1));
|
|
5251
|
+
}
|
|
5252
|
+
});
|
|
5253
|
+
}
|
|
5254
|
+
return [...new Set(values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean))];
|
|
5255
|
+
}
|
|
5256
|
+
|
|
3903
5257
|
// apps/server/src/server.ts
|
|
3904
|
-
import
|
|
3905
|
-
import
|
|
5258
|
+
import fs10 from "node:fs";
|
|
5259
|
+
import path10 from "node:path";
|
|
3906
5260
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3907
5261
|
import fastifyStatic from "@fastify/static";
|
|
3908
5262
|
import Fastify from "fastify";
|
|
3909
5263
|
|
|
3910
|
-
// apps/server/src/demo
|
|
5264
|
+
// apps/server/src/demo/fixtures.ts
|
|
3911
5265
|
var baseDate = "2026-05-18";
|
|
3912
|
-
var demoPricing =
|
|
3913
|
-
|
|
3914
|
-
"gpt-5.3-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
3915
|
-
"gpt-5.3-codex-spark": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
3916
|
-
"claude-haiku-4-5-20251001": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 }
|
|
3917
|
-
};
|
|
3918
|
-
function demoModeEnabled() {
|
|
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() {
|
|
5266
|
+
var demoPricing = defaultPricing;
|
|
5267
|
+
function issue(command2, category, severity, impact, reason) {
|
|
3930
5268
|
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
|
|
5269
|
+
command: command2,
|
|
5270
|
+
category,
|
|
5271
|
+
classification: severity === "critical" ? "blocking_failure" : "warning",
|
|
5272
|
+
severity,
|
|
5273
|
+
impact,
|
|
5274
|
+
reason
|
|
3980
5275
|
};
|
|
3981
5276
|
}
|
|
3982
5277
|
var demoSources = [
|
|
@@ -3993,6 +5288,17 @@ var demoSources = [
|
|
|
3993
5288
|
available: true,
|
|
3994
5289
|
paths: ["/demo/middle-earth/claude/projects"],
|
|
3995
5290
|
warnings: []
|
|
5291
|
+
},
|
|
5292
|
+
{
|
|
5293
|
+
id: "copilot",
|
|
5294
|
+
label: "GitHub Copilot",
|
|
5295
|
+
available: true,
|
|
5296
|
+
paths: [
|
|
5297
|
+
"/demo/middle-earth/copilot/otel",
|
|
5298
|
+
"/demo/middle-earth/copilot/session-state",
|
|
5299
|
+
"/demo/middle-earth/vscode/User/workspaceStorage"
|
|
5300
|
+
],
|
|
5301
|
+
warnings: []
|
|
3996
5302
|
}
|
|
3997
5303
|
];
|
|
3998
5304
|
var demoSourceStats = [
|
|
@@ -4024,6 +5330,26 @@ var demoSourceStats = [
|
|
|
4024
5330
|
malformedFileCount: 0,
|
|
4025
5331
|
projectDirCount: 4,
|
|
4026
5332
|
lastScannedAt: "2026-05-18T19:01:00.000Z"
|
|
5333
|
+
},
|
|
5334
|
+
{
|
|
5335
|
+
sourceId: "copilot",
|
|
5336
|
+
sourceLabel: "GitHub Copilot",
|
|
5337
|
+
homePath: "/demo/middle-earth/copilot",
|
|
5338
|
+
copilotHome: "/demo/middle-earth/copilot",
|
|
5339
|
+
otelPath: "/demo/middle-earth/copilot/otel",
|
|
5340
|
+
workspaceStoragePath: "/demo/middle-earth/vscode/User/workspaceStorage",
|
|
5341
|
+
otelExists: true,
|
|
5342
|
+
sessionsExists: true,
|
|
5343
|
+
otelFileCount: 5,
|
|
5344
|
+
transcriptFileCount: 3,
|
|
5345
|
+
debugLogFileCount: 2,
|
|
5346
|
+
sessionStateFileCount: 2,
|
|
5347
|
+
sessionFileCount: 10,
|
|
5348
|
+
sessionsImported: 3,
|
|
5349
|
+
parseFailureCount: 0,
|
|
5350
|
+
unreadableFileCount: 0,
|
|
5351
|
+
malformedFileCount: 0,
|
|
5352
|
+
lastScannedAt: "2026-05-18T19:02:00.000Z"
|
|
4027
5353
|
}
|
|
4028
5354
|
];
|
|
4029
5355
|
var demoInputs = [
|
|
@@ -4095,11 +5421,11 @@ var demoInputs = [
|
|
|
4095
5421
|
{
|
|
4096
5422
|
id: "lotr-session-003",
|
|
4097
5423
|
repo: "shire-mobile",
|
|
4098
|
-
title: "Investigate token spike after second breakfast",
|
|
5424
|
+
title: "Investigate Opus 4.8 token spike after second breakfast",
|
|
4099
5425
|
sourceClient: "claude",
|
|
4100
5426
|
sourceApp: "Claude Code",
|
|
4101
5427
|
sourceAppRaw: "claude-code",
|
|
4102
|
-
model: "claude-opus-4-
|
|
5428
|
+
model: "claude-opus-4-8",
|
|
4103
5429
|
provider: "anthropic",
|
|
4104
5430
|
hour: 11,
|
|
4105
5431
|
minute: 5,
|
|
@@ -4178,11 +5504,11 @@ var demoInputs = [
|
|
|
4178
5504
|
{
|
|
4179
5505
|
id: "lotr-session-006",
|
|
4180
5506
|
repo: "rivendell-dashboard",
|
|
4181
|
-
title: "Refactor the council-of-elrond planner",
|
|
5507
|
+
title: "Refactor the council-of-elrond planner with Fable 5",
|
|
4182
5508
|
sourceClient: "claude",
|
|
4183
5509
|
sourceApp: "Claude Desktop App",
|
|
4184
5510
|
sourceAppRaw: "claude-desktop-local-agent",
|
|
4185
|
-
model: "claude-
|
|
5511
|
+
model: "claude-fable-5",
|
|
4186
5512
|
provider: "anthropic",
|
|
4187
5513
|
hour: 14,
|
|
4188
5514
|
minute: 15,
|
|
@@ -4200,6 +5526,32 @@ var demoInputs = [
|
|
|
4200
5526
|
outcome: "completed",
|
|
4201
5527
|
branch: "planner/elrond-v2"
|
|
4202
5528
|
},
|
|
5529
|
+
{
|
|
5530
|
+
id: "lotr-session-015",
|
|
5531
|
+
repo: "rivendell-dashboard",
|
|
5532
|
+
title: "Use Copilot to draft council agenda actions",
|
|
5533
|
+
sourceClient: "copilot",
|
|
5534
|
+
sourceApp: "VS Code",
|
|
5535
|
+
sourceAppRaw: "vscode.copilot-chat",
|
|
5536
|
+
model: "lark-vscode-preview",
|
|
5537
|
+
provider: "github-copilot",
|
|
5538
|
+
hour: 14,
|
|
5539
|
+
minute: 55,
|
|
5540
|
+
durationMinutes: 36,
|
|
5541
|
+
inputTokens: 51e4,
|
|
5542
|
+
cachedInputTokens: 18e4,
|
|
5543
|
+
outputTokens: 74e3,
|
|
5544
|
+
reasoningTokens: 0,
|
|
5545
|
+
prompts: 4,
|
|
5546
|
+
replies: 7,
|
|
5547
|
+
tools: 24,
|
|
5548
|
+
commands: 6,
|
|
5549
|
+
edits: 6,
|
|
5550
|
+
reads: 29,
|
|
5551
|
+
outcome: "completed",
|
|
5552
|
+
branch: "planner/copilot-actions",
|
|
5553
|
+
detectedSurface: "vscode_extension"
|
|
5554
|
+
},
|
|
4203
5555
|
{
|
|
4204
5556
|
id: "lotr-session-007",
|
|
4205
5557
|
repo: "palantir-observability",
|
|
@@ -4226,6 +5578,39 @@ var demoInputs = [
|
|
|
4226
5578
|
harmlessNonZero: 2,
|
|
4227
5579
|
exploratoryMisses: 3
|
|
4228
5580
|
},
|
|
5581
|
+
{
|
|
5582
|
+
id: "lotr-session-016",
|
|
5583
|
+
repo: "palantir-observability",
|
|
5584
|
+
title: "Trace Copilot alert summaries through the seeing-stone feed",
|
|
5585
|
+
sourceClient: "copilot",
|
|
5586
|
+
sourceApp: "Copilot CLI",
|
|
5587
|
+
sourceAppRaw: "copilot-cli",
|
|
5588
|
+
model: "copilot/gpt-5-mini",
|
|
5589
|
+
provider: "openai",
|
|
5590
|
+
hour: 16,
|
|
5591
|
+
minute: 35,
|
|
5592
|
+
durationMinutes: 43,
|
|
5593
|
+
inputTokens: 69e4,
|
|
5594
|
+
cachedInputTokens: 21e4,
|
|
5595
|
+
outputTokens: 88e3,
|
|
5596
|
+
reasoningTokens: 0,
|
|
5597
|
+
prompts: 5,
|
|
5598
|
+
replies: 8,
|
|
5599
|
+
tools: 30,
|
|
5600
|
+
commands: 13,
|
|
5601
|
+
edits: 3,
|
|
5602
|
+
reads: 34,
|
|
5603
|
+
outcome: "partial",
|
|
5604
|
+
branch: "alerts/copilot-summaries",
|
|
5605
|
+
warnings: ["copilot_partial_token_breakdown"],
|
|
5606
|
+
importantFailures: 1,
|
|
5607
|
+
topFailureType: "test",
|
|
5608
|
+
detectedSurface: "terminal_cli",
|
|
5609
|
+
tokenConfidence: "medium",
|
|
5610
|
+
issueSamples: [
|
|
5611
|
+
issue("pnpm test --filter palantir-alerts", "test", "warning", "medium", "A generated summary snapshot changed before the matcher was tightened.")
|
|
5612
|
+
]
|
|
5613
|
+
},
|
|
4229
5614
|
{
|
|
4230
5615
|
id: "lotr-session-008",
|
|
4231
5616
|
repo: "mordor-ci",
|
|
@@ -4255,11 +5640,11 @@ var demoInputs = [
|
|
|
4255
5640
|
{
|
|
4256
5641
|
id: "lotr-session-009",
|
|
4257
5642
|
repo: "minas-tirith-admin",
|
|
4258
|
-
title: "Tighten city-gate roles before the beacons launch",
|
|
5643
|
+
title: "Tighten city-gate roles with Mythos 5 before the beacons launch",
|
|
4259
5644
|
sourceClient: "claude",
|
|
4260
5645
|
sourceApp: "Claude Code",
|
|
4261
5646
|
sourceAppRaw: "claude-code",
|
|
4262
|
-
model: "claude-
|
|
5647
|
+
model: "claude-mythos-5",
|
|
4263
5648
|
provider: "anthropic",
|
|
4264
5649
|
hour: 17,
|
|
4265
5650
|
minute: 5,
|
|
@@ -4301,6 +5686,32 @@ var demoInputs = [
|
|
|
4301
5686
|
outcome: "completed",
|
|
4302
5687
|
branch: "docs/ring-config"
|
|
4303
5688
|
},
|
|
5689
|
+
{
|
|
5690
|
+
id: "lotr-session-017",
|
|
5691
|
+
repo: "fellowship-docs",
|
|
5692
|
+
title: "Ask Copilot to review the ring-config redaction guide",
|
|
5693
|
+
sourceClient: "copilot",
|
|
5694
|
+
sourceApp: "VS Code",
|
|
5695
|
+
sourceAppRaw: "vscode.copilot-chat",
|
|
5696
|
+
model: "copilot/claude-sonnet-4.5",
|
|
5697
|
+
provider: "anthropic",
|
|
5698
|
+
hour: 18,
|
|
5699
|
+
minute: 42,
|
|
5700
|
+
durationMinutes: 18,
|
|
5701
|
+
inputTokens: 24e4,
|
|
5702
|
+
cachedInputTokens: 96e3,
|
|
5703
|
+
outputTokens: 31e3,
|
|
5704
|
+
reasoningTokens: 0,
|
|
5705
|
+
prompts: 2,
|
|
5706
|
+
replies: 4,
|
|
5707
|
+
tools: 12,
|
|
5708
|
+
commands: 3,
|
|
5709
|
+
edits: 2,
|
|
5710
|
+
reads: 15,
|
|
5711
|
+
outcome: "completed",
|
|
5712
|
+
branch: "docs/copilot-redaction",
|
|
5713
|
+
detectedSurface: "vscode_extension"
|
|
5714
|
+
},
|
|
4304
5715
|
{
|
|
4305
5716
|
id: "lotr-session-011",
|
|
4306
5717
|
repo: "one-ring-infra",
|
|
@@ -4385,11 +5796,11 @@ var demoInputs = [
|
|
|
4385
5796
|
{
|
|
4386
5797
|
id: "lotr-session-014",
|
|
4387
5798
|
repo: "shire-mobile",
|
|
4388
|
-
title: "Make pantry sync work offline in Bag End",
|
|
5799
|
+
title: "Make pantry sync work offline in Bag End with Sonnet 4.6",
|
|
4389
5800
|
sourceClient: "claude",
|
|
4390
5801
|
sourceApp: "Claude Code",
|
|
4391
5802
|
sourceAppRaw: "claude-code",
|
|
4392
|
-
model: "claude-
|
|
5803
|
+
model: "claude-sonnet-4-6-20260601",
|
|
4393
5804
|
provider: "anthropic",
|
|
4394
5805
|
hour: 20,
|
|
4395
5806
|
minute: 45,
|
|
@@ -4408,6 +5819,74 @@ var demoInputs = [
|
|
|
4408
5819
|
branch: "offline/bag-end"
|
|
4409
5820
|
}
|
|
4410
5821
|
];
|
|
5822
|
+
|
|
5823
|
+
// apps/server/src/demo/rtk.ts
|
|
5824
|
+
function readDemoRtkGain() {
|
|
5825
|
+
return {
|
|
5826
|
+
available: true,
|
|
5827
|
+
rtkDetected: true,
|
|
5828
|
+
rtkVersion: "rtk-demo 1.0.0",
|
|
5829
|
+
rtkCodexHookStatus: "active",
|
|
5830
|
+
rtkLastActivityAt: "2026-05-18T18:42:00.000Z",
|
|
5831
|
+
rtkDataSource: "fictional demo data",
|
|
5832
|
+
totalCommands: 248,
|
|
5833
|
+
inputTokens: "18.4M",
|
|
5834
|
+
outputTokens: "2.6M",
|
|
5835
|
+
tokensSaved: "5.8M",
|
|
5836
|
+
savedPercent: 23.9,
|
|
5837
|
+
totalExecTime: "41m",
|
|
5838
|
+
averageExecTime: "9.9s",
|
|
5839
|
+
rtkTokensSaved: "5.8M",
|
|
5840
|
+
rtkSavingsRate: 23.9,
|
|
5841
|
+
rtkCommandsProcessed: 248,
|
|
5842
|
+
rtkAverageCommandRuntime: "9.9s",
|
|
5843
|
+
rtkTopSavingsCommands: [
|
|
5844
|
+
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
5845
|
+
{ command: "pnpm test", count: 18, saved: "840K", averageSavedPercent: 17.4, time: "12m" },
|
|
5846
|
+
{ command: "git diff", count: 39, saved: "620K", averageSavedPercent: 26.8, time: "2m" }
|
|
5847
|
+
],
|
|
5848
|
+
rtkRecentActivity: [
|
|
5849
|
+
"rg council-of-elrond planner",
|
|
5850
|
+
"pnpm test --filter gondor-api",
|
|
5851
|
+
"git diff -- shire-mobile"
|
|
5852
|
+
],
|
|
5853
|
+
rtkDiscoverAvailable: true,
|
|
5854
|
+
rtkDiscoverError: void 0,
|
|
5855
|
+
rtkDiscoverSummary: "Total: 248 fictional command events scanned.",
|
|
5856
|
+
rtkCoverageGaps: [
|
|
5857
|
+
{ command: "cat", count: 21, rtkEquivalent: "rtk sed", status: "easy win", estimatedSavings: "410K" },
|
|
5858
|
+
{ command: "grep", count: 13, rtkEquivalent: "rtk rg", status: "easy win", estimatedSavings: "360K" }
|
|
5859
|
+
],
|
|
5860
|
+
rtkUnhandledCommands: [
|
|
5861
|
+
{ command: "palantirctl inspect", count: 5, example: "palantirctl inspect eye-of-sauron-alerts" }
|
|
5862
|
+
],
|
|
5863
|
+
topCommands: [
|
|
5864
|
+
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
5865
|
+
{ command: "git", count: 46, saved: "720K", averageSavedPercent: 18.3, time: "3m" },
|
|
5866
|
+
{ command: "pnpm", count: 31, saved: "1.1M", averageSavedPercent: 16.8, time: "17m" }
|
|
5867
|
+
],
|
|
5868
|
+
recentCommands: [
|
|
5869
|
+
"rtk rg auth gates-of-moria",
|
|
5870
|
+
"rtk pnpm test --filter palantir-observability",
|
|
5871
|
+
"rtk git diff -- one-ring-infra"
|
|
5872
|
+
],
|
|
5873
|
+
raw: "RepoSpend LOTR demo RTK output",
|
|
5874
|
+
error: void 0
|
|
5875
|
+
};
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
// apps/server/src/demo/index.ts
|
|
5879
|
+
function demoModeEnabled() {
|
|
5880
|
+
return process.env.REPOSPEND_DEMO_DATA === "lotr";
|
|
5881
|
+
}
|
|
5882
|
+
function readDemoDashboardData(filters = {}, options = {}) {
|
|
5883
|
+
return buildDashboardSnapshot({
|
|
5884
|
+
sources: demoSources,
|
|
5885
|
+
sourceStats: demoSourceStats,
|
|
5886
|
+
sessions: displaySessions(demoSessions, options),
|
|
5887
|
+
filters
|
|
5888
|
+
});
|
|
5889
|
+
}
|
|
4411
5890
|
var demoSessions = demoInputs.map(toSession);
|
|
4412
5891
|
function toSession(input) {
|
|
4413
5892
|
const start = timestamp2(input.hour, input.minute);
|
|
@@ -4496,16 +5975,6 @@ function detectedSurfaceForApp(sourceApp) {
|
|
|
4496
5975
|
function timestamp2(hour, minute) {
|
|
4497
5976
|
return `${baseDate}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00.000Z`;
|
|
4498
5977
|
}
|
|
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
5978
|
function demoReply(repo, edits, importantFailures) {
|
|
4510
5979
|
if (importantFailures > 0) {
|
|
4511
5980
|
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 +6177,15 @@ function recentCommandLines(lines) {
|
|
|
4708
6177
|
// apps/server/src/server.ts
|
|
4709
6178
|
function createServer(options = {}) {
|
|
4710
6179
|
const app = Fastify({ logger: false });
|
|
6180
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
6181
|
+
if (!isProtectedLocalMutation(request.method, request.url)) return;
|
|
6182
|
+
if (!isAllowedLocalHost(request.headers.host)) {
|
|
6183
|
+
return reply.code(403).send({ error: "Mutating RepoSpend local settings requires a localhost Host header." });
|
|
6184
|
+
}
|
|
6185
|
+
if (!isAllowedLocalOrigin(request.headers.origin)) {
|
|
6186
|
+
return reply.code(403).send({ error: "Mutating RepoSpend local settings requires a localhost Origin." });
|
|
6187
|
+
}
|
|
6188
|
+
});
|
|
4711
6189
|
app.get("/api/health", async () => ({ ok: true, app: "RepoSpend", version: readPackageVersion() }));
|
|
4712
6190
|
app.get("/api/dashboard", async (request) => {
|
|
4713
6191
|
const query = request.query;
|
|
@@ -4745,19 +6223,21 @@ function createServer(options = {}) {
|
|
|
4745
6223
|
const query = request.query;
|
|
4746
6224
|
return warmDashboardCache(warmCacheFilters(), parseDashboardOptions(query));
|
|
4747
6225
|
});
|
|
4748
|
-
app.get("/api/export.json", async (
|
|
6226
|
+
app.get("/api/export.json", async (request, reply) => {
|
|
6227
|
+
const query = request.query;
|
|
4749
6228
|
reply.header("content-type", "application/json; charset=utf-8");
|
|
4750
|
-
return exportJson();
|
|
6229
|
+
return exportJson(parseFilters(query), parseDashboardOptions(query));
|
|
4751
6230
|
});
|
|
4752
|
-
app.get("/api/export.csv", async (
|
|
6231
|
+
app.get("/api/export.csv", async (request, reply) => {
|
|
6232
|
+
const query = request.query;
|
|
4753
6233
|
reply.header("content-type", "text/csv; charset=utf-8");
|
|
4754
6234
|
reply.header("content-disposition", 'attachment; filename="repospend-export.csv"');
|
|
4755
|
-
return exportCsv();
|
|
6235
|
+
return exportCsv(parseFilters(query), parseDashboardOptions(query));
|
|
4756
6236
|
});
|
|
4757
6237
|
if (options.serveWeb !== false) {
|
|
4758
|
-
const serverDist =
|
|
4759
|
-
const packagedWebDist =
|
|
4760
|
-
const workspaceWebDist =
|
|
6238
|
+
const serverDist = path10.dirname(fileURLToPath3(import.meta.url));
|
|
6239
|
+
const packagedWebDist = path10.resolve(serverDist, "../web-dist");
|
|
6240
|
+
const workspaceWebDist = path10.resolve(serverDist, "../../web/dist");
|
|
4761
6241
|
const webDist = fsExists(packagedWebDist) ? packagedWebDist : workspaceWebDist;
|
|
4762
6242
|
if (!fsExists(webDist)) {
|
|
4763
6243
|
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 +6258,13 @@ function createServer(options = {}) {
|
|
|
4778
6258
|
return app;
|
|
4779
6259
|
}
|
|
4780
6260
|
function fsExists(inputPath) {
|
|
4781
|
-
return Boolean(inputPath) &&
|
|
6261
|
+
return Boolean(inputPath) && path10.isAbsolute(inputPath) && fs10.existsSync(inputPath);
|
|
4782
6262
|
}
|
|
4783
6263
|
function readPackageVersion() {
|
|
4784
|
-
const serverDist =
|
|
4785
|
-
const packagePath =
|
|
6264
|
+
const serverDist = path10.dirname(fileURLToPath3(import.meta.url));
|
|
6265
|
+
const packagePath = path10.resolve(serverDist, "../package.json");
|
|
4786
6266
|
try {
|
|
4787
|
-
const parsed = JSON.parse(
|
|
6267
|
+
const parsed = JSON.parse(fs10.readFileSync(packagePath, "utf8"));
|
|
4788
6268
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
4789
6269
|
} catch {
|
|
4790
6270
|
return "unknown";
|
|
@@ -4816,6 +6296,33 @@ function portCandidates(startPort, fallbackCount) {
|
|
|
4816
6296
|
function isAddressInUse(error) {
|
|
4817
6297
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE");
|
|
4818
6298
|
}
|
|
6299
|
+
function isProtectedLocalMutation(method, url) {
|
|
6300
|
+
if (method !== "PUT" && method !== "POST" && method !== "DELETE") return false;
|
|
6301
|
+
return url.startsWith("/api/settings/") || url.startsWith("/api/cache/");
|
|
6302
|
+
}
|
|
6303
|
+
function isAllowedLocalHost(hostHeader) {
|
|
6304
|
+
if (!hostHeader) return false;
|
|
6305
|
+
const host = hostFromHeader(hostHeader).toLowerCase();
|
|
6306
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
6307
|
+
}
|
|
6308
|
+
function isAllowedLocalOrigin(originHeader) {
|
|
6309
|
+
if (!originHeader) return true;
|
|
6310
|
+
try {
|
|
6311
|
+
return isAllowedLocalHost(new URL(originHeader).host);
|
|
6312
|
+
} catch {
|
|
6313
|
+
return false;
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
function hostFromHeader(hostHeader) {
|
|
6317
|
+
const value = hostHeader.trim();
|
|
6318
|
+
if (value.startsWith("[")) {
|
|
6319
|
+
const end = value.indexOf("]");
|
|
6320
|
+
return end >= 0 ? value.slice(1, end) : value;
|
|
6321
|
+
}
|
|
6322
|
+
const colonCount = (value.match(/:/g) ?? []).length;
|
|
6323
|
+
if (colonCount === 1) return value.slice(0, value.indexOf(":"));
|
|
6324
|
+
return value;
|
|
6325
|
+
}
|
|
4819
6326
|
function validatePricingBody(body) {
|
|
4820
6327
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
4821
6328
|
throw new Error("Expected JSON body with a models object.");
|
|
@@ -4836,9 +6343,13 @@ function validatePricingBody(body) {
|
|
|
4836
6343
|
outputPerMillion
|
|
4837
6344
|
};
|
|
4838
6345
|
const cachedInputPerMillion = finiteNumber(row.cachedInputPerMillion);
|
|
6346
|
+
const cacheCreationInput5mPerMillion = finiteNumber(row.cacheCreationInput5mPerMillion);
|
|
6347
|
+
const cacheCreationInput1hPerMillion = finiteNumber(row.cacheCreationInput1hPerMillion);
|
|
4839
6348
|
const cacheCreationInputPerMillion = finiteNumber(row.cacheCreationInputPerMillion);
|
|
4840
6349
|
const reasoningOutputPerMillion = finiteNumber(row.reasoningOutputPerMillion);
|
|
4841
6350
|
if (cachedInputPerMillion !== void 0) modelPricing.cachedInputPerMillion = cachedInputPerMillion;
|
|
6351
|
+
if (cacheCreationInput5mPerMillion !== void 0) modelPricing.cacheCreationInput5mPerMillion = cacheCreationInput5mPerMillion;
|
|
6352
|
+
if (cacheCreationInput1hPerMillion !== void 0) modelPricing.cacheCreationInput1hPerMillion = cacheCreationInput1hPerMillion;
|
|
4842
6353
|
if (cacheCreationInputPerMillion !== void 0) modelPricing.cacheCreationInputPerMillion = cacheCreationInputPerMillion;
|
|
4843
6354
|
if (reasoningOutputPerMillion !== void 0) modelPricing.reasoningOutputPerMillion = reasoningOutputPerMillion;
|
|
4844
6355
|
if (typeof row.note === "string") modelPricing.note = row.note;
|
|
@@ -4851,7 +6362,6 @@ function validateConfigBody(body) {
|
|
|
4851
6362
|
const input = body;
|
|
4852
6363
|
const config = {};
|
|
4853
6364
|
if (Array.isArray(input.repos)) config.repos = input.repos;
|
|
4854
|
-
if (Array.isArray(input.budgets)) config.budgets = input.budgets;
|
|
4855
6365
|
if (typeof input.pricingPath === "string") config.pricingPath = input.pricingPath;
|
|
4856
6366
|
const experimentalSources = input.experimentalSources;
|
|
4857
6367
|
if (experimentalSources && typeof experimentalSources === "object" && !Array.isArray(experimentalSources)) {
|
|
@@ -4890,24 +6400,26 @@ try {
|
|
|
4890
6400
|
console.log(`RepoSpend dashboard: ${url}`);
|
|
4891
6401
|
openBrowser(url);
|
|
4892
6402
|
} else if (command === "__warm-cache") {
|
|
4893
|
-
readDashboardData(
|
|
6403
|
+
readDashboardData(parseCliDateFilters(args), parseCliDashboardOptions(args));
|
|
4894
6404
|
} else if (command === "scan") {
|
|
4895
|
-
const data = readDashboardData(
|
|
6405
|
+
const data = readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args));
|
|
4896
6406
|
console.log(JSON.stringify({ sources: data.sources, sessionCount: data.sessions.length, summary: data.summary }, null, 2));
|
|
4897
6407
|
} else if (command === "by-repo") {
|
|
4898
|
-
printGroups(groupByRepo(readDashboardData(
|
|
6408
|
+
printGroups(groupByRepo(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4899
6409
|
} else if (command === "by-day") {
|
|
4900
|
-
printGroups(groupByDay(readDashboardData(
|
|
6410
|
+
printGroups(groupByDay(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4901
6411
|
} else if (command === "by-hour") {
|
|
4902
|
-
printGroups(groupByHour(readDashboardData(
|
|
6412
|
+
printGroups(groupByHour(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4903
6413
|
} else if (command === "by-model") {
|
|
4904
|
-
printGroups(groupByModel(readDashboardData(
|
|
6414
|
+
printGroups(groupByModel(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
4905
6415
|
} else if (command === "by-app") {
|
|
4906
|
-
printGroups(groupBySourceApp(readDashboardData(
|
|
6416
|
+
printGroups(groupBySourceApp(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
|
|
6417
|
+
} else if (command === "doctor") {
|
|
6418
|
+
printDoctor(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)));
|
|
4907
6419
|
} else if (command === "export") {
|
|
4908
6420
|
const format = valueAfter(args, "--format") ?? "json";
|
|
4909
|
-
const filters =
|
|
4910
|
-
const sessions = readDashboardData(filters).sessions;
|
|
6421
|
+
const filters = parseCliFilters(args);
|
|
6422
|
+
const sessions = readDashboardData(filters, parseCliDashboardOptions(args)).sessions;
|
|
4911
6423
|
console.log(format === "csv" ? toCsv(sessions) : JSON.stringify(sessions, null, 2));
|
|
4912
6424
|
} else {
|
|
4913
6425
|
console.error(`Unknown command: ${command}`);
|
|
@@ -4940,26 +6452,42 @@ function printGroups(groups) {
|
|
|
4940
6452
|
}));
|
|
4941
6453
|
console.table(rows);
|
|
4942
6454
|
}
|
|
4943
|
-
function
|
|
4944
|
-
const
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
}
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
6455
|
+
function printDoctor(data) {
|
|
6456
|
+
const confidence = data.confidence;
|
|
6457
|
+
console.log(`RepoSpend data confidence: ${confidence.label} (${confidence.score}/100)`);
|
|
6458
|
+
console.table([
|
|
6459
|
+
{ metric: "Sessions", value: confidence.sessionCount },
|
|
6460
|
+
{ metric: "Token data coverage", value: `${Math.round(confidence.tokenDataPct * 100)}% (${confidence.tokenDataSessions}/${confidence.sessionCount})` },
|
|
6461
|
+
{ metric: "Pricing coverage", value: `${Math.round(confidence.pricingCoveragePct * 100)}% (${confidence.pricedTokenSessions}/${confidence.tokenDataSessions})` },
|
|
6462
|
+
{ metric: "Repo grouping", value: `${Math.round(confidence.verifiedRepoPct * 100)}% verified (${confidence.verifiedRepoSessions}/${confidence.sessionCount})` },
|
|
6463
|
+
{ metric: "Parse issues", value: confidence.parseIssueCount },
|
|
6464
|
+
{ metric: "Source warnings", value: confidence.sourceWarningCount }
|
|
6465
|
+
]);
|
|
6466
|
+
if (confidence.issues.length) {
|
|
6467
|
+
console.table(confidence.issues.map((issue2) => ({
|
|
6468
|
+
severity: issue2.tone,
|
|
6469
|
+
issue: issue2.title,
|
|
6470
|
+
detail: issue2.detail,
|
|
6471
|
+
sessions: issue2.affectedSessionIds.length
|
|
6472
|
+
})));
|
|
6473
|
+
} else {
|
|
6474
|
+
console.log("No data-confidence issues detected.");
|
|
6475
|
+
}
|
|
6476
|
+
console.table(data.sourceStats.map((source) => ({
|
|
6477
|
+
source: source.sourceLabel ?? source.sourceId ?? "unknown",
|
|
6478
|
+
imported: source.sessionsImported ?? 0,
|
|
6479
|
+
files: source.sessionFileCount,
|
|
6480
|
+
parseIssues: source.parseFailureCount ?? 0,
|
|
6481
|
+
primaryData: sourcePrimaryDataFound(source) ? "found" : "missing"
|
|
6482
|
+
})));
|
|
4955
6483
|
}
|
|
4956
|
-
function
|
|
4957
|
-
|
|
4958
|
-
return index >= 0 ? args2[index + 1] : void 0;
|
|
6484
|
+
function sourcePrimaryDataFound(source) {
|
|
6485
|
+
return Boolean(source.stateExists || source.sessionsExists || source.projectsExists || source.historyExists || source.otelExists || (source.otelFileCount ?? 0) > 0 || (source.databaseFileCount ?? 0) > 0);
|
|
4959
6486
|
}
|
|
4960
6487
|
function printHelp() {
|
|
4961
6488
|
console.log(`RepoSpend commands:
|
|
4962
6489
|
repospend serve
|
|
6490
|
+
repospend doctor
|
|
4963
6491
|
repospend scan
|
|
4964
6492
|
repospend by-repo
|
|
4965
6493
|
repospend by-day
|
|
@@ -4970,7 +6498,13 @@ function printHelp() {
|
|
|
4970
6498
|
repospend export --format csv
|
|
4971
6499
|
|
|
4972
6500
|
Options:
|
|
4973
|
-
--source all|codex|claude|cursor
|
|
6501
|
+
--source all|codex|claude|copilot|cursor
|
|
6502
|
+
--sourceApp "VS Code" | --source-app "VS Code" | --app "VS Code"
|
|
6503
|
+
--repo <repo name or path>
|
|
6504
|
+
--model <model id>
|
|
6505
|
+
--from YYYY-MM-DD
|
|
6506
|
+
--to YYYY-MM-DD
|
|
6507
|
+
--split-source-apps
|
|
4974
6508
|
|
|
4975
6509
|
Cursor is experimental and only scans when enabled in ~/.repospend/config.json.`);
|
|
4976
6510
|
}
|