Compare commits

..

2 Commits

Author SHA1 Message Date
901af7d17d fix(codex): prevent sound hook timeouts 2026-08-08 19:09:04 +03:00
22b4daa5ce fix(add-account): unhang oauth login via fifo stdin
Логин Claude.ai подвисал навсегда: claude auth login ждёт код из браузера в stdin, а хук запускал его в фоне с </dev/null, куда код вставить невозможно. Теперь /add-account поднимает логин со stdin из fifo и печатает ссылку, а /add-account <код> отдаёт код, дожидается обмена и сохраняет новый аккаунт по email из токена. Провал логина больше не маскируется под успешное сохранение текущего аккаунта, а фоновые процессы гасятся строго по своим PID вместо широкого pkill -f. Поведение закреплено пятью тестами в tests/test_fixes.sh.
2026-08-08 18:33:54 +03:00
14 changed files with 257 additions and 1454 deletions

View File

@@ -2,4 +2,4 @@
Данные правила применяются ко всем агентам, работающим конкретно в этом репозитории.
1. **Полная перегенерация standalone-скриптов при запуске сетап-скрипта (Full regeneration of standalone scripts on setup run):** При каждом запуске `ai-setup.sh` все генерируемые скрипты в выбранном `BIN_DIR` (`~/bin`, если он уже есть в `PATH`, иначе `~/.local/bin`: `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-gemini`, `ai-api-helpers.sh`, `ai-claude`, `claude-gpt-effort-proxy.py`) должны быть **полностью перезаписаны** актуальными версиями. Запрещено выполнять слияние (merge) старого и нового содержимого или дополнение (append). Скрипт обязан привести все генерируемые файлы к эталонному виду, однозначно определяемому текущей конфигурацией.
1. **Полная перегенерация standalone-скриптов при запуске сетап-скрипта (Full regeneration of standalone scripts on setup run):** При каждом запуске `ai-setup.sh` все генерируемые скрипты в выбранном `BIN_DIR` (`~/bin`, если он уже есть в `PATH`, иначе `~/.local/bin`: `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-gemini`, `ai-api-helpers.sh`, `ai-claude`, `claude-gpt-effort-proxy.py`) должны быть **полностью перезаписаны** актуальными версиями. Запрещено выполнять слияние (merge) старого и нового содержимого или дополнение (append). Скрипт обязан привести все генерируемые файлы к эталонному виду, однозначно определяемому текущей конфигурацией.

View File

