@playwo/opencode-cursor-oauth 0.0.0-dev.1b946f85e9b0 → 0.0.0-dev.2a59bf1639ea

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 (72) hide show
  1. package/README.md +19 -91
  2. package/dist/auth.js +1 -2
  3. package/dist/constants.d.ts +2 -0
  4. package/dist/constants.js +2 -0
  5. package/dist/cursor/bidi-session.d.ts +13 -0
  6. package/dist/cursor/bidi-session.js +149 -0
  7. package/dist/cursor/config.d.ts +4 -0
  8. package/dist/cursor/config.js +4 -0
  9. package/dist/cursor/connect-framing.d.ts +10 -0
  10. package/dist/cursor/connect-framing.js +80 -0
  11. package/dist/cursor/headers.d.ts +6 -0
  12. package/dist/cursor/headers.js +16 -0
  13. package/dist/cursor/index.d.ts +5 -0
  14. package/dist/cursor/index.js +5 -0
  15. package/dist/cursor/unary-rpc.d.ts +13 -0
  16. package/dist/cursor/unary-rpc.js +181 -0
  17. package/dist/index.d.ts +2 -14
  18. package/dist/index.js +2 -297
  19. package/dist/logger.js +7 -2
  20. package/dist/models.js +1 -23
  21. package/dist/openai/index.d.ts +3 -0
  22. package/dist/openai/index.js +3 -0
  23. package/dist/openai/messages.d.ts +39 -0
  24. package/dist/openai/messages.js +223 -0
  25. package/dist/openai/tools.d.ts +7 -0
  26. package/dist/openai/tools.js +58 -0
  27. package/dist/openai/types.d.ts +41 -0
  28. package/dist/openai/types.js +1 -0
  29. package/dist/plugin/cursor-auth-plugin.d.ts +3 -0
  30. package/dist/plugin/cursor-auth-plugin.js +140 -0
  31. package/dist/proto/agent_pb.js +637 -319
  32. package/dist/provider/index.d.ts +2 -0
  33. package/dist/provider/index.js +2 -0
  34. package/dist/provider/model-cost.d.ts +9 -0
  35. package/dist/provider/model-cost.js +206 -0
  36. package/dist/provider/models.d.ts +8 -0
  37. package/dist/provider/models.js +86 -0
  38. package/dist/proxy/bridge-non-streaming.d.ts +3 -0
  39. package/dist/proxy/bridge-non-streaming.js +119 -0
  40. package/dist/proxy/bridge-session.d.ts +5 -0
  41. package/dist/proxy/bridge-session.js +13 -0
  42. package/dist/proxy/bridge-streaming.d.ts +5 -0
  43. package/dist/proxy/bridge-streaming.js +311 -0
  44. package/dist/proxy/bridge.d.ts +3 -0
  45. package/dist/proxy/bridge.js +3 -0
  46. package/dist/proxy/chat-completion.d.ts +2 -0
  47. package/dist/proxy/chat-completion.js +113 -0
  48. package/dist/proxy/conversation-meta.d.ts +12 -0
  49. package/dist/proxy/conversation-meta.js +1 -0
  50. package/dist/proxy/conversation-state.d.ts +35 -0
  51. package/dist/proxy/conversation-state.js +95 -0
  52. package/dist/proxy/cursor-request.d.ts +5 -0
  53. package/dist/proxy/cursor-request.js +86 -0
  54. package/dist/proxy/index.d.ts +12 -0
  55. package/dist/proxy/index.js +12 -0
  56. package/dist/proxy/server.d.ts +6 -0
  57. package/dist/proxy/server.js +89 -0
  58. package/dist/proxy/sse.d.ts +5 -0
  59. package/dist/proxy/sse.js +5 -0
  60. package/dist/proxy/state-sync.d.ts +2 -0
  61. package/dist/proxy/state-sync.js +17 -0
  62. package/dist/proxy/stream-dispatch.d.ts +42 -0
  63. package/dist/proxy/stream-dispatch.js +491 -0
  64. package/dist/proxy/stream-state.d.ts +9 -0
  65. package/dist/proxy/stream-state.js +1 -0
  66. package/dist/proxy/title.d.ts +1 -0
  67. package/dist/proxy/title.js +103 -0
  68. package/dist/proxy/types.d.ts +27 -0
  69. package/dist/proxy/types.js +1 -0
  70. package/dist/proxy.d.ts +2 -20
  71. package/dist/proxy.js +2 -1385
  72. package/package.json +1 -1
package/README.md CHANGED
@@ -1,103 +1,31 @@
1
- # @playwo/opencode-cursor-oauth
1
+ # opencode-cursor-oauth
2
2
 
3
- OpenCode plugin that connects to Cursor's API, giving you access to Cursor
4
- models inside OpenCode with full tool-calling support.
3
+ Use Cursor models (Claude, GPT, Gemini, etc.) inside [OpenCode](https://opencode.ai).
5
4
 
6
- ## Install in OpenCode
5
+ ## What it does
7
6
 
8
- Add this to `~/.config/opencode/opencode.json`:
7
+ - **OAuth login** to Cursor via browser
8
+ - **Model discovery** — automatically fetches your available Cursor models
9
+ - **Local proxy** — runs an OpenAI-compatible endpoint that translates to Cursor's gRPC protocol
10
+ - **Auto-refresh** — handles token expiration automatically
9
11
 
10
- ```jsonc
11
- {
12
- "$schema": "https://opencode.ai/config.json",
13
- "plugin": [
14
- "@playwo/opencode-cursor-oauth"
15
- ],
16
- "provider": {
17
- "cursor": {
18
- "name": "Cursor"
19
- }
20
- }
21
- }
22
- ```
23
-
24
- The `cursor` provider stub is required because OpenCode drops providers that do
25
- not already exist in its bundled provider catalog.
26
-
27
- OpenCode installs npm plugins automatically at startup, so users do not need to
28
- clone this repository.
29
-
30
- ## Authenticate
31
-
32
- ```sh
33
- opencode auth login --provider cursor
34
- ```
35
-
36
- This opens Cursor OAuth in the browser. Tokens are stored in
37
- `~/.local/share/opencode/auth.json` and refreshed automatically.
38
-
39
- ## Use
40
-
41
- Start OpenCode and select any Cursor model. The plugin starts a local
42
- OpenAI-compatible proxy on demand and routes requests through Cursor's gRPC API.
43
-
44
- ## How it works
45
-
46
- 1. OAuth — browser-based login to Cursor via PKCE.
47
- 2. Model discovery — queries Cursor's gRPC API for all available models; if discovery fails, the plugin disables the Cursor provider for that load and shows a visible error toast instead of crashing OpenCode.
48
- 3. Local proxy — translates `POST /v1/chat/completions` into Cursor's
49
- protobuf/Connect protocol.
50
- 4. Native tool routing — rejects Cursor's built-in filesystem/shell tools and
51
- exposes OpenCode's tool surface via Cursor MCP instead.
52
-
53
- Cursor agent streaming uses Cursor's `RunSSE` + `BidiAppend` transport, so the
54
- plugin runs entirely inside OpenCode without a Node sidecar.
12
+ ## Install
55
13
 
56
- ## Architecture
14
+ Add to your `opencode.json`:
57
15
 
16
+ ```json
17
+ {
18
+ "plugin": ["@playwo/opencode-cursor-oauth"]
19
+ }
58
20
  ```
59
- OpenCode --> /v1/chat/completions --> Bun.serve (proxy)
60
- |
61
- RunSSE stream + BidiAppend writes
62
- |
63
- Cursor Connect/SSE transport
64
- |
65
- api2.cursor.sh gRPC
66
- ```
67
-
68
- ### Tool call flow
69
-
70
- ```
71
- 1. Cursor model receives OpenAI tools via RequestContext (as MCP tool defs)
72
- 2. Model tries native tools (readArgs, shellArgs, etc.)
73
- 3. Proxy rejects each with typed error (ReadRejected, ShellRejected, etc.)
74
- 4. Model falls back to MCP tool -> mcpArgs exec message
75
- 5. Proxy emits OpenAI tool_calls SSE chunk, pauses the Cursor stream
76
- 6. OpenCode executes tool, sends result in follow-up request
77
- 7. Proxy resumes the Cursor stream with mcpResult and continues streaming
78
- ```
79
-
80
- ## Develop locally
81
-
82
- ```sh
83
- bun install
84
- bun run build
85
- bun test/smoke.ts
86
- ```
87
-
88
- ## Publish
89
21
 
90
- GitHub Actions publishes this package with `.github/workflows/publish-npm.yml`.
22
+ Then authenticate via the OpenCode UI (Settings → Providers → Cursor → Login).
91
23
 
92
- - branch pushes publish a `dev` build as `0.0.0-dev.<sha>`
93
- - versioned releases publish `latest` using the `package.json` version and upload the packed `.tgz` to the GitHub release
94
-
95
- Repository secrets required:
24
+ ## Requirements
96
25
 
97
- - `NPM_TOKEN` for npm publish access
26
+ - Cursor account with API access
27
+ - OpenCode 1.2+
98
28
 
99
- ## Requirements
29
+ ## License
100
30
 
101
- - [OpenCode](https://opencode.ai)
102
- - [Bun](https://bun.sh)
103
- - Active [Cursor](https://cursor.com) subscription
31
+ MIT
package/dist/auth.js CHANGED
@@ -111,7 +111,6 @@ export function getTokenExpiry(token) {
111
111
  return decoded.exp * 1000 - 5 * 60 * 1000;
112
112
  }
113
113
  }
114
- catch {
115
- }
114
+ catch { }
116
115
  return Date.now() + 3600 * 1000;
117
116
  }
@@ -0,0 +1,2 @@
1
+ export declare const CURSOR_PROVIDER_ID: "cursor";
2
+ export declare const OPENCODE_TITLE_REQUEST_MARKER = "Generate a title for this conversation:";
@@ -0,0 +1,2 @@
1
+ export const CURSOR_PROVIDER_ID = "cursor";
2
+ export const OPENCODE_TITLE_REQUEST_MARKER = "Generate a title for this conversation:";
@@ -0,0 +1,13 @@
1
+ import { type CursorBaseRequestOptions } from "./headers";
2
+ export declare function encodeBidiAppendRequest(dataHex: string, requestId: string, appendSeqno: number): Uint8Array;
3
+ export interface CursorSession {
4
+ write: (data: Uint8Array) => void;
5
+ end: () => void;
6
+ onData: (cb: (chunk: Buffer) => void) => void;
7
+ onClose: (cb: (code: number) => void) => void;
8
+ readonly alive: boolean;
9
+ }
10
+ export interface CreateCursorSessionOptions extends CursorBaseRequestOptions {
11
+ requestId: string;
12
+ }
13
+ export declare function createCursorSession(options: CreateCursorSessionOptions): Promise<CursorSession>;
@@ -0,0 +1,149 @@
1
+ import { create, toBinary } from "@bufbuild/protobuf";
2
+ import { BidiRequestIdSchema } from "../proto/agent_pb";
3
+ import { CURSOR_API_URL } from "./config";
4
+ import { concatBytes, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, frameConnectMessage, toFetchBody, } from "./connect-framing";
5
+ import { buildCursorHeaders } from "./headers";
6
+ import { errorDetails, logPluginError, logPluginWarn } from "../logger";
7
+ export function encodeBidiAppendRequest(dataHex, requestId, appendSeqno) {
8
+ const requestIdBytes = toBinary(BidiRequestIdSchema, create(BidiRequestIdSchema, { requestId }));
9
+ return concatBytes([
10
+ encodeProtoStringField(1, dataHex),
11
+ encodeProtoMessageField(2, requestIdBytes),
12
+ encodeProtoVarintField(3, appendSeqno),
13
+ ]);
14
+ }
15
+ export async function createCursorSession(options) {
16
+ const response = await fetch(new URL("/agent.v1.AgentService/RunSSE", options.url ?? CURSOR_API_URL), {
17
+ method: "POST",
18
+ headers: buildCursorHeaders(options, "application/connect+proto", {
19
+ accept: "text/event-stream",
20
+ "connect-protocol-version": "1",
21
+ }),
22
+ body: toFetchBody(frameConnectMessage(toBinary(BidiRequestIdSchema, create(BidiRequestIdSchema, { requestId: options.requestId })))),
23
+ });
24
+ if (!response.ok || !response.body) {
25
+ const errorBody = await response.text().catch(() => "");
26
+ logPluginError("Cursor RunSSE request failed", {
27
+ requestId: options.requestId,
28
+ status: response.status,
29
+ responseBody: errorBody,
30
+ });
31
+ throw new Error(`RunSSE failed: ${response.status}${errorBody ? ` ${errorBody}` : ""}`);
32
+ }
33
+ const cbs = {
34
+ data: null,
35
+ close: null,
36
+ };
37
+ const abortController = new AbortController();
38
+ const reader = response.body.getReader();
39
+ let appendSeqno = 0;
40
+ let alive = true;
41
+ let closeCode = 0;
42
+ let writeChain = Promise.resolve();
43
+ const pendingChunks = [];
44
+ const finish = (code) => {
45
+ if (!alive)
46
+ return;
47
+ alive = false;
48
+ closeCode = code;
49
+ cbs.close?.(code);
50
+ };
51
+ const append = async (data) => {
52
+ const requestBody = encodeBidiAppendRequest(Buffer.from(data).toString("hex"), options.requestId, appendSeqno++);
53
+ const appendResponse = await fetch(new URL("/aiserver.v1.BidiService/BidiAppend", options.url ?? CURSOR_API_URL), {
54
+ method: "POST",
55
+ headers: buildCursorHeaders(options, "application/proto"),
56
+ body: toFetchBody(requestBody),
57
+ signal: abortController.signal,
58
+ });
59
+ if (!appendResponse.ok) {
60
+ const errorBody = await appendResponse.text().catch(() => "");
61
+ logPluginError("Cursor BidiAppend request failed", {
62
+ requestId: options.requestId,
63
+ appendSeqno: appendSeqno - 1,
64
+ status: appendResponse.status,
65
+ responseBody: errorBody,
66
+ });
67
+ throw new Error(`BidiAppend failed: ${appendResponse.status}${errorBody ? ` ${errorBody}` : ""}`);
68
+ }
69
+ await appendResponse.arrayBuffer().catch(() => undefined);
70
+ };
71
+ (async () => {
72
+ try {
73
+ while (true) {
74
+ const { done, value } = await reader.read();
75
+ if (done) {
76
+ finish(0);
77
+ break;
78
+ }
79
+ if (value && value.length > 0) {
80
+ const chunk = Buffer.from(value);
81
+ if (cbs.data) {
82
+ cbs.data(chunk);
83
+ }
84
+ else {
85
+ pendingChunks.push(chunk);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ catch (error) {
91
+ logPluginWarn("Cursor stream reader closed with error", {
92
+ requestId: options.requestId,
93
+ ...errorDetails(error),
94
+ });
95
+ finish(alive ? 1 : closeCode);
96
+ }
97
+ })();
98
+ return {
99
+ get alive() {
100
+ return alive;
101
+ },
102
+ write(data) {
103
+ if (!alive)
104
+ return;
105
+ writeChain = writeChain
106
+ .then(() => append(data))
107
+ .catch((error) => {
108
+ logPluginError("Cursor stream append failed", {
109
+ requestId: options.requestId,
110
+ ...errorDetails(error),
111
+ });
112
+ try {
113
+ abortController.abort();
114
+ }
115
+ catch { }
116
+ try {
117
+ reader.cancel();
118
+ }
119
+ catch { }
120
+ finish(1);
121
+ });
122
+ },
123
+ end() {
124
+ try {
125
+ abortController.abort();
126
+ }
127
+ catch { }
128
+ try {
129
+ reader.cancel();
130
+ }
131
+ catch { }
132
+ finish(0);
133
+ },
134
+ onData(cb) {
135
+ cbs.data = cb;
136
+ while (pendingChunks.length > 0) {
137
+ cb(pendingChunks.shift());
138
+ }
139
+ },
140
+ onClose(cb) {
141
+ if (!alive) {
142
+ queueMicrotask(() => cb(closeCode));
143
+ }
144
+ else {
145
+ cbs.close = cb;
146
+ }
147
+ },
148
+ };
149
+ }
@@ -0,0 +1,4 @@
1
+ export declare const CURSOR_API_URL: string;
2
+ export declare const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
3
+ export declare const CURSOR_CONNECT_PROTOCOL_VERSION = "1";
4
+ export declare const CONNECT_END_STREAM_FLAG = 2;
@@ -0,0 +1,4 @@
1
+ export const CURSOR_API_URL = process.env.CURSOR_API_URL ?? "https://api2.cursor.sh";
2
+ export const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
3
+ export const CURSOR_CONNECT_PROTOCOL_VERSION = "1";
4
+ export const CONNECT_END_STREAM_FLAG = 0b00000010;
@@ -0,0 +1,10 @@
1
+ /** Connect protocol frame: [1-byte flags][4-byte BE length][payload] */
2
+ export declare function frameConnectMessage(data: Uint8Array, flags?: number): Buffer;
3
+ export declare function decodeConnectUnaryBody(payload: Uint8Array): Uint8Array | null;
4
+ export declare function encodeVarint(value: number): Uint8Array;
5
+ export declare function encodeProtoField(tag: number, wireType: number, value: Uint8Array): Uint8Array;
6
+ export declare function encodeProtoStringField(tag: number, value: string): Uint8Array;
7
+ export declare function encodeProtoMessageField(tag: number, value: Uint8Array): Uint8Array;
8
+ export declare function encodeProtoVarintField(tag: number, value: number): Uint8Array;
9
+ export declare function concatBytes(parts: Uint8Array[]): Uint8Array;
10
+ export declare function toFetchBody(data: Uint8Array): ArrayBuffer;
@@ -0,0 +1,80 @@
1
+ import { CONNECT_END_STREAM_FLAG } from "./config";
2
+ /** Connect protocol frame: [1-byte flags][4-byte BE length][payload] */
3
+ export function frameConnectMessage(data, flags = 0) {
4
+ const frame = Buffer.alloc(5 + data.length);
5
+ frame[0] = flags;
6
+ frame.writeUInt32BE(data.length, 1);
7
+ frame.set(data, 5);
8
+ return frame;
9
+ }
10
+ export function decodeConnectUnaryBody(payload) {
11
+ if (payload.length < 5)
12
+ return null;
13
+ let offset = 0;
14
+ while (offset + 5 <= payload.length) {
15
+ const flags = payload[offset];
16
+ const view = new DataView(payload.buffer, payload.byteOffset + offset, payload.byteLength - offset);
17
+ const messageLength = view.getUint32(1, false);
18
+ const frameEnd = offset + 5 + messageLength;
19
+ if (frameEnd > payload.length)
20
+ return null;
21
+ if ((flags & 0b0000_0001) !== 0)
22
+ return null;
23
+ if ((flags & CONNECT_END_STREAM_FLAG) === 0) {
24
+ return payload.subarray(offset + 5, frameEnd);
25
+ }
26
+ offset = frameEnd;
27
+ }
28
+ return null;
29
+ }
30
+ export function encodeVarint(value) {
31
+ if (!Number.isSafeInteger(value) || value < 0) {
32
+ throw new Error(`Unsupported varint value: ${value}`);
33
+ }
34
+ const bytes = [];
35
+ let current = value;
36
+ while (current >= 0x80) {
37
+ bytes.push((current & 0x7f) | 0x80);
38
+ current = Math.floor(current / 128);
39
+ }
40
+ bytes.push(current);
41
+ return Uint8Array.from(bytes);
42
+ }
43
+ export function encodeProtoField(tag, wireType, value) {
44
+ const key = encodeVarint((tag << 3) | wireType);
45
+ const out = new Uint8Array(key.length + value.length);
46
+ out.set(key, 0);
47
+ out.set(value, key.length);
48
+ return out;
49
+ }
50
+ export function encodeProtoStringField(tag, value) {
51
+ const bytes = new TextEncoder().encode(value);
52
+ const len = encodeVarint(bytes.length);
53
+ const payload = new Uint8Array(len.length + bytes.length);
54
+ payload.set(len, 0);
55
+ payload.set(bytes, len.length);
56
+ return encodeProtoField(tag, 2, payload);
57
+ }
58
+ export function encodeProtoMessageField(tag, value) {
59
+ const len = encodeVarint(value.length);
60
+ const payload = new Uint8Array(len.length + value.length);
61
+ payload.set(len, 0);
62
+ payload.set(value, len.length);
63
+ return encodeProtoField(tag, 2, payload);
64
+ }
65
+ export function encodeProtoVarintField(tag, value) {
66
+ return encodeProtoField(tag, 0, encodeVarint(value));
67
+ }
68
+ export function concatBytes(parts) {
69
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
70
+ const out = new Uint8Array(total);
71
+ let offset = 0;
72
+ for (const part of parts) {
73
+ out.set(part, offset);
74
+ offset += part.length;
75
+ }
76
+ return out;
77
+ }
78
+ export function toFetchBody(data) {
79
+ return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
80
+ }
@@ -0,0 +1,6 @@
1
+ export interface CursorBaseRequestOptions {
2
+ accessToken: string;
3
+ url?: string;
4
+ }
5
+ export declare function buildCursorHeaders(options: CursorBaseRequestOptions, contentType: string, extra?: Record<string, string>): Headers;
6
+ export declare function buildCursorHeaderValues(options: CursorBaseRequestOptions, contentType: string, extra?: Record<string, string>): Record<string, string>;
@@ -0,0 +1,16 @@
1
+ import { CURSOR_CLIENT_VERSION } from "./config";
2
+ export function buildCursorHeaders(options, contentType, extra = {}) {
3
+ const headers = new Headers(buildCursorHeaderValues(options, contentType, extra));
4
+ return headers;
5
+ }
6
+ export function buildCursorHeaderValues(options, contentType, extra = {}) {
7
+ return {
8
+ authorization: `Bearer ${options.accessToken}`,
9
+ "content-type": contentType,
10
+ "x-ghost-mode": "true",
11
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
12
+ "x-cursor-client-type": "cli",
13
+ "x-request-id": crypto.randomUUID(),
14
+ ...extra,
15
+ };
16
+ }
@@ -0,0 +1,5 @@
1
+ export { CURSOR_API_URL, CURSOR_CLIENT_VERSION, CURSOR_CONNECT_PROTOCOL_VERSION, CONNECT_END_STREAM_FLAG, } from "./config";
2
+ export { concatBytes, decodeConnectUnaryBody, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, encodeVarint, frameConnectMessage, toFetchBody, } from "./connect-framing";
3
+ export { buildCursorHeaders, buildCursorHeaderValues, type CursorBaseRequestOptions, } from "./headers";
4
+ export { createCursorSession, encodeBidiAppendRequest, type CreateCursorSessionOptions, type CursorSession, } from "./bidi-session";
5
+ export { callCursorUnaryRpc, type CursorUnaryRpcOptions } from "./unary-rpc";
@@ -0,0 +1,5 @@
1
+ export { CURSOR_API_URL, CURSOR_CLIENT_VERSION, CURSOR_CONNECT_PROTOCOL_VERSION, CONNECT_END_STREAM_FLAG, } from "./config";
2
+ export { concatBytes, decodeConnectUnaryBody, encodeProtoMessageField, encodeProtoStringField, encodeProtoVarintField, encodeVarint, frameConnectMessage, toFetchBody, } from "./connect-framing";
3
+ export { buildCursorHeaders, buildCursorHeaderValues, } from "./headers";
4
+ export { createCursorSession, encodeBidiAppendRequest, } from "./bidi-session";
5
+ export { callCursorUnaryRpc } from "./unary-rpc";
@@ -0,0 +1,13 @@
1
+ export interface CursorUnaryRpcOptions {
2
+ accessToken: string;
3
+ rpcPath: string;
4
+ requestBody: Uint8Array;
5
+ url?: string;
6
+ timeoutMs?: number;
7
+ transport?: "auto" | "fetch" | "http2";
8
+ }
9
+ export declare function callCursorUnaryRpc(options: CursorUnaryRpcOptions): Promise<{
10
+ body: Uint8Array;
11
+ exitCode: number;
12
+ timedOut: boolean;
13
+ }>;
@@ -0,0 +1,181 @@
1
+ import { connect as connectHttp2, } from "node:http2";
2
+ import { CURSOR_API_URL, CURSOR_CONNECT_PROTOCOL_VERSION } from "./config";
3
+ import { toFetchBody } from "./connect-framing";
4
+ import { buildCursorHeaders, buildCursorHeaderValues } from "./headers";
5
+ import { errorDetails, logPluginError } from "../logger";
6
+ export async function callCursorUnaryRpc(options) {
7
+ const target = new URL(options.rpcPath, options.url ?? CURSOR_API_URL);
8
+ const transport = options.transport ?? "auto";
9
+ if (transport === "http2" ||
10
+ (transport === "auto" && target.protocol === "https:")) {
11
+ const http2Result = await callCursorUnaryRpcOverHttp2(options, target);
12
+ if (transport === "http2" ||
13
+ http2Result.timedOut ||
14
+ http2Result.exitCode !== 1) {
15
+ return http2Result;
16
+ }
17
+ }
18
+ return callCursorUnaryRpcOverFetch(options, target);
19
+ }
20
+ async function callCursorUnaryRpcOverFetch(options, target) {
21
+ let timedOut = false;
22
+ const timeoutMs = options.timeoutMs ?? 5_000;
23
+ const controller = new AbortController();
24
+ const timeout = timeoutMs > 0
25
+ ? setTimeout(() => {
26
+ timedOut = true;
27
+ controller.abort();
28
+ }, timeoutMs)
29
+ : undefined;
30
+ try {
31
+ const response = await fetch(target, {
32
+ method: "POST",
33
+ headers: buildCursorHeaders(options, "application/proto", {
34
+ accept: "application/proto, application/json",
35
+ "connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
36
+ "connect-timeout-ms": String(timeoutMs),
37
+ }),
38
+ body: toFetchBody(options.requestBody),
39
+ signal: controller.signal,
40
+ });
41
+ const body = new Uint8Array(await response.arrayBuffer());
42
+ return {
43
+ body,
44
+ exitCode: response.ok ? 0 : response.status,
45
+ timedOut,
46
+ };
47
+ }
48
+ catch {
49
+ logPluginError("Cursor unary fetch transport failed", {
50
+ rpcPath: options.rpcPath,
51
+ url: target.toString(),
52
+ timeoutMs,
53
+ timedOut,
54
+ });
55
+ return {
56
+ body: new Uint8Array(),
57
+ exitCode: timedOut ? 124 : 1,
58
+ timedOut,
59
+ };
60
+ }
61
+ finally {
62
+ if (timeout)
63
+ clearTimeout(timeout);
64
+ }
65
+ }
66
+ async function callCursorUnaryRpcOverHttp2(options, target) {
67
+ const timeoutMs = options.timeoutMs ?? 5_000;
68
+ const authority = `${target.protocol}//${target.host}`;
69
+ return new Promise((resolve) => {
70
+ let settled = false;
71
+ let timedOut = false;
72
+ let session;
73
+ let stream;
74
+ const finish = (result) => {
75
+ if (settled)
76
+ return;
77
+ settled = true;
78
+ if (timeout)
79
+ clearTimeout(timeout);
80
+ try {
81
+ stream?.close();
82
+ }
83
+ catch { }
84
+ try {
85
+ session?.close();
86
+ }
87
+ catch { }
88
+ resolve(result);
89
+ };
90
+ const timeout = timeoutMs > 0
91
+ ? setTimeout(() => {
92
+ timedOut = true;
93
+ finish({
94
+ body: new Uint8Array(),
95
+ exitCode: 124,
96
+ timedOut: true,
97
+ });
98
+ }, timeoutMs)
99
+ : undefined;
100
+ try {
101
+ session = connectHttp2(authority);
102
+ session.once("error", (error) => {
103
+ logPluginError("Cursor unary HTTP/2 session failed", {
104
+ rpcPath: options.rpcPath,
105
+ url: target.toString(),
106
+ timedOut,
107
+ ...errorDetails(error),
108
+ });
109
+ finish({
110
+ body: new Uint8Array(),
111
+ exitCode: timedOut ? 124 : 1,
112
+ timedOut,
113
+ });
114
+ });
115
+ const headers = {
116
+ ":method": "POST",
117
+ ":path": `${target.pathname}${target.search}`,
118
+ ...buildCursorHeaderValues(options, "application/proto", {
119
+ accept: "application/proto, application/json",
120
+ "connect-protocol-version": CURSOR_CONNECT_PROTOCOL_VERSION,
121
+ "connect-timeout-ms": String(timeoutMs),
122
+ }),
123
+ };
124
+ stream = session.request(headers);
125
+ let statusCode = 0;
126
+ const chunks = [];
127
+ stream.once("response", (responseHeaders) => {
128
+ const statusHeader = responseHeaders[":status"];
129
+ statusCode =
130
+ typeof statusHeader === "number"
131
+ ? statusHeader
132
+ : Number(statusHeader ?? 0);
133
+ });
134
+ stream.on("data", (chunk) => {
135
+ chunks.push(Buffer.from(chunk));
136
+ });
137
+ stream.once("end", () => {
138
+ const body = new Uint8Array(Buffer.concat(chunks));
139
+ finish({
140
+ body,
141
+ exitCode: statusCode >= 200 && statusCode < 300 ? 0 : statusCode || 1,
142
+ timedOut,
143
+ });
144
+ });
145
+ stream.once("error", (error) => {
146
+ logPluginError("Cursor unary HTTP/2 stream failed", {
147
+ rpcPath: options.rpcPath,
148
+ url: target.toString(),
149
+ timedOut,
150
+ ...errorDetails(error),
151
+ });
152
+ finish({
153
+ body: new Uint8Array(),
154
+ exitCode: timedOut ? 124 : 1,
155
+ timedOut,
156
+ });
157
+ });
158
+ // Bun's node:http2 client currently breaks on end(Buffer.alloc(0)) against
159
+ // Cursor's HTTPS endpoint, but a header-only end() succeeds for empty unary bodies.
160
+ if (options.requestBody.length > 0) {
161
+ stream.end(Buffer.from(options.requestBody));
162
+ }
163
+ else {
164
+ stream.end();
165
+ }
166
+ }
167
+ catch (error) {
168
+ logPluginError("Cursor unary HTTP/2 setup failed", {
169
+ rpcPath: options.rpcPath,
170
+ url: target.toString(),
171
+ timedOut,
172
+ ...errorDetails(error),
173
+ });
174
+ finish({
175
+ body: new Uint8Array(),
176
+ exitCode: timedOut ? 124 : 1,
177
+ timedOut,
178
+ });
179
+ }
180
+ });
181
+ }
package/dist/index.d.ts CHANGED
@@ -1,14 +1,2 @@
1
- /**
2
- * OpenCode Cursor Auth Plugin
3
- *
4
- * Enables using Cursor models (Claude, GPT, etc.) inside OpenCode via:
5
- * 1. Browser-based OAuth login to Cursor
6
- * 2. Local proxy translating OpenAI format → Cursor gRPC protocol
7
- */
8
- import type { Plugin } from "@opencode-ai/plugin";
9
- /**
10
- * OpenCode plugin that provides Cursor authentication and model access.
11
- * Register in opencode.json: { "plugin": ["opencode-cursor-oauth"] }
12
- */
13
- export declare const CursorAuthPlugin: Plugin;
14
- export default CursorAuthPlugin;
1
+ export { CursorAuthPlugin } from "./plugin/cursor-auth-plugin";
2
+ export { CursorAuthPlugin as default } from "./plugin/cursor-auth-plugin";