ccus-cli 0.2.8 → 0.2.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 +2 -2
- package/dist/cli.js +1 -1
- package/dist/lib/codex-sessions.js +113 -23
- package/dist/lib/paths.js +37 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
178
178
|
|
|
179
179
|
- `ccus sessions` 默认打包本周;支持 `tw`、`lw`、`today`、`5h` 等与 `export` 相同的范围写法。
|
|
180
180
|
- 只选择文件中至少一条记录落入时间窗的 session,但 zip 内保存完整原始文件,不裁剪内容。
|
|
181
|
-
- Claude 文件保持 `<projectDir>/<sessionId>.jsonl` 路径;Codex rollout
|
|
181
|
+
- Claude 文件保持 `<projectDir>/<sessionId>.jsonl` 路径;Codex rollout 合并 `<CODEX_HOME>/sessions` 与 `%APPDATA%/orca/codex-runtime-home/home/sessions`,按相对路径和 JSONL 事件去重后保持 `codex/<sessions 相对路径>`。
|
|
182
182
|
- 默认写入 `<data-dir>/sessions/projects_<start>_<end>_<gitUserName>.zip`;`--out` 可指定路径。
|
|
183
183
|
|
|
184
184
|
## 多人汇总
|
|
@@ -311,7 +311,7 @@ ccus install --codex --uninstall # 移除 ccus 的 Stop hook
|
|
|
311
311
|
- **定时同步**:Stop hook 还兜底触发 `ccus sync`(与 Claude statusline 对称)——配过 `ccus sync config --target` 后,Codex 每 turn 结束都会检查 3h 周期,到期自动 export + 复制 bundle(含 Codex token/消息)到目标目录。只用 Codex、不开 Claude Code 时也能自动同步。
|
|
312
312
|
- **缓存节流**:Stop 每 turn 触发,额度按 5 分钟 TTL 缓存(`codex-quota-cache.json`),命中秒回不 spawn;过期才拉一次(带 ~10s 超时),失败回退旧缓存。
|
|
313
313
|
- **字段映射**:app-server 返回 `primary`(5h)/ `secondary`(weekly)两窗口,取各自的 `usedPercent`(驼峰,clamp 0–100);ccus 填进 `rate_limits.five_hour` / `seven_day` 的 `used_percentage`,`computeStatuslineEvent` 读时自动算出 usage。
|
|
314
|
-
- **token / 消息 / 额度都进 export / aggregate**:Codex 的 token、用户消息数、API
|
|
314
|
+
- **token / 消息 / 额度都进 export / aggregate**:Codex 的 token、用户消息数、API 请求数合并读取 `<CODEX_HOME>/sessions` 与 `%APPDATA%/orca/codex-runtime-home/home/sessions` 的 rollout;同一相对路径的副本按 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` 列区分来源。
|
|
315
315
|
- **Windows**:`install --codex` 在 Windows 写 `ccus.cmd __codex-hook`(npm 全局装会生成 `ccus.cmd`);Windows 上 Stop hook 偶发收到非法 JSON(已知 bug #23784),ccus 容错按无 payload 处理、仍照常拉额度 + 落盘。
|
|
316
316
|
|
|
317
317
|
> 该路径依赖 Codex 内部 app-server 协议(`account/rateLimits/read` 的返回结构)与 hooks payload schema,随版本变;解析层宽松,字段缺失返回 null、失败静默。
|
package/dist/cli.js
CHANGED
|
@@ -683,7 +683,7 @@ async function handleSessions(options) {
|
|
|
683
683
|
}));
|
|
684
684
|
const codexEntries = codexSessions.map(async (session) => ({
|
|
685
685
|
name: `codex/${session.relativePath.replaceAll("\\", "/")}`,
|
|
686
|
-
data:
|
|
686
|
+
data: Buffer.from(session.content, "utf8"),
|
|
687
687
|
}));
|
|
688
688
|
const entries = await Promise.all([...claudeEntries, ...codexEntries]);
|
|
689
689
|
const zipBuffer = await buildZipBuffer(entries);
|
|
@@ -19,6 +19,29 @@ function getNumber(value) {
|
|
|
19
19
|
function getString(value) {
|
|
20
20
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
21
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
|
+
}
|
|
22
45
|
async function collectRolloutFiles(directoryPath) {
|
|
23
46
|
try {
|
|
24
47
|
const entries = await promises_1.default.readdir(directoryPath, { withFileTypes: true });
|
|
@@ -38,6 +61,74 @@ async function collectRolloutFiles(directoryPath) {
|
|
|
38
61
|
throw error;
|
|
39
62
|
}
|
|
40
63
|
}
|
|
64
|
+
function rolloutPathKey(relativePath) {
|
|
65
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
66
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
67
|
+
}
|
|
68
|
+
/** 收集标准 Codex 与 Orca session,并按 sessions 下的相对路径归并同一 rollout。 */
|
|
69
|
+
async function collectRolloutFileGroups() {
|
|
70
|
+
const sessionDirs = (0, paths_1.getCodexSessionHomes)().map((home) => node_path_1.default.join(home, "sessions"));
|
|
71
|
+
const filesByDir = await Promise.all(sessionDirs.map((directory) => collectRolloutFiles(directory)));
|
|
72
|
+
const groups = new Map();
|
|
73
|
+
for (let index = 0; index < sessionDirs.length; index += 1) {
|
|
74
|
+
const sessionsDir = sessionDirs[index];
|
|
75
|
+
for (const filePath of filesByDir[index]) {
|
|
76
|
+
const relativePath = node_path_1.default.relative(sessionsDir, filePath);
|
|
77
|
+
const key = rolloutPathKey(relativePath);
|
|
78
|
+
const group = groups.get(key);
|
|
79
|
+
if (group) {
|
|
80
|
+
group.filePaths.push(filePath);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
groups.set(key, { relativePath, filePaths: [filePath] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...groups.values()];
|
|
88
|
+
}
|
|
89
|
+
function rolloutLineKey(line) {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.stringify(JSON.parse(line));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return line;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 合并同一 rollout 在不同根目录下的副本。
|
|
99
|
+
*
|
|
100
|
+
* 对每种 JSONL 行保留各副本中的最大出现次数,既消除完整/部分镜像造成的重复,
|
|
101
|
+
* 又保留任一副本独有的新增事件以及单个源内真实存在的重复行。
|
|
102
|
+
*/
|
|
103
|
+
function mergeRolloutContents(contents) {
|
|
104
|
+
const mergedLines = [];
|
|
105
|
+
const mergedCounts = new Map();
|
|
106
|
+
for (const content of contents) {
|
|
107
|
+
const sourceCounts = new Map();
|
|
108
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
const key = rolloutLineKey(line);
|
|
111
|
+
const sourceCount = (sourceCounts.get(key) ?? 0) + 1;
|
|
112
|
+
sourceCounts.set(key, sourceCount);
|
|
113
|
+
if (sourceCount > (mergedCounts.get(key) ?? 0)) {
|
|
114
|
+
mergedCounts.set(key, sourceCount);
|
|
115
|
+
mergedLines.push(line);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return mergedLines.length > 0 ? `${mergedLines.join("\n")}\n` : "";
|
|
120
|
+
}
|
|
121
|
+
async function readMergedRollout(group) {
|
|
122
|
+
const reads = await Promise.all(group.filePaths.map(async (filePath) => {
|
|
123
|
+
try {
|
|
124
|
+
return await promises_1.default.readFile(filePath, "utf8");
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
return mergeRolloutContents(reads.filter((content) => content !== null));
|
|
131
|
+
}
|
|
41
132
|
function timestampInRange(timestamp, start, end) {
|
|
42
133
|
if (!timestamp) {
|
|
43
134
|
return false;
|
|
@@ -46,17 +137,16 @@ function timestampInRange(timestamp, start, end) {
|
|
|
46
137
|
return Number.isFinite(value) && value >= start.getTime() && value <= end.getTime();
|
|
47
138
|
}
|
|
48
139
|
/**
|
|
49
|
-
*
|
|
140
|
+
* 找出标准 Codex 与 Orca sessions 中在指定时间范围内有活动的 rollout 文件。
|
|
50
141
|
*
|
|
51
|
-
*
|
|
142
|
+
* 只判断合并后的文件里是否存在范围内的记录,不过滤内容,导出时写入完整合并结果。
|
|
52
143
|
*/
|
|
53
144
|
async function findActiveCodexSessionFiles(start, end) {
|
|
54
|
-
const
|
|
55
|
-
const files = await collectRolloutFiles(sessionsDir);
|
|
145
|
+
const groups = await collectRolloutFileGroups();
|
|
56
146
|
const result = [];
|
|
57
|
-
for (const
|
|
147
|
+
for (const group of groups) {
|
|
58
148
|
try {
|
|
59
|
-
const content = await
|
|
149
|
+
const content = await readMergedRollout(group);
|
|
60
150
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
61
151
|
let hasInRange = false;
|
|
62
152
|
for (const line of lines) {
|
|
@@ -73,8 +163,9 @@ async function findActiveCodexSessionFiles(start, end) {
|
|
|
73
163
|
}
|
|
74
164
|
if (hasInRange) {
|
|
75
165
|
result.push({
|
|
76
|
-
filePath,
|
|
77
|
-
relativePath:
|
|
166
|
+
filePath: group.filePaths[0],
|
|
167
|
+
relativePath: group.relativePath,
|
|
168
|
+
content,
|
|
78
169
|
});
|
|
79
170
|
}
|
|
80
171
|
}
|
|
@@ -149,23 +240,26 @@ function summarizeRollout(content, start, end) {
|
|
|
149
240
|
return result;
|
|
150
241
|
}
|
|
151
242
|
/**
|
|
152
|
-
*
|
|
243
|
+
* 从标准 Codex 与 Orca 本地 session rollout 统计消息数、请求数和 token 用量。
|
|
244
|
+
* Codex Desktop 的 guardian 安全审查 rollout 整体排除,避免把内部审批轮次计为用户使用量。
|
|
153
245
|
*
|
|
154
246
|
* 消息数 = task_started 的 distinct turn_id(跨文件去重)。重放副本会让同一 turn_id 出现在多个文件,
|
|
155
247
|
* 故用全局 Map<turn_id, minMs> 收集(取最早 timestamp = 真实发生时刻,早于任何重放副本),最后取 size。
|
|
156
248
|
*/
|
|
157
249
|
async function summarizeCodexSessionUsage(start, end) {
|
|
158
250
|
const codexDataDir = (0, paths_1.getCodexHome)();
|
|
159
|
-
const
|
|
160
|
-
const files = await collectRolloutFiles(sessionsDir);
|
|
251
|
+
const groups = await collectRolloutFileGroups();
|
|
161
252
|
const turnMinMs = new Map();
|
|
162
253
|
let apiRequestCount = 0;
|
|
163
254
|
let inputTokens = 0;
|
|
164
255
|
let outputTokens = 0;
|
|
165
256
|
let cacheReadInputTokens = 0;
|
|
166
|
-
for (const
|
|
257
|
+
for (const group of groups) {
|
|
167
258
|
try {
|
|
168
|
-
const content = await
|
|
259
|
+
const content = await readMergedRollout(group);
|
|
260
|
+
if (isGuardianRollout(content)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
169
263
|
const parsed = summarizeRollout(content, start, end);
|
|
170
264
|
for (const turn of parsed.turns) {
|
|
171
265
|
const prev = turnMinMs.get(turn.turnId);
|
|
@@ -188,20 +282,19 @@ async function summarizeCodexSessionUsage(start, end) {
|
|
|
188
282
|
inputTokens,
|
|
189
283
|
outputTokens,
|
|
190
284
|
cacheReadInputTokens,
|
|
191
|
-
matchedFileCount:
|
|
285
|
+
matchedFileCount: groups.length,
|
|
192
286
|
codexDataDir,
|
|
193
287
|
};
|
|
194
288
|
}
|
|
195
289
|
/**
|
|
196
290
|
* 按天汇总 Codex session rollout 中的消息数、请求数和 token 用量。
|
|
291
|
+
* 与周汇总一致,排除 Codex Desktop 的 guardian 安全审查 rollout。
|
|
197
292
|
*
|
|
198
293
|
* 消息数同 weekly:先全局 Map<turn_id, minMs> 去重,再按 minMs 的本地日归桶(保证 weekly = Σ daily、
|
|
199
294
|
* 且重放副本跨天不重复)。token 维度按 token_count 事件 timestamp 的本地日累加。
|
|
200
295
|
*/
|
|
201
296
|
async function summarizeCodexSessionUsageByDay(start, end) {
|
|
202
|
-
const
|
|
203
|
-
const sessionsDir = node_path_1.default.join(codexDataDir, "sessions");
|
|
204
|
-
const files = await collectRolloutFiles(sessionsDir);
|
|
297
|
+
const groups = await collectRolloutFileGroups();
|
|
205
298
|
const turnMinMs = new Map();
|
|
206
299
|
const daily = new Map();
|
|
207
300
|
const ensureDay = (date) => {
|
|
@@ -219,12 +312,9 @@ async function summarizeCodexSessionUsageByDay(start, end) {
|
|
|
219
312
|
}
|
|
220
313
|
return current;
|
|
221
314
|
};
|
|
222
|
-
for (const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
content = await promises_1.default.readFile(filePath, "utf8");
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
315
|
+
for (const group of groups) {
|
|
316
|
+
const content = await readMergedRollout(group);
|
|
317
|
+
if (isGuardianRollout(content)) {
|
|
228
318
|
continue;
|
|
229
319
|
}
|
|
230
320
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
package/dist/lib/paths.js
CHANGED
|
@@ -13,6 +13,8 @@ exports.getApiConfigPath = getApiConfigPath;
|
|
|
13
13
|
exports.getApiQuotaCachePath = getApiQuotaCachePath;
|
|
14
14
|
exports.getCodexQuotaCachePath = getCodexQuotaCachePath;
|
|
15
15
|
exports.getCodexHome = getCodexHome;
|
|
16
|
+
exports.getOrcaCodexHome = getOrcaCodexHome;
|
|
17
|
+
exports.getCodexSessionHomes = getCodexSessionHomes;
|
|
16
18
|
exports.getCodexConfigPath = getCodexConfigPath;
|
|
17
19
|
exports.getCodexHooksPath = getCodexHooksPath;
|
|
18
20
|
exports.getClaudeDataDir = getClaudeDataDir;
|
|
@@ -78,6 +80,41 @@ function getCodexQuotaCachePath(dataDir) {
|
|
|
78
80
|
function getCodexHome() {
|
|
79
81
|
return process.env.CODEX_HOME ?? node_path_1.default.join(node_os_1.default.homedir(), ".codex");
|
|
80
82
|
}
|
|
83
|
+
/** Orca 内置 Codex 的运行时数据目录;非 Orca 环境下返回 null。 */
|
|
84
|
+
function getOrcaCodexHome() {
|
|
85
|
+
const appData = process.env.APPDATA;
|
|
86
|
+
return appData ? node_path_1.default.join(appData, "orca", "codex-runtime-home", "home") : null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Codex session 的全部数据源。
|
|
90
|
+
*
|
|
91
|
+
* 默认合并 `~/.codex` 与 Orca runtime home;`CODEX_HOME` 指向 Orca 时仍保留
|
|
92
|
+
* `~/.codex`,其它显式覆盖则沿用原有覆盖语义。路径按平台规则去重。
|
|
93
|
+
*/
|
|
94
|
+
function getCodexSessionHomes() {
|
|
95
|
+
const configuredHome = node_path_1.default.resolve(getCodexHome());
|
|
96
|
+
const defaultHome = node_path_1.default.resolve(node_path_1.default.join(node_os_1.default.homedir(), ".codex"));
|
|
97
|
+
const orcaHome = getOrcaCodexHome();
|
|
98
|
+
const candidates = orcaHome && samePath(configuredHome, orcaHome)
|
|
99
|
+
? [defaultHome, configuredHome]
|
|
100
|
+
: [configuredHome, ...(orcaHome ? [node_path_1.default.resolve(orcaHome)] : [])];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
return candidates.filter((candidate) => {
|
|
103
|
+
const key = pathKey(candidate);
|
|
104
|
+
if (seen.has(key)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
seen.add(key);
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function pathKey(value) {
|
|
112
|
+
const resolved = node_path_1.default.resolve(value);
|
|
113
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
114
|
+
}
|
|
115
|
+
function samePath(left, right) {
|
|
116
|
+
return pathKey(left) === pathKey(right);
|
|
117
|
+
}
|
|
81
118
|
/** Codex CLI 用户级 config.toml 路径,`ccus install --codex` 往里写 notify。 */
|
|
82
119
|
function getCodexConfigPath() {
|
|
83
120
|
return node_path_1.default.join(getCodexHome(), "config.toml");
|