Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -795,6 +795,21 @@
"region": "zrh"
},
"notes": "API-centric cloud platform with regions in ZRH (Zurich), WDC (Washington DC), and LVS (Las Vegas). Granular resource control (CPU/RAM/Disk independently configurable). Uses 'cloudsigma' user for SSH."
},
"webdock": {
"name": "Webdock",
"description": "European VPS provider with affordable plans",
"url": "https://webdock.io/",
"type": "api",
"auth": "WEBDOCK_API_TOKEN",
"provision_method": "POST /v1/servers",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"profile": "webdockmicro",
"location": "fi",
"image": "ubuntu2404"
}
}
},
"matrix": {
Expand DownExpand Up@@ -1337,6 +1352,21 @@
"cloudsigma/opencode": "missing",
"cloudsigma/plandex": "missing",
"cloudsigma/kilocode": "missing",
"cloudsigma/continue": "implemented"
"cloudsigma/continue": "implemented",
"webdock/claude": "implemented",
"webdock/openclaw": "missing",
"webdock/nanoclaw": "missing",
"webdock/aider": "implemented",
"webdock/goose": "missing",
"webdock/codex": "missing",
"webdock/interpreter": "missing",
"webdock/gemini": "missing",
"webdock/amazonq": "missing",
"webdock/cline": "implemented",
"webdock/gptme": "missing",
"webdock/opencode": "missing",
"webdock/plandex": "missing",
"webdock/kilocode": "missing",
"webdock/continue": "missing"
}
}
}
2 changes: 2 additions & 0 deletions test/mock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -263,6 +263,7 @@ _strip_api_base() {
https://cloudapi.atlantic.net/*) ENDPOINT=$(echo "$URL" | sed 's|https://cloudapi.atlantic.net/\?||') ;;
https://invapi.hostkey.com*) ENDPOINT="${URL#https://invapi.hostkey.com}" ;;
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
esac
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
}
Expand All@@ -288,6 +289,7 @@ _validate_body() {
vultr) case "$EP_CLEAN" in /instances) _check_fields "label region plan os_id" ;; esac ;;
linode) case "$EP_CLEAN" in /linode/instances) _check_fields "label region type image" ;; esac ;;
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
esac
}

Expand Down
21 changes: 20 additions & 1 deletion test/record.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@ ERRORS=0
PROMPT_FOR_CREDS=true

# All clouds with REST APIs that we can record from
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma"
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock"

# --- Endpoint registry ---
# Format: "fixture_name:endpoint"
Expand DownExpand Up@@ -141,6 +141,14 @@ get_endpoints() {
"keypairs:/keypairs/" \
"servers:/servers/"
;;
webdock)
printf '%s\n' \
"account:/account" \
"publicKeys:/account/publicKeys" \
"servers:/servers" \
"profiles:/profiles" \
"locations:/locations"
;;
esac
}

Expand All@@ -165,6 +173,7 @@ get_auth_env_var() {
atlanticnet) printf "ATLANTICNET_API_KEY" ;;
hostkey) printf "HOSTKEY_API_KEY" ;;
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
webdock) printf "WEBDOCK_API_TOKEN" ;;
esac
}

Expand DownExpand Up@@ -399,6 +408,7 @@ call_api() {
atlanticnet) atlanticnet_api "$endpoint" ;;
hostkey) hostkey_api "$endpoint" ;;
cloudsigma) cloudsigma_api GET "$endpoint" ;;
webdock) webdock_api GET "$endpoint" ;;
esac
}

Expand DownExpand Up@@ -444,6 +454,9 @@ elif cloud == 'hostkey':
elif cloud == 'cloudsigma':
# CloudSigma returns error objects with 'error_message', 'error_type', etc
sys.exit(0 if 'error_message' in d or 'error_type' in d else 1)
elif cloud == 'webdock':
# Webdock returns error objects with 'message' field
sys.exit(0 if 'message' in d and len(d) <= 3 and not any(k in d for k in ('slug','name','status','ipv4')) else 1)
else:
sys.exit(1)
" "$cloud" 2>/dev/null
Expand DownExpand Up@@ -822,6 +835,12 @@ _live_cloudsigma() {
return 0
}

_live_webdock() {
local fixture_dir="$1"
printf '%b\n' " ${YELLOW}skip${NC} Webdock live test (not implemented yet)" >&2
return 0
}

# --- Record one cloud ---
# Check credentials and prompt if needed; returns 1 to skip this cloud
_record_ensure_credentials() {
Expand Down
134 changes: 134 additions & 0 deletions webdock/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
# Webdock

Webdock is a European VPS provider offering affordable cloud servers with API access.

## Features

- **REST API**: Full-featured API for server management
- **Affordable pricing**: Starting from €2.15/month (Intel Xeon) or €4.30/month (AMD EPYC)
- **Multiple locations**: Data centers in Europe (Finland, Netherlands, UK)
- **Developer-friendly**: Free API, automated backups, free SSL, 1-click deployments
- **SSH access**: Full root SSH access to all servers
- **GDPR compliant**: European data protection standards

## Authentication

Set your Webdock API token as an environment variable:

```bash
export WEBDOCK_API_TOKEN="your-api-token-here"
```

To obtain an API token:
1. Log in to [https://my.webdock.io](https://my.webdock.io)
2. Go to Account Area > API & Integrations
3. Generate a new API key

The token will be automatically saved to `~/.config/spawn/webdock.json` for future use.

## Usage

Launch Claude Code on a Webdock server:

```bash
bash webdock/claude.sh
```

Or use the CLI:

```bash
spawn run webdock claude
```

## Available Scripts

- `claude.sh` — Claude Code (Anthropic's CLI coding agent)
- `cline.sh` — Cline (AI pair programming in your editor)
- `aider.sh` — Aider (AI pair programming in the terminal)

## Configuration

### Server Profile

Set the server profile (hardware tier) via environment variable:

```bash
export WEBDOCK_PROFILE="webdockmicro" # Default
```

Available profiles:
- `webdockmicro` — 1 vCPU, 1GB RAM, 25GB SSD (€2.15-4.30/mo)
- `webdocksmall` — 1 vCPU, 2GB RAM, 50GB SSD
- `webdockmedium` — 2 vCPU, 4GB RAM, 80GB SSD
- `webdocklarge` — 4 vCPU, 8GB RAM, 160GB SSD
- `webdockxl` — 6 vCPU, 16GB RAM, 320GB SSD

Check [Webdock pricing](https://webdock.io/en/pricing) for the full list and current prices.

### Location

Set the server location via environment variable:

```bash
export WEBDOCK_LOCATION="fi" # Default (Finland)
```

Available locations:
- `fi` — Finland (Helsinki)
- `nl` — Netherlands (Amsterdam)
- `uk` — United Kingdom (London)

### Image

Set the OS image via environment variable:

```bash
export WEBDOCK_IMAGE="ubuntu2404" # Default
```

Available images include Ubuntu, Debian, and various pre-configured stacks (LEMP, WordPress, etc.).
Check the Webdock control panel for the full list of available images.

### Server Name

Set a custom server name:

```bash
export WEBDOCK_SERVER_NAME="my-ai-server"
```

If not set, you'll be prompted to enter one.

## How It Works

1. **Authenticate**: Validates your API token with Webdock
2. **SSH Key**: Ensures your SSH key is registered (generates ed25519 key if needed)
3. **Provision**: Creates a new server with the specified profile, location, and image
4. **Wait**: Polls until the server is online and SSH-ready
5. **Setup**: Installs the agent and configures OpenRouter API credentials
6. **Launch**: Drops you into an interactive session with the agent

## Cleanup

Servers persist after the script exits. To destroy a server:

```bash
# Using the Webdock API
curl -X DELETE "https://api.webdock.io/v1/servers/SERVER_SLUG" \
-H "Authorization: Bearer $WEBDOCK_API_TOKEN"
```

Or delete from the Webdock control panel: https://my.webdock.io/servers

## Limitations

- Webdock is primarily European-focused (data centers in EU only)
- Profile availability may vary by location
- Server creation can take 1-3 minutes depending on load

## Links

- Website: https://webdock.io
- API Documentation: https://api.webdock.io/v1
- Control Panel: https://my.webdock.io
- Pricing: https://webdock.io/en/pricing
48 changes: 48 additions & 0 deletions webdock/aider.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Aider on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Installing Aider..."
run_server "${WEBDOCK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Aider..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
58 changes: 58 additions & 0 deletions webdock/claude.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Claude Code on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
if ! run_server "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
run_server "${WEBDOCK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0"

setup_claude_code_config "${OPENROUTER_API_KEY}" \
"upload_file ${WEBDOCK_SERVER_IP}" \
"run_server ${WEBDOCK_SERVER_IP}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -795,6 +795,21 @@
"region": "zrh"
},
"notes": "API-centric cloud platform with regions in ZRH (Zurich), WDC (Washington DC), and LVS (Las Vegas). Granular resource control (CPU/RAM/Disk independently configurable). Uses 'cloudsigma' user for SSH."
},
"webdock": {
"name": "Webdock",
"description": "European VPS provider with affordable plans",
"url": "https://webdock.io/",
"type": "api",
"auth": "WEBDOCK_API_TOKEN",
"provision_method": "POST /v1/servers",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"profile": "webdockmicro",
"location": "fi",
"image": "ubuntu2404"
}
}
},
"matrix": {
Expand DownExpand Up@@ -1337,6 +1352,21 @@
"cloudsigma/opencode": "missing",
"cloudsigma/plandex": "missing",
"cloudsigma/kilocode": "missing",
"cloudsigma/continue": "implemented"
"cloudsigma/continue": "implemented",
"webdock/claude": "implemented",
"webdock/openclaw": "missing",
"webdock/nanoclaw": "missing",
"webdock/aider": "implemented",
"webdock/goose": "missing",
"webdock/codex": "missing",
"webdock/interpreter": "missing",
"webdock/gemini": "missing",
"webdock/amazonq": "missing",
"webdock/cline": "implemented",
"webdock/gptme": "missing",
"webdock/opencode": "missing",
"webdock/plandex": "missing",
"webdock/kilocode": "missing",
"webdock/continue": "missing"
}
}
}
2 changes: 2 additions & 0 deletions test/mock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -263,6 +263,7 @@ _strip_api_base() {
https://cloudapi.atlantic.net/*) ENDPOINT=$(echo "$URL" | sed 's|https://cloudapi.atlantic.net/\?||') ;;
https://invapi.hostkey.com*) ENDPOINT="${URL#https://invapi.hostkey.com}" ;;
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
esac
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
}
Expand All@@ -288,6 +289,7 @@ _validate_body() {
vultr) case "$EP_CLEAN" in /instances) _check_fields "label region plan os_id" ;; esac ;;
linode) case "$EP_CLEAN" in /linode/instances) _check_fields "label region type image" ;; esac ;;
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
esac
}

