@ait-co/devtools 0.1.70 → 0.1.72

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 (65) hide show
  1. package/dist/{chii-relay-D5Hc0G39.cjs → chii-relay-BcnVJBqm.cjs} +76 -3
  2. package/dist/chii-relay-BcnVJBqm.cjs.map +1 -0
  3. package/dist/{chii-relay-BcWDKbQ1.js → chii-relay-DSVG4Ui1.js} +76 -3
  4. package/dist/chii-relay-DSVG4Ui1.js.map +1 -0
  5. package/dist/devtools-opener-BbUXBzgA.js.map +1 -1
  6. package/dist/devtools-opener-Bp671YXu.cjs.map +1 -1
  7. package/dist/devtools-opener-D84kZFtR.js.map +1 -1
  8. package/dist/devtools-opener-h6A-UjzC.cjs.map +1 -1
  9. package/dist/mcp/cli.js +190 -62
  10. package/dist/mcp/cli.js.map +1 -1
  11. package/dist/mcp/server.js +2 -2
  12. package/dist/mcp/server.js.map +1 -1
  13. package/dist/mock/index.d.ts +24 -1
  14. package/dist/mock/index.d.ts.map +1 -1
  15. package/dist/mock/index.js +89 -1
  16. package/dist/mock/index.js.map +1 -1
  17. package/dist/panel/index.js +4 -4
  18. package/dist/panel/index.js.map +1 -1
  19. package/dist/{qr-http-server-JjGU81q7.js → qr-http-server-Bh9qkiRm.js} +5 -5
  20. package/dist/qr-http-server-Bh9qkiRm.js.map +1 -0
  21. package/dist/{qr-http-server-BIIMOcuU.cjs → qr-http-server-CG198dpc.cjs} +5 -5
  22. package/dist/qr-http-server-CG198dpc.cjs.map +1 -0
  23. package/dist/{qr-http-server-CeEzLS3g.js → qr-http-server-CwkTFbMV.js} +5 -5
  24. package/dist/qr-http-server-CwkTFbMV.js.map +1 -0
  25. package/dist/{qr-http-server-ClakYBO9.cjs → qr-http-server-DPOOrh6y.cjs} +5 -5
  26. package/dist/qr-http-server-DPOOrh6y.cjs.map +1 -0
  27. package/dist/{relay-secret-store-CLkF8Pa0.cjs → relay-secret-store-BFOEhsLO.cjs} +2 -2
  28. package/dist/{relay-secret-store-CLkF8Pa0.cjs.map → relay-secret-store-BFOEhsLO.cjs.map} +1 -1
  29. package/dist/{relay-secret-store-DBcKWUl9.js → relay-secret-store-C_LUxvAp.js} +2 -2
  30. package/dist/{relay-secret-store-DBcKWUl9.js.map → relay-secret-store-C_LUxvAp.js.map} +1 -1
  31. package/dist/{relay-secret-store-C4QQN5NA.js → relay-secret-store-D-W-WaSx.js} +2 -2
  32. package/dist/{relay-secret-store-C4QQN5NA.js.map → relay-secret-store-D-W-WaSx.js.map} +1 -1
  33. package/dist/{relay-url-store-COG2dSql.cjs → relay-url-store-2sy_l2bf.cjs} +2 -2
  34. package/dist/{relay-url-store-COG2dSql.cjs.map → relay-url-store-2sy_l2bf.cjs.map} +1 -1
  35. package/dist/{relay-url-store-WKfo0VQV.js → relay-url-store-DAh5KiJi.js} +2 -2
  36. package/dist/{relay-url-store-WKfo0VQV.js.map → relay-url-store-DAh5KiJi.js.map} +1 -1
  37. package/dist/{relay-url-store-Dq3vpd95.js → relay-url-store-DjKJJZ0d.js} +2 -2
  38. package/dist/{relay-url-store-Dq3vpd95.js.map → relay-url-store-DjKJJZ0d.js.map} +1 -1
  39. package/dist/{totp-D0a8VwoR.js → totp-BfVk8gQe.js} +6 -3
  40. package/dist/{totp-D0a8VwoR.js.map → totp-BfVk8gQe.js.map} +1 -1
  41. package/dist/totp-BxtxuEt4.js.map +1 -1
  42. package/dist/{totp-DA8vjAi7.cjs → totp-D4iTMA9U.cjs} +6 -3
  43. package/dist/{totp-DA8vjAi7.cjs.map → totp-D4iTMA9U.cjs.map} +1 -1
  44. package/dist/totp-D8f6qAEu.js +3 -0
  45. package/dist/totp-D9rndqg_.cjs.map +1 -1
  46. package/dist/{totp-BjtKFt88.js → totp-DbEfKQRi.js} +6 -3
  47. package/dist/{totp-BjtKFt88.js.map → totp-DbEfKQRi.js.map} +1 -1
  48. package/dist/{tunnel-DwVrcZ56.cjs → tunnel-BS6Td6f4.cjs} +3 -3
  49. package/dist/{tunnel-DwVrcZ56.cjs.map → tunnel-BS6Td6f4.cjs.map} +1 -1
  50. package/dist/{tunnel-aIy_7nWm.js → tunnel-CQdoKopR.js} +3 -3
  51. package/dist/{tunnel-aIy_7nWm.js.map → tunnel-CQdoKopR.js.map} +1 -1
  52. package/dist/unplugin/index.cjs +6 -6
  53. package/dist/unplugin/index.cjs.map +1 -1
  54. package/dist/unplugin/index.js +6 -6
  55. package/dist/unplugin/index.js.map +1 -1
  56. package/dist/unplugin/tunnel.cjs +1 -1
  57. package/dist/unplugin/tunnel.js +1 -1
  58. package/package.json +1 -1
  59. package/dist/chii-relay-BcWDKbQ1.js.map +0 -1
  60. package/dist/chii-relay-D5Hc0G39.cjs.map +0 -1
  61. package/dist/qr-http-server-BIIMOcuU.cjs.map +0 -1
  62. package/dist/qr-http-server-CeEzLS3g.js.map +0 -1
  63. package/dist/qr-http-server-ClakYBO9.cjs.map +0 -1
  64. package/dist/qr-http-server-JjGU81q7.js.map +0 -1
  65. package/dist/totp-CQFmgOhM.js +0 -3
