Skip to content

feat: Cursor SDK harness (cursor-sdk) alongside existing cursor CLI harness#569

Merged
conradkoh merged 14 commits into
release/v1.47.0from
feature/cursor-sdk-implementation
May 29, 2026
Merged

feat: Cursor SDK harness (cursor-sdk) alongside existing cursor CLI harness#569
conradkoh merged 14 commits into
release/v1.47.0from
feature/cursor-sdk-implementation

Conversation

@conradkoh
Copy link
Copy Markdown
Owner

Summary

  • Adds a new cursor-sdk remote agent harness using @cursor/sdk (Agent.create + agent.send + run.stream()), mirroring the existing opencode / opencode-sdk dual-harness pattern.
  • Keeps the existing cursor CLI harness unchanged; both appear in the UI as Cursor (CLI) and Cursor (SDK).
  • New implementation lives under packages/cli/src/infrastructure/services/remote-agents/cursor-sdk/ (service, stream adapter, tests).

Changes

  • CLI: @cursor/sdk dependency, CursorSdkAgentService, CursorSdkStreamAdapter, registry registration
  • Webapp: HARNESS_DISPLAY_NAMES, isCursorSdkHarness(), test updates
  • Backend: cursor-sdk added to AGENT_HARNESSES
  • Tests: stream adapter unit tests + mocked SDK integration tests

Auth / availability

  • Requires CURSOR_API_KEY; isInstalled() returns false when missing so the harness auto-hides.

Verification

  • pnpm typecheck — pass
  • pnpm test — pass

Test plan

  • Select Cursor (SDK) in agent config when CURSOR_API_KEY is set on the machine
  • Confirm Cursor (CLI) still works unchanged
  • Start agent with SDK harness and verify stdout log lines + agent_end lifecycle
  • Stop agent mid-run and confirm cleanup (no orphaned keeper process)

Made with Cursor

@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chatroom Ready Ready Preview, Comment May 29, 2026 1:20am

conradkoh and others added 3 commits May 28, 2026 15:41
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ite3 failure

The static top-level import caused the entire daemon to crash at startup if
sqlite3's native binary failed to load (ABI mismatch, wrong Node version, or
unsupported platform). Replace with a lazy dynamic import via loadSdk() so that:

- isInstalled() returns false when the SDK can't be loaded (harness hides
  gracefully instead of crashing the daemon)
- listModels() and spawn() call loadSdk() at runtime and surface the error
  only to that specific operation
- All other harnesses are unaffected when cursor-sdk native deps fail

Ref: https://forum.cursor.com/t/cursor-sdk-in-public-beta/159285 (sqlite3
native dep known issue; Cursor team working on custom store alternative)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…DK check

BaseCLIAgentService.detectInstallationEffect() checks for a CLI binary named
after the service's command ('cursor-sdk'). No such binary exists — the SDK
harness authenticates via CURSOR_API_KEY, not a CLI tool. This caused the
harness to always appear as NotInstalled and never show in the UI.

Override detectInstallationEffect() to delegate to isInstalled() (checks
CURSOR_API_KEY presence + successful @cursor/sdk native module load), matching
the actual installation semantics of this SDK-based harness.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lists all registered harnesses and their detection status on the current
machine — useful for debugging why a harness is or isn't appearing in the UI
without needing to inspect daemon logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…resolution

Bun was inlining @cursor/sdk into the bundle. When bundled, the SDK's
internal require('sqlite3') resolved from dist/index.js's context, where
sqlite3 is not reachable due to pnpm's isolation. Marking @cursor/sdk as
external keeps it as a real runtime import so sqlite3 resolves correctly
from the SDK's own node_modules.

This fixes cursor-sdk showing "not installed" even when CURSOR_API_KEY is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…validation

Moves the build flags from a long inline CLI command to a dedicated
build.config.ts using the Bun.build() API. TypeScript's BuildConfig type
from @types/bun provides compile-time schema validation — invalid options
are caught by tsc before the build runs. The package.json build script is
now a concise two-step: typecheck then run the config file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…calls

- name: labels each agent as "role@chatroomId" in Agent.list() for observability
- local.force: true on send() clears wedged runs left by crashed daemon processes
- idempotencyKey: unique per spawn prevents double-execution on network retries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Enables the SDK sandbox for local agents. The exact OS-level restrictions
aren't documented in the SDK, but enabling it applies whatever policy
Cursor loads — useful for limiting agent surface area during testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The sandbox was too restrictive and caused the agent to stop working.
Reverting to the previous state without sandbox until the SDK documents
exactly what it restricts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…wn across all harnesses

AgentProcessManager.doStop() was directly calling kill(-pid, 'SIGTERM') and bypassing
service.stop(pid). For CLI harnesses this worked, but for cursor-sdk the actual SDK run
lives as an async IIFE in the daemon process — killing the keeper placeholder didn't
stop the Cursor SDK run or call run.cancel()/agent.close().

Now doStop() delegates to the harness-registered service.stop(pid), which properly:
- For CLI harnesses: sends SIGTERM to process group + polls (BaseCLIAgentService.stop)
- For cursor-sdk: cancels SDK run, closes agent, then kills keeper (override in CursorSdkAgentService)

Falls back to direct kill-and-poll when no service is registered for the harness.
Also explicitly calls service.untrack(pid) after stop() since handleExit() returns
early in 'stopping' state and won't untrack otherwise.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ces.

Documents CLI-based and SDK-based harness patterns so future contributors can implement new runtimes without spelunking existing code.

Co-authored-by: Cursor <cursoragent@cursor.com>
Guard run.wait() when session is aborted to avoid hanging the background
IIFE for up to an hour. Pass SIGTERM on cancelled exits and use logPrefix
for stderr error lines. Add test covering abort-during-stream path.

Co-authored-by: Cursor <cursoragent@cursor.com>
@conradkoh conradkoh merged commit f1f9d35 into release/v1.47.0 May 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant