Skip to content

Commit eca4fa0

Browse files
committed
feat: Cursor provider plugin for opencode (v0.1.0)
0 parents  commit eca4fa0

52 files changed

Lines changed: 10114 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "npm"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"
7+
open-pull-requests-limit: 10
8+
groups:
9+
dev-dependencies:
10+
dependency-type: "development"
11+
12+
- package-ecosystem: "github-actions"
13+
directory: "/"
14+
schedule:
15+
interval: "weekly"

.github/workflows/ci.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
build:
13+
name: typecheck · test · build (node ${{ matrix.node-version }})
14+
runs-on: ubuntu-latest
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
node-version: ["22.x", "24.x"]
19+
steps:
20+
- uses: actions/checkout@v5
21+
22+
- name: Set up Node ${{ matrix.node-version }}
23+
uses: actions/setup-node@v5
24+
with:
25+
node-version: ${{ matrix.node-version }}
26+
cache: npm
27+
28+
- name: Install dependencies
29+
run: npm ci
30+
31+
- name: Typecheck
32+
run: npm run typecheck
33+
34+
- name: Test
35+
run: npm test
36+
37+
- name: Build
38+
run: npm run build
39+
40+
integration:
41+
name: e2e · opencode loads plugin & lists models
42+
runs-on: ubuntu-latest
43+
steps:
44+
- uses: actions/checkout@v5
45+
46+
- name: Set up Node
47+
uses: actions/setup-node@v5
48+
with:
49+
node-version: "24.x"
50+
cache: npm
51+
52+
- name: Install dependencies
53+
run: npm ci
54+
55+
- name: Install opencode, load the plugin, and list Cursor models
56+
run: bash scripts/integration-test.sh
57+
env:
58+
# Optional: when set, the script additionally verifies live auth and
59+
# Cursor.models.list(). Without it, the fallback model path is tested.
60+
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}

.github/workflows/release.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v[0-9]+.[0-9]+.[0-9]+"
7+
- "v[0-9]+.[0-9]+.[0-9]+-*"
8+
9+
permissions:
10+
contents: write # create GitHub Release
11+
id-token: write # npm provenance attestation
12+
13+
jobs:
14+
release:
15+
name: Publish to npm + create GitHub Release
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v5
19+
20+
- name: Set up Node
21+
uses: actions/setup-node@v5
22+
with:
23+
node-version: "22.x"
24+
registry-url: "https://registry.npmjs.org"
25+
cache: npm
26+
27+
- name: Install dependencies
28+
run: npm ci
29+
30+
- name: Typecheck, test, and build
31+
run: npm run prepublishOnly
32+
33+
# Run the full integration test (no CURSOR_API_KEY → fallback path).
34+
# Skip with SKIP_INTEGRATION=true for patch releases that don't touch
35+
# the plugin/provider entry-points.
36+
- name: Integration smoke test
37+
if: env.SKIP_INTEGRATION != 'true'
38+
run: bash scripts/integration-test.sh
39+
env:
40+
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
41+
42+
- name: Publish to npm
43+
run: npm publish --provenance --access public
44+
env:
45+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
46+
47+
- name: Extract version from tag
48+
id: version
49+
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
50+
51+
- name: Create GitHub Release
52+
uses: softprops/action-gh-release@v2
53+
with:
54+
name: "v${{ steps.version.outputs.VERSION }}"
55+
generate_release_notes: true
56+
make_latest: true

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
node_modules/
2+
dist/
3+
*.log
4+
.DS_Store
5+
coverage/
6+
7+
# Packaging artifacts
8+
*.tgz
9+
10+
# Local editor / agent tooling
11+
.serena/
12+
.opencode/
13+
.claude/
14+
15+
# Local-only dev config (never commit)
16+
opencode.json

