ccus-cli 0.2.27 → 0.2.29
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 +5 -3
- package/dist/cli.js +15 -1
- package/dist/lib/paths.js +32 -4
- package/dist/lib/sync.js +9 -1
- package/dist/lib/time.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
206
206
|
|
|
207
207
|
- `ccus sessions` 默认打包本周;支持 `tw`、`lw`、`today`、`5h` 等与 `export` 相同的范围写法。
|
|
208
208
|
- 只选择文件中至少一条记录落入时间窗的 session,但 zip 内保存完整原始文件,不裁剪内容。
|
|
209
|
-
- Claude 文件保持 `<projectDir>/<sessionId>.jsonl` 路径;Codex rollout 合并 `<CODEX_HOME>/sessions` 与 `%APPDATA%/orca/codex-
|
|
209
|
+
- Claude 文件保持 `<projectDir>/<sessionId>.jsonl` 路径;Codex rollout 合并 `<CODEX_HOME>/sessions`、`%APPDATA%/orca/codex-runtime-home/home/sessions` 与 `%APPDATA%/orca/codex-accounts/<账号 ID>/home/sessions`,自动发现所有账号,按相对路径和 JSONL 事件去重后保持 `codex/<sessions 相对路径>`。
|
|
210
210
|
- 默认写入 `<data-dir>/sessions/projects_<start>_<end>_<gitUserName>.zip`;`--out` 可指定路径。
|
|
211
211
|
|
|
212
212
|
## 多人汇总
|
|
@@ -260,6 +260,7 @@ ccus sync config --suffix laptop # 给目标文件名加机器
|
|
|
260
260
|
ccus sync config --no-suffix # 移除已配置的机器后缀
|
|
261
261
|
ccus sync config # 不带参数:打印当前配置
|
|
262
262
|
ccus sync # 用已存配置立即同步一次
|
|
263
|
+
ccus sync --week 2026-09-28 # 重新同步该日期所在整周,仅本次生效
|
|
263
264
|
ccus sync install # 注册系统调度器:每周五 18:00 自动同步(Windows 直接创建计划任务)
|
|
264
265
|
ccus sync uninstall # 卸载系统调度器
|
|
265
266
|
ccus sync status # 查看目标目录、周期、上次同步时间、是否到期
|
|
@@ -271,7 +272,8 @@ ccus sync status # 查看目标目录、周期
|
|
|
271
272
|
- **周一归档上一周**:周一同步时会额外把刚结束的上一整周(`last-week`)导出并归档到对应的上一周子目录(周一是第一个能拿到完整上一周数据的日子);同一天内多次同步用 `sync-state.lastArchivedWeek` 去重,不重复归档。
|
|
272
273
|
- **复制语义**:本地 `data-dir/exports` 仍保留一份,目标目录再放一份,本地照旧可 `aggregate` / `dashboard`。导出产物与 `ccus export` 完全一致,目标目录可直接喂给 `ccus aggregate`。
|
|
273
274
|
- **机器后缀**:`ccus sync config --suffix laptop` 会在**目标目录副本**的文件名扩展名前加 `-laptop`(如 `…_to_2026-06-07-laptop.json.gz`),本地原文件名不变。同一个人多台电脑同步到同一目标目录时,靠后缀让各机器的文件互不覆盖、都保留下来,正好供 `ccus aggregate` 按 personKey 合并去重。用 `ccus sync config --no-suffix` 移除后缀。
|
|
274
|
-
-
|
|
275
|
+
- **补同步指定周**:`ccus sync --week YYYY-MM-DD` 接受周内任一天,按本地时区和已配置的统计日开始时间重新导出周一至周日的数据,再复制到已有目标目录。保留机器后缀,覆盖该周同名文件;不修改配置或自动同步状态,不额外归档上一周。不能与 `--target` / `--interval` / `--range` / `--suffix` / `--no-suffix` 混用。
|
|
276
|
+
- **配置与同步分离**:`ccus sync config` 只读写配置(`--target` / `--interval` / `--range` 写进数据目录下的 `sync-config.json`,可手编;不带参数则打印当前配置);`ccus sync` 按已存配置执行一次同步,直接传入配置参数时仍会先保存再同步。普通同步的上次时间记录在 `sync-state.json`。
|
|
275
277
|
- **周期**:默认 `3h`(每 3 小时最多同步一次的滚动周期);也支持 `daily`(按**自然日**判断——同一天内不重复同步,跨到下一天才再同步)与 `<N>h` / `<N>m` 的滚动周期。
|
|
276
278
|
|
|
277
279
|
### 两种触发方式
|
|
@@ -359,7 +361,7 @@ ccus install --codex --uninstall # 移除 ccus 的 Stop hook
|
|
|
359
361
|
- **定时同步**:Stop hook 还兜底触发 `ccus sync`(与 Claude statusline 对称)——配过 `ccus sync config --target` 后,Codex 每 turn 结束都会检查 3h 周期,到期自动 export + 复制 bundle(含 Codex token/消息)到目标目录。只用 Codex、不开 Claude Code 时也能自动同步。
|
|
360
362
|
- **缓存节流**:Stop 每 turn 触发,额度按 5 分钟 TTL 缓存(`codex-quota-cache.json`),新鲜缓存命中秒回不 spawn;过期后依次尝试 app-server 与 wham,两路失败返回空,不回退过期缓存。
|
|
361
363
|
- **字段映射**:app-server 返回 `primary`(5h)/ `secondary`(weekly)两窗口,取各自的 `usedPercent`(驼峰,clamp 0–100);ccus 填进 `rate_limits.five_hour` / `seven_day` 的 `used_percentage`,`computeStatuslineEvent` 读时自动算出 usage。
|
|
362
|
-
- **token / 消息 / 额度都进 export / aggregate**:Codex 的 token、用户消息数、API 请求数合并读取 `<CODEX_HOME>/sessions` 与 `%APPDATA%/orca/codex-
|
|
364
|
+
- **token / 消息 / 额度都进 export / aggregate**:Codex 的 token、用户消息数、API 请求数合并读取 `<CODEX_HOME>/sessions`、`%APPDATA%/orca/codex-runtime-home/home/sessions` 与 `%APPDATA%/orca/codex-accounts/<账号 ID>/home/sessions` 的 rollout;`CODEX_HOME` 指向 Orca runtime 或托管账号时仍扫描标准 `~/.codex/sessions`,其它自定义目录沿用原有覆盖语义。同一相对路径的副本按 JSONL 事件去重,并排除 `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` 列区分来源。
|
|
363
365
|
- **Windows**:`install --codex` 在 Windows 写 `ccus.cmd __codex-hook`(npm 全局装会生成 `ccus.cmd`);Windows 上 Stop hook 偶发收到非法 JSON(已知 bug #23784),ccus 容错按无 payload 处理、仍照常拉额度 + 落盘。
|
|
364
366
|
|
|
365
367
|
> 该路径依赖 Codex 内部 app-server 协议(`account/rateLimits/read` 的返回结构)与 hooks payload schema,随版本变;解析层宽松,字段缺失返回 null、失败静默。
|
package/dist/cli.js
CHANGED
|
@@ -72,7 +72,7 @@ const version_1 = require("./lib/version");
|
|
|
72
72
|
/** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
|
|
73
73
|
function printHelp() {
|
|
74
74
|
process.stdout.write("统计日配置:ccus config [--day-start 07:00] [--data-dir PATH]\n统计命令可用 --day-start HH:mm 临时覆盖,默认 07:00(本地时区)。\n\n");
|
|
75
|
-
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 sessions repair [codex|claude] [--min-gap 3h] [--dry-run] (按末条记录时间修复 session 修改时间,默认仅跨天)\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`);
|
|
75
|
+
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 sessions repair [codex|claude] [--min-gap 3h] [--dry-run] (按末条记录时间修复 session 修改时间,默认仅跨天)\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 [--week YYYY-MM-DD] [--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`);
|
|
76
76
|
}
|
|
77
77
|
/** 一个轻量的参数解析器,当前命令面不复杂,没必要引入额外依赖。 */
|
|
78
78
|
function parseOptions(args) {
|
|
@@ -905,6 +905,18 @@ async function handleBackgroundCheckUpdate(options) {
|
|
|
905
905
|
async function handleSync(options) {
|
|
906
906
|
const startedAt = process.hrtime.bigint();
|
|
907
907
|
const dataDir = getDataDir(options);
|
|
908
|
+
if (options.week !== undefined) {
|
|
909
|
+
const week = getStringOption(options, "week");
|
|
910
|
+
if (!week)
|
|
911
|
+
throw new Error("--week 缺少日期,用法:ccus sync --week YYYY-MM-DD。");
|
|
912
|
+
if (["target", "interval", "range", "suffix", "no-suffix"].some(key => options[key] !== undefined)) {
|
|
913
|
+
throw new Error("--week 不能与同步配置参数同时使用;请先用 ccus sync config 配置目标目录和后缀。");
|
|
914
|
+
}
|
|
915
|
+
const result = await (0, sync_1.performSync)(dataDir, runExport, new Date(), week);
|
|
916
|
+
process.stdout.write(`已重新同步到 ${result.destPath}\n`);
|
|
917
|
+
process.stdout.write(`耗时:${(0, sync_1.formatSyncElapsed)(Number(process.hrtime.bigint() - startedAt) / 1_000_000)}\n`);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
908
920
|
const target = getStringOption(options, "target");
|
|
909
921
|
const interval = getStringOption(options, "interval");
|
|
910
922
|
const range = getStringOption(options, "range");
|
|
@@ -959,6 +971,8 @@ async function handleSyncStatus(options) {
|
|
|
959
971
|
* 不带任何参数时仅打印当前配置,方便确认。
|
|
960
972
|
*/
|
|
961
973
|
async function handleSyncConfig(options) {
|
|
974
|
+
if (options.week !== undefined)
|
|
975
|
+
throw new Error("--week 只用于本次同步,请使用 ccus sync --week YYYY-MM-DD。");
|
|
962
976
|
const dataDir = getDataDir(options);
|
|
963
977
|
const target = getStringOption(options, "target");
|
|
964
978
|
const interval = getStringOption(options, "interval");
|
package/dist/lib/paths.js
CHANGED
|
@@ -19,6 +19,7 @@ exports.getCodexConfigPath = getCodexConfigPath;
|
|
|
19
19
|
exports.getCodexHooksPath = getCodexHooksPath;
|
|
20
20
|
exports.getClaudeDataDir = getClaudeDataDir;
|
|
21
21
|
exports.getClaudeSettingsPath = getClaudeSettingsPath;
|
|
22
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
22
23
|
const node_os_1 = __importDefault(require("node:os"));
|
|
23
24
|
const node_path_1 = __importDefault(require("node:path"));
|
|
24
25
|
/**
|
|
@@ -85,19 +86,46 @@ function getOrcaCodexHome() {
|
|
|
85
86
|
const appData = process.env.APPDATA;
|
|
86
87
|
return appData ? node_path_1.default.join(appData, "orca", "codex-runtime-home", "home") : null;
|
|
87
88
|
}
|
|
89
|
+
/** 动态发现 Orca 托管账号,添加/移除账号后下一次扫描即可生效。 */
|
|
90
|
+
function getOrcaCodexAccountHomes() {
|
|
91
|
+
const appData = process.env.APPDATA;
|
|
92
|
+
if (!appData) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const accountsRoot = node_path_1.default.join(appData, "orca", "codex-accounts");
|
|
96
|
+
try {
|
|
97
|
+
return node_fs_1.default.readdirSync(accountsRoot, { withFileTypes: true })
|
|
98
|
+
.filter((entry) => entry.isDirectory())
|
|
99
|
+
.map((entry) => node_path_1.default.resolve(accountsRoot, entry.name, "home"))
|
|
100
|
+
.sort();
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const code = error.code;
|
|
104
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
88
110
|
/**
|
|
89
111
|
* Codex session 的全部数据源。
|
|
90
112
|
*
|
|
91
|
-
* 默认合并 `~/.codex
|
|
113
|
+
* 默认合并 `~/.codex`、Orca runtime home 与各托管账号 home;`CODEX_HOME` 指向 Orca 时仍保留
|
|
92
114
|
* `~/.codex`,其它显式覆盖则沿用原有覆盖语义。路径按平台规则去重。
|
|
93
115
|
*/
|
|
94
116
|
function getCodexSessionHomes() {
|
|
95
117
|
const configuredHome = node_path_1.default.resolve(getCodexHome());
|
|
96
118
|
const defaultHome = node_path_1.default.resolve(node_path_1.default.join(node_os_1.default.homedir(), ".codex"));
|
|
97
119
|
const orcaHome = getOrcaCodexHome();
|
|
98
|
-
const
|
|
99
|
-
? [
|
|
100
|
-
|
|
120
|
+
const orcaHomes = [
|
|
121
|
+
...(orcaHome ? [node_path_1.default.resolve(orcaHome)] : []),
|
|
122
|
+
...getOrcaCodexAccountHomes(),
|
|
123
|
+
];
|
|
124
|
+
const candidates = [
|
|
125
|
+
...(orcaHomes.some((home) => samePath(configuredHome, home)) ? [defaultHome] : []),
|
|
126
|
+
configuredHome,
|
|
127
|
+
...orcaHomes,
|
|
128
|
+
];
|
|
101
129
|
const seen = new Set();
|
|
102
130
|
return candidates.filter((candidate) => {
|
|
103
131
|
const key = pathKey(candidate);
|
package/dist/lib/sync.js
CHANGED
|
@@ -189,11 +189,19 @@ async function exportAndCopy(dataDir, runExport, targetDir, range, suffix) {
|
|
|
189
189
|
* 用 `sync-state.lastArchivedWeek` 去重,避免周一当天每次同步都重复归档。
|
|
190
190
|
* 失败时写 error 状态后向上抛,由调用方决定展示或静默。
|
|
191
191
|
*/
|
|
192
|
-
async function performSync(dataDir, runExport, now = new Date()) {
|
|
192
|
+
async function performSync(dataDir, runExport, now = new Date(), week) {
|
|
193
193
|
const config = readSyncConfig(dataDir);
|
|
194
194
|
if (!config.targetDir) {
|
|
195
195
|
throw new Error("未配置同步目标目录。请先运行 `ccus sync config --target DIR`。");
|
|
196
196
|
}
|
|
197
|
+
// 补同步独立执行,不推迟自动同步,也不触发周一归档。
|
|
198
|
+
if (week !== undefined) {
|
|
199
|
+
const dayStartMinutes = (0, time_1.parseDayStart)((await (0, config_1.readConfig)(dataDir)).dayStart);
|
|
200
|
+
const range = `week:${week}`;
|
|
201
|
+
(0, time_1.resolveRange)(range, now, dayStartMinutes);
|
|
202
|
+
const primary = await exportAndCopy(dataDir, runExport, config.targetDir, range, config.suffix);
|
|
203
|
+
return { ...primary, archivedLastWeekDest: null };
|
|
204
|
+
}
|
|
197
205
|
const state = readSyncStateSync(dataDir);
|
|
198
206
|
let lastArchivedWeek = state?.lastArchivedWeek;
|
|
199
207
|
try {
|
package/dist/lib/time.js
CHANGED
|
@@ -78,6 +78,23 @@ const RANGE_ALIASES = {
|
|
|
78
78
|
function resolveRange(range, now = new Date(), dayStartMinutes = exports.DEFAULT_DAY_START_MINUTES) {
|
|
79
79
|
const raw = (range ?? "5h").trim().toLowerCase();
|
|
80
80
|
const normalized = RANGE_ALIASES[raw] ?? raw;
|
|
81
|
+
if (normalized.startsWith("week:")) {
|
|
82
|
+
const dateKey = normalized.slice(5);
|
|
83
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateKey);
|
|
84
|
+
if (!match)
|
|
85
|
+
throw new Error("指定周的日期必须是 YYYY-MM-DD,例如 2026-09-28。");
|
|
86
|
+
const date = new Date(0);
|
|
87
|
+
date.setFullYear(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
88
|
+
date.setHours(0, dayStartMinutes, 0, 0);
|
|
89
|
+
if (localDateKey(date) !== dateKey)
|
|
90
|
+
throw new Error(`无效的周日期:${dateKey}。`);
|
|
91
|
+
return {
|
|
92
|
+
label: normalized,
|
|
93
|
+
start: startOfLocalWeek(date, dayStartMinutes),
|
|
94
|
+
end: endOfLocalWeek(date, dayStartMinutes),
|
|
95
|
+
dayStartMinutes,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
81
98
|
if (normalized === "today") {
|
|
82
99
|
return { label: "today", start: startOfReportingDay(now, dayStartMinutes), end: now, dayStartMinutes };
|
|
83
100
|
}
|