diff --git a/README.md b/README.md index d963a010..76f093ac 100644 --- a/README.md +++ b/README.md @@ -348,3 +348,67 @@ OPENROUTER_API_KEY=sk-or-v1-xxxxx \ - `OPENROUTER_API_KEY` - Skip OAuth and use this API key directly - `LINODE_TYPE` - Instance type (default: `g6-standard-1`) - `LINODE_REGION` - Datacenter region (default: `us-east`) + +--- + +## AWS Lightsail + +Spawn agents on [AWS Lightsail](https://aws.amazon.com/lightsail/) instances. Requires AWS CLI installed and configured (`aws configure`). + +### Usage + +#### Claude Code + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/aws-lightsail/claude.sh) +``` + +#### OpenClaw + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/aws-lightsail/openclaw.sh) +``` + +#### NanoClaw + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/aws-lightsail/nanoclaw.sh) +``` + +#### Aider + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/aws-lightsail/aider.sh) +``` + +#### Goose + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/aws-lightsail/goose.sh) +``` + +#### Codex CLI + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/aws-lightsail/codex.sh) +``` + +#### Open Interpreter + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/aws-lightsail/interpreter.sh) +``` + +### Non-Interactive Mode + +```bash +LIGHTSAIL_SERVER_NAME=dev-mk1 \ +OPENROUTER_API_KEY=sk-or-v1-xxxxx \ + bash <(curl -fsSL https://openrouter.ai/lab/spawn/aws-lightsail/claude.sh) +``` + +**Environment Variables:** +- `LIGHTSAIL_SERVER_NAME` - Name for the instance (skips prompt) +- `OPENROUTER_API_KEY` - Skip OAuth and use this API key directly +- `LIGHTSAIL_BUNDLE` - Instance bundle (default: `medium_3_0`) +- `LIGHTSAIL_REGION` - AWS region (default: `us-east-1`) diff --git a/aws-lightsail/aider.sh b/aws-lightsail/aider.sh new file mode 100755 index 00000000..79223a9f --- /dev/null +++ b/aws-lightsail/aider.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -e + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then + source "$SCRIPT_DIR/lib/common.sh" +else + source <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws-lightsail/lib/common.sh) +fi + +log_info "Aider on AWS Lightsail" +echo "" + +# 1. Ensure AWS CLI is configured +ensure_aws_cli + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get instance name and create server +SERVER_NAME=$(get_server_name) +create_server "$SERVER_NAME" + +# 4. Wait for SSH and cloud-init +verify_server_connectivity "$LIGHTSAIL_SERVER_IP" +wait_for_cloud_init "$LIGHTSAIL_SERVER_IP" + +# 5. Install Aider +log_warn "Installing Aider..." +run_server "$LIGHTSAIL_SERVER_IP" "pip install aider-chat 2>/dev/null || pip3 install aider-chat" +log_info "Aider installed" + +# 6. Get OpenRouter API key +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 + +# 7. Get model preference +echo "" +log_warn "Browse models at: https://openrouter.ai/models" +log_warn "Which model would you like to use with Aider?" +MODEL_ID=$(safe_read "Enter model ID [openrouter/auto]: ") || MODEL_ID="" +MODEL_ID="${MODEL_ID:-openrouter/auto}" + +# 8. Inject environment variables into ~/.zshrc +log_warn "Setting up environment variables..." + +ENV_TEMP=$(mktemp) +cat > "$ENV_TEMP" << EOF + +# [spawn:env] +export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}" +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$ENV_TEMP" "/tmp/env_config" +run_server "$LIGHTSAIL_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config" +rm "$ENV_TEMP" + +echo "" +log_info "Lightsail instance setup completed successfully!" +log_info "Instance: $SERVER_NAME (IP: $LIGHTSAIL_SERVER_IP)" +echo "" + +# 9. Start Aider interactively +log_warn "Starting Aider..." +sleep 1 +clear +interactive_session "$LIGHTSAIL_SERVER_IP" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}" diff --git a/aws-lightsail/claude.sh b/aws-lightsail/claude.sh new file mode 100755 index 00000000..b93d3081 --- /dev/null +++ b/aws-lightsail/claude.sh @@ -0,0 +1,114 @@ +#!/bin/bash +set -e + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then + source "$SCRIPT_DIR/lib/common.sh" +else + source <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws-lightsail/lib/common.sh) +fi + +log_info "Claude Code on AWS Lightsail" +echo "" + +# 1. Ensure AWS CLI is configured +ensure_aws_cli + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get instance name and create server +SERVER_NAME=$(get_server_name) +create_server "$SERVER_NAME" + +# 4. Wait for SSH and cloud-init +verify_server_connectivity "$LIGHTSAIL_SERVER_IP" +wait_for_cloud_init "$LIGHTSAIL_SERVER_IP" + +# 5. Verify Claude Code is installed (fallback to manual install) +log_warn "Verifying Claude Code installation..." +if ! run_server "$LIGHTSAIL_SERVER_IP" "command -v claude" >/dev/null 2>&1; then + log_warn "Claude Code not found, installing manually..." + run_server "$LIGHTSAIL_SERVER_IP" "curl -fsSL https://claude.ai/install.sh | bash" +fi +log_info "Claude Code is installed" + +# 6. Get OpenRouter API key +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 + +# 7. Inject environment variables into ~/.zshrc +log_warn "Setting up environment variables..." + +ENV_TEMP=$(mktemp) +cat > "$ENV_TEMP" << EOF + +# [spawn:env] +export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}" +export ANTHROPIC_BASE_URL="https://openrouter.ai/api" +export ANTHROPIC_AUTH_TOKEN="${OPENROUTER_API_KEY}" +export ANTHROPIC_API_KEY="" +export CLAUDE_CODE_SKIP_ONBOARDING="1" +export CLAUDE_CODE_ENABLE_TELEMETRY="0" +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$ENV_TEMP" "/tmp/env_config" +run_server "$LIGHTSAIL_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config" +rm "$ENV_TEMP" + +# 8. Configure Claude Code settings +log_warn "Configuring Claude Code..." + +run_server "$LIGHTSAIL_SERVER_IP" "mkdir -p ~/.claude" + +# Upload settings.json +SETTINGS_TEMP=$(mktemp) +cat > "$SETTINGS_TEMP" << EOF +{ + "theme": "dark", + "editor": "vim", + "env": { + "CLAUDE_CODE_ENABLE_TELEMETRY": "0", + "ANTHROPIC_BASE_URL": "https://openrouter.ai/api", + "ANTHROPIC_AUTH_TOKEN": "${OPENROUTER_API_KEY}" + }, + "permissions": { + "defaultMode": "bypassPermissions", + "dangerouslySkipPermissions": true + } +} +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$SETTINGS_TEMP" "/home/ubuntu/.claude/settings.json" +rm "$SETTINGS_TEMP" + +# Upload ~/.claude.json global state +GLOBAL_STATE_TEMP=$(mktemp) +cat > "$GLOBAL_STATE_TEMP" << EOF +{ + "hasCompletedOnboarding": true, + "bypassPermissionsModeAccepted": true +} +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$GLOBAL_STATE_TEMP" "/home/ubuntu/.claude.json" +rm "$GLOBAL_STATE_TEMP" + +# Create empty CLAUDE.md +run_server "$LIGHTSAIL_SERVER_IP" "touch ~/.claude/CLAUDE.md" + +echo "" +log_info "Lightsail instance setup completed successfully!" +log_info "Instance: $SERVER_NAME (IP: $LIGHTSAIL_SERVER_IP)" +echo "" + +# 9. Start Claude Code interactively +log_warn "Starting Claude Code..." +sleep 1 +clear +interactive_session "$LIGHTSAIL_SERVER_IP" "source ~/.zshrc && claude" diff --git a/aws-lightsail/codex.sh b/aws-lightsail/codex.sh new file mode 100755 index 00000000..b89a6b21 --- /dev/null +++ b/aws-lightsail/codex.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -e + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then + source "$SCRIPT_DIR/lib/common.sh" +else + source <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws-lightsail/lib/common.sh) +fi + +log_info "Codex CLI on AWS Lightsail" +echo "" + +# 1. Ensure AWS CLI is configured +ensure_aws_cli + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get instance name and create server +SERVER_NAME=$(get_server_name) +create_server "$SERVER_NAME" + +# 4. Wait for SSH and cloud-init +verify_server_connectivity "$LIGHTSAIL_SERVER_IP" +wait_for_cloud_init "$LIGHTSAIL_SERVER_IP" + +# 5. Install Codex CLI +log_warn "Installing Codex CLI..." +run_server "$LIGHTSAIL_SERVER_IP" "npm install -g @openai/codex" +log_info "Codex CLI installed" + +# 6. Get OpenRouter API key +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 + +# 7. Inject environment variables into ~/.zshrc +log_warn "Setting up environment variables..." + +ENV_TEMP=$(mktemp) +cat > "$ENV_TEMP" << EOF + +# [spawn:env] +export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}" +export OPENAI_API_KEY="${OPENROUTER_API_KEY}" +export OPENAI_BASE_URL="https://openrouter.ai/api/v1" +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$ENV_TEMP" "/tmp/env_config" +run_server "$LIGHTSAIL_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config" +rm "$ENV_TEMP" + +echo "" +log_info "Lightsail instance setup completed successfully!" +log_info "Instance: $SERVER_NAME (IP: $LIGHTSAIL_SERVER_IP)" +echo "" + +# 8. Start Codex interactively +log_warn "Starting Codex..." +sleep 1 +clear +interactive_session "$LIGHTSAIL_SERVER_IP" "source ~/.zshrc && codex" diff --git a/aws-lightsail/goose.sh b/aws-lightsail/goose.sh new file mode 100755 index 00000000..3f1ce3d7 --- /dev/null +++ b/aws-lightsail/goose.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -e + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then + source "$SCRIPT_DIR/lib/common.sh" +else + source <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws-lightsail/lib/common.sh) +fi + +log_info "Goose on AWS Lightsail" +echo "" + +# 1. Ensure AWS CLI is configured +ensure_aws_cli + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get instance name and create server +SERVER_NAME=$(get_server_name) +create_server "$SERVER_NAME" + +# 4. Wait for SSH and cloud-init +verify_server_connectivity "$LIGHTSAIL_SERVER_IP" +wait_for_cloud_init "$LIGHTSAIL_SERVER_IP" + +# 5. Install Goose +log_warn "Installing Goose..." +run_server "$LIGHTSAIL_SERVER_IP" "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash" +log_info "Goose installed" + +# 6. Get OpenRouter API key +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 + +# 7. Inject environment variables into ~/.zshrc +log_warn "Setting up environment variables..." + +ENV_TEMP=$(mktemp) +cat > "$ENV_TEMP" << EOF + +# [spawn:env] +export GOOSE_PROVIDER=openrouter +export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}" +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$ENV_TEMP" "/tmp/env_config" +run_server "$LIGHTSAIL_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config" +rm "$ENV_TEMP" + +echo "" +log_info "Lightsail instance setup completed successfully!" +log_info "Instance: $SERVER_NAME (IP: $LIGHTSAIL_SERVER_IP)" +echo "" + +# 8. Start Goose interactively +log_warn "Starting Goose..." +sleep 1 +clear +interactive_session "$LIGHTSAIL_SERVER_IP" "source ~/.zshrc && goose" diff --git a/aws-lightsail/interpreter.sh b/aws-lightsail/interpreter.sh new file mode 100755 index 00000000..855d4ed1 --- /dev/null +++ b/aws-lightsail/interpreter.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -e + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then + source "$SCRIPT_DIR/lib/common.sh" +else + source <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws-lightsail/lib/common.sh) +fi + +log_info "Open Interpreter on AWS Lightsail" +echo "" + +# 1. Ensure AWS CLI is configured +ensure_aws_cli + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get instance name and create server +SERVER_NAME=$(get_server_name) +create_server "$SERVER_NAME" + +# 4. Wait for SSH and cloud-init +verify_server_connectivity "$LIGHTSAIL_SERVER_IP" +wait_for_cloud_init "$LIGHTSAIL_SERVER_IP" + +# 5. Install Open Interpreter +log_warn "Installing Open Interpreter..." +run_server "$LIGHTSAIL_SERVER_IP" "pip install open-interpreter 2>/dev/null || pip3 install open-interpreter" +log_info "Open Interpreter installed" + +# 6. Get OpenRouter API key +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 + +# 7. Inject environment variables into ~/.zshrc +log_warn "Setting up environment variables..." + +ENV_TEMP=$(mktemp) +cat > "$ENV_TEMP" << EOF + +# [spawn:env] +export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}" +export OPENAI_API_KEY="${OPENROUTER_API_KEY}" +export OPENAI_BASE_URL="https://openrouter.ai/api/v1" +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$ENV_TEMP" "/tmp/env_config" +run_server "$LIGHTSAIL_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config" +rm "$ENV_TEMP" + +echo "" +log_info "Lightsail instance setup completed successfully!" +log_info "Instance: $SERVER_NAME (IP: $LIGHTSAIL_SERVER_IP)" +echo "" + +# 8. Start Open Interpreter interactively +log_warn "Starting Open Interpreter..." +sleep 1 +clear +interactive_session "$LIGHTSAIL_SERVER_IP" "source ~/.zshrc && interpreter" diff --git a/aws-lightsail/lib/common.sh b/aws-lightsail/lib/common.sh new file mode 100644 index 00000000..b4d43c1f --- /dev/null +++ b/aws-lightsail/lib/common.sh @@ -0,0 +1,270 @@ +#!/bin/bash +# Common bash functions for AWS Lightsail spawn scripts +# Uses AWS CLI (aws lightsail) — requires `aws` CLI configured with credentials + +# ============================================================ +# Provider-agnostic functions +# ============================================================ + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}$1${NC}" >&2; } +log_warn() { echo -e "${YELLOW}$1${NC}" >&2; } +log_error() { echo -e "${RED}$1${NC}" >&2; } + +safe_read() { + local prompt="$1" result="" + if [[ -t 0 ]]; then read -p "$prompt" result + elif echo -n "" > /dev/tty 2>/dev/null; then read -p "$prompt" result < /dev/tty + else log_error "Cannot read input: no TTY available"; return 1; fi + echo "$result" +} + +nc_listen() { + local port=$1; shift + if nc --help 2>&1 | grep -q "BusyBox\|busybox" || nc --help 2>&1 | grep -q "\-p "; then + nc -l -p "$port" "$@" + else nc -l "$port" "$@"; fi +} + +open_browser() { + local url=$1 + if command -v termux-open-url &>/dev/null; then termux-open-url "$url" /dev/null; then open "$url" /dev/null; then xdg-open "$url" /dev/null; then log_warn "netcat (nc) not found"; return 1; fi + local callback_url="http://localhost:${callback_port}/callback" + local auth_url="https://openrouter.ai/auth?callback_url=${callback_url}" + local oauth_dir=$(mktemp -d) code_file="$oauth_dir/code" + log_warn "Starting local OAuth server on port ${callback_port}..." + ( + local success_response='HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n