Expand Down
21 changes: 20 additions & 1 deletion test/record.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@ ERRORS=0
PROMPT_FOR_CREDS=true

# All clouds with REST APIs that we can record from
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma"
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock"

# --- Endpoint registry ---
# Format: "fixture_name:endpoint"
Expand DownExpand Up@@ -141,6 +141,14 @@ get_endpoints() {
"keypairs:/keypairs/" \
"servers:/servers/"
;;
webdock)
printf '%s\n' \
"account:/account" \
"publicKeys:/account/publicKeys" \
"servers:/servers" \
"profiles:/profiles" \
"locations:/locations"
;;
esac
}

Expand All@@ -165,6 +173,7 @@ get_auth_env_var() {
atlanticnet) printf "ATLANTICNET_API_KEY" ;;
hostkey) printf "HOSTKEY_API_KEY" ;;
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
webdock) printf "WEBDOCK_API_TOKEN" ;;
esac
}

Expand DownExpand Up@@ -399,6 +408,7 @@ call_api() {
atlanticnet) atlanticnet_api "$endpoint" ;;
hostkey) hostkey_api "$endpoint" ;;
cloudsigma) cloudsigma_api GET "$endpoint" ;;
webdock) webdock_api GET "$endpoint" ;;
esac
}

Expand DownExpand Up@@ -444,6 +454,9 @@ elif cloud == 'hostkey':
elif cloud == 'cloudsigma':
# CloudSigma returns error objects with 'error_message', 'error_type', etc
sys.exit(0 if 'error_message' in d or 'error_type' in d else 1)
elif cloud == 'webdock':
# Webdock returns error objects with 'message' field
sys.exit(0 if 'message' in d and len(d) <= 3 and not any(k in d for k in ('slug','name','status','ipv4')) else 1)
else:
sys.exit(1)
" "$cloud" 2>/dev/null
Expand DownExpand Up@@ -822,6 +835,12 @@ _live_cloudsigma() {
return 0
}

_live_webdock() {
local fixture_dir="$1"
printf '%b\n' " ${YELLOW}skip${NC} Webdock live test (not implemented yet)" >&2
return 0
}

# --- Record one cloud ---
# Check credentials and prompt if needed; returns 1 to skip this cloud
_record_ensure_credentials() {
Expand Down
134 changes: 134 additions & 0 deletions webdock/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
# Webdock

Webdock is a European VPS provider offering affordable cloud servers with API access.

## Features

- **REST API**: Full-featured API for server management
- **Affordable pricing**: Starting from €2.15/month (Intel Xeon) or €4.30/month (AMD EPYC)
- **Multiple locations**: Data centers in Europe (Finland, Netherlands, UK)
- **Developer-friendly**: Free API, automated backups, free SSL, 1-click deployments
- **SSH access**: Full root SSH access to all servers
- **GDPR compliant**: European data protection standards

## Authentication

Set your Webdock API token as an environment variable:

```bash
export WEBDOCK_API_TOKEN="your-api-token-here"
```

To obtain an API token:
1. Log in to [https://my.webdock.io](https://my.webdock.io)
2. Go to Account Area > API & Integrations
3. Generate a new API key

The token will be automatically saved to `~/.config/spawn/webdock.json` for future use.

## Usage

Launch Claude Code on a Webdock server:

```bash
bash webdock/claude.sh
```

Or use the CLI:

```bash
spawn run webdock claude
```

## Available Scripts

- `claude.sh` — Claude Code (Anthropic's CLI coding agent)
- `cline.sh` — Cline (AI pair programming in your editor)
- `aider.sh` — Aider (AI pair programming in the terminal)

## Configuration

### Server Profile

Set the server profile (hardware tier) via environment variable:

```bash
export WEBDOCK_PROFILE="webdockmicro" # Default
```

Available profiles:
- `webdockmicro` — 1 vCPU, 1GB RAM, 25GB SSD (€2.15-4.30/mo)
- `webdocksmall` — 1 vCPU, 2GB RAM, 50GB SSD
- `webdockmedium` — 2 vCPU, 4GB RAM, 80GB SSD
- `webdocklarge` — 4 vCPU, 8GB RAM, 160GB SSD
- `webdockxl` — 6 vCPU, 16GB RAM, 320GB SSD

Check [Webdock pricing](https://webdock.io/en/pricing) for the full list and current prices.

### Location

Set the server location via environment variable:

```bash
export WEBDOCK_LOCATION="fi" # Default (Finland)
```

Available locations:
- `fi` — Finland (Helsinki)
- `nl` — Netherlands (Amsterdam)
- `uk` — United Kingdom (London)

### Image

Set the OS image via environment variable:

```bash
export WEBDOCK_IMAGE="ubuntu2404" # Default
```

Available images include Ubuntu, Debian, and various pre-configured stacks (LEMP, WordPress, etc.).
Check the Webdock control panel for the full list of available images.

### Server Name

Set a custom server name:

```bash
export WEBDOCK_SERVER_NAME="my-ai-server"
```

If not set, you'll be prompted to enter one.

## How It Works

1. **Authenticate**: Validates your API token with Webdock
2. **SSH Key**: Ensures your SSH key is registered (generates ed25519 key if needed)
3. **Provision**: Creates a new server with the specified profile, location, and image
4. **Wait**: Polls until the server is online and SSH-ready
5. **Setup**: Installs the agent and configures OpenRouter API credentials
6. **Launch**: Drops you into an interactive session with the agent

## Cleanup

Servers persist after the script exits. To destroy a server:

```bash
# Using the Webdock API
curl -X DELETE "https://api.webdock.io/v1/servers/SERVER_SLUG" \
-H "Authorization: Bearer $WEBDOCK_API_TOKEN"
```

Or delete from the Webdock control panel: https://my.webdock.io/servers

## Limitations

- Webdock is primarily European-focused (data centers in EU only)
- Profile availability may vary by location
- Server creation can take 1-3 minutes depending on load

## Links

- Website: https://webdock.io
- API Documentation: https://api.webdock.io/v1
- Control Panel: https://my.webdock.io
- Pricing: https://webdock.io/en/pricing
48 changes: 48 additions & 0 deletions webdock/aider.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Aider on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Installing Aider..."
run_server "${WEBDOCK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Aider..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
58 changes: 58 additions & 0 deletions webdock/claude.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Claude Code on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
if ! run_server "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
run_server "${WEBDOCK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0"

setup_claude_code_config "${OPENROUTER_API_KEY}" \
"upload_file ${WEBDOCK_SERVER_IP}" \
"run_server ${WEBDOCK_SERVER_IP}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -795,6 +795,21 @@
"region": "zrh"
},
"notes": "API-centric cloud platform with regions in ZRH (Zurich), WDC (Washington DC), and LVS (Las Vegas). Granular resource control (CPU/RAM/Disk independently configurable). Uses 'cloudsigma' user for SSH."
},
"webdock": {
"name": "Webdock",
"description": "European VPS provider with affordable plans",
"url": "https://webdock.io/",
"type": "api",
"auth": "WEBDOCK_API_TOKEN",
"provision_method": "POST /v1/servers",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"profile": "webdockmicro",
"location": "fi",
"image": "ubuntu2404"
}
}
},
"matrix": {
Expand DownExpand Up@@ -1337,6 +1352,21 @@
"cloudsigma/opencode": "missing",
"cloudsigma/plandex": "missing",
"cloudsigma/kilocode": "missing",
"cloudsigma/continue": "implemented"
"cloudsigma/continue": "implemented",
"webdock/claude": "implemented",
"webdock/openclaw": "missing",
"webdock/nanoclaw": "missing",
"webdock/aider": "implemented",
"webdock/goose": "missing",
"webdock/codex": "missing",
"webdock/interpreter": "missing",
"webdock/gemini": "missing",
"webdock/amazonq": "missing",
"webdock/cline": "implemented",
"webdock/gptme": "missing",
"webdock/opencode": "missing",
"webdock/plandex": "missing",
"webdock/kilocode": "missing",
"webdock/continue": "missing"
}
}
}
2 changes: 2 additions & 0 deletions test/mock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -263,6 +263,7 @@ _strip_api_base() {
https://cloudapi.atlantic.net/*) ENDPOINT=$(echo "$URL" | sed 's|https://cloudapi.atlantic.net/\?||') ;;
https://invapi.hostkey.com*) ENDPOINT="${URL#https://invapi.hostkey.com}" ;;
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
esac
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
}
Expand All@@ -288,6 +289,7 @@ _validate_body() {
vultr) case "$EP_CLEAN" in /instances) _check_fields "label region plan os_id" ;; esac ;;
linode) case "$EP_CLEAN" in /linode/instances) _check_fields "label region type image" ;; esac ;;
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
esac
}

