Skip to content

Commit c61736e

Browse files
AhmedTMMclaudela14-1
authored
feat: add Cursor CLI agent across all clouds (#3018)
* feat: add Cursor CLI agent across all clouds Adds Cursor's terminal-based AI coding agent (the `agent` command from cursor.com/cli) to the spawn matrix. Routes LLM requests through OpenRouter via --endpoint flag and CURSOR_API_KEY env var. - manifest.json: new cursor agent entry + all 6 cloud matrix entries - agent-setup.ts: install, configure, launch, and update definitions - Shell scripts for all 6 clouds (local, hetzner, aws, do, gcp, sprite) - Config: writes ~/.cursor/cli-config.json with full permissions - Icon: cursor.png from cursor.com/apple-touch-icon.png - All cloud READMEs updated with cursor.sh usage - CLI version bumped to 0.26.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add spawn skill injection for Cursor CLI Writes a .cursor/rules/spawn.mdc rule file with alwaysApply: true during setup, teaching the Cursor agent how to use the spawn CLI to provision child cloud VMs. Uses the same base64 upload pattern as other agent config files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
1 parent 2dd87c9 commit c61736e

17 files changed

Lines changed: 397 additions & 1 deletion

File tree

.claude/rules/agent-default-models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Last verified: 2026-03-13
1515
| Kilo Code | _(provider default)_ | `KILO_PROVIDER_TYPE=openrouter` — model selection handled by Kilo Code natively |
1616
| Hermes | _(provider default)_ | `OPENAI_BASE_URL=https://openrouter.ai/api/v1` + `OPENAI_API_KEY` — model selection handled by Hermes |
1717
| Junie | _(provider default)_ | `JUNIE_OPENROUTER_API_KEY` — model selection handled by Junie natively |
18+
| Cursor CLI | _(provider default)_ | `--endpoint https://openrouter.ai/api/v1` + `CURSOR_API_KEY` — model selection via `--model` flag or `/model` in-session |
1819

1920
## When to update
2021

assets/agents/.sources.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,9 @@
3030
"junie": {
3131
"url": "custom:Junie_Icon.svg (official JetBrains Junie icon, converted to PNG)",
3232
"ext": "png"
33+
},
34+
"cursor": {
35+
"url": "https://cursor.com/apple-touch-icon.png",
36+
"ext": "png"
3337
}
3438
}

assets/agents/cursor.png

6.88 KB
Loading

manifest.json

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,55 @@
304304
"jetbrains",
305305
"byok"
306306
]
307+
},
308+
"cursor": {
309+
"name": "Cursor CLI",
310+
"description": "Cursor's terminal-based AI coding agent — autonomous coding with plan, agent, and ask modes",
311+
"url": "https://cursor.com/cli",
312+
"install": "curl https://cursor.com/install -fsS | bash",
313+
"launch": "agent",
314+
"env": {
315+
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}",
316+
"CURSOR_API_KEY": "${OPENROUTER_API_KEY}"
317+
},
318+
"config_files": {
319+
"~/.cursor/cli-config.json": {
320+
"version": 1,
321+
"permissions": {
322+
"allow": [
323+
"Shell(*)",
324+
"Read(*)",
325+
"Write(*)",
326+
"WebFetch(*)",
327+
"Mcp(*)"
328+
],
329+
"deny": []
330+
}
331+
}
332+
},
333+
"notes": "Works with OpenRouter via --endpoint flag pointing to openrouter.ai/api/v1 and CURSOR_API_KEY set to OpenRouter key. Binary installs to ~/.cursor/bin/agent.",
334+
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/cursor.png",
335+
"featured_cloud": [
336+
"digitalocean",
337+
"sprite"
338+
],
339+
"creator": "Anysphere",
340+
"repo": "anysphere/cursor",
341+
"license": "Proprietary",
342+
"created": "2025-01",
343+
"added": "2026-03",
344+
"github_stars": 10000,
345+
"stars_updated": "2026-03-26",
346+
"language": "TypeScript",
347+
"runtime": "binary",
348+
"category": "cli",
349+
"tagline": "Cursor's AI coding agent — plan, build, and ship from the terminal",
350+
"tags": [
351+
"coding",
352+
"terminal",
353+
"agentic",
354+
"cursor"
355+
]
307356
}
308357
},
309358
"clouds": {
@@ -450,6 +499,12 @@
450499
"aws/junie": "implemented",
451500
"digitalocean/junie": "implemented",
452501
"gcp/junie": "implemented",
453-
"sprite/junie": "implemented"
502+
"sprite/junie": "implemented",
503+
"local/cursor": "implemented",
504+
"hetzner/cursor": "implemented",
505+
"aws/cursor": "implemented",
506+
"digitalocean/cursor": "implemented",
507+
"gcp/cursor": "implemented",
508+
"sprite/cursor": "implemented"
454509
}
455510
}

