mirror of
https://github.com/Xevion/dotfiles.git
synced 2026-01-31 12:24:08 -06:00
Implements a fast Bun-based CLI utility to check Claude API rate limit usage by reading OAuth credentials and making a minimal API request to extract rate limit headers. Provides three Fish shell aliases (cu, ccu, usage) for convenient access to the claude-usage command.
189 lines
5.2 KiB
Plaintext
189 lines
5.2 KiB
Plaintext
#!/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<string> {
|
|
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<UsageData> {
|
|
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();
|