cursor-opencode-provider 0.1.1

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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/auth.d.ts +48 -0
  4. package/dist/auth.js +200 -0
  5. package/dist/context/agents.d.ts +8 -0
  6. package/dist/context/agents.js +76 -0
  7. package/dist/context/build.d.ts +12 -0
  8. package/dist/context/build.js +83 -0
  9. package/dist/context/env.d.ts +1 -0
  10. package/dist/context/env.js +29 -0
  11. package/dist/context/git.d.ts +20 -0
  12. package/dist/context/git.js +59 -0
  13. package/dist/context/index.d.ts +2 -0
  14. package/dist/context/index.js +2 -0
  15. package/dist/context/layout.d.ts +17 -0
  16. package/dist/context/layout.js +58 -0
  17. package/dist/context/paths.d.ts +3 -0
  18. package/dist/context/paths.js +11 -0
  19. package/dist/context/plugins.d.ts +8 -0
  20. package/dist/context/plugins.js +50 -0
  21. package/dist/context/rules.d.ts +19 -0
  22. package/dist/context/rules.js +198 -0
  23. package/dist/context/skills.d.ts +11 -0
  24. package/dist/context/skills.js +104 -0
  25. package/dist/index.d.ts +15 -0
  26. package/dist/index.js +18 -0
  27. package/dist/language-model.d.ts +45 -0
  28. package/dist/language-model.js +834 -0
  29. package/dist/models.d.ts +49 -0
  30. package/dist/models.js +136 -0
  31. package/dist/plugin-v2.d.ts +2 -0
  32. package/dist/plugin-v2.js +48 -0
  33. package/dist/plugin.d.ts +2 -0
  34. package/dist/plugin.js +201 -0
  35. package/dist/protocol/blob-store.d.ts +15 -0
  36. package/dist/protocol/blob-store.js +52 -0
  37. package/dist/protocol/checkpoint.d.ts +17 -0
  38. package/dist/protocol/checkpoint.js +29 -0
  39. package/dist/protocol/checksum.d.ts +2 -0
  40. package/dist/protocol/checksum.js +23 -0
  41. package/dist/protocol/client-version.d.ts +5 -0
  42. package/dist/protocol/client-version.js +150 -0
  43. package/dist/protocol/device-id.d.ts +8 -0
  44. package/dist/protocol/device-id.js +121 -0
  45. package/dist/protocol/framing.d.ts +10 -0
  46. package/dist/protocol/framing.js +90 -0
  47. package/dist/protocol/kv.d.ts +24 -0
  48. package/dist/protocol/kv.js +81 -0
  49. package/dist/protocol/messages.d.ts +11 -0
  50. package/dist/protocol/messages.js +676 -0
  51. package/dist/protocol/request.d.ts +36 -0
  52. package/dist/protocol/request.js +90 -0
  53. package/dist/protocol/stream.d.ts +38 -0
  54. package/dist/protocol/stream.js +64 -0
  55. package/dist/protocol/struct.d.ts +19 -0
  56. package/dist/protocol/struct.js +186 -0
  57. package/dist/protocol/thinking.d.ts +15 -0
  58. package/dist/protocol/thinking.js +17 -0
  59. package/dist/protocol/tools.d.ts +94 -0
  60. package/dist/protocol/tools.js +631 -0
  61. package/dist/session.d.ts +81 -0
  62. package/dist/session.js +96 -0
  63. package/dist/shared.d.ts +15 -0
  64. package/dist/shared.js +13 -0
  65. package/dist/transport/connect.d.ts +23 -0
  66. package/dist/transport/connect.js +275 -0
  67. package/package.json +65 -0
