Skip to content

Conversation

@rsnodgrass
Copy link

@rsnodgrass rsnodgrass commented Jan 12, 2026

Summary

  • Add --visual / -V flag for enhanced Tufte-style terminal visualizations
  • Significant performance improvements with concurrent file processing (pLimit)
  • Dramatically faster on large data directories (1GB+)

Features

Visual Mode (-V):

  • Compact table with 2-char model codes (op, so, ha)
  • Trend-to-average indicators with semantic colors (▲▲, ▲, ─, ▼, ▼▼)
  • Compact token formatting (951K, 5M, 1.2B)
  • Cost sparkline footer with min/max and averages
  • Calendar-style heatmap with intensity shading

New Terminal Utilities:

  • colors.ts: semantic color token system with model color palette
  • sparkline.ts: 8-level Unicode block sparklines, compact formatters
  • charts.ts: heatmap generator with weekly calendar grid

UX Refinements:

  • Costs rounded to nearest dollar in visual mode
  • Full total cost shown at bottom ($6,194 vs $6.2K)
  • Column alignment with consistent padding
  • Legend wrapping for many models
  • Heatmap columns centered under day names

Test plan

  • pnpm run format passes
  • pnpm typecheck passes
  • pnpm run test passes (all 391 tests)
  • Manual testing with pnpm run start daily -V --since 20260101

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Visual output modes (--visual): compact, bar, spark, heatmap; richer terminal charts, sparklines, heatmaps and colorized legends
    • New day subcommand for single-day activity (heatmap or JSON)
    • Visual mode integrated into daily reports with compact visual tables and visual footers
    • New --refresh-pricing flag to force pricing refresh
  • Bug Fixes / Reliability

    • 24-hour disk pricing cache with safe fallback and better concurrent data loading
  • Chores

    • Node.js runtime minimum updated to 24.11.0+

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

Add --visual / -V flag for enhanced terminal visualizations:
- Compact table with 2-char model codes (op, so, ha)
- Trend-to-average indicators with semantic colors
- Compact token formatting (951K, 5M, 1.2B)
- Cost sparkline footer with min/max and averages
- Calendar-style heatmap with intensity shading

New terminal utilities:
- colors.ts: semantic color token system with model color palette
- sparkline.ts: 8-level Unicode block sparklines, compact formatters
- charts.ts: heatmap generator with weekly calendar grid

Performance improvements:
- Concurrent file processing with pLimit
- Dramatically faster on large data directories (1GB+)

UX refinements:
- Model names shortened to 2 chars in visual mode
- Costs rounded to nearest dollar
- Full total cost shown at bottom ($6,194 vs $6.2K)
- Column alignment with consistent padding
- Legend wrapping for many models
- Heatmap columns centered under day names

Co-Authored-By: SageOx <ox@sageox.ai>
@coderabbitai
Copy link

coderabbitai bot commented Jan 12, 2026

📝 Walkthrough

Walkthrough

Replaces a boolean compact flag with a multi-mode visual setting; adds a 24‑hour disk-backed pricing cache with refresh control; introduces concurrency-limited, deduped data loading and new day activity command; and adds terminal charting, sparkline, and color utilities with public exports.

Changes

Cohort / File(s) Summary
Configuration & CLI args
apps/ccusage/config-schema.json, apps/ccusage/src/_shared-args.ts
Adds visual config/CLI (-V) across commands (string enum or boolean depending on block); marks compact deprecated; adds refreshPricing (-R) and clarifies offline default/description.
Pricing cache & fetcher
apps/ccusage/src/_pricing-fetcher.ts
Introduces PricingFetcherOptions and disk-backed 24h TTL cache (load/validate/save), supports legacy boolean constructor, implements cache-first fetch with refresh override and helpers to seed internal cache.
Data loading & concurrency
apps/ccusage/src/data-loader.ts
Adds p-limit concurrency for file I/O, threads refreshPricing through loaders, preloads pricing to compute per-entry costs, deduplicates entries, exports ActivityEntry and new loadDayActivityData.
Terminal visualization package
packages/terminal/src/charts.ts, packages/terminal/src/sparkline.ts, packages/terminal/src/colors.ts, packages/terminal/package.json
New charting, sparkline, and color utilities (bar charts, sparklines, heatmaps, day activity grid, color utils, thresholds, trend indicators) with many new exported types/functions; package exports updated to expose these modules.
Commands & wiring
apps/ccusage/src/commands/daily.ts, apps/ccusage/src/commands/day.ts, apps/ccusage/src/commands/index.ts
daily gains visual rendering path (compact visual + sparkline/heatmap footer); new day subcommand renders single-day activity grid or JSON and is registered in command index.
Package runtime constraint
package.json
Relaxed dev engines runtime constraint from ^24.11.0>=24.11.0.
Package exports
packages/terminal/package.json
Exposes ./colors, ./sparkline, and ./charts from terminal package.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI / Command
    participant Pricing as PricingFetcher
    participant Disk as Disk Cache
    participant Network as Network / Bundle
    participant Internal as Internal Cache

    CLI->>Pricing: fetchModelPricing(refresh? / offline?)
    alt refreshPricing
        Pricing->>Network: fetch fresh pricing
        Network-->>Pricing: pricing data
        Pricing->>Internal: set internal cache
        Pricing->>Disk: persist pricing
    else
        Pricing->>Disk: check cached file (24h TTL)
        alt disk valid
            Disk-->>Pricing: cached pricing
            Pricing->>Internal: set internal cache
        else
            Pricing->>Network: fetch pricing / fall back to bundle
            Network-->>Pricing: pricing data
            Pricing->>Internal: set internal cache
            Pricing->>Disk: persist pricing
        end
    end
    Pricing-->>CLI: pricing data
Loading
sequenceDiagram
    participant User as User
    participant Daily as Daily Command
    participant Loader as Data Loader
    participant Charts as Chart Utils
    participant Term as Terminal Output

    User->>Daily: run --visual
    Daily->>Loader: loadDailyUsageData(refreshPricing?, offline?)
    Loader->>Loader: read files (p-limit), compute per-entry cost, dedupe
    Loader-->>Daily: entries
    Daily->>Charts: toChartData(entries)
    Charts-->>Daily: chart data (bars/spark/heatmap)
    Daily->>Term: renderCompactVisual(table)
    Daily->>Term: renderVisualFooter(sparkline, heatmap)
    Term-->>User: display
Loading
sequenceDiagram
    participant User as User
    participant Day as Day Command
    participant Loader as Data Loader
    participant Grid as Activity Grid
    participant Term as Terminal Output

    User->>Day: run day YYYYMMDD (--visual or --json)
    Day->>Loader: loadDayActivityData(date, refreshPricing?, offline?)
    Loader->>Loader: extract 15-min buckets, compute costs
    Loader-->>Day: ActivityEntry[]
    alt --json requested
        Day->>Term: output JSON summary (optionally pipe to jq)
    else visual
        Day->>Grid: createDayActivityGrid(entries)
        Grid-->>Term: grid + legend + hourly costs
    end
    Term-->>User: display
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~90 minutes

Possibly related PRs

Suggested reviewers

  • ryoppippi

Poem

🐰 I hopped through code with a cheerful caper,
Sparklines, heatmaps — I drew each paper.
Cached the prices, kept I/O small,
Charts and colors — I painted them all.
Happy hops — enjoy the caper! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main feature: adding a Tufte-style visual mode for daily reports, which is the primary focus of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 97.62% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
apps/ccusage/src/data-loader.ts (2)

1345-1352: Consider extracting dedup fields to avoid type assertion.

The type assertion to add internal _messageId and _requestId fields works but introduces a mild type safety gap. Consider defining a separate internal type for entries during processing.

♻️ Optional: Define internal processing type
+// Internal type for entries during processing (before stripping dedup fields)
+type ActivityEntryWithDedup = ActivityEntry & {
+	_messageId?: string;
+	_requestId?: string;
+};

 // In the processing loop:
-						entries.push({
-							timestamp: data.timestamp,
-							cost,
-							outputTokens: data.message.usage.output_tokens,
-							model: data.message.model,
-							_messageId: data.message.id,
-							_requestId: data.requestId,
-						} as ActivityEntry & { _messageId?: string; _requestId?: string });
+						entries.push({
+							timestamp: data.timestamp,
+							cost,
+							outputTokens: data.message.usage.output_tokens,
+							model: data.message.model,
+							_messageId: data.message.id,
+							_requestId: data.requestId,
+						} satisfies ActivityEntryWithDedup);

4461-4575: Good test coverage, consider adding deduplication test.

The tests cover the main scenarios well, especially the timezone edge case test. Consider adding a test for deduplication behavior when the same message appears in multiple files.

📝 Optional: Add deduplication test
it('deduplicates entries with same message and request IDs', async () => {
	await using fixture = await createFixture({
		'.claude': {
			projects: {
				'project1': {
					'session-1.jsonl': `${JSON.stringify({
						timestamp: '2024-01-15T10:00:00Z',
						message: {
							id: 'msg_001',
							usage: { input_tokens: 100, output_tokens: 50 },
							model: 'claude-sonnet-4-20250514',
						},
						requestId: 'req_001',
						costUSD: 0.5,
					})}\n`,
				},
				'project2': {
					'session-2.jsonl': `${JSON.stringify({
						timestamp: '2024-01-15T11:00:00Z',
						message: {
							id: 'msg_001', // Same message ID
							usage: { input_tokens: 100, output_tokens: 50 },
							model: 'claude-sonnet-4-20250514',
						},
						requestId: 'req_001', // Same request ID
						costUSD: 0.5,
					})}\n`,
				},
			},
		},
	});

	vi.stubEnv('CLAUDE_CONFIG_DIR', fixture.getPath('.claude'));

	const result = await loadDayActivityData('2024-01-15', { mode: 'display' });

	// Should deduplicate and return only one entry
	expect(result).toHaveLength(1);
});

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f8b386 and 5319fe2.

