@ait-co/devtools 0.1.137 → 0.1.138

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 (119) hide show
  1. package/dist/{attach-orchestrator-Cjdhir2U.js → attach-orchestrator-6kz_ZdWL.js} +5 -3
  2. package/dist/{attach-orchestrator-Cjdhir2U.js.map → attach-orchestrator-6kz_ZdWL.js.map} +1 -1
  3. package/dist/{attach-orchestrator-CE0S09YU.js → attach-orchestrator-BEiz3wAx.js} +5 -3
  4. package/dist/{attach-orchestrator-CMoDuG2A.js.map → attach-orchestrator-BEiz3wAx.js.map} +1 -1
  5. package/dist/{attach-orchestrator-CMoDuG2A.js → attach-orchestrator-BG8TGJOZ.js} +5 -3
  6. package/dist/{attach-orchestrator-CE0S09YU.js.map → attach-orchestrator-BG8TGJOZ.js.map} +1 -1
  7. package/dist/{bundle-BKqyhEK9.d.ts → bundle-C796JIwG.d.ts} +65 -2
  8. package/dist/bundle-C796JIwG.d.ts.map +1 -0
  9. package/dist/{capture-ltuV0gZa.d.ts → capture-DsP525OZ.d.ts} +1 -1
  10. package/dist/{capture-ltuV0gZa.d.ts.map → capture-DsP525OZ.d.ts.map} +1 -1
  11. package/dist/{cdp-connection-BYE9meXe.d.ts → cdp-connection-rP1WdnH5.d.ts} +1 -1
  12. package/dist/{cdp-connection-BYE9meXe.d.ts.map → cdp-connection-rP1WdnH5.d.ts.map} +1 -1
  13. package/dist/{cell-D1y4shoV.js → cell-BMlFh08P.js} +2 -2
  14. package/dist/cell-BMlFh08P.js.map +1 -0
  15. package/dist/{cell-DHA578lX.js → cell-Dtgtusog.js} +90 -16
  16. package/dist/cell-Dtgtusog.js.map +1 -0
  17. package/dist/{cell-BIdSQb9T.js → cell-KsRXFdDs.js} +122 -16
  18. package/dist/cell-KsRXFdDs.js.map +1 -0
  19. package/dist/{debug-server-CBK7dn8O.js → debug-server-6c22qSWa.js} +132 -9
  20. package/dist/debug-server-6c22qSWa.js.map +1 -0
  21. package/dist/{debug-server-BQplCzf8.js → debug-server-BQHbaKPT.js} +134 -11
  22. package/dist/debug-server-BQHbaKPT.js.map +1 -0
  23. package/dist/{debug-server-D2wpPK3_.js → debug-server-DWat7d-9.js} +82 -6
  24. package/dist/debug-server-DWat7d-9.js.map +1 -0
  25. package/dist/devtools-opener-3Drge_RJ.js +75 -0
  26. package/dist/devtools-opener-3Drge_RJ.js.map +1 -0
  27. package/dist/devtools-opener-CJpEsXXQ.js +76 -0
  28. package/dist/devtools-opener-CJpEsXXQ.js.map +1 -0
  29. package/dist/devtools-opener-CxtryS8c.js +75 -0
  30. package/dist/devtools-opener-CxtryS8c.js.map +1 -0
  31. package/dist/in-app/auto.js +59 -12
  32. package/dist/in-app/auto.js.map +1 -1
  33. package/dist/in-app/index.d.ts +21 -2
  34. package/dist/in-app/index.d.ts.map +1 -1
  35. package/dist/in-app/index.js +60 -13
  36. package/dist/in-app/index.js.map +1 -1
  37. package/dist/mcp/cli.js +299 -60
  38. package/dist/mcp/cli.js.map +1 -1
  39. package/dist/mcp/server.js +1 -1
  40. package/dist/mock/index.d.ts +64 -1
  41. package/dist/mock/index.d.ts.map +1 -1
  42. package/dist/mock/index.js +221 -14
  43. package/dist/mock/index.js.map +1 -1
  44. package/dist/panel/index.js +178 -23
  45. package/dist/panel/index.js.map +1 -1
  46. package/dist/{pool-CmfvXnwh.d.ts → pool-CrP5CPvU.d.ts} +3 -3
  47. package/dist/{pool-CmfvXnwh.d.ts.map → pool-CrP5CPvU.d.ts.map} +1 -1
  48. package/dist/{qr-http-server-ZkT6F7is.js → qr-http-server-D1hpoyBF.js} +1 -1
  49. package/dist/{qr-http-server-ZkT6F7is.js.map → qr-http-server-D1hpoyBF.js.map} +1 -1
  50. package/dist/qr-http-server-D4rGz8M7.js +1640 -0
  51. package/dist/qr-http-server-D4rGz8M7.js.map +1 -0
  52. package/dist/{qr-http-server-C7q8Gn04.js → qr-http-server-DIjCV7h_.js} +1 -1
  53. package/dist/{qr-http-server-C7q8Gn04.js.map → qr-http-server-DIjCV7h_.js.map} +1 -1
  54. package/dist/{relay-factory-DVzpBRxz.js → relay-factory-C1X3G3WY.js} +71 -15
  55. package/dist/relay-factory-C1X3G3WY.js.map +1 -0
  56. package/dist/{relay-secret-store-CLEGyHou.js → relay-secret-store-Bmyleu0A.js} +1 -1
  57. package/dist/{relay-secret-store-CLEGyHou.js.map → relay-secret-store-Bmyleu0A.js.map} +1 -1
  58. package/dist/{relay-secret-store-DhzAnnj-.js → relay-secret-store-CQenfcSL.js} +2 -2
  59. package/dist/{relay-secret-store-DhzAnnj-.js.map → relay-secret-store-CQenfcSL.js.map} +1 -1
  60. package/dist/{relay-secret-store-BcVrWwTq.js → relay-secret-store-DKxs7zwq.js} +1 -1
  61. package/dist/{relay-secret-store-BcVrWwTq.js.map → relay-secret-store-DKxs7zwq.js.map} +1 -1
  62. package/dist/{relay-secret-store-DGduVJhs.js → relay-secret-store-WJ8EGkIl.js} +1 -1
  63. package/dist/{relay-secret-store-DGduVJhs.js.map → relay-secret-store-WJ8EGkIl.js.map} +1 -1
  64. package/dist/{relay-url-store-CKW8RQzf.js → relay-url-store-BR2XodiO.js} +2 -2
  65. package/dist/{relay-url-store-CKW8RQzf.js.map → relay-url-store-BR2XodiO.js.map} +1 -1
  66. package/dist/{relay-url-store-CdA58fgw.js → relay-url-store-CH63fVCm.js} +2 -2
  67. package/dist/{relay-url-store-B0X8TsGr.js.map → relay-url-store-CH63fVCm.js.map} +1 -1
  68. package/dist/{relay-url-store-CwKT7i04.js → relay-url-store-DaY1QPes.js} +2 -2
  69. package/dist/{relay-url-store-CwKT7i04.js.map → relay-url-store-DaY1QPes.js.map} +1 -1
  70. package/dist/{relay-url-store-B0X8TsGr.js → relay-url-store-xmUuTjXA.js} +2 -2
  71. package/dist/{relay-url-store-CdA58fgw.js.map → relay-url-store-xmUuTjXA.js.map} +1 -1
  72. package/dist/{relay-worker-6fy1eaIo.js → relay-worker-BUXI0K0b.js} +8 -4
  73. package/dist/{relay-worker-6fy1eaIo.js.map → relay-worker-BUXI0K0b.js.map} +1 -1
  74. package/dist/{relay-worker-DJnZkXza.d.ts → relay-worker-DcyboK43.d.ts} +30 -5
  75. package/dist/relay-worker-DcyboK43.d.ts.map +1 -0
  76. package/dist/{runtime-DfHHZms2.d.ts → runtime-BiigOuvb.d.ts} +30 -3
  77. package/dist/runtime-BiigOuvb.d.ts.map +1 -0
  78. package/dist/test-runner/bin.js +376 -38
  79. package/dist/test-runner/bin.js.map +1 -1
  80. package/dist/test-runner/bundle.d.ts +2 -2
  81. package/dist/test-runner/bundle.js +66 -13
  82. package/dist/test-runner/bundle.js.map +1 -1
  83. package/dist/test-runner/capture.d.ts +1 -1
  84. package/dist/test-runner/config.d.ts +58 -5
  85. package/dist/test-runner/config.d.ts.map +1 -1
  86. package/dist/test-runner/config.js +1 -1
  87. package/dist/test-runner/method-pace.d.ts +82 -0
  88. package/dist/test-runner/method-pace.d.ts.map +1 -0
  89. package/dist/test-runner/method-pace.js +120 -0
  90. package/dist/test-runner/method-pace.js.map +1 -0
  91. package/dist/test-runner/pool.d.ts +1 -1
  92. package/dist/test-runner/pool.js +1 -1
  93. package/dist/test-runner/relay-factory.d.ts +57 -4
  94. package/dist/test-runner/relay-factory.d.ts.map +1 -1
  95. package/dist/test-runner/relay-factory.js +70 -14
  96. package/dist/test-runner/relay-factory.js.map +1 -1
  97. package/dist/test-runner/relay-worker.d.ts +1 -1
  98. package/dist/test-runner/relay-worker.js +1 -1
  99. package/dist/test-runner/report.d.ts +3 -3
  100. package/dist/test-runner/rpc.d.ts +2 -2
  101. package/dist/test-runner/runtime.d.ts +2 -2
  102. package/dist/test-runner/runtime.js +83 -6
  103. package/dist/test-runner/runtime.js.map +1 -1
  104. package/dist/test-runner/task-graph.d.ts +1 -1
  105. package/dist/throttle-DKKzX1qC.js +59 -0
  106. package/dist/throttle-DKKzX1qC.js.map +1 -0
  107. package/dist/totp-Dwft0Kz7.js +3 -0
  108. package/package.json +1 -1
  109. package/dist/bundle-BKqyhEK9.d.ts.map +0 -1
  110. package/dist/cell-BIdSQb9T.js.map +0 -1
  111. package/dist/cell-D1y4shoV.js.map +0 -1
  112. package/dist/cell-DHA578lX.js.map +0 -1
  113. package/dist/debug-server-BQplCzf8.js.map +0 -1
  114. package/dist/debug-server-CBK7dn8O.js.map +0 -1
  115. package/dist/debug-server-D2wpPK3_.js.map +0 -1
  116. package/dist/relay-factory-DVzpBRxz.js.map +0 -1
  117. package/dist/relay-worker-DJnZkXza.d.ts.map +0 -1
  118. package/dist/runtime-DfHHZms2.d.ts.map +0 -1
  119. package/dist/totp-D1pulXLa.js +0 -3
package/dist/mcp/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-WY6l0ysP.js";
3
- import { t as loadRelaySecretReadOnly } from "../relay-secret-store-DhzAnnj-.js";
3
+ import { t as loadRelaySecretReadOnly } from "../relay-secret-store-CQenfcSL.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { accessSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { argv } from "node:process";
@@ -25,33 +25,44 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
25
25
  //#endregion
26
26
  //#region src/in-app/gate.ts
27
27
  /**
28
- * The host suffix the Toss app uses to serve dogfood / private mini-apps.
29
- *
30
- * A `intoss-private://` (dogfood) entry maps to a host such as
31
- * `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`
32
- * entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment
33
- * is absent. Confirmed live over CDP for mini-app 31146; the exact production
34
- * host is to be re-confirmed once 31146 passes review (spec open question 2).
35
- */
36
- const PRIVATE_APPS_HOST_SUFFIX = ".private-apps.tossmini.com";
37
- /**
38
28
  * The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.
39
29
  * See {@link isTrycloudflareHost} for why this host kind bypasses Layer B1.
40
30
  */
41
31
  const TRYCLOUDFLARE_HOST_SUFFIX = ".trycloudflare.com";
42
32
  /**
43
- * Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain
44
- * the host the Toss app reserves for dogfood / private mini-app entries.
33
+ * The parent host suffix for the whole Toss mini-app serving family.
45
34
  *
46
- * The match is an exact suffix check, not a substring `.includes()`: a
47
- * substring test would also accept an attacker-controlled host like
48
- * `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in
49
- * `.tossmini.com`. Requiring the string to END with the suffix closes that.
50
- * The leading `.` in the suffix also forces a real subdomain label, so a
51
- * bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.
35
+ * The 3.0 runtime loader serves mini-app pages from tossmini.com hosts that
36
+ * are NOT `*.private-apps.tossmini.com` (observed live 2026-07-08 on mini-app
37
+ * 31146 with a 3.0-beta bundle: a 4-label host ending in `.tossmini.com`
38
+ * whose middle label is not `private-apps`, with `_deploymentId` consumed by
39
+ * the native loader and not propagated to the page URL devtools#760).
40
+ *
41
+ * Under 3.0 the hostname therefore no longer distinguishes a dogfood
42
+ * candidate from a production entry, so for these hosts Layer B is demoted
43
+ * from a stage discriminator (#665) to a "Toss-owned host family" filter,
44
+ * and the effective boundary moves to Layer C: explicit `debug=1`, a valid
45
+ * `wss:` relay, and a MANDATORY `at=` TOTP code (see Layer C3 in
46
+ * {@link evaluateDebugGate}). A production user's entry URL carries none of
47
+ * those params, so an accidentally-shipped debug build stays dormant exactly
48
+ * as #665 intended; what changes is that a deliberate operator holding the
49
+ * TOTP secret can now attach on a 3.0-family host.
50
+ *
51
+ * The match is the same exact-suffix `endsWith` check as
52
+ * {@link isPrivateAppsHost} — never a substring `.includes()`, which would
53
+ * accept an attacker-controlled `x.tossmini.com.evil.example`. The leading
54
+ * `.` forces at least one subdomain label, so a bare `tossmini.com` does not
55
+ * match.
56
+ */
57
+ const TOSSMINI_HOST_SUFFIX = ".tossmini.com";
58
+ /**
59
+ * Returns whether `hostname` is any `*.tossmini.com` subdomain — the host
60
+ * family the Toss app serves mini-app pages from. Includes the 2.x
61
+ * `*.private-apps.tossmini.com` dogfood hosts and the 3.0 unified serving
62
+ * hosts (devtools#760).
52
63
  */
