Files
ai-setup/ai-setup.sh

793 lines
30 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` файлов проекта (они передаются тебе автоматически), чтобы сразу погрузиться в контекст и специфику текущего репозитория.
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 sp="=== ГЛОБАЛЬНЫЕ ПРАВИЛА ===\n"
[ -f "$global_rules" ] && sp+="$(cat "$global_rules" 2>/dev/null)\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
SYS_PROMPT=$(_build_ai_sys_prompt)
exec "$codex_bin" --dangerously-bypass-approvals-and-sandbox --system-prompt "$SYS_PROMPT" "$@"
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
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 \
SYS_PROMPT=$(_build_ai_sys_prompt)
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"
SYS_PROMPT=$(_build_ai_sys_prompt)
exec "$kimi_bin" --yolo --system-prompt "$SYS_PROMPT" "$@"
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}"