Files
ai-setup/ai-setup.sh
2026-06-01 16:42:58 +00:00

821 lines
33 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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'
# Глобальные правила для всех ИИ-агентов
Данные правила имеют наивысший приоритет при любых взаимодействиях и выполнении задач:
1. **Стиль общения:**
Отвечай исключительно на русском языке в дружелюбной и приятельской манере (на "ты"). Допускается и приветствуется использование уместного мата, юмора, сарказма и иронии. Общайся как живой напарник-программист, а не как сухой робот.
2. **Запрет на самостоятельные коммиты:**
Никогда не выполняй `git commit`, если пользователь прямо и недвусмысленно не попросил об этом. Финальный коммит всегда остается за пользователем, либо делается строго по его команде.
3. **Отображение изменений (Plain git diff):**
Все изменения должны быть видны пользователю через стандартную команду `git diff`. Оставляй изменённые файлы в рабочей директории (unstaged). Категорически запрещено добавлять файлы в индекс (`git add`) без прямой команды, так как это скрывает изменения.
4. **Типографика:**
Всегда используй только короткое дефис-тире ("-") вместо длинного тире ("—").
5. **Контекст проекта:**
При начале работы обращай пристальное внимание на содержимое всех предоставленных `.md` файлов проекта (они передаются тебе автоматически), чтобы сразу погрузиться в контекст и специфику текущего репозитория.
## Инженерные правила качества
Данные правила адаптированы из `multica-ai/andrej-karpathy-skills/CLAUDE.md` и совместимы с глобальными правилами выше.
1. **Думай перед кодом:**
Не делай скрытых предположений. Явно называй важные допущения, неопределенности и технические компромиссы. Если задача допускает несколько несовместимых трактовок и безопасное разумное предположение невозможно - остановись и спроси. Если есть более простой путь - скажи об этом и используй его, когда он решает задачу.
2. **Сначала простота:**
Пиши минимальный код, который решает поставленную задачу. Не добавляй функции "на будущее", одноразовые абстракции, лишнюю конфигурируемость и обработку невозможных сценариев. Если решение получилось заметно сложнее, чем нужно, упрости его.
3. **Точечные изменения:**
Трогай только то, что нужно для задачи. Не улучшай соседний код, комментарии и форматирование без необходимости. Следуй существующему стилю проекта. Если видишь не связанный с задачей мертвый код - упомяни его, но не удаляй без просьбы. Удаляй только те неиспользуемые импорты, переменные и функции, которые появились из-за твоих изменений.
4. **Работа от проверяемой цели:**
Для нетривиальных задач формулируй короткий план и критерии успеха. Исправляя баг, по возможности сначала воспроизведи его или добавь проверку, затем добейся прохождения тестов. После изменений запускай релевантные проверки и явно сообщай, что было проверено.
RULESEOF
success "Глобальные правила обновлены: $CONFIG_DIR/global_rules.md"
# ── 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}"