pi-cursor-sdk 0.1.9 → 0.1.11

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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.11 - 2026-05-18
4
+
5
+ ### Changed
6
+ - Updated the local pi package baseline to `@earendil-works/*` `0.75.3`, including the Node.js `>=22.19.0` runtime floor and refreshed npm lockfile.
7
+ - Added prompt metadata for the non-mutating Cursor replay tools so pi can describe `cursor_edit` and `cursor_write` more clearly in tool guidance.
8
+ - Removed tracked CueLoop runtime state from the repository and ignored local `.cueloop/` artifacts.
9
+
10
+
11
+ ## 0.1.10 - 2026-05-15
12
+
13
+ ### Added
14
+
15
+ - 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.
16
+ - 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.
17
+
18
+ ### Changed
19
+
20
+ - Improve Cursor edit/write replay card UX with concise created/updated/deleted/unchanged summaries and expanded colored diffs.
21
+ - Clarify image follow-up behavior: only latest user-message image bytes are forwarded; earlier images remain transcript placeholders and should be reattached or described.
22
+ - Allow `/cursor-refresh-models` to refresh the live Cursor model catalog after auth changes without restarting pi.
23
+ - Label local read fallback previews as transcript-time local previews when Cursor read result content is unavailable.
24
+
25
+ ### Fixed
26
+
27
+ - Prevent local read fallback previews from escaping the workspace through symlinks and from bypassing sensitive-path checks through sensitive symlink names.
28
+ - Budget oversized prompt history before `Agent.send`, including image-token reservations, while preserving system/tool-boundary instructions and the latest user request.
29
+ - Preserve assistant text emitted before native Cursor tool replay.
30
+ - Use the pi session cwd for native replay tool registration and update fallback execution to the latest session cwd.
31
+
3
32
  ## 0.1.9 - 2026-05-14
4
33
 
5
34
  ### Fixed
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
 
@@ -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
 
@@ -206,15 +206,15 @@ If no key is available from `/login`, `CURSOR_API_KEY`, or `--api-key`, model di
206
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,12 +13,12 @@ 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
- - Product decision pending: image payload forwarding currently 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. Changing this to carry images forward across turns requires a deliberate product decision about token cost, privacy, stale visual context, and expected multimodal follow-up behavior.
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.
17
17
  - `@cursor/sdk` is a package dependency of this extension; users should not need a global SDK install.
18
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.
19
- - 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`.
20
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.
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`, 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.
22
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.
23
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.
24
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.
@@ -66,7 +66,7 @@ Discovery resolves `apiKey` in this order:
66
66
  2. Stored pi auth for provider `cursor` from `AuthStorage.create().getApiKey("cursor", { includeFallback: false })`.
67
67
  3. `CURSOR_API_KEY`.
68
68
 
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, 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.
70
70
 
71
71
  For each model, use:
72
72
 
@@ -77,7 +77,7 @@ For each model, use:
77
77
  - `model.variants`
78
78
  - default variant: `variant.isDefault === true`, else first variant
79
79
 
80
- 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.
81
81
 
82
82
  Pi model metadata is also a source of truth for pi-native behavior:
83
83
 
@@ -93,24 +93,21 @@ If a Cursor parameter changes any of those pi-native fields, model registration
93
93
 
94
94
  ### Refresh Current Cursor Matrix
95
95
 
96
- 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:
97
97
 
98
98
  ```bash
99
- node --input-type=module <<'EOF'
100
- import { Cursor } from '@cursor/sdk';
101
-
102
- const models = await Cursor.models.list({ apiKey: process.env.CURSOR_API_KEY });
103
- for (const model of models) {
104
- const options = (model.parameters ?? [])
105
- .map((param) => `${param.id}: ${param.values.map((value) => value.value).join(', ')}`)
106
- .join(' | ') || 'none';
107
- const defaultVariant = model.variants?.find((variant) => variant.isDefault) ?? model.variants?.[0];
108
- const defaults = defaultVariant?.params?.map((param) => `${param.id}=${param.value}`).join('; ') || 'none';
109
- console.log(`${model.id}\t${model.displayName}\t${options}\t${defaults}`);
110
- }
111
- 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
112
107
  ```
113
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
+
114
111
  ## Design Direction
115
112
 
116
113
  Use native pi abstractions wherever possible:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",
@@ -30,23 +31,28 @@
30
31
  ],
31
32
  "type": "module",
32
33
  "engines": {
33
- "node": ">=18"
34
+ "node": ">=22.19.0"
34
35
  },
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"
42
44
  },
43
45
  "peerDependencies": {
44
46
  "@earendil-works/pi-ai": "*",
45
- "@earendil-works/pi-coding-agent": "*"
47
+ "@earendil-works/pi-coding-agent": "*",
48
+ "@earendil-works/pi-tui": "*",
49
+ "typebox": "*"
46
50
  },
47
51
  "devDependencies": {
48
- "@earendil-works/pi-ai": "^0.74.0",
49
- "@earendil-works/pi-coding-agent": "^0.74.0",
52
+ "@earendil-works/pi-ai": "^0.75.3",
53
+ "@earendil-works/pi-coding-agent": "^0.75.3",
54
+ "@earendil-works/pi-tui": "^0.75.3",
55
+ "typebox": "^1.1.38",
50
56
  "typescript": "^6.0.3",
51
57
  "vitest": "^4.1.6"
52
58
  },
@@ -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
+ }