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
|
@@ -2,8 +2,16 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { createServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
3
3
|
import type { AddressInfo } from "node:net";
|
|
4
4
|
import type { McpServerConfig } from "@cursor/sdk";
|
|
5
|
-
import type { Context } from "@earendil-works/pi-ai";
|
|
6
|
-
import type {
|
|
5
|
+
import type { Context, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
6
|
+
import type {
|
|
7
|
+
ExtensionAPI,
|
|
8
|
+
ExtensionHandler,
|
|
9
|
+
SessionShutdownEvent,
|
|
10
|
+
ToolCallEvent,
|
|
11
|
+
ToolCallEventResult,
|
|
12
|
+
ToolInfo,
|
|
13
|
+
ToolResultEvent,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
15
|
import { Server as McpProtocolServer } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
16
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
17
|
import {
|
|
@@ -12,10 +20,13 @@ import {
|
|
|
12
20
|
type CallToolResult,
|
|
13
21
|
type Tool,
|
|
14
22
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
import { buildCursorPiBridgeMcpToolDescription, CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX } from "./cursor-bridge-contract.js";
|
|
15
24
|
import { isExcludedFromCursorBridgeExposure } from "./cursor-tool-names.js";
|
|
16
25
|
|
|
17
26
|
const CURSOR_PI_TOOL_BRIDGE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE";
|
|
18
27
|
const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
|
|
28
|
+
const CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_DEBUG";
|
|
29
|
+
const CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX = "[pi-cursor-sdk:bridge]";
|
|
19
30
|
const LOOPBACK_HOST = "127.0.0.1";
|
|
20
31
|
const MCP_SERVER_NAME = "pi_tools";
|
|
21
32
|
const MCP_ENDPOINT_ROOT = "/cursor-pi-tool-bridge";
|
|
@@ -25,6 +36,74 @@ const DISABLED_ENV_VALUES = new Set(["0", "false", "off", "none", "no", "disable
|
|
|
25
36
|
const ENABLED_ENV_VALUES = new Set(["1", "true", "on", "yes", "enabled"]);
|
|
26
37
|
const OVERLAPPING_CURSOR_NATIVE_PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "write", "edit", "grep", "find", "ls"]);
|
|
27
38
|
|
|
39
|
+
type CursorPiToolBridgeSkippedReason = "disabled" | "no_exposed_tools";
|
|
40
|
+
type CursorPiToolBridgeRejectionKind = "cancelled" | "error";
|
|
41
|
+
|
|
42
|
+
interface CursorPiToolBridgeLifecycleDiagnosticFields {
|
|
43
|
+
runId: string;
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
exposedToolCount: number;
|
|
46
|
+
pendingCount: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface CursorPiToolBridgeRunCreatedDiagnostic extends CursorPiToolBridgeLifecycleDiagnosticFields {
|
|
50
|
+
event: "run_created";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface CursorPiToolBridgeRunSkippedDiagnostic extends CursorPiToolBridgeLifecycleDiagnosticFields {
|
|
54
|
+
event: "run_skipped";
|
|
55
|
+
reason: CursorPiToolBridgeSkippedReason;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface CursorPiToolBridgeToolsExposedDiagnostic extends CursorPiToolBridgeLifecycleDiagnosticFields {
|
|
59
|
+
event: "tools_exposed";
|
|
60
|
+
pairs: Array<{ piToolName: string; mcpToolName: string }>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface CursorPiToolBridgeRunCancelledDiagnostic extends CursorPiToolBridgeLifecycleDiagnosticFields {
|
|
64
|
+
event: "run_cancelled";
|
|
65
|
+
queuedCount: number;
|
|
66
|
+
cancelledRequestCount: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface CursorPiToolBridgeRunDisposedDiagnostic extends CursorPiToolBridgeLifecycleDiagnosticFields {
|
|
70
|
+
event: "run_disposed";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface CursorPiToolBridgeRequestDiagnosticFields {
|
|
74
|
+
runId: string;
|
|
75
|
+
bridgeCallId: string;
|
|
76
|
+
cursorMcpCallId?: string;
|
|
77
|
+
piToolCallId: string;
|
|
78
|
+
mcpToolName: string;
|
|
79
|
+
piToolName: string;
|
|
80
|
+
pendingCount: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface CursorPiToolBridgeRequestQueuedDiagnostic extends CursorPiToolBridgeRequestDiagnosticFields {
|
|
84
|
+
event: "request_queued";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface CursorPiToolBridgeRequestResolvedDiagnostic extends CursorPiToolBridgeRequestDiagnosticFields {
|
|
88
|
+
event: "request_resolved";
|
|
89
|
+
isError: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface CursorPiToolBridgeRequestRejectedDiagnostic extends CursorPiToolBridgeRequestDiagnosticFields {
|
|
93
|
+
event: "request_rejected";
|
|
94
|
+
rejectionKind: CursorPiToolBridgeRejectionKind;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type CursorPiToolBridgeDiagnosticEvent =
|
|
98
|
+
| CursorPiToolBridgeRunCreatedDiagnostic
|
|
99
|
+
| CursorPiToolBridgeRunSkippedDiagnostic
|
|
100
|
+
| CursorPiToolBridgeToolsExposedDiagnostic
|
|
101
|
+
| CursorPiToolBridgeRunCancelledDiagnostic
|
|
102
|
+
| CursorPiToolBridgeRunDisposedDiagnostic
|
|
103
|
+
| CursorPiToolBridgeRequestQueuedDiagnostic
|
|
104
|
+
| CursorPiToolBridgeRequestResolvedDiagnostic
|
|
105
|
+
| CursorPiToolBridgeRequestRejectedDiagnostic;
|
|
106
|
+
|
|
28
107
|
export interface CursorPiMcpInputSchema {
|
|
29
108
|
type: "object";
|
|
30
109
|
properties?: Record<string, object>;
|
|
@@ -66,23 +145,34 @@ export interface CursorPiToolBridgeRun {
|
|
|
66
145
|
mcpServers?: Record<string, McpServerConfig>;
|
|
67
146
|
snapshot: CursorPiToolBridgeSnapshot;
|
|
68
147
|
takeQueuedToolRequests(): CursorPiBridgeToolRequest[];
|
|
148
|
+
resolveToolResults(toolResults: readonly ToolResultMessage[]): void;
|
|
69
149
|
resolveToolResultsFromContext(context: Context): void;
|
|
70
150
|
hasPendingPiToolCallId(piToolCallId: string): boolean;
|
|
71
151
|
isBridgeMcpToolCall(toolCall: unknown): boolean;
|
|
152
|
+
setOnToolRequest(handler?: (request: CursorPiBridgeToolRequest) => void): void;
|
|
72
153
|
cancel(reason: string): void;
|
|
73
154
|
dispose(): Promise<void>;
|
|
74
155
|
}
|
|
75
156
|
|
|
76
|
-
export interface CursorPiToolBridgeRunOptions {
|
|
77
|
-
onToolRequest?: (request: CursorPiBridgeToolRequest) => void;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
157
|
export interface CursorPiToolBridge {
|
|
81
158
|
isEnabled(): boolean;
|
|
159
|
+
getToolSurfaceSignature(): string;
|
|
82
160
|
createRun(options?: CursorPiToolBridgeRunOptions): Promise<CursorPiToolBridgeRun>;
|
|
83
161
|
disposeAll(reason?: string): Promise<void>;
|
|
84
162
|
}
|
|
85
163
|
|
|
164
|
+
export interface CursorPiToolBridgeRunOptions {
|
|
165
|
+
onToolRequest?: (request: CursorPiBridgeToolRequest) => void;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
type CursorPiToolBridgeSnapshotApi = Pick<ExtensionAPI, "getActiveTools" | "getAllTools">;
|
|
169
|
+
|
|
170
|
+
interface CursorPiToolBridgeExtensionApi extends CursorPiToolBridgeSnapshotApi {
|
|
171
|
+
on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
|
|
172
|
+
on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent>): void;
|
|
173
|
+
on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
|
|
174
|
+
}
|
|
175
|
+
|
|
86
176
|
interface PendingBridgeCall {
|
|
87
177
|
request: CursorPiBridgeToolRequest;
|
|
88
178
|
resolve: (result: CallToolResult) => void;
|
|
@@ -92,6 +182,140 @@ interface PendingBridgeCall {
|
|
|
92
182
|
settled: boolean;
|
|
93
183
|
}
|
|
94
184
|
|
|
185
|
+
interface CursorPiToolBridgeActiveToolExecution {
|
|
186
|
+
toolCallId: string;
|
|
187
|
+
abort: () => Promise<void> | void;
|
|
188
|
+
cancelPending: (reason: string) => void;
|
|
189
|
+
signal?: AbortSignal;
|
|
190
|
+
onAbort?: () => void;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
class CursorPiToolBridgeToolExecutionAbortTracker {
|
|
194
|
+
private readonly activeExecutions = new Map<string, CursorPiToolBridgeActiveToolExecution>();
|
|
195
|
+
private processSignalHandlersInstalled = false;
|
|
196
|
+
|
|
197
|
+
track(
|
|
198
|
+
toolCallId: string,
|
|
199
|
+
options: {
|
|
200
|
+
signal?: AbortSignal;
|
|
201
|
+
abort: () => Promise<void> | void;
|
|
202
|
+
cancelPending: (reason: string) => void;
|
|
203
|
+
},
|
|
204
|
+
): boolean {
|
|
205
|
+
this.finish(toolCallId);
|
|
206
|
+
const execution: CursorPiToolBridgeActiveToolExecution = {
|
|
207
|
+
toolCallId,
|
|
208
|
+
abort: options.abort,
|
|
209
|
+
cancelPending: options.cancelPending,
|
|
210
|
+
signal: options.signal,
|
|
211
|
+
};
|
|
212
|
+
if (options.signal?.aborted) {
|
|
213
|
+
this.cancelExecution(execution, "Cursor pi bridge tool execution was already aborted");
|
|
214
|
+
this.abortExecution(execution);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
execution.onAbort = () => {
|
|
219
|
+
this.cancelExecution(execution, "Cursor pi bridge tool execution was aborted");
|
|
220
|
+
this.finish(toolCallId);
|
|
221
|
+
};
|
|
222
|
+
execution.signal?.addEventListener("abort", execution.onAbort, { once: true });
|
|
223
|
+
this.activeExecutions.set(toolCallId, execution);
|
|
224
|
+
this.installProcessSignalHandlers();
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
finish(toolCallId: string): void {
|
|
229
|
+
const execution = this.activeExecutions.get(toolCallId);
|
|
230
|
+
if (!execution) return;
|
|
231
|
+
if (execution.onAbort) execution.signal?.removeEventListener("abort", execution.onAbort);
|
|
232
|
+
this.activeExecutions.delete(toolCallId);
|
|
233
|
+
this.uninstallProcessSignalHandlersIfIdle();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
finishAll(): void {
|
|
237
|
+
for (const toolCallId of [...this.activeExecutions.keys()]) this.finish(toolCallId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
abortAll(reason: string): void {
|
|
241
|
+
for (const execution of [...this.activeExecutions.values()]) {
|
|
242
|
+
this.cancelExecution(execution, reason);
|
|
243
|
+
this.abortExecution(execution);
|
|
244
|
+
this.finish(execution.toolCallId);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
getActiveCount(): number {
|
|
249
|
+
return this.activeExecutions.size;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
emitProcessAbortSignalForTests(signal: NodeJS.Signals): void {
|
|
253
|
+
this.abortActiveExecutions(signal, { preserveProcessSignalBehavior: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private readonly handleSigint = (): void => {
|
|
257
|
+
this.abortActiveExecutions("SIGINT");
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
private readonly handleSigterm = (): void => {
|
|
261
|
+
this.abortActiveExecutions("SIGTERM");
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
private installProcessSignalHandlers(): void {
|
|
265
|
+
if (this.processSignalHandlersInstalled) return;
|
|
266
|
+
this.processSignalHandlersInstalled = true;
|
|
267
|
+
process.on("SIGINT", this.handleSigint);
|
|
268
|
+
process.on("SIGTERM", this.handleSigterm);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private uninstallProcessSignalHandlersIfIdle(): void {
|
|
272
|
+
if (!this.processSignalHandlersInstalled || this.activeExecutions.size > 0) return;
|
|
273
|
+
this.processSignalHandlersInstalled = false;
|
|
274
|
+
process.off("SIGINT", this.handleSigint);
|
|
275
|
+
process.off("SIGTERM", this.handleSigterm);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private abortActiveExecutions(
|
|
279
|
+
signal: NodeJS.Signals,
|
|
280
|
+
options: { preserveProcessSignalBehavior?: boolean } = {},
|
|
281
|
+
): void {
|
|
282
|
+
if (this.activeExecutions.size === 0) return;
|
|
283
|
+
const shouldRestoreDefaultSignalBehavior =
|
|
284
|
+
options.preserveProcessSignalBehavior !== true && !this.hasExternalProcessSignalListeners(signal);
|
|
285
|
+
this.abortAll(`Cursor pi bridge tool execution interrupted by ${signal}`);
|
|
286
|
+
if (shouldRestoreDefaultSignalBehavior) this.restoreDefaultProcessSignalBehavior(signal);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private cancelExecution(execution: CursorPiToolBridgeActiveToolExecution, reason: string): void {
|
|
290
|
+
try {
|
|
291
|
+
execution.cancelPending(reason);
|
|
292
|
+
} catch {
|
|
293
|
+
// Cancellation is best-effort during process abort/shutdown cleanup; keep aborting siblings.
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private abortExecution(execution: CursorPiToolBridgeActiveToolExecution): void {
|
|
298
|
+
try {
|
|
299
|
+
Promise.resolve(execution.abort()).catch(() => undefined);
|
|
300
|
+
} catch {
|
|
301
|
+
// Abort is best-effort during process abort/shutdown cleanup; keep aborting siblings.
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private hasExternalProcessSignalListeners(signal: NodeJS.Signals): boolean {
|
|
306
|
+
const ownHandler = signal === "SIGINT" ? this.handleSigint : this.handleSigterm;
|
|
307
|
+
return process.listeners(signal).some((listener) => listener !== ownHandler);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private restoreDefaultProcessSignalBehavior(signal: NodeJS.Signals): void {
|
|
311
|
+
setImmediate(() => {
|
|
312
|
+
process.kill(process.pid, signal);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const bridgeToolExecutionAbortTracker = new CursorPiToolBridgeToolExecutionAbortTracker();
|
|
318
|
+
|
|
95
319
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
96
320
|
return typeof value === "object" && value !== null;
|
|
97
321
|
}
|
|
@@ -121,8 +345,12 @@ function stableNameHash(value: string): string {
|
|
|
121
345
|
return createHash("sha256").update(value).digest("hex").slice(0, 8);
|
|
122
346
|
}
|
|
123
347
|
|
|
348
|
+
function createCursorMcpCallDiagnosticId(cursorMcpCallId: string | undefined): string | undefined {
|
|
349
|
+
return cursorMcpCallId ? `cursor-mcp-call-${stableNameHash(cursorMcpCallId)}` : undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
124
352
|
function createMcpToolName(piToolName: string, usedMcpToolNames: Set<string>): string {
|
|
125
|
-
const baseName =
|
|
353
|
+
const baseName = `${CURSOR_PI_BRIDGE_MCP_TOOL_PREFIX}${sanitizeMcpToolNameStem(piToolName)}`;
|
|
126
354
|
if (!usedMcpToolNames.has(baseName)) {
|
|
127
355
|
usedMcpToolNames.add(baseName);
|
|
128
356
|
return baseName;
|
|
@@ -168,12 +396,137 @@ export function resolveCursorPiToolBridgeBuiltinsEnabled(env: Record<string, str
|
|
|
168
396
|
return false;
|
|
169
397
|
}
|
|
170
398
|
|
|
399
|
+
export function resolveCursorPiToolBridgeDebugEnabled(env: Record<string, string | undefined> = process.env): boolean {
|
|
400
|
+
const raw = env[CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV]?.trim().toLowerCase();
|
|
401
|
+
if (!raw) return false;
|
|
402
|
+
if (ENABLED_ENV_VALUES.has(raw)) return true;
|
|
403
|
+
if (DISABLED_ENV_VALUES.has(raw)) return false;
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function assertNeverDiagnosticEvent(_event: never): never {
|
|
408
|
+
throw new Error("Unhandled Cursor pi tool bridge diagnostic event");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function serializeCursorPiToolBridgeDiagnostic(event: CursorPiToolBridgeDiagnosticEvent): Record<string, unknown> {
|
|
412
|
+
switch (event.event) {
|
|
413
|
+
case "run_created":
|
|
414
|
+
return {
|
|
415
|
+
event: event.event,
|
|
416
|
+
runId: event.runId,
|
|
417
|
+
enabled: event.enabled,
|
|
418
|
+
exposedToolCount: event.exposedToolCount,
|
|
419
|
+
pendingCount: event.pendingCount,
|
|
420
|
+
};
|
|
421
|
+
case "run_skipped":
|
|
422
|
+
return {
|
|
423
|
+
event: event.event,
|
|
424
|
+
runId: event.runId,
|
|
425
|
+
enabled: event.enabled,
|
|
426
|
+
exposedToolCount: event.exposedToolCount,
|
|
427
|
+
pendingCount: event.pendingCount,
|
|
428
|
+
reason: event.reason,
|
|
429
|
+
};
|
|
430
|
+
case "tools_exposed":
|
|
431
|
+
return {
|
|
432
|
+
event: event.event,
|
|
433
|
+
runId: event.runId,
|
|
434
|
+
enabled: event.enabled,
|
|
435
|
+
exposedToolCount: event.exposedToolCount,
|
|
436
|
+
pendingCount: event.pendingCount,
|
|
437
|
+
pairs: event.pairs.map((pair) => ({ piToolName: pair.piToolName, mcpToolName: pair.mcpToolName })),
|
|
438
|
+
};
|
|
439
|
+
case "run_cancelled":
|
|
440
|
+
return {
|
|
441
|
+
event: event.event,
|
|
442
|
+
runId: event.runId,
|
|
443
|
+
enabled: event.enabled,
|
|
444
|
+
exposedToolCount: event.exposedToolCount,
|
|
445
|
+
pendingCount: event.pendingCount,
|
|
446
|
+
queuedCount: event.queuedCount,
|
|
447
|
+
cancelledRequestCount: event.cancelledRequestCount,
|
|
448
|
+
};
|
|
449
|
+
case "run_disposed":
|
|
450
|
+
return {
|
|
451
|
+
event: event.event,
|
|
452
|
+
runId: event.runId,
|
|
453
|
+
enabled: event.enabled,
|
|
454
|
+
exposedToolCount: event.exposedToolCount,
|
|
455
|
+
pendingCount: event.pendingCount,
|
|
456
|
+
};
|
|
457
|
+
case "request_queued":
|
|
458
|
+
return {
|
|
459
|
+
event: event.event,
|
|
460
|
+
runId: event.runId,
|
|
461
|
+
bridgeCallId: event.bridgeCallId,
|
|
462
|
+
cursorMcpCallId: createCursorMcpCallDiagnosticId(event.cursorMcpCallId),
|
|
463
|
+
piToolCallId: event.piToolCallId,
|
|
464
|
+
mcpToolName: event.mcpToolName,
|
|
465
|
+
piToolName: event.piToolName,
|
|
466
|
+
pendingCount: event.pendingCount,
|
|
467
|
+
};
|
|
468
|
+
case "request_resolved":
|
|
469
|
+
return {
|
|
470
|
+
event: event.event,
|
|
471
|
+
runId: event.runId,
|
|
472
|
+
bridgeCallId: event.bridgeCallId,
|
|
473
|
+
cursorMcpCallId: createCursorMcpCallDiagnosticId(event.cursorMcpCallId),
|
|
474
|
+
piToolCallId: event.piToolCallId,
|
|
475
|
+
mcpToolName: event.mcpToolName,
|
|
476
|
+
piToolName: event.piToolName,
|
|
477
|
+
pendingCount: event.pendingCount,
|
|
478
|
+
isError: event.isError,
|
|
479
|
+
};
|
|
480
|
+
case "request_rejected":
|
|
481
|
+
return {
|
|
482
|
+
event: event.event,
|
|
483
|
+
runId: event.runId,
|
|
484
|
+
bridgeCallId: event.bridgeCallId,
|
|
485
|
+
cursorMcpCallId: createCursorMcpCallDiagnosticId(event.cursorMcpCallId),
|
|
486
|
+
piToolCallId: event.piToolCallId,
|
|
487
|
+
mcpToolName: event.mcpToolName,
|
|
488
|
+
piToolName: event.piToolName,
|
|
489
|
+
pendingCount: event.pendingCount,
|
|
490
|
+
rejectionKind: event.rejectionKind,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return assertNeverDiagnosticEvent(event);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function writeCursorPiToolBridgeDiagnostic(env: Record<string, string | undefined>, event: CursorPiToolBridgeDiagnosticEvent): void {
|
|
497
|
+
if (!resolveCursorPiToolBridgeDebugEnabled(env)) return;
|
|
498
|
+
try {
|
|
499
|
+
process.stderr.write(`${CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX} ${JSON.stringify(serializeCursorPiToolBridgeDiagnostic(event))}\n`);
|
|
500
|
+
} catch {
|
|
501
|
+
// Diagnostics must never affect bridge execution.
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
171
505
|
function isOverlappingCursorNativePiToolName(toolName: string): boolean {
|
|
172
506
|
return OVERLAPPING_CURSOR_NATIVE_PI_BUILTIN_TOOL_NAMES.has(toolName);
|
|
173
507
|
}
|
|
174
508
|
|
|
509
|
+
export function buildCursorPiToolBridgeSurfaceSignature(snapshot: CursorPiToolBridgeSnapshot): string {
|
|
510
|
+
if (snapshot.tools.length === 0) return "bridge:empty";
|
|
511
|
+
const serializedTools = snapshot.tools
|
|
512
|
+
.map((tool) =>
|
|
513
|
+
JSON.stringify({
|
|
514
|
+
piToolName: tool.piToolName,
|
|
515
|
+
mcpToolName: tool.mcpToolName,
|
|
516
|
+
description: tool.description,
|
|
517
|
+
inputSchema: tool.inputSchema,
|
|
518
|
+
source: tool.sourceInfo?.source,
|
|
519
|
+
path: tool.sourceInfo?.path,
|
|
520
|
+
scope: tool.sourceInfo?.scope,
|
|
521
|
+
}),
|
|
522
|
+
)
|
|
523
|
+
.sort()
|
|
524
|
+
.join("\0");
|
|
525
|
+
return `bridge:on:${stableNameHash(serializedTools)}`;
|
|
526
|
+
}
|
|
527
|
+
|
|
175
528
|
export function buildCursorPiToolBridgeSnapshot(
|
|
176
|
-
pi:
|
|
529
|
+
pi: CursorPiToolBridgeSnapshotApi,
|
|
177
530
|
options: CursorPiToolBridgeSnapshotOptions = {},
|
|
178
531
|
): CursorPiToolBridgeSnapshot {
|
|
179
532
|
const activeToolNames = new Set(pi.getActiveTools());
|
|
@@ -209,7 +562,11 @@ export function buildCursorPiToolBridgeSnapshot(
|
|
|
209
562
|
function snapshotToolToMcpTool(tool: CursorPiBridgeToolDefinition): Tool {
|
|
210
563
|
return {
|
|
211
564
|
name: tool.mcpToolName,
|
|
212
|
-
description:
|
|
565
|
+
description: buildCursorPiBridgeMcpToolDescription({
|
|
566
|
+
piToolName: tool.piToolName,
|
|
567
|
+
mcpToolName: tool.mcpToolName,
|
|
568
|
+
piToolDescription: tool.description,
|
|
569
|
+
}),
|
|
213
570
|
inputSchema: tool.inputSchema,
|
|
214
571
|
_meta: { piToolName: tool.piToolName },
|
|
215
572
|
};
|
|
@@ -237,7 +594,7 @@ function convertPiContentToMcpContent(content: unknown): CallToolResult["content
|
|
|
237
594
|
return mcpContent.length > 0 ? mcpContent : [{ type: "text", text: "" }];
|
|
238
595
|
}
|
|
239
596
|
|
|
240
|
-
function asToolResultMessage(value: Context["messages"][number]):
|
|
597
|
+
function asToolResultMessage(value: Context["messages"][number]): ToolResultMessage | undefined {
|
|
241
598
|
return value.role === "toolResult" ? value : undefined;
|
|
242
599
|
}
|
|
243
600
|
|
|
@@ -273,6 +630,7 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
273
630
|
mcpServers?: Record<string, McpServerConfig>;
|
|
274
631
|
|
|
275
632
|
private readonly registry: CursorPiToolBridgeRegistry;
|
|
633
|
+
private readonly env: Record<string, string | undefined>;
|
|
276
634
|
private readonly endpointPath: string;
|
|
277
635
|
private readonly knownMcpToolNames: ReadonlySet<string>;
|
|
278
636
|
private readonly knownCursorMcpCallIds = new Set<string>();
|
|
@@ -280,7 +638,8 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
280
638
|
private readonly pendingByPiToolCallId = new Map<string, PendingBridgeCall>();
|
|
281
639
|
private readonly pendingByBridgeCallId = new Map<string, PendingBridgeCall>();
|
|
282
640
|
private readonly pendingByCursorMcpCallId = new Map<string, PendingBridgeCall>();
|
|
283
|
-
private
|
|
641
|
+
private onToolRequest?: (request: CursorPiBridgeToolRequest) => void;
|
|
642
|
+
private liveRunHandlerDetached = false;
|
|
284
643
|
private mcpServer?: McpProtocolServer;
|
|
285
644
|
private mcpTransport?: StreamableHTTPServerTransport;
|
|
286
645
|
private toolCallCounter = 0;
|
|
@@ -288,16 +647,18 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
288
647
|
|
|
289
648
|
constructor(
|
|
290
649
|
registry: CursorPiToolBridgeRegistry,
|
|
650
|
+
env: Record<string, string | undefined>,
|
|
291
651
|
snapshot: CursorPiToolBridgeSnapshot,
|
|
292
652
|
enabled: boolean,
|
|
293
653
|
options: CursorPiToolBridgeRunOptions = {},
|
|
294
654
|
) {
|
|
295
655
|
this.registry = registry;
|
|
656
|
+
this.env = env;
|
|
296
657
|
this.snapshot = snapshot;
|
|
297
658
|
this.enabled = enabled;
|
|
298
659
|
this.onToolRequest = options.onToolRequest;
|
|
299
|
-
this.id = `cursor-pi-bridge-${randomUUID()}`;
|
|
300
|
-
this.endpointPath = `${MCP_ENDPOINT_ROOT}/${
|
|
660
|
+
this.id = `cursor-pi-bridge-run-${randomUUID()}`;
|
|
661
|
+
this.endpointPath = `${MCP_ENDPOINT_ROOT}/${randomUUID()}/mcp`;
|
|
301
662
|
this.knownMcpToolNames = new Set(snapshot.tools.map((tool) => tool.mcpToolName));
|
|
302
663
|
}
|
|
303
664
|
|
|
@@ -308,6 +669,27 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
308
669
|
this.mcpServers = { [MCP_SERVER_NAME]: { type: "http", url: endpointUrl } };
|
|
309
670
|
}
|
|
310
671
|
|
|
672
|
+
emitStartDiagnostics(bridgeEnabled: boolean): void {
|
|
673
|
+
const base = this.lifecycleDiagnosticFields();
|
|
674
|
+
this.emitDiagnostic({ event: "run_created", ...base });
|
|
675
|
+
if (!this.enabled) {
|
|
676
|
+
this.emitDiagnostic({
|
|
677
|
+
event: "run_skipped",
|
|
678
|
+
...base,
|
|
679
|
+
reason: bridgeEnabled ? "no_exposed_tools" : "disabled",
|
|
680
|
+
});
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this.emitDiagnostic({
|
|
684
|
+
event: "tools_exposed",
|
|
685
|
+
...base,
|
|
686
|
+
pairs: this.snapshot.tools.map((tool) => ({
|
|
687
|
+
piToolName: tool.piToolName,
|
|
688
|
+
mcpToolName: tool.mcpToolName,
|
|
689
|
+
})),
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
311
693
|
async handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
312
694
|
if (this.disposed || !this.mcpTransport) {
|
|
313
695
|
res.writeHead(410, { "content-type": "application/json" }).end(JSON.stringify({ error: "Cursor pi tool bridge run is disposed" }));
|
|
@@ -320,10 +702,23 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
320
702
|
return this.queuedRequests.splice(0);
|
|
321
703
|
}
|
|
322
704
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
705
|
+
setOnToolRequest(handler?: (request: CursorPiBridgeToolRequest) => void): void {
|
|
706
|
+
if (!handler) {
|
|
707
|
+
this.liveRunHandlerDetached = true;
|
|
708
|
+
this.rejectQueuedToolRequestsWithoutHandler("Cursor pi tool bridge has no active live run");
|
|
709
|
+
} else {
|
|
710
|
+
this.liveRunHandlerDetached = false;
|
|
711
|
+
}
|
|
712
|
+
this.onToolRequest = handler;
|
|
713
|
+
if (handler) {
|
|
714
|
+
for (const request of this.queuedRequests.splice(0)) {
|
|
715
|
+
handler(request);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
resolveToolResults(toolResults: readonly ToolResultMessage[]): void {
|
|
721
|
+
for (const toolResult of toolResults) {
|
|
327
722
|
const pending = this.pendingByPiToolCallId.get(toolResult.toolCallId);
|
|
328
723
|
if (!pending || pending.settled) continue;
|
|
329
724
|
this.resolvePending(pending, {
|
|
@@ -333,10 +728,21 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
333
728
|
}
|
|
334
729
|
}
|
|
335
730
|
|
|
731
|
+
resolveToolResultsFromContext(context: Context): void {
|
|
732
|
+
this.resolveToolResults(context.messages.map(asToolResultMessage).filter((message): message is ToolResultMessage => message !== undefined));
|
|
733
|
+
}
|
|
734
|
+
|
|
336
735
|
hasPendingPiToolCallId(piToolCallId: string): boolean {
|
|
337
736
|
return this.pendingByPiToolCallId.has(piToolCallId);
|
|
338
737
|
}
|
|
339
738
|
|
|
739
|
+
cancelPendingPiToolCallId(piToolCallId: string, reason: string): boolean {
|
|
740
|
+
const pending = this.pendingByPiToolCallId.get(piToolCallId);
|
|
741
|
+
if (!pending) return false;
|
|
742
|
+
this.rejectPending(pending, new Error(reason), "cancelled");
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
|
|
340
746
|
isBridgeMcpToolCall(toolCall: unknown): boolean {
|
|
341
747
|
if (!isRecord(toolCall)) return false;
|
|
342
748
|
const toolName = getStringField(toolCall, ["name", "toolName", "mcpToolName"]);
|
|
@@ -352,9 +758,19 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
352
758
|
|
|
353
759
|
cancel(reason: string): void {
|
|
354
760
|
const error = new Error(reason);
|
|
761
|
+
const pendingCount = this.pendingCount();
|
|
762
|
+
const queuedCount = this.queuedRequests.length;
|
|
763
|
+
if (pendingCount > 0 || queuedCount > 0) {
|
|
764
|
+
this.emitDiagnostic({
|
|
765
|
+
event: "run_cancelled",
|
|
766
|
+
...this.lifecycleDiagnosticFields(pendingCount),
|
|
767
|
+
queuedCount,
|
|
768
|
+
cancelledRequestCount: pendingCount,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
355
771
|
this.queuedRequests.splice(0);
|
|
356
772
|
for (const pending of [...this.pendingByBridgeCallId.values()]) {
|
|
357
|
-
this.rejectPending(pending, error);
|
|
773
|
+
this.rejectPending(pending, error, "cancelled");
|
|
358
774
|
}
|
|
359
775
|
}
|
|
360
776
|
|
|
@@ -368,6 +784,10 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
368
784
|
this.mcpServer?.close(),
|
|
369
785
|
]);
|
|
370
786
|
await this.registry.unregisterRun(this.endpointPath, this);
|
|
787
|
+
this.emitDiagnostic({
|
|
788
|
+
event: "run_disposed",
|
|
789
|
+
...this.lifecycleDiagnosticFields(),
|
|
790
|
+
});
|
|
371
791
|
}
|
|
372
792
|
|
|
373
793
|
private async createMcpServer(): Promise<void> {
|
|
@@ -422,7 +842,7 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
422
842
|
settled: false,
|
|
423
843
|
};
|
|
424
844
|
pending.onAbort = () => {
|
|
425
|
-
this.rejectPending(pending, new Error("Cursor MCP bridge tool request was aborted"));
|
|
845
|
+
this.rejectPending(pending, new Error("Cursor MCP bridge tool request was aborted"), "cancelled");
|
|
426
846
|
};
|
|
427
847
|
if (signal?.aborted) {
|
|
428
848
|
pending.onAbort();
|
|
@@ -433,35 +853,97 @@ class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
|
|
|
433
853
|
this.pendingByBridgeCallId.set(request.bridgeCallId, pending);
|
|
434
854
|
this.pendingByCursorMcpCallId.set(cursorMcpCallId, pending);
|
|
435
855
|
this.knownCursorMcpCallIds.add(cursorMcpCallId);
|
|
436
|
-
this.
|
|
437
|
-
|
|
856
|
+
if (!this.onToolRequest) {
|
|
857
|
+
if (this.liveRunHandlerDetached) {
|
|
858
|
+
this.rejectPending(pending, new Error("Cursor pi tool bridge has no active live run"), "cancelled");
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
this.queuedRequests.push(request);
|
|
862
|
+
this.emitRequestQueuedDiagnostic(request);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
this.emitRequestQueuedDiagnostic(request);
|
|
866
|
+
this.onToolRequest(request);
|
|
438
867
|
});
|
|
439
868
|
}
|
|
440
869
|
|
|
870
|
+
private rejectQueuedToolRequestsWithoutHandler(reason: string): void {
|
|
871
|
+
while (this.queuedRequests.length > 0) {
|
|
872
|
+
const request = this.queuedRequests.shift()!;
|
|
873
|
+
const pending = this.pendingByPiToolCallId.get(request.piToolCallId);
|
|
874
|
+
if (pending) this.rejectPending(pending, new Error(reason), "cancelled");
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
441
878
|
private resolvePending(pending: PendingBridgeCall, result: CallToolResult): void {
|
|
442
879
|
if (pending.settled) return;
|
|
443
880
|
pending.settled = true;
|
|
444
881
|
this.removePending(pending);
|
|
882
|
+
this.emitRequestResolvedDiagnostic(pending.request, result.isError === true);
|
|
445
883
|
pending.resolve(result);
|
|
446
884
|
}
|
|
447
885
|
|
|
448
|
-
private rejectPending(pending: PendingBridgeCall, error: Error): void {
|
|
886
|
+
private rejectPending(pending: PendingBridgeCall, error: Error, kind: "cancelled" | "error" = "error"): void {
|
|
449
887
|
if (pending.settled) return;
|
|
450
888
|
pending.settled = true;
|
|
451
889
|
this.removePending(pending);
|
|
890
|
+
this.emitRequestRejectedDiagnostic(pending.request, kind);
|
|
452
891
|
pending.reject(error);
|
|
453
892
|
}
|
|
454
893
|
|
|
894
|
+
private lifecycleDiagnosticFields(pendingCount = this.pendingCount()): CursorPiToolBridgeLifecycleDiagnosticFields {
|
|
895
|
+
return {
|
|
896
|
+
runId: this.id,
|
|
897
|
+
enabled: this.enabled,
|
|
898
|
+
exposedToolCount: this.snapshot.tools.length,
|
|
899
|
+
pendingCount,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private requestDiagnosticFields(request: CursorPiBridgeToolRequest): CursorPiToolBridgeRequestDiagnosticFields {
|
|
904
|
+
return {
|
|
905
|
+
runId: this.id,
|
|
906
|
+
bridgeCallId: request.bridgeCallId,
|
|
907
|
+
cursorMcpCallId: request.cursorMcpCallId,
|
|
908
|
+
piToolCallId: request.piToolCallId,
|
|
909
|
+
mcpToolName: request.mcpToolName,
|
|
910
|
+
piToolName: request.piToolName,
|
|
911
|
+
pendingCount: this.pendingCount(),
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
private emitRequestQueuedDiagnostic(request: CursorPiBridgeToolRequest): void {
|
|
916
|
+
this.emitDiagnostic({ event: "request_queued", ...this.requestDiagnosticFields(request) });
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private emitRequestResolvedDiagnostic(request: CursorPiBridgeToolRequest, isError: boolean): void {
|
|
920
|
+
this.emitDiagnostic({ event: "request_resolved", ...this.requestDiagnosticFields(request), isError });
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
private emitRequestRejectedDiagnostic(request: CursorPiBridgeToolRequest, rejectionKind: CursorPiToolBridgeRejectionKind): void {
|
|
924
|
+
this.emitDiagnostic({ event: "request_rejected", ...this.requestDiagnosticFields(request), rejectionKind });
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private emitDiagnostic(event: CursorPiToolBridgeDiagnosticEvent): void {
|
|
928
|
+
writeCursorPiToolBridgeDiagnostic(this.env, event);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private pendingCount(): number {
|
|
932
|
+
return this.pendingByBridgeCallId.size;
|
|
933
|
+
}
|
|
934
|
+
|
|
455
935
|
private removePending(pending: PendingBridgeCall): void {
|
|
456
936
|
pending.signal?.removeEventListener("abort", pending.onAbort ?? (() => undefined));
|
|
457
937
|
this.pendingByPiToolCallId.delete(pending.request.piToolCallId);
|
|
458
938
|
this.pendingByBridgeCallId.delete(pending.request.bridgeCallId);
|
|
459
939
|
if (pending.request.cursorMcpCallId) this.pendingByCursorMcpCallId.delete(pending.request.cursorMcpCallId);
|
|
940
|
+
const queuedIndex = this.queuedRequests.findIndex((request) => request.bridgeCallId === pending.request.bridgeCallId);
|
|
941
|
+
if (queuedIndex >= 0) this.queuedRequests.splice(queuedIndex, 1);
|
|
460
942
|
}
|
|
461
943
|
}
|
|
462
944
|
|
|
463
945
|
class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
|
|
464
|
-
private readonly pi:
|
|
946
|
+
private readonly pi: CursorPiToolBridgeSnapshotApi;
|
|
465
947
|
private readonly env: Record<string, string | undefined>;
|
|
466
948
|
private readonly runs = new Set<CursorPiToolBridgeRunImpl>();
|
|
467
949
|
private readonly routes = new Map<string, CursorPiToolBridgeRunImpl>();
|
|
@@ -469,7 +951,7 @@ class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
|
|
|
469
951
|
private listenPromise?: Promise<void>;
|
|
470
952
|
|
|
471
953
|
constructor(
|
|
472
|
-
pi:
|
|
954
|
+
pi: CursorPiToolBridgeSnapshotApi,
|
|
473
955
|
env: Record<string, string | undefined> = process.env,
|
|
474
956
|
) {
|
|
475
957
|
this.pi = pi;
|
|
@@ -480,6 +962,14 @@ class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
|
|
|
480
962
|
return resolveCursorPiToolBridgeEnabled(this.env);
|
|
481
963
|
}
|
|
482
964
|
|
|
965
|
+
getToolSurfaceSignature(): string {
|
|
966
|
+
if (!this.isEnabled()) return "bridge:off";
|
|
967
|
+
const snapshot = buildCursorPiToolBridgeSnapshot(this.pi, {
|
|
968
|
+
exposeOverlappingBuiltins: resolveCursorPiToolBridgeBuiltinsEnabled(this.env),
|
|
969
|
+
});
|
|
970
|
+
return buildCursorPiToolBridgeSurfaceSignature(snapshot);
|
|
971
|
+
}
|
|
972
|
+
|
|
483
973
|
async createRun(options: CursorPiToolBridgeRunOptions = {}): Promise<CursorPiToolBridgeRun> {
|
|
484
974
|
const bridgeEnabled = this.isEnabled();
|
|
485
975
|
const snapshot = bridgeEnabled
|
|
@@ -487,9 +977,10 @@ class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
|
|
|
487
977
|
exposeOverlappingBuiltins: resolveCursorPiToolBridgeBuiltinsEnabled(this.env),
|
|
488
978
|
})
|
|
489
979
|
: createEmptySnapshot();
|
|
490
|
-
const run = new CursorPiToolBridgeRunImpl(this, snapshot, bridgeEnabled && snapshot.tools.length > 0, options);
|
|
980
|
+
const run = new CursorPiToolBridgeRunImpl(this, this.env, snapshot, bridgeEnabled && snapshot.tools.length > 0, options);
|
|
491
981
|
this.runs.add(run);
|
|
492
982
|
await run.start();
|
|
983
|
+
run.emitStartDiagnostics(bridgeEnabled);
|
|
493
984
|
return run;
|
|
494
985
|
}
|
|
495
986
|
|
|
@@ -523,6 +1014,20 @@ class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
|
|
|
523
1014
|
return this.routes.size;
|
|
524
1015
|
}
|
|
525
1016
|
|
|
1017
|
+
hasPendingPiToolCallId(piToolCallId: string): boolean {
|
|
1018
|
+
for (const run of this.runs) {
|
|
1019
|
+
if (run.hasPendingPiToolCallId(piToolCallId)) return true;
|
|
1020
|
+
}
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
cancelPendingPiToolCallId(piToolCallId: string, reason: string): boolean {
|
|
1025
|
+
for (const run of this.runs) {
|
|
1026
|
+
if (run.cancelPendingPiToolCallId(piToolCallId, reason)) return true;
|
|
1027
|
+
}
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
526
1031
|
private async ensureHttpServer(): Promise<void> {
|
|
527
1032
|
if (this.httpServer) {
|
|
528
1033
|
await this.listenPromise;
|
|
@@ -601,12 +1106,32 @@ class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
|
|
|
601
1106
|
|
|
602
1107
|
let registeredCursorPiToolBridge: CursorPiToolBridgeRegistry | undefined;
|
|
603
1108
|
|
|
604
|
-
export function registerCursorPiToolBridge(pi:
|
|
1109
|
+
export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi): CursorPiToolBridge {
|
|
1110
|
+
bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge extension reloaded");
|
|
605
1111
|
void registeredCursorPiToolBridge?.disposeAll("Cursor pi tool bridge extension reloaded");
|
|
606
1112
|
const bridge = new CursorPiToolBridgeRegistry(pi);
|
|
607
1113
|
registeredCursorPiToolBridge = bridge;
|
|
1114
|
+
pi.on("tool_call", (event, ctx) => {
|
|
1115
|
+
if (!bridge.hasPendingPiToolCallId(event.toolCallId)) return undefined;
|
|
1116
|
+
const trackingStarted = bridgeToolExecutionAbortTracker.track(event.toolCallId, {
|
|
1117
|
+
signal: ctx.signal,
|
|
1118
|
+
abort: () => {
|
|
1119
|
+
void ctx.abort();
|
|
1120
|
+
},
|
|
1121
|
+
cancelPending: (reason) => {
|
|
1122
|
+
bridge.cancelPendingPiToolCallId(event.toolCallId, reason);
|
|
1123
|
+
},
|
|
1124
|
+
});
|
|
1125
|
+
if (trackingStarted) return undefined;
|
|
1126
|
+
return { block: true, reason: "Cursor pi bridge tool execution was aborted before it started" };
|
|
1127
|
+
});
|
|
1128
|
+
pi.on("tool_result", (event) => {
|
|
1129
|
+
bridgeToolExecutionAbortTracker.finish(event.toolCallId);
|
|
1130
|
+
});
|
|
608
1131
|
pi.on("session_shutdown", async (event) => {
|
|
609
|
-
|
|
1132
|
+
const reason = `Cursor pi tool bridge session shutdown: ${event.reason}`;
|
|
1133
|
+
bridgeToolExecutionAbortTracker.abortAll(reason);
|
|
1134
|
+
await bridge.disposeAll(reason);
|
|
610
1135
|
});
|
|
611
1136
|
return bridge;
|
|
612
1137
|
}
|
|
@@ -618,10 +1143,12 @@ export function getRegisteredCursorPiToolBridge(): CursorPiToolBridge | undefine
|
|
|
618
1143
|
export const __testUtils = {
|
|
619
1144
|
CURSOR_PI_TOOL_BRIDGE_ENV,
|
|
620
1145
|
CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
|
|
1146
|
+
CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
|
|
1147
|
+
CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
|
|
621
1148
|
LOOPBACK_HOST,
|
|
622
1149
|
MCP_SERVER_NAME,
|
|
623
1150
|
createRegistry(
|
|
624
|
-
pi:
|
|
1151
|
+
pi: CursorPiToolBridgeSnapshotApi,
|
|
625
1152
|
env: Record<string, string | undefined> = process.env,
|
|
626
1153
|
) {
|
|
627
1154
|
return new CursorPiToolBridgeRegistry(pi, env);
|
|
@@ -629,7 +1156,17 @@ export const __testUtils = {
|
|
|
629
1156
|
getRegisteredBridgeForTests() {
|
|
630
1157
|
return registeredCursorPiToolBridge;
|
|
631
1158
|
},
|
|
1159
|
+
serializeDiagnosticForTests(event: CursorPiToolBridgeDiagnosticEvent) {
|
|
1160
|
+
return serializeCursorPiToolBridgeDiagnostic(event);
|
|
1161
|
+
},
|
|
1162
|
+
getActiveBridgeToolExecutionAbortCount() {
|
|
1163
|
+
return bridgeToolExecutionAbortTracker.getActiveCount();
|
|
1164
|
+
},
|
|
1165
|
+
emitBridgeToolExecutionProcessAbortSignalForTests(signal: NodeJS.Signals) {
|
|
1166
|
+
bridgeToolExecutionAbortTracker.emitProcessAbortSignalForTests(signal);
|
|
1167
|
+
},
|
|
632
1168
|
resetRegisteredBridgeForTests() {
|
|
1169
|
+
bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge test reset");
|
|
633
1170
|
const bridge = registeredCursorPiToolBridge;
|
|
634
1171
|
registeredCursorPiToolBridge = undefined;
|
|
635
1172
|
return bridge?.disposeAll("Cursor pi tool bridge test reset") ?? Promise.resolve();
|