import { createServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http"; import type { AddressInfo } from "node:net"; import type { CursorPiToolBridge, CursorPiToolBridgeRun, CursorPiToolBridgeRunOptions, CursorPiToolBridgeSnapshotApi, } from "./pi-tool-bridge-types.js"; import { asRecord } from "./record-utils.js"; import type { CursorPiToolBridgeRunImpl } from "./pi-tool-bridge-run.js"; import { buildCursorPiToolBridgeSnapshot, buildCursorPiToolBridgeSurfaceSignature, createEmptySnapshot, } from "./pi-tool-bridge-snapshot.js"; import type { CursorPiToolBridgeConfig } from "./pi-tool-bridge-config.js"; export const LOOPBACK_HOST = "127.0.0.1"; const HTTP_SERVER_CLOSE_GRACE_MS = 250; export class CursorPiToolBridgeRegistry implements CursorPiToolBridge { private readonly pi: CursorPiToolBridgeSnapshotApi; private readonly config: CursorPiToolBridgeConfig; private readonly runs = new Set(); private readonly routes = new Map(); private httpServer?: HttpServer; private listenPromise?: Promise; constructor( pi: CursorPiToolBridgeSnapshotApi, config: CursorPiToolBridgeConfig, ) { this.pi = pi; this.config = config; } isEnabled(): boolean { return this.config.enabled; } getToolSurfaceSignature(excludedToolNames?: ReadonlySet): string { if (!this.isEnabled()) return "bridge:off"; const snapshot = buildCursorPiToolBridgeSnapshot(this.pi, { exposeOverlappingBuiltins: this.config.exposeBuiltins, excludedToolNames, }); return buildCursorPiToolBridgeSurfaceSignature(snapshot); } async createRun(options: CursorPiToolBridgeRunOptions = {}): Promise { const bridgeEnabled = this.isEnabled(); const snapshot = bridgeEnabled ? buildCursorPiToolBridgeSnapshot(this.pi, { exposeOverlappingBuiltins: this.config.exposeBuiltins, excludedToolNames: options.excludeToolNames, }) : createEmptySnapshot(); const { CursorPiToolBridgeRunImpl } = await import("./pi-tool-bridge-run.js"); const run = new CursorPiToolBridgeRunImpl(this, this.config, snapshot, bridgeEnabled && snapshot.tools.length > 0, options); this.runs.add(run); await run.start(); run.emitStartDiagnostics(bridgeEnabled); return run; } hasLiveRuns(): boolean { return this.runs.size > 0; } async disposeAll(reason = "Cursor pi tool bridge disposed"): Promise { await Promise.all([...this.runs].map(async (run) => { run.cancel(reason); await run.dispose(); })); } async registerRun(pathname: string, run: CursorPiToolBridgeRunImpl): Promise { await this.ensureHttpServer(); this.routes.set(pathname, run); const address = this.getHttpServerAddress(); if (!address) throw new Error("Cursor pi tool bridge HTTP server is not listening"); return `http://${LOOPBACK_HOST}:${address.port}${pathname}`; } async unregisterRun(pathname: string, run: CursorPiToolBridgeRunImpl): Promise { if (this.routes.get(pathname) === run) this.routes.delete(pathname); this.runs.delete(run); if (this.routes.size === 0) await this.closeHttpServer(); } getHttpServerAddress(): AddressInfo | undefined { const address = asRecord(this.httpServer?.address()); if (typeof address?.port !== "number") return undefined; return { address: typeof address.address === "string" ? address.address : LOOPBACK_HOST, family: typeof address.family === "string" ? address.family : "IPv4", port: address.port, }; } getEndpointCount(): number { return this.routes.size; } hasPendingPiToolCallId(piToolCallId: string): boolean { for (const run of this.runs) { if (run.hasPendingPiToolCallId(piToolCallId)) return true; } return false; } cancelPendingPiToolCallId(piToolCallId: string, reason: string): boolean { for (const run of this.runs) { if (run.cancelPendingPiToolCallId(piToolCallId, reason)) return true; } return false; } private async ensureHttpServer(): Promise { if (this.httpServer) { await this.listenPromise; return; } const server = createServer((req, res) => { void this.handleHttpRequest(req, res); }); this.httpServer = server; this.listenPromise = new Promise((resolve, reject) => { const onError = (error: Error) => { server.off("listening", onListening); reject(error); }; const onListening = () => { server.off("error", onError); resolve(); }; server.once("error", onError); server.once("listening", onListening); server.listen(0, LOOPBACK_HOST); }); await this.listenPromise; } private async closeHttpServer(): Promise { const server = this.httpServer; if (!server) return; this.httpServer = undefined; this.listenPromise = undefined; await new Promise((resolve, reject) => { let settled = false; let closeTimer: ReturnType | undefined; const settle = (error?: Error): void => { if (settled) return; settled = true; if (closeTimer) clearTimeout(closeTimer); if (error) reject(error); else resolve(); }; closeTimer = setTimeout(() => settle(), HTTP_SERVER_CLOSE_GRACE_MS); closeTimer.unref?.(); server.close((error) => { settle(error ?? undefined); }); server.closeIdleConnections(); server.closeAllConnections(); }); } private async handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise { if (req.socket.localAddress !== LOOPBACK_HOST) { res.writeHead(403, { "content-type": "application/json" }).end(JSON.stringify({ error: "Cursor pi tool bridge only accepts loopback requests" })); return; } const url = new URL(req.url ?? "/", `http://${LOOPBACK_HOST}`); const run = this.routes.get(url.pathname); if (!run) { res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "Cursor pi tool bridge endpoint not found" })); return; } try { await run.handleHttpRequest(req, res); } catch (error) { if (!res.headersSent) { res.writeHead(500, { "content-type": "application/json" }).end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); } } } }