@emmaneugene/pi-cursor-sdk 0.4.3 → 0.5.0

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +19 -39
  3. package/dist/bundled-context-windows.js +6 -13
  4. package/dist/context.js +0 -3
  5. package/dist/cursor-config.js +0 -3
  6. package/dist/cursor-provider-turn-prepare.js +0 -1
  7. package/dist/cursor-state.js +3 -16
  8. package/dist/index.js +0 -2
  9. package/dist/model-discovery.js +11 -47
  10. package/docs/cursor-dogfood-checklist.md +1 -1
  11. package/docs/cursor-live-smoke-checklist.md +1 -2
  12. package/docs/cursor-model-ux-spec.md +80 -151
  13. package/docs/cursor-native-tool-replay.md +2 -3
  14. package/docs/cursor-native-tool-visual-audit.md +1 -1
  15. package/docs/cursor-testing-lessons.md +1 -1
  16. package/docs/cursor-tool-surfaces.md +3 -6
  17. package/docs/platform-smoke-implementation.md +1 -1
  18. package/docs/platform-smoke.md +1 -1
  19. package/package.json +1 -1
  20. package/scripts/isolated-cursor-smoke.sh +1 -1
  21. package/scripts/lib/local-resume-smoke-harness.mjs +2 -1
  22. package/scripts/local-resume-smoke.mjs +1 -1
  23. package/scripts/platform-smoke/card-detect.mjs +1 -1
  24. package/scripts/refresh-cursor-model-snapshots.mjs +3 -3
  25. package/shared/cursor-model-selection-identities.d.mts +1 -4
  26. package/shared/cursor-model-selection-identities.mjs +21 -74
  27. package/src/bundled-context-windows.ts +6 -13
  28. package/src/context.ts +1 -5
  29. package/src/cursor-config.ts +9 -13
  30. package/src/cursor-provider-turn-prepare.ts +0 -1
  31. package/src/cursor-state.ts +3 -22
  32. package/src/index.ts +0 -3
  33. package/src/model-discovery.ts +16 -62
  34. package/dist/cursor-question-tool.js +0 -194
  35. package/node_modules/cross-spawn/node_modules/which/CHANGELOG.md +0 -166
  36. package/src/cursor-question-tool.ts +0 -267
@@ -8,7 +8,7 @@ pi-cursor-sdk runs Cursor models through the local `@cursor/sdk` agent runtime b
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`, `cursor_activate_skill`, extension tools, …) |
11
+ | **Pi bridge (`pi__*`)** | pi-cursor-sdk loopback MCP | Yes, when exposed | Real pi tool names (`cursor_activate_skill`, extension tools, …) |
12
12
 
13
13
  Pi CLI tool toggles apply at the pi tool-registry boundary. `--no-tools`, `--tools`, and `--exclude-tools` can remove pi bridge exposure, but they do **not** disable Cursor SDK host tools or configured Cursor MCP servers.
14
14
 
@@ -16,7 +16,7 @@ Pi CLI tool toggles apply at the pi tool-registry boundary. `--no-tools`, `--too
16
16
 
17
17
  ## Discoverability
18
18
 