📒 Files selected for processing (1)
  • apps/ccusage/src/data-loader.ts
🧰 Additional context used
📓 Path-based instructions (3)
apps/ccusage/src/**/*.ts

📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)

apps/ccusage/src/**/*.ts: Write tests in-source using if (import.meta.vitest != null) blocks instead of separate test files
Use Vitest globals (describe, it, expect) without imports in test blocks
In tests, use current Claude 4 models (sonnet-4, opus-4)
Use fs-fixture with createFixture() to simulate Claude data in tests
Only export symbols that are actually used by other modules
Do not use console.log; use the logger utilities from src/logger.ts instead

Files:

  • apps/ccusage/src/data-loader.ts
apps/ccusage/**/*.ts

📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)

apps/ccusage/**/*.ts: NEVER use await import() dynamic imports anywhere (especially in tests)
Prefer @praha/byethrow Result type for error handling instead of try-catch
Use .ts extensions for local imports (e.g., import { foo } from './utils.ts')

Files:

  • apps/ccusage/src/data-loader.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Import conventions: Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Use ESLint for linting and formatting with tab indentation and double quotes
No console.log allowed except where explicitly disabled with eslint-disable
Use camelCase for variable names (e.g., usageDataSchema, modelBreakdownSchema)
Use PascalCase for type names (e.g., UsageData, ModelBreakdown)
Use UPPER_SNAKE_CASE for constants (e.g., DEFAULT_CLAUDE_CODE_PATH)
Only export constants, functions, and types that are actually used by other modules - do not export internal/private constants used only within the same file
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.isFailure() for checking errors (more readable than !Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators with Result types
NEVER use await import() dynamic imports anywhere in the codebase - this causes tree-shaking issues
Do not use console.log - use logger.ts instead
Vitest globals (describe, it, expect) are available automatically without imports in test blocks
NEVER use await import() dynamic imports in vitest test blocks - this is particularly problematic for test execution
All test files must use current Claude 4 models (claude-sonnet-4-20250514, claude-opus-4-20250514), not outdated Claude 3 models
Model names in tests must exactly match LiteLLM's pricing database entries

Files:

  • apps/ccusage/src/data-loader.ts
🧠 Learnings (20)
📚 Learning: 2026-01-09T12:22:39.548Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.548Z
Learning: The data loading architecture follows a clear separation of concerns: Data Loading (data-loader.ts), Token Aggregation (calculate-cost.ts), Command Execution (commands/), and CLI Entry (index.ts)

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.625Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.625Z
Learning: Applies to apps/opencode/**/*.{ts,tsx,js,jsx} : Calculate costs using model pricing data when pre-calculated `cost` field is not present in OpenCode messages

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:22:39.548Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.548Z
Learning: Applies to **/*.ts : NEVER use `await import()` dynamic imports anywhere in the codebase - this causes tree-shaking issues

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:07:00.527Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.527Z
Learning: Applies to apps/pi/src/**/*.{ts,tsx} : NEVER use `await import()` dynamic imports anywhere in the codebase

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:22:39.548Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.548Z
Learning: Applies to **/*.ts : NEVER use `await import()` dynamic imports in vitest test blocks - this is particularly problematic for test execution

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/src/**/*.ts : Use `fs-fixture` with `createFixture()` to simulate Claude data in tests

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:35:27.250Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.250Z
Learning: Applies to apps/amp/**/*.{js,ts,jsx,tsx} : NEVER use await import() dynamic imports anywhere, especially in test blocks in Amp CLI

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:07:00.527Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.527Z
Learning: Applies to apps/pi/**/*.test.{ts,tsx} : Use `fs-fixture` for creating mock data in tests for testing data loading functionality

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.625Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.625Z
Learning: Applies to apps/opencode/**/*.test.{ts,tsx,js,jsx} : Use `fs-fixture` with `using` statement to ensure cleanup in tests

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Fetch per-model pricing from LiteLLM model_prices_and_context_window.json via LiteLLMPricingFetcher using an offline cache scoped to Codex-prefixed models; handle aliases (e.g., gpt-5-codex → gpt-5) in CodexPricingSource

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Cost calculation per model/date: charge non-cached input, cached input (fallback to input rate if missing), and output using the specified per-million token rates

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Tests should use fs-fixture with using to ensure cleanup

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:35:27.250Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.250Z
Learning: Use fs-fixture with using statement for test cleanup

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/mcp/CLAUDE.md:0-0
Timestamp: 2025-09-17T18:29:15.764Z
Learning: Applies to apps/mcp/**/*.{test,spec}.ts : Use `fs-fixture` for mock data in tests of MCP server functionality

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.625Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.625Z
Learning: Applies to apps/opencode/**/*.test.{ts,tsx,js,jsx} : Vitest globals are enabled - use `describe`, `it`, `expect` directly without imports

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:07:00.527Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.527Z
Learning: Applies to apps/pi/**/*.test.{ts,tsx} : Use vitest globals (`describe`, `it`, `expect`) directly without imports in test files

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
🧬 Code graph analysis (1)
apps/ccusage/src/data-loader.ts (5)
apps/ccusage/src/_pricing-fetcher.ts (1)
  • PricingFetcher (73-132)
apps/ccusage/src/_date-utils.ts (1)
  • formatDate (43-48)
apps/ccusage/src/_consts.ts (1)
  • DEFAULT_LOCALE (129-129)
apps/ccusage/src/_types.ts (1)
  • CostMode (145-145)
packages/terminal/src/charts.ts (1)
  • ActivityEntry (368-373)
🔇 Additional comments (8)
apps/ccusage/src/data-loader.ts (8)

11-25: LGTM!

The imports are well-organized. The stat function is now properly imported statically from node:fs/promises instead of using a dynamic import, which addresses the previous review feedback.


599-607: LGTM!

Good addition of concurrency control with pLimit(20) to prevent I/O saturation when scanning timestamps across many files. The limit is reasonable for file operations.


790-793: LGTM!

Pre-loading pricing data before parallel file processing is the correct approach to avoid race conditions where multiple concurrent cost calculations might trigger redundant fetches or cache inconsistencies.


799-869: LGTM!

The parallel file processing is well-designed:

  • Files are sorted by timestamp first, ensuring chronological processing order
  • Promise.all preserves the input order in its results
  • Sequential deduplication after parallel processing correctly keeps the earliest entry
  • The empty string sentinel for "no dedup key" is handled correctly

1294-1337: LGTM!

The timezone edge case handling is well-designed:

  • The fast-path regex accepts ±1 day in UTC dates to account for timezone differences (up to ±14 hours)
  • Full local date verification after JSON parsing ensures accurate filtering
  • The comments clearly explain the reasoning

741-741: LGTM!

The refreshPricing option is properly added to LoadOptions and consistently propagated through all data loading functions. This provides a clean API for callers to force pricing refresh when needed.


1219-1223: LGTM!

Re-exporting ActivityEntry from @ccusage/terminal/charts provides a convenient single import location for consumers of the data-loader module.


782-785: LGTM!

The PricingFetcher instantiation pattern is consistent throughout the module, properly passing both offline and refreshPricing options. The using statement ensures proper cleanup via the disposable pattern.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @apps/ccusage/config-schema.json:
- Around line 101-106: The schema lists "visual" as a string enum with values
["compact","bar","spark","heatmap"] but the codebase treats visual as a boolean
(checking visual === true) and only renders the compact mode; either update the
config-schema.json "visual" to a boolean type (removing the enum) to match
current runtime behavior, or change the runtime to accept the string modes:
replace the boolean check with a switch on the "visual" string and
implement/route to handlers for "compact", "bar", "spark", and "heatmap"
(add/rename functions used in rendering where visual is checked to explicit
renderCompact, renderBar, renderSpark, renderHeatmap or similar), and ensure
validation and tests reflect the chosen approach.

In @apps/ccusage/src/data-loader.ts:
- Line 1260: The code uses a dynamic import to get stat via "const { stat } =
await import('node:fs/promises')", which is unnecessary and harmful for
tree-shaking; instead add stat to the existing top-level import that currently
brings in readFile (e.g., import { readFile } from 'node:fs/promises') so it
becomes import { readFile, stat } from 'node:fs/promises', then replace the
dynamic import usage by calling stat directly where the dynamic import was used.

In @packages/terminal/src/charts.ts:
- Around line 76-80: The code uses new Date(entry.date) which treats
"YYYY-MM-DD" as UTC and can shift the day in some timezones; instead parse the
date string into local components or append a local time so it constructs a
local Date. Update the parsing around the date and dateStr variables (where
entry.date is read and date is created) to either split entry.date into
year/month/day and call new Date(year, month-1, day) or append "T00:00:00" (or
otherwise provide an explicit time) before calling new Date so dateStr is
computed consistently across timezones.
- Around line 353-357: ActivityEntry is duplicated with divergent shapes (one
variant includes an optional model) causing conversions in day.ts; extract a
single shared type definition for ActivityEntry in a common types module/package
and export it, update the local declarations in charts.ts and the data-loader to
import that shared ActivityEntry, include the optional model field if needed (or
make it optional to satisfy both callers), and remove the ad-hoc
aliasing/conversion in day.ts so all code imports and uses the single canonical
ActivityEntry type.
🧹 Nitpick comments (5)
apps/ccusage/src/commands/day.ts (1)

36-39: Consider extracting shortenModelName to the shared terminal package.

This function duplicates logic from @ccusage/terminal/colors which already exports shortenModelName. The daily.ts file imports it from there at line 11.

♻️ Suggested refactor
+import { shortenModelName } from '@ccusage/terminal/colors';
 import { createDayActivityGrid } from '@ccusage/terminal/charts';
 import { Result } from '@praha/byethrow';
 ...
-/**
- * Shorten model name by removing the trailing date suffix.
- * e.g., "claude-opus-4-5-20251101" -> "claude-opus-4-5"
- */
-function shortenModelName(model: string): string {
-	// match pattern like -YYYYMMDD at the end
-	return model.replace(/-\d{8}$/, '');
-}
apps/ccusage/src/_pricing-fetcher.ts (2)

44-52: Consider using Result.try() for JSON parsing.

The JSON.parse call can throw on malformed cache data. Per coding guidelines, prefer Result.try() for operations that may throw.

♻️ Suggested refactor
 async function loadDiskCache(): Promise<Record<string, LiteLLMModelPricing> | null> {
 	try {
 		const data = await fs.readFile(getCacheFilePath(), 'utf-8');
-		return JSON.parse(data) as Record<string, LiteLLMModelPricing>;
+		const parseResult = Result.try(() => JSON.parse(data) as Record<string, LiteLLMModelPricing>);
+		if (Result.isFailure(parseResult)) {
+			return null;
+		}
+		return parseResult.value;
 	} catch {
 		return null;
 	}
 }

127-131: Document the private cache access workaround.

The type assertion to access the parent's private cachedPricing property is fragile and could break if the parent class internals change. Consider adding a comment explaining why this is necessary and any upstream changes that could help.

📝 Suggested documentation
 	private setCachedPricing(pricing: Map<string, LiteLLMModelPricing>): void {
-		// access parent's private cache via type assertion
+		// WORKAROUND: Access parent's private cache via type assertion.
+		// This is necessary because LiteLLMPricingFetcher doesn't expose a setter for cachedPricing.
+		// If the parent class is updated to expose this, prefer using the official API.
 		(this as unknown as { cachedPricing: Map<string, LiteLLMModelPricing> | null }).cachedPricing =
 			pricing;
 	}
packages/terminal/src/colors.ts (1)

194-214: Extract duplicate letter extraction logic into a helper function.

The letter extraction logic (opus→O, sonnet→S, haiku→H, gpt→G) is duplicated between createModelIdentifier and createModelLegendEntry. This violates DRY and could lead to inconsistencies if updated in only one place.

♻️ Suggested refactor
+/**
+ * Extract a single-character identifier from a model name.
+ * @param modelName - Full model name
+ * @returns Single uppercase letter representing the model family
+ */
+function extractModelLetter(modelName: string): string {
+	const nameLower = modelName.toLowerCase();
+	if (nameLower.includes('opus')) return 'O';
+	if (nameLower.includes('sonnet')) return 'S';
+	if (nameLower.includes('haiku')) return 'H';
+	if (nameLower.includes('gpt')) return 'G';
+	const parts = nameLower.split('-').filter((p) => p !== 'claude');
+	return (parts[0]?.charAt(0) ?? 'M').toUpperCase();
+}
+
 export function createModelIdentifier(modelName: string, colorIndex: number): string {
 	const color = getModelColor(colorIndex);
-	const nameLower = modelName.toLowerCase();
-	let letter = 'M';
-	if (nameLower.includes('opus')) {
-		letter = 'O';
-	} else if (nameLower.includes('sonnet')) {
-		letter = 'S';
-	} else if (nameLower.includes('haiku')) {
-		letter = 'H';
-	} else if (nameLower.includes('gpt')) {
-		letter = 'G';
-	} else {
-		const parts = nameLower.split('-').filter((p) => p !== 'claude');
-		letter = (parts[0]?.charAt(0) ?? 'M').toUpperCase();
-	}
+	const letter = extractModelLetter(modelName);
 	return `${color(LEGEND_BULLET)}${letter}`;
 }

Also applies to: 235-254

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

1330-1337: Avoid extending ActivityEntry with internal fields via type assertion.

The pattern of adding _messageId and _requestId to ActivityEntry via type casting creates a hidden contract that's not visible in the type definition. Consider using a local internal type for the intermediate processing.

♻️ Suggested refactor
+// Internal type for deduplication during processing
+type InternalActivityEntry = ActivityEntry & {
+	_messageId?: string;
+	_requestId?: string;
+};
+
 // In the processing loop:
-						entries.push({
-							timestamp: data.timestamp,
-							cost,
-							outputTokens: data.message.usage.output_tokens,
-							model: data.message.model,
-							_messageId: data.message.id,
-							_requestId: data.requestId,
-						} as ActivityEntry & { _messageId?: string; _requestId?: string });
+						entries.push({
+							timestamp: data.timestamp,
+							cost,
+							outputTokens: data.message.usage.output_tokens,
+							model: data.message.model,
+							_messageId: data.message.id,
+							_requestId: data.requestId,
+						});

 // Change entries type:
-				const entries: ActivityEntry[] = [];
+				const entries: InternalActivityEntry[] = [];
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d04a18 and 8081599.

📒 Files selected for processing (12)
  • apps/ccusage/config-schema.json
  • apps/ccusage/src/_pricing-fetcher.ts
  • apps/ccusage/src/_shared-args.ts
  • apps/ccusage/src/commands/daily.ts
  • apps/ccusage/src/commands/day.ts
  • apps/ccusage/src/commands/index.ts
  • apps/ccusage/src/data-loader.ts
  • package.json
  • packages/terminal/package.json
  • packages/terminal/src/charts.ts
  • packages/terminal/src/colors.ts
  • packages/terminal/src/sparkline.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/ccusage/src/**/*.ts

📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)

