From c6161c3332248465c54c484cbb64489552f642e5 Mon Sep 17 00:00:00 2001 From: vitaly Date: Tue, 9 Jun 2026 20:58:06 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B0=D0=BA=D0=BA=D0=B0?= =?UTF-8?q?=D1=83=D0=BD=D1=82=D0=BE=D0=B2=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=20=D1=85=D1=83=D0=BA=20=D0=B1=D0=B5=D0=B7=20=D1=82=D0=BE=D0=BA?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=D0=B2=20LLM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserPromptSubmit хук перехватывает /switch-account до LLM, переключает credentials по кругу и возвращает decision:block - нулевой расход токенов - Статусная строка: effort и имя аккаунта в квадратных скобках [high·work] - ai-setup.sh деплоит хук switch-account-hook.sh и прописывает его в settings.json - Скилл switch-account оставлен как fallback-документация для setup Co-Authored-By: Claude Sonnet 4.6 --- .../claude/hooks/switch-account-hook.sh | 48 ++++++++++++ .../claude/skills/switch-account/SKILL.md | 73 +++++++++++++++++++ home-configs/claude/statusline-command.sh | 14 +++- scripts/ai-setup.sh | 35 +++++++++ 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100755 home-configs/claude/hooks/switch-account-hook.sh create mode 100644 home-configs/claude/skills/switch-account/SKILL.md diff --git a/home-configs/claude/hooks/switch-account-hook.sh b/home-configs/claude/hooks/switch-account-hook.sh new file mode 100755 index 0000000..8895f74 --- /dev/null +++ b/home-configs/claude/hooks/switch-account-hook.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# UserPromptSubmit hook: перехватывает /switch-account без участия LLM + +input=$(cat) +prompt=$(echo "$input" | jq -r '.user_prompt // empty' 2>/dev/null) + +# Нормализуем: убираем пробелы и слэш в начале +normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||') + +[ "$normalized" != "switch-account" ] && exit 0 + +# --- Переключаем аккаунт --- +ACCOUNTS_DIR="$HOME/.claude/accounts" +CREDS="$HOME/.claude/.credentials.json" +CURRENT_FILE="$ACCOUNTS_DIR/current" + +mkdir -p "$ACCOUNTS_DIR" + +mapfile -t accounts < <(ls "$ACCOUNTS_DIR"/*.credentials.json 2>/dev/null \ + | xargs -I{} basename {} .credentials.json | sort) + +if [ ${#accounts[@]} -eq 0 ]; then + jq -n '{ + "decision": "block", + "reason": "Аккаунты не настроены. Создай ~/.claude/accounts/.credentials.json для каждого аккаунта и запиши текущий в ~/.claude/accounts/current" + }' + exit 0 +fi + +current=$(cat "$CURRENT_FILE" 2>/dev/null || echo "") + +# Найти следующий по кругу +idx=-1 +for i in "${!accounts[@]}"; do + [ "${accounts[$i]}" = "$current" ] && idx=$i && break +done +next_idx=$(( (idx + 1) % ${#accounts[@]} )) +next="${accounts[$next_idx]}" + +cp "$ACCOUNTS_DIR/${next}.credentials.json" "$CREDS" +chmod 600 "$CREDS" +echo "$next" > "$CURRENT_FILE" + +total=${#accounts[@]} +msg="Аккаунт: ${current:-?} -> ${next} (${total} аккаунтов)" + +jq -n --arg msg "$msg" '{"decision": "block", "reason": $msg}' +exit 0 diff --git a/home-configs/claude/skills/switch-account/SKILL.md b/home-configs/claude/skills/switch-account/SKILL.md new file mode 100644 index 0000000..76804be --- /dev/null +++ b/home-configs/claude/skills/switch-account/SKILL.md @@ -0,0 +1,73 @@ +--- +name: switch-account +description: Use when user types /switch-account - switches to the next saved Claude.ai OAuth account in rotation +--- + +# Switch Account + +Переключает между сохранёнными Claude.ai OAuth аккаунтами по кругу. + +## Действия + +Выполни эту Bash-команду и интерпретируй вывод: + +```bash +python3 << 'EOF' +import os, json, glob, shutil, sys + +accounts_dir = os.path.expanduser("~/.claude/accounts") +creds_path = os.path.expanduser("~/.claude/.credentials.json") +current_file = os.path.join(accounts_dir, "current") + +os.makedirs(accounts_dir, exist_ok=True) + +files = sorted(glob.glob(os.path.join(accounts_dir, "*.credentials.json"))) +accounts = [os.path.basename(f).replace(".credentials.json", "") for f in files] + +if not accounts: + print("NO_ACCOUNTS") + sys.exit(0) + +current = open(current_file).read().strip() if os.path.exists(current_file) else "" + +try: + idx = accounts.index(current) + next_idx = (idx + 1) % len(accounts) +except ValueError: + next_idx = 0 + +next_account = accounts[next_idx] +shutil.copy(os.path.join(accounts_dir, f"{next_account}.credentials.json"), creds_path) +os.chmod(creds_path, 0o600) +open(current_file, "w").write(next_account) + +print(f"SWITCHED:{current}->{next_account}:{len(accounts)}") +EOF +``` + +Интерпретируй вывод: +- `NO_ACCOUNTS` -> скажи пользователю что аккаунты не настроены и покажи инструкцию из раздела Setup ниже +- `SWITCHED:old->new:N` -> сообщи коротко: "Переключено: **old** -> **new** (всего аккаунтов: N). Статусная строка обновится при следующем запросе." + +## Setup - как добавить аккаунты (если NO_ACCOUNTS) + +Показывай эту инструкцию пользователю дословно: + +```bash +mkdir -p ~/.claude/accounts + +# 1. Сохрани текущий залогиненный аккаунт (дай ему имя, например "personal"): +cp ~/.claude/.credentials.json ~/.claude/accounts/personal.credentials.json +echo personal > ~/.claude/accounts/current + +# 2. Залогинься во второй аккаунт в отдельном терминале (НЕ в Claude Code): +# claude auth login +# cp ~/.claude/.credentials.json ~/.claude/accounts/work.credentials.json + +# 3. Восстанови первый как активный: +# cp ~/.claude/accounts/personal.credentials.json ~/.claude/.credentials.json + +# Теперь /switch-account будет переключать между personal и work. +``` + +Имена файлов (`personal`, `work`) - произвольные, можно любые. diff --git a/home-configs/claude/statusline-command.sh b/home-configs/claude/statusline-command.sh index 1a17e0a..a4e8e91 100644 --- a/home-configs/claude/statusline-command.sh +++ b/home-configs/claude/statusline-command.sh @@ -15,7 +15,19 @@ short_cwd="${cwd/#$HOME/\~}" printf "\033[00;37m%s\033[00m" "$short_cwd" [ -n "$branch" ] && printf " \033[00;37m[%s]\033[00m" "$branch" -[ -n "$model" ] && printf " \033[38;5;173m%s\033[00m" "$model" +if [ -n "$model" ]; then + effort=$(jq -r '.effortLevel // empty' ~/.claude/settings.json 2>/dev/null) + account=$(cat ~/.claude/accounts/current 2>/dev/null) + if [ -n "$effort" ] && [ -n "$account" ]; then + printf " \033[38;5;173m%s [%s·%s]\033[00m" "$model" "$effort" "$account" + elif [ -n "$effort" ]; then + printf " \033[38;5;173m%s [%s]\033[00m" "$model" "$effort" + elif [ -n "$account" ]; then + printf " \033[38;5;173m%s [%s]\033[00m" "$model" "$account" + else + printf " \033[38;5;173m%s\033[00m" "$model" + fi +fi # Форматирует оставшееся время до сброса лимита fmt_remaining() { diff --git a/scripts/ai-setup.sh b/scripts/ai-setup.sh index 0b88586..3e0b340 100755 --- a/scripts/ai-setup.sh +++ b/scripts/ai-setup.sh @@ -681,6 +681,41 @@ else warn "Файл $STATUSLINE_SRC не найден, пропускаю" fi +# ── 6.7.1. Хук switch-account ─────────────────────────────────── +info "Деплою хук switch-account..." +SWITCH_HOOK_SRC="$SCRIPT_DIR/home-configs/claude/hooks/switch-account-hook.sh" +SWITCH_HOOK_DST="$HOME/.claude/hooks/switch-account-hook.sh" +mkdir -p "$HOME/.claude/hooks" +if [ -f "$SWITCH_HOOK_SRC" ]; then + cp "$SWITCH_HOOK_SRC" "$SWITCH_HOOK_DST" + chmod +x "$SWITCH_HOOK_DST" + # Прописываем хук в settings.json (идемпотентно) + python3 - "$HOME/.claude/settings.json" "$SWITCH_HOOK_DST" <<'PYEOF' +import sys, json, os +settings_path, hook_path = sys.argv[1], sys.argv[2] +data = {} +if os.path.exists(settings_path): + with open(settings_path) as f: + try: data = json.load(f) + except json.JSONDecodeError: pass +data.setdefault("hooks", {}).setdefault("UserPromptSubmit", [{"hooks": []}]) +hook_cmd = f'bash "{hook_path}"' +ups = data["hooks"]["UserPromptSubmit"] +already = any( + any(h.get("command", "") == hook_cmd for h in entry.get("hooks", [])) + for entry in ups +) +if not already: + ups[0]["hooks"].append({"type": "command", "command": hook_cmd}) +with open(settings_path, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") +PYEOF + success "Хук switch-account установлен" +else + warn "Файл $SWITCH_HOOK_SRC не найден, пропускаю" +fi + # ── 6.8. Регистрация официального маркетплейса плагинов Claude ── info "Настраиваю маркетплейс плагинов Claude Code..." if ! command -v claude &>/dev/null; then