ccusage 0.5.0 → 0.6.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.
Files changed (43) hide show
  1. package/README.md +65 -20
  2. package/dist/calculate-cost.d.ts +11 -8
  3. package/dist/{core-BgFXUe_h.js → core-B0ovMhJe.js} +4 -4
  4. package/dist/{data-loader-r5ZcMQy7.js → data-loader-DP5qBPn6.js} +153 -96
  5. package/dist/{data-loader-LMCrJ-lW.d.ts → data-loader-VdEcqJHc.d.ts} +28 -13
  6. package/dist/data-loader.d.ts +3 -5
  7. package/dist/data-loader.js +5 -5
  8. package/dist/{debug-BVxGf4UL.js → debug-C_5Qx11m.js} +13 -13
  9. package/dist/debug.d.ts +4 -4
  10. package/dist/debug.js +5 -5
  11. package/dist/{dist-FwNhpFrW.js → dist-C0-Tf5eD.js} +1 -92
  12. package/dist/{dist-C_i5I27w.js → dist-LwbOR2Yw.js} +5 -5
  13. package/dist/{effect-WSjEuzC9-BsxP11fz.js → effect-WSjEuzC9-CJfWUy0j.js} +1 -1
  14. package/dist/{esm-vjyZjnpZ.js → esm-Dqsc1zmX.js} +1 -1
  15. package/dist/{index-CISmcbXk-BotItq1T.js → index-CISmcbXk-DCA05NUL.js} +5 -5
  16. package/dist/index.js +246 -62
  17. package/dist/{logger-Cu4Ir1a5.js → logger-DsQC4OvA.js} +17 -17
  18. package/dist/logger.js +1 -1
  19. package/dist/{mcp-DAzj5Pua.js → mcp-BQdv12mr.js} +83 -74
  20. package/dist/mcp.d.ts +2 -4
  21. package/dist/mcp.js +7 -8
  22. package/dist/{pricing-fetcher-B5yPtoTB.js → pricing-fetcher-BPUgMrB_.js} +9 -9
  23. package/dist/{index-BurjgCfW.d.ts → pricing-fetcher-CfEgfzSr.d.ts} +19 -249
  24. package/dist/pricing-fetcher.d.ts +1 -2
  25. package/dist/pricing-fetcher.js +3 -3
  26. package/dist/{prompt-IToGuko2.js → prompt-DljZqwMa.js} +4 -4
  27. package/dist/{sury-DmrZ3_Oj-DhGOjCNc.js → sury-DmrZ3_Oj-CCL_DlTt.js} +1 -1
  28. package/dist/{types-CFnCBr2I.js → types-DS8M8QF_.js} +4 -4
  29. package/dist/{valibot-CQk-M5rL-Cq5E7F3g.js → valibot-CQk-M5rL-CkjrLVu1.js} +2 -2
  30. package/dist/{zod-Db63SLXj-BWdcigdx.js → zod-Db63SLXj-Dyc_OWjq.js} +3 -3
  31. package/package.json +8 -11
  32. package/dist/pricing-fetcher-DygIroMj.d.ts +0 -21
  33. package/dist/shared-args-DN3jRldX.js +0 -61
  34. package/dist/shared-args.d.ts +0 -94
  35. package/dist/shared-args.js +0 -8
  36. package/dist/types-B3ib19os.d.ts +0 -79
  37. package/dist/types-DFrbJmnT.js +0 -41
  38. package/dist/types.d.ts +0 -3
  39. package/dist/types.js +0 -4
  40. package/dist/utils-C7kg8MXN.js +0 -10
  41. package/dist/utils.d.ts +0 -5
  42. package/dist/utils.js +0 -3
  43. /package/dist/{arktype-C-GObzDh-Dj1DVoqC.js → arktype-C-GObzDh-Bx7Fdrqj.js} +0 -0
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/ccusage?color=yellow)](https://npmjs.com/package/ccusage)
6
6
  [![NPM Downloads](https://img.shields.io/npm/dy/ccusage)](https://www.npmjs.com/package/ccusage)
7
7
  [![DeepWiki](https://img.shields.io/badge/DeepWiki-ryoppippi%2Fccusage-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/ryoppippi/ccusage)
8
+
8
9
  <!-- DeepWiki badge generated by https://deepwiki.ryoppippi.com/ -->
9
10
 
10
11
  <div align="center">
@@ -16,34 +17,36 @@ A CLI tool for analyzing Claude Code usage from local JSONL files.
16
17
  Inspired by [this article](https://note.com/milliondev/n/n1d018da2d769) about tracking Claude Code usage costs.
17
18
 
18
19
  ## What is `ccusage` (by NotebookLM)
20
+
19
21
  <details>
20
22
  <summary>Podcact</summary>
21
23
 
22
24
  # English
23
- https://github.com/user-attachments/assets/7a00f2f3-82a7-41b6-a8da-e04b76b5e35a
24
25
 
26
+ https://github.com/user-attachments/assets/7a00f2f3-82a7-41b6-a8da-e04b76b5e35a
25
27
 
26
28
  # 日本語
29
+
27
30
  https://github.com/user-attachments/assets/db09fc06-bf57-4d37-9b06-514851bcc1d0
28
31
 
29
32
  </details>
30
33
 
31
-
32
34
  ## Motivation
33
35
 
34
- Claude Code's Max plan offers unlimited usage - but wouldn't it be interesting to know how much you'd be paying if you were on a pay-per-use plan?
36
+ Claude Code's Max plan offers unlimited usage - but wouldn't it be interesting to know how much you'd be paying if you were on a pay-per-use plan?
35
37
 
36
38
  This tool helps you understand the value you're getting from your subscription by calculating the equivalent costs of your actual usage. See how much you're saving and enjoy that satisfying feeling of getting great value! 😊
37
39
 
38
40
  ## Features
39
41
 
40
42
  - 📊 **Daily Report**: View token usage and costs aggregated by date
43
+ - 📅 **Monthly Report**: View token usage and costs aggregated by month
41
44
  - 💬 **Session Report**: View usage grouped by conversation sessions
42
45
  - 📅 **Date Filtering**: Filter reports by date range using `--since` and `--until`
43
46
  - 📁 **Custom Path**: Support for custom Claude data directory locations
44
47
  - 🎨 **Beautiful Output**: Colorful table-formatted display
45
48
  - 📄 **JSON Output**: Export data in structured JSON format with `--json`
46
- - 💰 **Cost Tracking**: Shows costs in USD for each day/session
49
+ - 💰 **Cost Tracking**: Shows costs in USD for each day/month/session
47
50
  - 🔄 **Cache Token Support**: Tracks and displays cache creation and cache read tokens separately
48
51
 
49
52
  ## Important Disclaimer
@@ -51,6 +54,7 @@ This tool helps you understand the value you're getting from your subscription b
51
54
  ⚠️ **This is NOT an official Claude tool** - it's an independent community project that analyzes locally stored usage data.
52
55
 
53
56
  **Cost calculations are estimates only** and may not reflect actual billing:
57
+
54
58
  - Costs shown are virtual/estimated based on token counts and model pricing data
55
59
  - Actual costs may vary due to pricing changes, special rates, or billing adjustments
56
60
  - We do not guarantee the accuracy of calculated costs
@@ -134,10 +138,41 @@ ccusage daily --json
134
138
  ccusage daily --mode auto # Use costUSD when available, calculate otherwise (default)
135
139
  ccusage daily --mode calculate # Always calculate costs from tokens
136
140
  ccusage daily --mode display # Always show pre-calculated costUSD values
141
+
142
+ # Control sort order
143
+ ccusage daily --order asc # Show oldest dates first
144
+ ccusage daily --order desc # Show newest dates first (default)
137
145
  ```
138
146
 
139
147
  `ccusage` is an alias for `ccusage daily`, so you can run it without specifying the subcommand.
140
148
 
149
+ ### Monthly Report
150
+
151
+ Shows token usage and costs aggregated by month:
152
+
153
+ ```bash
154
+ # Show all monthly usage
155
+ ccusage monthly
156
+
157
+ # Filter by date range
158
+ ccusage monthly --since 20250101 --until 20250531
159
+
160
+ # Use custom Claude data directory
161
+ ccusage monthly --path /custom/path/to/.claude
162
+
163
+ # Output in JSON format
164
+ ccusage monthly --json
165
+
166
+ # Control cost calculation mode
167
+ ccusage monthly --mode auto # Use costUSD when available, calculate otherwise (default)
168
+ ccusage monthly --mode calculate # Always calculate costs from tokens
169
+ ccusage monthly --mode display # Always show pre-calculated costUSD values
170
+
171
+ # Control sort order
172
+ ccusage monthly --order asc # Show oldest months first
173
+ ccusage monthly --order desc # Show newest months first (default)
174
+ ```
175
+
141
176
  ### Session Report
142
177
 
143
178
  Shows usage grouped by conversation sessions, sorted by cost:
@@ -159,6 +194,10 @@ ccusage session --json
159
194
  ccusage session --mode auto # Use costUSD when available, calculate otherwise (default)
160
195
  ccusage session --mode calculate # Always calculate costs from tokens
161
196
  ccusage session --mode display # Always show pre-calculated costUSD values
197
+
198
+ # Control sort order
199
+ ccusage session --order asc # Show oldest sessions first
200
+ ccusage session --order desc # Show newest sessions first (default)
162
201
  ```
163
202
 
164
203
  ### Options
@@ -166,10 +205,11 @@ ccusage session --mode display # Always show pre-calculated costUSD values
166
205
  All commands support the following options:
167
206
 
168
207
  - `-s, --since <date>`: Filter from date (YYYYMMDD format)
169
- - `-u, --until <date>`: Filter until date (YYYYMMDD format)
208
+ - `-u, --until <date>`: Filter until date (YYYYMMDD format)
170
209
  - `-p, --path <path>`: Custom path to Claude data directory (default: `~/.claude`)
171
210
  - `-j, --json`: Output results in JSON format instead of table
172
211
  - `-m, --mode <mode>`: Cost calculation mode: `auto` (default), `calculate`, or `display`
212
+ - `-o, --order <order>`: Sort order: `desc` (newest first, default) or `asc` (oldest first).
173
213
  - `-d, --debug`: Show pricing mismatch information for debugging
174
214
  - `--debug-samples <number>`: Number of sample discrepancies to show in debug output (default: 5)
175
215
  - `-h, --help`: Display help message
@@ -197,10 +237,12 @@ ccusage mcp --mode calculate
197
237
  ```
198
238
 
199
239
  The MCP server supports both **stdio** and **HTTP stream** transports:
240
+
200
241
  - **stdio** (default): Best for local integration where the client directly spawns the process
201
242
  - **HTTP stream**: Best for remote access when you need to call the server from another machine or network location
202
243
 
203
244
  Available MCP tools:
245
+
204
246
  - `daily`: Returns daily usage reports (accepts `since`, `until`, `mode` parameters)
205
247
  - `session`: Returns session usage reports (accepts `since`, `until`, `mode` parameters)
206
248
 
@@ -212,18 +254,18 @@ Available MCP tools:
212
254
 
213
255
  To use ccusage MCP with Claude Desktop, add this to your Claude Desktop configuration file:
214
256
 
215
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
257
+ **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
216
258
  **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
217
259
 
218
260
  ```json
219
261
  {
220
- "mcpServers": {
221
- "ccusage": {
222
- "command": "npx",
223
- "args": ["ccusage@latest", "mcp"],
224
- "env": {}
225
- }
226
- }
262
+ "mcpServers": {
263
+ "ccusage": {
264
+ "command": "npx",
265
+ "args": ["ccusage@latest", "mcp"],
266
+ "env": {}
267
+ }
268
+ }
227
269
  }
228
270
  ```
229
271
 
@@ -231,13 +273,13 @@ Or if you have ccusage installed globally:
231
273
 
232
274
  ```json
233
275
  {
234
- "mcpServers": {
235
- "ccusage": {
236
- "command": "ccusage",
237
- "args": ["mcp"],
238
- "env": {}
239
- }
240
- }
276
+ "mcpServers": {
277
+ "ccusage": {
278
+ "command": "ccusage",
279
+ "args": ["mcp"],
280
+ "env": {}
281
+ }
282
+ }
241
283
  }
242
284
  ```
243
285
 
@@ -246,6 +288,7 @@ After adding this configuration, restart Claude Desktop. You'll then be able to
246
288
  ## Output Example
247
289
 
248
290
  ### Daily Report
291
+
249
292
  ```
250
293
  ╭──────────────────────────────────────────╮
251
294
  │ │
@@ -265,6 +308,7 @@ After adding this configuration, restart Claude Desktop. You'll then be able to
265
308
  ```
266
309
 
267
310
  ### Session Report
311
+
268
312
  ```
269
313
  ╭───────────────────────────────────────────────╮
270
314
  │ │
@@ -284,6 +328,7 @@ After adding this configuration, restart Claude Desktop. You'll then be able to
284
328
  ```
285
329
 
286
330
  ## Requirements
331
+
287
332
  - Claude Code usage history files (`~/.claude/projects/**/*.jsonl`)
288
333
 
289
334
  ## License
@@ -1,18 +1,21 @@
1
- import "./index-BurjgCfW.js";
2
- import "./pricing-fetcher-DygIroMj.js";
3
- import { TokenData, TokenTotals } from "./types-B3ib19os.js";
4
- import { DailyUsage, SessionUsage } from "./data-loader-LMCrJ-lW.js";
1
+ import "./pricing-fetcher-CfEgfzSr.js";
2
+ import { DailyUsage, MonthlyUsage, SessionUsage } from "./data-loader-VdEcqJHc.js";
5
3
 
6
4
  //#region src/calculate-cost.d.ts
7
- declare function calculateTotals(data: Array<DailyUsage | SessionUsage>): TokenTotals;
8
- declare function getTotalTokens(tokens: TokenData): number;
9
- declare function createTotalsObject(totals: TokenTotals): {
5
+ type TokenData = {
10
6
  inputTokens: number;
11
7
  outputTokens: number;
12
8
  cacheCreationTokens: number;
13
9
  cacheReadTokens: number;
14
- totalTokens: number;
10
+ };
11
+ type TokenTotals = TokenData & {
15
12
  totalCost: number;
16
13
  };
14
+ type TotalsObject = TokenTotals & {
15
+ totalTokens: number;
16
+ };
17
+ declare function calculateTotals(data: Array<DailyUsage | MonthlyUsage | SessionUsage>): TokenTotals;
18
+ declare function getTotalTokens(tokens: TokenData): number;
19
+ declare function createTotalsObject(totals: TokenTotals): TotalsObject;
17
20
  //#endregion
18
21
  export { calculateTotals, createTotalsObject, getTotalTokens };
@@ -38,8 +38,8 @@ const $output = Symbol("ZodOutput");
38
38
  const $input = Symbol("ZodInput");
39
39
  var $ZodRegistry = class {
40
40
  constructor() {
41
- this._map = new WeakMap();
42
- this._idmap = new Map();
41
+ this._map = /* @__PURE__ */ new WeakMap();
42
+ this._idmap = /* @__PURE__ */ new Map();
43
43
  }
44
44
  add(schema, ..._meta) {
45
45
  const meta = _meta[0];
@@ -85,7 +85,7 @@ var JSONSchemaGenerator = class {
85
85
  this.unrepresentable = params?.unrepresentable ?? "throw";
86
86
  this.override = params?.override ?? (() => {});
87
87
  this.io = params?.io ?? "output";
88
- this.seen = new Map();
88
+ this.seen = /* @__PURE__ */ new Map();
89
89
  }
90
90
  process(schema, _params = {
91
91
  path: [],
@@ -630,7 +630,7 @@ function toJSONSchema(input, _params) {
630
630
  return gen.emit(input, _params);
631
631
  }
632
632
  function isTransforming(_schema, _ctx) {
633
- const ctx = _ctx ?? { seen: new Set() };
633
+ const ctx = _ctx ?? { seen: /* @__PURE__ */ new Set() };
634
634
  if (ctx.seen.has(_schema)) return false;
635
635
  ctx.seen.add(_schema);
636
636
  const schema = _schema;
@@ -1,5 +1,5 @@
1
- import { array, number, object, optional, safeParse, string } from "./dist-FwNhpFrW.js";
2
- import { calculateCostFromTokens, fetchModelPricing, getModelPricing } from "./pricing-fetcher-B5yPtoTB.js";
1
+ import { array, number, object, optional, pipe, regex, safeParse, string } from "./dist-C0-Tf5eD.js";
2
+ import { calculateCostFromTokens, fetchModelPricing, getModelPricing } from "./pricing-fetcher-BPUgMrB_.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { homedir } from "node:os";
@@ -575,7 +575,7 @@ var require_walker = __commonJS({ "node_modules/fdir/dist/api/walker.js"(exports
575
575
  counts: new counter_1.Counter(),
576
576
  options,
577
577
  queue: new queue_1.Queue((error, state) => this.callbackInvoker(state, error, callback$1)),
578
- symlinks: new Map(),
578
+ symlinks: /* @__PURE__ */ new Map(),
579
579
  visited: [""].slice(0, 0),
580
580
  controller: new AbortController()
581
581
  };
@@ -2110,9 +2110,9 @@ var require_picomatch$1 = __commonJS({ "node_modules/picomatch/lib/picomatch.js"
2110
2110
  if (glob$1 === "" || typeof glob$1 !== "string" && !isState) throw new TypeError("Expected pattern to be a non-empty string");
2111
2111
  const opts = options || {};
2112
2112
  const posix$1 = opts.windows;
2113
- const regex = isState ? picomatch$2.compileRe(glob$1, options) : picomatch$2.makeRe(glob$1, options, false, true);
2114
- const state = regex.state;
2115
- delete regex.state;
2113
+ const regex$1 = isState ? picomatch$2.compileRe(glob$1, options) : picomatch$2.makeRe(glob$1, options, false, true);
2114
+ const state = regex$1.state;
2115
+ delete regex$1.state;
2116
2116
  let isIgnored = () => false;
2117
2117
  if (opts.ignore) {
2118
2118
  const ignoreOpts = {
@@ -2124,14 +2124,14 @@ var require_picomatch$1 = __commonJS({ "node_modules/picomatch/lib/picomatch.js"
2124
2124
  isIgnored = picomatch$2(opts.ignore, ignoreOpts, returnState);
2125
2125
  }
2126
2126
  const matcher = (input, returnObject = false) => {
2127
- const { isMatch, match, output } = picomatch$2.test(input, regex, options, {
2127
+ const { isMatch, match, output } = picomatch$2.test(input, regex$1, options, {
2128
2128
  glob: glob$1,
2129
2129
  posix: posix$1
2130
2130
  });
2131
2131
  const result = {
2132
2132
  glob: glob$1,
2133
2133
  state,
2134
- regex,
2134
+ regex: regex$1,
2135
2135
  posix: posix$1,
2136
2136
  input,
2137
2137
  output,
@@ -2170,7 +2170,7 @@ var require_picomatch$1 = __commonJS({ "node_modules/picomatch/lib/picomatch.js"
2170
2170
  * @return {Object} Returns an object with matching info.
2171
2171
  * @api public
2172
2172
  */
2173
- picomatch$2.test = (input, regex, options, { glob: glob$1, posix: posix$1 } = {}) => {
2173
+ picomatch$2.test = (input, regex$1, options, { glob: glob$1, posix: posix$1 } = {}) => {
2174
2174
  if (typeof input !== "string") throw new TypeError("Expected input to be a string");
2175
2175
  if (input === "") return {
2176
2176
  isMatch: false,
@@ -2184,8 +2184,8 @@ var require_picomatch$1 = __commonJS({ "node_modules/picomatch/lib/picomatch.js"
2184
2184
  output = format ? format(input) : input;
2185
2185
  match = output === glob$1;
2186
2186
  }
2187
- if (match === false || opts.capture === true) if (opts.matchBase === true || opts.basename === true) match = picomatch$2.matchBase(input, regex, options, posix$1);
2188
- else match = regex.exec(output);
2187
+ if (match === false || opts.capture === true) if (opts.matchBase === true || opts.basename === true) match = picomatch$2.matchBase(input, regex$1, options, posix$1);
2188
+ else match = regex$1.exec(output);
2189
2189
  return {
2190
2190
  isMatch: Boolean(match),
2191
2191
  match,
@@ -2206,8 +2206,8 @@ var require_picomatch$1 = __commonJS({ "node_modules/picomatch/lib/picomatch.js"
2206
2206
  * @api public
2207
2207
  */
2208
2208
  picomatch$2.matchBase = (input, glob$1, options) => {
2209
- const regex = glob$1 instanceof RegExp ? glob$1 : picomatch$2.makeRe(glob$1, options);
2210
- return regex.test(utils$1.basename(input));
2209
+ const regex$1 = glob$1 instanceof RegExp ? glob$1 : picomatch$2.makeRe(glob$1, options);
2210
+ return regex$1.test(utils$1.basename(input));
2211
2211
  };
2212
2212
  /**
2213
2213
  * Returns true if **any** of the given glob `patterns` match the specified `string`.
@@ -2291,9 +2291,9 @@ var require_picomatch$1 = __commonJS({ "node_modules/picomatch/lib/picomatch.js"
2291
2291
  const append = opts.contains ? "" : "$";
2292
2292
  let source = `${prepend}(?:${state.output})${append}`;
2293
2293
  if (state && state.negated === true) source = `^(?!${source}).*$`;
2294
- const regex = picomatch$2.toRegex(source, options);
2295
- if (returnState === true) regex.state = state;
2296
- return regex;
2294
+ const regex$1 = picomatch$2.toRegex(source, options);
2295
+ if (returnState === true) regex$1.state = state;
2296
+ return regex$1;
2297
2297
  };
2298
2298
  /**
2299
2299
  * Create a regular expression from a parsed glob pattern.
@@ -2573,14 +2573,14 @@ function getPartialMatcher(patterns, options) {
2573
2573
  if (inputParts[0] === ".." && ONLY_PARENT_DIRECTORIES.test(input)) return true;
2574
2574
  for (let i = 0; i < patterns.length; i++) {
2575
2575
  const patternParts = patternsParts[i];
2576
- const regex = regexes[i];
2576
+ const regex$1 = regexes[i];
2577
2577
  const inputPatternCount = inputParts.length;
2578
2578
  const minParts = Math.min(inputPatternCount, patternParts.length);
2579
2579
  let j = 0;
2580
2580
  while (j < minParts) {
2581
2581
  const part = patternParts[j];
2582
2582
  if (part.includes("/")) return true;
2583
- const match = regex[j].test(inputParts[j]);
2583
+ const match = regex$1[j].test(inputParts[j]);
2584
2584
  if (!match) break;
2585
2585
  if (part === "**") return true;
2586
2586
  j++;
@@ -2608,7 +2608,7 @@ function isDynamicPattern(pattern, options) {
2608
2608
  return scan$2.isGlob || scan$2.negated;
2609
2609
  }
2610
2610
  function log(...tasks) {
2611
- console.log(`[tinyglobby ${new Date().toLocaleTimeString("es")}]`, ...tasks);
2611
+ console.log(`[tinyglobby ${(/* @__PURE__ */ new Date()).toLocaleTimeString("es")}]`, ...tasks);
2612
2612
  }
2613
2613
  const PARENT_DIRECTORY = /^(\/?\.\.)+/;
2614
2614
  const ESCAPING_BACKSLASHES = /\\(?=[()[\]{}!*+?@|])/g;
@@ -2770,7 +2770,9 @@ async function glob(patternsOrOptions, options) {
2770
2770
 
2771
2771
  //#endregion
2772
2772
  //#region src/data-loader.ts
2773
- const getDefaultClaudePath = () => path.join(homedir(), ".claude");
2773
+ function getDefaultClaudePath() {
2774
+ return path.join(homedir(), ".claude");
2775
+ }
2774
2776
  const UsageDataSchema = object({
2775
2777
  timestamp: string(),
2776
2778
  version: optional(string()),
@@ -2786,7 +2788,7 @@ const UsageDataSchema = object({
2786
2788
  costUSD: optional(number())
2787
2789
  });
2788
2790
  const DailyUsageSchema = object({
2789
- date: string(),
2791
+ date: pipe(string(), regex(/^\d{4}-\d{2}-\d{2}$/)),
2790
2792
  inputTokens: number(),
2791
2793
  outputTokens: number(),
2792
2794
  cacheCreationTokens: number(),
@@ -2804,30 +2806,38 @@ const SessionUsageSchema = object({
2804
2806
  lastActivity: string(),
2805
2807
  versions: array(string())
2806
2808
  });
2807
- const formatDate = (dateStr) => {
2809
+ const MonthlyUsageSchema = object({
2810
+ month: pipe(string(), regex(/^\d{4}-\d{2}$/)),
2811
+ inputTokens: number(),
2812
+ outputTokens: number(),
2813
+ cacheCreationTokens: number(),
2814
+ cacheReadTokens: number(),
2815
+ totalCost: number()
2816
+ });
2817
+ function formatDate(dateStr) {
2808
2818
  const date = new Date(dateStr);
2809
2819
  const year = date.getFullYear();
2810
2820
  const month = String(date.getMonth() + 1).padStart(2, "0");
2811
2821
  const day = String(date.getDate()).padStart(2, "0");
2812
2822
  return `${year}-${month}-${day}`;
2813
- };
2814
- const calculateCostForEntry = (data, mode, modelPricing) => {
2823
+ }
2824
+ function calculateCostForEntry(data, mode, modelPricing) {
2815
2825
  if (mode === "display") return data.costUSD ?? 0;
2816
2826
  if (mode === "calculate") {
2817
- if (data.message.model) {
2827
+ if (data.message.model != null) {
2818
2828
  const pricing = getModelPricing(data.message.model, modelPricing);
2819
- if (pricing) return calculateCostFromTokens(data.message.usage, pricing);
2829
+ if (pricing != null) return calculateCostFromTokens(data.message.usage, pricing);
2820
2830
  }
2821
2831
  return 0;
2822
2832
  }
2823
- if (data.costUSD !== void 0) return data.costUSD;
2824
- if (data.message.model) {
2833
+ if (data.costUSD != null) return data.costUSD;
2834
+ if (data.message.model != null) {
2825
2835
  const pricing = getModelPricing(data.message.model, modelPricing);
2826
- if (pricing) return calculateCostFromTokens(data.message.usage, pricing);
2836
+ if (pricing != null) return calculateCostFromTokens(data.message.usage, pricing);
2827
2837
  }
2828
2838
  return 0;
2829
- };
2830
- async function loadUsageData(options) {
2839
+ }
2840
+ async function loadDailyUsageData(options) {
2831
2841
  const claudePath = options?.claudePath ?? getDefaultClaudePath();
2832
2842
  const claudeDir = path.join(claudePath, "projects");
2833
2843
  const files = await glob(["**/*.jsonl"], {
@@ -2835,9 +2845,9 @@ async function loadUsageData(options) {
2835
2845
  absolute: true
2836
2846
  });
2837
2847
  if (files.length === 0) return [];
2838
- const mode = options?.mode || "auto";
2848
+ const mode = options?.mode ?? "auto";
2839
2849
  const modelPricing = mode === "display" ? {} : await fetchModelPricing();
2840
- const dailyMap = new Map();
2850
+ const allEntries = [];
2841
2851
  for (const file of files) {
2842
2852
  const content = await readFile(file, "utf-8");
2843
2853
  const lines = content.trim().split("\n").filter((line) => line.length > 0);
@@ -2847,31 +2857,43 @@ async function loadUsageData(options) {
2847
2857
  if (!result.success) continue;
2848
2858
  const data = result.output;
2849
2859
  const date = formatDate(data.timestamp);
2850
- const existing = dailyMap.get(date) || {
2851
- date,
2852
- inputTokens: 0,
2853
- outputTokens: 0,
2854
- cacheCreationTokens: 0,
2855
- cacheReadTokens: 0,
2856
- totalCost: 0
2857
- };
2858
- existing.inputTokens += data.message.usage.input_tokens ?? 0;
2859
- existing.outputTokens += data.message.usage.output_tokens ?? 0;
2860
- existing.cacheCreationTokens += data.message.usage.cache_creation_input_tokens ?? 0;
2861
- existing.cacheReadTokens += data.message.usage.cache_read_input_tokens ?? 0;
2862
2860
  const cost = calculateCostForEntry(data, mode, modelPricing);
2863
- existing.totalCost += cost;
2864
- dailyMap.set(date, existing);
2865
- } catch (e) {}
2861
+ allEntries.push({
2862
+ data,
2863
+ date,
2864
+ cost
2865
+ });
2866
+ } catch {}
2866
2867
  }
2867
- let results = Array.from(dailyMap.values());
2868
- if (options?.since || options?.until) results = results.filter((data) => {
2869
- const dateStr = data.date.replace(/-/g, "");
2870
- if (options.since && dateStr < options.since) return false;
2871
- if (options.until && dateStr > options.until) return false;
2868
+ const groupedByDate = Object.groupBy(allEntries, (entry) => entry.date);
2869
+ const results = Object.entries(groupedByDate).map(([date, entries]) => {
2870
+ if (entries == null) return void 0;
2871
+ return entries.reduce((acc, entry) => ({
2872
+ date,
2873
+ inputTokens: acc.inputTokens + (entry.data.message.usage.input_tokens ?? 0),
2874
+ outputTokens: acc.outputTokens + (entry.data.message.usage.output_tokens ?? 0),
2875
+ cacheCreationTokens: acc.cacheCreationTokens + (entry.data.message.usage.cache_creation_input_tokens ?? 0),
2876
+ cacheReadTokens: acc.cacheReadTokens + (entry.data.message.usage.cache_read_input_tokens ?? 0),
2877
+ totalCost: acc.totalCost + entry.cost
2878
+ }), {
2879
+ date,
2880
+ inputTokens: 0,
2881
+ outputTokens: 0,
2882
+ cacheCreationTokens: 0,
2883
+ cacheReadTokens: 0,
2884
+ totalCost: 0
2885
+ });
2886
+ }).filter((item) => item != null).filter((item) => {
2887
+ if (options?.since != null || options?.until != null) {
2888
+ const dateStr = item.date.replace(/-/g, "");
2889
+ if (options.since != null && dateStr < options.since) return false;
2890
+ if (options.until != null && dateStr > options.until) return false;
2891
+ }
2872
2892
  return true;
2873
2893
  });
2874
- return sort(results).desc((item) => new Date(item.date).getTime());
2894
+ const sortOrder = options?.order ?? "desc";
2895
+ const sortedResults = sort(results);
2896
+ return sortOrder === "desc" ? sortedResults.desc((item) => new Date(item.date).getTime()) : sortedResults.asc((item) => new Date(item.date).getTime());
2875
2897
  }
2876
2898
  async function loadSessionData(options) {
2877
2899
  const claudePath = options?.claudePath ?? getDefaultClaudePath();
@@ -2881,64 +2903,99 @@ async function loadSessionData(options) {
2881
2903
  absolute: true
2882
2904
  });
2883
2905
  if (files.length === 0) return [];
2884
- const mode = options?.mode || "auto";
2906
+ const mode = options?.mode ?? "auto";
2885
2907
  const modelPricing = mode === "display" ? {} : await fetchModelPricing();
2886
- const sessionMap = new Map();
2908
+ const allEntries = [];
2887
2909
  for (const file of files) {
2888
2910
  const relativePath = path.relative(claudeDir, file);
2889
2911
  const parts = relativePath.split(path.sep);
2890
- const sessionId = parts[parts.length - 2];
2891
- const projectPath = parts.slice(0, -2).join(path.sep);
2912
+ const sessionId = parts[parts.length - 2] ?? "unknown";
2913
+ const joinedPath = parts.slice(0, -2).join(path.sep);
2914
+ const projectPath = joinedPath.length > 0 ? joinedPath : "Unknown Project";
2892
2915
  const content = await readFile(file, "utf-8");
2893
2916
  const lines = content.trim().split("\n").filter((line) => line.length > 0);
2894
- let lastTimestamp = "";
2895
2917
  for (const line of lines) try {
2896
2918
  const parsed = JSON.parse(line);
2897
2919
  const result = safeParse(UsageDataSchema, parsed);
2898
2920
  if (!result.success) continue;
2899
2921
  const data = result.output;
2900
- const key = `${projectPath}/${sessionId}`;
2901
- const existing = sessionMap.get(key) || {
2902
- sessionId: sessionId || "unknown",
2903
- projectPath: projectPath || "Unknown Project",
2904
- inputTokens: 0,
2905
- outputTokens: 0,
2906
- cacheCreationTokens: 0,
2907
- cacheReadTokens: 0,
2908
- totalCost: 0,
2909
- lastActivity: "",
2910
- versions: [],
2911
- versionSet: new Set()
2912
- };
2913
- existing.inputTokens += data.message.usage.input_tokens ?? 0;
2914
- existing.outputTokens += data.message.usage.output_tokens ?? 0;
2915
- existing.cacheCreationTokens += data.message.usage.cache_creation_input_tokens ?? 0;
2916
- existing.cacheReadTokens += data.message.usage.cache_read_input_tokens ?? 0;
2922
+ const sessionKey = `${projectPath}/${sessionId}`;
2917
2923
  const cost = calculateCostForEntry(data, mode, modelPricing);
2918
- existing.totalCost += cost;
2919
- if (data.timestamp > lastTimestamp) {
2920
- lastTimestamp = data.timestamp;
2921
- existing.lastActivity = formatDate(data.timestamp);
2922
- }
2923
- if (data.version) existing.versionSet.add(data.version);
2924
- sessionMap.set(key, existing);
2925
- } catch (e) {}
2924
+ allEntries.push({
2925
+ data,
2926
+ sessionKey,
2927
+ sessionId,
2928
+ projectPath,
2929
+ cost,
2930
+ timestamp: data.timestamp
2931
+ });
2932
+ } catch {}
2926
2933
  }
2927
- let results = Array.from(sessionMap.values()).map((session) => {
2928
- const { versionSet,...sessionData } = session;
2929
- return {
2930
- ...sessionData,
2934
+ const groupedBySessions = Object.groupBy(allEntries, (entry) => entry.sessionKey);
2935
+ const results = Object.entries(groupedBySessions).map(([_, entries]) => {
2936
+ if (entries == null) return void 0;
2937
+ const latestEntry = entries.reduce((latest, current) => current.timestamp > latest.timestamp ? current : latest);
2938
+ const versionSet = /* @__PURE__ */ new Set();
2939
+ for (const entry of entries) if (entry.data.version != null) versionSet.add(entry.data.version);
2940
+ const aggregated = entries.reduce((acc, entry) => ({
2941
+ sessionId: latestEntry.sessionId,
2942
+ projectPath: latestEntry.projectPath,
2943
+ inputTokens: acc.inputTokens + (entry.data.message.usage.input_tokens ?? 0),
2944
+ outputTokens: acc.outputTokens + (entry.data.message.usage.output_tokens ?? 0),
2945
+ cacheCreationTokens: acc.cacheCreationTokens + (entry.data.message.usage.cache_creation_input_tokens ?? 0),
2946
+ cacheReadTokens: acc.cacheReadTokens + (entry.data.message.usage.cache_read_input_tokens ?? 0),
2947
+ totalCost: acc.totalCost + entry.cost,
2948
+ lastActivity: formatDate(latestEntry.timestamp),
2931
2949
  versions: Array.from(versionSet).sort()
2932
- };
2933
- });
2934
- if (options?.since || options?.until) results = results.filter((session) => {
2935
- const dateStr = session.lastActivity.replace(/-/g, "");
2936
- if (options.since && dateStr < options.since) return false;
2937
- if (options.until && dateStr > options.until) return false;
2950
+ }), {
2951
+ sessionId: latestEntry.sessionId,
2952
+ projectPath: latestEntry.projectPath,
2953
+ inputTokens: 0,
2954
+ outputTokens: 0,
2955
+ cacheCreationTokens: 0,
2956
+ cacheReadTokens: 0,
2957
+ totalCost: 0,
2958
+ lastActivity: formatDate(latestEntry.timestamp),
2959
+ versions: Array.from(versionSet).sort()
2960
+ });
2961
+ return aggregated;
2962
+ }).filter((item) => item != null).filter((item) => {
2963
+ if (options?.since != null || options?.until != null) {
2964
+ const dateStr = item.lastActivity.replace(/-/g, "");
2965
+ if (options.since != null && dateStr < options.since) return false;
2966
+ if (options.until != null && dateStr > options.until) return false;
2967
+ }
2938
2968
  return true;
2939
2969
  });
2940
- return sort(results).desc((item) => new Date(item.lastActivity).getTime());
2970
+ const sortOrder = options?.order ?? "desc";
2971
+ const sortedResults = sort(results);
2972
+ return sortOrder === "desc" ? sortedResults.desc((item) => new Date(item.lastActivity).getTime()) : sortedResults.asc((item) => new Date(item.lastActivity).getTime());
2973
+ }
2974
+ async function loadMonthlyUsageData(options) {
2975
+ const dailyData = await loadDailyUsageData(options);
2976
+ const groupedByMonth = Object.groupBy(dailyData, (data) => data.date.substring(0, 7));
2977
+ const monthlyArray = Object.entries(groupedByMonth).map(([month, dailyEntries]) => {
2978
+ if (dailyEntries == null) return void 0;
2979
+ return dailyEntries.reduce((acc, data) => ({
2980
+ month,
2981
+ inputTokens: acc.inputTokens + data.inputTokens,
2982
+ outputTokens: acc.outputTokens + data.outputTokens,
2983
+ cacheCreationTokens: acc.cacheCreationTokens + data.cacheCreationTokens,
2984
+ cacheReadTokens: acc.cacheReadTokens + data.cacheReadTokens,
2985
+ totalCost: acc.totalCost + data.totalCost
2986
+ }), {
2987
+ month,
2988
+ inputTokens: 0,
2989
+ outputTokens: 0,
2990
+ cacheCreationTokens: 0,
2991
+ cacheReadTokens: 0,
2992
+ totalCost: 0
2993
+ });
2994
+ }).filter((item) => item != null);
2995
+ const sortOrder = options?.order ?? "desc";
2996
+ const sortedMonthly = sort(monthlyArray);
2997
+ return sortOrder === "desc" ? sortedMonthly.desc((item) => item.month) : sortedMonthly.asc((item) => item.month);
2941
2998
  }
2942
2999
 
2943
3000
  //#endregion
2944
- export { DailyUsageSchema, SessionUsageSchema, UsageDataSchema, __commonJS, __require, __toESM, calculateCostForEntry, formatDate, getDefaultClaudePath, glob, loadSessionData, loadUsageData };
3001
+ export { DailyUsageSchema, MonthlyUsageSchema, SessionUsageSchema, UsageDataSchema, __commonJS, __require, __toESM, calculateCostForEntry, formatDate, getDefaultClaudePath, glob, loadDailyUsageData, loadMonthlyUsageData, loadSessionData };