packages/cli/src/shared/agent-setup.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,71 @@ async function setupClaudeCodeConfig(runner: CloudRunner, apiKey: string): Promi
169169
logInfo("Claude Code configured");
170170
}
171171

172+
// ─── Cursor CLI Config ────────────────────────────────────────────────────────
173+
174+
async function setupCursorConfig(runner: CloudRunner, _apiKey: string): Promise<void> {
175+
logStep("Configuring Cursor CLI for OpenRouter...");
176+
177+
const configJson = `{
178+
"version": 1,
179+
"permissions": {
180+
"allow": ["Shell(*)", "Read(*)", "Write(*)", "WebFetch(*)", "Mcp(*)"],
181+
"deny": []
182+
}
183+
}`;
184+
185+
// Spawn skill rule — injected into .cursor/rules/ so the agent knows
186+
// it can provision child VMs via the spawn CLI.
187+
const spawnRule = [
188+
"---",
189+
"description: Spawn CLI — provision child cloud VMs with AI coding agents",
190+
"alwaysApply: true",
191+
"---",
192+
"",
193+
"# Spawn — Create Child VMs",
194+
"",
195+
"You have the `spawn` CLI installed. Use it to provision cloud VMs with AI agents.",
196+
"",
197+
"## Usage",
198+
"",
199+
"```bash",
200+
'spawn <agent> <cloud> --headless --output json --prompt "task description"',
201+
"```",
202+
"",
203+
"## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie, cursor",
204+
"## Clouds: hetzner, digitalocean, aws, gcp, sprite",
205+
"",
206+
"The command returns JSON with connection details. Use this to delegate subtasks",
207+
"to specialized agents running on separate cloud VMs.",
208+
"",
209+
].join("\n");
210+
211+
const configB64 = Buffer.from(configJson).toString("base64");
212+
if (!/^[A-Za-z0-9+/=]+$/.test(configB64)) {
213+
throw new Error("Unexpected characters in base64 output");
214+
}
215+
216+
const ruleB64 = Buffer.from(spawnRule).toString("base64");
217+
if (!/^[A-Za-z0-9+/=]+$/.test(ruleB64)) {
218+
throw new Error("Unexpected characters in base64 output");
219+
}
220+
221+
const script = [
222+
"mkdir -p ~/.cursor ~/.cursor/rules",
223+
`printf '%s' '${configB64}' | base64 -d > ~/.cursor/cli-config.json`,
224+
"chmod 600 ~/.cursor/cli-config.json",
225+
// Inject spawn skill as a Cursor rule
226+
`printf '%s' '${ruleB64}' | base64 -d > ~/.cursor/rules/spawn.mdc`,
227+
"chmod 644 ~/.cursor/rules/spawn.mdc",
228+
// Persist PATH so agent binary is available
229+
'grep -q ".cursor/bin" ~/.bashrc 2>/dev/null || printf \'\\nexport PATH="$HOME/.cursor/bin:$PATH"\\n\' >> ~/.bashrc',
230+
'grep -q ".cursor/bin" ~/.zshrc 2>/dev/null || printf \'\\nexport PATH="$HOME/.cursor/bin:$PATH"\\n\' >> ~/.zshrc',
231+
].join(" && ");
232+
233+
await runner.runServer(script);
234+
logInfo("Cursor CLI configured");
235+
}
236+
172237
// ─── GitHub Auth ─────────────────────────────────────────────────────────────
173238

174239
let githubAuthRequested = false;
@@ -1115,6 +1180,28 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
11151180
'export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$PATH"; ' +
11161181
"npm install -g ${_NPM_G_FLAGS:-} @jetbrains/junie-cli@latest",
11171182
},
1183+
1184+
cursor: {
1185+
name: "Cursor CLI",
1186+
cloudInitTier: "minimal",
1187+
preProvision: detectGithubAuth,
1188+
install: () =>
1189+
installAgent(
1190+
runner,
1191+
"Cursor CLI",
1192+
"curl https://cursor.com/install -fsS | bash && " +
1193+
'export PATH="$HOME/.cursor/bin:$PATH" && ' +
1194+
"agent --version",
1195+
),
1196+
envVars: (apiKey) => [
1197+
`OPENROUTER_API_KEY=${apiKey}`,
1198+
`CURSOR_API_KEY=${apiKey}`,
1199+
],
1200+
configure: (apiKey) => setupCursorConfig(runner, apiKey),
1201+
launchCmd: () =>
1202+
'source ~/.spawnrc 2>/dev/null; export PATH="$HOME/.cursor/bin:$PATH"; agent --endpoint https://openrouter.ai/api/v1',
1203+
updateCmd: 'export PATH="$HOME/.cursor/bin:$PATH"; agent update',
1204+
},
11181205
};
11191206
}
11201207

sh/aws/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/hermes.sh)
6060
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/junie.sh)
6161
```
6262

63+
#### Cursor CLI
64+
65+
```bash
66+
bash <(curl -fsSL https://openrouter.ai/labs/spawn/aws/cursor.sh)
67+
```
68+
6369
## Non-Interactive Mode
6470

6571
```bash

sh/aws/cursor.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/bash
2+
set -eo pipefail
3+
4+
# Thin shim: ensures bun is available, runs bundled aws.js (local or from GitHub release)
5+
6+
_ensure_bun() {
7+
if command -v bun &>/dev/null; then return 0; fi
8+
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
9+
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; }
10+
export PATH="$HOME/.bun/bin:$PATH"
11+
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
12+
}
13+
14+
_ensure_bun
15+
16+
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
17+
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/aws/main.ts" ]]; then
18+
exec bun run "$SPAWN_CLI_DIR/packages/cli/src/aws/main.ts" cursor "$@"
19+
fi
20+
21+
# Remote — download and run compiled TypeScript bundle
22+
AWS_JS=$(mktemp)
23+
trap 'rm -f "$AWS_JS"' EXIT
24+
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/aws-latest/aws.js" -o "$AWS_JS" \
25+
|| { printf '\033[0;31mFailed to download aws.js\033[0m\n' >&2; exit 1; }
26+
exec bun run "$AWS_JS" cursor "$@"

