ccus-cli 0.2.3 → 0.2.5
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 +2 -2
- package/dist/lib/codex-sessions.js +89 -49
- 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
|
}
|
|
@@ -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)) {
|
|
@@ -55,16 +57,18 @@ function extractLastTokenUsage(payload) {
|
|
|
55
57
|
return isRecord(payload.info.last_token_usage) ? payload.info.last_token_usage : null;
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
60
|
+
* 解析单个 Codex rollout 文件,收集范围内的 task_started turn 节点与 token_count 用量。
|
|
59
61
|
*
|
|
60
62
|
* Codex rollout 每行一个事件,timestamp 在 top-level:
|
|
61
|
-
* - `event_msg` + `payload.type=="
|
|
63
|
+
* - `event_msg` + `payload.type=="task_started"`:一个用户 turn,带全局唯一 `turn_id`。
|
|
64
|
+
* fork / spawn sub-agent / resume 会把历史 task_started 跨文件重放,故 turn 节点交上层按
|
|
65
|
+
* turn_id 全局去重(取最早 timestamp),不能在单文件层计数。
|
|
62
66
|
* - `event_msg` + `payload.type=="token_count"`:一次模型请求,token 取 `info.last_token_usage` 增量。
|
|
63
67
|
*/
|
|
64
68
|
function summarizeRollout(content, start, end) {
|
|
65
69
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
66
|
-
const
|
|
67
|
-
|
|
70
|
+
const result = {
|
|
71
|
+
turns: [],
|
|
68
72
|
apiRequestCount: 0,
|
|
69
73
|
inputTokens: 0,
|
|
70
74
|
outputTokens: 0,
|
|
@@ -80,16 +84,21 @@ function summarizeRollout(content, start, end) {
|
|
|
80
84
|
continue;
|
|
81
85
|
}
|
|
82
86
|
const payload = record.payload;
|
|
83
|
-
if (payload.type === "
|
|
84
|
-
|
|
87
|
+
if (payload.type === "task_started" && typeof payload.turn_id === "string") {
|
|
88
|
+
const ms = new Date(getString(record.timestamp) ?? "").getTime();
|
|
89
|
+
if (Number.isFinite(ms)) {
|
|
90
|
+
result.turns.push({ turnId: payload.turn_id, ms });
|
|
91
|
+
}
|
|
85
92
|
}
|
|
86
93
|
if (payload.type === "token_count") {
|
|
87
94
|
const usage = extractLastTokenUsage(payload);
|
|
88
95
|
if (usage) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
// 净输入 = input_tokens - cached_input_tokens(input_tokens 含缓存命中,减去对齐 Claude 口径)。
|
|
97
|
+
const netInputTokens = Math.max(0, getNumber(usage.input_tokens) - getNumber(usage.cached_input_tokens));
|
|
98
|
+
result.apiRequestCount += 1;
|
|
99
|
+
result.inputTokens += netInputTokens;
|
|
100
|
+
result.outputTokens += getNumber(usage.output_tokens);
|
|
101
|
+
result.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
}
|
|
@@ -97,48 +106,79 @@ function summarizeRollout(content, start, end) {
|
|
|
97
106
|
continue;
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
|
-
return
|
|
109
|
+
return result;
|
|
101
110
|
}
|
|
102
111
|
/**
|
|
103
112
|
* 从 Codex 本地 session rollout(<CODEX_HOME>/sessions 下递归的 .jsonl)统计消息数、请求数和 token 用量。
|
|
113
|
+
*
|
|
114
|
+
* 消息数 = task_started 的 distinct turn_id(跨文件去重)。重放副本会让同一 turn_id 出现在多个文件,
|
|
115
|
+
* 故用全局 Map<turn_id, minMs> 收集(取最早 timestamp = 真实发生时刻,早于任何重放副本),最后取 size。
|
|
104
116
|
*/
|
|
105
117
|
async function summarizeCodexSessionUsage(start, end) {
|
|
106
118
|
const codexDataDir = (0, paths_1.getCodexHome)();
|
|
107
119
|
const sessionsDir = node_path_1.default.join(codexDataDir, "sessions");
|
|
108
120
|
const files = await collectRolloutFiles(sessionsDir);
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
cacheReadInputTokens: 0,
|
|
115
|
-
matchedFileCount: files.length,
|
|
116
|
-
codexDataDir,
|
|
117
|
-
};
|
|
121
|
+
const turnMinMs = new Map();
|
|
122
|
+
let apiRequestCount = 0;
|
|
123
|
+
let inputTokens = 0;
|
|
124
|
+
let outputTokens = 0;
|
|
125
|
+
let cacheReadInputTokens = 0;
|
|
118
126
|
for (const filePath of files) {
|
|
119
127
|
try {
|
|
120
128
|
const content = await promises_1.default.readFile(filePath, "utf8");
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
const parsed = summarizeRollout(content, start, end);
|
|
130
|
+
for (const turn of parsed.turns) {
|
|
131
|
+
const prev = turnMinMs.get(turn.turnId);
|
|
132
|
+
if (prev === undefined || turn.ms < prev) {
|
|
133
|
+
turnMinMs.set(turn.turnId, turn.ms);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
apiRequestCount += parsed.apiRequestCount;
|
|
137
|
+
inputTokens += parsed.inputTokens;
|
|
138
|
+
outputTokens += parsed.outputTokens;
|
|
139
|
+
cacheReadInputTokens += parsed.cacheReadInputTokens;
|
|
127
140
|
}
|
|
128
141
|
catch {
|
|
129
142
|
continue;
|
|
130
143
|
}
|
|
131
144
|
}
|
|
132
|
-
return
|
|
145
|
+
return {
|
|
146
|
+
userMessageCount: turnMinMs.size,
|
|
147
|
+
apiRequestCount,
|
|
148
|
+
inputTokens,
|
|
149
|
+
outputTokens,
|
|
150
|
+
cacheReadInputTokens,
|
|
151
|
+
matchedFileCount: files.length,
|
|
152
|
+
codexDataDir,
|
|
153
|
+
};
|
|
133
154
|
}
|
|
134
155
|
/**
|
|
135
156
|
* 按天汇总 Codex session rollout 中的消息数、请求数和 token 用量。
|
|
157
|
+
*
|
|
158
|
+
* 消息数同 weekly:先全局 Map<turn_id, minMs> 去重,再按 minMs 的本地日归桶(保证 weekly = Σ daily、
|
|
159
|
+
* 且重放副本跨天不重复)。token 维度按 token_count 事件 timestamp 的本地日累加。
|
|
136
160
|
*/
|
|
137
161
|
async function summarizeCodexSessionUsageByDay(start, end) {
|
|
138
162
|
const codexDataDir = (0, paths_1.getCodexHome)();
|
|
139
163
|
const sessionsDir = node_path_1.default.join(codexDataDir, "sessions");
|
|
140
164
|
const files = await collectRolloutFiles(sessionsDir);
|
|
165
|
+
const turnMinMs = new Map();
|
|
141
166
|
const daily = new Map();
|
|
167
|
+
const ensureDay = (date) => {
|
|
168
|
+
let current = daily.get(date);
|
|
169
|
+
if (!current) {
|
|
170
|
+
current = {
|
|
171
|
+
date,
|
|
172
|
+
userMessageCount: 0,
|
|
173
|
+
apiRequestCount: 0,
|
|
174
|
+
inputTokens: 0,
|
|
175
|
+
outputTokens: 0,
|
|
176
|
+
cacheReadInputTokens: 0,
|
|
177
|
+
};
|
|
178
|
+
daily.set(date, current);
|
|
179
|
+
}
|
|
180
|
+
return current;
|
|
181
|
+
};
|
|
142
182
|
for (const filePath of files) {
|
|
143
183
|
let content = "";
|
|
144
184
|
try {
|
|
@@ -162,36 +202,36 @@ async function summarizeCodexSessionUsageByDay(start, end) {
|
|
|
162
202
|
continue;
|
|
163
203
|
}
|
|
164
204
|
const payload = record.payload;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
205
|
+
const ms = new Date(timestamp).getTime();
|
|
206
|
+
if (payload.type === "task_started" && typeof payload.turn_id === "string") {
|
|
207
|
+
if (Number.isFinite(ms)) {
|
|
208
|
+
const prev = turnMinMs.get(payload.turn_id);
|
|
209
|
+
if (prev === undefined || ms < prev) {
|
|
210
|
+
turnMinMs.set(payload.turn_id, ms);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
169
213
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
214
|
+
if (payload.type === "token_count") {
|
|
215
|
+
const usage = extractLastTokenUsage(payload);
|
|
216
|
+
if (usage) {
|
|
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));
|
|
220
|
+
current.apiRequestCount += 1;
|
|
221
|
+
current.inputTokens += netInputTokens;
|
|
222
|
+
current.outputTokens += getNumber(usage.output_tokens);
|
|
223
|
+
current.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
|
|
224
|
+
}
|
|
181
225
|
}
|
|
182
|
-
if (usage) {
|
|
183
|
-
current.apiRequestCount += 1;
|
|
184
|
-
current.inputTokens += getNumber(usage.input_tokens);
|
|
185
|
-
current.outputTokens += getNumber(usage.output_tokens);
|
|
186
|
-
current.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
|
|
187
|
-
}
|
|
188
|
-
daily.set(date, current);
|
|
189
226
|
}
|
|
190
227
|
catch {
|
|
191
228
|
continue;
|
|
192
229
|
}
|
|
193
230
|
}
|
|
194
231
|
}
|
|
232
|
+
for (const ms of turnMinMs.values()) {
|
|
233
|
+
ensureDay((0, time_1.localDateKey)(new Date(ms))).userMessageCount += 1;
|
|
234
|
+
}
|
|
195
235
|
return daily;
|
|
196
236
|
}
|
|
197
237
|
//# sourceMappingURL=codex-sessions.js.map
|