Skip to content

feat: Add limits command to show rate limit history#838

Open
terry-li-hm wants to merge 1 commit intoryoppippi:mainfrom
terry-li-hm:feat/limits-command
Open

feat: Add limits command to show rate limit history#838
terry-li-hm wants to merge 1 commit intoryoppippi:mainfrom
terry-li-hm:feat/limits-command

Conversation

@terry-li-hm
Copy link

@terry-li-hm terry-li-hm commented Feb 2, 2026

Summary

Adds a new ccusage limits command 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

  • Parses JSONL files from ~/.claude/projects/*/*.jsonl
  • Finds entries with "error":"rate_limit"
  • Extracts timestamp and reset time from each event
  • Infers limit type (Weekly vs 5-hour) from reset message patterns
  • Supports --limit N, --since YYYYMMDD, --json, --jq

Example Output

╭──────────────────────────────────╮
│  Claude Code Rate Limit History  │
╰──────────────────────────────────╯

┌───────────────────┬──────────────────────────┬──────────┐
│ Hit Time          │ Reset Time               │ Type     │
├───────────────────┼──────────────────────────┼──────────┤
│ 2026-01-30 07:28  │ 6pm (Asia/Hong_Kong)     │ Weekly   │
│ 2026-01-26 14:48  │ 4pm (Asia/Hong_Kong)     │ 5-hour   │
│ 2026-01-23 12:07  │ Jan 24 at 6pm            │ Weekly   │
└───────────────────┴──────────────────────────┴──────────┘

Type Inference Logic

  • Weekly: Reset message contains a date (e.g., "Jan 24"), or is "6pm" on a Saturday
  • 5-hour: Simple time patterns (e.g., "10am", "4pm")

Use Cases

  1. Calibrate weekly limits — See when you've hit limits historically to estimate your actual cap
  2. Auto-detect reset time — The logs contain exact reset times, no manual input needed
  3. Track patterns — Identify if you're consistently hitting limits on certain days

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

  • New Features
    • Added a new limits command to display rate-limit events with filtering and formatting options.
    • Supports both table and JSON output formats with optional jq transformation.
    • Includes configurable options for result limits, date filtering, timezone, and locale settings.

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)
@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

A 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

Cohort / File(s) Summary
Configuration Schema
apps/ccusage/config-schema.json
Added new "limits" command definition to the ccusage-config schema with properties: limit (default 10), since, json (default false), jq, timezone, and locale (default "en-CA").
Command Registration
apps/ccusage/src/commands/index.ts
Registered the new limits subcommand by importing limitsCommand and adding it to the subCommandUnion mapping.
Limits Command Implementation
apps/ccusage/src/commands/limits.ts
Implemented the limits command with JSONL log scanning, rate-limit event extraction via pattern matching and timezone-aware inference, and dual output modes (JSON with optional jq transformation or colored table format).

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Suggested reviewers

  • ryoppippi

Poem

🐰 A limits command hops into place,
Scanning logs at a rapid pace,
Rate-limit events, now on display,
In JSON or tables, come what may! 📊✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: adding a new 'limits' command to display rate limit history, which aligns with all file changes in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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/byethrow Result type over traditional try-catch for functional error handling.

Comment on lines +128 to +132
// Extract project path from file path
const projectsDir = join(homedir(), '.claude', 'projects');
const projectPath = relative(projectsDir, filePath);
const project = projectPath.split('/')[0] ?? 'unknown';

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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.

Comment on lines +155 to +158
for (const filePath of files) {
const { createReadStream } = await import('node:fs');
const { createInterface } = await import('node:readline');

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/ccusage/src/commands/limits.ts | head -180 | tail -60

Repository: 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.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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.

Comment on lines +165 to +167
for await (const line of rl) {
if (line.includes('"error":"rate_limit"')) {
const event = parseRateLimitEntry(line, filePath);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant