Files
fusion/docs/cursor-cli-contract.md
gsxdsm b6efd89ae5 FN-9097: add Cursor CLI execution runtime
Add a supervised Cursor CLI runtime for routed Cursor model sessions.

- Stream Cursor prompt output into Fusion session callbacks with resume support.
- Support Windows Cursor shims, inactivity-based supervision, and cursor-agent to cursor fallback execution.
- Route Cursor CLI models through the bundled plugin and document the runtime contract.

Files changed:
 .changeset/fn-9097-cursor-cli-runtime.md           |   7 ++
 docs/cursor-cli-contract.md                        |  21 ++++
 docs/settings-reference.md                         |   4 +-
 .../cli-runtime-routing-conformance.test.ts        |  11 +-
 .../engine/src/agents/agent-session-helpers.ts     |   6 +-
 packages/engine/src/agents/cli-provider-routing.ts |  27 ++---
 plugins/fusion-plugin-cursor-runtime/README.md     |  21 ++++
 plugins/fusion-plugin-cursor-runtime/package.json  |   1 +
 .../src/__tests__/cli-spawn.test.ts                |  25 ++++-
 .../src/__tests__/prompt-transport.test.ts         |  58 ++++++++++
 .../src/__tests__/runtime-adapter.test.ts          |  44 +++++---
 .../src/__tests__/stream-parser.test.ts            |  12 +++
 .../fusion-plugin-cursor-runtime/src/cli-spawn.ts  |  57 +++++++++-
 plugins/fusion-plugin-cursor-runtime/src/index.ts  |   4 +-
 .../src/prompt-transport.ts                        | 120 +++++++++++++++++++++
 .../src/runtime-adapter.ts                         |  59 ++++++----
 .../src/stream-parser.ts                           |  43 ++++++++
 plugins/fusion-plugin-cursor-runtime/src/types.ts  |  18 ++--
 pnpm-lock.yaml                                     |   3 +
 19 files changed, 458 insertions(+), 83 deletions(-)

Fusion-Task-Id: FN-9097

Fusion-Task-Lineage: a4eed861-6059-4528-9f80-3426f5ccad58

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 08:51:35 -07:00

12 KiB

Cursor CLI Contract (FN-3396 Step 0)

Date: 2026-05-07

Update history: 2026-07-08 — corrected the model-discovery and auth-status contract from FN-3396's assumed --json commands to the verified cursor-agent models / cursor-agent status --format json contract captured and implemented in FN-7697.

Research method

  • Local runtime inspection in the task environment (which, direct command execution).
  • Local binary wrapper inspection (cursor, cursor-agent launch scripts and install layout).
  • Bounded fn_research_run was attempted but failed in this environment with: table research_runs has no column named projectId.

Confirmed invocation and binary detection

  • Primary executable aliases found on PATH:
    • cursor
    • cursor-agent
  • Not found on PATH:
    • cursor-cli
  • cursor is a wrapper that can delegate to agent mode and emits a targeted message when IDE install is missing.
  • cursor-agent is the direct CLI runtime entrypoint and is symlinked to a versioned install under:
    • ~/.local/share/cursor-agent/versions/<version>/cursor-agent

Detection strategy

  1. If the global cursorCliBinaryPath setting is a non-empty string, probe that configured binary first.
  2. Probe cursor-agent from PATH.
  3. Probe cursor from PATH.
  4. Deduplicate candidates when the configured value is exactly cursor-agent or cursor.
  5. Persist the resolved path and executable name in probe results.
  6. Report explicit failure reason when neither exists.

Manual binary path override

Settings → Authentication → Cursor CLI exposes an optional binary path field. Leave it blank to use PATH auto-detection. When populated, Fusion validates the configured path by running the same --version probe used for status/enable, saves it only if that configured candidate itself succeeds, and then uses it for status, enable validation, and Cursor model discovery before falling back to PATH candidates.

If the configured path fails during ordinary status/model-discovery probes but a PATH candidate succeeds, Fusion remains usable and reports the PATH candidate as the effective binaryPath; bounded diagnostics include the configured-path failure. If saving a new non-empty override fails or only succeeds via PATH fallback, the Settings save returns a 400 diagnostic and does not persist the path.

Windows paths with spaces, for example C:\Users\A User\AppData\Roaming\npm\cursor-agent.cmd, are treated as one operator-provided string. Users should not quote or split the path in the UI.

Windows PATH shim invocation

On Windows, cursor-agent, cursor, and manual override paths can resolve to .cmd / .bat wrappers rather than native executables. Node.js direct spawn(binary, args) does not execute those wrappers reliably; Fusion's Cursor command runner therefore sets shell execution only when process.platform === "win32".

The Windows shell-backed path applies to every Cursor CLI command Fusion currently runs through the shared runner:

  • Configured binary / cursor-agent --version / cursor --version probe attempts.
  • Auth-status probe against the effective probe-selected binary: cursor-agent status --format json.
  • Model discovery against the effective probe-selected binary: cursor-agent models (plain text, no --json flag).

Non-Windows probes and discovery continue to use direct spawn. Spawn errors such as ENOENT or EACCES are included in the unavailable probe reason in bounded diagnostic form so a working terminal command is distinguishable from known Cursor runtime/auth states; Fusion does not dump PATH, environment variables, or unbounded stdout/stderr.

Confirmed error/auth/runtime signals

Observed command behavior in this environment:

  • cursor --help (without IDE install):
    • Error: No Cursor IDE installation found. Use 'cursor agent' or 'agent' to run the agent.
  • cursor-agent --help and cursor agent --help (with locked keychain):
    • Error: Your macOS login keychain is locked.
    • Run security unlock-keychain and try again.

