ccgauge 0.4.0 → 1.0.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 (91) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-build-manifest.json +37 -37
  3. package/.next/standalone/.next/app-path-routes-manifest.json +5 -5
  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.js +2 -2
  7. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/api/blocks/route.js +1 -1
  10. package/.next/standalone/.next/server/app/api/blocks/route.js.nft.json +1 -1
  11. package/.next/standalone/.next/server/app/api/blocks/route_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/api/export/usage/route.js +1 -1
  13. package/.next/standalone/.next/server/app/api/export/usage/route.js.nft.json +1 -1
  14. package/.next/standalone/.next/server/app/api/export/usage/route_client-reference-manifest.js +1 -1
  15. package/.next/standalone/.next/server/app/api/pricing/route.js +1 -1
  16. package/.next/standalone/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
  17. package/.next/standalone/.next/server/app/api/projects/route.js +1 -1
  18. package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/api/scan/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/scan/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/scan/route_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/api/sessions/route.js +1 -1
  24. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  26. package/.next/standalone/.next/server/app/api/usage/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/usage/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/usage/route_client-reference-manifest.js +1 -1
  29. package/.next/standalone/.next/server/app/models/page.js +2 -2
  30. package/.next/standalone/.next/server/app/models/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/models/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/page.js +2 -2
  33. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/projects/[id]/page.js +2 -2
  36. package/.next/standalone/.next/server/app/projects/[id]/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  39. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/sessions/[id]/page.js +2 -2
  42. package/.next/standalone/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/sessions/page.js +2 -2
  45. package/.next/standalone/.next/server/app/sessions/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/settings/page.js +2 -2
  48. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/usage/page.js +3 -2
  51. package/.next/standalone/.next/server/app/usage/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app-paths-manifest.json +5 -5
  54. package/.next/standalone/.next/server/chunks/155.js +1 -0
  55. package/.next/standalone/.next/server/chunks/567.js +28 -0
  56. package/.next/standalone/.next/server/chunks/716.js +1 -1
  57. package/.next/standalone/.next/server/chunks/775.js +1 -1
  58. package/.next/standalone/.next/server/functions-config-manifest.json +3 -3
  59. package/.next/standalone/.next/server/pages/500.html +1 -1
  60. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  61. package/.next/standalone/.next/static/chunks/148-d2db1767205d1ca8.js +1 -0
  62. package/.next/standalone/.next/static/chunks/app/{error-89ee9e078058915d.js → error-3e48784f89c5ae8d.js} +1 -1
  63. package/.next/standalone/.next/static/chunks/app/layout-ca9328306c8cbb8e.js +1 -0
  64. package/.next/standalone/.next/static/chunks/app/models/page-dcd29049a7b0641c.js +1 -0
  65. package/.next/standalone/.next/static/chunks/app/page-11fc9a0ded501248.js +1 -0
  66. package/.next/standalone/.next/static/chunks/app/projects/[id]/page-d6725ed17b04a743.js +1 -0
  67. package/.next/standalone/.next/static/chunks/app/sessions/[id]/page-d6725ed17b04a743.js +1 -0
  68. package/.next/standalone/.next/static/chunks/app/settings/page-cfeb089549c94f88.js +1 -0
  69. package/.next/standalone/.next/static/chunks/app/usage/page-63c230b1e2c5c63c.js +1 -0
  70. package/.next/standalone/.next/static/css/b34dbb2d1cbeaf5e.css +3 -0
  71. package/.next/standalone/package.json +15 -4
  72. package/CHANGELOG.md +192 -0
  73. package/README.md +41 -2
  74. package/README.zh-CN.md +50 -2
  75. package/bin/cli.mjs +95 -3
  76. package/dist/mcp/server.mjs +151 -30
  77. package/dist/report/index.mjs +2177 -0
  78. package/package.json +15 -4
  79. package/.next/standalone/.next/server/chunks/426.js +0 -23
  80. package/.next/standalone/.next/server/chunks/520.js +0 -1
  81. package/.next/standalone/.next/static/chunks/454-d0e7d0fa6f643c41.js +0 -1
  82. package/.next/standalone/.next/static/chunks/app/layout-a6e30ba3a7f39737.js +0 -1
  83. package/.next/standalone/.next/static/chunks/app/models/page-e0e1b5979547421a.js +0 -1
  84. package/.next/standalone/.next/static/chunks/app/page-9347dfa20dabb24b.js +0 -1
  85. package/.next/standalone/.next/static/chunks/app/projects/[id]/page-5804875e3dc384df.js +0 -1
  86. package/.next/standalone/.next/static/chunks/app/sessions/[id]/page-5804875e3dc384df.js +0 -1
  87. package/.next/standalone/.next/static/chunks/app/settings/page-334168b522eac1b1.js +0 -1
  88. package/.next/standalone/.next/static/chunks/app/usage/page-7789fec27778df9a.js +0 -1
  89. package/.next/standalone/.next/static/css/c34cd36ce5fc39e2.css +0 -3
  90. /package/.next/standalone/.next/static/{w_l54xHgbhALYXmZcmUxC → 2kImy5ZkabMplKu3i19s7}/_buildManifest.js +0 -0
  91. /package/.next/standalone/.next/static/{w_l54xHgbhALYXmZcmUxC → 2kImy5ZkabMplKu3i19s7}/_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
 
