@ait-co/devtools 0.1.143 → 0.2.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 (223) hide show
  1. package/README.en.md +65 -163
  2. package/README.md +65 -192
  3. package/dist/in-app/auto.d.ts +1 -138
  4. package/dist/in-app/auto.js +28 -1102
  5. package/dist/in-app/auto.js.map +1 -1
  6. package/dist/in-app/index.d.ts +38 -547
  7. package/dist/in-app/index.d.ts.map +1 -1
  8. package/dist/in-app/index.js +62 -939
  9. package/dist/in-app/index.js.map +1 -1
  10. package/dist/mcp/cli.d.ts +1 -54
  11. package/dist/mcp/cli.js +33 -9720
  12. package/dist/mcp/cli.js.map +1 -1
  13. package/dist/mcp/server.d.ts +1 -88
  14. package/dist/mcp/server.js +36 -1076
  15. package/dist/mcp/server.js.map +1 -1
  16. package/dist/mock/index.d.ts +19 -20
  17. package/dist/mock/index.d.ts.map +1 -1
  18. package/dist/mock/index.js.map +1 -1
  19. package/dist/optional-peers-CDEFhlhJ.cjs +131 -0
  20. package/dist/optional-peers-CDEFhlhJ.cjs.map +1 -0
  21. package/dist/optional-peers-FdVbUJst.js +96 -0
  22. package/dist/optional-peers-FdVbUJst.js.map +1 -0
  23. package/dist/panel/index.js +1 -103
  24. package/dist/panel/index.js.map +1 -1
  25. package/dist/relay-url-store-CPZAn-T5.js +107 -0
  26. package/dist/relay-url-store-CPZAn-T5.js.map +1 -0
  27. package/dist/relay-url-store-DLjlvMSA.cjs +108 -0
  28. package/dist/relay-url-store-DLjlvMSA.cjs.map +1 -0
  29. package/dist/stubs/bin-devtools-mcp.js +58 -0
  30. package/dist/stubs/bin-devtools-mcp.js.map +1 -0
  31. package/dist/stubs/bin-devtools-test.d.ts +2 -0
  32. package/dist/stubs/bin-devtools-test.js +55 -0
  33. package/dist/stubs/bin-devtools-test.js.map +1 -0
  34. package/dist/test-runner/config.d.ts +1 -231
  35. package/dist/test-runner/config.js +41 -45
  36. package/dist/test-runner/config.js.map +1 -1
  37. package/dist/{tunnel-BVSMXctM.cjs → tunnel-BKZkOyQp.cjs} +27 -75
  38. package/dist/tunnel-BKZkOyQp.cjs.map +1 -0
  39. package/dist/{tunnel-D7xkimBu.js → tunnel-CqSCIrdU.js} +27 -75
  40. package/dist/tunnel-CqSCIrdU.js.map +1 -0
  41. package/dist/unplugin/index.cjs +17 -30
  42. package/dist/unplugin/index.cjs.map +1 -1
  43. package/dist/unplugin/index.d.cts +34 -5
  44. package/dist/unplugin/index.d.cts.map +1 -1
  45. package/dist/unplugin/index.d.ts +35 -6
  46. package/dist/unplugin/index.d.ts.map +1 -1
  47. package/dist/unplugin/index.js +17 -30
  48. package/dist/unplugin/index.js.map +1 -1
  49. package/dist/unplugin/tunnel.cjs +61 -74
  50. package/dist/unplugin/tunnel.cjs.map +1 -1
  51. package/dist/unplugin/tunnel.d.cts +22 -16
  52. package/dist/unplugin/tunnel.d.cts.map +1 -1
  53. package/dist/unplugin/tunnel.d.ts +22 -16
  54. package/dist/unplugin/tunnel.d.ts.map +1 -1
  55. package/dist/unplugin/tunnel.js +61 -74
  56. package/dist/unplugin/tunnel.js.map +1 -1
  57. package/package.json +17 -22
  58. package/dist/attach-orchestrator-0F0m_UqQ.js +0 -1845
  59. package/dist/attach-orchestrator-0F0m_UqQ.js.map +0 -1
  60. package/dist/attach-orchestrator-D65KxFy_.js +0 -1831
  61. package/dist/attach-orchestrator-D65KxFy_.js.map +0 -1
  62. package/dist/attach-orchestrator-DL3NQ9ca.js +0 -1846
  63. package/dist/attach-orchestrator-DL3NQ9ca.js.map +0 -1
  64. package/dist/bundle-C796JIwG.d.ts +0 -159
  65. package/dist/bundle-C796JIwG.d.ts.map +0 -1
  66. package/dist/capture-DsP525OZ.d.ts +0 -58
  67. package/dist/capture-DsP525OZ.d.ts.map +0 -1
  68. package/dist/cdp-connection-rP1WdnH5.d.ts +0 -287
  69. package/dist/cdp-connection-rP1WdnH5.d.ts.map +0 -1
  70. package/dist/cell-BaLvusOl.js +0 -68
  71. package/dist/cell-BaLvusOl.js.map +0 -1
  72. package/dist/cell-CBUS3-nT.js +0 -274
  73. package/dist/cell-CBUS3-nT.js.map +0 -1
  74. package/dist/cell-EBKKpAAT.js +0 -307
  75. package/dist/cell-EBKKpAAT.js.map +0 -1
  76. package/dist/chii-relay-BZ3HqWL5.js +0 -304
  77. package/dist/chii-relay-BZ3HqWL5.js.map +0 -1
  78. package/dist/chii-relay-D7eK2acz.cjs +0 -304
  79. package/dist/chii-relay-D7eK2acz.cjs.map +0 -1
  80. package/dist/debug-server-B3ABDrRI.js +0 -456
  81. package/dist/debug-server-B3ABDrRI.js.map +0 -1
  82. package/dist/debug-server-BWhwrVXa.js +0 -1158
  83. package/dist/debug-server-BWhwrVXa.js.map +0 -1
  84. package/dist/debug-server-CfQNxxGW.js +0 -600
  85. package/dist/debug-server-CfQNxxGW.js.map +0 -1
  86. package/dist/deeplink-B5-Hxu0Q.js +0 -62
  87. package/dist/deeplink-B5-Hxu0Q.js.map +0 -1
  88. package/dist/deeplink-BpO9qc-D.js +0 -62
  89. package/dist/deeplink-BpO9qc-D.js.map +0 -1
  90. package/dist/deeplink-BzdbA1gV.cjs +0 -62
  91. package/dist/deeplink-BzdbA1gV.cjs.map +0 -1
  92. package/dist/deeplink-DCScMYcp.cjs +0 -62
  93. package/dist/deeplink-DCScMYcp.cjs.map +0 -1
  94. package/dist/devtools-opener-3Drge_RJ.js +0 -75
  95. package/dist/devtools-opener-3Drge_RJ.js.map +0 -1
  96. package/dist/devtools-opener-B8nxrxqu.js +0 -71
  97. package/dist/devtools-opener-B8nxrxqu.js.map +0 -1
  98. package/dist/devtools-opener-BDY0w3_0.cjs +0 -68
  99. package/dist/devtools-opener-BDY0w3_0.cjs.map +0 -1
  100. package/dist/devtools-opener-BTl5A6Cd.js +0 -71
  101. package/dist/devtools-opener-BTl5A6Cd.js.map +0 -1
  102. package/dist/devtools-opener-CJpEsXXQ.js +0 -76
  103. package/dist/devtools-opener-CJpEsXXQ.js.map +0 -1
  104. package/dist/devtools-opener-CxtryS8c.js +0 -75
  105. package/dist/devtools-opener-CxtryS8c.js.map +0 -1
  106. package/dist/devtools-opener-iv1OwfJN.cjs +0 -68
  107. package/dist/devtools-opener-iv1OwfJN.cjs.map +0 -1
  108. package/dist/in-app/auto.d.ts.map +0 -1
  109. package/dist/mcp/cli.d.ts.map +0 -1
  110. package/dist/mcp/server.d.ts.map +0 -1
  111. package/dist/pool-DcaaOwUq.d.ts +0 -14761
  112. package/dist/pool-DcaaOwUq.d.ts.map +0 -1
  113. package/dist/qr-http-server--gl2-WKc.cjs +0 -1644
  114. package/dist/qr-http-server--gl2-WKc.cjs.map +0 -1
  115. package/dist/qr-http-server-Bb7lMeqi.js +0 -1644
  116. package/dist/qr-http-server-Bb7lMeqi.js.map +0 -1
  117. package/dist/qr-http-server-C_lqOrgc.js +0 -1644
  118. package/dist/qr-http-server-C_lqOrgc.js.map +0 -1
  119. package/dist/qr-http-server-CopuMbub.js +0 -1644
  120. package/dist/qr-http-server-CopuMbub.js.map +0 -1
  121. package/dist/qr-http-server-D-Off6K1.cjs +0 -1644
  122. package/dist/qr-http-server-D-Off6K1.cjs.map +0 -1
  123. package/dist/qr-http-server-DrbIVDjO.js +0 -1645
  124. package/dist/qr-http-server-DrbIVDjO.js.map +0 -1
  125. package/dist/qr-http-server-n1twN18z.js +0 -1644
  126. package/dist/qr-http-server-n1twN18z.js.map +0 -1
  127. package/dist/relay-factory-N9QobQxG.js +0 -206
  128. package/dist/relay-factory-N9QobQxG.js.map +0 -1
  129. package/dist/relay-secret-store-BPhN1upr.js +0 -240
  130. package/dist/relay-secret-store-BPhN1upr.js.map +0 -1
  131. package/dist/relay-secret-store-Bmyleu0A.js +0 -154
  132. package/dist/relay-secret-store-Bmyleu0A.js.map +0 -1
  133. package/dist/relay-secret-store-CQenfcSL.js +0 -154
  134. package/dist/relay-secret-store-CQenfcSL.js.map +0 -1
  135. package/dist/relay-secret-store-DKxs7zwq.js +0 -153
  136. package/dist/relay-secret-store-DKxs7zwq.js.map +0 -1
  137. package/dist/relay-secret-store-DWKdV-eY.cjs +0 -241
  138. package/dist/relay-secret-store-DWKdV-eY.cjs.map +0 -1
  139. package/dist/relay-secret-store-WJ8EGkIl.js +0 -153
  140. package/dist/relay-secret-store-WJ8EGkIl.js.map +0 -1
  141. package/dist/relay-url-store-1FGuSYAn.cjs +0 -115
  142. package/dist/relay-url-store-1FGuSYAn.cjs.map +0 -1
  143. package/dist/relay-url-store-BR2XodiO.js +0 -123
  144. package/dist/relay-url-store-BR2XodiO.js.map +0 -1
  145. package/dist/relay-url-store-Bskcyeg8.js +0 -114
  146. package/dist/relay-url-store-Bskcyeg8.js.map +0 -1
  147. package/dist/relay-url-store-CH63fVCm.js +0 -122
  148. package/dist/relay-url-store-CH63fVCm.js.map +0 -1
  149. package/dist/relay-url-store-DaY1QPes.js +0 -123
  150. package/dist/relay-url-store-DaY1QPes.js.map +0 -1
  151. package/dist/relay-url-store-xmUuTjXA.js +0 -122
  152. package/dist/relay-url-store-xmUuTjXA.js.map +0 -1
  153. package/dist/relay-worker-B5HKkGUY.js +0 -832
  154. package/dist/relay-worker-B5HKkGUY.js.map +0 -1
  155. package/dist/relay-worker-YdlpZQl9.d.ts +0 -214
  156. package/dist/relay-worker-YdlpZQl9.d.ts.map +0 -1
  157. package/dist/rolldown-runtime-DGkTqVfb.js +0 -15
  158. package/dist/rolldown-runtime-DUslC3ob.js +0 -14
  159. package/dist/runtime-kn9DxOeg.d.ts +0 -249
  160. package/dist/runtime-kn9DxOeg.d.ts.map +0 -1
  161. package/dist/test-runner/bin.js +0 -2584
  162. package/dist/test-runner/bin.js.map +0 -1
  163. package/dist/test-runner/bridge-stub.d.ts +0 -125
  164. package/dist/test-runner/bridge-stub.d.ts.map +0 -1
  165. package/dist/test-runner/bridge-stub.js +0 -92
  166. package/dist/test-runner/bridge-stub.js.map +0 -1
  167. package/dist/test-runner/bundle.d.ts +0 -2
  168. package/dist/test-runner/bundle.js +0 -439
  169. package/dist/test-runner/bundle.js.map +0 -1
  170. package/dist/test-runner/capture.d.ts +0 -2
  171. package/dist/test-runner/capture.js +0 -44
  172. package/dist/test-runner/capture.js.map +0 -1
  173. package/dist/test-runner/config.d.ts.map +0 -1
  174. package/dist/test-runner/method-pace.d.ts +0 -82
  175. package/dist/test-runner/method-pace.d.ts.map +0 -1
  176. package/dist/test-runner/method-pace.js +0 -120
  177. package/dist/test-runner/method-pace.js.map +0 -1
  178. package/dist/test-runner/pool.d.ts +0 -2
  179. package/dist/test-runner/pool.js +0 -136
  180. package/dist/test-runner/pool.js.map +0 -1
  181. package/dist/test-runner/relay-factory.d.ts +0 -11245
  182. package/dist/test-runner/relay-factory.d.ts.map +0 -1
  183. package/dist/test-runner/relay-factory.js +0 -206
  184. package/dist/test-runner/relay-factory.js.map +0 -1
  185. package/dist/test-runner/relay-worker.d.ts +0 -2
  186. package/dist/test-runner/relay-worker.js +0 -2
  187. package/dist/test-runner/report.d.ts +0 -163
  188. package/dist/test-runner/report.d.ts.map +0 -1
  189. package/dist/test-runner/report.js +0 -198
  190. package/dist/test-runner/report.js.map +0 -1
  191. package/dist/test-runner/rpc.d.ts +0 -56
  192. package/dist/test-runner/rpc.d.ts.map +0 -1
  193. package/dist/test-runner/rpc.js +0 -98
  194. package/dist/test-runner/rpc.js.map +0 -1
  195. package/dist/test-runner/runtime.d.ts +0 -2
  196. package/dist/test-runner/runtime.js +0 -659
  197. package/dist/test-runner/runtime.js.map +0 -1
  198. package/dist/test-runner/task-graph.d.ts +0 -38
  199. package/dist/test-runner/task-graph.d.ts.map +0 -1
  200. package/dist/test-runner/task-graph.js +0 -182
  201. package/dist/test-runner/task-graph.js.map +0 -1
  202. package/dist/throttle-DKKzX1qC.js +0 -59
  203. package/dist/throttle-DKKzX1qC.js.map +0 -1
  204. package/dist/totp-95OAa20j.js +0 -64
  205. package/dist/totp-95OAa20j.js.map +0 -1
  206. package/dist/totp-BjtoQNfu.cjs +0 -64
  207. package/dist/totp-BjtoQNfu.cjs.map +0 -1
  208. package/dist/totp-CZLLKfOC.js +0 -200
  209. package/dist/totp-CZLLKfOC.js.map +0 -1
  210. package/dist/totp-DAxys-r0.js +0 -199
  211. package/dist/totp-DAxys-r0.js.map +0 -1
  212. package/dist/totp-DIbrZtI7.js +0 -189
  213. package/dist/totp-DIbrZtI7.js.map +0 -1
  214. package/dist/totp-Df252ZdA.cjs +0 -192
  215. package/dist/totp-Df252ZdA.cjs.map +0 -1
  216. package/dist/totp-DfekTBk3.js +0 -211
  217. package/dist/totp-DfekTBk3.js.map +0 -1
  218. package/dist/totp-Dwft0Kz7.js +0 -3
  219. package/dist/totp-WY6l0ysP.js +0 -190
  220. package/dist/totp-WY6l0ysP.js.map +0 -1
  221. package/dist/tunnel-BVSMXctM.cjs.map +0 -1
  222. package/dist/tunnel-D7xkimBu.js.map +0 -1
  223. /package/dist/{test-runner/bin.d.ts → stubs/bin-devtools-mcp.d.ts} +0 -0
