ccus-cli 0.1.8 → 0.1.10

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
@@ -8,6 +8,7 @@
8
8
  - `ccus export`:默认导出当前周数据包(gzip 压缩的 `.json.gz`),里面同时包含原始事件和按天维度的周汇总。
9
9
  - `ccus aggregate`:读取一个目录里的多人 export bundle(`.json.gz` 或 `.json`),输出明细、按天、按周三个 CSV。
10
10
  - `ccus aggregate serve`:同样以 bundle 目录为输入,启动本地多人 dashboard 页面,不落地任何文件。
11
+ - `ccus open`:用系统文件管理器打开 ccus 本地存储目录(事件日志、exports、dashboard 都在里面);加 `--print` 只输出目录路径、不打开。
11
12
  - `ccus update`:主动检查 npm 上是否有新版本,有则提示手动升级命令;`ccus --version` 查看当前版本。
12
13
 
13
14
  > **支持范围**:`ccus statusline emit` 依赖 Claude Code 的 statusLine 机制(从 `stdin` 读 JSON、向 `stdout` 回一行文本),**只在命令行版 Claude Code(CLI / 终端)里生效**。
@@ -165,11 +166,12 @@ ccus aggregate serve --input-dir ./team-exports
165
166
 
166
167
  - 输入目录放很多通过 `ccus export` 导出的 bundle 文件,`.json.gz`(gzip 压缩)与明文 `.json` 都能识别,gzip 文件读取时自动解压
167
168
  - `aggregate` 目前只接受 `schemaVersion: 6` 的 bundle;旧导出请先用当前版本重新 `ccus export`
169
+ - 同一个人在多台电脑上各自导出 bundle 时会自动合并去重:累加类指标(token、消息数、采样数等)按「同人同天 / 同人同周取 `generatedAt` 最新的那份导出」保留,不相加,避免同一台机器重复导出或周与周重叠造成翻倍;usage(5h / 7d)从选中那份的 `rawEvents` 按真实时间戳重算(peak 取 max、latest 取最新)
168
170
  - `ccus aggregate --input-dir DIR --out-dir DIR`
169
171
  - 输出三个文件:
170
- - `detail.csv`:来自每个 bundle 的 `rawEvents`,`contextUsedM` / `contextMaxM` 为单条事件的 context window token;另附带 `inputTokensM` / `outputTokensM` / `cacheReadInputTokensM`(按 `date` 取自当天 `dailySummaries` 的日总量,同一天多行会重复,不能按行求和)
171
- - `daily.csv`:直接来自每个 bundle 的 `dailySummaries`
172
- - `weekly.csv`:直接来自每个 bundle 的 `weeklySummary`
172
+ - `detail.csv`:来自 winner bundle 的 `rawEvents`(同人同天只展开最新那份的事件),`contextUsedM` / `contextMaxM` 为单条事件的 context window token;另附带 `inputTokensM` / `outputTokensM` / `cacheReadInputTokensM`(按 `date` 取自当天 `dailySummaries` 的日总量,同一天多行会重复,不能按行求和)
173
+ - `daily.csv`:同人同天取最新导出 bundle 的 `dailySummaries`,usage 从该 bundle 当天事件重算
174
+ - `weekly.csv`:同人同周取最新导出 bundle 的 `weeklySummary`,usage 从该 bundle 全部事件重算
173
175
  - CSV 里所有以 token 计的列(context 与 in/out/cache)都以百万(M)为单位(原始值除以 1,000,000),列名统一带 `M` 后缀;`contextWindowPct` 仍是百分比
174
176
  - 想直接查看团队多人 dashboard,可以用 `ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]`:默认监听 `127.0.0.1` 上的随机端口,启动后会自动用系统默认浏览器打开,每次请求实时读取目录里的 bundle,不写入任何文件
175
177
 
