feat(codex): add weekly report command by sidhanthp · Pull Request #971 · ccusage/ccusage · GitHub
Skip to content

feat(codex): add weekly report command#971

Closed
sidhanthp wants to merge 1 commit into
ccusage:mainfrom
sidhanthp:sid/codex-weekly-command
Closed

feat(codex): add weekly report command#971
sidhanthp wants to merge 1 commit into
ccusage:mainfrom
sidhanthp:sid/codex-weekly-command

Conversation

@sidhanthp

@sidhanthp sidhanthp commented May 2, 2026

Copy link
Copy Markdown

Summary

  • add a Codex weekly subcommand registered alongside daily/monthly/session
  • match Claude weekly semantics with Sunday as the default week start and -w/--start-of-week for configurable week starts
  • aggregate Codex usage by week with the existing report columns and shared CLI options
  • factor daily/monthly/weekly rollup aggregation through a shared Codex report builder
  • document weekly in the Codex package README

Tests

  • pnpm --filter @ccusage/codex test -- --run
  • pnpm --filter @ccusage/codex typecheck
  • pnpm --filter @ccusage/codex build
  • pnpm --filter @ccusage/codex lint
  • pnpm vitest run --changed
  • node apps/codex/dist/index.js weekly --help

Summary by CodeRabbit

  • New Features

    • Added a weekly rollup command to the CLI with timezone, locale, date-range filtering, JSON/table output, and a configurable start-of-week option.
  • Documentation

    • Updated usage examples, aliases, and feature list to include weekly rollups.
  • Refactor

    • Reporting internals unified into a shared bucketing engine powering daily/weekly/monthly rollups.
  • Tests

    • Added/updated tests covering weekly grouping, start-of-week behavior, and timezone boundaries.

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown

@sidhanthp sidhanthp force-pushed the sid/codex-weekly-command branch from 0e15e68 to 0b7c2e2 Compare May 2, 2026 17:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/codex/src/weekly-report.ts`:
- Around line 122-124: The code currently formats the week bucket inside
buildWeeklyReport by calling formatDisplayDate(summary.week, locale, timezone);
instead, keep the raw ISO bucket key in the emitted JSON so rendering/CLI can
localize it later—replace the formatted value with the raw summary.week in the
rows object (i.e., change the week assignment in the buildWeeklyReport/rows.push
logic to use summary.week and remove locale/timezone formatting here).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e836ed8b-33dd-413c-949f-047c2576f1b3

📥 Commits

Reviewing files that changed from the base of the PR and between 1a4bd69 and 0e15e68.

📒 Files selected for processing (5)
  • apps/codex/README.md
  • apps/codex/src/_types.ts
  • apps/codex/src/commands/weekly.ts
  • apps/codex/src/run.ts
  • apps/codex/src/weekly-report.ts

Comment thread apps/codex/src/weekly-report.ts Outdated
@sidhanthp sidhanthp force-pushed the sid/codex-weekly-command branch from 0b7c2e2 to 24861d3 Compare May 2, 2026 17:44
@sidhanthp sidhanthp force-pushed the sid/codex-weekly-command branch from 24861d3 to bfa87a3 Compare May 2, 2026 17:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/codex/src/weekly-report.ts (1)

46-57: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

formatBucket: formatDisplayDate keeps the locale-dependent week field from the prior review cycle.

The refactored delegation to buildBucketedReport with formatBucket: formatDisplayDate still produces a localized display string in every WeeklyReportRow.week, the same concern raised in the previous review round. Three concrete downstream effects remain:

  1. JSON output is locale-fragile. commands/weekly.ts emits { weekly: rows, totals } where rows[n].week is an already-formatted locale string, making programmatic consumers locale-sensitive.
  2. Table's dateFormatter receives a pre-formatted string. commands/weekly.ts passes dateFormatter: (dateStr) => formatDateCompact(dateStr) to ResponsiveTable (line 142), but row.week won't be an ISO date at that point — the cell will either be double-processed or silently pass through, depending on ResponsiveTable's detection logic.
  3. Tests are forced to use weak assertions. Line 125 can only assert toContain('2025') because the exact formatted output is locale-dependent. With a raw ISO key, you could assert expect(first.week).toBe('2025-09-07').
💡 Suggested fix — propagate raw ISO key, format at render time

In buildWeeklyReport, omit the formatBucket field so buildBucketedReport stores the raw ISO key:

 	return buildBucketedReport({
 		bucketField: 'week',
 		events,
-		formatBucket: formatDisplayDate,
 		getBucketKey: (timestamp, timezone) => toWeekKey(timestamp, timezone, startDay),
 		getFilterDateKey: toDateKey,
 		locale: options.locale,
 		pricingSource: options.pricingSource,
 		since: options.since,
 		timezone: options.timezone,
 		until: options.until,
 	});

Then format at the table push site in commands/weekly.ts:

-			row.week,
+			formatDisplayDate(row.week, options.locale, options.timezone),

And tighten the test assertion:

-		expect(first.week).toContain('2025');
+		expect(first.week).toBe('2025-09-07');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/weekly-report.ts` around lines 46 - 57, buildBucketedReport is
being given formatBucket: formatDisplayDate which stores a locale-formatted
string in each WeeklyReportRow.week; remove the formatBucket option from the
buildBucketedReport(...) call in weekly-report.ts so toWeekKey produces and
stores the raw ISO week key, then update the presentation layer
(commands/weekly.ts) to format the ISO key at render time by using dateFormatter
or calling formatDisplayDate/formatDateCompact when pushing rows into
ResponsiveTable; finally tighten tests to assert the raw ISO key (e.g.,
'2025-09-07') instead of locale-dependent substrings.
🧹 Nitpick comments (2)
apps/codex/src/bucketed-report.ts (2)

93-96: 💤 Low value

Consider parallelising pricing lookups with Promise.all.

Serial await inside a for…of loop is fine for in-memory lookups, but if getPricing ever involves I/O the requests will be queued. A Promise.all version is equally readable and future-proof.

♻️ Proposed refactor
-	const modelPricing = new Map<string, Awaited<ReturnType<PricingSource['getPricing']>>>();
-	for (const modelName of uniqueModels) {
-		modelPricing.set(modelName, await pricingSource.getPricing(modelName));
-	}
+	const modelPricing = new Map(
+		await Promise.all(
+			[...uniqueModels].map(async (modelName) =>
+				[modelName, await pricingSource.getPricing(modelName)] as const,
+			),
+		),
+	);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/bucketed-report.ts` around lines 93 - 96, The current loop
awaits pricingSource.getPricing sequentially which will serialize I/O; instead
kick off all requests in parallel using Promise.all on uniqueModels.map(m =>
pricingSource.getPricing(m)), await the array of results, then populate
modelPricing (the Map named modelPricing) by pairing uniqueModels with the
returned pricing values; update references to PricingSource.getPricing and
ensure errors propagate/are handled as before.

67-83: ⚡ Quick win

Redundant double Map lookups — simplify to single get + conditional set.

The same get(…) ?? create(…) / if (!has(…)) set(…) pattern appears twice (lines 67–70 for summaries, lines 73–79 for summary.models). Each miss causes two separate Map operations when one is enough. The idiomatic pattern is a single .get() result checked for nullability.

♻️ Proposed refactor
-		const bucketKey = getBucketKey(event.timestamp, timezone);
-		const summary = summaries.get(bucketKey) ?? createSummary(bucketKey);
-		if (!summaries.has(bucketKey)) {
-			summaries.set(bucketKey, summary);
-		}
+		const bucketKey = getBucketKey(event.timestamp, timezone);
+		let summary = summaries.get(bucketKey);
+		if (summary == null) {
+			summary = createSummary(bucketKey);
+			summaries.set(bucketKey, summary);
+		}

 		addUsage(summary, event);
-		const modelUsage: ModelUsage = summary.models.get(modelName) ?? {
-			...createEmptyUsage(),
-			isFallback: false,
-		};
-		if (!summary.models.has(modelName)) {
-			summary.models.set(modelName, modelUsage);
-		}
+		let modelUsage = summary.models.get(modelName);
+		if (modelUsage == null) {
+			modelUsage = { ...createEmptyUsage(), isFallback: false };
+			summary.models.set(modelName, modelUsage);
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/bucketed-report.ts` around lines 67 - 83, The code does two
Map lookups for both summaries and summary.models; replace each get/has/set pair
with a single get and a conditional set using that local variable: for
summaries, call summaries.get(bucketKey) once, if falsy
createSummary(bucketKey), set it into summaries and assign to summary, then call
addUsage(summary, event); for model usage, call summary.models.get(modelName)
once, if falsy create the ModelUsage via createEmptyUsage(), set it into
summary.models and assign to modelUsage, then call addUsage(modelUsage, event)
and preserve the existing isFallbackModel -> modelUsage.isFallback logic. Use
the existing identifiers summaries, createSummary, addUsage, summary.models,
createEmptyUsage, ModelUsage, modelName, event, and isFallbackModel to locate
and update the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/codex/src/weekly-report.ts`:
- Around line 46-57: buildBucketedReport is being given formatBucket:
formatDisplayDate which stores a locale-formatted string in each
WeeklyReportRow.week; remove the formatBucket option from the
buildBucketedReport(...) call in weekly-report.ts so toWeekKey produces and
stores the raw ISO week key, then update the presentation layer
(commands/weekly.ts) to format the ISO key at render time by using dateFormatter
or calling formatDisplayDate/formatDateCompact when pushing rows into
ResponsiveTable; finally tighten tests to assert the raw ISO key (e.g.,
'2025-09-07') instead of locale-dependent substrings.

---

Nitpick comments:
In `@apps/codex/src/bucketed-report.ts`:
- Around line 93-96: The current loop awaits pricingSource.getPricing
sequentially which will serialize I/O; instead kick off all requests in parallel
using Promise.all on uniqueModels.map(m => pricingSource.getPricing(m)), await
the array of results, then populate modelPricing (the Map named modelPricing) by
pairing uniqueModels with the returned pricing values; update references to
PricingSource.getPricing and ensure errors propagate/are handled as before.
- Around line 67-83: The code does two Map lookups for both summaries and
summary.models; replace each get/has/set pair with a single get and a
conditional set using that local variable: for summaries, call
summaries.get(bucketKey) once, if falsy createSummary(bucketKey), set it into
summaries and assign to summary, then call addUsage(summary, event); for model
usage, call summary.models.get(modelName) once, if falsy create the ModelUsage
via createEmptyUsage(), set it into summary.models and assign to modelUsage,
then call addUsage(modelUsage, event) and preserve the existing isFallbackModel
-> modelUsage.isFallback logic. Use the existing identifiers summaries,
createSummary, addUsage, summary.models, createEmptyUsage, ModelUsage,
modelName, event, and isFallbackModel to locate and update the code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d03e468-6f4b-48e7-b2db-8cb42759c070

📥 Commits

Reviewing files that changed from the base of the PR and between 0b7c2e2 and 24861d3.

📒 Files selected for processing (9)
  • apps/codex/README.md
  • apps/codex/src/_consts.ts
  • apps/codex/src/_types.ts
  • apps/codex/src/bucketed-report.ts
  • apps/codex/src/commands/weekly.ts
  • apps/codex/src/daily-report.ts
  • apps/codex/src/monthly-report.ts
  • apps/codex/src/run.ts
  • apps/codex/src/weekly-report.ts
✅ Files skipped from review due to trivial changes (4)
  • apps/codex/src/run.ts
  • apps/codex/src/_types.ts
  • apps/codex/README.md
  • apps/codex/src/daily-report.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/codex/src/monthly-report.ts

@sidhanthp sidhanthp changed the title Add weekly report command for Codex feat(codex): add weekly report command May 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/codex/src/daily-report.ts (1)

5-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

locale in DailyReportOptions is silently ignored — remove it.

buildDailyReport accepts options.locale but never uses or forwards it to buildBucketedReport. The date field in DailyReportRow is always a plain string from toDateKey, without any locale-aware formatting. Leaving this field in the exported type creates a false contract with callers who pass locale expecting it to influence date output.

Proposed fix
 export type DailyReportOptions = {
 	timezone?: string;
-	locale?: string;
 	since?: string;
 	until?: string;
 	pricingSource: PricingSource;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/daily-report.ts` around lines 5 - 11, The exported
