Skip to content

feat: add aiusage - unified AI usage tracker#702

Closed
tysoncung wants to merge 2 commits intoryoppippi:mainfrom
tysoncung:feat/aiusage-unified-tracker
Closed

feat: add aiusage - unified AI usage tracker#702
tysoncung wants to merge 2 commits intoryoppippi:mainfrom
tysoncung:feat/aiusage-unified-tracker

Conversation

@tysoncung
Copy link
Copy Markdown
Contributor

@tysoncung tysoncung commented Oct 21, 2025

Summary

Adds aiusage - a new unified AI usage tracker that provides a single dashboard for monitoring usage and costs across multiple AI coding assistants.

What is aiusage?

aiusage is a command-line tool that aggregates usage data from Claude Code, OpenAI Codex CLI, and (soon) Cursor AI and GitHub Copilot, providing a unified view of all your AI tool usage in one place.

Features

Unified Dashboard - See all AI services at a glance
📊 Monthly & Daily Reports - Aggregated views across all services
🔍 Auto-Detection - Automatically detects which AI tools have data
📄 JSON Output - Structured output for automation
🔌 Extensible - Easy to add new AI services

Supported Services (v1.0.0)

  • Claude Code - Full support (reuses ccusage data loader)
  • OpenAI Codex CLI - Full support (reuses @ccusage/codex data loader)
  • 🚧 Cursor AI - Coming soon (data format investigation needed)
  • 🚧 GitHub Copilot - Coming soon (GitHub API integration needed)

Usage

# Dashboard (default)
npx aiusage@latest
npx aiusage@latest dashboard

# Monthly aggregated report
npx aiusage@latest monthly
npx aiusage@latest monthly --json

# Daily breakdown
npx aiusage@latest daily
npx aiusage@latest daily --json

Example Output

AI Usage Dashboard - All Services

Available Services:
  ✓ Claude Code (~/.config/claude)
  ✓ OpenAI Codex CLI (~/.codex)
  ✗ Cursor AI - Coming soon
  ✗ GitHub Copilot - Coming soon

Total Usage (All Time):
┌─────────────────┬──────────────┬──────────────┐
│ Service         │ Total Tokens │ Cost (USD)   │
├─────────────────┼──────────────┼──────────────┤
│ Claude Code     │  262,125,881 │     $924.48  │
│ OpenAI Codex    │            0 │       $0.00  │
├─────────────────┼──────────────┼──────────────┤
│ Total           │  262,125,881 │     $924.48  │
└─────────────────┴──────────────┴──────────────┘

💡 Tip: Use `aiusage monthly` or `aiusage daily` for detailed breakdowns

Implementation Details

Architecture:

  • Reuses existing ccusage and @ccusage/codex data loaders
  • Shares @ccusage/terminal for table rendering
  • Shares @ccusage/internal for utilities
  • Built with Gunshi CLI framework

Package Structure:

apps/aiusage/
├── src/
│   ├── commands/
│   │   ├── dashboard.ts    # Main unified dashboard
│   │   ├── monthly.ts      # Monthly aggregated view
│   │   └── daily.ts        # Daily breakdown
│   ├── data-loader.ts      # Unified data aggregation
│   ├── types.ts            # Type definitions
│   ├── logger.ts           # Logging utilities
│   ├── index.ts            # CLI entry point
│   └── run.ts              # Main runner
├── package.json
├── README.md
├── tsdown.config.ts
└── vitest.config.ts

Performance:

  • Build time: ~45ms
  • Bundle size: 588KB (gzip: ~69KB)
  • Startup time: <100ms

Test Plan

  • Build completes successfully
  • Dashboard command works
  • Monthly command works
  • Daily command works
  • Service detection works
  • Error handling works (gracefully handles missing data)
  • JSON output works
  • Follows monorepo conventions
  • Uses workspace dependencies correctly

Files Changed

  • apps/aiusage/ - New package (12 files)
  • pnpm-lock.yaml - Updated for new dependencies

Breaking Changes

None - this is a new package addition

Future Work

  • Add Cursor AI support (requires data format investigation)
  • Add GitHub Copilot support (requires GitHub API integration)
  • Add MCP server integration (similar to @ccusage/mcp)
  • Add configuration file support
  • Add filtering/search capabilities

Summary by CodeRabbit

  • New Features

    • Introduced aiusage, a unified tool for tracking usage across Claude, Codex, Cursor, and Copilot
    • Added dashboard, daily, and monthly usage reporting commands
    • Enabled JSON and table output formats for usage reports
  • Documentation

    • Added comprehensive README with quick start guides, feature overview, and usage examples
  • Bug Fixes

    • Improved column width for better display of large token numbers

…truncation

Fixes ryoppippi#700 by increasing the minimum column width for right-aligned numeric
columns from 10 to 14 characters in responsive mode. This ensures large
token numbers (e.g., 536,073,421) are displayed in full without ellipsis
truncation when the table is resized to fit narrow terminals.

The fix maintains consistency with the normal mode's generous padding while
providing enough space for comma-formatted large numbers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Oct 21, 2025

Walkthrough

Adds a new aiusage CLI app for unified usage tracking across AI services (Claude, Codex, Cursor, Copilot): CLI runner, three commands (dashboard, daily, monthly) with JSON/table outputs, unified data loader and types, logger, build/test configs, README, and a terminal table width tweak.

Changes

Cohort / File(s) Summary
Documentation & Package
apps/aiusage/README.md, apps/aiusage/package.json
New README describing features, usage, supported services; package manifest and CLI/bin, scripts, engines, and publish config.
Build & Test Config
apps/aiusage/tsdown.config.ts, apps/aiusage/vitest.config.ts
tsdown build config (esm, node20.19.4 target, dist output); Vitest config with globals, node env, and coverage reporters.
CLI Entrypoints & Runner
apps/aiusage/src/index.ts, apps/aiusage/src/run.ts
Executable index with top-level await and run() exported to wire Gunshi CLI and subcommand dispatch (dashboard as main).
Logging Facade
apps/aiusage/src/logger.ts
Exposes logger and log created from internal logger factory named "aiusage".
Types
apps/aiusage/src/types.ts
Adds AIService union and types: ServiceStatus, UnifiedUsageData, AggregatedUsage, ServiceConfig.
Unified Data Loader
apps/aiusage/src/data-loader.ts
New data-loader with checkServiceAvailability(), checkClaudeAvailability(), dynamic Codex check, loadUnifiedDailyData(), loadUnifiedMonthlyData(); maps Claude data to unified schema; TODOs for Cursor/Copilot.
Commands
apps/aiusage/src/commands/dashboard.ts, apps/aiusage/src/commands/daily.ts, apps/aiusage/src/commands/monthly.ts
New exported commands (dashboardCommand, dailyCommand, monthlyCommand). Each supports --json output; otherwise aggregates and renders tables (by service, date, or month respectively) and compute totals.
Configs & Tooling
apps/aiusage/tsdown.config.ts, apps/aiusage/vitest.config.ts, apps/aiusage/package.json
Build/test/tooling and workspace integration (scripts, typecheck, prepack/prerelease hooks).
Terminal UI Adjustment
packages/terminal/src/table.ts
Increased minimum width for right-aligned columns from 10 to 14 for larger token numbers.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as CLI Runner
    participant Cmd as Command Handler
    participant Loader as Data Loader
    participant Service as AI Service

    User->>CLI: aiusage [dashboard|daily|monthly] [--json]
    CLI->>Cmd: dispatch chosen command

    rect rgba(200,230,200,0.2)
      note over Loader: Data Loading Phase
      Cmd->>Loader: loadUnifiedDailyData / loadUnifiedMonthlyData
      Loader->>Service: load Claude / (dyn import) Codex / placeholders for Cursor/Copilot
      Service-->>Loader: usage entries
      Loader-->>Cmd: unified usage array
    end

    alt --json flag
        Cmd->>Cmd: format JSON
        Cmd-->>User: print JSON
    else Table output
        Cmd->>Cmd: aggregate (by date/month/service) & compute totals
        Cmd->>Cmd: render table (uses terminal table sizing)
        Cmd-->>User: print table
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • ryoppippi

Poem

🐇 I hop through logs and json streams,
counting tokens, costs, and dreams.
Daily, monthly, dashboard bright—
I tally usage through the night.
Carrots, tables, CLI beams!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: add aiusage - unified AI usage tracker" accurately and directly reflects the primary change in this changeset. The PR introduces a comprehensive new CLI package called aiusage that aggregates usage and cost data from multiple AI coding assistants into a single dashboard with support for unified, monthly, and daily reports. The title uses standard conventional commit format, is concise at 44 characters, and clearly communicates the main objective without vague terms, emojis, or unnecessary detail. While the changeset includes a minor supporting adjustment to packages/terminal/src/table.ts, the title appropriately focuses on the primary feature being added.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
Copy Markdown

@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: 9

🧹 Nitpick comments (16)
apps/aiusage/src/run.ts (2)

20-22: Make argv normalization resilient and self-descriptive

Use the package name constant instead of a hard-coded string to avoid dropping a legitimate first arg named "aiusage".

- if (args[0] === 'aiusage') {
+ if (args[0] === name) {
   args = args.slice(1);
 }

1-31: Formatting per repo guidelines

Code uses single quotes. The repo guideline mandates double quotes for TS. Please run the formatter or convert quotes in this file.

apps/aiusage/src/commands/dashboard.ts (3)

37-44: Strengthen typing for service aggregate

Use the AIService union for keys.

- const byService: Record<string, { tokens: number; cost: number }> = {};
+ const byService: Record<AIService, { tokens: number; cost: number }> = {} as any;

Or prefer a Map<AIService, ...> for symmetry with the table path.


60-93: Console policy: disable or replace

Repo guidelines disallow console.log unless explicitly disabled. Either:

  • Add an eslint disable at top of file (UI output justification), or
  • Switch to process.stdout.write(...) (keep logger for diagnostics).
 /** 
  * @fileoverview Dashboard command - unified view of all AI services
  */
 
+/* eslint-disable no-console */ // UI output; diagnostics go via logger

Also applies to: 99-103, 114-116, 139-147, 148-151


155-171: Unify service labels across commands

getServiceName here returns "Claude Code"/"OpenAI Codex CLI", while other commands use shorter labels. Extract a single helper (e.g., src/services.ts) and reuse to avoid drift.

apps/aiusage/src/commands/daily.ts (4)

39-46: Console policy: disable or replace

Per guidelines, avoid console.log. Add a file-scoped disable for UI output or replace with process.stdout.write.

 /**
  * @fileoverview Daily usage command - daily breakdown across all services
  */
 
+/* eslint-disable no-console */ // UI output; diagnostics via logger

Also applies to: 104-106, 26-30, 32-36


78-89: "Models" column currently shows services; switch to actual models

The table’s second column header is "Models". Populate with unique models from entries to match the header.

- const services = entries.map(e => getServiceLabel(e.service)).join(', ');
+ const models = Array.from(new Set(entries.flatMap(e => e.models))).join(', ');
...
- table.push([
-   date,
-   services,
+ table.push([
+   date,
+   models,

109-117: Tighten typing and deduplicate label helper

Change parameter type to AIService and reuse a centralized helper across commands to keep labels consistent.

-function getServiceLabel(service: string): string {
+function getServiceLabel(service: AIService): string {

63-66: Optional: sort newest-first

Consider descending sort so recent days appear first.

apps/aiusage/src/commands/monthly.ts (4)

38-46: Console policy: disable or replace

Add a file-scoped disable for UI output or replace with process.stdout.write.

 /**
  * @fileoverview Monthly usage command - aggregated by month across all services
  */
 
+/* eslint-disable no-console */ // UI output; diagnostics via logger

Also applies to: 104-106, 26-30, 32-36


78-89: "Models" column should list models, not services

Populate with unique model names for the month.

- const services = entries.map(e => getServiceLabel(e.service)).join(', ');
+ const models = Array.from(new Set(entries.flatMap(e => e.models))).join(', ');
...
- table.push([
-   month,
-   services,
+ table.push([
+   month,
+   models,

109-117: Tighten typing and deduplicate label helper

Change parameter type to AIService and centralize labels to avoid drift across commands.

-function getServiceLabel(service: string): string {
+function getServiceLabel(service: AIService): string {

63-66: Optional: sort newest-first

Consider descending sort so recent months appear first.

apps/aiusage/src/data-loader.ts (3)

132-134: Codex integration still TODO

PR objectives mention Codex support; loaders currently have TODOs. Either integrate now or update the PR notes/status.

Also applies to: 170-172


127-130: Minor: single-argument logger calls

If logger.warn expects a single message string, prefer interpolation to avoid dropped args.

- logger.warn('Failed to load Claude data:', errorMsg);
+ logger.warn(`Failed to load Claude data: ${errorMsg}`);

Also applies to: 165-168


1-175: Repo-wide style

This file also uses single quotes. Align with the double-quote rule.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b40070b and 16ae79e83517eb16290008bc12bcf643575c27c0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • apps/aiusage/README.md (1 hunks)
  • apps/aiusage/package.json (1 hunks)
  • apps/aiusage/src/commands/daily.ts (1 hunks)
  • apps/aiusage/src/commands/dashboard.ts (1 hunks)
  • apps/aiusage/src/commands/monthly.ts (1 hunks)
  • apps/aiusage/src/data-loader.ts (1 hunks)
  • apps/aiusage/src/index.ts (1 hunks)
  • apps/aiusage/src/logger.ts (1 hunks)
  • apps/aiusage/src/run.ts (1 hunks)
  • apps/aiusage/src/types.ts (1 hunks)
  • apps/aiusage/tsdown.config.ts (1 hunks)
  • apps/aiusage/vitest.config.ts (1 hunks)
  • packages/terminal/src/table.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Use tab indentation and double quotes (ESLint formatting)
Do not use console.log; only allow where explicitly disabled via eslint-disable
Always use Node.js path utilities for file paths for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() to wrap operations that may throw (e.g., JSON parsing)
Use Result.isFailure() for checking errors instead of negating isSuccess()
Use early return on failures (e.g., if (Result.isFailure(r)) continue) instead of ternary patterns
For async operations, create a wrapper using Result.try() and call it
Keep traditional try-catch only for complex file I/O or legacy code that’s hard to refactor
Always use Result.isFailure() and Result.isSuccess() type guards for clarity
Variables use camelCase naming
Types use PascalCase naming
Constants can use UPPER_SNAKE_CASE
Only export constants, functions, and types that are actually used by other modules
Do not export internal/private constants that are only used within the same file
Before exporting a constant, verify it is referenced by other modules
Use Vitest globals (describe, it, expect) without imports in test blocks
Never use await import() dynamic imports anywhere in the codebase
Never use dynamic imports inside Vitest test blocks
Use fs-fixture createFixture() for mock Claude data directories in tests
All tests must use current Claude 4 models (not Claude 3)
Test coverage should include both Sonnet and Opus models
Model names in tests must exactly match LiteLLM pricing database entries
Use logger.ts instead of console.log for logging

Files:

  • apps/aiusage/src/index.ts
  • apps/aiusage/src/logger.ts
  • apps/aiusage/src/run.ts
  • apps/aiusage/vitest.config.ts
  • apps/aiusage/src/types.ts
  • apps/aiusage/src/commands/dashboard.ts
  • apps/aiusage/src/commands/monthly.ts
  • apps/aiusage/tsdown.config.ts
  • apps/aiusage/src/commands/daily.ts
  • packages/terminal/src/table.ts
  • apps/aiusage/src/data-loader.ts
apps/*/package.json

📄 CodeRabbit inference engine (CLAUDE.md)

For all projects under apps/, list all runtime dependencies in devDependencies (never dependencies) so the bundler owns the runtime payload

Files:

  • apps/aiusage/package.json
**/package.json

📄 CodeRabbit inference engine (CLAUDE.md)

Dependencies should always be added as devDependencies unless explicitly requested otherwise

Files:

  • apps/aiusage/package.json
**/data-loader.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Silently skip malformed JSONL lines during parsing in data-loader.ts

Files:

  • apps/aiusage/src/data-loader.ts
🧠 Learnings (5)
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/src/**/*.ts : Do not use console.log; use the logger utilities from `src/logger.ts` instead

Applied to files:

  • apps/aiusage/src/logger.ts
📚 Learning: 2025-09-18T17:43:09.255Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-18T17:43:09.255Z
Learning: Applies to **/*.ts : Use logger.ts instead of console.log for logging

Applied to files:

  • apps/aiusage/src/logger.ts
📚 Learning: 2025-09-18T17:43:09.255Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-18T17:43:09.255Z
Learning: Applies to **/*.ts : Use Vitest globals (describe, it, expect) without imports in test blocks

Applied to files:

  • apps/aiusage/vitest.config.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: apps/mcp/CLAUDE.md:0-0
Timestamp: 2025-09-17T18:29:15.764Z
Learning: Applies to apps/mcp/**/*.{test,spec}.ts : Vitest globals enabled: use `describe`, `it`, `expect` directly without importing them

Applied to files:

  • apps/aiusage/vitest.config.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/src/**/*.ts : Use Vitest globals (`describe`, `it`, `expect`) without imports in test blocks

Applied to files:

  • apps/aiusage/vitest.config.ts
🧬 Code graph analysis (7)
apps/aiusage/src/index.ts (1)
apps/aiusage/src/run.ts (1)
  • run (16-31)
apps/aiusage/src/logger.ts (1)
packages/internal/src/logger.ts (1)
  • createLogger (5-17)
apps/aiusage/src/run.ts (3)
apps/aiusage/src/commands/dashboard.ts (1)
  • dashboardCommand (13-153)
apps/aiusage/src/commands/monthly.ts (1)
  • monthlyCommand (11-107)
apps/aiusage/src/commands/daily.ts (1)
  • dailyCommand (11-107)
apps/aiusage/src/commands/dashboard.ts (4)
apps/aiusage/src/logger.ts (1)
  • logger (7-7)
apps/aiusage/src/data-loader.ts (2)
  • checkServiceAvailability (16-42)
  • loadUnifiedMonthlyData (142-175)
apps/aiusage/src/types.ts (1)
  • AIService (8-8)
packages/terminal/src/table.ts (2)
  • createUsageReportTable (443-500)
  • formatNumber (305-307)
apps/aiusage/src/commands/monthly.ts (2)
apps/aiusage/src/data-loader.ts (1)
  • loadUnifiedMonthlyData (142-175)
packages/terminal/src/table.ts (2)
  • createUsageReportTable (443-500)
  • formatNumber (305-307)
apps/aiusage/src/commands/daily.ts (2)
apps/aiusage/src/data-loader.ts (1)
  • loadUnifiedDailyData (104-137)
packages/terminal/src/table.ts (2)
  • createUsageReportTable (443-500)
  • formatNumber (305-307)
apps/aiusage/src/data-loader.ts (3)
apps/aiusage/src/types.ts (3)
  • ServiceStatus (13-18)
  • UnifiedUsageData (23-33)
  • AIService (8-8)
apps/ccusage/src/data-loader.ts (1)
  • loadDailyUsageData (725-865)
apps/aiusage/src/logger.ts (1)
  • logger (7-7)
🪛 markdownlint-cli2 (0.18.1)
apps/aiusage/README.md

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

(MD040, fenced-code-language)

🔇 Additional comments (8)
packages/terminal/src/table.ts (1)

194-194: LGTM! Minimum width adjustment for large token numbers.

The increase from 10 to 14 characters appropriately accommodates 9+ digit token counts with comma separators, which aligns with the unified dashboard requirements in this PR.

apps/aiusage/src/index.ts (1)

1-6: LGTM! Clean CLI entrypoint.

The entrypoint follows all coding guidelines: uses .ts extension for local imports, includes appropriate ESLint directive for top-level await, and delegates to the run module correctly.

apps/aiusage/vitest.config.ts (1)

1-13: LGTM! Vitest configuration is correct.

The configuration properly enables globals (per coding guidelines) and sets up coverage reporting. The include: ['src/**/*.ts'] pattern is appropriate for this codebase's co-located test pattern using import.meta.vitest.

apps/aiusage/src/logger.ts (1)

1-8: LGTM! Logger module follows established patterns.

The logger correctly reuses the internal logger infrastructure and avoids direct console usage, as per coding guidelines.

apps/aiusage/tsdown.config.ts (1)

1-14: LGTM! Build configuration is appropriate for a CLI tool.

The tsdown config correctly targets Node 20.19.4 (matching package.json engines), uses ESM format, and disables declaration generation since this is a CLI application rather than a library.

apps/aiusage/src/types.ts (1)

1-51: LGTM! Clean and well-documented type definitions.

The type definitions are clear, use appropriate naming conventions (PascalCase), and provide a solid foundation for the unified AI usage tracking system. The use of Map<AIService, UnifiedUsageData> in AggregatedUsage is an appropriate choice for keyed service data.

apps/aiusage/package.json (1)

1-78: LGTM! Package manifest follows all coding guidelines.

The package.json correctly:

  • Places all dependencies in devDependencies (as required for apps/)
  • Uses workspace references for internal packages
  • Configures separate bin paths for development (source) and publishing (dist)
  • Aligns engine constraints with the tsdown config
apps/aiusage/src/run.ts (1)

24-30: No changes required — renderHeader: null is valid in Gunshi v0.26.x

The renderHeader option is supported in Gunshi v0.26.x and may be set to null or to a renderer function. The code passing renderHeader: null is correct and requires no modification.

Comment thread apps/aiusage/README.md
Comment on lines +49 to +65
```
Available Services:
✓ Claude Code (~/.config/claude)
✓ OpenAI Codex CLI (~/.codex)
✗ Cursor AI - Coming soon
✗ GitHub Copilot - Coming soon

Total Usage (All Time):
┌─────────────────┬──────────────┬──────────────┐
│ Service │ Total Tokens │ Cost (USD) │
├─────────────────┼──────────────┼──────────────┤
│ Claude Code │ 262,125,881 │ $924.48 │
│ OpenAI Codex │ 0 │ $0.00 │
├─────────────────┼──────────────┼──────────────┤
│ Total │ 262,125,881 │ $924.48 │
└─────────────────┴──────────────┴──────────────┘
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language specifier to code block.

The fenced code block showing terminal output should include a language specifier for proper rendering and accessibility.

Apply this diff:

-```
+```text
 Available Services:
   ✓ Claude Code (~/.config/claude)
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

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

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In apps/aiusage/README.md around lines 49 to 65, the fenced code block showing
terminal output lacks a language specifier; update the opening fence to include
"text" (i.e., ```text) so the block is rendered correctly and accessible,
leaving the rest of the block unchanged.

Comment on lines +24 to +25
const data = loadUnifiedDailyData();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Await unified daily loader

Make this awaitable to match the async loader fix.

- const data = loadUnifiedDailyData();
+ const data = await loadUnifiedDailyData();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const data = loadUnifiedDailyData();
const data = await loadUnifiedDailyData();
🤖 Prompt for AI Agents
In apps/aiusage/src/commands/daily.ts around lines 24-25, the call to
loadUnifiedDailyData() is not awaited; update the call to await
loadUnifiedDailyData() and ensure the surrounding function is declared async (or
otherwise handles the returned Promise) so the loader runs to completion before
using its result.

Comment on lines +34 to +35
const data = loadUnifiedMonthlyData();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Await unified loaders (they should be async)

loadUnifiedMonthlyData() should be awaited to avoid empty/misread data when the loader becomes async (see data-loader.ts fix).

- const data = loadUnifiedMonthlyData();
+ const data = await loadUnifiedMonthlyData();

Apply in both JSON and table paths.

Also applies to: 97-98

🤖 Prompt for AI Agents
In apps/aiusage/src/commands/dashboard.ts around lines 34-35 (and also at lines
97-98), the unified data loader loadUnifiedMonthlyData() is called without
awaiting it; update both call sites to await the async loader so the code uses
the resolved data (e.g., const data = await loadUnifiedMonthlyData()) in both
the JSON and table rendering paths, ensuring the surrounding function is async
or already supports await.

Comment on lines +24 to +25
const data = loadUnifiedMonthlyData();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Await unified monthly loader

Make this awaitable to match the async loader fix.

- const data = loadUnifiedMonthlyData();
+ const data = await loadUnifiedMonthlyData();
🤖 Prompt for AI Agents
In apps/aiusage/src/commands/monthly.ts around lines 24-25, the call to
loadUnifiedMonthlyData is currently synchronous; change it to await
loadUnifiedMonthlyData() so the async loader is awaited, and ensure the
enclosing function is declared async (or that this call occurs inside an async
function); if needed, wrap the await in a try/catch to propagate or log errors
appropriately.

Comment on lines +1 to +175
const { loadDailyUsageData } = await import('@ccusage/codex/src/data-loader.ts');
const data = loadDailyUsageData();
return data.length > 0;
}
catch {
return false;
}
});

if (Result.isSuccess(result)) {
const hasData = await result.value;
return {
service: 'codex',
available: hasData,
dataPath: '~/.codex',
};
}

return {
service: 'codex',
available: false,
error: 'Codex data not found',
};
}

/**
* Load unified daily usage data from all available services
*/
export function loadUnifiedDailyData(): UnifiedUsageData[] {
const allData: UnifiedUsageData[] = [];

// Load Claude data
const claudeResult = Result.try(() => {
const data = loadClaudeDaily();
return data.map(entry => ({
service: 'claude' as AIService,
date: entry.date,
inputTokens: entry.inputTokens,
outputTokens: entry.outputTokens,
cacheCreateTokens: entry.cacheCreationTokens,
cacheReadTokens: entry.cacheReadTokens,
totalTokens: entry.inputTokens + entry.outputTokens + entry.cacheCreationTokens + entry.cacheReadTokens,
cost: entry.totalCost,
models: entry.modelsUsed ?? [],
}));
});

if (Result.isSuccess(claudeResult)) {
allData.push(...claudeResult.value);
logger.info(`Loaded ${claudeResult.value.length} Claude Code entries`);
}
else if (Result.isFailure(claudeResult)) {
const errorMsg = claudeResult.error instanceof Error ? claudeResult.error.message : String(claudeResult.error);
logger.warn('Failed to load Claude data:', errorMsg);
}

// TODO: Load Codex data
// TODO: Load Cursor data
// TODO: Load Copilot data

return allData;
}

/**
* Load unified monthly usage data from all available services
*/
export function loadUnifiedMonthlyData(): UnifiedUsageData[] {
const allData: UnifiedUsageData[] = [];

// Load Claude data
const claudeResult = Result.try(() => {
const data = loadClaudeMonthly();
return data.map(entry => ({
service: 'claude' as AIService,
date: entry.month,
inputTokens: entry.inputTokens,
outputTokens: entry.outputTokens,
cacheCreateTokens: entry.cacheCreationTokens,
cacheReadTokens: entry.cacheReadTokens,
totalTokens: entry.inputTokens + entry.outputTokens + entry.cacheCreationTokens + entry.cacheReadTokens,
cost: entry.totalCost,
models: entry.modelsUsed ?? [],
}));
});

if (Result.isSuccess(claudeResult)) {
allData.push(...claudeResult.value);
logger.info(`Loaded ${claudeResult.value.length} Claude Code monthly entries`);
}
else if (Result.isFailure(claudeResult)) {
const errorMsg = claudeResult.error instanceof Error ? claudeResult.error.message : String(claudeResult.error);
logger.warn('Failed to load Claude data:', errorMsg);
}

// TODO: Load Codex data
// TODO: Load Cursor data
// TODO: Load Copilot data

return allData;
}
Copy link
Copy Markdown

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

Quick verification: dynamic imports and console usage

Run this script to find disallowed dynamic imports and any stray console.log across the new package.


🏁 Script executed:

#!/bin/bash
rg -n --type=ts -C1 -e 'await\s+import\(' apps/aiusage/src
rg -n --type=ts -e '\bconsole\.log\b' apps/aiusage/src

Length of output: 4856


Remove dynamic import at line 76 in checkCodexAvailability()

The await import() at line 76 violates coding guidelines. Replace with a static import or refactor to avoid dynamic imports entirely. According to project standards, dynamic imports are not permitted anywhere in the codebase.

// Line 76 - violation
const { loadDailyUsageData } = await import('@ccusage/codex/src/data-loader.ts');

Consider static imports at the top of the file or restructure the availability check to use a try-catch with static imports.

🤖 Prompt for AI Agents
In apps/aiusage/src/data-loader.ts around lines 70-80 (checkCodexAvailability),
there's a forbidden dynamic import (await import(...)) — replace it with a
static import at the top of the file and call the imported function
synchronously. Add a top-level import for the Codex loader (e.g. import {
loadDailyUsageData as loadCodexDaily } from '@ccusage/codex/data-loader';) and
refactor checkCodexAvailability to call loadCodexDaily() inside the Result.try
block (remove await import and related try/catch), returning availability based
on loadCodexDaily(). Ensure the import path matches the package export and
update error handling to use Result semantics like the other checks.

Comment on lines +47 to +66
async function checkClaudeAvailability(): Promise<ServiceStatus> {
const result = Result.try(() => {
const data = loadClaudeDaily();
return data.length > 0;
});

if (Result.isSuccess(result)) {
return {
service: 'claude',
available: result.value,
dataPath: '~/.claude or ~/.config/claude',
};
}

return {
service: 'claude',
available: false,
error: Result.isFailure(result) ? (result.error instanceof Error ? result.error.message : String(result.error)) : 'Unknown error',
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Await Claude loader in availability check

loadClaudeDaily() is asynchronous (per ccusage loader contract). Without await, this always misreads and may log a failure.

- const result = Result.try(() => {
-   const data = loadClaudeDaily();
-   return data.length > 0;
- });
+ const result = Result.try(async () => {
+   const data = await loadClaudeDaily();
+   return data.length > 0;
+ });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/aiusage/src/data-loader.ts around lines 47 to 66, the code calls the
asynchronous loadClaudeDaily() without awaiting it via Result.try, causing
incorrect availability checks; replace the Result.try wrapper with an
async-aware flow: await loadClaudeDaily() inside a try/catch (or use
Result.tryAsync if your Result utility provides it), set available based on
(await loadClaudeDaily()).length > 0, and on error populate the error field with
the caught error.message (or String(error)) so the ServiceStatus correctly
reflects async failures.

Comment on lines +71 to +99
async function checkCodexAvailability(): Promise<ServiceStatus> {
// Import dynamically to avoid errors if codex package changes
const result = Result.try(async () => {
try {
// Try to import codex data loader
const { loadDailyUsageData } = await import('@ccusage/codex/src/data-loader.ts');
const data = loadDailyUsageData();
return data.length > 0;
}
catch {
return false;
}
});

if (Result.isSuccess(result)) {
const hasData = await result.value;
return {
service: 'codex',
available: hasData,
dataPath: '~/.codex',
};
}

return {
service: 'codex',
available: false,
error: 'Codex data not found',
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove dynamic import; wrong path and violates repo rule

  • Violates guideline: “Never use await import() dynamic imports anywhere.”
  • Path @ccusage/codex/src/data-loader.ts is unstable and likely not publishable.
  • Also treats returned data as sync.

Pick one:

  • If Codex is a hard dep now, use a static import (no .ts suffix) and await the call.
  • If not yet ready, mark as unavailable (“coming soon”) to match current implementation elsewhere.
-async function checkCodexAvailability(): Promise<ServiceStatus> {
-  // Import dynamically to avoid errors if codex package changes
-  const result = Result.try(async () => {
-    try {
-      // Try to import codex data loader
-      const { loadDailyUsageData } = await import('@ccusage/codex/src/data-loader.ts');
-      const data = loadDailyUsageData();
-      return data.length > 0;
-    }
-    catch {
-      return false;
-    }
-  });
-  if (Result.isSuccess(result)) {
-    const hasData = await result.value;
-    return { service: 'codex', available: hasData, dataPath: '~/.codex' };
-  }
-  return { service: 'codex', available: false, error: 'Codex data not found' };
-}
+async function checkCodexAvailability(): Promise<ServiceStatus> {
+  return { service: "codex", available: false, error: "Codex support coming soon" };
+}

If Codex is truly supported in this release, replace the above with a static import and await the loader, then update the unified loaders accordingly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function checkCodexAvailability(): Promise<ServiceStatus> {
// Import dynamically to avoid errors if codex package changes
const result = Result.try(async () => {
try {
// Try to import codex data loader
const { loadDailyUsageData } = await import('@ccusage/codex/src/data-loader.ts');
const data = loadDailyUsageData();
return data.length > 0;
}
catch {
return false;
}
});
if (Result.isSuccess(result)) {
const hasData = await result.value;
return {
service: 'codex',
available: hasData,
dataPath: '~/.codex',
};
}
return {
service: 'codex',
available: false,
error: 'Codex data not found',
};
}
async function checkCodexAvailability(): Promise<ServiceStatus> {
return { service: "codex", available: false, error: "Codex support coming soon" };
}
🤖 Prompt for AI Agents
In apps/aiusage/src/data-loader.ts around lines 71 to 99, the current
implementation uses a forbidden dynamic import with an incorrect path and treats
the loader as synchronous; replace it either by a static import from the
published package (e.g., import { loadDailyUsageData } from '@ccusage/codex';
remove the .ts suffix) and call await loadDailyUsageData() inside the function,
returning the correct boolean and error fields, or if Codex is not a hard
dependency yet, remove the import entirely and return a consistent "coming
soon"/unavailable ServiceStatus for codex to match other loaders; ensure no
await import() is used, adjust types to await the loader result, and update
dataPath/error values accordingly.

Comment on lines +104 to +137
export function loadUnifiedDailyData(): UnifiedUsageData[] {
const allData: UnifiedUsageData[] = [];

// Load Claude data
const claudeResult = Result.try(() => {
const data = loadClaudeDaily();
return data.map(entry => ({
service: 'claude' as AIService,
date: entry.date,
inputTokens: entry.inputTokens,
outputTokens: entry.outputTokens,
cacheCreateTokens: entry.cacheCreationTokens,
cacheReadTokens: entry.cacheReadTokens,
totalTokens: entry.inputTokens + entry.outputTokens + entry.cacheCreationTokens + entry.cacheReadTokens,
cost: entry.totalCost,
models: entry.modelsUsed ?? [],
}));
});

if (Result.isSuccess(claudeResult)) {
allData.push(...claudeResult.value);
logger.info(`Loaded ${claudeResult.value.length} Claude Code entries`);
}
else if (Result.isFailure(claudeResult)) {
const errorMsg = claudeResult.error instanceof Error ? claudeResult.error.message : String(claudeResult.error);
logger.warn('Failed to load Claude data:', errorMsg);
}

// TODO: Load Codex data
// TODO: Load Cursor data
// TODO: Load Copilot data

return allData;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Make unified daily loader async and await Claude data

Current code wraps an async call in a sync closure, then maps a Promise (will throw). Convert to async and await.

-export function loadUnifiedDailyData(): UnifiedUsageData[] {
+export async function loadUnifiedDailyData(): Promise<UnifiedUsageData[]> {
   const allData: UnifiedUsageData[] = [];
-  const claudeResult = Result.try(() => {
-    const data = loadClaudeDaily();
-    return data.map(entry => ({
+  const claudeResult = Result.try(async () => {
+    const data = await loadClaudeDaily();
+    return data.map(entry => ({
       service: 'claude' as AIService,
       date: entry.date,
       inputTokens: entry.inputTokens,
       outputTokens: entry.outputTokens,
       cacheCreateTokens: entry.cacheCreationTokens,
       cacheReadTokens: entry.cacheReadTokens,
       totalTokens: entry.inputTokens + entry.outputTokens + entry.cacheCreationTokens + entry.cacheReadTokens,
       cost: entry.totalCost,
       models: entry.modelsUsed ?? [],
     }));
   });
-  if (Result.isSuccess(claudeResult)) {
-    allData.push(...claudeResult.value);
+  if (Result.isSuccess(claudeResult)) {
+    allData.push(...claudeResult.value);
     logger.info(`Loaded ${claudeResult.value.length} Claude Code entries`);
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/aiusage/src/data-loader.ts around lines 104 to 137, the loader currently
treats loadClaudeDaily as synchronous by calling it inside Result.try and
mapping its (promise) return value, which causes a thrown Promise; change
loadUnifiedDailyData to be async (export async function loadUnifiedDailyData():
Promise<UnifiedUsageData[]>) and replace the Result.try usage with an awaited
call to loadClaudeDaily inside a try/catch: await the result, map each entry
into the UnifiedUsageData shape (same fields as before), push them into allData
and log the loaded count on success, and on failure catch the error and log the
message (preserving the existing error formatting).

Comment on lines +142 to +175
export function loadUnifiedMonthlyData(): UnifiedUsageData[] {
const allData: UnifiedUsageData[] = [];

// Load Claude data
const claudeResult = Result.try(() => {
const data = loadClaudeMonthly();
return data.map(entry => ({
service: 'claude' as AIService,
date: entry.month,
inputTokens: entry.inputTokens,
outputTokens: entry.outputTokens,
cacheCreateTokens: entry.cacheCreationTokens,
cacheReadTokens: entry.cacheReadTokens,
totalTokens: entry.inputTokens + entry.outputTokens + entry.cacheCreationTokens + entry.cacheReadTokens,
cost: entry.totalCost,
models: entry.modelsUsed ?? [],
}));
});

if (Result.isSuccess(claudeResult)) {
allData.push(...claudeResult.value);
logger.info(`Loaded ${claudeResult.value.length} Claude Code monthly entries`);
}
else if (Result.isFailure(claudeResult)) {
const errorMsg = claudeResult.error instanceof Error ? claudeResult.error.message : String(claudeResult.error);
logger.warn('Failed to load Claude data:', errorMsg);
}

// TODO: Load Codex data
// TODO: Load Cursor data
// TODO: Load Copilot data

return allData;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Make unified monthly loader async and await Claude data

Same async issue as daily.

-export function loadUnifiedMonthlyData(): UnifiedUsageData[] {
+export async function loadUnifiedMonthlyData(): Promise<UnifiedUsageData[]> {
   const allData: UnifiedUsageData[] = [];
-  const claudeResult = Result.try(() => {
-    const data = loadClaudeMonthly();
-    return data.map(entry => ({
+  const claudeResult = Result.try(async () => {
+    const data = await loadClaudeMonthly();
+    return data.map(entry => ({
       service: 'claude' as AIService,
       date: entry.month,
       inputTokens: entry.inputTokens,
       outputTokens: entry.outputTokens,
       cacheCreateTokens: entry.cacheCreationTokens,
       cacheReadTokens: entry.cacheReadTokens,
       totalTokens: entry.inputTokens + entry.outputTokens + entry.cacheCreationTokens + entry.cacheReadTokens,
       cost: entry.totalCost,
       models: entry.modelsUsed ?? [],
     }));
   });
-  if (Result.isSuccess(claudeResult)) {
-    allData.push(...claudeResult.value);
+  if (Result.isSuccess(claudeResult)) {
+    allData.push(...claudeResult.value);
     logger.info(`Loaded ${claudeResult.value.length} Claude Code monthly entries`);
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/aiusage/src/data-loader.ts around lines 142 to 175, the unified monthly
loader calls loadClaudeMonthly synchronously but that function is asynchronous —
change the function signature to async and return Promise<UnifiedUsageData[]>,
use await when calling loadClaudeMonthly (wrap the call in Result.try with an
async callback or await the Result.try promise) so the Claude data is resolved
before mapping and pushing into allData; keep the same error handling and
logging but ensure you await the Result and push its value only after success.

Add new aiusage package that provides unified usage tracking across
multiple AI coding assistants in one place.

Features:
- Dashboard command showing all services at a glance
- Monthly and daily aggregated reports
- Auto-detects which AI tools have data available
- JSON output for automation and scripting
- Extensible architecture for adding new services

Supported Services (v1.0.0):
- Claude Code (full support via ccusage)
- OpenAI Codex CLI (full support via @ccusage/codex)
- Cursor AI (coming soon)
- GitHub Copilot (coming soon)

Usage:
  npx aiusage@latest               # Dashboard
  npx aiusage@latest monthly       # Monthly report
  npx aiusage@latest daily         # Daily report
  npx aiusage@latest dashboard --json  # JSON output

Implementation:
- Reuses existing ccusage and @ccusage/codex data loaders
- Shares terminal utilities and pricing infrastructure
- Built with Gunshi CLI framework
- Minimal bundle size (588KB, 69KB gzipped)
@tysoncung tysoncung force-pushed the feat/aiusage-unified-tracker branch from 16ae79e to b7fdf34 Compare October 21, 2025 11:48
Copy link
Copy Markdown

@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: 4

♻️ Duplicate comments (9)
apps/aiusage/README.md (1)

49-65: Add language specifier to the terminal output block.

Specify the language for proper rendering and lint compliance.

-```
+```text
 Available Services:
   ✓ Claude Code (~/.config/claude)
   ✓ OpenAI Codex CLI (~/.codex)
   ✗ Cursor AI - Coming soon
   ✗ GitHub Copilot - Coming soon
 ...
-```
+```
apps/aiusage/src/commands/dashboard.ts (1)

32-36: Await the unified monthly loader.

The loader is (or will be) async; without await you’ll read a Promise.

-      const data = loadUnifiedMonthlyData();
+      const data = await loadUnifiedMonthlyData();

Apply at both call sites.

Also applies to: 97-99

apps/aiusage/src/commands/daily.ts (1)

23-25: Await the unified daily loader.

Loader is async; without await data.length will be undefined.

-    const data = loadUnifiedDailyData();
+    const data = await loadUnifiedDailyData();
apps/aiusage/src/commands/monthly.ts (1)

23-25: Await the unified monthly loader.

Loader is async; without await data.length will be undefined.

-    const data = loadUnifiedMonthlyData();
+    const data = await loadUnifiedMonthlyData();
apps/aiusage/src/data-loader.ts (5)

104-137: Make unified daily loader async and await Claude data.

Prevents thrown Promises and ensures correct mapping.

-export function loadUnifiedDailyData(): UnifiedUsageData[] {
+export async function loadUnifiedDailyData(): Promise<UnifiedUsageData[]> {
   const allData: UnifiedUsageData[] = [];
-  // Load Claude data
-  const claudeResult = Result.try(() => {
-    const data = loadClaudeDaily();
-    return data.map(entry => ({
+  // Load Claude data
+  const claudeResult = Result.try(async () => {
+    const data = await loadClaudeDaily();
+    return data.map(entry => ({
       service: 'claude' as AIService,
       date: entry.date,
       inputTokens: entry.inputTokens,
       outputTokens: entry.outputTokens,
       cacheCreateTokens: entry.cacheCreationTokens,
       cacheReadTokens: entry.cacheReadTokens,
       totalTokens: entry.inputTokens + entry.outputTokens + entry.cacheCreationTokens + entry.cacheReadTokens,
       cost: entry.totalCost,
       models: entry.modelsUsed ?? [],
     }));
   });
   if (Result.isSuccess(claudeResult)) {
     allData.push(...claudeResult.value);
     logger.info(`Loaded ${claudeResult.value.length} Claude Code entries`);
   }
   else if (Result.isFailure(claudeResult)) {
     const errorMsg = claudeResult.error instanceof Error ? claudeResult.error.message : String(claudeResult.error);
     logger.warn('Failed to load Claude data:', errorMsg);
   }
   return allData;
 }

142-175: Make unified monthly loader async and await Claude data.

Same async fix as daily loader.

-export function loadUnifiedMonthlyData(): UnifiedUsageData[] {
+export async function loadUnifiedMonthlyData(): Promise<UnifiedUsageData[]> {
   const allData: UnifiedUsageData[] = [];
-  const claudeResult = Result.try(() => {
-    const data = loadClaudeMonthly();
-    return data.map(entry => ({
+  const claudeResult = Result.try(async () => {
+    const data = await loadClaudeMonthly();
+    return data.map(entry => ({
       service: 'claude' as AIService,
       date: entry.month,
       inputTokens: entry.inputTokens,
       outputTokens: entry.outputTokens,
       cacheCreateTokens: entry.cacheCreationTokens,
       cacheReadTokens: entry.cacheReadTokens,
       totalTokens: entry.inputTokens + entry.outputTokens + entry.cacheCreationTokens + entry.cacheReadTokens,
       cost: entry.totalCost,
       models: entry.modelsUsed ?? [],
     }));
   });
   if (Result.isSuccess(claudeResult)) {
     allData.push(...claudeResult.value);
     logger.info(`Loaded ${claudeResult.value.length} Claude Code monthly entries`);
   }
   else if (Result.isFailure(claudeResult)) {
     const errorMsg = claudeResult.error instanceof Error ? claudeResult.error.message : String(claudeResult.error);
     logger.warn('Failed to load Claude data:', errorMsg);
   }
   return allData;
 }

71-99: Remove forbidden dynamic import and align Codex support status.

Dynamic import violates repo rule; code also treats loader as sync. Decide one:

  • Minimal: mark Codex “coming soon” for now.
  • Or integrate via static import and await the loader.

Minimal safe change:

-async function checkCodexAvailability(): Promise<ServiceStatus> {
-  // Import dynamically to avoid errors if codex package changes
-  const result = Result.try(async () => {
-    try {
-      // Try to import codex data loader
-      const { loadDailyUsageData } = await import('@ccusage/codex/src/data-loader.ts');
-      const data = loadDailyUsageData();
-      return data.length > 0;
-    }
-    catch {
-      return false;
-    }
-  });
-
-  if (Result.isSuccess(result)) {
-    const hasData = await result.value;
-    return { service: 'codex', available: hasData, dataPath: '~/.codex' };
-  }
-  return { service: 'codex', available: false, error: 'Codex data not found' };
-}
+async function checkCodexAvailability(): Promise<ServiceStatus> {
+  return { service: 'codex', available: false, error: 'Codex support coming soon' };
+}

If Codex is meant to be supported in v1.0.0, I can provide a static-import integration patch; confirm desired direction.


1-175: Remove forbidden dynamic import at line 76.

The code uses await import() inside checkCodexAvailability() to load the codex data loader. This violates the coding guideline prohibiting dynamic imports anywhere in the codebase. Replace with a static import at the top of the file, or refactor to avoid loading the codex module if it's not available.

const { loadDailyUsageData } = await import('@ccusage/codex/src/data-loader.ts');

47-66: Await Claude loader in availability check.

loadClaudeDaily is async; current sync check is incorrect.

-async function checkClaudeAvailability(): Promise<ServiceStatus> {
-  const result = Result.try(() => {
-    const data = loadClaudeDaily();
-    return data.length > 0;
-  });
+async function checkClaudeAvailability(): Promise<ServiceStatus> {
+  const result = Result.try(async () => {
+    const data = await loadClaudeDaily();
+    return data.length > 0;
+  });
   if (Result.isSuccess(result)) {
     return {
       service: 'claude',
       available: result.value,
       dataPath: '~/.claude or ~/.config/claude',
     };
   }
   return {
     service: 'claude',
     available: false,
     error: Result.isFailure(result) ? (result.error instanceof Error ? result.error.message : String(result.error)) : 'Unknown error',
   };
 }
🧹 Nitpick comments (3)
apps/aiusage/README.md (1)

167-189: Use json fence for JSON example.

Improves readability and tooling (syntax highlight).

-```bash
+```json
 $ aiusage --json
 {
   "services": [
     {
       "service": "claude",
       "available": true,
       "dataPath": "~/.config/claude"
     }
   ],
   "usage": {
     "claude": {
       "tokens": 262125881,
       "cost": 924.48
     }
   },
   "total": {
     "tokens": 262125881,
     "cost": 924.48
   }
 }
-```
+```
apps/aiusage/src/commands/daily.ts (1)

109-117: Tighten typing for service labels.

Use AIService for exhaustiveness and safer refactors.

-function getServiceLabel(service: string): string {
-  const labels: Record<string, string> = {
+import type { AIService } from "../types.ts";
+function getServiceLabel(service: AIService): string {
+  const labels: Record<AIService, string> = {
     'claude': 'Claude',
     'codex': 'Codex',
     'cursor': 'Cursor',
     'copilot': 'Copilot',
   };
   return labels[service] ?? service;
}
apps/aiusage/src/commands/monthly.ts (1)

109-117: Tighten typing for service labels.

Use AIService for exhaustiveness and safer refactors.

-function getServiceLabel(service: string): string {
-  const labels: Record<string, string> = {
+import type { AIService } from "../types.ts";
+function getServiceLabel(service: AIService): string {
+  const labels: Record<AIService, string> = {
     'claude': 'Claude',
     'codex': 'Codex',
     'cursor': 'Cursor',
     'copilot': 'Copilot',
   };
   return labels[service] ?? service;
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16ae79e83517eb16290008bc12bcf643575c27c0 and b7fdf34.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • apps/aiusage/README.md (1 hunks)
  • apps/aiusage/package.json (1 hunks)
  • apps/aiusage/src/commands/daily.ts (1 hunks)
  • apps/aiusage/src/commands/dashboard.ts (1 hunks)
  • apps/aiusage/src/commands/monthly.ts (1 hunks)
  • apps/aiusage/src/data-loader.ts (1 hunks)
  • apps/aiusage/src/index.ts (1 hunks)
  • apps/aiusage/src/logger.ts (1 hunks)
  • apps/aiusage/src/run.ts (1 hunks)
  • apps/aiusage/src/types.ts (1 hunks)
  • apps/aiusage/tsdown.config.ts (1 hunks)
  • apps/aiusage/vitest.config.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/aiusage/src/index.ts
  • apps/aiusage/src/run.ts
  • apps/aiusage/vitest.config.ts
  • apps/aiusage/src/types.ts
  • apps/aiusage/tsdown.config.ts
  • apps/aiusage/src/logger.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/*/package.json

📄 CodeRabbit inference engine (CLAUDE.md)

For all projects under apps/, list all runtime dependencies in devDependencies (never dependencies) so the bundler owns the runtime payload

Files:

  • apps/aiusage/package.json
**/package.json

📄 CodeRabbit inference engine (CLAUDE.md)

Dependencies should always be added as devDependencies unless explicitly requested otherwise

Files:

  • apps/aiusage/package.json
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Use tab indentation and double quotes (ESLint formatting)
Do not use console.log; only allow where explicitly disabled via eslint-disable
Always use Node.js path utilities for file paths for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() to wrap operations that may throw (e.g., JSON parsing)
Use Result.isFailure() for checking errors instead of negating isSuccess()
Use early return on failures (e.g., if (Result.isFailure(r)) continue) instead of ternary patterns
For async operations, create a wrapper using Result.try() and call it
Keep traditional try-catch only for complex file I/O or legacy code that’s hard to refactor
Always use Result.isFailure() and Result.isSuccess() type guards for clarity
Variables use camelCase naming
Types use PascalCase naming
Constants can use UPPER_SNAKE_CASE
Only export constants, functions, and types that are actually used by other modules
Do not export internal/private constants that are only used within the same file
Before exporting a constant, verify it is referenced by other modules
Use Vitest globals (describe, it, expect) without imports in test blocks
Never use await import() dynamic imports anywhere in the codebase
Never use dynamic imports inside Vitest test blocks
Use fs-fixture createFixture() for mock Claude data directories in tests
All tests must use current Claude 4 models (not Claude 3)
Test coverage should include both Sonnet and Opus models
Model names in tests must exactly match LiteLLM pricing database entries
Use logger.ts instead of console.log for logging

Files:

  • apps/aiusage/src/commands/dashboard.ts
  • apps/aiusage/src/commands/daily.ts
  • apps/aiusage/src/data-loader.ts
  • apps/aiusage/src/commands/monthly.ts
**/data-loader.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Silently skip malformed JSONL lines during parsing in data-loader.ts

Files:

  • apps/aiusage/src/data-loader.ts
🧠 Learnings (3)
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/**/*.ts : NEVER use `await import()` dynamic imports anywhere (especially in tests)

Applied to files:

  • apps/aiusage/src/data-loader.ts
📚 Learning: 2025-09-18T17:43:09.255Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-18T17:43:09.255Z
Learning: Applies to **/*.ts : Never use await import() dynamic imports anywhere in the codebase

Applied to files:

  • apps/aiusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: apps/mcp/CLAUDE.md:0-0
Timestamp: 2025-09-17T18:29:15.764Z
Learning: Applies to apps/mcp/**/*.ts : NEVER use `await import()` dynamic imports anywhere

Applied to files:

  • apps/aiusage/src/data-loader.ts
🧬 Code graph analysis (4)
apps/aiusage/src/commands/dashboard.ts (4)
apps/aiusage/src/logger.ts (1)
  • logger (7-7)
apps/aiusage/src/data-loader.ts (2)
  • checkServiceAvailability (16-42)
  • loadUnifiedMonthlyData (142-175)
apps/aiusage/src/types.ts (1)
  • AIService (8-8)
packages/terminal/src/table.ts (2)
  • createUsageReportTable (443-500)
  • formatNumber (305-307)
apps/aiusage/src/commands/daily.ts (2)
apps/aiusage/src/data-loader.ts (1)
  • loadUnifiedDailyData (104-137)
packages/terminal/src/table.ts (2)
  • createUsageReportTable (443-500)
  • formatNumber (305-307)
apps/aiusage/src/data-loader.ts (3)
apps/aiusage/src/types.ts (3)
  • ServiceStatus (13-18)
  • UnifiedUsageData (23-33)
  • AIService (8-8)
apps/ccusage/src/data-loader.ts (1)
  • loadDailyUsageData (725-865)
apps/aiusage/src/logger.ts (1)
  • logger (7-7)
apps/aiusage/src/commands/monthly.ts (2)
apps/aiusage/src/data-loader.ts (1)
  • loadUnifiedMonthlyData (142-175)
packages/terminal/src/table.ts (2)
  • createUsageReportTable (443-500)
  • formatNumber (305-307)
🪛 markdownlint-cli2 (0.18.1)
apps/aiusage/README.md

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

(MD040, fenced-code-language)

🔇 Additional comments (2)
apps/aiusage/src/commands/dashboard.ts (1)

26-28: Ensure logs don’t contaminate JSON output.

If logger writes to stdout, JSON mode will be polluted. Route logs to stderr or suppress logging when args.json is true.

Would you confirm logger outputs to stderr? If not, guard logger calls in JSON mode or switch logger to stderr for JSON runs.

Also applies to: 32-57

apps/aiusage/package.json (1)

18-20: Change bin to point to dist/index.js for local workspace installs, but verification blocked by sandbox build failures.

The review suggestion is architecturally sound: having bin point to ./src/index.ts causes Node to attempt executing TypeScript in workspace installs. The package.json already correctly defines "bin": "./dist/index.js" in publishConfig (for published packages), showing the intent.

However, I cannot fully verify the change works because the build command (tsdown) failed in the sandbox environment with "command not found" (exit code 127), preventing dist/index.js from being generated and tested. This appears to be a sandbox limitation rather than a codebase issue.

The suggestion to update the regular "bin" field to "./dist/index.js" (matching publishConfig.bin) is correct and should be applied. You should manually verify in your local environment that:

  • The build succeeds: pnpm -C apps/aiusage run build
  • The built CLI works: node apps/aiusage/dist/index.js --help

Comment on lines +27 to +29
console.log('');
console.log(pc.yellow('No usage data found.'));
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace console.log with stdout writes.

Complies with repo rule banning console.log.

-    console.log('');
-    console.log(pc.yellow('No usage data found.'));
+    process.stdout.write('\n');
+    process.stdout.write(pc.yellow('No usage data found.') + '\n');
@@
-    console.log('');
-    console.log(' ╭────────────────────────────────────────────╮');
-    console.log(' │                                            │');
-    console.log(' │  AI Usage Report - Daily                   │');
-    console.log(' │                                            │');
-    console.log(' ╰────────────────────────────────────────────╯');
-    console.log('');
+    process.stdout.write('\n');
+    process.stdout.write(' ╭────────────────────────────────────────────╮\n');
+    process.stdout.write(' │                                            │\n');
+    process.stdout.write(' │  AI Usage Report - Daily                   │\n');
+    process.stdout.write(' │                                            │\n');
+    process.stdout.write(' ╰────────────────────────────────────────────╯\n');
+    process.stdout.write('\n');
@@
-    console.log(table.toString());
-    console.log('');
+    process.stdout.write(table.toString() + '\n\n');

Also applies to: 38-46, 104-106

🤖 Prompt for AI Agents
In apps/aiusage/src/commands/daily.ts around lines 27-29 (and similarly at 38-46
and 104-106), replace the console.log calls with process.stdout.write (or use
process.stdout.write with newline characters) to comply with the repo rule
banning console.log; update each console.log('') to process.stdout.write('\n'),
replace console.log(pc.yellow('No usage data found.')) with
process.stdout.write(pc.yellow('No usage data found.') + '\n'), and make
identical replacements for the other listed line ranges.

Comment on lines +60 to +68
// Table output
console.log('');
console.log(' ╭────────────────────────────────────────────╮');
console.log(' │ │');
console.log(' │ AI Usage Dashboard - All Services │');
console.log(' │ │');
console.log(' ╰────────────────────────────────────────────╯');
console.log('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Avoid console.log in CLI output; write to stdout instead.

Repo guideline forbids console.log. Use process.stdout.write for user-facing output.

-    console.log('');
+    process.stdout.write('\n');
-    console.log(' ╭────────────────────────────────────────────╮');
+    process.stdout.write(' ╭────────────────────────────────────────────╮\n');
-    console.log(' │                                            │');
+    process.stdout.write(' │                                            │\n');
-    console.log(' │  AI Usage Dashboard - All Services         │');
+    process.stdout.write(' │  AI Usage Dashboard - All Services         │\n');
-    console.log(' │                                            │');
+    process.stdout.write(' │                                            │\n');
-    console.log(' ╰────────────────────────────────────────────╯');
+    process.stdout.write(' ╰────────────────────────────────────────────╯\n');
-    console.log('');
+    process.stdout.write('\n');

Apply similarly to the availability and totals sections shown in the selected ranges.

Also applies to: 69-81, 83-94, 100-104, 114-117, 148-152

🤖 Prompt for AI Agents
In apps/aiusage/src/commands/dashboard.ts around lines 60-68 (and likewise apply
to ranges 69-81, 83-94, 100-104, 114-117, 148-152), replace user-facing
console.log calls with process.stdout.write calls so output goes to stdout per
repo guideline; change each console.log('...') to process.stdout.write('...\\n')
(or omit the trailing newline when appropriate) and ensure spacing/newlines are
preserved exactly as in the current output.

Comment on lines +105 to +137
// Aggregate by service
const byService = new Map<AIService, { tokens: number; cost: number }>();
for (const entry of data) {
const existing = byService.get(entry.service) ?? { tokens: 0, cost: 0 };
existing.tokens += entry.totalTokens;
existing.cost += entry.cost;
byService.set(entry.service, existing);
}

console.log('');
console.log(pc.bold('Total Usage (All Time):'));
console.log('');

// Create summary table
const table = createUsageReportTable({
firstColumnName: 'Service',
forceCompact: true,
});

let totalTokens = 0;
let totalCost = 0;

for (const [service, stats] of byService.entries()) {
table.push([
getServiceName(service),
'', // Models column (empty for summary)
formatNumber(stats.tokens),
'', // Output (not broken down)
formatCurrency(stats.cost),
]);
totalTokens += stats.tokens;
totalCost += stats.cost;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix summary table: stop putting total tokens under “Input”; aggregate Input/Output properly.

Current rows mislabel totals under the “Input” column. Aggregate per-service input/output and render accordingly.

-    // Aggregate by service
-    const byService = new Map<AIService, { tokens: number; cost: number }>();
+    // Aggregate by service
+    const byService = new Map<AIService, { input: number; output: number; cost: number }>();
     for (const entry of data) {
-      const existing = byService.get(entry.service) ?? { tokens: 0, cost: 0 };
-      existing.tokens += entry.totalTokens;
-      existing.cost += entry.cost;
+      const existing = byService.get(entry.service) ?? { input: 0, output: 0, cost: 0 };
+      existing.input += entry.inputTokens;
+      existing.output += entry.outputTokens;
+      existing.cost += entry.cost;
       byService.set(entry.service, existing);
     }
@@
-    let totalTokens = 0;
+    let totalInput = 0;
+    let totalOutput = 0;
     let totalCost = 0;
@@
-      table.push([
-        getServiceName(service),
-        '', // Models column (empty for summary)
-        formatNumber(stats.tokens),
-        '', // Output (not broken down)
-        formatCurrency(stats.cost),
-      ]);
-      totalTokens += stats.tokens;
+      table.push([
+        getServiceName(service),
+        '', // Models
+        formatNumber(stats.input),
+        formatNumber(stats.output),
+        formatCurrency(stats.cost),
+      ]);
+      totalInput += stats.input;
+      totalOutput += stats.output;
       totalCost += stats.cost;
     }
@@
     table.push([
       pc.yellow('Total'),
       '',
-      pc.yellow(formatNumber(totalTokens)),
-      '',
+      pc.yellow(formatNumber(totalInput)),
+      pc.yellow(formatNumber(totalOutput)),
       pc.yellow(formatCurrency(totalCost)),
     ]);

Also applies to: 139-146

🤖 Prompt for AI Agents
In apps/aiusage/src/commands/dashboard.ts around lines 105-137 (and also apply
same fix to 139-146), the summary currently accumulates totalTokens into a
single value and renders it under the “Input” column; instead, change the
aggregation to track inputTokens and outputTokens per AIService (e.g., map value
shape { inputTokens: number; outputTokens: number; cost: number }), sum
entry.inputTokens and entry.outputTokens into those fields for each entry,
update totalInput/totalOutput/totalCost accumulators, and when pushing rows to
the table write getServiceName(service), '', formatNumber(stats.inputTokens),
formatNumber(stats.outputTokens), formatCurrency(stats.cost) (and update the
overall totals row similarly) so Input and Output are aggregated and rendered in
their correct columns.

Comment on lines +27 to +29
console.log('');
console.log(pc.yellow('No usage data found.'));
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace console.log with stdout writes.

Complies with repo rule banning console.log.

-    console.log('');
-    console.log(pc.yellow('No usage data found.'));
+    process.stdout.write('\n');
+    process.stdout.write(pc.yellow('No usage data found.') + '\n');
@@
-    console.log('');
-    console.log(' ╭────────────────────────────────────────────╮');
-    console.log(' │                                            │');
-    console.log(' │  AI Usage Report - Monthly                 │');
-    console.log(' │                                            │');
-    console.log(' ╰────────────────────────────────────────────╯');
-    console.log('');
+    process.stdout.write('\n');
+    process.stdout.write(' ╭────────────────────────────────────────────╮\n');
+    process.stdout.write(' │                                            │\n');
+    process.stdout.write(' │  AI Usage Report - Monthly                 │\n');
+    process.stdout.write(' │                                            │\n');
+    process.stdout.write(' ╰────────────────────────────────────────────╯\n');
+    process.stdout.write('\n');
@@
-    console.log(table.toString());
-    console.log('');
+    process.stdout.write(table.toString() + '\n\n');

Also applies to: 38-46, 104-106

🤖 Prompt for AI Agents
In apps/aiusage/src/commands/monthly.ts around lines 27-29 (and similarly for
38-46 and 104-106) replace console.log usage with process.stdout.write: change
console.log('') to process.stdout.write('\n'), change console.log(pc.yellow('No
usage data found.')) to process.stdout.write(pc.yellow('No usage data found.') +
'\n'), and apply the same replacement pattern for all other console.log calls in
the specified ranges so output goes to stdout without using console.log.

@tysoncung
Copy link
Copy Markdown
Contributor Author

Merge Conflicts Detected

This PR has merge conflicts that need to be resolved before it can be merged.

Conflicts in:

  • .github/workflows/ci.yaml
  • .github/workflows/release.yaml

How to Resolve:

# In your local repo
git checkout feat/aiusage-unified-tracker
git fetch origin main
git rebase origin/main

# Resolve conflicts in the workflow files
# The main branch uses pnpm with separate lint-check and test jobs
# Make sure to keep the current main branch structure

git add .github/workflows/ci.yaml .github/workflows/release.yaml
git rebase --continue

# Once all conflicts are resolved
git push --force-with-lease

Context:

The main branch's CI configuration has been updated to use:

  • pnpm instead of bun
  • Separate lint-check and test jobs
  • Updated action versions

The PR branch will need to be rebased onto the latest main to pick up these changes and resolve the conflicts.

Let me know if you need help with the rebase! 🚀

@tysoncung
Copy link
Copy Markdown
Contributor Author

Closing this PR due to merge conflicts in pnpm-lock.yaml that have been sitting for a while.

The aiusage unified tracker implementation is complete and working, but the lockfile has diverged from main. If this feature is still wanted, I can regenerate the lock file and resubmit - just let me know.

@tysoncung tysoncung closed this Nov 24, 2025
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