Add guardrails: CLAUDE.md rules, hooks, pre-commit validation (#33)

* feat: add gptme agent to spawn matrix

Add gptme (https://github.com/gptme/gptme) - a personal AI agent in the
terminal with tools for code editing, terminal commands, web browsing,
and more. Natively supports OpenRouter via OPENROUTER_API_KEY.

- Add gptme agent entry to manifest.json with OpenRouter env vars
- Implement sprite/gptme.sh deployment script
- Implement hetzner/gptme.sh deployment script
- Add "missing" matrix entries for remaining 8 clouds
- Update README.md with usage instructions for Sprite and Hetzner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add Fly.io cloud provider with claude and aider agents

Add Fly.io as a new cloud provider using the Machines REST API for
provisioning and flyctl CLI for SSH access. Docker-based machines
with pay-per-second pricing.

- Create fly/lib/common.sh with Fly.io Machines API integration
- Implement fly/claude.sh for Claude Code deployment
- Implement fly/aider.sh for Aider deployment
- Update README.md with Fly.io usage instructions and env vars

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add gemini, amazonq, cline, gptme to Fly.io

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add openclaw, nanoclaw, goose, codex, interpreter to Fly.io

Implements 5 new agent scripts for the Fly.io cloud provider:
- fly/openclaw.sh: OpenClaw with gateway + TUI, model selection, config
- fly/nanoclaw.sh: NanoClaw WhatsApp agent with .env configuration
- fly/goose.sh: Block's Goose agent with OpenRouter provider
- fly/codex.sh: OpenAI Codex CLI with OpenRouter base URL override
- fly/interpreter.sh: Open Interpreter with OpenRouter base URL override

All scripts follow the Fly.io pattern (flyctl-based, no IP args for
run_server/interactive_session) and use upload_file for env injection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add gptme agent to 8 remaining clouds

Implement gptme agent scripts for digitalocean, vultr, linode, lambda,
aws-lightsail, gcp, e2b, and modal. Each script follows the exact
pattern of that cloud's existing aider.sh, adapted for gptme's install
and launch commands. Updates manifest.json matrix entries from "missing"
to "implemented".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add guardrails from insights: CLAUDE.md rules, hooks, pre-commit

Based on usage insights analysis:

CLAUDE.md:
- Shell script rules: curl|bash compat, macOS bash 3.x compat
- Autonomous loop rules: test after each iteration, never revert fixes
- Git workflow rules: always use feature branches

.claude/settings.json:
- PostToolUse hook validates .sh files on every Write/Edit:
  syntax check, no relative source, no echo -e, no set -u

.githooks/pre-commit:
- Blocks commits with: syntax errors, relative sources, echo -e,
  set -euo, references to deleted functions
- Install: git config core.hooksPath .githooks

README.md:
- Added developer setup section with hook installation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
L 2026-02-07 20:02:19 -08:00 committed by GitHub
parent ce0f2ce7fb
commit b6ee6b6ab1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 2069 additions and 10 deletions

View file

@ -5,5 +5,13 @@
"permissions": {
"defaultMode": "bypassPermissions",
"dangerouslySkipPermissions": true
},
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "bash -c 'FILE=\"$CLAUDE_FILE\"; if [[ \"$FILE\" == *.sh ]]; then bash -n \"$FILE\" 2>&1 || { echo \"SYNTAX ERROR in $FILE\"; exit 2; }; if grep -qn \"source \\.\\./\\|source \\./\" \"$FILE\" 2>/dev/null; then echo \"RELATIVE SOURCE detected in $FILE — breaks curl|bash execution\"; exit 2; fi; if grep -qn \"echo -e \" \"$FILE\" 2>/dev/null; then echo \"echo -e detected in $FILE — use printf instead (macOS bash 3.x compat)\"; exit 2; fi; if grep -qn \"set -.*u\" \"$FILE\" 2>/dev/null && ! grep -qn \"set -eo pipefail\" \"$FILE\" 2>/dev/null; then echo \"set -u (nounset) detected in $FILE — use set -eo pipefail instead\"; exit 2; fi; fi'"
}
]
}
}

64
.githooks/pre-commit Executable file
View file

@ -0,0 +1,64 @@
#!/bin/bash
# Pre-commit hook: validates all staged .sh files
# Install: git config core.hooksPath .githooks
set -eo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
errors=0
# Get staged .sh files
staged_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sh$' || true)
if [[ -z "$staged_files" ]]; then
exit 0
fi
echo "Validating staged shell scripts..."
for file in $staged_files; do
# 1. Syntax check
if ! bash -n "$file" 2>/dev/null; then
echo -e "${RED}FAIL${NC} $file: syntax error"
bash -n "$file" 2>&1 | head -3
errors=$((errors + 1))
continue
fi
# 2. No relative source (breaks curl|bash)
if grep -qn 'source \.\./' "$file" 2>/dev/null || grep -qn 'source \./' "$file" 2>/dev/null; then
echo -e "${RED}FAIL${NC} $file: relative source path (breaks curl|bash)"
grep -n 'source \.\.' "$file" 2>/dev/null || true
errors=$((errors + 1))
fi
# 3. No echo -e (breaks macOS bash 3.x)
if grep -qn 'echo -e ' "$file" 2>/dev/null; then
echo -e "${RED}FAIL${NC} $file: echo -e (use printf for macOS compat)"
errors=$((errors + 1))
fi
# 4. No set -u / set -euo (breaks env var checks)
if grep -qn 'set -euo' "$file" 2>/dev/null; then
echo -e "${RED}FAIL${NC} $file: set -euo pipefail (drop the 'u', use set -eo pipefail)"
errors=$((errors + 1))
fi
# 5. Check for calls to deleted functions
if grep -qn 'write_oauth_response_file\|create_oauth_response_html' "$file" 2>/dev/null; then
echo -e "${RED}FAIL${NC} $file: references deleted function"
errors=$((errors + 1))
fi
done
if [[ $errors -gt 0 ]]; then
echo ""
echo -e "${RED}$errors error(s) found. Commit blocked.${NC}"
echo "Fix the issues above and try again."
exit 1
fi
echo -e "${GREEN}All $( echo "$staged_files" | wc -w | tr -d ' ') scripts passed validation.${NC}"

