@ait-co/devtools 0.1.71 → 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.
- package/dist/devtools-opener-BbUXBzgA.js.map +1 -1
- package/dist/devtools-opener-Bp671YXu.cjs.map +1 -1
- package/dist/devtools-opener-D84kZFtR.js.map +1 -1
- package/dist/devtools-opener-h6A-UjzC.cjs.map +1 -1
- package/dist/mcp/cli.js +31 -31
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +4 -4
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-JjGU81q7.js → qr-http-server-Bh9qkiRm.js} +5 -5
- package/dist/qr-http-server-Bh9qkiRm.js.map +1 -0
- package/dist/{qr-http-server-BIIMOcuU.cjs → qr-http-server-CG198dpc.cjs} +5 -5
- package/dist/qr-http-server-CG198dpc.cjs.map +1 -0
- package/dist/{qr-http-server-CeEzLS3g.js → qr-http-server-CwkTFbMV.js} +5 -5
- package/dist/qr-http-server-CwkTFbMV.js.map +1 -0
- package/dist/{qr-http-server-ClakYBO9.cjs → qr-http-server-DPOOrh6y.cjs} +5 -5
- package/dist/qr-http-server-DPOOrh6y.cjs.map +1 -0
- package/dist/{relay-secret-store-CLkF8Pa0.cjs → relay-secret-store-BFOEhsLO.cjs} +2 -2
- package/dist/{relay-secret-store-CLkF8Pa0.cjs.map → relay-secret-store-BFOEhsLO.cjs.map} +1 -1
- package/dist/{relay-secret-store-DBcKWUl9.js → relay-secret-store-C_LUxvAp.js} +2 -2
- package/dist/{relay-secret-store-DBcKWUl9.js.map → relay-secret-store-C_LUxvAp.js.map} +1 -1
- package/dist/{relay-secret-store-C4QQN5NA.js → relay-secret-store-D-W-WaSx.js} +2 -2
- package/dist/{relay-secret-store-C4QQN5NA.js.map → relay-secret-store-D-W-WaSx.js.map} +1 -1
- package/dist/{relay-url-store-COG2dSql.cjs → relay-url-store-2sy_l2bf.cjs} +2 -2
- package/dist/{relay-url-store-COG2dSql.cjs.map → relay-url-store-2sy_l2bf.cjs.map} +1 -1
- package/dist/{relay-url-store-WKfo0VQV.js → relay-url-store-DAh5KiJi.js} +2 -2
- package/dist/{relay-url-store-WKfo0VQV.js.map → relay-url-store-DAh5KiJi.js.map} +1 -1
- package/dist/{relay-url-store-Dq3vpd95.js → relay-url-store-DjKJJZ0d.js} +2 -2
- package/dist/{relay-url-store-Dq3vpd95.js.map → relay-url-store-DjKJJZ0d.js.map} +1 -1
- package/dist/{totp-D0a8VwoR.js → totp-BfVk8gQe.js} +6 -3
- package/dist/{totp-D0a8VwoR.js.map → totp-BfVk8gQe.js.map} +1 -1
- package/dist/totp-BxtxuEt4.js.map +1 -1
- package/dist/{totp-DA8vjAi7.cjs → totp-D4iTMA9U.cjs} +6 -3
- package/dist/{totp-DA8vjAi7.cjs.map → totp-D4iTMA9U.cjs.map} +1 -1
- package/dist/totp-D8f6qAEu.js +3 -0
- package/dist/totp-D9rndqg_.cjs.map +1 -1
- package/dist/{totp-BjtKFt88.js → totp-DbEfKQRi.js} +6 -3
- package/dist/{totp-BjtKFt88.js.map → totp-DbEfKQRi.js.map} +1 -1
- package/dist/{tunnel-DwVrcZ56.cjs → tunnel-BS6Td6f4.cjs} +3 -3
- package/dist/{tunnel-DwVrcZ56.cjs.map → tunnel-BS6Td6f4.cjs.map} +1 -1
- package/dist/{tunnel-aIy_7nWm.js → tunnel-CQdoKopR.js} +3 -3
- package/dist/{tunnel-aIy_7nWm.js.map → tunnel-CQdoKopR.js.map} +1 -1
- package/dist/unplugin/index.cjs +5 -5
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.js +5 -5
- package/dist/unplugin/index.js.map +1 -1
- package/dist/unplugin/tunnel.cjs +1 -1
- package/dist/unplugin/tunnel.js +1 -1
- package/package.json +1 -1
- package/dist/qr-http-server-BIIMOcuU.cjs.map +0 -1
- package/dist/qr-http-server-CeEzLS3g.js.map +0 -1
- package/dist/qr-http-server-ClakYBO9.cjs.map +0 -1
- package/dist/qr-http-server-JjGU81q7.js.map +0 -1
- package/dist/totp-CQFmgOhM.js +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-opener-BbUXBzgA.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 the 30-second RFC 6238\n * window (±1 step skew = 90 s acceptance). If the developer does not open the\n * URL within that window the WebSocket upgrade will be rejected with 4401.\n * In practice the browser opens immediately after the OS `open` command, so\n * the window is always satisfied; if it is not (e.g. the URL is copied and\n * opened later) the developer can copy the wss= param, replace `at=`, and\n * 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 the current 30-second RFC 6238 step (±1 step skew = 90 s acceptance\n * window). The developer must open the returned URL within that window. If\n * 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 the\n * current 30-second RFC 6238 step (±1 skew = 90 s). The developer must open\n * the URL within that window; if they miss it, reload the page or re-run\n * `open()` (though the once-per-session guard prevents that — restart the\n * 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= 코드는 30초 창 안에서만 유효합니다.\\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":";;;;;;;;;;AAuJA,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
|
+
{"version":3,"file":"devtools-opener-BbUXBzgA.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>` (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,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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-opener-Bp671YXu.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 the 30-second RFC 6238\n * window (±1 step skew = 90 s acceptance). If the developer does not open the\n * URL within that window the WebSocket upgrade will be rejected with 4401.\n * In practice the browser opens immediately after the OS `open` command, so\n * the window is always satisfied; if it is not (e.g. the URL is copied and\n * opened later) the developer can copy the wss= param, replace `at=`, and\n * 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 the current 30-second RFC 6238 step (±1 step skew = 90 s acceptance\n * window). The developer must open the returned URL within that window. If\n * 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 the\n * current 30-second RFC 6238 step (±1 skew = 90 s). The developer must open\n * the URL within that window; if they miss it, reload the page or re-run\n * `open()` (though the once-per-session guard prevents that — restart the\n * 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= 코드는 30초 창 안에서만 유효합니다.\\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":";;;;;;AAuJA,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-Bp671YXu.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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-opener-D84kZFtR.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 the 30-second RFC 6238\n * window (±1 step skew = 90 s acceptance). If the developer does not open the\n * URL within that window the WebSocket upgrade will be rejected with 4401.\n * In practice the browser opens immediately after the OS `open` command, so\n * the window is always satisfied; if it is not (e.g. the URL is copied and\n * opened later) the developer can copy the wss= param, replace `at=`, and\n * 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 the current 30-second RFC 6238 step (±1 step skew = 90 s acceptance\n * window). The developer must open the returned URL within that window. If\n * 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 the\n * current 30-second RFC 6238 step (±1 skew = 90 s). The developer must open\n * the URL within that window; if they miss it, reload the page or re-run\n * `open()` (though the once-per-session guard prevents that — restart the\n * 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= 코드는 30초 창 안에서만 유효합니다.\\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":";;;;;;;;;;AAuJA,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
|
+
{"version":3,"file":"devtools-opener-D84kZFtR.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>` (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,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 +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. 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 the 30-second RFC 6238\n * window (±1 step skew = 90 s acceptance). If the developer does not open the\n * URL within that window the WebSocket upgrade will be rejected with 4401.\n * In practice the browser opens immediately after the OS `open` command, so\n * the window is always satisfied; if it is not (e.g. the URL is copied and\n * opened later) the developer can copy the wss= param, replace `at=`, and\n * 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 the current 30-second RFC 6238 step (±1 step skew = 90 s acceptance\n * window). The developer must open the returned URL within that window. If\n * 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 the\n * current 30-second RFC 6238 step (±1 skew = 90 s). The developer must open\n * the URL within that window; if they miss it, reload the page or re-run\n * `open()` (though the once-per-session guard prevents that — restart the\n * 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= 코드는 30초 창 안에서만 유효합니다.\\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":";;;;;;AAuJA,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-
|
|
3
|
-
import { t as loadRelaySecretReadOnly } from "../relay-secret-store-
|
|
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";
|
|
@@ -1158,9 +1158,9 @@ function buildDeepLinkAttachUrl(schemeUrl, wssUrl, totpCode) {
|
|
|
1158
1158
|
* `chii/public/index.js`).
|
|
1159
1159
|
*
|
|
1160
1160
|
* The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
|
|
1161
|
-
* for
|
|
1162
|
-
*
|
|
1163
|
-
* the window expires before the browser connects, the relay will reject the
|
|
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
1164
|
* WebSocket upgrade with close code 4401.
|
|
1165
1165
|
*
|
|
1166
1166
|
* SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
|
|
@@ -1291,11 +1291,11 @@ var AutoDevtoolsOpener = class {
|
|
|
1291
1291
|
* Always writes the DevTools URL to stderr so the developer can copy it
|
|
1292
1292
|
* if the browser open fails or the popup is blocked.
|
|
1293
1293
|
*
|
|
1294
|
-
* TOTP expiry caveat: the `at=` code embedded in the URL is valid for
|
|
1295
|
-
*
|
|
1296
|
-
* the URL within that window; if they miss it, reload
|
|
1297
|
-
* `open()` (though the once-per-session guard prevents
|
|
1298
|
-
* MCP server if needed).
|
|
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
1299
|
*
|
|
1300
1300
|
* SECRET-HANDLING: the inspector URL (written to stderr) contains the relay
|
|
1301
1301
|
* host and a short-lived TOTP code. Do NOT write it to stdout or any
|
|
@@ -1311,7 +1311,7 @@ var AutoDevtoolsOpener = class {
|
|
|
1311
1311
|
const inspectorUrl = buildChiiInspectorUrl(options.relayHttpBaseUrl, options.targetId, options.mintTotp);
|
|
1312
1312
|
process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chii DevTools를 자동으로 엽니다.
|
|
1313
1313
|
[ait-debug] DevTools URL: ${inspectorUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
|
|
1314
|
-
[ait-debug] 주의: URL의 at= 코드는
|
|
1314
|
+
[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.
|
|
1315
1315
|
`);
|
|
1316
1316
|
if (!openUrlInBrowser(inspectorUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
|
|
1317
1317
|
}
|
|
@@ -2093,7 +2093,7 @@ const en = {
|
|
|
2093
2093
|
"attach.sandbox.step3": "The mini-app opens fullscreen and the debug session attaches automatically.",
|
|
2094
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",
|
|
2095
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",
|
|
2096
|
-
"attach.sandbox.faq.totp": "<strong>QR expired (TOTP
|
|
2096
|
+
"attach.sandbox.faq.totp": "<strong>QR expired (TOTP ~3 min)</strong> — scan a fresh QR code",
|
|
2097
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",
|
|
2098
2098
|
"attach.intoss.step1": "Open the Toss app.",
|
|
2099
2099
|
"attach.intoss.step2": "Scan the QR code with your phone camera app.",
|
|
@@ -2337,7 +2337,7 @@ const tables = {
|
|
|
2337
2337
|
"attach.sandbox.step3": "미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.",
|
|
2338
2338
|
"attach.sandbox.faq.notInstalled": "<strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요",
|
|
2339
2339
|
"attach.sandbox.faq.cameraApp": "<strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요",
|
|
2340
|
-
"attach.sandbox.faq.totp": "<strong>QR이 만료된 경우 (TOTP
|
|
2340
|
+
"attach.sandbox.faq.totp": "<strong>QR이 만료된 경우 (TOTP ~3분)</strong> — 새 QR을 다시 스캔하세요",
|
|
2341
2341
|
"attach.sandbox.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
|
|
2342
2342
|
"attach.intoss.step1": "토스 앱을 실행하세요.",
|
|
2343
2343
|
"attach.intoss.step2": "폰 카메라 앱으로 QR 코드를 스캔하세요.",
|
|
@@ -2514,7 +2514,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
2514
2514
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2515
2515
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2516
2516
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
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
|
|
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>`;
|
|
2518
2518
|
const attachChromeHtmlKoIntoss = `<!DOCTYPE html>
|
|
2519
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>
|
|
2520
2520
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -2666,7 +2666,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
2666
2666
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2667
2667
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2668
2668
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
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
|
|
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>`;
|
|
2670
2670
|
const attachChromeHtmlEnIntoss = `<!DOCTYPE html>
|
|
2671
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>
|
|
2672
2672
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -3552,7 +3552,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
3552
3552
|
},
|
|
3553
3553
|
{
|
|
3554
3554
|
name: "build_attach_url",
|
|
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>)
|
|
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.",
|
|
3556
3556
|
inputSchema: {
|
|
3557
3557
|
type: "object",
|
|
3558
3558
|
properties: {
|
|
@@ -3904,10 +3904,10 @@ function listPages(connection, tunnel) {
|
|
|
3904
3904
|
* relay URL to splice in) — the caller surfaces that as a tool error.
|
|
3905
3905
|
*
|
|
3906
3906
|
* When `AIT_DEBUG_TOTP_SECRET` is set, generates the current TOTP code and
|
|
3907
|
-
* splices it as `at=<code>` into the attach URL. The code is valid for
|
|
3908
|
-
*
|
|
3909
|
-
*
|
|
3910
|
-
* `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).
|
|
3911
3911
|
*
|
|
3912
3912
|
* Also validates the scheme URL's authority. A suspicious authority (empty,
|
|
3913
3913
|
* "web", "localhost", etc.) is surfaced as a non-fatal `authorityWarning` on
|
|
@@ -3935,10 +3935,10 @@ function buildAttachUrl(schemeUrl, tunnel, totpSecret) {
|
|
|
3935
3935
|
const now = Date.now();
|
|
3936
3936
|
totpCode = generateTotp(totpSecret, now);
|
|
3937
3937
|
const STEP_SECONDS = 30;
|
|
3938
|
-
const expiresAtMs =
|
|
3938
|
+
const expiresAtMs = now + 6 * STEP_SECONDS * 1e3;
|
|
3939
3939
|
totpMeta = {
|
|
3940
3940
|
enabled: true,
|
|
3941
|
-
ttlSeconds: STEP_SECONDS,
|
|
3941
|
+
ttlSeconds: 6 * STEP_SECONDS,
|
|
3942
3942
|
expiresAt: new Date(expiresAtMs).toISOString()
|
|
3943
3943
|
};
|
|
3944
3944
|
}
|
|
@@ -4566,7 +4566,7 @@ async function readMcpSdkVersion() {
|
|
|
4566
4566
|
* some test environments that skip the build step).
|
|
4567
4567
|
*/
|
|
4568
4568
|
function readDevtoolsVersion() {
|
|
4569
|
-
return "0.1.
|
|
4569
|
+
return "0.1.72";
|
|
4570
4570
|
}
|
|
4571
4571
|
/**
|
|
4572
4572
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -4602,7 +4602,7 @@ function computeNextRecommendedAction(tunnel, pages, env, authRejects = null) {
|
|
|
4602
4602
|
};
|
|
4603
4603
|
if (authRejects !== null && authRejects.count > 0 && pages !== null && pages.pages.length === 0) return {
|
|
4604
4604
|
tool: "build_attach_url",
|
|
4605
|
-
reason: `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? "unknown"}) — QR을 다시 스캔해 새 코드로 attach하세요(코드는
|
|
4605
|
+
reason: `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? "unknown"}) — QR을 다시 스캔해 새 코드로 attach하세요(코드는 ~3분마다 만료). 반복되면 폰 페이지 URL에 at 파라미터가 전달되는지(target-side TOTP 전달 경로)를 확인하세요`
|
|
4606
4606
|
};
|
|
4607
4607
|
if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
|
|
4608
4608
|
tool: "build_attach_url",
|
|
@@ -5070,7 +5070,7 @@ function createDebugServer(deps) {
|
|
|
5070
5070
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
5071
5071
|
const server = new Server({
|
|
5072
5072
|
name: "ait-debug",
|
|
5073
|
-
version: "0.1.
|
|
5073
|
+
version: "0.1.72"
|
|
5074
5074
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
5075
5075
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
5076
5076
|
const conn = router.active;
|
|
@@ -5149,7 +5149,7 @@ function createDebugServer(deps) {
|
|
|
5149
5149
|
const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
|
|
5150
5150
|
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
5151
5151
|
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
5152
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
5152
|
+
const { readRelayUrls } = await import("../relay-url-store-DjKJJZ0d.js");
|
|
5153
5153
|
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
5154
5154
|
}
|
|
5155
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 환경변수로 직접 전달하세요.");
|
|
@@ -5163,11 +5163,11 @@ function createDebugServer(deps) {
|
|
|
5163
5163
|
const now = Date.now();
|
|
5164
5164
|
totpCode = generateTotp(secret, now);
|
|
5165
5165
|
const STEP_SECONDS = 30;
|
|
5166
|
-
const
|
|
5166
|
+
const expiresAtMs = now + 6 * STEP_SECONDS * 1e3;
|
|
5167
5167
|
totpMeta = {
|
|
5168
5168
|
enabled: true,
|
|
5169
|
-
ttlSeconds: STEP_SECONDS,
|
|
5170
|
-
expiresAt:
|
|
5169
|
+
ttlSeconds: 6 * STEP_SECONDS,
|
|
5170
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
5171
5171
|
};
|
|
5172
5172
|
}
|
|
5173
5173
|
const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode);
|
|
@@ -5904,7 +5904,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
|
|
|
5904
5904
|
const envValue = typeof raw === "string" ? raw.trim() : "";
|
|
5905
5905
|
if (envValue !== "") return envValue;
|
|
5906
5906
|
if (projectRoot !== void 0) {
|
|
5907
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
5907
|
+
const { readRelayUrls } = await import("../relay-url-store-DjKJJZ0d.js");
|
|
5908
5908
|
const stored = await readRelayUrls({ projectRoot });
|
|
5909
5909
|
if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
|
|
5910
5910
|
}
|
|
@@ -6957,7 +6957,7 @@ function createDevServer(deps = {}) {
|
|
|
6957
6957
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
6958
6958
|
const server = new Server({
|
|
6959
6959
|
name: "ait-devtools",
|
|
6960
|
-
version: "0.1.
|
|
6960
|
+
version: "0.1.72"
|
|
6961
6961
|
}, { capabilities: { tools: {} } });
|
|
6962
6962
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
6963
6963
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|