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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.10 - 2026-05-15
4
+
5
+ ### Added
6
+
7
+ - Replay Cursor SDK `edit` and `write` activity through native pi tool-use turns using non-mutating `cursor_edit` and `cursor_write` cards, so Cursor file changes are visible as first-class tool activity without shadowing pi's built-in `edit` and `write` schemas.
8
+ - Add a maintainer `npm run refresh:cursor-snapshots` workflow for refreshing the reviewable Cursor fallback model catalog and optional checkpoint-derived context-window snapshot before releases.
9
+
10
+ ### Changed
11
+
12
+ - Improve Cursor edit/write replay card UX with concise created/updated/deleted/unchanged summaries and expanded colored diffs.
13
+ - Clarify image follow-up behavior: only latest user-message image bytes are forwarded; earlier images remain transcript placeholders and should be reattached or described.
14
+ - Allow `/cursor-refresh-models` to refresh the live Cursor model catalog after auth changes without restarting pi.
15
+ - Label local read fallback previews as transcript-time local previews when Cursor read result content is unavailable.
16
+
17
+ ### Fixed
18
+
19
+ - Prevent local read fallback previews from escaping the workspace through symlinks and from bypassing sensitive-path checks through sensitive symlink names.
20
+ - Budget oversized prompt history before `Agent.send`, including image-token reservations, while preserving system/tool-boundary instructions and the latest user request.
21
+ - Preserve assistant text emitted before native Cursor tool replay.
22
+ - Use the pi session cwd for native replay tool registration and update fallback execution to the latest session cwd.
23
+
24
+ ## 0.1.9 - 2026-05-14
25
+
26
+ ### Fixed
27
+
28
+ - Clean up recorded native Cursor tool replay outputs when abandoned replay runs are disposed, avoiding retained file or command output in process memory.
29
+ - Restore `/cursor-fast` state when session persistence fails during command handling.
30
+ - Preserve distinct same-payload Cursor tool completions while deduplicating duplicate SDK completion surfaces.
31
+ - Respect exact `model@context` context-window cache overrides before falling back to parsed base-model context values.
32
+ - Emit native replay text block endings with saved content indexes instead of searching by object identity.
33
+ - Redact discovery failure details with the same secret patterns used for stream errors.
34
+
35
+ ### Changed
36
+
37
+ - Update fallback Sonnet 4.6 context variants from `300k` to the current `200k` catalog variant.
38
+ - Skip ambiguous Cursor SDK aliases shared by multiple base models or colliding with base model IDs, preventing misleading pi model rows.
39
+ - Reduce context-window cache reloads during model catalog registration.
40
+ - Document image carry-forward as a product decision rather than silently changing current latest-user-message image forwarding behavior.
41
+
3
42
  ## 0.1.8 - 2026-05-14
4
43
 
5
44
  ### Changed
package/README.md CHANGED
@@ -26,7 +26,7 @@ pi --model cursor/composer-2
26
26
 
27
27
  3. In pi, run `/login`, choose `Use an API key`, choose `Cursor`, and paste your Cursor API key.
28
28
 
29
- If pi started without a key, run `/reload` or restart pi after `/login` to refresh the full live Cursor model catalog. Inside pi, use `/model` to choose another Cursor model.
29
+ If pi started without a key, run `/cursor-refresh-models` after `/login` to refresh the full live Cursor model catalog without restarting pi. Inside pi, use `/model` to choose another Cursor model.
30
30
 
31
31
  ## Requirements
32
32
 
@@ -83,7 +83,7 @@ Then, inside pi:
83
83
  4. Paste your Cursor API key.
84
84
  5. The key is saved in pi's native `~/.pi/agent/auth.json`.
85
85
 
86
- If pi started without a key, fallback Cursor models still register so `/login` is reachable. After `/login`, fallback model runs can use the stored key, but `/reload` or a restart is needed to refresh the full live Cursor model catalog discovered from the Cursor SDK.
86
+ If pi started without a key, fallback Cursor models still register so `/login` is reachable. After `/login`, fallback model runs can use the stored key, and `/cursor-refresh-models` refreshes the full live Cursor model catalog discovered from the Cursor SDK without restarting pi.
87
87
 
88
88
  Environment setup:
89
89
 
@@ -113,7 +113,7 @@ pi --list-models cursor
113
113
  Expected behavior:
114
114
 
115
115
  - with a valid key, Cursor models appear under the `cursor` provider
116
- - if discovery cannot authenticate or reach Cursor, pi may still show fallback Cursor models; after adding auth with `/login`, fallback model runs can use the saved key, and `/reload` or restart refreshes the live catalog
116
+ - if discovery cannot authenticate or reach Cursor, pi may still show fallback Cursor models; after adding auth with `/login`, fallback model runs can use the saved key, and `/cursor-refresh-models` refreshes the live catalog
117
117
 
118
118
  Smoke test:
119
119
 
