#!/bin/sh # Nodule: one-command setup for Claude Code and Codex. # Usage: curl -fsSL https://nodule-provider.store/i | sh -s NOD-SETUP-XXXX-XXXX # The code is one-time and lasts 15 minutes; your key is not in the command. # You can read this script first: https://nodule-provider.store/i set -eu BASE="https://nodule-provider.store" CODE="$(printf '%s' "${1:-}" | tr '[:lower:]' '[:upper:]')" say() { printf '%s\n' "$*"; } fail() { printf 'Nodule: %s\n' "$*" >&2; exit 1; } case "$CODE" in NOD-SETUP-[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]-[0-9A-Z][0-9A-Z][0-9A-Z][0-9A-Z]) ;; *) fail "нужен одноразовый код: curl -fsSL $BASE/i | sh -s NOD-SETUP-XXXX-XXXX (получите его на $BASE/setup)" ;; esac command -v curl >/dev/null 2>&1 || fail "нужен curl" [ -n "${HOME:-}" ] && [ -d "$HOME" ] || fail "не найдена домашняя папка (HOME)" TMP="$(mktemp)" trap 'rm -f "$TMP"' EXIT STATUS="$(curl -sS -o "$TMP" -w '%{http_code}' -X POST -H 'content-type: application/json' \ --data "{\"code\":\"$CODE\"}" "$BASE/setup/api/install")" || fail "не удалось связаться с $BASE" if [ "$STATUS" != "200" ]; then if [ "$STATUS" = "410" ]; then fail "код уже использован или истёк. Получите новый на $BASE/setup"; fi if [ "$STATUS" = "429" ]; then fail "слишком много попыток. Подождите несколько минут"; fi fail "сервер ответил $STATUS. Попробуйте получить новый код на $BASE/setup" fi URL=""; KEY=""; CLAUDE_MODEL=""; CODEX_MODEL="" while IFS='=' read -r name value; do case "$name" in NODULE_BASE_URL) URL="$value" ;; NODULE_KEY) KEY="$value" ;; NODULE_CLAUDE_MODEL) CLAUDE_MODEL="$value" ;; NODULE_CODEX_MODEL) CODEX_MODEL="$value" ;; esac done < "$TMP" case "$URL" in http://*|https://*) ;; *) fail "сервер вернул неверный адрес" ;; esac case "$URL$KEY$CLAUDE_MODEL$CODEX_MODEL" in *[!]A-Za-z0-9._:/[-]*) fail "сервер вернул неожиданные символы" ;; esac [ -n "$KEY" ] || fail "сервер не вернул ключ" TS="$(date -u +%Y%m%dT%H%M%SZ)" backup() { if [ -f "$1" ]; then cp -p "$1" "$1.bak-$TS"; chmod 600 "$1.bak-$TS"; BACKUPS="$BACKUPS ~${1#"$HOME"}.bak-$TS"; fi; } BACKUPS="" DONE="" NOTE="" if [ -n "$CLAUDE_MODEL" ]; then mkdir -p "$HOME/.claude" F="$HOME/.claude/settings.json" backup "$F" MERGED=0 if command -v node >/dev/null 2>&1; then if NODULE_FILE="$F" NODULE_URL="$URL" NODULE_KEY="$KEY" NODULE_MODEL="$CLAUDE_MODEL" node -e ' const fs = require("fs"); const e = process.env; let s = {}; try { s = JSON.parse(fs.readFileSync(e.NODULE_FILE, "utf8")); } catch (error) { if (error.code !== "ENOENT") process.exit(3); } if (!s || typeof s !== "object" || Array.isArray(s)) s = {}; if (!s.env || typeof s.env !== "object" || Array.isArray(s.env)) s.env = {}; delete s.env.ANTHROPIC_API_KEY; s.model = e.NODULE_MODEL; Object.assign(s.env, { ANTHROPIC_BASE_URL: e.NODULE_URL, ANTHROPIC_AUTH_TOKEN: e.NODULE_KEY, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1", CLAUDE_CODE_ATTRIBUTION_HEADER: "0" }); fs.writeFileSync(e.NODULE_FILE, JSON.stringify(s, null, 2) + "\n", { mode: 0o600 }); '; then MERGED=1; fi fi if [ "$MERGED" != "1" ]; then if [ -s "$F" ]; then NOTE=" Прежний ~/.claude/settings.json прочитать не удалось, записан новый; прежний лежит в копии."; fi umask 077 printf '{\n "model": "%s",\n "env": {\n "ANTHROPIC_BASE_URL": "%s",\n "ANTHROPIC_AUTH_TOKEN": "%s",\n "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",\n "CLAUDE_CODE_ATTRIBUTION_HEADER": "0"\n }\n}\n' \ "$CLAUDE_MODEL" "$URL" "$KEY" > "$F" fi chmod 600 "$F" DONE="$DONE claude" fi if [ -n "$CODEX_MODEL" ]; then mkdir -p "$HOME/.codex" C="$HOME/.codex/config.toml" A="$HOME/.codex/auth.json" backup "$C" backup "$A" umask 077 { printf 'model_provider = "nodule"\nmodel = "%s"\nmodel_reasoning_effort = "high"\ndisable_response_storage = true\n' "$CODEX_MODEL" if [ -f "$C" ]; then awk ' /^[ \t]*\[/ { table = 1; skip = ($0 ~ /^[ \t]*\[model_providers\.nodule\][ \t]*(#.*)?$/) } skip { next } !table && /^[ \t]*(model_provider|model|model_reasoning_effort|disable_response_storage)[ \t]*=/ { next } { print } ' "$C" fi printf '\n[model_providers.nodule]\nname = "Nodule"\nbase_url = "%s"\nwire_api = "responses"\nrequires_openai_auth = true\nsupports_websockets = false\n' "$URL" } > "$C.nodule-tmp" mv "$C.nodule-tmp" "$C" printf '{\n "OPENAI_API_KEY": "%s"\n}\n' "$KEY" > "$A" chmod 600 "$C" "$A" DONE="$DONE codex" fi [ -n "$DONE" ] || fail "по этому ключу нет моделей Claude или GPT для Claude Code и Codex" CHECK="$(printf 'header = "Authorization: Bearer %s"\n' "$KEY" | curl -sS -o /dev/null -w '%{http_code}' -K - "$URL/v1/models" || true)" say "" say "Готово, Nodule подключён." case "$DONE" in *claude*) say " Claude Code: ~/.claude/settings.json, модель $CLAUDE_MODEL" if command -v claude >/dev/null 2>&1; then say " запустите в папке проекта: claude" else say " Claude Code не найден, установите: npm install -g @anthropic-ai/claude-code"; fi ;; esac case "$DONE" in *codex*) say " Codex: ~/.codex/config.toml и auth.json, модель $CODEX_MODEL" if command -v codex >/dev/null 2>&1; then say " запустите в папке проекта: codex" else say " Codex не найден, установите: npm install -g @openai/codex"; fi ;; esac if [ "$CHECK" = "200" ]; then say " Проверка ключа: работает." else say " Проверка ключа не прошла (ответ $CHECK). Откройте $BASE/setup, там подскажем."; fi if [ -n "$BACKUPS" ]; then say " Старые настройки сохранены:$BACKUPS"; fi if [ -n "$NOTE" ]; then say "$NOTE"; fi say " Баланс и расход: $BASE/usage"