pi-cursor-sdk 0.5.3 → 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 (94) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +62 -816
  3. package/dist/context.js +11 -5
  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-fallback-models.generated.js +264 -1
  10. package/dist/cursor-live-run-coordinator.js +1 -0
  11. package/dist/cursor-mcp-timeout-override.js +6 -17
  12. package/dist/cursor-native-tool-display-replay.js +5 -5
  13. package/dist/cursor-native-tools.js +11 -0
  14. package/dist/cursor-pi-tool-bridge-env.js +25 -2
  15. package/dist/cursor-pi-tool-bridge-server.js +98 -48
  16. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  17. package/dist/cursor-pi-tool-bridge.js +4 -12
  18. package/dist/cursor-provider-errors.js +0 -6
  19. package/dist/cursor-provider-live-run-drain.js +8 -3
  20. package/dist/cursor-provider-overflow.js +7 -21
  21. package/dist/cursor-provider-turn-prepare.js +8 -1
  22. package/dist/cursor-provider-turn-runner.js +17 -6
  23. package/dist/cursor-provider.js +1 -4
  24. package/dist/cursor-runtime-state.js +3 -3
  25. package/dist/cursor-sdk-event-debug.js +2 -2
  26. package/dist/cursor-sdk-output-filter.js +1 -1
  27. package/dist/cursor-sdk-process-error-guard.js +1 -2
  28. package/dist/cursor-session-agent.js +6 -8
  29. package/dist/cursor-session-scope.js +1 -7
  30. package/dist/cursor-session-store.js +1 -1
  31. package/dist/cursor-state.js +9 -23
  32. package/dist/cursor-tool-lifecycle.js +0 -3
  33. package/dist/cursor-tool-manifest.js +3 -1
  34. package/dist/cursor-tool-presentation-registry.js +2 -4
  35. package/dist/cursor-tool-visibility.js +0 -3
  36. package/dist/cursor-transcript-tool-specs.js +1 -2
  37. package/dist/index.js +21 -0
  38. package/dist/model-discovery.js +0 -1
  39. package/dist/model-list-cache.js +4 -9
  40. package/docs/cursor-live-smoke-checklist.md +3 -3
  41. package/docs/cursor-model-ux-spec.md +16 -8
  42. package/docs/cursor-native-tool-replay.md +2 -2
  43. package/docs/cursor-native-tool-visual-audit.md +1 -1
  44. package/docs/cursor-testing-lessons.md +15 -3
  45. package/docs/cursor-tool-surfaces.md +32 -3
  46. package/docs/development.md +93 -0
  47. package/docs/platform-smoke-implementation.md +3 -2
  48. package/docs/reference.md +806 -0
  49. package/package.json +6 -4
  50. package/scripts/platform-smoke/artifact-secrets.mjs +13 -4
  51. package/scripts/platform-smoke/local-resume-runner.mjs +0 -1
  52. package/scripts/platform-smoke/target-runtime.mjs +4 -2
  53. package/scripts/platform-smoke/targets.mjs +0 -1
  54. package/shared/cursor-sdk-output-filter.d.mts +2 -0
  55. package/shared/cursor-sdk-output-filter.mjs +92 -28
  56. package/shared/cursor-sensitive-text.d.mts +1 -0
  57. package/shared/cursor-sensitive-text.mjs +7 -0
  58. package/src/context.ts +16 -9
  59. package/src/cursor-agents-context.ts +6 -6
  60. package/src/cursor-api-key.ts +1 -1
  61. package/src/cursor-bridge-contract.ts +1 -15
  62. package/src/cursor-cloud-local-state.ts +0 -2
  63. package/src/cursor-config.ts +1 -24
  64. package/src/cursor-fallback-models.generated.ts +264 -1
  65. package/src/cursor-live-run-coordinator.ts +5 -0
  66. package/src/cursor-mcp-timeout-override.ts +6 -30
  67. package/src/cursor-native-tool-display-replay.ts +4 -6
  68. package/src/cursor-native-tools.ts +14 -0
  69. package/src/cursor-pi-tool-bridge-env.ts +31 -2
  70. package/src/cursor-pi-tool-bridge-server.ts +89 -45
  71. package/src/cursor-pi-tool-bridge-snapshot.ts +0 -1
  72. package/src/cursor-pi-tool-bridge.ts +2 -18
  73. package/src/cursor-provider-errors.ts +0 -8
  74. package/src/cursor-provider-live-run-drain.ts +8 -2
  75. package/src/cursor-provider-overflow.ts +6 -24
  76. package/src/cursor-provider-turn-prepare.ts +10 -1
  77. package/src/cursor-provider-turn-runner.ts +17 -6
  78. package/src/cursor-provider.ts +0 -5
  79. package/src/cursor-runtime-state.ts +3 -3
  80. package/src/cursor-sdk-event-debug.ts +2 -2
  81. package/src/cursor-sdk-output-filter.ts +2 -1
  82. package/src/cursor-sdk-process-error-guard.ts +1 -3
  83. package/src/cursor-session-agent.ts +8 -8
  84. package/src/cursor-session-scope.ts +1 -9
  85. package/src/cursor-session-store.ts +1 -1
  86. package/src/cursor-state.ts +9 -36
  87. package/src/cursor-tool-lifecycle.ts +0 -4
  88. package/src/cursor-tool-manifest.ts +4 -1
  89. package/src/cursor-tool-presentation-registry.ts +3 -5
  90. package/src/cursor-tool-visibility.ts +0 -4
  91. package/src/cursor-transcript-tool-specs.ts +0 -3
  92. package/src/index.ts +18 -0
  93. package/src/model-discovery.ts +0 -1
  94. package/src/model-list-cache.ts +4 -10
@@ -1,6 +1,6 @@
1
1
  import type { ModelListItem } from "@cursor/sdk";
2
2
 
3
- // Generated with @cursor/sdk@1.0.37 from 46 Cursor models.
3
+ // Generated with @cursor/sdk@1.0.37 from 48 Cursor models.
4
4
  // Refresh with: npm run refresh:cursor-snapshots -- --write
5
5
  // Do not add secrets; this file stores public model metadata only.
6
6
  export const FALLBACK_MODEL_ITEMS = [
@@ -6518,6 +6518,60 @@ export const FALLBACK_MODEL_ITEMS = [
6518
6518
  }
6519
6519
  ]
6520
6520
  },
6521
+ {
6522
+ id: "gemini-4-argon",
6523
+ displayName: "Gemini 4 Argon",
6524
+ parameters: [
6525
+ {
6526
+ id: "reasoning_effort",
6527
+ displayName: "Effort",
6528
+ values: [
6529
+ {
6530
+ value: "low",
6531
+ displayName: "Low"
6532
+ },
6533
+ {
6534
+ value: "medium",
6535
+ displayName: "Medium"
6536
+ },
6537
+ {
6538
+ value: "high",
6539
+ displayName: "High"
6540
+ }
6541
+ ]
6542
+ }
6543
+ ],
6544
+ variants: [
6545
+ {
6546
+ params: [
6547
+ {
6548
+ id: "reasoning_effort",
6549
+ value: "low"
6550
+ }
6551
+ ],
6552
+ displayName: "Gemini 4 Argon Low"
6553
+ },
6554
+ {
6555
+ params: [
6556
+ {
6557
+ id: "reasoning_effort",
6558
+ value: "medium"
6559
+ }
6560
+ ],
6561
+ displayName: "Gemini 4 Argon Medium",
6562
+ isDefault: true
6563
+ },
6564
+ {
6565
+ params: [
6566
+ {
6567
+ id: "reasoning_effort",
6568
+ value: "high"
6569
+ }
6570
+ ],
6571
+ displayName: "Gemini 4 Argon High"
6572
+ }
6573
+ ]
6574
+ },
6521
6575
  {
6522
6576
  id: "glm-5.2",
6523
6577
  displayName: "GLM 5.2",
@@ -10157,5 +10211,214 @@ export const FALLBACK_MODEL_ITEMS = [
10157
10211
  displayName: "Muse Spark 1.3"
10158
10212
  }
10159
10213
  ]
10214
+ },
10215
+ {
10216
+ id: "muse-wonder-1.0",
10217
+ displayName: "Muse Wonder",
10218
+ parameters: [
10219
+ {
10220
+ id: "context",
10221
+ displayName: "Context",
10222
+ values: [
10223
+ {
10224
+ value: "300k",
10225
+ displayName: "300K"
10226
+ },
10227
+ {
10228
+ value: "1m",
10229
+ displayName: "1M"
10230
+ }
10231
+ ]
10232
+ },
10233
+ {
10234
+ id: "reasoning_effort",
10235
+ displayName: "Reasoning",
10236
+ values: [
10237
+ {
10238
+ value: "minimal",
10239
+ displayName: "Minimal"
10240
+ },
10241
+ {
10242
+ value: "low",
10243
+ displayName: "Low"
10244
+ },
10245
+ {
10246
+ value: "medium",
10247
+ displayName: "Medium"
10248
+ },
10249
+ {
10250
+ value: "high",
10251
+ displayName: "High"
10252
+ },
10253
+ {
10254
+ value: "xhigh",
10255
+ displayName: "Extra High"
10256
+ },
10257
+ {
10258
+ value: "max",
10259
+ displayName: "Max"
10260
+ }
10261
+ ]
10262
+ }
10263
+ ],
10264
+ variants: [
10265
+ {
10266
+ params: [
10267
+ {
10268
+ id: "context",
10269
+ value: "300k"
10270
+ },
10271
+ {
10272
+ id: "reasoning_effort",
10273
+ value: "minimal"
10274
+ }
10275
+ ],
10276
+ displayName: "Muse Wonder Minimal"
10277
+ },
10278
+ {
10279
+ params: [
10280
+ {
10281
+ id: "context",
10282
+ value: "300k"
10283
+ },
10284
+ {
10285
+ id: "reasoning_effort",
10286
+ value: "low"
10287
+ }
10288
+ ],
10289
+ displayName: "Muse Wonder Low"
10290
+ },
10291
+ {
10292
+ params: [
10293
+ {
10294
+ id: "context",
10295
+ value: "300k"
10296
+ },
10297
+ {
10298
+ id: "reasoning_effort",
10299
+ value: "medium"
10300
+ }
10301
+ ],
10302
+ displayName: "Muse Wonder Medium"
10303
+ },
10304
+ {
10305
+ params: [
10306
+ {
10307
+ id: "context",
10308
+ value: "300k"
10309
+ },
10310
+ {
10311
+ id: "reasoning_effort",
10312
+ value: "high"
10313
+ }
10314
+ ],
10315
+ displayName: "Muse Wonder High"
10316
+ },
10317
+ {
10318
+ params: [
10319
+ {
10320
+ id: "context",
10321
+ value: "300k"
10322
+ },
10323
+ {
10324
+ id: "reasoning_effort",
10325
+ value: "xhigh"
10326
+ }
10327
+ ],
10328
+ displayName: "Muse Wonder Extra High"
10329
+ },
10330
+ {
10331
+ params: [
10332
+ {
10333
+ id: "context",
10334
+ value: "300k"
10335
+ },
10336
+ {
10337
+ id: "reasoning_effort",
10338
+ value: "max"
10339
+ }
10340
+ ],
10341
+ displayName: "Muse Wonder Max"
10342
+ },
10343
+ {
10344
+ params: [
10345
+ {
10346
+ id: "context",
10347
+ value: "1m"
10348
+ },
10349
+ {
10350
+ id: "reasoning_effort",
10351
+ value: "minimal"
10352
+ }
10353
+ ],
10354
+ displayName: "Muse Wonder 1M Minimal"
10355
+ },
10356
+ {
10357
+ params: [
10358
+ {
10359
+ id: "context",
10360
+ value: "1m"
10361
+ },
10362
+ {
10363
+ id: "reasoning_effort",
10364
+ value: "low"
10365
+ }
10366
+ ],
10367
+ displayName: "Muse Wonder 1M Low"
10368
+ },
10369
+ {
10370
+ params: [
10371
+ {
10372
+ id: "context",
10373
+ value: "1m"
10374
+ },
10375
+ {
10376
+ id: "reasoning_effort",
10377
+ value: "medium"
10378
+ }
10379
+ ],
10380
+ displayName: "Muse Wonder 1M Medium"
10381
+ },
10382
+ {
10383
+ params: [
10384
+ {
10385
+ id: "context",
10386
+ value: "1m"
10387
+ },
10388
+ {
10389
+ id: "reasoning_effort",
10390
+ value: "high"
10391
+ }
10392
+ ],
10393
+ displayName: "Muse Wonder 1M High"
10394
+ },
10395
+ {
10396
+ params: [
10397
+ {
10398
+ id: "context",
10399
+ value: "1m"
10400
+ },
10401
+ {
10402
+ id: "reasoning_effort",
10403
+ value: "xhigh"
10404
+ }
10405
+ ],
10406
+ displayName: "Muse Wonder 1M Extra High",
10407
+ isDefault: true
10408
+ },
10409
+ {
10410
+ params: [
10411
+ {
10412
+ id: "context",
10413
+ value: "1m"
10414
+ },
10415
+ {
10416
+ id: "reasoning_effort",
10417
+ value: "max"
10418
+ }
10419
+ ],
10420
+ displayName: "Muse Wonder 1M Max"
10421
+ }
10422
+ ]
10160
10423
  }
10161
10424
  ] satisfies ModelListItem[];
@@ -39,6 +39,8 @@ export interface CursorLiveSdkRun {
39
39
  export interface CursorLiveRun {
40
40
  id: string;
41
41
  agent: SDKAgent;
42
+ /** Effective ordinary main-loop policy captured when this SDK run was created. */
43
+ nativeToolsEnabled?: boolean;
42
44
  bridgeRun?: CursorPiToolBridgeRun;
43
45
  sessionBridgeRun?: CursorPiToolBridgeRun;
44
46
  sessionAgentScopeKey: string;
@@ -65,6 +67,8 @@ export interface CursorLiveRun {
65
67
  export interface CursorLiveRunCreateParams {
66
68
  id: string;
67
69
  agent: SDKAgent;
70
+ /** Effective ordinary main-loop policy captured when this SDK run was created. */
71
+ nativeToolsEnabled?: boolean;
68
72
  bridgeRun?: CursorPiToolBridgeRun;
69
73
  sessionBridgeRun?: CursorPiToolBridgeRun;
70
74
  sessionAgentScopeKey?: string;
@@ -293,6 +297,7 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
293
297
  const run: CursorLiveRun = {
294
298
  id: params.id,
295
299
  agent: params.agent,
300
+ nativeToolsEnabled: params.nativeToolsEnabled !== false,
296
301
  bridgeRun: params.bridgeRun,
297
302
  sessionBridgeRun: params.sessionBridgeRun,
298
303
  sessionAgentScopeKey,
@@ -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,7 +83,7 @@ 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
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.
@@ -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
+ }
@@ -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
+ }
@@ -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,