Files
esh-pfi-infrastructure/scripts/claude-statusline-command.sh
T
vh 22da609053 feat: registry-push the post office image; version the statusline
## Registry

The image moved by `docker save | ssh | docker load`, so a rebuild meant
repeating that by hand. It is now published and the compose pulls a
digest-pinned reference, so a redeploy is `compose up -d` on any host
that has logged in.

Pinned by digest rather than by tag: `:3.0.0` is a mutable pointer on a
registry anyone can re-push, and this container is the fleet's whole
message bus. The tag rides alongside so a human can read what it is.

Namespace is claude-bot, not vh. claude-bot's token carries
write:package and `docker login` succeeds, but package namespaces are
owned -- pushing to vh/ returns "unauthorized: authentication required"
after a successful login, which reads like a credential fault and is
actually an ownership one. Publishing under claude-bot's own namespace
also satisfies the standing directive to stop reusing the operator's
personal credentials for infra work, so the constraint and the policy
point the same way. Recorded in the compose header so the next person
does not read that error as a broken token.

Pull path proven rather than assumed: the running container was
recreated from the registry reference and its data verified afterwards.

## Statusline

Brought under version control because the v3 cutover broke it invisibly.
The segment gated on `command -v althing-cli`, a binary the cutover
deleted, so the unread badge and the armed bell silently vanished for
every session on the box. With 71 of 73 handles pull-only, that badge is
the only out-of-band signal telling a session with no armed waiter that
it has mail -- a dead statusline made a working bus look like an empty
one.

Canonical here, live at ~/.claude/statusline-command.sh, copies rather
than symlinks per the same rule as stacks/.
2026-08-28 10:08:05 -07:00

