@ait-co/devtools 0.1.128 → 0.1.131

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 (113) hide show
  1. package/dist/{attach-orchestrator-CPMQWkGV.js → attach-orchestrator-BiaYiNOr.js} +34 -107
  2. package/dist/attach-orchestrator-BiaYiNOr.js.map +1 -0
  3. package/dist/{attach-orchestrator-RhOdO3DS.js → attach-orchestrator-Cjdhir2U.js} +31 -13
  4. package/dist/{attach-orchestrator-RhOdO3DS.js.map → attach-orchestrator-Cjdhir2U.js.map} +1 -1
  5. package/dist/{attach-orchestrator-CpE0dMew.js → attach-orchestrator-ca34r46y.js} +33 -106
  6. package/dist/attach-orchestrator-ca34r46y.js.map +1 -0
  7. package/dist/{bundle-Cb3GFuw6.d.ts → bundle-BKqyhEK9.d.ts} +1 -1
  8. package/dist/{bundle-Cb3GFuw6.d.ts.map → bundle-BKqyhEK9.d.ts.map} +1 -1
  9. package/dist/{capture-B1zfGyTo.d.ts → capture-ltuV0gZa.d.ts} +1 -1
  10. package/dist/{capture-B1zfGyTo.d.ts.map → capture-ltuV0gZa.d.ts.map} +1 -1
  11. package/dist/{cdp-connection-Dxw_EY8s.d.ts → cdp-connection-D7AYQck2.d.ts} +1 -1
  12. package/dist/{cdp-connection-Dxw_EY8s.d.ts.map → cdp-connection-D7AYQck2.d.ts.map} +1 -1
  13. package/dist/{cell-D0dJSGHi.js → cell-Bi8MAPA8.js} +11 -7
  14. package/dist/cell-Bi8MAPA8.js.map +1 -0
  15. package/dist/{cell-Ba9lCI3B.js → cell-Cw4fVOl2.js} +11 -7
  16. package/dist/cell-Cw4fVOl2.js.map +1 -0
  17. package/dist/{cell-CX92lDI0.js → cell-rr6gL9AT.js} +11 -7
  18. package/dist/cell-rr6gL9AT.js.map +1 -0
  19. package/dist/debug-server-BSbfnFR5.js +436 -0
  20. package/dist/debug-server-BSbfnFR5.js.map +1 -0
  21. package/dist/debug-server-CD4zYBq4.js +379 -0
  22. package/dist/{debug-server-BzzNkETg.js.map → debug-server-CD4zYBq4.js.map} +1 -1
  23. package/dist/{debug-server-Dctc9GAh.js → debug-server-qRSRG7YZ.js} +68 -65
  24. package/dist/debug-server-qRSRG7YZ.js.map +1 -0
  25. package/dist/in-app/auto.js +15 -0
  26. package/dist/in-app/auto.js.map +1 -1
  27. package/dist/in-app/index.d.ts +27 -0
  28. package/dist/in-app/index.d.ts.map +1 -1
  29. package/dist/in-app/index.js +15 -0
  30. package/dist/in-app/index.js.map +1 -1
  31. package/dist/log-CUik_tHp.js +95 -0
  32. package/dist/log-CUik_tHp.js.map +1 -0
  33. package/dist/mcp/cli.js +1272 -1111
  34. package/dist/mcp/cli.js.map +1 -1
  35. package/dist/mcp/server.js +1 -1
  36. package/dist/panel/index.js +11 -1
  37. package/dist/panel/index.js.map +1 -1
  38. package/dist/{pool-DBLKNeRC.d.ts → pool-BaSCZALv.d.ts} +12 -4
  39. package/dist/{pool-DBLKNeRC.d.ts.map → pool-BaSCZALv.d.ts.map} +1 -1
  40. package/dist/{qr-http-server-Bo8LMOJQ.js → qr-http-server-BdNGPRoL.js} +76 -6
  41. package/dist/qr-http-server-BdNGPRoL.js.map +1 -0
  42. package/dist/{qr-http-server-CrGohOwC.js → qr-http-server-Bq9zR7-z.js} +76 -6
  43. package/dist/qr-http-server-Bq9zR7-z.js.map +1 -0
  44. package/dist/{qr-http-server-Bt_RAxFn.cjs → qr-http-server-BxoRp2w7.cjs} +76 -6
  45. package/dist/qr-http-server-BxoRp2w7.cjs.map +1 -0
  46. package/dist/{qr-http-server-DM2R25jP.cjs → qr-http-server-CV6TA9ZN.cjs} +76 -6
  47. package/dist/qr-http-server-CV6TA9ZN.cjs.map +1 -0
  48. package/dist/{qr-http-server-s1iW_5VQ.js → qr-http-server-DCbjORFa.js} +76 -6
  49. package/dist/qr-http-server-DCbjORFa.js.map +1 -0
  50. package/dist/{qr-http-server-BH0UFeUn.js → qr-http-server-cKbciY5k.js} +76 -6
  51. package/dist/qr-http-server-cKbciY5k.js.map +1 -0
  52. package/dist/{relay-factory-Dug7W6Ao.js → relay-factory-Cloog1jb.js} +34 -14
  53. package/dist/relay-factory-Cloog1jb.js.map +1 -0
  54. package/dist/{relay-secret-store-DKxs7zwq.js → relay-secret-store-DzxDsK6S.js} +1 -1
  55. package/dist/{relay-secret-store-DKxs7zwq.js.map → relay-secret-store-DzxDsK6S.js.map} +1 -1
  56. package/dist/{relay-url-store-xmUuTjXA.js → relay-url-store-D3rY-GJJ.js} +2 -2
  57. package/dist/{relay-url-store-xmUuTjXA.js.map → relay-url-store-D3rY-GJJ.js.map} +1 -1
  58. package/dist/{debug-server-BzzNkETg.js → relay-worker-CCAHLC4a.js} +220 -350
  59. package/dist/relay-worker-CCAHLC4a.js.map +1 -0
  60. package/dist/{relay-worker-TReZtzGl.d.ts → relay-worker-o0M2ndyJ.d.ts} +7 -7
  61. package/dist/relay-worker-o0M2ndyJ.d.ts.map +1 -0
  62. package/dist/{runtime-ozyPnRbU.d.ts → runtime-9xhN9pr8.d.ts} +1 -1
  63. package/dist/{runtime-ozyPnRbU.d.ts.map → runtime-9xhN9pr8.d.ts.map} +1 -1
  64. package/dist/test-runner/bin.js +744 -25
  65. package/dist/test-runner/bin.js.map +1 -1
  66. package/dist/test-runner/bundle.d.ts +1 -1
  67. package/dist/test-runner/capture.d.ts +1 -1
  68. package/dist/test-runner/config.d.ts +15 -9
  69. package/dist/test-runner/config.d.ts.map +1 -1
  70. package/dist/test-runner/config.js +1 -1
  71. package/dist/test-runner/pool.d.ts +1 -1
  72. package/dist/test-runner/pool.js +1 -1
  73. package/dist/test-runner/pool.js.map +1 -1
  74. package/dist/test-runner/relay-factory.d.ts +23 -9
  75. package/dist/test-runner/relay-factory.d.ts.map +1 -1
  76. package/dist/test-runner/relay-factory.js +32 -12
  77. package/dist/test-runner/relay-factory.js.map +1 -1
  78. package/dist/test-runner/relay-worker.d.ts +1 -1
  79. package/dist/test-runner/relay-worker.js +1 -187
  80. package/dist/test-runner/report.d.ts +3 -3
  81. package/dist/test-runner/rpc.d.ts +2 -2
  82. package/dist/test-runner/runtime.d.ts +1 -1
  83. package/dist/test-runner/task-graph.d.ts +1 -1
  84. package/dist/{tunnel-DyEV6WwL.cjs → tunnel-B2UCKAc7.cjs} +4 -3
  85. package/dist/{tunnel-DyEV6WwL.cjs.map → tunnel-B2UCKAc7.cjs.map} +1 -1
  86. package/dist/{tunnel-BFwOSwT4.js → tunnel-C-rQ34dM.js} +4 -3
  87. package/dist/{tunnel-BFwOSwT4.js.map → tunnel-C-rQ34dM.js.map} +1 -1
  88. package/dist/unplugin/index.cjs +1 -1
  89. package/dist/unplugin/index.js +1 -1
  90. package/dist/unplugin/tunnel.cjs +3 -2
  91. package/dist/unplugin/tunnel.cjs.map +1 -1
  92. package/dist/unplugin/tunnel.d.cts.map +1 -1
  93. package/dist/unplugin/tunnel.d.ts.map +1 -1
  94. package/dist/unplugin/tunnel.js +3 -2
  95. package/dist/unplugin/tunnel.js.map +1 -1
  96. package/package.json +1 -1
  97. package/dist/attach-orchestrator-CPMQWkGV.js.map +0 -1
  98. package/dist/attach-orchestrator-CpE0dMew.js.map +0 -1
  99. package/dist/cell-Ba9lCI3B.js.map +0 -1
  100. package/dist/cell-CX92lDI0.js.map +0 -1
  101. package/dist/cell-D0dJSGHi.js.map +0 -1
  102. package/dist/debug-server-1dhDA3aw.js +0 -980
  103. package/dist/debug-server-1dhDA3aw.js.map +0 -1
  104. package/dist/debug-server-Dctc9GAh.js.map +0 -1
  105. package/dist/qr-http-server-BH0UFeUn.js.map +0 -1
  106. package/dist/qr-http-server-Bo8LMOJQ.js.map +0 -1
  107. package/dist/qr-http-server-Bt_RAxFn.cjs.map +0 -1
  108. package/dist/qr-http-server-CrGohOwC.js.map +0 -1
  109. package/dist/qr-http-server-DM2R25jP.cjs.map +0 -1
  110. package/dist/qr-http-server-s1iW_5VQ.js.map +0 -1
  111. package/dist/relay-factory-Dug7W6Ao.js.map +0 -1
  112. package/dist/relay-worker-TReZtzGl.d.ts.map +0 -1
  113. package/dist/test-runner/relay-worker.js.map +0 -1
@@ -1,17 +1,10 @@
1
- #!/usr/bin/env node
2
- import { n as buildRelayVerifyAuth, r as generateTotp, t as assertRelayAuthConfigured } from "./totp-DvOYkYim.js";
3
- import "./test-runner/bin.js";
4
- import { a as startTunnelHealthProbe, i as startQuickTunnel, n as makeTunnelStatus, o as logError, r as printAttachBanner, s as logInfo, t as generateAttachToken } from "./attach-orchestrator-CpE0dMew.js";
5
- import "./cell-CX92lDI0.js";
6
- import "./qr-http-server-s1iW_5VQ.js";
7
- import "./relay-secret-store-CFc9n0OA.js";
8
- import { createRequire } from "node:module";
9
- import "@modelcontextprotocol/sdk/server/index.js";
10
- import "@modelcontextprotocol/sdk/server/stdio.js";
11
- import "@modelcontextprotocol/sdk/types.js";
1
+ import { r as generateTotp } from "./totp-DfekTBk3.js";
2
+ import { n as logInfo } from "./log-CUik_tHp.js";
3
+ import { bundleTestFile } from "./test-runner/bundle.js";
4
+ import { parseCaptureLines } from "./test-runner/capture.js";
5
+ import { injectAndRunBundle } from "./test-runner/rpc.js";
12
6
  import { EventEmitter } from "node:events";
13
- import { WebSocket, WebSocketServer } from "ws";
14
- import { createServer } from "node:http";
7
+ import { WebSocket } from "ws";
15
8
  //#region src/shared/relay-auth-close.ts
16
9
  /**
17
10
  * Shared constants for the relay's named TOTP-auth rejection (issue #478).
@@ -75,6 +68,25 @@ const RELAY_AUTH_REJECT_REASON = "totp-rejected";
75
68
  */
76
69
  /** Max events retained per domain ring buffer. */
77
70
  const DEFAULT_BUFFER_SIZE = 500;
71
+ /**
72
+ * Substrings that mark a "relay websocket is dead" class error, as produced by
73
+ * `handleDisconnect('relay WebSocket 연결이 끊겼습니다')` (ws close handler) and
74
+ * the fail-fast `sendCommand` rejection (`relay에 연결되어 있지 않습니다 (...)`).
75
+ *
76
+ * Exported so callers outside this module (e.g. `test-runner/relay-worker.ts`)
77
+ * can detect the same error class without hardcoding a fragile full-sentence
78
+ * match — mirrors the `EVALUATE_TIMEOUT_MARKER` precedent in
79
+ * `test-runner/relay-worker.ts`. Kept in sync with `classifyToolError`'s
80
+ * `relayDisconnectError` branch in `mcp/errors.ts`.
81
+ */
82
+ const RELAY_DISCONNECT_MARKERS = ["relay WebSocket", "relay에 연결되어 있지 않습니다"];
83
+ /**
84
+ * Returns true when `message` matches the relay-websocket-dead error class
85
+ * (socket closed, socket errored, or fail-fast "not connected" rejection).
86
+ */
87
+ function isRelayDisconnectMessage(message) {
88
+ return RELAY_DISCONNECT_MARKERS.some((marker) => message.includes(marker));
89
+ }
78
90
  function isObject(value) {
79
91
  return typeof value === "object" && value !== null;
80
92
  }
