@ait-co/devtools 0.1.70 → 0.1.71

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.
@@ -1 +1 @@
1
- {"version":3,"file":"devtools-opener-h6A-UjzC.cjs","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. The\n * Chrome DevTools frontend can connect to any such endpoint via:\n *\n * https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html\n * ?wss=<host>[/<path>]\n * &panel=console\n *\n * Where `<host>` is the public WSS relay URL without the `wss://` scheme prefix\n * (the DevTools frontend adds it). This module assembles that URL and opens it\n * in the OS default browser so the developer immediately gets a full Chrome\n * DevTools UI.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chrome DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Base URL for the Chrome DevTools inspector hosted on appspot.\n *\n * The `@` path segment is the \"latest / bleeding edge\" alias which tracks the\n * current Chrome stable CDP protocol version — compatible with the chobitsu-\n * based CDP that Chii injects. A specific commit hash may be pinned here if\n * a regression is observed.\n */\nconst DEVTOOLS_FRONTEND_BASE =\n 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html';\n\n// ---------------------------------------------------------------------------\n// URL assembly\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chrome DevTools inspector URL that connects to a Chii relay\n * WebSocket.\n *\n * The `wss=` parameter expects a host-and-path string without the `wss://`\n * scheme prefix — the DevTools frontend prepends it automatically.\n *\n * @param wssRelayUrl - Full `wss://` URL of the Chii relay (public tunnel).\n * Example: `wss://abc.trycloudflare.com`\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @example\n * buildChromeDevtoolsUrl('wss://abc.trycloudflare.com')\n * // → 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html?wss=abc.trycloudflare.com&panel=console'\n */\nexport function buildChromeDevtoolsUrl(\n wssRelayUrl: string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string {\n // Strip `wss://` prefix — the DevTools frontend expects host[/path] only.\n const wssParam = wssRelayUrl.replace(/^wss:\\/\\//i, '');\n const params = new URLSearchParams({ wss: wssParam, panel });\n return `${DEVTOOLS_FRONTEND_BASE}?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled** via the `AIT_AUTO_DEVTOOLS`\n * env var. Only the explicit `\"0\"` value disables it; anything else (including\n * absent) leaves auto-open enabled.\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS === '0';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Manages auto-opening Chrome DevTools exactly once per relay attach session.\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onFirstAttach` callback to `startAttachWatcher`.\n *\n * The open fires at most once. Subsequent `open()` calls are no-ops.\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n private _opened = false;\n\n /**\n * Attempts to auto-open Chrome DevTools.\n *\n * No-op when any of the following conditions hold:\n * 1. Already opened this session (`_opened` is true).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. Environment is `mock` (env 1 — F12 is already available).\n * 4. `wssRelayUrl` is null/undefined/empty (tunnel not yet up).\n *\n * Always writes the DevTools URL to stderr so the developer can copy it\n * if the browser open fails or the popup is blocked.\n *\n * @param wssRelayUrl - The public `wss://` relay URL (from tunnel status).\n * @param env - Current MCP environment (`mock` | `relay`).\n */\n open(wssRelayUrl: string | null | undefined, env: McpEnvironment): void {\n if (this._opened) return;\n if (isAutoDevtoolsDisabled()) return;\n if (env === 'mock') return;\n if (!wssRelayUrl) return;\n\n this._opened = true;\n\n const devtoolsUrl = buildChromeDevtoolsUrl(wssRelayUrl);\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — Chrome DevTools를 자동으로 엽니다.\\n' +\n `[ait-debug] Chrome DevTools URL: ${devtoolsUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)\\n',\n );\n\n const opened = openUrlInBrowser(devtoolsUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /** Returns `true` if `open()` has passed all guards and fired once. */\n get opened(): boolean {\n return this._opened;\n }\n}\n"],"mappings":";;;;;;AAgGA,SAAgB,yBAAkC;AAChD,QAAO,QAAQ,IAAI,sBAAsB;;;;;;;;;AAc3C,SAAgB,iBAAiB,KAAsB;AAGrD,KAAI,QAAQ,IAAI,sCAAsC,IAAK,QAAO;CAElE,MAAM,EAAE,cAAc,QAAQ,qBAAqB;CACnD,MAAM,WAAW,QAAQ;CAGzB,IAAI;AACJ,KAAI,aAAa,SACf,cAAa,CAAC;EAAE,KAAK;EAAQ,MAAM,CAAC,IAAI;EAAE,CAAC;UAClC,aAAa,QACtB,cAAa,CAAC;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAI;EAAE,CAAC;KAG7D,cAAa;EACX;GAAE,KAAK;GAAY,MAAM,CAAC,IAAI;GAAE;EAChC;GAAE,KAAK;GAAoB,MAAM,CAAC,IAAI;GAAE;EACxC;GAAE,KAAK;GAAiB,MAAM,CAAC,IAAI;GAAE;EACtC;AAGH,MAAK,MAAM,EAAE,KAAK,UAAU,WAC1B,KAAI;EACF,MAAM,SAAS,UAAU,KAAK,MAAM;GAAE,UAAU;GAAQ,SAAS;GAAO,CAAC;AACzE,MAAI,CAAC,OAAO,SAAS,OAAO,WAAW,EAAG,QAAO;SAC3C;AAIV,QAAO"}
1
+ {"version":3,"file":"devtools-opener-h6A-UjzC.cjs","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for 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"}
package/dist/mcp/cli.js CHANGED
@@ -812,6 +812,66 @@ var ChiiCdpConnection = class {
812
812
  * predicate from the caller's perspective; this module only forwards pass/fail.
813
813
  */
814
814
  const require$1 = createRequire(import.meta.url);
815
+ /**
816
+ * WS keepalive ping interval (ms).
817
+ *
818
+ * Cloudflare proxied connections are dropped after ~100 s of no traffic.
819
+ * 45 s comfortably fits inside that window and lets both the phone-target leg
820
+ * and the daemon-client leg survive idle CDP sessions.
821
+ */
822
+ const DEFAULT_KEEPALIVE_INTERVAL_MS = 45e3;
823
+ /**
824
+ * Loads chii's internal WebSocketServer class and returns it together with a
825
+ * flag indicating whether the real class was found.
826
+ *
827
+ * Returns `null` if the internal path is not resolvable (future chii release
828
+ * changes the layout) — callers skip keepalive gracefully.
829
+ */
830
+ function tryLoadChiiWssClass() {
831
+ try {
832
+ const mod = require$1("chii/server/lib/WebSocketServer");
833
+ if (typeof mod === "function") return mod;
834
+ } catch {}
835
+ return null;
836
+ }
837
+ /**
838
+ * Calls `chii.start()` and returns the chii `WebSocketServer` instance that
839
+ * was constructed during the call.
840
+ *
841
+ * How: `chii/server/index.js`'s `start()` creates `new WebSocketServer()`
842
+ * where `WebSocketServer` is captured from `require('./lib/WebSocketServer')`
843
+ * at module load time. The class reference is stable, so we can temporarily
844
+ * patch `ChiiWssClass.prototype.start` — which runs *on the instance* —
845
+ * to record `this` before the original `start` runs.
846
+ *
847
+ * The patch is installed before `chii.start()` and removed (via `finally`)
848
+ * immediately after, so concurrent `startChiiRelay` calls nest correctly: each
849
+ * call's patch overrides the previous in the prototype chain for the duration
850
+ * of its own `chii.start()` call, restoring the prior descriptor on exit.
851
+ *
852
+ * If `ChiiWssClass` is null (internal path changed in a future chii release),
853
+ * `chii.start()` runs unpatched and the function returns null — callers skip
854
+ * keepalive gracefully without affecting relay correctness.
855
+ */
856
+ async function startChiiWithCapture(chii, startOptions, ChiiWssClass) {
857
+ if (ChiiWssClass === null) {
858
+ await chii.start(startOptions);
859
+ return null;
860
+ }
861
+ let captured = null;
862
+ const proto = ChiiWssClass.prototype;
863
+ const originalStart = proto.start;
864
+ proto.start = function(server) {
865
+ captured = this;
866
+ return originalStart.call(this, server);
867
+ };
868
+ try {
869
+ await chii.start(startOptions);
870
+ } finally {
871
+ proto.start = originalStart;
872
+ }
873
+ return captured;
874
+ }
815
875
  function loadChiiServer() {
816
876
  const mod = require$1("chii");
817
877
  if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
@@ -864,6 +924,7 @@ async function startChiiRelay(options = {}) {
864
924
  const requestedPort = options.port ?? 0;
865
925
  const host = options.host ?? "127.0.0.1";
866
926
  const { verifyAuth, onAuthReject } = options;
927
+ const keepaliveIntervalMs = options.keepaliveIntervalMs !== void 0 ? options.keepaliveIntervalMs : DEFAULT_KEEPALIVE_INTERVAL_MS;
867
928
  const httpServer = createServer();
868
929
  const notifyAuthReject = (kind) => {
869
930
  if (onAuthReject === void 0) return;
@@ -883,11 +944,12 @@ async function startChiiRelay(options = {}) {
883
944
  notifyAuthReject("http-request");
884
945
  }
885
946
  });
886
- await loadChiiServer().start({
947
+ const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;
948
+ const capturedChiiWss = await startChiiWithCapture(loadChiiServer(), {
887
949
  server: httpServer,
888
950
  domain: `${host}:${requestedPort}`,
889
951
  port: requestedPort
890
- });
952
+ }, chiiWssClass);
891
953
  if (verifyAuth) {
892
954
  const chiiUpgradeListeners = httpServer.listeners("upgrade");
893
955
  httpServer.removeAllListeners("upgrade");
@@ -912,10 +974,21 @@ async function startChiiRelay(options = {}) {
912
974
  resolve(httpServer.address().port);
913
975
  });
914
976
  });