apps/ccusage/src/**/*.ts: Write tests in-source using if (import.meta.vitest != null) blocks instead of separate test files
Use Vitest globals (describe, it, expect) without imports in test blocks
In tests, use current Claude 4 models (sonnet-4, opus-4)
Use fs-fixture with createFixture() to simulate Claude data in tests
Only export symbols that are actually used by other modules
Do not use console.log; use the logger utilities from src/logger.ts instead

Files:

  • apps/ccusage/src/commands/daily.ts
  • apps/ccusage/src/commands/index.ts
  • apps/ccusage/src/commands/day.ts
  • apps/ccusage/src/data-loader.ts
  • apps/ccusage/src/_pricing-fetcher.ts
  • apps/ccusage/src/_shared-args.ts
apps/ccusage/**/*.ts

📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)

apps/ccusage/**/*.ts: NEVER use await import() dynamic imports anywhere (especially in tests)
Prefer @praha/byethrow Result type for error handling instead of try-catch
Use .ts extensions for local imports (e.g., import { foo } from './utils.ts')

Files:

  • apps/ccusage/src/commands/daily.ts
  • apps/ccusage/src/commands/index.ts
  • apps/ccusage/src/commands/day.ts
  • apps/ccusage/src/data-loader.ts
  • apps/ccusage/src/_pricing-fetcher.ts
  • apps/ccusage/src/_shared-args.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Import conventions: Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Use ESLint for linting and formatting with tab indentation and double quotes
No console.log allowed except where explicitly disabled with eslint-disable
Use camelCase for variable names (e.g., usageDataSchema, modelBreakdownSchema)
Use PascalCase for type names (e.g., UsageData, ModelBreakdown)
Use UPPER_SNAKE_CASE for constants (e.g., DEFAULT_CLAUDE_CODE_PATH)
Only export constants, functions, and types that are actually used by other modules - do not export internal/private constants used only within the same file
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.isFailure() for checking errors (more readable than !Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators with Result types
NEVER use await import() dynamic imports anywhere in the codebase - this causes tree-shaking issues
Do not use console.log - use logger.ts instead
Vitest globals (describe, it, expect) are available automatically without imports in test blocks
NEVER use await import() dynamic imports in vitest test blocks - this is particularly problematic for test execution
All test files must use current Claude 4 models (claude-sonnet-4-20250514, claude-opus-4-20250514), not outdated Claude 3 models
Model names in tests must exactly match LiteLLM's pricing database entries

Files:

  • apps/ccusage/src/commands/daily.ts
  • apps/ccusage/src/commands/index.ts
  • apps/ccusage/src/commands/day.ts
  • apps/ccusage/src/data-loader.ts
  • apps/ccusage/src/_pricing-fetcher.ts
  • packages/terminal/src/charts.ts
  • packages/terminal/src/colors.ts
  • packages/terminal/src/sparkline.ts
  • apps/ccusage/src/_shared-args.ts
**/_*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use underscore prefix for internal files (e.g., _types.ts, _utils.ts, _consts.ts)

Files:

  • apps/ccusage/src/_pricing-fetcher.ts
  • apps/ccusage/src/_shared-args.ts
🧠 Learnings (39)
📓 Common learnings
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.240Z
Learning: Reuse shared packages ccusage/terminal and ccusage/internal wherever possible in Amp CLI
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: The data loading architecture follows a clear separation of concerns: Data Loading (data-loader.ts), Token Aggregation (calculate-cost.ts), Command Execution (commands/), and CLI Entry (index.ts)

Applied to files:

  • apps/ccusage/src/commands/daily.ts
  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: --json outputs structured results with aggregated tokens and USD cost included in totals

Applied to files:

  • apps/ccusage/src/commands/daily.ts
📚 Learning: 2026-01-09T11:07:17.610Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.610Z
Learning: Applies to apps/opencode/**/*.{ts,tsx,js,jsx} : Reuse shared packages (`ccusage/terminal`, `ccusage/internal`) wherever possible

Applied to files:

  • packages/terminal/package.json
  • apps/ccusage/src/commands/index.ts
  • packages/terminal/src/colors.ts
  • apps/ccusage/src/_shared-args.ts
📚 Learning: 2026-01-09T12:35:27.240Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.240Z
Learning: Reuse shared packages ccusage/terminal and ccusage/internal wherever possible in Amp CLI

Applied to files:

  • packages/terminal/package.json
  • apps/ccusage/src/commands/index.ts
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: For packages, use the package directory name in commit scope (e.g., `feat(terminal):`, `fix(ui):`)

Applied to files:

  • packages/terminal/package.json
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/mcp/CLAUDE.md:0-0
Timestamp: 2025-09-17T18:29:15.764Z
Learning: Applies to apps/mcp/**/*.ts : Only export what is actually used

Applied to files:

  • packages/terminal/package.json
📚 Learning: 2026-01-09T12:07:00.515Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.515Z
Learning: Applies to apps/pi/src/**/*.{ts,tsx} : Only export what's actually used from modules

Applied to files:

  • packages/terminal/package.json
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: Applies to **/*.ts : Only export constants, functions, and types that are actually used by other modules - do not export internal/private constants used only within the same file

Applied to files:

  • packages/terminal/package.json
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/src/**/*.ts : Only export symbols that are actually used by other modules

Applied to files:

  • packages/terminal/package.json
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/**/*.ts : Use `.ts` extensions for local imports (e.g., `import { foo } from './utils.ts'`)

Applied to files:

  • packages/terminal/package.json
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: Applies to **/*.ts : Import conventions: Use `.ts` extensions for local file imports (e.g., `import { foo } from './utils.ts'`)

Applied to files:

  • packages/terminal/package.json
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/**/package.json : Because the CLI is bundled, keep all runtime libraries in devDependencies so the bundler captures them

Applied to files:

  • packages/terminal/package.json
  • package.json
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Entry point remains Gunshi-based; only the daily subcommand is wired for now

Applied to files:

  • apps/ccusage/src/commands/index.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/commands/index.ts
📚 Learning: 2026-01-09T12:35:27.240Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.240Z
Learning: Use Gunshi framework with subcommands (daily, monthly, session) as entry point for Amp CLI

Applied to files:

  • apps/ccusage/src/commands/index.ts
📚 Learning: 2026-01-09T11:07:17.610Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.610Z
Learning: Applies to apps/opencode/**/*.{ts,tsx,js,jsx} : Calculate costs using model pricing data when pre-calculated `cost` field is not present in OpenCode messages

Applied to files:

  • apps/ccusage/src/data-loader.ts
  • apps/ccusage/src/_pricing-fetcher.ts
📚 Learning: 2026-01-09T12:07:00.515Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.515Z
Learning: Applies to apps/pi/src/**/*.{ts,tsx} : Prefer `praha/byethrow Result` type over try-catch for error handling

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.610Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.610Z
Learning: Applies to apps/opencode/**/*.test.{ts,tsx,js,jsx} : Use `fs-fixture` with `using` statement to ensure cleanup in tests

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/src/**/*.ts : Use `fs-fixture` with `createFixture()` to simulate Claude data in tests

Applied to files:

  • apps/ccusage/src/data-loader.ts
  • apps/ccusage/src/_pricing-fetcher.ts
📚 Learning: 2026-01-09T12:07:00.515Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.515Z
Learning: Applies to apps/pi/**/*.test.{ts,tsx} : Use `fs-fixture` for creating mock data in tests for testing data loading functionality

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.610Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.610Z
Learning: Applies to apps/opencode/**/*.test.{ts,tsx,js,jsx} : Vitest globals are enabled - use `describe`, `it`, `expect` directly without imports

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: Applies to **/*.ts : Prefer praha/byethrow Result type over traditional try-catch for functional error handling

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/mcp/CLAUDE.md:0-0
Timestamp: 2025-09-17T18:29:15.764Z
Learning: Applies to apps/mcp/**/*.{test,spec}.ts : Use `fs-fixture` for mock data in tests of MCP server functionality

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Tests should use fs-fixture with using to ensure cleanup

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:35:27.240Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.240Z
Learning: Applies to apps/amp/**/*.{test,spec}.{js,ts,jsx,tsx} : Enable vitest globals (describe, it, expect) without imports in test blocks

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Fetch per-model pricing from LiteLLM model_prices_and_context_window.json via LiteLLMPricingFetcher using an offline cache scoped to Codex-prefixed models; handle aliases (e.g., gpt-5-codex → gpt-5) in CodexPricingSource

Applied to files:

  • apps/ccusage/src/data-loader.ts
  • apps/ccusage/src/_pricing-fetcher.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Cost calculation per model/date: charge non-cached input, cached input (fallback to input rate if missing), and output using the specified per-million token rates

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:35:27.240Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.240Z
Learning: Calculate costs using LiteLLM pricing data with formula: input (inputTokens/1M * rate), cached read (cacheReadInputTokens/1M * rate), cache creation (cacheCreationInputTokens/1M * rate), output (outputTokens/1M * rate)

Applied to files:

  • apps/ccusage/src/_pricing-fetcher.ts
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: Applies to **/*.ts : Model names in tests must exactly match LiteLLM's pricing database entries

Applied to files:

  • apps/ccusage/src/_pricing-fetcher.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/src/**/*.ts : In tests, use current Claude 4 models (sonnet-4, opus-4)

Applied to files:

  • apps/ccusage/src/_pricing-fetcher.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Pricing tests must inject stub offline loaders to avoid network access

Applied to files:

  • apps/ccusage/src/_pricing-fetcher.ts
📚 Learning: 2026-01-09T12:07:00.515Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.515Z
Learning: Applies to apps/pi/**/package.json : Add dependencies as `devDependencies` unless explicitly requested to add as runtime dependencies

Applied to files:

  • package.json
📚 Learning: 2026-01-09T11:07:17.610Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.610Z
Learning: Applies to apps/opencode/**/package.json : Keep every runtime dependency in `devDependencies` because OpenCode is packaged as a bundled CLI

Applied to files:

  • package.json
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: Applies to apps/**/package.json : All projects under `apps/` ship as bundled CLIs/binaries - list runtime dependencies in `devDependencies` (never `dependencies`) so the bundler owns the runtime payload

Applied to files:

  • package.json
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/mcp/CLAUDE.md:0-0
Timestamp: 2025-09-17T18:29:15.764Z
Learning: Applies to apps/mcp/**/package.json : Add new dependencies as `devDependencies` unless explicitly requested otherwise

Applied to files:

  • package.json
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Applies to apps/codex/**/package.json : Package Codex as a bundled CLI and keep every runtime dependency in devDependencies so the bundle includes shipped code

Applied to files:

  • package.json
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Command flag --offline forces use of the embedded pricing snapshot

Applied to files:

  • apps/ccusage/src/_shared-args.ts
🧬 Code graph analysis (5)
apps/ccusage/src/commands/daily.ts (5)
apps/ccusage/src/data-loader.ts (1)
  • DailyUsage (257-257)
packages/terminal/src/charts.ts (2)
  • ChartDataEntry (11-17)
  • createHeatmap (230-336)
apps/ccusage/src/calculate-cost.ts (1)
  • calculateTotals (48-67)
packages/terminal/src/colors.ts (2)
  • colors (15-49)
  • getModelColor (61-63)
packages/terminal/src/sparkline.ts (3)
  • formatTokensCompact (273-284)
  • createSparkline (51-85)
  • formatCostCompact (289-297)
apps/ccusage/src/commands/index.ts (1)
apps/ccusage/src/commands/day.ts (1)
  • dayCommand (41-151)
apps/ccusage/src/_pricing-fetcher.ts (1)
packages/internal/src/pricing.ts (2)
  • LiteLLMModelPricing (49-49)
  • LiteLLMPricingFetcher (89-362)
packages/terminal/src/charts.ts (4)
packages/terminal/src/sparkline.ts (3)
  • formatCostCompact (289-297)
  • formatTokensCompact (273-284)
  • createSparkline (51-85)
packages/terminal/src/utils.ts (1)
  • width (153-155)
apps/ccusage/src/_date-utils.ts (1)
  • formatDate (43-48)
apps/ccusage/src/data-loader.ts (1)
  • ActivityEntry (1221-1226)
packages/terminal/src/sparkline.ts (1)
packages/terminal/src/colors.ts (2)
  • DeviationThresholds (97-106)
  • colors (15-49)
🪛 Biome (2.1.2)
apps/ccusage/src/commands/daily.ts

[error] 123-123: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)

🔇 Additional comments (26)
package.json (1)

52-52: LGTM - Node.js version constraint relaxation is reasonable.

Changing from ^24.11.0 to >=24.11.0 allows developers to use newer Node versions. Since this is a devEngines constraint with "onFail": "download", the flexibility is appropriate for development environments.