19
- - **MCP `listTools`** (and pi's MCP catalog when present) lists **MCP servers only** — for example `pi_tools` with `pi__cursor_ask_question`. It does **not** enumerate Cursor SDK host tools such as `Read` or `Shell`.
19
+ - **MCP `listTools`** (and pi's MCP catalog when present) lists **MCP servers only** — for example `pi_tools` with `pi__cursor_activate_skill`. It does **not** enumerate Cursor SDK host tools such as `Read` or `Shell`.
20
20
  - **Bootstrap prompts** include a short **Cursor SDK tool boundary** block plus a compact **callable tool surfaces** manifest by default (disable manifest with `PI_CURSOR_TOOL_MANIFEST=0`). The manifest reminds the model that Cursor host/configured MCP tools are controlled by Cursor, while pi tool toggles only affect pi tools/bridge exposure; when bridge tools are exposed, it lists the current `pi__*` names. MCP `listTools` entries for bridged pi tools point back to the bootstrap prompt instead of repeating the full contract.
21
21
  - **Incremental prompts** omit the full boundary block but keep a short tail guard (including an explicit shell `cd` hint); the session agent retains prior bootstrap context. They also omit invariant Pi system instructions; a changed system prompt forces bootstrap with the new section.
22
22
  - **In-session debug:** `/cursor-tools` prints bridge enablement, manifest enablement, effective `PI_CURSOR_SETTING_SOURCES`, and the current callable-surface snapshot.
@@ -31,12 +31,9 @@ Default behavior:
31
31
  - Hide individual pi tools from the bridge with `bridge.excludeTools` in `~/.pi/agent/cursor-sdk.json` or trusted `.pi/cursor-sdk.json`. Denylisted active pi tools are hidden from Cursor; unset means no restriction, and an explicitly empty list parses to unset so it falls through to lower-precedence config. Trusted project config wins over user config.
32
32
  - Overlapping pi builtins (`read`, `bash`, `write`, `edit`, `grep`, `find`, `ls`) are **hidden** from the bridge unless `PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1`; a `bridge.excludeTools` entry keeps a tool hidden even with that opt-in.
33
33
 
34
- `pi-cursor-sdk` registers `cursor_ask_question` for Cursor models when the bridge is on and `PI_CURSOR_ASK_QUESTION` is enabled (the default); Cursor sees `pi__cursor_ask_question`. The tool is sequential and emits `pi-cursor-sdk:ask-question:blocked` `{ active }` while awaiting UI input. Set `PI_CURSOR_ASK_QUESTION=0` to remove only this tool while preserving the rest of the bridge. Pending bridged calls use a local deadline capped by the effective MCP tool timeout; `PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS` can lower it. 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.
34
+ Pending bridged calls use a local deadline capped by the effective MCP tool timeout; `PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS` can lower it. 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.
35
35
 
36
36
  ```bash
37
- # Disable only Cursor's interactive question tool
38
- PI_CURSOR_ASK_QUESTION=0 pi --model cursor/grok-4.6
39
-
40
37
  # Disable pi bridge entirely
41
38
  PI_CURSOR_PI_TOOL_BRIDGE=0 pi --model cursor/grok-4.6
42
39
 
@@ -48,7 +48,7 @@ Excluded from release visual matrix with required rationale:
48
48
  - `webSearch`: network/search dependent.
49
49
  - `webFetch`: network dependent.
50
50
 
51
- Adding a registry entry requires adding it to the required or excluded list with rationale. `ls` is currently excluded from the required one-prompt matrix because composer-2-5 does not route the deterministic source-enumeration step through the native `ls` surface reliably; the suite instead gates that behavior through a successful native `find` result for `src/cursor-provider.ts`.
51
+ Adding a registry entry requires adding it to the required or excluded list with rationale. `ls` is currently excluded from the required one-prompt matrix because composer-2.5 does not route the deterministic source-enumeration step through the native `ls` surface reliably; the suite instead gates that behavior through a successful native `find` result for `src/cursor-provider.ts`.
52
52
 
53
53
  ## Platform command rendering
54
54
 
@@ -18,7 +18,7 @@ This fork does not use the Crabbox platform matrix as a current release gate. Th
18
18
  npm test
19
19
  npm run typecheck
20
20
  npm pack --dry-run
21
- # one live print-mode Cursor run with cursor/grok-4.6:slow
21
+ # one live print-mode Cursor run with cursor/grok-4.6
22
22
  npm run smoke:visual -- --label release-check --prompt 'Read ./package.json and reply with its package name.'
23
23
  ```
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emmaneugene/pi-cursor-sdk",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "pi provider extension backed by @cursor/sdk local agents",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -369,7 +369,7 @@ LIST_OUT="$ISOLATED/list-models.txt"
369
369
  run_in_dir_capture_combined "list-models" 30 "$PROJECT_DIR" "$LIST_OUT" "${PI_CURSOR_ENV[@]}" \
370
370
  "$PI_BIN" --approve --cursor-no-fast --list-models cursor
371
371
  "$RG_BIN" -q "grok-4\\.6" "$LIST_OUT" || fail "grok-4.6 not listed (see $LIST_OUT)"
372
- "$RG_BIN" -q "composer-2\\.5|composer-2-5" "$LIST_OUT" || fail "composer-2-5 not listed (see $LIST_OUT)"
372
+ "$RG_BIN" -q "composer-2\\.5" "$LIST_OUT" || fail "composer-2.5 not listed (see $LIST_OUT)"
373
373
 
374
374
  log "check: basic provider prompt"
375
375
  BASIC_DIR="$SESSION_ROOT/basic"
@@ -136,7 +136,7 @@ export function startRpc({
136
136
  baseEnv = process.env,
137
137
  }) {
138
138
  const model =
139
- process.env.CURSOR_LOCAL_RESUME_SMOKE_MODEL || "cursor/grok-4.6:slow";
139
+ process.env.CURSOR_LOCAL_RESUME_SMOKE_MODEL || "cursor/grok-4.6";
140
140
  const workspaceDir = join(artifactDir, "workspace");
141
141
  mkdirSync(workspaceDir, { recursive: true });
142
142
  const pi = findPiCommand();
@@ -160,6 +160,7 @@ export function startRpc({
160
160
  ...extraExtensions.flatMap((path) => ["-e", path]),
161
161
  "--model",
162
162
  model,
163
+ "--cursor-no-fast",
163
164
  "--cursor-runtime",
164
165
  "local",
165
166
  "--approve",
@@ -46,7 +46,7 @@ ${npmUsage}
46
46
  ${nodeUsage}
47
47
 
48
48
  Environment:
49
- CURSOR_LOCAL_RESUME_SMOKE_MODEL Cursor model id (default: cursor/grok-4.6:slow).
49
+ CURSOR_LOCAL_RESUME_SMOKE_MODEL Canonical Cursor model id (default: cursor/grok-4.6; smoke forces fast off).
50
50
  CURSOR_LOCAL_RESUME_SMOKE_TIMEOUT_MS Timeout in ms per model turn (default: 300000).
51
51
  CURSOR_LOCAL_RESUME_SMOKE_KEEP_ARTIFACTS Keep temp artifacts when set to 1.
52
52
  CURSOR_LOCAL_RESUME_SMOKE_EXTENSION_PATH Packed extension path override (platform runner only).
@@ -31,7 +31,7 @@ const CARD_PATTERNS = [
31
31
  { id: "bridge-read-failure", pattern: /^\s*(?:read \.\/definitely-missing-platform-smoke-file\.txt|ENOENT: no such file)\s*/i },
32
32
  { id: "bridge-shell-success", pattern: /^\s*bridge visual smoke\s*$/i },
33
33
  { id: "http1-status", pattern: /\bcursor\b.*\bhttp1\b/i },
34
- { id: "footer-status", pattern: /\bgrok-4\.6\b|\bcomposer-2-5\b|\bcomposer-2\.5\b/i },
34
+ { id: "footer-status", pattern: /\bgrok-4\.6\b|\bcomposer-2\.5\b/i },
35
35
  ];
36
36
 
37
37
  function cleanLine(line) {
@@ -42,8 +42,8 @@ Notes:
42
42
  - Cursor.models.list() is the source of truth for fallback catalog metadata.
43
43
  - Checkpoint-derived context windows are optional input because collecting them
44
44
  requires successful local SDK runs; this script does not start agents.
45
- - Context-window keys are limited to current selectable model IDs. Redundant
46
- :fast/:slow aliases that send the default parameters collapse to one key.`);
45
+ - Context-window evidence is normalized to canonical model IDs only when it
46
+ matches the model's default context; aliases and other variants are omitted.`);
47
47
  }
48
48
 
49
49
  const fail = createScriptFail("refresh-cursor-snapshots");
@@ -171,7 +171,7 @@ function formatContextWindows(models, checkpointWindows, fallbackContextWindow)
171
171
  const date = new Date().toISOString().slice(0, 10);
172
172
  const sorted = [...merged.entries()].sort(([a], [b]) => (a === "default" ? -1 : b === "default" ? 1 : a.localeCompare(b)));
173
173
  const lines = sorted.map(([modelId, contextWindow]) => `\t${JSON.stringify(modelId)}: ${contextWindow},`);
174
- 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// Keys are current selectable model IDs. Equivalent default :fast/:slow selections\n// collapse to one key; stale and ambiguous aliases are omitted. Values are observed\n// or conservative default/non-Max-mode limits and may override a catalog context\n// label when the completed SDK checkpoint reports a different effective limit.\nexport const BUNDLED_CONTEXT_WINDOWS = {\n${lines.join("\n")}\n} as const satisfies Record<string, number>;\n`;
174
+ 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// Keys are canonical Cursor catalog model IDs. Evidence for a model's default\n// context is normalized to that ID; aliases, forced-speed variants, non-default\n// contexts, and stale IDs are omitted. Values are observed or conservative\n// defaults and can override a catalog context label.\nexport const BUNDLED_CONTEXT_WINDOWS = {\n${lines.join("\n")}\n} as const satisfies Record<string, number>;\n`;
175
175
  }
176
176
 
177
177
  const args = parseRefreshArgs(process.argv.slice(2));
@@ -2,12 +2,9 @@ import type { ModelListItem } from "@cursor/sdk";
2
2
 
3
3
  export interface CursorModelSelectionIdentity {
4
4
  model: ModelListItem;
5
- selectionModelId: string;
6
- context?: string;
7
- fastOverride?: boolean;
8
5
  piModelId: string;
6
+ defaultContext?: string;
9
7
  contextWindowKey: string;
10
- baseContextWindowKey: string;
11
8
  }
12
9
 
13
10
  export declare function getCursorModelSelectionIdentities(
@@ -1,89 +1,36 @@
1
- function getParameter(model, id) {
2
- return (model.parameters ?? []).find((parameter) => parameter.id === id);
3
- }
4
-
5
1
  function getDefaultParam(model, id) {
6
2
  const variant = (model.variants ?? []).find((candidate) => candidate.isDefault) ?? model.variants?.[0];
7
- return variant?.params?.find((param) => param.id === id)?.value.toLowerCase();
8
- }
9
-
10
- function getAmbiguousAliases(models) {
11
- const ownersByAlias = new Map();
12
- for (const model of models) {
13
- for (const rawAlias of model.aliases ?? []) {
14
- const alias = rawAlias.trim();
15
- if (!alias || alias === model.id) continue;
16
- const owners = ownersByAlias.get(alias) ?? new Set();
17
- owners.add(model.id);
18
- ownersByAlias.set(alias, owners);
19
- }
20
- }
21
- return new Set([...ownersByAlias].filter(([, owners]) => owners.size > 1).map(([alias]) => alias));
22
- }
23
-
24
- function getSelectableIds(model, reservedIds, ambiguousAliases) {
25
- const ids = [model.id];
26
- for (const rawAlias of model.aliases ?? []) {
27
- const alias = rawAlias.trim();
28
- if (!alias || alias === model.id || ids.includes(alias) || reservedIds.has(alias) || ambiguousAliases.has(alias)) continue;
29
- ids.push(alias);
30
- }
31
- return ids;
3
+ return variant?.params?.find((param) => param.id === id)?.value;
32
4
  }
33
5
 
34
- function encodePiModelId(modelId, context, fastOverride) {
35
- const contextQualified = context ? `${modelId}@${context}` : modelId;
36
- if (fastOverride === true) return `${contextQualified}:fast`;
37
- if (fastOverride === false) return `${contextQualified}:slow`;
38
- return contextQualified;
6
+ function getContextWindowKey(modelId, defaultContext) {
7
+ return defaultContext ? `${modelId}@${defaultContext}` : modelId;
39
8
  }
40
9
 
41
10
  export function getCursorModelSelectionIdentities(models) {
42
- const sortedModels = [...models].sort((a, b) => a.id.localeCompare(b.id));
43
- const reservedIds = new Set(models.map((model) => model.id));
44
- const ambiguousAliases = getAmbiguousAliases(models);
45
- const usedPiModelIds = new Set();
46
- const identities = [];
47
-
48
- for (const model of sortedModels) {
49
- const contextValues = getParameter(model, "context")?.values?.map(({ value }) => value) ?? [];
50
- const contexts = contextValues.length > 0 ? contextValues : [undefined];
51
- const hasFast = getParameter(model, "fast") !== undefined;
52
- const fastOverrides = hasFast ? [undefined, true, false] : [undefined];
53
- const defaultFast = getDefaultParam(model, "fast");
54
-
55
- for (const selectionModelId of getSelectableIds(model, reservedIds, ambiguousAliases)) {
56
- for (const context of contexts) {
57
- for (const fastOverride of fastOverrides) {
58
- const piModelId = encodePiModelId(selectionModelId, context, fastOverride);
59
- if (usedPiModelIds.has(piModelId)) continue;
60
- usedPiModelIds.add(piModelId);
61
- const defaultEquivalent =
62
- (fastOverride === true && defaultFast === "true") || (fastOverride === false && defaultFast === "false");
63
- identities.push({
64
- model,
65
- selectionModelId,
66
- ...(context ? { context } : {}),
67
- ...(fastOverride !== undefined ? { fastOverride } : {}),
68
- piModelId,
69
- contextWindowKey: defaultEquivalent ? encodePiModelId(selectionModelId, context) : piModelId,
70
- baseContextWindowKey: encodePiModelId(model.id, context, defaultEquivalent ? undefined : fastOverride),
71
- });
72
- }
73
- }
74
- }
75
- }
76
-
77
- return identities;
11
+ return [...models]
12
+ .sort((a, b) => a.id.localeCompare(b.id))
13
+ .map((model) => {
14
+ const defaultContext = getDefaultParam(model, "context");
15
+ return {
16
+ model,
17
+ piModelId: model.id,
18
+ ...(defaultContext ? { defaultContext } : {}),
19
+ contextWindowKey: getContextWindowKey(model.id, defaultContext),
20
+ };
21
+ });
78
22
  }
79
23
 
80
24
  export function normalizeCursorContextWindowEntries(models, entries, source = "context windows") {
81
- const canonicalBySelectableId = new Map(
82
- getCursorModelSelectionIdentities(models).map(({ piModelId, contextWindowKey }) => [piModelId, contextWindowKey]),
83
- );
25
+ const canonicalByInputId = new Map();
26
+ for (const { piModelId, contextWindowKey } of getCursorModelSelectionIdentities(models)) {
27
+ canonicalByInputId.set(piModelId, piModelId);
28
+ canonicalByInputId.set(contextWindowKey, piModelId);
29
+ }
30
+
84
31
  const normalized = new Map();
85
32
  for (const [modelId, contextWindow] of entries) {
86
- const canonicalId = modelId === "default" ? modelId : canonicalBySelectableId.get(modelId);
33
+ const canonicalId = modelId === "default" ? modelId : canonicalByInputId.get(modelId);
87
34
  if (!canonicalId) continue;
88
35
  const existing = normalized.get(canonicalId);
89
36
  if (existing !== undefined && existing !== contextWindow) {
@@ -1,18 +1,16 @@
1
1
  // Generated from Cursor SDK checkpoint tokenDetails.maxTokens on 2026-08-18.
2
2
  // Refresh with: npm run refresh:cursor-snapshots -- --write --context-windows ~/.pi/agent/cursor-sdk-context-windows.json
3
- // Keys are current selectable model IDs. Equivalent default :fast/:slow selections
4
- // collapse to one key; stale and ambiguous aliases are omitted. Values are observed
5
- // or conservative default/non-Max-mode limits and may override a catalog context
6
- // label when the completed SDK checkpoint reports a different effective limit.
3
+ // Keys are canonical Cursor catalog model IDs. Evidence for a model's default
4
+ // context is normalized to that ID; aliases, forced-speed variants, non-default
5
+ // contexts, and stale IDs are omitted.
7
6
  export const BUNDLED_CONTEXT_WINDOWS = {
8
- "default": 200000,
9
7
  "auto-smart": 200000,
10
- "claude-fable-5@300k": 300000,
11
8
  "claude-haiku-4-5": 200000,
12
9
  "claude-opus-4-5": 200000,
13
- "claude-opus-4-8@1m": 300000,
10
+ "claude-opus-4-8": 300000,
14
11
  "composer-2": 200000,
15
12
  "composer-2.5": 200000,
13
+ "default": 200000,
16
14
  "gemini-2.5-flash": 200000,
17
15
  "gemini-3-flash": 200000,
18
16
  "gemini-3.1-pro": 200000,
@@ -20,20 +18,15 @@ export const BUNDLED_CONTEXT_WINDOWS = {
20
18
  "gemini-3.6-flash": 200000,
21
19
  "gemini-3.7-flash": 200000,
22
20
  "glm-5.2": 200000,
23
- "gpt-5-5@272k": 272000,
24
21
  "gpt-5-mini": 272000,
25
22
  "gpt-5.1": 272000,
26
23
  "gpt-5.2": 272000,
27
24
  "gpt-5.3-codex": 272000,
28
25
  "gpt-5.4-mini": 272000,
29
26
  "gpt-5.4-nano": 272000,
30
- "gpt-5.5@272k": 272000,
31
- "gpt-5.6@1m": 272000,
27
+ "gpt-5.6-sol": 272000,
32
28
  "grok-4.5": 256000,
33
- "grok-4.5:slow": 256000,
34
29
  "grok-4.6": 256000,
35
30
  "kimi-k2.7-code": 200000,
36
31
  "kimi-k3": 200000,
37
- "opus-4.8@1m": 300000,
38
- "opus-4.8@300k": 300000,
39
32
  } as const satisfies Record<string, number>;
package/src/context.ts CHANGED
@@ -18,7 +18,6 @@ export interface CursorPromptOptions {
18
18
  /** Compact callable-surface summary; included on bootstrap prompts when set. */
19
19
  toolManifest?: string;
20
20
  includePiBridgeGuidance?: boolean;
21
- includePiAskQuestionGuidance?: boolean;
22
21
  }
23
22
 
24
23
  export const CURSOR_APPROX_CHARS_PER_TOKEN = 4;
@@ -54,10 +53,9 @@ export function getCursorToolTailGuardText(
54
53
  }
55
54
 
56
55
  function getCursorToolBoundaryText(
57
- options: Pick<CursorPromptOptions, "agentMode" | "includePiAskQuestionGuidance"> & { hasToolManifest?: boolean; includePiBridgeGuidance?: boolean } = {},
56
+ options: Pick<CursorPromptOptions, "agentMode"> & { hasToolManifest?: boolean; includePiBridgeGuidance?: boolean } = {},
58
57
  ): string {
59
58
  const includePiBridgeGuidance = options.includePiBridgeGuidance !== false;
60
- const includePiAskQuestionGuidance = includePiBridgeGuidance && options.includePiAskQuestionGuidance !== false;
61
59
  const lines = [
62
60
  "Cursor SDK tool boundary:",
63
61
  "Call only Cursor SDK/MCP tools exposed in this run; pi history names, replay labels, and transcript names are not callable.",
@@ -65,7 +63,6 @@ function getCursorToolBoundaryText(
65
63
  ? "For exposed pi bridge tools, call pi__* MCP names, not pi card/history names."
66
64
  : undefined,
67
65
  "Do not claim pi-side or WebSearch/WebFetch tools unless Cursor ran an equivalent tool.",
68
- includePiAskQuestionGuidance ? "Use pi__cursor_ask_question for material choices if exposed." : undefined,
69
66
  getCursorPlanModeToolGuidanceText(options.agentMode, { includePiBridgeGuidance }),
70
67
  "Images: only latest user images are sent; ask to reattach prior images.",
71
68
  ].filter((line): line is string => line !== undefined);
@@ -406,7 +403,6 @@ export function buildCursorPrompt(context: Context, options: CursorPromptOptions
406
403
  agentMode: options.agentMode,
407
404
  hasToolManifest: Boolean(options.toolManifest),
408
405
  includePiBridgeGuidance: options.includePiBridgeGuidance,
409
- includePiAskQuestionGuidance: options.includePiAskQuestionGuidance,
410
406
  })];
411
407
  if (options.toolManifest) {
412
408
  sectionsBeforeMessages.push(options.toolManifest);
@@ -24,8 +24,8 @@ export const CURSOR_LOCAL_FORCE_ENV = "PI_CURSOR_LOCAL_FORCE";
24
24
  export const CURSOR_LOCAL_RESUME_ENV = "PI_CURSOR_LOCAL_RESUME";
25
25
  export const CURSOR_HTTP1_ENV = "PI_CURSOR_HTTP_1_1";
26
26
 
27
- export type CursorConfigSource = "cli" | "environment" | "project" | "user" | "session" | "model-alias" | "builtin";
28
- export type CursorConfigTrustLevel = "one-shot" | "environment" | "trusted-project" | "user" | "session" | "model-catalog" | "builtin";
27
+ export type CursorConfigSource = "cli" | "environment" | "project" | "user" | "session" | "builtin";
28
+ export type CursorConfigTrustLevel = "one-shot" | "environment" | "trusted-project" | "user" | "session" | "builtin";
29
29
 
30
30
  export interface CursorSdkConfig {
31
31
  fastDefaults?: Record<string, boolean>;
@@ -97,7 +97,6 @@ const TRUST_LEVELS: Record<CursorConfigSource, CursorConfigTrustLevel> = {
97
97
  project: "trusted-project",
98
98
  user: "user",
99
99
  session: "session",
100
- "model-alias": "model-catalog",
101
100
  builtin: "builtin",
102
101
  };
103
102
 
@@ -357,19 +356,18 @@ function resolveOrdinary<T>(layers: Array<CursorResolvedSetting<T> | undefined>)
357
356
  // precedence), plus its per-layer values. Sources omitted from a field's order (e.g. local fields skip
358
357
  // "session", force skips project/user/session) are the field-specific omissions the review asked to keep
359
358
  // explicit; test/cursor-config.test.ts asserts each one.
360
- type CursorFieldSource = Exclude<CursorConfigSource, "model-alias">;
361
- type CursorFieldValues<T> = Partial<Record<CursorFieldSource, T>>;
359
+ type CursorFieldValues<T> = Partial<Record<CursorConfigSource, T>>;
362
360
 
363
- const LOCAL_ORDER: CursorFieldSource[] = ["cli", "environment", "project", "user", "builtin"];
364
- const LOCAL_FORCE_ORDER: CursorFieldSource[] = ["cli", "environment", "builtin"];
365
- const HTTP1_ORDER: CursorFieldSource[] = ["session", "environment", "user", "builtin"];
366
- const BRIDGE_ORDER: CursorFieldSource[] = ["project", "user", "builtin"];
361
+ const LOCAL_ORDER: CursorConfigSource[] = ["cli", "environment", "project", "user", "builtin"];
362
+ const LOCAL_FORCE_ORDER: CursorConfigSource[] = ["cli", "environment", "builtin"];
363
+ const HTTP1_ORDER: CursorConfigSource[] = ["session", "environment", "user", "builtin"];
364
+ const BRIDGE_ORDER: CursorConfigSource[] = ["project", "user", "builtin"];
367
365
 
368
- function buildFieldLayers<T>(order: CursorFieldSource[], values: CursorFieldValues<T>): Array<CursorResolvedSetting<T> | undefined> {
366
+ function buildFieldLayers<T>(order: CursorConfigSource[], values: CursorFieldValues<T>): Array<CursorResolvedSetting<T> | undefined> {
369
367
  return order.map((source) => (source === "builtin" ? resolved("builtin", values.builtin as T) : valueFrom(source, values[source])));
370
368
  }
371
369
 
372
- function resolveOrdinaryField<T>(order: CursorFieldSource[], values: CursorFieldValues<T>): CursorResolvedSetting<T> {
370
+ function resolveOrdinaryField<T>(order: CursorConfigSource[], values: CursorFieldValues<T>): CursorResolvedSetting<T> {
373
371
  return resolveOrdinary(buildFieldLayers(order, values));
374
372
  }
375
373
 
@@ -452,14 +450,12 @@ export function buildCursorBridgeExcludeToolNames(resolvedConfig: CursorResolved
452
450
  export function resolveCursorFastDefault(options: {
453
451
  cliForceFast?: boolean;
454
452
  cliForceNoFast?: boolean;
455
- aliasOverride?: boolean;
456
453
  sessionValue?: boolean;
457
454
  userValue?: boolean;
458
455
  modelDefault: boolean;
459
456
  }): CursorResolvedSetting<boolean> {
460
457
  if (options.cliForceNoFast) return resolved("cli", false);
461
458
  if (options.cliForceFast) return resolved("cli", true);
462
- if (options.aliasOverride !== undefined) return resolved("model-alias", options.aliasOverride);
463
459
  if (options.sessionValue !== undefined) return resolved("session", options.sessionValue);
464
460
  if (options.userValue !== undefined) return resolved("user", options.userValue);
465
461
  return resolved("builtin", options.modelDefault);
@@ -153,7 +153,6 @@ async function prepareCursorLocalProviderTurn(
153
153
  ...getCursorPromptOptions(model),
154
154
  agentMode,
155
155
  includePiBridgeGuidance,
156
- includePiAskQuestionGuidance: bridgeToolNames.has("pi__cursor_ask_question"),
157
156
  };
158
157
  if (plan.mode !== "bootstrap" || !resolveCursorToolManifestEnabled()) {
159
158
  return promptOptions;
@@ -191,20 +191,11 @@ function restoreSessionCursorHttp1(branch: readonly SessionEntry[]): void {
191
191
  }
192
192
  }
193
193
 
194
- function getFastPreferenceModelId(metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>): string {
195
- return metadata.selectionModelId || metadata.baseModelId;
196
- }
197
-
198
- function getVirtualFastBaseModelId(modelId: string): string {
199
- return modelId.replace(/:(?:fast|slow)$/, "");
200
- }
201
-
202
194
  function getMapFastPreference(
203
195
  map: Map<string, boolean>,
204
196
  metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>,
205
197
  ): boolean | undefined {
206
- const preferenceModelId = getFastPreferenceModelId(metadata);
207
- return map.get(preferenceModelId) ?? (preferenceModelId !== metadata.baseModelId ? map.get(metadata.baseModelId) : undefined);
198
+ return map.get(metadata.piModelId);
208
199
  }
209
200
 
210
201
  function getEffectiveFast(modelId: string): boolean | undefined {
@@ -213,8 +204,7 @@ function getEffectiveFast(modelId: string): boolean | undefined {
213
204
  return resolveCursorFastDefault({
214
205
  cliForceNoFast,
215
206
  cliForceFast,
216
- aliasOverride: metadata.fastOverride,
217
- sessionValue: authoritativeGlobalFastPreferenceIds.has(getFastPreferenceModelId(metadata))
207
+ sessionValue: authoritativeGlobalFastPreferenceIds.has(metadata.piModelId)
218
208
  ? undefined
219
209
  : getMapFastPreference(sessionFastPreferences, metadata),
220
210
  userValue: getMapFastPreference(globalFastPreferences, metadata),
@@ -465,16 +455,7 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
465
455
  ctx.ui.notify("Cursor fast is forced by --cursor-fast", "info");
466
456
  return;
467
457
  }
468
- if (metadata.fastOverride !== undefined) {
469
- const state = metadata.fastOverride ? "enabled" : "disabled";
470
- ctx.ui.notify(
471
- `Cursor fast is fixed ${state} by selected model ${metadata.piModelId}; choose ${getVirtualFastBaseModelId(metadata.piModelId)} to use /cursor-fast preferences`,
472
- "info",
473
- );
474
- return;
475
- }
476
-
477
- const preferenceModelId = getFastPreferenceModelId(metadata);
458
+ const preferenceModelId = metadata.piModelId;
478
459
  const current = getEffectiveFast(metadata.piModelId) ?? false;
479
460
  const next = !current;
480
461
  let appendError: unknown;
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@ import { discoverModels, type CursorModelFallbackIssue } from "./model-discovery
4
4
  import { registerCursorRuntimeControls } from "./cursor-state.js";
5
5
  import { registerCursorNativeToolDisplay } from "./cursor-native-tool-display-registration.js";
6
6
  import { registerCursorPiToolBridge, registerNestedCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
7
- import { registerCursorQuestionTool } from "./cursor-question-tool.js";
8
7
  import { registerCursorSkillTool } from "./cursor-skill-tool.js";
9
8
  import { registerCursorSessionScope } from "./cursor-session-scope.js";
10
9
  import { registerCursorSessionAgentLifecycle } from "./cursor-session-agent-lifecycle.js";
@@ -34,7 +33,6 @@ type CursorExtensionApi =
34
33
  & Parameters<typeof registerCursorSessionAgentResume>[0]
35
34
  & Parameters<typeof registerCursorRuntimeControls>[0]
36
35
  & Parameters<typeof registerCursorNativeToolDisplay>[0]
37
- & Parameters<typeof registerCursorQuestionTool>[0]
38
36
  & Parameters<typeof registerCursorSkillTool>[0]
39
37
  & Parameters<typeof registerCursorPiToolBridge>[0]
40
38
  & Parameters<typeof registerCursorFallbackIssueWarning>[0]
@@ -129,7 +127,6 @@ export default async function (pi: CursorExtensionApi) {
129
127
  });
130
128
  registerCursorRuntimeControls(pi);
131
129
  registerCursorNativeToolDisplay(pi);
132
- registerCursorQuestionTool(pi);
133
130
  registerCursorSkillTool(pi);
134
131
  registerCursorPiToolBridge(pi);
135
132
  registerCursorAgentsContextDedup(pi);
@@ -49,15 +49,11 @@ async function getDiscoveryApiKey(apiKey?: string): Promise<string | undefined>
49
49
 
50
50
  export interface CursorModelMetadata {
51
51
  piModelId: string;
52
- baseModelId: string;
53
- selectionModelId: string;
54
52
  displayName: string;
55
53
  defaultParams: ModelParameterValue[];
56
- context?: string;
57
54
  contextWindow: number;
58
55
  supportsFast: boolean;
59
56
  defaultFast: boolean;
60
- fastOverride?: boolean;
61
57
  supportsReasoning: boolean;
62
58
  thinkingLevelMap?: ThinkingLevelMap;
63
59
  parameterIds: {
@@ -158,48 +154,20 @@ function getDefaultParams(item: ModelListItem): ModelParameterValue[] {
158
154
  return cloneParams(defaultVariant?.params ?? []);
159
155
  }
160
156
 
161
- function replaceParam(
162
- params: ModelParameterValue[],
163
- id: string,
164
- value: string,
165
- ): ModelParameterValue[] {
166
- let replaced = false;
167
- const next = params.map((param) => {
168
- if (param.id !== id) return { ...param };
169
- replaced = true;
170
- return { id, value };
171
- });
172
- if (!replaced) next.push({ id, value });
173
- return next;
174
- }
175
-
176
157
  function getParamValue(params: ModelParameterValue[], id: string): string | undefined {
177
158
  return params.find((param) => param.id === id)?.value;
178
159
  }
179
160
 
180
- function getModelName(item: ModelListItem, context?: string, alias?: string, fastOverride?: boolean): string {
181
- const displayName = item.displayName || item.id;
182
- const qualifiers: string[] = [];
183
- if (alias) qualifiers.push(alias);
184
- if (fastOverride === true) qualifiers.push("fast");
185
- if (fastOverride === false) qualifiers.push("slow");
186
- const baseName = qualifiers.length > 0 ? `${displayName} (${qualifiers.join(", ")})` : displayName;
187
- return context ? `${baseName} @ ${context}` : baseName;
188
- }
189
-
190
161
  function getContextWindow(
191
162
  contextWindowCache: Map<string, number>,
192
- selectionKeys: readonly string[],
193
- context?: string,
194
- baseModelId?: string,
163
+ modelId: string,
164
+ defaultContext: string | undefined,
165
+ contextWindowKey: string,
195
166
  ): number {
196
- for (const key of new Set(selectionKeys)) {
197
- const contextWindow = contextWindowCache.get(key);
198
- if (contextWindow !== undefined) return contextWindow;
199
- }
200
167
  return (
201
- (context ? parseContextWindow(context) : undefined) ??
202
- (baseModelId ? contextWindowCache.get(baseModelId) : undefined) ??
168
+ contextWindowCache.get(modelId) ??
169
+ contextWindowCache.get(contextWindowKey) ??
170
+ (defaultContext ? parseContextWindow(defaultContext) : undefined) ??
203
171
  contextWindowCache.get("default") ??
204
172
  FALLBACK_CONTEXT_WINDOW
205
173
  );
@@ -207,27 +175,20 @@ function getContextWindow(
207
175
 
208
176
  function toMetadata(
209
177
  item: ModelListItem,
210
- piModelId: string,
211
- selectionModelId: string,
212
178
  defaultParams: ModelParameterValue[],
213
- context: string | undefined,
179
+ defaultContext: string | undefined,
214
180
  contextWindowCache: Map<string, number>,
215
- contextWindowKeys: readonly string[],
216
- fastOverride?: boolean,
181
+ contextWindowKey: string,
217
182
  ): CursorModelMetadata {
218
183
  const thinkingLevelMap = getThinkingLevelMap(item);
219
184
  const fastValue = getParamValue(defaultParams, "fast")?.toLowerCase();
220
185
  return {
221
- piModelId,
222
- baseModelId: item.id,
223
- selectionModelId,
186
+ piModelId: item.id,
224
187
  displayName: item.displayName || item.id,
225
188
  defaultParams: cloneParams(defaultParams),
226
- ...(context ? { context } : {}),
227
- contextWindow: getContextWindow(contextWindowCache, contextWindowKeys, context, item.id),
189
+ contextWindow: getContextWindow(contextWindowCache, item.id, defaultContext, contextWindowKey),
228
190
  supportsFast: getParameter(item, "fast") !== undefined,
229
191
  defaultFast: fastValue === "true",
230
- ...(fastOverride !== undefined ? { fastOverride } : {}),
231
192
  supportsReasoning: thinkingLevelMap !== undefined,
232
193
  ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
233
194
  parameterIds: {
@@ -256,23 +217,16 @@ function toModelConfig(metadata: CursorModelMetadata, name: string): ProviderMod
256
217
  function registerModelItems(items: ModelListItem[]): ProviderModelConfig[] {
257
218
  metadataByPiModelId.clear();
258
219
  const contextWindowCache = loadContextWindowCache();
259
- return getCursorModelSelectionIdentities(items).map(({ model: item, selectionModelId, context, fastOverride, piModelId, contextWindowKey, baseContextWindowKey }) => {
260
- const defaultParams = getDefaultParams(item);
261
- const contextParams = context ? replaceParam(defaultParams, "context", context) : defaultParams;
262
- const params = fastOverride === undefined ? contextParams : replaceParam(contextParams, "fast", fastOverride ? "true" : "false");
220
+ return getCursorModelSelectionIdentities(items).map(({ model: item, piModelId, defaultContext, contextWindowKey }) => {
263
221
  const metadata = toMetadata(
264
222
  item,
265
- piModelId,
266
- selectionModelId,
267
- params,
268
- context,
223
+ getDefaultParams(item),
224
+ defaultContext,
269
225
  contextWindowCache,
270
- [piModelId, contextWindowKey, baseContextWindowKey],
271
- fastOverride,
226
+ contextWindowKey,
272
227
  );
273
228
  metadataByPiModelId.set(piModelId, metadata);
274
- const alias = selectionModelId === item.id ? undefined : selectionModelId;
275
- return toModelConfig(metadata, getModelName(item, context, alias, fastOverride));
229
+ return toModelConfig(metadata, metadata.displayName);
276
230
  });
277
231
  }
278
232
 
@@ -354,7 +308,7 @@ export function buildCursorModelSelection(
354
308
  setParam(params, "fast", fastEnabled ? "true" : "false");
355
309
  }
356
310
 
357
- return params.length > 0 ? { id: metadata.selectionModelId, params } : { id: metadata.selectionModelId };
311
+ return params.length > 0 ? { id: metadata.piModelId, params } : { id: metadata.piModelId };
358
312
  }
359
313
 
360
314
  function sanitizeDiscoveryError(error: unknown, apiKey: string): string | undefined {