opencode-cursor-oauth 0.0.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.
- package/README.md +94 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.js +98 -0
- package/dist/h2-bridge.mjs +140 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +157 -0
- package/dist/models.d.ts +22 -0
- package/dist/models.js +218 -0
- package/dist/pkce.d.ts +8 -0
- package/dist/pkce.js +13 -0
- package/dist/proto/agent_pb.d.ts +13022 -0
- package/dist/proto/agent_pb.js +3250 -0
- package/dist/proxy.d.ts +3 -0
- package/dist/proxy.js +992 -0
- package/package.json +55 -0
package/dist/models.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor model discovery via GetUsableModels gRPC endpoint.
|
|
3
|
+
* Uses curl for HTTP/2 transport (Bun's node:http2 is broken).
|
|
4
|
+
* Falls back to a hardcoded list if the endpoint is unreachable.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, } from "./proto/agent_pb";
|
|
13
|
+
const CURSOR_BASE_URL = "https://api2.cursor.sh";
|
|
14
|
+
const CURSOR_CLIENT_VERSION = "cli-2026.02.13-41ac335";
|
|
15
|
+
const GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
|
|
16
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
17
|
+
const DEFAULT_MAX_TOKENS = 64_000;
|
|
18
|
+
// --- Zod schemas for safe parsing of gRPC response ---
|
|
19
|
+
const CursorModelDetailsSchema = z.object({
|
|
20
|
+
modelId: z.string(),
|
|
21
|
+
displayName: z.string().optional().catch(undefined),
|
|
22
|
+
displayNameShort: z.string().optional().catch(undefined),
|
|
23
|
+
displayModelId: z.string().optional().catch(undefined),
|
|
24
|
+
aliases: z
|
|
25
|
+
.array(z.unknown())
|
|
26
|
+
.optional()
|
|
27
|
+
.catch([])
|
|
28
|
+
.transform((aliases) => (aliases ?? []).filter((alias) => typeof alias === "string")),
|
|
29
|
+
thinkingDetails: z.unknown().optional(),
|
|
30
|
+
});
|
|
31
|
+
const CursorDecodedResponseSchema = z.object({
|
|
32
|
+
models: z.array(z.unknown()).optional().catch([]),
|
|
33
|
+
});
|
|
34
|
+
// --- Hardcoded fallback models ---
|
|
35
|
+
const FALLBACK_MODELS = [
|
|
36
|
+
{ id: "composer-2", name: "Composer 2", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
|
|
37
|
+
{ id: "claude-4-sonnet", name: "Claude 4 Sonnet", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
|
|
38
|
+
{ id: "claude-3.5-sonnet", name: "Claude 3.5 Sonnet", reasoning: false, contextWindow: 200_000, maxTokens: 8_192 },
|
|
39
|
+
{ id: "gpt-4o", name: "GPT-4o", reasoning: false, contextWindow: 128_000, maxTokens: 16_384 },
|
|
40
|
+
{ id: "cursor-small", name: "Cursor Small", reasoning: false, contextWindow: 200_000, maxTokens: 64_000 },
|
|
41
|
+
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, contextWindow: 1_000_000, maxTokens: 65_536 },
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* Fetch models from Cursor's GetUsableModels gRPC endpoint.
|
|
45
|
+
* Returns null on failure (caller should use fallback list).
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchCursorUsableModels(options) {
|
|
48
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
49
|
+
try {
|
|
50
|
+
const requestPayload = create(GetUsableModelsRequestSchema, {});
|
|
51
|
+
const body = toBinary(GetUsableModelsRequestSchema, requestPayload);
|
|
52
|
+
const baseUrl = (options.baseUrl ?? CURSOR_BASE_URL).replace(/\/+$/, "");
|
|
53
|
+
const responseBuffer = await fetchViaHttp2(baseUrl, body, options, timeoutMs);
|
|
54
|
+
if (!responseBuffer)
|
|
55
|
+
return null;
|
|
56
|
+
const decoded = decodeGetUsableModelsResponse(responseBuffer);
|
|
57
|
+
const parsedDecoded = CursorDecodedResponseSchema.safeParse(decoded);
|
|
58
|
+
if (!parsedDecoded.success)
|
|
59
|
+
return null;
|
|
60
|
+
return normalizeCursorModels(parsedDecoded.data.models);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get cursor models: try dynamic discovery, fall back to hardcoded list.
|
|
68
|
+
*/
|
|
69
|
+
export async function getCursorModels(apiKey) {
|
|
70
|
+
const discovered = await fetchCursorUsableModels({ apiKey });
|
|
71
|
+
return discovered && discovered.length > 0 ? discovered : FALLBACK_MODELS;
|
|
72
|
+
}
|
|
73
|
+
// --- Internal helpers ---
|
|
74
|
+
function buildRequestHeaders(options) {
|
|
75
|
+
return {
|
|
76
|
+
"content-type": "application/proto",
|
|
77
|
+
te: "trailers",
|
|
78
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
79
|
+
"x-ghost-mode": "true",
|
|
80
|
+
"x-cursor-client-version": options.clientVersion ?? CURSOR_CLIENT_VERSION,
|
|
81
|
+
"x-cursor-client-type": "cli",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* HTTP/2 transport via curl (Bun's node:http2 doesn't work with Cursor's API).
|
|
86
|
+
* Writes request body to a temp file, invokes curl --http2, reads response.
|
|
87
|
+
*/
|
|
88
|
+
async function fetchViaHttp2(baseUrl, body, options, timeoutMs) {
|
|
89
|
+
const reqPath = join(tmpdir(), `cursor-req-${Date.now()}.bin`);
|
|
90
|
+
const respPath = join(tmpdir(), `cursor-resp-${Date.now()}.bin`);
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(reqPath, body);
|
|
93
|
+
const headers = buildRequestHeaders(options);
|
|
94
|
+
const headerArgs = Object.entries(headers)
|
|
95
|
+
.flatMap(([k, v]) => ["-H", `${k}: ${v}`]);
|
|
96
|
+
const timeoutSecs = Math.ceil(timeoutMs / 1000);
|
|
97
|
+
const url = `${baseUrl}${GET_USABLE_MODELS_PATH}`;
|
|
98
|
+
const args = [
|
|
99
|
+
"curl", "-s", "--http2",
|
|
100
|
+
"--max-time", String(timeoutSecs),
|
|
101
|
+
"-X", "POST",
|
|
102
|
+
...headerArgs,
|
|
103
|
+
"--data-binary", `@${reqPath}`,
|
|
104
|
+
"-o", respPath,
|
|
105
|
+
"-w", "%{http_code}",
|
|
106
|
+
url,
|
|
107
|
+
];
|
|
108
|
+
const status = execSync(args.map(a => a.includes(' ') ? `"${a}"` : a).join(' '), {
|
|
109
|
+
timeout: timeoutMs + 2000,
|
|
110
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
111
|
+
}).toString().trim();
|
|
112
|
+
if (!status.startsWith("2"))
|
|
113
|
+
return null;
|
|
114
|
+
if (!existsSync(respPath))
|
|
115
|
+
return null;
|
|
116
|
+
return new Uint8Array(readFileSync(respPath));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
try {
|
|
123
|
+
unlinkSync(reqPath);
|
|
124
|
+
}
|
|
125
|
+
catch { }
|
|
126
|
+
try {
|
|
127
|
+
unlinkSync(respPath);
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function decodeGetUsableModelsResponse(payload) {
|
|
133
|
+
if (payload.length === 0)
|
|
134
|
+
return null;
|
|
135
|
+
// Try Connect framing first (5-byte header)
|
|
136
|
+
const framedBody = decodeConnectUnaryBody(payload);
|
|
137
|
+
if (framedBody) {
|
|
138
|
+
try {
|
|
139
|
+
return fromBinary(GetUsableModelsResponseSchema, framedBody);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Raw protobuf
|
|
146
|
+
try {
|
|
147
|
+
return fromBinary(GetUsableModelsResponseSchema, payload);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function decodeConnectUnaryBody(payload) {
|
|
154
|
+
if (payload.length < 5)
|
|
155
|
+
return null;
|
|
156
|
+
let offset = 0;
|
|
157
|
+
while (offset + 5 <= payload.length) {
|
|
158
|
+
const flags = payload[offset];
|
|
159
|
+
const view = new DataView(payload.buffer, payload.byteOffset + offset, payload.byteLength - offset);
|
|
160
|
+
const messageLength = view.getUint32(1, false);
|
|
161
|
+
const frameEnd = offset + 5 + messageLength;
|
|
162
|
+
if (frameEnd > payload.length)
|
|
163
|
+
return null;
|
|
164
|
+
// Compression flag
|
|
165
|
+
if ((flags & 0b0000_0001) !== 0)
|
|
166
|
+
return null;
|
|
167
|
+
// End-of-stream flag — skip trailer frames
|
|
168
|
+
if (!((flags & 0b0000_0010) !== 0)) {
|
|
169
|
+
return payload.subarray(offset + 5, frameEnd);
|
|
170
|
+
}
|
|
171
|
+
offset = frameEnd;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function normalizeCursorModels(models) {
|
|
176
|
+
if (!models || models.length === 0)
|
|
177
|
+
return [];
|
|
178
|
+
const byId = new Map();
|
|
179
|
+
for (const model of models) {
|
|
180
|
+
const normalized = normalizeSingleModel(model);
|
|
181
|
+
if (normalized)
|
|
182
|
+
byId.set(normalized.id, normalized);
|
|
183
|
+
}
|
|
184
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
185
|
+
}
|
|
186
|
+
function normalizeSingleModel(model) {
|
|
187
|
+
const parsed = CursorModelDetailsSchema.safeParse(model);
|
|
188
|
+
if (!parsed.success)
|
|
189
|
+
return null;
|
|
190
|
+
const details = parsed.data;
|
|
191
|
+
const id = details.modelId.trim();
|
|
192
|
+
if (!id)
|
|
193
|
+
return null;
|
|
194
|
+
return {
|
|
195
|
+
id,
|
|
196
|
+
name: pickDisplayName(details, id),
|
|
197
|
+
reasoning: Boolean(details.thinkingDetails),
|
|
198
|
+
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
199
|
+
maxTokens: DEFAULT_MAX_TOKENS,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function pickDisplayName(model, fallbackId) {
|
|
203
|
+
const candidates = [
|
|
204
|
+
model.displayName,
|
|
205
|
+
model.displayNameShort,
|
|
206
|
+
model.displayModelId,
|
|
207
|
+
...model.aliases,
|
|
208
|
+
fallbackId,
|
|
209
|
+
];
|
|
210
|
+
for (const candidate of candidates) {
|
|
211
|
+
if (typeof candidate !== "string")
|
|
212
|
+
continue;
|
|
213
|
+
const trimmed = candidate.trim();
|
|
214
|
+
if (trimmed)
|
|
215
|
+
return trimmed;
|
|
216
|
+
}
|
|
217
|
+
return fallbackId;
|
|
218
|
+
}
|
package/dist/pkce.d.ts
ADDED
package/dist/pkce.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKCE (Proof Key for Code Exchange) helpers for Cursor OAuth.
|
|
3
|
+
* Uses Web Crypto API for cross-runtime compatibility.
|
|
4
|
+
*/
|
|
5
|
+
export async function generatePKCE() {
|
|
6
|
+
const verifierBytes = new Uint8Array(96);
|
|
7
|
+
crypto.getRandomValues(verifierBytes);
|
|
8
|
+
const verifier = Buffer.from(verifierBytes).toString("base64url");
|
|
9
|
+
const data = new TextEncoder().encode(verifier);
|
|
10
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
11
|
+
const challenge = Buffer.from(hashBuffer).toString("base64url");
|
|
12
|
+
return { verifier, challenge };
|
|
13
|
+
}
|