packages/terminal/package.json (1)

9-12: LGTM - New terminal utility exports are properly structured.

The new exports for ./colors, ./sparkline, and ./charts follow the existing export pattern and enable the visual mode features across the CLI commands. Based on learnings, this aligns with the pattern of reusing shared packages like @ccusage/terminal.

apps/ccusage/src/commands/index.ts (2)

6-6: LGTM - Day command import follows conventions.

The import uses the .ts extension as required by the coding guidelines.


16-16: LGTM - Day command properly integrated into CLI.

The dayCommand is correctly exported and added to subCommandUnion, following the same pattern as existing commands.

Also applies to: 28-28

apps/ccusage/src/commands/day.ts (4)

1-11: LGTM - Imports are properly structured.

All local imports use .ts extensions as required. The file uses @praha/byethrow Result type and logger from the project's logger module, following the coding guidelines.


61-85: LGTM - Command run implementation follows established patterns.

The implementation correctly:

  • Merges config with CLI args
  • Handles --jq implying --json
  • Falls back to today's date using local time
  • Passes options to loadDayActivityData

87-115: LGTM - JSON output path with proper error handling.

Uses Result.isFailure() for jq processing errors as required by coding guidelines, and logs errors via logger.error() instead of console.log.


136-148: LGTM - Visual output path with model filtering.

The filtering of synthetic models and shortening of model names provides clean output for the heatmap display.

apps/ccusage/src/commands/daily.ts (5)

1-16: LGTM - Imports properly structured for visual mode.

All new imports for chart data, colors, sparkline formatting, and heatmap rendering use the correct .ts extensions for local imports and proper package paths for terminal utilities.


122-125: Intentional ANSI escape code pattern - static analysis false positive.

The regex /\x1B\[[0-9;]*m/g is the standard pattern for stripping ANSI color codes to calculate visible string width. The Biome warning about "unexpected control character" is a false positive since \x1B (ESC) is required for matching ANSI sequences.


144-252: LGTM - Compact visual rendering is well-structured.

The implementation properly:

  • Defines explicit column widths for alignment
  • Uses semantic colors from the terminal package
  • Calculates trend deviation against average
  • Wraps legend entries to fit terminal width

254-287: LGTM - Visual footer with sparkline and heatmap.

The footer provides a cost trend sparkline with min/max range and renders the calendar-style heatmap, completing the Tufte-style visualization.


402-414: LGTM - Visual mode integration with backward compatibility.

The condition mergedOptions.visual === true || ctx.values.compact === true maintains backward compatibility with the deprecated --compact flag while enabling the new --visual mode. The early return prevents duplicate output.

apps/ccusage/src/_shared-args.ts (1)

77-82: LGTM - New refreshPricing flag for cache control.

The -R short flag and description clearly indicate this forces a network refresh, bypassing the 24-hour disk cache.

apps/ccusage/config-schema.json (1)

97-100: LGTM - Deprecation notice for compact is clear.

The deprecation message properly directs users to use --visual compact instead.

apps/ccusage/src/_pricing-fetcher.ts (1)

91-116: LGTM - Disk cache integration logic is well-structured.

The cache-first approach with proper fallback to parent implementation is clean. The diskCacheLoaded flag prevents redundant cache checks on subsequent calls.

packages/terminal/src/colors.ts (2)

1-49: LGTM - Well-designed semantic color token system.

The hierarchical organization (text, semantic, ui) provides good separation of concerns. The as const assertion ensures type safety for the color tokens.


256-375: LGTM - Comprehensive test coverage.

Tests cover color tokens, model color cycling, value thresholds, trend indicators, and model identifiers. The test assertions verify both behavior and expected values.

packages/terminal/src/sparkline.ts (2)

51-85: LGTM - Sparkline generation handles edge cases well.

Empty arrays, single values, and uniform values are all handled gracefully. The normalization and character mapping logic is correct.


273-297: LGTM - Compact formatting utilities are well-implemented.

The threshold-based formatting for tokens and costs provides good readability for different magnitudes.

packages/terminal/src/charts.ts (2)

402-567: LGTM - Day activity grid is well-implemented.

The 1-minute resolution bucketing, current time indicator, and hourly cost column provide useful visualization. Edge cases like empty entries and future time dimming are handled appropriately.


230-336: LGTM - Heatmap implementation is solid.

The week-based calendar layout with configurable thresholds and legend provides good data visualization. The Monday-based week grouping logic is correct.

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

789-792: LGTM - Pre-loading pricing data before parallel processing.

This prevents race conditions where multiple concurrent file processors might trigger simultaneous network fetches. Good performance optimization.


598-606: LGTM - Concurrency-limited file timestamp scanning.

Using pLimit(20) prevents I/O saturation when scanning many files for timestamps. The approach is appropriate for this use case.


1299-1314: LGTM - Fast-path regex optimization for timestamp extraction.

The regex pre-check before full JSON parsing is a good performance optimization for filtering entries by date, especially with large files.


1218-1226: ActivityEntry type is duplicated in packages/terminal/src/charts.ts.

Both apps/ccusage/src/data-loader.ts (lines 1218-1226) and packages/terminal/src/charts.ts (lines 353-357) export this type. The data-loader version includes an optional model field while the charts version omits it. Consider consolidating to a shared location (e.g., _types.ts) to prevent divergence.

rsnodgrass and others added 2 commits January 11, 2026 19:24
Apply cyan accent color to activity blocks in day heatmap.
Block density (░▒▓█) already conveys intensity - using a single
color avoids visual noise while still distinguishing activity
from empty periods.

Changes:
- Activity blocks use cyan accent color
- Empty periods use dim gray
- Cost column uses semantic colors (green/yellow/red by relative value)
- Costs rounded to nearest dollar

Co-Authored-By: SageOx <ox@sageox.ai>
Replace semantic colors (green/yellow/red) with intensity-based
shading using the same cyan accent color:
- Low costs: dim gray
- Medium costs: regular cyan
- Peak costs: bold cyan
- Total: bold cyan (highlighted)

This creates a calmer, more cohesive visual while still showing
relative cost intensity through brightness/boldness.

Co-Authored-By: SageOx <ox@sageox.ai>
@rsnodgrass
Copy link
Author

Example output with --visual:

image

And 'start day' output:

image

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @packages/terminal/src/charts.ts:
- Around line 471-473: The check that sets isToday uses
now.toISOString().slice(0, 10) which yields a UTC date and can differ from local
date; replace that UTC-based comparison with a local-date string comparison so
targetDate (which is treated as local) is compared to the local calendar
date—e.g., generate a YYYY-MM-DD from now using local getters (getFullYear,
getMonth+1, getDate) or toLocaleDateString('en-CA') and use that in the isToday
assignment so it matches the same timezone used by currentHour/currentMinute.
- Around line 297-302: The week label is sensitive to the environment timezone
because weekDate is formatted without a timezone, causing inconsistent displays;
update the toLocaleDateString call that builds weekLabel (inside the loop over
sortedWeeks using weekKey/weekDate) to include a fixed timeZone (e.g., timeZone:
'UTC') in the options so labels are consistent, and apply the same timeZone
option to any other date formatting of weekDate in this file for uniform
behavior.
- Around line 152-155: The formatDate function uses new Date(dateStr) which
allows the runtime to interpret the date in local timezone and can shift the
displayed day for negative UTC offsets; modify formatDate to parse the date
string into year/month/day (e.g., split on '-' or use a reliable parser) and
construct the Date as UTC (e.g., new Date(Date.UTC(year, month-1, day))) so the
date represents midnight UTC before calling toLocaleDateString; update the
formatDate function to use this UTC-based Date construction (same approach used
to fix createBarChart) to avoid one-day shifts across timezones.
- Around line 77-81: The date parsing uses new Date(entry.date) which treats
"YYYY-MM-DD" as UTC midnight and can shift the displayed day in negative
timezones; update the parsing in the charts code (the const date = new
Date(entry.date) line) to force a local/noon time (e.g., append "T12:00:00" or
construct via Date.UTC/year/month/day and set hours to 12) so the resulting Date
object won’t roll back a day in client timezones before creating dateStr.
- Around line 267-275: The current loop uses new Date(entry.date), getDay(), and
setDate(), which shift by local timezones and mis-group weeks; instead parse the
ISO date parts and construct a UTC date (e.g., new Date(Date.UTC(year, month-1,
day))), use UTC methods (getUTCDay, getUTCDate, setUTCDate) to compute monday
and daysSinceMonday, and keep weekKey = monday.toISOString().slice(0,10); update
the code that creates date, dayOfWeek, monday, daysSinceMonday, and weekKey to
use the UTC-based parsing/UTC getters/setters so week grouping is stable across
timezones.
🧹 Nitpick comments (1)
packages/terminal/src/charts.ts (1)

689-746: Test data timestamps may cause timezone-dependent behavior.

The test timestamps like '2025-01-11T09:15:00Z' are UTC, but createDayActivityGrid filters entries using local time conversion. In extreme timezones (e.g., UTC-12), these entries would fall on a different local date and fail to match date: '2025-01-11'.

Consider using local ISO strings without the Z suffix or timestamps that clearly fall within the target date across all timezones.

💡 Example adjustment for timezone resilience
 	const sampleEntries: ActivityEntry[] = [
-		{ timestamp: '2025-01-11T09:15:00Z', cost: 10, outputTokens: 1000 },
-		{ timestamp: '2025-01-11T09:20:00Z', cost: 20, outputTokens: 2000 },
-		{ timestamp: '2025-01-11T14:30:00Z', cost: 50, outputTokens: 5000 },
+		{ timestamp: '2025-01-11T12:15:00', cost: 10, outputTokens: 1000 },
+		{ timestamp: '2025-01-11T12:20:00', cost: 20, outputTokens: 2000 },
+		{ timestamp: '2025-01-11T14:30:00', cost: 50, outputTokens: 5000 },
 	];
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8081599 and 3756b7e.

📒 Files selected for processing (1)
  • packages/terminal/src/charts.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Import conventions: Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Use ESLint for linting and formatting with tab indentation and double quotes
No console.log allowed except where explicitly disabled with eslint-disable
Use camelCase for variable names (e.g., usageDataSchema, modelBreakdownSchema)
Use PascalCase for type names (e.g., UsageData, ModelBreakdown)
Use UPPER_SNAKE_CASE for constants (e.g., DEFAULT_CLAUDE_CODE_PATH)
Only export constants, functions, and types that are actually used by other modules - do not export internal/private constants used only within the same file
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.isFailure() for checking errors (more readable than !Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators with Result types
NEVER use await import() dynamic imports anywhere in the codebase - this causes tree-shaking issues
Do not use console.log - use logger.ts instead
Vitest globals (describe, it, expect) are available automatically without imports in test blocks
NEVER use await import() dynamic imports in vitest test blocks - this is particularly problematic for test execution
All test files must use current Claude 4 models (claude-sonnet-4-20250514, claude-opus-4-20250514), not outdated Claude 3 models
Model names in tests must exactly match LiteLLM's pricing database entries

Files:

  • packages/terminal/src/charts.ts
🧠 Learnings (2)
📚 Learning: 2026-01-09T11:07:17.610Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.610Z
Learning: Applies to apps/opencode/**/*.{ts,tsx,js,jsx} : Reuse shared packages (`ccusage/terminal`, `ccusage/internal`) wherever possible

Applied to files:

  • packages/terminal/src/charts.ts
📚 Learning: 2026-01-09T12:35:27.240Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.240Z
Learning: Reuse shared packages ccusage/terminal and ccusage/internal wherever possible in Amp CLI

Applied to files:

  • packages/terminal/src/charts.ts
🔇 Additional comments (4)
packages/terminal/src/charts.ts (4)

1-32: LGTM!

Imports use .ts extensions as per coding guidelines, and type definitions follow proper naming conventions with clear JSDoc documentation.


339-372: LGTM!

Type definitions and the isValidVisualMode validator are clean and follow conventions.


411-468: Well-structured activity grid implementation.

The minute-bucket approach with local time handling is solid. Good defensive coding with nullish coalescing on array access and the Math.max(...buckets, 1) to avoid division by zero.


616-687: LGTM!

Tests are well-structured with good coverage of edge cases (empty data, different metrics, validation). The assertions check for expected output content appropriately.

rsnodgrass and others added 2 commits January 11, 2026 19:31
- config-schema.json: change visual field from string enum to boolean
  to match CLI argument definition in _shared-args.ts
- data-loader.ts: remove unnecessary dynamic import of stat, use
  top-level import instead
- charts.ts: add parseLocalDate helper to avoid timezone issues when
  parsing YYYY-MM-DD dates (treats as local timezone, not UTC)
- sparkline.ts: fix expansion logic bug that failed when targetWidth
  is an exact multiple of sourceLength

Co-Authored-By: SageOx <ox@sageox.ai>
- formatDate: use parseLocalDate helper to avoid UTC interpretation
- createHeatmap week grouping: use Date.UTC() and getUTCDay/setUTCDate
  for consistent week boundaries across timezones
- createHeatmap week labels: add timeZone: 'UTC' to toLocaleDateString
  for consistent display
- createDayHeatmap isToday: use local date string comparison instead of
  UTC-based toISOString to match local currentHour/currentMinute

Co-Authored-By: SageOx <ox@sageox.ai>
@rsnodgrass
Copy link
Author

@ryoppippi this is ready for review/merge. Thanks for building ccusage!

Extract canonical ActivityEntry type definition to @ccusage/terminal/charts
with optional model field to satisfy both chart rendering and data loading
use cases. Remove duplicate type in data-loader.ts (re-export instead) and
eliminate ad-hoc aliasing/conversion in day.ts.

Co-Authored-By: SageOx <ox@sageox.ai>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
apps/ccusage/src/data-loader.ts (1)

1734-1735: Remove redundant dynamic imports of fs-fixture in test blocks.

The createFixture function is already imported statically at line 24, making the dynamic await import('fs-fixture') at line 1735 (and another instance elsewhere in the file) redundant. Dynamic imports violate the coding guideline "NEVER use await import() dynamic imports anywhere (especially in tests)" and cause tree-shaking issues. Delete the dynamic import lines in both describe blocks and use the statically imported createFixture directly.

🧹 Nitpick comments (3)
apps/ccusage/src/commands/day.ts (1)

15-22: Consider validating actual date values, not just format.

The regex /^\d{8}$/ accepts any 8 digits (e.g., 20241399), which would create an invalid date string 2024-13-99. While downstream code may handle this gracefully, early validation would provide better error messages.

🔧 Optional: Add date validation
 const dateArgSchema = v.pipe(
 	v.string(),
 	v.regex(/^\d{8}$/, 'Date must be in YYYYMMDD format'),
 	v.transform((val) => {
 		// convert YYYYMMDD to YYYY-MM-DD
-		return `${val.slice(0, 4)}-${val.slice(4, 6)}-${val.slice(6, 8)}`;
+		const formatted = `${val.slice(0, 4)}-${val.slice(4, 6)}-${val.slice(6, 8)}`;
+		const date = new Date(formatted);
+		if (Number.isNaN(date.getTime())) {
+			throw new Error(`Invalid date: ${val}`);
+		}
+		return formatted;
 	}),
 );
packages/terminal/src/charts.ts (1)

177-190: Edge case: marker positioning may be off when sparkline is resized.

The marker position is calculated as Math.floor((position / data.length) * sparklineWidth), but when sparklineWidth < data.length (sparkline is compressed), the marker position may not align with the actual peak in the rendered sparkline since resizeSparkline uses sampling.

This is a minor visual issue that may cause the ^ and v markers to point to nearby but not exactly correct positions when the sparkline is significantly compressed.

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

1326-1333: Type assertion pattern for internal fields.

Using as ActivityEntry & { _messageId?: string; _requestId?: string } is a workaround for carrying internal deduplication data through the collection phase. While functional, consider using a dedicated internal type for the collection phase to avoid runtime type assertions.

🔧 Optional: Use explicit internal type
type CollectedEntry = ActivityEntry & { _messageId?: string; _requestId?: string };
// Then use CollectedEntry in the collection phase and clean up before returning
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3756b7e and 9e09154.

📒 Files selected for processing (5)
  • apps/ccusage/config-schema.json
  • apps/ccusage/src/commands/day.ts
  • apps/ccusage/src/data-loader.ts
  • packages/terminal/src/charts.ts
  • packages/terminal/src/sparkline.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/ccusage/config-schema.json
🧰 Additional context used
📓 Path-based instructions (3)
apps/ccusage/src/**/*.ts

📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)

apps/ccusage/src/**/*.ts: Write tests in-source using if (import.meta.vitest != null) blocks instead of separate test files
Use Vitest globals (describe, it, expect) without imports in test blocks
In tests, use current Claude 4 models (sonnet-4, opus-4)
Use fs-fixture with createFixture() to simulate Claude data in tests
Only export symbols that are actually used by other modules
Do not use console.log; use the logger utilities from src/logger.ts instead

Files:

  • apps/ccusage/src/commands/day.ts
  • apps/ccusage/src/data-loader.ts
apps/ccusage/**/*.ts

📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)

apps/ccusage/**/*.ts: NEVER use await import() dynamic imports anywhere (especially in tests)
Prefer @praha/byethrow Result type for error handling instead of try-catch
Use .ts extensions for local imports (e.g., import { foo } from './utils.ts')

Files:

  • apps/ccusage/src/commands/day.ts
  • apps/ccusage/src/data-loader.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Import conventions: Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Use ESLint for linting and formatting with tab indentation and double quotes
