Compare commits

..

83 Commits

Author SHA1 Message Date
Виталий Никитенко
61a4421ed2 fix: статусбар показывает реальный email активного Claude-аккаунта
Статусбар слепо доверял accounts/current и показывал legacy-имя даже когда
активна сессия другого аккаунта. Теперь сверяем accessToken из
<current>.credentials.json с активным ~/.claude/.credentials.json и при
расхождении резолвим аккаунт по токену: локальный матч плюс account-email.sh
вместо haiku-велосипеда. Kimi-блок показывает alias (account1/account2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 06:41:33 +03:00
1cb4853dca refactor: нативный persistence effort, выпил мёртвого кэша effort_*
EFFORT_MAPPING, statusline и _apply_effort переведены на нативное
хранение уровня в settings.json лаунчера. Убран CLAUDE_CODE_EFFORT_LEVEL
(он блокировал /effort внутри сессии) и кэш ~/.cache/ai-setup/effort_*,
который никто не читал и который врал относительно реального уровня.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 05:21:12 +03:00
7a5a977aca feat: поддержка API-ключей в add/switch-account, не только Claude.ai
Хуки add-account и switch-account теперь ветвятся по AI_LAUNCHER:
claude - циклический обход сохранённых Claude.ai аккаунтов,
kimi - добавление и переключение API-ключей по кругу.
Skills обновлены под "account or API key".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 05:21:12 +03:00
2632f4af11 feat: добавить поддержку Google Android CLI и android-cli skill
- Установка android CLI из dl.google.com для linux/mac/windows
- Запуск android init для деплоя skills в агентов
- Кастомный skill android-cli в home-configs/claude/skills/
- Пункт меню 8) Android CLI в setup.sh
- Тесты и документация

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-14 09:52:26 +03:00
38b5f2710c docs: sync README/QUICK_START/EFFORT_MAPPING with current code
- Update ai-openrouter models in ai-setup.sh output and header
- Fix effort persistence wording (statusbar -> launcher)
- Unify KS_EXCEPTIONS -> KILL_SWITCH_EXCEPTIONS in docs and setup.sh prompt
- Clarify menu item 7 only updates AI launchers
- Add IPv6 note after ks-off.sh
- Scope UFW before.rules insertion to *filter section

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 09:16:37 +03:00
6c2844ce92 feat: добавить поддержку Kimi K2.7 Code в ai-kimi
- Opus/Sonnet: kimi-k2.7 (флагманский программист)
- Haiku/Subagent: kimi-k2.6 (быстрый универсал)
- Проверка ключа тестирует kimi-k2.7
- Обновлён EFFORT_MAPPING.md и тесты

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-11 22:45:49 +03:00
54742d6a36 feat: гармоничная палитра статусной строки и радужный effort max
Цветовая палитра (все из 256-color, мягкие оттенки):
  CWD/Branch:      серый (250)
  Model/Account:    кремовый (223)
  Effort [low]:     золотой (220)
  Effort [medium]:  бирюзовый (43)
  Effort [high]:    небесно-голубой (39)
  Effort [xhigh]:   лавандовый (171)
  Effort [max]:     радуга (210/220/114/43/171) — скобки участвуют
  DeepSeek баланс:  светло-фиолетовый (147)
  Rate/ctx <40%:    мягкий зелёный (114)
  Rate/ctx 40-60%:  золотистый (221)
  Rate/ctx >=60%:   мягкий красный (210)

- _effort_color выводит [level] целиком (для max — посимвольно)
- Effort persistence: StatusLine сохраняет effort в кэш лаунчера
- Мультивалютный баланс DeepSeek с символами ($ и ¥)
- Автоопределение аккаунта Claude.ai по OAuth токену

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-11 22:21:19 +03:00
44e3ea90f9 fix: изоляция effort между ai-* лаунчерами и персистентность
- _restore_effort: каждый лаунчер читает свой effort из
  ~/.cache/ai-setup/effort_<launcher> и записывает в settings.json
- effort-save-hook.sh: сохраняет effortLevel из settings.json в кэш
  при завершении сессии (через Claude Code hooks)
- Все лаунчеры (claude/deepseek/kimi/openrouter) экспортируют
  AI_LAUNCHER для идентификации в statusline и хуках
- _deepseek_balance: мультивалютный вывод (USD + CNY с символами $ и ¥)
- Дефолтные effort: claude=xhigh, deepseek/kimi/openrouter=high

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-11 22:21:13 +03:00
3f61f15507 fix: автоопределение аккаунта, изоляция effort, фикс разлогина
- switch-account-hook.sh: сохранять обновлённые OAuth-токены обратно в файл
  аккаунта перед переключением — предотвращает 401 после обратного свитча
- statusline-command.sh:
  - effort.level из stdin (сессия), а не из общего settings.json —
    ai-claude и ai-deepseek больше не пересекаются
  - автоопределение аккаунта по access-токену в .credentials.json
  - фолбек: если аккаунт не найден — запрос к haiku напрямую через
    Anthropic (в обход DeepSeek) для определения email
  - показ аккаунта только для claude-* моделей