package/dist/cli.js CHANGED
@@ -59,7 +59,7 @@ const update_check_1 = require("./lib/update-check");
59
59
  const version_1 = require("./lib/version");
60
60
  /** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
61
61
  function printHelp() {
62
- process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH]\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
62
+ process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH]\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus open [--data-dir PATH] [--print]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
63
63
  }
64
64
  /** 一个轻量的参数解析器,当前命令面不复杂,没必要引入额外依赖。 */
65
65
  function parseOptions(args) {
@@ -508,6 +508,25 @@ async function handleAggregateServe(options) {
508
508
  process.once("SIGTERM", shutdown);
509
509
  });
510
510
  }
511
+ /**
512
+ * `ccus open`:用系统文件管理器打开 ccus 本地存储目录(事件日志、exports、dashboard 都在里面)。
513
+ *
514
+ * 默认目录由 `getDefaultDataDir` 推导,可用 `--data-dir` 覆盖。
515
+ * 加 `--print` 只把目录路径打到 stdout、不真正打开,方便在脚本里取路径。
516
+ */
517
+ async function handleOpenDataDir(options) {
518
+ const dataDir = getDataDir(options);
519
+ (0, debug_1.debugLog)("open", "resolved data dir", { dataDir, print: getBooleanOption(options, "print") });
520
+ if (getBooleanOption(options, "print")) {
521
+ process.stdout.write(`${dataDir}\n`);
522
+ return;
523
+ }
524
+ // 目录可能还没被 statusline 创建过,先确保存在,否则系统打开会失败。
525
+ const fs = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
526
+ await fs.mkdir(dataDir, { recursive: true });
527
+ await (0, open_1.openPath)(dataDir);
528
+ process.stdout.write(`${dataDir}\n`);
529
+ }
511
530
  /** 历史上被移除的导出格式 token,作为位置参数出现时要明确报错而不是当成 range。 */
512
531
  const REMOVED_EXPORT_FORMAT_TOKENS = new Set(["csv", "jsonl", "raw"]);
513
532
  /**
@@ -532,11 +551,23 @@ function resolveExportOptions(action, args, rest) {
532
551
  }
533
552
  return options;
534
553
  }
554
+ /** 用 readline 向用户提问,返回用户输入的一行文本。 */
555
+ async function prompt(question) {
556
+ const rl = (await Promise.resolve().then(() => __importStar(require("node:readline")))).createInterface({
557
+ input: process.stdin,
558
+ output: process.stderr,
559
+ });
560
+ return new Promise((resolve) => {
561
+ rl.question(question, (answer) => {
562
+ rl.close();
563
+ resolve(answer.trim());
564
+ });
565
+ });
566
+ }
535
567
  /**
536
568
  * `ccus update`:用户主动检查更新(绕过 24h 节流)。
537
569
  *
538
- * 根据当前的产品选择,这里只做「检查 + 提示」,不替用户执行全局安装。
539
- * 有新版本时打印应当手动运行的 `npm i -g` 命令。
570
+ * 发现新版本时交互式询问用户是否立即安装;输入 y/Y 则执行 `npm i -g ccus-cli@latest`。
540
571
  */
