@loredunk/ccoach 0.1.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.
@@ -0,0 +1,1957 @@
1
+ import { dirname, join } from "node:path";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+
5
+ //#region src/window.ts
6
+ const YMD = /^\d{4}-\d{2}-\d{2}$/;
7
+ function localYmd(d) {
8
+ return new Intl.DateTimeFormat("en-CA").format(d);
9
+ }
10
+ function addDaysYmd(ymd, delta) {
11
+ const [y, m, day] = ymd.split("-").map(Number);
12
+ const dt = new Date(Date.UTC(y, m - 1, day + delta));
13
+ return dt.toISOString().slice(0, 10);
14
+ }
15
+ function resolveWindow(o, now) {
16
+ const today = localYmd(now);
17
+ if (o.date) {
18
+ if (!YMD.test(o.date)) throw new Error(`invalid --date ${o.date} (want YYYY-MM-DD)`);
19
+ return {
20
+ fromYmd: o.date,
21
+ toYmd: o.date,
22
+ desc: o.date
23
+ };
24
+ }
25
+ if (o.since) {
26
+ if (!YMD.test(o.since)) throw new Error(`invalid --since ${o.since} (want YYYY-MM-DD)`);
27
+ return {
28
+ fromYmd: o.since,
29
+ toYmd: today,
30
+ desc: `${o.since} 至 ${today}`
31
+ };
32
+ }
33
+ if (o.days && o.days > 0) {
34
+ const from = addDaysYmd(today, -(o.days - 1));
35
+ return {
36
+ fromYmd: from,
37
+ toYmd: today,
38
+ desc: `最近 ${o.days} 天 (${from} 至 ${today})`
39
+ };
40
+ }
41
+ return {
42
+ fromYmd: today,
43
+ toYmd: today,
44
+ desc: today
45
+ };
46
+ }
47
+ function inLocalRange(ts, w) {
48
+ const ymd = localYmd(ts);
49
+ return ymd >= w.fromYmd && ymd <= w.toYmd;
50
+ }
51
+
52
+ //#endregion
53
+ //#region src/model.ts
54
+ const emptyTokens = () => ({
55
+ input: 0,
56
+ cached_input: 0,
57
+ output: 0,
58
+ reasoning_output: 0,
59
+ cache_creation: 0,
60
+ total: 0
61
+ });
62
+ const REPORT_GLOSSARY = {
63
+ _about: "仅本机数据,不跨机器汇总;不含任何账户级配额百分比(CLI 下 rate_limits 恒为 null)。",
64
+ cache_hit_rate: "cached_input /(cached_input + 非缓存输入)的缓存命中率,两平台口径统一、恒在 0–1(Codex 下 input 含缓存,等价于 cached/input);越高越省钱(重复上下文被缓存复用)。",
65
+ reasoning_ratio: "reasoning_output / output,推理 token 占输出的比例;偏高常意味任务被反复推理。",
66
+ estimated_cost_usd: "离线 best-effort fallback 估算,仅供参考、不等于实际账单。权威成本由 skill 层联网查询各模型官方定价后计算覆盖;本字段仅在未联网查价时作兜底(用内置离线 fallback 价表)。",
67
+ models_timeline: "每个模型的首/末出现日期(first_day/last_day,本机时区)与每日 token;用于时间感知判断:某旧模型占大头若只因新模型当时还没出现,不应判为浪费。(防 token 爆炸:列表取 token 前 10 个模型,每个 days[] 只列最近 31 天;first_day/last_day/tokens 为真实全量。repos/sources/languages 同样按 token 取前 N。)",
68
+ model_tokens: "每个模型的全窗口 token 分桶(input/cached_input/output/reasoning_output/cache_creation/total)+ 离线 fallback 估算成本与 priced 标记。供 skill 层按实际模型名联网查官方定价后自行计价(Claude 互斥桶 vs Codex cached⊆input 公式不同,必须分桶)。按 token 取前 10 个模型。",
69
+ tokens: "input/cached_input/output/reasoning_output/cache_creation/total;cached_input 是 input 的子集。",
70
+ prompt_signals: "仅由 user prompt 派生的数值信号(长度/结构化率/文件引用率/约束率/返工率),不含任何原文。",
71
+ error_signals: "工具失败率/中断数/API错误,及失败按工具与按白名单类别(git/test/build/permission/network/timeout/not-read/other)。仅由工具结果派生计数+类别,绝不含原始 stderr/输出/文件内容/命令全行(隐私红线细化,ADR 0016)。",
72
+ rework_signals: "编辑次数、用户事后手改率(userModified)、累计新增/删除行数(structuredPatch)。只派生计数,绝不含 diff 文本(ADR 0017)。",
73
+ skills: "各 skill 被调用的次数(按 attributionSkill),反映 skill 使用画像。",
74
+ environment: "Claude Code 版本、权限模式分布、附件数、子代理消息数——只由记录元数据派生的非敏感标签/计数(ADR 0017)。",
75
+ billing: "仅 Codex:按订阅 plan tier(plus/pro/…) 拆 token + 未分类桶(有 token 无 plan_type,≠确定API)。只从 rate_limits 的存在性+plan_type 标签派生 token 归类,不输出任何配额%/余额/重置时间(rate_limits 顶层仍恒 null)。confidence=spoofable-by-relay:plan_type 来自后端响应、可被中转透传或伪造,不能据此断言\"官方订阅\"(ADR 0022 D1)。",
76
+ codex_specific: "仅 Codex 的执行画像:effort/审批策略/沙箱/协作模式/personality/客户端身份(originators) 分布 + 压缩(compactions)/放弃回合(aborted_turns)/上下文窗口(context_window)/git 仓库身份(布尔)。全为派生计数/白名单枚举标签,collaboration_mode 仅取 mode 名、绝不读 developer_instructions 正文(ADR 0023 D1 / 0017)。",
77
+ claude_specific: "仅 Claude 的服务端工具计数:web 搜索/抓取请求数(usage.server_tool_use)。Claude 多数差异化信号已在 environment/skills/rework 覆盖,此处仅补 server_tool_use(ADR 0023 D2)。",
78
+ endpoints: "端点/计费模式检测(账户级当前快照,读本机 config):endpoint(official|custom|unknown)+relay_suspected+auth_mode+subscription_type+billing_mode(subscription|api_or_relay|unknown)+confidence。只派生布尔/host 白名单/枚举标签,绝不存 key/token/完整 base_url URL(custom 不回显中转域名)。与 billing 块(历史 token 拆分)正交:这是\"现在怎么计费/是否走中转\"。endpoint=custom 时 plan_type 标签可能被中转伪造,billing_mode 保守判 api_or_relay(ADR 0022 D2/D3/D4)。",
79
+ git_habits: "git 子命令频次与评审/风险信号(如只 diff/status 不 commit)。",
80
+ project_management: "各仓库是否有测试/构建/CI 信号。",
81
+ "tools.by_name": "各工具被调用次数(仅工具名计数,如 Bash/Edit/Glob/mcp__…;不含命令行/参数)。",
82
+ "tools.categories": "工具类别计数 shell/web/file/search/mcp/other(纯计数,无内容)。",
83
+ file_languages: "按读写/编辑文件的扩展名派生的语言文件数(仅扩展名映射语言,绝不含路径/文件内容)。",
84
+ "hours.count": "该时段的活跃事件数(与 tokens 并列;用于活跃度热力,不含内容)。",
85
+ scope: "分析层级:global(默认,跨项目聚合)/ project(额外给 projects[])/ session(额外给 sessions_detail[])。",
86
+ projects: "每项目跨会话的派生信号桶(tokens/tool_calls/cache_hit_rate/categories/git_top/prompt_signals);仅 --scope project。",
87
+ sessions_detail: "每会话的派生信号桶(含 session_id/duration_seconds 等);仅 --scope session;不含 prompt 原文。",
88
+ duration: "活跃时长(相邻事件间隔 ≤5 分钟才计入),非墙钟跨度。"
89
+ };
90
+
91
+ //#endregion
92
+ //#region src/pricing.ts
93
+ const priceTable = [
94
+ {
95
+ prefix: "gpt-5.5",
96
+ p: {
97
+ input: 5,
98
+ cachedInput: .5,
99
+ output: 30
100
+ }
101
+ },
102
+ {
103
+ prefix: "gpt-5.4-mini",
104
+ p: {
105
+ input: .75,
106
+ cachedInput: .075,
107
+ output: 4.5
108
+ }
109
+ },
110
+ {
111
+ prefix: "gpt-5.4-nano",
112
+ p: {
113
+ input: .2,
114
+ cachedInput: .02,
115
+ output: 1.25
116
+ }
117
+ },
118
+ {
119
+ prefix: "gpt-5.4",
120
+ p: {
121
+ input: 2.5,
122
+ cachedInput: .25,
123
+ output: 15
124
+ }
125
+ },
126
+ {
127
+ prefix: "gpt-5.3-codex",
128
+ p: {
129
+ input: 1.75,
130
+ cachedInput: .175,
131
+ output: 14
132
+ }
133
+ },
134
+ {
135
+ prefix: "gpt-5.2-codex",
136
+ p: {
137
+ input: 1.75,
138
+ cachedInput: .175,
139
+ output: 14
140
+ }
141
+ },
142
+ {
143
+ prefix: "gpt-5.2",
144
+ p: {
145
+ input: 1.75,
146
+ cachedInput: .175,
147
+ output: 14
148
+ }
149
+ },
150
+ {
151
+ prefix: "gpt-5.1-codex-mini",
152
+ p: {
153
+ input: .25,
154
+ cachedInput: .025,
155
+ output: 2
156
+ }
157
+ },
158
+ {
159
+ prefix: "gpt-5.1",
160
+ p: {
161
+ input: 1.25,
162
+ cachedInput: .125,
163
+ output: 10
164
+ }
165
+ },
166
+ {
167
+ prefix: "gpt-5-mini",
168
+ p: {
169
+ input: .25,
170
+ cachedInput: .025,
171
+ output: 2
172
+ }
173
+ },
174
+ {
175
+ prefix: "gpt-5-nano",
176
+ p: {
177
+ input: .05,
178
+ cachedInput: .005,
179
+ output: .4
180
+ }
181
+ },
182
+ {
183
+ prefix: "gpt-5",
184
+ p: {
185
+ input: 1.25,
186
+ cachedInput: .125,
187
+ output: 10
188
+ }
189
+ },
190
+ {
191
+ prefix: "codex-mini",
192
+ p: {
193
+ input: 1.5,
194
+ cachedInput: .375,
195
+ output: 6
196
+ }
197
+ },
198
+ {
199
+ prefix: "claude-opus",
200
+ p: {
201
+ input: 5,
202
+ cachedInput: .5,
203
+ output: 25,
204
+ cacheCreation: 6.25
205
+ }
206
+ },
207
+ {
208
+ prefix: "claude-sonnet",
209
+ p: {
210
+ input: 3,
211
+ cachedInput: .3,
212
+ output: 15,
213
+ cacheCreation: 3.75
214
+ }
215
+ },
216
+ {
217
+ prefix: "claude-haiku",
218
+ p: {
219
+ input: 1,
220
+ cachedInput: .1,
221
+ output: 5,
222
+ cacheCreation: 1.25
223
+ }
224
+ }
225
+ ];
226
+ function normalizeModel(model) {
227
+ let m = model.trim().toLowerCase();
228
+ if (m.startsWith("gpt5") && !m.startsWith("gpt-5")) m = "gpt-5" + m.slice(4);
229
+ if (m.startsWith("codex-mini")) return "codex-mini";
230
+ return m;
231
+ }
232
+ function disjointInputBuckets(model) {
233
+ return normalizeModel(model).startsWith("claude");
234
+ }
235
+ function lookupPrice(model) {
236
+ const m = normalizeModel(model);
237
+ let bestLen = -1;
238
+ let best;
239
+ for (const e of priceTable) if (m.startsWith(e.prefix) && e.prefix.length > bestLen) {
240
+ bestLen = e.prefix.length;
241
+ best = e.p;
242
+ }
243
+ return {
244
+ price: best ?? {
245
+ input: 0,
246
+ cachedInput: 0,
247
+ output: 0
248
+ },
249
+ found: bestLen >= 0
250
+ };
251
+ }
252
+ function estimateCost(d, model) {
253
+ const { price, found } = lookupPrice(model);
254
+ if (!found) return {
255
+ usd: 0,
256
+ priced: false
257
+ };
258
+ let usd;
259
+ if (price.cacheCreation !== void 0) usd = d.input * price.input / 1e6 + d.cached_input * price.cachedInput / 1e6 + d.cache_creation * price.cacheCreation / 1e6 + d.output * price.output / 1e6;
260
+ else {
261
+ const cached = Math.min(d.cached_input, d.input);
262
+ const nonCached = d.input - cached;
263
+ usd = nonCached * price.input / 1e6 + cached * price.cachedInput / 1e6 + d.output * price.output / 1e6;
264
+ }
265
+ return {
266
+ usd,
267
+ priced: true
268
+ };
269
+ }
270
+
271
+ //#endregion
272
+ //#region src/text.ts
273
+ const GIT_SUBCMDS = new Set([
274
+ "add",
275
+ "commit",
276
+ "push",
277
+ "pull",
278
+ "fetch",
279
+ "diff",
280
+ "status",
281
+ "log",
282
+ "checkout",
283
+ "branch",
284
+ "merge",
285
+ "rebase",
286
+ "stash",
287
+ "show",
288
+ "reset",
289
+ "clone",
290
+ "switch",
291
+ "restore",
292
+ "tag",
293
+ "cherry-pick",
294
+ "revert",
295
+ "rev-parse",
296
+ "remote",
297
+ "init",
298
+ "blame"
299
+ ]);
300
+ function firstToken(cmd) {
301
+ if (typeof cmd !== "string") return "";
302
+ cmd = cmd.trim();
303
+ for (const part of cmd.split(/\s+/).filter(Boolean)) {
304
+ if (part.includes("=") && !/^[-/.]/.test(part)) {
305
+ const name = part.split("=", 1)[0];
306
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue;
307
+ }
308
+ return part.split("/").pop();
309
+ }
310
+ return "";
311
+ }
312
+ function gitSubcommand(cmd) {
313
+ if (typeof cmd !== "string") return null;
314
+ const toks = cmd.trim().split(/\s+/).filter(Boolean);
315
+ let seenGit = false;
316
+ for (const t of toks) {
317
+ const base = t.split("/").pop();
318
+ if (!seenGit) {
319
+ if (base === "git") seenGit = true;
320
+ continue;
321
+ }
322
+ if (t.startsWith("-")) continue;
323
+ const sub = base.toLowerCase();
324
+ if (GIT_SUBCMDS.has(sub)) return sub;
325
+ return null;
326
+ }
327
+ return null;
328
+ }
329
+ function extOf(filePath) {
330
+ if (typeof filePath !== "string" || !filePath) return "";
331
+ const base = filePath.split("/").pop();
332
+ if (!base.includes(".")) return "";
333
+ return base.split(".").pop().toLowerCase();
334
+ }
335
+ function repoName(cwd) {
336
+ if (typeof cwd !== "string" || !cwd) return "(unknown)";
337
+ const name = cwd.replace(/\/+$/, "").split("/").pop();
338
+ return name || "(unknown)";
339
+ }
340
+ function comma(n) {
341
+ const neg = n < 0;
342
+ let s = String(Math.abs(Math.trunc(n)));
343
+ const parts = [];
344
+ while (s.length > 3) {
345
+ parts.unshift(s.slice(s.length - 3));
346
+ s = s.slice(0, s.length - 3);
347
+ }
348
+ parts.unshift(s);
349
+ const out = parts.join(",");
350
+ return neg ? "-" + out : out;
351
+ }
352
+
353
+ //#endregion
354
+ //#region src/habits.ts
355
+ function topCounts(counts, n) {
356
+ const cc = Object.entries(counts).map(([command, count]) => ({
357
+ command,
358
+ count
359
+ }));
360
+ cc.sort((a, b) => {
361
+ if (a.count !== b.count) return b.count - a.count;
362
+ return a.command < b.command ? -1 : a.command > b.command ? 1 : 0;
363
+ });
364
+ return cc.length > n ? cc.slice(0, n) : cc;
365
+ }
366
+ function buildGitHabits(gitCommands, branchCount, multiBranchRepos) {
367
+ const commandCount = Object.values(gitCommands).reduce((sum, n) => sum + n, 0);
368
+ const top = topCounts(gitCommands, 10);
369
+ const status = gitCommands.status ?? 0;
370
+ const diff = gitCommands.diff ?? 0;
371
+ const log = gitCommands.log ?? 0;
372
+ const show = gitCommands.show ?? 0;
373
+ const commit = gitCommands.commit ?? 0;
374
+ const push = gitCommands.push ?? 0;
375
+ const reviewSignals = [];
376
+ if (status > 0) reviewSignals.push(`经常检查工作区状态: git status ${status} 次`);
377
+ if (diff > 0) reviewSignals.push(`会查看差异: git diff ${diff} 次`);
378
+ if (log > 0 || show > 0) reviewSignals.push("会读取历史上下文");
379
+ const riskSignals = [];
380
+ if (commit === 0 && (diff > 0 || status > 0)) riskSignals.push("只看到 diff/status 等检查、没有 commit 提交;可能偏向让人类最后提交");
381
+ if (push > 0) riskSignals.push("观察到 push 命令;适合在 AGENTS.md 中写清推送前检查");
382
+ return {
383
+ command_count: commandCount,
384
+ top_subcommands: top.length > 0 ? top : void 0,
385
+ branch_count: branchCount,
386
+ multi_branch_repos: multiBranchRepos,
387
+ review_signals: reviewSignals.length > 0 ? reviewSignals : void 0,
388
+ risk_signals: riskSignals.length > 0 ? riskSignals : void 0
389
+ };
390
+ }
391
+ function buildProjectMgmt(repos) {
392
+ const reposWithTests = repos.filter((r) => r.hasTests === true).length;
393
+ const reposWithBuildSystem = repos.filter((r) => r.hasBuild === true).length;
394
+ const reposWithCI = repos.filter((r) => r.hasCI === true).length;
395
+ const signals = [];
396
+ if (repos.length > 0) {
397
+ if (reposWithTests === 0) signals.push("活跃项目中没有观察到测试命令");
398
+ else signals.push(`${reposWithTests}/${repos.length} 个活跃项目观察到测试命令`);
399
+ if (reposWithCI > 0) signals.push(`${reposWithCI} 个活跃项目检测到 GitHub Actions`);
400
+ }
401
+ return {
402
+ repos_with_tests: reposWithTests,
403
+ repos_with_build_system: reposWithBuildSystem,
404
+ repos_with_ci: reposWithCI,
405
+ signals: signals.length > 0 ? signals : void 0
406
+ };
407
+ }
408
+
409
+ //#endregion
410
+ //#region src/language.ts
411
+ const EXT_LANG = {
412
+ py: "Python",
413
+ go: "Go",
414
+ js: "JavaScript",
415
+ jsx: "JavaScript",
416
+ ts: "TypeScript",
417
+ tsx: "TypeScript",
418
+ rs: "Rust",
419
+ java: "Java",
420
+ kt: "Kotlin",
421
+ swift: "Swift",
422
+ c: "C",
423
+ h: "C/C++ Header",
424
+ cc: "C++",
425
+ cpp: "C++",
426
+ cxx: "C++",
427
+ hpp: "C++ Header",
428
+ rb: "Ruby",
429
+ php: "PHP",
430
+ cs: "C#",
431
+ sh: "Shell",
432
+ bash: "Shell",
433
+ zsh: "Shell",
434
+ fish: "Shell",
435
+ html: "HTML",
436
+ htm: "HTML",
437
+ css: "CSS",
438
+ scss: "CSS",
439
+ less: "CSS",
440
+ md: "Markdown",
441
+ json: "JSON",
442
+ yaml: "YAML",
443
+ yml: "YAML",
444
+ toml: "TOML",
445
+ xml: "XML",
446
+ sql: "SQL",
447
+ vue: "Vue",
448
+ svelte: "Svelte",
449
+ dart: "Dart",
450
+ scala: "Scala",
451
+ lua: "Lua",
452
+ r: "R",
453
+ ipynb: "Jupyter",
454
+ txt: "Text",
455
+ cfg: "Config",
456
+ ini: "Config",
457
+ env: "Config",
458
+ gradle: "Gradle",
459
+ proto: "Protobuf"
460
+ };
461
+ const AUX_LABELS = new Set([
462
+ "Markdown",
463
+ "Config",
464
+ "Text",
465
+ "JSON",
466
+ "YAML",
467
+ "TOML",
468
+ "XML"
469
+ ]);
470
+ function dominantLanguage(fileTypes) {
471
+ const entries = fileTypes instanceof Map ? [...fileTypes.entries()] : Object.entries(fileTypes);
472
+ let best;
473
+ for (const [extRaw, count] of entries) {
474
+ const ext = extRaw.toLowerCase();
475
+ const lang = EXT_LANG[ext];
476
+ if (!lang || AUX_LABELS.has(lang)) continue;
477
+ if (!best || count > best.count || count === best.count && ext < best.ext) best = {
478
+ lang,
479
+ count,
480
+ ext
481
+ };
482
+ }
483
+ return best?.lang;
484
+ }
485
+
486
+ //#endregion
487
+ //#region src/prompt-signals.ts
488
+ const CONSTRAINT_WORDS_EN = [
489
+ "must",
490
+ "should",
491
+ "don't",
492
+ "do not",
493
+ "only",
494
+ "never",
495
+ "ensure",
496
+ "require",
497
+ "avoid",
498
+ "without",
499
+ "acceptance"
500
+ ];
501
+ const CONSTRAINT_WORDS_ZH = [
502
+ "必须",
503
+ "应该",
504
+ "不要",
505
+ "不能",
506
+ "只",
507
+ "确保",
508
+ "需要",
509
+ "避免",
510
+ "禁止",
511
+ "验收",
512
+ "务必"
513
+ ];
514
+ const CORRECTION_STARTS_EN = [
515
+ "actually",
516
+ "instead",
517
+ "wait",
518
+ "no,",
519
+ "no ",
520
+ "not ",
521
+ "sorry",
522
+ "oops",
523
+ "rather"
524
+ ];
525
+ const CORRECTION_STARTS_ZH = [
526
+ "不对",
527
+ "重来",
528
+ "不是",
529
+ "错了",
530
+ "应该是",
531
+ "改成",
532
+ "其实",
533
+ "等等",
534
+ "算了"
535
+ ];
536
+ const LIST_RE = /(^|\n)\s*([-*•]|\d+[.)])\s+/;
537
+ const _FILE_EXT_GROUP = "(?:py|go|js|jsx|ts|tsx|rs|java|kt|swift|c|h|cc|cpp|cxx|hpp|rb|php|cs|sh|bash|zsh|html|htm|css|scss|less|md|json|yaml|yml|toml|xml|sql|vue|svelte|dart|scala|lua|ipynb|cfg|ini|env|gradle|proto|txt|lock|mod)";
538
+ const FILE_REF_RE = new RegExp("@[\\w\\-]*[./][\\w./\\-]+|(?:[\\w\\-]+/)+[\\w\\-.]*\\." + _FILE_EXT_GROUP + "\\b|\\b[\\w\\-]+\\." + _FILE_EXT_GROUP + "\\b", "i");
539
+ function newPromptAcc() {
540
+ return {
541
+ count: 0,
542
+ len_sum: 0,
543
+ structured: 0,
544
+ file_ref: 0,
545
+ constraint: 0,
546
+ correction: 0
547
+ };
548
+ }
549
+ function promptAccUpdate(acc, rawText) {
550
+ if (typeof rawText !== "string") return;
551
+ const text = rawText.trim();
552
+ if (!text) return;
553
+ acc.count += 1;
554
+ acc.len_sum += [...text].length;
555
+ const low = text.toLowerCase();
556
+ if (text.includes("```") || LIST_RE.test(text)) acc.structured += 1;
557
+ if (FILE_REF_RE.test(text)) acc.file_ref += 1;
558
+ if (CONSTRAINT_WORDS_EN.some((w) => low.includes(w)) || CONSTRAINT_WORDS_ZH.some((w) => text.includes(w))) acc.constraint += 1;
559
+ if (CORRECTION_STARTS_EN.some((w) => low.startsWith(w)) || CORRECTION_STARTS_ZH.some((w) => text.startsWith(w))) acc.correction += 1;
560
+ }
561
+ function promptFlags(text) {
562
+ const low = text.toLowerCase();
563
+ return {
564
+ len: [...text].length,
565
+ structured: text.includes("```") || LIST_RE.test(text),
566
+ file_ref: FILE_REF_RE.test(text),
567
+ constraint: CONSTRAINT_WORDS_EN.some((w) => low.includes(w)) || CONSTRAINT_WORDS_ZH.some((w) => text.includes(w)),
568
+ correction: CORRECTION_STARTS_EN.some((w) => low.startsWith(w)) || CORRECTION_STARTS_ZH.some((w) => text.startsWith(w))
569
+ };
570
+ }
571
+ function promptSignals(acc) {
572
+ const n = acc.count;
573
+ if (!n) return {
574
+ prompts: 0,
575
+ avg_len: 0,
576
+ structured_ratio: 0,
577
+ file_ref_ratio: 0,
578
+ constraint_ratio: 0,
579
+ correction_rate: 0
580
+ };
581
+ const r4 = (x) => Math.round(x * 1e4) / 1e4;
582
+ return {
583
+ prompts: n,
584
+ avg_len: Math.round(acc.len_sum / n * 10) / 10,
585
+ structured_ratio: r4(acc.structured / n),
586
+ file_ref_ratio: r4(acc.file_ref / n),
587
+ constraint_ratio: r4(acc.constraint / n),
588
+ correction_rate: r4(acc.correction / n)
589
+ };
590
+ }
591
+
592
+ //#endregion
593
+ //#region src/aggregate.ts
594
+ const IDLE_CAP_MS = 5 * 60 * 1e3;
595
+ const REPOS_MAX = 50;
596
+ const USAGE_MAX = 15;
597
+ const MT_MODELS_MAX = 10;
598
+ const MT_DAYS_MAX = 31;
599
+ var Aggregator = class {
600
+ platform;
601
+ tokens = emptyTokens();
602
+ cost = 0;
603
+ freshInput = 0;
604
+ shellCalls = 0;
605
+ webSearches = 0;
606
+ fileChanges = 0;
607
+ totalCalls = 0;
608
+ shellCommands = new Map();
609
+ gitCommands = new Map();
610
+ sessionIds = new Set();
611
+ durationMs = 0;
612
+ prevActive = null;
613
+ byRepo = new Map();
614
+ byHour = new Array(24).fill(0);
615
+ byHourCount = new Array(24).fill(0);
616
+ categories = new Map();
617
+ toolByName = new Map();
618
+ langFiles = new Map();
619
+ bySource = new Map();
620
+ byLanguage = new Map();
621
+ byModelDay = new Map();
622
+ modelsSeen = new Set();
623
+ missingPrice = new Set();
624
+ promptAcc = newPromptAcc();
625
+ errToolResults = 0;
626
+ errToolErrors = 0;
627
+ errInterrupted = 0;
628
+ errApiErrors = 0;
629
+ errByTool = new Map();
630
+ errByCategory = new Map();
631
+ rwEdits = 0;
632
+ rwUserModified = 0;
633
+ rwLinesAdded = 0;
634
+ rwLinesRemoved = 0;
635
+ skillCounts = new Map();
636
+ versions = new Set();
637
+ permModes = new Map();
638
+ attachments = 0;
639
+ subagentMsgs = 0;
640
+ billingByTier = new Map();
641
+ billingUnclassified = 0;
642
+ billingSessionsWithPlan = 0;
643
+ billingSessionsUnclassified = 0;
644
+ cxLabels = new Map();
645
+ cxCompactions = 0;
646
+ cxAbortedTurns = 0;
647
+ cxContextWindow = new Map();
648
+ cxGitIdentity = false;
649
+ cxNonDefaultProvider = false;
650
+ clWebSearchReq = 0;
651
+ clWebFetchReq = 0;
652
+ scope;
653
+ groups = new Map();
654
+ curBucket = null;
655
+ constructor(platform, scope = "global") {
656
+ this.platform = platform;
657
+ this.scope = scope;
658
+ }
659
+ beginRecord(repo, session, ts) {
660
+ this.curBucket = null;
661
+ if (this.scope === "global") return;
662
+ let key;
663
+ if (this.scope === "session") {
664
+ if (!session) return;
665
+ key = session;
666
+ } else {
667
+ const r = (repo ?? "").trim();
668
+ if (!r || r === "(unknown)") return;
669
+ key = r;
670
+ }
671
+ let g = this.groups.get(key);
672
+ if (!g) {
673
+ g = {
674
+ key,
675
+ repo: this.scope === "session" ? "(unknown)" : key,
676
+ sessions: new Set(),
677
+ tokens: 0,
678
+ cacheRead: 0,
679
+ input: 0,
680
+ toolCalls: 0,
681
+ cat: new Map(),
682
+ git: new Map(),
683
+ prompt: newPromptAcc(),
684
+ first: null,
685
+ last: null
686
+ };
687
+ this.groups.set(key, g);
688
+ }
689
+ if (this.scope === "session" && repo && repo !== "(unknown)" && g.repo === "(unknown)") g.repo = repo;
690
+ if (this.scope === "project" && session) g.sessions.add(session);
691
+ if (ts && !Number.isNaN(ts.getTime())) {
692
+ const t = ts.getTime();
693
+ if (g.first === null || t < g.first) g.first = t;
694
+ if (g.last === null || t > g.last) g.last = t;
695
+ }
696
+ this.curBucket = g;
697
+ }
698
+ repoFor(repo) {
699
+ let name = (repo ?? "").trim();
700
+ if (!name) name = "(unknown)";
701
+ let r = this.byRepo.get(name);
702
+ if (!r) {
703
+ r = {
704
+ repo: name,
705
+ sessions: new Set(),
706
+ tokens: 0,
707
+ cost: 0,
708
+ branches: new Set(),
709
+ fileTypes: new Map(),
710
+ hasTests: false,
711
+ hasBuild: false,
712
+ hasCI: false
713
+ };
714
+ this.byRepo.set(name, r);
715
+ }
716
+ return r;
717
+ }
718
+ applyTokens(d, model, repo, session, ts, branch) {
719
+ this.tokens.input += d.input;
720
+ this.tokens.cached_input += d.cached_input;
721
+ this.tokens.output += d.output;
722
+ this.tokens.reasoning_output += d.reasoning_output;
723
+ this.tokens.cache_creation += d.cache_creation;
724
+ this.tokens.total += d.total;
725
+ const { usd, priced } = estimateCost(d, model);
726
+ this.cost += usd;
727
+ const nm = normalizeModel(model);
728
+ if (nm) {
729
+ this.modelsSeen.add(nm);
730
+ if (!priced && model.trim() !== "") this.missingPrice.add(nm);
731
+ }
732
+ this.freshInput += disjointInputBuckets(model) ? d.input : Math.max(0, d.input - Math.min(d.cached_input, d.input));
733
+ const r = this.repoFor(repo);
734
+ r.tokens += d.total;
735
+ r.cost += usd;
736
+ if (session) r.sessions.add(session);
737
+ if (branch) r.branches.add(branch);
738
+ if (nm) this.applyModelDay(nm, ts, d, usd);
739
+ this.byHour[ts.getHours()] += d.total;
740
+ this.byHourCount[ts.getHours()]++;
741
+ if (this.curBucket) {
742
+ this.curBucket.tokens += d.total;
743
+ this.curBucket.cacheRead += d.cached_input;
744
+ this.curBucket.input += d.input;
745
+ }
746
+ }
747
+ applyModelDay(model, ts, d, cost) {
748
+ const day = localYmd(ts);
749
+ let days = this.byModelDay.get(model);
750
+ if (!days) {
751
+ days = new Map();
752
+ this.byModelDay.set(model, days);
753
+ }
754
+ let md = days.get(day);
755
+ if (!md) {
756
+ md = {
757
+ tokens: 0,
758
+ cost: 0,
759
+ input: 0,
760
+ cached_input: 0,
761
+ output: 0,
762
+ reasoning_output: 0,
763
+ cache_creation: 0
764
+ };
765
+ days.set(day, md);
766
+ }
767
+ md.tokens += d.total;
768
+ md.cost += cost;
769
+ md.input += d.input;
770
+ md.cached_input += d.cached_input;
771
+ md.output += d.output;
772
+ md.reasoning_output += d.reasoning_output;
773
+ md.cache_creation += d.cache_creation;
774
+ }
775
+ applyTool(kind, command) {
776
+ this.totalCalls++;
777
+ this.categories.set(kind, (this.categories.get(kind) ?? 0) + 1);
778
+ if (kind === "shell") {
779
+ this.shellCalls++;
780
+ if (command) {
781
+ const ft = firstToken(command);
782
+ if (ft) this.shellCommands.set(ft, (this.shellCommands.get(ft) ?? 0) + 1);
783
+ const gs = gitSubcommand(command);
784
+ if (gs) this.gitCommands.set(gs, (this.gitCommands.get(gs) ?? 0) + 1);
785
+ }
786
+ } else if (kind === "web") this.webSearches++;
787
+ else if (kind === "file") this.fileChanges++;
788
+ if (this.curBucket) {
789
+ this.curBucket.toolCalls++;
790
+ this.curBucket.cat.set(kind, (this.curBucket.cat.get(kind) ?? 0) + 1);
791
+ if (kind === "shell" && command) {
792
+ const gs = gitSubcommand(command);
793
+ if (gs) this.curBucket.git.set(gs, (this.curBucket.git.get(gs) ?? 0) + 1);
794
+ }
795
+ }
796
+ }
797
+ applyToolName(name) {
798
+ if (name) this.toolByName.set(name, (this.toolByName.get(name) ?? 0) + 1);
799
+ }
800
+ applyLanguageFile(ext) {
801
+ if (!ext) return;
802
+ const lang = EXT_LANG[ext.toLowerCase()] ?? ext.toUpperCase();
803
+ this.langFiles.set(lang, (this.langFiles.get(lang) ?? 0) + 1);
804
+ }
805
+ applyFileChangeExt(repo, ext) {
806
+ if (!ext) return;
807
+ const r = this.repoFor(repo);
808
+ r.fileTypes.set(ext, (r.fileTypes.get(ext) ?? 0) + 1);
809
+ }
810
+ applyUsage(groups, name, tokens, session) {
811
+ let n = (name ?? "").trim();
812
+ if (!n) n = "(unknown)";
813
+ let g = groups.get(n);
814
+ if (!g) {
815
+ g = {
816
+ name: n,
817
+ tokens: 0,
818
+ sessions: new Set()
819
+ };
820
+ groups.set(n, g);
821
+ }
822
+ g.tokens += tokens.total;
823
+ if (session) g.sessions.add(session);
824
+ }
825
+ applyUsageSource(name, tokens, session) {
826
+ this.applyUsage(this.bySource, name, tokens, session);
827
+ }
828
+ applyLanguage(name, tokens, session) {
829
+ this.applyUsage(this.byLanguage, name, tokens, session);
830
+ }
831
+ applyRepoFacts(repo, facts) {
832
+ const r = this.repoFor(repo);
833
+ if (facts.hasTests) r.hasTests = true;
834
+ if (facts.hasBuild) r.hasBuild = true;
835
+ if (facts.hasCI) r.hasCI = true;
836
+ }
837
+ applyPrompt(text) {
838
+ promptAccUpdate(this.promptAcc, text);
839
+ if (this.curBucket) promptAccUpdate(this.curBucket.prompt, text);
840
+ }
841
+ applyToolResult(toolName, isError, category) {
842
+ this.errToolResults++;
843
+ if (isError) {
844
+ this.errToolErrors++;
845
+ const tn = toolName || "(unknown)";
846
+ this.errByTool.set(tn, (this.errByTool.get(tn) ?? 0) + 1);
847
+ if (category) this.errByCategory.set(category, (this.errByCategory.get(category) ?? 0) + 1);
848
+ }
849
+ }
850
+ markInterrupted() {
851
+ this.errInterrupted++;
852
+ }
853
+ applyApiError() {
854
+ this.errApiErrors++;
855
+ }
856
+ applyEdit(linesAdded, linesRemoved, userModified) {
857
+ this.rwEdits++;
858
+ if (userModified) this.rwUserModified++;
859
+ this.rwLinesAdded += linesAdded;
860
+ this.rwLinesRemoved += linesRemoved;
861
+ }
862
+ applySkill(name) {
863
+ if (name) this.skillCounts.set(name, (this.skillCounts.get(name) ?? 0) + 1);
864
+ }
865
+ applyVersion(v) {
866
+ if (v) this.versions.add(v);
867
+ }
868
+ applyPermissionMode(m) {
869
+ if (m) this.permModes.set(m, (this.permModes.get(m) ?? 0) + 1);
870
+ }
871
+ markAttachment() {
872
+ this.attachments++;
873
+ }
874
+ markSubagentMessage() {
875
+ this.subagentMsgs++;
876
+ }
877
+ applyBillingRollout(planType, tokens) {
878
+ if (tokens <= 0) return;
879
+ if (planType) {
880
+ this.billingByTier.set(planType, (this.billingByTier.get(planType) ?? 0) + tokens);
881
+ this.billingSessionsWithPlan++;
882
+ } else {
883
+ this.billingUnclassified += tokens;
884
+ this.billingSessionsUnclassified++;
885
+ }
886
+ }
887
+ applyCodexLabel(field, label) {
888
+ if (!field || !label) return;
889
+ let m = this.cxLabels.get(field);
890
+ if (!m) {
891
+ m = new Map();
892
+ this.cxLabels.set(field, m);
893
+ }
894
+ m.set(label, (m.get(label) ?? 0) + 1);
895
+ }
896
+ markCodexCompaction() {
897
+ this.cxCompactions++;
898
+ }
899
+ markCodexAbortedTurn() {
900
+ this.cxAbortedTurns++;
901
+ }
902
+ applyCodexContextWindow(n) {
903
+ if (n > 0) this.cxContextWindow.set(n, (this.cxContextWindow.get(n) ?? 0) + 1);
904
+ }
905
+ markCodexGitIdentity() {
906
+ this.cxGitIdentity = true;
907
+ }
908
+ markCodexNonDefaultProvider() {
909
+ this.cxNonDefaultProvider = true;
910
+ }
911
+ getCodexNonDefaultProvider() {
912
+ return this.cxNonDefaultProvider;
913
+ }
914
+ applyClaudeServerTool(webSearch, webFetch) {
915
+ this.clWebSearchReq += webSearch;
916
+ this.clWebFetchReq += webFetch;
917
+ }
918
+ touchSession(id) {
919
+ if (id) this.sessionIds.add(id);
920
+ }
921
+ markActive(ts) {
922
+ const t = ts.getTime();
923
+ if (this.prevActive !== null) {
924
+ const gap = t - this.prevActive;
925
+ if (gap > 0 && gap <= IDLE_CAP_MS) this.durationMs += gap;
926
+ }
927
+ this.prevActive = t;
928
+ }
929
+ resetActive() {
930
+ this.prevActive = null;
931
+ }
932
+ durationSeconds() {
933
+ return Math.floor(this.durationMs / 1e3);
934
+ }
935
+ assemble(window, source) {
936
+ const cached = this.tokens.cached_input;
937
+ const denom = cached + this.freshInput;
938
+ const cacheHitRate = denom > 0 ? cached / denom : 0;
939
+ const reasoningRatio = this.tokens.output > 0 ? this.tokens.reasoning_output / this.tokens.output : 0;
940
+ const repos = [];
941
+ const repoFacts = [];
942
+ const branchSet = new Set();
943
+ let multiBranchRepos = 0;
944
+ for (const r of this.byRepo.values()) {
945
+ const branches = [...r.branches].filter(Boolean).sort();
946
+ const rep = {
947
+ repo: r.repo,
948
+ sessions: r.sessions.size,
949
+ tokens: r.tokens,
950
+ estimated_cost_usd: r.cost
951
+ };
952
+ if (branches.length) rep.branches = branches;
953
+ const lang = dominantLanguage(r.fileTypes);
954
+ if (lang) rep.language = lang;
955
+ repos.push(rep);
956
+ repoFacts.push({
957
+ hasTests: r.hasTests,
958
+ hasBuild: r.hasBuild,
959
+ hasCI: r.hasCI
960
+ });
961
+ for (const b of r.branches) branchSet.add(r.repo + "@" + b);
962
+ if (r.branches.size > 1) multiBranchRepos++;
963
+ }
964
+ repos.sort((a, b) => b.tokens - a.tokens);
965
+ const hours = [];
966
+ for (let h = 0; h < 24; h++) if (this.byHour[h] > 0) hours.push({
967
+ hour: h,
968
+ tokens: this.byHour[h],
969
+ count: this.byHourCount[h]
970
+ });
971
+ const shellRecord = {};
972
+ for (const [k, v] of this.shellCommands) shellRecord[k] = v;
973
+ const gitRecord = {};
974
+ for (const [k, v] of this.gitCommands) gitRecord[k] = v;
975
+ const errToolRec = {};
976
+ for (const [k, v] of this.errByTool) errToolRec[k] = v;
977
+ const errCatRec = {};
978
+ for (const [k, v] of this.errByCategory) errCatRec[k] = v;
979
+ const errorSignals = {
980
+ tool_calls: this.errToolResults,
981
+ tool_errors: this.errToolErrors,
982
+ error_rate: this.errToolResults > 0 ? Math.round(this.errToolErrors / this.errToolResults * 1e4) / 1e4 : 0,
983
+ interrupted: this.errInterrupted,
984
+ api_errors: this.errApiErrors
985
+ };
986
+ if (this.errByTool.size) errorSignals.by_tool = topCounts(errToolRec, 8);
987
+ if (this.errByCategory.size) errorSignals.by_category = topCounts(errCatRec, 8);
988
+ const reworkSignals = {
989
+ edits: this.rwEdits,
990
+ user_modified: this.rwUserModified,
991
+ user_modified_rate: this.rwEdits > 0 ? Math.round(this.rwUserModified / this.rwEdits * 1e4) / 1e4 : 0,
992
+ lines_added: this.rwLinesAdded,
993
+ lines_removed: this.rwLinesRemoved
994
+ };
995
+ const skillRec = {};
996
+ for (const [k, v] of this.skillCounts) skillRec[k] = v;
997
+ const permRec = {};
998
+ for (const [k, v] of this.permModes) permRec[k] = v;
999
+ const report = {
1000
+ generated_for: window.desc,
1001
+ timezone: localTimezone(),
1002
+ platform: this.platform,
1003
+ source,
1004
+ sessions: this.sessionIds.size,
1005
+ duration_seconds: this.durationSeconds(),
1006
+ duration: humanizeDuration(this.durationMs),
1007
+ tokens: { ...this.tokens },
1008
+ cache_hit_rate: cacheHitRate,
1009
+ reasoning_ratio: reasoningRatio,
1010
+ estimated_cost_usd: this.cost,
1011
+ models: [...this.modelsSeen].sort(),
1012
+ tools: {
1013
+ shell_calls: this.shellCalls,
1014
+ web_searches: this.webSearches,
1015
+ file_changes: this.fileChanges,
1016
+ total_calls: this.totalCalls,
1017
+ top_commands: topCounts(shellRecord, 12)
1018
+ },
1019
+ repos: repos.slice(0, REPOS_MAX),
1020
+ hours,
1021
+ sources: usageReports(this.bySource).slice(0, USAGE_MAX),
1022
+ languages: usageReports(this.byLanguage).slice(0, USAGE_MAX),
1023
+ git_habits: buildGitHabits(gitRecord, branchSet.size, multiBranchRepos),
1024
+ project_management: buildProjectMgmt(repoFacts),
1025
+ prompt_signals: promptSignals(this.promptAcc),
1026
+ error_signals: errorSignals,
1027
+ rework_signals: reworkSignals,
1028
+ rate_limits: null,
1029
+ glossary: REPORT_GLOSSARY
1030
+ };
1031
+ if (this.toolByName.size) {
1032
+ const byNameRec = {};
1033
+ for (const [k, v] of this.toolByName) byNameRec[k] = v;
1034
+ report.tools.by_name = topCounts(byNameRec, 15).map((c) => ({
1035
+ name: c.command,
1036
+ count: c.count
1037
+ }));
1038
+ }
1039
+ if (this.categories.size) {
1040
+ const cats = {};
1041
+ for (const [k, v] of this.categories) cats[k] = v;
1042
+ report.tools.categories = cats;
1043
+ }
1044
+ if (this.langFiles.size) report.file_languages = [...this.langFiles.entries()].sort((a, b) => b[1] !== a[1] ? b[1] - a[1] : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).slice(0, USAGE_MAX).map(([name, files]) => ({
1045
+ name,
1046
+ files
1047
+ }));
1048
+ if (this.skillCounts.size) report.skills = topCounts(skillRec, 12);
1049
+ if (this.attachments || this.subagentMsgs || this.versions.size || this.permModes.size) {
1050
+ const env = {
1051
+ attachments: this.attachments,
1052
+ subagent_messages: this.subagentMsgs
1053
+ };
1054
+ if (this.versions.size) env.claude_versions = [...this.versions].sort();
1055
+ if (this.permModes.size) env.permission_modes = topCounts(permRec, 8);
1056
+ report.environment = env;
1057
+ }
1058
+ if (this.billingByTier.size || this.billingUnclassified || this.billingSessionsWithPlan || this.billingSessionsUnclassified) {
1059
+ const byTier = {};
1060
+ for (const [k, v] of this.billingByTier) byTier[k] = v;
1061
+ const billing = {
1062
+ by_plan_tier: byTier,
1063
+ unclassified: this.billingUnclassified,
1064
+ sessions_with_plan: this.billingSessionsWithPlan,
1065
+ sessions_unclassified: this.billingSessionsUnclassified,
1066
+ confidence: "spoofable-by-relay"
1067
+ };
1068
+ report.billing = billing;
1069
+ }
1070
+ if (this.cxLabels.size || this.cxCompactions || this.cxAbortedTurns || this.cxContextWindow.size || this.cxGitIdentity) {
1071
+ const cs = {
1072
+ compactions: this.cxCompactions,
1073
+ aborted_turns: this.cxAbortedTurns,
1074
+ git_repo_identity: this.cxGitIdentity
1075
+ };
1076
+ const asRec = (m) => {
1077
+ if (!m || !m.size) return void 0;
1078
+ const r = {};
1079
+ for (const [k, v] of m) r[k] = v;
1080
+ return r;
1081
+ };
1082
+ cs.effort = asRec(this.cxLabels.get("effort"));
1083
+ cs.approval_policy = asRec(this.cxLabels.get("approval_policy"));
1084
+ cs.sandbox = asRec(this.cxLabels.get("sandbox"));
1085
+ cs.collaboration_mode = asRec(this.cxLabels.get("collaboration_mode"));
1086
+ cs.personality = asRec(this.cxLabels.get("personality"));
1087
+ cs.originators = asRec(this.cxLabels.get("originators"));
1088
+ if (this.cxContextWindow.size) {
1089
+ let best = 0;
1090
+ let bestC = -1;
1091
+ for (const [n, c] of this.cxContextWindow) if (c > bestC) {
1092
+ bestC = c;
1093
+ best = n;
1094
+ }
1095
+ cs.context_window = best;
1096
+ }
1097
+ report.codex_specific = cs;
1098
+ }
1099
+ if (this.clWebSearchReq || this.clWebFetchReq) {
1100
+ const cl = {
1101
+ web_search_requests: this.clWebSearchReq,
1102
+ web_fetch_requests: this.clWebFetchReq
1103
+ };
1104
+ report.claude_specific = cl;
1105
+ }
1106
+ if (this.scope !== "global") {
1107
+ report.scope = this.scope;
1108
+ const toBucket = (g) => {
1109
+ const denom$1 = g.cacheRead + g.input;
1110
+ const cat = {};
1111
+ for (const [k, v] of g.cat) cat[k] = v;
1112
+ const gitRec = {};
1113
+ for (const [k, v] of g.git) gitRec[k] = v;
1114
+ return {
1115
+ tokens: g.tokens,
1116
+ tool_calls: g.toolCalls,
1117
+ cache_hit_rate: denom$1 > 0 ? Math.round(g.cacheRead / denom$1 * 1e4) / 1e4 : 0,
1118
+ categories: cat,
1119
+ git_top: topCounts(gitRec, 6),
1120
+ prompt_signals: promptSignals(g.prompt)
1121
+ };
1122
+ };
1123
+ if (this.scope === "project") report.projects = [...this.groups.values()].map((g) => ({
1124
+ repo: g.key,
1125
+ sessions: g.sessions.size,
1126
+ ...toBucket(g)
1127
+ })).sort((a, b) => b.tokens - a.tokens);
1128
+ else report.sessions_detail = [...this.groups.values()].map((g) => ({
1129
+ session_id: g.key,
1130
+ repo: g.repo,
1131
+ duration_seconds: g.first !== null && g.last !== null ? Math.floor((g.last - g.first) / 1e3) : 0,
1132
+ ...toBucket(g)
1133
+ })).sort((a, b) => b.tokens - a.tokens);
1134
+ } else report.scope = "global";
1135
+ if (this.missingPrice.size) report.unpriced_models = [...this.missingPrice].sort();
1136
+ if (this.byModelDay.size) {
1137
+ report.models_timeline = buildModelsTimeline(this.byModelDay);
1138
+ report.model_tokens = buildModelTokens(this.byModelDay, this.missingPrice);
1139
+ }
1140
+ return report;
1141
+ }
1142
+ };
1143
+ function usageReports(groups) {
1144
+ const out = [...groups.values()].map((g) => ({
1145
+ name: g.name,
1146
+ sessions: g.sessions.size,
1147
+ tokens: g.tokens
1148
+ }));
1149
+ out.sort((a, b) => b.tokens !== a.tokens ? b.tokens - a.tokens : a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1150
+ return out;
1151
+ }
1152
+ function buildModelsTimeline(byModelDay) {
1153
+ const out = [];
1154
+ for (const [model, days] of byModelDay) {
1155
+ const dayKeys = [...days.keys()].sort();
1156
+ let tokens = 0;
1157
+ let cost = 0;
1158
+ const dayCounts = [];
1159
+ for (const d of dayKeys) {
1160
+ const da = days.get(d);
1161
+ tokens += da.tokens;
1162
+ cost += da.cost;
1163
+ dayCounts.push({
1164
+ date: d,
1165
+ tokens: da.tokens
1166
+ });
1167
+ }
1168
+ out.push({
1169
+ model,
1170
+ first_day: dayKeys[0],
1171
+ last_day: dayKeys[dayKeys.length - 1],
1172
+ tokens,
1173
+ estimated_cost_usd: cost,
1174
+ days: dayCounts.slice(-MT_DAYS_MAX)
1175
+ });
1176
+ }
1177
+ out.sort((a, b) => b.tokens !== a.tokens ? b.tokens - a.tokens : a.model < b.model ? -1 : a.model > b.model ? 1 : 0);
1178
+ return out.slice(0, MT_MODELS_MAX);
1179
+ }
1180
+ function buildModelTokens(byModelDay, missingPrice) {
1181
+ const out = [];
1182
+ for (const [model, days] of byModelDay) {
1183
+ const t = emptyTokens();
1184
+ let cost = 0;
1185
+ for (const md of days.values()) {
1186
+ t.input += md.input;
1187
+ t.cached_input += md.cached_input;
1188
+ t.output += md.output;
1189
+ t.reasoning_output += md.reasoning_output;
1190
+ t.cache_creation += md.cache_creation;
1191
+ t.total += md.tokens;
1192
+ cost += md.cost;
1193
+ }
1194
+ out.push({
1195
+ model,
1196
+ tokens: t,
1197
+ estimated_cost_usd: cost,
1198
+ priced: !missingPrice.has(model)
1199
+ });
1200
+ }
1201
+ out.sort((a, b) => b.tokens.total !== a.tokens.total ? b.tokens.total - a.tokens.total : a.model < b.model ? -1 : a.model > b.model ? 1 : 0);
1202
+ return out.slice(0, MT_MODELS_MAX);
1203
+ }
1204
+ function humanizeDuration(ms) {
1205
+ if (ms <= 0) return "0m";
1206
+ const totalMin = Math.floor(ms / 6e4);
1207
+ const h = Math.floor(totalMin / 60);
1208
+ const m = totalMin % 60;
1209
+ return h > 0 ? `${h}h${m}m` : `${m}m`;
1210
+ }
1211
+ function localTimezone() {
1212
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
1213
+ const offsetMin = -new Date().getTimezoneOffset();
1214
+ const sign = offsetMin >= 0 ? "+" : "-";
1215
+ const abs = Math.abs(offsetMin);
1216
+ const hours = Math.trunc(abs / 60);
1217
+ const mins = abs % 60;
1218
+ const off = mins ? `${hours}:${String(mins).padStart(2, "0")}` : `${hours}`;
1219
+ return `${tz} (UTC${sign}${off})`;
1220
+ }
1221
+
1222
+ //#endregion
1223
+ //#region src/errors.ts
1224
+ function classifyError(errorText) {
1225
+ const t = typeof errorText === "string" ? errorText : "";
1226
+ if (/not been read|read it first before/i.test(t)) return "not-read";
1227
+ if (/permission denied|EACCES|not permitted|operation not permitted/i.test(t)) return "permission";
1228
+ if (/ETIMEDOUT|timed out|deadline exceeded/i.test(t)) return "timeout";
1229
+ if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|getaddrinfo|fetch failed|network is unreachable/i.test(t)) return "network";
1230
+ if (/\bFAIL\b|tests? failed|assertionerror|✗ /i.test(t)) return "test";
1231
+ if (/fatal:|not a git repository|exit code 128/i.test(t)) return "git";
1232
+ if (/npm (err|error)|cannot find module|\btsc\b|build failed|cargo |compil(e|ation)|\bgo: /i.test(t)) return "build";
1233
+ return "other";
1234
+ }
1235
+
1236
+ //#endregion
1237
+ //#region src/parsers/claude-code.ts
1238
+ function claudeProjectsDir() {
1239
+ const cfg = process.env.CLAUDE_CONFIG_DIR;
1240
+ if (cfg && cfg.trim()) return join(cfg.trim(), "projects");
1241
+ return join(homedir(), ".claude", "projects");
1242
+ }
1243
+ function walkJsonl(dir) {
1244
+ const out = [];
1245
+ let entries;
1246
+ try {
1247
+ entries = readdirSync(dir, { withFileTypes: true });
1248
+ } catch {
1249
+ return out;
1250
+ }
1251
+ for (const e of entries) {
1252
+ const p = join(dir, e.name);
1253
+ if (e.isDirectory()) out.push(...walkJsonl(p));
1254
+ else if (e.isFile() && e.name.endsWith(".jsonl")) out.push(p);
1255
+ }
1256
+ return out.sort();
1257
+ }
1258
+ function num$1(x) {
1259
+ return typeof x === "number" && isFinite(x) ? x : 0;
1260
+ }
1261
+ function userText(message) {
1262
+ const content = message?.content;
1263
+ if (typeof content === "string") return content;
1264
+ if (!Array.isArray(content)) return "";
1265
+ const parts = [];
1266
+ for (const c of content) if (typeof c === "string") parts.push(c);
1267
+ else if (c && c.type === "text" && typeof c.text === "string") parts.push(c.text);
1268
+ return parts.join("\n");
1269
+ }
1270
+ function feedClaudeCode(agg, dir, window) {
1271
+ const seen = new Set();
1272
+ const maxUsageByKey = new Map();
1273
+ const files = walkJsonl(dir);
1274
+ const toolUseNames = new Map();
1275
+ const sessionEntrypoint = new Map();
1276
+ for (const file of files) {
1277
+ let pre;
1278
+ try {
1279
+ pre = readFileSync(file, "utf8");
1280
+ } catch {
1281
+ continue;
1282
+ }
1283
+ for (const line of pre.split("\n")) {
1284
+ const t = line.trim();
1285
+ if (!t) continue;
1286
+ let rec;
1287
+ try {
1288
+ rec = JSON.parse(t);
1289
+ } catch {
1290
+ continue;
1291
+ }
1292
+ const psid = typeof rec?.sessionId === "string" ? rec.sessionId : "";
1293
+ const pep = typeof rec?.entrypoint === "string" ? rec.entrypoint.trim() : "";
1294
+ if (psid && pep && !sessionEntrypoint.has(psid)) sessionEntrypoint.set(psid, pep);
1295
+ if (rec?.type !== "assistant") continue;
1296
+ const blocks = Array.isArray(rec.message?.content) ? rec.message.content : [];
1297
+ for (const b of blocks) if (b && b.type === "tool_use" && typeof b.id === "string") toolUseNames.set(b.id, b.name);
1298
+ const pmsgId = rec?.message?.id;
1299
+ const pKey = typeof pmsgId === "string" && pmsgId !== "" ? `${pmsgId}:${rec?.requestId ?? ""}` : typeof rec?.uuid === "string" && rec.uuid !== "" ? rec.uuid : null;
1300
+ if (pKey !== null) {
1301
+ const u = rec.message?.usage ?? {};
1302
+ const input = num$1(u.input_tokens);
1303
+ const cachedInput = num$1(u.cache_read_input_tokens);
1304
+ const output = num$1(u.output_tokens);
1305
+ const cacheCreation = num$1(u.cache_creation_input_tokens);
1306
+ const total = input + output + cachedInput + cacheCreation;
1307
+ const prev = maxUsageByKey.get(pKey);
1308
+ if (!prev || total > prev.total) maxUsageByKey.set(pKey, {
1309
+ input,
1310
+ cached_input: cachedInput,
1311
+ output,
1312
+ reasoning_output: 0,
1313
+ cache_creation: cacheCreation,
1314
+ total
1315
+ });
1316
+ }
1317
+ }
1318
+ }
1319
+ for (const file of files) {
1320
+ agg.resetActive();
1321
+ let content;
1322
+ try {
1323
+ content = readFileSync(file, "utf8");
1324
+ } catch {
1325
+ continue;
1326
+ }
1327
+ for (const line of content.split("\n")) {
1328
+ const trimmed = line.trim();
1329
+ if (!trimmed) continue;
1330
+ let rec;
1331
+ try {
1332
+ rec = JSON.parse(trimmed);
1333
+ } catch {
1334
+ continue;
1335
+ }
1336
+ const tsRaw = rec?.timestamp;
1337
+ if (typeof tsRaw !== "string") continue;
1338
+ const ts = new Date(tsRaw);
1339
+ if (Number.isNaN(ts.getTime())) continue;
1340
+ if (!inLocalRange(ts, window)) continue;
1341
+ const msgId = rec?.message?.id;
1342
+ const dedupKey = typeof msgId === "string" && msgId !== "" ? `${msgId}:${rec?.requestId ?? ""}` : typeof rec?.uuid === "string" && rec.uuid !== "" ? rec.uuid : null;
1343
+ if (dedupKey !== null) {
1344
+ if (seen.has(dedupKey)) continue;
1345
+ seen.add(dedupKey);
1346
+ }
1347
+ const sidechain = rec?.isSidechain === true;
1348
+ const session = typeof rec.sessionId === "string" ? rec.sessionId : "";
1349
+ const repo = repoName(typeof rec.cwd === "string" ? rec.cwd : "");
1350
+ const branch = typeof rec.gitBranch === "string" ? rec.gitBranch : void 0;
1351
+ if (rec.isApiErrorMessage === true && !sidechain) agg.applyApiError();
1352
+ if (typeof rec.version === "string") agg.applyVersion(rec.version);
1353
+ if (typeof rec.permissionMode === "string") agg.applyPermissionMode(rec.permissionMode);
1354
+ if (typeof rec.attributionSkill === "string") agg.applySkill(rec.attributionSkill);
1355
+ if (rec.type === "attachment") agg.markAttachment();
1356
+ if (sidechain) agg.markSubagentMessage();
1357
+ agg.beginRecord(repo, sidechain ? "" : session, ts);
1358
+ if (rec.type === "user") {
1359
+ if (!sidechain) {
1360
+ agg.touchSession(session);
1361
+ const text = userText(rec.message);
1362
+ if (text) agg.applyPrompt(text);
1363
+ const blocks = Array.isArray(rec.message?.content) ? rec.message.content : [];
1364
+ for (const b of blocks) {
1365
+ if (!b || b.type !== "tool_result") continue;
1366
+ const toolName = (typeof b.tool_use_id === "string" ? toolUseNames.get(b.tool_use_id) : void 0) ?? "(unknown)";
1367
+ const isError = b.is_error === true;
1368
+ let category = null;
1369
+ if (isError) {
1370
+ const txt = typeof b.content === "string" ? b.content : Array.isArray(b.content) ? b.content.map((c) => c && typeof c.text === "string" ? c.text : "").join(" ") : "";
1371
+ category = classifyError(txt);
1372
+ }
1373
+ agg.applyToolResult(toolName, isError, category);
1374
+ }
1375
+ const tur = rec.toolUseResult;
1376
+ if (tur && typeof tur === "object") {
1377
+ if (tur.interrupted === true) agg.markInterrupted();
1378
+ if (Array.isArray(tur.structuredPatch)) {
1379
+ let added = 0;
1380
+ let removed = 0;
1381
+ for (const h of tur.structuredPatch) if (h && Array.isArray(h.lines)) {
1382
+ for (const l of h.lines) if (typeof l === "string") {
1383
+ if (l.startsWith("+")) added++;
1384
+ else if (l.startsWith("-")) removed++;
1385
+ }
1386
+ }
1387
+ agg.applyEdit(added, removed, tur.userModified === true);
1388
+ }
1389
+ }
1390
+ }
1391
+ } else if (rec.type === "assistant") {
1392
+ const msg = rec.message ?? {};
1393
+ const best = dedupKey !== null ? maxUsageByKey.get(dedupKey) : void 0;
1394
+ let tokens;
1395
+ if (best) tokens = { ...best };
1396
+ else {
1397
+ const usage = msg.usage ?? {};
1398
+ const input = num$1(usage.input_tokens);
1399
+ const cachedInput = num$1(usage.cache_read_input_tokens);
1400
+ const output = num$1(usage.output_tokens);
1401
+ const cacheCreation = num$1(usage.cache_creation_input_tokens);
1402
+ tokens = {
1403
+ input,
1404
+ cached_input: cachedInput,
1405
+ output,
1406
+ reasoning_output: 0,
1407
+ cache_creation: cacheCreation,
1408
+ total: input + output + cachedInput + cacheCreation
1409
+ };
1410
+ }
1411
+ const model = typeof msg.model === "string" ? msg.model : "";
1412
+ agg.applyTokens(tokens, model, repo, sidechain ? "" : session, ts, branch);
1413
+ if (!sidechain) {
1414
+ agg.touchSession(session);
1415
+ agg.markActive(ts);
1416
+ const ep = sessionEntrypoint.get(session);
1417
+ if (ep) agg.applyUsageSource(ep, tokens, session);
1418
+ const stu = (msg.usage ?? {}).server_tool_use;
1419
+ if (stu && typeof stu === "object") agg.applyClaudeServerTool(num$1(stu.web_search_requests), num$1(stu.web_fetch_requests));
1420
+ const blocks = Array.isArray(msg.content) ? msg.content : [];
1421
+ for (const b of blocks) {
1422
+ if (!b || b.type !== "tool_use") continue;
1423
+ const name = typeof b.name === "string" ? b.name : "";
1424
+ const inp = b.input ?? {};
1425
+ agg.applyToolName(name);
1426
+ if (name === "Bash") agg.applyTool("shell", typeof inp.command === "string" ? inp.command : void 0);
1427
+ else if (name === "WebFetch" || name === "WebSearch") agg.applyTool("web");
1428
+ else if (name === "Edit" || name === "Write" || name === "Read" || name === "NotebookEdit") {
1429
+ agg.applyTool("file");
1430
+ const ext = extOf(typeof inp.file_path === "string" ? inp.file_path : "");
1431
+ agg.applyFileChangeExt(repo, ext);
1432
+ agg.applyLanguageFile(ext);
1433
+ } else if (name === "Glob" || name === "Grep" || name === "ToolSearch") agg.applyTool("search");
1434
+ else if (name.startsWith("mcp__")) agg.applyTool("mcp");
1435
+ else agg.applyTool("other");
1436
+ }
1437
+ }
1438
+ }
1439
+ }
1440
+ }
1441
+ }
1442
+
1443
+ //#endregion
1444
+ //#region src/parsers/codex.ts
1445
+ function codexOutcome(output) {
1446
+ if (typeof output !== "string") return {
1447
+ exitCode: null,
1448
+ text: "",
1449
+ interrupted: false
1450
+ };
1451
+ let parsed = null;
1452
+ try {
1453
+ parsed = JSON.parse(output);
1454
+ } catch {}
1455
+ if (parsed && typeof parsed === "object") {
1456
+ const meta = parsed.metadata ?? {};
1457
+ const ec = meta.exit_code ?? parsed.exit_code;
1458
+ return {
1459
+ exitCode: typeof ec === "number" ? ec : null,
1460
+ text: typeof parsed.output === "string" ? parsed.output : output,
1461
+ interrupted: meta.interrupted === true || parsed.interrupted === true
1462
+ };
1463
+ }
1464
+ return {
1465
+ exitCode: null,
1466
+ text: output,
1467
+ interrupted: false
1468
+ };
1469
+ }
1470
+ function codexHome() {
1471
+ const env = process.env.CODEX_HOME;
1472
+ if (env && env.trim()) return env.trim();
1473
+ return join(homedir(), ".codex");
1474
+ }
1475
+ function globRollouts(home) {
1476
+ const root = join(home, "sessions");
1477
+ const out = [];
1478
+ const walk = (dir) => {
1479
+ let entries;
1480
+ try {
1481
+ entries = readdirSync(dir, { withFileTypes: true });
1482
+ } catch {
1483
+ return;
1484
+ }
1485
+ for (const e of entries) {
1486
+ const p = join(dir, e.name);
1487
+ if (e.isDirectory()) walk(p);
1488
+ else if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(".jsonl")) out.push(p);
1489
+ }
1490
+ };
1491
+ walk(root);
1492
+ return out.sort();
1493
+ }
1494
+ function num(x) {
1495
+ return typeof x === "number" && isFinite(x) ? x : 0;
1496
+ }
1497
+ function fromCodex(o) {
1498
+ return {
1499
+ input: num(o?.input_tokens),
1500
+ cached: num(o?.cached_input_tokens),
1501
+ output: num(o?.output_tokens),
1502
+ reasoning: num(o?.reasoning_output_tokens),
1503
+ total: num(o?.total_tokens)
1504
+ };
1505
+ }
1506
+ function satSub(a, b) {
1507
+ const s = (x, y) => x < y ? 0 : x - y;
1508
+ return {
1509
+ input: s(a.input, b.input),
1510
+ cached: s(a.cached, b.cached),
1511
+ output: s(a.output, b.output),
1512
+ reasoning: s(a.reasoning, b.reasoning),
1513
+ total: s(a.total, b.total)
1514
+ };
1515
+ }
1516
+ function addCodex(a, b) {
1517
+ return {
1518
+ input: a.input + b.input,
1519
+ cached: a.cached + b.cached,
1520
+ output: a.output + b.output,
1521
+ reasoning: a.reasoning + b.reasoning,
1522
+ total: a.total + b.total
1523
+ };
1524
+ }
1525
+ function sourceKey(source) {
1526
+ const s = (source ?? "").trim().toLowerCase();
1527
+ if (s === "") return "(unknown)";
1528
+ if (s.includes("vscode") || s.includes("ide")) return "plugin";
1529
+ if (s.includes("codex-app") || s.includes("desktop") || s === "app") return "codex-app";
1530
+ if (s.includes("cli") || s.includes("terminal")) return "cli";
1531
+ return s;
1532
+ }
1533
+ function commandLine(args) {
1534
+ if (!args) return "";
1535
+ let a;
1536
+ try {
1537
+ a = JSON.parse(args);
1538
+ } catch {
1539
+ return "";
1540
+ }
1541
+ if (typeof a?.cmd === "string") return a.cmd.trim();
1542
+ if (Array.isArray(a?.command)) return a.command.join(" ").trim();
1543
+ return "";
1544
+ }
1545
+ function stripShellWrapper(fields) {
1546
+ const wrappers = new Set([
1547
+ "bash",
1548
+ "sh",
1549
+ "zsh",
1550
+ "fish",
1551
+ "-lc",
1552
+ "-c",
1553
+ "env"
1554
+ ]);
1555
+ let f = fields;
1556
+ while (f.length > 0) {
1557
+ const head = f[0].replace(/^["'`]+|["'`]+$/g, "");
1558
+ if (wrappers.has(head)) {
1559
+ f = f.slice(1);
1560
+ continue;
1561
+ }
1562
+ break;
1563
+ }
1564
+ return f;
1565
+ }
1566
+ function normalizedCommandLine(args) {
1567
+ return stripShellWrapper(commandLine(args).split(/\s+/).filter(Boolean)).join(" ");
1568
+ }
1569
+ function isSubagentRollout(lines) {
1570
+ for (const line of lines) {
1571
+ const t = line.trim();
1572
+ if (!t) continue;
1573
+ let rec;
1574
+ try {
1575
+ rec = JSON.parse(t);
1576
+ } catch {
1577
+ continue;
1578
+ }
1579
+ if (rec?.type === "session_meta") {
1580
+ const probe = { ...rec.payload ?? {} };
1581
+ delete probe.cwd;
1582
+ const s = JSON.stringify(probe);
1583
+ return s.includes("subagent") || s.includes("thread_spawn");
1584
+ }
1585
+ }
1586
+ return false;
1587
+ }
1588
+ function feedCodex(agg, home, window) {
1589
+ const inWin = (d) => !Number.isNaN(d.getTime()) && inLocalRange(d, window);
1590
+ for (const file of globRollouts(home)) {
1591
+ let content;
1592
+ try {
1593
+ content = readFileSync(file, "utf8");
1594
+ } catch {
1595
+ continue;
1596
+ }
1597
+ const lines = content.split("\n");
1598
+ const sidechain = isSubagentRollout(lines);
1599
+ agg.resetActive();
1600
+ let prevTotal = {
1601
+ input: 0,
1602
+ cached: 0,
1603
+ output: 0,
1604
+ reasoning: 0,
1605
+ total: 0
1606
+ };
1607
+ let curModel = "";
1608
+ let sessionId = "";
1609
+ let repo = "(unknown)";
1610
+ let source = "(unknown)";
1611
+ let threadTouched = false;
1612
+ let originator = "";
1613
+ let gitIdentity = false;
1614
+ let rolloutPlanType = null;
1615
+ let billingTokens = 0;
1616
+ const callNames = new Map();
1617
+ for (const line of lines) {
1618
+ const trimmed = line.trim();
1619
+ if (!trimmed) continue;
1620
+ let rec;
1621
+ try {
1622
+ rec = JSON.parse(trimmed);
1623
+ } catch {
1624
+ continue;
1625
+ }
1626
+ const tsRaw = rec?.timestamp;
1627
+ const ts = typeof tsRaw === "string" ? new Date(tsRaw) : new Date(NaN);
1628
+ const payload = rec?.payload ?? {};
1629
+ switch (rec?.type) {
1630
+ case "session_meta": {
1631
+ if (!sessionId && typeof payload.id === "string") sessionId = payload.id;
1632
+ if (source === "(unknown)" && typeof payload.source === "string") source = sourceKey(payload.source);
1633
+ if (typeof payload.cwd === "string" && payload.cwd) repo = repoName(payload.cwd);
1634
+ if (typeof payload.originator === "string" && payload.originator) originator = payload.originator;
1635
+ const git = payload.git;
1636
+ if (git && typeof git === "object" && (git.repository_url || git.commit_hash)) gitIdentity = true;
1637
+ if (typeof payload.model_provider === "string" && payload.model_provider && payload.model_provider !== "openai") agg.markCodexNonDefaultProvider();
1638
+ break;
1639
+ }
1640
+ case "compacted": {
1641
+ if (!sidechain && inWin(ts)) agg.markCodexCompaction();
1642
+ break;
1643
+ }
1644
+ case "turn_context": {
1645
+ if (typeof payload.model === "string" && payload.model) curModel = payload.model;
1646
+ if (!sidechain && inWin(ts)) {
1647
+ if (typeof payload.effort === "string") agg.applyCodexLabel("effort", payload.effort);
1648
+ if (typeof payload.approval_policy === "string") agg.applyCodexLabel("approval_policy", payload.approval_policy);
1649
+ const sb = payload.sandbox_policy;
1650
+ if (sb && typeof sb === "object") {
1651
+ const mode = typeof sb.mode === "string" ? sb.mode : typeof sb.type === "string" ? sb.type : "";
1652
+ if (mode) agg.applyCodexLabel("sandbox", mode);
1653
+ }
1654
+ const cm = payload.collaboration_mode;
1655
+ const cmMode = cm && typeof cm === "object" && typeof cm.mode === "string" ? cm.mode : typeof cm === "string" ? cm : "";
1656
+ if (cmMode) agg.applyCodexLabel("collaboration_mode", cmMode);
1657
+ if (typeof payload.personality === "string") agg.applyCodexLabel("personality", payload.personality);
1658
+ }
1659
+ break;
1660
+ }
1661
+ case "event_msg": {
1662
+ if (payload.type === "error" || payload.type === "stream_error") {
1663
+ if (!sidechain && inWin(ts)) agg.applyApiError();
1664
+ break;
1665
+ }
1666
+ if (payload.type === "context_compacted") {
1667
+ if (!sidechain && inWin(ts)) agg.markCodexCompaction();
1668
+ break;
1669
+ }
1670
+ if (payload.type === "turn_aborted") {
1671
+ if (!sidechain && inWin(ts)) agg.markCodexAbortedTurn();
1672
+ break;
1673
+ }
1674
+ if (payload.type !== "token_count") break;
1675
+ const rl = payload.rate_limits;
1676
+ if (rl && typeof rl === "object" && rolloutPlanType === null && typeof rl.plan_type === "string" && rl.plan_type) rolloutPlanType = rl.plan_type;
1677
+ const info = payload.info;
1678
+ if (!info) break;
1679
+ let delta;
1680
+ if (info.last_token_usage) {
1681
+ delta = fromCodex(info.last_token_usage);
1682
+ prevTotal = info.total_token_usage ? fromCodex(info.total_token_usage) : addCodex(prevTotal, delta);
1683
+ } else if (info.total_token_usage) {
1684
+ delta = satSub(fromCodex(info.total_token_usage), prevTotal);
1685
+ prevTotal = fromCodex(info.total_token_usage);
1686
+ } else break;
1687
+ if (delta.cached > delta.input) delta.cached = delta.input;
1688
+ if (delta.total <= 0) delta.total = delta.input + delta.output;
1689
+ if (delta.input <= 0 && delta.cached <= 0 && delta.output <= 0 && delta.reasoning <= 0) break;
1690
+ if (Number.isNaN(ts.getTime()) || !inLocalRange(ts, window)) break;
1691
+ const tokens = {
1692
+ input: delta.input,
1693
+ cached_input: delta.cached,
1694
+ output: delta.output,
1695
+ reasoning_output: delta.reasoning,
1696
+ cache_creation: 0,
1697
+ total: delta.total
1698
+ };
1699
+ agg.beginRecord(repo, sidechain ? "" : sessionId, ts);
1700
+ agg.applyTokens(tokens, curModel, repo, sidechain ? "" : sessionId, ts);
1701
+ billingTokens += delta.total;
1702
+ if (!sidechain && typeof info.model_context_window === "number") agg.applyCodexContextWindow(info.model_context_window);
1703
+ if (sidechain) agg.markSubagentMessage();
1704
+ else {
1705
+ threadTouched = true;
1706
+ agg.markActive(ts);
1707
+ }
1708
+ break;
1709
+ }
1710
+ case "response_item": {
1711
+ if (sidechain) break;
1712
+ if (Number.isNaN(ts.getTime()) || !inLocalRange(ts, window)) break;
1713
+ agg.beginRecord(repo, sessionId, ts);
1714
+ const t = payload.type;
1715
+ if (t === "function_call") {
1716
+ const name = payload.name;
1717
+ if (typeof payload.call_id === "string") callNames.set(payload.call_id, name);
1718
+ if (name === "exec_command" || name === "local_shell_call" || name === "shell") agg.applyTool("shell", normalizedCommandLine(typeof payload.arguments === "string" ? payload.arguments : ""));
1719
+ else agg.applyTool("other");
1720
+ } else if (t === "function_call_output" || t === "local_shell_call_output") {
1721
+ const { exitCode, text, interrupted } = codexOutcome(payload.output);
1722
+ const name = (typeof payload.call_id === "string" ? callNames.get(payload.call_id) : void 0) ?? "shell";
1723
+ const isError = exitCode !== null && exitCode !== 0;
1724
+ agg.applyToolResult(name, isError, isError ? classifyError(text) : null);
1725
+ if (interrupted) agg.markInterrupted();
1726
+ } else if (t === "local_shell_call") agg.applyTool("shell");
1727
+ else if (t === "web_search_call") agg.applyTool("web");
1728
+ else if (t === "custom_tool_call" || t === "image_generation_call") agg.applyTool("other");
1729
+ break;
1730
+ }
1731
+ }
1732
+ }
1733
+ agg.applyBillingRollout(rolloutPlanType, billingTokens);
1734
+ if (threadTouched) {
1735
+ agg.touchSession(sessionId);
1736
+ if (originator) agg.applyCodexLabel("originators", originator);
1737
+ if (gitIdentity) agg.markCodexGitIdentity();
1738
+ }
1739
+ }
1740
+ }
1741
+
1742
+ //#endregion
1743
+ //#region src/endpoint.ts
1744
+ const OFFICIAL_HOSTS = new Set([
1745
+ "api.anthropic.com",
1746
+ "api.openai.com",
1747
+ "chatgpt.com"
1748
+ ]);
1749
+ function readText(path) {
1750
+ try {
1751
+ return readFileSync(path, "utf8");
1752
+ } catch {
1753
+ return null;
1754
+ }
1755
+ }
1756
+ function readJson(path) {
1757
+ const t = readText(path);
1758
+ if (t == null) return null;
1759
+ try {
1760
+ return JSON.parse(t);
1761
+ } catch {
1762
+ return null;
1763
+ }
1764
+ }
1765
+ function hostOf(url) {
1766
+ if (typeof url !== "string" || !url.trim()) return "";
1767
+ let u = url.trim();
1768
+ if (!/^[a-z]+:\/\//i.test(u)) u = "https://" + u;
1769
+ try {
1770
+ return new URL(u).hostname.toLowerCase();
1771
+ } catch {
1772
+ return "";
1773
+ }
1774
+ }
1775
+ function classifyHost(host) {
1776
+ if (!host) return { endpoint: "unknown" };
1777
+ if (OFFICIAL_HOSTS.has(host)) return {
1778
+ endpoint: "official",
1779
+ officialHost: host
1780
+ };
1781
+ return { endpoint: "custom" };
1782
+ }
1783
+ function tomlActiveProvider(toml) {
1784
+ for (const raw of toml.split("\n")) {
1785
+ const line = raw.trim();
1786
+ if (line.startsWith("[")) break;
1787
+ const m = line.match(/^model_provider\s*=\s*["']([^"']+)["']/);
1788
+ if (m) return m[1];
1789
+ }
1790
+ return null;
1791
+ }
1792
+ function tomlProviderBaseUrl(toml, name) {
1793
+ const lines = toml.split("\n");
1794
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1795
+ const header = new RegExp(`^\\[model_providers\\.(?:"?${esc}"?)\\]\\s*$`);
1796
+ let inSection = false;
1797
+ for (const raw of lines) {
1798
+ const line = raw.trim();
1799
+ if (line.startsWith("[")) {
1800
+ inSection = header.test(line);
1801
+ continue;
1802
+ }
1803
+ if (inSection) {
1804
+ const m = line.match(/^base_url\s*=\s*["']([^"']+)["']/);
1805
+ if (m) return m[1];
1806
+ }
1807
+ }
1808
+ return null;
1809
+ }
1810
+ function detectCodexEndpoint(home, nonDefaultProviderInJsonl) {
1811
+ const basis = [];
1812
+ let authMode;
1813
+ const auth = readJson(join(home, "auth.json"));
1814
+ if (auth && typeof auth === "object") {
1815
+ if (typeof auth.auth_mode === "string") authMode = auth.auth_mode === "chatgpt" ? "chatgpt" : auth.auth_mode === "apikey" ? "apikey" : "other";
1816
+ else if (auth.OPENAI_API_KEY) authMode = "apikey";
1817
+ if (authMode) basis.push(`auth_mode:${authMode}`);
1818
+ }
1819
+ let endpoint = "unknown";
1820
+ let officialHost;
1821
+ const toml = readText(join(home, "config.toml"));
1822
+ if (toml != null) {
1823
+ const active = tomlActiveProvider(toml);
1824
+ if (!active || active === "openai") {
1825
+ endpoint = "official";
1826
+ officialHost = "chatgpt.com";
1827
+ basis.push("config:provider=openai");
1828
+ } else {
1829
+ const baseUrl = tomlProviderBaseUrl(toml, active);
1830
+ const host = baseUrl ? hostOf(baseUrl) : "";
1831
+ const c = classifyHost(host);
1832
+ endpoint = c.endpoint === "unknown" ? "custom" : c.endpoint;
1833
+ officialHost = c.officialHost;
1834
+ basis.push(endpoint === "official" ? `base_url:official(${officialHost})` : "config:provider=custom");
1835
+ }
1836
+ } else {
1837
+ endpoint = "official";
1838
+ officialHost = "chatgpt.com";
1839
+ basis.push("config:default");
1840
+ }
1841
+ if (nonDefaultProviderInJsonl) basis.push("jsonl:non-default-provider");
1842
+ let billingMode = "unknown";
1843
+ let confidence = "low";
1844
+ if (endpoint === "custom") {
1845
+ billingMode = "api_or_relay";
1846
+ confidence = "high";
1847
+ } else if (endpoint === "official") if (authMode === "chatgpt") {
1848
+ billingMode = "subscription";
1849
+ confidence = nonDefaultProviderInJsonl ? "medium" : "high";
1850
+ } else if (authMode === "apikey") {
1851
+ billingMode = "api_or_relay";
1852
+ confidence = "high";
1853
+ } else {
1854
+ billingMode = "unknown";
1855
+ confidence = "low";
1856
+ }
1857
+ const out = {
1858
+ platform: "codex",
1859
+ endpoint,
1860
+ relay_suspected: endpoint === "custom",
1861
+ non_default_provider: nonDefaultProviderInJsonl,
1862
+ billing_mode: billingMode,
1863
+ confidence,
1864
+ basis
1865
+ };
1866
+ if (officialHost) out.official_host = officialHost;
1867
+ if (authMode) out.auth_mode = authMode;
1868
+ return out;
1869
+ }
1870
+ function detectClaudeEndpoint(home) {
1871
+ const basis = [];
1872
+ const envOf = (file) => {
1873
+ const j = readJson(join(home, file));
1874
+ const e = j && typeof j === "object" ? j.env : null;
1875
+ return e && typeof e === "object" ? e : {};
1876
+ };
1877
+ const env = {
1878
+ ...envOf("settings.json"),
1879
+ ...envOf("settings.local.json")
1880
+ };
1881
+ const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : "";
1882
+ const hasAuthToken = !!env.ANTHROPIC_AUTH_TOKEN;
1883
+ const hasApiKey = !!env.ANTHROPIC_API_KEY;
1884
+ let endpoint;
1885
+ let officialHost;
1886
+ if (baseUrl) {
1887
+ const c = classifyHost(hostOf(baseUrl));
1888
+ endpoint = c.endpoint === "unknown" ? "custom" : c.endpoint;
1889
+ officialHost = c.officialHost;
1890
+ basis.push(endpoint === "official" ? `base_url:official(${officialHost})` : "settings:ANTHROPIC_BASE_URL=custom");
1891
+ } else {
1892
+ endpoint = "official";
1893
+ officialHost = "api.anthropic.com";
1894
+ basis.push("settings:default");
1895
+ }
1896
+ let subscriptionType;
1897
+ const creds = readJson(join(home, ".credentials.json"));
1898
+ const oauth = creds && typeof creds === "object" ? creds.claudeAiOauth : null;
1899
+ if (oauth && typeof oauth === "object" && typeof oauth.subscriptionType === "string") subscriptionType = oauth.subscriptionType;
1900
+ let authMode;
1901
+ if (hasAuthToken) authMode = "auth-token";
1902
+ else if (hasApiKey) authMode = "api-key";
1903
+ else if (oauth) authMode = "oauth-subscription";
1904
+ if (authMode) basis.push(`auth_mode:${authMode}`);
1905
+ if (subscriptionType) basis.push(`subscription:${subscriptionType}`);
1906
+ let billingMode = "unknown";
1907
+ let confidence = "low";
1908
+ if (endpoint === "custom") {
1909
+ billingMode = "api_or_relay";
1910
+ confidence = "high";
1911
+ } else if (authMode === "oauth-subscription") {
1912
+ billingMode = "subscription";
1913
+ confidence = "high";
1914
+ } else if (authMode === "api-key" || authMode === "auth-token") {
1915
+ billingMode = "api_or_relay";
1916
+ confidence = "medium";
1917
+ } else {
1918
+ billingMode = "unknown";
1919
+ confidence = "low";
1920
+ }
1921
+ const out = {
1922
+ platform: "claude-code",
1923
+ endpoint,
1924
+ relay_suspected: endpoint === "custom",
1925
+ billing_mode: billingMode,
1926
+ confidence,
1927
+ basis
1928
+ };
1929
+ if (officialHost) out.official_host = officialHost;
1930
+ if (authMode) out.auth_mode = authMode;
1931
+ if (subscriptionType) out.subscription_type = subscriptionType;
1932
+ return out;
1933
+ }
1934
+
1935
+ //#endregion
1936
+ //#region src/index.ts
1937
+ const VERSION = "0.1.0";
1938
+ function buildReport(opts) {
1939
+ const { platform, window } = opts;
1940
+ const scope = opts.scope ?? "global";
1941
+ const claudeDir = opts.claudeDir ?? claudeProjectsDir();
1942
+ const cxHome = opts.codexHome ?? codexHome();
1943
+ const wantClaude = platform === "claude-code" || platform === "all";
1944
+ const wantCodex = platform === "codex" || platform === "all";
1945
+ const agg = new Aggregator(platform, scope);
1946
+ if (wantClaude) feedClaudeCode(agg, claudeDir, window);
1947
+ if (wantCodex) feedCodex(agg, cxHome, window);
1948
+ const report = agg.assemble(window, "glob");
1949
+ const endpoints = [];
1950
+ if (wantCodex) endpoints.push(detectCodexEndpoint(cxHome, agg.getCodexNonDefaultProvider()));
1951
+ if (wantClaude) endpoints.push(detectClaudeEndpoint(dirname(claudeDir)));
1952
+ if (endpoints.length) report.endpoints = endpoints;
1953
+ return report;
1954
+ }
1955
+
1956
+ //#endregion
1957
+ export { VERSION, buildReport, claudeProjectsDir, codexHome, comma, inLocalRange, promptFlags, repoName, resolveWindow };