diff --git a/proxmox/lxc102-docker/README.md b/proxmox/lxc102-docker/README.md index a52b39d..2cebbae 100644 --- a/proxmox/lxc102-docker/README.md +++ b/proxmox/lxc102-docker/README.md @@ -70,6 +70,64 @@ 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 | + +### `sbt` — tmux + sandbox + agent + +```bash +sbt # sesiune 1, /home/workspace, claude în pane-ul stâng +sbt 2 proiect-a # sesiune 2, /home/workspace/proiect-a +sbt 1 proiect-a opencode # alt agent (orice binar din sandbox: opencode, jcode…) +sbt 1 proiect-a shell # doar shell, fără agent +sbt list | sbt kill 2 | sbt detach 2 +``` + +Sesiunile se numesc `sbx1`, `sbx2`, `sbx3`. Layout: 2 pane-uri pe orizontală — stânga cu +agentul, dreapta shell. Când agentul se oprește, pane-ul rămâne cu un shell în sandbox +(`exec bash -l`), deci 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 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`. + +### 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. + +--- + ## Cum funcționează sbx (relevant pentru depanare) `sbx` **nu** folosește dockerd-ul de sistem. Are propriul containerd embedded, per-user, care pornește diff --git a/proxmox/lxc102-docker/scripts/agents b/proxmox/lxc102-docker/scripts/agents new file mode 100755 index 0000000..9ac8429 --- /dev/null +++ b/proxmox/lxc102-docker/scripts/agents @@ -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 diff --git a/proxmox/lxc102-docker/scripts/bootstrap-statusline.sh b/proxmox/lxc102-docker/scripts/bootstrap-statusline.sh new file mode 100644 index 0000000..f889bec --- /dev/null +++ b/proxmox/lxc102-docker/scripts/bootstrap-statusline.sh @@ -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 diff --git a/proxmox/lxc102-docker/scripts/sb b/proxmox/lxc102-docker/scripts/sb new file mode 100755 index 0000000..c0e7139 --- /dev/null +++ b/proxmox/lxc102-docker/scripts/sb @@ -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 diff --git a/proxmox/lxc102-docker/scripts/sbt b/proxmox/lxc102-docker/scripts/sbt new file mode 100755 index 0000000..174c3ab --- /dev/null +++ b/proxmox/lxc102-docker/scripts/sbt @@ -0,0 +1,122 @@ +#!/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. +# +# sbt sesiune 1, /home/workspace, claude în pane-ul stâng +# sbt 2 proiect-a sesiune 2, /home/workspace/proiect-a +# sbt 1 proiect-a opencode lansează opencode +# sbt 1 proiect-a shell doar shell în sandbox, fără agent +# 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) +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="claude" + +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:-claude}" + ;; + "") + : # 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 +tmux split-window -h -t "$SESSION:0" -c "$ROOT" "$(sandbox_pane "")" +tmux select-pane -t "$SESSION:0.0" +exec tmux attach-session -t "$SESSION" diff --git a/proxmox/lxc102-docker/scripts/statusline.sh b/proxmox/lxc102-docker/scripts/statusline.sh new file mode 100755 index 0000000..b60b176 --- /dev/null +++ b/proxmox/lxc102-docker/scripts/statusline.sh @@ -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/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"