@@ -3,7 +3,7 @@
## Как это работает
`/effort` и `AI_EFFORT` относятся к лаунчерам, которые запускают Claude Code:
`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`.
`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`.
Нативные лаунчеры `ai-gpt` (OpenAI Codex), `ai-gemini` (Antigravity `agy`) и `ai-cursor`
(Cursor CLI `agent`) не используют эту схему. У них свои настройки reasoning внутри
@@ -13,13 +13,13 @@
## Таблица маппинга
| Claude Code `/effort` | Anthropic (`ai-claude`) | DeepSeek V4 (`ai-deepseek`) | Kimi K2.7/K2.6 (`ai-kimi`) | GLM 5.3 (`ai-glm`) | OpenRouter/GPT-5.5 (`ai-openrouter`) | OpenCode Go (`ai-opencode`) |
| Claude Code `/effort` | Anthropic (`ai-claude`) | DeepSeek V4 (`ai-deepseek`) | Kimi K2.7/K2.6 (`ai-kimi`) | GLM 5.2 (`ai-glm`) | OpenRouter/GPT-5.5 (`ai-openrouter`) | OpenCode Go (`ai-opencode`) |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| `low` | нативно `low` | нативно `low` | thinking on | нативно `low` | без локального маппинга | без локального маппинга |
| `medium` | нативно `medium` | провайдер поднимает до `high` | thinking on | провайдер опускает до `low` | без локального маппинга | без локального маппинга |
| `high` | нативно `high` | нативно `high` | thinking on | нативно `high` | без локального маппинга | без локального маппинга |
| `xhigh` | нативно `xhigh` | провайдер поднимает до `high` | thinking on | провайдер опускает до `high` | без локального маппинга | без локального маппинга |
| `max` | нативно `max` | нативно `max` | thinking on | нативно `max` | без локального маппинга | без локального маппинга |
| `low` | нативно `low` | провайдер поднимает до `high` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
| `medium` | нативно `medium` | провайдер поднимает до `high` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
| `high` | нативно `high` | нативно `high` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
| `xhigh` | нативно `xhigh` | провайдер поднимает до `max` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
| `max` | нативно `max` | нативно `max` | thinking on | на стороне сервера | без локального маппинга | без локального маппинга |
Для OpenRouter текущий лаунчер не делает локального преобразования effort и отправляет
значение дальше через Claude Code/OpenRouter. Конкретная интерпретация зависит от выбранной
@@ -45,13 +45,12 @@ low -> medium -> high -> xhigh -> max
### DeepSeek V4
```text
low -> high -> max
high -> max
```
- 3 реальных уровня.
- `low` остаётся `low` — реально снижает thinking бюджет.
- `medium` и `xhigh` фактически поднимаются до `high`.
- `max` остаётся `max` — максимальный бюджет thinking.
- 2 реальных уровня.
- `low` и `medium` фактически поднимаются до `high`.
- `xhigh` фактически поднимается до `max`.
- Маппинг выполняется на стороне DeepSeek API.
- Дефолт `ai-deepseek`: `high`.
@@ -66,25 +65,15 @@ on / off
- Градаций глубины reasoning нет.
- Дефолт `ai-kimi`: `high`.
### GLM 5.3 (Zhipu AI)
### GLM 5.2 (Zhipu AI)
```text
low -> high -> max
high / max
```
- 3 реальных уровня, thinking выключить нельзя (`thinking.type=disabled` тоже даёт reasoning).
- Anthropic-endpoint Z.ai читает `output_config.effort` от Claude Code и принимает все 5 уровней
(невалидное значение -> HTTP 400 `reasoning_effort must be one of: none, minimal, low, medium, high, xhigh, max`).
- `medium` фактически опускается до `low`, `xhigh` - до `high`.
- Если effort не передан, Z.ai использует `max`. Поэтому `ai-glm` выставляет
`CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1` - иначе Claude Code не считает `glm-5.3` effort-capable
и параметр не уходит.
- Маппинг выполняется на стороне Z.ai API, `ai-glm` локально effort не преобразует.
- Дефолт `ai-glm`: `xhigh` (нативный `high` у Z.ai), задаётся через CLI `--effort` при каждом запуске.
- Официально Z.ai маппинг `medium`/`xhigh` не документирует. Он выведен замером 2026-09-25:
по 3 запроса на уровень, средний `output_tokens` на счётной задаче - `low` ~1.5K,
`medium` ~1.1K, `high` ~3.4K, `xhigh` ~2.8K, `max` ~3.8K. `low`/`medium` уверенно короче
остальных, а `high` и `max` на такой выборке пересекаются по разбросу.
- Маппинг выполняется на стороне Zhipu AI API.
- Текущий `ai-glm` не делает локального преобразования effort.
- Дефолт `ai-glm`: `high`.
### OpenRouter / GPT-5.5
@@ -99,51 +88,6 @@ low -> high -> max
Effort не преобразуется локально — thinking budget не имеет прямого эффекта на OpenCode Go бекенд.
Дефолт `ai-opencode`: `high`.
### Xiaomi MiMo V2.6
```text
thinking on (единственный уровень)
```
- `ai-mimo` работает через Claude Code с Anthropic-эндпоинтом Xiaomi (Token Plan региона
подписки или `api.xiaomimimo.com`).
- Claude Code 2.1.294 отправляет `thinking: {type: "adaptive"}` и `output_config.effort` со всеми
5 уровнями и для `mimo-v2.6-pro`, и для `mimo-v2.6-flash`, даже без
`CLAUDE_CODE_ALWAYS_ENABLE_EFFORT` (проверено перехватом запроса).
- MiMo effort игнорирует. В доке Anthropic API MiMo из параметров рассуждения есть только
`thinking.type` (`enabled` по умолчанию / `disabled` / `adaptive`), `effort` и `budget_tokens` нет.
Невалидные `output_config.effort=bogus` и `thinking.type=bogus` дают HTTP 200.
- Замер 2026-10-13, по 3 запроса на уровень, средний `output_tokens`. Pro, счётная задача:
без effort ~310, `low` ~340, `high` ~280, `max` ~315. Pro, задача на включения-исключения:
`low` ~970, `max` ~825. Flash на ней же: `low` ~460, `max` ~580. Разброс внутри уровня больше
разницы между уровнями, тренда нет.
- Реально переключается только thinking: `thinking.type=disabled` даёт 4 токена без размышлений
и неверные ответы (2 из 3). Claude Code его для MiMo не отправляет.
- `thinking.type=adaptive` (его шлёт Claude Code) MiMo отрабатывает так же, как `enabled`:
размышления есть даже на «скажи привет», на задаче на включения-исключения `adaptive` ~670,
`enabled` ~600 токенов при разбросе 356-1161. `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1`
в Claude Code 2.1.294 запрос не меняет, да и не нужен.
- Глубину размышлений ограничивает только `max_tokens`: `ai-mimo` выставляет
`CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000` (потолок Claude Code, MiMo принимает до 131072).
- Дефолт `ai-mimo`: `max`, задаётся через CLI `--effort` при каждом запуске (как у `ai-eltex-qwen`).
На размышления не влияет, но если Xiaomi начнёт учитывать effort, лаунчер сразу будет на максимуме.
### Qwen3.8 (Alibaba Token Plan)
```text
low -> medium -> high -> xhigh -> max (принимаются, у Max тренда нет)
```
- `ai-qwen` работает через Claude Code с Anthropic-эндпоинтом Alibaba Model Studio
(`token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic`).
- Qwen проверяет `output_config.effort`: `bogus` даёт HTTP 400 со списком
`low, medium, high, xhigh, max`. `thinking.type=adaptive` принимается, размышления есть всегда.
- Замер 2026-10-20, задача на включения-исключения, `output_tokens`. `qwen3.8-max`, по 3 запроса:
`low` 435/513/615, `high` 363/412/662, `max` 397/539/657 - разброс больше разницы, тренда нет.
`qwen3.8-flash`, по 2 запроса: `low` ~430, `max` ~790.
- Дефолт `ai-qwen`: `max` через CLI `--effort` при каждом запуске, `CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1`
как страховка.
`claude-gpt-effort-proxy.py` все еще генерируется как совместимый helper для старого
`claude-code-proxy` backend-а. Если использовать его вручную, он маппит только
`xhigh` -> `high`, потому что `claude-code-proxy` принимает `low`, `medium`, `high`, `max`.
@@ -155,16 +99,14 @@ low -> medium -> high -> xhigh -> max (принимаются, у Max тренд
| Anthropic | Не нужен | - |
| DeepSeek V4 | DeepSeek API | На стороне сервера |
| Kimi K2.7/K2.6 | Moonshot API | На стороне сервера |
| GLM 5.3 | Z.ai API (`low,medium→low`, `high,xhigh→high`, `max`) | На стороне сервера |
| Xiaomi MiMo V2.6 | Не маппится: MiMo принимает `output_config.effort`, но игнорирует его | - |
| Qwen3.8 | Alibaba API, значение уходит как есть | На стороне сервера |
| GLM 5.2 | Zhipu AI API | На стороне сервера |
| OpenRouter/GPT-5.5 | Локального маппинга нет | `ai-openrouter` отправляет значение как есть |
| OpenCode Go | Локального маппинга нет | `ai-opencode` отправляет значение через routatic-proxy |
| Legacy GPT proxy | helper-маппинг `xhigh` -> `high` | `~/bin/claude-gpt-effort-proxy.py` или `~/.local/bin/claude-gpt-effort-proxy.py` |
## Persistence effort между сессиями
Каждый Claude Code лаунчер (`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`)
Каждый Claude Code лаунчер (`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`)
запоминает свой уровень effort отдельно в `settings.json` лаунчера.
`/effort` внутри сессии работает для всех уровней. `CLAUDE_CODE_EFFORT_LEVEL` не используется.
@@ -177,17 +119,10 @@ AI_EFFORT=high ai-deepseek # включить high
ai-deepseek # без флага - использовать уровень из settings.json
```
Дефолты: `xhigh` для `ai-claude` и `ai-glm`, `max` для `ai-eltex`, `ai-eltex-qwen`, `ai-mimo` и `ai-qwen`, `high` для
остальных Claude Code лаунчеров. `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo` и `ai-qwen` передают уровень через CLI
`--effort`, поэтому каждый запуск стартует на дефолте (или на `AI_EFFORT`), а `/effort` внутри сессии
действует до её конца.
Дефолты: `xhigh` для `ai-claude`, `high` для остальных Claude Code лаунчеров.
## Рекомендации
- Для повседневной работы: `high` или `xhigh`.
- `max` effort имеет реальный эффект у Anthropic, DeepSeek и GLM; у Kimi это всё тот же thinking on.
- `low`/`medium` у Kimi фактически не снижают reasoning; у DeepSeek `low` и у GLM `low`/`medium` дают реальный уровень `low` и экономят бюджет.
- У GLM осмысленно выбирать только `low`, `high` (или `xhigh`) и `max`.
- `ai-eltex` (21.10): effort у `anth-glm-5.3` (Anthropic-эндпоинт Z.ai через прокси) работает: `low` - без
размышлений, `medium`/`high`/`max` - с размышлениями. Как и `ai-glm`, стартует на `max`
через CLI `--effort`, `AI_EFFORT=high ai-eltex` работает. У `ai-eltex-qwen` прокси effort игнорирует, `/effort` и `AI_EFFORT` там ни на что не влияют.
- `max` effort имеет реальный эффект у Anthropic и DeepSeek; у Kimi и GLM это всё тот же thinking on / маппинг на стороне сервера.
- `low`/`medium` у DeepSeek, Kimi и GLM фактически не снижают reasoning.

View File

@@ -7,7 +7,7 @@ bash setup.sh
```
Установит лаунчеры
`ai-claude`, `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`, `ai-gemini`, `ai-cursor`
`ai-claude`, `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`, `ai-gemini`, `ai-cursor`
в `~/bin`, если он уже есть в `PATH`, иначе в `~/.local/bin/`, и запишет API-ключи.
После установки, если команды не видны:
@@ -15,11 +15,6 @@ bash setup.sh
exec bash
```
Обновление всех AI-инструментов одной командой:
```bash
./setup.sh update
```
## 2. Настроить сеть (Amnezia + ru-bypass)
Нужно один раз на каждой машине. .ru трафик идёт напрямую,

View File

@@ -56,15 +56,6 @@ bash setup.sh
exec bash
```
### Обновление одной командой
```bash
./setup.sh update # или: ./setup.sh --update
```
То же, что пункт меню `7) Обновить`: `git pull --ff-only` + перегенерация всех
`ai-*` лаунчеров через `scripts/ai-setup.sh`. Без аргументов открывается меню.
## Сеть: ru-bypass + kill switch
Полная документация: [`home-configs/network/README.md`](home-configs/network/README.md)
@@ -113,7 +104,7 @@ sudo GATEWAY=10.0.0.1 DEV=eth0 bash scripts/ru-bypass.sh
- `ru-bypass.timer` - ежедневно запускает обновление RIPE-списка и маршрутов
- NM dispatcher `/etc/NetworkManager/dispatcher.d/99-ru-bypass` - перезапускает скрипт при поднятии amn0
- `/etc/ru-bypass.conf` - root-конфиг для запусков из systemd/NM dispatcher
- `/etc/hosts` блок для локальных Eltex-доменов (между `# ru-bypass: local hosts` и `# ru-bypass: end`; ручные записи вне блока скрипт не трогает)
- `/etc/hosts` блок для локальных Eltex-доменов
При каждом запуске:
@@ -160,11 +151,7 @@ bash tests/test_network.sh # полные тесты
- `ai-gpt` - нативный OpenAI Codex CLI
- `ai-deepseek` - Claude Code через DeepSeek API
- `ai-kimi` - Claude Code через официальный Kimi Code API (`api.kimi.com/coding`)
- `ai-glm` - Claude Code через Zhipu AI GLM 5.3 API (`api.z.ai/api/anthropic`)
- `ai-eltex` - Claude Code через корпоративный LLM-прокси Eltex (`http://llm.eltex.loc:4000`, LiteLLM, team-подписка GLM). Ключ выдают админы ([дока](https://intdocs.eltex.loc/pages/viewpage.action?pageId=262329878)), модель `anth-glm-5.3` (GLM 5.3 на Anthropic-эндпоинте Z.ai) во всех слотах `/model`: картинки через серверный `analyze_image`, как в `ai-glm`, effort, кеш, окно 1M; если ключу недоступна `anth-glm-5.3`, лаунчер выходит с понятной ошибкой; переопределение: `ELTEX_MODEL`, `ELTEX_BASE_URL`
- `ai-eltex-qwen` - то же через корпоративный LLM-прокси Eltex, но на моделях Qwen со своим ключом от админов: по умолчанию `Eltex-Coder-Senior` (Qwen3.8-27B), если она есть у ключа, иначе `Eltex-Qwen3_8-Flash-Next`; ключ можно задать в `AI_ELTEX_LLM_API_KEY`, как в онбординге Eltex AI; переопределение: `ELTEX_QWEN_MODEL`, `ELTEX_BASE_URL`
- `ai-mimo` - Claude Code через Xiaomi MiMo V2.6 (Anthropic-совместимый API, Token Plan или pay-as-you-go): всегда самая умная модель `mimo-v2.6-pro` (во всех слотах и сабагентах, `--model opus` на каждом запуске), effort `max`, вывод до 128000 токенов, окно 1M. Эндпоинт региона подписки (`token-plan-sgp`/`-ams`, для `sk-` ключа `api.xiaomimimo.com`) находится перебором при вводе ключа; переопределение: `MIMO_BASE_URL` (нужно для `token-plan-cn`: он в перебор не входит, с наших машин не отвечает)
- `ai-qwen` - Claude Code через Alibaba Cloud Model Studio (Token Plan, Anthropic-совместимый API `token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic`): общий ключ вшит в скрипт, запускается сразу без ввода ключа. `qwen3.8-max` в opus/sonnet и сабагентах (`--model opus` на каждом запуске), `qwen3.8-flash` в haiku, effort `max`, окно 1M, картинки видят обе модели
- `ai-glm` - Claude Code через Zhipu AI GLM 5.2 API (`api.z.ai/api/anthropic`)
- `ai-openrouter` - Claude Code через OpenRouter (GPT-5.5, Grok, Qwen, GLM 5.2 и др.)
- `ai-opencode` - Claude Code через OpenCode Go + routatic-proxy (DeepSeek, Kimi, Qwen, GLM, MiniMax, MiMo)
- `ai-gemini` - нативный Antigravity CLI `agy`
@@ -179,7 +166,7 @@ bash tests/test_network.sh # полные тесты
## Android CLI
[Google Android CLI](https://d.android.com/tools/agents) (preview) - официальный терминальный инструмент для Android-разработки, заточенный под AI-агентов. По умолчанию **не устанавливается**: `scripts/ai-setup.sh` (пункты меню 1 и 7) его не трогает. Ставится только по выбору - `bash setup.sh` → пункт `8) Android CLI` (или напрямую `bash scripts/android-cli-setup.sh`).
`scripts/ai-setup.sh` также устанавливает [Google Android CLI](https://d.android.com/tools/agents) (preview) - официальный терминальный инструмент для Android-разработки, заточенный под AI-агентов.
Что устанавливается:
- `android` - бинарь CLI в `~/.local/bin/` (или `~/bin` через symlink)
@@ -206,7 +193,7 @@ android update # обновить сам Android CLI
Интеграция с Claude Code:
```bash
# После установки android-cli-setup.sh выполняется автоматически:
# После установки ai-setup.sh выполняется автоматически:
android init
```
@@ -222,9 +209,6 @@ android init
- `~/.config/ai-setup/deepseek_key` - DeepSeek API key (права 600)
- `~/.config/ai-setup/kimi_key` - Kimi API key (права 600)
- `~/.config/ai-setup/glm_key` - Zhipu AI (GLM) API key (права 600)
- `~/.config/ai-setup/eltex_key` - ключ корпоративного LLM-прокси Eltex от админов (права 600)
- `~/.config/ai-setup/eltex_qwen_key` - ключ для моделей Qwen на том же прокси (права 600)
- `~/.config/ai-setup/mimo_key` - Xiaomi MiMo API key (права 600), `mimo_base_url` - найденный для него эндпоинт
- `~/.config/ai-setup/openrouter_key` - OpenRouter API key (права 600)
- `~/.config/ai-setup/opencode_key` - OpenCode Go API key (права 600)
- `~/.config/ai-setup/global_rules.md` - глобальные правила агентов
@@ -233,16 +217,16 @@ android init
## Права запуска агентов
- `ai-gpt` использует `--dangerously-bypass-approvals-and-sandbox`
- `ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-eltex`, `ai-eltex-qwen`, `ai-mimo`, `ai-qwen`, `ai-openrouter`, `ai-opencode`, `ai-gemini` используют `--dangerously-skip-permissions`
- `ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-glm`, `ai-openrouter`, `ai-opencode`, `ai-gemini` используют `--dangerously-skip-permissions`
- `ai-cursor` использует `--force` (алиас `--yolo`)
Удобно для локального coding workflow, но это не sandbox для недоверенного кода.
## Правила агентов
В `home-configs/GLOBAL_RULES.md` - компактные правила о границах автономности, минимальных
изменениях, пропорциональной проверке, работе с Git и контекстом проекта, а также пользовательские
настройки: русский язык, доступ к Redmine и запрет на commit/staging без явной команды.
В `home-configs/GLOBAL_RULES.md` - правила Карпати (Think Before Coding, Simplicity First,
Surgical Changes, Goal-Driven Execution) плюс пользовательские правила (отвечать по-русски,
не коммитить без команды, не делать git add без команды и др.).
`scripts/ai-setup.sh` копирует их в native rule-файлы всех CLI. Лаунчеры обновляют их при каждом запуске.

View File

@@ -1,92 +1,100 @@
# Global AI Agent Rules
Use these defaults across projects, then apply any more specific repository instructions. Prefer clear outcomes and proportional safeguards over process for its own sake.
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
## 1. Resolve Intent Without Needless Blocking
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
- Inspect the available context before asking the user for information.
- Ask a focused question only when an ambiguity would materially change the result, create meaningful risk, or cannot be resolved from the repository or task context.
- Otherwise choose the simplest reasonable interpretation and state only assumptions that materially affect the result.
- Surface important tradeoffs and recommend a simpler or safer approach when it better serves the user's goal.
## 1. Think Before Coding
## 2. Keep Changes Minimal and Scoped
**Don't assume. Don't hide confusion. Surface tradeoffs.**
- Implement only the requested behavior and the support required to make it correct.
- Do not add speculative features, single-use abstractions, unrequested configurability, or handling for implausible scenarios.
- Preserve existing public behavior and local conventions unless the task requires changing them.
- Do not refactor, reformat, or clean up unrelated code. Mention unrelated issues only when they are relevant risks or blockers.
- Remove imports, variables, functions, files, or comments made obsolete by your own changes; leave pre-existing dead code alone unless asked.
- Every changed line should trace to the requested outcome.
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 3. Act Autonomously Within the Request
## 2. Simplicity First
- For requests to answer, explain, review, diagnose, or plan, inspect the relevant materials and report the result. Do not implement changes unless the request also asks for them.
- For requests to change, build, or fix, perform safe in-scope local edits and validation without asking for routine confirmation.
- Ask before destructive or difficult-to-recover actions, external writes or publication, purchases, or a material expansion of scope.
- Exception for Eltex merge-request reviews: a bare review request or explicit `eltex-review` invocation authorizes the skill's documented GitLab writes, including inline discussions, approval, and its thank-you note. Explicit chat-only wording keeps the review read-only.
- Preserve unrelated user changes and adapt to a dirty working tree. Never discard or overwrite work you did not create.
**Minimum code that solves the problem. Nothing speculative.**
## 4. Plan and Verify Proportionally
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
- Define observable success criteria before changing code.
- For non-trivial multi-step work, keep a brief plan whose steps include their verification. Skip planning ceremony for simple tasks.
- For a bug fix, reproduce the failure first when feasible. Add a focused regression test when it provides durable value and the repository has an appropriate test layer.
- Run the smallest relevant checks first, then broader checks when the change's risk justifies them.
- Do not claim completion without fresh evidence. Report the commands run, their results, and anything that remains unverified.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 5. Keep Git Changes Reviewable
## 3. Surgical Changes
- Never run `git commit` unless the user explicitly asks for a commit.
- Do not stage changes unless the user explicitly asks for staging, committing, or another operation that requires it. Do not disturb changes already staged by the user.
- Leave tracked edits unstaged so they remain visible in `git diff`.
- Remember that untracked files do not appear in ordinary `git diff`. Show them with `git status --short`, list their paths in the handoff, and use `git diff --no-index -- /dev/null <path>` when a patch view is useful.
- Before handing off, inspect both `git diff` and `git status --short` and distinguish your changes from unrelated existing work.
**Touch only what you must. Clean up only your own mess.**
## 6. Load Relevant Project Context
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
- Before acting, read the applicable agent instruction files and the project documentation directly relevant to the task.
- Follow referenced documents when they are needed to understand requirements, commands, architecture, or validation. Do not scan unrelated Markdown files indiscriminately.
- Use the repository's existing tools, conventions, and test commands unless the task requires otherwise.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
## 7. Communication Style
The test: Every changed line should trace directly to the user's request.
- Reply in Russian, use informal "ты", and communicate like a friendly programmer teammate.
- Lead with the outcome and keep the explanation concise while preserving evidence, material caveats, and next steps.
- Keep identifiers, commands, exact error messages, and source quotations in their original language when accuracy matters.
- Humor, irony, and swearing are welcome when they fit the conversation; clarity and respect come first.
## 4. Goal-Driven Execution
## 8. Reusable Skills
**Define success criteria. Loop until verified.**
- After completing the current task, propose a focused reusable skill only when the user has repeated a workflow and reuse would clearly help.
- Do not interrupt active work to create a skill, and do not create or edit skill files without explicit user approval.
- If approved skill changes live outside the current repository, report their exact paths and how the user can inspect them.
Transform tasks into verifiable goals:
- "Add validation" -> "Write tests for invalid inputs, then make them pass"
- "Fix the bug" -> "Write a test that reproduces it, then make it pass"
- "Refactor X" -> "Ensure tests pass before and after"
## 9. Redmine (red.eltex.loc)
For multi-step tasks, state a brief plan:
```
1. [Step] -> verify: [check]
2. [Step] -> verify: [check]
3. [Step] -> verify: [check]
```
- When the request or an Eltex branch identifies a Redmine issue, read its specification, status, and comments before implementing. Eltex branches commonly start with the issue number (for example, `485448_keyboard_opening_fix` maps to issue `485448`).
- Use the personal API key stored only in `~/.config/ai-setup/redmine_key` (mode 600):
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
# Global Rules for All AI Agents
The rules below are mandatory for every interaction and task. They are intentionally placed after the general coding guidelines above, but they have higher priority when a conflict exists.
1. **Communication style:**
Always reply in Russian, in a friendly peer-to-peer tone, using informal "ты". Appropriate swearing, humor, sarcasm, and irony are allowed and welcome. Communicate like a live programmer teammate, not like a dry robot.
2. **No commits without an explicit request:**
Never run `git commit` unless the user has directly and unambiguously asked for it. The final commit always remains with the user, or is made strictly by the user's command.
3. **Plain git diff visibility:**
All changes must remain visible to the user through the standard `git diff` command. Leave modified files in the working directory unstaged. Do not add files to the Git index with `git add` unless the user explicitly asks for staging, committing, or another action that requires staging, because staging hides changes from plain `git diff`.
4. **Project context:**
At the start of work, pay close attention to all provided project `.md` files, because they are provided automatically and contain the current repository's context and specifics.
5. **Reusable skills:**
When the user repeats the same instruction, output format, correction, or workflow, treat it as a candidate for a reusable skill.
Do not interrupt active work just to create a skill. Finish the current task first, then briefly propose the skill name, trigger description, and what files or tools it should contain.
Do not create or edit skill files silently. Create or update a skill only after explicit user approval.
Keep skills focused: prefer several small skills over one broad skill. A useful skill should have a precise description for when to use it, short instructions for how to act, and reusable tools, templates, references, or examples when they make the result more stable.
If skill files are outside the current git repository, clearly state the exact paths changed and how the user can inspect them, because those changes are not visible in the project `git diff`.
6. **Redmine (red.eltex.loc) access:**
Redmine issues require authentication. To read a task's spec and comments, use the personal API key stored in `~/.config/ai-setup/redmine_key` (chmod 600). Eltex branches usually start with the issue number (e.g. `485448_keyboard_opening_fix` → issue `485448` → https://red.eltex.loc/issues/485448). Fetch the issue with its comments (journals) via:
```bash
curl -fsS -H "X-Redmine-API-Key: $(cat ~/.config/ai-setup/redmine_key)" \
curl -s -H "X-Redmine-API-Key: $(cat ~/.config/ai-setup/redmine_key)" \
"https://red.eltex.loc/issues/<N>.json?include=journals"
```
- Read `issue.description`, `issue.status.name`, and recent non-empty `issue.journals[].notes`. If the key is missing or the request fails, report that instead of guessing the task requirements.
- Never print, log, or write the API key into repository files.
## 10. GitLab (gitlab.eltex.loc)
- GitLab access uses **per-project** tokens stored in `~/.config/ai-setup/` (mode 600). Each one is a project access token and works only for its own project:
| File | Project | id |
| :--- | :--- | :--- |
| `gitlab_token_superapp` | `voip/vp-apps/superapp-launcher` | 4928 |
| `gitlab_token_common_library` | `voip/vp-apps/common-library` | 4825 |
| `gitlab_token_smart_home` | `android_apps/smart_home` | 1409 |
- Pick the file matching the project you are working with. A token from another project returns HTTP 404, not 403, so a wrong token looks exactly like a missing project — check the token before concluding the resource does not exist.
```bash
curl -fsS -H "PRIVATE-TOKEN: $(cat ~/.config/ai-setup/gitlab_token_superapp)" \
"https://gitlab.eltex.loc/api/v4/projects/4928/merge_requests/241"
```
- There is no generic `gitlab_token` fallback any more. Tools that accept an explicit token path (for example the `eltex-review` helper) must be given `--token-file <path>`.
- `local.properties` in superapp-launcher also holds `gitlab.token` and `redmine.apiKey`; it is gitignored and must stay that way.
- Never print, log, or write these tokens into repository files.
Read `issue.description` for the spec, `issue.journals[].notes` for comments (recent non-empty ones matter most), `issue.status.name` for status. Keep the key only in that file — never write it into files tracked by git.

View File

@@ -6,7 +6,7 @@ The custom statusline script renders **only ASCII-safe characters and ANSI color
- ASCII brackets `[ ]` for git branch and effort level
- ANSI 256-color escape sequences for model names, effort levels, and rate-limit percentages
- The `$` symbol for the DeepSeek balance (USD only)
- The `¥` symbol (U+00A5) for CNY balance in DeepSeek output
It does **not** use Nerd Font glyphs, powerline separators, or emoji.

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash
# Copyright (c) 2026 Виталий Никитенко. MIT License.
# UserPromptSubmit hook: перехватывает /logout, /logout-glm, /logout-eltex, /logout-eltex-qwen, /logout-mimo, /logout-deepseek, /logout-kimi.
# UserPromptSubmit hook: перехватывает /logout, /logout-glm, /logout-deepseek, /logout-kimi.
# Удаляет сохранённый API-ключ провайдера, чтобы при следующем запуске лаунчер
# запросил новый.
# /logout-glm | /logout-eltex | /logout-eltex-qwen | /logout-mimo | /logout-deepseek | /logout-kimi — явно указанный провайдер.
# /logout (без суффикса) — провайдер по AI_LAUNCHER (glm/eltex/eltex-qwen/mimo/deepseek/kimi).
# /logout-glm | /logout-deepseek | /logout-kimi — явно указанный провайдер.
# /logout (без суффикса) — провайдер по AI_LAUNCHER (glm/deepseek/kimi).
# Для ai-claude и прочих лаунчеров /logout отступает (exit 0) — пусть работает
# нативный oauth-logout. exit 2 после удаления: LLM не нужна, и нативная
# /logout не пытается сделать бессмысленный oauth-logout для api-key провайдеров.
@@ -17,14 +17,11 @@ cmd=$(echo "$normalized" | awk '{print $1}')
target=""
case "$cmd" in
logout-glm) target=glm ;;
logout-eltex) target=eltex ;;
logout-eltex-qwen) target=eltex-qwen ;;
logout-mimo) target=mimo ;;
logout-deepseek) target=deepseek ;;
logout-kimi) target=kimi ;;
logout)
case "${AI_LAUNCHER:-claude}" in
glm|eltex|eltex-qwen|mimo|deepseek|kimi) target="$AI_LAUNCHER" ;;
glm|deepseek|kimi) target="$AI_LAUNCHER" ;;
*) exit 0 ;;
esac
;;
@@ -38,18 +35,6 @@ case "$target" in
rm -f "$CFG/glm_key"
echo "✓ GLM ключ удалён. Перезапусти ai-glm, чтобы ввести новый." >&2
;;
eltex)
rm -f "$CFG/eltex_key"
echo "✓ Eltex ключ удалён. Перезапусти ai-eltex, чтобы ввести новый." >&2
;;
eltex-qwen)
rm -f "$CFG/eltex_qwen_key"
echo "✓ Eltex Qwen ключ удалён. Перезапусти ai-eltex-qwen, чтобы ввести новый." >&2
;;
mimo)
rm -f "$CFG/mimo_key" "$CFG/mimo_base_url"
echo "✓ MiMo ключ удалён. Перезапусти ai-mimo, чтобы ввести новый." >&2
;;
deepseek)
rm -f "$CFG/deepseek_key"
echo "✓ DeepSeek ключ удалён. Перезапусти ai-deepseek, чтобы ввести новый." >&2