No console.log allowed except where explicitly disabled with eslint-disable
Use camelCase for variable names (e.g., usageDataSchema, modelBreakdownSchema)
Use PascalCase for type names (e.g., UsageData, ModelBreakdown)
Use UPPER_SNAKE_CASE for constants (e.g., DEFAULT_CLAUDE_CODE_PATH)
Only export constants, functions, and types that are actually used by other modules - do not export internal/private constants used only within the same file
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.isFailure() for checking errors (more readable than !Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators with Result types
NEVER use await import() dynamic imports anywhere in the codebase - this causes tree-shaking issues
Do not use console.log - use logger.ts instead
Vitest globals (describe, it, expect) are available automatically without imports in test blocks
NEVER use await import() dynamic imports in vitest test blocks - this is particularly problematic for test execution
All test files must use current Claude 4 models (claude-sonnet-4-20250514, claude-opus-4-20250514), not outdated Claude 3 models
Model names in tests must exactly match LiteLLM's pricing database entries

Files:

  • apps/ccusage/src/commands/day.ts
  • packages/terminal/src/charts.ts
  • packages/terminal/src/sparkline.ts
  • apps/ccusage/src/data-loader.ts
🧠 Learnings (15)
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: The data loading architecture follows a clear separation of concerns: Data Loading (data-loader.ts), Token Aggregation (calculate-cost.ts), Command Execution (commands/), and CLI Entry (index.ts)

Applied to files:

  • apps/ccusage/src/commands/day.ts
  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.610Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.610Z
Learning: Applies to apps/opencode/**/*.{ts,tsx,js,jsx} : Reuse shared packages (`ccusage/terminal`, `ccusage/internal`) wherever possible

Applied to files:

  • packages/terminal/src/charts.ts
📚 Learning: 2026-01-09T12:35:27.240Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.240Z
Learning: Reuse shared packages ccusage/terminal and ccusage/internal wherever possible in Amp CLI

Applied to files:

  • packages/terminal/src/charts.ts
📚 Learning: 2026-01-09T11:07:17.610Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.610Z
Learning: Applies to apps/opencode/**/*.{ts,tsx,js,jsx} : Calculate costs using model pricing data when pre-calculated `cost` field is not present in OpenCode messages

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: Applies to **/*.ts : NEVER use `await import()` dynamic imports anywhere in the codebase - this causes tree-shaking issues

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:07:00.515Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.515Z
Learning: Applies to apps/pi/src/**/*.{ts,tsx} : NEVER use `await import()` dynamic imports anywhere in the codebase

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:22:39.520Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.520Z
Learning: Applies to **/*.ts : NEVER use `await import()` dynamic imports in vitest test blocks - this is particularly problematic for test execution

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/src/**/*.ts : Use `fs-fixture` with `createFixture()` to simulate Claude data in tests

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:35:27.240Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.240Z
Learning: Applies to apps/amp/**/*.{js,ts,jsx,tsx} : NEVER use await import() dynamic imports anywhere, especially in test blocks in Amp CLI

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:07:00.515Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.515Z
Learning: Applies to apps/pi/**/*.test.{ts,tsx} : Use `fs-fixture` for creating mock data in tests for testing data loading functionality

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.610Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.610Z
Learning: Applies to apps/opencode/**/*.test.{ts,tsx,js,jsx} : Use `fs-fixture` with `using` statement to ensure cleanup in tests

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Fetch per-model pricing from LiteLLM model_prices_and_context_window.json via LiteLLMPricingFetcher using an offline cache scoped to Codex-prefixed models; handle aliases (e.g., gpt-5-codex → gpt-5) in CodexPricingSource

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Cost calculation per model/date: charge non-cached input, cached input (fallback to input rate if missing), and output using the specified per-million token rates

Applied to files:

  • apps/ccusage/src/data-loader.ts
🧬 Code graph analysis (3)
packages/terminal/src/charts.ts (3)
packages/terminal/src/sparkline.ts (3)
  • formatCostCompact (287-295)
  • formatTokensCompact (271-282)
  • createSparkline (51-85)
apps/ccusage/src/_date-utils.ts (1)
  • formatDate (43-48)
packages/terminal/src/colors.ts (1)
  • colors (15-49)
packages/terminal/src/sparkline.ts (1)
packages/terminal/src/colors.ts (2)
  • DeviationThresholds (97-106)
  • colors (15-49)
apps/ccusage/src/data-loader.ts (5)
apps/ccusage/src/_pricing-fetcher.ts (1)
  • PricingFetcher (73-132)
apps/ccusage/src/_date-utils.ts (1)
  • formatDate (43-48)
apps/ccusage/src/_consts.ts (1)
  • DEFAULT_LOCALE (129-129)
apps/ccusage/src/_types.ts (1)
  • CostMode (145-145)
packages/terminal/src/charts.ts (1)
  • ActivityEntry (368-373)
🔇 Additional comments (26)
apps/ccusage/src/commands/day.ts (5)

1-11: LGTM!

Imports follow coding guidelines: .ts extensions for local imports, uses @praha/byethrow for Result handling, and imports logger utilities instead of using console.log.


31-38: LGTM!

Clean helper function with clear documentation and appropriate regex for stripping date suffixes from model names.


40-58: LGTM!

Command definition is well-structured with clear descriptions and appropriate defaults.


104-114: Good use of Result type for error handling.

The jq processing correctly uses Result.isFailure() check and handles errors appropriately. This follows the coding guidelines for @praha/byethrow usage.


129-136: LGTM!

Good filtering of synthetic models and application of the shortenModelName helper for cleaner display output.

packages/terminal/src/sparkline.ts (8)

1-23: LGTM!

Clean imports with .ts extension and well-documented SPARK_CHARS constant with appropriate Unicode block characters.


51-85: LGTM!

The sparkline generation logic correctly handles edge cases (empty array, single value, uniform values) and properly maps normalized values to character indices.


90-115: LGTM!

The resize logic correctly handles both expansion (with even distribution) and shrinking (with sampling). Since SPARK_CHARS are single Unicode code points, the spread operator works correctly.


139-169: LGTM!

Well-structured function with proper empty data handling and clean formatting.


189-207: Verify timezone behavior is intentional.

getHours() returns the local timezone hour, which means the 2-hour buckets will be based on the user's local timezone. This seems intentional for an "intraday" view, but verify this aligns with how timestamps are stored and displayed elsewhere in the codebase.


226-266: LGTM!

Clean implementation for multi-row sparkline summaries with appropriate minimum width handling and consistent formatting.


271-295: LGTM!

Compact formatting functions have sensible thresholds and consistent output formats for different magnitudes.


297-418: LGTM!

Comprehensive in-source tests following coding guidelines with proper use of vitest globals and good edge case coverage.

packages/terminal/src/charts.ts (6)

1-18: LGTM!

Clean imports and well-documented parseLocalDate helper that correctly handles the UTC interpretation issue when parsing YYYY-MM-DD date strings.


53-98: LGTM!

Well-implemented bar chart with proper metric selection, formatting, and date display.


274-293: Good UTC-based week grouping.

The heatmap correctly uses UTC methods (Date.UTC, getUTCDay, getUTCDate, setUTCDate) for consistent week boundaries across timezones, avoiding date shift issues.


467-483: Intensity thresholds are well-calibrated.

The 5-level intensity mapping (0%, ≤20%, ≤40%, ≤70%, >70%) provides good visual differentiation without excessive granularity.


534-549: Clear visual distinction for time states.

Good separation of current time marker (yellow ▼), future minutes (dim dot), no activity (dim), and activity levels (cyan with density).


706-763: Good test coverage for createDayActivityGrid.

Tests cover key scenarios: 24-row grid with 1-minute resolution, legend content, summary stats, current time indicator for "today", and empty entries handling.

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

11-11: LGTM!

New imports for ActivityEntry type and pLimit for concurrency control are appropriate for the added functionality.

Also applies to: 25-25


598-608: LGTM!

Good use of pLimit(20) to prevent I/O saturation when scanning many files for timestamps.


790-793: Good: Pre-loading pricing data before parallel processing.

This prevents race conditions where multiple parallel workers might try to fetch pricing data simultaneously.


852-869: Deduplication logic is correct.

The post-collection deduplication correctly handles entries without hash keys (empty string) by allowing them through, and properly tracks processed hashes for entries that have valid keys.


1246-1248: Potential timezone issue in targetDateTime calculation.

new Date(targetDate).getTime() where targetDate is YYYY-MM-DD will be interpreted as UTC midnight, which may cause edge cases when filtering files by modification time in local timezone.

Consider using the same local date parsing approach as elsewhere:

const [year, month, day] = targetDate.split('-').map(Number);
const targetDateTime = new Date(year, month - 1, day).getTime();

741-741: LGTM!

refreshPricing option is properly added to LoadOptions type and consistently propagated through all data loading functions.


1219-1223: LGTM!

Clean re-export of ActivityEntry type with documentation for convenience.

Remove duplicate await import('fs-fixture') calls since createFixture
is already imported statically at file top. Dynamic imports in tests
violate coding guidelines and cause tree-shaking issues.

Co-Authored-By: SageOx <ox@sageox.ai>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/ccusage/src/data-loader.ts (2)

1326-1333: Consider using a separate internal type for intermediate processing.

The type assertion to ActivityEntry & { _messageId?: string; _requestId?: string } is a workaround to carry deduplication keys through processing. While functional, a cleaner approach would use an explicit internal type.

♻️ Optional refactor for type clarity
+// Internal type for processing with dedup keys
+type ActivityEntryWithDedup = ActivityEntry & {
+	_messageId?: string;
+	_requestId?: string;
+};
+
 // In the processing loop:
-						entries.push({
-							timestamp: data.timestamp,
-							cost,
-							outputTokens: data.message.usage.output_tokens,
-							model: data.message.model,
-							_messageId: data.message.id,
-							_requestId: data.requestId,
-						} as ActivityEntry & { _messageId?: string; _requestId?: string });
+						entries.push({
+							timestamp: data.timestamp,
+							cost,
+							outputTokens: data.message.usage.output_tokens,
+							model: data.message.model,
+							_messageId: data.message.id,
+							_requestId: data.requestId,
+						} satisfies ActivityEntryWithDedup);

1347-1356: Consider consistent sentinel value pattern across functions.

The deduplication here uses ':' as the "no dedup key" sentinel (when both _messageId and _requestId are empty), while loadDailyUsageData uses ''. While both work correctly, a consistent pattern (e.g., always using createUniqueHash or a shared constant) would improve maintainability.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e09154 and 2f8b386.

📒 Files selected for processing (1)
  • apps/ccusage/src/data-loader.ts
🧰 Additional context used
📓 Path-based instructions (3)
apps/ccusage/src/**/*.ts

📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)