2026-06-11 21:20:44 +03:00
Виталий Никитенко
23256d9579 feat: аккаунт между веткой и моделью, exit 0 в switch-account хуке
- statusline: [account] выводится между [branch] и model, тот же оранжевый цвет
- switch-account-hook: exit 0 вместо exit 2, чтобы Claude ответил и перерисовал статусбар
- switch-account skill: инструкция отвечать одним символом ✓

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:55:05 +03:00
cff3ed880d fix: убрать все хаки обновления статусной строки
Claude Code не предоставляет API для обновления статусной строки в idle.
SIGWINCH, TIOCSWINSZ, запись в TTY, write в stdin - ничего не работает.
Статусная строка обновится при следующем LLM запросе автоматически.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:35:31 +03:00
08f23e857e feat: прямая перерисовка статусной строки через TTY escape-коды
SIGWINCH и TIOCSWINSZ не заставляют Claude Code обновить статусную строку.
Запускаем statusline-command.sh с кешем и пишем результат напрямую
в TTY claude через \0337 (save cursor) / \033[999B / \033[2K / \0338.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:22:36 +03:00
986abf5101 fix: resize через TIOCSWINSZ ioctl на TTY вместо kill -WINCH
SIGWINCH напрямую игнорируется. TIOCSWINSZ на TTY claude посылает
SIGWINCH через kernel к foreground process group.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:20:35 +03:00
7187aa6669 fix: SIGWINCH с задержкой 0.3s в фоне после exit 2
SIGWINCH до exit 2 игнорируется - claude ещё рисует блокировку.
Запускаем sleep+kill в фоне, они живут после завершения хука.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:18:29 +03:00
c86110fbd6 fix: SIGWINCH точно в процесс claude (дедушка хука sh->claude)
bash(хук) -> sh -c -> claude. Шлём SIGWINCH через ppid от sh_pid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:17:06 +03:00
76fb86f910 feat: статусная строка обновляется сразу после /switch-account
Посылаем SIGWINCH родительскому процессу claude после переключения -
это заставляет TUI перерисовать UI включая статусную строку.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:11:44 +03:00
50c26736f1 fix: хук читает поле prompt (не user_prompt) - реальный Claude Code
Документация врала: реальный UserPromptSubmit шлёт поле prompt, а не user_prompt.
Хук получал пустую строку и выходил с exit 0, пропуская блокировку.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:06:10 +03:00
71ef0f76f3 fix: switch-account - скилл-заглушка + exit 2 для блокировки LLM
Без скилла Claude Code выдаёт "Unknown command" до запуска хука.
Скилл нужен как регистрация команды, но тело пустое - хук перехватывает
через exit 2 (stderr) до вызова LLM. Откат изменения в ai-setup.sh
которое скрывало скилл от деплоя.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:03:20 +03:00
88061f310a fix: switch-account не деплоится как скилл, только как хук
Скилл switch-account загружался в LLM раньше чем срабатывал UserPromptSubmit
хук - из-за этого каждый /switch-account съедал токены. Теперь ai-setup.sh
пропускает "hook-backed skills" при деплое в ~/.claude/skills/, хук перехватывает
команду до LLM и возвращает decision:block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:59:29 +03:00
c6161c3332 feat: переключение аккаунтов через хук без токенов LLM
- UserPromptSubmit хук перехватывает /switch-account до LLM, переключает
  credentials по кругу и возвращает decision:block - нулевой расход токенов
- Статусная строка: effort и имя аккаунта в квадратных скобках [high·work]
- ai-setup.sh деплоит хук switch-account-hook.sh и прописывает его в settings.json
- Скилл switch-account оставлен как fallback-документация для setup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:58:06 +03:00
57d171a592 scripts/fuck-rkn.sh 2026-06-09 23:59:57 +07:00
Виталий Никитенко
aed7468068 fix: ru-bypass больше не раздувает /etc/hosts дублями
- sed удаляет всё от маркера до конца файла, а не только строку с маркером
- заодно чистит старые дубли без маркера (eltex.loc, eltex-co.ru)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 14:36:26 +03:00
Виталий Никитенко
4ae5ca149a feat: автоопределение BIN_DIR — ~/bin если есть в PATH, иначе ~/.local/bin
- BIN_DIR теперь определяется по наличию ~/bin в PATH (для Mint, где нет .bashrc)
- source путей в генерируемых скриптах заменены на self-referential (через BASH_SOURCE)
- agy_bin подменяется post-generation sed при нестандартном BIN_DIR
- add_path_to_rc() формирует PATH динамически с учётом BIN_DIR
- при миграции на ~/bin старые скрипты из ~/.local/bin удаляются

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 13:39:29 +03:00
Виталий Никитенко
cb8c3c9544 fix: автоотключение IPv6 при настройке kill switch
IPv6-трафик обходит UFW (который работает только с IPv4) — kill switch
не защищает от утечек по IPv6.

Изменения:
- ru-bypass.sh: молча отключает IPv6 (sysctl + /etc/sysctl.d) при
  каждом запуске, если есть глобальные IPv6-адреса
- ks-on.sh: тоже отключает IPv6, но без интерактивного вопроса
  (раньше спрашивал «Отключить IPv6? [Y/n]»)
- ai-setup.sh (direct mode): не восстанавливает IPv6, если
  UFW kill switch активен (раньше безусловно включал обратно,
  из-за чего после каждого запуска setup.sh IPv6 снова утекал)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:48:32 +03:00
Виталий Никитенко
d2bbcc7e33 fix: kill switch — UFW before.rules с актуальным DEV, прямые iptables, /etc/hosts для *.eltex.loc и elph
Три корневые проблемы и их исправления:

1. MANAGE_BUILTINS=no в /etc/default/ufw — цепочка ufw-before-output
   не вызывалась из OUTPUT, правила before.rules не применялись.
   → автофикс no→yes + прямые правила iptables (не зависят от UFW).

2. UFW-правила создавались однократно по маркеру — при смене DEV
   (wlp1s0→enp4s0) продолжали ссылаться на старый интерфейс.
   → теперь при каждом запуске удаляются и пересоздаются с актуальным DEV.

3. DNS через VPN для локальных доменов возвращал внешние IP вместо
   внутренних (RFC1918) — трафик уходил в VPN и не достигал серверов.
   → /etc/hosts с фиксированными IP для *.eltex.loc, mattermost, elph.
   → замена dig +short на getent hosts (уважает /etc/hosts).

Добавлены built-in KILL_SWITCH_EXCEPTIONS:
  mattermost.eltex-co.ru elph.eltex-co.ru 10.80.0.15

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 12:44:20 +03:00
Виталий Никитенко
e955c928d3 fix: сохранение конфига в /etc/ru-bypass.conf для systemd/NM dispatcher
ru-bypass.sh теперь сохраняет параметры (GATEWAY, DEV, LOCAL_DNS, AMNEZIA_SERVER,
KILL_SWITCH_EXCEPTIONS) в /etc/ru-bypass.conf при каждом запуске, и читает их
оттуда при старте из systemd/NM dispatcher (без env). ENV-переменные имеют приоритет.

setup.sh: read -e -i для KS_EXCEPTIONS - редактирование значения инлайн вместо
показа текущего значения в скобках.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:14:47 +03:00
Виталий Никитенко
775bca1cee fix: чтение KS_EXCEPTIONS из профиля сети
Ключ при записи KILL_SWITCH_EXCEPTIONS, а при чтении искался KS_EXCEPTIONS —
исключения kill switch не загружались при повторном запуске setup.sh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:32:16 +03:00
Виталий Никитенко
2079768318 fix: system-prompt через файл, кеш rate_limits при старте, SessionStart хук
- Все лаунчеры (ai-claude, ai-deepseek, ai-kimi, ai-openrouter): промпт
  пишется во временный файл через --system-prompt-file вместо аргумента
  командной строки. Решает E2BIG при промптах > 128KB (MAX_ARG_STRLEN)
  из проектов с большими .md файлами.

- statusline: кешируем rate_limits по model_id (раздельные файлы для
  claude/kimi/openrouter). При старте сессии показываем данные из кеша
  + ctx:0%. Убирает пустую статусную строку до первого запроса.

- settings.json: добавляем SessionStart хук при setup, триггерит
  вызов statusLine при открытии сессии.

- ai-claude: --model sonnet зафиксирован, убрали exec для корректной
  работы trap (cleanup временного файла).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:24:03 +03:00
Виталий Никитенко
cf34698116 Revert "fix: статусная строка — DeepSeek per-request стоимость вместо статичного баланса"
This reverts commit 6c7324bfd8.
2026-06-08 07:56:25 +03:00
Виталий Никитенко
6c7324bfd8 fix: статусная строка — DeepSeek per-request стоимость вместо статичного баланса
DeepSeek: накопленная стоимость сессии по DeepSeek-ценам (V4: $0.55/$2.19, V3: $0.27/$1.10)
Anthropic/Kimi/прочие: рейт-лимиты (5h, 7d) без долларов
Все: заполнение контекста (ctx%)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 07:51:52 +03:00
Виталий Никитенко
beca4de9cd refactor: разделил AMNEZIA_SERVER и KILL_SWITCH_EXCEPTIONS
- AMNEZIA_SERVER — только IP/домены серверов Amnezia (для поднятия VPN)
- KILL_SWITCH_EXCEPTIONS — дополнительные исключения (git, etc.)
- Обе переменные поддерживают IP и домены (DNS-резолвинг)
- setup.sh: раздельные промпты в меню

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 07:01:51 +03:00
Виталий Никитенко
398e57c648 fix: kill switch не блокировал не-.ru трафик + Amnezia не могла подключиться
- ru-bypass.sh: добавлен ufw default deny outgoing (раньше нигде не выполнялся)
- ru-bypass.sh: добавлен ufw allow out on amn0 (разрешён трафик через VPN)
- ru-bypass.sh: поддержка AMNEZIA_SERVER — IP добавляется в ipset и маршруты
- ks-on.sh: default deny + allow amn0 при восстановлении kill switch
- setup.sh: меню запрашивает/сохраняет/передаёт AMNEZIA_SERVER
- test_network.sh: DEV читается из конфига вместо жёсткого wl[pi]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 06:51:48 +03:00
5599194d92 fix: geo-тест корректно работает при активном kill switch
Прямой curl до ipinfo.io блокируется UFW (не-.ru трафик via wlp1s0
запрещён kill switch-ем — это штатное поведение). Теперь при недоступности
прямого IP тест проверяет маршрут ya.ru: ожидает DEV_DIRECT, а не amn0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:37:06 +03:00
2805ade773 feat: issue #10 — whiptail TUI меню с fallback на plain-text
setup.sh: если whiptail доступен — показывает интерактивное меню
со стрелками (стандартный пакет newt, обычно уже установлен).
Если whiptail не найден — прежнее plain-text меню без изменений.
case-блок единый для обоих путей.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:20:25 +03:00
2c8ff61968 feat: issue #9 — лог установки в ~/.config/ai-setup/setup.log
- setup.sh: функция _log(), вызов при каждом пункте меню
- scripts/ru-bypass.sh, ks-on.sh, ks-off.sh: пишут события в лог через USER_HOME
- setup.sh item 5: показывает tail -10 лога в «Последние события»
- USER_HOME передаётся через sudo env в ks-on/ks-off/ru-bypass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:19:43 +03:00
feb401f4ba feat: issue #8 — именованные профили сети (home/office/hostname)
setup.sh item 2: показывает существующие профили, предлагает ввести имя.
Профиль сохраняется как network_<name>.conf (вместо фиксированного hostname).
Позволяет держать home/office конфиги на одной машине и переключаться явно.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:18:09 +03:00
df401ca333 feat: issue #7 — латентность до API провайдеров в пункте 5 «Статус»
Добавлен блок «API доступность» с curl time_connect до api.anthropic.com,
api.deepseek.com, api.openai.com, api.kimi.com, openrouter.ai.
Показывает время соединения или «недоступен» при timeout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:17:28 +03:00
71c30b17bc feat: issue #6 — пункт 7 «Обновить» (git pull + перегенерация)
setup.sh: добавлен пункт 7 в меню и case-ветка:
git pull --ff-only + bash scripts/ai-setup.sh для обновления
всех лаунчеров в ~/.local/bin без ручного вмешательства.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:16:57 +03:00
7b0756bf96 fix: issue #5 — 5xx от API не блокирует запуск лаунчеров
_handle_api_response и _handle_openai_api_response теперь имеют явный case
для 5xx: предупреждают что сервер временно недоступен и продолжают (_API_RET=0).
Раньше 5xx попадал в wildcard → _API_RET=$code (non-zero) → exit 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:16:15 +03:00
7c8b3a7147 feat: issue #4 — geo-тест внешних IP в test_network.sh
Добавлен блок «7. Geo: внешние IP» — curl через прямой интерфейс
и через дефолт (VPN). Показывает оба IP и проверяет что они разные.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:14:54 +03:00
3b591766e1 feat: issue #3 — проверка IPv6 leak при включении kill switch
- scripts/ks-on.sh: перед включением UFW проверяет глобальные IPv6 адреса,
  предлагает отключить IPv6 через sysctl если они есть
- setup.sh item 5: показывает статус IPv6 (отключён / активен с предупреждением)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:14:23 +03:00
6f623a5b3e feat: issue #2 — systemd timer для ежесуточного обновления RIPE-списка
ru-bypass.sh теперь при первом запуске устанавливает ru-bypass.timer
(OnCalendar=daily, Persistent=true). Timer запускает ru-bypass.service
раз в сутки и обновляет ipset + маршруты без ручного вмешательства.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:13:47 +03:00
7429802612 feat: issue #1 — DNS для *.loc через LOCAL_DNS + resolvectl
- setup.sh item 2: добавлен вопрос LOCAL_DNS (офисный DNS-сервер для *.loc)
  сохраняется в network_<hostname>.conf, передаётся в ru-bypass.sh через env
- scripts/ru-bypass.sh: добавлена переменная LOCAL_DNS, после RFC1918 маршрутов
  настраивает resolvectl dns/domain ~loc на интерфейсе DEV

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:13:18 +03:00
00722b430f docs: актуализировать README — добавить ru-bypass, структуру scripts/, сетевой раздел
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:04:29 +03:00
b37482312f feat: пункт 6 — проверить сеть (запускает tests/test_network.sh)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:37:57 +03:00
c266b64dab feat: пункт 5 — статус (Amnezia, UFW, сервисы, инструменты, ключи)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:37:13 +03:00
9e74d53025 feat: сохранение GATEWAY/DEV в ~/.config/ai-setup/network_<hostname>.conf
При повторном запуске пункта 2 параметры загружаются из файла — не нужно
вводить снова. Сохраняется отдельно для каждой машины по hostname.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:36:33 +03:00
0ba7416047 feat: автоопределение GATEWAY и DEV из ip route show default
При выборе пункта 2 скрипт сам парсит маршрут по умолчанию и предлагает
найденные значения — пользователь просто жмёт Enter для подтверждения.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:36:03 +03:00
805951d920 feat: мастер-скрипт setup.sh + перенос скриптов в scripts/
- все скрипты перенесены в scripts/ (ai-setup.sh, ru-bypass.sh, ks-off.sh, ks-on.sh)
- setup.sh — новый мастер-скрипт с меню: шаги 1-2 отделены от опций 3-4
- пояснения к GATEWAY и DEV при выборе пункта 2
- ai-setup.sh: SCRIPT_DIR поднят на уровень выше (/../) чтобы находить home-configs/
- tests/test_fixes.sh, test_isolated.sh: пути обновлены на scripts/ai-setup.sh
- QUICK_START.md, README.md, home-configs/network/README.md: пути обновлены

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:47:26 +03:00
3d21f6b620 feat: добавить QUICK_START.md в корень проекта
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:24:56 +03:00
c9a0fca582 fix: убрать абсолютные пути из README network/
Все команды теперь относительные (от корня репозитория).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:32:44 +03:00
66a040cc03 feat: RFC1918 (*.loc) напрямую + динамический пример IP в выводе
- ru-bypass.sh: маршруты для 10/8, 172.16/12, 192.168/16 через локальный роутер
- ru-bypass.sh: UFW правила для тех же диапазонов (однократно, before.rules)
- ru-bypass.sh: пример IP в конце теперь резолвится из ya.ru динамически
- README.md: описание RFC1918 bypass в разделах про UFW и про шаги скрипта

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:03:59 +03:00
9669f5ff15 feat: поддержка GATEWAY/DEV через env + README для network скриптов
- ru-bypass.sh: GATEWAY и DEV теперь переопределяются через env (${VAR:-default})
  Запуск на другой машине: sudo GATEWAY=10.0.0.1 DEV=eth0 bash ru-bypass.sh
- home-configs/network/README.md: объяснение как работает ru-bypass, kill switch,
  как запускать на разных машинах с разными сетевыми интерфейсами

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:56:58 +03:00
5cb56124da fix: rm -f /tmp/ru-routes.batch перед записью (PermissionError при запуске от root)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:44:02 +03:00
fcf810fd75 feat: ru-bypass — .ru трафик напрямую мимо Amnezia, всё остальное через VPN
- ipset ru-direct + маршруты через 192.168.1.1 для всех RU IP-блоков (RIPE)
- kill switch (UFW) остаётся: не-.ru трафик при отвале Amnezia блокируется
- ru-ipset-restore.service стартует до UFW — исправляет проблему перезагрузки
- ru-bypass.service стартует после network-online — обновляет RIPE и маршруты
- NM dispatcher авто-перезапускает при reconnect amn0
- python3 summarize_address_range для корректной обработки невыровненных блоков RIPE
- tests/test_network.sh — автотесты маршрутизации и связности

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:41:16 +03:00
0b0d51b77c feat: обновить цвета статусной строки
- путь и ветка: светло-серый (37), как хинты Claude Code
- модель: приглушённый лососевый (173), фирменный цвет Claude
- лимиты и ctx: единая функция pct_color (зелёный/жёлтый/красный)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:28:49 +03:00
c9db0be030 feat: скорректировать пороги цветов ctx
зелёный <40%, жёлтый 40-60%, красный >=60% (совпадает с порогом звука)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 23:58:12 +03:00
a21a77723c fix: оставить только один звуковой сигнал при ctx=60%
timeout возвращает код 124 при обрезке, что запускало fallback-цепочку.
Заменил || на ; true чтобы подавить код выхода timeout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 15:57:37 +03:00
ac64bb1505 feat: раскрасить ctx по уровням и добавить звуковой сигнал при 60%
- зелёный <30%, жёлтый 30-50%, красный 50%+
- однократный звуковой сигнал (alarm-clock-elapsed.oga, 1s) при первом достижении 60%
- сброс флага алерта когда ctx опускается ниже 50%

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 13:13:34 +03:00
bae481172f fix: добавить exit 0 в конец statusline скрипта
Без него [ -n "$ctx_pct" ] && ... возвращал exit 1 когда ctx пустой,
и Claude Code прятал всю строку статуса.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 13:01:33 +03:00
01b69c341b feat: показывать заполнение контекста в статусной строке для всех моделей
ctx:35% отображается серым в конце строки у всех провайдеров.
Для Kimi (нет rate limits) это единственная полезная метрика.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 12:58:57 +03:00
08be1dfc08 feat: показывать оба лимита в статусной строке (5ч и 7д)
Вместо одного показываются оба: "4ч20м:1% 5д3ч:4%".
Для недельного добавлен формат дней: "5д3ч".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 12:48:26 +03:00
6b80364344 fix: показывать реальное время до сброса лимита в статусной строке
Вместо захардкоженного "5h" теперь считается оставшееся время
из resets_at timestamp: "4ч40м:25%", "58м:80%" и т.д.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 12:47:00 +03:00
fb952ad371 docs: добавить секцию про сетевые скрипты в README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 12:04:43 +03:00
dfaf835cb6 chore: удалить недоработанные скрипты ru-direct
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 11:45:33 +03:00
17c456ed4f feat: добавить скрипты управления сетью и kill switch
- ks-off.sh — временно отключить UFW kill switch перед сёрфингом без VPN
- ks-on.sh — восстановить kill switch (с проверкой что amn0 поднят)
- ru-direct.sh — роутинг всех российских IP напрямую через провайдера, минуя Amnezia
- ru-direct-undo.sh — откат ru-direct.sh, возврат к "всё через VPN"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 11:44:40 +03:00
c1e68571f8 feat: показывать баланс DeepSeek при запуске и в статусной строке Claude Code
- Функция _deepseek_balance в ai-api-helpers: запрос к GET /user/balance
- Вывод баланса при проверке сохранённого и нового ключа в ai-deepseek
- Кеширование баланса в ~/.cache/ai-setup/deepseek_balance для статусной строки
- statusline-command.sh: если модель содержит deepseek — показывать  вместо usage

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:14:57 +03:00
2f48d038bd fix: убрать имя и хост из статусной строки, сократить путь до ~
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:21:18 +03:00
3dfd7ff034 feat: добавить настройку статусной строки Claude Code
Шаг 6.7 - копирует statusline-command.sh и прописывает statusLine
в settings.json. Показывает user@host, путь, git-ветку, модель и usage%.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:17:13 +03:00
25d42e8b50 feat: добавить установку Claude Notifier в ai-setup.sh
Шаг 6.8 - устанавливает claude-notifier через curl, идемпотентно.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:11:18 +03:00
704e30d3be feat: добавить авторегистрацию маркетплейса плагинов Claude Code
Шаг 6.7 в ai-setup.sh - регистрирует claude-plugins-official через
GitHub PAT (из env GITHUB_TOKEN или интерактивно). Идемпотентен.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 09:04:53 +03:00
6120f34199 feat: добавить TZ и документацию для skill'ов el-review
- Установить часовую зону Europe/Helsinki в ai-api-helpers.sh
- Добавить примеры вызова обоих skill'ов
- Документировать обработку ошибок (отсутствие веток, пустой дифф)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-06 08:45:36 +03:00
2f396ac27a docs: актуализация README, +ai-openrouter в AGENTS.md, перегруппировка вывода команд
- README: добавлены ai-openrouter, VLESS/Xray, effort mapping, Gemini skills
- README: команды сгруппированы (Claude Code → нативные)
- README: убрано предупреждение про отдельный Google-аккаунт
- AGENTS.md: ai-openrouter добавлен в список генерируемых скриптов
- ai-setup.sh: финальный вывод перегруппирован, предупреждение Gemini удалено

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 03:26:01 +07:00
Виталий Никитенко
2928298e6b fix(ai-setup): add plugin.json generation for gemini skills 2026-06-05 19:02:56 +07:00
Виталий Никитенко
7a3f14fa48 fix: update OpenRouter API base URL
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:17:47 +07:00
Виталий Никитенко
797e8448af feat: add ai-openrouter script with gpt-5.5, opus 4.8 and sonnet 4.6 models 2026-06-05 18:04:09 +07:00
23 changed files with 4856 additions and 233 deletions

View File

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

View File

@@ -1,90 +1,103 @@
# Effort Mapping Соответствие уровней для всех провайдеров # Effort Mapping - соответствие уровней для Claude Code лаунчеров
## Как это работает ## Как это работает
Когда вы меняете effort в Claude Code (через `/effort` или `--effort`), значение передаётся `/effort` и `AI_EFFORT` относятся к лаунчерам, которые запускают Claude Code:
в API провайдера. Каждый провайдер поддерживает свой набор уровней reasoning effort. `ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-openrouter`.
Если выбранного уровня нет у провайдера — он автоматически маппится на ближайший
**более высокий** нативный уровень. Нативные лаунчеры `ai-gpt` (OpenAI Codex) и `ai-gemini` (Antigravity `agy`) не используют
эту схему. У них свои настройки reasoning внутри соответствующего CLI.
Документ отражает текущую реализацию `scripts/ai-setup.sh`.
## Таблица маппинга ## Таблица маппинга
> Актуально на 30 мая 2026 г. | Claude Code `/effort` | Anthropic (`ai-claude`) | DeepSeek V4 (`ai-deepseek`) | Kimi K2.7/K2.6 (`ai-kimi`) | OpenRouter/GPT-5.5 (`ai-openrouter`) |
|:---:|:---:|:---:|:---:|:---:|
| `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 | без локального маппинга |
| Claude Code<br>`/effort` | Anthropic<br>(Claude) | GPT-5.5<br>(ChatGPT) | DeepSeek V4 | Kimi K2.6<br>(Moonshot) | Gemini 3.x | Для OpenRouter текущий лаунчер не делает локального преобразования effort и отправляет
|:---:|:---:|:---:|:---:|:---:|:---:| значение дальше через Claude Code/OpenRouter. Конкретная интерпретация зависит от выбранной
| `low` | ✅ `low` | ✅ `low` | ⬆ `high` | 🔛 thinking on | ✅ `LOW` | модели и backend-а OpenRouter.
| `medium` | ✅ `medium` | ✅ `medium` | ⬆ `high` | 🔛 thinking on | ✅ `MEDIUM` |
| `high` | ✅ `high` | ✅ `high` | ✅ `high` | 🔛 thinking on | ✅ `HIGH` |
| `xhigh` | ✅ `xhigh` | ✅ `xhigh` | ⬆ `max` | 🔛 thinking on | ⬆ `HIGH` |
| `max` | ✅ `max` | ⬆ `xhigh` ¹ | ✅ `max` | 🔛 thinking on | ⬆ `HIGH` |
**Обозначения:**
- ✅ — нативная поддержка (1:1 соответствие)
- ⬆ — маппинг на ближайший доступный уровень вверх
- 🔛 — бинарный режим (thinking вкл/выкл, без градаций)
¹ GPT-5.5 не имеет уровня `max` — effort-proxy (`claude-gpt-effort-proxy.py`) маппит `max``xhigh`
## Нативные уровни каждого провайдера ## Нативные уровни каждого провайдера
### Anthropic (Claude) — эталон ### Anthropic (Claude)
```
low → medium → high → xhigh → max
```
- 5 уровней, полное соответствие с Claude Code
- По умолчанию: `xhigh`
- `max` — максимальный бюджет на thinking, для самых сложных задач
### GPT-5.5 (через claude-code-proxy) ```text
low -> medium -> high -> xhigh -> max
``` ```
low → medium → high → xhigh
``` - 5 уровней, полное соответствие с Claude Code.
- 4 основных уровня (также существуют `none` и `minimal`) - Дефолт `ai-claude`: `xhigh`.
- Нет `max` маппится в `xhigh` - `max` - максимальный бюджет thinking для самых сложных задач.
- По умолчанию: `medium`
- Маппинг выполняется effort-proxy на стороне лаунчера
### DeepSeek V4 ### DeepSeek V4
```
high → max
```
- Всего 2 реальных уровня
- `low` и `medium` → автоматически поднимаются до `high`
- `xhigh` → автоматически поднимается до `max`
- Маппинг выполняется на стороне DeepSeek API
### Kimi K2.6 (Moonshot AI) ```text
high -> max
``` ```
- 2 реальных уровня.
- `low` и `medium` фактически поднимаются до `high`.
- `xhigh` фактически поднимается до `max`.
- Маппинг выполняется на стороне DeepSeek API.
- Дефолт `ai-deepseek`: `high`.
### Kimi K2.7/K2.6 (Moonshot AI)
```text
on / off on / off
``` ```
- Бинарный режим — thinking либо включён, либо выключен
- Все уровни effort = thinking включён
- Нет градаций глубины reasoning
- Маппинг выполняется на стороне Moonshot API
### Gemini 3.x (через antigravity-claude-proxy) - Бинарный режим: thinking либо включен, либо выключен.
``` - Все уровни Claude Code effort в текущем лаунчере означают thinking on.
MINIMAL → LOW → MEDIUM → HIGH - Градаций глубины reasoning нет.
``` - Дефолт `ai-kimi`: `high`.
- 4 уровня
- Нет `xhigh` и `max`оба маппятся в `HIGH` ### OpenRouter / GPT-5.5
- Pro-модели: по умолчанию `HIGH`
- Flash-модели: по умолчанию `MEDIUM` Текущий `ai-openrouter` работает через Claude Code с `ANTHROPIC_BASE_URL=https://openrouter.ai/api`
- Маппинг выполняется на стороне antigravity-claude-proxy и не вызывает локальный effort proxy. Поэтому `low`/`medium`/`high`/`xhigh`/`max` не
преобразуются в shell-лаунчере.
`claude-gpt-effort-proxy.py` все еще генерируется как совместимый helper для старого
`claude-code-proxy` backend-а. Если использовать его вручную, он маппит только
`xhigh` -> `high`, потому что `claude-code-proxy` принимает `low`, `medium`, `high`, `max`.
## Где выполняется маппинг ## Где выполняется маппинг
| Провайдер | Кто маппит | Файл/компонент | | Провайдер | Кто маппит | Файл/компонент |
|:---|:---|:---| |:---|:---|:---|
| Anthropic | Не нужен | | | Anthropic | Не нужен | - |
| GPT-5.5 | effort-proxy (наш) | `~/.local/bin/claude-gpt-effort-proxy.py` |
| DeepSeek V4 | DeepSeek API | На стороне сервера | | DeepSeek V4 | DeepSeek API | На стороне сервера |
| Kimi K2.6 | Moonshot API | На стороне сервера | | Kimi K2.7/K2.6 | Moonshot API | На стороне сервера |
| Gemini 3.x | antigravity-claude-proxy | npm пакет | | OpenRouter/GPT-5.5 | Локального маппинга нет | `ai-openrouter` отправляет значение как есть |
| 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-openrouter`)
запоминает свой уровень effort отдельно в `settings.json` лаунчера.
`/effort` внутри сессии работает для всех уровней. `CLAUDE_CODE_EFFORT_LEVEL` не используется.
Форсировать уровень при запуске:
```bash
AI_EFFORT=max ai-deepseek # включить max
AI_EFFORT=high ai-deepseek # включить high
ai-deepseek # без флага - использовать уровень из settings.json
```
Дефолты: `xhigh` для `ai-claude`, `high` для остальных Claude Code лаунчеров.
## Рекомендации ## Рекомендации
- **Для повседневной работы:** `high` или `xhigh` — работает одинаково хорошо у всех провайдеров - Для повседневной работы: `high` или `xhigh`.
- **`max` effort:** имеет реальный эффект только у **Anthropic** и **DeepSeek**. Для GPT маппится в `xhigh`, для Gemini и Kimi — в их максимальный уровень - `max` effort имеет реальный эффект у Anthropic и DeepSeek; у Kimi это все тот же thinking on.
- **`low`/`medium`:** у DeepSeek и Kimi фактически не снижают reasoning — DeepSeek поднимет до `high`, Kimi просто включит thinking - `low`/`medium` у DeepSeek и Kimi фактически не снижают reasoning.

60
QUICK_START.md Normal file
View File

@@ -0,0 +1,60 @@
# Quick Start
## 1. Установить AI-инструменты
```bash
bash setup.sh
```
Спросит про VLESS-прокси (по умолчанию `Y`; если не нужен, ответь `n`), затем установит лаунчеры
`ai-claude`, `ai-gpt`, `ai-deepseek`, `ai-kimi`, `ai-openrouter`, `ai-gemini`
в `~/bin`, если он уже есть в `PATH`, иначе в `~/.local/bin/`, и запишет API-ключи.
После установки, если команды не видны:
```bash
exec bash
```
## 2. Настроить сеть (Amnezia + ru-bypass)
Нужно один раз на каждой машине. .ru трафик идёт напрямую,
остальное — через Amnezia. Если Amnezia падает, не-.ru блокируется.
Запускай через `bash setup.sh` и выбери пункт `2) Сеть: ru-bypass + kill switch`.
Меню само предложит `GATEWAY` и `DEV` из `ip route show default`, даст выбрать профиль
(`home`, `office`, имя хоста и т.д.) и сохранит настройки в
`~/.config/ai-setup/network_<profile>.conf`.
Дополнительные поля можно оставить пустыми:
- `LOCAL_DNS` - DNS для `*.loc`
- `AMNEZIA_SERVER` - IP/домен сервера Amnezia, чтобы VPN мог подняться при kill switch
- `KILL_SWITCH_EXCEPTIONS` - IP/домены, которые должны ходить напрямую
После первого запуска systemd-сервисы и timer установлены - при перезагрузке всё поднимается само,
а RIPE-список обновляется ежедневно.
## Проверка сети
```bash
ip route get 8.8.8.8 # -> dev amn0 (через VPN)
ip route get $(dig +short ya.ru A | head -1) # -> dev <DEV> (напрямую)
bash tests/test_network.sh
```
## Временно отключить VPN (нужен российский IP)
```bash
# Выйти из Claude Code, затем:
sudo bash scripts/ks-off.sh
# Отключить Amnezia в GUI
# Вернуться к нормальному режиму:
# Подключить Amnezia в GUI, затем:
sudo bash scripts/ks-on.sh
```
## Подробнее
- `README.md` — полное описание всего проекта
- `home-configs/network/README.md` — детали сетевой настройки
- `EFFORT_MAPPING.md` — таблица effort-уровней по провайдерам

315
README.md
View File

@@ -1,59 +1,60 @@
# AI Setup # AI Setup
Набор shell-лаунчеров для локальной установки и запуска нескольких AI coding CLI из единой точки входа. Набор shell-скриптов для установки AI coding CLI и настройки сети с Amnezia VPN.
Главный скрипт - `ai-setup.sh`. Он настраивает пользовательские директории, устанавливает глобальные правила агентов из `GLOBAL_RULES.md` и полностью перегенерирует standalone-скрипты в `~/.local/bin`. Точка входа - `setup.sh` (мастер-скрипт с меню). Он вызывает нужный скрипт из `scripts/`.
## Что реально устанавливается и генерируется ## Структура репозитория
`ai-setup.sh` работает с такими путями:
- `~/.local/bin` - standalone-лаунчеры и вспомогательные скрипты.
- `~/.config/ai-setup` - сохранённые ключи и глобальные правила.
- `~/.npm-global` - пользовательский npm prefix.
После запуска генерируются или обновляются:
- `~/.local/bin/ai-claude`
- `~/.local/bin/ai-gpt`
- `~/.local/bin/ai-deepseek`
- `~/.local/bin/ai-kimi`
- `~/.local/bin/ai-gemini`
- `~/.local/bin/ai-api-helpers.sh`
- `~/.local/bin/claude-gpt-effort-proxy.py`
Все генерируемые standalone-скрипты полностью перезаписываются текущей эталонной версией из `ai-setup.sh`. Старое содержимое не сливается и не дописывается.
## Структура конфигов
Все конфиги, которые разворачиваются в домашнюю директорию, живут в папке `home-configs/`:
``` ```
setup.sh # мастер-скрипт с меню
scripts/
├── ai-setup.sh # устанавливает AI-инструменты в выбранный BIN_DIR
├── fuck-rkn.sh # серверный установщик nginx + telemt + xray
├── ru-bypass.sh # .ru трафик напрямую, kill switch для остального
├── ks-off.sh # временно отключить kill switch
└── ks-on.sh # восстановить kill switch
home-configs/ home-configs/
├── GLOBAL_RULES.md # глобальные правила для всех агентов ├── GLOBAL_RULES.md # глобальные правила для всех агентов
── claude/ ── claude/
── skills/ # кастомные скиллы для Claude Code ── hooks/ # хуки аккаунтов Claude Code
├── el-review/ │ ├── skills/ # кастомные скиллы для Claude Code (и Gemini)
│ └── SKILL.md └── statusline-command.sh # статусная строка Claude Code
└── el-review-heavy/ ├── network/
└── SKILL.md └── README.md # подробная документация по сетевой настройке
├── vless/
│ └── servers.conf # список VLESS-серверов для прокси
└── proxychains/
└── proxychains-xray.conf # конфиг proxychains (SOCKS5 через xray)
tests/
├── test_fixes.sh # юнит-тесты структуры ai-setup.sh
└── test_network.sh # тесты маршрутизации
test_isolated.sh # проверка автоустановки ai-gpt и ai-kimi
test_sigint.sh # ручной тест поведения SIGINT
``` ```
При запуске `ai-setup.sh`:
- `GLOBAL_RULES.md` копируется в `~/.config/ai-setup/global_rules.md` и рассылается в нативные rule-файлы (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` и т.д.)
- Скиллы из `home-configs/claude/skills/` копируются в `~/.claude/skills/`
Также скрипт при отсутствии скачивает `~/.local/bin/claude-code-proxy`, но текущий `ai-gpt` запускает нативный Codex CLI и не использует старую proxy-логику через `ANTHROPIC_BASE_URL`.
## Установка ## Установка
Запускать от обычного пользователя, не через `sudo`: Запускать от обычного пользователя, не через `sudo`:
```bash ```bash
bash ai-setup.sh bash setup.sh
``` ```
Скрипт прямо запрещает запуск от root. При этом, если Node.js не найден, он может попытаться установить Node.js через `apt-get` или `dnf` и тогда попросит `sudo` уже внутри этого шага. Мастер-скрипт показывает меню:
```
Шаги для новой машины:
1) AI-инструменты
2) Сеть: ru-bypass + kill switch
Дополнительно (по необходимости):
3) Отключить kill switch
4) Включить kill switch
5) Статус
6) Проверить сеть
7) Обновить # git pull + перегенерация AI-лаунчеров
```
После установки, если shell ещё не видит новые команды: После установки, если shell ещё не видит новые команды:
@@ -61,83 +62,211 @@ bash ai-setup.sh
exec bash exec bash
``` ```
## Требования ## Сеть: ru-bypass + kill switch
- `bash` Полная документация: [`home-configs/network/README.md`](home-configs/network/README.md)
- `curl`
- `python3`
- Node.js/npm для npm-глобальных инструментов
Если Node.js отсутствует, скрипт пытается поставить его автоматически для систем с `apt-get` или `dnf`. Для остальных систем Node.js нужно поставить вручную. ### Что это
## Команды - **.ru сайты** (ozon.ru, госуслуги и др.) - идут **напрямую** через провайдера с российским IP
- **\*.loc офисные адреса** - тоже напрямую через локальный роутер
- **Всё остальное** - только через Amnezia VPN
- **Если Amnezia упала** - не-.ru трафик блокируется UFW (kill switch), .ru и *.loc продолжают работать
- `ai-claude` - запускает оригинальный Claude Code через `claude`. ### Как работает
- `ai-gpt` - запускает нативный OpenAI Codex CLI, при отсутствии пытается поставить его через `https://chatgpt.com/codex/install.sh`.
- `ai-deepseek` - запускает Claude Code через DeepSeek Anthropic-compatible API, проверяет и сохраняет DeepSeek API key.
- `ai-kimi` - запускает Claude Code через официальный Kimi Code API (`https://api.kimi.com/coding/`), проверяет и сохраняет Kimi API key.
- `ai-gemini` - запускает нативный Antigravity CLI `agy`, при отсутствии пытается поставить его через `https://antigravity.google/cli/install.sh`.
Для `ai-gemini` скрипт в конце отдельно предупреждает использовать отдельный Google-аккаунт. Amnezia захватывает весь трафик двумя широкими маршрутами (`0.0.0.0/1` и `128.0.0.0/1` через `amn0`).
`ru-bypass.sh` добавляет тысячи более специфичных маршрутов для .ru IP-блоков через локальный роутер.
Ядро Linux выбирает самый специфичный маршрут - .ru идёт напрямую, остальное в amn0.
UFW настроен так:
```
default deny outgoing — запрещено всё по умолчанию
allow out on amn0 — через Amnezia можно всё
before.rules: ipset ru-direct — для .ru IP разрешён прямой выход
before.rules: RFC1918 — 10/8, 172.16/12, 192.168/16 тоже напрямую (*.loc)
```
### Первый запуск
Через меню (`bash setup.sh` → пункт 2) - он автоматически определит GATEWAY и DEV
из `ip route show default`, предложит профиль и сохранит настройки в
`~/.config/ai-setup/network_<profile>.conf` (по умолчанию профиль равен `$(hostname)`).
Или напрямую:
```bash
sudo bash scripts/ru-bypass.sh
# для другой сети:
sudo GATEWAY=10.0.0.1 DEV=eth0 bash scripts/ru-bypass.sh
```
### Что устанавливается при первом запуске
- `ipset` (если не установлен)
- Скрипт копируется в `/usr/local/bin/ru-bypass.sh`
- `ru-ipset-restore.service` - восстанавливает ipset из файла **до старта UFW** при загрузке
- `ru-bypass.service` - обновляет RIPE-список и маршруты после network-online
- `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 IP-блоков из RIPE (кэш 24ч, `/var/cache/ru-delegations.txt`)
- Обновляет ipset `ru-direct` (~11000 записей)
- Добавляет в ipset встроенные и пользовательские kill switch исключения
- Сохраняет ipset в `/etc/ipset.conf` для восстановления после ребута
- Добавляет маршруты через GATEWAY для всех .ru блоков и RFC1918
- Обновляет UFW `before.rules`, добавляет прямые iptables-правила и отключает IPv6 при активном kill switch
### ks-off / ks-on
Для ситуаций когда нужен временный доступ без VPN (сайты, блокирующие нероссийский IP):
```bash
# 1. Выйти из Claude Code
# 2. Отключить kill switch
sudo bash scripts/ks-off.sh
# 3. Отключить Amnezia в GUI
# Возврат:
# 4. Подключить Amnezia в GUI (дождаться amn0)
sudo bash scripts/ks-on.sh
# 5. Войти в Claude Code
```
### Проверка
```bash
ip route get 8.8.8.8 # -> dev amn0 (Google через VPN)
ip route get 77.88.8.8 # -> dev <DEV> (ya.ru напрямую)
ip route get 10.10.0.1 # -> dev <DEV> (*.loc напрямую)
bash tests/test_network.sh # полные тесты
```
## AI инструменты
`scripts/ai-setup.sh` устанавливает и настраивает все AI CLI.
После запуска генерируются или обновляются в выбранном `BIN_DIR`: `~/bin`, если он уже есть
в `PATH`, иначе `~/.local/bin`.
- `ai-claude` - Claude Code (Anthropic API)
- `ai-gpt` - нативный OpenAI Codex CLI
- `ai-deepseek` - Claude Code через DeepSeek API
- `ai-kimi` - Claude Code через официальный Kimi Code API (`api.kimi.com/coding`)
- `ai-openrouter` - Claude Code через OpenRouter (GPT-5.5, Grok, Qwen, MiniMax, Llama и др.)
- `ai-gemini` - нативный Antigravity CLI `agy`
- `ai-api-helpers.sh` - вспомогательные функции для лаунчеров
- `claude-gpt-effort-proxy.py` - совместимый helper для старого GPT/claude-code-proxy backend
Все генерируемые скрипты полностью перезаписываются при каждом запуске `scripts/ai-setup.sh`.
Также устанавливается:
- `~/.config/ai-setup/global_rules.md` и native rule-файлы (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.kimi-code/AGENTS.md`, `~/.gemini/GEMINI.md`)
- Скиллы из `home-configs/claude/skills/` в `~/.claude/skills/` и `~/.gemini/config/plugins/local-setup/skills/`
## Android CLI
`scripts/ai-setup.sh` также устанавливает [Google Android CLI](https://d.android.com/tools/agents) (preview) - официальный терминальный инструмент для Android-разработки, заточенный под AI-агентов.
Что устанавливается:
- `android` - бинарь CLI в `~/.local/bin/` (или `~/bin` через symlink)
- `android-cli` skill в `~/.claude/skills/` для Claude Code
Поддерживаемые платформы:
- Linux: `x86_64`, `arm64`
- macOS: `x86_64`, `arm64`
- Windows: `x86_64` (ограниченная поддержка, `android emulator` отключён в v0.7)
Ключевые команды:
```bash
android --version # версия CLI
android info # информация об SDK и путях
android sdk list # список доступных SDK-пакетов
android sdk install ... # установить пакеты SDK
android create ... # создать новый Android-проект
android emulator start # запустить эмулятор
android run --apks ... # установить и запустить APK
android update # обновить сам Android CLI
```
Интеграция с Claude Code:
```bash
# После установки ai-setup.sh выполняется автоматически:
android init
```
Это разворачивает `android-cli` skill в `~/.claude/skills/`, после чего Claude Code понимает Android CLI и может использовать его для SDK, эмулятора и сборок.
Ограничения:
- Android CLI находится в preview, API/команды могут меняться.
- На Windows в v0.7 отключена команда `android emulator`.
- Если уже установлен Android Studio, Android CLI может использовать тот же SDK или создать новый в `~/.android/sdk`. Проверяй активный путь через `android info`.
- Android CLI - нативный бинарь, который спавнит дочерние процессы (emulator, gradle, adb), поэтому он не оборачивается в `proxychains4`.
## VLESS / Xray (опционально)
При запуске `scripts/ai-setup.sh` спрашивает, нужен ли VLESS-прокси для AI API запросов.
Если выбрать **Y**:
- Читает список серверов из `home-configs/vless/servers.conf`
- Проверяет каждый сервер реальным curl'ом через SOCKS5
- Устанавливает Xray, генерирует конфиг, создаёт systemd сервис
- Все лаунчеры оборачиваются в `proxychains4`
Если выбрать **n** - VLESS отключается, xray останавливается, системный/Firefox прокси
сбрасываются в direct. IPv6 включается обратно только если UFW kill switch не активен.
## Ключи и конфиги ## Ключи и конфиги
- DeepSeek key хранится в `~/.config/ai-setup/deepseek_key` с правами `600`. - `~/.config/ai-setup/deepseek_key` - DeepSeek API key (права 600)
- Kimi key хранится в `~/.config/ai-setup/kimi_key` с правами `600`. - `~/.config/ai-setup/kimi_key` - Kimi API key (права 600)
- Исходник глобальных правил лежит в `home-configs/GLOBAL_RULES.md`. - `~/.config/ai-setup/openrouter_key` - OpenRouter API key (права 600)
- При запуске глобальные правила пишутся в `~/.config/ai-setup/global_rules.md`. - `~/.config/ai-setup/global_rules.md` - глобальные правила агентов
- `~/.config/ai-setup/network_<profile>.conf` - сохранённые сетевые профили (`GATEWAY`, `DEV`, `LOCAL_DNS`, `AMNEZIA_SERVER`, `KILL_SWITCH_EXCEPTIONS`)
При запуске `ai-setup.sh` сразу обновляются native rule-файлы:
- `~/.codex/AGENTS.md`
- `~/.kimi-code/AGENTS.md`
- `~/.claude/CLAUDE.md`
- `~/.gemini/GEMINI.md`
Лаунчеры дополнительно обновляют эти файлы через helper `_build_ai_sys_prompt` при своём запуске.
В native rule-файлы попадают только глобальные правила. Полный prompt с проектными `.md` используется в `ai-claude`, `ai-deepseek` и `ai-gemini`; `ai-gpt` и `ai-kimi` полагаются на native rule-файлы своих CLI.
## Права запуска агентов ## Права запуска агентов
Лаунчеры запускают CLI в максимально свободном режиме: - `ai-gpt` использует `--dangerously-bypass-approvals-and-sandbox`
- `ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-openrouter`, `ai-gemini` используют `--dangerously-skip-permissions`
- `ai-gpt` использует `--dangerously-bypass-approvals-and-sandbox`. Удобно для локального coding workflow, но это не sandbox для недоверенного кода.
- `ai-claude`, `ai-deepseek`, `ai-kimi` и `ai-gemini` используют `--dangerously-skip-permissions`.
Это удобно для локального coding workflow, но это не sandbox для недоверенного кода.
## Правила агентов ## Правила агентов
Действуют правила Карпати как есть: английский блок из `GLOBAL_RULES.md` устанавливается в `~/.config/ai-setup/global_rules.md` без перевода и смысловых правок. В `home-configs/GLOBAL_RULES.md` - правила Карпати (Think Before Coding, Simplicity First,
Surgical Changes, Goal-Driven Execution) плюс пользовательские правила (отвечать по-русски,
не коммитить без команды, не делать git add без команды и др.).
Кратко правила Карпати: `scripts/ai-setup.sh` копирует их в native rule-файлы всех CLI. Лаунчеры обновляют их при каждом запуске.
1. Think Before Coding - не гадать, явно проговаривать допущения, варианты и неясности. ## Effort Mapping
2. Simplicity First - писать минимальный код без speculative features и лишней конфигурируемости.
3. Surgical Changes - трогать только нужное, не рефакторить соседний код, чистить только свои следы.
4. Goal-Driven Execution - формулировать проверяемую цель и доводить работу до верификации.
Пользовательские глобальные правила: `EFFORT_MAPPING.md` - таблица effort-уровней (`low`/`medium`/`high`/`xhigh`/`max`)
для Claude Code лаунчеров (`ai-claude`, `ai-deepseek`, `ai-kimi`, `ai-openrouter`).
Нативные `ai-gpt` и `ai-gemini` живут отдельно.
1. Всегда отвечать по-русски, на "ты", дружелюбно и как живой программист. `claude-gpt-effort-proxy.py` маппит `xhigh` -> `high` для совместимости со старым
2. Не выполнять `git commit` без прямой и однозначной просьбы. GPT/claude-code-proxy backend; текущие лаунчеры его напрямую не вызывают.
3. Не выполнять `git add` без прямой просьбы, чтобы изменения оставались видны через обычный `git diff`.
4. Использовать обычный дефис `-`, не em dash.
5. В начале работы внимательно учитывать все проектные `.md` файлы.
6. При повторяющихся инструкциях предлагать reusable skill, но создавать или менять skill-файлы только после явного согласия.
Правило этого проекта: ## Требования
1. При каждом запуске `ai-setup.sh` все генерируемые standalone-скрипты в `~/.local/bin` должны полностью перезаписываться актуальными версиями. Merge и append старого содержимого запрещены. - `bash`, `curl`, `python3`
- Node.js/npm (для ai-gpt через Codex CLI и ai-gemini через agy)
- `ipset` (устанавливается автоматически при запуске ru-bypass.sh)
- (опционально) `proxychains-ng` - для VLESS-режима
- (опционально) `gsettings` - для автонастройки системного прокси GNOME
## Тесты ## Тесты
В репозитории есть shell-тесты:
```bash ```bash
bash tests/test_fixes.sh bash tests/test_fixes.sh # структура ai-setup.sh и синтаксис bash
bash test_isolated.sh bash test_isolated.sh # автоустановка ai-gpt и ai-kimi через mock curl
bash tests/test_network.sh # маршрутизация (нужен активный ru-bypass)
# test_sigint.sh # ручной диагностический тест SIGINT
``` ```
`tests/test_fixes.sh` проверяет структуру `ai-setup.sh` и синтаксис bash. `test_isolated.sh` проверяет автоустановочные URL для `ai-gpt` и `ai-kimi` через mock `curl`.

View File

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

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env bash
# UserPromptSubmit hook: перехватывает /add-account.
# 1) сохраняет текущий аккаунт по его реальному email (account-email.sh)
# 2) запускает oauth-логин в фоне (открывает браузер)
# 3) после логина фоновый процесс сам определяет email нового аккаунта по токену
# и сохраняет его credentials + делает current
input=$(cat)
prompt=$(echo "$input" | jq -r '.user_prompt // .prompt // empty' 2>/dev/null)
normalized=$(echo "$prompt" | sed 's|^[[:space:]]*/||; s|[[:space:]]*$||')
# Для /add-account ключ передаётся в том же сообщении, поэтому проверяем префикс.
normalized_cmd=$(echo "$normalized" | awk '{print $1}')
[ "$normalized_cmd" != "add-account" ] && exit 0
LAUNCHER="${AI_LAUNCHER:-claude}"
case "$LAUNCHER" in
claude)
CREDS="$HOME/.claude/.credentials.json"
ACCOUNTS_DIR="$HOME/.claude/accounts"
CURRENT_FILE="$ACCOUNTS_DIR/current"
EMAIL_HELPER="$HOME/.claude/hooks/account-email.sh"
mkdir -p "$ACCOUNTS_DIR"
# Сохраняем текущий активный аккаунт под его реальным email (по токену)
if [ -f "$CREDS" ]; then
cur_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null)
if [ -n "$cur_email" ]; then
cp "$CREDS" "$ACCOUNTS_DIR/${cur_email}.credentials.json"
chmod 600 "$ACCOUNTS_DIR/${cur_email}.credentials.json"
echo "$cur_email" > "$CURRENT_FILE"
fi
fi
# Фоновый процесс: логин нового аккаунта + автосохранение после успеха.
# claude auth login ждёт авторизации в браузере и завершается после неё,
# затем определяем email нового аккаунта по токену (через API) и сохраняем.
(
claude auth login --claudeai </dev/null >/tmp/claude-add-account.log 2>&1
new_email=$(bash "$EMAIL_HELPER" "$CREDS" 2>/dev/null)
if [ -n "$new_email" ] && [ -f "$CREDS" ]; then
cp "$CREDS" "$ACCOUNTS_DIR/${new_email}.credentials.json"
chmod 600 "$ACCOUNTS_DIR/${new_email}.credentials.json"
echo "$new_email" > "$CURRENT_FILE"
echo "SAVED: $new_email" >> /tmp/claude-add-account.log
fi
) &
disown
# exit 0: Claude загружает скилл add-account и говорит что делать
exit 0
;;
kimi)
KEYS_DIR="$HOME/.config/ai-setup/kimi_keys"
CURRENT_FILE="$KEYS_DIR/current"
mkdir -p "$KEYS_DIR"
# Убедиться, что текущий ключ сохранён под своим alias.
current=$(cat "$CURRENT_FILE" 2>/dev/null || true)
if [ -n "$current" ] && [ -f "$KEYS_DIR/${current}.key" ]; then
chmod 600 "$KEYS_DIR/${current}.key"
fi
# Ключ передаётся в том же сообщении: /add-account <key>
new_key=$(echo "$prompt" | sed 's|^[[:space:]]*/add-account[[:space:]]*||; s|[[:space:]]*$||')
if [ -z "$new_key" ]; then
echo "" >&2
echo "Укажите Kimi API ключ в том же сообщении: /add-account sk-..." >&2
exit 2
fi
# Проверка ключа.
echo -n "Проверяю Kimi ключ... " >&2
response=$(curl -s -w "\n%{http_code}" --max-time 15 \
"https://api.kimi.com/coding/v1/messages" \
-H "x-api-key: $new_key" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"kimi-k2.7","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' \
2>/dev/null || echo "000")
code=$(echo "$response" | tail -1)
case "$code" in
200|400|429)
echo "OK" >&2
;;
401|403)
echo "" >&2
echo "Ошибка: ключ недействителен (HTTP $code)." >&2
exit 2
;;
000)
echo "" >&2
echo "Не удалось проверить ключ (сеть недоступна?). Попробуйте позже." >&2
exit 2
;;
*)
echo "" >&2
echo "Ошибка при проверке ключа (HTTP $code)." >&2
exit 2
;;
esac
# Генерация следующего свободного alias.
i=1
while [ -f "$KEYS_DIR/account${i}.key" ]; do
i=$((i + 1))
done
alias_name="account${i}"
echo "$new_key" > "$KEYS_DIR/${alias_name}.key"
chmod 600 "$KEYS_DIR/${alias_name}.key"
# Пытаемся получить email/имя аккаунта Kimi для статусной строки.
_kimi_account_info() {
local api_key="$1"
local resp email name
for url in "https://api.kimi.com/coding/v1/account" "https://api.kimi.com/coding/v1/users/me"; do
resp=$(curl -s --max-time 10 "$url" \
-H "x-api-key: $api_key" \
-H "Accept: application/json" \
2>/dev/null || echo "")
[ -z "$resp" ] && continue
email=$(echo "$resp" | jq -r '.email // .data.email // .account.email // empty' 2>/dev/null)
name=$(echo "$resp" | jq -r '.name // .data.name // .account.name // empty' 2>/dev/null)
if [ -n "$email" ]; then echo "$email"; return 0; fi
if [ -n "$name" ]; then echo "$name"; return 0; fi
done
return 1
}
meta=$(_kimi_account_info "$new_key" 2>/dev/null || true)
if [ -n "$meta" ]; then
echo "$meta" > "$KEYS_DIR/${alias_name}.meta"
chmod 600 "$KEYS_DIR/${alias_name}.meta"
fi
echo "$alias_name" > "$CURRENT_FILE"
echo "Новый Kimi ключ сохранён как: $alias_name. ai-kimi перезапустится с ним." >&2
exit 0
;;
*)
exit 0
;;
esac

