repospend 0.0.9 → 0.1.0

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