apps/ccusage/src/**/*.ts: Write tests in-source using if (import.meta.vitest != null) blocks instead of separate test files
Use Vitest globals (describe, it, expect) without imports in test blocks
In tests, use current Claude 4 models (sonnet-4, opus-4)
Use fs-fixture with createFixture() to simulate Claude data in tests
Only export symbols that are actually used by other modules
Do not use console.log; use the logger utilities from src/logger.ts instead

Files:

  • apps/ccusage/src/data-loader.ts
apps/ccusage/**/*.ts

📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)

apps/ccusage/**/*.ts: NEVER use await import() dynamic imports anywhere (especially in tests)
Prefer @praha/byethrow Result type for error handling instead of try-catch
Use .ts extensions for local imports (e.g., import { foo } from './utils.ts')

Files:

  • apps/ccusage/src/data-loader.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Import conventions: Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Use ESLint for linting and formatting with tab indentation and double quotes
No console.log allowed except where explicitly disabled with eslint-disable
Use camelCase for variable names (e.g., usageDataSchema, modelBreakdownSchema)
Use PascalCase for type names (e.g., UsageData, ModelBreakdown)
Use UPPER_SNAKE_CASE for constants (e.g., DEFAULT_CLAUDE_CODE_PATH)
Only export constants, functions, and types that are actually used by other modules - do not export internal/private constants used only within the same file
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.isFailure() for checking errors (more readable than !Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators with Result types
NEVER use await import() dynamic imports anywhere in the codebase - this causes tree-shaking issues
Do not use console.log - use logger.ts instead
Vitest globals (describe, it, expect) are available automatically without imports in test blocks
NEVER use await import() dynamic imports in vitest test blocks - this is particularly problematic for test execution
All test files must use current Claude 4 models (claude-sonnet-4-20250514, claude-opus-4-20250514), not outdated Claude 3 models
Model names in tests must exactly match LiteLLM's pricing database entries

Files:

  • apps/ccusage/src/data-loader.ts
🧠 Learnings (20)
📚 Learning: 2026-01-09T12:22:39.548Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.548Z
Learning: The data loading architecture follows a clear separation of concerns: Data Loading (data-loader.ts), Token Aggregation (calculate-cost.ts), Command Execution (commands/), and CLI Entry (index.ts)

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.625Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.625Z
Learning: Applies to apps/opencode/**/*.{ts,tsx,js,jsx} : Calculate costs using model pricing data when pre-calculated `cost` field is not present in OpenCode messages

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:22:39.548Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.548Z
Learning: Applies to **/*.ts : NEVER use `await import()` dynamic imports anywhere in the codebase - this causes tree-shaking issues

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:07:00.527Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.527Z
Learning: Applies to apps/pi/src/**/*.{ts,tsx} : NEVER use `await import()` dynamic imports anywhere in the codebase

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:22:39.548Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.548Z
Learning: Applies to **/*.ts : NEVER use `await import()` dynamic imports in vitest test blocks - this is particularly problematic for test execution

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/src/**/*.ts : Use `fs-fixture` with `createFixture()` to simulate Claude data in tests

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:35:27.250Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.250Z
Learning: Applies to apps/amp/**/*.{js,ts,jsx,tsx} : NEVER use await import() dynamic imports anywhere, especially in test blocks in Amp CLI

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:07:00.527Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.527Z
Learning: Applies to apps/pi/**/*.test.{ts,tsx} : Use `fs-fixture` for creating mock data in tests for testing data loading functionality

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.625Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.625Z
Learning: Applies to apps/opencode/**/*.test.{ts,tsx,js,jsx} : Use `fs-fixture` with `using` statement to ensure cleanup in tests

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Fetch per-model pricing from LiteLLM model_prices_and_context_window.json via LiteLLMPricingFetcher using an offline cache scoped to Codex-prefixed models; handle aliases (e.g., gpt-5-codex → gpt-5) in CodexPricingSource

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Cost calculation per model/date: charge non-cached input, cached input (fallback to input rate if missing), and output using the specified per-million token rates

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:07:16.293Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/codex/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:07:16.293Z
Learning: Tests should use fs-fixture with using to ensure cleanup

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:35:27.250Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/amp/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:35:27.250Z
Learning: Use fs-fixture with using statement for test cleanup

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T16:06:37.474Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/mcp/CLAUDE.md:0-0
Timestamp: 2025-09-17T18:29:15.764Z
Learning: Applies to apps/mcp/**/*.{test,spec}.ts : Use `fs-fixture` for mock data in tests of MCP server functionality

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T11:07:17.625Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/opencode/CLAUDE.md:0-0
Timestamp: 2026-01-09T11:07:17.625Z
Learning: Applies to apps/opencode/**/*.test.{ts,tsx,js,jsx} : Vitest globals are enabled - use `describe`, `it`, `expect` directly without imports

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2026-01-09T12:07:00.527Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.527Z
Learning: Applies to apps/pi/**/*.test.{ts,tsx} : Use vitest globals (`describe`, `it`, `expect`) directly without imports in test files

Applied to files:

  • apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-17T18:29:15.764Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 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/ccusage/src/data-loader.ts
🧬 Code graph analysis (1)
apps/ccusage/src/data-loader.ts (5)
apps/ccusage/src/_pricing-fetcher.ts (1)
  • PricingFetcher (73-132)
apps/ccusage/src/_date-utils.ts (1)
  • formatDate (43-48)
apps/ccusage/src/_consts.ts (1)
  • DEFAULT_LOCALE (129-129)
apps/ccusage/src/_types.ts (1)
  • CostMode (145-145)
packages/terminal/src/charts.ts (1)
  • ActivityEntry (368-373)
🔇 Additional comments (9)
apps/ccusage/src/data-loader.ts (9)

599-607: LGTM - Concurrency control for file timestamp scanning.

The pLimit(20) appropriately bounds concurrent I/O when scanning many files for timestamps, preventing file descriptor exhaustion on large data directories.


790-793: Good defensive pattern for pricing data pre-loading.

Pre-fetching pricing data before parallel file processing prevents potential race conditions where multiple concurrent file processors might trigger simultaneous network requests or cache writes. This ensures pricing is loaded once and cached.


1294-1310: Good performance optimization with regex-based date filtering.

The regex fast-path avoids full JSON parsing for lines that don't match the target date. This is a reasonable optimization given the controlled JSONL format from Claude's data files.


1246-1248: Verify timezone handling for date filtering.

The options?.timezone parameter is accepted but not used when filtering entries by date. The regex comparison at line 1308 extracts the UTC date from timestamps and compares directly with targetDate. If a user provides a timezone, entries near midnight UTC might be incorrectly included or excluded.

For example, with timezone: 'America/New_York' and date: '2024-01-15', an entry with timestamp 2024-01-15T03:00:00Z (which is 2024-01-14 22:00 EST) would be included even though it's the previous day in the specified timezone.

Consider whether the timezone parameter should affect date filtering, or document that filtering uses UTC dates.

Also applies to: 1306-1310


1455-1459: LGTM - Consistent refreshPricing propagation.

The refreshPricing parameter is properly propagated to PricingFetcher, maintaining consistency with other data loading functions.


1734-1813: LGTM - Tests follow coding guidelines.

The tests correctly use:

  • Vitest globals without imports
  • fs-fixture with await using for cleanup
  • Current Claude 4 models (claude-sonnet-4-20250514)
  • No dynamic imports

1255-1275: LGTM - Efficient file pre-filtering with safe fallback.

The mtime-based pre-filtering with a 2-day window is a reasonable optimization. Even if mtime is unreliable (e.g., from backup restoration), the worst case is reading extra files—the actual date filtering via regex ensures correctness.


977-980: LGTM - Consistent refreshPricing pattern.

The loadSessionData function follows the same pattern as other data loading functions for pricing fetcher initialization with the new refreshPricing option.


1219-1223: Remove unused ActivityEntry re-export.

The ActivityEntry type is re-exported from data-loader.ts for convenience, but no other modules import it from this file. Per coding guidelines, only export symbols actually used by other modules. Remove the re-export at lines 1219-1223.

⛔ Skipped due to learnings
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/ccusage/CLAUDE.md:0-0
Timestamp: 2025-09-18T16:06:37.474Z
Learning: Applies to apps/ccusage/src/**/*.ts : Only export symbols that are actually used by other modules
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/pi/CLAUDE.md:0-0
Timestamp: 2026-01-09T12:07:00.527Z
Learning: Applies to apps/pi/src/**/*.{ts,tsx} : Only export what's actually used from modules
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-09T12:22:39.548Z
Learning: Applies to **/*.ts : Only export constants, functions, and types that are actually used by other modules - do not export internal/private constants used only within the same file
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: apps/mcp/CLAUDE.md:0-0
Timestamp: 2025-09-17T18:29:15.764Z
Learning: Applies to apps/mcp/**/*.ts : Only export what is actually used

The loadDayActivityData function used a regex to extract the date from
ISO timestamps, comparing the UTC date against the local target date.
This caused events from late local time (after 4PM Pacific, etc.) to be
filtered out because their UTC timestamps have the next day's date.

Changes:
- Expand fast-path filter to accept ±1 day in UTC time
- Add local-timezone verification after JSON parsing
- Add tests for timezone edge cases

This ensures the heatmap shows all usage data regardless of timezone,
matching the behavior of other daily reports.
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