Auth/readiness implications

  • Keychain-locked is a distinct, expected failure mode and must be surfaced as an auth/runtime-blocked state (not as unknown crash).
  • Missing IDE install is a distinct expected failure mode from missing binary.

Structured output and model discovery

  • Confirmed: cursor-agent models is the model-list command. Output is plain text — passing an unsupported JSON output flag (e.g. appending --json to the models subcommand) fails with error: unknown option '--json'.
  • Output shape: an Available models header line, a blank line, then one model per line formatted as <id> - <Label> (e.g. auto - Auto (default), claude-4.5-sonnet - Sonnet 4.5), followed by a trailing tip line: Tip: use --model <id> (or /model <id> in interactive mode) to switch..
  • Empty-account state: No models available for this account. (no model lines follow).
  • cursor-agent --list-models exists but is unreliable — it can report "No models available for this account." even while the CLI is authenticated with models available. Prefer cursor-agent models.

Model discovery parsing strategy (implemented)

  1. Run cursor-agent models (or the effective probe-selected binary) with a short timeout.
  2. Split stdout into lines; extract the bare model id as the segment before the first - on each line.
  3. Filter out the Available models header, the trailing Tip: line, the No models available for this account. empty-state line, and blank lines.
  4. Normalize and dedupe the remaining ids into the discovered model set.
  5. If the command is unavailable or fails, return an empty discovered set with a machine-readable reason; host surfaces Cursor models only when provider readiness + discovery usability conditions are met.

Authentication / status

  • Confirmed: authentication state is derived from cursor-agent status --format json (alias whoami), which returns a JSON object with isAuthenticated (boolean), plus status, hasAccessToken, and userInfo.
  • Use isAuthenticated as the auth signal instead of treating a successful --version probe as a proxy for readiness. --version remains the availability/version probe (bare version string), separate from auth.
  • Keychain-locked and missing-IDE-install remain distinct expected failure modes on top of this (see "Confirmed error/auth/runtime signals" above) — a locked keychain or missing IDE surfaces as its own runtime-blocked state rather than folding into isAuthenticated: false.

Provider ID decision

  • Use cursor-cli as the provider ID.
  • Rationale: aligns with task requirement; no conflicting provider ID observed in current codebase scan.

Contract freeze for FN-3396 (superseded by the verified contract below)

The original FN-3396 preflight treated the following as canonical pending stronger evidence:

  • Binary candidates: cursor-agent, cursor.
  • Expected failure states include: missing binary, missing IDE installation, keychain locked, unauthenticated/not-ready CLI.
  • Model discovery must be dynamic-first with resilient fallback and no hardcoded static catalog by default.

Binary candidates and expected failure states above remain accurate. The dynamic-first/no-static-catalog principle also still holds, but the specific commands are now confirmed rather than assumed — see "Structured output and model discovery" and "Windows PATH shim invocation" above for the verified cursor-agent models / cursor-agent status --format json contract that replaces the earlier --json-flag guesswork.

Update history: 2026-08-15 — FN-9097 verified the execution transport and added the supervised Windows launch contract.

Execution transport contract

External integration evidence: Cursor CLI is closed-source with no canonical upstream source repository; public tracker: https://github.com/cursor/cursor. Docs: https://cursor.com/docs/cli/overview. Installation: curl https://cursor.com/install -fsS | bash or irm 'https://cursor.com/install?win32=true' | iex. Binary: cursor-agent. Checksum: upstream-pending-verification because Cursor publishes no per-release manifest; local observed version: 2026.08.11-e8db854.

On 2026-08-15, in an external scratch directory, printf 'Reply with exactly: ok\\n' | cursor-agent --print --output-format stream-json --force --trust --model auto exited 0 and emitted NDJSON system/init, user, thinking, assistant, and result events. The init event reported the process cwd, proving no --workspace argument is needed. A --mode plan --trust run read a scratch file, emitted a readToolCall started/completed pair, and terminated without --force. -p help states it has write/shell tool access; --force controls approvals, while --trust clears workspace trust. --sandbox was left config-driven. --resume <session_id> and --model <id> are supported; no system-prompt flag exists, so the first prompt contains the fused system context. mcp --help confirms MCP configuration is .cursor/mcp.json-based and has no --mcp-config flag.

Contract supports non-interactive execution: yes. The transport maps coding to --force --trust; readonly and unset tools to --mode plan --trust, never --force. --stream-partial-output is omitted: assistant messages are deduplicated by emitted content because Cursor can emit the same response with and without model_call_id.

Streaming Windows launch decision

Prompt turns always use superviseSpawn with shell:false, a first-line deadline, and an inactivity deadline reset by every stream line; active output has no total-duration cap. When no binary override is configured, an absent cursor-agent launch retries the documented cursor PATH fallback. They prefer direct executable targets, send the prompt on stdin, and omit --workspace. A cmd shim is the sole cmd boundary: each token is validated then rejected for " % ! ^ & | < > ( ) and controls before quote-only escaping, ComSpec must be an absolute cmd.exe, and command lines over 8000 characters fail. windowsVerbatimArguments is used only there (it had no prior repository call site). A .ps1 target uses PowerShell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File; unknown extensions fail.

This avoids cmd's re-parse (Node argv escaping is not cmd-safe), Node's refusal to direct-spawn batch files, and launcher-only kills that orphan agents. Resolution honors the first where.exe directory and PATHEXT only within it. POSIX teardown uses the supervisor process group; Windows additionally invokes taskkill /T /F. Windows PATH shim shape remains unverified on this macOS host, so launch is shape-agnostic. Probe/discovery retains its existing shell-backed behavior described above; only streaming turns use this hardened branch.