Skip to content

Conversation

@ben-vargas
Copy link
Contributor

@ben-vargas ben-vargas commented Jan 17, 2026

Summary

Adds @ccusage/omni, a new unified CLI that aggregates usage data from all supported AI coding assistants (Claude Code, Codex, OpenCode, Pi-agent) into a single report.

  • Combines daily, monthly, and session reports across all data sources
  • Normalizes token semantics while preserving source-faithful totals
  • Shows cost-only grand totals (token semantics differ by source)
  • Supports filtering by source, date range, and timezone
  • Includes performance optimizations for date-filtered data loading

Disclaimer: Majority code generated by gpt-5.2-codex high with minimal manual review - feel free to make changes/improvements, take and make your own in another branch, etc. I'm not heavily vested in the code itself, just wanted a way to run/check all harnesses combined.

Changes

New Package: @ccusage/omni

  • src/_normalizers/ - Per-source data normalizers (claude, codex, opencode, pi)
  • src/data-aggregator.ts - Main aggregation logic with parallel loading
  • src/commands/ - CLI commands (daily, monthly, session)
  • Unified types preserving source-faithful token calculations

Required Changes to Existing Packages

  • @ccusage/codex: Added exports for daily-report, monthly-report, session-report, data-loader
  • @ccusage/opencode: Created report builder functions, added exports
  • @ccusage/pi: Added exports for data-loader

Performance Optimizations

  • Added date prefiltering to all loaders to avoid loading unnecessary data
  • Cached DateTimeFormat instances in Codex for faster date/month key generation
  • OpenCode now skips scanning old session directories when date filters apply
  • Deduplicated LiteLLM pricing fetches - Added shared static cache to LiteLLMPricingFetcher so parallel source loaders share a single network request

Usage

# All sources, daily report
npx @ccusage/omni@latest daily

# Specific sources only
npx @ccusage/omni@latest monthly --sources claude,codex

# Last 7 days, session report
npx @ccusage/omni@latest session --days 7

# JSON output
npx @ccusage/omni@latest daily --json

Notes

  • Amp is intentionally excluded from v1 due to schema/billing differences (credits vs subscription)
  • Codex cache values are marked with † to indicate subset-of-input semantics
  • Grand totals show cost only; token totals are per-source (different semantics)
CleanShot 2026-01-17 at 15 22 55@2x

Summary by CodeRabbit

  • New Features

    • Added comprehensive CLI reporting tool with daily, monthly, and session usage reports.
    • Introduced date-range filtering (since/until) across all usage data sources.
    • Added multi-source cost aggregation and totals across multiple platforms.
    • Enhanced reporting with JSON and formatted table output options.
    • Added timezone-aware date handling for accurate time-period grouping.
  • Improvements

    • Optimized data loading with file-level filtering and deduplication.
    • Added pricing fetch caching for improved performance.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 17, 2026

📝 Walkthrough

Walkthrough

Introduces a new Omni CLI app for aggregated usage reporting across multiple sources (Claude, Codex, OpenCode, Pi) with unified data normalization. Adds date filtering and timezone-aware support to existing data loaders, refactors report builders into reusable modules, enhances pricing fetcher with shared caching, and adjusts terminal table column logic for improved Models display.

Changes

