pi-cursor-sdk 0.1.15 → 0.1.16
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 +27 -0
- package/README.md +19 -7
- package/docs/cursor-live-smoke-checklist.md +271 -0
- package/docs/cursor-model-ux-spec.md +12 -3
- package/docs/cursor-native-tool-replay.md +16 -5
- package/package.json +2 -1
- package/src/context.ts +180 -5
- package/src/cursor-bridge-contract.ts +27 -0
- package/src/cursor-live-run-accounting.ts +65 -0
- package/src/cursor-native-tool-display.ts +14 -5
- package/src/cursor-pi-tool-bridge.ts +565 -28
- package/src/cursor-provider.ts +200 -128
- package/src/cursor-question-tool.ts +7 -2
- package/src/cursor-session-agent.ts +372 -0
- package/src/cursor-session-cwd.ts +14 -19
- package/src/cursor-session-scope.ts +65 -0
- package/src/cursor-state.ts +38 -10
- package/src/cursor-usage-accounting.ts +71 -0
- package/src/index.ts +20 -3
package/src/context.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import type { Context, Message, ToolCall } from "@earendil-works/pi-ai";
|
|
3
|
+
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
2
4
|
import type { SDKImage } from "@cursor/sdk";
|
|
5
|
+
import { getCursorPiBridgeContractText } from "./cursor-bridge-contract.js";
|
|
3
6
|
import { getCursorReplayPromptLabel } from "./cursor-tool-names.js";
|
|
4
7
|
|
|
5
8
|
export interface CursorPrompt {
|
|
@@ -13,9 +16,14 @@ export interface CursorPromptOptions {
|
|
|
13
16
|
imageTokenEstimate?: number;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
const
|
|
19
|
+
export const CURSOR_APPROX_CHARS_PER_TOKEN = 4;
|
|
20
|
+
export const CURSOR_IMAGE_TOKEN_ESTIMATE = 1200;
|
|
17
21
|
const SECTION_SEPARATOR = "\n\n";
|
|
18
22
|
|
|
23
|
+
function normalizePiContextMessages(messages: Context["messages"]): Message[] {
|
|
24
|
+
return convertToLlm(messages as Parameters<typeof convertToLlm>[0]);
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
function isTextBlock(block: { type: string }): block is { type: "text"; text: string } {
|
|
20
28
|
return block.type === "text";
|
|
21
29
|
}
|
|
@@ -131,7 +139,7 @@ function applyPromptBudget(
|
|
|
131
139
|
return [...sectionsBeforeMessages, ...messageSections.map((section) => section.text), ...sectionsAfterMessages];
|
|
132
140
|
}
|
|
133
141
|
|
|
134
|
-
const charsPerToken = options.charsPerToken ??
|
|
142
|
+
const charsPerToken = options.charsPerToken ?? CURSOR_APPROX_CHARS_PER_TOKEN;
|
|
135
143
|
const maxChars = Math.max(1, Math.floor(maxInputTokens * charsPerToken));
|
|
136
144
|
const requiredMessageSections = messageSections.filter((section) => section.index === latestUserMessageIndex);
|
|
137
145
|
const requiredCost = [...sectionsBeforeMessages, ...requiredMessageSections.map((section) => section.text), ...sectionsAfterMessages].reduce(
|
|
@@ -167,11 +175,177 @@ function applyPromptBudget(
|
|
|
167
175
|
return [...sectionsBeforeMessages, ...budgetNotice, ...includedMessages, ...sectionsAfterMessages];
|
|
168
176
|
}
|
|
169
177
|
|
|
178
|
+
export function estimateCursorTextTokens(text: string, options: Pick<CursorPromptOptions, "charsPerToken"> = {}): number {
|
|
179
|
+
const charsPerToken = options.charsPerToken ?? CURSOR_APPROX_CHARS_PER_TOKEN;
|
|
180
|
+
return Math.ceil(text.length / charsPerToken);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function estimateCursorPromptTokens(prompt: CursorPrompt, options: Pick<CursorPromptOptions, "charsPerToken" | "imageTokenEstimate"> = {}): number {
|
|
184
|
+
return estimateCursorTextTokens(prompt.text, options) + prompt.images.length * (options.imageTokenEstimate ?? CURSOR_IMAGE_TOKEN_ESTIMATE);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function estimateCursorPromptMessageTokens(message: Message, options: Pick<CursorPromptOptions, "charsPerToken"> = {}): number {
|
|
188
|
+
const text = formatMessage(message);
|
|
189
|
+
return text ? estimateCursorTextTokens(text, options) : 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function estimateCursorContextTokens(context: Context, options: CursorPromptOptions = {}): number {
|
|
193
|
+
return estimateCursorPromptTokens(buildCursorPrompt(context, options), options);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface CursorContextFingerprintPayload {
|
|
197
|
+
systemHash: string;
|
|
198
|
+
messageHashes: string[];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function hashCursorContextValue(value: string): string {
|
|
202
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function serializeMessageForFingerprint(message: Message, index: number): string {
|
|
206
|
+
switch (message.role) {
|
|
207
|
+
case "user": {
|
|
208
|
+
const text =
|
|
209
|
+
typeof message.content === "string"
|
|
210
|
+
? message.content
|
|
211
|
+
: JSON.stringify(message.content);
|
|
212
|
+
return hashCursorContextValue(`user:${message.timestamp ?? index}:${text}`);
|
|
213
|
+
}
|
|
214
|
+
case "assistant":
|
|
215
|
+
return hashCursorContextValue(`assistant:${message.timestamp ?? index}:${JSON.stringify(message.content)}`);
|
|
216
|
+
case "toolResult":
|
|
217
|
+
return hashCursorContextValue(
|
|
218
|
+
`toolResult:${message.timestamp ?? index}:${message.toolCallId}:${message.toolName}:${JSON.stringify(message.content)}:${message.isError === true}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function serializeRawPiMessageForFingerprint(message: Context["messages"][number], index: number): string {
|
|
224
|
+
const role = (message as { role?: string }).role;
|
|
225
|
+
switch (role) {
|
|
226
|
+
case "branchSummary": {
|
|
227
|
+
const entry = message as { summary?: string; fromId?: string; timestamp?: number };
|
|
228
|
+
return hashCursorContextValue(
|
|
229
|
+
`branchSummary:${entry.timestamp ?? index}:${entry.fromId ?? ""}:${entry.summary ?? ""}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
case "compactionSummary": {
|
|
233
|
+
const entry = message as { summary?: string; tokensBefore?: number; timestamp?: number };
|
|
234
|
+
return hashCursorContextValue(
|
|
235
|
+
`compactionSummary:${entry.timestamp ?? index}:${entry.tokensBefore ?? ""}:${entry.summary ?? ""}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
case "custom": {
|
|
239
|
+
const entry = message as { customType?: string; content?: unknown; timestamp?: number };
|
|
240
|
+
return hashCursorContextValue(
|
|
241
|
+
`custom:${entry.timestamp ?? index}:${entry.customType ?? ""}:${JSON.stringify(entry.content)}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
case "bashExecution": {
|
|
245
|
+
const entry = message as {
|
|
246
|
+
command?: string;
|
|
247
|
+
output?: string;
|
|
248
|
+
exitCode?: number | null;
|
|
249
|
+
cancelled?: boolean;
|
|
250
|
+
excludeFromContext?: boolean;
|
|
251
|
+
timestamp?: number;
|
|
252
|
+
};
|
|
253
|
+
if (entry.excludeFromContext) {
|
|
254
|
+
return hashCursorContextValue(`bashExecution:excluded:${entry.timestamp ?? index}`);
|
|
255
|
+
}
|
|
256
|
+
return hashCursorContextValue(
|
|
257
|
+
`bashExecution:${entry.timestamp ?? index}:${entry.command ?? ""}:${entry.output ?? ""}:${entry.exitCode ?? ""}:${entry.cancelled === true}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
default:
|
|
261
|
+
return serializeMessageForFingerprint(message as Message, index);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function parseCursorContextFingerprint(fingerprint: string): CursorContextFingerprintPayload | undefined {
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(fingerprint) as CursorContextFingerprintPayload;
|
|
268
|
+
if (!parsed || typeof parsed.systemHash !== "string" || !Array.isArray(parsed.messageHashes)) return undefined;
|
|
269
|
+
if (!parsed.messageHashes.every((entry) => typeof entry === "string")) return undefined;
|
|
270
|
+
return parsed;
|
|
271
|
+
} catch {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function computeCursorContextFingerprint(context: Context): string {
|
|
277
|
+
const payload: CursorContextFingerprintPayload = {
|
|
278
|
+
systemHash: hashCursorContextValue(context.systemPrompt ?? ""),
|
|
279
|
+
messageHashes: context.messages.map((message, index) => serializeRawPiMessageForFingerprint(message, index)),
|
|
280
|
+
};
|
|
281
|
+
return JSON.stringify(payload);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function shouldBootstrapCursorSend(
|
|
285
|
+
sendState: { bootstrapped: boolean; contextFingerprint: string },
|
|
286
|
+
context: Context,
|
|
287
|
+
): boolean {
|
|
288
|
+
if (!sendState.bootstrapped) return true;
|
|
289
|
+
const previous = parseCursorContextFingerprint(sendState.contextFingerprint);
|
|
290
|
+
if (!previous) return true;
|
|
291
|
+
const current = parseCursorContextFingerprint(computeCursorContextFingerprint(context));
|
|
292
|
+
if (!current) return true;
|
|
293
|
+
if (current.systemHash !== previous.systemHash) return true;
|
|
294
|
+
if (current.messageHashes.length < previous.messageHashes.length) return true;
|
|
295
|
+
if (current.messageHashes.length > previous.messageHashes.length) {
|
|
296
|
+
for (let index = previous.messageHashes.length; index < context.messages.length; index += 1) {
|
|
297
|
+
const role = (context.messages[index] as { role?: string }).role;
|
|
298
|
+
if (role === "branchSummary" || role === "compactionSummary") return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (let index = 0; index < previous.messageHashes.length; index += 1) {
|
|
302
|
+
if (current.messageHashes[index] !== previous.messageHashes[index]) return true;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function buildCursorIncrementalPrompt(context: Context, options: CursorPromptOptions = {}): CursorPrompt {
|
|
308
|
+
// Incremental sends omit the full Cursor SDK tool boundary block; the session agent retains prior bootstrap context.
|
|
309
|
+
const messages = normalizePiContextMessages(context.messages);
|
|
310
|
+
const latestUserMessageIndex = getLatestUserMessageIndex(messages);
|
|
311
|
+
const latestUserMessage = latestUserMessageIndex >= 0 ? messages[latestUserMessageIndex] : undefined;
|
|
312
|
+
const latestUserText = latestUserMessage ? formatMessage(latestUserMessage) : undefined;
|
|
313
|
+
const sectionsBeforeMessages = [
|
|
314
|
+
"Continue the conversation using Cursor SDK capabilities only. Do not list, promise, or call pi-only tools from earlier context as if they were available.",
|
|
315
|
+
];
|
|
316
|
+
if (context.systemPrompt) {
|
|
317
|
+
sectionsBeforeMessages.push(`System instructions from pi:\n${sanitizeSystemPromptForCursor(context.systemPrompt)}`);
|
|
318
|
+
}
|
|
319
|
+
const latestUserMessageSections =
|
|
320
|
+
latestUserText && latestUserMessageIndex >= 0 ? [{ index: latestUserMessageIndex, text: latestUserText }] : [];
|
|
321
|
+
const images = extractLatestImages(messages);
|
|
322
|
+
const imageTokenReserve = images.length * (options.imageTokenEstimate ?? 0);
|
|
323
|
+
const budgetOptions =
|
|
324
|
+
options.maxInputTokens === undefined
|
|
325
|
+
? options
|
|
326
|
+
: { ...options, maxInputTokens: Math.max(1, options.maxInputTokens - imageTokenReserve) };
|
|
327
|
+
const parts = applyPromptBudget(sectionsBeforeMessages, latestUserMessageSections, [], latestUserMessageIndex, budgetOptions);
|
|
328
|
+
return { text: parts.join(SECTION_SEPARATOR), images };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function buildCursorSendPrompt(
|
|
332
|
+
context: Context,
|
|
333
|
+
options: CursorPromptOptions,
|
|
334
|
+
sendState: { bootstrapped: boolean; contextFingerprint: string },
|
|
335
|
+
): { prompt: CursorPrompt; bootstrap: boolean } {
|
|
336
|
+
const bootstrap = shouldBootstrapCursorSend(sendState, context);
|
|
337
|
+
if (bootstrap) {
|
|
338
|
+
return { prompt: buildCursorPrompt(context, options), bootstrap: true };
|
|
339
|
+
}
|
|
340
|
+
return { prompt: buildCursorIncrementalPrompt(context, options), bootstrap: false };
|
|
341
|
+
}
|
|
342
|
+
|
|
170
343
|
export function buildCursorPrompt(context: Context, options: CursorPromptOptions = {}): CursorPrompt {
|
|
171
344
|
const sectionsBeforeMessages: string[] = [
|
|
172
345
|
[
|
|
173
346
|
"Cursor SDK tool boundary:",
|
|
174
347
|
"You can call only tools actually exposed by Cursor SDK in this run. Pi tool names, replay tool names, and transcript tool names are context only, not callable capabilities.",
|
|
348
|
+
getCursorPiBridgeContractText(),
|
|
175
349
|
"If asked to list or exercise available tools, list and exercise Cursor SDK tools only; do not claim access to pi-side tools from the system prompt unless Cursor exposes an equivalent tool that runs.",
|
|
176
350
|
"Use pi__cursor_ask_question for material choices if exposed.",
|
|
177
351
|
"Web: use Cursor web/search/browser/MCP or say web search is not configured; do not claim WebSearch/WebFetch unless Cursor executes them.",
|
|
@@ -184,7 +358,8 @@ export function buildCursorPrompt(context: Context, options: CursorPromptOptions
|
|
|
184
358
|
sectionsBeforeMessages.push(`System instructions from pi:\n${sanitizeSystemPromptForCursor(context.systemPrompt)}`);
|
|
185
359
|
}
|
|
186
360
|
|
|
187
|
-
const
|
|
361
|
+
const messages = normalizePiContextMessages(context.messages);
|
|
362
|
+
const messageSections = messages
|
|
188
363
|
.map((msg, index) => {
|
|
189
364
|
const text = formatMessage(msg);
|
|
190
365
|
return text ? { index, text } : undefined;
|
|
@@ -196,7 +371,7 @@ export function buildCursorPrompt(context: Context, options: CursorPromptOptions
|
|
|
196
371
|
"If web research is requested, do not claim it unless a Cursor web/search/browser/MCP tool ran.",
|
|
197
372
|
].join("\n"),
|
|
198
373
|
];
|
|
199
|
-
const images = extractLatestImages(
|
|
374
|
+
const images = extractLatestImages(messages);
|
|
200
375
|
const imageTokenReserve = images.length * (options.imageTokenEstimate ?? 0);
|
|
201
376
|
const budgetOptions =
|
|
202
377
|
options.maxInputTokens === undefined
|
|
@@ -206,7 +381,7 @@ export function buildCursorPrompt(context: Context, options: CursorPromptOptions
|
|
|
206
381
|
sectionsBeforeMessages,
|
|
207
382
|
messageSections,
|
|
208
383
|
sectionsAfterMessages,
|
|
209
|
-
getLatestUserMessageIndex(
|
|
384
|
+
getLatestUserMessageIndex(messages),
|
|
210
385
|
budgetOptions,
|
|
211
386
|
);
|
|
212
387
|
const text = parts.join(SECTION_SEPARATOR);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX = "pi__";
|
|
2
|
+
|
|
3
|
+
const CURSOR_PI_BRIDGE_CONTRACT_LINES = [
|
|
4
|
+
"Pi bridge contract:",
|
|
5
|
+
`${CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX}* names are live Cursor MCP bridge tool names only when exposed in the current run.`,
|
|
6
|
+
`Call the ${CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX}* MCP tool name, not the real pi tool name shown in pi history or transcripts.`,
|
|
7
|
+
"Bridged calls execute through normal pi tool flow, so pi shows the real pi tool name and returns a normal pi tool result.",
|
|
8
|
+
"Replay IDs, replay labels, and transcript tool names are display-only/context-only, not callable tools.",
|
|
9
|
+
"Cursor-native host tools, settings, plugins, and configured MCP servers are separate from the pi bridge.",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export function getCursorPiBridgeContractText(): string {
|
|
13
|
+
return CURSOR_PI_BRIDGE_CONTRACT_LINES.join("\n");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildCursorPiBridgeMcpToolDescription(options: {
|
|
17
|
+
piToolName: string;
|
|
18
|
+
mcpToolName: string;
|
|
19
|
+
piToolDescription: string;
|
|
20
|
+
}): string {
|
|
21
|
+
return [
|
|
22
|
+
options.piToolDescription,
|
|
23
|
+
"",
|
|
24
|
+
getCursorPiBridgeContractText(),
|
|
25
|
+
`This run exposes real pi tool ${options.piToolName} as Cursor MCP tool ${options.mcpToolName}.`,
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Context, Message, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { CURSOR_APPROX_CHARS_PER_TOKEN, estimateCursorPromptMessageTokens } from "./context.js";
|
|
3
|
+
|
|
4
|
+
export interface CursorLiveRunAccountingState {
|
|
5
|
+
promptInputTokens: number;
|
|
6
|
+
promptInputTokensReported: boolean;
|
|
7
|
+
consumedToolResultIds: ReadonlySet<string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CursorLiveToolResultConsumption {
|
|
11
|
+
state: CursorLiveRunAccountingState;
|
|
12
|
+
toolResults: ToolResultMessage[];
|
|
13
|
+
toolResultInputTokens: number;
|
|
14
|
+
toolCallIds: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createCursorLiveRunAccountingState(promptInputTokens: number): CursorLiveRunAccountingState {
|
|
18
|
+
return {
|
|
19
|
+
promptInputTokens,
|
|
20
|
+
promptInputTokensReported: false,
|
|
21
|
+
consumedToolResultIds: new Set(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function asToolResultMessage(message: Message): ToolResultMessage | undefined {
|
|
26
|
+
return message.role === "toolResult" ? message : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function consumeCursorLiveToolResults(
|
|
30
|
+
state: CursorLiveRunAccountingState,
|
|
31
|
+
context: Context,
|
|
32
|
+
isMatchingToolResult: (toolResult: ToolResultMessage) => boolean,
|
|
33
|
+
): CursorLiveToolResultConsumption {
|
|
34
|
+
const consumedToolResultIds = new Set(state.consumedToolResultIds);
|
|
35
|
+
const toolResults: ToolResultMessage[] = [];
|
|
36
|
+
let toolResultInputTokens = 0;
|
|
37
|
+
|
|
38
|
+
for (const message of context.messages) {
|
|
39
|
+
const toolResult = asToolResultMessage(message);
|
|
40
|
+
if (!toolResult) continue;
|
|
41
|
+
if (consumedToolResultIds.has(toolResult.toolCallId)) continue;
|
|
42
|
+
if (!isMatchingToolResult(toolResult)) continue;
|
|
43
|
+
consumedToolResultIds.add(toolResult.toolCallId);
|
|
44
|
+
toolResults.push(toolResult);
|
|
45
|
+
toolResultInputTokens += estimateCursorPromptMessageTokens(toolResult, { charsPerToken: CURSOR_APPROX_CHARS_PER_TOKEN });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
state: { ...state, consumedToolResultIds },
|
|
50
|
+
toolResults,
|
|
51
|
+
toolResultInputTokens,
|
|
52
|
+
toolCallIds: toolResults.map((toolResult) => toolResult.toolCallId),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function takeCursorLiveTurnInputTokens(
|
|
57
|
+
state: CursorLiveRunAccountingState,
|
|
58
|
+
toolResultInputTokens: number,
|
|
59
|
+
): { state: CursorLiveRunAccountingState; sessionInputTokens: number } {
|
|
60
|
+
const promptInputTokens = state.promptInputTokensReported ? 0 : state.promptInputTokens;
|
|
61
|
+
return {
|
|
62
|
+
state: state.promptInputTokensReported ? state : { ...state, promptInputTokensReported: true },
|
|
63
|
+
sessionInputTokens: promptInputTokens + toolResultInputTokens,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
highlightCode,
|
|
13
13
|
type ExtensionAPI,
|
|
14
14
|
type ExtensionContext,
|
|
15
|
+
type ExtensionHandler,
|
|
16
|
+
type SessionStartEvent,
|
|
15
17
|
type ToolDefinition,
|
|
16
18
|
} from "@earendil-works/pi-coding-agent";
|
|
17
19
|
import { Image, Text, type Component } from "@earendil-works/pi-tui";
|
|
@@ -45,6 +47,13 @@ export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
|
|
|
45
47
|
const registeredNativeToolNames = new Set<NativeCursorToolName>();
|
|
46
48
|
const nativeToolResults = new Map<string, CursorNativeToolDisplayItem>();
|
|
47
49
|
|
|
50
|
+
type CursorNativeToolRegistryApi = Pick<ExtensionAPI, "getActiveTools" | "getAllTools" | "registerTool" | "setActiveTools">;
|
|
51
|
+
|
|
52
|
+
interface CursorNativeToolDisplayExtensionApi extends CursorNativeToolRegistryApi {
|
|
53
|
+
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
54
|
+
on(event: "model_select", handler: (event: { model: ExtensionContext["model"] }, ctx: ExtensionContext) => Promise<void> | void): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
function readBooleanEnv(name: string): boolean | undefined {
|
|
49
58
|
const value = process.env[name]?.trim().toLowerCase();
|
|
50
59
|
if (value === "1" || value === "true" || value === "yes" || value === "on") return true;
|
|
@@ -566,12 +575,12 @@ function createNativeCursorToolDefinition(toolName: NativeCursorToolName, cwd: s
|
|
|
566
575
|
throw new Error(`Unsupported Cursor native replay tool: ${toolName}`);
|
|
567
576
|
}
|
|
568
577
|
|
|
569
|
-
function registerNativeCursorTool(pi: ExtensionAPI, toolName: NativeCursorToolName): void {
|
|
578
|
+
function registerNativeCursorTool(pi: Pick<ExtensionAPI, "registerTool">, toolName: NativeCursorToolName): void {
|
|
570
579
|
const definition = createNativeCursorToolDefinition(toolName, getCursorSessionCwd());
|
|
571
580
|
pi.registerTool(wrapNativeCursorTool(definition, () => createNativeCursorToolDefinition(toolName, getCursorSessionCwd())));
|
|
572
581
|
}
|
|
573
582
|
|
|
574
|
-
function hasNonBuiltinTool(pi: ExtensionAPI, toolName: NativeCursorToolName): boolean {
|
|
583
|
+
function hasNonBuiltinTool(pi: Pick<ExtensionAPI, "getAllTools">, toolName: NativeCursorToolName): boolean {
|
|
575
584
|
const existingTool = pi.getAllTools().find((tool) => tool.name === toolName);
|
|
576
585
|
return existingTool !== undefined && existingTool.sourceInfo.source !== "builtin";
|
|
577
586
|
}
|
|
@@ -582,7 +591,7 @@ function isCursorModel(model: ExtensionContext["model"]): boolean {
|
|
|
582
591
|
return model?.provider === "cursor" || model?.api === "cursor-sdk";
|
|
583
592
|
}
|
|
584
593
|
|
|
585
|
-
function syncRegisteredNativeCursorToolsForModel(pi: ExtensionAPI, model: ExtensionContext["model"]): void {
|
|
594
|
+
function syncRegisteredNativeCursorToolsForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
|
|
586
595
|
if (registeredNativeToolNames.size === 0) return;
|
|
587
596
|
const activeToolNames = new Set(pi.getActiveTools());
|
|
588
597
|
let changed = false;
|
|
@@ -602,7 +611,7 @@ function syncRegisteredNativeCursorToolsForModel(pi: ExtensionAPI, model: Extens
|
|
|
602
611
|
if (changed) pi.setActiveTools([...activeToolNames]);
|
|
603
612
|
}
|
|
604
613
|
|
|
605
|
-
function registerAvailableNativeCursorTools(pi:
|
|
614
|
+
function registerAvailableNativeCursorTools(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
|
|
606
615
|
if (!isCursorNativeToolRegistrationRequested()) {
|
|
607
616
|
registeredNativeToolNames.clear();
|
|
608
617
|
return;
|
|
@@ -629,7 +638,7 @@ function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: NativeRegistr
|
|
|
629
638
|
}
|
|
630
639
|
}
|
|
631
640
|
|
|
632
|
-
export function registerCursorNativeToolDisplay(pi:
|
|
641
|
+
export function registerCursorNativeToolDisplay(pi: CursorNativeToolDisplayExtensionApi): void {
|
|
633
642
|
pi.on("session_start", (_event, ctx) => {
|
|
634
643
|
registerAvailableNativeCursorTools(pi, ctx);
|
|
635
644
|
});
|