Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Buzz Relay ──WS──→ buzz-acp ──stdio──→ Your Agent
(send_message, etc.)
```

Supports any agent that speaks [ACP](https://agentclientprotocol.com/) over stdio: **goose**, **codex** (via [codex-acp](https://github.com/agentclientprotocol/codex-acp)), and **claude code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)).
Supports any agent that speaks [ACP](https://agentclientprotocol.com/) over stdio: **Goose**, **Codex** (via [codex-acp](https://github.com/agentclientprotocol/codex-acp)), **Claude Code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)), **OpenCode**, and **Cursor Agent**.

## Prerequisites

Expand Down Expand Up @@ -89,6 +89,36 @@ buzz-acp
Older installs that still expose `claude-code-acp` are also supported. `buzz-acp`
treats both Claude ACP command names as the same zero-arg runtime.

## Running with OpenCode

[OpenCode](https://opencode.ai/docs/acp/) includes a native ACP server.

```bash
# Install OpenCode if needed
curl -fsSL https://opencode.ai/install | bash

# buzz-acp recognizes OpenCode and supplies the required `acp` argument.
export BUZZ_ACP_AGENT_COMMAND="opencode"
buzz-acp
```

## Running with Cursor Agent

[Cursor Agent](https://cursor.com/docs/cli/acp) also includes a native ACP server.

```bash
# Install Cursor Agent if needed
curl https://cursor.com/install -fsS | bash

# `agent` is Cursor's ACP command. `cursor-agent` is accepted as an alias.
export BUZZ_ACP_AGENT_COMMAND="agent"
buzz-acp
```

For both runtimes, an empty `BUZZ_ACP_AGENT_ARGS` is normalized to `acp`. If
you set it for runtime-specific arguments, keep `acp` first (for example,
`BUZZ_ACP_AGENT_ARGS="acp,--pure"`).

## Configuration

All configuration is via environment variables (or CLI flags — every env var has a matching flag).
Expand Down
15 changes: 13 additions & 2 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,9 @@ pub(crate) fn normalize_agent_command_identity(command: &str) -> String {

fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_agent_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
// These runtimes expose their ACP servers behind an explicit `acp`
// subcommand. Keep this list in sync with desktop discovery.
"goose" | "opencode" | "agent" | "cursor-agent" => Some(vec!["acp".to_string()]),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "buzz-agent" => Some(Vec::new()),
_ => None,
Expand Down Expand Up @@ -1427,9 +1429,18 @@ mod tests {
}

#[test]
fn normalizes_goose_args_to_acp() {
fn normalizes_acp_subcommand_runtimes_to_acp() {
assert_eq!(normalize_agent_args("goose", Vec::new()), vec!["acp"]);
assert_eq!(normalize_agent_args("goose", vec!["".into()]), vec!["acp"]);
assert_eq!(normalize_agent_args("opencode", Vec::new()), vec!["acp"]);
assert_eq!(
normalize_agent_args("/Users/me/.local/bin/agent", vec!["".into()]),
vec!["acp"]
);
assert_eq!(
normalize_agent_args("cursor-agent", Vec::new()),
vec!["acp"]
);
}

#[test]
Expand Down
72 changes: 71 additions & 1 deletion desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub(crate) use runtime_metadata::KnownAcpRuntime;
const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png";
const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default";
const CODEX_AVATAR_URL: &str = "https://openai.gallerycdn.vsassets.io/extensions/openai/chatgpt/26.5313.41514/1773706730621/Microsoft.VisualStudio.Services.Icons.Default";
const OPENCODE_AVATAR_URL: &str = "https://opencode.ai/favicon.svg";
const CURSOR_AVATAR_URL: &str = "https://cursor.com/favicon.svg";
const BUZZ_AGENT_AVATAR_URL: &str =
"https://raw.githubusercontent.com/block/buzz/refs/heads/main/crates/buzz-agent/buzz-agent.png";

Expand Down Expand Up @@ -157,6 +159,72 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
// Verified: `codex login status` exits 0 when logged in, non-zero otherwise.
auth_probe_args: Some(&["codex", "login", "status"]),
},
KnownAcpRuntime {
id: "opencode",
label: "OpenCode",
commands: &["opencode"],
aliases: &[],
avatar_url: OPENCODE_AVATAR_URL,
mcp_command: None,
mcp_hooks: false,
// OpenCode's CLI is its native ACP server; retain the CLI identity so
// the installer executes Phase 1 when the command is absent.
underlying_cli: Some("opencode"),
cli_install_commands: &["curl -fsSL https://opencode.ai/install | bash"],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
install_instructions_url: "https://opencode.ai/docs/acp/",
cli_install_hint: "Install OpenCode via the official install script.",
adapter_install_hint: "",
skill_dir: None,
supports_acp_model_switching: false,
model_env_var: None,
provider_env_var: None,
provider_locked: false,
default_env: &[],
config_file_path: None,
config_file_format: None,
supports_acp_native_config: false,
thinking_env_var: None,
max_tokens_env_var: None,
context_limit_env_var: None,
required_normalized_fields: &[],
login_hint: Some("Run `opencode auth login` to configure a provider."),
auth_probe_args: None,
},
KnownAcpRuntime {
id: "cursor",
label: "Cursor Agent",
commands: &["agent", "cursor-agent"],
aliases: &[],
avatar_url: CURSOR_AVATAR_URL,
mcp_command: None,
mcp_hooks: false,
// Cursor Agent is likewise the native ACP server. This makes its
// install command reachable through the CLI-install phase.
underlying_cli: Some("agent"),
cli_install_commands: &["curl https://cursor.com/install -fsS | bash"],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
install_instructions_url: "https://cursor.com/docs/cli/acp",
cli_install_hint: "Install Cursor Agent via the official install script.",
adapter_install_hint: "",
skill_dir: None,
supports_acp_model_switching: false,
model_env_var: None,
provider_env_var: None,
provider_locked: false,
default_env: &[],
config_file_path: None,
config_file_format: None,
supports_acp_native_config: false,
thinking_env_var: None,
max_tokens_env_var: None,
context_limit_env_var: None,
required_normalized_fields: &[],
login_hint: Some("Run `agent login` to authenticate with Cursor."),
auth_probe_args: Some(&["agent", "status"]),
},
KnownAcpRuntime {
id: "buzz-agent",
label: "Buzz Agent",
Expand Down Expand Up @@ -342,7 +410,9 @@ pub use overrides::{apply_agent_command_update, create_time_agent_command_overri

fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
// These runtimes expose their ACP servers behind an explicit `acp`
// subcommand. Keep this list in sync with buzz-acp configuration.
"goose" | "opencode" | "agent" | "cursor-agent" => Some(vec!["acp".to_string()]),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "buzz-agent" => Some(Vec::new()),
_ => None,
Expand Down
23 changes: 23 additions & 0 deletions desktop/src-tauri/src/managed_agents/discovery/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,29 @@ fn normalizes_claude_and_codex_args_to_empty() {
);
}

#[test]
fn normalizes_opencode_and_cursor_args_to_acp() {
assert_eq!(normalize_agent_args("opencode", Vec::new()), vec!["acp"]);
assert_eq!(normalize_agent_args("agent", vec!["".into()]), vec!["acp"]);
assert_eq!(normalize_agent_args("cursor-agent", Vec::new()), vec!["acp"]);
}

#[test]
fn opencode_and_cursor_are_known_native_acp_runtimes() {
let opencode = super::known_acp_runtime_exact("opencode").expect("OpenCode runtime");
assert_eq!(opencode.commands, ["opencode"]);
assert_eq!(opencode.underlying_cli, Some("opencode"));
assert!(!opencode.cli_install_commands_for_os().is_empty());
assert_eq!(normalize_agent_args(opencode.commands[0], Vec::new()), vec!["acp"]);

let cursor = super::known_acp_runtime_exact("cursor").expect("Cursor runtime");
assert_eq!(cursor.commands, ["agent", "cursor-agent"]);
assert_eq!(cursor.underlying_cli, Some("agent"));
assert!(!cursor.cli_install_commands_for_os().is_empty());
assert!(!cursor.provider_locked);
assert_eq!(normalize_agent_args(cursor.commands[0], Vec::new()), vec!["acp"]);
}

#[test]
fn resolves_buzz_agent_avatar() {
assert_eq!(
Expand Down