#!/bin/bash # Status Line para Claude Code - Optimizado para Vibecoding # Muestra: modelo, contexto, git, costos, rate limits y más # Leer JSON desde stdin INPUT=$(cat) # Colores ANSI RESET='\033[0m' BOLD='\033[1m' DIM='\033[2m' CYAN='\033[36m' GREEN='\033[32m' YELLOW='\033[33m' RED='\033[31m' BLUE='\033[34m' MAGENTA='\033[35m' WHITE='\033[37m' GRAY='\033[90m' # Extraer datos del JSON MODEL=$(echo "$INPUT" | jq -r '.model.display_name // "Unknown"') CONTEXT_PCT=$(echo "$INPUT" | jq -r '.context_window.used_percentage // 0' | xargs printf "%.0f") CONTEXT_TOTAL=$(echo "$INPUT" | jq -r '.context_window.context_window_size // 200000') CURRENT_DIR=$(echo "$INPUT" | jq -r '.workspace.current_dir // "~"' | sed "s|$HOME|~|") SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""') # Purga: borra goal files de sesiones muertas (no tocados en >7 dias). El worker # refresca el mtime en cada respuesta, asi que las sesiones vivas nunca caen. find "$HOME/.claude/goals" -maxdepth 1 -name '*.json' -mtime +7 -delete 2>/dev/null # Tokens de entrada y salida (current_usage puede ser null antes del primer API call) INPUT_TOKENS=$(echo "$INPUT" | jq -r '.context_window.current_usage.input_tokens // 0') OUTPUT_TOKENS=$(echo "$INPUT" | jq -r '.context_window.current_usage.output_tokens // 0') CACHE_READ=$(echo "$INPUT" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0') CACHE_CREATION=$(echo "$INPUT" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0') TOTAL_INPUT=$(echo "$INPUT" | jq -r '.context_window.total_input_tokens // 0') TOTAL_OUTPUT=$(echo "$INPUT" | jq -r '.context_window.total_output_tokens // 0') # Calcular contexto usado (suma de todos los tokens de entrada) CONTEXT_USED=$((INPUT_TOKENS + CACHE_READ + CACHE_CREATION)) # Calcular porcentaje manualmente si el del JSON es 0 if [ "$CONTEXT_PCT" -eq 0 ] && [ "$CONTEXT_USED" -gt 0 ]; then CONTEXT_PCT=$(echo "scale=0; $CONTEXT_USED * 100 / $CONTEXT_TOTAL" | bc) fi # Costos TOTAL_COST=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // 0' | xargs printf "%.3f") SESSION_DURATION=$(echo "$INPUT" | jq -r '.cost.total_duration_ms // 0') LINES_ADDED=$(echo "$INPUT" | jq -r '.cost.total_lines_added // 0') LINES_REMOVED=$(echo "$INPUT" | jq -r '.cost.total_lines_removed // 0') # Rate Limits RATE_5H=$(echo "$INPUT" | jq -r '.rate_limits.five_hour.used_percentage // 0' | xargs printf "%.0f") RATE_7D=$(echo "$INPUT" | jq -r '.rate_limits.seven_day.used_percentage // 0' | xargs printf "%.0f") RESET_5H_EPOCH=$(echo "$INPUT" | jq -r '.rate_limits.five_hour.resets_at // 0') RESET_7D_EPOCH=$(echo "$INPUT" | jq -r '.rate_limits.seven_day.resets_at // 0') # Formatear resets (vacio si epoch=0) RESET_5H="" RESET_7D="" [ "$RESET_5H_EPOCH" -gt 0 ] && RESET_5H=$(date -d "@$RESET_5H_EPOCH" +"%H:%M" 2>/dev/null) [ "$RESET_7D_EPOCH" -gt 0 ] && RESET_7D=$(date -d "@$RESET_7D_EPOCH" +"%a %H:%M" 2>/dev/null) # Git info (si estamos en un repo). Con cache de TTL corto: como el statusline # se re-ejecuta cada pocos segundos (refreshInterval), recomputar git en cada # tick es caro en repos grandes y el estado git no cambia estando idle. Se cachea # por directorio y se recomputa solo si el cache tiene mas de 6s. GIT_BRANCH="" GIT_STATUS="" GIT_CACHE="/tmp/fn_sl_git_$(printf '%s' "$CURRENT_DIR" | cksum | cut -d' ' -f1).cache" GIT_CACHE_AGE=999 [ -f "$GIT_CACHE" ] && GIT_CACHE_AGE=$(( $(date +%s) - $(stat -c %Y "$GIT_CACHE" 2>/dev/null || echo 0) )) if [ "$GIT_CACHE_AGE" -lt 6 ]; then . "$GIT_CACHE" elif git rev-parse --git-dir > /dev/null 2>&1; then GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "detached") # Obtener archivos staged, modified, untracked STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l) MODIFIED=$(git diff --numstat 2>/dev/null | wc -l) UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l) # Commits ahead/behind UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null) if [ -n "$UPSTREAM" ]; then AHEAD=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo "0") BEHIND=$(git rev-list --count HEAD..@{u} 2>/dev/null || echo "0") else AHEAD="0" BEHIND="0" fi # Construir status compacto GIT_STATUS="" [ "$STAGED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}+${STAGED} " [ "$MODIFIED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}~${MODIFIED} " [ "$UNTRACKED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}?${UNTRACKED} " [ "$AHEAD" -gt 0 ] && GIT_STATUS="${GIT_STATUS}↑${AHEAD} " [ "$BEHIND" -gt 0 ] && GIT_STATUS="${GIT_STATUS}↓${BEHIND} " # Trim trailing space GIT_STATUS=$(echo "$GIT_STATUS" | sed 's/ $//') # Guardar en cache (quoting seguro para re-source). printf 'GIT_BRANCH=%q\nGIT_STATUS=%q\n' "$GIT_BRANCH" "$GIT_STATUS" > "$GIT_CACHE" 2>/dev/null fi # Color estable por sesión (hash del session_id → paleta ANSI 256 legible). # Cada terminal mantiene su color toda su vida; distinto entre terminales. goal_color() { local sid="$1" local palette=(39 45 51 75 81 114 120 156 183 210 215 222 213 159 228) local h h=$(printf '%s' "$sid" | cksum | cut -d' ' -f1) local idx=$(( h % ${#palette[@]} )) printf '\033[1;38;5;%dm' "${palette[$idx]}" } # Fase de trabajo → icono | color ANSI | etiqueta visible. # El slug (clave) lo escribe el agente del Stop hook; aqui se mapea a su estilo. phase_style() { case "$1" in investigando) printf '🔎|36|investigando' ;; planificando) printf '📋|34|planificando' ;; haciendo) printf '🔨|33|haciendo' ;; testeando) printf '🧪|35|testeando' ;; puliendo) printf '✨|95|puliendo detalles' ;; pendiente_revision) printf '👀|93|pendiente de revisión' ;; preguntando) printf '❓|96|esperando respuesta' ;; bloqueado) printf '⛔|31|bloqueado' ;; en_pausa) printf '⏸️|90|en pausa' ;; hecho) printf '✅|32|hecho' ;; iterando) printf '🔁|94|iterando' ;; *) printf "•|90|$1" ;; esac } # Función para crear barra de progreso progress_bar() { local pct=$1 local width=10 local filled=$(( pct * width / 100 )) local empty=$(( width - filled )) # Color según porcentaje local color=$GREEN if [ "$pct" -ge 80 ]; then color=$RED elif [ "$pct" -ge 60 ]; then color=$YELLOW fi printf "${color}" for ((i=0; i 0" | bc -l) )); then LINE2="${LINE2}${BOLD}${GREEN}\$${TOTAL_COST}${RESET}" else LINE2="${LINE2}${DIM}\$0.000${RESET}" fi # 2. Ediciones LINE2="${LINE2} ${GRAY}│${RESET} ${GREEN}+${LINES_ADDED}${RESET}${GRAY}/${RED}-${LINES_REMOVED}${RESET}" # 3. Tokens totales acumulados (formateados) TOTAL_IN_FMT=$(format_tokens $TOTAL_INPUT) TOTAL_OUT_FMT=$(format_tokens $TOTAL_OUTPUT) LINE2="${LINE2} ${GRAY}│${RESET} ${DIM}Total:${RESET} ${CYAN}↓${TOTAL_IN_FMT}${RESET}${GRAY}/${MAGENTA}↑${TOTAL_OUT_FMT}${RESET}" # 4. Rate Limits (siempre mostrar) LINE2="${LINE2} ${GRAY}│${RESET} ${BOLD}Limits:${RESET}" # Color por burndown vs tasa esperada # Tasa: % consumible permitido por unidad de tiempo (5h: 20%/h, 7d: 14%/dia) # expected = tasa * unidades_restantes_hasta_reset # available = 100 - used% # verde: available >= expected (consumo bajo control) # amarillo: available >= expected/2 (consumo agresivo) # rojo: available < expected/2 (riesgo de agotar antes del reset) NOW_EPOCH=$(date +%s) burndown_color() { local used_pct=$1 local secs_left=$2 local rate=$3 local secs_per_unit=$4 local available=$((100 - used_pct)) if [ "$secs_left" -le 0 ]; then printf "%s" "$GREEN"; return fi local expected expected=$(echo "scale=2; $rate * $secs_left / $secs_per_unit" | bc) if (( $(echo "$available >= $expected" | bc -l) )); then printf "%s" "$GREEN" elif (( $(echo "$available >= $expected / 2" | bc -l) )); then printf "%s" "$YELLOW" else printf "%s" "$RED" fi } # 5h limit (tasa 20%/h) SECS_5H=0 [ "$RESET_5H_EPOCH" -gt "$NOW_EPOCH" ] && SECS_5H=$((RESET_5H_EPOCH - NOW_EPOCH)) C5=$(burndown_color $RATE_5H $SECS_5H 20 3600) RESET_5H_STR="" [ -n "$RESET_5H" ] && RESET_5H_STR=" ${C5}→${RESET_5H}${RESET}" LINE2="${LINE2} ${C5}5h:${RATE_5H}%${RESET}${RESET_5H_STR} ${GRAY}│${RESET}" # 7d limit (tasa 14%/dia) SECS_7D=0 [ "$RESET_7D_EPOCH" -gt "$NOW_EPOCH" ] && SECS_7D=$((RESET_7D_EPOCH - NOW_EPOCH)) C7=$(burndown_color $RATE_7D $SECS_7D 14 86400) RESET_7D_STR="" [ -n "$RESET_7D" ] && RESET_7D_STR=" ${C7}→${RESET_7D}${RESET}" LINE2="${LINE2} ${C7}7d:${RATE_7D}%${RESET}${RESET_7D_STR}" # 5. Duración sesión if [ "$SESSION_DURATION" -gt 0 ]; then DURATION_STR=$(format_duration $SESSION_DURATION) LINE2="${LINE2} ${GRAY}│${RESET} ${DIM}⏱ ${DURATION_STR}${RESET}" fi # 6. Directorio actual LINE2="${LINE2} ${GRAY}│${RESET} ${BLUE}${CURRENT_DIR}${RESET}" # ===== LÍNEA 0: Objetivo (izq) + Fase (der) ===== # Solo si la sesión tiene archivo de objetivo con goal no vacío. GOAL_FILE="$HOME/.claude/goals/${SESSION_ID}.json" if [ -n "$SESSION_ID" ] && [ -f "$GOAL_FILE" ]; then GOAL=$(jq -r '.goal // ""' "$GOAL_FILE" 2>/dev/null) PHASE=$(jq -r '.phase // ""' "$GOAL_FILE" 2>/dev/null) DOD=$(jq -r '.dod // ""' "$GOAL_FILE" 2>/dev/null) if [ -n "$GOAL" ]; then GC=$(goal_color "$SESSION_ID") LEFT="${GC}🎯 ${GOAL}${RESET}" LINE0="${LEFT}" # Historial: emojis de los ultimos 7 estados PREVIOS (sin el actual, que # se muestra completo a la derecha), atenuados y separados por │. PREV=$(jq -r '(.history // []) | .[0:-1] | .[-7:] | .[]' "$GOAL_FILE" 2>/dev/null) if [ -n "$PREV" ]; then HJOIN="" while IFS= read -r slug; do [ -z "$slug" ] && continue HS=$(phase_style "$slug") HIC="${HS%%|*}" HJOIN="${HJOIN}${HIC}" done <<< "$PREV" [ -n "$HJOIN" ] && LINE0="${LINE0} ${GRAY}│${RESET} ${DIM}${HJOIN}${RESET}" fi # Fase actual (completa, con color e icono). if [ -n "$PHASE" ]; then PS=$(phase_style "$PHASE") PICON="${PS%%|*}" REST="${PS#*|}" PCOL="${REST%%|*}" PLABEL="${REST#*|}" LINE0="${LINE0} ${GRAY}│${RESET} \033[1;${PCOL}m${PICON} ${PLABEL}${RESET}" fi echo -e "$LINE0" # DoD en su propia linea debajo del objetivo, atenuado (🏁 = condicion de hecho). [ -n "$DOD" ] && echo -e " ${DIM}🏁 ${DOD}${RESET}" fi fi # Imprimir resultado (2 líneas) echo -e "$LINE1" echo -e "$LINE2"