@emmaneugene/pi-cursor-sdk 0.4.2 → 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.
- package/CHANGELOG.md +16 -0
- package/README.md +19 -39
- package/dist/bundled-context-windows.js +6 -13
- package/dist/context.js +0 -3
- package/dist/cursor-config.js +0 -3
- package/dist/cursor-live-run-coordinator.js +11 -0
- package/dist/cursor-pi-tool-bridge-run.js +3 -0
- package/dist/cursor-provider-turn-prepare.js +0 -1
- package/dist/cursor-state.js +3 -16
- package/dist/index.js +0 -2
- package/dist/model-discovery.js +11 -47
- package/docs/cursor-dogfood-checklist.md +1 -1
- package/docs/cursor-live-smoke-checklist.md +1 -2
- package/docs/cursor-model-ux-spec.md +80 -151
- package/docs/cursor-native-tool-replay.md +2 -3
- package/docs/cursor-native-tool-visual-audit.md +1 -1
- package/docs/cursor-testing-lessons.md +1 -1
- package/docs/cursor-tool-surfaces.md +3 -6
- package/docs/platform-smoke-implementation.md +1 -1
- package/docs/platform-smoke.md +1 -1
- package/package.json +1 -1
- package/scripts/isolated-cursor-smoke.sh +1 -1
- package/scripts/lib/local-resume-smoke-harness.mjs +2 -1
- package/scripts/local-resume-smoke.mjs +1 -1
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/refresh-cursor-model-snapshots.mjs +3 -3
- package/shared/cursor-model-selection-identities.d.mts +1 -4
- package/shared/cursor-model-selection-identities.mjs +21 -74
- package/src/bundled-context-windows.ts +6 -13
- package/src/context.ts +1 -5
- package/src/cursor-config.ts +9 -13
- package/src/cursor-live-run-coordinator.ts +11 -0
- package/src/cursor-pi-tool-bridge-run.ts +4 -0
- package/src/cursor-pi-tool-bridge-types.ts +1 -0
- package/src/cursor-provider-turn-prepare.ts +0 -1
- package/src/cursor-state.ts +3 -22
- package/src/index.ts +0 -3
- package/src/model-discovery.ts +16 -62
- package/dist/cursor-question-tool.js +0 -194
- package/node_modules/cross-spawn/node_modules/which/CHANGELOG.md +0 -166
- 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 (`
|
|
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 `
|
|
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
|
-
|
|
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
|
|
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
|
|
package/docs/platform-smoke.md
CHANGED
|
@@ -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
|
|
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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
46
|
-
|
|
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
|
|
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
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
82
|
-
|
|
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 :
|
|
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
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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.
|
|
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"
|
|
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);
|
package/src/cursor-config.ts
CHANGED
|
@@ -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" | "
|
|
28
|
-
export type CursorConfigTrustLevel = "one-shot" | "environment" | "trusted-project" | "user" | "session" | "
|
|
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
|
|
361
|
-
type CursorFieldValues<T> = Partial<Record<CursorFieldSource, T>>;
|
|
359
|
+
type CursorFieldValues<T> = Partial<Record<CursorConfigSource, T>>;
|
|
362
360
|
|
|
363
|
-
const LOCAL_ORDER:
|
|
364
|
-
const LOCAL_FORCE_ORDER:
|
|
365
|
-
const HTTP1_ORDER:
|
|
366
|
-
const BRIDGE_ORDER:
|
|
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:
|
|
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:
|
|
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);
|
|
@@ -111,6 +111,11 @@ function isPendingBridgeToolRequest(run: CursorLiveRun, request: CursorPiBridgeT
|
|
|
111
111
|
return bridgeRun?.hasPendingPiToolCallId(request.piToolCallId) === true;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
function hasPendingBridgeCalls(run: CursorLiveRun): boolean {
|
|
115
|
+
if (run.bridgeRun?.hasPendingCalls()) return true;
|
|
116
|
+
return run.sessionBridgeRun !== run.bridgeRun && run.sessionBridgeRun?.hasPendingCalls() === true;
|
|
117
|
+
}
|
|
118
|
+
|
|
114
119
|
export interface CursorLiveRunRecord {
|
|
115
120
|
id: string;
|
|
116
121
|
disposed: boolean;
|
|
@@ -494,6 +499,12 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
494
499
|
if (state.leased || state.leaseQueue.length > 0) return;
|
|
495
500
|
state.idleDisposeRequested = false;
|
|
496
501
|
state.idleDisposeTimer = setTimeout(() => {
|
|
502
|
+
if (hasPendingBridgeCalls(run)) {
|
|
503
|
+
// Bridge calls have their own deadline. Reuse the idle interval to avoid a hot
|
|
504
|
+
// polling loop if the SDK does not emit a terminal event after that deadline.
|
|
505
|
+
coordinator.requestIdleDispose(run);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
497
508
|
void coordinator.release(run).catch(() => {
|
|
498
509
|
// Idle dispose must not leave release failures as unhandled rejections.
|
|
499
510
|
});
|
|
@@ -183,6 +183,10 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
183
183
|
await this.resolveToolResults(context.messages.map(asToolResultMessage).filter((message): message is ToolResultMessage => message !== undefined));
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
hasPendingCalls(): boolean {
|
|
187
|
+
return this.pendingCount() > 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
186
190
|
hasPendingPiToolCallId(piToolCallId: string): boolean {
|
|
187
191
|
return this.pendingByPiToolCallId.has(piToolCallId);
|
|
188
192
|
}
|
|
@@ -65,6 +65,7 @@ export interface CursorPiToolBridgeRun {
|
|
|
65
65
|
takeQueuedToolRequests(): CursorPiBridgeToolRequest[];
|
|
66
66
|
resolveToolResults(toolResults: readonly ToolResultMessage[]): Promise<void>;
|
|
67
67
|
resolveToolResultsFromContext(context: Context): Promise<void>;
|
|
68
|
+
hasPendingCalls(): boolean;
|
|
68
69
|
hasPendingPiToolCallId(piToolCallId: string): boolean;
|
|
69
70
|
isBridgeMcpToolCall(toolCall: unknown): boolean;
|
|
70
71
|
setOnToolRequest(handler?: (request: CursorPiBridgeToolRequest) => void): void;
|
|
@@ -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;
|
package/src/cursor-state.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|