977
+ let keepaliveHandle = null;
978
+ if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {
979
+ const chiiWss = capturedChiiWss;
980
+ keepaliveHandle = setInterval(() => {
981
+ for (const client of chiiWss._wss.clients) if (client.readyState === 1) client.ping();
982
+ }, keepaliveIntervalMs);
983
+ }
915
984
  return {
916
985
  port: actualPort,
917
986
  baseUrl: `http://${host}:${actualPort}`,
918
987
  close: () => new Promise((resolve) => {
988
+ if (keepaliveHandle !== null) {
989
+ clearInterval(keepaliveHandle);
990
+ keepaliveHandle = null;
991
+ }
919
992
  httpServer.close(() => resolve());
920
993
  })
921
994
  };
@@ -1074,35 +1147,65 @@ function buildDeepLinkAttachUrl(schemeUrl, wssUrl, totpCode) {
1074
1147
  //#endregion
1075
1148
  //#region src/mcp/devtools-opener.ts
1076
1149
  /**
1077
- * Base URL for the Chrome DevTools inspector hosted on appspot.
1078
- *
1079
- * The `@` path segment is the "latest / bleeding edge" alias which tracks the
1080
- * current Chrome stable CDP protocol version — compatible with the chobitsu-
1081
- * based CDP that Chii injects. A specific commit hash may be pinned here if
1082
- * a regression is observed.
1083
- */
1084
- const DEVTOOLS_FRONTEND_BASE = "https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html";
1085
- /**
1086
- * Assembles the Chrome DevTools inspector URL that connects to a Chii relay
1087
- * WebSocket.
1088
- *
1089
- * The `wss=` parameter expects a host-and-path string without the `wss://`
1090
- * scheme prefix the DevTools frontend prepends it automatically.
1091
- *
1092
- * @param wssRelayUrl - Full `wss://` URL of the Chii relay (public tunnel).
1093
- * Example: `wss://abc.trycloudflare.com`
1150
+ * Assembles the Chii self-hosted DevTools inspector URL for a given relay
1151
+ * and target.
1152
+ *
1153
+ * Chii serves its own DevTools frontend at
1154
+ * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)
1155
+ * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form
1156
+ * `<relay-host>/client/<uuid>?target=<id>` (and optionally `&at=<totp>`) —
1157
+ * the same format used by Chii's own target list page (derived from
1158
+ * `chii/public/index.js`).
1159
+ *
1160
+ * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
1161
+ * for the current 30-second RFC 6238 step (±1 step skew = 90 s acceptance
1162
+ * window). The developer must open the returned URL within that window. If
1163
+ * the window expires before the browser connects, the relay will reject the
1164
+ * WebSocket upgrade with close code 4401.
1165
+ *
1166
+ * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
1167
+ * embedded in the `wss=` parameter (inside the `at=` param) of the returned
1168
+ * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is
1169
+ * the intended fallback surface for the developer to copy the URL).
1170
+ *
1171
+ * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.
1172
+ * `http://127.0.0.1:9100`. No trailing slash.
1173
+ * @param targetId - Chii target id (from `GET <relay>/targets`).
1174
+ * @param mintTotp - Optional function that returns a fresh 6-digit TOTP code
1175
+ * string. Called at most once. When omitted (TOTP disabled) no `at=` param
1176
+ * is added.
1094
1177
  * @param panel - Initial panel. Defaults to `"console"`.
1095
1178
  *
1096
1179
  * @example
1097
- * buildChromeDevtoolsUrl('wss://abc.trycloudflare.com')
1098
- * // → 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html?wss=abc.trycloudflare.com&panel=console'
1099
- */
1100
- function buildChromeDevtoolsUrl(wssRelayUrl, panel = "console") {
1101
- const wssParam = wssRelayUrl.replace(/^wss:\/\//i, "");
1102
- return `${DEVTOOLS_FRONTEND_BASE}?${new URLSearchParams({
1103
- wss: wssParam,
1180
+ * buildChiiInspectorUrl(
1181
+ * 'http://127.0.0.1:9100',
1182
+ * 'abc123',
1183
+ * () => generateTotp(secret),
1184
+ * )
1185
+ * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'
1186
+ */
1187
+ function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "console") {
1188
+ let relayHost;
1189
+ let wsParamName;
1190
+ try {
1191
+ const parsed = new URL(relayHttpBaseUrl);
1192
+ relayHost = parsed.host;
1193
+ wsParamName = parsed.protocol === "https:" ? "wss" : "ws";
1194
+ } catch {
1195
+ relayHost = relayHttpBaseUrl.replace(/^https?:\/\//i, "");
1196
+ wsParamName = /^https:/i.test(relayHttpBaseUrl) ? "wss" : "ws";
1197
+ }
1198
+ const clientId = `devtools-opener-${Date.now().toString(36)}`;
1199
+ let wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}`;
1200
+ if (mintTotp) {
1201
+ const code = mintTotp();
1202
+ wsPath += `&at=${encodeURIComponent(code)}`;
1203
+ }
1204
+ const params = new URLSearchParams({
1205
+ [wsParamName]: wsPath,
1104
1206
  panel
1105
- }).toString()}`;
1207
+ });
1208
+ return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
1106
1209
  }
1107
1210
  /**
1108
1211
  * Returns `true` when auto-open is **disabled** via the `AIT_AUTO_DEVTOOLS`
@@ -1172,31 +1275,45 @@ function openUrlInBrowser(url) {
1172
1275
  var AutoDevtoolsOpener = class {
1173
1276
  _opened = false;
1174
1277
  /**
1175
- * Attempts to auto-open Chrome DevTools.
1278
+ * Attempts to auto-open Chii DevTools in the developer's browser.
1279
+ *
1280
+ * Builds a `<relay-base>/front_end/chii_app.html?wss=…` URL pointing at the
1281
+ * attached target. A fresh TOTP `at=` code is minted at call time so the
1282
+ * relay's WebSocket upgrade gate accepts the connection.
1176
1283
  *
1177
1284
  * No-op when any of the following conditions hold:
1178
1285
  * 1. Already opened this session (`_opened` is true).
1179
1286
  * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.
1180
- * 3. Environment is `mock` (env 1 — F12 is already available).
1181
- * 4. `wssRelayUrl` is null/undefined/empty (tunnel not yet up).
1287
+ * 3. `options.env` is `mock` (env 1 — F12 is already available).
1288
+ * 4. `options.relayHttpBaseUrl` is null/undefined/empty (relay not up yet).
1289
+ * 5. `options.targetId` is null/undefined/empty (no page attached yet).
1182
1290
  *
1183
1291
  * Always writes the DevTools URL to stderr so the developer can copy it
1184
1292
  * if the browser open fails or the popup is blocked.
1185
1293
  *
1186
- * @param wssRelayUrl - The public `wss://` relay URL (from tunnel status).
1187
- * @param env - Current MCP environment (`mock` | `relay`).
1294
+ * TOTP expiry caveat: the `at=` code embedded in the URL is valid for the
1295
+ * current 30-second RFC 6238 step (±1 skew = 90 s). The developer must open
1296
+ * the URL within that window; if they miss it, reload the page or re-run
1297
+ * `open()` (though the once-per-session guard prevents that — restart the
1298
+ * MCP server if needed).
1299
+ *
1300
+ * SECRET-HANDLING: the inspector URL (written to stderr) contains the relay
1301
+ * host and a short-lived TOTP code. Do NOT write it to stdout or any
1302
+ * persistent log.
1188
1303
  */
1189
- open(wssRelayUrl, env) {
1304
+ open(options) {
1190
1305
  if (this._opened) return;
1191
1306
  if (isAutoDevtoolsDisabled()) return;
1192
- if (env === "mock") return;
1193
- if (!wssRelayUrl) return;
1307
+ if (options.env === "mock") return;
1308
+ if (!options.relayHttpBaseUrl) return;
1309
+ if (!options.targetId) return;
1194
1310
  this._opened = true;
1195
- const devtoolsUrl = buildChromeDevtoolsUrl(wssRelayUrl);
1196
- process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chrome DevTools를 자동으로 엽니다.
1197
- [ait-debug] Chrome DevTools URL: ${devtoolsUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
1311
+ const inspectorUrl = buildChiiInspectorUrl(options.relayHttpBaseUrl, options.targetId, options.mintTotp);
1312
+ process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chii DevTools를 자동으로 엽니다.
1313
+ [ait-debug] DevTools URL: ${inspectorUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
1314
+ [ait-debug] 주의: URL의 at= 코드는 30초 창 안에서만 유효합니다.
1198
1315
  `);
1199
- if (!openUrlInBrowser(devtoolsUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
1316
+ if (!openUrlInBrowser(inspectorUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
1200
1317
  }
1201
1318
  /** Returns `true` if `open()` has passed all guards and fired once. */
1202
1319
  get opened() {
@@ -4449,7 +4566,7 @@ async function readMcpSdkVersion() {
4449
4566
  * some test environments that skip the build step).
4450
4567
  */
4451
4568
  function readDevtoolsVersion() {
4452
- return "0.1.70";
4569
+ return "0.1.71";
4453
4570
  }
4454
4571
  /**
4455
4572
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -4953,7 +5070,7 @@ function createDebugServer(deps) {
4953
5070
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
4954
5071
  const server = new Server({
4955
5072
  name: "ait-debug",
4956
- version: "0.1.70"
5073
+ version: "0.1.71"
4957
5074
  }, { capabilities: { tools: { listChanged: true } } });
4958
5075
  server.setRequestHandler(ListToolsRequestSchema, () => {
4959
5076
  const conn = router.active;
@@ -5709,6 +5826,7 @@ async function bootRelayFamily(options = {}) {
5709
5826
  return {
5710
5827
  connection,
5711
5828
  relayOrigin: "intoss-webview",
5829
+ relayHttpUrl: relay.baseUrl,
5712
5830
  getTunnelStatus: () => tunnelStatus,
5713
5831
  stop() {
5714
5832
  tunnelProbe?.stop();
@@ -5745,6 +5863,7 @@ async function bootExternalRelayFamily(relayBaseUrl) {
5745
5863
  return {
5746
5864
  connection,
5747
5865
  relayOrigin: "external-pwa",
5866
+ relayHttpUrl: relayBaseUrl,
5748
5867
  getTunnelStatus: () => tunnelStatus,
5749
5868
  stop() {
5750
5869
  connection.close();
@@ -5911,7 +6030,16 @@ var DualConnectionRouter = class {
5911
6030
  this.attachWatcher = startAttachWatcher(activeFamily.connection, server, this.deps.attachWatcherIntervalMs ?? 1e3, () => {
5912
6031
  this.deps.diagnosticsCollector.recordAttach();
5913
6032
  this.deps.onPageAttach?.();
5914
- if (activeFamily.connection.kind === "relay") this.deps.devtoolsOpener.open(this.relayTunnelStatus().wssUrl, deriveEnvironment(activeFamily.connection.kind, getLiveIntent(), activeFamily.relayOrigin));
6033
+ if (activeFamily.connection.kind === "relay") {
6034
+ const firstTarget = activeFamily.connection.listTargets()[0];
6035
+ const env = deriveEnvironment(activeFamily.connection.kind, getLiveIntent(), activeFamily.relayOrigin);
6036
+ this.deps.devtoolsOpener.open({
6037
+ relayHttpBaseUrl: activeFamily.relayHttpUrl,
6038
+ targetId: firstTarget?.id,
6039
+ mintTotp: process.env.AIT_DEBUG_TOTP_SECRET ? () => generateTotp(process.env.AIT_DEBUG_TOTP_SECRET) : void 0,
6040
+ env
6041
+ });
6042
+ }
5915
6043
  });
5916
6044
  }
5917
6045
  /**
@@ -6829,7 +6957,7 @@ function createDevServer(deps = {}) {
6829
6957
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
6830
6958
  const server = new Server({
6831
6959
  name: "ait-devtools",
6832
- version: "0.1.70"
6960
+ version: "0.1.71"
6833
6961
  }, { capabilities: { tools: {} } });
6834
6962
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
6835
6963
  server.setRequestHandler(CallToolRequestSchema, async (request) => {