CHANGELOG.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## [0.1.0] — 2026-06-10
6+
7+
Initial public release. A complete opencode integration for Cursor built on the
8+
official `@cursor/sdk`: a streaming chat provider, an auth/config/model plugin,
9+
and a permission-gated delegation tool surface.
10+
11+
### Provider
12+
13+
- **Cursor provider** backed by the official `@cursor/sdk` — drives a local
14+
Cursor agent (`Agent.create` / `agent.send`) and translates its `onDelta`
15+
callbacks into AI SDK `LanguageModelV3` stream parts (text, reasoning,
16+
tool activity, usage). Implements both `doStream()` and `doGenerate()`.
17+
- **Per-request controls** via `providerOptions.cursor``mode` (agent/plan),
18+
`params`, and `thinking` level; works with opencode's model variant picker.
19+
- **Model variants** auto-generated from `Cursor.models.list` parameters: a
20+
`plan` variant plus one per reasoning level a model advertises.
21+
- **Session reuse** (`session: true`) — keeps one Cursor agent per opencode
22+
session via `Agent.resume()` across turns, with automatic fallback to a fresh
23+
agent. A run wedged by a crashed/duplicate process is recovered by retrying
24+
the send once with the SDK's `local.force` escape hatch.
25+
- **`toolDisplay` provider option** (`"reasoning"` default | `"blocks"`):
26+
- `"reasoning"` renders Cursor's internal tool activity (including the real
27+
MCP tool name) as concise `[tool] …` reasoning lines. Always safe — tool
28+
calls never cross opencode's tool-execution boundary.
29+
- `"blocks"` emits structured, provider-executed **dynamic** `tool-call` /
30+
`tool-result` parts so opencode renders native tool blocks. Names are
31+
`cursor_`-prefixed and sanitized (`shell``cursor_shell`,
32+
`serena/find_symbol``cursor_serena_find_symbol`) so they can't collide
33+
with opencode-registered tools, and carry `providerExecuted: true` +
34+
`dynamic: true` so ai v6's `parseToolCall` accepts them without
35+
registered-tool validation. Tool-results use the V3-spec `result` +
36+
`isError` fields. A tool call whose completion never arrives (run
37+
errored/cancelled mid-tool) is closed with a synthetic error result so the
38+
block never dangles as "Tool execution aborted", and a run that ends with
39+
status `error` surfaces the failure instead of finishing silently.
40+
41+
### Node sidecar (Bun compatibility)
42+
43+
- **Automatic Node sidecar** — opencode runs on Bun, whose `node:http2` client
44+
is incompatible with the Cursor SDK's long-lived streaming RPC
45+
(`NGHTTP2_FRAME_SIZE_ERROR`), causing native tool calls to execute but never
46+
report completion. When Bun is detected and `node` is on `PATH`, the SDK
47+
agent is hosted in a Node child process and driven over a JSON-lines stdio
48+
protocol; the provider is otherwise unchanged. Under Node the SDK runs
49+
in-process. Override with `OPENCODE_CURSOR_SIDECAR=1` (force on) or
50+
`OPENCODE_CURSOR_SIDECAR=0` (force in-process / silence the Bun warning).
51+
52+
### Plugin
53+
54+
- **opencode plugin** (`@stablekernel/opencode-cursor/plugin`): auth hook (API-key login;
55+
the key is validated on first use rather than at login), config hook
56+
(auto-injects `provider.cursor`),
57+
`provider.models()` (live catalog via `Cursor.models.list`), and the
58+
`cursor_refresh_models` tool. The auth loader warms a key-independent catalog
59+
cache so the model picker is populated on first authed load (and restart)
60+
rather than showing only the fallback snapshot.
61+
- **MCP server forwarding** — opencode's configured `config.mcp` entries are
62+
translated to Cursor `McpServerConfig` and passed to the local agent so it can
63+
use the same servers (e.g. Serena). Opt out with `provider.cursor.options.forwardMcp`.
64+
- **Model discovery** with a 24-hour cache (keyed by key fingerprint) and a
65+
built-in fallback snapshot (composer-2.5, claude-opus-4-8, claude-sonnet-4-6,
66+
gpt-5.5) for use without an API key.
67+
68+
### Delegation tools
69+
70+
- **`cursor_cloud_agent`** — launch a Cursor cloud (background) agent on a
71+
remote repo via `Agent.create({ cloud: { repos, autoCreatePR } })`; returns
72+
the agent id, terminal status, result, and PR url. Progress is collected from
73+
`run.onDidChangeStatus`, `onStep`, and `onDelta`.
74+
- **`cursor_delegate`** — run a single local Cursor turn as a permission-gated,
75+
auditable opencode tool call (reuses the provider's `acquireAgent` +
76+
`streamAgentTurn` plumbing). Both tools honor opencode's `permission` config
77+
via `ToolContext.ask` and are fail-closed when no permission gate is present.
78+
79+
### Tooling
80+
81+
- **Provider debug tracing** — opt-in via `OPENCODE_CURSOR_DEBUG=1`.
82+
- End-to-end CI: unit tests on two Node versions plus a full integration test
83+
(opencode loads the plugin, lists models, optionally runs a live chat turn).

