Last active
May 12, 2026 01:16
-
-
Save captainpete/afccec338c4916f3a837f41450d268c2 to your computer and use it in GitHub Desktop.
ClaudeWatch statusline for Claude Code — model, cwd@branch, token usage, effort level, 5h/7d rate-limit bars with linear-pace deltas, Claude Code service status. Derived from daniel3303/ClaudeCodeStatusLine (MIT).
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 | |
| # ClaudeWatch statusline for Claude Code. | |
| # | |
| # Derived from ClaudeCodeStatusLine by @daniel3303: | |
| # https://github.com/daniel3303/ClaudeCodeStatusLine | |
| # The upstream project is MIT-licensed; see its LICENSE file for the | |
| # original copyright and permission notice. This derivative is | |
| # redistributed under the same MIT terms. | |
| # | |
| # Modifications for ClaudeWatch: | |
| # - Write resolved usage data to `$CLAUDE_CONFIG_DIR/claudewatch-usage.json` | |
| # on every render so the ClaudeWatch menu-bar app can consume it. | |
| # - Removed the upstream self-update check (version comparison against | |
| # the upstream GitHub releases and the "Update available" line). | |
| # | |
| # Output (single line): Model | tokens | %used | %remain | think | | |
| # 5h bar @reset | 7d bar @reset | extra | |
| set -f # disable globbing | |
| input=$(cat) | |
| if [ -z "$input" ]; then | |
| printf "Claude" | |
| exit 0 | |
| fi | |
| # ANSI colors matching oh-my-posh theme | |
| blue='\033[38;2;0;153;255m' | |
| orange='\033[38;2;255;176;85m' | |
| green='\033[38;2;0;160;0m' | |
| cyan='\033[38;2;46;149;153m' | |
| red='\033[38;2;255;85;85m' | |
| yellow='\033[38;2;230;200;0m' | |
| white='\033[38;2;220;220;220m' | |
| dim='\033[2m' | |
| reset='\033[0m' | |
| # Format token counts (e.g., 50k / 200k) | |
| format_tokens() { | |
| local num=$1 | |
| if [ "$num" -ge 1000000 ]; then | |
| awk "BEGIN {printf \"%.1fm\", $num / 1000000}" | |
| elif [ "$num" -ge 1000 ]; then | |
| awk "BEGIN {printf \"%.0fk\", $num / 1000}" | |
| else | |
| printf "%d" "$num" | |
| fi | |
| } | |
| # Format number with commas (e.g., 134,938) | |
| format_commas() { | |
| printf "%'d" "$1" | |
| } | |
| # Return color escape based on usage percentage | |
| # Usage: usage_color <pct> | |
| usage_color() { | |
| local pct=$1 | |
| if [ "$pct" -ge 90 ]; then echo "$red" | |
| elif [ "$pct" -ge 70 ]; then echo "$orange" | |
| elif [ "$pct" -ge 50 ]; then echo "$yellow" | |
| else echo "$green" | |
| fi | |
| } | |
| # Compute pace delta in seconds vs. the linear-target burn rate. | |
| # Positive = under pace (conservative); negative = ahead (aggressive). | |
| # Args: pct (may be float), reset_epoch, window_secs | |
| compute_pace_delta() { | |
| local pct=$1 reset_epoch=$2 window_secs=$3 | |
| { [ -z "$pct" ] || [ -z "$reset_epoch" ] || [ "$reset_epoch" = "null" ]; } && return 1 | |
| local now | |
| now=$(date +%s) | |
| awk -v now="$now" -v reset="$reset_epoch" -v p="$pct" -v w="$window_secs" \ | |
| 'BEGIN { | |
| elapsed = now - (reset - w); | |
| if (elapsed < 0) elapsed = 0; | |
| if (elapsed > w) elapsed = w; | |
| printf "%d", elapsed - (p * w / 100); | |
| }' | |
| } | |
| # Format pace seconds as ±H:MM (for 5h window; max ~5:00) | |
| format_pace_short() { | |
| local secs=$1 sign="+" | |
| if [ "$secs" -lt 0 ]; then sign="-"; secs=$(( -secs )); fi | |
| printf "%s%d:%02d" "$sign" $(( secs / 3600 )) $(( (secs % 3600) / 60 )) | |
| } | |
| # Format pace seconds as ±Xd HH:MM (or ±H:MM when <1 day) for 7d window | |
| format_pace_long() { | |
| local secs=$1 sign="+" | |
| if [ "$secs" -lt 0 ]; then sign="-"; secs=$(( -secs )); fi | |
| local d=$(( secs / 86400 )) | |
| local h=$(( (secs % 86400) / 3600 )) | |
| local m=$(( (secs % 3600) / 60 )) | |
| if [ "$d" -gt 0 ]; then | |
| printf "%s%dd %02d:%02d" "$sign" "$d" "$h" "$m" | |
| else | |
| printf "%s%d:%02d" "$sign" "$h" "$m" | |
| fi | |
| } | |
| # Format seconds-until-reset as compact relative time. | |
| # short: "Hh Mm" or "Mm" (5h window, minute resolution) | |
| # long: "Dd Hh" or "Hh" (7d window, hour resolution) | |
| format_time_until() { | |
| local secs=$1 style=$2 | |
| [ -z "$secs" ] && return | |
| [ "$secs" -lt 0 ] && secs=0 | |
| local d=$(( secs / 86400 )) | |
| local h=$(( (secs % 86400) / 3600 )) | |
| local m=$(( (secs % 3600) / 60 )) | |
| case "$style" in | |
| short) | |
| if [ "$h" -gt 0 ]; then printf "%dh %dm" "$h" "$m" | |
| else printf "%dm" "$m" | |
| fi | |
| ;; | |
| long) | |
| if [ "$d" -gt 0 ]; then printf "%dd %dh" "$d" "$h" | |
| else printf "%dh" "$h" | |
| fi | |
| ;; | |
| esac | |
| } | |
| # Color for pace delta: green when under pace, orange when ahead. | |
| pace_color() { | |
| if [ "$1" -lt 0 ]; then echo "$orange"; else echo "$green"; fi | |
| } | |
| # Resolve config directory: CLAUDE_CONFIG_DIR (set by alias) or default ~/.claude | |
| claude_config_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" | |
| # ===== Extract data from JSON ===== | |
| model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"') | |
| # Context window | |
| size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000') | |
| [ "$size" -eq 0 ] 2>/dev/null && size=200000 | |
| # Token usage | |
| input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0') | |
| cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0') | |
| cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0') | |
| current=$(( input_tokens + cache_create + cache_read )) | |
| used_tokens=$(format_tokens $current) | |
| total_tokens=$(format_tokens $size) | |
| if [ "$size" -gt 0 ]; then | |
| pct_used=$(( current * 100 / size )) | |
| else | |
| pct_used=0 | |
| fi | |
| pct_remain=$(( 100 - pct_used )) | |
| used_comma=$(format_commas $current) | |
| remain_comma=$(format_commas $(( size - current ))) | |
| # Check reasoning effort | |
| settings_path="$claude_config_dir/settings.json" | |
| effort_level="medium" | |
| if [ -n "$CLAUDE_CODE_EFFORT_LEVEL" ]; then | |
| effort_level="$CLAUDE_CODE_EFFORT_LEVEL" | |
| elif [ -f "$settings_path" ]; then | |
| effort_val=$(jq -r '.effortLevel // empty' "$settings_path" 2>/dev/null) | |
| [ -n "$effort_val" ] && effort_level="$effort_val" | |
| fi | |
| # ===== Build single-line output ===== | |
| out="" | |
| out+="${blue}${model_name}${reset}" | |
| # Current working directory | |
| cwd=$(echo "$input" | jq -r '.cwd // empty') | |
| if [ -n "$cwd" ]; then | |
| display_dir="${cwd##*/}" | |
| git_branch=$(git -C "${cwd}" rev-parse --abbrev-ref HEAD 2>/dev/null) | |
| out+=" ${dim}|${reset} " | |
| out+="${cyan}${display_dir}${reset}" | |
| if [ -n "$git_branch" ]; then | |
| out+="${dim}@${reset}${green}${git_branch}${reset}" | |
| git_stat=$(git -C "${cwd}" diff --numstat 2>/dev/null | awk '{a+=$1; d+=$2} END {if (a+d>0) printf "+%d -%d", a, d}') | |
| [ -n "$git_stat" ] && out+=" ${dim}(${reset}${green}${git_stat%% *}${reset} ${red}${git_stat##* }${reset}${dim})${reset}" | |
| fi | |
| fi | |
| out+=" ${dim}|${reset} " | |
| out+="${orange}${used_tokens}/${total_tokens}${reset} ${dim}(${reset}${green}${pct_used}%${reset}${dim})${reset}" | |
| out+=" ${dim}|${reset} " | |
| out+="effort: " | |
| case "$effort_level" in | |
| low) out+="${dim}${effort_level}${reset}" ;; | |
| medium) out+="${orange}med${reset}" ;; | |
| max) out+="${red}${effort_level}${reset}" ;; | |
| *) out+="${green}${effort_level}${reset}" ;; | |
| esac | |
| # ===== Cross-platform OAuth token resolution (from statusline.sh) ===== | |
| # Tries credential sources in order: env var → macOS Keychain → Linux creds file → GNOME Keyring | |
| get_oauth_token() { | |
| local token="" | |
| # 1. Explicit env var override | |
| if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then | |
| echo "$CLAUDE_CODE_OAUTH_TOKEN" | |
| return 0 | |
| fi | |
| # 2. macOS Keychain (Claude Code appends a SHA256 hash of CLAUDE_CONFIG_DIR to the service name) | |
| if command -v security >/dev/null 2>&1; then | |
| local keychain_svc="Claude Code-credentials" | |
| if [ -n "$CLAUDE_CONFIG_DIR" ]; then | |
| local dir_hash | |
| dir_hash=$(echo -n "$CLAUDE_CONFIG_DIR" | shasum -a 256 | cut -c1-8) | |
| keychain_svc="Claude Code-credentials-${dir_hash}" | |
| fi | |
| local blob | |
| blob=$(security find-generic-password -s "$keychain_svc" -w 2>/dev/null) | |
| if [ -n "$blob" ]; then | |
| token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) | |
| if [ -n "$token" ] && [ "$token" != "null" ]; then | |
| echo "$token" | |
| return 0 | |
| fi | |
| fi | |
| fi | |
| # 3. Linux credentials file | |
| local creds_file="${claude_config_dir}/.credentials.json" | |
| if [ -f "$creds_file" ]; then | |
| token=$(jq -r '.claudeAiOauth.accessToken // empty' "$creds_file" 2>/dev/null) | |
| if [ -n "$token" ] && [ "$token" != "null" ]; then | |
| echo "$token" | |
| return 0 | |
| fi | |
| fi | |
| # 4. GNOME Keyring via secret-tool | |
| if command -v secret-tool >/dev/null 2>&1; then | |
| local blob | |
| blob=$(timeout 2 secret-tool lookup service "Claude Code-credentials" 2>/dev/null) | |
| if [ -n "$blob" ]; then | |
| token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null) | |
| if [ -n "$token" ] && [ "$token" != "null" ]; then | |
| echo "$token" | |
| return 0 | |
| fi | |
| fi | |
| fi | |
| echo "" | |
| } | |
| # ===== LINE 2 & 3: Usage limits with progress bars ===== | |
| # First, try to use rate_limits data provided directly by Claude Code in the JSON input. | |
| # This is the most reliable source — no OAuth token or API call required. | |
| builtin_five_hour_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') | |
| builtin_five_hour_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty') | |
| builtin_seven_day_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty') | |
| builtin_seven_day_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty') | |
| use_builtin=false | |
| if [ -n "$builtin_five_hour_pct" ] || [ -n "$builtin_seven_day_pct" ]; then | |
| use_builtin=true | |
| fi | |
| # Fall back to cached API call only when Claude Code didn't supply rate_limits data | |
| claude_config_dir_hash=$(echo -n "$claude_config_dir" | shasum -a 256 2>/dev/null || echo -n "$claude_config_dir" | sha256sum 2>/dev/null) | |
| claude_config_dir_hash=$(echo "$claude_config_dir_hash" | cut -c1-8) | |
| cache_file="/tmp/claude/statusline-usage-cache-${claude_config_dir_hash}.json" | |
| cache_max_age=60 # seconds between API calls | |
| mkdir -p /tmp/claude | |
| needs_refresh=true | |
| usage_data="" | |
| # Check cache — shared across all Claude Code instances to avoid rate limits. | |
| # We always read the cached API response (even when use_builtin=true) because | |
| # extra_usage only comes from the API, not from Claude Code's rate_limits input. | |
| if [ -f "$cache_file" ] && [ -s "$cache_file" ]; then | |
| cache_mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null) | |
| now=$(date +%s) | |
| cache_age=$(( now - cache_mtime )) | |
| if [ "$cache_age" -lt "$cache_max_age" ]; then | |
| needs_refresh=false | |
| fi | |
| usage_data=$(cat "$cache_file" 2>/dev/null) | |
| fi | |
| # When rate_limits are provided inline by Claude Code we don't need a fresh | |
| # fetch just for 5h/7d, but we do refresh so extra_usage stays current. | |
| if $needs_refresh; then | |
| touch "$cache_file" # stampede lock: prevent parallel panes from fetching simultaneously | |
| token=$(get_oauth_token) | |
| if [ -n "$token" ] && [ "$token" != "null" ]; then | |
| response=$(curl -s --max-time 10 \ | |
| -H "Accept: application/json" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer $token" \ | |
| -H "anthropic-beta: oauth-2025-04-20" \ | |
| -H "User-Agent: claude-code/2.1.34" \ | |
| "https://api.anthropic.com/api/oauth/usage" 2>/dev/null) | |
| # Only cache valid usage responses (not error/rate-limit JSON) | |
| if [ -n "$response" ] && echo "$response" | jq -e '.five_hour' >/dev/null 2>&1; then | |
| usage_data="$response" | |
| echo "$response" > "$cache_file" | |
| fi | |
| fi | |
| fi | |
| # Cross-platform ISO to epoch conversion | |
| # Converts ISO 8601 timestamp (e.g. "2025-06-15T12:30:00Z" or "2025-06-15T12:30:00.123+00:00") to epoch seconds. | |
| # Properly handles UTC timestamps and converts to local time. | |
| iso_to_epoch() { | |
| local iso_str="$1" | |
| # Try GNU date first (Linux) — handles ISO 8601 format automatically | |
| local epoch | |
| epoch=$(date -d "${iso_str}" +%s 2>/dev/null) | |
| if [ -n "$epoch" ]; then | |
| echo "$epoch" | |
| return 0 | |
| fi | |
| # BSD date (macOS) - handle various ISO 8601 formats | |
| local stripped="${iso_str%%.*}" # Remove fractional seconds (.123456) | |
| stripped="${stripped%%Z}" # Remove trailing Z | |
| stripped="${stripped%%+*}" # Remove timezone offset (+00:00) | |
| stripped="${stripped%%-[0-9][0-9]:[0-9][0-9]}" # Remove negative timezone offset | |
| # Check if timestamp is UTC (has Z or +00:00 or -00:00) | |
| if [[ "$iso_str" == *"Z"* ]] || [[ "$iso_str" == *"+00:00"* ]] || [[ "$iso_str" == *"-00:00"* ]]; then | |
| # For UTC timestamps, parse with timezone set to UTC | |
| epoch=$(env TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null) | |
| else | |
| epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null) | |
| fi | |
| if [ -n "$epoch" ]; then | |
| echo "$epoch" | |
| return 0 | |
| fi | |
| return 1 | |
| } | |
| # Format ISO reset time to compact local time | |
| # Usage: format_reset_time <iso_string> <style: time|datetime|date> | |
| format_reset_time() { | |
| local iso_str="$1" | |
| local style="$2" | |
| { [ -z "$iso_str" ] || [ "$iso_str" = "null" ]; } && return | |
| # Parse ISO datetime and convert to local time (cross-platform) | |
| local epoch | |
| epoch=$(iso_to_epoch "$iso_str") | |
| [ -z "$epoch" ] && return | |
| # Format based on style | |
| # Try GNU date first (Linux), then BSD date (macOS) | |
| # Previous implementation piped BSD date through sed/tr, which always returned | |
| # exit code 0 from the last pipe stage, preventing the GNU date fallback from | |
| # ever executing on Linux. | |
| local formatted="" | |
| case "$style" in | |
| time) | |
| formatted=$(date -d "@$epoch" +"%H:%M" 2>/dev/null) || \ | |
| formatted=$(date -j -r "$epoch" +"%H:%M" 2>/dev/null) | |
| ;; | |
| datetime) | |
| formatted=$(date -d "@$epoch" +"%b %-d, %H:%M" 2>/dev/null) || \ | |
| formatted=$(date -j -r "$epoch" +"%b %-d, %H:%M" 2>/dev/null) | |
| ;; | |
| *) | |
| formatted=$(date -d "@$epoch" +"%b %-d" 2>/dev/null) || \ | |
| formatted=$(date -j -r "$epoch" +"%b %-d" 2>/dev/null) | |
| ;; | |
| esac | |
| [ -n "$formatted" ] && echo "$formatted" | |
| } | |
| # ===== Write usage data for ClaudeWatch ===== | |
| claudewatch_file="$claude_config_dir/claudewatch-usage.json" | |
| # Coerces a value to a JSON-safe number literal or "null". | |
| # --argjson aborts jq if passed a non-numeric string, which would silently | |
| # nuke the entire claudewatch-usage.json write (including the 5h/7d fields). | |
| numeric_or_null() { | |
| case "$1" in | |
| ''|null) echo null ;; | |
| *[!0-9.+-]*) echo null ;; | |
| *) echo "$1" ;; | |
| esac | |
| } | |
| cw_five_pct="" | |
| cw_five_reset="" | |
| cw_seven_pct="" | |
| cw_seven_reset="" | |
| cw_extra_enabled="false" | |
| cw_extra_pct="" | |
| cw_extra_used_credits="" | |
| cw_extra_monthly_limit="" | |
| if $use_builtin; then | |
| cw_five_pct="$builtin_five_hour_pct" | |
| cw_five_reset="$builtin_five_hour_reset" | |
| cw_seven_pct="$builtin_seven_day_pct" | |
| cw_seven_reset="$builtin_seven_day_reset" | |
| fi | |
| if [ -n "$usage_data" ] && echo "$usage_data" | jq -e '.five_hour' >/dev/null 2>&1; then | |
| if ! $use_builtin; then | |
| cw_five_pct=$(echo "$usage_data" | jq -r '.five_hour.utilization // empty') | |
| cw_seven_pct=$(echo "$usage_data" | jq -r '.seven_day.utilization // empty') | |
| cw_five_reset_iso=$(echo "$usage_data" | jq -r '.five_hour.resets_at // empty') | |
| cw_seven_reset_iso=$(echo "$usage_data" | jq -r '.seven_day.resets_at // empty') | |
| [ -n "$cw_five_reset_iso" ] && [ "$cw_five_reset_iso" != "null" ] && cw_five_reset=$(iso_to_epoch "$cw_five_reset_iso") | |
| [ -n "$cw_seven_reset_iso" ] && [ "$cw_seven_reset_iso" != "null" ] && cw_seven_reset=$(iso_to_epoch "$cw_seven_reset_iso") | |
| fi | |
| cw_extra_enabled=$(echo "$usage_data" | jq -r '.extra_usage.is_enabled // false') | |
| if [ "$cw_extra_enabled" = "true" ]; then | |
| cw_extra_pct=$(echo "$usage_data" | jq -r '.extra_usage.utilization // empty') | |
| cw_extra_used_credits=$(echo "$usage_data" | jq -r '.extra_usage.used_credits // empty') | |
| cw_extra_monthly_limit=$(echo "$usage_data" | jq -r '.extra_usage.monthly_limit // empty') | |
| fi | |
| fi | |
| if [ -n "$cw_five_pct" ] || [ -n "$cw_seven_pct" ] || [ "$cw_extra_enabled" = "true" ]; then | |
| case "$cw_extra_enabled" in true|false) ;; *) cw_extra_enabled=false ;; esac | |
| jq -n \ | |
| --argjson five_hour_pct "$(numeric_or_null "$cw_five_pct")" \ | |
| --argjson five_hour_reset "$(numeric_or_null "$cw_five_reset")" \ | |
| --argjson seven_day_pct "$(numeric_or_null "$cw_seven_pct")" \ | |
| --argjson seven_day_reset "$(numeric_or_null "$cw_seven_reset")" \ | |
| --argjson extra_enabled "$cw_extra_enabled" \ | |
| --argjson extra_pct "$(numeric_or_null "$cw_extra_pct")" \ | |
| --argjson extra_used_credits "$(numeric_or_null "$cw_extra_used_credits")" \ | |
| --argjson extra_monthly_limit "$(numeric_or_null "$cw_extra_monthly_limit")" \ | |
| --argjson updated_at "$(date +%s)" \ | |
| '{ | |
| five_hour: { used_percentage: $five_hour_pct, resets_at: $five_hour_reset }, | |
| seven_day: { used_percentage: $seven_day_pct, resets_at: $seven_day_reset }, | |
| extra_usage: { | |
| is_enabled: $extra_enabled, | |
| used_percentage: $extra_pct, | |
| used_credits: $extra_used_credits, | |
| monthly_limit: $extra_monthly_limit | |
| }, | |
| updated_at: $updated_at | |
| }' > "$claudewatch_file" 2>/dev/null | |
| fi | |
| sep=" ${dim}|${reset} " | |
| if $use_builtin; then | |
| # ---- Use rate_limits data provided directly by Claude Code in JSON input ---- | |
| # resets_at values are Unix epoch integers in this source | |
| if [ -n "$builtin_five_hour_pct" ]; then | |
| five_hour_pct=$(printf "%.0f" "$builtin_five_hour_pct") | |
| five_hour_color=$(usage_color "$five_hour_pct") | |
| out+="${sep}${white}5h${reset} ${five_hour_color}${five_hour_pct}%${reset}" | |
| if [ "$five_hour_pct" -lt 100 ] && [ -n "$builtin_five_hour_reset" ] && [ "$builtin_five_hour_reset" != "null" ]; then | |
| pace_secs=$(compute_pace_delta "$builtin_five_hour_pct" "$builtin_five_hour_reset" 18000) | |
| if [ -n "$pace_secs" ]; then | |
| pace_clr=$(pace_color "$pace_secs") | |
| out+=" ${dim}(${reset}${pace_clr}$(format_pace_short "$pace_secs")${reset}${dim})${reset}" | |
| fi | |
| fi | |
| if [ -n "$builtin_five_hour_reset" ] && [ "$builtin_five_hour_reset" != "null" ]; then | |
| now_secs=$(date +%s) | |
| time_until=$(format_time_until "$(( builtin_five_hour_reset - now_secs ))" short) | |
| [ -n "$time_until" ] && out+=" ${dim}→${time_until}${reset}" | |
| fi | |
| fi | |
| if [ -n "$builtin_seven_day_pct" ]; then | |
| seven_day_pct=$(printf "%.0f" "$builtin_seven_day_pct") | |
| seven_day_color=$(usage_color "$seven_day_pct") | |
| out+="${sep}${white}7d${reset} ${seven_day_color}${seven_day_pct}%${reset}" | |
| if [ "$seven_day_pct" -lt 100 ] && [ -n "$builtin_seven_day_reset" ] && [ "$builtin_seven_day_reset" != "null" ]; then | |
| pace_secs=$(compute_pace_delta "$builtin_seven_day_pct" "$builtin_seven_day_reset" 604800) | |
| if [ -n "$pace_secs" ]; then | |
| pace_clr=$(pace_color "$pace_secs") | |
| out+=" ${dim}(${reset}${pace_clr}$(format_pace_long "$pace_secs")${reset}${dim})${reset}" | |
| fi | |
| fi | |
| if [ -n "$builtin_seven_day_reset" ] && [ "$builtin_seven_day_reset" != "null" ]; then | |
| now_secs=$(date +%s) | |
| time_until=$(format_time_until "$(( builtin_seven_day_reset - now_secs ))" long) | |
| [ -n "$time_until" ] && out+=" ${dim}→${time_until}${reset}" | |
| fi | |
| fi | |
| elif [ -n "$usage_data" ] && echo "$usage_data" | jq -e '.five_hour' >/dev/null 2>&1; then | |
| # ---- Fall back: API-fetched usage data ---- | |
| # ---- 5-hour (current) ---- | |
| five_hour_pct_raw=$(echo "$usage_data" | jq -r '.five_hour.utilization // 0') | |
| five_hour_pct=$(echo "$five_hour_pct_raw" | awk '{printf "%.0f", $1}') | |
| five_hour_reset_iso=$(echo "$usage_data" | jq -r '.five_hour.resets_at // empty') | |
| five_hour_reset_epoch=$(iso_to_epoch "$five_hour_reset_iso") | |
| five_hour_color=$(usage_color "$five_hour_pct") | |
| out+="${sep}${white}5h${reset} ${five_hour_color}${five_hour_pct}%${reset}" | |
| if [ "$five_hour_pct" -lt 100 ] && [ -n "$five_hour_reset_epoch" ]; then | |
| pace_secs=$(compute_pace_delta "$five_hour_pct_raw" "$five_hour_reset_epoch" 18000) | |
| if [ -n "$pace_secs" ]; then | |
| pace_clr=$(pace_color "$pace_secs") | |
| out+=" ${dim}(${reset}${pace_clr}$(format_pace_short "$pace_secs")${reset}${dim})${reset}" | |
| fi | |
| fi | |
| if [ -n "$five_hour_reset_epoch" ]; then | |
| now_secs=$(date +%s) | |
| time_until=$(format_time_until "$(( five_hour_reset_epoch - now_secs ))" short) | |
| [ -n "$time_until" ] && out+=" ${dim}→${time_until}${reset}" | |
| fi | |
| # ---- 7-day (weekly) ---- | |
| seven_day_pct_raw=$(echo "$usage_data" | jq -r '.seven_day.utilization // 0') | |
| seven_day_pct=$(echo "$seven_day_pct_raw" | awk '{printf "%.0f", $1}') | |
| seven_day_reset_iso=$(echo "$usage_data" | jq -r '.seven_day.resets_at // empty') | |
| seven_day_reset_epoch=$(iso_to_epoch "$seven_day_reset_iso") | |
| seven_day_color=$(usage_color "$seven_day_pct") | |
| out+="${sep}${white}7d${reset} ${seven_day_color}${seven_day_pct}%${reset}" | |
| if [ "$seven_day_pct" -lt 100 ] && [ -n "$seven_day_reset_epoch" ]; then | |
| pace_secs=$(compute_pace_delta "$seven_day_pct_raw" "$seven_day_reset_epoch" 604800) | |
| if [ -n "$pace_secs" ]; then | |
| pace_clr=$(pace_color "$pace_secs") | |
| out+=" ${dim}(${reset}${pace_clr}$(format_pace_long "$pace_secs")${reset}${dim})${reset}" | |
| fi | |
| fi | |
| if [ -n "$seven_day_reset_epoch" ]; then | |
| now_secs=$(date +%s) | |
| time_until=$(format_time_until "$(( seven_day_reset_epoch - now_secs ))" long) | |
| [ -n "$time_until" ] && out+=" ${dim}→${time_until}${reset}" | |
| fi | |
| # ---- Extra usage ---- | |
| extra_enabled=$(echo "$usage_data" | jq -r '.extra_usage.is_enabled // false') | |
| if [ "$extra_enabled" = "true" ]; then | |
| extra_pct=$(echo "$usage_data" | jq -r '.extra_usage.utilization // 0' | awk '{printf "%.0f", $1}') | |
| extra_used=$(echo "$usage_data" | jq -r '.extra_usage.used_credits // 0' | LC_NUMERIC=C awk '{printf "%.2f", $1/100}') | |
| extra_limit=$(echo "$usage_data" | jq -r '.extra_usage.monthly_limit // 0' | LC_NUMERIC=C awk '{printf "%.2f", $1/100}') | |
| # Validate: if values are empty or contain unexpanded variables, show simple "enabled" label | |
| if [ -n "$extra_used" ] && [ -n "$extra_limit" ] && [[ "$extra_used" != *'$'* ]] && [[ "$extra_limit" != *'$'* ]]; then | |
| extra_color=$(usage_color "$extra_pct") | |
| out+="${sep}${white}extra${reset} ${extra_color}\$${extra_used}/\$${extra_limit}${reset}" | |
| else | |
| out+="${sep}${white}extra${reset} ${green}enabled${reset}" | |
| fi | |
| fi | |
| else | |
| # No valid usage data — show placeholders | |
| out+="${sep}${white}5h${reset} ${dim}-${reset}" | |
| out+="${sep}${white}7d${reset} ${dim}-${reset}" | |
| fi | |
| # ===== Claude Code Status (from summary API, cached 5min TTL) ===== | |
| # Shows the Claude Code component status as the primary label, and appends | |
| # the page-wide rollup in parentheses when it disagrees — so broad Anthropic | |
| # incidents (API, console, etc.) that aren't yet scoped to the Claude Code | |
| # component on the status page still surface in the statusline. | |
| status_cache_file="/tmp/claude/statusline-service-status-cache.json" | |
| status_cache_max_age=300 # 5 minutes | |
| status_needs_refresh=true | |
| status_data="" | |
| if [ -f "$status_cache_file" ] && [ -s "$status_cache_file" ]; then | |
| sc_mtime=$(stat -c %Y "$status_cache_file" 2>/dev/null || stat -f %m "$status_cache_file" 2>/dev/null) | |
| sc_now=$(date +%s) | |
| sc_age=$(( sc_now - sc_mtime )) | |
| if [ "$sc_age" -lt "$status_cache_max_age" ]; then | |
| status_needs_refresh=false | |
| fi | |
| status_data=$(cat "$status_cache_file" 2>/dev/null) | |
| fi | |
| if $status_needs_refresh; then | |
| touch "$status_cache_file" 2>/dev/null | |
| status_response=$(curl -s --max-time 5 "https://status.claude.com/api/v2/summary.json" 2>/dev/null) | |
| if [ -n "$status_response" ] && echo "$status_response" | jq -e '.components and .status.indicator' >/dev/null 2>&1; then | |
| status_data="$status_response" | |
| echo "$status_response" > "$status_cache_file" | |
| fi | |
| fi | |
| if [ -n "$status_data" ]; then | |
| cc_status=$(echo "$status_data" | jq -r '.components[] | select(.name == "Claude Code") | .status // empty') | |
| overall_indicator=$(echo "$status_data" | jq -r '.status.indicator // empty') | |
| cc_color=""; cc_label="" | |
| case "$cc_status" in | |
| operational) cc_color="$green"; cc_label="ok" ;; | |
| degraded_performance) cc_color="$yellow"; cc_label="degraded" ;; | |
| partial_outage) cc_color="$orange"; cc_label="partial" ;; | |
| major_outage) cc_color="$red"; cc_label="major" ;; | |
| under_maintenance) cc_color="$yellow"; cc_label="maintenance" ;; | |
| "") ;; | |
| *) cc_color="$dim"; cc_label="$cc_status" ;; | |
| esac | |
| overall_color=""; overall_label="" | |
| case "$overall_indicator" in | |
| none) overall_color="$green"; overall_label="ok" ;; | |
| minor) overall_color="$yellow"; overall_label="degraded" ;; | |
| major) overall_color="$orange"; overall_label="major" ;; | |
| critical) overall_color="$red"; overall_label="critical" ;; | |
| "") ;; | |
| *) overall_color="$dim"; overall_label="$overall_indicator" ;; | |
| esac | |
| if [ -n "$cc_label" ]; then | |
| out+="${sep}${white}status${reset} ${cc_color}${cc_label}${reset}" | |
| if [ -n "$overall_label" ] && [ "$overall_label" != "$cc_label" ]; then | |
| out+=" ${dim}(overall:${reset} ${overall_color}${overall_label}${dim})${reset}" | |
| fi | |
| elif [ -n "$overall_label" ]; then | |
| # Component missing but rollup present — show overall as fallback. | |
| out+="${sep}${white}status${reset} ${dim}(overall:${reset} ${overall_color}${overall_label}${dim})${reset}" | |
| fi | |
| fi | |
| # Output | |
| printf "%b" "$out" | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment