@ait-co/devtools 0.1.101 → 0.1.103
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/README.en.md +0 -27
- package/README.md +0 -27
- package/dist/{chii-relay-CUS9FJKB.js → chii-relay-DkTOopRj.js} +1 -1
- package/dist/{chii-relay-CUS9FJKB.js.map → chii-relay-DkTOopRj.js.map} +1 -1
- package/dist/{chii-relay-BVVTS3tE.cjs → chii-relay-P6SKmB1g.cjs} +1 -1
- package/dist/{chii-relay-BVVTS3tE.cjs.map → chii-relay-P6SKmB1g.cjs.map} +1 -1
- package/dist/{deeplink-D1HXJ2YG.js → deeplink-B5-Hxu0Q.js} +1 -1
- package/dist/{deeplink-D1HXJ2YG.js.map → deeplink-B5-Hxu0Q.js.map} +1 -1
- package/dist/{deeplink-DDOe0FQl.cjs → deeplink-BzdbA1gV.cjs} +1 -1
- package/dist/{deeplink-DDOe0FQl.cjs.map → deeplink-BzdbA1gV.cjs.map} +1 -1
- package/dist/{devtools-opener-XpwL3fZ9.js → devtools-opener-B8nxrxqu.js} +2 -12
- package/dist/{devtools-opener-XpwL3fZ9.js.map → devtools-opener-B8nxrxqu.js.map} +1 -1
- package/dist/{devtools-opener-mDgeg_MX.cjs → devtools-opener-iv1OwfJN.cjs} +1 -1
- package/dist/{devtools-opener-mDgeg_MX.cjs.map → devtools-opener-iv1OwfJN.cjs.map} +1 -1
- package/dist/mcp/cli.js +20 -65
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +3 -751
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-Clvk1weS.cjs → qr-http-server-C9YPBo6H.cjs} +13 -58
- package/dist/qr-http-server-C9YPBo6H.cjs.map +1 -0
- package/dist/{qr-http-server-B1fmICC4.js → qr-http-server-Ck8o4PLI.js} +13 -58
- package/dist/qr-http-server-Ck8o4PLI.js.map +1 -0
- package/dist/{qr-http-server-ofopTUL-.js → qr-http-server-D09oMVit.js} +13 -58
- package/dist/qr-http-server-D09oMVit.js.map +1 -0
- package/dist/{qr-http-server-C9NUBysQ.cjs → qr-http-server-DegdwsSj.cjs} +13 -58
- package/dist/qr-http-server-DegdwsSj.cjs.map +1 -0
- package/dist/{relay-secret-store-J0SUUXjH.js → relay-secret-store-B0DH-8Qb.js} +46 -3
- package/dist/relay-secret-store-B0DH-8Qb.js.map +1 -0
- package/dist/{relay-secret-store-BvNWdSjV.js → relay-secret-store-Bns5rndt.js} +44 -3
- package/dist/relay-secret-store-Bns5rndt.js.map +1 -0
- package/dist/{relay-secret-store-B5WAozDv.cjs → relay-secret-store-I5q2Wvvv.cjs} +44 -3
- package/dist/relay-secret-store-I5q2Wvvv.cjs.map +1 -0
- package/dist/{relay-url-store-RKcao_yG.js → relay-url-store-BPeUZsiY.js} +2 -2
- package/dist/{relay-url-store-RKcao_yG.js.map → relay-url-store-BPeUZsiY.js.map} +1 -1
- package/dist/{relay-url-store-D2lX9POP.cjs → relay-url-store-CvmnevcO.cjs} +2 -2
- package/dist/{relay-url-store-D2lX9POP.cjs.map → relay-url-store-CvmnevcO.cjs.map} +1 -1
- package/dist/{relay-url-store-1CXVqNDL.js → relay-url-store-DJHZjk8o.js} +2 -2
- package/dist/{relay-url-store-1CXVqNDL.js.map → relay-url-store-DJHZjk8o.js.map} +1 -1
- package/dist/{totp-D9fjaVak.cjs → totp-CNw0w89F.cjs} +1 -1
- package/dist/{totp-D9fjaVak.cjs.map → totp-CNw0w89F.cjs.map} +1 -1
- package/dist/{totp-CauHjkdE.js → totp-DYdP9N3o.js} +1 -1
- package/dist/{totp-CauHjkdE.js.map → totp-DYdP9N3o.js.map} +1 -1
- package/dist/{tunnel-BmDfjkQI.cjs → tunnel-3RCjGaND.cjs} +6 -6
- package/dist/{tunnel-BmDfjkQI.cjs.map → tunnel-3RCjGaND.cjs.map} +1 -1
- package/dist/{tunnel-C_qpse3-.js → tunnel-qB2Soaaz.js} +6 -6
- package/dist/{tunnel-C_qpse3-.js.map → tunnel-qB2Soaaz.js.map} +1 -1
- package/dist/unplugin/index.cjs +5 -90
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +5 -90
- 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/machine-state-Chg_6SPq.js +0 -188
- package/dist/machine-state-Chg_6SPq.js.map +0 -1
- package/dist/machine-state-DOUweFsJ.cjs +0 -216
- package/dist/machine-state-DOUweFsJ.cjs.map +0 -1
- package/dist/qr-http-server-B1fmICC4.js.map +0 -1
- package/dist/qr-http-server-C9NUBysQ.cjs.map +0 -1
- package/dist/qr-http-server-Clvk1weS.cjs.map +0 -1
- package/dist/qr-http-server-ofopTUL-.js.map +0 -1
- package/dist/relay-secret-store-B5WAozDv.cjs.map +0 -1
- package/dist/relay-secret-store-BvNWdSjV.js.map +0 -1
- package/dist/relay-secret-store-J0SUUXjH.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-opener-XpwL3fZ9.js","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA2KA,SAAgB,yBAAkC;AAChD,QAAO,QAAQ,IAAI,sBAAsB;;;;;;;;;AAc3C,SAAgB,iBAAiB,KAAsB;AAGrD,KAAI,QAAQ,IAAI,sCAAsC,IAAK,QAAO;CAElE,MAAM,EAAE,cAAA,UAAsB,qBAAqB;CACnD,MAAM,WAAW,QAAQ;CAGzB,IAAI;AACJ,KAAI,aAAa,SACf,cAAa,CAAC;EAAE,KAAK;EAAQ,MAAM,CAAC,IAAI;EAAE,CAAC;UAClC,aAAa,QACtB,cAAa,CAAC;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAI;EAAE,CAAC;KAG7D,cAAa;EACX;GAAE,KAAK;GAAY,MAAM,CAAC,IAAI;GAAE;EAChC;GAAE,KAAK;GAAoB,MAAM,CAAC,IAAI;GAAE;EACxC;GAAE,KAAK;GAAiB,MAAM,CAAC,IAAI;GAAE;EACtC;AAGH,MAAK,MAAM,EAAE,KAAK,UAAU,WAC1B,KAAI;EACF,MAAM,SAAS,UAAU,KAAK,MAAM;GAAE,UAAU;GAAQ,SAAS;GAAO,CAAC;AACzE,MAAI,CAAC,OAAO,SAAS,OAAO,WAAW,EAAG,QAAO;SAC3C;AAIV,QAAO"}
|
|
1
|
+
{"version":3,"file":"devtools-opener-B8nxrxqu.js","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA2KA,SAAgB,yBAAkC;AAChD,QAAO,QAAQ,IAAI,sBAAsB;;;;;;;;;AAc3C,SAAgB,iBAAiB,KAAsB;AAGrD,KAAI,QAAQ,IAAI,sCAAsC,IAAK,QAAO;CAElE,MAAM,EAAE,cAAA,UAAsB,qBAAqB;CACnD,MAAM,WAAW,QAAQ;CAGzB,IAAI;AACJ,KAAI,aAAa,SACf,cAAa,CAAC;EAAE,KAAK;EAAQ,MAAM,CAAC,IAAI;EAAE,CAAC;UAClC,aAAa,QACtB,cAAa,CAAC;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAI;EAAE,CAAC;KAG7D,cAAa;EACX;GAAE,KAAK;GAAY,MAAM,CAAC,IAAI;GAAE;EAChC;GAAE,KAAK;GAAoB,MAAM,CAAC,IAAI;GAAE;EACxC;GAAE,KAAK;GAAiB,MAAM,CAAC,IAAI;GAAE;EACtC;AAGH,MAAK,MAAM,EAAE,KAAK,UAAU,WAC1B,KAAI;EACF,MAAM,SAAS,UAAU,KAAK,MAAM;GAAE,UAAU;GAAQ,SAAS;GAAO,CAAC;AACzE,MAAI,CAAC,OAAO,SAAS,OAAO,WAAW,EAAG,QAAO;SAC3C;AAIV,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-opener-mDgeg_MX.cjs","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;AA2KA,SAAgB,yBAAkC;AAChD,QAAO,QAAQ,IAAI,sBAAsB;;;;;;;;;AAc3C,SAAgB,iBAAiB,KAAsB;AAGrD,KAAI,QAAQ,IAAI,sCAAsC,IAAK,QAAO;CAElE,MAAM,EAAE,cAAc,QAAQ,qBAAqB;CACnD,MAAM,WAAW,QAAQ;CAGzB,IAAI;AACJ,KAAI,aAAa,SACf,cAAa,CAAC;EAAE,KAAK;EAAQ,MAAM,CAAC,IAAI;EAAE,CAAC;UAClC,aAAa,QACtB,cAAa,CAAC;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAI;EAAE,CAAC;KAG7D,cAAa;EACX;GAAE,KAAK;GAAY,MAAM,CAAC,IAAI;GAAE;EAChC;GAAE,KAAK;GAAoB,MAAM,CAAC,IAAI;GAAE;EACxC;GAAE,KAAK;GAAiB,MAAM,CAAC,IAAI;GAAE;EACtC;AAGH,MAAK,MAAM,EAAE,KAAK,UAAU,WAC1B,KAAI;EACF,MAAM,SAAS,UAAU,KAAK,MAAM;GAAE,UAAU;GAAQ,SAAS;GAAO,CAAC;AACzE,MAAI,CAAC,OAAO,SAAS,OAAO,WAAW,EAAG,QAAO;SAC3C;AAIV,QAAO"}
|
|
1
|
+
{"version":3,"file":"devtools-opener-iv1OwfJN.cjs","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;AA2KA,SAAgB,yBAAkC;AAChD,QAAO,QAAQ,IAAI,sBAAsB;;;;;;;;;AAc3C,SAAgB,iBAAiB,KAAsB;AAGrD,KAAI,QAAQ,IAAI,sCAAsC,IAAK,QAAO;CAElE,MAAM,EAAE,cAAc,QAAQ,qBAAqB;CACnD,MAAM,WAAW,QAAQ;CAGzB,IAAI;AACJ,KAAI,aAAa,SACf,cAAa,CAAC;EAAE,KAAK;EAAQ,MAAM,CAAC,IAAI;EAAE,CAAC;UAClC,aAAa,QACtB,cAAa,CAAC;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAI;EAAE,CAAC;KAG7D,cAAa;EACX;GAAE,KAAK;GAAY,MAAM,CAAC,IAAI;GAAE;EAChC;GAAE,KAAK;GAAoB,MAAM,CAAC,IAAI;GAAE;EACxC;GAAE,KAAK;GAAiB,MAAM,CAAC,IAAI;GAAE;EACtC;AAGH,MAAK,MAAM,EAAE,KAAK,UAAU,WAC1B,KAAI;EACF,MAAM,SAAS,UAAU,KAAK,MAAM;GAAE,UAAU;GAAQ,SAAS;GAAO,CAAC;AACzE,MAAI,CAAC,OAAO,SAAS,OAAO,WAAW,EAAG,QAAO;SAC3C;AAIV,QAAO"}
|
package/dist/mcp/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-Xq3ACwkm.js";
|
|
3
|
-
import { t as loadRelaySecretReadOnly } from "../relay-secret-store-
|
|
3
|
+
import { t as loadRelaySecretReadOnly } from "../relay-secret-store-B0DH-8Qb.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";
|
|
@@ -1933,7 +1933,7 @@ function findFreePort() {
|
|
|
1933
1933
|
else resolve(port);
|
|
1934
1934
|
});
|
|
1935
1935
|
});
|
|
1936
|
-
server.
|
|
1936
|
+
server.on("error", reject);
|
|
1937
1937
|
});
|
|
1938
1938
|
}
|
|
1939
1939
|
/**
|
|
@@ -2034,11 +2034,6 @@ const en = {
|
|
|
2034
2034
|
"panel.tab.analytics": "Analytics",
|
|
2035
2035
|
"panel.tab.storage": "Storage",
|
|
2036
2036
|
"common.readOnly": "Read-only — mock responses are controlled at build time.",
|
|
2037
|
-
"toast.consent.title": "Send anonymous usage stats?",
|
|
2038
|
-
"toast.consent.body": "We collect anonymous events only, to improve the tool. You can turn this off anytime in the Environment tab.",
|
|
2039
|
-
"toast.consent.learnMore": "Learn more",
|
|
2040
|
-
"toast.consent.accept": "Yes, send",
|
|
2041
|
-
"toast.consent.deny": "No, thanks",
|
|
2042
2037
|
"env.section.platform": "Platform",
|
|
2043
2038
|
"env.row.os": "OS",
|
|
2044
2039
|
"env.row.appVersion": "App Version",
|
|
@@ -2055,27 +2050,6 @@ const en = {
|
|
|
2055
2050
|
"env.value.iosSwipeGesture.enabled": "enabled",
|
|
2056
2051
|
"env.value.iosSwipeGesture.disabled": "disabled",
|
|
2057
2052
|
"env.hint.iosSwipeGesture": "Last value passed to setIosSwipeGestureEnabled. Switching Environment to toss lets a toss-gated guard toggle this.",
|
|
2058
|
-
"env.telemetry.section": "Telemetry",
|
|
2059
|
-
"env.telemetry.t0Row": "Anonymous usage signal (Tier 0)",
|
|
2060
|
-
"env.telemetry.t0On": "On",
|
|
2061
|
-
"env.telemetry.t0Off": "Off",
|
|
2062
|
-
"env.telemetry.t0TurnOn": "Turn on",
|
|
2063
|
-
"env.telemetry.t0TurnOff": "Turn off",
|
|
2064
|
-
"env.telemetry.t0Desc": "Version + date only, no PII. Once per day. Helps improve the package.",
|
|
2065
|
-
"env.telemetry.row": "Extended telemetry (Tier 1)",
|
|
2066
|
-
"env.telemetry.on": "On",
|
|
2067
|
-
"env.telemetry.off": "Off",
|
|
2068
|
-
"env.telemetry.turnOn": "Turn on",
|
|
2069
|
-
"env.telemetry.turnOff": "Turn off",
|
|
2070
|
-
"env.telemetry.anonIdLabel": "anon_id: {value}",
|
|
2071
|
-
"env.telemetry.anonIdNotSet": "(not yet set)",
|
|
2072
|
-
"env.telemetry.anonIdCopyTitle": "Click to copy full anon_id",
|
|
2073
|
-
"env.telemetry.deleteBtn": "Delete my data",
|
|
2074
|
-
"env.telemetry.deleting": "Deleting…",
|
|
2075
|
-
"env.telemetry.deleted": "Deleted",
|
|
2076
|
-
"env.telemetry.deleteFailedRetry": "Delete failed (please retry)",
|
|
2077
|
-
"env.telemetry.deleteFailed": "Delete failed",
|
|
2078
|
-
"env.telemetry.privacyLink": "Privacy policy →",
|
|
2079
2053
|
"env.section.language": "Language",
|
|
2080
2054
|
"env.language.row": "Language",
|
|
2081
2055
|
"env.language.ko": "한국어",
|
|
@@ -2296,11 +2270,6 @@ const tables = {
|
|
|
2296
2270
|
"panel.tab.analytics": "Analytics",
|
|
2297
2271
|
"panel.tab.storage": "Storage",
|
|
2298
2272
|
"common.readOnly": "읽기 전용 — mock 응답은 빌드 타임에 고정됩니다.",
|
|
2299
|
-
"toast.consent.title": "익명 사용 통계를 보낼까요?",
|
|
2300
|
-
"toast.consent.body": "도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.",
|
|
2301
|
-
"toast.consent.learnMore": "더 알아보기",
|
|
2302
|
-
"toast.consent.accept": "네, 보낼게요",
|
|
2303
|
-
"toast.consent.deny": "아니요",
|
|
2304
2273
|
"env.section.platform": "Platform",
|
|
2305
2274
|
"env.row.os": "OS",
|
|
2306
2275
|
"env.row.appVersion": "App Version",
|
|
@@ -2317,27 +2286,6 @@ const tables = {
|
|
|
2317
2286
|
"env.value.iosSwipeGesture.enabled": "enabled",
|
|
2318
2287
|
"env.value.iosSwipeGesture.disabled": "disabled",
|
|
2319
2288
|
"env.hint.iosSwipeGesture": "setIosSwipeGestureEnabled의 마지막 호출값. Environment를 toss로 바꾸면 toss-gated 가드가 이 값을 토글합니다.",
|
|
2320
|
-
"env.telemetry.section": "Telemetry",
|
|
2321
|
-
"env.telemetry.t0Row": "익명 사용 신호 (Tier 0)",
|
|
2322
|
-
"env.telemetry.t0On": "On",
|
|
2323
|
-
"env.telemetry.t0Off": "Off",
|
|
2324
|
-
"env.telemetry.t0TurnOn": "Turn on",
|
|
2325
|
-
"env.telemetry.t0TurnOff": "Turn off",
|
|
2326
|
-
"env.telemetry.t0Desc": "버전·날짜만 수집, PII 없음. 하루 1회. 패키지 개선에 사용됩니다.",
|
|
2327
|
-
"env.telemetry.row": "확장 텔레메트리 (Tier 1)",
|
|
2328
|
-
"env.telemetry.on": "On",
|
|
2329
|
-
"env.telemetry.off": "Off",
|
|
2330
|
-
"env.telemetry.turnOn": "Turn on",
|
|
2331
|
-
"env.telemetry.turnOff": "Turn off",
|
|
2332
|
-
"env.telemetry.anonIdLabel": "anon_id: {value}",
|
|
2333
|
-
"env.telemetry.anonIdNotSet": "(not yet set)",
|
|
2334
|
-
"env.telemetry.anonIdCopyTitle": "전체 anon_id 복사",
|
|
2335
|
-
"env.telemetry.deleteBtn": "내 데이터 삭제",
|
|
2336
|
-
"env.telemetry.deleting": "삭제 중…",
|
|
2337
|
-
"env.telemetry.deleted": "삭제 완료",
|
|
2338
|
-
"env.telemetry.deleteFailedRetry": "삭제 실패 (다시 시도해주세요)",
|
|
2339
|
-
"env.telemetry.deleteFailed": "삭제 실패",
|
|
2340
|
-
"env.telemetry.privacyLink": "개인정보 처리방침 →",
|
|
2341
2289
|
"env.section.language": "Language",
|
|
2342
2290
|
"env.language.row": "Language",
|
|
2343
2291
|
"env.language.ko": "한국어",
|
|
@@ -3003,7 +2951,7 @@ function buildLangSwitcher(path, existingParams, locale, s) {
|
|
|
3003
2951
|
* - tunnel wssUrl은 "터널 연결됨" 상태 표시에서 UP/DOWN만 노출.
|
|
3004
2952
|
* wssUrl 값 자체는 dashboard HTML에 넣지 않는다.
|
|
3005
2953
|
*/
|
|
3006
|
-
function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams()) {
|
|
2954
|
+
function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams(), devtoolsEntryUrl = null) {
|
|
3007
2955
|
const s = resolveLocaleStrings(locale);
|
|
3008
2956
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3009
2957
|
const tunnelStatus = state.tunnel.up ? s("dashboard.tunnel.up") : s("dashboard.tunnel.down");
|
|
@@ -3016,7 +2964,7 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
3016
2964
|
} else attachSection = `<p class="hint">${escapeHtml(s("dashboard.attach.hint"))}</p>`;
|
|
3017
2965
|
const pagesAttached = Array.isArray(state.pages) && state.pages.length > 0;
|
|
3018
2966
|
let inspectorSection;
|
|
3019
|
-
if (pagesAttached &&
|
|
2967
|
+
if (pagesAttached && devtoolsEntryUrl) inspectorSection = `<a class="inspector-link" id="inspector-link" href="${escapeHtml(devtoolsEntryUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(s("dashboard.inspector.open"))}</a>`;
|
|
3020
2968
|
else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
|
|
3021
2969
|
const pagesSection = state.pages === null ? "" : `<hr /><section id="pages-section"><h2>${escapeHtml(s("dashboard.pages.section"))}</h2><ul id="pages-list">${state.pages.length > 0 ? state.pages.map((p) => {
|
|
3022
2970
|
return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
|
|
@@ -3301,7 +3249,13 @@ async function startQrHttpServer(getDashboardState, options) {
|
|
|
3301
3249
|
errorCorrectionLevel: "M"
|
|
3302
3250
|
});
|
|
3303
3251
|
} catch {}
|
|
3304
|
-
const
|
|
3252
|
+
const devtoolsEntryUrl = (() => {
|
|
3253
|
+
if (!options?.getDirectInspectorUrl) return null;
|
|
3254
|
+
const addr = server.address();
|
|
3255
|
+
if (!addr || typeof addr === "string") return null;
|
|
3256
|
+
return `http://127.0.0.1:${addr.port}/devtools/`;
|
|
3257
|
+
})();
|
|
3258
|
+
const html = buildDashboardHtml(state, qrDataUrl, locale, path, params, devtoolsEntryUrl);
|
|
3305
3259
|
res.writeHead(200, {
|
|
3306
3260
|
"Content-Type": "text/html; charset=utf-8",
|
|
3307
3261
|
"Cache-Control": "no-store"
|
|
@@ -3369,11 +3323,12 @@ async function startQrHttpServer(getDashboardState, options) {
|
|
|
3369
3323
|
});
|
|
3370
3324
|
return;
|
|
3371
3325
|
}
|
|
3372
|
-
if (path === "/inspector") {
|
|
3326
|
+
if (path === "/inspector" || path === "/devtools" || path === "/devtools/") {
|
|
3373
3327
|
const getDirectInspectorUrl = options?.getDirectInspectorUrl;
|
|
3374
3328
|
if (!getDirectInspectorUrl) {
|
|
3329
|
+
const body = path === "/inspector" ? "Inspector endpoint is not available in this server mode." : "relay 연결 세션에서만 DevTools UI를 열 수 있습니다.";
|
|
3375
3330
|
res.writeHead(503, { "Content-Type": "text/plain; charset=utf-8" });
|
|
3376
|
-
res.end(
|
|
3331
|
+
res.end(body);
|
|
3377
3332
|
return;
|
|
3378
3333
|
}
|
|
3379
3334
|
const result = getDirectInspectorUrl();
|
|
@@ -4858,7 +4813,7 @@ async function readMcpSdkVersion() {
|
|
|
4858
4813
|
* some test environments that skip the build step).
|
|
4859
4814
|
*/
|
|
4860
4815
|
function readDevtoolsVersion() {
|
|
4861
|
-
return "0.1.
|
|
4816
|
+
return "0.1.103";
|
|
4862
4817
|
}
|
|
4863
4818
|
/**
|
|
4864
4819
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -5450,7 +5405,7 @@ function createDebugServer(deps) {
|
|
|
5450
5405
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
5451
5406
|
const server = new Server({
|
|
5452
5407
|
name: "ait-debug",
|
|
5453
|
-
version: "0.1.
|
|
5408
|
+
version: "0.1.103"
|
|
5454
5409
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
5455
5410
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
5456
5411
|
const conn = router.active;
|
|
@@ -5538,7 +5493,7 @@ function createDebugServer(deps) {
|
|
|
5538
5493
|
const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
|
|
5539
5494
|
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
5540
5495
|
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
5541
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
5496
|
+
const { readRelayUrls } = await import("../relay-url-store-BPeUZsiY.js");
|
|
5542
5497
|
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
5543
5498
|
}
|
|
5544
5499
|
if (tunnelHttpUrl === "") return mcpError("build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.");
|
|
@@ -6291,7 +6246,7 @@ async function readRelayLocalUrl(env = process.env, projectRoot) {
|
|
|
6291
6246
|
const envValue = (env.AIT_RELAY_LOCAL_URL ?? "").trim();
|
|
6292
6247
|
if (envValue !== "") return envValue;
|
|
6293
6248
|
if (projectRoot !== void 0) try {
|
|
6294
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
6249
|
+
const { readRelayUrls } = await import("../relay-url-store-BPeUZsiY.js");
|
|
6295
6250
|
const stored = await readRelayUrls({ projectRoot });
|
|
6296
6251
|
if (stored?.relayLocalUrl) return stored.relayLocalUrl;
|
|
6297
6252
|
} catch {}
|
|
@@ -6345,7 +6300,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
|
|
|
6345
6300
|
const envValue = typeof raw === "string" ? raw.trim() : "";
|
|
6346
6301
|
if (envValue !== "") return envValue;
|
|
6347
6302
|
if (projectRoot !== void 0) {
|
|
6348
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
6303
|
+
const { readRelayUrls } = await import("../relay-url-store-BPeUZsiY.js");
|
|
6349
6304
|
const stored = await readRelayUrls({ projectRoot });
|
|
6350
6305
|
if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
|
|
6351
6306
|
}
|
|
@@ -7584,7 +7539,7 @@ function createDevServer(deps = {}) {
|
|
|
7584
7539
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
7585
7540
|
const server = new Server({
|
|
7586
7541
|
name: "ait-devtools",
|
|
7587
|
-
version: "0.1.
|
|
7542
|
+
version: "0.1.103"
|
|
7588
7543
|
}, { capabilities: { tools: {} } });
|
|
7589
7544
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
7590
7545
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|