pi-cursor-sdk 0.1.9 → 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 +21 -0
- package/README.md +29 -10
- package/docs/cursor-model-ux-spec.md +16 -19
- package/package.json +4 -2
- package/scripts/refresh-cursor-model-snapshots.mjs +234 -0
- package/src/context.ts +128 -35
- package/src/cursor-fallback-models.generated.ts +145 -0
- package/src/cursor-native-tool-display.ts +148 -13
- package/src/cursor-provider.ts +32 -5
- package/src/cursor-tool-transcript.ts +44 -5
- package/src/index.ts +35 -8
- package/src/model-discovery.ts +2 -142
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
3
24
|
## 0.1.9 - 2026-05-14
|
|
4
25
|
|
|
5
26
|
### 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 `/
|
|
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,
|
|
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 `/
|
|
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 `/
|
|
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 `
|
|
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
|
-
- **
|
|
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 `/
|
|
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
|
-
|
|
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 `
|
|
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
|
-
-
|
|
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
|
|
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 `
|
|
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,
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.
|
|
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
|
+
}
|