mirror of
https://github.com/Xevion/dotfiles.git
synced 2026-01-31 02:24:11 -06:00
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.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
function ccu --description 'Check Claude usage (alias for claude-usage)'
|
||||
claude-usage $argv
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
function cu --description 'Check Claude usage (alias for claude-usage)'
|
||||
claude-usage $argv
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
function usage --description 'Check Claude usage (alias for claude-usage)'
|
||||
claude-usage $argv
|
||||
end
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user