View File

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

View File

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

View File

@@ -0,0 +1,88 @@
---
name: android-cli
description: Google Android CLI - официальный терминальный инструмент для Android-разработки, SDK, эмуляторов и проектов
---
# Android CLI
## Назначение
Google Android CLI (preview) - официальный инструмент от Google для Android-разработки через терминал. Он создан специально для AI-агентов, CI/CD и автоматизации. Позволяет управлять SDK, создавать проекты, запускать эмуляторы и собирать APK без необходимости вручную разбираться с `sdkmanager`, `adb` и Gradle.
## Установка и обновление
```bash
# Установка (выполняется ai-setup.sh)
curl -fsSL https://dl.google.com/android/cli/latest/linux_x86_64/install.sh | bash
# Обновление
android update
```
## Ключевые команды
| Команда | Назначение |
|---------|------------|
| `android --version` | Версия Android CLI |
| `android info` | Информация об SDK, путях, эмуляторах |
| `android sdk list` | Список доступных SDK-пакетов |
| `android sdk install <package>` | Установить пакет SDK |
| `android create --name="App" --output=./app` | Создать новый Android-проект |
| `android emulator create` | Создать виртуальное устройство |
| `android emulator start` | Запустить эмулятор |
| `android run --apks <path>` | Установить и запустить APK на устройстве/эмуляторе |
| `android init` | Развернуть skills для AI-агентов в `~/.claude/skills/` |
| `android update` | Обновить сам Android CLI |
## Workflow с Claude Code
1. **Проверь окружение**:
```bash
android info
```
2. **Установи нужные SDK-компоненты**:
```bash
android sdk install platforms/android-35 build-tools/35.0.0
```
3. **Создай проект** (если нужен новый):
```bash
android create --name="MyApp" --output=./myapp
```
4. **Собери проект**:
```bash
./gradlew assembleDebug
```
5. **Запусти на эмуляторе или устройстве**:
```bash
android emulator start
android run --apks app/build/outputs/apk/debug/app-debug.apk
```
## Когда использовать Android CLI
- Для управления SDK и установки build-tools/platforms без Android Studio.
- Для создания новых Android-проектов из терминала.
- Для запуска эмулятора и установки APK одной командой.
- Для работы в CI/CD или внутри AI-агентов.
## Когда НЕ нужен Android CLI
- Для визуального редактирования UI/Layout - используй Android Studio.
- Для отладки с breakpoints, Layout Inspector, Compose Preview - используй Android Studio.
- Для сложного рефакторинга больших проектов - используй Claude Code в паре с Android Studio.
## Ограничения
- **Preview-версия**: API и команды могут меняться. Обновляй через `android update`.
- **Windows**: `android emulator` отключён в v0.7.
- **Существующий SDK**: если уже установлен Android Studio или `sdkmanager`, Android CLI может использовать тот же SDK или создать новый в `~/.android/sdk`. Проверяй активный путь через `android info`.
- **Прокси**: Android CLI - нативный бинарь, который спавнит дочерние процессы (emulator, gradle, adb). Не оборачивай его в `proxychains4`, это может сломать child-процессы.
## Полезные ссылки
- Официальная документация: https://d.android.com/tools/agents
- Android CLI skills: https://github.com/android/skills

View File

@@ -31,6 +31,20 @@ description: Use when пользователь запрашивает тяжел
- Для каждой предложи готовый дифф с исправлением - Для каждой предложи готовый дифф с исправлением
- Объясни простым языком - Объясни простым языком
## Примеры вызова
```
/el-review-heavy feature main
/el-review-heavy my-fix develop
/el-review-heavy TASK-123-payment-screen master
```
## Ошибки и крайние случаи
- Если ветка не существует на remote - сообщи пользователю и не продолжай
- Если дифф пустой - сообщи "изменений между ветками нет" и не продолжай
- Если аргументы не указаны - попроси уточнить: `/el-review-heavy <source-ветка> <target-ветка>`
## Формат вывода ## Формат вывода
``` ```

View File

@@ -31,6 +31,20 @@ description: Use when пользователь запрашивает легко
- Для каждой предложи готовый дифф с исправлением - Для каждой предложи готовый дифф с исправлением
- Объясни простым языком - Объясни простым языком
## Примеры вызова
```
/el-review feature main
/el-review my-fix develop
/el-review TASK-123-payment-screen master
```
## Ошибки и крайние случаи
- Если ветка не существует на remote - сообщи пользователю и не продолжай
- Если дифф пустой - сообщи "изменений между ветками нет" и не продолжай
- Если аргументы не указаны - попроси уточнить: `/el-review <source-ветка> <target-ветка>`
## Формат вывода ## Формат вывода
``` ```