@@ -137,7 +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
+ - unambiguous latest-style Cursor aliases returned by `Cursor.models.list()` are registered too, using the same context suffixes when the target model has context variants; aliases shared by multiple base models or colliding with a base model ID are skipped because their SDK resolution and displayed metadata can diverge
141
141
 
142
142
  Examples with pi thinking controls:
143
143
 
@@ -195,7 +195,7 @@ If you do not see `cursor fast`, fast mode is off.
195
195
 
196
196
  ## Images
197
197
 
198
- Images from the latest user message are forwarded to Cursor. Historical images are kept out of the transcript. The extension advertises `text` and `image` input for Cursor models because Cursor's SDK accepts image messages and Cursor models are expected to support them.
198
+ Images from the latest user message are forwarded to Cursor. Historical images are kept out of the transcript and appear only as `[image omitted from transcript]` placeholders, so follow-up questions about an earlier image should reattach the image or include a textual description. The extension advertises `text` and `image` input for Cursor models because Cursor's SDK accepts image messages and Cursor models are expected to support them.
199
199
 
200
200
  ## Fallback models
201
201
 
@@ -203,18 +203,18 @@ If no key is available from `/login`, `CURSOR_API_KEY`, or `--api-key`, model di
203
203
 
204
204
  - `composer-2`
205
205
  - `gpt-5.5@1m`, `gpt-5.5@272k`
206
- - `claude-sonnet-4-6@1m`, `claude-sonnet-4-6@300k`
206
+ - `claude-sonnet-4-6@1m`, `claude-sonnet-4-6@200k`
207
207
  - `claude-opus-4-7@1m`, `claude-opus-4-7@300k`
208
208
 
209
- Fallback models are a conservative startup model list. Actual Cursor runs still need a key from `/login`, `CURSOR_API_KEY`, or `--api-key`. If you add auth after startup, run `/reload` or restart pi to refresh the full live Cursor model catalog.
209
+ Fallback models are a conservative startup model list. Actual Cursor runs still need a key from `/login`, `CURSOR_API_KEY`, or `--api-key`. If you add auth after startup, run `/cursor-refresh-models` to refresh the full live Cursor model catalog without restarting pi.
210
210
 
211
211
  ## Limits
212
212
 
213
213
  - **Local Cursor SDK agents only.** This extension does not use Cursor cloud agents.
214
- - **Cursor-side tool use is not re-executed by pi.** Cursor still uses its own internal SDK tools. The extension records completed Cursor tool activity and, in interactive TTY sessions, replays supported `read`, `bash`, and `ls` activity through pi's native tool-call path with recorded results (for example green `read` and `$ ...` cards) without forcing Cursor to call pi tools or rerun commands. Native replay wrappers are registered only for tool names not already owned by another extension; skipped tools fall back to the scrubbed Cursor activity transcript. As Cursor SDK tool completions arrive, the extension mirrors native Codex ordering by ending a tool-use turn, letting pi render the recorded tool results immediately, then continuing with live post-tool Cursor thinking/text, any later Cursor tool batches, or Cursor's final answer as the next assistant turn. Non-interactive/session consumers still get bounded scrubbed transcript data so `pi -p` keeps printing normal assistant text.
214
+ - **Cursor-side tool use is not re-executed by pi.** Cursor still uses its own internal SDK tools. The extension records completed Cursor tool activity and, in interactive TTY sessions, replays supported `read`, `bash`, `ls`, `edit`, and `write` activity through pi's native tool-call path with recorded results (for example green `read`, `$ ...`, and Cursor edit/write cards) without forcing Cursor to call pi tools or rerun commands. Cursor edit/write activity uses replay-only `cursor_edit` and `cursor_write` tool cards because Cursor's file-editing schema is not the same as pi's built-in `edit`/`write` schemas; those replay tools only display recorded Cursor results and never mutate files directly. If a Cursor read completion reports no content, the extension may include a bounded local file preview for safe in-workspace paths; that preview is explicitly labeled as a local preview captured at transcript time, not guaranteed Cursor-observed content. Native replay wrappers are registered only for tool names not already owned by another extension; skipped tools fall back to the scrubbed Cursor activity transcript. As Cursor SDK tool completions arrive, the extension mirrors native Codex ordering by ending a tool-use turn, letting pi render the recorded tool results immediately, then continuing with live post-tool Cursor thinking/text, any later Cursor tool batches, or Cursor's final answer as the next assistant turn. Non-interactive/session consumers still get bounded scrubbed transcript data so `pi -p` keeps printing normal assistant text.
215
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.
216
216
  - **One fresh Cursor agent is created per provider call.** Cursor agent state is not reused between pi provider calls.
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
+ - **Cursor setting sources are opt-in.** The extension does not pass `local.settingSources` by default because the Cursor SDK can print settings/skills loading output directly to the terminal during startup. To load configured Cursor MCP servers, plugin tools, project/user settings, and related Cursor-native capabilities, start pi with `PI_CURSOR_SETTING_SOURCES=all`. To narrow loading, set a comma-separated list such as `PI_CURSOR_SETTING_SOURCES=project,user,plugins`.
218
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.
219
219
  - **Output token limits are conservative.** Cursor SDK model metadata does not currently expose output token limits directly.
