#!/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 -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 for cmd in docker python3 curl systemctl; do if command -v "$cmd" &>/dev/null; then success "$cmd found" else error "$cmd not found" 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" MISSING=$((MISSING + 1)) fi # Check inotifywait if command -v inotifywait &>/dev/null; then success "inotifywait found" USE_INOTIFY=true else warn "inotifywait not found (inotify-tools package)" echo -e " ${DIM}Install with: apt install inotify-tools${NC}" if confirm " Install inotify-tools now?" "Y"; then apt-get install -y inotify-tools 2>&1 | tail -1 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: Detect Claude CLI Credentials # ============================================================================ header "Step 3: Detecting Claude CLI Credentials" CLAUDE_CREDS_FILE="" # Strategy 1: Find claude-proxy or similar container 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" # Get its volume mounts and find credentials 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 if [ -z "$CLAUDE_CREDS_FILE" ] || [ ! -f "$CLAUDE_CREDS_FILE" ]; then warn "Could not auto-detect Claude CLI credentials" CLAUDE_CREDS_FILE=$(ask "Enter path to Claude CLI .credentials.json") if [ ! -f "$CLAUDE_CREDS_FILE" ]; then error "File not found: $CLAUDE_CREDS_FILE" exit 1 fi fi # Validate credentials 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 ""