Authentication Successful!

You can close this tab

' + while true; do + local response_file=$(mktemp); echo -e "$success_response" > "$response_file" + local request=$(nc_listen "$callback_port" < "$response_file" 2>/dev/null | head -1) + local nc_status=$?; rm -f "$response_file" + if [[ $nc_status -ne 0 ]]; then break; fi + if [[ "$request" == *"/callback?code="* ]]; then + echo "$request" | sed -n 's/.*code=\([^ &]*\).*/\1/p' > "$code_file"; break + fi + done + ) /dev/null; then log_warn "Failed to start OAuth server"; rm -rf "$oauth_dir"; return 1; fi + log_warn "Opening browser to authenticate with OpenRouter..."; open_browser "$auth_url" + local timeout=120 elapsed=0 + while [[ ! -f "$code_file" ]] && [[ $elapsed -lt $timeout ]]; do sleep 1; ((elapsed++)); done + kill $server_pid 2>/dev/null || true; wait $server_pid 2>/dev/null || true + if [[ ! -f "$code_file" ]]; then log_warn "OAuth timeout"; rm -rf "$oauth_dir"; return 1; fi + local oauth_code=$(cat "$code_file"); rm -rf "$oauth_dir" + log_warn "Exchanging OAuth code for API key..." + local key_response=$(curl -s -X POST "https://openrouter.ai/api/v1/auth/keys" \ + -H "Content-Type: application/json" -d "{\"code\": \"$oauth_code\"}") + local api_key=$(echo "$key_response" | grep -o '"key":"[^"]*"' | sed 's/"key":"//;s/"$//') + if [[ -z "$api_key" ]]; then log_error "Failed to exchange OAuth code: ${key_response}"; return 1; fi + log_info "Successfully obtained OpenRouter API key via OAuth!"; echo "$api_key" +} + +get_openrouter_api_key_oauth() { + local callback_port=${1:-5180} + local api_key=$(try_oauth_flow "$callback_port") + if [[ -n "$api_key" ]]; then echo "$api_key"; return 0; fi + echo ""; log_warn "OAuth authentication failed or unavailable" + log_warn "You can enter your API key manually instead"; echo "" + local manual_choice=$(safe_read "Would you like to enter your API key manually? (Y/n): ") || { + log_error "Cannot prompt for manual entry in non-interactive mode" + log_warn "Set OPENROUTER_API_KEY environment variable for non-interactive usage"; return 1 + } + if [[ ! "$manual_choice" =~ ^[Nn]$ ]]; then + api_key=$(get_openrouter_api_key_manual); echo "$api_key"; return 0 + else log_error "Authentication cancelled by user"; return 1; fi +} + +# ============================================================ +# AWS Lightsail specific functions +# ============================================================ + +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -i $HOME/.ssh/id_ed25519" + +ensure_aws_cli() { + if ! command -v aws &>/dev/null; then + log_error "AWS CLI is required. Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" + return 1 + fi + # Verify credentials are configured + if ! aws sts get-caller-identity &>/dev/null; then + log_error "AWS CLI not configured. Run: aws configure" + return 1 + fi + local region="${AWS_DEFAULT_REGION:-${LIGHTSAIL_REGION:-us-east-1}}" + export AWS_DEFAULT_REGION="$region" + log_info "Using AWS region: $region" +} + +ensure_ssh_key() { + local key_path="$HOME/.ssh/id_ed25519" pub_path="${key_path}.pub" + if [[ ! -f "$key_path" ]]; then + log_warn "Generating SSH key..." + mkdir -p "$HOME/.ssh" + ssh-keygen -t ed25519 -f "$key_path" -N "" -q + log_info "SSH key generated at $key_path" + fi + local pub_key=$(cat "$pub_path") + local key_name="spawn-key" + + # Check if already registered + if aws lightsail get-key-pair --key-pair-name "$key_name" &>/dev/null; then + log_info "SSH key already registered with Lightsail" + return 0 + fi + + log_warn "Importing SSH key to Lightsail..." + aws lightsail import-key-pair \ + --key-pair-name "$key_name" \ + --public-key-base64 "$(base64 -w0 "$pub_path" 2>/dev/null || base64 "$pub_path")" \ + >/dev/null + log_info "SSH key imported to Lightsail" +} + +get_server_name() { + if [[ -n "$LIGHTSAIL_SERVER_NAME" ]]; then + log_info "Using instance name from environment: $LIGHTSAIL_SERVER_NAME" + echo "$LIGHTSAIL_SERVER_NAME"; return 0 + fi + local server_name=$(safe_read "Enter Lightsail instance name: ") + if [[ -z "$server_name" ]]; then + log_error "Instance name is required" + log_warn "Set LIGHTSAIL_SERVER_NAME environment variable for non-interactive usage"; return 1 + fi + echo "$server_name" +} + +get_cloud_init_userdata() { + cat << 'CLOUD_INIT_EOF' +#!/bin/bash +apt-get update -y +apt-get install -y curl unzip git zsh +# Install Bun +su - ubuntu -c 'curl -fsSL https://bun.sh/install | bash' +# Install Claude Code +su - ubuntu -c 'curl -fsSL https://claude.ai/install.sh | bash' +# Configure PATH +echo 'export PATH="$HOME/.claude/local/bin:$HOME/.bun/bin:$PATH"' >> /home/ubuntu/.bashrc +echo 'export PATH="$HOME/.claude/local/bin:$HOME/.bun/bin:$PATH"' >> /home/ubuntu/.zshrc +touch /home/ubuntu/.cloud-init-complete +chown ubuntu:ubuntu /home/ubuntu/.cloud-init-complete +CLOUD_INIT_EOF +} + +create_server() { + local name="$1" + local bundle="${LIGHTSAIL_BUNDLE:-medium_3_0}" + local region="${AWS_DEFAULT_REGION:-us-east-1}" + local az="${region}a" + local blueprint="ubuntu_24_04" + + log_warn "Creating Lightsail instance '$name' (bundle: $bundle, AZ: $az)..." + + local userdata=$(get_cloud_init_userdata) + + aws lightsail create-instances \ + --instance-names "$name" \ + --availability-zone "$az" \ + --blueprint-id "$blueprint" \ + --bundle-id "$bundle" \ + --key-pair-name "spawn-key" \ + --user-data "$userdata" \ + >/dev/null + + if [[ $? -ne 0 ]]; then + log_error "Failed to create Lightsail instance" + return 1 + fi + + export LIGHTSAIL_INSTANCE_NAME="$name" + log_info "Instance creation initiated: $name" + + # Wait for instance to become running and get IP + log_warn "Waiting for instance to become running..." + local max_attempts=60 attempt=1 + while [[ $attempt -le $max_attempts ]]; do + local state=$(aws lightsail get-instance --instance-name "$name" \ + --query 'instance.state.name' --output text 2>/dev/null) + + if [[ "$state" == "running" ]]; then + LIGHTSAIL_SERVER_IP=$(aws lightsail get-instance --instance-name "$name" \ + --query 'instance.publicIpAddress' --output text) + export LIGHTSAIL_SERVER_IP + log_info "Instance running: IP=$LIGHTSAIL_SERVER_IP" + return 0 + fi + log_warn "Instance state: $state ($attempt/$max_attempts)" + sleep 5; ((attempt++)) + done + log_error "Instance did not become running in time"; return 1 +} + +verify_server_connectivity() { + local ip="$1" max_attempts=${2:-30} attempt=1 + log_warn "Waiting for SSH connectivity to $ip..." + while [[ $attempt -le $max_attempts ]]; do + if ssh $SSH_OPTS -o ConnectTimeout=5 "ubuntu@$ip" "echo ok" >/dev/null 2>&1; then + log_info "SSH connection established"; return 0 + fi + log_warn "Waiting for SSH... ($attempt/$max_attempts)"; sleep 5; ((attempt++)) + done + log_error "Server failed to respond via SSH after $max_attempts attempts"; return 1 +} + +wait_for_cloud_init() { + local ip="$1" max_attempts=${2:-60} attempt=1 + log_warn "Waiting for cloud-init to complete..." + while [[ $attempt -le $max_attempts ]]; do + if ssh $SSH_OPTS "ubuntu@$ip" "test -f /home/ubuntu/.cloud-init-complete" >/dev/null 2>&1; then + log_info "Cloud-init completed"; return 0 + fi + log_warn "Cloud-init in progress... ($attempt/$max_attempts)"; sleep 5; ((attempt++)) + done + log_error "Cloud-init did not complete after $max_attempts attempts"; return 1 +} + +# Note: Lightsail uses 'ubuntu' user, not 'root' +run_server() { local ip="$1" cmd="$2"; ssh $SSH_OPTS "ubuntu@$ip" "$cmd"; } +upload_file() { local ip="$1" local_path="$2" remote_path="$3"; scp $SSH_OPTS "$local_path" "ubuntu@$ip:$remote_path"; } +interactive_session() { local ip="$1" cmd="$2"; ssh -t $SSH_OPTS "ubuntu@$ip" "$cmd"; } + +destroy_server() { + local name="$1" + log_warn "Destroying Lightsail instance $name..." + aws lightsail delete-instance --instance-name "$name" >/dev/null + log_info "Instance $name destroyed" +} + +list_servers() { + aws lightsail get-instances --query 'instances[].{Name:name,State:state.name,IP:publicIpAddress,Bundle:bundleId}' --output table +} diff --git a/aws-lightsail/nanoclaw.sh b/aws-lightsail/nanoclaw.sh new file mode 100755 index 00000000..4895fdfd --- /dev/null +++ b/aws-lightsail/nanoclaw.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -e + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then + source "$SCRIPT_DIR/lib/common.sh" +else + source <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws-lightsail/lib/common.sh) +fi + +log_info "NanoClaw on AWS Lightsail" +echo "" + +# 1. Ensure AWS CLI is configured +ensure_aws_cli + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get instance name and create server +SERVER_NAME=$(get_server_name) +create_server "$SERVER_NAME" + +# 4. Wait for SSH and cloud-init +verify_server_connectivity "$LIGHTSAIL_SERVER_IP" +wait_for_cloud_init "$LIGHTSAIL_SERVER_IP" + +# 5. Install Node.js deps and clone nanoclaw +log_warn "Installing tsx..." +run_server "$LIGHTSAIL_SERVER_IP" "source ~/.bashrc && bun install -g tsx" + +log_warn "Cloning and building nanoclaw..." +run_server "$LIGHTSAIL_SERVER_IP" "git clone https://github.com/gavrielc/nanoclaw.git ~/nanoclaw && cd ~/nanoclaw && npm install && npm run build" +log_info "NanoClaw installed" + +# 6. Get OpenRouter API key +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 + +# 7. Inject environment variables into ~/.zshrc +log_warn "Setting up environment variables..." + +ENV_TEMP=$(mktemp) +cat > "$ENV_TEMP" << EOF + +# [spawn:env] +export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}" +export ANTHROPIC_API_KEY="${OPENROUTER_API_KEY}" +export ANTHROPIC_BASE_URL="https://openrouter.ai/api" +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$ENV_TEMP" "/tmp/env_config" +run_server "$LIGHTSAIL_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config" +rm "$ENV_TEMP" + +# 8. Create nanoclaw .env file +log_warn "Configuring nanoclaw..." + +DOTENV_TEMP=$(mktemp) +cat > "$DOTENV_TEMP" << EOF +ANTHROPIC_API_KEY=${OPENROUTER_API_KEY} +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$DOTENV_TEMP" "/home/ubuntu/nanoclaw/.env" +rm "$DOTENV_TEMP" + +echo "" +log_info "Lightsail instance setup completed successfully!" +log_info "Instance: $SERVER_NAME (IP: $LIGHTSAIL_SERVER_IP)" +echo "" + +# 9. Start nanoclaw +log_warn "Starting nanoclaw..." +log_warn "You will need to scan a WhatsApp QR code to authenticate." +echo "" +interactive_session "$LIGHTSAIL_SERVER_IP" "cd ~/nanoclaw && source ~/.zshrc && npm run dev" diff --git a/aws-lightsail/openclaw.sh b/aws-lightsail/openclaw.sh new file mode 100755 index 00000000..2ec9cd3c --- /dev/null +++ b/aws-lightsail/openclaw.sh @@ -0,0 +1,107 @@ +#!/bin/bash +set -e + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then + source "$SCRIPT_DIR/lib/common.sh" +else + source <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws-lightsail/lib/common.sh) +fi + +log_info "OpenClaw on AWS Lightsail" +echo "" + +# 1. Ensure AWS CLI is configured +ensure_aws_cli + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get instance name and create server +SERVER_NAME=$(get_server_name) +create_server "$SERVER_NAME" + +# 4. Wait for SSH and cloud-init +verify_server_connectivity "$LIGHTSAIL_SERVER_IP" +wait_for_cloud_init "$LIGHTSAIL_SERVER_IP" + +# 5. Install openclaw via bun +log_warn "Installing openclaw..." +run_server "$LIGHTSAIL_SERVER_IP" "source ~/.bashrc && bun install -g openclaw" +log_info "OpenClaw installed" + +# 6. Get OpenRouter API key +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 + +# 7. Get model preference +echo "" +log_warn "Browse models at: https://openrouter.ai/models" +log_warn "Which model would you like to use?" +MODEL_ID=$(safe_read "Enter model ID [openrouter/auto]: ") || MODEL_ID="" +MODEL_ID="${MODEL_ID:-openrouter/auto}" + +# 8. Inject environment variables into ~/.zshrc +log_warn "Setting up environment variables..." + +ENV_TEMP=$(mktemp) +cat > "$ENV_TEMP" << EOF + +# [spawn:env] +export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}" +export ANTHROPIC_API_KEY="${OPENROUTER_API_KEY}" +export ANTHROPIC_BASE_URL="https://openrouter.ai/api" +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$ENV_TEMP" "/tmp/env_config" +run_server "$LIGHTSAIL_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config" +rm "$ENV_TEMP" + +# 9. Configure openclaw +log_warn "Configuring openclaw..." + +run_server "$LIGHTSAIL_SERVER_IP" "rm -rf ~/.openclaw && mkdir -p ~/.openclaw" + +# Generate a random gateway token +GATEWAY_TOKEN=$(openssl rand -hex 16) + +OPENCLAW_CONFIG_TEMP=$(mktemp) +cat > "$OPENCLAW_CONFIG_TEMP" << EOF +{ + "env": { + "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}" + }, + "gateway": { + "mode": "local", + "auth": { + "token": "${GATEWAY_TOKEN}" + } + }, + "agents": { + "defaults": { + "model": { + "primary": "openrouter/${MODEL_ID}" + } + } + } +} +EOF + +upload_file "$LIGHTSAIL_SERVER_IP" "$OPENCLAW_CONFIG_TEMP" "/home/ubuntu/.openclaw/openclaw.json" +rm "$OPENCLAW_CONFIG_TEMP" + +echo "" +log_info "Lightsail instance setup completed successfully!" +log_info "Instance: $SERVER_NAME (IP: $LIGHTSAIL_SERVER_IP)" +echo "" + +# 10. Start openclaw gateway in background and launch TUI +log_warn "Starting openclaw..." +run_server "$LIGHTSAIL_SERVER_IP" "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" +sleep 2 +interactive_session "$LIGHTSAIL_SERVER_IP" "source ~/.zshrc && openclaw tui" diff --git a/manifest.json b/manifest.json index f1f224b5..4f4c13d5 100644 --- a/manifest.json +++ b/manifest.json @@ -192,6 +192,22 @@ "region": "us-east", "image": "linode/ubuntu24.04" } + }, + "aws-lightsail": { + "name": "AWS Lightsail", + "description": "AWS Lightsail instances via AWS CLI", + "url": "https://aws.amazon.com/lightsail/", + "type": "cli", + "auth": "aws configure (AWS credentials)", + "provision_method": "aws lightsail create-instances with --user-data", + "exec_method": "ssh ubuntu@IP", + "interactive_method": "ssh -t ubuntu@IP", + "defaults": { + "bundle": "medium_3_0", + "region": "us-east-1", + "blueprint": "ubuntu_24_04" + }, + "notes": "Uses 'ubuntu' user instead of 'root'. Requires AWS CLI installed and configured." } }, "matrix": { @@ -229,6 +245,13 @@ "hetzner/interpreter": "implemented", "digitalocean/interpreter": "implemented", "vultr/interpreter": "implemented", - "linode/interpreter": "implemented" + "linode/interpreter": "implemented", + "aws-lightsail/claude": "implemented", + "aws-lightsail/openclaw": "implemented", + "aws-lightsail/nanoclaw": "implemented", + "aws-lightsail/aider": "implemented", + "aws-lightsail/goose": "implemented", + "aws-lightsail/codex": "implemented", + "aws-lightsail/interpreter": "implemented" } }