repospend 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to RepoSpend will be documented in this file.
4
4
 
5
+ ## 0.1.1
6
+
7
+ - Add bundled Claude Fable 5 and Claude Mythos 5 API-equivalent pricing, including dated model ID family pricing and future Fable/Mythos version fallback.
8
+ - Price Claude cache writes from the local 5-minute vs 1-hour TTL split when transcripts expose it, with the 1-hour rate kept as the fallback for unsplit cache creation.
9
+ - Improve the dashboard filter bar by replacing always-expanded filter groups with compact dropdowns that close when another filter opens, Escape is pressed, or the user clicks outside.
10
+ - Move the Overview timeline and chart controls directly under the KPI row, keeping diagnostics such as Token Counting in Settings instead of the daily-glance flow.
11
+ - Replace Settings save and local-cache actions that forced full page reloads with React refetches that preserve page context.
12
+ - Improve chart readability with stable repo colors, positive-only minimum bar size, and a model distribution share panel for skewed model usage.
13
+ - Reduce badge and copy noise by quieting provider badges, preserving outcome color, simplifying empty filter copy, and showing a compact filtered-view notice on pages without the filter bar.
14
+ - Improve responsive and accessibility polish with a horizontal narrow-width nav, safer filter popover alignment, system font fallback, tabular numeric values, and reduced-motion CSS.
15
+
5
16
  ## 0.1.0
6
17
 
7
18
  - Add GitHub Copilot support for local OTEL exports, Copilot session-state files, and VS Code Copilot Chat transcript/debug files, leaving cost unknown when local data lacks full token splits.
package/README.md CHANGED
@@ -35,11 +35,11 @@ Best for developers who want to:
35
35
 
36
36
  ## Preview
37
37
 
38
- ![RepoSpend overview dashboard with fictional Middle-earth usage data](docs/screenshots/dashboard-overview.png)
38
+ ![Animated RepoSpend overview and sessions tour with fictional Middle-earth usage data](docs/screenshots/overview-sessions-tour.gif)
39
39
 
40
- Screenshots use fictional Middle-earth demo data. The Lord of the Rings themed
41
- repo names, sessions, prompts, token counts, and costs are intentional; no private
42
- repository data is shown.
40
+ Preview media uses fictional Middle-earth demo data. The Lord of the Rings themed
41
+ repo names, sessions, prompts, token counts, models, and costs are intentional; no
42
+ private repository data is shown.
43
43
 
44
44
  ## What is RepoSpend?
45
45
 
@@ -196,9 +196,12 @@ RepoSpend uses normalized model-work totals:
196
196
  totalTokens = inputTokens + outputTokens + reasoningTokens
197
197
  ```
198
198
 
199
- Cache reads and cache writes are kept as input sub-buckets and priced once. This
200
- means RepoSpend token totals may look lower than tools that display cache
201
- reads/writes as separate addable token columns.
199
+ Cache reads and cache writes are kept as input sub-buckets and priced once. For
200
+ Claude, RepoSpend also uses the 5-minute vs 1-hour cache-write split recorded in
201
+ local transcripts when available. This means RepoSpend token totals may look
202
+ lower than tools that display cache reads/writes as separate addable token
203
+ columns, and Claude API-equivalent cost may differ from tools that collapse all
204
+ cache writes into one rate.
202
205
 
203
206
  For the detailed accounting model and comparison with `ccusage` and Tokscale,
204
207
  see [docs/token-accounting.md](docs/token-accounting.md).
@@ -275,6 +278,17 @@ view that is close to Claude Code's local usage files. Use RepoSpend when you
275
278
  want a local dashboard that compares AI coding usage across repos, sessions,
276
279
  models, and tools.
277
280
 
281
+ When comparing totals, expect some intentional differences:
282
+
283
+ - RepoSpend includes Claude Desktop/local-agent session files when they exist;
284
+ many terminal-first tools count only `~/.claude/projects`.
285
+ - RepoSpend keeps cached input and cache writes as input sub-buckets rather than
286
+ adding them again to headline token totals.
287
+ - RepoSpend prices Claude cache writes by the recorded 5-minute vs 1-hour TTL
288
+ split when local transcripts expose it.
289
+ - RepoSpend separates Codex visible output from reasoning output so reasoning is
290
+ priced once.
291
+
278
292
  Generic Claude Code monitors usually focus on one source. RepoSpend is designed
279
293
  as a repo-level AI coding cost tracker: it brings together local Codex, Claude
280
294
  Code, and GitHub Copilot data, includes experimental Cursor imports when enabled,
package/dist/cli.js CHANGED
@@ -9,11 +9,17 @@ function summarize(sessions) {
9
9
  const cost = sumKnownCost(sessions);
10
10
  const inputTokens = sum(sessions, "inputTokens");
11
11
  const cachedInputTokens = sum(sessions, "cachedInputTokens");
12
+ const cacheCreationInputTokens = optionalSum(sessions, "cacheCreationInputTokens");
13
+ const cacheCreationInputTokens5m = optionalSum(sessions, "cacheCreationInputTokens5m");
14
+ const cacheCreationInputTokens1h = optionalSum(sessions, "cacheCreationInputTokens1h");
12
15
  return {
13
16
  estimatedCostUsd: cost.knownCount > 0 ? cost.value : void 0,
14
17
  knownCostSessions: cost.knownCount,
15
18
  totalTokens: sum(sessions, "totalTokens"),
16
19
  cachedInputTokens,
20
+ cacheCreationInputTokens,
21
+ cacheCreationInputTokens5m,
22
+ cacheCreationInputTokens1h,
17
23
  inputTokens,
18
24
  outputTokens: sum(sessions, "outputTokens"),
19
25
  reasoningTokens: sum(sessions, "reasoningTokens"),
@@ -80,6 +86,9 @@ function groupBy(sessions, keyFn, labelFn) {
80
86
  unknownCostSessions: 0,
81
87
  inputTokens: 0,
82
88
  cachedInputTokens: 0,
89
+ cacheCreationInputTokens: 0,
90
+ cacheCreationInputTokens5m: 0,
91
+ cacheCreationInputTokens1h: 0,
83
92
  outputTokens: 0,
84
93
  reasoningTokens: 0,
85
94
  totalTokens: 0,
@@ -89,6 +98,9 @@ function groupBy(sessions, keyFn, labelFn) {
89
98
  };
90
99
  group.inputTokens += session.inputTokens;
91
100
  group.cachedInputTokens += session.cachedInputTokens;
101
+ group.cacheCreationInputTokens = (group.cacheCreationInputTokens ?? 0) + (session.cacheCreationInputTokens ?? 0);
102
+ group.cacheCreationInputTokens5m = (group.cacheCreationInputTokens5m ?? 0) + (session.cacheCreationInputTokens5m ?? 0);
103
+ group.cacheCreationInputTokens1h = (group.cacheCreationInputTokens1h ?? 0) + (session.cacheCreationInputTokens1h ?? 0);
92
104
  group.outputTokens += session.outputTokens;
93
105
  group.reasoningTokens += session.reasoningTokens;
94
106
  group.totalTokens += session.totalTokens;
@@ -128,6 +140,8 @@ function toCsv(sessions) {
128
140
  "inputTokens",
129
141
  "cachedInputTokens",
130
142
  "cacheCreationInputTokens",
143
+ "cacheCreationInputTokens5m",
144
+ "cacheCreationInputTokens1h",
131
145
  "outputTokens",
132
146
  "reasoningTokens",
133
147
  "reasoningOutputTokens",
@@ -219,6 +233,8 @@ function normalizePricingModelId(model) {
219
233
  }
220
234
  function claudePricingFamilyModel(model, isUsable = () => true) {
221
235
  const families = [
236
+ "claude-fable-5",
237
+ "claude-mythos-5",
222
238
  "claude-opus-4-7",
223
239
  "claude-opus-4-6",
224
240
  "claude-opus-4-5",
@@ -250,7 +266,7 @@ function copilotPricingFamilyModel(model, isUsable = () => true) {
250
266
  }
251
267
  function parseVersionedModel(normalized) {
252
268
  if (normalized.includes("fast-mode")) return void 0;
253
- const claude = normalized.match(/^(claude-(?:opus|sonnet|haiku))-(\d+)(?:-(\d+))?/);
269
+ const claude = normalized.match(/^(claude-(?:fable|mythos|opus|sonnet|haiku))-(\d+)(?:-(\d+))?/);
254
270
  if (claude) {
255
271
  const minor = claude[3] ? Number(claude[3]) : 0;
256
272
  return { tier: claude[1], version: Number(claude[2]) + minor / 100 };
@@ -307,7 +323,7 @@ var pricingInfo = {
307
323
  { label: "GitHub Copilot model pricing reference", url: "https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing" }
308
324
  ],
309
325
  unit: "USD per 1M tokens",
310
- updatedAt: "2026-05-18",
326
+ updatedAt: "2026-06-10",
311
327
  note: "RepoSpend estimates API-equivalent cost from local token counts and public API-style Standard pricing. This is not your actual bill; subscriptions, credits, provider terms, cache behavior, regional processing, or other billing factors can make your real cost different."
312
328
  };
313
329
  var defaultPricing = {
@@ -342,18 +358,20 @@ var defaultPricing = {
342
358
  "oswe-vscode-prime": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot Raptor mini internal model id." },
343
359
  "lark": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, note: "GitHub Copilot preview model; estimated with lightweight Copilot pricing." },
344
360
  "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 },
346
- "claude-opus-4-7": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
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." },
349
- "claude-opus-4-5": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
350
- "claude-opus-4-1": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
351
- "claude-opus-4": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
352
- "claude-sonnet-4-6": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
353
- "claude-sonnet-4-5": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
354
- "claude-sonnet-4": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
355
- "claude-haiku-4-5": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 },
356
- "claude-3-5-haiku": { inputPerMillion: 0.8, cacheCreationInputPerMillion: 1, cachedInputPerMillion: 0.08, outputPerMillion: 4 }
361
+ "claude-fable-5": { inputPerMillion: 10, cacheCreationInput5mPerMillion: 12.5, cacheCreationInput1hPerMillion: 20, cacheCreationInputPerMillion: 20, cachedInputPerMillion: 1, outputPerMillion: 50 },
362
+ "claude-mythos-5": { inputPerMillion: 10, cacheCreationInput5mPerMillion: 12.5, cacheCreationInput1hPerMillion: 20, cacheCreationInputPerMillion: 20, cachedInputPerMillion: 1, outputPerMillion: 50, note: "Limited availability Anthropic model; same public API-equivalent pricing as Claude Fable 5." },
363
+ "claude-opus-4-8": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
364
+ "claude-opus-4-7": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
365
+ "claude-opus-4-6": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
366
+ "claude-opus-4-6-fast-mode": { inputPerMillion: 0, cacheCreationInput5mPerMillion: 0, cacheCreationInput1hPerMillion: 0, cacheCreationInputPerMillion: 0, cachedInputPerMillion: 0, outputPerMillion: 0, note: "GitHub Copilot supported model, but public per-token pricing is not listed separately. RepoSpend leaves API-equivalent pricing unset." },
367
+ "claude-opus-4-5": { inputPerMillion: 5, cacheCreationInput5mPerMillion: 6.25, cacheCreationInput1hPerMillion: 10, cacheCreationInputPerMillion: 10, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
368
+ "claude-opus-4-1": { inputPerMillion: 15, cacheCreationInput5mPerMillion: 18.75, cacheCreationInput1hPerMillion: 30, cacheCreationInputPerMillion: 30, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
369
+ "claude-opus-4": { inputPerMillion: 15, cacheCreationInput5mPerMillion: 18.75, cacheCreationInput1hPerMillion: 30, cacheCreationInputPerMillion: 30, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
370
+ "claude-sonnet-4-6": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
371
+ "claude-sonnet-4-5": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
372
+ "claude-sonnet-4": { inputPerMillion: 3, cacheCreationInput5mPerMillion: 3.75, cacheCreationInput1hPerMillion: 6, cacheCreationInputPerMillion: 6, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
373
+ "claude-haiku-4-5": { inputPerMillion: 1, cacheCreationInput5mPerMillion: 1.25, cacheCreationInput1hPerMillion: 2, cacheCreationInputPerMillion: 2, cachedInputPerMillion: 0.1, outputPerMillion: 5 },
374
+ "claude-3-5-haiku": { inputPerMillion: 0.8, cacheCreationInput5mPerMillion: 1, cacheCreationInput1hPerMillion: 1.6, cacheCreationInputPerMillion: 1.6, cachedInputPerMillion: 0.08, outputPerMillion: 4 }
357
375
  };
358
376
  function loadPricingTable(pricingPath) {
359
377
  if (!pricingPath) {
@@ -385,14 +403,23 @@ function calculateCostUsd(usage, pricing) {
385
403
  if (!modelPricing) {
386
404
  return void 0;
387
405
  }
388
- const cacheCreationInput = usage.cacheCreationInputTokens ?? 0;
406
+ const cacheCreationInput5m = usage.cacheCreationInputTokens5m ?? 0;
407
+ const cacheCreationInput1h = usage.cacheCreationInputTokens1h ?? 0;
408
+ const splitCacheCreationInput = cacheCreationInput5m + cacheCreationInput1h;
409
+ const cacheCreationInput = usage.cacheCreationInputTokens ?? splitCacheCreationInput;
410
+ const unclassifiedCacheCreationInput = Math.max(cacheCreationInput - splitCacheCreationInput, 0);
389
411
  const billableInput = Math.max(usage.inputTokens - usage.cachedInputTokens - cacheCreationInput, 0);
390
412
  const cachedRate = modelPricing.cachedInputPerMillion ?? modelPricing.inputPerMillion;
391
- const cacheCreationRate = modelPricing.cacheCreationInputPerMillion ?? modelPricing.inputPerMillion;
413
+ const cacheCreationRate = firstPositiveRate(modelPricing.cacheCreationInputPerMillion, modelPricing.cacheCreationInput1hPerMillion, modelPricing.cacheCreationInput5mPerMillion, modelPricing.inputPerMillion);
414
+ const cacheCreation5mRate = firstPositiveRate(modelPricing.cacheCreationInput5mPerMillion, modelPricing.cacheCreationInputPerMillion, modelPricing.inputPerMillion);
415
+ const cacheCreation1hRate = firstPositiveRate(modelPricing.cacheCreationInput1hPerMillion, modelPricing.cacheCreationInputPerMillion, modelPricing.inputPerMillion);
392
416
  const reasoningRate = modelPricing.reasoningOutputPerMillion ?? modelPricing.outputPerMillion;
393
- const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate + cacheCreationInput / 1e6 * cacheCreationRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
417
+ const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate + cacheCreationInput5m / 1e6 * cacheCreation5mRate + cacheCreationInput1h / 1e6 * cacheCreation1hRate + unclassifiedCacheCreationInput / 1e6 * cacheCreationRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
394
418
  return Number(cost.toFixed(6));
395
419
  }
420
+ function firstPositiveRate(...rates) {
421
+ return rates.find((rate) => typeof rate === "number" && Number.isFinite(rate) && rate > 0) ?? 0;
422
+ }
396
423
 
397
424
  // packages/core/src/cache.ts
398
425
  var activeCacheVersion = "parse-v1";
@@ -2795,6 +2822,8 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
2795
2822
  inputTokens: breakdown.inputTokens,
2796
2823
  cachedInputTokens: breakdown.cachedInputTokens,
2797
2824
  cacheCreationInputTokens: breakdown.cacheCreationInputTokens,
2825
+ cacheCreationInputTokens5m: breakdown.cacheCreationInputTokens5m,
2826
+ cacheCreationInputTokens1h: breakdown.cacheCreationInputTokens1h,
2798
2827
  outputTokens: breakdown.outputTokens,
2799
2828
  reasoningTokens: 0,
2800
2829
  reasoningOutputTokens: 0,
@@ -2817,12 +2846,14 @@ function claudeBreakdownToUsage(file, index, options, breakdown, idSuffix = "")
2817
2846
  serviceTierSource: serviceTierSummary.serviceTier ? "session_usage" : void 0,
2818
2847
  serviceTierConfidence: serviceTierSummary.serviceTier ? "high" : void 0,
2819
2848
  serviceTierDetail: serviceTierSummary.serviceTierDetail,
2820
- sourceMetadata: breakdown.serviceTiers.length || breakdown.serviceSpeeds.length ? {
2849
+ sourceMetadata: breakdown.serviceTiers.length || breakdown.serviceSpeeds.length || breakdown.cacheCreationInputTokens5m > 0 || breakdown.cacheCreationInputTokens1h > 0 ? {
2821
2850
  claude: {
2822
2851
  serviceTiers: breakdown.serviceTiers,
2823
2852
  speeds: breakdown.serviceSpeeds,
2824
2853
  serviceTier: serviceTierSummary.serviceTier,
2825
- speed: serviceSpeedSummary.serviceTier
2854
+ speed: serviceSpeedSummary.serviceTier,
2855
+ cacheCreationInputTokens5m: breakdown.cacheCreationInputTokens5m,
2856
+ cacheCreationInputTokens1h: breakdown.cacheCreationInputTokens1h
2826
2857
  }
2827
2858
  } : void 0,
2828
2859
  userPromptCount: breakdown.userPromptCount,
@@ -2951,19 +2982,30 @@ function occurrenceKey(filePath, recordIndex) {
2951
2982
  return `${path6.resolve(filePath)}:${recordIndex}`;
2952
2983
  }
2953
2984
  function cloneUsage(usage) {
2985
+ const cacheCreation = firstObject4(usage.cache_creation);
2954
2986
  return {
2955
2987
  input_tokens: numberValue(usage.input_tokens),
2956
2988
  cache_read_input_tokens: numberValue(usage.cache_read_input_tokens),
2957
2989
  cache_creation_input_tokens: numberValue(usage.cache_creation_input_tokens),
2990
+ cache_creation: cacheCreation ? {
2991
+ ephemeral_5m_input_tokens: numberValue(cacheCreation.ephemeral_5m_input_tokens),
2992
+ ephemeral_1h_input_tokens: numberValue(cacheCreation.ephemeral_1h_input_tokens)
2993
+ } : void 0,
2958
2994
  output_tokens: numberValue(usage.output_tokens),
2959
2995
  service_tier: stringValue3(usage.service_tier),
2960
2996
  speed: stringValue3(usage.speed)
2961
2997
  };
2962
2998
  }
2963
2999
  function mergeUsageMax(target, usage) {
3000
+ const targetCacheCreation = firstObject4(target.cache_creation) ?? {};
3001
+ const usageCacheCreation = firstObject4(usage.cache_creation) ?? {};
2964
3002
  target.input_tokens = Math.max(numberValue(target.input_tokens), numberValue(usage.input_tokens));
2965
3003
  target.cache_read_input_tokens = Math.max(numberValue(target.cache_read_input_tokens), numberValue(usage.cache_read_input_tokens));
2966
3004
  target.cache_creation_input_tokens = Math.max(numberValue(target.cache_creation_input_tokens), numberValue(usage.cache_creation_input_tokens));
3005
+ target.cache_creation = {
3006
+ ephemeral_5m_input_tokens: Math.max(numberValue(targetCacheCreation.ephemeral_5m_input_tokens), numberValue(usageCacheCreation.ephemeral_5m_input_tokens)),
3007
+ ephemeral_1h_input_tokens: Math.max(numberValue(targetCacheCreation.ephemeral_1h_input_tokens), numberValue(usageCacheCreation.ephemeral_1h_input_tokens))
3008
+ };
2967
3009
  target.output_tokens = Math.max(numberValue(target.output_tokens), numberValue(usage.output_tokens));
2968
3010
  target.service_tier ??= stringValue3(usage.service_tier);
2969
3011
  target.speed ??= stringValue3(usage.speed);
@@ -2971,11 +3013,17 @@ function mergeUsageMax(target, usage) {
2971
3013
  function applyUsage(breakdown, usage) {
2972
3014
  const input = numberValue(usage.input_tokens);
2973
3015
  const cacheRead = numberValue(usage.cache_read_input_tokens);
2974
- const cacheCreation = numberValue(usage.cache_creation_input_tokens);
3016
+ const cacheCreationBreakdown = firstObject4(usage.cache_creation);
3017
+ const cacheCreation5m = numberValue(cacheCreationBreakdown?.ephemeral_5m_input_tokens);
3018
+ const cacheCreation1h = numberValue(cacheCreationBreakdown?.ephemeral_1h_input_tokens);
3019
+ const splitCacheCreation = cacheCreation5m + cacheCreation1h;
3020
+ const cacheCreation = Math.max(numberValue(usage.cache_creation_input_tokens), splitCacheCreation);
2975
3021
  const output = numberValue(usage.output_tokens);
2976
3022
  breakdown.inputTokens += input + cacheRead + cacheCreation;
2977
3023
  breakdown.cachedInputTokens += cacheRead;
2978
3024
  breakdown.cacheCreationInputTokens += cacheCreation;
3025
+ breakdown.cacheCreationInputTokens5m += cacheCreation5m;
3026
+ breakdown.cacheCreationInputTokens1h += cacheCreation1h;
2979
3027
  breakdown.outputTokens += output;
2980
3028
  breakdown.usageCount += 1;
2981
3029
  pushUnique(breakdown.serviceTiers, normalizedServiceTier(usage.service_tier));
@@ -3030,7 +3078,8 @@ function dateKey2(timestamp3) {
3030
3078
  return timestamp3?.slice(0, 10) ?? "unknown-date";
3031
3079
  }
3032
3080
  function usageTotal(usage) {
3033
- return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + numberValue(usage.cache_creation_input_tokens) + numberValue(usage.output_tokens);
3081
+ const cacheCreation = firstObject4(usage.cache_creation);
3082
+ return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + Math.max(numberValue(usage.cache_creation_input_tokens), numberValue(cacheCreation?.ephemeral_5m_input_tokens) + numberValue(cacheCreation?.ephemeral_1h_input_tokens)) + numberValue(usage.output_tokens);
3034
3083
  }
3035
3084
  function applyContentSignals(breakdown, content) {
3036
3085
  if (!Array.isArray(content)) return;
@@ -3073,6 +3122,8 @@ function emptyBreakdown2() {
3073
3122
  inputTokens: 0,
3074
3123
  cachedInputTokens: 0,
3075
3124
  cacheCreationInputTokens: 0,
3125
+ cacheCreationInputTokens5m: 0,
3126
+ cacheCreationInputTokens1h: 0,
3076
3127
  outputTokens: 0,
3077
3128
  totalTokens: 0,
3078
3129
  usageCount: 0,
@@ -5212,12 +5263,7 @@ import Fastify from "fastify";
5212
5263
 
5213
5264
  // apps/server/src/demo/fixtures.ts
5214
5265
  var baseDate = "2026-05-18";
5215
- var demoPricing = {
5216
- ...defaultPricing,
5217
- "gpt-5.3-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
5218
- "gpt-5.3-codex-spark": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
5219
- "claude-haiku-4-5-20251001": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 }
5220
- };
5266
+ var demoPricing = defaultPricing;
5221
5267
  function issue(command2, category, severity, impact, reason) {
5222
5268
  return {
5223
5269
  command: command2,
@@ -5375,11 +5421,11 @@ var demoInputs = [
5375
5421
  {
5376
5422
  id: "lotr-session-003",
5377
5423
  repo: "shire-mobile",
5378
- title: "Investigate token spike after second breakfast",
5424
+ title: "Investigate Opus 4.8 token spike after second breakfast",
5379
5425
  sourceClient: "claude",
5380
5426
  sourceApp: "Claude Code",
5381
5427
  sourceAppRaw: "claude-code",
5382
- model: "claude-opus-4-7",
5428
+ model: "claude-opus-4-8",
5383
5429
  provider: "anthropic",
5384
5430
  hour: 11,
5385
5431
  minute: 5,
@@ -5458,11 +5504,11 @@ var demoInputs = [
5458
5504
  {
5459
5505
  id: "lotr-session-006",
5460
5506
  repo: "rivendell-dashboard",
5461
- title: "Refactor the council-of-elrond planner",
5507
+ title: "Refactor the council-of-elrond planner with Fable 5",
5462
5508
  sourceClient: "claude",
5463
5509
  sourceApp: "Claude Desktop App",
5464
5510
  sourceAppRaw: "claude-desktop-local-agent",
5465
- model: "claude-sonnet-4-6",
5511
+ model: "claude-fable-5",
5466
5512
  provider: "anthropic",
5467
5513
  hour: 14,
5468
5514
  minute: 15,
@@ -5594,11 +5640,11 @@ var demoInputs = [
5594
5640
  {
5595
5641
  id: "lotr-session-009",
5596
5642
  repo: "minas-tirith-admin",
5597
- title: "Tighten city-gate roles before the beacons launch",
5643
+ title: "Tighten city-gate roles with Mythos 5 before the beacons launch",
5598
5644
  sourceClient: "claude",
5599
5645
  sourceApp: "Claude Code",
5600
5646
  sourceAppRaw: "claude-code",
5601
- model: "claude-haiku-4-5-20251001",
5647
+ model: "claude-mythos-5",
5602
5648
  provider: "anthropic",
5603
5649
  hour: 17,
5604
5650
  minute: 5,
@@ -5750,11 +5796,11 @@ var demoInputs = [
5750
5796
  {
5751
5797
  id: "lotr-session-014",
5752
5798
  repo: "shire-mobile",
5753
- title: "Make pantry sync work offline in Bag End",
5799
+ title: "Make pantry sync work offline in Bag End with Sonnet 4.6",
5754
5800
  sourceClient: "claude",
5755
5801
  sourceApp: "Claude Code",
5756
5802
  sourceAppRaw: "claude-code",
5757
- model: "claude-haiku-4-5-20251001",
5803
+ model: "claude-sonnet-4-6-20260601",
5758
5804
  provider: "anthropic",
5759
5805
  hour: 20,
5760
5806
  minute: 45,
@@ -6297,9 +6343,13 @@ function validatePricingBody(body) {
6297
6343
  outputPerMillion
6298
6344
  };
6299
6345
  const cachedInputPerMillion = finiteNumber(row.cachedInputPerMillion);
6346
+ const cacheCreationInput5mPerMillion = finiteNumber(row.cacheCreationInput5mPerMillion);
6347
+ const cacheCreationInput1hPerMillion = finiteNumber(row.cacheCreationInput1hPerMillion);
6300
6348
  const cacheCreationInputPerMillion = finiteNumber(row.cacheCreationInputPerMillion);
6301
6349
  const reasoningOutputPerMillion = finiteNumber(row.reasoningOutputPerMillion);
6302
6350
  if (cachedInputPerMillion !== void 0) modelPricing.cachedInputPerMillion = cachedInputPerMillion;
6351
+ if (cacheCreationInput5mPerMillion !== void 0) modelPricing.cacheCreationInput5mPerMillion = cacheCreationInput5mPerMillion;
6352
+ if (cacheCreationInput1hPerMillion !== void 0) modelPricing.cacheCreationInput1hPerMillion = cacheCreationInput1hPerMillion;
6303
6353
  if (cacheCreationInputPerMillion !== void 0) modelPricing.cacheCreationInputPerMillion = cacheCreationInputPerMillion;
6304
6354
  if (reasoningOutputPerMillion !== void 0) modelPricing.reasoningOutputPerMillion = reasoningOutputPerMillion;
6305
6355
  if (typeof row.note === "string") modelPricing.note = row.note;
@@ -30,6 +30,13 @@ RepoSpend scans:
30
30
 
31
31
  Claude Code session transcripts are parsed from JSONL files. Useful fields include `sessionId`, `cwd`, `gitBranch`, `timestamp`, `entrypoint`, `message.model`, and `message.usage`. When `message.usage` is present, RepoSpend sums Claude assistant usage records directly. When token counts or model names are absent in a transcript, sessions remain visible with unknown cost.
32
32
 
33
+ RepoSpend intentionally includes Claude Desktop/local-agent session roots when
34
+ they exist. Tools such as `ccusage` and Tokscale commonly focus on
35
+ `~/.claude/projects`, so RepoSpend's Claude totals can be higher when local-agent
36
+ sessions are present. In one all-time local audit, RepoSpend's
37
+ `~/.claude/projects` slice matched `ccusage`, while the difference came from
38
+ `~/Library/Application Support/Claude/local-agent-mode-sessions`.
39
+
33
40
  RepoSpend checks `~/.claude/history.jsonl` only for source status/counting. History-only entries are not imported into usage analytics because they do not include reliable token, model, or transcript data.
34
41
 
35
42
  ## GitHub Copilot
package/docs/llms.txt CHANGED
@@ -53,6 +53,7 @@ Positioning:
53
53
  - RepoSpend complements CLI tools such as ccusage.
54
54
  - ccusage is useful for terminal-first Claude Code totals and daily breakdowns.
55
55
  - RepoSpend is useful for visual repo-level analytics, session inspection, source/tool comparison, model mix, token shape, cache reuse, exports, and command/agent friction.
56
+ - RepoSpend totals can intentionally differ from ccusage or Tokscale because it includes Claude Desktop/local-agent session roots when present, treats cache reads/writes as input sub-buckets, prices Claude cache writes by recorded 5-minute vs 1-hour TTL, and separates Codex visible output from reasoning output.
56
57
 
57
58
  Cost language:
58
59
  - RepoSpend shows estimated API-equivalent cost.
package/docs/pricing.md CHANGED
@@ -4,10 +4,20 @@ RepoSpend estimates API-equivalent cost from a local pricing table. The bundled
4
4
 
5
5
  The bundled table is seeded from public OpenAI, Anthropic, Google, and GitHub Copilot model references and is expressed as USD per 1M tokens. Pricing changes over time, so treat RepoSpend costs as API-equivalent estimates rather than invoice-grade accounting.
6
6
 
7
+ For Claude models, RepoSpend uses Anthropic's cache-write TTL split when local
8
+ Claude Code usage reports expose it. Tokens recorded under
9
+ `cache_creation.ephemeral_5m_input_tokens` use the 5-minute cache-write rate, and
10
+ tokens recorded under `cache_creation.ephemeral_1h_input_tokens` use the 1-hour
11
+ cache-write rate. If a source only exposes the older aggregate
12
+ `cache_creation_input_tokens` field, RepoSpend falls back to the model's generic
13
+ cache-write rate.
14
+
7
15
  Each model can define:
8
16
 
9
17
  - input tokens
10
- - cache write input tokens
18
+ - 5-minute cache write input tokens
19
+ - 1-hour cache write input tokens
20
+ - generic cache write input tokens for sources without a TTL split
11
21
  - cached input tokens
12
22
  - output tokens
13
23
  - reasoning output tokens
@@ -25,8 +35,20 @@ from visible output before storing and pricing both buckets. If a comparison too
25
35
  shows output inclusive of reasoning and also prices reasoning separately, its
26
36
  cost will be higher because reasoning is counted twice.
27
37
 
38
+ This is a known reason RepoSpend Codex API-equivalent cost can be lower than a
39
+ tool whose output bucket remains inclusive of reasoning while also exposing a
40
+ separate reasoning bucket. Compare visible output and reasoning separately before
41
+ treating a cost delta as a pricing-table problem.
42
+
28
43
  See [token-accounting.md](token-accounting.md) for the detailed comparison model.
29
44
 
45
+ This can differ from tools or older `ccusage` versions that price all Claude
46
+ cache creation tokens with one cache-write rate. A single-rate calculation can
47
+ understate sessions that mostly used Anthropic's 1-hour cache writes or overstate
48
+ sessions that mostly used 5-minute cache writes. RepoSpend prices the buckets
49
+ recorded in the local Claude transcript instead of forcing every cache write into
50
+ one column.
51
+
30
52
  When an exact model id is not present in the pricing table, RepoSpend first tries conservative family matching for known provider naming patterns. For example, a nearby newer Claude Opus 4.x or GPT 5.x variant can inherit the closest older bundled rate so the dashboard stays useful while public rate cards catch up. Inherited rates are labeled in Settings.
31
53
 
32
54
  If RepoSpend cannot resolve a usable rate, it still displays token totals and marks cost as unknown. Unknown pricing does not stop scans, dashboard responses, CLI output, or exports.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -5,6 +5,7 @@ RepoSpend uses one normalized token shape across local clients:
5
5
  - `inputTokens` is total input for the request/session. When a source reports cache reads or cache writes separately, RepoSpend includes them in `inputTokens` and also stores them in sub-buckets.
6
6
  - `cachedInputTokens` is the cache-read portion of input.
7
7
  - `cacheCreationInputTokens` is the cache-write portion of input, when the source exposes it.
8
+ - `cacheCreationInputTokens5m` and `cacheCreationInputTokens1h` preserve Anthropic's Claude cache-write TTL split when local transcripts expose `usage.cache_creation`.
8
9
  - `outputTokens` is visible/non-reasoning output.
9
10
  - `reasoningTokens` is reasoning output, when the source exposes it.
10
11
  - `totalTokens = inputTokens + outputTokens + reasoningTokens`.
@@ -37,6 +38,39 @@ cache write input = cacheCreationInputTokens
37
38
 
38
39
  Adding cached tokens again would inflate token totals and double-charge cached input in API-equivalent cost.
39
40
 
41
+ ## Claude Source Scope
42
+
43
+ RepoSpend scans both Claude project transcripts and Claude Desktop/local-agent
44
+ session roots:
45
+
46
+ ```text
47
+ ~/.claude/projects
48
+ ~/.config/claude/projects
49
+ ~/Library/Application Support/Claude/local-agent-mode-sessions
50
+ ~/.config/Claude/local-agent-mode-sessions
51
+ ```
52
+
53
+ Many terminal-first usage tools focus on `~/.claude/projects`. If local-agent
54
+ session files exist, RepoSpend can show higher Claude totals than `ccusage` or
55
+ Tokscale without a parser bug. When comparing tools, first check whether the
56
+ same Claude roots are included.
57
+
58
+ For Claude, RepoSpend also preserves the cache-write TTL split when it is present
59
+ in local logs:
60
+
61
+ ```text
62
+ 5-minute cache write = cacheCreationInputTokens5m
63
+ 1-hour cache write = cacheCreationInputTokens1h
64
+ unclassified cache write = cacheCreationInputTokens - cacheCreationInputTokens5m - cacheCreationInputTokens1h
65
+ ```
66
+
67
+ This is one reason RepoSpend API-equivalent cost can differ from `ccusage`.
68
+ Anthropic prices 5-minute cache writes at a different rate than 1-hour cache
69
+ writes. RepoSpend applies the recorded TTL-specific rate for each bucket; tools
70
+ that collapse all Claude cache creation tokens into a single cache-write column
71
+ can drift when a session mixes TTLs or when the chosen single rate does not match
72
+ the session's actual cache usage.
73
+
40
74
  ## Reasoning Tokens
41
75
 
42
76
  RepoSpend keeps reasoning tokens separate when the local source exposes them. The accurate treatment depends on the source's raw usage shape:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repospend",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local-first dashboard for tracking AI coding token usage and API-equivalent spend by repository, session, model, and tool.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Mehmet Mustafa Demir",
@@ -45,7 +45,8 @@
45
45
  "typecheck": "pnpm --filter @repospend/types --filter @repospend/core build && pnpm --stream --filter @repospend/types --filter @repospend/core --filter @repospend/server --filter @repospend/web typecheck",
46
46
  "clean": "pnpm -r clean && rm -rf dist web-dist",
47
47
  "rebuild:native": "pnpm rebuild better-sqlite3",
48
- "doctor:native": "node scripts/check-native.mjs"
48
+ "doctor:native": "node scripts/check-native.mjs",
49
+ "media:capture": "node scripts/capture-release-media.mjs"
49
50
  },
50
51
  "keywords": [
51
52
  "codex",
@@ -81,6 +82,9 @@
81
82
  "@types/node": "^22.19.1",
82
83
  "esbuild": "^0.28.0",
83
84
  "eslint": "^9.39.1",
85
+ "gifenc": "^1.0.3",
86
+ "playwright": "^1.60.0",
87
+ "sharp": "^0.35.1",
84
88
  "typescript": "^5.9.3",
85
89
  "typescript-eslint": "^8.46.4",
86
90
  "vitest": "^3.2.4"