feat: add burn limits command for Claude quota tracking#45
feat: add burn limits command for Claude quota tracking#45BossChaos wants to merge 1 commit intoAgentWorkforce:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new burn limits subcommand to the CLI to display Claude quota-window usage (with optional watch mode and JSON output), aligning with Issue #5’s “quota-window tracking” MVP for Claude.
Changes:
- Introduces
packages/cli/src/commands/limits.tsimplementing token lookup from Claude Code state and calling Anthropic’s OAuth usage endpoint. - Adds
burn limitsrouting and help text to the main CLI dispatcher.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| packages/cli/src/commands/limits.ts | Implements the limits command, including output formatting, watch loop, and token loading. |
| packages/cli/src/cli.ts | Wires burn limits into the CLI help and command switch. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| process.stderr.write('Error: Could not find Claude OAuth token\n'); | ||
| process.stderr.write('Make sure you have used Claude Code recently.\n'); | ||
| return 1; |
| const runOnce = async (): Promise<void> => { | ||
| const usage = await fetchClaudeUsage(token); | ||
| if (!usage) return; | ||
|
|
||
| if (isJson) { | ||
| process.stdout.write(formatUsageJson(usage) + '\n'); | ||
| } else { | ||
| process.stdout.write(formatUsageTable(usage)); | ||
| } | ||
| }; | ||
|
|
||
| if (watchMode) { | ||
| // Initial run | ||
| await runOnce(); | ||
|
|
||
| // Set up watch loop | ||
| while (true) { | ||
| await sleep(watchInterval); | ||
| // Clear screen and move cursor to top | ||
| process.stdout.write('\x1B[2J\x1B[0;0H'); | ||
| await runOnce(); | ||
| } | ||
| } else { | ||
| await runOnce(); | ||
| } | ||
|
|
||
| return 0; | ||
| } |
| if (watchMode) { | ||
| // Initial run | ||
| await runOnce(); | ||
|
|
||
| // Set up watch loop | ||
| while (true) { | ||
| await sleep(watchInterval); | ||
| // Clear screen and move cursor to top | ||
| process.stdout.write('\x1B[2J\x1B[0;0H'); | ||
| await runOnce(); | ||
| } |
| // Clear screen and move cursor to top | ||
| process.stdout.write('\x1B[2J\x1B[0;0H'); |
| import type { ParsedArgs } from '../args.js'; | ||
| import { formatUsd } from '../format.js'; | ||
|
|
||
| interface UsageWindow { | ||
| percent_used: number; | ||
| reset_at: string; | ||
| } | ||
|
|
||
| interface ClaudeUsageResponse { | ||
| five_hour?: UsageWindow; | ||
| seven_day?: UsageWindow; | ||
| seven_day_opus?: UsageWindow; | ||
| extra_usage?: UsageWindow; | ||
| } | ||
|
|
||
| interface PlanInfo { | ||
| name: string; | ||
| monthlyBudget: number; | ||
| spent: number; | ||
| elapsedDays: number; | ||
| totalDays: number; | ||
| projected: number; | ||
| runway: number; | ||
| } |
| export async function runLimits(args: ParsedArgs): Promise<number> { | ||
| if (args.flags['help'] || args.flags['h']) { | ||
| process.stdout.write(HELP); | ||
| return 0; | ||
| } | ||
|
|
||
| const isJson = args.flags['json'] === true; | ||
| const watchMode = args.flags['watch'] !== undefined; | ||
| const watchInterval = typeof args.flags['watch'] === 'string' | ||
| ? parseInterval(args.flags['watch']) | ||
| : 5000; | ||
|
|
||
| const token = await getClaudeOAuthToken(); | ||
| if (!token) { | ||
| process.stderr.write('Error: Could not find Claude OAuth token\n'); | ||
| process.stderr.write('Make sure you have used Claude Code recently.\n'); | ||
| return 1; | ||
| } | ||
|
|
||
| const runOnce = async (): Promise<void> => { | ||
| const usage = await fetchClaudeUsage(token); | ||
| if (!usage) return; | ||
|
|
||
| if (isJson) { | ||
| process.stdout.write(formatUsageJson(usage) + '\n'); | ||
| } else { | ||
| process.stdout.write(formatUsageTable(usage)); | ||
| } | ||
| }; | ||
|
|
||
| if (watchMode) { | ||
| // Initial run | ||
| await runOnce(); | ||
|
|
||
| // Set up watch loop | ||
| while (true) { | ||
| await sleep(watchInterval); | ||
| // Clear screen and move cursor to top | ||
| process.stdout.write('\x1B[2J\x1B[0;0H'); | ||
| await runOnce(); | ||
| } | ||
| } else { | ||
| await runOnce(); | ||
| } | ||
|
|
||
| return 0; | ||
| } |
| function formatDuration(ms: number): string { | ||
| const totalSeconds = Math.floor(ms / 1000); | ||
| const hours = Math.floor(totalSeconds / 3600); | ||
| const minutes = Math.floor((totalSeconds % 3600) / 60); | ||
|
|
||
| if (hours > 0) { | ||
| return `${hours}h ${minutes}m`; | ||
| } | ||
| return `${minutes}m`; | ||
| } |
| } | ||
|
|
||
| export async function runLimits(args: ParsedArgs): Promise<number> { | ||
| if (args.flags['help'] || args.flags['h']) { |
- Implements burn limits command with TTY table and JSON output - Adds 60s API response caching to avoid rate limiting - Fixes exit code 2 for token/usage errors per acceptance criteria - Adds --watch/--json mutual exclusion with clear error message - Extends formatDuration to show days for 7-day windows - Removes unused -h short flag check (parseArgs only supports --long) - Removes unused PlanInfo interface and related code - Adds comprehensive unit tests for token handling, output formatting, and flag validation Closes AgentWorkforce#5
ab52abe to
af3d5c1
Compare
|
Thanks for the detailed review! All 8 issues have been addressed:
All changes squashed into commit af3d5c1. The implementation aligns with Issue #5 MVP requirements for quota-window tracking. Ready for merge! 🚀 |
|
@willwashburn @barryollama Friendly ping! 👋 PR is approved and mergeable ( This implements the |
Summary
Implements the
burn limitscommand to track Claude API quota usage across all windows.Features
burn limits- One-shot snapshot of all quota windowsburn limits --watch [5s]- Real-time monitoring with configurable refresh intervalburn limits --json- Programmatic JSON outputImplementation Details
~/.claude/state.jsonGET https://api.anthropic.com/api/oauth/usageendpointTesting
Closes #5