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 +2 -2
- package/dist/cli.js +2 -2
- package/dist/lib/aggregate.js +33 -14
- package/dist/lib/codex-sessions.js +8 -2
- package/dist/lib/dashboard.js +4 -3
- package/package.json +1 -1
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` 为 `
|
|
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:
|
|
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:
|
|
482
|
+
schemaVersion: 9,
|
|
483
483
|
generatedAt: new Date().toISOString(),
|
|
484
484
|
range: {
|
|
485
485
|
label: window.label,
|
package/dist/lib/aggregate.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
444
|
+
target.set(personKey, [event]);
|
|
441
445
|
}
|
|
442
446
|
}
|
|
443
447
|
}
|
|
444
448
|
const curves = new Map();
|
|
445
|
-
for (const
|
|
446
|
-
curves.set(personKey,
|
|
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
|
-
//
|
|
530
|
-
const sevenDayCumulativeUsagePct =
|
|
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
|
-
//
|
|
625
|
-
const sevenDayCumulativeUsagePct =
|
|
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 +=
|
|
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 +=
|
|
221
|
+
current.inputTokens += netInputTokens;
|
|
216
222
|
current.outputTokens += getNumber(usage.output_tokens);
|
|
217
223
|
current.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
|
|
218
224
|
}
|
package/dist/lib/dashboard.js
CHANGED
|
@@ -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
|
|
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;
|