@@ -0,0 +1,81 @@
1
+ import type { BidiStream } from "./transport/connect.js";
2
+ export type Frame = {
3
+ flags: number;
4
+ payload: Uint8Array;
5
+ };
6
+ /**
7
+ * A held-open Run stream. Cursor drives the agentic loop server-side and
8
+ * expects tool results on the SAME bidi stream. opencode, by contrast, owns
9
+ * its tool loop: it calls `doStream`, gets a tool-call, executes it, then calls
10
+ * `doStream` again with the result. We bridge the two by keeping the Run stream
11
+ * (and its single frames iterator) alive in a session across `doStream` calls,
12
+ * keyed by the exec ids we are waiting results for.
13
+ */
14
+ export type PendingExec = {
15
+ /** ExecClientMessage result field to reply with (matches the request variant). */
16
+ resultField: string;
17
+ };
18
+ export type CursorSession = {
19
+ /**
20
+ * Stable per-Run-stream id (distinct from Cursor's own conversation_id).
21
+ * Tags toolCallIds so two concurrent Run streams with overlapping exec ids
22
+ * (Cursor resets them per stream) can't cross-deliver results.
23
+ */
24
+ sessionId: string;
25
+ /**
26
+ * Cursor conversation_id for this Run — used to store/echo
27
+ * conversation_checkpoint_update (CLI parity).
28
+ */
29
+ conversationId: string;
30
+ stream: BidiStream;
31
+ frames: AsyncIterator<Frame>;
32
+ pending: Map<number, PendingExec>;
33
+ /** KV blob store: blob_id (hex) → data, for Cursor's out-of-band payload channel. */
34
+ blobs: Map<string, Uint8Array>;
35
+ /** McpToolDefinition list advertised this turn — echoed into the request_context reply. */
36
+ toolDescriptors: Array<Record<string, unknown>>;
37
+ /** Full RequestContext for exec #10 replies. */
38
+ requestContext: Record<string, unknown>;
39
+ /**
40
+ * False when OpenCode passed no tools (compaction/summary) or toolChoice "none".
41
+ * Cursor may still fire native Grep/etc.; we must refuse those on the Run
42
+ * stream instead of emitting tool-call parts OpenCode will reject.
43
+ */
44
+ allowTools: boolean;
45
+ /**
46
+ * True while a doStream pull() is actively reading this session's frames.
47
+ * Prevents a late cancel/abort from a prior ReadableStream from destroying
48
+ * the Run connection after tool results were delivered (pending cleared) but
49
+ * Cursor is still generating.
50
+ */
51
+ pumpActive: boolean;
52
+ heartbeat: ReturnType<typeof setInterval> | null;
53
+ expiresAt: number;
54
+ };
55
+ export declare class SessionManager {
56
+ private byExecId;
57
+ private readonly idleTimeoutMs;
58
+ constructor(idleTimeoutMs?: number);
59
+ touch(session: CursorSession): void;
60
+ /** Register that `session` is awaiting a result for `execId`. */
61
+ registerPending(execId: number, session: CursorSession, resultField: string): void;
62
+ /** The pending exec info for an id on a specific session, if still awaiting it. */
63
+ pendingFor(sessionId: string, execId: number): PendingExec | undefined;
64
+ /** Find the live session awaiting one of the given exec ids. */
65
+ findByExecIds(sessionId: string, execIds: number[]): CursorSession | undefined;
66
+ /** Mark an exec id as resolved (its result has been delivered). */
67
+ resolve(sessionId: string, execId: number): void;
68
+ private key;
69
+ close(session: CursorSession): void;
70
+ /**
71
+ * Close only if nothing is awaiting a tool result AND no pull() is actively
72
+ * pumping this session. OpenCode aborts each doStream after finishReason
73
+ * "tool-calls"; that abort must NOT tear down the Cursor Run stream.
74
+ * Equally, a late cancel from the previous ReadableStream must not destroy
75
+ * the session once the continuation has cleared pending and resumed pumping.
76
+ * Returns true if the session was closed.
77
+ */
78
+ closeUnlessPending(session: CursorSession): boolean;
79
+ dispose(): void;
80
+ }
81
+ export declare const sessionManager: SessionManager;
@@ -0,0 +1,96 @@
1
+ import { trace } from "./transport/connect.js";
2
+ export class SessionManager {
3
+ // Composite key `${sessionId}:${execId}` → owning session. Composite keying
4
+ // means two Run streams that both register an execId of 1 (Cursor resets
5
+ // counters per stream) coexist instead of overwriting each other.
6
+ byExecId = new Map();
7
+ idleTimeoutMs;
8
+ constructor(idleTimeoutMs = 300_000) {
9
+ this.idleTimeoutMs = idleTimeoutMs;
10
+ }
11
+ touch(session) {
12
+ session.expiresAt = Date.now() + this.idleTimeoutMs;
13
+ }
14
+ /** Register that `session` is awaiting a result for `execId`. */
15
+ registerPending(execId, session, resultField) {
16
+ session.pending.set(execId, { resultField });
17
+ this.byExecId.set(this.key(session.sessionId, execId), session);
18
+ this.touch(session);
19
+ }
20
+ /** The pending exec info for an id on a specific session, if still awaiting it. */
21
+ pendingFor(sessionId, execId) {
22
+ return this.byExecId.get(this.key(sessionId, execId))?.pending.get(execId);
23
+ }
24
+ /** Find the live session awaiting one of the given exec ids. */
25
+ findByExecIds(sessionId, execIds) {
26
+ for (const id of execIds) {
27
+ const s = this.byExecId.get(this.key(sessionId, id));
28
+ if (s && Date.now() < s.expiresAt)
29
+ return s;
30
+ if (s)
31
+ this.close(s);
32
+ }
33
+ return undefined;
34
+ }
35
+ /** Mark an exec id as resolved (its result has been delivered). */
36
+ resolve(sessionId, execId) {
37
+ const k = this.key(sessionId, execId);
38
+ const s = this.byExecId.get(k);
39
+ if (s)
40
+ s.pending.delete(execId);
41
+ this.byExecId.delete(k);
42
+ }
43
+ key(sessionId, execId) {
44
+ return `${sessionId}:${execId}`;
45
+ }
46
+ close(session) {
47
+ trace(`sessionManager.close: pending=[${[...session.pending.keys()].join(",")}] blobs=${session.blobs.size}`);
48
+ if (session.heartbeat)
49
+ clearInterval(session.heartbeat);
50
+ session.heartbeat = null;
51
+ session.stream.destroy();
52
+ for (const id of session.pending.keys())
53
+ this.byExecId.delete(this.key(session.sessionId, id));
54
+ session.pending.clear();
55
+ }
56
+ /**
57
+ * Close only if nothing is awaiting a tool result AND no pull() is actively
58
+ * pumping this session. OpenCode aborts each doStream after finishReason
59
+ * "tool-calls"; that abort must NOT tear down the Cursor Run stream.
60
+ * Equally, a late cancel from the previous ReadableStream must not destroy
61
+ * the session once the continuation has cleared pending and resumed pumping.
62
+ * Returns true if the session was closed.
63
+ */
64
+ closeUnlessPending(session) {
65
+ if (session.pending.size > 0 || session.pumpActive) {
66
+ trace(`sessionManager.closeUnlessPending: KEEP open pending=[${[...session.pending.keys()].join(",")}] pumpActive=${session.pumpActive}`);
67
+ return false;
68
+ }
69
+ this.close(session);
70
+ return true;
71
+ }
72
+ dispose() {
73
+ const seen = new Set();
74
+ for (const s of this.byExecId.values()) {
75
+ if (seen.has(s))
76
+ continue;
77
+ seen.add(s);
78
+ this.close(s);
79
+ }
80
+ this.byExecId.clear();
81
+ }
82
+ }
83
+ export const sessionManager = new SessionManager();
84
+ function installProcessCleanup() {
85
+ const dispose = () => {
86
+ try {
87
+ sessionManager.dispose();
88
+ }
89
+ catch {
90
+ /* ignore */
91
+ }
92
+ };
93
+ process.once("exit", dispose);
94
+ process.once("beforeExit", dispose);
95
+ }
96
+ installProcessCleanup();
@@ -0,0 +1,15 @@
1
+ export declare const CURSOR_AGENT_HOST = "agentn.global.api5.cursor.sh";
2
+ export declare const CURSOR_API_HOST = "api2.cursor.sh";
3
+ export declare const CURSOR_WEBSITE_HOST = "cursor.com";
4
+ export declare const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
5
+ export declare const CURSOR_PROVIDER_ID = "cursor";
6
+ export declare const TOKEN_EXPIRY_THRESHOLD_S = 300;
7
+ export declare const RUN_PATH = "/agent.v1.AgentService/Run";
8
+ export declare const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
9
+ export declare const MODEL_CACHE_FILE = "cursor-models.json";
10
+ export declare const MODEL_CACHE_TTL_MS = 86400000;
11
+ export declare const VERSION_CACHE_FILE = "cursor-client-version.json";
12
+ export declare const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
13
+ export declare const CONNECT_PROTOCOL_VERSION = "1";
14
+ export type ReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max";
15
+ export type ContextOption = "200k" | "272k" | "300k" | "1m";
package/dist/shared.js ADDED
@@ -0,0 +1,13 @@
1
+ export const CURSOR_AGENT_HOST = "agentn.global.api5.cursor.sh";
2
+ export const CURSOR_API_HOST = "api2.cursor.sh";
3
+ export const CURSOR_WEBSITE_HOST = "cursor.com";
4
+ export const FALLBACK_CLIENT_VERSION = "cli-2026.07.09-a3815c0";
5
+ export const CURSOR_PROVIDER_ID = "cursor";
6
+ export const TOKEN_EXPIRY_THRESHOLD_S = 300;
7
+ export const RUN_PATH = "/agent.v1.AgentService/Run";
8
+ export const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
9
+ export const MODEL_CACHE_FILE = "cursor-models.json";
10
+ export const MODEL_CACHE_TTL_MS = 86_400_000;
11
+ export const VERSION_CACHE_FILE = "cursor-client-version.json";
12
+ export const CONTENT_TYPE_CONNECT_PROTO = "application/connect+proto";
13
+ export const CONNECT_PROTOCOL_VERSION = "1";
@@ -0,0 +1,23 @@
1
+ export declare function trace(msg: string): void;
2
+ export declare function buildBaseHeaders(token: string, clientVersion: string, extra?: Record<string, string>): Record<string, string>;
3
+ export declare function unaryAvailableModels(token: string, options?: {
4
+ baseURL?: string;
5
+ headers?: Record<string, string>;
6
+ }): Promise<Record<string, unknown>>;
7
+ export type BidiStream = {
8
+ write(msg: Uint8Array): void;
9
+ end(): void;
10
+ frames(): AsyncIterable<{
11
+ flags: number;
12
+ payload: Uint8Array;
13
+ }>;
14
+ destroy(): void;
15
+ };
16
+ /** Resolve the HTTP/2 connect origin for a Run stream (exported for tests). */
17
+ export declare function resolveAgentOrigin(baseURL?: string): string;
18
+ export declare function bidiRunStream(token: string, options?: {
19
+ signal?: AbortSignal;
20
+ baseURL?: string;
21
+ headers?: Record<string, string>;
22
+ }): Promise<BidiStream>;
23
+ export declare function makeRequestId(): string;
@@ -0,0 +1,275 @@
1
+ import { CURSOR_API_HOST, CURSOR_AGENT_HOST, CONNECT_PROTOCOL_VERSION } from "../shared.js";
2
+ import { encodeFrame, streamFrames } from "../protocol/framing.js";
3
+ import { createCursorChecksumHeader } from "../protocol/checksum.js";
4
+ import { getDeviceIds } from "../protocol/device-id.js";
5
+ import { resolveClientVersion } from "../protocol/client-version.js";
6
+ import http2 from "node:http2";
7
+ import fs from "node:fs";
8
+ const API_BASE = `https://${CURSOR_API_HOST}`;
9
+ const AGENT = CURSOR_AGENT_HOST;
10
+ // Wire-level diagnostics. Opt in with CURSOR_PROVIDER_DEBUG=1 (or "true").
11
+ // Writes to CURSOR_PROVIDER_DEBUG_FILE (default /tmp/cursor-provider-debug.log).
12
+ // Truncated once per process. Captures h2 response status, parsed frames, and
13
+ // stream errors. Tokens / checksums are redacted in header dumps.
14
+ const DEBUG_ENABLED = process.env.CURSOR_PROVIDER_DEBUG === "1" ||
15
+ process.env.CURSOR_PROVIDER_DEBUG === "true";
16
+ const DEBUG_FILE = process.env.CURSOR_PROVIDER_DEBUG_FILE || "/tmp/cursor-provider-debug.log";
17
+ let _traceInitialized = false;
18
+ export function trace(msg) {
19
+ if (!DEBUG_ENABLED)
20
+ return;
21
+ try {
22
+ if (!_traceInitialized) {
23
+ _traceInitialized = true;
24
+ fs.writeFileSync(DEBUG_FILE, `--- cursor-provider debug (pid ${process.pid}) ${new Date().toISOString()} ---\n`);
25
+ }
26
+ fs.appendFileSync(DEBUG_FILE, `[${new Date().toISOString()}] ${msg}\n`);
27
+ }
28
+ catch { /* ignore */ }
29
+ }
30
+ trace("connect.ts module loaded");
31
+ export function buildBaseHeaders(token, clientVersion, extra) {
32
+ const { machineId, macMachineId } = getDeviceIds();
33
+ return {
34
+ authorization: `Bearer ${token}`,
35
+ "connect-protocol-version": CONNECT_PROTOCOL_VERSION,
36
+ "x-cursor-client-type": "cli",
37
+ "x-cursor-client-version": clientVersion,
38
+ "x-cursor-checksum": createCursorChecksumHeader(machineId, macMachineId),
39
+ "x-ghost-mode": "true",
40
+ "x-request-id": crypto.randomUUID(),
41
+ ...extra,
42
+ };
43
+ }
44
+ // ── Unary (AvailableModels) ──
45
+ export async function unaryAvailableModels(token, options = {}) {
46
+ const base = options.baseURL ?? API_BASE;
47
+ const url = `${base}/aiserver.v1.AiService/AvailableModels`;
48
+ const clientVersion = await resolveClientVersion();
49
+ const headers = buildBaseHeaders(token, clientVersion, options.headers);
50
+ const res = await fetch(url, {
51
+ method: "POST",
52
+ headers: {
53
+ ...headers,
54
+ "content-type": "application/json",
55
+ accept: "application/json",
56
+ },
57
+ body: "{}",
58
+ });
59
+ if (!res.ok) {
60
+ const text = await res.text().catch(() => "");
61
+ throw new Error(`AvailableModels failed: ${res.status} ${res.statusText}${text ? ` - ${text.slice(0, 200)}` : ""}`);
62
+ }
63
+ return (await res.json());
64
+ }
65
+ // Cache http2 sessions keyed by origin so a custom baseURL never reuses a
66
+ // connection opened to the default agent host — and vice versa.
67
+ const _http2Sessions = new Map();
68
+ // In-flight connects for the same origin share one Promise (avoid parallel races).
69
+ const _http2Connecting = new Map();
70
+ // Bound the time we'll wait for the initial connect. Without this, a dead or
71
+ // unreachable host (DNS failure, network partition) hangs the provider forever
72
+ // because the 'connect' / 'error' event may never fire.
73
+ const CONNECT_TIMEOUT_MS = 15_000;
74
+ /** Resolve the HTTP/2 connect origin for a Run stream (exported for tests). */
75
+ export function resolveAgentOrigin(baseURL) {
76
+ // baseURL may be https://host or https://host:port — http2.connect accepts both.
77
+ return (baseURL ? new URL(baseURL) : new URL(`https://${AGENT}`)).origin;
78
+ }
79
+ function dropSession(origin, session) {
80
+ if (_http2Sessions.get(origin) === session)
81
+ _http2Sessions.delete(origin);
82
+ }
83
+ function getSession(baseURL) {
84
+ const origin = resolveAgentOrigin(baseURL);
85
+ const existing = _http2Sessions.get(origin);
86
+ if (existing && !existing.destroyed && !existing.closed) {
87
+ return Promise.resolve(existing);
88
+ }
89
+ const inflight = _http2Connecting.get(origin);
90
+ if (inflight)
91
+ return inflight;
92
+ const promise = new Promise((resolve, reject) => {
93
+ const session = http2.connect(origin);
94
+ const cleanup = () => {
95
+ clearTimeout(timer);
96
+ session.removeListener("error", onError);
97
+ session.removeListener("connect", onConnect);
98
+ };
99
+ const onError = (err) => {
100
+ cleanup();
101
+ _http2Connecting.delete(origin);
102
+ dropSession(origin, session);
103
+ try {
104
+ session.destroy();
105
+ }
106
+ catch { /* ignore */ }
107
+ reject(err);
108
+ };
109
+ const onConnect = () => {
110
+ cleanup();
111
+ _http2Connecting.delete(origin);
112
+ // Future post-connect errors (GOAWAY, RST_STREAM) must invalidate the
113
+ // cache; otherwise subsequent getSession() calls reuse a degraded session.
114
+ const onCloseOrError = () => dropSession(origin, session);
115
+ session.once("close", onCloseOrError);
116
+ session.on("error", onCloseOrError);
117
+ _http2Sessions.set(origin, session);
118
+ resolve(session);
119
+ };
120
+ const timer = setTimeout(() => {
121
+ cleanup();
122
+ _http2Connecting.delete(origin);
123
+ dropSession(origin, session);
124
+ try {
125
+ session.destroy();
126
+ }
127
+ catch { /* ignore */ }
128
+ reject(new Error(`HTTP/2 connect to ${origin} timed out after ${CONNECT_TIMEOUT_MS}ms`));
129
+ }, CONNECT_TIMEOUT_MS);
130
+ session.on("error", onError);
131
+ session.on("connect", onConnect);
132
+ });
133
+ _http2Connecting.set(origin, promise);
134
+ return promise;
135
+ }
136
+ export async function bidiRunStream(token, options = {}) {
137
+ const [session, clientVersion] = await Promise.all([
138
+ getSession(options.baseURL),
139
+ resolveClientVersion(),
140
+ ]);
141
+ const headers = {
142
+ ...buildBaseHeaders(token, clientVersion, options.headers),
143
+ ":method": "POST",
144
+ ":path": "/agent.v1.AgentService/Run",
145
+ "content-type": "application/connect+proto",
146
+ "connect-accept-encoding": "gzip,br",
147
+ // The CLI's streaming interceptor sets this on the bidi Run stream
148
+ // (decompiled client.ts:1190). Without it Cursor may treat the stream as
149
+ // non-streaming.
150
+ "x-cursor-streaming": "true",
151
+ "user-agent": "connect-es/1.6.1",
152
+ };
153
+ const stream = session.request(headers, {
154
+ endStream: false,
155
+ });
156
+ let writable = true;
157
+ let closed = false;
158
+ let responseStatus = 0;
159
+ let responseHeaders = {};
160
+ let streamError = null;
161
+ // Capture the HTTP/2 response status/headers and any stream-level error.
162
+ // Without this, a non-200 or RST_STREAM surfaces as a silent clean end
163
+ // (frames() just stops) — which looks exactly like "no response, no error".
164
+ stream.on("response", (h) => {
165
+ responseHeaders = h;
166
+ responseStatus = h[":status"] !== undefined ? Number(h[":status"]) : 0;
167
+ trace(`h2 response: status=${responseStatus} headers=${JSON.stringify(stripPseudo(h))}`);
168
+ });
169
+ stream.on("error", (err) => {
170
+ streamError = err;
171
+ trace(`h2 stream error: ${err?.name}: ${err?.message}`);
172
+ });
173
+ stream.on("close", () => trace(`h2 stream closed (status=${responseStatus}, err=${streamError?.message ?? "none"})`));
174
+ if (options.signal) {
175
+ options.signal.addEventListener("abort", () => {
176
+ if (!closed) {
177
+ closed = true;
178
+ writable = false;
179
+ stream.close();
180
+ }
181
+ }, { once: true });
182
+ }
183
+ return {
184
+ write(msg) {
185
+ if (!writable)
186
+ throw new Error("Stream not writable");
187
+ const frame = encodeFrame(0x00, msg);
188
+ stream.write(frame);
189
+ },
190
+ end() {
191
+ writable = false;
192
+ stream.end();
193
+ },
194
+ async *frames() {
195
+ const buffer = [];
196
+ for await (const chunk of stream) {
197
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
198
+ if (closed)
199
+ return;
200
+ buffer.push(new Uint8Array(buf));
201
+ // Try to parse frames from accumulated data
202
+ const merged = mergeBuffers(buffer);
203
+ const parsed = Array.from(streamFrames(merged));
204
+ if (parsed.length > 0) {
205
+ const consumed = parsed.reduce((sum, f) => sum + 5 + f.payload.length, 0);
206
+ // Only clear if we fully consumed all pending data
207
+ if (consumed === merged.length) {
208
+ buffer.length = 0;
209
+ }
210
+ else {
211
+ buffer.length = 0;
212
+ buffer.push(merged.subarray(consumed));
213
+ }
214
+ for (const frame of parsed) {
215
+ trace(`frame yield: flags=0x${frame.flags.toString(16)} payload=${frame.payload.length}B`);
216
+ yield frame;
217
+ }
218
+ }
219
+ }
220
+ closed = true;
221
+ // Surface connection-level failures instead of ending silently. A
222
+ // non-200, a Connect error in the trailers, or an RST_STREAM would
223
+ // otherwise look like an empty successful response.
224
+ if (streamError)
225
+ throw streamError;
226
+ if (responseStatus !== 0 && responseStatus !== 200) {
227
+ throw new Error(`Cursor Run HTTP ${responseStatus} ${JSON.stringify(stripPseudo(responseHeaders))}`);
228
+ }
229
+ const grpcStatus = responseHeaders["grpc-status"];
230
+ if (grpcStatus !== undefined && String(grpcStatus) !== "0") {
231
+ throw new Error(`Cursor Run gRPC status ${grpcStatus}: ${responseHeaders["grpc-message"] ?? ""}`.trim());
232
+ }
233
+ },
234
+ destroy() {
235
+ if (!closed) {
236
+ closed = true;
237
+ writable = false;
238
+ stream.close();
239
+ }
240
+ },
241
+ };
242
+ }
243
+ function mergeBuffers(buffers) {
244
+ if (buffers.length === 0)
245
+ return new Uint8Array(0);
246
+ if (buffers.length === 1)
247
+ return buffers[0];
248
+ const total = buffers.reduce((s, b) => s + b.length, 0);
249
+ const merged = new Uint8Array(total);
250
+ let offset = 0;
251
+ for (const b of buffers) {
252
+ merged.set(b, offset);
253
+ offset += b.length;
254
+ }
255
+ return merged;
256
+ }
257
+ export function makeRequestId() {
258
+ return crypto.randomUUID();
259
+ }
260
+ function stripPseudo(h) {
261
+ const out = {};
262
+ for (const [k, v] of Object.entries(h)) {
263
+ if (k.startsWith(":")) {
264
+ if (k === ":status")
265
+ out[k] = v;
266
+ continue;
267
+ }
268
+ if (k === "authorization" || k === "x-cursor-checksum") {
269
+ out[k] = "<redacted>";
270
+ continue;
271
+ }
272
+ out[k] = v;
273
+ }
274
+ return out;
275
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "cursor-opencode-provider",
3
+ "version": "0.1.1",
4
+ "description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "oakimov",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/oakimov/cursor-opencode-provider.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/oakimov/cursor-opencode-provider/issues"
14
+ },
15
+ "homepage": "https://github.com/oakimov/cursor-opencode-provider#readme",
16
+ "keywords": [
17
+ "opencode",
18
+ "opencode-plugin",
19
+ "cursor",
20
+ "ai-sdk",
21
+ "provider",
22
+ "plugin"
23
+ ],
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./plugin": {
32
+ "types": "./dist/plugin.d.ts",
33
+ "import": "./dist/plugin.js"
34
+ },
35
+ "./plugin/v2": {
36
+ "types": "./dist/plugin-v2.d.ts",
37
+ "import": "./dist/plugin-v2.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "LICENSE",
43
+ "README.md"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsc",
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "bun test",
49
+ "test:watch": "bun test --watch",
50
+ "prepublishOnly": "bun run build"
51
+ },
52
+ "dependencies": {
53
+ "@ai-sdk/provider": "3.0.8",
54
+ "protobufjs": "^7.4.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@opencode-ai/plugin": "~1.17.13"
58
+ },
59
+ "devDependencies": {
60
+ "@opencode-ai/plugin": "~1.17.13",
61
+ "@tsconfig/node22": "^22.0.1",
62
+ "@types/node": "^22.15.3",
63
+ "typescript": "^5.8.3"
64
+ }
65
+ }