Expand Down
21 changes: 20 additions & 1 deletion test/record.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@ ERRORS=0
PROMPT_FOR_CREDS=true

# All clouds with REST APIs that we can record from
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma"
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock"

# --- Endpoint registry ---
# Format: "fixture_name:endpoint"
Expand DownExpand Up@@ -141,6 +141,14 @@ get_endpoints() {
"keypairs:/keypairs/" \
"servers:/servers/"
;;
webdock)
printf '%s\n' \
"account:/account" \
"publicKeys:/account/publicKeys" \
"servers:/servers" \
"profiles:/profiles" \
"locations:/locations"
;;
esac
}

Expand All@@ -165,6 +173,7 @@ get_auth_env_var() {
atlanticnet) printf "ATLANTICNET_API_KEY" ;;
hostkey) printf "HOSTKEY_API_KEY" ;;
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
webdock) printf "WEBDOCK_API_TOKEN" ;;
esac
}

Expand DownExpand Up@@ -399,6 +408,7 @@ call_api() {
atlanticnet) atlanticnet_api "$endpoint" ;;
hostkey) hostkey_api "$endpoint" ;;
cloudsigma) cloudsigma_api GET "$endpoint" ;;
webdock) webdock_api GET "$endpoint" ;;
esac
}

Expand DownExpand Up@@ -444,6 +454,9 @@ elif cloud == 'hostkey':
elif cloud == 'cloudsigma':
# CloudSigma returns error objects with 'error_message', 'error_type', etc
sys.exit(0 if 'error_message' in d or 'error_type' in d else 1)
elif cloud == 'webdock':
# Webdock returns error objects with 'message' field
sys.exit(0 if 'message' in d and len(d) <= 3 and not any(k in d for k in ('slug','name','status','ipv4')) else 1)
else:
sys.exit(1)
" "$cloud" 2>/dev/null
Expand DownExpand Up@@ -822,6 +835,12 @@ _live_cloudsigma() {
return 0
}

_live_webdock() {
local fixture_dir="$1"
printf '%b\n' " ${YELLOW}skip${NC} Webdock live test (not implemented yet)" >&2
return 0
}

# --- Record one cloud ---
# Check credentials and prompt if needed; returns 1 to skip this cloud
_record_ensure_credentials() {
Expand Down
134 changes: 134 additions & 0 deletions webdock/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
# Webdock

Webdock is a European VPS provider offering affordable cloud servers with API access.

## Features

- **REST API**: Full-featured API for server management
- **Affordable pricing**: Starting from €2.15/month (Intel Xeon) or €4.30/month (AMD EPYC)
- **Multiple locations**: Data centers in Europe (Finland, Netherlands, UK)
- **Developer-friendly**: Free API, automated backups, free SSL, 1-click deployments
- **SSH access**: Full root SSH access to all servers
- **GDPR compliant**: European data protection standards

## Authentication

Set your Webdock API token as an environment variable:

```bash
export WEBDOCK_API_TOKEN="your-api-token-here"
```

To obtain an API token:
1. Log in to [https://my.webdock.io](https://my.webdock.io)
2. Go to Account Area > API & Integrations
3. Generate a new API key

The token will be automatically saved to `~/.config/spawn/webdock.json` for future use.

## Usage

Launch Claude Code on a Webdock server:

```bash
bash webdock/claude.sh
```

Or use the CLI:

```bash
spawn run webdock claude
```

## Available Scripts

- `claude.sh` — Claude Code (Anthropic's CLI coding agent)
- `cline.sh` — Cline (AI pair programming in your editor)
- `aider.sh` — Aider (AI pair programming in the terminal)

## Configuration

### Server Profile

Set the server profile (hardware tier) via environment variable:

```bash
export WEBDOCK_PROFILE="webdockmicro" # Default
```

Available profiles:
- `webdockmicro` — 1 vCPU, 1GB RAM, 25GB SSD (€2.15-4.30/mo)
- `webdocksmall` — 1 vCPU, 2GB RAM, 50GB SSD
- `webdockmedium` — 2 vCPU, 4GB RAM, 80GB SSD
- `webdocklarge` — 4 vCPU, 8GB RAM, 160GB SSD
- `webdockxl` — 6 vCPU, 16GB RAM, 320GB SSD

Check [Webdock pricing](https://webdock.io/en/pricing) for the full list and current prices.

### Location

Set the server location via environment variable:

```bash
export WEBDOCK_LOCATION="fi" # Default (Finland)
```

Available locations:
- `fi` — Finland (Helsinki)
- `nl` — Netherlands (Amsterdam)
- `uk` — United Kingdom (London)

### Image

Set the OS image via environment variable:

```bash
export WEBDOCK_IMAGE="ubuntu2404" # Default
```

Available images include Ubuntu, Debian, and various pre-configured stacks (LEMP, WordPress, etc.).
Check the Webdock control panel for the full list of available images.

### Server Name

Set a custom server name:

```bash
export WEBDOCK_SERVER_NAME="my-ai-server"
```

If not set, you'll be prompted to enter one.

## How It Works

1. **Authenticate**: Validates your API token with Webdock
2. **SSH Key**: Ensures your SSH key is registered (generates ed25519 key if needed)
3. **Provision**: Creates a new server with the specified profile, location, and image
4. **Wait**: Polls until the server is online and SSH-ready
5. **Setup**: Installs the agent and configures OpenRouter API credentials
6. **Launch**: Drops you into an interactive session with the agent

## Cleanup

Servers persist after the script exits. To destroy a server:

```bash
# Using the Webdock API
curl -X DELETE "https://api.webdock.io/v1/servers/SERVER_SLUG" \
-H "Authorization: Bearer $WEBDOCK_API_TOKEN"
```

Or delete from the Webdock control panel: https://my.webdock.io/servers

## Limitations

- Webdock is primarily European-focused (data centers in EU only)
- Profile availability may vary by location
- Server creation can take 1-3 minutes depending on load

## Links

- Website: https://webdock.io
- API Documentation: https://api.webdock.io/v1
- Control Panel: https://my.webdock.io
- Pricing: https://webdock.io/en/pricing
48 changes: 48 additions & 0 deletions webdock/aider.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Aider on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Installing Aider..."
run_server "${WEBDOCK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Aider..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
58 changes: 58 additions & 0 deletions webdock/claude.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Claude Code on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
if ! run_server "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
run_server "${WEBDOCK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0"

setup_claude_code_config "${OPENROUTER_API_KEY}" \
"upload_file ${WEBDOCK_SERVER_IP}" \
"run_server ${WEBDOCK_SERVER_IP}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -795,6 +795,21 @@
"region": "zrh"
},
"notes": "API-centric cloud platform with regions in ZRH (Zurich), WDC (Washington DC), and LVS (Las Vegas). Granular resource control (CPU/RAM/Disk independently configurable). Uses 'cloudsigma' user for SSH."
},
"webdock": {
"name": "Webdock",
"description": "European VPS provider with affordable plans",
"url": "https://webdock.io/",
"type": "api",
"auth": "WEBDOCK_API_TOKEN",
"provision_method": "POST /v1/servers",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"profile": "webdockmicro",
"location": "fi",
"image": "ubuntu2404"
}
}
},
"matrix": {
Expand DownExpand Up@@ -1337,6 +1352,21 @@
"cloudsigma/opencode": "missing",
"cloudsigma/plandex": "missing",
"cloudsigma/kilocode": "missing",
"cloudsigma/continue": "implemented"
"cloudsigma/continue": "implemented",
"webdock/claude": "implemented",
"webdock/openclaw": "missing",
"webdock/nanoclaw": "missing",
"webdock/aider": "implemented",
"webdock/goose": "missing",
"webdock/codex": "missing",
"webdock/interpreter": "missing",
"webdock/gemini": "missing",
"webdock/amazonq": "missing",
"webdock/cline": "implemented",
"webdock/gptme": "missing",
"webdock/opencode": "missing",
"webdock/plandex": "missing",
"webdock/kilocode": "missing",
"webdock/continue": "missing"
}
}
}
2 changes: 2 additions & 0 deletions test/mock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -263,6 +263,7 @@ _strip_api_base() {
https://cloudapi.atlantic.net/*) ENDPOINT=$(echo "$URL" | sed 's|https://cloudapi.atlantic.net/\?||') ;;
https://invapi.hostkey.com*) ENDPOINT="${URL#https://invapi.hostkey.com}" ;;
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
esac
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
}
Expand All@@ -288,6 +289,7 @@ _validate_body() {
vultr) case "$EP_CLEAN" in /instances) _check_fields "label region plan os_id" ;; esac ;;
linode) case "$EP_CLEAN" in /linode/instances) _check_fields "label region type image" ;; esac ;;
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
esac
}