CONTRIBUTING.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Contributing
2+
3+
Thanks for your interest in improving `@stablekernel/opencode-cursor`! Issues and pull
4+
requests are welcome.
5+
6+
## Reporting issues
7+
8+
- **Bugs / features:** open an issue at
9+
<https://github.com/stablekernel/opencode-cursor/issues>.
10+
- **Security vulnerabilities:** do **not** file a public issue — see
11+
[SECURITY.md](./SECURITY.md).
12+
13+
When reporting a runtime bug, please include:
14+
15+
- your runtime (Bun vs. Node) and version, and your opencode version,
16+
- whether the Node sidecar is in use (Bun + `node` on `PATH`),
17+
- output from running with `OPENCODE_CURSOR_DEBUG=1`.
18+
19+
## Development setup
20+
21+
Requires **Node.js 22+**.
22+
23+
```bash
24+
npm install
25+
npm run typecheck
26+
npm test
27+
npm run build
28+
29+
# End-to-end: install the real opencode CLI, load this plugin, and assert
30+
# `opencode models` lists the Cursor provider (no API key required — uses the
31+
# fallback snapshot; set CURSOR_API_KEY to also verify live discovery).
32+
bash scripts/integration-test.sh
33+
```
34+
35+
CI runs unit tests + build on Node 22 and 24 plus the end-to-end check on every
36+
push. Built with `@cursor/sdk`, `@opencode-ai/plugin`, and `@ai-sdk/provider`.
37+
38+
## Pull requests
39+
40+
- Add or update tests for behavior changes (this repo uses
41+
[Vitest](https://vitest.dev); see `test/`).
42+
- Keep `npm run typecheck`, `npm test`, and `npm run build` green.
43+
- Update `CHANGELOG.md` under the appropriate version/`[Unreleased]` heading.
44+
45+
## Releasing (maintainers)
46+
47+
The `.github/workflows/release.yml` workflow publishes automatically when a
48+
version tag is pushed:
49+
50+
```bash
51+
# 1. Bump the version in package.json (patch / minor / major)
52+
npm version patch # or: minor, major, or e.g. --new-version 0.2.0
53+
54+
# 2. Push the commit and the generated tag together
55+
git push origin main --follow-tags
56+
```
57+
58+
The release job will:
59+
60+
1. Run `prepublishOnly` (typecheck → test → build) to gate the publish.
61+
2. Run the integration smoke test (uses the `CURSOR_API_KEY` secret if set).
62+
3. Publish to npm with [provenance attestation](https://docs.npmjs.com/generating-provenance-statements).
63+
4. Create a GitHub Release with auto-generated release notes.
64+
65+
**Required repository secrets** (Settings → Secrets → Actions):
66+
67+
| Secret | Purpose |
68+
| --- | --- |
69+
| `NPM_TOKEN` | npm automation token with `publish` access |
70+
| `CURSOR_API_KEY` | (optional) live-path integration test during release |
71+
72+
**Pre-publish checklist:** update `CHANGELOG.md`, confirm `version` in
73+
`package.json` matches the tag, and ensure the branch is merged to `main`.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 justin-carper
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

0 commit comments

Comments
 (0)