View File

@@ -0,0 +1,8 @@
---
name: switch-account
description: Switch to next account or API key (handled by UserPromptSubmit hook, no LLM needed)
---
Переключение аккаунта/ключа уже выполнено хуком до того, как ты это читаешь.
Ответь ровно одним символом: `✓`
Никаких инструментов. Никаких объяснений. Только `✓`.

View File

@@ -0,0 +1,279 @@
#!/bin/bash
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')
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')
week_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
# Цвет effort: насыщенные, глубже чем пастельные (jewel tones)
_effort_color() {
case "$1" in
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
}
# Брендовый цвет имени модели по лаунчеру
_brand_color() {
case "${1:-}" in
deepseek) printf '\033[38;5;69m' ;; # DeepSeek фирменный синий
claude) printf '\033[38;5;173m' ;; # Anthropic оранжевый
kimi) printf '\033[38;5;81m' ;; # Kimi голубой
openrouter) printf '\033[38;5;135m' ;; # OpenRouter фиолетовый
*) printf '\033[38;5;223m' ;; # кремовый (fallback)
esac
}
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" ] && 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")
# Аккаунт Claude.ai актуален только для нативных моделей Claude
if [[ "$model_id" == claude-* ]]; then
ACCOUNTS_DIR="$HOME/.claude/accounts"
account=$(cat "$ACCOUNTS_DIR/current" 2>/dev/null)
current_token=$(jq -r '.claudeAiOauth.accessToken // empty' "$HOME/.claude/.credentials.json" 2>/dev/null)
# current надёжен, только если его файл существует И токен совпадает с активной сессией.
account_token=""
if [ -n "$account" ] && [ -f "$ACCOUNTS_DIR/${account}.credentials.json" ]; then
account_token=$(jq -r '.claudeAiOauth.accessToken // empty' "$ACCOUNTS_DIR/${account}.credentials.json" 2>/dev/null)
fi
# Рассинхрон (пусто или чужой токен) — определяем аккаунт по активному токену.
if [ -z "$account" ] || [ "$account_token" != "$current_token" ]; then
account=""
if [ -n "$current_token" ]; then
# 1) Локальный матч по токену среди сохранённых аккаунтов (мгновенно)
for f in "$ACCOUNTS_DIR"/*.credentials.json; do
[ ! -f "$f" ] && continue
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$f" 2>/dev/null)
if [ "$token" = "$current_token" ]; then
account=$(basename "$f" .credentials.json)
echo "$account" > "$ACCOUNTS_DIR/current"
break
fi
done
# 2) Не нашли локально — резолвим email через account-email.sh (один раз на токен)
if [ -z "$account" ]; then
sentinel="$HOME/.cache/ai-setup/email_fetch_token"
if [ "$(cat "$sentinel" 2>/dev/null)" != "$current_token" ]; then
mkdir -p "$HOME/.cache/ai-setup"
echo "$current_token" > "$sentinel"
email=$(bash "$HOME/.claude/hooks/account-email.sh" "$HOME/.claude/.credentials.json" 2>/dev/null)
if [ -n "$email" ]; then
cp "$HOME/.claude/.credentials.json" "$ACCOUNTS_DIR/${email}.credentials.json"
chmod 600 "$ACCOUNTS_DIR/${email}.credentials.json"
account="$email"
echo "$account" > "$ACCOUNTS_DIR/current"
fi
fi
fi
fi
fi
[ -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" ] && printf " %s[%s]\033[00m" "$brand_color" "$kimi_account"
fi
if [ -n "$effort" ]; then
printf " %s%s " "$brand_color" "$model"
_effort_color "$effort"
else
printf " %s%s\033[00m" "$brand_color" "$model"
fi
fi
# Форматирует оставшееся время до сброса лимита
fmt_remaining() {
local reset_ts="$1"
local now
now=$(date +%s)
local diff=$(( reset_ts - now ))
[ "$diff" -le 0 ] && echo "скоро" && return
local d=$(( diff / 86400 ))
local h=$(( (diff % 86400) / 3600 ))
local m=$(( (diff % 3600) / 60 ))
if [ "$d" -gt 0 ]; then
echo "${d}д${h}ч"
elif [ "$h" -gt 0 ]; then
echo "${h}ч${m}м"
else
echo "${m}м"
fi
}
# Возвращает ANSI-цвет по проценту: зелёный <40%, жёлтый 40-60%, красный >=60%
pct_color() {
local pct="$1"
if [ "$pct" -lt 40 ]; then
printf '\033[38;5;114m' # мягкий зелёный
elif [ "$pct" -lt 60 ]; then
printf '\033[38;5;220m' # золотой (как effort low)
else
printf '\033[38;5;210m' # мягкий красный
fi
}
# --- Баланс DeepSeek ---
# Моментально показываем кэшированный баланс, в фоне обновляем через API.
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" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
fi
# Фоновое обновление баланса (не чаще раза в 30 секунд)
refresh_ts="$HOME/.cache/ai-setup/deepseek_balance_refresh_ts"
now=$(date +%s)
last=$(cat "$refresh_ts" 2>/dev/null || echo 0)
if [ $(( now - last )) -gt 30 ]; then
key_file="$HOME/.config/ai-setup/deepseek_key"
if [ -f "$key_file" ]; then
echo "$now" > "$refresh_ts" 2>/dev/null
(
api_key=$(cat "$key_file")
resp=$(curl -s --max-time 10 "https://api.deepseek.com/user/balance" \
-H "Authorization: Bearer $api_key" \
-H "Accept: application/json" 2>/dev/null)
if [ -n "$resp" ]; then
new_balance=$(echo "$resp" | python3 -c "
import sys, json
d = json.load(sys.stdin)
infos = d.get('balance_infos', [])
symbols = {'USD': '\$', 'CNY': '\u00a5'}
parts = []
for info in infos:
curr = info.get('currency', '')
total = info.get('total_balance', '0')
sym = symbols.get(curr, curr)
parts.append(f'{sym}{total}')
if parts:
print(' '.join(parts))
" 2>/dev/null)
if [ -n "$new_balance" ]; then
echo "$new_balance" > "$cache_file"
fi
fi
) &
fi
fi
elif [ "${AI_LAUNCHER:-}" = "openrouter" ]; then
# --- Баланс OpenRouter ---
# Моментально показываем кэшированный остаток, в фоне обновляем через API.
cache_file="$HOME/.cache/ai-setup/openrouter_balance"
if [ -f "$cache_file" ]; then
balance=$(head -1 "$cache_file")
[ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance"
fi
# Фоновое обновление баланса (не чаще раза в 30 секунд)
refresh_ts="$HOME/.cache/ai-setup/openrouter_balance_refresh_ts"
now=$(date +%s)
last=$(cat "$refresh_ts" 2>/dev/null || echo 0)
if [ $(( now - last )) -gt 30 ]; then
key_file="$HOME/.config/ai-setup/openrouter_key"
if [ -f "$key_file" ]; then
echo "$now" > "$refresh_ts" 2>/dev/null
(
api_key=$(cat "$key_file")
resp=$(curl -s --max-time 10 "https://openrouter.ai/api/v1/credits" \
-H "Authorization: Bearer $api_key" \
-H "Accept: application/json" 2>/dev/null)
if [ -n "$resp" ]; then
new_balance=$(echo "$resp" | python3 -c "
import sys, json
d = json.load(sys.stdin)
data = d.get('data', {})
total = data.get('total_credits', 0) or 0
usage = data.get('total_usage', 0) or 0
remaining = total - usage
print(f'${remaining:.2f}')
" 2>/dev/null)
if [ -n "$new_balance" ]; then
echo "$new_balance" > "$cache_file"
fi
fi
) &
fi
fi
else
# Рейт-лимиты для НЕ-DeepSeek/OpenRouter провайдеров
# Кеш специфичен для провайдера (model_id) И аккаунта (account): лимиты привязаны
# к аккаунту, поэтому при переключении /switch-account проценты не должны смешиваться.
_cache_key=$(echo "${model_id:-unknown}_${account:-}" | sed 's/[^a-zA-Z0-9._-]/_/g')
RATE_CACHE="$HOME/.cache/ai-setup/rate_limits_${_cache_key}.cache"
mkdir -p "$HOME/.cache/ai-setup"
# Если есть свежие данные - сохранить в кеш
if [ -n "$five_pct" ] || [ -n "$week_pct" ]; then
{
echo "FIVE_PCT=${five_pct}"
echo "FIVE_RESET=${five_reset}"
echo "WEEK_PCT=${week_pct}"
echo "WEEK_RESET=${week_reset}"
} > "$RATE_CACHE"
fi
# Если нет данных - читать из кеша (старт сессии до первого запроса)
if [ -z "$five_pct" ] && [ -z "$week_pct" ] && [ -f "$RATE_CACHE" ]; then
# shellcheck source=/dev/null
source "$RATE_CACHE" 2>/dev/null
five_pct="${FIVE_PCT:-}"
five_reset="${FIVE_RESET:-}"
week_pct="${WEEK_PCT:-}"
week_reset="${WEEK_RESET:-}"
fi
if [ -n "$five_pct" ] && [ -n "$five_reset" ]; then
five_int=$(printf '%.0f' "$five_pct")
remaining=$(fmt_remaining "$five_reset")
color=$(pct_color "$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")
printf " %s%s:%s%%\033[00m" "$color" "$remaining" "$week_int"
fi
fi
# ctx:0% при старте новой сессии (нет данных от API)
[ -z "$ctx_pct" ] && ctx_pct="0"
if [ -n "$ctx_pct" ]; then
ctx_int=$(printf '%.0f' "$ctx_pct")
color=$(pct_color "$ctx_int")
printf " %sctx:%s%%\033[00m" "$color" "$ctx_int"
# Звуковой сигнал при первом достижении 60%
alert_file="$HOME/.cache/ai-setup/ctx_alert_state"
if [ "$ctx_int" -ge 60 ]; then
if [ ! -f "$alert_file" ] || [ "$(cat "$alert_file")" != "alerted" ]; then
mkdir -p "$HOME/.cache/ai-setup"
echo "alerted" > "$alert_file"
(timeout 1s paplay /usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga 2>/dev/null; true) &
fi
elif [ "$ctx_int" -lt 50 ]; then
rm -f "$alert_file" 2>/dev/null
fi
fi
exit 0

View File

@@ -0,0 +1,188 @@
# Сетевые скрипты
Скрипты для окружения с **Amnezia VPN** и **UFW kill switch**.
Главная идея: весь не-.ru трафик идёт через Amnezia, .ru сайты (ozon.ru, госуслуги и т.д.) -
напрямую через провайдера с российским IP. Если Amnezia падает - не-.ru трафик
блокируется UFW, .ru продолжает работать.
---
## Скрипты
| Файл | Назначение |
|---|---|
| `ru-bypass.sh` | Основной - настраивает .ru маршруты и ipset |
| `ks-off.sh` | Временно отключить UFW kill switch (доступ без VPN) |
| `ks-on.sh` | Восстановить UFW kill switch |
Сетевые скрипты запускаются от root. Обычно их дергает `setup.sh` через `sudo` и сохраняет
профиль в `~/.config/ai-setup/network_<profile>.conf`; сам `ru-bypass.sh` дополнительно
пишет root-конфиг `/etc/ru-bypass.conf` для systemd и NetworkManager dispatcher.
---
## Как работает ru-bypass
### Принцип
Amnezia поднимает интерфейс `amn0` и захватывает весь трафик двумя широкими маршрутами:
- `0.0.0.0/1 dev amn0`
- `128.0.0.0/1 dev amn0`
`ru-bypass.sh` добавляет тысячи **более специфичных** маршрутов для .ru IP-блоков через
локальный роутер (например `95.173.0.0/16 via <GATEWAY> dev <DEV>`). Ядро Linux
выбирает самый специфичный маршрут - .ru уходит напрямую, всё остальное - в amn0.
### UFW kill switch
```
default deny outgoing — запрещено всё по умолчанию
allow out on amn0 — через Amnezia можно всё
before.rules: ipset ru-direct — для .ru IP разрешён прямой выход на DEV
before.rules: 10/8, 172.16/12,
192.168/16 — RFC1918 (*.loc) разрешён прямой выход на DEV
```
Если Amnezia падает: `amn0` исчезает, не-.ru трафик блокируется UFW. Маршруты .ru
через `DEV` и правило UFW для ipset остаются - .ru работает.
### Что делает скрипт при запуске
1. Скачивает список .ru IP-блоков из RIPE (кэш 24 часа в `/var/cache/ru-delegations.txt`)
2. Создаёт/обновляет ipset `ru-direct` (~11000 записей)
3. Сохраняет ipset в `/etc/ipset.conf`
4. Добавляет маршруты через локальный роутер для всех .ru блоков
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`
9. Если задан `LOCAL_DNS`, настраивает `resolvectl` для `~loc` на интерфейсе `DEV`
10. Обновляет `/etc/ufw/before.rules`, прямые iptables-правила и отключает IPv6 при активном kill switch
11. При первом запуске: устанавливает systemd service/timer и NetworkManager dispatcher
### Сервисы (устанавливаются однократно)
- `ru-ipset-restore.service` - запускается **до UFW**, восстанавливает ipset из файла.
Нужен потому что UFW стартует рано и не знает об ipset `ru-direct`.
- `ru-bypass.service` - запускается после network-online, качает свежий RIPE-список и
добавляет маршруты.
- `ru-bypass.timer` - ежедневно запускает `ru-bypass.service` для обновления RIPE-списка.
- NM dispatcher `/etc/NetworkManager/dispatcher.d/99-ru-bypass` - автоматически перезапускает
скрипт когда amn0 поднимается (Amnezia переподключилась).
---
## Запуск
### Первый запуск / обновление через меню
```bash
bash setup.sh
# выбери пункт 2) Сеть: ru-bypass + kill switch
```
Меню предложит значения из `ip route show default`, даст выбрать профиль (`home`, `office`,
`$(hostname)` и т.д.) и сохранит:
```bash
GATEWAY=...
DEV=...
LOCAL_DNS=...
AMNEZIA_SERVER=...
KILL_SWITCH_EXCEPTIONS=...
```
### Прямой запуск скрипта
```bash
sudo bash scripts/ru-bypass.sh
```
Если env-переменные не переданы, скрипт сначала читает `/etc/ru-bypass.conf`, а потом
использует legacy-дефолты `GATEWAY=192.168.1.1`, `DEV=wlp1s0`.
### Параметры для другой сети
Для другой сети передай параметры через env:
```bash
sudo GATEWAY=10.0.0.1 DEV=enp3s0 LOCAL_DNS=10.0.0.53 bash scripts/ru-bypass.sh
```
Чтобы узнать нужные значения:
```bash
ip route show default
# Пример: default via 10.0.0.1 dev enp3s0 proto dhcp
# ^^^^^^^ ^^^^^^
# GATEWAY DEV
```
### Офисная машина
Если в офисе другой gateway и другой сетевой интерфейс - создай отдельный профиль через
пункт 2 в `setup.sh` или передай значения через env. Amnezia там тоже поднимает `amn0`,
так что остальное работает одинаково.
Пример (офис с проводным интерфейсом):
```bash
sudo GATEWAY=192.168.0.1 DEV=eth0 LOCAL_DNS=192.168.0.53 bash scripts/ru-bypass.sh
```
---
## ks-off.sh и ks-on.sh
Для ситуаций когда нужен временный доступ без VPN (сайты, которые блокируют нероссийские
IP, например для оплаты или личного кабинета).
### ks-off.sh - отключить kill switch
```bash
sudo bash scripts/ks-off.sh
```
Отключает UFW. После этого - отключить Amnezia через GUI. Трафик пойдёт напрямую через
провайдера (российский IP).
**Примечание:** IPv6 остаётся отключённым после `ks-off.sh` (если он был выключен `ks-on.sh`
или `ru-bypass.sh`). Он восстановится автоматически при следующем `ks-on.sh` или перезагрузке.
**Важно:** выйди из Claude Code перед этим - сессия будет идти с другого IP.
### ks-on.sh - восстановить kill switch
```bash
sudo bash scripts/ks-on.sh
```
Сначала подключи Amnezia через GUI (дождись появления amn0), потом запускай. Иначе
скрипт предупредит - без amn0 UFW сразу заблокирует весь интернет.
### Типичный workflow
```
# Нужен ru-сайт без VPN:
1. Выйти из Claude Code
2. sudo bash ks-off.sh
3. Отключить Amnezia в GUI
# Возврат к нормальному режиму:
4. Подключить Amnezia в GUI (подождать amn0)
5. sudo bash ks-on.sh
6. Войти в Claude Code
```
---
## Проверка
```bash
# Эти команды без sudo:
ip route get 8.8.8.8 # -> dev amn0 (Google через VPN)
ip route get 95.173.136.1 # -> dev <DEV> (ozon.ru напрямую)
ip route get $(dig +short api.anthropic.com A | head -1) # -> dev amn0
# Полные тесты:
bash tests/test_network.sh
```

File diff suppressed because it is too large Load Diff

1908
scripts/fuck-rkn.sh Normal file

File diff suppressed because it is too large Load Diff

19
scripts/ks-off.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# ks-off.sh — временно отключить kill switch UFW
# После этого отключи Amnezia через её GUI — трафик пойдёт напрямую через провайдера.
# Обратная команда: sudo bash ks-on.sh
if [ "$(id -u)" != "0" ]; then
echo "Запускай от root: sudo bash $0"
exit 1
fi
echo "Отключаем UFW kill switch..."
ufw disable
echo ""
echo "Готово. UFW выключен."
echo "Теперь отключи Amnezia через её GUI — трафик пойдёт напрямую (российский IP)."
echo ""
echo "Чтобы вернуть kill switch: sudo bash ks-on.sh"
_log_file="${USER_HOME:-$HOME}/.config/ai-setup/setup.log"
printf '%s [ks-off] Kill switch отключён на %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$(hostname)" >> "$_log_file" 2>/dev/null || true

48
scripts/ks-on.sh Normal file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
# ks-on.sh — включить/восстановить kill switch UFW
# Перед этим подключи Amnezia через её GUI, иначе интернет будет полностью заблокирован.
if [ "$(id -u)" != "0" ]; then
echo "Запускай от root: sudo bash $0"
exit 1
fi
echo "Проверяем что Amnezia поднята..."
if ! ip link show amn0 &>/dev/null && ! ip link show amnezia0 &>/dev/null; then
echo ""
echo "ВНИМАНИЕ: интерфейс amn0/amnezia0 не найден!"
echo "Похоже Amnezia не подключена."
echo "Если включить UFW сейчас — интернет полностью заблокируется."
echo ""
read -p "Всё равно включить kill switch? [y/N] " CONFIRM
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
echo "Отменено."
exit 0
fi
fi
# Отключаем IPv6 (утечка мимо UFW kill switch, UFW работает только с IPv4)
ipv6_cnt=$(ip -6 addr show scope global 2>/dev/null | grep -c 'inet6' || true)
if [ "$ipv6_cnt" -gt 0 ]; then
sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null
sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null
cat > /etc/sysctl.d/99-disable-ipv6.conf << 'SYSCTEOF'
# Отключение IPv6 — требуется для защиты kill switch (UFW работает только с IPv4)
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
SYSCTEOF
systemctl restart systemd-resolved 2>/dev/null || true
echo "IPv6 отключён ($ipv6_cnt адресов)."
fi
echo "Включаем UFW kill switch..."
ufw default deny outgoing >/dev/null 2>&1 || true
ufw allow out on amn0 >/dev/null 2>&1 || true
ufw enable
echo ""
echo "Готово. Kill switch активен."
echo ""
ufw status | head -3
_log_file="${USER_HOME:-$HOME}/.config/ai-setup/setup.log"
printf '%s [ks-on] Kill switch включён на %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$(hostname)" >> "$_log_file" 2>/dev/null || true

398
scripts/ru-bypass.sh Normal file
View File

@@ -0,0 +1,398 @@
#!/bin/bash
# ru-bypass.sh — .ru трафик напрямую мимо Amnezia, всё остальное через VPN + kill switch
#
# Принцип: ipset + более специфичные маршруты имеют приоритет над amn0.
# Kill switch (UFW) остаётся активным — не-.ru трафик при отвале Amnezia блокируется.
#
# Первый запуск устанавливает ipset, два systemd сервиса и NetworkManager dispatcher:
# - ru-ipset-restore.service запускается ДО UFW, восстанавливает ipset из файла
# - ru-bypass.service запускается после network-online, обновляет RIPE-список и маршруты
# Каждый запуск обновляет список .ru IP-блоков из RIPE (кэш 24ч).
#
# Использование: sudo bash ru-bypass.sh
# Сохраняем env-переменные до загрузки конфига (env имеет приоритет)
_env_gw="${GATEWAY:-}"
_env_dev="${DEV:-}"
_env_local_dns="${LOCAL_DNS:-}"
_env_amn_srv="${AMNEZIA_SERVER:-}"
_env_ks_exc="${KILL_SWITCH_EXCEPTIONS:-}"
# Загружаем сохранённый конфиг (для запуска из systemd/NM dispatcher без env)
[ -f /etc/ru-bypass.conf ] && . /etc/ru-bypass.conf
# ENV-переменные имеют приоритет над конфигом
[ -n "$_env_gw" ] && GATEWAY="$_env_gw"
[ -n "$_env_dev" ] && DEV="$_env_dev"
[ -n "$_env_local_dns" ] && LOCAL_DNS="$_env_local_dns"
[ -n "$_env_amn_srv" ] && AMNEZIA_SERVER="$_env_amn_srv"
[ -n "$_env_ks_exc" ] && KILL_SWITCH_EXCEPTIONS="$_env_ks_exc"
# Дефолты (если ни конфиг, ни env не задали значение)
GATEWAY="${GATEWAY:-192.168.1.1}"
DEV="${DEV:-wlp1s0}"
LOCAL_DNS="${LOCAL_DNS:-}"
AMNEZIA_SERVER="${AMNEZIA_SERVER:-}"
KILL_SWITCH_EXCEPTIONS="${KILL_SWITCH_EXCEPTIONS:-}"
# Базовые исключения, необходимые для работы корпоративных сервисов
# Добавляются автоматически, даже если не указаны в конфиге
_BUILTIN_EXCEPTIONS="mattermost.eltex-co.ru elph.eltex-co.ru 10.80.0.15"
for _exc in $_BUILTIN_EXCEPTIONS; do
case " $KILL_SWITCH_EXCEPTIONS " in
*" $_exc "*) ;;
*) KILL_SWITCH_EXCEPTIONS="$KILL_SWITCH_EXCEPTIONS $_exc" ;;
esac
done
KILL_SWITCH_EXCEPTIONS="${KILL_SWITCH_EXCEPTIONS# }"
# Сохраняем конфиг для будущих запусков (systemd, NM dispatcher)
cat > /etc/ru-bypass.conf <<_CONF
GATEWAY="$GATEWAY"
DEV="$DEV"
LOCAL_DNS="$LOCAL_DNS"
AMNEZIA_SERVER="$AMNEZIA_SERVER"
KILL_SWITCH_EXCEPTIONS="$KILL_SWITCH_EXCEPTIONS"
_CONF
SETNAME="ru-direct"
CACHE="/var/cache/ru-delegations.txt"
IPSET_SAVE="/etc/ipset.conf"
RIPE_URL="https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest"
SCRIPT_DEST="/usr/local/bin/ru-bypass.sh"
UFW_BEFORE="/etc/ufw/before.rules"
if [ "$(id -u)" != "0" ]; then
echo "Запускай от root: sudo bash $0"
exit 1
fi
# --- Первичная настройка (однократно) ---
if ! command -v ipset >/dev/null 2>&1; then
echo "Устанавливаем ipset..."
apt-get install -y ipset
fi
# Копируем скрипт в /usr/local/bin (нужно для systemd + NM dispatcher)
SELF=$(realpath "$0")
if [ "$SELF" != "$SCRIPT_DEST" ]; then
cp "$SELF" "$SCRIPT_DEST"
chmod +x "$SCRIPT_DEST"
echo "Скрипт скопирован в $SCRIPT_DEST"
fi
# Сервис восстановления ipset ДО старта UFW (однократно)
RESTORE_SVC="/etc/systemd/system/ru-ipset-restore.service"
if [ ! -f "$RESTORE_SVC" ]; then
cat > "$RESTORE_SVC" <<EOF
[Unit]
Description=Restore ru-direct ipset before UFW starts
DefaultDependencies=no
Before=ufw.service network.target
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/sbin/ipset restore -exist -file $IPSET_SAVE
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable ru-ipset-restore.service
echo "Сервис ru-ipset-restore установлен (стартует до UFW)."
fi
# Основной сервис обновления маршрутов (однократно)
BYPASS_SVC="/etc/systemd/system/ru-bypass.service"
if [ ! -f "$BYPASS_SVC" ]; then
cat > "$BYPASS_SVC" <<'EOF'
[Unit]
Description=Route .ru IP blocks directly (bypass Amnezia VPN)
After=network-online.target ru-ipset-restore.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ru-bypass.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable ru-bypass.service
echo "Сервис ru-bypass установлен."
fi
# Timer для ежесуточного обновления (однократно)
BYPASS_TIMER="/etc/systemd/system/ru-bypass.timer"
if [ ! -f "$BYPASS_TIMER" ]; then
cat > "$BYPASS_TIMER" <<'EOF'
[Unit]
Description=Daily update of .ru IP routes (ru-bypass)
[Timer]
OnCalendar=daily
Persistent=true
Unit=ru-bypass.service
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now ru-bypass.timer
echo "Timer ru-bypass.timer установлен (ежесуточное обновление RIPE)."
fi
# NetworkManager dispatcher — авто-перезапуск когда amn0 поднимается (однократно)
NM_DISPATCHER="/etc/NetworkManager/dispatcher.d/99-ru-bypass"
if [ ! -f "$NM_DISPATCHER" ]; then
cat > "$NM_DISPATCHER" <<'EOF'
#!/bin/bash
[ "$1" = "amn0" ] && [ "$2" = "up" ] && exec /usr/local/bin/ru-bypass.sh
EOF
chmod +x "$NM_DISPATCHER"
echo "NetworkManager dispatcher установлен."
fi
# --- Локальные хосты (фиксируем IP для доменов, которые должны резолвиться локально) ---
# Без этого DNS через VPN может вернуть внешний IP вместо внутреннего,
# и трафик пойдёт через VPN вместо прямого соединения.
HOSTS_MARKER="# ru-bypass: local hosts"
# Удаляем старый блок целиком (от маркера до конца файла)
# и заодно чистим дубликаты от старых версий скрипта (без маркера)
sed -i "/$HOSTS_MARKER/,\$ d; /eltex\.loc/d; /eltex-co\.ru/d" /etc/hosts
# Добавляем актуальные
cat >> /etc/hosts <<_HOSTS
$HOSTS_MARKER
# Eltex corporate services (*.eltex.loc, mattermost, elph)
172.16.0.3 eltex.loc
172.16.5.103 intdocs.eltex.loc
172.16.5.251 red.eltex.loc
172.16.1.17 gitlab.eltex.loc
172.16.1.106 pixso.eltex.loc
172.16.1.94 mcpe-builder.eltex.loc
172.16.5.63 proxy.eltex.loc
10.80.0.16 ssw.eltex.loc
172.16.5.78 nexus.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
_HOSTS
echo "Локальные хосты: *.eltex.loc, mattermost, elph → /etc/hosts"
# --- Обновляем RIPE-список (кэш 24ч) ---
if [ ! -f "$CACHE" ] || [ $(( $(date +%s) - $(stat -c %Y "$CACHE" 2>/dev/null || echo 0) )) -gt 86400 ]; then
echo "Обновляем список .ru IP-блоков из RIPE..."
if curl -fsS -o "$CACHE.tmp" "$RIPE_URL"; then
mv "$CACHE.tmp" "$CACHE"
else
echo "Предупреждение: не удалось скачать RIPE-список"
if [ ! -f "$CACHE" ]; then exit 1; fi
echo "Используем старый кэш от $(date -r "$CACHE")"
fi
fi
# --- Создаём/обновляем ipset ---
echo "Обновляем ipset $SETNAME..."
# create -exist: не падает если уже есть (UFW на него ссылается, destroy ломает цепочку)
ipset create "$SETNAME" hash:net -exist
# flush: очищаем записи, но сохраняем сам set (iptables-правило остаётся валидным)
ipset flush "$SETNAME"
python3 -c "
import ipaddress
entries = 0
with open('$CACHE') as f_in:
for line in f_in:
parts = line.strip().split('|')
if len(parts) < 5 or parts[1] != 'RU' or parts[2] != 'ipv4':
continue
ip_str, count = parts[3], int(parts[4])
first = ipaddress.IPv4Address(ip_str)
last = first + count - 1
for net in ipaddress.summarize_address_range(first, last):
print(f'add $SETNAME {net}')
entries += 1
import sys; print(f'# entries: {entries}', file=sys.stderr)
" 2>/tmp/ru-ipset-count.txt | ipset restore -exist -quiet
ENTRIES=$(ipset list "$SETNAME" 2>/dev/null | grep -c '/')
echo "ipset обновлён: $ENTRIES записей"
# --- Исключения для kill switch ---
# AMNEZIA_SERVER — IP/домены серверов Amnezia (нужны для поднятия VPN при активном kill switch)
# KILL_SWITCH_EXCEPTIONS — дополнительные IP/домены, доступные напрямую даже при kill switch
ALL_EXC="${AMNEZIA_SERVER} ${KILL_SWITCH_EXCEPTIONS}"
if [ -n "${ALL_EXC// }" ]; then
for item in $ALL_EXC; do
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
ips="$item"
else
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
echo "Исключение kill switch: $item$ip (ipset)"
done
done
fi
ipset save ru-direct > /etc/ipset.conf
echo "ipset сохранён в /etc/ipset.conf"
# --- Добавляем маршруты ---
echo "Добавляем маршруты..."
rm -f /tmp/ru-routes.batch
python3 -c "
import ipaddress
with open('$CACHE') as f, open('/tmp/ru-routes.batch', 'w') as out:
count = 0
for line in f:
parts = line.strip().split('|')
if len(parts) < 5 or parts[1] != 'RU' or parts[2] != 'ipv4':
continue
ip_str, n = parts[3], int(parts[4])
first = ipaddress.IPv4Address(ip_str)
last = first + n - 1
for net in ipaddress.summarize_address_range(first, last):
out.write(f'route replace {net} via $GATEWAY dev $DEV\n')
count += 1
print(f'Маршрутов: {count}')
"
ip -force -batch /tmp/ru-routes.batch 2>/dev/null
# --- Маршруты для локальных сетей (*.loc, RFC1918) ---
LOCAL_NETS="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16"
echo "Добавляем маршруты для локальных сетей (*.loc / RFC1918)..."
for net in $LOCAL_NETS; do
ip route replace "$net" via "$GATEWAY" dev "$DEV" 2>/dev/null
done
# Маршруты для исключений kill switch
ALL_EXC="${AMNEZIA_SERVER} ${KILL_SWITCH_EXCEPTIONS}"
if [ -n "${ALL_EXC// }" ]; then
for item in $ALL_EXC; do
if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then
ips="$item"
else
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
echo "Маршрут для исключения: $item$ip"
done
done
fi
# --- DNS для *.loc через LOCAL_DNS (если задан) ---
if [ -n "$LOCAL_DNS" ]; then
if command -v resolvectl >/dev/null 2>&1; then
resolvectl dns "$DEV" "$LOCAL_DNS" 2>/dev/null && \
resolvectl domain "$DEV" "~loc" 2>/dev/null && \
echo "DNS для *.loc → $LOCAL_DNS (интерфейс $DEV)"
else
echo "Предупреждение: resolvectl не найден, LOCAL_DNS=$LOCAL_DNS не применён"
fi
fi
# --- Правила в UFW before.rules (обновляются при каждом запуске) ---
# Маркеры используются для идентификации правил; DEV всегда актуальный.
UFW_IPSET_MARKER="ru-bypass: ipset $SETNAME"
UFW_LOCAL_MARKER="ru-bypass: local-nets-bypass"
echo "Обновляем правила UFW before.rules..."
# Удаляем старые правила (если есть) — и в новом, и в старом формате маркеров
sed -i "/# $UFW_IPSET_MARKER/d; /# \.ru bypass (ipset $SETNAME)/d" "$UFW_BEFORE"
sed -i "/-A ufw-before-output -m set --match-set $SETNAME dst/d" "$UFW_BEFORE"
sed -i "/# $UFW_LOCAL_MARKER/d; /# local nets bypass (local-nets-bypass)/d" "$UFW_BEFORE"
sed -i "/-A ufw-before-output -d 10\.0\.0\.0\/8 -o/d" "$UFW_BEFORE"
sed -i "/-A ufw-before-output -d 172\.16\.0\.0\/12 -o/d" "$UFW_BEFORE"
sed -i "/-A ufw-before-output -d 192\.168\.0\.0\/16 -o/d" "$UFW_BEFORE"
# Добавляем правила заново с актуальным DEV
sed -i "/^\\*filter/,/^COMMIT/{s/^COMMIT/# $UFW_IPSET_MARKER\\n-A ufw-before-output -m set --match-set $SETNAME dst -o $DEV -j ACCEPT\\nCOMMIT/}" "$UFW_BEFORE"
sed -i "/^\\*filter/,/^COMMIT/{s/^COMMIT/# $UFW_LOCAL_MARKER\\n-A ufw-before-output -d 10.0.0.0\\/8 -o $DEV -j ACCEPT\\n-A ufw-before-output -d 172.16.0.0\\/12 -o $DEV -j ACCEPT\\n-A ufw-before-output -d 192.168.0.0\\/16 -o $DEV -j ACCEPT\\nCOMMIT/}" "$UFW_BEFORE"
echo "UFW before.rules обновлён (ipset + локальные сети, DEV=$DEV)."
# --- Исправляем MANAGE_BUILTINS (должен быть yes, иначе before.rules не вызывается) ---
if grep -q '^MANAGE_BUILTINS=no' /etc/default/ufw 2>/dev/null; then
sed -i 's/^MANAGE_BUILTINS=no/MANAGE_BUILTINS=yes/' /etc/default/ufw
echo "UFW: MANAGE_BUILTINS исправлен (no → yes)."
fi
# --- Настройка UFW default deny + allow amn0 (однократно) ---
ufw default deny outgoing >/dev/null 2>&1 || true
ufw allow out on amn0 >/dev/null 2>&1 || true
if grep -qE "$UFW_IPSET_MARKER|$UFW_LOCAL_MARKER" "$UFW_BEFORE" 2>/dev/null; then
if ufw status | grep -qE "активен|active"; then
ufw reload
fi
fi
# --- Прямые правила iptables (гарантия работы даже при MANAGE_BUILTINS=no) ---
echo "Добавляем прямые правила iptables..."
# Правило для ipset ru-direct (RU-IP + исключения kill switch)
iptables -C OUTPUT -m set --match-set "$SETNAME" dst -o "$DEV" -j ACCEPT 2>/dev/null || \
iptables -I OUTPUT 1 -m set --match-set "$SETNAME" dst -o "$DEV" -j ACCEPT
# Правила для локальных сетей (RFC1918)
for _net in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16; do
iptables -C OUTPUT -d "$_net" -o "$DEV" -j ACCEPT 2>/dev/null || \
iptables -I OUTPUT 1 -d "$_net" -o "$DEV" -j ACCEPT
done
echo "iptables: прямые правила добавлены."
# --- Отключаем IPv6 (утечка мимо UFW kill switch) ---
# UFW работает только с IPv4 — IPv6-трафик обходит kill switch полностью.
_ipv6_cnt=$(ip -6 addr show scope global 2>/dev/null | grep -c 'inet6' || true)
if [ "$_ipv6_cnt" -gt 0 ]; then
sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null 2>&1
sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1
cat > /etc/sysctl.d/99-disable-ipv6.conf << 'SYSCTEOF'
# Отключение IPv6 — требуется для защиты kill switch (UFW работает только с IPv4)
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
SYSCTEOF
systemctl restart systemd-resolved 2>/dev/null || true
echo "IPv6: отключён ($_ipv6_cnt адресов) для защиты kill switch."
fi
echo ""
echo "Готово."
RU_EXAMPLE=$(dig +short ya.ru A 2>/dev/null | head -1)
echo " ip route get 8.8.8.8 -> dev amn0 (через VPN)"
echo " ip route get ${RU_EXAMPLE:-<ya.ru ip>} -> dev $DEV (напрямую .ru)"
echo " ip route get 10.10.0.1 -> dev $DEV (напрямую *.loc / RFC1918)"
_log_file="${USER_HOME:-$HOME}/.config/ai-setup/setup.log"
printf '%s [ru-bypass] GATEWAY=%s DEV=%s, блоков: %s\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" "$GATEWAY" "$DEV" "$ENTRIES" >> "$_log_file" 2>/dev/null || true

238
setup.sh Executable file
View File

@@ -0,0 +1,238 @@
#!/bin/bash
# Мастер-скрипт. Запускай от обычного пользователя (sudo попросит сам где нужно).
cd "$(dirname "$0")"
BLD='\033[1m'
GRN='\033[0;32m'
YEL='\033[0;33m'
GRY='\033[0;37m'
CLR='\033[0m'
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"; }
if command -v whiptail >/dev/null 2>&1; then
choice=$(whiptail --title "AI Setup" \
--menu "Выбери действие (стрелки + Enter):" 24 70 10 \
"1" "AI-инструменты (установить лаунчеры + ключи)" \
"2" "Сеть: ru-bypass + kill switch" \
"" "─── Дополнительно ───────────────────────────" \
"3" "Отключить kill switch (прямой доступ без VPN)" \
"4" "Включить kill switch (восстановить защиту)" \
"5" "Статус (Amnezia, UFW, AI инструменты, ключи)" \
"6" "Проверить сеть (маршрутизация, geo)" \
"7" "Обновить (git pull + перегенерация скриптов)" \
"8" "Android CLI (установить Google Android CLI)" \
3>&1 1>&2 2>&3) || exit 0
echo ""
else
echo ""
echo -e "${BLD}=== AI Setup ===${CLR}"
echo ""
echo -e "${YEL}Шаги для новой машины (выполнить по порядку):${CLR}"
echo ""
echo -e " ${BLD}1) AI-инструменты${CLR}"
echo -e " ${GRY}Устанавливает ai-claude, ai-gpt, ai-deepseek, ai-gemini и др.${CLR}"
echo -e " ${GRY}Запрашивает API-ключи. Запускать один раз.${CLR}"
echo ""
echo -e " ${BLD}2) Сеть: ru-bypass + kill switch${CLR}"
echo -e " ${GRY}.ru сайты (ozon, госуслуги) — напрямую с российским IP.${CLR}"
echo -e " ${GRY}*.loc офисные адреса — тоже напрямую.${CLR}"
echo -e " ${GRY}Всё остальное — только через Amnezia (kill switch).${CLR}"
echo -e " ${GRY}Запускать один раз на каждой машине.${CLR}"
echo ""
echo -e "${YEL}Дополнительно (по необходимости):${CLR}"
echo ""
echo -e " ${BLD}3) Отключить kill switch${CLR}"
echo -e " ${GRY}Временно — когда нужен прямой доступ без VPN (российский IP).${CLR}"
echo ""
echo -e " ${BLD}4) Включить kill switch${CLR}"
echo -e " ${GRY}Вернуть защиту обратно после отключения.${CLR}"
echo ""
echo -e " ${BLD}5) Статус${CLR}"
echo -e " ${GRY}Amnezia, UFW, сервисы, установленные AI инструменты и ключи.${CLR}"
echo ""
echo -e " ${BLD}6) Проверить сеть${CLR}"
echo -e " ${GRY}Тесты маршрутизации: .ru напрямую, остальное через Amnezia.${CLR}"
echo ""
echo -e " ${BLD}7) Обновить${CLR}"
echo -e " ${GRY}git pull + перегенерация всех скриптов в ~/bin (или ~/.local/bin).${CLR}"
echo ""
echo -e " ${BLD}8) Android CLI${CLR}"
echo -e " ${GRY}Установить Google Android CLI для Android-разработки.${CLR}"
echo ""
echo -n "Выбери [1-8] или Enter для выхода: "
read -r choice
echo ""
fi
[ -n "$choice" ] && _log "setup" "Пункт $choice на $(hostname)"
case "$choice" in
1)
bash scripts/ai-setup.sh
;;
2)
echo -e "${GRY}Нужно указать параметры твоей локальной сети:${CLR}"
echo -e "${GRY} GATEWAY — IP домашнего/офисного роутера (через него пойдёт .ru трафик напрямую)${CLR}"
echo -e "${GRY} DEV — сетевой интерфейс (wifi или провод), через который ты подключён к роутеру${CLR}"
echo -e "${GRY} LOCAL_DNS — IP офисного DNS-сервера для разрешения *.loc доменов (необязательно)${CLR}"
echo ""
mkdir -p "$HOME/.config/ai-setup"
cfg_dir="$HOME/.config/ai-setup"
# Показываем существующие профили
existing=$(ls "$cfg_dir"/network_*.conf 2>/dev/null | sed 's|.*/network_||;s|\.conf||' | tr '\n' ' ')
if [ -n "$existing" ]; then
echo -e "Существующие профили: ${BLD}${existing}${CLR}"
echo -e "${GRY}Введи имя профиля (home/office/$(hostname) и т.д.) или Enter для текущего${CLR}"
read -rp "Профиль [$(hostname)]: " chosen_profile
chosen_profile="${chosen_profile:-$(hostname)}"
else
chosen_profile="$(hostname)"
fi
net_conf="$cfg_dir/network_${chosen_profile}.conf"
auto_gw=$(ip route show default 2>/dev/null | awk '/default/ {print $3; exit}')
auto_dev=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
auto_gw="${auto_gw:-192.168.1.1}"
auto_dev="${auto_dev:-wlp1s0}"
saved_local_dns=""
saved_amn_srv=""
saved_ks_exc=""
if [ -f "$net_conf" ]; then
saved_gw=$(grep '^GATEWAY=' "$net_conf" | cut -d= -f2)
saved_dev=$(grep '^DEV=' "$net_conf" | cut -d= -f2)
saved_local_dns=$(grep '^LOCAL_DNS=' "$net_conf" | cut -d= -f2)
saved_amn_srv=$(grep '^AMNEZIA_SERVER=' "$net_conf" | cut -d= -f2)
saved_ks_exc=$(grep '^KILL_SWITCH_EXCEPTIONS=' "$net_conf" | cut -d= -f2)
auto_gw="${saved_gw:-$auto_gw}"
auto_dev="${saved_dev:-$auto_dev}"
echo -e "Загружены параметры профиля ${BLD}${chosen_profile}${CLR}: GATEWAY=${BLD}${auto_gw}${CLR} DEV=${BLD}${auto_dev}${CLR}"
else
echo -e "Новый профиль ${BLD}${chosen_profile}${CLR}. Определено автоматически: GATEWAY=${BLD}${auto_gw}${CLR} DEV=${BLD}${auto_dev}${CLR}"
fi
echo -e "${GRY}(просто Enter чтобы принять, или введи другое значение)${CLR}"
echo ""
read -rp "GATEWAY (IP роутера) [${auto_gw}]: " gw
read -rp "DEV (интерфейс) [${auto_dev}]: " dev
read -rp "LOCAL_DNS (DNS для *.loc) [${saved_local_dns:-пусто}]: " local_dns
read -rp "AMNEZIA_SERVER (IP/домен сервера Amnezia) [${saved_amn_srv:-пусто}]: " amn_srv
read -e -rp "KILL_SWITCH_EXCEPTIONS (исключения kill switch: IP/домены через пробел): " -i "${saved_ks_exc}" ks_exc
gw="${gw:-$auto_gw}"
dev="${dev:-$auto_dev}"
[ "$local_dns" = "пусто" ] && local_dns=""
local_dns="${local_dns:-$saved_local_dns}"
[ "$amn_srv" = "пусто" ] && amn_srv=""
amn_srv="${amn_srv:-$saved_amn_srv}"
[ "$ks_exc" = "пусто" ] && ks_exc=""
printf 'GATEWAY=%s\nDEV=%s\nLOCAL_DNS=%s\nAMNEZIA_SERVER=%s\nKILL_SWITCH_EXCEPTIONS=%s\n' "$gw" "$dev" "$local_dns" "$amn_srv" "$ks_exc" > "$net_conf"
echo ""
sudo GATEWAY="$gw" DEV="$dev" LOCAL_DNS="$local_dns" AMNEZIA_SERVER="$amn_srv" KILL_SWITCH_EXCEPTIONS="$ks_exc" USER_HOME="$HOME" bash scripts/ru-bypass.sh
;;
3)
echo -e "${YEL}Перед этим выйди из Claude Code — сессия сменит IP.${CLR}"
echo -n "Продолжить? [y/N] "
read -r confirm
[ "$confirm" = "y" ] || [ "$confirm" = "Y" ] || exit 0
sudo USER_HOME="$HOME" bash scripts/ks-off.sh
;;
4)
sudo USER_HOME="$HOME" bash scripts/ks-on.sh
;;
5)
echo -e "${BLD}=== Статус ===${CLR}"
echo ""
echo -e "${BLD}Сеть:${CLR}"
if ip link show amn0 &>/dev/null; then
echo -e " ${GRN}${CLR} Amnezia (amn0) подключена"
else
echo -e " ${YEL}${CLR} Amnezia (amn0) не найдена"
fi
if sudo ufw status 2>/dev/null | grep -qE "активен|active"; then
echo -e " ${GRN}${CLR} UFW kill switch активен"
else
echo -e " ${YEL}${CLR} UFW выключен"
fi
if systemctl is-active --quiet ru-bypass.service 2>/dev/null || systemctl is-enabled --quiet ru-bypass.service 2>/dev/null; then
echo -e " ${GRN}${CLR} ru-bypass.service установлен"
else
echo -e " ${YEL}${CLR} ru-bypass.service не установлен (запусти пункт 2)"
fi
if systemctl is-enabled --quiet ru-ipset-restore.service 2>/dev/null; then
echo -e " ${GRN}${CLR} ru-ipset-restore.service установлен"
else
echo -e " ${YEL}${CLR} ru-ipset-restore.service не установлен (запусти пункт 2)"
fi
ipv6_cnt=$(ip -6 addr show scope global 2>/dev/null | grep -c 'inet6' || true)
if [ "$ipv6_cnt" -eq 0 ]; then
echo -e " ${GRN}${CLR} IPv6 отключён (нет утечки)"
else
echo -e " ${YEL}!${CLR} IPv6 активен ($ipv6_cnt адресов) — возможна утечка, запусти пункт 4"
fi
echo ""
echo -e "${BLD}AI инструменты:${CLR}"
for cmd in ai-claude ai-gpt ai-deepseek ai-kimi ai-openrouter ai-gemini android; do
if command -v "$cmd" &>/dev/null; then
echo -e " ${GRN}${CLR} $cmd"
else
echo -e " ${YEL}${CLR} $cmd"
fi
done
echo ""
echo -e "${BLD}API ключи:${CLR}"
cfg="$HOME/.config/ai-setup"
for f in deepseek_key kimi_key openrouter_key; do
name="${f/_key/}"
if [ -s "$cfg/$f" ]; then
echo -e " ${GRN}${CLR} $name"
else
echo -e " ${YEL}${CLR} $name (не задан)"
fi
done
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" "OpenRouter:openrouter.ai"; do
_name="${entry%%:*}"
_host="${entry##*:}"
_ms=$(curl -s -o /dev/null -w "%{time_connect}" --max-time 5 "https://$_host" 2>/dev/null)
if [ -n "$_ms" ] && [ "$_ms" != "0.000000" ]; then
echo -e " ${GRN}${CLR} $_name: ${_ms}s"
else
echo -e " ${YEL}${CLR} $_name: недоступен"
fi
done
echo ""
echo -e "${BLD}Последние события:${CLR}"
if [ -f "$LOG" ]; then
tail -10 "$LOG" | sed 's/^/ /'
else
echo " (лог пуст)"
fi
;;
6)
bash tests/test_network.sh
;;
7)
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/ai-setup.sh
;;
"")
exit 0
;;
*)
echo "Неверный выбор."
exit 1
;;
esac

