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.
Files changed (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +39 -36
  3. package/docs/crabbox-platform-testing-lessons.md +508 -0
  4. package/docs/cursor-dogfood-checklist.md +4 -3
  5. package/docs/cursor-live-smoke-checklist.md +22 -20
  6. package/docs/cursor-model-ux-spec.md +13 -13
  7. package/docs/cursor-native-tool-replay.md +11 -11
  8. package/docs/cursor-native-tool-visual-audit.md +9 -7
  9. package/docs/cursor-testing-lessons.md +20 -15
  10. package/docs/cursor-tool-surfaces.md +5 -5
  11. package/docs/platform-smoke.md +994 -0
  12. package/package.json +32 -3
  13. package/platform-smoke.config.mjs +21 -0
  14. package/scripts/debug-provider-events.mjs +10 -3
  15. package/scripts/debug-sdk-events.mjs +10 -2
  16. package/scripts/isolated-cursor-smoke.sh +4 -4
  17. package/scripts/lib/cursor-visual-render.mjs +1 -0
  18. package/scripts/platform-smoke/artifacts.mjs +124 -0
  19. package/scripts/platform-smoke/assertions.mjs +101 -0
  20. package/scripts/platform-smoke/card-detect.mjs +96 -0
  21. package/scripts/platform-smoke/crabbox-runner.mjs +215 -0
  22. package/scripts/platform-smoke/doctor.mjs +446 -0
  23. package/scripts/platform-smoke/jsonl-text.mjs +31 -0
  24. package/scripts/platform-smoke/live-suite-runner.mjs +677 -0
  25. package/scripts/platform-smoke/platform-build-windows.ps1 +187 -0
  26. package/scripts/platform-smoke/pty-capture.mjs +131 -0
  27. package/scripts/platform-smoke/render-ansi.mjs +65 -0
  28. package/scripts/platform-smoke/scenarios.mjs +186 -0
  29. package/scripts/platform-smoke/targets.mjs +900 -0
  30. package/scripts/platform-smoke/visual-evidence.mjs +139 -0
  31. package/scripts/platform-smoke.mjs +193 -0
  32. package/scripts/probe-mcp-coldstart.mjs +8 -1
  33. package/scripts/steering-rpc-smoke.mjs +1 -1
  34. package/scripts/tmux-live-smoke.sh +3 -3
  35. package/scripts/visual-tui-smoke.mjs +1 -1
  36. package/src/context.ts +2 -4
  37. package/src/cursor-pi-tool-bridge-abort.ts +1 -0
  38. package/src/cursor-pi-tool-bridge-diagnostics.ts +12 -1
  39. package/src/cursor-pi-tool-bridge.ts +46 -1
  40. package/src/cursor-provider-turn-lifecycle-emitter.ts +65 -8
  41. package/src/cursor-provider-turn-tool-ledger.ts +2 -3
  42. package/src/cursor-run-final-text.ts +11 -1
  43. package/src/cursor-skill-tool.ts +273 -0
  44. package/src/cursor-state.ts +38 -19
  45. package/src/cursor-tool-lifecycle.ts +1 -1
  46. package/src/cursor-tool-manifest.ts +1 -1
  47. package/src/cursor-transcript-utils.ts +7 -3
  48. package/src/index.ts +3 -0
@@ -1,4 +1,5 @@
1
- import { hasUsableText } from "./cursor-record-utils.js";
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, "&amp;")
50
+ .replace(/</g, "&lt;")
51
+ .replace(/>/g, "&gt;")
52
+ .replace(/\"/g, "&quot;")
53
+ .replace(/'/g, "&apos;");
54
+ }
55
+
56
+ function getVisibleSkills(skills: readonly Skill[] | undefined): Skill[] {
57
+ return (skills ?? []).filter((skill) => !skill.disableModelInvocation);
58
+ }
59
+
60
+ function setCurrentSkills(skills: readonly Skill[] | undefined): void {
61
+ currentSkillsByName = new Map(getVisibleSkills(skills).map((skill) => [skill.name, skill]));
62
+ }
63
+
64
+ function getAvailableSkillNames(): string[] {
65
+ return [...currentSkillsByName.keys()].sort();
66
+ }
67
+
68
+ function shouldExposeSkillTool(model: ExtensionContext["model"]): boolean {
69
+ return isCursorModel(model) && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
70
+ }
71
+
72
+ function syncCursorSkillToolForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
73
+ const activeToolNames = new Set(pi.getActiveTools());
74
+ const shouldBeActive = shouldExposeSkillTool(model);
75
+ const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
76
+ if (shouldBeActive === alreadyActive) return;
77
+ if (shouldBeActive) {
78
+ activeToolNames.add(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
79
+ } else {
80
+ activeToolNames.delete(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
81
+ }
82
+ pi.setActiveTools([...activeToolNames]);
83
+ }
84
+
85
+ export function formatCursorSkillsForPrompt(skills: readonly Skill[]): string {
86
+ const visibleSkills = getVisibleSkills(skills);
87
+ if (visibleSkills.length === 0) return "";
88
+
89
+ const lines = [
90
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
91
+ `When a task matches a skill's description, call ${CURSOR_ACTIVATE_SKILL_MCP_NAME} with the skill name to load its full SKILL.md instructions before proceeding.`,
92
+ "If the pi bridge is disabled and the activation tool is unavailable, use Cursor's file-read capability on the listed SKILL.md location instead.",
93
+ "When a skill references relative paths, resolve them against the skill directory (the parent of SKILL.md / dirname of the path) and use absolute paths in tool calls.",
94
+ "",
95
+ "<available_skills>",
96
+ ];
97
+ for (const skill of visibleSkills) {
98
+ lines.push(" <skill>");
99
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
100
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
101
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
102
+ lines.push(" </skill>");
103
+ }
104
+ lines.push("</available_skills>");
105
+ return lines.join("\n");
106
+ }
107
+
108
+ export function resolveCursorSkillSystemPrompt(
109
+ systemPrompt: string,
110
+ model: ExtensionContext["model"],
111
+ systemPromptOptions?: BuildSystemPromptOptions,
112
+ ): string {
113
+ if (!isCursorModel(model)) return systemPrompt;
114
+ const skills = getVisibleSkills(systemPromptOptions?.skills);
115
+ if (skills.length === 0) return systemPrompt;
116
+ const replacement = formatCursorSkillsForPrompt(skills);
117
+ if (AVAILABLE_SKILLS_SECTION_PATTERN.test(systemPrompt)) {
118
+ return systemPrompt.replace(AVAILABLE_SKILLS_SECTION_PATTERN, replacement);
119
+ }
120
+ return `${systemPrompt}${replacement}`;
121
+ }
122
+
123
+ async function collectResourcePaths(root: string, absoluteDir: string, output: string[]): Promise<void> {
124
+ if (output.length >= MAX_SKILL_RESOURCES) return;
125
+ let entries: Dirent[];
126
+ try {
127
+ entries = await readdir(absoluteDir, { withFileTypes: true });
128
+ } catch {
129
+ return;
130
+ }
131
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
132
+ if (output.length >= MAX_SKILL_RESOURCES) return;
133
+ const absolutePath = join(absoluteDir, entry.name);
134
+ if (entry.isSymbolicLink()) continue;
135
+ if (entry.isDirectory()) {
136
+ await collectResourcePaths(root, absolutePath, output);
137
+ continue;
138
+ }
139
+ if (!entry.isFile()) continue;
140
+ output.push(relative(root, absolutePath).replace(/\\/g, "/"));
141
+ }
142
+ }
143
+
144
+ async function listSkillResourcePaths(baseDir: string): Promise<string[]> {
145
+ const resources: string[] = [];
146
+ for (const resourceDirName of RESOURCE_DIR_NAMES) {
147
+ await collectResourcePaths(baseDir, join(baseDir, resourceDirName), resources);
148
+ if (resources.length >= MAX_SKILL_RESOURCES) break;
149
+ }
150
+ return resources;
151
+ }
152
+
153
+ function buildActivationDetails(skill: Skill | undefined, resources: string[] = []): CursorSkillActivationDetails {
154
+ return {
155
+ name: skill?.name,
156
+ filePath: skill?.filePath,
157
+ baseDir: skill ? dirname(skill.filePath) : undefined,
158
+ resources,
159
+ availableSkillNames: getAvailableSkillNames(),
160
+ };
161
+ }
162
+
163
+ function formatSkillResources(resources: readonly string[]): string {
164
+ if (resources.length === 0) return "<skill_resources />";
165
+ return [
166
+ "<skill_resources>",
167
+ ...resources.map((resource) => ` <file>${escapeXml(resource)}</file>`),
168
+ "</skill_resources>",
169
+ ].join("\n");
170
+ }
171
+
172
+ function wrapSkillContent(skill: Skill, content: string, resources: readonly string[]): string {
173
+ const baseDir = dirname(skill.filePath);
174
+ return [
175
+ `<skill_content name=\"${escapeXml(skill.name)}\">`,
176
+ content.trim(),
177
+ "",
178
+ `Skill directory: ${baseDir}`,
179
+ "Relative paths in this skill are relative to the skill directory.",
180
+ formatSkillResources(resources),
181
+ "</skill_content>",
182
+ ].join("\n");
183
+ }
184
+
185
+ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
186
+ pi.registerTool({
187
+ name: CURSOR_ACTIVATE_SKILL_TOOL_NAME,
188
+ label: "Cursor skill",
189
+ description: "Load full pi Agent Skill instructions for Cursor. Use with a skill name from the current <available_skills> catalog before applying that skill.",
190
+ parameters: Type.Object({
191
+ name: Type.String({ description: "Skill name from the current <available_skills> catalog" }),
192
+ }),
193
+ promptGuidelines: [
194
+ `Use ${CURSOR_ACTIVATE_SKILL_TOOL_NAME} only for skill names listed in the current <available_skills> catalog.`,
195
+ "After loading a skill, follow its instructions and resolve relative skill paths against the returned skill directory.",
196
+ ],
197
+ async execute(_toolCallId, params) {
198
+ const requestedName = (params as CursorActivateSkillParams).name?.trim();
199
+ if (!requestedName) {
200
+ return {
201
+ content: [{ type: "text" as const, text: "No skill name was provided." }],
202
+ details: buildActivationDetails(undefined),
203
+ isError: true,
204
+ };
205
+ }
206
+ const skill = currentSkillsByName.get(requestedName);
207
+ if (!skill) {
208
+ return {
209
+ content: [{ type: "text" as const, text: `Skill not available: ${requestedName}. Available skills: ${getAvailableSkillNames().join(", ") || "none"}.` }],
210
+ details: buildActivationDetails(undefined),
211
+ isError: true,
212
+ };
213
+ }
214
+
215
+ try {
216
+ const [content, resources] = await Promise.all([
217
+ readFile(skill.filePath, "utf8"),
218
+ listSkillResourcePaths(dirname(skill.filePath)),
219
+ ]);
220
+ return {
221
+ content: [{ type: "text" as const, text: wrapSkillContent(skill, content, resources) }],
222
+ details: buildActivationDetails(skill, resources),
223
+ };
224
+ } catch (error) {
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text" as const,
229
+ text: `Failed to load skill ${requestedName} from ${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`,
230
+ },
231
+ ],
232
+ details: buildActivationDetails(skill),
233
+ isError: true,
234
+ };
235
+ }
236
+ },
237
+ });
238
+
239
+ const clearSkillsAndSync = (model: ExtensionContext["model"]): void => {
240
+ setCurrentSkills([]);
241
+ syncCursorSkillToolForModel(pi, model);
242
+ };
243
+
244
+ pi.on("session_start", (_event, ctx) => {
245
+ clearSkillsAndSync(ctx.model);
246
+ });
247
+ pi.on("model_select", (event) => {
248
+ clearSkillsAndSync(event.model);
249
+ });
250
+ pi.on("turn_start", (_event, ctx) => {
251
+ if (!isCursorModel(ctx.model)) setCurrentSkills([]);
252
+ syncCursorSkillToolForModel(pi, ctx.model);
253
+ });
254
+ pi.on("before_agent_start", (event, ctx) => {
255
+ if (isCursorModel(ctx.model)) {
256
+ setCurrentSkills(event.systemPromptOptions?.skills);
257
+ } else {
258
+ setCurrentSkills([]);
259
+ }
260
+ syncCursorSkillToolForModel(pi, ctx.model);
261
+ const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions);
262
+ if (resolved === event.systemPrompt) return undefined;
263
+ return { systemPrompt: resolved };
264
+ });
265
+ }
266
+
267
+ export const __testUtils = {
268
+ AVAILABLE_SKILLS_SECTION_PATTERN,
269
+ buildActivationDetails,
270
+ setCurrentSkills,
271
+ listSkillResourcePaths,
272
+ wrapSkillContent,
273
+ };
@@ -29,7 +29,8 @@ export type CursorAgentMode = AgentModeOption;
29
29
  const DEFAULT_CURSOR_AGENT_MODE: AgentModeOption = "agent";
