793 lines
30 KiB
Bash
Executable File
793 lines
30 KiB
Bash
Executable File
#!/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}"
|