pi-cursor-sdk 0.1.7 → 0.1.8
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 +7 -1
- package/README.md +3 -2
- package/docs/cursor-model-ux-spec.md +7 -5
- package/package.json +3 -3
- package/src/context-window-cache.ts +4 -0
- package/src/model-discovery.ts +46 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.1.8 - 2026-05-14
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Update the verified dependency baseline to `@cursor/sdk` 1.0.13 and Vitest 4.1.6.
|
|
8
|
+
- Register latest-style Cursor SDK model aliases returned by `Cursor.models.list()` as pi-selectable Cursor model IDs, including context-qualified alias variants where applicable.
|
|
9
|
+
- Clarify Max Mode behavior against current Cursor SDK docs: Cursor may enable required Max Mode automatically, but the extension still only advertises catalog-exposed context variants.
|
|
4
10
|
|
|
5
11
|
## 0.1.7 - 2026-05-10
|
|
6
12
|
|
package/README.md
CHANGED
|
@@ -137,6 +137,7 @@ How to read model IDs:
|
|
|
137
137
|
- `cursor/...` is the Cursor provider registered by this extension
|
|
138
138
|
- `@1m`, `@272k`, and `@300k` are context-window variants
|
|
139
139
|
- `:medium`, `:high`, and `:xhigh` are pi thinking-level suffixes for models where the Cursor SDK exposes a pi-controllable thinking parameter
|
|
140
|
+
- latest-style Cursor aliases returned by `Cursor.models.list()` are registered too, using the same context suffixes when the target model has context variants
|
|
140
141
|
|
|
141
142
|
Examples with pi thinking controls:
|
|
142
143
|
|
|
@@ -146,7 +147,7 @@ pi --model cursor/gpt-5.5@272k:xhigh
|
|
|
146
147
|
pi --model cursor/gpt-5.5@1m --thinking medium
|
|
147
148
|
```
|
|
148
149
|
|
|
149
|
-
Cursor-only parameters are not encoded into pi model IDs. Cursor `context` becomes a pi-visible model variant because it changes pi's native `contextWindow`; Cursor `fast` is extension state, not model identity.
|
|
150
|
+
Cursor-only parameters are not encoded into pi model IDs. Cursor `context` becomes a pi-visible model variant because it changes pi's native `contextWindow`; Cursor `fast` is extension state, not model identity. Alias model IDs still share Cursor-only state, such as fast defaults, with their underlying Cursor base model.
|
|
150
151
|
|
|
151
152
|
## Thinking support
|
|
152
153
|
|
|
@@ -214,7 +215,7 @@ Fallback models are a conservative startup model list. Actual Cursor runs still
|
|
|
214
215
|
- **Pi tool schemas are not passed through to Cursor.** This extension is a Cursor provider, not a bridge that forwards pi's tool system into Cursor.
|
|
215
216
|
- **One fresh Cursor agent is created per provider call.** Cursor agent state is not reused between pi provider calls.
|
|
216
217
|
- **Ambient Cursor setting/rule layers are not loaded by default.** The current Cursor SDK writes setting/rule loading logs directly to terminal output, which corrupts pi's TUI, so the extension leaves those layers out.
|
|
217
|
-
- **Max Mode is not
|
|
218
|
+
- **Max Mode is not a manual pi variant.** Cursor's SDK may enable Max Mode automatically for models that require it. This extension only advertises exact context-window variants that the SDK catalog exposes and otherwise uses conservative SDK-derived default/non-Max context windows.
|
|
218
219
|
- **Output token limits are conservative.** Cursor SDK model metadata does not currently expose output token limits directly.
|
|
219
220
|
- **Token usage is approximate in pi.** Cursor SDK usage events include internal agent/tool/cache work, so the extension reports an approximate replayable pi prompt/output size for context display and compaction decisions.
|
|
220
221
|
|
|
@@ -20,7 +20,8 @@ Current implementation notes:
|
|
|
20
20
|
- Cursor-side thinking remains visible. Cursor internal tool activity is recorded from SDK events and scrubbed. In interactive TTY sessions, supported completed `read`, `bash`, and `ls` activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so the TUI can show native green cards without forcing Cursor to call pi tools or rerunning Cursor's reads/shell commands. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. `PI_CURSOR_NATIVE_TOOL_DISPLAY=0` disables native replay, and `PI_CURSOR_REGISTER_NATIVE_TOOLS=0` is a registration-only opt-out that keeps the transcript fallback without shadowing pi tool names. When these native cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK completions arrive: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later Cursor tool batches as further `toolUse` turns, then Cursor's final assistant answer. Non-interactive runs keep bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when native tool replay is not active.
|
|
21
21
|
- Cursor SDK usage events report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension reports approximate prompt/output usage for pi context display and compaction decisions instead of copying raw Cursor SDK usage. When native replay splits one Cursor SDK run into multiple pi turns, prompt input is counted once for the run; later synthetic replay turns report `input: 0` and only their own output estimate.
|
|
22
22
|
- For models without a catalog `context` parameter, context windows are not hardcoded. The extension ships a bundled SDK-derived default/non-Max cache generated from `createAgentPlatform().checkpointStore.loadLatest(agentId).tokenDetails.maxTokens`. Successful runs can update a local override cache, but model discovery does not probe models at startup.
|
|
23
|
-
- Max Mode context windows are distinct from default/non-Max context windows. `@cursor/sdk` 1.0.
|
|
23
|
+
- Max Mode context windows are distinct from default/non-Max context windows. `@cursor/sdk` 1.0.13 documentation says the SDK may enable Max Mode automatically when a selected model requires it, but the public local-agent `ModelSelection` path still does not expose a manual Max Mode selector. Do not advertise Max Mode context windows unless the SDK catalog exposes an exact parameter/variant or the SDK public API adds a Max Mode selector that the extension actually sends.
|
|
24
|
+
- `@cursor/sdk` 1.0.13 adds latest-style `ModelListItem.aliases`. The extension registers those aliases as pi model IDs (with the same context suffixes when applicable) and sends the alias back in `ModelSelection.id`, while sharing Cursor-only state such as fast defaults with the underlying catalog `id`.
|
|
24
25
|
|
|
25
26
|
## Goal
|
|
26
27
|
|
|
@@ -69,6 +70,7 @@ Users can persist the stored key through `/login` -> `Use an API key` -> `Cursor
|
|
|
69
70
|
For each model, use:
|
|
70
71
|
|
|
71
72
|
- `model.id`
|
|
73
|
+
- `model.aliases`
|
|
72
74
|
- `model.displayName`
|
|
73
75
|
- `model.parameters`
|
|
74
76
|
- `model.variants`
|
|
@@ -135,8 +137,8 @@ Register a `cursor` provider with `pi.registerProvider()`.
|
|
|
135
137
|
|
|
136
138
|
Rules:
|
|
137
139
|
|
|
138
|
-
- Register one pi model for each Cursor base model when there is no Cursor `context` parameter.
|
|
139
|
-
- Register one pi model per Cursor `context` value when the model exposes a `context` parameter.
|
|
140
|
+
- Register one pi model for each Cursor base model and SDK alias when there is no Cursor `context` parameter.
|
|
141
|
+
- Register one pi model per Cursor `context` value for each Cursor base model and SDK alias when the model exposes a `context` parameter.
|
|
140
142
|
- Do not encode `reasoning`, `effort`, `thinking`, or `fast` into pi model IDs.
|
|
141
143
|
- Prefer stable, readable `@<context>` suffixes that do not conflict with pi's final `:<thinking>` suffix parser.
|
|
142
144
|
- Sort Cursor models by base ID, then context value in Cursor SDK order before calling `pi.registerProvider()`. Registration order matters for `/model` display and model cycling; `--list-models` sorts output separately.
|
|
@@ -177,7 +179,7 @@ Reason:
|
|
|
177
179
|
|
|
178
180
|
Each registered model must set:
|
|
179
181
|
|
|
180
|
-
- `id`: context-qualified pi model ID when needed.
|
|
182
|
+
- `id`: context-qualified pi model ID when needed. For SDK aliases, this uses the alias as the pi-visible ID and the alias is sent back to Cursor as `ModelSelection.id`.
|
|
181
183
|
- `name`: human-readable Cursor display name plus context when useful.
|
|
182
184
|
- `reasoning`: `true` only if a Cursor `reasoning`, `effort`, or `thinking` parameter can map to pi thinking. This controls pi's thinking UI and `pi --list-models` `thinking` column; it must not be used to claim whether the Cursor model can think internally. Cursor SDK models are thinking-capable even when this is `false`.
|
|
183
185
|
- `thinkingLevelMap`: model-specific pi-to-Cursor mapping for pi UI, clamping, persistence, and footer display.
|
|
@@ -186,7 +188,7 @@ Each registered model must set:
|
|
|
186
188
|
- `input`: supported input types. The installed Cursor SDK accepts `SDKUserMessage.images`, and Cursor models are expected to support image input, so advertise `["text", "image"]`.
|
|
187
189
|
- `cost`: zeroed unless reliable Cursor costs are available.
|
|
188
190
|
|
|
189
|
-
The extension stores runtime metadata in an internal map keyed by registered pi model ID. That map records the Cursor base model ID, selected context param, default params, and discovered capabilities. `ProviderModelConfig` has no dedicated metadata field, so do not rely on hidden custom fields for this state.
|
|
191
|
+
The extension stores runtime metadata in an internal map keyed by registered pi model ID. That map records the Cursor base catalog model ID, the Cursor selection model ID (base ID or alias), selected context param, default params, and discovered capabilities. `ProviderModelConfig` has no dedicated metadata field, so do not rely on hidden custom fields for this state.
|
|
190
192
|
|
|
191
193
|
## Dynamic Capabilities
|
|
192
194
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cursor-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "pi provider extension backed by @cursor/sdk local agents",
|
|
5
5
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"test:watch": "vitest"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@cursor/sdk": "^1.0.
|
|
41
|
+
"@cursor/sdk": "^1.0.13"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@earendil-works/pi-ai": "*",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@earendil-works/pi-ai": "^0.74.0",
|
|
49
49
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
50
50
|
"typescript": "^6.0.3",
|
|
51
|
-
"vitest": "^4.1.
|
|
51
|
+
"vitest": "^4.1.6"
|
|
52
52
|
},
|
|
53
53
|
"pi": {
|
|
54
54
|
"extensions": [
|
|
@@ -40,6 +40,10 @@ export function loadContextWindowCache(): Map<string, number> {
|
|
|
40
40
|
return cache;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export function getCachedContextWindowExact(modelId: string): number | undefined {
|
|
44
|
+
return loadContextWindowCache().get(modelId);
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
export function getCachedContextWindow(modelId: string): number | undefined {
|
|
44
48
|
const cache = loadContextWindowCache();
|
|
45
49
|
return cache.get(modelId) ?? cache.get("default");
|
package/src/model-discovery.ts
CHANGED
|
@@ -7,7 +7,7 @@ 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 { getCachedContextWindow } from "./context-window-cache.js";
|
|
10
|
+
import { getCachedContextWindow, getCachedContextWindowExact } from "./context-window-cache.js";
|
|
11
11
|
|
|
12
12
|
const CURSOR_PROVIDER_ID = "cursor";
|
|
13
13
|
const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
|
|
@@ -217,6 +217,7 @@ async function getDiscoveryApiKey(): Promise<string | undefined> {
|
|
|
217
217
|
export interface CursorModelMetadata {
|
|
218
218
|
piModelId: string;
|
|
219
219
|
baseModelId: string;
|
|
220
|
+
selectionModelId: string;
|
|
220
221
|
displayName: string;
|
|
221
222
|
defaultParams: ModelParameterValue[];
|
|
222
223
|
context?: string;
|
|
@@ -340,23 +341,25 @@ function getParamValue(params: ModelParameterValue[], id: string): string | unde
|
|
|
340
341
|
return params.find((param) => param.id === id)?.value;
|
|
341
342
|
}
|
|
342
343
|
|
|
343
|
-
function encodePiModelId(
|
|
344
|
-
return context ? `${
|
|
344
|
+
function encodePiModelId(modelId: string, context?: string): string {
|
|
345
|
+
return context ? `${modelId}@${context}` : modelId;
|
|
345
346
|
}
|
|
346
347
|
|
|
347
|
-
function getModelName(item: ModelListItem, context?: string): string {
|
|
348
|
+
function getModelName(item: ModelListItem, context?: string, alias?: string): string {
|
|
348
349
|
const displayName = item.displayName || item.id;
|
|
349
|
-
|
|
350
|
+
const baseName = alias ? `${displayName} (${alias})` : displayName;
|
|
351
|
+
return context ? `${baseName} @ ${context}` : baseName;
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
function getContextWindow(piModelId: string, context?: string): number {
|
|
354
|
+
function getContextWindow(piModelId: string, context?: string, baseModelId?: string): number {
|
|
353
355
|
if (context) return parseContextWindow(context) ?? FALLBACK_CONTEXT_WINDOW;
|
|
354
|
-
return
|
|
356
|
+
return getCachedContextWindowExact(piModelId) ?? (baseModelId ? getCachedContextWindow(baseModelId) : undefined) ?? FALLBACK_CONTEXT_WINDOW;
|
|
355
357
|
}
|
|
356
358
|
|
|
357
359
|
function toMetadata(
|
|
358
360
|
item: ModelListItem,
|
|
359
361
|
piModelId: string,
|
|
362
|
+
selectionModelId: string,
|
|
360
363
|
defaultParams: ModelParameterValue[],
|
|
361
364
|
context: string | undefined,
|
|
362
365
|
): CursorModelMetadata {
|
|
@@ -365,10 +368,11 @@ function toMetadata(
|
|
|
365
368
|
return {
|
|
366
369
|
piModelId,
|
|
367
370
|
baseModelId: item.id,
|
|
371
|
+
selectionModelId,
|
|
368
372
|
displayName: item.displayName || item.id,
|
|
369
373
|
defaultParams: cloneParams(defaultParams),
|
|
370
374
|
...(context ? { context } : {}),
|
|
371
|
-
contextWindow: getContextWindow(piModelId, context),
|
|
375
|
+
contextWindow: getContextWindow(piModelId, context, item.id),
|
|
372
376
|
supportsFast: getParameter(item, "fast") !== undefined,
|
|
373
377
|
defaultFast: fastValue === "true",
|
|
374
378
|
supportsReasoning: thinkingLevelMap !== undefined,
|
|
@@ -400,18 +404,40 @@ function getContextValues(item: ModelListItem): string[] {
|
|
|
400
404
|
return getParameter(item, "context")?.values.map((value) => value.value) ?? [];
|
|
401
405
|
}
|
|
402
406
|
|
|
403
|
-
function
|
|
407
|
+
function getModelIds(item: ModelListItem, reservedBaseModelIds: Set<string>): string[] {
|
|
408
|
+
const ids = [item.id];
|
|
409
|
+
for (const rawAlias of item.aliases ?? []) {
|
|
410
|
+
const alias = rawAlias.trim();
|
|
411
|
+
if (!alias || alias === item.id || ids.includes(alias) || reservedBaseModelIds.has(alias)) continue;
|
|
412
|
+
ids.push(alias);
|
|
413
|
+
}
|
|
414
|
+
return ids;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function toModelConfigs(
|
|
418
|
+
item: ModelListItem,
|
|
419
|
+
usedPiModelIds: Set<string>,
|
|
420
|
+
reservedBaseModelIds: Set<string>,
|
|
421
|
+
): ProviderModelConfig[] {
|
|
404
422
|
const defaultParams = getDefaultParams(item);
|
|
405
423
|
const contextValues = getContextValues(item);
|
|
406
424
|
const contexts = contextValues.length > 0 ? contextValues : [undefined];
|
|
425
|
+
const configs: ProviderModelConfig[] = [];
|
|
426
|
+
|
|
427
|
+
for (const selectionModelId of getModelIds(item, reservedBaseModelIds)) {
|
|
428
|
+
const alias = selectionModelId === item.id ? undefined : selectionModelId;
|
|
429
|
+
for (const context of contexts) {
|
|
430
|
+
const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
|
|
431
|
+
const piModelId = encodePiModelId(selectionModelId, context);
|
|
432
|
+
if (usedPiModelIds.has(piModelId)) continue;
|
|
433
|
+
usedPiModelIds.add(piModelId);
|
|
434
|
+
const metadata = toMetadata(item, piModelId, selectionModelId, params, context);
|
|
435
|
+
metadataByPiModelId.set(piModelId, metadata);
|
|
436
|
+
configs.push(toModelConfig(metadata, getModelName(item, context, alias)));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
407
439
|
|
|
408
|
-
return
|
|
409
|
-
const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
|
|
410
|
-
const piModelId = encodePiModelId(item.id, context);
|
|
411
|
-
const metadata = toMetadata(item, piModelId, params, context);
|
|
412
|
-
metadataByPiModelId.set(piModelId, metadata);
|
|
413
|
-
return toModelConfig(metadata, getModelName(item, context));
|
|
414
|
-
});
|
|
440
|
+
return configs;
|
|
415
441
|
}
|
|
416
442
|
|
|
417
443
|
function sortModelsByBaseId(items: ModelListItem[]): ModelListItem[] {
|
|
@@ -420,7 +446,9 @@ function sortModelsByBaseId(items: ModelListItem[]): ModelListItem[] {
|
|
|
420
446
|
|
|
421
447
|
function registerModelItems(items: ModelListItem[]): ProviderModelConfig[] {
|
|
422
448
|
metadataByPiModelId.clear();
|
|
423
|
-
|
|
449
|
+
const usedPiModelIds = new Set<string>();
|
|
450
|
+
const reservedBaseModelIds = new Set(items.map((item) => item.id));
|
|
451
|
+
return sortModelsByBaseId(items).flatMap((item) => toModelConfigs(item, usedPiModelIds, reservedBaseModelIds));
|
|
424
452
|
}
|
|
425
453
|
|
|
426
454
|
export function getCursorModelMetadata(modelId: string): CursorModelMetadata | undefined {
|
|
@@ -501,7 +529,7 @@ export function buildCursorModelSelection(
|
|
|
501
529
|
setParam(params, "fast", fastEnabled ? "true" : "false");
|
|
502
530
|
}
|
|
503
531
|
|
|
504
|
-
return params.length > 0 ? { id: metadata.
|
|
532
|
+
return params.length > 0 ? { id: metadata.selectionModelId, params } : { id: metadata.selectionModelId };
|
|
505
533
|
}
|
|
506
534
|
|
|
507
535
|
function useFallbackModels(options: DiscoverModelsOptions, issue: CursorModelFallbackIssue): ProviderModelConfig[] {
|