Compare commits

...

8 Commits

Author SHA1 Message Date
6c2844ce92 feat: добавить поддержку Kimi K2.7 Code в ai-kimi
- Opus/Sonnet: kimi-k2.7 (флагманский программист)
- Haiku/Subagent: kimi-k2.6 (быстрый универсал)
- Проверка ключа тестирует kimi-k2.7
- Обновлён EFFORT_MAPPING.md и тесты

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-12 21:42:28 +03:00
dfa706e7a0 feat: поддержка баланса OpenRouter в statusline + обновление модели-пикера
- Добавлен _openrouter_balance() в ai-api-helpers.sh
- Statusline теперь показывает баланс для openrouter лаунчера
- Обновлены дефолтные модели в ai-openrouter (Grok 4.20 как opus и т.д.)
- Улучшена изоляция конфигов для deepseek/kimi/openrouter

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-12 11:19:29 +03:00
648c9f068b fix: кэш процентов usage по аккаунту, а не только по модели
Лимиты привязаны к аккаунту, но кэш rate_limits ключевался только по
model_id. При /switch-account (та же модель) проценты смешивались между
аккаунтами. Добавил account в ключ кэша — у каждого аккаунта свои проценты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 09:28:05 +03:00
f3d1b6d5c5 fix: определение Claude-аккаунта по токену вместо auth status
Корень багов с потерей токенов: claude auth status читает
oauthAccount.emailAddress из ~/.claude.json, который рассинхронизирован
с реальным токеном в .credentials.json. Из-за этого хуки определяли
текущий аккаунт неверно и сохраняли активный токен под чужим именем,
затирая credentials другого аккаунта.

- account-email.sh (новый): определяет email по OAuth-токену —
  локальный матчинг с accounts/, затем API /api/oauth/profile
- switch-account-hook.sh: current выводится из токена, а не из
  auth status/хрупкого файла current — порча файлов исключена.
  Перезапуск не нужен: на Linux Claude Code перечитывает
  .credentials.json на лету
- add-account-hook.sh: email нового аккаунта тоже через хелпер
- skill add-account: убрано упоминание перезапуска
- ai-setup.sh: деплой account-email.sh (секция 6.7.05)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 09:06:44 +03:00
fe439fd4a6 feat: автодобавление Claude-аккаунта через /add-account
- новый хук add-account-hook.sh: сохраняет текущий аккаунт по реальному
  email (claude auth status), запускает OAuth-логин в фоне и после успеха
  сам сохраняет новый аккаунт в ~/.claude/accounts + делает его current
- switch-account-hook.sh: активный аккаунт определяется через
  claude auth status, а не через хрупкий файл current - защита от порчи
  сохранённых credentials при рассинхроне токена
- скилл add-account: краткая инструкция после срабатывания хука
- ai-setup.sh: деплой add-account-hook + регистрация в UserPromptSubmit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 08:19:46 +03:00
f8465580e0 feat: полная изоляция моделей между ai-* и гибридный persistence effort
Раньше все ai-* лаунчеры делили один ~/.claude и общий settings.json, из-за
чего кастомная модель (openai/gpt-5.5) из ai-openrouter протекала в пикер
ai-claude. Теперь каждый сторонний провайдер изолирован в своём
CLAUDE_CONFIG_DIR (~/.config/ai-setup/cfg/<launcher>) - свои settings.json и
.claude.json, ноль протечек. ai-claude остаётся на ~/.claude (нативный логин).

Пикеры /model приведены к требуемому виду:
- ai-deepseek: только DeepSeek V4 Pro (opus) и DeepSeek V4 Flash (haiku),
  дефолт Pro; через availableModels + ANTHROPIC_DEFAULT_*_MODEL_NAME
- ai-kimi: только Kimi K2.6 (opus)
- ai-claude: только нативные модели Claude
Общие skills и CLAUDE.md шарятся симлинком из ~/.claude.

Persistence effort - гибрид:
- low/medium/high/xhigh живут нативно в settings.json лаунчера, /effort
  внутри сессии работает свободно и уровень сохраняется
- max нельзя сохранить в settings.json (session-only), поэтому он
  восстанавливается через CLAUDE_CODE_EFFORT_LEVEL; в такой max-сессии
  /effort залочен (ограничение Claude Code), выход - AI_EFFORT=<lvl> ai-*
Текущий уровень ловит статусбар в ~/.cache/ai-setup/effort_<launcher>.

Удалён устаревший effort-save-hook (заменён нативным persistence + гибридом),
почищен из ~/.claude/settings.json и осиротевший кэш model_*.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 07:33:30 +03:00
07983ea84e fix: денежный зелёный цвет баланса DeepSeek (78 вместо 183) 2026-06-11 22:49:32 +03:00
222bb129eb feat: брендовые цвета имён моделей, персистентность model между сессиями
- _brand_color: цвет имени модели по AI_LAUNCHER
  deepseek=синий(69), claude=оранжевый(173), kimi=голубой(81),
  openrouter=фиолетовый(135), остальные=кремовый(223)
- _restore_model / _restore_model_str: сохранение и восстановление
  model_id в кэше лаунчера (~/.cache/ai-setup/model_<launcher>)
- effort-save-hook также сохраняет model_id при завершении сессии
- ai-claude/ai-openrouter используют восстановленную модель при старте

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-11 22:45:49 +03:00
9 changed files with 471 additions and 130 deletions

View File