541
572
  async function handleUpdate(options) {
542
573
  const dataDir = getDataDir(options);
@@ -552,11 +583,30 @@ async function handleUpdate(options) {
552
583
  }
553
584
  // 顺手刷新缓存,让 statusline 标记和这次检查结果保持一致。
554
585
  await (0, update_check_1.performUpdateCheck)(dataDir);
555
- if ((0, version_1.isNewerVersion)(latest, current)) {
556
- process.stdout.write(`发现新版本:v${current} -> v${latest}\n运行以下命令升级:\n npm i -g ccus-cli@latest\n`);
557
- }
558
- else {
586
+ if (!(0, version_1.isNewerVersion)(latest, current)) {
559
587
  process.stdout.write(`已是最新版本 v${current}\n`);
588
+ return;
589
+ }
590
+ process.stdout.write(`发现新版本:v${current} -> v${latest}\n`);
591
+ let answer;
592
+ try {
593
+ answer = await prompt("立即升级?[y/N] ");
594
+ }
595
+ catch {
596
+ // stdin 不可交互(如管道),退回纯提示。
597
+ process.stdout.write(`运行以下命令升级:\n npm i -g ccus-cli@latest\n`);
598
+ return;
599
+ }
600
+ if (answer.toLowerCase() !== "y") {
601
+ process.stdout.write(`已取消。如需手动升级:\n npm i -g ccus-cli@latest\n`);
602
+ return;
603
+ }
604
+ process.stdout.write("正在升级…\n");
605
+ const { spawnSync } = await Promise.resolve().then(() => __importStar(require("node:child_process")));
606
+ const result = spawnSync("npm", ["i", "-g", "ccus-cli@latest"], { stdio: "inherit", shell: true });
607
+ if (result.status !== 0) {
608
+ process.stdout.write("升级失败,请手动运行:\n npm i -g ccus-cli@latest\n");
609
+ process.exitCode = 1;
560
610
  }
561
611
  }
562
612
  /**
@@ -583,6 +633,10 @@ async function main(args = process.argv.slice(2)) {
583
633
  process.stdout.write(`${(0, version_1.getCurrentVersion)()}\n`);
584
634
  return;
585
635
  }
636
+ if (group === "open") {
637
+ await handleOpenDataDir(parseOptions(args.slice(1)));
638
+ return;
639
+ }
586
640
  if (group === "update") {
587
641
  await handleUpdate(parseOptions(args.slice(1)));
588
642
  return;
@@ -110,75 +110,172 @@ async function loadWeeklyExportBundles(inputDir) {
110
110
  }
111
111
  return bundles;
112
112
  }
113
- /** 从 bundle.rawEvents 展开 detail.csv 明细。 */
113
+ /**
114
+ * 同一个人在多台电脑导出多个 bundle 时的合并策略。
115
+ *
116
+ * 累加类字段(token / 消息数 / 采样数等)怕重复计数(同一台机器重复导出、周与周重叠),
117
+ * 所以按「同人同天 / 同人同周取 generatedAt 最新的那份导出 bundle」去重,不相加。
118
+ * usage 是百分比快照、不是累加量,从选中那份 winner bundle 的 rawEvents 按真实时间戳重算
119
+ * (peak 取 max,latest 取时间戳最新),某指标在 rawEvents 里缺失时回退到 daySummary/weeklySummary 自带值。
120
+ */
121
+ /** bundle 的 personKey 解析结果做一次缓存,避免反复计算。 */
122
+ function bundlePersonKey(bundle) {
123
+ return toPersonKey(bundle.identity.gitUserEmail, bundle.identity.gitUserName);
124
+ }
125
+ /** 某天 daySummary 是否承载真实数据(用于优先选中有数据的那份 bundle,而非占位空天)。 */
126
+ function dayHasData(day) {
127
+ return day.sampleCount > 0 || day.userMessageCount > 0 || day.apiRequestCount > 0;
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
+ /** winner 比较:有数据优先,其次 generatedAt 较新,最后用 filePath 做稳定 tie-break。 */
135
+ function isBetterCandidate(nextHasData, nextGeneratedAt, nextFilePath, currentHasData, currentGeneratedAt, currentFilePath) {
136
+ if (nextHasData !== currentHasData) {
137
+ return nextHasData;
138
+ }
139
+ if (nextGeneratedAt !== currentGeneratedAt) {
140
+ return nextGeneratedAt > currentGeneratedAt;
141
+ }
142
+ return nextFilePath > currentFilePath;
143
+ }
144
+ /** 对每个 (personKey, date) 选出 generatedAt 最新、且尽量有数据的那份 bundle 当天数据。 */
145
+ function selectDailyWinners(bundles) {
146
+ const winners = new Map();
147
+ for (const { filePath, bundle } of bundles) {
148
+ const personKey = bundlePersonKey(bundle);
149
+ const generatedAt = bundle.generatedAt ?? "";
150
+ for (const day of bundle.dailySummaries) {
151
+ const key = `${personKey}|${day.date}`;
152
+ const current = winners.get(key);
153
+ if (!current || isBetterCandidate(dayHasData(day), generatedAt, filePath, dayHasData(current.day), current.generatedAt, current.filePath)) {
154
+ winners.set(key, { personKey, date: day.date, day, bundle, generatedAt, filePath });
155
+ }
156
+ }
157
+ }
158
+ return winners;
159
+ }
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
+ /** 把 bundle 的 rawEvents 计算成 StatuslineEvent 并按本地自然日分组,结果做缓存复用。 */
176
+ const bundleEventsCache = new WeakMap();
177
+ function bundleEventsByDate(bundle) {
178
+ const cached = bundleEventsCache.get(bundle);
179
+ if (cached) {
180
+ return cached;
181
+ }
182
+ const byDate = new Map();
183
+ for (const record of bundle.rawEvents.filter(isPersistedStatuslineEvent)) {
184
+ const event = (0, payload_1.computeStatuslineEvent)(record);
185
+ const dateKey = localDateKey(new Date(event.timestamp));
186
+ const list = byDate.get(dateKey);
187
+ if (list) {
188
+ list.push(event);
189
+ }
190
+ else {
191
+ byDate.set(dateKey, [event]);
192
+ }
193
+ }
194
+ bundleEventsCache.set(bundle, byDate);
195
+ return byDate;
196
+ }
197
+ /** 从一组事件按真实时间戳重算 5h / 7d 的 peak(max)与 latest(时间戳最新非空)。 */
198
+ function recomputeUsage(events) {
199
+ const newestFirst = [...events].sort((left, right) => new Date(right.timestamp).getTime() - new Date(left.timestamp).getTime());
200
+ const fiveValues = events.map((event) => event.usagePct).filter((value) => value !== null);
201
+ const sevenValues = events.map((event) => event.sevenDayUsagePct).filter((value) => value !== null);
202
+ return {
203
+ fiveHourPeakUsagePct: fiveValues.length > 0 ? (0, time_1.roundNumber)(Math.max(...fiveValues), 1) : null,
204
+ fiveHourLatestUsagePct: newestFirst.find((event) => event.usagePct !== null)?.usagePct ?? null,
205
+ sevenDayPeakUsagePct: sevenValues.length > 0 ? (0, time_1.roundNumber)(Math.max(...sevenValues), 1) : null,
206
+ sevenDayLatestUsagePct: newestFirst.find((event) => event.sevenDayUsagePct !== null)?.sevenDayUsagePct ?? null,
207
+ };
208
+ }
209
+ /** 从 winner bundle 的事件展开 detail.csv,同人同天只保留 winner,那份的当天 token 总量随行附带。 */
114
210
  function buildAggregatedDetailRows(bundles) {
211
+ const winners = selectDailyWinners(bundles);
115
212
  const rows = [];
116
- for (const { bundle } of bundles) {
117
- const personKey = toPersonKey(bundle.identity.gitUserEmail, bundle.identity.gitUserName);
118
- const tokensByDate = new Map(bundle.dailySummaries.map((day) => [day.date, day]));
119
- for (const record of bundle.rawEvents.filter(isPersistedStatuslineEvent)) {
120
- const event = (0, payload_1.computeStatuslineEvent)(record);
121
- const ts = new Date(event.timestamp);
122
- const dateKey = localDateKey(ts);
123
- const dayTokens = tokensByDate.get(dateKey);
213
+ for (const winner of winners.values()) {
214
+ const events = bundleEventsByDate(winner.bundle).get(winner.date) ?? [];
215
+ for (const event of events) {
124
216
  rows.push({
125
217
  ...event,
126
- personKey,
127
- weekKey: weekKey(ts),
128
- dateKey,
129
- inputTokens: dayTokens?.inputTokens ?? 0,
130
- outputTokens: dayTokens?.outputTokens ?? 0,
131
- cacheReadInputTokens: dayTokens?.cacheReadInputTokens ?? 0,
218
+ personKey: winner.personKey,
219
+ weekKey: weekKey(new Date(event.timestamp)),
220
+ dateKey: winner.date,
221
+ inputTokens: winner.day.inputTokens,
222
+ outputTokens: winner.day.outputTokens,
223
+ cacheReadInputTokens: winner.day.cacheReadInputTokens,
132
224
  });
133
225
  }
134
226
  }
135
227
  return rows.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
136
228
  }
137
- /** 直接从 bundle.dailySummaries 展开 daily.csv */
229
+ /** 展开 daily.csv:同人同天取 winner bundle 的累加值,usage 从该 bundle 当天事件重算。 */
138
230
  function buildAggregatedDailyRows(bundles) {
231
+ const winners = selectDailyWinners(bundles);
139
232
  const rows = [];
140
- for (const { bundle } of bundles) {
141
- const personKey = toPersonKey(bundle.identity.gitUserEmail, bundle.identity.gitUserName);
142
- for (const item of bundle.dailySummaries) {
143
- rows.push({
144
- personKey,
145
- date: item.date,
146
- userMessageCount: item.userMessageCount,
147
- apiRequestCount: item.apiRequestCount,
148
- inputTokens: item.inputTokens,
149
- outputTokens: item.outputTokens,
150
- cacheReadInputTokens: item.cacheReadInputTokens,
151
- sampleCount: item.sampleCount,
152
- fiveHourPeakUsagePct: item.fiveHourPeakUsagePct,
153
- fiveHourLatestUsagePct: item.fiveHourLatestUsagePct,
154
- sevenDayPeakUsagePct: item.sevenDayPeakUsagePct,
155
- sevenDayLatestUsagePct: item.sevenDayLatestUsagePct,
156
- uniqueSessions: item.uniqueSessions,
157
- uniqueWorkspaces: item.uniqueWorkspaces,
158
- });
159
- }
233
+ for (const winner of winners.values()) {
234
+ const day = winner.day;
235
+ const usage = recomputeUsage(bundleEventsByDate(winner.bundle).get(winner.date) ?? []);
236
+ rows.push({
237
+ personKey: winner.personKey,
238
+ date: day.date,
239
+ userMessageCount: day.userMessageCount,
240
+ apiRequestCount: day.apiRequestCount,
241
+ inputTokens: day.inputTokens,
242
+ outputTokens: day.outputTokens,
243
+ cacheReadInputTokens: day.cacheReadInputTokens,
244
+ sampleCount: day.sampleCount,
245
+ fiveHourPeakUsagePct: usage.fiveHourPeakUsagePct ?? day.fiveHourPeakUsagePct,
246
+ fiveHourLatestUsagePct: usage.fiveHourLatestUsagePct ?? day.fiveHourLatestUsagePct,
247
+ sevenDayPeakUsagePct: usage.sevenDayPeakUsagePct ?? day.sevenDayPeakUsagePct,
248
+ sevenDayLatestUsagePct: usage.sevenDayLatestUsagePct ?? day.sevenDayLatestUsagePct,
249
+ uniqueSessions: day.uniqueSessions,
250
+ uniqueWorkspaces: day.uniqueWorkspaces,
251
+ });
160
252
  }
161
253
  return rows.sort((left, right) => `${left.personKey}|${left.date}`.localeCompare(`${right.personKey}|${right.date}`));
162
254
  }
163
- /** 直接从 bundle.weeklySummary 展开 weekly.csv */
255
+ /** 展开 weekly.csv:同人同周取 winner bundle 的累加值,usage 从该 bundle 全部事件重算。 */
164
256
  function buildAggregatedWeeklyRows(bundles) {
165
- return bundles
166
- .map(({ bundle }) => ({
167
- personKey: toPersonKey(bundle.identity.gitUserEmail, bundle.identity.gitUserName),
168
- week: weekKey(new Date(bundle.range.start)),
169
- userMessageCount: bundle.weeklySummary.counts.userMessageCount,
170
- apiRequestCount: bundle.weeklySummary.counts.apiRequestCount,
171
- inputTokens: bundle.weeklySummary.tokens.inputTokens,
172
- outputTokens: bundle.weeklySummary.tokens.outputTokens,
173
- cacheReadInputTokens: bundle.weeklySummary.tokens.cacheReadInputTokens,
174
- sampleCount: bundle.weeklySummary.statusline.sampleCount,
175
- fiveHourPeakUsagePct: bundle.weeklySummary.statusline.fiveHourPeakUsagePct,
176
- fiveHourLatestUsagePct: bundle.weeklySummary.statusline.fiveHourLatestUsagePct,
177
- sevenDayPeakUsagePct: bundle.weeklySummary.statusline.sevenDayPeakUsagePct,
178
- sevenDayLatestUsagePct: bundle.weeklySummary.statusline.sevenDayLatestUsagePct,
179
- uniqueSessions: bundle.weeklySummary.statusline.uniqueSessions,
180
- uniqueWorkspaces: bundle.weeklySummary.statusline.uniqueWorkspaces,
181
- }))
182
- .sort((left, right) => `${left.personKey}|${left.week}`.localeCompare(`${right.personKey}|${right.week}`));
257
+ const winners = selectWeeklyWinners(bundles);
258
+ const rows = [];
259
+ for (const winner of winners.values()) {
260
+ const summary = winner.bundle.weeklySummary;
261
+ const usage = recomputeUsage([...bundleEventsByDate(winner.bundle).values()].flat());
262
+ 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,
277
+ });
278
+ }
279
+ return rows.sort((left, right) => `${left.personKey}|${left.week}`.localeCompare(`${right.personKey}|${right.week}`));
183
280
  }
184
281
  //# sourceMappingURL=aggregate.js.map
package/dist/lib/open.js CHANGED
@@ -1,18 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openPath = openPath;
3
4
  exports.openInBrowser = openInBrowser;
4
5
  const node_child_process_1 = require("node:child_process");
5
6
  /**
6
- * 用系统默认浏览器打开生成好的 dashboard。
7
+ * 用系统默认程序打开一个文件或目录:HTML 走默认浏览器,目录走文件管理器。
7
8
  *
8
9
  * 这里不拼接 shell 字符串,而是显式传参数,降低路径注入风险。
9
10
  */
10
- async function openInBrowser(filePath) {
11
+ async function openPath(target) {
11
12
  const command = process.platform === "win32"
12
- ? { program: "cmd", args: ["/c", "start", "", filePath] }
13
+ ? { program: "cmd", args: ["/c", "start", "", target] }
13
14
  : process.platform === "darwin"
14
- ? { program: "open", args: [filePath] }
15
- : { program: "xdg-open", args: [filePath] };
15
+ ? { program: "open", args: [target] }
16
+ : { program: "xdg-open", args: [target] };
16
17
  await new Promise((resolve, reject) => {
17
18
  const child = (0, node_child_process_1.spawn)(command.program, command.args, {
18
19
  detached: true,
@@ -23,4 +24,8 @@ async function openInBrowser(filePath) {
23
24
  resolve();
24
25
  });
25
26
  }
27
+ /** 用系统默认浏览器打开生成好的 dashboard。 */
28
+ async function openInBrowser(filePath) {
29
+ await openPath(filePath);
30
+ }
26
31
  //# sourceMappingURL=open.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {