Files
dotfiles/home/dot_local/bin/executable_claude-usage
Xevion 8b77454d2b feat: add claude-usage CLI tool with Fish shell aliases
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.
2025-12-27 17:00:25 -06:00

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();