pi-cursor-sdk 0.1.8 → 0.1.10
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 +39 -0
- package/README.md +31 -12
- package/docs/cursor-model-ux-spec.md +33 -54
- package/package.json +4 -2
- package/scripts/refresh-cursor-model-snapshots.mjs +234 -0
- package/src/context-window-cache.ts +6 -0
- package/src/context.ts +128 -35
- package/src/cursor-fallback-models.generated.ts +145 -0
- package/src/cursor-native-tool-display.ts +156 -15
- package/src/cursor-provider.ts +137 -20
- package/src/cursor-state.ts +10 -1
- package/src/cursor-tool-transcript.ts +53 -11
- package/src/index.ts +35 -8
- package/src/model-discovery.ts +59 -154
package/src/model-discovery.ts
CHANGED
|
@@ -7,7 +7,8 @@ import type {
|
|
|
7
7
|
} from "@cursor/sdk";
|
|
8
8
|
import { AuthStorage, type ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { ModelThinkingLevel, ThinkingLevelMap } from "@earendil-works/pi-ai";
|
|
10
|
-
import {
|
|
10
|
+
import { loadContextWindowCache } from "./context-window-cache.js";
|
|
11
|
+
import { FALLBACK_MODEL_ITEMS } from "./cursor-fallback-models.generated.js";
|
|
11
12
|
|
|
12
13
|
const CURSOR_PROVIDER_ID = "cursor";
|
|
13
14
|
const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
|
|
@@ -17,154 +18,14 @@ const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
|
17
18
|
const TEXT_AND_IMAGE_INPUT: ProviderModelConfig["input"] = ["text", "image"];
|
|
18
19
|
const AUTH_SETUP_HINT = "/login (Use an API key -> Cursor), CURSOR_API_KEY, or --api-key";
|
|
19
20
|
const CATALOG_REFRESH_HINT =
|
|
20
|
-
"After adding auth to an already-started pi session, run /
|
|
21
|
-
|
|
22
|
-
const FALLBACK_MODEL_ITEMS: ModelListItem[] = [
|
|
23
|
-
{
|
|
24
|
-
id: "composer-2",
|
|
25
|
-
displayName: "Cursor Composer 2",
|
|
26
|
-
parameters: [
|
|
27
|
-
{
|
|
28
|
-
id: "fast",
|
|
29
|
-
displayName: "Fast",
|
|
30
|
-
values: [{ value: "false" }, { value: "true" }],
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
variants: [
|
|
34
|
-
{
|
|
35
|
-
params: [{ id: "fast", value: "true" }],
|
|
36
|
-
displayName: "Cursor Composer 2",
|
|
37
|
-
isDefault: true,
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: "gpt-5.5",
|
|
43
|
-
displayName: "GPT-5.5",
|
|
44
|
-
parameters: [
|
|
45
|
-
{
|
|
46
|
-
id: "context",
|
|
47
|
-
displayName: "Context",
|
|
48
|
-
values: [{ value: "1m" }, { value: "272k" }],
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: "reasoning",
|
|
52
|
-
displayName: "Reasoning",
|
|
53
|
-
values: [
|
|
54
|
-
{ value: "none" },
|
|
55
|
-
{ value: "low" },
|
|
56
|
-
{ value: "medium" },
|
|
57
|
-
{ value: "high" },
|
|
58
|
-
{ value: "extra-high" },
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
id: "fast",
|
|
63
|
-
displayName: "Fast",
|
|
64
|
-
values: [{ value: "false" }, { value: "true" }],
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
variants: [
|
|
68
|
-
{
|
|
69
|
-
params: [
|
|
70
|
-
{ id: "context", value: "1m" },
|
|
71
|
-
{ id: "reasoning", value: "medium" },
|
|
72
|
-
{ id: "fast", value: "false" },
|
|
73
|
-
],
|
|
74
|
-
displayName: "GPT-5.5",
|
|
75
|
-
isDefault: true,
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: "claude-sonnet-4-6",
|
|
81
|
-
displayName: "Sonnet 4.6",
|
|
82
|
-
parameters: [
|
|
83
|
-
{
|
|
84
|
-
id: "thinking",
|
|
85
|
-
displayName: "Thinking",
|
|
86
|
-
values: [{ value: "false" }, { value: "true" }],
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
id: "context",
|
|
90
|
-
displayName: "Context",
|
|
91
|
-
values: [{ value: "1m" }, { value: "300k" }],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: "effort",
|
|
95
|
-
displayName: "Effort",
|
|
96
|
-
values: [
|
|
97
|
-
{ value: "low" },
|
|
98
|
-
{ value: "medium" },
|
|
99
|
-
{ value: "high" },
|
|
100
|
-
{ value: "xhigh" },
|
|
101
|
-
{ value: "max" },
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
id: "fast",
|
|
106
|
-
displayName: "Fast",
|
|
107
|
-
values: [{ value: "false" }, { value: "true" }],
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
variants: [
|
|
111
|
-
{
|
|
112
|
-
params: [
|
|
113
|
-
{ id: "thinking", value: "true" },
|
|
114
|
-
{ id: "context", value: "1m" },
|
|
115
|
-
{ id: "effort", value: "medium" },
|
|
116
|
-
{ id: "fast", value: "false" },
|
|
117
|
-
],
|
|
118
|
-
displayName: "Sonnet 4.6",
|
|
119
|
-
isDefault: true,
|
|
120
|
-
},
|
|
121
|
-
],
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
id: "claude-opus-4-7",
|
|
125
|
-
displayName: "Opus 4.7",
|
|
126
|
-
parameters: [
|
|
127
|
-
{
|
|
128
|
-
id: "thinking",
|
|
129
|
-
displayName: "Thinking",
|
|
130
|
-
values: [{ value: "false" }, { value: "true" }],
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
id: "context",
|
|
134
|
-
displayName: "Context",
|
|
135
|
-
values: [{ value: "1m" }, { value: "300k" }],
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
id: "effort",
|
|
139
|
-
displayName: "Effort",
|
|
140
|
-
values: [
|
|
141
|
-
{ value: "low" },
|
|
142
|
-
{ value: "medium" },
|
|
143
|
-
{ value: "high" },
|
|
144
|
-
{ value: "xhigh" },
|
|
145
|
-
{ value: "max" },
|
|
146
|
-
],
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
variants: [
|
|
150
|
-
{
|
|
151
|
-
params: [
|
|
152
|
-
{ id: "thinking", value: "true" },
|
|
153
|
-
{ id: "context", value: "1m" },
|
|
154
|
-
{ id: "effort", value: "xhigh" },
|
|
155
|
-
],
|
|
156
|
-
displayName: "Opus 4.7",
|
|
157
|
-
isDefault: true,
|
|
158
|
-
},
|
|
159
|
-
],
|
|
160
|
-
},
|
|
161
|
-
];
|
|
21
|
+
"After adding auth to an already-started pi session, run /cursor-refresh-models to refresh the full live Cursor model catalog without restarting pi.";
|
|
162
22
|
|
|
163
23
|
export type CursorModelFallbackReason = "missing-api-key" | "discovery-failed" | "empty-model-list";
|
|
164
24
|
|
|
165
25
|
export interface CursorModelFallbackIssue {
|
|
166
26
|
reason: CursorModelFallbackReason;
|
|
167
27
|
message: string;
|
|
28
|
+
errorMessage?: string;
|
|
168
29
|
}
|
|
169
30
|
|
|
170
31
|
export interface DiscoverModelsOptions {
|
|
@@ -351,9 +212,14 @@ function getModelName(item: ModelListItem, context?: string, alias?: string): st
|
|
|
351
212
|
return context ? `${baseName} @ ${context}` : baseName;
|
|
352
213
|
}
|
|
353
214
|
|
|
354
|
-
function getContextWindow(piModelId: string, context?: string, baseModelId?: string): number {
|
|
355
|
-
|
|
356
|
-
|
|
215
|
+
function getContextWindow(contextWindowCache: Map<string, number>, piModelId: string, context?: string, baseModelId?: string): number {
|
|
216
|
+
return (
|
|
217
|
+
contextWindowCache.get(piModelId) ??
|
|
218
|
+
(context ? parseContextWindow(context) : undefined) ??
|
|
219
|
+
(baseModelId ? contextWindowCache.get(baseModelId) : undefined) ??
|
|
220
|
+
contextWindowCache.get("default") ??
|
|
221
|
+
FALLBACK_CONTEXT_WINDOW
|
|
222
|
+
);
|
|
357
223
|
}
|
|
358
224
|
|
|
359
225
|
function toMetadata(
|
|
@@ -362,6 +228,7 @@ function toMetadata(
|
|
|
362
228
|
selectionModelId: string,
|
|
363
229
|
defaultParams: ModelParameterValue[],
|
|
364
230
|
context: string | undefined,
|
|
231
|
+
contextWindowCache: Map<string, number>,
|
|
365
232
|
): CursorModelMetadata {
|
|
366
233
|
const thinkingLevelMap = getThinkingLevelMap(item);
|
|
367
234
|
const fastValue = getParamValue(defaultParams, "fast")?.toLowerCase();
|
|
@@ -372,7 +239,7 @@ function toMetadata(
|
|
|
372
239
|
displayName: item.displayName || item.id,
|
|
373
240
|
defaultParams: cloneParams(defaultParams),
|
|
374
241
|
...(context ? { context } : {}),
|
|
375
|
-
contextWindow: getContextWindow(piModelId, context, item.id),
|
|
242
|
+
contextWindow: getContextWindow(contextWindowCache, piModelId, context, item.id),
|
|
376
243
|
supportsFast: getParameter(item, "fast") !== undefined,
|
|
377
244
|
defaultFast: fastValue === "true",
|
|
378
245
|
supportsReasoning: thinkingLevelMap !== undefined,
|
|
@@ -404,11 +271,25 @@ function getContextValues(item: ModelListItem): string[] {
|
|
|
404
271
|
return getParameter(item, "context")?.values.map((value) => value.value) ?? [];
|
|
405
272
|
}
|
|
406
273
|
|
|
407
|
-
function
|
|
274
|
+
function getAmbiguousAliases(items: ModelListItem[]): Set<string> {
|
|
275
|
+
const aliasOwners = new Map<string, Set<string>>();
|
|
276
|
+
for (const item of items) {
|
|
277
|
+
for (const rawAlias of item.aliases ?? []) {
|
|
278
|
+
const alias = rawAlias.trim();
|
|
279
|
+
if (!alias || alias === item.id) continue;
|
|
280
|
+
const owners = aliasOwners.get(alias) ?? new Set<string>();
|
|
281
|
+
owners.add(item.id);
|
|
282
|
+
aliasOwners.set(alias, owners);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return new Set([...aliasOwners.entries()].filter(([, owners]) => owners.size > 1).map(([alias]) => alias));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getModelIds(item: ModelListItem, reservedBaseModelIds: Set<string>, ambiguousAliases: Set<string>): string[] {
|
|
408
289
|
const ids = [item.id];
|
|
409
290
|
for (const rawAlias of item.aliases ?? []) {
|
|
410
291
|
const alias = rawAlias.trim();
|
|
411
|
-
if (!alias || alias === item.id || ids.includes(alias) || reservedBaseModelIds.has(alias)) continue;
|
|
292
|
+
if (!alias || alias === item.id || ids.includes(alias) || reservedBaseModelIds.has(alias) || ambiguousAliases.has(alias)) continue;
|
|
412
293
|
ids.push(alias);
|
|
413
294
|
}
|
|
414
295
|
return ids;
|
|
@@ -418,20 +299,22 @@ function toModelConfigs(
|
|
|
418
299
|
item: ModelListItem,
|
|
419
300
|
usedPiModelIds: Set<string>,
|
|
420
301
|
reservedBaseModelIds: Set<string>,
|
|
302
|
+
ambiguousAliases: Set<string>,
|
|
303
|
+
contextWindowCache: Map<string, number>,
|
|
421
304
|
): ProviderModelConfig[] {
|
|
422
305
|
const defaultParams = getDefaultParams(item);
|
|
423
306
|
const contextValues = getContextValues(item);
|
|
424
307
|
const contexts = contextValues.length > 0 ? contextValues : [undefined];
|
|
425
308
|
const configs: ProviderModelConfig[] = [];
|
|
426
309
|
|
|
427
|
-
for (const selectionModelId of getModelIds(item, reservedBaseModelIds)) {
|
|
310
|
+
for (const selectionModelId of getModelIds(item, reservedBaseModelIds, ambiguousAliases)) {
|
|
428
311
|
const alias = selectionModelId === item.id ? undefined : selectionModelId;
|
|
429
312
|
for (const context of contexts) {
|
|
430
313
|
const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
|
|
431
314
|
const piModelId = encodePiModelId(selectionModelId, context);
|
|
432
315
|
if (usedPiModelIds.has(piModelId)) continue;
|
|
433
316
|
usedPiModelIds.add(piModelId);
|
|
434
|
-
const metadata = toMetadata(item, piModelId, selectionModelId, params, context);
|
|
317
|
+
const metadata = toMetadata(item, piModelId, selectionModelId, params, context, contextWindowCache);
|
|
435
318
|
metadataByPiModelId.set(piModelId, metadata);
|
|
436
319
|
configs.push(toModelConfig(metadata, getModelName(item, context, alias)));
|
|
437
320
|
}
|
|
@@ -448,7 +331,9 @@ function registerModelItems(items: ModelListItem[]): ProviderModelConfig[] {
|
|
|
448
331
|
metadataByPiModelId.clear();
|
|
449
332
|
const usedPiModelIds = new Set<string>();
|
|
450
333
|
const reservedBaseModelIds = new Set(items.map((item) => item.id));
|
|
451
|
-
|
|
334
|
+
const ambiguousAliases = getAmbiguousAliases(items);
|
|
335
|
+
const contextWindowCache = loadContextWindowCache();
|
|
336
|
+
return sortModelsByBaseId(items).flatMap((item) => toModelConfigs(item, usedPiModelIds, reservedBaseModelIds, ambiguousAliases, contextWindowCache));
|
|
452
337
|
}
|
|
453
338
|
|
|
454
339
|
export function getCursorModelMetadata(modelId: string): CursorModelMetadata | undefined {
|
|
@@ -532,6 +417,24 @@ export function buildCursorModelSelection(
|
|
|
532
417
|
return params.length > 0 ? { id: metadata.selectionModelId, params } : { id: metadata.selectionModelId };
|
|
533
418
|
}
|
|
534
419
|
|
|
420
|
+
function scrubDiscoveryErrorText(text: string, apiKey: string): string {
|
|
421
|
+
let scrubbed = text.replace(new RegExp(apiKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "[redacted]");
|
|
422
|
+
return scrubbed
|
|
423
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
|
|
424
|
+
.replace(/((?:^|[\s,{])cookie["']?\s*[:=]\s*["']?)[^\n]+/gi, "$1[redacted]")
|
|
425
|
+
.replace(
|
|
426
|
+
/((?:authorization|api[_-]?key|apiKey|token|session(?:[_-]?id)?)["']?\s*[:=]\s*["']?)[^"'\s,;}]+/gi,
|
|
427
|
+
"$1[redacted]",
|
|
428
|
+
)
|
|
429
|
+
.trim();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function sanitizeDiscoveryError(error: unknown, apiKey: string): string | undefined {
|
|
433
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
434
|
+
const scrubbed = scrubDiscoveryErrorText(message, apiKey);
|
|
435
|
+
return scrubbed || undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
535
438
|
function useFallbackModels(options: DiscoverModelsOptions, issue: CursorModelFallbackIssue): ProviderModelConfig[] {
|
|
536
439
|
options.onFallback?.(issue);
|
|
537
440
|
return registerModelItems(FALLBACK_MODEL_ITEMS);
|
|
@@ -555,10 +458,12 @@ export async function discoverModels(options: DiscoverModelsOptions = {}): Promi
|
|
|
555
458
|
reason: "empty-model-list",
|
|
556
459
|
message: `Cursor model discovery returned no models. Using fallback Cursor models; verify ${AUTH_SETUP_HINT}. ${CATALOG_REFRESH_HINT}`,
|
|
557
460
|
});
|
|
558
|
-
} catch {
|
|
461
|
+
} catch (error) {
|
|
462
|
+
const errorMessage = sanitizeDiscoveryError(error, apiKey);
|
|
559
463
|
return useFallbackModels(options, {
|
|
560
464
|
reason: "discovery-failed",
|
|
561
|
-
message: `Cursor model discovery failed. Using fallback Cursor models; verify ${AUTH_SETUP_HINT}. ${CATALOG_REFRESH_HINT}`,
|
|
465
|
+
message: `Cursor model discovery failed${errorMessage ? `: ${errorMessage}` : ""}. Using fallback Cursor models; verify ${AUTH_SETUP_HINT}. ${CATALOG_REFRESH_HINT}`,
|
|
466
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
562
467
|
});
|
|
563
468
|
}
|
|
564
469
|
}
|