View File

@@ -12,12 +12,12 @@ BIN_DIR="$TMPDIR/bin"
mkdir -p "$BIN_DIR" mkdir -p "$BIN_DIR"
# Извлекаем ai-gpt # Извлекаем ai-gpt
awk '/^cat > "\$BIN_DIR\/ai-gpt"/,/^GPTEOF/' ai-setup.sh | \ awk '/^cat > "\$BIN_DIR\/ai-gpt"/,/^GPTEOF/' scripts/ai-setup.sh | \
sed "s|\\\$BIN_DIR|$BIN_DIR|g" | bash sed "s|\\\$BIN_DIR|$BIN_DIR|g" | bash
chmod +x "$BIN_DIR/ai-gpt" chmod +x "$BIN_DIR/ai-gpt"
# Извлекаем ai-kimi # Извлекаем ai-kimi
awk '/^cat > "\$BIN_DIR\/ai-kimi"/,/^KIMIEOF/' ai-setup.sh | \ awk '/^cat > "\$BIN_DIR\/ai-kimi"/,/^KIMIEOF/' scripts/ai-setup.sh | \
sed "s|\\\$BIN_DIR|$BIN_DIR|g" | bash sed "s|\\\$BIN_DIR|$BIN_DIR|g" | bash
chmod +x "$BIN_DIR/ai-kimi" chmod +x "$BIN_DIR/ai-kimi"

