Add cursor-agent support (#118)

- Add Cursor to AgentId enum
- Implement install_cursor() function for binary installation
- Add Cursor spawn logic with JSON format support
- Update README to mention Cursor support in all relevant sections

Cursor-agent runs on localhost:32123 and uses OpenCode-compatible format.
Based on opencode-cursor-auth pattern for Cursor Pro integration.

Resolves #118
This commit is contained in:
Bobby The Lobster 2026-02-09 00:01:10 +00:00
parent d236edf35c
commit 2cb2c07c6f
2 changed files with 45 additions and 4 deletions

View file

@ -5,7 +5,7 @@
<h3 align="center">Run Coding Agents in Sandboxes. Control Them Over HTTP.</h3> <h3 align="center">Run Coding Agents in Sandboxes. Control Them Over HTTP.</h3>
<p align="center"> <p align="center">
A server that runs inside your sandbox. Your app connects remotely to control Claude Code, Codex, OpenCode, or Amp — streaming events, handling permissions, managing sessions. A server that runs inside your sandbox. Your app connects remotely to control Claude Code, Codex, OpenCode, Cursor, or Amp — streaming events, handling permissions, managing sessions.
</p> </p>
<p align="center"> <p align="center">
@ -24,13 +24,13 @@ Sandbox Agent solves three problems:
1. **Coding agents need sandboxes** — You can't let AI execute arbitrary code on your production servers. Coding agents need isolated environments, but existing SDKs assume local execution. Sandbox Agent is a server that runs inside the sandbox and exposes HTTP/SSE. 1. **Coding agents need sandboxes** — You can't let AI execute arbitrary code on your production servers. Coding agents need isolated environments, but existing SDKs assume local execution. Sandbox Agent is a server that runs inside the sandbox and exposes HTTP/SSE.
2. **Every coding agent is different** — Claude Code, Codex, OpenCode, and Amp each have proprietary APIs, event formats, and behaviors. Swapping agents means rewriting your integration. Sandbox Agent provides one HTTP API — write your code once, swap agents with a config change. 2. **Every coding agent is different** — Claude Code, Codex, OpenCode, Cursor, and Amp each have proprietary APIs, event formats, and behaviors. Swapping agents means rewriting your integration. Sandbox Agent provides one HTTP API — write your code once, swap agents with a config change.
3. **Sessions are ephemeral** — Agent transcripts live in the sandbox. When the process ends, you lose everything. Sandbox Agent streams events in a universal schema to your storage. Persist to Postgres, ClickHouse, or [Rivet](https://rivet.dev). Replay later, audit everything. 3. **Sessions are ephemeral** — Agent transcripts live in the sandbox. When the process ends, you lose everything. Sandbox Agent streams events in a universal schema to your storage. Persist to Postgres, ClickHouse, or [Rivet](https://rivet.dev). Replay later, audit everything.
## Features ## Features
- **Universal Agent API**: Single interface to control Claude Code, Codex, OpenCode, and Amp with full feature coverage - **Universal Agent API**: Single interface to control Claude Code, Codex, OpenCode, Cursor, and Amp with full feature coverage
- **Streaming Events**: Real-time SSE stream of everything the agent does — tool calls, permission requests, file edits, and more - **Streaming Events**: Real-time SSE stream of everything the agent does — tool calls, permission requests, file edits, and more
- **Universal Session Schema**: [Standardized schema](https://sandboxagent.dev/docs/session-transcript-schema) that normalizes all agent event formats for storage and replay - **Universal Session Schema**: [Standardized schema](https://sandboxagent.dev/docs/session-transcript-schema) that normalizes all agent event formats for storage and replay
- **Human-in-the-Loop**: Approve or deny tool executions and answer agent questions remotely over HTTP - **Human-in-the-Loop**: Approve or deny tool executions and answer agent questions remotely over HTTP
@ -234,7 +234,7 @@ No, they're complementary. AI SDK is for building chat interfaces and calling LL
<details> <details>
<summary><strong>Which coding agents are supported?</strong></summary> <summary><strong>Which coding agents are supported?</strong></summary>
Claude Code, Codex, OpenCode, and Amp. The SDK normalizes their APIs so you can swap between them without changing your code. Claude Code, Codex, OpenCode, Cursor, and Amp. The SDK normalizes their APIs so you can swap between them without changing your code.
</details> </details>
<details> <details>

View file

@ -21,6 +21,7 @@ pub enum AgentId {
Codex, Codex,
Opencode, Opencode,
Amp, Amp,
Cursor,
Mock, Mock,
} }
@ -31,6 +32,7 @@ impl AgentId {
AgentId::Codex => "codex", AgentId::Codex => "codex",
AgentId::Opencode => "opencode", AgentId::Opencode => "opencode",
AgentId::Amp => "amp", AgentId::Amp => "amp",
AgentId::Cursor => "cursor",
AgentId::Mock => "mock", AgentId::Mock => "mock",
} }
} }
@ -41,6 +43,7 @@ impl AgentId {
AgentId::Codex => "codex", AgentId::Codex => "codex",
AgentId::Opencode => "opencode", AgentId::Opencode => "opencode",
AgentId::Amp => "amp", AgentId::Amp => "amp",
AgentId::Cursor => "cursor-agent",
AgentId::Mock => "mock", AgentId::Mock => "mock",
} }
} }
@ -51,6 +54,7 @@ impl AgentId {
"codex" => Some(AgentId::Codex), "codex" => Some(AgentId::Codex),
"opencode" => Some(AgentId::Opencode), "opencode" => Some(AgentId::Opencode),
"amp" => Some(AgentId::Amp), "amp" => Some(AgentId::Amp),
"cursor" => Some(AgentId::Cursor),
"mock" => Some(AgentId::Mock), "mock" => Some(AgentId::Mock),
_ => None, _ => None,
} }
@ -151,6 +155,7 @@ impl AgentManager {
install_opencode(&install_path, self.platform, options.version.as_deref())? install_opencode(&install_path, self.platform, options.version.as_deref())?
} }
AgentId::Amp => install_amp(&install_path, self.platform, options.version.as_deref())?, AgentId::Amp => install_amp(&install_path, self.platform, options.version.as_deref())?,
AgentId::Cursor => install_cursor(&install_path, self.platform, options.version.as_deref())?,
AgentId::Mock => { AgentId::Mock => {
if !install_path.exists() { if !install_path.exists() {
fs::write(&install_path, b"mock")?; fs::write(&install_path, b"mock")?;
@ -268,6 +273,18 @@ impl AgentManager {
} }
command.arg(&options.prompt); command.arg(&options.prompt);
} }
AgentId::Cursor => {
// cursor-agent typically runs as HTTP server on localhost:32123
// For CLI usage similar to opencode
command.arg("run").arg("--format").arg("json");
if let Some(model) = options.model.as_deref() {
command.arg("-m").arg(model);
}
if let Some(session_id) = options.session_id.as_deref() {
command.arg("-s").arg(session_id);
}
command.arg(&options.prompt);
}
AgentId::Amp => { AgentId::Amp => {
let output = spawn_amp(&path, &working_dir, &options)?; let output = spawn_amp(&path, &working_dir, &options)?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
@ -1187,6 +1204,30 @@ fn find_in_path(binary_name: &str) -> Option<PathBuf> {
None None
} }
fn install_cursor(path: &Path, platform: Platform, _version: Option<&str>) -> Result<(), AgentError> {
// Note: cursor-agent binary URL needs to be verified
// Cursor Pro includes cursor-agent, typically installed via: curl -fsS https://cursor.com/install | bash
// For sandbox-agent, we need standalone cursor-agent binary
// TODO: Determine correct download URL for cursor-agent releases
let platform_segment = match platform {
Platform::LinuxX64 | Platform::LinuxX64Musl => "linux-x64",
Platform::LinuxArm64 => "linux-arm64",
Platform::MacosArm64 => "darwin-arm64",
Platform::MacosX64 => "darwin-x64",
};
// Placeholder URL - needs to be updated with actual cursor-agent release URL
let url = Url::parse(&format!(
"https://cursor.com/api/v1/releases/latest/download/cursor-agent-{platform_segment}",
platform_segment = platform_segment
))?;
let bytes = download_bytes(&url)?;
write_executable(path, &bytes)?;
Ok(())
}
fn download_bytes(url: &Url) -> Result<Vec<u8>, AgentError> { fn download_bytes(url: &Url) -> Result<Vec<u8>, AgentError> {
let client = Client::builder().build()?; let client = Client::builder().build()?;
let mut response = client.get(url.clone()).send()?; let mut response = client.get(url.clone()).send()?;