ccus-cli 0.2.4 → 0.2.6

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/README.md CHANGED
@@ -162,7 +162,7 @@ ccus aggregate serve --input-dir ./team-exports
162
162
  - 默认输出一个 `json` 数据包,里面同时包含 `rawEvents`、`weeklySummary`、`dailySummaries`
163
163
  - 导出文件内容为**紧凑 JSON**(无缩进),并默认 **gzip 压缩**后写成 `.json.gz`;gzip 与紧凑化都只是存储/展示层变化,解压后的字段集合与 `schemaVersion` 不变
164
164
  - 默认写 `.json.gz`;若用 `--out` 指定一个非 `.gz` 结尾的路径,则按明文 JSON 写出(不压缩)
165
- - 当前导出 bundle / weeklySummary 的 `schemaVersion` 为 `8`:v7 起新增 `weeklySummary.codex` 与 `dailySummaries[].codex`(Codex CLI 的消息/请求/token 统计),v8 起该 codex 段再加 5h/7d 的 peak/latest 额度(从 `source="codex"` 事件重算、与 Claude 额度分开看);aggregate daily/weekly CSV 的主字段(消息/请求/token/额度)已把 Codex **叠加进 Claude 合计**(累加量相加、额度 peak 取两源 max、latest 两源相加、7d 累计含两源读数),不再单列 `codex*` 列,detail.csv 加 `source` 列区分来源
165
+ - 当前导出 bundle / weeklySummary 的 `schemaVersion` 为 `9`:v7 起新增 `weeklySummary.codex` 与 `dailySummaries[].codex`(Codex CLI 的消息/请求/token 统计),v8 起该 codex 段再加 5h/7d 的 peak/latest 额度(从 `source="codex"` 事件重算、与 Claude 额度分开看),v9 起 codex `inputTokens` 改为净输入(`input_tokens - cached_input_tokens`,不含缓存命中)以对齐 Claude 的 `input_tokens` 口径;aggregate daily/weekly CSV 的主字段(消息/请求/token/额度)已把 Codex **叠加进 Claude 合计**(累加量相加、额度 peak 取两源 max、latest 两源相加、7d 累计含两源读数),不再单列 `codex*` 列,detail.csv 加 `source` 列区分来源
166
166
  - 默认文件名会带 git email 的帐号名前缀和起止日期,例如:`alice_export_2026-05-26_to_2026-06-01.json.gz`
167
167
  - `userMessageCount` 来自 `~/.claude/projects/**/*.jsonl` 的非 meta `type:user` 事件
168
168
  - `apiRequestCount` 与 token 指标来自 `~/.claude/projects/**/*.jsonl` 中带 `message.usage` 的 `type:assistant` 事件
@@ -172,7 +172,7 @@ ccus aggregate serve --input-dir ./team-exports
172
172
  ## 多人汇总
173
173
 
174
174
  - 输入目录放很多通过 `ccus export` 导出的 bundle 文件,`.json.gz`(gzip 压缩)与明文 `.json` 都能识别,gzip 文件读取时自动解压
175
- - `aggregate` 接受 `schemaVersion: 6/7/8` 的 bundle(v8 含 codex 额度,v7/v6 容错回退 null/零值);更旧的导出请先用当前版本重新 `ccus export`
175
+ - `aggregate` 接受 `schemaVersion: 6/7/8/9` 的 bundle(v9 codex inputTokens 为净输入,v8 含 codex 额度,v7/v6 容错回退 null/零值);更旧的导出请先用当前版本重新 `ccus export`
176
176
  - 同一个人在多台电脑上各自导出 bundle 时会自动合并去重:去重以**天**为粒度,对每个「同人同天」取 `generatedAt` 最新的那份导出,避免同一台机器重复导出或周与周重叠造成翻倍;**周汇总不取整周单份,而是把按天去重后的各天数据上卷累加**,所以多台电脑在不同天产生的用量会正确合进同一周。usage(5h / 7d)从选中事件按真实时间戳重算,Claude+Codex 合并:peak 取两源 max、latest 两源相加
177
177
  - `ccus aggregate --input-dir DIR --out-dir DIR`
178
178
  - 输出三个文件:
package/dist/cli.js CHANGED
@@ -395,7 +395,7 @@ async function runExport(options) {
395
395
  exportUserName = gitIdentity.userName;
396
396
  }
397
397
  const weeklySummary = {
398
- schemaVersion: 8,
398
+ schemaVersion: 9,
399
399
  generatedAt: new Date().toISOString(),
400
400
  range: {
401
401
  label: window.label,
@@ -479,7 +479,7 @@ async function runExport(options) {
479
479
  };
480
480
  });
