ccus-cli 0.2.13 → 0.2.14

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/dist/cli.js CHANGED
@@ -264,8 +264,12 @@ async function loadDashboardData(options, defaultRange) {
264
264
  // codex 消息柱图走 dailyUserMessages,不受影响。
265
265
  const events = (await (0, storage_1.readEventsForRange)(dataDir, range, now))
266
266
  .map((record) => (0, payload_1.computeStatuslineEvent)(record));
267
- const claudeDailyUsage = await (0, claude_1.summarizeClaudeProjectUsageByDay)(window.start, window.end);
268
- const codexDailyUsage = await (0, codex_sessions_1.summarizeCodexSessionUsageByDay)(window.start, window.end);
267
+ const [claudeUsage, codexUsage] = await Promise.all([
268
+ (0, claude_1.summarizeClaudeProjectUsageCombined)(window.start, window.end),
269
+ (0, codex_sessions_1.summarizeCodexSessionUsageCombined)(window.start, window.end),
270
+ ]);
271
+ const claudeDailyUsage = claudeUsage.daily;
272
+ const codexDailyUsage = codexUsage.daily;
269
273
  const dailyUserMessages = (0, time_1.enumerateDateKeys)(window.start, window.end).map((date) => ({
270
274
  date,
271
275
  userMessageCount: claudeDailyUsage.get(date)?.userMessageCount ?? 0,
@@ -397,10 +401,14 @@ async function runExport(options) {
397
401
  const statuslineDailyRows = (0, export_1.buildSummaryRows)(claudeEvents);
398
402
  const codexStatuslineSummary = (0, dashboard_2.summarizeEvents)(codexEvents);
399
403
  const codexStatuslineDailyRows = (0, export_1.buildSummaryRows)(codexEvents);
400
- const claudeUsage = await (0, claude_1.summarizeClaudeProjectUsage)(window.start, window.end);
401
- const claudeDailyUsage = await (0, claude_1.summarizeClaudeProjectUsageByDay)(window.start, window.end);
402
- const codexUsage = await (0, codex_sessions_1.summarizeCodexSessionUsage)(window.start, window.end);
403
- const codexDailyUsage = await (0, codex_sessions_1.summarizeCodexSessionUsageByDay)(window.start, window.end);
404
+ const [claudeScan, codexScan] = await Promise.all([
405
+ (0, claude_1.summarizeClaudeProjectUsageCombined)(window.start, window.end),
406
+ (0, codex_sessions_1.summarizeCodexSessionUsageCombined)(window.start, window.end),
407
+ ]);
408
+ const claudeUsage = claudeScan.weekly;
409
+ const claudeDailyUsage = claudeScan.daily;
410
+ const codexUsage = codexScan.weekly;
411
+ const codexDailyUsage = codexScan.daily;
404
412
  const claudeWeeklyCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([...claudeDailyUsage.values()].map((day) => day.apiEquivalentCost));
405
413
  const codexWeeklyCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([...codexDailyUsage.values()].map((day) => day.apiEquivalentCost));
406
414
  (0, debug_1.debugLog)("export", "data collected", {
@@ -823,6 +831,7 @@ async function handleBackgroundCheckUpdate(options) {
823
831
  * 不带参数则用已存配置同步。最终仍无目标目录则报错引导用户先配置。
824
832
  */
825
833
  async function handleSync(options) {
834
+ const startedAt = process.hrtime.bigint();
826
835
  const dataDir = getDataDir(options);
827
836
  const target = getStringOption(options, "target");
828
837
  const interval = getStringOption(options, "interval");
@@ -848,6 +857,8 @@ async function handleSync(options) {
848
857
  if (result.archivedLastWeekDest) {
849
858
  process.stdout.write(`已归档上一周到 ${result.archivedLastWeekDest}\n`);
850
859
  }
860
+ const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
861
+ process.stdout.write(`耗时:${(0, sync_1.formatSyncElapsed)(elapsedMs)}\n`);
851
862
  }
852
863
  /**
853
864
  * `ccus sync status`:打印当前同步配置、上次同步时间与是否到期。
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.summarizeClaudeProjectUsage = summarizeClaudeProjectUsage;
7
+ exports.summarizeClaudeProjectUsageCombined = summarizeClaudeProjectUsageCombined;
7
8
  exports.findActiveSessionFiles = findActiveSessionFiles;
8
9
  exports.summarizeClaudeProjectUsageByDay = summarizeClaudeProjectUsageByDay;
9
10
  const promises_1 = __importDefault(require("node:fs/promises"));
@@ -84,6 +85,14 @@ function localDateKey(date) {
84
85
  const day = String(date.getDate()).padStart(2, "0");
85
86
  return `${year}-${month}-${day}`;
86
87
  }
88
+ async function fileMayContainRange(filePath, start) {
89
+ try {
90
+ return (await promises_1.default.stat(filePath)).mtimeMs >= start.getTime();
91
+ }
92
+ catch {
93
+ return true;
94
+ }
95
+ }
87
96
  /**
88
97
  * 判断一条 `type:"user"` 事件是否算作一次用户请求。
89
98
  *
@@ -113,50 +122,18 @@ function isHumanUserMessage(record) {
113
122
  }
114
123
  return true;
115
124
  }
116
- function summarizeProjectTranscript(content, start, end) {
117
- const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
118
- const summary = {
119
- userMessageCount: 0,
120
- apiRequestCount: 0,
121
- inputTokens: 0,
122
- outputTokens: 0,
123
- cacheReadInputTokens: 0,
124
- apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
125
- };
126
- for (const line of lines) {
127
- try {
128
- const record = JSON.parse(line);
129
- const timestamp = isRecord(record) ? getString(record.timestamp) : null;
130
- if (!isRecord(record) || !timestampInRange(timestamp, start, end)) {
131
- continue;
132
- }
133
- if (isHumanUserMessage(record)) {
134
- summary.userMessageCount += 1;
135
- }
136
- if (record.type !== "assistant" || !isRecord(record.message) || !isRecord(record.message.usage)) {
137
- continue;
138
- }
139
- const assistantUsage = summarizeAssistantUsage(record.message, timestamp);
140
- summary.apiRequestCount += 1;
141
- summary.inputTokens += assistantUsage.inputTokens;
142
- summary.outputTokens += assistantUsage.outputTokens;
143
- summary.cacheReadInputTokens += assistantUsage.cacheReadInputTokens;
144
- summary.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([summary.apiEquivalentCost, assistantUsage.apiEquivalentCost]);
145
- }
146
- catch {
147
- continue;
148
- }
149
- }
150
- return summary;
151
- }
152
125
  /**
153
126
  * 从 Claude 本地 project transcript 中统计本周消息数、请求数、token 用量和标准 API 等效成本。
154
127
  */
155
128
  async function summarizeClaudeProjectUsage(start, end) {
129
+ return (await summarizeClaudeProjectUsageCombined(start, end)).weekly;
130
+ }
131
+ /** 单次扫描同时生成周汇总与按天汇总。 */
132
+ async function summarizeClaudeProjectUsageCombined(start, end) {
156
133
  const claudeDataDir = (0, paths_1.getClaudeDataDir)();
157
134
  const projectDir = node_path_1.default.join(claudeDataDir, "projects");
158
135
  const files = await collectProjectJsonlFiles(projectDir);
159
- const totals = {
136
+ const weekly = {
160
137
  userMessageCount: 0,
161
138
  apiRequestCount: 0,
162
139
  inputTokens: 0,
@@ -166,22 +143,75 @@ async function summarizeClaudeProjectUsage(start, end) {
166
143
  matchedFileCount: files.length,
167
144
  claudeDataDir,
168
145
  };
146
+ const daily = new Map();
169
147
  for (const filePath of files) {
148
+ if (!(await fileMayContainRange(filePath, start))) {
149
+ continue;
150
+ }
151
+ let content = "";
170
152
  try {
171
- const content = await promises_1.default.readFile(filePath, "utf8");
172
- const summary = summarizeProjectTranscript(content, start, end);
173
- totals.userMessageCount += summary.userMessageCount;
174
- totals.apiRequestCount += summary.apiRequestCount;
175
- totals.inputTokens += summary.inputTokens;
176
- totals.outputTokens += summary.outputTokens;
177
- totals.cacheReadInputTokens += summary.cacheReadInputTokens;
178
- totals.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([totals.apiEquivalentCost, summary.apiEquivalentCost]);
153
+ content = await promises_1.default.readFile(filePath, "utf8");
179
154
  }
180
155
  catch {
181
156
  continue;
182
157
  }
158
+ const fileSummary = {
159
+ userMessageCount: 0,
160
+ apiRequestCount: 0,
161
+ inputTokens: 0,
162
+ outputTokens: 0,
163
+ cacheReadInputTokens: 0,
164
+ apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
165
+ };
166
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
167
+ for (const line of lines) {
168
+ try {
169
+ const record = JSON.parse(line);
170
+ const timestamp = isRecord(record) ? getString(record.timestamp) : null;
171
+ if (!isRecord(record) || !timestampInRange(timestamp, start, end)) {
172
+ continue;
173
+ }
174
+ const date = localDateKey(new Date(timestamp));
175
+ const day = daily.get(date) ?? {
176
+ date,
177
+ userMessageCount: 0,
178
+ apiRequestCount: 0,
179
+ inputTokens: 0,
180
+ outputTokens: 0,
181
+ cacheReadInputTokens: 0,
182
+ apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
183
+ };
184
+ if (isHumanUserMessage(record)) {
185
+ fileSummary.userMessageCount += 1;
186
+ day.userMessageCount += 1;
187
+ }
188
+ if (record.type === "assistant" && isRecord(record.message) && isRecord(record.message.usage)) {
189
+ const usage = summarizeAssistantUsage(record.message, timestamp);
190
+ fileSummary.apiRequestCount += 1;
191
+ fileSummary.inputTokens += usage.inputTokens;
192
+ fileSummary.outputTokens += usage.outputTokens;
193
+ fileSummary.cacheReadInputTokens += usage.cacheReadInputTokens;
194
+ fileSummary.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([fileSummary.apiEquivalentCost, usage.apiEquivalentCost]);
195
+ day.apiRequestCount += 1;
196
+ day.inputTokens += usage.inputTokens;
197
+ day.outputTokens += usage.outputTokens;
198
+ day.cacheReadInputTokens += usage.cacheReadInputTokens;
199
+ day.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([day.apiEquivalentCost, usage.apiEquivalentCost]);
200
+ }
201
+ daily.set(date, day);
202
+ }
203
+ catch {
204
+ continue;
205
+ }
206
+ }
207
+ weekly.userMessageCount += fileSummary.userMessageCount;
208
+ weekly.apiRequestCount += fileSummary.apiRequestCount;
209
+ weekly.inputTokens += fileSummary.inputTokens;
210
+ weekly.outputTokens += fileSummary.outputTokens;
211
+ weekly.cacheReadInputTokens += fileSummary.cacheReadInputTokens;
212
+ weekly.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([weekly.apiEquivalentCost, fileSummary.apiEquivalentCost]);
183
213
  }
184
- return totals;
214
+ return { weekly, daily };
185
215
  }
186
216
  /**
187
217
  * 找出 ~/.claude/projects 中在指定时间范围内有活动的 session 文件,返回文件路径及结构信息。
@@ -230,57 +260,6 @@ async function findActiveSessionFiles(start, end) {
230
260
  * 按天汇总 Claude project transcript 中的消息数、请求数、token 用量和标准 API 等效成本。
231
261
  */
232
262
  async function summarizeClaudeProjectUsageByDay(start, end) {
233
- const claudeDataDir = (0, paths_1.getClaudeDataDir)();
234
- const projectDir = node_path_1.default.join(claudeDataDir, "projects");
235
- const files = await collectProjectJsonlFiles(projectDir);
236
- const daily = new Map();
237
- for (const filePath of files) {
238
- let content = "";
239
- try {
240
- content = await promises_1.default.readFile(filePath, "utf8");
241
- }
242
- catch {
243
- continue;
244
- }
245
- const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
246
- for (const line of lines) {
247
- try {
248
- const record = JSON.parse(line);
249
- if (!isRecord(record)) {
250
- continue;
251
- }
252
- const timestamp = getString(record.timestamp);
253
- if (!timestampInRange(timestamp, start, end)) {
254
- continue;
255
- }
256
- const date = localDateKey(new Date(timestamp));
257
- const current = daily.get(date) ?? {
258
- date,
259
- userMessageCount: 0,
260
- apiRequestCount: 0,
261
- inputTokens: 0,
262
- outputTokens: 0,
263
- cacheReadInputTokens: 0,
264
- apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
265
- };
266
- if (isHumanUserMessage(record)) {
267
- current.userMessageCount += 1;
268
- }
269
- if (record.type === "assistant" && isRecord(record.message) && isRecord(record.message.usage)) {
270
- const assistantUsage = summarizeAssistantUsage(record.message, timestamp);
271
- current.apiRequestCount += 1;
272
- current.inputTokens += assistantUsage.inputTokens;
273
- current.outputTokens += assistantUsage.outputTokens;
274
- current.cacheReadInputTokens += assistantUsage.cacheReadInputTokens;
275
- current.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([current.apiEquivalentCost, assistantUsage.apiEquivalentCost]);
276
- }
277
- daily.set(date, current);
278
- }
279
- catch {
280
- continue;
281
- }
282
- }
283
- }
284
- return daily;
263
+ return (await summarizeClaudeProjectUsageCombined(start, end)).daily;
285
264
  }
286
265
  //# sourceMappingURL=claude.js.map
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.findActiveCodexSessionFiles = findActiveCodexSessionFiles;
7
+ exports.summarizeCodexSessionUsageCombined = summarizeCodexSessionUsageCombined;
7
8
  exports.summarizeCodexSessionUsage = summarizeCodexSessionUsage;
8
9
  exports.summarizeCodexSessionUsageByDay = summarizeCodexSessionUsageByDay;
9
10
  const promises_1 = __importDefault(require("node:fs/promises"));
@@ -126,8 +127,36 @@ function mergeRolloutContents(contents) {
126
127
  }
127
128
  return mergedLines.length > 0 ? `${mergedLines.join("\n")}\n` : "";
128
129
  }
129
- async function readMergedRollout(group) {
130
- const reads = await Promise.all(group.filePaths.map(async (filePath) => {
130
+ async function readRolloutFileRef(filePath) {
131
+ try {
132
+ const stat = await promises_1.default.stat(filePath, { bigint: true });
133
+ return {
134
+ filePath,
135
+ identityKey: stat.ino === 0n ? null : `${stat.dev}:${stat.ino}`,
136
+ mtimeMs: Number(stat.mtimeMs),
137
+ };
138
+ }
139
+ catch {
140
+ return { filePath, identityKey: null, mtimeMs: null };
141
+ }
142
+ }
143
+ function isCompletePrefix(shorter, longer) {
144
+ return shorter.length === 0
145
+ || ((shorter.endsWith("\n") || shorter.endsWith("\r")) && longer.startsWith(shorter));
146
+ }
147
+ async function readMergedRollout(group, start) {
148
+ const refs = await Promise.all(group.filePaths.map((filePath) => readRolloutFileRef(filePath)));
149
+ if (start && refs.length > 0 && refs.every((ref) => ref.mtimeMs !== null && ref.mtimeMs < start.getTime())) {
150
+ return null;
151
+ }
152
+ const uniqueRefs = new Map();
153
+ for (const ref of refs) {
154
+ const key = ref.identityKey ?? `path:${rolloutPathKey(ref.filePath)}`;
155
+ if (!uniqueRefs.has(key)) {
156
+ uniqueRefs.set(key, ref);
157
+ }
158
+ }
159
+ const reads = await Promise.all([...uniqueRefs.values()].map(async ({ filePath }) => {
131
160
  try {
132
161
  return await promises_1.default.readFile(filePath, "utf8");
133
162
  }
@@ -135,7 +164,18 @@ async function readMergedRollout(group) {
135
164
  return null;
136
165
  }
137
166
  }));
138
- return mergeRolloutContents(reads.filter((content) => content !== null));
167
+ const contents = reads.filter((content) => content !== null);
168
+ if (contents.length === 0) {
169
+ return "";
170
+ }
171
+ if (contents.length === 1 || contents.every((content) => content === contents[0])) {
172
+ return contents[0];
173
+ }
174
+ const longest = contents.reduce((current, content) => content.length > current.length ? content : current);
175
+ if (contents.every((content) => content === longest || isCompletePrefix(content, longest))) {
176
+ return longest;
177
+ }
178
+ return mergeRolloutContents(contents);
139
179
  }
140
180
  function timestampInRange(timestamp, start, end) {
141
181
  if (!timestamp) {
@@ -155,6 +195,9 @@ async function findActiveCodexSessionFiles(start, end) {
155
195
  for (const group of groups) {
156
196
  try {
157
197
  const content = await readMergedRollout(group);
198
+ if (content === null) {
199
+ continue;
200
+ }
158
201
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
159
202
  let hasInRange = false;
160
203
  for (const line of lines) {
@@ -203,7 +246,23 @@ function extractLastTokenUsage(payload) {
203
246
  * turn_id 全局去重(取最早 timestamp),不能在单文件层计数。
204
247
  * - `event_msg` + `payload.type=="token_count"`:一次模型请求,token 取 `info.last_token_usage` 增量。
205
248
  */
206
- function summarizeRollout(content, start, end) {
249
+ function ensureCodexDay(daily, date) {
250
+ let current = daily.get(date);
251
+ if (!current) {
252
+ current = {
253
+ date,
254
+ userMessageCount: 0,
255
+ apiRequestCount: 0,
256
+ inputTokens: 0,
257
+ outputTokens: 0,
258
+ cacheReadInputTokens: 0,
259
+ apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
260
+ };
261
+ daily.set(date, current);
262
+ }
263
+ return current;
264
+ }
265
+ function summarizeRollout(content, start, end, daily) {
207
266
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
208
267
  const result = {
209
268
  turns: [],
@@ -242,21 +301,27 @@ function summarizeRollout(content, start, end) {
242
301
  if (usage) {
243
302
  const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
244
303
  const outputTokens = getNumber(usage.output_tokens);
304
+ const cost = (0, api_equivalent_cost_1.priceApiRequest)({
305
+ provider: "codex",
306
+ timestamp: timestamp,
307
+ model: currentModel,
308
+ inputTokens,
309
+ outputTokens,
310
+ cacheReadInputTokens,
311
+ });
245
312
  result.apiRequestCount += 1;
246
313
  result.inputTokens += inputTokens;
247
314
  result.outputTokens += outputTokens;
248
315
  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
- ]);
316
+ result.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([result.apiEquivalentCost, cost]);
317
+ if (daily) {
318
+ const day = ensureCodexDay(daily, (0, time_1.localDateKey)(new Date(timestamp)));
319
+ day.apiRequestCount += 1;
320
+ day.inputTokens += inputTokens;
321
+ day.outputTokens += outputTokens;
322
+ day.cacheReadInputTokens += cacheReadInputTokens;
323
+ day.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([day.apiEquivalentCost, cost]);
324
+ }
260
325
  }
261
326
  }
262
327
  }
@@ -273,10 +338,11 @@ function summarizeRollout(content, start, end) {
273
338
  * 消息数 = task_started 的 distinct turn_id(跨文件去重)。重放副本会让同一 turn_id 出现在多个文件,
274
339
  * 故用全局 Map<turn_id, minMs> 收集(取最早 timestamp = 真实发生时刻,早于任何重放副本),最后取 size。
275
340
  */
276
- async function summarizeCodexSessionUsage(start, end) {
341
+ async function summarizeCodexSessionUsageCombined(start, end) {
277
342
  const codexDataDir = (0, paths_1.getCodexHome)();
278
343
  const groups = await collectRolloutFileGroups();
279
344
  const turnMinMs = new Map();
345
+ const daily = new Map();
280
346
  let apiRequestCount = 0;
281
347
  let inputTokens = 0;
282
348
  let outputTokens = 0;
@@ -284,14 +350,14 @@ async function summarizeCodexSessionUsage(start, end) {
284
350
  let apiEquivalentCost = (0, api_equivalent_cost_1.emptyApiEquivalentCost)();
285
351
  for (const group of groups) {
286
352
  try {
287
- const content = await readMergedRollout(group);
288
- if (isGuardianRollout(content)) {
353
+ const content = await readMergedRollout(group, start);
354
+ if (content === null || isGuardianRollout(content)) {
289
355
  continue;
290
356
  }
291
- const parsed = summarizeRollout(content, start, end);
357
+ const parsed = summarizeRollout(content, start, end, daily);
292
358
  for (const turn of parsed.turns) {
293
- const prev = turnMinMs.get(turn.turnId);
294
- if (prev === undefined || turn.ms < prev) {
359
+ const previous = turnMinMs.get(turn.turnId);
360
+ if (previous === undefined || turn.ms < previous) {
295
361
  turnMinMs.set(turn.turnId, turn.ms);
296
362
  }
297
363
  }
@@ -305,17 +371,26 @@ async function summarizeCodexSessionUsage(start, end) {
305
371
  continue;
306
372
  }
307
373
  }
374
+ for (const ms of turnMinMs.values()) {
375
+ ensureCodexDay(daily, (0, time_1.localDateKey)(new Date(ms))).userMessageCount += 1;
376
+ }
308
377
  return {
309
- userMessageCount: turnMinMs.size,
310
- apiRequestCount,
311
- inputTokens,
312
- outputTokens,
313
- cacheReadInputTokens,
314
- apiEquivalentCost,
315
- matchedFileCount: groups.length,
316
- codexDataDir,
378
+ weekly: {
379
+ userMessageCount: turnMinMs.size,
380
+ apiRequestCount,
381
+ inputTokens,
382
+ outputTokens,
383
+ cacheReadInputTokens,
384
+ apiEquivalentCost,
385
+ matchedFileCount: groups.length,
386
+ codexDataDir,
387
+ },
388
+ daily,
317
389
  };
318
390
  }
391
+ async function summarizeCodexSessionUsage(start, end) {
392
+ return (await summarizeCodexSessionUsageCombined(start, end)).weekly;
393
+ }
319
394
  /**
320
395
  * 按天汇总 Codex session rollout 中的消息数、请求数、token 用量和标准 API 等效成本。
321
396
  * 与周汇总一致,排除 Codex Desktop 的 guardian 安全审查 rollout。
@@ -324,93 +399,6 @@ async function summarizeCodexSessionUsage(start, end) {
324
399
  * 且重放副本跨天不重复)。token 维度按 token_count 事件 timestamp 的本地日累加。
325
400
  */
326
401
  async function summarizeCodexSessionUsageByDay(start, end) {
327
- const groups = await collectRolloutFileGroups();
328
- const turnMinMs = new Map();
329
- const daily = new Map();
330
- const ensureDay = (date) => {
331
- let current = daily.get(date);
332
- if (!current) {
333
- current = {
334
- date,
335
- userMessageCount: 0,
336
- apiRequestCount: 0,
337
- inputTokens: 0,
338
- outputTokens: 0,
339
- cacheReadInputTokens: 0,
340
- apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
341
- };
342
- daily.set(date, current);
343
- }
344
- return current;
345
- };
346
- for (const group of groups) {
347
- const content = await readMergedRollout(group);
348
- if (isGuardianRollout(content)) {
349
- continue;
350
- }
351
- const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
352
- let currentModel = null;
353
- for (const line of lines) {
354
- try {
355
- const record = JSON.parse(line);
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
- }
364
- continue;
365
- }
366
- const timestamp = getString(record.timestamp);
367
- if (!timestampInRange(timestamp, start, end)) {
368
- continue;
369
- }
370
- if (record.type !== "event_msg") {
371
- continue;
372
- }
373
- const ms = new Date(timestamp).getTime();
374
- if (payload.type === "task_started" && typeof payload.turn_id === "string") {
375
- if (Number.isFinite(ms)) {
376
- const prev = turnMinMs.get(payload.turn_id);
377
- if (prev === undefined || ms < prev) {
378
- turnMinMs.set(payload.turn_id, ms);
379
- }
380
- }
381
- }
382
- if (payload.type === "token_count") {
383
- const usage = extractLastTokenUsage(payload);
384
- if (usage) {
385
- const current = ensureDay((0, time_1.localDateKey)(new Date(ms)));
386
- const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
387
- const outputTokens = getNumber(usage.output_tokens);
388
- current.apiRequestCount += 1;
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
- ]);
403
- }
404
- }
405
- }
406
- catch {
407
- continue;
408
- }
409
- }
410
- }
411
- for (const ms of turnMinMs.values()) {
412
- ensureDay((0, time_1.localDateKey)(new Date(ms))).userMessageCount += 1;
413
- }
414
- return daily;
402
+ return (await summarizeCodexSessionUsageCombined(start, end)).daily;
415
403
  }
416
404
  //# sourceMappingURL=codex-sessions.js.map
package/dist/lib/sync.js CHANGED
@@ -10,6 +10,7 @@ exports.readSyncConfig = readSyncConfig;
10
10
  exports.writeSyncConfig = writeSyncConfig;
11
11
  exports.readSyncStateSync = readSyncStateSync;
12
12
  exports.isSyncDue = isSyncDue;
13
+ exports.formatSyncElapsed = formatSyncElapsed;
13
14
  exports.performSync = performSync;
14
15
  exports.maybeSpawnBackgroundSync = maybeSpawnBackgroundSync;
15
16
  const node_child_process_1 = require("node:child_process");
@@ -159,6 +160,10 @@ function isSyncDue(config, state, now = new Date()) {
159
160
  }
160
161
  return now.getTime() - last >= interval.ms;
161
162
  }
163
+ /** 把同步耗时统一格式化为秒,保留三位小数。 */
164
+ function formatSyncElapsed(elapsedMs) {
165
+ return `${(Math.max(0, elapsedMs) / 1000).toFixed(3)}s`;
166
+ }
162
167
  /**
163
168
  * 导出某个 range 的 bundle,并复制到目标目录下对应的「按周」子目录。
164
169
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {