From da8a2416dcf52f37e27a5858020bfb701b360013 Mon Sep 17 00:00:00 2001 From: Jonathan Borgwing Date: Sun, 9 Aug 2026 13:04:20 -0400 Subject: [PATCH] fix(cursor): use namespaced agent executable --- .../src/provider-cli-health.test.ts | 38 +++++++++++-------- apps/host-daemon/src/provider-cli-health.ts | 6 +-- apps/server/src/services/system/onboarding.ts | 2 +- .../agent-runtime/src/acp/adapter.test.ts | 14 +++++-- .../src/acp/bridge/model-catalog.ts | 3 +- packages/agent-runtime/src/acp/profiles.ts | 10 +++-- .../src/provider-registry.test.ts | 2 +- packages/bb-app/README.md | 2 +- packages/host-daemon-contract/src/commands.ts | 2 +- .../test/contract.test.ts | 13 +++---- 10 files changed, 54 insertions(+), 38 deletions(-) diff --git a/apps/host-daemon/src/provider-cli-health.test.ts b/apps/host-daemon/src/provider-cli-health.test.ts index e9bebce131..bbc603d7f8 100644 --- a/apps/host-daemon/src/provider-cli-health.test.ts +++ b/apps/host-daemon/src/provider-cli-health.test.ts @@ -216,7 +216,7 @@ const CLAUDE_CODE_DEFINITION: ProviderCliDefinition = { const CURSOR_DEFINITION: ProviderCliDefinition = { key: "cursor", displayName: "Cursor", - executableName: "agent", + executableName: "cursor-agent", npmPackageName: null, minimumSupportedVersion: null, installCommand: { @@ -225,8 +225,8 @@ const CURSOR_DEFINITION: ProviderCliDefinition = { }, updateCommand: { commandKind: "exec", - displayCommand: "agent update", - command: "agent", + displayCommand: "cursor-agent update", + command: "cursor-agent", args: ["update"], }, }; @@ -280,8 +280,12 @@ function installMissingClaudeCommands( function installMissingCursorCommands( runner: FakeProviderCliCommandRunner, ): void { - runner.setExit("which", ["agent"], 1, "agent not found"); - runner.setSpawnError("agent", ["--version"], "spawn agent ENOENT"); + runner.setExit("which", ["cursor-agent"], 1, "cursor-agent not found"); + runner.setSpawnError( + "cursor-agent", + ["--version"], + "spawn cursor-agent ENOENT", + ); runner.setSuccess("npm", ["prefix", "-g"], "/usr/local\n"); } @@ -342,8 +346,12 @@ function installCurrentClaudeCommands( function installCurrentCursorCommands( runner: FakeProviderCliCommandRunner, ): void { - runner.setSuccess("which", ["agent"], "/Users/me/.local/bin/agent\n"); - runner.setSuccess("agent", ["--version"], "agent 1.2.3\n"); + runner.setSuccess( + "which", + ["cursor-agent"], + "/Users/me/.local/bin/cursor-agent\n", + ); + runner.setSuccess("cursor-agent", ["--version"], "cursor-agent 1.2.3\n"); runner.setSuccess("npm", ["prefix", "-g"], "/usr/local\n"); } @@ -442,7 +450,7 @@ describe("provider CLI health", () => { expect(status).toEqual({ displayName: "Cursor", - executableName: "agent", + executableName: "cursor-agent", executablePath: null, installed: false, installSource: "notInstalled", @@ -568,7 +576,7 @@ describe("provider CLI health", () => { expect(runner.commandLines()).toContain( "npm view @anthropic-ai/claude-code version", ); - expect(runner.commandLines()).toContain("which agent"); + expect(runner.commandLines()).toContain("which cursor-agent"); expect(runner.commandLines()).not.toContain("npm view cursor version"); }); @@ -607,14 +615,14 @@ describe("provider CLI health", () => { ]); }); - it("checks Cursor installation using its agent executable", async () => { + it("checks Cursor installation using its namespaced executable", async () => { const runner = new FakeProviderCliCommandRunner(); - runner.setExit("which", ["agent"], 1, "agent not found"); + runner.setExit("which", ["cursor-agent"], 1, "cursor-agent not found"); - await expect( - isProviderCliInstalled("cursor", { runner }), - ).resolves.toBe(false); - expect(runner.commandLines()).toEqual(["which agent"]); + await expect(isProviderCliInstalled("cursor", { runner })).resolves.toBe( + false, + ); + expect(runner.commandLines()).toEqual(["which cursor-agent"]); }); it("streams failed npm installs without hiding the exit status", async () => { diff --git a/apps/host-daemon/src/provider-cli-health.ts b/apps/host-daemon/src/provider-cli-health.ts index e99d64b1f4..b3b2a8979f 100644 --- a/apps/host-daemon/src/provider-cli-health.ts +++ b/apps/host-daemon/src/provider-cli-health.ts @@ -292,7 +292,7 @@ const PROVIDER_CLI_DEFINITIONS = { cursor: { key: "cursor", displayName: "Cursor", - executableName: "agent", + executableName: "cursor-agent", npmPackageName: null, minimumSupportedVersion: null, installCommand: { @@ -301,8 +301,8 @@ const PROVIDER_CLI_DEFINITIONS = { }, updateCommand: { commandKind: "exec", - displayCommand: "agent update", - command: "agent", + displayCommand: "cursor-agent update", + command: "cursor-agent", args: ["update"], }, }, diff --git a/apps/server/src/services/system/onboarding.ts b/apps/server/src/services/system/onboarding.ts index 5de4bce0ca..05fa61f91e 100644 --- a/apps/server/src/services/system/onboarding.ts +++ b/apps/server/src/services/system/onboarding.ts @@ -67,7 +67,7 @@ const PLAN_CAPABLE_BY_PROVIDER_ID = new Map([ "acp-cursor", { cliKey: "cursor", - loginCommand: "agent login", + loginCommand: "cursor-agent login", }, ], ]); diff --git a/packages/agent-runtime/src/acp/adapter.test.ts b/packages/agent-runtime/src/acp/adapter.test.ts index c820203a10..11ce484f2a 100644 --- a/packages/agent-runtime/src/acp/adapter.test.ts +++ b/packages/agent-runtime/src/acp/adapter.test.ts @@ -27,7 +27,10 @@ function createAdapter(): AcpProviderAdapter { }); } -const CURSOR_LIST_COMMAND = { command: "agent", args: ["--list-models"] }; +const CURSOR_LIST_COMMAND = { + command: "cursor-agent", + args: ["--list-models"], +}; const THREAD_CONTEXT = { threadId: "thread-1" }; @@ -115,7 +118,7 @@ describe("acp adapter command plans", () => { params: { threadId: "thread-1", cwd: "/workspace", - agent: { command: "agent", args: ["acp"] }, + agent: { command: "cursor-agent", args: ["acp"] }, permissionMode: "accept-edits", permissionEscalation: "ask", workspaceWriteRoots: ["/workspace", "/extra-root"], @@ -393,7 +396,7 @@ describe("acp adapter model cli", () => { }); expect(plan).toMatchObject({ params: { - agent: { command: "agent", args: ["acp"] }, + agent: { command: "cursor-agent", args: ["acp"] }, modelSelection: { listCommand: CURSOR_LIST_COMMAND, selectFlag: "--model", @@ -530,7 +533,10 @@ describe("acp adapter model cli", () => { }); const params = (plan as { params: Record }).params; expect("modelSelection" in params).toBe(false); - expect(params.agent).toEqual({ command: "agent", args: ["acp"] }); + expect(params.agent).toEqual({ + command: "cursor-agent", + args: ["acp"], + }); }); }); diff --git a/packages/agent-runtime/src/acp/bridge/model-catalog.ts b/packages/agent-runtime/src/acp/bridge/model-catalog.ts index c98d6052c2..709c8a4b49 100644 --- a/packages/agent-runtime/src/acp/bridge/model-catalog.ts +++ b/packages/agent-runtime/src/acp/bridge/model-catalog.ts @@ -1,7 +1,8 @@ /** * Agent CLI model catalog. * - * Cursor's `agent --list-models` prints one `id - Display Name` line per model, + * Cursor's `cursor-agent --list-models` prints one + * `id - Display Name` line per model, * OpenCode's `opencode models` prints one bare id per line, and Grok's * `grok models` prints a bulleted list. These ids can encode reasoning effort: * `gpt-5.3-codex-low`, bare `gpt-5.3-codex` for medium, `gpt-5.5-extra-high` diff --git a/packages/agent-runtime/src/acp/profiles.ts b/packages/agent-runtime/src/acp/profiles.ts index ca7af2fa1e..7033c3786d 100644 --- a/packages/agent-runtime/src/acp/profiles.ts +++ b/packages/agent-runtime/src/acp/profiles.ts @@ -46,11 +46,13 @@ export const ACP_AGENT_PROFILES: readonly BuiltInAcpAgentProfile[] = [ { providerId: "acp-cursor", displayName: "Cursor", - // Cursor CLI installs its agent binary as `agent` (cursor.com/docs/cli); - // `cursor` is the editor's shell launcher and does not speak ACP. - agentCommand: { command: "agent", args: ["acp"] }, + // Cursor installs both `cursor-agent` and the generic `agent` alias. Use + // the namespaced executable so another provider's `agent` binary earlier + // on PATH cannot silently replace Cursor and collapse model discovery to + // the synthetic fallback. + agentCommand: { command: "cursor-agent", args: ["acp"] }, // Global flags must precede the `acp` subcommand, matching the documented - // `agent --api-key ... acp` form. + // `cursor-agent --api-key ... acp` form. modelCli: { listArgs: ["--list-models"], selectFlag: "--model", diff --git a/packages/agent-runtime/src/provider-registry.test.ts b/packages/agent-runtime/src/provider-registry.test.ts index b70c3474fd..42723e0494 100644 --- a/packages/agent-runtime/src/provider-registry.test.ts +++ b/packages/agent-runtime/src/provider-registry.test.ts @@ -195,7 +195,7 @@ describe("provider registry", () => { kind: "request", method: "thread/start", params: { - agent: { command: "agent", args: ["acp"] }, + agent: { command: "cursor-agent", args: ["acp"] }, }, }); }); diff --git a/packages/bb-app/README.md b/packages/bb-app/README.md index 857f06f53b..8628a7b122 100644 --- a/packages/bb-app/README.md +++ b/packages/bb-app/README.md @@ -132,7 +132,7 @@ bb uses whichever providers you have configured. Common providers: | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `codex` | Install the [Codex CLI](https://developers.openai.com/codex/cli). Then run `codex login` or configure credentials per the Codex docs. | | `claude-code` | Install [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and authenticate per its docs. | -| `cursor` | Install [Cursor's agent CLI](https://cursor.com/cli) (`agent`) and authenticate per Cursor's docs. | +| `cursor` | Install [Cursor's agent CLI](https://cursor.com/cli) (`cursor-agent`) and authenticate per Cursor's docs. | | `pi` | See the [Pi coding agent docs](https://github.com/earendil-works/pi/tree/main/packages/coding-agent). BB includes a pinned Pi runtime, so it does not require an installed Pi executable. | | `opencode` | Install [opencode](https://opencode.ai/) and authenticate per its docs. | | `grok` | Install [Grok Build](https://docs.x.ai/build/overview) and authenticate with `grok login` or `XAI_API_KEY`. | diff --git a/packages/host-daemon-contract/src/commands.ts b/packages/host-daemon-contract/src/commands.ts index 596e5574be..c87c46ea3d 100644 --- a/packages/host-daemon-contract/src/commands.ts +++ b/packages/host-daemon-contract/src/commands.ts @@ -35,7 +35,7 @@ import { providerCliStatusResponseSchema, } from "./local.js"; -export const HOST_DAEMON_PROTOCOL_VERSION = 87 as const; +export const HOST_DAEMON_PROTOCOL_VERSION = 88 as const; export { BRANCH_LIST_LIMIT_MAX, diff --git a/packages/host-daemon-contract/test/contract.test.ts b/packages/host-daemon-contract/test/contract.test.ts index 0715baaa4a..bad6ddb014 100644 --- a/packages/host-daemon-contract/test/contract.test.ts +++ b/packages/host-daemon-contract/test/contract.test.ts @@ -1039,13 +1039,12 @@ describe("host-daemon local schemas", () => { }); describe("host-daemon command schemas", () => { - // Version 87 keeps the provider thread id across an environment directory - // switch. The server now sends `turn.submit` with that id where it sent - // `thread.start` before, and the daemon must release the old runtime owner - // first. A daemon without that release would own the same provider session - // twice, so an older daemon must update instead of connecting. - it("uses protocol version 87 for moved-thread session handoff", () => { - expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(87); + // Version 88 switches the built-in Cursor profile from the generic `agent` + // executable to `cursor-agent`. Older daemons retain their local built-in + // profile, so they must update before connecting to avoid launching another + // provider's `agent` binary and losing Cursor model discovery. + it("uses protocol version 88 for the namespaced Cursor executable", () => { + expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(88); }); it("binds Plan cancellation to a required turn id and typed result", () => {