ccus-cli 0.1.12 → 0.1.13

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
@@ -171,12 +171,12 @@ ccus aggregate serve --input-dir ./team-exports
171
171
 
172
172
  - 输入目录放很多通过 `ccus export` 导出的 bundle 文件,`.json.gz`(gzip 压缩)与明文 `.json` 都能识别,gzip 文件读取时自动解压
173
173
  - `aggregate` 目前只接受 `schemaVersion: 6` 的 bundle;旧导出请先用当前版本重新 `ccus export`
174
- - 同一个人在多台电脑上各自导出 bundle 时会自动合并去重:累加类指标(token、消息数、采样数等)按「同人同天 / 同人同周取 `generatedAt` 最新的那份导出」保留,不相加,避免同一台机器重复导出或周与周重叠造成翻倍;usage(5h / 7d)从选中那份的 `rawEvents` 按真实时间戳重算(peak 取 max、latest 取最新)
174
+ - 同一个人在多台电脑上各自导出 bundle 时会自动合并去重:去重以**天**为粒度,对每个「同人同天」取 `generatedAt` 最新的那份导出,避免同一台机器重复导出或周与周重叠造成翻倍;**周汇总不取整周单份,而是把按天去重后的各天数据上卷累加**,所以多台电脑在不同天产生的用量会正确合进同一周。usage(5h / 7d)从选中事件按真实时间戳重算(peak 取 max、latest 取最新)
175
175
  - `ccus aggregate --input-dir DIR --out-dir DIR`
176
176
  - 输出三个文件:
177
177
  - `detail.csv`:来自 winner bundle 的 `rawEvents`(同人同天只展开最新那份的事件),`contextUsedM` / `contextMaxM` 为单条事件的 context window token;另附带 `inputTokensM` / `outputTokensM` / `cacheReadInputTokensM`(按 `date` 取自当天 `dailySummaries` 的日总量,同一天多行会重复,不能按行求和)
178
178
  - `daily.csv`:同人同天取最新导出 bundle 的 `dailySummaries`,usage 从该 bundle 当天事件重算
179
- - `weekly.csv`:同人同周取最新导出 bundle 的 `weeklySummary`,usage 从该 bundle 全部事件重算
179
+ - `weekly.csv`:把同人同周的各天 winner(已按天去重)上卷累加得到,usage 从该周全部 winner 天的事件重算
180
180
  - CSV 里所有以 token 计的列(context 与 in/out/cache)都以百万(M)为单位(原始值除以 1,000,000),列名统一带 `M` 后缀;`contextWindowPct` 仍是百分比
181
181
  - 想直接查看团队多人 dashboard,可以用 `ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]`:默认监听 `127.0.0.1` 上的随机端口,启动后会自动用系统默认浏览器打开,每次请求实时读取目录里的 bundle,不写入任何文件
182
182
 