@@ -1 +1 @@
1
- {"version":3,"file":"devtools-opener-h6A-UjzC.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. The\n * Chrome DevTools frontend can connect to any such endpoint via:\n *\n * https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html\n * ?wss=<host>[/<path>]\n * &panel=console\n *\n * Where `<host>` is the public WSS relay URL without the `wss://` scheme prefix\n * (the DevTools frontend adds it). This module assembles that URL and opens it\n * in the OS default browser so the developer immediately gets a full Chrome\n * DevTools UI.\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 * 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// Chrome DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Base URL for the Chrome DevTools inspector hosted on appspot.\n *\n * The `@` path segment is the \"latest / bleeding edge\" alias which tracks the\n * current Chrome stable CDP protocol version — compatible with the chobitsu-\n * based CDP that Chii injects. A specific commit hash may be pinned here if\n * a regression is observed.\n */\nconst DEVTOOLS_FRONTEND_BASE =\n 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html';\n\n// ---------------------------------------------------------------------------\n// URL assembly\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chrome DevTools inspector URL that connects to a Chii relay\n * WebSocket.\n *\n * The `wss=` parameter expects a host-and-path string without the `wss://`\n * scheme prefix — the DevTools frontend prepends it automatically.\n *\n * @param wssRelayUrl - Full `wss://` URL of the Chii relay (public tunnel).\n * Example: `wss://abc.trycloudflare.com`\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @example\n * buildChromeDevtoolsUrl('wss://abc.trycloudflare.com')\n * // → 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html?wss=abc.trycloudflare.com&panel=console'\n */\nexport function buildChromeDevtoolsUrl(\n wssRelayUrl: string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string {\n // Strip `wss://` prefix — the DevTools frontend expects host[/path] only.\n const wssParam = wssRelayUrl.replace(/^wss:\\/\\//i, '');\n const params = new URLSearchParams({ wss: wssParam, panel });\n return `${DEVTOOLS_FRONTEND_BASE}?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled** via the `AIT_AUTO_DEVTOOLS`\n * env var. Only the explicit `\"0\"` value disables it; anything else (including\n * absent) leaves auto-open enabled.\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS === '0';\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 * Manages auto-opening Chrome DevTools exactly once per relay attach session.\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onFirstAttach` callback to `startAttachWatcher`.\n *\n * The open fires at most once. Subsequent `open()` calls are no-ops.\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n private _opened = false;\n\n /**\n * Attempts to auto-open Chrome DevTools.\n *\n * No-op when any of the following conditions hold:\n * 1. Already opened this session (`_opened` is true).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. Environment is `mock` (env 1 — F12 is already available).\n * 4. `wssRelayUrl` is null/undefined/empty (tunnel not yet up).\n *\n * Always writes the DevTools URL to stderr so the developer can copy it\n * if the browser open fails or the popup is blocked.\n *\n * @param wssRelayUrl - The public `wss://` relay URL (from tunnel status).\n * @param env - Current MCP environment (`mock` | `relay`).\n */\n open(wssRelayUrl: string | null | undefined, env: McpEnvironment): void {\n if (this._opened) return;\n if (isAutoDevtoolsDisabled()) return;\n if (env === 'mock') return;\n if (!wssRelayUrl) return;\n\n this._opened = true;\n\n const devtoolsUrl = buildChromeDevtoolsUrl(wssRelayUrl);\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — Chrome DevTools를 자동으로 엽니다.\\n' +\n `[ait-debug] Chrome DevTools URL: ${devtoolsUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)\\n',\n );\n\n const opened = openUrlInBrowser(devtoolsUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /** Returns `true` if `open()` has passed all guards and fired once. */\n get opened(): boolean {\n return this._opened;\n }\n}\n"],"mappings":";;;;;;AAgGA,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
+ {"version":3,"file":"devtools-opener-h6A-UjzC.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>` (and optionally `&at=<totp>`) —\n * the same format used by Chii's own target list page (derived from\n * `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 * 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 - Optional function that returns a fresh 6-digit TOTP code\n * string. Called at most once. When omitted (TOTP disabled) no `at=` param\n * is added.\n * @param panel - Initial panel. Defaults to `\"console\"`.\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 {\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 let wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}`;\n\n if (mintTotp) {\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 wsPath += `&at=${encodeURIComponent(code)}`;\n }\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** via the `AIT_AUTO_DEVTOOLS`\n * env var. Only the explicit `\"0\"` value disables it; anything else (including\n * absent) leaves auto-open enabled.\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS === '0';\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 * 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.\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 *\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 exactly once per relay attach session.\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onFirstAttach` callback to `startAttachWatcher`.\n *\n * The open fires at most once. Subsequent `open()` calls are no-ops.\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n private _opened = false;\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Builds a `<relay-base>/front_end/chii_app.html?wss=…` URL pointing at the\n * attached target. A fresh TOTP `at=` code is minted at call time so the\n * relay's WebSocket upgrade gate accepts the connection.\n *\n * No-op when any of the following conditions hold:\n * 1. Already opened this session (`_opened` is true).\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.relayHttpBaseUrl` is null/undefined/empty (relay not up yet).\n * 5. `options.targetId` is null/undefined/empty (no page attached yet).\n *\n * Always writes the DevTools URL to stderr so the developer can copy it\n * if the browser open fails or the popup is blocked.\n *\n * TOTP expiry caveat: the `at=` code embedded in the URL is valid for ~3\n * minutes (relay gate ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). The\n * developer must open the URL within that window; if they miss it, reload\n * the page or re-run `open()` (though the once-per-session guard prevents\n * that — restart the MCP server if needed).\n *\n * SECRET-HANDLING: the inspector URL (written to stderr) contains the relay\n * host and a short-lived TOTP code. Do NOT write it to stdout or any\n * persistent log.\n */\n open(options: DevtoolsOpenOptions): void {\n if (this._opened) return;\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.relayHttpBaseUrl) return;\n if (!options.targetId) return;\n\n this._opened = true;\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n options.targetId,\n options.mintTotp,\n );\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — Chii DevTools를 자동으로 엽니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)\\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 /** Returns `true` if `open()` has passed all guards and fired once. */\n get opened(): boolean {\n return this._opened;\n }\n}\n"],"mappings":";;;;;;AAsJA,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"}
package/dist/mcp/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-D0a8VwoR.js";
3
- import { t as loadRelaySecretReadOnly } from "../relay-secret-store-DBcKWUl9.js";
2
+ import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-BfVk8gQe.js";
3
+ import { t as loadRelaySecretReadOnly } from "../relay-secret-store-C_LUxvAp.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { argv } from "node:process";
@@ -812,6 +812,66 @@ var ChiiCdpConnection = class {
812
812
  * predicate from the caller's perspective; this module only forwards pass/fail.
813
813
  */
814
814
  const require$1 = createRequire(import.meta.url);
815
+ /**
816
+ * WS keepalive ping interval (ms).
817
+ *
818
+ * Cloudflare proxied connections are dropped after ~100 s of no traffic.
819
+ * 45 s comfortably fits inside that window and lets both the phone-target leg
820
+ * and the daemon-client leg survive idle CDP sessions.
821
+ */
822
+ const DEFAULT_KEEPALIVE_INTERVAL_MS = 45e3;
823
+ /**
824
+ * Loads chii's internal WebSocketServer class and returns it together with a
825
+ * flag indicating whether the real class was found.
826
+ *
827
+ * Returns `null` if the internal path is not resolvable (future chii release
828
+ * changes the layout) — callers skip keepalive gracefully.
829
+ */
830
+ function tryLoadChiiWssClass() {
831
+ try {
832
+ const mod = require$1("chii/server/lib/WebSocketServer");
833
+ if (typeof mod === "function") return mod;
834
+ } catch {}
835
+ return null;
836
+ }
837
+ /**
838
+ * Calls `chii.start()` and returns the chii `WebSocketServer` instance that
839
+ * was constructed during the call.
840
+ *
841
+ * How: `chii/server/index.js`'s `start()` creates `new WebSocketServer()`
842
+ * where `WebSocketServer` is captured from `require('./lib/WebSocketServer')`
843
+ * at module load time. The class reference is stable, so we can temporarily
844
+ * patch `ChiiWssClass.prototype.start` — which runs *on the instance* —
845
+ * to record `this` before the original `start` runs.
846
+ *
847
+ * The patch is installed before `chii.start()` and removed (via `finally`)
848
+ * immediately after, so concurrent `startChiiRelay` calls nest correctly: each
849
+ * call's patch overrides the previous in the prototype chain for the duration
850
+ * of its own `chii.start()` call, restoring the prior descriptor on exit.
851
+ *
852
+ * If `ChiiWssClass` is null (internal path changed in a future chii release),
853
+ * `chii.start()` runs unpatched and the function returns null — callers skip
854
+ * keepalive gracefully without affecting relay correctness.
855
+ */
856
+ async function startChiiWithCapture(chii, startOptions, ChiiWssClass) {
857
+ if (ChiiWssClass === null) {
858
+ await chii.start(startOptions);
859
+ return null;
860
+ }
861
+ let captured = null;
862
+ const proto = ChiiWssClass.prototype;
863
+ const originalStart = proto.start;
864
+ proto.start = function(server) {
865
+ captured = this;
866
+ return originalStart.call(this, server);
867
+ };
868
+ try {
869
+ await chii.start(startOptions);
870
+ } finally {
871
+ proto.start = originalStart;
872
+ }
873
+ return captured;
874
+ }
815
875
  function loadChiiServer() {
816
876
  const mod = require$1("chii");
817
877
  if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
@@ -864,6 +924,7 @@ async function startChiiRelay(options = {}) {
864
924
  const requestedPort = options.port ?? 0;
865
925
  const host = options.host ?? "127.0.0.1";
866
926
  const { verifyAuth, onAuthReject } = options;
927
+ const keepaliveIntervalMs = options.keepaliveIntervalMs !== void 0 ? options.keepaliveIntervalMs : DEFAULT_KEEPALIVE_INTERVAL_MS;
867
928
  const httpServer = createServer();
868
929
  const notifyAuthReject = (kind) => {
869
930
  if (onAuthReject === void 0) return;
@@ -883,11 +944,12 @@ async function startChiiRelay(options = {}) {
883
944
  notifyAuthReject("http-request");
884
945
  }
885
946
  });
886
- await loadChiiServer().start({
947
+ const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;
948
+ const capturedChiiWss = await startChiiWithCapture(loadChiiServer(), {
887
949
  server: httpServer,
888
950
  domain: `${host}:${requestedPort}`,
889
951
  port: requestedPort
890
- });
952
+ }, chiiWssClass);
891
953
  if (verifyAuth) {
892
954
  const chiiUpgradeListeners = httpServer.listeners("upgrade");
893
955
  httpServer.removeAllListeners("upgrade");
@@ -912,10 +974,21 @@ async function startChiiRelay(options = {}) {
912
974
  resolve(httpServer.address().port);
913
975
  });
914
976
  });
