pi-cursor-sdk 0.2.0 → 0.3.7

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 (177) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +55 -51
  3. package/dist/bundled-context-windows.js +39 -0
  4. package/dist/context-window-cache.js +92 -0
  5. package/dist/context.js +353 -0
  6. package/dist/cursor-active-tools.js +3 -0
  7. package/dist/cursor-agent-message-web-tools.js +165 -0
  8. package/dist/cursor-agents-context-registration.js +17 -0
  9. package/dist/cursor-agents-context.js +127 -0
  10. package/dist/cursor-api-key.js +34 -0
  11. package/dist/cursor-bridge-contract.js +27 -0
  12. package/dist/cursor-cloud-lifecycle.js +650 -0
  13. package/dist/cursor-cloud-local-state.js +460 -0
  14. package/dist/cursor-cloud-options.js +145 -0
  15. package/dist/cursor-cloud-reporting.js +222 -0
  16. package/dist/cursor-compact-tool-summary.js +75 -0
  17. package/dist/cursor-config.js +613 -0
  18. package/dist/cursor-context-tools.js +6 -0
  19. package/dist/cursor-display-only-trace.js +11 -0
  20. package/dist/cursor-display-text.js +18 -0
  21. package/dist/cursor-durable-fs.js +53 -0
  22. package/dist/cursor-edit-diff.js +13 -0
  23. package/dist/cursor-env-boolean.js +19 -0
  24. package/dist/cursor-fallback-models.generated.js +7760 -0
  25. package/dist/cursor-fallback-warning.js +15 -0
  26. package/dist/cursor-http1.js +45 -0
  27. package/dist/cursor-incomplete-tool-visibility.js +83 -0
  28. package/dist/cursor-live-run-accounting.js +55 -0
  29. package/dist/cursor-live-run-coordinator.js +409 -0
  30. package/dist/cursor-mcp-timeout-override.js +116 -0
  31. package/dist/cursor-model-lifecycle.js +32 -0
  32. package/dist/cursor-model.js +5 -0
  33. package/dist/cursor-native-replay-routing.js +35 -0
  34. package/dist/cursor-native-replay-trace.js +28 -0
  35. package/dist/cursor-native-tool-display-registration.js +102 -0
  36. package/dist/cursor-native-tool-display-replay.js +428 -0
  37. package/dist/cursor-native-tool-display-state.js +70 -0
  38. package/dist/cursor-native-tool-display-tools.js +123 -0
  39. package/dist/cursor-native-tool-names.js +11 -0
  40. package/dist/cursor-partial-content-emitter.js +130 -0
  41. package/dist/cursor-pi-context.js +30 -0
  42. package/dist/cursor-pi-tool-bridge-abort.js +114 -0
  43. package/dist/cursor-pi-tool-bridge-constants.js +6 -0
  44. package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
  45. package/dist/cursor-pi-tool-bridge-env.js +18 -0
  46. package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
  47. package/dist/cursor-pi-tool-bridge-run.js +355 -0
  48. package/dist/cursor-pi-tool-bridge-server.js +163 -0
  49. package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
  50. package/dist/cursor-pi-tool-bridge-types.js +1 -0
  51. package/dist/cursor-pi-tool-bridge.js +123 -0
  52. package/dist/cursor-provider-errors.js +311 -0
  53. package/dist/cursor-provider-lazy.js +39 -0
  54. package/dist/cursor-provider-live-run-drain.js +420 -0
  55. package/dist/cursor-provider-overflow.js +90 -0
  56. package/dist/cursor-provider-run-finalizer.js +176 -0
  57. package/dist/cursor-provider-run-outcome.js +76 -0
  58. package/dist/cursor-provider-turn-coordinator.js +293 -0
  59. package/dist/cursor-provider-turn-display-router.js +158 -0
  60. package/dist/cursor-provider-turn-emit.js +41 -0
  61. package/dist/cursor-provider-turn-finalize.js +154 -0
  62. package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
  63. package/dist/cursor-provider-turn-prepare.js +379 -0
  64. package/dist/cursor-provider-turn-runner.js +146 -0
  65. package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
  66. package/dist/cursor-provider-turn-send.js +138 -0
  67. package/dist/cursor-provider-turn-shell-output.js +117 -0
  68. package/dist/cursor-provider-turn-tool-ledger.js +99 -0
  69. package/dist/cursor-provider-turn-types.js +1 -0
  70. package/dist/cursor-provider.js +72 -0
  71. package/dist/cursor-question-tool.js +194 -0
  72. package/dist/cursor-record-utils.js +61 -0
  73. package/dist/cursor-replay-activity-builders.js +114 -0
  74. package/dist/cursor-replay-source-names.js +27 -0
  75. package/dist/cursor-replay-summary-args.js +105 -0
  76. package/dist/cursor-replay-tool-details.js +167 -0
  77. package/dist/cursor-ripgrep-path.js +27 -0
  78. package/dist/cursor-run-final-text.js +63 -0
  79. package/dist/cursor-runtime-state.js +380 -0
  80. package/dist/cursor-sdk-billed-usage.js +86 -0
  81. package/dist/cursor-sdk-event-debug-constants.js +33 -0
  82. package/dist/cursor-sdk-event-debug-session.js +107 -0
  83. package/dist/cursor-sdk-event-debug.js +461 -0
  84. package/dist/cursor-sdk-output-filter.js +1 -0
  85. package/dist/cursor-sdk-process-error-guard.js +221 -0
  86. package/dist/cursor-sdk-runtime.js +3 -0
  87. package/dist/cursor-sensitive-text.js +23 -0
  88. package/dist/cursor-session-agent-cleanup.js +311 -0
  89. package/dist/cursor-session-agent-lifecycle.js +32 -0
  90. package/dist/cursor-session-agent-lineage.js +103 -0
  91. package/dist/cursor-session-agent-resume.js +417 -0
  92. package/dist/cursor-session-agent.js +507 -0
  93. package/dist/cursor-session-compaction-prep.js +21 -0
  94. package/dist/cursor-session-scope.js +113 -0
  95. package/dist/cursor-session-send-policy.js +18 -0
  96. package/dist/cursor-session-store.js +105 -0
  97. package/dist/cursor-session-turn-queue.js +50 -0
  98. package/dist/cursor-setting-sources.js +15 -0
  99. package/dist/cursor-skill-tool.js +230 -0
  100. package/dist/cursor-state.js +519 -0
  101. package/dist/cursor-task-presentation.js +59 -0
  102. package/dist/cursor-tool-lifecycle.js +94 -0
  103. package/dist/cursor-tool-manifest.js +34 -0
  104. package/dist/cursor-tool-presentation-registry.js +305 -0
  105. package/dist/cursor-tool-result-display-readers.js +158 -0
  106. package/dist/cursor-tool-transcript.js +32 -0
  107. package/dist/cursor-tool-visibility.js +28 -0
  108. package/dist/cursor-transcript-tool-formatters.js +618 -0
  109. package/dist/cursor-transcript-tool-specs.js +284 -0
  110. package/dist/cursor-transcript-utils.js +170 -0
  111. package/dist/cursor-usage-accounting.js +166 -0
  112. package/dist/cursor-web-tool-activity.js +23 -0
  113. package/dist/cursor-web-tool-args.js +12 -0
  114. package/dist/index.js +82 -0
  115. package/dist/model-discovery.js +305 -0
  116. package/dist/model-list-cache.js +156 -0
  117. package/docs/cursor-dogfood-checklist.md +2 -1
  118. package/docs/cursor-live-smoke-checklist.md +13 -12
  119. package/docs/cursor-model-ux-spec.md +27 -6
  120. package/docs/cursor-native-tool-replay.md +13 -13
  121. package/docs/cursor-native-tool-visual-audit.md +6 -3
  122. package/docs/cursor-testing-lessons.md +14 -8
  123. package/docs/cursor-tool-surfaces.md +5 -5
  124. package/docs/platform-smoke.md +3 -3
  125. package/package.json +19 -7
  126. package/platform-smoke.config.mjs +1 -1
  127. package/scripts/build.mjs +126 -0
  128. package/scripts/cloud-runtime-smoke.mjs +3 -1
  129. package/scripts/debug-provider-events.mjs +3 -1
  130. package/scripts/debug-sdk-events.mjs +1 -1
  131. package/scripts/isolated-cursor-smoke.sh +5 -4
  132. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
  133. package/scripts/lib/cursor-visual-render.mjs +1 -1
  134. package/scripts/lib/ensure-built.mjs +16 -0
  135. package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
  136. package/scripts/local-resume-smoke.mjs +3 -1
  137. package/scripts/platform-smoke/card-detect.mjs +1 -1
  138. package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
  139. package/scripts/platform-smoke/render-ansi.mjs +1 -1
  140. package/scripts/platform-smoke/targets.mjs +2 -2
  141. package/scripts/prepare.mjs +102 -0
  142. package/scripts/steering-rpc-smoke.mjs +4 -2
  143. package/scripts/tmux-live-smoke.sh +11 -11
  144. package/scripts/visual-tui-smoke-self-test.mjs +4 -1
  145. package/scripts/visual-tui-smoke.mjs +51 -7
  146. package/src/bundled-context-windows.ts +4 -1
  147. package/src/context.ts +27 -4
  148. package/src/cursor-agents-context-registration.ts +2 -2
  149. package/src/cursor-agents-context.ts +24 -10
  150. package/src/cursor-api-key.ts +5 -4
  151. package/src/cursor-cloud-reporting.ts +23 -2
  152. package/src/cursor-context-tools.ts +3 -1
  153. package/src/cursor-fallback-models.generated.ts +592 -204
  154. package/src/cursor-live-run-coordinator.ts +11 -6
  155. package/src/cursor-native-tool-display-registration.ts +10 -11
  156. package/src/cursor-partial-content-emitter.ts +13 -0
  157. package/src/cursor-pi-context.ts +44 -0
  158. package/src/cursor-provider-errors.ts +1 -1
  159. package/src/cursor-provider-lazy.ts +5 -4
  160. package/src/cursor-provider-live-run-drain.ts +14 -9
  161. package/src/cursor-provider-run-finalizer.ts +1 -0
  162. package/src/cursor-provider-turn-coordinator.ts +15 -0
  163. package/src/cursor-provider-turn-emit.ts +6 -0
  164. package/src/cursor-provider-turn-finalize.ts +12 -0
  165. package/src/cursor-provider-turn-prepare.ts +10 -5
  166. package/src/cursor-provider-turn-types.ts +2 -0
  167. package/src/cursor-sdk-billed-usage.ts +107 -0
  168. package/src/cursor-sdk-process-error-guard.ts +6 -4
  169. package/src/cursor-session-agent-lifecycle.ts +8 -9
  170. package/src/cursor-session-agent-resume.ts +23 -0
  171. package/src/cursor-session-compaction-prep.ts +4 -0
  172. package/src/cursor-skill-tool.ts +1 -1
  173. package/src/cursor-tool-manifest.ts +1 -1
  174. package/src/cursor-usage-accounting.ts +70 -9
  175. package/src/index.ts +1 -1
  176. package/tsconfig.build.json +8 -0
  177. package/tsconfig.json +16 -0
@@ -0,0 +1,355 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Server as McpProtocolServer } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
6
+ import { MCP_ENDPOINT_ROOT, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
7
+ import { writeCursorPiToolBridgeDiagnostic, } from "./cursor-pi-tool-bridge-diagnostics.js";
8
+ import { resolveCursorPiToolBridgeCallTimeoutMs } from "./cursor-pi-tool-bridge-env.js";
9
+ import { asToolResultMessage, containsKnownMcpToolName, convertPiContentToMcpContent, normalizeMcpArgs, snapshotToolToMcpTool, waitForProtocolFlush, } from "./cursor-pi-tool-bridge-mcp.js";
10
+ import { asRecord, getFirstStringByKeys } from "./cursor-record-utils.js";
11
+ const MCP_SERVER_VERSION = "0.1.0";
12
+ export class CursorPiToolBridgeRunImpl {
13
+ id;
14
+ enabled;
15
+ snapshot;
16
+ mcpServers;
17
+ registry;
18
+ env;
19
+ endpointPath;
20
+ callTimeoutMs;
21
+ knownMcpToolNames;
22
+ knownCursorMcpCallIds = new Set();
23
+ queuedRequests = [];
24
+ pendingByPiToolCallId = new Map();
25
+ pendingByBridgeCallId = new Map();
26
+ pendingByCursorMcpCallId = new Map();
27
+ onToolRequest;
28
+ debugRecorder;
29
+ liveRunHandlerDetached = false;
30
+ mcpServer;
31
+ mcpTransport;
32
+ toolCallCounter = 0;
33
+ disposed = false;
34
+ constructor(registry, env, snapshot, enabled, options = {}) {
35
+ this.registry = registry;
36
+ this.env = env;
37
+ this.snapshot = snapshot;
38
+ this.enabled = enabled;
39
+ this.onToolRequest = options.onToolRequest;
40
+ this.debugRecorder = options.debugRecorder;
41
+ this.id = `cursor-pi-bridge-run-${randomUUID()}`;
42
+ this.endpointPath = `${MCP_ENDPOINT_ROOT}/${randomUUID()}/mcp`;
43
+ this.callTimeoutMs = resolveCursorPiToolBridgeCallTimeoutMs(env);
44
+ this.knownMcpToolNames = new Set(snapshot.tools.map((tool) => tool.mcpToolName));
45
+ }
46
+ async start() {
47
+ if (!this.enabled)
48
+ return;
49
+ await this.createMcpServer();
50
+ const endpointUrl = await this.registry.registerRun(this.endpointPath, this);
51
+ this.mcpServers = { [MCP_SERVER_NAME]: { type: "http", url: endpointUrl } };
52
+ }
53
+ emitStartDiagnostics(bridgeEnabled) {
54
+ const base = this.lifecycleDiagnosticFields();
55
+ this.emitDiagnostic({ event: "run_created", ...base });
56
+ if (!this.enabled) {
57
+ this.emitDiagnostic({
58
+ event: "run_skipped",
59
+ ...base,
60
+ reason: bridgeEnabled ? "no_exposed_tools" : "disabled",
61
+ });
62
+ return;
63
+ }
64
+ this.emitDiagnostic({
65
+ event: "tools_exposed",
66
+ ...base,
67
+ pairs: this.snapshot.tools.map((tool) => ({
68
+ piToolName: tool.piToolName,
69
+ mcpToolName: tool.mcpToolName,
70
+ })),
71
+ });
72
+ }
73
+ async handleHttpRequest(req, res) {
74
+ if (this.disposed || !this.mcpTransport) {
75
+ res.writeHead(410, { "content-type": "application/json" }).end(JSON.stringify({ error: "Cursor pi tool bridge run is disposed" }));
76
+ return;
77
+ }
78
+ await this.mcpTransport.handleRequest(req, res);
79
+ }
80
+ takeQueuedToolRequests() {
81
+ return this.queuedRequests.splice(0);
82
+ }
83
+ setOnToolRequest(handler) {
84
+ if (!handler) {
85
+ this.liveRunHandlerDetached = true;
86
+ this.rejectQueuedToolRequestsWithoutHandler("Cursor pi tool bridge has no active live run");
87
+ }
88
+ else {
89
+ this.liveRunHandlerDetached = false;
90
+ }
91
+ this.onToolRequest = handler;
92
+ if (handler) {
93
+ for (const request of this.queuedRequests.splice(0)) {
94
+ const pending = this.pendingByPiToolCallId.get(request.piToolCallId);
95
+ if (pending)
96
+ this.dispatchPendingToolRequest(pending, handler);
97
+ }
98
+ }
99
+ }
100
+ setDebugRecorder(recorder) {
101
+ this.debugRecorder = recorder;
102
+ }
103
+ recordBridgeRaw(payload) {
104
+ try {
105
+ this.debugRecorder?.recordBridgeRaw(payload);
106
+ }
107
+ catch {
108
+ // Debug capture must never block or strand a bridge call.
109
+ }
110
+ }
111
+ async resolveToolResults(toolResults) {
112
+ let resolvedCount = 0;
113
+ for (const toolResult of toolResults) {
114
+ const pending = this.pendingByPiToolCallId.get(toolResult.toolCallId);
115
+ if (!pending || pending.settled)
116
+ continue;
117
+ this.resolvePending(pending, {
118
+ content: convertPiContentToMcpContent(toolResult.content),
119
+ isError: toolResult.isError || undefined,
120
+ });
121
+ resolvedCount += 1;
122
+ }
123
+ if (resolvedCount > 0)
124
+ await waitForProtocolFlush();
125
+ }
126
+ async resolveToolResultsFromContext(context) {
127
+ await this.resolveToolResults(context.messages.map(asToolResultMessage).filter((message) => message !== undefined));
128
+ }
129
+ hasPendingPiToolCallId(piToolCallId) {
130
+ return this.pendingByPiToolCallId.has(piToolCallId);
131
+ }
132
+ cancelPendingPiToolCallId(piToolCallId, reason) {
133
+ const pending = this.pendingByPiToolCallId.get(piToolCallId);
134
+ if (!pending)
135
+ return false;
136
+ this.rejectPending(pending, new Error(reason), "cancelled");
137
+ return true;
138
+ }
139
+ isBridgeMcpToolCall(toolCall) {
140
+ const record = asRecord(toolCall);
141
+ if (!record)
142
+ return false;
143
+ const toolName = getFirstStringByKeys(record, ["name", "toolName", "mcpToolName"], { nonEmpty: true });
144
+ if (toolName && this.knownMcpToolNames.has(toolName))
145
+ return true;
146
+ const isMcpEnvelope = toolName === "mcp" || toolName === MCP_SERVER_NAME;
147
+ const cursorMcpCallId = getFirstStringByKeys(record, ["call_id", "callId", "id", "toolCallId", "requestId"], { nonEmpty: true });
148
+ if (cursorMcpCallId && this.knownCursorMcpCallIds.has(cursorMcpCallId) && isMcpEnvelope)
149
+ return true;
150
+ if (containsKnownMcpToolName(toolCall, this.knownMcpToolNames))
151
+ return true;
152
+ return false;
153
+ }
154
+ cancel(reason) {
155
+ const error = new Error(reason);
156
+ const pendingCount = this.pendingCount();
157
+ const queuedCount = this.queuedRequests.length;
158
+ if (pendingCount > 0 || queuedCount > 0) {
159
+ this.emitDiagnostic({
160
+ event: "run_cancelled",
161
+ ...this.lifecycleDiagnosticFields(pendingCount),
162
+ queuedCount,
163
+ cancelledRequestCount: pendingCount,
164
+ });
165
+ }
166
+ this.queuedRequests.splice(0);
167
+ for (const pending of [...this.pendingByBridgeCallId.values()]) {
168
+ this.rejectAndAbortPending(pending, error, "cancelled");
169
+ }
170
+ }
171
+ async dispose() {
172
+ if (this.disposed)
173
+ return;
174
+ this.disposed = true;
175
+ this.cancel("Cursor pi tool bridge run disposed");
176
+ await waitForProtocolFlush();
177
+ await Promise.allSettled([
178
+ this.mcpTransport?.close(),
179
+ this.mcpServer?.close(),
180
+ ]);
181
+ await this.registry.unregisterRun(this.endpointPath, this);
182
+ this.emitDiagnostic({
183
+ event: "run_disposed",
184
+ ...this.lifecycleDiagnosticFields(),
185
+ });
186
+ }
187
+ async createMcpServer() {
188
+ const server = new McpProtocolServer({ name: "pi-cursor-sdk-tool-bridge", version: MCP_SERVER_VERSION }, { capabilities: { tools: {} } });
189
+ const transport = new StreamableHTTPServerTransport({
190
+ sessionIdGenerator: randomUUID,
191
+ });
192
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
193
+ tools: this.snapshot.tools.map(snapshotToolToMcpTool),
194
+ }));
195
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
196
+ return this.enqueueToolRequest(request.params.name, request.params.arguments, String(extra.requestId), extra.signal);
197
+ });
198
+ this.mcpServer = server;
199
+ this.mcpTransport = transport;
200
+ await server.connect(transport);
201
+ }
202
+ enqueueToolRequest(mcpToolName, argsValue, cursorMcpCallId, signal) {
203
+ const piToolName = this.snapshot.mcpToolNameToPiToolName.get(mcpToolName);
204
+ if (!piToolName) {
205
+ return Promise.resolve({
206
+ content: [{ type: "text", text: `Unknown pi bridge tool: ${mcpToolName}` }],
207
+ isError: true,
208
+ });
209
+ }
210
+ if (this.disposed)
211
+ return Promise.reject(new Error("Cursor pi tool bridge run is disposed"));
212
+ this.toolCallCounter += 1;
213
+ const bridgeCallId = `${this.id}-bridge-${this.toolCallCounter}`;
214
+ const request = {
215
+ runId: this.id,
216
+ bridgeCallId,
217
+ cursorMcpCallId,
218
+ piToolCallId: `${this.id}-tool-${this.toolCallCounter}`,
219
+ piToolName,
220
+ mcpToolName,
221
+ args: normalizeMcpArgs(argsValue),
222
+ };
223
+ return new Promise((resolve, reject) => {
224
+ const pending = {
225
+ request,
226
+ resolve,
227
+ reject,
228
+ signal,
229
+ settled: false,
230
+ };
231
+ pending.onAbort = () => {
232
+ this.rejectAndAbortPending(pending, new Error("Cursor MCP bridge tool request was aborted"), "cancelled");
233
+ };
234
+ if (signal?.aborted) {
235
+ pending.onAbort();
236
+ return;
237
+ }
238
+ signal?.addEventListener("abort", pending.onAbort, { once: true });
239
+ this.pendingByPiToolCallId.set(request.piToolCallId, pending);
240
+ this.pendingByBridgeCallId.set(request.bridgeCallId, pending);
241
+ this.pendingByCursorMcpCallId.set(cursorMcpCallId, pending);
242
+ this.knownCursorMcpCallIds.add(cursorMcpCallId);
243
+ pending.timeout = setTimeout(() => {
244
+ const reason = `Cursor pi bridge CallTool timed out after ${this.callTimeoutMs} ms`;
245
+ this.rejectAndAbortPending(pending, new Error(reason));
246
+ }, this.callTimeoutMs);
247
+ pending.timeout.unref?.();
248
+ if (!this.onToolRequest) {
249
+ if (this.liveRunHandlerDetached) {
250
+ this.rejectPending(pending, new Error("Cursor pi tool bridge has no active live run"), "cancelled");
251
+ return;
252
+ }
253
+ this.queuedRequests.push(request);
254
+ this.emitRequestQueuedDiagnostic(request);
255
+ this.recordBridgeRaw({ kind: "queued", request });
256
+ return;
257
+ }
258
+ this.emitRequestQueuedDiagnostic(request);
259
+ this.recordBridgeRaw({ kind: "queued", request });
260
+ this.dispatchPendingToolRequest(pending, this.onToolRequest);
261
+ });
262
+ }
263
+ dispatchPendingToolRequest(pending, handler) {
264
+ try {
265
+ handler(pending.request);
266
+ }
267
+ catch (error) {
268
+ this.rejectPending(pending, error instanceof Error ? error : new Error(String(error)), "error");
269
+ }
270
+ }
271
+ rejectQueuedToolRequestsWithoutHandler(reason) {
272
+ while (this.queuedRequests.length > 0) {
273
+ const request = this.queuedRequests.shift();
274
+ const pending = this.pendingByPiToolCallId.get(request.piToolCallId);
275
+ if (pending)
276
+ this.rejectPending(pending, new Error(reason), "cancelled");
277
+ }
278
+ }
279
+ resolvePending(pending, result) {
280
+ if (pending.settled)
281
+ return;
282
+ pending.settled = true;
283
+ this.removePending(pending);
284
+ this.emitRequestResolvedDiagnostic(pending.request, result.isError === true);
285
+ this.recordBridgeRaw({ kind: "resolved", request: pending.request, result });
286
+ pending.resolve(result);
287
+ }
288
+ rejectPending(pending, error, kind = "error") {
289
+ if (pending.settled)
290
+ return false;
291
+ pending.settled = true;
292
+ this.removePending(pending);
293
+ this.emitRequestRejectedDiagnostic(pending.request, kind);
294
+ this.recordBridgeRaw({
295
+ kind: "rejected",
296
+ request: pending.request,
297
+ error: error.message,
298
+ rejectionKind: kind,
299
+ });
300
+ pending.reject(error);
301
+ return true;
302
+ }
303
+ rejectAndAbortPending(pending, error, kind = "error") {
304
+ if (this.rejectPending(pending, error, kind)) {
305
+ bridgeToolExecutionAbortTracker.abort(pending.request.piToolCallId, error.message);
306
+ }
307
+ }
308
+ lifecycleDiagnosticFields(pendingCount = this.pendingCount()) {
309
+ return {
310
+ runId: this.id,
311
+ enabled: this.enabled,
312
+ exposedToolCount: this.snapshot.tools.length,
313
+ pendingCount,
314
+ };
315
+ }
316
+ requestDiagnosticFields(request) {
317
+ return {
318
+ runId: this.id,
319
+ bridgeCallId: request.bridgeCallId,
320
+ cursorMcpCallId: request.cursorMcpCallId,
321
+ piToolCallId: request.piToolCallId,
322
+ mcpToolName: request.mcpToolName,
323
+ piToolName: request.piToolName,
324
+ pendingCount: this.pendingCount(),
325
+ };
326
+ }
327
+ emitRequestQueuedDiagnostic(request) {
328
+ this.emitDiagnostic({ event: "request_queued", ...this.requestDiagnosticFields(request) });
329
+ }
330
+ emitRequestResolvedDiagnostic(request, isError) {
331
+ this.emitDiagnostic({ event: "request_resolved", ...this.requestDiagnosticFields(request), isError });
332
+ }
333
+ emitRequestRejectedDiagnostic(request, rejectionKind) {
334
+ this.emitDiagnostic({ event: "request_rejected", ...this.requestDiagnosticFields(request), rejectionKind });
335
+ }
336
+ emitDiagnostic(event) {
337
+ writeCursorPiToolBridgeDiagnostic(this.env, event, this.debugRecorder);
338
+ }
339
+ pendingCount() {
340
+ return this.pendingByBridgeCallId.size;
341
+ }
342
+ removePending(pending) {
343
+ if (pending.onAbort)
344
+ pending.signal?.removeEventListener("abort", pending.onAbort);
345
+ if (pending.timeout)
346
+ clearTimeout(pending.timeout);
347
+ this.pendingByPiToolCallId.delete(pending.request.piToolCallId);
348
+ this.pendingByBridgeCallId.delete(pending.request.bridgeCallId);
349
+ if (pending.request.cursorMcpCallId)
350
+ this.pendingByCursorMcpCallId.delete(pending.request.cursorMcpCallId);
351
+ const queuedIndex = this.queuedRequests.findIndex((request) => request.bridgeCallId === pending.request.bridgeCallId);
352
+ if (queuedIndex >= 0)
353
+ this.queuedRequests.splice(queuedIndex, 1);
354
+ }
355
+ }
@@ -0,0 +1,163 @@
1
+ import { createServer } from "node:http";
2
+ import { asRecord } from "./cursor-record-utils.js";
3
+ import { buildCursorPiToolBridgeSnapshot, buildCursorPiToolBridgeSurfaceSignature, createEmptySnapshot, resolveCursorPiToolBridgeBuiltinsEnabled, resolveCursorPiToolBridgeEnabled, } from "./cursor-pi-tool-bridge-snapshot.js";
4
+ export const LOOPBACK_HOST = "127.0.0.1";
5
+ const HTTP_SERVER_CLOSE_GRACE_MS = 250;
6
+ export class CursorPiToolBridgeRegistry {
7
+ pi;
8
+ env;
9
+ runs = new Set();
10
+ routes = new Map();
11
+ httpServer;
12
+ listenPromise;
13
+ constructor(pi, env = process.env) {
14
+ this.pi = pi;
15
+ this.env = env;
16
+ }
17
+ isEnabled() {
18
+ return resolveCursorPiToolBridgeEnabled(this.env);
19
+ }
20
+ getToolSurfaceSignature() {
21
+ if (!this.isEnabled())
22
+ return "bridge:off";
23
+ const snapshot = buildCursorPiToolBridgeSnapshot(this.pi, {
24
+ exposeOverlappingBuiltins: resolveCursorPiToolBridgeBuiltinsEnabled(this.env),
25
+ });
26
+ return buildCursorPiToolBridgeSurfaceSignature(snapshot);
27
+ }
28
+ async createRun(options = {}) {
29
+ const bridgeEnabled = this.isEnabled();
30
+ const snapshot = bridgeEnabled
31
+ ? buildCursorPiToolBridgeSnapshot(this.pi, {
32
+ exposeOverlappingBuiltins: resolveCursorPiToolBridgeBuiltinsEnabled(this.env),
33
+ })
34
+ : createEmptySnapshot();
35
+ const { CursorPiToolBridgeRunImpl } = await import("./cursor-pi-tool-bridge-run.js");
36
+ const run = new CursorPiToolBridgeRunImpl(this, this.env, snapshot, bridgeEnabled && snapshot.tools.length > 0, options);
37
+ this.runs.add(run);
38
+ await run.start();
39
+ run.emitStartDiagnostics(bridgeEnabled);
40
+ return run;
41
+ }
42
+ async disposeAll(reason = "Cursor pi tool bridge disposed") {
43
+ await Promise.all([...this.runs].map(async (run) => {
44
+ run.cancel(reason);
45
+ await run.dispose();
46
+ }));
47
+ }
48
+ async registerRun(pathname, run) {
49
+ await this.ensureHttpServer();
50
+ this.routes.set(pathname, run);
51
+ const address = this.getHttpServerAddress();
52
+ if (!address)
53
+ throw new Error("Cursor pi tool bridge HTTP server is not listening");
54
+ return `http://${LOOPBACK_HOST}:${address.port}${pathname}`;
55
+ }
56
+ async unregisterRun(pathname, run) {
57
+ if (this.routes.get(pathname) === run)
58
+ this.routes.delete(pathname);
59
+ this.runs.delete(run);
60
+ if (this.routes.size === 0)
61
+ await this.closeHttpServer();
62
+ }
63
+ getHttpServerAddress() {
64
+ const address = asRecord(this.httpServer?.address());
65
+ if (typeof address?.port !== "number")
66
+ return undefined;
67
+ return {
68
+ address: typeof address.address === "string" ? address.address : LOOPBACK_HOST,
69
+ family: typeof address.family === "string" ? address.family : "IPv4",
70
+ port: address.port,
71
+ };
72
+ }
73
+ getEndpointCount() {
74
+ return this.routes.size;
75
+ }
76
+ hasPendingPiToolCallId(piToolCallId) {
77
+ for (const run of this.runs) {
78
+ if (run.hasPendingPiToolCallId(piToolCallId))
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+ cancelPendingPiToolCallId(piToolCallId, reason) {
84
+ for (const run of this.runs) {
85
+ if (run.cancelPendingPiToolCallId(piToolCallId, reason))
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+ async ensureHttpServer() {
91
+ if (this.httpServer) {
92
+ await this.listenPromise;
93
+ return;
94
+ }
95
+ const server = createServer((req, res) => {
96
+ void this.handleHttpRequest(req, res);
97
+ });
98
+ this.httpServer = server;
99
+ this.listenPromise = new Promise((resolve, reject) => {
100
+ const onError = (error) => {
101
+ server.off("listening", onListening);
102
+ reject(error);
103
+ };
104
+ const onListening = () => {
105
+ server.off("error", onError);
106
+ resolve();
107
+ };
108
+ server.once("error", onError);
109
+ server.once("listening", onListening);
110
+ server.listen(0, LOOPBACK_HOST);
111
+ });
112
+ await this.listenPromise;
113
+ }
114
+ async closeHttpServer() {
115
+ const server = this.httpServer;
116
+ if (!server)
117
+ return;
118
+ this.httpServer = undefined;
119
+ this.listenPromise = undefined;
120
+ await new Promise((resolve, reject) => {
121
+ let settled = false;
122
+ let closeTimer;
123
+ const settle = (error) => {
124
+ if (settled)
125
+ return;
126
+ settled = true;
127
+ if (closeTimer)
128
+ clearTimeout(closeTimer);
129
+ if (error)
130
+ reject(error);
131
+ else
132
+ resolve();
133
+ };
134
+ closeTimer = setTimeout(() => settle(), HTTP_SERVER_CLOSE_GRACE_MS);
135
+ closeTimer.unref?.();
136
+ server.close((error) => {
137
+ settle(error ?? undefined);
138
+ });
139
+ server.closeIdleConnections();
140
+ server.closeAllConnections();
141
+ });
142
+ }
143
+ async handleHttpRequest(req, res) {
144
+ if (req.socket.localAddress !== LOOPBACK_HOST) {
145
+ res.writeHead(403, { "content-type": "application/json" }).end(JSON.stringify({ error: "Cursor pi tool bridge only accepts loopback requests" }));
146
+ return;
147
+ }
148
+ const url = new URL(req.url ?? "/", `http://${LOOPBACK_HOST}`);
149
+ const run = this.routes.get(url.pathname);
150
+ if (!run) {
151
+ res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "Cursor pi tool bridge endpoint not found" }));
152
+ return;
153
+ }
154
+ try {
155
+ await run.handleHttpRequest(req, res);
156
+ }
157
+ catch (error) {
158
+ if (!res.headersSent) {
159
+ res.writeHead(500, { "content-type": "application/json" }).end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
160
+ }
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,63 @@
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";
3
+ import { isRegisteredCursorNativeToolName } from "./cursor-native-tool-display-state.js";
4
+ import { isExcludedFromCursorBridgeExposure } from "./cursor-tool-presentation-registry.js";
5
+ const OVERLAPPING_CURSOR_NATIVE_PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "write", "edit", "grep", "find", "ls"]);
6
+ export function createEmptySnapshot() {
7
+ return {
8
+ tools: [],
9
+ mcpToolNameToPiToolName: new Map(),
10
+ piToolNameToMcpToolName: new Map(),
11
+ };
12
+ }
13
+ function isOverlappingCursorNativePiToolName(toolName) {
14
+ return OVERLAPPING_CURSOR_NATIVE_PI_BUILTIN_TOOL_NAMES.has(toolName);
15
+ }
16
+ export function buildCursorPiToolBridgeSurfaceSignature(snapshot) {
17
+ if (snapshot.tools.length === 0)
18
+ return "bridge:empty";
19
+ const serializedTools = snapshot.tools
20
+ .map((tool) => JSON.stringify({
21
+ piToolName: tool.piToolName,
22
+ mcpToolName: tool.mcpToolName,
23
+ description: tool.description,
24
+ promptGuidelines: tool.promptGuidelines,
25
+ inputSchema: tool.inputSchema,
26
+ source: tool.sourceInfo?.source,
27
+ path: tool.sourceInfo?.path,
28
+ scope: tool.sourceInfo?.scope,
29
+ }))
30
+ .sort()
31
+ .join("\0");
32
+ return `bridge:on:${stableNameHash(serializedTools)}`;
33
+ }
34
+ export function buildCursorPiToolBridgeSnapshot(pi, options = {}) {
35
+ const activeToolNames = new Set(pi.getActiveTools());
36
+ const allTools = pi.getAllTools();
37
+ const usedMcpToolNames = new Set();
38
+ const mcpToolNameToPiToolName = new Map();
39
+ const piToolNameToMcpToolName = new Map();
40
+ const tools = [];
41
+ const exposeOverlappingBuiltins = options.exposeOverlappingBuiltins === true;
42
+ for (const tool of allTools) {
43
+ if (!activeToolNames.has(tool.name))
44
+ continue;
45
+ if (isExcludedFromCursorBridgeExposure(tool.name) && isRegisteredCursorNativeToolName(tool.name))
46
+ continue;
47
+ if (!exposeOverlappingBuiltins && isOverlappingCursorNativePiToolName(tool.name))
48
+ continue;
49
+ const mcpToolName = createMcpToolName(tool.name, usedMcpToolNames);
50
+ const description = tool.description || `Run pi tool ${tool.name}`;
51
+ mcpToolNameToPiToolName.set(mcpToolName, tool.name);
52
+ piToolNameToMcpToolName.set(tool.name, mcpToolName);
53
+ tools.push({
54
+ piToolName: tool.name,
55
+ mcpToolName,
56
+ description,
57
+ promptGuidelines: tool.promptGuidelines,
58
+ inputSchema: normalizeMcpInputSchema(tool.parameters),
59
+ sourceInfo: tool.sourceInfo,
60
+ });
61
+ }
62
+ return { tools, mcpToolNameToPiToolName, piToolNameToMcpToolName };
63
+ }
@@ -0,0 +1 @@
1
+ export {};