Expand Down
21 changes: 20 additions & 1 deletion test/record.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@ ERRORS=0
PROMPT_FOR_CREDS=true

# All clouds with REST APIs that we can record from
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma"
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock"

# --- Endpoint registry ---
# Format: "fixture_name:endpoint"
Expand DownExpand Up@@ -141,6 +141,14 @@ get_endpoints() {
"keypairs:/keypairs/" \
"servers:/servers/"
;;
webdock)
printf '%s\n' \
"account:/account" \
"publicKeys:/account/publicKeys" \
"servers:/servers" \
"profiles:/profiles" \
"locations:/locations"
;;
esac
}

Expand All@@ -165,6 +173,7 @@ get_auth_env_var() {
atlanticnet) printf "ATLANTICNET_API_KEY" ;;
hostkey) printf "HOSTKEY_API_KEY" ;;
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
webdock) printf "WEBDOCK_API_TOKEN" ;;
esac
}

Expand DownExpand Up@@ -399,6 +408,7 @@ call_api() {
atlanticnet) atlanticnet_api "$endpoint" ;;
hostkey) hostkey_api "$endpoint" ;;
cloudsigma) cloudsigma_api GET "$endpoint" ;;
webdock) webdock_api GET "$endpoint" ;;
esac
}

Expand DownExpand Up@@ -444,6 +454,9 @@ elif cloud == 'hostkey':
elif cloud == 'cloudsigma':
# CloudSigma returns error objects with 'error_message', 'error_type', etc
sys.exit(0 if 'error_message' in d or 'error_type' in d else 1)
elif cloud == 'webdock':
# Webdock returns error objects with 'message' field
sys.exit(0 if 'message' in d and len(d) <= 3 and not any(k in d for k in ('slug','name','status','ipv4')) else 1)
else:
sys.exit(1)
" "$cloud" 2>/dev/null
Expand DownExpand Up@@ -822,6 +835,12 @@ _live_cloudsigma() {
return 0
}

_live_webdock() {
local fixture_dir="$1"
printf '%b\n' " ${YELLOW}skip${NC} Webdock live test (not implemented yet)" >&2
return 0
}

# --- Record one cloud ---
# Check credentials and prompt if needed; returns 1 to skip this cloud
_record_ensure_credentials() {
Expand Down
134 changes: 134 additions & 0 deletions webdock/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
# Webdock

Webdock is a European VPS provider offering affordable cloud servers with API access.

## Features

- **REST API**: Full-featured API for server management
- **Affordable pricing**: Starting from €2.15/month (Intel Xeon) or €4.30/month (AMD EPYC)
- **Multiple locations**: Data centers in Europe (Finland, Netherlands, UK)
- **Developer-friendly**: Free API, automated backups, free SSL, 1-click deployments
- **SSH access**: Full root SSH access to all servers
- **GDPR compliant**: European data protection standards

## Authentication

Set your Webdock API token as an environment variable:

```bash
export WEBDOCK_API_TOKEN="your-api-token-here"
```

To obtain an API token:
1. Log in to [https://my.webdock.io](https://my.webdock.io)
2. Go to Account Area > API & Integrations
3. Generate a new API key

The token will be automatically saved to `~/.config/spawn/webdock.json` for future use.

## Usage

Launch Claude Code on a Webdock server:

```bash
bash webdock/claude.sh
```

Or use the CLI:

```bash
spawn run webdock claude
```

## Available Scripts

- `claude.sh` — Claude Code (Anthropic's CLI coding agent)
- `cline.sh` — Cline (AI pair programming in your editor)
- `aider.sh` — Aider (AI pair programming in the terminal)

## Configuration

### Server Profile

Set the server profile (hardware tier) via environment variable:

```bash
export WEBDOCK_PROFILE="webdockmicro" # Default
```

Available profiles:
- `webdockmicro` — 1 vCPU, 1GB RAM, 25GB SSD (€2.15-4.30/mo)
- `webdocksmall` — 1 vCPU, 2GB RAM, 50GB SSD
- `webdockmedium` — 2 vCPU, 4GB RAM, 80GB SSD
- `webdocklarge` — 4 vCPU, 8GB RAM, 160GB SSD
- `webdockxl` — 6 vCPU, 16GB RAM, 320GB SSD

Check [Webdock pricing](https://webdock.io/en/pricing) for the full list and current prices.

### Location

Set the server location via environment variable:

```bash
export WEBDOCK_LOCATION="fi" # Default (Finland)
```

Available locations:
- `fi` — Finland (Helsinki)
- `nl` — Netherlands (Amsterdam)
- `uk` — United Kingdom (London)

### Image

Set the OS image via environment variable:

```bash
export WEBDOCK_IMAGE="ubuntu2404" # Default
```

Available images include Ubuntu, Debian, and various pre-configured stacks (LEMP, WordPress, etc.).
Check the Webdock control panel for the full list of available images.

### Server Name

Set a custom server name:

```bash
export WEBDOCK_SERVER_NAME="my-ai-server"
```

If not set, you'll be prompted to enter one.

## How It Works

1. **Authenticate**: Validates your API token with Webdock
2. **SSH Key**: Ensures your SSH key is registered (generates ed25519 key if needed)
3. **Provision**: Creates a new server with the specified profile, location, and image
4. **Wait**: Polls until the server is online and SSH-ready
5. **Setup**: Installs the agent and configures OpenRouter API credentials
6. **Launch**: Drops you into an interactive session with the agent

## Cleanup

Servers persist after the script exits. To destroy a server:

```bash
# Using the Webdock API
curl -X DELETE "https://api.webdock.io/v1/servers/SERVER_SLUG" \
-H "Authorization: Bearer $WEBDOCK_API_TOKEN"
```

Or delete from the Webdock control panel: https://my.webdock.io/servers

## Limitations

- Webdock is primarily European-focused (data centers in EU only)
- Profile availability may vary by location
- Server creation can take 1-3 minutes depending on load

## Links

- Website: https://webdock.io
- API Documentation: https://api.webdock.io/v1
- Control Panel: https://my.webdock.io
- Pricing: https://webdock.io/en/pricing
48 changes: 48 additions & 0 deletions webdock/aider.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Aider on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Installing Aider..."
run_server "${WEBDOCK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Aider..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
58 changes: 58 additions & 0 deletions webdock/claude.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Claude Code on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
if ! run_server "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
run_server "${WEBDOCK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0"

setup_claude_code_config "${OPENROUTER_API_KEY}" \
"upload_file ${WEBDOCK_SERVER_IP}" \
"run_server ${WEBDOCK_SERVER_IP}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -795,6 +795,21 @@
"region": "zrh"
},
"notes": "API-centric cloud platform with regions in ZRH (Zurich), WDC (Washington DC), and LVS (Las Vegas). Granular resource control (CPU/RAM/Disk independently configurable). Uses 'cloudsigma' user for SSH."
},
"webdock": {
"name": "Webdock",
"description": "European VPS provider with affordable plans",
"url": "https://webdock.io/",
"type": "api",
"auth": "WEBDOCK_API_TOKEN",
"provision_method": "POST /v1/servers",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"profile": "webdockmicro",
"location": "fi",
"image": "ubuntu2404"
}
}
},
"matrix": {
Expand DownExpand Up@@ -1337,6 +1352,21 @@
"cloudsigma/opencode": "missing",
"cloudsigma/plandex": "missing",
"cloudsigma/kilocode": "missing",
"cloudsigma/continue": "implemented"
"cloudsigma/continue": "implemented",
"webdock/claude": "implemented",
"webdock/openclaw": "missing",
"webdock/nanoclaw": "missing",
"webdock/aider": "implemented",
"webdock/goose": "missing",
"webdock/codex": "missing",
"webdock/interpreter": "missing",
"webdock/gemini": "missing",
"webdock/amazonq": "missing",
"webdock/cline": "implemented",
"webdock/gptme": "missing",
"webdock/opencode": "missing",
"webdock/plandex": "missing",
"webdock/kilocode": "missing",
"webdock/continue": "missing"
}
}
}
2 changes: 2 additions & 0 deletions test/mock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -263,6 +263,7 @@ _strip_api_base() {
https://cloudapi.atlantic.net/*) ENDPOINT=$(echo "$URL" | sed 's|https://cloudapi.atlantic.net/\?||') ;;
https://invapi.hostkey.com*) ENDPOINT="${URL#https://invapi.hostkey.com}" ;;
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
esac
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
}
Expand All@@ -288,6 +289,7 @@ _validate_body() {
vultr) case "$EP_CLEAN" in /instances) _check_fields "label region plan os_id" ;; esac ;;
linode) case "$EP_CLEAN" in /linode/instances) _check_fields "label region type image" ;; esac ;;
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
esac
}

Expand Down
21 changes: 20 additions & 1 deletion test/record.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@ ERRORS=0
PROMPT_FOR_CREDS=true

# All clouds with REST APIs that we can record from
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma"
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock"

# --- Endpoint registry ---
# Format: "fixture_name:endpoint"
Expand DownExpand Up@@ -141,6 +141,14 @@ get_endpoints() {
"keypairs:/keypairs/" \
"servers:/servers/"
;;
webdock)
printf '%s\n' \
"account:/account" \
"publicKeys:/account/publicKeys" \
"servers:/servers" \
"profiles:/profiles" \
"locations:/locations"
;;
esac
}

Expand All@@ -165,6 +173,7 @@ get_auth_env_var() {
atlanticnet) printf "ATLANTICNET_API_KEY" ;;
hostkey) printf "HOSTKEY_API_KEY" ;;
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
webdock) printf "WEBDOCK_API_TOKEN" ;;
esac
}

Expand DownExpand Up@@ -399,6 +408,7 @@ call_api() {
atlanticnet) atlanticnet_api "$endpoint" ;;
hostkey) hostkey_api "$endpoint" ;;
cloudsigma) cloudsigma_api GET "$endpoint" ;;
webdock) webdock_api GET "$endpoint" ;;
esac
}

Expand DownExpand Up@@ -444,6 +454,9 @@ elif cloud == 'hostkey':
elif cloud == 'cloudsigma':
# CloudSigma returns error objects with 'error_message', 'error_type', etc
sys.exit(0 if 'error_message' in d or 'error_type' in d else 1)
elif cloud == 'webdock':
# Webdock returns error objects with 'message' field
sys.exit(0 if 'message' in d and len(d) <= 3 and not any(k in d for k in ('slug','name','status','ipv4')) else 1)
else:
sys.exit(1)
" "$cloud" 2>/dev/null
Expand DownExpand Up@@ -822,6 +835,12 @@ _live_cloudsigma() {
return 0
}

_live_webdock() {
local fixture_dir="$1"
printf '%b\n' " ${YELLOW}skip${NC} Webdock live test (not implemented yet)" >&2
return 0
}

# --- Record one cloud ---
# Check credentials and prompt if needed; returns 1 to skip this cloud
_record_ensure_credentials() {
Expand Down
134 changes: 134 additions & 0 deletions webdock/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
# Webdock

Webdock is a European VPS provider offering affordable cloud servers with API access.

## Features

- **REST API**: Full-featured API for server management
- **Affordable pricing**: Starting from €2.15/month (Intel Xeon) or €4.30/month (AMD EPYC)
- **Multiple locations**: Data centers in Europe (Finland, Netherlands, UK)
- **Developer-friendly**: Free API, automated backups, free SSL, 1-click deployments
- **SSH access**: Full root SSH access to all servers
- **GDPR compliant**: European data protection standards

## Authentication

Set your Webdock API token as an environment variable:

```bash
export WEBDOCK_API_TOKEN="your-api-token-here"
```

To obtain an API token:
1. Log in to [https://my.webdock.io](https://my.webdock.io)
2. Go to Account Area > API & Integrations
3. Generate a new API key

The token will be automatically saved to `~/.config/spawn/webdock.json` for future use.

## Usage

Launch Claude Code on a Webdock server:

```bash
bash webdock/claude.sh
```

Or use the CLI:

```bash
spawn run webdock claude
```

## Available Scripts

- `claude.sh` — Claude Code (Anthropic's CLI coding agent)
- `cline.sh` — Cline (AI pair programming in your editor)
- `aider.sh` — Aider (AI pair programming in the terminal)

## Configuration

### Server Profile

Set the server profile (hardware tier) via environment variable:

```bash
export WEBDOCK_PROFILE="webdockmicro" # Default
```

Available profiles:
- `webdockmicro` — 1 vCPU, 1GB RAM, 25GB SSD (€2.15-4.30/mo)
- `webdocksmall` — 1 vCPU, 2GB RAM, 50GB SSD
- `webdockmedium` — 2 vCPU, 4GB RAM, 80GB SSD
- `webdocklarge` — 4 vCPU, 8GB RAM, 160GB SSD
- `webdockxl` — 6 vCPU, 16GB RAM, 320GB SSD

Check [Webdock pricing](https://webdock.io/en/pricing) for the full list and current prices.

### Location

Set the server location via environment variable:

```bash
export WEBDOCK_LOCATION="fi" # Default (Finland)
```

Available locations:
- `fi` — Finland (Helsinki)
- `nl` — Netherlands (Amsterdam)
- `uk` — United Kingdom (London)

### Image

Set the OS image via environment variable:

```bash
export WEBDOCK_IMAGE="ubuntu2404" # Default
```

Available images include Ubuntu, Debian, and various pre-configured stacks (LEMP, WordPress, etc.).
Check the Webdock control panel for the full list of available images.

### Server Name

Set a custom server name:

```bash
export WEBDOCK_SERVER_NAME="my-ai-server"
```

If not set, you'll be prompted to enter one.

## How It Works

1. **Authenticate**: Validates your API token with Webdock
2. **SSH Key**: Ensures your SSH key is registered (generates ed25519 key if needed)
3. **Provision**: Creates a new server with the specified profile, location, and image
4. **Wait**: Polls until the server is online and SSH-ready
5. **Setup**: Installs the agent and configures OpenRouter API credentials
6. **Launch**: Drops you into an interactive session with the agent

## Cleanup

Servers persist after the script exits. To destroy a server:

```bash
# Using the Webdock API
curl -X DELETE "https://api.webdock.io/v1/servers/SERVER_SLUG" \
-H "Authorization: Bearer $WEBDOCK_API_TOKEN"
```

Or delete from the Webdock control panel: https://my.webdock.io/servers

## Limitations

- Webdock is primarily European-focused (data centers in EU only)
- Profile availability may vary by location
- Server creation can take 1-3 minutes depending on load

## Links

- Website: https://webdock.io
- API Documentation: https://api.webdock.io/v1
- Control Panel: https://my.webdock.io
- Pricing: https://webdock.io/en/pricing
48 changes: 48 additions & 0 deletions webdock/aider.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Aider on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Installing Aider..."
run_server "${WEBDOCK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Aider..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
58 changes: 58 additions & 0 deletions webdock/claude.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Claude Code on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
if ! run_server "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
run_server "${WEBDOCK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0"

setup_claude_code_config "${OPENROUTER_API_KEY}" \
"upload_file ${WEBDOCK_SERVER_IP}" \
"run_server ${WEBDOCK_SERVER_IP}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -795,6 +795,21 @@
"region": "zrh"
},
"notes": "API-centric cloud platform with regions in ZRH (Zurich), WDC (Washington DC), and LVS (Las Vegas). Granular resource control (CPU/RAM/Disk independently configurable). Uses 'cloudsigma' user for SSH."
},
"webdock": {
"name": "Webdock",
"description": "European VPS provider with affordable plans",
"url": "https://webdock.io/",
"type": "api",
"auth": "WEBDOCK_API_TOKEN",
"provision_method": "POST /v1/servers",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"profile": "webdockmicro",
"location": "fi",
"image": "ubuntu2404"
}
}
},
"matrix": {
Expand DownExpand Up@@ -1337,6 +1352,21 @@
"cloudsigma/opencode": "missing",
"cloudsigma/plandex": "missing",
"cloudsigma/kilocode": "missing",
"cloudsigma/continue": "implemented"
"cloudsigma/continue": "implemented",
"webdock/claude": "implemented",
"webdock/openclaw": "missing",
"webdock/nanoclaw": "missing",
"webdock/aider": "implemented",
"webdock/goose": "missing",
"webdock/codex": "missing",
"webdock/interpreter": "missing",
"webdock/gemini": "missing",
"webdock/amazonq": "missing",
"webdock/cline": "implemented",
"webdock/gptme": "missing",
"webdock/opencode": "missing",
"webdock/plandex": "missing",
"webdock/kilocode": "missing",
"webdock/continue": "missing"
}
}
}
2 changes: 2 additions & 0 deletions test/mock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -263,6 +263,7 @@ _strip_api_base() {
https://cloudapi.atlantic.net/*) ENDPOINT=$(echo "$URL" | sed 's|https://cloudapi.atlantic.net/\?||') ;;
https://invapi.hostkey.com*) ENDPOINT="${URL#https://invapi.hostkey.com}" ;;
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
esac
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
}
Expand All@@ -288,6 +289,7 @@ _validate_body() {
vultr) case "$EP_CLEAN" in /instances) _check_fields "label region plan os_id" ;; esac ;;
linode) case "$EP_CLEAN" in /linode/instances) _check_fields "label region type image" ;; esac ;;
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
esac
}

