ccus-cli 0.2.7 → 0.2.8

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 文件读取时自动解压
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"));
@@ -44,6 +45,45 @@ function timestampInRange(timestamp, start, end) {
44
45
  const value = new Date(timestamp).getTime();
45
46
  return Number.isFinite(value) && value >= start.getTime() && value <= end.getTime();
46
47
  }
48
+ /**
49
+ * 找出 `<CODEX_HOME>/sessions` 中在指定时间范围内有活动的 rollout 文件。
50
+ *
51
+ * 只判断文件里是否存在范围内的记录,不过滤内容,导出时完整复制原始文件。
52
+ */
53
+ async function findActiveCodexSessionFiles(start, end) {
54
+ const sessionsDir = node_path_1.default.join((0, paths_1.getCodexHome)(), "sessions");
55
+ const files = await collectRolloutFiles(sessionsDir);
56
+ const result = [];
57
+ for (const filePath of files) {
58
+ try {
59
+ const content = await promises_1.default.readFile(filePath, "utf8");
60
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
61
+ let hasInRange = false;
62
+ for (const line of lines) {
63
+ try {
64
+ const record = JSON.parse(line);
65
+ if (isRecord(record) && timestampInRange(getString(record.timestamp), start, end)) {
66
+ hasInRange = true;
67
+ break;
68
+ }
69
+ }
70
+ catch {
71
+ continue;
72
+ }
73
+ }
74
+ if (hasInRange) {
75
+ result.push({
76
+ filePath,
77
+ relativePath: node_path_1.default.relative(sessionsDir, filePath),
78
+ });
79
+ }
80
+ }
81
+ catch {
82
+ continue;
83
+ }
84
+ }
85
+ return result;
86
+ }
47
87
  /**
48
88
  * 从 `token_count` 事件的 `info.last_token_usage` 取 token 用量对象(缺失返回 null)。
49
89
  * 必须用 last_token_usage(本次增量),不能用 total_token_usage(会话累计,会重复计)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
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": [