View File

@@ -4,12 +4,6 @@ input=$(cat)
cwd=$(echo "$input" | jq -r '.cwd')
model=$(echo "$input" | jq -r '.model.display_name // empty')
model_id=$(echo "$input" | jq -r '.model.id // empty')
# Суффикс [1m] в отображении не нужен: окно контекста задаётся
# CLAUDE_CODE_MAX_CONTEXT_TOKENS, а не суффиксом модели
model="${model/\[1m\]/}"
model="${model% \[1M\]}"
model="${model% (1M context)}"
model_id="${model_id/\[1m\]/}"
five_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
five_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
week_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
@@ -17,18 +11,15 @@ week_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
# Разделитель сегментов: жирная серая средняя точка ·
_sep() { printf " \033[1m\033[38;5;240m·\033[00m "; }
# Цвет effort: насыщенные, глубже чем пастельные (jewel tones)
_effort_color() {
case "$1" in
low) printf '\033[38;5;220mlow\033[00m' ;; # золотой
medium) printf '\033[38;5;50mmedium\033[00m' ;; # насыщенный teal
high) printf '\033[38;5;38mhigh\033[00m' ;; # глубокий голубой
xhigh) printf '\033[38;5;206mxhigh\033[00m' ;; # насыщенный фиолетовый
max) printf '\033[38;5;220mm\033[38;5;114ma\033[38;5;50mx\033[38;5;206m\033[00m' ;; # радуга
*) printf '\033[38;5;252m%s\033[00m' "$1" ;;
low) printf '\033[38;5;220m[low]\033[00m' ;; # золотой
medium) printf '\033[38;5;50m[medium]\033[00m' ;; # насыщенный teal
high) printf '\033[38;5;38m[high]\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;50mx\033[38;5;206m]\033[00m' ;; # радуга
*) printf '\033[38;5;252m[%s]\033[00m' "$1" ;;
esac
}
@@ -38,9 +29,6 @@ _brand_color() {
deepseek) printf '\033[38;5;69m' ;; # DeepSeek фирменный синий
claude) printf '\033[38;5;173m' ;; # Anthropic оранжевый
kimi) printf '\033[38;5;81m' ;; # Kimi голубой
glm) printf '\033[38;5;223m' ;; # Z.ai кремовый (бренд-фон z.ai, #FFE4B5)
mimo) printf '\033[38;5;202m' ;; # Xiaomi оранжевый (#FF6900)
qwen) printf '\033[38;5;99m' ;; # Qwen фиолетовый (#615CED)
openrouter) printf '\033[38;5;135m' ;; # OpenRouter фиолетовый
*) printf '\033[38;5;223m' ;; # кремовый (fallback)
esac
@@ -51,7 +39,7 @@ branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null
short_cwd="${cwd/#$HOME/\~}"
printf "\033[38;5;252m%s\033[00m" "$short_cwd"
[ -n "$branch" ] && { _sep; printf "\033[38;5;111m%s\033[00m" "$branch"; }
[ -n "$branch" ] && printf " \033[38;5;252m[%s]\033[00m" "$branch"
if [ -n "$model" ]; then
brand_color=$(_brand_color "${AI_LAUNCHER:-}")
effort=$(echo "$input" | jq -r ".effort.level // empty")
@@ -96,20 +84,18 @@ if [ -n "$model" ]; then
fi
fi
fi
[ -n "$account" ] && { _sep; printf "%s%s\033[00m" "$brand_color" "$account"; }
[ -n "$account" ] && printf " %s[%s]\033[00m" "$brand_color" "$account"
fi
# Аккаунт/ключ Kimi — показываем alias (account1/account2/...).
if [ "${AI_LAUNCHER:-}" = "kimi" ]; then
kimi_account=$(cat "$HOME/.config/ai-setup/kimi_keys/current" 2>/dev/null)
[ -n "$kimi_account" ] && { _sep; printf "%s%s\033[00m" "$brand_color" "$kimi_account"; }
[ -n "$kimi_account" ] && printf " %s[%s]\033[00m" "$brand_color" "$kimi_account"
fi
if [ -n "$effort" ]; then
_sep
printf " %s%s " "$brand_color" "$model"
_effort_color "$effort"
else
_sep
printf " %s%s\033[00m" "$brand_color" "$model"
fi
fi
@@ -152,7 +138,7 @@ if [[ "$model_id" == *deepseek* ]]; then
cache_file="$HOME/.cache/ai-setup/deepseek_balance"
if [ -f "$cache_file" ]; then
balance=$(head -1 "$cache_file")
[ -n "$balance" ] && { _sep; printf "\033[38;5;78m%s\033[00m" "$balance"; }
[ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
fi
# Фоновое обновление баланса (не чаще раза в 30 секунд)
@@ -173,12 +159,13 @@ if [[ "$model_id" == *deepseek* ]]; then
import sys, json
d = json.load(sys.stdin)
infos = d.get('balance_infos', [])
symbols = {'USD': '\$', 'CNY': '\u00a5'}
parts = []
for info in infos:
if info.get('currency', '').upper() != 'USD':
continue
curr = info.get('currency', '')
total = info.get('total_balance', '0')
parts.append(f'\${total}')
sym = symbols.get(curr, curr)
parts.append(f'{sym}{total}')
if parts:
print(' '.join(parts))
" 2>/dev/null)
@@ -195,7 +182,7 @@ elif [ "${AI_LAUNCHER:-}" = "openrouter" ]; then
cache_file="$HOME/.cache/ai-setup/openrouter_balance"
if [ -f "$cache_file" ]; then
balance=$(head -1 "$cache_file")
[ -n "$balance" ] && { _sep; printf "\033[38;5;78m%s\033[00m" "$balance"; }
[ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
fi
# Фоновое обновление баланса (не чаще раза в 30 секунд)
@@ -260,31 +247,13 @@ else
five_int=$(printf '%.0f' "$five_pct")
remaining=$(fmt_remaining "$five_reset")
color=$(pct_color "$five_int")
_sep; printf "%s%s:%s%%\033[00m" "$color" "$remaining" "$five_int"
printf " %s%s:%s%%\033[00m" "$color" "$remaining" "$five_int"
fi
if [ -n "$week_pct" ] && [ -n "$week_reset" ]; then
week_int=$(printf '%.0f' "$week_pct")
remaining=$(fmt_remaining "$week_reset")
color=$(pct_color "$week_int")
_sep; printf "%s%s:%s%%\033[00m" "$color" "$remaining" "$week_int"
fi
fi
# Z.ai (ai-glm, ai-eltex) отдаёт usage только в конце ответа, в начале потока
# input_tokens=0, поэтому пока идёт запрос, Claude Code присылает used_percentage=0.
# Держим последнее ненулевое значение этой сессии.
session_id=$(echo "$input" | jq -r '.session_id // empty')
if [ -n "$session_id" ]; then
ctx_cache="$HOME/.cache/ai-setup/ctx_last_${session_id}"
if [ -n "$ctx_pct" ] && [ "$(printf '%.0f' "$ctx_pct")" -gt 0 ]; then
if [ ! -f "$ctx_cache" ]; then
mkdir -p "$HOME/.cache/ai-setup"
# Раз за сессию чистим кэш старых сессий
find "$HOME/.cache/ai-setup" -name 'ctx_last_*' -mtime +7 -delete 2>/dev/null
fi
echo "$ctx_pct" > "$ctx_cache"
elif [ -f "$ctx_cache" ]; then
ctx_pct=$(cat "$ctx_cache")
printf " %s%s:%s%%\033[00m" "$color" "$remaining" "$week_int"
fi
fi
@@ -294,7 +263,7 @@ fi
if [ -n "$ctx_pct" ]; then
ctx_int=$(printf '%.0f' "$ctx_pct")
color=$(pct_color "$ctx_int")
_sep; printf "%sctx:%s%%\033[00m" "$color" "$ctx_int"
printf " %sctx:%s%%\033[00m" "$color" "$ctx_int"
# Звуковой сигнал при первом достижении 60%
alert_file="$HOME/.cache/ai-setup/ctx_alert_state"

View File

@@ -56,7 +56,7 @@ before.rules: 10/8, 172.16/12,
5. Добавляет маршруты для RFC1918 диапазонов (`10/8`, `172.16/12`, `192.168/16`) - нужно для `*.loc`
6. Добавляет встроенные исключения kill switch (`mattermost.eltex-co.ru`, `elph.eltex-co.ru`, `10.80.0.15`)
7. Добавляет пользовательские исключения из `AMNEZIA_SERVER` и `KILL_SWITCH_EXCEPTIONS`
8. Обновляет блок локальных Eltex-хостов в `/etc/hosts` (только между маркерами `# ru-bypass: local hosts` / `# ru-bypass: end`, ручные записи сохраняются)
8. Обновляет блок локальных Eltex-хостов в `/etc/hosts`
9. Если задан `LOCAL_DNS`, настраивает `resolvectl` для `~loc` на интерфейсе `DEV`
10. Обновляет `/etc/ufw/before.rules`, прямые iptables-правила и отключает IPv6 при активном kill switch
11. При первом запуске: устанавливает systemd service/timer и NetworkManager dispatcher

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Copyright (c) 2026 Виталий Никитенко. MIT License.
# ============================================================
# AI Setup - Claude / GPT-5.5 / Grok / Qwen / DeepSeek / Kimi / GLM / Eltex / Eltex Qwen / Gemini
# AI Setup - Claude / GPT-5.5 / Grok / Qwen / DeepSeek / Kimi / GLM / Gemini
# Запуск: bash ai-setup.sh
# ============================================================
@@ -267,6 +267,56 @@ else
success "agy уже установлен: $(agy --version 2>/dev/null | head -1)"
fi
# ── 4d. Google Android CLI ───────────────────────────────────
info "Проверяю Android CLI..."
ANDROID_BIN="$HOME/.local/bin/android"
[ ! -f "$ANDROID_BIN" ] && ANDROID_BIN="$(command -v android 2>/dev/null || true)"
if [ -z "$ANDROID_BIN" ] || [ ! -f "$ANDROID_BIN" ]; then
info "Устанавливаю Android CLI..."
_uname_s=$(uname -s)
_uname_m=$(uname -m)
ANDROID_PLATFORM=""
case "${_uname_s}-${_uname_m}" in
Linux-x86_64) ANDROID_PLATFORM="linux_x86_64" ;;
Linux-aarch64) ANDROID_PLATFORM="linux_arm64" ;;
Linux-arm64) ANDROID_PLATFORM="linux_arm64" ;;
Darwin-x86_64) ANDROID_PLATFORM="darwin_x86_64" ;;
Darwin-arm64) ANDROID_PLATFORM="darwin_arm64" ;;
Darwin-aarch64) ANDROID_PLATFORM="darwin_arm64" ;;
MINGW*|MSYS*|CYGWIN*-x86_64) ANDROID_PLATFORM="windows_x86_64" ;;
esac
if [ -n "$ANDROID_PLATFORM" ]; then
ANDROID_INSTALL_URL="https://dl.google.com/android/cli/latest/${ANDROID_PLATFORM}/install.sh"
if curl -fsSL "$ANDROID_INSTALL_URL" | bash; then
success "Android CLI установлен"
else
warn "Не удалось установить Android CLI через официальный скрипт"
fi
else
warn "Неизвестная платформа ${_uname_s} ${_uname_m}. Пропускаю Android CLI."
fi
else
success "Android CLI уже установлен: $($ANDROID_BIN --version 2>/dev/null | head -1)"
fi
# Официальный install.sh ставит бинарь в ~/.local/bin/android.
# Если пользователь использует ~/bin вместо ~/.local/bin - symlink.
if [ "$BIN_DIR" != "$HOME/.local/bin" ] && [ -f "$HOME/.local/bin/android" ] && [ ! -f "$BIN_DIR/android" ]; then
ln -sf "$HOME/.local/bin/android" "$BIN_DIR/android"
success "Android CLI symlink -> $BIN_DIR/android"
fi
# Разворачиваем Android CLI skills для агентов (Claude Code и др.)
if command -v android &>/dev/null; then
info "Запускаю android init для деплоя skills..."
mkdir -p "$HOME/.claude/skills"
android init || warn "android init завершился с ошибкой"
else
info "Android CLI не найден в PATH, пропускаю android init"
fi
# ── 5. routatic-proxy (OpenCode Go → Claude Code) ──────────
info "Проверяю routatic-proxy..."
ROUTATIC_BIN="$HOME/.local/bin/routatic-proxy"
@@ -656,24 +706,25 @@ try:
d = json.load(sys.stdin)
available = d.get('is_available', False)
infos = d.get('balance_infos', [])
# Показываем только USD (CNY не выводим)
infos = [i for i in infos if i.get('currency', '').upper() == 'USD']
if not infos:
print(' \033[0;33m[БАЛАНС]\033[0m Нет данных о балансе')
sys.exit(0)
symbols = {'USD': '\$', 'CNY': '¥'}
cache_parts = []
for info in infos:
curr = info.get('currency', '???')
total = info.get('total_balance', '0')
granted = info.get('granted_balance', '0')
topped_up = info.get('topped_up_balance', '0')
sym = symbols.get(curr, curr)
status = '✅ доступен' if available else '❌ не активен'
print(f' \033[1;36m💰 Баланс DeepSeek:\033[0m \${total} {status}')
print(f' \033[1;36m💰 Баланс DeepSeek:\033[0m {total} {curr} {status}')
if float(granted) > 0:
print(f' └─ Начислено: \${granted}')
print(f' └─ Начислено: {granted} {curr}')
if float(topped_up) > 0:
print(f' └─ Пополнено: \${topped_up}')
cache_parts.append(f'\${total}')
# Cache USD balance for statusline
print(f' └─ Пополнено: {topped_up} {curr}')
cache_parts.append(f'{sym}{total}')
# Cache all currencies with symbols for statusline
if cache_parts:
cache_dir = os.path.expanduser('~/.cache/ai-setup')
os.makedirs(cache_dir, exist_ok=True)
@@ -1192,9 +1243,7 @@ fi
_build_ai_sys_prompt > /dev/null # сохраняет в ~/.codex/AGENTS.md (codex читает авто)
_setup_codex_sound_hooks
exec "$codex_bin" --dangerously-bypass-approvals-and-sandbox \
-c 'tui.status_line=["model-with-reasoning","git-branch","context-used","five-hour-limit","current-dir"]' \
"$@"
exec "$codex_bin" --dangerously-bypass-approvals-and-sandbox "$@"
GPTEOF
chmod +x "$BIN_DIR/ai-gpt"
@@ -1511,7 +1560,7 @@ chmod +x "$BIN_DIR/ai-kimi"
# === ai-glm ===
cat > "$BIN_DIR/ai-glm" << 'GLMEOF'
#!/usr/bin/env bash
# ai-glm - запуск Claude Code через Zhipu AI GLM 5.3 (Anthropic-compatible API)
# ai-glm - запуск Claude Code через Zhipu AI GLM 5.2 (Anthropic-compatible API)
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
key_file="$HOME/.config/ai-setup/glm_key"
@@ -1531,7 +1580,7 @@ glm_base_url="${GLM_BASE_URL:-https://api.z.ai/api/anthropic}"
if [ -n "$api_key" ]; then
echo -n "Проверка сохранённого GLM ключа... "
_claude_test_api "$glm_base_url/v1/messages" "x-api-key: $api_key" "glm-5.3"
_claude_test_api "$glm_base_url/v1/messages" "x-api-key: $api_key" "glm-5.2"
_handle_api_response "GLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://z.ai/manage-apikey/apikey-list"
ret=$_API_RET
if [ $ret -eq 401 ]; then
@@ -1572,7 +1621,7 @@ if [ -z "$api_key" ]; then
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
echo -n "Проверяю ключ... "
_claude_test_api "$glm_base_url/v1/messages" "x-api-key: $api_key" "glm-5.3"
_claude_test_api "$glm_base_url/v1/messages" "x-api-key: $api_key" "glm-5.2"
_handle_api_response "GLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://z.ai/manage-apikey/apikey-list"
ret=$_API_RET
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
@@ -1595,621 +1644,37 @@ trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=glm
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/glm"
# Пикер: все три слота = glm-5.3. availableModels НЕ задаём (он схлопывает пикер).
# Claude Code навязывает 3 слота; незаданный показал бы чужой Claude, поэтому
# opus+sonnet+haiku = glm-5.3. Окно контекста у всех слотов задаёт env
# CLAUDE_CODE_MAX_CONTEXT_TOKENS ниже — суффикс [1m] больше не нужен.
_setup_isolated_config glm opus xhigh ''
# ai-glm стартует на /effort xhigh через CLI --effort (перебивает effortLevel из
# settings.json при каждом запуске). Z.ai сворачивает 5 уровней Claude Code в 3
# нативных GLM 5.3: low,medium→low; high,xhigh→high; max→max (см. EFFORT_MAPPING.md),
# то есть xhigh = нативный high.
# Пикер: Opus/Sonnet = GLM 5.2, Haiku = GLM 5.2[1m]. availableModels НЕ задаём
# (он схлопывает пикер). Claude Code навязывает 3 слота; незаданный показал бы
# чужой Claude, поэтому opus+sonnet = glm-5.2, haiku = glm-5.2[1m].
_setup_isolated_config glm opus high ''
# ai-glm стартует на /effort max. max невалиден в settings.json (там только
# low/medium/high/xhigh), поэтому задаём через CLI --effort, а не _apply_effort.
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT заставляет Claude Code отправлять effort в API
# для GLM (иначе glm-5.3 не распознан как effort-capable и параметр не уходит,
# а Z.ai по умолчанию берёт max).
# /effort внутри сессии работает; ручной override запуска: AI_EFFORT=max ai-glm.
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000: по умолчанию Claude Code просит у glm-5.3
# max_tokens=32000 (размышления + ответ), на трудных задачах GLM в них не укладывается
# и ответ обрывается. Z.ai принимает и 128000.
# CLAUDE_CODE_DISABLE_1M_CONTEXT НЕ выставляем: GLM 5.3 реально держит 1M контекста,
# для GLM (иначе glm-5.2 не распознан как effort-capable и параметр не уходит).
# /effort внутри сессии работает; ручной override запуска: AI_EFFORT=high ai-glm.
# CLAUDE_CODE_DISABLE_1M_CONTEXT НЕ выставляем: GLM 5.2 реально держит 1M контекста,
# а флаг заставлял бы счётчик ctx: считать окно = 200K (врал в ~5 раз). Прочие
# лаунчеры (deepseek/kimi/openrouter/opencode) флаг оставляют — у них другой контекст
# и флаг убирает [1M]-дубли из пикера; ai-claude его и так не использует.
# CLAUDE_CODE_MAX_CONTEXT_TOKENS: glm-* нет в реестре моделей Claude Code, поэтому
# окно по умолчанию считается = 200K — ctx: врал бы в ~5 раз, а автокомпакт жал бы
# сессию в 200K. Задаём реальное окно 1M явно; env применяется к не-claude моделям
# независимо от суффикса, поэтому [1m] у Haiku-слота не нужен и убран.
ANTHROPIC_BASE_URL="$glm_base_url" \
ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.3 \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="GLM 5.3" \
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="GLM 5.3 — флагман Zhipu AI" \
ANTHROPIC_DEFAULT_SONNET_MODEL=glm-5.3 \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="GLM 5.3" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="GLM 5.3 — флагман Zhipu AI" \
ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-5.3 \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="GLM 5.3" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.3 — флагман Zhipu AI" \
CLAUDE_CODE_SUBAGENT_MODEL=glm-5.3 \
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.2 \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="GLM 5.2" \
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="GLM 5.2 — флагман Zhipu AI" \
ANTHROPIC_DEFAULT_SONNET_MODEL=glm-5.2 \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="GLM 5.2" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="GLM 5.2 — флагман Zhipu AI" \
ANTHROPIC_DEFAULT_HAIKU_MODEL='glm-5.2[1m]' \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="GLM 5.2 [1M]" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.2 [1M] — расширенный контекст" \
CLAUDE_CODE_SUBAGENT_MODEL=glm-5.2 \
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-xhigh}" --system-prompt-file "$_PROMPT_FILE" "$@"
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
GLMEOF
chmod +x "$BIN_DIR/ai-glm"
# === ai-eltex ===
cat > "$BIN_DIR/ai-eltex" << 'ELTEXEOF'
#!/usr/bin/env bash
# ai-eltex - запуск Claude Code через корпоративный LLM-прокси Eltex (LiteLLM, team-подписка GLM).
# Персональный ключ выдают админы. Дока:
# https://intdocs.eltex.loc/pages/viewpage.action?pageId=262329878
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
key_file="$HOME/.config/ai-setup/eltex_key"
api_key=""
reauth=0
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
if [ "${AI_RESET_KEY:-0}" == "1" ] && [ -f "$key_file" ]; then
rm -f "$key_file"
api_key=""
fi
[ -f "$key_file" ] && api_key=$(cat "$key_file")
eltex_base_url="${ELTEX_BASE_URL:-http://llm.eltex.loc:4000}"
# _eltex_check_key: проверка ключа через GET /v1/models. Виртуальному ключу LiteLLM
# доступны только LLM-роуты, а /v1/models заодно отдаёт модели, разрешённые ключу.
_eltex_check_key() {
local response
# Без "|| echo 000": при сетевой ошибке -w сам печатает 000.
response=$(curl -s -w "\n%{http_code}" --max-time 15 "$eltex_base_url/v1/models" \
-H "Authorization: Bearer $1" \
2>/dev/null)
_CLAUDE_TEST_CODE=$(echo "$response" | tail -1)
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
if [ "$_CLAUDE_TEST_CODE" = "000" ]; then
echo ""
echo -e "\033[0;31m[СЕТЬ]\033[0m Прокси $eltex_base_url недоступен."
echo " Нужна сеть Eltex. Если не резолвится llm.eltex.loc - перезапусти ru-bypass (добавит его в /etc/hosts)"
echo " или задай адрес явно: ELTEX_BASE_URL=http://172.16.4.68:4000 ai-eltex"
exit 1
fi
_handle_openai_api_response "Eltex LLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит team-подписки исчерпан - обратись к админам"
}
# _eltex_pick_models: модель anth-glm-5.3 из _CLAUDE_TEST_BODY (последний ответ /v1/models).
# Если имя с суффиксом (anth-glm-5.3-2), берём точное имя, иначе первое с тем же префиксом;
# имена групп доступа (ZAI-Test-Group) так не попадут. Список не распарсился
# (429/сетевая ошибка/5xx) - стандартное имя наугад. Распарсился, но anth-glm-5.3 нет -
# ошибка в stderr, model пустой, возврат 1: дефолтный запуск невозможен.
_eltex_pick_models() {
local picked avail
if ! picked=$(echo "$_CLAUDE_TEST_BODY" | python3 -c '
import sys, json
ids = [m["id"] for m in json.load(sys.stdin)["data"]]
def pick(exact, prefix, skip=None):
if exact in ids:
return exact
return next((i for i in ids if i.startswith(prefix) and not (skip and skip in i)), "-")
print(pick("anth-glm-5.3", "anth-glm-5.3-"), " ".join(ids) or "(пусто)")' 2>/dev/null); then
model="anth-glm-5.3"
return 0
fi
read -r model avail <<< "$picked"
if [ "$model" = "-" ]; then
model=""
echo -e "\033[0;31m[МОДЕЛЬ]\033[0m Ключу недоступна модель anth-glm-5.3. Прокси отдаёт: $avail" >&2
echo " Напиши админам про доступ к anth-glm-5.3 или задай модель явно: ELTEX_MODEL=<имя> ai-eltex" >&2
return 1
fi
return 0
}
if [ -n "$api_key" ]; then
echo -n "Проверка сохранённого Eltex ключа... "
_eltex_check_key "$api_key"
ret=$_API_RET
if [ $ret -eq 401 ]; then
rm -f "$key_file"
api_key=""
reauth=1
elif [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
elif [ $ret -ne 0 ]; then
exit 1
elif [ -z "${ELTEX_MODEL:-}" ] && ! _eltex_pick_models; then
# Ключ валиден, но anth-glm-5.3 недоступна (не та группа доступа) - авто-логаут,
# как при 401: такой ключ не годится для дефолтного запуска, сразу просим новый.
echo " Сохранённый ключ удалён (авто-логаут)."
rm -f "$key_file"
api_key=""
reauth=1
fi
fi
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
echo -n "Хотите ввести новый Eltex ключ? [Y/n] "
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
fi
if [ -z "$api_key" ]; then
echo "Ключ выдают админы: https://intdocs.eltex.loc/pages/viewpage.action?pageId=262329878"
read -r -p "Введите ваш Eltex LLM ключ (sk-...): " api_key
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
echo -n "Проверяю ключ... "
_eltex_check_key "$api_key"
ret=$_API_RET
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
if [ -z "${ELTEX_MODEL:-}" ] && ! _eltex_pick_models; then
echo "Ключ НЕ сохранён."
exit 1
fi
mkdir -p "$(dirname "$key_file")"
echo "$api_key" > "$key_file"
chmod 600 "$key_file"
echo "Ключ сохранён."
if [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
fi
else
echo "Ключ НЕ сохранён."
exit 1
fi
fi
# Модель: ELTEX_MODEL, иначе anth-glm-5.3 - glm-5.3 на Anthropic-эндпоинте Z.ai, Claude Code
# ходит в обычный /v1/messages прокси. Проверено 21.10 на всех 6 ключах пилота: картинки
# через серверный analyze_image (Read png -> server_tool_use analyze_image -> верный ответ,
# как в ai-glm), effort, кеш (~95% входа со второго вызова), окно >200K. zai-glm-5.3
# (OpenAI-эндпоинт, без картинок) и flash не используем. Сырой путь /anthropic на этом
# прокси не настроен (21.10: 401 "No Anthropic credential"). Первую модель из
# /v1/models НЕ берём: там бывают не те модели и имена групп доступа (ZAI-Test-Group),
# которые не запустить. Если у ключа нет anth-glm-5.3 - авто-логаут выше (свежевведённый
# такой ключ не сохраняется), вместо 400/403 внутри Claude Code. Модель не выбиралась
# (429/сетевая ошибка или задан ELTEX_MODEL) - выбираем здесь.
[ -n "${model:-}" ] || _eltex_pick_models 2>/dev/null || true
model="${ELTEX_MODEL:-${model:-anth-glm-5.3}}"
# Окно 1M через суффикс [1m], а не глобальным CLAUDE_CODE_MAX_CONTEXT_TOKENS (через прокси
# прошли запросы на 598K 07.10 и 309K 21.10); суффикс Claude Code в API не шлёт.
opus_model="$model"
case "$model" in anth-glm-5.3*) opus_model="${model}[1m]" ;; esac
echo "Модель: $model"
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=eltex
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex"
# Пикер: все слоты (opus, sonnet, haiku) = anth-glm-5.3, других моделей ai-eltex не использует.
# Незаданный слот показал бы чужой Claude, а haiku Claude Code берёт и для фоновых задач
# вроде заголовков. availableModels НЕ задаём (он схлопывает пикер).
# Effort как у ai-glm: старт на max через CLI --effort (перебивает effortLevel из
# settings.json; AI_EFFORT=max в settings Claude Code не принимает и шлёт high), /effort
# в сессии работает. Z.ai учитывает output_config.effort (21.10): low - без размышлений,
# medium/high/max - с размышлениями.
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT - страховка, как в ai-glm.
# Имена-алиасы claude-* на прокси НЕ используем: claude-3-5-sonnet Claude Code считает
# старым Claude 3.5 Sonnet (без effort и thinking, max_tokens=8192); у claude-5-5-sonnet
# один деплоймент на всех, модели говорят, что она Claude Sonnet, а statusline показывает
# аккаунт Claude.ai. На качество имя не влияет (09.10, одинаковый запрос с разной строкой
# "You are powered by": разброс внутри одного имени больше разницы между именами).
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 вместо дефолтных 32000, как у ai-glm.
# API_TIMEOUT_MS из доки админов: прокси бывает медленным.
# Окно anth-glm-5.3 - 1M через [1m] (см. выше). Автокомпакт на 200K из доки админов и
# CLAUDE_CODE_DISABLE_1M_CONTEXT не ставим: ctx: показывал бы занятость в ~5 раз больше
# реальной.
# ENABLE_TOOL_SEARCH НЕ включаем: на Anthropic-эндпоинте Z.ai
# GLM с ним путается - ищет Read/Bash через ToolSearch и решает, что их нет. Цена -
# схемы встроенных тулов в каждом запросе (~+11K входных токенов).
_setup_isolated_config eltex opus xhigh ''
# /logout-eltex: без зарегистрированной команды Claude Code отвечает "Unknown command" и
# не зовёт UserPromptSubmit-хук (logout-account-hook.sh), а встроенный /logout хук обходит.
# Заглушка-команда в конфиге лаунчера пропускает /logout-eltex до хука, он удаляет ключ.
mkdir -p "$CLAUDE_CONFIG_DIR/commands"
cat > "$CLAUDE_CONFIG_DIR/commands/logout-eltex.md" <<'CMDEOF'
---
description: Удалить сохранённый ключ ai-eltex (обрабатывает хук, без LLM)
---
Хук не сработал: удали ~/.config/ai-setup/eltex_key вручную и перезапусти ai-eltex (или запусти AI_RESET_KEY=1 ai-eltex).
CMDEOF
ANTHROPIC_BASE_URL="$eltex_base_url" \
ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_DEFAULT_OPUS_MODEL="$opus_model" \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="GLM 5.3: картинки, effort, окно 1M" \
ANTHROPIC_DEFAULT_SONNET_MODEL="$opus_model" \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="GLM 5.3: картинки, effort, окно 1M" \
ANTHROPIC_DEFAULT_HAIKU_MODEL="$opus_model" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="GLM 5.3: картинки, effort, окно 1M" \
CLAUDE_CODE_SUBAGENT_MODEL="$opus_model" \
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
API_TIMEOUT_MS=600000 \
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
ELTEXEOF
chmod +x "$BIN_DIR/ai-eltex"
# === ai-eltex-qwen ===
cat > "$BIN_DIR/ai-eltex-qwen" << 'ELTEXQWENEOF'
#!/usr/bin/env bash
# ai-eltex-qwen - запуск Claude Code через корпоративный LLM-прокси Eltex на моделях Qwen
# (Eltex-Coder-Senior = Qwen3.8-27B, Eltex-Qwen3_8-Flash-Next = Qwen3.8-Flash-Next-FP8).
# Ключ отдельный от ai-eltex, его выдают админы.
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
key_file="$HOME/.config/ai-setup/eltex_qwen_key"
api_key=""
reauth=0
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
if [ "${AI_RESET_KEY:-0}" == "1" ] && [ -f "$key_file" ]; then
rm -f "$key_file"
api_key=""
fi
[ -f "$key_file" ] && api_key=$(cat "$key_file")
# Ключ из онбординга Eltex AI (переменная для opencode), если своего файла ещё нет
if [ -z "$api_key" ] && [ "${AI_RESET_KEY:-0}" != "1" ]; then
api_key="${AI_ELTEX_LLM_API_KEY:-}"
fi
eltex_base_url="${ELTEX_BASE_URL:-http://llm.eltex.loc:4000}"
# _eltex_check_key: проверка ключа через GET /v1/models (как в ai-eltex), заодно отдаёт
# модели, разрешённые ключу.
_eltex_check_key() {
local response
# Без "|| echo 000": при сетевой ошибке -w сам печатает 000.
response=$(curl -s -w "\n%{http_code}" --max-time 15 "$eltex_base_url/v1/models" \
-H "Authorization: Bearer $1" \
2>/dev/null)
_CLAUDE_TEST_CODE=$(echo "$response" | tail -1)
_CLAUDE_TEST_BODY=$(echo "$response" | sed '$d')
if [ "$_CLAUDE_TEST_CODE" = "000" ]; then
echo ""
echo -e "\033[0;31m[СЕТЬ]\033[0m Прокси $eltex_base_url недоступен."
echo " Нужна сеть Eltex. Если не резолвится llm.eltex.loc - перезапусти ru-bypass (добавит его в /etc/hosts)"
echo " или задай адрес явно: ELTEX_BASE_URL=http://172.16.4.68:4000 ai-eltex-qwen"
exit 1
fi
_handle_openai_api_response "Eltex LLM" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит исчерпан - обратись к админам"
}
if [ -n "$api_key" ]; then
echo -n "Проверка сохранённого Eltex Qwen ключа... "
_eltex_check_key "$api_key"
ret=$_API_RET
if [ $ret -eq 401 ]; then
rm -f "$key_file"
api_key=""
reauth=1
elif [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
elif [ $ret -ne 0 ]; then
exit 1
fi
fi
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
echo -n "Хотите ввести новый Eltex Qwen ключ? [Y/n] "
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
fi
if [ -z "$api_key" ]; then
echo "Ключ выдают админы (ключ для Eltex-Coder-Senior, не GLM-ключ от ai-eltex)."
echo "Можно задать его в AI_ELTEX_LLM_API_KEY, как в онбординге Eltex AI."
read -r -p "Введите ваш Eltex Qwen ключ (sk-...): " api_key
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
echo -n "Проверяю ключ... "
_eltex_check_key "$api_key"
ret=$_API_RET
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
mkdir -p "$(dirname "$key_file")"
echo "$api_key" > "$key_file"
chmod 600 "$key_file"
echo "Ключ сохранён."
if [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
fi
else
echo "Ключ НЕ сохранён."
exit 1
fi
fi
# Модель: ELTEX_QWEN_MODEL, иначе Eltex-Coder-Senior, если она есть у ключа, иначе
# Eltex-Qwen3_8-Flash-Next. Эмбеддер/реранкер из /v1/models для чата не годятся.
model="${ELTEX_QWEN_MODEL:-}"
if [ -z "$model" ]; then
model=$(echo "$_CLAUDE_TEST_BODY" | python3 -c '
import sys, json
ids = [m["id"] for m in json.load(sys.stdin).get("data", [])]
print(next((m for m in ("Eltex-Coder-Senior", "Eltex-Qwen3_8-Flash-Next") if m in ids), ""))' 2>/dev/null)
fi
model="${model:-Eltex-Coder-Senior}"
echo "Модель: $model"
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=eltex-qwen
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex-qwen"
# Пикер: все три слота = модель ключа (незаданный слот показал бы чужой Claude).
# Окно 200K: у Qwen3.8-Flash-Next max_input_tokens=204800. CLAUDE_CODE_DISABLE_1M_CONTEXT
# обязателен: без него Claude Code на чистом конфиге переводит opus в opus[1m] (окно 1M).
# Effort прокси игнорирует (08.10: low/high = 3227/3297, 09.10: low/max = 4424-5838/
# 977-3370 выходных токенов), /effort ни на что не влияет. Стартуем на max через CLI
# --effort, как ai-glm и ai-eltex. Картинки Qwen3.8-Flash-Next видит сама.
# max_output_tokens=8192 из /models прокси не применяет (09.10 принят max_tokens=64000),
# поэтому вывод поднят до 64000, как у ai-glm и ai-eltex.
_setup_isolated_config eltex-qwen opus high ''
ANTHROPIC_BASE_URL="$eltex_base_url" \
ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_DEFAULT_OPUS_MODEL="$model" \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
ANTHROPIC_DEFAULT_SONNET_MODEL="$model" \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
ANTHROPIC_DEFAULT_HAIKU_MODEL="$model" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="$model" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="$model - корпоративный прокси Eltex" \
CLAUDE_CODE_SUBAGENT_MODEL="$model" \
CLAUDE_CODE_DISABLE_1M_CONTEXT=1 \
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
API_TIMEOUT_MS=600000 \
claude --dangerously-skip-permissions --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
ELTEXQWENEOF
chmod +x "$BIN_DIR/ai-eltex-qwen"
# === ai-mimo ===
cat > "$BIN_DIR/ai-mimo" << 'MIMOEOF'
#!/usr/bin/env bash
# ai-mimo - запуск Claude Code через Xiaomi MiMo V2.6 (Anthropic-compatible API, Token Plan)
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
key_file="$HOME/.config/ai-setup/mimo_key"
url_file="$HOME/.config/ai-setup/mimo_base_url"
api_key=""
reauth=0
# Принудительный сброс сохранённого ключа через AI_RESET_KEY=1
if [ "${AI_RESET_KEY:-0}" == "1" ]; then
rm -f "$key_file" "$url_file"
fi
[ -f "$key_file" ] && api_key=$(cat "$key_file")
# Ключ Token Plan (tp-...) работает только на эндпоинте региона подписки (sgp/ams/cn),
# ключ pay-as-you-go (sk-...) - на api.xiaomimimo.com. Эндпоинт находим перебором при
# проверке ключа и запоминаем в mimo_base_url; MIMO_BASE_URL перебивает.
mimo_base_url="${MIMO_BASE_URL:-}"
[ -z "$mimo_base_url" ] && [ -f "$url_file" ] && mimo_base_url=$(cat "$url_file")
# _mimo_check_key: 1-токенный запрос с ключом $1. Без известного эндпоинта перебирает
# регионы: чужой регион отвечает 401/403, первый другой ответ задаёт mimo_base_url.
# Недоступный эндпоинт (код 000*: _claude_test_api при сетевой ошибке отдаёт 000000)
# пропускаем. token-plan-cn в переборе нет: с наших машин он не отвечает (таймаут 15 с),
# для него MIMO_BASE_URL=https://token-plan-cn.xiaomimimo.com/anthropic.
# Bearer, как у Claude Code с ANTHROPIC_AUTH_TOKEN.
_mimo_check_key() {
local url auth_code="" auth_body=""
if [ -n "$mimo_base_url" ]; then
_claude_test_api "$mimo_base_url/v1/messages" "Authorization: Bearer $1" "mimo-v2.6-flash"
return
fi
for url in https://token-plan-sgp.xiaomimimo.com/anthropic \
https://token-plan-ams.xiaomimimo.com/anthropic \
https://api.xiaomimimo.com/anthropic; do
_claude_test_api "$url/v1/messages" "Authorization: Bearer $1" "mimo-v2.6-flash"
case "$_CLAUDE_TEST_CODE" in
401|403) auth_code="$_CLAUDE_TEST_CODE"; auth_body="$_CLAUDE_TEST_BODY" ;;
000*) ;;
*) mimo_base_url="$url"; return ;;
esac
done
# Ни один эндпоинт не принял ключ. Отказ в авторизации разберёт _handle_api_response,
# а без сети эндпоинт не узнать и запускать Claude Code некуда.
if [ -n "$auth_code" ]; then
_CLAUDE_TEST_CODE="$auth_code"
_CLAUDE_TEST_BODY="$auth_body"
return
fi
echo ""
echo -e "\033[0;31m[СЕТЬ]\033[0m Эндпоинты MiMo недоступны, ключ не проверен."
echo " Задай эндпоинт явно: MIMO_BASE_URL=https://token-plan-sgp.xiaomimimo.com/anthropic ai-mimo"
exit 1
}
# _mimo_check_result: разбор ответа проверки. 402 на pay-as-you-go эндпоинте значит ключ
# sk-... с нулевым балансом: подписку Token Plan он не тратит, для неё свой ключ tp-...
_mimo_check_result() {
_handle_api_response "MiMo" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Лимит Token Plan: https://platform.xiaomimimo.com"
if [ "$_API_RET" = "402" ] && [ "$mimo_base_url" = "https://api.xiaomimimo.com/anthropic" ]; then
echo " Это ключ pay-as-you-go (sk-...), подписку Token Plan он не использует."
echo " Для Token Plan создай ключ на странице Token Plan: Dedicated API Key -> Create API Key (tp-...)."
fi
}
if [ -n "$api_key" ]; then
echo -n "Проверка сохранённого MiMo ключа... "
_mimo_check_key "$api_key"
_mimo_check_result
ret=$_API_RET
if [ $ret -eq 401 ]; then
rm -f "$key_file" "$url_file"
api_key=""
mimo_base_url="${MIMO_BASE_URL:-}"
reauth=1
elif [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans
case "${_ans:-N}" in
[Yy]*) ;;
*)
echo -n "Хотите ввести новый MiMo ключ? [Y/n] "
read -r _ans2
case "${_ans2:-Y}" in
[Yy]*)
rm -f "$key_file" "$url_file"
api_key=""
mimo_base_url="${MIMO_BASE_URL:-}"
reauth=1
;;
*) exit 1 ;;
esac
;;
esac
elif [ $ret -ne 0 ]; then
exit 1
fi
fi
if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then
echo -n "Хотите ввести новый MiMo ключ? [Y/n] "
read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac
fi
if [ -z "$api_key" ]; then
echo "Получить ключ: https://platform.xiaomimimo.com (Token Plan - tp-..., pay-as-you-go - sk-...)"
read -r -p "Введите ваш Xiaomi MiMo API ключ: " api_key
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
echo -n "Проверяю ключ... "
_mimo_check_key "$api_key"
_mimo_check_result
ret=$_API_RET
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
mkdir -p "$(dirname "$key_file")"
echo "$api_key" > "$key_file"
echo "$mimo_base_url" > "$url_file"
chmod 600 "$key_file" "$url_file"
echo "Ключ сохранён (эндпоинт: $mimo_base_url)."
if [ $ret -eq 429 ]; then
echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] "
read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac
fi
else
echo "Ключ НЕ сохранён."
exit 1
fi
fi
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=mimo
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/mimo"
# Модель: всегда самая умная MiMo - mimo-v2.6-pro во всех трёх слотах и в сабагентах, как
# у ai-glm. Flash не используем: ai-mimo задуман как максимум качества, а не экономия
# кредитов. pro-ultraspeed - тот же Pro, только быстрее и в ~10 раз дороже.
# mimo-v2.5-pro/mimo-v2.5 сняты 21.10.2026 (с 14.10 их роутят на v2.6-pro/v2.6-flash):
# https://mimo.mi.com/docs/en-US/updates/deprecate
# availableModels НЕ задаём (он схлопывает пикер). --model opus при каждом запуске: выбор
# через /model живёт до конца сессии, следующий запуск снова на Pro.
# CLAUDE_CODE_MAX_CONTEXT_TOKENS: mimo-* нет в реестре моделей Claude Code, без env окно
# считалось бы = 200K (ctx: врал бы в ~5 раз, автокомпакт жал бы сессию). У Pro реальное
# окно 1M, поэтому CLAUDE_CODE_DISABLE_1M_CONTEXT не выставляем.
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000: потолок Claude Code (131072 он режет до 128000),
# MiMo V2.6 отдаёт до 131072. По умолчанию незнакомой модели досталось бы 32000 на
# размышления + ответ.
# Размышления: MiMo думает всегда. Claude Code шлёт thinking adaptive, а MiMo отрабатывает
# его так же, как enabled (замер 13.10: даже на "скажи привет" есть thinking, длина та же).
# Effort MiMo игнорирует: Claude Code шлёт output_config.effort (и без
# CLAUDE_CODE_ALWAYS_ENABLE_EFFORT), MiMo принимает даже effort=bogus, а длина размышлений
# от low до max не меняется (см. EFFORT_MAPPING.md). Стартуем на max через CLI --effort,
# как ai-eltex-qwen: если Xiaomi начнёт его учитывать, будет максимум.
_setup_isolated_config mimo opus high ''
# /logout-mimo: без зарегистрированной команды Claude Code отвечает "Unknown command" и
# не зовёт UserPromptSubmit-хук (см. ai-eltex).
mkdir -p "$CLAUDE_CONFIG_DIR/commands"
cat > "$CLAUDE_CONFIG_DIR/commands/logout-mimo.md" <<'CMDEOF'
---
description: Удалить сохранённый ключ ai-mimo (обрабатывает хук, без LLM)
---
Хук не сработал: удали ~/.config/ai-setup/mimo_key вручную и перезапусти ai-mimo (или запусти AI_RESET_KEY=1 ai-mimo).
CMDEOF
ANTHROPIC_BASE_URL="$mimo_base_url" \
ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_DEFAULT_OPUS_MODEL=mimo-v2.6-pro \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="MiMo V2.6 Pro" \
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="Xiaomi MiMo V2.6 Pro - флагман для сложных задач" \
ANTHROPIC_DEFAULT_SONNET_MODEL=mimo-v2.6-pro \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="MiMo V2.6 Pro" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Xiaomi MiMo V2.6 Pro - флагман для сложных задач" \
ANTHROPIC_DEFAULT_HAIKU_MODEL=mimo-v2.6-pro \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="MiMo V2.6 Pro" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="Xiaomi MiMo V2.6 Pro - флагман для сложных задач" \
CLAUDE_CODE_SUBAGENT_MODEL=mimo-v2.6-pro \
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
MIMOEOF
chmod +x "$BIN_DIR/ai-mimo"
# === ai-qwen ===
cat > "$BIN_DIR/ai-qwen" << 'QWENEOF'
#!/usr/bin/env bash
# ai-qwen - запуск Claude Code через Alibaba Cloud Model Studio (Qwen 3.8, Token Plan,
# Anthropic-compatible API). Ключ общий и вшит в скрипт: запускается сразу, без ввода ключа.
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
qwen_base_url="https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic"
api_key="sk-sp-H.LEDIL.UJfr.MEUCIBmN5Rtjyn9iKPnhw2sym_1auFL4YZ4tnJh54TurNMo8AiEA1SbkS2Gt872ncq-32xK2ZxlpZxS3ldNHnTgX6dNCIok"
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
_build_ai_sys_prompt > "$_PROMPT_FILE"
export AI_LAUNCHER=qwen
export CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/qwen"
# Модели (список ключа: .../compatible-mode/v1/models): qwen3.8-max - самая умная, она в
# opus/sonnet и в сабагентах; haiku = qwen3.8-flash (быстрая, Claude Code берёт слот haiku
# и для фоновых задач вроде заголовков - так дешевле для лимита Token Plan).
# availableModels НЕ задаём (он схлопывает пикер). --model opus при каждом запуске, как в ai-mimo.
# Окно: у обеих моделей вход до 983616 токенов (замер 20.10: ответ "Range of input length
# should be [1, 983616]"). qwen3.8-* нет в реестре Claude Code, без
# CLAUDE_CODE_MAX_CONTEXT_TOKENS окно считалось бы = 200K. Автокомпакт с окном 1000000
# срабатывает раньше предела API. Картинки обе модели видят сами.
# Effort Qwen проверяет (bogus -> 400, low/medium/high/xhigh/max принимаются), но у Max тренда
# нет, у Flash max размышляет почти вдвое дольше low (см. EFFORT_MAPPING.md). Стартуем на max
# через CLI --effort, как ai-mimo. CLAUDE_CODE_ALWAYS_ENABLE_EFFORT - страховка, как в ai-glm.
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 вместо дефолтных 32000, как у ai-glm.
_setup_isolated_config qwen opus high ''
ANTHROPIC_BASE_URL="$qwen_base_url" \
ANTHROPIC_AUTH_TOKEN="$api_key" \
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3.8-max \
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Qwen3.8 Max" \
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION="Qwen3.8 Max - флагман Alibaba для сложных задач" \
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3.8-max \
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="Qwen3.8 Max" \
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION="Qwen3.8 Max - флагман Alibaba для сложных задач" \
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3.8-flash \
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="Qwen3.8 Flash" \
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION="Qwen3.8 Flash - быстрая" \
CLAUDE_CODE_SUBAGENT_MODEL=qwen3.8-max \
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000 \
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}" --system-prompt-file "$_PROMPT_FILE" "$@"
QWENEOF
chmod +x "$BIN_DIR/ai-qwen"
# === ai-openrouter ===
cat > "$BIN_DIR/ai-openrouter" << 'OPENROUTEREOF'
#!/usr/bin/env bash
@@ -2510,11 +1975,7 @@ chmod +x "$BIN_DIR/ai-opencode"
source "$BIN_DIR/ai-api-helpers.sh"
_setup_isolated_config deepseek opus high ''
_setup_isolated_config kimi opus high ''
_setup_isolated_config glm opus xhigh ''
_setup_isolated_config eltex opus xhigh ''
_setup_isolated_config eltex-qwen opus high ''
_setup_isolated_config mimo opus high ''
_setup_isolated_config qwen opus high ''
_setup_isolated_config glm opus high ''
_setup_isolated_config openrouter "openai/gpt-5.5" high ''
_setup_isolated_config opencode deepseek-v4-pro high ''
@@ -2525,7 +1986,7 @@ rm -f "$BIN_DIR/claude_gpt" "$BIN_DIR/claude_deepseek" "$BIN_DIR/claude_kimi" "$
if [ "$BIN_DIR" != "$HOME/.local/bin" ]; then
warn "BIN_DIR=$BIN_DIR — удаляю старые скрипты из ~/.local/bin/ ..."
rm -f "$HOME/.local/bin/ai-gpt" "$HOME/.local/bin/ai-deepseek" "$HOME/.local/bin/ai-kimi" \
"$HOME/.local/bin/ai-glm" "$HOME/.local/bin/ai-eltex" "$HOME/.local/bin/ai-eltex-qwen" "$HOME/.local/bin/ai-mimo" "$HOME/.local/bin/ai-qwen" "$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-cursor" "$HOME/.local/bin/ai-claude" \
"$HOME/.local/bin/ai-glm" "$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-cursor" "$HOME/.local/bin/ai-claude" \
"$HOME/.local/bin/ai-opencode" \
"$HOME/.local/bin/ai-api-helpers.sh" "$HOME/.local/bin/claude-gpt-effort-proxy.py"
fi
@@ -2544,11 +2005,7 @@ echo " На базе Claude Code:"
echo -e " ${CYAN}ai-claude${NC} - Оригинальный Claude Code (Anthropic)"
echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (через Claude Code, API ключ сохраняется)"
echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.7 Code (через Claude Code, API ключ сохраняется)"
echo -e " ${CYAN}ai-glm${NC} - GLM 5.3 (через Claude Code, Zhipu AI API)"
echo -e " ${CYAN}ai-eltex${NC} - Корпоративный LLM-прокси Eltex (через Claude Code, ключ от админов)"
echo -e " ${CYAN}ai-eltex-qwen${NC} - Корпоративный LLM-прокси Eltex, модели Qwen (через Claude Code, свой ключ)"
echo -e " ${CYAN}ai-mimo${NC} - Xiaomi MiMo V2.6 Pro, максимум рассуждений (через Claude Code, Token Plan)"
echo -e " ${CYAN}ai-qwen${NC} - Qwen3.8 Max (через Claude Code, Alibaba Token Plan, ключ встроен)"
echo -e " ${CYAN}ai-glm${NC} - GLM 5.2 (через Claude Code, Zhipu AI API)"
echo -e " ${CYAN}ai-openrouter${NC} - OpenRouter (через Claude Code: GPT-5.5, Grok, Qwen, GLM 5.2 и др.)"
echo -e " ${CYAN}ai-opencode${NC} - OpenCode Go (через Claude Code + routatic-proxy: DeepSeek, Kimi, Qwen, GLM, MiniMax, MiMo)"
echo ""
@@ -2557,7 +2014,7 @@ echo -e " ${CYAN}ai-gpt${NC} - OpenAI Codex (нативный CLI
echo -e " ${CYAN}ai-gemini${NC} - Gemini (нативный agy CLI, автоустановка)"
echo -e " ${CYAN}ai-cursor${NC} - Cursor (нативный Cursor CLI, автоустановка)"
echo ""
echo " Android CLI (не ставится по умолчанию):"
echo -e " ${CYAN}android${NC} - Google Android CLI: bash setup.sh → пункт 8"
echo " Android CLI:"
echo -e " ${CYAN}android${NC} - Google Android CLI (SDK, эмулятор, проекты)"
echo ""
echo -e "Чтобы команды были доступны сразу, выполните: ${GREEN}exec bash${NC}"