DailyReportOptions currently includes locale but buildDailyReport does not use
or forward it (buildDailyReport, buildBucketedReport) and DailyReportRow.date is
generated via toDateKey (plain string), so remove the unused locale from the
public type to avoid a false contract; update DailyReportOptions to drop the
locale property and any references/parameters expecting it in buildDailyReport
and related callers, or alternatively, if locale-aware formatting is desired,
wire options.locale through buildDailyReport into buildBucketedReport and use it
when formatting the date (e.g., replace toDateKey usage with a locale-aware
formatter) — choose one approach and make the type and implementation consistent
(adjust DailyReportOptions, buildDailyReport, buildBucketedReport, and
DailyReportRow generation accordingly).
🧹 Nitpick comments (1)
apps/codex/src/bucketed-report.ts (1)

89-92: ⚡ Quick win

Parallelize independent pricing fetches with Promise.all.

The sequential await inside the for...of loop serializes pricing lookups that are entirely independent of each other. If each async call takes one second, four sequential calls take four seconds whereas running them in parallel takes approximately one second. Use Promise.all to fan them out concurrently:

⚡ Proposed refactor
-	const modelPricing = new Map<string, Awaited<ReturnType<PricingSource['getPricing']>>>();
-	for (const modelName of uniqueModels) {
-		modelPricing.set(modelName, await pricingSource.getPricing(modelName));
-	}
+	const modelPricing = new Map<string, Awaited<ReturnType<PricingSource['getPricing']>>>(
+		await Promise.all(
+			Array.from(uniqueModels).map(async (modelName) => [
+				modelName,
+				await pricingSource.getPricing(modelName),
+			] as const),
+		),
+	);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/bucketed-report.ts` around lines 89 - 92, The current code
serially awaits pricingSource.getPricing for each model in the for...of loop
(using uniqueModels and modelPricing), causing unnecessary latency; instead,
fire all requests concurrently using Promise.all: map uniqueModels to promises
that call pricingSource.getPricing(modelName), await Promise.all to get results,
then populate modelPricing (the Map) by pairing each modelName with its
corresponding resolved pricing; this retains use of modelPricing and
pricingSource.getPricing but replaces the sequential await with parallelized
fetches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/codex/src/daily-report.ts`:
- Around line 5-11: The exported DailyReportOptions currently includes locale
but buildDailyReport does not use or forward it (buildDailyReport,
buildBucketedReport) and DailyReportRow.date is generated via toDateKey (plain
string), so remove the unused locale from the public type to avoid a false
contract; update DailyReportOptions to drop the locale property and any
references/parameters expecting it in buildDailyReport and related callers, or
alternatively, if locale-aware formatting is desired, wire options.locale
through buildDailyReport into buildBucketedReport and use it when formatting the
date (e.g., replace toDateKey usage with a locale-aware formatter) — choose one
approach and make the type and implementation consistent (adjust
DailyReportOptions, buildDailyReport, buildBucketedReport, and DailyReportRow
generation accordingly).