30
30
 
31
31
  interface CursorFastEntryData {
32
- baseModelId: string;
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
- sessionFastPreferences.set(entry.data.baseModelId, entry.data.fast);
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 getEffectiveFast(baseModelId: string, modelId: string): boolean | undefined {
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 sessionFastPreferences.get(baseModelId) ?? globalFastPreferences.get(baseModelId) ?? metadata.defaultFast;
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(metadata.baseModelId, model.id) : undefined;
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">, baseModelId: string, fast: boolean): void {
190
- const previousSession = sessionFastPreferences.get(baseModelId);
191
- const previousGlobal = globalFastPreferences.get(baseModelId);
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(baseModelId, fast);
194
- globalFastPreferences.set(baseModelId, fast);
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, { baseModelId, fast });
218
+ pi.appendEntry<CursorFastEntryData>(FAST_ENTRY_TYPE, { modelId, fast });
199
219
  } catch (error) {
200
- restoreMapValue(sessionFastPreferences, baseModelId, previousSession);
201
- restoreMapValue(globalFastPreferences, baseModelId, previousGlobal);
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
- const metadata = getCursorModelMetadata(modelId);
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 current = getEffectiveFast(metadata.baseModelId, metadata.piModelId) ?? false;
348
+ const preferenceModelId = getFastPreferenceModelId(metadata);
349
+ const current = getEffectiveFast(metadata.piModelId) ?? false;
331
350
  const next = !current;
332
351
  try {
333
- persistFastPreference(pi, metadata.baseModelId, next);
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 "shell";
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.16 local-agent ToolType values; actual exposure can vary by run.
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;