feat: add Pi coding agent (shittycodingagent.ai) to spawn (#3128)

Pi is a minimal terminal coding agent by Mario Zechner (~29.8k GitHub
stars) that natively supports OpenRouter via OPENROUTER_API_KEY.
Installed via npm as @mariozechner/pi-coding-agent, CLI command is `pi`.

- Add Pi agent config across all 6 clouds (local, hetzner, aws, do, gcp, sprite)
- Add manifest.json entry with matrix entries
- Add agent-setup.ts config (node cloudInitTier, npm install)
- Add spawn-skill.ts injection path (~/.pi/agent/skills/spawn/SKILL.md)
- Add bash wrappers for all clouds
- Update README matrix (also adds missing Cursor CLI row: 10 agents, 60 combos)

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-03-31 17:34:34 -07:00 committed by GitHub
parent 14ea507313
commit c1d8acb73e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 316 additions and 2 deletions

View file

@ -15,6 +15,7 @@ Last verified: 2026-03-13
| Hermes | _(provider default)_ | `OPENAI_BASE_URL=https://openrouter.ai/api/v1` + `OPENAI_API_KEY` — model selection handled by Hermes |
| Junie | _(provider default)_ | `JUNIE_OPENROUTER_API_KEY` — model selection handled by Junie natively |
| Cursor CLI | _(provider default)_ | `--endpoint https://openrouter.ai/api/v1` + `CURSOR_API_KEY` — model selection via `--model` flag or `/model` in-session |
| Pi | _(provider default)_ | `OPENROUTER_API_KEY` — model selection via `/model` in-session |
## When to update

View file

@ -2,7 +2,7 @@
Launch any AI agent on any cloud with a single command. Coding agents, research agents, self-hosted AI tools — Spawn deploys them all. All models powered by [OpenRouter](https://openrouter.ai). (ALPHA software, use at your own risk!)
**8 agents. 6 clouds. 48 working combinations. Zero config.**
**10 agents. 6 clouds. 60 working combinations. Zero config.**
## Install
@ -352,6 +352,7 @@ If an agent fails to install or launch on a cloud:
| [**Hermes Agent**](https://github.com/NousResearch/hermes-agent) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**Junie**](https://www.jetbrains.com/junie/) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**Cursor CLI**](https://cursor.com/cli) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [**Pi**](https://pi.dev) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
### How it works

View file

@ -30,5 +30,9 @@
"cursor": {
"url": "https://cursor.com/apple-touch-icon.png",
"ext": "png"
},
"pi": {
"url": "https://avatars.githubusercontent.com/u/506932?s=200&v=4",
"ext": "png"
}
}

BIN
assets/agents/pi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -259,6 +259,39 @@
"byok"
]
},
"pi": {
"name": "Pi",
"description": "Minimal terminal coding agent — multi-provider, tree-structured sessions, and TypeScript extensions",
"url": "https://pi.dev",
"install": "npm install -g @mariozechner/pi-coding-agent",
"launch": "pi",
"env": {
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}"
},
"notes": "Natively supports OpenRouter as a provider via OPENROUTER_API_KEY. The CLI command is 'pi'. Config lives in ~/.pi/agent/. Also known as shittycodingagent.ai.",
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/pi.png",
"featured_cloud": [
"digitalocean",
"sprite"
],
"creator": "Mario Zechner",
"repo": "badlogic/pi-mono",
"license": "MIT",
"created": "2025-06",
"added": "2026-04",
"github_stars": 29800,
"stars_updated": "2026-04-01",
"language": "TypeScript",
"runtime": "node",
"category": "cli",
"tagline": "Minimal terminal coding harness — multi-provider, extensible, tree sessions",
"tags": [
"coding",
"terminal",
"agent",
"extensible"
]
},
"cursor": {
"name": "Cursor CLI",
"description": "Cursor's terminal-based AI coding agent — autonomous coding with plan, agent, and ask modes",
@ -447,6 +480,12 @@
"digitalocean/junie": "implemented",
"gcp/junie": "implemented",
"sprite/junie": "implemented",
"local/pi": "implemented",
"hetzner/pi": "implemented",
"aws/pi": "implemented",
"digitalocean/pi": "implemented",
"gcp/pi": "implemented",
"sprite/pi": "implemented",
"local/cursor": "implemented",
"hetzner/cursor": "implemented",
"aws/cursor": "implemented",

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.29.4",
"version": "0.30.0",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -1021,6 +1021,25 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
"npm install -g ${_NPM_G_FLAGS:-} @jetbrains/junie-cli@latest",
},
pi: {
name: "Pi",
cloudInitTier: "node",
preProvision: detectGithubAuth,
install: () =>
installAgent(
runner,
"Pi",
`cd "$HOME" && ${NPM_PREFIX_SETUP} && npm install -g \${_NPM_G_FLAGS} @mariozechner/pi-coding-agent && ${NPM_GLOBAL_PATH_PERSIST}`,
),
envVars: (apiKey) => [
`OPENROUTER_API_KEY=${apiKey}`,
],
launchCmd: () => "source ~/.spawnrc 2>/dev/null; source ~/.zshrc 2>/dev/null; pi",
updateCmd:
'export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$PATH"; ' +
"npm install -g ${_NPM_G_FLAGS:-} @mariozechner/pi-coding-agent@latest",
},
cursor: {
name: "Cursor CLI",
cloudInitTier: "bun",

View file

@ -124,6 +124,11 @@ const AGENT_SKILLS: Record<string, SkillConfig> = {
content: SKILL_BODY,
append: false,
},
pi: {
remotePath: "~/.pi/agent/skills/spawn/SKILL.md",
content: SKILL_BODY,
append: false,
},
};
/** Get the remote target path for a given agent's spawn skill file. */