977
+ let keepaliveHandle = null;
978
+ if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {
979
+ const chiiWss = capturedChiiWss;
980
+ keepaliveHandle = setInterval(() => {
981
+ for (const client of chiiWss._wss.clients) if (client.readyState === 1) client.ping();
982
+ }, keepaliveIntervalMs);
983
+ }
915
984
  return {
916
985
  port: actualPort,
917
986
  baseUrl: `http://${host}:${actualPort}`,
918
987
  close: () => new Promise((resolve) => {
988
+ if (keepaliveHandle !== null) {
989
+ clearInterval(keepaliveHandle);
990
+ keepaliveHandle = null;
991
+ }
919
992
  httpServer.close(() => resolve());
920
993
  })
921
994
  };
@@ -1074,35 +1147,65 @@ function buildDeepLinkAttachUrl(schemeUrl, wssUrl, totpCode) {
1074
1147
  //#endregion
1075
1148
  //#region src/mcp/devtools-opener.ts
1076
1149
  /**
1077
- * Base URL for the Chrome DevTools inspector hosted on appspot.
1078
- *
1079
- * The `@` path segment is the "latest / bleeding edge" alias which tracks the
1080
- * current Chrome stable CDP protocol version — compatible with the chobitsu-
1081
- * based CDP that Chii injects. A specific commit hash may be pinned here if
1082
- * a regression is observed.
1083
- */
1084
- const DEVTOOLS_FRONTEND_BASE = "https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html";
1085
- /**
1086
- * Assembles the Chrome DevTools inspector URL that connects to a Chii relay
1087
- * WebSocket.
1088
- *
1089
- * The `wss=` parameter expects a host-and-path string without the `wss://`
1090
- * scheme prefix the DevTools frontend prepends it automatically.
1091
- *
1092
- * @param wssRelayUrl - Full `wss://` URL of the Chii relay (public tunnel).
1093
- * Example: `wss://abc.trycloudflare.com`
1150
+ * Assembles the Chii self-hosted DevTools inspector URL for a given relay
1151
+ * and target.
1152
+ *
1153
+ * Chii serves its own DevTools frontend at
1154
+ * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)
1155
+ * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form
1156
+ * `<relay-host>/client/<uuid>?target=<id>` (and optionally `&at=<totp>`) —
1157
+ * the same format used by Chii's own target list page (derived from
1158
+ * `chii/public/index.js`).
1159
+ *
1160
+ * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
1161
+ * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =
1162
+ * 180–210 s). The developer must open the returned URL within that window.
1163
+ * If the window expires before the browser connects, the relay will reject the
1164
+ * WebSocket upgrade with close code 4401.
1165
+ *
1166
+ * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
1167
+ * embedded in the `wss=` parameter (inside the `at=` param) of the returned
1168
+ * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is
1169
+ * the intended fallback surface for the developer to copy the URL).
1170
+ *
1171
+ * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.
1172
+ * `http://127.0.0.1:9100`. No trailing slash.
1173
+ * @param targetId - Chii target id (from `GET <relay>/targets`).
1174
+ * @param mintTotp - Optional function that returns a fresh 6-digit TOTP code
1175
+ * string. Called at most once. When omitted (TOTP disabled) no `at=` param
1176
+ * is added.
1094
1177
  * @param panel - Initial panel. Defaults to `"console"`.
1095
1178
  *
1096
1179
  * @example
1097
- * buildChromeDevtoolsUrl('wss://abc.trycloudflare.com')
1098
- * // → 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html?wss=abc.trycloudflare.com&panel=console'
1099
- */
1100
- function buildChromeDevtoolsUrl(wssRelayUrl, panel = "console") {
1101
- const wssParam = wssRelayUrl.replace(/^wss:\/\//i, "");
1102
- return `${DEVTOOLS_FRONTEND_BASE}?${new URLSearchParams({
1103
- wss: wssParam,
1180
+ * buildChiiInspectorUrl(
1181
+ * 'http://127.0.0.1:9100',
1182
+ * 'abc123',
1183
+ * () => generateTotp(secret),
1184
+ * )
1185
+ * // → '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>'
1186
+ */
1187
+ function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "console") {
1188
+ let relayHost;
1189
+ let wsParamName;
1190
+ try {
1191
+ const parsed = new URL(relayHttpBaseUrl);
1192
+ relayHost = parsed.host;
1193
+ wsParamName = parsed.protocol === "https:" ? "wss" : "ws";
1194
+ } catch {
1195
+ relayHost = relayHttpBaseUrl.replace(/^https?:\/\//i, "");
1196
+ wsParamName = /^https:/i.test(relayHttpBaseUrl) ? "wss" : "ws";
1197
+ }
1198
+ const clientId = `devtools-opener-${Date.now().toString(36)}`;
1199
+ let wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}`;
1200
+ if (mintTotp) {
1201
+ const code = mintTotp();
1202
+ wsPath += `&at=${encodeURIComponent(code)}`;
1203
+ }
1204
+ const params = new URLSearchParams({
1205
+ [wsParamName]: wsPath,
1104
1206
  panel
1105
- }).toString()}`;
1207
+ });
1208
+ return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
1106
1209
  }
1107
1210
  /**
1108
1211
  * Returns `true` when auto-open is **disabled** via the `AIT_AUTO_DEVTOOLS`
@@ -1172,31 +1275,45 @@ function openUrlInBrowser(url) {
1172
1275
  var AutoDevtoolsOpener = class {
1173
1276
  _opened = false;
1174
1277
  /**
1175
- * Attempts to auto-open Chrome DevTools.
1278
+ * Attempts to auto-open Chii DevTools in the developer's browser.
1279
+ *
1280
+ * Builds a `<relay-base>/front_end/chii_app.html?wss=…` URL pointing at the
1281
+ * attached target. A fresh TOTP `at=` code is minted at call time so the
1282
+ * relay's WebSocket upgrade gate accepts the connection.
1176
1283
  *
1177
1284
  * No-op when any of the following conditions hold:
1178
1285
  * 1. Already opened this session (`_opened` is true).
1179
1286
  * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.
1180
- * 3. Environment is `mock` (env 1 — F12 is already available).
1181
- * 4. `wssRelayUrl` is null/undefined/empty (tunnel not yet up).
1287
+ * 3. `options.env` is `mock` (env 1 — F12 is already available).
1288
+ * 4. `options.relayHttpBaseUrl` is null/undefined/empty (relay not up yet).
1289
+ * 5. `options.targetId` is null/undefined/empty (no page attached yet).
1182
1290
  *
1183
1291
  * Always writes the DevTools URL to stderr so the developer can copy it
1184
1292
  * if the browser open fails or the popup is blocked.
1185
1293
  *
1186
- * @param wssRelayUrl - The public `wss://` relay URL (from tunnel status).
1187
- * @param env - Current MCP environment (`mock` | `relay`).
1294
+ * TOTP expiry caveat: the `at=` code embedded in the URL is valid for ~3
1295
+ * minutes (relay gate ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). The
1296
+ * developer must open the URL within that window; if they miss it, reload
1297
+ * the page or re-run `open()` (though the once-per-session guard prevents
1298
+ * that — restart the MCP server if needed).
1299
+ *
1300
+ * SECRET-HANDLING: the inspector URL (written to stderr) contains the relay
1301
+ * host and a short-lived TOTP code. Do NOT write it to stdout or any
1302
+ * persistent log.
1188
1303
  */
1189
- open(wssRelayUrl, env) {
1304
+ open(options) {
1190
1305
  if (this._opened) return;
1191
1306
  if (isAutoDevtoolsDisabled()) return;
1192
- if (env === "mock") return;
1193
- if (!wssRelayUrl) return;
1307
+ if (options.env === "mock") return;
1308
+ if (!options.relayHttpBaseUrl) return;
1309
+ if (!options.targetId) return;
1194
1310
  this._opened = true;
1195
- const devtoolsUrl = buildChromeDevtoolsUrl(wssRelayUrl);
1196
- process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chrome DevTools를 자동으로 엽니다.
1197
- [ait-debug] Chrome DevTools URL: ${devtoolsUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
1311
+ const inspectorUrl = buildChiiInspectorUrl(options.relayHttpBaseUrl, options.targetId, options.mintTotp);
1312
+ process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chii DevTools를 자동으로 엽니다.
1313
+ [ait-debug] DevTools URL: ${inspectorUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
1314
+ [ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.
1198
1315
  `);
1199
- if (!openUrlInBrowser(devtoolsUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
1316
+ if (!openUrlInBrowser(inspectorUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
1200
1317
  }
1201
1318
  /** Returns `true` if `open()` has passed all guards and fired once. */
1202
1319
  get opened() {
@@ -1976,7 +2093,7 @@ const en = {
1976
2093
  "attach.sandbox.step3": "The mini-app opens fullscreen and the debug session attaches automatically.",
1977
2094
  "attach.sandbox.faq.notInstalled": "<strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen",
1978
2095
  "attach.sandbox.faq.cameraApp": "<strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner",
1979
- "attach.sandbox.faq.totp": "<strong>QR expired (TOTP 30 s)</strong> — scan a fresh QR code",
2096
+ "attach.sandbox.faq.totp": "<strong>QR expired (TOTP ~3 min)</strong> — scan a fresh QR code",
1980
2097
  "attach.sandbox.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
1981
2098
  "attach.intoss.step1": "Open the Toss app.",
1982
2099
  "attach.intoss.step2": "Scan the QR code with your phone camera app.",
@@ -2220,7 +2337,7 @@ const tables = {
2220
2337
  "attach.sandbox.step3": "미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.",
2221
2338
  "attach.sandbox.faq.notInstalled": "<strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요",
2222
2339
  "attach.sandbox.faq.cameraApp": "<strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요",
2223
- "attach.sandbox.faq.totp": "<strong>QR이 만료된 경우 (TOTP 30초)</strong> — 새 QR을 다시 스캔하세요",
2340
+ "attach.sandbox.faq.totp": "<strong>QR이 만료된 경우 (TOTP ~3분)</strong> — 새 QR을 다시 스캔하세요",
2224
2341
  "attach.sandbox.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
2225
2342
  "attach.intoss.step1": "토스 앱을 실행하세요.",
2226
2343
  "attach.intoss.step2": "폰 카메라 앱으로 QR 코드를 스캔하세요.",
@@ -2397,7 +2514,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
2397
2514
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2398
2515
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2399
2516
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2400
- </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>"QR 카메라로 스캔"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP 30초)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section></body></html>`;
2517
+ </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>"QR 카메라로 스캔"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP ~3분)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section></body></html>`;
2401
2518
  const attachChromeHtmlKoIntoss = `<!DOCTYPE html>
2402
2519
  <html lang="ko"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="__QR_DATA_URL__"/><title>AIT 디버그 세션 — QR 스캔</title><style>
2403
2520
  *, *::before, *::after { box-sizing: border-box; }
@@ -2549,7 +2666,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
2549
2666
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2550
2667
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2551
2668
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2552
- </style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>"Scan QR with camera"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP 30 s)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section></body></html>`;
2669
+ </style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>"Scan QR with camera"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP ~3 min)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section></body></html>`;
2553
2670
  const attachChromeHtmlEnIntoss = `<!DOCTYPE html>
2554
2671
  <html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="__QR_DATA_URL__"/><title>AIT Debug Session — QR Scan</title><style>
2555
2672
  *, *::before, *::after { box-sizing: border-box; }
@@ -3435,7 +3552,7 @@ const DEBUG_TOOL_DEFINITIONS = [
3435
3552
  },
3436
3553
  {
3437
3554
  name: "build_attach_url",
3438
- description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). \n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>) the URL is single-use for that 30-second step. The response includes a `totp` field with `expiresAt` (ISO timestamp). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh one-time URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
3555
+ description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). \n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
3439
3556
  inputSchema: {
3440
3557
  type: "object",
3441
3558
  properties: {
@@ -3787,10 +3904,10 @@ function listPages(connection, tunnel) {
3787
3904
  * relay URL to splice in) — the caller surfaces that as a tool error.
3788
3905
  *
3789
3906
  * When `AIT_DEBUG_TOTP_SECRET` is set, generates the current TOTP code and
3790
- * splices it as `at=<code>` into the attach URL. The code is valid for one
3791
- * 30-second time step (±1 skew accepted by the relay, so the effective window
3792
- * is up to 90 s). If the scan happens after `totp.expiresAt`, call
3793
- * `build_attach_url` again to get a fresh code.
3907
+ * splices it as `at=<code>` into the attach URL. The code is valid for ~3
3908
+ * minutes (the relay gate uses {@link RELAY_VERIFY_SKEW_STEPS}=6, accepting
3909
+ * past 6 steps = 180–210 s backwards from issuance). If the scan happens after
3910
+ * `totp.expiresAt`, call `build_attach_url` again to get a fresh code (#490).
3794
3911
  *
3795
3912
  * Also validates the scheme URL's authority. A suspicious authority (empty,
3796
3913
  * "web", "localhost", etc.) is surfaced as a non-fatal `authorityWarning` on
@@ -3818,10 +3935,10 @@ function buildAttachUrl(schemeUrl, tunnel, totpSecret) {
3818
3935
  const now = Date.now();
3819
3936
  totpCode = generateTotp(totpSecret, now);
3820
3937
  const STEP_SECONDS = 30;
3821
- const expiresAtMs = (Math.floor(now / 1e3 / STEP_SECONDS) + 1) * STEP_SECONDS * 1e3;
3938
+ const expiresAtMs = now + 6 * STEP_SECONDS * 1e3;
3822
3939
  totpMeta = {
3823
3940
  enabled: true,
3824
- ttlSeconds: STEP_SECONDS,
3941
+ ttlSeconds: 6 * STEP_SECONDS,
3825
3942
  expiresAt: new Date(expiresAtMs).toISOString()
3826
3943
  };
3827
3944
  }
@@ -4449,7 +4566,7 @@ async function readMcpSdkVersion() {
4449
4566
  * some test environments that skip the build step).
4450
4567
  */
4451
4568
  function readDevtoolsVersion() {
4452
- return "0.1.70";
4569
+ return "0.1.72";
4453
4570
  }
4454
4571
  /**
4455
4572
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -4485,7 +4602,7 @@ function computeNextRecommendedAction(tunnel, pages, env, authRejects = null) {
4485
4602
  };
4486
4603
  if (authRejects !== null && authRejects.count > 0 && pages !== null && pages.pages.length === 0) return {
4487
4604
  tool: "build_attach_url",
4488
- reason: `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? "unknown"}) — QR을 다시 스캔해 새 코드로 attach하세요(코드는 30초 주기로 만료). 반복되면 폰 페이지 URL에 at 파라미터가 전달되는지(target-side TOTP 전달 경로)를 확인하세요`
4605
+ reason: `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? "unknown"}) — QR을 다시 스캔해 새 코드로 attach하세요(코드는 ~3분마다 만료). 반복되면 폰 페이지 URL에 at 파라미터가 전달되는지(target-side TOTP 전달 경로)를 확인하세요`
4489
4606
  };
4490
4607
  if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
4491
4608
  tool: "build_attach_url",
@@ -4953,7 +5070,7 @@ function createDebugServer(deps) {
4953
5070
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
4954
5071
  const server = new Server({
4955
5072
  name: "ait-debug",
4956
- version: "0.1.70"
5073
+ version: "0.1.72"
4957
5074
  }, { capabilities: { tools: { listChanged: true } } });
4958
5075
  server.setRequestHandler(ListToolsRequestSchema, () => {
4959
5076
  const conn = router.active;
@@ -5032,7 +5149,7 @@ function createDebugServer(deps) {
5032
5149
  const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
5033
5150
  let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
5034
5151
  if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
5035
- const { readRelayUrls } = await import("../relay-url-store-Dq3vpd95.js");
5152
+ const { readRelayUrls } = await import("../relay-url-store-DjKJJZ0d.js");
5036
5153
  tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
5037
5154
  }
5038
5155
  if (tunnelHttpUrl === "") return mcpError("build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.");
@@ -5046,11 +5163,11 @@ function createDebugServer(deps) {
5046
5163
  const now = Date.now();
5047
5164
  totpCode = generateTotp(secret, now);
5048
5165
  const STEP_SECONDS = 30;
5049
- const currentStep = Math.floor(now / 1e3 / STEP_SECONDS);
5166
+ const expiresAtMs = now + 6 * STEP_SECONDS * 1e3;
5050
5167
  totpMeta = {
5051
5168
  enabled: true,
5052
- ttlSeconds: STEP_SECONDS,
5053
- expiresAt: (/* @__PURE__ */ new Date((currentStep + 1) * STEP_SECONDS * 1e3)).toISOString()
5169
+ ttlSeconds: 6 * STEP_SECONDS,
5170
+ expiresAt: new Date(expiresAtMs).toISOString()
5054
5171
  };
5055
5172
  }
5056
5173
  const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode);
@@ -5709,6 +5826,7 @@ async function bootRelayFamily(options = {}) {
5709
5826
  return {
5710
5827
  connection,
5711
5828
  relayOrigin: "intoss-webview",
5829
+ relayHttpUrl: relay.baseUrl,
5712
5830
  getTunnelStatus: () => tunnelStatus,
5713
5831
  stop() {
5714
5832
  tunnelProbe?.stop();
@@ -5745,6 +5863,7 @@ async function bootExternalRelayFamily(relayBaseUrl) {
5745
5863
  return {
5746
5864
  connection,
5747
5865
  relayOrigin: "external-pwa",
5866
+ relayHttpUrl: relayBaseUrl,
5748
5867
  getTunnelStatus: () => tunnelStatus,
5749
5868
  stop() {
5750
5869
  connection.close();
@@ -5785,7 +5904,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
5785
5904
  const envValue = typeof raw === "string" ? raw.trim() : "";
5786
5905
  if (envValue !== "") return envValue;
5787
5906
  if (projectRoot !== void 0) {
5788
- const { readRelayUrls } = await import("../relay-url-store-Dq3vpd95.js");
5907
+ const { readRelayUrls } = await import("../relay-url-store-DjKJJZ0d.js");
5789
5908
  const stored = await readRelayUrls({ projectRoot });
5790
5909
  if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
5791
5910
  }
@@ -5911,7 +6030,16 @@ var DualConnectionRouter = class {
5911
6030
  this.attachWatcher = startAttachWatcher(activeFamily.connection, server, this.deps.attachWatcherIntervalMs ?? 1e3, () => {
5912
6031
  this.deps.diagnosticsCollector.recordAttach();
5913
6032
  this.deps.onPageAttach?.();
5914
- if (activeFamily.connection.kind === "relay") this.deps.devtoolsOpener.open(this.relayTunnelStatus().wssUrl, deriveEnvironment(activeFamily.connection.kind, getLiveIntent(), activeFamily.relayOrigin));
6033
+ if (activeFamily.connection.kind === "relay") {
6034
+ const firstTarget = activeFamily.connection.listTargets()[0];
6035
+ const env = deriveEnvironment(activeFamily.connection.kind, getLiveIntent(), activeFamily.relayOrigin);
6036
+ this.deps.devtoolsOpener.open({
6037
+ relayHttpBaseUrl: activeFamily.relayHttpUrl,
6038
+ targetId: firstTarget?.id,
6039
+ mintTotp: process.env.AIT_DEBUG_TOTP_SECRET ? () => generateTotp(process.env.AIT_DEBUG_TOTP_SECRET) : void 0,
6040
+ env
6041
+ });
6042
+ }
5915
6043
  });
5916
6044
  }
5917
6045
  /**
@@ -6829,7 +6957,7 @@ function createDevServer(deps = {}) {
6829
6957
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
6830
6958
  const server = new Server({
6831
6959
  name: "ait-devtools",
6832
- version: "0.1.70"
6960
+ version: "0.1.72"
6833
6961
  }, { capabilities: { tools: {} } });
6834
6962
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
6835
6963
  server.setRequestHandler(CallToolRequestSchema, async (request) => {