177 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# CANONICAL COPY of the Claude Code statusline. Deployed to (and read from):
#
# ~/.claude/statusline-command.sh <- the LIVE path CC actually runs
#
# Install / update after editing here:
# cp scripts/claude-statusline-command.sh ~/.claude/statusline-command.sh
#
# Copies, not symlinks — same rule as stacks/: this tree is intent, the live
# path is reality, and they diverge until someone deploys. Diff them with
# diff -u scripts/claude-statusline-command.sh ~/.claude/statusline-command.sh
#
# It is version-controlled here because the althing v3 cutover broke it in a way
# that was invisible: the segment gated on `command -v althing-cli`, a binary the
# cutover deleted, so the 📬 badge and 🔔 bell silently vanished for every session
# on the box. With 71 of 73 handles pull-only, that badge is the ONLY out-of-band
# signal telling a session with no armed waiter that it has mail — a dead
# statusline made a working bus look like an empty one.
#
# Smoke test (CC pipes session JSON on stdin):
# echo '{"model":{"display_name":"opus"},"workspace":{"current_dir":"/tmp"},"cwd":"/tmp"}' \
# | ALTHING_HANDLE=<a-handle-with-unread> bash scripts/claude-statusline-command.sh
# want: a leading `📬 N`; and with an unreachable post office, degradation in
# ~2s rather than a hang.
# Claude Code statusline. Layout:
# [📬N] [🔔/🔕] | <proj> ⎇<branch> *<dirty> ↑<unpushed> | <model> | ctx:<pct> <toks> | $<session-cost> | 5h:% 7d:%
# ctx% and rate-limit %s are threshold-colored: green <60, yellow 60-90, red >90.
# All segments degrade gracefully (missing tool / non-git dir / no handle => segment omitted).
input=$(cat)
# --- threshold color: $1=numeric pct, $2=display text -> colored text ---
color_pct() {
local p="$1" txt="$2" c
if awk "BEGIN{exit !($p < 60)}"; then c=$'\033[32m' # green <60
elif awk "BEGIN{exit !($p > 90)}"; then c=$'\033[31m' # red >90
else c=$'\033[33m' # yellow 60-90
fi
printf '%s%s\033[0m' "$c" "$txt"
}
# --- reset countdown: $1=unix ts -> "1d3h"/"3h20m"/"45m" (2-unit; "now"/empty edge) ---
reset_in() {
local ts="$1" now delta d h m
[ -z "$ts" ] && return
now=$(date +%s)
delta=$(( ts - now ))
[ "$delta" -le 0 ] && { printf 'now'; return; }
if [ "$delta" -ge 86400 ]; then
d=$(( delta / 86400 )); h=$(( (delta % 86400) / 3600 ))
if [ "$h" -gt 0 ]; then printf '%dd%dh' "$d" "$h"; else printf '%dd' "$d"; fi
elif [ "$delta" -ge 3600 ]; then
h=$(( delta / 3600 )); m=$(( (delta % 3600) / 60 ))
if [ "$m" -gt 0 ]; then printf '%dh%dm' "$h" "$m"; else printf '%dh' "$h"; fi
else
printf '%dm' $(( delta / 60 ))
fi
}
# --- one jq pass for every payload field ---
# \x1f (unit separator) delimiter, NOT tab: tab is IFS-whitespace so `read` would
# collapse consecutive tabs and shift every field after an empty one (e.g. a
# session with no rate_limits). \x1f is non-whitespace -> empty fields preserved.
IFS=$'\x1f' read -r model used_pct input_tok five_pct week_pct cwd fast cost_usd model_id five_reset week_reset < <(
printf '%s' "$input" | jq -r '[
(.model.display_name // "unknown"),
(.context_window.used_percentage // ""),
(.context_window.total_input_tokens // 0),
(.rate_limits.five_hour.used_percentage // ""),
(.rate_limits.seven_day.used_percentage // ""),
(.cwd // .workspace.current_dir // ""),
(.fast_mode // false),
(.cost.total_cost_usd // 0),
(.model.id // ""),
(.rate_limits.five_hour.resets_at // ""),
(.rate_limits.seven_day.resets_at // "")
] | map(tostring) | join("")'
)
[ -z "$model" ] && model="unknown"
# --- model (compact) + fast-mode flag ---
model="${model%% (*}" # "Opus 4.8 (1M context)" -> "Opus 4.8"
[ "$fast" = "true" ] && model="$model"
# --- context % (colored) + absolute input tokens ---
if [ -n "$used_pct" ]; then
ctx_seg=$(color_pct "$used_pct" "ctx:$(printf '%.0f%%' "$used_pct")")
else
ctx_seg="ctx:--"
fi
if [ "${input_tok:-0}" -ge 1000 ] 2>/dev/null; then
toks=$(awk "BEGIN{printf \"%.0fk\", ${input_tok}/1000}")
else
toks="${input_tok:-0}"
fi
# --- per-session cost (Claude Code's own cache/model-aware accounting) ---
# adaptive precision: whole dollars once it's real money, cents when small.
cost=$(awk "BEGIN{c=${cost_usd:-0}; if(c>=100) printf \"%.0f\",c; else if(c>=10) printf \"%.1f\",c; else printf \"%.2f\",c}")
# --- rate limits (each % colored on the same thresholds, + reset countdown) ---
_rl() { # $1=pct $2=label $3=reset_ts -> "<label>:NN%·<reset>"
local seg r; seg=$(color_pct "$1" "$2:$(printf '%.0f' "$1")%")
r=$(reset_in "$3"); [ -n "$r" ] && seg="$seg·$r"
printf '%s' "$seg"
}
rate=""
[ -n "$five_pct" ] && rate=$(_rl "$five_pct" "5h" "$five_reset")
if [ -n "$week_pct" ]; then
wk=$(_rl "$week_pct" "7d" "$week_reset")
[ -n "$rate" ] && rate="$rate "
rate="${rate}${wk}"
fi
# --- project tag + git state (branch, dirty, unpushed) ---
proj=""; gitseg=""
if [ -n "$cwd" ]; then
proj=$(basename "$cwd")
if git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
br=$(git -C "$cwd" branch --show-current 2>/dev/null)
[ -z "$br" ] && br=$(git -C "$cwd" rev-parse --short HEAD 2>/dev/null)
dirty=$(git -C "$cwd" status --porcelain 2>/dev/null | grep -c .)
ahead=$(git -C "$cwd" rev-list --count '@{upstream}..HEAD' 2>/dev/null)
gitseg="${br:-?}"
[ "${dirty:-0}" -gt 0 ] 2>/dev/null && gitseg="$gitseg *$dirty"
[ -n "$ahead" ] && [ "$ahead" -gt 0 ] 2>/dev/null && gitseg="$gitseg$ahead"
fi
fi
# --- althing: unread count (📬 N) + waiter-armed (🔔 armed / 🔕 not) ---
# v3 (the post office, 2026-08-28). ⚠ This block used to gate on
# `command -v althing-cli`, which the v3 cutover DELETED -- so the whole segment,
# badge and bell both, silently disappeared for every session on this box. That is
# worse than a cosmetic loss: 71 of 73 handles are pull-only (no waiter armed, never
# poked), and this badge is the ONLY out-of-band signal telling such a session it has
# mail waiting. A dead statusline made the new bus look like an empty one.
althing=""; mon=""
if command -v postbox >/dev/null 2>&1; then
# postbox has NO default post-office address and the statusline runs in a bare
# shell with neither var set. Hardcoded here deliberately: an unset address makes
# postbox error, which in a must-never-crash segment is indistinguishable from
# "no mail" -- the exact conflation v3 exists to prevent.
export ALTHING_POST_OFFICE="${ALTHING_POST_OFFICE:-http://10.100.50.40:8390}"
h="${ALTHING_HANDLE:-}"
# ALTHING_HANDLE isn't set in the statusline env — resolve the per-project handle
# from althing's session map, keyed by the cwd Claude Code passes on stdin.
[ -z "$h" ] && [ -n "$cwd" ] && h=$(jq -r --arg d "$cwd" '.[$d] // empty' "$HOME/.althing/session_handles.json" 2>/dev/null)
if [ -n "$h" ]; then
# `timeout` is load-bearing, not belt-and-braces: v2 read a local SQLite file,
# v3 makes an HTTP call. An unreachable post office must cost this segment two
# seconds and nothing else — a statusline that hangs blocks the whole prompt.
unread=$(timeout 2 postbox --handle "$h" status --json 2>/dev/null </dev/null | jq -r '.unread // 0' 2>/dev/null)
case "${unread:-0}" in ''|0|*[!0-9]*) : ;; *) althing="📬 $unread" ;; esac
# v3 has ONE arming mechanism where v2 had three: `althing-listen` takes
# wake-listener-<handle>.lock. monitor-*.lock and light-monitor-*.lock belonged
# to binaries that no longer exist. kill -0 discards a crashed listener's lock.
mon="🔕"
lk="$HOME/.althing/wake-listener-$h.lock"
if [ -f "$lk" ]; then
pid=$(tr -dc '0-9' < "$lk" 2>/dev/null)
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && mon="🔔"
fi
fi
fi
# --- assemble ---
lead="$althing"
[ -n "$mon" ] && lead="${lead:+$lead }$mon"
pg="$proj"
[ -n "$gitseg" ] && pg="${pg:+$pg }$gitseg"
parts="$lead"
[ -n "$pg" ] && parts="${parts:+$parts | }$pg"
parts="${parts:+$parts | }$model | $ctx_seg $toks | \$$cost"
[ -n "$rate" ] && parts="$parts | $rate"
printf '%s' "$parts"