#!/usr/bin/env bash # Copyright (c) 2026 Виталий Никитенко. MIT License. # UserPromptSubmit hook: перехватывает /add-account и /add-account <код>. # Логин в два шага, целиком в этом же терминале (без второго окна): # /add-account — сохраняет текущий аккаунт, поднимает фоновый # claude auth login со stdin из fifo и печатает ссылку; # /add-account <код> — отдаёт код из браузера в fifo, ждёт обмена, определяет # email нового аккаунта по токену, сохраняет и делает current. # CLI отдаёт redirect_uri на platform.claude.com и ждёт код в stdin — при # /dev/null) normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||') # Для /add-account ключ передаётся в том же сообщении, поэтому проверяем префикс. normalized_cmd=$(echo "$normalized" | awk '{print $1}') [ "$normalized_cmd" != "add-account" ] && exit 0 LAUNCHER="${AI_LAUNCHER:-claude}" case "$LAUNCHER" in claude) CREDS="$HOME/.claude/.credentials.json" ACCOUNTS_DIR="$HOME/.claude/accounts" CURRENT_FILE="$ACCOUNTS_DIR/current" EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh" FIFO="$HOME/.claude/add-account.fifo" LOG="$HOME/.claude/add-account.log" LOGIN_PID_FILE="$HOME/.claude/add-account.login.pid" HOLDER_PID_FILE="$HOME/.claude/add-account.holder.pid" mkdir -p "$ACCOUNTS_DIR" # Сохраняем текущий активный аккаунт под его реальным email (по токену), # чтобы логин новым аккаунтом не затёр его креды безвозвратно. cur_email="" if [ -f "$CREDS" ]; then cur_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null) if [ -n "$cur_email" ]; then cp "$CREDS" "$ACCOUNTS_DIR/${cur_email}.credentials.json" chmod 600 "$ACCOUNTS_DIR/${cur_email}.credentials.json" echo "$cur_email" > "$CURRENT_FILE" fi fi # Процессы гасим строго по своим PID: pkill -f "claude auth login" выкосил бы # любой процесс, в чьей командной строке встретилась эта подстрока. pid_of() { # $1=pid-файл, $2=ожидаемое имя процесса; печатает живой pid [ -f "$1" ] || return 1 pid=$(cat "$1" 2>/dev/null) [ -n "$pid" ] || return 1 [ "$(ps -p "$pid" -o comm= 2>/dev/null)" = "$2" ] || return 1 printf '%s' "$pid" } login_alive() { pid_of "$LOGIN_PID_FILE" claude >/dev/null; } # Останавливаем предыдущую попытку логина, чтобы не копить висящие процессы. stop_login() { pid=$(pid_of "$LOGIN_PID_FILE" claude) && kill "$pid" 2>/dev/null pid=$(pid_of "$HOLDER_PID_FILE" sleep) && kill "$pid" 2>/dev/null rm -f "$LOGIN_PID_FILE" "$HOLDER_PID_FILE" "$FIFO" } code=$(echo "$normalized" | awk '{print $2}') # ── Шаг 2: код из браузера ─────────────────────────────────────── if [ -n "$code" ]; then if ! login_alive || [ ! -p "$FIFO" ]; then echo "Сессия логина не запущена (или уже закрылась). Начни заново: /add-account" >&2 stop_login exit 2 fi printf '%s\n' "$code" > "$FIFO" # Ждём, пока CLI обменяет код на токен и завершится. for _ in $(seq 1 60); do login_alive || break sleep 0.5 done stop_login if grep -q "Login failed" "$LOG" 2>/dev/null; then echo "✗ $(grep -o 'Login failed.*' "$LOG" | tail -1)" >&2 echo "Код одноразовый и живёт недолго — начни заново: /add-account" >&2 exit 2 fi new_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null) if [ -z "$new_email" ]; then echo "✗ Логин завершился, но email аккаунта определить не удалось. Аккаунт не сохранён." >&2 exit 2 fi cp "$CREDS" "$ACCOUNTS_DIR/${new_email}.credentials.json" chmod 600 "$ACCOUNTS_DIR/${new_email}.credentials.json" echo "$new_email" > "$CURRENT_FILE" if [ "$new_email" = "$cur_email" ]; then echo "⚠ Вошёл тот же аккаунт ($new_email) — новых аккаунтов не добавилось." >&2 else echo "✓ Аккаунт $new_email добавлен и стал активным. /switch-account — переключение по кругу." >&2 fi exit 2 fi # ── Шаг 1: запуск логина и ссылка ──────────────────────────────── stop_login mkfifo -m 600 "$FIFO" || { echo "Не удалось создать $FIFO" >&2; exit 2; } : > "$LOG" # stdin из fifo: CLI ждёт код в stdin, закрытый дескриптор его подвешивает. # Держатель fifo не даёт CLI получить EOF и задаёт таймаут попытки (15 мин). claude auth login --claudeai < "$FIFO" > "$LOG" 2>&1 & echo $! > "$LOGIN_PID_FILE" disown sleep 900 > "$FIFO" 2>/dev/null & echo $! > "$HOLDER_PID_FILE" disown url="" for _ in $(seq 1 40); do url=$(grep -o 'https://claude\.com/[^ ]*' "$LOG" 2>/dev/null | head -1) [ -n "$url" ] && break sleep 0.5 done if [ -z "$url" ]; then stop_login echo "Не дождался ссылки для входа. Лог: $LOG" >&2 exit 2 fi echo "Авторизуйся в браузере (ссылка ниже), скопируй код и пришли его сюда: /add-account <код>" >&2 echo "" >&2 echo "$url" >&2 exit 2 ;; kimi) KEYS_DIR="$HOME/.config/ai-setup/kimi_keys" CURRENT_FILE="$KEYS_DIR/current" mkdir -p "$KEYS_DIR" # Убедиться, что текущий ключ сохранён под своим alias. current=$(cat "$CURRENT_FILE" 2>/dev/null || true) if [ -n "$current" ] && [ -f "$KEYS_DIR/${current}.key" ]; then chmod 600 "$KEYS_DIR/${current}.key" fi # Ключ передаётся в том же сообщении: /add-account new_key=$(echo "$prompt" | sed 's|^[[:space:]]*/add-account[[:space:]]*||; s|[[:space:]]*$||') if [ -z "$new_key" ]; then echo "" >&2 echo "Укажите Kimi API ключ в том же сообщении: /add-account sk-..." >&2 exit 2 fi # Проверка ключа. echo -n "Проверяю Kimi ключ... " >&2 response=$(curl -s -w "\n%{http_code}" --max-time 15 \ "https://api.kimi.com/coding/v1/messages" \ -H "x-api-key: $new_key" \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{"model":"kimi-k2.7","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' \ 2>/dev/null || echo "000") code=$(echo "$response" | tail -1) case "$code" in 200|400|429) echo "OK" >&2 ;; 401|403) echo "" >&2 echo "Ошибка: ключ недействителен (HTTP $code)." >&2 exit 2 ;; 000) echo "" >&2 echo "Не удалось проверить ключ (сеть недоступна?). Попробуйте позже." >&2 exit 2 ;; *) echo "" >&2 echo "Ошибка при проверке ключа (HTTP $code)." >&2 exit 2 ;; esac # Генерация следующего свободного alias. i=1 while [ -f "$KEYS_DIR/account${i}.key" ]; do i=$((i + 1)) done alias_name="account${i}" echo "$new_key" > "$KEYS_DIR/${alias_name}.key" chmod 600 "$KEYS_DIR/${alias_name}.key" # Пытаемся получить email/имя аккаунта Kimi для статусной строки. _kimi_account_info() { local api_key="$1" local resp email name for url in "https://api.kimi.com/coding/v1/account" "https://api.kimi.com/coding/v1/users/me"; do resp=$(curl -s --max-time 10 "$url" \ -H "x-api-key: $api_key" \ -H "Accept: application/json" \ 2>/dev/null || echo "") [ -z "$resp" ] && continue email=$(echo "$resp" | jq -r '.email // .data.email // .account.email // empty' 2>/dev/null) name=$(echo "$resp" | jq -r '.name // .data.name // .account.name // empty' 2>/dev/null) if [ -n "$email" ]; then echo "$email"; return 0; fi if [ -n "$name" ]; then echo "$name"; return 0; fi done return 1 } meta=$(_kimi_account_info "$new_key" 2>/dev/null || true) if [ -n "$meta" ]; then echo "$meta" > "$KEYS_DIR/${alias_name}.meta" chmod 600 "$KEYS_DIR/${alias_name}.meta" fi echo "$alias_name" > "$CURRENT_FILE" echo "Новый Kimi ключ сохранён как: $alias_name. ai-kimi перезапустится с ним." >&2 exit 0 ;; *) exit 0 ;; esac