sh/digitalocean/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/hermes.sh)
5252
bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/junie.sh)
5353
```
5454

55+
#### Cursor CLI
56+
57+
```bash
58+
bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/cursor.sh)
59+
```
60+
5561
## Environment Variables
5662

5763
| Variable | Description | Default |

sh/digitalocean/cursor.sh

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/bin/bash
2+
set -eo pipefail
3+
4+
# Thin shim: ensures bun is available, runs bundled digitalocean.js (local or from GitHub release)
5+
# Includes restart loop for SIGTERM recovery on DigitalOcean
6+
7+
_AGENT_NAME="cursor"
8+
_MAX_RETRIES=3
9+
10+
_ensure_bun() {
11+
if command -v bun &>/dev/null; then return 0; fi
12+
printf '\033[0;36mInstalling bun...\033[0m\n' >&2
13+
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; }
14+
export PATH="$HOME/.bun/bin:$PATH"
15+
command -v bun &>/dev/null || { printf '\033[0;31mbun not found after install\033[0m\n' >&2; exit 1; }
16+
}
17+
18+
# Run command in the foreground so bun gets full terminal access (raw mode,
19+
# arrow keys for interactive prompts). The old pattern backgrounded the child
20+
# with & + wait so a SIGTERM trap could forward the signal, but that removed
21+
# bun from the foreground process group and broke @clack/prompts multiselect.
22+
# Now SIGTERM is detected from exit code 143 (128 + 15) after the child exits.
23+
_run_with_restart() {
24+
# In headless mode (E2E / --headless), skip the restart loop entirely.
25+
# Restarting in headless mode creates duplicate droplets, exhausting the
26+
# account's droplet quota and causing all subsequent agents to fail.
27+
if [ "${SPAWN_HEADLESS:-}" = "1" ]; then
28+
"$@"
29+
return $?
30+
fi
31+
32+
local attempt=0
33+
local backoff=2
34+
while [ "$attempt" -lt "$_MAX_RETRIES" ]; do
35+
attempt=$((attempt + 1))
36+
37+
"$@"
38+
local exit_code=$?
39+
40+
# Normal exit
41+
if [ "$exit_code" -eq 0 ]; then
42+
return 0
43+
fi
44+
45+
# SIGTERM (143) or SIGKILL (137) — attempt restart
46+
if [ "$exit_code" -eq 143 ] || [ "$exit_code" -eq 137 ]; then
47+
printf '\033[0;33m[spawn/%s] Agent process terminated (exit %s). The droplet is likely still running.\033[0m\n' \
48+
"$_AGENT_NAME" "$exit_code" >&2
49+
printf '\033[0;33m[spawn/%s] Check your DigitalOcean dashboard: https://cloud.digitalocean.com/droplets\033[0m\n' \
50+
"$_AGENT_NAME" >&2
51+
if [ "$attempt" -lt "$_MAX_RETRIES" ]; then
52+
printf '\033[0;33m[spawn/%s] Restarting (attempt %s/%s, backoff %ss)...\033[0m\n' \
53+
"$_AGENT_NAME" "$((attempt + 1))" "$_MAX_RETRIES" "$backoff" >&2
54+
sleep "$backoff"
55+
backoff=$((backoff * 2))
56+
continue
57+
else
58+
printf '\033[0;31m[spawn/%s] Max restart attempts reached (%s). Giving up.\033[0m\n' \
59+
"$_AGENT_NAME" "$_MAX_RETRIES" >&2
60+
return "$exit_code"
61+
fi
62+
fi
63+
64+
# Other failure — exit with the original code
65+
return "$exit_code"
66+
done
67+
}
68+
69+
_ensure_bun
70+
71+
# SPAWN_CLI_DIR override — force local source (used by e2e tests)
72+
if [[ -n "${SPAWN_CLI_DIR:-}" && -f "$SPAWN_CLI_DIR/packages/cli/src/digitalocean/main.ts" ]]; then
73+
_run_with_restart bun run "$SPAWN_CLI_DIR/packages/cli/src/digitalocean/main.ts" "$_AGENT_NAME" "$@"
74+
exit $?
75+
fi
76+
77+
# Remote — download bundled digitalocean.js from GitHub release
78+
DO_JS=$(mktemp)
79+
trap 'rm -f "$DO_JS"' EXIT
80+
curl -fsSL --proto '=https' "https://github.com/OpenRouterTeam/spawn/releases/download/digitalocean-latest/digitalocean.js" -o "$DO_JS" \
81+
|| { printf '\033[0;31mFailed to download digitalocean.js\033[0m\n' >&2; exit 1; }
82+
83+
_run_with_restart bun run "$DO_JS" "$_AGENT_NAME" "$@"
84+
exit $?

sh/gcp/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcp/hermes.sh)
5454
bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcp/junie.sh)
5555
```
5656

57+
#### Cursor CLI
58+
59+
```bash
60+
bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcp/cursor.sh)
61+
```
62+
5763
## Non-Interactive Mode
5864

5965
```bash

0 commit comments

Comments
 (0)