@playwo/opencode-cursor-oauth 0.0.0-dev.a9d6c62f0dd9 → 0.0.0-dev.be56e19110ad
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/README.md +19 -91
- package/dist/auth.js +27 -3
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/cursor/bidi-session.d.ts +13 -0
- package/dist/cursor/bidi-session.js +149 -0
- package/dist/cursor/config.d.ts +4 -0
- package/dist/cursor/config.js +4 -0
- package/dist/cursor/connect-framing.d.ts +10 -0
- package/dist/cursor/connect-framing.js +80 -0
- package/dist/cursor/headers.d.ts +6 -0
- package/dist/cursor/headers.js +16 -0
- package/dist/cursor/index.d.ts +5 -0
- package/dist/cursor/index.js +5 -0
- package/dist/cursor/unary-rpc.d.ts +13 -0
- package/dist/cursor/unary-rpc.js +181 -0
- package/dist/index.d.ts +2 -14
- package/dist/index.js +2 -229
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +147 -0
- package/dist/models.js +27 -25
- package/dist/openai/index.d.ts +3 -0
- package/dist/openai/index.js +3 -0
- package/dist/openai/messages.d.ts +39 -0
- package/dist/openai/messages.js +228 -0
- package/dist/openai/tools.d.ts +7 -0
- package/dist/openai/tools.js +58 -0
- package/dist/openai/types.d.ts +41 -0
- package/dist/openai/types.js +1 -0
- package/dist/plugin/cursor-auth-plugin.d.ts +3 -0
- package/dist/plugin/cursor-auth-plugin.js +140 -0
- package/dist/proto/agent_pb.js +637 -319
- package/dist/provider/index.d.ts +2 -0
- package/dist/provider/index.js +2 -0
- package/dist/provider/model-cost.d.ts +9 -0
- package/dist/provider/model-cost.js +206 -0
- package/dist/provider/models.d.ts +8 -0
- package/dist/provider/models.js +86 -0
- package/dist/proxy/bridge-non-streaming.d.ts +3 -0
- package/dist/proxy/bridge-non-streaming.js +119 -0
- package/dist/proxy/bridge-session.d.ts +5 -0
- package/dist/proxy/bridge-session.js +13 -0
- package/dist/proxy/bridge-streaming.d.ts +5 -0
- package/dist/proxy/bridge-streaming.js +311 -0
- package/dist/proxy/bridge.d.ts +3 -0
- package/dist/proxy/bridge.js +3 -0
- package/dist/proxy/chat-completion.d.ts +2 -0
- package/dist/proxy/chat-completion.js +114 -0
- package/dist/proxy/conversation-meta.d.ts +12 -0
- package/dist/proxy/conversation-meta.js +1 -0
- package/dist/proxy/conversation-state.d.ts +35 -0
- package/dist/proxy/conversation-state.js +95 -0
- package/dist/proxy/cursor-request.d.ts +6 -0
- package/dist/proxy/cursor-request.js +104 -0
- package/dist/proxy/index.d.ts +12 -0
- package/dist/proxy/index.js +12 -0
- package/dist/proxy/server.d.ts +6 -0
- package/dist/proxy/server.js +109 -0
- package/dist/proxy/sse.d.ts +5 -0
- package/dist/proxy/sse.js +5 -0
- package/dist/proxy/state-sync.d.ts +2 -0
- package/dist/proxy/state-sync.js +17 -0
- package/dist/proxy/stream-dispatch.d.ts +42 -0
- package/dist/proxy/stream-dispatch.js +491 -0
- package/dist/proxy/stream-state.d.ts +9 -0
- package/dist/proxy/stream-state.js +1 -0
- package/dist/proxy/title.d.ts +1 -0
- package/dist/proxy/title.js +103 -0
- package/dist/proxy/types.d.ts +27 -0
- package/dist/proxy/types.js +1 -0
- package/dist/proxy.d.ts +2 -19
- package/dist/proxy.js +2 -1221
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,103 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-cursor-oauth
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## What it does
|
|
7
6
|
|
|
8
|
-
|
|
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
|
-
|
|
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 and fails plugin loading visibly if discovery does not succeed.
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
Then authenticate via the OpenCode UI (Settings → Providers → Cursor → Login).
|
|
91
23
|
|
|
92
|
-
|
|
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
|
-
-
|
|
26
|
+
- Cursor account with API access
|
|
27
|
+
- OpenCode 1.2+
|
|
98
28
|
|
|
99
|
-
##
|
|
29
|
+
## License
|
|
100
30
|
|
|
101
|
-
|
|
102
|
-
- [Bun](https://bun.sh)
|
|
103
|
-
- Active [Cursor](https://cursor.com) subscription
|
|
31
|
+
MIT
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generatePKCE } from "./pkce";
|
|
2
|
+
import { errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
2
3
|
const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
|
|
3
4
|
const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
|
|
4
5
|
const CURSOR_REFRESH_URL = process.env.CURSOR_REFRESH_URL ??
|
|
@@ -40,13 +41,32 @@ export async function pollCursorAuth(uuid, verifier) {
|
|
|
40
41
|
}
|
|
41
42
|
throw new Error(`Poll failed: ${response.status}`);
|
|
42
43
|
}
|
|
43
|
-
catch {
|
|
44
|
+
catch (error) {
|
|
44
45
|
consecutiveErrors++;
|
|
45
46
|
if (consecutiveErrors >= 3) {
|
|
47
|
+
logPluginError("Cursor auth polling failed repeatedly", {
|
|
48
|
+
stage: "oauth_poll",
|
|
49
|
+
uuid,
|
|
50
|
+
attempts: attempt + 1,
|
|
51
|
+
consecutiveErrors,
|
|
52
|
+
...errorDetails(error),
|
|
53
|
+
});
|
|
46
54
|
throw new Error("Too many consecutive errors during Cursor auth polling");
|
|
47
55
|
}
|
|
56
|
+
logPluginWarn("Cursor auth polling attempt failed", {
|
|
57
|
+
stage: "oauth_poll",
|
|
58
|
+
uuid,
|
|
59
|
+
attempt: attempt + 1,
|
|
60
|
+
consecutiveErrors,
|
|
61
|
+
...errorDetails(error),
|
|
62
|
+
});
|
|
48
63
|
}
|
|
49
64
|
}
|
|
65
|
+
logPluginError("Cursor authentication polling timed out", {
|
|
66
|
+
stage: "oauth_poll",
|
|
67
|
+
uuid,
|
|
68
|
+
attempts: POLL_MAX_ATTEMPTS,
|
|
69
|
+
});
|
|
50
70
|
throw new Error("Cursor authentication polling timeout");
|
|
51
71
|
}
|
|
52
72
|
export async function refreshCursorToken(refreshToken) {
|
|
@@ -60,6 +80,11 @@ export async function refreshCursorToken(refreshToken) {
|
|
|
60
80
|
});
|
|
61
81
|
if (!response.ok) {
|
|
62
82
|
const error = await response.text();
|
|
83
|
+
logPluginError("Cursor token refresh failed", {
|
|
84
|
+
stage: "token_refresh",
|
|
85
|
+
status: response.status,
|
|
86
|
+
responseBody: error,
|
|
87
|
+
});
|
|
63
88
|
throw new Error(`Cursor token refresh failed: ${error}`);
|
|
64
89
|
}
|
|
65
90
|
const data = (await response.json());
|
|
@@ -86,7 +111,6 @@ export function getTokenExpiry(token) {
|
|
|
86
111
|
return decoded.exp * 1000 - 5 * 60 * 1000;
|
|
87
112
|
}
|
|
88
113
|
}
|
|
89
|
-
catch {
|
|
90
|
-
}
|
|
114
|
+
catch { }
|
|
91
115
|
return Date.now() + 3600 * 1000;
|
|
92
116
|
}
|
|
@@ -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,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
|
+
}>;
|