Cohort / File(s) Summary
Omni CLI Application
apps/omni/
New comprehensive CLI app with 900+ lines spanning configuration (package.json, tsconfig.json, tsdown.config.ts, vitest.config.ts, eslint.config.js), type definitions (_types.ts, _consts.ts), data normalizers for four sources (_normalizers/), multi-source data aggregator (data-aggregator.ts), three reporting commands (commands/), and CLI infrastructure (run.ts, index.ts, logger.ts). Unifies usage data from Claude, Codex, OpenCode, and Pi with daily/monthly/session reports, cost summaries, and timezone support.
Codex Date Filtering & Export Maps
apps/codex/package.json, apps/codex/src/data-loader.ts, apps/codex/tsdown.config.ts
Added root and publishConfig exports fields mapping entry points to source/dist files. Extended LoadOptions with since/until/timezone fields and implemented per-event date range filtering. Expanded build entry points to include data-loader and report modules.
Codex Report Builders & Utilities
apps/codex/src/daily-report.ts, apps/codex/src/monthly-report.ts, apps/codex/src/date-utils.ts
Added formatDate option to both daily and monthly report options with conditional formatting. Enhanced date-utils with timezone caching, Intl formatter caching, and improved safeTimeZone validation; updated toDateKey and toMonthKey to use cached formatters.
OpenCode Report Builders & Refactoring
apps/opencode/src/daily-report.ts, apps/opencode/src/monthly-report.ts, apps/opencode/src/session-report.ts, apps/opencode/src/commands/daily.ts, apps/opencode/src/commands/monthly.ts, apps/opencode/src/commands/session.ts, apps/opencode/tsdown.config.ts
Extracted aggregation logic from commands into dedicated report builder modules (daily, monthly, session) with reusable DailyReportRow, MonthlyReportRow, SessionReportRow types. Refactored three command modules to use builders instead of inline aggregation. Expanded tsdown entry points and added platform/target/shims config. Added exports field to package.json.
OpenCode Data Loader Date Filtering
apps/opencode/src/data-loader.ts, apps/opencode/package.json
Introduced OpenCodeMessageLoadOptions with since/until fields and helper functions (normalizeDateInput, getDateKeyFromTimestamp, isWithinRange). Added per-session directory date checks and per-message timestamp filtering. Exported new data-loader entry point via package exports.
Pricing & Terminal Utilities
packages/internal/src/pricing.ts, packages/terminal/src/table.ts
Added timeoutMs option to LiteLLMPricingFetcherOptions with shared online pricing cache and in-flight request deduplication. Replaced direct fetch logic with loadSharedOnlinePricing method. Updated table column-width logic to dynamically detect Models column and apply context-aware width calculations instead of hard-coded index checks.
Pi Data Loader & Build Config
apps/pi/src/data-loader.ts, apps/pi/package.json, apps/pi/tsdown.config.ts
Added filterFilesBySince helper for pre-filtering files by mtime before processing; uses timezone-aware date formatting. Exported data-loader entry point and added src/data-loader.ts to build entry points.
Configuration & Linting
eslint.config.js
Added OMNI_PLAN.md to ESLint ignore patterns.

Sequence Diagram(s)

sequenceDiagram
    participant User as CLI User
    participant Run as run.ts<br/>(CLI Orchestrator)
    participant Cmd as Command<br/>(daily/monthly/session)
    participant Agg as DataAggregator<br/>(loadCombined*)
    participant Norm as Normalizers<br/>(normalize*)
    participant Src1 as Claude<br/>Loader
    participant Src2 as Codex<br/>Loader
    participant Src3 as OpenCode<br/>Loader
    participant Src4 as Pi<br/>Loader

    User->>Run: ccusage-omni daily --sources claude,codex
    Run->>Cmd: run(args, context)
    Cmd->>Agg: loadCombinedDailyData({<br/>sources, since, until,<br/>timezone, locale, offline})
    par Multi-source Loading
        Agg->>Src1: loadClaudeDaily(options)
        Src1-->>Agg: daily data
        Agg->>Src2: buildDailyReport(options)
        Src2-->>Agg: daily data
        Agg->>Src3: (filtered/skipped)
        Agg->>Src4: (filtered/skipped)
    end
    par Normalization
        Agg->>Norm: normalizeClaudeDaily(data)
        Norm-->>Agg: UnifiedDailyUsage
        Agg->>Norm: normalizeCodexDaily(data)
        Norm-->>Agg: UnifiedDailyUsage
    end
    Agg->>Agg: calculateTotals(all data)
    Agg-->>Cmd: CombinedResult<br/>{data, totals}
    Cmd->>Cmd: Format & render<br/>table/JSON
    Cmd-->>User: Daily report
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • ryoppippi

Poem

🐰 Hops through four sources with grace,
Omni gathers data from every place,
With dates filtered, normalized with care,
Reports unified, costs laid bare!
Cache marks and timezones dance in the light,
A CLI symphony, perfectly right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: introducing a new 'omni' package for unified usage aggregation across multiple AI coding assistants, which aligns with the PR's primary objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@apps/omni/src/_normalizers/codex.ts`:
- Around line 4-29: In normalizeCodexDaily and normalizeCodexMonthly, fix the
incorrect field and models handling: assign cacheReadTokens from
data.cacheReadTokens (not data.cachedInputTokens) and set models to data.models
directly (not Object.keys(data.models)); also guard the optional costUSD and
totalTokens fields by using null-coalescing/defaults (e.g., fallback to 0 or
null) when mapping data.costUSD and data.totalTokens so the returned
UnifiedDailyUsage/UnifiedMonthlyUsage never receives undefined.

In `@apps/omni/src/_normalizers/pi.ts`:
- Around line 59-69: The test fixture's modelsUsed array in the data object
(satisfies DailyUsageWithSource) uses an outdated model name '[pi]
claude-opus-4-5'; update that string to a current Claude 4 pricing name such as
'claude-opus-4-20250514' (or 'claude-sonnet-4-20250514') and ensure any related
entries in modelBreakdowns or other test fixtures match the same updated model
identifier so the test data aligns with current Claude 4 pricing names.

In `@apps/opencode/src/data-loader.ts`:
- Around line 365-371: The current date-filter branch uses Date.now() when
message.time.created is missing which can mistakenly include messages; update
the logic in the block that computes createdMs (the variables/identifiers:
createdMs, message.time.created, hasDateFilter, getDateKeyFromTimestamp,
isWithinRange, since, until) so that when hasDateFilter is true you do NOT fall
back to Date.now(): first prefer the file mtime value already available (use the
existing file mtime variable if present), and if message.time.created is missing
and no file mtime is available then skip this message (continue) instead of
using Date.now(); only call getDateKeyFromTimestamp and isWithinRange when you
have a real timestamp.
- Around line 130-142: normalizeDateInput currently returns undefined for
non-null/non-empty invalid date strings, silently disabling filters; change it
to surface errors by throwing a descriptive Error when a trimmed, non-empty
value does not match the expected YYYYMMDD/ YYYY-MM-DD pattern. Update the
function normalizeDateInput to validate the input (use the existing trimmed and
compact logic) and throw new Error(`Invalid date filter: "${value}"`) (or
similar) instead of returning undefined for invalid formats so callers can
catch/log and avoid assuming the filter was applied.

In `@OMNI_PLAN.md`:
- Around line 230-232: The fenced code block containing the directory tree
snippet starting with "apps/omni/" should include a language identifier (e.g.,
text) to satisfy markdownlint rule MD040; update the fenced block in
OMNI_PLAN.md from ``` to ```text so the directory tree (apps/omni/ ├── src/) is
marked as plain text.
- Around line 73-95: The JSON code blocks containing the "exports" and
"publishConfig" objects contain hard tab characters that trigger MD010; replace
all hard tabs with spaces in those fenced JSON snippets (the blocks showing
"exports": { ... } and "publishConfig": { ... }) so the markdown linter no
longer flags them, ensuring indentation uses spaces consistently throughout the
OMNI_PLAN.md examples.
- Around line 9-16: The markdown table under the "Supported Sources (v1):"
heading needs blank lines before and after it to satisfy MD058; edit the
markdown so there is an empty line between the heading and the table header row
and another empty line after the table (i.e., add a blank line above the line
starting with "| Source" and a blank line after the final table row) to resolve
the lint warning.
🧹 Nitpick comments (12)
apps/codex/src/daily-report.ts (1)

126-206: Consider adding a test case for formatDate: false.

The existing test validates the default formatting behavior, but adding a test case with formatDate: false would ensure the raw date key passthrough is covered.

💡 Example test addition
it('returns raw date keys when formatDate is false', async () => {
	const stubPricingSource: PricingSource = {
		async getPricing(): Promise<ModelPricing> {
			return { inputCostPerMToken: 1, cachedInputCostPerMToken: 0.1, outputCostPerMToken: 2 };
		},
	};
	const report = await buildDailyReport(
		[{
			sessionId: 's1',
			timestamp: '2025-09-11T03:00:00.000Z',
			model: 'gpt-5',
			inputTokens: 100,
			cachedInputTokens: 0,
			outputTokens: 50,
			reasoningOutputTokens: 0,
			totalTokens: 150,
		}],
		{ pricingSource: stubPricingSource, formatDate: false },
	);
	expect(report[0]!.date).toBe('2025-09-11');
});
apps/omni/src/run.ts (1)

16-29: Consider deriving the alternate name from the package name.

The hardcoded 'ccusage-omni' string duplicates knowledge that could potentially be derived. If the package name in package.json is @ccusage/omni, you might consider deriving the alternate form programmatically (e.g., name.replace('@ccusage/', 'ccusage-')).

That said, if this is intentional for handling a specific invocation pattern, the current approach is acceptable.

♻️ Optional: derive alternate name
 export async function run(): Promise<void> {
 	let args = process.argv.slice(2);
-	if (args[0] === name || args[0] === 'ccusage-omni') {
+	const alternateName = name.replace('@ccusage/', 'ccusage-');
+	if (args[0] === name || args[0] === alternateName) {
 		args = args.slice(1);
 	}
apps/omni/src/commands/session.ts (1)

83-100: Prefer Result.try() over try/catch for parse/normalize flow.
Aligns with the project’s functional error-handling guideline.

♻️ Suggested refactor
-import { log, logger } from '../logger.ts';
+import { log, logger } from '../logger.ts';
+import { Result } from '@praha/byethrow';

-		try {
-			sources = parseSources(ctx.values.sources);
-			since = normalizeDateInput(ctx.values.since);
-			until = normalizeDateInput(ctx.values.until);
-
-			if (ctx.values.days != null) {
-				const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
-				since = range.since;
-				until = range.until;
-			}
-		} catch (error) {
-			logger.error(String(error));
-			process.exit(1);
-		}
+		const parsed = Result.try(() => {
+			const parsedSources = parseSources(ctx.values.sources);
+			let parsedSince = normalizeDateInput(ctx.values.since);
+			let parsedUntil = normalizeDateInput(ctx.values.until);
+			if (ctx.values.days != null) {
+				const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
+				parsedSince = range.since;
+				parsedUntil = range.until;
+			}
+			return { parsedSources, parsedSince, parsedUntil };
+		});
+
+		if (Result.isFailure(parsed)) {
+			logger.error(String(parsed.error));
+			process.exit(1);
+		}
+
+		sources = parsed.value.parsedSources;
+		since = parsed.value.parsedSince;
+		until = parsed.value.parsedUntil;
packages/internal/src/pricing.ts (1)

91-117: Consider clearing shared caches when clearCache() is used.

With sharedOnlinePricing, per-instance clearCache() no longer forces a refresh. If refresh behavior is expected, consider evicting shared entries for this.url as well.

apps/ccusage/src/data-loader.ts (1)

719-737: Make date-key formatting explicit in filterFilesBySince.

Passing DEFAULT_LOCALE here keeps the date key stable even if the formatDate default ever changes.

🔧 Suggested fix
-					const dateKey = formatDate(new Date(fileStat.mtimeMs).toISOString(), timezone).replace(
-						/-/g,
-						'',
-					);
+					const dateKey = formatDate(
+						new Date(fileStat.mtimeMs).toISOString(),
+						timezone,
+						DEFAULT_LOCALE,
+					).replace(/-/g, '');
apps/codex/src/data-loader.ts (2)

192-206: Consider extracting shared date normalization logic.

This normalizeDateInput function duplicates logic from apps/omni/src/data-aggregator.ts (lines 560-571), though with different output formats (compact YYYYMMDD here vs YYYY-MM-DD there). The behavioral difference is intentional for dateKey comparisons, but consider extracting a shared helper that accepts a format parameter to reduce duplication.


244-257: File mtime filtering only checks since, not until.

The file-level prefiltering skips files with mtime < since but doesn't check until. This is acceptable as a performance optimization—files modified after the until date may still contain events within the range. However, add a brief comment explaining this asymmetry to clarify intent for future maintainers.

📝 Suggested comment
 		for (const file of files) {
+			// Optimization: Skip files modified before `since` as they cannot contain
+			// relevant events. We don't filter by `until` here since file mtime may
+			// be later than the events it contains.
 			if (since != null) {
 				try {
apps/omni/src/commands/monthly.ts (1)

88-92: Silent override when both days and since/until are provided.

When days is specified, it silently overrides any since/until values. Consider logging a warning to inform users that their explicit date range is being ignored.

📝 Suggested improvement
 			if (ctx.values.days != null) {
+				if (ctx.values.since != null || ctx.values.until != null) {
+					logger.warn('--days overrides --since/--until; ignoring explicit date range');
+				}
 				const range = resolveDateRangeFromDays(ctx.values.days, ctx.values.timezone);
 				since = range.since;
 				until = range.until;
 			}
apps/opencode/src/daily-report.ts (1)

37-44: Sequential await in loop may impact performance for large datasets.

Each calculateCostForEntry call is awaited sequentially. If the pricing fetcher involves network calls without caching, this could be slow. Consider batching cost calculations or using Promise.all if entries can be processed in parallel.

However, if pricingFetcher caches results internally (which is typical), this may be acceptable.

♻️ Alternative using Promise.all for parallel cost calculation
+	// Pre-calculate all costs in parallel
+	const costs = await Promise.all(
+		entries.map((entry) => calculateCostForEntry(entry, options.pricingFetcher)),
+	);
+	const costByEntry = new Map(entries.map((entry, i) => [entry, costs[i]!]));
+
 	for (const [date, dayEntries] of Object.entries(entriesByDate)) {
 		let inputTokens = 0;
 		let outputTokens = 0;
 		let cacheCreationTokens = 0;
 		let cacheReadTokens = 0;
 		let totalCost = 0;
 		const modelsSet = new Set<string>();

 		for (const entry of dayEntries) {
 			inputTokens += entry.usage.inputTokens;
 			outputTokens += entry.usage.outputTokens;
 			cacheCreationTokens += entry.usage.cacheCreationInputTokens;
 			cacheReadTokens += entry.usage.cacheReadInputTokens;
-			totalCost += await calculateCostForEntry(entry, options.pricingFetcher);
+			totalCost += costByEntry.get(entry) ?? 0;
 			modelsSet.add(entry.model);
 		}
apps/opencode/src/monthly-report.ts (1)

21-63: High code duplication with daily-report.ts.

The buildMonthlyReport function is nearly identical to buildDailyReport, differing only in the grouping key (slice(0, 7) vs split('T')[0]). Consider extracting a shared buildReport helper parameterized by a grouping function to reduce duplication.

♻️ Conceptual refactor to reduce duplication
// Shared helper in a common module
type ReportOptions<T> = {
  pricingFetcher: LiteLLMPricingFetcher;
  groupKey: (entry: LoadedUsageEntry) => string;
  keyName: 'date' | 'month';
};

async function buildReport<T extends { [K in keyof T]: T[K] }>(
  entries: LoadedUsageEntry[],
  options: ReportOptions<T>,
): Promise<T[]> {
  // Shared aggregation logic
}

// Then in daily-report.ts:
export const buildDailyReport = (entries, options) =>
  buildReport(entries, {
    ...options,
    groupKey: (e) => e.timestamp.toISOString().split('T')[0]!,
    keyName: 'date',
  });
apps/omni/src/commands/_shared.ts (1)

23-39: Consider handling empty bySource array.

If totals.bySource is empty (e.g., no data from any source), the output would show only the header and total line. While this may be acceptable, consider adding a check or a "No source data" message for clarity.

📝 Optional guard for empty bySource
 export function formatCostSummary(totals: CombinedTotals): string {
+	if (totals.bySource.length === 0) {
+		return `By Source (Cost)\n  TOTAL ........ ${formatCurrency(totals.costUSD)}`;
+	}
+
 	const labels = totals.bySource.map((entry) => SOURCE_LABELS[entry.source]);
apps/opencode/src/session-report.ts (1)

25-28: Add JSDoc for exported buildSessionReport.

Exported functions in this area should include brief JSDoc for purpose, parameters, and return value. As per coding guidelines, add a short doc block above the function.

Comment on lines 4 to 29
export function normalizeCodexDaily(data: DailyReportRow): UnifiedDailyUsage {
return {
source: 'codex',
date: data.date,
inputTokens: data.inputTokens,
outputTokens: data.outputTokens,
cacheReadTokens: data.cachedInputTokens,
cacheCreationTokens: 0,
totalTokens: data.totalTokens,
costUSD: data.costUSD,
models: Object.keys(data.models),
};
}

export function normalizeCodexMonthly(data: MonthlyReportRow): UnifiedMonthlyUsage {
return {
source: 'codex',
month: data.month,
inputTokens: data.inputTokens,
outputTokens: data.outputTokens,
cacheReadTokens: data.cachedInputTokens,
cacheCreationTokens: 0,
totalTokens: data.totalTokens,
costUSD: data.costUSD,
models: Object.keys(data.models),
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for Codex type definitions in node_modules and project files
find . -path ./node_modules -prune -o -name "*.ts" -type f -print | xargs grep -l "DailyReportRow\|MonthlyReportRow" 2>/dev/null | head -20

Repository: ryoppippi/ccusage

Length of output: 178


🏁 Script executed:

# Search more broadly for `@ccusage/codex` or codex types
fd -t f "\.ts$" | xargs grep -l "DailyReportRow\|MonthlyReportRow" 2>/dev/null | grep -v node_modules | head -20

Repository: ryoppippi/ccusage

Length of output: 178


🏁 Script executed:

# Check the actual normalizer file to see what fields are being accessed
cat -n apps/omni/src/_normalizers/codex.ts 2>/dev/null || echo "File not found"

Repository: ryoppippi/ccusage

Length of output: 178


🏁 Script executed:

# Search for type definitions or interfaces that might define these row types
fd -t f "\.ts$" -path "*/codex/*" | head -20

Repository: ryoppippi/ccusage

Length of output: 178


🏁 Script executed:

# Look for any types file in the codex-related packages
find . -name "*types*" -o -name "*type.ts" 2>/dev/null | grep -i codex | head -20

Repository: ryoppippi/ccusage

Length of output: 178


🌐 Web query:

@ccusage/codex DailyReportRow MonthlyReportRow types definition

💡 Result:

I couldn't find published TypeScript declarations, but the ccusage JSON output docs show the report row shapes — here are TypeScript interfaces inferred from those examples:

  • DailyReportRow
    interface DailyReportRow {
    date: string; // "YYYY-MM-DD"
    models: string[]; // model names used that day
    inputTokens: number;
    outputTokens: number;
    cacheCreationTokens?: number;
    cacheReadTokens?: number;
    totalTokens?: number;
    costUSD?: number;
    }

  • MonthlyReportRow
    interface MonthlyReportRow {
    month: string; // "YYYY-MM"
    models: string[]; // model names used that month
    inputTokens: number;
    outputTokens: number;
    cacheCreationTokens?: number;
    cacheReadTokens?: number;
    totalTokens?: number;
    costUSD?: number;
    }

Source: ccusage JSON output examples (daily & monthly). [1]

References

  1. ccusage — JSON Output (daily/monthly examples). [ccusage JSON Output]. (ccusage.ryoppippi.com)

Citations:


🌐 Web query:

ryoppippi/ccusage codex types DailyReportRow cachedInputTokens

💡 Result:

Short answer: In the @ccusage/codex codebase the daily-report row uses the Codex token field name cachedInputTokens (from cumulative events cached_input_tokens) and the JSON/TypeScript report rows expose it as cachedInputTokens / cacheReadTokens depending on report (JSON output uses cachedInputTokens / cache_read_tokens semantics; the CLI display may show "Cache Read" / cacheReadTokens). See the Codex report schema and repo docs. [1][2][3]

Sources:

  • ccusage JSON output / daily report docs (shows cache fields in daily JSON). [1]
  • ryoppippi/ccusage GitHub repository (apps/codex package and README). [2]
  • Codex docs describing token_count fields including cached_input_tokens and how daily rows are built. [3]

🌐 Web query:

@ccusage/codex npm package types github

💡 Result:

You can find the package and source here:

  • GitHub repo (source, including the codex app at apps/codex — entry point apps/codex/src/index.ts): ryoppippi/ccusage. [1]
  • npm / package metadata and docs for the scoped package @ccusage/codex (published CLI package). [2]

Notes:

  • The repo is TypeScript-based and the codex package lives under apps/codex (so typings are authored in the source). [1][2]

Sources:
[1] ryoppippi/ccusage (GitHub).
[2] @ccusage/codex package documentation / registry (ccusage / npm listings).


Fix field name and Object.keys() bug in Codex normalizers.

Per the official ccusage JSON output documentation, the Codex report row types use cacheReadTokens (not cachedInputTokens), and models is already a string[], not an object—so Object.keys(data.models) will return numeric indices ["0", "1", ...] instead of model names.

Change line assignments:

  • cacheReadTokens: data.cacheReadTokens (not data.cachedInputTokens)
  • models: data.models (not Object.keys(data.models))

Additionally, costUSD and totalTokens are optional fields in the interface; add null-coalescing guards or ensure upstream always populates them.

🤖 Prompt for AI Agents
In `@apps/omni/src/_normalizers/codex.ts` around lines 4 - 29, In
normalizeCodexDaily and normalizeCodexMonthly, fix the incorrect field and
models handling: assign cacheReadTokens from data.cacheReadTokens (not
data.cachedInputTokens) and set models to data.models directly (not
Object.keys(data.models)); also guard the optional costUSD and totalTokens
fields by using null-coalescing/defaults (e.g., fallback to 0 or null) when
mapping data.costUSD and data.totalTokens so the returned
UnifiedDailyUsage/UnifiedMonthlyUsage never receives undefined.

OMNI_PLAN.md Outdated
Comment on lines 9 to 16
**Supported Sources (v1):**
| Source | Package | Data Directory | Env Override |
|--------|---------|----------------|--------------|
| Claude Code | `ccusage` | `~/.claude/projects/` or `~/.config/claude/projects/` | `CLAUDE_CONFIG_DIR` |
| OpenAI Codex | `@ccusage/codex` | `~/.codex/sessions/` | `CODEX_HOME` |
| OpenCode | `@ccusage/opencode` | `~/.local/share/opencode/storage/message/` | `OPENCODE_DATA_DIR` |
| Pi-agent | `@ccusage/pi` | `~/.pi/agent/sessions/` | `PI_AGENT_DIR` |

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add blank lines around tables (MD058).

The “Supported Sources (v1)” table isn’t surrounded by blank lines, which trips markdownlint.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

10-10: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
In `@OMNI_PLAN.md` around lines 9 - 16, The markdown table under the "Supported
Sources (v1):" heading needs blank lines before and after it to satisfy MD058;
edit the markdown so there is an empty line between the heading and the table
header row and another empty line after the table (i.e., add a blank line above
the line starting with "| Source" and a blank line after the final table row) to
resolve the lint warning.

OMNI_PLAN.md Outdated
Comment on lines 73 to 95
```json
{
"exports": {
".": "./src/index.ts",
"./data-loader": "./src/data-loader.ts",
"./daily-report": "./src/daily-report.ts",
"./monthly-report": "./src/monthly-report.ts",
"./session-report": "./src/session-report.ts",
"./types": "./src/_types.ts",
"./package.json": "./package.json"
},
"publishConfig": {
"exports": {
".": "./dist/index.js",
"./data-loader": "./dist/data-loader.js",
"./daily-report": "./dist/daily-report.js",
"./monthly-report": "./dist/monthly-report.js",
"./session-report": "./dist/session-report.js",
"./types": "./dist/_types.js",
"./package.json": "./package.json"
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace hard tabs in code snippets (MD010).

Markdownlint flags hard tabs in these JSON snippets; switch to spaces to keep lint clean.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

75-75: Hard tabs
Column: 1

(MD010, no-hard-tabs)


76-76: Hard tabs
Column: 1

(MD010, no-hard-tabs)


77-77: Hard tabs
Column: 1

(MD010, no-hard-tabs)


78-78: Hard tabs
Column: 1

(MD010, no-hard-tabs)


79-79: Hard tabs
Column: 1

(MD010, no-hard-tabs)


80-80: Hard tabs
Column: 1

(MD010, no-hard-tabs)


81-81: Hard tabs
Column: 1

(MD010, no-hard-tabs)


82-82: Hard tabs
Column: 1

(MD010, no-hard-tabs)


83-83: Hard tabs
Column: 1

(MD010, no-hard-tabs)


84-84: Hard tabs
Column: 1

(MD010, no-hard-tabs)


85-85: Hard tabs
Column: 1

(MD010, no-hard-tabs)


86-86: Hard tabs
Column: 1

(MD010, no-hard-tabs)


87-87: Hard tabs
Column: 1

(MD010, no-hard-tabs)


88-88: Hard tabs
Column: 1

(MD010, no-hard-tabs)


89-89: Hard tabs
Column: 1

(MD010, no-hard-tabs)


90-90: Hard tabs
Column: 1

(MD010, no-hard-tabs)


91-91: Hard tabs
Column: 1

(MD010, no-hard-tabs)


92-92: Hard tabs
Column: 1

(MD010, no-hard-tabs)


93-93: Hard tabs
Column: 1

(MD010, no-hard-tabs)


94-94: Hard tabs
Column: 1

(MD010, no-hard-tabs)

🤖 Prompt for AI Agents
In `@OMNI_PLAN.md` around lines 73 - 95, The JSON code blocks containing the
"exports" and "publishConfig" objects contain hard tab characters that trigger
MD010; replace all hard tabs with spaces in those fenced JSON snippets (the
blocks showing "exports": { ... } and "publishConfig": { ... }) so the markdown
linter no longer flags them, ensuring indentation uses spaces consistently
throughout the OMNI_PLAN.md examples.

OMNI_PLAN.md Outdated
Comment on lines 230 to 232
```
apps/omni/
├── src/
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language identifier to fenced blocks (MD040).

Use a language like text for the directory tree block to satisfy markdownlint.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

230-230: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@OMNI_PLAN.md` around lines 230 - 232, The fenced code block containing the
directory tree snippet starting with "apps/omni/" should include a language
identifier (e.g., text) to satisfy markdownlint rule MD040; update the fenced
block in OMNI_PLAN.md from ``` to ```text so the directory tree (apps/omni/ ├──
src/) is marked as plain text.

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.

1 participant