pi-cursor-sdk 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.31 - 2026-06-01
6
+
7
+ ### Added
8
+
9
+ - Add Cursor `:fast` and `:slow` virtual model aliases for models with a Cursor SDK `fast` parameter so subagents and workflow-spawned agents can choose fast/slow independently of saved `/cursor-fast` defaults (#112).
10
+
11
+ ## 0.1.30 - 2026-06-01
12
+
13
+ ### Added
14
+
15
+ - Preserve pi Agent Skills for Cursor runs by rewriting pi's skill catalog into Cursor-safe activation instructions and exposing `cursor_activate_skill` through the pi MCP bridge as `pi__cursor_activate_skill` when visible pi skills are available (#113).
16
+
17
+ ### Changed
18
+
19
+ - Document that deprecated install warnings currently come from the closed-source Cursor SDK's `sqlite3@5.1.x` transitive dependency chain, and document root-project override limits/workarounds instead of relying on unsupported transitive package overrides (#115).
20
+
5
21
  ## 0.1.29 - 2026-06-01
6
22
 
7
23
  ### Added
package/README.md CHANGED
@@ -34,7 +34,7 @@ If pi started without a key, run `/cursor-refresh-models` after `/login` to refr
34
34
  - pi 0.76.0 or newer
35
35
  - a Cursor SDK API key saved through `/login`, available as `CURSOR_API_KEY`, or passed with pi's `--api-key`
36
36
 
37
- No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.17`, so normal package installation brings in the SDK version this extension was built and tested against. This package declares a pi **minimum** of 0.76.0 with no maximum peer version, so users who update pi before this extension is republished are not blocked from trying the existing extension. The current validation baseline is pi 0.78.0 plus Cursor SDK 1.0.17; older pi or Cursor SDK compatibility paths are not maintained.
37
+ No global `@cursor/sdk` install is required. This package depends on exact `@cursor/sdk@1.0.17`, so normal package installation brings in the SDK version this extension was built and tested against. The Cursor SDK currently depends on `sqlite3@^5.1.7`, whose install path can print deprecated transitive `node-gyp@8` dependency warnings such as `inflight`, `rimraf`, `glob`, `npmlog`, `gauge`, `are-we-there-yet`, and `tar@6`. Those warnings are non-fatal and come from the closed-source Cursor SDK dependency boundary; this package cannot force npm overrides into consumer projects. If you install from a root `package.json` you control, you may choose a root-level override such as `"overrides": { "sqlite3": "6.0.1" }`; pi package installs will still follow npm's normal transitive dependency rules. This package declares a pi **minimum** of 0.76.0 with no maximum peer version, so users who update pi before this extension is republished are not blocked from trying the existing extension. The current validation baseline is pi 0.78.0 plus Cursor SDK 1.0.17; older pi or Cursor SDK compatibility paths are not maintained.
38
38
 
39
39
  ## Install
40
40
 
@@ -168,7 +168,7 @@ pi --model cursor/gpt-5.5@272k:xhigh
168
168
  pi --model cursor/gpt-5.5@1m --thinking medium
169
169
  ```
170
170
 
171
- Cursor-only parameters are not encoded into pi model IDs. Cursor `context` becomes a pi-visible model variant because it changes pi's native `contextWindow`; Cursor `fast` and Cursor SDK conversation mode are extension state, not model identity. Alias model IDs use their selected SDK ID for Cursor-only state such as fast defaults, with read fallback for older defaults keyed by the underlying Cursor base model.
171
+ Cursor `context` becomes a pi-visible model variant because it changes pi's native `contextWindow`. For models that expose Cursor's boolean `fast` parameter, the extension also registers virtual `:fast` and `:slow` model aliases such as `cursor/composer-2-5:slow` and `cursor/gpt-5.5@1m:fast`. Those aliases are selection-only controls for subagents and workflow-spawned agents: they send the same Cursor SDK model ID plus an explicit `fast=true` or `fast=false` param, and they take precedence over saved `/cursor-fast` session/global defaults. Cursor SDK conversation mode remains extension state, not model identity. Alias model IDs use their selected SDK ID for Cursor-only state such as fast defaults, with read fallback for older defaults keyed by the underlying Cursor base model.
172
172
 
173
173
  ## Thinking support
174
174
 
@@ -190,7 +190,7 @@ Some Cursor SDK models do not expose a `reasoning`, `effort`, or `thinking` para
190
190
 
191
191
  ## Fast mode
192
192
 
193
- Use `/cursor-fast` to persistently toggle fast mode for the selected Cursor model when the model supports Cursor's `fast` parameter.
193
+ Use `/cursor-fast` to persistently toggle fast mode for the selected unsuffixed Cursor model when the model supports Cursor's `fast` parameter.
194
194
 
195
195
  Fast preferences are remembered per selected Cursor SDK model ID or alias and stored:
196
196
 
@@ -204,7 +204,16 @@ pi --model cursor/gpt-5.5@1m --cursor-fast -p "Say ok only"
204
204
  pi --model cursor/composer-2-5 --cursor-no-fast -p "Say ok only"
205
205
  ```
206
206
 
207
- Composer 2 and Composer 2.5 can default to fast. Use `--cursor-no-fast` for a one-shot no-fast Composer run. In print mode (`-p`), `--cursor-no-fast` is silent and does not write `~/.pi/agent/cursor-sdk.json`.
207
+ For per-agent control, select the virtual model alias instead of mutating the shared saved default:
208
+
209
+ ```bash
210
+ pi --model cursor/composer-2-5:slow -p "Say ok only"
211
+ pi --model cursor/gpt-5.5@1m:fast -p "Say ok only"
212
+ ```
213
+
214
+ The `:fast` and `:slow` aliases are available only for Cursor models whose catalog exposes a `fast` parameter. They override saved `/cursor-fast` session/global defaults while leaving `--cursor-fast` and `--cursor-no-fast` as explicit process-level force flags. `/cursor-fast` does not persist a new default while a virtual fast/slow alias is selected; switch to the unsuffixed model first.
215
+
216
+ Composer 2 and Composer 2.5 can default to fast. Use `--cursor-no-fast` or a `:slow` virtual alias for a one-shot no-fast Composer run. In print mode (`-p`), `--cursor-no-fast` is silent and does not write `~/.pi/agent/cursor-sdk.json`.
208
217
 
209
218
  In interactive mode, the footer only shows fast mode when fast is enabled and Cursor mode when it is non-default. Fast and plan mode share one Cursor status value, so they do not overwrite each other:
210
219
 
@@ -218,7 +227,7 @@ If you do not see `cursor fast`, fast mode is off. If you do not see `cursor pla
218
227
 
219
228
  ## Cursor SDK mode
220
229
 
221
- Cursor SDK conversation mode is Cursor-only extension state. It is not a pi model variant, not pi thinking/reasoning, not Cursor `fast`, and not pi's separate read-only plan-mode extension.
230
+ Cursor SDK conversation mode is Cursor-only extension state. It is not a pi model variant, not pi thinking/reasoning, not a `:fast`/`:slow` virtual fast alias, and not pi's separate read-only plan-mode extension.
222
231
 
223
232
  Default mode is `agent`. Start a one-shot run in a specific mode:
224
233
 
@@ -259,7 +268,7 @@ Cursor runs use local Cursor SDK agents with two separate tool surfaces:
259
268
 
260
269
  Bridge capabilities are snapshotted from `pi.getActiveTools()` and `pi.getAllTools()` for each Cursor run, including per-tool prompt guidelines when pi exposes them. Cursor sees active bridgeable pi tools as collision-safe MCP names such as `pi__sem_reindex` only when they are exposed in that current run. Pi session output, tool cards, confirmations, hooks, renderers, history, and abort behavior use the real pi tool name, such as `sem_reindex`. The bridge queues Cursor's MCP call, emits a normal pi `toolCall`, waits for the matching pi `toolResult`, and resolves that result back into the same live Cursor SDK run without creating a new `Agent`, unless the run was disposed, aborted, or cancelled. The bridge does not call pi tool `execute()` handlers directly.
261
270
 
262
- Overlapping built-in pi tools (`read`, `bash`, `write`, `edit`, `grep`, `find`, `ls`) are hidden by default because Cursor local agents already have native equivalents. Extension/custom tools and non-overlapping active tools present in pi's active tool registry normally remain exposed. The bridge also exposes `cursor_ask_question` as `pi__cursor_ask_question` when enabled, allowing Cursor to ask the user through pi UI instead of silently choosing a default.
271
+ Overlapping built-in pi tools (`read`, `bash`, `write`, `edit`, `grep`, `find`, `ls`) are hidden by default because Cursor local agents already have native equivalents. Extension/custom tools and non-overlapping active tools present in pi's active tool registry normally remain exposed. The bridge also exposes `cursor_ask_question` as `pi__cursor_ask_question` when enabled, allowing Cursor to ask the user through pi UI instead of silently choosing a default. When pi has visible Agent Skills loaded, the extension rewrites pi's skill catalog for Cursor and exposes `cursor_activate_skill` as `pi__cursor_activate_skill`; Cursor should call that bridge tool with a listed skill name to load the full `SKILL.md` and bundled resource list before applying the skill. If the bridge is disabled, the catalog remains available and instructs Cursor to fall back to reading the listed `SKILL.md` path directly.
263
272
 
264
273
  Cursor-native tool replay is separate from the bridge. Replay cards are display-only recorded Cursor SDK activity. They never re-run Cursor-side commands, reapply Cursor edits, call MCP servers, or mutate pi state. See [Cursor native tool replay](docs/cursor-native-tool-replay.md).
265
274
 
@@ -10,7 +10,7 @@ Current implementation notes:
10
10
 
11
11
  - Cursor context variants use `base@context` pi model IDs.
12
12
  - Cursor `reasoning`, `effort`, and boolean `thinking` parameters are driven by pi native thinking when the Cursor SDK exposes those controls.
13
- - Cursor `fast` is extension state, not model identity.
13
+ - Cursor `fast` is extension state by default; models that expose `fast` also get selection-only `:fast` / `:slow` virtual aliases for per-agent overrides.
14
14
  - Cursor SDK `mode` (`agent` or `plan`) is extension session state, not model identity, pi thinking, Cursor `fast`, or pi's separate plan-mode extension.
15
15
  - Cursor status uses one coordinated `ctx.ui.setStatus("cursor", ...)` value for fast and non-default plan mode; the default pi footer remains intact.
16
16
  - Installed `@cursor/sdk` user messages accept images, and Cursor models are treated as image-capable; registered input metadata is `text` plus `image`.
@@ -24,7 +24,7 @@ Current implementation notes:
24
24
  - Local Cursor agents get two tool surfaces. First, Cursor keeps the Cursor SDK local-agent tool surface plus configured Cursor settings, plugins, and Cursor MCP servers. Second, pi-cursor-sdk exposes active pi tools through a default-on, tokenized loopback MCP bridge when bridgeable tools exist.
25
25
  - `buildCursorPiToolBridgeSnapshot()` is the runtime capability source for pi bridge tools. It snapshots `pi.getActiveTools()` and `pi.getAllTools()`, carries pi 0.77+ per-tool `promptGuidelines` into bridge MCP descriptions, filters internal replay names, hides overlapping built-in pi tools (`read`, `bash`, `write`, `edit`, `grep`, `find`, `ls`) unless `PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1`, and creates collision-safe MCP names such as `pi__sem_reindex`. Cursor discovers the current run's exposed bridge tools through MCP `listTools`. Bootstrap prompts include a compact callable-surface manifest from `buildCursorToolManifestText()` by default (`PI_CURSOR_TOOL_MANIFEST=1`); disable with `PI_CURSOR_TOOL_MANIFEST=0`. There is no per-turn visible tool list, status manifest, or footer manifest. User-facing summary: [Cursor tool surfaces in pi](./cursor-tool-surfaces.md).
26
26
  - Prompt text is the primary provider/bridge contract. Bootstrap prompts carry a short boundary block plus the callable-surface manifest by default (`PI_CURSOR_TOOL_MANIFEST=1`). MCP `listTools` descriptions use a one-line pointer to the bootstrap prompt instead of repeating the full contract (`buildCursorPiBridgeMcpToolDescription()`). Cursor must call the exposed `pi__*` MCP name, not the real pi tool name shown in pi history or transcripts. Pi emits and executes the real pi tool name. Maintainer debug: `/cursor-tools` prints bridge/manifest enablement, effective `PI_CURSOR_SETTING_SOURCES`, and the current callable-surface snapshot.
27
- - The provider also registers `cursor_ask_question` for Cursor models when the bridge is enabled. Cursor sees it as `pi__cursor_ask_question`, and pi executes it through the normal tool path so interactive users can choose options from pi UI. In non-UI modes it reports that UI is unavailable so Cursor can state a default assumption instead. `PI_CURSOR_PI_TOOL_BRIDGE=0` disables the local bridge, including question bridging. Cloud Cursor agents remain out of scope for the bridge.
27
+ - The provider also registers `cursor_ask_question` for Cursor models when the bridge is enabled. Cursor sees it as `pi__cursor_ask_question`, and pi executes it through the normal tool path so interactive users can choose options from pi UI. In non-UI modes it reports that UI is unavailable so Cursor can state a default assumption instead. When pi has visible Agent Skills loaded, the provider rewrites the skill catalog for Cursor and registers `cursor_activate_skill` as `pi__cursor_activate_skill`; pi executes it through the normal tool path so Cursor can load the full `SKILL.md` and skill resource list for the current pi-loaded skill source of truth. `PI_CURSOR_PI_TOOL_BRIDGE=0` disables the local bridge, including question and skill activation bridging. Cloud Cursor agents remain out of scope for the bridge.
28
28
  - The bridge queues MCP calls, emits provider `toolcall_*` events, waits for matching pi `toolResult` messages by `toolCallId`, resolves the result back into the same live Cursor SDK run without creating a new `Agent`, and never calls tool `execute()` handlers directly. The same-run resume invariant holds unless the run was disposed, aborted, or cancelled.
29
29
  - Cursor SDK MCP tool calls use a guarded timeout override because installed `@cursor/sdk` 1.0.17 has a 60-second MCP request default with no public per-server timeout option. The extension extends the verified Cursor SDK MCP `callTool` timeout path to 3600 seconds by default and shortens the verified first-send MCP initialize/listTools timeout paths to 10 seconds by default so unavailable configured MCP servers do not block the first reply for a full minute; unknown MCP protocol timeout stacks keep the SDK default. Users can override tool-call timeouts with `PI_CURSOR_MCP_TOOL_TIMEOUT_MS` or `PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS`, and initialize/listTools timeouts with `PI_CURSOR_MCP_CONNECT_TIMEOUT_MS` or `PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS`.
30
30
  - Bridge diagnostics are opt-in only: `PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1` writes typed, allowlisted, scrubbed single-line JSONL records to `process.stderr` with prefix `[pi-cursor-sdk:bridge]`. Diagnostics are scrubbed operational logs, not anonymous telemetry. They intentionally include tool names, safe correlation IDs, run lifecycle, exposed pi↔MCP name pairs, queued requests, result resolution, rejection, cancellation, and pending counts. Correlation IDs are generated independently from the tokenized endpoint path, and Cursor MCP call IDs are hashed before serialization. Diagnostics must not include endpoint paths/URLs/path components/tokens, API keys, bearer tokens, cookies, session credentials, raw args/results, stdout/stderr payloads, file contents, Cursor settings output, or local private session paths in tracked docs, and they must not call pi UI status, notification, or footer APIs. If tool names themselves are unacceptable for a release target, bridge debug diagnostics are not safe for shared logs under the current contract.
@@ -136,7 +136,7 @@ Use native pi abstractions wherever possible:
136
136
  | Cursor `reasoning` | pi native thinking via `thinkingLevelMap` |
137
137
  | Cursor `effort` | pi native thinking via `thinkingLevelMap` |
138
138
  | Cursor `thinking=false` | pi native `off` |
139
- | Cursor `fast` | extension state, not model identity |
139
+ | Cursor `fast` | extension state plus `:fast` / `:slow` virtual aliases for per-agent overrides |
140
140
  | Cursor SDK `mode` | extension session state; `agent` by default, `plan` via SDK-native mode |
141
141
  | Footer | default pi footer plus optional extension status |
142
142
 
@@ -156,7 +156,7 @@ Rules:
156
156
  - Register one pi model for each Cursor base model and each unambiguous SDK alias when there is no Cursor `context` parameter.
157
157
  - Register one pi model per Cursor `context` value for each Cursor base model and each unambiguous SDK alias when the model exposes a `context` parameter.
158
158
  - Skip SDK aliases that collide with another base model ID or are shared by multiple base models; those aliases can resolve differently from the pi row metadata.
159
- - Do not encode `reasoning`, `effort`, `thinking`, `fast`, or Cursor SDK `mode` into pi model IDs.
159
+ - Do not encode `reasoning`, `effort`, `thinking`, or Cursor SDK `mode` into pi model IDs. For models with a Cursor `fast` parameter, also register selection-only `:fast` and `:slow` virtual model aliases that do not change pi-native metadata.
160
160
  - Prefer stable, readable `@<context>` suffixes that do not conflict with pi's final `:<thinking>` suffix parser.
161
161
  - Sort Cursor models by base ID, then context value in Cursor SDK order before calling `pi.registerProvider()`. Registration order matters for `/model` display and model cycling; `--list-models` sorts output separately.
162
162
 
@@ -168,6 +168,9 @@ cursor/gpt-5.5@272k
168
168
  cursor/claude-opus-4-8@1m
169
169
  cursor/claude-opus-4-8@300k
170
170
  cursor/composer-2-5
171
+ cursor/composer-2-5:fast
172
+ cursor/composer-2-5:slow
173
+ cursor/gpt-5.5@1m:fast
171
174
  ```
172
175
 
173
176
  Avoid colon-based context IDs in the first implementation unless this spec is intentionally changed:
@@ -190,7 +193,7 @@ Reason:
190
193
 
191
194
  - `@1m` keeps context visually separate from pi's native `:medium` thinking suffix.
192
195
  - Context variants make `contextWindow` accurate in `--list-models`, the native footer, context overflow checks, and compaction logic.
193
- - `fast` is intentionally not a model variant because it does not affect pi model metadata and would double list noise.
196
+ - `:fast` / `:slow` are virtual aliases, not separate Cursor SDK base models: they keep the same context/thinking metadata and only force the outgoing Cursor `fast` param. They exist so subagents and workflow-spawned agents can choose fast/slow without mutating shared `/cursor-fast` defaults.
194
197
 
195
198
  ### Metadata Per Registered Model
196
199
 
@@ -362,17 +365,18 @@ fast=false <-> fast=true
362
365
 
363
366
  Rules:
364
367
 
365
- - `fast` is extension state, not pi model identity.
366
- - Toggle with `/cursor-fast`.
367
- - Store per-session and global per-base-model preferences.
368
- - When calling `Agent.create()`, include the selected `fast` value in Cursor model params.
368
+ - Unsuffixed models use extension state from `/cursor-fast`, per-session entries, and global defaults.
369
+ - `:fast` / `:slow` virtual model aliases force fast on/off for that selected agent and override saved defaults without writing state.
370
+ - Toggle unsuffixed models with `/cursor-fast`; do not persist a new default while a virtual fast alias is selected.
371
+ - Store per-session and global per-base-model preferences for unsuffixed models.
372
+ - When calling `Agent.create()` or `agent.send()`, include the selected `fast` value in Cursor model params.
369
373
  - Show `fast` through `ctx.ui.setStatus()` when enabled.
370
- - Support a first-pass CLI flag, `--cursor-fast`, to force fast mode for one run when the selected model supports it.
374
+ - Keep `--cursor-fast` and `--cursor-no-fast` as explicit process-level force flags.
371
375
 
372
376
  Reason:
373
377
 
374
378
  - `fast` does not affect pi `contextWindow`, thinking levels, or input support.
375
- - Registering fast/non-fast variants would make `--list-models` noisy without improving native pi behavior.
379
+ - The virtual aliases trade small `--list-models` noise for per-agent selection that works with subagents and dynamic workflows, where mutating a shared global fast default is the wrong abstraction.
376
380
 
377
381
  Status example:
378
382
 
@@ -521,7 +525,7 @@ Reason:
521
525
  - pi supports one final `:<thinking>` suffix.
522
526
  - Cursor-only parameters are not generic pi CLI parameters.
523
527
  - Context is already represented by the registered pi model ID.
524
- - `fast` is controlled by saved extension defaults or the first-pass `--cursor-fast` extension flag.
528
+ - `fast` is controlled by saved extension defaults, `:fast` / `:slow` virtual model aliases, or the `--cursor-fast` / `--cursor-no-fast` extension flags.
525
529
  - Cursor SDK `mode` is controlled by `/cursor-mode` session state or the first-pass `--cursor-mode` extension flag; it is never encoded in `--model`.
526
530
 
527
531
  For print mode:
@@ -529,7 +533,7 @@ For print mode:
529
533
  - no keybindings,
530
534
  - use selected context model variant,
531
535
  - use `--thinking` or `:medium` for reasoning/effort,
532
- - use saved global `fast` defaults unless `--cursor-fast` is present,
536
+ - use saved global `fast` defaults unless a virtual `:fast` / `:slow` model alias or force flag is present,
533
537
  - use Cursor SDK `agent` mode unless `/cursor-mode` session state or `--cursor-mode` overrides it.
534
538
 
535
539
  Fast flag example:
@@ -23,7 +23,7 @@ Cursor SDK `plan` mode (`--cursor-mode plan` or `/cursor-mode plan`) can make Cu
23
23
 
24
24
  ## Local pi bridge summary
25
25
 
26
- The bridge is enabled by default when bridgeable active pi tools exist. Cursor sees bridge-owned MCP names such as `pi__sem_reindex`, while pi history and tool cards use the real pi tool name such as `sem_reindex`. The bridge hides overlapping built-in pi tools by default because Cursor already has native equivalents; extension/custom tools and non-overlapping active tools present in pi's active tool registry normally remain exposed. pi-cursor-sdk also registers `cursor_ask_question` for Cursor models when the bridge is enabled, exposed to Cursor as `pi__cursor_ask_question`, so Cursor can ask the user to choose instead of silently defaulting when the pi UI is available. The bridge does not call pi tool `execute()` handlers directly; it queues the request, emits a real pi `toolCall`, waits for the matching pi `toolResult`, and resolves the Cursor MCP call back into the same live Cursor SDK run without creating a new `Agent`, unless the run was disposed, aborted, or cancelled.
26
+ The bridge is enabled by default when bridgeable active pi tools exist. Cursor sees bridge-owned MCP names such as `pi__sem_reindex`, while pi history and tool cards use the real pi tool name such as `sem_reindex`. The bridge hides overlapping built-in pi tools by default because Cursor already has native equivalents; extension/custom tools and non-overlapping active tools present in pi's active tool registry normally remain exposed. pi-cursor-sdk also registers `cursor_ask_question` for Cursor models when the bridge is enabled, exposed to Cursor as `pi__cursor_ask_question`, so Cursor can ask the user to choose instead of silently defaulting when the pi UI is available. When pi has visible Agent Skills loaded, pi-cursor-sdk registers `cursor_activate_skill`, exposed as `pi__cursor_activate_skill`, so Cursor can load the full pi `SKILL.md` that corresponds to the current pi skill catalog. The bridge does not call pi tool `execute()` handlers directly; it queues the request, emits a real pi `toolCall`, waits for the matching pi `toolResult`, and resolves the Cursor MCP call back into the same live Cursor SDK run without creating a new `Agent`, unless the run was disposed, aborted, or cancelled.
27
27
 
28
28
  Rollback, timeout, and diagnostics controls:
29
29
 
@@ -8,7 +8,7 @@ pi-cursor-sdk runs Cursor models through the local `@cursor/sdk` agent runtime.
8
8
  | --- | --- | --- | --- |
9
9
  | **Cursor SDK host tools** | Cursor local agent | Yes | Native replay cards (`read`, `bash`, …) or neutral Cursor activity. Representative ToolType list: [SDK ToolType replay matrix](./cursor-native-tool-replay.md#sdk-tooltype-replay-matrix). |
10
10
  | **Configured Cursor MCP** | Cursor settings / `~/.cursor/mcp.json` | Yes (when loaded) | Neutral **Cursor MCP** activity cards on replay |
11
- | **Pi bridge (`pi__*`)** | pi-cursor-sdk loopback MCP | Yes, when exposed | Real pi tool names (`cursor_ask_question`, extension tools, …) |
11
+ | **Pi bridge (`pi__*`)** | pi-cursor-sdk loopback MCP | Yes, when exposed | Real pi tool names (`cursor_ask_question`, `cursor_activate_skill`, extension tools, …) |
12
12
 
13
13
  **Not callable:** `cursor-replay-*` IDs in JSONL, pi history tool names used only for display, and transcript labels. Cursor must call exposed `pi__*` MCP names for bridged pi tools, not the pi card name.
14
14
 
@@ -27,7 +27,7 @@ Default behavior:
27
27
  - The pi bridge exposes **active pi tools** as `pi__*` MCP names when `PI_CURSOR_PI_TOOL_BRIDGE` is enabled (default on).
28
28
  - Overlapping pi builtins (`read`, `bash`, `write`, `edit`, `grep`, `find`, `ls`) are **hidden** from the bridge unless `PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1`.
29
29
 
30
- `pi-cursor-sdk` always registers `cursor_ask_question` for Cursor models when the bridge is on; Cursor sees `pi__cursor_ask_question`.
30
+ `pi-cursor-sdk` always registers `cursor_ask_question` for Cursor models when the bridge is on; Cursor sees `pi__cursor_ask_question`. When pi has visible Agent Skills loaded, the extension also rewrites pi's skill catalog for Cursor and activates `cursor_activate_skill`; Cursor sees `pi__cursor_activate_skill` and should call it with a listed skill name before applying that skill. The activation result returns the full `SKILL.md`, the skill directory for relative paths, and a bounded list of bundled `scripts/`, `references/`, and `assets/` files without eagerly reading those resources.
31
31
 
32
32
  ```bash
33
33
  # Disable pi bridge entirely
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "pi provider extension backed by @cursor/sdk local agents",
5
5
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
6
6
  "license": "MIT",
package/src/context.ts CHANGED
@@ -114,10 +114,8 @@ function sanitizeSystemPromptForCursor(systemPrompt: string): string {
114
114
  /Guidelines:\n[\s\S]*?\n\nPi documentation /g,
115
115
  "Guidelines:\n- Be concise in your responses.\n- Show file paths clearly when working with files.\n\nPi documentation ",
116
116
  );
117
- sanitized = sanitized.replace(
118
- /\n\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>/g,
119
- "",
120
- );
117
+ // Keep the Agent Skills catalog. Cursor-specific skill activation wording is normalized
118
+ // by cursor-skill-tool.ts before this prompt reaches the Cursor SDK provider.
121
119
  sanitized = sanitized.replace(/\n+Semantic code intelligence priority:[\s\S]*$/g, "");
122
120
  return sanitized.trim();
123
121
  }
@@ -0,0 +1,273 @@
1
+ import type { Dirent } from "node:fs";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { dirname, join, relative } from "node:path";
4
+ import type {
5
+ BeforeAgentStartEvent,
6
+ BeforeAgentStartEventResult,
7
+ BuildSystemPromptOptions,
8
+ ExtensionAPI,
9
+ ExtensionContext,
10
+ ExtensionHandler,
11
+ SessionStartEvent,
12
+ Skill,
13
+ TurnStartEvent,
14
+ } from "@earendil-works/pi-coding-agent";
15
+ import { Type } from "typebox";
16
+ import { isCursorModel } from "./cursor-model.js";
17
+ import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-snapshot.js";
18
+
19
+ export const CURSOR_ACTIVATE_SKILL_TOOL_NAME = "cursor_activate_skill";
20
+ export const CURSOR_ACTIVATE_SKILL_MCP_NAME = "pi__cursor_activate_skill";
21
+
22
+ const AVAILABLE_SKILLS_SECTION_PATTERN = /\n\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>/;
23
+ const MAX_SKILL_RESOURCES = 80;
24
+ const RESOURCE_DIR_NAMES = ["scripts", "references", "assets"] as const;
25
+
26
+ type CursorSkillToolExtensionApi = Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools"> & {
27
+ on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
28
+ on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;
29
+ on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
30
+ on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
31
+ };
32
+
33
+ type CursorActivateSkillParams = {
34
+ name?: string;
35
+ };
36
+
37
+ interface CursorSkillActivationDetails {
38
+ name?: string;
39
+ filePath?: string;
40
+ baseDir?: string;
41
+ resources: string[];
42
+ availableSkillNames: string[];
43
+ }
44
+
45
+ let currentSkillsByName = new Map<string, Skill>();
46
+
47
+ function escapeXml(value: string): string {
48
+ return value
49
+ .replace(/&/g, "&amp;")
50
+ .replace(/</g, "&lt;")
51
+ .replace(/>/g, "&gt;")
52
+ .replace(/\"/g, "&quot;")
53
+ .replace(/'/g, "&apos;");
54
+ }
55
+
56
+ function getVisibleSkills(skills: readonly Skill[] | undefined): Skill[] {
57
+ return (skills ?? []).filter((skill) => !skill.disableModelInvocation);
58
+ }
59
+
60
+ function setCurrentSkills(skills: readonly Skill[] | undefined): void {
61
+ currentSkillsByName = new Map(getVisibleSkills(skills).map((skill) => [skill.name, skill]));
62
+ }
63
+
64
+ function getAvailableSkillNames(): string[] {
65
+ return [...currentSkillsByName.keys()].sort();
66
+ }
67
+
68
+ function shouldExposeSkillTool(model: ExtensionContext["model"]): boolean {
69
+ return isCursorModel(model) && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
70
+ }
71
+
72
+ function syncCursorSkillToolForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
73
+ const activeToolNames = new Set(pi.getActiveTools());
74
+ const shouldBeActive = shouldExposeSkillTool(model);
75
+ const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
76
+ if (shouldBeActive === alreadyActive) return;
77
+ if (shouldBeActive) {
78
+ activeToolNames.add(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
79
+ } else {
80
+ activeToolNames.delete(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
81
+ }
82
+ pi.setActiveTools([...activeToolNames]);
83
+ }
84
+
85
+ export function formatCursorSkillsForPrompt(skills: readonly Skill[]): string {
86
+ const visibleSkills = getVisibleSkills(skills);
87
+ if (visibleSkills.length === 0) return "";
88
+
89
+ const lines = [
90
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
91
+ `When a task matches a skill's description, call ${CURSOR_ACTIVATE_SKILL_MCP_NAME} with the skill name to load its full SKILL.md instructions before proceeding.`,
92
+ "If the pi bridge is disabled and the activation tool is unavailable, use Cursor's file-read capability on the listed SKILL.md location instead.",
93
+ "When a skill references relative paths, resolve them against the skill directory (the parent of SKILL.md / dirname of the path) and use absolute paths in tool calls.",
94
+ "",
95
+ "<available_skills>",
96
+ ];
97
+ for (const skill of visibleSkills) {
98
+ lines.push(" <skill>");
99
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
100
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
101
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
102
+ lines.push(" </skill>");
103
+ }
104
+ lines.push("</available_skills>");
105
+ return lines.join("\n");
106
+ }
107
+
108
+ export function resolveCursorSkillSystemPrompt(
109
+ systemPrompt: string,
110
+ model: ExtensionContext["model"],
111
+ systemPromptOptions?: BuildSystemPromptOptions,
112
+ ): string {
113
+ if (!isCursorModel(model)) return systemPrompt;
114
+ const skills = getVisibleSkills(systemPromptOptions?.skills);
115
+ if (skills.length === 0) return systemPrompt;
116
+ const replacement = formatCursorSkillsForPrompt(skills);
117
+ if (AVAILABLE_SKILLS_SECTION_PATTERN.test(systemPrompt)) {
118
+ return systemPrompt.replace(AVAILABLE_SKILLS_SECTION_PATTERN, replacement);
119
+ }
120
+ return `${systemPrompt}${replacement}`;
121
+ }
122
+
123
+ async function collectResourcePaths(root: string, absoluteDir: string, output: string[]): Promise<void> {
124
+ if (output.length >= MAX_SKILL_RESOURCES) return;
125
+ let entries: Dirent[];
126
+ try {
127
+ entries = await readdir(absoluteDir, { withFileTypes: true });
128
+ } catch {
129
+ return;
130
+ }
131
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
132
+ if (output.length >= MAX_SKILL_RESOURCES) return;
133
+ const absolutePath = join(absoluteDir, entry.name);
134
+ if (entry.isSymbolicLink()) continue;
135
+ if (entry.isDirectory()) {
136
+ await collectResourcePaths(root, absolutePath, output);
137
+ continue;
138
+ }
139
+ if (!entry.isFile()) continue;
140
+ output.push(relative(root, absolutePath).replace(/\\/g, "/"));
141
+ }
142
+ }
143
+
144
+ async function listSkillResourcePaths(baseDir: string): Promise<string[]> {
145
+ const resources: string[] = [];
146
+ for (const resourceDirName of RESOURCE_DIR_NAMES) {
147
+ await collectResourcePaths(baseDir, join(baseDir, resourceDirName), resources);
148
+ if (resources.length >= MAX_SKILL_RESOURCES) break;
149
+ }
150
+ return resources;
151
+ }
152
+
153
+ function buildActivationDetails(skill: Skill | undefined, resources: string[] = []): CursorSkillActivationDetails {
154
+ return {
155
+ name: skill?.name,
156
+ filePath: skill?.filePath,
157
+ baseDir: skill ? dirname(skill.filePath) : undefined,
158
+ resources,
159
+ availableSkillNames: getAvailableSkillNames(),
160
+ };
161
+ }
162
+
163
+ function formatSkillResources(resources: readonly string[]): string {
164
+ if (resources.length === 0) return "<skill_resources />";
165
+ return [
166
+ "<skill_resources>",
167
+ ...resources.map((resource) => ` <file>${escapeXml(resource)}</file>`),
168
+ "</skill_resources>",
169
+ ].join("\n");
170
+ }
171
+
172
+ function wrapSkillContent(skill: Skill, content: string, resources: readonly string[]): string {
173
+ const baseDir = dirname(skill.filePath);
174
+ return [
175
+ `<skill_content name=\"${escapeXml(skill.name)}\">`,
176
+ content.trim(),
177
+ "",
178
+ `Skill directory: ${baseDir}`,
179
+ "Relative paths in this skill are relative to the skill directory.",
180
+ formatSkillResources(resources),
181
+ "</skill_content>",
182
+ ].join("\n");
183
+ }
184
+
185
+ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
186
+ pi.registerTool({
187
+ name: CURSOR_ACTIVATE_SKILL_TOOL_NAME,
188
+ label: "Cursor skill",
189
+ description: "Load full pi Agent Skill instructions for Cursor. Use with a skill name from the current <available_skills> catalog before applying that skill.",
190
+ parameters: Type.Object({
191
+ name: Type.String({ description: "Skill name from the current <available_skills> catalog" }),
192
+ }),
193
+ promptGuidelines: [
194
+ `Use ${CURSOR_ACTIVATE_SKILL_TOOL_NAME} only for skill names listed in the current <available_skills> catalog.`,
195
+ "After loading a skill, follow its instructions and resolve relative skill paths against the returned skill directory.",
196
+ ],
197
+ async execute(_toolCallId, params) {
198
+ const requestedName = (params as CursorActivateSkillParams).name?.trim();
199
+ if (!requestedName) {
200
+ return {
201
+ content: [{ type: "text" as const, text: "No skill name was provided." }],
202
+ details: buildActivationDetails(undefined),
203
+ isError: true,
204
+ };
205
+ }
206
+ const skill = currentSkillsByName.get(requestedName);
207
+ if (!skill) {
208
+ return {
209
+ content: [{ type: "text" as const, text: `Skill not available: ${requestedName}. Available skills: ${getAvailableSkillNames().join(", ") || "none"}.` }],
210
+ details: buildActivationDetails(undefined),
211
+ isError: true,
212
+ };
213
+ }
214
+
215
+ try {
216
+ const [content, resources] = await Promise.all([
217
+ readFile(skill.filePath, "utf8"),
218
+ listSkillResourcePaths(dirname(skill.filePath)),
219
+ ]);
220
+ return {
221
+ content: [{ type: "text" as const, text: wrapSkillContent(skill, content, resources) }],
222
+ details: buildActivationDetails(skill, resources),
223
+ };
224
+ } catch (error) {
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text" as const,
229
+ text: `Failed to load skill ${requestedName} from ${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`,
230
+ },
231
+ ],
232
+ details: buildActivationDetails(skill),
233
+ isError: true,
234
+ };
235
+ }
236
+ },
237
+ });
238
+
239
+ const clearSkillsAndSync = (model: ExtensionContext["model"]): void => {
240
+ setCurrentSkills([]);
241
+ syncCursorSkillToolForModel(pi, model);
242
+ };
243
+
244
+ pi.on("session_start", (_event, ctx) => {
245
+ clearSkillsAndSync(ctx.model);
246
+ });
247
+ pi.on("model_select", (event) => {
248
+ clearSkillsAndSync(event.model);
249
+ });
250
+ pi.on("turn_start", (_event, ctx) => {
251
+ if (!isCursorModel(ctx.model)) setCurrentSkills([]);
252
+ syncCursorSkillToolForModel(pi, ctx.model);
253
+ });
254
+ pi.on("before_agent_start", (event, ctx) => {
255
+ if (isCursorModel(ctx.model)) {
256
+ setCurrentSkills(event.systemPromptOptions?.skills);
257
+ } else {
258
+ setCurrentSkills([]);
259
+ }
260
+ syncCursorSkillToolForModel(pi, ctx.model);
261
+ const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions);
262
+ if (resolved === event.systemPrompt) return undefined;
263
+ return { systemPrompt: resolved };
264
+ });
265
+ }
266
+
267
+ export const __testUtils = {
268
+ AVAILABLE_SKILLS_SECTION_PATTERN,
269
+ buildActivationDetails,
270
+ setCurrentSkills,
271
+ listSkillResourcePaths,
272
+ wrapSkillContent,
273
+ };
@@ -138,6 +138,10 @@ function getFastPreferenceModelId(metadata: NonNullable<ReturnType<typeof getCur
138
138
  return metadata.selectionModelId || metadata.baseModelId;
139
139
  }
140
140
 
141
+ function getVirtualFastBaseModelId(modelId: string): string {
142
+ return modelId.replace(/:(?:fast|slow)$/, "");
143
+ }
144
+
141
145
  function getStoredFastPreference(metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>): boolean | undefined {
142
146
  const preferenceModelId = getFastPreferenceModelId(metadata);
143
147
  return (
@@ -153,6 +157,7 @@ function getEffectiveFast(modelId: string): boolean | undefined {
153
157
  if (!metadata?.supportsFast) return undefined;
154
158
  if (cliForceNoFast) return false;
155
159
  if (cliForceFast) return true;
160
+ if (metadata.fastOverride !== undefined) return metadata.fastOverride;
156
161
  return getStoredFastPreference(metadata) ?? metadata.defaultFast;
157
162
  }
158
163
 
@@ -344,6 +349,14 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
344
349
  ctx.ui.notify("Cursor fast is forced by --cursor-fast", "info");
345
350
  return;
346
351
  }
352
+ if (metadata.fastOverride !== undefined) {
353
+ const state = metadata.fastOverride ? "enabled" : "disabled";
354
+ ctx.ui.notify(
355
+ `Cursor fast is fixed ${state} by selected model ${metadata.piModelId}; choose ${getVirtualFastBaseModelId(metadata.piModelId)} to use /cursor-fast preferences`,
356
+ "info",
357
+ );
358
+ return;
359
+ }
347
360
 
348
361
  const preferenceModelId = getFastPreferenceModelId(metadata);
349
362
  const current = getEffectiveFast(metadata.piModelId) ?? false;
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { registerCursorRuntimeControls } from "./cursor-state.js";
4
4
  import { registerCursorNativeToolDisplay } from "./cursor-native-tool-display.js";
5
5
  import { registerCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
6
6
  import { registerCursorQuestionTool } from "./cursor-question-tool.js";
7
+ import { registerCursorSkillTool } from "./cursor-skill-tool.js";
7
8
  import { registerCursorSessionCwd } from "./cursor-session-cwd.js";
8
9
  import { registerCursorAgentsContextDedup } from "./cursor-agents-context.js";
9
10
  import { registerCursorSessionAgent } from "./cursor-session-agent.js";
@@ -18,6 +19,7 @@ type CursorExtensionApi =
18
19
  & Parameters<typeof registerCursorRuntimeControls>[0]
19
20
  & Parameters<typeof registerCursorNativeToolDisplay>[0]
20
21
  & Parameters<typeof registerCursorQuestionTool>[0]
22
+ & Parameters<typeof registerCursorSkillTool>[0]
21
23
  & Parameters<typeof registerCursorPiToolBridge>[0]
22
24
  & Parameters<typeof registerCursorAgentsContextDedup>[0];
23
25
 
@@ -46,6 +48,7 @@ export default async function (pi: CursorExtensionApi) {
46
48
  registerCursorRuntimeControls(pi);
47
49
  registerCursorNativeToolDisplay(pi);
48
50
  registerCursorQuestionTool(pi);
51
+ registerCursorSkillTool(pi);
49
52
  registerCursorPiToolBridge(pi);
50
53
  registerCursorAgentsContextDedup(pi);
51
54
  let fallbackIssue: CursorModelFallbackIssue | undefined;
@@ -88,6 +88,7 @@ export interface CursorModelMetadata {
88
88
  contextWindow: number;
89
89
  supportsFast: boolean;
90
90
  defaultFast: boolean;
91
+ fastOverride?: boolean;
91
92
  supportsReasoning: boolean;
92
93
  thinkingLevelMap?: ThinkingLevelMap;
93
94
  parameterIds: {
@@ -205,19 +206,35 @@ function getParamValue(params: ModelParameterValue[], id: string): string | unde
205
206
  return params.find((param) => param.id === id)?.value;
206
207
  }
207
208
 
208
- function encodePiModelId(modelId: string, context?: string): string {
209
- return context ? `${modelId}@${context}` : modelId;
209
+ function encodePiModelId(modelId: string, context?: string, fastOverride?: boolean): string {
210
+ const contextQualified = context ? `${modelId}@${context}` : modelId;
211
+ if (fastOverride === true) return `${contextQualified}:fast`;
212
+ if (fastOverride === false) return `${contextQualified}:slow`;
213
+ return contextQualified;
210
214
  }
211
215
 
212
- function getModelName(item: ModelListItem, context?: string, alias?: string): string {
216
+ function getModelName(item: ModelListItem, context?: string, alias?: string, fastOverride?: boolean): string {
213
217
  const displayName = item.displayName || item.id;
214
- const baseName = alias ? `${displayName} (${alias})` : displayName;
218
+ const qualifiers: string[] = [];
219
+ if (alias) qualifiers.push(alias);
220
+ if (fastOverride === true) qualifiers.push("fast");
221
+ if (fastOverride === false) qualifiers.push("slow");
222
+ const baseName = qualifiers.length > 0 ? `${displayName} (${qualifiers.join(", ")})` : displayName;
215
223
  return context ? `${baseName} @ ${context}` : baseName;
216
224
  }
217
225
 
226
+ function getFastOverrideBasePiModelId(piModelId: string): string {
227
+ return piModelId.replace(/:(?:fast|slow)$/, "");
228
+ }
229
+
218
230
  function getContextWindow(contextWindowCache: Map<string, number>, piModelId: string, context?: string, baseModelId?: string): number {
219
- return (
231
+ const fastOverrideBasePiModelId = getFastOverrideBasePiModelId(piModelId);
232
+ const contextWindowOverride =
220
233
  contextWindowCache.get(piModelId) ??
234
+ (fastOverrideBasePiModelId !== piModelId ? contextWindowCache.get(fastOverrideBasePiModelId) : undefined);
235
+
236
+ return (
237
+ contextWindowOverride ??
221
238
  (context ? parseContextWindow(context) : undefined) ??
222
239
  (baseModelId ? contextWindowCache.get(baseModelId) : undefined) ??
223
240
  contextWindowCache.get("default") ??
@@ -232,6 +249,7 @@ function toMetadata(
232
249
  defaultParams: ModelParameterValue[],
233
250
  context: string | undefined,
234
251
  contextWindowCache: Map<string, number>,
252
+ fastOverride?: boolean,
235
253
  ): CursorModelMetadata {
236
254
  const thinkingLevelMap = getThinkingLevelMap(item);
237
255
  const fastValue = getParamValue(defaultParams, "fast")?.toLowerCase();
@@ -245,6 +263,7 @@ function toMetadata(
245
263
  contextWindow: getContextWindow(contextWindowCache, piModelId, context, item.id),
246
264
  supportsFast: getParameter(item, "fast") !== undefined,
247
265
  defaultFast: fastValue === "true",
266
+ ...(fastOverride !== undefined ? { fastOverride } : {}),
248
267
  supportsReasoning: thinkingLevelMap !== undefined,
249
268
  ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
250
269
  parameterIds: {
@@ -310,16 +329,21 @@ function toModelConfigs(
310
329
  const contexts = contextValues.length > 0 ? contextValues : [undefined];
311
330
  const configs: ProviderModelConfig[] = [];
312
331
 
332
+ const fastOverrides = getParameter(item, "fast") === undefined ? [undefined] : [undefined, true, false];
333
+
313
334
  for (const selectionModelId of getModelIds(item, reservedBaseModelIds, ambiguousAliases)) {
314
335
  const alias = selectionModelId === item.id ? undefined : selectionModelId;
315
336
  for (const context of contexts) {
316
- const params = context ? replaceParam(defaultParams, "context", context) : defaultParams;
317
- const piModelId = encodePiModelId(selectionModelId, context);
318
- if (usedPiModelIds.has(piModelId)) continue;
319
- usedPiModelIds.add(piModelId);
320
- const metadata = toMetadata(item, piModelId, selectionModelId, params, context, contextWindowCache);
321
- metadataByPiModelId.set(piModelId, metadata);
322
- configs.push(toModelConfig(metadata, getModelName(item, context, alias)));
337
+ const contextParams = context ? replaceParam(defaultParams, "context", context) : defaultParams;
338
+ for (const fastOverride of fastOverrides) {
339
+ const params = fastOverride === undefined ? contextParams : replaceParam(contextParams, "fast", fastOverride ? "true" : "false");
340
+ const piModelId = encodePiModelId(selectionModelId, context, fastOverride);
341
+ if (usedPiModelIds.has(piModelId)) continue;
342
+ usedPiModelIds.add(piModelId);
343
+ const metadata = toMetadata(item, piModelId, selectionModelId, params, context, contextWindowCache, fastOverride);
344
+ metadataByPiModelId.set(piModelId, metadata);
345
+ configs.push(toModelConfig(metadata, getModelName(item, context, alias, fastOverride)));
346
+ }
323
347
  }
324
348
  }
325
349