#!/usr/bin/env bun /** * claude-usage - Fast CLI tool to fetch Anthropic Claude usage percentages * * Requirements: * - Bun runtime * - ~/.claude/.credentials.json with valid OAuth token * * Usage: * claude-usage * cu # alias * usage # alias * ccu # alias (claude-check-usage) */ import { existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; // Configuration const CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json'); interface UsagePeriod { utilization: number; reset: number; } interface UsageData { five_hour: UsagePeriod; seven_day: UsagePeriod; } interface Credentials { claudeAiOauth?: { accessToken?: string; }; } /** * Generate user_id metadata for the request (mimics Claude Code's format) */ function generateUserId(accessToken: string): string { // Use hash of token as device/user ID const hash = Bun.hash(accessToken).toString(16).padStart(64, '0').substring(0, 64); // Generate a session UUID const sessionId = crypto.randomUUID(); return `user_${hash}_session_${sessionId}`; } /** * Read OAuth token from credentials file */ async function readToken(): Promise { if (!existsSync(CREDENTIALS_PATH)) { throw new Error(`Credentials file not found: ${CREDENTIALS_PATH}\nPlease run 'claude setup-token' first.`); } const fileContent = Bun.file(CREDENTIALS_PATH); const credentials: Credentials = await fileContent.json() as Credentials; const token = credentials.claudeAiOauth?.accessToken; if (!token) { throw new Error('No access token found in credentials file. Please run \'claude setup-token\' to authenticate.'); } return token; } /** * Fetch usage data by making an API request and reading rate limit headers * This request matches what Claude Code sends for checking quota */ async function fetchUsage(accessToken: string): Promise { const userId = generateUserId(accessToken); // Make the exact same request Claude Code makes for checking quota const response = await fetch('https://api.anthropic.com/v1/messages?beta=true', { method: 'POST', headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'anthropic-version': '2023-06-01', 'anthropic-beta': 'oauth-2025-04-20,interleaved-thinking-2025-05-14', 'anthropic-dangerous-direct-browser-access': 'true', 'x-app': 'cli', 'User-Agent': 'claude-cli/2.0.76 (external, sdk-cli)', 'Content-Type': 'application/json', 'accept-language': '*', 'sec-fetch-mode': 'cors', 'accept-encoding': 'br, gzip, deflate', }, body: JSON.stringify({ model: 'claude-haiku-4-5-20251001', max_tokens: 1, messages: [ { role: 'user', content: 'quota', }, ], metadata: { user_id: userId, }, }), }); if (!response.ok) { const text = await response.text(); throw new Error(`API request failed (${response.status}): ${text.substring(0, 200)}`); } // Extract rate limit headers from response const headers = response.headers; const fiveHourUtil = parseFloat(headers.get('anthropic-ratelimit-unified-5h-utilization') || '0'); const sevenDayUtil = parseFloat(headers.get('anthropic-ratelimit-unified-7d-utilization') || '0'); const fiveHourReset = parseInt(headers.get('anthropic-ratelimit-unified-5h-reset') || '0'); const sevenDayReset = parseInt(headers.get('anthropic-ratelimit-unified-7d-reset') || '0'); return { five_hour: { utilization: fiveHourUtil, reset: fiveHourReset, }, seven_day: { utilization: sevenDayUtil, reset: sevenDayReset, }, }; } /** * Format reset time from Unix timestamp */ function formatResetTime(resetTimestamp: number): string { if (!resetTimestamp) return ''; const resetDate = new Date(resetTimestamp * 1000); // Convert to milliseconds const now = new Date(); const diffMs = resetDate.getTime() - now.getTime(); if (diffMs < 0) return '(reset overdue)'; const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); if (diffHours < 24) { return `(resets in ${diffHours}h ${diffMins}m)`; } const diffDays = Math.floor(diffHours / 24); const remainingHours = diffHours % 24; return `(resets in ${diffDays}d ${remainingHours}h)`; } /** * Main entry point */ async function main() { try { // Read OAuth token from credentials file const accessToken = await readToken(); // Fetch usage data from API headers const usage = await fetchUsage(accessToken); // Format and display const fiveHourPct = Math.round(usage.five_hour.utilization * 100); const sevenDayPct = Math.round(usage.seven_day.utilization * 100); const fiveHourReset = formatResetTime(usage.five_hour.reset); const sevenDayReset = formatResetTime(usage.seven_day.reset); console.log(`5h: ${fiveHourPct}% ${fiveHourReset} | 7d: ${sevenDayPct}% ${sevenDayReset}`); } catch (error) { if (error instanceof Error) { console.error(`Error: ${error.message}`); } else { console.error('Error: Unknown error occurred'); } process.exit(1); } } main();