pi-cursor-sdk 0.1.8 → 0.1.10

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/src/context.ts CHANGED
@@ -6,6 +6,15 @@ export interface CursorPrompt {
6
6
  images: SDKImage[];
7
7
  }
8
8
 
9
+ export interface CursorPromptOptions {
10
+ maxInputTokens?: number;
11
+ charsPerToken?: number;
12
+ imageTokenEstimate?: number;
13
+ }
14
+
15
+ const DEFAULT_CHARS_PER_TOKEN = 4;
16
+ const SECTION_SEPARATOR = "\n\n";
17
+
9
18
  function isTextBlock(block: { type: string }): block is { type: "text"; text: string } {
10
19
  return block.type === "text";
11
20
  }
@@ -53,48 +62,132 @@ function formatToolCall(toolCall: ToolCall): string {
53
62
  return `Tool call (${toolCall.name}, call ${toolCall.id}): ${args}`;
54
63
  }
55
64
 
56
- export function buildCursorPrompt(context: Context): CursorPrompt {
57
- const parts: string[] = [];
65
+ function formatMessage(msg: Message): string | undefined {
66
+ switch (msg.role) {
67
+ case "user": {
68
+ const text = formatContentBlocks(msg.content);
69
+ return text ? `User: ${text}` : undefined;
70
+ }
71
+ case "assistant": {
72
+ const blocks = Array.isArray(msg.content) ? msg.content : [{ type: "text" as const, text: String(msg.content) }];
73
+ const textParts: string[] = [];
74
+ for (const block of blocks) {
75
+ if (isTextBlock(block)) {
76
+ textParts.push(block.text);
77
+ } else if (isToolCallBlock(block)) {
78
+ textParts.push(formatToolCall(block));
79
+ }
80
+ // Omit thinking content from transcript
81
+ }
82
+ return textParts.length > 0 ? `Assistant: ${textParts.join("\n")}` : undefined;
83
+ }
84
+ case "toolResult": {
85
+ const text = formatContentBlocks(msg.content);
86
+ const label = msg.isError ? "Tool error" : "Tool result";
87
+ return `${label} (${msg.toolName}, call ${msg.toolCallId}): ${text}`;
88
+ }
89
+ }
90
+ }
58
91
 
59
- if (context.systemPrompt) {
60
- parts.push(`System instructions from pi:\n${context.systemPrompt}`);
92
+ function getLatestUserMessageIndex(messages: Message[]): number {
93
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
94
+ if (messages[index].role === "user") return index;
61
95
  }
96
+ return -1;
97
+ }
62
98
 
63
- for (const msg of context.messages) {
64
- switch (msg.role) {
65
- case "user": {
66
- const text = formatContentBlocks(msg.content);
67
- if (text) parts.push(`User: ${text}`);
68
- break;
69
- }
70
- case "assistant": {
71
- const blocks = Array.isArray(msg.content) ? msg.content : [{ type: "text" as const, text: String(msg.content) }];
72
- const textParts: string[] = [];
73
- for (const block of blocks) {
74
- if (isTextBlock(block)) {
75
- textParts.push(block.text);
76
- } else if (isToolCallBlock(block)) {
77
- textParts.push(formatToolCall(block));
78
- }
79
- // Omit thinking content from transcript
80
- }
81
- if (textParts.length > 0) {
82
- parts.push(`Assistant: ${textParts.join("\n")}`);
83
- }
84
- break;
85
- }
86
- case "toolResult": {
87
- const text = formatContentBlocks(msg.content);
88
- const label = msg.isError ? "Tool error" : "Tool result";
89
- parts.push(`${label} (${msg.toolName}, call ${msg.toolCallId}): ${text}`);
90
- break;
91
- }
99
+ function getSectionCost(section: string): number {
100
+ return section.length + SECTION_SEPARATOR.length;
101
+ }
102
+
103
+ function applyPromptBudget(
104
+ sectionsBeforeMessages: string[],
105
+ messageSections: Array<{ index: number; text: string }>,
106
+ sectionsAfterMessages: string[],
107
+ latestUserMessageIndex: number,
108
+ options: CursorPromptOptions,
109
+ ): string[] {
110
+ const maxInputTokens = options.maxInputTokens;
111
+ if (maxInputTokens === undefined || !Number.isFinite(maxInputTokens) || maxInputTokens <= 0) {
112
+ return [...sectionsBeforeMessages, ...messageSections.map((section) => section.text), ...sectionsAfterMessages];
113
+ }
114
+
115
+ const charsPerToken = options.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
116
+ const maxChars = Math.max(1, Math.floor(maxInputTokens * charsPerToken));
117
+ const requiredMessageSections = messageSections.filter((section) => section.index === latestUserMessageIndex);
118
+ const requiredCost = [...sectionsBeforeMessages, ...requiredMessageSections.map((section) => section.text), ...sectionsAfterMessages].reduce(
119
+ (total, section) => total + getSectionCost(section),
120
+ 0,
121
+ );
122
+ let remainingChars = maxChars - requiredCost;
123
+ const includedMessageIndexes = new Set(requiredMessageSections.map((section) => section.index));
124
+ let omittedMessageCount = 0;
125
+
126
+ for (let index = messageSections.length - 1; index >= 0; index -= 1) {
127
+ const section = messageSections[index];
128
+ if (includedMessageIndexes.has(section.index)) continue;
129
+ const cost = getSectionCost(section.text);
130
+ if (cost <= remainingChars) {
131
+ includedMessageIndexes.add(section.index);
132
+ remainingChars -= cost;
133
+ continue;
92
134
  }
135
+ omittedMessageCount += messageSections
136
+ .slice(0, index + 1)
137
+ .filter((candidate) => !includedMessageIndexes.has(candidate.index)).length;
138
+ break;
93
139
  }
94
140
 
95
- parts.push("Answer the latest user request above using your capabilities. Do not assume access to pi tools.");
141
+ const budgetNotice =
142
+ omittedMessageCount > 0
143
+ ? [`[Earlier transcript omitted: ${omittedMessageCount} message${omittedMessageCount === 1 ? "" : "s"} to fit Cursor context budget]`]
144
+ : [];
145
+ const includedMessages = messageSections
146
+ .filter((section) => includedMessageIndexes.has(section.index))
147
+ .map((section) => section.text);
148
+ return [...sectionsBeforeMessages, ...budgetNotice, ...includedMessages, ...sectionsAfterMessages];
149
+ }
150
+
151
+ export function buildCursorPrompt(context: Context, options: CursorPromptOptions = {}): CursorPrompt {
152
+ const sectionsBeforeMessages: string[] = [
153
+ [
154
+ "Cursor SDK tool boundary:",
155
+ "Only tools exposed by the Cursor SDK in this run are callable. The pi system prompt and transcript are context only; they do not grant access to pi tools or tool names mentioned there.",
156
+ "If the user asks you to search, fetch, browse, or research the web, use an actual Cursor SDK web/search/browser/MCP tool call. If no such Cursor SDK tool is available, say that web search is not configured for this Cursor SDK run.",
157
+ "Do not plan to use or claim to have used pi-only tools such as WebSearch or WebFetch unless the Cursor SDK actually exposes and executes that tool in this run.",
158
+ "Image payload boundary: only images attached to the latest user message are available as image bytes. Earlier images appear only as [image omitted from transcript] placeholders; ask the user to reattach or describe a prior image if the latest request depends on it.",
159
+ ].join("\n"),
160
+ ];
96
161
 
162
+ if (context.systemPrompt) {
163
+ sectionsBeforeMessages.push(`System instructions from pi:\n${context.systemPrompt}`);
164
+ }
165
+
166
+ const messageSections = context.messages
167
+ .map((msg, index) => {
168
+ const text = formatMessage(msg);
169
+ return text ? { index, text } : undefined;
170
+ })
171
+ .filter((section): section is { index: number; text: string } => section !== undefined);
172
+ const sectionsAfterMessages = [
173
+ [
174
+ "Answer the latest user request above using your capabilities. Do not assume access to pi tools.",
175
+ "If the user asks for web research, do not claim to have searched the web unless a Cursor SDK web/search/browser/MCP tool was actually used.",
176
+ ].join("\n"),
177
+ ];
97
178
  const images = extractLatestImages(context.messages);
179
+ const imageTokenReserve = images.length * (options.imageTokenEstimate ?? 0);
180
+ const budgetOptions =
181
+ options.maxInputTokens === undefined
182
+ ? options
183
+ : { ...options, maxInputTokens: Math.max(1, options.maxInputTokens - imageTokenReserve) };
184
+ const parts = applyPromptBudget(
185
+ sectionsBeforeMessages,
186
+ messageSections,
187
+ sectionsAfterMessages,
188
+ getLatestUserMessageIndex(context.messages),
189
+ budgetOptions,
190
+ );
98
191
 
99
- return { text: parts.join("\n\n"), images };
192
+ return { text: parts.join(SECTION_SEPARATOR), images };
100
193
  }
@@ -0,0 +1,145 @@
1
+ import type { ModelListItem } from "@cursor/sdk";
2
+
3
+ // Generated/maintained fallback Cursor catalog snapshot.
4
+ // Refresh with: npm run refresh:cursor-snapshots -- --write
5
+ // Do not add secrets; this file stores public model metadata only.
6
+ export const FALLBACK_MODEL_ITEMS = [
7
+ {
8
+ id: "composer-2",
9
+ displayName: "Cursor Composer 2",
10
+ parameters: [
11
+ {
12
+ id: "fast",
13
+ displayName: "Fast",
14
+ values: [{ value: "false" }, { value: "true" }],
15
+ },
16
+ ],
17
+ variants: [
18
+ {
19
+ params: [{ id: "fast", value: "true" }],
20
+ displayName: "Cursor Composer 2",
21
+ isDefault: true,
22
+ },
23
+ ],
24
+ },
25
+ {
26
+ id: "gpt-5.5",
27
+ displayName: "GPT-5.5",
28
+ parameters: [
29
+ {
30
+ id: "context",
31
+ displayName: "Context",
32
+ values: [{ value: "1m" }, { value: "272k" }],
33
+ },
34
+ {
35
+ id: "reasoning",
36
+ displayName: "Reasoning",
37
+ values: [
38
+ { value: "none" },
39
+ { value: "low" },
40
+ { value: "medium" },
41
+ { value: "high" },
42
+ { value: "extra-high" },
43
+ ],
44
+ },
45
+ {
46
+ id: "fast",
47
+ displayName: "Fast",
48
+ values: [{ value: "false" }, { value: "true" }],
49
+ },
50
+ ],
51
+ variants: [
52
+ {
53
+ params: [
54
+ { id: "context", value: "1m" },
55
+ { id: "reasoning", value: "medium" },
56
+ { id: "fast", value: "false" },
57
+ ],
58
+ displayName: "GPT-5.5",
59
+ isDefault: true,
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ id: "claude-sonnet-4-6",
65
+ displayName: "Sonnet 4.6",
66
+ parameters: [
67
+ {
68
+ id: "thinking",
69
+ displayName: "Thinking",
70
+ values: [{ value: "false" }, { value: "true" }],
71
+ },
72
+ {
73
+ id: "context",
74
+ displayName: "Context",
75
+ values: [{ value: "1m" }, { value: "200k" }],
76
+ },
77
+ {
78
+ id: "effort",
79
+ displayName: "Effort",
80
+ values: [
81
+ { value: "low" },
82
+ { value: "medium" },
83
+ { value: "high" },
84
+ { value: "xhigh" },
85
+ { value: "max" },
86
+ ],
87
+ },
88
+ {
89
+ id: "fast",
90
+ displayName: "Fast",
91
+ values: [{ value: "false" }, { value: "true" }],
92
+ },
93
+ ],
94
+ variants: [
95
+ {
96
+ params: [
97
+ { id: "thinking", value: "true" },
98
+ { id: "context", value: "1m" },
99
+ { id: "effort", value: "medium" },
100
+ { id: "fast", value: "false" },
101
+ ],
102
+ displayName: "Sonnet 4.6",
103
+ isDefault: true,
104
+ },
105
+ ],
106
+ },
107
+ {
108
+ id: "claude-opus-4-7",
109
+ displayName: "Opus 4.7",
110
+ parameters: [
111
+ {
112
+ id: "thinking",
113
+ displayName: "Thinking",
114
+ values: [{ value: "false" }, { value: "true" }],
115
+ },
116
+ {
117
+ id: "context",
118
+ displayName: "Context",
119
+ values: [{ value: "1m" }, { value: "300k" }],
120
+ },
121
+ {
122
+ id: "effort",
123
+ displayName: "Effort",
124
+ values: [
125
+ { value: "low" },
126
+ { value: "medium" },
127
+ { value: "high" },
128
+ { value: "xhigh" },
129
+ { value: "max" },
130
+ ],
131
+ },
132
+ ],
133
+ variants: [
134
+ {
135
+ params: [
136
+ { id: "thinking", value: "true" },
137
+ { id: "context", value: "1m" },
138
+ { id: "effort", value: "xhigh" },
139
+ ],
140
+ displayName: "Opus 4.7",
141
+ isDefault: true,
142
+ },
143
+ ],
144
+ },
145
+ ] satisfies ModelListItem[];
@@ -6,14 +6,16 @@ import {
6
6
  type ExtensionContext,
7
7
  type ToolDefinition,
8
8
  } from "@earendil-works/pi-coding-agent";
9
- import type { TSchema } from "typebox";
9
+ import { Text } from "@earendil-works/pi-tui";
10
+ import { Type, type TSchema } from "typebox";
10
11
  import type { CursorPiToolDisplay } from "./cursor-tool-transcript.js";
11
12
 
12
- const NATIVE_CURSOR_TOOL_NAMES = ["read", "bash", "ls"] as const;
13
+ const NATIVE_CURSOR_TOOL_NAMES = ["read", "bash", "ls", "cursor_edit", "cursor_write"] as const;
13
14
  type NativeCursorToolName = (typeof NATIVE_CURSOR_TOOL_NAMES)[number];
14
15
  const NATIVE_CURSOR_TOOL_DISPLAY_ENV = "PI_CURSOR_NATIVE_TOOL_DISPLAY";
15
16
  // Registration-only kill switch for users who want transcript fallback without shadowing read/bash/ls.
16
17
  const NATIVE_CURSOR_TOOL_REGISTRATION_ENV = "PI_CURSOR_REGISTER_NATIVE_TOOLS";
18
+ const cursorReplayToolSchema = Type.Object({}, { additionalProperties: true });
17
19
 
18
20
  export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
19
21
  id: string;
@@ -22,6 +24,7 @@ export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
22
24
 
23
25
  const registeredNativeToolNames = new Set<NativeCursorToolName>();
24
26
  const nativeToolResults = new Map<string, CursorNativeToolDisplayItem>();
27
+ let currentNativeToolCwd = process.cwd();
25
28
 
26
29
  function readBooleanEnv(name: string): boolean | undefined {
27
30
  const value = process.env[name]?.trim().toLowerCase();
@@ -56,9 +59,14 @@ export function canRenderCursorToolNatively(toolName: string): boolean {
56
59
  return isNativeCursorToolName(toolName) && registeredNativeToolNames.has(toolName);
57
60
  }
58
61
 
59
- export function recordCursorNativeToolDisplay(item: CursorNativeToolDisplayItem): void {
60
- if (!canRenderCursorToolNatively(item.toolName)) return;
62
+ export function recordCursorNativeToolDisplay(item: CursorNativeToolDisplayItem): boolean {
63
+ if (!canRenderCursorToolNatively(item.toolName)) return false;
61
64
  nativeToolResults.set(item.id, item);
65
+ return true;
66
+ }
67
+
68
+ export function deleteCursorNativeToolDisplay(id: string): void {
69
+ nativeToolResults.delete(id);
62
70
  }
63
71
 
64
72
  function consumeCursorNativeToolDisplay(id: string): CursorNativeToolDisplayItem | undefined {
@@ -68,14 +76,17 @@ function consumeCursorNativeToolDisplay(id: string): CursorNativeToolDisplayItem
68
76
  }
69
77
 
70
78
  export const __testUtils = {
79
+ nativeToolResultCount: () => nativeToolResults.size,
71
80
  reset(): void {
72
81
  registeredNativeToolNames.clear();
73
82
  nativeToolResults.clear();
83
+ currentNativeToolCwd = process.cwd();
74
84
  },
75
85
  };
76
86
 
77
87
  function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
78
88
  definition: ToolDefinition<TParams, TDetails, TState>,
89
+ getCurrentDefinition: () => ToolDefinition<TParams, TDetails, TState>,
79
90
  ): ToolDefinition<TParams, TDetails, TState> {
80
91
  return {
81
92
  ...definition,
@@ -88,21 +99,151 @@ function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
88
99
  terminate: cursorDisplay.terminate ?? true,
89
100
  };
90
101
  }
91
- return definition.execute(toolCallId, params, signal, onUpdate, ctx);
102
+ return getCurrentDefinition().execute(toolCallId, params, signal, onUpdate, ctx);
92
103
  },
93
104
  };
94
105
  }
95
106
 
96
- function registerNativeCursorTool(pi: ExtensionAPI, toolName: NativeCursorToolName, cwd: string): void {
97
- if (toolName === "read") {
98
- pi.registerTool(wrapNativeCursorTool(createReadToolDefinition(cwd)));
99
- return;
107
+ interface CursorReplayToolDetails {
108
+ cursorToolName?: "edit" | "write";
109
+ path?: string;
110
+ linesAdded?: number;
111
+ linesRemoved?: number;
112
+ linesCreated?: number;
113
+ fileSize?: number;
114
+ diffString?: string;
115
+ }
116
+
117
+ function asCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
118
+ return value && typeof value === "object" ? (value as CursorReplayToolDetails) : undefined;
119
+ }
120
+
121
+ function getCursorReplayPath(args: Record<string, unknown> | undefined, details: CursorReplayToolDetails | undefined): string {
122
+ const argPath = args?.path;
123
+ return details?.path ?? (typeof argPath === "string" && argPath.trim() ? argPath : "unknown");
124
+ }
125
+
126
+ type CursorReplayRenderCall = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderCall"]>;
127
+ type CursorReplayRenderResult = NonNullable<ToolDefinition<typeof cursorReplayToolSchema, unknown>["renderResult"]>;
128
+ type CursorReplayRenderTheme = Parameters<CursorReplayRenderCall>[1];
129
+
130
+ function formatCursorReplayDiff(diff: string, theme: CursorReplayRenderTheme, maxLines: number): string {
131
+ const lines = diff.split("\n");
132
+ const visible = lines.slice(0, maxLines);
133
+ const rendered = visible.map((line) => {
134
+ if (line.startsWith("+") && !line.startsWith("+++")) return theme.fg("success", line);
135
+ if (line.startsWith("-") && !line.startsWith("---")) return theme.fg("error", line);
136
+ return theme.fg("muted", line);
137
+ });
138
+ if (lines.length > maxLines) rendered.push(theme.fg("muted", `... (${lines.length - maxLines} more diff lines)`));
139
+ return rendered.join("\n");
140
+ }
141
+
142
+ function renderCursorReplayCall(
143
+ toolName: "cursor_edit" | "cursor_write",
144
+ args: Record<string, unknown> | undefined,
145
+ theme: CursorReplayRenderTheme,
146
+ isPartial: boolean,
147
+ ): Text {
148
+ if (!isPartial) return new Text("", 0, 0);
149
+ const cursorToolName = toolName === "cursor_edit" ? "edit" : "write";
150
+ let text = theme.fg("toolTitle", theme.bold(`Cursor ${cursorToolName} `));
151
+ text += theme.fg("accent", getCursorReplayPath(args, undefined));
152
+ return new Text(text, 0, 0);
153
+ }
154
+
155
+ function pluralize(count: number, noun: string): string {
156
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
157
+ }
158
+
159
+ function hasCursorEditChanges(details: CursorReplayToolDetails): boolean {
160
+ return Boolean(details.diffString) || Boolean(details.linesAdded) || Boolean(details.linesRemoved);
161
+ }
162
+
163
+ function classifyCursorEditOperation(details: CursorReplayToolDetails): "created" | "deleted" | "updated" | "unchanged" {
164
+ if (!hasCursorEditChanges(details)) return "unchanged";
165
+ if (details.diffString?.startsWith("--- /dev/null")) return "created";
166
+ if (details.diffString?.includes("\n+++ /dev/null")) return "deleted";
167
+ return "updated";
168
+ }
169
+
170
+ function formatCursorEditSummary(details: CursorReplayToolDetails): string {
171
+ const operation = classifyCursorEditOperation(details);
172
+ if (operation === "unchanged") return "no changes needed";
173
+ if (operation === "created" && details.linesAdded !== undefined) return `created ${pluralize(details.linesAdded, "line")}`;
174
+ if (operation === "deleted" && details.linesRemoved !== undefined) return `deleted ${pluralize(details.linesRemoved, "line")}`;
175
+ const parts = [
176
+ details.linesAdded ? `added ${pluralize(details.linesAdded, "line")}` : undefined,
177
+ details.linesRemoved ? `removed ${pluralize(details.linesRemoved, "line")}` : undefined,
178
+ ].filter((part): part is string => Boolean(part));
179
+ return parts.length > 0 ? parts.join(", ") : "updated file";
180
+ }
181
+
182
+ function renderCursorReplayResult(
183
+ result: Parameters<CursorReplayRenderResult>[0],
184
+ options: Parameters<CursorReplayRenderResult>[1],
185
+ theme: Parameters<CursorReplayRenderResult>[2],
186
+ isError: boolean,
187
+ ): Text {
188
+ if (options.isPartial) return new Text(theme.fg("warning", "Replaying Cursor tool result..."), 0, 0);
189
+ const details = asCursorReplayToolDetails(result.details);
190
+ const content = result.content[0];
191
+ const text = content?.type === "text" ? content.text : "";
192
+ if (isError) return new Text(theme.fg("error", text.split("\n")[0] || "Cursor replay failed"), 0, 0);
193
+
194
+ if (details?.cursorToolName === "edit") {
195
+ const summary = formatCursorEditSummary(details);
196
+ let rendered = `${theme.fg("toolTitle", theme.bold(`Cursor ${classifyCursorEditOperation(details)}`))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`;
197
+ if (details.diffString) rendered += options.expanded ? `\n${formatCursorReplayDiff(details.diffString, theme, 40)}` : theme.fg("muted", " (expand for diff)");
198
+ return new Text(rendered, 0, 0);
100
199
  }
101
- if (toolName === "bash") {
102
- pi.registerTool(wrapNativeCursorTool(createBashToolDefinition(cwd)));
103
- return;
200
+
201
+ if (details?.cursorToolName === "write") {
202
+ const parts = [
203
+ details.linesCreated !== undefined ? `${details.linesCreated} line${details.linesCreated === 1 ? "" : "s"}` : undefined,
204
+ details.fileSize !== undefined ? `${details.fileSize} bytes` : undefined,
205
+ ].filter(Boolean);
206
+ const summary = parts.length > 0 ? parts.join(", ") : "written";
207
+ return new Text(
208
+ `${theme.fg("toolTitle", theme.bold("Cursor write"))} ${theme.fg("accent", getCursorReplayPath(undefined, details))} ${theme.fg("success", summary)}`,
209
+ 0,
210
+ 0,
211
+ );
104
212
  }
105
- pi.registerTool(wrapNativeCursorTool(createLsToolDefinition(cwd)));
213
+
214
+ return new Text(text || theme.fg("success", "Cursor tool result replayed"), 0, 0);
215
+ }
216
+
217
+ function createCursorReplayOnlyToolDefinition(toolName: "cursor_edit" | "cursor_write"): ToolDefinition<typeof cursorReplayToolSchema, unknown> {
218
+ const cursorToolName = toolName === "cursor_edit" ? "edit" : "write";
219
+ return {
220
+ name: toolName,
221
+ label: `Cursor ${cursorToolName}`,
222
+ description: `Replay display for a Cursor SDK ${cursorToolName} operation. This tool only returns recorded Cursor results and never mutates files directly.`,
223
+ parameters: cursorReplayToolSchema,
224
+ renderShell: "self",
225
+ async execute() {
226
+ throw new Error(`No recorded Cursor ${cursorToolName} result was available. This replay-only tool does not execute file mutations.`);
227
+ },
228
+ renderCall(args, theme, context) {
229
+ return renderCursorReplayCall(toolName, args as Record<string, unknown>, theme, context.isPartial);
230
+ },
231
+ renderResult(result, options, theme, context) {
232
+ return renderCursorReplayResult(result, options, theme, context.isError);
233
+ },
234
+ };
235
+ }
236
+
237
+ function createNativeCursorToolDefinition(toolName: NativeCursorToolName, cwd: string): ToolDefinition<TSchema, unknown, unknown> {
238
+ if (toolName === "read") return createReadToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
239
+ if (toolName === "bash") return createBashToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
240
+ if (toolName === "ls") return createLsToolDefinition(cwd) as ToolDefinition<TSchema, unknown, unknown>;
241
+ return createCursorReplayOnlyToolDefinition(toolName) as ToolDefinition<TSchema, unknown, unknown>;
242
+ }
243
+
244
+ function registerNativeCursorTool(pi: ExtensionAPI, toolName: NativeCursorToolName): void {
245
+ const definition = createNativeCursorToolDefinition(toolName, currentNativeToolCwd);
246
+ pi.registerTool(wrapNativeCursorTool(definition, () => createNativeCursorToolDefinition(toolName, currentNativeToolCwd)));
106
247
  }
107
248
 
108
249
  function hasNonBuiltinTool(pi: ExtensionAPI, toolName: NativeCursorToolName): boolean {
@@ -116,7 +257,7 @@ function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionCont
116
257
  return;
117
258
  }
118
259
 
119
- const cwd = process.cwd();
260
+ currentNativeToolCwd = ctx.cwd;
120
261
  const skippedToolNames: string[] = [];
121
262
  for (const toolName of NATIVE_CURSOR_TOOL_NAMES) {
122
263
  if (registeredNativeToolNames.has(toolName)) continue;
@@ -124,7 +265,7 @@ function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionCont
124
265
  skippedToolNames.push(toolName);
125
266
  continue;
126
267
  }
127
- registerNativeCursorTool(pi, toolName, cwd);
268
+ registerNativeCursorTool(pi, toolName);
128
269
  registeredNativeToolNames.add(toolName);
129
270
  }
130
271