@@ -1,68 +0,0 @@
1
- //#region src/mcp/devtools-opener.ts
2
- /**
3
- * Returns `true` when auto-open is **disabled**.
4
- *
5
- * Default (env var absent or any value other than `"1"`) is **disabled** —
6
- * the developer uses the "디버그 툴 열기" button on the /attach or dashboard
7
- * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic
8
- * browser-open behaviour on device attach.
9
- *
10
- * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward
11
- * compatibility (same effect as absent).
12
- */
13
- function isAutoDevtoolsDisabled() {
14
- return process.env.AIT_AUTO_DEVTOOLS !== "1";
15
- }
16
- /**
17
- * Opens the given URL in the OS default browser using a platform-appropriate
18
- * command. Returns `true` on success.
19
- *
20
- * Failures are silent from the caller's perspective — the caller should log
21
- * the URL to stderr as a fallback before calling this function.
22
- */
23
- function openUrlInBrowser(url) {
24
- if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === "1") return false;
25
- const { spawnSync } = require("node:child_process");
26
- const platform = process.platform;
27
- let candidates;
28
- if (platform === "darwin") candidates = [{
29
- cmd: "open",
30
- args: [url]
31
- }];
32
- else if (platform === "win32") candidates = [{
33
- cmd: "cmd",
34
- args: [
35
- "/c",
36
- "start",
37
- "",
38
- url
39
- ]
40
- }];
41
- else candidates = [
42
- {
43
- cmd: "xdg-open",
44
- args: [url]
45
- },
46
- {
47
- cmd: "sensible-browser",
48
- args: [url]
49
- },
50
- {
51
- cmd: "x-www-browser",
52
- args: [url]
53
- }
54
- ];
55
- for (const { cmd, args } of candidates) try {
56
- const result = spawnSync(cmd, args, {
57
- encoding: "utf8",
58
- timeout: 5e3
59
- });
60
- if (!result.error && result.status === 0) return true;
61
- } catch {}
62
- return false;
63
- }
64
- //#endregion
65
- exports.isAutoDevtoolsDisabled = isAutoDevtoolsDisabled;
66
- exports.openUrlInBrowser = openUrlInBrowser;
67
-
68
- //# sourceMappingURL=devtools-opener-BDY0w3_0.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"devtools-opener-BDY0w3_0.cjs","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;AA2KA,SAAgB,yBAAkC;AAChD,QAAO,QAAQ,IAAI,sBAAsB;;;;;;;;;AAc3C,SAAgB,iBAAiB,KAAsB;AAGrD,KAAI,QAAQ,IAAI,sCAAsC,IAAK,QAAO;CAElE,MAAM,EAAE,cAAc,QAAQ,qBAAqB;CACnD,MAAM,WAAW,QAAQ;CAGzB,IAAI;AACJ,KAAI,aAAa,SACf,cAAa,CAAC;EAAE,KAAK;EAAQ,MAAM,CAAC,IAAI;EAAE,CAAC;UAClC,aAAa,QACtB,cAAa,CAAC;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAI;EAAE,CAAC;KAG7D,cAAa;EACX;GAAE,KAAK;GAAY,MAAM,CAAC,IAAI;GAAE;EAChC;GAAE,KAAK;GAAoB,MAAM,CAAC,IAAI;GAAE;EACxC;GAAE,KAAK;GAAiB,MAAM,CAAC,IAAI;GAAE;EACtC;AAGH,MAAK,MAAM,EAAE,KAAK,UAAU,WAC1B,KAAI;EACF,MAAM,SAAS,UAAU,KAAK,MAAM;GAAE,UAAU;GAAQ,SAAS;GAAO,CAAC;AACzE,MAAI,CAAC,OAAO,SAAS,OAAO,WAAW,EAAG,QAAO;SAC3C;AAIV,QAAO"}
@@ -1,71 +0,0 @@
1
- import { createRequire } from "node:module";
2
- //#region \0rolldown/runtime.js
3
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
- //#endregion
5
- //#region src/mcp/devtools-opener.ts
6
- /**
7
- * Returns `true` when auto-open is **disabled**.
8
- *
9
- * Default (env var absent or any value other than `"1"`) is **disabled** —
10
- * the developer uses the "디버그 툴 열기" button on the /attach or dashboard
11
- * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic
12
- * browser-open behaviour on device attach.
13
- *
14
- * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward
15
- * compatibility (same effect as absent).
16
- */
17
- function isAutoDevtoolsDisabled() {
18
- return process.env.AIT_AUTO_DEVTOOLS !== "1";
19
- }
20
- /**
21
- * Opens the given URL in the OS default browser using a platform-appropriate
22
- * command. Returns `true` on success.
23
- *
24
- * Failures are silent from the caller's perspective — the caller should log
25
- * the URL to stderr as a fallback before calling this function.
26
- */
27
- function openUrlInBrowser(url) {
28
- if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === "1") return false;
29
- const { spawnSync } = __require("node:child_process");
30
- const platform = process.platform;
31
- let candidates;
32
- if (platform === "darwin") candidates = [{
33
- cmd: "open",
34
- args: [url]
35
- }];
36
- else if (platform === "win32") candidates = [{
37
- cmd: "cmd",
38
- args: [
39
- "/c",
40
- "start",
41
- "",
42
- url
43
- ]
44
- }];
45
- else candidates = [
46
- {
47
- cmd: "xdg-open",
48
- args: [url]
49
- },
50
- {
51
- cmd: "sensible-browser",
52
- args: [url]
53
- },
54
- {
55
- cmd: "x-www-browser",
56
- args: [url]
57
- }
58
- ];
59
- for (const { cmd, args } of candidates) try {
60
- const result = spawnSync(cmd, args, {
61
- encoding: "utf8",
62
- timeout: 5e3
63
- });
64
- if (!result.error && result.status === 0) return true;
65
- } catch {}
66
- return false;
67
- }
68
- //#endregion
69
- export { isAutoDevtoolsDisabled, openUrlInBrowser };
70
-
71
- //# sourceMappingURL=devtools-opener-BTl5A6Cd.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"devtools-opener-BTl5A6Cd.js","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA2KA,SAAgB,yBAAkC;AAChD,QAAO,QAAQ,IAAI,sBAAsB;;;;;;;;;AAc3C,SAAgB,iBAAiB,KAAsB;AAGrD,KAAI,QAAQ,IAAI,sCAAsC,IAAK,QAAO;CAElE,MAAM,EAAE,cAAA,UAAsB,qBAAqB;CACnD,MAAM,WAAW,QAAQ;CAGzB,IAAI;AACJ,KAAI,aAAa,SACf,cAAa,CAAC;EAAE,KAAK;EAAQ,MAAM,CAAC,IAAI;EAAE,CAAC;UAClC,aAAa,QACtB,cAAa,CAAC;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAI;EAAE,CAAC;KAG7D,cAAa;EACX;GAAE,KAAK;GAAY,MAAM,CAAC,IAAI;GAAE;EAChC;GAAE,KAAK;GAAoB,MAAM,CAAC,IAAI;GAAE;EACxC;GAAE,KAAK;GAAiB,MAAM,CAAC,IAAI;GAAE;EACtC;AAGH,MAAK,MAAM,EAAE,KAAK,UAAU,WAC1B,KAAI;EACF,MAAM,SAAS,UAAU,KAAK,MAAM;GAAE,UAAU;GAAQ,SAAS;GAAO,CAAC;AACzE,MAAI,CAAC,OAAO,SAAS,OAAO,WAAW,EAAG,QAAO;SAC3C;AAIV,QAAO"}
@@ -1,76 +0,0 @@
1
- #!/usr/bin/env node
2
- //#region src/mcp/devtools-opener.ts
3
- /**
4
- * Assembles the Chii self-hosted DevTools inspector URL for a given relay
5
- * and target.
6
- *
7
- * Chii serves its own DevTools frontend at
8
- * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)
9
- * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form
10
- * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used
11
- * by Chii's own target list page (derived from `chii/public/index.js`).
12
- *
13
- * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
14
- * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =
15
- * 180–210 s). The developer must open the returned URL within that window.
16
- * If the window expires before the browser connects, the relay will reject the
17
- * WebSocket upgrade with close code 4401.
18
- *
19
- * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.
20
- * `undefined`), this function returns `null` — the caller must treat `null` as
21
- * "inspector not yet available" and show a waiting hint instead of a broken
22
- * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built
23
- * without `at=` would be rejected with WS 4401 immediately — there is no
24
- * non-TOTP relay path in production. Returning `null` surfaces this cleanly as
25
- * a "TOTP not yet configured" state rather than silently producing a URL that
26
- * will always fail at the WS handshake.
27
- *
28
- * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
29
- * embedded in the `wss=` parameter (inside the `at=` param) of the returned
30
- * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is
31
- * the intended fallback surface for the developer to copy the URL).
32
- *
33
- * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.
34
- * `http://127.0.0.1:9100`. No trailing slash.
35
- * @param targetId - Chii target id (from `GET <relay>/targets`).
36
- * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.
37
- * Called at most once. **Required** — when `undefined`, the function returns
38
- * `null` (fail-closed: no `at=` param means the relay WS gate rejects the
39
- * handshake, so a null result is safer than a URL that always 404s).
40
- * @param panel - Initial panel. Defaults to `"console"`.
41
- *
42
- * @returns The inspector URL string, or `null` when `mintTotp` is absent.
43
- *
44
- * @example
45
- * buildChiiInspectorUrl(
46
- * 'http://127.0.0.1:9100',
47
- * 'abc123',
48
- * () => generateTotp(secret),
49
- * )
50
- * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'
51
- */
52
- function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "console") {
53
- if (!mintTotp) return null;
54
- let relayHost;
55
- let wsParamName;
56
- try {
57
- const parsed = new URL(relayHttpBaseUrl);
58
- relayHost = parsed.host;
59
- wsParamName = parsed.protocol === "https:" ? "wss" : "ws";
60
- } catch {
61
- relayHost = relayHttpBaseUrl.replace(/^https?:\/\//i, "");
62
- wsParamName = /^https:/i.test(relayHttpBaseUrl) ? "wss" : "ws";
63
- }
64
- const clientId = `devtools-opener-${Date.now().toString(36)}`;
65
- const code = mintTotp();
66
- const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;
67
- const params = new URLSearchParams({
68
- [wsParamName]: wsPath,
69
- panel
70
- });
71
- return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
72
- }
73
- //#endregion
74
- export { buildChiiInspectorUrl };
75
-
76
- //# sourceMappingURL=devtools-opener-CJpEsXXQ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"devtools-opener-CJpEsXXQ.js","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4GA,SAAgB,sBACd,kBACA,UACA,UACA,QAAwD,WACzC;AAKf,KAAI,CAAC,SACH,QAAO;CAST,IAAI;CACJ,IAAI;AACJ,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,iBAAiB;AACxC,cAAY,OAAO;AACnB,gBAAc,OAAO,aAAa,WAAW,QAAQ;SAC/C;AAEN,cAAY,iBAAiB,QAAQ,iBAAiB,GAAG;AACzD,gBAAc,WAAW,KAAK,iBAAiB,GAAG,QAAQ;;CAK5D,MAAM,WAAW,mBAAmB,KAAK,KAAK,CAAC,SAAS,GAAG;CAO3D,MAAM,OAAO,UAAU;CACvB,MAAM,SAAS,GAAG,UAAU,UAAU,SAAS,UAAU,mBAAmB,SAAS,CAAC,MAAM,mBAAmB,KAAK;CAEpH,MAAM,SAAS,IAAI,gBAAgB;GAAG,cAAc;EAAQ;EAAO,CAAC;AACpE,QAAO,GAAG,iBAAiB,QAAQ,OAAO,GAAG,CAAC,2BAA2B,OAAO,UAAU"}
@@ -1,75 +0,0 @@
1
- //#region src/mcp/devtools-opener.ts
2
- /**
3
- * Assembles the Chii self-hosted DevTools inspector URL for a given relay
4
- * and target.
5
- *
6
- * Chii serves its own DevTools frontend at
7
- * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)
8
- * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form
9
- * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used
10
- * by Chii's own target list page (derived from `chii/public/index.js`).
11
- *
12
- * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
13
- * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =
14
- * 180–210 s). The developer must open the returned URL within that window.
15
- * If the window expires before the browser connects, the relay will reject the
16
- * WebSocket upgrade with close code 4401.
17
- *
18
- * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.
19
- * `undefined`), this function returns `null` — the caller must treat `null` as
20
- * "inspector not yet available" and show a waiting hint instead of a broken
21
- * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built
22
- * without `at=` would be rejected with WS 4401 immediately — there is no
23
- * non-TOTP relay path in production. Returning `null` surfaces this cleanly as
24
- * a "TOTP not yet configured" state rather than silently producing a URL that
25
- * will always fail at the WS handshake.
26
- *
27
- * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
28
- * embedded in the `wss=` parameter (inside the `at=` param) of the returned
29
- * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is
30
- * the intended fallback surface for the developer to copy the URL).
31
- *
32
- * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.
33
- * `http://127.0.0.1:9100`. No trailing slash.
34
- * @param targetId - Chii target id (from `GET <relay>/targets`).
35
- * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.
36
- * Called at most once. **Required** — when `undefined`, the function returns
37
- * `null` (fail-closed: no `at=` param means the relay WS gate rejects the
38
- * handshake, so a null result is safer than a URL that always 404s).
39
- * @param panel - Initial panel. Defaults to `"console"`.
40
- *
41
- * @returns The inspector URL string, or `null` when `mintTotp` is absent.
42
- *
43
- * @example
44
- * buildChiiInspectorUrl(
45
- * 'http://127.0.0.1:9100',
46
- * 'abc123',
47
- * () => generateTotp(secret),
48
- * )
49
- * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'
50
- */
51
- function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "console") {
52
- if (!mintTotp) return null;
53
- let relayHost;
54
- let wsParamName;
55
- try {
56
- const parsed = new URL(relayHttpBaseUrl);
57
- relayHost = parsed.host;
58
- wsParamName = parsed.protocol === "https:" ? "wss" : "ws";
59
- } catch {
60
- relayHost = relayHttpBaseUrl.replace(/^https?:\/\//i, "");
61
- wsParamName = /^https:/i.test(relayHttpBaseUrl) ? "wss" : "ws";
62
- }
63
- const clientId = `devtools-opener-${Date.now().toString(36)}`;
64
- const code = mintTotp();
65
- const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;
66
- const params = new URLSearchParams({
67
- [wsParamName]: wsPath,
68
- panel
69
- });
70
- return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
71
- }
72
- //#endregion
73
- export { buildChiiInspectorUrl };
74
-
75
- //# sourceMappingURL=devtools-opener-CxtryS8c.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"devtools-opener-CxtryS8c.js","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4GA,SAAgB,sBACd,kBACA,UACA,UACA,QAAwD,WACzC;AAKf,KAAI,CAAC,SACH,QAAO;CAST,IAAI;CACJ,IAAI;AACJ,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,iBAAiB;AACxC,cAAY,OAAO;AACnB,gBAAc,OAAO,aAAa,WAAW,QAAQ;SAC/C;AAEN,cAAY,iBAAiB,QAAQ,iBAAiB,GAAG;AACzD,gBAAc,WAAW,KAAK,iBAAiB,GAAG,QAAQ;;CAK5D,MAAM,WAAW,mBAAmB,KAAK,KAAK,CAAC,SAAS,GAAG;CAO3D,MAAM,OAAO,UAAU;CACvB,MAAM,SAAS,GAAG,UAAU,UAAU,SAAS,UAAU,mBAAmB,SAAS,CAAC,MAAM,mBAAmB,KAAK;CAEpH,MAAM,SAAS,IAAI,gBAAgB;GAAG,cAAc;EAAQ;EAAO,CAAC;AACpE,QAAO,GAAG,iBAAiB,QAAQ,OAAO,GAAG,CAAC,2BAA2B,OAAO,UAAU"}
@@ -1,68 +0,0 @@
1
- //#region src/mcp/devtools-opener.ts
2
- /**
3
- * Returns `true` when auto-open is **disabled**.
4
- *
5
- * Default (env var absent or any value other than `"1"`) is **disabled** —
6
- * the developer uses the "디버그 툴 열기" button on the /attach or dashboard
7
- * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic
8
- * browser-open behaviour on device attach.
9
- *
10
- * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward
11
- * compatibility (same effect as absent).
12
- */
13
- function isAutoDevtoolsDisabled() {
14
- return process.env.AIT_AUTO_DEVTOOLS !== "1";
15
- }
16
- /**
17
- * Opens the given URL in the OS default browser using a platform-appropriate
18
- * command. Returns `true` on success.
19
- *
20
- * Failures are silent from the caller's perspective — the caller should log
21
- * the URL to stderr as a fallback before calling this function.
22
- */
23
- function openUrlInBrowser(url) {
24
- if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === "1") return false;
25
- const { spawnSync } = require("node:child_process");
26
- const platform = process.platform;
27
- let candidates;
28
- if (platform === "darwin") candidates = [{
29
- cmd: "open",
30
- args: [url]
31
- }];
32
- else if (platform === "win32") candidates = [{
33
- cmd: "cmd",
34
- args: [
35
- "/c",
36
- "start",
37
- "",
38
- url
39
- ]
40
- }];
41
- else candidates = [
42
- {
43
- cmd: "xdg-open",
44
- args: [url]
45
- },
46
- {
47
- cmd: "sensible-browser",
48
- args: [url]
49
- },
50
- {
51
- cmd: "x-www-browser",
52
- args: [url]
53
- }
54
- ];
55
- for (const { cmd, args } of candidates) try {
56
- const result = spawnSync(cmd, args, {
57
- encoding: "utf8",
58
- timeout: 5e3
59
- });
60
- if (!result.error && result.status === 0) return true;
61
- } catch {}
62
- return false;
63
- }
64
- //#endregion
65
- exports.isAutoDevtoolsDisabled = isAutoDevtoolsDisabled;
66
- exports.openUrlInBrowser = openUrlInBrowser;
67
-
68
- //# sourceMappingURL=devtools-opener-iv1OwfJN.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"devtools-opener-iv1OwfJN.cjs","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;AA2KA,SAAgB,yBAAkC;AAChD,QAAO,QAAQ,IAAI,sBAAsB;;;;;;;;;AAc3C,SAAgB,iBAAiB,KAAsB;AAGrD,KAAI,QAAQ,IAAI,sCAAsC,IAAK,QAAO;CAElE,MAAM,EAAE,cAAc,QAAQ,qBAAqB;CACnD,MAAM,WAAW,QAAQ;CAGzB,IAAI;AACJ,KAAI,aAAa,SACf,cAAa,CAAC;EAAE,KAAK;EAAQ,MAAM,CAAC,IAAI;EAAE,CAAC;UAClC,aAAa,QACtB,cAAa,CAAC;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAI;EAAE,CAAC;KAG7D,cAAa;EACX;GAAE,KAAK;GAAY,MAAM,CAAC,IAAI;GAAE;EAChC;GAAE,KAAK;GAAoB,MAAM,CAAC,IAAI;GAAE;EACxC;GAAE,KAAK;GAAiB,MAAM,CAAC,IAAI;GAAE;EACtC;AAGH,MAAK,MAAM,EAAE,KAAK,UAAU,WAC1B,KAAI;EACF,MAAM,SAAS,UAAU,KAAK,MAAM;GAAE,UAAU;GAAQ,SAAS;GAAO,CAAC;AACzE,MAAI,CAAC,OAAO,SAAS,OAAO,WAAW,EAAG,QAAO;SAC3C;AAIV,QAAO"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"auto.d.ts","names":[],"sources":["../../src/in-app/auto.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;AA8IA;;;;;AAkCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAhGQ,MAAA;EAAA,UACI,MAAA;;;;;;;;;;IAUR,KAAA,GAAQ,MAAA;;;;;;;;;IAUR,SAAA,IACE,IAAA,aACG,IAAA,gBACA,OAAA;MAAU,EAAA;MAAa,KAAA;MAAiB,KAAA;IAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;iBAsCjC,eAAA,CAAA;;;;;;;;;;;;;;iBAkCA,cAAA,CACd,KAAA,YACA,SAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.d.ts","names":[],"sources":["../../src/mcp/cli.ts"],"mappings":";;;;;;;;AAoCS;;;;;AAST;;;;;AAKA;;;;;AA0BA;;;;;;;;KAxCK,IAAA;AAAA,KACA,MAAA;;;;;;;iBAQW,UAAA,CAAW,IAAA;;iBAKX,SAAA,CAAU,IAAA,sBAA0B,IAAA;;;;;;;;;iBA0BpC,WAAA,CAAY,IAAA,sBAA0B,MAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../../src/mcp/ait-source.ts","../../src/mcp/server.ts"],"mappings":";;;;;;;AAsBA;;;;;AAGA;;;;;;;;;;;;;KAHY,kBAAA;AAqBZ;AAAA,UAlBiB,UAAA;;EAEf,MAAA;EAiBiB;EAfjB,IAAA;EAwBsB;EAtBtB,SAAA;EAsByB;EApBzB,MAAA;EAuBe;EArBf,MAAA;;EAEA,KAAA;EAuBU;EArBV,QAAA,EAAU,kBAAA;AAAA;;UAIK,iBAAA;EACf,KAAA,EAAO,UAAA;AAAA;;;;;;;KASG,YAAA,GAAe,MAAA;;UAGV,yBAAA;EAW2C;EAT1D,WAAA;EAYuB;EAVvB,UAAA;AAAA;;UAIe,YAAA;EACf,uBAAA,EAAyB,iBAAA;EACzB,kBAAA,EAAoB,YAAA;EACpB,+BAAA,EAAiC,yBAAA;AAAA;AAAA,KAGvB,aAAA,SAAsB,YAAA;;;;;UAMjB,SAAA;EACf,GAAA,WAAc,aAAA,EAAe,MAAA,EAAQ,CAAA,GAAI,OAAA,CAAQ,YAAA,CAAa,CAAA;AAAA;;;UCyR/C,mBAAA;ED1RS;EC4RxB,SAAA,GAAY,SAAA;AAAA;;iBA2IE,eAAA,CAAgB,IAAA,GAAM,mBAAA,GAA2B,MAAA;;iBAqH3C,YAAA,CAAA,GAAgB,OAAA"}