View File

@@ -4,7 +4,7 @@
set -euo pipefail set -euo pipefail
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/ai-setup.sh" SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/scripts/ai-setup.sh"
GLOBAL_RULES_SOURCE="$(cd "$(dirname "$0")/.." && pwd)/home-configs/GLOBAL_RULES.md" GLOBAL_RULES_SOURCE="$(cd "$(dirname "$0")/.." && pwd)/home-configs/GLOBAL_RULES.md"
PASS=0; FAIL=0 PASS=0; FAIL=0
@@ -48,11 +48,12 @@ test_kimi_claude_launcher() {
# ── ai-kimi: uses official Kimi API ────────────────────────────────────── # ── ai-kimi: uses official Kimi API ──────────────────────────────────────
test_kimi_official_api() { test_kimi_official_api() {
if echo "$KIMI_SECTION" | grep -q 'api.kimi.com/coding' \ if echo "$KIMI_SECTION" | grep -q 'api.kimi.com/coding' \
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_MODEL=kimi-k2.6' \ && echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.7' \
&& echo "$KIMI_SECTION" | grep -q 'ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6' \
&& ! echo "$KIMI_SECTION" | grep -q 'artemox'; then && ! echo "$KIMI_SECTION" | grep -q 'artemox'; then
ok "ai-kimi: uses official Kimi API and model" ok "ai-kimi: uses official Kimi API (K2.7 opus/sonnet, K2.6 haiku)"
else else
fail "ai-kimi: must use official Kimi API (api.kimi.com/coding) and model kimi-k2.6" fail "ai-kimi: must use official Kimi API (api.kimi.com/coding) with K2.7 opus/sonnet, K2.6 haiku"
fi fi
} }
@@ -126,6 +127,48 @@ test_script_syntax() {
fi fi
} }
# ── Android CLI: install block exists ─────────────────────────────────────
test_android_cli_install_url() {
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"
fi
}
test_android_init_called() {
if grep -q 'android init' "$SCRIPT"; then
ok "android-cli: android init is invoked after install"
else
fail "android-cli: missing android init invocation"
fi
}
test_android_symlink_to_bin_dir() {
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"
fi
}
test_no_proxychains_for_android() {
if grep -q 'proxychains4.*android' "$SCRIPT"; then
fail "android-cli: should not be wrapped in proxychains4 (breaks child processes)"
else
ok "android-cli: not wrapped in proxychains4"
fi
}
test_android_skill_exists() {
skill_file="$(cd "$(dirname "$0")/.." && pwd)/home-configs/claude/skills/android-cli/SKILL.md"
if [ -f "$skill_file" ]; then
ok "android-cli: custom skill file exists"
else
fail "android-cli: custom skill file is missing"
fi
}
# ── run all tests ───────────────────────────────────────────────────────────── # ── run all tests ─────────────────────────────────────────────────────────────
test_script_syntax test_script_syntax
test_gpt_autoinstall test_gpt_autoinstall
@@ -137,6 +180,11 @@ test_gemini_native_launcher
test_global_rules_include_quality_guidelines test_global_rules_include_quality_guidelines
test_native_rule_files_generated test_native_rule_files_generated
test_fix7_trap_tmp test_fix7_trap_tmp
test_android_cli_install_url
test_android_init_called
test_android_symlink_to_bin_dir
test_no_proxychains_for_android
test_android_skill_exists
echo "" echo ""
echo "Results: $PASS passed, $FAIL failed" echo "Results: $PASS passed, $FAIL failed"