Expand Down
21 changes: 20 additions & 1 deletion test/record.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@ ERRORS=0
PROMPT_FOR_CREDS=true

# All clouds with REST APIs that we can record from
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma"
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock"

# --- Endpoint registry ---
# Format: "fixture_name:endpoint"
Expand DownExpand Up@@ -141,6 +141,14 @@ get_endpoints() {
"keypairs:/keypairs/" \
"servers:/servers/"
;;
webdock)
printf '%s\n' \
"account:/account" \
"publicKeys:/account/publicKeys" \
"servers:/servers" \
"profiles:/profiles" \
"locations:/locations"
;;
esac
}

Expand All@@ -165,6 +173,7 @@ get_auth_env_var() {
atlanticnet) printf "ATLANTICNET_API_KEY" ;;
hostkey) printf "HOSTKEY_API_KEY" ;;
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
webdock) printf "WEBDOCK_API_TOKEN" ;;
esac
}

Expand DownExpand Up@@ -399,6 +408,7 @@ call_api() {
atlanticnet) atlanticnet_api "$endpoint" ;;
hostkey) hostkey_api "$endpoint" ;;
cloudsigma) cloudsigma_api GET "$endpoint" ;;
webdock) webdock_api GET "$endpoint" ;;
esac
}

Expand DownExpand Up@@ -444,6 +454,9 @@ elif cloud == 'hostkey':
elif cloud == 'cloudsigma':
# CloudSigma returns error objects with 'error_message', 'error_type', etc
sys.exit(0 if 'error_message' in d or 'error_type' in d else 1)
elif cloud == 'webdock':
# Webdock returns error objects with 'message' field
sys.exit(0 if 'message' in d and len(d) <= 3 and not any(k in d for k in ('slug','name','status','ipv4')) else 1)
else:
sys.exit(1)
" "$cloud" 2>/dev/null
Expand DownExpand Up@@ -822,6 +835,12 @@ _live_cloudsigma() {
return 0
}

_live_webdock() {
local fixture_dir="$1"
printf '%b\n' " ${YELLOW}skip${NC} Webdock live test (not implemented yet)" >&2
return 0
}

# --- Record one cloud ---
# Check credentials and prompt if needed; returns 1 to skip this cloud
_record_ensure_credentials() {
Expand Down
134 changes: 134 additions & 0 deletions webdock/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
# Webdock

Webdock is a European VPS provider offering affordable cloud servers with API access.

## Features

- **REST API**: Full-featured API for server management
- **Affordable pricing**: Starting from €2.15/month (Intel Xeon) or €4.30/month (AMD EPYC)
- **Multiple locations**: Data centers in Europe (Finland, Netherlands, UK)
- **Developer-friendly**: Free API, automated backups, free SSL, 1-click deployments
- **SSH access**: Full root SSH access to all servers
- **GDPR compliant**: European data protection standards

## Authentication

Set your Webdock API token as an environment variable:

```bash
export WEBDOCK_API_TOKEN="your-api-token-here"
```

To obtain an API token:
1. Log in to [https://my.webdock.io](https://my.webdock.io)
2. Go to Account Area > API & Integrations
3. Generate a new API key

The token will be automatically saved to `~/.config/spawn/webdock.json` for future use.

## Usage

Launch Claude Code on a Webdock server:

```bash
bash webdock/claude.sh
```

Or use the CLI:

```bash
spawn run webdock claude
```

## Available Scripts

- `claude.sh` — Claude Code (Anthropic's CLI coding agent)
- `cline.sh` — Cline (AI pair programming in your editor)
- `aider.sh` — Aider (AI pair programming in the terminal)

## Configuration

### Server Profile

Set the server profile (hardware tier) via environment variable:

```bash
export WEBDOCK_PROFILE="webdockmicro" # Default
```

Available profiles:
- `webdockmicro` — 1 vCPU, 1GB RAM, 25GB SSD (€2.15-4.30/mo)
- `webdocksmall` — 1 vCPU, 2GB RAM, 50GB SSD
- `webdockmedium` — 2 vCPU, 4GB RAM, 80GB SSD
- `webdocklarge` — 4 vCPU, 8GB RAM, 160GB SSD
- `webdockxl` — 6 vCPU, 16GB RAM, 320GB SSD

Check [Webdock pricing](https://webdock.io/en/pricing) for the full list and current prices.

### Location

Set the server location via environment variable:

```bash
export WEBDOCK_LOCATION="fi" # Default (Finland)
```

Available locations:
- `fi` — Finland (Helsinki)
- `nl` — Netherlands (Amsterdam)
- `uk` — United Kingdom (London)

### Image

Set the OS image via environment variable:

```bash
export WEBDOCK_IMAGE="ubuntu2404" # Default
```

Available images include Ubuntu, Debian, and various pre-configured stacks (LEMP, WordPress, etc.).
Check the Webdock control panel for the full list of available images.

### Server Name

Set a custom server name:

```bash
export WEBDOCK_SERVER_NAME="my-ai-server"
```

If not set, you'll be prompted to enter one.

## How It Works

1. **Authenticate**: Validates your API token with Webdock
2. **SSH Key**: Ensures your SSH key is registered (generates ed25519 key if needed)
3. **Provision**: Creates a new server with the specified profile, location, and image
4. **Wait**: Polls until the server is online and SSH-ready
5. **Setup**: Installs the agent and configures OpenRouter API credentials
6. **Launch**: Drops you into an interactive session with the agent

## Cleanup

Servers persist after the script exits. To destroy a server:

```bash
# Using the Webdock API
curl -X DELETE "https://api.webdock.io/v1/servers/SERVER_SLUG" \
-H "Authorization: Bearer $WEBDOCK_API_TOKEN"
```

Or delete from the Webdock control panel: https://my.webdock.io/servers

## Limitations

- Webdock is primarily European-focused (data centers in EU only)
- Profile availability may vary by location
- Server creation can take 1-3 minutes depending on load

## Links

- Website: https://webdock.io
- API Documentation: https://api.webdock.io/v1
- Control Panel: https://my.webdock.io
- Pricing: https://webdock.io/en/pricing
48 changes: 48 additions & 0 deletions webdock/aider.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Aider on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Installing Aider..."
run_server "${WEBDOCK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Aider..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
58 changes: 58 additions & 0 deletions webdock/claude.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Claude Code on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
if ! run_server "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
run_server "${WEBDOCK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0"

setup_claude_code_config "${OPENROUTER_API_KEY}" \
"upload_file ${WEBDOCK_SERVER_IP}" \
"run_server ${WEBDOCK_SERVER_IP}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -795,6 +795,21 @@
"region": "zrh"
},
"notes": "API-centric cloud platform with regions in ZRH (Zurich), WDC (Washington DC), and LVS (Las Vegas). Granular resource control (CPU/RAM/Disk independently configurable). Uses 'cloudsigma' user for SSH."
},
"webdock": {
"name": "Webdock",
"description": "European VPS provider with affordable plans",
"url": "https://webdock.io/",
"type": "api",
"auth": "WEBDOCK_API_TOKEN",
"provision_method": "POST /v1/servers",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"profile": "webdockmicro",
"location": "fi",
"image": "ubuntu2404"
}
}
},
"matrix": {
Expand DownExpand Up@@ -1337,6 +1352,21 @@
"cloudsigma/opencode": "missing",
"cloudsigma/plandex": "missing",
"cloudsigma/kilocode": "missing",
"cloudsigma/continue": "implemented"
"cloudsigma/continue": "implemented",
"webdock/claude": "implemented",
"webdock/openclaw": "missing",
"webdock/nanoclaw": "missing",
"webdock/aider": "implemented",
"webdock/goose": "missing",
"webdock/codex": "missing",
"webdock/interpreter": "missing",
"webdock/gemini": "missing",
"webdock/amazonq": "missing",
"webdock/cline": "implemented",
"webdock/gptme": "missing",
"webdock/opencode": "missing",
"webdock/plandex": "missing",
"webdock/kilocode": "missing",
"webdock/continue": "missing"
}
}
}
2 changes: 2 additions & 0 deletions test/mock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -263,6 +263,7 @@ _strip_api_base() {
https://cloudapi.atlantic.net/*) ENDPOINT=$(echo "$URL" | sed 's|https://cloudapi.atlantic.net/\?||') ;;
https://invapi.hostkey.com*) ENDPOINT="${URL#https://invapi.hostkey.com}" ;;
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
esac
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
}
Expand All@@ -288,6 +289,7 @@ _validate_body() {
vultr) case "$EP_CLEAN" in /instances) _check_fields "label region plan os_id" ;; esac ;;
linode) case "$EP_CLEAN" in /linode/instances) _check_fields "label region type image" ;; esac ;;
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
esac
}