View File

@@ -1,72 +0,0 @@
#!/usr/bin/env bash
# Copyright (c) 2026 Виталий Никитенко. MIT License.
# android-cli-setup.sh — установка Google Android CLI (по выбору, пункт 8 в setup.sh)
# Запуск: bash scripts/android-cli-setup.sh
# Автоопределение: ~/bin если есть в PATH, иначе ~/.local/bin
if [[ ":$PATH:" == *":$HOME/bin:"* ]]; then
BIN_DIR="$HOME/bin"
else
BIN_DIR="$HOME/.local/bin"
fi
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
# Запрет запуска от root
if [ "$EUID" -eq 0 ]; then
echo -e "${RED}Не запускайте этот скрипт через sudo!${NC}"
echo "Запустите просто: bash scripts/android-cli-setup.sh"
exit 1
fi
info "Проверяю Android CLI..."
ANDROID_BIN="$HOME/.local/bin/android"
[ ! -f "$ANDROID_BIN" ] && ANDROID_BIN="$(command -v android 2>/dev/null || true)"
if [ -z "$ANDROID_BIN" ] || [ ! -f "$ANDROID_BIN" ]; then
info "Устанавливаю Android CLI..."
_uname_s=$(uname -s)
_uname_m=$(uname -m)
ANDROID_PLATFORM=""
case "${_uname_s}-${_uname_m}" in
Linux-x86_64) ANDROID_PLATFORM="linux_x86_64" ;;
Linux-aarch64) ANDROID_PLATFORM="linux_arm64" ;;
Linux-arm64) ANDROID_PLATFORM="linux_arm64" ;;
Darwin-x86_64) ANDROID_PLATFORM="darwin_x86_64" ;;
Darwin-arm64) ANDROID_PLATFORM="darwin_arm64" ;;
Darwin-aarch64) ANDROID_PLATFORM="darwin_arm64" ;;
MINGW*|MSYS*|CYGWIN*-x86_64) ANDROID_PLATFORM="windows_x86_64" ;;
esac
if [ -n "$ANDROID_PLATFORM" ]; then
ANDROID_INSTALL_URL="https://dl.google.com/android/cli/latest/${ANDROID_PLATFORM}/install.sh"
if curl -fsSL "$ANDROID_INSTALL_URL" | bash; then
success "Android CLI установлен"
else
warn "Не удалось установить Android CLI через официальный скрипт"
fi
else
warn "Неизвестная платформа ${_uname_s} ${_uname_m}. Пропускаю Android CLI."
fi
else
success "Android CLI уже установлен: $($ANDROID_BIN --version 2>/dev/null | head -1)"
fi
# Официальный install.sh ставит бинарь в ~/.local/bin/android.
# Если пользователь использует ~/bin вместо ~/.local/bin - symlink.
if [ "$BIN_DIR" != "$HOME/.local/bin" ] && [ -f "$HOME/.local/bin/android" ] && [ ! -f "$BIN_DIR/android" ]; then
ln -sf "$HOME/.local/bin/android" "$BIN_DIR/android"
success "Android CLI symlink -> $BIN_DIR/android"
fi
# Разворачиваем Android CLI skills для агентов (Claude Code и др.)
if command -v android &>/dev/null; then
info "Запускаю android init для деплоя skills..."
mkdir -p "$HOME/.claude/skills"
android init || warn "android init завершился с ошибкой"
else
info "Android CLI не найден в PATH, пропускаю android init"
fi

