Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fc449c2
chore: bump @cursor/sdk ^1.0.24, @opencode-ai/plugin ^1.18.4, node fl…
justin-carper Jul 23, 2026
8c27dce
feat: typed error classification + sidecar error field fidelity
justin-carper Jul 23, 2026
5e7b6cb
test: sidecar error round-trip field fidelity
justin-carper Jul 23, 2026
06d0d23
feat: transport resolution (http1/http2-direct/sidecar) with provider…
justin-carper Jul 23, 2026
9be9022
test: e2e harness (bun driver, cheapest-model picker, e2e vitest config)
justin-carper Jul 23, 2026
4f208ec
test: robust long-stream + forced web-search scenarios in evidence ma…
justin-carper Jul 23, 2026
24e0796
feat: default Bun transport to http1 (evidence gate passed); sidecar …
justin-carper Jul 23, 2026
b45cd44
feat: idempotency keys on all sends (safe retries, server-side dedupe)
justin-carper Jul 23, 2026
6d39018
feat: typed send recovery — busy->force, rate-limit/network->bounded …
justin-carper Jul 23, 2026
279dc60
feat: classify self-heal — auth/config fail fast, typed replayable set
justin-carper Jul 23, 2026
1131efe
feat: stream watchdog — pre-first-event stall cancels + force-resends…
justin-carper Jul 23, 2026
725f14f
fix: watchdog abort race + cancel wedged run on mid-stream stall
justin-carper Jul 24, 2026
39c3105
fix: capture silent-replay usage; aggregate into visible turn's finis…
justin-carper Jul 24, 2026
a9d5544
feat: thinking duration + compaction count in finish providerMetadata
justin-carper Jul 24, 2026
36f938a
feat: live tool-input streaming (partial-tool-call) + live plan strea…
justin-carper Jul 24, 2026
cfddded
feat: semSearch format-only adapter
justin-carper Jul 24, 2026
d7805c7
test: shell-output-delta probe + findings (documented-ignore: not emi…
justin-carper Jul 24, 2026
43fa285
feat: prefer SDK-authoritative model variants (displayName/isDefault)
justin-carper Jul 24, 2026
5d905ee
feat: autoReview option (classifier tool-gating middle ground)
justin-carper Jul 24, 2026
45e5d0b
feat: cursor_delegate multi-root workspace (additionalCwds)
justin-carper Jul 24, 2026
bef6e5a
docs+release: v0.5.0-next.0 — transport, reliability, streaming fidelity
justin-carper Jul 24, 2026
6e0eca1
fix: final-review follow-ups — legacy SIDECAR=0 http1 under Bun, vari…
justin-carper Jul 24, 2026
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
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,56 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.5.0-next.0] — 2026-07-23

Native-experience overhaul: in-process HTTP/1.1 transport under Bun, typed-error
reliability, and full streaming fidelity.

- **HTTP/1.1 in-process transport is now the default under Bun; the Node sidecar
is a fallback.** opencode runs on Bun, whose `node:http2` client breaks the
Cursor SDK's streaming RPC (`NGHTTP2_FRAME_SIZE_ERROR`; oven-sh/bun#31499).
The SDK now runs in-process over HTTP/1.1 + SSE
(`Cursor.configure({ local: { useHttp1ForAgent: true } })`) — no Node child
process required. Three transports are selectable via the `transport` provider
option or `OPENCODE_CURSOR_TRANSPORT`: `http1` (Bun default), `http2-direct`
(Node default), and `sidecar` (rollback). Resolution order is option →
`OPENCODE_CURSOR_TRANSPORT` → legacy `OPENCODE_CURSOR_SIDECAR`
(`1`→`sidecar`, `0`→`http2-direct`) → per-runtime default. Roll back with
`OPENCODE_CURSOR_TRANSPORT=sidecar`.
- **Typed error classification with per-kind recovery.** SDK errors are
classified into `agent-not-found`, `agent-busy`, `rate-limit`, `network`,
`auth`, `config`, and `unknown` (by error `name`/`status`/`code`, never
`instanceof` — sidecar-forwarded errors arrive as plain objects). `agent-busy`
resends once with `local.force`; `rate-limit`/`network` retry with bounded
backoff on the same agent; `auth`/`config` fail fast.
- **Idempotent resends.** Every (re)send of a turn carries an idempotency key so
a retry is a server-side dedupe, not a duplicate turn.
- **Stream watchdog.** A wedged run that streams nothing is bounded by
`OPENCODE_CURSOR_STALL_MS` (default `60000`): a pre-first-event stall cancels
and force-resends once; a stall after partial output surfaces a terminal error
rather than re-emitting the already-yielded prefix. Set to `0` to disable.
- **Fixed: silent-replay turns dropped their token usage.** A multi-message
interjection replays leading messages silently and streams only the last; the
usage from the silent turns is now summed into the visible turn's reported
usage instead of being lost.
- **Live tool-input streaming.** Cursor's `partial-tool-call` updates are bridged
to incremental tool-input parts, so tool arguments stream as they arrive
instead of appearing all at once when the call completes.
- **Thinking duration and compaction metadata.** `thinking-completed` carries the
reasoning duration, and Cursor's summary/compaction updates are surfaced as
compaction events in the stream.
- **SDK-authoritative model variants.** Variant construction prefers the SDK's
own `displayName`/`isDefault` metadata rather than deriving it locally.
- **`autoReview` option and multi-root delegation.** New `autoReview` provider
option gates tool calls through Cursor's classifier-backed Auto review
(best-effort, not a security boundary). `cursor_delegate` gains
`additionalCwds` to combine extra workspace roots into a multi-root agent
workspace.
- **Node floor raised to >=22.13** (`engines.node`, from >=22.0), and only
needed for the `sidecar` fallback transport.
- **Dependency bumps.** `@cursor/sdk` 1.0.23→1.0.24, `@opencode-ai/plugin`
1.17.14→1.18.4, `@opencode-ai/sdk` 1.17.14→1.18.4.

