Files
claude-brains/setup.sh
T

439 lines
20 KiB
Bash
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.
#!/bin/bash
# =============================================================
# Claude на чужих мозгах — настройщик (Мак и Windows, один файл)
#
# Запуск (Мак, Терминал):
# curl -fsSL {URL}/setup.sh | bash -s ollama только Ollama
# curl -fsSL {URL}/setup.sh | bash -s zai только Z.ai
# curl -fsSL {URL}/setup.sh | bash -s both обе конфигурации
# curl -fsSL {URL}/setup.sh | bash спросит, что ставить
# curl -fsSL {URL}/setup.sh | bash -s restore вернуть родного Claude
#
# Что делает: находит УЖЕ УСТАНОВЛЕННЫЕ приложения, кладёт конфиги
# в приложение Claude и честно его перезапускает.
# НИЧЕГО НЕ СКАЧИВАЕТ И НЕ УСТАНАВЛИВАЕТ: Claude и Ollama ставятся
# руками по ссылкам из гайда. Трогает только приложение Claude,
# терминальный Claude Code не затрагивается.
#
# Флаг --no-restart: не перезапускать приложение.
# =============================================================
set -euo pipefail
# ---------- ОС и пути ----------
case "$(uname -s)" in
Darwin) OS=mac ;;
MINGW*|MSYS*|CYGWIN*) OS=win ;;
*) echo "Эта система не поддерживается (нужен Мак или Windows с Git Bash)."; exit 1 ;;
esac
if [ "$OS" = "mac" ]; then
C1P="$HOME/Library/Application Support/Claude"
else
C1P="$LOCALAPPDATA/Claude"
fi
# Папка стороннего режима это та же папка с суффиксом -3p.
C3P="${C1P}-3p"
LIB="$C3P/configLibrary"
ID_OLLAMA="a11a0000-0000-4000-8000-0000000000aa"
ID_ZAI="a11a0000-0000-4000-8000-0000000000bb"
ID_DEFAULT="a11a0000-0000-4000-8000-0000000000dd"
ZAI_PLACEHOLDER="ВСТАВЬ_СЮДА_СВОЙ_КЛЮЧ_ZAI"
MODE=install
BRAIN="${AK:-}"
NO_RESTART=false
for a in "$@"; do
case "$a" in
restore) MODE=restore ;;
ollama|zai|both) BRAIN="$a" ;;
--no-restart) NO_RESTART=true ;;
esac
done
[ "$BRAIN" = "restore" ] && { MODE=restore; BRAIN=""; }
say() { printf '\n\033[1m%s\033[0m\n' "$*"; }
ok() { printf ' \033[32m[ok]\033[0m %s\n' "$*"; }
warn() { printf ' \033[33m[!]\033[0m %s\n' "$*"; }
die() { printf ' \033[31m[стоп]\033[0m %s\n' "$*"; exit 1; }
# ---------- JSON-хелпер ----------
PY=""
if command -v python3 >/dev/null 2>&1; then PY=python3
elif command -v python >/dev/null 2>&1; then PY=python
fi
have_ps() { command -v powershell.exe >/dev/null 2>&1; }
[ -n "$PY" ] || have_ps || die "Нужен python3 (Мак) или powershell (Windows) для работы с JSON."
# ---------- Флаг разработчика ----------
# ВАЖНО. Приложение читает developer_settings.json из ТЕКУЩЕЙ папки данных,
# а она переключается на -3p только когда сторонний режим уже взлетел.
# Конфиги при этом всегда читаются из -3p. Замкнутый круг: если положить
# флаг только в -3p, до переключения приложение его не увидит и пункта
# Developer не будет. Поэтому пишем в ОБЕ папки.
write_dev_flag() {
local d
for d in "$C1P" "$C3P"; do
mkdir -p "$d"
printf '{"allowDevTools": true}\n' > "$d/developer_settings.json"
done
ok "Режим разработчика включён в обеих папках приложения."
}
# ---------- deploymentMode ----------
# Ключ "deploymentMode": "1p" намертво запрещает сторонний режим.
clear_deployment_pin() {
local f="$C3P/claude_desktop_config.json"
[ -f "$f" ] || return 0
[ -n "$PY" ] || return 0
"$PY" - "$f" <<'PY'
import json, sys
p = sys.argv[1]
try:
d = json.load(open(p, encoding="utf-8"))
except Exception:
sys.exit(0)
if isinstance(d, dict) and d.get("deploymentMode") == "1p":
d.pop("deploymentMode", None)
json.dump(d, open(p, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
print("снят")
PY
}
# merge_meta <applied: ollama|zai|default> <routes: ollama|zai|both|none>
merge_meta() {
local applied="$1" routes="$2"
if [ -n "$PY" ]; then
"$PY" - "$LIB" "$ID_OLLAMA" "$ID_ZAI" "$ID_DEFAULT" "$applied" "$routes" <<'PY'
import json, os, sys
lib, id_o, id_z, id_d, applied, routes = sys.argv[1:7]
p = os.path.join(lib, "_meta.json")
try:
meta = json.load(open(p, encoding="utf-8"))
if not isinstance(meta, dict): meta = {}
except Exception:
meta = {}
ent = meta.get("entries") or []
def ensure(i, n):
e = next((x for x in ent if x.get("id") == i), None)
if e: e["name"] = n
else: ent.append({"id": i, "name": n})
# Default обязан существовать как пустой конфиг: это точка возврата.
dflt = next((x for x in ent if x.get("name") == "Default"), None)
if dflt is None:
open(os.path.join(lib, id_d + ".json"), "w", encoding="utf-8").write("{}")
ensure(id_d, "Default")
dflt_id = id_d
else:
dflt_id = dflt["id"]
if routes in ("ollama", "both"): ensure(id_o, "ollama-cloud")
if routes in ("zai", "both"): ensure(id_z, "zai-glm")
# Выкидываем записи, у которых файла на диске нет: они ломают выбор в панели.
ent = [x for x in ent if os.path.exists(os.path.join(lib, x.get("id", "") + ".json"))]
meta["entries"] = ent
meta["appliedId"] = {"ollama": id_o, "zai": id_z}.get(applied, dflt_id)
json.dump(meta, open(p, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
PY
else
MERGE_LIB="$(cygpath -w "$LIB")" MERGE_APPLIED="$applied" MERGE_ROUTES="$routes" \
MERGE_IDO="$ID_OLLAMA" MERGE_IDZ="$ID_ZAI" MERGE_IDD="$ID_DEFAULT" \
powershell.exe -NoProfile -Command '
$lib=$env:MERGE_LIB; $p=Join-Path $lib "_meta.json"
if (Test-Path $p) { $meta=Get-Content $p -Raw -Encoding UTF8 | ConvertFrom-Json }
else { $meta=[pscustomobject]@{appliedId="";entries=@()} }
function Ensure($id,$name){
$e=$script:meta.entries | Where-Object { $_.id -eq $id }
if($e){$e.name=$name}
else{$script:meta.entries=@($script:meta.entries)+[pscustomobject]@{id=$id;name=$name}}
}
if(-not ($meta.entries | Where-Object {$_.name -eq "Default"})){
"{}" | Set-Content (Join-Path $lib ($env:MERGE_IDD+".json")) -Encoding UTF8
Ensure $env:MERGE_IDD "Default"
}
if($env:MERGE_ROUTES -eq "ollama" -or $env:MERGE_ROUTES -eq "both"){ Ensure $env:MERGE_IDO "ollama-cloud" }
if($env:MERGE_ROUTES -eq "zai" -or $env:MERGE_ROUTES -eq "both"){ Ensure $env:MERGE_IDZ "zai-glm" }
$meta.entries = @($meta.entries | Where-Object { Test-Path (Join-Path $lib ($_.id+".json")) })
$def=($meta.entries | Where-Object {$_.name -eq "Default"} | Select-Object -First 1).id
switch($env:MERGE_APPLIED){
"ollama" { $meta.appliedId=$env:MERGE_IDO }
"zai" { $meta.appliedId=$env:MERGE_IDZ }
default { $meta.appliedId=$def }
}
$meta | ConvertTo-Json -Depth 5 | Set-Content $p -Encoding UTF8
'
fi
}
# Проверяем, что применённый конфиг действительно включает сторонний режим.
check_applied() {
[ -n "$PY" ] || return 0
"$PY" - "$LIB" <<'PY'
import json, os, sys
lib = sys.argv[1]
try:
meta = json.load(open(os.path.join(lib, "_meta.json"), encoding="utf-8"))
applied = meta.get("appliedId", "")
cfg = json.load(open(os.path.join(lib, applied + ".json"), encoding="utf-8"))
except Exception as e:
print("НЕТ: не смог прочитать конфиг (%s)" % e); sys.exit(0)
name = next((x.get("name") for x in meta.get("entries", []) if x.get("id") == applied), applied)
print("ДА: %s" % name if cfg.get("inferenceProvider") else "НЕТ: применён пустой конфиг «%s»" % name)
PY
}
# ---------- Честный перезапуск ----------
# open -a на живом процессе только активирует окно: приложение не завершается
# при закрытии окна и настройки перечитывает лишь при настоящем старте.
restart_claude() {
if [ "$NO_RESTART" = "true" ]; then warn "Перезапуск пропущен (--no-restart). Настройки применятся после Cmd+Q и запуска."; return 0; fi
say "Перезапускаю приложение Claude..."
if [ "$OS" = "mac" ]; then
if pgrep -x "Claude" >/dev/null 2>&1; then
osascript -e 'tell application "Claude" to quit' >/dev/null 2>&1 || true
i=0; while pgrep -x "Claude" >/dev/null 2>&1 && [ "$i" -lt 12 ]; do sleep 1; i=$((i+1)); done
if pgrep -x "Claude" >/dev/null 2>&1; then
pkill -x "Claude" >/dev/null 2>&1 || true
sleep 2
fi
if pgrep -x "Claude" >/dev/null 2>&1; then
warn "Claude не закрылся сам (обычно это запрет на автоматизацию:"
echo " Системные настройки -> Конфиденциальность и безопасность -> Автоматизация)."
echo " Конфиги уже на месте. Закрой Claude вручную через Cmd+Q и открой заново."
return 0
fi
fi
open -a "Claude" >/dev/null 2>&1 || { warn "Не смог открыть Claude, открой вручную."; return 0; }
# Проверяем, что процесс реально поднялся: рапортовать успех вслепую нельзя.
j=0; while ! pgrep -x "Claude" >/dev/null 2>&1 && [ "$j" -lt 8 ]; do sleep 1; j=$((j+1)); done
if ! pgrep -x "Claude" >/dev/null 2>&1; then
warn "Claude не поднялся автоматически. Конфиги на месте, открой приложение вручную."
return 0
fi
else
powershell.exe -NoProfile -Command '
Get-Process -Name "Claude*" -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep 3
$exe=@("$env:LOCALAPPDATA\AnthropicClaude","$env:LOCALAPPDATA\Programs") | ForEach-Object { Get-ChildItem $_ -Recurse -Filter "claude.exe" -ErrorAction SilentlyContinue } | Select-Object -First 1
if($exe){ Start-Process $exe.FullName } else { exit 1 }
' || { warn "Не смог перезапустить Claude, закрой и открой его вручную."; return 0; }
fi
ok "Claude перезапущен."
}
# ---------- TLS-фикс песочницы (только Мак, для вкладки Code) ----------
apply_tls_fix() {
[ "$OS" = "mac" ] || return 0
[ -n "$PY" ] || return 0
local sj="$HOME/.claude/settings.json"
mkdir -p "$HOME/.claude"
[ -f "$sj" ] || echo '{}' > "$sj"
"$PY" - "$sj" <<'PY'
import json, sys
p = sys.argv[1]
try:
d = json.load(open(p, encoding="utf-8"))
if not isinstance(d, dict): d = {}
except Exception:
d = {}
sb = d.get("sandbox")
if not isinstance(sb, dict): sb = {}
if sb.get("enableWeakerNetworkIsolation") is not True:
sb["enableWeakerNetworkIsolation"] = True
d["sandbox"] = sb
json.dump(d, open(p, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
print("добавлен")
else:
print("уже был")
PY
}
# ---------- ОТКАТ ----------
if [ "$MODE" = "restore" ]; then
say "Откат на родного Claude..."
if [ ! -f "$LIB/_meta.json" ]; then ok "Конфигов нет, откатывать нечего."; exit 0; fi
merge_meta default none
ok "Применён пустой конфиг Default: приложение вернётся к родному входу."
write_dev_flag
restart_claude
echo
echo "Если что-то осталось сломанным, полный сброс (сотрёт чаты стороннего режима):"
echo " rm -rf \"$C3P\""
exit 0
fi
# ---------- 1. Приложение Claude ----------
say "[1/4] Ищу приложение Claude..."
CLAUDE_OK=false
if [ "$OS" = "mac" ] && [ -d "/Applications/Claude.app" ]; then CLAUDE_OK=true; fi
if [ "$OS" = "win" ] && find "$LOCALAPPDATA/AnthropicClaude" "$LOCALAPPDATA/Programs" -maxdepth 3 -iname "claude.exe" 2>/dev/null | grep -q .; then CLAUDE_OK=true; fi
if [ "$CLAUDE_OK" = "true" ]; then
ok "Claude на месте."
else
warn "Приложение Claude не найдено, а без него настраивать нечего."
echo
echo " Поставь его руками, это одна минута:"
echo " 1. Открой https://claude.com/download и скачай версию для своей системы."
echo " 2. Установи и запусти приложение хотя бы один раз."
echo " 3. Вернись сюда и запусти эту же команду снова."
echo
echo " Логиниться в аккаунт Anthropic не обязательно."
exit 1
fi
# ---------- 2. Выбор мозга ----------
if [ -z "$BRAIN" ]; then
say "[2/4] Какой мозг подключаем?"
echo " 1) Ollama бесплатный старт, программа Ollama должна быть установлена и запущена"
echo " 2) Z.ai по подписке от \$18, локально ничего не нужно, только ключ"
echo " 3) оба положу обе конфигурации, переключаться будешь в приложении"
printf " Твой выбор [1/2/3, по умолчанию 3]: "
choice=""
[ -r /dev/tty ] && { read -r choice < /dev/tty || choice=""; }
case "${choice:-3}" in
1) BRAIN=ollama ;;
2) BRAIN=zai ;;
*) BRAIN=both ;;
esac
else
say "[2/4] Режим: $BRAIN"
fi
case "$BRAIN" in
ollama) ok "Ставлю только Ollama." ;;
zai) ok "Ставлю только Z.ai." ;;
both) ok "Ставлю обе конфигурации." ;;
*) warn "Непонятный режим «$BRAIN», беру оба."; BRAIN=both ;;
esac
# ---------- 3. Ollama и псевдонимы ----------
HAVE_OLLAMA=false
if [ "$BRAIN" = "ollama" ] || [ "$BRAIN" = "both" ]; then
say "[3/4] Проверяю программу Ollama..."
OLLAMA_BIN=""
if command -v ollama >/dev/null 2>&1; then OLLAMA_BIN="ollama"
elif [ "$OS" = "mac" ] && [ -x "/Applications/Ollama.app/Contents/Resources/ollama" ]; then OLLAMA_BIN="/Applications/Ollama.app/Contents/Resources/ollama"
fi
if [ -z "$OLLAMA_BIN" ]; then
warn "Ollama не найдена."
echo
echo " Поставь её руками:"
echo " 1. Открой https://ollama.com/download и скачай версию для своей системы."
echo " 2. Установи, запусти, войди в аккаунт (иконка ламы -> Sign in)."
echo " 3. Вернись сюда и запусти команду снова."
echo
if [ "$BRAIN" = "ollama" ]; then
echo " Или подключи Z.ai, ему Ollama не нужна: та же команда со словом zai вместо ollama."
exit 1
fi
warn "Пропускаю Ollama, кладу только конфиг Z.ai."
BRAIN=zai
else
ok "Ollama на месте."
[ "$OS" = "mac" ] && { open -a "Ollama" >/dev/null 2>&1 || true; }
HAVE_OLLAMA=true
say " Создаю псевдонимы облачных моделей (нужен вход в аккаунт Ollama)..."
# Приложение пускает только точные идентификаторы моделей Claude,
# поэтому облачная модель получает второе, «правильное» имя.
make_alias() {
"$OLLAMA_BIN" pull "$1" >/dev/null 2>&1 || {
if [ "${3:-}" = "optional" ]; then
warn "$1 недоступна на твоём тарифе Ollama, пропускаю (нужен Pro или Max)."
return 0
fi
warn "Не получилось скачать $1. Обычно это нет входа в аккаунт Ollama."
echo " Открой окно Ollama (иконка ламы) -> Sign in, затем запусти команду снова."
exit 1
}
"$OLLAMA_BIN" cp "$1" "$2" >/dev/null
ok "$2 -> $1"
}
make_alias "glm-5.3:cloud" "claude-sonnet-4-6"
make_alias "glm-5.3-flash:cloud" "claude-haiku-4-5"
make_alias "kimi-k3:cloud" "claude-opus-4-6" optional
fi
else
say "[3/4] Маршрут Z.ai, Ollama не нужна."
fi
# ---------- 4. Конфиги ----------
say "[4/4] Кладу конфиги в приложение Claude..."
mkdir -p "$LIB"
write_dev_flag
pin="$(clear_deployment_pin || true)"
[ -n "$pin" ] && ok "Снят запрет на сторонний режим (deploymentMode)."
if [ "$BRAIN" = "ollama" ] || [ "$BRAIN" = "both" ]; then
cat > "$LIB/$ID_OLLAMA.json" <<'JSON'
{
"inferenceProvider": "gateway",
"inferenceGatewayBaseUrl": "http://localhost:11434",
"inferenceGatewayApiKey": "ollama",
"inferenceGatewayAuthScheme": "bearer",
"inferenceCredentialKind": "static",
"modelDiscoveryEnabled": false,
"inferenceModels": [
{ "name": "claude-sonnet-4-6", "labelOverride": "GLM-5.3 (Ollama Cloud)", "anthropicFamilyTier": "sonnet", "isFamilyDefault": true },
{ "name": "claude-opus-4-6", "labelOverride": "Kimi K3 (Ollama Cloud)", "anthropicFamilyTier": "opus", "isFamilyDefault": true },
{ "name": "claude-haiku-4-5", "labelOverride": "GLM-5.3 Flash (Ollama Cloud)", "anthropicFamilyTier": "haiku", "isFamilyDefault": true }
]
}
JSON
ok "Конфиг ollama-cloud на месте."
fi
if [ "$BRAIN" = "zai" ] || [ "$BRAIN" = "both" ]; then
if [ -f "$LIB/$ID_ZAI.json" ] && ! grep -q "$ZAI_PLACEHOLDER" "$LIB/$ID_ZAI.json"; then
ok "Конфиг zai-glm уже настроен, ключ не трогаю."
else
cat > "$LIB/$ID_ZAI.json" <<JSON
{
"inferenceProvider": "gateway",
"inferenceGatewayBaseUrl": "https://api.z.ai/api/anthropic",
"inferenceGatewayApiKey": "$ZAI_PLACEHOLDER",
"inferenceGatewayAuthScheme": "bearer",
"inferenceCredentialKind": "static",
"modelDiscoveryEnabled": false,
"inferenceModels": [
{ "name": "claude-sonnet-4-6", "labelOverride": "GLM-5.3 (Z.ai Coding Plan)", "anthropicFamilyTier": "sonnet", "isFamilyDefault": true },
{ "name": "claude-haiku-4-5", "labelOverride": "GLM-5.3 Flash (Z.ai)", "anthropicFamilyTier": "haiku", "isFamilyDefault": true }
]
}
JSON
ok "Конфиг zai-glm на месте, ждёт твой ключ."
fi
fi
# Активной делаем ту конфигурацию, которая реально готова работать.
if [ "$HAVE_OLLAMA" = "true" ]; then merge_meta ollama "$BRAIN"; else merge_meta zai "$BRAIN"; fi
if [ "$OS" = "mac" ]; then
ok "TLS-фикс песочницы для вкладки Code: $(apply_tls_fix)"
fi
# ---------- Самопроверка ----------
say "Проверяю, что получилось..."
res="$(check_applied || true)"
case "$res" in
ДА:*) ok "Сторонний режим включится: ${res#ДА: }" ;;
НЕТ:*) warn "${res#НЕТ: }"
echo " Открой Developer -> Configure Third-Party Inference и выбери конфигурацию вручную." ;;
esac
restart_claude
# ---------- Что дальше ----------
echo
echo "Что дальше:"
if [ "$HAVE_OLLAMA" = "true" ]; then
echo " · Открой новый чат и кликни по имени модели рядом с полем ввода:"
echo " там GLM-5.3 и GLM-5.3 Flash, а на платном тарифе Ollama ещё и Kimi K3."
echo " · Программа Ollama должна быть запущена: закрыл её, мост отвалился."
fi
if [ "$BRAIN" = "zai" ] || [ "$BRAIN" = "both" ]; then
echo " · Z.ai: возьми ключ на z.ai/manage-apikey/apikey-list, затем в приложении"
echo " Developer -> Configure Third-Party Inference -> конфигурация zai-glm ->"
echo " вставь ключ в Gateway API key -> Apply Changes -> перезапусти Claude."
fi
echo " · Нет пункта Developer? Полностью выйди из Claude (Cmd+Q) и открой заново."
echo " · Вернуть родного Claude: та же команда со словом restore."