220
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.
@@ -223,7 +223,7 @@ Fallback models are a conservative startup model list. Actual Cursor runs still
223
223
 
224
224
  ### I can see Cursor models, but runs fail
225
225
 
226
- You may be seeing fallback startup models or a missing/invalid key. In interactive pi, run `/login`, choose `Use an API key`, choose `Cursor`, paste the key, then run `/reload` or restart pi.
226
+ You may be seeing fallback startup models or a missing/invalid key. In interactive pi, run `/login`, choose `Use an API key`, choose `Cursor`, paste the key, then run `/cursor-refresh-models`.
227
227
 
228
228
  You can also restart pi with a key in the same shell or launcher that starts pi:
229
229
 
@@ -262,11 +262,15 @@ Fast mode is currently off. The footer only shows `cursor fast` when fast mode i
262
262
 
263
263
  ### My Cursor app settings or rules do not seem to apply
264
264
 
265
- They are not loaded by default in this extension. See [Limits](#limits).
265
+ Cursor setting sources are not loaded by default because the Cursor SDK can print settings/skills loading output directly to the terminal. Start pi with `PI_CURSOR_SETTING_SOURCES=all`, or choose a narrower list such as `PI_CURSOR_SETTING_SOURCES=project,user,plugins`.
266
+
267
+ ### Cursor does not call my web search MCP/tool
268
+
269
+ Cursor SDK local agents load MCP servers from Cursor setting sources and inline SDK config. This extension leaves Cursor setting sources off by default to avoid startup log noise, so a web search tool needs to be configured in Cursor and settings sources need to be enabled with `PI_CURSOR_SETTING_SOURCES=all` or a narrower list.
266
270
 
267
271
  ### Cursor native tool cards conflict with another extension
268
272
 
269
- Cursor native replay is a UI enhancement for interactive TTY sessions. If another extension already owns `read`, `bash`, or `ls`, this extension skips only the conflicting native replay wrapper and uses the scrubbed Cursor activity transcript for that tool instead. To disable Cursor native replay registration entirely, start pi with:
273
+ Cursor native replay is a UI enhancement for interactive TTY sessions. If another extension already owns `read`, `bash`, `ls`, `cursor_edit`, or `cursor_write`, this extension skips only the conflicting native replay wrapper and uses the scrubbed Cursor activity transcript for that tool instead. To disable Cursor native replay registration entirely, start pi with:
270
274
 
271
275
  ```bash
272
276
  PI_CURSOR_NATIVE_TOOL_DISPLAY=0 pi --model cursor/composer-2
@@ -283,6 +287,21 @@ npm test
283
287
  npm run typecheck
284
288
  ```
285
289
 
290
+ Refresh the reviewable Cursor fallback catalog before releases or after Cursor model changes:
291
+
292
+ ```bash
293
+ CURSOR_API_KEY="your-key" npm run refresh:cursor-snapshots -- --write
294
+ ```
295
+
296
+ Refresh the bundled default/non-Max context-window snapshot only when checkpoint-derived context windows have been collected from live local runs:
297
+
298
+ ```bash
299
+ CURSOR_API_KEY="your-key" npm run refresh:cursor-snapshots -- --write \
300
+ --context-windows ~/.pi/agent/cursor-sdk-context-windows.json
301
+ ```
302
+
303
+ The refresh script writes public model metadata only and scrubs known auth material from SDK errors. It must not be run with shell tracing that would echo API keys.
304
+
286
305
  Local development run:
287
306
 
288
307
  ```bash
@@ -13,15 +13,16 @@ Current implementation notes:
13
13
  - Cursor `fast` is extension state, not model identity.
14
14
  - Cursor fast status uses `ctx.ui.setStatus()`; the default pi footer remains intact.
15
15
  - Installed `@cursor/sdk` user messages accept images, and Cursor models are treated as image-capable; registered input metadata is `text` plus `image`.
16
+ - Image payload forwarding sends images only from the latest user message. If the latest user turn is plain text after an earlier image turn, the transcript keeps an `[image omitted from transcript]` placeholder but no image bytes are sent to Cursor. The prompt explicitly tells Cursor that prior image bytes are unavailable and to ask the user to reattach or describe a prior image when needed. Carrying images forward across turns remains a future product decision because it affects token cost, privacy, stale visual context, and expected multimodal follow-up behavior.
16
17
  - `@cursor/sdk` is a package dependency of this extension; users should not need a global SDK install.
17
18
  - Cursor auth uses pi-native API-key resolution for provider `cursor`: CLI `--api-key`, stored `~/.pi/agent/auth.json` API key from `/login`, then `CURSOR_API_KEY`. The extension config file stores only non-secret Cursor-only state such as fast defaults.
18
- - Local agents do not pass `settingSources` by default because the current Cursor SDK writes setting/rule loading INFO logs directly to terminal output, which corrupts pi's TUI.
19
+ - Local agents do not pass `settingSources` by default because the Cursor SDK can print settings/skills loading output directly to the terminal during startup. Users can opt in with `PI_CURSOR_SETTING_SOURCES=all` or narrow loading with a comma-separated list such as `PI_CURSOR_SETTING_SOURCES=project,user,plugins`.
19
20
  - Cursor SDK models are treated as thinking-capable even when pi reports `thinking=no`; that pi column only means the SDK did not expose a pi-controllable thinking parameter for that model.
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
+ - Cursor-side thinking remains visible. Cursor internal tool activity is recorded from SDK events and scrubbed. In interactive TTY sessions, supported completed `read`, `bash`, `ls`, `edit`, and `write` 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/file edits. Cursor edit/write activity is replayed through `cursor_edit` and `cursor_write` cards rather than pi's built-in `edit`/`write` names because Cursor's edit/write schemas differ from pi's schemas; these replay-only tools display recorded Cursor results and fail closed if called without a recorded result. 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
22
  - 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
23
  - 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
24
  - 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`.
25
+ - `@cursor/sdk` 1.0.13 adds latest-style `ModelListItem.aliases`. The extension registers only unambiguous 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`. Aliases shared by multiple base models, such as generic family aliases, are skipped because the pi row metadata would otherwise imply one base model while Cursor may resolve the alias to another.
25
26
 
26
27
  ## Goal
27
28
 
@@ -65,7 +66,7 @@ Discovery resolves `apiKey` in this order:
65
66
  2. Stored pi auth for provider `cursor` from `AuthStorage.create().getApiKey("cursor", { includeFallback: false })`.
66
67
  3. `CURSOR_API_KEY`.
67
68
 
68
- Users can persist the stored key through `/login` -> `Use an API key` -> `Cursor`. If auth is added after startup, fallback models can run once pi resolves the saved key for provider requests, but `/reload` or restart is required to refresh the full live Cursor model catalog.
69
+ Users can persist the stored key through `/login` -> `Use an API key` -> `Cursor`. If auth is added after startup, fallback models can run once pi resolves the saved key for provider requests, and `/cursor-refresh-models` refreshes the full live Cursor model catalog without restarting pi.
69
70
 
70
71
  For each model, use:
71
72
 
@@ -76,7 +77,7 @@ For each model, use:
76
77
  - `model.variants`
77
78
  - default variant: `variant.isDefault === true`, else first variant
78
79
 
79
- This means new Cursor models and changed Cursor parameters are picked up after reload/restart.
80
+ This means new Cursor models and changed Cursor parameters are picked up after `/cursor-refresh-models`, reload, or restart.
80
81
 
81
82
  Pi model metadata is also a source of truth for pi-native behavior:
82
83
 
@@ -92,24 +93,21 @@ If a Cursor parameter changes any of those pi-native fields, model registration
92
93
 
93
94
  ### Refresh Current Cursor Matrix
94
95
 
95
- Run this whenever Cursor releases or changes models:
96
+ Run this whenever Cursor releases or changes models, and before releases that may ship stale fallback metadata:
96
97
 
97
98
  ```bash
98
- node --input-type=module <<'EOF'
99
- import { Cursor } from '@cursor/sdk';
100
-
101
- const models = await Cursor.models.list({ apiKey: process.env.CURSOR_API_KEY });
102
- for (const model of models) {
103
- const options = (model.parameters ?? [])
104
- .map((param) => `${param.id}: ${param.values.map((value) => value.value).join(', ')}`)
105
- .join(' | ') || 'none';
106
- const defaultVariant = model.variants?.find((variant) => variant.isDefault) ?? model.variants?.[0];
107
- const defaults = defaultVariant?.params?.map((param) => `${param.id}=${param.value}`).join('; ') || 'none';
108
- console.log(`${model.id}\t${model.displayName}\t${options}\t${defaults}`);
109
- }
110
- EOF
99
+ CURSOR_API_KEY="your-key" npm run refresh:cursor-snapshots -- --write
100
+ ```
101
+
102
+ That command refreshes `src/cursor-fallback-models.generated.ts` only. If live local Cursor runs have collected checkpoint-derived context windows, merge them into the bundled default/non-Max snapshot too:
103
+
104
+ ```bash
105
+ CURSOR_API_KEY="your-key" npm run refresh:cursor-snapshots -- --write \
106
+ --context-windows ~/.pi/agent/cursor-sdk-context-windows.json
111
107
  ```
112
108
 
109
+ The script calls `Cursor.models.list({ apiKey })`, writes `src/cursor-fallback-models.generated.ts`, and updates `src/bundled-context-windows.ts` only when `--context-windows` is provided. It prints model IDs/counts only and scrubs known auth material from SDK errors; it must not print or store API keys. Review the generated diff before committing because Cursor can change aliases, defaults, and parameter meanings.
110
+
113
111
  ## Design Direction
114
112
 
115
113
  Use native pi abstractions wherever possible:
@@ -137,8 +135,9 @@ Register a `cursor` provider with `pi.registerProvider()`.
137
135
 
138
136
  Rules:
139
137
 
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.
138
+ - Register one pi model for each Cursor base model and each unambiguous SDK alias when there is no Cursor `context` parameter.
139
+ - 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.
140
+ - 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.
142
141
  - Do not encode `reasoning`, `effort`, `thinking`, or `fast` into pi model IDs.
143
142
  - Prefer stable, readable `@<context>` suffixes that do not conflict with pi's final `:<thinking>` suffix parser.
144
143
  - 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.
@@ -486,42 +485,22 @@ Fast flag example:
486
485
  pi --model cursor/gpt-5.5@1m --cursor-fast -p "Say ok only"
487
486
  ```
488
487
 
489
- ## Current Discovered Model Capability Examples
488
+ ## Discovered Model Capability Examples
490
489
 
491
- Current live Cursor data says:
490
+ These examples document the capability shapes the extension handles, not an exhaustive live catalog. The exact Cursor catalog changes over time; use `pi -e . --list-models cursor` or `Cursor.models.list()` for the current model surface. When the SDK reports aliases, only unambiguous aliases are registered; shared generic aliases are skipped.
492
491
 
493
- | Model | Cursor controls | Pi representation |
492
+ | Example model shape | Cursor controls | Pi representation |
494
493
  |---|---|---|
495
- | `default` | none | plain model |
496
- | `composer-2` | fast | plain model + fast extension state |
497
- | `composer-1.5` | none | plain model |
498
- | `gpt-5.5` | context, reasoning, fast | context variants + native thinking + fast state |
499
- | `gpt-5.4` | context, reasoning, fast | context variants + native thinking + fast state |
500
- | `gpt-5.4-mini` | reasoning | plain model + native thinking |
501
- | `gpt-5.4-nano` | reasoning | plain model + native thinking |
502
- | `gpt-5.3-codex` | reasoning, fast | plain model + native thinking + fast state |
503
- | `gpt-5.3-codex-spark` | reasoning | plain model + native thinking |
504
- | `gpt-5.2` | reasoning, fast | plain model + native thinking + fast state |
505
- | `gpt-5.2-codex` | reasoning, fast | plain model + native thinking + fast state |
506
- | `gpt-5.1-codex-max` | reasoning, fast | plain model + native thinking + fast state |
507
- | `gpt-5.1-codex-mini` | reasoning | plain model + native thinking |
508
- | `gpt-5.1` | reasoning | plain model + native thinking |
509
- | `claude-opus-4-7` | thinking, context, effort | context variants + native thinking |
510
- | `claude-opus-4-6` | thinking, context, effort, fast | context variants + native thinking + fast state |
511
- | `claude-opus-4-5` | thinking | plain model + native thinking |
512
- | `claude-sonnet-4-6` | thinking, context, effort | context variants + native thinking |
513
- | `claude-sonnet-4-5` | thinking, context | context-qualified model + native thinking |
514
- | `claude-sonnet-4` | thinking, context | context-qualified model + native thinking |
515
- | `claude-haiku-4-5` | thinking | plain model + native thinking |
516
- | `grok-4.3` | context | context variants |
517
- | `grok-4-20` | thinking | plain model + native thinking |
518
- | `gemini-3.1-pro` | none | plain model |
519
- | `gemini-3-flash` | none | plain model |
520
- | `gemini-2.5-flash` | none | plain model |
521
- | `gpt-5-mini` | none | plain model |
522
- | `kimi-k2.5` | none | plain model |
523
-
524
- If Cursor later adds `fast`, `context`, `reasoning`, or `effort` to a model, the extension picks it up dynamically.
494
+ | plain model, such as `default` or models with no exposed controls | none | plain model |
495
+ | `composer-2`-style model | fast | plain model + fast extension state |
496
+ | GPT-style reasoning model with context variants | context, reasoning, fast when exposed | context variants + native thinking + optional fast state |
497
+ | Claude-style thinking model with context variants | thinking, context, effort when exposed | context variants + native thinking + optional fast state |
498
+ | Claude-style thinking model without context variants | thinking and/or effort | plain model + native thinking |
499
+ | context-only model | context | context variants |
500
+ | unique latest alias for any shape | aliases | same pi rows as the base model shape, using the alias as `ModelSelection.id` |
501
+ | shared generic alias across multiple base models | aliases | skipped to avoid misleading pi rows |
502
+
503
+ If Cursor later adds `fast`, `context`, `reasoning`, `effort`, or aliases to a model, the extension picks up unambiguous capability changes dynamically.
525
504
 
526
505
  ## Detailed Examples
527
506
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",
@@ -23,6 +23,7 @@
23
23
  "homepage": "https://github.com/fitchmultz/pi-cursor-sdk#readme",
24
24
  "files": [
25
25
  "src",
26
+ "scripts/refresh-cursor-model-snapshots.mjs",
26
27
  "README.md",
27
28
  "docs/cursor-model-ux-spec.md",
28
29
  "LICENSE",
@@ -35,7 +36,8 @@
35
36
  "scripts": {
36
37
  "typecheck": "tsc --noEmit",
37
38
  "test": "vitest run",
38
- "test:watch": "vitest"
39
+ "test:watch": "vitest",
40
+ "refresh:cursor-snapshots": "node scripts/refresh-cursor-model-snapshots.mjs"
39
41
  },
40
42
  "dependencies": {
41
43
  "@cursor/sdk": "^1.0.13"
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, resolve } from "node:path";
4
+ import { Cursor } from "@cursor/sdk";
5
+
6
+ const FALLBACK_MODELS_PATH = "src/cursor-fallback-models.generated.ts";
7
+ const CONTEXT_WINDOWS_PATH = "src/bundled-context-windows.ts";
8
+ const DEFAULT_CONTEXT_WINDOW = 200000;
9
+
10
+ function printHelp() {
11
+ console.log(`Refresh reviewable Cursor model fallback snapshots.
12
+
13
+ Usage:
14
+ npm run refresh:cursor-snapshots -- --write [options]
15
+ node scripts/refresh-cursor-model-snapshots.mjs [options]
16
+
17
+ Options:
18
+ --write Write ${FALLBACK_MODELS_PATH}. Also write
19
+ ${CONTEXT_WINDOWS_PATH} when --context-windows is supplied.
20
+ Without --write, print a summary only.
21
+ --api-key <key> Cursor API key. Prefer CURSOR_API_KEY to avoid shell history.
22
+ --context-windows <file> Optional JSON file with {"contextWindows": {"model": 123}}
23
+ or a plain {"model": 123} map, usually copied from
24
+ ~/.pi/agent/cursor-sdk-context-windows.json after live runs.
25
+ --fallback-context-window <n> Context window to use for newly seen models that lack
26
+ a catalog context parameter and no checkpoint override.
27
+ Default: ${DEFAULT_CONTEXT_WINDOW}.
28
+ -h, --help Show this help.
29
+
30
+ Exit codes:
31
+ 0 success
32
+ 1 invalid arguments, missing auth, or Cursor SDK failure
33
+
34
+ Notes:
35
+ - This script prints model IDs/counts only; it never prints API keys.
36
+ - Cursor.models.list() is the source of truth for fallback catalog metadata.
37
+ - Checkpoint-derived context windows are optional input because collecting them
38
+ requires successful local SDK runs; this script does not start agents.`);
39
+ }
40
+
41
+ function fail(message) {
42
+ console.error(`refresh-cursor-snapshots: ${message}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ function scrubSensitiveText(text, secrets = []) {
47
+ let scrubbed = text;
48
+ for (const secret of secrets) {
49
+ if (secret) scrubbed = scrubbed.split(secret).join("[REDACTED]");
50
+ }
51
+ return scrubbed
52
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]")
53
+ .replace(/(api[_-]?key|authorization|auth[_-]?token)([\"'\s:=]+)[^\"'\s,}]+/gi, "$1$2[REDACTED]");
54
+ }
55
+
56
+ function parseArgs(argv) {
57
+ const args = {
58
+ write: false,
59
+ apiKey: process.env.CURSOR_API_KEY?.trim() || undefined,
60
+ contextWindowsPath: undefined,
61
+ fallbackContextWindow: DEFAULT_CONTEXT_WINDOW,
62
+ };
63
+ for (let index = 0; index < argv.length; index++) {
64
+ const arg = argv[index];
65
+ if (arg === "-h" || arg === "--help") {
66
+ printHelp();
67
+ process.exit(0);
68
+ }
69
+ if (arg === "--write") {
70
+ args.write = true;
71
+ continue;
72
+ }
73
+ if (arg === "--api-key") {
74
+ const value = argv[++index];
75
+ if (!value || value.startsWith("--")) fail("--api-key requires a value");
76
+ args.apiKey = value.trim();
77
+ continue;
78
+ }
79
+ if (arg.startsWith("--api-key=")) {
80
+ args.apiKey = arg.slice("--api-key=".length).trim();
81
+ continue;
82
+ }
83
+ if (arg === "--context-windows") {
84
+ const value = argv[++index];
85
+ if (!value || value.startsWith("--")) fail("--context-windows requires a file path");
86
+ args.contextWindowsPath = value;
87
+ continue;
88
+ }
89
+ if (arg.startsWith("--context-windows=")) {
90
+ args.contextWindowsPath = arg.slice("--context-windows=".length);
91
+ continue;
92
+ }
93
+ if (arg === "--fallback-context-window") {
94
+ const value = argv[++index];
95
+ if (!value || value.startsWith("--")) fail("--fallback-context-window requires a positive integer");
96
+ args.fallbackContextWindow = parsePositiveInteger(value, "--fallback-context-window");
97
+ continue;
98
+ }
99
+ if (arg.startsWith("--fallback-context-window=")) {
100
+ args.fallbackContextWindow = parsePositiveInteger(arg.slice("--fallback-context-window=".length), "--fallback-context-window");
101
+ continue;
102
+ }
103
+ fail(`unknown argument ${arg}`);
104
+ }
105
+ if (!args.apiKey) fail("missing Cursor API key; set CURSOR_API_KEY or pass --api-key");
106
+ return args;
107
+ }
108
+
109
+ function parsePositiveInteger(value, label) {
110
+ const parsed = Number(value);
111
+ if (!Number.isInteger(parsed) || parsed <= 0) fail(`${label} must be a positive integer`);
112
+ return parsed;
113
+ }
114
+
115
+ function sanitizeModelItem(item) {
116
+ return stripUndefined({
117
+ id: item.id,
118
+ displayName: item.displayName,
119
+ description: item.description,
120
+ aliases: Array.isArray(item.aliases) ? [...item.aliases] : undefined,
121
+ parameters: Array.isArray(item.parameters)
122
+ ? item.parameters.map((parameter) =>
123
+ stripUndefined({
124
+ id: parameter.id,
125
+ displayName: parameter.displayName,
126
+ values: Array.isArray(parameter.values)
127
+ ? parameter.values.map((value) => stripUndefined({ value: value.value, displayName: value.displayName }))
128
+ : [],
129
+ }),
130
+ )
131
+ : undefined,
132
+ variants: Array.isArray(item.variants)
133
+ ? item.variants.map((variant) =>
134
+ stripUndefined({
135
+ params: Array.isArray(variant.params) ? variant.params.map((param) => ({ id: param.id, value: param.value })) : [],
136
+ displayName: variant.displayName,
137
+ description: variant.description,
138
+ isDefault: variant.isDefault,
139
+ }),
140
+ )
141
+ : undefined,
142
+ });
143
+ }
144
+
145
+ function stripUndefined(value) {
146
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
147
+ }
148
+
149
+ function stableStringify(value) {
150
+ return JSON.stringify(value, null, "\t").replace(/"([^"\\]+)":/g, "$1:");
151
+ }
152
+
153
+ function formatFallbackModels(models) {
154
+ return `import type { ModelListItem } from "@cursor/sdk";\n\n// Generated/maintained fallback Cursor catalog snapshot.\n// Refresh with: npm run refresh:cursor-snapshots -- --write\n// Do not add secrets; this file stores public model metadata only.\nexport const FALLBACK_MODEL_ITEMS = ${stableStringify(models)} satisfies ModelListItem[];\n`;
155
+ }
156
+
157
+ function parseExistingContextWindows() {
158
+ if (!existsSync(CONTEXT_WINDOWS_PATH)) return new Map();
159
+ const source = readFileSync(CONTEXT_WINDOWS_PATH, "utf8");
160
+ const entries = new Map();
161
+ const entryPattern = /(?:"([^"]+)"|([A-Za-z_$][\w$]*)):\s*(\d+)/g;
162
+ for (const match of source.matchAll(entryPattern)) {
163
+ entries.set(match[1] ?? match[2], Number(match[3]));
164
+ }
165
+ return entries;
166
+ }
167
+
168
+ function parseContextWindowsFile(path) {
169
+ if (!path) return new Map();
170
+ let parsed;
171
+ try {
172
+ parsed = JSON.parse(readFileSync(path, "utf8"));
173
+ } catch (error) {
174
+ fail(`could not read ${path}: ${error instanceof Error ? error.message : String(error)}`);
175
+ }
176
+ const source = parsed.contextWindows && typeof parsed.contextWindows === "object" ? parsed.contextWindows : parsed;
177
+ const windows = new Map();
178
+ for (const [modelId, contextWindow] of Object.entries(source)) {
179
+ if (Number.isInteger(contextWindow) && contextWindow > 0) windows.set(modelId, contextWindow);
180
+ }
181
+ return windows;
182
+ }
183
+
184
+ function hasContextParameter(model) {
185
+ return (model.parameters ?? []).some((parameter) => parameter.id === "context");
186
+ }
187
+
188
+ function formatContextWindows(models, checkpointWindows, fallbackContextWindow) {
189
+ const existing = parseExistingContextWindows();
190
+ const merged = new Map(existing);
191
+ for (const [modelId, contextWindow] of checkpointWindows) merged.set(modelId, contextWindow);
192
+ for (const model of models) {
193
+ if (!hasContextParameter(model) && !merged.has(model.id)) merged.set(model.id, fallbackContextWindow);
194
+ }
195
+ if (!merged.has("default")) merged.set("default", fallbackContextWindow);
196
+
197
+ const date = new Date().toISOString().slice(0, 10);
198
+ const sorted = [...merged.entries()].sort(([a], [b]) => (a === "default" ? -1 : b === "default" ? 1 : a.localeCompare(b)));
199
+ const lines = sorted.map(([modelId, contextWindow]) => `\t${JSON.stringify(modelId)}: ${contextWindow},`);
200
+ return `// Generated from Cursor SDK checkpoint tokenDetails.maxTokens on ${date}.\n// Refresh with: npm run refresh:cursor-snapshots -- --write --context-windows ~/.pi/agent/cursor-sdk-context-windows.json\n// These are default/non-Max-mode SDK context windows for Cursor models that do not\n// expose a catalog \`context\` parameter. Do not replace them with Max Mode values\n// unless the Cursor SDK exposes an exact Max Mode model selection and the extension\n// uses that selection for matching pi model IDs.\nexport const BUNDLED_CONTEXT_WINDOWS = {\n${lines.join("\n")}\n} as const satisfies Record<string, number>;\n`;
201
+ }
202
+
203
+ const args = parseArgs(process.argv.slice(2));
204
+ let rawModels;
205
+ try {
206
+ rawModels = await Cursor.models.list({ apiKey: args.apiKey });
207
+ } catch (error) {
208
+ const rawMessage = error instanceof Error ? error.message : String(error);
209
+ fail(`Cursor.models.list() failed: ${scrubSensitiveText(rawMessage, [args.apiKey])}`);
210
+ }
211
+ if (!Array.isArray(rawModels) || rawModels.length === 0) fail("Cursor.models.list() returned no models");
212
+
213
+ const models = rawModels.map(sanitizeModelItem).sort((a, b) => a.id.localeCompare(b.id));
214
+ const checkpointWindows = parseContextWindowsFile(args.contextWindowsPath);
215
+ const fallbackSource = formatFallbackModels(models);
216
+ const contextWindowSource = args.contextWindowsPath ? formatContextWindows(models, checkpointWindows, args.fallbackContextWindow) : undefined;
217
+ const existingContextWindowCount = parseExistingContextWindows().size;
218
+
219
+ console.log(`Fetched ${models.length} Cursor models.`);
220
+ console.log(`Context windows: ${checkpointWindows.size} checkpoint override(s), ${existingContextWindowCount} existing bundled entr${existingContextWindowCount === 1 ? "y" : "ies"}.`);
221
+ console.log(`First models: ${models.slice(0, 8).map((model) => model.id).join(", ")}${models.length > 8 ? ", ..." : ""}`);
222
+
223
+ if (args.write) {
224
+ writeFileSync(FALLBACK_MODELS_PATH, fallbackSource);
225
+ console.log(`Wrote ${FALLBACK_MODELS_PATH}`);
226
+ if (contextWindowSource) {
227
+ writeFileSync(CONTEXT_WINDOWS_PATH, contextWindowSource);
228
+ console.log(`Wrote ${CONTEXT_WINDOWS_PATH}`);
229
+ } else {
230
+ console.log(`Skipped ${CONTEXT_WINDOWS_PATH}; pass --context-windows to refresh checkpoint-derived context windows.`);
231
+ }
232
+ } else {
233
+ console.log("Dry run only. Re-run with --write to update snapshots.");
234
+ }
@@ -4,6 +4,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
  import { BUNDLED_CONTEXT_WINDOWS } from "./bundled-context-windows.js";
5
5
 
6
6
  const CONTEXT_WINDOW_CACHE_FILE = "cursor-sdk-context-windows.json";
7
+ let userContextWindowOverrideLoadCount = 0;
7
8
 
8
9
  interface ContextWindowCacheFile {
9
10
  contextWindows?: Record<string, number>;
@@ -18,6 +19,7 @@ function isPositiveInteger(value: unknown): value is number {
18
19
  }
19
20
 
20
21
  function loadUserContextWindowOverrides(): Map<string, number> {
22
+ userContextWindowOverrideLoadCount += 1;
21
23
  const path = getCachePath();
22
24
  const overrides = new Map<string, number>();
23
25
  if (!existsSync(path)) return overrides;
@@ -80,4 +82,8 @@ export function saveCachedContextWindow(modelId: string, contextWindow: number):
80
82
 
81
83
  export const __testUtils = {
82
84
  getCachePath,
85
+ getUserContextWindowOverrideLoadCount: () => userContextWindowOverrideLoadCount,
86
+ resetUserContextWindowOverrideLoadCount: () => {
87
+ userContextWindowOverrideLoadCount = 0;
88
+ },
83
89
  };