View File

@@ -162,12 +162,10 @@ fi
# Без этого DNS через VPN может вернуть внешний IP вместо внутреннего,
# и трафик пойдёт через VPN вместо прямого соединения.
HOSTS_MARKER="# ru-bypass: local hosts"
HOSTS_END_MARKER="# ru-bypass: end"
# Удаляем только свой блок (от маркера до конечного маркера).
# Записи, добавленные вручную вне блока, не трогаем.
# Блок старых версий без конечного маркера удаляется до конца файла.
sed -i "/^$HOSTS_MARKER\$/,/^$HOSTS_END_MARKER\$/d" /etc/hosts
# Удаляем старый блок целиком (от маркера до конца файла)
# и заодно чистим дубликаты от старых версий скрипта (без маркера)
sed -i "/$HOSTS_MARKER/,\$ d; /eltex\.loc/d; /eltex-co\.ru/d" /etc/hosts
# Добавляем актуальные
cat >> /etc/hosts <<_HOSTS
@@ -182,10 +180,8 @@ $HOSTS_MARKER
172.16.5.63 proxy.eltex.loc
10.80.0.16 ssw.eltex.loc
172.16.5.78 nexus.eltex.loc
172.16.4.68 llm.eltex.loc llm2.eltex.loc
172.16.1.149 cpe-worker.eltex.loc
172.16.5.22 mattermost.eltex-co.ru elph.eltex-co.ru ecss-elph-proxy.eltex-co.ru study-git.eltex-co.ru study-red.eltex-co.ru
$HOSTS_END_MARKER
_HOSTS
echo "Локальные хосты: *.eltex.loc, mattermost, elph → /etc/hosts"
@@ -245,8 +241,7 @@ if [ -n "${ALL_EXC// }" ]; then
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
ips="$item"
else
ips=$(getent ahostsv4 "$item" 2>/dev/null | awk '{print $1}' | sort -u)
[ -z "$ips" ] && ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
fi
for ip in $ips; do
ipset add ru-direct "$ip" -exist 2>/dev/null || true
@@ -300,8 +295,7 @@ if [ -n "${ALL_EXC// }" ]; then
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
ips="$item"
else
ips=$(getent ahostsv4 "$item" 2>/dev/null | awk '{print $1}' | sort -u)
[ -z "$ips" ] && ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u)
fi
for ip in $ips; do
ip route replace "$ip/32" via "$GATEWAY" dev "$DEV" 2>/dev/null

