|
#!/bin/bash |
|
# Fetch real usage limits from Anthropic API and combine with ccusage statusline |
|
|
|
set -euo pipefail |
|
|
|
export PATH="/opt/homebrew/bin:$PATH" |
|
|
|
# Cache settings |
|
CACHE_FILE="/tmp/claude-usage-cache.json" |
|
CACHE_TTL=60 # seconds |
|
|
|
# Get cached or fresh usage data |
|
get_usage() { |
|
local now |
|
now=$(date +%s) |
|
|
|
# Check cache |
|
if [[ -f "$CACHE_FILE" ]]; then |
|
local cache_time |
|
cache_time=$(stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) |
|
if (( now - cache_time < CACHE_TTL )); then |
|
cat "$CACHE_FILE" |
|
return |
|
fi |
|
fi |
|
|
|
# Get credentials from Keychain |
|
local creds token |
|
creds=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null) || return 1 |
|
token=$(echo "$creds" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) || return 1 |
|
|
|
if [[ -z "$token" ]]; then |
|
return 1 |
|
fi |
|
|
|
# Fetch usage from API |
|
local response |
|
response=$(curl --silent --max-time 5 \ |
|
--header "Authorization: Bearer $token" \ |
|
--header "anthropic-beta: oauth-2025-04-20" \ |
|
"https://api.anthropic.com/api/oauth/usage" 2>/dev/null) || return 1 |
|
|
|
# Cache response |
|
echo "$response" > "$CACHE_FILE" |
|
echo "$response" |
|
} |
|
|
|
# Calculate time remaining (in minutes) from ISO timestamp (UTC) |
|
time_remaining_mins() { |
|
local reset_at=$1 |
|
local now reset_ts diff |
|
|
|
now=$(date +%s) |
|
# Parse ISO timestamp as UTC |
|
# Remove microseconds and timezone suffix, parse in UTC |
|
local ts_clean="${reset_at%%.*}" |
|
ts_clean="${ts_clean//T/ }" # Replace T with space |
|
reset_ts=$(TZ=UTC date -j -f "%Y-%m-%d %H:%M:%S" "$ts_clean" +%s 2>/dev/null) || return 1 |
|
|
|
diff=$((reset_ts - now)) |
|
echo $(( diff / 60 )) |
|
} |
|
|
|
# Format minutes to human readable |
|
format_time() { |
|
local mins=$1 |
|
if (( mins <= 0 )); then |
|
echo "now" |
|
return |
|
fi |
|
|
|
local days hours minutes |
|
days=$((mins / 1440)) |
|
hours=$(((mins % 1440) / 60)) |
|
minutes=$((mins % 60)) |
|
|
|
if (( days > 0 )); then |
|
echo "${days}d ${hours}h" |
|
elif (( hours > 0 )); then |
|
echo "${hours}h ${minutes}m" |
|
else |
|
echo "${minutes}m" |
|
fi |
|
} |
|
|
|
# Get rate indicator based on usage% vs time elapsed% |
|
# Usage: rate_indicator <usage%> <time_remaining_mins> <total_window_mins> |
|
rate_indicator() { |
|
local usage=$1 |
|
local remaining_mins=$2 |
|
local total_mins=$3 |
|
|
|
# Calculate time elapsed percentage |
|
local elapsed_mins=$((total_mins - remaining_mins)) |
|
if (( elapsed_mins < 0 )); then elapsed_mins=0; fi |
|
|
|
local time_pct |
|
if (( total_mins > 0 )); then |
|
time_pct=$((elapsed_mins * 100 / total_mins)) |
|
else |
|
time_pct=0 |
|
fi |
|
|
|
# Compare usage% with time% |
|
local diff=$((usage - time_pct)) |
|
|
|
if (( diff <= 0 )); then |
|
echo "🟢" # On track or under |
|
elif (( diff <= 5 )); then |
|
echo "🟡" # Slightly ahead |
|
elif (( diff <= 15 )); then |
|
echo "🟠" # Ahead |
|
else |
|
echo "🔴" # Way ahead, will likely hit limit |
|
fi |
|
} |
|
|
|
# Main - receives JSON from Claude Code via stdin |
|
main() { |
|
# Read stdin (Claude Code passes JSON) |
|
local input |
|
input=$(cat) |
|
|
|
# Get base statusline from ccusage (pass through the input) |
|
local base |
|
base=$(echo "$input" | ccusage statusline --offline --visual-burn-rate off 2>/dev/null) || base="" |
|
|
|
# Remove fields not useful on subscription (ccusage doesn't have options for these) |
|
base=$(echo "$base" | sed -E 's/ *\([0-9]+h [0-9]+m left\)//g; s| */ *\$[0-9]+\.[0-9]+ block||g') |
|
|
|
# Get usage data from API |
|
local usage five_hour seven_day |
|
usage=$(get_usage 2>/dev/null) || usage="" |
|
|
|
if [[ -n "$usage" ]]; then |
|
five_hour=$(echo "$usage" | jq -r '.five_hour.utilization // empty' 2>/dev/null) |
|
five_hour_resets=$(echo "$usage" | jq -r '.five_hour.resets_at // empty' 2>/dev/null) |
|
seven_day=$(echo "$usage" | jq -r '.seven_day.utilization // empty' 2>/dev/null) |
|
seven_day_resets=$(echo "$usage" | jq -r '.seven_day.resets_at // empty' 2>/dev/null) |
|
|
|
local quota_info="" |
|
|
|
# 7d only if >= 70% |
|
if [[ -n "$seven_day" ]]; then |
|
local seven_day_int |
|
seven_day_int=$(printf "%.0f" "$seven_day") |
|
if (( seven_day_int >= 70 )); then |
|
local seven_day_remaining_mins seven_day_indicator seven_day_time_str |
|
seven_day_remaining_mins=$(time_remaining_mins "$seven_day_resets" 2>/dev/null) || seven_day_remaining_mins=0 |
|
seven_day_indicator=$(rate_indicator "$seven_day_int" "$seven_day_remaining_mins" 10080) # 7d = 10080 mins |
|
seven_day_time_str=$(format_time "$seven_day_remaining_mins") |
|
quota_info+="${seven_day_indicator} 7d: ${seven_day_int}% (${seven_day_time_str})" |
|
fi |
|
fi |
|
|
|
# 5h always shown |
|
if [[ -n "$five_hour" ]]; then |
|
local five_hour_int five_hour_remaining_mins five_hour_indicator five_hour_time_str |
|
five_hour_int=$(printf "%.0f" "$five_hour") |
|
five_hour_remaining_mins=$(time_remaining_mins "$five_hour_resets" 2>/dev/null) || five_hour_remaining_mins=0 |
|
five_hour_indicator=$(rate_indicator "$five_hour_int" "$five_hour_remaining_mins" 300) # 5h = 300 mins |
|
five_hour_time_str=$(format_time "$five_hour_remaining_mins") |
|
|
|
[[ -n "$quota_info" ]] && quota_info+=" | " |
|
quota_info+="${five_hour_indicator} 5h: ${five_hour_int}% (${five_hour_time_str})" |
|
fi |
|
|
|
if [[ -n "$quota_info" ]]; then |
|
echo "${base} | ${quota_info}" |
|
else |
|
echo "$base" |
|
fi |
|
else |
|
echo "$base" |
|
fi |
|
} |
|
|
|
main |