Expand Down
21 changes: 20 additions & 1 deletion test/record.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@ ERRORS=0
PROMPT_FOR_CREDS=true

# All clouds with REST APIs that we can record from
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma"
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock"

# --- Endpoint registry ---
# Format: "fixture_name:endpoint"
Expand DownExpand Up@@ -141,6 +141,14 @@ get_endpoints() {
"keypairs:/keypairs/" \
"servers:/servers/"
;;
webdock)
printf '%s\n' \
"account:/account" \
"publicKeys:/account/publicKeys" \
"servers:/servers" \
"profiles:/profiles" \
"locations:/locations"
;;
esac
}

Expand All@@ -165,6 +173,7 @@ get_auth_env_var() {
atlanticnet) printf "ATLANTICNET_API_KEY" ;;
hostkey) printf "HOSTKEY_API_KEY" ;;
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
webdock) printf "WEBDOCK_API_TOKEN" ;;
esac
}

Expand DownExpand Up@@ -399,6 +408,7 @@ call_api() {
atlanticnet) atlanticnet_api "$endpoint" ;;
hostkey) hostkey_api "$endpoint" ;;
cloudsigma) cloudsigma_api GET "$endpoint" ;;
webdock) webdock_api GET "$endpoint" ;;
esac
}

Expand DownExpand Up@@ -444,6 +454,9 @@ elif cloud == 'hostkey':
elif cloud == 'cloudsigma':
# CloudSigma returns error objects with 'error_message', 'error_type', etc
sys.exit(0 if 'error_message' in d or 'error_type' in d else 1)
elif cloud == 'webdock':
# Webdock returns error objects with 'message' field
sys.exit(0 if 'message' in d and len(d) <= 3 and not any(k in d for k in ('slug','name','status','ipv4')) else 1)
else:
sys.exit(1)
" "$cloud" 2>/dev/null
Expand DownExpand Up@@ -822,6 +835,12 @@ _live_cloudsigma() {
return 0
}

_live_webdock() {
local fixture_dir="$1"
printf '%b\n' " ${YELLOW}skip${NC} Webdock live test (not implemented yet)" >&2
return 0
}

# --- Record one cloud ---
# Check credentials and prompt if needed; returns 1 to skip this cloud
_record_ensure_credentials() {
Expand Down
134 changes: 134 additions & 0 deletions webdock/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
# Webdock

Webdock is a European VPS provider offering affordable cloud servers with API access.

## Features

- **REST API**: Full-featured API for server management
- **Affordable pricing**: Starting from €2.15/month (Intel Xeon) or €4.30/month (AMD EPYC)
- **Multiple locations**: Data centers in Europe (Finland, Netherlands, UK)
- **Developer-friendly**: Free API, automated backups, free SSL, 1-click deployments
- **SSH access**: Full root SSH access to all servers
- **GDPR compliant**: European data protection standards

## Authentication

Set your Webdock API token as an environment variable:

```bash
export WEBDOCK_API_TOKEN="your-api-token-here"
```

To obtain an API token:
1. Log in to [https://my.webdock.io](https://my.webdock.io)
2. Go to Account Area > API & Integrations
3. Generate a new API key

The token will be automatically saved to `~/.config/spawn/webdock.json` for future use.

## Usage

Launch Claude Code on a Webdock server:

```bash
bash webdock/claude.sh
```

Or use the CLI:

```bash
spawn run webdock claude
```

## Available Scripts

- `claude.sh` — Claude Code (Anthropic's CLI coding agent)
- `cline.sh` — Cline (AI pair programming in your editor)
- `aider.sh` — Aider (AI pair programming in the terminal)

## Configuration

### Server Profile

Set the server profile (hardware tier) via environment variable:

```bash
export WEBDOCK_PROFILE="webdockmicro" # Default
```

Available profiles:
- `webdockmicro` — 1 vCPU, 1GB RAM, 25GB SSD (€2.15-4.30/mo)
- `webdocksmall` — 1 vCPU, 2GB RAM, 50GB SSD
- `webdockmedium` — 2 vCPU, 4GB RAM, 80GB SSD
- `webdocklarge` — 4 vCPU, 8GB RAM, 160GB SSD
- `webdockxl` — 6 vCPU, 16GB RAM, 320GB SSD

Check [Webdock pricing](https://webdock.io/en/pricing) for the full list and current prices.

### Location

Set the server location via environment variable:

```bash
export WEBDOCK_LOCATION="fi" # Default (Finland)
```

Available locations:
- `fi` — Finland (Helsinki)
- `nl` — Netherlands (Amsterdam)
- `uk` — United Kingdom (London)

### Image

Set the OS image via environment variable:

```bash
export WEBDOCK_IMAGE="ubuntu2404" # Default
```

Available images include Ubuntu, Debian, and various pre-configured stacks (LEMP, WordPress, etc.).
Check the Webdock control panel for the full list of available images.

### Server Name

Set a custom server name:

```bash
export WEBDOCK_SERVER_NAME="my-ai-server"
```

If not set, you'll be prompted to enter one.

## How It Works

1. **Authenticate**: Validates your API token with Webdock
2. **SSH Key**: Ensures your SSH key is registered (generates ed25519 key if needed)
3. **Provision**: Creates a new server with the specified profile, location, and image
4. **Wait**: Polls until the server is online and SSH-ready
5. **Setup**: Installs the agent and configures OpenRouter API credentials
6. **Launch**: Drops you into an interactive session with the agent

## Cleanup

Servers persist after the script exits. To destroy a server:

```bash
# Using the Webdock API
curl -X DELETE "https://api.webdock.io/v1/servers/SERVER_SLUG" \
-H "Authorization: Bearer $WEBDOCK_API_TOKEN"
```

Or delete from the Webdock control panel: https://my.webdock.io/servers

## Limitations

- Webdock is primarily European-focused (data centers in EU only)
- Profile availability may vary by location
- Server creation can take 1-3 minutes depending on load

## Links

- Website: https://webdock.io
- API Documentation: https://api.webdock.io/v1
- Control Panel: https://my.webdock.io
- Pricing: https://webdock.io/en/pricing
48 changes: 48 additions & 0 deletions webdock/aider.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Aider on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Installing Aider..."
run_server "${WEBDOCK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Aider..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
58 changes: 58 additions & 0 deletions webdock/claude.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Claude Code on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
if ! run_server "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
run_server "${WEBDOCK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0"

setup_claude_code_config "${OPENROUTER_API_KEY}" \
"upload_file ${WEBDOCK_SERVER_IP}" \
"run_server ${WEBDOCK_SERVER_IP}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -795,6 +795,21 @@
"region": "zrh"
},
"notes": "API-centric cloud platform with regions in ZRH (Zurich), WDC (Washington DC), and LVS (Las Vegas). Granular resource control (CPU/RAM/Disk independently configurable). Uses 'cloudsigma' user for SSH."
},
"webdock": {
"name": "Webdock",
"description": "European VPS provider with affordable plans",
"url": "https://webdock.io/",
"type": "api",
"auth": "WEBDOCK_API_TOKEN",
"provision_method": "POST /v1/servers",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"profile": "webdockmicro",
"location": "fi",
"image": "ubuntu2404"
}
}
},
"matrix": {
Expand DownExpand Up@@ -1337,6 +1352,21 @@
"cloudsigma/opencode": "missing",
"cloudsigma/plandex": "missing",
"cloudsigma/kilocode": "missing",
"cloudsigma/continue": "implemented"
"cloudsigma/continue": "implemented",
"webdock/claude": "implemented",
"webdock/openclaw": "missing",
"webdock/nanoclaw": "missing",
"webdock/aider": "implemented",
"webdock/goose": "missing",
"webdock/codex": "missing",
"webdock/interpreter": "missing",
"webdock/gemini": "missing",
"webdock/amazonq": "missing",
"webdock/cline": "implemented",
"webdock/gptme": "missing",
"webdock/opencode": "missing",
"webdock/plandex": "missing",
"webdock/kilocode": "missing",
"webdock/continue": "missing"
}
}
}
2 changes: 2 additions & 0 deletions test/mock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -263,6 +263,7 @@ _strip_api_base() {
https://cloudapi.atlantic.net/*) ENDPOINT=$(echo "$URL" | sed 's|https://cloudapi.atlantic.net/\?||') ;;
https://invapi.hostkey.com*) ENDPOINT="${URL#https://invapi.hostkey.com}" ;;
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
esac
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
}
Expand All@@ -288,6 +289,7 @@ _validate_body() {
vultr) case "$EP_CLEAN" in /instances) _check_fields "label region plan os_id" ;; esac ;;
linode) case "$EP_CLEAN" in /linode/instances) _check_fields "label region type image" ;; esac ;;
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
esac
}

Expand Down
21 changes: 20 additions & 1 deletion test/record.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@ ERRORS=0
PROMPT_FOR_CREDS=true

# All clouds with REST APIs that we can record from
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma"
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock"

# --- Endpoint registry ---
# Format: "fixture_name:endpoint"
Expand DownExpand Up@@ -141,6 +141,14 @@ get_endpoints() {
"keypairs:/keypairs/" \
"servers:/servers/"
;;
webdock)
printf '%s\n' \
"account:/account" \
"publicKeys:/account/publicKeys" \
"servers:/servers" \
"profiles:/profiles" \
"locations:/locations"
;;
esac
}

Expand All@@ -165,6 +173,7 @@ get_auth_env_var() {
atlanticnet) printf "ATLANTICNET_API_KEY" ;;
hostkey) printf "HOSTKEY_API_KEY" ;;
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
webdock) printf "WEBDOCK_API_TOKEN" ;;
esac
}

Expand DownExpand Up@@ -399,6 +408,7 @@ call_api() {
atlanticnet) atlanticnet_api "$endpoint" ;;
hostkey) hostkey_api "$endpoint" ;;
cloudsigma) cloudsigma_api GET "$endpoint" ;;
webdock) webdock_api GET "$endpoint" ;;
esac
}

Expand DownExpand Up@@ -444,6 +454,9 @@ elif cloud == 'hostkey':
elif cloud == 'cloudsigma':
# CloudSigma returns error objects with 'error_message', 'error_type', etc
sys.exit(0 if 'error_message' in d or 'error_type' in d else 1)
elif cloud == 'webdock':
# Webdock returns error objects with 'message' field
sys.exit(0 if 'message' in d and len(d) <= 3 and not any(k in d for k in ('slug','name','status','ipv4')) else 1)
else:
sys.exit(1)
" "$cloud" 2>/dev/null
Expand DownExpand Up@@ -822,6 +835,12 @@ _live_cloudsigma() {
return 0
}

_live_webdock() {
local fixture_dir="$1"
printf '%b\n' " ${YELLOW}skip${NC} Webdock live test (not implemented yet)" >&2
return 0
}

# --- Record one cloud ---
# Check credentials and prompt if needed; returns 1 to skip this cloud
_record_ensure_credentials() {
Expand Down
134 changes: 134 additions & 0 deletions webdock/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
# Webdock

Webdock is a European VPS provider offering affordable cloud servers with API access.

## Features

- **REST API**: Full-featured API for server management
- **Affordable pricing**: Starting from €2.15/month (Intel Xeon) or €4.30/month (AMD EPYC)
- **Multiple locations**: Data centers in Europe (Finland, Netherlands, UK)
- **Developer-friendly**: Free API, automated backups, free SSL, 1-click deployments
- **SSH access**: Full root SSH access to all servers
- **GDPR compliant**: European data protection standards

## Authentication

Set your Webdock API token as an environment variable:

```bash
export WEBDOCK_API_TOKEN="your-api-token-here"
```

To obtain an API token:
1. Log in to [https://my.webdock.io](https://my.webdock.io)
2. Go to Account Area > API & Integrations
3. Generate a new API key

The token will be automatically saved to `~/.config/spawn/webdock.json` for future use.

## Usage

Launch Claude Code on a Webdock server:

```bash
bash webdock/claude.sh
```

Or use the CLI:

```bash
spawn run webdock claude
```

## Available Scripts

- `claude.sh` — Claude Code (Anthropic's CLI coding agent)
- `cline.sh` — Cline (AI pair programming in your editor)
- `aider.sh` — Aider (AI pair programming in the terminal)

## Configuration

### Server Profile

Set the server profile (hardware tier) via environment variable:

```bash
export WEBDOCK_PROFILE="webdockmicro" # Default
```

Available profiles:
- `webdockmicro` — 1 vCPU, 1GB RAM, 25GB SSD (€2.15-4.30/mo)
- `webdocksmall` — 1 vCPU, 2GB RAM, 50GB SSD
- `webdockmedium` — 2 vCPU, 4GB RAM, 80GB SSD
- `webdocklarge` — 4 vCPU, 8GB RAM, 160GB SSD
- `webdockxl` — 6 vCPU, 16GB RAM, 320GB SSD

Check [Webdock pricing](https://webdock.io/en/pricing) for the full list and current prices.

### Location

Set the server location via environment variable:

```bash
export WEBDOCK_LOCATION="fi" # Default (Finland)
```

Available locations:
- `fi` — Finland (Helsinki)
- `nl` — Netherlands (Amsterdam)
- `uk` — United Kingdom (London)

### Image

Set the OS image via environment variable:

```bash
export WEBDOCK_IMAGE="ubuntu2404" # Default
```

Available images include Ubuntu, Debian, and various pre-configured stacks (LEMP, WordPress, etc.).
Check the Webdock control panel for the full list of available images.

### Server Name

Set a custom server name:

```bash
export WEBDOCK_SERVER_NAME="my-ai-server"
```

If not set, you'll be prompted to enter one.

## How It Works

1. **Authenticate**: Validates your API token with Webdock
2. **SSH Key**: Ensures your SSH key is registered (generates ed25519 key if needed)
3. **Provision**: Creates a new server with the specified profile, location, and image
4. **Wait**: Polls until the server is online and SSH-ready
5. **Setup**: Installs the agent and configures OpenRouter API credentials
6. **Launch**: Drops you into an interactive session with the agent

## Cleanup

Servers persist after the script exits. To destroy a server:

```bash
# Using the Webdock API
curl -X DELETE "https://api.webdock.io/v1/servers/SERVER_SLUG" \
-H "Authorization: Bearer $WEBDOCK_API_TOKEN"
```

Or delete from the Webdock control panel: https://my.webdock.io/servers

## Limitations

- Webdock is primarily European-focused (data centers in EU only)
- Profile availability may vary by location
- Server creation can take 1-3 minutes depending on load

## Links

- Website: https://webdock.io
- API Documentation: https://api.webdock.io/v1
- Control Panel: https://my.webdock.io
- Pricing: https://webdock.io/en/pricing
48 changes: 48 additions & 0 deletions webdock/aider.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Aider on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Installing Aider..."
run_server "${WEBDOCK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Aider..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
58 changes: 58 additions & 0 deletions webdock/claude.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
#!/bin/bash
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=webdock/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/webdock/lib/common.sh)"
fi

log_info "Claude Code on Webdock"
echo ""

ensure_webdock_token
ensure_ssh_key

SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
verify_server_connectivity "${WEBDOCK_SERVER_IP}"
wait_for_cloud_init "${WEBDOCK_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
if ! run_server "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
run_server "${WEBDOCK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"

echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
inject_env_vars_ssh "${WEBDOCK_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0"

setup_claude_code_config "${OPENROUTER_API_KEY}" \
"upload_file ${WEBDOCK_SERVER_IP}" \
"run_server ${WEBDOCK_SERVER_IP}"

echo ""
log_info "Webdock server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (slug: ${WEBDOCK_SERVER_SLUG}, IP: ${WEBDOCK_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
sleep 1
clear
interactive_session "${WEBDOCK_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading