#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CLAUDE_DIR="$HOME/.claude" SETTINGS="$CLAUDE_DIR/settings.json" GATEWAY="$CLAUDE_DIR/iop-claude-gateway.py" DEV_CORP_KEY_HELPER="$CLAUDE_DIR/dev-corp-openai-key.sh" USE_MODEL="$CLAUDE_DIR/use-model.sh" LABEL="com.iop.agent-client.claude" PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" PYTHON_BIN="/usr/bin/python3" UID_VALUE="$(id -u)" GATEWAY_PORT="${IOP_CLAUDE_GATEWAY_PORT:-18444}" if [[ ! -x "$PYTHON_BIN" ]]; then PYTHON_BIN="$(command -v python3 || true)" fi if [[ -z "$PYTHON_BIN" || ! -x "$PYTHON_BIN" ]]; then echo "python3 를 찾을 수 없습니다." exit 1 fi if [[ ! -x "$CLAUDE_DIR/anthropic_key.sh" ]]; then echo "~/.claude/anthropic_key.sh 설정이 먼저 필요합니다." exit 1 fi mkdir -p "$CLAUDE_DIR/cache" "$HOME/Library/LaunchAgents" OLD_LABELS=( "$LABEL" "com.leedongmyung.dev-corp-claude-gateway" "com.seulgivibe.claude.models" "com.seulgivibe.claude.anthropic-proxy" "com.leedongmyung.claude.seulgivibe-anthropic-proxy" ) for old_label in "${OLD_LABELS[@]}"; do launchctl bootout "gui/$UID_VALUE/$old_label" >/dev/null 2>&1 || true done OLD_PLISTS=( "$PLIST" "$HOME/Library/LaunchAgents/com.leedongmyung.dev-corp-claude-gateway.plist" "$HOME/Library/LaunchAgents/com.seulgivibe.claude.models.plist" "$HOME/Library/LaunchAgents/com.seulgivibe.claude.anthropic-proxy.plist" "$HOME/Library/LaunchAgents/com.leedongmyung.claude.seulgivibe-anthropic-proxy.plist" ) for old_plist in "${OLD_PLISTS[@]}"; do rm -f "$old_plist" done for old_process in \ "$CLAUDE_DIR/iop-claude-gateway.py" \ "$CLAUDE_DIR/seulgivibe-claude-gateway.py" \ "$CLAUDE_DIR/dev-corp-claude-gateway.py" \ "$CLAUDE_DIR/seulgivibe-anthropic-proxy.mjs" do pids="$(pgrep -f "$old_process" || true)" if [[ -n "$pids" ]]; then while IFS= read -r pid; do [[ -n "$pid" && "$pid" != "$$" ]] && kill "$pid" >/dev/null 2>&1 || true done <<< "$pids" fi done rm -f "$CLAUDE_DIR/seulgivibe-anthropic-proxy.mjs" install -m 755 "$SCRIPT_DIR/iop-claude-gateway.py" "$GATEWAY" if [[ ! -x "$DEV_CORP_KEY_HELPER" ]]; then cat > "$DEV_CORP_KEY_HELPER" <<'EOF' #!/bin/sh set -eu if [ -f "$HOME/.claude/dev-corp-openai-api-key" ]; then tr -d '\n' < "$HOME/.claude/dev-corp-openai-api-key" exit 0 fi if [ -f "$HOME/.pi/agent/dev-corp-openai-api-key" ]; then tr -d '\n' < "$HOME/.pi/agent/dev-corp-openai-api-key" exit 0 fi exec "$HOME/.claude/anthropic_key.sh" EOF chmod 755 "$DEV_CORP_KEY_HELPER" fi "$PYTHON_BIN" - "$SETTINGS" "$CLAUDE_DIR" "$GATEWAY_PORT" <<'PY' import json import pathlib import sys import time settings_path = pathlib.Path(sys.argv[1]) claude_dir = pathlib.Path(sys.argv[2]) gateway_port = sys.argv[3] settings_path.parent.mkdir(parents=True, exist_ok=True) settings = {} if settings_path.exists(): backup = settings_path.with_name(f"{settings_path.name}.bak-{time.strftime('%Y%m%d%H%M%S')}") backup.write_bytes(settings_path.read_bytes()) try: settings = json.loads(settings_path.read_text() or "{}") except Exception: settings = {} base_url = f"http://127.0.0.1:{gateway_port}/anthropic" common_env = { "ANTHROPIC_BASE_URL": base_url, "NODE_TLS_REJECT_UNAUTHORIZED": "0", "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1", } removed_env_keys = [ "ANTHROPIC_CUSTOM_MODEL_OPTION", "ANTHROPIC_CUSTOM_MODEL_OPTION_NAME", "ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION", "ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES", "ANTHROPIC_DEFAULT_FABLE_MODEL", "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME", "ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION", ] model_aliases = { "claude-seulgivibe-sonnet-4-5": "sonnet", "claude-seulgivibe-opus-4-8": "opus", "gemma4:26b": "claude-dev-corp-gemma4-26b", "ornith35b": "claude-dev-corp-ornith35b", "ornith:35b": "claude-dev-corp-ornith35b", } def normalize_settings(value, model=None): output = dict(value or {}) output["apiKeyHelper"] = output.get("apiKeyHelper") or "~/.claude/anthropic_key.sh" env = dict(output.get("env") or {}) for key in removed_env_keys: env.pop(key, None) env.update(common_env) output["env"] = env output.pop("availableModels", None) output.pop("enforceAvailableModels", None) if model is not None: output["model"] = model elif output.get("model") in model_aliases: output["model"] = model_aliases[output["model"]] output.setdefault("permissions", {}) output["permissions"].setdefault("defaultMode", "bypassPermissions") output["permissions"].setdefault("skipDangerousModePermissionPrompt", True) output.setdefault("effortLevel", "xhigh") output.setdefault("alwaysThinkingEnabled", True) output.setdefault("skipDangerousModePermissionPrompt", True) return output settings = normalize_settings(settings) settings_path.write_text(json.dumps(settings, indent=2, ensure_ascii=False) + "\n") profiles = { "settings.dev-corp.json": "claude-dev-corp-gemma4-26b", "settings.ornith35b.json": "claude-dev-corp-ornith35b", "settings.fable.json": "claude-seulgivibe-mythos-5", "settings.sonnet-4-5.json": "sonnet", "settings.opus-4-8.json": "opus", } for filename, model in profiles.items(): profile_path = claude_dir / filename profile = {} if profile_path.exists(): try: profile = json.loads(profile_path.read_text() or "{}") except Exception: profile = {} profile = normalize_settings(profile, model=model) profile_path.write_text(json.dumps(profile, indent=2, ensure_ascii=False) + "\n") PY cat > "$USE_MODEL" <<'EOF' #!/bin/sh set -eu profile="${1:-}" case "$profile" in dev-corp|devcorp|gemma|gemma4) source_file="$HOME/.claude/settings.dev-corp.json" ;; ornith|ornith35b) source_file="$HOME/.claude/settings.ornith35b.json" ;; fable) source_file="$HOME/.claude/settings.fable.json" ;; sonnet|sonnet-4-5) source_file="$HOME/.claude/settings.sonnet-4-5.json" ;; opus|opus-4-8) source_file="$HOME/.claude/settings.opus-4-8.json" ;; *) echo "Usage: $0 dev-corp|ornith|fable|sonnet|opus" >&2 exit 2 ;; esac mkdir -p "$HOME/.claude/backups" if [ -f "$HOME/.claude/settings.json" ]; then cp "$HOME/.claude/settings.json" "$HOME/.claude/backups/settings.json.$(date +%Y%m%d-%H%M%S)" fi cp "$source_file" "$HOME/.claude/settings.json" if command -v jq >/dev/null 2>&1; then jq -r '"Claude Code model set to " + .model + " / effort=" + (.effortLevel // "default")' "$HOME/.claude/settings.json" else echo "Claude Code model profile applied: $profile" fi EOF chmod 755 "$USE_MODEL" rm -f "$CLAUDE_DIR/cache/gateway-models.json" cat > "$PLIST" < Label $LABEL ProgramArguments $PYTHON_BIN $GATEWAY EnvironmentVariables IOP_CLAUDE_GATEWAY_PORT $GATEWAY_PORT PATH /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin RunAtLoad KeepAlive StandardOutPath $CLAUDE_DIR/iop-claude-gateway.out.log StandardErrorPath $CLAUDE_DIR/iop-claude-gateway.err.log EOF launchctl bootstrap "gui/$UID_VALUE" "$PLIST" launchctl kickstart -k "gui/$UID_VALUE/$LABEL" for _ in 1 2 3 4 5; do curl -fsS "http://127.0.0.1:$GATEWAY_PORT/health" >/dev/null 2>&1 && break sleep 1 done curl -fsS "http://127.0.0.1:$GATEWAY_PORT/anthropic/v1/models" >/dev/null echo "완료: Claude TUI를 새로 열고 /model 에서 모델을 선택하세요."