@@ -397,7 +436,7 @@ This repo is a working Next.js project — run the dashboard against your live d
397
436
  git clone https://github.com/chengzuopeng/ccgauge.git
398
437
  cd ccgauge
399
438
  pnpm install
400
- pnpm dev # http://localhost:3737
439
+ pnpm dev # http://localhost:3738
401
440
  ```
402
441
 
403
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
 
@@ -392,7 +429,7 @@ lib/providers/<name>/
392
429
  git clone https://github.com/chengzuopeng/ccgauge.git
393
430
  cd ccgauge
394
431
  pnpm install
395
- pnpm dev # http://localhost:3737
432
+ pnpm dev # http://localhost:3738
396
433
  ```
397
434
 
398
435
  常用脚本:
@@ -455,6 +492,17 @@ pnpm publish --access public # 会自动先跑 pnpm build(prepublishOnly)
455
492
  `prompt caching 节省` · `5 小时窗口监控` · `rate limit 倒计时` · `ccusage 替代品` ·
456
493
  `ccusage web 版` · `token 用量分析` · `本地 AI 用量监控` · `自部署 AI 看板`
457
494
 
495
+ ## 产品官网
496
+
497
+ 产品官网(Astro + Tailwind 自建、中英双语、暗 / 亮主题、独立部署)放在
498
+ [`site/`](./site/) 目录。它跟着主仓库一起在 git 里,但**不会**进 npm 包。
499
+
500
+ ```bash
501
+ cd site && pnpm install && pnpm dev # http://localhost:4321
502
+ ```
503
+
504
+ 构建 / 部署细节见 [`site/README.md`](./site/README.md)。
505
+
458
506
  ## 许可证
459
507
 
460
508
  MIT —— 详见 [LICENSE](https://github.com/chengzuopeng/ccgauge/blob/main/LICENSE)。
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', 'mcp']);
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';
@@ -139,6 +146,28 @@ program
139
146
  await startMcp();
140
147
  });
141
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
+
142
171
  await program.parseAsync(normalizeArgv(process.argv));
143
172
 
144
173
  function normalizeArgv(argv) {
@@ -228,6 +257,9 @@ async function startBackground(standaloneEntry, opts) {
228
257
  env,
229
258
  detached: true,
230
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,
231
263
  });
232
264
  child.unref();
233
265
  // Once spawn() has dup'd these fds into the child, the parent can release them.
@@ -351,6 +383,61 @@ or run the dev server with
351
383
  process.exit(1);
352
384
  }
