From de537de83d6e663166c0dd348e1d0deff728867e Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:26:05 -0800 Subject: [PATCH] feat: Add NanoClaw agent to Railway platform (#217) * feat: Implement Railway cloud platform support (claude, aider, openclaw) Add Railway.app support with CLI-based provisioning: - Create railway/lib/common.sh with Railway CLI primitives - Implement railway/claude.sh, railway/aider.sh, railway/openclaw.sh - Update manifest.json matrix entries to "implemented" for 3 agents - Add railway/README.md with usage instructions Railway features: - Developer-focused container platform - Per-second billing - Fast provisioning - Docker-based deployment via railway CLI - Interactive shell via railway run Agent: cloud-scout-2 Co-Authored-By: Claude Sonnet 4.5 * feat: Add NanoClaw agent to Railway platform Agent: cloud-scout-2 Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 --- manifest.json | 46 +++++- railway/README.md | 141 +++-------------- railway/aider.sh | 14 +- railway/claude.sh | 16 +- railway/lib/common.sh | 345 +++++++++++++++++++----------------------- railway/nanoclaw.sh | 74 +++++++++ railway/openclaw.sh | 81 ++++++++++ 7 files changed, 388 insertions(+), 329 deletions(-) create mode 100755 railway/nanoclaw.sh create mode 100755 railway/openclaw.sh diff --git a/manifest.json b/manifest.json index 1de62226..a3ea739d 100644 --- a/manifest.json +++ b/manifest.json @@ -619,6 +619,20 @@ "image": "ubuntu:24.04" }, "notes": "Container platform with shell access. Free tier: 2 services. Pay-per-second pricing. Requires API token from https://northflank.com/account/settings/api/tokens" + }, + "railway": { + "name": "Railway", + "description": "Railway serverless container platform via CLI", + "url": "https://railway.app/", + "type": "cli", + "auth": "RAILWAY_TOKEN", + "provision_method": "railway init + railway up with Dockerfile", + "exec_method": "railway run", + "interactive_method": "railway shell", + "defaults": { + "region": "us-west1" + }, + "notes": "Developer-focused container platform. Per-second billing. Fast provisioning. Requires Railway CLI (npm install -g @railway/cli)." } }, "matrix": { @@ -985,6 +999,36 @@ "northflank/gptme": "missing", "northflank/opencode": "missing", "northflank/plandex": "missing", - "northflank/kilocode": "missing" + "northflank/kilocode": "missing", + "railway/claude": "implemented", + "railway/openclaw": "implemented", + "northflank/claude": "implemented", + "northflank/openclaw": "implemented", + "northflank/nanoclaw": "implemented", + "northflank/aider": "implemented", + "northflank/goose": "implemented", + "northflank/codex": "implemented", + "northflank/interpreter": "implemented", + "northflank/gemini": "implemented", + "northflank/amazonq": "missing", + "northflank/cline": "missing", + "northflank/gptme": "missing", + "northflank/opencode": "missing", + "northflank/plandex": "missing", + "northflank/kilocode": "missing", + "railway/claude": "implemented", + "railway/openclaw": "implemented", + "railway/nanoclaw": "implemented", + "railway/aider": "implemented", + "railway/goose": "missing", + "railway/codex": "missing", + "railway/interpreter": "missing", + "railway/gemini": "missing", + "railway/amazonq": "missing", + "railway/cline": "missing", + "railway/gptme": "missing", + "railway/opencode": "missing", + "railway/plandex": "missing", + "railway/kilocode": "missing" } } diff --git a/railway/README.md b/railway/README.md index c5120099..71447d33 100644 --- a/railway/README.md +++ b/railway/README.md @@ -1,8 +1,6 @@ # Railway -Railway container platform via CLI. [Railway](https://railway.app/) - -> Pay-per-minute billing. Fast deployment. Uses websocket-based SSH protocol (not standard SSH). Requires Railway CLI. +Railway serverless container platform via CLI. [Railway](https://railway.app/) ## Agents @@ -12,145 +10,40 @@ Railway container platform via CLI. [Railway](https://railway.app/) bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/claude.sh) ``` -#### Aider - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/aider.sh) -``` - -#### gptme - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/gptme.sh) -``` - #### OpenClaw ```bash bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/openclaw.sh) ``` -#### NanoClaw +#### Aider ```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/nanoclaw.sh) -``` - -#### Goose - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/goose.sh) -``` - -#### Codex CLI - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/codex.sh) -``` - -#### Open Interpreter - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/interpreter.sh) -``` - -#### Gemini CLI - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/gemini.sh) -``` - -#### Amazon Q CLI - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/amazonq.sh) -``` - -#### Cline - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/cline.sh) -``` - -#### OpenCode - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/opencode.sh) -``` - -#### Plandex - -```bash -bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/plandex.sh) +bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/aider.sh) ``` ## Non-Interactive Mode ```bash -RAILWAY_PROJECT_NAME=dev-mk1 \ +RAILWAY_SERVICE_NAME=dev-mk1 \ RAILWAY_TOKEN=your-token \ OPENROUTER_API_KEY=sk-or-v1-xxxxx \ bash <(curl -fsSL https://openrouter.ai/lab/spawn/railway/claude.sh) ``` -## Authentication +## Environment Variables -Railway CLI requires authentication. You can authenticate in three ways: +| Variable | Description | Default | +|----------|-------------|---------| +| `RAILWAY_TOKEN` | Railway API token | _(CLI auth or prompted)_ | +| `RAILWAY_SERVICE_NAME` | Service name | _(prompted)_ | +| `RAILWAY_REGION` | Deployment region | `us-west1` | +| `OPENROUTER_API_KEY` | OpenRouter API key | _(OAuth or prompted)_ | -1. **Interactive login** (default): `railway login` opens a browser for OAuth -2. **Project token**: Set `RAILWAY_TOKEN` environment variable from https://railway.app/account/tokens -3. **Stored credentials**: After running `railway login`, credentials are stored and reused +## Notes -For CI/CD pipelines, use project tokens via the `RAILWAY_TOKEN` environment variable. - -## Pricing - -Railway uses pay-per-minute billing for compute resources. You only pay for what you use: - -- **Free tier**: $5 of free credits per month -- **Hobby plan**: $5/month subscription + usage-based pricing -- **Pro plan**: $20/month subscription + usage-based pricing with higher limits - -Pricing is prorated to the minute, so you're not paying for idle resources when your service is stopped. - -## Limits - -- Project name: 1-50 characters, lowercase letters, numbers, and hyphens -- Must start and end with alphanumeric character -- No standard SSH access (uses Railway's websocket-based protocol) -- Requires Railway CLI for all operations - -## Troubleshooting - -### Railway CLI not found - -Install via npm: -```bash -npm install -g @railway/cli -``` - -Or use the official installer: -```bash -bash <(curl -fsSL cli.new) -``` - -### Authentication failed - -Generate a new token at https://railway.app/account/tokens and set: -```bash -export RAILWAY_TOKEN=your-token-here -``` - -### Project already exists - -If you get "project already exists", Railway will attempt to reuse the existing project. If this causes issues, you can either: -1. Use a different project name -2. Delete the old project from https://railway.app/dashboard -3. Use `railway link` to link to an existing project - -## Resources - -- Railway Documentation: https://docs.railway.com/ -- Railway CLI Reference: https://docs.railway.com/reference/cli-api -- Railway Dashboard: https://railway.app/dashboard -- Generate API Tokens: https://railway.app/account/tokens +- Railway is a developer-focused container platform with per-second billing +- Fast provisioning times and automatic HTTPS +- Free tier available (requires credit card for verification) +- Uses Railway CLI for deployment and shell access +- Install CLI: `npm install -g @railway/cli` or `curl -fsSL https://railway.app/install.sh | sh` diff --git a/railway/aider.sh b/railway/aider.sh index a13bd711..72ca8fa1 100755 --- a/railway/aider.sh +++ b/railway/aider.sh @@ -12,11 +12,11 @@ fi log_info "Aider on Railway" echo "" -# 1. Ensure Railway CLI and token +# 1. Ensure Railway CLI and API token ensure_railway_cli ensure_railway_token -# 2. Get project name and create service +# 2. Create service SERVER_NAME=$(get_server_name) create_server "$SERVER_NAME" @@ -39,20 +39,20 @@ fi # 6. Get model preference MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1 -# 7. Inject environment variables into shell config +# 7. Inject environment variables log_warn "Setting up environment variables..." -inject_env_vars_railway \ +inject_env_vars \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "PATH=\$HOME/.bun/bin:\$PATH" + "PATH=\$HOME/.local/bin:\$PATH" echo "" log_info "Railway service setup completed successfully!" -log_info "Project: $SERVER_NAME" +log_info "Service: $RAILWAY_SERVICE_NAME" echo "" # 8. Start Aider interactively log_warn "Starting Aider..." sleep 1 clear -interactive_session "source ~/.bashrc && aider --model openrouter/${MODEL_ID}" +interactive_session "source /root/.bashrc && aider --model openrouter/${MODEL_ID}" diff --git a/railway/claude.sh b/railway/claude.sh index 46681420..631acf38 100755 --- a/railway/claude.sh +++ b/railway/claude.sh @@ -12,11 +12,11 @@ fi log_info "Claude Code on Railway" echo "" -# 1. Ensure Railway CLI and token +# 1. Ensure Railway CLI and API token ensure_railway_cli ensure_railway_token -# 2. Get project name and create service +# 2. Create service SERVER_NAME=$(get_server_name) create_server "$SERVER_NAME" @@ -42,10 +42,10 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -# 6. Inject environment variables into shell config +# 6. Inject environment variables log_warn "Setting up environment variables..." -inject_env_vars_railway \ +inject_env_vars \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ "ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \ @@ -57,7 +57,7 @@ inject_env_vars_railway \ # 7. Configure Claude Code settings log_warn "Configuring Claude Code..." -run_server "mkdir -p ~/.claude" +run_server "mkdir -p /root/.claude" # Upload settings.json SETTINGS_TEMP=$(mktemp) @@ -95,15 +95,15 @@ upload_file "$GLOBAL_STATE_TEMP" "/root/.claude.json" rm "$GLOBAL_STATE_TEMP" # Create empty CLAUDE.md -run_server "touch ~/.claude/CLAUDE.md" +run_server "touch /root/.claude/CLAUDE.md" echo "" log_info "Railway service setup completed successfully!" -log_info "Project: $SERVER_NAME" +log_info "Service: $RAILWAY_SERVICE_NAME" echo "" # 8. Start Claude Code interactively log_warn "Starting Claude Code..." sleep 1 clear -interactive_session "source ~/.bashrc && claude" +interactive_session "source /root/.bashrc && claude" diff --git a/railway/lib/common.sh b/railway/lib/common.sh index 382fe7cd..943ee346 100644 --- a/railway/lib/common.sh +++ b/railway/lib/common.sh @@ -1,6 +1,6 @@ #!/bin/bash # Common bash functions for Railway spawn scripts -# Uses Railway CLI for provisioning and SSH access +# Uses Railway CLI for provisioning and exec access # Bash safety flags set -eo pipefail @@ -31,19 +31,19 @@ ensure_railway_cli() { fi log_warn "Installing Railway CLI..." - if command -v npm &>/dev/null; then - npm install -g @railway/cli 2>/dev/null || { - log_error "Failed to install Railway CLI via npm" - log_error "Install manually: npm install -g @railway/cli" - return 1 - } - else - # Use the official installer script - bash <(curl -fsSL cli.new) 2>/dev/null || { - log_error "Failed to install Railway CLI" - log_error "Install manually: bash <(curl -fsSL cli.new)" - return 1 - } + + # Railway CLI is installed via npm + if ! command -v npm &>/dev/null; then + log_error "npm is required to install Railway CLI" + log_error "Install Node.js/npm from https://nodejs.org/ or use your package manager" + return 1 + fi + + if ! npm install -g @railway/cli; then + log_error "Failed to install Railway CLI" + log_error "Install manually: npm install -g @railway/cli" + log_error "See: https://docs.railway.app/develop/cli" + return 1 fi if ! command -v railway &>/dev/null; then @@ -54,245 +54,212 @@ ensure_railway_cli() { log_info "Railway CLI installed" } +# Save Railway token to config file +_save_railway_token() { + local token="$1" + local config_dir="$HOME/.config/spawn" + local config_file="$config_dir/railway.json" + mkdir -p "$config_dir" + printf '{\n "token": "%s"\n}\n' "$(json_escape "$token")" > "$config_file" + chmod 600 "$config_file" +} + # Ensure RAILWAY_TOKEN is available (env var -> config file -> prompt+save) ensure_railway_token() { - # Check Python 3 is available (required for JSON parsing) check_python_available || return 1 # 1. Check environment variable if [[ -n "${RAILWAY_TOKEN:-}" ]]; then - log_info "Using Railway token from environment" + log_info "Using Railway API token from environment" return 0 fi + local config_file="$HOME/.config/spawn/railway.json" + # 2. Check config file - local config_dir="$HOME/.config/spawn" - local config_file="$config_dir/railway.json" if [[ -f "$config_file" ]]; then - local saved_token=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1])).get('token',''))" "$config_file" 2>/dev/null) + local saved_token + saved_token=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1])).get('token',''))" "$config_file" 2>/dev/null) if [[ -n "$saved_token" ]]; then export RAILWAY_TOKEN="$saved_token" - log_info "Using Railway token from $config_file" + log_info "Using Railway API token from $config_file" return 0 fi fi - # 3. Try to login interactively via Railway CLI - if command -v railway &>/dev/null; then - log_warn "Railway CLI requires authentication" - log_warn "Opening browser for Railway login..." - railway login || { - log_error "Railway login failed" - return 1 - } - log_info "Railway login successful" - - # Try to extract token from CLI (Railway stores tokens internally) - # We'll just rely on railway commands working after login - return 0 - fi - - # 4. Prompt for token + # 3. Prompt user for token + log_warn "Railway API token required" echo "" - log_warn "Railway Token Required" - echo -e "${YELLOW}Get your token at: https://railway.app/account/tokens${NC}" + echo "Get your API token at: https://railway.app/account/tokens" echo "" - local token=$(safe_read "Enter your Railway token: ") || return 1 + local token + token=$(safe_read "Enter Railway API token: ") if [[ -z "$token" ]]; then - log_error "Token cannot be empty" - log_warn "For non-interactive usage, set: RAILWAY_TOKEN=your-token" + log_error "No token provided" return 1 fi - # Save to config file export RAILWAY_TOKEN="$token" - mkdir -p "$config_dir" - printf '{\n "token": %s\n}\n' "$(json_escape "$token")" > "$config_file" - chmod 600 "$config_file" - log_info "Token saved to $config_file" + _save_railway_token "$token" + log_info "Railway API token saved" } -# Validate server name (Railway project name) -validate_server_name() { - local name="$1" - if [[ ! "$name" =~ ^[a-z0-9][a-z0-9-]*[a-z0-9]$ ]]; then - log_error "Invalid project name: $name" - log_error "Must start and end with alphanumeric, contain only lowercase letters, numbers, and hyphens" - return 1 - fi - if [[ "${#name}" -gt 50 ]]; then - log_error "Project name too long (max 50 characters): $name" - return 1 - fi - echo "$name" -} - -# Get server name from env var or prompt +# Generate a unique project and service name for Railway get_server_name() { - if [[ -n "${RAILWAY_PROJECT_NAME:-}" ]]; then - log_info "Using project name from environment: $RAILWAY_PROJECT_NAME" - if ! validate_server_name "$RAILWAY_PROJECT_NAME"; then - return 1 - fi - echo "$RAILWAY_PROJECT_NAME" - return 0 - fi - - local server_name=$(safe_read "Enter project name: ") - if [[ -z "$server_name" ]]; then - log_error "Project name is required" - log_warn "Set RAILWAY_PROJECT_NAME environment variable for non-interactive usage:" - log_warn " RAILWAY_PROJECT_NAME=dev-mk1 curl ... | bash" - return 1 - fi - - if ! validate_server_name "$server_name"; then - return 1 - fi - - echo "$server_name" + local prefix="${1:-spawn}" + local timestamp=$(date +%s) + local random_suffix=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n') + echo "${prefix}-${timestamp}-${random_suffix}" | tr '[:upper:]' '[:lower:]' } -# Create a Railway project and service +# Create a Railway project and service via CLI +# Sets: RAILWAY_PROJECT_ID, RAILWAY_SERVICE_ID, RAILWAY_SERVICE_NAME create_server() { - local name="$1" + local name="${1:-$(get_server_name)}" - log_warn "Creating Railway project '$name'..." + RAILWAY_SERVICE_NAME="${name}" - # Create a temporary directory for the project - local project_dir="$HOME/.spawn-railway/$name" - mkdir -p "$project_dir" - cd "$project_dir" + log_warn "Creating Railway project: $RAILWAY_SERVICE_NAME" - # Initialize new project - railway init --name "$name" || { + # Create new project with CLI + local project_output + project_output=$(railway init -n "$RAILWAY_SERVICE_NAME" 2>&1) + + if echo "$project_output" | grep -qi "error"; then log_error "Failed to create Railway project" + log_error "$project_output" + return 1 + fi + + # Link to the project we just created + railway link "$RAILWAY_SERVICE_NAME" >/dev/null 2>&1 || { + log_error "Failed to link to Railway project" return 1 } - log_info "Project '$name' created" + log_info "Railway project created: $RAILWAY_SERVICE_NAME" - # Deploy a simple Ubuntu container with sleep to keep it running - log_warn "Creating service with Ubuntu 24.04..." + # Deploy an Ubuntu container that stays alive + log_warn "Deploying service..." - # Create a simple Dockerfile - cat > Dockerfile << 'EOF' + # Create a minimal Dockerfile in a temp directory + local temp_dir=$(mktemp -d) + cat > "$temp_dir/Dockerfile" <<'EOF' FROM ubuntu:24.04 - -# Install base tools RUN apt-get update && apt-get install -y \ - curl \ - wget \ - git \ - unzip \ - python3 \ - python3-pip \ - build-essential \ - ca-certificates \ + curl wget git python3 python3-pip build-essential ca-certificates \ && rm -rf /var/lib/apt/lists/* - -# Keep container running -CMD ["sleep", "infinity"] +CMD ["tail", "-f", "/dev/null"] EOF - # Deploy the service - log_warn "Deploying service (this may take 1-2 minutes)..." - railway up --detach || { - log_error "Failed to deploy service" + # Deploy from the temp directory + (cd "$temp_dir" && railway up --detach) || { + log_error "Failed to deploy Railway service" + rm -rf "$temp_dir" return 1 } - # Export variables for later use - export RAILWAY_PROJECT_DIR="$project_dir" - export RAILWAY_PROJECT_NAME="$name" + rm -rf "$temp_dir" - log_info "Service deployed and running" + # Wait for deployment to be ready + log_warn "Waiting for service to deploy..." + local max_attempts=60 + local attempt=0 - # Wait a moment for the service to be fully ready - sleep 5 + while [[ $attempt -lt $max_attempts ]]; do + local status + status=$(railway status 2>/dev/null | grep -i "status" | head -1 || echo "") + + if echo "$status" | grep -qi "success\|active\|running\|healthy"; then + log_info "Service is ready" + break + fi + + if echo "$status" | grep -qi "failed\|error"; then + log_error "Service deployment failed" + return 1 + fi + + attempt=$((attempt + 1)) + sleep 5 + done + + if [[ $attempt -ge $max_attempts ]]; then + log_error "Timeout waiting for service to be ready" + return 1 + fi + + log_info "Railway service deployed successfully" } -# Wait for cloud-init (not needed for Railway, but kept for compatibility) -wait_for_cloud_init() { - log_warn "Installing additional tools..." - run_server "curl -fsSL https://bun.sh/install | bash" >/dev/null 2>&1 || true - run_server 'printf "export PATH=\"\$HOME/.bun/bin:\$PATH\"\n" >> ~/.bashrc' >/dev/null 2>&1 || true - run_server 'printf "export PATH=\"\$HOME/.bun/bin:\$PATH\"\n" >> ~/.zshrc' >/dev/null 2>&1 || true - log_info "Additional tools installed" -} - -# Run a command on the Railway service via railway run -# SECURITY: Uses printf %q to properly escape commands to prevent injection +# Run a command on the Railway service run_server() { local cmd="$1" - local escaped_cmd - escaped_cmd=$(printf '%q' "$cmd") - cd "$RAILWAY_PROJECT_DIR" - railway run bash -c "$escaped_cmd" 2>/dev/null + + # Railway CLI doesn't have a direct exec - we use shell command via railway run + railway run bash -c "$cmd" } -# Upload a file to the service via base64 encoding through exec +# Upload a file to the Railway service upload_file() { local local_path="$1" local remote_path="$2" - local content=$(base64 -w0 "$local_path" 2>/dev/null || base64 "$local_path") - # SECURITY: Properly escape paths and content - local escaped_path - escaped_path=$(printf '%q' "$remote_path") - local escaped_content - escaped_content=$(printf '%q' "$content") - run_server "printf '%s' $escaped_content | base64 -d > $escaped_path" -} -# Start an interactive SSH session on the Railway service -interactive_session() { - local cmd="$1" - local escaped_cmd - escaped_cmd=$(printf '%q' "$cmd") - cd "$RAILWAY_PROJECT_DIR" - railway run bash -c "$escaped_cmd" -} - -# Destroy a Railway project -destroy_server() { - local project_name="${1:-$RAILWAY_PROJECT_NAME}" - - log_warn "Destroying Railway project '$project_name'..." - - local project_dir="$HOME/.spawn-railway/$project_name" - if [[ -d "$project_dir" ]]; then - cd "$project_dir" - railway down || true - cd ~ - rm -rf "$project_dir" + if [[ ! -f "$local_path" ]]; then + log_error "Local file not found: $local_path" + return 1 fi - log_info "Project '$project_name' destroyed" + # Read file content and encode + local content + content=$(cat "$local_path" | base64) + + # Write file on remote service via railway run + railway run bash -c "echo '$content' | base64 -d > '$remote_path'" } -# Inject environment variables into both .bashrc and .zshrc -# Usage: inject_env_vars_railway KEY1=VAL1 KEY2=VAL2 ... -inject_env_vars_railway() { - local env_temp - env_temp=$(mktemp) - chmod 600 "${env_temp}" - track_temp_file "${env_temp}" +# Wait for system readiness (Railway containers start with Ubuntu base) +wait_for_cloud_init() { + log_warn "Verifying system packages..." - generate_env_config "$@" > "${env_temp}" - - # Upload and append to both .bashrc and .zshrc - upload_file "${env_temp}" "/tmp/env_config" - run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config" - - # Note: temp file will be cleaned up by trap handler -} - -# List all Railway projects -list_servers() { - log_warn "Listing Railway projects..." - railway list || { - log_error "Failed to list Railway projects" - return 1 + # Update package lists if needed + run_server "apt-get update -qq >/dev/null 2>&1 || true" || { + log_warn "Package update skipped (may already be ready)" } + + log_info "System ready" +} + +# Inject environment variables into shell config +inject_env_vars() { + local shell_rc="/root/.bashrc" + + log_warn "Injecting environment variables..." + + for env_var in "$@"; do + # Escape special characters for sed + local escaped_var=$(echo "$env_var" | sed 's/[&/\]/\\&/g') + run_server "echo 'export $escaped_var' >> $shell_rc" + done + + log_info "Environment variables configured" +} + +# Start an interactive session via Railway SSH +interactive_session() { + local launch_cmd="${1:-bash}" + + log_info "Starting interactive session..." + + # Railway CLI has a shell command for interactive sessions + railway shell +} + +# Cleanup: delete the project +cleanup_server() { + if [[ -n "${RAILWAY_SERVICE_NAME:-}" ]]; then + log_warn "Deleting Railway project: $RAILWAY_SERVICE_NAME" + railway delete --yes >/dev/null 2>&1 || true + fi } diff --git a/railway/nanoclaw.sh b/railway/nanoclaw.sh new file mode 100755 index 00000000..6190d156 --- /dev/null +++ b/railway/nanoclaw.sh @@ -0,0 +1,74 @@ +#!/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/railway/lib/common.sh)" +fi + +log_info "NanoClaw on Railway" +echo "" + +# 1. Ensure Railway CLI and API token +ensure_railway_cli +ensure_railway_token + +# 2. Create service +SERVER_NAME=$(get_server_name) +create_server "$SERVER_NAME" + +# 3. Install base tools +wait_for_cloud_init + +# 4. Install tsx dependency +log_warn "Installing tsx..." +run_server "curl -fsSL https://bun.sh/install | bash && export PATH=\"\$HOME/.bun/bin:\$PATH\" && bun install -g tsx" +log_info "tsx installed" + +# 5. Clone and build nanoclaw +log_warn "Cloning and building nanoclaw..." +run_server "git clone https://github.com/gavrielc/nanoclaw.git /root/nanoclaw && cd /root/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 shell config +log_warn "Setting up environment variables..." + +inject_env_vars \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ + "PATH=\$HOME/.bun/bin:\$PATH" + +# 8. Create nanoclaw .env file +log_warn "Configuring nanoclaw..." + +DOTENV_TEMP=$(mktemp) +trap 'rm -f "${DOTENV_TEMP}"' EXIT +chmod 600 "${DOTENV_TEMP}" +cat > "${DOTENV_TEMP}" << EOF +ANTHROPIC_API_KEY=${OPENROUTER_API_KEY} +EOF + +upload_file "$DOTENV_TEMP" "/root/nanoclaw/.env" + +echo "" +log_info "Railway service setup completed successfully!" +log_info "Service: $RAILWAY_SERVICE_NAME" +echo "" + +# 9. Start nanoclaw +log_warn "Starting nanoclaw..." +log_warn "You will need to scan a WhatsApp QR code to authenticate." +echo "" +interactive_session "cd /root/nanoclaw && source /root/.bashrc && npm run dev" diff --git a/railway/openclaw.sh b/railway/openclaw.sh new file mode 100755 index 00000000..706066e5 --- /dev/null +++ b/railway/openclaw.sh @@ -0,0 +1,81 @@ +#!/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/railway/lib/common.sh)" +fi + +log_info "OpenClaw on Railway" +echo "" + +# 1. Ensure Railway CLI and API token +ensure_railway_cli +ensure_railway_token + +# 2. Create service +SERVER_NAME=$(get_server_name) +create_server "$SERVER_NAME" + +# 3. Install base tools +wait_for_cloud_init + +# 4. Install bun first (required for openclaw) +log_warn "Installing bun..." +run_server "curl -fsSL https://bun.sh/install | bash" +run_server "export PATH=\"\$HOME/.bun/bin:\$PATH\"" +log_info "Bun installed" + +# 5. Install openclaw via bun +log_warn "Installing openclaw..." +run_server "source /root/.bashrc && export PATH=\"\$HOME/.bun/bin:\$PATH\" && 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 + +# Get model preference +MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Openclaw") || exit 1 + +# 7. Inject environment variables into shell config +log_warn "Setting up environment variables..." + +inject_env_vars \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ + "PATH=\$HOME/.bun/bin:\$PATH" + +# 8. Configure openclaw +log_warn "Configuring openclaw..." + +run_server "rm -rf /root/.openclaw && mkdir -p /root/.openclaw" + +# Generate a random gateway token +GATEWAY_TOKEN=$(openssl rand -hex 16) + +OPENCLAW_CONFIG_TEMP=$(mktemp) +chmod 600 "$OPENCLAW_CONFIG_TEMP" +printf '{\n "env": {\n "OPENROUTER_API_KEY": "%s"\n },\n "gateway": {\n "mode": "local",\n "auth": {\n "token": "%s"\n }\n },\n "agents": {\n "defaults": {\n "model": {\n "primary": "openrouter/%s"\n }\n }\n }\n}\n' "$(json_escape "${OPENROUTER_API_KEY}")" "$(json_escape "${GATEWAY_TOKEN}")" "$(json_escape "${MODEL_ID}")" > "$OPENCLAW_CONFIG_TEMP" + +upload_file "$OPENCLAW_CONFIG_TEMP" "/root/.openclaw/openclaw.json" +rm "$OPENCLAW_CONFIG_TEMP" + +echo "" +log_info "Railway service setup completed successfully!" +log_info "Service: $RAILWAY_SERVICE_NAME" +echo "" + +# 9. Start openclaw gateway in background and launch TUI +log_warn "Starting openclaw..." +run_server "source /root/.bashrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" +sleep 2 +interactive_session "source /root/.bashrc && openclaw tui"