#!/bin/bash # Установка панели «Claude»: дерево сессий и агентов Claude Code, лимиты подписки и баланс. # Сервер панели — служба launchd (работает и без iTerm2), iTerm2 показывает её в Toolbelt. # Повторный запуск безопасен — обновляет установленное. set -euo pipefail say() { printf '\033[1m==>\033[0m %s\n' "$*"; } ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; } warn() { printf ' \033[33m!\033[0m %s\n' "$*"; } die() { printf '\033[31mОшибка:\033[0m %s\n' "$*" >&2; exit 1; } # из SSH-сессии launchd (gui/) и iTerm2 не настроить, а прежний сервер уже был бы остановлен [ -z "${SSH_CONNECTION:-}${SSH_TTY:-}" ] \ || die "запустите за самим Mac, не по SSH — из SSH-сессии launchd и iTerm2 не настроить. Если вы и так за Mac (например, в панели tmux, открытой раньше из SSH), запустите в обычной вкладке iTerm2 или после unset SSH_CONNECTION SSH_TTY" HERE="$(cd "$(dirname "$0")" && pwd)" FILES="$HERE/files" AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch" HOOKS_DIR="$HOME/.claude/hooks" SETTINGS="$HOME/.claude/settings.json" CONFIG_DIR="$HOME/.config/claude-agents" BACKUP_DIR="$HOME/.claude-panel-backup/$(date +%Y%m%d-%H%M%S)-$$" PORT=8766 LABEL=com.andrey.claude-agents SERVICE_DIR="$HOME/.local/share/claude-agents" PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" SERVICE_LOG="$HOME/Library/Logs/claude-agents/panel.log" GUI="gui/$(id -u)" backup() { mkdir -p "$BACKUP_DIR"; cp -p "$1" "$BACKUP_DIR/"; } # ---------- проверки ---------- say "Проверяю окружение" [ "$(uname)" = "Darwin" ] || [ "${CLAUDE_PANEL_ANY_OS:-}" = 1 ] || die "нужен macOS с iTerm2" python3 -c 'import sys; assert sys.version_info >= (3, 8)' 2>/dev/null \ || die "нужен python3 ≥ 3.8 (xcode-select --install или brew install python)" PY="$(command -v python3)" # не sys.executable: версионный путь Homebrew исчезает после brew cleanup ok "python3: $(python3 -V 2>&1) ($PY)" if command -v jq >/dev/null; then ok "jq: $(command -v jq)" else warn "нет jq — хук без него молча ничего не пишет. Установите: brew install jq"; fi if [ -d "/Applications/iTerm.app" ] || [ -d "$HOME/Applications/iTerm.app" ]; then ok "iTerm2 найден" else warn "iTerm2 не найден в /Applications — панель будет доступна только в браузере"; fi # ---------- хук Claude Code ---------- say "Хук Claude Code" mkdir -p "$HOOKS_DIR" install -m 755 "$FILES/agents_panel_hook.sh" "$HOOKS_DIR/agents_panel_hook.sh" ok "$HOOKS_DIR/agents_panel_hook.sh" [ -f "$SETTINGS" ] && backup "$SETTINGS" added=$(python3 "$FILES/settings_hooks.py" add "$SETTINGS") \ || die "не смог обновить $SETTINGS (битый JSON?) — копия в $BACKUP_DIR" if [ "$added" = "—" ]; then ok "settings.json: хуки уже на месте" else ok "settings.json: добавлены хуки $added (копия старого — в $BACKUP_DIR)"; fi # ---------- скрипт iTerm2 ---------- say "Скрипт iTerm2 (регистрация в Toolbelt)" mkdir -p "$AUTOLAUNCH" if [ -f "$AUTOLAUNCH/claude_usage.py" ]; then # прежняя отдельная панель лимитов backup "$AUTOLAUNCH/claude_usage.py" && rm "$AUTOLAUNCH/claude_usage.py" ok "старый claude_usage.py убран из AutoLaunch (копия в $BACKUP_DIR)" fi [ -f "$AUTOLAUNCH/claude_agents.py" ] && backup "$AUTOLAUNCH/claude_agents.py" install -m 644 "$FILES/claude_agents.py" "$AUTOLAUNCH/claude_agents.py" ok "$AUTOLAUNCH/claude_agents.py" # ---------- настройки панели ---------- mkdir -p "$CONFIG_DIR" install -m 644 "$FILES/config.example.json" "$CONFIG_DIR/config.example.json" if [ -f "$CONFIG_DIR/config.json" ]; then ok "ваш config.json не тронут" else ok "пример настроек: $CONFIG_DIR/config.example.json (скопируйте в config.json, чтобы менять)"; fi # ---------- фоновая служба ---------- say "Фоновая служба" mkdir -p "$SERVICE_DIR" install -m 644 "$FILES/claude_agents.py" "$SERVICE_DIR/claude_agents.py" # PATH из текущей оболочки: панели tmux, открытые службой (lf), должны находить то же, что и в терминале python3 "$FILES/launchagent.py" write "$PLIST" "$PY" "$SERVICE_DIR/claude_agents.py" "$SERVICE_LOG" "$PATH" >/dev/null ok "$PLIST" launchctl bootout "$GUI/$LABEL" >/dev/null 2>&1 || true # прежняя копия панели (сервер внутри iTerm2) держит порт — останавливаем, чтобы его заняла служба if command -v lsof >/dev/null; then pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true) if [ -n "$pids" ]; then kill $pids 2>/dev/null || true; sleep 1; ok "остановлена прежняя версия панели"; fi fi # сразу после bootout bootstrap иногда падает с «Input/output error» — повторяем for attempt in 1 2 3 4 5; do launchctl bootstrap "$GUI" "$PLIST" && break [ "$attempt" = 5 ] && die "launchd не принял службу — смотрите launchctl print $GUI/$LABEL и $SERVICE_LOG" sleep 1 done up=0 for _ in 1 2 3 4 5 6 7 8 9 10; do if curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$PORT/"; then up=1; break; fi sleep 1 done if [ $up = 1 ]; then ok "служба запущена: http://127.0.0.1:$PORT/" else warn "служба не ответила — смотрите $SERVICE_LOG"; fi # ---------- Python API в iTerm2 ---------- say "iTerm2" iterm_running=0 pgrep -xq iTerm2 2>/dev/null && iterm_running=1 api=$(defaults read com.googlecode.iterm2 EnableAPIServer 2>/dev/null || echo 0) if [ "$api" != 1 ]; then if [ $iterm_running = 0 ] && command -v defaults >/dev/null; then defaults write com.googlecode.iterm2 EnableAPIServer -bool true ok "Python API включён" else warn "включите Python API: iTerm2 → Settings → General → Magic → Enable Python API" [ $iterm_running = 1 ] && warn "(пока iTerm2 запущен, установщик не меняет его настройки — он бы их перезаписал)" fi else ok "Python API уже включён" fi if [ $iterm_running = 1 ] && [ "$api" = 1 ]; then pkill -f "Scripts/AutoLaunch/claude_agents.py" || true # прежний регистратор, иначе их станет по одному на запуск if osascript -e 'tell application "iTerm2" to launch API script named "claude_agents"' >/dev/null 2>&1; then ok "панель зарегистрирована в Toolbelt" else warn "не смог зарегистрировать сам — Scripts → AutoLaunch → claude_agents.py" fi else warn "в Toolbelt панель появится при следующем запуске iTerm2" fi cat <