#!/usr/bin/env bash # ============================================================ # Claude Code Setup - GPT-5.5 / DeepSeek / Kimi / Gemini # Запуск: bash ai-setup.sh # ============================================================ CONFIG_DIR="$HOME/.config/ai-setup" BIN_DIR="$HOME/.local/bin" NPM_GLOBAL="$HOME/.npm-global" PROXY_BIN="$BIN_DIR/claude-code-proxy" RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' info() { echo -e "${CYAN}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } err() { echo -e "${RED}[ERR]${NC} $*"; exit 1; } # Запрет запуска от root if [ "$EUID" -eq 0 ]; then echo -e "${RED}Не запускайте этот скрипт через sudo!${NC}" echo "Запустите просто: bash ai-setup.sh" exit 1 fi info "Проверяю зависимости (python3)..." if ! command -v python3 &>/dev/null; then err "Python 3 не установлен, но он требуется для работы скрипта." fi success "Python 3 найден" # ── 1. npm prefix в домашнюю папку ────────────────────────── info "Настраиваю npm prefix..." mkdir -p "$NPM_GLOBAL" npm config set prefix "$NPM_GLOBAL" success "npm prefix -> $NPM_GLOBAL" export PATH="$NPM_GLOBAL/bin:$BIN_DIR:$PATH" # ── 2. Node.js ─────────────────────────────────────────────── info "Проверяю Node.js..." if ! command -v node &>/dev/null; then info "Попытка установки Node.js (нужен sudo)..." if command -v apt-get &>/dev/null; then curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs elif command -v dnf &>/dev/null; then sudo dnf install -y nodejs else warn "Не удалось определить пакетный менеджер. Установите Node.js вручную." fi fi if command -v node &>/dev/null; then success "Node.js $(node --version)" else warn "Node.js не найден. Некоторые функции могут не работать." fi # ── 3. Claude Code ─────────────────────────────────────────── info "Проверяю Claude Code..." if ! command -v claude &>/dev/null; then info "Устанавливаю Claude Code..." if curl -fsSL https://claude.ai/install.sh | bash 2>/dev/null; then success "Claude Code установлен (официальный инсталлер)" else npm install -g @anthropic-ai/claude-code success "Claude Code установлен (npm)" fi else success "Claude Code уже установлен: $(claude --version 2>/dev/null | head -1)" fi # ── 4. claude-code-proxy (GPT) ─────────────────────────────── mkdir -p "$BIN_DIR" install_proxy() { info "Устанавливаю claude-code-proxy..." ARCH=$(uname -m) case "$ARCH" in x86_64) ARCH_TAG="amd64" ;; aarch64) ARCH_TAG="arm64" ;; *) err "Неизвестная архитектура: $ARCH" ;; esac LATEST=$(curl -fsSL "https://api.github.com/repos/raine/claude-code-proxy/releases/latest" \ | grep '"tag_name"' | sed 's/.*"tag_name": *"\(.*\)".*/\1/') [ -z "$LATEST" ] && err "Не удалось получить версию claude-code-proxy с GitHub" TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT URL="https://github.com/raine/claude-code-proxy/releases/download/${LATEST}/claude-code-proxy-linux-${ARCH_TAG}.tar.gz" info "Скачиваю $URL" curl -fsSL "$URL" -o "$TMP/proxy.tar.gz" || err "Не удалось скачать claude-code-proxy" tar -xzf "$TMP/proxy.tar.gz" -C "$TMP" BINARY=$(find "$TMP" -name "claude-code-proxy" -type f | head -1) [ -z "$BINARY" ] && err "Бинарник не найден в архиве" cp "$BINARY" "$PROXY_BIN" chmod +x "$PROXY_BIN" success "claude-code-proxy $LATEST -> $PROXY_BIN" } if [ -f "$PROXY_BIN" ]; then CURRENT_VER=$("$PROXY_BIN" --version 2>/dev/null | head -1 || echo "unknown") success "claude-code-proxy уже установлен ($CURRENT_VER)" else install_proxy fi # ── 4b. effort-proxy wrapper (маппинг effort: xhigh→high) ───────── EFFORT_PROXY_BIN="$BIN_DIR/claude-gpt-effort-proxy.py" cat > "$EFFORT_PROXY_BIN" << 'PYEOF' #!/usr/bin/env python3 """Effort mapping proxy for GPT backend. claude-code-proxy now accepts: low, medium, high, max (no "xhigh"). Claude Code may send "xhigh" effort - we map it to "high". """ import http.client, http.server, sys, logging, socketserver UPSTREAM_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 18766 LISTEN_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 18765 logging.basicConfig( filename="/tmp/claude-gpt-effort-proxy.log", level=logging.INFO, format='{"t":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s"}', datefmt='%Y-%m-%dT%H:%M:%S', ) log = logging.getLogger("effort-proxy") # Потокобезопасный сервер - обрабатывает несколько запросов одновременно class _ThreadedServer(socketserver.ThreadingMixIn, http.server.HTTPServer): daemon_threads = True class _Proxy(http.server.BaseHTTPRequestHandler): def proxy_request(self): body = b"" if cl := self.headers.get("Content-Length"): body = self.rfile.read(int(cl)) # Маппинг xhigh→high: claude-code-proxy больше не принимает xhigh body = body.replace(b'"xhigh"', b'"high"') try: conn = http.client.HTTPConnection("127.0.0.1", UPSTREAM_PORT, timeout=300) hdrs = dict(self.headers) hdrs.pop("Host", None) hdrs["Content-Length"] = str(len(body)) conn.request(self.command, self.path, body=body or None, headers=hdrs) resp = conn.getresponse() self.send_response(resp.status, resp.reason) chunked = False for k, v in resp.getheaders(): if k.lower() == "transfer-encoding" and "chunked" in v.lower(): chunked = True self.send_header(k, v) self.end_headers() while chunk := resp.read(4096): try: if chunked: self.wfile.write(f"{len(chunk):X}\r\n".encode() + chunk + b"\r\n") else: self.wfile.write(chunk) self.wfile.flush() except (BrokenPipeError, ConnectionResetError): break if chunked: try: self.wfile.write(b"0\r\n\r\n") except: pass conn.close() except Exception as e: log.error("proxy error: %s", e) try: self.send_error(502, str(e)) except: pass do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = proxy_request def log_message(self, *args): pass log.info("effort-proxy starting on 127.0.0.1:%d → upstream %d", LISTEN_PORT, UPSTREAM_PORT) _ThreadedServer(("127.0.0.1", LISTEN_PORT), _Proxy).serve_forever() PYEOF chmod +x "$EFFORT_PROXY_BIN" success "claude-gpt-effort-proxy -> $EFFORT_PROXY_BIN" # ── 4c. antigravity CLI (Gemini) ────────────────────────── info "Проверяю antigravity CLI (agy)..." if ! command -v agy &>/dev/null; then info "Устанавливаю agy..." curl -fsSL https://antigravity.google/cli/install.sh | bash success "agy установлен" else success "agy уже установлен: $(agy --version 2>/dev/null | head -1)" fi # ── 6. Папка для конфигов ──────────────────────────────────── mkdir -p "$CONFIG_DIR" # ── 6.5. Генерация глобальных правил агентов ───────────────── info "Обновляю глобальные правила агентов..." cat > "$CONFIG_DIR/global_rules.md" << 'RULESEOF' # CLAUDE.md Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. ## 1. Think Before Coding **Don't assume. Don't hide confusion. Surface tradeoffs.** Before implementing: - State your assumptions explicitly. If uncertain, ask. - If multiple interpretations exist, present them - don't pick silently. - If a simpler approach exists, say so. Push back when warranted. - If something is unclear, stop. Name what's confusing. Ask. ## 2. Simplicity First **Minimum code that solves the problem. Nothing speculative.** - No features beyond what was asked. - No abstractions for single-use code. - No "flexibility" or "configurability" that wasn't requested. - No error handling for impossible scenarios. - If you write 200 lines and it could be 50, rewrite it. Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. ## 3. Surgical Changes **Touch only what you must. Clean up only your own mess.** When editing existing code: - Don't "improve" adjacent code, comments, or formatting. - Don't refactor things that aren't broken. - Match existing style, even if you'd do it differently. - If you notice unrelated dead code, mention it - don't delete it. When your changes create orphans: - Remove imports/variables/functions that YOUR changes made unused. - Don't remove pre-existing dead code unless asked. The test: Every changed line should trace directly to the user's request. ## 4. Goal-Driven Execution **Define success criteria. Loop until verified.** Transform tasks into verifiable goals: - "Add validation" → "Write tests for invalid inputs, then make them pass" - "Fix the bug" → "Write a test that reproduces it, then make it pass" - "Refactor X" → "Ensure tests pass before and after" For multi-step tasks, state a brief plan: ``` 1. [Step] → verify: [check] 2. [Step] → verify: [check] 3. [Step] → verify: [check] ``` Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. --- **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. # Global Rules for All AI Agents The rules below are mandatory for every interaction and task. They are intentionally placed after the general coding guidelines above, but they have higher priority when a conflict exists. 1. **Communication style:** Always reply in Russian, in a friendly peer-to-peer tone, using informal "ты". Appropriate swearing, humor, sarcasm, and irony are allowed and welcome. Communicate like a live programmer teammate, not like a dry robot. 2. **No commits without an explicit request:** Never run `git commit` unless the user has directly and unambiguously asked for it. The final commit always remains with the user, or is made strictly by the user's command. 3. **Plain git diff visibility:** All changes must remain visible to the user through the standard `git diff` command. Leave modified files in the working directory unstaged. Do not add files to the Git index with `git add` unless the user explicitly asks for staging, committing, or another action that requires staging, because staging hides changes from plain `git diff`. 4. **Typography:** Always use the regular hyphen-minus (`-`) instead of long dash characters. 5. **Project context:** At the start of work, pay close attention to all provided project `.md` files, because they are provided automatically and contain the current repository's context and specifics. RULESEOF success "Глобальные правила обновлены: $CONFIG_DIR/global_rules.md" info "Обновляю native rule-файлы агентов..." mkdir -p "$HOME/.codex" "$HOME/.kimi-code" "$HOME/.claude" "$HOME/.gemini" cp "$CONFIG_DIR/global_rules.md" "$HOME/.codex/AGENTS.md" cp "$CONFIG_DIR/global_rules.md" "$HOME/.kimi-code/AGENTS.md" cp "$CONFIG_DIR/global_rules.md" "$HOME/.claude/CLAUDE.md" cp "$CONFIG_DIR/global_rules.md" "$HOME/.gemini/GEMINI.md" success "Native rule-файлы обновлены" # ── 7. Очистка старых функций из .bashrc / .zshrc ─────────── clean_rc() { local rc_file="$1" if [ -f "$rc_file" ] && grep -q "# === CLAUDE LAUNCHER ===" "$rc_file"; then info "Очищаю старые функции из $rc_file..." python3 - "$rc_file" <<'PYEOF' import sys path = sys.argv[1] with open(path, 'r') as f: content = f.read() marker = '# === CLAUDE LAUNCHER ===' end_marker = '# === END CLAUDE LAUNCHER ===' start = content.find(marker) end = content.find(end_marker) if start != -1 and end != -1: new_content = content[:start] + content[end + len(end_marker):].lstrip('\n') with open(path, 'w') as f: f.write(new_content) PYEOF success "Старые функции удалены из $rc_file" fi } clean_rc "$HOME/.bashrc" clean_rc "$HOME/.zshrc" add_path_to_rc() { local rc_file="$1" if [ -f "$rc_file" ]; then if ! grep -q 'NPM_GLOBAL' "$rc_file" 2>/dev/null; then cat >> "$rc_file" << 'PATHEOF' # Claude Code Launcher PATH export NPM_GLOBAL="$HOME/.npm-global" export PATH="$NPM_GLOBAL/bin:$HOME/.local/bin:$PATH" PATHEOF success "PATH добавлен в $rc_file" fi fi } add_path_to_rc "$HOME/.bashrc" [ -f "$HOME/.zshrc" ] && add_path_to_rc "$HOME/.zshrc" # ── 8. Генерация Standalone скриптов ──────────────────────── info "Генерирую standalone скрипты в $BIN_DIR..." HELPERS_FILE="$BIN_DIR/ai-api-helpers.sh" cat > "$HELPERS_FILE" << 'HELPEREOF' #!/usr/bin/env bash # _claude_test_api: Send 1-token test to an Anthropic-compatible endpoint _claude_test_api() { local url="$1" auth_header="$2" model="${3:-claude-sonnet-4-6}" local response response=$(curl -s -w "\n%{http_code}" --max-time 15 "$url" \ -H "$auth_header" \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d "{\"model\":\"$model\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" \ 2>/dev/null || echo "000") _CLAUDE_TEST_CODE=$(echo "$response" | tail -1) _CLAUDE_TEST_BODY=$(echo "$response" | sed '$d') } # _claude_test_openai_api: Send a tiny test to an OpenAI-compatible endpoint _claude_test_openai_api() { local url="$1" api_key="$2" model="${3:-kimi-k2.6}" local response response=$(curl -s -w "\n%{http_code}" --max-time 30 "$url" \ -H "Authorization: Bearer $api_key" \ -H "Content-Type: application/json" \ -d "{\"model\":\"$model\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" \ 2>/dev/null || echo "000") _CLAUDE_TEST_CODE=$(echo "$response" | tail -1) _CLAUDE_TEST_BODY=$(echo "$response" | sed '$d') } _handle_openai_api_response() { local provider="$1" local code="$2" local body="$3" local topup_url="$4" local _emsg case "$code" in 200) echo -e "\033[0;32mOK\033[0m" return 0 ;; 401|403) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация $provider недействительна (HTTP $code)." [ -n "$_emsg" ] && echo " $_emsg" return 401 ;; 429) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс/лимит $provider исчерпан." [ -n "$_emsg" ] && echo " $_emsg" [ -n "$topup_url" ] && echo " $topup_url" return 429 ;; 000) echo "" echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..." return 0 ;; *) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА]\033[0m API $provider вернул HTTP $code." [ -n "$_emsg" ] && echo " $_emsg" return 1 ;; esac } _claude_extract_error() { local body="$1" echo "$body" | python3 -c " import sys, json try: d = json.load(sys.stdin) e = d.get('error', {}) if isinstance(e, str): print(e) else: msg = e.get('message', '') if msg: print(msg) except: pass " 2>/dev/null } _claude_offer_reauth() { local provider="$1" echo "" echo -n "Хотите выполнить повторную авторизацию $provider? [Y/n] " local answer read -r answer case "${answer:-Y}" in [Yy]|[Yy][Ee][Ss]) return 0 ;; *) echo "Отменено."; return 1 ;; esac } _handle_api_response() { local provider="$1" local code="$2" local body="$3" local topup_url="$4" local _emsg case "$code" in 200) echo -e "\033[0;32mOK\033[0m" return 0 ;; 401|403) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация $provider недействительна (HTTP $code)." [ -n "$_emsg" ] && echo " $_emsg" return 401 ;; 429) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;33m[ЛИМИТ ИСЧЕРПАН]\033[0m Баланс/лимит $provider исчерпан." [ -n "$_emsg" ] && echo " $_emsg" [ -n "$topup_url" ] && echo " $topup_url" return 429 ;; 000) echo "" echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..." return 0 ;; 400) _emsg=$(_claude_extract_error "$body") if echo "${_emsg:-$body}" | grep -qi "RESOURCE_EXHAUSTED"; then echo "" echo -e "\033[0;33m[КВОТА ИСЧЕРПАНА]\033[0m Лимит запросов исчерпан." [ -n "$topup_url" ] && echo " $topup_url" return 429 fi # 400 = auth is valid, but max_tokens=1 is too small for thinking models echo -e "\033[0;32mOK\033[0m" return 0 ;; *) _emsg=$(_claude_extract_error "$body") echo "" echo -e "\033[0;31m[ОШИБКА]\033[0m API $provider вернул HTTP $code." [ -n "$_emsg" ] && echo " $_emsg" return 1 ;; esac } _open_browser() { local url="$1" if command -v xdg-open &>/dev/null; then xdg-open "$url" 2>/dev/null elif command -v open &>/dev/null; then open "$url" 2>/dev/null elif command -v sensible-browser &>/dev/null; then sensible-browser "$url" 2>/dev/null else echo "Откройте вручную: $url"; fi } _build_ai_sys_prompt() { local global_rules="$HOME/.config/ai-setup/global_rules.md" local global_rendered="" [ -f "$global_rules" ] && global_rendered="$(cat "$global_rules" 2>/dev/null)" # Нативные глобальные правила: только global_rules.md, без проектного контекста. # Проектные *.md файлы агент должен читать из текущего репозитория сам, если умеет. # Для агентов без надежного нативного чтения проектный контекст добавляется ниже в prompt. mkdir -p "$HOME/.codex" "$HOME/.kimi-code" "$HOME/.claude" "$HOME/.gemini" echo "$global_rendered" > "$HOME/.codex/AGENTS.md" echo "$global_rendered" > "$HOME/.kimi-code/AGENTS.md" echo "$global_rendered" > "$HOME/.claude/CLAUDE.md" echo "$global_rendered" > "$HOME/.gemini/GEMINI.md" local sp="=== ГЛОБАЛЬНЫЕ ПРАВИЛА ===\n" [ -n "$global_rendered" ] && sp+="$global_rendered\n\n" sp+="=== ПРАВИЛА ПРОЕКТА ===\n" for f in *.md; do [ -f "$f" ] && sp+="\n--- Файл $f ---\n$(cat "$f")\n" done echo -e "$sp" } HELPEREOF chmod +x "$HELPERS_FILE" # === ai-gpt === cat > "$BIN_DIR/ai-gpt" << 'GPTEOF' #!/usr/bin/env bash # ai-gpt - запуск нативного OpenAI Codex source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true codex_bin="$HOME/.npm-global/bin/codex" [ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)" if [ -z "$codex_bin" ] || [ ! -f "$codex_bin" ]; then echo "OpenAI Codex не найден. Устанавливаю..." curl -fsSL https://chatgpt.com/codex/install.sh | sh codex_bin="$HOME/.npm-global/bin/codex" [ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)" fi if [ -z "$codex_bin" ] || [ ! -f "$codex_bin" ]; then echo "Ошибка: не удалось установить OpenAI Codex." exit 1 fi _build_ai_sys_prompt > /dev/null # сохраняет в ~/.codex/AGENTS.md (codex читает авто) exec "$codex_bin" --dangerously-bypass-approvals-and-sandbox "$@" GPTEOF chmod +x "$BIN_DIR/ai-gpt" # === ai-deepseek === cat > "$BIN_DIR/ai-deepseek" << 'DEEPSEEKEOF' #!/usr/bin/env bash source ~/.local/bin/ai-api-helpers.sh key_file="$HOME/.config/ai-setup/deepseek_key" api_key="" reauth=0 [ -f "$key_file" ] && api_key=$(cat "$key_file") if [ -n "$api_key" ]; then echo -n "Проверка сохранённого DeepSeek ключа... " _claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "x-api-key: $api_key" "deepseek-v4-flash" _handle_api_response "DeepSeek" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://platform.deepseek.com/top_up" ret=$? if [ $ret -eq 401 ]; then rm -f "$key_file" api_key="" reauth=1 elif [ $ret -eq 429 ]; then echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] " read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac elif [ $ret -ne 0 ]; then exit 1 fi fi if [ -z "$api_key" ] && [ "$reauth" -eq 1 ]; then echo -n "Хотите ввести новый DeepSeek ключ? [Y/n] " read -r _ans; case "${_ans:-Y}" in [Yy]*) ;; *) exit 1 ;; esac fi if [ -z "$api_key" ]; then echo "Получить ключ: https://platform.deepseek.com/api_keys" read -r -p "Введите ваш DeepSeek API ключ: " api_key [ -z "$api_key" ] && { echo "Выход."; exit 1; } echo -n "Проверяю ключ и баланс... " _claude_test_api "https://api.deepseek.com/anthropic/v1/messages" "x-api-key: $api_key" "deepseek-v4-flash" _handle_api_response "DeepSeek" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://platform.deepseek.com/top_up" ret=$? if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then mkdir -p "$(dirname "$key_file")" echo "$api_key" > "$key_file" chmod 600 "$key_file" echo "Ключ сохранён." if [ $ret -eq 429 ]; then echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] " read -r _ans; case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac fi else echo "Ключ НЕ сохранён." exit 1 fi fi SYS_PROMPT=$(_build_ai_sys_prompt) ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic \ ANTHROPIC_AUTH_TOKEN="$api_key" \ ANTHROPIC_MODEL=deepseek-v4-pro \ ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro \ ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro \ ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash \ CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash \ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ claude --dangerously-skip-permissions --system-prompt "$SYS_PROMPT" "$@" DEEPSEEKEOF chmod +x "$BIN_DIR/ai-deepseek" # === ai-kimi === cat > "$BIN_DIR/ai-kimi" << 'KIMIEOF' #!/usr/bin/env bash # ai-kimi - запуск нативного Kimi Code через Artemox API source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true kimi_bin="$HOME/.kimi-code/bin/kimi" [ ! -f "$kimi_bin" ] && kimi_bin="$(command -v kimi 2>/dev/null)" if [ -z "$kimi_bin" ] || [ ! -f "$kimi_bin" ]; then echo "Kimi Code не найден. Устанавливаю..." curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash kimi_bin="$HOME/.kimi-code/bin/kimi" [ ! -f "$kimi_bin" ] && kimi_bin="$(command -v kimi 2>/dev/null)" fi if [ -z "$kimi_bin" ] || [ ! -f "$kimi_bin" ]; then echo "Ошибка: не удалось установить Kimi Code." exit 1 fi case "${1:-}" in --version|-V|--help|-h) exec "$kimi_bin" "$@" ;; esac config_file="${KIMI_CODE_HOME:-$HOME/.kimi-code}/config.toml" key_file="$HOME/.config/ai-setup/kimi_key" model_alias="artemox/kimi-k2.6" model_name="kimi-k2.6" base_url="https://api.artemox.com/v1" _extract_artemox_key() { [ -f "$config_file" ] || return 0 python3 - "$config_file" <<'PYEOF' import re import sys path = sys.argv[1] try: lines = open(path, encoding="utf-8").read().splitlines() except OSError: raise SystemExit(0) inside = False for line in lines: stripped = line.strip() if re.match(r'^\[providers\.(?:"artemox"|artemox)\]$', stripped): inside = True continue if inside and stripped.startswith("["): inside = False if inside: match = re.match(r'^api_key\s*=\s*"(.*)"\s*$', stripped) if match: print(match.group(1)) break PYEOF } _write_artemox_config() { mkdir -p "$(dirname "$config_file")" ARTEMOX_API_KEY="$api_key" python3 - "$config_file" <<'PYEOF' import json import os import re import sys path = sys.argv[1] api_key = os.environ["ARTEMOX_API_KEY"] try: content = open(path, encoding="utf-8").read() except OSError: content = "" lines = content.splitlines() filtered = [] skip = False for line in lines: stripped = line.strip() if re.match(r'^\[providers\.(?:"artemox"|artemox)\]$', stripped): skip = True continue if re.match(r'^\[models\."artemox/kimi-k2\.6"\]$', stripped): skip = True continue if skip and stripped.startswith("["): skip = False if skip: continue if re.match(r'^\s*default_model\s*=', line): continue filtered.append(line) body = "\n".join(filtered).strip() managed = f'''default_model = "artemox/kimi-k2.6" # Managed by ai-kimi. Re-run ai-kimi to refresh Artemox settings. [providers.artemox] type = "openai" base_url = "https://api.artemox.com/v1" api_key = {json.dumps(api_key)} [models."artemox/kimi-k2.6"] provider = "artemox" model = "kimi-k2.6" max_context_size = 262144 capabilities = ["thinking", "tool_use"] ''' new_content = managed if body: new_content += "\n" + body + "\n" with open(path, "w", encoding="utf-8") as fh: fh.write(new_content) PYEOF chmod 600 "$config_file" } _prompt_artemox_key() { echo "Получить/проверить ключ: https://artemox.com/dashboard" if [ ! -t 0 ]; then echo "Artemox API ключ не найден. Запустите ai-kimi в интерактивном терминале и введите ключ." exit 1 fi read -r -p "Введите ваш Artemox API ключ: " api_key [ -z "$api_key" ] && { echo "Выход."; exit 1; } } api_key="" [ -f "$key_file" ] && api_key=$(cat "$key_file") [ -z "$api_key" ] && api_key=$(_extract_artemox_key) [ -z "$api_key" ] && _prompt_artemox_key if declare -F _claude_test_openai_api >/dev/null && declare -F _handle_openai_api_response >/dev/null; then echo -n "Проверка Artemox ключа... " _claude_test_openai_api "$base_url/chat/completions" "$api_key" "$model_name" _handle_openai_api_response "Artemox/Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://artemox.com/dashboard" case "$_CLAUDE_TEST_CODE" in 200|000) ;; 401|403) rm -f "$key_file" api_key="" echo "Сохранённый Artemox ключ недействителен." _prompt_artemox_key echo -n "Повторная проверка Artemox ключа... " _claude_test_openai_api "$base_url/chat/completions" "$api_key" "$model_name" _handle_openai_api_response "Artemox/Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://artemox.com/dashboard" case "$_CLAUDE_TEST_CODE" in 200|000) ;; *) echo "Ключ НЕ сохранён."; exit 1 ;; esac ;; 429) echo -n "Продолжить всё равно? (запросы могут не проходить) [y/N] " read -r _ans case "${_ans:-N}" in [Yy]*) ;; *) exit 1 ;; esac ;; *) echo "Ключ НЕ сохранён." exit 1 ;; esac fi mkdir -p "$(dirname "$key_file")" echo "$api_key" > "$key_file" chmod 600 "$key_file" _write_artemox_config echo "Kimi настроен на Artemox: $model_alias" _build_ai_sys_prompt > /dev/null # сохраняет в ~/.kimi-code/AGENTS.md (kimi читает авто) exec "$kimi_bin" --yolo "$@" KIMIEOF chmod +x "$BIN_DIR/ai-kimi" # === ai-gemini === cat > "$BIN_DIR/ai-gemini" << 'GEMINIEOF' #!/usr/bin/env bash # ============================================================ # ai-gemini - запуск нативного antigravity CLI (agy) # ============================================================ agy_bin="$HOME/.local/bin/agy" [ ! -f "$agy_bin" ] && agy_bin="$(command -v agy 2>/dev/null)" if [ -z "$agy_bin" ] || [ ! -f "$agy_bin" ]; then echo "Antigravity CLI (agy) не найден. Устанавливаю..." curl -fsSL https://antigravity.google/cli/install.sh | bash agy_bin="$HOME/.local/bin/agy" [ ! -f "$agy_bin" ] && agy_bin="$(command -v agy 2>/dev/null)" fi if [ -z "$agy_bin" ] || [ ! -f "$agy_bin" ]; then echo "Ошибка: не удалось установить antigravity CLI." exit 1 fi source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true SYS_PROMPT=$(_build_ai_sys_prompt) if [ $# -eq 0 ]; then exec "$agy_bin" --dangerously-skip-permissions -i "$SYS_PROMPT\n\nПрочитай правила выше и коротко подтверди готовность к работе." else ARGS=("$@") INJECTED=0 for i in "${!ARGS[@]}"; do if [[ "${ARGS[$i]}" == "-i" || "${ARGS[$i]}" == "-p" || "${ARGS[$i]}" == "--prompt-interactive" || "${ARGS[$i]}" == "--print" ]]; then ARGS[$((i+1))]="$SYS_PROMPT\n\nЗапрос пользователя:\n${ARGS[$((i+1))]}" INJECTED=1 break fi done exec "$agy_bin" --dangerously-skip-permissions "${ARGS[@]}" fi GEMINIEOF chmod +x "$BIN_DIR/ai-gemini" # === ai-claude === cat > "$BIN_DIR/ai-claude" << 'CLAUDEEOF' #!/usr/bin/env bash # ai-claude - запуск оригинального Claude Code (Anthropic) source "$HOME/.local/bin/ai-api-helpers.sh" 2>/dev/null || true SYS_PROMPT=$(_build_ai_sys_prompt) exec claude --dangerously-skip-permissions --system-prompt "$SYS_PROMPT" "$@" CLAUDEEOF chmod +x "$BIN_DIR/ai-claude" info "Удаляю старые версии скриптов (claude_*)..." rm -f "$BIN_DIR/claude_gpt" "$BIN_DIR/claude_deepseek" "$BIN_DIR/claude_kimi" "$BIN_DIR/claude_gemini" "$BIN_DIR/claude_api_helpers.sh" success "Скрипты сгенерированы." # ── 9. Итог ────────────────────────────────────────────────── echo "" echo -e "${GREEN}════════════════════════════════════════════════════${NC}" echo -e "${GREEN} Установка завершена!${NC}" echo -e "${GREEN}════════════════════════════════════════════════════${NC}" echo "" echo "Доступные команды (теперь это независимые скрипты в ~/.local/bin):" echo -e " ${CYAN}ai-claude${NC} - Оригинальный Claude Code (Anthropic)" echo -e " ${CYAN}ai-gpt${NC} - OpenAI Codex (нативный CLI, автоустановка)" echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (API ключ сохраняется)" echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.6 (нативный CLI, автоустановка)" echo -e " ${CYAN}ai-gemini${NC} - Gemini (нативный agy CLI, автоустановка)" echo "" echo -e "${YELLOW}⚠️ Для Gemini используйте отдельный Google-аккаунт!${NC}" echo "" echo -e "Чтобы команды были доступны сразу, выполните: ${GREEN}exec bash${NC}"