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
@@ -36,7 +36,7 @@ function normalizeConnectTimeoutMs(timeoutMs) {
36
36
  return DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
37
37
  return Math.min(Math.max(Math.trunc(timeoutMs), MIN_CURSOR_MCP_CONNECT_TIMEOUT_MS), CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS);
38
38
  }
39
- export function resolveCursorMcpConnectTimeoutMs(env = process.env) {
39
+ function resolveCursorMcpConnectTimeoutMs(env = process.env) {
40
40
  const explicitMs = parsePositiveNumber(env[CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV]);
41
41
  if (explicitMs !== undefined)
42
42
  return normalizeConnectTimeoutMs(explicitMs);
@@ -51,16 +51,16 @@ function isCursorSdkMcpProtocolTimeoutStack(stack) {
51
51
  return (/(?:node_modules[/\\]@cursor[/\\]sdk|node_modules\/\@cursor\/sdk|@cursor\/sdk\/dist)/.test(stack) &&
52
52
  /\b_setupTimeout\b|\bProtocol\._setupTimeout\b/.test(stack));
53
53
  }
54
- export function isCursorSdkMcpToolTimeoutStack(stack) {
54
+ function isCursorSdkMcpToolTimeoutStack(stack) {
55
55
  if (!stack)
56
56
  return false;
57
57
  return (isCursorSdkMcpProtocolTimeoutStack(stack) &&
58
58
  /\bcallTool\b|\bClient\.callTool\b|\bMcpSdkClient\.callTool\b/.test(stack));
59
59
  }
60
- export function isCursorSdkMcpConnectTimeoutStack(stack) {
60
+ function isCursorSdkMcpConnectTimeoutStack(stack) {
61
61
  if (!stack || !isCursorSdkMcpProtocolTimeoutStack(stack))
62
62
  return false;
63
- // SDK 1.0.36 minifies the Client class name; the protocol method and SDK
63
+ // The SDK minifies the Client class name; the protocol method and SDK
64
64
  // frame, not that constructor's spelling, establish this timeout purpose.
65
65
  return /^\s*at (?:[\w$]+\.)?(?:connect|listTools|getTools)\s*\(.*(?:node_modules[/\\]@cursor[/\\]sdk|@cursor\/sdk\/dist)/m.test(stack);
66
66
  }
@@ -83,9 +83,9 @@ function patchedSetTimeout(handler, delay, ...args) {
83
83
  }
84
84
  return Reflect.apply(delegate, globalThis, [handler, nextDelay, ...args]);
85
85
  }
86
- export function installCursorMcpToolTimeoutOverride(options = {}) {
87
- installedToolTimeoutMs = normalizeOverrideTimeoutMs(options.timeoutMs ?? resolveCursorMcpToolTimeoutMs(options.env));
88
- installedConnectTimeoutMs = normalizeConnectTimeoutMs(options.connectTimeoutMs ?? resolveCursorMcpConnectTimeoutMs(options.env));
86
+ export function installCursorMcpToolTimeoutOverride() {
87
+ installedToolTimeoutMs = resolveCursorMcpToolTimeoutMs();
88
+ installedConnectTimeoutMs = resolveCursorMcpConnectTimeoutMs();
89
89
  if (!originalSetTimeout) {
90
90
  originalSetTimeout = globalThis.setTimeout;
91
91
  globalThis.setTimeout = patchedSetTimeout;
@@ -105,14 +105,3 @@ export function restoreCursorMcpToolTimeoutOverride() {
105
105
  installedToolTimeoutMs = DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
106
106
  installedConnectTimeoutMs = DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
107
107
  }
108
- export const cursorMcpToolTimeoutOverrideDefaults = {
109
- cursorSdkDefaultTimeoutMs: CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
110
- defaultOverrideTimeoutMs: DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS,
111
- defaultConnectTimeoutMs: DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS,
112
- minConnectTimeoutMs: MIN_CURSOR_MCP_CONNECT_TIMEOUT_MS,
113
- maxNodeTimerDelayMs: MAX_NODE_TIMER_DELAY_MS,
114
- timeoutMsEnv: CURSOR_MCP_TOOL_TIMEOUT_MS_ENV,
115
- timeoutSecondsEnv: CURSOR_MCP_TOOL_TIMEOUT_SECONDS_ENV,
116
- connectTimeoutMsEnv: CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV,
117
- connectTimeoutSecondsEnv: CURSOR_MCP_CONNECT_TIMEOUT_SECONDS_ENV,
118
- };
@@ -8,9 +8,9 @@ import { inferImageMimeType } from "./cursor-tool-result-display-readers.js";
8
8
  import { LOCAL_READ_PREVIEW_NOTICE, isLocalReadPreviewContent } from "./cursor-transcript-utils.js";
9
9
  import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME, getCursorReplayCallSummary, shouldShowCursorReplayCollapsedExpandHint, } from "./cursor-tool-presentation-registry.js";
10
10
  import { CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE, isCursorReplayGenerateImageDetails, isCursorReplayActivityDetails, parseCursorReplayToolDetails, } from "./cursor-replay-tool-details.js";
11
- export { isCursorReplayNativeEditDetails, isCursorReplayGenerateImageDetails, isCursorReplayActivityDetails, isCursorReplayNativeWriteDetails, parseCursorReplayToolDetails, } from "./cursor-replay-tool-details.js";
11
+ export { isCursorReplayNativeEditDetails, isCursorReplayNativeWriteDetails, parseCursorReplayToolDetails, } from "./cursor-replay-tool-details.js";
12
12
  export const CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES = 8;
13
- export const CURSOR_REPLAY_PREVIEW_MAX_CHARS = 4000;
13
+ const CURSOR_REPLAY_PREVIEW_MAX_CHARS = 4000;
14
14
  export const CURSOR_REPLAY_PREVIEW_MAX_LINE_CHARS = 240;
15
15
  const CURSOR_REPLAY_HIGHLIGHT_MAX_CHARS = 12000;
16
16
  export const cursorReplayToolSchema = Type.Object({}, { additionalProperties: true });
@@ -40,7 +40,7 @@ function buildImageReplayComponent(text, imageData, mimeType, filename, theme) {
40
40
  },
41
41
  };
42
42
  }
43
- export function getCursorReplayPath(args, details) {
43
+ function getCursorReplayPath(args, details) {
44
44
  const argPath = args?.path;
45
45
  return details?.path ?? (typeof argPath === "string" && argPath.trim() ? argPath : "unknown");
46
46
  }
@@ -202,7 +202,7 @@ function formatCursorReplayActivityPreview(details, text, theme, maxLines, strip
202
202
  }
203
203
  return stripHeader ? formatCursorReplayPreview(text, theme, maxLines, true) : formatMutedBlock(text, theme);
204
204
  }
205
- export function formatCursorReplayPreview(text, theme, maxLines = CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES, stripHeader = true) {
205
+ function formatCursorReplayPreview(text, theme, maxLines = CURSOR_REPLAY_COLLAPSED_PREVIEW_LINES, stripHeader = true) {
206
206
  const body = (stripHeader ? stripCursorReplayHeader(text) : text).trimEnd();
207
207
  if (!body)
208
208
  return undefined;
@@ -240,7 +240,7 @@ function getCursorReplayCardTitle(toolName, args) {
240
240
  }
241
241
  return "Cursor activity";
242
242
  }
243
- export function renderCursorReplayCall(toolName, args, theme, isPartial) {
243
+ function renderCursorReplayCall(toolName, args, theme, isPartial) {
244
244
  if (!isPartial)
245
245
  return new Text("", 0, 0);
246
246
  let text = theme.fg("toolTitle", theme.bold(`${getCursorReplayCardTitle(toolName, args)} `));
@@ -0,0 +1,11 @@
1
+ import { parseEnvBoolean } from "./cursor-env-boolean.js";
2
+ export const CURSOR_NATIVE_TOOLS_ENV = "PI_CURSOR_NATIVE_TOOLS";
3
+ /** Default preserves the SDK toolset; false selects the public MCP-only main loop. */
4
+ export function resolveCursorNativeToolsEnabled(env = process.env) {
5
+ return parseEnvBoolean(env[CURSOR_NATIVE_TOOLS_ENV], true);
6
+ }
7
+ export function assertCursorNativeToolsRuntime(runtime, nativeToolsEnabled) {
8
+ if (runtime === "cloud" && !nativeToolsEnabled) {
9
+ 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.");
10
+ }
11
+ }
@@ -15,6 +15,10 @@ function assertNeverDiagnosticEvent(_event) {
15
15
  }
16
16
  export function serializeCursorPiToolBridgeDiagnostic(event) {
17
17
  switch (event.event) {
18
+ case "mcp_initialized":
19
+ return { event: event.event, runId: event.runId };
20
+ case "mcp_tools_list":
21
+ return { event: event.event, runId: event.runId, toolCount: event.toolCount };
18
22
  case "run_created":
19
23
  return {
20
24
  event: event.event,
@@ -1,8 +1,8 @@
1
1
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
2
2
  import { resolveCursorMcpToolTimeoutMs } from "./cursor-mcp-timeout-override.js";
3
3
  export const CURSOR_PI_TOOL_BRIDGE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE";
4
- export const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
5
- export const CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV = "PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS";
4
+ const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
5
+ const CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV = "PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS";
6
6
  export function resolveCursorPiToolBridgeEnabled(env = process.env) {
7
7
  return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_ENV], true);
8
8
  }
@@ -16,3 +16,26 @@ export function resolveCursorPiToolBridgeCallTimeoutMs(env = process.env) {
16
16
  return mcpToolTimeoutMs;
17
17
  return Math.min(Math.max(Math.trunc(parsed), 1), mcpToolTimeoutMs);
18
18
  }
19
+ const CURSOR_PI_TOOL_BRIDGE_HOST_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_HOST";
20
+ const CURSOR_PI_TOOL_BRIDGE_PORT_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_PORT";
21
+ const DEFAULT_URL_HOST = "127.0.0.1";
22
+ const PORT_PATTERN = /^\d{1,5}$/;
23
+ export function resolveCursorPiToolBridgeUrlHost(env = process.env) {
24
+ return env[CURSOR_PI_TOOL_BRIDGE_HOST_ENV]?.trim().toLowerCase() === "localhost" ? "localhost" : DEFAULT_URL_HOST;
25
+ }
26
+ function parsePort(text) {
27
+ if (!PORT_PATTERN.test(text))
28
+ return undefined;
29
+ const port = Number(text);
30
+ return port >= 1 && port <= 65535 ? port : undefined;
31
+ }
32
+ export function resolveCursorPiToolBridgePortRange(env = process.env) {
33
+ const parts = env[CURSOR_PI_TOOL_BRIDGE_PORT_ENV]?.trim().split("-");
34
+ if (!parts || parts.length > 2)
35
+ return undefined;
36
+ const first = parsePort(parts[0]);
37
+ const last = parts.length === 2 ? parsePort(parts[1]) : first;
38
+ if (first === undefined || last === undefined || first > last)
39
+ return undefined;
40
+ return { first, last };
41
+ }
@@ -208,9 +208,16 @@ export class CursorPiToolBridgeRunImpl {
208
208
  const transport = new WebStandardStreamableHTTPServerTransport({
209
209
  sessionIdGenerator: randomUUID,
210
210
  });
211
- server.setRequestHandler("tools/list", async () => ({
212
- tools: this.snapshot.tools.map(snapshotToolToMcpTool),
213
- }));
211
+ // ponytail: server-side receipts only, not SDK/model consumption or ongoing
212
+ // connection health; upgrade when Cursor exposes public MCP lifecycle callbacks.
213
+ server.oninitialized = () => {
214
+ this.emitDiagnostic({ event: "mcp_initialized", runId: this.id });
215
+ };
216
+ server.setRequestHandler("tools/list", async () => {
217
+ const tools = this.snapshot.tools.map(snapshotToolToMcpTool);
218
+ this.emitDiagnostic({ event: "mcp_tools_list", runId: this.id, toolCount: tools.length });
219
+ return { tools };
220
+ });
214
221
  server.setRequestHandler("tools/call", async (request, context) => {
215
222
  return this.enqueueToolRequest(request.params.name, request.params.arguments, String(context.mcpReq.id), context.mcpReq.signal);
216
223
  });
@@ -1,15 +1,57 @@
1
1
  import { createServer } from "node:http";
2
+ import { resolveCursorPiToolBridgePortRange, resolveCursorPiToolBridgeUrlHost, } from "./cursor-pi-tool-bridge-env.js";
2
3
  import { asRecord } from "./cursor-record-utils.js";
3
4
  import { CursorPiToolBridgeRunImpl } from "./cursor-pi-tool-bridge-run.js";
4
5
  import { buildCursorPiToolBridgeSnapshot, buildCursorPiToolBridgeSurfaceSignature, createEmptySnapshot, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-snapshot.js";
5
- export const LOOPBACK_HOST = "127.0.0.1";
6
+ const LOOPBACK_HOST = "127.0.0.1";
6
7
  const HTTP_SERVER_CLOSE_GRACE_MS = 250;
8
+ const EPHEMERAL_BIND_ATTEMPTS = 10;
9
+ function listenOnce(server, port, host = LOOPBACK_HOST) {
10
+ return new Promise((resolve, reject) => {
11
+ const onError = (error) => {
12
+ server.off("listening", onListening);
13
+ reject(error);
14
+ };
15
+ const onListening = () => {
16
+ server.off("error", onError);
17
+ resolve();
18
+ };
19
+ server.once("error", onError);
20
+ server.once("listening", onListening);
21
+ server.listen({ port, host, ipv6Only: host === "::1" });
22
+ });
23
+ }
24
+ async function closeServers(servers) {
25
+ await Promise.all(servers.map(async (server) => {
26
+ if (!server.listening)
27
+ return;
28
+ await new Promise((resolve, reject) => {
29
+ let settled = false;
30
+ const settle = (error) => {
31
+ if (settled)
32
+ return;
33
+ settled = true;
34
+ clearTimeout(closeTimer);
35
+ if (error)
36
+ reject(error);
37
+ else
38
+ resolve();
39
+ };
40
+ const closeTimer = setTimeout(() => settle(), HTTP_SERVER_CLOSE_GRACE_MS);
41
+ closeTimer.unref?.();
42
+ server.close((error) => settle(error ?? undefined));
43
+ server.closeIdleConnections();
44
+ server.closeAllConnections();
45
+ });
46
+ }));
47
+ }
7
48
  export class CursorPiToolBridgeRegistry {
8
49
  pi;
9
50
  env;
10
51
  runs = new Set();
11
52
  routes = new Map();
12
- httpServer;
53
+ httpServers = [];
54
+ advertisedHost;
13
55
  listenPromise;
14
56
  serverLifecycle = Promise.resolve();
15
57
  constructor(pi, env = process.env) {
@@ -36,7 +78,17 @@ export class CursorPiToolBridgeRegistry {
36
78
  : createEmptySnapshot();
37
79
  const run = new CursorPiToolBridgeRunImpl(this, this.env, snapshot, bridgeEnabled && snapshot.tools.length > 0, options);
38
80
  this.runs.add(run);
39
- await run.start();
81
+ try {
82
+ await run.start();
83
+ }
84
+ catch (error) {
85
+ try {
86
+ await run.dispose();
87
+ }
88
+ catch { /* Preserve the original startup failure, not a secondary cleanup error. */ }
89
+ this.runs.delete(run);
90
+ throw error;
91
+ }
40
92
  run.emitStartDiagnostics(bridgeEnabled);
41
93
  return run;
42
94
  }
@@ -51,9 +103,9 @@ export class CursorPiToolBridgeRegistry {
51
103
  await this.ensureHttpServer();
52
104
  this.routes.set(pathname, run);
53
105
  const address = this.getHttpServerAddress();
54
- if (!address)
106
+ if (!address || !this.advertisedHost)
55
107
  throw new Error("Cursor pi tool bridge HTTP server is not listening");
56
- return `http://${LOOPBACK_HOST}:${address.port}${pathname}`;
108
+ return `http://${this.advertisedHost}:${address.port}${pathname}`;
57
109
  });
58
110
  }
59
111
  async unregisterRun(pathname, run) {
@@ -71,7 +123,7 @@ export class CursorPiToolBridgeRegistry {
71
123
  return result;
72
124
  }
73
125
  getHttpServerAddress() {
74
- const address = asRecord(this.httpServer?.address());
126
+ const address = asRecord(this.httpServers[0]?.address());
75
127
  if (typeof address?.port !== "number")
76
128
  return undefined;
77
129
  return {
@@ -99,13 +151,17 @@ export class CursorPiToolBridgeRegistry {
99
151
  }
100
152
  async ensureHttpServer() {
101
153
  this.listenPromise ??= this.startHttpServer().catch((error) => {
102
- this.httpServer = undefined;
154
+ this.httpServers = [];
155
+ this.advertisedHost = undefined;
103
156
  this.listenPromise = undefined;
104
157
  throw error;
105
158
  });
106
159
  await this.listenPromise;
107
160
  }
108
161
  async startHttpServer() {
162
+ // A hostname is a listener policy, not a mutable per-run URL preference.
163
+ const advertisedHost = resolveCursorPiToolBridgeUrlHost(this.env);
164
+ const range = resolveCursorPiToolBridgePortRange(this.env);
109
165
  const [{ getRequestListener }, { createMcpHonoApp }] = await Promise.all([
110
166
  import("@hono/node-server"),
111
167
  import("@modelcontextprotocol/hono"),
@@ -115,51 +171,45 @@ export class CursorPiToolBridgeRegistry {
115
171
  const parsedBody = context.get("parsedBody");
116
172
  return this.handleHttpRequest(context.req.raw, parsedBody);
117
173
  });
118
- const server = createServer(getRequestListener(app.fetch, { overrideGlobalObjects: false }));
119
- this.httpServer = server;
120
- await new Promise((resolve, reject) => {
121
- const onError = (error) => {
122
- server.off("listening", onListening);
123
- reject(error);
124
- };
125
- const onListening = () => {
126
- server.off("error", onError);
127
- resolve();
128
- };
129
- server.once("error", onError);
130
- server.once("listening", onListening);
131
- server.listen(0, LOOPBACK_HOST);
132
- });
174
+ const listener = getRequestListener(app.fetch, { overrideGlobalObjects: false });
175
+ const attempts = range ? range.last - range.first + 1 : advertisedHost === "localhost" ? EPHEMERAL_BIND_ATTEMPTS : 1;
176
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
177
+ const servers = [createServer(listener)];
178
+ try {
179
+ await listenOnce(servers[0], range ? range.first + attempt : 0);
180
+ if (advertisedHost === "localhost") {
181
+ const port = servers[0].address().port;
182
+ const ipv6 = createServer(listener);
183
+ servers.push(ipv6);
184
+ // Own both explicit loopbacks before publishing any tokenized URL.
185
+ await listenOnce(ipv6, port, "::1");
186
+ }
187
+ this.httpServers = servers;
188
+ this.advertisedHost = advertisedHost;
189
+ return;
190
+ }
191
+ catch (error) {
192
+ try {
193
+ await closeServers(servers);
194
+ }
195
+ catch {
196
+ throw error;
197
+ } // Never retry or replace the bind failure when rollback fails.
198
+ if (error.code !== "EADDRINUSE" || (!range && advertisedHost !== "localhost"))
199
+ throw error;
200
+ }
201
+ }
202
+ if (range)
203
+ throw new Error(`Cursor pi tool bridge could not listen on any port in ${range.first}-${range.last} (all in use)`);
204
+ throw new Error("Cursor pi tool bridge could not own both localhost loopback listeners on an ephemeral port");
133
205
  }
134
206
  async closeHttpServer() {
135
207
  await this.listenPromise?.catch(() => undefined);
136
- const server = this.httpServer;
137
- if (!server)
138
- return;
139
- this.httpServer = undefined;
208
+ const servers = this.httpServers;
209
+ this.httpServers = [];
210
+ this.advertisedHost = undefined;
140
211
  this.listenPromise = undefined;
141
- await new Promise((resolve, reject) => {
142
- let settled = false;
143
- let closeTimer;
144
- const settle = (error) => {
145
- if (settled)
146
- return;
147
- settled = true;
148
- if (closeTimer)
149
- clearTimeout(closeTimer);
150
- if (error)
151
- reject(error);
152
- else
153
- resolve();
154
- };
155
- closeTimer = setTimeout(() => settle(), HTTP_SERVER_CLOSE_GRACE_MS);
156
- closeTimer.unref?.();
157
- server.close((error) => {
158
- settle(error ?? undefined);
159
- });
160
- server.closeIdleConnections();
161
- server.closeAllConnections();
162
- });
212
+ await closeServers(servers);
163
213
  }
164
214
  async handleHttpRequest(request, parsedBody) {
165
215
  const url = new URL(request.url);
@@ -1,5 +1,5 @@
1
1
  import { createMcpToolName, normalizeMcpInputSchema, stableNameHash } from "./cursor-pi-tool-bridge-mcp.js";
2
- export { CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV, CURSOR_PI_TOOL_BRIDGE_ENV, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-env.js";
2
+ export { CURSOR_PI_TOOL_BRIDGE_ENV, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-env.js";
3
3
  import { isRegisteredCursorNativeToolName } from "./cursor-native-tool-display-state.js";
4
4
  import { isExcludedFromCursorBridgeExposure } from "./cursor-tool-presentation-registry.js";
5
5
  const OVERLAPPING_CURSOR_NATIVE_PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "write", "edit", "grep", "find", "ls"]);
@@ -1,11 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV, CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX, serializeCursorPiToolBridgeDiagnostic, } from "./cursor-pi-tool-bridge-diagnostics.js";
3
- import { CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV, CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV, CURSOR_PI_TOOL_BRIDGE_ENV, } from "./cursor-pi-tool-bridge-env.js";
2
+ import { CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX, serializeCursorPiToolBridgeDiagnostic, } from "./cursor-pi-tool-bridge-diagnostics.js";
4
3
  import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
5
- import { isCursorPiBridgeToolCallId, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
6
- import { LOOPBACK_HOST, CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
4
+ import { isCursorPiBridgeToolCallId } from "./cursor-pi-tool-bridge-constants.js";
5
+ import { CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
7
6
  export { resolveCursorPiToolBridgeDebugEnabled } from "./cursor-pi-tool-bridge-diagnostics.js";
8
- export { CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV, CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV, CURSOR_PI_TOOL_BRIDGE_ENV, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeCallTimeoutMs, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-env.js";
7
+ export { resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeCallTimeoutMs, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-env.js";
9
8
  export { buildCursorPiToolBridgeSnapshot, buildCursorPiToolBridgeSurfaceSignature, } from "./cursor-pi-tool-bridge-snapshot.js";
10
9
  const bridges = new Map();
11
10
  const activeCallsByPi = new Map();
@@ -110,13 +109,7 @@ export function getRegisteredCursorPiToolBridge(pi) {
110
109
  return pi ? bridges.get(pi) : registeredCursorPiToolBridge;
111
110
  }
112
111
  export const __testUtils = {
113
- CURSOR_PI_TOOL_BRIDGE_ENV,
114
- CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
115
- CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
116
- CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
117
112
  CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
118
- LOOPBACK_HOST,
119
- MCP_SERVER_NAME,
120
113
  createRegistry(pi, env = process.env) {
121
114
  return new CursorPiToolBridgeRegistry(pi, env);
122
115
  },
@@ -130,7 +123,6 @@ export const __testUtils = {
130
123
  return bridgeToolExecutionAbortTracker.getActiveCount();
131
124
  },
132
125
  buildWindowsBridgeBashAbortCommandForTests: buildWindowsBridgeBashAbortCommand,
133
- installWindowsBridgeBashAbortMarkerForTests: installWindowsBridgeBashAbortMarker,
134
126
  emitBridgeToolExecutionProcessAbortSignalForTests(signal) {
135
127
  bridgeToolExecutionAbortTracker.emitProcessAbortSignalForTests(signal);
136
128
  },
@@ -140,7 +140,7 @@ function isCursorSdkStallAbortNetworkError(code, evidence, stackEvidence) {
140
140
  /\b(?:onStall|reportStall|StallDetected)\b/i.test(stackEvidence));
141
141
  }
142
142
  /**
143
- * @cursor/sdk@1.0.36 throws internal RetriableError (name/kind "RetriableError") with message
143
+ * @cursor/sdk@1.0.37 throws internal RetriableError (name/kind "RetriableError") with message
144
144
  * "Connection stalled" or "Connection stalled repeatedly" after fetchWithRetry exhausts stalls.
145
145
  * The ConnectError is only the cause; the top-level error is not a ConnectError.
146
146
  */
@@ -192,7 +192,7 @@ export function classifyCursorConnectError(error) {
192
192
  }
193
193
  const directAbort = (code === 1 || code === "canceled") &&
194
194
  (causeName === "AbortError" || /AbortError/.test(stack));
195
- // The vendored 1.0.36 transport wraps a canceled ConnectError as Unknown.
195
+ // The vendored SDK transport can wrap a canceled ConnectError as Unknown.
196
196
  // Require the observed canceled -> AbortError cause chain, not generic text.
197
197
  const wrappedAbort = (code === 2 || code === "unknown") &&
198
198
  causeName === "ConnectError" &&
@@ -214,12 +214,6 @@ export function classifyCursorConnectError(error) {
214
214
  }
215
215
  return undefined;
216
216
  }
217
- export function isCursorSdkAbortConnectError(error) {
218
- return classifyCursorConnectError(error)?.kind === "abort";
219
- }
220
- export function isUnauthenticatedConnectError(error) {
221
- return classifyCursorConnectError(error)?.kind === "unauthenticated";
222
- }
223
217
  function isLikelyNetworkTimeout(message) {
224
218
  return (/\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENETUNREACH|EAI_AGAIN|NGHTTP2_ENHANCE_YOUR_CALM|ERR_HTTP2_STREAM_ERROR|ERR_HTTP2_SESSION_ERROR)\b/i.test(message) ||
225
219
  /\bConnectError\b.*\b(unavailable|deadline|timeout|timed out)\b/i.test(message) ||
@@ -9,7 +9,7 @@ import { trimCurrentTurnAlreadyEmittedCursorText } from "./cursor-run-final-text
9
9
  import { formatCursorSdkAbortMessage, resolveCursorSdkAbortCause } from "./cursor-provider-errors.js";
10
10
  import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
11
11
  import { partitionNativeToolsByActiveContext } from "./cursor-native-replay-routing.js";
12
- export const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
12
+ const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
13
13
  function parseCursorNativeReplayIdleDisposeMs(raw) {
14
14
  if (!raw || /[^0-9]/.test(raw))
15
15
  return DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
@@ -36,7 +36,7 @@ export function createCursorNativeReplayId() {
36
36
  function getCursorNativeReplayIdFromToolCallId(toolCallId) {
37
37
  return CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN.exec(toolCallId)?.[1];
38
38
  }
39
- export function getPendingCursorLiveRun(context, scopeKey) {
39
+ function getPendingCursorLiveRun(context, scopeKey) {
40
40
  return cursorLiveRuns.getPendingFromContext(context, getCursorNativeReplayIdFromToolCallId, scopeKey);
41
41
  }
42
42
  export function getActiveCursorLiveRunForCurrentScope(scopeKey) {
@@ -357,7 +357,7 @@ export async function drainCursorLiveRunTurn(stream, partial, model, context, ru
357
357
  });
358
358
  }
359
359
  }
360
- export async function drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, signal, turnDebugRecorder, scopeKey, occupancyFloor) {
360
+ export async function drainExistingCursorLiveRunBeforeSend(stream, partial, model, context, signal, turnDebugRecorder, scopeKey, occupancyFloor, nativeToolsEnabled = true) {
361
361
  turnDebugRecorder?.recordDrainEvent("pre_send_start", {});
362
362
  while (true) {
363
363
  const run = getPendingCursorLiveRun(context, scopeKey) ?? getActiveCursorLiveRunForCurrentScope(scopeKey);
@@ -369,6 +369,11 @@ export async function drainExistingCursorLiveRunBeforeSend(stream, partial, mode
369
369
  const outcome = await cursorLiveRuns.withRunLease(run, signal, async () => {
370
370
  if (run.disposed)
371
371
  return "continue_send";
372
+ // Check under the drain lease, before consuming/delivering Pi results:
373
+ // resolving a bridge waiter can resume the SDK's old tool surface.
374
+ if ((run.nativeToolsEnabled !== false) !== nativeToolsEnabled) {
375
+ 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.");
376
+ }
372
377
  const consumed = cursorLiveRuns.consumeToolResults(run, context, getCursorNativeReplayIdFromToolCallId);
373
378
  await run.bridgeRun?.resolveToolResults(consumed.toolResults);
374
379
  const shouldChainUserInput = run.chainUserInputAfterCompletion || hasTrailingUserMessagesAfterToolResults(context);
@@ -12,7 +12,7 @@ import { CURSOR_PROVIDER } from "./cursor-model.js";
12
12
  *
13
13
  * See pi `docs/custom-provider.md` -> "Context Overflow Errors".
14
14
  */
15
- export const CURSOR_OVERFLOW_MARKER = "context_length_exceeded";
15
+ const CURSOR_OVERFLOW_MARKER = "context_length_exceeded";
16
16
  /**
17
17
  * Context-overflow phrases. These overlap pi's own `OVERFLOW_PATTERNS` so the
18
18
  * false-positive surface matches pi's built-in detection for other providers.
@@ -37,9 +37,9 @@ const CURSOR_THROTTLE_PATTERN = /too many requests|rate.?limit|throttl|retry.?af
37
37
  * Map a finalized Cursor assistant `errorMessage` to pi's overflow form.
38
38
  * Returns `undefined` when the message is not a Cursor context-overflow failure.
39
39
  *
40
- * Pure and idempotent so it is safe to call repeatedly and unit-test in isolation.
40
+ * Idempotent so repeated message delivery does not add another prefix.
41
41
  */
42
- export function normalizeCursorOverflowErrorMessage(errorMessage) {
42
+ function normalizeCursorOverflowErrorMessage(errorMessage) {
43
43
  const message = errorMessage?.trim();
44
44
  if (!message)
45
45
  return undefined;
@@ -53,22 +53,6 @@ export function normalizeCursorOverflowErrorMessage(errorMessage) {
53
53
  }
54
54
  return undefined;
55
55
  }
56
- /**
57
- * Rewrite a finalized assistant message into pi's overflow form when it is a
58
- * Cursor context-overflow failure. Returns the replacement message (for the
59
- * `message_end` result) or `undefined` to leave it unchanged.
60
- *
61
- * `isCursorProvider` carries the provider scoping decision out of the handler so
62
- * the full logic is unit-testable without a pi event harness.
63
- */
64
- export function rewriteCursorOverflowAssistantMessage(message, isCursorProvider) {
65
- if (!isCursorProvider || message.stopReason !== "error")
66
- return undefined;
67
- const rewritten = normalizeCursorOverflowErrorMessage(message.errorMessage);
68
- if (!rewritten)
69
- return undefined;
70
- return { ...message, errorMessage: rewritten };
71
- }
72
56
  /**
73
57
  * Register a `message_end` handler that rewrites Cursor context-overflow
74
58
  * failures into the `context_length_exceeded` form pi auto-compacts on.
@@ -82,9 +66,11 @@ export function registerCursorOverflowNormalization(pi) {
82
66
  if (message.role !== "assistant")
83
67
  return undefined;
84
68
  const isCursorProvider = message.provider === CURSOR_PROVIDER || ctx.model?.provider === CURSOR_PROVIDER;
85
- const rewritten = rewriteCursorOverflowAssistantMessage(message, isCursorProvider);
69
+ if (!isCursorProvider || message.stopReason !== "error")
70
+ return undefined;
71
+ const rewritten = normalizeCursorOverflowErrorMessage(message.errorMessage);
86
72
  if (!rewritten)
87
73
  return undefined;
88
- return { message: rewritten };
74
+ return { message: { ...message, errorMessage: rewritten } };
89
75
  });
90
76
  }
@@ -24,6 +24,7 @@ import { MISSING_CURSOR_API_KEY_MESSAGE } from "./cursor-provider-errors.js";
24
24
  import { CursorSdkTurnCoordinator } from "./cursor-provider-turn-coordinator.js";
25
25
  import { resolveCursorApiKey } from "./cursor-api-key.js";
26
26
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
27
+ import { assertCursorNativeToolsRuntime } from "./cursor-native-tools.js";
27
28
  import { prepareCursorSummaryProviderTurn } from "./cursor-provider-turn-summary.js";
28
29
  function buildCursorCloudPromptContext(context, handoff, nativeSourceRoles) {
29
30
  if (handoff === "bootstrap")
@@ -182,7 +183,7 @@ async function prepareCursorCloudProviderTurn(prepareParams) {
182
183
  }
183
184
  }
184
185
  async function prepareCursorLocalProviderTurn(prepareParams) {
185
- const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled, agents, customSubagentNames } = prepareParams;
186
+ const { params, cwd, resolvedApiKey, sdkEventDebug, throwIfAborted, resolvedConfig, agentMode, selection, fastEnabled, agents, customSubagentNames, nativeToolsEnabled } = prepareParams;
186
187
  const { model, context, options } = params;
187
188
  let restoreCursorSdkOutputFilter;
188
189
  let sessionAgentScopeKey;
@@ -212,6 +213,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
212
213
  settingSources,
213
214
  localSafety,
214
215
  agents,
216
+ nativeToolsEnabled,
215
217
  localResume: resolvedConfig.local.resume.value,
216
218
  storeRoot: resolvedConfig.local.storeRoot.value,
217
219
  useHttp1ForAgent,
@@ -236,6 +238,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
236
238
  ...getCursorPromptOptions(model),
237
239
  nativeSourceRoles: params.request.nativeSourceRoles,
238
240
  agentMode,
241
+ nativeToolsEnabled,
239
242
  includePiBridgeGuidance,
240
243
  includePiAskQuestionGuidance: false,
241
244
  };
@@ -245,6 +248,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
245
248
  return {
246
249
  ...promptOptions,
247
250
  toolManifest: buildCursorToolManifestText({
251
+ nativeToolsEnabled,
248
252
  bridgeSnapshot: sessionAgentLease.bridgeRun?.snapshot,
249
253
  piBridgeEnabled: resolveCursorPiToolBridgeEnabled(),
250
254
  includePiBridgeGuidance,
@@ -295,6 +299,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
295
299
  agentMode,
296
300
  localForce: resolvedConfig.local.force.value,
297
301
  localResume: resolvedConfig.local.resume.value,
302
+ ...(nativeToolsEnabled ? {} : { nativeToolsEnabled: false, tools: ["mcp"] }),
298
303
  resumedAgent: sessionAgentLease.resumed,
299
304
  activeToolNames: activeToolNames ? [...activeToolNames] : [],
300
305
  sessionAgentScopeKey,
@@ -308,6 +313,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
308
313
  ? cursorLiveRuns.start({
309
314
  id: useNativeToolReplay ? nativeReplayId : bridgeRun?.id ?? nativeReplayId,
310
315
  agent,
316
+ nativeToolsEnabled,
311
317
  bridgeRun,
312
318
  sessionBridgeRun,
313
319
  sessionAgentScopeKey,
@@ -387,6 +393,7 @@ async function prepareCursorLocalProviderTurn(prepareParams) {
387
393
  export async function prepareCursorProviderTurn(prepareParams) {
388
394
  const { params, resolvedConfig } = prepareParams;
389
395
  const { model, options } = params;
396
+ assertCursorNativeToolsRuntime(resolvedConfig.runtime.value, prepareParams.nativeToolsEnabled);
390
397
  const fastEnabled = resolvedConfig.runtime.value === "cloud" ? undefined : getEffectiveFastForModelId(model.id, params.scope.scopeKey);
391
398
  const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
392
399
  if (resolvedConfig.runtime.value === "local" && params.request.purpose !== "normal") {