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/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
- return groupBy(sessions, (session) => session.model ?? "unknown-model", (session) => session.model ?? "Model not recorded");
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 OpenAI and Claude API pricing",
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-05-18",
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
- "claude-opus-4-7": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
242
- "claude-opus-4-6": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
243
- "claude-opus-4-5": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
244
- "claude-opus-4-1": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
245
- "claude-opus-4": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
246
- "claude-sonnet-4-6": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
247
- "claude-sonnet-4-5": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
248
- "claude-sonnet-4": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
249
- "claude-haiku-4-5": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 },
250
- "claude-3-5-haiku": { inputPerMillion: 0.8, cacheCreationInputPerMillion: 1, cachedInputPerMillion: 0.08, outputPerMillion: 4 }
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 cacheCreationInput = usage.cacheCreationInputTokens ?? 0;
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 ?? modelPricing.inputPerMillion;
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 + cacheCreationInput / 1e6 * cacheCreationRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
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 findModelPricing(model, pricing) {
291
- const lower = model.toLowerCase();
292
- const exactPricing = pricing[model] ?? pricing[lower];
293
- if (isUsablePricing(exactPricing)) return exactPricing;
294
- const familyPricing = claudeFamilyPricing(lower, pricing);
295
- if (isUsablePricing(familyPricing)) return familyPricing;
296
- return void 0;
297
- }
298
- function claudeFamilyPricing(model, pricing) {
299
- const families = [
300
- "claude-opus-4-7",
301
- "claude-opus-4-6",
302
- "claude-opus-4-5",
303
- "claude-opus-4-1",
304
- "claude-opus-4",
305
- "claude-sonnet-4-6",
306
- "claude-sonnet-4-5",
307
- "claude-sonnet-4",
308
- "claude-haiku-4-5",
309
- "claude-3-5-haiku"
310
- ];
311
- const family = families.find((candidate) => model === candidate || model.startsWith(`${candidate}-`));
312
- return family ? pricing[family] : void 0;
313
- }
314
- function isUsablePricing(pricing) {
315
- return typeof pricing?.inputPerMillion === "number" && pricing.inputPerMillion > 0 && typeof pricing.outputPerMillion === "number" && pricing.outputPerMillion > 0;
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
- return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1, notation: "compact" }).format(value);
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: "Important command issue rate looks high",
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: "Open Settings and add model rates, or inspect older Codex sessions that only expose raw totals.",
730
- actionTarget: "settings",
731
- actionLabel: "Open settings",
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", "Command issue sessions", "Sessions with blocking, repeated, or token-expensive command issues.", (session) => importantCommandFailures(session) > 0),
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
- return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1, notation: "compact" }).format(value);
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 = text.includes("exit_code") && !text.includes('"exit_code":0') && !text.includes('"exit_code": 0');
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 issue that may have stopped progress.";
1123
- if (issue2.severity === "warning") return "Non-zero command event classified as important for this session.";
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 category = commandCategory(text);
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(text)) {
1136
- return { command: commandKey, category, classification: isExploratoryMiss(text) ? "exploratory_miss" : "harmless_nonzero", severity: "ignored", impact: "low" };
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(text, category) || repeated) {
1139
- return { command: commandKey, category, classification: "blocking_failure", severity: repeated ? "critical" : importantSeverity(text, category), impact: repeated ? "high" : importantImpact(category) };
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
- output_tokens: numberValue(usage.output_tokens)
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 cacheCreation = numberValue(usage.cache_creation_input_tokens);
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
- return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + numberValue(usage.cache_creation_input_tokens) + numberValue(usage.output_tokens);
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/cursor.ts
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
- import Database2 from "better-sqlite3";
2850
- var parserVersion = "cursor-v1";
2851
- var maxJsonlBytes = 20 * 1024 * 1024;
2852
- var maxDatabaseBytes = 100 * 1024 * 1024;
2853
- var cursorStoragePrefixes = ["cursor.", "anysphere."];
2854
- var interestingTerms = ["cursor", "composer", "aichat", "ai_chat", "aiservice", "agent-transcript", "prompttokenbreakdown"];
2855
- function scanCursor(options) {
2856
- const cursorHome = options.cursorHome ?? path7.join(os4.homedir(), ".cursor");
2857
- const discovery = discoverCursorFiles(cursorHome, Boolean(options.cursorHome));
2858
- const jsonlFiles = discovery.jsonlFiles.filter((filePath) => fileMayOverlapScanWindow2(filePath, options.scanWindow));
2859
- const databaseFiles = discovery.databaseFiles.filter((filePath) => fileMayOverlapScanWindow2(filePath, options.scanWindow));
2860
- const jsonlCandidates = jsonlFiles.map((filePath) => cursorJsonlToCandidate(filePath, cursorHome));
2861
- const databaseCandidates = databaseFiles.flatMap((filePath) => cursorDatabaseToCandidates(filePath, cursorHome, discovery.warnings));
2862
- const candidates = dedupeCandidates([...jsonlCandidates, ...databaseCandidates]);
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: "cursor",
2876
- label: "Cursor",
2877
- available: discovery.jsonlFiles.length > 0 || discovery.databaseFiles.length > 0,
2878
- paths: unique4([cursorHome, discovery.projectsPath, discovery.statePath, discovery.workspaceStoragePath, ...discovery.roots]),
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: "cursor",
2884
- sourceLabel: "Cursor",
2885
- homePath: cursorHome,
2886
- cursorHome,
2887
- globalStoragePath: discovery.globalStoragePath,
3370
+ sourceId: "copilot",
3371
+ sourceLabel: "GitHub Copilot",
3372
+ homePath: copilotHome,
3373
+ copilotHome,
3374
+ otelPath: discovery.otelPath,
2888
3375
  workspaceStoragePath: discovery.workspaceStoragePath,
2889
- projectsPath: discovery.projectsPath,
2890
- statePath: discovery.statePath,
2891
- projectsExists: fs7.existsSync(discovery.projectsPath),
2892
- stateExists: fs7.existsSync(discovery.statePath),
2893
- sessionsExists: jsonlFiles.length > 0,
2894
- sessionFileCount: jsonlFiles.length,
2895
- databaseFileCount: databaseFiles.length,
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 discoverCursorFiles(cursorHome, explicitHome) {
3391
+ function discoverCopilotFiles(copilotHome, explicitHome, explicitCodeUserRoots) {
2906
3392
  const warnings = [];
2907
- const appPaths = defaultCursorAppPaths();
2908
- const roots = explicitHome ? [cursorHome] : unique4([cursorHome, appPaths.statePath, ...cursorOwnedGlobalStorageRoots(appPaths.globalStoragePath), ...cursorOwnedWorkspaceStorageRoots(appPaths.workspaceStoragePath)]);
2909
- const jsonlFiles = [];
2910
- const databaseFiles = [];
2911
- let skippedFileCount = 0;
2912
- for (const root of roots) {
2913
- if (!fs7.existsSync(root)) continue;
2914
- try {
2915
- if (isReadableRegularFile(root, maxDatabaseBytes)) {
2916
- databaseFiles.push(root);
2917
- continue;
2918
- }
2919
- walk3(root, 10, (filePath) => {
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
- roots,
2937
- projectsPath: path7.join(cursorHome, "projects"),
2938
- globalStoragePath: appPaths.globalStoragePath,
2939
- workspaceStoragePath: appPaths.workspaceStoragePath,
2940
- statePath: appPaths.statePath,
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 cursorOwnedGlobalStorageRoots(globalStoragePath) {
2948
- return cursorOwnedChildDirs(globalStoragePath);
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 cursorOwnedWorkspaceStorageRoots(workspaceStoragePath) {
2951
- if (!fs7.existsSync(workspaceStoragePath)) return [];
2952
- const roots = [];
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(workspaceStoragePath, { withFileTypes: true })) {
3436
+ for (const workspace of fs7.readdirSync(workspaceStorage, { withFileTypes: true })) {
2955
3437
  if (!workspace.isDirectory()) continue;
2956
- roots.push(...cursorOwnedChildDirs(path7.join(workspaceStoragePath, workspace.name)));
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
- return [];
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 cursorOwnedChildDirs(root) {
2964
- if (!fs7.existsSync(root)) return [];
3450
+ function collectCodeChatSessionModelHints(root, modelHints, warnings) {
3451
+ if (!fs7.existsSync(root)) return;
2965
3452
  try {
2966
- return fs7.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && cursorStoragePrefixes.some((prefix) => entry.name.toLowerCase().startsWith(prefix))).map((entry) => path7.join(root, entry.name));
2967
- } catch {
2968
- return [];
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 defaultCursorAppPaths() {
2972
- const home = os4.homedir();
2973
- if (process.platform === "win32") {
2974
- const appData = process.env.APPDATA ?? path7.join(home, "AppData", "Roaming");
2975
- const globalStoragePath2 = path7.join(appData, "Cursor", "User", "globalStorage");
2976
- return {
2977
- globalStoragePath: globalStoragePath2,
2978
- workspaceStoragePath: path7.join(appData, "Cursor", "User", "workspaceStorage"),
2979
- statePath: path7.join(globalStoragePath2, "state.vscdb")
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
- if (process.platform === "darwin") {
2983
- const globalStoragePath2 = path7.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage");
2984
- return {
2985
- globalStoragePath: globalStoragePath2,
2986
- workspaceStoragePath: path7.join(home, "Library", "Application Support", "Cursor", "User", "workspaceStorage"),
2987
- statePath: path7.join(globalStoragePath2, "state.vscdb")
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
- const xdg = process.env.XDG_CONFIG_HOME || path7.join(home, ".config");
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 walk3(root, maxDepth, onFile, depth = 0) {
2999
- if (depth > maxDepth) return;
3000
- for (const entry of fs7.readdirSync(root, { withFileTypes: true })) {
3001
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "CachedData" || entry.name === "Cache") continue;
3002
- const fullPath = path7.join(root, entry.name);
3003
- if (entry.isDirectory()) {
3004
- walk3(fullPath, maxDepth, onFile, depth + 1);
3005
- } else if (entry.isFile()) {
3006
- onFile(fullPath);
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 isReadableRegularFile(filePath, maxBytes) {
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.accessSync(filePath, fs7.constants.R_OK);
3013
- const stat = fs7.statSync(filePath);
3014
- return stat.isFile() && stat.size <= maxBytes;
3015
- } catch {
3016
- return false;
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 cursorJsonlSort(a, b) {
3020
- const aTranscript = a.includes(`${path7.sep}agent-transcripts${path7.sep}`) ? 0 : 1;
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 fileMayOverlapScanWindow2(filePath, scanWindow) {
3025
- if (!scanWindow?.fromMs && !scanWindow?.toMs) return true;
3026
- const inferredTimestamp = timestampMs2(inferDateFromCursorPath(filePath));
3027
- if (scanWindow.fromMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp < scanWindow.fromMs) return false;
3028
- if (scanWindow.toMs !== void 0 && inferredTimestamp !== void 0 && inferredTimestamp > scanWindow.toMs) return false;
3029
- if (scanWindow.fromMs === void 0) return true;
3030
- try {
3031
- return fs7.statSync(filePath).mtimeMs >= scanWindow.fromMs;
3032
- } catch {
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 = emptyBreakdown3();
4331
+ const breakdown = emptyBreakdown4();
3038
4332
  try {
3039
- const parse = parseJsonLines3(fs7.readFileSync(filePath, "utf8"));
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:${errorMessage3(error)}`;
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 = path7.basename(filePath).replace(/\.jsonl$/i, "") || void 0;
3052
- const fallbackDate = inferDateFromCursorPath(filePath) ?? fileModifiedDate(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}: ${errorMessage3(error)}`);
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}: ${errorMessage3(error)}`);
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}: ${errorMessage3(error)}`);
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 = emptyBreakdown3();
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 = unique4(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: errorMessage3(error) };
4452
+ return { ok: false, error: errorMessage4(error) };
3159
4453
  }
3160
4454
  }
3161
- function parseJsonLines3(text) {
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}:${errorMessage3(error)}`);
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 = firstObject5(record);
4469
+ const root = firstObject6(record);
3176
4470
  const timestamp3 = firstValueByKeys(objects, ["timestamp", "createdAt", "created_at", "lastUpdatedAt", "conversationCheckpointLastUpdatedAt", "time", "date"]);
3177
- const date = dateValue4(timestamp3);
4471
+ const date = dateValue5(timestamp3);
3178
4472
  if (date) {
3179
- breakdown.startedAt = minDate2(breakdown.startedAt, date);
3180
- breakdown.endedAt = maxDate2(breakdown.endedAt, date);
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 firstObject5(value) {
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 = stringValue4(object[key]);
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 = numberValue2(object[key]);
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 = (stringValue4(root?.type) ?? stringValue4(root?.event) ?? "").toLowerCase();
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 = firstObject5(value);
4579
+ const object = firstObject6(value);
3286
4580
  if (!object) return void 0;
3287
- return stringValue4(object.text) ?? stringValue4(object.content) ?? stringValue4(object.value);
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) => [stringValue4(object.type), stringValue4(object.event), stringValue4(object.toolName), stringValue4(object.tool_name), stringValue4(object.name)]).filter((item) => Boolean(item)).join(" ").toLowerCase();
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 = stringValue4(object.status)?.toLowerCase();
4605
+ const status = stringValue5(object.status)?.toLowerCase();
3312
4606
  if (status === "failed" || status === "error") return true;
3313
- const exitCode = numberValue2(object.exitCode ?? object.exit_code ?? object.statusCode ?? object.status_code);
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 ?? `${path7.basename(candidate.sourcePath).replace(/\.(jsonl|sqlite|db|vscdb)$/i, "") || "cursor-session"}-${index}`;
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: durationMs3(breakdown.startedAt, breakdown.endedAt),
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(path7.sep);
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 `${path7.sep}${projectId.slice(1).split("-").filter(Boolean).join(path7.sep)}`;
4706
+ return `${path8.sep}${projectId.slice(1).split("-").filter(Boolean).join(path8.sep)}`;
3413
4707
  }
3414
- if (projectId) return path7.join(cursorHome, "projects", projectId);
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(path7.sep);
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 = dateValue4(Number(part));
4716
+ const date = dateValue5(Number(part));
3423
4717
  if (date) return date;
3424
4718
  }
3425
4719
  return void 0;
3426
4720
  }
3427
- function timestampMs2(value) {
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 fileModifiedDate(filePath) {
4726
+ function fileModifiedDate2(filePath) {
3433
4727
  try {
3434
- return fs7.statSync(filePath).mtime.toISOString();
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 path7.join(root.path, ...tokens2.slice(root.consumed));
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: path7.join(path7.sep, "Users", tokens2[1]), consumed: 2 };
3452
- if (tokens2[0] === "home" && tokens2[1]) return { path: path7.join(path7.sep, "home", tokens2[1]), consumed: 2 };
3453
- if (tokens2[0] === "tmp") return { path: path7.join(path7.sep, "tmp"), consumed: 1 };
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 fs7.existsSync(root) ? root : void 0;
3459
- if (!fs7.existsSync(root)) return void 0;
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 fs7.readdirSync(root, { withFileTypes: true })) {
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(path7.join(root, entry.name), tokens2.slice(entryTokens.length));
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(`${path7.sep}agent-transcripts${path7.sep}`) || lower.endsWith(".jsonl")) return "cli";
3479
- if (lower.includes(`${path7.sep}workspaceStorage${path7.sep}`.toLowerCase()) || lower.includes(`${path7.sep}globalStorage${path7.sep}`.toLowerCase()) || lower.endsWith(".vscdb")) return "desktop";
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 emptyBreakdown3() {
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: unique4([...primary.parseErrors, ...secondary.parseErrors]),
4882
+ parseErrors: unique5([...primary.parseErrors, ...secondary.parseErrors]),
3589
4883
  promptTimeline: primary.promptTimeline.length >= secondary.promptTimeline.length ? primary.promptTimeline : secondary.promptTimeline,
3590
- warnings: unique4([...primary.warnings, ...secondary.warnings, "cursor_duplicate_session_merged"]),
3591
- matchedKeys: unique4([...primary.matchedKeys, ...secondary.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 ? minDate2(current, candidate) : current;
4894
+ return candidate ? minDate3(current, candidate) : current;
3601
4895
  }
3602
4896
  function optionalMaxDate(current, candidate) {
3603
- return candidate ? maxDate2(current, candidate) : current;
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 stringValue4(value) {
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 numberValue2(value) {
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 dateValue4(value) {
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 minDate2(current, candidate) {
4925
+ function minDate3(current, candidate) {
3632
4926
  return !current || candidate < current ? candidate : current;
3633
4927
  }
3634
- function maxDate2(current, candidate) {
4928
+ function maxDate3(current, candidate) {
3635
4929
  return !current || candidate > current ? candidate : current;
3636
4930
  }
3637
- function durationMs3(startedAt, endedAt) {
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 unique4(values) {
4943
+ function unique5(values) {
3650
4944
  return [...new Set(values.filter(Boolean))];
3651
4945
  }
3652
- function errorMessage3(error) {
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
- function scanUsageSources(options) {
3658
- const codex = scanCodex({
3659
- ...options.codexHome ? { codexHome: options.codexHome } : {},
3660
- ...options.config ? { config: options.config } : {},
3661
- ...options.scanWindow ? { scanWindow: options.scanWindow } : {},
3662
- pricing: options.pricing
3663
- });
3664
- const claude = scanClaude({
3665
- ...options.claudeHome ? { claudeHome: options.claudeHome } : {},
3666
- ...options.config ? { config: options.config } : {},
3667
- ...options.scanWindow ? { scanWindow: options.scanWindow } : {},
3668
- pricing: options.pricing
3669
- });
3670
- const cursorEnabled = options.config?.experimentalSources?.cursor === true || Boolean(options.cursorHome);
3671
- const cursor = cursorEnabled ? scanCursor({
3672
- ...options.cursorHome ? { cursorHome: options.cursorHome } : {},
3673
- ...options.config ? { config: options.config } : {},
3674
- ...options.scanWindow ? { scanWindow: options.scanWindow } : {},
3675
- pricing: options.pricing
3676
- }) : void 0;
3677
- return {
3678
- sources: [codex.source, claude.source, ...cursor ? [cursor.source] : []],
3679
- sourceStats: [codex.stats, claude.stats, ...cursor ? [cursor.stats] : []],
3680
- sessions: [...codex.sessions, ...claude.sessions, ...cursor?.sessions ?? []]
3681
- };
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 fs8 from "node:fs";
3686
- import path8 from "node:path";
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 = fs8.existsSync(target);
3814
- fs8.rmSync(target, { recursive: true, force: true });
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 = path8.dirname(fileURLToPath2(import.meta.url));
3875
- const sourceCli = path8.join(currentDir, "cli.ts");
3876
- if (fs8.existsSync(sourceCli)) return sourceCli;
3877
- return path8.join(currentDir, "cli.js");
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 fs9 from "node:fs";
3905
- import path9 from "node:path";
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-data.ts
5264
+ // apps/server/src/demo/fixtures.ts
3911
5265
  var baseDate = "2026-05-18";
3912
- var demoPricing = {
3913
- ...defaultPricing,
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
- available: true,
3932
- rtkDetected: true,
3933
- rtkVersion: "rtk-demo 1.0.0",
3934
- rtkCodexHookStatus: "active",
3935
- rtkLastActivityAt: "2026-05-18T18:42:00.000Z",
3936
- rtkDataSource: "fictional demo data",
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-7",
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-sonnet-4-6",
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-haiku-4-5-20251001",
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-haiku-4-5-20251001",
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 (_request, reply) => {
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 (_request, reply) => {
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 = path9.dirname(fileURLToPath3(import.meta.url));
4759
- const packagedWebDist = path9.resolve(serverDist, "../web-dist");
4760
- const workspaceWebDist = path9.resolve(serverDist, "../../web/dist");
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) && path9.isAbsolute(inputPath) && fs9.existsSync(inputPath);
6261
+ return Boolean(inputPath) && path10.isAbsolute(inputPath) && fs10.existsSync(inputPath);
4782
6262
  }
4783
6263
  function readPackageVersion() {
4784
- const serverDist = path9.dirname(fileURLToPath3(import.meta.url));
4785
- const packagePath = path9.resolve(serverDist, "../package.json");
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(fs9.readFileSync(packagePath, "utf8"));
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(cliDateFilters(args), { splitSourceApps: args.includes("--splitSourceApps") });
6403
+ readDashboardData(parseCliDateFilters(args), parseCliDashboardOptions(args));
4894
6404
  } else if (command === "scan") {
4895
- const data = readDashboardData(cliFilters(args));
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(cliFilters(args)).sessions));
6408
+ printGroups(groupByRepo(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
4899
6409
  } else if (command === "by-day") {
4900
- printGroups(groupByDay(readDashboardData(cliFilters(args)).sessions));
6410
+ printGroups(groupByDay(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
4901
6411
  } else if (command === "by-hour") {
4902
- printGroups(groupByHour(readDashboardData(cliFilters(args)).sessions));
6412
+ printGroups(groupByHour(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
4903
6413
  } else if (command === "by-model") {
4904
- printGroups(groupByModel(readDashboardData(cliFilters(args)).sessions));
6414
+ printGroups(groupByModel(readDashboardData(parseCliFilters(args), parseCliDashboardOptions(args)).sessions));
4905
6415
  } else if (command === "by-app") {
4906
- printGroups(groupBySourceApp(readDashboardData(cliFilters(args)).sessions));
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 = cliFilters(args);
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 cliFilters(args2) {
4944
- const source = valueAfter(args2, "--source");
4945
- if (!source || source === "all") return {};
4946
- return { source };
4947
- }
4948
- function cliDateFilters(args2) {
4949
- const from = valueAfter(args2, "--from");
4950
- const to = valueAfter(args2, "--to");
4951
- return {
4952
- ...from ? { from } : {},
4953
- ...to ? { to } : {}
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 valueAfter(args2, flag) {
4957
- const index = args2.indexOf(flag);
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
  }