ccgauge 1.0.2 → 1.0.4

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 (121) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-build-manifest.json +36 -36
  3. package/.next/standalone/.next/app-path-routes-manifest.json +9 -9
  4. package/.next/standalone/.next/build-manifest.json +2 -2
  5. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/api/blocks/route.js +1 -1
  7. package/.next/standalone/.next/server/app/api/blocks/route.js.nft.json +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_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/api/projects/route.js +1 -1
  14. package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +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.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/api/scan/route_client-reference-manifest.js +1 -1
  19. package/.next/standalone/.next/server/app/api/sessions/route.js +1 -1
  20. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/api/usage/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/usage/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/usage/route_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app/models/page.js +2 -2
  26. package/.next/standalone/.next/server/app/models/page.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/models/page_client-reference-manifest.js +1 -1
  28. package/.next/standalone/.next/server/app/page.js +2 -2
  29. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  31. package/.next/standalone/.next/server/app/projects/[id]/page.js +2 -2
  32. package/.next/standalone/.next/server/app/projects/[id]/page.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  35. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/sessions/[id]/page.js +2 -2
  38. package/.next/standalone/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/sessions/page.js +1 -1
  41. package/.next/standalone/.next/server/app/sessions/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  44. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/usage/page.js +3 -3
  47. package/.next/standalone/.next/server/app/usage/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app-paths-manifest.json +9 -9
  50. package/.next/standalone/.next/server/chunks/517.js +1 -1
  51. package/.next/standalone/.next/server/chunks/567.js +2 -2
  52. package/.next/standalone/.next/server/chunks/971.js +1 -1
  53. package/.next/standalone/.next/server/chunks/98.js +1 -0
  54. package/.next/standalone/.next/server/functions-config-manifest.json +2 -2
  55. package/.next/standalone/.next/server/pages/500.html +1 -1
  56. package/.next/standalone/.next/static/chunks/148-6c2eaf5508bfe739.js +1 -0
  57. package/.next/standalone/.next/static/chunks/930-ca5c6f8b5cb6ac3d.js +1 -0
  58. package/.next/standalone/.next/static/chunks/app/layout-4f3538437c5e8366.js +1 -0
  59. package/.next/standalone/.next/static/chunks/app/page-3cda7f70ecf5017a.js +1 -0
  60. package/.next/standalone/.next/static/chunks/app/settings/page-1ba7c4a4c0fae2f8.js +1 -0
  61. package/.next/standalone/.next/static/css/{406e067663b8b429.css → fbd2c395e5bf32cb.css} +1 -1
  62. package/.next/standalone/node_modules/next/node_modules/@img/sharp-darwin-arm64/LICENSE +191 -0
  63. package/.next/standalone/node_modules/next/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node +0 -0
  64. package/.next/standalone/node_modules/next/node_modules/@img/sharp-darwin-arm64/package.json +40 -0
  65. package/.next/standalone/node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64/lib/index.js +1 -0
  66. package/.next/standalone/node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib +0 -0
  67. package/.next/standalone/node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64/package.json +36 -0
  68. package/.next/standalone/node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64/versions.json +30 -0
  69. package/.next/standalone/node_modules/next/node_modules/postcss/package.json +0 -0
  70. package/.next/standalone/node_modules/next/node_modules/sharp/lib/channel.js +177 -0
  71. package/.next/standalone/node_modules/next/node_modules/sharp/lib/colour.js +195 -0
  72. package/.next/standalone/node_modules/next/node_modules/sharp/lib/composite.js +212 -0
  73. package/.next/standalone/node_modules/next/node_modules/sharp/lib/constructor.js +499 -0
  74. package/.next/standalone/node_modules/next/node_modules/sharp/lib/index.js +16 -0
  75. package/.next/standalone/node_modules/next/node_modules/sharp/lib/input.js +809 -0
  76. package/.next/standalone/node_modules/next/node_modules/sharp/lib/is.js +143 -0
  77. package/.next/standalone/node_modules/next/node_modules/sharp/lib/libvips.js +207 -0
  78. package/.next/standalone/node_modules/next/node_modules/sharp/lib/operation.js +1016 -0
  79. package/.next/standalone/node_modules/next/node_modules/sharp/lib/output.js +1666 -0
  80. package/.next/standalone/node_modules/next/node_modules/sharp/lib/resize.js +595 -0
  81. package/.next/standalone/node_modules/next/node_modules/sharp/lib/sharp.js +121 -0
  82. package/.next/standalone/node_modules/next/node_modules/sharp/lib/utility.js +291 -0
  83. package/.next/standalone/node_modules/next/node_modules/sharp/package.json +202 -0
  84. package/.next/standalone/node_modules/semver/classes/comparator.js +143 -0
  85. package/.next/standalone/node_modules/semver/classes/range.js +557 -0
  86. package/.next/standalone/node_modules/semver/classes/semver.js +333 -0
  87. package/.next/standalone/node_modules/semver/functions/cmp.js +54 -0
  88. package/.next/standalone/node_modules/semver/functions/coerce.js +62 -0
  89. package/.next/standalone/node_modules/semver/functions/compare.js +7 -0
  90. package/.next/standalone/node_modules/semver/functions/eq.js +5 -0
  91. package/.next/standalone/node_modules/semver/functions/gt.js +5 -0
  92. package/.next/standalone/node_modules/semver/functions/gte.js +5 -0
  93. package/.next/standalone/node_modules/semver/functions/lt.js +5 -0
  94. package/.next/standalone/node_modules/semver/functions/lte.js +5 -0
  95. package/.next/standalone/node_modules/semver/functions/neq.js +5 -0
  96. package/.next/standalone/node_modules/semver/functions/parse.js +18 -0
  97. package/.next/standalone/node_modules/semver/functions/satisfies.js +12 -0
  98. package/.next/standalone/node_modules/semver/internal/constants.js +37 -0
  99. package/.next/standalone/node_modules/semver/internal/debug.js +11 -0
  100. package/.next/standalone/node_modules/semver/internal/identifiers.js +29 -0
  101. package/.next/standalone/node_modules/semver/internal/lrucache.js +42 -0
  102. package/.next/standalone/node_modules/semver/internal/parse-options.js +17 -0
  103. package/.next/standalone/node_modules/semver/internal/re.js +223 -0
  104. package/.next/standalone/node_modules/semver/package.json +78 -0
  105. package/.next/standalone/package.json +13 -2
  106. package/.next/standalone/public/favicon.svg +19 -5
  107. package/CHANGELOG.md +212 -0
  108. package/README.md +21 -6
  109. package/README.zh-CN.md +24 -8
  110. package/bin/cli.mjs +47 -18
  111. package/dist/mcp/server.mjs +40 -23699
  112. package/dist/report/index.mjs +49 -18
  113. package/package.json +29 -16
  114. package/.next/standalone/.next/server/chunks/155.js +0 -1
  115. package/.next/standalone/.next/static/chunks/148-0a1e1b0207b89e3f.js +0 -1
  116. package/.next/standalone/.next/static/chunks/930-3035d0b294080d0b.js +0 -1
  117. package/.next/standalone/.next/static/chunks/app/layout-2512ccdfb13aeb17.js +0 -1
  118. package/.next/standalone/.next/static/chunks/app/page-19d3e77d4aa35a63.js +0 -1
  119. package/.next/standalone/.next/static/chunks/app/settings/page-cfeb089549c94f88.js +0 -1
  120. /package/.next/standalone/.next/static/{4YjiQrRI-CsVEPC1UOUEJ → ir1LZCnQKkiNUVXLprtzh}/_buildManifest.js +0 -0
  121. /package/.next/standalone/.next/static/{4YjiQrRI-CsVEPC1UOUEJ → ir1LZCnQKkiNUVXLprtzh}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -38,8 +38,9 @@ Everything runs locally as a Next.js app. Your conversation transcripts never le
