Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Agent
e72c1f9f48 docs(lxc102): installer + procedură de replicare pentru sbt/statusline
- scripts/install-sbt.sh: instalare idempotentă (scripturi + statusline +
  patch settings.json) cu verificări de mediu
- README: replicare de la zero (transfer prin pct exec, installer, sandbox,
  ce se pierde la recrearea sandbox-ului), verificare rapidă, căi noi

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:38:40 +00:00
Claude Agent
0729010284 fix(lxc102): sbt nu mai lansează agent implicit + fix warning docker hub lock
- fără agent implicit: ambele pane-uri sunt shell în sandbox, agentul se
  pornește manual; argumentul 3 rămâne pentru lansare explicită
- al doilea pane intră în sandbox cu 4s întârziere (SBT_PANE_DELAY):
  două `sbx exec` simultane se calcă pe lock-ul de refresh Docker Hub și
  scot "could not acquire docker hub refresh lock"

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:35:55 +00:00
Claude Agent
6ff2c6ee2c feat(lxc102): script tmux sbt pentru sandbox + statusline Claude Code
- 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>
2026-07-31 11:13:55 +00:00
7 changed files with 615 additions and 0 deletions

View File

@@ -70,6 +70,137 @@ sbx daemon stop
---
## Scripturi de lucru (userul `work`)
Trei scripturi în `/home/work/.local/bin/`, copii versionate în `scripts/` din acest director:
| Script | Rol |
|--------|-----|
| `agents` | pornește sandbox-ul și îl ține pornit (keepalive); nu lansează niciun agent |
| `sb` | un shell interactiv în sandbox, într-un director de proiect |
| `sbt` | **sesiune tmux** cu sandbox + agent — echivalentul lui `start-agent.sh` de pe LXC 171 |
Instalare / reinstalare: `scripts/install-sbt.sh` — vezi „Replicare de la zero" mai jos.
### `sbt` — tmux + sandbox + agent
**Nu lansează niciun agent** — deschide shell-uri în sandbox, agentul îl pornești manual
(`claude`, `opencode`, `jcode`…).
```bash
sbt # sesiune 1, shell în /home/workspace
sbt 2 proiect-a # sesiune 2, shell în /home/workspace/proiect-a
sbt 1 proiect-a opencode # opțional: lansează explicit un agent în pane-ul stâng
sbt list | sbt kill 2 | sbt detach 2
```
Sesiunile se numesc `sbx1`, `sbx2`, `sbx3`. Layout: 2 pane-uri pe orizontală, ambele shell în
sandbox, în directorul proiectului. Dacă ceri explicit un agent (argumentul 3), acesta rulează
în pane-ul stâng și, la ieșire, pane-ul rămâne cu un shell (`exec bash -l`) — sesiunea nu moare.
**tmux rulează pe host (LXC 102), nu în sandbox** — în imaginea sandbox-ului nu există tmux
(și nici `git`). Fiecare pane e un `sbx exec -it -w <dir> <sandbox> bash -l`. Consecința utilă:
sesiunea supraviețuiește oricărei reporniri a agentului, iar `sbt` apelează întâi `agents up`,
deci sandbox-ul e pornit chiar dacă nu era.
Prefixul tmux e `Ctrl+A` (cu `Ctrl+B` ca secundar) — vezi `/home/work/.tmux.conf`.
**`WARN: could not acquire docker hub refresh lock … context deadline exceeded`** — apare când
două procese `sbx exec` pornesc simultan (cele două pane-uri) și se calcă pe lock-ul de refresh
al credențialelor Docker Hub. E inofensiv (`proceeding without cross-process lock`), dar zgomotos.
Reprodus determinist:
```bash
for i in 1 2 3; do sbx exec claude-workspace true & done; wait # 3 warning-uri
sbx exec claude-workspace true # curat
```
De aceea `sbt` pornește al doilea pane cu 4 secunde întârziere (`SBT_PANE_DELAY`) — la ≥4s
lock-ul e liber și warning-ul dispare. Atașarea nu e întârziată, doar intrarea în sandbox a
pane-ului din dreapta.
### Statusline Claude Code în sandbox
Același statusline ca pe LXC 171 (model, context, cote 5h / weekly / sonnet din API-ul OAuth
Anthropic). Două fișiere, ambele pe **`/home/workspace/.agent/`**, adică pe bind-mount-ul
virtiofs — singurul loc persistent văzut identic din host și din sandbox:
| Cale | Rol |
|------|-----|
| `/home/workspace/.agent/statusline.sh` | scriptul propriu-zis (rulat de Claude Code) |
| `/home/workspace/.agent/bootstrap-statusline.sh` | scrie `statusLine` în `~/.claude/settings.json` din sandbox |
`sbt` face `source` la bootstrap la fiecare intrare în sandbox. Este necesar pentru că
**`/home/agent` din sandbox stă pe overlay efemer** — `settings.json` se pierde la recrearea
sandbox-ului, în timp ce `/home/workspace` rămâne. Bootstrap-ul e idempotent și nu suprascrie
restul setărilor (merge cu `jq`).
Statusline-ul e activat și pentru `claude` rulat direct pe host ca `work`, prin aceeași cale.
**De reținut:**
- `git` lipsește din imaginea sandbox-ului, deci linia 1 nu arată branch-ul acolo (degradează curat).
- Datele de cotă vin de la `https://api.anthropic.com/api/oauth/usage`, cu token-ul din
`~/.claude/.credentials.json` **al sandbox-ului** (login separat de cel al userului `work`).
Cache 60s în `/tmp/claude-statusline-usage.json`. Dacă API-ul răspunde `rate_limit_error`,
barele arată `0% ↺ --` până la următoarea reîmprospătare — nu e o defecțiune de config.
### Replicare de la zero
Presupune sbx deja funcțional (vezi „Depanare" mai jos) și userul `work` în grupul `kvm`.
**1. Copiază `scripts/` din acest repo pe LXC 102.** LXC-ul nu are acces direct la repo, deci
transfer prin `pct exec` (rulat de pe LXC 171, unde e clonat repo-ul):
```bash
cd proxmox/lxc102-docker
tar czf - scripts | base64 -w0 | ssh root@10.0.20.201 \
"pct exec 102 -- bash -c 'rm -rf /tmp/sbt-install; mkdir -p /tmp/sbt-install;
cd /tmp/sbt-install && base64 -d | tar xzf - && chown -R work:work /tmp/sbt-install'"
```
**2. Rulează installer-ul ca `work`** (idempotent, poate fi rulat oricând la update):
```bash
ssh root@10.0.20.201 "pct exec 102 -- su - work -c 'cd /tmp/sbt-install/scripts && ./install-sbt.sh'"
```
Pune `sbt`/`agents`/`sb` în `~/.local/bin`, statusline-ul în `/home/workspace/.agent/`,
adaugă `statusLine` în `~/.claude/settings.json` al userului `work` și verifică
`tmux` / `sbx` / `jq` / PATH.
**3. Sandbox-ul.** Scripturile țintesc implicit sandbox-ul `claude-workspace` (numele rezultă din
agent + directorul de lucru, la `sbx create claude /home/workspace`). Alt sandbox se dă prin
variabila `SANDBOX`:
```bash
SANDBOX=alt-sandbox sbt 2 proiect-b
```
**4. După recrearea sandbox-ului** trebuie refăcut ce stă pe overlay-ul efemer `/home/agent`:
login-ul Claude Code (`~/.claude/.credentials.json`) și orice unelte instalate manual în
`~/.local/bin` din sandbox. Persistă doar `/home/workspace` (virtiofs) și volumele montate de sbx
peste `~/.claude/{projects,sessions,todos,skills,shell-snapshots,statsig}` — de aceea statusline-ul
stă în `/home/workspace/.agent/` și e re-legat de `sbt` la fiecare intrare.
### Verificare rapidă
```bash
# statusline randează în sandbox (fără a porni un agent)
sbx exec claude-workspace bash -lc '. /home/workspace/.agent/bootstrap-statusline.sh;
jq -c .statusLine ~/.claude/settings.json; claude --version'
# sesiune tmux fără terminal (test): panes trebuie să fie vii, cu prompt agent@claude-workspace
sbt 3 test-sbt; sleep 10
tmux list-panes -t sbx3 -F '#{pane_index} dead=#{pane_dead}'
tmux capture-pane -p -t sbx3:0.0 | tail -2
tmux kill-session -t sbx3; rm -rf /home/workspace/test-sbt
```
`open terminal failed: not a terminal` la final e normal când `sbt` e rulat neinteractiv —
sesiunea se creează oricum, doar atașarea eșuează.
---
## Cum funcționează sbx (relevant pentru depanare)
`sbx` **nu** folosește dockerd-ul de sistem. Are propriul containerd embedded, per-user, care pornește
@@ -201,3 +332,8 @@ vechi rămân afișate — ce contează e că nu mai cresc.
| `/usr/libexec/containerd-shim-nerdbox-v1` | shim microVM |
| `/usr/libexec/lib/libsailor.so` | bibliotecă VMM (sensibilă la versiunea de glibc) |
| `/home/work/.profile` | conține fix-ul de PATH pentru `mkfs.ext4` |
| `/home/work/.local/bin/{sbt,agents,sb}` | scripturile de lucru (sursa versionată: `scripts/`) |
| `/home/work/.tmux.conf` | prefix `Ctrl+A`, mouse on |
| `/home/workspace/` | bind-mount virtiofs — singurul spațiu persistent partajat cu sandbox-ul |
| `/home/workspace/.agent/statusline.sh` | statusline Claude Code (host + sandbox) |
| `/home/workspace/.agent/bootstrap-statusline.sh` | re-leagă statusline-ul în sandbox la fiecare intrare |

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Se asigură doar că sandbox-ul e pornit și RĂMÂNE pornit.
# Nu lansează niciun agent — agentul îl pornești tu, manual.
#
# agents pornește sandbox-ul + keepalive (idempotent)
# agents status arată starea
# agents stop oprește keepalive-ul și sandbox-ul
set -euo pipefail
SANDBOX="${SANDBOX:-claude-workspace}"
PIDFILE="$HOME/.cache/agents-keepalive-$SANDBOX.pid"
mkdir -p "$(dirname "$PIDFILE")"
running() { [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; }
case "${1:-up}" in
status)
running && echo "keepalive: activ (pid $(cat "$PIDFILE"))" || echo "keepalive: oprit"
sbx ls 2>/dev/null | grep -E "^(SANDBOX|$SANDBOX)" || true
;;
stop)
running && { kill "$(cat "$PIDFILE")" 2>/dev/null || true; rm -f "$PIDFILE"; echo "keepalive oprit"; }
sbx stop "$SANDBOX" 2>&1 | tail -1
;;
up)
sbx ls >/dev/null 2>&1 || true # repornește daemonul dacă e picat
sbx exec "$SANDBOX" true >/dev/null
if running; then
echo "sandbox '$SANDBOX' pornit; keepalive deja activ (pid $(cat "$PIDFILE"))"
else
nohup bash -c "while true; do sbx exec $SANDBOX sleep 86400 >/dev/null 2>&1; sleep 5; done" \
>/dev/null 2>&1 &
echo $! > "$PIDFILE"
echo "sandbox '$SANDBOX' pornit; keepalive activ (pid $!)"
fi
echo "intră cu: sb [proiect]"
;;
*) echo "utilizare: agents [up|status|stop]"; exit 1 ;;
esac

View File

@@ -0,0 +1,23 @@
# Se rulează cu `source` la intrarea în sandbox (din sbt).
# Reinstalează statusline-ul în ~/.claude/settings.json — necesar pentru că
# /home/agent stă pe overlay-ul efemer al sandbox-ului, în timp ce
# /home/workspace e virtiofs persistent.
# Idempotent, silențios, nu strică settings.json existent.
_sl_script="/home/workspace/.agent/statusline.sh"
_sl_cfg="$HOME/.claude/settings.json"
if [ -x "$_sl_script" ] && command -v jq >/dev/null 2>&1; then
[ -f "$_sl_cfg" ] || printf '{}\n' > "$_sl_cfg"
if [ "$(jq -r '.statusLine.command // ""' "$_sl_cfg" 2>/dev/null)" != "$_sl_script" ]; then
_sl_tmp=$(mktemp) || _sl_tmp=""
if [ -n "$_sl_tmp" ] && jq --arg c "$_sl_script" \
'.statusLine = {type: "command", command: $c}' "$_sl_cfg" > "$_sl_tmp" 2>/dev/null; then
mv "$_sl_tmp" "$_sl_cfg"
else
rm -f "$_sl_tmp"
fi
fi
fi
unset _sl_script _sl_cfg _sl_tmp

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Instalează pe LXC 102 (userul `work`) mediul tmux + sandbox + statusline:
# ~/.local/bin/{sbt,agents,sb}
# /home/workspace/.agent/{statusline.sh,bootstrap-statusline.sh}
# statusLine în ~/.claude/settings.json (pentru claude rulat pe host)
#
# Se rulează DIN directorul care conține scripturile (acest scripts/ copiat pe LXC 102):
# su - work -c 'cd /tmp/sbt-install && ./install-sbt.sh'
#
# Idempotent — poate fi rulat de câte ori e nevoie.
set -euo pipefail
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN="$HOME/.local/bin"
AGENT_DIR="/home/workspace/.agent"
CFG="$HOME/.claude/settings.json"
STATUSLINE="$AGENT_DIR/statusline.sh"
[ "$(id -un)" = "work" ] || echo "ATENȚIE: rulezi ca $(id -un), nu ca 'work'."
# --- 1. scripturile de pe host ---
mkdir -p "$BIN"
for f in sbt agents sb; do
install -m 755 "$SRC/$f" "$BIN/$f"
echo "instalat $BIN/$f"
done
# --- 2. statusline pe volumul partajat cu sandbox-ul ---
# /home/workspace e bind-mount virtiofs, singurul loc persistent văzut identic
# din host și din sandbox (/home/agent din sandbox e overlay efemer).
mkdir -p "$AGENT_DIR"
install -m 755 "$SRC/statusline.sh" "$AGENT_DIR/statusline.sh"
install -m 644 "$SRC/bootstrap-statusline.sh" "$AGENT_DIR/bootstrap-statusline.sh"
echo "instalat $AGENT_DIR/{statusline.sh,bootstrap-statusline.sh}"
# --- 3. statusline și pentru claude rulat direct pe host ---
mkdir -p "$(dirname "$CFG")"
[ -f "$CFG" ] || printf '{}\n' > "$CFG"
if [ "$(jq -r '.statusLine.command // ""' "$CFG" 2>/dev/null)" != "$STATUSLINE" ]; then
tmp=$(mktemp)
if jq --arg c "$STATUSLINE" '.statusLine = {type: "command", command: $c}' "$CFG" > "$tmp"; then
mv "$tmp" "$CFG"; echo "statusLine adăugat în $CFG"
else
rm -f "$tmp"; echo "EȘEC: $CFG nu e JSON valid — adaugă statusLine manual." >&2
fi
else
echo "statusLine deja configurat în $CFG"
fi
# --- 4. verificări ---
echo "--- verificări ---"
for c in tmux sbx jq; do
command -v "$c" >/dev/null && echo "ok: $c" || echo "LIPSĂ: $c"
done
bash -n "$BIN/sbt" && echo "ok: sintaxă sbt"
grep -q "$HOME/.local/bin" <<<"$PATH" && echo "ok: ~/.local/bin în PATH" \
|| echo "ATENȚIE: adaugă ~/.local/bin în PATH (~/.profile)"
echo
echo "Gata. Pornește sandbox-ul și o sesiune: agents up && sbt"

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Shell interactiv în sandbox, într-un director de proiect.
# Agentul îl pornești tu de la prompt: claude / opencode / altceva.
#
# sb shell în /home/workspace
# sb proiect-a shell în /home/workspace/proiect-a (îl creează dacă lipsește)
set -euo pipefail
SANDBOX="${SANDBOX:-claude-workspace}"
ROOT=/home/workspace
DIR="$ROOT${1:+/$1}"
[ -n "${1:-}" ] && mkdir -p "$DIR"
sbx ls >/dev/null 2>&1 || true
exec sbx exec -it -w "$DIR" "$SANDBOX" bash -l