View file

@ -164,18 +164,60 @@ This pattern ensures:
- Path resolution works when sourced from any location
- Script fails fast if shared library is missing
## Script Conventions
## Shell Script Rules
- `#!/bin/bash` + `set -e`
- Source `lib/common.sh` with local-first, remote-fallback pattern
- Use `OPENROUTER_API_KEY` env var to skip OAuth when set
- All env vars documented in README.md under the relevant section
These rules are **non-negotiable** — violating them breaks remote execution for all users.
### curl|bash Compatibility
Every script MUST work when executed via `bash <(curl -fsSL URL)`:
- **NEVER** use relative paths for sourcing (`source ./lib/...`, `source ../shared/...`)
- **NEVER** rely on `$0`, `dirname $0`, or `BASH_SOURCE` resolving to a real filesystem path
- **ALWAYS** use the local-or-remote fallback pattern:
```bash
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/{cloud}/lib/common.sh)"
fi
```
- Similarly, `{cloud}/lib/common.sh` MUST use the same fallback for `shared/common.sh`
### macOS bash 3.x Compatibility
macOS ships bash 3.2. All scripts MUST work on it:
- **NO** `echo -e` — use `printf` for escape sequences
- **NO** `source <(cmd)` inside `bash <(curl ...)` — use `eval "$(cmd)"` instead
- **NO** `((var++))` with `set -e` — use `var=$((var + 1))` (avoids falsy-zero exit)
- **NO** `local` keyword inside `( ... ) &` subshells — not function scope
- **NO** `set -u` (nounset) — use `${VAR:-}` for optional env var checks instead
### Conventions
- `#!/bin/bash` + `set -eo pipefail` (no `u` flag)
- Use `${VAR:-}` for all optional env var checks (`OPENROUTER_API_KEY`, cloud tokens, etc.)
- Remote fallback URL: `https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/{path}`
- Scripts must be runnable via: `bash <(curl -fsSL https://openrouter.ai/lab/spawn/{cloud}/{agent}.sh)`
- All env vars documented in the cloud's README.md
## Autonomous Loops
When running autonomous improvement/refactoring loops (`./improve.sh --loop`):
- **Run `bash -n` on every changed .sh file** before committing — syntax errors break everything
- **NEVER revert a prior fix** — if `shared/common.sh` was changed to fix macOS compat, don't undo it
- **NEVER re-introduce deleted functions** — if `write_oauth_response_file` was removed, don't call it
- **NEVER change the source/eval fallback pattern** in lib/common.sh files — it's load-bearing for curl|bash
- **Test after EACH iteration** — don't batch multiple changes without verification
- **If a change breaks tests, STOP** — revert and ask for guidance rather than compounding the regression
## Git Workflow
- Always work on a feature branch — never commit directly to main (except urgent one-line fixes)
- Before creating a PR, check `git status` and `git log` to verify branch state
- Use `gh pr create` from the feature branch, then `gh pr merge --squash`
- Never rebase main or use `--force` unless explicitly asked
## After Each Change
1. Update `manifest.json` matrix status to `"implemented"`
2. Update `README.md` with usage instructions
3. Run `bash test/run.sh` if tests exist for the cloud
1. `bash -n {file}` syntax check on all modified scripts
2. Update `manifest.json` matrix status to `"implemented"`
3. Update the cloud's `README.md` with usage instructions
4. Commit with a descriptive message

View file

