diff --git a/home-configs/claude/hooks/add-account-hook.sh b/home-configs/claude/hooks/add-account-hook.sh index 0e06468..76c5f85 100755 --- a/home-configs/claude/hooks/add-account-hook.sh +++ b/home-configs/claude/hooks/add-account-hook.sh @@ -8,40 +8,144 @@ input=$(cat) prompt=$(echo "$input" | jq -r '.user_prompt // .prompt // empty' 2>/dev/null) normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||') +# Для /add-account ключ передаётся в том же сообщении, поэтому проверяем префикс. +normalized_cmd=$(echo "$normalized" | awk '{print $1}') -[ "$normalized" != "add-account" ] && exit 0 +[ "$normalized_cmd" != "add-account" ] && exit 0 -CREDS="$HOME/.claude/.credentials.json" -ACCOUNTS_DIR="$HOME/.claude/accounts" -CURRENT_FILE="$ACCOUNTS_DIR/current" -EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh" +LAUNCHER="${AI_LAUNCHER:-claude}" -mkdir -p "$ACCOUNTS_DIR" +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" -# Сохраняем текущий активный аккаунт под его реальным 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 + mkdir -p "$ACCOUNTS_DIR" -# Фоновый процесс: логин нового аккаунта + автосохранение после успеха. -# claude auth login ждёт авторизации в браузере и завершается после неё, -# затем определяем email нового аккаунта по токену (через API) и сохраняем. -( - claude auth login --claudeai /tmp/claude-add-account.log 2>&1 - new_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null) - if [ -n "$new_email" ] && [ -f "$CREDS" ]; then - cp "$CREDS" "$ACCOUNTS_DIR/${new_email}.credentials.json" - chmod 600 "$ACCOUNTS_DIR/${new_email}.credentials.json" - echo "$new_email" > "$CURRENT_FILE" - echo "SAVED: $new_email" >> /tmp/claude-add-account.log - fi -) & -disown + # Сохраняем текущий активный аккаунт под его реальным 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 -# exit 0: Claude загружает скилл add-account и говорит что делать -exit 0 + # Фоновый процесс: логин нового аккаунта + автосохранение после успеха. + # claude auth login ждёт авторизации в браузере и завершается после неё, + # затем определяем email нового аккаунта по токену (через API) и сохраняем. + ( + claude auth login --claudeai /tmp/claude-add-account.log 2>&1 + new_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null) + if [ -n "$new_email" ] && [ -f "$CREDS" ]; then + cp "$CREDS" "$ACCOUNTS_DIR/${new_email}.credentials.json" + chmod 600 "$ACCOUNTS_DIR/${new_email}.credentials.json" + echo "$new_email" > "$CURRENT_FILE" + echo "SAVED: $new_email" >> /tmp/claude-add-account.log + fi + ) & + disown + + # exit 0: Claude загружает скилл add-account и говорит что делать + exit 0 + ;; + + 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 diff --git a/home-configs/claude/hooks/switch-account-hook.sh b/home-configs/claude/hooks/switch-account-hook.sh index d1bd93b..087881a 100755 --- a/home-configs/claude/hooks/switch-account-hook.sh +++ b/home-configs/claude/hooks/switch-account-hook.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# UserPromptSubmit hook: перехватывает /switch-account и циклически меняет аккаунт. +# UserPromptSubmit hook: перехватывает /switch-account и циклически меняет аккаунт/ключ. # Текущий аккаунт определяется ПО ТОКЕНУ в .credentials.json (account-email.sh), # а не по claude auth status — он читает рассинхронизированный oauthAccount. # На Linux Claude Code перечитывает .credentials.json на лету: новый аккаунт @@ -13,42 +13,86 @@ 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" -EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh" +LAUNCHER="${AI_LAUNCHER:-claude}" -mkdir -p "$ACCOUNTS_DIR" +case "$LAUNCHER" in + claude) + ACCOUNTS_DIR="$HOME/.claude/accounts" + CREDS="$HOME/.claude/.credentials.json" + CURRENT_FILE="$ACCOUNTS_DIR/current" + EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh" -# Реальный текущий аккаунт — по токену активной сессии (не по хрупкому current) -current=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null) -[ -z "$current" ] && current=$(cat "$CURRENT_FILE" 2>/dev/null) + mkdir -p "$ACCOUNTS_DIR" -# Сохранить актуальные (возможно обновлённые рефрешем) токены под реальным email. -# current выведен из самого токена — порча файла другого аккаунта исключена. -if [ -n "$current" ] && [ -f "$CREDS" ]; then - cp "$CREDS" "$ACCOUNTS_DIR/${current}.credentials.json" - chmod 600 "$ACCOUNTS_DIR/${current}.credentials.json" -fi + # Реальный текущий аккаунт — по токену активной сессии (не по хрупкому current) + current=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null) + [ -z "$current" ] && current=$(cat "$CURRENT_FILE" 2>/dev/null) -mapfile -t accounts < <(ls "$ACCOUNTS_DIR"/*.credentials.json 2>/dev/null \ - | xargs -I{} basename {} .credentials.json | sort) + # Сохранить актуальные (возможно обновлённые рефрешем) токены под реальным email. + # current выведен из самого токена — порча файла другого аккаунта исключена. + if [ -n "$current" ] && [ -f "$CREDS" ]; then + cp "$CREDS" "$ACCOUNTS_DIR/${current}.credentials.json" + chmod 600 "$ACCOUNTS_DIR/${current}.credentials.json" + fi -if [ ${#accounts[@]} -le 1 ]; then - echo "Только один аккаунт (${current:-нет}). Добавь второй через /add-account." >&2 - exit 2 -fi + mapfile -t accounts < <(ls "$ACCOUNTS_DIR"/*.credentials.json 2>/dev/null \ + | xargs -I{} basename {} .credentials.json | sort) -# Найти следующий по кругу -idx=-1 -for i in "${!accounts[@]}"; do - [ "${accounts[$i]}" = "$current" ] && idx=$i && break -done -next_idx=$(( (idx + 1) % ${#accounts[@]} )) -next="${accounts[$next_idx]}" + if [ ${#accounts[@]} -le 1 ]; then + echo "Только один аккаунт (${current:-нет}). Добавь второй через /add-account." >&2 + exit 2 + fi -cp "$ACCOUNTS_DIR/${next}.credentials.json" "$CREDS" -chmod 600 "$CREDS" -echo "$next" > "$CURRENT_FILE" + # Найти следующий по кругу + idx=-1 + for i in "${!accounts[@]}"; do + [ "${accounts[$i]}" = "$current" ] && idx=$i && break + done + next_idx=$(( (idx + 1) % ${#accounts[@]} )) + next="${accounts[$next_idx]}" -exit 0 + cp "$ACCOUNTS_DIR/${next}.credentials.json" "$CREDS" + chmod 600 "$CREDS" + echo "$next" > "$CURRENT_FILE" + + exit 0 + ;; + + kimi) + KEYS_DIR="$HOME/.config/ai-setup/kimi_keys" + CURRENT_FILE="$KEYS_DIR/current" + + mkdir -p "$KEYS_DIR" + + current=$(cat "$CURRENT_FILE" 2>/dev/null || true) + + # Если current указывает в никуда, но есть ключи — сбросить на первый попавшийся. + if [ -n "$current" ] && [ ! -f "$KEYS_DIR/${current}.key" ]; then + current="" + fi + + mapfile -t keys < <(ls "$KEYS_DIR"/*.key 2>/dev/null \ + | xargs -I{} basename {} .key | sort) + + if [ ${#keys[@]} -le 1 ]; then + echo "Только один Kimi ключ (${current:-нет}). Добавь второй через /add-account." >&2 + exit 2 + fi + + idx=-1 + for i in "${!keys[@]}"; do + [ "${keys[$i]}" = "$current" ] && idx=$i && break + done + next_idx=$(( (idx + 1) % ${#keys[@]} )) + next="${keys[$next_idx]}" + + echo "$next" > "$CURRENT_FILE" + echo "Kimi ключ переключён на: $next. ai-kimi перезапустится с новым ключом." >&2 + + exit 0 + ;; + + *) + exit 0 + ;; +esac diff --git a/home-configs/claude/skills/add-account/SKILL.md b/home-configs/claude/skills/add-account/SKILL.md index 7861c29..4e2e697 100644 --- a/home-configs/claude/skills/add-account/SKILL.md +++ b/home-configs/claude/skills/add-account/SKILL.md @@ -1,8 +1,12 @@ --- name: add-account -description: Add a new Claude.ai account (handled by UserPromptSubmit hook, no LLM needed) +description: Add a new account or API key (handled by UserPromptSubmit hook, no LLM needed) --- -Хук сохранил текущий аккаунт и открыл браузер для логина нового. Ответь ТОЛЬКО этим текстом (без markdown, без лишних слов): +Хук уже обработал запрос. Ответь ТОЛЬКО этим текстом (без markdown, без лишних слов): -Браузер открыт — авторизуйся там. После авторизации новый аккаунт сохранится автоматически и сразу станет активным (Claude Code на Linux перечитывает токен на лету). /switch-account переключает между всеми сохранёнными аккаунтами по кругу. +Для Claude.ai: браузер открыт — авторизуйся там. После авторизации новый аккаунт сохранится автоматически и сразу станет активным (Claude Code на Linux перечитывает токен на лету). + +Для Kimi: укажи API-ключ в том же сообщении: `/add-account sk-...`. Хук проверит и сохранит ключ; ai-kimi перезапустится с ним. + +/switch-account переключает между всеми сохранёнными аккаунтами/ключами по кругу. diff --git a/home-configs/claude/skills/switch-account/SKILL.md b/home-configs/claude/skills/switch-account/SKILL.md index 6a26350..ae51c30 100644 --- a/home-configs/claude/skills/switch-account/SKILL.md +++ b/home-configs/claude/skills/switch-account/SKILL.md @@ -1,8 +1,8 @@ --- name: switch-account -description: Switch to next Claude.ai account (handled by UserPromptSubmit hook, no LLM needed) +description: Switch to next account or API key (handled by UserPromptSubmit hook, no LLM needed) --- -Переключение аккаунта уже выполнено хуком до того, как ты это читаешь. +Переключение аккаунта/ключа уже выполнено хуком до того, как ты это читаешь. Ответь ровно одним символом: `✓` Никаких инструментов. Никаких объяснений. Только `✓`.