- sbt: sesiuni tmux (sbx1..3) care intră în sandbox și lansează agentul (claude/opencode/orice binar); tmux rulează pe host, panes prin sbx exec - statusline.sh + bootstrap-statusline.sh pe /home/workspace/.agent (virtiofs persistent), reinstalat la fiecare intrare fiindcă /home/agent din sandbox e overlay efemer - fix printf "--" -> printf -- "--" (eroare de usage când API-ul de cote nu răspunde) - copii versionate ale scripturilor agents/sb + documentație README Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
216 lines
6.8 KiB
Bash
Executable File
216 lines
6.8 KiB
Bash
Executable File
#!/bin/bash
|
|
# Claude Code Statusline — real usage data from Anthropic OAuth API
|
|
# Format (3 lines):
|
|
# Model │ ctx 33K/1M 3% │ project (branch*)
|
|
# current ●●●●●○○○○○ 21% ↺ 2hr 15min
|
|
# weekly ●●●●○○○○○○ 36% ↺ Fri 6:59am
|
|
|
|
set -o pipefail
|
|
|
|
input=$(cat)
|
|
|
|
# Validate JSON
|
|
if ! echo "$input" | jq -e . >/dev/null 2>&1; then
|
|
printf "\033[90mLoading...\033[0m"
|
|
exit 0
|
|
fi
|
|
|
|
# --- Extract fields ---
|
|
model=$(echo "$input" | jq -r '.model.display_name // "?"')
|
|
workdir=$(echo "$input" | jq -r '.workspace.current_dir // ""')
|
|
project=$(basename "$workdir" 2>/dev/null) || project="?"
|
|
|
|
# Context window
|
|
ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0')
|
|
ctx_pct=$(printf "%.0f" "$ctx_pct" 2>/dev/null) || ctx_pct=0
|
|
[[ "$ctx_pct" =~ ^[0-9]+$ ]] || ctx_pct=0
|
|
|
|
ctx_size=$(echo "$input" | jq -r '.context_window.context_window_size // 0')
|
|
# Calculate used tokens from current_usage
|
|
ctx_input=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0')
|
|
ctx_output=$(echo "$input" | jq -r '.context_window.current_usage.output_tokens // 0')
|
|
ctx_cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
|
|
ctx_cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
|
|
ctx_used=$(( ctx_input + ctx_output + ctx_cache_create + ctx_cache_read ))
|
|
|
|
# Format tokens: 1000000 → "1M", 33000 → "33K"
|
|
fmt_tokens() {
|
|
local n=$1
|
|
if [ "$n" -ge 1000000 ]; then
|
|
printf "%dM" $(( n / 1000000 ))
|
|
elif [ "$n" -ge 1000 ]; then
|
|
printf "%dK" $(( n / 1000 ))
|
|
else
|
|
printf "%d" "$n"
|
|
fi
|
|
}
|
|
|
|
ctx_used_fmt=$(fmt_tokens "$ctx_used")
|
|
ctx_size_fmt=$(fmt_tokens "$ctx_size")
|
|
|
|
# Git branch with dirty marker
|
|
git_branch=""
|
|
if [ -n "$workdir" ] && [ -d "$workdir" ]; then
|
|
git_branch=$(cd "$workdir" 2>/dev/null && git -c gc.auto=0 rev-parse --abbrev-ref HEAD 2>/dev/null) || git_branch=""
|
|
if [ -n "$git_branch" ]; then
|
|
git_dirty=$(cd "$workdir" 2>/dev/null && git -c gc.auto=0 status --porcelain 2>/dev/null | head -1)
|
|
[ -n "$git_dirty" ] && git_branch="${git_branch}*"
|
|
fi
|
|
fi
|
|
|
|
# Session cost/duration not available in statusLine JSON input — skip
|
|
|
|
# --- Fetch real usage from Anthropic API (cached 60s) ---
|
|
USAGE_CACHE="/tmp/claude-statusline-usage.json"
|
|
USAGE_CACHE_AGE=60
|
|
|
|
fetch_usage() {
|
|
local creds_file="$HOME/.claude/.credentials.json"
|
|
if [ ! -f "$creds_file" ]; then
|
|
echo '{}' > "$USAGE_CACHE"
|
|
return
|
|
fi
|
|
local token
|
|
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$creds_file" 2>/dev/null)
|
|
if [ -z "$token" ]; then
|
|
echo '{}' > "$USAGE_CACHE"
|
|
return
|
|
fi
|
|
local result
|
|
result=$(curl -s --max-time 5 \
|
|
-H "Authorization: Bearer $token" \
|
|
-H "anthropic-beta: oauth-2025-04-20" \
|
|
-H "Content-Type: application/json" \
|
|
"https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
|
|
if echo "$result" | jq -e '.five_hour' >/dev/null 2>&1; then
|
|
echo "$result" > "$USAGE_CACHE"
|
|
else
|
|
echo '{}' > "$USAGE_CACHE"
|
|
fi
|
|
}
|
|
|
|
# Refresh cache if stale
|
|
if [ ! -f "$USAGE_CACHE" ]; then
|
|
fetch_usage
|
|
else
|
|
cache_age=$(( $(date +%s) - $(stat -c%Y "$USAGE_CACHE" 2>/dev/null || echo 0) ))
|
|
if [ "$cache_age" -gt "$USAGE_CACHE_AGE" ]; then
|
|
fetch_usage
|
|
fi
|
|
fi
|
|
|
|
# Parse usage data
|
|
five_hr_pct=$(jq -r '.five_hour.utilization // 0' "$USAGE_CACHE" 2>/dev/null)
|
|
five_hr_pct=$(printf "%.0f" "$five_hr_pct" 2>/dev/null) || five_hr_pct=0
|
|
five_hr_reset=$(jq -r '.five_hour.resets_at // ""' "$USAGE_CACHE" 2>/dev/null)
|
|
|
|
weekly_pct=$(jq -r '.seven_day.utilization // 0' "$USAGE_CACHE" 2>/dev/null)
|
|
weekly_pct=$(printf "%.0f" "$weekly_pct" 2>/dev/null) || weekly_pct=0
|
|
weekly_reset=$(jq -r '.seven_day.resets_at // ""' "$USAGE_CACHE" 2>/dev/null)
|
|
|
|
# Format reset time as local time "Fri 6:59am" style
|
|
fmt_reset() {
|
|
local iso_ts="$1"
|
|
if [ -z "$iso_ts" ] || [ "$iso_ts" = "null" ]; then
|
|
printf -- "--"
|
|
return
|
|
fi
|
|
# Convert ISO to epoch, then to local time
|
|
local epoch
|
|
epoch=$(date -d "$iso_ts" +%s 2>/dev/null)
|
|
if [ -n "$epoch" ]; then
|
|
TZ="Europe/Bucharest" date -d "@$epoch" "+%a %-I:%M%P" 2>/dev/null || printf -- "--"
|
|
else
|
|
printf -- "--"
|
|
fi
|
|
}
|
|
|
|
# Format time remaining until reset "2hr 15min"
|
|
fmt_remaining() {
|
|
local iso_ts="$1"
|
|
if [ -z "$iso_ts" ] || [ "$iso_ts" = "null" ]; then
|
|
printf -- "--"
|
|
return
|
|
fi
|
|
local epoch now_ts remaining
|
|
epoch=$(date -d "$iso_ts" +%s 2>/dev/null)
|
|
now_ts=$(date +%s)
|
|
if [ -n "$epoch" ] && [ "$epoch" -gt "$now_ts" ]; then
|
|
remaining=$(( epoch - now_ts ))
|
|
local rh rm
|
|
rh=$(( remaining / 3600 ))
|
|
rm=$(( (remaining % 3600) / 60 ))
|
|
if [ "$rh" -gt 24 ]; then
|
|
local rd
|
|
rd=$(( rh / 24 ))
|
|
rh=$(( rh % 24 ))
|
|
printf "%dd %dh" "$rd" "$rh"
|
|
elif [ "$rh" -gt 0 ]; then
|
|
printf "%dhr %dmin" "$rh" "$rm"
|
|
else
|
|
printf "%dmin" "$rm"
|
|
fi
|
|
else
|
|
printf "resetting"
|
|
fi
|
|
}
|
|
|
|
five_hr_reset_str=$(fmt_reset "$five_hr_reset")
|
|
five_hr_remain=$(fmt_remaining "$five_hr_reset")
|
|
weekly_reset_str=$(fmt_reset "$weekly_reset")
|
|
weekly_remain=$(fmt_remaining "$weekly_reset")
|
|
|
|
# Sonnet weekly
|
|
sonnet_pct=$(jq -r '.seven_day_sonnet.utilization // 0' "$USAGE_CACHE" 2>/dev/null)
|
|
sonnet_pct=$(printf "%.0f" "$sonnet_pct" 2>/dev/null) || sonnet_pct=0
|
|
|
|
# --- Dot bar builder ---
|
|
DOT_TOTAL=10
|
|
make_dots() {
|
|
local filled=$1 total=$2 dots="" i
|
|
for (( i=0; i<total; i++ )); do
|
|
if [ "$i" -lt "$filled" ]; then
|
|
dots="${dots}●"
|
|
else
|
|
dots="${dots}○"
|
|
fi
|
|
done
|
|
printf "%s" "$dots"
|
|
}
|
|
|
|
# --- BUILD LINES ---
|
|
|
|
# Line 1: Model │ ctx 33K/1M 3% │ project (branch*)
|
|
if [ -n "$git_branch" ]; then
|
|
proj_display="${project} (${git_branch})"
|
|
else
|
|
proj_display="${project}"
|
|
fi
|
|
line1=$(printf "%s │ ctx %s/%s %d%% │ %s" "$model" "$ctx_used_fmt" "$ctx_size_fmt" "$ctx_pct" "$proj_display")
|
|
|
|
# Line 2: current ●●○○○○○○○○ 21% ↺ 3hr │ weekly ●●●○○○○○○○ 36% ↺ Fri 6:00am │ sonnet 22%
|
|
current_filled=$(( five_hr_pct * DOT_TOTAL / 100 ))
|
|
current_dots=$(make_dots "$current_filled" "$DOT_TOTAL")
|
|
|
|
weekly_filled=$(( weekly_pct * DOT_TOTAL / 100 ))
|
|
weekly_dots=$(make_dots "$weekly_filled" "$DOT_TOTAL")
|
|
|
|
sonnet_reset=$(jq -r '.seven_day_sonnet.resets_at // ""' "$USAGE_CACHE" 2>/dev/null)
|
|
sonnet_reset_str=$(fmt_reset "$sonnet_reset")
|
|
|
|
# Build line 2
|
|
if [ "$sonnet_reset_str" != "$weekly_reset_str" ] && [ "$sonnet_reset_str" != "--" ]; then
|
|
line2=$(printf "current %s %d%% ↺ %s │ weekly %s %d%% ↺ %s │ sonnet %d%% ↺ %s" \
|
|
"$current_dots" "$five_hr_pct" "$five_hr_remain" \
|
|
"$weekly_dots" "$weekly_pct" "$weekly_reset_str" \
|
|
"$sonnet_pct" "$sonnet_reset_str")
|
|
else
|
|
line2=$(printf "current %s %d%% ↺ %s │ weekly %s %d%% ↺ %s │ sonnet %d%%" \
|
|
"$current_dots" "$five_hr_pct" "$five_hr_remain" \
|
|
"$weekly_dots" "$weekly_pct" "$weekly_reset_str" \
|
|
"$sonnet_pct")
|
|
fi
|
|
|
|
# --- Output ---
|
|
printf "%s\n%s" "$line1" "$line2"
|