ccus-cli 0.2.9 → 0.2.11

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.
@@ -8,6 +8,7 @@ exports.summarizeCodexSessionUsage = summarizeCodexSessionUsage;
8
8
  exports.summarizeCodexSessionUsageByDay = summarizeCodexSessionUsageByDay;
9
9
  const promises_1 = __importDefault(require("node:fs/promises"));
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ const api_equivalent_cost_1 = require("./api-equivalent-cost");
11
12
  const paths_1 = require("./paths");
12
13
  const time_1 = require("./time");
13
14
  function isRecord(value) {
@@ -19,6 +20,13 @@ function getNumber(value) {
19
20
  function getString(value) {
20
21
  return typeof value === "string" && value.length > 0 ? value : null;
21
22
  }
23
+ function splitInputTokens(usage) {
24
+ const cacheReadInputTokens = getNumber(usage.cached_input_tokens);
25
+ return {
26
+ inputTokens: Math.max(0, getNumber(usage.input_tokens) - cacheReadInputTokens),
27
+ cacheReadInputTokens,
28
+ };
29
+ }
22
30
  /** Guardian 是 Codex Desktop 的动作安全审查子代理,不代表用户请求或业务模型调用。 */
23
31
  function isGuardianRollout(content) {
24
32
  let offset = 0;
@@ -61,6 +69,74 @@ async function collectRolloutFiles(directoryPath) {
61
69
  throw error;
62
70
  }
63
71
  }
72
+ function rolloutPathKey(relativePath) {
73
+ const normalized = relativePath.replaceAll("\\", "/");
74
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
75
+ }
76
+ /** 收集标准 Codex 与 Orca session,并按 sessions 下的相对路径归并同一 rollout。 */
77
+ async function collectRolloutFileGroups() {
78
+ const sessionDirs = (0, paths_1.getCodexSessionHomes)().map((home) => node_path_1.default.join(home, "sessions"));
79
+ const filesByDir = await Promise.all(sessionDirs.map((directory) => collectRolloutFiles(directory)));
80
+ const groups = new Map();
81
+ for (let index = 0; index < sessionDirs.length; index += 1) {
82
+ const sessionsDir = sessionDirs[index];
83
+ for (const filePath of filesByDir[index]) {
84
+ const relativePath = node_path_1.default.relative(sessionsDir, filePath);
85
+ const key = rolloutPathKey(relativePath);
86
+ const group = groups.get(key);
87
+ if (group) {
88
+ group.filePaths.push(filePath);
89
+ }
90
+ else {
91
+ groups.set(key, { relativePath, filePaths: [filePath] });
92
+ }
93
+ }
94
+ }
95
+ return [...groups.values()];
96
+ }
97
+ function rolloutLineKey(line) {
98
+ try {
99
+ return JSON.stringify(JSON.parse(line));
100
+ }
101
+ catch {
102
+ return line;
103
+ }
104
+ }
105
+ /**
106
+ * 合并同一 rollout 在不同根目录下的副本。
107
+ *
108
+ * 对每种 JSONL 行保留各副本中的最大出现次数,既消除完整/部分镜像造成的重复,
109
+ * 又保留任一副本独有的新增事件以及单个源内真实存在的重复行。
110
+ */
111
+ function mergeRolloutContents(contents) {
112
+ const mergedLines = [];
113
+ const mergedCounts = new Map();
114
+ for (const content of contents) {
115
+ const sourceCounts = new Map();
116
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
117
+ for (const line of lines) {
118
+ const key = rolloutLineKey(line);
119
+ const sourceCount = (sourceCounts.get(key) ?? 0) + 1;
120
+ sourceCounts.set(key, sourceCount);
121
+ if (sourceCount > (mergedCounts.get(key) ?? 0)) {
122
+ mergedCounts.set(key, sourceCount);
123
+ mergedLines.push(line);
124
+ }
125
+ }
126
+ }
127
+ return mergedLines.length > 0 ? `${mergedLines.join("\n")}\n` : "";
128
+ }
129
+ async function readMergedRollout(group) {
130
+ const reads = await Promise.all(group.filePaths.map(async (filePath) => {
131
+ try {
132
+ return await promises_1.default.readFile(filePath, "utf8");
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ }));
138
+ return mergeRolloutContents(reads.filter((content) => content !== null));
139
+ }
64
140
  function timestampInRange(timestamp, start, end) {
65
141
  if (!timestamp) {
66
142
  return false;
@@ -69,17 +145,16 @@ function timestampInRange(timestamp, start, end) {
69
145
  return Number.isFinite(value) && value >= start.getTime() && value <= end.getTime();
70
146
  }
71
147
  /**
72
- * 找出 `<CODEX_HOME>/sessions` 中在指定时间范围内有活动的 rollout 文件。
148
+ * 找出标准 Codex 与 Orca sessions 中在指定时间范围内有活动的 rollout 文件。
73
149
  *
74
- * 只判断文件里是否存在范围内的记录,不过滤内容,导出时完整复制原始文件。
150
+ * 只判断合并后的文件里是否存在范围内的记录,不过滤内容,导出时写入完整合并结果。
75
151
  */
76
152
  async function findActiveCodexSessionFiles(start, end) {
77
- const sessionsDir = node_path_1.default.join((0, paths_1.getCodexHome)(), "sessions");
78
- const files = await collectRolloutFiles(sessionsDir);
153
+ const groups = await collectRolloutFileGroups();
79
154
  const result = [];
80
- for (const filePath of files) {
155
+ for (const group of groups) {
81
156
  try {
82
- const content = await promises_1.default.readFile(filePath, "utf8");
157
+ const content = await readMergedRollout(group);
83
158
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
84
159
  let hasInRange = false;
85
160
  for (const line of lines) {
@@ -96,8 +171,9 @@ async function findActiveCodexSessionFiles(start, end) {
96
171
  }
97
172
  if (hasInRange) {
98
173
  result.push({
99
- filePath,
100
- relativePath: node_path_1.default.relative(sessionsDir, filePath),
174
+ filePath: group.filePaths[0],
175
+ relativePath: group.relativePath,
176
+ content,
101
177
  });
102
178
  }
103
179
  }
@@ -110,8 +186,7 @@ async function findActiveCodexSessionFiles(start, end) {
110
186
  /**
111
187
  * 从 `token_count` 事件的 `info.last_token_usage` 取 token 用量对象(缺失返回 null)。
112
188
  * 必须用 last_token_usage(本次增量),不能用 total_token_usage(会话累计,会重复计)。
113
- * 注意:Codex `input_tokens` 含缓存命中(`cached_input_tokens` 是其子集),
114
- * 累加 inputTokens 时要减去 cached 得到净输入,对齐 Claude 的 `input_tokens` 口径。
189
+ * `cached_input_tokens` `input_tokens` 的子集;普通输入需扣除缓存输入并钳制为非负数。
115
190
  */
116
191
  function extractLastTokenUsage(payload) {
117
192
  if (!isRecord(payload.info)) {
@@ -136,19 +211,28 @@ function summarizeRollout(content, start, end) {
136
211
  inputTokens: 0,
137
212
  outputTokens: 0,
138
213
  cacheReadInputTokens: 0,
214
+ apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
139
215
  };
216
+ let currentModel = null;
140
217
  for (const line of lines) {
141
218
  try {
142
219
  const record = JSON.parse(line);
143
- if (!isRecord(record) || !timestampInRange(getString(record.timestamp), start, end)) {
220
+ if (!isRecord(record) || !isRecord(record.payload)) {
144
221
  continue;
145
222
  }
146
- if (record.type !== "event_msg" || !isRecord(record.payload)) {
223
+ const payload = record.payload;
224
+ if (record.type === "turn_context") {
225
+ if (typeof payload.model === "string") {
226
+ currentModel = payload.model;
227
+ }
228
+ continue;
229
+ }
230
+ const timestamp = getString(record.timestamp);
231
+ if (!timestampInRange(timestamp, start, end) || record.type !== "event_msg") {
147
232
  continue;
148
233
  }
149
- const payload = record.payload;
150
234
  if (payload.type === "task_started" && typeof payload.turn_id === "string") {
151
- const ms = new Date(getString(record.timestamp) ?? "").getTime();
235
+ const ms = new Date(timestamp ?? "").getTime();
152
236
  if (Number.isFinite(ms)) {
153
237
  result.turns.push({ turnId: payload.turn_id, ms });
154
238
  }
@@ -156,12 +240,23 @@ function summarizeRollout(content, start, end) {
156
240
  if (payload.type === "token_count") {
157
241
  const usage = extractLastTokenUsage(payload);
158
242
  if (usage) {
159
- // 净输入 = input_tokens - cached_input_tokens(input_tokens 含缓存命中,减去对齐 Claude 口径)。
160
- const netInputTokens = Math.max(0, getNumber(usage.input_tokens) - getNumber(usage.cached_input_tokens));
243
+ const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
244
+ const outputTokens = getNumber(usage.output_tokens);
161
245
  result.apiRequestCount += 1;
162
- result.inputTokens += netInputTokens;
163
- result.outputTokens += getNumber(usage.output_tokens);
164
- result.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
246
+ result.inputTokens += inputTokens;
247
+ result.outputTokens += outputTokens;
248
+ result.cacheReadInputTokens += cacheReadInputTokens;
249
+ result.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([
250
+ result.apiEquivalentCost,
251
+ (0, api_equivalent_cost_1.priceApiRequest)({
252
+ provider: "codex",
253
+ timestamp: timestamp,
254
+ model: currentModel,
255
+ inputTokens,
256
+ outputTokens,
257
+ cacheReadInputTokens,
258
+ }),
259
+ ]);
165
260
  }
166
261
  }
167
262
  }
@@ -172,7 +267,7 @@ function summarizeRollout(content, start, end) {
172
267
  return result;
173
268
  }
174
269
  /**
175
- * Codex 本地 session rollout(<CODEX_HOME>/sessions 下递归的 .jsonl)统计消息数、请求数和 token 用量。
270
+ * 从标准 Codex 与 Orca 本地 session rollout 统计消息数、请求数、token 用量和标准 API 等效成本。
176
271
  * Codex Desktop 的 guardian 安全审查 rollout 整体排除,避免把内部审批轮次计为用户使用量。
177
272
  *
178
273
  * 消息数 = task_started 的 distinct turn_id(跨文件去重)。重放副本会让同一 turn_id 出现在多个文件,
@@ -180,16 +275,16 @@ function summarizeRollout(content, start, end) {
180
275
  */
181
276
  async function summarizeCodexSessionUsage(start, end) {
182
277
  const codexDataDir = (0, paths_1.getCodexHome)();
183
- const sessionsDir = node_path_1.default.join(codexDataDir, "sessions");
184
- const files = await collectRolloutFiles(sessionsDir);
278
+ const groups = await collectRolloutFileGroups();
185
279
  const turnMinMs = new Map();
186
280
  let apiRequestCount = 0;
187
281
  let inputTokens = 0;
188
282
  let outputTokens = 0;
189
283
  let cacheReadInputTokens = 0;
190
- for (const filePath of files) {
284
+ let apiEquivalentCost = (0, api_equivalent_cost_1.emptyApiEquivalentCost)();
285
+ for (const group of groups) {
191
286
  try {
192
- const content = await promises_1.default.readFile(filePath, "utf8");
287
+ const content = await readMergedRollout(group);
193
288
  if (isGuardianRollout(content)) {
194
289
  continue;
195
290
  }
@@ -204,6 +299,7 @@ async function summarizeCodexSessionUsage(start, end) {
204
299
  inputTokens += parsed.inputTokens;
205
300
  outputTokens += parsed.outputTokens;
206
301
  cacheReadInputTokens += parsed.cacheReadInputTokens;
302
+ apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([apiEquivalentCost, parsed.apiEquivalentCost]);
207
303
  }
208
304
  catch {
209
305
  continue;
@@ -215,21 +311,20 @@ async function summarizeCodexSessionUsage(start, end) {
215
311
  inputTokens,
216
312
  outputTokens,
217
313
  cacheReadInputTokens,
218
- matchedFileCount: files.length,
314
+ apiEquivalentCost,
315
+ matchedFileCount: groups.length,
219
316
  codexDataDir,
220
317
  };
221
318
  }
222
319
  /**
223
- * 按天汇总 Codex session rollout 中的消息数、请求数和 token 用量。
320
+ * 按天汇总 Codex session rollout 中的消息数、请求数、token 用量和标准 API 等效成本。
224
321
  * 与周汇总一致,排除 Codex Desktop 的 guardian 安全审查 rollout。
225
322
  *
226
323
  * 消息数同 weekly:先全局 Map<turn_id, minMs> 去重,再按 minMs 的本地日归桶(保证 weekly = Σ daily、
227
324
  * 且重放副本跨天不重复)。token 维度按 token_count 事件 timestamp 的本地日累加。
228
325
  */
229
326
  async function summarizeCodexSessionUsageByDay(start, end) {
230
- const codexDataDir = (0, paths_1.getCodexHome)();
231
- const sessionsDir = node_path_1.default.join(codexDataDir, "sessions");
232
- const files = await collectRolloutFiles(sessionsDir);
327
+ const groups = await collectRolloutFileGroups();
233
328
  const turnMinMs = new Map();
234
329
  const daily = new Map();
235
330
  const ensureDay = (date) => {
@@ -242,37 +337,39 @@ async function summarizeCodexSessionUsageByDay(start, end) {
242
337
  inputTokens: 0,
243
338
  outputTokens: 0,
244
339
  cacheReadInputTokens: 0,
340
+ apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
245
341
  };
246
342
  daily.set(date, current);
247
343
  }
248
344
  return current;
249
345
  };
250
- for (const filePath of files) {
251
- let content = "";
252
- try {
253
- content = await promises_1.default.readFile(filePath, "utf8");
254
- }
255
- catch {
256
- continue;
257
- }
346
+ for (const group of groups) {
347
+ const content = await readMergedRollout(group);
258
348
  if (isGuardianRollout(content)) {
259
349
  continue;
260
350
  }
261
351
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
352
+ let currentModel = null;
262
353
  for (const line of lines) {
263
354
  try {
264
355
  const record = JSON.parse(line);
265
- if (!isRecord(record)) {
356
+ if (!isRecord(record) || !isRecord(record.payload)) {
357
+ continue;
358
+ }
359
+ const payload = record.payload;
360
+ if (record.type === "turn_context") {
361
+ if (typeof payload.model === "string") {
362
+ currentModel = payload.model;
363
+ }
266
364
  continue;
267
365
  }
268
366
  const timestamp = getString(record.timestamp);
269
367
  if (!timestampInRange(timestamp, start, end)) {
270
368
  continue;
271
369
  }
272
- if (record.type !== "event_msg" || !isRecord(record.payload)) {
370
+ if (record.type !== "event_msg") {
273
371
  continue;
274
372
  }
275
- const payload = record.payload;
276
373
  const ms = new Date(timestamp).getTime();
277
374
  if (payload.type === "task_started" && typeof payload.turn_id === "string") {
278
375
  if (Number.isFinite(ms)) {
@@ -286,12 +383,23 @@ async function summarizeCodexSessionUsageByDay(start, end) {
286
383
  const usage = extractLastTokenUsage(payload);
287
384
  if (usage) {
288
385
  const current = ensureDay((0, time_1.localDateKey)(new Date(ms)));
289
- // summarizeRollout:累加净输入(input - cached),对齐 Claude 口径。
290
- const netInputTokens = Math.max(0, getNumber(usage.input_tokens) - getNumber(usage.cached_input_tokens));
386
+ const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
387
+ const outputTokens = getNumber(usage.output_tokens);
291
388
  current.apiRequestCount += 1;
292
- current.inputTokens += netInputTokens;
293
- current.outputTokens += getNumber(usage.output_tokens);
294
- current.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
389
+ current.inputTokens += inputTokens;
390
+ current.outputTokens += outputTokens;
391
+ current.cacheReadInputTokens += cacheReadInputTokens;
392
+ current.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([
393
+ current.apiEquivalentCost,
394
+ (0, api_equivalent_cost_1.priceApiRequest)({
395
+ provider: "codex",
396
+ timestamp: timestamp,
397
+ model: currentModel,
398
+ inputTokens,
399
+ outputTokens,
400
+ cacheReadInputTokens,
401
+ }),
402
+ ]);
295
403
  }
296
404
  }
297
405
  }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderDashboardPage = renderDashboardPage;
4
+ const api_pricing_table_1 = require("./api-pricing-table");
5
+ /** 两类 dashboard serve 共用的页面路由;null 表示 404。 */
6
+ async function renderDashboardPage(pathname, renderDashboard, generatedAt = new Date()) {
7
+ if (pathname === "/") {
8
+ return renderDashboard();
9
+ }
10
+ if (pathname === api_pricing_table_1.API_PRICING_PAGE_PATH) {
11
+ return (0, api_pricing_table_1.buildApiPricingPage)(generatedAt);
12
+ }
13
+ return null;
14
+ }
15
+ //# sourceMappingURL=dashboard-pages.js.map
@@ -5,6 +5,7 @@ exports.bucketizeEvents = bucketizeEvents;
5
5
  exports.buildDashboardHtml = buildDashboardHtml;
6
6
  const aggregate_1 = require("./aggregate");
7
7
  const chart_assets_1 = require("./chart-assets");
8
+ const api_pricing_table_1 = require("./api-pricing-table");
8
9
  const payload_1 = require("./payload");
9
10
  const time_1 = require("./time");
10
11
  /** 所有插入到 HTML/SVG 的动态文本都先转义,避免本地页面被注入内容。 */
@@ -216,6 +217,36 @@ function renderDailyMessages(points) {
216
217
  function statValue(value, suffix = "%") {
217
218
  return value === null ? "--" : `${value.toFixed(1)}${suffix}`;
218
219
  }
220
+ function formatCostUsd(value) {
221
+ return `$${value < 0.01 ? value.toFixed(4) : value.toFixed(2)}`;
222
+ }
223
+ function costDisplay(result) {
224
+ if (result.pricedApiRequestCount === 0 && result.unpricedApiRequestCount > 0) {
225
+ return { value: "不可用", note: `${result.unpricedApiRequestCount} 个请求未定价` };
226
+ }
227
+ const amount = formatCostUsd(result.estimatedUsd ?? 0);
228
+ if (result.unpricedApiRequestCount > 0) {
229
+ return { value: `≥ ${amount}`, note: `${result.unpricedApiRequestCount} 个请求未定价,结果不完整` };
230
+ }
231
+ return { value: amount, note: `${result.pricedApiRequestCount} 个请求已定价` };
232
+ }
233
+ function renderApiEquivalentCostCard(costs) {
234
+ if (!costs) {
235
+ return "";
236
+ }
237
+ const display = costDisplay(costs.total);
238
+ return `<article class="panel stat-card">
239
+ <h2>合计等效 API 成本</h2>
240
+ <p class="stat-value">${escapeHtml(display.value)}</p>
241
+ <p class="stat-note">${escapeHtml(display.note)}</p>
242
+ <a class="pricing-link" href="${api_pricing_table_1.API_PRICING_PAGE_FILE}" target="_blank" rel="noopener noreferrer">查看当前价格表</a>
243
+ </article>`;
244
+ }
245
+ function renderApiEquivalentCostNote(costs, pricingCatalogVersion) {
246
+ return costs
247
+ ? `<p class="muted">标准同步 API 等效成本,不是订阅或实际账单 · 事件时间计价 · 目录 ${escapeHtml(pricingCatalogVersion ?? "不可用")}</p>`
248
+ : "";
249
+ }
219
250
  /** 最近事件表帮助回看 statusline 在某一时刻到底输出了什么。 */
220
251
  function renderRecentEvents(events) {
221
252
  const rows = [...events]
@@ -263,7 +294,7 @@ function renderRecentEvents(events) {
263
294
  *
264
295
  * 整个 dashboard 保持“单文件可打开”,方便导出和直接分享本地结果。
265
296
  */
266
- function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages = []) {
297
+ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages = [], apiEquivalentCost = null, pricingCatalogVersion = null) {
267
298
  const summary = summarizeEvents(events);
268
299
  const buckets = bucketizeEvents(events, start, end, pickBucketMinutes(start, end));
269
300
  const totalUserMessages = dailyUserMessages.reduce((sum, point) => sum + point.userMessageCount, 0);
@@ -373,6 +404,7 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
373
404
  color: var(--muted);
374
405
  font-size: 14px;
375
406
  }
407
+ .pricing-link { display: inline-block; margin-top: 12px; color: var(--accent); font-size: 13px; }
376
408
  .panel-header {
377
409
  display: flex;
378
410
  align-items: end;
@@ -464,7 +496,9 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
464
496
  <p class="stat-value">${combinedUserMessages}</p>
465
497
  <p class="stat-note">窗口内每日真实用户请求数合计(Claude ${totalUserMessages} · Codex ${codexTotalUserMessages})</p>
466
498
  </article>
499
+ ${renderApiEquivalentCostCard(apiEquivalentCost)}
467
500
  </section>
501
+ ${renderApiEquivalentCostNote(apiEquivalentCost, pricingCatalogVersion)}
468
502
  ${renderChart(buckets)}
469
503
  ${renderDailyMessages(dailyUserMessages)}
470
504
  ${renderRecentEvents(events)}
@@ -28,6 +28,10 @@ function quoteCsv(value) {
28
28
  function toMillions(value) {
29
29
  return value === null ? null : (0, time_1.roundNumber)(value / 1_000_000, 6);
30
30
  }
31
+ /** 等效 API 成本以美元输出,最多保留 6 位小数;不可用时留空。 */
32
+ function formatApiEquivalentCost(value) {
33
+ return value === null ? null : (0, time_1.roundNumber)(value, 6);
34
+ }
31
35
  /**
32
36
  * 清理控制字符,并防止 Excel/Sheets 把值解释成公式。
33
37
  *
@@ -219,6 +223,8 @@ function buildAggregatedDailyCsv(rows) {
219
223
  "sevenDayCumulativeUsagePct",
220
224
  "uniqueSessions",
221
225
  "uniqueWorkspaces",
226
+ "estimatedApiEquivalentCostUsd",
227
+ "pricingCatalogVersion",
222
228
  ];
223
229
  const lines = rows.map((row) => toCsvLine([
224
230
  row.personKey,
@@ -236,6 +242,8 @@ function buildAggregatedDailyCsv(rows) {
236
242
  row.sevenDayCumulativeUsagePct,
237
243
  row.uniqueSessions,
238
244
  row.uniqueWorkspaces,
245
+ formatApiEquivalentCost(row.estimatedApiEquivalentCostUsd),
246
+ row.pricingCatalogVersion,
239
247
  ]));
240
248
  return [header.join(","), ...lines].join("\n");
241
249
  }
@@ -257,6 +265,8 @@ function buildAggregatedWeeklyCsv(rows) {
257
265
  "sevenDayCumulativeUsagePct",
258
266
  "uniqueSessions",
259
267
  "uniqueWorkspaces",
268
+ "estimatedApiEquivalentCostUsd",
269
+ "pricingCatalogVersion",
260
270
  ];
261
271
  const lines = rows.map((row) => toCsvLine([
262
272
  row.personKey,
@@ -274,6 +284,8 @@ function buildAggregatedWeeklyCsv(rows) {
274
284
  row.sevenDayCumulativeUsagePct,
275
285
  row.uniqueSessions,
276
286
  row.uniqueWorkspaces,
287
+ formatApiEquivalentCost(row.estimatedApiEquivalentCostUsd),
288
+ row.pricingCatalogVersion,
277
289
  ]));
278
290
  return [header.join(","), ...lines].join("\n");
279
291
  }
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "dist/cli.js",
11
11
  "dist/lib/**/*.js",
12
+ "dist/lib/api-pricing-catalog.json",
12
13
  "dist/types.js",
13
14
  "dist/vendor/**"
14
15
  ],
@@ -16,7 +17,7 @@
16
17
  "build": "tsc -p tsconfig.json && node scripts/copy-vendor.js",
17
18
  "prepublishOnly": "npm run build",
18
19
  "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/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
+ "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/api-equivalent-cost.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
21
  "smoke:api": "node scripts/smoke-api.mjs"
21
22
  },
22
23
  "keywords": [