@ -53,6 +53,12 @@ bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/codex.sh)
bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/interpreter.sh)
```
#### gptme
```bash
bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/gptme.sh)
```
### Non-Interactive Mode
For automation or CI/CD, set environment variables:
@ -135,6 +141,12 @@ bash <(curl -fsSL https://openrouter.ai/lab/spawn/hetzner/codex.sh)
bash <(curl -fsSL https://openrouter.ai/lab/spawn/hetzner/interpreter.sh)
```
#### gptme
```bash
bash <(curl -fsSL https://openrouter.ai/lab/spawn/hetzner/gptme.sh)
```
### Non-Interactive Mode
```bash
@ -351,6 +363,43 @@ OPENROUTER_API_KEY=sk-or-v1-xxxxx \
---
## Fly.io
Spawn agents on [Fly.io](https://fly.io) Machines via REST API and flyctl CLI. Docker-based VMs with pay-per-second pricing.
### Usage
#### Claude Code
```bash
bash <(curl -fsSL https://openrouter.ai/lab/spawn/fly/claude.sh)
```
#### Aider
```bash
bash <(curl -fsSL https://openrouter.ai/lab/spawn/fly/aider.sh)
```
### Non-Interactive Mode
```bash
FLY_APP_NAME=dev-mk1 \
FLY_API_TOKEN=your-fly-api-token \
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
bash <(curl -fsSL https://openrouter.ai/lab/spawn/fly/claude.sh)
```
**Environment Variables:**
- `FLY_APP_NAME` - Name for the Fly app (skips prompt)
- `FLY_API_TOKEN` - Fly.io API token (skips prompt, saved to `~/.config/spawn/fly.json`)
- `OPENROUTER_API_KEY` - Skip OAuth and use this API key directly
- `FLY_REGION` - Deployment region (default: `iad`)
- `FLY_VM_MEMORY` - VM memory in MB (default: `1024`)
- `FLY_ORG` - Fly.io organization slug (default: `personal`)
---
## Architecture
Spawn uses a **shared library pattern** to reduce code duplication across cloud providers:
@ -398,6 +447,16 @@ spawn/
## Development
### Setup
```bash
git clone https://github.com/OpenRouterTeam/spawn.git
cd spawn
git config core.hooksPath .githooks
```
The pre-commit hook validates all staged `.sh` files: syntax check, no relative sources, no `echo -e`, no `set -u`, no references to deleted functions.
### Running ShellCheck Locally
Spawn uses [ShellCheck](https://www.shellcheck.net/) to lint all bash scripts and catch common mistakes.
@ -444,6 +503,7 @@ Spawn stores cloud provider API tokens and OpenRouter API keys locally in JSON f
- `digitalocean.json` - DigitalOcean API token
- `vultr.json` - Vultr API key
- `linode.json` - Linode API token
- `fly.json` - Fly.io API token
- OpenRouter API keys stored in shell config files (`~/.bashrc`, `~/.zshrc`)
**Security Posture:**

72
aws-lightsail/gptme.sh Normal file
View file

@ -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
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/aws-lightsail/lib/common.sh)"
fi
log_info "gptme 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 gptme
log_warn "Installing gptme..."
run_server "$LIGHTSAIL_SERVER_IP" "pip install gptme 2>/dev/null || pip3 install gptme"
log_info "gptme 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 gptme?"
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 gptme interactively
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "$LIGHTSAIL_SERVER_IP" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"

65
digitalocean/gptme.sh Normal file
View file

@ -0,0 +1,65 @@
#!/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/digitalocean/lib/common.sh)"
fi
log_info "gptme on DigitalOcean"
echo ""
# 1. Resolve DigitalOcean API token
ensure_do_token
# 2. Generate + register SSH key
ensure_ssh_key
# 3. Get droplet name and create droplet
DROPLET_NAME=$(get_server_name)
create_server "$DROPLET_NAME"
# 4. Wait for SSH and cloud-init
verify_server_connectivity "$DO_SERVER_IP"
wait_for_cloud_init "$DO_SERVER_IP"
# 5. Install gptme
log_warn "Installing gptme..."
run_server "$DO_SERVER_IP" "pip install gptme 2>/dev/null || pip3 install gptme"
# Verify installation succeeded
if ! run_server "$DO_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 $DO_SERVER_IP"
exit 1
fi
log_info "gptme installation verified successfully"
# 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
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "gptme") || exit 1
log_warn "Setting up environment variables..."
inject_env_vars_ssh "$DO_SERVER_IP" upload_file run_server \
"OPENROUTER_API_KEY=$OPENROUTER_API_KEY"
echo ""
log_info "DigitalOcean droplet setup completed successfully!"
log_info "Droplet: $DROPLET_NAME (ID: $DO_DROPLET_ID, IP: $DO_SERVER_IP)"
echo ""
# 9. Start gptme interactively
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "$DO_SERVER_IP" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"

69
e2b/gptme.sh Normal file
View file

@ -0,0 +1,69 @@
#!/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
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/e2b/lib/common.sh)"
fi
log_info "gptme on E2B"
echo ""
# 1. Ensure E2B CLI and API token
ensure_e2b_cli
ensure_e2b_token
# 2. Get sandbox name and create sandbox
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Wait for base tools
wait_for_cloud_init
# 4. Install gptme
log_warn "Installing gptme..."
run_server "pip install gptme 2>/dev/null || pip3 install gptme"
log_info "gptme installed"
# 5. 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
# 6. Get model preference
echo ""
log_warn "Browse models at: https://openrouter.ai/models"
log_warn "Which model would you like to use with gptme?"
MODEL_ID=$(safe_read "Enter model ID [openrouter/auto]: ") || MODEL_ID=""
MODEL_ID="${MODEL_ID:-openrouter/auto}"
# 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}"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "E2B sandbox setup completed successfully!"
log_info "Sandbox: $SERVER_NAME (ID: $E2B_SANDBOX_ID)"
echo ""
# 8. Start gptme interactively
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"

67
fly/aider.sh Normal file
View file

@ -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/fly/lib/common.sh)"
fi
log_info "Aider on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Aider
log_warn "Installing Aider..."
run_server "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"
# 5. 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
# 6. Get model preference
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1
# 7. Inject environment variables into ~/.zshrc
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
cat > "$ENV_TEMP" << EOF
# [spawn:env]
export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 8. Start Aider interactively
log_warn "Starting Aider..."
sleep 1
clear
interactive_session "source ~/.bashrc && aider --model openrouter/${MODEL_ID}"

66
fly/amazonq.sh Normal file
View file

@ -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/fly/lib/common.sh)"
fi
log_info "Amazon Q on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Amazon Q CLI
log_warn "Installing Amazon Q CLI..."
run_server "curl -fsSL https://desktop-release.q.us-east-1.amazonaws.com/latest/amazon-q-cli-install.sh | bash"
log_info "Amazon Q CLI installed"
# 5. 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
# 6. Inject environment variables into ~/.bashrc and ~/.zshrc
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
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"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 7. Start Amazon Q interactively
log_warn "Starting Amazon Q..."
sleep 1
clear
interactive_session "source ~/.bashrc && q chat"

118
fly/claude.sh Normal file
View file

@ -0,0 +1,118 @@
#!/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/fly/lib/common.sh)"
fi
log_info "Claude Code on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Claude Code
log_warn "Installing Claude Code..."
run_server "curl -fsSL https://claude.ai/install.sh | bash"
# Verify installation
if ! run_server "command -v claude" >/dev/null 2>&1; then
log_error "Claude Code installation failed"
exit 1
fi
log_info "Claude Code installed"
# 5. 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
# 6. Inject environment variables into ~/.zshrc
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
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"
export PATH="\$HOME/.claude/local/bin:\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
# 7. Configure Claude Code settings
log_warn "Configuring Claude Code..."
run_server "mkdir -p ~/.claude"
# Upload settings.json
SETTINGS_TEMP=$(mktemp)
chmod 600 "$SETTINGS_TEMP"
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 "$SETTINGS_TEMP" "/root/.claude/settings.json"
rm "$SETTINGS_TEMP"
# Upload ~/.claude.json global state
GLOBAL_STATE_TEMP=$(mktemp)
chmod 600 "$GLOBAL_STATE_TEMP"
cat > "$GLOBAL_STATE_TEMP" << EOF
{
"hasCompletedOnboarding": true,
"bypassPermissionsModeAccepted": true
}
EOF
upload_file "$GLOBAL_STATE_TEMP" "/root/.claude.json"
rm "$GLOBAL_STATE_TEMP"
# Create empty CLAUDE.md
run_server "touch ~/.claude/CLAUDE.md"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 8. Start Claude Code interactively
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "source ~/.bashrc && claude"

66
fly/cline.sh Normal file
View file

@ -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/fly/lib/common.sh)"
fi
log_info "Cline on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Cline
log_warn "Installing Cline..."
run_server "npm install -g cline"
log_info "Cline installed"
# 5. 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
# 6. Inject environment variables into ~/.bashrc and ~/.zshrc
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
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"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 7. Start Cline interactively
log_warn "Starting Cline..."
sleep 1
clear
interactive_session "source ~/.bashrc && cline"

66
fly/codex.sh Normal file
View file

@ -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/fly/lib/common.sh)"
fi
log_info "Codex CLI on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Codex CLI
log_warn "Installing Codex CLI..."
run_server "npm install -g @openai/codex"
log_info "Codex CLI installed"
# 5. 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
# 6. Inject environment variables into shell config
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
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"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 7. Start Codex interactively
log_warn "Starting Codex..."
sleep 1
clear
interactive_session "source ~/.bashrc && codex"

67
fly/gemini.sh Normal file
View file

@ -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/fly/lib/common.sh)"
fi
log_info "Gemini CLI on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Gemini CLI
log_warn "Installing Gemini CLI..."
run_server "npm install -g @google/gemini-cli"
log_info "Gemini CLI installed"
# 5. 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
# 6. Inject environment variables into ~/.bashrc and ~/.zshrc
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
cat > "$ENV_TEMP" << EOF
# [spawn:env]
export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}"
export GEMINI_API_KEY="${OPENROUTER_API_KEY}"
export OPENAI_API_KEY="${OPENROUTER_API_KEY}"
export OPENAI_BASE_URL="https://openrouter.ai/api/v1"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 7. Start Gemini interactively
log_warn "Starting Gemini..."
sleep 1
clear
interactive_session "source ~/.bashrc && gemini"

71
fly/goose.sh Normal file
View file

@ -0,0 +1,71 @@
#!/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/fly/lib/common.sh)"
fi
log_info "Goose on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Goose
log_warn "Installing Goose..."
run_server "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash"
# Verify installation
if ! run_server "command -v goose &> /dev/null && goose --version &> /dev/null"; then
log_error "Goose installation verification failed"
exit 1
fi
log_info "Goose installed"
# 5. 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
# 6. Inject environment variables into shell config
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
cat > "$ENV_TEMP" << EOF
# [spawn:env]
export GOOSE_PROVIDER="openrouter"
export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 7. Start Goose interactively
log_warn "Starting Goose..."
sleep 1
clear
interactive_session "source ~/.bashrc && goose"

67
fly/gptme.sh Normal file
View file

@ -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/fly/lib/common.sh)"
fi
log_info "gptme on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install gptme
log_warn "Installing gptme..."
run_server "pip install gptme 2>/dev/null || pip3 install gptme"
log_info "gptme installed"
# 5. 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
# 6. Get model preference
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "gptme") || exit 1
# 7. Inject environment variables into ~/.bashrc and ~/.zshrc
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
cat > "$ENV_TEMP" << EOF
# [spawn:env]
export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 8. Start gptme interactively
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "source ~/.bashrc && gptme -m openrouter/${MODEL_ID}"

66
fly/interpreter.sh Normal file
View file

@ -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/fly/lib/common.sh)"
fi
log_info "Open Interpreter on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Open Interpreter
log_warn "Installing Open Interpreter..."
run_server "pip install open-interpreter 2>/dev/null || pip3 install open-interpreter"
log_info "Open Interpreter installed"
# 5. 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
# 6. Inject environment variables into shell config
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
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"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 7. Start Open Interpreter interactively
log_warn "Starting Open Interpreter..."
sleep 1
clear
interactive_session "source ~/.bashrc && interpreter"

369
fly/lib/common.sh Normal file
View file

@ -0,0 +1,369 @@
#!/bin/bash
# Common bash functions for Fly.io spawn scripts
# Uses Fly.io Machines API + flyctl CLI for provisioning and SSH access
# Bash safety flags
set -eo pipefail
# ============================================================
# 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
# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh
# ============================================================
# Fly.io specific functions
# ============================================================
readonly FLY_API_BASE="https://api.machines.dev/v1"
# Centralized curl wrapper for Fly.io Machines API
fly_api() {
local method="$1"
local endpoint="$2"
local body="${3:-}"
generic_cloud_api "$FLY_API_BASE" "$FLY_API_TOKEN" "$method" "$endpoint" "$body"
}
# Ensure flyctl CLI is installed
ensure_fly_cli() {
if command -v fly &>/dev/null; then
log_info "flyctl CLI available"
return 0
fi
if command -v flyctl &>/dev/null; then
log_info "flyctl CLI available (as flyctl)"
# Create alias function so we can use 'fly' consistently
fly() { flyctl "$@"; }
export -f fly
return 0
fi
log_warn "Installing flyctl CLI..."
curl -L https://fly.io/install.sh | sh 2>/dev/null || {
log_error "Failed to install flyctl CLI"
log_error "Install manually: curl -L https://fly.io/install.sh | sh"
return 1
}
# Add to PATH if installed to ~/.fly/bin
if [[ -d "$HOME/.fly/bin" ]]; then
export PATH="$HOME/.fly/bin:$PATH"
fi
if ! command -v fly &>/dev/null && ! command -v flyctl &>/dev/null; then
log_error "flyctl not found in PATH after installation"
return 1
fi
log_info "flyctl CLI installed"
}
# Ensure FLY_API_TOKEN is available (env var -> config file -> prompt+save)
ensure_fly_token() {
# Check Python 3 is available (required for JSON parsing)
check_python_available || return 1
# 1. Check environment variable
if [[ -n "${FLY_API_TOKEN:-}" ]]; then
log_info "Using Fly.io API token from environment"
return 0
fi
# 2. Check config file
local config_dir="$HOME/.config/spawn"
local config_file="$config_dir/fly.json"
if [[ -f "$config_file" ]]; then
local saved_token=$(python3 -c "import json; print(json.load(open('$config_file')).get('token',''))" 2>/dev/null)
if [[ -n "$saved_token" ]]; then
export FLY_API_TOKEN="$saved_token"
log_info "Using Fly.io API token from $config_file"
return 0
fi
fi
# 3. Try to get token from flyctl auth
if command -v fly &>/dev/null || command -v flyctl &>/dev/null; then
local fly_cmd="fly"
command -v fly &>/dev/null || fly_cmd="flyctl"
local token=$("$fly_cmd" auth token 2>/dev/null || true)
if [[ -n "$token" ]]; then
export FLY_API_TOKEN="$token"
log_info "Using Fly.io API token from flyctl auth"
# Save to config file
mkdir -p "$config_dir"
cat > "$config_file" << EOF
{
"token": "$token"
}
EOF
chmod 600 "$config_file"
return 0
fi
fi
# 4. Prompt and save
echo ""
log_warn "Fly.io API Token Required"
echo -e "${YELLOW}Get your token by running: fly tokens deploy${NC}"
echo -e "${YELLOW}Or create one at: https://fly.io/dashboard → Tokens${NC}"
echo ""
local token=$(safe_read "Enter your Fly.io API token: ") || return 1
if [[ -z "$token" ]]; then
log_error "API token cannot be empty"
log_warn "For non-interactive usage, set: FLY_API_TOKEN=your-token"
return 1
fi
# Validate token by making a test API call
export FLY_API_TOKEN="$token"
local response=$(fly_api GET "/apps?org_slug=personal")
if echo "$response" | grep -q '"error"'; then
log_error "Authentication failed: Invalid Fly.io API token"
local error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('error','No details available'))" 2>/dev/null || echo "Unable to parse error")
log_error "API Error: $error_msg"
log_warn "Remediation steps:"
log_warn " 1. Run: fly tokens deploy"
log_warn " 2. Or generate a token at: https://fly.io/dashboard"
log_warn " 3. Ensure the token has appropriate permissions"
unset FLY_API_TOKEN
return 1
fi
# Save to config file
mkdir -p "$config_dir"
cat > "$config_file" << EOF
{
"token": "$token"
}
EOF
chmod 600 "$config_file"
log_info "API token saved to $config_file"
}
# Get the Fly.io org slug (default: personal)
get_fly_org() {
echo "${FLY_ORG:-personal}"
}
# Get server name from env var or prompt
get_server_name() {
if [[ -n "${FLY_APP_NAME:-}" ]]; then
log_info "Using app name from environment: $FLY_APP_NAME"
if ! validate_server_name "$FLY_APP_NAME"; then
return 1
fi
echo "$FLY_APP_NAME"
return 0
fi
local server_name=$(safe_read "Enter app name: ")
if [[ -z "$server_name" ]]; then
log_error "App name is required"
log_warn "Set FLY_APP_NAME environment variable for non-interactive usage:"
log_warn " FLY_APP_NAME=dev-mk1 curl ... | bash"
return 1
fi
if ! validate_server_name "$server_name"; then
return 1
fi
echo "$server_name"
}
# Create a Fly.io app and machine
create_server() {
local name="$1"
local region="${FLY_REGION:-iad}"
local vm_size="${FLY_VM_SIZE:-shared-cpu-1x}"
local vm_memory="${FLY_VM_MEMORY:-1024}"
# Step 1: Create the app
log_warn "Creating Fly.io app '$name'..."
local org=$(get_fly_org)
local app_body="{\"app_name\":\"$name\",\"org_slug\":\"$org\"}"
local app_response=$(fly_api POST "/apps" "$app_body")
if echo "$app_response" | grep -q '"error"'; then
# App might already exist, try to continue
local error_msg=$(echo "$app_response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('error','Unknown error'))" 2>/dev/null || echo "$app_response")
if echo "$error_msg" | grep -qi "already exists"; then
log_warn "App '$name' already exists, reusing it"
else
log_error "Failed to create Fly.io app"
log_error "API Error: $error_msg"
log_warn "Common issues:"
log_warn " - App name already taken by another user"
log_warn " - Invalid organization slug"
log_warn " - API token lacks permissions"
return 1
fi
else
log_info "App '$name' created"
fi
# Step 2: Create a machine in the app
log_warn "Creating Fly.io machine (region: $region, size: $vm_size, memory: ${vm_memory}MB)..."
local machine_body=$(python3 -c "
import json
body = {
'name': '$name',
'region': '$region',
'config': {
'image': 'ubuntu:24.04',
'guest': {
'cpu_kind': 'shared',
'cpus': 1,
'memory_mb': $vm_memory
},
'init': {
'exec': ['/bin/sleep', 'inf']
},
'auto_destroy': False
}
}
print(json.dumps(body))
")
local response=$(fly_api POST "/apps/$name/machines" "$machine_body")
if echo "$response" | grep -q '"error"'; then
log_error "Failed to create Fly.io machine"
local error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('error','Unknown error'))" 2>/dev/null || echo "$response")
log_error "API Error: $error_msg"
log_warn "Common issues:"
log_warn " - Insufficient account balance or payment method required"
log_warn " - Region unavailable (try different FLY_REGION)"
log_warn " - Machine limit reached"
log_warn "Remediation: Check https://fly.io/dashboard"
return 1
fi
# Extract machine ID and state
FLY_MACHINE_ID=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['id'])")
export FLY_MACHINE_ID FLY_APP_NAME="$name"
log_info "Machine created: ID=$FLY_MACHINE_ID, App=$name"
# Wait for machine to be in started state
log_warn "Waiting for machine to start..."
local max_attempts=30
local attempt=1
while [[ "$attempt" -le "$max_attempts" ]]; do
local status_response=$(fly_api GET "/apps/$name/machines/$FLY_MACHINE_ID")
local state=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('state','unknown'))")
if [[ "$state" == "started" ]]; then
log_info "Machine is running"
return 0
fi
log_warn "Machine state: $state ($attempt/$max_attempts)"
sleep 3
((attempt++))
done
log_error "Machine did not start in time"
return 1
}
# Wait for base tools to be installed (Fly.io uses bare Ubuntu image)
wait_for_cloud_init() {
log_warn "Installing base tools on Fly.io machine..."
run_server "apt-get update -y && apt-get install -y curl unzip git zsh python3 pip" >/dev/null 2>&1 || true
run_server "curl -fsSL https://bun.sh/install | bash" >/dev/null 2>&1 || true
run_server 'echo "export PATH=\"\$HOME/.bun/bin:\$PATH\"" >> ~/.bashrc' >/dev/null 2>&1 || true
run_server 'echo "export PATH=\"\$HOME/.bun/bin:\$PATH\"" >> ~/.zshrc' >/dev/null 2>&1 || true
log_info "Base tools installed"
}
# Run a command on the Fly.io machine via flyctl ssh
run_server() {
local cmd="$1"
local fly_cmd="fly"
command -v fly &>/dev/null || fly_cmd="flyctl"
"$fly_cmd" ssh console -a "$FLY_APP_NAME" -C "bash -c '$cmd'" --quiet 2>/dev/null
}
# Upload a file to the machine via base64 encoding through exec
upload_file() {
local local_path="$1"
local remote_path="$2"
local content=$(base64 -w0 "$local_path" 2>/dev/null || base64 "$local_path")
run_server "echo '$content' | base64 -d > '$remote_path'"
}
# Start an interactive SSH session on the Fly.io machine
interactive_session() {
local cmd="$1"
local fly_cmd="fly"
command -v fly &>/dev/null || fly_cmd="flyctl"
"$fly_cmd" ssh console -a "$FLY_APP_NAME" -C "bash -c '$cmd'"
}
# Destroy a Fly.io machine and app
destroy_server() {
local app_name="${1:-$FLY_APP_NAME}"
log_warn "Destroying Fly.io app and machines for '$app_name'..."
# List and destroy all machines in the app
local machines=$(fly_api GET "/apps/$app_name/machines")
local machine_ids=$(echo "$machines" | python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
if isinstance(data, list):
for m in data:
print(m['id'])
" 2>/dev/null || true)
for mid in $machine_ids; do
log_warn "Stopping machine $mid..."
fly_api POST "/apps/$app_name/machines/$mid/stop" '{}' >/dev/null 2>&1 || true
sleep 2
log_warn "Destroying machine $mid..."
fly_api DELETE "/apps/$app_name/machines/$mid?force=true" >/dev/null 2>&1 || true
done
# Delete the app
fly_api DELETE "/apps/$app_name" >/dev/null 2>&1 || true
log_info "App '$app_name' destroyed"
}
# List all Fly.io apps and machines
list_servers() {
local org=$(get_fly_org)
local response=$(fly_api GET "/apps?org_slug=$org")
python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
apps = data if isinstance(data, list) else data.get('apps', [])
if not apps:
print('No apps found')
sys.exit(0)
print(f\"{'NAME':<25} {'ID':<20} {'STATUS':<12} {'NETWORK':<20}\")
print('-' * 77)
for a in apps:
name = a.get('name', 'N/A')
aid = a.get('id', 'N/A')
status = a.get('status', 'N/A')
network = a.get('network', 'N/A')
print(f'{name:<25} {aid:<20} {status:<12} {network:<20}')
" <<< "$response"
}

81
fly/nanoclaw.sh Normal file
View file

@ -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/fly/lib/common.sh)"
fi
log_info "NanoClaw on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install tsx and clone nanoclaw
log_warn "Installing tsx..."
run_server "source ~/.bashrc && bun install -g tsx"
log_warn "Cloning and building nanoclaw..."
run_server "git clone https://github.com/gavrielc/nanoclaw.git ~/nanoclaw && cd ~/nanoclaw && npm install && npm run build"
log_info "NanoClaw installed"
# 5. 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
# 6. Inject environment variables into shell config
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
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"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
# 7. Create nanoclaw .env file
log_warn "Configuring nanoclaw..."
DOTENV_TEMP=$(mktemp)
chmod 600 "$DOTENV_TEMP"
cat > "$DOTENV_TEMP" << EOF
ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}
EOF
upload_file "$DOTENV_TEMP" "/root/nanoclaw/.env"
rm "$DOTENV_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 8. Start nanoclaw
log_warn "Starting nanoclaw..."
log_warn "You will need to scan a WhatsApp QR code to authenticate."
echo ""
interactive_session "cd ~/nanoclaw && source ~/.zshrc && npm run dev"

103
fly/openclaw.sh Normal file
View file

@ -0,0 +1,103 @@
#!/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/fly/lib/common.sh)"
fi
log_info "OpenClaw on Fly.io"
echo ""
# 1. Ensure flyctl CLI and API token
ensure_fly_cli
ensure_fly_token
# 2. Get app name and create machine
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install openclaw via bun
log_warn "Installing openclaw..."
run_server "source ~/.bashrc && bun install -g openclaw"
log_info "OpenClaw installed"
# 5. 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
# 6. Inject environment variables into shell config
log_warn "Setting up environment variables..."
ENV_TEMP=$(mktemp)
chmod 600 "$ENV_TEMP"
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"
export PATH="\$HOME/.bun/bin:\$PATH"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
# 7. Configure openclaw
log_warn "Configuring openclaw..."
run_server "rm -rf ~/.openclaw && mkdir -p ~/.openclaw"
# Generate a random gateway token
GATEWAY_TOKEN=$(openssl rand -hex 16)
OPENCLAW_CONFIG_TEMP=$(mktemp)
chmod 600 "$OPENCLAW_CONFIG_TEMP"
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 "$OPENCLAW_CONFIG_TEMP" "/root/.openclaw/openclaw.json"
rm "$OPENCLAW_CONFIG_TEMP"
echo ""
log_info "Fly.io machine setup completed successfully!"
log_info "App: $SERVER_NAME (Machine ID: $FLY_MACHINE_ID)"
echo ""
# 8. Start openclaw gateway in background and launch TUI
log_warn "Starting openclaw..."
run_server "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &"
sleep 2
interactive_session "source ~/.zshrc && openclaw tui"

72
gcp/gptme.sh Normal file
View file

@ -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
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/gcp/lib/common.sh)"
fi
log_info "gptme on GCP Compute Engine"
echo ""
# 1. Ensure gcloud is configured
ensure_gcloud
# 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 SSH and cloud-init
verify_server_connectivity "$GCP_SERVER_IP"
wait_for_cloud_init "$GCP_SERVER_IP"
# 5. Install gptme
log_warn "Installing gptme..."
run_server "$GCP_SERVER_IP" "pip install gptme 2>/dev/null || pip3 install gptme"
log_info "gptme 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 gptme?"
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 "$GCP_SERVER_IP" "$ENV_TEMP" "/tmp/env_config"
run_server "$GCP_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "GCP instance setup completed successfully!"
log_info "Instance: $GCP_INSTANCE_NAME_ACTUAL (Zone: $GCP_ZONE, IP: $GCP_SERVER_IP)"
echo ""
# 9. Start gptme interactively
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "$GCP_SERVER_IP" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"

65
hetzner/gptme.sh Normal file
View file

@ -0,0 +1,65 @@
#!/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/hetzner/lib/common.sh)"
fi
log_info "gptme on Hetzner Cloud"
echo ""
# 1. Resolve Hetzner API token
ensure_hcloud_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 SSH and cloud-init
verify_server_connectivity "$HETZNER_SERVER_IP"
wait_for_cloud_init "$HETZNER_SERVER_IP"
# 5. Install gptme
log_warn "Installing gptme..."
run_server "$HETZNER_SERVER_IP" "pip install gptme 2>/dev/null || pip3 install gptme"
# Verify installation succeeded
if ! run_server "$HETZNER_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 $HETZNER_SERVER_IP"
exit 1
fi
log_info "gptme installation verified successfully"
# 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" "gptme") || exit 1
log_warn "Setting up environment variables..."
inject_env_vars_ssh "$HETZNER_SERVER_IP" upload_file run_server \
"OPENROUTER_API_KEY=$OPENROUTER_API_KEY"
echo ""
log_info "Hetzner server setup completed successfully!"
log_info "Server: $SERVER_NAME (ID: $HETZNER_SERVER_ID, IP: $HETZNER_SERVER_IP)"
echo ""
# 9. Start gptme interactively
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "$HETZNER_SERVER_IP" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"

72
lambda/gptme.sh Normal file
View file

@ -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
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/lambda/lib/common.sh)"
fi
log_info "gptme on Lambda Cloud"
echo ""
# 1. Ensure Lambda API key is configured
ensure_lambda_token
# 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 "$LAMBDA_SERVER_IP"
wait_for_cloud_init "$LAMBDA_SERVER_IP"
# 5. Install gptme
log_warn "Installing gptme..."
run_server "$LAMBDA_SERVER_IP" "pip install gptme 2>/dev/null || pip3 install gptme"
log_info "gptme 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 gptme?"
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 "$LAMBDA_SERVER_IP" "$ENV_TEMP" "/tmp/env_config"
run_server "$LAMBDA_SERVER_IP" "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Lambda Cloud instance setup completed successfully!"
log_info "Instance: $SERVER_NAME (IP: $LAMBDA_SERVER_IP)"
echo ""
# 9. Start gptme interactively
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "$LAMBDA_SERVER_IP" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"

30
linode/gptme.sh Normal file
View file

@ -0,0 +1,30 @@
#!/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/linode/lib/common.sh)"; fi
log_info "gptme on Linode"
echo ""
ensure_linode_token
ensure_ssh_key
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
verify_server_connectivity "$LINODE_SERVER_IP"
wait_for_cloud_init "$LINODE_SERVER_IP"
log_warn "Installing gptme..."
run_server "$LINODE_SERVER_IP" "pip install gptme 2>/dev/null || pip3 install gptme"
log_info "gptme installed"
echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then log_info "Using OpenRouter API key from environment"
else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180); fi
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "gptme") || exit 1
log_warn "Setting up environment variables..."
inject_env_vars_ssh "$LINODE_SERVER_IP" upload_file run_server \
"OPENROUTER_API_KEY=$OPENROUTER_API_KEY"
echo ""
log_info "Linode setup completed successfully!"
echo ""
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "$LINODE_SERVER_IP" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"

View file

@ -161,6 +161,23 @@
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}"
},
"notes": "Works with OpenRouter via OPENAI_BASE_URL override"
},
"gptme": {
"name": "gptme",
"description": "Personal AI agent in the terminal with tools for code, terminal, browser, and more",
"url": "https://github.com/gptme/gptme",
"install": "pip install gptme",
"launch": "gptme -m openrouter/${MODEL_ID}",
"env": {
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}"
},
"interactive_prompts": {
"model_id": {
"prompt": "Enter model ID",
"default": "openrouter/auto"
}
},
"notes": "Natively supports OpenRouter via OPENROUTER_API_KEY and -m openrouter/... flag"
}
},
"clouds": {
@ -308,6 +325,23 @@
"image": "debian_slim"
},
"notes": "No SSH — uses Modal Python SDK for exec. Sub-second cold starts. Requires pip install modal."
},
"fly": {
"name": "Fly.io",
"description": "Fly.io Machines via REST API and flyctl CLI",
"url": "https://fly.io",
"type": "api+cli",
"auth": "FLY_API_TOKEN",
"provision_method": "POST /v1/apps + POST /v1/apps/{app}/machines",
"exec_method": "fly ssh console -C",
"interactive_method": "fly ssh console",
"defaults": {
"region": "iad",
"vm_size": "shared-cpu-1x",
"vm_memory": 1024,
"image": "ubuntu:24.04"
},
"notes": "Uses Machines API for provisioning and flyctl SSH for exec. Docker-based, pay-per-second pricing. Requires flyctl CLI."
}
},
"matrix": {
@ -410,6 +444,27 @@
"modal/interpreter": "implemented",
"modal/gemini": "implemented",
"modal/amazonq": "implemented",
"modal/cline": "implemented"
"modal/cline": "implemented",
"sprite/gptme": "implemented",
"hetzner/gptme": "implemented",
"digitalocean/gptme": "implemented",
"vultr/gptme": "implemented",
"linode/gptme": "implemented",
"lambda/gptme": "implemented",
"aws-lightsail/gptme": "implemented",
"gcp/gptme": "implemented",
"e2b/gptme": "implemented",
"modal/gptme": "implemented",
"fly/claude": "implemented",
"fly/aider": "implemented",
"fly/openclaw": "implemented",
"fly/nanoclaw": "implemented",
"fly/goose": "implemented",
"fly/codex": "implemented",
"fly/interpreter": "implemented",
"fly/gemini": "implemented",
"fly/amazonq": "implemented",
"fly/cline": "implemented",
"fly/gptme": "implemented"
}
}

68
modal/gptme.sh Normal file
View file

@ -0,0 +1,68 @@
#!/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
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/modal/lib/common.sh)"
fi
log_info "gptme on Modal"
echo ""
# 1. Ensure Modal CLI
ensure_modal_cli
# 2. Get sandbox name and create sandbox
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Wait for base tools
wait_for_cloud_init
# 4. Install gptme
log_warn "Installing gptme..."
run_server "pip install gptme 2>/dev/null || pip3 install gptme"
log_info "gptme installed"
# 5. 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
# 6. Get model preference
echo ""
log_warn "Browse models at: https://openrouter.ai/models"
log_warn "Which model would you like to use with gptme?"
MODEL_ID=$(safe_read "Enter model ID [openrouter/auto]: ") || MODEL_ID=""
MODEL_ID="${MODEL_ID:-openrouter/auto}"
# 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}"
EOF
upload_file "$ENV_TEMP" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config"
rm "$ENV_TEMP"
echo ""
log_info "Modal sandbox setup completed successfully!"
log_info "Sandbox: $SERVER_NAME (ID: $MODAL_SANDBOX_ID)"
echo ""
# 8. Start gptme interactively
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"

63
sprite/gptme.sh Normal file
View file

@ -0,0 +1,63 @@
#!/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/sprite/lib/common.sh)"
fi
log_info "gptme on Sprite"
echo ""
# Setup sprite environment
ensure_sprite_installed
ensure_sprite_authenticated
SPRITE_NAME=$(get_sprite_name)
ensure_sprite_exists "$SPRITE_NAME" 5
verify_sprite_connectivity "$SPRITE_NAME"
log_warn "Setting up sprite environment..."
# Configure shell environment
setup_shell_environment "$SPRITE_NAME"
# Install gptme
log_warn "Installing gptme..."
run_sprite "$SPRITE_NAME" "pip install gptme 2>/dev/null || pip3 install gptme"
# Verify installation succeeded
if ! run_sprite "$SPRITE_NAME" "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"
exit 1
fi
log_info "gptme installation verified successfully"
# Get OpenRouter API key via OAuth
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_sprite "$SPRITE_NAME" \
"OPENROUTER_API_KEY=$OPENROUTER_API_KEY"
echo ""
log_info "Sprite setup completed successfully!"
echo ""
# Start gptme interactively
log_warn "Starting gptme..."
sleep 1
clear
sprite exec -s "$SPRITE_NAME" -tty -- zsh -c "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"

47
vultr/gptme.sh Normal file
View file

@ -0,0 +1,47 @@
#!/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/vultr/lib/common.sh)"
fi
log_info "gptme on Vultr"
echo ""
ensure_vultr_token
ensure_ssh_key
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
verify_server_connectivity "$VULTR_SERVER_IP"
wait_for_cloud_init "$VULTR_SERVER_IP"
log_warn "Installing gptme..."
run_server "$VULTR_SERVER_IP" "pip install gptme 2>/dev/null || pip3 install gptme"
log_info "gptme installed"
echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "gptme") || exit 1
log_warn "Setting up environment variables..."
inject_env_vars_ssh "$VULTR_SERVER_IP" upload_file run_server \
"OPENROUTER_API_KEY=$OPENROUTER_API_KEY"
echo ""
log_info "Vultr instance setup completed successfully!"
log_info "Server: $SERVER_NAME (ID: $VULTR_SERVER_ID, IP: $VULTR_SERVER_IP)"
echo ""
log_warn "Starting gptme..."
sleep 1
clear
interactive_session "$VULTR_SERVER_IP" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"