353
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
+ let payload;
420
+ try {
421
+ const mod = await import(pathToFileURL(bundle).href);
422
+ const out = await mod.runReport(reportOpts);
423
+ payload = out.endsWith('\n') ? out : out + '\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
+ // Use the write() return value rather than chaining a `drain` listener
431
+ // after the fact: if drain fires between the write and the listener
432
+ // attach, we'd hang forever waiting for an event that already happened.
433
+ const flushed = process.stdout.write(payload);
434
+ if (flushed) {
435
+ process.exit(0);
436
+ } else {
437
+ process.stdout.once('drain', () => process.exit(0));
438
+ }
439
+ }
440
+
354
441
  async function startMcp() {
355
442
  const bundle = join(packageRoot, 'dist', 'mcp', 'server.mjs');
356
443
  if (!existsSync(bundle)) {
@@ -389,6 +476,11 @@ async function resolvePort(opts) {
389
476
  if (!Number.isInteger(preferred) || preferred <= 0 || preferred > 65535) {
390
477
  throw new Error(`invalid port: ${opts.port}`);
391
478
  }
479
+ // Try the preferred port first, then up to 19 ports above it (capped at
480
+ // 65535), then 0 (let the OS pick an ephemeral port). For unusually high
481
+ // preferred values (e.g. 65530) the +N candidates are clamped by the
482
+ // filter, leaving just the preferred + ephemeral fallback — that's still
483
+ // correct, just narrower.
392
484
  const candidates = opts.strictPort
393
485
  ? preferred
394
486
  : [preferred, ...Array.from({ length: 19 }, (_, i) => preferred + i + 1).filter((p) => p <= 65535), 0];
@@ -21097,13 +21097,52 @@ var StdioServerTransport = class {
21097
21097
  }
21098
21098
  };
21099
21099
 
21100
+ // lib/date-utils.ts
21101
+ var DATE_PREFIX_RE = /^(\d{4})-(\d{2})-(\d{2})(.*)$/;
21102
+ function atStartOfDay(d) {
21103
+ const r = new Date(d);
21104
+ r.setHours(0, 0, 0, 0);
21105
+ return r;
21106
+ }
21107
+ function atEndOfDay(d) {
21108
+ const r = new Date(d);
21109
+ r.setHours(23, 59, 59, 999);
21110
+ return r;
21111
+ }
21112
+ function parseLocalDateOnly(s) {
21113
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
21114
+ if (!m) return null;
21115
+ const y = Number(m[1]);
21116
+ const month = Number(m[2]);
21117
+ const day = Number(m[3]);
21118
+ const dt = new Date(y, month - 1, day);
21119
+ if (dt.getFullYear() !== y || dt.getMonth() !== month - 1 || dt.getDate() !== day) {
21120
+ return null;
21121
+ }
21122
+ return dt;
21123
+ }
21124
+ function isLocalDateOnly(s) {
21125
+ return parseLocalDateOnly(s) !== null;
21126
+ }
21127
+ function parseDateLike(s, opts = {}) {
21128
+ const datePrefix = DATE_PREFIX_RE.exec(s);
21129
+ if (datePrefix) {
21130
+ const dateOnly = `${datePrefix[1]}-${datePrefix[2]}-${datePrefix[3]}`;
21131
+ const localDate = parseLocalDateOnly(dateOnly);
21132
+ if (!localDate) return null;
21133
+ if (datePrefix[4] === "") {
21134
+ return opts.upperBoundDateOnly ? atEndOfDay(localDate) : localDate;
21135
+ }
21136
+ }
21137
+ const dt = new Date(s);
21138
+ return Number.isNaN(dt.getTime()) ? null : dt;
21139
+ }
21140
+
21100
21141
  // lib/mcp/schema.ts
21101
21142
  var sourceSchema = external_exports.enum(["claude", "codex", "all"]).default("all");
21102
21143
  var granularitySchema = external_exports.enum(["hour", "day", "week", "month"]).default("day");
21103
21144
  function isValidDateString(s) {
21104
- if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return true;
21105
- const dt = new Date(s);
21106
- return !Number.isNaN(dt.getTime());
21145
+ return parseDateLike(s) !== null;
21107
21146
  }
21108
21147
  var dateBoundSchema = external_exports.string().refine(isValidDateString, {
21109
21148
  message: "must be a YYYY-MM-DD date or a full ISO 8601 timestamp"
@@ -21131,7 +21170,7 @@ var daySchema = external_exports.string().refine(
21131
21170
  const lower = s.toLowerCase();
21132
21171
  if (SPECIAL_DAYS.includes(lower)) return true;
21133
21172
  if (WEEKDAYS.includes(lower)) return true;
21134
- return /^\d{4}-\d{2}-\d{2}$/.test(s);
21173
+ return isLocalDateOnly(s);
21135
21174
  },
21136
21175
  {
21137
21176
  message: 'must be "today", "yesterday", a weekday name (monday..sunday), or YYYY-MM-DD'
@@ -21257,7 +21296,8 @@ function parseAssistant(raw, file) {
21257
21296
  toolNames,
21258
21297
  hasThinking,
21259
21298
  textPreview,
21260
- filePath: file
21299
+ filePath: file,
21300
+ isSidechain: raw.isSidechain === true ? true : void 0
21261
21301
  };
21262
21302
  }
21263
21303
  function parseUser(raw, file) {
@@ -21277,6 +21317,8 @@ function parseUser(raw, file) {
21277
21317
  }
21278
21318
  }
21279
21319
  }
21320
+ const isSidechain = raw.isSidechain === true;
21321
+ const isSynthetic = isSidechain || !!textPreview && isSyntheticUserText(textPreview);
21280
21322
  return {
21281
21323
  type: "user",
21282
21324
  source: "claude",
@@ -21286,9 +21328,18 @@ function parseUser(raw, file) {
21286
21328
  sessionId: raw.sessionId ?? "",
21287
21329
  cwd: raw.cwd ?? "",
21288
21330
  textPreview,
21331
+ isSynthetic,
21332
+ isSidechain: isSidechain ? true : void 0,
21289
21333
  filePath: file
21290
21334
  };
21291
21335
  }
21336
+ function isSyntheticUserText(text) {
21337
+ const t = text.trimStart();
21338
+ if (t.startsWith("Base directory for this skill:")) return true;
21339
+ if (t.startsWith("<system-reminder>")) return true;
21340
+ if (t.startsWith("Caveat: The messages below were generated by")) return true;
21341
+ return false;
21342
+ }
21292
21343
 
21293
21344
  // lib/pricing/builtin.ts
21294
21345
  var BUILTIN_PRICING = {
@@ -21489,7 +21540,16 @@ var claudeAdapter = {
21489
21540
  displayName: { en: "Claude", zh: "Claude" },
21490
21541
  shortLabel: "C",
21491
21542
  color: { fg: "#b45309", bg: "#fef3c7" },
21492
- parserVersion: "claude-v1",
21543
+ // v1 → v3 (no v2 ever shipped on npm): user records now carry an
21544
+ // `isSynthetic` flag so skill metadata + <system-reminder> blocks can
21545
+ // still be displayed as the per-call "prompt" on child rows, but are
21546
+ // skipped as turn-boundary anchors so they don't wrongly split a single
21547
+ // conversation into multiple turns.
21548
+ // v4: extend `isSynthetic` to sub-agent first-user records (every record
21549
+ // in a `subagents/agent-*.jsonl` file has `isSidechain: true`); also
21550
+ // propagate `isSidechain` to all records so the indexer's post-link pass
21551
+ // can stitch sub-agent files into the parent session's turn graph.
21552
+ parserVersion: "claude-v4-sidechain-merge",
21493
21553
  capabilities: {
21494
21554
  hasCacheCreation: true,
21495
21555
  hasReasoningTokens: false,
@@ -21714,7 +21774,8 @@ async function parseCodexJsonlFile(file) {
21714
21774
  toolNames: [...turn.toolNames],
21715
21775
  hasThinking: turn.hasThinking,
21716
21776
  textPreview: turn.pendingTextPreview,
21717
- filePath: file
21777
+ filePath: file,
21778
+ effort: turn.effort
21718
21779
  });
21719
21780
  parentLinks.push([uuid2, turn.userUuid]);
21720
21781
  turn.toolNames = [];
@@ -21930,7 +21991,9 @@ var codexAdapter = {
21930
21991
  // ~26% over-counting from duplicate/refresh token_count events).
21931
21992
  // v3: split reasoning_tokens out as a display-only breakdown alongside
21932
21993
  // output_tokens (which still includes reasoning for billing).
21933
- parserVersion: "codex-v3-reasoning-detail",
21994
+ // v4: persist `effort` from turn_context onto each emitted record so the
21995
+ // UI can tag the model column (e.g. `gpt-5.2-codex · high`).
21996
+ parserVersion: "codex-v4-effort",
21934
21997
  capabilities: {
21935
21998
  hasCacheCreation: false,
21936
21999
  hasReasoningTokens: true,
@@ -22018,6 +22081,74 @@ async function savePersistedIndex(payload, name = DEFAULT_INDEX_NAME) {
22018
22081
  await fs3.rename(tmp, filePath);
22019
22082
  }
22020
22083
 
22084
+ // lib/data-loader/link-sidechain.ts
22085
+ var SUBAGENT_FILE_PATTERN = /\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/subagents\/agent-[^/]+\.jsonl$/i;
22086
+ function extractParentSessionFromSubagentPath(filePath) {
22087
+ const m = SUBAGENT_FILE_PATTERN.exec(filePath);
22088
+ return m ? m[1] : null;
22089
+ }
22090
+ function linkSidechainParents({
22091
+ assistantRecords,
22092
+ userRecords,
22093
+ parentMap
22094
+ }) {
22095
+ const parentAssistantsBySession = /* @__PURE__ */ new Map();
22096
+ for (const a of assistantRecords) {
22097
+ if (a.isSidechain) continue;
22098
+ if (!a.sessionId) continue;
22099
+ let list = parentAssistantsBySession.get(a.sessionId);
22100
+ if (!list) {
22101
+ list = [];
22102
+ parentAssistantsBySession.set(a.sessionId, list);
22103
+ }
22104
+ list.push(a);
22105
+ }
22106
+ for (const list of parentAssistantsBySession.values()) {
22107
+ list.sort((x, y) => x.timestamp < y.timestamp ? -1 : x.timestamp > y.timestamp ? 1 : 0);
22108
+ }
22109
+ const firstSidechainUserByFile = /* @__PURE__ */ new Map();
22110
+ for (const u of userRecords) {
22111
+ if (!u.isSidechain) continue;
22112
+ const existing = firstSidechainUserByFile.get(u.filePath);
22113
+ if (!existing || u.timestamp < existing.timestamp) {
22114
+ firstSidechainUserByFile.set(u.filePath, u);
22115
+ }
22116
+ }
22117
+ const stats = {
22118
+ subagentFiles: 0,
22119
+ relinked: 0,
22120
+ orphans: 0,
22121
+ alreadyLinked: 0
22122
+ };
22123
+ for (const [filePath, firstUser] of firstSidechainUserByFile) {
22124
+ const parentSessionId = extractParentSessionFromSubagentPath(filePath);
22125
+ if (!parentSessionId) continue;
22126
+ stats.subagentFiles += 1;
22127
+ const existingParent = parentMap[firstUser.uuid];
22128
+ if (existingParent !== null && existingParent !== void 0) {
22129
+ stats.alreadyLinked += 1;
22130
+ continue;
22131
+ }
22132
+ const parentAssistants = parentAssistantsBySession.get(parentSessionId);
22133
+ if (!parentAssistants || parentAssistants.length === 0) {
22134
+ stats.orphans += 1;
22135
+ continue;
22136
+ }
22137
+ const t0 = firstUser.timestamp;
22138
+ let anchor;
22139
+ for (let i = parentAssistants.length - 1; i >= 0; i -= 1) {
22140
+ if (parentAssistants[i].timestamp <= t0) {
22141
+ anchor = parentAssistants[i];
22142
+ break;
22143
+ }
22144
+ }
22145
+ if (!anchor) anchor = parentAssistants[0];
22146
+ parentMap[firstUser.uuid] = anchor.uuid;
22147
+ stats.relinked += 1;
22148
+ }
22149
+ return stats;
22150
+ }
22151
+
22021
22152
  // lib/data-loader/indexer.ts
22022
22153
  var RECONCILE_DEBOUNCE_MS = 200;
22023
22154
  var SNAPSHOT_REBUILD_DEBOUNCE_MS = 100;
@@ -22334,6 +22465,11 @@ var FileIndexer = class {
22334
22465
  const dedupedUsers = dedupUserRecords(user).sort(
22335
22466
  (a, b) => a.timestamp.localeCompare(b.timestamp)
22336
22467
  );
22468
+ linkSidechainParents({
22469
+ assistantRecords: dedupedAssistants,
22470
+ userRecords: dedupedUsers,
22471
+ parentMap
22472
+ });
22337
22473
  for (const rec of dedupedAssistants) bySource[rec.source].assistantRecords += 1;
22338
22474
  const stats = {
22339
22475
  filesScanned: this.files.size,
@@ -22591,29 +22727,14 @@ function parseDateRange(args) {
22591
22727
  }
22592
22728
  }
22593
22729
  function parseStrictDate(s, field, isUpperBound) {
22594
- if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
22595
- const [y, m, d] = s.split("-").map(Number);
22596
- const dt2 = new Date(y, m - 1, d);
22597
- return isUpperBound ? atEndOfDay(dt2) : dt2;
22598
- }
22599
- const dt = new Date(s);
22600
- if (Number.isNaN(dt.getTime())) {
22730
+ const dt = parseDateLike(s, { upperBoundDateOnly: isUpperBound });
22731
+ if (!dt) {
22601
22732
  throw new Error(
22602
22733
  `invalid '${field}' argument: ${JSON.stringify(s)}. Expected YYYY-MM-DD or a full ISO 8601 timestamp.`
22603
22734
  );
22604
22735
  }
22605
22736
  return dt;
22606
22737
  }
22607
- function atStartOfDay(d) {
22608
- const r = new Date(d);
22609
- r.setHours(0, 0, 0, 0);
22610
- return r;
22611
- }
22612
- function atEndOfDay(d) {
22613
- const r = new Date(d);
22614
- r.setHours(23, 59, 59, 999);
22615
- return r;
22616
- }
22617
22738
  function startOfWeek(d) {
22618
22739
  const day = d.getDay() || 7;
22619
22740
  const monday = new Date(d);
@@ -22632,7 +22753,8 @@ function costOfRecord(rec) {
22632
22753
  // lib/utils.ts
22633
22754
  function projectNameFromCwd(cwd) {
22634
22755
  if (!cwd) return "(unknown)";
22635
- const parts = cwd.replace(/\/+$/, "").split("/");
22756
+ const trimmed = cwd.replace(/[/\\]+$/, "");
22757
+ const parts = trimmed.split(/[/\\]+/);
22636
22758
  return parts[parts.length - 1] || cwd;
22637
22759
  }
22638
22760
 
@@ -23317,10 +23439,9 @@ function parseDayArg(input) {
23317
23439
  const { start, end } = dayOf(target);
23318
23440
  return { from: start, to: end, label: lower };
23319
23441
  }
23320
- if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
23321
- const [y, m, d] = input.split("-").map(Number);
23322
- const dt = new Date(y, m - 1, d);
23323
- const { start, end } = dayOf(dt);
23442
+ const explicitDate = parseLocalDateOnly(input);
23443
+ if (explicitDate) {
23444
+ const { start, end } = dayOf(explicitDate);
23324
23445
  return { from: start, to: end, label: input };
23325
23446
  }
23326
23447
  throw new Error(