cursor-opencode-provider 0.2.1 → 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 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
@@ -131,6 +131,7 @@ The selected variant’s Cursor parameter map is forwarded on the Run as `reques
131
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
132
 
133
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`)
134
135
  - only the long-context variants in its picker
135
136
  - the real Cursor model id carried in `options.cursorModelId` (not `config.id`, which would make OpenCode merge base variants into the 1M entry)
136
137
 
@@ -211,7 +212,7 @@ OpenCode
211
212
  | `src/models.ts` | `AvailableModels` fetch and `cursor-models.json` cache |
212
213
  | `src/agent-url.ts` | `GetServerConfig` fetch + in-process memo (region-specific Run host) |
213
214
  | `src/transport/connect.ts` | HTTP/2 bidi stream and unary RPC calls |
214
- | `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`) |
215
216
 
216
217
  ## Package exports
217
218
 
@@ -231,7 +232,7 @@ OpenCode
231
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. |
232
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. |
233
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. |
234
- | 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`. |
235
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. |
236
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. |
237
238
 
@@ -239,7 +240,11 @@ OpenCode
239
240
 
240
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.
241
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.
242
- - **Tool-call display stub** — semantic `tool_call_started` frames are only partially decoded. Tool execution itself goes through the exec channel and works; UI that relied solely on the display type would be incomplete.
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.
243
248
  - **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
244
249
 
245
250
  ## License
@@ -24,8 +24,9 @@ export async function buildRequestContext(input) {
24
24
  collectGit(workspaceRoot),
25
25
  collectProjectLayout(workspaceRoot),
26
26
  ]);
27
- const flat = toolsToDescriptors(tools, providerIdentifier);
28
- const nested = toolsToMcpDescriptors(tools, providerIdentifier);
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" &&
@@ -7,6 +7,7 @@ export type OpencodeJson = {
7
7
  permission?: unknown;
8
8
  plugin?: string[];
9
9
  plugins?: string[];
10
+ mcp?: Record<string, unknown>;
10
11
  };
11
12
  export declare function loadMergedConfig(workspaceRoot: string): Promise<OpencodeJson>;
12
13
  /**
@@ -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
  }
@@ -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
- * Map OpenCode's session id header to a stable Cursor conversation_id (UUID).
28
- * Falls back to a random UUID when headers are absent (direct SDK use).
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
- /** Deterministic UUID (version-4 shape) from an arbitrary session key. */
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;