pi-cursor-sdk 0.1.28 → 0.1.30
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 +29 -0
- package/README.md +39 -36
- package/docs/crabbox-platform-testing-lessons.md +508 -0
- package/docs/cursor-dogfood-checklist.md +4 -3
- package/docs/cursor-live-smoke-checklist.md +22 -20
- package/docs/cursor-model-ux-spec.md +13 -13
- package/docs/cursor-native-tool-replay.md +11 -11
- package/docs/cursor-native-tool-visual-audit.md +9 -7
- package/docs/cursor-testing-lessons.md +20 -15
- package/docs/cursor-tool-surfaces.md +5 -5
- package/docs/platform-smoke.md +994 -0
- package/package.json +32 -3
- package/platform-smoke.config.mjs +21 -0
- package/scripts/debug-provider-events.mjs +10 -3
- package/scripts/debug-sdk-events.mjs +10 -2
- package/scripts/isolated-cursor-smoke.sh +4 -4
- package/scripts/lib/cursor-visual-render.mjs +1 -0
- package/scripts/platform-smoke/artifacts.mjs +124 -0
- package/scripts/platform-smoke/assertions.mjs +101 -0
- package/scripts/platform-smoke/card-detect.mjs +96 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +215 -0
- package/scripts/platform-smoke/doctor.mjs +446 -0
- package/scripts/platform-smoke/jsonl-text.mjs +31 -0
- package/scripts/platform-smoke/live-suite-runner.mjs +677 -0
- package/scripts/platform-smoke/platform-build-windows.ps1 +187 -0
- package/scripts/platform-smoke/pty-capture.mjs +131 -0
- package/scripts/platform-smoke/render-ansi.mjs +65 -0
- package/scripts/platform-smoke/scenarios.mjs +186 -0
- package/scripts/platform-smoke/targets.mjs +900 -0
- package/scripts/platform-smoke/visual-evidence.mjs +139 -0
- package/scripts/platform-smoke.mjs +193 -0
- package/scripts/probe-mcp-coldstart.mjs +8 -1
- package/scripts/steering-rpc-smoke.mjs +1 -1
- package/scripts/tmux-live-smoke.sh +3 -3
- package/scripts/visual-tui-smoke.mjs +1 -1
- package/src/context.ts +2 -4
- package/src/cursor-pi-tool-bridge-abort.ts +1 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +12 -1
- package/src/cursor-pi-tool-bridge.ts +46 -1
- package/src/cursor-provider-turn-lifecycle-emitter.ts +65 -8
- package/src/cursor-provider-turn-tool-ledger.ts +2 -3
- package/src/cursor-run-final-text.ts +11 -1
- package/src/cursor-skill-tool.ts +273 -0
- package/src/cursor-state.ts +38 -19
- package/src/cursor-tool-lifecycle.ts +1 -1
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-transcript-utils.ts +7 -3
- package/src/index.ts +3 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { asRecord, hasUsableText } from "./cursor-record-utils.js";
|
|
2
3
|
|
|
3
4
|
function isCursorTextBoundary(text: string, index: number): boolean {
|
|
4
5
|
if (index <= 0 || index >= text.length) return true;
|
|
@@ -39,6 +40,15 @@ export function trimCurrentTurnAlreadyEmittedCursorText(
|
|
|
39
40
|
return trimAlreadyEmittedCursorText(text, emittedText);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
export function getFinalAssistantText(message: Pick<AssistantMessage, "content">): string {
|
|
44
|
+
for (let index = message.content.length - 1; index >= 0; index--) {
|
|
45
|
+
const block = asRecord(message.content[index]);
|
|
46
|
+
if (block?.type !== "text" || typeof block.text !== "string") continue;
|
|
47
|
+
if (hasUsableText(block.text)) return block.text;
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
export function selectCursorFinalText(
|
|
43
53
|
resultText: unknown,
|
|
44
54
|
textDeltas: readonly string[],
|
|
@@ -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, "&")
|
|
50
|
+
.replace(/</g, "<")
|
|
51
|
+
.replace(/>/g, ">")
|
|
52
|
+
.replace(/\"/g, """)
|
|
53
|
+
.replace(/'/g, "'");
|
|
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
|
+
};
|
package/src/cursor-state.ts
CHANGED
|
@@ -29,7 +29,8 @@ export type CursorAgentMode = AgentModeOption;
|
|
|
29
29
|
const DEFAULT_CURSOR_AGENT_MODE: AgentModeOption = "agent";
|
|
30
30
|
|
|
31
31
|
interface CursorFastEntryData {
|
|
32
|
-
|
|
32
|
+
modelId?: string;
|
|
33
|
+
baseModelId?: string;
|
|
33
34
|
fast: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -71,7 +72,11 @@ export function parseCursorAgentMode(raw: unknown): AgentModeOption | undefined
|
|
|
71
72
|
function isCursorFastEntryData(value: unknown): value is CursorFastEntryData {
|
|
72
73
|
if (!value || typeof value !== "object") return false;
|
|
73
74
|
const data = value as Record<string, unknown>;
|
|
74
|
-
return typeof data.baseModelId === "string" && typeof data.fast === "boolean";
|
|
75
|
+
return (typeof data.modelId === "string" || typeof data.baseModelId === "string") && typeof data.fast === "boolean";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getCursorFastEntryModelId(data: CursorFastEntryData): string {
|
|
79
|
+
return data.modelId ?? data.baseModelId ?? "";
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
function isCursorModeEntryData(value: unknown): value is CursorModeEntryData {
|
|
@@ -113,7 +118,8 @@ function restoreSessionFastPreferences(ctx: { sessionManager: Pick<ExtensionCont
|
|
|
113
118
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
114
119
|
if (entry.type !== "custom" || entry.customType !== FAST_ENTRY_TYPE) continue;
|
|
115
120
|
if (isCursorFastEntryData(entry.data)) {
|
|
116
|
-
|
|
121
|
+
const modelId = getCursorFastEntryModelId(entry.data);
|
|
122
|
+
if (modelId) sessionFastPreferences.set(modelId, entry.data.fast);
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
}
|
|
@@ -128,12 +134,26 @@ function restoreSessionCursorMode(ctx: { sessionManager: Pick<ExtensionContext["
|
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
function
|
|
137
|
+
function getFastPreferenceModelId(metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>): string {
|
|
138
|
+
return metadata.selectionModelId || metadata.baseModelId;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getStoredFastPreference(metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>): boolean | undefined {
|
|
142
|
+
const preferenceModelId = getFastPreferenceModelId(metadata);
|
|
143
|
+
return (
|
|
144
|
+
sessionFastPreferences.get(preferenceModelId) ??
|
|
145
|
+
(preferenceModelId !== metadata.baseModelId ? sessionFastPreferences.get(metadata.baseModelId) : undefined) ??
|
|
146
|
+
globalFastPreferences.get(preferenceModelId) ??
|
|
147
|
+
(preferenceModelId !== metadata.baseModelId ? globalFastPreferences.get(metadata.baseModelId) : undefined)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getEffectiveFast(modelId: string): boolean | undefined {
|
|
132
152
|
const metadata = getCursorModelMetadata(modelId);
|
|
133
153
|
if (!metadata?.supportsFast) return undefined;
|
|
134
154
|
if (cliForceNoFast) return false;
|
|
135
155
|
if (cliForceFast) return true;
|
|
136
|
-
return
|
|
156
|
+
return getStoredFastPreference(metadata) ?? metadata.defaultFast;
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
function formatInvalidCursorMode(raw: string): string {
|
|
@@ -168,7 +188,7 @@ function updateCursorStatus(ctx: Pick<ExtensionContext, "model" | "ui">, model =
|
|
|
168
188
|
return;
|
|
169
189
|
}
|
|
170
190
|
const metadata = getCursorModelMetadata(model.id);
|
|
171
|
-
const fast = metadata?.supportsFast ? getEffectiveFast(
|
|
191
|
+
const fast = metadata?.supportsFast ? getEffectiveFast(model.id) : undefined;
|
|
172
192
|
ctx.ui.setStatus("cursor", formatCursorStatus(fast));
|
|
173
193
|
}
|
|
174
194
|
|
|
@@ -186,19 +206,19 @@ function restoreMapValue(map: Map<string, boolean>, key: string, previous: boole
|
|
|
186
206
|
}
|
|
187
207
|
}
|
|
188
208
|
|
|
189
|
-
function persistFastPreference(pi: Pick<ExtensionAPI, "appendEntry">,
|
|
190
|
-
const previousSession = sessionFastPreferences.get(
|
|
191
|
-
const previousGlobal = globalFastPreferences.get(
|
|
209
|
+
function persistFastPreference(pi: Pick<ExtensionAPI, "appendEntry">, modelId: string, fast: boolean): void {
|
|
210
|
+
const previousSession = sessionFastPreferences.get(modelId);
|
|
211
|
+
const previousGlobal = globalFastPreferences.get(modelId);
|
|
192
212
|
let savedGlobal = false;
|
|
193
|
-
sessionFastPreferences.set(
|
|
194
|
-
globalFastPreferences.set(
|
|
213
|
+
sessionFastPreferences.set(modelId, fast);
|
|
214
|
+
globalFastPreferences.set(modelId, fast);
|
|
195
215
|
try {
|
|
196
216
|
saveGlobalFastPreferences();
|
|
197
217
|
savedGlobal = true;
|
|
198
|
-
pi.appendEntry<CursorFastEntryData>(FAST_ENTRY_TYPE, {
|
|
218
|
+
pi.appendEntry<CursorFastEntryData>(FAST_ENTRY_TYPE, { modelId, fast });
|
|
199
219
|
} catch (error) {
|
|
200
|
-
restoreMapValue(sessionFastPreferences,
|
|
201
|
-
restoreMapValue(globalFastPreferences,
|
|
220
|
+
restoreMapValue(sessionFastPreferences, modelId, previousSession);
|
|
221
|
+
restoreMapValue(globalFastPreferences, modelId, previousGlobal);
|
|
202
222
|
if (savedGlobal) {
|
|
203
223
|
try {
|
|
204
224
|
saveGlobalFastPreferences();
|
|
@@ -285,9 +305,7 @@ function emitCursorToolsDebugReport(
|
|
|
285
305
|
}
|
|
286
306
|
|
|
287
307
|
export function getEffectiveFastForModelId(modelId: string): boolean | undefined {
|
|
288
|
-
|
|
289
|
-
if (!metadata) return undefined;
|
|
290
|
-
return getEffectiveFast(metadata.baseModelId, modelId);
|
|
308
|
+
return getEffectiveFast(modelId);
|
|
291
309
|
}
|
|
292
310
|
|
|
293
311
|
export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtensionApi): void {
|
|
@@ -327,10 +345,11 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
|
|
|
327
345
|
return;
|
|
328
346
|
}
|
|
329
347
|
|
|
330
|
-
const
|
|
348
|
+
const preferenceModelId = getFastPreferenceModelId(metadata);
|
|
349
|
+
const current = getEffectiveFast(metadata.piModelId) ?? false;
|
|
331
350
|
const next = !current;
|
|
332
351
|
try {
|
|
333
|
-
persistFastPreference(pi,
|
|
352
|
+
persistFastPreference(pi, preferenceModelId, next);
|
|
334
353
|
} catch (error) {
|
|
335
354
|
updateCursorStatus(ctx);
|
|
336
355
|
ctx.ui.notify(`Failed to save Cursor fast preference: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
@@ -45,7 +45,7 @@ export function buildCursorToolLifecycleLabel(toolCall: unknown, apiKey?: string
|
|
|
45
45
|
return scrubLifecycleDetail(getString(args, "description"), apiKey) ?? "task";
|
|
46
46
|
}
|
|
47
47
|
case "shell": {
|
|
48
|
-
return "
|
|
48
|
+
return scrubLifecycleDetail(getString(args, "command") ?? getString(args, "cmd"), apiKey);
|
|
49
49
|
}
|
|
50
50
|
case "mcp": {
|
|
51
51
|
return scrubLifecycleDetail(getString(args, "toolName"), apiKey) ?? "mcp";
|
|
@@ -4,7 +4,7 @@ import type { CursorPiToolBridgeSnapshot } from "./cursor-pi-tool-bridge-types.j
|
|
|
4
4
|
export const CURSOR_TOOL_MANIFEST_ENV = "PI_CURSOR_TOOL_MANIFEST";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Representative @cursor/sdk@1.0.
|
|
7
|
+
* Representative @cursor/sdk@1.0.17 local-agent ToolType values; actual exposure can vary by run.
|
|
8
8
|
* See docs/cursor-native-tool-replay.md#sdk-tooltype-replay-matrix.
|
|
9
9
|
*/
|
|
10
10
|
export const CURSOR_HOST_TOOL_MANIFEST_SUMMARY =
|
|
@@ -145,14 +145,18 @@ export function formatError(error: unknown): string {
|
|
|
145
145
|
return text ? `Error: ${text}` : "Error";
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function normalizeDisplaySeparators(path: string): string {
|
|
149
|
+
return path.replace(/\\/g, "/");
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
export function formatDisplayPath(path: string, cwd = process.cwd()): string {
|
|
149
153
|
const trimmed = path.trim();
|
|
150
154
|
if (!trimmed) return trimmed;
|
|
151
|
-
if (!isAbsolute(trimmed)) return trimmed;
|
|
155
|
+
if (!isAbsolute(trimmed)) return normalizeDisplaySeparators(trimmed);
|
|
152
156
|
const relativePath = relative(cwd, trimmed);
|
|
153
157
|
if (!relativePath || relativePath === "") return ".";
|
|
154
|
-
if (relativePath.startsWith("..") || isAbsolute(relativePath)) return trimmed;
|
|
155
|
-
return relativePath;
|
|
158
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) return normalizeDisplaySeparators(trimmed);
|
|
159
|
+
return normalizeDisplaySeparators(relativePath);
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
export function formatDiffPath(path: string, cwd = process.cwd()): string {
|
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;
|