---

Nitpick comments:
In `@apps/codex/src/bucketed-report.ts`:
- Around line 89-92: The current code serially awaits pricingSource.getPricing
for each model in the for...of loop (using uniqueModels and modelPricing),
causing unnecessary latency; instead, fire all requests concurrently using
Promise.all: map uniqueModels to promises that call
pricingSource.getPricing(modelName), await Promise.all to get results, then
populate modelPricing (the Map) by pairing each modelName with its corresponding
resolved pricing; this retains use of modelPricing and pricingSource.getPricing
but replaces the sequential await with parallelized fetches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c61d1761-d6f4-4929-877e-8af819d3bfc7

📥 Commits

Reviewing files that changed from the base of the PR and between 24861d3 and bfa87a3.

📒 Files selected for processing (9)
  • apps/codex/README.md
  • apps/codex/src/_consts.ts
  • apps/codex/src/_types.ts
  • apps/codex/src/bucketed-report.ts
  • apps/codex/src/commands/weekly.ts
  • apps/codex/src/daily-report.ts
  • apps/codex/src/monthly-report.ts
  • apps/codex/src/run.ts
  • apps/codex/src/weekly-report.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/codex/README.md
  • apps/codex/src/_consts.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/codex/src/run.ts
  • apps/codex/src/monthly-report.ts
  • apps/codex/src/commands/weekly.ts
  • apps/codex/src/weekly-report.ts

@ryoppippi ryoppippi closed this May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants