pi-cursor-sdk 0.5.2 → 0.5.4

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 (114) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/README.md +62 -795
  3. package/dist/context.js +13 -7
  4. package/dist/cursor-agents-context.js +6 -6
  5. package/dist/cursor-api-key.js +1 -1
  6. package/dist/cursor-bridge-contract.js +1 -13
  7. package/dist/cursor-cloud-local-state.js +0 -1
  8. package/dist/cursor-config.js +1 -17
  9. package/dist/cursor-custom-subagent-definitions.js +1 -1
  10. package/dist/cursor-fallback-models.generated.js +665 -1
  11. package/dist/cursor-live-run-coordinator.js +1 -0
  12. package/dist/cursor-local-agent-ownership.js +17 -0
  13. package/dist/cursor-mcp-timeout-override.js +7 -18
  14. package/dist/cursor-native-tool-display-replay.js +5 -5
  15. package/dist/cursor-native-tools.js +11 -0
  16. package/dist/cursor-pi-tool-bridge-diagnostics.js +4 -0
  17. package/dist/cursor-pi-tool-bridge-env.js +25 -2
  18. package/dist/cursor-pi-tool-bridge-run.js +10 -3
  19. package/dist/cursor-pi-tool-bridge-server.js +98 -48
  20. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  21. package/dist/cursor-pi-tool-bridge.js +4 -12
  22. package/dist/cursor-provider-errors.js +2 -8
  23. package/dist/cursor-provider-live-run-drain.js +8 -3
  24. package/dist/cursor-provider-overflow.js +7 -21
  25. package/dist/cursor-provider-turn-prepare.js +8 -1
  26. package/dist/cursor-provider-turn-runner.js +17 -6
  27. package/dist/cursor-provider-turn-summary.js +1 -1
  28. package/dist/cursor-provider.js +1 -4
  29. package/dist/cursor-runtime-state.js +3 -3
  30. package/dist/cursor-sdk-event-debug.js +2 -2
  31. package/dist/cursor-sdk-output-filter.js +1 -1
  32. package/dist/cursor-sdk-process-error-guard.js +1 -2
  33. package/dist/cursor-session-agent-cleanup.js +10 -1
  34. package/dist/cursor-session-agent-lifecycle.js +2 -2
  35. package/dist/cursor-session-agent-lineage.js +11 -7
  36. package/dist/cursor-session-agent-resume.js +15 -9
  37. package/dist/cursor-session-agent.js +28 -15
  38. package/dist/cursor-session-scope.js +28 -21
  39. package/dist/cursor-session-settings.js +3 -3
  40. package/dist/cursor-session-store.js +2 -2
  41. package/dist/cursor-state.js +26 -29
  42. package/dist/cursor-tool-lifecycle.js +0 -3
  43. package/dist/cursor-tool-manifest.js +3 -1
  44. package/dist/cursor-tool-presentation-registry.js +2 -4
  45. package/dist/cursor-tool-visibility.js +0 -3
  46. package/dist/cursor-transcript-tool-specs.js +1 -2
  47. package/dist/index.js +21 -0
  48. package/dist/model-discovery.js +8 -10
  49. package/dist/model-list-cache.js +4 -9
  50. package/docs/cursor-live-smoke-checklist.md +4 -4
  51. package/docs/cursor-model-ux-spec.md +36 -19
  52. package/docs/cursor-native-tool-replay.md +8 -5
  53. package/docs/cursor-native-tool-visual-audit.md +1 -1
  54. package/docs/cursor-testing-lessons.md +39 -9
  55. package/docs/cursor-tool-surfaces.md +48 -4
  56. package/docs/development.md +93 -0
  57. package/docs/platform-smoke-implementation.md +3 -2
  58. package/docs/reference.md +806 -0
  59. package/package.json +13 -11
  60. package/scripts/platform-smoke/artifact-secrets.mjs +13 -4
  61. package/scripts/platform-smoke/local-resume-runner.mjs +0 -1
  62. package/scripts/platform-smoke/target-runtime.mjs +4 -2
  63. package/scripts/platform-smoke/targets.mjs +0 -1
  64. package/shared/cursor-sdk-output-filter.d.mts +2 -0
  65. package/shared/cursor-sdk-output-filter.mjs +92 -28
  66. package/shared/cursor-sensitive-text.d.mts +1 -0
  67. package/shared/cursor-sensitive-text.mjs +7 -0
  68. package/src/context.ts +18 -11
  69. package/src/cursor-agents-context.ts +6 -6
  70. package/src/cursor-api-key.ts +1 -1
  71. package/src/cursor-bridge-contract.ts +1 -15
  72. package/src/cursor-cloud-local-state.ts +0 -2
  73. package/src/cursor-config.ts +1 -24
  74. package/src/cursor-custom-subagent-definitions.ts +1 -1
  75. package/src/cursor-fallback-models.generated.ts +665 -1
  76. package/src/cursor-live-run-coordinator.ts +5 -0
  77. package/src/cursor-local-agent-ownership.ts +18 -0
  78. package/src/cursor-mcp-timeout-override.ts +7 -31
  79. package/src/cursor-native-tool-display-replay.ts +4 -6
  80. package/src/cursor-native-tools.ts +14 -0
  81. package/src/cursor-pi-tool-bridge-diagnostics.ts +17 -0
  82. package/src/cursor-pi-tool-bridge-env.ts +31 -2
  83. package/src/cursor-pi-tool-bridge-run.ts +10 -3
  84. package/src/cursor-pi-tool-bridge-server.ts +89 -45
  85. package/src/cursor-pi-tool-bridge-snapshot.ts +0 -1
  86. package/src/cursor-pi-tool-bridge.ts +2 -18
  87. package/src/cursor-provider-errors.ts +2 -10
  88. package/src/cursor-provider-live-run-drain.ts +8 -2
  89. package/src/cursor-provider-overflow.ts +6 -24
  90. package/src/cursor-provider-turn-prepare.ts +10 -1
  91. package/src/cursor-provider-turn-runner.ts +17 -6
  92. package/src/cursor-provider-turn-summary.ts +1 -1
  93. package/src/cursor-provider.ts +0 -5
  94. package/src/cursor-runtime-state.ts +3 -3
  95. package/src/cursor-sdk-event-debug.ts +2 -2
  96. package/src/cursor-sdk-output-filter.ts +2 -1
  97. package/src/cursor-sdk-process-error-guard.ts +1 -3
  98. package/src/cursor-session-agent-cleanup.ts +6 -1
  99. package/src/cursor-session-agent-lifecycle.ts +2 -2
  100. package/src/cursor-session-agent-lineage.ts +10 -5
  101. package/src/cursor-session-agent-resume.ts +14 -7
  102. package/src/cursor-session-agent.ts +31 -15
  103. package/src/cursor-session-scope.ts +34 -20
  104. package/src/cursor-session-settings.ts +3 -3
  105. package/src/cursor-session-store.ts +2 -2
  106. package/src/cursor-state.ts +34 -41
  107. package/src/cursor-tool-lifecycle.ts +0 -4
  108. package/src/cursor-tool-manifest.ts +4 -1
  109. package/src/cursor-tool-presentation-registry.ts +3 -5
  110. package/src/cursor-tool-visibility.ts +0 -4
  111. package/src/cursor-transcript-tool-specs.ts +0 -3
  112. package/src/index.ts +18 -0
  113. package/src/model-discovery.ts +7 -9
  114. package/src/model-list-cache.ts +4 -10
@@ -8,12 +8,6 @@ const CURSOR_MCP_TOOL_TIMEOUT_SECONDS_ENV = "PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS"
8
8
  const CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV = "PI_CURSOR_MCP_CONNECT_TIMEOUT_MS";
9
9
  const CURSOR_MCP_CONNECT_TIMEOUT_SECONDS_ENV = "PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS";
10
10
 
11
- interface CursorMcpToolTimeoutOverrideOptions {
12
- timeoutMs?: number;
13
- connectTimeoutMs?: number;
14
- env?: Record<string, string | undefined>;
15
- }
16
-
17
11
  interface CursorMcpToolTimeoutOverrideState {
18
12
  installed: boolean;
19
13
  timeoutMs: number;
@@ -61,7 +55,7 @@ function normalizeConnectTimeoutMs(timeoutMs: number): number {
61
55
  );
62
56
  }
63
57
 
64
- export function resolveCursorMcpConnectTimeoutMs(
58
+ function resolveCursorMcpConnectTimeoutMs(
65
59
  env: Record<string, string | undefined> = process.env,
66
60
  ): number {
67
61
  const explicitMs = parsePositiveNumber(env[CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV]);
@@ -81,7 +75,7 @@ function isCursorSdkMcpProtocolTimeoutStack(stack: string | undefined): boolean
81
75
  );
82
76
  }
83
77
 
84
- export function isCursorSdkMcpToolTimeoutStack(stack: string | undefined): boolean {
78
+ function isCursorSdkMcpToolTimeoutStack(stack: string | undefined): boolean {
85
79
  if (!stack) return false;
86
80
  return (
87
81
  isCursorSdkMcpProtocolTimeoutStack(stack) &&
@@ -89,9 +83,9 @@ export function isCursorSdkMcpToolTimeoutStack(stack: string | undefined): boole
89
83
  );
90
84
  }
91
85
 
92
- export function isCursorSdkMcpConnectTimeoutStack(stack: string | undefined): boolean {
86
+ function isCursorSdkMcpConnectTimeoutStack(stack: string | undefined): boolean {
93
87
  if (!stack || !isCursorSdkMcpProtocolTimeoutStack(stack)) return false;
94
- // SDK 1.0.36 minifies the Client class name; the protocol method and SDK
88
+ // The SDK minifies the Client class name; the protocol method and SDK
95
89
  // frame, not that constructor's spelling, establish this timeout purpose.
96
90
  return /^\s*at (?:[\w$]+\.)?(?:connect|listTools|getTools)\s*\(.*(?:node_modules[/\\]@cursor[/\\]sdk|@cursor\/sdk\/dist)/m.test(stack);
97
91
  }
@@ -121,15 +115,9 @@ function patchedSetTimeout(
121
115
  return Reflect.apply(delegate, globalThis, [handler, nextDelay, ...args]) as ReturnType<GlobalSetTimeout>;
122
116
  }
123
117
 
124
- export function installCursorMcpToolTimeoutOverride(
125
- options: CursorMcpToolTimeoutOverrideOptions = {},
126
- ): CursorMcpToolTimeoutOverrideState {
127
- installedToolTimeoutMs = normalizeOverrideTimeoutMs(
128
- options.timeoutMs ?? resolveCursorMcpToolTimeoutMs(options.env),
129
- );
130
- installedConnectTimeoutMs = normalizeConnectTimeoutMs(
131
- options.connectTimeoutMs ?? resolveCursorMcpConnectTimeoutMs(options.env),
132
- );
118
+ export function installCursorMcpToolTimeoutOverride(): CursorMcpToolTimeoutOverrideState {
119
+ installedToolTimeoutMs = resolveCursorMcpToolTimeoutMs();
120
+ installedConnectTimeoutMs = resolveCursorMcpConnectTimeoutMs();
133
121
 
134
122
  if (!originalSetTimeout) {
135
123
  originalSetTimeout = globalThis.setTimeout;
@@ -152,15 +140,3 @@ export function restoreCursorMcpToolTimeoutOverride(): void {
152
140
  installedToolTimeoutMs = DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
153
141
  installedConnectTimeoutMs = DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
154
142
  }
155
-
156
- export const cursorMcpToolTimeoutOverrideDefaults = {
157
- cursorSdkDefaultTimeoutMs: CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
158
- defaultOverrideTimeoutMs: DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS,
159
- defaultConnectTimeoutMs: DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS,
160
- minConnectTimeoutMs: MIN_CURSOR_MCP_CONNECT_TIMEOUT_MS,
161
- maxNodeTimerDelayMs: MAX_NODE_TIMER_DELAY_MS,
162
- timeoutMsEnv: CURSOR_MCP_TOOL_TIMEOUT_MS_ENV,
163
- timeoutSecondsEnv: CURSOR_MCP_TOOL_TIMEOUT_SECONDS_ENV,
164
- connectTimeoutMsEnv: CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV,
165
- connectTimeoutSecondsEnv: CURSOR_MCP_CONNECT_TIMEOUT_SECONDS_ENV,
166
- } as const;
@@ -34,14 +34,12 @@ export type {
34
34
  } from "./cursor-replay-tool-details.js";
35
35
  export {
36
36
  isCursorReplayNativeEditDetails,
37
- isCursorReplayGenerateImageDetails,
38
- isCursorReplayActivityDetails,
39
37
  isCursorReplayNativeWriteDetails,
40
38
  parseCursorReplayToolDetails,
41
39
  } from "./cursor-replay-tool-details.js";
42
40
 
43
41
  export const CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES = 8;
44
- export const CURSOR_REPLAY_PREVIEW_MAX_CHARS = 4000;
42
+ const CURSOR_REPLAY_PREVIEW_MAX_CHARS = 4000;
45
43
  export const CURSOR_REPLAY_PREVIEW_MAX_LINE_CHARS = 240;
46
44
  const CURSOR_REPLAY_HIGHLIGHT_MAX_CHARS = 12000;
47
45
  export const cursorReplayToolSchema = Type.Object({}, { additionalProperties: true });
@@ -75,7 +73,7 @@ function buildImageReplayComponent(text: string, imageData: string, mimeType: st
75
73
  };
76
74
  }
77
75
 
78
- export function getCursorReplayPath(
76
+ function getCursorReplayPath(
79
77
  args: Record<string, unknown> | undefined,
80
78
  details: Pick<CursorReplayNativeEditDetails, "path"> | Pick<CursorReplayNativeWriteDetails, "path"> | undefined,
81
79
  ): string {
@@ -282,7 +280,7 @@ function formatCursorReplayActivityPreview(
282
280
  return stripHeader ? formatCursorReplayPreview(text, theme, maxLines, true) : formatMutedBlock(text, theme);
283
281
  }
284
282
 
285
- export function formatCursorReplayPreview(
283
+ function formatCursorReplayPreview(
286
284
  text: string,
287
285
  theme: CursorReplayRenderTheme,
288
286
  maxLines = CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES,
@@ -330,7 +328,7 @@ function getCursorReplayCardTitle(toolName: CursorReplayToolName, args: Record<s
330
328
  return "Cursor activity";
331
329
  }
332
330
 
333
- export function renderCursorReplayCall(
331
+ function renderCursorReplayCall(
334
332
  toolName: CursorReplayToolName,
335
333
  args: Record<string, unknown> | undefined,
336
334
  theme: CursorReplayRenderTheme,
@@ -0,0 +1,14 @@
1
+ import { parseEnvBoolean } from "./cursor-env-boolean.js";
2
+
3
+ export const CURSOR_NATIVE_TOOLS_ENV = "PI_CURSOR_NATIVE_TOOLS";
4
+
5
+ /** Default preserves the SDK toolset; false selects the public MCP-only main loop. */
6
+ export function resolveCursorNativeToolsEnabled(env: Record<string, string | undefined> = process.env): boolean {
7
+ return parseEnvBoolean(env[CURSOR_NATIVE_TOOLS_ENV], true);
8
+ }
9
+
10
+ export function assertCursorNativeToolsRuntime(runtime: "local" | "cloud", nativeToolsEnabled: boolean): void {
11
+ if (runtime === "cloud" && !nativeToolsEnabled) {
12
+ throw new Error("PI_CURSOR_NATIVE_TOOLS=0 requires local runtime; Cursor Cloud does not support the SDK tools restriction. Select local runtime or remove the opt-out.");
13
+ }
14
+ }
@@ -49,6 +49,17 @@ interface CursorPiToolBridgeRunDisposedDiagnostic extends CursorPiToolBridgeLife
49
49
  event: "run_disposed";
50
50
  }
51
51
 
52
+ interface CursorPiToolBridgeMcpInitializedDiagnostic {
53
+ event: "mcp_initialized";
54
+ runId: string;
55
+ }
56
+
57
+ interface CursorPiToolBridgeMcpToolsListDiagnostic {
58
+ event: "mcp_tools_list";
59
+ runId: string;
60
+ toolCount: number;
61
+ }
62
+
52
63
  export interface CursorPiToolBridgeRequestDiagnosticFields {
53
64
  runId: string;
54
65
  bridgeCallId: string;
@@ -79,6 +90,8 @@ export type CursorPiToolBridgeDiagnosticEvent =
79
90
  | CursorPiToolBridgeToolsExposedDiagnostic
80
91
  | CursorPiToolBridgeRunCancelledDiagnostic
81
92
  | CursorPiToolBridgeRunDisposedDiagnostic
93
+ | CursorPiToolBridgeMcpInitializedDiagnostic
94
+ | CursorPiToolBridgeMcpToolsListDiagnostic
82
95
  | CursorPiToolBridgeRequestQueuedDiagnostic
83
96
  | CursorPiToolBridgeRequestResolvedDiagnostic
84
97
  | CursorPiToolBridgeRequestRejectedDiagnostic;
@@ -89,6 +102,10 @@ function assertNeverDiagnosticEvent(_event: never): never {
89
102
 
90
103
  export function serializeCursorPiToolBridgeDiagnostic(event: CursorPiToolBridgeDiagnosticEvent): Record<string, unknown> {
91
104
  switch (event.event) {
105
+ case "mcp_initialized":
106
+ return { event: event.event, runId: event.runId };
107
+ case "mcp_tools_list":
108
+ return { event: event.event, runId: event.runId, toolCount: event.toolCount };
92
109
  case "run_created":
93
110
  return {
94
111
  event: event.event,
@@ -2,8 +2,8 @@ import { parseEnvBoolean } from "./cursor-env-boolean.js";
2
2
  import { resolveCursorMcpToolTimeoutMs } from "./cursor-mcp-timeout-override.js";
3
3
 
4
4
  export const CURSOR_PI_TOOL_BRIDGE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE";
5
- export const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
6
- export const CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV = "PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS";
5
+ const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
6
+ const CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV = "PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS";
7
7
 
8
8
  export function resolveCursorPiToolBridgeEnabled(env: Record<string, string | undefined> = process.env): boolean {
9
9
  return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_ENV], true);
@@ -19,3 +19,32 @@ export function resolveCursorPiToolBridgeCallTimeoutMs(env: Record<string, strin
19
19
  if (!Number.isFinite(parsed) || parsed <= 0) return mcpToolTimeoutMs;
20
20
  return Math.min(Math.max(Math.trunc(parsed), 1), mcpToolTimeoutMs);
21
21
  }
22
+
23
+ const CURSOR_PI_TOOL_BRIDGE_HOST_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_HOST";
24
+ const CURSOR_PI_TOOL_BRIDGE_PORT_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_PORT";
25
+ const DEFAULT_URL_HOST = "127.0.0.1";
26
+ const PORT_PATTERN = /^\d{1,5}$/;
27
+
28
+ export interface CursorPiToolBridgePortRange {
29
+ readonly first: number;
30
+ readonly last: number;
31
+ }
32
+
33
+ export function resolveCursorPiToolBridgeUrlHost(env: Record<string, string | undefined> = process.env): string {
34
+ return env[CURSOR_PI_TOOL_BRIDGE_HOST_ENV]?.trim().toLowerCase() === "localhost" ? "localhost" : DEFAULT_URL_HOST;
35
+ }
36
+
37
+ function parsePort(text: string): number | undefined {
38
+ if (!PORT_PATTERN.test(text)) return undefined;
39
+ const port = Number(text);
40
+ return port >= 1 && port <= 65535 ? port : undefined;
41
+ }
42
+
43
+ export function resolveCursorPiToolBridgePortRange(env: Record<string, string | undefined> = process.env): CursorPiToolBridgePortRange | undefined {
44
+ const parts = env[CURSOR_PI_TOOL_BRIDGE_PORT_ENV]?.trim().split("-");
45
+ if (!parts || parts.length > 2) return undefined;
46
+ const first = parsePort(parts[0]!);
47
+ const last = parts.length === 2 ? parsePort(parts[1]!) : first;
48
+ if (first === undefined || last === undefined || first > last) return undefined;
49
+ return { first, last };
50
+ }
@@ -274,9 +274,16 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
274
274
  sessionIdGenerator: randomUUID,
275
275
  });
276
276
 
277
- server.setRequestHandler("tools/list", async () => ({
278
- tools: this.snapshot.tools.map(snapshotToolToMcpTool),
279
- }));
277
+ // ponytail: server-side receipts only, not SDK/model consumption or ongoing
278
+ // connection health; upgrade when Cursor exposes public MCP lifecycle callbacks.
279
+ server.oninitialized = () => {
280
+ this.emitDiagnostic({ event: "mcp_initialized", runId: this.id });
281
+ };
282
+ server.setRequestHandler("tools/list", async () => {
283
+ const tools = this.snapshot.tools.map(snapshotToolToMcpTool);
284
+ this.emitDiagnostic({ event: "mcp_tools_list", runId: this.id, toolCount: tools.length });
285
+ return { tools };
286
+ });
280
287
  server.setRequestHandler("tools/call", async (request, context) => {
281
288
  return this.enqueueToolRequest(
282
289
  request.params.name,
@@ -6,6 +6,10 @@ import type {
6
6
  CursorPiToolBridgeRunOptions,
7
7
  CursorPiToolBridgeSnapshotApi,
8
8
  } from "./cursor-pi-tool-bridge-types.js";
9
+ import {
10
+ resolveCursorPiToolBridgePortRange,
11
+ resolveCursorPiToolBridgeUrlHost,
12
+ } from "./cursor-pi-tool-bridge-env.js";
9
13
  import { asRecord } from "./cursor-record-utils.js";
10
14
  import { CursorPiToolBridgeRunImpl } from "./cursor-pi-tool-bridge-run.js";
11
15
  import {
@@ -16,15 +20,54 @@ import {
16
20
  resolveCursorPiToolBridgeEnabled,
17
21
  } from "./cursor-pi-tool-bridge-snapshot.js";
18
22
 
19
- export const LOOPBACK_HOST = "127.0.0.1";
23
+ const LOOPBACK_HOST = "127.0.0.1";
20
24
  const HTTP_SERVER_CLOSE_GRACE_MS = 250;
25
+ const EPHEMERAL_BIND_ATTEMPTS = 10;
26
+
27
+ function listenOnce(server: HttpServer, port: number, host = LOOPBACK_HOST): Promise<void> {
28
+ return new Promise<void>((resolve, reject) => {
29
+ const onError = (error: Error) => {
30
+ server.off("listening", onListening);
31
+ reject(error);
32
+ };
33
+ const onListening = () => {
34
+ server.off("error", onError);
35
+ resolve();
36
+ };
37
+ server.once("error", onError);
38
+ server.once("listening", onListening);
39
+ server.listen({ port, host, ipv6Only: host === "::1" });
40
+ });
41
+ }
42
+
43
+ async function closeServers(servers: readonly HttpServer[]): Promise<void> {
44
+ await Promise.all(servers.map(async (server) => {
45
+ if (!server.listening) return;
46
+ await new Promise<void>((resolve, reject) => {
47
+ let settled = false;
48
+ const settle = (error?: Error): void => {
49
+ if (settled) return;
50
+ settled = true;
51
+ clearTimeout(closeTimer);
52
+ if (error) reject(error);
53
+ else resolve();
54
+ };
55
+ const closeTimer = setTimeout(() => settle(), HTTP_SERVER_CLOSE_GRACE_MS);
56
+ closeTimer.unref?.();
57
+ server.close((error) => settle(error ?? undefined));
58
+ server.closeIdleConnections();
59
+ server.closeAllConnections();
60
+ });
61
+ }));
62
+ }
21
63
 
22
64
  export class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
23
65
  private readonly pi: CursorPiToolBridgeSnapshotApi;
24
66
  private readonly env: Record<string, string | undefined>;
25
67
  private readonly runs = new Set<CursorPiToolBridgeRunImpl>();
26
68
  private readonly routes = new Map<string, CursorPiToolBridgeRunImpl>();
27
- private httpServer?: HttpServer;
69
+ private httpServers: HttpServer[] = [];
70
+ private advertisedHost?: string;
28
71
  private listenPromise?: Promise<void>;
29
72
  private serverLifecycle = Promise.resolve();
30
73
 
@@ -57,7 +100,14 @@ export class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
57
100
  : createEmptySnapshot();
58
101
  const run = new CursorPiToolBridgeRunImpl(this, this.env, snapshot, bridgeEnabled && snapshot.tools.length > 0, options);
59
102
  this.runs.add(run);
60
- await run.start();
103
+ try {
104
+ await run.start();
105
+ } catch (error) {
106
+ try { await run.dispose(); }
107
+ catch { /* Preserve the original startup failure, not a secondary cleanup error. */ }
108
+ this.runs.delete(run);
109
+ throw error;
110
+ }
61
111
  run.emitStartDiagnostics(bridgeEnabled);
62
112
  return run;
63
113
  }
@@ -74,8 +124,8 @@ export class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
74
124
  await this.ensureHttpServer();
75
125
  this.routes.set(pathname, run);
76
126
  const address = this.getHttpServerAddress();
77
- if (!address) throw new Error("Cursor pi tool bridge HTTP server is not listening");
78
- return `http://${LOOPBACK_HOST}:${address.port}${pathname}`;
127
+ if (!address || !this.advertisedHost) throw new Error("Cursor pi tool bridge HTTP server is not listening");
128
+ return `http://${this.advertisedHost}:${address.port}${pathname}`;
79
129
  });
80
130
  }
81
131
 
@@ -94,7 +144,7 @@ export class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
94
144
  }
95
145
 
96
146
  getHttpServerAddress(): AddressInfo | undefined {
97
- const address = asRecord(this.httpServer?.address());
147
+ const address = asRecord(this.httpServers[0]?.address());
98
148
  if (typeof address?.port !== "number") return undefined;
99
149
  return {
100
150
  address: typeof address.address === "string" ? address.address : LOOPBACK_HOST,
@@ -123,7 +173,8 @@ export class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
123
173
 
124
174
  private async ensureHttpServer(): Promise<void> {
125
175
  this.listenPromise ??= this.startHttpServer().catch((error: unknown) => {
126
- this.httpServer = undefined;
176
+ this.httpServers = [];
177
+ this.advertisedHost = undefined;
127
178
  this.listenPromise = undefined;
128
179
  throw error;
129
180
  });
@@ -131,6 +182,9 @@ export class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
131
182
  }
132
183
 
133
184
  private async startHttpServer(): Promise<void> {
185
+ // A hostname is a listener policy, not a mutable per-run URL preference.
186
+ const advertisedHost = resolveCursorPiToolBridgeUrlHost(this.env);
187
+ const range = resolveCursorPiToolBridgePortRange(this.env);
134
188
  const [{ getRequestListener }, { createMcpHonoApp }] = await Promise.all([
135
189
  import("@hono/node-server"),
136
190
  import("@modelcontextprotocol/hono"),
@@ -142,49 +196,39 @@ export class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
142
196
  ).get("parsedBody");
143
197
  return this.handleHttpRequest(context.req.raw, parsedBody);
144
198
  });
145
- const server = createServer(getRequestListener(app.fetch, { overrideGlobalObjects: false }));
146
- this.httpServer = server;
147
- await new Promise<void>((resolve, reject) => {
148
- const onError = (error: Error) => {
149
- server.off("listening", onListening);
150
- reject(error);
151
- };
152
- const onListening = () => {
153
- server.off("error", onError);
154
- resolve();
155
- };
156
- server.once("error", onError);
157
- server.once("listening", onListening);
158
- server.listen(0, LOOPBACK_HOST);
159
- });
199
+ const listener = getRequestListener(app.fetch, { overrideGlobalObjects: false });
200
+ const attempts = range ? range.last - range.first + 1 : advertisedHost === "localhost" ? EPHEMERAL_BIND_ATTEMPTS : 1;
201
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
202
+ const servers = [createServer(listener)];
203
+ try {
204
+ await listenOnce(servers[0]!, range ? range.first + attempt : 0);
205
+ if (advertisedHost === "localhost") {
206
+ const port = (servers[0]!.address() as AddressInfo).port;
207
+ const ipv6 = createServer(listener);
208
+ servers.push(ipv6);
209
+ // Own both explicit loopbacks before publishing any tokenized URL.
210
+ await listenOnce(ipv6, port, "::1");
211
+ }
212
+ this.httpServers = servers;
213
+ this.advertisedHost = advertisedHost;
214
+ return;
215
+ } catch (error) {
216
+ try { await closeServers(servers); }
217
+ catch { throw error; } // Never retry or replace the bind failure when rollback fails.
218
+ if ((error as NodeJS.ErrnoException).code !== "EADDRINUSE" || (!range && advertisedHost !== "localhost")) throw error;
219
+ }
220
+ }
221
+ if (range) throw new Error(`Cursor pi tool bridge could not listen on any port in ${range.first}-${range.last} (all in use)`);
222
+ throw new Error("Cursor pi tool bridge could not own both localhost loopback listeners on an ephemeral port");
160
223
  }
161
224
 
162
225
  private async closeHttpServer(): Promise<void> {
163
226
  await this.listenPromise?.catch(() => undefined);
164
- const server = this.httpServer;
165
- if (!server) return;
166
- this.httpServer = undefined;
227
+ const servers = this.httpServers;
228
+ this.httpServers = [];
229
+ this.advertisedHost = undefined;
167
230
  this.listenPromise = undefined;
168
- await new Promise<void>((resolve, reject) => {
169
- let settled = false;
170
- let closeTimer: ReturnType<typeof setTimeout> | undefined;
171
- const settle = (error?: Error): void => {
172
- if (settled) return;
173
- settled = true;
174
- if (closeTimer) clearTimeout(closeTimer);
175
- if (error) reject(error);
176
- else resolve();
177
- };
178
-
179
- closeTimer = setTimeout(() => settle(), HTTP_SERVER_CLOSE_GRACE_MS);
180
- closeTimer.unref?.();
181
-
182
- server.close((error) => {
183
- settle(error ?? undefined);
184
- });
185
- server.closeIdleConnections();
186
- server.closeAllConnections();
187
- });
231
+ await closeServers(servers);
188
232
  }
189
233
 
190
234
  private async handleHttpRequest(request: Request, parsedBody?: unknown): Promise<Response> {
@@ -6,7 +6,6 @@ import type {
6
6
  } from "./cursor-pi-tool-bridge-types.js";
7
7
  import { createMcpToolName, normalizeMcpInputSchema, stableNameHash } from "./cursor-pi-tool-bridge-mcp.js";
8
8
  export {
9
- CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
10
9
  CURSOR_PI_TOOL_BRIDGE_ENV,
11
10
  resolveCursorPiToolBridgeBuiltinsEnabled,
12
11
  resolveCursorPiToolBridgeEnabled,
@@ -1,18 +1,12 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import {
3
- CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
4
3
  CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
5
4
  type CursorPiToolBridgeDiagnosticEvent,
6
5
  serializeCursorPiToolBridgeDiagnostic,
7
6
  } from "./cursor-pi-tool-bridge-diagnostics.js";
8
- import {
9
- CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
10
- CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
11
- CURSOR_PI_TOOL_BRIDGE_ENV,
12
- } from "./cursor-pi-tool-bridge-env.js";
13
7
  import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
14
- import { isCursorPiBridgeToolCallId, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
15
- import { LOOPBACK_HOST, CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
8
+ import { isCursorPiBridgeToolCallId } from "./cursor-pi-tool-bridge-constants.js";
9
+ import { CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
16
10
  import type {
17
11
  CursorPiToolBridge,
18
12
  CursorPiToolBridgeExtensionApi,
@@ -34,9 +28,6 @@ export type {
34
28
  export type { CursorPiToolBridgeDiagnosticEvent } from "./cursor-pi-tool-bridge-diagnostics.js";
35
29
  export { resolveCursorPiToolBridgeDebugEnabled } from "./cursor-pi-tool-bridge-diagnostics.js";
36
30
  export {
37
- CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
38
- CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
39
- CURSOR_PI_TOOL_BRIDGE_ENV,
40
31
  resolveCursorPiToolBridgeBuiltinsEnabled,
41
32
  resolveCursorPiToolBridgeCallTimeoutMs,
42
33
  resolveCursorPiToolBridgeEnabled,
@@ -145,13 +136,7 @@ export function getRegisteredCursorPiToolBridge(pi?: object): CursorPiToolBridge
145
136
  }
146
137
 
147
138
  export const __testUtils = {
148
- CURSOR_PI_TOOL_BRIDGE_ENV,
149
- CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
150
- CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
151
- CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
152
139
  CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
153
- LOOPBACK_HOST,
154
- MCP_SERVER_NAME,
155
140
  createRegistry(
156
141
  pi: CursorPiToolBridgeSnapshotApi,
157
142
  env: Record<string, string | undefined> = process.env,
@@ -168,7 +153,6 @@ export const __testUtils = {
168
153
  return bridgeToolExecutionAbortTracker.getActiveCount();
169
154
  },
170
155
  buildWindowsBridgeBashAbortCommandForTests: buildWindowsBridgeBashAbortCommand,
171
- installWindowsBridgeBashAbortMarkerForTests: installWindowsBridgeBashAbortMarker,
172
156
  emitBridgeToolExecutionProcessAbortSignalForTests(signal: NodeJS.Signals) {
173
157
  bridgeToolExecutionAbortTracker.emitProcessAbortSignalForTests(signal);
174
158
  },
@@ -168,7 +168,7 @@ function isCursorSdkStallAbortNetworkError(code: unknown, evidence: string, stac
168
168
  }
169
169
 
170
170
  /**
171
- * @cursor/sdk@1.0.36 throws internal RetriableError (name/kind "RetriableError") with message
171
+ * @cursor/sdk@1.0.37 throws internal RetriableError (name/kind "RetriableError") with message
172
172
  * "Connection stalled" or "Connection stalled repeatedly" after fetchWithRetry exhausts stalls.
173
173
  * The ConnectError is only the cause; the top-level error is not a ConnectError.
174
174
  */
@@ -229,7 +229,7 @@ export function classifyCursorConnectError(error: unknown): CursorConnectErrorCl
229
229
 
230
230
  const directAbort = (code === 1 || code === "canceled") &&
231
231
  (causeName === "AbortError" || /AbortError/.test(stack));
232
- // The vendored 1.0.36 transport wraps a canceled ConnectError as Unknown.
232
+ // The vendored SDK transport can wrap a canceled ConnectError as Unknown.
233
233
  // Require the observed canceled -> AbortError cause chain, not generic text.
234
234
  const wrappedAbort = (code === 2 || code === "unknown") &&
235
235
  causeName === "ConnectError" &&
@@ -258,14 +258,6 @@ export function classifyCursorConnectError(error: unknown): CursorConnectErrorCl
258
258
  return undefined;
259
259
  }
260
260
 
261
- export function isCursorSdkAbortConnectError(error: unknown): boolean {
262
- return classifyCursorConnectError(error)?.kind === "abort";
263
- }
264
-
265
- export function isUnauthenticatedConnectError(error: unknown): boolean {
266
- return classifyCursorConnectError(error)?.kind === "unauthenticated";
267
- }
268
-
269
261
  function isLikelyNetworkTimeout(message: string): boolean {
270
262
  return (
271
263
  /\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENETUNREACH|EAI_AGAIN|NGHTTP2_ENHANCE_YOUR_CALM|ERR_HTTP2_STREAM_ERROR|ERR_HTTP2_SESSION_ERROR)\b/i.test(
@@ -29,7 +29,7 @@ import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js
29
29
  import { partitionNativeToolsByActiveContext } from "./cursor-native-replay-routing.js";
30
30
  import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
31
31
 
32
- export const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
32
+ const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
33
33
  function parseCursorNativeReplayIdleDisposeMs(raw: string | undefined): number {
34
34
  if (!raw || /[^0-9]/.test(raw)) return DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
35
35
  const value = Number(raw);
@@ -69,7 +69,7 @@ function getCursorNativeReplayIdFromToolCallId(toolCallId: string): string | und
69
69
  return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
70
70
  }
71
71
 
72
- export function getPendingCursorLiveRun(context: Context, scopeKey: string): CursorLiveRun | undefined {
72
+ function getPendingCursorLiveRun(context: Context, scopeKey: string): CursorLiveRun | undefined {
73
73
  return cursorLiveRuns.getPendingFromContext(context, getCursorNativeReplayIdFromToolCallId, scopeKey);
74
74
  }
75
75
 
@@ -449,6 +449,7 @@ export async function drainExistingCursorLiveRunBeforeSend(
449
449
  turnDebugRecorder: CursorSdkEventDebugRecorder | undefined,
450
450
  scopeKey: string,
451
451
  occupancyFloor?: number,
452
+ nativeToolsEnabled = true,
452
453
  ): Promise<LiveRunPreSendOutcome> {
453
454
  turnDebugRecorder?.recordDrainEvent("pre_send_start", {});
454
455
  while (true) {
@@ -461,6 +462,11 @@ export async function drainExistingCursorLiveRunBeforeSend(
461
462
  try {
462
463
  const outcome = await cursorLiveRuns.withRunLease(run, signal, async () => {
463
464
  if (run.disposed) return "continue_send" as const;
465
+ // Check under the drain lease, before consuming/delivering Pi results:
466
+ // resolving a bridge waiter can resume the SDK's old tool surface.
467
+ if ((run.nativeToolsEnabled !== false) !== nativeToolsEnabled) {
468
+ throw new Error("Cursor native tool policy changed while a local run is pending; restore the prior policy to finish it or run /cursor-abort before retrying. Already-executed work is not undone.");
469
+ }
464
470
  const consumed = cursorLiveRuns.consumeToolResults(run, context, getCursorNativeReplayIdFromToolCallId);
465
471
  await run.bridgeRun?.resolveToolResults(consumed.toolResults);
466
472
  const shouldChainUserInput = run.chainUserInputAfterCompletion || hasTrailingUserMessagesAfterToolResults(context);
@@ -1,5 +1,4 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { AssistantMessage } from "@earendil-works/pi-ai";
3
2
  import { CURSOR_PROVIDER } from "./cursor-model.js";
4
3
 
5
4
  /**
@@ -15,7 +14,7 @@ import { CURSOR_PROVIDER } from "./cursor-model.js";
15
14
  *
16
15
  * See pi `docs/custom-provider.md` -> "Context Overflow Errors".
17
16
  */
18
- export const CURSOR_OVERFLOW_MARKER = "context_length_exceeded";
17
+ const CURSOR_OVERFLOW_MARKER = "context_length_exceeded";
19
18
 
20
19
  /**
21
20
  * Context-overflow phrases. These overlap pi's own `OVERFLOW_PATTERNS` so the
@@ -43,9 +42,9 @@ const CURSOR_THROTTLE_PATTERN = /too many requests|rate.?limit|throttl|retry.?af
43
42
  * Map a finalized Cursor assistant `errorMessage` to pi's overflow form.
44
43
  * Returns `undefined` when the message is not a Cursor context-overflow failure.
45
44
  *
46
- * Pure and idempotent so it is safe to call repeatedly and unit-test in isolation.
45
+ * Idempotent so repeated message delivery does not add another prefix.
47
46
  */
48
- export function normalizeCursorOverflowErrorMessage(errorMessage: string | undefined): string | undefined {
47
+ function normalizeCursorOverflowErrorMessage(errorMessage: string | undefined): string | undefined {
49
48
  const message = errorMessage?.trim();
50
49
  if (!message) return undefined;
51
50
  // Idempotent: never double-prefix an already-normalized message.
@@ -57,24 +56,6 @@ export function normalizeCursorOverflowErrorMessage(errorMessage: string | undef
57
56
  return undefined;
58
57
  }
59
58
 
60
- /**
61
- * Rewrite a finalized assistant message into pi's overflow form when it is a
62
- * Cursor context-overflow failure. Returns the replacement message (for the
63
- * `message_end` result) or `undefined` to leave it unchanged.
64
- *
65
- * `isCursorProvider` carries the provider scoping decision out of the handler so
66
- * the full logic is unit-testable without a pi event harness.
67
- */
68
- export function rewriteCursorOverflowAssistantMessage(
69
- message: AssistantMessage,
70
- isCursorProvider: boolean,
71
- ): AssistantMessage | undefined {
72
- if (!isCursorProvider || message.stopReason !== "error") return undefined;
73
- const rewritten = normalizeCursorOverflowErrorMessage(message.errorMessage);
74
- if (!rewritten) return undefined;
75
- return { ...message, errorMessage: rewritten };
76
- }
77
-
78
59
  export type CursorOverflowExtensionApi = Pick<ExtensionAPI, "on">;
79
60
 
80
61
  /**
@@ -89,8 +70,9 @@ export function registerCursorOverflowNormalization(pi: CursorOverflowExtensionA
89
70
  const message = event.message;
90
71
  if (message.role !== "assistant") return undefined;
91
72
  const isCursorProvider = message.provider === CURSOR_PROVIDER || ctx.model?.provider === CURSOR_PROVIDER;
92
- const rewritten = rewriteCursorOverflowAssistantMessage(message, isCursorProvider);
73
+ if (!isCursorProvider || message.stopReason !== "error") return undefined;
74
+ const rewritten = normalizeCursorOverflowErrorMessage(message.errorMessage);
93
75
  if (!rewritten) return undefined;
94
- return { message: rewritten };
76
+ return { message: { ...message, errorMessage: rewritten } };
95
77
  });
96
78
  }