View File

@@ -18,34 +18,6 @@ mkdir -p "$HOME/.config/ai-setup"
LOG="$HOME/.config/ai-setup/setup.log"
_log() { printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" >> "$LOG"; }
# Обновление: git pull + перегенерация всех ai-* лаунчеров (пункт меню 7)
do_update() {
local repo_dir
repo_dir="$(pwd)"
echo -e "${BLD}Обновляем репозиторий...${CLR}"
git -C "$repo_dir" pull --ff-only
echo ""
echo -e "${BLD}Перегенерация скриптов...${CLR}"
bash "$repo_dir/scripts/ai-setup.sh"
}
# CLI-режим: ./setup.sh update (или --update) — обновление без меню
case "${1:-}" in
update|--update)
_log "setup" "update (CLI) на $(hostname)"
do_update
exit $?
;;
"")
# без аргументов — интерактивное меню ниже
;;
*)
echo "Неизвестный аргумент: $1"
echo "Использование: ./setup.sh [update] — без аргументов открывается меню"
exit 1
;;
esac
if command -v whiptail >/dev/null 2>&1; then
MENU_ARGS=()
MENU_ARGS+=("1" "AI-инструменты (установить лаунчеры + ключи)")
@@ -107,7 +79,7 @@ else
echo -e " ${GRY}git pull + перегенерация всех скриптов в ~/bin (или ~/.local/bin).${CLR}"
echo ""
echo -e " ${BLD}8) Android CLI${CLR}"
echo -e " ${GRY}Установить Google Android CLI для Android-разработки (пункт 1 его не ставит).${CLR}"
echo -e " ${GRY}Установить Google Android CLI для Android-разработки.${CLR}"
echo ""
echo -n "Выбери [1-8] или Enter для выхода: "
read -r choice
@@ -230,7 +202,7 @@ case "$choice" in
fi
echo -e "${BLD}AI инструменты:${CLR}"
for cmd in ai-claude ai-gpt ai-deepseek ai-kimi ai-glm ai-eltex ai-eltex-qwen ai-mimo ai-qwen ai-openrouter ai-opencode ai-gemini android; do
for cmd in ai-claude ai-gpt ai-deepseek ai-kimi ai-glm ai-openrouter ai-opencode ai-gemini android; do
if command -v "$cmd" &>/dev/null; then
echo -e " ${GRN}✓${CLR} $cmd"
else
@@ -241,7 +213,7 @@ case "$choice" in
echo ""
echo -e "${BLD}API ключи:${CLR}"
cfg="$HOME/.config/ai-setup"
for f in deepseek_key kimi_key glm_key eltex_key eltex_qwen_key mimo_key openrouter_key opencode_key; do
for f in deepseek_key kimi_key glm_key openrouter_key opencode_key; do
name="${f/_key/}"
if [ -s "$cfg/$f" ]; then
echo -e " ${GRN}✓${CLR} $name"
@@ -252,7 +224,7 @@ case "$choice" in
echo ""
echo -e "${BLD}API доступность:${CLR}"
for entry in "Anthropic:api.anthropic.com" "DeepSeek:api.deepseek.com" "OpenAI:api.openai.com" "Kimi:api.kimi.com" "GLM:api.z.ai" "MiMo:api.xiaomimimo.com" "Qwen:token-plan.ap-southeast-1.maas.aliyuncs.com" "OpenRouter:openrouter.ai" "OpenCode:opencode.ai"; do
for entry in "Anthropic:api.anthropic.com" "DeepSeek:api.deepseek.com" "OpenAI:api.openai.com" "Kimi:api.kimi.com" "GLM:api.z.ai" "OpenRouter:openrouter.ai" "OpenCode:opencode.ai"; do
_name="${entry%%:*}"
_host="${entry##*:}"
_ms=$(curl -s -o /dev/null -w "%{time_connect}" --max-time 5 "https://$_host" 2>/dev/null)
@@ -276,10 +248,15 @@ case "$choice" in
bash tests/test_network.sh
;;
7)
do_update
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
echo -e "${BLD}Обновляем репозиторий...${CLR}"
git -C "$REPO_DIR" pull --ff-only
echo ""
echo -e "${BLD}Перегенерация скриптов...${CLR}"
bash "$REPO_DIR/scripts/ai-setup.sh"
;;
8)
bash scripts/android-cli-setup.sh
bash scripts/ai-setup.sh
;;
"")
exit 0