@@ -9,9 +9,9 @@
## Таблица маппинга ## Таблица маппинга
> Актуально на 30 мая 2026 г. > Актуально на 12 июня 2026 г.
| Claude Code<br>`/effort` | Anthropic<br>(Claude) | GPT-5.5<br>(ChatGPT) | DeepSeek V4 | Kimi K2.6<br>(Moonshot) | Gemini 3.x | | Claude Code<br>`/effort` | Anthropic<br>(Claude) | GPT-5.5<br>(ChatGPT) | DeepSeek V4 | Kimi K2.7<br>(Moonshot) | Gemini 3.x |
|:---:|:---:|:---:|:---:|:---:|:---:| |:---:|:---:|:---:|:---:|:---:|:---:|
| `low` | ✅ `low` | ✅ `low` | ⬆ `high` | 🔛 thinking on | ✅ `LOW` | | `low` | ✅ `low` | ✅ `low` | ⬆ `high` | 🔛 thinking on | ✅ `LOW` |
| `medium` | ✅ `medium` | ✅ `medium` | ⬆ `high` | 🔛 thinking on | ✅ `MEDIUM` | | `medium` | ✅ `medium` | ✅ `medium` | ⬆ `high` | 🔛 thinking on | ✅ `MEDIUM` |
@@ -54,7 +54,7 @@ high → max
- `xhigh` → автоматически поднимается до `max` - `xhigh` → автоматически поднимается до `max`
- Маппинг выполняется на стороне DeepSeek API - Маппинг выполняется на стороне DeepSeek API
### Kimi K2.6 (Moonshot AI) ### Kimi K2.7 (Moonshot AI)
``` ```
on / off on / off
``` ```
@@ -80,11 +80,37 @@ MINIMAL → LOW → MEDIUM → HIGH
| Anthropic | Не нужен | — | | Anthropic | Не нужен | — |
| GPT-5.5 | effort-proxy (наш) | `~/.local/bin/claude-gpt-effort-proxy.py` | | GPT-5.5 | effort-proxy (наш) | `~/.local/bin/claude-gpt-effort-proxy.py` |
| DeepSeek V4 | DeepSeek API | На стороне сервера | | DeepSeek V4 | DeepSeek API | На стороне сервера |
| Kimi K2.6 | Moonshot API | На стороне сервера | | Kimi K2.7 | Moonshot API | На стороне сервера |
| Gemini 3.x | antigravity-claude-proxy | npm пакет | | Gemini 3.x | antigravity-claude-proxy | npm пакет |
## Persistence effort между сессиями
Каждый лаунчер (`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-openrouter`) запоминает свой
уровень effort отдельно. Логика гибридная:
- **`low` / `medium` / `high` / `xhigh`** живут нативно в `settings.json` лаунчера.
`/effort` внутри сессии работает как обычно, уровень сохраняется между сессиями.
- **`max`** — единственный, который Claude Code **не сохраняет** в `settings.json`
(он session-only). Поэтому его восстанавливаем через `CLAUDE_CODE_EFFORT_LEVEL`.
Текущий уровень (включая `max`) статусбар пишет в `~/.cache/ai-setup/effort_<launcher>`.
**Важное следствие (только для `max`):** когда восстановлена `max`-сессия, выставлена
`CLAUDE_CODE_EFFORT_LEVEL=max`, и `/effort` внутри неё **не сменит** уровень
(env-переменная — жёсткий override Claude Code). На остальных уровнях `/effort` свободен.
**Как выйти из `max` (или форсить любой уровень):** перезапусти лаунчер с `AI_EFFORT`:
```bash
AI_EFFORT=max ai-deepseek # включить и запомнить max
AI_EFFORT=high ai-deepseek # вернуться на high (выйти из max)
ai-deepseek # без флага - восстанавливает последний уровень
```
Дефолты при пустом кэше: `xhigh` для `ai-claude`, `high` для остальных.
## Рекомендации ## Рекомендации
- **Для повседневной работы:** `high` или `xhigh` — работает одинаково хорошо у всех провайдеров - **Для повседневной работы:** `high` или `xhigh` — работает одинаково хорошо у всех провайдеров
- **`max` effort:** имеет реальный эффект только у **Anthropic** и **DeepSeek**. Для GPT маппится в `xhigh`, для Gemini и Kimi — в их максимальный уровень - **`max` effort:** имеет реальный эффект только у **Anthropic** и **DeepSeek**. Для GPT маппится в `xhigh`, для Gemini и Kimi — в их максимальный уровень
- **`low`/`medium`:** у DeepSeek и Kimi фактически не снижают reasoning — DeepSeek поднимет до `high`, Kimi просто включит thinking - **`low`/`medium`:** у DeepSeek и Kimi фактически не снижают reasoning — DeepSeek поднимет до `high`, Kimi просто включит thinking
- **Смена уровня:** на `low..xhigh` обычным `/effort`; из `max` — через `AI_EFFORT=<lvl> ai-<launcher>` (в max-сессии `/effort` залочен env-переменной, см. «Persistence effort»)

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Определяет email Claude.ai аккаунта по OAuth-токену в credentials-файле.
# Источник истины — сам токен (НЕ claude auth status, который читает
# рассинхронизированный oauthAccount из ~/.claude.json).
# Сначала локальный матчинг с сохранёнными accounts/, затем API /api/oauth/profile.
# Использование: account-email.sh [credentials-file] (по умолчанию ~/.claude/.credentials.json)
CREDS="${1:-$HOME/.claude/.credentials.json}"
ACCOUNTS_DIR="$HOME/.claude/accounts"
[ -f "$CREDS" ] || exit 0
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$CREDS" 2>/dev/null)
[ -z "$token" ] && exit 0
# 1) Локальный матчинг по токену с сохранёнными аккаунтами (мгновенно)
if [ -d "$ACCOUNTS_DIR" ]; then
for f in "$ACCOUNTS_DIR"/*.credentials.json; do
[ -f "$f" ] || continue
t=$(jq -r '.claudeAiOauth.accessToken // empty' "$f" 2>/dev/null)
if [ -n "$t" ] && [ "$t" = "$token" ]; then
basename "$f" .credentials.json
exit 0
fi
done
fi
# 2) Достоверно через API (по реальному владельцу токена)
email=$(curl -s --max-time 15 "https://api.anthropic.com/api/oauth/profile" \
-H "Authorization: Bearer $token" \
-H "anthropic-beta: oauth-2025-04-20" 2>/dev/null \
| jq -r '.account.email // empty' 2>/dev/null)
[ -n "$email" ] && echo "$email"
exit 0

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: перехватывает /add-account.
# 1) сохраняет текущий аккаунт по его реальному email (account-email.sh)
# 2) запускает oauth-логин в фоне (открывает браузер)
# 3) после логина фоновый процесс сам определяет email нового аккаунта по токену
# и сохраняет его credentials + делает current
input=$(cat)
prompt=$(echo "$input" | jq -r '.user_prompt // .prompt // empty' 2>/dev/null)
normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||')
[ "$normalized" != "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"
mkdir -p "$ACCOUNTS_DIR"
# Сохраняем текущий активный аккаунт под его реальным 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
# Фоновый процесс: логин нового аккаунта + автосохранение после успеха.
# claude auth login ждёт авторизации в браузере и завершается после неё,
# затем определяем email нового аккаунта по токену (через API) и сохраняем.
(
claude auth login --claudeai </dev/null >/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

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
# Сохраняет текущий effortLevel в кэш лаунчера при завершении сессии.
# /effort внутри Claude Code обновляет settings.json - мы читаем оттуда.
launcher="${AI_LAUNCHER:-}"
[ -z "$launcher" ] && exit 0
cat /dev/stdin > /dev/null 2>&1 # drain stdin (Claude Code передаёт JSON)
effort=$(python3 -c "
import json, os
p = os.path.expanduser('~/.claude/settings.json')
if os.path.exists(p):
try:
d = json.load(open(p))
print(d.get('effortLevel', ''))
except Exception:
pass
" 2>/dev/null)
[ -z "$effort" ] && exit 0
mkdir -p "$HOME/.cache/ai-setup"
echo "$effort" > "$HOME/.cache/ai-setup/effort_${launcher}"
exit 0

View File

@@ -1,33 +1,44 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# UserPromptSubmit hook: перехватывает /switch-account и обновляет аккаунт. # UserPromptSubmit hook: перехватывает /switch-account и циклически меняет аккаунт.
# Используем exit 0 (не exit 2) чтобы Claude ответил - это единственный способ # Текущий аккаунт определяется ПО ТОКЕНУ в .credentials.json (account-email.sh),
# обновить статусную строку, т.к. Claude Code перерисовывает её только после ответа LLM. # а не по claude auth status — он читает рассинхронизированный oauthAccount.
# На Linux Claude Code перечитывает .credentials.json на лету: новый аккаунт
# применяется со следующего сообщения, перезапуск не нужен.
# exit 0 (не exit 2): /switch-account доходит до Claude, грузится скилл,
# отвечает "✓" (1 токен) — так перерисовывается статусная строка.
input=$(cat) input=$(cat)
prompt=$(echo "$input" | jq -r '.user_prompt // .prompt // empty' 2>/dev/null) prompt=$(echo "$input" | jq -r '.user_prompt // .prompt // empty' 2>/dev/null)
# Нормализуем: убираем пробелы и слэш в начале
normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||') normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||')
[ "$normalized" != "switch-account" ] && exit 0 [ "$normalized" != "switch-account" ] && exit 0
# --- Переключаем аккаунт ---
ACCOUNTS_DIR="$HOME/.claude/accounts" ACCOUNTS_DIR="$HOME/.claude/accounts"
CREDS="$HOME/.claude/.credentials.json" CREDS="$HOME/.claude/.credentials.json"
CURRENT_FILE="$ACCOUNTS_DIR/current" CURRENT_FILE="$ACCOUNTS_DIR/current"
EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh"
mkdir -p "$ACCOUNTS_DIR" mkdir -p "$ACCOUNTS_DIR"
# Реальный текущий аккаунт — по токену активной сессии (не по хрупкому current)
current=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null)
[ -z "$current" ] && current=$(cat "$CURRENT_FILE" 2>/dev/null)
# Сохранить актуальные (возможно обновлённые рефрешем) токены под реальным email.
# current выведен из самого токена — порча файла другого аккаунта исключена.
if [ -n "$current" ] && [ -f "$CREDS" ]; then
cp "$CREDS" "$ACCOUNTS_DIR/${current}.credentials.json"
chmod 600 "$ACCOUNTS_DIR/${current}.credentials.json"
fi
mapfile -t accounts < <(ls "$ACCOUNTS_DIR"/*.credentials.json 2>/dev/null \ mapfile -t accounts < <(ls "$ACCOUNTS_DIR"/*.credentials.json 2>/dev/null \
| xargs -I{} basename {} .credentials.json | sort) | xargs -I{} basename {} .credentials.json | sort)
if [ ${#accounts[@]} -eq 0 ]; then if [ ${#accounts[@]} -le 1 ]; then
echo "Аккаунты не настроены. Создай ~/.claude/accounts/<name>.credentials.json для каждого аккаунта." >&2 echo "Только один аккаунт (${current:-нет}). Добавь второй через /add-account." >&2
exit 2 exit 2
fi fi
current=$(cat "$CURRENT_FILE" 2>/dev/null || echo "")
# Найти следующий по кругу # Найти следующий по кругу
idx=-1 idx=-1
for i in "${!accounts[@]}"; do for i in "${!accounts[@]}"; do
@@ -36,17 +47,8 @@ done
next_idx=$(( (idx + 1) % ${#accounts[@]} )) next_idx=$(( (idx + 1) % ${#accounts[@]} ))
next="${accounts[$next_idx]}" next="${accounts[$next_idx]}"
# Сохранить текущие (возможно обновлённые Claude Code) токены обратно в файл аккаунта
if [ -n "$current" ] && [ -f "$CREDS" ]; then
cp "$CREDS" "$ACCOUNTS_DIR/${current}.credentials.json"
chmod 600 "$ACCOUNTS_DIR/${current}.credentials.json"
fi
cp "$ACCOUNTS_DIR/${next}.credentials.json" "$CREDS" cp "$ACCOUNTS_DIR/${next}.credentials.json" "$CREDS"
chmod 600 "$CREDS" chmod 600 "$CREDS"
echo "$next" > "$CURRENT_FILE" echo "$next" > "$CURRENT_FILE"
# exit 0 (не exit 2): оригинальный /switch-account доходит до Claude,
# Claude загружает скилл switch-account, скилл велит ответить только "✓" (1 токен).
# Это единственный способ обновить статусную строку без лишних токенов.
exit 0 exit 0

View File

@@ -0,0 +1,8 @@
---
name: add-account
description: Add a new Claude.ai account (handled by UserPromptSubmit hook, no LLM needed)
---
Хук сохранил текущий аккаунт и открыл браузер для логина нового. Ответь ТОЛЬКО этим текстом (без markdown, без лишних слов):
Браузер открыт — авторизуйся там. После авторизации новый аккаунт сохранится автоматически и сразу станет активным (Claude Code на Linux перечитывает токен на лету). /switch-account переключает между всеми сохранёнными аккаунтами по кругу.

View File

@@ -10,31 +10,44 @@ week_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty') ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
# Цвет effort: мягкая гармоничная палитра # Цвет effort: насыщенные, глубже чем пастельные (jewel tones)
_effort_color() { _effort_color() {
case "$1" in case "$1" in
low) printf '\033[38;5;220m[low]\033[00m' ;; # золотой low) printf '\033[38;5;220m[low]\033[00m' ;; # золотой
medium) printf '\033[38;5;43m[medium]\033[00m' ;; # бирюзовый medium) printf '\033[38;5;50m[medium]\033[00m' ;; # насыщенный teal
high) printf '\033[38;5;39m[high]\033[00m' ;; # небесно-голубой high) printf '\033[38;5;38m[high]\033[00m' ;; # глубокий голубой
xhigh) printf '\033[38;5;171m[xhigh]\033[00m' ;; # лавандовый xhigh) printf '\033[38;5;206m[xhigh]\033[00m' ;; # насыщенный фиолетовый
max) printf '\033[38;5;210m[\033[38;5;220mm\033[38;5;114ma\033[38;5;43mx\033[38;5;171m]\033[00m' ;; # радуга max) printf '\033[38;5;210m[\033[38;5;220mm\033[38;5;114ma\033[38;5;50mx\033[38;5;206m]\033[00m' ;; # радуга
*) printf '\033[38;5;250m[%s]\033[00m' "$1" ;; *) printf '\033[38;5;252m[%s]\033[00m' "$1" ;;
esac
}
# Брендовый цвет имени модели по лаунчеру
_brand_color() {
case "${1:-}" in
deepseek) printf '\033[38;5;69m' ;; # DeepSeek фирменный синий
claude) printf '\033[38;5;173m' ;; # Anthropic оранжевый
kimi) printf '\033[38;5;81m' ;; # Kimi голубой
openrouter) printf '\033[38;5;135m' ;; # OpenRouter фиолетовый
*) printf '\033[38;5;223m' ;; # кремовый (fallback)
esac esac
} }
branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null) branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null)
short_cwd="${cwd/#$HOME/\~}" short_cwd="${cwd/#$HOME/\~}"
printf "\033[38;5;250m%s\033[00m" "$short_cwd" printf "\033[38;5;252m%s\033[00m" "$short_cwd"
[ -n "$branch" ] && printf " \033[38;5;250m[%s]\033[00m" "$branch" [ -n "$branch" ] && printf " \033[38;5;252m[%s]\033[00m" "$branch"
if [ -n "$model" ]; then if [ -n "$model" ]; then
brand_color=$(_brand_color "${AI_LAUNCHER:-}")
effort=$(echo "$input" | jq -r ".effort.level // empty") effort=$(echo "$input" | jq -r ".effort.level // empty")
# Сохраняем effort для persistence между сессиями одного лаунчера # Ловим выбранный уровень в кэш лаунчера (чтобы запомнить max между сессиями).
if [ -n "$effort" ] && [ -n "${AI_LAUNCHER:-}" ]; then # Когда CLAUDE_CODE_EFFORT_LEVEL выставлена (восстановленная max-сессия) - уровень
# форсится env, кэш НЕ трогаем, чтобы дисплей-баг (.effort.level=xhigh) не затёр max.
if [ -n "${AI_LAUNCHER:-}" ] && [ -z "${CLAUDE_CODE_EFFORT_LEVEL:-}" ] && [ -n "$effort" ]; then
effort_file="$HOME/.cache/ai-setup/effort_${AI_LAUNCHER}" effort_file="$HOME/.cache/ai-setup/effort_${AI_LAUNCHER}"
prev_effort=$(cat "$effort_file" 2>/dev/null) if [ "$effort" != "$(cat "$effort_file" 2>/dev/null)" ]; then
if [ "$effort" != "$prev_effort" ]; then
mkdir -p "$HOME/.cache/ai-setup" mkdir -p "$HOME/.cache/ai-setup"
echo "$effort" > "$effort_file" echo "$effort" > "$effort_file"
fi fi
@@ -79,13 +92,13 @@ if [ -n "$model" ]; then
fi fi
fi fi
fi fi
[ -n "$account" ] && printf " \033[38;5;223m[%s]\033[00m" "$account" [ -n "$account" ] && printf " %s[%s]\033[00m" "$brand_color" "$account"
fi fi
if [ -n "$effort" ]; then if [ -n "$effort" ]; then
printf " \033[38;5;223m%s " "$model" printf " %s%s " "$brand_color" "$model"
_effort_color "$effort" _effort_color "$effort"
else else
printf " \033[38;5;223m%s\033[00m" "$model" printf " %s%s\033[00m" "$brand_color" "$model"
fi fi
fi fi
@@ -114,7 +127,7 @@ pct_color() {
if [ "$pct" -lt 40 ]; then if [ "$pct" -lt 40 ]; then
printf '\033[38;5;114m' # мягкий зелёный printf '\033[38;5;114m' # мягкий зелёный
elif [ "$pct" -lt 60 ]; then elif [ "$pct" -lt 60 ]; then
printf '\033[38;5;221m' # золотистый printf '\033[38;5;220m' # золотой (как effort low)
else else
printf '\033[38;5;210m' # мягкий красный printf '\033[38;5;210m' # мягкий красный
fi fi
@@ -127,7 +140,7 @@ if [[ "$model_id" == *deepseek* ]]; then
cache_file="$HOME/.cache/ai-setup/deepseek_balance" cache_file="$HOME/.cache/ai-setup/deepseek_balance"
if [ -f "$cache_file" ]; then if [ -f "$cache_file" ]; then
balance=$(head -1 "$cache_file") balance=$(head -1 "$cache_file")
[ -n "$balance" ] && printf " \033[38;5;147m%s\033[00m" "$balance" [ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
fi fi
# Фоновое обновление баланса (не чаще раза в 30 секунд) # Фоновое обновление баланса (не чаще раза в 30 секунд)
@@ -157,6 +170,45 @@ for info in infos:
parts.append(f'{sym}{total}') parts.append(f'{sym}{total}')
if parts: if parts:
print(' '.join(parts)) print(' '.join(parts))
" 2>/dev/null)
if [ -n "$new_balance" ]; then
echo "$new_balance" > "$cache_file"
fi
fi
) &
fi
fi
elif [ "${AI_LAUNCHER:-}" = "openrouter" ]; then
# --- Баланс OpenRouter ---
# Моментально показываем кэшированный остаток, в фоне обновляем через API.
cache_file="$HOME/.cache/ai-setup/openrouter_balance"
if [ -f "$cache_file" ]; then
balance=$(head -1 "$cache_file")
[ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
fi
# Фоновое обновление баланса (не чаще раза в 30 секунд)
refresh_ts="$HOME/.cache/ai-setup/openrouter_balance_refresh_ts"
now=$(date +%s)
last=$(cat "$refresh_ts" 2>/dev/null || echo 0)
if [ $(( now - last )) -gt 30 ]; then
key_file="$HOME/.config/ai-setup/openrouter_key"
if [ -f "$key_file" ]; then
echo "$now" > "$refresh_ts" 2>/dev/null
(
api_key=$(cat "$key_file")
resp=$(curl -s --max-time 10 "https://openrouter.ai/api/v1/credits" \
-H "Authorization: Bearer $api_key" \
-H "Accept: application/json" 2>/dev/null)
if [ -n "$resp" ]; then
new_balance=$(echo "$resp" | python3 -c "
import sys, json
d = json.load(sys.stdin)
data = d.get('data', {})
total = data.get('total_credits', 0) or 0
usage = data.get('total_usage', 0) or 0
remaining = total - usage
print(f'${remaining:.2f}')
" 2>/dev/null) " 2>/dev/null)
if [ -n "$new_balance" ]; then if [ -n "$new_balance" ]; then
echo "$new_balance" > "$cache_file" echo "$new_balance" > "$cache_file"
@@ -166,9 +218,10 @@ if parts:
fi fi
fi fi
else else
# Рейт-лимиты для НЕ-DeepSeek провайдеров # Рейт-лимиты для НЕ-DeepSeek/OpenRouter провайдеров
# Кеш специфичен для провайдера (по model_id) чтобы не смешивать claude/kimi/openrouter # Кеш специфичен для провайдера (model_id) И аккаунта (account): лимиты привязаны
_cache_key=$(echo "${model_id:-unknown}" | sed 's/[^a-zA-Z0-9._-]/_/g') # к аккаунту, поэтому при переключении /switch-account проценты не должны смешиваться.
_cache_key=$(echo "${model_id:-unknown}_${account:-}" | sed 's/[^a-zA-Z0-9._-]/_/g')
RATE_CACHE="$HOME/.cache/ai-setup/rate_limits_${_cache_key}.cache" RATE_CACHE="$HOME/.cache/ai-setup/rate_limits_${_cache_key}.cache"
mkdir -p "$HOME/.cache/ai-setup" mkdir -p "$HOME/.cache/ai-setup"

View File

@@ -683,38 +683,49 @@ else
warn "Файл $STATUSLINE_SRC не найден, пропускаю" warn "Файл $STATUSLINE_SRC не найден, пропускаю"
fi fi
# ── 6.7.0. Хук effort-save (сохраняет effort при завершении сессии) ── # ── 6.7.0. Чистка устаревшего хука effort-save ──────────────
info "Деплою хук effort-save..." # effort/model теперь персистятся нативно в settings.json каждого CLAUDE_CONFIG_DIR
EFFORT_HOOK_SRC="$SCRIPT_DIR/home-configs/claude/hooks/effort-save-hook.sh" # (полная изоляция лаунчеров), самопальный кэш ~/.cache/ai-setup/{effort,model}_* не нужен.
EFFORT_HOOK_DST="$HOME/.claude/hooks/effort-save-hook.sh" # Удаляем старый хук с диска и из ~/.claude/settings.json (нотифаер в Stop сохраняем).
mkdir -p "$HOME/.claude/hooks" info "Удаляю устаревший хук effort-save..."
if [ -f "$EFFORT_HOOK_SRC" ]; then rm -f "$HOME/.claude/hooks/effort-save-hook.sh"
cp "$EFFORT_HOOK_SRC" "$EFFORT_HOOK_DST" # Осиротевший кэш моделей (источник старой протечки между ai-*); effort_* НЕ трогаем -
chmod +x "$EFFORT_HOOK_DST" # он снова используется для персиста effort (включая max) через CLAUDE_CODE_EFFORT_LEVEL.
python3 - "$HOME/.claude/settings.json" "$EFFORT_HOOK_DST" <<'PYEOF' rm -f "$HOME"/.cache/ai-setup/model_*
python3 - "$HOME/.claude/settings.json" <<'PYEOF'
import sys, json, os import sys, json, os
settings_path, hook_path = sys.argv[1], sys.argv[2] settings_path = sys.argv[1]
data = {} if not os.path.exists(settings_path):
if os.path.exists(settings_path): sys.exit(0)
try:
with open(settings_path) as f: with open(settings_path) as f:
try: data = json.load(f) data = json.load(f)
except json.JSONDecodeError: pass except Exception:
data.setdefault("hooks", {}).setdefault("Stop", [{"hooks": []}]) sys.exit(0)
hook_cmd = f'bash "{hook_path}"' stop = data.get("hooks", {}).get("Stop")
stop_hooks = data["hooks"]["Stop"] if isinstance(stop, list):
already = any( for entry in stop:
any(h.get("command", "") == hook_cmd for h in entry.get("hooks", [])) hooks = entry.get("hooks")
for entry in stop_hooks if isinstance(hooks, list):
) entry["hooks"] = [h for h in hooks if "effort-save-hook" not in h.get("command", "")]
if not already: data["hooks"]["Stop"] = [e for e in stop if e.get("hooks")]
stop_hooks[0]["hooks"].append({"type": "command", "command": hook_cmd}) if not data["hooks"]["Stop"]:
with open(settings_path, "w") as f: del data["hooks"]["Stop"]
with open(settings_path, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False) json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n") f.write("\n")
PYEOF PYEOF
success "Хук effort-save установлен" success "Старый хук effort-save удалён"
else
warn "Файл $EFFORT_HOOK_SRC не найден, пропускаю" # ── 6.7.05. Хелпер account-email (определение email по токену) ──
# Вспомогательный скрипт для хуков switch-account/add-account.
# Не регистрируется в settings.json — вызывается из хуков напрямую.
EMAIL_HELPER_SRC="$SCRIPT_DIR/home-configs/claude/hooks/account-email.sh"
EMAIL_HELPER_DST="$HOME/.claude/hooks/account-email.sh"
mkdir -p "$HOME/.claude/hooks"
if [ -f "$EMAIL_HELPER_SRC" ]; then
cp "$EMAIL_HELPER_SRC" "$EMAIL_HELPER_DST"
chmod +x "$EMAIL_HELPER_DST"
fi fi
# ── 6.7.1. Хук switch-account ─────────────────────────────────── # ── 6.7.1. Хук switch-account ───────────────────────────────────
@@ -752,6 +763,41 @@ else
warn "Файл $SWITCH_HOOK_SRC не найден, пропускаю" warn "Файл $SWITCH_HOOK_SRC не найден, пропускаю"
fi fi
# ── 6.7.2. Хук add-account ──────────────────────────────────────
info "Деплою хук add-account..."
ADD_HOOK_SRC="$SCRIPT_DIR/home-configs/claude/hooks/add-account-hook.sh"
ADD_HOOK_DST="$HOME/.claude/hooks/add-account-hook.sh"
mkdir -p "$HOME/.claude/hooks"
if [ -f "$ADD_HOOK_SRC" ]; then
cp "$ADD_HOOK_SRC" "$ADD_HOOK_DST"
chmod +x "$ADD_HOOK_DST"
# Прописываем хук в settings.json (идемпотентно)
python3 - "$HOME/.claude/settings.json" "$ADD_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 "Хук add-account установлен"
else
warn "Файл $ADD_HOOK_SRC не найден, пропускаю"
fi
# ── 6.8. Регистрация официального маркетплейса плагинов Claude ── # ── 6.8. Регистрация официального маркетплейса плагинов Claude ──
info "Настраиваю маркетплейс плагинов Claude Code..." info "Настраиваю маркетплейс плагинов Claude Code..."
if ! command -v claude &>/dev/null; then if ! command -v claude &>/dev/null; then
@@ -920,6 +966,39 @@ except Exception as e:
" 2>/dev/null || echo -e " \033[0;33m[БАЛАНС]\033[0m Ошибка парсинга ответа" " 2>/dev/null || echo -e " \033[0;33m[БАЛАНС]\033[0m Ошибка парсинга ответа"
} }
# _openrouter_balance: Query OpenRouter credits API and print balance
# Uses /api/v1/credits (works with regular API key too)
_openrouter_balance() {
local api_key="$1"
local response
response=$(curl -s --max-time 10 "https://openrouter.ai/api/v1/credits" \
-H "Authorization: Bearer $api_key" \
-H "Accept: application/json" \
2>/dev/null || echo "")
if [ -z "$response" ]; then
echo -e " \033[0;33m[БАЛАНС]\033[0m Не удалось получить баланс (сеть?)"
return 1
fi
echo "$response" | python3 -c "
import sys, json, os
try:
d = json.load(sys.stdin)
data = d.get('data', {})
total = data.get('total_credits', 0) or 0
usage = data.get('total_usage', 0) or 0
remaining = total - usage
cache_dir = os.path.expanduser('~/.cache/ai-setup')
os.makedirs(cache_dir, exist_ok=True)
cache_file = os.path.join(cache_dir, 'openrouter_balance')
# Зелёный как у DeepSeek: \033[38;5;78m
print(f' \033[1;36m💰 Баланс OpenRouter:\033[0m \033[38;5;78m\${remaining:.2f}\033[0m (загружено \${total:.2f}, потрачено \${usage:.2f})')
with open(cache_file, 'w') as f:
f.write(f'\${remaining:.2f}\n')
except Exception as e:
print(f' ⚠️ Не удалось разобрать баланс: {e}')
" 2>/dev/null || echo -e " \033[0;33m[БАЛАНС]\033[0m Ошибка парсинга ответа"
}
_handle_openai_api_response() { _handle_openai_api_response() {
local provider="$1" local provider="$1"
local code="$2" local code="$2"
@@ -1077,35 +1156,97 @@ _open_browser() {
else echo "Откройте вручную: $url"; fi else echo "Откройте вручную: $url"; fi
} }
# _restore_effort: читает сохранённый effort для текущего AI_LAUNCHER из кэша # _setup_isolated_config: готовит изолированную папку CLAUDE_CONFIG_DIR для лаунчера.
# и записывает его в settings.json, чтобы Claude Code подхватил нужный уровень. # Каждый сторонний провайдер получает собственные settings.json и .claude.json,
# Не передаём --effort через CLI, чтобы /effort внутри сессии работал без блокировки. # поэтому выбор модели и кэш кастом-моделей НЕ протекают между ai-* лаунчерами.
_restore_effort() { # Общие ресурсы (skills, CLAUDE.md) шарятся симлинком из ~/.claude.
local default_effort="${1:-high}" # model и effortLevel сидируются как дефолты - выбор юзера через /model и /effort
local launcher="${AI_LAUNCHER:-}" # (для low/medium/high/xhigh) сохраняется нативно в этом же settings.json.
[ -z "$launcher" ] && return # Уровень max обрабатывается отдельно в _apply_effort (settings.json его не хранит).
local effort_file="$HOME/.cache/ai-setup/effort_${launcher}" # Использование: _setup_isolated_config <launcher> <default_model> <default_effort> <available_models_json>
local effort _setup_isolated_config() {
effort=$(cat "$effort_file" 2>/dev/null) local launcher="$1" default_model="$2" default_effort="${3:-high}" avail="${4:-}"
[ -z "$effort" ] && effort="$default_effort" local cfg="$HOME/.config/ai-setup/cfg/$launcher"
mkdir -p "$HOME/.cache/ai-setup" mkdir -p "$cfg"
python3 - "$HOME/.claude/settings.json" "$effort" <<'PYEOF' # Общие ресурсы из ~/.claude (единый источник правды)
ln -sfn "$HOME/.claude/skills" "$cfg/skills"
ln -sfn "$HOME/.claude/CLAUDE.md" "$cfg/CLAUDE.md"
[ -e "$HOME/.claude/agents" ] && ln -sfn "$HOME/.claude/agents" "$cfg/agents"
# .claude.json в свежей папке: пропускаем онбординг
[ -f "$cfg/.claude.json" ] || echo '{"hasCompletedOnboarding": true}' > "$cfg/.claude.json"
python3 - "$cfg/settings.json" "$HOME/.claude/statusline-command.sh" \
"$default_model" "$default_effort" "$avail" <<'PYEOF'
import sys, json, os import sys, json, os
settings_path, effort = sys.argv[1], sys.argv[2] cfg_settings, statusline, model, effort, avail = sys.argv[1:6]
data = {} data = {}
if os.path.exists(settings_path): if os.path.exists(cfg_settings):
try: try:
with open(settings_path) as f: with open(cfg_settings) as f:
data = json.load(f) data = json.load(f)
except Exception: except Exception:
pass pass
data['effortLevel'] = effort # Структурные настройки лаунчера (переустанавливаем всегда)
with open(settings_path, 'w') as f: data["statusLine"] = {"type": "command", "command": f"bash {statusline}"}
data["skipDangerousModePermissionPrompt"] = True
data.setdefault("hooks", {})["SessionStart"] = [{"hooks": [{"type": "command", "command": "true"}]}]
# availableModels - белый список пикера (политика лаунчера)
if avail:
data["availableModels"] = json.loads(avail)
else:
data.pop("availableModels", None)
# model/effortLevel - сидируем дефолты, не перетирая выбор юзера (нативный persistence)
if model:
data.setdefault("model", model)
data.setdefault("effortLevel", effort)
with open(cfg_settings, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False) json.dump(data, f, indent=2, ensure_ascii=False)
f.write('\n') f.write("\n")
PYEOF PYEOF
} }
# _apply_effort: per-launcher persistence уровня effort (гибрид).
# - low/medium/high/xhigh живут нативно в settings.json лаунчера -> /effort работает,
# уровень сохраняется между сессиями, env-переменная НЕ ставится.
# - max единственный нельзя сохранить в settings.json (он session-only), поэтому
# его восстанавливаем через CLAUDE_CODE_EFFORT_LEVEL. В такой max-сессии /effort
# залочен env-переменной (ограничение Claude Code).
# Текущий уровень (вкл. max) ловит статусбар в ~/.cache/ai-setup/effort_<launcher>.
# Сменить уровень из max: AI_EFFORT=<lvl> ai-<launcher>.
# Использование: _apply_effort <launcher> <default_effort>
_apply_effort() {
local launcher="$1" default_effort="${2:-high}"
local f="$HOME/.cache/ai-setup/effort_${launcher}"
local settings="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
local eff
if [ -n "${AI_EFFORT:-}" ]; then
# Явный override: запоминаем и применяем
eff="$AI_EFFORT"
mkdir -p "$HOME/.cache/ai-setup"
echo "$eff" > "$f"
else
eff=$(cat "$f" 2>/dev/null)
fi
if [ "$eff" = "max" ]; then
# Единственный способ восстановить max между сессиями
export CLAUDE_CODE_EFFORT_LEVEL=max
elif [ -n "${AI_EFFORT:-}" ] && [ -n "$eff" ]; then
# Явный сброс на low/medium/high/xhigh - пишем нативно в settings.json лаунчера
python3 - "$settings" "$eff" <<'PYEOF'
import sys, json, os
p, eff = sys.argv[1], sys.argv[2]
d = {}
if os.path.exists(p):
try: d = json.load(open(p))
except Exception: pass
d["effortLevel"] = eff
os.makedirs(os.path.dirname(p), exist_ok=True)
with open(p, "w") as fp:
json.dump(d, fp, indent=2, ensure_ascii=False); fp.write("\n")
PYEOF
fi
# Иначе (≤xhigh без AI_EFFORT): ничего не делаем - effortLevel уже персистнут нативно.
}
_build_ai_sys_prompt() { _build_ai_sys_prompt() {
local global_rules="$HOME/.config/ai-setup/global_rules.md" local global_rules="$HOME/.config/ai-setup/global_rules.md"
local global_rendered="" local global_rendered=""
@@ -1228,14 +1369,27 @@ _PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE" _build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=deepseek export AI_LAUNCHER=deepseek
_restore_effort high export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/deepseek"
# Пикер: DeepSeek V4 Pro (opus+sonnet, дефолт) и DeepSeek V4 Flash (haiku).
# availableModels НЕ задаём: при кастомном провайдере он схлопывает пикер в Default.
# Claude Code навязывает 3 слота opus/sonnet/haiku; незаданный слот показал бы чужой
# Claude, поэтому sonnet тоже мапим на Pro (лёгкий дубль, но все пункты - DeepSeek).
# FABLE не навязывается - не задаём. DISABLE_1M убирает [1M] дубли из пикера.
_setup_isolated_config deepseek opus high ''
_apply_effort deepseek high
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \ ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \
ANTHROPIC_AUTH_TOKEN="$api_key" \ ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_MODEL=deepseek-v4-pro \
ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro \ ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="DeepSeek V4 Pro" \
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="DeepSeek V4 Pro - флагман для сложных задач" \
ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro \ ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="DeepSeek V4 Pro" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="DeepSeek V4 Pro - флагман для сложных задач" \
ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash \ ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="DeepSeek V4 Flash" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="DeepSeek V4 Flash - быстрый и дешёвый" \
CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash \ CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash \
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@" claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
DEEPSEEKEOF DEEPSEEKEOF
@@ -1254,7 +1408,7 @@ api_key=""
if [ -n "$api_key" ]; then if [ -n "$api_key" ]; then
echo -n "Проверка сохранённого Kimi ключа... " echo -n "Проверка сохранённого Kimi ключа... "
_claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.6" _claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.7"
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code" _handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code"
ret=$_API_RET ret=$_API_RET
if [ $ret -eq 401 ]; then if [ $ret -eq 401 ]; then
@@ -1274,7 +1428,7 @@ if [ -z "$api_key" ]; then
[ -z "$api_key" ] && { echo "Выход."; exit 1; } [ -z "$api_key" ] && { echo "Выход."; exit 1; }
echo -n "Проверяю ключ и баланс... " echo -n "Проверяю ключ и баланс... "
_claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.6" _claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.7"
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code" _handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code"
ret=$_API_RET ret=$_API_RET
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
@@ -1302,14 +1456,26 @@ _PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE" _build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=kimi export AI_LAUNCHER=kimi
_restore_effort high export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/kimi"
# Пикер: Opus/Sonnet = Kimi K2.7 Code, Haiku = Kimi K2.6. availableModels НЕ задаём
# (он схлопывает пикер в Default). Claude Code навязывает 3 слота opus/sonnet/haiku;
# незаданный показал бы чужой Claude, поэтому opus+sonnet = K2.7, haiku = K2.6
# DISABLE_1M убирает [1M] дубли из пикера.
_setup_isolated_config kimi opus high ''
_apply_effort kimi high
ANTHROPIC_BASE_URL=https://api.kimi.com/coding \ ANTHROPIC_BASE_URL=https://api.kimi.com/coding \
ANTHROPIC_AUTH_TOKEN="$api_key" \ ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_MODEL=kimi-k2.6 \ ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.7 \
ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.6 \ ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Kimi K2.7 Code" \
ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k2.6 \ ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="Kimi K2.7 Code — флагманский программист (Moonshot AI)" \
ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k2.7 \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="Kimi K2.7 Code" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Kimi K2.7 Code — флагманский программист (Moonshot AI)" \
ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6 \ ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6 \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="Kimi K2.6" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="Kimi K2.6 — быстрый универсал (Moonshot AI)" \
CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.6 \ CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.6 \
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@" claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
KIMIEOF KIMIEOF
@@ -1335,10 +1501,13 @@ if [ -n "$api_key" ]; then
rm -f "$key_file" rm -f "$key_file"
api_key="" api_key=""
elif [ $ret -eq 429 ]; then elif [ $ret -eq 429 ]; then
_openrouter_balance "$api_key"
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] " echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
elif [ $ret -ne 0 ]; then elif [ $ret -ne 0 ]; then
exit 1 exit 1
else
_openrouter_balance "$api_key"
fi fi
fi fi
@@ -1356,6 +1525,7 @@ if [ -z "$api_key" ]; then
echo "$api_key" > "$key_file" echo "$api_key" > "$key_file"
chmod 600 "$key_file" chmod 600 "$key_file"
echo "Ключ сохранён." echo "Ключ сохранён."
_openrouter_balance "$api_key"
if [ $ret -eq 429 ]; then if [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] " echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
@@ -1376,14 +1546,31 @@ _PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE" _build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=openrouter export AI_LAUNCHER=openrouter
_restore_effort high export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/openrouter"
# openrouter - модели, которых НЕТ в других ai-* лаунчерах (без anthropic/deepseek/
# kimi/gemini). Пикер строится из 4 алиас-слотов + 1 custom-пункта (потолок Claude Code).
# availableModels НЕ задаём (он схлопывает пикер). Дефолт - GPT-5.5 (custom-пункт).
export ANTHROPIC_CUSTOM_MODEL_OPTION="openai/gpt-5.5"
export ANTHROPIC_CUSTOM_MODEL_OPTION_NAME="GPT-5.5"
export ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION="OpenAI GPT-5.5 (OpenRouter)"
_setup_isolated_config openrouter "openai/gpt-5.5" high ''
_apply_effort openrouter high
ANTHROPIC_BASE_URL=https://openrouter.ai/api \ ANTHROPIC_BASE_URL=https://openrouter.ai/api \
ANTHROPIC_AUTH_TOKEN="$api_key" \ ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_MODEL=openai/gpt-5.5 \ ANTHROPIC_DEFAULT_OPUS_MODEL=x-ai/grok-4.20 \
ANTHROPIC_DEFAULT_OPUS_MODEL=anthropic/claude-4.8-opus \ ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Grok 4.20" \
ANTHROPIC_DEFAULT_SONNET_MODEL=anthropic/claude-4.6-sonnet \ ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="xAI Grok 4.20 (OpenRouter)" \
ANTHROPIC_DEFAULT_HAIKU_MODEL=openai/gpt-5.5 \ ANTHROPIC_DEFAULT_SONNET_MODEL=qwen/qwen3.7-max \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="Qwen3.7 Max" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Qwen3.7 Max (OpenRouter)" \
ANTHROPIC_DEFAULT_HAIKU_MODEL=minimax/minimax-m3 \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="MiniMax M3" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="MiniMax M3 (OpenRouter)" \
ANTHROPIC_DEFAULT_FABLE_MODEL=meta-llama/llama-4-maverick \
ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="Llama 4 Maverick" \
ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION="Meta Llama 4 Maverick (OpenRouter)" \
CLAUDE_CODE_SUBAGENT_MODEL=openai/gpt-5.5 \ CLAUDE_CODE_SUBAGENT_MODEL=openai/gpt-5.5 \
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@" claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
OPENROUTEREOF OPENROUTEREOF
@@ -1430,8 +1617,12 @@ _PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE" _build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=claude export AI_LAUNCHER=claude
_restore_effort xhigh # ai-claude работает в дефолтном ~/.claude (нативный логин и аккаунты).
claude --dangerously-skip-permissions --model sonnet --system-prompt-file "$_PROMPT_FILE" "$@" # Модель хранится нативно в ~/.claude/settings.json; другие ai-* лаунчеры теперь
# изолированы в своих CLAUDE_CONFIG_DIR, поэтому в пикер не протекают чужие модели -
# показываются только нативные модели Claude Code.
_apply_effort claude xhigh
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
CLAUDEEOF CLAUDEEOF
chmod +x "$BIN_DIR/ai-claude" chmod +x "$BIN_DIR/ai-claude"
@@ -1471,7 +1662,7 @@ echo ""
echo " На базе Claude Code:" echo " На базе Claude Code:"
echo -e " ${CYAN}ai-claude${NC} - Оригинальный Claude Code (Anthropic)" echo -e " ${CYAN}ai-claude${NC} - Оригинальный Claude Code (Anthropic)"
echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (через Claude Code, API ключ сохраняется)" echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (через Claude Code, API ключ сохраняется)"
echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.6 (через Claude Code, API ключ сохраняется)" echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.7 Code (через Claude Code, API ключ сохраняется)"
echo -e " ${CYAN}ai-openrouter${NC} - OpenRouter (через Claude Code: GPT-5.5, Opus 4.8, Sonnet 4.6)" echo -e " ${CYAN}ai-openrouter${NC} - OpenRouter (через Claude Code: GPT-5.5, Opus 4.8, Sonnet 4.6)"
echo "" echo ""
echo " Нативные CLI:" echo " Нативные CLI:"

View File

@@ -48,11 +48,12 @@ test_kimi_claude_launcher() {
# ── ai-kimi: uses official Kimi API ────────────────────────────────────── # ── ai-kimi: uses official Kimi API ──────────────────────────────────────
test_kimi_official_api() { test_kimi_official_api() {
if echo "$KIMI_SECTION" | grep -q 'api.kimi.com/coding' \ if echo "$KIMI_SECTION" | grep -q 'api.kimi.com/coding' \
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_MODEL=kimi-k2.6' \ && echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.7' \
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6' \
&& ! echo "$KIMI_SECTION" | grep -q 'artemox'; then && ! echo "$KIMI_SECTION" | grep -q 'artemox'; then
ok "ai-kimi: uses official Kimi API and model" ok "ai-kimi: uses official Kimi API (K2.7 opus/sonnet, K2.6 haiku)"
else else
fail "ai-kimi: must use official Kimi API (api.kimi.com/coding) and model kimi-k2.6" fail "ai-kimi: must use official Kimi API (api.kimi.com/coding) with K2.7 opus/sonnet, K2.6 haiku"
fi fi
} }