118
tests/test_network.sh Normal file
View File

@@ -0,0 +1,118 @@
#!/bin/bash
# Тесты сетевой настройки: Amnezia + ru-bypass + kill switch
# Запускать без sudo (проверяет что доступно обычному пользователю)
RED='\033[0;31m'
GRN='\033[0;32m'
YEL='\033[0;33m'
CLR='\033[0m'
pass=0 fail=0
check() {
local desc="$1" expected="$2"
local actual
actual=$(eval "$3" 2>&1)
if echo "$actual" | grep -qE "$expected"; then
echo -e "${GRN}${CLR} $desc"
pass=$((pass+1))
else
echo -e "${RED}${CLR} $desc"
echo " ожидалось: $expected"
echo " получено: $(echo "$actual" | head -3)"
fail=$((fail+1))
fi
}
echo "=== 1. Проверка окружения ==="
check "Amnezia интерфейс (amn0) существует" "amn0" "ip link show amn0 2>/dev/null"
# Определяем DEV из конфига или из default route
if [ -f "$HOME/.config/ai-setup/network_$(hostname).conf" ]; then
source "$HOME/.config/ai-setup/network_$(hostname).conf"
fi
DEV="${DEV:-$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')}"
echo " DEV=$DEV (из конфига)"
IPSET_INFO=$(sudo ipset list ru-direct 2>/dev/null)
if [ -n "$IPSET_INFO" ]; then
echo -e "${GRN}${CLR} ipset ru-direct существует"
IPSET_COUNT=$(echo "$IPSET_INFO" | grep -c '/')
if [ "$IPSET_COUNT" -gt 100 ]; then
echo -e "${GRN}${CLR} ipset не пуст ($IPSET_COUNT блоков)"
else
echo -e "${RED}${CLR} ipset слишком мал ($IPSET_COUNT блоков)"
fi
RU_IP=$(echo "$IPSET_INFO" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)
else
echo -e "${YEL}?${CLR} ipset — проверь с sudo"
RU_IP=$(dig +short ya.ru A | head -1)
fi
echo ""
echo "=== 2. Маршрутизация .ru vs не-.ru ==="
check ".ru IP ($RU_IP) → НЕ через amn0" "$DEV" "ip route get $RU_IP 2>/dev/null"
check "8.8.8.8 → через amn0" "amn0" "ip route get 8.8.8.8 2>/dev/null"
check "1.1.1.1 → через amn0" "amn0" "ip route get 1.1.1.1 2>/dev/null"
echo ""
echo "=== 3. DNS резолвинг ==="
check "ozon.ru резолвится" "185\.73\." "dig +short ozon.ru A 2>/dev/null"
check "google.com резолвится" "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" "dig +short google.com A 2>/dev/null | head -1"
check "gosuslugi.ru резолвится" "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" "dig +short gosuslugi.ru A 2>/dev/null | head -1"
echo ""
echo "=== 4. Связность (Amnezia вкл) ==="
check "google.com отвечает (VPN)" "HTTP" "curl -sI --max-time 5 https://google.com 2>&1 | head -1"
check "ozon.ru отвечает (прямо)" "HTTP" "curl -sI --max-time 5 https://ozon.ru 2>&1 | head -1"
check "gosuslugi.ru отвечает (прямо)" "HTTP" "curl -sI --max-time 5 https://gosuslugi.ru 2>&1 | head -1"
echo ""
echo "=== 5. Инфраструктура (нужен sudo) ==="
UFW_STATUS=$(sudo ufw status 2>/dev/null)
if echo "$UFW_STATUS" | grep -qE "активен|active"; then
echo -e "${GRN}${CLR} UFW активен"
else
echo -e "${YEL}?${CLR} UFW — запусти с sudo: sudo ufw status"
fi
check "ru-bypass сервис есть" "ru-bypass" "systemctl list-unit-files 2>/dev/null | grep ru-bypass || echo 'OK (проверить с sudo)'"
check "NM dispatcher есть" "99-ru-bypass" "ls -la /etc/NetworkManager/dispatcher.d/99-ru-bypass 2>/dev/null"
echo ""
echo "=== 6. Краевые случаи ==="
check "api.anthropic.com → amn0" "amn0" "ip route get $(dig +short api.anthropic.com A | head -1) 2>/dev/null"
check "ya.ru → НЕ через amn0 (прямо)" "$DEV" "ip route get $(dig +short ya.ru A | head -1) 2>/dev/null"
echo ""
echo "=== 7. Geo: внешние IP ==="
DEV_DIRECT=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
ip_vpn=$(curl -s --max-time 10 https://ipinfo.io/ip 2>/dev/null)
ip_direct=$(curl -s --interface "$DEV_DIRECT" --max-time 5 https://ipinfo.io/ip 2>/dev/null)
echo " VPN IP (через amn0): ${ip_vpn:-недоступно}"
if [ -n "$ip_direct" ]; then
echo " Прямой IP (через ${DEV_DIRECT:-?}): $ip_direct"
if [ "$ip_direct" != "$ip_vpn" ]; then
echo -e " ${GRN}${CLR} IP разные — .ru идёт напрямую, остальное через VPN"
pass=$((pass+1))
else
echo -e " ${YEL}!${CLR} IP одинаковые — проверь маршрутизацию"
fail=$((fail+1))
fi
else
# Kill switch блокирует прямой доступ к ipinfo.io (не-.ru IP) — это штатное поведение.
# Вместо этого проверяем что ya.ru маршрутизируется через прямой интерфейс.
ya_ip=$(dig +short ya.ru A 2>/dev/null | head -1)
ya_dev=$(ip route get "$ya_ip" 2>/dev/null | awk '/dev/ {for(i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}')
echo " Прямой IP: недоступен (UFW kill switch блокирует не-.ru трафик через ${DEV_DIRECT:-?})"
if [ "$ya_dev" = "$DEV_DIRECT" ]; then
echo -e " ${GRN}${CLR} .ru (ya.ru $ya_ip) → $ya_dev (напрямую, не через VPN)"
pass=$((pass+1))
else
echo -e " ${YEL}!${CLR} .ru (ya.ru $ya_ip) → ${ya_dev:-?} (ожидался $DEV_DIRECT)"
fail=$((fail+1))
fi
fi
echo ""
echo "========================================="
echo -e "Итого: ${GRN}$pass пройдено${CLR}, ${RED}$fail провалено${CLR}"
[ "$fail" -eq 0 ] && echo -e "${GRN}ВСЁ ОК${CLR}" || echo -e "${RED}ЕСТЬ ПРОБЛЕМЫ${CLR}"