-
-
Notifications
You must be signed in to change notification settings - Fork 332
feat(ccusage): add Tufte-style visual mode for daily reports #799
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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>
📝 WalkthroughWalkthroughReplaces 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
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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~90 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧹 Recent nitpick comments
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used📓 Path-based instructions (3)apps/ccusage/src/**/*.ts📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)
Files:
apps/ccusage/**/*.ts📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)
Files:
**/*.ts📄 CodeRabbit inference engine (CLAUDE.md)
Files:
🧠 Learnings (20)📚 Learning: 2026-01-09T12:22:39.548ZApplied to files:
📚 Learning: 2026-01-09T11:07:17.625ZApplied to files:
📚 Learning: 2025-09-18T16:06:37.474ZApplied to files:
📚 Learning: 2026-01-09T12:22:39.548ZApplied to files:
📚 Learning: 2026-01-09T12:07:00.527ZApplied to files:
📚 Learning: 2026-01-09T12:22:39.548ZApplied to files:
📚 Learning: 2025-09-18T16:06:37.474ZApplied to files:
📚 Learning: 2025-09-17T18:29:15.764ZApplied to files:
📚 Learning: 2026-01-09T12:35:27.250ZApplied to files:
📚 Learning: 2026-01-09T12:07:00.527ZApplied to files:
📚 Learning: 2026-01-09T11:07:17.625ZApplied to files:
📚 Learning: 2025-09-18T16:07:16.293ZApplied to files:
📚 Learning: 2025-09-18T16:07:16.293ZApplied to files:
📚 Learning: 2025-09-18T16:07:16.293ZApplied to files:
📚 Learning: 2026-01-09T12:35:27.250ZApplied to files:
📚 Learning: 2025-09-18T16:06:37.474ZApplied to files:
📚 Learning: 2025-09-17T18:29:15.764ZApplied to files:
📚 Learning: 2026-01-09T11:07:17.625ZApplied to files:
📚 Learning: 2026-01-09T12:07:00.527ZApplied to files:
📚 Learning: 2025-09-17T18:29:15.764ZApplied to files:
🧬 Code graph analysis (1)apps/ccusage/src/data-loader.ts (5)
🔇 Additional comments (8)
✏️ Tip: You can disable this entire section by setting Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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 extractingshortenModelNameto the shared terminal package.This function duplicates logic from
@ccusage/terminal/colorswhich already exportsshortenModelName. 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 usingResult.try()for JSON parsing.The
JSON.parsecall can throw on malformed cache data. Per coding guidelines, preferResult.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
cachedPricingproperty 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
createModelIdentifierandcreateModelLegendEntry. 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 extendingActivityEntrywith internal fields via type assertion.The pattern of adding
_messageIdand_requestIdtoActivityEntryvia 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
📒 Files selected for processing (12)
apps/ccusage/config-schema.jsonapps/ccusage/src/_pricing-fetcher.tsapps/ccusage/src/_shared-args.tsapps/ccusage/src/commands/daily.tsapps/ccusage/src/commands/day.tsapps/ccusage/src/commands/index.tsapps/ccusage/src/data-loader.tspackage.jsonpackages/terminal/package.jsonpackages/terminal/src/charts.tspackages/terminal/src/colors.tspackages/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 usingif (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)
Usefs-fixturewithcreateFixture()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 fromsrc/logger.tsinstead
Files:
apps/ccusage/src/commands/daily.tsapps/ccusage/src/commands/index.tsapps/ccusage/src/commands/day.tsapps/ccusage/src/data-loader.tsapps/ccusage/src/_pricing-fetcher.tsapps/ccusage/src/_shared-args.ts
apps/ccusage/**/*.ts
📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)
apps/ccusage/**/*.ts: NEVER useawait import()dynamic imports anywhere (especially in tests)
Prefer@praha/byethrowResult type for error handling instead of try-catch
Use.tsextensions for local imports (e.g.,import { foo } from './utils.ts')
Files:
apps/ccusage/src/commands/daily.tsapps/ccusage/src/commands/index.tsapps/ccusage/src/commands/day.tsapps/ccusage/src/data-loader.tsapps/ccusage/src/_pricing-fetcher.tsapps/ccusage/src/_shared-args.ts
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Import conventions: Use.tsextensions 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
UseResult.try()for wrapping operations that may throw (JSON parsing, etc.)
UseResult.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 useawait 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 useawait 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.tsapps/ccusage/src/commands/index.tsapps/ccusage/src/commands/day.tsapps/ccusage/src/data-loader.tsapps/ccusage/src/_pricing-fetcher.tspackages/terminal/src/charts.tspackages/terminal/src/colors.tspackages/terminal/src/sparkline.tsapps/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.tsapps/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.tsapps/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.jsonapps/ccusage/src/commands/index.tspackages/terminal/src/colors.tsapps/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.jsonapps/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.jsonpackage.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.tsapps/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.tsapps/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.tsapps/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.0to>=24.11.0allows developers to use newer Node versions. Since this is adevEnginesconstraint 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./chartsfollow 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
.tsextension as required by the coding guidelines.
16-16: LGTM - Day command properly integrated into CLI.The
dayCommandis correctly exported and added tosubCommandUnion, 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
.tsextensions as required. The file uses@praha/byethrowResult type andloggerfrom 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
--jqimplying--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 vialogger.error()instead ofconsole.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
.tsextensions 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/gis 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 === truemaintains backward compatibility with the deprecated--compactflag while enabling the new--visualmode. The early return prevents duplicate output.apps/ccusage/src/_shared-args.ts (1)
77-82: LGTM - NewrefreshPricingflag for cache control.The
-Rshort 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 forcompactis clear.The deprecation message properly directs users to use
--visual compactinstead.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
diskCacheLoadedflag 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 constassertion 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:ActivityEntrytype is duplicated inpackages/terminal/src/charts.ts.Both
apps/ccusage/src/data-loader.ts(lines 1218-1226) andpackages/terminal/src/charts.ts(lines 353-357) export this type. The data-loader version includes an optionalmodelfield while the charts version omits it. Consider consolidating to a shared location (e.g.,_types.ts) to prevent divergence.
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>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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, butcreateDayActivityGridfilters entries using local time conversion. In extreme timezones (e.g., UTC-12), these entries would fall on a different local date and fail to matchdate: '2025-01-11'.Consider using local ISO strings without the
Zsuffix 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
📒 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.tsextensions 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
UseResult.try()for wrapping operations that may throw (JSON parsing, etc.)
UseResult.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 useawait 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 useawait 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
.tsextensions as per coding guidelines, and type definitions follow proper naming conventions with clear JSDoc documentation.
339-372: LGTM!Type definitions and the
isValidVisualModevalidator 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.
- 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>
|
@ryoppippi this is ready for review/merge. Thanks for building |
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>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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 offs-fixturein test blocks.The
createFixturefunction is already imported statically at line 24, making the dynamicawait import('fs-fixture')at line 1735 (and another instance elsewhere in the file) redundant. Dynamic imports violate the coding guideline "NEVER useawait 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 importedcreateFixturedirectly.
🧹 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 string2024-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 whensparklineWidth < data.length(sparkline is compressed), the marker position may not align with the actual peak in the rendered sparkline sinceresizeSparklineuses sampling.This is a minor visual issue that may cause the
^andvmarkers 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
📒 Files selected for processing (5)
apps/ccusage/config-schema.jsonapps/ccusage/src/commands/day.tsapps/ccusage/src/data-loader.tspackages/terminal/src/charts.tspackages/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 usingif (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)
Usefs-fixturewithcreateFixture()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 fromsrc/logger.tsinstead
Files:
apps/ccusage/src/commands/day.tsapps/ccusage/src/data-loader.ts
apps/ccusage/**/*.ts
📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)
apps/ccusage/**/*.ts: NEVER useawait import()dynamic imports anywhere (especially in tests)
Prefer@praha/byethrowResult type for error handling instead of try-catch
Use.tsextensions for local imports (e.g.,import { foo } from './utils.ts')
Files:
apps/ccusage/src/commands/day.tsapps/ccusage/src/data-loader.ts
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Import conventions: Use.tsextensions 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
UseResult.try()for wrapping operations that may throw (JSON parsing, etc.)
UseResult.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 useawait 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 useawait 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.tspackages/terminal/src/charts.tspackages/terminal/src/sparkline.tsapps/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.tsapps/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:
.tsextensions for local imports, uses@praha/byethrowfor 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/byethrowusage.
129-136: LGTM!Good filtering of synthetic models and application of the
shortenModelNamehelper for cleaner display output.packages/terminal/src/sparkline.ts (8)
1-23: LGTM!Clean imports with
.tsextension 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
parseLocalDatehelper 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
ActivityEntrytype andpLimitfor 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()wheretargetDateisYYYY-MM-DDwill 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!
refreshPricingoption is properly added toLoadOptionstype and consistently propagated through all data loading functions.
1219-1223: LGTM!Clean re-export of
ActivityEntrytype 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>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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_messageIdand_requestIdare empty), whileloadDailyUsageDatauses''. While both work correctly, a consistent pattern (e.g., always usingcreateUniqueHashor a shared constant) would improve maintainability.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 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 usingif (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)
Usefs-fixturewithcreateFixture()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 fromsrc/logger.tsinstead
Files:
apps/ccusage/src/data-loader.ts
apps/ccusage/**/*.ts
📄 CodeRabbit inference engine (apps/ccusage/CLAUDE.md)
apps/ccusage/**/*.ts: NEVER useawait import()dynamic imports anywhere (especially in tests)
Prefer@praha/byethrowResult type for error handling instead of try-catch
Use.tsextensions 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.tsextensions 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
UseResult.try()for wrapping operations that may throw (JSON parsing, etc.)
UseResult.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 useawait 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 useawait 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?.timezoneparameter 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 withtargetDate. If a user provides a timezone, entries near midnight UTC might be incorrectly included or excluded.For example, with
timezone: 'America/New_York'anddate: '2024-01-15', an entry with timestamp2024-01-15T03:00:00Z(which is2024-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
refreshPricingparameter is properly propagated toPricingFetcher, maintaining consistency with other data loading functions.
1734-1813: LGTM - Tests follow coding guidelines.The tests correctly use:
- Vitest globals without imports
fs-fixturewithawait usingfor 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
loadSessionDatafunction follows the same pattern as other data loading functions for pricing fetcher initialization with the newrefreshPricingoption.
1219-1223: Remove unused ActivityEntry re-export.The
ActivityEntrytype is re-exported fromdata-loader.tsfor 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 modulesLearnt 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 modulesLearnt 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 fileLearnt 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.


Summary
--visual/-Vflag for enhanced Tufte-style terminal visualizationsFeatures
Visual Mode (
-V):New Terminal Utilities:
colors.ts: semantic color token system with model color palettesparkline.ts: 8-level Unicode block sparklines, compact formatterscharts.ts: heatmap generator with weekly calendar gridUX Refinements:
Test plan
pnpm run formatpassespnpm typecheckpassespnpm run testpasses (all 391 tests)pnpm run start daily -V --since 20260101🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes / Reliability
Chores
✏️ Tip: You can customize this high-level summary in your review settings.