#!/bin/bash # ============================================================================ # OAuth Fix for OpenClaw — Interactive Setup Wizard # ============================================================================ # Configures automatic Anthropic OAuth token refresh for OpenClaw. # Detects paths, installs the sync service, and configures the Anthropic model. # # Usage: # ./setup.sh # Interactive mode # ./setup.sh --uninstall # Remove everything # ============================================================================ set -euo pipefail VERSION="1.0.0" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # --- Colors --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } header() { echo -e "\n${BOLD}${CYAN}━━━ $* ━━━${NC}\n"; } ask() { local prompt="$1" local default="${2:-}" local result if [ -n "$default" ]; then read -rp "$(echo -e "${BOLD}$prompt${NC} [$default]: ")" result echo "${result:-$default}" else read -rp "$(echo -e "${BOLD}$prompt${NC}: ")" result echo "$result" fi } confirm() { local prompt="$1" local default="${2:-Y}" local result read -rp "$(echo -e "${BOLD}$prompt${NC} [${default}]: ")" result result="${result:-$default}" [[ "$result" =~ ^[Yy] ]] } # --- Uninstall --- if [ "${1:-}" = "--uninstall" ]; then bash "$SCRIPT_DIR/scripts/uninstall.sh" exit $? fi # ============================================================================ # BANNER # ============================================================================ echo "" echo -e "${BOLD}${CYAN}" echo " ╔══════════════════════════════════════════════════════╗" echo " ║ OAuth Fix for OpenClaw + Claude Max ║" echo " ║ Automatic Anthropic Token Refresh ║" echo " ║ v${VERSION} ║" echo " ║ ║" echo " ║ Created by ROOH — www.rooh.red ║" echo " ╚══════════════════════════════════════════════════════╝" echo -e "${NC}" echo -e "${DIM} Keeps your Anthropic OAuth tokens fresh by syncing" echo -e " Claude Code CLI's auto-refreshed credentials to OpenClaw.${NC}" echo "" # ============================================================================ # STEP 1: Prerequisites # ============================================================================ header "Step 1: Checking Prerequisites" MISSING=0 # --- Required (cannot be installed by wizard) --- for cmd in systemctl docker; do if command -v "$cmd" &>/dev/null; then success "$cmd found" else error "$cmd not found — required, cannot be installed by this wizard" MISSING=$((MISSING + 1)) fi done # Check docker compose (v2) if docker compose version &>/dev/null; then success "docker compose found ($(docker compose version --short 2>/dev/null || echo 'v2'))" else error "docker compose v2 not found — required" MISSING=$((MISSING + 1)) fi # --- python3 (offer to install) --- if command -v python3 &>/dev/null; then success "python3 found" else warn "python3 not found (required for JSON processing)" if confirm " Install python3 now?" "Y"; then apt-get install -y python3 2>&1 | tail -3 if command -v python3 &>/dev/null; then success "python3 installed" else error "python3 installation failed" MISSING=$((MISSING + 1)) fi else error "python3 is required — install it manually and re-run" MISSING=$((MISSING + 1)) fi fi # --- curl (offer to install) --- if command -v curl &>/dev/null; then success "curl found" else warn "curl not found" if confirm " Install curl now?" "Y"; then apt-get install -y curl 2>&1 | tail -3 if command -v curl &>/dev/null; then success "curl installed" else error "curl installation failed" MISSING=$((MISSING + 1)) fi else error "curl is required — install it manually and re-run" MISSING=$((MISSING + 1)) fi fi # --- inotifywait (offer to install, optional) --- if command -v inotifywait &>/dev/null; then success "inotifywait found" USE_INOTIFY=true else warn "inotifywait not found (inotify-tools package)" echo -e " ${DIM}Provides real-time credential sync. Without it, a 6-hour timer is used instead.${NC}" if confirm " Install inotify-tools now?" "Y"; then apt-get install -y inotify-tools 2>&1 | tail -3 if command -v inotifywait &>/dev/null; then success "inotifywait installed" USE_INOTIFY=true else warn "Installation failed. Will use timer-based fallback." USE_INOTIFY=false fi else warn "Will use timer-based fallback (6-hour refresh cycle)" USE_INOTIFY=false fi fi if [ "$MISSING" -gt 0 ]; then error "$MISSING prerequisite(s) missing. Please install them and re-run." exit 1 fi # ============================================================================ # STEP 2: Detect OpenClaw Installation # ============================================================================ header "Step 2: Detecting OpenClaw Installation" # Find openclaw.json OPENCLAW_CONFIG_DIR="" for path in \ "${OPENCLAW_CONFIG_DIR:-}" \ /root/.openclaw \ /home/*/.openclaw; do if [ -f "$path/openclaw.json" ] 2>/dev/null; then OPENCLAW_CONFIG_DIR="$path" break fi done if [ -z "$OPENCLAW_CONFIG_DIR" ]; then # Try docker inspect GATEWAY=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i 'openclaw.*gateway' | head -1) if [ -n "$GATEWAY" ]; then OPENCLAW_CONFIG_DIR=$(docker inspect "$GATEWAY" --format '{{range .Mounts}}{{if eq .Destination "/home/node/.openclaw"}}{{.Source}}{{end}}{{end}}' 2>/dev/null) fi fi if [ -z "$OPENCLAW_CONFIG_DIR" ] || [ ! -f "$OPENCLAW_CONFIG_DIR/openclaw.json" ]; then error "Cannot find OpenClaw installation" OPENCLAW_CONFIG_DIR=$(ask "Enter OpenClaw config directory (contains openclaw.json)") if [ ! -f "$OPENCLAW_CONFIG_DIR/openclaw.json" ]; then error "openclaw.json not found in $OPENCLAW_CONFIG_DIR" exit 1 fi fi success "Config: $OPENCLAW_CONFIG_DIR/openclaw.json" # Find docker-compose directory COMPOSE_DIR="" for path in \ /root/openclaw \ "$(dirname "$OPENCLAW_CONFIG_DIR")/openclaw" \ /opt/openclaw; do if [ -f "$path/docker-compose.yml" ] || [ -f "$path/docker-compose.yaml" ] || [ -f "$path/compose.yml" ]; then COMPOSE_DIR="$path" break fi done if [ -z "$COMPOSE_DIR" ]; then COMPOSE_DIR=$(ask "Enter OpenClaw docker-compose directory" "/root/openclaw") fi success "Compose: $COMPOSE_DIR" # Find .env file ENV_FILE="$COMPOSE_DIR/.env" if [ ! -f "$ENV_FILE" ]; then warn ".env not found at $ENV_FILE" ENV_FILE=$(ask "Enter .env file path" "$COMPOSE_DIR/.env") fi success "Env: $ENV_FILE" # Find gateway container GATEWAY_CONTAINER=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i 'openclaw.*gateway' | head -1) if [ -z "$GATEWAY_CONTAINER" ]; then GATEWAY_CONTAINER=$(ask "Enter gateway container name" "openclaw-openclaw-gateway-1") fi success "Gateway: $GATEWAY_CONTAINER" # Derive oauth.json path OPENCLAW_OAUTH_FILE="$OPENCLAW_CONFIG_DIR/credentials/oauth.json" success "OAuth file: $OPENCLAW_OAUTH_FILE" # ============================================================================ # STEP 3: Claude CLI & Credentials # ============================================================================ header "Step 3: Claude CLI & Credentials" CLAUDE_CREDS_FILE="" EARLY_CLI_MODE="" EARLY_CLI_CONTAINER="" CLI_INSTALLED=false # --- Phase 1: Search for existing credentials --- info "Searching for Claude CLI credentials..." # Strategy 1: Find claude container and check mounts CLAUDE_CONTAINER=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i 'claude' | head -1) if [ -n "$CLAUDE_CONTAINER" ]; then info "Found Claude container: $CLAUDE_CONTAINER" MOUNT_SOURCE=$(docker inspect "$CLAUDE_CONTAINER" --format '{{range .Mounts}}{{if or (eq .Destination "/root") (eq .Destination "/root/.claude") (eq .Destination "/home/node/.claude")}}{{.Source}}{{end}}{{end}}' 2>/dev/null) if [ -n "$MOUNT_SOURCE" ]; then for suffix in "/.claude/.credentials.json" "/.credentials.json"; do if [ -f "${MOUNT_SOURCE}${suffix}" ]; then CLAUDE_CREDS_FILE="${MOUNT_SOURCE}${suffix}" break fi done fi fi # Strategy 2: Search workspace directories if [ -z "$CLAUDE_CREDS_FILE" ]; then for path in "$OPENCLAW_CONFIG_DIR"/workspaces/*/config/.claude/.credentials.json; do if [ -f "$path" ]; then CLAUDE_CREDS_FILE="$path" break fi done fi # Strategy 3: Direct paths if [ -z "$CLAUDE_CREDS_FILE" ]; then for path in \ /root/.claude/.credentials.json \ "$HOME/.claude/.credentials.json"; do if [ -f "$path" ]; then CLAUDE_CREDS_FILE="$path" break fi done fi # --- Phase 2: If no credentials found, detect CLI and help user --- if [ -z "$CLAUDE_CREDS_FILE" ] || [ ! -f "$CLAUDE_CREDS_FILE" ]; then warn "No Claude CLI credentials found" echo "" # Detect if Claude CLI is installed if [ -n "$CLAUDE_CONTAINER" ] && docker exec "$CLAUDE_CONTAINER" which claude &>/dev/null; then CLI_INSTALLED=true EARLY_CLI_MODE="container" EARLY_CLI_CONTAINER="$CLAUDE_CONTAINER" info "Claude Code CLI found in container: $CLAUDE_CONTAINER" elif command -v claude &>/dev/null; then CLI_INSTALLED=true EARLY_CLI_MODE="host" info "Claude Code CLI found on host: $(which claude)" fi # --- CLI not installed: offer to install --- if ! $CLI_INSTALLED; then echo -e " ${BOLD}Claude Code CLI is not installed.${NC}" echo " It's required for automatic OAuth token refresh." echo "" echo " Options:" echo " 1) Install Claude Code CLI now (requires npm/Node.js)" echo " 2) Show me the install instructions (exit wizard)" echo " 3) Skip — I'll provide the credentials path manually" CHOICE=$(ask "Select" "1") case "$CHOICE" in 1) if command -v npm &>/dev/null; then info "Installing Claude Code CLI via npm..." npm install -g @anthropic-ai/claude-code 2>&1 | tail -5 if command -v claude &>/dev/null; then success "Claude Code CLI installed successfully" CLI_INSTALLED=true EARLY_CLI_MODE="host" else error "Installation failed. Check npm/Node.js setup." fi else warn "npm not found. Node.js is required to install Claude Code CLI." echo "" echo " Install Node.js first:" echo " curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -" echo " apt install -y nodejs" echo "" if confirm " Install Node.js + Claude Code CLI now?" "N"; then info "Installing Node.js LTS..." curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - 2>&1 | tail -3 apt-get install -y nodejs 2>&1 | tail -3 if command -v npm &>/dev/null; then info "Installing Claude Code CLI..." npm install -g @anthropic-ai/claude-code 2>&1 | tail -5 if command -v claude &>/dev/null; then success "Claude Code CLI installed successfully" CLI_INSTALLED=true EARLY_CLI_MODE="host" else error "Claude CLI installation failed" fi else error "Node.js installation failed" fi fi fi ;; 2) echo "" info "Install Claude Code CLI:" echo "" echo " # Option A: npm (recommended)" echo " npm install -g @anthropic-ai/claude-code" echo "" echo " # Option B: Run directly with npx" echo " npx @anthropic-ai/claude-code" echo "" echo " # More info:" echo " https://docs.anthropic.com/en/docs/claude-code" echo "" info "After installing and signing in, re-run: sudo ./setup.sh" exit 0 ;; 3) info "Continuing without Claude CLI..." ;; esac fi # --- CLI installed but no credentials: offer sign-in --- if $CLI_INSTALLED && ([ -z "$CLAUDE_CREDS_FILE" ] || [ ! -f "$CLAUDE_CREDS_FILE" ]); then echo "" warn "Claude Code CLI is installed but not signed in (no OAuth credentials)." info "You need to authenticate with your Claude Max subscription." info "This will open an OAuth flow — you'll get a URL to visit in your browser." echo "" if confirm " Launch Claude CLI now to sign in?" "Y"; then echo "" echo -e " ${BOLD}Complete the OAuth sign-in in your browser.${NC}" echo -e " ${BOLD}After signing in, press Ctrl+C or type /exit to return here.${NC}" echo "" if [ "$EARLY_CLI_MODE" = "container" ]; then docker exec -it "$EARLY_CLI_CONTAINER" claude || true else claude || true fi echo "" info "Checking for credentials after sign-in..." # Re-search for credentials for path in \ /root/.claude/.credentials.json \ "$HOME/.claude/.credentials.json"; do if [ -f "$path" ]; then CLAUDE_CREDS_FILE="$path" break fi done # Also check container mounts again if [ -z "$CLAUDE_CREDS_FILE" ] && [ -n "$CLAUDE_CONTAINER" ]; then MOUNT_SOURCE=$(docker inspect "$CLAUDE_CONTAINER" --format '{{range .Mounts}}{{if or (eq .Destination "/root") (eq .Destination "/root/.claude") (eq .Destination "/home/node/.claude")}}{{.Source}}{{end}}{{end}}' 2>/dev/null) if [ -n "$MOUNT_SOURCE" ]; then for suffix in "/.claude/.credentials.json" "/.credentials.json"; do if [ -f "${MOUNT_SOURCE}${suffix}" ]; then CLAUDE_CREDS_FILE="${MOUNT_SOURCE}${suffix}" break fi done fi fi fi fi # --- Last resort: ask for manual path --- if [ -z "$CLAUDE_CREDS_FILE" ] || [ ! -f "$CLAUDE_CREDS_FILE" ]; then echo "" warn "Could not find credentials automatically." CLAUDE_CREDS_FILE=$(ask "Enter path to .credentials.json (or 'quit' to exit)") if [ "$CLAUDE_CREDS_FILE" = "quit" ] || [ "$CLAUDE_CREDS_FILE" = "q" ]; then echo "" info "To set up credentials:" info " 1. Install Claude Code CLI: npm install -g @anthropic-ai/claude-code" info " 2. Sign in: claude" info " 3. Re-run this wizard: sudo ./setup.sh" exit 1 fi if [ ! -f "$CLAUDE_CREDS_FILE" ]; then error "File not found: $CLAUDE_CREDS_FILE" exit 1 fi fi fi # --- Phase 3: Validate credentials --- HAS_OAUTH=$(python3 -c " import json with open('$CLAUDE_CREDS_FILE') as f: d = json.load(f) oauth = d.get('claudeAiOauth', {}) print('yes' if oauth.get('accessToken') else 'no') " 2>/dev/null || echo "no") if [ "$HAS_OAUTH" != "yes" ]; then error "Credentials file exists but contains no OAuth tokens." info "Claude CLI may not be signed in with a Claude Max subscription." echo "" if $CLI_INSTALLED; then info "Sign in to Claude CLI and re-run this wizard:" info " claude # sign in with OAuth" info " sudo ./setup.sh # re-run wizard" else info "Install Claude Code CLI, sign in, and re-run:" info " npm install -g @anthropic-ai/claude-code" info " claude # sign in with OAuth" info " sudo ./setup.sh # re-run wizard" fi exit 1 fi TOKEN_INFO=$(python3 -c " import json, time with open('$CLAUDE_CREDS_FILE') as f: d = json.load(f) oauth = d.get('claudeAiOauth', {}) access = oauth.get('accessToken', '') refresh = oauth.get('refreshToken', '') expires = oauth.get('expiresAt', 0) remaining = (expires / 1000 - time.time()) / 3600 status = 'VALID' if remaining > 0 else 'EXPIRED' print(f'access={access[:20]}... refresh={\"yes\" if refresh else \"no\"} remaining={remaining:.1f}h status={status}') " 2>/dev/null || echo "error") if echo "$TOKEN_INFO" | grep -q "error"; then error "Cannot parse credentials file. Is it a valid Claude CLI .credentials.json?" exit 1 fi success "Credentials: $CLAUDE_CREDS_FILE" info " $TOKEN_INFO" CURRENT_TOKEN=$(python3 -c " import json with open('$CLAUDE_CREDS_FILE') as f: d = json.load(f) print(d['claudeAiOauth']['accessToken']) " 2>/dev/null) # ============================================================================ # STEP 4: Confirm Configuration # ============================================================================ header "Step 4: Configuration Summary" echo -e " ${BOLD}OpenClaw config:${NC} $OPENCLAW_CONFIG_DIR" echo -e " ${BOLD}Docker compose:${NC} $COMPOSE_DIR" echo -e " ${BOLD}Environment file:${NC} $ENV_FILE" echo -e " ${BOLD}Gateway container:${NC} $GATEWAY_CONTAINER" echo -e " ${BOLD}OAuth file:${NC} $OPENCLAW_OAUTH_FILE" echo -e " ${BOLD}Claude CLI creds:${NC} $CLAUDE_CREDS_FILE" echo -e " ${BOLD}Sync method:${NC} $([ "$USE_INOTIFY" = true ] && echo 'inotifywait (real-time)' || echo 'systemd timer (every 6h)')" echo "" if ! confirm "Proceed with these settings?" "Y"; then echo "Aborted." exit 0 fi # ============================================================================ # STEP 5: Configure Anthropic Model in OpenClaw # ============================================================================ header "Step 5: Configuring Anthropic Model" # Check if anthropic model is already configured HAS_ANTHROPIC=$(python3 -c " import json with open('$OPENCLAW_CONFIG_DIR/openclaw.json') as f: d = json.load(f) primary = d.get('agents', {}).get('defaults', {}).get('model', {}).get('primary', '') print('yes' if 'anthropic/' in primary else 'no') " 2>/dev/null || echo "no") if [ "$HAS_ANTHROPIC" = "yes" ]; then success "Anthropic model already configured as primary" else info "Anthropic model not yet configured" if confirm " Set anthropic/claude-opus-4-6 as primary model?" "Y"; then python3 -c " import json with open('$OPENCLAW_CONFIG_DIR/openclaw.json') as f: d = json.load(f) # Set primary model if 'agents' not in d: d['agents'] = {} if 'defaults' not in d['agents']: d['agents']['defaults'] = {} if 'model' not in d['agents']['defaults']: d['agents']['defaults']['model'] = {} d['agents']['defaults']['model']['primary'] = 'anthropic/claude-opus-4-6' # Add fallback if not present fallbacks = d['agents']['defaults']['model'].get('fallbacks', []) if 'anthropic/claude-sonnet-4-6' not in fallbacks: fallbacks.insert(0, 'anthropic/claude-sonnet-4-6') d['agents']['defaults']['model']['fallbacks'] = fallbacks # Add model aliases if 'models' not in d['agents']['defaults']: d['agents']['defaults']['models'] = {} d['agents']['defaults']['models']['anthropic/claude-opus-4-6'] = {'alias': 'Claude Opus 4.6 (Max)'} d['agents']['defaults']['models']['anthropic/claude-sonnet-4-6'] = {'alias': 'Claude Sonnet 4.6 (Max)'} with open('$OPENCLAW_CONFIG_DIR/openclaw.json', 'w') as f: json.dump(d, f, indent=2) " success "Set anthropic/claude-opus-4-6 as primary model" success "Added anthropic/claude-sonnet-4-6 to fallbacks" success "Added model aliases" fi fi # Check for broken custom anthropic provider (common mistake) HAS_CUSTOM_PROVIDER=$(python3 -c " import json with open('$OPENCLAW_CONFIG_DIR/openclaw.json') as f: d = json.load(f) providers = d.get('models', {}).get('providers', {}) print('yes' if 'anthropic' in providers else 'no') " 2>/dev/null || echo "no") if [ "$HAS_CUSTOM_PROVIDER" = "yes" ]; then warn "Found custom 'anthropic' in models.providers — this causes 404 errors!" warn "The built-in provider handles Anthropic. Custom entry causes double /v1 in URLs." if confirm " Remove the custom anthropic provider entry?" "Y"; then python3 -c " import json with open('$OPENCLAW_CONFIG_DIR/openclaw.json') as f: d = json.load(f) del d['models']['providers']['anthropic'] with open('$OPENCLAW_CONFIG_DIR/openclaw.json', 'w') as f: json.dump(d, f, indent=2) " success "Removed custom anthropic provider" fi fi # Add ANTHROPIC_OAUTH_TOKEN to .env if missing if grep -q 'ANTHROPIC_OAUTH_TOKEN=' "$ENV_FILE" 2>/dev/null; then success "ANTHROPIC_OAUTH_TOKEN already in .env" else echo "ANTHROPIC_OAUTH_TOKEN=\"$CURRENT_TOKEN\"" >> "$ENV_FILE" success "Added ANTHROPIC_OAUTH_TOKEN to .env" fi # ============================================================================ # STEP 6: Fix/Create Auth Profiles # ============================================================================ header "Step 6: Configuring Auth Profiles" AGENTS_DIR="$OPENCLAW_CONFIG_DIR/agents" if [ -d "$AGENTS_DIR" ]; then for agent_dir in "$AGENTS_DIR"/*/agent; do [ -d "$agent_dir" ] || continue agent=$(basename "$(dirname "$agent_dir")") f="$agent_dir/auth-profiles.json" if [ ! -f "$f" ]; then # Create new auth-profiles.json python3 -c " import json data = { 'version': 1, 'profiles': { 'anthropic:default': { 'type': 'oauth', 'provider': 'anthropic', 'access': '$CURRENT_TOKEN' } }, 'lastGood': { 'anthropic': 'anthropic:default' }, 'usageStats': {} } with open('$f', 'w') as fh: json.dump(data, fh, indent=2) " success "$agent: created auth-profiles.json" else # Fix existing profile python3 -c " import json with open('$f') as fh: data = json.load(fh) changed = False if 'profiles' not in data: data['profiles'] = {} p = data['profiles'].get('anthropic:default', {}) if not p: data['profiles']['anthropic:default'] = { 'type': 'oauth', 'provider': 'anthropic', 'access': '$CURRENT_TOKEN' } changed = True else: if p.get('type') != 'oauth': p['type'] = 'oauth' changed = True if 'key' in p and 'access' not in p: p['access'] = p.pop('key') changed = True elif 'key' in p: del p['key'] changed = True if p.get('access') != '$CURRENT_TOKEN': p['access'] = '$CURRENT_TOKEN' changed = True # Clear cooldown stats = data.get('usageStats', {}).get('anthropic:default', {}) for k in ['cooldownUntil', 'errorCount', 'failureCounts', 'lastFailureAt']: if k in stats: del stats[k] changed = True if 'lastGood' not in data: data['lastGood'] = {} data['lastGood']['anthropic'] = 'anthropic:default' if changed: with open('$f', 'w') as fh: json.dump(data, fh, indent=2) print('fixed') else: print('ok') " 2>/dev/null RESULT=$(python3 -c " import json with open('$f') as fh: data = json.load(fh) p = data.get('profiles', {}).get('anthropic:default', {}) has_access = 'access' in p print(f'type={p.get(\"type\",\"none\")} access={\"yes\" if has_access else \"no\"}') " 2>/dev/null) success "$agent: $RESULT" fi done else warn "No agents directory found at $AGENTS_DIR" fi # ============================================================================ # STEP 7: Install Sync Script # ============================================================================ header "Step 7: Installing Sync Service" INSTALL_DIR="/usr/local/bin" if [ "$USE_INOTIFY" = true ]; then # Install inotify-based watcher SYNC_SCRIPT="$INSTALL_DIR/sync-oauth-token.sh" sed \ -e "s|@@CLAUDE_CREDS_FILE@@|$CLAUDE_CREDS_FILE|g" \ -e "s|@@OPENCLAW_OAUTH_FILE@@|$OPENCLAW_OAUTH_FILE|g" \ -e "s|@@OPENCLAW_ENV_FILE@@|$ENV_FILE|g" \ -e "s|@@COMPOSE_DIR@@|$COMPOSE_DIR|g" \ "$SCRIPT_DIR/scripts/sync-oauth-token.sh" > "$SYNC_SCRIPT" chmod +x "$SYNC_SCRIPT" success "Installed $SYNC_SCRIPT" # Install systemd service sed "s|@@SYNC_SCRIPT_PATH@@|$SYNC_SCRIPT|g" \ "$SCRIPT_DIR/templates/sync-oauth-token.service" > /etc/systemd/system/sync-oauth-token.service success "Installed systemd service" systemctl daemon-reload systemctl enable sync-oauth-token.service systemctl start sync-oauth-token.service success "Service started and enabled" else # Install timer-based refresh REFRESH_SCRIPT="$INSTALL_DIR/refresh-claude-token.sh" sed \ -e "s|@@CLAUDE_CREDS_FILE@@|$CLAUDE_CREDS_FILE|g" \ -e "s|@@OPENCLAW_OAUTH_FILE@@|$OPENCLAW_OAUTH_FILE|g" \ -e "s|@@OPENCLAW_ENV_FILE@@|$ENV_FILE|g" \ -e "s|@@COMPOSE_DIR@@|$COMPOSE_DIR|g" \ "$SCRIPT_DIR/scripts/refresh-claude-token.sh" > "$REFRESH_SCRIPT" chmod +x "$REFRESH_SCRIPT" success "Installed $REFRESH_SCRIPT" # Install systemd timer sed "s|@@REFRESH_SCRIPT_PATH@@|$REFRESH_SCRIPT|g" \ "$SCRIPT_DIR/templates/refresh-claude-token.service" > /etc/systemd/system/refresh-claude-token.service cp "$SCRIPT_DIR/templates/refresh-claude-token.timer" /etc/systemd/system/ success "Installed systemd timer" systemctl daemon-reload systemctl enable refresh-claude-token.timer systemctl start refresh-claude-token.timer success "Timer started and enabled (runs every 6 hours)" fi # ============================================================================ # STEP 8: Detect Claude CLI & Install Auto-Refresh Trigger # ============================================================================ header "Step 8: Detecting Claude CLI & Installing Auto-Refresh Trigger" info "The sync service watches for credential changes, but something must" info "trigger Claude CLI to actually refresh the token before it expires." info "This step installs a timer that triggers the refresh automatically." echo "" # Detect Claude CLI — container or host? CLI_MODE="" CLI_CONTAINER="" CLI_BASE_URL_OVERRIDE="false" # Strategy 1: Check for Claude container CLAUDE_CONTAINER_FOUND=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i 'claude' | head -1) if [ -n "$CLAUDE_CONTAINER_FOUND" ]; then # Verify claude binary exists inside if docker exec "$CLAUDE_CONTAINER_FOUND" which claude &>/dev/null; then CLI_MODE="container" CLI_CONTAINER="$CLAUDE_CONTAINER_FOUND" info "Found Claude CLI in container: $CLI_CONTAINER" # Check if ANTHROPIC_BASE_URL needs override CTR_BASE_URL=$(docker inspect "$CLI_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^ANTHROPIC_BASE_URL=' | cut -d= -f2-) if [ -n "$CTR_BASE_URL" ] && [ "$CTR_BASE_URL" != "https://api.anthropic.com" ]; then CLI_BASE_URL_OVERRIDE="true" info "Container has ANTHROPIC_BASE_URL=$CTR_BASE_URL" info "Will use temporary override for CLI invocations (does not affect container)" fi fi fi # Strategy 2: Check host if [ -z "$CLI_MODE" ]; then if command -v claude &>/dev/null; then CLI_MODE="host" info "Found Claude CLI on host: $(which claude)" fi fi # Strategy 3: Ask user if [ -z "$CLI_MODE" ]; then warn "Could not auto-detect Claude CLI" echo "" echo " Where is Claude CLI installed?" echo " 1) In a Docker container" echo " 2) Directly on this system" echo " 3) Skip (no auto-refresh trigger)" CHOICE=$(ask "Select" "3") case "$CHOICE" in 1) CLI_MODE="container" CLI_CONTAINER=$(ask "Enter container name" "claude-proxy") if ! docker exec "$CLI_CONTAINER" which claude &>/dev/null; then error "Claude CLI not found in container $CLI_CONTAINER" CLI_MODE="" else # Check base URL CTR_BASE_URL=$(docker inspect "$CLI_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^ANTHROPIC_BASE_URL=' | cut -d= -f2-) if [ -n "$CTR_BASE_URL" ] && [ "$CTR_BASE_URL" != "https://api.anthropic.com" ]; then CLI_BASE_URL_OVERRIDE="true" fi fi ;; 2) CLI_MODE="host" if ! command -v claude &>/dev/null; then error "Claude CLI not found on system" CLI_MODE="" fi ;; *) warn "Skipping auto-refresh trigger installation" ;; esac fi TRIGGER_INSTALLED=false if [ -n "$CLI_MODE" ]; then # Test Claude CLI invocation info "Testing Claude CLI invocation..." if [ "$CLI_MODE" = "container" ]; then if [ "$CLI_BASE_URL_OVERRIDE" = "true" ]; then TEST_CMD="docker exec -e ANTHROPIC_BASE_URL=https://api.anthropic.com $CLI_CONTAINER claude -p 'say ok' --no-session-persistence" TEST_OUTPUT=$(timeout 60 docker exec -e ANTHROPIC_BASE_URL=https://api.anthropic.com "$CLI_CONTAINER" claude -p "say ok" --no-session-persistence 2>&1) else TEST_CMD="docker exec $CLI_CONTAINER claude -p 'say ok' --no-session-persistence" TEST_OUTPUT=$(timeout 60 docker exec "$CLI_CONTAINER" claude -p "say ok" --no-session-persistence 2>&1) fi else TEST_CMD="claude -p 'say ok' --no-session-persistence" TEST_OUTPUT=$(timeout 60 claude -p "say ok" --no-session-persistence 2>&1) fi TEST_EXIT=$? if [ "$TEST_EXIT" -eq 0 ] && [ -n "$TEST_OUTPUT" ]; then success "CLI test passed: $TEST_OUTPUT" # Verify proxy/container env unchanged (if container mode) if [ "$CLI_MODE" = "container" ] && [ "$CLI_BASE_URL_OVERRIDE" = "true" ]; then VERIFY_URL=$(docker inspect "$CLI_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^ANTHROPIC_BASE_URL=' | cut -d= -f2-) if [ "$VERIFY_URL" = "$CTR_BASE_URL" ]; then success "Container env unchanged (temporary override confirmed)" else warn "Container env may have changed — check manually" fi fi # Install trigger script TRIGGER_SCRIPT="$INSTALL_DIR/trigger-claude-refresh.sh" REAUTH_FLAG="$COMPOSE_DIR/REAUTH_NEEDED" sed \ -e "s|@@CREDS_FILE@@|$CLAUDE_CREDS_FILE|g" \ -e "s|@@REAUTH_FLAG@@|$REAUTH_FLAG|g" \ -e "s|@@CLI_MODE@@|$CLI_MODE|g" \ -e "s|@@CLI_CONTAINER@@|$CLI_CONTAINER|g" \ -e "s|@@CLI_BASE_URL_OVERRIDE@@|$CLI_BASE_URL_OVERRIDE|g" \ "$SCRIPT_DIR/scripts/trigger-claude-refresh.sh" > "$TRIGGER_SCRIPT" chmod +x "$TRIGGER_SCRIPT" success "Installed $TRIGGER_SCRIPT" # Install systemd service and timer sed "s|@@TRIGGER_SCRIPT_PATH@@|$TRIGGER_SCRIPT|g" \ "$SCRIPT_DIR/templates/trigger-claude-refresh.service" > /etc/systemd/system/trigger-claude-refresh.service cp "$SCRIPT_DIR/templates/trigger-claude-refresh.timer" /etc/systemd/system/ success "Installed systemd timer" systemctl daemon-reload systemctl enable trigger-claude-refresh.timer systemctl start trigger-claude-refresh.timer success "Auto-refresh trigger timer started (runs every 30 minutes)" TRIGGER_INSTALLED=true # Run the trigger script once to verify info "Running trigger script for initial verification..." bash "$TRIGGER_SCRIPT" 2>&1 | while read -r line; do echo " $line"; done else error "CLI test failed (exit code: $TEST_EXIT)" if [ -n "$TEST_OUTPUT" ]; then error "Output: $TEST_OUTPUT" fi warn "Skipping auto-refresh trigger installation" warn "You can re-run the wizard later to retry" fi fi # ============================================================================ # STEP 9: Initial Sync # ============================================================================ header "Step 9: Running Initial Sync" # Create oauth.json mkdir -p "$(dirname "$OPENCLAW_OAUTH_FILE")" python3 -c " import json, time with open('$CLAUDE_CREDS_FILE') as f: src = json.load(f) oauth = src['claudeAiOauth'] openclaw = { 'anthropic': { 'access': oauth['accessToken'], 'refresh': oauth['refreshToken'], 'expires': oauth['expiresAt'], 'scopes': oauth.get('scopes', []), 'subscriptionType': oauth.get('subscriptionType', 'max'), 'rateLimitTier': oauth.get('rateLimitTier', 'default_claude_max_5x') } } with open('$OPENCLAW_OAUTH_FILE', 'w') as f: json.dump(openclaw, f) remaining = (oauth['expiresAt'] / 1000 - time.time()) / 3600 print(f'Token: {oauth[\"accessToken\"][:20]}... expires in {remaining:.1f}h') " success "Created $OPENCLAW_OAUTH_FILE" # Update .env token python3 -c " import json, re with open('$CLAUDE_CREDS_FILE') as f: token = json.load(f)['claudeAiOauth']['accessToken'] with open('$ENV_FILE') as f: env = f.read() env = re.sub(r'ANTHROPIC_OAUTH_TOKEN=.*', f'ANTHROPIC_OAUTH_TOKEN=\"{token}\"', env) with open('$ENV_FILE', 'w') as f: f.write(env) " success "Updated $ENV_FILE" # Recreate gateway info "Recreating gateway container..." cd "$COMPOSE_DIR" docker compose down openclaw-gateway 2>&1 | tail -2 docker compose up -d openclaw-gateway 2>&1 | tail -2 success "Gateway recreated with fresh token" # ============================================================================ # STEP 10: Verify # ============================================================================ header "Step 10: Verification" sleep 3 ERRORS=0 # Check service if [ "$USE_INOTIFY" = true ]; then if systemctl is-active --quiet sync-oauth-token.service; then success "sync-oauth-token.service is running" else error "Service not running" ERRORS=$((ERRORS + 1)) fi else if systemctl is-active --quiet refresh-claude-token.timer; then success "refresh-claude-token.timer is active" else error "Timer not active" ERRORS=$((ERRORS + 1)) fi fi # Check trigger timer if [ "$TRIGGER_INSTALLED" = true ]; then if systemctl is-active --quiet trigger-claude-refresh.timer; then success "trigger-claude-refresh.timer is active" else error "Trigger timer not active" ERRORS=$((ERRORS + 1)) fi fi # Check oauth.json if [ -f "$OPENCLAW_OAUTH_FILE" ]; then HAS_ACCESS=$(python3 -c " import json with open('$OPENCLAW_OAUTH_FILE') as f: d = json.load(f) print('yes' if d.get('anthropic', {}).get('access') else 'no') " 2>/dev/null) if [ "$HAS_ACCESS" = "yes" ]; then success "oauth.json has correct format" else error "oauth.json missing anthropic.access" ERRORS=$((ERRORS + 1)) fi else error "oauth.json not found" ERRORS=$((ERRORS + 1)) fi # Check gateway if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$GATEWAY_CONTAINER"; then success "Gateway container is running" CTR_TOKEN=$(docker exec "$GATEWAY_CONTAINER" printenv ANTHROPIC_OAUTH_TOKEN 2>/dev/null | cut -c1-20) if [ -n "$CTR_TOKEN" ]; then success "Container has ANTHROPIC_OAUTH_TOKEN: ${CTR_TOKEN}..." fi else error "Gateway container not running" ERRORS=$((ERRORS + 1)) fi # ============================================================================ # SUMMARY # ============================================================================ echo "" echo -e "${BOLD}${CYAN}" echo " ╔══════════════════════════════════════════════════════╗" if [ "$ERRORS" -eq 0 ]; then echo " ║ Setup Complete! ║" else echo " ║ Setup Complete (with $ERRORS warning(s)) ║" fi echo " ╚══════════════════════════════════════════════════════╝" echo -e "${NC}" echo -e " ${BOLD}Installed:${NC}" if [ "$USE_INOTIFY" = true ]; then echo " - sync-oauth-token.sh (real-time file watcher)" echo " - sync-oauth-token.service (systemd)" else echo " - refresh-claude-token.sh (direct API refresh)" echo " - refresh-claude-token.timer (every 6 hours)" fi if [ "$TRIGGER_INSTALLED" = true ]; then echo " - trigger-claude-refresh.sh (auto-refresh trigger)" echo " - trigger-claude-refresh.timer (every 30 minutes)" echo " - Claude CLI mode: $CLI_MODE$([ "$CLI_MODE" = "container" ] && echo " ($CLI_CONTAINER)")" fi echo " - Anthropic model configured in openclaw.json" echo " - Auth profiles updated for all agents" echo " - oauth.json created with fresh token" echo "" echo -e " ${BOLD}Useful commands:${NC}" if [ "$USE_INOTIFY" = true ]; then echo " journalctl -u sync-oauth-token.service -f # Watch sync logs" echo " systemctl restart sync-oauth-token.service # Force re-sync" else echo " journalctl -u refresh-claude-token.service # View last refresh" echo " systemctl list-timers refresh-claude-token* # Check timer" fi if [ "$TRIGGER_INSTALLED" = true ]; then echo " journalctl -u trigger-claude-refresh -n 20 # Trigger logs" echo " systemctl list-timers trigger-claude-refresh* # Check trigger timer" fi echo " ./scripts/verify.sh # Health check" echo " ./setup.sh --uninstall # Remove everything" echo "" echo -e " ${DIM}Created by ROOH — www.rooh.red${NC}" echo ""