View File

@@ -8,8 +8,6 @@ set -euo pipefail
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/scripts/ai-setup.sh"
GLOBAL_RULES_SOURCE="$(cd "$(dirname "$0")/.." && pwd)/home-configs/GLOBAL_RULES.md"
ADD_HOOK="$(cd "$(dirname "$0")/.." && pwd)/home-configs/claude/hooks/add-account-hook.sh"
ANDROID_SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/scripts/android-cli-setup.sh"
SETUP_MENU="$(cd "$(dirname "$0")/.." && pwd)/setup.sh"
PASS=0; FAIL=0
ok() { echo "[PASS] $1"; PASS=$((PASS+1)); }
@@ -19,11 +17,6 @@ fail() { echo "[FAIL] $1"; FAIL=$((FAIL+1)); }
GPT_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gpt"/,/^GPTEOF/' "$SCRIPT")
KIMI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-kimi"/,/^KIMIEOF/' "$SCRIPT")
GLM_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-glm"/,/^GLMEOF/' "$SCRIPT")
ELTEX_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-eltex"/,/^ELTEXEOF/' "$SCRIPT")
ELTEX_QWEN_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-eltex-qwen"/,/^ELTEXQWENEOF/' "$SCRIPT")
MIMO_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-mimo"/,/^MIMOEOF/' "$SCRIPT")
QWEN_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-qwen"/,/^QWENEOF/' "$SCRIPT")
LOGOUT_HOOK="$(cd "$(dirname "$0")/.." && pwd)/home-configs/claude/hooks/logout-account-hook.sh"
GEMINI_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-gemini"/,/^GEMINIEOF/' "$SCRIPT")
OPENCODE_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-opencode"/,/^OPENCODEEOF/' "$SCRIPT")
CURSOR_SECTION=$(awk '/^cat > "\$BIN_DIR\/ai-cursor"/,/^CURSOREOF/' "$SCRIPT")
@@ -48,15 +41,6 @@ test_gpt_no_proxy() {
fi
}
# ── ai-gpt: informative Codex status line ───────────────────────────────────
test_gpt_status_line() {
if echo "$GPT_SECTION" | grep -Fq 'tui.status_line=["model-with-reasoning","git-branch","context-used","five-hour-limit","current-dir"]'; then
ok "ai-gpt: shows model, branch, context usage, primary limit, and directory"
else
fail "ai-gpt: missing configured Codex status line"
fi
}
# ── ai-kimi: launches claude with Kimi backend ────────────────────────────
test_kimi_claude_launcher() {
if echo "$KIMI_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
@@ -101,384 +85,12 @@ test_glm_claude_launcher() {
# ── ai-glm: uses official Zhipu AI API ────────────────────────────────────
test_glm_official_api() {
if echo "$GLM_SECTION" | grep -q 'api.z.ai/api/anthropic' \
&& echo "$GLM_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.3' \
&& echo "$GLM_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-5.3' \
&& echo "$GLM_SECTION" | grep -q 'CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
&& echo "$GLM_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL=glm-5.2' \
&& echo "$GLM_SECTION" | grep -q "ANTHROPIC_DEFAULT_HAIKU_MODEL='glm-5.2\[1m\]'" \
&& echo "$GLM_SECTION" | grep -q 'x-api-key'; then
ok "ai-glm: uses official Zhipu AI API (glm-5.3 everywhere, ctx window 1M via env)"
ok "ai-glm: uses official Zhipu AI API (glm-5.2 opus/sonnet, glm-5.2[1m] haiku)"
else
fail "ai-glm: must use official Zhipu AI API with glm-5.3, 1M context window and x-api-key"
fi
}
# ── ai-eltex: Claude Code через корпоративный прокси Eltex ─────────────────
test_eltex_claude_launcher() {
if echo "$ELTEX_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
&& echo "$ELTEX_SECTION" | grep -q 'ELTEX_BASE_URL:-http://llm.eltex.loc:4000' \
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_BASE_URL="$eltex_base_url"' \
&& echo "$ELTEX_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex"' \
&& echo "$ELTEX_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/eltex_key"'; then
ok "ai-eltex: launches claude via llm.eltex.loc with its own key and config dir"
else
fail "ai-eltex: must launch claude via llm.eltex.loc with eltex_key and cfg/eltex"
fi
}
# ── ai-eltex: модель берётся из /v1/models ключа ──────────────────────────
test_eltex_model_from_key() {
if echo "$ELTEX_SECTION" | grep -q '/v1/models' \
&& echo "$ELTEX_SECTION" | grep -Fq 'model="${ELTEX_MODEL:-${model:-anth-glm-5.3}}"' \
&& echo "$ELTEX_SECTION" | grep -Fq 'pick("anth-glm-5.3", "anth-glm-5.3-")' \
&& ! echo "$ELTEX_SECTION" | grep -q 'ids\[0\]' \
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL="$opus_model"' \
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_SONNET_MODEL="$opus_model"' \
&& echo "$ELTEX_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL="$opus_model"'; then
ok "ai-eltex: anth-glm-5.3 by default, never the first /v1/models entry (ELTEX_MODEL overrides)"
else
fail "ai-eltex: must default to anth-glm-5.3 (not the first /v1/models entry), ELTEX_MODEL overrides"
fi
}
# ── ai-eltex: anth-glm-5.3 во всех слотах пикера (имена с суффиксом тоже) ──
test_eltex_three_slots() {
local tmp ok_run=1 out
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home/.config/ai-setup"
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
echo "$ELTEX_SECTION" | BIN_DIR="$tmp/bin" bash
cat > "$tmp/mock/curl" <<'CURLEOF'
#!/usr/bin/env bash
printf '%s\n%s' "$MODELS_BODY" "200"
CURLEOF
cat > "$tmp/mock/claude" <<'CLAUDEEOF'
#!/usr/bin/env bash
echo "SLOTS opus=$ANTHROPIC_DEFAULT_OPUS_MODEL sonnet=$ANTHROPIC_DEFAULT_SONNET_MODEL haiku=$ANTHROPIC_DEFAULT_HAIKU_MODEL sub=$CLAUDE_CODE_SUBAGENT_MODEL"
CLAUDEEOF
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
echo sk-test > "$tmp/home/.config/ai-setup/eltex_key"
# Как на прокси 21.10: zai-модели идут первыми, плюс имя группы доступа
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-flash"},{"id":"anth-glm-5.3"},{"id":"zai-glm-5.3"},{"id":"ZAI-Test-Group"}]}' \
bash "$tmp/bin/ai-eltex" 2>&1)
echo "$out" | grep -Fq 'SLOTS opus=anth-glm-5.3[1m] sonnet=anth-glm-5.3[1m] haiku=anth-glm-5.3[1m] sub=anth-glm-5.3[1m]' || ok_run=0
# Имя с суффиксом
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-2"},{"id":"anth-glm-5.3-2"}]}' \
bash "$tmp/bin/ai-eltex" 2>&1)
echo "$out" | grep -Fq 'SLOTS opus=anth-glm-5.3-2[1m] sonnet=anth-glm-5.3-2[1m] haiku=anth-glm-5.3-2[1m]' || ok_run=0
if [ "$ok_run" -eq 1 ]; then
ok "ai-eltex: anth-glm-5.3[1m] in every picker slot, zai/flash never picked"
else
fail "ai-eltex: every picker slot must be anth-glm-5.3[1m], zai/flash must never be picked"
fi
rm -rf "$tmp"
}
# ── ai-eltex: нет anth-glm-5.3 у ключа -> авто-логаут, свежий ключ не сохраняется ──
test_eltex_autologout_on_missing_model() {
local tmp ok_run=1 rc=0 out
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home/.config/ai-setup"
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
echo "$ELTEX_SECTION" | BIN_DIR="$tmp/bin" bash
# Прокси отвечает 200, но anth-glm-5.3 в списке моделей ключа нет
cat > "$tmp/mock/curl" <<'CURLEOF'
#!/usr/bin/env bash
if [[ "$*" == *"/v1/models"* ]]; then
printf '%s\n%s' "$MODELS_BODY" "200"
else
printf '%s\n%s' '{"ok":true}' "200"
fi
CURLEOF
printf '#!/usr/bin/env bash\nexit 0\n' > "$tmp/mock/claude"
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
# Сохранённый ключ: авто-логаут (файл удалён), новый ключ не вводим -> выход 1
echo sk-old > "$tmp/home/.config/ai-setup/eltex_key"
out=$(printf 'n\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-flash-2"},{"id":"ZAI-Test-Group"}]}' \
bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
[ "$rc" -eq 1 ] || ok_run=0
echo "$out" | grep -q 'Ключу недоступна модель anth-glm-5.3' || ok_run=0
echo "$out" | grep -q 'авто-логаут' || ok_run=0
[ ! -f "$tmp/home/.config/ai-setup/eltex_key" ] || ok_run=0
# Свежий ключ без anth-glm-5.3 не сохраняется вовсе
rc=0
out=$(printf 'sk-new\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-flash-2"}]}' \
bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
[ "$rc" -eq 1 ] || ok_run=0
echo "$out" | grep -q 'Ключ НЕ сохранён' || ok_run=0
[ ! -f "$tmp/home/.config/ai-setup/eltex_key" ] || ok_run=0
# ELTEX_MODEL перебивает проверку: ключ живёт, запуск идёт
echo sk-keep > "$tmp/home/.config/ai-setup/eltex_key"
rc=0
out=$(HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MODELS_BODY='{"data":[{"id":"zai-glm-5.3-flash-2"}]}' \
ELTEX_MODEL=anth-glm-5.3-2 bash "$tmp/bin/ai-eltex" 2>&1) || rc=$?
[ "$rc" -eq 0 ] || ok_run=0
echo "$out" | grep -q 'Модель: anth-glm-5.3-2' || ok_run=0
[ "$(cat "$tmp/home/.config/ai-setup/eltex_key")" = "sk-keep" ] || ok_run=0
if [ "$ok_run" -eq 1 ]; then
ok "ai-eltex: missing anth-glm-5.3 auto-logouts saved key, fresh key not saved, ELTEX_MODEL bypasses"
else
fail "ai-eltex: missing anth-glm-5.3 must auto-logout (saved key removed, fresh key not saved, ELTEX_MODEL bypass)"
fi
rm -rf "$tmp"
}
# ── ai-eltex: без ENABLE_TOOL_SEARCH (на anthropic-пути GLM теряет Read/Bash) ──
test_eltex_no_tool_search() {
if echo "$ELTEX_SECTION" | grep -q '^ENABLE_TOOL_SEARCH='; then
fail "ai-eltex: must not force ENABLE_TOOL_SEARCH (GLM loses core tools on anthropic path)"
else
ok "ai-eltex: ENABLE_TOOL_SEARCH not forced"
fi
}
# ── ai-eltex: окно 1M, как у ai-glm (иначе ctx: завышен в ~5 раз) ─────────
test_eltex_context_window() {
if echo "$ELTEX_SECTION" | grep -Fq 'anth-glm-5.3*) opus_model="${model}[1m]"' \
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=' \
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=' \
&& ! echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_AUTO_COMPACT_WINDOW='; then
ok "ai-eltex: 1M window for anth-glm-5.3 via [1m], no global MAX_CONTEXT / DISABLE_1M"
else
fail "ai-eltex: 1M for anth-glm-5.3 via [1m], no global CLAUDE_CODE_MAX_CONTEXT_TOKENS / DISABLE_1M / AUTO_COMPACT_WINDOW"
fi
}
# ── ai-eltex-qwen: свой ключ и конфиг, модели Qwen, окно 200K ─────────────
test_eltex_qwen_launcher() {
if echo "$ELTEX_QWEN_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/eltex_qwen_key"' \
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/eltex-qwen"' \
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'model="${ELTEX_QWEN_MODEL:-}"' \
&& echo "$ELTEX_QWEN_SECTION" | grep -q '("Eltex-Coder-Senior", "Eltex-Qwen3_8-Flash-Next")' \
&& echo "$ELTEX_QWEN_SECTION" | grep -q 'api_key="${AI_ELTEX_LLM_API_KEY:-}"' \
&& echo "$ELTEX_QWEN_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=1'; then
ok "ai-eltex-qwen: own key/config, Coder-Senior then Flash-Next, 200K window"
else
fail "ai-eltex-qwen: must use eltex_qwen_key, cfg/eltex-qwen, Qwen model preference and DISABLE_1M"
fi
}
test_logout_eltex_qwen() {
local tmp_home rc=0
tmp_home=$(mktemp -d)
mkdir -p "$tmp_home/.config/ai-setup"
echo "sk-test" > "$tmp_home/.config/ai-setup/eltex_qwen_key"
echo "sk-glm" > "$tmp_home/.config/ai-setup/eltex_key"
echo '{"prompt":"/logout"}' | HOME="$tmp_home" AI_LAUNCHER=eltex-qwen bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/eltex_qwen_key" ] && [ -f "$tmp_home/.config/ai-setup/eltex_key" ]; then
ok "logout hook: /logout in ai-eltex-qwen removes only eltex_qwen_key"
else
fail "logout hook: /logout in ai-eltex-qwen must remove eltex_qwen_key and keep eltex_key (rc=$rc)"
fi
rm -rf "$tmp_home"
}
# ── ai-eltex / ai-eltex-qwen: effort max по умолчанию через CLI, вывод 64000 ────
test_eltex_effort() {
if echo "$ELTEX_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \
&& echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1' \
&& echo "$ELTEX_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \
&& ! echo "$ELTEX_SECTION" | grep -q '^_apply_effort'; then
ok "ai-eltex: starts with --effort max by default, AI_EFFORT via CLI, 64000 output tokens"
else
fail "ai-eltex: must start with --effort \"\${AI_EFFORT:-max}\", CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1, MAX_OUTPUT_TOKENS=64000, no _apply_effort"
fi
}
test_eltex_qwen_effort() {
if echo "$ELTEX_QWEN_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \
&& echo "$ELTEX_QWEN_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \
&& ! echo "$ELTEX_QWEN_SECTION" | grep -q '^_apply_effort'; then
ok "ai-eltex-qwen: starts with --effort max by default, 64000 output tokens"
else
fail "ai-eltex-qwen: must start with --effort \"\${AI_EFFORT:-max}\" and MAX_OUTPUT_TOKENS=64000, no _apply_effort"
fi
}
# ── ai-eltex: команда /logout-eltex зарегистрирована (иначе Unknown command, хук не зовётся) ──
test_eltex_logout_command() {
if echo "$ELTEX_SECTION" | grep -q 'cat > "$CLAUDE_CONFIG_DIR/commands/logout-eltex.md"'; then
ok "ai-eltex: registers /logout-eltex command so the logout hook receives it"
else
fail "ai-eltex: must create \$CLAUDE_CONFIG_DIR/commands/logout-eltex.md"
fi
}
# ── logout hook: /logout-eltex удаляет eltex_key ──────────────────────────
test_logout_eltex() {
local tmp_home rc=0
tmp_home=$(mktemp -d)
mkdir -p "$tmp_home/.config/ai-setup"
echo "sk-test" > "$tmp_home/.config/ai-setup/eltex_key"
echo '{"prompt":"/logout"}' | HOME="$tmp_home" AI_LAUNCHER=eltex bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/eltex_key" ]; then
ok "logout hook: /logout in ai-eltex removes eltex_key"
else
fail "logout hook: /logout in ai-eltex must remove eltex_key (rc=$rc)"
fi
rm -rf "$tmp_home"
}
# ── ai-mimo: Claude Code через Xiaomi MiMo V2.6 ───────────────────────────
test_mimo_launcher() {
if echo "$MIMO_SECTION" | grep -q 'claude --dangerously-skip-permissions' \
&& echo "$MIMO_SECTION" | grep -q 'key_file="$HOME/.config/ai-setup/mimo_key"' \
&& echo "$MIMO_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/mimo"' \
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_BASE_URL="$mimo_base_url"' \
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_OPUS_MODEL=mimo-v2.6-pro' \
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_SONNET_MODEL=mimo-v2.6-pro' \
&& echo "$MIMO_SECTION" | grep -q '^ANTHROPIC_DEFAULT_HAIKU_MODEL=mimo-v2.6-pro' \
&& echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_SUBAGENT_MODEL=mimo-v2.6-pro' \
&& echo "$MIMO_SECTION" | grep -q -- 'claude --dangerously-skip-permissions --model opus ' \
&& echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000' \
&& echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
&& ! echo "$MIMO_SECTION" | grep -q '^CLAUDE_CODE_DISABLE_1M_CONTEXT=' \
&& echo "$MIMO_SECTION" | grep -q 'cat > "$CLAUDE_CONFIG_DIR/commands/logout-mimo.md"'; then
ok "ai-mimo: own key/config, Pro in every slot and subagents, --model opus, 128000 output, 1M window, /logout-mimo"
else
fail "ai-mimo: must use mimo_key, cfg/mimo, mimo-v2.6-pro everywhere, --model opus, 128000 output, 1M window and logout-mimo.md"
fi
}
# ── ai-mimo: эндпоинт региона Token Plan находится перебором и запоминается ──
test_mimo_region_probe() {
local tmp ok_run=1
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home"
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
echo "$MIMO_SECTION" | BIN_DIR="$tmp/bin" bash
# Ключ подходит только к региону ams, sgp недоступен по сети (как curl: -w печатает
# 000 и exit 28), остальные эндпоинты отвечают 401. MOCK_DOWN=1 - сети нет совсем.
cat > "$tmp/mock/curl" <<CURLEOF
#!/usr/bin/env bash
for a in "\$@"; do
case "\$a" in
https://*) echo "\$a" >> "$tmp/curl.log" ;;
esac
case "\$a" in
https://*) [ -n "\${MOCK_DOWN:-}" ] && { printf '\n000'; exit 28; } ;;
esac
case "\$a" in
https://api.xiaomimimo.com/*) [ -n "\${MOCK_SK:-}" ] && { printf '{"error":{"message":"Insufficient account balance"}}\n402'; exit 0; } ;;
https://token-plan-*) [ -n "\${MOCK_SK:-}" ] && { printf '{"error":{"message":"invalid api key"}}\n401'; exit 0; } ;;
esac
case "\$a" in
https://token-plan-sgp.xiaomimimo.com/*) printf '\n000'; exit 28 ;;
https://token-plan-ams.xiaomimimo.com/*) printf '{"content":[]}\n200'; exit 0 ;;
https://*) printf '{"error":{"message":"invalid api key"}}\n401'; exit 0 ;;
esac
done
CURLEOF
printf '#!/usr/bin/env bash\necho "$ANTHROPIC_BASE_URL" > "%s/claude.url"\n' "$tmp" > "$tmp/mock/claude"
chmod +x "$tmp/mock/curl" "$tmp/mock/claude"
_run_mimo() {
(cd "$tmp" && printf 'tp-test\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" \
MIMO_BASE_URL= AI_RESET_KEY= AI_EFFORT= bash "$tmp/bin/ai-mimo" >/dev/null 2>&1)
}
local ams=https://token-plan-ams.xiaomimimo.com/anthropic
_run_mimo || ok_run=0
[ "$(cat "$tmp/claude.url" 2>/dev/null)" = "$ams" ] || ok_run=0
[ "$(cat "$tmp/home/.config/ai-setup/mimo_base_url" 2>/dev/null)" = "$ams" ] || ok_run=0
[ "$(cat "$tmp/home/.config/ai-setup/mimo_key" 2>/dev/null)" = "tp-test" ] || ok_run=0
# Повторный запуск: сохранённый эндпоинт, без перебора регионов.
rm -f "$tmp/curl.log" "$tmp/claude.url"
_run_mimo || ok_run=0
[ "$(cat "$tmp/curl.log" 2>/dev/null)" = "$ams/v1/messages" ] || ok_run=0
[ "$(cat "$tmp/claude.url" 2>/dev/null)" = "$ams" ] || ok_run=0
# Без сети: ключ не сохраняется и Claude Code не запускается с пустым эндпоинтом.
rm -rf "$tmp/home" "$tmp/claude.url"
MOCK_DOWN=1 _run_mimo && ok_run=0
[ ! -e "$tmp/claude.url" ] && [ ! -e "$tmp/home/.config/ai-setup/mimo_key" ] || ok_run=0
# Ключ pay-as-you-go без баланса: Token Plan отвечает 401, api.xiaomimimo.com - 402,
# нужна подсказка про ключ tp-...
rm -rf "$tmp/home"
local out
out=$(cd "$tmp" && printf 'sk-test\n' | HOME="$tmp/home" PATH="$tmp/mock:$PATH" MOCK_SK=1 \
MIMO_BASE_URL= AI_RESET_KEY= AI_EFFORT= bash "$tmp/bin/ai-mimo" 2>&1) && ok_run=0
echo "$out" | grep -q 'Dedicated API Key' || ok_run=0
[ ! -e "$tmp/claude.url" ] && [ ! -e "$tmp/home/.config/ai-setup/mimo_key" ] || ok_run=0
if [ "$ok_run" -eq 1 ]; then
ok "ai-mimo: finds the Token Plan region by probing (skips unreachable), saves and reuses it"
else
fail "ai-mimo: region probe must skip unreachable regions, pick the one that accepts the key and reuse it"
fi
rm -rf "$tmp"
}
# ── ai-mimo: effort max по умолчанию через CLI (MiMo effort игнорирует) ────
test_mimo_effort() {
if echo "$MIMO_SECTION" | grep -q -- '--effort "${AI_EFFORT:-max}"' \
&& ! echo "$MIMO_SECTION" | grep -q '^_apply_effort'; then
ok "ai-mimo: starts with --effort max by default, AI_EFFORT via CLI"
else
fail "ai-mimo: must start with --effort \"\${AI_EFFORT:-max}\", no _apply_effort"
fi
}
test_logout_mimo() {
local tmp_home rc=0
tmp_home=$(mktemp -d)
mkdir -p "$tmp_home/.config/ai-setup"
echo "tp-test" > "$tmp_home/.config/ai-setup/mimo_key"
echo "https://token-plan-ams.xiaomimimo.com/anthropic" > "$tmp_home/.config/ai-setup/mimo_base_url"
echo '{"prompt":"/logout-mimo"}' | HOME="$tmp_home" AI_LAUNCHER=claude bash "$LOGOUT_HOOK" 2>/dev/null || rc=$?
if [ "$rc" -eq 2 ] && [ ! -f "$tmp_home/.config/ai-setup/mimo_key" ] && [ ! -f "$tmp_home/.config/ai-setup/mimo_base_url" ]; then
ok "logout hook: /logout-mimo removes mimo_key and mimo_base_url"
else
fail "logout hook: /logout-mimo must remove mimo_key and mimo_base_url (rc=$rc)"
fi
rm -rf "$tmp_home"
}
# ── ai-qwen: Claude Code через Alibaba Token Plan, ключ вшит ───────────────
test_qwen_launcher() {
if echo "$QWEN_SECTION" | grep -q 'CLAUDE_CONFIG_DIR="$HOME/.config/ai-setup/cfg/qwen"' \
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_BASE_URL="$qwen_base_url"' \
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3.8-max' \
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3.8-max' \
&& echo "$QWEN_SECTION" | grep -q '^ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3.8-flash' \
&& echo "$QWEN_SECTION" | grep -q '^CLAUDE_CODE_SUBAGENT_MODEL=qwen3.8-max' \
&& echo "$QWEN_SECTION" | grep -q '^CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000' \
&& echo "$QWEN_SECTION" | grep -q -- 'claude --dangerously-skip-permissions --model opus --effort "${AI_EFFORT:-max}"'; then
ok "ai-qwen: own config, qwen3.8-max in opus/sonnet/subagents, flash in haiku, 1M window, effort max"
else
fail "ai-qwen: must use cfg/qwen, qwen3.8-max/flash slots, 1M window, --model opus and --effort max"
fi
}
# ── ai-qwen: стартует сразу со вшитым ключом, ничего не спрашивая ─────────
test_qwen_starts_without_prompt() {
local tmp url token ok_run=1
tmp=$(mktemp -d)
mkdir -p "$tmp/bin" "$tmp/mock" "$tmp/home"
echo "$HELPERS_SECTION" | HELPERS_FILE="$tmp/bin/ai-api-helpers.sh" bash
echo "$QWEN_SECTION" | BIN_DIR="$tmp/bin" bash
printf '#!/usr/bin/env bash\necho "$ANTHROPIC_BASE_URL $ANTHROPIC_AUTH_TOKEN" > "%s/claude.env"\n' "$tmp" > "$tmp/mock/claude"
chmod +x "$tmp/mock/claude"
(cd "$tmp" && HOME="$tmp/home" PATH="$tmp/mock:$PATH" AI_EFFORT= bash "$tmp/bin/ai-qwen" </dev/null >/dev/null 2>&1) || ok_run=0
read -r url token < "$tmp/claude.env" 2>/dev/null || ok_run=0
[ "${url:-}" = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic" ] || ok_run=0
case "${token:-}" in sk-sp-?*) ;; *) ok_run=0 ;; esac
if [ "$ok_run" -eq 1 ]; then
ok "ai-qwen: starts Claude Code with the built-in endpoint and key, no prompt"
else
fail "ai-qwen: must start Claude Code right away with the built-in Token Plan endpoint and sk-sp- key"
fi
rm -rf "$tmp"
}
# ── ai-glm: default effort is xhigh (still overridable via AI_EFFORT) ──────
test_glm_default_effort() {
if echo "$GLM_SECTION" | grep -q -- '--effort "${AI_EFFORT:-xhigh}"' \
&& echo "$GLM_SECTION" | grep -q '^CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000' \
&& echo "$GLM_SECTION" | grep -q 'CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1'; then
ok "ai-glm: starts with --effort xhigh by default, effort sent to Z.ai, 64000 output tokens"
else
fail "ai-glm: must start with --effort \"\${AI_EFFORT:-xhigh}\", CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 and MAX_OUTPUT_TOKENS=64000"
fail "ai-glm: must use official Zhipu AI API with glm-5.2 and x-api-key"
fi
}
@@ -502,14 +114,20 @@ test_cursor_native_launcher() {
fi
}
# ── global rules: source and native rule files ───────────────────────
test_global_rules_source_installed() {
if [ -s "$GLOBAL_RULES_SOURCE" ] \
# ── global rules: Karpathy-style guidelines and native rule files ───────────
test_global_rules_include_quality_guidelines() {
karpathy_line=$(grep -n '^## 1\. Think Before Coding$' "$GLOBAL_RULES_SOURCE" | head -1 | cut -d: -f1)
global_line=$(grep -n '^# Global Rules for All AI Agents$' "$GLOBAL_RULES_SOURCE" | head -1 | cut -d: -f1)
if [ -n "$karpathy_line" ] \
&& [ -n "$global_line" ] \
&& [ "$karpathy_line" -lt "$global_line" ] \
&& grep -q 'Always reply in Russian' "$GLOBAL_RULES_SOURCE" \
&& grep -q 'Plain git diff visibility' "$GLOBAL_RULES_SOURCE" \
&& grep -q 'GLOBAL_RULES_SOURCE=' "$SCRIPT" \
&& grep -q 'cp "$GLOBAL_RULES_SOURCE" "$CONFIG_DIR/global_rules.md"' "$SCRIPT"; then
ok "global rules: non-empty source file is installed as global_rules.md"
ok "global rules: source file includes English Karpathy guidelines before user rules"
else
fail "global rules: missing source file or installation wiring"
fail "global rules: missing source file, English Karpathy guidelines, or user rules"
fi
}
@@ -557,33 +175,9 @@ test_script_syntax() {
fi
}
# ── Android CLI: opt-in install (setup.sh item 8), not part of ai-setup.sh ─
test_android_script_syntax() {
if bash -n "$ANDROID_SCRIPT" 2>&1; then
ok "android-cli: android-cli-setup.sh passes 'bash -n'"
else
fail "android-cli: android-cli-setup.sh has syntax errors"
fi
}
test_android_not_installed_by_default() {
if ! grep -q 'dl.google.com/android/cli' "$SCRIPT" && ! grep -q 'android init' "$SCRIPT"; then
ok "android-cli: ai-setup.sh does not install Android CLI by default"
else
fail "android-cli: ai-setup.sh still installs Android CLI"
fi
}
test_android_menu_item_runs_script() {
if awk '/^ 8\)/,/;;/' "$SETUP_MENU" | grep -q 'bash scripts/android-cli-setup.sh'; then
ok "android-cli: setup.sh item 8 runs android-cli-setup.sh"
else
fail "android-cli: setup.sh item 8 does not run android-cli-setup.sh"
fi
}
# ── Android CLI: install block exists ─────────────────────────────────────
test_android_cli_install_url() {
if grep -q 'dl.google.com/android/cli/latest' "$ANDROID_SCRIPT"; then
if grep -q 'dl.google.com/android/cli/latest' "$SCRIPT"; then
ok "android-cli: install URL for Android CLI is present"
else
fail "android-cli: missing install URL for Android CLI"
@@ -591,7 +185,7 @@ test_android_cli_install_url() {
}
test_android_init_called() {
if grep -q 'android init' "$ANDROID_SCRIPT"; then
if grep -q 'android init' "$SCRIPT"; then
ok "android-cli: android init is invoked after install"
else
fail "android-cli: missing android init invocation"
@@ -599,7 +193,7 @@ test_android_init_called() {
}
test_android_symlink_to_bin_dir() {
if grep -q 'ln -sf "$HOME/.local/bin/android" "$BIN_DIR/android"' "$ANDROID_SCRIPT"; then
if grep -q 'ln -sf "$HOME/.local/bin/android" "$BIN_DIR/android"' "$SCRIPT"; then
ok "android-cli: symlink to BIN_DIR is handled"
else
fail "android-cli: missing symlink to BIN_DIR"
@@ -697,40 +291,17 @@ test_add_hook_syntax() {
test_script_syntax
test_gpt_autoinstall
test_gpt_no_proxy
test_gpt_status_line
test_kimi_claude_launcher
test_kimi_official_api
test_kimi_no_artemox
test_glm_claude_launcher
test_glm_official_api
test_glm_default_effort
test_eltex_claude_launcher
test_eltex_model_from_key
test_eltex_three_slots
test_eltex_autologout_on_missing_model
test_eltex_no_tool_search
test_eltex_context_window
test_eltex_logout_command
test_eltex_effort
test_eltex_qwen_effort
test_logout_eltex
test_eltex_qwen_launcher
test_logout_eltex_qwen
test_mimo_launcher
test_mimo_region_probe
test_mimo_effort
test_logout_mimo
test_qwen_launcher
test_qwen_starts_without_prompt
test_gemini_native_launcher
test_cursor_native_launcher
test_global_rules_source_installed
test_global_rules_include_quality_guidelines
test_native_rule_files_generated
test_codex_sound_hooks_detach_stdio
test_fix7_trap_tmp
test_android_script_syntax
test_android_not_installed_by_default
test_android_menu_item_runs_script
test_android_cli_install_url
test_android_init_called
test_android_symlink_to_bin_dir