@@ -126,11 +126,6 @@ function bundlePersonKey(bundle) {
126
126
  function dayHasData(day) {
127
127
  return day.sampleCount > 0 || day.userMessageCount > 0 || day.apiRequestCount > 0;
128
128
  }
129
- /** 整周 weeklySummary 是否承载真实数据。 */
130
- function weekHasData(bundle) {
131
- const summary = bundle.weeklySummary;
132
- return summary.statusline.sampleCount > 0 || summary.counts.userMessageCount > 0 || summary.counts.apiRequestCount > 0;
133
- }
134
129
  /** winner 比较:有数据优先,其次 generatedAt 较新,最后用 filePath 做稳定 tie-break。 */
135
130
  function isBetterCandidate(nextHasData, nextGeneratedAt, nextFilePath, currentHasData, currentGeneratedAt, currentFilePath) {
136
131
  if (nextHasData !== currentHasData) {
@@ -157,21 +152,6 @@ function selectDailyWinners(bundles) {
157
152
  }
158
153
  return winners;
159
154
  }
160
- /** 对每个 (personKey, week) 选出 generatedAt 最新、且尽量有数据的那份 bundle。 */
161
- function selectWeeklyWinners(bundles) {
162
- const winners = new Map();
163
- for (const { filePath, bundle } of bundles) {
164
- const personKey = bundlePersonKey(bundle);
165
- const week = weekKey(new Date(bundle.range.start));
166
- const key = `${personKey}|${week}`;
167
- const generatedAt = bundle.generatedAt ?? "";
168
- const current = winners.get(key);
169
- if (!current || isBetterCandidate(weekHasData(bundle), generatedAt, filePath, weekHasData(current.bundle), current.generatedAt, current.filePath)) {
170
- winners.set(key, { personKey, week, bundle, generatedAt, filePath });
171
- }
172
- }
173
- return winners;
174
- }
175
155
  /** 把 bundle 的 rawEvents 计算成 StatuslineEvent 并按本地自然日分组,结果做缓存复用。 */
176
156
  const bundleEventsCache = new WeakMap();
177
157
  function bundleEventsByDate(bundle) {
@@ -252,28 +232,78 @@ function buildAggregatedDailyRows(bundles) {
252
232
  }
253
233
  return rows.sort((left, right) => `${left.personKey}|${left.date}`.localeCompare(`${right.personKey}|${right.date}`));
254
234
  }
255
- /** 展开 weekly.csv:同人同周取 winner bundle 的累加值,usage 从该 bundle 全部事件重算。 */
235
+ /** 周级 usage 回退:rawEvents 缺失时从各天 daySummary 自带值取 peak(max)/ latest(date 最新非空)。 */
236
+ function fallbackWeeklyUsage(days) {
237
+ const byDateDesc = [...days].sort((left, right) => right.date.localeCompare(left.date));
238
+ const fivePeaks = days.map((day) => day.fiveHourPeakUsagePct).filter((value) => value !== null);
239
+ const sevenPeaks = days.map((day) => day.sevenDayPeakUsagePct).filter((value) => value !== null);
240
+ return {
241
+ fiveHourPeakUsagePct: fivePeaks.length > 0 ? (0, time_1.roundNumber)(Math.max(...fivePeaks), 1) : null,
242
+ fiveHourLatestUsagePct: byDateDesc.find((day) => day.fiveHourLatestUsagePct !== null)?.fiveHourLatestUsagePct ?? null,
243
+ sevenDayPeakUsagePct: sevenPeaks.length > 0 ? (0, time_1.roundNumber)(Math.max(...sevenPeaks), 1) : null,
244
+ sevenDayLatestUsagePct: byDateDesc.find((day) => day.sevenDayLatestUsagePct !== null)?.sevenDayLatestUsagePct ?? null,
245
+ };
246
+ }
247
+ /**
248
+ * 展开 weekly.csv:一个人同一周有多份 bundle(多台电脑各导出)时,不取单独一份的整周汇总,
249
+ * 而是复用按天去重后的 daily winner(每天选有数据的那份),按 (person, 周) 把每天的 token / 计数累加上卷,
250
+ * usage 从该周所有 winner 天的事件重算(peak 取 max、latest 取时间戳最新),缺失时回退到 daySummary 自带值。
251
+ */
256
252
  function buildAggregatedWeeklyRows(bundles) {
257
- const winners = selectWeeklyWinners(bundles);
253
+ const dailyWinners = selectDailyWinners(bundles);
254
+ const groups = new Map();
255
+ for (const winner of dailyWinners.values()) {
256
+ const week = weekKey(new Date(winner.bundle.range.start));
257
+ const key = `${winner.personKey}|${week}`;
258
+ let acc = groups.get(key);
259
+ if (!acc) {
260
+ acc = {
261
+ personKey: winner.personKey,
262
+ week,
263
+ userMessageCount: 0,
264
+ apiRequestCount: 0,
265
+ inputTokens: 0,
266
+ outputTokens: 0,
267
+ cacheReadInputTokens: 0,
268
+ sampleCount: 0,
269
+ uniqueSessions: 0,
270
+ uniqueWorkspaces: 0,
271
+ days: [],
272
+ events: [],
273
+ };
274
+ groups.set(key, acc);
275
+ }
276
+ const day = winner.day;
277
+ acc.userMessageCount += day.userMessageCount;
278
+ acc.apiRequestCount += day.apiRequestCount;
279
+ acc.inputTokens += day.inputTokens;
280
+ acc.outputTokens += day.outputTokens;
281
+ acc.cacheReadInputTokens += day.cacheReadInputTokens;
282
+ acc.sampleCount += day.sampleCount;
283
+ acc.uniqueSessions += day.uniqueSessions;
284
+ acc.uniqueWorkspaces += day.uniqueWorkspaces;
285
+ acc.days.push(day);
286
+ acc.events.push(...(bundleEventsByDate(winner.bundle).get(winner.date) ?? []));
287
+ }
258
288
  const rows = [];
259
- for (const winner of winners.values()) {
260
- const summary = winner.bundle.weeklySummary;
261
- const usage = recomputeUsage([...bundleEventsByDate(winner.bundle).values()].flat());
289
+ for (const acc of groups.values()) {
290
+ const usage = recomputeUsage(acc.events);
291
+ const fallback = fallbackWeeklyUsage(acc.days);
262
292
  rows.push({
263
- personKey: winner.personKey,
264
- week: winner.week,
265
- userMessageCount: summary.counts.userMessageCount,
266
- apiRequestCount: summary.counts.apiRequestCount,
267
- inputTokens: summary.tokens.inputTokens,
268
- outputTokens: summary.tokens.outputTokens,
269
- cacheReadInputTokens: summary.tokens.cacheReadInputTokens,
270
- sampleCount: summary.statusline.sampleCount,
271
- fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ?? summary.statusline.fiveHourPeakUsagePct,
272
- fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? summary.statusline.fiveHourLatestUsagePct,
273
- sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? summary.statusline.sevenDayPeakUsagePct,
274
- sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? summary.statusline.sevenDayLatestUsagePct,
275
- uniqueSessions: summary.statusline.uniqueSessions,
276
- uniqueWorkspaces: summary.statusline.uniqueWorkspaces,
293
+ personKey: acc.personKey,
294
+ week: acc.week,
295
+ userMessageCount: acc.userMessageCount,
296
+ apiRequestCount: acc.apiRequestCount,
297
+ inputTokens: acc.inputTokens,
298
+ outputTokens: acc.outputTokens,
299
+ cacheReadInputTokens: acc.cacheReadInputTokens,
300
+ sampleCount: acc.sampleCount,
301
+ fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ?? fallback.fiveHourPeakUsagePct,
302
+ fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? fallback.fiveHourLatestUsagePct,
303
+ sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? fallback.sevenDayPeakUsagePct,
304
+ sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? fallback.sevenDayLatestUsagePct,
305
+ uniqueSessions: acc.uniqueSessions,
306
+ uniqueWorkspaces: acc.uniqueWorkspaces,
277
307
  });
278
308
  }
279
309
  return rows.sort((left, right) => `${left.personKey}|${left.week}`.localeCompare(`${right.personKey}|${right.week}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {