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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-build-manifest.json +37 -37
- package/.next/standalone/.next/app-path-routes-manifest.json +5 -5
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/blocks/route.js +1 -1
- package/.next/standalone/.next/server/app/api/blocks/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/blocks/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/export/usage/route.js +1 -1
- package/.next/standalone/.next/server/app/api/export/usage/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/export/usage/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/pricing/route.js +1 -1
- package/.next/standalone/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/projects/route.js +1 -1
- package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/scan/route.js +1 -1
- package/.next/standalone/.next/server/app/api/scan/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scan/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/usage/route.js +1 -1
- package/.next/standalone/.next/server/app/api/usage/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/usage/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/models/page.js +2 -2
- package/.next/standalone/.next/server/app/models/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/models/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/[id]/page.js +2 -2
- package/.next/standalone/.next/server/app/projects/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -2
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/sessions/[id]/page.js +2 -2
- package/.next/standalone/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/sessions/page.js +2 -2
- package/.next/standalone/.next/server/app/sessions/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page.js +2 -2
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/usage/page.js +3 -2
- package/.next/standalone/.next/server/app/usage/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +5 -5
- package/.next/standalone/.next/server/chunks/155.js +1 -0
- package/.next/standalone/.next/server/chunks/567.js +28 -0
- package/.next/standalone/.next/server/chunks/716.js +1 -1
- package/.next/standalone/.next/server/chunks/775.js +1 -1
- package/.next/standalone/.next/server/functions-config-manifest.json +3 -3
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/148-d2db1767205d1ca8.js +1 -0
- package/.next/standalone/.next/static/chunks/app/{error-89ee9e078058915d.js → error-3e48784f89c5ae8d.js} +1 -1
- package/.next/standalone/.next/static/chunks/app/layout-ca9328306c8cbb8e.js +1 -0
- package/.next/standalone/.next/static/chunks/app/models/page-dcd29049a7b0641c.js +1 -0
- package/.next/standalone/.next/static/chunks/app/page-11fc9a0ded501248.js +1 -0
- package/.next/standalone/.next/static/chunks/app/projects/[id]/page-d6725ed17b04a743.js +1 -0
- package/.next/standalone/.next/static/chunks/app/sessions/[id]/page-d6725ed17b04a743.js +1 -0
- package/.next/standalone/.next/static/chunks/app/settings/page-cfeb089549c94f88.js +1 -0
- package/.next/standalone/.next/static/chunks/app/usage/page-63c230b1e2c5c63c.js +1 -0
- package/.next/standalone/.next/static/css/b34dbb2d1cbeaf5e.css +3 -0
- package/.next/standalone/package.json +15 -4
- package/CHANGELOG.md +192 -0
- package/README.md +41 -2
- package/README.zh-CN.md +50 -2
- package/bin/cli.mjs +95 -3
- package/dist/mcp/server.mjs +151 -30
- package/dist/report/index.mjs +2177 -0
- package/package.json +15 -4
- package/.next/standalone/.next/server/chunks/426.js +0 -23
- package/.next/standalone/.next/server/chunks/520.js +0 -1
- package/.next/standalone/.next/static/chunks/454-d0e7d0fa6f643c41.js +0 -1
- package/.next/standalone/.next/static/chunks/app/layout-a6e30ba3a7f39737.js +0 -1
- package/.next/standalone/.next/static/chunks/app/models/page-e0e1b5979547421a.js +0 -1
- package/.next/standalone/.next/static/chunks/app/page-9347dfa20dabb24b.js +0 -1
- package/.next/standalone/.next/static/chunks/app/projects/[id]/page-5804875e3dc384df.js +0 -1
- package/.next/standalone/.next/static/chunks/app/sessions/[id]/page-5804875e3dc384df.js +0 -1
- package/.next/standalone/.next/static/chunks/app/settings/page-334168b522eac1b1.js +0 -1
- package/.next/standalone/.next/static/chunks/app/usage/page-7789fec27778df9a.js +0 -1
- package/.next/standalone/.next/static/css/c34cd36ce5fc39e2.css +0 -3
- /package/.next/standalone/.next/static/{w_l54xHgbhALYXmZcmUxC → 2kImy5ZkabMplKu3i19s7}/_buildManifest.js +0 -0
- /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
|
|
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:
|
|
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:
|
|
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([
|
|
29
|
-
|
|
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];
|
package/dist/mcp/server.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22595
|
-
|
|
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
|
|
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
|
-
|
|
23321
|
-
|
|
23322
|
-
const
|
|
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(
|