pi-cursor-sdk 0.0.0 → 0.1.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.
@@ -0,0 +1,519 @@
1
+ import { Cursor } from "@cursor/sdk";
2
+ import type {
3
+ ModelListItem,
4
+ ModelParameterDefinition,
5
+ ModelParameterValue,
6
+ ModelSelection,
7
+ } from "@cursor/sdk";
8
+ import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
9
+ import type { ModelThinkingLevel, ThinkingLevelMap } from "@mariozechner/pi-ai";
10
+ import { getCachedContextWindow } from "./context-window-cache.js";
11
+
12
+ const FALLBACK_CONTEXT_WINDOW = 128000;
13
+ const FALLBACK_MAX_TOKENS = 16384;
14
+ const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
15
+ const TEXT_AND_IMAGE_INPUT: ProviderModelConfig["input"] = ["text", "image"];
16
+
17
+ const FALLBACK_MODEL_ITEMS: ModelListItem[] = [
18
+ {
19
+ id: "composer-2",
20
+ displayName: "Cursor Composer 2",
21
+ parameters: [
22
+ {
23
+ id: "fast",
24
+ displayName: "Fast",
25
+ values: [{ value: "false" }, { value: "true" }],
26
+ },
27
+ ],
28
+ variants: [
29
+ {
30
+ params: [{ id: "fast", value: "true" }],
31
+ displayName: "Cursor Composer 2",
32
+ isDefault: true,
33
+ },
34
+ ],
35
+ },
36
+ {
37
+ id: "gpt-5.5",
38
+ displayName: "GPT-5.5",
39
+ parameters: [
40
+ {
41
+ id: "context",
42
+ displayName: "Context",
43
+ values: [{ value: "1m" }, { value: "272k" }],
44
+ },
45
+ {
46
+ id: "reasoning",
47
+ displayName: "Reasoning",
48
+ values: [
49
+ { value: "none" },
50
+ { value: "low" },
51
+ { value: "medium" },
52
+ { value: "high" },
53
+ { value: "extra-high" },
54
+ ],
55
+ },
56
+ {
57
+ id: "fast",
58
+ displayName: "Fast",
59
+ values: [{ value: "false" }, { value: "true" }],
60
+ },
61
+ ],
62
+ variants: [
63
+ {
64
+ params: [
65
+ { id: "context", value: "1m" },
66
+ { id: "reasoning", value: "medium" },
67
+ { id: "fast", value: "false" },
68
+ ],
69
+ displayName: "GPT-5.5",
70
+ isDefault: true,
71
+ },
72
+ ],
73
+ },
74
+ {
75
+ id: "claude-sonnet-4-6",
76
+ displayName: "Sonnet 4.6",
77
+ parameters: [
78
+ {
79
+ id: "thinking",
80
+ displayName: "Thinking",
81
+ values: [{ value: "false" }, { value: "true" }],
82
+ },
83
+ {
84
+ id: "context",
85
+ displayName: "Context",
86
+ values: [{ value: "1m" }, { value: "300k" }],
87
+ },
88
+ {
89
+ id: "effort",
90
+ displayName: "Effort",
91
+ values: [
92
+ { value: "low" },
93
+ { value: "medium" },
94
+ { value: "high" },
95
+ { value: "xhigh" },
96
+ { value: "max" },
97
+ ],
98
+ },
99
+ {
100
+ id: "fast",
101
+ displayName: "Fast",
102
+ values: [{ value: "false" }, { value: "true" }],
103
+ },
104
+ ],
105
+ variants: [
106
+ {
107
+ params: [
108
+ { id: "thinking", value: "true" },
109
+ { id: "context", value: "1m" },
110
+ { id: "effort", value: "medium" },
111
+ { id: "fast", value: "false" },
112
+ ],
113
+ displayName: "Sonnet 4.6",
114
+ isDefault: true,
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ id: "claude-opus-4-7",
120
+ displayName: "Opus 4.7",
121
+ parameters: [
122
+ {
123
+ id: "thinking",
124
+ displayName: "Thinking",
125
+ values: [{ value: "false" }, { value: "true" }],
126
+ },
127
+ {
128
+ id: "context",
129
+ displayName: "Context",
130
+ values: [{ value: "1m" }, { value: "300k" }],
131
+ },
132
+ {
133
+ id: "effort",
134
+ displayName: "Effort",
135
+ values: [
136
+ { value: "low" },
137
+ { value: "medium" },
138
+ { value: "high" },
139
+ { value: "xhigh" },
140
+ { value: "max" },
141
+ ],
142
+ },
143
+ ],
144
+ variants: [
145
+ {
146
+ params: [
147
+ { id: "thinking", value: "true" },
148
+ { id: "context", value: "1m" },
149
+ { id: "effort", value: "xhigh" },
150
+ ],
151
+ displayName: "Opus 4.7",
152
+ isDefault: true,
153
+ },
154
+ ],
155
+ },
156
+ ];
157
+
158
+ export type CursorModelFallbackReason = "missing-api-key" | "discovery-failed" | "empty-model-list";
159
+
160
+ export interface CursorModelFallbackIssue {
161
+ reason: CursorModelFallbackReason;
162
+ message: string;
163
+ }
164
+
165
+ export interface DiscoverModelsOptions {
166
+ onFallback?: (issue: CursorModelFallbackIssue) => void;
167
+ }
168
+
169
+ function getCliApiKeyFromArgv(argv: string[] = process.argv): string | undefined {
170
+ for (let index = 0; index < argv.length; index++) {
171
+ const arg = argv[index];
172
+ if (arg === "--api-key") {
173
+ const value = argv[index + 1];
174
+ if (!value || value.startsWith("--")) return undefined;
175
+ const trimmed = value.trim();
176
+ return trimmed || undefined;
177
+ }
178
+ const prefix = "--api-key=";
179
+ if (arg.startsWith(prefix)) {
180
+ const trimmed = arg.slice(prefix.length).trim();
181
+ return trimmed || undefined;
182
+ }
183
+ }
184
+ return undefined;
185
+ }
186
+
187
+ function getDiscoveryApiKey(): string | undefined {
188
+ return process.env.CURSOR_API_KEY?.trim() || getCliApiKeyFromArgv();
189
+ }
190
+
191
+ export interface CursorModelMetadata {
192
+ piModelId: string;
193
+ baseModelId: string;
194
+ displayName: string;
195
+ defaultParams: ModelParameterValue[];
196
+ context?: string;
197
+ contextWindow: number;
198
+ supportsFast: boolean;
199
+ defaultFast: boolean;
200
+ supportsReasoning: boolean;
201
+ thinkingLevelMap?: ThinkingLevelMap;
202
+ parameterIds: {
203
+ context: boolean;
204
+ reasoning: boolean;
205
+ effort: boolean;
206
+ thinking: boolean;
207
+ fast: boolean;
208
+ };
209
+ }
210
+
211
+ const metadataByPiModelId = new Map<string, CursorModelMetadata>();
212
+
213
+ function cloneParams(params: ModelParameterValue[]): ModelParameterValue[] {
214
+ return params.map((param) => ({ ...param }));
215
+ }
216
+
217
+ function getParameter(item: ModelListItem, id: string): ModelParameterDefinition | undefined {
218
+ return item.parameters?.find((parameter) => parameter.id === id);
219
+ }
220
+
221
+ function hasBooleanValues(parameter: ModelParameterDefinition | undefined): boolean {
222
+ const values = new Set((parameter?.values ?? []).map((value) => value.value.toLowerCase()));
223
+ return values.has("false") && values.has("true");
224
+ }
225
+
226
+ function getParameterValue(parameter: ModelParameterDefinition | undefined, lowerValue: string): string | null {
227
+ const value = parameter?.values.find((candidate) => candidate.value.toLowerCase() === lowerValue);
228
+ return value?.value ?? null;
229
+ }
230
+
231
+ function getPreferredParameterValue(
232
+ parameter: ModelParameterDefinition | undefined,
233
+ lowerValues: string[],
234
+ ): string | null {
235
+ for (const value of lowerValues) {
236
+ const candidate = getParameterValue(parameter, value);
237
+ if (candidate) return candidate;
238
+ }
239
+ return null;
240
+ }
241
+
242
+ function mapComparableLevel(
243
+ parameter: ModelParameterDefinition | undefined,
244
+ level: Exclude<ModelThinkingLevel, "off">,
245
+ ): string | null {
246
+ if (level === "xhigh") {
247
+ return getPreferredParameterValue(parameter, ["xhigh", "max", "extra-high"]);
248
+ }
249
+ return getParameterValue(parameter, level);
250
+ }
251
+
252
+ function getThinkingLevelMap(item: ModelListItem): ThinkingLevelMap | undefined {
253
+ const reasoningParameter = getParameter(item, "reasoning");
254
+ const effortParameter = getParameter(item, "effort");
255
+ const thinkingParameter = getParameter(item, "thinking");
256
+ const valueParameter = effortParameter ?? reasoningParameter ?? thinkingParameter;
257
+ if (!valueParameter) return undefined;
258
+
259
+ if (valueParameter.id === "thinking" && hasBooleanValues(valueParameter)) {
260
+ return {
261
+ off: getParameterValue(valueParameter, "false"),
262
+ minimal: null,
263
+ low: null,
264
+ medium: null,
265
+ high: getParameterValue(valueParameter, "true"),
266
+ xhigh: null,
267
+ };
268
+ }
269
+
270
+ return {
271
+ off:
272
+ getParameterValue(reasoningParameter, "none") ??
273
+ getParameterValue(reasoningParameter, "off") ??
274
+ getParameterValue(thinkingParameter, "false"),
275
+ minimal: mapComparableLevel(valueParameter, "minimal"),
276
+ low: mapComparableLevel(valueParameter, "low"),
277
+ medium: mapComparableLevel(valueParameter, "medium"),
278
+ high: mapComparableLevel(valueParameter, "high"),
279
+ xhigh: mapComparableLevel(valueParameter, "xhigh"),
280
+ };
281
+ }
282
+
283
+ function parseContextWindow(value: string): number | undefined {
284
+ const match = /^(\d+(?:\.\d+)?)([km])$/i.exec(value.trim());
285
+ if (!match) return undefined;
286
+ const amount = Number(match[1]);
287
+ const unit = match[2]?.toLowerCase();
288
+ if (!Number.isFinite(amount)) return undefined;
289
+ return Math.round(amount * (unit === "m" ? 1000000 : 1000));
290
+ }
291
+
292
+ function getDefaultParams(item: ModelListItem): ModelParameterValue[] {
293
+ if (!item.variants?.length) return [];
294
+ const defaultVariant = item.variants.find((variant) => variant.isDefault) ?? item.variants[0];
295
+ return cloneParams(defaultVariant?.params ?? []);
296
+ }
297
+
298
+ function replaceParam(
299
+ params: ModelParameterValue[],
300
+ id: string,
301
+ value: string,
302
+ ): ModelParameterValue[] {
303
+ let replaced = false;
304
+ const next = params.map((param) => {
305
+ if (param.id !== id) return { ...param };
306
+ replaced = true;
307
+ return { id, value };
308
+ });
309
+ if (!replaced) next.push({ id, value });
310
+ return next;
311
+ }
312
+
313
+ function getParamValue(params: ModelParameterValue[], id: string): string | undefined {
314
+ return params.find((param) => param.id === id)?.value;
315
+ }
316
+
317
+ function encodePiModelId(baseModelId: string, context?: string): string {
318
+ return context ? `${baseModelId}@${context}` : baseModelId;
319
+ }
320
+
321
+ function getModelName(item: ModelListItem, context?: string): string {
322
+ const displayName = item.displayName || item.id;
323
+ return context ? `${displayName} @ ${context}` : displayName;
324
+ }
325
+
326
+ function getContextWindow(piModelId: string, context?: string): number {
327
+ if (context) return parseContextWindow(context) ?? FALLBACK_CONTEXT_WINDOW;
328
+ return getCachedContextWindow(piModelId) ?? FALLBACK_CONTEXT_WINDOW;
329
+ }
330
+
331
+ function toMetadata(
332
+ item: ModelListItem,
333
+ piModelId: string,
334
+ defaultParams: ModelParameterValue[],
335
+ context: string | undefined,
336
+ ): CursorModelMetadata {
337
+ const thinkingLevelMap = getThinkingLevelMap(item);
338
+ const fastValue = getParamValue(defaultParams, "fast")?.toLowerCase();
339
+ return {
340
+ piModelId,
341
+ baseModelId: item.id,
342
+ displayName: item.displayName || item.id,
343
+ defaultParams: cloneParams(defaultParams),
344
+ ...(context ? { context } : {}),
345
+ contextWindow: getContextWindow(piModelId, context),
346
+ supportsFast: getParameter(item, "fast") !== undefined,
347
+ defaultFast: fastValue === "true",
348
+ supportsReasoning: thinkingLevelMap !== undefined,
349
+ ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
350
+ parameterIds: {
351
+ context: getParameter(item, "context") !== undefined,
352
+ reasoning: getParameter(item, "reasoning") !== undefined,
353
+ effort: getParameter(item, "effort") !== undefined,
354
+ thinking: getParameter(item, "thinking") !== undefined,
355
+ fast: getParameter(item, "fast") !== undefined,
356
+ },
357
+ };
358
+ }
359
+
360
+ function toModelConfig(metadata: CursorModelMetadata, name: string): ProviderModelConfig {
361
+ return {
362
+ id: metadata.piModelId,
363
+ name,
364
+ reasoning: metadata.supportsReasoning,
365
+ ...(metadata.thinkingLevelMap ? { thinkingLevelMap: metadata.thinkingLevelMap } : {}),
366
+ input: [...TEXT_AND_IMAGE_INPUT],
367
+ cost: { ...ZERO_COST },
368
+ contextWindow: metadata.contextWindow,
369
+ maxTokens: FALLBACK_MAX_TOKENS,
370
+ };
371
+ }
372
+
373
+ function getContextValues(item: ModelListItem): string[] {
374
+ return getParameter(item, "context")?.values.map((value) => value.value) ?? [];
375
+ }
376
+
377
+ function toModelConfigs(item: ModelListItem): ProviderModelConfig[] {
378
+ const defaultParams = getDefaultParams(item);
379
+ const contextValues = getContextValues(item);
380
+ const contexts = contextValues.length > 0 ? contextValues : [undefined];
381
+
382
+ return contexts.map((context) => {
383
+ const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
384
+ const piModelId = encodePiModelId(item.id, context);
385
+ const metadata = toMetadata(item, piModelId, params, context);
386
+ metadataByPiModelId.set(piModelId, metadata);
387
+ return toModelConfig(metadata, getModelName(item, context));
388
+ });
389
+ }
390
+
391
+ function sortModelsByBaseId(items: ModelListItem[]): ModelListItem[] {
392
+ return [...items].sort((a, b) => a.id.localeCompare(b.id));
393
+ }
394
+
395
+ function registerModelItems(items: ModelListItem[]): ProviderModelConfig[] {
396
+ metadataByPiModelId.clear();
397
+ return sortModelsByBaseId(items).flatMap(toModelConfigs);
398
+ }
399
+
400
+ export function getCursorModelMetadata(modelId: string): CursorModelMetadata | undefined {
401
+ return metadataByPiModelId.get(modelId);
402
+ }
403
+
404
+ export function getCursorModelMetadataEntries(): CursorModelMetadata[] {
405
+ return [...metadataByPiModelId.values()].map((metadata) => ({
406
+ ...metadata,
407
+ defaultParams: cloneParams(metadata.defaultParams),
408
+ ...(metadata.thinkingLevelMap ? { thinkingLevelMap: { ...metadata.thinkingLevelMap } } : {}),
409
+ parameterIds: { ...metadata.parameterIds },
410
+ }));
411
+ }
412
+
413
+ function setParam(params: ModelParameterValue[], id: string, value: string): void {
414
+ const existing = params.find((param) => param.id === id);
415
+ if (existing) {
416
+ existing.value = value;
417
+ } else {
418
+ params.push({ id, value });
419
+ }
420
+ }
421
+
422
+ function deleteParam(params: ModelParameterValue[], id: string): void {
423
+ const index = params.findIndex((param) => param.id === id);
424
+ if (index >= 0) params.splice(index, 1);
425
+ }
426
+
427
+ function applyThinkingLevel(
428
+ metadata: CursorModelMetadata,
429
+ params: ModelParameterValue[],
430
+ level: ModelThinkingLevel,
431
+ ): void {
432
+ const mapped = metadata.thinkingLevelMap?.[level];
433
+ if (mapped === undefined || mapped === null) return;
434
+
435
+ if (level === "off") {
436
+ if (metadata.parameterIds.thinking && mapped === "false") {
437
+ setParam(params, "thinking", mapped);
438
+ deleteParam(params, "effort");
439
+ return;
440
+ }
441
+ if (metadata.parameterIds.reasoning) {
442
+ setParam(params, "reasoning", mapped);
443
+ }
444
+ return;
445
+ }
446
+
447
+ if (metadata.parameterIds.effort) {
448
+ if (metadata.parameterIds.thinking) setParam(params, "thinking", "true");
449
+ setParam(params, "effort", mapped);
450
+ return;
451
+ }
452
+
453
+ if (metadata.parameterIds.reasoning) {
454
+ setParam(params, "reasoning", mapped);
455
+ return;
456
+ }
457
+
458
+ if (metadata.parameterIds.thinking) {
459
+ setParam(params, "thinking", mapped);
460
+ }
461
+ }
462
+
463
+ export function buildCursorModelSelection(
464
+ modelId: string,
465
+ thinkingLevel: ModelThinkingLevel,
466
+ fastEnabled?: boolean,
467
+ ): ModelSelection {
468
+ const metadata = getCursorModelMetadata(modelId);
469
+ if (!metadata) return { id: modelId };
470
+
471
+ const params = cloneParams(metadata.defaultParams);
472
+ applyThinkingLevel(metadata, params, thinkingLevel);
473
+
474
+ if (metadata.supportsFast && fastEnabled !== undefined) {
475
+ setParam(params, "fast", fastEnabled ? "true" : "false");
476
+ }
477
+
478
+ return params.length > 0 ? { id: metadata.baseModelId, params } : { id: metadata.baseModelId };
479
+ }
480
+
481
+ function useFallbackModels(options: DiscoverModelsOptions, issue: CursorModelFallbackIssue): ProviderModelConfig[] {
482
+ options.onFallback?.(issue);
483
+ return registerModelItems(FALLBACK_MODEL_ITEMS);
484
+ }
485
+
486
+ export async function discoverModels(options: DiscoverModelsOptions = {}): Promise<ProviderModelConfig[]> {
487
+ const apiKey = getDiscoveryApiKey();
488
+ if (!apiKey) {
489
+ return useFallbackModels(options, {
490
+ reason: "missing-api-key",
491
+ message:
492
+ "CURSOR_API_KEY or --api-key is required for Cursor model discovery. Using fallback Cursor models for selection only; Cursor runs in this session will fail until pi is restarted with a key.",
493
+ });
494
+ }
495
+
496
+ try {
497
+ const models = await Cursor.models.list({ apiKey });
498
+ if (models.length > 0) {
499
+ return registerModelItems(models);
500
+ }
501
+ return useFallbackModels(options, {
502
+ reason: "empty-model-list",
503
+ message:
504
+ "Cursor model discovery returned no models. Using fallback Cursor models for selection only; verify CURSOR_API_KEY or restart pi with --api-key before running Cursor models.",
505
+ });
506
+ } catch {
507
+ return useFallbackModels(options, {
508
+ reason: "discovery-failed",
509
+ message:
510
+ "Cursor model discovery failed. Using fallback Cursor models for selection only; verify CURSOR_API_KEY or restart pi with --api-key before running Cursor models.",
511
+ });
512
+ }
513
+ }
514
+
515
+ export const __testUtils = {
516
+ parseContextWindow,
517
+ registerModelItems,
518
+ getCliApiKeyFromArgv,
519
+ };
package/index.js DELETED
@@ -1 +0,0 @@
1
- throw new Error("pi-cursor-sdk is reserved. The real release is coming soon.");