128
proxmox/lxc102-docker/scripts/sbt Executable file
View File

@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# sbt — sesiune tmux cu shell/agent în Docker Sandbox (sbx).
#
# tmux rulează pe HOST (LXC 102, userul work); fiecare pane intră în sandbox
# prin `sbx exec -it`. În sandbox nu există tmux, deci sesiunea supraviețuiește
# oricărei reporniri a agentului.
#
# NU lansează niciun agent — deschide shell-uri în sandbox, agentul îl pornești tu.
#
# sbt sesiune 1, shell în /home/workspace
# sbt 2 proiect-a sesiune 2, shell în /home/workspace/proiect-a
# sbt 1 proiect-a opencode opțional: lansează explicit un agent în pane-ul stâng
# sbt list listează sesiunile tmux
# sbt kill [N] omoară sesiunea N
# sbt detach [N] detașează clienții de la sesiunea N
#
# Variabile: SANDBOX (implicit claude-workspace), SBT_PANE_DELAY (implicit 4s)
set -euo pipefail
SANDBOX="${SANDBOX:-claude-workspace}"
ROOT="/home/workspace"
BOOTSTRAP="$ROOT/.agent/bootstrap-statusline.sh"
usage() {
sed -n '2,/^set -/p' "$0" | grep '^#' | sed 's/^# \{0,1\}//'
exit "${1:-0}"
}
session_name() {
case "$1" in
1|2|3) echo "sbx$1" ;;
*) return 1 ;;
esac
}
# ---- Parsare argumente ----
ACTION="start"; NUM="1"; PROJ=""; AGENT="shell"
case "${1:-}" in
list)
tmux ls 2>/dev/null || echo "Nicio sesiune tmux activă."
exit 0
;;
kill|detach)
ACTION="$1"; NUM="${2:-1}"
;;
-h|--help|help)
usage
;;
1|2|3)
NUM="$1"; PROJ="${2:-}"; AGENT="${3:-shell}"
;;
"")
: # implicite
;;
*)
echo "Argument necunoscut: $1" >&2
usage 1
;;
esac
SESSION=$(session_name "$NUM") || { echo "Sesiune invalidă: $NUM (1, 2 sau 3)" >&2; exit 1; }
if [ "$ACTION" = "kill" ]; then
if tmux has-session -t "$SESSION" 2>/dev/null; then
tmux kill-session -t "$SESSION"; echo "Sesiune oprită: $SESSION"
else
echo "Sesiunea nu există: $SESSION"
fi
exit 0
fi
if [ "$ACTION" = "detach" ]; then
if tmux has-session -t "$SESSION" 2>/dev/null; then
tmux detach-client -s "$SESSION"; echo "Clienți detașați de la: $SESSION"
else
echo "Sesiunea nu există: $SESSION"
fi
exit 0
fi
# ---- Atașare dacă sesiunea există deja ----
TMUX_SOCK="/tmp/tmux-$(id -u)/default"
[ -e "$TMUX_SOCK" ] && ! tmux ls &>/dev/null && rm -f "$TMUX_SOCK"
if tmux has-session -t "$SESSION" 2>/dev/null; then
echo "Mă atașez la sesiunea existentă: $SESSION"
exec tmux attach-session -t "$SESSION"
fi
# ---- Sandbox pornit + keepalive (idempotent) ----
if command -v agents >/dev/null 2>&1; then
SANDBOX="$SANDBOX" agents up
else
sbx ls >/dev/null 2>&1 || true
sbx exec "$SANDBOX" true >/dev/null
fi
DIR="$ROOT${PROJ:+/$PROJ}"
[ -n "$PROJ" ] && mkdir -p "$DIR"
# ---- Comanda rulată în fiecare pane ----
# bootstrap-ul reinstalează statusline-ul în settings.json din sandbox
# (settings.json stă pe overlay-ul efemer, deci se pierde la recreare).
sandbox_pane() {
local inner="$1"
printf 'sbx exec -it -w %q %q bash -lc %q' "$DIR" "$SANDBOX" \
"[ -r '$BOOTSTRAP' ] && . '$BOOTSTRAP'; $inner exec bash -l"
}
case "$AGENT" in
shell|none|"") INNER="" ;;
claude) INNER="claude;" ;;
*) INNER="$AGENT;" ;;
esac
echo "Sesiune nouă: $SESSION (sandbox=$SANDBOX dir=$DIR agent=${AGENT:-shell})"
tmux new-session -d -s "$SESSION" -c "$ROOT" "$(sandbox_pane "$INNER")"
tmux set-option -t "$SESSION" -g mouse on
# Al doilea pane intră în sandbox cu întârziere: două `sbx exec` pornite simultan
# se calcă pe lock-ul de refresh Docker Hub al lui sbx și scot
# „could not acquire docker hub refresh lock". Warning inofensiv, dar zgomotos.
tmux split-window -h -t "$SESSION:0" -c "$ROOT" \
"sh -c $(printf '%q' "sleep ${SBT_PANE_DELAY:-4}; exec $(sandbox_pane "")")"
tmux select-pane -t "$SESSION:0.0"
exec tmux attach-session -t "$SESSION"

View File

@@ -0,0 +1,215 @@
#!/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"