View file

@ -60,6 +60,12 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/junie.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/cursor.sh)
```
#### Pi
```bash
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/pi.sh)
```
## Non-Interactive Mode
```bash

26
sh/aws/pi.sh Normal file
View file

@ -0,0 +1,26 @@
#!/bin/bash
set -eo pipefail
# Thin shim: ensures bun is available, runs bundled aws.js (local or from GitHub release)
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/aws/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/aws/main.ts" pi "$@"
fi
# Remote — download and run compiled TypeScript bundle
AWS_JS=$(mktemp)
trap 'rm -f "$AWS_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/aws-latest/aws.js" -o "$AWS_JS" \
|| { printf '\033[0;31mFailed to download aws.js\033[0m\n' >&2; exit 1; }
exec bun run "$AWS_JS" pi "$@"

View file

@ -52,6 +52,12 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/junie.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/cursor.sh)
```
#### Pi
```bash
bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/pi.sh)
```
## Environment Variables
| Variable | Description | Default |

84
sh/digitalocean/pi.sh Normal file
View file

@ -0,0 +1,84 @@
#!/bin/bash
set -eo pipefail
# Thin shim: ensures bun is available, runs bundled digitalocean.js (local or from GitHub release)
# Includes restart loop for SIGTERM recovery on DigitalOcean
_AGENT_NAME="pi"
_MAX_RETRIES=3
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
# Run command in the foreground so bun gets full terminal access (raw mode,
# arrow keys for interactive prompts). The old pattern backgrounded the child
# with & + wait so a SIGTERM trap could forward the signal, but that removed
# bun from the foreground process group and broke @clack/prompts multiselect.
# Now SIGTERM is detected from exit code 143 (128 + 15) after the child exits.
_run_with_restart() {
# In headless mode (E2E / --headless), skip the restart loop entirely.
# Restarting in headless mode creates duplicate droplets, exhausting the
# account's droplet quota and causing all subsequent agents to fail.
if [ "${SPAWN_HEADLESS:-}" = "1" ]; then
"$@"
return $?
fi
local attempt=0
local backoff=2
while [ "$attempt" -lt "$_MAX_RETRIES" ]; do
attempt=$((attempt + 1))
"$@"
local exit_code=$?
# Normal exit
if [ "$exit_code" -eq 0 ]; then
return 0
fi
# SIGTERM (143) or SIGKILL (137) — attempt restart
if [ "$exit_code" -eq 143 ] || [ "$exit_code" -eq 137 ]; then
printf '\033[0;33m[spawn/%s] Agent process terminated (exit %s). The droplet is likely still running.\033[0m\n' \
"$_AGENT_NAME" "$exit_code" >&2
printf '\033[0;33m[spawn/%s] Check your DigitalOcean dashboard: https://cloud.digitalocean.com/droplets\033[0m\n' \
"$_AGENT_NAME" >&2
if [ "$attempt" -lt "$_MAX_RETRIES" ]; then
printf '\033[0;33m[spawn/%s] Restarting (attempt %s/%s, backoff %ss)...\033[0m\n' \
"$_AGENT_NAME" "$((attempt + 1))" "$_MAX_RETRIES" "$backoff" >&2
sleep "$backoff"
backoff=$((backoff * 2))
continue
else
printf '\033[0;31m[spawn/%s] Max restart attempts reached (%s). Giving up.\033[0m\n' \
"$_AGENT_NAME" "$_MAX_RETRIES" >&2
return "$exit_code"
fi
fi
# Other failure — exit with the original code
return "$exit_code"
done
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/digitalocean/main.ts" ]]; then
_run_with_restart bun run "$SPAWN_CLI_DIR/packages/cli/src/digitalocean/main.ts" "$_AGENT_NAME" "$@"
exit $?
fi
# Remote — download bundled digitalocean.js from GitHub release
DO_JS=$(mktemp)
trap 'rm -f "$DO_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
|| { printf '\033[0;31mFailed to download digitalocean.js\033[0m\n' >&2; exit 1; }
_run_with_restart bun run "$DO_JS" "$_AGENT_NAME" "$@"
exit $?

View file

@ -54,6 +54,12 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcp/junie.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcp/cursor.sh)
```
#### Pi
```bash
bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcp/pi.sh)
```
## Non-Interactive Mode
```bash

27
sh/gcp/pi.sh Normal file
View file

@ -0,0 +1,27 @@
#!/bin/bash
set -eo pipefail
# Thin shim: ensures bun is available, runs bundled gcp.js (local or from GitHub release)
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/gcp/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/gcp/main.ts" pi "$@"
fi
# Remote — download bundled gcp.js from GitHub release
GCP_JS=$(mktemp)
trap 'rm -f "$GCP_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/gcp-latest/gcp.js" -o "$GCP_JS" \
|| { printf '\033[0;31mFailed to download gcp.js\033[0m\n' >&2; exit 1; }
exec bun run "$GCP_JS" pi "$@"

View file

@ -52,6 +52,12 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/hetzner/junie.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/hetzner/cursor.sh)
```
#### Pi
```bash
bash <(curl -fsSL https://openrouter.ai/labs/spawn/hetzner/pi.sh)
```
## Non-Interactive Mode
```bash

22
sh/hetzner/pi.sh Normal file
View file

@ -0,0 +1,22 @@
#!/bin/bash
set -eo pipefail
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/hetzner/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/hetzner/main.ts" pi "$@"
fi
HETZNER_JS=$(mktemp)
trap 'rm -f "$HETZNER_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/hetzner-latest/hetzner.js" -o "$HETZNER_JS" \
|| { printf '\033[0;31mFailed to download hetzner.js\033[0m\n' >&2; exit 1; }
exec bun run "$HETZNER_JS" pi "$@"

View file

@ -17,6 +17,7 @@ spawn kilocode local
spawn hermes local
spawn junie local
spawn cursor local
spawn pi local
```
Or run directly without the CLI:
@ -30,6 +31,7 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/local/kilocode.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/local/hermes.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/local/junie.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/local/cursor.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/local/pi.sh)
```
## Non-Interactive Mode

27
sh/local/pi.sh Normal file
View file

@ -0,0 +1,27 @@
#!/bin/bash
set -eo pipefail
# Thin shim: ensures bun is available, runs bundled local.js (local or from GitHub release)
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/local/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/local/main.ts" pi "$@"
fi
# Remote — download bundled local.js from GitHub release
LOCAL_JS=$(mktemp)
trap 'rm -f "$LOCAL_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/local-latest/local.js" -o "$LOCAL_JS" \
|| { printf '\033[0;31mFailed to download local.js\033[0m\n' >&2; exit 1; }
exec bun run "$LOCAL_JS" pi "$@"

View file

@ -52,6 +52,12 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/sprite/junie.sh)
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sprite/cursor.sh)
```
#### Pi
```bash
bash <(curl -fsSL https://openrouter.ai/labs/spawn/sprite/pi.sh)
```
## Non-Interactive Mode
```bash

27
sh/sprite/pi.sh Normal file
View file

@ -0,0 +1,27 @@
#!/bin/bash
set -eo pipefail
# Thin shim: ensures bun is available, runs bundled sprite.js (local or from GitHub release)
_ensure_bun() {
if command -v bun &>/dev/null; then return 0; fi
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
curl -fsSL --proto '=https' --show-error https://bun.sh/install?version=1.3.9 | bash >/dev/null || { printf '\033[0;31mFailed to install bun\033[0m\n' >&2; exit 1; }
export PATH="$HOME/.bun/bin:$PATH"
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
}
_ensure_bun
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/sprite/main.ts" ]]; then
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/sprite/main.ts" pi "$@"
fi
# Remote — download bundled sprite.js from GitHub release
SPRITE_JS=$(mktemp)
trap 'rm -f "$SPRITE_JS"' EXIT
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/sprite-latest/sprite.js" -o "$SPRITE_JS" \
|| { printf '\033[0;31mFailed to download sprite.js\033[0m\n' >&2; exit 1; }
exec bun run "$SPRITE_JS" pi "$@"