cursor-oauth-opencode 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.
- package/README.md +126 -0
- package/bin/setup.js +168 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.js +92 -0
- package/dist/h2-bridge.mjs +173 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +229 -0
- package/dist/models.d.ts +10 -0
- package/dist/models.js +158 -0
- package/dist/pkce.d.ts +4 -0
- package/dist/pkce.js +9 -0
- package/dist/proto/agent_pb.d.ts +13022 -0
- package/dist/proto/agent_pb.js +3250 -0
- package/dist/proxy.d.ts +19 -0
- package/dist/proxy.js +1175 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { generateCursorAuthParams, getTokenExpiry, pollCursorAuth, refreshCursorToken, } from "./auth";
|
|
2
|
+
import { getCursorModels } from "./models";
|
|
3
|
+
import { startProxy } from "./proxy";
|
|
4
|
+
const CURSOR_PROVIDER_ID = "cursor";
|
|
5
|
+
/**
|
|
6
|
+
* OpenCode plugin that provides Cursor authentication and model access.
|
|
7
|
+
* Register in opencode.json: { "plugin": ["cursor-oauth-opencode"] }
|
|
8
|
+
*/
|
|
9
|
+
export const CursorAuthPlugin = async (input) => {
|
|
10
|
+
return {
|
|
11
|
+
auth: {
|
|
12
|
+
provider: CURSOR_PROVIDER_ID,
|
|
13
|
+
async loader(getAuth, provider) {
|
|
14
|
+
const auth = await getAuth();
|
|
15
|
+
if (!auth || auth.type !== "oauth")
|
|
16
|
+
return {};
|
|
17
|
+
// Ensure we have a valid access token, refreshing if expired
|
|
18
|
+
let accessToken = auth.access;
|
|
19
|
+
if (!accessToken || auth.expires < Date.now()) {
|
|
20
|
+
const refreshed = await refreshCursorToken(auth.refresh);
|
|
21
|
+
await input.client.auth.set({
|
|
22
|
+
path: { id: CURSOR_PROVIDER_ID },
|
|
23
|
+
body: {
|
|
24
|
+
type: "oauth",
|
|
25
|
+
refresh: refreshed.refresh,
|
|
26
|
+
access: refreshed.access,
|
|
27
|
+
expires: refreshed.expires,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
accessToken = refreshed.access;
|
|
31
|
+
}
|
|
32
|
+
const models = await getCursorModels(accessToken);
|
|
33
|
+
const port = await startProxy(async () => {
|
|
34
|
+
const currentAuth = await getAuth();
|
|
35
|
+
if (currentAuth.type !== "oauth") {
|
|
36
|
+
throw new Error("Cursor auth not configured");
|
|
37
|
+
}
|
|
38
|
+
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
39
|
+
const refreshed = await refreshCursorToken(currentAuth.refresh);
|
|
40
|
+
await input.client.auth.set({
|
|
41
|
+
path: { id: CURSOR_PROVIDER_ID },
|
|
42
|
+
body: {
|
|
43
|
+
type: "oauth",
|
|
44
|
+
refresh: refreshed.refresh,
|
|
45
|
+
access: refreshed.access,
|
|
46
|
+
expires: refreshed.expires,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return refreshed.access;
|
|
50
|
+
}
|
|
51
|
+
return currentAuth.access;
|
|
52
|
+
}, models);
|
|
53
|
+
if (provider) {
|
|
54
|
+
provider.models = buildCursorProviderModels(models, port);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
baseURL: `http://localhost:${port}/v1`,
|
|
58
|
+
apiKey: "cursor-proxy",
|
|
59
|
+
async fetch(requestInput, init) {
|
|
60
|
+
if (init?.headers) {
|
|
61
|
+
if (init.headers instanceof Headers) {
|
|
62
|
+
init.headers.delete("authorization");
|
|
63
|
+
}
|
|
64
|
+
else if (Array.isArray(init.headers)) {
|
|
65
|
+
init.headers = init.headers.filter(([key]) => key.toLowerCase() !== "authorization");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
delete init.headers["authorization"];
|
|
69
|
+
delete init.headers["Authorization"];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return fetch(requestInput, init);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
methods: [
|
|
77
|
+
{
|
|
78
|
+
type: "oauth",
|
|
79
|
+
label: "Login with Cursor",
|
|
80
|
+
async authorize() {
|
|
81
|
+
const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
|
|
82
|
+
return {
|
|
83
|
+
url: loginUrl,
|
|
84
|
+
instructions: "Complete login in your browser. This window will close automatically.",
|
|
85
|
+
method: "auto",
|
|
86
|
+
async callback() {
|
|
87
|
+
const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
|
|
88
|
+
return {
|
|
89
|
+
type: "success",
|
|
90
|
+
refresh: refreshToken,
|
|
91
|
+
access: accessToken,
|
|
92
|
+
expires: getTokenExpiry(accessToken),
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
function buildCursorProviderModels(models, port) {
|
|
103
|
+
return Object.fromEntries(models.map((model) => [
|
|
104
|
+
model.id,
|
|
105
|
+
{
|
|
106
|
+
id: model.id,
|
|
107
|
+
providerID: CURSOR_PROVIDER_ID,
|
|
108
|
+
api: {
|
|
109
|
+
id: model.id,
|
|
110
|
+
url: `http://localhost:${port}/v1`,
|
|
111
|
+
npm: "@ai-sdk/openai-compatible",
|
|
112
|
+
},
|
|
113
|
+
name: model.name,
|
|
114
|
+
capabilities: {
|
|
115
|
+
temperature: true,
|
|
116
|
+
reasoning: model.reasoning,
|
|
117
|
+
attachment: false,
|
|
118
|
+
toolcall: true,
|
|
119
|
+
input: {
|
|
120
|
+
text: true,
|
|
121
|
+
audio: false,
|
|
122
|
+
image: false,
|
|
123
|
+
video: false,
|
|
124
|
+
pdf: false,
|
|
125
|
+
},
|
|
126
|
+
output: {
|
|
127
|
+
text: true,
|
|
128
|
+
audio: false,
|
|
129
|
+
image: false,
|
|
130
|
+
video: false,
|
|
131
|
+
pdf: false,
|
|
132
|
+
},
|
|
133
|
+
interleaved: false,
|
|
134
|
+
},
|
|
135
|
+
cost: estimateModelCost(model.id),
|
|
136
|
+
limit: {
|
|
137
|
+
context: model.contextWindow,
|
|
138
|
+
output: model.maxTokens,
|
|
139
|
+
},
|
|
140
|
+
status: "active",
|
|
141
|
+
options: {},
|
|
142
|
+
headers: {},
|
|
143
|
+
release_date: "",
|
|
144
|
+
variants: {},
|
|
145
|
+
},
|
|
146
|
+
]));
|
|
147
|
+
}
|
|
148
|
+
// $/M token rates from cursor.com/docs/models-and-pricing
|
|
149
|
+
const MODEL_COST_TABLE = {
|
|
150
|
+
// Anthropic
|
|
151
|
+
"claude-4-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
|
|
152
|
+
"claude-4-sonnet-1m": { input: 6, output: 22.5, cache: { read: 0.6, write: 7.5 } },
|
|
153
|
+
"claude-4.5-haiku": { input: 1, output: 5, cache: { read: 0.1, write: 1.25 } },
|
|
154
|
+
"claude-4.5-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
|
|
155
|
+
"claude-4.5-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
|
|
156
|
+
"claude-4.6-opus": { input: 5, output: 25, cache: { read: 0.5, write: 6.25 } },
|
|
157
|
+
"claude-4.6-opus-fast": { input: 30, output: 150, cache: { read: 3, write: 37.5 } },
|
|
158
|
+
"claude-4.6-sonnet": { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
|
|
159
|
+
// Cursor
|
|
160
|
+
"composer-1": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
161
|
+
"composer-1.5": { input: 3.5, output: 17.5, cache: { read: 0.35, write: 0 } },
|
|
162
|
+
"composer-2": { input: 0.5, output: 2.5, cache: { read: 0.2, write: 0 } },
|
|
163
|
+
"composer-2-fast": { input: 1.5, output: 7.5, cache: { read: 0.2, write: 0 } },
|
|
164
|
+
// Google
|
|
165
|
+
"gemini-2.5-flash": { input: 0.3, output: 2.5, cache: { read: 0.03, write: 0 } },
|
|
166
|
+
"gemini-3-flash": { input: 0.5, output: 3, cache: { read: 0.05, write: 0 } },
|
|
167
|
+
"gemini-3-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
|
|
168
|
+
"gemini-3-pro-image": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
|
|
169
|
+
"gemini-3.1-pro": { input: 2, output: 12, cache: { read: 0.2, write: 0 } },
|
|
170
|
+
// OpenAI
|
|
171
|
+
"gpt-5": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
172
|
+
"gpt-5-fast": { input: 2.5, output: 20, cache: { read: 0.25, write: 0 } },
|
|
173
|
+
"gpt-5-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
|
|
174
|
+
"gpt-5-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
175
|
+
"gpt-5.1-codex": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
176
|
+
"gpt-5.1-codex-max": { input: 1.25, output: 10, cache: { read: 0.125, write: 0 } },
|
|
177
|
+
"gpt-5.1-codex-mini": { input: 0.25, output: 2, cache: { read: 0.025, write: 0 } },
|
|
178
|
+
"gpt-5.2": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
|
|
179
|
+
"gpt-5.2-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
|
|
180
|
+
"gpt-5.3-codex": { input: 1.75, output: 14, cache: { read: 0.175, write: 0 } },
|
|
181
|
+
"gpt-5.4": { input: 2.5, output: 15, cache: { read: 0.25, write: 0 } },
|
|
182
|
+
"gpt-5.4-mini": { input: 0.75, output: 4.5, cache: { read: 0.075, write: 0 } },
|
|
183
|
+
"gpt-5.4-nano": { input: 0.2, output: 1.25, cache: { read: 0.02, write: 0 } },
|
|
184
|
+
// xAI
|
|
185
|
+
"grok-4.20": { input: 2, output: 6, cache: { read: 0.2, write: 0 } },
|
|
186
|
+
// Moonshot
|
|
187
|
+
"kimi-k2.5": { input: 0.6, output: 3, cache: { read: 0.1, write: 0 } },
|
|
188
|
+
};
|
|
189
|
+
// Most-specific first
|
|
190
|
+
const MODEL_COST_PATTERNS = [
|
|
191
|
+
{ match: (id) => /claude.*opus.*fast/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus-fast"] },
|
|
192
|
+
{ match: (id) => /claude.*opus/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus"] },
|
|
193
|
+
{ match: (id) => /claude.*haiku/i.test(id), cost: MODEL_COST_TABLE["claude-4.5-haiku"] },
|
|
194
|
+
{ match: (id) => /claude.*sonnet/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
|
|
195
|
+
{ match: (id) => /claude/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"] },
|
|
196
|
+
{ match: (id) => /composer-?2/i.test(id), cost: MODEL_COST_TABLE["composer-2"] },
|
|
197
|
+
{ match: (id) => /composer-?1\.5/i.test(id), cost: MODEL_COST_TABLE["composer-1.5"] },
|
|
198
|
+
{ match: (id) => /composer/i.test(id), cost: MODEL_COST_TABLE["composer-1"] },
|
|
199
|
+
{ match: (id) => /gpt-5\.4.*nano/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-nano"] },
|
|
200
|
+
{ match: (id) => /gpt-5\.4.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-mini"] },
|
|
201
|
+
{ match: (id) => /gpt-5\.4/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4"] },
|
|
202
|
+
{ match: (id) => /gpt-5\.3/i.test(id), cost: MODEL_COST_TABLE["gpt-5.3-codex"] },
|
|
203
|
+
{ match: (id) => /gpt-5\.2/i.test(id), cost: MODEL_COST_TABLE["gpt-5.2"] },
|
|
204
|
+
{ match: (id) => /gpt-5\.1.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex-mini"] },
|
|
205
|
+
{ match: (id) => /gpt-5\.1/i.test(id), cost: MODEL_COST_TABLE["gpt-5.1-codex"] },
|
|
206
|
+
{ match: (id) => /gpt-5.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5-mini"] },
|
|
207
|
+
{ match: (id) => /gpt-5.*fast/i.test(id), cost: MODEL_COST_TABLE["gpt-5-fast"] },
|
|
208
|
+
{ match: (id) => /gpt-5/i.test(id), cost: MODEL_COST_TABLE["gpt-5"] },
|
|
209
|
+
{ match: (id) => /gemini.*3\.1/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
|
|
210
|
+
{ match: (id) => /gemini.*3.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-3-flash"] },
|
|
211
|
+
{ match: (id) => /gemini.*3/i.test(id), cost: MODEL_COST_TABLE["gemini-3-pro"] },
|
|
212
|
+
{ match: (id) => /gemini.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-2.5-flash"] },
|
|
213
|
+
{ match: (id) => /gemini/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"] },
|
|
214
|
+
{ match: (id) => /grok/i.test(id), cost: MODEL_COST_TABLE["grok-4.20"] },
|
|
215
|
+
{ match: (id) => /kimi/i.test(id), cost: MODEL_COST_TABLE["kimi-k2.5"] },
|
|
216
|
+
];
|
|
217
|
+
const DEFAULT_COST = { input: 3, output: 15, cache: { read: 0.3, write: 0 } };
|
|
218
|
+
function estimateModelCost(modelId) {
|
|
219
|
+
const normalized = modelId.toLowerCase();
|
|
220
|
+
const exact = MODEL_COST_TABLE[normalized];
|
|
221
|
+
if (exact)
|
|
222
|
+
return exact;
|
|
223
|
+
const stripped = normalized.replace(/-(high|medium|low|preview|thinking|spark-preview)$/g, "");
|
|
224
|
+
const strippedMatch = MODEL_COST_TABLE[stripped];
|
|
225
|
+
if (strippedMatch)
|
|
226
|
+
return strippedMatch;
|
|
227
|
+
return MODEL_COST_PATTERNS.find((p) => p.match(normalized))?.cost ?? DEFAULT_COST;
|
|
228
|
+
}
|
|
229
|
+
export default CursorAuthPlugin;
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CursorModel {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
reasoning: boolean;
|
|
5
|
+
contextWindow: number;
|
|
6
|
+
maxTokens: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function getCursorModels(apiKey: string): Promise<CursorModel[]>;
|
|
9
|
+
/** @internal Test-only. */
|
|
10
|
+
export declare function clearModelCache(): void;
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor model discovery via GetUsableModels.
|
|
3
|
+
* Uses the H2 bridge for transport. Falls back to a hardcoded list
|
|
4
|
+
* when discovery fails.
|
|
5
|
+
*/
|
|
6
|
+
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { callCursorUnaryRpc } from "./proxy";
|
|
9
|
+
import { GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, } from "./proto/agent_pb";
|
|
10
|
+
const GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
|
|
11
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
12
|
+
const DEFAULT_MAX_TOKENS = 64_000;
|
|
13
|
+
const CursorModelDetailsSchema = z.object({
|
|
14
|
+
modelId: z.string(),
|
|
15
|
+
displayName: z.string().optional().catch(undefined),
|
|
16
|
+
displayNameShort: z.string().optional().catch(undefined),
|
|
17
|
+
displayModelId: z.string().optional().catch(undefined),
|
|
18
|
+
aliases: z
|
|
19
|
+
.array(z.unknown())
|
|
20
|
+
.optional()
|
|
21
|
+
.catch([])
|
|
22
|
+
.transform((aliases) => (aliases ?? []).filter((alias) => typeof alias === "string")),
|
|
23
|
+
thinkingDetails: z.unknown().optional(),
|
|
24
|
+
});
|
|
25
|
+
const FALLBACK_MODELS = [
|
|
26
|
+
// Composer models
|
|
27
|
+
{ id: "composer-1", name: "Composer 1", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
|
|
28
|
+
{ id: "composer-1.5", name: "Composer 1.5", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
|
|
29
|
+
// Claude models
|
|
30
|
+
{ id: "claude-4.6-opus-high", name: "Claude 4.6 Opus", reasoning: true, contextWindow: 200_000, maxTokens: 128_000 },
|
|
31
|
+
{ id: "claude-4.6-sonnet-medium", name: "Claude 4.6 Sonnet", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
|
|
32
|
+
{ id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
|
|
33
|
+
// GPT models
|
|
34
|
+
{ id: "gpt-5.4-medium", name: "GPT-5.4", reasoning: true, contextWindow: 272_000, maxTokens: 128_000 },
|
|
35
|
+
{ id: "gpt-5.2", name: "GPT-5.2", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 },
|
|
36
|
+
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 },
|
|
37
|
+
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 },
|
|
38
|
+
{ id: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark", reasoning: true, contextWindow: 128_000, maxTokens: 128_000 },
|
|
39
|
+
// Other models
|
|
40
|
+
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", reasoning: true, contextWindow: 1_000_000, maxTokens: 64_000 },
|
|
41
|
+
{ id: "grok-code-fast-1", name: "Grok Code Fast 1", reasoning: false, contextWindow: 128_000, maxTokens: 64_000 },
|
|
42
|
+
];
|
|
43
|
+
async function fetchCursorUsableModels(apiKey) {
|
|
44
|
+
try {
|
|
45
|
+
const requestPayload = create(GetUsableModelsRequestSchema, {});
|
|
46
|
+
const requestBody = toBinary(GetUsableModelsRequestSchema, requestPayload);
|
|
47
|
+
const response = await callCursorUnaryRpc({
|
|
48
|
+
accessToken: apiKey,
|
|
49
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
50
|
+
requestBody,
|
|
51
|
+
});
|
|
52
|
+
if (response.timedOut || response.exitCode !== 0 || response.body.length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const decoded = decodeGetUsableModelsResponse(response.body);
|
|
56
|
+
if (!decoded)
|
|
57
|
+
return null;
|
|
58
|
+
const models = normalizeCursorModels(decoded.models);
|
|
59
|
+
return models.length > 0 ? models : null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let cachedModels = null;
|
|
66
|
+
export async function getCursorModels(apiKey) {
|
|
67
|
+
if (cachedModels)
|
|
68
|
+
return cachedModels;
|
|
69
|
+
const discovered = await fetchCursorUsableModels(apiKey);
|
|
70
|
+
cachedModels = discovered && discovered.length > 0 ? discovered : FALLBACK_MODELS;
|
|
71
|
+
return cachedModels;
|
|
72
|
+
}
|
|
73
|
+
/** @internal Test-only. */
|
|
74
|
+
export function clearModelCache() {
|
|
75
|
+
cachedModels = null;
|
|
76
|
+
}
|
|
77
|
+
function decodeGetUsableModelsResponse(payload) {
|
|
78
|
+
try {
|
|
79
|
+
return fromBinary(GetUsableModelsResponseSchema, payload);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
const framedBody = decodeConnectUnaryBody(payload);
|
|
83
|
+
if (!framedBody)
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
return fromBinary(GetUsableModelsResponseSchema, framedBody);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function decodeConnectUnaryBody(payload) {
|
|
94
|
+
if (payload.length < 5)
|
|
95
|
+
return null;
|
|
96
|
+
let offset = 0;
|
|
97
|
+
while (offset + 5 <= payload.length) {
|
|
98
|
+
const flags = payload[offset];
|
|
99
|
+
const view = new DataView(payload.buffer, payload.byteOffset + offset, payload.byteLength - offset);
|
|
100
|
+
const messageLength = view.getUint32(1, false);
|
|
101
|
+
const frameEnd = offset + 5 + messageLength;
|
|
102
|
+
if (frameEnd > payload.length)
|
|
103
|
+
return null;
|
|
104
|
+
// Compression flag
|
|
105
|
+
if ((flags & 0b0000_0001) !== 0)
|
|
106
|
+
return null;
|
|
107
|
+
// End-of-stream flag — skip trailer frames
|
|
108
|
+
if ((flags & 0b0000_0010) === 0) {
|
|
109
|
+
return payload.subarray(offset + 5, frameEnd);
|
|
110
|
+
}
|
|
111
|
+
offset = frameEnd;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function normalizeCursorModels(models) {
|
|
116
|
+
if (models.length === 0)
|
|
117
|
+
return [];
|
|
118
|
+
const byId = new Map();
|
|
119
|
+
for (const model of models) {
|
|
120
|
+
const normalized = normalizeSingleModel(model);
|
|
121
|
+
if (normalized)
|
|
122
|
+
byId.set(normalized.id, normalized);
|
|
123
|
+
}
|
|
124
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
125
|
+
}
|
|
126
|
+
function normalizeSingleModel(model) {
|
|
127
|
+
const parsed = CursorModelDetailsSchema.safeParse(model);
|
|
128
|
+
if (!parsed.success)
|
|
129
|
+
return null;
|
|
130
|
+
const details = parsed.data;
|
|
131
|
+
const id = details.modelId.trim();
|
|
132
|
+
if (!id)
|
|
133
|
+
return null;
|
|
134
|
+
return {
|
|
135
|
+
id,
|
|
136
|
+
name: pickDisplayName(details, id),
|
|
137
|
+
reasoning: Boolean(details.thinkingDetails),
|
|
138
|
+
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
139
|
+
maxTokens: DEFAULT_MAX_TOKENS,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function pickDisplayName(model, fallbackId) {
|
|
143
|
+
const candidates = [
|
|
144
|
+
model.displayName,
|
|
145
|
+
model.displayNameShort,
|
|
146
|
+
model.displayModelId,
|
|
147
|
+
...model.aliases,
|
|
148
|
+
fallbackId,
|
|
149
|
+
];
|
|
150
|
+
for (const candidate of candidates) {
|
|
151
|
+
if (typeof candidate !== "string")
|
|
152
|
+
continue;
|
|
153
|
+
const trimmed = candidate.trim();
|
|
154
|
+
if (trimmed)
|
|
155
|
+
return trimmed;
|
|
156
|
+
}
|
|
157
|
+
return fallbackId;
|
|
158
|
+
}
|
package/dist/pkce.d.ts
ADDED
package/dist/pkce.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function generatePKCE() {
|
|
2
|
+
const verifierBytes = new Uint8Array(96);
|
|
3
|
+
crypto.getRandomValues(verifierBytes);
|
|
4
|
+
const verifier = Buffer.from(verifierBytes).toString("base64url");
|
|
5
|
+
const data = new TextEncoder().encode(verifier);
|
|
6
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
7
|
+
const challenge = Buffer.from(hashBuffer).toString("base64url");
|
|
8
|
+
return { verifier, challenge };
|
|
9
|
+
}
|