Created
May 8, 2026 13:31
-
-
Save mickaelxd/d7d4b70475e5440ca7be3c5356de6d3d to your computer and use it in GitHub Desktop.
AI Usage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Required parameters: | |
| # @raycast.schemaVersion 1 | |
| # @raycast.title AI Usage | |
| # @raycast.mode fullOutput | |
| # Optional parameters: | |
| # @raycast.icon 📊 | |
| # @raycast.packageName AI Usage | |
| # @raycast.description Shows Claude Code, Codex, and Copilot real-time usage limits and reset times | |
| # ────────────────────────────────────── | |
| # Helpers | |
| # ────────────────────────────────────── | |
| bar() { | |
| local pct_remaining=$1 | |
| local width=20 | |
| local used=$(( (100 - pct_remaining) * width / 100 )) | |
| local empty=$(( width - used )) | |
| printf '%s%s' "$(printf '%*s' "$used" '' | tr ' ' '█')" "$(printf '%*s' "$empty" '' | tr ' ' '░')" | |
| } | |
| icon() { | |
| local pct=$1 | |
| if [ "$pct" -le 10 ]; then echo "🔴" | |
| elif [ "$pct" -le 30 ]; then echo "🟡" | |
| else echo "🟢" | |
| fi | |
| } | |
| fmt_secs() { | |
| local s=$1 | |
| [ "$s" -le 0 ] && echo "now" && return | |
| local h=$(( s / 3600 )) m=$(( (s % 3600) / 60 )) | |
| if [ "$h" -gt 23 ]; then | |
| local d=$(( h / 24 )) rh=$(( h % 24 )) | |
| [ "$rh" -gt 0 ] && echo "${d}d ${rh}h" || echo "${d}d" | |
| elif [ "$h" -gt 0 ]; then | |
| [ "$m" -gt 0 ] && echo "${h}h ${m}m" || echo "${h}h" | |
| else echo "${m}m" | |
| fi | |
| } | |
| clock_from_secs() { | |
| local reset_epoch=$(( $(date +%s) + $1 )) | |
| date -r "$reset_epoch" '+%H:%M' 2>/dev/null | |
| } | |
| iso_to_reset() { | |
| local iso=$1 | |
| [ -z "$iso" ] || [ "$iso" = "null" ] && return | |
| local clean="${iso%%Z*}"; clean="${clean%%+*}"; clean="${clean%%.*}" | |
| local epoch | |
| epoch=$(TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$clean" +%s 2>/dev/null) | |
| [ -z "$epoch" ] && return | |
| local diff=$(( epoch - $(date +%s) )) | |
| [ "$diff" -le 0 ] && echo "now" && return | |
| echo "$(fmt_secs "$diff") ($(date -r "$epoch" '+%H:%M'))" | |
| } | |
| # Collect rows: "agent|window|remaining_pct|reset_str|extra" | |
| ROWS=() | |
| add_row() { ROWS+=("$1|$2|$3|$4|$5"); } | |
| CLAUDE_FILE=$(mktemp) | |
| CODEX_FILE=$(mktemp) | |
| COPILOT_FILE=$(mktemp) | |
| trap "rm -f $CLAUDE_FILE $CODEX_FILE $COPILOT_FILE" EXIT | |
| # ────────────────────────────────────── | |
| # Claude: token from Keychain | |
| # ────────────────────────────────────── | |
| fetch_claude() { | |
| local raw | |
| raw=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null) | |
| [ -z "$raw" ] && echo '{"error":"No credentials. Run claude to auth."}' > "$CLAUDE_FILE" && return | |
| local json | |
| echo "$raw" | grep -q '^{' && json="$raw" || json=$(echo "$raw" | xxd -r -p 2>/dev/null) | |
| local token | |
| token=$(echo "$json" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) | |
| [ -z "$token" ] && echo '{"error":"No OAuth token in credentials."}' > "$CLAUDE_FILE" && return | |
| token="${token#Bearer }"; token="${token#bearer }" | |
| curl -s --max-time 10 \ | |
| -H "Authorization: Bearer $token" \ | |
| -H "Accept: application/json" \ | |
| -H "Content-Type: application/json" \ | |
| -H "anthropic-beta: oauth-2025-04-20" \ | |
| "https://api.anthropic.com/api/oauth/usage" > "$CLAUDE_FILE" 2>/dev/null | |
| } | |
| # ────────────────────────────────────── | |
| # Codex: token from auth.json | |
| # ────────────────────────────────────── | |
| fetch_codex() { | |
| local token | |
| token=$(jq -r '.tokens.access_token // empty' ~/.codex/auth.json 2>/dev/null) | |
| [ -z "$token" ] && echo '{"error":"No token. Run codex login."}' > "$CODEX_FILE" && return | |
| curl -s --max-time 10 \ | |
| -H "Authorization: Bearer $token" \ | |
| -H "Accept: application/json" \ | |
| -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \ | |
| "https://chatgpt.com/backend-api/wham/usage" > "$CODEX_FILE" 2>/dev/null | |
| } | |
| # ────────────────────────────────────── | |
| # Copilot: token from gh / env | |
| # ────────────────────────────────────── | |
| fetch_copilot() { | |
| local token="${GITHUB_TOKEN:-${GH_TOKEN:-}}" | |
| [ -z "$token" ] && token=$(gh auth token 2>/dev/null) | |
| [ -z "$token" ] && echo '{"error":"No GH token. Set GITHUB_TOKEN or run gh auth login."}' > "$COPILOT_FILE" && return | |
| curl -s --max-time 10 \ | |
| -H "Authorization: token $token" \ | |
| -H "Accept: application/json" \ | |
| -H "Editor-Version: vscode/1.96.2" \ | |
| -H "Editor-Plugin-Version: copilot-chat/0.26.7" \ | |
| -H "User-Agent: GitHubCopilotChat/0.26.7" \ | |
| -H "X-Github-Api-Version: 2025-04-01" \ | |
| "https://api.github.com/copilot_internal/user" > "$COPILOT_FILE" 2>/dev/null | |
| } | |
| # ────────────────────────────────────── | |
| # Fetch all in parallel | |
| # ────────────────────────────────────── | |
| fetch_claude & | |
| fetch_codex & | |
| fetch_copilot & | |
| wait | |
| # ────────────────────────────────────── | |
| # Parse Claude | |
| # ────────────────────────────────────── | |
| claude_err=$(jq -r '.error // empty' "$CLAUDE_FILE" 2>/dev/null) | |
| if [ -n "$claude_err" ]; then | |
| add_row "Claude" "—" "-1" "⚠ $claude_err" "" | |
| else | |
| c5_util=$(jq -r '.five_hour.utilization // 0' "$CLAUDE_FILE") | |
| c5_reset=$(jq -r '.five_hour.resets_at // empty' "$CLAUDE_FILE") | |
| c5_rem=$(( 100 - ${c5_util%.*} )) | |
| [ "$c5_rem" -lt 0 ] && c5_rem=0; [ "$c5_rem" -gt 100 ] && c5_rem=100 | |
| add_row "Claude" "Session" "$c5_rem" "$(iso_to_reset "$c5_reset")" "" | |
| c7_util=$(jq -r '.seven_day.utilization // empty' "$CLAUDE_FILE") | |
| c7_reset=$(jq -r '.seven_day.resets_at // empty' "$CLAUDE_FILE") | |
| if [ -n "$c7_util" ] && [ "$c7_util" != "null" ]; then | |
| c7_rem=$(( 100 - ${c7_util%.*} )) | |
| [ "$c7_rem" -lt 0 ] && c7_rem=0; [ "$c7_rem" -gt 100 ] && c7_rem=100 | |
| add_row "Claude" "Weekly" "$c7_rem" "$(iso_to_reset "$c7_reset")" "" | |
| fi | |
| fi | |
| # ────────────────────────────────────── | |
| # Parse Codex | |
| # ────────────────────────────────────── | |
| codex_err=$(jq -r '.error // .detail // empty' "$CODEX_FILE" 2>/dev/null) | |
| if [ -n "$codex_err" ]; then | |
| add_row "Codex" "—" "-1" "⚠ $codex_err" "" | |
| else | |
| cx_plan=$(jq -r '.plan_type // ""' "$CODEX_FILE") | |
| cx5_used=$(jq -r '.rate_limit.primary_window.used_percent // 0' "$CODEX_FILE") | |
| cx5_secs=$(jq -r '.rate_limit.primary_window.reset_after_seconds // 0' "$CODEX_FILE") | |
| cx5_rem=$(( 100 - ${cx5_used%.*} )) | |
| [ "$cx5_rem" -lt 0 ] && cx5_rem=0; [ "$cx5_rem" -gt 100 ] && cx5_rem=100 | |
| cx5_reset="$(fmt_secs "${cx5_secs%.*}") ($(clock_from_secs "${cx5_secs%.*}"))" | |
| add_row "Codex" "Session" "$cx5_rem" "$cx5_reset" "" | |
| cxw_used=$(jq -r '.rate_limit.secondary_window.used_percent // 0' "$CODEX_FILE") | |
| cxw_secs=$(jq -r '.rate_limit.secondary_window.reset_after_seconds // 0' "$CODEX_FILE") | |
| cxw_rem=$(( 100 - ${cxw_used%.*} )) | |
| [ "$cxw_rem" -lt 0 ] && cxw_rem=0; [ "$cxw_rem" -gt 100 ] && cxw_rem=100 | |
| add_row "Codex" "Weekly" "$cxw_rem" "$(fmt_secs "${cxw_secs%.*}")" "" | |
| fi | |
| # ────────────────────────────────────── | |
| # Parse Copilot | |
| # ────────────────────────────────────── | |
| copilot_err=$(jq -r '.error // .message // empty' "$COPILOT_FILE" 2>/dev/null) | |
| if [ -n "$copilot_err" ]; then | |
| add_row "Copilot" "—" "-1" "⚠ $copilot_err" "" | |
| else | |
| cp_plan=$(jq -r '.copilot_plan // ""' "$COPILOT_FILE") | |
| cp_reset_date=$(jq -r '.quota_reset_date // ""' "$COPILOT_FILE") | |
| # Convert YYYY-MM-DD to relative time | |
| cp_reset_rel="" | |
| if [ -n "$cp_reset_date" ] && [ "$cp_reset_date" != "null" ]; then | |
| cp_epoch=$(date -j -f "%Y-%m-%d" "$cp_reset_date" +%s 2>/dev/null) | |
| if [ -n "$cp_epoch" ]; then | |
| cp_diff=$(( cp_epoch - $(date +%s) )) | |
| [ "$cp_diff" -gt 0 ] && cp_reset_rel=$(fmt_secs "$cp_diff") || cp_reset_rel="now" | |
| fi | |
| fi | |
| # Premium interactions | |
| cp_prem=$(jq -r '.quota_snapshots.premium_interactions.percent_remaining // empty' "$COPILOT_FILE" 2>/dev/null) | |
| if [ -z "$cp_prem" ]; then | |
| cp_ent=$(jq -r '.monthly_quotas.completions // empty' "$COPILOT_FILE" 2>/dev/null) | |
| cp_lim=$(jq -r '.limited_user_quotas.completions // empty' "$COPILOT_FILE" 2>/dev/null) | |
| if [ -n "$cp_ent" ] && [ -n "$cp_lim" ] && [ "$cp_ent" != "0" ]; then | |
| cp_prem=$(echo "scale=0; $cp_lim * 100 / $cp_ent" | bc 2>/dev/null) | |
| fi | |
| fi | |
| if [ -n "$cp_prem" ]; then | |
| cp_prem_int=${cp_prem%.*} | |
| [ "$cp_prem_int" -lt 0 ] 2>/dev/null && cp_prem_int=0 | |
| [ "$cp_prem_int" -gt 100 ] 2>/dev/null && cp_prem_int=100 | |
| add_row "Copilot" "Premium" "$cp_prem_int" "$cp_reset_rel" "" | |
| fi | |
| # Chat | |
| cp_chat=$(jq -r '.quota_snapshots.chat.percent_remaining // empty' "$COPILOT_FILE" 2>/dev/null) | |
| if [ -z "$cp_chat" ]; then | |
| cp_cent=$(jq -r '.monthly_quotas.chat // empty' "$COPILOT_FILE" 2>/dev/null) | |
| cp_clim=$(jq -r '.limited_user_quotas.chat // empty' "$COPILOT_FILE" 2>/dev/null) | |
| if [ -n "$cp_cent" ] && [ -n "$cp_clim" ] && [ "$cp_cent" != "0" ]; then | |
| cp_chat=$(echo "scale=0; $cp_clim * 100 / $cp_cent" | bc 2>/dev/null) | |
| fi | |
| fi | |
| if [ -n "$cp_chat" ]; then | |
| cp_chat_int=${cp_chat%.*} | |
| [ "$cp_chat_int" -lt 0 ] 2>/dev/null && cp_chat_int=0 | |
| [ "$cp_chat_int" -gt 100 ] 2>/dev/null && cp_chat_int=100 | |
| add_row "Copilot" "Chat" "$cp_chat_int" "$cp_reset_rel" "" | |
| fi | |
| # If no rows added for copilot, show error | |
| if [ -z "$cp_prem" ] && [ -z "$cp_chat" ]; then | |
| add_row "Copilot" "—" "-1" "⚠ No quota data in response" "" | |
| fi | |
| fi | |
| # ────────────────────────────────────── | |
| # Render table | |
| # ────────────────────────────────────── | |
| echo "" | |
| echo " AI Usage — $(date '+%d %b %Y, %H:%M')" | |
| echo "" | |
| printf " %-10s %-9s %-22s %5s %s\n" "Agent" "Window" "" "Left" "Resets" | |
| echo " ──────────────────────────────────────────────────────────────────" | |
| prev_agent="" | |
| for row in "${ROWS[@]}"; do | |
| IFS='|' read -r agent window pct reset extra <<< "$row" | |
| # Separator between different agents | |
| if [ -n "$prev_agent" ] && [ "$agent" != "$prev_agent" ]; then | |
| echo " ──────────────────────────────────────────────────────────────────" | |
| fi | |
| prev_agent="$agent" | |
| if [ "$pct" = "-1" ]; then | |
| # Error row | |
| printf " %-10s %s\n" "$agent" "$reset" | |
| else | |
| local_icon=$(icon "$pct") | |
| local_bar=$(bar "$pct") | |
| printf " %-10s %-9s %s %s %4s%% %s\n" "$agent" "$window" "$local_icon" "$local_bar" "$pct" "$reset" | |
| fi | |
| done | |
| echo " ──────────────────────────────────────────────────────────────────" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment