bb735cad17
- goal_autogen.sh: genera 3 emojis representativos del objetivo (haiku) junto al goal+DoD, guardados en goals/<id>.json. - goal_tracker.sh: comando meta /rename (y rename:) para nombrar la terminal; se guarda en goals/<id>.json .rename. - commands/rename.md: slash command /rename. - statusline.sh: persiste el % de contexto por sesion en runtime/<id>.json para que FleetView lo muestre. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
364 lines
13 KiB
Bash
Executable File
364 lines
13 KiB
Bash
Executable File
#!/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
|
|
|
|
# Persistir el contexto por sesión en un sidecar para que fleetview (y otras
|
|
# herramientas) puedan mostrarlo sin tener este stdin. El statusline se re-ejecuta
|
|
# cada pocos segundos, así que el dato se mantiene fresco mientras la sesión vive.
|
|
if [ -n "$SESSION_ID" ]; then
|
|
RTDIR="$HOME/.claude/runtime"
|
|
mkdir -p "$RTDIR" 2>/dev/null
|
|
RTF="$RTDIR/${SESSION_ID}.json"
|
|
RTMP="${RTF}.tmp.$$"
|
|
if jq -n \
|
|
--argjson pct "${CONTEXT_PCT:-0}" \
|
|
--argjson used "${CONTEXT_USED:-0}" \
|
|
--argjson total "${CONTEXT_TOTAL:-200000}" \
|
|
'{ctx_pct:$pct, ctx_used:$used, ctx_total:$total}' > "$RTMP" 2>/dev/null; then
|
|
mv "$RTMP" "$RTF" 2>/dev/null
|
|
else
|
|
rm -f "$RTMP" 2>/dev/null
|
|
fi
|
|
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<filled; i++)); do printf "█"; done
|
|
printf "${GRAY}"
|
|
for ((i=0; i<empty; i++)); do printf "░"; done
|
|
printf "${RESET}"
|
|
}
|
|
|
|
# Función para formatear duración
|
|
format_duration() {
|
|
local ms=$1
|
|
local seconds=$((ms / 1000))
|
|
local minutes=$((seconds / 60))
|
|
local hours=$((minutes / 60))
|
|
|
|
if [ "$hours" -gt 0 ]; then
|
|
printf "%dh%dm" $hours $((minutes % 60))
|
|
elif [ "$minutes" -gt 0 ]; then
|
|
printf "%dm%ds" $minutes $((seconds % 60))
|
|
else
|
|
printf "%ds" $seconds
|
|
fi
|
|
}
|
|
|
|
# Función para formatear tokens (k para miles, M para millones)
|
|
format_tokens() {
|
|
local tokens=$1
|
|
if [ "$tokens" -ge 1000000 ]; then
|
|
printf "%.1fM" $(echo "scale=1; $tokens / 1000000" | bc)
|
|
elif [ "$tokens" -ge 1000 ]; then
|
|
printf "%.0fk" $(echo "scale=0; $tokens / 1000" | bc)
|
|
else
|
|
printf "%d" $tokens
|
|
fi
|
|
}
|
|
|
|
# Hora actual
|
|
CURRENT_TIME=$(date +"%H:%M")
|
|
|
|
# ===== CONSTRUIR STATUS LINE (2 LÍNEAS) =====
|
|
|
|
# ===== LÍNEA 1: Información Principal =====
|
|
LINE1=""
|
|
|
|
# 1. Modelo
|
|
LINE1="${LINE1}${BOLD}${CYAN}${MODEL}${RESET}"
|
|
|
|
# 2. Contexto con barra y usado/total
|
|
CONTEXT_USED_FMT=$(format_tokens $CONTEXT_USED)
|
|
CONTEXT_TOTAL_FMT=$(format_tokens $CONTEXT_TOTAL)
|
|
LINE1="${LINE1} ${GRAY}│${RESET} ${BOLD}CTX:${RESET} $(progress_bar $CONTEXT_PCT) ${YELLOW}${CONTEXT_PCT}%${RESET} ${DIM}(${CONTEXT_USED_FMT}/${CONTEXT_TOTAL_FMT})${RESET}"
|
|
|
|
# 3. Tokens entrada/salida (formateados)
|
|
INPUT_FMT=$(format_tokens $INPUT_TOKENS)
|
|
OUTPUT_FMT=$(format_tokens $OUTPUT_TOKENS)
|
|
LINE1="${LINE1} ${GRAY}│${RESET} ${CYAN}IN:${RESET}${INPUT_FMT} ${MAGENTA}OUT:${RESET}${OUTPUT_FMT}"
|
|
if [ "$CACHE_READ" -gt 0 ]; then
|
|
CACHE_FMT=$(format_tokens $CACHE_READ)
|
|
LINE1="${LINE1} ${DIM}(cache:${CACHE_FMT})${RESET}"
|
|
fi
|
|
|
|
# 4. Git (si existe)
|
|
if [ -n "$GIT_BRANCH" ]; then
|
|
LINE1="${LINE1} ${GRAY}│${RESET} ${BOLD}${MAGENTA}⎇ ${GIT_BRANCH}${RESET}"
|
|
if [ -n "$GIT_STATUS" ]; then
|
|
LINE1="${LINE1} ${DIM}[${GIT_STATUS}]${RESET}"
|
|
fi
|
|
fi
|
|
|
|
# 5. Hora actual
|
|
LINE1="${LINE1} ${GRAY}│${RESET} ${WHITE}${CURRENT_TIME}${RESET}"
|
|
|
|
# ===== LÍNEA 2: Detalles =====
|
|
LINE2=""
|
|
|
|
# 1. Costos
|
|
if (( $(echo "$TOTAL_COST > 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"
|