- New skills: clawddocs, claude-code-usage, summarize, homeassistant, humanizer, self-improving-agent - Add .learnings/ for self-improvement tracking - Document proaktive cron config (LRN-20260126-001) - Update USER.md: Löchgau as former residence - Update TOOLS.md: Peekaboo workaround - Memory files for 2026-01-25 and 2026-01-26
269 lines
7.1 KiB
Bash
Executable File
269 lines
7.1 KiB
Bash
Executable File
#!/bin/bash
|
|
# Claude Code Usage Checker
|
|
# Queries Anthropic OAuth API for Claude Code rate limits
|
|
|
|
set -euo pipefail
|
|
|
|
CACHE_FILE="${CACHE_FILE:-/tmp/claude-usage-cache}"
|
|
CACHE_TTL="${CACHE_TTL:-60}" # 1 minute default
|
|
|
|
# Parse arguments
|
|
FORCE_REFRESH=0
|
|
FORMAT="text"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--fresh|--force)
|
|
FORCE_REFRESH=1
|
|
shift
|
|
;;
|
|
--json)
|
|
FORMAT="json"
|
|
shift
|
|
;;
|
|
--cache-ttl)
|
|
CACHE_TTL="$2"
|
|
shift 2
|
|
;;
|
|
--help|-h)
|
|
cat << 'EOF'
|
|
Usage: claude-usage.sh [OPTIONS]
|
|
|
|
Check Claude Code OAuth usage limits (session & weekly).
|
|
|
|
Options:
|
|
--fresh, --force Force refresh (ignore cache)
|
|
--json Output as JSON
|
|
--cache-ttl SEC Cache TTL in seconds (default: 60)
|
|
--help, -h Show this help
|
|
|
|
Examples:
|
|
claude-usage.sh # Use cache if fresh
|
|
claude-usage.sh --fresh # Force API call
|
|
claude-usage.sh --json # JSON output
|
|
EOF
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Function to convert seconds to human readable
|
|
secs_to_human() {
|
|
local secs=$1
|
|
if [ "$secs" -lt 0 ]; then secs=0; fi
|
|
local days=$((secs / 86400))
|
|
local hours=$(((secs % 86400) / 3600))
|
|
local mins=$(((secs % 3600) / 60))
|
|
|
|
if [ "$days" -gt 0 ]; then
|
|
echo "${days}d ${hours}h"
|
|
elif [ "$hours" -gt 0 ]; then
|
|
echo "${hours}h ${mins}m"
|
|
else
|
|
echo "${mins}m"
|
|
fi
|
|
}
|
|
|
|
# Check cache (unless force refresh)
|
|
if [ "$FORCE_REFRESH" -eq 0 ] && [ -f "$CACHE_FILE" ]; then
|
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
age=$(($(date +%s) - $(stat -f%m "$CACHE_FILE")))
|
|
else
|
|
age=$(($(date +%s) - $(stat -c%Y "$CACHE_FILE")))
|
|
fi
|
|
|
|
if [ "$age" -lt "$CACHE_TTL" ]; then
|
|
cat "$CACHE_FILE"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# Get OAuth token from keychain (macOS)
|
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
CREDS=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || echo "")
|
|
else
|
|
# Linux: check common credential stores
|
|
if command -v secret-tool >/dev/null 2>&1; then
|
|
CREDS=$(secret-tool lookup application "Claude Code" 2>/dev/null || echo "")
|
|
else
|
|
echo "Error: Credential storage not found (macOS keychain or secret-tool required)" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$CREDS" ]; then
|
|
if [ "$FORMAT" = "json" ]; then
|
|
echo '{"error":"no_credentials","session":null,"weekly":null}'
|
|
else
|
|
echo "❌ No Claude Code credentials found"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
TOKEN=$(echo "$CREDS" | grep -o '"accessToken":"[^"]*"' | sed 's/"accessToken":"//;s/"//')
|
|
REFRESH_TOKEN=$(echo "$CREDS" | grep -o '"refreshToken":"[^"]*"' | sed 's/"refreshToken":"//;s/"//')
|
|
EXPIRES_AT=$(echo "$CREDS" | grep -o '"expiresAt":[0-9]*' | sed 's/"expiresAt"://')
|
|
|
|
if [ -z "$TOKEN" ]; then
|
|
if [ "$FORMAT" = "json" ]; then
|
|
echo '{"error":"no_token","session":null,"weekly":null}'
|
|
else
|
|
echo "❌ Could not extract access token"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
# Check if token is expired and refresh if needed
|
|
if [ -n "$EXPIRES_AT" ]; then
|
|
NOW_MS=$(($(date +%s) * 1000))
|
|
if [ "$NOW_MS" -gt "$EXPIRES_AT" ]; then
|
|
# Token expired - trigger Claude CLI to auto-refresh
|
|
if command -v claude >/dev/null 2>&1; then
|
|
# Run a simple query to trigger token refresh
|
|
echo "2+2" | claude >/dev/null 2>&1 || true
|
|
|
|
# Reload credentials from keychain after refresh
|
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
CREDS=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || echo "")
|
|
else
|
|
if command -v secret-tool >/dev/null 2>&1; then
|
|
CREDS=$(secret-tool lookup application "Claude Code" 2>/dev/null || echo "")
|
|
fi
|
|
fi
|
|
|
|
if [ -n "$CREDS" ]; then
|
|
TOKEN=$(echo "$CREDS" | grep -o '"accessToken":"[^"]*"' | sed 's/"accessToken":"//;s/"//')
|
|
fi
|
|
else
|
|
if [ "$FORMAT" = "json" ]; then
|
|
echo '{"error":"token_expired","session":null,"weekly":null}'
|
|
else
|
|
echo "❌ OAuth token expired. Run 'claude' CLI to refresh."
|
|
fi
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Fetch usage from API
|
|
RESP=$(curl -s "https://api.anthropic.com/api/oauth/usage" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "anthropic-beta: oauth-2025-04-20" 2>/dev/null)
|
|
|
|
if [ -z "$RESP" ]; then
|
|
if [ "$FORMAT" = "json" ]; then
|
|
echo '{"error":"api_error","session":null,"weekly":null}'
|
|
else
|
|
echo "❌ API request failed"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
# Parse session (5-hour)
|
|
SESSION=$(echo "$RESP" | grep -o '"five_hour":{[^}]*}' | grep -o '"utilization":[0-9]*' | sed 's/.*://')
|
|
SESSION_RESET=$(echo "$RESP" | grep -o '"five_hour":{[^}]*}' | grep -o '"resets_at":"[^"]*"' | sed 's/"resets_at":"//;s/"//')
|
|
|
|
# Parse weekly (7-day)
|
|
WEEKLY=$(echo "$RESP" | grep -o '"seven_day":{[^}]*}' | grep -o '"utilization":[0-9]*' | sed 's/.*://')
|
|
WEEKLY_RESET=$(echo "$RESP" | grep -o '"seven_day":{[^}]*}' | grep -o '"resets_at":"[^"]*"' | sed 's/"resets_at":"//;s/"//')
|
|
|
|
SESSION=${SESSION:-0}
|
|
WEEKLY=${WEEKLY:-0}
|
|
|
|
# Calculate time until reset
|
|
NOW=$(date +%s)
|
|
|
|
if [ -n "$SESSION_RESET" ]; then
|
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
SESSION_TS=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${SESSION_RESET%Z}" +%s 2>/dev/null || echo 0)
|
|
else
|
|
SESSION_TS=$(date -d "${SESSION_RESET}" +%s 2>/dev/null || echo 0)
|
|
fi
|
|
SESSION_LEFT=$(secs_to_human $((SESSION_TS - NOW)))
|
|
else
|
|
SESSION_LEFT="unknown"
|
|
fi
|
|
|
|
if [ -n "$WEEKLY_RESET" ]; then
|
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
WEEKLY_TS=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${WEEKLY_RESET%Z}" +%s 2>/dev/null || echo 0)
|
|
else
|
|
WEEKLY_TS=$(date -d "${WEEKLY_RESET}" +%s 2>/dev/null || echo 0)
|
|
fi
|
|
WEEKLY_LEFT=$(secs_to_human $((WEEKLY_TS - NOW)))
|
|
else
|
|
WEEKLY_LEFT="unknown"
|
|
fi
|
|
|
|
# Output format
|
|
if [ "$FORMAT" = "json" ]; then
|
|
OUTPUT=$(cat <<EOF
|
|
{
|
|
"session": {
|
|
"utilization": $SESSION,
|
|
"resets_in": "$SESSION_LEFT",
|
|
"resets_at": "$SESSION_RESET"
|
|
},
|
|
"weekly": {
|
|
"utilization": $WEEKLY,
|
|
"resets_in": "$WEEKLY_LEFT",
|
|
"resets_at": "$WEEKLY_RESET"
|
|
},
|
|
"cached_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
}
|
|
EOF
|
|
)
|
|
else
|
|
# Beautiful text output with emojis
|
|
SESSION_BAR=""
|
|
WEEKLY_BAR=""
|
|
|
|
# Session progress bar
|
|
SESSION_FILLED=$((SESSION / 10))
|
|
SESSION_EMPTY=$((10 - SESSION_FILLED))
|
|
for ((i=0; i<SESSION_FILLED; i++)); do SESSION_BAR="${SESSION_BAR}█"; done
|
|
for ((i=0; i<SESSION_EMPTY; i++)); do SESSION_BAR="${SESSION_BAR}░"; done
|
|
|
|
# Weekly progress bar
|
|
WEEKLY_FILLED=$((WEEKLY / 10))
|
|
WEEKLY_EMPTY=$((10 - WEEKLY_FILLED))
|
|
for ((i=0; i<WEEKLY_FILLED; i++)); do WEEKLY_BAR="${WEEKLY_BAR}█"; done
|
|
for ((i=0; i<WEEKLY_EMPTY; i++)); do WEEKLY_BAR="${WEEKLY_BAR}░"; done
|
|
|
|
# Determine emoji based on usage level
|
|
if [ "$SESSION" -gt 80 ]; then
|
|
SESSION_EMOJI="🔴"
|
|
elif [ "$SESSION" -gt 50 ]; then
|
|
SESSION_EMOJI="🟡"
|
|
else
|
|
SESSION_EMOJI="🟢"
|
|
fi
|
|
|
|
if [ "$WEEKLY" -gt 80 ]; then
|
|
WEEKLY_EMOJI="🔴"
|
|
elif [ "$WEEKLY" -gt 50 ]; then
|
|
WEEKLY_EMOJI="🟡"
|
|
else
|
|
WEEKLY_EMOJI="🟢"
|
|
fi
|
|
|
|
OUTPUT=$(cat <<EOF
|
|
🦞 Claude Code Usage
|
|
|
|
⏱️ Session (5h): $SESSION_EMOJI $SESSION_BAR $SESSION%
|
|
Resets in: $SESSION_LEFT
|
|
|
|
📅 Weekly (7d): $WEEKLY_EMOJI $WEEKLY_BAR $WEEKLY%
|
|
Resets in: $WEEKLY_LEFT
|
|
EOF
|
|
)
|
|
fi
|
|
|
|
# Cache the output
|
|
echo "$OUTPUT" > "$CACHE_FILE"
|
|
echo "$OUTPUT"
|