- _restore_effort: каждый лаунчер читает свой effort из ~/.cache/ai-setup/effort_<launcher> и записывает в settings.json - effort-save-hook.sh: сохраняет effortLevel из settings.json в кэш при завершении сессии (через Claude Code hooks) - Все лаунчеры (claude/deepseek/kimi/openrouter) экспортируют AI_LAUNCHER для идентификации в statusline и хуках - _deepseek_balance: мультивалютный вывод (USD + CNY с символами $ и ¥) - Дефолтные effort: claude=xhigh, deepseek/kimi/openrouter=high Co-Authored-By: Claude <noreply@anthropic.com>
1482 lines
57 KiB
Bash
Executable File
1482 lines
57 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================
|
||
# AI Setup - Claude / GPT-5.5 / DeepSeek / Kimi / Gemini
|
||
# Запуск: bash ai-setup.sh
|
||
# ============================================================
|
||
|
||
CONFIG_DIR="$HOME/.config/ai-setup"
|
||
# Автоопределение: ~/bin если есть в PATH, иначе ~/.local/bin
|
||
if [[ ":$PATH:" == *":$HOME/bin:"* ]]; then
|
||
BIN_DIR="$HOME/bin"
|
||
else
|
||
BIN_DIR="$HOME/.local/bin"
|
||
fi
|
||
NPM_GLOBAL="$HOME/.npm-global"
|
||
PROXY_BIN="$BIN_DIR/claude-code-proxy"
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
GLOBAL_RULES_SOURCE="$SCRIPT_DIR/home-configs/GLOBAL_RULES.md"
|
||
|
||
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 найден"
|
||
|
||
# ── VLESS URL parser ───────────────────────────────────────────
|
||
# Принимает vless:// URL, устанавливает переменные VL_*
|
||
parse_vless_url() {
|
||
local url="$1"
|
||
eval "$(python3 -c "
|
||
import urllib.parse, sys
|
||
|
||
url = sys.argv[1]
|
||
rest = url[8:] # strip 'vless://'
|
||
|
||
at_pos = rest.index('@')
|
||
uuid = rest[:at_pos]
|
||
rest = rest[at_pos+1:]
|
||
|
||
colon_pos = rest.index(':')
|
||
q_pos = rest.index('?')
|
||
host = rest[:colon_pos]
|
||
port = rest[colon_pos+1:q_pos]
|
||
|
||
rest = rest[q_pos+1:]
|
||
hash_pos = rest.index('#') if '#' in rest else len(rest)
|
||
qs = rest[:hash_pos]
|
||
name = rest[hash_pos+1:] if '#' in rest else ''
|
||
|
||
params = urllib.parse.parse_qs(qs)
|
||
def get(p, default=''):
|
||
vals = params.get(p, [default])
|
||
return vals[0] if vals else default
|
||
|
||
flow = get('flow')
|
||
vtype = get('type')
|
||
if not vtype:
|
||
vtype = 'xhttp' if not flow else 'tcp'
|
||
spx_default = '' if vtype == 'tcp' else '/'
|
||
|
||
print(f'VL_UUID={uuid}')
|
||
print(f'VL_ADDRESS={host}')
|
||
print(f'VL_PORT={port}')
|
||
print(f'VL_ENCRYPTION={get(\"encryption\")}')
|
||
print(f'VL_FLOW={flow}')
|
||
print(f'VL_SECURITY={get(\"security\")}')
|
||
print(f'VL_SNI={get(\"sni\")}')
|
||
print(f'VL_FP={get(\"fp\", \"chrome\")}')
|
||
print(f'VL_PBK={get(\"pbk\")}')
|
||
print(f'VL_SID={get(\"sid\")}')
|
||
print(f'VL_TYPE={vtype}')
|
||
print(f'VL_PATH={urllib.parse.unquote(get(\"path\", \"/\"))}')
|
||
print(f'VL_MODE={get(\"mode\", \"auto\")}')
|
||
print(f'VL_SPX={get(\"spx\", spx_default)}')
|
||
print(f'VL_NAME={name}')
|
||
" "$url")"
|
||
}
|
||
|
||
# ── VLESS connectivity test ────────────────────────────────────
|
||
# Запускает xray с тестовым конфигом и проверяет curl'ом
|
||
# Возвращает: "ok" или "fail"
|
||
test_vless_server() {
|
||
local url="$1" test_port="$2"
|
||
|
||
parse_vless_url "$url"
|
||
|
||
# Генерируем временный конфиг
|
||
_TMP_CONF="/tmp/xray_test_${test_port}.json"
|
||
_VL_XHTTP=""
|
||
if [ "$VL_TYPE" = "xhttp" ]; then
|
||
_VL_XHTTP=$(printf ',\n "xhttpSettings": {\n "path": "%s",\n "mode": "%s"\n }' "$VL_PATH" "$VL_MODE")
|
||
fi
|
||
|
||
cat > "$_TMP_CONF" << XRAYEOF
|
||
{
|
||
"log": { "loglevel": "none" },
|
||
"inbounds": [
|
||
{
|
||
"port": $test_port,
|
||
"listen": "127.0.0.1",
|
||
"protocol": "socks",
|
||
"settings": { "udp": true }
|
||
}
|
||
],
|
||
"outbounds": [
|
||
{
|
||
"protocol": "vless",
|
||
"settings": {
|
||
"vnext": [
|
||
{
|
||
"address": "$VL_ADDRESS",
|
||
"port": $VL_PORT,
|
||
"users": [
|
||
{
|
||
"id": "$VL_UUID",
|
||
"encryption": "$VL_ENCRYPTION",
|
||
"flow": "$VL_FLOW"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"streamSettings": {
|
||
"network": "$VL_TYPE",
|
||
"security": "$VL_SECURITY",
|
||
"realitySettings": {
|
||
"serverName": "$VL_SNI",
|
||
"fingerprint": "$VL_FP",
|
||
"publicKey": "$VL_PBK",
|
||
"shortId": "$VL_SID",
|
||
"spiderX": "$VL_SPX"
|
||
}$_VL_XHTTP
|
||
}
|
||
}
|
||
]
|
||
}
|
||
XRAYEOF
|
||
|
||
# Запускаем xray
|
||
/usr/local/bin/xray run -c "$_TMP_CONF" >/dev/null 2>&1 &
|
||
local xpid=$!
|
||
sleep 2
|
||
|
||
# Тест через SOCKS5
|
||
local result="fail"
|
||
if curl --socks5 "127.0.0.1:${test_port}" --max-time 5 -s https://ifconfig.co/ >/dev/null 2>&1; then
|
||
result="ok"
|
||
fi
|
||
|
||
kill $xpid 2>/dev/null; wait $xpid 2>/dev/null
|
||
rm -f "$_TMP_CONF"
|
||
echo "$result"
|
||
}
|
||
|
||
# ── 0. Выбор режима работы (vless / direct) ─────────────────
|
||
read -r -p "Установить встроенный vless? [Y/n] " _vless_ans
|
||
_vless_ans="${_vless_ans:-Y}"
|
||
if [[ "$_vless_ans" =~ ^[Yy]$ ]]; then
|
||
USE_VLESS=1
|
||
|
||
# Читаем список серверов
|
||
_VL_URLS=()
|
||
_VL_LABELS=()
|
||
_SERVERS_FILE="$SCRIPT_DIR/home-configs/vless/servers.conf"
|
||
|
||
if [ ! -f "$_SERVERS_FILE" ]; then
|
||
err "Файл servers.conf не найден: $_SERVERS_FILE"
|
||
fi
|
||
|
||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||
[[ -z "$line" ]] && continue
|
||
_vl_rest="${line#vless://}"
|
||
_vl_rest="${_vl_rest#*@}"
|
||
_vl_ip="${_vl_rest%%:*}"
|
||
_vl_name="${line##*#}"
|
||
[[ "$_vl_name" == "$line" ]] && _vl_name=""
|
||
_VL_URLS+=("$line")
|
||
_VL_LABELS+=("$_vl_ip ($_vl_name)")
|
||
done < "$_SERVERS_FILE"
|
||
|
||
if [ "${#_VL_URLS[@]}" -eq 0 ]; then
|
||
err "Нет VLESS серверов в $_SERVERS_FILE"
|
||
fi
|
||
|
||
# Реальная проверка VLESS (запускаем xray для каждого сервера)
|
||
echo ""
|
||
info "Проверяю VLESS-доступность серверов..."
|
||
_VL_STATUS=()
|
||
for i in "${!_VL_URLS[@]}"; do
|
||
_label="${_VL_LABELS[$i]}"
|
||
_test_port=$((10980 + i))
|
||
printf " %-45s" "$((i+1))) $_label"
|
||
_status=$(test_vless_server "${_VL_URLS[$i]}" "$_test_port")
|
||
if [ "$_status" = "ok" ]; then
|
||
echo -e "${GREEN}✓ работает${NC}"
|
||
_VL_STATUS+=("ok")
|
||
else
|
||
echo -e "${RED}✗ не работает${NC}"
|
||
_VL_STATUS+=("fail")
|
||
fi
|
||
done
|
||
echo ""
|
||
|
||
read -r -p "Выбери сервер [1-${#_VL_URLS[@]}]: " _vl_choice
|
||
_vl_choice="${_vl_choice:-1}"
|
||
if ! [[ "$_vl_choice" =~ ^[0-9]+$ ]] || [ "$_vl_choice" -lt 1 ] || [ "$_vl_choice" -gt "${#_VL_URLS[@]}" ]; then
|
||
err "Неверный выбор: $_vl_choice"
|
||
fi
|
||
|
||
_VL_SELECTED="${_VL_URLS[$((_vl_choice-1))]}"
|
||
parse_vless_url "$_VL_SELECTED"
|
||
info "Выбран: $VL_ADDRESS ($VL_NAME)"
|
||
else
|
||
USE_VLESS=0
|
||
info "Режим: direct (без проксирования)"
|
||
|
||
# ── Откат всех настроек vless ──────────────────────────────
|
||
info "Откатываю настройки прокси..."
|
||
|
||
# Останавливаем и отключаем xray
|
||
sudo systemctl stop xray 2>/dev/null || true
|
||
sudo systemctl disable xray 2>/dev/null || true
|
||
success "xray остановлен и отключён"
|
||
|
||
# Системный прокси → none
|
||
if command -v gsettings &>/dev/null; then
|
||
gsettings set org.gnome.system.proxy mode 'none' 2>/dev/null || true
|
||
success "Системный прокси отключён"
|
||
fi
|
||
|
||
# Firefox → direct (type=0)
|
||
FIREFOX_PROFILE=""
|
||
if [ -d "$HOME/snap/firefox/common/.mozilla/firefox" ]; then
|
||
FIREFOX_PROFILE=$(find "$HOME/snap/firefox/common/.mozilla/firefox" -name "*.default*" -type d | head -1)
|
||
elif [ -d "$HOME/.mozilla/firefox" ]; then
|
||
FIREFOX_PROFILE=$(find "$HOME/.mozilla/firefox" -name "*.default*" -type d | head -1)
|
||
fi
|
||
if [ -n "$FIREFOX_PROFILE" ]; then
|
||
cat > "$FIREFOX_PROFILE/user.js" << 'FJSEOF'
|
||
user_pref("network.proxy.type", 0);
|
||
FJSEOF
|
||
success "Firefox переключён на прямой доступ"
|
||
fi
|
||
|
||
# Включаем IPv6 обратно (только если kill switch не активен)
|
||
if ufw status | grep -qE "активен|active"; then
|
||
info "UFW kill switch активен — оставляю IPv6 отключённым"
|
||
else
|
||
sudo rm -f /etc/sysctl.d/99-disable-ipv6.conf
|
||
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0 2>/dev/null || true
|
||
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=0 2>/dev/null || true
|
||
sudo systemctl restart systemd-resolved 2>/dev/null || true
|
||
success "IPv6 восстановлен"
|
||
fi
|
||
fi
|
||
|
||
# ── 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
|
||
|
||
# ── 2.5. proxychains-ng + xray (только в режиме vless) ──────
|
||
if [ "$USE_VLESS" -eq 1 ]; then
|
||
info "Проверяю proxychains-ng..."
|
||
if ! command -v proxychains4 &>/dev/null; then
|
||
info "Устанавливаю proxychains-ng (нужен sudo)..."
|
||
if command -v apt-get &>/dev/null; then
|
||
sudo apt-get install -y proxychains-ng
|
||
elif command -v dnf &>/dev/null; then
|
||
sudo dnf install -y proxychains-ng
|
||
else
|
||
warn "Не удалось установить proxychains-ng автоматически. Установите вручную."
|
||
fi
|
||
fi
|
||
if command -v proxychains4 &>/dev/null; then
|
||
success "proxychains4 найден"
|
||
else
|
||
warn "proxychains4 не найден. Продолжаю без проксирования."
|
||
fi
|
||
|
||
info "Устанавливаю xray..."
|
||
|
||
# Останавливаем старый процесс (мог остаться от предыдущей установки)
|
||
sudo systemctl stop xray 2>/dev/null || true
|
||
|
||
XRAY_VERSION="26.3.27"
|
||
XRAY_ARCH="64"
|
||
XRAY_URL="https://github.com/XTLS/Xray-core/releases/download/v${XRAY_VERSION}/Xray-linux-${XRAY_ARCH}.zip"
|
||
|
||
TMPDIR=$(mktemp -d)
|
||
curl -fsSL "$XRAY_URL" -o "$TMPDIR/xray.zip"
|
||
unzip -q "$TMPDIR/xray.zip" -d "$TMPDIR"
|
||
sudo install -m 755 "$TMPDIR/xray" /usr/local/bin/xray
|
||
rm -rf "$TMPDIR"
|
||
|
||
sudo mkdir -p /etc/xray
|
||
|
||
# xhttpSettings — только для xhttp-транспорта
|
||
_VL_XHTTP=""
|
||
if [ "$VL_TYPE" = "xhttp" ]; then
|
||
_VL_XHTTP=$(printf ',\n "xhttpSettings": {\n "path": "%s",\n "mode": "%s"\n }' "$VL_PATH" "$VL_MODE")
|
||
fi
|
||
|
||
sudo tee /etc/xray/config.json > /dev/null << XRAYEOF
|
||
{
|
||
"log": { "loglevel": "warning" },
|
||
"inbounds": [
|
||
{
|
||
"port": 1080,
|
||
"listen": "127.0.0.1",
|
||
"protocol": "socks",
|
||
"settings": { "udp": true }
|
||
},
|
||
{
|
||
"port": 2080,
|
||
"listen": "127.0.0.1",
|
||
"protocol": "http"
|
||
}
|
||
],
|
||
"outbounds": [
|
||
{
|
||
"protocol": "vless",
|
||
"settings": {
|
||
"vnext": [
|
||
{
|
||
"address": "$VL_ADDRESS",
|
||
"port": $VL_PORT,
|
||
"users": [
|
||
{
|
||
"id": "$VL_UUID",
|
||
"encryption": "$VL_ENCRYPTION",
|
||
"flow": "$VL_FLOW"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"streamSettings": {
|
||
"network": "$VL_TYPE",
|
||
"security": "$VL_SECURITY",
|
||
"realitySettings": {
|
||
"serverName": "$VL_SNI",
|
||
"fingerprint": "$VL_FP",
|
||
"publicKey": "$VL_PBK",
|
||
"shortId": "$VL_SID",
|
||
"spiderX": "$VL_SPX"
|
||
}$_VL_XHTTP
|
||
}
|
||
}
|
||
]
|
||
}
|
||
XRAYEOF
|
||
sudo chmod 644 /etc/xray/config.json
|
||
|
||
sudo tee /etc/systemd/system/xray.service > /dev/null << 'SVCEOF'
|
||
[Unit]
|
||
Description=Xray Service
|
||
After=network.target
|
||
|
||
[Service]
|
||
ExecStart=/usr/local/bin/xray run -c /etc/xray/config.json
|
||
Restart=on-failure
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
SVCEOF
|
||
|
||
# Удаляем чужие drop-in оверрайды (могут переопределять ExecStart на старый конфиг)
|
||
sudo rm -rf /etc/systemd/system/xray.service.d/
|
||
# Удаляем старый дефолтный конфиг xray из других путей
|
||
sudo rm -rf /usr/local/etc/xray/
|
||
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now xray
|
||
success "xray установлен и запущен"
|
||
|
||
# ── Отключение IPv6 (VLESS не тянет IPv6, браузеры зависают) ──
|
||
info "Отключаю IPv6 на уровне системы..."
|
||
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
|
||
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1
|
||
sudo tee /etc/sysctl.d/99-disable-ipv6.conf > /dev/null << 'SYSCTEOF'
|
||
# Отключение IPv6 — требуется для стабильной работы VLESS/xray
|
||
net.ipv6.conf.all.disable_ipv6=1
|
||
net.ipv6.conf.default.disable_ipv6=1
|
||
SYSCTEOF
|
||
sudo systemctl restart systemd-resolved
|
||
success "IPv6 отключён, DNS-кэш очищен"
|
||
|
||
cp "$SCRIPT_DIR/home-configs/proxychains/proxychains-xray.conf" "$HOME/.proxychains-xray.conf"
|
||
success "Proxychains конфиг обновлён"
|
||
|
||
# ── Настройка Firefox на SOCKS5 + remote DNS ────────────────
|
||
info "Настраиваю Firefox на SOCKS5 прокси..."
|
||
FIREFOX_PROFILE=""
|
||
if [ -d "$HOME/snap/firefox/common/.mozilla/firefox" ]; then
|
||
FIREFOX_PROFILE=$(find "$HOME/snap/firefox/common/.mozilla/firefox" -name "*.default*" -type d | head -1)
|
||
elif [ -d "$HOME/.mozilla/firefox" ]; then
|
||
FIREFOX_PROFILE=$(find "$HOME/.mozilla/firefox" -name "*.default*" -type d | head -1)
|
||
fi
|
||
|
||
if [ -n "$FIREFOX_PROFILE" ]; then
|
||
cat > "$FIREFOX_PROFILE/user.js" << 'FJSEOF'
|
||
user_pref("network.proxy.type", 1);
|
||
user_pref("network.proxy.socks", "127.0.0.1");
|
||
user_pref("network.proxy.socks_port", 1080);
|
||
user_pref("network.proxy.socks_remote_dns", true);
|
||
user_pref("network.proxy.http", "");
|
||
user_pref("network.proxy.http_port", 0);
|
||
user_pref("network.proxy.ssl", "");
|
||
user_pref("network.proxy.ssl_port", 0);
|
||
FJSEOF
|
||
success "Firefox настроен на SOCKS5 (профиль: $FIREFOX_PROFILE)"
|
||
else
|
||
warn "Firefox не найден, пропускаю настройку прокси"
|
||
fi
|
||
|
||
# ── Настройка системного прокси (для Chrome/Chromium) ───────
|
||
info "Настраиваю системный прокси..."
|
||
if command -v gsettings &>/dev/null; then
|
||
gsettings set org.gnome.system.proxy mode 'manual' 2>/dev/null || true
|
||
gsettings set org.gnome.system.proxy.http host '127.0.0.1' 2>/dev/null || true
|
||
gsettings set org.gnome.system.proxy.http port 2080 2>/dev/null || true
|
||
gsettings set org.gnome.system.proxy.socks host '127.0.0.1' 2>/dev/null || true
|
||
gsettings set org.gnome.system.proxy.socks port 1080 2>/dev/null || true
|
||
success "Системный прокси настроен (HTTP 2080 + SOCKS 1080)"
|
||
else
|
||
warn "gsettings не найден, пропускаю настройку системного прокси"
|
||
fi
|
||
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 "Обновляю глобальные правила агентов..."
|
||
[ -f "$GLOBAL_RULES_SOURCE" ] || err "Файл глобальных правил не найден: $GLOBAL_RULES_SOURCE"
|
||
cp "$GLOBAL_RULES_SOURCE" "$CONFIG_DIR/global_rules.md"
|
||
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-файлы обновлены"
|
||
|
||
# ── 6.6. Деплой Claude и Gemini skills ───────────────────────
|
||
info "Обновляю skills для Claude и Gemini..."
|
||
SKILLS_SRC="$SCRIPT_DIR/home-configs/claude/skills"
|
||
CLAUDE_SKILLS_DST="$HOME/.claude/skills"
|
||
GEMINI_SKILLS_DST="$HOME/.gemini/config/plugins/local-setup/skills"
|
||
if [ -d "$SKILLS_SRC" ]; then
|
||
mkdir -p "$CLAUDE_SKILLS_DST" "$GEMINI_SKILLS_DST"
|
||
|
||
# Для Gemini нужен plugin.json, чтобы плагин со скилами загрузился
|
||
GEMINI_PLUGIN_DIR="$HOME/.gemini/config/plugins/local-setup"
|
||
cat <<EOF > "$GEMINI_PLUGIN_DIR/plugin.json"
|
||
{
|
||
"name": "local-setup",
|
||
"description": "Local custom skills deployed via ai-setup"
|
||
}
|
||
EOF
|
||
|
||
for skill_dir in "$SKILLS_SRC"/*; do
|
||
[ -d "$skill_dir" ] || continue
|
||
skill_name=$(basename "$skill_dir")
|
||
|
||
# Деплой для Claude
|
||
mkdir -p "$CLAUDE_SKILLS_DST/$skill_name"
|
||
cp -r "$skill_dir/"* "$CLAUDE_SKILLS_DST/$skill_name/"
|
||
|
||
# Деплой для Gemini (agy)
|
||
mkdir -p "$GEMINI_SKILLS_DST/$skill_name"
|
||
cp -r "$skill_dir/"* "$GEMINI_SKILLS_DST/$skill_name/"
|
||
done
|
||
success "Skills обновлены для Claude и Gemini"
|
||
else
|
||
info "Папка со skills не найдена, пропускаю"
|
||
fi
|
||
|
||
# ── 6.7. Статусная строка Claude Code ───────────────────────
|
||
info "Настраиваю статусную строку Claude Code..."
|
||
STATUSLINE_SRC="$SCRIPT_DIR/home-configs/claude/statusline-command.sh"
|
||
STATUSLINE_DST="$HOME/.claude/statusline-command.sh"
|
||
if [ -f "$STATUSLINE_SRC" ]; then
|
||
cp "$STATUSLINE_SRC" "$STATUSLINE_DST"
|
||
chmod +x "$STATUSLINE_DST"
|
||
# Вписываем statusLine в settings.json через python3
|
||
SETTINGS="$HOME/.claude/settings.json"
|
||
python3 - "$SETTINGS" "$STATUSLINE_DST" <<'PYEOF'
|
||
import sys, json, os
|
||
settings_path, script_path = sys.argv[1], sys.argv[2]
|
||
data = {}
|
||
if os.path.exists(settings_path):
|
||
with open(settings_path) as f:
|
||
try:
|
||
data = json.load(f)
|
||
except json.JSONDecodeError:
|
||
pass
|
||
data["statusLine"] = {"type": "command", "command": f"bash {script_path}"}
|
||
# Дефолтный effort для ai-claude (не передаётся через CLI, берётся отсюда)
|
||
data.setdefault("effortLevel", "xhigh")
|
||
# SessionStart хук - триггерит вызов statusLine при старте сессии
|
||
if "hooks" not in data:
|
||
data["hooks"] = {}
|
||
if "SessionStart" not in data["hooks"]:
|
||
data["hooks"]["SessionStart"] = [{"hooks": [{"type": "command", "command": "true"}]}]
|
||
with open(settings_path, "w") as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
f.write("\n")
|
||
PYEOF
|
||
success "Статусная строка настроена"
|
||
else
|
||
warn "Файл $STATUSLINE_SRC не найден, пропускаю"
|
||
fi
|
||
|
||
# ── 6.7.0. Хук effort-save (сохраняет effort при завершении сессии) ──
|
||
info "Деплою хук effort-save..."
|
||
EFFORT_HOOK_SRC="$SCRIPT_DIR/home-configs/claude/hooks/effort-save-hook.sh"
|
||
EFFORT_HOOK_DST="$HOME/.claude/hooks/effort-save-hook.sh"
|
||
mkdir -p "$HOME/.claude/hooks"
|
||
if [ -f "$EFFORT_HOOK_SRC" ]; then
|
||
cp "$EFFORT_HOOK_SRC" "$EFFORT_HOOK_DST"
|
||
chmod +x "$EFFORT_HOOK_DST"
|
||
python3 - "$HOME/.claude/settings.json" "$EFFORT_HOOK_DST" <<'PYEOF'
|
||
import sys, json, os
|
||
settings_path, hook_path = sys.argv[1], sys.argv[2]
|
||
data = {}
|
||
if os.path.exists(settings_path):
|
||
with open(settings_path) as f:
|
||
try: data = json.load(f)
|
||
except json.JSONDecodeError: pass
|
||
data.setdefault("hooks", {}).setdefault("Stop", [{"hooks": []}])
|
||
hook_cmd = f'bash "{hook_path}"'
|
||
stop_hooks = data["hooks"]["Stop"]
|
||
already = any(
|
||
any(h.get("command", "") == hook_cmd for h in entry.get("hooks", []))
|
||
for entry in stop_hooks
|
||
)
|
||
if not already:
|
||
stop_hooks[0]["hooks"].append({"type": "command", "command": hook_cmd})
|
||
with open(settings_path, "w") as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
f.write("\n")
|
||
PYEOF
|
||
success "Хук effort-save установлен"
|
||
else
|
||
warn "Файл $EFFORT_HOOK_SRC не найден, пропускаю"
|
||
fi
|
||
|
||
# ── 6.7.1. Хук switch-account ───────────────────────────────────
|
||
info "Деплою хук switch-account..."
|
||
SWITCH_HOOK_SRC="$SCRIPT_DIR/home-configs/claude/hooks/switch-account-hook.sh"
|
||
SWITCH_HOOK_DST="$HOME/.claude/hooks/switch-account-hook.sh"
|
||
mkdir -p "$HOME/.claude/hooks"
|
||
if [ -f "$SWITCH_HOOK_SRC" ]; then
|
||
cp "$SWITCH_HOOK_SRC" "$SWITCH_HOOK_DST"
|
||
chmod +x "$SWITCH_HOOK_DST"
|
||
# Прописываем хук в settings.json (идемпотентно)
|
||
python3 - "$HOME/.claude/settings.json" "$SWITCH_HOOK_DST" <<'PYEOF'
|
||
import sys, json, os
|
||
settings_path, hook_path = sys.argv[1], sys.argv[2]
|
||
data = {}
|
||
if os.path.exists(settings_path):
|
||
with open(settings_path) as f:
|
||
try: data = json.load(f)
|
||
except json.JSONDecodeError: pass
|
||
data.setdefault("hooks", {}).setdefault("UserPromptSubmit", [{"hooks": []}])
|
||
hook_cmd = f'bash "{hook_path}"'
|
||
ups = data["hooks"]["UserPromptSubmit"]
|
||
already = any(
|
||
any(h.get("command", "") == hook_cmd for h in entry.get("hooks", []))
|
||
for entry in ups
|
||
)
|
||
if not already:
|
||
ups[0]["hooks"].append({"type": "command", "command": hook_cmd})
|
||
with open(settings_path, "w") as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
f.write("\n")
|
||
PYEOF
|
||
success "Хук switch-account установлен"
|
||
else
|
||
warn "Файл $SWITCH_HOOK_SRC не найден, пропускаю"
|
||
fi
|
||
|
||
# ── 6.8. Регистрация официального маркетплейса плагинов Claude ──
|
||
info "Настраиваю маркетплейс плагинов Claude Code..."
|
||
if ! command -v claude &>/dev/null; then
|
||
warn "claude не найден, пропускаю настройку маркетплейса"
|
||
else
|
||
existing=$(claude plugin marketplace list 2>/dev/null | grep "claude-plugins-official" || true)
|
||
if [ -n "$existing" ]; then
|
||
success "Маркетплейс claude-plugins-official уже добавлен"
|
||
else
|
||
# Берём токен из env или спрашиваем
|
||
if [ -z "$GITHUB_TOKEN" ]; then
|
||
echo ""
|
||
echo "Для установки плагинов Claude нужен GitHub Personal Access Token."
|
||
echo "Создать можно на: https://github.com/settings/tokens (без scope, только public repos)"
|
||
read -rp "GitHub PAT (или Enter чтобы пропустить): " GITHUB_TOKEN
|
||
fi
|
||
if [ -z "$GITHUB_TOKEN" ]; then
|
||
warn "Токен не указан, маркетплейс плагинов не настроен"
|
||
warn "Позже запустите: claude plugin marketplace add https://TOKEN@github.com/anthropics/claude-plugins-official.git"
|
||
else
|
||
if claude plugin marketplace add "https://${GITHUB_TOKEN}@github.com/anthropics/claude-plugins-official.git" 2>&1; then
|
||
success "Маркетплейс claude-plugins-official добавлен"
|
||
else
|
||
warn "Не удалось добавить маркетплейс, проверьте токен"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── 6.9. Установка Claude Notifier ──────────────────────────
|
||
info "Устанавливаю Claude Notifier..."
|
||
if [ -f "$HOME/.claude/hooks/claude-notifier-on-stop.js" ]; then
|
||
success "Claude Notifier уже установлен"
|
||
else
|
||
if curl -fsSL https://raw.githubusercontent.com/ashmitb95/claude-notifier/main/install.sh | bash; then
|
||
success "Claude Notifier установлен"
|
||
else
|
||
warn "Не удалось установить Claude Notifier"
|
||
fi
|
||
fi
|
||
|
||
# ── 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"
|
||
local bin_rel="${BIN_DIR#$HOME/}"
|
||
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/${bin_rel}:\$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
|
||
|
||
export TZ="Europe/Helsinki"
|
||
|
||
# _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')
|
||
}
|
||
|
||
# _deepseek_balance: Query DeepSeek balance API and print info
|
||
_deepseek_balance() {
|
||
local api_key="$1"
|
||
local response
|
||
response=$(curl -s --max-time 10 "https://api.deepseek.com/user/balance" \
|
||
-H "Authorization: Bearer $api_key" \
|
||
-H "Accept: application/json" \
|
||
2>/dev/null || echo "")
|
||
if [ -z "$response" ]; then
|
||
echo -e " \033[0;33m[БАЛАНС]\033[0m Не удалось получить баланс (сеть?)"
|
||
return 1
|
||
fi
|
||
echo "$response" | python3 -c "
|
||
import sys, json, os
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
available = d.get('is_available', False)
|
||
infos = d.get('balance_infos', [])
|
||
if not infos:
|
||
print(' \033[0;33m[БАЛАНС]\033[0m Нет данных о балансе')
|
||
sys.exit(0)
|
||
symbols = {'USD': '\$', 'CNY': '¥'}
|
||
cache_parts = []
|
||
for info in infos:
|
||
curr = info.get('currency', '???')
|
||
total = info.get('total_balance', '0')
|
||
granted = info.get('granted_balance', '0')
|
||
topped_up = info.get('topped_up_balance', '0')
|
||
sym = symbols.get(curr, curr)
|
||
status = '✅ доступен' if available else '❌ не активен'
|
||
print(f' \033[1;36m💰 Баланс DeepSeek:\033[0m {total} {curr} {status}')
|
||
if float(granted) > 0:
|
||
print(f' └─ Начислено: {granted} {curr}')
|
||
if float(topped_up) > 0:
|
||
print(f' └─ Пополнено: {topped_up} {curr}')
|
||
cache_parts.append(f'{sym}{total}')
|
||
# Cache all currencies with symbols for statusline
|
||
if cache_parts:
|
||
cache_dir = os.path.expanduser('~/.cache/ai-setup')
|
||
os.makedirs(cache_dir, exist_ok=True)
|
||
with open(os.path.join(cache_dir, 'deepseek_balance'), 'w') as f:
|
||
f.write(' '.join(cache_parts) + '\n')
|
||
except Exception as e:
|
||
print(f' ⚠️ Не удалось разобрать баланс: {e}')
|
||
" 2>/dev/null || echo -e " \033[0;33m[БАЛАНС]\033[0m Ошибка парсинга ответа"
|
||
}
|
||
|
||
_handle_openai_api_response() {
|
||
local provider="$1"
|
||
local code="$2"
|
||
local body="$3"
|
||
local topup_url="$4"
|
||
|
||
# Используем глобальную переменную _API_RET вместо return,
|
||
# потому что bash return умеет только 0-255, а HTTP-коды
|
||
# вроде 401/429 обрезаются (401 % 256 = 145).
|
||
_API_RET=0
|
||
|
||
local _emsg
|
||
case "$code" in
|
||
200)
|
||
echo -e "\033[0;32mOK\033[0m"
|
||
_API_RET=0
|
||
;;
|
||
401|403)
|
||
_emsg=$(_claude_extract_error "$body")
|
||
echo ""
|
||
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация $provider недействительна (HTTP $code)."
|
||
[ -n "$_emsg" ] && echo " $_emsg"
|
||
_API_RET=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"
|
||
_API_RET=429
|
||
;;
|
||
000)
|
||
echo ""
|
||
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
|
||
_API_RET=0
|
||
;;
|
||
5[0-9][0-9])
|
||
echo ""
|
||
echo -e "\033[0;33m[СЕРВЕР]\033[0m $provider временно недоступен (HTTP $code). Продолжаю..."
|
||
_API_RET=0
|
||
;;
|
||
*)
|
||
_emsg=$(_claude_extract_error "$body")
|
||
echo ""
|
||
echo -e "\033[0;31m[ОШИБКА]\033[0m API $provider вернул HTTP $code."
|
||
[ -n "$_emsg" ] && echo " $_emsg"
|
||
_API_RET=$code
|
||
;;
|
||
esac
|
||
return 0
|
||
}
|
||
|
||
_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"
|
||
|
||
# Используем глобальную переменную _API_RET вместо return,
|
||
# потому что bash return умеет только 0-255, а HTTP-коды
|
||
# вроде 401/429 обрезаются (401 % 256 = 145).
|
||
_API_RET=0
|
||
|
||
local _emsg
|
||
case "$code" in
|
||
200)
|
||
echo -e "\033[0;32mOK\033[0m"
|
||
_API_RET=0
|
||
;;
|
||
401|403)
|
||
_emsg=$(_claude_extract_error "$body")
|
||
echo ""
|
||
echo -e "\033[0;31m[ОШИБКА АВТОРИЗАЦИИ]\033[0m Авторизация $provider недействительна (HTTP $code)."
|
||
[ -n "$_emsg" ] && echo " $_emsg"
|
||
_API_RET=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"
|
||
_API_RET=429
|
||
;;
|
||
000)
|
||
echo ""
|
||
echo -e "\033[0;33m[СЕТЬ]\033[0m Не удалось проверить ключ (нет сети?). Продолжаю..."
|
||
_API_RET=0
|
||
;;
|
||
5[0-9][0-9])
|
||
echo ""
|
||
echo -e "\033[0;33m[СЕРВЕР]\033[0m $provider временно недоступен (HTTP $code). Продолжаю..."
|
||
_API_RET=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"
|
||
_API_RET=429
|
||
else
|
||
# 400 = auth is valid, but max_tokens=1 is too small for thinking models
|
||
echo -e "\033[0;32mOK\033[0m"
|
||
_API_RET=0
|
||
fi
|
||
;;
|
||
*)
|
||
_emsg=$(_claude_extract_error "$body")
|
||
echo ""
|
||
echo -e "\033[0;31m[ОШИБКА]\033[0m API $provider вернул HTTP $code."
|
||
[ -n "$_emsg" ] && echo " $_emsg"
|
||
_API_RET=$code
|
||
;;
|
||
esac
|
||
return 0
|
||
}
|
||
|
||
_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
|
||
}
|
||
|
||
# _restore_effort: читает сохранённый effort для текущего AI_LAUNCHER из кэша
|
||
# и записывает его в settings.json, чтобы Claude Code подхватил нужный уровень.
|
||
# Не передаём --effort через CLI, чтобы /effort внутри сессии работал без блокировки.
|
||
_restore_effort() {
|
||
local default_effort="${1:-high}"
|
||
local launcher="${AI_LAUNCHER:-}"
|
||
[ -z "$launcher" ] && return
|
||
local effort_file="$HOME/.cache/ai-setup/effort_${launcher}"
|
||
local effort
|
||
effort=$(cat "$effort_file" 2>/dev/null)
|
||
[ -z "$effort" ] && effort="$default_effort"
|
||
mkdir -p "$HOME/.cache/ai-setup"
|
||
python3 - "$HOME/.claude/settings.json" "$effort" <<'PYEOF'
|
||
import sys, json, os
|
||
settings_path, effort = sys.argv[1], sys.argv[2]
|
||
data = {}
|
||
if os.path.exists(settings_path):
|
||
try:
|
||
with open(settings_path) as f:
|
||
data = json.load(f)
|
||
except Exception:
|
||
pass
|
||
data['effortLevel'] = effort
|
||
with open(settings_path, 'w') as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
f.write('\n')
|
||
PYEOF
|
||
}
|
||
|
||
_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 "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/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)"
|
||
# Fallback: если curl-установка не сработала (например, 403 от Cloudflare), ставим через npm
|
||
if [ -z "$codex_bin" ] || [ ! -f "$codex_bin" ]; then
|
||
echo "Установка через curl не удалась, пробую npm..."
|
||
npm install -g @openai/codex
|
||
codex_bin="$HOME/.npm-global/bin/codex"
|
||
[ ! -f "$codex_bin" ] && codex_bin="$(command -v codex 2>/dev/null)"
|
||
fi
|
||
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 "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||
|
||
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=$_API_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
|
||
_deepseek_balance "$api_key"
|
||
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=$_API_RET
|
||
if [ $ret -eq 0 ] || [ $ret -eq 429 ]; then
|
||
mkdir -p "$(dirname "$key_file")"
|
||
echo "$api_key" > "$key_file"
|
||
chmod 600 "$key_file"
|
||
echo "Ключ сохранён."
|
||
_deepseek_balance "$api_key"
|
||
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
|
||
|
||
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||
export AI_LAUNCHER=deepseek
|
||
_restore_effort high
|
||
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-file "$_PROMPT_FILE" "$@"
|
||
DEEPSEEKEOF
|
||
chmod +x "$BIN_DIR/ai-deepseek"
|
||
|
||
# === ai-kimi ===
|
||
cat > "$BIN_DIR/ai-kimi" << 'KIMIEOF'
|
||
#!/usr/bin/env bash
|
||
# ai-kimi - запуск Claude Code через официальный Kimi Code API
|
||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||
|
||
key_file="$HOME/.config/ai-setup/kimi_key"
|
||
api_key=""
|
||
|
||
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||
|
||
if [ -n "$api_key" ]; then
|
||
echo -n "Проверка сохранённого Kimi ключа... "
|
||
_claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.6"
|
||
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code"
|
||
ret=$_API_RET
|
||
if [ $ret -eq 401 ]; then
|
||
rm -f "$key_file"
|
||
api_key=""
|
||
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" ]; then
|
||
echo "Получить ключ: https://www.kimi.com/code"
|
||
read -r -p "Введите ваш Kimi API ключ: " api_key
|
||
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||
|
||
echo -n "Проверяю ключ и баланс... "
|
||
_claude_test_api "https://api.kimi.com/coding/v1/messages" "x-api-key: $api_key" "kimi-k2.6"
|
||
_handle_api_response "Kimi" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://www.kimi.com/code"
|
||
ret=$_API_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
|
||
|
||
if ! command -v claude &>/dev/null; then
|
||
echo "Ошибка: Claude Code не найден. Установите через npm:"
|
||
echo " npm install -g @anthropic-ai/claude-code"
|
||
exit 1
|
||
fi
|
||
|
||
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||
export AI_LAUNCHER=kimi
|
||
_restore_effort high
|
||
ANTHROPIC_BASE_URL=https://api.kimi.com/coding \
|
||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||
ANTHROPIC_MODEL=kimi-k2.6 \
|
||
ANTHROPIC_DEFAULT_OPUS_MODEL=kimi-k2.6 \
|
||
ANTHROPIC_DEFAULT_SONNET_MODEL=kimi-k2.6 \
|
||
ANTHROPIC_DEFAULT_HAIKU_MODEL=kimi-k2.6 \
|
||
CLAUDE_CODE_SUBAGENT_MODEL=kimi-k2.6 \
|
||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
|
||
KIMIEOF
|
||
chmod +x "$BIN_DIR/ai-kimi"
|
||
|
||
# === ai-openrouter ===
|
||
cat > "$BIN_DIR/ai-openrouter" << 'OPENROUTEREOF'
|
||
#!/usr/bin/env bash
|
||
# ai-openrouter - запуск Claude Code через OpenRouter (любые модели)
|
||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||
|
||
key_file="$HOME/.config/ai-setup/openrouter_key"
|
||
api_key=""
|
||
|
||
[ -f "$key_file" ] && api_key=$(cat "$key_file")
|
||
|
||
if [ -n "$api_key" ]; then
|
||
echo -n "Проверка сохранённого OpenRouter ключа... "
|
||
_claude_test_openai_api "https://openrouter.ai/api/v1/chat/completions" "$api_key" "openai/gpt-4o-mini"
|
||
_handle_openai_api_response "OpenRouter" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://openrouter.ai/settings/credits"
|
||
ret=$_API_RET
|
||
if [ $ret -eq 401 ]; then
|
||
rm -f "$key_file"
|
||
api_key=""
|
||
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" ]; then
|
||
echo "Получить ключ: https://openrouter.ai/settings/keys"
|
||
read -r -p "Введите ваш OpenRouter API ключ: " api_key
|
||
[ -z "$api_key" ] && { echo "Выход."; exit 1; }
|
||
|
||
echo -n "Проверяю ключ и баланс... "
|
||
_claude_test_openai_api "https://openrouter.ai/api/v1/chat/completions" "$api_key" "openai/gpt-4o-mini"
|
||
_handle_openai_api_response "OpenRouter" "$_CLAUDE_TEST_CODE" "$_CLAUDE_TEST_BODY" "Пополните баланс: https://openrouter.ai/settings/credits"
|
||
ret=$_API_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
|
||
|
||
if ! command -v claude &>/dev/null; then
|
||
echo "Ошибка: Claude Code не найден. Установите через npm:"
|
||
echo " npm install -g @anthropic-ai/claude-code"
|
||
exit 1
|
||
fi
|
||
|
||
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||
export AI_LAUNCHER=openrouter
|
||
_restore_effort high
|
||
ANTHROPIC_BASE_URL=https://openrouter.ai/api \
|
||
ANTHROPIC_AUTH_TOKEN="$api_key" \
|
||
ANTHROPIC_MODEL=openai/gpt-5.5 \
|
||
ANTHROPIC_DEFAULT_OPUS_MODEL=anthropic/claude-4.8-opus \
|
||
ANTHROPIC_DEFAULT_SONNET_MODEL=anthropic/claude-4.6-sonnet \
|
||
ANTHROPIC_DEFAULT_HAIKU_MODEL=openai/gpt-5.5 \
|
||
CLAUDE_CODE_SUBAGENT_MODEL=openai/gpt-5.5 \
|
||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
|
||
claude --dangerously-skip-permissions --system-prompt-file "$_PROMPT_FILE" "$@"
|
||
OPENROUTEREOF
|
||
chmod +x "$BIN_DIR/ai-openrouter"
|
||
|
||
# === 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 "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||
|
||
# agy нативно подтягивает правила и проектные .md файлы,
|
||
# поэтому ручная инъекция SYS_PROMPT больше не требуется.
|
||
exec "$agy_bin" --dangerously-skip-permissions "$@"
|
||
GEMINIEOF
|
||
chmod +x "$BIN_DIR/ai-gemini"
|
||
# Подменяем путь к agy, если BIN_DIR отличается от ~/.local/bin
|
||
[ "$BIN_DIR" != "$HOME/.local/bin" ] && sed -i "s|\$HOME/\.local/bin|\$HOME/${BIN_DIR#$HOME/}|g" "$BIN_DIR/ai-gemini"
|
||
|
||
# === ai-claude ===
|
||
cat > "$BIN_DIR/ai-claude" << 'CLAUDEEOF'
|
||
#!/usr/bin/env bash
|
||
# ai-claude - запуск оригинального Claude Code (Anthropic)
|
||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ai-api-helpers.sh" 2>/dev/null || true
|
||
_PROMPT_FILE=$(mktemp /tmp/ai-sys-prompt.XXXXXX)
|
||
trap 'rm -f "$_PROMPT_FILE"' EXIT INT TERM
|
||
_build_ai_sys_prompt > "$_PROMPT_FILE"
|
||
export AI_LAUNCHER=claude
|
||
_restore_effort xhigh
|
||
claude --dangerously-skip-permissions --model sonnet --system-prompt-file "$_PROMPT_FILE" "$@"
|
||
CLAUDEEOF
|
||
chmod +x "$BIN_DIR/ai-claude"
|
||
|
||
# ── 8.5. Proxychains инъекция (только в режиме vless) ────────
|
||
if [ "$USE_VLESS" -eq 1 ]; then
|
||
info "Включаю proxychains4 в ai-лаунчеры..."
|
||
sed -i 's/^exec "\$codex_bin"/exec proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" "\$codex_bin"/' "$BIN_DIR/ai-gpt"
|
||
sed -i 's/^claude --dangerously-skip-permissions/proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude --dangerously-skip-permissions/' "$BIN_DIR/ai-deepseek"
|
||
sed -i 's/^claude --dangerously-skip-permissions/proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude --dangerously-skip-permissions/' "$BIN_DIR/ai-kimi"
|
||
sed -i 's/^claude --dangerously-skip-permissions/proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude --dangerously-skip-permissions/' "$BIN_DIR/ai-openrouter"
|
||
sed -i 's/^\([[:space:]]*\)exec "\$agy_bin"/\1exec proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" "\$agy_bin"/' "$BIN_DIR/ai-gemini"
|
||
sed -i 's/^claude --dangerously-skip-permissions/proxychains4 -f "\$HOME\/\.proxychains-xray\.conf" claude --dangerously-skip-permissions/' "$BIN_DIR/ai-claude"
|
||
success "proxychains4 интегрирован"
|
||
fi
|
||
|
||
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"
|
||
|
||
# Если переехали на ~/bin — удаляем старые скрипты из ~/.local/bin
|
||
if [ "$BIN_DIR" != "$HOME/.local/bin" ]; then
|
||
warn "BIN_DIR=$BIN_DIR — удаляю старые скрипты из ~/.local/bin/ ..."
|
||
rm -f "$HOME/.local/bin/ai-gpt" "$HOME/.local/bin/ai-deepseek" "$HOME/.local/bin/ai-kimi" \
|
||
"$HOME/.local/bin/ai-openrouter" "$HOME/.local/bin/ai-gemini" "$HOME/.local/bin/ai-claude" \
|
||
"$HOME/.local/bin/ai-api-helpers.sh" "$HOME/.local/bin/claude-gpt-effort-proxy.py"
|
||
fi
|
||
|
||
success "Скрипты сгенерированы."
|
||
|
||
# ── 9. Итог ──────────────────────────────────────────────────
|
||
echo ""
|
||
echo -e "${GREEN}════════════════════════════════════════════════════${NC}"
|
||
echo -e "${GREEN} Установка завершена!${NC}"
|
||
echo -e "${GREEN}════════════════════════════════════════════════════${NC}"
|
||
echo ""
|
||
echo "Доступные команды (теперь это независимые скрипты в ~/${BIN_DIR#$HOME/}):"
|
||
echo ""
|
||
echo " На базе Claude Code:"
|
||
echo -e " ${CYAN}ai-claude${NC} - Оригинальный Claude Code (Anthropic)"
|
||
echo -e " ${CYAN}ai-deepseek${NC} - DeepSeek (через Claude Code, API ключ сохраняется)"
|
||
echo -e " ${CYAN}ai-kimi${NC} - Kimi K2.6 (через Claude Code, API ключ сохраняется)"
|
||
echo -e " ${CYAN}ai-openrouter${NC} - OpenRouter (через Claude Code: GPT-5.5, Opus 4.8, Sonnet 4.6)"
|
||
echo ""
|
||
echo " Нативные CLI:"
|
||
echo -e " ${CYAN}ai-gpt${NC} - OpenAI Codex (нативный CLI, автоустановка)"
|
||
echo -e " ${CYAN}ai-gemini${NC} - Gemini (нативный agy CLI, автоустановка)"
|
||
echo ""
|
||
echo -e "Чтобы команды были доступны сразу, выполните: ${GREEN}exec bash${NC}"
|