pi-cursor-sdk 0.1.30 → 0.1.31

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.31 - 2026-06-01
6
+
7
+ ### Added
8
+
9
+ - Add Cursor `:fast` and `:slow` virtual model aliases for models with a Cursor SDK `fast` parameter so subagents and workflow-spawned agents can choose fast/slow independently of saved `/cursor-fast` defaults (#112).
10
+
5
11
  ## 0.1.30 - 2026-06-01
6
12
 
7
13
  ### Added
package/README.md CHANGED
@@ -168,7 +168,7 @@ pi --model cursor/gpt-5.5@272k:xhigh
168
168
  pi --model cursor/gpt-5.5@1m --thinking medium
169
169
  ```
170
170
 
171
- 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` and Cursor SDK conversation mode are extension state, not model identity. Alias model IDs use their selected SDK ID for Cursor-only state such as fast defaults, with read fallback for older defaults keyed by the underlying Cursor base model.
171
+ Cursor `context` becomes a pi-visible model variant because it changes pi's native `contextWindow`. For models that expose Cursor's boolean `fast` parameter, the extension also registers virtual `:fast` and `:slow` model aliases such as `cursor/composer-2-5:slow` and `cursor/gpt-5.5@1m:fast`. Those aliases are selection-only controls for subagents and workflow-spawned agents: they send the same Cursor SDK model ID plus an explicit `fast=true` or `fast=false` param, and they take precedence over saved `/cursor-fast` session/global defaults. Cursor SDK conversation mode remains extension state, not model identity. Alias model IDs use their selected SDK ID for Cursor-only state such as fast defaults, with read fallback for older defaults keyed by the underlying Cursor base model.
172
172
 
173
173
  ## Thinking support
174
174
 
@@ -190,7 +190,7 @@ Some Cursor SDK models do not expose a `reasoning`, `effort`, or `thinking` para
190
190
 
191
191
  ## Fast mode
192
192
 
193
- Use `/cursor-fast` to persistently toggle fast mode for the selected Cursor model when the model supports Cursor's `fast` parameter.
193
+ Use `/cursor-fast` to persistently toggle fast mode for the selected unsuffixed Cursor model when the model supports Cursor's `fast` parameter.
194
194
 
195
195
  Fast preferences are remembered per selected Cursor SDK model ID or alias and stored:
196
196
 
@@ -204,7 +204,16 @@ pi --model cursor/gpt-5.5@1m --cursor-fast -p "Say ok only"
204
204
  pi --model cursor/composer-2-5 --cursor-no-fast -p "Say ok only"
205
205
  ```
206
206
 
207
- Composer 2 and Composer 2.5 can default to fast. Use `--cursor-no-fast` for a one-shot no-fast Composer run. In print mode (`-p`), `--cursor-no-fast` is silent and does not write `~/.pi/agent/cursor-sdk.json`.
207
+ For per-agent control, select the virtual model alias instead of mutating the shared saved default:
208
+
209
+ ```bash
210
+ pi --model cursor/composer-2-5:slow -p "Say ok only"
211
+ pi --model cursor/gpt-5.5@1m:fast -p "Say ok only"
212
+ ```
213
+
214
+ The `:fast` and `:slow` aliases are available only for Cursor models whose catalog exposes a `fast` parameter. They override saved `/cursor-fast` session/global defaults while leaving `--cursor-fast` and `--cursor-no-fast` as explicit process-level force flags. `/cursor-fast` does not persist a new default while a virtual fast/slow alias is selected; switch to the unsuffixed model first.
215
+
216
+ Composer 2 and Composer 2.5 can default to fast. Use `--cursor-no-fast` or a `:slow` virtual alias for a one-shot no-fast Composer run. In print mode (`-p`), `--cursor-no-fast` is silent and does not write `~/.pi/agent/cursor-sdk.json`.
208
217
 
209
218
  In interactive mode, the footer only shows fast mode when fast is enabled and Cursor mode when it is non-default. Fast and plan mode share one Cursor status value, so they do not overwrite each other:
210
219
 
@@ -218,7 +227,7 @@ If you do not see `cursor fast`, fast mode is off. If you do not see `cursor pla
218
227
 
219
228
  ## Cursor SDK mode
220
229
 
221
- Cursor SDK conversation mode is Cursor-only extension state. It is not a pi model variant, not pi thinking/reasoning, not Cursor `fast`, and not pi's separate read-only plan-mode extension.
230
+ Cursor SDK conversation mode is Cursor-only extension state. It is not a pi model variant, not pi thinking/reasoning, not a `:fast`/`:slow` virtual fast alias, and not pi's separate read-only plan-mode extension.
222
231
 
223
232
  Default mode is `agent`. Start a one-shot run in a specific mode:
224
233
 
@@ -10,7 +10,7 @@ Current implementation notes:
10
10
 
11
11
  - Cursor context variants use `base@context` pi model IDs.
12
12
  - Cursor `reasoning`, `effort`, and boolean `thinking` parameters are driven by pi native thinking when the Cursor SDK exposes those controls.
13
- - Cursor `fast` is extension state, not model identity.
13
+ - Cursor `fast` is extension state by default; models that expose `fast` also get selection-only `:fast` / `:slow` virtual aliases for per-agent overrides.
14
14
  - Cursor SDK `mode` (`agent` or `plan`) is extension session state, not model identity, pi thinking, Cursor `fast`, or pi's separate plan-mode extension.
15
15
  - Cursor status uses one coordinated `ctx.ui.setStatus("cursor", ...)` value for fast and non-default plan mode; the default pi footer remains intact.
16
16
  - Installed `@cursor/sdk` user messages accept images, and Cursor models are treated as image-capable; registered input metadata is `text` plus `image`.
@@ -136,7 +136,7 @@ Use native pi abstractions wherever possible:
136
136
  | Cursor `reasoning` | pi native thinking via `thinkingLevelMap` |
137
137
  | Cursor `effort` | pi native thinking via `thinkingLevelMap` |
138
138
  | Cursor `thinking=false` | pi native `off` |
139
- | Cursor `fast` | extension state, not model identity |
139
+ | Cursor `fast` | extension state plus `:fast` / `:slow` virtual aliases for per-agent overrides |
140
140
  | Cursor SDK `mode` | extension session state; `agent` by default, `plan` via SDK-native mode |
141
141
  | Footer | default pi footer plus optional extension status |
142
142
 
@@ -156,7 +156,7 @@ Rules:
156
156
  - Register one pi model for each Cursor base model and each unambiguous SDK alias when there is no Cursor `context` parameter.
157
157
  - Register one pi model per Cursor `context` value for each Cursor base model and each unambiguous SDK alias when the model exposes a `context` parameter.
158
158
  - Skip SDK aliases that collide with another base model ID or are shared by multiple base models; those aliases can resolve differently from the pi row metadata.
159
- - Do not encode `reasoning`, `effort`, `thinking`, `fast`, or Cursor SDK `mode` into pi model IDs.
159
+ - Do not encode `reasoning`, `effort`, `thinking`, or Cursor SDK `mode` into pi model IDs. For models with a Cursor `fast` parameter, also register selection-only `:fast` and `:slow` virtual model aliases that do not change pi-native metadata.
160
160
  - Prefer stable, readable `@<context>` suffixes that do not conflict with pi's final `:<thinking>` suffix parser.
161
161
  - 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.
162
162
 
@@ -168,6 +168,9 @@ cursor/gpt-5.5@272k
168
168
  cursor/claude-opus-4-8@1m
169
169
  cursor/claude-opus-4-8@300k
170
170
  cursor/composer-2-5
171
+ cursor/composer-2-5:fast
172
+ cursor/composer-2-5:slow
173
+ cursor/gpt-5.5@1m:fast
171
174
  ```
172
175
 
173
176
  Avoid colon-based context IDs in the first implementation unless this spec is intentionally changed:
@@ -190,7 +193,7 @@ Reason:
190
193
 
191
194
  - `@1m` keeps context visually separate from pi's native `:medium` thinking suffix.
192
195
  - Context variants make `contextWindow` accurate in `--list-models`, the native footer, context overflow checks, and compaction logic.
193
- - `fast` is intentionally not a model variant because it does not affect pi model metadata and would double list noise.
196
+ - `:fast` / `:slow` are virtual aliases, not separate Cursor SDK base models: they keep the same context/thinking metadata and only force the outgoing Cursor `fast` param. They exist so subagents and workflow-spawned agents can choose fast/slow without mutating shared `/cursor-fast` defaults.
194
197
 
195
198
  ### Metadata Per Registered Model
196
199
 
@@ -362,17 +365,18 @@ fast=false <-> fast=true
362
365
 
363
366
  Rules:
364
367
 
365
- - `fast` is extension state, not pi model identity.
366
- - Toggle with `/cursor-fast`.
367
- - Store per-session and global per-base-model preferences.
368
- - When calling `Agent.create()`, include the selected `fast` value in Cursor model params.
368
+ - Unsuffixed models use extension state from `/cursor-fast`, per-session entries, and global defaults.
369
+ - `:fast` / `:slow` virtual model aliases force fast on/off for that selected agent and override saved defaults without writing state.
370
+ - Toggle unsuffixed models with `/cursor-fast`; do not persist a new default while a virtual fast alias is selected.
371
+ - Store per-session and global per-base-model preferences for unsuffixed models.
372
+ - When calling `Agent.create()` or `agent.send()`, include the selected `fast` value in Cursor model params.
369
373
  - Show `fast` through `ctx.ui.setStatus()` when enabled.
370
- - Support a first-pass CLI flag, `--cursor-fast`, to force fast mode for one run when the selected model supports it.
374
+ - Keep `--cursor-fast` and `--cursor-no-fast` as explicit process-level force flags.
371
375
 
372
376
  Reason:
373
377
 
374
378
  - `fast` does not affect pi `contextWindow`, thinking levels, or input support.
375
- - Registering fast/non-fast variants would make `--list-models` noisy without improving native pi behavior.
379
+ - The virtual aliases trade small `--list-models` noise for per-agent selection that works with subagents and dynamic workflows, where mutating a shared global fast default is the wrong abstraction.
376
380
 
377
381
  Status example:
378
382
 
@@ -521,7 +525,7 @@ Reason:
521
525
  - pi supports one final `:<thinking>` suffix.
522
526
  - Cursor-only parameters are not generic pi CLI parameters.
523
527
  - Context is already represented by the registered pi model ID.
524
- - `fast` is controlled by saved extension defaults or the first-pass `--cursor-fast` extension flag.
528
+ - `fast` is controlled by saved extension defaults, `:fast` / `:slow` virtual model aliases, or the `--cursor-fast` / `--cursor-no-fast` extension flags.
525
529
  - Cursor SDK `mode` is controlled by `/cursor-mode` session state or the first-pass `--cursor-mode` extension flag; it is never encoded in `--model`.
526
530
 
527
531
  For print mode:
@@ -529,7 +533,7 @@ For print mode:
529
533
  - no keybindings,
530
534
  - use selected context model variant,
531
535
  - use `--thinking` or `:medium` for reasoning/effort,
532
- - use saved global `fast` defaults unless `--cursor-fast` is present,
536
+ - use saved global `fast` defaults unless a virtual `:fast` / `:slow` model alias or force flag is present,
533
537
  - use Cursor SDK `agent` mode unless `/cursor-mode` session state or `--cursor-mode` overrides it.
534
538
 
535
539
  Fast flag example:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
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",
@@ -138,6 +138,10 @@ function getFastPreferenceModelId(metadata: NonNullable<ReturnType<typeof getCur
138
138
  return metadata.selectionModelId || metadata.baseModelId;
139
139
  }
140
140
 
141
+ function getVirtualFastBaseModelId(modelId: string): string {
142
+ return modelId.replace(/:(?:fast|slow)$/, "");
143
+ }
144
+
141
145
  function getStoredFastPreference(metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>): boolean | undefined {
142
146
  const preferenceModelId = getFastPreferenceModelId(metadata);
143
147
  return (
@@ -153,6 +157,7 @@ function getEffectiveFast(modelId: string): boolean | undefined {
153
157
  if (!metadata?.supportsFast) return undefined;
154
158
  if (cliForceNoFast) return false;
155
159
  if (cliForceFast) return true;
160
+ if (metadata.fastOverride !== undefined) return metadata.fastOverride;
156
161
  return getStoredFastPreference(metadata) ?? metadata.defaultFast;
157
162
  }
158
163
 
@@ -344,6 +349,14 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
344
349
  ctx.ui.notify("Cursor fast is forced by --cursor-fast", "info");
345
350
  return;
346
351
  }
352
+ if (metadata.fastOverride !== undefined) {
353
+ const state = metadata.fastOverride ? "enabled" : "disabled";
354
+ ctx.ui.notify(
355
+ `Cursor fast is fixed ${state} by selected model ${metadata.piModelId}; choose ${getVirtualFastBaseModelId(metadata.piModelId)} to use /cursor-fast preferences`,
356
+ "info",
357
+ );
358
+ return;
359
+ }
347
360
 
348
361
  const preferenceModelId = getFastPreferenceModelId(metadata);
349
362
  const current = getEffectiveFast(metadata.piModelId) ?? false;
@@ -88,6 +88,7 @@ export interface CursorModelMetadata {
88
88
  contextWindow: number;
89
89
  supportsFast: boolean;
90
90
  defaultFast: boolean;
91
+ fastOverride?: boolean;
91
92
  supportsReasoning: boolean;
92
93
  thinkingLevelMap?: ThinkingLevelMap;
93
94
  parameterIds: {
@@ -205,19 +206,35 @@ function getParamValue(params: ModelParameterValue[], id: string): string | unde
205
206
  return params.find((param) => param.id === id)?.value;
206
207
  }
207
208
 
208
- function encodePiModelId(modelId: string, context?: string): string {
209
- return context ? `${modelId}@${context}` : modelId;
209
+ function encodePiModelId(modelId: string, context?: string, fastOverride?: boolean): string {
210
+ const contextQualified = context ? `${modelId}@${context}` : modelId;
211
+ if (fastOverride === true) return `${contextQualified}:fast`;
212
+ if (fastOverride === false) return `${contextQualified}:slow`;
213
+ return contextQualified;
210
214
  }
211
215
 
212
- function getModelName(item: ModelListItem, context?: string, alias?: string): string {
216
+ function getModelName(item: ModelListItem, context?: string, alias?: string, fastOverride?: boolean): string {
213
217
  const displayName = item.displayName || item.id;
214
- const baseName = alias ? `${displayName} (${alias})` : displayName;
218
+ const qualifiers: string[] = [];
219
+ if (alias) qualifiers.push(alias);
220
+ if (fastOverride === true) qualifiers.push("fast");
221
+ if (fastOverride === false) qualifiers.push("slow");
222
+ const baseName = qualifiers.length > 0 ? `${displayName} (${qualifiers.join(", ")})` : displayName;
215
223
  return context ? `${baseName} @ ${context}` : baseName;
216
224
  }
217
225
 
226
+ function getFastOverrideBasePiModelId(piModelId: string): string {
227
+ return piModelId.replace(/:(?:fast|slow)$/, "");
228
+ }
229
+
218
230
  function getContextWindow(contextWindowCache: Map<string, number>, piModelId: string, context?: string, baseModelId?: string): number {
219
- return (
231
+ const fastOverrideBasePiModelId = getFastOverrideBasePiModelId(piModelId);
232
+ const contextWindowOverride =
220
233
  contextWindowCache.get(piModelId) ??
234
+ (fastOverrideBasePiModelId !== piModelId ? contextWindowCache.get(fastOverrideBasePiModelId) : undefined);
235
+
236
+ return (
237
+ contextWindowOverride ??
221
238
  (context ? parseContextWindow(context) : undefined) ??
222
239
  (baseModelId ? contextWindowCache.get(baseModelId) : undefined) ??
223
240
  contextWindowCache.get("default") ??
@@ -232,6 +249,7 @@ function toMetadata(
232
249
  defaultParams: ModelParameterValue[],
233
250
  context: string | undefined,
234
251
  contextWindowCache: Map<string, number>,
252
+ fastOverride?: boolean,
235
253
  ): CursorModelMetadata {
236
254
  const thinkingLevelMap = getThinkingLevelMap(item);
237
255
  const fastValue = getParamValue(defaultParams, "fast")?.toLowerCase();
@@ -245,6 +263,7 @@ function toMetadata(
245
263
  contextWindow: getContextWindow(contextWindowCache, piModelId, context, item.id),
246
264
  supportsFast: getParameter(item, "fast") !== undefined,
247
265
  defaultFast: fastValue === "true",
266
+ ...(fastOverride !== undefined ? { fastOverride } : {}),
248
267
  supportsReasoning: thinkingLevelMap !== undefined,
249
268
  ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
250
269
  parameterIds: {
@@ -310,16 +329,21 @@ function toModelConfigs(
310
329
  const contexts = contextValues.length > 0 ? contextValues : [undefined];
311
330
  const configs: ProviderModelConfig[] = [];
312
331
 
332
+ const fastOverrides = getParameter(item, "fast") === undefined ? [undefined] : [undefined, true, false];
333
+
313
334
  for (const selectionModelId of getModelIds(item, reservedBaseModelIds, ambiguousAliases)) {
314
335
  const alias = selectionModelId === item.id ? undefined : selectionModelId;
315
336
  for (const context of contexts) {
316
- const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
317
- const piModelId = encodePiModelId(selectionModelId, context);
318
- if (usedPiModelIds.has(piModelId)) continue;
319
- usedPiModelIds.add(piModelId);
320
- const metadata = toMetadata(item, piModelId, selectionModelId, params, context, contextWindowCache);
321
- metadataByPiModelId.set(piModelId, metadata);
322
- configs.push(toModelConfig(metadata, getModelName(item, context, alias)));
337
+ const contextParams = context ? replaceParam(defaultParams, "context", context) : defaultParams;
338
+ for (const fastOverride of fastOverrides) {
339
+ const params = fastOverride === undefined ? contextParams : replaceParam(contextParams, "fast", fastOverride ? "true" : "false");
340
+ const piModelId = encodePiModelId(selectionModelId, context, fastOverride);
341
+ if (usedPiModelIds.has(piModelId)) continue;
342
+ usedPiModelIds.add(piModelId);
343
+ const metadata = toMetadata(item, piModelId, selectionModelId, params, context, contextWindowCache, fastOverride);
344
+ metadataByPiModelId.set(piModelId, metadata);
345
+ configs.push(toModelConfig(metadata, getModelName(item, context, alias, fastOverride)));
346
+ }
323
347
  }
324
348
  }
325
349