feat: Add limits command to show rate limit history#838
feat: Add limits command to show rate limit history#838terry-li-hm wants to merge 1 commit intoryoppippi:mainfrom
limits command to show rate limit history#838Conversation
Adds a new `ccusage limits` command that scans Claude Code conversation logs for rate limit events and displays them in a table. Features: - Parses JSONL files from ~/.claude/projects/*/*.jsonl - Finds entries with "error":"rate_limit" - Extracts timestamp and reset time - Infers limit type (Weekly vs 5-hour) from reset message - Supports --limit N, --since YYYYMMDD, --json, --jq Example output: ``` ┌───────────────────┬──────────────────────────┬──────────┐ │ Hit Time │ Reset Time │ Type │ ├───────────────────┼──────────────────────────┼──────────┤ │ 2026-01-30 07:28 │ 6pm (Asia/Hong_Kong) │ Weekly │ │ 2026-01-23 12:07 │ Jan 24 at 6pm │ Weekly │ └───────────────────┴──────────────────────────┴──────────┘ ``` Relates to ryoppippi#146 (enables auto-detection of reset times from logs)
📝 WalkthroughWalkthroughA new CLI command "limits" is introduced that scans Claude Code JSONL logs to extract and display rate-limit events with configurable output modes (JSON or formatted table). The feature includes schema configuration, command registration, and complete implementation with filtering and timezone support. Changes
Sequence DiagramsequenceDiagram
participant User as User/CLI
participant Cmd as Limits Command
participant FS as File System
participant Parser as JSONL Parser
participant Processor as Event Processor
participant Output as Output Formatter
User->>Cmd: Invoke limits command with options
Cmd->>FS: Glob ~/.claude/projects/**/*.jsonl
FS-->>Cmd: Return matching JSONL files
Cmd->>Parser: Stream and parse JSONL entries
Parser->>Processor: Extract rate-limit events
Processor->>Processor: Infer limit type (Weekly/5-hour)
Processor->>Processor: Filter by date range and limit
Processor-->>Output: Pass processed events
alt JSON Output Mode
Output->>Output: Serialize to JSON (optional jq)
Output-->>User: Display JSON with metadata
else Table Output Mode
Output->>Output: Format as colored table
Output-->>User: Display table + summary
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes 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
🧪 Generate unit tests (beta)
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.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/ccusage/src/commands/limits.ts`:
- Around line 165-167: The current loop over rl checks for the exact substring
'"error":"rate_limit"' which misses variants with whitespace; update the check
in the for-await loop that calls parseRateLimitEntry(line, filePath) to be
whitespace-tolerant (e.g., use a regex like /"error"\s*:\s*"rate_limit"/) or
attempt JSON.parse(line) and test parsed.error === 'rate_limit' before calling
parseRateLimitEntry; ensure the change is applied where rl is iterated and
preserves passing the original line and filePath to parseRateLimitEntry.
- Around line 128-132: The project name extraction is Windows-broken because
projectPath.split('/') only handles POSIX separators; change the split to be
path-separator-agnostic by using Node's path separator (path.sep) or a
cross-platform regex (e.g., split on both '/' and '\\') when deriving project
from projectPath (the variables to update are projectsDir, projectPath and the
project assignment that uses projectPath.split('/')[0]); ensure you import or
reference path.sep if using that approach so the code works correctly on Windows
and POSIX.
- Around line 155-158: Remove the dynamic imports inside the loop by adding
static imports for createReadStream and createInterface at the top of the module
and then reuse those identifiers inside the for (const filePath of files) loop
(replace the await import('node:fs') / await import('node:readline') usage).
Replace the try/catch around JSON.parse(...) with `@praha/byethrow`'s
Result.try(): call Result.try(() => JSON.parse(someString)) where JSON.parse is
currently used, then handle the Result returned (check .isErr()/.unwrap() or
pattern-match) instead of the catch block; keep existing variable names and
surrounding logic intact so callers like the code that reads lines and uses the
parsed object continue to work with the unwrapped value.
🧹 Nitpick comments (1)
apps/ccusage/src/commands/limits.ts (1)
90-142: Prefer Result.try() over try/catch for JSON parsing.This aligns with the byethrow pattern used elsewhere and avoids exception flow control.
♻️ Suggested refactor
function parseRateLimitEntry( line: string, filePath: string, ): RateLimitEvent | null { - try { - const entry = JSON.parse(line) as Record<string, unknown>; + const entryResult = Result.try(() => JSON.parse(line) as Record<string, unknown>); + if (Result.isFailure(entryResult)) { + return null; + } + const entry = entryResult.value; @@ - return { - timestamp, - resetMessage, - limitType: inferLimitType(resetMessage, timestamp), - project, - sessionId, - }; - } catch { - return null; - } + return { + timestamp, + resetMessage, + limitType: inferLimitType(resetMessage, timestamp), + project, + sessionId, + }; }As per coding guidelines: Prefer
@praha/byethrowResult type over traditional try-catch for functional error handling.
| // Extract project path from file path | ||
| const projectsDir = join(homedir(), '.claude', 'projects'); | ||
| const projectPath = relative(projectsDir, filePath); | ||
| const project = projectPath.split('/')[0] ?? 'unknown'; | ||
|
|
There was a problem hiding this comment.
Use a path-separator-agnostic split for project name.
relative() returns backslashes on Windows, so splitting only on / makes the project name incorrect there.
🔧 Suggested fix
- const project = projectPath.split('/')[0] ?? 'unknown';
+ const project = projectPath.split(/[/\\]/u)[0] ?? "unknown";📝 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.
| // Extract project path from file path | |
| const projectsDir = join(homedir(), '.claude', 'projects'); | |
| const projectPath = relative(projectsDir, filePath); | |
| const project = projectPath.split('/')[0] ?? 'unknown'; | |
| // Extract project path from file path | |
| const projectsDir = join(homedir(), '.claude', 'projects'); | |
| const projectPath = relative(projectsDir, filePath); | |
| const project = projectPath.split(/[/\\]/u)[0] ?? "unknown"; |
🤖 Prompt for AI Agents
In `@apps/ccusage/src/commands/limits.ts` around lines 128 - 132, The project name
extraction is Windows-broken because projectPath.split('/') only handles POSIX
separators; change the split to be path-separator-agnostic by using Node's path
separator (path.sep) or a cross-platform regex (e.g., split on both '/' and
'\\') when deriving project from projectPath (the variables to update are
projectsDir, projectPath and the project assignment that uses
projectPath.split('/')[0]); ensure you import or reference path.sep if using
that approach so the code works correctly on Windows and POSIX.
| for (const filePath of files) { | ||
| const { createReadStream } = await import('node:fs'); | ||
| const { createInterface } = await import('node:readline'); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/ccusage/src/commands/limits.ts | head -180 | tail -60Repository: ryoppippi/ccusage
Length of output: 1889
🏁 Script executed:
# Also check the exact content at lines 155-158
sed -n '1,10p' apps/ccusage/src/commands/limits.ts && echo "---" && sed -n '150,160p' apps/ccusage/src/commands/limits.tsRepository: ryoppippi/ccusage
Length of output: 854
🏁 Script executed:
# Check for other await import() usage in the file
rg -n "await\s+import\(" apps/ccusage/src/commands/limits.tsRepository: ryoppippi/ccusage
Length of output: 185
🏁 Script executed:
# Check how line 140 parseRateLimitEntry function is structured and other try/catch blocks
sed -n '95,145p' apps/ccusage/src/commands/limits.tsRepository: ryoppippi/ccusage
Length of output: 1256
🏁 Script executed:
# Check if there are other violations in the file
rg -n "try\s*\{" apps/ccusage/src/commands/limits.tsRepository: ryoppippi/ccusage
Length of output: 70
Remove dynamic imports and refactor error handling to use Result.try().
Dynamic await import() statements at lines 156-157 violate the tree-shaking policy and must be removed. Additionally, the try/catch block wrapping JSON.parse() (lines 94-142) should use Result.try() instead, per the @praha/byethrow error handling guidelines.
Move createReadStream and createInterface to the static imports at the top of the file, then reuse them inside the loop. For the JSON parsing at line 95, wrap it with Result.try() and handle the error result instead of using catch.
🔧 Suggested fix
+import { createReadStream } from "node:fs";
import { homedir } from "node:os";
import { join, relative } from "node:path";
import process from "node:process";
+import { createInterface } from "node:readline";- try {
- const entry = JSON.parse(line) as Record<string, unknown>;
- // ... rest of parsing logic
- } catch {
- return null;
- }
+ const parseResult = Result.try(() => JSON.parse(line) as Record<string, unknown>);
+ if (Result.isFailure(parseResult)) {
+ return null;
+ }
+ const entry = parseResult.value;
+ // ... rest of parsing logic- for (const filePath of files) {
- const { createReadStream } = await import('node:fs');
- const { createInterface } = await import('node:readline');
-
const fileStream = createReadStream(filePath);🤖 Prompt for AI Agents
In `@apps/ccusage/src/commands/limits.ts` around lines 155 - 158, Remove the
dynamic imports inside the loop by adding static imports for createReadStream
and createInterface at the top of the module and then reuse those identifiers
inside the for (const filePath of files) loop (replace the await
import('node:fs') / await import('node:readline') usage). Replace the try/catch
around JSON.parse(...) with `@praha/byethrow`'s Result.try(): call Result.try(()
=> JSON.parse(someString)) where JSON.parse is currently used, then handle the
Result returned (check .isErr()/.unwrap() or pattern-match) instead of the catch
block; keep existing variable names and surrounding logic intact so callers like
the code that reads lines and uses the parsed object continue to work with the
unwrapped value.
| for await (const line of rl) { | ||
| if (line.includes('"error":"rate_limit"')) { | ||
| const event = parseRateLimitEntry(line, filePath); |
There was a problem hiding this comment.
Avoid missing rate-limit entries with whitespace in JSON.
The exact string check skips lines like "error": "rate_limit". Use a whitespace-tolerant regex or parse every line.
🔧 Suggested fix
- if (line.includes('"error":"rate_limit"')) {
+ if (/"error"\s*:\s*"rate_limit"/u.test(line)) {📝 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.
| for await (const line of rl) { | |
| if (line.includes('"error":"rate_limit"')) { | |
| const event = parseRateLimitEntry(line, filePath); | |
| for await (const line of rl) { | |
| if (/"error"\s*:\s*"rate_limit"/u.test(line)) { | |
| const event = parseRateLimitEntry(line, filePath); |
🤖 Prompt for AI Agents
In `@apps/ccusage/src/commands/limits.ts` around lines 165 - 167, The current loop
over rl checks for the exact substring '"error":"rate_limit"' which misses
variants with whitespace; update the check in the for-await loop that calls
parseRateLimitEntry(line, filePath) to be whitespace-tolerant (e.g., use a regex
like /"error"\s*:\s*"rate_limit"/) or attempt JSON.parse(line) and test
parsed.error === 'rate_limit' before calling parseRateLimitEntry; ensure the
change is applied where rl is iterated and preserves passing the original line
and filePath to parseRateLimitEntry.
Summary
Adds a new
ccusage limitscommand that scans Claude Code conversation logs for rate limit events and displays them.This addresses the discussion in #146 — instead of users manually specifying reset times, this command extracts them automatically from the logs.
Features
~/.claude/projects/*/*.jsonl"error":"rate_limit"--limit N,--since YYYYMMDD,--json,--jqExample Output
Type Inference Logic
Use Cases
Testing
Tested locally with real Claude Code logs. The command correctly identifies rate limit events and categorizes them.
🤖 Generated with Claude Code
Summary by CodeRabbit
limitscommand to display rate-limit events with filtering and formatting options.