cursor-opencode-provider 0.1.5 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLAIMER.md +9 -0
- package/README.md +38 -8
- package/dist/context/build.js +3 -2
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +1 -0
- package/dist/language-model.d.ts +52 -6
- package/dist/language-model.js +435 -63
- package/dist/models.d.ts +29 -4
- package/dist/models.js +134 -17
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +145 -43
- package/dist/protocol/blob-store.d.ts +2 -0
- package/dist/protocol/blob-store.js +4 -0
- package/dist/protocol/conversation-bind.d.ts +18 -0
- package/dist/protocol/conversation-bind.js +66 -0
- package/dist/protocol/interactions.d.ts +23 -0
- package/dist/protocol/interactions.js +136 -0
- package/dist/protocol/messages.js +421 -13
- package/dist/protocol/request.d.ts +30 -0
- package/dist/protocol/request.js +23 -8
- package/dist/protocol/stream.js +9 -3
- package/dist/protocol/tool-call-bridge.d.ts +44 -0
- package/dist/protocol/tool-call-bridge.js +480 -0
- package/dist/protocol/tools.d.ts +35 -12
- package/dist/protocol/tools.js +179 -74
- package/dist/session.d.ts +26 -1
- package/dist/session.js +2 -2
- package/dist/shared.d.ts +4 -0
- package/dist/shared.js +4 -0
- package/dist/transport/connect.js +11 -1
- package/package.json +3 -2
package/DISCLAIMER.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# EU Interoperability Disclaimer
|
|
2
|
+
|
|
3
|
+
This project is expressly designed to comply with European Union law on the legal protection of computer programs, in particular Directive 2009/24/EC of the European Parliament and of the Council of 23 April 2009 (“EU Software Directive”), as implemented in the relevant EU Member States.
|
|
4
|
+
|
|
5
|
+
The project is architected on the basis that its reverse-engineering and interface-analysis activities are strictly limited to what EU law expressly permits: acts necessary to use a lawfully acquired program for its intended purpose, including error correction, and acts strictly necessary to obtain the information required to achieve interoperability between an independently created computer program and other programs, under Articles 5 and 6 of Directive 2009/24/EC and corresponding national provisions. It is not designed to copy or reproduce proprietary source code or non-essential implementation details of any third-party software, but to access and use only the minimum interface information objectively required to enable technical interoperability where such information is not otherwise readily available through official or documented means.
|
|
6
|
+
|
|
7
|
+
When used as intended—namely, to enable lawful interoperability between independently created software components in accordance with Directive 2009/24/EC and applicable national law - this project is meant to operate in full compliance with those legal provisions. Any use that departs from this intended, interoperability-focused purpose (for example, to circumvent technical protections, replicate proprietary implementations, or violate third-party service terms) falls outside the scope of the project’s design and may not comply with applicable law; responsibility for such misuse lies solely with the user.
|
|
8
|
+
|
|
9
|
+
This statement reflects the legal and compliance orientation of the project but does not replace tailored legal advice.
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ OpenCode driving a Cursor-routed Grok model through this provider:
|
|
|
18
18
|
- **Authentication** — browser OAuth (PKCE), or API key from [cursor.com/settings](https://cursor.com/settings)
|
|
19
19
|
- **Model discovery** — fetches available models from Cursor's API and caches them locally
|
|
20
20
|
- **Streaming** — bidirectional Connect-RPC stream for agent runs
|
|
21
|
-
- **Tool calls** — maps Cursor exec-server messages to AI SDK tool-call parts; strips OpenCode's `read` XML envelope (`<path>`/`<content>` + `N:` prefixes) before returning content to Cursor so the model cannot echo the wrapper into writes
|
|
21
|
+
- **Tool calls** — maps Cursor exec-server messages to AI SDK / OpenCode tool-call parts, including the Pi read/bash/edit/write/grep/find/ls request/result field range; mirrors finalized display-only todo/plan state into OpenCode; strips OpenCode's `read` XML envelope (`<path>`/`<content>` + `N:` prefixes) before returning content to Cursor so the model cannot echo the wrapper into writes
|
|
22
22
|
- **Thinking / reasoning** — surfaces extended-thinking deltas where the model supports it
|
|
23
23
|
|
|
24
24
|
## Requirements
|
|
@@ -47,7 +47,7 @@ Add the package name to OpenCode config. OpenCode installs npm plugins with Bun
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Pin a version if you want: `"cursor-opencode-provider@0.1
|
|
50
|
+
Pin a version if you want: `"cursor-opencode-provider@0.2.1"`.
|
|
51
51
|
|
|
52
52
|
### From a local clone
|
|
53
53
|
|
|
@@ -102,7 +102,7 @@ Choose the **cursor** provider, then one of:
|
|
|
102
102
|
| **Cursor account (browser login)** | PKCE OAuth — opens cursor.com to sign in |
|
|
103
103
|
| **API key** | Paste a key from [cursor.com/settings](https://cursor.com/settings) (`sk-...`) |
|
|
104
104
|
|
|
105
|
-
After login, the plugin fetches your available models and writes them to `~/.cache/opencode/cursor-models.json` (or `$XDG_CACHE_HOME/opencode/` when set). On later startups,
|
|
105
|
+
After login, the plugin fetches your available models and writes them to `~/.cache/opencode/cursor-models.json` (or `$XDG_CACHE_HOME/opencode/` when set). On later startups, a missing, empty, expired, or old-schema cache is refreshed during config load when Cursor auth is available; an existing stale cache remains usable if refresh fails.
|
|
106
106
|
|
|
107
107
|
### Paths (XDG)
|
|
108
108
|
|
|
@@ -114,12 +114,38 @@ After login, the plugin fetches your available models and writes them to `~/.cac
|
|
|
114
114
|
|
|
115
115
|
### Select a model
|
|
116
116
|
|
|
117
|
-
Pick a model from the cached list (for example `composer-2.5`, `default`, or a Claude/GPT
|
|
117
|
+
Pick a model from the cached list (for example `composer-2.5`, `default`, or a Claude/GPT model exposed by your subscription):
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
120
|
opencode run --model cursor/composer-2.5 "Hello from Cursor via OpenCode"
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
#### Variants
|
|
124
|
+
|
|
125
|
+
Cursor models often expose parameterized variants (effort, thinking, fast, context tier, …). The plugin materializes those as OpenCode **model variants**. In the TUI, pick one from the variant dialog or cycle with OpenCode’s `variant_cycle` keybind (default `ctrl+t`).
|
|
126
|
+
|
|
127
|
+
The selected variant’s Cursor parameter map is forwarded on the Run as `requested_model.parameters` (isolated under `providerOptions.cursor.cursorVariantParameters` so unrelated OpenCode options are not leaked onto the wire).
|
|
128
|
+
|
|
129
|
+
#### 1M / long context
|
|
130
|
+
|
|
131
|
+
OpenCode’s context limit is static per model entry, while Cursor’s long-context tier is a variant parameter (`context=1m`). When a model has both a base tier and a `1m` tier, the plugin emits a separate OpenCode entry `<model-id>-1m` (for example `claude-opus-4-8-1m`) with:
|
|
132
|
+
|
|
133
|
+
- `limit.context` set to the 1M window (so overflow checks and compaction match the tier)
|
|
134
|
+
- `limit.output` set to `128000` (max generation tokens — not the context window; base entries use `32000`)
|
|
135
|
+
- only the long-context variants in its picker
|
|
136
|
+
- the real Cursor model id carried in `options.cursorModelId` (not `config.id`, which would make OpenCode merge base variants into the 1M entry)
|
|
137
|
+
|
|
138
|
+
The Run still uses Cursor’s original model id; OpenCode’s synthetic `-1m` id is only for picking and limits.
|
|
139
|
+
|
|
140
|
+
#### Max mode
|
|
141
|
+
|
|
142
|
+
Cursor IDE has a separate **Max Mode** toggle that sets `requested_model.max_mode` and selects the default max / 1m variant. OpenCode has no equivalent custom toggle, so this provider approximates it:
|
|
143
|
+
|
|
144
|
+
- Selecting a `*-1m` model (or any resolved params with `context=1m`) sets wire `max_mode` to `true`
|
|
145
|
+
- An explicit `providerOptions.cursor.maxMode` hint also turns it on
|
|
146
|
+
|
|
147
|
+
There is no independent Max Mode chrome in OpenCode beyond choosing the 1M model / long-context variant.
|
|
148
|
+
|
|
123
149
|
## Programmatic usage
|
|
124
150
|
|
|
125
151
|
```ts
|
|
@@ -186,7 +212,7 @@ OpenCode
|
|
|
186
212
|
| `src/models.ts` | `AvailableModels` fetch and `cursor-models.json` cache |
|
|
187
213
|
| `src/agent-url.ts` | `GetServerConfig` fetch + in-process memo (region-specific Run host) |
|
|
188
214
|
| `src/transport/connect.ts` | HTTP/2 bidi stream and unary RPC calls |
|
|
189
|
-
| `src/protocol/` | Protobuf encode/decode, checksum/device ids, tool-call mapping |
|
|
215
|
+
| `src/protocol/` | Protobuf encode/decode, checksum/device ids, exec + display tool-call mapping (`tool-call-bridge.ts`) |
|
|
190
216
|
|
|
191
217
|
## Package exports
|
|
192
218
|
|
|
@@ -206,7 +232,7 @@ OpenCode
|
|
|
206
232
|
| Auth / 401 errors mid-session | Re-login. OAuth and exchanged API-key JWTs refresh automatically when near expiry; a revoked refresh token needs a fresh login. |
|
|
207
233
|
| “Too many connections from different devices” | Device IDs are derived from stable OS identifiers (same approach as the Cursor CLI). Avoid running multiple clients that invent different machine fingerprints for the same account. |
|
|
208
234
|
| Empty or stale model list | Delete `~/.cache/opencode/cursor-models.json` (or under `$XDG_CACHE_HOME/opencode/`) and restart OpenCode. Existing Cursor auth is enough to refill the cache; re-login only if auth itself is broken. Cache TTL is 24h; a failed background refresh keeps serving the previous cache. |
|
|
209
|
-
| Stream hangs or HTTP/2 errors | Abort the turn and retry. The agent Run uses a bidirectional HTTP/2 stream; a dropped connection leaves the in-flight session unusable. |
|
|
235
|
+
| Stream hangs or HTTP/2 errors | Abort the turn and retry. The agent Run uses a bidirectional HTTP/2 stream; a dropped connection leaves the in-flight session unusable. With debug logging enabled, `interaction_query: replied` means a Cursor UI/approval request was answered; a later heartbeat-only stall without that line usually means OpenCode is running an older build and should be restarted after `bun run build`. |
|
|
210
236
|
| No response / silent 200 + close | The provider resolves the Run host from `GetServerConfig` (`agentUrlConfig.agentnUrl`). The URL is resolved once per process and not cached on disk. If resolution fails, the model call reports the endpoint-resolution error instead of falling back to `agentn.global.api5.cursor.sh`, which some accounts reject silently ("This region is not yet available for your team"). Set `CURSOR_PROVIDER_DEBUG=1` to confirm the resolved host in the debug log. |
|
|
211
237
|
| Need wire-level logs | Set `CURSOR_PROVIDER_DEBUG=1` (optional `CURSOR_PROVIDER_DEBUG_FILE`, default `/tmp/cursor-provider-debug.log`) and reproduce the issue. |
|
|
212
238
|
|
|
@@ -214,9 +240,13 @@ OpenCode
|
|
|
214
240
|
|
|
215
241
|
- **Personal use / ToS** — this provider speaks Cursor’s private agent protocol (CLI-shaped client identity). Use only with an account you own; Cursor may change or restrict the API without notice.
|
|
216
242
|
- **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` built from OpenCode project context (workspace env, `AGENTS.md` / `instructions`, `.opencode` agents/skills/plugins, git, layout, plus `.claude`/`.agents` skill fallbacks). Same discovery as OpenCode — including `.cursor/` paths only when listed in `instructions`. Cursor-only cloud/sandbox marketplace surfaces are omitted.
|
|
217
|
-
- **
|
|
243
|
+
- **Configured MCP tools keep their upstream server id** — OpenCode builtins and plugin/custom tools are advertised under a synthetic `opencode` MCP server. Tools whose flattened name matches an MCP server in merged `opencode.json` configuration (`github_create_pull_request`, …) are grouped into that server's `mcp_descriptors` / `provider_identifier` (`github`, …). Unknown underscore-containing names stay under `opencode` rather than being guessed incorrectly. Exec still reconstructs the full OpenCode tool id.
|
|
244
|
+
- **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` / `create_plan_tool_call`) into advertised OpenCode `todowrite`; the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged; unknown exec variants fail the turn explicitly rather than receiving a guessed response that could deadlock the Run.
|
|
245
|
+
- **Cursor-native interaction queries remain headless** — Cursor UI/approval *queries* (as distinct from display tool calls) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects questions, planning, plan-mode transitions, and known-URL fetching to equivalent OpenCode tools only when they are advertised (`question`, `todowrite`, `plan_enter` / `plan_exit`, `webfetch`); native web/PR/MCP/image/SCM requests are declined so they remain behind OpenCode's tools and permissions. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream.
|
|
246
|
+
- **Compaction resets Cursor conversation state** — the classic plugin marks OpenCode's `compaction` agent explicitly. On those turns the provider mints an isolated Cursor `conversation_id`, drops the prior checkpoint + KV blobs, preserves real tool-result text in the seed history, and re-advertises the session's last tool catalog while refusing execution during the summary itself. The first normal turn then rebases once more onto a fresh conversation seeded with OpenCode's compacted prompt and normal system instructions, so the summary-agent checkpoint cannot suppress later tool calls. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state.
|
|
247
|
+
- **Conversation bindings and compaction catalogs are bounded** — process-global per-session bindings, prior tool catalogs, and pending post-compaction rebases use a 256-session LRU bound. Evicting a conversation binding also drops its checkpoint and KV blobs.
|
|
218
248
|
- **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
|
|
219
249
|
|
|
220
250
|
## License
|
|
221
251
|
|
|
222
|
-
MIT
|
|
252
|
+
MIT
|
package/dist/context/build.js
CHANGED
|
@@ -24,8 +24,9 @@ export async function buildRequestContext(input) {
|
|
|
24
24
|
collectGit(workspaceRoot),
|
|
25
25
|
collectProjectLayout(workspaceRoot),
|
|
26
26
|
]);
|
|
27
|
-
const
|
|
28
|
-
const
|
|
27
|
+
const mcpServerNames = Object.keys(config.mcp ?? {});
|
|
28
|
+
const flat = toolsToDescriptors(tools, providerIdentifier, mcpServerNames);
|
|
29
|
+
const nested = toolsToMcpDescriptors(tools, providerIdentifier, mcpServerNames);
|
|
29
30
|
const permission = config.permission;
|
|
30
31
|
const autoRun = permission === "allow" ||
|
|
31
32
|
(typeof permission === "object" &&
|
package/dist/context/rules.d.ts
CHANGED
package/dist/context/rules.js
CHANGED
|
@@ -138,6 +138,7 @@ export async function loadMergedConfig(workspaceRoot) {
|
|
|
138
138
|
instructions: [...(globalConfig.instructions ?? []), ...(projectConfig.instructions ?? [])],
|
|
139
139
|
plugin: [...new Set([...(globalConfig.plugin ?? []), ...(projectConfig.plugin ?? [])])],
|
|
140
140
|
plugins: [...new Set([...(globalConfig.plugins ?? []), ...(projectConfig.plugins ?? [])])],
|
|
141
|
+
mcp: { ...(globalConfig.mcp ?? {}), ...(projectConfig.mcp ?? {}) },
|
|
141
142
|
permission: projectConfig.permission ?? globalConfig.permission,
|
|
142
143
|
};
|
|
143
144
|
}
|
package/dist/language-model.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { LanguageModelV3, LanguageModelV3CallOptions } from "@ai-sdk/provider";
|
|
1
|
+
import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart } from "@ai-sdk/provider";
|
|
2
2
|
import type { CreateCursorOptions } from "./index.js";
|
|
3
|
+
import { type OpencodeToolDef } from "./protocol/tools.js";
|
|
3
4
|
import { type CursorSession } from "./session.js";
|
|
5
|
+
import type { SeedHistoryMessage } from "./protocol/request.js";
|
|
6
|
+
export declare const MAX_TURN_STATE_SESSIONS = 256;
|
|
7
|
+
type V3Part = LanguageModelV3StreamPart;
|
|
4
8
|
export declare function createCursorLanguageModel(modelId: string, providerId: string, options: CreateCursorOptions): LanguageModelV3;
|
|
5
9
|
/**
|
|
6
10
|
* OpenCode re-sends the full tool-result history on every continuation. Prefer
|
|
@@ -10,6 +14,16 @@ export declare function findContinuationSession(toolResults: Array<{
|
|
|
10
14
|
sessionId: string;
|
|
11
15
|
execId: number;
|
|
12
16
|
}>): CursorSession | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Read the held-open stream, emitting stream parts, until the turn boundary:
|
|
19
|
+
* - a tool call (exec_server_message) → emit tool-call, finish "tool-calls",
|
|
20
|
+
* and KEEP the session open for the result on the next doStream call;
|
|
21
|
+
* - turn_ended / stream end → finish "stop" and close the session.
|
|
22
|
+
*/
|
|
23
|
+
export declare function pump(session: CursorSession, controller: ReadableStreamDefaultController<V3Part>, ids: {
|
|
24
|
+
textId: string;
|
|
25
|
+
reasoningId: string;
|
|
26
|
+
}, abortSignal?: AbortSignal): Promise<void>;
|
|
13
27
|
type ExtractedToolResult = {
|
|
14
28
|
sessionId: string;
|
|
15
29
|
execId: number;
|
|
@@ -24,12 +38,28 @@ type ExtractedToolResult = {
|
|
|
24
38
|
*/
|
|
25
39
|
export declare function extractTrailingToolResults(prompt: LanguageModelV3CallOptions["prompt"]): ExtractedToolResult[];
|
|
26
40
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
41
|
+
* Cursor's native UI interactions cannot be surfaced through the AI SDK.
|
|
42
|
+
* Redirect only to OpenCode tools that are genuinely advertised this turn;
|
|
43
|
+
* compaction keeps its dedicated summary prompt unchanged.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildOpenCodeInteractionGuidance(tools: OpencodeToolDef[], isCompaction: boolean): string | undefined;
|
|
46
|
+
/** Rough char→token estimate for mid-turn usage before TurnEnded arrives. */
|
|
47
|
+
export declare function estimateTokens(chars: number): number;
|
|
48
|
+
/**
|
|
49
|
+
* Prior prompt turns for a seed ConversationStateStructure after compaction
|
|
50
|
+
* reset. Drops the trailing user message (live action), but preserves tool
|
|
51
|
+
* result payloads as labeled assistant observations for Cursor's text history.
|
|
52
|
+
*/
|
|
53
|
+
export declare function extractPromptHistory(prompt: LanguageModelV3CallOptions["prompt"]): SeedHistoryMessage[];
|
|
54
|
+
/** OpenCode session id header, if present. */
|
|
55
|
+
export declare function opencodeSessionKey(callOptions: LanguageModelV3CallOptions): string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Map OpenCode's session id header to the active Cursor conversation_id.
|
|
58
|
+
* Compaction resets remint via bindConversationId; otherwise the binding is
|
|
59
|
+
* sticky for the OpenCode session. Falls back to a random UUID with no header.
|
|
29
60
|
*/
|
|
30
61
|
export declare function resolveConversationId(callOptions: LanguageModelV3CallOptions): string;
|
|
31
|
-
|
|
32
|
-
export declare function sessionIdToUuid(sessionId: string): string;
|
|
62
|
+
export { sessionIdToUuid } from "./protocol/conversation-bind.js";
|
|
33
63
|
/** Exported for tests — AI SDK V3 span ends that must precede finish / tool-call. */
|
|
34
64
|
export declare function spanEndParts(opts: {
|
|
35
65
|
textStarted: boolean;
|
|
@@ -42,4 +72,20 @@ export declare function spanEndParts(opts: {
|
|
|
42
72
|
}>;
|
|
43
73
|
/** Exported for tests — false for compaction/summary (no tools) and toolChoice none. */
|
|
44
74
|
export declare function computeAllowTools(toolCount: number, toolChoice: LanguageModelV3CallOptions["toolChoice"] | undefined): boolean;
|
|
45
|
-
export {
|
|
75
|
+
export declare function resolveTurnToolState(input: {
|
|
76
|
+
sessionKey?: string;
|
|
77
|
+
incomingTools: OpencodeToolDef[];
|
|
78
|
+
toolChoice?: LanguageModelV3CallOptions["toolChoice"];
|
|
79
|
+
isCompaction: boolean;
|
|
80
|
+
}): {
|
|
81
|
+
advertisedTools: OpencodeToolDef[];
|
|
82
|
+
allowTools: boolean;
|
|
83
|
+
};
|
|
84
|
+
export declare function resolveTurnConversationReset(input: {
|
|
85
|
+
sessionKey?: string;
|
|
86
|
+
isCompaction: boolean;
|
|
87
|
+
}): {
|
|
88
|
+
reset: boolean;
|
|
89
|
+
reason?: "compaction" | "post-compaction-rebase";
|
|
90
|
+
};
|
|
91
|
+
export declare function resetTurnStateForTests(): void;
|