@cursorpool-dev/cli 0.5.6

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 (105) hide show
  1. package/bin/cursor-pool.mjs +9 -0
  2. package/bin/cursor-pool.ts +169 -0
  3. package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
  4. package/node_modules/@cursor-pool/extension/package.json +64 -0
  5. package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
  6. package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
  7. package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
  8. package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
  9. package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
  10. package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
  11. package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
  12. package/node_modules/@cursor-pool/patcher/package.json +17 -0
  13. package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
  14. package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
  15. package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
  16. package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
  17. package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
  18. package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
  19. package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
  20. package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
  21. package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
  22. package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
  23. package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
  24. package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
  25. package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
  26. package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
  27. package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
  28. package/node_modules/@cursor-pool/service/package.json +17 -0
  29. package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
  30. package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
  31. package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
  32. package/node_modules/@cursor-pool/service/src/health.ts +10 -0
  33. package/node_modules/@cursor-pool/service/src/index.ts +29 -0
  34. package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
  35. package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
  36. package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
  37. package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
  38. package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
  39. package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
  40. package/node_modules/@cursor-pool/service/src/server.ts +939 -0
  41. package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
  42. package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
  43. package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
  44. package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
  45. package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
  46. package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
  47. package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
  48. package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
  49. package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
  50. package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
  51. package/node_modules/@cursor-pool/shared/package.json +17 -0
  52. package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
  53. package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
  54. package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
  55. package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
  56. package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
  57. package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
  58. package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
  59. package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
  60. package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
  61. package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
  62. package/package.json +28 -0
  63. package/src/adHocResign.ts +65 -0
  64. package/src/autostart.ts +240 -0
  65. package/src/compat.ts +282 -0
  66. package/src/confirm.ts +76 -0
  67. package/src/cursor.ts +94 -0
  68. package/src/diagnostics.ts +558 -0
  69. package/src/environment.ts +18 -0
  70. package/src/extensionBundle.ts +111 -0
  71. package/src/extensionLink.ts +168 -0
  72. package/src/index.ts +23 -0
  73. package/src/install.ts +614 -0
  74. package/src/installRecord.ts +105 -0
  75. package/src/launch.ts +182 -0
  76. package/src/patchSet.ts +182 -0
  77. package/src/platform.ts +132 -0
  78. package/src/repair.ts +383 -0
  79. package/src/restore.ts +153 -0
  80. package/src/serviceCommands.ts +79 -0
  81. package/src/serviceProcess.ts +188 -0
  82. package/src/status.ts +241 -0
  83. package/src/target.ts +37 -0
  84. package/src/trial.ts +133 -0
  85. package/src/uninstall.ts +213 -0
  86. package/test/autostart.test.ts +151 -0
  87. package/test/compat.test.ts +192 -0
  88. package/test/confirm.test.ts +114 -0
  89. package/test/cursor-pool-bin.test.ts +658 -0
  90. package/test/cursor.test.ts +20 -0
  91. package/test/diagnostics.test.ts +709 -0
  92. package/test/e2e-install.test.ts +773 -0
  93. package/test/extensionBundle.test.ts +161 -0
  94. package/test/extensionLink.test.ts +209 -0
  95. package/test/install.test.ts +862 -0
  96. package/test/installRecord.test.ts +107 -0
  97. package/test/launch.test.ts +138 -0
  98. package/test/platform.test.ts +226 -0
  99. package/test/repair.test.ts +575 -0
  100. package/test/restore.test.ts +211 -0
  101. package/test/serviceCommands.test.ts +135 -0
  102. package/test/serviceProcess.test.ts +280 -0
  103. package/test/status.test.ts +615 -0
  104. package/test/target.test.ts +49 -0
  105. package/test/trial.test.ts +146 -0
@@ -0,0 +1,939 @@
1
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
2
+ import { createCanaryStore, sanitizeAgentCanary } from './canary';
3
+ import { appendAgentCanaryDiagnostic, appendAgentGatewayDiagnostic, appendAgentRequestCheckDiagnostic } from './diagnostics';
4
+ import { buildHealth } from './health';
5
+ import { sanitizeExtensionStatus, sanitizeServiceMetadata, type ServiceMetadata } from './metadata';
6
+ import {
7
+ loginWithPassword,
8
+ loginWithCode,
9
+ logoutPlatform,
10
+ readPlatformGatewayForwardContext,
11
+ refreshPlatformRoute,
12
+ platformCatalog,
13
+ platformStatus,
14
+ selectPlatformProduct,
15
+ sendHeartbeat,
16
+ startPlatformMode,
17
+ stopPlatformMode,
18
+ type HeartbeatPayload,
19
+ type LoginWithPasswordOptions,
20
+ type LoginWithCodeOptions,
21
+ type LogoutPlatformOptions,
22
+ type PlatformDeviceInput,
23
+ type SendHeartbeatOptions,
24
+ type StartPlatformModeOptions,
25
+ type StopPlatformModeOptions,
26
+ } from './platformSession';
27
+ import {
28
+ completeGatewayForward,
29
+ createGatewayHttpForwarder,
30
+ createGatewayStore,
31
+ evaluateGatewayDecision,
32
+ resolveGatewayForward,
33
+ resolveGatewayForwardResult,
34
+ sanitizeAgentGateway,
35
+ type GatewayForwardResult,
36
+ type GatewayForwarder,
37
+ } from './requestGateway';
38
+ import { createRequestCheckStore, sanitizeAgentRequestCheck } from './requestCheck';
39
+ import {
40
+ evaluateRequestGate,
41
+ evaluateRouteState,
42
+ INVALID_SESSION_GATE,
43
+ type RequestGateDecision,
44
+ } from './requestGate';
45
+ import { createRuntimeId, writeRuntimeInfo, type RuntimeInfo } from './runtime';
46
+ import { buildAgentTakeoverResponse, createAgentTakeoverStore } from './takeover';
47
+ import { readClientConfig } from '@cursor-pool/shared/clientConfig';
48
+
49
+ const LOOPBACK_HOST = '127.0.0.1' as const;
50
+ const DEFAULT_PORT = 56393;
51
+ const TRUSTED_RENDERER_ORIGINS = new Set([
52
+ 'vscode-file://vscode-app',
53
+ ]);
54
+
55
+ export type StartServerOptions = {
56
+ port?: number;
57
+ runtimeFile?: string;
58
+ diagnosticsFile?: string;
59
+ maxDiagnostics?: number;
60
+ onMetadata?: (metadata: ServiceMetadata) => void | Promise<void>;
61
+ platformSessionFile?: string;
62
+ platformApiBaseUrl?: string;
63
+ clientConfigFile?: string;
64
+ platformExchangePasswordDeviceToken?: LoginWithPasswordOptions['exchangePasswordDeviceToken'];
65
+ platformExchangeDeviceToken?: LoginWithCodeOptions['exchangeDeviceToken'];
66
+ platformPostHeartbeat?: SendHeartbeatOptions['postHeartbeat'];
67
+ platformPostLogout?: LogoutPlatformOptions['postLogout'];
68
+ platformStartPoolSession?: StartPlatformModeOptions['startPoolSession'];
69
+ platformStopPoolSession?: StopPlatformModeOptions['stopPoolSession'];
70
+ gatewayForwarder?: GatewayForwarder;
71
+ onShutdown?: () => void | Promise<void>;
72
+ };
73
+
74
+ export type LocalService = RuntimeInfo & {
75
+ server: Server;
76
+ stop: () => Promise<void>;
77
+ };
78
+
79
+ const localServices = new Map<string, LocalService>();
80
+
81
+ async function resolvePlatformApiBaseUrl(options: StartServerOptions) {
82
+ if (options.platformApiBaseUrl) {
83
+ return options.platformApiBaseUrl;
84
+ }
85
+ if (process.env.CURSOR_POOL_API_BASE_URL) {
86
+ return process.env.CURSOR_POOL_API_BASE_URL;
87
+ }
88
+ const config = await readClientConfig({ configFile: options.clientConfigFile });
89
+ return config.apiBaseUrl;
90
+ }
91
+
92
+ function runtimeKey(runtime: RuntimeInfo) {
93
+ return `${runtime.host}:${runtime.port}:${runtime.runtimeId}`;
94
+ }
95
+
96
+ export async function stopLocalService(runtime: RuntimeInfo | null) {
97
+ if (!runtime) {
98
+ return false;
99
+ }
100
+
101
+ const service = localServices.get(runtimeKey(runtime));
102
+ if (!service) {
103
+ return false;
104
+ }
105
+
106
+ await service.stop();
107
+ return true;
108
+ }
109
+
110
+ async function readJsonRequest(request: IncomingMessage): Promise<Record<string, unknown>> {
111
+ const chunks: Buffer[] = [];
112
+
113
+ for await (const chunk of request) {
114
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
115
+ }
116
+
117
+ if (chunks.length === 0) {
118
+ return {};
119
+ }
120
+
121
+ const body = Buffer.concat(chunks).toString('utf8');
122
+ if (!body) {
123
+ return {};
124
+ }
125
+
126
+ return JSON.parse(body) as Record<string, unknown>;
127
+ }
128
+
129
+ function writeJson(response: ServerResponse, statusCode: number, payload: Record<string, unknown>) {
130
+ const body = JSON.stringify(payload);
131
+ response.writeHead(statusCode, {
132
+ 'content-type': 'application/json',
133
+ 'content-length': Buffer.byteLength(body),
134
+ });
135
+ response.end(body);
136
+ }
137
+
138
+ function trustedRendererOrigin(request: IncomingMessage) {
139
+ const origin = request.headers.origin;
140
+ if (typeof origin !== 'string') {
141
+ return undefined;
142
+ }
143
+ if (TRUSTED_RENDERER_ORIGINS.has(origin) || origin.startsWith('vscode-webview://')) {
144
+ return origin;
145
+ }
146
+ return undefined;
147
+ }
148
+
149
+ function applyCorsHeaders(request: IncomingMessage, response: ServerResponse) {
150
+ const origin = trustedRendererOrigin(request);
151
+ if (!origin) {
152
+ return false;
153
+ }
154
+ response.setHeader('access-control-allow-origin', origin);
155
+ response.setHeader('access-control-allow-methods', 'GET,POST,OPTIONS');
156
+ response.setHeader('access-control-allow-headers', 'content-type,authorization');
157
+ return true;
158
+ }
159
+
160
+ function writeCorsPreflight(request: IncomingMessage, response: ServerResponse) {
161
+ if (!applyCorsHeaders(request, response)) {
162
+ response.writeHead(403);
163
+ response.end();
164
+ return;
165
+ }
166
+ response.writeHead(204, {
167
+ 'access-control-max-age': '600',
168
+ });
169
+ response.end();
170
+ }
171
+
172
+ function writeEventStream(response: ServerResponse, events: Record<string, unknown>[]) {
173
+ const body = `${events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join('')}data: [DONE]\n\n`;
174
+ response.writeHead(200, {
175
+ 'content-type': 'text/event-stream; charset=utf-8',
176
+ 'cache-control': 'no-cache',
177
+ 'connection': 'keep-alive',
178
+ 'content-length': Buffer.byteLength(body),
179
+ });
180
+ response.end(body);
181
+ }
182
+
183
+ function asRecord(value: unknown): Record<string, unknown> {
184
+ return typeof value === 'object' && value !== null ? value as Record<string, unknown> : {};
185
+ }
186
+
187
+ function safeOpenAiModel(value: unknown) {
188
+ return typeof value === 'string' && /^[A-Za-z0-9._:-]{1,96}$/.test(value)
189
+ ? value
190
+ : 'unknown';
191
+ }
192
+
193
+ function openAiModelsResponse() {
194
+ return {
195
+ object: 'list',
196
+ data: [
197
+ {
198
+ id: 'gpt-test',
199
+ object: 'model',
200
+ created: 0,
201
+ owned_by: 'cursor-pool',
202
+ api_types: ['chat_completions'],
203
+ capabilities: {
204
+ supports_tool_use: true,
205
+ supports_streaming: true,
206
+ output_modalities: ['text'],
207
+ context_length: 128000,
208
+ max_output_tokens: 8192,
209
+ supports_reasoning: false,
210
+ supports_vision: false,
211
+ },
212
+ },
213
+ ],
214
+ };
215
+ }
216
+
217
+ async function resolveOpenAiGatewayForward(
218
+ body: Record<string, unknown>,
219
+ runtime: RuntimeInfo,
220
+ options: StartServerOptions,
221
+ retryAfterRefresh = true,
222
+ ) {
223
+ let gate: RequestGateDecision;
224
+ try {
225
+ gate = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
226
+ } catch {
227
+ gate = { ...INVALID_SESSION_GATE };
228
+ }
229
+ const route = gate.state === 'allowed'
230
+ ? await evaluateRouteState({ sessionFile: options.platformSessionFile })
231
+ : { state: 'missing' as const };
232
+ const decision = evaluateGatewayDecision(gate, route);
233
+ const requestId = typeof body.requestId === 'string'
234
+ ? body.requestId
235
+ : `openai-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
236
+ const model = safeOpenAiModel(body.model);
237
+ const gateway = sanitizeAgentGateway({
238
+ requestId,
239
+ source: 'cursor-agent-exec',
240
+ model,
241
+ }, {
242
+ runtimeId: runtime.runtimeId,
243
+ decision,
244
+ });
245
+ const forwardContext = decision.state === 'accepted'
246
+ ? await readPlatformGatewayForwardContext({ sessionFile: options.platformSessionFile })
247
+ : {};
248
+ const forwarder = options.gatewayForwarder ?? (
249
+ decision.state === 'accepted'
250
+ ? createGatewayHttpForwarder({
251
+ apiBaseUrl: forwardContext.apiBaseUrl,
252
+ deviceToken: forwardContext.deviceToken,
253
+ })
254
+ : undefined
255
+ );
256
+ const { result, safe } = await resolveGatewayForwardResult({
257
+ gateway,
258
+ routeToken: forwardContext.routeToken ?? '',
259
+ poolSessionId: forwardContext.poolSessionId,
260
+ productId: decision.state === 'accepted' ? decision.productId : 'unknown',
261
+ model,
262
+ requestId: gateway.requestId,
263
+ settlementMode: 'client_response',
264
+ openAiRequest: body,
265
+ forwarder,
266
+ });
267
+ if (
268
+ retryAfterRefresh &&
269
+ (
270
+ decision.state === 'route-expired' ||
271
+ safe.state === 'skipped' && safe.reason === 'not-accepted' ||
272
+ safe.state === 'rejected' && safe.reason === 'pool-session-expired'
273
+ )
274
+ ) {
275
+ const refreshed = await refreshPlatformRoute({
276
+ sessionFile: options.platformSessionFile,
277
+ startPoolSession: options.platformStartPoolSession,
278
+ });
279
+ if (refreshed.state === 'active') {
280
+ return resolveOpenAiGatewayForward(body, runtime, options, false);
281
+ }
282
+ }
283
+ return {
284
+ gateway: {
285
+ ...gateway,
286
+ forward: safe,
287
+ },
288
+ forward: safe,
289
+ forwardResult: result,
290
+ completionContext: {
291
+ apiBaseUrl: forwardContext.apiBaseUrl,
292
+ deviceToken: forwardContext.deviceToken,
293
+ routeToken: forwardContext.routeToken,
294
+ poolSessionId: forwardContext.poolSessionId,
295
+ productId: decision.state === 'accepted' ? decision.productId : undefined,
296
+ requestId: gateway.requestId,
297
+ },
298
+ model,
299
+ };
300
+ }
301
+
302
+ function openAiChatCompletionResponse(model: string, content: string) {
303
+ const now = Math.floor(Date.now() / 1000);
304
+ return {
305
+ id: `chatcmpl-${now}`,
306
+ object: 'chat.completion',
307
+ created: now,
308
+ model,
309
+ choices: [
310
+ {
311
+ index: 0,
312
+ message: {
313
+ role: 'assistant',
314
+ content,
315
+ },
316
+ finish_reason: 'stop',
317
+ },
318
+ ],
319
+ usage: {
320
+ prompt_tokens: 0,
321
+ completion_tokens: 0,
322
+ total_tokens: 0,
323
+ },
324
+ };
325
+ }
326
+
327
+ function openAiChatCompletionStreamEvents(model: string, content: string) {
328
+ const now = Math.floor(Date.now() / 1000);
329
+ const id = `chatcmpl-${now}`;
330
+ return [
331
+ {
332
+ id,
333
+ object: 'chat.completion.chunk',
334
+ created: now,
335
+ model,
336
+ choices: [
337
+ {
338
+ index: 0,
339
+ delta: {
340
+ role: 'assistant',
341
+ },
342
+ finish_reason: null,
343
+ },
344
+ ],
345
+ },
346
+ {
347
+ id,
348
+ object: 'chat.completion.chunk',
349
+ created: now,
350
+ model,
351
+ choices: [
352
+ {
353
+ index: 0,
354
+ delta: {
355
+ content,
356
+ },
357
+ finish_reason: null,
358
+ },
359
+ ],
360
+ },
361
+ {
362
+ id,
363
+ object: 'chat.completion.chunk',
364
+ created: now,
365
+ model,
366
+ choices: [
367
+ {
368
+ index: 0,
369
+ delta: {},
370
+ finish_reason: 'stop',
371
+ },
372
+ ],
373
+ },
374
+ ];
375
+ }
376
+
377
+ function openAiErrorResponse(message: string) {
378
+ return {
379
+ error: {
380
+ message,
381
+ type: 'cursor_pool_gateway_error',
382
+ code: 'cursor_pool_gateway_error',
383
+ },
384
+ };
385
+ }
386
+
387
+ function openAiBypassResponse() {
388
+ return {
389
+ error: {
390
+ message: 'Cursor Pool platform mode is inactive; use the official Cursor path.',
391
+ type: 'cursor_pool_bypass',
392
+ code: 'cursor_pool_bypass',
393
+ },
394
+ };
395
+ }
396
+
397
+ function openAiFailClosedContent(forward: ReturnType<typeof sanitizeAgentGateway>['forward']) {
398
+ if (forward.state === 'rejected') {
399
+ return `Cursor Pool 号池 provider 未就绪:${forward.reason}`;
400
+ }
401
+ if (forward.state === 'not-configured') {
402
+ return 'Cursor Pool 号池 provider 未配置,当前请求已被平台接管但无法转发。';
403
+ }
404
+ if (forward.state === 'timeout') {
405
+ return 'Cursor Pool 号池 provider 响应超时,当前请求已被平台接管但没有完成。';
406
+ }
407
+ if (forward.state === 'network-error') {
408
+ return 'Cursor Pool 号池 provider 网络不可用,当前请求已被平台接管但无法转发。';
409
+ }
410
+ if (forward.state === 'skipped') {
411
+ return 'Cursor Pool 平台模式未允许本次请求。';
412
+ }
413
+ return 'Cursor Pool provider 暂时没有返回可显示内容';
414
+ }
415
+
416
+ function isOpenAiObject(value: unknown): value is Record<string, unknown> {
417
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
418
+ }
419
+
420
+ function forwardedOpenAiResponse(result: GatewayForwardResult): Record<string, unknown> | undefined {
421
+ return result.state === 'forwarded' && isOpenAiObject(result.openAiResponse)
422
+ ? result.openAiResponse
423
+ : undefined;
424
+ }
425
+
426
+ function forwardedOpenAiStreamEvents(result: GatewayForwardResult): Record<string, unknown>[] | undefined {
427
+ if (result.state !== 'forwarded' || !Array.isArray(result.openAiStreamEvents)) {
428
+ return undefined;
429
+ }
430
+ const events = result.openAiStreamEvents.filter(isOpenAiObject);
431
+ return events.length > 0 ? events : undefined;
432
+ }
433
+
434
+ async function completeForwardAfterClientResponse(context: {
435
+ apiBaseUrl?: string;
436
+ deviceToken?: string;
437
+ routeToken?: string;
438
+ poolSessionId?: string;
439
+ productId?: string;
440
+ requestId: string;
441
+ }) {
442
+ if (!context.routeToken || !context.poolSessionId || !context.productId) {
443
+ return;
444
+ }
445
+ await completeGatewayForward({
446
+ apiBaseUrl: context.apiBaseUrl,
447
+ deviceToken: context.deviceToken,
448
+ routeToken: context.routeToken,
449
+ poolSessionId: context.poolSessionId,
450
+ productId: context.productId,
451
+ requestId: context.requestId,
452
+ });
453
+ }
454
+
455
+ async function routeRequest(
456
+ request: IncomingMessage,
457
+ response: ServerResponse,
458
+ runtime: RuntimeInfo,
459
+ options: StartServerOptions,
460
+ canaryStore: ReturnType<typeof createCanaryStore>,
461
+ requestCheckStore: ReturnType<typeof createRequestCheckStore>,
462
+ gatewayStore: ReturnType<typeof createGatewayStore>,
463
+ takeoverStore: ReturnType<typeof createAgentTakeoverStore>,
464
+ stop: () => Promise<void>,
465
+ ) {
466
+ try {
467
+ if (request.method === 'OPTIONS') {
468
+ writeCorsPreflight(request, response);
469
+ return;
470
+ }
471
+ applyCorsHeaders(request, response);
472
+
473
+ if (request.method === 'GET' && request.url === '/health') {
474
+ writeJson(response, 200, buildHealth(runtime));
475
+ return;
476
+ }
477
+
478
+ if (request.method === 'GET' && request.url === '/models') {
479
+ writeJson(response, 200, openAiModelsResponse());
480
+ return;
481
+ }
482
+
483
+ if (request.method === 'POST' && request.url === '/chat/completions') {
484
+ let clientClosed = false;
485
+ let responseFinished = false;
486
+ response.once('finish', () => {
487
+ responseFinished = true;
488
+ });
489
+ response.once('close', () => {
490
+ if (!responseFinished) {
491
+ clientClosed = true;
492
+ }
493
+ });
494
+ const clientConnected = () => !clientClosed && !response.destroyed && !response.writableEnded;
495
+ const body = await readJsonRequest(request);
496
+ const { gateway, forward, forwardResult, completionContext, model } = await resolveOpenAiGatewayForward(body, runtime, options);
497
+ const recordedGateway = gatewayStore.record(gateway);
498
+ try {
499
+ await appendAgentGatewayDiagnostic(recordedGateway, {
500
+ diagnosticsFile: options.diagnosticsFile,
501
+ maxEntries: options.maxDiagnostics,
502
+ });
503
+ } catch {
504
+ // OpenAI-compatible diagnostics must not break local agent execution.
505
+ }
506
+ if (!clientConnected()) {
507
+ return;
508
+ }
509
+ if (gateway.decision.state !== 'accepted') {
510
+ writeJson(response, 409, openAiBypassResponse());
511
+ return;
512
+ }
513
+ if (forward.state !== 'forwarded') {
514
+ const content = openAiFailClosedContent(forward);
515
+ if (body.stream === true) {
516
+ writeEventStream(response, openAiChatCompletionStreamEvents(model, content));
517
+ return;
518
+ }
519
+ writeJson(response, 200, openAiChatCompletionResponse(model, content));
520
+ return;
521
+ }
522
+ if (body.stream === true) {
523
+ const providerEvents = forwardedOpenAiStreamEvents(forwardResult);
524
+ if (providerEvents) {
525
+ writeEventStream(response, providerEvents);
526
+ await completeForwardAfterClientResponse(completionContext);
527
+ return;
528
+ }
529
+ writeEventStream(
530
+ response,
531
+ openAiChatCompletionStreamEvents(
532
+ model,
533
+ forward.content ?? 'Cursor Pool provider 暂时没有返回可显示内容',
534
+ ),
535
+ );
536
+ await completeForwardAfterClientResponse(completionContext);
537
+ return;
538
+ }
539
+ const providerResponse = forwardedOpenAiResponse(forwardResult);
540
+ if (providerResponse) {
541
+ writeJson(response, 200, providerResponse);
542
+ await completeForwardAfterClientResponse(completionContext);
543
+ return;
544
+ }
545
+ writeJson(
546
+ response,
547
+ 200,
548
+ openAiChatCompletionResponse(
549
+ model,
550
+ forward.content ?? 'Cursor Pool provider 暂时没有返回可显示内容',
551
+ ),
552
+ );
553
+ await completeForwardAfterClientResponse(completionContext);
554
+ return;
555
+ }
556
+
557
+ if (request.method === 'POST' && request.url === '/cursor-metadata') {
558
+ const metadata = sanitizeServiceMetadata(await readJsonRequest(request));
559
+ await options.onMetadata?.(metadata);
560
+ writeJson(response, 200, { ok: true, metadata });
561
+ return;
562
+ }
563
+
564
+ if (request.method === 'POST' && request.url === '/agent/canary') {
565
+ let gate: RequestGateDecision;
566
+ try {
567
+ gate = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
568
+ } catch {
569
+ gate = { ...INVALID_SESSION_GATE };
570
+ }
571
+ const canary = canaryStore.record(
572
+ sanitizeAgentCanary(await readJsonRequest(request), { runtimeId: runtime.runtimeId, gate }),
573
+ );
574
+ try {
575
+ await appendAgentCanaryDiagnostic(canary, {
576
+ diagnosticsFile: options.diagnosticsFile,
577
+ maxEntries: options.maxDiagnostics,
578
+ });
579
+ } catch {
580
+ // Canary diagnostics must not break the Cursor Agent request path.
581
+ }
582
+ writeJson(response, 200, { ok: true, canary });
583
+ return;
584
+ }
585
+
586
+ if (request.method === 'GET' && request.url === '/agent/canary/latest') {
587
+ writeJson(response, 200, { ok: true, canary: canaryStore.latest() });
588
+ return;
589
+ }
590
+
591
+ if (request.method === 'POST' && request.url === '/agent/request-check') {
592
+ let decision: RequestGateDecision;
593
+ try {
594
+ decision = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
595
+ } catch {
596
+ decision = { ...INVALID_SESSION_GATE };
597
+ }
598
+ const route = decision.state === 'allowed'
599
+ ? await evaluateRouteState({ sessionFile: options.platformSessionFile })
600
+ : { state: 'missing' as const };
601
+ const check = requestCheckStore.record(
602
+ sanitizeAgentRequestCheck(await readJsonRequest(request), {
603
+ runtimeId: runtime.runtimeId,
604
+ decision,
605
+ route,
606
+ }),
607
+ );
608
+ try {
609
+ await appendAgentRequestCheckDiagnostic(check, {
610
+ diagnosticsFile: options.diagnosticsFile,
611
+ maxEntries: options.maxDiagnostics,
612
+ });
613
+ } catch {
614
+ // Request-check diagnostics must not break the local request pre-check path.
615
+ }
616
+ writeJson(response, 200, { ok: decision.state === 'allowed', check });
617
+ return;
618
+ }
619
+
620
+ if (request.method === 'GET' && request.url === '/agent/request-check/latest') {
621
+ writeJson(response, 200, { ok: true, check: requestCheckStore.latest() });
622
+ return;
623
+ }
624
+
625
+ if (request.method === 'POST' && request.url === '/agent/request-gateway') {
626
+ let gate: RequestGateDecision;
627
+ try {
628
+ gate = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
629
+ } catch {
630
+ gate = { ...INVALID_SESSION_GATE };
631
+ }
632
+ const route = gate.state === 'allowed'
633
+ ? await evaluateRouteState({ sessionFile: options.platformSessionFile })
634
+ : { state: 'missing' as const };
635
+ const decision = evaluateGatewayDecision(gate, route);
636
+ const body = await readJsonRequest(request);
637
+ const gateway = sanitizeAgentGateway(body, {
638
+ runtimeId: runtime.runtimeId,
639
+ decision,
640
+ });
641
+ const forwardContext = decision.state === 'accepted'
642
+ ? await readPlatformGatewayForwardContext({ sessionFile: options.platformSessionFile })
643
+ : {};
644
+ const forwarder = options.gatewayForwarder ?? (
645
+ decision.state === 'accepted'
646
+ ? createGatewayHttpForwarder({
647
+ apiBaseUrl: forwardContext.apiBaseUrl,
648
+ deviceToken: forwardContext.deviceToken,
649
+ })
650
+ : undefined
651
+ );
652
+ gateway.forward = await resolveGatewayForward({
653
+ gateway,
654
+ routeToken: forwardContext.routeToken ?? '',
655
+ poolSessionId: forwardContext.poolSessionId,
656
+ productId: decision.state === 'accepted' ? decision.productId : 'unknown',
657
+ model: gateway.model,
658
+ requestId: gateway.requestId,
659
+ forwarder,
660
+ });
661
+ const recordedGateway = gatewayStore.record(gateway);
662
+ try {
663
+ await appendAgentGatewayDiagnostic(recordedGateway, {
664
+ diagnosticsFile: options.diagnosticsFile,
665
+ maxEntries: options.maxDiagnostics,
666
+ });
667
+ } catch {
668
+ // Gateway diagnostics must not break the local gateway preflight path.
669
+ }
670
+ writeJson(response, 200, {
671
+ ok: decision.state === 'accepted' && recordedGateway.forward.state === 'forwarded',
672
+ gateway: recordedGateway,
673
+ });
674
+ return;
675
+ }
676
+
677
+ if (request.method === 'GET' && request.url === '/agent/request-gateway/latest') {
678
+ writeJson(response, 200, { ok: true, gateway: gatewayStore.latest() });
679
+ return;
680
+ }
681
+
682
+ if (request.method === 'POST' && request.url === '/agent/takeover') {
683
+ const body = await readJsonRequest(request);
684
+ let gate: RequestGateDecision;
685
+ try {
686
+ gate = await evaluateRequestGate({ sessionFile: options.platformSessionFile });
687
+ } catch {
688
+ gate = { ...INVALID_SESSION_GATE };
689
+ }
690
+ const route = gate.state === 'allowed'
691
+ ? await evaluateRouteState({ sessionFile: options.platformSessionFile })
692
+ : { state: 'missing' as const };
693
+ let forward;
694
+ if (route.state === 'ready') {
695
+ const decision = evaluateGatewayDecision(gate, route);
696
+ const gateway = sanitizeAgentGateway(body, {
697
+ runtimeId: runtime.runtimeId,
698
+ decision,
699
+ });
700
+ const forwardContext = await readPlatformGatewayForwardContext({ sessionFile: options.platformSessionFile });
701
+ const forwarder = options.gatewayForwarder ?? createGatewayHttpForwarder({
702
+ apiBaseUrl: forwardContext.apiBaseUrl,
703
+ deviceToken: forwardContext.deviceToken,
704
+ });
705
+ forward = await resolveGatewayForward({
706
+ gateway,
707
+ routeToken: forwardContext.routeToken ?? '',
708
+ poolSessionId: forwardContext.poolSessionId,
709
+ productId: decision.state === 'accepted' ? decision.productId : 'unknown',
710
+ model: gateway.model,
711
+ requestId: gateway.requestId,
712
+ forwarder,
713
+ });
714
+ }
715
+ const takeover = takeoverStore.record(
716
+ buildAgentTakeoverResponse(body, gate, route, forward),
717
+ );
718
+ writeJson(response, 200, takeover);
719
+ return;
720
+ }
721
+
722
+ if (request.method === 'GET' && request.url === '/agent/takeover/latest') {
723
+ writeJson(response, 200, { ok: true, takeover: takeoverStore.latest() });
724
+ return;
725
+ }
726
+
727
+ if (request.method === 'POST' && request.url === '/extension/status') {
728
+ const status = sanitizeExtensionStatus(await readJsonRequest(request));
729
+ writeJson(response, 200, { ok: true, status });
730
+ return;
731
+ }
732
+
733
+ if (request.method === 'GET' && request.url === '/platform/status') {
734
+ writeJson(response, 200, await platformStatus({ sessionFile: options.platformSessionFile }));
735
+ return;
736
+ }
737
+
738
+ if (request.method === 'GET' && request.url === '/platform/catalog') {
739
+ writeJson(response, 200, await platformCatalog({ sessionFile: options.platformSessionFile }));
740
+ return;
741
+ }
742
+
743
+ if (request.method === 'POST' && request.url === '/platform/selection') {
744
+ const body = await readJsonRequest(request);
745
+ if (typeof body.productId !== 'string' || body.productId.trim() === '') {
746
+ throw new Error('invalid platform selection request');
747
+ }
748
+
749
+ const selection = await selectPlatformProduct({
750
+ sessionFile: options.platformSessionFile,
751
+ productId: body.productId,
752
+ });
753
+ writeJson(response, 200, selection);
754
+ return;
755
+ }
756
+
757
+ if (request.method === 'POST' && request.url === '/platform/login') {
758
+ const body = await readJsonRequest(request);
759
+ const apiBaseUrl = await resolvePlatformApiBaseUrl(options);
760
+ if (typeof body.email === 'string' || typeof body.password === 'string') {
761
+ if (
762
+ typeof body.email !== 'string' ||
763
+ typeof body.password !== 'string' ||
764
+ typeof apiBaseUrl !== 'string' ||
765
+ apiBaseUrl.trim() === ''
766
+ ) {
767
+ throw new Error('invalid platform login request');
768
+ }
769
+
770
+ let login;
771
+ try {
772
+ login = await loginWithPassword({
773
+ email: body.email,
774
+ password: body.password,
775
+ apiBaseUrl,
776
+ sessionFile: options.platformSessionFile,
777
+ device: asRecord(body.device) as PlatformDeviceInput,
778
+ exchangePasswordDeviceToken: options.platformExchangePasswordDeviceToken,
779
+ });
780
+ } catch (error) {
781
+ const platformCode = (error as { platformCode?: unknown }).platformCode;
782
+ writeJson(response, 400, {
783
+ ok: false,
784
+ error: 'platform login failed',
785
+ ...(typeof platformCode === 'string' ? { code: platformCode } : {}),
786
+ });
787
+ return;
788
+ }
789
+ writeJson(response, 200, login);
790
+ return;
791
+ }
792
+
793
+ if (typeof body.code !== 'string' || typeof body.apiBaseUrl !== 'string') {
794
+ throw new Error('invalid platform login request');
795
+ }
796
+
797
+ writeJson(response, 200, await loginWithCode({
798
+ code: body.code,
799
+ apiBaseUrl: body.apiBaseUrl,
800
+ sessionFile: options.platformSessionFile,
801
+ device: asRecord(body.device) as PlatformDeviceInput,
802
+ exchangeDeviceToken: options.platformExchangeDeviceToken,
803
+ }));
804
+ return;
805
+ }
806
+
807
+ if (request.method === 'POST' && request.url === '/platform/heartbeat') {
808
+ writeJson(response, 200, await sendHeartbeat({
809
+ sessionFile: options.platformSessionFile,
810
+ payload: await readJsonRequest(request) as HeartbeatPayload,
811
+ postHeartbeat: options.platformPostHeartbeat,
812
+ }));
813
+ return;
814
+ }
815
+
816
+ if (request.method === 'POST' && request.url === '/platform/logout') {
817
+ writeJson(response, 200, await logoutPlatform({
818
+ sessionFile: options.platformSessionFile,
819
+ postLogout: options.platformPostLogout,
820
+ }));
821
+ return;
822
+ }
823
+
824
+ if (request.method === 'POST' && request.url === '/mode/start') {
825
+ writeJson(response, 200, await startPlatformMode({
826
+ sessionFile: options.platformSessionFile,
827
+ startPoolSession: options.platformStartPoolSession,
828
+ }));
829
+ return;
830
+ }
831
+
832
+ if (request.method === 'POST' && request.url === '/mode/stop') {
833
+ writeJson(response, 200, await stopPlatformMode({
834
+ sessionFile: options.platformSessionFile,
835
+ stopPoolSession: options.platformStopPoolSession,
836
+ }));
837
+ return;
838
+ }
839
+
840
+ if (request.method === 'POST' && request.url === '/shutdown') {
841
+ writeJson(response, 200, { ok: true });
842
+ response.once('finish', () => {
843
+ void stop().then(() => options.onShutdown?.());
844
+ });
845
+ return;
846
+ }
847
+
848
+ writeJson(response, 404, { ok: false, error: 'not found' });
849
+ } catch {
850
+ writeJson(response, 400, { ok: false, error: 'invalid request' });
851
+ }
852
+ }
853
+
854
+ function listen(server: Server, port: number): Promise<void> {
855
+ return new Promise((resolve, reject) => {
856
+ const onError = (error: Error) => {
857
+ server.off('listening', onListening);
858
+ reject(error);
859
+ };
860
+ const onListening = () => {
861
+ server.off('error', onError);
862
+ resolve();
863
+ };
864
+
865
+ server.once('error', onError);
866
+ server.once('listening', onListening);
867
+ server.listen(port, LOOPBACK_HOST);
868
+ });
869
+ }
870
+
871
+ async function closeServer(server: Server) {
872
+ if (!server.listening) {
873
+ return;
874
+ }
875
+
876
+ await new Promise<void>((resolve, reject) => {
877
+ server.close((error) => (error ? reject(error) : resolve()));
878
+ });
879
+ }
880
+
881
+ export async function startServer(options: StartServerOptions = {}): Promise<LocalService> {
882
+ let requestedPort = options.port ?? DEFAULT_PORT;
883
+
884
+ for (;;) {
885
+ const runtime: RuntimeInfo = {
886
+ host: LOOPBACK_HOST,
887
+ port: requestedPort,
888
+ runtimeId: createRuntimeId(),
889
+ };
890
+ const canaryStore = createCanaryStore();
891
+ const requestCheckStore = createRequestCheckStore();
892
+ const gatewayStore = createGatewayStore();
893
+ const takeoverStore = createAgentTakeoverStore();
894
+ const server = createServer((request, response) => {
895
+ void routeRequest(
896
+ request,
897
+ response,
898
+ runtime,
899
+ options,
900
+ canaryStore,
901
+ requestCheckStore,
902
+ gatewayStore,
903
+ takeoverStore,
904
+ stopService,
905
+ );
906
+ });
907
+ const stopService = async () => {
908
+ await closeServer(server);
909
+ localServices.delete(runtimeKey(runtime));
910
+ };
911
+
912
+ try {
913
+ await listen(server, requestedPort);
914
+ const address = server.address();
915
+ if (typeof address !== 'object' || address === null) {
916
+ await closeServer(server);
917
+ throw new Error('service did not bind a TCP address');
918
+ }
919
+
920
+ runtime.port = address.port;
921
+ await writeRuntimeInfo(runtime, { runtimeFile: options.runtimeFile });
922
+
923
+ const service: LocalService = {
924
+ ...runtime,
925
+ server,
926
+ stop: stopService,
927
+ };
928
+ localServices.set(runtimeKey(runtime), service);
929
+
930
+ return service;
931
+ } catch (error) {
932
+ await closeServer(server);
933
+ if ((error as NodeJS.ErrnoException).code !== 'EADDRINUSE') {
934
+ throw error;
935
+ }
936
+ requestedPort = 0;
937
+ }
938
+ }
939
+ }