openclaw_oauth_sync/setup.sh
shamid202 32a4e739dc Improve wizard UX: CLI install/sign-in flow, user-controlled installs, ROOH credit
- Add ROOH credit (www.rooh.red) to banner, summary, and README
- Step 1: Offer to install python3 and curl instead of hard-failing
- Step 3: Detect missing Claude CLI and offer to install via npm
- Step 3: Detect not-signed-in CLI and offer interactive OAuth sign-in
- Step 3: Provide clear instructions and exit paths at every decision point
- Update README with correct git clone URL and wizard capabilities
- All install steps now require user confirmation before proceeding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 02:08:50 +07:00

1070 lines
39 KiB
Bash
Executable File

#!/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 ""