53
- function isPrivateAppsHost(hostname) {
54
- return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);
64
+ function isTossminiHost(hostname) {
65
+ return hostname.endsWith(TOSSMINI_HOST_SUFFIX);
55
66
  }
56
67
  /**
57
68
  * The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.
@@ -98,18 +109,27 @@ function isLocalhostHost(hostname) {
98
109
  * known debug-allowed host. The debug surface is ONLY active on:
99
110
  * - localhost / loopback (env 1 desktop dev)
100
111
  * - *.trycloudflare.com (env 2 PWA tunnel)
101
- * - *.private-apps.tossmini.com (env 3 dog-food)
102
- *
103
- * Any other host (including apps.tossmini.com — the former env 4 LIVE host)
104
- * is silently blocked. This is a positive allowlist — unlisted hosts never
105
- * had debug surface regardless, but this function makes it explicit and
106
- * auditable in a single place.
112
+ * - *.tossmini.com (env 3 dog-food — 2.x private-apps hosts AND the 3.0
113
+ * unified serving family, devtools#760)
114
+ *
115
+ * Any other host is silently blocked. This is a positive allowlist —
116
+ * unlisted hosts never had debug surface regardless, but this function makes
117
+ * it explicit and auditable in a single place.
118
+ *
119
+ * #760 note on the #665 boundary: the former env 4 LIVE host family
120
+ * (`*.apps.tossmini.com`) now passes this coarse filter because the 3.0
121
+ * loader serves dogfood candidates and production entries from the same
122
+ * host family — the hostname alone can no longer separate them. The #665
123
+ * invariant ("no naked attach on a production-family host") is preserved
124
+ * one layer down: on tossmini hosts that are not `*.private-apps.*`, Layer
125
+ * C3 makes the TOTP `at=` code MANDATORY, and production entry URLs carry
126
+ * no debug/relay/at params at all.
107
127
  *
108
128
  * SECRET-HANDLING: the hostname value MUST NOT be logged or included in any
109
129
  * error reason string — only benign labels ('host not in allowlist') are safe.
110
130
  */
111
131
  function isDebugAllowedHost(hostname) {
112
- return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) || isPrivateAppsHost(hostname);
132
+ return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) || isTossminiHost(hostname);
113
133
  }
114
134
  //#endregion
115
135
  //#region src/shared/parent-watcher.ts
@@ -211,6 +231,39 @@ function startMaxAgeWatchdog(onExpired, opts = {}) {
211
231
  } };
212
232
  }
213
233
  //#endregion
234
+ //#region src/test-runner/throttle.ts
235
+ /**
236
+ * Shared `APP_BRIDGE_THROTTLED` detection (devtools#767).
237
+ *
238
+ * 2026-07-10 real-device observation: the Toss app native side added a
239
+ * per-method rate limit to the 2.x bridge — calling the SAME bridge method
240
+ * twice within a short window rejects the second call immediately with a
241
+ * 2.x native envelope (`{name, code, userInfo, moduleName, __isError}`) whose
242
+ * `code` is `'APP_BRIDGE_THROTTLED'` and whose `message` reads
243
+ * `"Too many app bridge calls from <method>."`. The 3.x bridge path is
244
+ * unaffected (devtools#767 issue body, sdk-example#284 2026-07-10 comment).
245
+ *
246
+ * This module is the SINGLE source of the detection predicate. Today's only
247
+ * consumer is `runtime.ts` (in-page, per-test retry loop) — a plain static
248
+ * import; esbuild inlines this file into the same IIFE bundle (same
249
+ * mechanism as `bridge-stub.ts`, see `bundle.ts#getPageSideModulePath`). It
250
+ * is exported from this standalone, dependency-free module (rather than
251
+ * defined inline in `runtime.ts`) so a Node-side consumer could import the
252
+ * exact same predicate in future if whole-FILE throttle-aware retry is ever
253
+ * added on top of the per-test retry `runtime.ts` already performs — no such
254
+ * Node-side caller exists yet (whole-file retry today only triggers on an
255
+ * `injectAndRunBundle`-level timeout/exception, a distinct failure mode from
256
+ * a `RunReport` that completed normally but contains a THROTTLED-failed
257
+ * test — see `relay-worker.ts`'s `EVALUATE_TIMEOUT_MARKER` gate).
258
+ *
259
+ * Browser-compatible, dependency-free (no Node APIs) — safe to import from
260
+ * either side of the CDP boundary, mirroring `runtime.ts`'s own constraint.
261
+ */
262
+ /** The native error code the Toss app bridge returns when rate-limited. */
263
+ const THROTTLED_ERROR_CODE = "APP_BRIDGE_THROTTLED";
264
+ /** The message substring the native envelope carries when rate-limited. */
265
+ const THROTTLED_MESSAGE_SUBSTRING = "Too many app bridge calls";
266
+ //#endregion
214
267
  //#region src/mcp/deeplink.ts
215
268
  /**
216
269
  * URL of the AITC Sandbox launcher PWA.
@@ -2024,7 +2077,7 @@ async function readMcpSdkVersion() {
2024
2077
  * some test environments that skip the build step).
2025
2078
  */
2026
2079
  function readDevtoolsVersion() {
2027
- return "0.1.137";
2080
+ return "0.1.138";
2028
2081
  }
2029
2082
  /**
2030
2083
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -2617,7 +2670,7 @@ async function prepareAttach(deps, env, args, conn) {
2617
2670
  const buildProjectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
2618
2671
  let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
2619
2672
  if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
2620
- const { readRelayUrls } = await import("../relay-url-store-CwKT7i04.js");
2673
+ const { readRelayUrls } = await import("../relay-url-store-DaY1QPes.js");
2621
2674
  tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
2622
2675
  }
2623
2676
  if (tunnelHttpUrl === "") return {
@@ -2694,10 +2747,12 @@ async function prepareAttach(deps, env, args, conn) {
2694
2747
  tool: "start_attach",
2695
2748
  msg: "no _deploymentId in scheme_url; matching on presence only"
2696
2749
  });
2750
+ const wssForMatch = tunnelForBuild.wssUrl;
2751
+ const matchesRelay = (url) => url.includes(wssForMatch) || url.includes(encodeURIComponent(wssForMatch));
2697
2752
  const isMatchingPage = (pages) => {
2698
2753
  if (pages.length === 0) return false;
2699
2754
  if (deploymentId === null) return true;
2700
- return pages.some((p) => p.url.includes(deploymentId));
2755
+ return pages.some((p) => p.url.includes(deploymentId) || matchesRelay(p.url));
2701
2756
  };
2702
2757
  const buildTimeoutError = (baseText, timeoutSec, observed) => {
2703
2758
  const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
@@ -2956,8 +3011,43 @@ const PERMISSION_PROBE_MAP = {
2956
3011
  contacts: "fetchContacts",
2957
3012
  location: "getCurrentLocation"
2958
3013
  };
2959
- /** Non-fatal bound for the permission preflight `Runtime.evaluate` round-trip. */
2960
- const PERMISSION_PREFLIGHT_TIMEOUT_MS = 1e4;
3014
+ /**
3015
+ * Inter-probe spacing (ms) applied BEFORE every probe after the first
3016
+ * (devtools#767). The native per-method bridge rate limit observed
3017
+ * 2026-07-10 rejects a SECOND call to the same method within a short window
3018
+ * with `APP_BRIDGE_THROTTLED` — the permission preflight's un-paced burst
3019
+ * (6 probes fired back-to-back) was the FIRST trigger of that limiter in the
3020
+ * wild (only the first probe passed; the remaining 5 came back throttled →
3021
+ * `unavailable`, cascading into skipped `__AIT_PERMS__`-branching tests).
3022
+ * Sequential-with-spacing is cheap insurance even though each probe targets a
3023
+ * DIFFERENT method — the limiter's exact scope (per-method vs. a shared
3024
+ * bridge-wide window) was not reverse-engineered (explicitly out of scope,
3025
+ * devtools#767) so spacing every probe is the conservative choice.
3026
+ */
3027
+ const PROBE_INTER_CALL_DELAY_MS = 250;
3028
+ /**
3029
+ * Backoff delays (ms) between retry attempts when a probe is throttled. Its
3030
+ * `.length` IS the max retry count (devtools#767: "최대 2회 재시도") — the
3031
+ * generated page-side expression below derives `attempt < backoff.length`
3032
+ * from this same array (via `backoffArray`), so there is a single source for
3033
+ * both the delay ladder and the retry count; no separate max-retries
3034
+ * constant is kept in this module (unlike `runtime.ts`'s `TEST_MAX_RETRIES`,
3035
+ * which IS consumed directly by a Node-side loop condition).
3036
+ */
3037
+ const PROBE_RETRY_BACKOFF_MS = [500, 1e3];
3038
+ /**
3039
+ * Non-fatal bound for the permission preflight `Runtime.evaluate` round-trip
3040
+ * (devtools#767: raised from the pre-pacing 10s to cover the new sequential +
3041
+ * backoff time budget).
3042
+ *
3043
+ * Worst-case budget with 6 probes, each requiring the full retry ladder:
3044
+ * - inter-probe spacing: only BEFORE probes 2‑6 (none before the first) =
3045
+ * 5 × {@link PROBE_INTER_CALL_DELAY_MS} = 1 250ms
3046
+ * - per-probe backoff (both retries taken): 6 × (500 + 1000) = 9 000ms
3047
+ * - probe round-trips themselves (headroom, not just the sleeps): ~4 500ms
3048
+ * total ≈ 14 750ms, rounded up to a clean 20 000ms for headroom.
3049
+ */
3050
+ const PERMISSION_PREFLIGHT_TIMEOUT_MS = 2e4;
2961
3051
  /**
2962
3052
  * Builds the `Runtime.evaluate` expression for the permission-state preflight
2963
3053
  * (devtools#739). Evaluated ONCE per relay session, before the first test
@@ -2972,6 +3062,29 @@ const PERMISSION_PREFLIGHT_TIMEOUT_MS = 1e4;
2972
3062
  * caller, and a single slow/broken permission must not fail the others (each
2973
3063
  * probe is awaited independently, not short-circuited).
2974
3064
  *
3065
+ * devtools#767: probes run SEQUENTIALLY (not a parallel burst) with a
3066
+ * {@link PROBE_INTER_CALL_DELAY_MS} gap before each probe after the first, and
3067
+ * a probe that comes back `APP_BRIDGE_THROTTLED` (native per-method bridge
3068
+ * rate limit) is retried up to {@link PROBE_RETRY_BACKOFF_MS}`.length` times
3069
+ * with the same {@link PROBE_RETRY_BACKOFF_MS} ladder before falling back to
3070
+ * `'unavailable'`.
3071
+ * The THROTTLED detection mirrors `throttle.ts#isThrottledError` inline (this
3072
+ * expression is raw JS text evaluated via CDP, not an esbuild-bundled module,
3073
+ * so it cannot statically `import` that helper) — kept in sync by checking
3074
+ * BOTH `e.code === 'APP_BRIDGE_THROTTLED'` and a message substring match, the
3075
+ * same two signals `isThrottledError` checks.
3076
+ *
3077
+ * devtools#767 acceptance criteria 2 ("3.x run duration은 유의미하게 늘지 않음,
3078
+ * pacing은 opt-in 또는 throttle-adaptive") requires this NOT be an unconditional
3079
+ * cost — `pace` (default `true`, matching the pre-existing sequential+spacing
3080
+ * behavior for 2.x/unknown cells) lets a 3.x caller skip BOTH the inter-probe
3081
+ * `sleep(250)` AND the retry backoff ladder, since `APP_BRIDGE_THROTTLED` is a
3082
+ * 2.x-bridge-only limiter (see `--pace`'s own doc in `cli.ts`: "3.x cells are
3083
+ * unaffected by that limiter"). When `pace` is `false`, probes still run
3084
+ * sequentially (never a parallel burst) but back-to-back with no added delay,
3085
+ * and a THROTTLED result resolves to `'unavailable'` immediately (no retry) —
3086
+ * this matches the 3.x runtime, which never emits `APP_BRIDGE_THROTTLED`.
3087
+ *
2975
3088
  * Tests (sdk-example#265) read `globalThis.__AIT_PERMS__` to branch
2976
3089
  * deterministically per permission state instead of blanket outcome-branching.
2977
3090
  *
@@ -2982,9 +3095,18 @@ const PERMISSION_PREFLIGHT_TIMEOUT_MS = 1e4;
2982
3095
  *
2983
3096
  * Pure function — no I/O, no CDP call. Callers pass the returned string to
2984
3097
  * `Runtime.evaluate`.
3098
+ *
3099
+ * @param pace - Defaults to `true` (today's sequential+spacing+backoff
3100
+ * behavior). Pass `false` for a 3.x cell to skip the inter-probe delay and
3101
+ * the THROTTLED retry ladder entirely.
2985
3102
  */
2986
- function buildPermissionPreflightExpression() {
2987
- return `(async () => { const probe = async (fnName) => { try { const fn = globalThis.__sdk && globalThis.__sdk[fnName]; if (!fn || typeof fn.getPermission !== 'function') return 'unavailable'; const status = await fn.getPermission(); if (status === 'allowed' || status === 'denied' || status === 'notDetermined') return status; return 'unavailable'; } catch (e) { return 'unavailable'; } }; const result = {};${Object.entries(PERMISSION_PROBE_MAP).map(([key, fnName]) => ` result[${JSON.stringify(key)}] = await probe(${JSON.stringify(fnName)});`).join("\n")}\n globalThis.__AIT_PERMS__ = result; return JSON.stringify(result);})()`;
3103
+ function buildPermissionPreflightExpression(pace = true) {
3104
+ const probeCalls = Object.keys(PERMISSION_PROBE_MAP).map((key, i) => {
3105
+ const fnName = PERMISSION_PROBE_MAP[key];
3106
+ return `${i === 0 || !pace ? "" : ` await sleep(${PROBE_INTER_CALL_DELAY_MS});\n`} result[${JSON.stringify(key)}] = await probeWithRetry(${JSON.stringify(fnName)});`;
3107
+ }).join("\n");
3108
+ const backoffArray = pace ? `[${PROBE_RETRY_BACKOFF_MS.join(",")}]` : "[]";
3109
+ return `(async () => { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const isThrottled = (e) => { if (!e) return false; if (e.code === ${JSON.stringify(THROTTLED_ERROR_CODE)}) return true; const msg = typeof e.message === 'string' ? e.message : ''; return msg.indexOf(${JSON.stringify(THROTTLED_MESSAGE_SUBSTRING)}) !== -1; }; const probe = async (fnName) => { const fn = globalThis.__sdk && globalThis.__sdk[fnName]; if (!fn || typeof fn.getPermission !== 'function') return { ok: true, value: 'unavailable' }; const status = await fn.getPermission(); if (status === 'allowed' || status === 'denied' || status === 'notDetermined') return { ok: true, value: status }; return { ok: true, value: 'unavailable' }; }; const backoff = ${backoffArray}; const probeWithRetry = async (fnName) => { for (let attempt = 0; attempt <= backoff.length; attempt++) { try { const r = await probe(fnName); return r.value; } catch (e) { if (isThrottled(e) && attempt < backoff.length) { await sleep(backoff[attempt]); continue; } return 'unavailable'; } } return 'unavailable'; }; const result = {};${probeCalls}\n globalThis.__AIT_PERMS__ = result; return JSON.stringify(result);})()`;
2988
3110
  }
2989
3111
  /**
2990
3112
  * Runs the permission-state preflight (devtools#739) via `Runtime.evaluate`
@@ -3003,13 +3125,19 @@ function buildPermissionPreflightExpression() {
3003
3125
  *
3004
3126
  * @param conn - The live CDP connection (relay-attached page).
3005
3127
  * @param timeoutMs - Round-trip bound. Defaults to {@link PERMISSION_PREFLIGHT_TIMEOUT_MS}.
3006
- */
3007
- async function runPermissionPreflight(conn, timeoutMs = PERMISSION_PREFLIGHT_TIMEOUT_MS) {
3128
+ * @param pace - Forwarded to {@link buildPermissionPreflightExpression}.
3129
+ * Defaults to `true` (today's sequential+spacing+backoff behavior, correct
3130
+ * for 2.x/unknown cells). Callers that know the cell is 3.x should pass
3131
+ * `false` — devtools#767 acceptance criteria 2 requires 3.x run duration not
3132
+ * grow from this preflight, and 3.x never hits the 2.x-only
3133
+ * `APP_BRIDGE_THROTTLED` limiter this pacing exists to dodge.
3134
+ */
3135
+ async function runPermissionPreflight(conn, timeoutMs = PERMISSION_PREFLIGHT_TIMEOUT_MS, pace = true) {
3008
3136
  const TIMEOUT_SENTINEL = Symbol("permission-preflight-timeout");
3009
3137
  const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs));
3010
3138
  try {
3011
3139
  const evalPromise = conn.send("Runtime.evaluate", {
3012
- expression: buildPermissionPreflightExpression(),
3140
+ expression: buildPermissionPreflightExpression(pace),
3013
3141
  returnByValue: true,
3014
3142
  awaitPromise: true
3015
3143
  });
@@ -3837,7 +3965,63 @@ const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]
3837
3965
  * `--stub-blocking` flag). Default off => zero behavior change: every existing
3838
3966
  * (non-stub) test run resolves the exact same `window.__sdk` reference it did
3839
3967
  * before this plugin learned about stubbing.
3968
+ *
3969
+ * Per-method pacing (devtools#769): BEFORE the stub wrap, `window.__sdk` is
3970
+ * run through `wrapWithMethodPacing`, which enforces a minimum interval
3971
+ * between calls to the SAME named SDK function (`getPaceMethodMs()` reads the
3972
+ * `__AIT_PACE_METHOD_MS__` page global — see `method-pace.ts`). Composition
3973
+ * order is pacing INSIDE the stub (`wrapSdkWithStub(wrapWithMethodPacing(sdk,
3974
+ * gap), enabled)`): a stubbed name resolves instantly from
3975
+ * `bridge-stub.ts`'s fixtures without ever reaching the paced wrapper, and
3976
+ * only calls that fall through to the real bridge — the ones that can
3977
+ * actually trigger `APP_BRIDGE_THROTTLED` — pay the pacing cost. `gapMs <= 0`
3978
+ * (default, no `--pace-method` / non-2.x cell) is a no-op fast path — see
3979
+ * `method-pace.ts#wrapWithMethodPacing`'s doc for the full contract.
3980
+ */
3981
+ /**
3982
+ * Builds the virtual CommonJS-style module contents the `sdk-redirect`
3983
+ * esbuild plugin loads for every `@apps-in-toss/web-framework` import.
3984
+ *
3985
+ * Pulled out to a pure, exported function (rather than inlined at the
3986
+ * `build.onLoad` call site) so unit tests can assert the exact composition
3987
+ * order (pacing wraps the raw SDK; the stub wraps the paced result) as a
3988
+ * string-level contract, independent of esbuild — esbuild cannot run inside
3989
+ * this repo's jsdom vitest environment (see `bundleTestFile`'s module doc's
3990
+ * lazy-import note), so a real bundling round-trip is out of unit-test reach
3991
+ * here; `wrapSdkWithStub`/`wrapWithMethodPacing`'s own composed-call behavior
3992
+ * is covered directly (see `bundle.test.ts`).
3993
+ *
3994
+ * Generates a virtual CommonJS-style module so that esbuild does NOT perform
3995
+ * strict named-export matching. When `format:'iife'` bundles a CJS module, it
3996
+ * wraps it with its own `__toCommonJS` helper and satisfies named imports via
3997
+ * property access on the `module.exports` object — which is our Proxy. This
3998
+ * means `import { getPlatformOS } from '...'` becomes
3999
+ * `__proxy.getPlatformOS` at runtime, which correctly reads from
4000
+ * `window.__sdk`.
4001
+ *
4002
+ * The bridge-stub and method-pace imports are STATIC imports of the real
4003
+ * `bridge-stub.ts`/`method-pace.ts` modules (not inlined JS) so esbuild
4004
+ * bundles each once and the fixture registry / pacing registry have a single
4005
+ * source of truth shared with their own unit tests.
4006
+ *
4007
+ * Exported for unit testing.
3840
4008
  */
4009
+ function buildSdkRedirectModuleContents() {
4010
+ return `
4011
+ import { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};
4012
+ import { wrapWithMethodPacing, getPaceMethodMs } from ${JSON.stringify(getMethodPacePath())};
4013
+ var __rawSdk = (typeof window !== 'undefined' && window.__sdk)
4014
+ ? window.__sdk
4015
+ : new Proxy({}, {
4016
+ get: function(_t, p) {
4017
+ throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
4018
+ }
4019
+ });
4020
+ var __pacedSdk = wrapWithMethodPacing(__rawSdk, getPaceMethodMs());
4021
+ var __proxy = wrapSdkWithStub(__pacedSdk, isStubBlockingEnabled());
4022
+ module.exports = __proxy;
4023
+ `;
4024
+ }
3841
4025
  function sdkRedirectPlugin() {
3842
4026
  return {
3843
4027
  name: "sdk-redirect",
@@ -3850,18 +4034,7 @@ function sdkRedirectPlugin() {
3850
4034
  filter: /.*/,
3851
4035
  namespace: "sdk-redirect"
3852
4036
  }, () => ({
3853
- contents: `
3854
- import { wrapSdkWithStub, isStubBlockingEnabled } from ${JSON.stringify(getBridgeStubPath())};
3855
- var __rawSdk = (typeof window !== 'undefined' && window.__sdk)
3856
- ? window.__sdk
3857
- : new Proxy({}, {
3858
- get: function(_t, p) {
3859
- throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
3860
- }
3861
- });
3862
- var __proxy = wrapSdkWithStub(__rawSdk, isStubBlockingEnabled());
3863
- module.exports = __proxy;
3864
- `,
4037
+ contents: buildSdkRedirectModuleContents(),
3865
4038
  loader: "js",
3866
4039
  resolveDir: process.cwd()
3867
4040
  }));
@@ -4062,6 +4235,14 @@ function getBridgeStubPath() {
4062
4235
  return getPageSideModulePath("bridge-stub");
4063
4236
  }
4064
4237
  /**
4238
+ * Absolute path to the per-method pacing wrapper (devtools#769) —
4239
+ * `wrapWithMethodPacing`/`getPaceMethodMs`, statically imported by the
4240
+ * `sdk-redirect` virtual module.
4241
+ */
4242
+ function getMethodPacePath() {
4243
+ return getPageSideModulePath("method-pace");
4244
+ }
4245
+ /**
4065
4246
  * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
4066
4247
  *
4067
4248
  * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
@@ -4335,13 +4516,17 @@ async function runTestFilesOverRelay(connection, files, opts) {
4335
4516
  let manualIndex = 0;
4336
4517
  const stubBlockingFiles = opts?.stubBlockingFiles;
4337
4518
  let preflightPermissions;
4338
- if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection);
4519
+ if (files.length > 0) preflightPermissions = await runPermissionPreflight(connection, PERMISSION_PREFLIGHT_TIMEOUT_MS, opts?.preflightSdkLine !== "3.x");
4520
+ const paceMs = opts?.paceMs;
4521
+ let ranFirstFile = false;
4339
4522
  try {
4340
4523
  for (const file of files) {
4341
4524
  if (pendingReconnectCheck) {
4342
4525
  await attemptReconnect("next-file");
4343
4526
  pendingReconnectCheck = false;
4344
4527
  }
4528
+ if (paceMs !== void 0 && paceMs > 0 && ranFirstFile) await new Promise((resolve) => setTimeout(resolve, paceMs));
4529
+ ranFirstFile = true;
4345
4530
  const isStubbed = stubBlockingFiles?.has(file) === true;
4346
4531
  const isManual = !isStubbed && manualFiles?.has(file) === true;
4347
4532
  if (isManual) {
@@ -4478,7 +4663,21 @@ USAGE
4478
4663
 
4479
4664
  OPTIONS
4480
4665
  --scheme-url <url> intoss-private:// URL from \`ait deploy --scheme-only\`
4481
- (required for standalone relay attach / env3)
4666
+ (required for standalone relay attach / env3). Unused
4667
+ and ignored when --attach-launcher is set.
4668
+ --attach-launcher Attach over the env-2 AITC Sandbox PWA launcher
4669
+ (real-device WebKit) instead of the env-3 intoss
4670
+ scheme deep-link. The relay/QR/dashboard/attach-wait
4671
+ are identical; only the QR is a launcher deep-link.
4672
+ Requires --app-url. The SDK still hits the mock in
4673
+ env 2, so this measures the mock + standard Web API
4674
+ layer's engine-attributable behavior (env1↔env2
4675
+ equivalence), NOT native-bridge fidelity (that is env3).
4676
+ --app-url <url> The consumer dev server's HTTP tunnel URL (e.g. the
4677
+ *.trycloudflare.com URL from \`pnpm dev:phone:cdp\`)
4678
+ that the launcher PWA frames. REQUIRED with
4679
+ --attach-launcher; ignored otherwise. SECRET: a tunnel
4680
+ host — never printed; it rides only inside the QR.
4482
4681
  --timeout <ms> Per-file evaluate timeout in ms (default: 60000).
4483
4682
  Controls how long a single test file is allowed to run
4484
4683
  before it is considered hung. Does NOT affect how long
@@ -4490,7 +4689,10 @@ OPTIONS
4490
4689
  Pass a value to bound the wait for CI/headless runs.
4491
4690
  --cell-sdk-line <line> SDK line to inject as __AIT_CELL__.sdkLine (2.x|3.x)
4492
4691
  --cell-platform <plat> Platform to inject as __AIT_CELL__.platform
4493
- (mock|ios|android, default: AIT_CELL_PLATFORM env)
4692
+ (mock|ios|android|ios-pwa, default: AIT_CELL_PLATFORM
4693
+ env). Use ios-pwa for env-2 (--attach-launcher) runs
4694
+ so env1(mock@desktop) and env2(mock@WebKit) captures
4695
+ stay distinguishable. An unknown value is rejected.
4494
4696
  --report-dir <dir> Persist a runner-agnostic report + captures to <dir>
4495
4697
  (report: <sdkLine>.<platform>.json; captures:
4496
4698
  <dir>/.ait-capture/<category>.<sdkLine>.<platform>.json).
@@ -4505,6 +4707,28 @@ OPTIONS
4505
4707
  --headless Disable browser auto-open (text QR only)
4506
4708
  --project-root <dir> Project root for .ait_relay secret lookup
4507
4709
  (default: current working directory)
4710
+ --pace <ms> Minimum delay in ms between test-to-test AND
4711
+ file-to-file bridge calls (default: 0, i.e. no
4712
+ added delay — today's behavior byte-for-byte).
4713
+ Falls back to the AIT_PACE env var when omitted
4714
+ (--pace takes precedence over the env var when both
4715
+ are given). Use on a 2.x cell scan when the native
4716
+ per-method bridge rate limit (APP_BRIDGE_THROTTLED,
4717
+ devtools#767) is rejecting rapid same-method calls —
4718
+ 3.x cells are unaffected by that limiter and do not
4719
+ need this flag.
4720
+ --pace-method <ms> Minimum delay in ms BETWEEN calls to the SAME named
4721
+ SDK function — paces a same-method burst WITHIN a
4722
+ single test body (e.g. a clipboard happy-path loop
4723
+ calling setClipboardText/getClipboardText 8 times
4724
+ back to back), which --pace's test/file spacing
4725
+ cannot reach (devtools#769). Falls back to the
4726
+ AIT_PACE_METHOD env var when omitted (--pace-method
4727
+ takes precedence over the env var when both are
4728
+ given). Default: 250ms when --cell-sdk-line is 2.x
4729
+ (unset --cell-sdk-line also defaults to 2.x — see
4730
+ --cell-sdk-line), 0 (no added delay) otherwise. Pass
4731
+ --pace-method 0 to opt out even on a 2.x cell.
4508
4732
  --manual-blocking Run manual-tagged test files (*.manual.ait.test.ts)
4509
4733
  LAST, after all regular files, with a human present.
4510
4734
  Before each manual file, the QR dashboard is pushed
@@ -4557,7 +4781,7 @@ DESCRIPTION
4557
4781
  The test files run against the live relay connection started by this process;
4558
4782
  no separate MCP daemon is required.
4559
4783
 
4560
- EXAMPLE
4784
+ EXAMPLE (env 3 — intoss-private scheme)
4561
4785
  devtools-test 'src/**/*.ait.test.ts' \\
4562
4786
  --scheme-url "intoss-private://..." \\
4563
4787
  --cell-sdk-line 3.x \\
@@ -4565,6 +4789,14 @@ EXAMPLE
4565
4789
  --report-dir .ait-report \\
4566
4790
  --timeout 60000
4567
4791
 
4792
+ EXAMPLE (env 2 — AITC Sandbox PWA launcher)
4793
+ devtools-test 'src/**/*.ait.test.ts' \\
4794
+ --attach-launcher \\
4795
+ --app-url "https://<subdomain>.trycloudflare.com" \\
4796
+ --cell-sdk-line 3.x \\
4797
+ --cell-platform ios-pwa \\
4798
+ --report-dir .ait-report
4799
+
4568
4800
  `.trimStart();
4569
4801
  /**
4570
4802
  * Renders per-file result lines and the aggregate totals line to a string.
@@ -7256,7 +7488,7 @@ function createDebugServer(deps) {
7256
7488
  };
7257
7489
  const server = new Server({
7258
7490
  name: "ait-debug",
7259
- version: "0.1.137"
7491
+ version: "0.1.138"
7260
7492
  }, { capabilities: { tools: { listChanged: true } } });
7261
7493
  server.setRequestHandler(ListToolsRequestSchema, () => {
7262
7494
  const conn = router.active;
@@ -7692,6 +7924,13 @@ function errorResult(err, name, isLocal = false) {
7692
7924
  * SECRET-HANDLING: target `id`/`title`/`url` are not written to any log here.
7693
7925
  * Only an attach-detected stderr line is emitted (no target details).
7694
7926
  *
7927
+ * `server` is optional (devtools#772): the standalone test-runner path
7928
+ * (`test-runner/relay-factory.ts`) has no MCP `Server` instance to notify —
7929
+ * it only needs the `onAttach`/`onDetach` dashboard-push side effects. When
7930
+ * omitted, `sendToolListChanged()` is simply skipped; the signature-diff and
7931
+ * callback logic is byte-for-byte the same. Existing MCP daemon call sites
7932
+ * always pass a real `Server`, so their behavior is unchanged.
7933
+ *
7695
7934
  * @returns `stop` — call this during shutdown to clear the interval.
7696
7935
  */
7697
7936
  function startAttachWatcher(connection, server, intervalMs = 1e3, onAttach, onDetach) {
@@ -7701,7 +7940,7 @@ function startAttachWatcher(connection, server, intervalMs = 1e3, onAttach, onDe
7701
7940
  }
7702
7941
  let lastSignature = signature();
7703
7942
  if (lastSignature !== "") {
7704
- server.sendToolListChanged();
7943
+ server?.sendToolListChanged();
7705
7944
  onAttach?.();
7706
7945
  }
7707
7946
  /** Compare current vs last signature and fire the appropriate callback. */
@@ -7711,7 +7950,7 @@ function startAttachWatcher(connection, server, intervalMs = 1e3, onAttach, onDe
7711
7950
  const wasNonEmpty = lastSignature !== "";
7712
7951
  lastSignature = current;
7713
7952
  if (current !== "") {
7714
- server.sendToolListChanged();
7953
+ server?.sendToolListChanged();
7715
7954
  onAttach?.();
7716
7955
  } else if (wasNonEmpty) onDetach?.();
7717
7956
  }
@@ -7903,7 +8142,7 @@ async function readRelayLocalUrl(env = process.env, projectRoot) {
7903
8142
  const envValue = (env.AIT_RELAY_LOCAL_URL ?? "").trim();
7904
8143
  if (envValue !== "") return envValue;
7905
8144
  if (projectRoot !== void 0) try {
7906
- const { readRelayUrls } = await import("../relay-url-store-CwKT7i04.js");
8145
+ const { readRelayUrls } = await import("../relay-url-store-DaY1QPes.js");
7907
8146
  const stored = await readRelayUrls({ projectRoot });
7908
8147
  if (stored?.relayLocalUrl) return stored.relayLocalUrl;
7909
8148
  } catch {}
@@ -7957,7 +8196,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
7957
8196
  const envValue = typeof raw === "string" ? raw.trim() : "";
7958
8197
  if (envValue !== "") return envValue;
7959
8198
  if (projectRoot !== void 0) {
7960
- const { readRelayUrls } = await import("../relay-url-store-CwKT7i04.js");
8199
+ const { readRelayUrls } = await import("../relay-url-store-DaY1QPes.js");
7961
8200
  const stored = await readRelayUrls({ projectRoot });
7962
8201
  if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
7963
8202
  }
@@ -9240,7 +9479,7 @@ function createDevServer(deps = {}) {
9240
9479
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
9241
9480
  const server = new Server({
9242
9481
  name: "ait-devtools",
9243
- version: "0.1.137"
9482
+ version: "0.1.138"
9244
9483
  }, { capabilities: { tools: {} } });
9245
9484
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
9246
9485
  server.setRequestHandler(CallToolRequestSchema, async (request) => {