diff --git a/README.md b/README.md index 8a70fa46..025ffd16 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Launch any AI coding agent on any cloud with a single command. All models powered by [OpenRouter](https://openrouter.ai). (ALPHA software, use at your own risk!) -**13 agents. 18 clouds. 234 combinations. Zero config.** +**13 agents. 19 clouds. 247 combinations. Zero config.** ## Install @@ -86,21 +86,21 @@ For cloud-specific auth, see each cloud's README in this repository. ## Matrix -| | [Sprite](sprite/) | [Hetzner](hetzner/) | [DigitalOcean](digitalocean/) | [Vultr](vultr/) | [Linode](linode/) | [Lambda](lambda/) | [Lightsail](aws-lightsail/) | [GCP](gcp/) | [E2B](e2b/) | [Modal](modal/) | [Fly.io](fly/) | [Civo](civo/) | [Scaleway](scaleway/) | [Daytona](daytona/) | [RunPod](runpod/) | [UpCloud](upcloud/) | [BinaryLane](binarylane/) | [Genesis Cloud](genesiscloud/) | -|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| -| [**Claude Code**](https://claude.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**OpenClaw**](https://github.com/OpenRouterTeam/openclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**NanoClaw**](https://github.com/gavrielc/nanoclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Aider**](https://github.com/paul-gauthier/aider) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Goose**](https://github.com/block/goose) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Codex CLI**](https://github.com/openai/codex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Open Interpreter**](https://github.com/OpenInterpreter/open-interpreter) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Gemini CLI**](https://github.com/google-gemini/gemini-cli) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Amazon Q CLI**](https://aws.amazon.com/q/developer/) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Cline**](https://github.com/cline/cline) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**gptme**](https://github.com/gptme/gptme) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**OpenCode**](https://github.com/opencode-ai/opencode) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| [**Plandex**](https://github.com/plandex-ai/plandex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| | [Sprite](sprite/) | [Hetzner](hetzner/) | [DigitalOcean](digitalocean/) | [Vultr](vultr/) | [Linode](linode/) | [Lambda](lambda/) | [Lightsail](aws-lightsail/) | [GCP](gcp/) | [E2B](e2b/) | [Modal](modal/) | [Fly.io](fly/) | [Civo](civo/) | [Scaleway](scaleway/) | [Daytona](daytona/) | [RunPod](runpod/) | [UpCloud](upcloud/) | [BinaryLane](binarylane/) | [Genesis Cloud](genesiscloud/) | [Latitude.sh](latitude/) | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| [**Claude Code**](https://claude.ai) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**OpenClaw**](https://github.com/OpenRouterTeam/openclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**NanoClaw**](https://github.com/gavrielc/nanoclaw) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Aider**](https://github.com/paul-gauthier/aider) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Goose**](https://github.com/block/goose) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Codex CLI**](https://github.com/openai/codex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Open Interpreter**](https://github.com/OpenInterpreter/open-interpreter) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Gemini CLI**](https://github.com/google-gemini/gemini-cli) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Amazon Q CLI**](https://aws.amazon.com/q/developer/) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Cline**](https://github.com/cline/cline) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**gptme**](https://github.com/gptme/gptme) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**OpenCode**](https://github.com/opencode-ai/opencode) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| [**Plandex**](https://github.com/plandex-ai/plandex) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ### How it works diff --git a/latitude/README.md b/latitude/README.md new file mode 100644 index 00000000..29305895 --- /dev/null +++ b/latitude/README.md @@ -0,0 +1,119 @@ +# Latitude.sh + +Bare metal and VM cloud servers via REST API. [Latitude.sh](https://www.latitude.sh/) + +## Agents + +#### Claude Code + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/claude.sh) +``` + +#### OpenClaw + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/openclaw.sh) +``` + +#### NanoClaw + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/nanoclaw.sh) +``` + +#### Aider + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/aider.sh) +``` + +#### Goose + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/goose.sh) +``` + +#### Codex CLI + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/codex.sh) +``` + +#### Open Interpreter + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/interpreter.sh) +``` + +#### Gemini CLI + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/gemini.sh) +``` + +#### Amazon Q CLI + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/amazonq.sh) +``` + +#### Cline + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/cline.sh) +``` + +#### gptme + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/gptme.sh) +``` + +#### OpenCode + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/opencode.sh) +``` + +#### Plandex + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/plandex.sh) +``` + +## Non-Interactive Mode + +```bash +LATITUDE_SERVER_NAME=dev-mk1 \ +LATITUDE_API_KEY=your-api-key \ +OPENROUTER_API_KEY=sk-or-v1-xxxxx \ + bash <(curl -fsSL https://openrouter.ai/lab/spawn/latitude/claude.sh) +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `LATITUDE_API_KEY` | Latitude.sh API key (required) | +| `LATITUDE_SERVER_NAME` | Server hostname (prompted if not set) | +| `LATITUDE_PROJECT_ID` | Project ID (auto-detected from first project) | +| `LATITUDE_PLAN` | Server plan (default: `vm.tiny`) | +| `LATITUDE_SITE` | Data center site (default: `DAL2`) | +| `LATITUDE_OS` | Operating system (default: `ubuntu_24_04_x64_lts`) | +| `OPENROUTER_API_KEY` | OpenRouter API key for agent access | + +## Available Plans + +| Plan | Specs | Price | +|------|-------|-------| +| `vm.tiny` | 4 vCPUs, 8GB RAM | $0.07/hr | +| `vm.small` | 8 vCPUs, 16GB RAM | $0.14/hr | +| `vm.medium` | 12 vCPUs, 24GB RAM | $0.25/hr | +| `m4.metal.small` | AMD 4244P (6 cores), 64GB RAM | $0.37/hr | + +## Available Sites + +US (Dallas, LAX, NYC, Chicago, Ashburn, Miami, Silicon Valley), Brazil, Australia, Chile, Japan, Mexico, UK, Germany, Argentina, Colombia, Singapore, Netherlands. + +Get your API key at: https://www.latitude.sh/dashboard (Settings & Billing -> API Keys) diff --git a/latitude/aider.sh b/latitude/aider.sh new file mode 100644 index 00000000..820a721f --- /dev/null +++ b/latitude/aider.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -eo pipefail + +# 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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "Aider on Latitude.sh" +echo "" + +# 1. Resolve Latitude.sh API token +ensure_latitude_token + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get server name and create server +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 4. Wait for server to become active and get IP +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 + +# 5. Wait for SSH connectivity +verify_server_connectivity "${LATITUDE_SERVER_IP}" + +# 6. Install base tools and Aider +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing Aider..." +run_server "${LATITUDE_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat" + +# Verify installation succeeded +if ! run_server "${LATITUDE_SERVER_IP}" "command -v aider &> /dev/null && aider --version &> /dev/null"; then + log_error "Aider installation verification failed" + log_error "The 'aider' command is not available or not working properly on server ${LATITUDE_SERVER_IP}" + exit 1 +fi +log_info "Aider installation verified successfully" + +# 7. 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 + +# Get model preference +MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1 + +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +# 8. Start Aider interactively +log_warn "Starting Aider..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}" diff --git a/latitude/amazonq.sh b/latitude/amazonq.sh new file mode 100644 index 00000000..5b8554da --- /dev/null +++ b/latitude/amazonq.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eo pipefail + +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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "Amazon Q on Latitude.sh" +echo "" + +ensure_latitude_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 +verify_server_connectivity "${LATITUDE_SERVER_IP}" +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing Amazon Q CLI..." +run_server "${LATITUDE_SERVER_IP}" "curl -fsSL https://desktop-release.q.us-east-1.amazonaws.com/latest/amazon-q-cli-install.sh | bash" +log_info "Amazon Q CLI 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +log_warn "Starting Amazon Q..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && q chat" diff --git a/latitude/claude.sh b/latitude/claude.sh new file mode 100644 index 00000000..85b7524e --- /dev/null +++ b/latitude/claude.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -eo pipefail + +# 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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "Claude Code on Latitude.sh" +echo "" + +# 1. Resolve Latitude.sh API token +ensure_latitude_token + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get server name and create server +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 4. Wait for server to become active and get IP +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 + +# 5. Wait for SSH connectivity +verify_server_connectivity "${LATITUDE_SERVER_IP}" + +# 6. Install base tools and Claude Code +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing Claude Code..." +run_server "${LATITUDE_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" + +# Verify installation succeeded +if ! run_server "${LATITUDE_SERVER_IP}" "command -v claude &> /dev/null && claude --version &> /dev/null"; then + log_error "Claude Code installation verification failed" + log_error "The 'claude' command is not available or not working properly on server ${LATITUDE_SERVER_IP}" + exit 1 +fi +log_info "Claude Code installation verified successfully" + +# 7. 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 + +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_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" + +# 8. Configure Claude Code settings +setup_claude_code_config "${OPENROUTER_API_KEY}" \ + "upload_file ${LATITUDE_SERVER_IP}" \ + "run_server ${LATITUDE_SERVER_IP}" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +# 9. Start Claude Code interactively +log_warn "Starting Claude Code..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && claude" diff --git a/latitude/cline.sh b/latitude/cline.sh new file mode 100644 index 00000000..4297fd56 --- /dev/null +++ b/latitude/cline.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eo pipefail + +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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "Cline on Latitude.sh" +echo "" + +ensure_latitude_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 +verify_server_connectivity "${LATITUDE_SERVER_IP}" +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing Cline..." +run_server "${LATITUDE_SERVER_IP}" "npm install -g cline" +log_info "Cline 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +log_warn "Starting Cline..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && cline" diff --git a/latitude/codex.sh b/latitude/codex.sh new file mode 100644 index 00000000..564e87b2 --- /dev/null +++ b/latitude/codex.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eo pipefail + +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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "Codex CLI on Latitude.sh" +echo "" + +ensure_latitude_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 +verify_server_connectivity "${LATITUDE_SERVER_IP}" +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing Codex CLI..." +run_server "${LATITUDE_SERVER_IP}" "npm install -g @openai/codex" +log_info "Codex CLI 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +log_warn "Starting Codex..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && codex" diff --git a/latitude/gemini.sh b/latitude/gemini.sh new file mode 100644 index 00000000..904c2a92 --- /dev/null +++ b/latitude/gemini.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -eo pipefail + +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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "Gemini CLI on Latitude.sh" +echo "" + +ensure_latitude_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 +verify_server_connectivity "${LATITUDE_SERVER_IP}" +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing Gemini CLI..." +run_server "${LATITUDE_SERVER_IP}" "npm install -g @google/gemini-cli" +log_info "Gemini CLI 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "GEMINI_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +log_warn "Starting Gemini..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && gemini" diff --git a/latitude/goose.sh b/latitude/goose.sh new file mode 100644 index 00000000..cce5fb31 --- /dev/null +++ b/latitude/goose.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -eo pipefail + +# 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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "Goose on Latitude.sh" +echo "" + +# 1. Resolve Latitude.sh API token +ensure_latitude_token + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get server name and create server +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 4. Wait for server to become active and get IP +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 + +# 5. Wait for SSH connectivity +verify_server_connectivity "${LATITUDE_SERVER_IP}" + +# 6. Install base tools and Goose +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing Goose..." +run_server "${LATITUDE_SERVER_IP}" "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash" + +# Verify installation succeeded +if ! run_server "${LATITUDE_SERVER_IP}" "command -v goose &> /dev/null && goose --version &> /dev/null"; then + log_error "Goose installation verification failed" + log_error "The 'goose' command is not available or not working properly on server ${LATITUDE_SERVER_IP}" + exit 1 +fi +log_info "Goose installation verified successfully" + +# 7. 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 + +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "GOOSE_PROVIDER=openrouter" \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +# 8. Start Goose interactively +log_warn "Starting Goose..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && goose" diff --git a/latitude/gptme.sh b/latitude/gptme.sh new file mode 100644 index 00000000..c2bb725f --- /dev/null +++ b/latitude/gptme.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -eo pipefail + +# 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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "gptme on Latitude.sh" +echo "" + +# 1. Resolve Latitude.sh API token +ensure_latitude_token + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get server name and create server +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 4. Wait for server to become active and get IP +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 + +# 5. Wait for SSH connectivity +verify_server_connectivity "${LATITUDE_SERVER_IP}" + +# 6. Install base tools and gptme +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing gptme..." +run_server "${LATITUDE_SERVER_IP}" "pip install gptme 2>/dev/null || pip3 install gptme" + +# Verify installation succeeded +if ! run_server "${LATITUDE_SERVER_IP}" "command -v gptme &> /dev/null && gptme --version &> /dev/null"; then + log_error "gptme installation verification failed" + log_error "The 'gptme' command is not available or not working properly on server ${LATITUDE_SERVER_IP}" + exit 1 +fi +log_info "gptme installation verified successfully" + +# 7. 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 + +# Get model preference +MODEL_ID=$(get_model_id_interactive "openrouter/auto" "gptme") || exit 1 + +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +# 8. Start gptme interactively +log_warn "Starting gptme..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}" diff --git a/latitude/interpreter.sh b/latitude/interpreter.sh new file mode 100644 index 00000000..aa65a048 --- /dev/null +++ b/latitude/interpreter.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -eo pipefail + +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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "Open Interpreter on Latitude.sh" +echo "" + +ensure_latitude_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 +verify_server_connectivity "${LATITUDE_SERVER_IP}" +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing Open Interpreter..." +run_server "${LATITUDE_SERVER_IP}" "pip install open-interpreter 2>/dev/null || pip3 install open-interpreter" +log_info "Open Interpreter 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ + "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +log_warn "Starting Open Interpreter..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && interpreter" diff --git a/latitude/lib/common.sh b/latitude/lib/common.sh new file mode 100644 index 00000000..3422a46d --- /dev/null +++ b/latitude/lib/common.sh @@ -0,0 +1,419 @@ +#!/bin/bash +set -eo pipefail +# Common bash functions for Latitude.sh spawn scripts + +# ============================================================ +# Provider-agnostic functions +# ============================================================ + +# Source shared provider-agnostic functions (local or remote fallback) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/../../shared/common.sh" ]]; then + source "$SCRIPT_DIR/../../shared/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/shared/common.sh)" +fi + +# ============================================================ +# Latitude.sh specific functions +# ============================================================ + +readonly LATITUDE_API_BASE="https://api.latitude.sh" + +# Centralized curl wrapper for Latitude.sh API +latitude_api() { + local method="$1" + local endpoint="$2" + local body="${3:-}" + # shellcheck disable=SC2154 + generic_cloud_api "$LATITUDE_API_BASE" "$LATITUDE_API_KEY" "$method" "$endpoint" "$body" +} + +# Test Latitude.sh API token validity +test_latitude_token() { + local response + response=$(latitude_api GET "/projects") + if echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); sys.exit(0 if 'data' in d else 1)" 2>/dev/null; then + return 0 + fi + local error_msg + error_msg=$(echo "$response" | python3 -c " +import json,sys +try: + d=json.loads(sys.stdin.read()) + errors = d.get('errors', d.get('error', {})) + if isinstance(errors, list) and errors: + print(errors[0].get('detail', errors[0].get('title', 'Unknown error'))) + elif isinstance(errors, dict): + print(errors.get('detail', errors.get('message', 'Unknown error'))) + else: + print('Unknown error') +except: print('Unable to parse error') +" 2>/dev/null || echo "Unable to parse error") + log_error "API Error: $error_msg" + log_error "" + log_error "How to fix:" + log_error " 1. Verify your API key at: https://www.latitude.sh/dashboard → Settings & Billing → API Keys" + log_error " 2. Ensure the API key has not expired" + log_error " 3. Check that you have an active project" + return 1 +} + +# Ensure LATITUDE_API_KEY is available (env var -> config file -> prompt+save) +ensure_latitude_token() { + ensure_api_token_with_provider \ + "Latitude.sh" \ + "LATITUDE_API_KEY" \ + "$HOME/.config/spawn/latitude.json" \ + "https://www.latitude.sh/dashboard → Settings & Billing → API Keys" \ + "test_latitude_token" +} + +# Get the default project ID from the Latitude.sh account +get_latitude_project_id() { + if [[ -n "${LATITUDE_PROJECT_ID:-}" ]]; then + echo "$LATITUDE_PROJECT_ID" + return 0 + fi + + local response + response=$(latitude_api GET "/projects") + local project_id + project_id=$(echo "$response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +projects = data.get('data', []) +if not projects: + sys.exit(1) +# Use first project +print(projects[0]['id']) +" 2>/dev/null) + + if [[ -z "$project_id" ]]; then + log_error "No projects found in your Latitude.sh account" + log_error "Create a project at: https://www.latitude.sh/dashboard" + return 1 + fi + + LATITUDE_PROJECT_ID="$project_id" + export LATITUDE_PROJECT_ID + log_info "Using Latitude.sh project: $project_id" + echo "$project_id" +} + +# Check if SSH key is registered with Latitude.sh +latitude_check_ssh_key() { + local fingerprint="$1" + local existing_keys + existing_keys=$(latitude_api GET "/ssh_keys") + echo "$existing_keys" | grep -q "$fingerprint" +} + +# Register SSH key with Latitude.sh +latitude_register_ssh_key() { + local key_name="$1" + local pub_path="$2" + local pub_key + pub_key=$(cat "$pub_path") + local json_pub_key + json_pub_key=$(json_escape "$pub_key") + + local body + body=$(python3 -c " +import json +body = { + 'data': { + 'type': 'ssh_keys', + 'attributes': { + 'name': '$key_name', + 'public_key': json.loads($json_pub_key) + } + } +} +print(json.dumps(body)) +") + + local response + response=$(latitude_api POST "/ssh_keys" "$body") + + if echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); sys.exit(0 if 'data' in d else 1)" 2>/dev/null; then + return 0 + fi + + local error_msg + error_msg=$(echo "$response" | python3 -c " +import json,sys +try: + d=json.loads(sys.stdin.read()) + errors = d.get('errors', []) + if isinstance(errors, list) and errors: + print(errors[0].get('detail', errors[0].get('title', 'Unknown error'))) + else: + print('Unknown error') +except: print(sys.stdin.read()) +" 2>/dev/null || echo "$response") + log_error "API Error: $error_msg" + log_error "" + log_error "Common causes:" + log_error " - SSH key already registered with this name" + log_error " - Invalid SSH key format (must be valid ed25519 public key)" + log_error " - API key lacks write permissions" + return 1 +} + +# Ensure SSH key exists locally and is registered with Latitude.sh +ensure_ssh_key() { + ensure_ssh_key_with_provider latitude_check_ssh_key latitude_register_ssh_key "Latitude.sh" +} + +# Get server name from env var or prompt +get_server_name() { + local server_name + server_name=$(get_resource_name "LATITUDE_SERVER_NAME" "Enter server name: ") || return 1 + + if ! validate_server_name "$server_name"; then + return 1 + fi + + echo "$server_name" +} + +# Create a Latitude.sh server +create_server() { + local hostname="$1" + local plan="${LATITUDE_PLAN:-vm.tiny}" + local site="${LATITUDE_SITE:-DAL2}" + local os="${LATITUDE_OS:-ubuntu_24_04_x64_lts}" + + log_warn "Creating Latitude.sh server '$hostname' (plan: $plan, site: $site)..." + + # Get project ID + local project_id + project_id=$(get_latitude_project_id) || return 1 + + # Get all SSH key IDs + local ssh_keys_response + ssh_keys_response=$(latitude_api GET "/ssh_keys") + local ssh_key_ids + ssh_key_ids=$(echo "$ssh_keys_response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +ids = [k['id'] for k in data.get('data', [])] +print(json.dumps(ids)) +" 2>/dev/null || echo "[]") + + local body + body=$(python3 -c " +import json +body = { + 'data': { + 'type': 'servers', + 'attributes': { + 'hostname': '$hostname', + 'plan': '$plan', + 'site': '$site', + 'operating_system': '$os', + 'project': '$project_id', + 'ssh_keys': $ssh_key_ids + } + } +} +print(json.dumps(body)) +") + + local response + response=$(latitude_api POST "/servers" "$body") + + # Check for errors + if ! echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); sys.exit(0 if 'data' in d else 1)" 2>/dev/null; then + log_error "Failed to create Latitude.sh server" + local error_msg + error_msg=$(echo "$response" | python3 -c " +import json,sys +try: + d=json.loads(sys.stdin.read()) + errors = d.get('errors', []) + if isinstance(errors, list) and errors: + print(errors[0].get('detail', errors[0].get('title', 'Unknown error'))) + else: + print('Unknown error') +except: print(sys.stdin.read()) +" 2>/dev/null || echo "$response") + log_error "API Error: $error_msg" + log_error "" + log_error "Common issues:" + log_error " - Insufficient account balance or payment method required" + log_error " - Plan/site unavailable (try different LATITUDE_PLAN or LATITUDE_SITE)" + log_error " - Server limit reached for your account" + log_error "" + log_error "Check your account status: https://www.latitude.sh/dashboard" + return 1 + fi + + # Extract server ID + LATITUDE_SERVER_ID=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['data']['id'])") + export LATITUDE_SERVER_ID + + log_info "Server created: ID=$LATITUDE_SERVER_ID" + log_warn "Waiting for server provisioning (this may take a few minutes for bare metal)..." +} + +# Wait for server to become active and get its IP address +wait_for_server_ready() { + local server_id="$1" + local max_attempts=${2:-60} + local attempt=1 + + log_warn "Waiting for server $server_id to become active..." + while [[ "$attempt" -le "$max_attempts" ]]; do + local response + response=$(latitude_api GET "/servers/$server_id") + + local status + status=$(echo "$response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +server = data.get('data', {}) +attrs = server.get('attributes', {}) +print(attrs.get('status', 'unknown')) +" 2>/dev/null || echo "unknown") + + if [[ "$status" == "on" ]] || [[ "$status" == "active" ]]; then + # Extract IP address + LATITUDE_SERVER_IP=$(echo "$response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +server = data.get('data', {}) +attrs = server.get('attributes', {}) +# Check for IP in network attributes +network = attrs.get('network', {}) +if isinstance(network, dict): + ip = network.get('ip', '') + if ip: + print(ip) + sys.exit(0) +# Check for IP in relationships or included data +ips = attrs.get('ip_addresses', []) +if isinstance(ips, list): + for ip_obj in ips: + if isinstance(ip_obj, dict): + addr = ip_obj.get('address', '') + if addr and ':' not in addr: # Skip IPv6 + print(addr) + sys.exit(0) + elif isinstance(ip_obj, str) and ':' not in ip_obj: + print(ip_obj) + sys.exit(0) +# Fallback: try primary_ipv4 +primary = attrs.get('primary_ipv4', '') +if primary: + print(primary) + sys.exit(0) +sys.exit(1) +" 2>/dev/null) + + if [[ -n "$LATITUDE_SERVER_IP" ]]; then + export LATITUDE_SERVER_IP + log_info "Server active: IP=$LATITUDE_SERVER_IP" + return 0 + fi + + # IP might not be assigned yet, keep waiting + log_warn "Server active but IP not yet assigned... (attempt $attempt/$max_attempts)" + else + log_warn "Server status: $status (attempt $attempt/$max_attempts)" + fi + + sleep 10 + attempt=$((attempt + 1)) + done + + log_error "Server failed to become active after $max_attempts attempts" + return 1 +} + +# Wait for SSH connectivity +verify_server_connectivity() { + local ip="$1" + local max_attempts=${2:-30} + # shellcheck disable=SC2154 + generic_ssh_wait "root" "$ip" "$SSH_OPTS -o ConnectTimeout=5" "echo ok" "SSH connectivity" "$max_attempts" 5 +} + +# Run a command on the server +run_server() { + local ip="$1" + local cmd="$2" + # shellcheck disable=SC2086 + ssh $SSH_OPTS "root@$ip" "$cmd" +} + +# Upload a file to the server +upload_file() { + local ip="$1" + local local_path="$2" + local remote_path="$3" + # shellcheck disable=SC2086 + scp $SSH_OPTS "$local_path" "root@$ip:$remote_path" +} + +# Start an interactive SSH session +interactive_session() { + local ip="$1" + local cmd="$2" + # shellcheck disable=SC2086 + ssh -t $SSH_OPTS "root@$ip" "$cmd" +} + +# Destroy a Latitude.sh server +destroy_server() { + local server_id="$1" + + log_warn "Destroying server $server_id..." + local response + response=$(latitude_api DELETE "/servers/$server_id") + + if echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); sys.exit(0 if d.get('errors') else 1)" 2>/dev/null; then + log_error "Failed to destroy server: $response" + return 1 + fi + + log_info "Server $server_id destroyed" +} + +# List all Latitude.sh servers +list_servers() { + local response + response=$(latitude_api GET "/servers") + + python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +servers = data.get('data', []) +if not servers: + print('No servers found') + sys.exit(0) +print(f\"{'HOSTNAME':<25} {'ID':<15} {'STATUS':<12} {'PLAN':<15} {'SITE':<10}\") +print('-' * 77) +for s in servers: + attrs = s.get('attributes', {}) + hostname = attrs.get('hostname', 'N/A') + sid = str(s.get('id', 'N/A')) + status = attrs.get('status', 'N/A') + plan = attrs.get('plan', 'N/A') + site = attrs.get('site', 'N/A') + print(f'{hostname:<25} {sid:<15} {status:<12} {plan:<15} {site:<10}') +" <<< "$response" +} + +# Install basic tools on the server (cloud-init equivalent for Latitude.sh) +install_base_tools() { + local ip="$1" + log_warn "Installing base tools..." + run_server "$ip" "apt-get update -qq && apt-get install -y -qq curl unzip git zsh > /dev/null 2>&1" + log_warn "Installing Bun..." + run_server "$ip" "curl -fsSL https://bun.sh/install | bash" + run_server "$ip" "printf '%s\n' 'export PATH=\"\${HOME}/.bun/bin:\${PATH}\"' >> /root/.bashrc" + run_server "$ip" "printf '%s\n' 'export PATH=\"\${HOME}/.bun/bin:\${PATH}\"' >> /root/.zshrc" + log_info "Base tools installed" +} diff --git a/latitude/nanoclaw.sh b/latitude/nanoclaw.sh new file mode 100644 index 00000000..6ff52e4d --- /dev/null +++ b/latitude/nanoclaw.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -eo pipefail + +# 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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "NanoClaw on Latitude.sh" +echo "" + +# 1. Resolve Latitude.sh API token +ensure_latitude_token + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get server name and create server +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 4. Wait for server to become active and get IP +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 + +# 5. Wait for SSH connectivity +verify_server_connectivity "${LATITUDE_SERVER_IP}" + +# 6. Install base tools and nanoclaw +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing tsx..." +run_server "${LATITUDE_SERVER_IP}" "source ~/.bashrc && bun install -g tsx" + +log_warn "Cloning and building nanoclaw..." +run_server "${LATITUDE_SERVER_IP}" "git clone https://github.com/gavrielc/nanoclaw.git ~/nanoclaw && cd ~/nanoclaw && npm install && npm run build" +log_info "NanoClaw installed" + +# 7. 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 + +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" + +# 8. Create nanoclaw .env file +log_warn "Configuring nanoclaw..." + +DOTENV_TEMP=$(mktemp) +track_temp_file "${DOTENV_TEMP}" +chmod 600 "${DOTENV_TEMP}" +cat > "${DOTENV_TEMP}" << EOF +ANTHROPIC_API_KEY=${OPENROUTER_API_KEY} +EOF + +upload_file "${LATITUDE_SERVER_IP}" "${DOTENV_TEMP}" "/root/nanoclaw/.env" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_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 "${LATITUDE_SERVER_IP}" "cd ~/nanoclaw && source ~/.zshrc && npm run dev" diff --git a/latitude/openclaw.sh b/latitude/openclaw.sh new file mode 100644 index 00000000..a1aec5c5 --- /dev/null +++ b/latitude/openclaw.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -eo pipefail + +# 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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "OpenClaw on Latitude.sh" +echo "" + +# 1. Resolve Latitude.sh API token +ensure_latitude_token + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get server name and create server +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 4. Wait for server to become active and get IP +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 + +# 5. Wait for SSH connectivity +verify_server_connectivity "${LATITUDE_SERVER_IP}" + +# 6. Install base tools and openclaw +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing openclaw..." +run_server "${LATITUDE_SERVER_IP}" "source ~/.bashrc && bun install -g openclaw" +log_info "OpenClaw installed" + +# 7. 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 + +# Get model preference +MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Openclaw") || exit 1 + +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" + +# 8. Configure openclaw +setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" \ + "upload_file ${LATITUDE_SERVER_IP}" \ + "run_server ${LATITUDE_SERVER_IP}" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +# 9. Start openclaw gateway in background and launch TUI +log_warn "Starting openclaw..." +run_server "${LATITUDE_SERVER_IP}" "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" +sleep 2 +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && openclaw tui" diff --git a/latitude/opencode.sh b/latitude/opencode.sh new file mode 100644 index 00000000..67b4148b --- /dev/null +++ b/latitude/opencode.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -eo pipefail + +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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "OpenCode on Latitude.sh" +echo "" + +ensure_latitude_token +ensure_ssh_key + +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 +verify_server_connectivity "${LATITUDE_SERVER_IP}" +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing OpenCode..." +run_server "${LATITUDE_SERVER_IP}" "$(opencode_install_cmd)" +log_info "OpenCode 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_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +log_warn "Starting OpenCode..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && opencode" diff --git a/latitude/plandex.sh b/latitude/plandex.sh new file mode 100644 index 00000000..0d87e1ab --- /dev/null +++ b/latitude/plandex.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -eo pipefail + +# 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 + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/latitude/lib/common.sh)" +fi + +log_info "Plandex on Latitude.sh" +echo "" + +# 1. Resolve Latitude.sh API token +ensure_latitude_token + +# 2. Generate + register SSH key +ensure_ssh_key + +# 3. Get server name and create server +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 4. Wait for server to become active and get IP +wait_for_server_ready "${LATITUDE_SERVER_ID}" 60 + +# 5. Wait for SSH connectivity +verify_server_connectivity "${LATITUDE_SERVER_IP}" + +# 6. Install base tools and Plandex +install_base_tools "${LATITUDE_SERVER_IP}" + +log_warn "Installing Plandex..." +run_server "${LATITUDE_SERVER_IP}" "curl -sL https://plandex.ai/install.sh | bash" + +# Verify installation succeeded +if ! run_server "${LATITUDE_SERVER_IP}" "command -v plandex &> /dev/null && plandex version &> /dev/null"; then + log_error "Plandex installation verification failed" + log_error "The 'plandex' command is not available or not working properly on server ${LATITUDE_SERVER_IP}" + exit 1 +fi +log_info "Plandex installation verified successfully" + +# 7. 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 + +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${LATITUDE_SERVER_IP}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Latitude.sh server setup completed successfully!" +log_info "Server: ${SERVER_NAME} (ID: ${LATITUDE_SERVER_ID}, IP: ${LATITUDE_SERVER_IP})" +echo "" + +# 8. Start Plandex interactively +log_warn "Starting Plandex..." +sleep 1 +clear +interactive_session "${LATITUDE_SERVER_IP}" "source ~/.zshrc && plandex" diff --git a/manifest.json b/manifest.json index d47001de..e8a8623f 100644 --- a/manifest.json +++ b/manifest.json @@ -479,6 +479,22 @@ "image": "Ubuntu 24.04" }, "notes": "GPU cloud provider with NVIDIA RTX 3080/3090, A100, and H100 instances. European data centers (Iceland, Norway). Requires GENESIS_API_KEY from https://developers.genesiscloud.com/" + }, + "latitude": { + "name": "Latitude.sh", + "description": "Latitude.sh bare metal and VM servers via REST API", + "url": "https://www.latitude.sh/", + "type": "api", + "auth": "LATITUDE_API_KEY", + "provision_method": "POST /servers with SSH keys", + "exec_method": "ssh root@IP", + "interactive_method": "ssh -t root@IP", + "defaults": { + "plan": "vm.tiny", + "site": "DAL2", + "os": "ubuntu_24_04_x64_lts" + }, + "notes": "Bare metal and VM cloud provider with global locations. Hourly billing. VMs from $0.07/hr. Requires LATITUDE_API_KEY from https://www.latitude.sh/dashboard" } }, "matrix": { @@ -715,6 +731,19 @@ "genesiscloud/cline": "implemented", "genesiscloud/gptme": "implemented", "genesiscloud/opencode": "implemented", - "genesiscloud/plandex": "implemented" + "genesiscloud/plandex": "implemented", + "latitude/claude": "implemented", + "latitude/openclaw": "implemented", + "latitude/nanoclaw": "implemented", + "latitude/aider": "implemented", + "latitude/goose": "implemented", + "latitude/codex": "implemented", + "latitude/interpreter": "implemented", + "latitude/gemini": "implemented", + "latitude/amazonq": "implemented", + "latitude/cline": "implemented", + "latitude/gptme": "implemented", + "latitude/opencode": "implemented", + "latitude/plandex": "implemented" } }