-
-
Notifications
You must be signed in to change notification settings - Fork 323
feat(omni): add unified usage aggregation across AI coding assistants #813
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughIntroduces 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this 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 forformatDate: false.The existing test validates the default formatting behavior, but adding a test case with
formatDate: falsewould 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 inpackage.jsonis@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: PreferResult.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 whenclearCache()is used.With
sharedOnlinePricing, per-instanceclearCache()no longer forces a refresh. If refresh behavior is expected, consider evicting shared entries forthis.urlas well.apps/ccusage/src/data-loader.ts (1)
719-737: Make date-key formatting explicit infilterFilesBySince.Passing
DEFAULT_LOCALEhere 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
normalizeDateInputfunction duplicates logic fromapps/omni/src/data-aggregator.ts(lines 560-571), though with different output formats (compactYYYYMMDDhere vsYYYY-MM-DDthere). 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 checkssince, notuntil.The file-level prefiltering skips files with
mtime < sincebut doesn't checkuntil. This is acceptable as a performance optimization—files modified after theuntildate 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 bothdaysandsince/untilare provided.When
daysis specified, it silently overrides anysince/untilvalues. 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: Sequentialawaitin loop may impact performance for large datasets.Each
calculateCostForEntrycall is awaited sequentially. If the pricing fetcher involves network calls without caching, this could be slow. Consider batching cost calculations or usingPromise.allif entries can be processed in parallel.However, if
pricingFetchercaches 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 withdaily-report.ts.The
buildMonthlyReportfunction is nearly identical tobuildDailyReport, differing only in the grouping key (slice(0, 7)vssplit('T')[0]). Consider extracting a sharedbuildReporthelper 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 emptybySourcearray.If
totals.bySourceis 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 exportedbuildSessionReport.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.
| 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), | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -20Repository: 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 -20Repository: 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 -20Repository: 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 -20Repository: 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
- 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(notdata.cachedInputTokens)models: data.models(notObject.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
| **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` | | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
| ```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" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
| ``` | ||
| apps/omni/ | ||
| ├── src/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.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/omnisrc/_normalizers/- Per-source data normalizers (claude, codex, opencode, pi)src/data-aggregator.ts- Main aggregation logic with parallel loadingsrc/commands/- CLI commands (daily, monthly, session)Required Changes to Existing Packages
daily-report,monthly-report,session-report,data-loaderdata-loaderPerformance Optimizations
DateTimeFormatinstances in Codex for faster date/month key generationLiteLLMPricingFetcherso parallel source loaders share a single network requestUsage
Notes
Summary by CodeRabbit
New Features
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.