@velum-labs/cursorkit 0.1.0

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 (142) hide show
  1. package/DISCLAIMER.md +12 -0
  2. package/README.md +157 -0
  3. package/dist/src/agentTools/diff.d.ts +11 -0
  4. package/dist/src/agentTools/diff.js +88 -0
  5. package/dist/src/agentTools/policy.d.ts +3 -0
  6. package/dist/src/agentTools/policy.js +12 -0
  7. package/dist/src/agentTools/registry.d.ts +114 -0
  8. package/dist/src/agentTools/registry.js +663 -0
  9. package/dist/src/agentTools/results.d.ts +14 -0
  10. package/dist/src/agentTools/results.js +117 -0
  11. package/dist/src/agentTools/schemas.d.ts +3 -0
  12. package/dist/src/agentTools/schemas.js +89 -0
  13. package/dist/src/agentTools/surface.d.ts +11 -0
  14. package/dist/src/agentTools/surface.js +251 -0
  15. package/dist/src/certs.d.ts +8 -0
  16. package/dist/src/certs.js +34 -0
  17. package/dist/src/ck.d.ts +2 -0
  18. package/dist/src/ck.js +6 -0
  19. package/dist/src/ckLauncher.d.ts +150 -0
  20. package/dist/src/ckLauncher.js +1496 -0
  21. package/dist/src/cli.d.ts +2 -0
  22. package/dist/src/cli.js +265 -0
  23. package/dist/src/config.d.ts +52 -0
  24. package/dist/src/config.js +210 -0
  25. package/dist/src/connectEnvelope.d.ts +16 -0
  26. package/dist/src/connectEnvelope.js +70 -0
  27. package/dist/src/desktop.d.ts +19 -0
  28. package/dist/src/desktop.js +167 -0
  29. package/dist/src/desktopConnectProxy.d.ts +26 -0
  30. package/dist/src/desktopConnectProxy.js +175 -0
  31. package/dist/src/extensions/index.d.ts +2 -0
  32. package/dist/src/extensions/index.js +1 -0
  33. package/dist/src/extensions/registry.d.ts +8 -0
  34. package/dist/src/extensions/registry.js +52 -0
  35. package/dist/src/extensions/types.d.ts +42 -0
  36. package/dist/src/extensions/types.js +1 -0
  37. package/dist/src/fixtures/modelFusion.d.ts +103 -0
  38. package/dist/src/fixtures/modelFusion.js +404 -0
  39. package/dist/src/fixtures/replay.d.ts +9 -0
  40. package/dist/src/fixtures/replay.js +41 -0
  41. package/dist/src/fixtures/sanitizer.d.ts +9 -0
  42. package/dist/src/fixtures/sanitizer.js +43 -0
  43. package/dist/src/fixtures/schema.d.ts +38 -0
  44. package/dist/src/fixtures/schema.js +33 -0
  45. package/dist/src/gen/agent/v1/agent_pb.d.ts +21577 -0
  46. package/dist/src/gen/agent/v1/agent_pb.js +5325 -0
  47. package/dist/src/gen/aiserver/v1/aiserver_pb.d.ts +135242 -0
  48. package/dist/src/gen/aiserver/v1/aiserver_pb.js +34430 -0
  49. package/dist/src/gen/anyrun/v1/anyrun_pb.d.ts +1163 -0
  50. package/dist/src/gen/anyrun/v1/anyrun_pb.js +374 -0
  51. package/dist/src/gen/google/protobuf/google_pb.d.ts +142 -0
  52. package/dist/src/gen/google/protobuf/google_pb.js +54 -0
  53. package/dist/src/gen/internapi/v1/internapi_pb.d.ts +121 -0
  54. package/dist/src/gen/internapi/v1/internapi_pb.js +79 -0
  55. package/dist/src/logger.d.ts +8 -0
  56. package/dist/src/logger.js +37 -0
  57. package/dist/src/modelFusion/cursorHarness.d.ts +146 -0
  58. package/dist/src/modelFusion/cursorHarness.js +647 -0
  59. package/dist/src/modelFusion/index.d.ts +4 -0
  60. package/dist/src/modelFusion/index.js +2 -0
  61. package/dist/src/models/registry.d.ts +22 -0
  62. package/dist/src/models/registry.js +30 -0
  63. package/dist/src/proto.d.ts +13 -0
  64. package/dist/src/proto.js +61 -0
  65. package/dist/src/providers/openai.d.ts +64 -0
  66. package/dist/src/providers/openai.js +355 -0
  67. package/dist/src/redaction.d.ts +4 -0
  68. package/dist/src/redaction.js +65 -0
  69. package/dist/src/routeInventory.d.ts +16 -0
  70. package/dist/src/routeInventory.js +39 -0
  71. package/dist/src/routes.d.ts +37 -0
  72. package/dist/src/routes.js +227 -0
  73. package/dist/src/server.d.ts +50 -0
  74. package/dist/src/server.js +1353 -0
  75. package/dist/src/services/agent.d.ts +1 -0
  76. package/dist/src/services/agent.js +7 -0
  77. package/dist/src/services/agentRun.d.ts +60 -0
  78. package/dist/src/services/agentRun.js +391 -0
  79. package/dist/src/services/chat.d.ts +11 -0
  80. package/dist/src/services/chat.js +47 -0
  81. package/dist/src/services/models.d.ts +10 -0
  82. package/dist/src/services/models.js +216 -0
  83. package/dist/src/services/serverConfig.d.ts +2 -0
  84. package/dist/src/services/serverConfig.js +19 -0
  85. package/dist/src/testing/artifacts.d.ts +14 -0
  86. package/dist/src/testing/artifacts.js +92 -0
  87. package/dist/src/testing/cli.d.ts +4 -0
  88. package/dist/src/testing/cli.js +192 -0
  89. package/dist/src/testing/localBackend.d.ts +24 -0
  90. package/dist/src/testing/localBackend.js +310 -0
  91. package/dist/src/testing/processRunner.d.ts +7 -0
  92. package/dist/src/testing/processRunner.js +74 -0
  93. package/dist/src/testing/runner.d.ts +9 -0
  94. package/dist/src/testing/runner.js +85 -0
  95. package/dist/src/testing/scenarios.d.ts +3 -0
  96. package/dist/src/testing/scenarios.js +2535 -0
  97. package/dist/src/testing/types.d.ts +66 -0
  98. package/dist/src/testing/types.js +1 -0
  99. package/dist/src/tools/baselineInventory.d.ts +12 -0
  100. package/dist/src/tools/baselineInventory.js +680 -0
  101. package/dist/src/tools/checkModelFusionProtocol.d.ts +1 -0
  102. package/dist/src/tools/checkModelFusionProtocol.js +274 -0
  103. package/dist/src/tools/checkReleasePublishConfig.d.ts +1 -0
  104. package/dist/src/tools/checkReleasePublishConfig.js +99 -0
  105. package/dist/src/tools/generateProtoInventory.d.ts +1 -0
  106. package/dist/src/tools/generateProtoInventory.js +89 -0
  107. package/dist/src/tools/normalizeGeneratedCode.d.ts +1 -0
  108. package/dist/src/tools/normalizeGeneratedCode.js +18 -0
  109. package/dist/src/tools/releaseCheck.d.ts +26 -0
  110. package/dist/src/tools/releaseCheck.js +367 -0
  111. package/dist/src/trace.d.ts +39 -0
  112. package/dist/src/trace.js +106 -0
  113. package/dist/src/translation.d.ts +6 -0
  114. package/dist/src/translation.js +22 -0
  115. package/dist/src/upstream.d.ts +20 -0
  116. package/dist/src/upstream.js +270 -0
  117. package/docs/configuration.md +55 -0
  118. package/docs/cursor-app.md +263 -0
  119. package/docs/implementation-inventory.json +609 -0
  120. package/docs/learnings.md +363 -0
  121. package/docs/model-fusion-protocol-origin.json +126 -0
  122. package/docs/model-fusion-protocol.md +110 -0
  123. package/docs/plugin-authoring.md +24 -0
  124. package/docs/proto-inventory.md +1477 -0
  125. package/docs/protocol-surface-audit.md +92 -0
  126. package/docs/protocol.md +52 -0
  127. package/docs/refreshing-protos.md +78 -0
  128. package/docs/release-gates.md +110 -0
  129. package/docs/release-summary.json +86 -0
  130. package/docs/route-contract-manifest.json +288 -0
  131. package/docs/route-policy.json +133 -0
  132. package/docs/service-manifest.json +9490 -0
  133. package/docs/test-manifest.json +155 -0
  134. package/docs/testing-harness.md +204 -0
  135. package/docs/troubleshooting.md +36 -0
  136. package/docs/type-manifest-summary.json +28927 -0
  137. package/package.json +93 -0
  138. package/proto/agent/v1/agent.proto +5371 -0
  139. package/proto/aiserver/v1/aiserver.proto +32944 -0
  140. package/proto/anyrun/v1/anyrun.proto +294 -0
  141. package/proto/google/protobuf/google.proto +37 -0
  142. package/proto/internapi/v1/internapi.proto +32 -0
@@ -0,0 +1,2535 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import http from "node:http";
4
+ import net from "node:net";
5
+ import path from "node:path";
6
+ import readline from "node:readline";
7
+ import { WebSocket } from "undici";
8
+ import { analyzeRouteInventoryLog, cleanupIsolatedCursorProcesses, seedLocalModelsIntoCursorState, } from "../ckLauncher.js";
9
+ import { AGENT_RUN_PATH, AGENT_RUN_SSE_PATH, BIDI_APPEND_PATH, STREAM_CHAT_WITH_TOOLS_PATH, } from "../routes.js";
10
+ import { probeLocalBackend } from "./localBackend.js";
11
+ export function createScenarios() {
12
+ return [
13
+ staticScenario(),
14
+ bridgeProtocolScenario(),
15
+ localBackendScenario(),
16
+ cursorAgentScenario(),
17
+ cursorAgentTrafficScenario(),
18
+ cursorAgentAcpScenario(),
19
+ desktopRouteScenario(),
20
+ desktopUiScenario(),
21
+ ];
22
+ }
23
+ function staticScenario() {
24
+ return {
25
+ id: "static",
26
+ suite: "static",
27
+ description: "Build, unit/integration tests, and format check",
28
+ async run(context) {
29
+ const started = Date.now();
30
+ const result = await context.processRunner.run({
31
+ command: "pnpm",
32
+ args: ["check"],
33
+ timeoutMs: context.options.timeoutMs,
34
+ logName: "static-pnpm-check",
35
+ });
36
+ return processScenarioResult({
37
+ id: "static",
38
+ suite: "static",
39
+ started,
40
+ result,
41
+ successMessage: "pnpm check passed",
42
+ failureCode: result.timedOut ? "timeout" : "command_failed",
43
+ });
44
+ },
45
+ };
46
+ }
47
+ function bridgeProtocolScenario() {
48
+ return {
49
+ id: "bridge-protocol",
50
+ suite: "bridge-protocol",
51
+ description: "Bridge protocol integration and route tests",
52
+ async run(context) {
53
+ const started = Date.now();
54
+ const result = await context.processRunner.run({
55
+ command: "pnpm",
56
+ args: [
57
+ "exec",
58
+ "vitest",
59
+ "run",
60
+ "tests/server.integration.test.ts",
61
+ "tests/routeInventory.test.ts",
62
+ "tests/upstream.test.ts",
63
+ ],
64
+ timeoutMs: context.options.timeoutMs,
65
+ logName: "bridge-protocol-vitest",
66
+ });
67
+ return processScenarioResult({
68
+ id: "bridge-protocol",
69
+ suite: "bridge-protocol",
70
+ started,
71
+ result,
72
+ successMessage: "bridge protocol tests passed",
73
+ failureCode: result.timedOut ? "timeout" : "command_failed",
74
+ });
75
+ },
76
+ };
77
+ }
78
+ function localBackendScenario() {
79
+ return {
80
+ id: "local-backend",
81
+ suite: "local-backend",
82
+ description: "OpenAI-compatible local backend probe",
83
+ async run(context) {
84
+ const started = Date.now();
85
+ const report = await probeLocalBackend({
86
+ baseUrl: context.options.baseUrl,
87
+ model: context.options.providerModel,
88
+ apiKey: context.options.apiKey,
89
+ timeoutMs: context.options.timeoutMs,
90
+ });
91
+ const artifact = context.artifacts.writeJson("local-backend-report.json", report);
92
+ return {
93
+ id: "local-backend",
94
+ suite: "local-backend",
95
+ status: report.ok ? "passed" : "failed",
96
+ durationMs: Date.now() - started,
97
+ message: report.message,
98
+ failureCode: report.failureCode,
99
+ artifacts: { report: artifact },
100
+ details: {
101
+ modelsStatus: report.modelsStatus,
102
+ chatStatus: report.chatStatus,
103
+ models: report.models,
104
+ completionPreview: report.completionPreview,
105
+ },
106
+ };
107
+ },
108
+ };
109
+ }
110
+ function cursorAgentScenario() {
111
+ return {
112
+ id: "cursor-agent",
113
+ suite: "cursor-agent",
114
+ description: "Real cursor-agent CLI smoke test",
115
+ async run(context) {
116
+ const started = Date.now();
117
+ const result = await context.processRunner.run({
118
+ command: "pnpm",
119
+ args: ["e2e:cursor-agent"],
120
+ env: isolatedCursorAgentEnv(context.options),
121
+ timeoutMs: context.options.timeoutMs,
122
+ logName: "cursor-agent-e2e",
123
+ });
124
+ return processScenarioResult({
125
+ id: "cursor-agent",
126
+ suite: "cursor-agent",
127
+ started,
128
+ result,
129
+ successMessage: "cursor-agent e2e passed",
130
+ failureCode: result.timedOut ? "timeout" : "model_metadata_rejected",
131
+ });
132
+ },
133
+ };
134
+ }
135
+ function cursorAgentTrafficScenario() {
136
+ return {
137
+ id: "cursor-agent-traffic",
138
+ suite: "cursor-agent-traffic",
139
+ description: "Capture real cursor-agent traffic through bridge route inventory",
140
+ async run(context) {
141
+ const started = Date.now();
142
+ const port = await freePort();
143
+ const endpoint = `http://127.0.0.1:${port}`;
144
+ const bridgeLogPath = context.artifacts.pathFor("traffic-probe-bridge.log");
145
+ const bridge = startTrafficProbeBridge(context, port, bridgeLogPath);
146
+ try {
147
+ await waitForFilePattern(bridgeLogPath, /bridge listening/, bridge);
148
+ const listModels = await context.processRunner.run({
149
+ command: "cursor-agent",
150
+ args: ["--endpoint", endpoint, "--list-models"],
151
+ env: scrubbedHarnessEnv(context.options.env),
152
+ timeoutMs: context.options.timeoutMs,
153
+ logName: "traffic-cursor-agent-list-models",
154
+ });
155
+ const prompt = await context.processRunner.run({
156
+ command: "cursor-agent",
157
+ args: [
158
+ "--endpoint",
159
+ endpoint,
160
+ "--model",
161
+ context.options.model,
162
+ "--print",
163
+ "Reply with traffic-probe-ok.",
164
+ ],
165
+ env: scrubbedHarnessEnv(context.options.env),
166
+ timeoutMs: context.options.timeoutMs,
167
+ logName: "traffic-cursor-agent-print",
168
+ });
169
+ await waitForFilePattern(bridgeLogPath, /desktop route inventory/, bridge, 2_000).catch(() => undefined);
170
+ const bridgeLog = fs.existsSync(bridgeLogPath)
171
+ ? fs.readFileSync(bridgeLogPath, "utf8")
172
+ : "";
173
+ const routeReport = analyzeRouteInventoryLog(bridgeLog);
174
+ const agentRunDiagnostics = parseAgentRunDiagnostics(bridgeLog);
175
+ const report = {
176
+ endpoint,
177
+ model: context.options.model,
178
+ listModels: processRunSummary(listModels),
179
+ prompt: processRunSummary(prompt),
180
+ routeInventory: routeReport,
181
+ agentRunDiagnostics,
182
+ listedModel: listModels.stdout.includes(context.options.model) ||
183
+ listModels.stdout.includes(context.options.displayName),
184
+ completedPrompt: prompt.stdout.includes("traffic-probe-ok") ||
185
+ prompt.stdout.includes(context.options.model),
186
+ };
187
+ const reportPath = context.artifacts.writeJson("traffic-probe-report.json", report);
188
+ const failureCode = trafficFailureCode(report);
189
+ return {
190
+ id: "cursor-agent-traffic",
191
+ suite: "cursor-agent-traffic",
192
+ status: failureCode === undefined ? "passed" : "failed",
193
+ durationMs: Date.now() - started,
194
+ message: failureCode === undefined
195
+ ? "cursor-agent traffic reached the bridge and completed the probe"
196
+ : trafficFailureMessage(failureCode),
197
+ failureCode,
198
+ artifacts: {
199
+ bridgeLog: bridgeLogPath,
200
+ listModelsLog: listModels.logPath,
201
+ promptLog: prompt.logPath,
202
+ report: reportPath,
203
+ },
204
+ details: {
205
+ endpoint,
206
+ observedPaths: routeReport.observedPaths,
207
+ modelRoutesSeen: routeReport.modelRoutesSeen,
208
+ agentRunDiagnostics,
209
+ listedModel: report.listedModel,
210
+ completedPrompt: report.completedPrompt,
211
+ },
212
+ };
213
+ }
214
+ finally {
215
+ bridge.kill("SIGTERM");
216
+ }
217
+ },
218
+ };
219
+ }
220
+ function cursorAgentAcpScenario() {
221
+ return {
222
+ id: "cursor-agent-acp-experimental",
223
+ suite: "cursor-agent-acp-experimental",
224
+ description: "Experimental ACP JSON-RPC smoke test through the bridge",
225
+ async run(context) {
226
+ const started = Date.now();
227
+ const port = await freePort();
228
+ const endpoint = `http://127.0.0.1:${port}`;
229
+ const bridgeLogPath = context.artifacts.pathFor("acp-probe-bridge.log");
230
+ const bridge = startTrafficProbeBridge(context, port, bridgeLogPath);
231
+ try {
232
+ await waitForFilePattern(bridgeLogPath, /bridge listening/, bridge);
233
+ const report = await runAcpProbe(context, endpoint, bridgeLogPath);
234
+ const reportPath = context.artifacts.writeJson("acp-probe-report.json", report);
235
+ const failureCode = acpFailureCode(report);
236
+ return {
237
+ id: "cursor-agent-acp-experimental",
238
+ suite: "cursor-agent-acp-experimental",
239
+ status: failureCode === undefined ? "passed" : "failed",
240
+ durationMs: Date.now() - started,
241
+ message: failureCode === undefined
242
+ ? "ACP JSON-RPC prompt reached the bridge and completed"
243
+ : acpFailureMessage(failureCode),
244
+ failureCode,
245
+ artifacts: {
246
+ bridgeLog: bridgeLogPath,
247
+ report: reportPath,
248
+ },
249
+ details: {
250
+ endpoint,
251
+ textPreview: report.textPreview,
252
+ observedPaths: report.routeInventory.observedPaths,
253
+ modelRoutesSeen: report.routeInventory.modelRoutesSeen,
254
+ },
255
+ };
256
+ }
257
+ finally {
258
+ bridge.kill("SIGTERM");
259
+ }
260
+ },
261
+ };
262
+ }
263
+ function desktopUiScenario() {
264
+ return {
265
+ id: "desktop-ui-experimental",
266
+ suite: "desktop-ui-experimental",
267
+ description: "Optional Cursor desktop CDP attachment probe",
268
+ async run(context) {
269
+ const started = Date.now();
270
+ const debugPort = await freePort();
271
+ const instanceId = `desktop-ui-${debugPort}`;
272
+ const scriptedBackend = context.options.env.E2E_SCRIPTED_TOOL_BACKEND === "true"
273
+ ? await startScriptedToolBackend()
274
+ : undefined;
275
+ const effectiveBaseUrl = scriptedBackend?.baseUrl ?? context.options.baseUrl;
276
+ const ckLogPath = path.join(context.options.cwd, ".cursor-rpc", "ck", instanceId, "bridge.log");
277
+ const userDataDir = path.join(context.options.cwd, ".cursor-rpc", "ck", instanceId, "user-data");
278
+ const modelEnv = {
279
+ BRIDGE_MODELS_JSON: JSON.stringify([
280
+ {
281
+ id: context.options.model,
282
+ displayName: context.options.displayName,
283
+ providerModel: context.options.providerModel,
284
+ baseUrl: effectiveBaseUrl,
285
+ apiKey: context.options.apiKey,
286
+ contextTokenLimit: 128000,
287
+ },
288
+ ]),
289
+ ...(context.options.env.BRIDGE_LOG_MODEL_PAYLOADS !== undefined
290
+ ? {
291
+ BRIDGE_LOG_MODEL_PAYLOADS: context.options.env.BRIDGE_LOG_MODEL_PAYLOADS,
292
+ }
293
+ : {}),
294
+ BRIDGE_AGENT_NATIVE_CONTEXT: "false",
295
+ };
296
+ const commandLogPath = context.artifacts.writeText("logs/desktop-ui-ck-live.log", "");
297
+ const log = fs.createWriteStream(commandLogPath, { flags: "w" });
298
+ const writeCommandLog = (chunk) => {
299
+ if (!log.writableEnded && !log.destroyed) {
300
+ log.write(chunk);
301
+ }
302
+ };
303
+ const ck = spawn("pnpm", desktopUiCkArgs(context.options, debugPort, instanceId), {
304
+ cwd: context.options.cwd,
305
+ env: {
306
+ ...process.env,
307
+ ...scrubbedHarnessEnv(context.options.env),
308
+ ...modelEnv,
309
+ },
310
+ detached: true,
311
+ stdio: ["ignore", "pipe", "pipe"],
312
+ });
313
+ ck.stdout?.on("data", writeCommandLog);
314
+ ck.stderr?.on("data", writeCommandLog);
315
+ try {
316
+ await waitForProcessOutput(ck, /bridge listening/, 15_000);
317
+ await waitForDesktopWorkbenchReady(debugPort, context.options.timeoutMs);
318
+ const localModelSeedStatus = seedLocalModelsIntoCursorState({
319
+ agentHttpPort: debugPort + 4,
320
+ bridge: {
321
+ executable: "pnpm",
322
+ args: [],
323
+ env: modelEnv,
324
+ },
325
+ profileMode: "isolated",
326
+ userDataDir,
327
+ });
328
+ await reloadDesktopWorkbench(debugPort, 10_000);
329
+ const cdp = await probeDesktopCdp(debugPort, context.options.timeoutMs, {
330
+ displayName: context.options.displayName,
331
+ model: context.options.model,
332
+ });
333
+ if (cdp.desktopPromptSubmitted) {
334
+ await waitForDesktopCompletionLog(ckLogPath, context.options.timeoutMs);
335
+ }
336
+ else {
337
+ await delay(2_000);
338
+ }
339
+ const ckLog = fs.existsSync(ckLogPath)
340
+ ? fs.readFileSync(ckLogPath, "utf8")
341
+ : "";
342
+ const routeInventory = analyzeRouteInventoryLog(ckLog);
343
+ const modelBackendRequestSeen = desktopModelBackendRequestSeen(ckLog, effectiveBaseUrl);
344
+ const modelBackendResponseComplete = desktopModelBackendResponseComplete(ckLog, effectiveBaseUrl);
345
+ const cursorToolResultSeen = desktopCursorToolResultSeen(ckLog);
346
+ const requiredCursorToolResultsSeen = scriptedBackend === undefined ||
347
+ desktopCursorToolNamesSeen(ckLog, ["read_file", "list_dir", "grep"]);
348
+ const backendRequestEvidence = desktopBackendRequestEvidence(ckLog, effectiveBaseUrl, modelBackendRequestSeen, modelBackendResponseComplete);
349
+ const routeUncertaintyEvidence = {
350
+ observedPaths: routeInventory.observedPaths,
351
+ routeSummary: routeInventory.routeSummary,
352
+ routeCategories: routeInventory.routeCategories,
353
+ passThroughRoutes: routeInventory.passThroughRoutes,
354
+ failedRoutes: routeInventory.failedRoutes,
355
+ };
356
+ const report = {
357
+ debugPort,
358
+ instanceId,
359
+ ckProfileMode: "isolated-seeded-from-default",
360
+ requestedDefaultProfile: context.options.useDefaultProfile,
361
+ localModelSeedStatus,
362
+ cdp,
363
+ routeInventory,
364
+ routeUncertaintyEvidence,
365
+ pickerDomEvidence: cdp.pickerDomEvidence,
366
+ selectedModelEvidence: cdp.selectedModelEvidence,
367
+ composerSubmissionEvidence: cdp.composerSubmissionEvidence,
368
+ backendRequestEvidence,
369
+ visibleResponseEvidence: cdp.visibleResponseEvidence,
370
+ modelBackendRequestSeen,
371
+ modelBackendResponseComplete,
372
+ cursorToolResultSeen,
373
+ requiredCursorToolResultsSeen,
374
+ scriptedBackendRequests: scriptedBackend?.requests,
375
+ };
376
+ const reportPath = context.artifacts.writeJson("desktop-ui-cdp-report.json", report);
377
+ const pickerDomEvidencePath = context.artifacts.writeJson("desktop-ui-picker-dom-evidence.json", cdp.pickerDomEvidence ?? {});
378
+ const selectedModelEvidencePath = context.artifacts.writeJson("desktop-ui-selected-model-evidence.json", cdp.selectedModelEvidence ?? {});
379
+ const composerSubmissionEvidencePath = context.artifacts.writeJson("desktop-ui-composer-submission-evidence.json", cdp.composerSubmissionEvidence ?? {});
380
+ const backendRequestEvidencePath = context.artifacts.writeJson("desktop-ui-mlx-backend-request-evidence.json", backendRequestEvidence);
381
+ const visibleResponseEvidencePath = context.artifacts.writeJson("desktop-ui-visible-response-evidence.json", cdp.visibleResponseEvidence ?? {});
382
+ const routeUncertaintyEvidencePath = context.artifacts.writeJson("desktop-ui-route-uncertainty.json", routeUncertaintyEvidence);
383
+ const desktopSendRoutesSeen = desktopSendRoutes(routeInventory);
384
+ const ckBridgeLogArtifact = captureTextArtifactIfExists(ckLogPath, "logs/desktop-ui-bridge.log", context);
385
+ const ckConnectProxyLogArtifact = captureTextArtifactIfExists(path.join(context.options.cwd, ".cursor-rpc", "ck", instanceId, "connect-proxy.log"), "logs/desktop-ui-connect-proxy.log", context);
386
+ const cursorLogArtifacts = captureCursorProfileLogs(userDataDir, context);
387
+ const passed = cdp.available &&
388
+ !cdp.signInRequired &&
389
+ cdp.workspaceOpened &&
390
+ cdp.composerVisible &&
391
+ cdp.modelPickerOpened &&
392
+ cdp.modelTextSeen &&
393
+ cdp.selectedModelTextSeen &&
394
+ (cdp.existingModelTextSeen ||
395
+ cdp.desktopProbeTextSeen ||
396
+ modelBackendRequestSeen) &&
397
+ cdp.desktopPromptSubmitted &&
398
+ !cdp.desktopModelErrorSeen &&
399
+ (desktopSendRoutesSeen.length > 0 || modelBackendRequestSeen) &&
400
+ modelBackendRequestSeen &&
401
+ modelBackendResponseComplete &&
402
+ cdp.desktopProbeTextSeen &&
403
+ (scriptedBackend === undefined || cursorToolResultSeen) &&
404
+ requiredCursorToolResultsSeen;
405
+ const failureCode = desktopUiFailureCode(cdp, routeInventory, modelBackendRequestSeen, modelBackendResponseComplete);
406
+ const status = passed
407
+ ? "passed"
408
+ : desktopUiSkipCode(failureCode) === undefined
409
+ ? "failed"
410
+ : "skipped";
411
+ return {
412
+ id: "desktop-ui-experimental",
413
+ suite: "desktop-ui-experimental",
414
+ status,
415
+ durationMs: Date.now() - started,
416
+ message: passed
417
+ ? "Cursor desktop model picker showed and used the configured local model"
418
+ : desktopUiFailureMessage(cdp, routeInventory, modelBackendRequestSeen, modelBackendResponseComplete),
419
+ failureCode: passed ? undefined : failureCode,
420
+ artifacts: {
421
+ commandLog: commandLogPath,
422
+ report: reportPath,
423
+ pickerDomEvidence: pickerDomEvidencePath,
424
+ selectedModelEvidence: selectedModelEvidencePath,
425
+ composerSubmissionEvidence: composerSubmissionEvidencePath,
426
+ mlxBackendRequestEvidence: backendRequestEvidencePath,
427
+ visibleResponseEvidence: visibleResponseEvidencePath,
428
+ routeUncertainty: routeUncertaintyEvidencePath,
429
+ ckBridgeLog: ckBridgeLogArtifact ?? ckLogPath,
430
+ ...(ckConnectProxyLogArtifact !== undefined
431
+ ? { ckConnectProxyLog: ckConnectProxyLogArtifact }
432
+ : {}),
433
+ ...cursorLogArtifacts,
434
+ },
435
+ details: {
436
+ debugPort,
437
+ ckProfileMode: "isolated-seeded-from-default",
438
+ requestedDefaultProfile: context.options.useDefaultProfile,
439
+ targetCount: cdp.targets.length,
440
+ browser: cdp.browser,
441
+ signInRequired: cdp.signInRequired,
442
+ workspaceOpened: cdp.workspaceOpened,
443
+ composerVisible: cdp.composerVisible,
444
+ modelPickerOpened: cdp.modelPickerOpened,
445
+ modelTextSeen: cdp.modelTextSeen,
446
+ existingModelTextSeen: cdp.existingModelTextSeen,
447
+ selectedModelTextSeen: cdp.selectedModelTextSeen,
448
+ desktopPromptSubmitted: cdp.desktopPromptSubmitted,
449
+ desktopProbeTextSeen: cdp.desktopProbeTextSeen,
450
+ desktopModelErrorSeen: cdp.desktopModelErrorSeen,
451
+ desktopSendRoutesSeen,
452
+ modelBackendRequestSeen,
453
+ modelBackendResponseComplete,
454
+ cursorToolResultSeen,
455
+ requiredCursorToolResultsSeen,
456
+ scriptedBackendRequests: scriptedBackend?.requests,
457
+ modelRoutesSeen: routeInventory.modelRoutesSeen,
458
+ localModelSeedStatus,
459
+ actions: cdp.actions,
460
+ resourceUrls: cdp.resourceUrls,
461
+ error: cdp.error,
462
+ },
463
+ };
464
+ }
465
+ finally {
466
+ ck.stdout?.off("data", writeCommandLog);
467
+ ck.stderr?.off("data", writeCommandLog);
468
+ terminateProcessGroup(ck, "SIGTERM");
469
+ cleanupIsolatedCursorProcesses(userDataDir);
470
+ log.end();
471
+ scriptedBackend?.server.close();
472
+ }
473
+ },
474
+ };
475
+ }
476
+ export function desktopUiCkArgs(options, debugPort, instanceId) {
477
+ // CDP attachment needs a fresh Electron profile; the default profile can hand
478
+ // off to an already-running Cursor process and drop the debug port.
479
+ return [
480
+ "ck",
481
+ "--debug-port",
482
+ String(debugPort),
483
+ "--instance-id",
484
+ instanceId,
485
+ "--seed-auth-from-default",
486
+ "--timeout-ms",
487
+ String(Math.min(options.timeoutMs, 5_000)),
488
+ ];
489
+ }
490
+ function terminateProcessGroup(childProcess, signal) {
491
+ if (childProcess.pid === undefined || process.platform === "win32") {
492
+ childProcess.kill(signal);
493
+ return;
494
+ }
495
+ try {
496
+ process.kill(-childProcess.pid, signal);
497
+ }
498
+ catch {
499
+ childProcess.kill(signal);
500
+ }
501
+ }
502
+ function captureCursorProfileLogs(userDataDir, context) {
503
+ const logsDir = path.join(userDataDir, "logs");
504
+ if (!fs.existsSync(logsDir)) {
505
+ return {};
506
+ }
507
+ const captured = {};
508
+ for (const filePath of listFilesRecursively(logsDir)) {
509
+ const name = path.basename(filePath);
510
+ if (!name.endsWith(".log")) {
511
+ continue;
512
+ }
513
+ const relative = path
514
+ .relative(logsDir, filePath)
515
+ .replaceAll(path.sep, "__");
516
+ const artifactName = `logs/cursor-${relative}`;
517
+ captured[`cursorLog_${relative.replaceAll(".", "_")}`] =
518
+ context.artifacts.writeText(artifactName, fs.readFileSync(filePath, "utf8").slice(-120_000));
519
+ }
520
+ return captured;
521
+ }
522
+ function captureTextArtifactIfExists(sourcePath, artifactName, context) {
523
+ if (!fs.existsSync(sourcePath)) {
524
+ return undefined;
525
+ }
526
+ return context.artifacts.writeText(artifactName, fs.readFileSync(sourcePath, "utf8").slice(-200_000));
527
+ }
528
+ function listFilesRecursively(directory) {
529
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
530
+ const files = [];
531
+ for (const entry of entries) {
532
+ const fullPath = path.join(directory, entry.name);
533
+ if (entry.isDirectory()) {
534
+ files.push(...listFilesRecursively(fullPath));
535
+ }
536
+ else if (entry.isFile()) {
537
+ files.push(fullPath);
538
+ }
539
+ }
540
+ return files;
541
+ }
542
+ function desktopRouteScenario() {
543
+ return {
544
+ id: "desktop-route",
545
+ suite: "desktop-route",
546
+ description: "Cursor desktop route inventory smoke test",
547
+ async run(context) {
548
+ const started = Date.now();
549
+ const result = await context.processRunner.run({
550
+ command: "pnpm",
551
+ args: [
552
+ "ck",
553
+ "test",
554
+ ...(context.options.useDefaultProfile
555
+ ? ["--use-default-profile"]
556
+ : []),
557
+ "--timeout-ms",
558
+ String(context.options.timeoutMs),
559
+ ],
560
+ env: {
561
+ BRIDGE_MODELS_JSON: JSON.stringify([
562
+ {
563
+ id: context.options.model,
564
+ displayName: context.options.displayName,
565
+ providerModel: context.options.providerModel,
566
+ baseUrl: context.options.baseUrl,
567
+ apiKey: context.options.apiKey,
568
+ contextTokenLimit: 128000,
569
+ },
570
+ ]),
571
+ },
572
+ timeoutMs: context.options.timeoutMs + 15_000,
573
+ logName: "desktop-route-ck-test",
574
+ });
575
+ const ckLogPath = path.join(context.options.cwd, ".cursor-rpc", "ck", "bridge.log");
576
+ const ckLog = fs.existsSync(ckLogPath)
577
+ ? fs.readFileSync(ckLogPath, "utf8")
578
+ : "";
579
+ const report = analyzeRouteInventoryLog(ckLog);
580
+ const reportPath = context.artifacts.writeJson("desktop-route-report.json", report);
581
+ const status = result.exitCode === 0 &&
582
+ report.routeInventorySeen &&
583
+ report.modelRoutesSeen.length > 0;
584
+ return {
585
+ id: "desktop-route",
586
+ suite: "desktop-route",
587
+ status: status ? "passed" : "failed",
588
+ durationMs: Date.now() - started,
589
+ message: status
590
+ ? "desktop route inventory reached the bridge"
591
+ : report.diagnosis.join(" "),
592
+ failureCode: desktopFailureCode(report, result),
593
+ artifacts: {
594
+ commandLog: result.logPath,
595
+ routeReport: reportPath,
596
+ ckBridgeLog: ckLogPath,
597
+ },
598
+ details: { ...report },
599
+ };
600
+ },
601
+ };
602
+ }
603
+ function processScenarioResult(options) {
604
+ const passed = options.result.exitCode === 0 && !options.result.timedOut;
605
+ return {
606
+ id: options.id,
607
+ suite: options.suite,
608
+ status: passed ? "passed" : "failed",
609
+ durationMs: Date.now() - options.started,
610
+ message: passed
611
+ ? options.successMessage
612
+ : commandFailureMessage(options.result),
613
+ failureCode: passed ? undefined : options.failureCode,
614
+ artifacts: { log: options.result.logPath },
615
+ details: {
616
+ exitCode: options.result.exitCode,
617
+ signal: options.result.signal,
618
+ timedOut: options.result.timedOut,
619
+ },
620
+ };
621
+ }
622
+ function commandFailureMessage(result) {
623
+ if (result.timedOut) {
624
+ return `Command timed out: ${result.command} ${result.args.join(" ")}`;
625
+ }
626
+ return `Command failed with exit ${String(result.exitCode)}: ${result.command} ${result.args.join(" ")}`;
627
+ }
628
+ function isolatedCursorAgentEnv(options) {
629
+ return {
630
+ ...scrubbedHarnessEnv(options.env),
631
+ MODEL_NAME: options.model,
632
+ MODEL_PROVIDER_MODEL: options.providerModel,
633
+ MODEL_BASE_URL: options.baseUrl,
634
+ MODEL_API_KEY: options.apiKey,
635
+ };
636
+ }
637
+ function desktopFailureCode(report, result) {
638
+ if (result.timedOut) {
639
+ return "timeout";
640
+ }
641
+ if (result.exitCode !== 0) {
642
+ return "bridge_start_failed";
643
+ }
644
+ if (!report.routeInventorySeen) {
645
+ return "route_missing";
646
+ }
647
+ if (report.modelRoutesSeen.length === 0) {
648
+ return "model_route_missing";
649
+ }
650
+ return "model_metadata_rejected";
651
+ }
652
+ function desktopUiFailureCode(cdp, routeInventory, modelBackendRequestSeen, modelBackendResponseComplete) {
653
+ if (!cdp.available) {
654
+ return "not_available";
655
+ }
656
+ if (cdp.signInRequired) {
657
+ return "auth_profile_blocked";
658
+ }
659
+ if (!cdp.workspaceOpened || !cdp.composerVisible) {
660
+ return "not_available";
661
+ }
662
+ if (!cdp.modelPickerOpened || !cdp.modelTextSeen) {
663
+ return "desktop_picker_missing";
664
+ }
665
+ if (!cdp.selectedModelTextSeen) {
666
+ return "desktop_model_selection_missing";
667
+ }
668
+ if (!cdp.desktopPromptSubmitted) {
669
+ return "desktop_prompt_submission_failed";
670
+ }
671
+ if (cdp.desktopModelErrorSeen) {
672
+ return "model_metadata_rejected";
673
+ }
674
+ if (desktopSendRoutes(routeInventory).length === 0 &&
675
+ !modelBackendRequestSeen) {
676
+ return "extension_host_route_missing";
677
+ }
678
+ if (!modelBackendRequestSeen) {
679
+ return "desktop_backend_request_missing";
680
+ }
681
+ if (!modelBackendResponseComplete) {
682
+ return "local_completion_failed";
683
+ }
684
+ if (!cdp.desktopProbeTextSeen) {
685
+ return "desktop_visible_response_missing";
686
+ }
687
+ return "model_metadata_rejected";
688
+ }
689
+ function desktopUiFailureMessage(cdp, routeInventory, modelBackendRequestSeen, modelBackendResponseComplete) {
690
+ if (!cdp.available) {
691
+ return "Cursor desktop did not expose a debuggable Chromium target";
692
+ }
693
+ if (cdp.signInRequired) {
694
+ return "Cursor desktop CDP worked, but the isolated seeded profile still requires login";
695
+ }
696
+ if (!cdp.workspaceOpened) {
697
+ return "Cursor desktop opened, but the test workspace was not visible";
698
+ }
699
+ if (!cdp.composerVisible) {
700
+ return "Cursor desktop opened, but the agent composer was not visible";
701
+ }
702
+ if (!cdp.modelPickerOpened) {
703
+ return "Cursor desktop opened, but the model picker did not open";
704
+ }
705
+ if (!cdp.modelTextSeen) {
706
+ return "Cursor desktop model picker opened, but the configured local model was not visible";
707
+ }
708
+ if (!cdp.selectedModelTextSeen) {
709
+ return "Cursor desktop model picker showed the local model, but the active composer did not show it as selected";
710
+ }
711
+ if (!cdp.existingModelTextSeen &&
712
+ !cdp.desktopProbeTextSeen &&
713
+ !modelBackendRequestSeen) {
714
+ return "Cursor desktop model picker opened, but existing Cursor models were not visible";
715
+ }
716
+ if (!cdp.desktopPromptSubmitted) {
717
+ return "Cursor desktop model picker opened, but the test prompt could not be submitted";
718
+ }
719
+ if (cdp.desktopModelErrorSeen) {
720
+ return "Cursor desktop selected the local model, but sending a prompt produced a model-not-found error";
721
+ }
722
+ if (desktopSendRoutes(routeInventory).length === 0 &&
723
+ !modelBackendRequestSeen) {
724
+ return "Cursor desktop selected the local model and submitted the prompt, but no Agent execution route or local model backend request was observed; bridge routing is working for desktop metadata routes, but the injected model is not yet accepted by the desktop Agent execution path";
725
+ }
726
+ if (!modelBackendRequestSeen) {
727
+ return "Cursor desktop Agent execution reached the bridge, but no OpenAI-compatible backend request was observed";
728
+ }
729
+ if (!modelBackendResponseComplete) {
730
+ return "Cursor desktop Agent execution reached the local OpenAI-compatible backend, but generation did not complete before validation cleanup";
731
+ }
732
+ if (!cdp.desktopProbeTextSeen) {
733
+ return "Cursor desktop completed the local backend request, but the expected desktop-probe-ok marker was not visible in the UI";
734
+ }
735
+ return "Cursor desktop model picker opened, but the configured local model was not visible";
736
+ }
737
+ function desktopUiSkipCode(code) {
738
+ switch (code) {
739
+ case "not_available":
740
+ case "auth_profile_blocked":
741
+ case "backend_unreachable":
742
+ return code;
743
+ case "desktop_picker_missing":
744
+ case "desktop_model_selection_missing":
745
+ case "desktop_prompt_submission_failed":
746
+ case "desktop_backend_request_missing":
747
+ case "desktop_visible_response_missing":
748
+ case "route_missing":
749
+ case "model_route_missing":
750
+ case "extension_host_route_missing":
751
+ case "model_metadata_rejected":
752
+ case "local_completion_failed":
753
+ case "bridge_start_failed":
754
+ case "upstream_passthrough_failed":
755
+ case "command_failed":
756
+ case "timeout":
757
+ return undefined;
758
+ default: {
759
+ const exhaustive = code;
760
+ return exhaustive;
761
+ }
762
+ }
763
+ }
764
+ function desktopModelBackendRequestSeen(logText, baseUrl) {
765
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
766
+ return (logText.includes('"message":"model backend request"') &&
767
+ logText.includes(`${normalizedBaseUrl}/chat/completions`));
768
+ }
769
+ function desktopModelBackendResponseComplete(logText, baseUrl) {
770
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
771
+ return (logText.includes('"message":"model backend response complete"') &&
772
+ logText.includes(`${normalizedBaseUrl}/chat/completions`) &&
773
+ logText.includes('"message":"served local agent run"'));
774
+ }
775
+ function desktopBackendRequestEvidence(logText, baseUrl, requestSeen, responseComplete) {
776
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
777
+ return {
778
+ baseUrl: normalizedBaseUrl,
779
+ chatCompletionsUrl: `${normalizedBaseUrl}/chat/completions`,
780
+ requestSeen,
781
+ responseComplete,
782
+ events: parseJsonLogEvents(logText, [
783
+ "model backend request",
784
+ "model backend response complete",
785
+ "served local agent run",
786
+ ]),
787
+ };
788
+ }
789
+ function parseJsonLogEvents(logText, messages) {
790
+ const messageSet = new Set(messages);
791
+ const events = [];
792
+ for (const line of logText.split(/\r?\n/)) {
793
+ const trimmed = line.trim();
794
+ if (trimmed.length === 0) {
795
+ continue;
796
+ }
797
+ try {
798
+ const parsed = JSON.parse(trimmed);
799
+ if (typeof parsed.message === "string" &&
800
+ messageSet.has(parsed.message)) {
801
+ events.push(parsed);
802
+ }
803
+ }
804
+ catch {
805
+ continue;
806
+ }
807
+ }
808
+ return events;
809
+ }
810
+ function desktopCursorToolResultSeen(logText) {
811
+ return (logText.includes('"message":"requested cursor tool execution"') &&
812
+ logText.includes('"message":"received cursor tool result"'));
813
+ }
814
+ function desktopCursorToolNamesSeen(logText, toolNames) {
815
+ const resultCount = (logText.match(/"message":"received cursor tool result"/g) ?? []).length;
816
+ return (resultCount >= toolNames.length &&
817
+ toolNames.every((toolName) => logText.includes(`"toolName":"${toolName}"`)));
818
+ }
819
+ async function startScriptedToolBackend() {
820
+ const requests = [];
821
+ const server = http.createServer((request, response) => {
822
+ if (request.url === "/v1/models" && request.method === "GET") {
823
+ response.writeHead(200, { "content-type": "application/json" });
824
+ response.end(JSON.stringify({
825
+ object: "list",
826
+ data: [
827
+ {
828
+ id: "mlx-community/Qwen3.5-4B-8bit",
829
+ object: "model",
830
+ created: 0,
831
+ },
832
+ ],
833
+ }));
834
+ return;
835
+ }
836
+ if (request.url !== "/v1/chat/completions" || request.method !== "POST") {
837
+ response.writeHead(404, { "content-type": "application/json" });
838
+ response.end(JSON.stringify({ error: "not found" }));
839
+ return;
840
+ }
841
+ const chunks = [];
842
+ request.on("data", (chunk) => chunks.push(chunk));
843
+ request.on("end", () => {
844
+ let body;
845
+ try {
846
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
847
+ }
848
+ catch (error) {
849
+ response.writeHead(400, { "content-type": "application/json" });
850
+ response.end(JSON.stringify({
851
+ error: error instanceof Error ? error.message : String(error),
852
+ }));
853
+ return;
854
+ }
855
+ const messages = Array.isArray(body.messages)
856
+ ? body.messages
857
+ : [];
858
+ const tools = Array.isArray(body.tools)
859
+ ? body.tools
860
+ : [];
861
+ requests.push({
862
+ messageRoles: messages
863
+ .map((message) => message.role)
864
+ .filter((role) => typeof role === "string"),
865
+ toolNames: tools
866
+ .map((tool) => typeof tool.function === "object" &&
867
+ tool.function !== null &&
868
+ !Array.isArray(tool.function)
869
+ ? tool.function.name
870
+ : undefined)
871
+ .filter((name) => typeof name === "string"),
872
+ assistantToolCallNames: messages.flatMap((message) => Array.isArray(message.tool_calls)
873
+ ? message.tool_calls
874
+ .map((toolCall) => typeof toolCall === "object" &&
875
+ toolCall !== null &&
876
+ !Array.isArray(toolCall) &&
877
+ typeof toolCall.function ===
878
+ "object" &&
879
+ toolCall.function !== null
880
+ ? toolCall
881
+ .function.name
882
+ : undefined)
883
+ .filter((name) => typeof name === "string")
884
+ : []),
885
+ lastMessageRole: typeof messages.at(-1)?.role === "string"
886
+ ? messages.at(-1)?.role
887
+ : undefined,
888
+ lastMessagePreview: typeof messages.at(-1)?.content === "string"
889
+ ? (messages.at(-1)?.content).slice(0, 500)
890
+ : undefined,
891
+ });
892
+ response.writeHead(200, { "content-type": "text/event-stream" });
893
+ const step = requests.length;
894
+ if (step === 1) {
895
+ response.end(scriptedToolCallSse("read-call", "read_file", {
896
+ path: "README.md",
897
+ }));
898
+ }
899
+ else if (step === 2) {
900
+ response.end(scriptedToolCallSse("list-call", "list_dir", { path: "." }));
901
+ }
902
+ else if (step === 3) {
903
+ response.end(scriptedToolCallSse("grep-call", "grep", {
904
+ pattern: "bridge",
905
+ path: ".",
906
+ head_limit: 5,
907
+ }));
908
+ }
909
+ else {
910
+ response.end([
911
+ 'data: {"choices":[{"delta":{"content":"desktop-probe-ok scripted-tool-loop-ok"}}]}',
912
+ "data: [DONE]",
913
+ "",
914
+ ].join("\n"));
915
+ }
916
+ });
917
+ });
918
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
919
+ const address = server.address();
920
+ if (address === null || typeof address === "string") {
921
+ throw new Error("scripted backend did not bind to a TCP port");
922
+ }
923
+ return {
924
+ server,
925
+ baseUrl: `http://127.0.0.1:${address.port}/v1`,
926
+ requests,
927
+ };
928
+ }
929
+ function scriptedToolCallSse(id, name, args) {
930
+ return [
931
+ `data: ${JSON.stringify({
932
+ choices: [
933
+ {
934
+ delta: {
935
+ tool_calls: [
936
+ {
937
+ index: 0,
938
+ id,
939
+ type: "function",
940
+ function: {
941
+ name,
942
+ arguments: JSON.stringify(args),
943
+ },
944
+ },
945
+ ],
946
+ },
947
+ },
948
+ ],
949
+ })}`,
950
+ 'data: {"choices":[{"finish_reason":"tool_calls"}]}',
951
+ "data: [DONE]",
952
+ "",
953
+ ].join("\n");
954
+ }
955
+ function desktopSendRoutes(routeInventory) {
956
+ return routeInventory.observedPaths.filter((path) => [
957
+ AGENT_RUN_SSE_PATH,
958
+ AGENT_RUN_PATH,
959
+ BIDI_APPEND_PATH,
960
+ STREAM_CHAT_WITH_TOOLS_PATH,
961
+ ].includes(path));
962
+ }
963
+ async function waitForDesktopCompletionLog(logPath, timeoutMs) {
964
+ const started = Date.now();
965
+ while (Date.now() - started < timeoutMs) {
966
+ const text = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf8") : "";
967
+ if (text.includes("served local agent run")) {
968
+ return;
969
+ }
970
+ await delay(500);
971
+ }
972
+ }
973
+ function startTrafficProbeBridge(context, port, logPath) {
974
+ const log = fs.createWriteStream(logPath, { flags: "w" });
975
+ const env = concreteEnv({
976
+ ...scrubbedHarnessEnv(context.options.env),
977
+ BRIDGE_PORT: String(port),
978
+ BRIDGE_LOG_LEVEL: "debug",
979
+ BRIDGE_ROUTE_INVENTORY: "true",
980
+ CURSOR_UPSTREAM_BASE_URL: context.options.env.CURSOR_UPSTREAM_BASE_URL ?? "https://api2.cursor.sh",
981
+ MODEL_BASE_URL: context.options.baseUrl,
982
+ MODEL_API_KEY: context.options.apiKey,
983
+ MODEL_NAME: context.options.model,
984
+ MODEL_PROVIDER_MODEL: context.options.providerModel,
985
+ MODEL_CONTEXT_TOKEN_LIMIT: "128000",
986
+ BRIDGE_HARDCODED_RESPONSE: "traffic-probe-ok",
987
+ });
988
+ const bridge = spawn(process.execPath, ["dist/src/cli.js", "serve"], {
989
+ cwd: context.options.cwd,
990
+ env,
991
+ stdio: ["ignore", "pipe", "pipe"],
992
+ });
993
+ bridge.stdout?.on("data", (chunk) => {
994
+ log.write(chunk);
995
+ });
996
+ bridge.stderr?.on("data", (chunk) => {
997
+ log.write(chunk);
998
+ });
999
+ bridge.on("exit", () => log.end());
1000
+ return bridge;
1001
+ }
1002
+ async function runAcpProbe(context, endpoint, bridgeLogPath) {
1003
+ const events = [];
1004
+ const acp = spawn("cursor-agent", [
1005
+ "--endpoint",
1006
+ endpoint,
1007
+ "--model",
1008
+ context.options.model,
1009
+ "--mode",
1010
+ "ask",
1011
+ "acp",
1012
+ ], {
1013
+ cwd: context.options.cwd,
1014
+ env: concreteEnv(scrubbedHarnessEnv(context.options.env)),
1015
+ stdio: ["pipe", "pipe", "pipe"],
1016
+ });
1017
+ if (acp.stdin === null || acp.stdout === null) {
1018
+ throw new Error("cursor-agent acp did not expose stdio pipes");
1019
+ }
1020
+ const acpStdin = acp.stdin;
1021
+ const acpStdout = acp.stdout;
1022
+ let text = "";
1023
+ let nextId = 1;
1024
+ const pending = new Map();
1025
+ const rl = readline.createInterface({ input: acpStdout });
1026
+ const stderrPath = context.artifacts.pathFor("acp-stderr.log");
1027
+ const stderrLog = fs.createWriteStream(stderrPath, { flags: "w" });
1028
+ acp.stderr?.on("data", (chunk) => stderrLog.write(chunk));
1029
+ function send(method, params) {
1030
+ const id = nextId;
1031
+ nextId += 1;
1032
+ events.push({ direction: "send", method });
1033
+ acpStdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
1034
+ return new Promise((resolve, reject) => {
1035
+ pending.set(id, { resolve, reject });
1036
+ });
1037
+ }
1038
+ function respond(id, result) {
1039
+ acpStdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}\n`);
1040
+ }
1041
+ rl.on("line", (line) => {
1042
+ let message;
1043
+ try {
1044
+ message = JSON.parse(line);
1045
+ }
1046
+ catch {
1047
+ events.push({ direction: "receive", malformed: true });
1048
+ return;
1049
+ }
1050
+ if (message.id !== undefined && message.method === undefined) {
1051
+ const id = Number(message.id);
1052
+ const waiter = pending.get(id);
1053
+ if (waiter === undefined) {
1054
+ return;
1055
+ }
1056
+ pending.delete(id);
1057
+ if (message.error !== undefined) {
1058
+ waiter.reject(message.error);
1059
+ }
1060
+ else {
1061
+ waiter.resolve(message.result);
1062
+ }
1063
+ return;
1064
+ }
1065
+ if (message.method !== undefined) {
1066
+ events.push({ direction: "receive", method: message.method });
1067
+ if (message.method === "session/update") {
1068
+ text += extractAcpText(message.params);
1069
+ }
1070
+ if (message.id !== undefined) {
1071
+ respond(message.id, responseForAcpRequest(message.method));
1072
+ }
1073
+ }
1074
+ });
1075
+ try {
1076
+ await acpStep(() => send("initialize", {
1077
+ protocolVersion: 1,
1078
+ clientCapabilities: {
1079
+ fs: { readTextFile: false, writeTextFile: false },
1080
+ terminal: false,
1081
+ },
1082
+ clientInfo: { name: "cursor-rpc-harness", version: "0.1.0" },
1083
+ }), context.options.timeoutMs);
1084
+ const initialized = true;
1085
+ await acpStep(() => send("authenticate", { methodId: "cursor_login" }), context.options.timeoutMs);
1086
+ const authenticated = true;
1087
+ const sessionResult = await acpStep(() => send("session/new", {
1088
+ cwd: context.options.cwd,
1089
+ mcpServers: [],
1090
+ }), context.options.timeoutMs);
1091
+ const sessionId = extractSessionId(sessionResult);
1092
+ if (sessionId === undefined) {
1093
+ throw new Error("ACP session/new did not return a session id");
1094
+ }
1095
+ await acpStep(() => send("session/prompt", {
1096
+ sessionId,
1097
+ prompt: [{ type: "text", text: "Reply with traffic-probe-ok." }],
1098
+ }), context.options.timeoutMs);
1099
+ await waitForFilePattern(bridgeLogPath, /desktop route inventory/, acp, 2_000).catch(() => undefined);
1100
+ const bridgeLog = fs.existsSync(bridgeLogPath)
1101
+ ? fs.readFileSync(bridgeLogPath, "utf8")
1102
+ : "";
1103
+ return {
1104
+ endpoint,
1105
+ initialized,
1106
+ authenticated,
1107
+ sessionCreated: true,
1108
+ promptCompleted: true,
1109
+ textPreview: text.slice(0, 2_000),
1110
+ routeInventory: analyzeRouteInventoryLog(bridgeLog),
1111
+ agentRunDiagnostics: parseAgentRunDiagnostics(bridgeLog),
1112
+ events,
1113
+ };
1114
+ }
1115
+ finally {
1116
+ rl.close();
1117
+ stderrLog.end();
1118
+ acp.kill("SIGTERM");
1119
+ }
1120
+ }
1121
+ async function acpStep(step, timeoutMs) {
1122
+ return await withTimeout(step(), timeoutMs);
1123
+ }
1124
+ async function withTimeout(promise, timeoutMs) {
1125
+ let timer;
1126
+ try {
1127
+ return await Promise.race([
1128
+ promise,
1129
+ new Promise((_resolve, reject) => {
1130
+ timer = setTimeout(() => reject(new Error("ACP step timed out")), timeoutMs);
1131
+ }),
1132
+ ]);
1133
+ }
1134
+ finally {
1135
+ if (timer !== undefined) {
1136
+ clearTimeout(timer);
1137
+ }
1138
+ }
1139
+ }
1140
+ function responseForAcpRequest(method) {
1141
+ switch (method) {
1142
+ case "session/request_permission":
1143
+ return { outcome: { outcome: "selected", optionId: "reject-once" } };
1144
+ case "cursor/ask_question":
1145
+ return { outcome: { outcome: "skipped", reason: "harness" } };
1146
+ case "cursor/create_plan":
1147
+ return { outcome: { outcome: "rejected", reason: "harness" } };
1148
+ default:
1149
+ return { outcome: { outcome: "skipped", reason: "harness" } };
1150
+ }
1151
+ }
1152
+ function extractSessionId(result) {
1153
+ if (typeof result !== "object" || result === null) {
1154
+ return undefined;
1155
+ }
1156
+ const record = result;
1157
+ if (typeof record.sessionId === "string") {
1158
+ return record.sessionId;
1159
+ }
1160
+ const session = record.session;
1161
+ if (typeof session === "object" && session !== null) {
1162
+ const sessionRecord = session;
1163
+ if (typeof sessionRecord.id === "string") {
1164
+ return sessionRecord.id;
1165
+ }
1166
+ }
1167
+ return undefined;
1168
+ }
1169
+ function extractAcpText(params) {
1170
+ const serialized = JSON.stringify(params);
1171
+ if (serialized === undefined) {
1172
+ return "";
1173
+ }
1174
+ const matches = serialized.match(/traffic-probe-ok/g);
1175
+ return matches === null ? "" : matches.join("");
1176
+ }
1177
+ function scrubbedHarnessEnv(env) {
1178
+ const result = {};
1179
+ for (const key of Object.keys(env)) {
1180
+ if (key.startsWith("BRIDGE_") ||
1181
+ key.startsWith("MODEL_") ||
1182
+ key.startsWith("E2E_") ||
1183
+ key === "CURSOR_UPSTREAM_BASE_URL" ||
1184
+ key === "CURSOR_UPSTREAM_CONNECT_HOST" ||
1185
+ key === "CURSOR_UPSTREAM_CONNECT_PORT") {
1186
+ result[key] = undefined;
1187
+ }
1188
+ }
1189
+ return result;
1190
+ }
1191
+ function concreteEnv(overrides) {
1192
+ const env = { ...process.env };
1193
+ for (const [key, value] of Object.entries(overrides)) {
1194
+ if (value === undefined) {
1195
+ delete env[key];
1196
+ }
1197
+ else {
1198
+ env[key] = value;
1199
+ }
1200
+ }
1201
+ return env;
1202
+ }
1203
+ function waitForProcessOutput(process, pattern, timeoutMs) {
1204
+ return new Promise((resolve, reject) => {
1205
+ let output = "";
1206
+ const timer = setTimeout(() => {
1207
+ cleanup();
1208
+ reject(new Error(`Timed out waiting for ${pattern.source}`));
1209
+ }, timeoutMs);
1210
+ const onData = (chunk) => {
1211
+ output += chunk.toString("utf8");
1212
+ if (pattern.test(output)) {
1213
+ cleanup();
1214
+ resolve();
1215
+ }
1216
+ };
1217
+ const onExit = () => {
1218
+ cleanup();
1219
+ reject(new Error("Process exited before expected output"));
1220
+ };
1221
+ const cleanup = () => {
1222
+ clearTimeout(timer);
1223
+ process.stdout?.off("data", onData);
1224
+ process.stderr?.off("data", onData);
1225
+ process.off("exit", onExit);
1226
+ };
1227
+ process.stdout?.on("data", onData);
1228
+ process.stderr?.on("data", onData);
1229
+ process.on("exit", onExit);
1230
+ });
1231
+ }
1232
+ async function waitForFilePattern(filePath, pattern, process, timeoutMs = 20_000) {
1233
+ const started = Date.now();
1234
+ while (Date.now() - started < timeoutMs) {
1235
+ if (process.exitCode !== null) {
1236
+ const contents = fs.existsSync(filePath)
1237
+ ? fs.readFileSync(filePath, "utf8")
1238
+ : "";
1239
+ throw new Error(`bridge exited before ${pattern.source}: ${contents}`);
1240
+ }
1241
+ const contents = fs.existsSync(filePath)
1242
+ ? fs.readFileSync(filePath, "utf8")
1243
+ : "";
1244
+ if (pattern.test(contents)) {
1245
+ return;
1246
+ }
1247
+ await delay(100);
1248
+ }
1249
+ throw new Error(`timed out waiting for ${pattern.source}`);
1250
+ }
1251
+ async function freePort() {
1252
+ return new Promise((resolve, reject) => {
1253
+ const server = net.createServer();
1254
+ server.listen(0, "127.0.0.1", () => {
1255
+ const address = server.address();
1256
+ if (address === null || typeof address === "string") {
1257
+ server.close();
1258
+ reject(new Error("Unable to allocate port"));
1259
+ return;
1260
+ }
1261
+ const port = address.port;
1262
+ server.close(() => resolve(port));
1263
+ });
1264
+ server.on("error", reject);
1265
+ });
1266
+ }
1267
+ async function delay(ms) {
1268
+ await new Promise((resolve) => setTimeout(resolve, ms));
1269
+ }
1270
+ function processRunSummary(result) {
1271
+ return {
1272
+ exitCode: result.exitCode,
1273
+ signal: result.signal,
1274
+ timedOut: result.timedOut,
1275
+ durationMs: result.durationMs,
1276
+ logPath: result.logPath,
1277
+ stdoutPreview: result.stdout.slice(0, 2_000),
1278
+ stderrPreview: result.stderr.slice(0, 2_000),
1279
+ };
1280
+ }
1281
+ async function probeDesktopCdp(port, timeoutMs, options) {
1282
+ const started = Date.now();
1283
+ let lastError = "";
1284
+ while (Date.now() - started < timeoutMs) {
1285
+ try {
1286
+ const versionResponse = await fetch(`http://127.0.0.1:${port}/json/version`);
1287
+ const targetsResponse = await fetch(`http://127.0.0.1:${port}/json/list`);
1288
+ if (versionResponse.ok && targetsResponse.ok) {
1289
+ const version = (await versionResponse.json());
1290
+ const targets = (await targetsResponse.json());
1291
+ const pageTarget = targets.find((target) => target.type === "page");
1292
+ if (pageTarget === undefined) {
1293
+ lastError = "CDP available but no page target yet";
1294
+ await delay(250);
1295
+ continue;
1296
+ }
1297
+ const pickerProbe = typeof pageTarget.webSocketDebuggerUrl === "string"
1298
+ ? await probeModelPickerDom(pageTarget.webSocketDebuggerUrl, timeoutMs, options)
1299
+ : {
1300
+ bodyText: "",
1301
+ pickerText: "",
1302
+ workspaceOpened: false,
1303
+ composerVisible: false,
1304
+ modelPickerOpened: false,
1305
+ modelTextSeen: false,
1306
+ existingModelTextSeen: false,
1307
+ selectedModelTextSeen: false,
1308
+ desktopPromptSubmitted: false,
1309
+ desktopProbeTextSeen: false,
1310
+ desktopModelErrorSeen: false,
1311
+ actions: [],
1312
+ resourceUrls: [],
1313
+ pickerDomEvidence: undefined,
1314
+ selectedModelEvidence: undefined,
1315
+ composerSubmissionEvidence: undefined,
1316
+ visibleResponseEvidence: undefined,
1317
+ };
1318
+ return {
1319
+ available: true,
1320
+ browser: typeof version.Browser === "string" ? version.Browser : undefined,
1321
+ targets: targets.map((target) => ({
1322
+ id: stringField(target.id),
1323
+ type: stringField(target.type),
1324
+ title: stringField(target.title),
1325
+ url: stringField(target.url),
1326
+ })),
1327
+ bodyTextPreview: pickerProbe.bodyText.slice(0, 2_000),
1328
+ pickerTextPreview: pickerProbe.pickerText.slice(0, 4_000),
1329
+ signInRequired: pickerProbe.bodyText.includes("Log In") ||
1330
+ pickerProbe.bodyText.includes("Sign in") ||
1331
+ pickerProbe.bodyText.includes("Sign Up"),
1332
+ workspaceOpened: pickerProbe.workspaceOpened,
1333
+ composerVisible: pickerProbe.composerVisible,
1334
+ modelPickerOpened: pickerProbe.modelPickerOpened,
1335
+ modelTextSeen: pickerProbe.modelTextSeen,
1336
+ existingModelTextSeen: pickerProbe.existingModelTextSeen,
1337
+ selectedModelTextSeen: pickerProbe.selectedModelTextSeen,
1338
+ desktopPromptSubmitted: pickerProbe.desktopPromptSubmitted,
1339
+ desktopProbeTextSeen: pickerProbe.desktopProbeTextSeen,
1340
+ desktopModelErrorSeen: pickerProbe.desktopModelErrorSeen,
1341
+ actions: pickerProbe.actions,
1342
+ resourceUrls: pickerProbe.resourceUrls,
1343
+ pickerDomEvidence: pickerProbe.pickerDomEvidence,
1344
+ selectedModelEvidence: pickerProbe.selectedModelEvidence,
1345
+ composerSubmissionEvidence: pickerProbe.composerSubmissionEvidence,
1346
+ visibleResponseEvidence: pickerProbe.visibleResponseEvidence,
1347
+ };
1348
+ }
1349
+ lastError = `HTTP ${versionResponse.status}/${targetsResponse.status}`;
1350
+ }
1351
+ catch (error) {
1352
+ lastError = error instanceof Error ? error.message : String(error);
1353
+ }
1354
+ await delay(250);
1355
+ }
1356
+ return {
1357
+ available: false,
1358
+ browser: undefined,
1359
+ targets: [],
1360
+ signInRequired: false,
1361
+ workspaceOpened: false,
1362
+ composerVisible: false,
1363
+ modelPickerOpened: false,
1364
+ modelTextSeen: false,
1365
+ existingModelTextSeen: false,
1366
+ selectedModelTextSeen: false,
1367
+ desktopPromptSubmitted: false,
1368
+ desktopProbeTextSeen: false,
1369
+ desktopModelErrorSeen: false,
1370
+ actions: [],
1371
+ resourceUrls: [],
1372
+ error: lastError,
1373
+ };
1374
+ }
1375
+ async function waitForDesktopWorkbenchReady(port, timeoutMs) {
1376
+ const webSocketDebuggerUrl = await waitForDesktopPageWebSocketUrl(port, timeoutMs);
1377
+ await waitForCdpBodyText(webSocketDebuggerUrl, timeoutMs);
1378
+ }
1379
+ async function reloadDesktopWorkbench(port, timeoutMs) {
1380
+ const webSocketDebuggerUrl = await waitForDesktopPageWebSocketUrl(port, timeoutMs);
1381
+ await evaluateCdpValue(webSocketDebuggerUrl, "location.reload()");
1382
+ await delay(1_000);
1383
+ }
1384
+ async function waitForDesktopPageWebSocketUrl(port, timeoutMs) {
1385
+ const started = Date.now();
1386
+ let lastError = "";
1387
+ while (Date.now() - started < timeoutMs) {
1388
+ try {
1389
+ const targetsResponse = await fetch(`http://127.0.0.1:${port}/json/list`);
1390
+ if (targetsResponse.ok) {
1391
+ const targets = (await targetsResponse.json());
1392
+ const pageTarget = targets.find((target) => target.type === "page");
1393
+ if (typeof pageTarget?.webSocketDebuggerUrl === "string") {
1394
+ return pageTarget.webSocketDebuggerUrl;
1395
+ }
1396
+ lastError = "CDP available but no page target yet";
1397
+ }
1398
+ else {
1399
+ lastError = `HTTP ${targetsResponse.status}`;
1400
+ }
1401
+ }
1402
+ catch (error) {
1403
+ lastError = error instanceof Error ? error.message : String(error);
1404
+ }
1405
+ await delay(250);
1406
+ }
1407
+ throw new Error(`Timed out waiting for desktop workbench: ${lastError}`);
1408
+ }
1409
+ async function probeModelPickerDom(webSocketDebuggerUrl, timeoutMs, options) {
1410
+ const actions = [];
1411
+ let bodyText = await waitForCdpBodyText(webSocketDebuggerUrl, 15_000);
1412
+ if (bodyText.includes("Log In") ||
1413
+ bodyText.includes("Sign in") ||
1414
+ bodyText.includes("Sign Up")) {
1415
+ return {
1416
+ bodyText,
1417
+ pickerText: bodyText,
1418
+ workspaceOpened: false,
1419
+ composerVisible: false,
1420
+ modelPickerOpened: false,
1421
+ modelTextSeen: false,
1422
+ existingModelTextSeen: false,
1423
+ selectedModelTextSeen: false,
1424
+ desktopPromptSubmitted: false,
1425
+ desktopProbeTextSeen: false,
1426
+ desktopModelErrorSeen: false,
1427
+ actions,
1428
+ resourceUrls: [],
1429
+ pickerDomEvidence: {
1430
+ workspaceOpened: false,
1431
+ composerVisible: false,
1432
+ modelPickerOpened: false,
1433
+ modelTextSeen: false,
1434
+ existingModelTextSeen: false,
1435
+ bodyTextPreview: bodyText.slice(0, 2_000),
1436
+ pickerTextPreview: bodyText.slice(0, 4_000),
1437
+ },
1438
+ selectedModelEvidence: {
1439
+ requestedModel: options.model,
1440
+ requestedDisplayName: options.displayName,
1441
+ activeModelAlreadySelected: false,
1442
+ selectedModelTextSeen: false,
1443
+ activeComposerTextPreview: bodyText.slice(0, 2_000),
1444
+ },
1445
+ composerSubmissionEvidence: {
1446
+ promptPreview: "",
1447
+ focused: false,
1448
+ inserted: false,
1449
+ sendAttempted: false,
1450
+ submitted: false,
1451
+ failureReason: "sign-in-required",
1452
+ },
1453
+ visibleResponseEvidence: {
1454
+ expectedMarker: "desktop-probe-ok",
1455
+ markerSeen: false,
1456
+ modelErrorSeen: false,
1457
+ bodyTextPreview: bodyText.slice(0, 2_000),
1458
+ },
1459
+ };
1460
+ }
1461
+ for (const label of ["Skip", "Start Building", "Continue"]) {
1462
+ if (await clickVisibleText(webSocketDebuggerUrl, label)) {
1463
+ actions.push(`clicked:${label}`);
1464
+ await delay(1_000);
1465
+ bodyText = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1466
+ }
1467
+ }
1468
+ bodyText = await dismissDataSharingPrompt(webSocketDebuggerUrl, actions, bodyText);
1469
+ const workspaceOpened = bodyText.includes("cursorkit");
1470
+ if (!composerBodyTextVisible(bodyText)) {
1471
+ if (await clickVisibleText(webSocketDebuggerUrl, "New Agent")) {
1472
+ actions.push("clicked:New Agent");
1473
+ await delay(1_500);
1474
+ bodyText = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1475
+ }
1476
+ }
1477
+ let composerVisible = (composerBodyTextVisible(bodyText) && composerPromptVisible(bodyText)) ||
1478
+ (await desktopComposerDomVisible(webSocketDebuggerUrl));
1479
+ let pickerText = bodyText;
1480
+ const activeModelAlreadySelected = bodyText
1481
+ .toLowerCase()
1482
+ .includes(options.displayName.toLowerCase());
1483
+ if (composerBodyTextVisible(bodyText) && !activeModelAlreadySelected) {
1484
+ const clickedModel = await clickVisibleModelPickerTrigger(webSocketDebuggerUrl);
1485
+ if (clickedModel !== undefined) {
1486
+ actions.push(`clicked:${clickedModel}`);
1487
+ await delay(2_000);
1488
+ pickerText = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1489
+ }
1490
+ }
1491
+ const lowerPickerText = pickerText.toLowerCase();
1492
+ const modelTextSeen = lowerPickerText.includes(options.displayName.toLowerCase()) ||
1493
+ lowerPickerText.includes(options.model.toLowerCase());
1494
+ const existingModelTextSeen = desktopPickerHasBuiltInModel(pickerText);
1495
+ const modelPickerOpened = pickerText.includes("Balanced quality and speed") ||
1496
+ pickerText.includes("Add Models") ||
1497
+ pickerText.includes("MAX Mode") ||
1498
+ existingModelTextSeen ||
1499
+ modelTextSeen;
1500
+ let afterPromptText = pickerText;
1501
+ let composerSubmissionEvidence = {
1502
+ promptPreview: "",
1503
+ focused: false,
1504
+ inserted: false,
1505
+ sendAttempted: false,
1506
+ submitted: false,
1507
+ failureReason: "model-not-visible",
1508
+ };
1509
+ let selectionAction;
1510
+ let selectedModelTextSeen = activeModelAlreadySelected;
1511
+ if (modelTextSeen) {
1512
+ if (!activeModelAlreadySelected &&
1513
+ (await clickVisibleText(webSocketDebuggerUrl, options.displayName))) {
1514
+ selectionAction = `selected:${options.displayName}`;
1515
+ actions.push(selectionAction);
1516
+ await delay(750);
1517
+ }
1518
+ else if (activeModelAlreadySelected) {
1519
+ selectionAction = `selected-preseeded:${options.displayName}`;
1520
+ actions.push(selectionAction);
1521
+ }
1522
+ if (await clickVisibleText(webSocketDebuggerUrl, "Continue")) {
1523
+ actions.push("clicked:Continue");
1524
+ await delay(750);
1525
+ }
1526
+ bodyText = await dismissDataSharingPrompt(webSocketDebuggerUrl, actions, await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''"));
1527
+ bodyText = await ensureDesktopComposerOpen(webSocketDebuggerUrl, actions);
1528
+ if (!bodyText.includes(options.displayName)) {
1529
+ const activeModel = await clickVisibleModelPickerTrigger(webSocketDebuggerUrl);
1530
+ if (activeModel !== undefined) {
1531
+ actions.push(`clicked:${activeModel}`);
1532
+ await delay(750);
1533
+ if (await clickVisibleText(webSocketDebuggerUrl, options.displayName)) {
1534
+ selectionAction = `selected-active:${options.displayName}`;
1535
+ actions.push(selectionAction);
1536
+ await delay(750);
1537
+ }
1538
+ }
1539
+ }
1540
+ bodyText = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1541
+ const lowerBodyText = bodyText.toLowerCase();
1542
+ selectedModelTextSeen =
1543
+ lowerBodyText.includes(options.displayName.toLowerCase()) ||
1544
+ lowerBodyText.includes(options.model.toLowerCase());
1545
+ await cdpCommand(webSocketDebuggerUrl, "Input.dispatchKeyEvent", {
1546
+ type: "rawKeyDown",
1547
+ key: "Escape",
1548
+ code: "Escape",
1549
+ windowsVirtualKeyCode: 27,
1550
+ nativeVirtualKeyCode: 53,
1551
+ });
1552
+ await cdpCommand(webSocketDebuggerUrl, "Input.dispatchKeyEvent", {
1553
+ type: "keyUp",
1554
+ key: "Escape",
1555
+ code: "Escape",
1556
+ windowsVirtualKeyCode: 27,
1557
+ nativeVirtualKeyCode: 53,
1558
+ });
1559
+ await delay(500);
1560
+ bodyText = await ensureDesktopComposerOpen(webSocketDebuggerUrl, actions);
1561
+ composerVisible =
1562
+ composerVisible ||
1563
+ (composerPromptVisible(bodyText) &&
1564
+ (composerBodyTextVisible(bodyText) ||
1565
+ bodyText.includes(options.displayName) ||
1566
+ bodyText.includes(options.model))) ||
1567
+ (await desktopComposerDomVisible(webSocketDebuggerUrl));
1568
+ composerSubmissionEvidence = await submitDesktopComposerPrompt(webSocketDebuggerUrl, "Reply with exactly: desktop-probe-ok", actions, [options.displayName, options.model]);
1569
+ if (composerSubmissionEvidence.submitted) {
1570
+ actions.push("submitted:desktop-probe");
1571
+ afterPromptText = await waitForDesktopPromptResult(webSocketDebuggerUrl, Math.min(timeoutMs, 60_000));
1572
+ }
1573
+ }
1574
+ const desktopProbeTextSeen = afterPromptText.includes("desktop-probe-ok");
1575
+ const desktopModelErrorSeen = afterPromptText.includes("AI Model Not Found") ||
1576
+ afterPromptText.includes("Model name is not valid");
1577
+ const resourceUrls = await evaluateCdpStringArray(webSocketDebuggerUrl, `performance.getEntriesByType("resource").map((entry) => entry.name).filter(Boolean).slice(-200)`);
1578
+ const pickerDomEvidence = {
1579
+ workspaceOpened,
1580
+ composerVisible,
1581
+ modelPickerOpened,
1582
+ modelTextSeen,
1583
+ existingModelTextSeen,
1584
+ bodyTextPreview: bodyText.slice(0, 2_000),
1585
+ pickerTextPreview: pickerText.slice(0, 4_000),
1586
+ };
1587
+ const selectedModelEvidence = {
1588
+ requestedModel: options.model,
1589
+ requestedDisplayName: options.displayName,
1590
+ activeModelAlreadySelected,
1591
+ selectedModelTextSeen,
1592
+ ...(selectionAction !== undefined ? { selectionAction } : {}),
1593
+ activeComposerTextPreview: bodyText.slice(0, 2_000),
1594
+ };
1595
+ const visibleResponseEvidence = {
1596
+ expectedMarker: "desktop-probe-ok",
1597
+ markerSeen: desktopProbeTextSeen,
1598
+ modelErrorSeen: desktopModelErrorSeen,
1599
+ bodyTextPreview: afterPromptText.slice(0, 4_000),
1600
+ };
1601
+ return {
1602
+ bodyText,
1603
+ pickerText,
1604
+ workspaceOpened,
1605
+ composerVisible,
1606
+ modelPickerOpened,
1607
+ modelTextSeen,
1608
+ existingModelTextSeen,
1609
+ selectedModelTextSeen,
1610
+ desktopPromptSubmitted: composerSubmissionEvidence.submitted,
1611
+ desktopProbeTextSeen,
1612
+ desktopModelErrorSeen,
1613
+ actions,
1614
+ resourceUrls,
1615
+ pickerDomEvidence,
1616
+ selectedModelEvidence,
1617
+ composerSubmissionEvidence,
1618
+ visibleResponseEvidence,
1619
+ };
1620
+ }
1621
+ async function ensureDesktopComposerOpen(webSocketDebuggerUrl, actions) {
1622
+ let text = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1623
+ if (composerBodyTextVisible(text) &&
1624
+ composerPromptVisible(text) &&
1625
+ !(await composerIsCancelled(webSocketDebuggerUrl))) {
1626
+ return text;
1627
+ }
1628
+ if ((await clickTopRightNewAgent(webSocketDebuggerUrl)) ||
1629
+ (await clickVisibleText(webSocketDebuggerUrl, "New Agent"))) {
1630
+ actions.push("clicked:New Agent");
1631
+ await delay(1_250);
1632
+ }
1633
+ return evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1634
+ }
1635
+ async function composerIsCancelled(webSocketDebuggerUrl) {
1636
+ const status = await evaluateCdpValue(webSocketDebuggerUrl, `document.querySelector('.composer-bar')?.getAttribute('data-composer-status') ?? ''`);
1637
+ return status === "cancelled";
1638
+ }
1639
+ async function clickTopRightNewAgent(webSocketDebuggerUrl) {
1640
+ const clicked = await evaluateCdpValue(webSocketDebuggerUrl, `(() => {
1641
+ const candidates = [...document.querySelectorAll('button, [role="button"]')]
1642
+ .map((element) => ({
1643
+ element,
1644
+ text: (element.textContent || '').replace(/\\s+/g, ' ').trim(),
1645
+ rect: element.getBoundingClientRect(),
1646
+ }))
1647
+ .filter(({ text, rect }) =>
1648
+ text.includes('New Agent') &&
1649
+ rect.width > 0 &&
1650
+ rect.height > 0 &&
1651
+ rect.left > window.innerWidth * 0.65
1652
+ )
1653
+ .sort((left, right) => right.rect.left - left.rect.left);
1654
+ const candidate = candidates.at(0);
1655
+ if (!candidate) {
1656
+ return false;
1657
+ }
1658
+ candidate.element.click();
1659
+ return true;
1660
+ })()`);
1661
+ return clicked === true;
1662
+ }
1663
+ async function dismissDataSharingPrompt(webSocketDebuggerUrl, actions, currentText) {
1664
+ if (!currentText.includes("Data Sharing") &&
1665
+ !currentText.includes("team admin controls data collection")) {
1666
+ return currentText;
1667
+ }
1668
+ const checked = await evaluateCdpValue(webSocketDebuggerUrl, `(() => {
1669
+ const inputs = [...document.querySelectorAll('input[type="checkbox"], input')]
1670
+ .map((element) => ({ element, rect: element.getBoundingClientRect() }))
1671
+ .filter(({ rect }) => rect.width > 0 && rect.height > 0 && rect.top > window.innerHeight * 0.35);
1672
+ const checkbox = inputs.at(0)?.element;
1673
+ if (!checkbox) {
1674
+ return false;
1675
+ }
1676
+ checkbox.click();
1677
+ checkbox.checked = true;
1678
+ checkbox.dispatchEvent(new Event('input', { bubbles: true }));
1679
+ checkbox.dispatchEvent(new Event('change', { bubbles: true }));
1680
+ return true;
1681
+ })()`);
1682
+ if (checked === true) {
1683
+ actions.push("checked:data-sharing");
1684
+ await delay(250);
1685
+ }
1686
+ if (await clickVisibleText(webSocketDebuggerUrl, "Continue")) {
1687
+ actions.push("clicked:Continue");
1688
+ await delay(1_000);
1689
+ }
1690
+ return evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1691
+ }
1692
+ async function submitDesktopComposerPrompt(webSocketDebuggerUrl, prompt, actions, blockedControlTexts = []) {
1693
+ const evidence = {
1694
+ promptPreview: prompt.slice(0, 500),
1695
+ focused: false,
1696
+ inserted: false,
1697
+ sendAttempted: false,
1698
+ submitted: false,
1699
+ };
1700
+ const focusResult = await evaluateCdpString(webSocketDebuggerUrl, `(() => {
1701
+ const monacoTextarea = document.querySelector('.composer-input-blur-wrapper textarea');
1702
+ if (monacoTextarea) {
1703
+ monacoTextarea.focus();
1704
+ monacoTextarea.click();
1705
+ return 'monaco-textarea';
1706
+ }
1707
+ const candidates = [...document.querySelectorAll('textarea, input, [contenteditable="true"]')]
1708
+ .map((element) => ({ element, rect: element.getBoundingClientRect() }))
1709
+ .filter(({ element, rect }) => {
1710
+ const tag = element.tagName.toLowerCase();
1711
+ return rect.width >= 100 && rect.height >= 10 && (tag !== 'input' || element.getAttribute('type') !== 'checkbox');
1712
+ })
1713
+ .sort((left, right) => {
1714
+ const leftEditable = left.element.getAttribute('contenteditable') === 'true' ? 1 : 0;
1715
+ const rightEditable = right.element.getAttribute('contenteditable') === 'true' ? 1 : 0;
1716
+ return rightEditable - leftEditable || right.rect.width * right.rect.height - left.rect.width * left.rect.height;
1717
+ });
1718
+ const element = candidates.at(0)?.element;
1719
+ if (!element) {
1720
+ return '';
1721
+ }
1722
+ element.click();
1723
+ element.focus();
1724
+ return element.tagName.toLowerCase() + ':' + (element.getAttribute('contenteditable') || element.getAttribute('placeholder') || '');
1725
+ })()`);
1726
+ evidence.focused = focusResult.length > 0;
1727
+ evidence.focusTarget = focusResult;
1728
+ if (!evidence.focused) {
1729
+ evidence.failureReason = "composer-focus-target-missing";
1730
+ return evidence;
1731
+ }
1732
+ await clickComposerInputArea(webSocketDebuggerUrl);
1733
+ evidence.editorSummary = (await describeComposerEditors(webSocketDebuggerUrl)).slice(0, 1200);
1734
+ evidence.placeholderSummary = (await describeComposerPlaceholders(webSocketDebuggerUrl)).slice(0, 1200);
1735
+ evidence.composerHtml = (await describeComposerHtml(webSocketDebuggerUrl)).slice(0, 1200);
1736
+ actions.push(`composer-editors:${evidence.editorSummary}`);
1737
+ actions.push(`composer-placeholders:${evidence.placeholderSummary}`);
1738
+ actions.push(`composer-html:${evidence.composerHtml}`);
1739
+ await cdpCommand(webSocketDebuggerUrl, "Input.insertText", { text: prompt });
1740
+ await delay(100);
1741
+ if (!(await composerEditorContains(webSocketDebuggerUrl, prompt))) {
1742
+ await typeTextWithCharEvents(webSocketDebuggerUrl, prompt);
1743
+ await delay(100);
1744
+ }
1745
+ if (!(await composerEditorContains(webSocketDebuggerUrl, prompt))) {
1746
+ await evaluateCdpValue(webSocketDebuggerUrl, `document.execCommand('insertText', false, ${JSON.stringify(prompt)})`);
1747
+ }
1748
+ await delay(250);
1749
+ evidence.editorTextAfterInsert = (await composerEditorText(webSocketDebuggerUrl)).slice(0, 500);
1750
+ evidence.inserted = evidence.editorTextAfterInsert.includes(prompt);
1751
+ actions.push(`composer-editor-after-insert:${evidence.editorTextAfterInsert}`);
1752
+ if (!evidence.inserted) {
1753
+ evidence.failureReason = "composer-insert-verification-failed";
1754
+ return evidence;
1755
+ }
1756
+ await clickComposerInputArea(webSocketDebuggerUrl);
1757
+ const controlSummary = await describeComposerControls(webSocketDebuggerUrl);
1758
+ evidence.controlSummary = controlSummary.slice(0, 1200);
1759
+ actions.push(`composer-controls:${controlSummary.slice(0, 1200)}`);
1760
+ const sendClickStatus = (await composerEditorContains(webSocketDebuggerUrl, prompt))
1761
+ ? await clickComposerSendButton(webSocketDebuggerUrl, prompt, blockedControlTexts)
1762
+ : "missing-prompt";
1763
+ let clicked = sendClickStatus.startsWith("clicked");
1764
+ evidence.sendAttempted = clicked;
1765
+ evidence.sendClickStatus = sendClickStatus;
1766
+ actions.push(`send-clicked:${sendClickStatus}`);
1767
+ if (!clicked &&
1768
+ (await composerEditorContains(webSocketDebuggerUrl, prompt))) {
1769
+ await dispatchEnter(webSocketDebuggerUrl, 4);
1770
+ clicked = true;
1771
+ evidence.sendAttempted = true;
1772
+ evidence.sendClickStatus = "cmd-enter";
1773
+ actions.push("send-fallback:cmd-enter");
1774
+ }
1775
+ await delay(750);
1776
+ let text = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1777
+ if (clicked &&
1778
+ text.includes(prompt) &&
1779
+ !text.includes("Taking longer than expected")) {
1780
+ await dispatchEnter(webSocketDebuggerUrl, 4);
1781
+ await delay(500);
1782
+ text = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1783
+ }
1784
+ const remainingEditorText = await composerEditorText(webSocketDebuggerUrl);
1785
+ if (remainingEditorText.includes(prompt)) {
1786
+ await clickComposerInputArea(webSocketDebuggerUrl);
1787
+ await dispatchEnter(webSocketDebuggerUrl, 4);
1788
+ evidence.sendAttempted = true;
1789
+ evidence.sendClickStatus = "cmd-enter-after-click";
1790
+ actions.push("send-fallback:cmd-enter-after-click");
1791
+ await delay(750);
1792
+ text = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
1793
+ }
1794
+ const finalEditorText = await composerEditorText(webSocketDebuggerUrl);
1795
+ evidence.editorTextAfterSubmit = finalEditorText.slice(0, 500);
1796
+ evidence.bodyTextAfterSubmitPreview = text.slice(0, 2_000);
1797
+ actions.push(`composer-editor-after-submit:${finalEditorText.slice(0, 500)}`);
1798
+ evidence.submitted =
1799
+ (clicked && !finalEditorText.includes(prompt)) ||
1800
+ text.includes("Taking longer than expected") ||
1801
+ text.includes("desktop-probe-ok");
1802
+ if (!evidence.submitted) {
1803
+ evidence.failureReason = clicked
1804
+ ? "composer-submit-did-not-clear-or-start"
1805
+ : "composer-send-control-missing";
1806
+ }
1807
+ return evidence;
1808
+ }
1809
+ async function typeTextWithCharEvents(webSocketDebuggerUrl, text) {
1810
+ for (const char of text) {
1811
+ await cdpCommand(webSocketDebuggerUrl, "Input.dispatchKeyEvent", {
1812
+ type: "char",
1813
+ text: char,
1814
+ unmodifiedText: char,
1815
+ });
1816
+ }
1817
+ }
1818
+ async function composerEditorContains(webSocketDebuggerUrl, text) {
1819
+ const value = await composerEditorText(webSocketDebuggerUrl);
1820
+ return value.includes(text);
1821
+ }
1822
+ async function composerEditorText(webSocketDebuggerUrl) {
1823
+ return evaluateCdpString(webSocketDebuggerUrl, `document.querySelector('.aislash-editor-input')?.textContent ?? ''`);
1824
+ }
1825
+ async function clickComposerInputArea(webSocketDebuggerUrl) {
1826
+ const point = await evaluateCdpValue(webSocketDebuggerUrl, `(() => {
1827
+ const element =
1828
+ document.querySelector('.composer-input-blur-wrapper .aislash-editor-input') ||
1829
+ document.querySelector('.aislash-editor-input') ||
1830
+ document.querySelector('.composer-input-blur-wrapper textarea') ||
1831
+ document.querySelector('.composer-input-blur-wrapper, .composer-bar, .pane-body');
1832
+ if (!element) {
1833
+ return undefined;
1834
+ }
1835
+ const rect = element.getBoundingClientRect();
1836
+ return {
1837
+ x: Math.round(rect.left + rect.width / 2),
1838
+ y: Math.round(rect.top + rect.height / 2),
1839
+ };
1840
+ })()`);
1841
+ if (typeof point !== "object" ||
1842
+ point === null ||
1843
+ typeof point.x !== "number" ||
1844
+ typeof point.y !== "number") {
1845
+ return;
1846
+ }
1847
+ const { x, y } = point;
1848
+ await evaluateCdpValue(webSocketDebuggerUrl, `document.querySelector('.composer-input-blur-wrapper .aislash-editor-input')?.focus?.() || document.querySelector('.aislash-editor-input')?.focus?.()`);
1849
+ await cdpCommand(webSocketDebuggerUrl, "Input.dispatchMouseEvent", {
1850
+ type: "mousePressed",
1851
+ x,
1852
+ y,
1853
+ button: "left",
1854
+ clickCount: 1,
1855
+ });
1856
+ await cdpCommand(webSocketDebuggerUrl, "Input.dispatchMouseEvent", {
1857
+ type: "mouseReleased",
1858
+ x,
1859
+ y,
1860
+ button: "left",
1861
+ clickCount: 1,
1862
+ });
1863
+ }
1864
+ async function describeComposerControls(webSocketDebuggerUrl) {
1865
+ return evaluateCdpString(webSocketDebuggerUrl, `(() => {
1866
+ const controls = [...document.querySelectorAll('button, [role="button"], [aria-label], [title]')]
1867
+ .map((element) => {
1868
+ const rect = element.getBoundingClientRect();
1869
+ const style = getComputedStyle(element);
1870
+ const visible = rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
1871
+ return {
1872
+ visible,
1873
+ text: [
1874
+ element.getAttribute('aria-label'),
1875
+ element.getAttribute('title'),
1876
+ element.textContent,
1877
+ ].filter(Boolean).join(' ').replace(/\\s+/g, ' ').trim().slice(0, 120),
1878
+ x: Math.round(rect.left),
1879
+ y: Math.round(rect.top),
1880
+ w: Math.round(rect.width),
1881
+ h: Math.round(rect.height),
1882
+ };
1883
+ })
1884
+ .filter((entry) => entry.visible && entry.x > window.innerWidth * 0.55)
1885
+ .slice(-30);
1886
+ return JSON.stringify({
1887
+ active: {
1888
+ tag: document.activeElement?.tagName,
1889
+ text: (document.activeElement?.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 160),
1890
+ placeholder: document.activeElement?.getAttribute?.('placeholder') || document.activeElement?.getAttribute?.('aria-placeholder') || '',
1891
+ },
1892
+ controls,
1893
+ });
1894
+ })()`);
1895
+ }
1896
+ async function describeComposerEditors(webSocketDebuggerUrl) {
1897
+ return evaluateCdpString(webSocketDebuggerUrl, `(() => {
1898
+ const editors = [...document.querySelectorAll('textarea, input, [contenteditable="true"], [data-lexical-editor="true"]')]
1899
+ .map((element) => {
1900
+ const rect = element.getBoundingClientRect();
1901
+ const style = getComputedStyle(element);
1902
+ const visible = rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
1903
+ return {
1904
+ visible,
1905
+ tag: element.tagName,
1906
+ contenteditable: element.getAttribute('contenteditable'),
1907
+ placeholder: element.getAttribute('placeholder') || element.getAttribute('aria-placeholder') || '',
1908
+ text: (element.textContent || element.getAttribute('value') || '').replace(/\\s+/g, ' ').trim().slice(0, 160),
1909
+ x: Math.round(rect.left),
1910
+ y: Math.round(rect.top),
1911
+ w: Math.round(rect.width),
1912
+ h: Math.round(rect.height),
1913
+ };
1914
+ })
1915
+ .filter((entry) => entry.visible || entry.tag === 'TEXTAREA')
1916
+ .sort((left, right) => left.y - right.y);
1917
+ return JSON.stringify(editors);
1918
+ })()`);
1919
+ }
1920
+ async function describeComposerPlaceholders(webSocketDebuggerUrl) {
1921
+ return evaluateCdpString(webSocketDebuggerUrl, `(() => {
1922
+ const entries = [...document.querySelectorAll('*')]
1923
+ .map((element) => {
1924
+ const text = (element.textContent || '').replace(/\\s+/g, ' ').trim();
1925
+ if (!/Plan, Build|Plan, search|for skills|context/i.test(text)) {
1926
+ return undefined;
1927
+ }
1928
+ const rect = element.getBoundingClientRect();
1929
+ const style = getComputedStyle(element);
1930
+ if (rect.width <= 0 || rect.height <= 0 || style.visibility === 'hidden' || style.display === 'none') {
1931
+ return undefined;
1932
+ }
1933
+ return {
1934
+ tag: element.tagName,
1935
+ cls: String(element.className || '').slice(0, 120),
1936
+ text: text.slice(0, 160),
1937
+ x: Math.round(rect.left),
1938
+ y: Math.round(rect.top),
1939
+ w: Math.round(rect.width),
1940
+ h: Math.round(rect.height),
1941
+ };
1942
+ })
1943
+ .filter(Boolean)
1944
+ .slice(-20);
1945
+ return JSON.stringify(entries);
1946
+ })()`);
1947
+ }
1948
+ async function describeComposerHtml(webSocketDebuggerUrl) {
1949
+ return evaluateCdpString(webSocketDebuggerUrl, `(() => {
1950
+ const element = document.querySelector('.composer-input-blur-wrapper') || document.querySelector('.composer-bar');
1951
+ return element?.outerHTML.replace(/\\s+/g, ' ').slice(0, 4000) ?? '';
1952
+ })()`);
1953
+ }
1954
+ async function dispatchEnter(webSocketDebuggerUrl, modifiers) {
1955
+ await cdpCommand(webSocketDebuggerUrl, "Input.dispatchKeyEvent", {
1956
+ type: "rawKeyDown",
1957
+ key: "Enter",
1958
+ code: "Enter",
1959
+ windowsVirtualKeyCode: 13,
1960
+ nativeVirtualKeyCode: 36,
1961
+ modifiers,
1962
+ unmodifiedText: "\r",
1963
+ text: "\r",
1964
+ });
1965
+ await cdpCommand(webSocketDebuggerUrl, "Input.dispatchKeyEvent", {
1966
+ type: "keyUp",
1967
+ key: "Enter",
1968
+ code: "Enter",
1969
+ windowsVirtualKeyCode: 13,
1970
+ nativeVirtualKeyCode: 36,
1971
+ modifiers,
1972
+ });
1973
+ }
1974
+ async function clickComposerSendButton(webSocketDebuggerUrl, prompt, blockedControlTexts) {
1975
+ const directClick = await evaluateCdpString(webSocketDebuggerUrl, `(() => {
1976
+ const visible = (element) => {
1977
+ const rect = element.getBoundingClientRect();
1978
+ const style = getComputedStyle(element);
1979
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
1980
+ };
1981
+ const prompt = ${JSON.stringify(prompt)};
1982
+ const blockedControlTexts = ${JSON.stringify(blockedControlTexts)};
1983
+ const isBlockedControlText = (text) => {
1984
+ const normalized = text.toLowerCase();
1985
+ return blockedControlTexts
1986
+ .filter(Boolean)
1987
+ .some((blockedText) => normalized.includes(blockedText.toLowerCase()));
1988
+ };
1989
+ const editor = [...document.querySelectorAll('.aislash-editor-input, [contenteditable="true"], textarea')]
1990
+ .map((element) => ({ element, rect: element.getBoundingClientRect(), text: element.textContent || element.value || '' }))
1991
+ .filter(({ element, rect, text }) => visible(element) && text.includes(prompt) && rect.width > 20 && rect.height > 10)
1992
+ .sort((left, right) => right.rect.left - left.rect.left || left.rect.top - right.rect.top)
1993
+ .at(0);
1994
+ if (editor) {
1995
+ const wrapper =
1996
+ editor.element.closest('.composer-input-blur-wrapper') ||
1997
+ editor.element.closest('.composer-bar') ||
1998
+ editor.element.closest('.pane-body');
1999
+ if (wrapper) {
2000
+ const wrapperRect = wrapper.getBoundingClientRect();
2001
+ const candidates = [...wrapper.querySelectorAll('button, [role="button"], [aria-label], [title]')]
2002
+ .map((element) => ({
2003
+ element,
2004
+ text: [
2005
+ element.getAttribute('aria-label'),
2006
+ element.getAttribute('title'),
2007
+ element.textContent,
2008
+ ].filter(Boolean).join(' ').replace(/\\s+/g, ' ').trim(),
2009
+ rect: element.getBoundingClientRect(),
2010
+ disabled: element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true',
2011
+ }))
2012
+ .filter(({ element, text, rect, disabled }) =>
2013
+ !disabled &&
2014
+ visible(element) &&
2015
+ rect.left >= wrapperRect.left &&
2016
+ rect.right <= wrapperRect.right + 8 &&
2017
+ rect.top >= wrapperRect.top &&
2018
+ rect.bottom <= wrapperRect.bottom + 8 &&
2019
+ !isBlockedControlText(text) &&
2020
+ !/\\b(agent|model|local-qwen|gpt|composer|plan|build|context)\\b/i.test(text)
2021
+ )
2022
+ .sort((left, right) => {
2023
+ const leftScore = Math.abs(wrapperRect.right - left.rect.right) + Math.abs(wrapperRect.bottom - left.rect.bottom);
2024
+ const rightScore = Math.abs(wrapperRect.right - right.rect.right) + Math.abs(wrapperRect.bottom - right.rect.bottom);
2025
+ return leftScore - rightScore;
2026
+ });
2027
+ const candidate = candidates.at(0);
2028
+ if (candidate) {
2029
+ candidate.element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerId: 1, pointerType: 'mouse' }));
2030
+ candidate.element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
2031
+ candidate.element.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, cancelable: true, pointerId: 1, pointerType: 'mouse' }));
2032
+ candidate.element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
2033
+ candidate.element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
2034
+ return 'clicked-dom:' + (candidate.text || 'untitled') + '@' + Math.round(candidate.rect.left) + ',' + Math.round(candidate.rect.top) + ',' + Math.round(candidate.rect.width) + 'x' + Math.round(candidate.rect.height);
2035
+ }
2036
+ }
2037
+ }
2038
+ return '';
2039
+ })()`);
2040
+ if (directClick.startsWith("clicked-dom:")) {
2041
+ return directClick;
2042
+ }
2043
+ const shortcut = await evaluateCdpString(webSocketDebuggerUrl, `(() => {
2044
+ const prompt = ${JSON.stringify(prompt)};
2045
+ const blockedControlTexts = ${JSON.stringify(blockedControlTexts)};
2046
+ const isBlockedControlText = (text) => {
2047
+ const normalized = text.toLowerCase();
2048
+ return blockedControlTexts
2049
+ .filter(Boolean)
2050
+ .some((blockedText) => normalized.includes(blockedText.toLowerCase()));
2051
+ };
2052
+ const visible = (element) => {
2053
+ const rect = element.getBoundingClientRect();
2054
+ const style = getComputedStyle(element);
2055
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
2056
+ };
2057
+ const editor = [...document.querySelectorAll('.aislash-editor-input, [contenteditable="true"], textarea')]
2058
+ .map((element) => ({ element, rect: element.getBoundingClientRect(), text: element.textContent || element.value || '' }))
2059
+ .filter(({ element, rect, text }) => visible(element) && text.includes(prompt) && rect.width > 20 && rect.height > 10)
2060
+ .sort((left, right) => right.rect.left - left.rect.left || left.rect.top - right.rect.top)
2061
+ .at(0);
2062
+ if (!editor) {
2063
+ return '';
2064
+ }
2065
+ editor.element.focus?.();
2066
+ const selection = window.getSelection?.();
2067
+ if (selection && editor.element instanceof HTMLElement) {
2068
+ const range = document.createRange();
2069
+ range.selectNodeContents(editor.element);
2070
+ range.collapse(false);
2071
+ selection.removeAllRanges();
2072
+ selection.addRange(range);
2073
+ }
2074
+ for (const event of [
2075
+ new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, metaKey: true, bubbles: true, cancelable: true }),
2076
+ new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, metaKey: true, bubbles: true, cancelable: true }),
2077
+ new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true }),
2078
+ new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true }),
2079
+ ]) {
2080
+ editor.element.dispatchEvent(event);
2081
+ }
2082
+ return 'clicked-dom-shortcut';
2083
+ })()`);
2084
+ if (shortcut.startsWith("clicked-dom-shortcut")) {
2085
+ return shortcut;
2086
+ }
2087
+ const point = await evaluateCdpValue(webSocketDebuggerUrl, `(() => {
2088
+ const visible = (element) => {
2089
+ const rect = element.getBoundingClientRect();
2090
+ const style = getComputedStyle(element);
2091
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
2092
+ };
2093
+ const labeled = [...document.querySelectorAll('button, [role="button"], [aria-label], [title]')]
2094
+ .map((element) => {
2095
+ const text = [
2096
+ element.getAttribute('aria-label'),
2097
+ element.getAttribute('title'),
2098
+ element.textContent,
2099
+ ].filter(Boolean).join(' ');
2100
+ return { element, text, rect: element.getBoundingClientRect() };
2101
+ })
2102
+ .filter(({ element, text }) => visible(element) && /\\b(send|submit)\\b/i.test(text))
2103
+ .sort((left, right) => right.rect.top - left.rect.top || right.rect.left - left.rect.left);
2104
+ const labeledCandidate = labeled.at(0);
2105
+ if (labeledCandidate) {
2106
+ return {
2107
+ x: Math.round(labeledCandidate.rect.left + labeledCandidate.rect.width / 2),
2108
+ y: Math.round(labeledCandidate.rect.top + labeledCandidate.rect.height / 2),
2109
+ };
2110
+ }
2111
+
2112
+ const wrapper =
2113
+ document.querySelector('.composer-input-blur-wrapper') ||
2114
+ document.querySelector('.composer-bar');
2115
+ if (wrapper) {
2116
+ const wrapperRect = wrapper.getBoundingClientRect();
2117
+ const wrapperCandidates = [...wrapper.querySelectorAll('button, [role="button"]')]
2118
+ .map((element) => ({
2119
+ element,
2120
+ text: [
2121
+ element.getAttribute('aria-label'),
2122
+ element.getAttribute('title'),
2123
+ element.textContent,
2124
+ ].filter(Boolean).join(' ').replace(/\\s+/g, ' ').trim(),
2125
+ rect: element.getBoundingClientRect(),
2126
+ }))
2127
+ .filter(({ element, text, rect }) =>
2128
+ visible(element) &&
2129
+ rect.left >= wrapperRect.right - 140 &&
2130
+ rect.top >= wrapperRect.bottom - 80 &&
2131
+ rect.top <= wrapperRect.bottom + 12 &&
2132
+ !isBlockedControlText(text) &&
2133
+ !/\\b(agent|model|local-qwen|gpt|composer|plan|build)\\b/i.test(text)
2134
+ )
2135
+ .sort((left, right) => right.rect.left - left.rect.left || right.rect.top - left.rect.top);
2136
+ const wrapperCandidate = wrapperCandidates.at(0);
2137
+ if (wrapperCandidate) {
2138
+ return {
2139
+ x: Math.round(wrapperCandidate.rect.left + wrapperCandidate.rect.width / 2),
2140
+ y: Math.round(wrapperCandidate.rect.top + wrapperCandidate.rect.height / 2),
2141
+ };
2142
+ }
2143
+ return {
2144
+ x: Math.round(wrapperRect.right - 24),
2145
+ y: Math.round(wrapperRect.bottom - 24),
2146
+ };
2147
+ }
2148
+
2149
+ const editors = [...document.querySelectorAll('textarea, input, [contenteditable="true"]')]
2150
+ .map((element) => ({ element, rect: element.getBoundingClientRect() }))
2151
+ .filter(({ element, rect }) => {
2152
+ const tag = element.tagName.toLowerCase();
2153
+ return visible(element) && rect.width >= 100 && rect.height >= 10 && (tag !== 'input' || element.getAttribute('type') !== 'checkbox');
2154
+ })
2155
+ .sort((left, right) => {
2156
+ const leftEditable = left.element.getAttribute('contenteditable') === 'true' ? 1 : 0;
2157
+ const rightEditable = right.element.getAttribute('contenteditable') === 'true' ? 1 : 0;
2158
+ return rightEditable - leftEditable || right.rect.width * right.rect.height - left.rect.width * left.rect.height;
2159
+ });
2160
+ const editor = editors.at(0);
2161
+ if (!editor) {
2162
+ return false;
2163
+ }
2164
+ const candidates = [...document.querySelectorAll('button, [role="button"]')]
2165
+ .map((element) => ({ element, rect: element.getBoundingClientRect() }))
2166
+ .filter(({ element, rect }) =>
2167
+ visible(element) &&
2168
+ rect.left >= editor.rect.right - 220 &&
2169
+ rect.top >= editor.rect.top - 60 &&
2170
+ rect.top <= editor.rect.bottom + 120
2171
+ )
2172
+ .sort((left, right) => right.rect.left - left.rect.left || right.rect.top - left.rect.top);
2173
+ const candidate = candidates.at(0);
2174
+ if (!candidate) {
2175
+ return undefined;
2176
+ }
2177
+ return {
2178
+ x: Math.round(candidate.rect.left + candidate.rect.width / 2),
2179
+ y: Math.round(candidate.rect.top + candidate.rect.height / 2),
2180
+ };
2181
+ })()`);
2182
+ if (typeof point !== "object" ||
2183
+ point === null ||
2184
+ typeof point.x !== "number" ||
2185
+ typeof point.y !== "number") {
2186
+ return `not-clicked:${directClick || "no-target"}`;
2187
+ }
2188
+ const { x, y } = point;
2189
+ await cdpCommand(webSocketDebuggerUrl, "Input.dispatchMouseEvent", {
2190
+ type: "mousePressed",
2191
+ x,
2192
+ y,
2193
+ button: "left",
2194
+ clickCount: 1,
2195
+ });
2196
+ await cdpCommand(webSocketDebuggerUrl, "Input.dispatchMouseEvent", {
2197
+ type: "mouseReleased",
2198
+ x,
2199
+ y,
2200
+ button: "left",
2201
+ clickCount: 1,
2202
+ });
2203
+ return `clicked-coordinates:${x},${y}`;
2204
+ }
2205
+ async function waitForDesktopPromptResult(webSocketDebuggerUrl, timeoutMs) {
2206
+ const started = Date.now();
2207
+ let text = "";
2208
+ while (Date.now() - started < timeoutMs) {
2209
+ text = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
2210
+ if (text.includes("desktop-probe-ok") ||
2211
+ text.includes("AI Model Not Found") ||
2212
+ text.includes("Model name is not valid")) {
2213
+ return text;
2214
+ }
2215
+ await delay(500);
2216
+ }
2217
+ return text;
2218
+ }
2219
+ function desktopPickerHasBuiltInModel(text) {
2220
+ return [
2221
+ "Auto",
2222
+ "Composer",
2223
+ "Fable",
2224
+ "Opus",
2225
+ "GPT-",
2226
+ "Claude",
2227
+ "Sonnet",
2228
+ "Gemini",
2229
+ "Codex",
2230
+ ].some((modelText) => text.includes(modelText));
2231
+ }
2232
+ async function desktopComposerDomVisible(webSocketDebuggerUrl) {
2233
+ const visible = await evaluateCdpValue(webSocketDebuggerUrl, `(() => {
2234
+ const isVisible = (element) => {
2235
+ const rect = element.getBoundingClientRect();
2236
+ const style = getComputedStyle(element);
2237
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
2238
+ };
2239
+ return [
2240
+ '.composer-input-blur-wrapper .aislash-editor-input',
2241
+ '.composer-input-blur-wrapper textarea',
2242
+ '.composer-input-blur-wrapper',
2243
+ '.composer-bar',
2244
+ ].some((selector) => {
2245
+ const element = document.querySelector(selector);
2246
+ return element !== null && isVisible(element);
2247
+ });
2248
+ })()`);
2249
+ return visible === true;
2250
+ }
2251
+ function composerBodyTextVisible(text) {
2252
+ const lowerText = text.toLowerCase();
2253
+ return (lowerText.includes("auto") ||
2254
+ lowerText.includes("gpt-") ||
2255
+ lowerText.includes("local-qwen") ||
2256
+ lowerText.includes("claude") ||
2257
+ lowerText.includes("composer") ||
2258
+ lowerText.includes("fable") ||
2259
+ lowerText.includes("opus") ||
2260
+ lowerText.includes("gemini"));
2261
+ }
2262
+ function composerPromptVisible(text) {
2263
+ return (text.includes("Plan, Build") ||
2264
+ text.includes("Plan, search") ||
2265
+ text.includes("Plan, build") ||
2266
+ text.includes("Agent"));
2267
+ }
2268
+ async function clickVisibleModelPickerTrigger(webSocketDebuggerUrl) {
2269
+ const result = await evaluateCdpValue(webSocketDebuggerUrl, `(() => {
2270
+ const patterns = [
2271
+ /^Auto$/i,
2272
+ /^GPT[-\\w.\\s]+$/i,
2273
+ /^Claude[\\w.\\s-]*$/i,
2274
+ /^Composer[\\w.\\s-]*$/i,
2275
+ /^Fable[\\w.\\s-]*$/i,
2276
+ /^Opus[\\w.\\s-]*$/i,
2277
+ /^Gemini[\\w.\\s-]*$/i,
2278
+ /^local[-\\w.\\s]*$/i,
2279
+ ];
2280
+ const candidates = [...document.querySelectorAll("*")]
2281
+ .map((element) => {
2282
+ const text = (element.innerText || element.textContent || "").trim();
2283
+ const rect = element.getBoundingClientRect();
2284
+ return { element, text, rect };
2285
+ })
2286
+ .filter(({ text, rect }) =>
2287
+ text.length > 0 &&
2288
+ text.length < 80 &&
2289
+ rect.width > 0 &&
2290
+ rect.height > 0 &&
2291
+ patterns.some((pattern) => pattern.test(text))
2292
+ )
2293
+ .sort((left, right) => left.rect.top - right.rect.top);
2294
+ const candidate = candidates.at(-1);
2295
+ if (!candidate) {
2296
+ return undefined;
2297
+ }
2298
+ candidate.element.click();
2299
+ return candidate.text;
2300
+ })()`);
2301
+ return typeof result === "string" ? result : undefined;
2302
+ }
2303
+ async function evaluateCdpStringArray(webSocketDebuggerUrl, expression) {
2304
+ const value = await evaluateCdpValue(webSocketDebuggerUrl, expression);
2305
+ if (!Array.isArray(value)) {
2306
+ return [];
2307
+ }
2308
+ return value.filter((item) => typeof item === "string");
2309
+ }
2310
+ async function waitForCdpBodyText(webSocketDebuggerUrl, timeoutMs) {
2311
+ const started = Date.now();
2312
+ let text = "";
2313
+ while (Date.now() - started < timeoutMs) {
2314
+ text = await evaluateCdpString(webSocketDebuggerUrl, "document.body?.innerText ?? ''");
2315
+ if (text.trim().length > 0) {
2316
+ return text;
2317
+ }
2318
+ await delay(250);
2319
+ }
2320
+ return text;
2321
+ }
2322
+ async function clickVisibleText(webSocketDebuggerUrl, label) {
2323
+ const result = await evaluateCdpValue(webSocketDebuggerUrl, `(() => {
2324
+ const label = ${JSON.stringify(label)};
2325
+ const elements = [...document.querySelectorAll("*")]
2326
+ .filter((element) => {
2327
+ const text = (element.innerText || element.textContent || "").trim();
2328
+ const rect = element.getBoundingClientRect();
2329
+ return text === label && rect.width > 0 && rect.height > 0;
2330
+ });
2331
+ const element = elements.at(-1);
2332
+ if (!element) {
2333
+ return false;
2334
+ }
2335
+ element.click();
2336
+ return true;
2337
+ })()`);
2338
+ return result === true;
2339
+ }
2340
+ async function evaluateCdpString(webSocketDebuggerUrl, expression) {
2341
+ const value = await evaluateCdpValue(webSocketDebuggerUrl, expression);
2342
+ return typeof value === "string" ? value : "";
2343
+ }
2344
+ async function evaluateCdpValue(webSocketDebuggerUrl, expression) {
2345
+ const response = await cdpCommand(webSocketDebuggerUrl, "Runtime.evaluate", {
2346
+ expression,
2347
+ returnByValue: true,
2348
+ });
2349
+ const result = response.result;
2350
+ return result?.result?.value;
2351
+ }
2352
+ async function cdpCommand(webSocketDebuggerUrl, method, params = {}) {
2353
+ const socket = new WebSocket(webSocketDebuggerUrl);
2354
+ let nextId = 1;
2355
+ const pending = new Map();
2356
+ socket.addEventListener("message", (event) => {
2357
+ const parsed = JSON.parse(String(event.data));
2358
+ const id = typeof parsed.id === "number" ? parsed.id : undefined;
2359
+ if (id === undefined) {
2360
+ return;
2361
+ }
2362
+ const resolve = pending.get(id);
2363
+ if (resolve === undefined) {
2364
+ return;
2365
+ }
2366
+ pending.delete(id);
2367
+ resolve(parsed);
2368
+ });
2369
+ await new Promise((resolve, reject) => {
2370
+ socket.addEventListener("open", () => resolve(), { once: true });
2371
+ socket.addEventListener("error", () => reject(new Error("CDP socket error")), { once: true });
2372
+ });
2373
+ try {
2374
+ const id = nextId;
2375
+ nextId += 1;
2376
+ socket.send(JSON.stringify({
2377
+ id,
2378
+ method,
2379
+ params,
2380
+ }));
2381
+ return await new Promise((resolve) => {
2382
+ pending.set(id, resolve);
2383
+ });
2384
+ }
2385
+ finally {
2386
+ socket.close();
2387
+ }
2388
+ }
2389
+ function parseAgentRunDiagnostics(logText) {
2390
+ const summaries = [];
2391
+ for (const line of logText.split(/\r?\n/)) {
2392
+ const trimmed = line.trim();
2393
+ if (trimmed.length === 0) {
2394
+ continue;
2395
+ }
2396
+ try {
2397
+ const parsed = JSON.parse(trimmed);
2398
+ if (parsed.message !== "local agent run diagnostics") {
2399
+ continue;
2400
+ }
2401
+ summaries.push({
2402
+ modelId: stringField(parsed.modelId),
2403
+ promptLength: numberField(parsed.promptLength),
2404
+ customSystemPromptLength: numberField(parsed.customSystemPromptLength),
2405
+ promptContextNodes: numberField(parsed.promptContextNodes),
2406
+ inlinePromptContextNodes: numberField(parsed.inlinePromptContextNodes),
2407
+ inlinePromptContextCharacters: numberField(parsed.inlinePromptContextCharacters),
2408
+ mcpToolCount: numberField(parsed.mcpToolCount),
2409
+ mcpToolNames: stringArrayField(parsed.mcpToolNames),
2410
+ hasMcpFileSystemOptions: booleanField(parsed.hasMcpFileSystemOptions),
2411
+ hasSkillOptions: booleanField(parsed.hasSkillOptions),
2412
+ excludeWorkspaceContext: booleanField(parsed.excludeWorkspaceContext),
2413
+ preFetchedBlobCount: numberField(parsed.preFetchedBlobCount),
2414
+ injectedContextMessages: numberField(parsed.injectedContextMessages),
2415
+ });
2416
+ }
2417
+ catch {
2418
+ continue;
2419
+ }
2420
+ }
2421
+ return summaries;
2422
+ }
2423
+ function trafficFailureCode(report) {
2424
+ if (report.listModels.exitCode !== 0) {
2425
+ return "model_metadata_rejected";
2426
+ }
2427
+ if (!report.routeInventory.routeInventorySeen) {
2428
+ return "route_missing";
2429
+ }
2430
+ if (report.routeInventory.modelRoutesSeen.length === 0) {
2431
+ return "model_route_missing";
2432
+ }
2433
+ if (!report.listedModel) {
2434
+ return "model_metadata_rejected";
2435
+ }
2436
+ if (report.prompt.exitCode !== 0 || !report.completedPrompt) {
2437
+ return "local_completion_failed";
2438
+ }
2439
+ return undefined;
2440
+ }
2441
+ function trafficFailureMessage(code) {
2442
+ switch (code) {
2443
+ case "route_missing":
2444
+ return "cursor-agent traffic did not reach bridge route inventory";
2445
+ case "model_route_missing":
2446
+ return "cursor-agent reached the bridge, but known model routes were not observed";
2447
+ case "extension_host_route_missing":
2448
+ return "desktop extension-host agent traffic did not reach the bridge";
2449
+ case "model_metadata_rejected":
2450
+ return "cursor-agent did not list or accept the local model";
2451
+ case "local_completion_failed":
2452
+ return "cursor-agent listed the model, but a normal prompt did not complete through the local path";
2453
+ case "desktop_picker_missing":
2454
+ case "desktop_model_selection_missing":
2455
+ case "desktop_prompt_submission_failed":
2456
+ case "desktop_backend_request_missing":
2457
+ case "desktop_visible_response_missing":
2458
+ return code;
2459
+ case "backend_unreachable":
2460
+ case "bridge_start_failed":
2461
+ case "upstream_passthrough_failed":
2462
+ case "auth_profile_blocked":
2463
+ case "command_failed":
2464
+ case "timeout":
2465
+ case "not_available":
2466
+ return code;
2467
+ default: {
2468
+ const exhaustive = code;
2469
+ return exhaustive;
2470
+ }
2471
+ }
2472
+ }
2473
+ function stringField(value) {
2474
+ return typeof value === "string" ? value : "";
2475
+ }
2476
+ function numberField(value) {
2477
+ return typeof value === "number" ? value : 0;
2478
+ }
2479
+ function booleanField(value) {
2480
+ return typeof value === "boolean" ? value : false;
2481
+ }
2482
+ function stringArrayField(value) {
2483
+ if (!Array.isArray(value)) {
2484
+ return [];
2485
+ }
2486
+ return value.filter((item) => typeof item === "string");
2487
+ }
2488
+ function acpFailureCode(report) {
2489
+ if (!report.initialized || !report.authenticated || !report.sessionCreated) {
2490
+ return "auth_profile_blocked";
2491
+ }
2492
+ if (!report.routeInventory.routeInventorySeen) {
2493
+ return "route_missing";
2494
+ }
2495
+ if (report.routeInventory.modelRoutesSeen.length === 0) {
2496
+ return "model_route_missing";
2497
+ }
2498
+ if (!report.promptCompleted ||
2499
+ !report.textPreview.includes("traffic-probe-ok")) {
2500
+ return "local_completion_failed";
2501
+ }
2502
+ return undefined;
2503
+ }
2504
+ function acpFailureMessage(code) {
2505
+ switch (code) {
2506
+ case "auth_profile_blocked":
2507
+ return "ACP did not complete initialize/authenticate/session setup";
2508
+ case "route_missing":
2509
+ return "ACP traffic did not reach bridge route inventory";
2510
+ case "model_route_missing":
2511
+ return "ACP reached the bridge, but known model routes were not observed";
2512
+ case "extension_host_route_missing":
2513
+ return "desktop extension-host agent traffic did not reach the bridge";
2514
+ case "local_completion_failed":
2515
+ return "ACP session prompt did not complete through the local path";
2516
+ case "desktop_picker_missing":
2517
+ case "desktop_model_selection_missing":
2518
+ case "desktop_prompt_submission_failed":
2519
+ case "desktop_backend_request_missing":
2520
+ case "desktop_visible_response_missing":
2521
+ return code;
2522
+ case "backend_unreachable":
2523
+ case "bridge_start_failed":
2524
+ case "model_metadata_rejected":
2525
+ case "upstream_passthrough_failed":
2526
+ case "command_failed":
2527
+ case "timeout":
2528
+ case "not_available":
2529
+ return code;
2530
+ default: {
2531
+ const exhaustive = code;
2532
+ return exhaustive;
2533
+ }
2534
+ }
2535
+ }