@@ -225,7 +237,11 @@ var ChiiCdpConnection = class {
225
237
  * EventEmitter is missed (defensive belt-and-suspenders).
226
238
  *
227
239
  * @param filterFn - Predicate that the returned targets must satisfy.
228
- * @param timeoutMs - Reject after this many ms (default 90 000).
240
+ * @param timeoutMs - Reject after this many ms (default 90 000). Pass a
241
+ * non-finite value (`Infinity`) to disable the rejection timer entirely —
242
+ * used by the test-runner's unbounded QR-attach wait (devtools#735). Node
243
+ * clamps `setTimeout(fn, Infinity)` to ~1ms, so a non-finite value MUST
244
+ * skip arming the timer rather than pass it through.
229
245
  * @param pollIntervalMs - Fallback poll interval (default 500ms).
230
246
  */
231
247
  waitForFirstTarget(filterFn, timeoutMs = 9e4, pollIntervalMs = 500) {
@@ -237,7 +253,7 @@ var ChiiCdpConnection = class {
237
253
  const settle = (targets) => {
238
254
  if (settled) return;
239
255
  settled = true;
240
- clearTimeout(timeoutHandle);
256
+ if (timeoutHandle !== null) clearTimeout(timeoutHandle);
241
257
  if (pollHandle !== null) {
242
258
  clearInterval(pollHandle);
243
259
  pollHandle = null;
@@ -248,7 +264,7 @@ var ChiiCdpConnection = class {
248
264
  const onAttach = (targets) => {
249
265
  if (filterFn(targets)) settle(targets);
250
266
  };
251
- const timeoutHandle = setTimeout(() => {
267
+ const timeoutHandle = Number.isFinite(timeoutMs) ? setTimeout(() => {
252
268
  if (settled) return;
253
269
  settled = true;
254
270
  if (pollHandle !== null) {
@@ -257,7 +273,7 @@ var ChiiCdpConnection = class {
257
273
  }
258
274
  this.emitter.off("target:attached", onAttach);
259
275
  reject(/* @__PURE__ */ new Error(`waitForFirstTarget: 타임아웃 (${timeoutMs}ms) — 폰이 relay에 attach되지 않았습니다.`));
260
- }, timeoutMs);
276
+ }, timeoutMs) : null;
261
277
  this.emitter.on("target:attached", onAttach);
262
278
  pollHandle = setInterval(() => {
263
279
  this.refreshTargets().then((targets) => {
@@ -557,368 +573,222 @@ var ChiiCdpConnection = class {
557
573
  }
558
574
  };
559
575
  //#endregion
560
- //#region src/mcp/chii-relay.ts
576
+ //#region src/test-runner/relay-worker.ts
561
577
  /**
562
- * Boots the local Chii relay server.
563
- *
564
- * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome
565
- * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.
566
- * The relay accepts a `target` websocket from the phone's injected `target.js`
567
- * and `client` websockets from CDP frontends (our MCP connection).
568
- *
569
- * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app
570
- * entries.
571
- *
572
- * TOTP auth (relay-side, authoritative gate):
573
- * When `verifyAuth` is provided, this module gates both inbound surfaces:
574
- *
575
- * - HTTP 'request': a listener registered BEFORE `chii.start({server})`.
576
- * Node's `http.Server` calls listeners in registration order; the first
577
- * to call `res.end()` wins. Invalid auth → 401 + CORS header + a tiny
578
- * JSON body (`{"error":"totp-rejected"}`) so a cross-origin script
579
- * `fetch()` probe can READ the status (issue #478). Valid auth → return
580
- * without side-effect (chii's Koa handler serves it).
578
+ * Sentinel string embedded in the error message by `injectAndRunBundle` when
579
+ * the per-file evaluate race hits the timeout. Used by the retry guard so only
580
+ * genuine timeouts get a second attempt not bundle errors or parse failures.
581
581
  *
582
- * - WS 'upgrade': after `chii.start()` has registered chii's own upgrade
583
- * listener, we take over the upgrade chain (remove chii's listeners,
584
- * re-dispatch manually). Invalid auth → accept-then-close: complete the
585
- * handshake via a `noServer` WebSocketServer, then immediately close
586
- * with code 4401 reason 'totp-rejected' (issue #478). A raw 401 +
587
- * `socket.destroy()` only ever surfaced as close code 1006 in the
588
- * browser — indistinguishable from a tunnel failure, which left the
589
- * env-2 phone UI silent. The explicit dispatch (not listener ordering)
590
- * is what keeps chii away from rejected sockets: accept-then-close
591
- * leaves the socket alive, so an order-based early-return would let
592
- * chii's later listener complete a SECOND handshake on the same socket
593
- * — an auth bypass. Valid auth → forward to chii's captured listeners.
594
- *
595
- * TOTP code transports (issue #466) — two equivalent ways to carry the code:
596
- * 1. Query param `at=<code>` — used by the daemon-side `/client` connection
597
- * (`chii-connection.ts` appends it; it holds the secret).
598
- * 2. Path prefix `/at/<code>/…` — used by the phone-side target. Chii's
599
- * stock `target.js` derives its WS endpoint from the script `src`
600
- * (`scriptEl.src.replace('target.js','')`), so the only way for the
601
- * phone to carry a code is to embed it in the script URL path. The
602
- * in-app attach injects `https://<host>/at/<code>/target.js`; both the
603
- * script fetch and the derived `wss://<host>/at/<code>/target/<id>` WS
604
- * dial then carry the prefix. The listeners below rewrite the prefix
605
- * into the query form (`rewriteAtPathPrefix`) and MUTATE `req.url`
606
- * before chii's own handlers (registered later) parse it — chii only
607
- * ever sees the stripped URL.
608
- *
609
- * Threat model: "URL leak" — someone obtains the tunnel URL (Slack paste, QR
610
- * screenshot, shoulder-surfing) but does not have the shared TOTP secret.
611
- * Rotating 6-digit code makes the URL stale after 30 s.
612
- * A determined attacker who extracts the secret from the dogfood bundle can
613
- * still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).
614
- *
615
- * SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear
616
- * in any log, error message, or process output. `verifyAuth` is a black-box
617
- * predicate from the caller's perspective; this module only forwards pass/fail.
582
+ * Exported for unit tests that assert the retry path is taken.
618
583
  */
619
- const require = createRequire(import.meta.url);
584
+ const EVALUATE_TIMEOUT_MARKER = "rpc: evaluate timed out after";
620
585
  /**
621
- * WS keepalive ping interval (ms).
586
+ * Runs all `files` sequentially over the given CDP `connection`.
622
587
  *
623
- * Cloudflare proxied connections are dropped after ~100 s of no traffic.
624
- * 45 s comfortably fits inside that window and lets both the phone-target leg
625
- * and the daemon-client leg survive idle CDP sessions.
626
- */
627
- const DEFAULT_KEEPALIVE_INTERVAL_MS = 45e3;
628
- /**
629
- * Loads chii's internal WebSocketServer class and returns it together with a
630
- * flag indicating whether the real class was found.
588
+ * For each file:
589
+ * 1. Bundle with esbuild (includes SDK shim + runtime).
590
+ * 2. Inject into the attached page via `Runtime.evaluate`.
591
+ * 3. Await the `RunReport` JSON response.
592
+ * 4. Accumulate results.
631
593
  *
632
- * Returns `null` if the internal path is not resolvable (future chii release
633
- * changes the layout) — callers skip keepalive gracefully.
634
- */
635
- function tryLoadChiiWssClass() {
636
- try {
637
- const mod = require("chii/server/lib/WebSocketServer");
638
- if (typeof mod === "function") return mod;
639
- } catch {}
640
- return null;
641
- }
642
- /**
643
- * Calls `chii.start()` and returns the chii `WebSocketServer` instance that
644
- * was constructed during the call.
594
+ * Returns a `RelayRunReport` with per-file results and flattened totals.
645
595
  *
646
- * How: `chii/server/index.js`'s `start()` creates `new WebSocketServer()`
647
- * where `WebSocketServer` is captured from `require('./lib/WebSocketServer')`
648
- * at module load time. The class reference is stable, so we can temporarily
649
- * patch `ChiiWssClass.prototype.start` — which runs *on the instance* —
650
- * to record `this` before the original `start` runs.
596
+ * This function does NOT open or manage the relay connection — the caller
597
+ * is responsible for attaching and closing it.
651
598
  *
652
- * The patch is installed before `chii.start()` and removed (via `finally`)
653
- * immediately after, so concurrent `startChiiRelay` calls nest correctly: each
654
- * call's patch overrides the previous in the prototype chain for the duration
655
- * of its own `chii.start()` call, restoring the prior descriptor on exit.
599
+ * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
600
+ * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
656
601
  *
657
- * If `ChiiWssClass` is null (internal path changed in a future chii release),
658
- * `chii.start()` runs unpatched and the function returns null callers skip
659
- * keepalive gracefully without affecting relay correctness.
602
+ * @param connection - Active CDP connection (relay or local kind).
603
+ * @param files - Absolute paths to test files, run in order.
604
+ * @param opts - Optional per-run overrides.
660
605
  */
661
- async function startChiiWithCapture(chii, startOptions, ChiiWssClass) {
662
- if (ChiiWssClass === null) {
663
- await chii.start(startOptions);
664
- return null;
665
- }
666
- let captured = null;
667
- const proto = ChiiWssClass.prototype;
668
- const originalStart = proto.start;
669
- proto.start = function(server) {
670
- captured = this;
671
- return originalStart.call(this, server);
672
- };
606
+ async function runTestFilesOverRelay(connection, files, opts) {
607
+ const wallStart = Date.now();
608
+ const startedAt = new Date(wallStart).toISOString();
609
+ const fileResults = [];
610
+ let domainsEnabled = false;
673
611
  try {
674
- await chii.start(startOptions);
675
- } finally {
676
- proto.start = originalStart;
612
+ await connection.enableDomains();
613
+ domainsEnabled = true;
614
+ } catch (e) {
615
+ process.stderr.write(`relay-worker: enableDomains() failed before run — console capture may be empty (${e instanceof Error ? e.message : String(e)})\n`);
677
616
  }
678
- return captured;
679
- }
680
- function loadChiiServer() {
681
- const mod = require("chii");
682
- if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
683
- throw new Error("chii server module did not expose start()");
684
- }
685
- /**
686
- * Rewrites a `/at/<code>/…` path-prefixed request URL into the equivalent
687
- * query-based form, e.g.:
688
- *
689
- * `/at/123456/target.js` → `/target.js?at=123456`
690
- * `/at/123456/target/x?url=u` → `/target/x?url=u&at=123456`
691
- * `/at/123456/` → `/?at=123456`
692
- *
693
- * Returns `null` when the URL does not carry the prefix (including an empty
694
- * code segment) callers fall back to the unmodified URL and the existing
695
- * query-based auth path.
696
- *
697
- * Pure string surgerythis function knows nothing about secrets or code
698
- * validity; verification stays inside the caller-provided `verifyAuth`
699
- * predicate (which parses the query). The raw path segment is appended
700
- * verbatim to the query: both path segments and query values are
701
- * percent-decoded exactly once by their consumers, so no re-encoding is
702
- * needed (TOTP codes are 6 digits and never percent-encoded in practice).
703
- */
704
- function rewriteAtPathPrefix(rawUrl) {
705
- const match = /^\/at\/([^/?]+)(\/[^?]*)?(\?.*)?$/.exec(rawUrl);
706
- if (match === null) return null;
707
- const code = match[1];
708
- const path = match[2] === void 0 || match[2] === "" ? "/" : match[2];
709
- const query = match[3] ?? "";
710
- return `${path}${query}${query === "" ? "?" : "&"}at=${code}`;
711
- }
712
- /**
713
- * Starts the Chii relay and resolves once listening.
714
- *
715
- * Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral
716
- * port on every start, so a stale cloudflared orphan holding any particular
717
- * port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`
718
- * always reflect the actual bound port.
719
- *
720
- * chii.start() is called with `server` (our pre-created httpServer) BEFORE
721
- * httpServer.listen(). This is intentional: chii attaches its Koa handler and
722
- * WS upgrade listener to the server object, but the actual TCP bind is
723
- * performed by our httpServer.listen() call below. The `port`/`domain` values
724
- * passed to chii.start() are used for display/banner purposes inside chii and
725
- * do not affect which port the server binds. The connection path (clients
726
- * connecting to `relay.baseUrl`) always uses the post-listen confirmed port.
727
- */
728
- async function startChiiRelay(options = {}) {
729
- const requestedPort = options.port ?? 0;
730
- const host = options.host ?? "127.0.0.1";
731
- const { verifyAuth, onAuthReject } = options;
732
- const keepaliveIntervalMs = options.keepaliveIntervalMs !== void 0 ? options.keepaliveIntervalMs : DEFAULT_KEEPALIVE_INTERVAL_MS;
733
- const httpServer = createServer();
734
- const notifyAuthReject = (kind) => {
735
- if (onAuthReject === void 0) return;
617
+ const collectCaptures = opts?.collectCaptures === true;
618
+ const liveConsole = [];
619
+ let unsubscribeConsole;
620
+ if (collectCaptures && domainsEnabled) unsubscribeConsole = connection.on("Runtime.consoleAPICalled", (event) => {
621
+ liveConsole.push(event);
622
+ });
623
+ let pendingReconnectCheck = false;
624
+ /**
625
+ * Attempts one `enableDomains()` reconnect. Logs the attempt and outcome to
626
+ * stderr. Never throws — a failed reconnect just means the caller proceeds
627
+ * as today (each remaining file / the retry will fail-fast on the still-dead
628
+ * socket).
629
+ *
630
+ * `context` selects the fixed log literal: 'next-file' for the between-files
631
+ * case (a file's FINAL result was a WS-dead-class error — the primary #731
632
+ * scenario), 'retry-precheck' for the defensive reconnect before retrying a
633
+ * timed-out file (cheap no-op via `enableDomains()`'s idempotency when the
634
+ * socket never actually died).
635
+ *
636
+ * SECRET-HANDLING: fixed literals only no relay wss URL, TOTP code, or
637
+ * tunnel host.
638
+ */
639
+ const attemptReconnect = async (context) => {
640
+ process.stderr.write(context === "next-file" ? "relay-worker: relay connection lost attempting reconnect before next file\n" : "relay-worker: evaluate timed out — attempting reconnect before retry\n");
736
641
  try {
737
- onAuthReject({ kind });
738
- } catch {}
642
+ await connection.enableDomains();
643
+ process.stderr.write("relay-worker: reconnect succeeded\n");
644
+ } catch (e) {
645
+ process.stderr.write(`relay-worker: reconnect failed (${e instanceof Error ? e.message : String(e)}) — continuing, remaining files may fail\n`);
646
+ }
739
647
  };
740
- if (verifyAuth) httpServer.on("request", (req, res) => {
741
- const rewritten = rewriteAtPathPrefix(req.url ?? "");
742
- if (rewritten !== null) {
743
- req.url = rewritten;
744
- if (!verifyAuth(req)) {
745
- res.statusCode = 401;
746
- res.setHeader("Access-Control-Allow-Origin", "*");
747
- res.setHeader("Content-Type", "application/json");
748
- res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
749
- notifyAuthReject("http-request");
648
+ try {
649
+ for (const file of files) {
650
+ if (pendingReconnectCheck) {
651
+ await attemptReconnect("next-file");
652
+ pendingReconnectCheck = false;
750
653
  }
751
- return;
752
- }
753
- const pathname = (req.url ?? "").split("?")[0];
754
- if (pathname === "/targets" || pathname === "/targets/") {
755
- if (!verifyAuth(req)) {
756
- res.statusCode = 401;
757
- res.setHeader("Access-Control-Allow-Origin", "*");
758
- res.setHeader("Content-Type", "application/json");
759
- res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
760
- notifyAuthReject("http-request");
761
- return;
654
+ let fileEntry;
655
+ try {
656
+ const { code } = await bundleTestFile(file, opts?.bundleOptions);
657
+ /**
658
+ * Runs one evaluate attempt and returns a FileResult, or `null` when the
659
+ * result is a genuine timeout and the caller should retry.
660
+ *
661
+ * We need to distinguish:
662
+ * - `rpcResult.ok = false` + timeout error retry candidate (`return null`)
663
+ * - `rpcResult.ok = false` + other error → final error, no retry
664
+ * - `injectAndRunBundle` throws → CDP exceptionDetails OR a
665
+ * relay-disconnect rejection from `connection.send()` (page engine
666
+ * threw, or the ws died mid-evaluate); treated as a final
667
+ * (non-retryable) error either way.
668
+ *
669
+ * The Promise.race timeout in rpc.ts RETURNS `{ok:false, error: '…'}` (it
670
+ * does NOT throw/reject). Only genuine CDP `exceptionDetails` (or a dead
671
+ * `connection.send()`) cause a throw. This distinction is what makes the
672
+ * EVALUATE_TIMEOUT_MARKER gate below reachable — the timeout result
673
+ * surfaces as `rpcResult.ok=false` with the marker string, not as a
674
+ * caught exception.
675
+ */
676
+ const attempt = async () => {
677
+ let rpcResult;
678
+ try {
679
+ rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
680
+ } catch (e) {
681
+ return {
682
+ file,
683
+ result: { error: e instanceof Error ? e.message : String(e) }
684
+ };
685
+ }
686
+ if (rpcResult.ok) return {
687
+ file,
688
+ result: rpcResult.report
689
+ };
690
+ if (rpcResult.error.includes("rpc: evaluate timed out after")) return null;
691
+ return {
692
+ file,
693
+ result: { error: rpcResult.error }
694
+ };
695
+ };
696
+ const firstResult = await attempt();
697
+ if (firstResult !== null) fileEntry = firstResult;
698
+ else {
699
+ await attemptReconnect("retry-precheck");
700
+ process.stderr.write(`relay-worker: evaluate timed out for ${file} — retrying once\n`);
701
+ const retryResult = await attempt();
702
+ if (retryResult !== null) fileEntry = retryResult;
703
+ else fileEntry = {
704
+ file,
705
+ result: { error: `${EVALUATE_TIMEOUT_MARKER} ${opts?.timeoutMs ?? 6e4}ms (after retry)` }
706
+ };
707
+ }
708
+ } catch (e) {
709
+ fileEntry = {
710
+ file,
711
+ result: { error: e instanceof Error ? e.message : String(e) }
712
+ };
762
713
  }
763
- return;
714
+ if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
715
+ fileResults.push(fileEntry);
764
716
  }
765
- });
766
- const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;
767
- const capturedChiiWss = await startChiiWithCapture(loadChiiServer(), {
768
- server: httpServer,
769
- domain: `${host}:${requestedPort}`,
770
- port: requestedPort
771
- }, chiiWssClass);
772
- if (verifyAuth) {
773
- const chiiUpgradeListeners = httpServer.listeners("upgrade");
774
- httpServer.removeAllListeners("upgrade");
775
- const rejectWss = new WebSocketServer({ noServer: true });
776
- httpServer.on("upgrade", (req, socket, head) => {
777
- const rewritten = rewriteAtPathPrefix(req.url ?? "");
778
- if (rewritten !== null) req.url = rewritten;
779
- if (!verifyAuth(req)) {
780
- rejectWss.handleUpgrade(req, socket, head, (ws) => {
781
- ws.close(RELAY_AUTH_REJECT_CLOSE_CODE, RELAY_AUTH_REJECT_REASON);
782
- });
783
- notifyAuthReject("ws-upgrade");
784
- return;
785
- }
786
- for (const listener of chiiUpgradeListeners) listener(req, socket, head);
787
- });
717
+ } finally {
718
+ unsubscribeConsole?.();
788
719
  }
789
- const actualPort = await new Promise((resolve, reject) => {
790
- httpServer.once("error", reject);
791
- httpServer.listen(requestedPort, host, () => {
792
- httpServer.off("error", reject);
793
- resolve(httpServer.address().port);
794
- });
720
+ const captures = collectCaptures ? parseCaptureLines(liveConsole.map((e) => ({ text: renderConsoleLineText(e) }))) : [];
721
+ const totals = fileResults.reduce((acc, { result }) => {
722
+ if ("error" in result) {
723
+ acc.failed += 1;
724
+ acc.total += 1;
725
+ } else {
726
+ acc.passed += result.passed;
727
+ acc.failed += result.failed;
728
+ acc.skipped += result.skipped;
729
+ acc.total += result.passed + result.failed + result.skipped;
730
+ }
731
+ return acc;
732
+ }, {
733
+ passed: 0,
734
+ failed: 0,
735
+ skipped: 0,
736
+ total: 0
795
737
  });
796
- let keepaliveHandle = null;
797
- if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {
798
- const chiiWss = capturedChiiWss;
799
- keepaliveHandle = setInterval(() => {
800
- for (const client of chiiWss._wss.clients) if (client.readyState === 1) client.ping();
801
- }, keepaliveIntervalMs);
802
- }
803
738
  return {
804
- port: actualPort,
805
- baseUrl: `http://${host}:${actualPort}`,
806
- close: () => new Promise((resolve) => {
807
- if (keepaliveHandle !== null) {
808
- clearInterval(keepaliveHandle);
809
- keepaliveHandle = null;
810
- }
811
- httpServer.close(() => resolve());
812
- })
739
+ startedAt,
740
+ duration: Date.now() - wallStart,
741
+ files: fileResults,
742
+ totals,
743
+ captures
813
744
  };
814
745
  }
815
- //#endregion
816
- //#region src/mcp/debug-server.ts
817
746
  /**
818
- * Factory that constructs a `ChiiCdpConnection` for the given relay base URL.
819
- *
820
- * Introduced as a named seam so PR-2 (dual-connection, #348) can defer
821
- * construction to first-activation time by moving or replacing this call. Since
822
- * #396 every family (relay included) is constructed lazily on its first
823
- * `start_debug`, so this is always called from the lazy boot path.
824
- *
825
- * The relay base URL is only available after `startChiiRelay()` resolves, so
826
- * the factory is called right after that point (same as before this refactor).
747
+ * Renders one `Runtime.consoleAPICalled` event to a single line of text, the
748
+ * same way `tools.ts#normalizeConsoleMessage` does (args rendered + space-
749
+ * joined). Inlined here (≈8 lines) so this module avoids importing `tools.ts`,
750
+ * which would drag the heavy MCP/Node graph (server-lock, parent-watcher, …)
751
+ * onto the test-runner entry.
752
+ *
753
+ * SECRET-HANDLING: this only stringifies console args; the caller's
754
+ * allowlist-prefix parser then discards everything that is not a genuine
755
+ * `__AIT_CAPTURE__` line.
827
756
  */
828
- function createRelayConnection(relayBaseUrl) {
829
- return new ChiiCdpConnection({
830
- relayBaseUrl,
831
- totpSecret: process.env.AIT_DEBUG_TOTP_SECRET
832
- });
757
+ function renderConsoleLineText(event) {
758
+ return event.args.map((arg) => {
759
+ if (arg.value !== void 0) {
760
+ if (typeof arg.value === "string") return arg.value;
761
+ try {
762
+ return JSON.stringify(arg.value);
763
+ } catch {
764
+ return String(arg.value);
765
+ }
766
+ }
767
+ if (arg.description !== void 0) return arg.description;
768
+ if (arg.className !== void 0) return arg.className;
769
+ return arg.subtype ?? arg.type;
770
+ }).join(" ");
833
771
  }
834
772
  /**
835
- * Boots the relay family (issues #348, #356): starts the Chii relay on an
836
- * OS-assigned port (with optional TOTP gate), opens a cloudflared quick tunnel
837
- * to the relay's confirmed port in the background, prints the attach banner,
838
- * and arms the tunnel health probe. Returns a {@link BootedFamily} whose
839
- * `getTunnelStatus()` reflects the live tunnel (it flips up once the background
840
- * tunnel resolves and follows reissues).
841
- *
842
- * Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback
843
- * (symmetry with {@link bootLocalFamily}), at most once on the first
844
- * `start_debug({ mode: 'relay-staging' })` (all-lazy, #396 — every relay boot now
845
- * flows through `switchMode` after the project-local secret load). `relay-live`
846
- * removed (#665).
847
- *
848
- * The relay base URL is only known after `startChiiRelay()` resolves, so the
849
- * `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
850
- * this function, after the relay port is confirmed.
851
- *
852
- * SECRET-HANDLING: the TOTP secret rides only inside `verifyAuth`; the wssUrl
853
- * (relay host) is never logged here directly.
773
+ * Flattens all test results from a `RelayRunReport` into a single array.
774
+ * Files that errored during bundle/inject produce a synthetic failed entry.
854
775
  */
855
- async function bootRelayFamily(options = {}) {
856
- assertRelayAuthConfigured();
857
- const relayPort = options.relayPort ?? 0;
858
- const totpEnabled = options.verifyAuth !== void 0;
859
- const relay = await startChiiRelay({
860
- port: relayPort,
861
- verifyAuth: options.verifyAuth,
862
- onAuthReject: options.onAuthReject
776
+ function flattenResults(report) {
777
+ const out = [];
778
+ for (const { file, result } of report.files) if ("error" in result) out.push({
779
+ file,
780
+ name: `<bundle/inject error>`,
781
+ status: "fail",
782
+ duration: 0,
783
+ error: result.error
863
784
  });
864
- logInfo("server.start", {
865
- port: relay.port,
866
- totpEnabled
785
+ else for (const t of result.tests) out.push({
786
+ ...t,
787
+ file
867
788
  });
868
- let tunnel = null;
869
- let tunnelStatus = makeTunnelStatus(false, null);
870
- let tunnelProbe = null;
871
- generateAttachToken();
872
- startQuickTunnel(relay.port).then((t) => {
873
- tunnel = t;
874
- tunnelStatus = makeTunnelStatus(true, t.wssUrl);
875
- options.onWssUrl?.(t.wssUrl);
876
- if (t.childPid !== void 0) options.onTunnelChildPid?.(t.childPid);
877
- logInfo("tunnel.up", { totpEnabled });
878
- tunnelProbe = startTunnelHealthProbe(t, relay.port, {
879
- onReissue: (newTunnel) => {
880
- tunnel = newTunnel;
881
- tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);
882
- options.onWssUrl?.(newTunnel.wssUrl);
883
- if (newTunnel.childPid !== void 0) options.onTunnelChildPid?.(newTunnel.childPid);
884
- printAttachBanner({
885
- wssUrl: newTunnel.wssUrl,
886
- totpEnabled
887
- }).then(() => {
888
- logInfo("tunnel.up", {
889
- totpEnabled,
890
- reissued: true
891
- });
892
- });
893
- },
894
- onPermanentDrop: (droppedAt) => {
895
- tunnelStatus = makeTunnelStatus(false, null, droppedAt, 3);
896
- logError("tunnel.down", { msg: `tunnel permanently dropped (${droppedAt}). Restart: npx @ait-co/devtools devtools-mcp` });
897
- options.onTunnelDown?.();
898
- }
899
- });
900
- return printAttachBanner({
901
- wssUrl: t.wssUrl,
902
- totpEnabled
903
- });
904
- }, (err) => {
905
- logError("tunnel.down", { msg: `Failed to open cloudflared quick tunnel: ${err instanceof Error ? err.message : String(err)}. The relay is up locally; attach over the public URL is unavailable until the tunnel starts.` });
906
- });
907
- const connection = createRelayConnection(relay.baseUrl);
908
- return {
909
- connection,
910
- relayOrigin: "intoss-webview",
911
- relayHttpUrl: relay.baseUrl,
912
- getTunnelStatus: () => tunnelStatus,
913
- stop() {
914
- tunnelProbe?.stop();
915
- tunnel?.stop();
916
- connection.close();
917
- relay.close();
918
- }
919
- };
789
+ return out;
920
790
  }
921
791
  //#endregion
922
- export { bootRelayFamily, buildRelayVerifyAuth };
792
+ export { RELAY_AUTH_REJECT_CLOSE_CODE as a, ChiiCdpConnection as i, flattenResults as n, RELAY_AUTH_REJECT_REASON as o, runTestFilesOverRelay as r, EVALUATE_TIMEOUT_MARKER as t };
923
793
 
924
- //# sourceMappingURL=debug-server-BzzNkETg.js.map
794
+ //# sourceMappingURL=relay-worker-CCAHLC4a.js.map