481
481
  const bundle = {
482
- schemaVersion: 8,
482
+ schemaVersion: 9,
483
483
  generatedAt: new Date().toISOString(),
484
484
  range: {
485
485
  label: window.label,
@@ -50,7 +50,7 @@ function isWeeklyExportBundle(value) {
50
50
  if (!isRecord(value)) {
51
51
  return false;
52
52
  }
53
- if (typeof value.schemaVersion !== "number" || ![6, 7, 8].includes(value.schemaVersion)) {
53
+ if (typeof value.schemaVersion !== "number" || ![6, 7, 8, 9].includes(value.schemaVersion)) {
54
54
  return false;
55
55
  }
56
56
  if (!Array.isArray(value.rawEvents) || !Array.isArray(value.dailySummaries) || !isRecord(value.weeklySummary) || !isRecord(value.identity) || !isRecord(value.range)) {
@@ -131,7 +131,7 @@ async function loadWeeklyExportBundles(inputDir) {
131
131
  }
132
132
  }
133
133
  if (invalidFiles.length > 0) {
134
- throw new Error(`Unsupported export bundle schema in files: ${invalidFiles.join(", ")}. Re-export with current ccus so aggregate receives schemaVersion 6/7/8 bundles.`);
134
+ throw new Error(`Unsupported export bundle schema in files: ${invalidFiles.join(", ")}. Re-export with current ccus so aggregate receives schemaVersion 6/7/8/9 bundles.`);
135
135
  }
136
136
  return bundles;
137
137
  }
@@ -411,8 +411,11 @@ function buildSevenDayCurveFromEvents(events) {
411
411
  }
412
412
  /**
413
413
  * 同一 personKey 的 7d 累计曲线:把该人**所有 bundle**(非仅 winner)的 rawEvents 计算成事件、
414
- * 取非 null `sevenDayUsagePct`、按 timestamp 升序合并、对完全相同 timestamp 去重,再做读数去毛刺,
415
- * 得到一条账号级曲线。
414
+ * 取非 null `sevenDayUsagePct`、按 timestamp 升序合并、对完全相同 timestamp 去重,再做读数去毛刺。
415
+ *
416
+ * Claude 与 Codex 是两个不同额度池的独立曲线(读数水平常常相差悬殊),按 source 各自建一条,
417
+ * 算累计时分别做分段峰谷和再相加。混成一条会让低位源频繁触发假 reset、把高位源的上升段反复重算,
418
+ * 导致累计严重虚高(实测 jizhiqiang 混算 221% vs 分源相加 44%)。
416
419
  *
417
420
  * 走全样本而非 winner,是因为累计指标是同一条共享额度曲线的密集采样:只取 winner 会漏掉非 winner
418
421
  * 机器的样本(曲线稀疏、累计偏小),分机各自累计再相加又会翻倍。结果按 bundles 数组缓存复用。
@@ -423,27 +426,31 @@ function buildPersonSevenDayCurve(bundles) {
423
426
  if (cached) {
424
427
  return cached;
425
428
  }
426
- const collected = new Map();
429
+ const claudeByPerson = new Map();
430
+ const codexByPerson = new Map();
427
431
  for (const { bundle } of bundles) {
428
432
  const personKey = bundlePersonKey(bundle);
429
- // 含 Claude + Codex 两源读数:codex 7d 读数不再过滤,并入同一条曲线算累计
430
433
  for (const record of bundle.rawEvents.filter((r) => isPersistedStatuslineEvent(r))) {
431
434
  const event = (0, payload_1.computeStatuslineEvent)(record);
432
435
  if (event.sevenDayUsagePct === null) {
433
436
  continue;
434
437
  }
435
- const list = collected.get(personKey);
438
+ const target = (0, payload_1.isCodexSourceEvent)(event) ? codexByPerson : claudeByPerson;
439
+ const list = target.get(personKey);
436
440
  if (list) {
437
441
  list.push(event);
438
442
  }
439
443
  else {
440
- collected.set(personKey, [event]);
444
+ target.set(personKey, [event]);
441
445
  }
442
446
  }
443
447
  }
444
448
  const curves = new Map();
445
- for (const [personKey, events] of collected.entries()) {
446
- curves.set(personKey, buildSevenDayCurveFromEvents(events));
449
+ for (const personKey of new Set([...claudeByPerson.keys(), ...codexByPerson.keys()])) {
450
+ curves.set(personKey, {
451
+ claude: buildSevenDayCurveFromEvents(claudeByPerson.get(personKey) ?? []),
452
+ codex: buildSevenDayCurveFromEvents(codexByPerson.get(personKey) ?? []),
453
+ });
447
454
  }
448
455
  personSevenDayCurveCache.set(bundles, curves);
449
456
  return curves;
@@ -456,6 +463,18 @@ function sliceCurveByDate(curve, date) {
456
463
  function sliceCurveByWeek(curve, week) {
457
464
  return curve.filter((event) => weekKey(new Date(event.timestamp)) === week);
458
465
  }
466
+ /**
467
+ * 分源 7d 累计:Claude 与 Codex 各自按区间切片做分段峰谷和,再用 `addNullable` 相加。
468
+ * 两源是不同额度池的独立曲线,混算会互相触发假 reset 而虚高;分源算后相加,与 CSV 里 token /
469
+ * 消息数「Claude+Codex 合计」口径一致。该 personKey 无任何曲线、或两源都无有效样本时返回 null。
470
+ */
471
+ function computeCumulativeSevenDayBySource(curves, personKey, slicer, rangeKey) {
472
+ const bySource = curves.get(personKey);
473
+ if (!bySource) {
474
+ return null;
475
+ }
476
+ return addNullable(computeCumulativeSevenDay(slicer(bySource.claude, rangeKey)), computeCumulativeSevenDay(slicer(bySource.codex, rangeKey)));
477
+ }
459
478
  /** 从一组事件按真实时间戳重算 5h / 7d 的 peak(max)与 latest(时间戳最新非空)。 */
460
479
  function recomputeUsage(events) {
461
480
  const newestFirst = [...events].sort((left, right) => new Date(right.timestamp).getTime() - new Date(left.timestamp).getTime());
@@ -526,8 +545,8 @@ function buildAggregatedDailyRows(bundles) {
526
545
  const codexUsage = recomputeUsage(codexEvents);
527
546
  // 额度回退:rawEvents 缺时从 daySummary 取(Claude+Codex 合并,见 fallbackWeeklyUsage)
528
547
  const fallback = fallbackWeeklyUsage(reps.map((r) => r.day));
529
- // 累计指标走全样本合并曲线,不走单机的 recomputeUsage,避免漏掉另一台机器的样本。
530
- const sevenDayCumulativeUsagePct = computeCumulativeSevenDay(sliceCurveByDate(curves.get(personKey) ?? [], date));
548
+ // 累计指标走全样本分源曲线(Claude + Codex 各自切片累计后相加),不走单机的 recomputeUsage,避免漏掉另一台机器的样本。
549
+ const sevenDayCumulativeUsagePct = computeCumulativeSevenDayBySource(curves, personKey, sliceCurveByDate, date);
531
550
  rows.push({
532
551
  personKey,
533
552
  date,
@@ -621,8 +640,8 @@ function buildAggregatedWeeklyRows(bundles) {
621
640
  const usage = recomputeUsage(claudeEvents);
622
641
  const codexUsage = recomputeUsage(codexEvents);
623
642
  const fallback = fallbackWeeklyUsage(acc.days);
624
- // 整周累计:在整周合并曲线上一次性求正增量,跨天边界增量被计入,故 weekly ≥ Σ daily。
625
- const sevenDayCumulativeUsagePct = computeCumulativeSevenDay(sliceCurveByWeek(curves.get(acc.personKey) ?? [], acc.week));
643
+ // 整周累计:Claude + Codex 各自在整周子曲线上做分段峰谷和后相加,跨天边界增量被计入,故 weekly ≥ Σ daily。
644
+ const sevenDayCumulativeUsagePct = computeCumulativeSevenDayBySource(curves, acc.personKey, sliceCurveByWeek, acc.week);
626
645
  rows.push({
627
646
  personKey: acc.personKey,
628
647
  week: acc.week,
@@ -47,6 +47,8 @@ function timestampInRange(timestamp, start, end) {
47
47
  /**
48
48
  * 从 `token_count` 事件的 `info.last_token_usage` 取 token 用量对象(缺失返回 null)。
49
49
  * 必须用 last_token_usage(本次增量),不能用 total_token_usage(会话累计,会重复计)。
50
+ * 注意:Codex 的 `input_tokens` 含缓存命中(`cached_input_tokens` 是其子集),
51
+ * 累加 inputTokens 时要减去 cached 得到净输入,对齐 Claude 的 `input_tokens` 口径。
50
52
  */
51
53
  function extractLastTokenUsage(payload) {
52
54
  if (!isRecord(payload.info)) {
@@ -91,8 +93,10 @@ function summarizeRollout(content, start, end) {
91
93
  if (payload.type === "token_count") {
92
94
  const usage = extractLastTokenUsage(payload);
93
95
  if (usage) {
96
+ // 净输入 = input_tokens - cached_input_tokens(input_tokens 含缓存命中,减去对齐 Claude 口径)。
97
+ const netInputTokens = Math.max(0, getNumber(usage.input_tokens) - getNumber(usage.cached_input_tokens));
94
98
  result.apiRequestCount += 1;
95
- result.inputTokens += getNumber(usage.input_tokens);
99
+ result.inputTokens += netInputTokens;
96
100
  result.outputTokens += getNumber(usage.output_tokens);
97
101
  result.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
98
102
  }
@@ -211,8 +215,10 @@ async function summarizeCodexSessionUsageByDay(start, end) {
211
215
  const usage = extractLastTokenUsage(payload);
212
216
  if (usage) {
213
217
  const current = ensureDay((0, time_1.localDateKey)(new Date(ms)));
218
+ // 同 summarizeRollout:累加净输入(input - cached),对齐 Claude 口径。
219
+ const netInputTokens = Math.max(0, getNumber(usage.input_tokens) - getNumber(usage.cached_input_tokens));
214
220
  current.apiRequestCount += 1;
215
- current.inputTokens += getNumber(usage.input_tokens);
221
+ current.inputTokens += netInputTokens;
216
222
  current.outputTokens += getNumber(usage.output_tokens);
217
223
  current.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
218
224
  }
@@ -34,8 +34,8 @@ function summarizeEvents(events) {
34
34
  const codexByDesc = events.filter(payload_1.isCodexSourceEvent).sort(byDesc);
35
35
  const latestUsagePct = (0, aggregate_1.addNullable)(claudeByDesc.find((event) => event.usagePct !== null)?.usagePct ?? null, codexByDesc.find((event) => event.usagePct !== null)?.usagePct ?? null);
36
36
  const latestSevenDayUsagePct = (0, aggregate_1.addNullable)(claudeByDesc.find((event) => event.sevenDayUsagePct !== null)?.sevenDayUsagePct ?? null, codexByDesc.find((event) => event.sevenDayUsagePct !== null)?.sevenDayUsagePct ?? null);
37
- // 7d 分区叠加累计:与 aggregate 同一套「去毛刺 + 分段峰谷和」,口径与团队看板一致。
38
- const sevenDayCumulativeUsagePct = (0, aggregate_1.computeCumulativeSevenDay)((0, aggregate_1.buildSevenDayCurveFromEvents)(events));
37
+ // 7d 分源累计:Claude + Codex 各自「去毛刺 + 分段峰谷和」后相加,与 aggregate 同口径(混算会虚高)。
38
+ const sevenDayCumulativeUsagePct = (0, aggregate_1.addNullable)((0, aggregate_1.computeCumulativeSevenDay)((0, aggregate_1.buildSevenDayCurveFromEvents)(events.filter((event) => !(0, payload_1.isCodexSourceEvent)(event)))), (0, aggregate_1.computeCumulativeSevenDay)((0, aggregate_1.buildSevenDayCurveFromEvents)(events.filter(payload_1.isCodexSourceEvent))));
39
39
  return {
40
40
  fiveHourLatestUsagePct: latestUsagePct,
41
41
  fiveHourPeakUsagePct: usages.length > 0 ? (0, time_1.roundNumber)(Math.max(...usages), 1) : null,
@@ -79,8 +79,9 @@ function bucketizeEvents(events, start, end, bucketMinutes = 5) {
79
79
  }
80
80
  // 7d 累计曲线(去毛刺 + 分段峰谷和的逐点版本)映射到时间桶:曲线已按时间升序,
81
81
  // 同桶内后写覆盖前写,最终每个桶留下落在其区间内最后一个累计点的值(单调,等于桶内最大累计)。
82
+ // 只画 Claude 源:折线图是 Claude 趋势看板(与同图 5h/7d 两条线同源),Codex 累计已并入顶部 summary 数值。
82
83
  const cumulativeByBucket = new Map();
83
- for (const point of (0, aggregate_1.computeCumulativeSevenDayCurve)((0, aggregate_1.buildSevenDayCurveFromEvents)(events))) {
84
+ for (const point of (0, aggregate_1.computeCumulativeSevenDayCurve)((0, aggregate_1.buildSevenDayCurveFromEvents)(events.filter((event) => !(0, payload_1.isCodexSourceEvent)(event))))) {
84
85
  const ts = new Date(point.timestamp).getTime();
85
86
  if (ts < start.getTime() || ts > end.getTime()) {
86
87
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {