ccus-cli 0.2.7 → 0.2.9

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
@@ -6,6 +6,7 @@
6
6
  - `ccus statusline emit`:读取 Claude Code statusline 通过 `stdin` 传入的 JSON,输出 statusline 文本,并写入本地日志(加 `--no-store` / `--no-log` 则只输出、不落盘)。
7
7
  - `ccus dashboard serve`:直接启动本地 Web 页面,不用先手动生成 HTML 文件。
8
8
  - `ccus export`:默认导出当前周数据包(gzip 压缩的 `.json.gz`),里面同时包含原始事件和按天维度的周汇总。
9
+ - `ccus sessions`:把指定周内活跃的 Claude 与 Codex 原始 session 完整打包成 zip。
9
10
  - `ccus aggregate`:读取一个目录里的多人 export bundle(`.json.gz` 或 `.json`),输出明细、按天、按周三个 CSV。
10
11
  - `ccus aggregate serve`:同样以 bundle 目录为输入,启动本地多人 dashboard 页面,不落地任何文件。
11
12
  - `ccus sync`:定时把当前周数据包导出并复制到一个目标目录(如团队共享盘),目标目录下按周自动建子目录;日常由 statusline 兜底触发,也可挂系统计划任务做到严格每天定时。
@@ -81,6 +82,7 @@ ccus install
81
82
  ccus export # 导出当前周数据包(this-week)
82
83
  ccus export lw # 导出上一整周(last-week,周一到周日)
83
84
  ccus export tw # 导出本周(等价于默认 ccus export)
85
+ ccus sessions # 打包本周活跃的 Claude 与 Codex 原始 session
84
86
 
85
87
  ccus dashboard serve # 启动本地页面,默认看本周(this-week)的 5 小时使用率曲线与每日用户消息数
86
88
 
@@ -138,6 +140,9 @@ ccus export --range last-week
138
140
  ccus export lw # 位置参数简写,等价于 --range last-week
139
141
  ccus export tw # 等价于 --range this-week
140
142
  ccus export --out ./alice_export_2026-05-26_to_2026-06-01.json # --out 指定非 .gz 路径时写明文 JSON
143
+ ccus sessions # 默认导出本周
144
+ ccus sessions lw # 导出上一整周
145
+ ccus sessions --out ./sessions.zip
141
146
  ccus aggregate --input-dir ./team-exports --out-dir ./team-report
142
147
  ccus aggregate serve --input-dir ./team-exports
143
148
  ```
@@ -169,6 +174,13 @@ ccus aggregate serve --input-dir ./team-exports
169
174
  - `dailySummaries` 会按每天输出消息数、请求数、token 和当天 statusline usage 摘要
170
175
  - 不再支持其它导出格式
171
176
 
177
+ ### 原始 session 打包
178
+
179
+ - `ccus sessions` 默认打包本周;支持 `tw`、`lw`、`today`、`5h` 等与 `export` 相同的范围写法。
180
+ - 只选择文件中至少一条记录落入时间窗的 session,但 zip 内保存完整原始文件,不裁剪内容。
181
+ - Claude 文件保持 `<projectDir>/<sessionId>.jsonl` 路径;Codex rollout 保持 `codex/<CODEX_HOME 下 sessions 的相对路径>`。
182
+ - 默认写入 `<data-dir>/sessions/projects_<start>_<end>_<gitUserName>.zip`;`--out` 可指定路径。
183
+
172
184
  ## 多人汇总
173
185
 
174
186
  - 输入目录放很多通过 `ccus export` 导出的 bundle 文件,`.json.gz`(gzip 压缩)与明文 `.json` 都能识别,gzip 文件读取时自动解压
@@ -299,7 +311,7 @@ ccus install --codex --uninstall # 移除 ccus 的 Stop hook
299
311
  - **定时同步**:Stop hook 还兜底触发 `ccus sync`(与 Claude statusline 对称)——配过 `ccus sync config --target` 后,Codex 每 turn 结束都会检查 3h 周期,到期自动 export + 复制 bundle(含 Codex token/消息)到目标目录。只用 Codex、不开 Claude Code 时也能自动同步。
300
312
  - **缓存节流**:Stop 每 turn 触发,额度按 5 分钟 TTL 缓存(`codex-quota-cache.json`),命中秒回不 spawn;过期才拉一次(带 ~10s 超时),失败回退旧缓存。
301
313
  - **字段映射**:app-server 返回 `primary`(5h)/ `secondary`(weekly)两窗口,取各自的 `usedPercent`(驼峰,clamp 0–100);ccus 填进 `rate_limits.five_hour` / `seven_day` 的 `used_percentage`,`computeStatuslineEvent` 读时自动算出 usage。
302
- - **token / 消息 / 额度都进 export / aggregate**:Codex 的 token、用户消息数、API 请求数从 `<CODEX_HOME>/sessions` 的 rollout 统计,进 `ccus export` 的 `weeklySummary.codex` / `dailySummaries[].codex` 段;Stop 落盘的额度快照(`source="codex"` 事件)也进 export/aggregate——export bundle 里 Codex 额度单列到 codex 段、与 Claude 分开看,aggregate daily/weekly CSV 则把 Codex **叠加进 Claude 主字段**合计(累加量相加、额度 peak 取两源 max、latest 两源相加、7d 累计含两源),不再单列 `codex*` 列;detail.csv 的 `source` 列区分来源。
314
+ - **token / 消息 / 额度都进 export / aggregate**:Codex 的 token、用户消息数、API 请求数从 `<CODEX_HOME>/sessions` 的 rollout 统计,并排除 `source.subagent.other="guardian"` 的 Codex Desktop 内部安全审查会话,进 `ccus export` 的 `weeklySummary.codex` / `dailySummaries[].codex` 段;Stop 落盘的额度快照(`source="codex"` 事件)也进 export/aggregate——export bundle 里 Codex 额度单列到 codex 段、与 Claude 分开看,aggregate daily/weekly CSV 则把 Codex **叠加进 Claude 主字段**合计(累加量相加、额度 peak 取两源 max、latest 两源相加、7d 累计含两源读数),不再单列 `codex*` 列;detail.csv 的 `source` 列区分来源。
303
315
  - **Windows**:`install --codex` 在 Windows 写 `ccus.cmd __codex-hook`(npm 全局装会生成 `ccus.cmd`);Windows 上 Stop hook 偶发收到非法 JSON(已知 bug #23784),ccus 容错按无 payload 处理、仍照常拉额度 + 落盘。
304
316
 
305
317
  > 该路径依赖 Codex 内部 app-server 协议(`account/rateLimits/read` 的返回结构)与 hooks payload schema,随版本变;解析层宽松,字段缺失返回 null、失败静默。
package/dist/cli.js CHANGED
@@ -67,7 +67,7 @@ const update_check_1 = require("./lib/update-check");
67
67
  const version_1 = require("./lib/version");
68
68
  /** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
69
69
  function printHelp() {
70
- process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH] (默认装 Claude statusLine;--codex 改写 Codex 的 ~/.codex/config.toml notify,配 --uninstall 移除)\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 sessions [RANGE] [--out FILE] [--data-dir PATH] (把 ~/.claude/projects 本周活跃 session 打包成 zip,名如 projects_<dates>_<user>.zip)\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 sync [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api status [--data-dir PATH]\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`);
70
+ process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH] (默认装 Claude statusLine;--codex 改写 Codex 的 ~/.codex/config.toml notify,配 --uninstall 移除)\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 sessions [RANGE] [--out FILE] [--data-dir PATH] (把 Claude Codex 的活跃 session 打包成 zip,默认本周)\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 sync [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api status [--data-dir PATH]\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`);
71
71
  }
72
72
  /** 一个轻量的参数解析器,当前命令面不复杂,没必要引入额外依赖。 */
73
73
  function parseOptions(args) {
@@ -656,9 +656,9 @@ function resolveExportOptions(action, args, rest) {
656
656
  return options;
657
657
  }
658
658
  /**
659
- * `ccus sessions`:把 ~/.claude/projects 中在指定时间范围内有活动的 session 文件打包成 zip。
659
+ * `ccus sessions`:把 Claude 与 Codex 中在指定时间范围内有活动的 session 文件打包成 zip。
660
660
  *
661
- * zip 内部结构保持 <projectDir>/<sessionId>.jsonl 层级(路径分隔符统一用 /)。
661
+ * Claude 内部结构保持 <projectDir>/<sessionId>.jsonl,Codex 保持 codex/<sessions 相对路径>。
662
662
  * 文件名格式:projects_<start>_<end>_<gitUserName>.zip。
663
663
  * 默认输出到 <data-dir>/sessions/,加 --out 可指定完整路径。
664
664
  * 位置参数作为 range 简写,例如 `ccus sessions lw` 等价于 `--range last-week`。
@@ -670,14 +670,22 @@ async function handleSessions(options) {
670
670
  const now = new Date();
671
671
  const window = (0, time_1.expandToFullWeekWindow)((0, time_1.resolveRange)(range, now));
672
672
  (0, debug_1.debugLog)("sessions", "range resolved", { range, label: window.label, start: window.start.toISOString(), end: window.end.toISOString() });
673
- const sessions = await (0, claude_1.findActiveSessionFiles)(window.start, window.end);
674
- (0, debug_1.debugLog)("sessions", "active sessions found", { count: sessions.length });
673
+ const [claudeSessions, codexSessions] = await Promise.all([
674
+ (0, claude_1.findActiveSessionFiles)(window.start, window.end),
675
+ (0, codex_sessions_1.findActiveCodexSessionFiles)(window.start, window.end),
676
+ ]);
677
+ (0, debug_1.debugLog)("sessions", "active sessions found", { claude: claudeSessions.length, codex: codexSessions.length });
675
678
  const fsRead = (await Promise.resolve().then(() => __importStar(require("node:fs/promises")))).readFile;
676
679
  const { buildZipBuffer } = await Promise.resolve().then(() => __importStar(require("./lib/zip")));
677
- const entries = await Promise.all(sessions.map(async (session) => ({
680
+ const claudeEntries = claudeSessions.map(async (session) => ({
678
681
  name: `${session.projectDir.replaceAll("\\", "/")}/${session.sessionId}.jsonl`,
679
682
  data: await fsRead(session.filePath),
680
- })));
683
+ }));
684
+ const codexEntries = codexSessions.map(async (session) => ({
685
+ name: `codex/${session.relativePath.replaceAll("\\", "/")}`,
686
+ data: await fsRead(session.filePath),
687
+ }));
688
+ const entries = await Promise.all([...claudeEntries, ...codexEntries]);
681
689
  const zipBuffer = await buildZipBuffer(entries);
682
690
  const gitIdentity = await (0, git_1.readGitIdentity)();
683
691
  const userName = (0, time_1.formatGitEmailFilePrefix)(gitIdentity.userEmail) ?? "unknown";
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findActiveCodexSessionFiles = findActiveCodexSessionFiles;
6
7
  exports.summarizeCodexSessionUsage = summarizeCodexSessionUsage;
7
8
  exports.summarizeCodexSessionUsageByDay = summarizeCodexSessionUsageByDay;
8
9
  const promises_1 = __importDefault(require("node:fs/promises"));
@@ -18,6 +19,29 @@ function getNumber(value) {
18
19
  function getString(value) {
19
20
  return typeof value === "string" && value.length > 0 ? value : null;
20
21
  }
22
+ /** Guardian 是 Codex Desktop 的动作安全审查子代理,不代表用户请求或业务模型调用。 */
23
+ function isGuardianRollout(content) {
24
+ let offset = 0;
25
+ while (offset < content.length) {
26
+ const newline = content.indexOf("\n", offset);
27
+ const line = content.slice(offset, newline === -1 ? content.length : newline).trim();
28
+ offset = newline === -1 ? content.length : newline + 1;
29
+ try {
30
+ const record = JSON.parse(line);
31
+ if (!isRecord(record) || record.type !== "session_meta" || !isRecord(record.payload)) {
32
+ continue;
33
+ }
34
+ const source = record.payload.source;
35
+ return isRecord(source)
36
+ && isRecord(source.subagent)
37
+ && source.subagent.other === "guardian";
38
+ }
39
+ catch {
40
+ continue;
41
+ }
42
+ }
43
+ return false;
44
+ }
21
45
  async function collectRolloutFiles(directoryPath) {
22
46
  try {
23
47
  const entries = await promises_1.default.readdir(directoryPath, { withFileTypes: true });
@@ -44,6 +68,45 @@ function timestampInRange(timestamp, start, end) {
44
68
  const value = new Date(timestamp).getTime();
45
69
  return Number.isFinite(value) && value >= start.getTime() && value <= end.getTime();
46
70
  }
71
+ /**
72
+ * 找出 `<CODEX_HOME>/sessions` 中在指定时间范围内有活动的 rollout 文件。
73
+ *
74
+ * 只判断文件里是否存在范围内的记录,不过滤内容,导出时完整复制原始文件。
75
+ */
76
+ async function findActiveCodexSessionFiles(start, end) {
77
+ const sessionsDir = node_path_1.default.join((0, paths_1.getCodexHome)(), "sessions");
78
+ const files = await collectRolloutFiles(sessionsDir);
79
+ const result = [];
80
+ for (const filePath of files) {
81
+ try {
82
+ const content = await promises_1.default.readFile(filePath, "utf8");
83
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
84
+ let hasInRange = false;
85
+ for (const line of lines) {
86
+ try {
87
+ const record = JSON.parse(line);
88
+ if (isRecord(record) && timestampInRange(getString(record.timestamp), start, end)) {
89
+ hasInRange = true;
90
+ break;
91
+ }
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ }
97
+ if (hasInRange) {
98
+ result.push({
99
+ filePath,
100
+ relativePath: node_path_1.default.relative(sessionsDir, filePath),
101
+ });
102
+ }
103
+ }
104
+ catch {
105
+ continue;
106
+ }
107
+ }
108
+ return result;
109
+ }
47
110
  /**
48
111
  * 从 `token_count` 事件的 `info.last_token_usage` 取 token 用量对象(缺失返回 null)。
49
112
  * 必须用 last_token_usage(本次增量),不能用 total_token_usage(会话累计,会重复计)。
@@ -110,6 +173,7 @@ function summarizeRollout(content, start, end) {
110
173
  }
111
174
  /**
112
175
  * 从 Codex 本地 session rollout(<CODEX_HOME>/sessions 下递归的 .jsonl)统计消息数、请求数和 token 用量。
176
+ * Codex Desktop 的 guardian 安全审查 rollout 整体排除,避免把内部审批轮次计为用户使用量。
113
177
  *
114
178
  * 消息数 = task_started 的 distinct turn_id(跨文件去重)。重放副本会让同一 turn_id 出现在多个文件,
115
179
  * 故用全局 Map<turn_id, minMs> 收集(取最早 timestamp = 真实发生时刻,早于任何重放副本),最后取 size。
@@ -126,6 +190,9 @@ async function summarizeCodexSessionUsage(start, end) {
126
190
  for (const filePath of files) {
127
191
  try {
128
192
  const content = await promises_1.default.readFile(filePath, "utf8");
193
+ if (isGuardianRollout(content)) {
194
+ continue;
195
+ }
129
196
  const parsed = summarizeRollout(content, start, end);
130
197
  for (const turn of parsed.turns) {
131
198
  const prev = turnMinMs.get(turn.turnId);
@@ -154,6 +221,7 @@ async function summarizeCodexSessionUsage(start, end) {
154
221
  }
155
222
  /**
156
223
  * 按天汇总 Codex session rollout 中的消息数、请求数和 token 用量。
224
+ * 与周汇总一致,排除 Codex Desktop 的 guardian 安全审查 rollout。
157
225
  *
158
226
  * 消息数同 weekly:先全局 Map<turn_id, minMs> 去重,再按 minMs 的本地日归桶(保证 weekly = Σ daily、
159
227
  * 且重放副本跨天不重复)。token 维度按 token_count 事件 timestamp 的本地日累加。
@@ -187,6 +255,9 @@ async function summarizeCodexSessionUsageByDay(start, end) {
187
255
  catch {
188
256
  continue;
189
257
  }
258
+ if (isGuardianRollout(content)) {
259
+ continue;
260
+ }
190
261
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
191
262
  for (const line of lines) {
192
263
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "build": "tsc -p tsconfig.json && node scripts/copy-vendor.js",
17
17
  "prepublishOnly": "npm run build",
18
18
  "test": "node --test --test-concurrency 1 \"dist/test/**/*.test.js\"",
19
- "test:src": "node --import tsx --test --test-concurrency 1 src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts src/test/time.test.ts src/test/update-check.test.ts src/test/sync.test.ts src/test/scheduler.test.ts src/test/api-mode.test.ts src/test/codex-fetcher.test.ts src/test/codex-sessions.test.ts src/test/codex-install.test.ts src/test/cli-codex-notify.test.ts src/test/cli-codex-hook.test.ts src/test/cli-install-codex.test.ts",
19
+ "test:src": "node --import tsx --test --test-concurrency 1 src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts src/test/time.test.ts src/test/update-check.test.ts src/test/sync.test.ts src/test/scheduler.test.ts src/test/api-mode.test.ts src/test/codex-fetcher.test.ts src/test/codex-sessions.test.ts src/test/sessions.test.ts src/test/codex-install.test.ts src/test/cli-codex-notify.test.ts src/test/cli-codex-hook.test.ts src/test/cli-install-codex.test.ts",
20
20
  "smoke:api": "node scripts/smoke-api.mjs"
21
21
  },
22
22
  "keywords": [