38
38
  ## Highlights
39
39
 
40
40
  ### Cross-provider analytics
41
- - One dashboard for both **Claude Code** and **OpenAI Codex CLI**
42
- - Toggle data source from the nav bar; URL persists via `?source=`, last choice cached in cookie
41
+ - One dashboard for both **Claude Code** and **OpenAI Codex CLI**, plus an **All view** that merges the two
42
+ - Toggle data source from the nav bar (Claude · Codex · All), each button rendered with the real provider logo; URL persists via `?source=`, last choice cached in cookie
43
+ - **Worktree-aware Projects** — all worktrees of the same repo collapse into a single project row
43
44
  - Built-in **provider adapter layer** (`lib/providers/`) — adding a third CLI (Gemini CLI, Cursor, Aider, …) is one new file plus a single registry line
44
45
 
45
46
  ### At-a-glance KPIs
@@ -51,7 +52,7 @@ Everything runs locally as a Next.js app. Your conversation transcripts never le
51
52
  - **Sessions** — per-conversation list with model / tokens / cost / duration, plus a message-level timeline
52
53
  - **Projects** — per-`cwd` aggregation cards with sparkline and spend share
53
54
  - **Models** — side-by-side comparison: cost share, tokens share, cache hit, USD pricing
54
- - **Usage** — turn-grouped table with expandable tool-call breakdown, CSV export
55
+ - **Usage** — turn-grouped table with expandable tool-call breakdown, CSV export. **Tokens / Conversations** toggle on the trend chart so you can count rows the way the usage table counts them
55
56
 
56
57
  ### Cost transparency
57
58
  - **Cache savings** is its own KPI — quantifies how much Anthropic prompt caching saved you vs. paying full input price
@@ -63,6 +64,17 @@ Everything runs locally as a Next.js app. Your conversation transcripts never le
63
64
  - **English / 中文** (cookie + localStorage)
64
65
  - Filters: time range (today / 7d / 30d / 90d / all), granularity (hour / day / week / month), model and project multi-select
65
66
 
67
+ ### CLI report (no server)
68
+ - `ccgauge report` prints a colored, aligned terminal usage report in ~0.2 s from the same JSONL the dashboard reads
69
+ - `--range / --source / --by / --since / --until / --model / --project` filters
70
+ - `--json` for machine-readable output; `--no-color` auto-applied when piped — drops cleanly into shell scripts and CI
71
+
72
+ ### MCP server (for LLMs)
73
+ - `ccgauge mcp` runs a stdio JSON-RPC server so **Claude Desktop / Cursor / Cline** can query your local usage directly
74
+ - Nine MCP tools: `usage_summary`, `usage_by_time`, `usage_by_model`, `usage_by_project`, `usage_by_session`, `daily_summary`, `weekly_summary`, `recent_activity`, `cost_estimator`
75
+ - Reasoning-token breakdown surfaced for the models that emit one
76
+ - Separate named cache (`index-mcp-v2.json`) so MCP runs don't contend with the dashboard
77
+
66
78
  ### Privacy by design
67
79
  - 100 % local: read-only access to existing JSONL files, zero outbound network calls
68
80
  - Open source, MIT-licensed
@@ -217,6 +229,7 @@ English with real numbers from your machine.
217
229
  | `daily_summary` | "What did I do today / yesterday / Monday / on YYYY-MM-DD?" Sessions grouped by project + models + top tool calls. |
218
230
  | `weekly_summary` | 7-day roll-up: per-day cost trend, top sessions, top projects, models. `week_offset=-1` for last week. |
219
231
  | `recent_activity` | The N most recently active sessions across providers. |
232
+ | `cost_estimator` | Compute the USD cost of a hypothetical request (`{ source, model, input_tokens, output_tokens, cache_* }`). Uses built-in per-1M-token pricing; does NOT consult usage history. |
220
233
 
221
234
  | Resource URI | Content |
222
235
  | --- | --- |
@@ -334,8 +347,8 @@ debug "why did it answer X".
334
347
  → `daily_summary({ date: "yesterday" })`
335
348
  - *"Generate a Monday stand-up bullet list of what I shipped last week."*
336
349
  → `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`)
350
+ - *"Which 3 projects have I touched most in the last two weeks?"*
351
+ → `usage_by_project({ from: "2026-05-01", to: "2026-05-15", limit: 3 })` — pass explicit `from`/`to` for any window not covered by the named ranges (`7d` / `30d` / `90d` / `this_week` / `last_week` / …).
339
352
  - *"What was my last coding session about?"*
340
353
  → `recent_activity({ limit: 1 })`
341
354
 
@@ -351,7 +364,7 @@ debug "why did it answer X".
351
364
  - *"At my current burn rate, how much will I spend this month?"*
352
365
  → `usage_summary({ range: "this_month" })` + `usage_by_time({ range: "this_month", granularity: "day" })` — LLM extrapolates.
353
366
  - *"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.
367
+ → `cost_estimator({ source: "claude", model: "claude-opus-4-7", input_tokens: 200000, output_tokens: 50000 })` + `usage_summary({ range: "this_month" })` the estimator returns the dollar cost for the hypothetical request without touching your usage history.
355
368
 
356
369
  #### Cross-source comparisons
357
370
 
@@ -448,6 +461,8 @@ pnpm test # codex parser smoke test (Node 22+)
448
461
  pnpm build # next build + copy static into .next/standalone
449
462
  pnpm start # run bin/cli.mjs against the standalone build
450
463
  pnpm screenshots # regenerate docs/screenshots/*.png
464
+ pnpm site:dev # marketing site dev server, http://localhost:4321
465
+ pnpm site:build # build only the site/ marketing site
451
466
  pnpm clean # rm -rf .next node_modules
452
467
  ```
453
468
 
package/README.zh-CN.md CHANGED
@@ -38,8 +38,9 @@ npx ccgauge
38
38
  ## 亮点
39
39
 
40
40
  ### 多 CLI 数据源
41
- - 一份看板覆盖 **Claude Code** 与 **OpenAI Codex CLI**
42
- - 顶部一键切换,URL 用 `?source=` 持久化,cookie 记忆上次选择
41
+ - 一份看板覆盖 **Claude Code** 与 **OpenAI Codex CLI**,并提供 **All 视图**把两者合并查看
42
+ - 顶部三档切换(Claude · Codex · All),每个按钮都带真品牌 logo;URL 用 `?source=` 持久化,cookie 记忆上次选择
43
+ - **Worktree 感知的 Projects 合并** —— 同一个 repo 的所有 worktree 自动并到同一个项目行
43
44
  - 内置 **Provider 适配层**(`lib/providers/`)—— 增加第三个 CLI(Gemini CLI / Cursor / Aider …)只需一个新文件加注册表一行
44
45
 
45
46
  ### KPI 一眼看完
@@ -51,7 +52,7 @@ npx ccgauge
51
52
  - **会话页** —— 每场对话单独成行(模型 / token / 花费 / 时长),点进去看消息级时间线
52
53
  - **项目页** —— 按 `cwd` 聚合成卡片网格,含趋势条与花费占比
53
54
  - **模型页** —— 各模型并排对比:成本占比、token 占比、缓存命中、官方单价
54
- - **用量页** —— 按对话轮次分组的明细表,可展开看每次工具调用,支持 CSV 导出
55
+ - **用量页** —— 按对话轮次分组的明细表,可展开看每次工具调用,支持 CSV 导出。趋势图支持 **Token / 对话数** 切换,让条形图行数和用量表 1:1 对齐
55
56
 
56
57
  ### 成本透明
57
58
  - **缓存节省** 单独成卡 —— 量化 Anthropic prompt caching 实际帮你节省了多少美元
@@ -63,6 +64,17 @@ npx ccgauge
63
64
  - **English / 中文** 双语,cookie + localStorage 双向同步
64
65
  - 完整筛选:时间区间(今天 / 7 天 / 30 天 / 90 天 / 全部)、粒度(小时 / 天 / 周 / 月)、模型 / 项目 multi-select
65
66
 
67
+ ### 命令行报告(无 server)
68
+ - `ccgauge report` 读取同一份 JSONL,在 ~0.2 秒内打出彩色对齐的终端报告
69
+ - `--range / --source / --by / --since / --until / --model / --project` 滤波参数
70
+ - `--json` 输出给脚本;`--no-color` 走管道时自动开启 —— 可以直接塞进 shell 和 CI
71
+
72
+ ### MCP 服务(给 LLM 用)
73
+ - `ccgauge mcp` 起一个 stdio JSON-RPC 服务,让 **Claude Desktop / Cursor / Cline** 等 MCP 客户端直接查你本地的 ccgauge 历史
74
+ - 9 个 MCP tool:`usage_summary`、`usage_by_time`、`usage_by_model`、`usage_by_project`、`usage_by_session`、`daily_summary`、`weekly_summary`、`recent_activity`、`cost_estimator`
75
+ - 支持模型有 reasoning token 时单独折算
76
+ - 独立命名缓存(`index-mcp-v2.json`),MCP 进程不会和仪表盘抢同一份磁盘索引
77
+
66
78
  ### 隐私优先
67
79
  - 100 % 本地:只读访问已有 JSONL 文件,零外网调用
68
80
  - 开源,MIT 协议
@@ -213,7 +225,8 @@ LLM 会自动选合适的 tool、本地调用、用大白话给你带真实数
213
225
  | `usage_by_session` | 会话列表,含标题(首条用户消息)/ 模型 / 时长 / 花费。可按 recent / cost / tokens / duration 排序。 |
214
226
  | `daily_summary` | "今天 / 昨天 / 周一 / YYYY-MM-DD 我都干了啥?" 按项目分组的会话 + 模型 + top 工具调用。 |
215
227
  | `weekly_summary` | 7 天 roll-up:每日花费趋势 + top 会话 + top 项目 + 模型分布。`week_offset=-1` 看上周。 |
216
- | `recent_activity` | 最近 N 条活跃会话(不限日期)。 |
228
+ | `recent_activity` | 最近 N 条活跃会话(默认最近 30 天,可显式给 `from`/`to`)。 |
229
+ | `cost_estimator` | 计算"假设我用 X 模型发 N 个 token 要花多少钱"。直接读内置 per-1M-token 单价表,不查历史。常用于额度规划 / what-if。 |
217
230
 
218
231
  | Resource URI | 内容 |
219
232
  | --- | --- |
@@ -329,7 +342,7 @@ LLM 大概率会调的工具——方便你"为什么会这样回答"反查。
329
342
  - *"给我一份周一 standup 用的 bullet list,列我上周完成的事。"*
330
343
  → `weekly_summary({ week_offset: -1 })`
331
344
  - *"过去两周我接触最多的 3 个项目是什么?"*
332
- → `usage_by_project({ range: "14d", limit: 3 })`(LLM 也可能补一次 `weekly_summary`)
345
+ → `usage_by_project({ from: "2026-05-01", to: "2026-05-15", limit: 3 })`——非 `7d` / `30d` / `90d` / `this_week` / `last_week` 等命名窗口时,传显式 `from` / `to`。
333
346
  - *"我最近一次的编码会话是关于什么的?"*
334
347
  → `recent_activity({ limit: 1 })`
335
348
 
@@ -345,7 +358,7 @@ LLM 大概率会调的工具——方便你"为什么会这样回答"反查。
345
358
  - *"按当前消耗速度,本月预计花多少?"*
346
359
  → `usage_summary({ range: "this_month" })` + `usage_by_time({ range: "this_month", granularity: "day" })`——LLM 自己外推。
347
360
  - *"如果我今天再在 Opus 4.7 上跑 200K input + 50K output,本月累计要多少?"*
348
- → `usage_summary({ range: "this_month" })` + LLM 按公开单价做算术。
361
+ → `cost_estimator({ source: "claude", model: "claude-opus-4-7", input_tokens: 200000, output_tokens: 50000 })` + `usage_summary({ range: "this_month" })`——estimator 直接按内置单价表返回这笔假设请求的美元成本,不查历史。
349
362
 
350
363
  #### 跨数据源对比
351
364
 
@@ -441,6 +454,8 @@ pnpm test # codex parser 烟测(Node 22+)
441
454
  pnpm build # next build + 把 static 拷进 .next/standalone
442
455
  pnpm start # 用 bin/cli.mjs 跑 standalone 产物
443
456
  pnpm screenshots # 重新生成 docs/screenshots/*.png
457
+ pnpm site:dev # 产品官网开发服务,http://localhost:4321
458
+ pnpm site:build # 只构建 site/ 产品官网
444
459
  pnpm clean # rm -rf .next node_modules
445
460
  ```
446
461
 
@@ -495,10 +510,11 @@ pnpm publish --access public # 会自动先跑 pnpm build(prepublishOnly)
495
510
  ## 产品官网
496
511
 
497
512
  产品官网(Astro + Tailwind 自建、中英双语、暗 / 亮主题、独立部署)放在
498
- [`site/`](./site/) 目录。它跟着主仓库一起在 git 里,但**不会**进 npm 包。
513
+ [`site/`](./site/) 目录。它跟着主仓库一起在 git 里,但**不会**进 npm 包;
514
+ 命令和依赖统一由根目录 `package.json` 管理。
499
515
 
500
516
  ```bash
501
- cd site && pnpm install && pnpm dev # http://localhost:4321
517
+ pnpm site:dev # http://localhost:4321
502
518
  ```
503
519
 
504
520
  构建 / 部署细节见 [`site/README.md`](./site/README.md)。
package/bin/cli.mjs CHANGED
@@ -142,8 +142,9 @@ program
142
142
  program
143
143
  .command('mcp')
144
144
  .description('start the MCP server (stdio) so LLMs can query usage data')
145
- .action(async () => {
146
- await startMcp();
145
+ .option('--check', 'verify the bundle + indexer; print one line per provider and exit')
146
+ .action(async (opts) => {
147
+ await startMcp(opts);
147
148
  });
148
149
 
149
150
  function addReportOptions(cmd) {
@@ -438,7 +439,7 @@ or run the full build with
438
439
  }
439
440
  }
440
441
 
441
- async function startMcp() {
442
+ async function startMcp(opts = {}) {
442
443
  const bundle = join(packageRoot, 'dist', 'mcp', 'server.mjs');
443
444
  if (!existsSync(bundle)) {
444
445
  console.error(`
@@ -455,20 +456,39 @@ or run the full build with
455
456
  `);
456
457
  process.exit(1);
457
458
  }
458
- // Hand control to the bundled MCP server. It owns the stdio JSON-RPC
459
- // session for the lifetime of the parent (the LLM client) process.
460
- const child = spawn(process.execPath, [bundle], {
461
- stdio: 'inherit',
462
- env: process.env,
463
- });
464
- const forward = (sig) => () => {
465
- if (!child.killed) child.kill(sig);
466
- };
467
- process.on('SIGINT', forward('SIGINT'));
468
- process.on('SIGTERM', forward('SIGTERM'));
469
- child.on('exit', (code, sig) => {
470
- process.exit(typeof code === 'number' ? code : sig ? 128 : 0);
471
- });
459
+
460
+ // --check: don't actually run the JSON-RPC server load the bundle,
461
+ // boot the indexer, print one line per provider, and exit. Lets users
462
+ // verify their install without wiring up an MCP client.
463
+ if (opts.check) {
464
+ const mod = await import(pathToFileURL(bundle).href);
465
+ if (typeof mod.printCheck !== 'function') {
466
+ console.error('[ccgauge-mcp] this bundle was built without --check support');
467
+ process.exit(1);
468
+ }
469
+ const code = await mod.printCheck();
470
+ process.exit(typeof code === 'number' ? code : 0);
471
+ }
472
+
473
+ // Run the bundled MCP server **in this process** — the bundle exposes a
474
+ // top-level `runStdioServer()` so we just import + invoke it. Spawning a
475
+ // second Node process here is wasted memory/latency (LLM clients already
476
+ // spawn `ccgauge mcp` per conversation), and forwarding signals across
477
+ // processes is brittle (e.g. SIGHUP isn't covered by the old shim).
478
+ try {
479
+ const mod = await import(pathToFileURL(bundle).href);
480
+ if (typeof mod.runStdioServer !== 'function') {
481
+ console.error('[ccgauge-mcp] bundle missing runStdioServer export');
482
+ process.exit(1);
483
+ }
484
+ await mod.runStdioServer();
485
+ // runStdioServer keeps the loop alive via the stdio transport; if it
486
+ // ever returns it means the transport closed cleanly.
487
+ process.exit(0);
488
+ } catch (err) {
489
+ console.error('[ccgauge-mcp] failed to start:', err?.message ?? err);
490
+ process.exit(1);
491
+ }
472
492
  }
473
493
 
474
494
  async function resolvePort(opts) {
@@ -567,9 +587,18 @@ async function readState() {
567
587
  try {
568
588
  const raw = await readFile(STATE_FILE, 'utf8');
569
589
  const parsed = JSON.parse(raw);
570
- if (!parsed || typeof parsed !== 'object') return null;
590
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
571
591
  // Treat unknown / future versions as stale (auto-clean on next stop/start).
572
592
  if (parsed.version !== STATE_VERSION) return null;
593
+ // Type-guard the fields callers actually use — `stop`, `status`, and
594
+ // `restart` all assume these have the right shape. A hand-edited
595
+ // state.json with the right `version` but garbage in `pid` could
596
+ // otherwise crash `safeKill()` or `isProcessRunning()`.
597
+ if (typeof parsed.pid !== 'number' || !Number.isInteger(parsed.pid) || parsed.pid <= 0) {
598
+ return null;
599
+ }
600
+ if (typeof parsed.url !== 'string' || !parsed.url) return null;
601
+ if (typeof parsed.logFile !== 'string' || !parsed.logFile) return null;
573
602
  return parsed;
574
603
  } catch {
575
604
  return null;