feat(ccusage): add hourly usage report#724
Conversation
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
apps/ccusage/src/data-loader.ts (1)
1148-1165: Avoid relying on non‑ISO date parsing when sorting hourly buckets
formatHourKeygenerates strings like"YYYY-MM-DD HH:00"andsortByDatelater parses these vianew Date(getDate(item)). That format isn’t ISO‑8601, so parsing is implementation‑dependent and could break on non‑Node runtimes or future engines.Since the hour strings are already lexicographically sortable, you can avoid parsing entirely:
- // Sort ascending by hour for a chronological 24h view - return sortByDate(results, item => item.hour, 'asc'); + // Sort ascending by hour for a chronological 24h view. + // Hour strings are in "YYYY-MM-DD HH:00" format, so lexicographic order matches chronological order. + return results.sort((a, b) => a.hour.localeCompare(b.hour));Additionally, you recreate
Intl.DateTimeFormaton everyformatHourKeycall. For large datasets, consider hoisting it once per invocation:- // Helper to format hour key with timezone awareness - function formatHourKey(timestamp: string): string { - const d = new Date(timestamp); - // Use Intl with timezone if provided to extract local parts - const formatter = new Intl.DateTimeFormat(options?.locale ?? DEFAULT_LOCALE, { + // Helper to format hour key with timezone awareness + const hourFormatter = new Intl.DateTimeFormat(options?.locale ?? DEFAULT_LOCALE, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hour12: false, timeZone: options?.timezone, - }); + }); + + function formatHourKey(timestamp: string): string { + const d = new Date(timestamp); const parts = formatter.formatToParts(d); const year = parts.find(p => p.type === 'year')?.value ?? ''; const month = parts.find(p => p.type === 'month')?.value ?? ''; const day = parts.find(p => p.type === 'day')?.value ?? ''; const hour = parts.find(p => p.type === 'hour')?.value ?? '00'; return `${year}-${month}-${day} ${hour}:00`; }Also applies to: 1262-1263
apps/ccusage/src/commands/hourly.ts (2)
81-93: Use shared default locale constant instead of hard‑coding'en-CA'The
--todayfilter buildstodayDateusingIntl.DateTimeFormat(mergedOptions.locale ?? 'en-CA', …), while the loader and other code paths rely onDEFAULT_LOCALEfrom_consts.tsfor consistent date handling.To avoid drift if the default ever changes, consider switching to the shared constant:
- const formatter = new Intl.DateTimeFormat(mergedOptions.locale ?? 'en-CA', { + const formatter = new Intl.DateTimeFormat(mergedOptions.locale ?? DEFAULT_LOCALE, {(You’d need to import
DEFAULT_LOCALEhere for consistency withloadHourlyUsageData.)
187-203: ReuseaddEmptySeparatorRowhelper instead of manual empty rowsWhen separating project groups you currently push a literal
['', '', '', '', '', '', '', ''], while later you already useaddEmptySeparatorRow(table, 8)for the totals separator.For consistency and to avoid hard‑coding column counts in multiple places, you can reuse the helper:
- if (!isFirstProject) { - table.push(['', '', '', '', '', '', '', '']); - } + if (!isFirstProject) { + addEmptySeparatorRow(table, 8); + }This keeps separator rendering logic in one place.
Also applies to: 242-253
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📥 Commits
Reviewing files that changed from the base of the PR and between 0850376 and bd492d605fea2bf2743f62f139dce6daebe421fc.
📒 Files selected for processing (3)
apps/ccusage/src/commands/hourly.ts(1 hunks)apps/ccusage/src/commands/index.ts(1 hunks)apps/ccusage/src/data-loader.ts(2 hunks)
🧰 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/hourly.tsapps/ccusage/src/commands/index.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/hourly.tsapps/ccusage/src/commands/index.tsapps/ccusage/src/data-loader.ts
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Use tab indentation and double quotes (ESLint formatting)
Do not use console.log; only allow where explicitly disabled via eslint-disable
Always use Node.js path utilities for file paths for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() to wrap operations that may throw (e.g., JSON parsing)
Use Result.isFailure() for checking errors instead of negating isSuccess()
Use early return on failures (e.g., if (Result.isFailure(r)) continue) instead of ternary patterns
For async operations, create a wrapper using Result.try() and call it
Keep traditional try-catch only for complex file I/O or legacy code that’s hard to refactor
Always use Result.isFailure() and Result.isSuccess() type guards for clarity
Variables use camelCase naming
Types use PascalCase naming
Constants can use UPPER_SNAKE_CASE
Only export constants, functions, and types that are actually used by other modules
Do not export internal/private constants that are only used within the same file
Before exporting a constant, verify it is referenced by other modules
Use Vitest globals (describe, it, expect) without imports in test blocks
Never use await import() dynamic imports anywhere in the codebase
Never use dynamic imports inside Vitest test blocks
Use fs-fixture createFixture() for mock Claude data directories in tests
All tests must use current Claude 4 models (not Claude 3)
Test coverage should include both Sonnet and Opus models
Model names in tests must exactly match LiteLLM pricing database entries
Use logger.ts instead of console.log for logging
Files:
apps/ccusage/src/commands/hourly.tsapps/ccusage/src/commands/index.tsapps/ccusage/src/data-loader.ts
**/data-loader.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Silently skip malformed JSONL lines during parsing in data-loader.ts
Files:
apps/ccusage/src/data-loader.ts
🧠 Learnings (5)
📚 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:
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 `fs-fixture` with `createFixture()` to simulate Claude data 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 : In tests, use current Claude 4 models (sonnet-4, opus-4)
Applied to files:
apps/ccusage/src/data-loader.ts
📚 Learning: 2025-09-18T17:43:09.255Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-18T17:43:09.255Z
Learning: Applies to **/*.ts : Use fs-fixture createFixture() for mock Claude data directories 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: Tests should use fs-fixture with using to ensure cleanup
Applied to files:
apps/ccusage/src/data-loader.ts
🧬 Code graph analysis (3)
apps/ccusage/src/commands/hourly.ts (8)
apps/ccusage/src/_shared-args.ts (1)
sharedCommandConfig(116-119)apps/ccusage/src/_config-loader-tokens.ts (1)
mergeConfigWithArgs(212-298)apps/ccusage/src/data-loader.ts (1)
loadHourlyUsageData(1100-1264)apps/ccusage/src/debug.ts (2)
detectMismatches(75-218)printMismatchReport(225-309)apps/ccusage/src/calculate-cost.ts (1)
createTotalsObject(77-82)apps/ccusage/src/_jq-processor.ts (1)
processWithJq(10-36)packages/terminal/src/table.ts (6)
UsageReportConfig(415-424)createUsageReportTable(443-500)formatUsageDataRow(509-532)pushBreakdownRows(369-410)addEmptySeparatorRow(566-569)formatTotalsRow(540-559)apps/ccusage/src/_project-names.ts (1)
formatProjectName(137-152)
apps/ccusage/src/commands/index.ts (1)
apps/ccusage/src/commands/hourly.ts (1)
hourlyCommand(19-264)
apps/ccusage/src/data-loader.ts (6)
apps/codex/src/data-loader.ts (1)
LoadOptions(169-171)apps/ccusage/src/_types.ts (2)
ModelName(91-91)createISOTimestamp(113-113)apps/ccusage/src/_pricing-fetcher.ts (1)
PricingFetcher(16-25)apps/ccusage/src/_consts.ts (1)
DEFAULT_LOCALE(148-148)apps/ccusage/src/calculate-cost.ts (1)
calculateTotals(48-67)apps/ccusage/src/_date-utils.ts (1)
sortByDate(94-108)
🔇 Additional comments (4)
apps/ccusage/src/data-loader.ts (2)
1100-1212: Hourly loader ignoressince/untileven though options carry them
loadHourlyUsageDataalways enforces a “last 24 hours from now” window and never appliesoptions.since/options.until, even thoughLoadOptionsincludes those fields and the hourly command inherits the shared date-range args. This is fine if hourly is explicitly defined as “last 24h only”, but it means--since/--untilwill be silently ignored for this command.Consider either:
- Hiding/removing
since/untilfrom the hourly command’s args, or- Applying them as an additional filter on top of the 24h window, or
- Documenting clearly in help text that they are ignored for
hourly.
2774-3037: Hourly loader tests give good coverage and follow repo testing conventionsThe
loadHourlyUsageDatatests cover aggregation, 24‑hour filtering, empty data, timezone option, per‑project grouping, and hourly vs daily alignment, and they usecreateFixture()and in‑source Vitest blocks as per guidelines. The use of relative timestamps (hours ago) keeps them robust against clock drift.Looks solid as‑is.
apps/ccusage/src/commands/hourly.ts (1)
19-77: Hourly command wiring and JSON output look consistent with existing commandsThe new
hourlycommand integrates cleanly with the shared config/args pattern, handles--instances,--project,--today,--json, and--jqcoherently, and correctly reusesloadHourlyUsageData,getTotalTokens, andcreateTotalsObjectfor JSON output. Debug mismatch reporting is also gated behinddebugand non‑JSON mode.No functional issues stand out here.
Also applies to: 128-164
apps/ccusage/src/commands/index.ts (1)
6-26: Hourly subcommand is correctly registered and exportedImporting
hourlyCommand, adding it to the export list, and including['hourly', hourlyCommand]insubCommandUnionkeeps the registry andCommandNametype in sync. ThesubCommandsmap will now correctly dispatch the new subcommand.Looks good.
- Add new `hourly` command showing last 24 hours of usage - Group usage data into hourly buckets (YYYY-MM-DD HH:00) - Support timezone-aware hour formatting - Add --today flag to filter to current calendar day - Add project grouping with --instances flag - Include 6 comprehensive tests verifying functionality - Document hour-to-day alignment for data consistency - Support all standard output modes (table, JSON, compact) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
bd492d6 to
27687a9
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/ccusage/src/data-loader.ts (2)
1679-1679: Remove dynamic import in tests (repo rule: never use await import)You already import createFixture at module top; drop this line.
Apply this diff:
- const { createFixture } = await import('fs-fixture');As per coding guidelines.
5133-5133: Remove dynamic import in tests (repo rule: never use await import)Use the existing top‑level import instead.
Apply this diff:
- const { createFixture } = await import('fs-fixture');As per coding guidelines.
🧹 Nitpick comments (2)
apps/ccusage/src/data-loader.ts (1)
1148-1165: Avoid per‑call Intl.DateTimeFormat allocation in formatHourKeyCache the formatter once per invocation; large logs benefit materially.
Apply this diff:
- // Helper to format hour key with timezone awareness - function formatHourKey(timestamp: string): string { - const d = new Date(timestamp); - // Use Intl with timezone if provided to extract local parts - const formatter = new Intl.DateTimeFormat(options?.locale ?? DEFAULT_LOCALE, { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - hour12: false, - timeZone: options?.timezone, - }); - const parts = formatter.formatToParts(d); + // Helper to format hour key with timezone awareness + const hourFormatter = new Intl.DateTimeFormat(options?.locale ?? DEFAULT_LOCALE, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + hour12: false, + timeZone: options?.timezone, + }); + function formatHourKey(timestamp: string): string { + const d = new Date(timestamp); + const parts = hourFormatter.formatToParts(d); const year = parts.find(p => p.type === 'year')?.value ?? ''; const month = parts.find(p => p.type === 'month')?.value ?? ''; const day = parts.find(p => p.type === 'day')?.value ?? ''; const hour = parts.find(p => p.type === 'hour')?.value ?? '00'; return `${year}-${month}-${day} ${hour}:00`; }apps/ccusage/src/commands/hourly.ts (1)
190-201: Use adaptive separator row (compact vs full table)Avoid hard‑coding 8 empty cells; reuse helper so widths match both modes.
Apply this diff:
- if (!isFirstProject) { - table.push(['', '', '', '', '', '', '', '']); - } + if (!isFirstProject) { + addEmptySeparatorRow(table, table.isCompactMode() ? 5 : 8); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📥 Commits
Reviewing files that changed from the base of the PR and between bd492d605fea2bf2743f62f139dce6daebe421fc and 27687a9.
📒 Files selected for processing (4)
apps/ccusage/config-schema.json(1 hunks)apps/ccusage/src/commands/hourly.ts(1 hunks)apps/ccusage/src/commands/index.ts(1 hunks)apps/ccusage/src/data-loader.ts(2 hunks)
🧰 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/hourly.tsapps/ccusage/src/data-loader.tsapps/ccusage/src/commands/index.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/hourly.tsapps/ccusage/src/data-loader.tsapps/ccusage/src/commands/index.ts
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Use tab indentation and double quotes (ESLint formatting)
Do not use console.log; only allow where explicitly disabled via eslint-disable
Always use Node.js path utilities for file paths for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() to wrap operations that may throw (e.g., JSON parsing)
Use Result.isFailure() for checking errors instead of negating isSuccess()
Use early return on failures (e.g., if (Result.isFailure(r)) continue) instead of ternary patterns
For async operations, create a wrapper using Result.try() and call it
Keep traditional try-catch only for complex file I/O or legacy code that’s hard to refactor
Always use Result.isFailure() and Result.isSuccess() type guards for clarity
Variables use camelCase naming
Types use PascalCase naming
Constants can use UPPER_SNAKE_CASE
Only export constants, functions, and types that are actually used by other modules
Do not export internal/private constants that are only used within the same file
Before exporting a constant, verify it is referenced by other modules
Use Vitest globals (describe, it, expect) without imports in test blocks
Never use await import() dynamic imports anywhere in the codebase
Never use dynamic imports inside Vitest test blocks
Use fs-fixture createFixture() for mock Claude data directories in tests
All tests must use current Claude 4 models (not Claude 3)
Test coverage should include both Sonnet and Opus models
Model names in tests must exactly match LiteLLM pricing database entries
Use logger.ts instead of console.log for logging
Files:
apps/ccusage/src/commands/hourly.tsapps/ccusage/src/data-loader.tsapps/ccusage/src/commands/index.ts
**/data-loader.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Silently skip malformed JSONL lines during parsing in data-loader.ts
Files:
apps/ccusage/src/data-loader.ts
🧠 Learnings (5)
📚 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-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/data-loader.ts
📚 Learning: 2025-09-18T17:43:09.255Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-18T17:43:09.255Z
Learning: Applies to **/*.ts : Use fs-fixture createFixture() for mock Claude data directories 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: Tests should use fs-fixture with using to ensure 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/**/*.ts : Use `.ts` extensions for local imports (e.g., `import { foo } from './utils.ts'`)
Applied to files:
apps/ccusage/src/commands/index.ts
🧬 Code graph analysis (3)
apps/ccusage/src/commands/hourly.ts (8)
apps/ccusage/src/_shared-args.ts (1)
sharedCommandConfig(116-119)apps/ccusage/src/_config-loader-tokens.ts (2)
loadConfig(146-197)mergeConfigWithArgs(212-298)apps/ccusage/src/data-loader.ts (1)
loadHourlyUsageData(1100-1264)apps/ccusage/src/debug.ts (2)
detectMismatches(75-218)printMismatchReport(225-309)apps/ccusage/src/calculate-cost.ts (1)
createTotalsObject(77-82)apps/ccusage/src/_jq-processor.ts (1)
processWithJq(10-36)packages/terminal/src/table.ts (6)
UsageReportConfig(415-424)createUsageReportTable(443-500)formatUsageDataRow(509-532)pushBreakdownRows(369-410)addEmptySeparatorRow(566-569)formatTotalsRow(540-559)apps/ccusage/src/_project-names.ts (1)
formatProjectName(137-152)
apps/ccusage/src/data-loader.ts (6)
apps/codex/src/data-loader.ts (1)
LoadOptions(169-171)apps/ccusage/src/_types.ts (2)
ModelName(91-91)createISOTimestamp(113-113)apps/ccusage/src/_pricing-fetcher.ts (1)
PricingFetcher(16-25)apps/ccusage/src/_consts.ts (1)
DEFAULT_LOCALE(148-148)apps/ccusage/src/calculate-cost.ts (1)
calculateTotals(48-67)apps/ccusage/src/_date-utils.ts (1)
sortByDate(94-108)
apps/ccusage/src/commands/index.ts (1)
apps/ccusage/src/commands/hourly.ts (1)
hourlyCommand(19-264)
🔇 Additional comments (3)
apps/ccusage/config-schema.json (1)
231-351: Hourly schema looks consistent with existing commandsProperties and defaults mirror daily/weekly; today flag is clearly documented. No issues.
apps/ccusage/src/commands/index.ts (1)
6-6: Wiring looks correctImport/export and subCommandUnion entry for hourly are consistent.
Also applies to: 13-13, 20-21
apps/ccusage/src/data-loader.ts (1)
3400-3460: The review comment is factually incorrect.The code at lines 3400–3460 uses only the correct canonical model names:
- Line 3402:
claude-sonnet-4-20250514- Line 3442:
claude-opus-4-20250514These models are confirmed in the LiteLLM documentation and exist in the bundled pricing. The alleged incorrect format
claude-4-sonnet-20250514does not appear anywhere in the codebase. LiteLLM may use "claude-4-sonnet" as an internal alias, but the underlying model ID is alwaysanthropic/claude-sonnet-4-20250514, which matches the tests. No naming inconsistency or pricing risk exists.Likely an incorrect or invalid review comment.
| // Sort ascending by hour for a chronological 24h view | ||
| return sortByDate(results, item => item.hour, 'asc'); | ||
| } |
There was a problem hiding this comment.
Respect the --order option (don’t hard‑code 'asc')
Hourly loader ignores options.order. Align with daily/weekly by honoring the flag.
Apply this diff:
- // Sort ascending by hour for a chronological 24h view
- return sortByDate(results, item => item.hour, 'asc');
+ // Sort by hour honoring --order (default to asc for hourly)
+ return sortByDate(results, item => item.hour, options?.order ?? 'asc');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Sort ascending by hour for a chronological 24h view | |
| return sortByDate(results, item => item.hour, 'asc'); | |
| } | |
| // Sort by hour honoring --order (default to asc for hourly) | |
| return sortByDate(results, item => item.hour, options?.order ?? 'asc'); | |
| } |
🤖 Prompt for AI Agents
In apps/ccusage/src/data-loader.ts around lines 1262-1264, the hourly loader
currently hardcodes the sort direction to 'asc' when calling sortByDate,
ignoring the CLI --order flag; change the call to use the incoming options.order
(falling back to 'asc' if undefined) so it respects the same order handling as
the daily/weekly loaders, and validate/normalize the value to either 'asc' or
'desc' before passing it to sortByDate.
- Remove dynamic imports (violates project rules) - Respect --order flag in hourly loader - Hoist DateTimeFormatter for performance - Use DEFAULT_LOCALE constant for consistency - Use addEmptySeparatorRow helper for cleaner code
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/ccusage/src/commands/hourly.ts(1 hunks)apps/ccusage/src/data-loader.ts(4 hunks)
🧰 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/hourly.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/hourly.tsapps/ccusage/src/data-loader.ts
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Use tab indentation and double quotes (ESLint formatting)
Do not use console.log; only allow where explicitly disabled via eslint-disable
Always use Node.js path utilities for file paths for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() to wrap operations that may throw (e.g., JSON parsing)
Use Result.isFailure() for checking errors instead of negating isSuccess()
Use early return on failures (e.g., if (Result.isFailure(r)) continue) instead of ternary patterns
For async operations, create a wrapper using Result.try() and call it
Keep traditional try-catch only for complex file I/O or legacy code that’s hard to refactor
Always use Result.isFailure() and Result.isSuccess() type guards for clarity
Variables use camelCase naming
Types use PascalCase naming
Constants can use UPPER_SNAKE_CASE
Only export constants, functions, and types that are actually used by other modules
Do not export internal/private constants that are only used within the same file
Before exporting a constant, verify it is referenced by other modules
Use Vitest globals (describe, it, expect) without imports in test blocks
Never use await import() dynamic imports anywhere in the codebase
Never use dynamic imports inside Vitest test blocks
Use fs-fixture createFixture() for mock Claude data directories in tests
All tests must use current Claude 4 models (not Claude 3)
Test coverage should include both Sonnet and Opus models
Model names in tests must exactly match LiteLLM pricing database entries
Use logger.ts instead of console.log for logging
Files:
apps/ccusage/src/commands/hourly.tsapps/ccusage/src/data-loader.ts
**/data-loader.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Silently skip malformed JSONL lines during parsing in data-loader.ts
Files:
apps/ccusage/src/data-loader.ts
🧠 Learnings (8)
📚 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:
apps/ccusage/src/commands/hourly.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-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/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: 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-18T17:43:09.255Z
Learnt from: CR
Repo: ryoppippi/ccusage PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-09-18T17:43:09.255Z
Learning: Applies to **/*.ts : Use fs-fixture createFixture() for mock Claude data directories 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/**/*.{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-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 (2)
apps/ccusage/src/commands/hourly.ts (8)
apps/ccusage/src/_shared-args.ts (1)
sharedCommandConfig(116-119)apps/ccusage/src/_config-loader-tokens.ts (2)
loadConfig(146-197)mergeConfigWithArgs(212-298)apps/ccusage/src/data-loader.ts (1)
loadHourlyUsageData(1100-1265)apps/ccusage/src/debug.ts (2)
detectMismatches(75-218)printMismatchReport(225-309)apps/ccusage/src/calculate-cost.ts (1)
createTotalsObject(77-82)apps/ccusage/src/_jq-processor.ts (1)
processWithJq(10-36)packages/terminal/src/table.ts (6)
UsageReportConfig(415-424)createUsageReportTable(443-500)addEmptySeparatorRow(566-569)formatUsageDataRow(509-532)pushBreakdownRows(369-410)formatTotalsRow(540-559)apps/ccusage/src/_project-names.ts (1)
formatProjectName(137-152)
apps/ccusage/src/data-loader.ts (6)
apps/codex/src/data-loader.ts (1)
LoadOptions(169-171)apps/ccusage/src/_types.ts (2)
ModelName(91-91)createISOTimestamp(113-113)apps/ccusage/src/_pricing-fetcher.ts (1)
PricingFetcher(16-25)apps/ccusage/src/_consts.ts (1)
DEFAULT_LOCALE(148-148)apps/ccusage/src/calculate-cost.ts (1)
calculateTotals(48-67)apps/ccusage/src/_date-utils.ts (1)
sortByDate(94-108)
🔇 Additional comments (2)
apps/ccusage/src/data-loader.ts (2)
1263-1264: LGTM! The --order flag is properly respected.The sorting correctly honors
options?.orderand only defaults to'asc'when not specified, which aligns with the expected chronological behavior for hourly data.
2950-3036: Excellent alignment test for data integrity.This test verifies the critical property documented in the JSDoc: hourly totals sum to daily totals for the same timezone-aware day. It loads both hourly and daily data, aggregates each independently, and asserts equality across all token types and costs. This provides strong confidence in the correctness of both loaders.
| // Show debug information if requested | ||
| if (mergedOptions.debug && !useJson) { | ||
| const mismatchStats = await detectMismatches(undefined); | ||
| printMismatchReport(mismatchStats, mergedOptions.debugSamples as number | undefined); | ||
| } |
There was a problem hiding this comment.
Pass custom claudePath to debug mismatch detection.
When detectMismatches is called with undefined, it uses the default Claude path. If the user specified a custom claudePath via config or --config, the debug report will analyze different data than what's displayed in the hourly report.
Apply this diff to pass the custom path:
if (mergedOptions.debug && !useJson) {
- const mismatchStats = await detectMismatches(undefined);
+ const mismatchStats = await detectMismatches(mergedOptions.claudePath);
printMismatchReport(mismatchStats, mergedOptions.debugSamples as number | undefined);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Show debug information if requested | |
| if (mergedOptions.debug && !useJson) { | |
| const mismatchStats = await detectMismatches(undefined); | |
| printMismatchReport(mismatchStats, mergedOptions.debugSamples as number | undefined); | |
| } | |
| // Show debug information if requested | |
| if (mergedOptions.debug && !useJson) { | |
| const mismatchStats = await detectMismatches(mergedOptions.claudePath); | |
| printMismatchReport(mismatchStats, mergedOptions.debugSamples as number | undefined); | |
| } |
🤖 Prompt for AI Agents
In apps/ccusage/src/commands/hourly.ts around lines 129 to 133, the debug path
for mismatch detection is hardcoded by calling detectMismatches(undefined) which
ignores a user-provided claudePath; change the call to pass the
configured/custom path (e.g., detectMismatches(mergedOptions.claudePath)) so the
debug mismatch report uses the same Claude instance as the hourly run, leaving
the printMismatchReport call and debugSamples handling unchanged.
- Change forceCompact from ctx.values.compact to mergedOptions.compact - Ensures config file settings are properly merged with CLI flags - Fixes inconsistency with other option handling in hourly command Note: CodeRabbit also suggested passing claudePath to detectMismatches, but this option is not available in mergedOptions for any command. All commands (daily, monthly, weekly, session) use undefined.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/ccusage/src/commands/hourly.ts (1)
129-133: Pass custom claudePath to debug mismatch detection.When
detectMismatchesis called withundefined, it uses the default Claude path. If the user specified a customclaudePathvia config or--config, the debug report will analyze different data than what's displayed in the hourly report.Apply this diff to pass the custom path:
if (mergedOptions.debug && !useJson) { - const mismatchStats = await detectMismatches(undefined); + const mismatchStats = await detectMismatches(mergedOptions.claudePath); printMismatchReport(mismatchStats, mergedOptions.debugSamples as number | undefined); }
🧹 Nitpick comments (1)
apps/ccusage/src/commands/hourly.ts (1)
1-265: Consider adding in-source tests for command logic.According to coding guidelines, tests should be written in-source using
if (import.meta.vitest != null)blocks. While the data-loader has tests forloadHourlyUsageData, the command logic itself (projectAliases parsing, today filtering, totals calculation, project grouping) would benefit from test coverage.Based on coding guidelines.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/ccusage/src/commands/hourly.ts(1 hunks)
🧰 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/hourly.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/hourly.ts
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Use tab indentation and double quotes (ESLint formatting)
Do not use console.log; only allow where explicitly disabled via eslint-disable
Always use Node.js path utilities for file paths for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() to wrap operations that may throw (e.g., JSON parsing)
Use Result.isFailure() for checking errors instead of negating isSuccess()
Use early return on failures (e.g., if (Result.isFailure(r)) continue) instead of ternary patterns
For async operations, create a wrapper using Result.try() and call it
Keep traditional try-catch only for complex file I/O or legacy code that’s hard to refactor
Always use Result.isFailure() and Result.isSuccess() type guards for clarity
Variables use camelCase naming
Types use PascalCase naming
Constants can use UPPER_SNAKE_CASE
Only export constants, functions, and types that are actually used by other modules
Do not export internal/private constants that are only used within the same file
Before exporting a constant, verify it is referenced by other modules
Use Vitest globals (describe, it, expect) without imports in test blocks
Never use await import() dynamic imports anywhere in the codebase
Never use dynamic imports inside Vitest test blocks
Use fs-fixture createFixture() for mock Claude data directories in tests
All tests must use current Claude 4 models (not Claude 3)
Test coverage should include both Sonnet and Opus models
Model names in tests must exactly match LiteLLM pricing database entries
Use logger.ts instead of console.log for logging
Files:
apps/ccusage/src/commands/hourly.ts
🧠 Learnings (3)
📚 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:
apps/ccusage/src/commands/hourly.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/commands/hourly.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/commands/hourly.ts
🧬 Code graph analysis (1)
apps/ccusage/src/commands/hourly.ts (7)
apps/ccusage/src/_config-loader-tokens.ts (2)
loadConfig(146-197)mergeConfigWithArgs(212-298)apps/ccusage/src/data-loader.ts (1)
loadHourlyUsageData(1100-1265)apps/ccusage/src/debug.ts (2)
detectMismatches(75-218)printMismatchReport(225-309)apps/ccusage/src/calculate-cost.ts (1)
createTotalsObject(77-82)apps/ccusage/src/_jq-processor.ts (1)
processWithJq(10-36)packages/terminal/src/table.ts (6)
UsageReportConfig(415-424)createUsageReportTable(443-500)addEmptySeparatorRow(566-569)formatUsageDataRow(509-532)pushBreakdownRows(369-410)formatTotalsRow(540-559)apps/ccusage/src/_project-names.ts (1)
formatProjectName(137-152)
|
we can add those commands infinitely |
|
sure thing no worries, I tried to follow your practices as best as I could, will just keep it as a fork for myself for what I need. thanks! |
|
Thanks |

PR Notes: Add Hourly Usage Report
Summary
Adds a new
hourlycommand to ccusage that displays usage data aggregated by hour for the last 24 hours.Changes Made
New Files
apps/ccusage/src/commands/hourly.ts(238 lines)--instances,--project,--json,--compact,--breakdown,--timezone,--locale,--jq--today/-tto show only hours from current calendar dayModified Files
apps/ccusage/src/commands/index.tshourlyCommandto exportssubCommandUnionarrayapps/ccusage/src/data-loader.tsloadHourlyUsageData()(lines 1087-1257)Test Coverage
Tests Added (6 new tests in
data-loader.ts)--instancesflag functionalityTest Results
Hour-to-Day Alignment
Documentation Added
Comprehensive JSDoc explaining how hours map to days:
Test Verification
The alignment test (lines 2944-3038) proves:
Real-World Verification
Manually tested with actual Claude usage data:
Key Features
YYYY-MM-DD HH:00format matching user's timezone--instancesflag for per-project breakdown--todayflag to show only current day's hoursauto,calculate, anddisplaymodesUsage Examples
Code Quality
Follows Project Standards
data-loader.ts).test.tsfiles (matches repo convention)daily.ts,monthly.ts,weekly.ts)Pattern Consistency
loadWeeklyUsageData()structureCommit Message Suggestion
Files Changed Summary
Pre-PR Checklist
Additional Notes
Why
--todayis Not Testeddaily.ts,monthly.ts,weekly.ts,blocks.tshave zero tests--todayflag is 8 lines of simple filtering logicWhy This Matters
The hourly report fills a gap between session-level detail and daily aggregation:
Users can now:
--todayTesting the PR
Ready for review! 🚀
Summary by CodeRabbit
New Features
Documentation