## [0.4.7-next.0] — 2026-07-17

- **Fixed: subagents silently ran Cursor's server-side `fast` default (e.g.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ When reporting a runtime bug, please include:

## Development setup

Requires **Node.js 22+**.
Requires **Node.js 22.13+**.

```bash
npm install
Expand Down
71 changes: 55 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ It uses the [official Cursor SDK](https://cursor.com/docs/sdk/typescript) (`@cur
## Requirements

- **opencode 1.17+**
- **Node.js 22+ on your `PATH`** — opencode runs on [Bun](https://bun.sh); the plugin needs a
Node sidecar to host the Cursor SDK (see [Runtime](#runtime-bun-and-the-node-sidecar)).
- **Node.js 22.13+ on your `PATH`** (optional) — opencode runs on [Bun](https://bun.sh) and the
plugin runs the Cursor SDK in-process by default; Node is only needed for the `sidecar`
transport fallback (see [Transport](#transport)).
- A **Cursor account and API key** (from the Cursor dashboard).

## Install
Expand All @@ -28,7 +29,7 @@ curl -fsSL https://raw.githubusercontent.com/stablekernel/opencode-cursor/main/i
```

Registers the plugin in your global `opencode.json` (`~/.config/opencode/opencode.json`), checks
for Node.js 22+, and offers to set `CURSOR_API_KEY`. Flags:
for Node.js 22.13+, and offers to set `CURSOR_API_KEY`. Flags:

- `--project` — write `./opencode.json` in the current directory instead.
- `--yes` / `-y` — non-interactive.
Expand Down Expand Up @@ -137,20 +138,25 @@ See [SECURITY.md](./SECURITY.md) for the full threat model.
| `mode` | `"agent"` | Default conversation mode (`"agent"` or `"plan"`) |
| `params` | — | Default model params, e.g. `{ thinking: "high" }` |
| `settingSources` | — | Cursor settings layers to load: `["project","user","all",...]` — pulls in your Cursor skills, rules, and `.cursor/mcp.json` |
| `sandbox` | — | Run the agent's tools in Cursor's sandbox |
| `sandbox` | — | Run the agent's tools in [Cursor's sandbox](https://cursor.com/docs/agent/sandbox) |
| `autoReview` | `false` | Gate tool calls through Cursor's classifier-backed Auto review (best-effort, not a security boundary) |
| `agents` | — | Cursor subagent definitions |
| `session` | `"auto"` | Session reuse strategy — see [Session reuse](#session-reuse-session) |
| `forwardMcp` | `true` | Forward opencode's configured MCP servers to the Cursor agent |
| `mcpServers` | — | Extra MCP servers (Cursor `McpServerConfig` shape); merged with forwarded ones |
| `toolDisplay` | `"blocks"` | How Cursor's internal tool activity is shown — see [Tool display](#tool-display) |
| `systemPrompt` | `"rules"` | How opencode's system prompt reaches the agent — see [System prompt](#system-prompt) |
| `transport` | — | Cursor agent transport (`"http1"` \| `"http2-direct"` \| `"sidecar"`) — see [Transport](#transport) |

| Environment variable | Default | Meaning |
| --- | --- | --- |
| `CURSOR_API_KEY` | — | API key fallback |
| `OPENCODE_CURSOR_MODEL_CACHE_TTL_MS` | `86400000` | Model-list cache lifetime (ms) |
| `OPENCODE_CURSOR_DEBUG` | — | Set to `1` for trace logging on stderr |
| `OPENCODE_CURSOR_SIDECAR` | — | `1` = always use Node sidecar; `0` = never |
| `OPENCODE_CURSOR_TRANSPORT` | — | Force a transport: `http1` \| `http2-direct` \| `sidecar` — see [Transport](#transport) |
| `OPENCODE_CURSOR_STALL_MS` | `60000` | Stream watchdog timeout (ms); `0` disables — see [Reliability](#reliability) |
| `OPENCODE_CURSOR_SIDECAR` | — | Legacy: `1` maps to `sidecar`, `0` maps to `http2-direct` (superseded by `OPENCODE_CURSOR_TRANSPORT`) |
| `OPENCODE_CURSOR_TOOL_INPUT_STREAM` | on | Set to `0` to disable live tool-input streaming (`tool-input-start`/`-delta`/`-end` parts) |

### Session reuse (`session`)

Expand Down Expand Up @@ -295,23 +301,56 @@ To force the fallback:
{ "provider": { "cursor": { "options": { "toolDisplay": "reasoning" } } } }
```

## Runtime: Bun and the Node sidecar
## Transport

opencode runs on [Bun](https://bun.sh), which has an `node:http2` incompatibility with the Cursor
SDK's streaming RPC. The plugin transparently hosts the Cursor SDK in a short-lived **Node child
process** when running under Bun. Under Node it runs in-process.
opencode runs on [Bun](https://bun.sh), whose `node:http2` client is incompatible with the Cursor
SDK's long-lived streaming RPC (`NGHTTP2_FRAME_SIZE_ERROR`; see
[oven-sh/bun#31499](https://github.com/oven-sh/bun/issues/31499)). The plugin works around this by
running the SDK over HTTP/1.1 in-process — no Node child process required. The historical Node
sidecar remains as a rollback fallback.

This is why **Node.js 22+ on your `PATH`** is required. If Node isn't found, the plugin warns once
and falls back to in-process (native Cursor tools will misbehave until Node is available).
| Transport | Where it runs | When it's the default |
| --- | --- | --- |
| `http1` | in-process, HTTP/1.1 + SSE (Bun-safe) | under Bun |
| `http2-direct` | in-process, SDK's default HTTP/2 | under Node (tests, scripts, non-Bun hosts) |
| `sidecar` | spawned Node child hosting the SDK | never (rollback only) |

Resolution order: the `transport` provider option → `OPENCODE_CURSOR_TRANSPORT` →
legacy `OPENCODE_CURSOR_SIDECAR` (`1`→`sidecar`, `0`→`http2-direct`) → the per-runtime default
above.

If you hit a regression on the in-process path, roll back to the sidecar:

```bash
export OPENCODE_CURSOR_TRANSPORT=sidecar # requires Node.js 22.13+ on PATH
```

An explicit `sidecar` request with no Node on `PATH` falls back to `http1` (Bun) or `http2-direct`
(Node) with a stderr notice.

## Reliability

The provider classifies Cursor SDK errors into typed kinds (`agent-not-found`, `agent-busy`,
`rate-limit`, `network`, `auth`, `config`, `unknown`) and recovers per kind:

- **agent-busy** — a previous crash left a run wedged; the send is retried once with the SDK's
`local.force` escape hatch.
- **rate-limit / network** — bounded exponential backoff on the same agent.
- **auth / config** — fail fast (not retried).

Sends carry an idempotency key so a retry is a server-side dedupe, not a duplicate turn.

Override with `OPENCODE_CURSOR_SIDECAR=1` (always sidecar) or `OPENCODE_CURSOR_SIDECAR=0` (never).
A **stream watchdog** guards against a wedged run that streams nothing: if no event arrives within
`OPENCODE_CURSOR_STALL_MS` (default `60000`), a pre-first-event stall cancels and force-resends
once; a stall after partial output is surfaced as a terminal error rather than re-emitting the
already-yielded prefix. Set `OPENCODE_CURSOR_STALL_MS=0` to disable.

## Troubleshooting

- **Native Cursor tools hang / "Tool execution aborted" (`NGHTTP2_FRAME_SIZE_ERROR`).** Node isn't
on your `PATH`. Install Node.js 22+, or force the sidecar with `OPENCODE_CURSOR_SIDECAR=1`.
- **"Running under Bun without a usable Node sidecar" warning.** Install Node.js 22+, or set
`OPENCODE_CURSOR_SIDECAR=0` to accept in-process behavior and silence the warning.
- **Native Cursor tools hang / "Tool execution aborted" (`NGHTTP2_FRAME_SIZE_ERROR`).** A
`http2-direct` transport was forced under Bun. Unset `OPENCODE_CURSOR_TRANSPORT` (defaults to the
Bun-safe `http1`), or roll back with `OPENCODE_CURSOR_TRANSPORT=sidecar` (needs Node.js 22.13+ on
`PATH`).
- **Plugin enabled but no `cursor` provider/models appear, or you see a stale-version warning.**
opencode caches the `@latest` plugin install on first use and never refreshes it.
Exit opencode, delete `~/.cache/opencode/packages/@stablekernel/opencode-cursor@latest`
Expand Down
23 changes: 13 additions & 10 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
# startup; @latest makes it re-resolve to the newest release). Reuses an
# existing opencode.jsonc or opencode.json if present (comments preserved),
# else creates opencode.json. An older/pinned entry is upgraded in place.
# 2. Verifies Node.js 22+ is on your PATH — the plugin spawns a short-lived
# Node sidecar to host the Cursor SDK (opencode itself runs on Bun).
# 2. Verifies Node.js 22.13+ is on your PATH — the plugin can spawn a
# short-lived Node sidecar to host the Cursor SDK (opencode runs on Bun).
# 3. Offers to set CURSOR_API_KEY in your shell profile if it is not set.
#
# Flags:
Expand All @@ -28,6 +28,7 @@ PKG_NAME="@stablekernel/opencode-cursor"
PKG="${PKG_NAME}@latest"
REPO_URL="https://github.com/stablekernel/opencode-cursor"
MIN_NODE_MAJOR=22
MIN_NODE_MINOR=13

SCOPE="global"
ASSUME_YES=0
Expand Down Expand Up @@ -106,22 +107,24 @@ info "${DIM}${REPO_URL}${RESET}"
info "Scope: $SCOPE"
info "Config: $CONFIG_PATH"

# ---- 1. Node.js 22+ check ----------------------------------------------------
step "Checking Node.js (>= ${MIN_NODE_MAJOR}) on PATH"
# ---- 1. Node.js 22.13+ check -------------------------------------------------
step "Checking Node.js (>= ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}) on PATH"
if command -v node >/dev/null 2>&1; then
NODE_VER="$(node --version 2>/dev/null | sed 's/^v//')"
NODE_MAJOR="${NODE_VER%%.*}"
if [ "${NODE_MAJOR:-0}" -ge "$MIN_NODE_MAJOR" ] 2>/dev/null; then
NODE_REST="${NODE_VER#*.}"
NODE_MINOR="${NODE_REST%%.*}"
if [ "${NODE_MAJOR:-0}" -gt "$MIN_NODE_MAJOR" ] ||
{ [ "${NODE_MAJOR:-0}" -eq "$MIN_NODE_MAJOR" ] && [ "${NODE_MINOR:-0}" -ge "$MIN_NODE_MINOR" ]; }; then
ok "node v${NODE_VER}"
else
warn "node v${NODE_VER} found, but the plugin needs Node ${MIN_NODE_MAJOR}+."
warn "opencode runs on Bun and spawns a Node sidecar for the Cursor SDK."
warn "Install Node ${MIN_NODE_MAJOR}+ (https://nodejs.org) and ensure it is on your PATH."
warn "node v${NODE_VER} found, but the plugin needs Node ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}+."
warn "Install Node ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}+ (https://nodejs.org) and ensure it is on your PATH."
fi
else
warn "node not found on PATH."
warn "The plugin spawns a short-lived Node process to host the Cursor SDK."
warn "Install Node ${MIN_NODE_MAJOR}+ (https://nodejs.org) before using cursor/* models."
warn "The plugin may spawn a short-lived Node process to host the Cursor SDK."
warn "Install Node ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}+ (https://nodejs.org) before using cursor/* models."
fi

# ---- 2. Add plugin to opencode.json ------------------------------------------
Expand Down
74 changes: 37 additions & 37 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading