ccgauge 0.3.1 → 1.0.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.
Files changed (92) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-build-manifest.json +41 -41
  3. package/.next/standalone/.next/app-path-routes-manifest.json +6 -6
  4. package/.next/standalone/.next/build-manifest.json +2 -2
  5. package/.next/standalone/.next/prerender-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  7. package/.next/standalone/.next/server/app/api/blocks/route.js +1 -1
  8. package/.next/standalone/.next/server/app/api/blocks/route_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/api/export/usage/route.js +1 -1
  10. package/.next/standalone/.next/server/app/api/export/usage/route.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/api/export/usage/route_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/api/pricing/route.js +1 -1
  13. package/.next/standalone/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/api/projects/route.js +1 -1
  15. package/.next/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/api/scan/route.js +1 -1
  17. package/.next/standalone/.next/server/app/api/scan/route_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/api/sessions/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/api/usage/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/usage/route_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/models/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/page.js +2 -2
  24. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  26. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  27. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  28. package/.next/standalone/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/.next/server/app/sessions/page.js +1 -1
  30. package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  31. package/.next/standalone/.next/server/app/settings/page.js +2 -2
  32. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  33. package/.next/standalone/.next/server/app/usage/page.js +2 -2
  34. package/.next/standalone/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app-paths-manifest.json +6 -6
  36. package/.next/standalone/.next/server/chunks/426.js +9 -4
  37. package/.next/standalone/.next/server/chunks/520.js +1 -1
  38. package/.next/standalone/.next/server/chunks/716.js +1 -1
  39. package/.next/standalone/.next/server/chunks/775.js +1 -1
  40. package/.next/standalone/.next/server/functions-config-manifest.json +4 -4
  41. package/.next/standalone/.next/server/pages/500.html +1 -1
  42. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  43. package/.next/standalone/.next/static/chunks/148-557ee562aff993b1.js +1 -0
  44. package/.next/standalone/.next/static/chunks/app/{error-89ee9e078058915d.js → error-3e48784f89c5ae8d.js} +1 -1
  45. package/.next/standalone/.next/static/chunks/app/layout-6c973d790f015707.js +1 -0
  46. package/.next/standalone/.next/static/chunks/app/models/page-dff43b9050382020.js +1 -0
  47. package/.next/standalone/.next/static/chunks/app/page-6d87d7a8aa752100.js +1 -0
  48. package/.next/standalone/.next/static/chunks/app/projects/[id]/page-3f812f0e20137f2b.js +1 -0
  49. package/.next/standalone/.next/static/chunks/app/sessions/[id]/page-3f812f0e20137f2b.js +1 -0
  50. package/.next/standalone/.next/static/chunks/app/settings/{page-334168b522eac1b1.js → page-d1af886a5c22af9b.js} +1 -1
  51. package/.next/standalone/.next/static/chunks/app/usage/page-26297e0641d51da8.js +1 -0
  52. package/.next/standalone/.next/static/css/b07523b7c353538d.css +3 -0
  53. package/.next/standalone/node_modules/next/node_modules/postcss/package.json +0 -0
  54. package/.next/standalone/package.json +20 -4
  55. package/CHANGELOG.md +208 -0
  56. package/README.md +235 -2
  57. package/README.zh-CN.md +229 -2
  58. package/bin/cli.mjs +123 -3
  59. package/dist/mcp/server.mjs +23622 -0
  60. package/dist/report/index.mjs +2098 -0
  61. package/package.json +29 -15
  62. package/.next/standalone/.next/static/chunks/454-d0e7d0fa6f643c41.js +0 -1
  63. package/.next/standalone/.next/static/chunks/app/layout-a6e30ba3a7f39737.js +0 -1
  64. package/.next/standalone/.next/static/chunks/app/models/page-e0e1b5979547421a.js +0 -1
  65. package/.next/standalone/.next/static/chunks/app/page-9347dfa20dabb24b.js +0 -1
  66. package/.next/standalone/.next/static/chunks/app/projects/[id]/page-5804875e3dc384df.js +0 -1
  67. package/.next/standalone/.next/static/chunks/app/sessions/[id]/page-5804875e3dc384df.js +0 -1
  68. package/.next/standalone/.next/static/chunks/app/usage/page-7789fec27778df9a.js +0 -1
  69. package/.next/standalone/.next/static/css/2e5f36bcdf442844.css +0 -3
  70. package/.next/standalone/node_modules/semver/classes/comparator.js +0 -143
  71. package/.next/standalone/node_modules/semver/classes/range.js +0 -557
  72. package/.next/standalone/node_modules/semver/classes/semver.js +0 -333
  73. package/.next/standalone/node_modules/semver/functions/cmp.js +0 -54
  74. package/.next/standalone/node_modules/semver/functions/coerce.js +0 -62
  75. package/.next/standalone/node_modules/semver/functions/compare.js +0 -7
  76. package/.next/standalone/node_modules/semver/functions/eq.js +0 -5
  77. package/.next/standalone/node_modules/semver/functions/gt.js +0 -5
  78. package/.next/standalone/node_modules/semver/functions/gte.js +0 -5
  79. package/.next/standalone/node_modules/semver/functions/lt.js +0 -5
  80. package/.next/standalone/node_modules/semver/functions/lte.js +0 -5
  81. package/.next/standalone/node_modules/semver/functions/neq.js +0 -5
  82. package/.next/standalone/node_modules/semver/functions/parse.js +0 -18
  83. package/.next/standalone/node_modules/semver/functions/satisfies.js +0 -12
  84. package/.next/standalone/node_modules/semver/internal/constants.js +0 -37
  85. package/.next/standalone/node_modules/semver/internal/debug.js +0 -11
  86. package/.next/standalone/node_modules/semver/internal/identifiers.js +0 -29
  87. package/.next/standalone/node_modules/semver/internal/lrucache.js +0 -42
  88. package/.next/standalone/node_modules/semver/internal/parse-options.js +0 -17
  89. package/.next/standalone/node_modules/semver/internal/re.js +0 -223
  90. package/.next/standalone/node_modules/semver/package.json +0 -78
  91. /package/.next/standalone/.next/static/{kmNFwlVOydWtqBX3zI8OH → ZPycmg0NLiIflO5NXMT75}/_buildManifest.js +0 -0
  92. /package/.next/standalone/.next/static/{kmNFwlVOydWtqBX3zI8OH → ZPycmg0NLiIflO5NXMT75}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -128,7 +128,46 @@ Background mode persists state under `~/.ccgauge/`:
128
128
  | `ccgauge restart [options]` | Stop and re-start with new options. |
129
129
  | `ccgauge status [--json]` | Inspect the background service. |
130
130
  | `ccgauge open` | Open the running dashboard in your browser. |
131
- | `ccgauge logs [-f] [-n <lines>]` | Print background logs. |
131
+ | `ccgauge logs [-f] [-n <lines>]` | Print background-service log file (the server's stdout). |
132
+ | `ccgauge report [options]` | Print a formatted **usage report** to stdout (one-shot, no server). |
133
+ | `ccgauge mcp` | Start the MCP server on stdio so LLMs can query usage. |
134
+
135
+ ### Report
136
+
137
+ A no-server one-shot summary that reads the same JSONL files the dashboard does
138
+ and prints a colored, aligned report:
139
+
140
+ ```bash
141
+ ccgauge report # last 7d, all sources, top 10 models
142
+ ccgauge report -r 30d -b project # 30 days, broken down by project
143
+ ccgauge report -s codex -m gpt-5.5 # only codex, only gpt-5.5*
144
+ ccgauge report --json # JSON output for scripting
145
+ ccgauge report --since 2026-05-01 --until 2026-05-08
146
+ ```
147
+
148
+ Report options:
149
+
150
+ | Option | Default | Purpose |
151
+ | --- | --- | --- |
152
+ | `-r, --range <range>` | `7d` | `today` / `1d` / `7d` / `30d` / `90d` / `all` |
153
+ | `-s, --source <provider>` | `all` | `claude` / `codex` / `all` |
154
+ | `-b, --by <dim>` | `model` | Breakdown dimension: `model` / `project` / `session` |
155
+ | `-g, --gran <granularity>` | `day` | Trend bucket: `hour` / `day` / `week` / `month` |
156
+ | `-n, --limit <n>` | `10` | Rows in the breakdown table |
157
+ | `--since <date>` | — | Override range start (ISO date or `YYYY-MM-DD`) |
158
+ | `--until <date>` | — | Override range end |
159
+ | `-m, --model <pat>` | — | Filter records whose model contains `<pat>` |
160
+ | `--project <pat>` | — | Filter by project basename / cwd substring |
161
+ | `-j, --json` | off | Machine-readable JSON instead of formatted text |
162
+ | `--no-color` | — | Disable ANSI colors (auto-disabled when piped) |
163
+ | `--no-trend` | — | Skip the trend chart |
164
+ | `--no-breakdown` | — | Skip the breakdown table |
165
+
166
+ Date-only `--since/--until` values use local calendar-day boundaries, so
167
+ `--until 2026-05-08` includes all of May 8.
168
+
169
+ > The name `report` (not `logs`) avoids clashing with `ccgauge logs`, which tails
170
+ > the background server's stdout log file.
132
171
 
133
172
  ### Startup options
134
173
 
@@ -143,6 +182,200 @@ Background mode persists state under `~/.ccgauge/`:
143
182
  | `--strict-port` | start, restart, root | Fail if the preferred port is busy. |
144
183
  | `--log <path>` | start --background, restart | Background log file path. |
145
184
 
185
+ ## MCP server (let an LLM query your usage)
186
+
187
+ ccgauge ships an [Model Context Protocol](https://modelcontextprotocol.io/)
188
+ server so any MCP-aware client (Claude Desktop, Cursor, Cline, Codex CLI,
189
+ your own agent…) can talk to your local Claude Code + Codex CLI history
190
+ through structured tools — no copy-paste, no screenshots of the dashboard.
191
+
192
+ ### What you can ask
193
+
194
+ Once configured, you can ask things like:
195
+
196
+ - *"How much did I spend on AI coding this week? Break it down by Claude vs Codex."*
197
+ - *"What did I work on yesterday?"*
198
+ - *"Show me my 10 most expensive sessions this month."*
199
+ - *"Which project ate the most tokens in the last 30 days?"*
200
+ - *"Was prompt caching saving me money? How much?"*
201
+ - *"Estimate the cost of 100K input + 20K output on Opus 4.7."*
202
+ - *"How big was my Codex reasoning overhead last week?"*
203
+ - *"Give me a weekly stand-up bullet list of what I shipped."*
204
+
205
+ The LLM picks the right tool, calls it locally, and answers in plain
206
+ English with real numbers from your machine.
207
+
208
+ ### Capabilities at a glance
209
+
210
+ | Tool | What it answers |
211
+ | --- | --- |
212
+ | `usage_summary` | Total tokens / cost / cache savings for a date range. Always returns combined totals + per-source breakdown. |
213
+ | `usage_by_time` | Time-series buckets (hour / day / week / month) for trend questions. |
214
+ | `usage_by_model` | Per-model cost share. Each entry tagged with its `source`. |
215
+ | `usage_by_project` | Per-project (cwd) cost share + session counts + last-activity. |
216
+ | `usage_by_session` | Session list with title (= first user message), models, duration, cost. Sort by recent / cost / tokens / duration. |
217
+ | `daily_summary` | "What did I do today / yesterday / Monday / on YYYY-MM-DD?" Sessions grouped by project + models + top tool calls. |
218
+ | `weekly_summary` | 7-day roll-up: per-day cost trend, top sessions, top projects, models. `week_offset=-1` for last week. |
219
+ | `recent_activity` | The N most recently active sessions across providers. |
220
+
221
+ | Resource URI | Content |
222
+ | --- | --- |
223
+ | `ccgauge://providers` | Detected providers, data dirs, file/record counts, indexer status. |
224
+
225
+ **Common arguments** (every analytical tool accepts these):
226
+
227
+ - `source`: `"claude"` | `"codex"` | `"all"` (default `"all"`). When `"all"`, the response carries combined totals **and** a `bySource: { claude, codex }` breakdown so the LLM can answer either combined or provider-specific questions in a single call.
228
+ - Date range: pass `range` (one of `today`, `yesterday`, `this_week`, `last_week`, `this_month`, `last_month`, `7d`, `30d`, `90d`, `all`) **or** explicit `from` / `to` (ISO date or full timestamp).
229
+
230
+ ### Configure your MCP client
231
+
232
+ The exact config-file location depends on your client; the snippet shape
233
+ is the same.
234
+
235
+ #### Claude Desktop
236
+
237
+ `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) /
238
+ `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
239
+
240
+ ```json
241
+ {
242
+ "mcpServers": {
243
+ "ccgauge": {
244
+ "command": "npx",
245
+ "args": ["-y", "ccgauge", "mcp"]
246
+ }
247
+ }
248
+ }
249
+ ```
250
+
251
+ If you've installed ccgauge globally (`npm i -g ccgauge`), drop the `npx`:
252
+
253
+ ```json
254
+ {
255
+ "mcpServers": {
256
+ "ccgauge": {
257
+ "command": "ccgauge",
258
+ "args": ["mcp"]
259
+ }
260
+ }
261
+ }
262
+ ```
263
+
264
+ Restart Claude Desktop. The 8 ccgauge tools appear in the tool picker.
265
+
266
+ #### Cursor
267
+
268
+ `~/.cursor/mcp.json` (project-level: `<project>/.cursor/mcp.json`):
269
+
270
+ ```json
271
+ {
272
+ "mcpServers": {
273
+ "ccgauge": {
274
+ "command": "ccgauge",
275
+ "args": ["mcp"]
276
+ }
277
+ }
278
+ }
279
+ ```
280
+
281
+ #### Cline / Continue / generic MCP clients
282
+
283
+ Anything that follows the standard `{ command, args, env? }` shape works.
284
+ Use either `npx -y ccgauge mcp` (no global install) or `ccgauge mcp`
285
+ (with global install). To override scan paths, pass them via `env`:
286
+
287
+ ```json
288
+ {
289
+ "mcpServers": {
290
+ "ccgauge": {
291
+ "command": "ccgauge",
292
+ "args": ["mcp"],
293
+ "env": {
294
+ "CCGAUGE_CODEX_DIR": "/custom/codex/path",
295
+ "CLAUDE_CONFIG_DIR": "/custom/claude/path",
296
+ "CCGAUGE_STATE_DIR": "/custom/cache/path"
297
+ }
298
+ }
299
+ }
300
+ }
301
+ ```
302
+
303
+ #### Verify it's working
304
+
305
+ In Claude Desktop, open a new chat and ask:
306
+
307
+ > *"What ccgauge tools do you have? Run usage_summary for the last 7 days."*
308
+
309
+ You should see Claude pick `usage_summary`, return a JSON payload with
310
+ `totals` + `bySource`, then summarise it in prose with real numbers.
311
+
312
+ ### Prompt cookbook
313
+
314
+ Drop these into Claude Desktop / Cursor / Cline as-is. The italics next
315
+ to each one are the tool(s) the LLM will pick — useful if you want to
316
+ debug "why did it answer X".
317
+
318
+ #### Cost & usage
319
+
320
+ - *"How much did I spend on AI coding this week, broken down by Claude and Codex?"*
321
+ → `usage_summary({ range: "7d" })`
322
+ - *"What's my AI coding cost this month? How does that compare to last month?"*
323
+ → `usage_summary({ range: "this_month" })` + `usage_summary({ range: "last_month" })`
324
+ - *"Show me a daily cost trend for the last 30 days."*
325
+ → `usage_by_time({ range: "30d", granularity: "day" })`
326
+ - *"Which Claude model did I use the most this month, and how much did it cost?"*
327
+ → `usage_by_model({ range: "this_month", source: "claude" })`
328
+ - *"Top 5 most expensive sessions this month?"*
329
+ → `usage_by_session({ range: "this_month", sort: "cost", limit: 5 })`
330
+
331
+ #### Work content / standup
332
+
333
+ - *"What did I work on yesterday? Group by project."*
334
+ → `daily_summary({ date: "yesterday" })`
335
+ - *"Generate a Monday stand-up bullet list of what I shipped last week."*
336
+ → `weekly_summary({ week_offset: -1 })`
337
+ - *"Which 3 projects have I touched most in the last 2 weeks?"*
338
+ → `usage_by_project({ range: "14d", limit: 3 })` (LLM may also pull `weekly_summary`)
339
+ - *"What was my last coding session about?"*
340
+ → `recent_activity({ limit: 1 })`
341
+
342
+ #### Caching / efficiency
343
+
344
+ - *"How many tokens did Anthropic prompt caching save me this month?"*
345
+ → `usage_summary({ range: "this_month", source: "claude" })` — the response includes `saved_usd`.
346
+ - *"What percentage of my Codex output is reasoning tokens this week?"*
347
+ → `usage_summary({ range: "7d", source: "codex" })` — response carries `reasoning_tokens` next to `output_tokens`.
348
+
349
+ #### Budget / planning
350
+
351
+ - *"At my current burn rate, how much will I spend this month?"*
352
+ → `usage_summary({ range: "this_month" })` + `usage_by_time({ range: "this_month", granularity: "day" })` — LLM extrapolates.
353
+ - *"If I run another 200K input + 50K output on Opus 4.7 today, what does that add to my month-to-date cost?"*
354
+ → `usage_summary({ range: "this_month" })` + LLM does the arithmetic from the published per-1M-token rates.
355
+
356
+ #### Cross-source comparisons
357
+
358
+ - *"Am I getting more value out of Claude or Codex this month, by tokens-per-dollar?"*
359
+ → `usage_summary({ range: "this_month" })` — both totals are in `bySource`.
360
+ - *"For each provider, which project ate the most tokens last week?"*
361
+ → `usage_by_project({ range: "last_week" })` (each entry already carries `source`).
362
+
363
+ ### Privacy posture
364
+
365
+ - **stdio only** in v1 — no network ports, no remote access
366
+ - Reads only the JSONL files you already have on disk; no upstream API calls
367
+ - Absolute paths in error messages are scrubbed (`$HOME` → `~`)
368
+ - The MCP server uses a separate persisted cache (`~/.ccgauge/cache/index-mcp-v2.json`) so it never fights the dashboard for the same on-disk state file
369
+
370
+ ### Troubleshooting
371
+
372
+ | Symptom | Try |
373
+ | --- | --- |
374
+ | Client doesn't see ccgauge tools | Restart the client after editing the config; check `npx -y ccgauge mcp` runs in your shell |
375
+ | First call is slow | First call after a cold start indexes all JSONL files (~1–3 s for 100 files); subsequent calls are O(1) |
376
+ | "no providers detected" in the resource | The MCP process can't see `~/.claude/projects` / `~/.codex/sessions`; pass `CLAUDE_CONFIG_DIR` / `CCGAUGE_CODEX_DIR` via `env` in the MCP config |
377
+ | Want to see what the server is logging | Watch the client's MCP log; ccgauge writes to **stderr** (stdout is reserved for JSON-RPC) |
378
+
146
379
  ## Configuration
147
380
 
148
381
  ccgauge auto-detects the standard locations:
@@ -203,7 +436,7 @@ This repo is a working Next.js project — run the dashboard against your live d
203
436
  git clone https://github.com/chengzuopeng/ccgauge.git
204
437
  cd ccgauge
205
438
  pnpm install
206
- pnpm dev # http://localhost:3737
439
+ pnpm dev # http://localhost:3738
207
440
  ```
208
441
 
209
442
  Scripts:
package/README.zh-CN.md CHANGED
@@ -128,7 +128,44 @@ ccgauge stop
128
128
  | `ccgauge restart [options]` | 停止再用新参数启动。 |
129
129
  | `ccgauge status [--json]` | 查看后台状态。 |
130
130
  | `ccgauge open` | 在浏览器打开正在运行的看板。 |
131
- | `ccgauge logs [-f] [-n <lines>]` | 查看后台日志。 |
131
+ | `ccgauge logs [-f] [-n <lines>]` | 查看后台服务的日志(server stdout)。 |
132
+ | `ccgauge report [options]` | 命令行**用量报告**,直接打到终端(一次性,不起服务)。 |
133
+ | `ccgauge mcp` | 起 MCP 服务(stdio),让 LLM 查你的用量。 |
134
+
135
+ ### 命令行报告(report)
136
+
137
+ 不需要起 server,直接读 JSONL,在终端打印漂亮的彩色对齐报告:
138
+
139
+ ```bash
140
+ ccgauge report # 默认:近 7 天 / 所有数据源 / 前 10 个模型
141
+ ccgauge report -r 30d -b project # 30 天,按项目分组
142
+ ccgauge report -s codex -m gpt-5.5 # 只看 codex 的 gpt-5.5*
143
+ ccgauge report --json # 输出 JSON 给脚本用
144
+ ccgauge report --since 2026-05-01 --until 2026-05-08
145
+ ```
146
+
147
+ report 参数:
148
+
149
+ | 参数 | 默认 | 作用 |
150
+ | --- | --- | --- |
151
+ | `-r, --range <range>` | `7d` | `today` / `1d` / `7d` / `30d` / `90d` / `all` |
152
+ | `-s, --source <provider>` | `all` | `claude` / `codex` / `all` |
153
+ | `-b, --by <dim>` | `model` | 分组维度:`model` / `project` / `session` |
154
+ | `-g, --gran <granularity>` | `day` | 趋势粒度:`hour` / `day` / `week` / `month` |
155
+ | `-n, --limit <n>` | `10` | 分组表显示行数 |
156
+ | `--since <date>` | — | 自定义起始日期(覆盖 `--range`,支持 `YYYY-MM-DD`) |
157
+ | `--until <date>` | — | 自定义截止日期 |
158
+ | `-m, --model <pat>` | — | 按模型名子串过滤 |
159
+ | `--project <pat>` | — | 按项目名 / cwd 子串过滤 |
160
+ | `-j, --json` | off | 输出 JSON 而不是格式化文本 |
161
+ | `--no-color` | — | 关掉 ANSI 颜色(管道里会自动关) |
162
+ | `--no-trend` | — | 不画趋势条 |
163
+ | `--no-breakdown` | — | 不打分组表 |
164
+
165
+ 只写日期的 `--since/--until` 会按本地自然日边界处理,所以
166
+ `--until 2026-05-08` 会包含 5 月 8 日整天。
167
+
168
+ > 用 `report` 而不是 `logs` 是为了避免和 `ccgauge logs`(tail 后台 server 的 stdout)混淆。
132
169
 
133
170
  ### 启动参数
134
171
 
@@ -143,6 +180,196 @@ ccgauge stop
143
180
  | `--strict-port` | start, restart, 根命令 | 端口不可用时直接失败。 |
144
181
  | `--log <path>` | start --background, restart | 后台日志文件。 |
145
182
 
183
+ ## MCP 服务(让大模型直接查你的用量)
184
+
185
+ ccgauge 内置了一个 [Model Context Protocol](https://modelcontextprotocol.io/) 服务,
186
+ 任何 MCP 客户端(Claude Desktop / Cursor / Cline / Codex CLI / 自建 agent)都能
187
+ 通过结构化 tool 调用,直接问大模型关于你本机 Claude Code + Codex CLI 历史的问题——
188
+ 不用复制粘贴、不用截图看板。
189
+
190
+ ### 你能问什么
191
+
192
+ 配好之后,可以这样问:
193
+
194
+ - *"我这周在 AI 编程上花了多少?分别看下 Claude 和 Codex。"*
195
+ - *"我昨天都在做什么?"*
196
+ - *"列一下本月最贵的 10 个会话。"*
197
+ - *"过去 30 天哪个项目最吃 token?"*
198
+ - *"prompt caching 帮我省了多少钱?"*
199
+ - *"如果我在 Opus 4.7 上再跑 100K input + 20K output,要多少钱?"*
200
+ - *"上周 Codex 的 reasoning 开销有多大?"*
201
+ - *"给我一份本周完成事项的 standup bullet list。"*
202
+
203
+ LLM 会自动选合适的 tool、本地调用、用大白话给你带真实数字的答案。
204
+
205
+ ### 工具一览
206
+
207
+ | Tool | 回答什么 |
208
+ | --- | --- |
209
+ | `usage_summary` | 一段时间内总 tokens / 花费 / 缓存节省。永远同时返回合并总数 + 按 source 拆分。 |
210
+ | `usage_by_time` | 时间序列(小时/天/周/月),用于趋势 / "什么时候开销爆了"。 |
211
+ | `usage_by_model` | 按模型的成本占比,每条带 source。 |
212
+ | `usage_by_project` | 按项目(cwd)的成本占比 + 会话数 + 最近活跃时间。 |
213
+ | `usage_by_session` | 会话列表,含标题(首条用户消息)/ 模型 / 时长 / 花费。可按 recent / cost / tokens / duration 排序。 |
214
+ | `daily_summary` | "今天 / 昨天 / 周一 / YYYY-MM-DD 我都干了啥?" 按项目分组的会话 + 模型 + top 工具调用。 |
215
+ | `weekly_summary` | 7 天 roll-up:每日花费趋势 + top 会话 + top 项目 + 模型分布。`week_offset=-1` 看上周。 |
216
+ | `recent_activity` | 最近 N 条活跃会话(不限日期)。 |
217
+
218
+ | Resource URI | 内容 |
219
+ | --- | --- |
220
+ | `ccgauge://providers` | 检测到的 provider、数据目录、文件 / 记录数、indexer 状态。 |
221
+
222
+ **公共参数**(每个分析类工具都接):
223
+
224
+ - `source`:`"claude"` | `"codex"` | `"all"`(默认 `"all"`)。当 `"all"` 时,响应同时带合并总数 **和** `bySource: { claude, codex }` 拆分,让 LLM 一次调用就能回答 "总共多少" 和 "分别多少" 两类问题。
225
+ - 时间范围:传 `range`(`today` / `yesterday` / `this_week` / `last_week` / `this_month` / `last_month` / `7d` / `30d` / `90d` / `all`),**或**显式 `from` / `to`(ISO 日期或完整时间戳)。
226
+
227
+ ### 在 MCP 客户端里配置
228
+
229
+ 不同客户端的配置文件位置不同,但 snippet 形状一样。
230
+
231
+ #### Claude Desktop
232
+
233
+ `~/Library/Application Support/Claude/claude_desktop_config.json`(macOS)/
234
+ `%APPDATA%\Claude\claude_desktop_config.json`(Windows):
235
+
236
+ ```json
237
+ {
238
+ "mcpServers": {
239
+ "ccgauge": {
240
+ "command": "npx",
241
+ "args": ["-y", "ccgauge", "mcp"]
242
+ }
243
+ }
244
+ }
245
+ ```
246
+
247
+ 如果已经全局装了 ccgauge(`npm i -g ccgauge`),可以省掉 `npx`:
248
+
249
+ ```json
250
+ {
251
+ "mcpServers": {
252
+ "ccgauge": {
253
+ "command": "ccgauge",
254
+ "args": ["mcp"]
255
+ }
256
+ }
257
+ }
258
+ ```
259
+
260
+ 重启 Claude Desktop,工具选择器里就能看到 ccgauge 的 8 个工具。
261
+
262
+ #### Cursor
263
+
264
+ `~/.cursor/mcp.json`(项目级:`<project>/.cursor/mcp.json`):
265
+
266
+ ```json
267
+ {
268
+ "mcpServers": {
269
+ "ccgauge": {
270
+ "command": "ccgauge",
271
+ "args": ["mcp"]
272
+ }
273
+ }
274
+ }
275
+ ```
276
+
277
+ #### Cline / Continue / 通用 MCP 客户端
278
+
279
+ 任何遵循标准 `{ command, args, env? }` 格式的客户端都能用。`npx -y ccgauge mcp`
280
+ (无需全局装)或 `ccgauge mcp`(已全局装)任选其一。要覆盖扫描路径,通过 `env` 传:
281
+
282
+ ```json
283
+ {
284
+ "mcpServers": {
285
+ "ccgauge": {
286
+ "command": "ccgauge",
287
+ "args": ["mcp"],
288
+ "env": {
289
+ "CCGAUGE_CODEX_DIR": "/custom/codex/path",
290
+ "CLAUDE_CONFIG_DIR": "/custom/claude/path",
291
+ "CCGAUGE_STATE_DIR": "/custom/cache/path"
292
+ }
293
+ }
294
+ }
295
+ }
296
+ ```
297
+
298
+ #### 验证是否生效
299
+
300
+ 在 Claude Desktop 新开一个对话,问:
301
+
302
+ > *"你有哪些 ccgauge 工具?跑一下 usage_summary 看最近 7 天数据。"*
303
+
304
+ 如果配置成功,Claude 会调 `usage_summary`,返回带 `totals` + `bySource` 的 JSON,
305
+ 然后用大白话总结成带真实数字的回答。
306
+
307
+ ### Prompt 示例集
308
+
309
+ 直接复制丢进 Claude Desktop / Cursor / Cline 即可。每个 prompt 后面斜体注的是
310
+ LLM 大概率会调的工具——方便你"为什么会这样回答"反查。
311
+
312
+ #### 用量与花费
313
+
314
+ - *"我这周用 AI 编程花了多少钱?分开看 Claude 和 Codex。"*
315
+ → `usage_summary({ range: "7d" })`
316
+ - *"本月 AI 编程花了多少?跟上个月比怎么样?"*
317
+ → `usage_summary({ range: "this_month" })` + `usage_summary({ range: "last_month" })`
318
+ - *"画一下最近 30 天的每日花费趋势。"*
319
+ → `usage_by_time({ range: "30d", granularity: "day" })`
320
+ - *"本月用的最多的 Claude 模型是哪个?花了多少?"*
321
+ → `usage_by_model({ range: "this_month", source: "claude" })`
322
+ - *"本月最贵的 5 个会话是哪些?"*
323
+ → `usage_by_session({ range: "this_month", sort: "cost", limit: 5 })`
324
+
325
+ #### 工作内容回顾 / standup
326
+
327
+ - *"我昨天都做了什么?按项目分一下。"*
328
+ → `daily_summary({ date: "yesterday" })`
329
+ - *"给我一份周一 standup 用的 bullet list,列我上周完成的事。"*
330
+ → `weekly_summary({ week_offset: -1 })`
331
+ - *"过去两周我接触最多的 3 个项目是什么?"*
332
+ → `usage_by_project({ range: "14d", limit: 3 })`(LLM 也可能补一次 `weekly_summary`)
333
+ - *"我最近一次的编码会话是关于什么的?"*
334
+ → `recent_activity({ limit: 1 })`
335
+
336
+ #### 缓存 / 效率
337
+
338
+ - *"本月 Anthropic prompt caching 帮我省了多少 tokens?"*
339
+ → `usage_summary({ range: "this_month", source: "claude" })`——返回里有 `saved_usd`。
340
+ - *"本周 Codex 的 output 里有多少比例是 reasoning tokens?"*
341
+ → `usage_summary({ range: "7d", source: "codex" })`——返回里 `reasoning_tokens` 紧挨着 `output_tokens`。
342
+
343
+ #### 预算 / 规划
344
+
345
+ - *"按当前消耗速度,本月预计花多少?"*
346
+ → `usage_summary({ range: "this_month" })` + `usage_by_time({ range: "this_month", granularity: "day" })`——LLM 自己外推。
347
+ - *"如果我今天再在 Opus 4.7 上跑 200K input + 50K output,本月累计要多少?"*
348
+ → `usage_summary({ range: "this_month" })` + LLM 按公开单价做算术。
349
+
350
+ #### 跨数据源对比
351
+
352
+ - *"本月 Claude 和 Codex 哪个性价比更高(按每美元 tokens)?"*
353
+ → `usage_summary({ range: "this_month" })`——两边数字都在 `bySource` 里。
354
+ - *"上周每个 provider 的最吃 token 项目分别是哪个?"*
355
+ → `usage_by_project({ range: "last_week" })`(每条 entry 自带 `source`)。
356
+
357
+ ### 隐私边界
358
+
359
+ - v1 **仅 stdio**——不开网络端口,不能远程访问
360
+ - 只读本机已有的 JSONL 文件,零上游 API 调用
361
+ - 错误信息里的绝对路径会脱敏(`$HOME` → `~`)
362
+ - MCP 用独立的持久化缓存文件(`~/.ccgauge/cache/index-mcp-v2.json`),永远不会和看板抢同一份磁盘状态
363
+
364
+ ### 排障
365
+
366
+ | 现象 | 建议 |
367
+ | --- | --- |
368
+ | 客户端看不到 ccgauge 工具 | 改完配置重启客户端;终端里手动跑 `npx -y ccgauge mcp` 看是否能起 |
369
+ | 第一次调用比较慢 | 冷启动后第一次会全量索引(100 文件 ~1–3s);之后都是 O(1) |
370
+ | Resource 显示 "no providers detected" | MCP 进程看不到 `~/.claude/projects` / `~/.codex/sessions`;通过 MCP 配置的 `env` 传 `CLAUDE_CONFIG_DIR` / `CCGAUGE_CODEX_DIR` |
371
+ | 想看 server 在打什么日志 | 看客户端的 MCP 日志;ccgauge 把日志写到 **stderr**(stdout 被 JSON-RPC 占用)|
372
+
146
373
  ## 配置
147
374
 
148
375
  ccgauge 会自动识别标准路径:
@@ -202,7 +429,7 @@ lib/providers/<name>/
202
429
  git clone https://github.com/chengzuopeng/ccgauge.git
203
430
  cd ccgauge
204
431
  pnpm install
205
- pnpm dev # http://localhost:3737
432
+ pnpm dev # http://localhost:3738
206
433
  ```
207
434
 
208
435
  常用脚本:
package/bin/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import { closeSync, createReadStream, existsSync, openSync } from 'node:fs';
4
4
  import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
5
5
  import os from 'node:os';
6
6
  import { dirname, join, resolve } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import { createRequire } from 'node:module';
9
9
 
10
10
  const require = createRequire(import.meta.url);
@@ -25,8 +25,15 @@ const DEFAULT_LOG_FILE = join(STATE_DIR, 'ccgauge.log');
25
25
  const STATE_VERSION = 1;
26
26
  const DEFAULT_PORT = '3737';
27
27
  const DEFAULT_HOST = '127.0.0.1';
28
- const COMMAND_NAMES = new Set(['start', 'stop', 'restart', 'status', 'open', 'logs']);
29
- const VALUE_OPTIONS = new Set(['-p', '--port', '-H', '--host', '--dir', '--log', '-n', '--lines']);
28
+ const COMMAND_NAMES = new Set([
29
+ 'start', 'stop', 'restart', 'status', 'open', 'logs', 'mcp',
30
+ 'report',
31
+ ]);
32
+ const VALUE_OPTIONS = new Set([
33
+ '-p', '--port', '-H', '--host', '--dir', '--log', '-n', '--lines',
34
+ '-r', '--range', '-s', '--source', '-b', '--by', '-g', '--gran',
35
+ '-m', '--model', '--project', '--since', '--until',
36
+ ]);
30
37
 
31
38
  function browserHost(host) {
32
39
  if (!host || host === '0.0.0.0' || host === '::' || host === '[::]') return '127.0.0.1';
@@ -132,6 +139,35 @@ program
132
139
  await logs(opts);
133
140
  });
134
141
 
142
+ program
143
+ .command('mcp')
144
+ .description('start the MCP server (stdio) so LLMs can query usage data')
145
+ .action(async () => {
146
+ await startMcp();
147
+ });
148
+
149
+ function addReportOptions(cmd) {
150
+ return cmd
151
+ .option('-r, --range <range>', 'today | 1d | 7d | 30d | 90d | all', '7d')
152
+ .option('-s, --source <provider>', 'claude | codex | all', 'all')
153
+ .option('-b, --by <dim>', 'breakdown dimension: model | project | session', 'model')
154
+ .option('-g, --gran <granularity>', 'trend granularity: hour | day | week | month', 'day')
155
+ .option('-n, --limit <n>', 'rows in breakdown table', '10')
156
+ .option('--since <date>', 'override range start (ISO date or YYYY-MM-DD)')
157
+ .option('--until <date>', 'override range end (ISO date or YYYY-MM-DD)')
158
+ .option('-m, --model <pat>', 'filter by model substring')
159
+ .option('--project <pat>', 'filter by project (cwd basename match)')
160
+ .option('-j, --json', 'output JSON instead of formatted text')
161
+ .option('--no-color', 'disable ANSI colors')
162
+ .option('--no-trend', 'skip the trend chart')
163
+ .option('--no-breakdown', 'skip the breakdown table');
164
+ }
165
+
166
+ addReportOptions(program.command('report').description('print a formatted usage report to stdout'))
167
+ .action(async (opts) => {
168
+ await report(opts);
169
+ });
170
+
135
171
  await program.parseAsync(normalizeArgv(process.argv));
136
172
 
137
173
  function normalizeArgv(argv) {
@@ -221,6 +257,9 @@ async function startBackground(standaloneEntry, opts) {
221
257
  env,
222
258
  detached: true,
223
259
  stdio: ['ignore', out, err],
260
+ // Suppress the fleeting console window that Windows pops up for a
261
+ // detached background child. No-op on macOS/Linux.
262
+ windowsHide: true,
224
263
  });
225
264
  child.unref();
226
265
  // Once spawn() has dup'd these fds into the child, the parent can release them.
@@ -344,6 +383,87 @@ or run the dev server with
344
383
  process.exit(1);
345
384
  }
346
385
 
386
+ async function report(opts) {
387
+ const bundle = join(packageRoot, 'dist', 'report', 'index.mjs');
388
+ if (!existsSync(bundle)) {
389
+ console.error(`
390
+ [ccgauge] Report bundle not found:
391
+ ${bundle}
392
+
393
+ If you installed ccgauge from npm: please reinstall — the published package
394
+ should include the report bundle.
395
+
396
+ If you are running from source: build it first with
397
+ $ pnpm build:report
398
+ or run the full build with
399
+ $ pnpm build
400
+ `);
401
+ process.exit(1);
402
+ }
403
+ const limit = parseInt(String(opts.limit ?? '10'), 10);
404
+ const reportOpts = {
405
+ range: String(opts.range ?? '7d'),
406
+ source: String(opts.source ?? 'all'),
407
+ by: String(opts.by ?? 'model'),
408
+ gran: String(opts.gran ?? 'day'),
409
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 10,
410
+ since: opts.since ? String(opts.since) : undefined,
411
+ until: opts.until ? String(opts.until) : undefined,
412
+ json: Boolean(opts.json),
413
+ color: opts.color !== false && process.stdout.isTTY,
414
+ showTrend: opts.trend !== false,
415
+ showBreakdown: opts.breakdown !== false,
416
+ model: opts.model ? String(opts.model) : undefined,
417
+ project: opts.project ? String(opts.project) : undefined,
418
+ };
419
+ try {
420
+ const mod = await import(pathToFileURL(bundle).href);
421
+ const out = await mod.runReport(reportOpts);
422
+ process.stdout.write(out);
423
+ if (!out.endsWith('\n')) process.stdout.write('\n');
424
+ } catch (err) {
425
+ console.error(`[ccgauge] report failed: ${(err && err.message) || err}`);
426
+ process.exit(1);
427
+ }
428
+ // The indexer keeps fs watchers alive, which would block process exit.
429
+ // For a one-shot report we explicitly exit once stdout is drained.
430
+ process.stdout.once?.('drain', () => process.exit(0));
431
+ if (process.stdout.writableLength === 0) process.exit(0);
432
+ }
433
+
434
+ async function startMcp() {
435
+ const bundle = join(packageRoot, 'dist', 'mcp', 'server.mjs');
436
+ if (!existsSync(bundle)) {
437
+ console.error(`
438
+ [ccgauge-mcp] Build artifact not found:
439
+ ${bundle}
440
+
441
+ If you installed ccgauge from npm: please reinstall — the published package should
442
+ include the MCP server bundle.
443
+
444
+ If you are running from source: build first with
445
+ $ pnpm build:mcp
446
+ or run the full build with
447
+ $ pnpm build
448
+ `);
449
+ process.exit(1);
450
+ }
451
+ // Hand control to the bundled MCP server. It owns the stdio JSON-RPC
452
+ // session for the lifetime of the parent (the LLM client) process.
453
+ const child = spawn(process.execPath, [bundle], {
454
+ stdio: 'inherit',
455
+ env: process.env,
456
+ });
457
+ const forward = (sig) => () => {
458
+ if (!child.killed) child.kill(sig);
459
+ };
460
+ process.on('SIGINT', forward('SIGINT'));
461
+ process.on('SIGTERM', forward('SIGTERM'));
462
+ child.on('exit', (code, sig) => {
463
+ process.exit(typeof code === 'number' ? code : sig ? 128 : 0);
464
+ });
465
+ }
466
+
347
467
  async function resolvePort(opts) {
348
468
  const preferred = parseInt(String(opts.port), 10);
349
469
  if (!Number.isInteger(preferred) || preferred <= 0 || preferred > 65535) {