22da609053
## 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/.
177 lines
8.0 KiB
Bash
Executable File
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"
|