@rahularya01/pi-cursor 1.0.0 → 1.2.0
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/CHANGELOG.md +29 -0
- package/README.md +28 -4
- package/dist/index.js +27 -0
- package/package.json +15 -9
- package/src/auth/cli-credentials.ts +0 -168
- package/src/auth/index.ts +0 -10
- package/src/auth/oauth.ts +0 -214
- package/src/client/bridge.ts +0 -206
- package/src/client/cursor-wire.ts +0 -212
- package/src/client/index.ts +0 -19
- package/src/diagnostics/diagnostics.ts +0 -62
- package/src/diagnostics/index.ts +0 -1
- package/src/index.ts +0 -1393
- package/src/models/catalog.json +0 -1163
- package/src/models/index.ts +0 -2
- package/src/proto/agent_pb.ts +0 -15294
- package/src/stream/index.ts +0 -11
- package/src/stream/native-core.ts +0 -5760
- package/src/usage.ts +0 -262
- package/src/utils/index.ts +0 -2
- package/src/utils/security.ts +0 -65
- package/src/utils/util.ts +0 -19
- package/tsconfig.json +0 -21
- /package/{src/client → dist}/h2-bridge.mjs +0 -0
package/src/index.ts
DELETED
|
@@ -1,1393 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cursor Provider Extension for pi
|
|
3
|
-
*
|
|
4
|
-
* Provides access to Cursor models (Claude, GPT, Gemini, etc.) via:
|
|
5
|
-
* 1. Browser-based PKCE OAuth login to Cursor
|
|
6
|
-
* 2. Native Pi streamSimple provider translating Pi context → Cursor gRPC protocol
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* /login cursor — authenticate via browser
|
|
10
|
-
* /model — select any Cursor model
|
|
11
|
-
*
|
|
12
|
-
* Based on https://github.com/ephraimduncan/opencode-cursor by Ephraim Duncan.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import rawFallbackModels from "./models/catalog.json" with { type: "json" };
|
|
16
|
-
import {
|
|
17
|
-
readStoredCredential,
|
|
18
|
-
type ExtensionAPI,
|
|
19
|
-
type ExtensionCommandContext,
|
|
20
|
-
type ExtensionContext,
|
|
21
|
-
} from "@earendil-works/pi-coding-agent";
|
|
22
|
-
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
23
|
-
import { appendFileSync } from "node:fs";
|
|
24
|
-
import { createHash } from "node:crypto";
|
|
25
|
-
import { tmpdir } from "node:os";
|
|
26
|
-
import { join as pathJoin } from "node:path";
|
|
27
|
-
import {
|
|
28
|
-
generateCursorAuthParams,
|
|
29
|
-
getTokenExpiry,
|
|
30
|
-
pollCursorAuth,
|
|
31
|
-
refreshCursorToken,
|
|
32
|
-
} from "./auth/oauth.js";
|
|
33
|
-
import { resolveSystemCursorAccessToken, type CredentialSource } from "./auth/cli-credentials.js";
|
|
34
|
-
import { getLastDiagnostics, setLastAvailableModels } from "./diagnostics/index.js";
|
|
35
|
-
import { redactSecrets } from "./utils/security.js";
|
|
36
|
-
import { formatCursorUsage, getCursorUsageSummary } from "./usage.js";
|
|
37
|
-
import {
|
|
38
|
-
cleanupSessionState,
|
|
39
|
-
createCursorNativeStream,
|
|
40
|
-
getCursorAgentUrl,
|
|
41
|
-
getCursorModels,
|
|
42
|
-
getCursorParameterizedModels,
|
|
43
|
-
type CursorModel,
|
|
44
|
-
type CursorModelParameter,
|
|
45
|
-
type CursorParameterizedModel,
|
|
46
|
-
type CursorParameterizedVariant,
|
|
47
|
-
} from "./stream/native-core.js";
|
|
48
|
-
|
|
49
|
-
// ── Cost estimation ──
|
|
50
|
-
|
|
51
|
-
interface ModelCost {
|
|
52
|
-
input: number;
|
|
53
|
-
output: number;
|
|
54
|
-
cacheRead: number;
|
|
55
|
-
cacheWrite: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let extensionDebugLogFilePath: string | undefined;
|
|
59
|
-
|
|
60
|
-
function isExtensionDebugEnabled(): boolean {
|
|
61
|
-
const raw = process.env.PI_CURSOR_PROVIDER_DEBUG?.trim().toLowerCase();
|
|
62
|
-
return !!raw && raw !== "0" && raw !== "false" && raw !== "off";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getExtensionDebugLogFilePath(): string {
|
|
66
|
-
if (extensionDebugLogFilePath) return extensionDebugLogFilePath;
|
|
67
|
-
const configured = process.env.PI_CURSOR_PROVIDER_EXTENSION_DEBUG_FILE?.trim();
|
|
68
|
-
if (configured) {
|
|
69
|
-
extensionDebugLogFilePath = configured;
|
|
70
|
-
return extensionDebugLogFilePath;
|
|
71
|
-
}
|
|
72
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
73
|
-
extensionDebugLogFilePath = pathJoin(
|
|
74
|
-
tmpdir(),
|
|
75
|
-
`pi-cursor-provider-extension-debug-${stamp}-${process.pid}.log`,
|
|
76
|
-
);
|
|
77
|
-
return extensionDebugLogFilePath;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function truncateDebugValue(value: string, max = 240): string {
|
|
81
|
-
return value.length > max
|
|
82
|
-
? `${value.slice(0, max)}…<truncated ${value.length - max} chars>`
|
|
83
|
-
: value;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function summarizeBase64ImageData(data: string): {
|
|
87
|
-
base64Length: number;
|
|
88
|
-
byteLength?: number;
|
|
89
|
-
sha256?: string;
|
|
90
|
-
} {
|
|
91
|
-
const summary: { base64Length: number; byteLength?: number; sha256?: string } = {
|
|
92
|
-
base64Length: data.length,
|
|
93
|
-
};
|
|
94
|
-
try {
|
|
95
|
-
const bytes = Buffer.from(data.replace(/\s/g, ""), "base64");
|
|
96
|
-
if (bytes.length > 0) {
|
|
97
|
-
summary.byteLength = bytes.length;
|
|
98
|
-
summary.sha256 = createHash("sha256").update(bytes).digest("hex").slice(0, 16);
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
// Invalid base64 — keep length-only summary.
|
|
102
|
-
}
|
|
103
|
-
return summary;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function summarizeImageBlock(type: unknown, mimeType: unknown, data: unknown): unknown {
|
|
107
|
-
return {
|
|
108
|
-
type,
|
|
109
|
-
mimeType,
|
|
110
|
-
...(typeof data === "string"
|
|
111
|
-
? summarizeBase64ImageData(data)
|
|
112
|
-
: { data: `<redacted base64 ${String(data ?? "").length} chars>` }),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function summarizeDataImageUrl(url: string): unknown {
|
|
117
|
-
const match = url.trim().match(/^data:([^;,]+)(?:;[^,]*)?;base64,(.*)$/is);
|
|
118
|
-
if (!match)
|
|
119
|
-
return {
|
|
120
|
-
url: url.startsWith("data:image/")
|
|
121
|
-
? `<redacted data image ${url.length} chars>`
|
|
122
|
-
: truncateDebugValue(url),
|
|
123
|
-
};
|
|
124
|
-
return {
|
|
125
|
-
mimeType: match[1]?.toLowerCase(),
|
|
126
|
-
...summarizeBase64ImageData(match[2]!),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function summarizeContent(content: unknown): unknown {
|
|
131
|
-
if (typeof content === "string") return truncateDebugValue(content);
|
|
132
|
-
if (!Array.isArray(content)) return content;
|
|
133
|
-
return content.map((block) => {
|
|
134
|
-
if (!block || typeof block !== "object") return block;
|
|
135
|
-
const typed = block as Record<string, unknown>;
|
|
136
|
-
switch (typed.type) {
|
|
137
|
-
case "text":
|
|
138
|
-
return { type: "text", text: truncateDebugValue(String(typed.text ?? "")) };
|
|
139
|
-
case "thinking":
|
|
140
|
-
return { type: "thinking", thinking: truncateDebugValue(String(typed.thinking ?? "")) };
|
|
141
|
-
case "toolCall":
|
|
142
|
-
return {
|
|
143
|
-
type: "toolCall",
|
|
144
|
-
id: typed.id,
|
|
145
|
-
name: typed.name,
|
|
146
|
-
arguments: typed.arguments,
|
|
147
|
-
};
|
|
148
|
-
case "image":
|
|
149
|
-
return summarizeImageBlock("image", typed.mimeType, typed.data);
|
|
150
|
-
case "image_url": {
|
|
151
|
-
const url = (typed.image_url as Record<string, unknown> | undefined)?.url;
|
|
152
|
-
const text = typeof url === "string" ? url : "";
|
|
153
|
-
return { type: "image_url", image_url: summarizeDataImageUrl(text) };
|
|
154
|
-
}
|
|
155
|
-
default:
|
|
156
|
-
return typed;
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function summarizeMessage(message: unknown): unknown {
|
|
162
|
-
if (!message || typeof message !== "object") return message;
|
|
163
|
-
const typed = message as Record<string, unknown>;
|
|
164
|
-
return {
|
|
165
|
-
role: typed.role,
|
|
166
|
-
stopReason: typed.stopReason,
|
|
167
|
-
toolCallId: typed.toolCallId,
|
|
168
|
-
toolName: typed.toolName,
|
|
169
|
-
isError: typed.isError,
|
|
170
|
-
errorMessage: typed.errorMessage,
|
|
171
|
-
content: summarizeContent(typed.content),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function summarizeBranchTail(
|
|
176
|
-
ctx: {
|
|
177
|
-
sessionManager?: {
|
|
178
|
-
getBranch?: () => unknown[];
|
|
179
|
-
getLeafId?: () => string | null;
|
|
180
|
-
getSessionId?: () => string;
|
|
181
|
-
};
|
|
182
|
-
},
|
|
183
|
-
limit = 6,
|
|
184
|
-
): unknown {
|
|
185
|
-
try {
|
|
186
|
-
const branch = ctx.sessionManager?.getBranch?.();
|
|
187
|
-
if (!Array.isArray(branch)) return undefined;
|
|
188
|
-
return {
|
|
189
|
-
sessionId: ctx.sessionManager?.getSessionId?.(),
|
|
190
|
-
leafId: ctx.sessionManager?.getLeafId?.(),
|
|
191
|
-
size: branch.length,
|
|
192
|
-
tail: branch.slice(-limit).map((entry) => {
|
|
193
|
-
if (!entry || typeof entry !== "object") return entry;
|
|
194
|
-
const typed = entry as Record<string, unknown>;
|
|
195
|
-
return {
|
|
196
|
-
type: typed.type,
|
|
197
|
-
id: typed.id,
|
|
198
|
-
parentId: typed.parentId,
|
|
199
|
-
customType: typed.customType,
|
|
200
|
-
message: summarizeMessage(typed.message),
|
|
201
|
-
};
|
|
202
|
-
}),
|
|
203
|
-
};
|
|
204
|
-
} catch (error) {
|
|
205
|
-
return { error: error instanceof Error ? error.message : String(error) };
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export interface CursorToolResultImagePayload {
|
|
210
|
-
toolCallId: string;
|
|
211
|
-
images: Array<{ data: string; mimeType: string }>;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function payloadToolCallIds(payload: Record<string, unknown>): Set<string> {
|
|
215
|
-
const ids = new Set<string>();
|
|
216
|
-
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
|
217
|
-
for (const message of messages) {
|
|
218
|
-
if (!message || typeof message !== "object") continue;
|
|
219
|
-
const typed = message as Record<string, unknown>;
|
|
220
|
-
if (typed.role === "tool" && typeof typed.tool_call_id === "string" && typed.tool_call_id)
|
|
221
|
-
ids.add(typed.tool_call_id);
|
|
222
|
-
}
|
|
223
|
-
return ids;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function extractToolResultImagePayloads(
|
|
227
|
-
ctx: { sessionManager?: { getBranch?: () => unknown[] } },
|
|
228
|
-
payload: Record<string, unknown>,
|
|
229
|
-
): CursorToolResultImagePayload[] {
|
|
230
|
-
const idsInPayload = payloadToolCallIds(payload);
|
|
231
|
-
if (idsInPayload.size === 0) return [];
|
|
232
|
-
const branch = ctx.sessionManager?.getBranch?.();
|
|
233
|
-
if (!Array.isArray(branch)) return [];
|
|
234
|
-
|
|
235
|
-
const byToolCallId = new Map<string, CursorToolResultImagePayload>();
|
|
236
|
-
for (const entry of branch) {
|
|
237
|
-
if (!entry || typeof entry !== "object") continue;
|
|
238
|
-
const message = (entry as Record<string, unknown>).message;
|
|
239
|
-
if (!message || typeof message !== "object") continue;
|
|
240
|
-
const typed = message as Record<string, unknown>;
|
|
241
|
-
const toolCallId = typeof typed.toolCallId === "string" ? typed.toolCallId : "";
|
|
242
|
-
if (typed.role !== "toolResult" || !toolCallId || !idsInPayload.has(toolCallId)) continue;
|
|
243
|
-
const content = Array.isArray(typed.content) ? typed.content : [];
|
|
244
|
-
const images = content.flatMap((block) => {
|
|
245
|
-
if (!block || typeof block !== "object") return [];
|
|
246
|
-
const image = block as Record<string, unknown>;
|
|
247
|
-
if (
|
|
248
|
-
image.type !== "image" ||
|
|
249
|
-
typeof image.data !== "string" ||
|
|
250
|
-
typeof image.mimeType !== "string"
|
|
251
|
-
)
|
|
252
|
-
return [];
|
|
253
|
-
return [{ data: image.data, mimeType: image.mimeType }];
|
|
254
|
-
});
|
|
255
|
-
if (images.length === 0) continue;
|
|
256
|
-
const existing = byToolCallId.get(toolCallId);
|
|
257
|
-
if (existing) existing.images.push(...images);
|
|
258
|
-
else byToolCallId.set(toolCallId, { toolCallId, images });
|
|
259
|
-
}
|
|
260
|
-
return [...byToolCallId.values()];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function debugExtensionLog(event: string, data?: Record<string, unknown>): void {
|
|
264
|
-
if (!isExtensionDebugEnabled()) return;
|
|
265
|
-
const payload = JSON.stringify({
|
|
266
|
-
ts: new Date().toISOString(),
|
|
267
|
-
pid: process.pid,
|
|
268
|
-
scope: "extension",
|
|
269
|
-
event,
|
|
270
|
-
...data,
|
|
271
|
-
});
|
|
272
|
-
appendFileSync(getExtensionDebugLogFilePath(), `${payload}\n`, "utf8");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const MODEL_COST_TABLE: Record<string, ModelCost> = {
|
|
276
|
-
"claude-4-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
277
|
-
"claude-4.5-haiku": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
278
|
-
"claude-4.5-opus": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
279
|
-
"claude-4.5-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
280
|
-
"claude-4.6-opus": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
281
|
-
"claude-4.6-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
282
|
-
"composer-1": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
|
|
283
|
-
"composer-1.5": { input: 3.5, output: 17.5, cacheRead: 0.35, cacheWrite: 0 },
|
|
284
|
-
"composer-2": { input: 0.5, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
|
|
285
|
-
"gemini-2.5-flash": { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 },
|
|
286
|
-
"gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 },
|
|
287
|
-
"gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
|
|
288
|
-
"gemini-3.1-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
|
|
289
|
-
"gpt-5": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
|
|
290
|
-
"gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 },
|
|
291
|
-
"gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
|
292
|
-
"gpt-5.2-codex": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
|
293
|
-
"gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
|
294
|
-
"gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
|
295
|
-
"gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
|
|
296
|
-
"gpt-5.5": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
|
297
|
-
"grok-4.20": { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 0 },
|
|
298
|
-
"kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const MODEL_COST_PATTERNS: Array<{ match: (id: string) => boolean; cost: ModelCost }> = [
|
|
302
|
-
{
|
|
303
|
-
match: (id) => /claude.*opus.*fast/i.test(id),
|
|
304
|
-
cost: { input: 30, output: 150, cacheRead: 3, cacheWrite: 37.5 },
|
|
305
|
-
},
|
|
306
|
-
{ match: (id) => /claude.*opus/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-opus"]! },
|
|
307
|
-
{ match: (id) => /claude.*haiku/i.test(id), cost: MODEL_COST_TABLE["claude-4.5-haiku"]! },
|
|
308
|
-
{ match: (id) => /claude.*sonnet/i.test(id), cost: MODEL_COST_TABLE["claude-4.6-sonnet"]! },
|
|
309
|
-
{ match: (id) => /composer/i.test(id), cost: MODEL_COST_TABLE["composer-1"]! },
|
|
310
|
-
{ match: (id) => /gpt-5\.5/i.test(id), cost: MODEL_COST_TABLE["gpt-5.5"]! },
|
|
311
|
-
{ match: (id) => /gpt-5\.4.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4-mini"]! },
|
|
312
|
-
{ match: (id) => /gpt-5\.4/i.test(id), cost: MODEL_COST_TABLE["gpt-5.4"]! },
|
|
313
|
-
{ match: (id) => /gpt-5\.3/i.test(id), cost: MODEL_COST_TABLE["gpt-5.3-codex"]! },
|
|
314
|
-
{ match: (id) => /gpt-5\.2/i.test(id), cost: MODEL_COST_TABLE["gpt-5.2"]! },
|
|
315
|
-
{ match: (id) => /gpt-5.*mini/i.test(id), cost: MODEL_COST_TABLE["gpt-5-mini"]! },
|
|
316
|
-
{ match: (id) => /gpt-5/i.test(id), cost: MODEL_COST_TABLE["gpt-5"]! },
|
|
317
|
-
{ match: (id) => /gemini.*3\.1/i.test(id), cost: MODEL_COST_TABLE["gemini-3.1-pro"]! },
|
|
318
|
-
{ match: (id) => /gemini.*flash/i.test(id), cost: MODEL_COST_TABLE["gemini-2.5-flash"]! },
|
|
319
|
-
{ match: (id) => /gemini/i.test(id), cost: MODEL_COST_TABLE["gemini-3-pro"]! },
|
|
320
|
-
{ match: (id) => /grok/i.test(id), cost: MODEL_COST_TABLE["grok-4.20"]! },
|
|
321
|
-
{ match: (id) => /kimi/i.test(id), cost: MODEL_COST_TABLE["kimi-k2.5"]! },
|
|
322
|
-
];
|
|
323
|
-
|
|
324
|
-
const DEFAULT_COST: ModelCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 };
|
|
325
|
-
|
|
326
|
-
function estimateModelCost(modelId: string): ModelCost {
|
|
327
|
-
const normalized = modelId.toLowerCase();
|
|
328
|
-
const exact = MODEL_COST_TABLE[normalized];
|
|
329
|
-
if (exact) return exact;
|
|
330
|
-
const stripped = normalized.replace(
|
|
331
|
-
/-(high|medium|low|preview|thinking|spark-preview|fast)$/g,
|
|
332
|
-
"",
|
|
333
|
-
);
|
|
334
|
-
const strippedMatch = MODEL_COST_TABLE[stripped];
|
|
335
|
-
if (strippedMatch) return strippedMatch;
|
|
336
|
-
return MODEL_COST_PATTERNS.find((p) => p.match(normalized))?.cost ?? DEFAULT_COST;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// ── Effort-level dedup ──
|
|
340
|
-
|
|
341
|
-
const CURSOR_EFFORT_SUFFIXES: Array<{ suffix: string; effort: string }> = [
|
|
342
|
-
{ suffix: "extra-high", effort: "xhigh" },
|
|
343
|
-
{ suffix: "minimal", effort: "minimal" },
|
|
344
|
-
{ suffix: "xhigh", effort: "xhigh" },
|
|
345
|
-
{ suffix: "medium", effort: "medium" },
|
|
346
|
-
{ suffix: "high", effort: "high" },
|
|
347
|
-
{ suffix: "low", effort: "low" },
|
|
348
|
-
{ suffix: "max", effort: "max" },
|
|
349
|
-
{ suffix: "none", effort: "none" },
|
|
350
|
-
];
|
|
351
|
-
|
|
352
|
-
type PiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
353
|
-
type CursorEffortMap = Record<PiThinkingLevel, string | null>;
|
|
354
|
-
|
|
355
|
-
interface ParsedModelId {
|
|
356
|
-
base: string; // model ID with effort stripped
|
|
357
|
-
effort: string; // effort level, or "" if no effort suffix
|
|
358
|
-
fast: boolean; // has -fast suffix
|
|
359
|
-
thinking: boolean; // has -thinking suffix
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function stripEffortSuffix(id: string): { remaining: string; effort: string } {
|
|
363
|
-
for (const { suffix, effort } of CURSOR_EFFORT_SUFFIXES) {
|
|
364
|
-
const marker = `-${suffix}`;
|
|
365
|
-
if (id.endsWith(marker)) {
|
|
366
|
-
return { remaining: id.slice(0, -marker.length), effort };
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return { remaining: id, effort: "" };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export function parseModelId(id: string): ParsedModelId {
|
|
373
|
-
let remaining = id;
|
|
374
|
-
let fast = false;
|
|
375
|
-
let thinking = false;
|
|
376
|
-
|
|
377
|
-
if (remaining.endsWith("-fast")) {
|
|
378
|
-
fast = true;
|
|
379
|
-
remaining = remaining.slice(0, -5);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Cursor has used both orders for thinking effort variants:
|
|
383
|
-
// claude-4.6-opus-max-thinking (effort before -thinking)
|
|
384
|
-
// claude-opus-4-7-thinking-max (effort after -thinking)
|
|
385
|
-
let effort: string;
|
|
386
|
-
if (remaining.endsWith("-thinking")) {
|
|
387
|
-
thinking = true;
|
|
388
|
-
remaining = remaining.slice(0, -9);
|
|
389
|
-
const parsed = stripEffortSuffix(remaining);
|
|
390
|
-
remaining = parsed.remaining;
|
|
391
|
-
effort = parsed.effort;
|
|
392
|
-
} else {
|
|
393
|
-
const parsed = stripEffortSuffix(remaining);
|
|
394
|
-
remaining = parsed.remaining;
|
|
395
|
-
effort = parsed.effort;
|
|
396
|
-
if (remaining.endsWith("-thinking")) {
|
|
397
|
-
thinking = true;
|
|
398
|
-
remaining = remaining.slice(0, -9);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return { base: remaining, effort, fast, thinking };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export interface CursorModelRouting {
|
|
406
|
-
modelId: string;
|
|
407
|
-
parameters?: CursorModelParameter[];
|
|
408
|
-
requiresMaxMode?: boolean;
|
|
409
|
-
requestedMaxMode?: boolean;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
export interface ProcessedModel extends CursorModel {
|
|
413
|
-
supportsEffort: boolean;
|
|
414
|
-
effortMap?: CursorEffortMap;
|
|
415
|
-
rawModelByEffort?: Record<string, string>;
|
|
416
|
-
rawRoutingByEffort?: Record<string, CursorModelRouting>;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
export function buildNoReasoningEffortLookup(models: ProcessedModel[]): Map<string, string> {
|
|
420
|
-
const lookup = new Map<string, string>();
|
|
421
|
-
for (const model of models) {
|
|
422
|
-
if (
|
|
423
|
-
model.supportsEffort &&
|
|
424
|
-
model.effortMap &&
|
|
425
|
-
Object.values(model.effortMap).includes("none")
|
|
426
|
-
) {
|
|
427
|
-
lookup.set(model.id, "none");
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
return lookup;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function routingForModel(model: CursorModel): CursorModelRouting | undefined {
|
|
434
|
-
if (
|
|
435
|
-
!model.requestedModelId &&
|
|
436
|
-
!model.parameters?.length &&
|
|
437
|
-
!model.requiresMaxMode &&
|
|
438
|
-
typeof model.requestedMaxMode !== "boolean"
|
|
439
|
-
) {
|
|
440
|
-
return undefined;
|
|
441
|
-
}
|
|
442
|
-
return {
|
|
443
|
-
modelId: model.requestedModelId ?? model.id,
|
|
444
|
-
...(model.parameters?.length ? { parameters: model.parameters } : {}),
|
|
445
|
-
...(model.requiresMaxMode ? { requiresMaxMode: true } : {}),
|
|
446
|
-
...(typeof model.requestedMaxMode === "boolean"
|
|
447
|
-
? { requestedMaxMode: model.requestedMaxMode }
|
|
448
|
-
: {}),
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function defaultRoutingEffort(model: ProcessedModel): string | undefined {
|
|
453
|
-
const routes = model.rawRoutingByEffort;
|
|
454
|
-
if (!routes) return undefined;
|
|
455
|
-
const mappedMedium = model.effortMap?.medium;
|
|
456
|
-
for (const effort of [mappedMedium, "medium", "", "low", "high", "none", "xhigh", "max"]) {
|
|
457
|
-
if (typeof effort === "string" && routes[effort]) return effort;
|
|
458
|
-
}
|
|
459
|
-
return Object.keys(routes)[0];
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
export function buildRawModelLookup(
|
|
463
|
-
models: ProcessedModel[],
|
|
464
|
-
): Map<string, Record<string, CursorModelRouting>> {
|
|
465
|
-
const lookup = new Map<string, Record<string, CursorModelRouting>>();
|
|
466
|
-
for (const model of models) {
|
|
467
|
-
if (model.supportsEffort && model.rawRoutingByEffort) {
|
|
468
|
-
const routes = { ...model.rawRoutingByEffort };
|
|
469
|
-
if (model.effortMap) {
|
|
470
|
-
for (const [piEffort, cursorEffort] of Object.entries(model.effortMap)) {
|
|
471
|
-
if (typeof cursorEffort === "string" && !routes[piEffort] && routes[cursorEffort]) {
|
|
472
|
-
routes[piEffort] = routes[cursorEffort];
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
const defaultEffort = defaultRoutingEffort(model);
|
|
477
|
-
if (defaultEffort !== undefined && !routes[""])
|
|
478
|
-
routes[""] = model.rawRoutingByEffort[defaultEffort]!;
|
|
479
|
-
lookup.set(model.id, routes);
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const routing = routingForModel(model);
|
|
484
|
-
if (routing) lookup.set(model.id, { "": routing });
|
|
485
|
-
}
|
|
486
|
-
return lookup;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
export function applyRawCursorModelId(
|
|
490
|
-
payload: Record<string, unknown>,
|
|
491
|
-
rawRoutingByEffortByModelId: Map<string, Record<string, CursorModelRouting>>,
|
|
492
|
-
): void {
|
|
493
|
-
if (typeof payload.model !== "string") return;
|
|
494
|
-
const rawRoutingByEffort = rawRoutingByEffortByModelId.get(payload.model);
|
|
495
|
-
const effort = typeof payload.reasoning_effort === "string" ? payload.reasoning_effort : "";
|
|
496
|
-
const routing = rawRoutingByEffort?.[effort];
|
|
497
|
-
if (!routing) return;
|
|
498
|
-
payload.cursor_model_id = routing.modelId;
|
|
499
|
-
if (routing.parameters?.length) payload.cursor_model_parameters = routing.parameters;
|
|
500
|
-
if (routing.requiresMaxMode) payload.cursor_requires_max_mode = true;
|
|
501
|
-
if (typeof routing.requestedMaxMode === "boolean")
|
|
502
|
-
payload.cursor_model_max_mode = routing.requestedMaxMode;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export function applyNoReasoningEffort(
|
|
506
|
-
payload: Record<string, unknown>,
|
|
507
|
-
thinkingLevel: string,
|
|
508
|
-
noReasoningEffortByModelId: Map<string, string>,
|
|
509
|
-
): void {
|
|
510
|
-
if (
|
|
511
|
-
thinkingLevel !== "off" ||
|
|
512
|
-
payload.reasoning_effort !== undefined ||
|
|
513
|
-
typeof payload.model !== "string"
|
|
514
|
-
)
|
|
515
|
-
return;
|
|
516
|
-
const noReasoningEffort = noReasoningEffortByModelId.get(payload.model);
|
|
517
|
-
if (noReasoningEffort) payload.reasoning_effort = noReasoningEffort;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
export function supportsReasoningModelId(id: string): boolean {
|
|
521
|
-
const { base, effort, thinking } = parseModelId(id);
|
|
522
|
-
if (effort || thinking) return true;
|
|
523
|
-
if (base === "default" || base === "auto") return true;
|
|
524
|
-
return /^(claude|composer|gemini|gpt|grok|kimi)(-|$)/i.test(base);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Map only controls Cursor explicitly advertised. Null hides unsupported Pi
|
|
529
|
-
* levels instead of silently routing them to a different Cursor effort.
|
|
530
|
-
*/
|
|
531
|
-
export function buildEffortMap(efforts: Set<string>): CursorEffortMap {
|
|
532
|
-
const supported = (effort: string): string | null => (efforts.has(effort) ? effort : null);
|
|
533
|
-
return {
|
|
534
|
-
off: supported("none"),
|
|
535
|
-
minimal: supported("minimal"),
|
|
536
|
-
low: supported("low"),
|
|
537
|
-
// A bare Cursor model ID is the provider's default effort, equivalent to Pi medium.
|
|
538
|
-
medium: efforts.has("medium") ? "medium" : supported(""),
|
|
539
|
-
high: supported("high"),
|
|
540
|
-
xhigh: supported("xhigh"),
|
|
541
|
-
max: supported("max"),
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/** Dedup raw models: collapse effort variants into one entry with supportsReasoningEffort. */
|
|
546
|
-
export function processModels(raw: CursorModel[]): ProcessedModel[] {
|
|
547
|
-
// Group by (base, fast, thinking)
|
|
548
|
-
const groups = new Map<
|
|
549
|
-
string,
|
|
550
|
-
{
|
|
551
|
-
base: string;
|
|
552
|
-
fast: boolean;
|
|
553
|
-
thinking: boolean;
|
|
554
|
-
efforts: Map<string, CursorModel>;
|
|
555
|
-
}
|
|
556
|
-
>();
|
|
557
|
-
|
|
558
|
-
for (const model of raw) {
|
|
559
|
-
const p = parseModelId(model.id);
|
|
560
|
-
const key = `${p.base}|${p.fast}|${p.thinking}`;
|
|
561
|
-
let g = groups.get(key);
|
|
562
|
-
if (!g) {
|
|
563
|
-
g = { base: p.base, fast: p.fast, thinking: p.thinking, efforts: new Map() };
|
|
564
|
-
groups.set(key, g);
|
|
565
|
-
}
|
|
566
|
-
g.efforts.set(p.effort, model);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const result: ProcessedModel[] = [];
|
|
570
|
-
|
|
571
|
-
for (const g of groups.values()) {
|
|
572
|
-
const effortNames = new Set(g.efforts.keys());
|
|
573
|
-
|
|
574
|
-
// Dedup when there are multiple effort variants, OR a single variant
|
|
575
|
-
// whose effort is non-empty (e.g. claude-4.5-opus-high — strip the
|
|
576
|
-
// mandatory effort suffix so the model appears as claude-4.5-opus
|
|
577
|
-
// with effort mapping).
|
|
578
|
-
const hasOnlyEffortVariants = g.efforts.size === 1 && !g.efforts.has("");
|
|
579
|
-
const shouldDedup = effortNames.size >= 2 || hasOnlyEffortVariants;
|
|
580
|
-
if (shouldDedup) {
|
|
581
|
-
// Pick representative: prefer "medium" or default ("") for name/metadata
|
|
582
|
-
const rep = g.efforts.get("medium") ?? g.efforts.get("") ?? [...g.efforts.values()][0]!;
|
|
583
|
-
|
|
584
|
-
// Build deduped model ID: base + thinking/fast suffix (no effort)
|
|
585
|
-
let id = g.base;
|
|
586
|
-
if (g.thinking) id += "-thinking";
|
|
587
|
-
if (g.fast) id += "-fast";
|
|
588
|
-
|
|
589
|
-
const effortMap = buildEffortMap(effortNames);
|
|
590
|
-
const rawModelByEffort = Object.fromEntries(
|
|
591
|
-
[...g.efforts.entries()].map(([effort, model]) => [effort, model.id]),
|
|
592
|
-
);
|
|
593
|
-
const rawRoutingByEffort = Object.fromEntries(
|
|
594
|
-
[...g.efforts.entries()].map(([effort, model]) => [
|
|
595
|
-
effort,
|
|
596
|
-
{
|
|
597
|
-
modelId: model.requestedModelId ?? model.id,
|
|
598
|
-
...(model.parameters?.length ? { parameters: model.parameters } : {}),
|
|
599
|
-
...(model.requiresMaxMode ? { requiresMaxMode: true } : {}),
|
|
600
|
-
...(typeof model.requestedMaxMode === "boolean"
|
|
601
|
-
? { requestedMaxMode: model.requestedMaxMode }
|
|
602
|
-
: {}),
|
|
603
|
-
},
|
|
604
|
-
]),
|
|
605
|
-
);
|
|
606
|
-
|
|
607
|
-
result.push({
|
|
608
|
-
...rep,
|
|
609
|
-
id,
|
|
610
|
-
supportsEffort: true,
|
|
611
|
-
effortMap,
|
|
612
|
-
rawModelByEffort,
|
|
613
|
-
rawRoutingByEffort,
|
|
614
|
-
});
|
|
615
|
-
} else {
|
|
616
|
-
// Keep single entries as-is (base model without effort variants)
|
|
617
|
-
for (const model of g.efforts.values()) {
|
|
618
|
-
result.push({ ...model, supportsEffort: false });
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
return result.sort((a, b) => a.id.localeCompare(b.id));
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
export function modelConfig(m: ProcessedModel) {
|
|
627
|
-
const input = (m.supportsImages === false ? ["text"] : ["text", "image"]) as ("text" | "image")[];
|
|
628
|
-
return {
|
|
629
|
-
id: m.id,
|
|
630
|
-
name: m.name,
|
|
631
|
-
// Pi's thinking control must only appear when Cursor exposed selectable
|
|
632
|
-
// effort variants. A model name alone is not evidence of a controllable level.
|
|
633
|
-
reasoning: m.supportsEffort,
|
|
634
|
-
...(m.supportsEffort &&
|
|
635
|
-
m.effortMap && {
|
|
636
|
-
thinkingLevelMap: m.effortMap,
|
|
637
|
-
}),
|
|
638
|
-
input,
|
|
639
|
-
cost: estimateModelCost(m.id),
|
|
640
|
-
contextWindow: m.contextWindow,
|
|
641
|
-
maxTokens: m.maxTokens,
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const GPT55_VARIANTS = [
|
|
646
|
-
{
|
|
647
|
-
idPart: "",
|
|
648
|
-
label: "272K",
|
|
649
|
-
context: "272k",
|
|
650
|
-
contextWindow: 272_000,
|
|
651
|
-
requestedMaxMode: false,
|
|
652
|
-
fastOptions: [false, true],
|
|
653
|
-
},
|
|
654
|
-
{
|
|
655
|
-
idPart: "-max",
|
|
656
|
-
label: "272K Max",
|
|
657
|
-
context: "272k",
|
|
658
|
-
contextWindow: 272_000,
|
|
659
|
-
requestedMaxMode: true,
|
|
660
|
-
fastOptions: [false, true],
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
idPart: "-1m",
|
|
664
|
-
label: "1M",
|
|
665
|
-
context: "1m",
|
|
666
|
-
contextWindow: 1_000_000,
|
|
667
|
-
requestedMaxMode: true,
|
|
668
|
-
fastOptions: [false],
|
|
669
|
-
},
|
|
670
|
-
] as const;
|
|
671
|
-
|
|
672
|
-
const GPT55_REASONING_LEVELS = [
|
|
673
|
-
{ suffix: "none", label: "None", value: "none" },
|
|
674
|
-
{ suffix: "low", label: "Low", value: "low" },
|
|
675
|
-
{ suffix: "medium", label: "", value: "medium" },
|
|
676
|
-
{ suffix: "high", label: "High", value: "high" },
|
|
677
|
-
{ suffix: "extra-high", label: "Extra High", value: "extra-high" },
|
|
678
|
-
] as const;
|
|
679
|
-
|
|
680
|
-
function gpt55ParameterizedModels(): CursorModel[] {
|
|
681
|
-
const models: CursorModel[] = [];
|
|
682
|
-
for (const variant of GPT55_VARIANTS) {
|
|
683
|
-
// Cursor treats maxMode as an orthogonal request flag. The model picker
|
|
684
|
-
// cannot toggle Cursor-specific flags, so expose useful maxMode states as
|
|
685
|
-
// explicit rows. Cursor's metadata does not include context=1m + fast=true,
|
|
686
|
-
// so the 1M variant intentionally has fast=false only.
|
|
687
|
-
for (const fast of variant.fastOptions) {
|
|
688
|
-
for (const reasoning of GPT55_REASONING_LEVELS) {
|
|
689
|
-
const id = `gpt-5.5${variant.idPart}-${reasoning.suffix}${fast ? "-fast" : ""}`;
|
|
690
|
-
const nameParts = ["GPT-5.5", variant.label, reasoning.label, fast ? "Fast" : ""].filter(
|
|
691
|
-
Boolean,
|
|
692
|
-
);
|
|
693
|
-
models.push({
|
|
694
|
-
id,
|
|
695
|
-
name: nameParts.join(" "),
|
|
696
|
-
reasoning: true,
|
|
697
|
-
contextWindow: variant.contextWindow,
|
|
698
|
-
maxTokens: 64_000,
|
|
699
|
-
requestedModelId: "gpt-5.5",
|
|
700
|
-
requiresMaxMode: variant.context === "1m",
|
|
701
|
-
requestedMaxMode: variant.requestedMaxMode,
|
|
702
|
-
parameters: [
|
|
703
|
-
{ id: "context", value: variant.context },
|
|
704
|
-
{ id: "reasoning", value: reasoning.value },
|
|
705
|
-
{ id: "fast", value: String(fast) },
|
|
706
|
-
],
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
return models;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function parameterValue(parameters: CursorModelParameter[], id: string): string | undefined {
|
|
715
|
-
return parameters.find((parameter) => parameter.id === id)?.value;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function contextWindowFromParameter(context: string | undefined, fallback = 200_000): number {
|
|
719
|
-
if (context === "272k") return 272_000;
|
|
720
|
-
if (context === "1m") return 1_000_000;
|
|
721
|
-
const k = context?.match(/^(\d+)k$/i)?.[1];
|
|
722
|
-
if (k) return Number(k) * 1_000;
|
|
723
|
-
const m = context?.match(/^(\d+)m$/i)?.[1];
|
|
724
|
-
if (m) return Number(m) * 1_000_000;
|
|
725
|
-
return fallback;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function cursorEffortSuffix(value: string): string {
|
|
729
|
-
return value;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function cursorEffortLabel(value: string): string {
|
|
733
|
-
return (
|
|
734
|
-
GPT55_REASONING_LEVELS.find((level) => level.value === value)?.label ||
|
|
735
|
-
({ xhigh: "Extra High", max: "Max", none: "None" } as Record<string, string>)[value] ||
|
|
736
|
-
value.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function metadataEffortParameterId(
|
|
741
|
-
variant: CursorParameterizedVariant,
|
|
742
|
-
): "reasoning" | "effort" | undefined {
|
|
743
|
-
if (variant.parameters.some((parameter) => parameter.id === "reasoning")) return "reasoning";
|
|
744
|
-
if (variant.parameters.some((parameter) => parameter.id === "effort")) return "effort";
|
|
745
|
-
return undefined;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function isDefaultContext(context: string | undefined): boolean {
|
|
749
|
-
if (!context) return true;
|
|
750
|
-
return context === "200k" || context === "272k" || context === "300k";
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function contextIdPart(context: string | undefined): string {
|
|
754
|
-
return context && !isDefaultContext(context) ? `-${context.toLowerCase()}` : "";
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function contextLabel(context: string | undefined): string | undefined {
|
|
758
|
-
if (!context || isDefaultContext(context)) return undefined;
|
|
759
|
-
return context.toUpperCase();
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function maxModeIdPart(
|
|
763
|
-
modelName: string,
|
|
764
|
-
context: string | undefined,
|
|
765
|
-
requestedMaxMode: boolean,
|
|
766
|
-
hasEffortParameter: boolean,
|
|
767
|
-
): string {
|
|
768
|
-
// 1M context already names the Max/extended-context selection. For default
|
|
769
|
-
// context windows, expose maxMode as an explicit row suffix. If the Cursor
|
|
770
|
-
// model ID already contains "max" (for example gpt-5.1-codex-max), or if
|
|
771
|
-
// this row has no effort parameter, use a clearer suffix so the model parser
|
|
772
|
-
// does not confuse Max Mode with a Cursor effort value.
|
|
773
|
-
if (!requestedMaxMode || context === "1m") return "";
|
|
774
|
-
return !hasEffortParameter || /(^|-)max($|-)/i.test(modelName) ? "-max-mode" : "-max";
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function maxModeLabel(
|
|
778
|
-
modelName: string,
|
|
779
|
-
context: string | undefined,
|
|
780
|
-
requestedMaxMode: boolean,
|
|
781
|
-
hasEffortParameter: boolean,
|
|
782
|
-
): string | undefined {
|
|
783
|
-
const idPart = maxModeIdPart(modelName, context, requestedMaxMode, hasEffortParameter);
|
|
784
|
-
if (!idPart) return undefined;
|
|
785
|
-
return idPart === "-max-mode" ? "Max Mode" : "Max";
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function parameterizedBaseId(
|
|
789
|
-
modelName: string,
|
|
790
|
-
variant: CursorParameterizedVariant,
|
|
791
|
-
requestedMaxMode: boolean,
|
|
792
|
-
hasEffortParameter: boolean,
|
|
793
|
-
): string {
|
|
794
|
-
const context = parameterValue(variant.parameters, "context");
|
|
795
|
-
return `${modelName}${contextIdPart(context)}${maxModeIdPart(modelName, context, requestedMaxMode, hasEffortParameter)}`;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function parameterizedBaseLabel(
|
|
799
|
-
model: CursorParameterizedModel,
|
|
800
|
-
variant: CursorParameterizedVariant,
|
|
801
|
-
requestedMaxMode: boolean,
|
|
802
|
-
hasEffortParameter: boolean,
|
|
803
|
-
): string[] {
|
|
804
|
-
const context = parameterValue(variant.parameters, "context");
|
|
805
|
-
return [
|
|
806
|
-
model.clientDisplayName || model.name,
|
|
807
|
-
contextLabel(context),
|
|
808
|
-
maxModeLabel(model.name, context, requestedMaxMode, hasEffortParameter),
|
|
809
|
-
].filter(Boolean) as string[];
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function hasVariantParameterSet(
|
|
813
|
-
model: CursorParameterizedModel,
|
|
814
|
-
parameters: CursorModelParameter[],
|
|
815
|
-
): boolean {
|
|
816
|
-
const normalized = normalizeParameterValues(parameters);
|
|
817
|
-
return model.variants.some(
|
|
818
|
-
(variant) => normalizeParameterValues(variant.parameters) === normalized,
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
function normalizeParameterValues(parameters: CursorModelParameter[]): string {
|
|
823
|
-
return parameters
|
|
824
|
-
.map((parameter) => `${parameter.id}=${parameter.value}`)
|
|
825
|
-
.sort()
|
|
826
|
-
.join(";");
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function buildParameterizedRowsFromGroup(options: {
|
|
830
|
-
model: CursorParameterizedModel;
|
|
831
|
-
variants: CursorParameterizedVariant[];
|
|
832
|
-
requestedMaxMode: boolean;
|
|
833
|
-
effortParameterId?: "reasoning" | "effort";
|
|
834
|
-
}): CursorModel[] {
|
|
835
|
-
const first = options.variants[0];
|
|
836
|
-
if (!first) return [];
|
|
837
|
-
if (options.requestedMaxMode && !first.isMaxMode && !options.model.supportsMaxMode) return [];
|
|
838
|
-
|
|
839
|
-
const context = parameterValue(first.parameters, "context");
|
|
840
|
-
const fast = parameterValue(first.parameters, "fast") === "true";
|
|
841
|
-
const thinking = parameterValue(first.parameters, "thinking") === "true";
|
|
842
|
-
const hasEffortParameter = Boolean(options.effortParameterId);
|
|
843
|
-
const baseId = parameterizedBaseId(
|
|
844
|
-
options.model.name,
|
|
845
|
-
first,
|
|
846
|
-
options.requestedMaxMode,
|
|
847
|
-
hasEffortParameter,
|
|
848
|
-
);
|
|
849
|
-
const baseLabelParts = parameterizedBaseLabel(
|
|
850
|
-
options.model,
|
|
851
|
-
first,
|
|
852
|
-
options.requestedMaxMode,
|
|
853
|
-
hasEffortParameter,
|
|
854
|
-
);
|
|
855
|
-
const contextWindow = contextWindowFromParameter(
|
|
856
|
-
context,
|
|
857
|
-
options.requestedMaxMode
|
|
858
|
-
? (options.model.contextTokenLimitForMaxMode ?? options.model.contextTokenLimit ?? 200_000)
|
|
859
|
-
: (options.model.contextTokenLimit ?? 200_000),
|
|
860
|
-
);
|
|
861
|
-
|
|
862
|
-
return options.variants.flatMap((variant) => {
|
|
863
|
-
const parameters = variant.parameters.map((parameter) => ({
|
|
864
|
-
id: parameter.id,
|
|
865
|
-
value: parameter.value,
|
|
866
|
-
}));
|
|
867
|
-
if (!hasVariantParameterSet(options.model, parameters)) return [];
|
|
868
|
-
|
|
869
|
-
const effort = options.effortParameterId
|
|
870
|
-
? parameterValue(variant.parameters, options.effortParameterId)
|
|
871
|
-
: undefined;
|
|
872
|
-
const id = options.effortParameterId
|
|
873
|
-
? `${baseId}-${cursorEffortSuffix(effort!)}${thinking ? "-thinking" : ""}${fast ? "-fast" : ""}`
|
|
874
|
-
: `${baseId}${thinking ? "-thinking" : ""}${fast ? "-fast" : ""}`;
|
|
875
|
-
const name = [
|
|
876
|
-
...baseLabelParts,
|
|
877
|
-
effort ? cursorEffortLabel(effort) : undefined,
|
|
878
|
-
thinking ? "Thinking" : undefined,
|
|
879
|
-
fast ? "Fast" : undefined,
|
|
880
|
-
]
|
|
881
|
-
.filter(Boolean)
|
|
882
|
-
.join(" ");
|
|
883
|
-
|
|
884
|
-
return [
|
|
885
|
-
{
|
|
886
|
-
id,
|
|
887
|
-
name,
|
|
888
|
-
reasoning: Boolean(options.effortParameterId) || thinking,
|
|
889
|
-
contextWindow,
|
|
890
|
-
maxTokens: 64_000,
|
|
891
|
-
requestedModelId: options.model.name,
|
|
892
|
-
requiresMaxMode: variant.isMaxMode,
|
|
893
|
-
requestedMaxMode: options.requestedMaxMode,
|
|
894
|
-
supportsImages: options.model.supportsImages,
|
|
895
|
-
parameters,
|
|
896
|
-
} satisfies CursorModel,
|
|
897
|
-
];
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
function parameterGroupKey(
|
|
902
|
-
variant: CursorParameterizedVariant,
|
|
903
|
-
effortParameterId?: string,
|
|
904
|
-
): string {
|
|
905
|
-
const params = variant.parameters
|
|
906
|
-
.filter((parameter) => parameter.id !== effortParameterId)
|
|
907
|
-
.map((parameter) => `${parameter.id}=${parameter.value}`)
|
|
908
|
-
.sort()
|
|
909
|
-
.join(";");
|
|
910
|
-
return `${variant.isMaxMode ? "max" : "nonmax"}|${params}`;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function shouldGenerateSyntheticMaxRows(
|
|
914
|
-
model: CursorParameterizedModel,
|
|
915
|
-
variant: CursorParameterizedVariant,
|
|
916
|
-
): boolean {
|
|
917
|
-
// Cursor's metadata has both per-variant isMaxMode and model-level
|
|
918
|
-
// supportsMaxMode. Some supported Max Mode combinations are represented only
|
|
919
|
-
// by supportsMaxMode=true over a non-Max parameter set, so expose explicit
|
|
920
|
-
// max-mode rows for every such advertised parameter set.
|
|
921
|
-
return model.supportsMaxMode === true && !variant.isMaxMode;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
export function modelsFromParameterizedMetadata(
|
|
925
|
-
parameterizedModels: CursorParameterizedModel[],
|
|
926
|
-
): CursorModel[] {
|
|
927
|
-
const rows: CursorModel[] = [];
|
|
928
|
-
for (const model of parameterizedModels) {
|
|
929
|
-
const groups = new Map<
|
|
930
|
-
string,
|
|
931
|
-
{ effortParameterId?: "reasoning" | "effort"; variants: CursorParameterizedVariant[] }
|
|
932
|
-
>();
|
|
933
|
-
for (const variant of model.variants) {
|
|
934
|
-
if (variant.parameters.length === 0) continue;
|
|
935
|
-
const effortParameterId = metadataEffortParameterId(variant);
|
|
936
|
-
const key = parameterGroupKey(variant, effortParameterId);
|
|
937
|
-
const group = groups.get(key) ?? { effortParameterId, variants: [] };
|
|
938
|
-
group.variants.push(variant);
|
|
939
|
-
groups.set(key, group);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
for (const group of groups.values()) {
|
|
943
|
-
const first = group.variants[0];
|
|
944
|
-
if (!first) continue;
|
|
945
|
-
rows.push(
|
|
946
|
-
...buildParameterizedRowsFromGroup({
|
|
947
|
-
model,
|
|
948
|
-
variants: group.variants,
|
|
949
|
-
requestedMaxMode: first.isMaxMode,
|
|
950
|
-
effortParameterId: group.effortParameterId,
|
|
951
|
-
}),
|
|
952
|
-
);
|
|
953
|
-
if (shouldGenerateSyntheticMaxRows(model, first)) {
|
|
954
|
-
rows.push(
|
|
955
|
-
...buildParameterizedRowsFromGroup({
|
|
956
|
-
model,
|
|
957
|
-
variants: group.variants,
|
|
958
|
-
requestedMaxMode: true,
|
|
959
|
-
effortParameterId: group.effortParameterId,
|
|
960
|
-
}),
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
return rows;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
function normalizeDisplayModel(model: CursorModel): CursorModel {
|
|
969
|
-
if (model.id !== "default") return model;
|
|
970
|
-
return {
|
|
971
|
-
...model,
|
|
972
|
-
id: "auto",
|
|
973
|
-
name: model.name && model.name !== "default" ? model.name : "Auto",
|
|
974
|
-
requestedModelId: model.requestedModelId ?? "default",
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
export function augmentCursorModels(
|
|
979
|
-
raw: CursorModel[],
|
|
980
|
-
parameterizedModels: CursorParameterizedModel[] = [],
|
|
981
|
-
): CursorModel[] {
|
|
982
|
-
const byId = new Map<string, CursorModel>();
|
|
983
|
-
const imageSupportByModelId = new Map(
|
|
984
|
-
parameterizedModels
|
|
985
|
-
.filter((model) => typeof model.supportsImages === "boolean")
|
|
986
|
-
.map((model) => [model.name, model.supportsImages!]),
|
|
987
|
-
);
|
|
988
|
-
for (const model of raw.map(normalizeDisplayModel)) {
|
|
989
|
-
const lookupId = model.requestedModelId ?? model.id;
|
|
990
|
-
const metadataSupportsImages = imageSupportByModelId.get(lookupId);
|
|
991
|
-
byId.set(model.id, {
|
|
992
|
-
...model,
|
|
993
|
-
...(model.supportsImages === undefined && metadataSupportsImages !== undefined
|
|
994
|
-
? { supportsImages: metadataSupportsImages }
|
|
995
|
-
: {}),
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
const metadataRows =
|
|
1000
|
-
modelsFromParameterizedMetadata(parameterizedModels).map(normalizeDisplayModel);
|
|
1001
|
-
for (const model of metadataRows) byId.set(model.id, model);
|
|
1002
|
-
|
|
1003
|
-
// Fallback for static/offline discovery. Cursor exposes GPT-5.5 context as
|
|
1004
|
-
// parameters (272K vs 1M), not distinct backend model IDs.
|
|
1005
|
-
if (metadataRows.length === 0 && raw.some((model) => /^gpt-5\.5(?:-|$)/.test(model.id))) {
|
|
1006
|
-
for (const model of gpt55ParameterizedModels()) byId.set(model.id, model);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
return [...byId.values()];
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
export const FALLBACK_MODELS: CursorModel[] = augmentCursorModels(
|
|
1013
|
-
rawFallbackModels as CursorModel[],
|
|
1014
|
-
).map((model) => ({
|
|
1015
|
-
...model,
|
|
1016
|
-
reasoning: supportsReasoningModelId(model.id),
|
|
1017
|
-
}));
|
|
1018
|
-
|
|
1019
|
-
// ── Extension ──
|
|
1020
|
-
|
|
1021
|
-
const CURSOR_PROVIDER_ID = "cursor";
|
|
1022
|
-
|
|
1023
|
-
async function getStoredCursorOAuthAccessToken(): Promise<
|
|
1024
|
-
{ accessToken: string; source: CredentialSource } | undefined
|
|
1025
|
-
> {
|
|
1026
|
-
const credential = readStoredCredential(CURSOR_PROVIDER_ID);
|
|
1027
|
-
if (!credential || credential.type !== "oauth") return undefined;
|
|
1028
|
-
|
|
1029
|
-
if (Date.now() < credential.expires && credential.access) {
|
|
1030
|
-
return { accessToken: credential.access, source: "pi_oauth" };
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// Refresh is handled by Pi's OAuth runtime on demand; avoid writing auth.json here.
|
|
1034
|
-
if (credential.refresh) {
|
|
1035
|
-
try {
|
|
1036
|
-
const refreshed = await refreshCursorToken(credential.refresh);
|
|
1037
|
-
return { accessToken: refreshed.access, source: "pi_oauth_refresh" };
|
|
1038
|
-
} catch {
|
|
1039
|
-
return undefined;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
return undefined;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
async function getStartupCursorAccessToken(): Promise<
|
|
1046
|
-
{ accessToken: string; source: CredentialSource } | undefined
|
|
1047
|
-
> {
|
|
1048
|
-
const systemToken = await resolveSystemCursorAccessToken();
|
|
1049
|
-
if (systemToken) return systemToken;
|
|
1050
|
-
return getStoredCursorOAuthAccessToken();
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
export function registerSessionLifecycleCleanup(pi: ExtensionAPI) {
|
|
1054
|
-
const cleanupCurrentSession = (_event: unknown, ctx: ExtensionContext) => {
|
|
1055
|
-
debugExtensionLog("session.cleanup_hook", {
|
|
1056
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
1057
|
-
leafId: ctx.sessionManager.getLeafId?.(),
|
|
1058
|
-
});
|
|
1059
|
-
cleanupSessionState(ctx.sessionManager.getSessionId());
|
|
1060
|
-
};
|
|
1061
|
-
|
|
1062
|
-
pi.on("session_before_switch", cleanupCurrentSession);
|
|
1063
|
-
pi.on("session_before_fork", cleanupCurrentSession);
|
|
1064
|
-
pi.on("session_before_tree", cleanupCurrentSession);
|
|
1065
|
-
pi.on("session_shutdown", cleanupCurrentSession);
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
function registerExtensionDebugHooks(pi: ExtensionAPI) {
|
|
1069
|
-
if (!isExtensionDebugEnabled()) return;
|
|
1070
|
-
|
|
1071
|
-
pi.on("message_start", async (event, ctx) => {
|
|
1072
|
-
if (ctx.model?.provider !== "cursor") return;
|
|
1073
|
-
debugExtensionLog("message.start", {
|
|
1074
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
1075
|
-
leafId: ctx.sessionManager.getLeafId?.(),
|
|
1076
|
-
model: ctx.model?.id,
|
|
1077
|
-
message: summarizeMessage((event as { message?: unknown }).message),
|
|
1078
|
-
});
|
|
1079
|
-
});
|
|
1080
|
-
|
|
1081
|
-
pi.on("message_update", async (event, ctx) => {
|
|
1082
|
-
if (ctx.model?.provider !== "cursor") return;
|
|
1083
|
-
const typedEvent = event as {
|
|
1084
|
-
message?: unknown;
|
|
1085
|
-
assistantMessageEvent?: Record<string, unknown>;
|
|
1086
|
-
};
|
|
1087
|
-
debugExtensionLog("message.update", {
|
|
1088
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
1089
|
-
leafId: ctx.sessionManager.getLeafId?.(),
|
|
1090
|
-
model: ctx.model?.id,
|
|
1091
|
-
assistantMessageEvent: typedEvent.assistantMessageEvent
|
|
1092
|
-
? {
|
|
1093
|
-
type: typedEvent.assistantMessageEvent.type,
|
|
1094
|
-
delta: truncateDebugValue(
|
|
1095
|
-
String(
|
|
1096
|
-
(typedEvent.assistantMessageEvent as Record<string, unknown>).delta ??
|
|
1097
|
-
(typedEvent.assistantMessageEvent as Record<string, unknown>).content ??
|
|
1098
|
-
"",
|
|
1099
|
-
),
|
|
1100
|
-
),
|
|
1101
|
-
}
|
|
1102
|
-
: undefined,
|
|
1103
|
-
message: summarizeMessage(typedEvent.message),
|
|
1104
|
-
});
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
pi.on("message_end", async (event, ctx) => {
|
|
1108
|
-
if (ctx.model?.provider !== "cursor") return;
|
|
1109
|
-
debugExtensionLog("message.end", {
|
|
1110
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
1111
|
-
leafId: ctx.sessionManager.getLeafId?.(),
|
|
1112
|
-
model: ctx.model?.id,
|
|
1113
|
-
message: summarizeMessage((event as { message?: unknown }).message),
|
|
1114
|
-
branch: summarizeBranchTail(ctx),
|
|
1115
|
-
});
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
pi.on("context", async (event, ctx) => {
|
|
1119
|
-
if (ctx.model?.provider !== "cursor") return;
|
|
1120
|
-
const typedEvent = event as { messages?: unknown[] };
|
|
1121
|
-
debugExtensionLog("context", {
|
|
1122
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
1123
|
-
leafId: ctx.sessionManager.getLeafId?.(),
|
|
1124
|
-
model: ctx.model?.id,
|
|
1125
|
-
messageCount: Array.isArray(typedEvent.messages) ? typedEvent.messages.length : undefined,
|
|
1126
|
-
messages: Array.isArray(typedEvent.messages)
|
|
1127
|
-
? typedEvent.messages.slice(-8).map((message) => summarizeMessage(message))
|
|
1128
|
-
: undefined,
|
|
1129
|
-
branch: summarizeBranchTail(ctx),
|
|
1130
|
-
});
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
pi.on("turn_end", async (event, ctx) => {
|
|
1134
|
-
if (ctx.model?.provider !== "cursor") return;
|
|
1135
|
-
const typedEvent = event as { turnIndex?: number; message?: unknown; toolResults?: unknown[] };
|
|
1136
|
-
debugExtensionLog("turn.end", {
|
|
1137
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
1138
|
-
leafId: ctx.sessionManager.getLeafId?.(),
|
|
1139
|
-
model: ctx.model?.id,
|
|
1140
|
-
turnIndex: typedEvent.turnIndex,
|
|
1141
|
-
message: summarizeMessage(typedEvent.message),
|
|
1142
|
-
toolResults: Array.isArray(typedEvent.toolResults)
|
|
1143
|
-
? typedEvent.toolResults.map((message) => summarizeMessage(message))
|
|
1144
|
-
: undefined,
|
|
1145
|
-
branch: summarizeBranchTail(ctx),
|
|
1146
|
-
});
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
debugExtensionLog("extension.debug_hooks_registered", {
|
|
1150
|
-
logFile: getExtensionDebugLogFilePath(),
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
export default async function (pi: ExtensionAPI) {
|
|
1155
|
-
// Current access token, updated by login/refresh/getApiKey
|
|
1156
|
-
let currentToken = "";
|
|
1157
|
-
let currentTokenSource: CredentialSource | "none" = "none";
|
|
1158
|
-
let noReasoningEffortByModelId = new Map<string, string>();
|
|
1159
|
-
let rawModelByEffortByModelId = new Map<string, Record<string, CursorModelRouting>>();
|
|
1160
|
-
let lastRegisteredModels: ProcessedModel[] = [];
|
|
1161
|
-
|
|
1162
|
-
const getAccessToken = async () => {
|
|
1163
|
-
if (!currentToken) {
|
|
1164
|
-
const resolved = await getStartupCursorAccessToken();
|
|
1165
|
-
if (resolved) {
|
|
1166
|
-
currentToken = resolved.accessToken;
|
|
1167
|
-
currentTokenSource = resolved.source;
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
if (!currentToken)
|
|
1171
|
-
throw new Error("Not logged in to Cursor. Run /login cursor or log in via Cursor CLI");
|
|
1172
|
-
return currentToken;
|
|
1173
|
-
};
|
|
1174
|
-
|
|
1175
|
-
const skipDedup = !!process.env.PI_CURSOR_RAW_MODELS;
|
|
1176
|
-
|
|
1177
|
-
registerSessionLifecycleCleanup(pi);
|
|
1178
|
-
registerExtensionDebugHooks(pi);
|
|
1179
|
-
debugExtensionLog("extension.start", {
|
|
1180
|
-
mode: "native-streamSimple",
|
|
1181
|
-
debugLogFile: isExtensionDebugEnabled() ? getExtensionDebugLogFilePath() : undefined,
|
|
1182
|
-
});
|
|
1183
|
-
|
|
1184
|
-
const startupModels = await discoverStartupModels();
|
|
1185
|
-
register(pi, startupModels.rawModels, startupModels.parameterizedModels);
|
|
1186
|
-
|
|
1187
|
-
pi.registerCommand("cursor.models", {
|
|
1188
|
-
description: "List Cursor runtime models registered by this provider",
|
|
1189
|
-
handler: async (args, ctx: ExtensionCommandContext) => {
|
|
1190
|
-
const all = /\ball\b/i.test(args || "");
|
|
1191
|
-
const rows = all
|
|
1192
|
-
? lastRegisteredModels
|
|
1193
|
-
: lastRegisteredModels.filter((m) => !/tab_|chat_/i.test(m.id));
|
|
1194
|
-
const lines = [
|
|
1195
|
-
`Cursor models (${rows.length}${all ? " all" : ""})`,
|
|
1196
|
-
`endpoint=${getCursorAgentUrl()}`,
|
|
1197
|
-
"",
|
|
1198
|
-
];
|
|
1199
|
-
const maxId = Math.max(8, ...rows.map((m) => m.id.length));
|
|
1200
|
-
for (const m of rows) {
|
|
1201
|
-
const processed = m as ProcessedModel;
|
|
1202
|
-
const levels = Object.entries(processed.effortMap ?? {})
|
|
1203
|
-
.filter(([, cursorEffort]) => typeof cursorEffort === "string")
|
|
1204
|
-
.map(([level]) => level)
|
|
1205
|
-
.join("/");
|
|
1206
|
-
const flags = [
|
|
1207
|
-
processed.supportsEffort ? "thinking" : "",
|
|
1208
|
-
levels ? `levels=${levels}` : "",
|
|
1209
|
-
m.supportsImages ? "images" : "",
|
|
1210
|
-
]
|
|
1211
|
-
.filter(Boolean)
|
|
1212
|
-
.join(",");
|
|
1213
|
-
lines.push(
|
|
1214
|
-
`${m.id.padEnd(maxId)} ctx ${String(m.contextWindow ?? "?").padStart(7)} ${m.name || ""}${flags ? ` [${flags}]` : ""}`,
|
|
1215
|
-
);
|
|
1216
|
-
}
|
|
1217
|
-
if (!rows.length) lines.push("No models registered. Run /login cursor first.");
|
|
1218
|
-
const text = lines.join("\n");
|
|
1219
|
-
if (ctx.hasUI) ctx.ui.notify(text, "info");
|
|
1220
|
-
console.log(text);
|
|
1221
|
-
},
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
pi.registerCommand("cursor.usage", {
|
|
1225
|
-
description: "Show Cursor plan quota and on-demand spend",
|
|
1226
|
-
handler: async (_args, ctx: ExtensionCommandContext) => {
|
|
1227
|
-
try {
|
|
1228
|
-
const text = formatCursorUsage(await getCursorUsageSummary(getAccessToken));
|
|
1229
|
-
if (ctx.hasUI) ctx.ui.notify(text, "info");
|
|
1230
|
-
console.log(text);
|
|
1231
|
-
} catch (error) {
|
|
1232
|
-
const text = `Cursor usage unavailable: ${redactSecrets(
|
|
1233
|
-
error instanceof Error ? error.message : String(error),
|
|
1234
|
-
)}`;
|
|
1235
|
-
if (ctx.hasUI) ctx.ui.notify(text, "error");
|
|
1236
|
-
console.error(text);
|
|
1237
|
-
}
|
|
1238
|
-
},
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
pi.registerCommand("cursor.doctor", {
|
|
1242
|
-
description: "Show sanitized Cursor provider diagnostics",
|
|
1243
|
-
handler: async (_args, ctx: ExtensionCommandContext) => {
|
|
1244
|
-
const d = getLastDiagnostics();
|
|
1245
|
-
const lines = [
|
|
1246
|
-
`provider=${CURSOR_PROVIDER_ID}`,
|
|
1247
|
-
`agentUrl=${getCursorAgentUrl()}`,
|
|
1248
|
-
`tokenSource=${currentTokenSource || "none"}`,
|
|
1249
|
-
`lastResolvedRuntimeModel=${d.resolvedRuntimeModel || "none"}`,
|
|
1250
|
-
`availableModels=${d.availableModels || lastRegisteredModels.length || "none"}`,
|
|
1251
|
-
`matchedModel=${d.matchedModelDebug || "none"}`,
|
|
1252
|
-
`lastEndpoint=${d.endpoint || "none"}`,
|
|
1253
|
-
`lastStatus=${d.status ?? "none"}`,
|
|
1254
|
-
`lastRpc=${d.lastRpc || "none"}`,
|
|
1255
|
-
`lastError=${d.error ? redactSecrets(d.error) : "none"}`,
|
|
1256
|
-
"transport=native-streamSimple",
|
|
1257
|
-
"runtimeCli=not-used",
|
|
1258
|
-
"commands=/cursor.models /cursor.usage /cursor.doctor",
|
|
1259
|
-
];
|
|
1260
|
-
const text = lines.join("\n");
|
|
1261
|
-
if (ctx.hasUI) ctx.ui.notify(`Cursor doctor\n${text}`, "info");
|
|
1262
|
-
console.log(text);
|
|
1263
|
-
},
|
|
1264
|
-
});
|
|
1265
|
-
|
|
1266
|
-
async function discoverStartupModels(): Promise<{
|
|
1267
|
-
rawModels: CursorModel[];
|
|
1268
|
-
parameterizedModels: CursorParameterizedModel[];
|
|
1269
|
-
}> {
|
|
1270
|
-
if (process.env.PI_OFFLINE) return { rawModels: FALLBACK_MODELS, parameterizedModels: [] };
|
|
1271
|
-
|
|
1272
|
-
let startupToken: { accessToken: string; source: CredentialSource } | undefined;
|
|
1273
|
-
try {
|
|
1274
|
-
startupToken = await getStartupCursorAccessToken();
|
|
1275
|
-
} catch (err) {
|
|
1276
|
-
debugExtensionLog("model_discovery.startup.token_failed", {
|
|
1277
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
if (!startupToken) {
|
|
1282
|
-
debugExtensionLog("model_discovery.startup.skipped", { reason: "no_cursor_oauth_token" });
|
|
1283
|
-
return { rawModels: FALLBACK_MODELS, parameterizedModels: [] };
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
try {
|
|
1287
|
-
currentToken = startupToken.accessToken;
|
|
1288
|
-
currentTokenSource = startupToken.source;
|
|
1289
|
-
const [discovered, parameterized] = await Promise.all([
|
|
1290
|
-
getCursorModels(startupToken.accessToken),
|
|
1291
|
-
getCursorParameterizedModels(startupToken.accessToken),
|
|
1292
|
-
]);
|
|
1293
|
-
debugExtensionLog("model_discovery.startup", {
|
|
1294
|
-
tokenSource: startupToken.source,
|
|
1295
|
-
discoveredCount: discovered.length,
|
|
1296
|
-
parameterizedCount: parameterized.length,
|
|
1297
|
-
});
|
|
1298
|
-
if (discovered.length > 0 || parameterized.length > 0) {
|
|
1299
|
-
return {
|
|
1300
|
-
rawModels: discovered.length > 0 ? discovered : FALLBACK_MODELS,
|
|
1301
|
-
parameterizedModels: parameterized,
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
} catch (err) {
|
|
1305
|
-
debugExtensionLog("model_discovery.startup.failed", {
|
|
1306
|
-
tokenSource: startupToken.source,
|
|
1307
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1308
|
-
});
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
return { rawModels: FALLBACK_MODELS, parameterizedModels: [] };
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
function register(
|
|
1315
|
-
pi: ExtensionAPI,
|
|
1316
|
-
rawModels: CursorModel[],
|
|
1317
|
-
parameterizedModels: CursorParameterizedModel[] = [],
|
|
1318
|
-
) {
|
|
1319
|
-
const augmentedModels = augmentCursorModels(rawModels, parameterizedModels);
|
|
1320
|
-
const processed = skipDedup
|
|
1321
|
-
? augmentedModels.map((m) => ({ ...m, supportsEffort: false }) as ProcessedModel)
|
|
1322
|
-
: processModels(augmentedModels);
|
|
1323
|
-
lastRegisteredModels = processed;
|
|
1324
|
-
setLastAvailableModels(
|
|
1325
|
-
processed
|
|
1326
|
-
.map((m) => m.id)
|
|
1327
|
-
.slice(0, 24)
|
|
1328
|
-
.join(","),
|
|
1329
|
-
);
|
|
1330
|
-
noReasoningEffortByModelId = buildNoReasoningEffortLookup(processed);
|
|
1331
|
-
rawModelByEffortByModelId = buildRawModelLookup(processed);
|
|
1332
|
-
|
|
1333
|
-
pi.registerProvider("cursor", {
|
|
1334
|
-
baseUrl: getCursorAgentUrl(),
|
|
1335
|
-
api: "cursor-native",
|
|
1336
|
-
streamSimple: createCursorNativeStream({
|
|
1337
|
-
getAccessToken,
|
|
1338
|
-
getNoReasoningEffortByModelId: () => noReasoningEffortByModelId,
|
|
1339
|
-
getRawModelRoutingByModelId: () => rawModelByEffortByModelId,
|
|
1340
|
-
}),
|
|
1341
|
-
models: processed.map(modelConfig),
|
|
1342
|
-
oauth: {
|
|
1343
|
-
name: "Cursor",
|
|
1344
|
-
|
|
1345
|
-
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
1346
|
-
const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
|
|
1347
|
-
callbacks.onAuth({ url: loginUrl });
|
|
1348
|
-
const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
|
|
1349
|
-
currentToken = accessToken;
|
|
1350
|
-
currentTokenSource = "pi_oauth";
|
|
1351
|
-
|
|
1352
|
-
// Discover real models and re-register
|
|
1353
|
-
const [discovered, parameterized] = await Promise.all([
|
|
1354
|
-
getCursorModels(accessToken),
|
|
1355
|
-
getCursorParameterizedModels(accessToken),
|
|
1356
|
-
]);
|
|
1357
|
-
if (discovered.length > 0 || parameterized.length > 0) {
|
|
1358
|
-
register(pi, discovered.length > 0 ? discovered : FALLBACK_MODELS, parameterized);
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
return {
|
|
1362
|
-
refresh: refreshToken,
|
|
1363
|
-
access: accessToken,
|
|
1364
|
-
expires: getTokenExpiry(accessToken),
|
|
1365
|
-
};
|
|
1366
|
-
},
|
|
1367
|
-
|
|
1368
|
-
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
1369
|
-
const refreshed = await refreshCursorToken(credentials.refresh);
|
|
1370
|
-
currentToken = refreshed.access;
|
|
1371
|
-
currentTokenSource = "pi_oauth_refresh";
|
|
1372
|
-
|
|
1373
|
-
// Discover real models on refresh too
|
|
1374
|
-
const [discovered, parameterized] = await Promise.all([
|
|
1375
|
-
getCursorModels(refreshed.access),
|
|
1376
|
-
getCursorParameterizedModels(refreshed.access),
|
|
1377
|
-
]);
|
|
1378
|
-
if (discovered.length > 0 || parameterized.length > 0) {
|
|
1379
|
-
register(pi, discovered.length > 0 ? discovered : FALLBACK_MODELS, parameterized);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
return refreshed as OAuthCredentials;
|
|
1383
|
-
},
|
|
1384
|
-
|
|
1385
|
-
getApiKey(credentials: OAuthCredentials): string {
|
|
1386
|
-
currentToken = credentials.access;
|
|
1387
|
-
currentTokenSource = "pi_oauth";
|
|
1388
|
-
return "cursor-native";
|
|
1389
|
-
},
|
|
1390
|
-
},
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
}
|