@ait-co/devtools 0.1.100 → 0.1.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/mcp/cli.js +107 -19
  2. package/dist/mcp/cli.js.map +1 -1
  3. package/dist/mcp/server.js +1 -1
  4. package/dist/panel/index.js +2 -2
  5. package/dist/{qr-http-server-Clvk1weS.cjs → qr-http-server-B7DsRdN1.cjs} +13 -6
  6. package/dist/{qr-http-server-Clvk1weS.cjs.map → qr-http-server-B7DsRdN1.cjs.map} +1 -1
  7. package/dist/{qr-http-server-B1fmICC4.js → qr-http-server-CK-ZT_pC.js} +13 -6
  8. package/dist/{qr-http-server-B1fmICC4.js.map → qr-http-server-CK-ZT_pC.js.map} +1 -1
  9. package/dist/{qr-http-server-ofopTUL-.js → qr-http-server-DI3A6f5L.js} +13 -6
  10. package/dist/{qr-http-server-ofopTUL-.js.map → qr-http-server-DI3A6f5L.js.map} +1 -1
  11. package/dist/{qr-http-server-C9NUBysQ.cjs → qr-http-server-Dqb3GQju.cjs} +13 -6
  12. package/dist/{qr-http-server-C9NUBysQ.cjs.map → qr-http-server-Dqb3GQju.cjs.map} +1 -1
  13. package/dist/{relay-secret-store-J0SUUXjH.js → relay-secret-store-B0DH-8Qb.js} +46 -3
  14. package/dist/relay-secret-store-B0DH-8Qb.js.map +1 -0
  15. package/dist/{relay-secret-store-B5WAozDv.cjs → relay-secret-store-CqDaaFW1.cjs} +43 -2
  16. package/dist/relay-secret-store-CqDaaFW1.cjs.map +1 -0
  17. package/dist/{relay-secret-store-BvNWdSjV.js → relay-secret-store-DKuoAJmA.js} +43 -2
  18. package/dist/relay-secret-store-DKuoAJmA.js.map +1 -0
  19. package/dist/{relay-url-store-RKcao_yG.js → relay-url-store-BPeUZsiY.js} +2 -2
  20. package/dist/{relay-url-store-RKcao_yG.js.map → relay-url-store-BPeUZsiY.js.map} +1 -1
  21. package/dist/{relay-url-store-D2lX9POP.cjs → relay-url-store-CIZlFBkR.cjs} +2 -2
  22. package/dist/{relay-url-store-D2lX9POP.cjs.map → relay-url-store-CIZlFBkR.cjs.map} +1 -1
  23. package/dist/{relay-url-store-1CXVqNDL.js → relay-url-store-DASEZiT9.js} +2 -2
  24. package/dist/{relay-url-store-1CXVqNDL.js.map → relay-url-store-DASEZiT9.js.map} +1 -1
  25. package/dist/{tunnel-C_qpse3-.js → tunnel-CepDBgEc.js} +2 -2
  26. package/dist/{tunnel-C_qpse3-.js.map → tunnel-CepDBgEc.js.map} +1 -1
  27. package/dist/{tunnel-BmDfjkQI.cjs → tunnel-D0QnxKsF.cjs} +2 -2
  28. package/dist/{tunnel-BmDfjkQI.cjs.map → tunnel-D0QnxKsF.cjs.map} +1 -1
  29. package/dist/unplugin/index.cjs +3 -3
  30. package/dist/unplugin/index.js +3 -3
  31. package/dist/unplugin/tunnel.cjs +1 -1
  32. package/dist/unplugin/tunnel.js +1 -1
  33. package/package.json +1 -1
  34. package/dist/relay-secret-store-B5WAozDv.cjs.map +0 -1
  35. package/dist/relay-secret-store-BvNWdSjV.js.map +0 -1
  36. package/dist/relay-secret-store-J0SUUXjH.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":["isPidAlive","isObject","DEFAULT_BUFFER_SIZE","isObject","parseInbound","PHASE_1_EVENTS","require","isObject","_isPidAlive","isObject","jsonResult","envelopeResult"],"sources":["../../src/shared/parent-watcher.ts","../../src/mcp/ait-chii-source.ts","../../src/shared/relay-auth-close.ts","../../src/mcp/log.ts","../../src/mcp/chii-connection.ts","../../src/mcp/chii-relay.ts","../../src/mcp/deeplink.ts","../../src/mcp/devtools-opener.ts","../../src/mcp/envelope.ts","../../src/mcp/environment.ts","../../src/mcp/errors.ts","../../src/mcp/local-connection.ts","../../src/mcp/local-launcher.ts","../../src/i18n/en.ts","../../src/i18n/ko.ts","../../src/i18n/index.ts","../../src/mcp/dashboard.generated.ts","../../src/mcp/qr-http-server.ts","../../src/mcp/server-lock.ts","../../src/mcp/sdk-signatures.ts","../../src/mcp/tools.ts","../../src/mcp/tunnel.ts","../../src/mcp/debug-server.ts","../../src/mcp/ait-http-source.ts","../../src/mcp/server.ts","../../src/mcp/cli.ts"],"sourcesContent":["/**\n * Shared parent-PID watcher — used by both the MCP debug daemon and the\n * unplugin tunnel path to self-terminate when the parent process (e.g. Claude\n * Code, vite) has died or been reparented without sending SIGTERM/SIGHUP.\n *\n * Intentionally react-free and Node-stdlib-only so this module is safe to\n * import from the MCP daemon bundle (`dist/mcp/cli.js`) without violating the\n * install-graph invariant.\n */\n\n// ---------------------------------------------------------------------------\n// isPidAlive — extracted from src/mcp/server-lock.ts\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when the given PID refers to a running process.\n *\n * Uses `process.kill(pid, 0)` — a no-op signal that succeeds when the process\n * exists and we have permission to signal it; throws ESRCH when it doesn't exist.\n */\nexport function isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err: unknown) {\n // ESRCH = no such process → stale lock.\n // EPERM = process exists but we can't signal it (still alive).\n if ((err as NodeJS.ErrnoException).code === 'EPERM') return true;\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// startParentWatcher — extracted from src/mcp/debug-server.ts\n// ---------------------------------------------------------------------------\n\n/**\n * Starts a periodic watcher that detects when the parent process (e.g. Claude\n * Code) has died without sending SIGTERM/SIGHUP, and calls `onOrphaned` so the\n * daemon can self-terminate rather than running as a zombie.\n *\n * Mirrors the `startAttachWatcher` pattern: `setInterval`-based, returns\n * `{ stop(): void }`, injectable deps for testability.\n *\n * @param onOrphaned - Called once when the parent is gone.\n * @param opts.intervalMs - Poll interval in milliseconds (default 5 000).\n * @param opts.initialPpid - Parent PID to watch (default `process.ppid`).\n * @param opts.isAlive - Predicate to test if a PID is running (default `isPidAlive`).\n * @param opts.getPpid - Supplier of current ppid (default `() => process.ppid`).\n * Detects ppid changes as well as death.\n * @param opts.log - Logger (default `process.stderr.write`).\n *\n * @returns `stop` — call during shutdown to clear the interval.\n */\nexport function startParentWatcher(\n onOrphaned: () => void,\n opts?: {\n intervalMs?: number;\n initialPpid?: number;\n isAlive?: (pid: number) => boolean;\n getPpid?: () => number;\n log?: (msg: string) => void;\n },\n): { stop(): void } {\n const {\n intervalMs = 5_000,\n initialPpid = process.ppid,\n isAlive = isPidAlive,\n getPpid = () => process.ppid,\n log = (msg: string) => process.stderr.write(msg),\n } = opts ?? {};\n\n // PID 1 is init/launchd — running under a process manager or as a detached\n // daemon. There is no meaningful parent to watch; skip the watcher entirely.\n if (initialPpid <= 1) {\n log('[ait-debug] parent-pid watcher: no parent to watch (ppid<=1), skipping\\n');\n return { stop() {} };\n }\n\n let fired = false;\n\n const handle = setInterval(() => {\n if (fired) return;\n\n const currentPpid = getPpid();\n const orphaned = currentPpid !== initialPpid || !isAlive(initialPpid);\n\n if (orphaned) {\n fired = true;\n clearInterval(handle);\n log(\n `[ait-debug] parent-pid watcher: parent PID ${initialPpid} is gone (currentPpid=${currentPpid}) — shutting down\\n`,\n );\n onOrphaned();\n }\n }, intervalMs);\n\n return {\n stop() {\n clearInterval(handle);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// startMaxAgeWatchdog — FIX 4: daemon lifetime cap\n// ---------------------------------------------------------------------------\n\n/**\n * Starts a periodic watchdog that calls `onExpired` once after `maxAgeMs`\n * milliseconds have elapsed since the watchdog was created.\n *\n * Motivation (issue #571): cloudflared quick-tunnel lifetimes are finite (a\n * few hours). A daemon that has been running for days will have outlived its\n * tunnel regardless of whether the tunnel process exited cleanly. This watchdog\n * caps the daemon's maximum age and forces a fresh start so the tunnel is\n * replaced before it silently expires.\n *\n * @param onExpired - Called once when the maximum age is reached. The caller\n * should call `shutdown()` then `process.exit(0)`.\n * @param opts.maxAgeMs - Maximum daemon lifetime in ms. Default 6 h.\n * @param opts.intervalMs - Check interval in ms. Default 60 000 (1 min).\n * @param opts.now - Time source (injectable for tests). Default `Date.now`.\n *\n * @returns `stop` — call during shutdown to clear the interval.\n */\nexport function startMaxAgeWatchdog(\n onExpired: () => void,\n opts: {\n maxAgeMs?: number;\n intervalMs?: number;\n now?: () => number;\n } = {},\n): { stop(): void } {\n const {\n maxAgeMs = 6 * 60 * 60 * 1_000, // 6 hours\n intervalMs = 60_000,\n now = () => Date.now(),\n } = opts;\n\n const startedAt = now();\n let fired = false;\n\n const handle = setInterval(() => {\n if (fired) return;\n if (now() - startedAt >= maxAgeMs) {\n fired = true;\n clearInterval(handle);\n onExpired();\n }\n }, intervalMs);\n\n return {\n stop() {\n clearInterval(handle);\n },\n };\n}\n","/**\n * Debug-mode `AitSource` — forwards `AIT.*` methods over the Chii channel.\n *\n * The AIT domain (`AIT.getSdkCallHistory` / `getMockState` /\n * `getOperationalEnvironment`) is non-standard CDP: the in-app side registers a\n * handler for these methods and answers them over the same Chii websocket the\n * CDP commands use. Building the AIT source on `ChiiCdpConnection.sendCommand`\n * means both domains share one transport (spec: \"the same MCP server forwards\n * both CDP and AIT domains\").\n *\n * The in-app `AIT.*` handler lives downstream in sdk-example. Here we build\n * the MCP-server-side forwarding + the injectable seam; tests inject a fake\n * `AitSource` returning canned responses, so this forwarding layer needs no\n * phone.\n *\n * Node-only (wraps the relay websocket connection).\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** The slice of `ChiiCdpConnection` this source needs (keeps it testable). */\nexport interface AitCommandSender {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Narrows an `AIT.getSdkCallHistory` response, tolerating a missing array. */\nfunction asSdkCallHistory(raw: unknown): AitSdkCallHistory {\n if (isObject(raw) && Array.isArray(raw.calls)) {\n return { calls: raw.calls as AitSdkCallHistory['calls'] };\n }\n return { calls: [] };\n}\n\n/** Narrows an `AIT.getMockState` response to an opaque record. */\nfunction asMockState(raw: unknown): AitMockState {\n return isObject(raw) ? raw : {};\n}\n\n/** Narrows an `AIT.getOperationalEnvironment` response. */\nfunction asOperationalEnvironment(raw: unknown): AitOperationalEnvironment {\n const environment =\n isObject(raw) && typeof raw.environment === 'string' ? raw.environment : 'unknown';\n const sdkVersion = isObject(raw) && typeof raw.sdkVersion === 'string' ? raw.sdkVersion : null;\n return { environment, sdkVersion };\n}\n\nexport class ChiiAitSource implements AitSource {\n constructor(private readonly sender: AitCommandSender) {}\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n const raw = await this.sender.sendCommand(method);\n // The map's value type is resolved per-key below; the cast is the single\n // narrowing point (each branch returns the precise shape for `method`).\n switch (method) {\n case 'AIT.getSdkCallHistory':\n return asSdkCallHistory(raw) as AitMethodMap[M];\n case 'AIT.getMockState':\n return asMockState(raw) as AitMethodMap[M];\n case 'AIT.getOperationalEnvironment':\n return asOperationalEnvironment(raw) as AitMethodMap[M];\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * Shared constants for the relay's named TOTP-auth rejection (issue #478).\n *\n * Before #478 the relay rejected an unauthenticated WebSocket upgrade with a\n * raw `HTTP/1.1 401` + `socket.destroy()`. A handshake aborted that way is\n * indistinguishable from a network failure on the browser side — the\n * WebSocket only ever sees close code 1006, so the phone (env-2 launcher PWA)\n * could not tell \"stale TOTP code\" apart from \"tunnel down\" and stayed\n * silent. The fix is accept-then-close: complete the handshake, then close\n * with an application close code that NAMES the rejection.\n *\n * Three parties share this contract:\n * - `src/mcp/chii-relay.ts` (Node) sends the close frame / HTTP error body;\n * - `src/in-app/attach.ts` (browser) observes relay-bound WebSockets and\n * surfaces the code to the launcher shell;\n * - `src/mcp/chii-connection.ts` (Node daemon client) recognises the code\n * as an auth failure on its own `/client` dial (defensive — #439's fresh\n * code mint means it should not normally hit this).\n *\n * This module is intentionally dependency-free (no Node, no DOM) so it is\n * safe to import from both the browser in-app bundle and the MCP daemon\n * bundle.\n *\n * SECRET-HANDLING: these are fixed enum values. The close reason / error body\n * must never grow to carry a secret, a TOTP code, or a host.\n */\n\n/**\n * WebSocket close code sent by the relay when TOTP auth is rejected.\n *\n * 4000–4999 is the application-reserved range (RFC 6455 §7.4.2); 4401 mirrors\n * HTTP 401 so it reads as \"unauthorized\" at a glance.\n */\nexport const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;\n\n/**\n * Close reason string accompanying {@link RELAY_AUTH_REJECT_CLOSE_CODE}, and\n * the `error` value of the relay's HTTP 401 JSON body. Enum string only —\n * never interpolated with request data.\n */\nexport const RELAY_AUTH_REJECT_REASON = 'totp-rejected';\n","/**\n * Structured JSON-line server logger + allowlist-based secret redact.\n *\n * Every log line emitted by the debug-mode MCP server is a single JSON object:\n * { \"ts\": \"<ISO-8601>\", \"level\": \"info\"|\"warn\"|\"error\", \"event\": \"<category>\", ...fields }\n *\n * Allowlist approach — only the keys in ALLOWED_KEYS pass through to the output\n * object unchanged. Any value that matches a known-secret pattern is replaced\n * with \"***\" regardless of key name. This provides two complementary layers:\n * 1. Key allowlist — unknown keys (e.g. a future field accidentally containing\n * a credential) are dropped entirely.\n * 2. Value redact — pattern matching catches secrets that slip through under\n * an allowed key name (e.g. a message string that includes a TOTP code).\n *\n * SECRET-HANDLING (MUST NOT appear in stdout/stderr/logs):\n * - TOTP 6-digit codes (pattern: standalone 6-digit run)\n * - AITCC_API_KEY values (pattern: \"aitcc_\" or \"AITCC_\" prefix — Deploy Key format)\n * - cookie header values (pattern: \"cookie:\" header content)\n * - relay WSS URLs (contain the relay host which is semi-sensitive)\n * - \"at=<TOTP>\" query params\n *\n * Canonical event categories:\n * server.start — MCP server started (relay port, TOTP enabled, etc.)\n * tunnel.up — cloudflared tunnel assigned a public URL\n * tunnel.down — tunnel error / shutdown\n * page.attached — first CDP target appeared (deploymentId, env)\n * page.detached — target evicted / session replaced\n * page.crashed — target crash detected\n * tool.call — MCP tool invocation (tool name only — no args/results)\n * tool.error — MCP tool error (tool name + safe error category)\n */\n\n/** Structured log levels. */\nexport type LogLevel = 'info' | 'warn' | 'error';\n\n/** Every valid event category. */\nexport type LogEvent =\n | 'server.start'\n | 'tunnel.up'\n | 'tunnel.down'\n | 'page.attached'\n | 'page.detached'\n | 'page.crashed'\n | 'tool.call'\n | 'tool.error';\n\n/**\n * Allowed field keys that may pass through to a log line.\n * Unknown keys are dropped. Values are still redact-scanned.\n */\nconst ALLOWED_KEYS = new Set([\n 'ts',\n 'level',\n 'event',\n 'msg',\n 'port',\n 'totpEnabled',\n 'env',\n 'tool',\n 'deploymentId',\n 'errorKind',\n 'reason',\n 'prevTargetId',\n 'mode',\n]);\n\n/**\n * Patterns that match secret values.\n * Match order matters — more-specific patterns first.\n *\n * #268 redact script covers: relay=wss://…, at=<TOTP>, _deploymentId=<uuid>.\n * Here we extend to in-process value-level patterns used in server logs.\n */\nconst SECRET_PATTERNS: RegExp[] = [\n // TOTP 6-digit code as a standalone value (whole string is exactly 6 digits).\n /^\\d{6}$/,\n // Deploy Key — AITCC_API_KEY value prefix formats.\n /^(aitcc_|AITCC_)/i,\n // Cookie header value (whole string starts with a cookie-like name=value pair).\n /^[A-Za-z0-9_-]+=.{4,}/,\n // WSS relay URL value.\n /^wss:\\/\\//,\n // TOTP \"at=\" query param embedded in a string.\n /(?:^|[?&])at=[A-Z0-9]{6}/i,\n];\n\n/**\n * Returns `true` when the string value matches any known-secret pattern.\n * Only string values are tested — numbers/booleans are always safe.\n */\nfunction isSecretValue(value: string): boolean {\n return SECRET_PATTERNS.some((re) => re.test(value));\n}\n\n/**\n * Redacts a single scalar value.\n * - strings: return \"***\" if the value matches a secret pattern.\n * - other: return as-is.\n */\nfunction redactValue(value: unknown): unknown {\n if (typeof value === 'string' && isSecretValue(value)) {\n return '***';\n }\n return value;\n}\n\n/**\n * Builds a safe log payload from raw fields.\n *\n * - Only keys in `ALLOWED_KEYS` are included.\n * - String values are scanned for secret patterns and replaced with \"***\".\n * - `ts` and `level` and `event` are always included (they are injected by the\n * logger functions below, not by callers).\n */\nfunction buildPayload(\n level: LogLevel,\n event: LogEvent,\n fields: Record<string, unknown>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {\n ts: new Date().toISOString(),\n level,\n event,\n };\n\n for (const [key, value] of Object.entries(fields)) {\n if (!ALLOWED_KEYS.has(key)) continue;\n // ts/level/event are controlled above.\n if (key === 'ts' || key === 'level' || key === 'event') continue;\n out[key] = redactValue(value);\n }\n\n return out;\n}\n\n/**\n * Writes a single JSON log line to stderr.\n * MCP stdio transport uses stdout; all diagnostics go to stderr.\n */\nfunction writeLog(level: LogLevel, event: LogEvent, fields: Record<string, unknown> = {}): void {\n const payload = buildPayload(level, event, fields);\n process.stderr.write(`${JSON.stringify(payload)}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// Public logger functions — one per level.\n// ---------------------------------------------------------------------------\n\n/** Log an informational structured event. */\nexport function logInfo(event: LogEvent, fields: Record<string, unknown> = {}): void {\n writeLog('info', event, fields);\n}\n\n/** Log a warning structured event. */\nexport function logWarn(event: LogEvent, fields: Record<string, unknown> = {}): void {\n writeLog('warn', event, fields);\n}\n\n/** Log an error structured event. */\nexport function logError(event: LogEvent, fields: Record<string, unknown> = {}): void {\n writeLog('error', event, fields);\n}\n\n// ---------------------------------------------------------------------------\n// Exported redact helper for use in tests and callers that need to sanitise\n// before passing to the logger (e.g. error message strings).\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a redacted copy of `value`:\n * - string: \"***\" if it matches a secret pattern, otherwise the original.\n * - other types: returned as-is.\n *\n * Exposed for unit tests and for callers that build dynamic `msg` strings.\n */\nexport function redact(value: unknown): unknown {\n return redactValue(value);\n}\n","/**\n * Production `CdpConnection` backed by the local Chii relay.\n *\n * Topology (debug mode):\n * phone target.js --WS--> Chii relay :9100 <--WS-- this connection\n *\n * The phone connects to the relay as a `target`; this module connects as a\n * `client` (the role a CDP frontend would take) so CDP events the page emits\n * (`Runtime.consoleAPICalled`, `Network.*`) flow back here. We buffer recent\n * events in ring buffers the tool layer reads via `getBufferedEvents`.\n *\n * Node-only: imports `ws`. Never bundled into the browser/in-app entries.\n *\n * Attach reliability (#281):\n * `refreshTargets()` emits an internal 'target:attached' event whenever a\n * new target is added to the relay. `waitForFirstTarget()` awaits that event\n * (with a polling-interval fallback) so `build_attach_url wait_for_attach`\n * resolves deterministically rather than racing between polling rounds.\n */\n\nimport { EventEmitter } from 'node:events';\nimport { WebSocket } from 'ws';\nimport { RELAY_AUTH_REJECT_CLOSE_CODE } from '../shared/relay-auth-close.js';\nimport type {\n CdpCommandMap,\n CdpCommandName,\n CdpConnection,\n CdpEventMap,\n CdpEventName,\n CdpTarget,\n} from './cdp-connection.js';\nimport { logInfo } from './log.js';\nimport { generateTotp } from './totp.js';\n\n/** Max events retained per domain ring buffer. */\nconst DEFAULT_BUFFER_SIZE = 500;\n\n/** A CDP message arriving over the relay websocket. */\ninterface CdpInboundMessage {\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { message: string };\n}\n\n/**\n * Events emitted by `ChiiCdpConnection` for crash / lifecycle notifications.\n * Consumers (e.g. the tool layer) can subscribe with `.onLifecycle(cb)`.\n */\nexport interface TargetLifecycleEvent {\n /**\n * 'crashed' → Inspector.targetCrashed\n * 'destroyed' → Target.targetDestroyed\n * 'detached' → Target.detachedFromTarget\n * 'replaced' → evicted by single-attach model (last-attach wins)\n */\n kind: 'crashed' | 'destroyed' | 'detached' | 'replaced';\n targetId: string | null;\n /** ISO timestamp of detection. */\n detectedAt: string;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction parseInbound(raw: string): CdpInboundMessage | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isObject(parsed)) return null;\n const message: CdpInboundMessage = {};\n if (typeof parsed.id === 'number') message.id = parsed.id;\n if (typeof parsed.method === 'string') message.method = parsed.method;\n if ('params' in parsed) message.params = parsed.params;\n if ('result' in parsed) message.result = parsed.result;\n if (isObject(parsed.error) && typeof parsed.error.message === 'string') {\n message.error = { message: parsed.error.message };\n }\n return message;\n}\n\nconst PHASE_1_EVENTS: readonly CdpEventName[] = [\n 'Runtime.consoleAPICalled',\n 'Network.requestWillBeSent',\n 'Network.responseReceived',\n];\n\n/**\n * Ring buffer size for `Runtime.exceptionThrown`.\n *\n * Exceptions are rarer than console messages but each is heavier (stack\n * trace). 50 is generous enough to cover a crash scenario while keeping\n * memory bounded.\n *\n * **Lifecycle note**: the exception buffer intentionally survives `replaced` /\n * `crashed` / `destroyed` lifecycle events — it is NOT cleared on target\n * transitions. Rationale: an exception fired just before a crash is exactly\n * the signal we want to preserve for root-cause analysis. The buffer\n * represents \"exceptions seen in this MCP session\", not \"exceptions in the\n * current page\".\n */\nconst EXCEPTION_BUFFER_SIZE = 50;\n\nexport interface ChiiCdpConnectionOptions {\n /** Base URL of the local Chii relay HTTP/WS server, e.g. `http://127.0.0.1:9100`. */\n relayBaseUrl: string;\n /** Per-domain ring buffer size. */\n bufferSize?: number;\n /**\n * Default per-command timeout in milliseconds.\n * Override via env `AIT_CDP_COMMAND_TIMEOUT_MS`.\n * Defaults to 30 000 ms (30s).\n */\n commandTimeoutMs?: number;\n /**\n * Hex-encoded TOTP secret (the SECRET, never a code). When set, each client WS\n * (re)connect mints a fresh `at=` code so it lands inside the relay's 90s\n * acceptance window. Leave undefined when the relay has TOTP disabled.\n * SECRET-HANDLING: stored privately, never logged; the minted code rides only\n * in the WS URL query.\n */\n totpSecret?: string;\n}\n\n/** Default per-command timeout if neither option nor env var is set. */\nconst DEFAULT_COMMAND_TIMEOUT_MS = 30_000;\n\n/**\n * Production CDP connection. Polls the relay for the first attached target,\n * opens a client websocket to it, enables Phase 1 domains, and buffers events.\n */\nexport class ChiiCdpConnection implements CdpConnection {\n /** Authoritative connection kind (issue #348) — relay-backed. */\n readonly kind = 'relay' as const;\n\n private readonly relayBaseUrl: string;\n private readonly bufferSize: number;\n private readonly commandTimeoutMs: number;\n private readonly totpSecret: string | undefined;\n private readonly emitter = new EventEmitter();\n private readonly buffers = new Map<CdpEventName, unknown[]>();\n private readonly targets = new Map<string, CdpTarget>();\n\n private ws: WebSocket | null = null;\n private connectionState: 'idle' | 'connected' | 'disconnected' = 'idle';\n private nextCommandId = 1;\n /**\n * The single active target id under the single-attach model.\n * Updated by `refreshTargets()` whenever a non-null target is present.\n * Used to detect a new (different) target attach and evict the previous one.\n */\n private activeTargetId: string | null = null;\n /** In-flight enableDomains() promise — concurrent callers share it. */\n private enablingPromise: Promise<void> | null = null;\n /** Pending request→response commands keyed by CDP message id. */\n private readonly pending = new Map<\n number,\n { resolve: (result: unknown) => void; reject: (err: Error) => void }\n >();\n\n /**\n * Timestamp (ms since epoch) of the most recent crash/destroy/detach event,\n * or `null` if no crash has been detected since the last `enableDomains()`.\n */\n private lastCrashDetectedAt: number | null = null;\n\n /**\n * Per-target last-seen timestamp (ms since epoch). Updated on any inbound\n * CDP message carrying data from a target. Keyed by target id.\n */\n private readonly targetLastSeenAt = new Map<string, number>();\n\n /** Active heartbeat interval handle (only when `AIT_CDP_HEARTBEAT_MS` is set). */\n private heartbeatHandle: ReturnType<typeof setInterval> | null = null;\n\n /** Lifecycle event listeners (crash / destroyed / detached). */\n private readonly lifecycleListeners: Array<(event: TargetLifecycleEvent) => void> = [];\n\n constructor(options: ChiiCdpConnectionOptions) {\n this.relayBaseUrl = options.relayBaseUrl.replace(/\\/$/, '');\n this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;\n this.totpSecret = options.totpSecret;\n const envMs = process.env.AIT_CDP_COMMAND_TIMEOUT_MS\n ? Number(process.env.AIT_CDP_COMMAND_TIMEOUT_MS)\n : undefined;\n this.commandTimeoutMs =\n (envMs !== undefined && Number.isFinite(envMs) && envMs > 0 ? envMs : undefined) ??\n options.commandTimeoutMs ??\n DEFAULT_COMMAND_TIMEOUT_MS;\n for (const event of PHASE_1_EVENTS) this.buffers.set(event, []);\n // Exception buffer initialized separately — its per-event size cap\n // (EXCEPTION_BUFFER_SIZE=50) is enforced in handleMessage below.\n this.buffers.set('Runtime.exceptionThrown', []);\n // EventEmitter caps listeners at 10 by default; the tool layer may add\n // several short-lived subscriptions, so lift the cap.\n this.emitter.setMaxListeners(0);\n }\n\n /** Refresh the attached-target list from the relay's `GET /targets`. */\n async refreshTargets(): Promise<CdpTarget[]> {\n // When TOTP is active, append a freshly-minted code as `?at=<code>` so the\n // relay's /targets gate (issue #474) accepts this daemon poll. Mirrors the\n // existing /client WS pattern (~line 412). `generateTotp` defaults `when` to\n // Date.now() — rely on that default; never hand-compute the time.\n // SECRET-HANDLING: never log `code` or `this.totpSecret`; the code rides only\n // in the at= param.\n let targetsUrl = `${this.relayBaseUrl}/targets`;\n if (this.totpSecret) {\n const code = generateTotp(this.totpSecret);\n targetsUrl += `?at=${encodeURIComponent(code)}`;\n }\n const res = await fetch(targetsUrl);\n if (!res.ok) {\n throw new Error(`Chii relay /targets returned HTTP ${res.status} ${res.statusText}`);\n }\n const body: unknown = await res.json();\n const list = isObject(body) && Array.isArray(body.targets) ? body.targets : [];\n\n // Single-attach model: find the \"newest\" target id from the relay response.\n // The relay may return multiple targets if the previous session did not cleanly\n // detach. We keep only the last entry (last-attach wins) and evict the previous\n // active target if it differs.\n let newestTargetId: string | null = null;\n for (const item of list) {\n if (!isObject(item) || typeof item.id !== 'string') continue;\n newestTargetId = item.id; // last wins\n }\n\n // Evict previous active target when a genuinely new targetId arrives.\n if (\n newestTargetId !== null &&\n this.activeTargetId !== null &&\n newestTargetId !== this.activeTargetId\n ) {\n const prevId = this.activeTargetId;\n // SECRET-HANDLING: prevTargetId is a Chii internal ID (not a secret) but\n // keep it short — no URL or credentials logged here.\n logInfo('page.detached', { prevTargetId: prevId });\n this.evictTarget(prevId);\n }\n\n // Rebuild the targets map with at most the single newest target.\n this.targets.clear();\n for (const item of list) {\n if (!isObject(item) || typeof item.id !== 'string') continue;\n // Single-attach model: only register the newest target.\n if (item.id !== newestTargetId) continue;\n this.targets.set(item.id, {\n id: item.id,\n title: typeof item.title === 'string' ? item.title : '',\n url: typeof item.url === 'string' ? item.url : '',\n });\n }\n\n if (newestTargetId !== null) {\n this.activeTargetId = newestTargetId;\n } else {\n this.activeTargetId = null;\n }\n\n const result = [...this.targets.values()];\n\n // Emit 'target:attached' for every newly-seen target so waitForFirstTarget()\n // can race against the next refreshTargets() polling round.\n if (newestTargetId !== null) {\n this.emitter.emit('target:attached', result);\n }\n\n return result;\n }\n\n listTargets(): CdpTarget[] {\n return [...this.targets.values()];\n }\n\n /**\n * Waits until at least one target matching `filterFn` is attached, then\n * resolves with the full target list at that moment.\n *\n * Resolution happens on whichever comes first:\n * (a) a `'target:attached'` event from `refreshTargets()` (triggered by\n * the /targets poll finding a new target), OR\n * (b) a `'target:attached'` event from `handleMessage()` (triggered by\n * the first inbound CDP message from a target — confirms the relay\n * websocket has data from the phone, not just a target entry in the map).\n *\n * This dual-signal approach eliminates the polling race that previously\n * caused `wait_for_attach` to resolve before the first CDP message arrived.\n *\n * Falls back to checking `listTargets()` every `pollIntervalMs` in case the\n * EventEmitter is missed (defensive belt-and-suspenders).\n *\n * @param filterFn - Predicate that the returned targets must satisfy.\n * @param timeoutMs - Reject after this many ms (default 90 000).\n * @param pollIntervalMs - Fallback poll interval (default 500ms).\n */\n waitForFirstTarget(\n filterFn: (targets: CdpTarget[]) => boolean,\n timeoutMs = 90_000,\n pollIntervalMs = 500,\n ): Promise<CdpTarget[]> {\n // Fast path: already attached.\n const current = this.listTargets();\n if (filterFn(current)) return Promise.resolve(current);\n\n return new Promise<CdpTarget[]>((resolve, reject) => {\n let settled = false;\n let pollHandle: ReturnType<typeof setInterval> | null = null;\n\n const settle = (targets: CdpTarget[]): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutHandle);\n if (pollHandle !== null) {\n clearInterval(pollHandle);\n pollHandle = null;\n }\n this.emitter.off('target:attached', onAttach);\n resolve(targets);\n };\n\n const onAttach = (targets: CdpTarget[]): void => {\n if (filterFn(targets)) settle(targets);\n };\n\n const timeoutHandle = setTimeout(() => {\n if (settled) return;\n settled = true;\n if (pollHandle !== null) {\n clearInterval(pollHandle);\n pollHandle = null;\n }\n this.emitter.off('target:attached', onAttach);\n reject(\n new Error(\n `waitForFirstTarget: 타임아웃 (${timeoutMs}ms) — 폰이 relay에 attach되지 않았습니다.`,\n ),\n );\n }, timeoutMs);\n\n // Primary: event-driven path.\n this.emitter.on('target:attached', onAttach);\n\n // Fallback: polling path — also calls refreshTargets() to keep the in-memory\n // target map up-to-date. This ensures the polling path works even without\n // a live WebSocket (pre-enableDomains) and catches targets that appear\n // between 'target:attached' events.\n pollHandle = setInterval(() => {\n // Refresh from relay, then check. Errors are ignored — we keep polling.\n this.refreshTargets().then(\n (targets) => {\n if (filterFn(targets)) settle(targets);\n },\n () => {\n // Relay temporarily unreachable — keep polling.\n },\n );\n }, pollIntervalMs);\n });\n }\n\n /**\n * Timestamp (ms since epoch) of the most recent crash/destroy/detach event\n * detected since the last `enableDomains()` call, or `null` if none.\n */\n getLastCrashDetectedAt(): number | null {\n return this.lastCrashDetectedAt;\n }\n\n /**\n * Last-seen timestamp (ms since epoch) for a given target id, or `null` if\n * the target is unknown / no message has been received from it yet.\n */\n getTargetLastSeenAt(targetId: string): number | null {\n return this.targetLastSeenAt.get(targetId) ?? null;\n }\n\n /** Subscribe to target lifecycle events (crash / destroyed / detached). */\n onLifecycle(listener: (event: TargetLifecycleEvent) => void): () => void {\n this.lifecycleListeners.push(listener);\n return () => {\n const idx = this.lifecycleListeners.indexOf(listener);\n if (idx !== -1) this.lifecycleListeners.splice(idx, 1);\n };\n }\n\n /**\n * Connect a client websocket to the first attached target and enable Phase 1\n * domains. Resolves once the socket is open and enable commands are sent.\n */\n async enableDomains(): Promise<void> {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) return;\n // If a connect attempt is already in-flight, await it rather than racing\n // to open a second websocket that would overwrite `this.ws` and leak the first.\n if (this.enablingPromise) return this.enablingPromise;\n this.enablingPromise = this._doEnableDomains().finally(() => {\n this.enablingPromise = null;\n });\n return this.enablingPromise;\n }\n\n private async _doEnableDomains(): Promise<void> {\n const targets = await this.refreshTargets();\n const target = targets[0];\n if (!target) {\n throw new Error('No mini-app page attached to the Chii relay yet.');\n }\n\n const wsBase = this.relayBaseUrl.replace(/^http/, 'ws');\n const clientId = `devtools-mcp-${Date.now()}`;\n let clientUrl = `${wsBase}/client/${clientId}?target=${encodeURIComponent(target.id)}`;\n // Append a freshly-minted TOTP code so the relay's WS upgrade gate\n // (chii-relay.ts verifyAuth) accepts this /client upgrade. Minted per-connect\n // so reconnects stay inside the 90s acceptance window. generateTotp defaults\n // `when` to Date.now() — rely on that default; never hand-compute the time.\n // SECRET-HANDLING: never log `code` or `this.totpSecret`; the code rides only\n // in the URL query.\n if (this.totpSecret) {\n const code = generateTotp(this.totpSecret);\n clientUrl += `&at=${encodeURIComponent(code)}`;\n }\n const ws = new WebSocket(clientUrl);\n this.ws = ws;\n\n await new Promise<void>((resolve, reject) => {\n ws.once('open', () => resolve());\n ws.once('error', (err: Error) => reject(err));\n // Issue #478: the relay rejects auth with accept-then-close (4401)\n // instead of a raw 401 destroy, so a rejected dial no longer surfaces\n // as an 'error' event. 'open' always precedes the close frame, making\n // this reject a settled-promise no-op in practice — kept as a defensive\n // boundary (and for any relay that closes before open). The post-open\n // 4401 is recognised by the persistent close handler below.\n ws.once('close', (code: number) => {\n if (code === RELAY_AUTH_REJECT_CLOSE_CODE) {\n reject(\n new Error(\n 'relay 인증(TOTP)이 거부됐습니다 (close 4401). 코드가 만료됐을 수 있습니다 — 재연결 시 새 코드가 발급됩니다.',\n ),\n );\n }\n });\n });\n\n // Reset crash state when a new connection is established.\n this.lastCrashDetectedAt = null;\n this.targetLastSeenAt.clear();\n // activeTargetId is already set by refreshTargets() above; don't reset here.\n\n this.connectionState = 'connected';\n ws.on('message', (data: WebSocket.RawData) => this.handleMessage(data.toString()));\n // Issue #478: close 4401 is the relay's named TOTP rejection\n // (accept-then-close) — name it as an auth failure instead of a generic\n // drop. #439's per-connect fresh code mint means this should not happen in\n // practice; defensive alignment with the relay contract.\n ws.on('close', (code: number) =>\n this.handleDisconnect(\n code === RELAY_AUTH_REJECT_CLOSE_CODE\n ? 'relay 인증(TOTP)이 거부돼 연결이 종료됐습니다 (close 4401)'\n : 'relay WebSocket 연결이 끊겼습니다',\n ),\n );\n ws.on('error', (err: Error) => this.handleDisconnect(`relay WebSocket 오류: ${err.message}`));\n\n this.sendFireAndForget('Runtime.enable');\n this.sendFireAndForget('Network.enable');\n // DOM/Page domains back the Phase 2 command tools; Chii answers their\n // request→response commands once enabled.\n this.sendFireAndForget('DOM.enable');\n this.sendFireAndForget('Page.enable');\n // Subscribe to page-level crash and target lifecycle events.\n // Inspector.targetCrashed fires when a page OOM/JS-crash/native-bridge crash.\n // Target.setDiscoverTargets enables Target.targetDestroyed + Target.detachedFromTarget.\n this.sendFireAndForget('Inspector.enable');\n this.sendFireAndForget('Target.setDiscoverTargets', { discover: true });\n\n // Optional heartbeat: env AIT_CDP_HEARTBEAT_MS=N enables a ping loop.\n this.startHeartbeat(target.id);\n }\n\n /** Fire-and-forget CDP message (used for `*.enable`, no result awaited). */\n private sendFireAndForget(method: string, params: Record<string, unknown> = {}): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n const id = this.nextCommandId++;\n this.ws.send(JSON.stringify({ id, method, params }));\n }\n\n /**\n * Issue a CDP command and resolve with its result (Phase 2). Rejects on a CDP\n * error frame or when no websocket is open (no page attached yet).\n */\n send<M extends CdpCommandName>(\n method: M,\n params?: CdpCommandMap[M]['params'],\n ): Promise<CdpCommandMap[M]['result']> {\n return this.sendCommand(method, (params ?? {}) as Record<string, unknown>) as Promise<\n CdpCommandMap[M]['result']\n >;\n }\n\n /**\n * Issue an arbitrary request→response command over the relay and resolve with\n * its raw result. Both the typed CDP {@link send} and the AIT domain (Phase 3\n * `AIT.*` methods, forwarded over the same Chii channel) build on this.\n *\n * Rejects immediately if the connection is disconnected (fail-fast — no\n * auto-reconnect). Caller should re-run `list_pages` or `enableDomains` to\n * reattach.\n *\n * Times out after `commandTimeoutMs` (default 30s, env\n * `AIT_CDP_COMMAND_TIMEOUT_MS`). On timeout the pending entry is cleaned up\n * and the promise rejects with a descriptive Korean error.\n */\n sendCommand(method: string, params: Record<string, unknown> = {}): Promise<unknown> {\n // Fail-fast: connection already known to be dead — don't write into a dead socket.\n if (this.connectionState === 'disconnected') {\n return Promise.reject(\n new Error(\n `relay에 연결되어 있지 않습니다 (${method}). list_pages로 attach 상태를 확인하고 enableDomains()로 재연결하세요.`,\n ),\n );\n }\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n return Promise.reject(\n new Error('No mini-app page attached to the Chii relay yet. Call enableDomains() first.'),\n );\n }\n const id = this.nextCommandId++;\n const ws = this.ws;\n const timeoutMs = this.commandTimeoutMs;\n return new Promise<unknown>((resolve, reject) => {\n const handle = setTimeout(() => {\n this.pending.delete(id);\n reject(\n new Error(\n `CDP 명령이 타임아웃됐습니다 (${method}, ${timeoutMs}ms). ` +\n '폰 측 토스 앱이 백그라운드로 내려갔거나 미니앱이 unload됐을 수 있습니다. ' +\n 'list_pages로 attach 상태를 확인하세요.',\n ),\n );\n }, timeoutMs);\n this.pending.set(id, {\n resolve: (v) => {\n clearTimeout(handle);\n resolve(v);\n },\n reject: (e) => {\n clearTimeout(handle);\n reject(e);\n },\n });\n ws.send(JSON.stringify({ id, method, params }));\n });\n }\n\n /**\n * Called on WebSocket `close` or `error` after a successful connection.\n * Rejects all pending commands and marks the connection as disconnected so\n * subsequent `sendCommand` calls fail fast (no auto-reconnect).\n */\n private handleDisconnect(reason: string): void {\n if (this.connectionState === 'disconnected') return; // already handled\n this.connectionState = 'disconnected';\n this.ws = null;\n this.stopHeartbeat();\n const err = new Error(\n `${reason}. list_pages로 attach 상태를 확인하고 enableDomains()로 재연결하세요.`,\n );\n for (const waiter of this.pending.values()) {\n waiter.reject(err);\n }\n this.pending.clear();\n }\n\n /**\n * Evict a previously active target under the single-attach model.\n * Rejects pending commands with a 'replaced-by-new-attach' reason and emits\n * a 'replaced' lifecycle event. Does NOT clear all targets — only the specific\n * targetId. The caller is responsible for rebuilding the targets map afterwards.\n *\n * The error message uses 'replaced-by-new-attach' so test assertions can match it.\n */\n private evictTarget(targetId: string): void {\n const detectedAt = new Date().toISOString();\n this.targets.delete(targetId);\n this.targetLastSeenAt.delete(targetId);\n\n const err = new Error(\n `[ait-debug] replaced-by-new-attach — 이전 page 세션이 새 attach로 교체됐습니다 (targetId=${targetId}). ` +\n 'list_pages로 현재 attach 상태를 확인하세요.',\n );\n for (const waiter of this.pending.values()) {\n waiter.reject(err);\n }\n this.pending.clear();\n\n const event: TargetLifecycleEvent = { kind: 'replaced', targetId, detectedAt };\n for (const listener of this.lifecycleListeners) {\n try {\n listener(event);\n } catch {\n // Listeners must not crash the connection.\n }\n }\n }\n\n /**\n * Handle a page-level crash or target destruction event.\n * Removes the target from the in-memory map, rejects all pending commands,\n * and emits a lifecycle event.\n *\n * @param kind - Event kind: 'crashed' | 'destroyed' | 'detached'\n * @param targetId - The target ID from the event params (may be null for\n * Inspector.targetCrashed which has no targetId in the params).\n */\n private handleTargetGone(kind: TargetLifecycleEvent['kind'], targetId: string | null): void {\n const detectedAt = new Date().toISOString();\n this.lastCrashDetectedAt = Date.now();\n\n // Remove matching target(s) from the in-memory map.\n if (targetId !== null) {\n this.targets.delete(targetId);\n this.targetLastSeenAt.delete(targetId);\n // Also clear activeTargetId when the active target is gone.\n if (this.activeTargetId === targetId) {\n this.activeTargetId = null;\n }\n } else {\n // Inspector.targetCrashed carries no targetId — clear all targets.\n this.targets.clear();\n this.targetLastSeenAt.clear();\n this.activeTargetId = null;\n }\n\n // Reject pending commands with a descriptive Korean error.\n const label =\n kind === 'crashed'\n ? 'page crash (Inspector.targetCrashed)'\n : kind === 'destroyed'\n ? 'target 종료 (Target.targetDestroyed)'\n : 'target detach (Target.detachedFromTarget)';\n const err = new Error(\n `[ait-debug] ${label} 감지됨 — relay에서 제거됐습니다. ` +\n '새 attach가 필요합니다 (list_pages로 확인 → enableDomains()로 재연결).',\n );\n for (const waiter of this.pending.values()) {\n waiter.reject(err);\n }\n this.pending.clear();\n\n // Notify lifecycle listeners.\n const event: TargetLifecycleEvent = { kind, targetId, detectedAt };\n for (const listener of this.lifecycleListeners) {\n try {\n listener(event);\n } catch {\n // Listeners must not crash the connection.\n }\n }\n }\n\n /**\n * Start the optional CDP heartbeat loop.\n *\n * When `AIT_CDP_HEARTBEAT_MS` is set to a positive integer, every interval\n * we send `Runtime.evaluate({expression: '1'})` to each active target. If\n * the command times out (2 s hard deadline) or errors, we treat the target\n * as dead and call `handleTargetGone`.\n *\n * This is a zombie-detector fallback: cloudflared keeps-alive the tunnel ws\n * even when the phone app has crashed, so the ws-level disconnect (#252) won't\n * fire. The heartbeat catches this gap.\n *\n * Default: OFF. Only activates when `AIT_CDP_HEARTBEAT_MS` is set.\n */\n private startHeartbeat(initialTargetId: string): void {\n this.stopHeartbeat(); // clear any previous interval\n\n const envMs = process.env.AIT_CDP_HEARTBEAT_MS\n ? Number(process.env.AIT_CDP_HEARTBEAT_MS)\n : undefined;\n if (envMs === undefined || !Number.isFinite(envMs) || envMs <= 0) return;\n\n const PING_TIMEOUT_MS = 2_000;\n\n this.heartbeatHandle = setInterval(() => {\n // Take a snapshot of current targets to avoid mutation during iteration.\n const targetIds = this.targets.size > 0 ? [...this.targets.keys()] : [initialTargetId];\n for (const targetId of targetIds) {\n // Issue a lightweight eval with a 2 s deadline.\n const pingPromise = this.sendCommand('Runtime.evaluate', {\n expression: '1',\n returnByValue: true,\n timeout: PING_TIMEOUT_MS,\n });\n const timeoutPromise = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error('heartbeat timeout')),\n PING_TIMEOUT_MS + 500, // slightly longer than the CDP timeout\n ),\n );\n Promise.race([pingPromise, timeoutPromise]).catch(() => {\n // Ping failed: mark target as dead if it still exists in the map.\n if (this.targets.has(targetId)) {\n this.handleTargetGone('destroyed', targetId);\n }\n });\n }\n }, envMs) as unknown as ReturnType<typeof setInterval>;\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatHandle !== null) {\n clearInterval(this.heartbeatHandle);\n this.heartbeatHandle = null;\n }\n }\n\n private handleMessage(raw: string): void {\n const message = parseInbound(raw);\n if (!message) return;\n\n // Command response (has an id matching a pending request).\n if (typeof message.id === 'number' && this.pending.has(message.id)) {\n const waiter = this.pending.get(message.id);\n this.pending.delete(message.id);\n if (waiter) {\n if (message.error) waiter.reject(new Error(message.error.message));\n else waiter.resolve(message.result);\n }\n return;\n }\n\n // Any inbound message implies the connection is active — update lastSeenAt\n // for whichever target we currently know about (single-target model).\n // Also emit 'target:attached' on the first inbound message from a target\n // (targetLastSeenAt unset) so waitForFirstTarget() resolves on first CDP\n // message, not just on the next /targets poll.\n const now = Date.now();\n let firstMessageSeen = false;\n for (const targetId of this.targets.keys()) {\n if (!this.targetLastSeenAt.has(targetId)) {\n firstMessageSeen = true;\n }\n this.targetLastSeenAt.set(targetId, now);\n }\n if (firstMessageSeen && this.targets.size > 0) {\n this.emitter.emit('target:attached', [...this.targets.values()]);\n }\n\n if (typeof message.method !== 'string') return;\n\n // --- Target lifecycle events ---\n\n // Inspector.targetCrashed: page OOM / JS exception / native bridge crash.\n // Params are usually empty; no targetId field in the event.\n if (message.method === 'Inspector.targetCrashed') {\n this.handleTargetGone('crashed', null);\n return;\n }\n\n // Target.targetDestroyed: params = { targetId: string }\n if (message.method === 'Target.targetDestroyed') {\n const targetId =\n isObject(message.params) && typeof message.params.targetId === 'string'\n ? message.params.targetId\n : null;\n this.handleTargetGone('destroyed', targetId);\n return;\n }\n\n // Target.detachedFromTarget: params = { sessionId, targetId? }\n if (message.method === 'Target.detachedFromTarget') {\n const targetId =\n isObject(message.params) && typeof message.params.targetId === 'string'\n ? message.params.targetId\n : null;\n this.handleTargetGone('detached', targetId);\n return;\n }\n\n // --- Phase 1 event stream (buffered ring-buffer) ---\n if (!this.buffers.has(message.method as CdpEventName)) return;\n const event = message.method as CdpEventName;\n const buffer = this.buffers.get(event);\n if (!buffer) return;\n buffer.push(message.params);\n // Runtime.exceptionThrown uses a dedicated smaller cap (50); all other\n // Phase 1 events use the default bufferSize (500).\n const cap = event === 'Runtime.exceptionThrown' ? EXCEPTION_BUFFER_SIZE : this.bufferSize;\n if (buffer.length > cap) buffer.shift();\n this.emitter.emit(event, message.params);\n }\n\n getBufferedEvents<E extends CdpEventName>(event: E): ReadonlyArray<CdpEventMap[E]> {\n const buffer = this.buffers.get(event);\n return (buffer ?? []) as ReadonlyArray<CdpEventMap[E]>;\n }\n\n on<E extends CdpEventName>(event: E, listener: (payload: CdpEventMap[E]) => void): () => void {\n this.emitter.on(event, listener as (payload: unknown) => void);\n return () => this.emitter.off(event, listener as (payload: unknown) => void);\n }\n\n /** Close the relay client websocket and reject any in-flight commands. */\n close(): void {\n const ws = this.ws;\n this.stopHeartbeat();\n // handleDisconnect clears this.ws and pending; call it first so the 'close'\n // event from ws.close() below is a no-op (already disconnected).\n this.handleDisconnect('Chii relay connection closed');\n ws?.close();\n }\n}\n","/**\n * Boots the local Chii relay server.\n *\n * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome\n * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.\n * The relay accepts a `target` websocket from the phone's injected `target.js`\n * and `client` websockets from CDP frontends (our MCP connection).\n *\n * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app\n * entries.\n *\n * TOTP auth (relay-side, authoritative gate):\n * When `verifyAuth` is provided, this module gates both inbound surfaces:\n *\n * - HTTP 'request': a listener registered BEFORE `chii.start({server})`.\n * Node's `http.Server` calls listeners in registration order; the first\n * to call `res.end()` wins. Invalid auth → 401 + CORS header + a tiny\n * JSON body (`{\"error\":\"totp-rejected\"}`) so a cross-origin script\n * `fetch()` probe can READ the status (issue #478). Valid auth → return\n * without side-effect (chii's Koa handler serves it).\n *\n * - WS 'upgrade': after `chii.start()` has registered chii's own upgrade\n * listener, we take over the upgrade chain (remove chii's listeners,\n * re-dispatch manually). Invalid auth → accept-then-close: complete the\n * handshake via a `noServer` WebSocketServer, then immediately close\n * with code 4401 reason 'totp-rejected' (issue #478). A raw 401 +\n * `socket.destroy()` only ever surfaced as close code 1006 in the\n * browser — indistinguishable from a tunnel failure, which left the\n * env-2 phone UI silent. The explicit dispatch (not listener ordering)\n * is what keeps chii away from rejected sockets: accept-then-close\n * leaves the socket alive, so an order-based early-return would let\n * chii's later listener complete a SECOND handshake on the same socket\n * — an auth bypass. Valid auth → forward to chii's captured listeners.\n *\n * TOTP code transports (issue #466) — two equivalent ways to carry the code:\n * 1. Query param `at=<code>` — used by the daemon-side `/client` connection\n * (`chii-connection.ts` appends it; it holds the secret).\n * 2. Path prefix `/at/<code>/…` — used by the phone-side target. Chii's\n * stock `target.js` derives its WS endpoint from the script `src`\n * (`scriptEl.src.replace('target.js','')`), so the only way for the\n * phone to carry a code is to embed it in the script URL path. The\n * in-app attach injects `https://<host>/at/<code>/target.js`; both the\n * script fetch and the derived `wss://<host>/at/<code>/target/<id>` WS\n * dial then carry the prefix. The listeners below rewrite the prefix\n * into the query form (`rewriteAtPathPrefix`) and MUTATE `req.url`\n * before chii's own handlers (registered later) parse it — chii only\n * ever sees the stripped URL.\n *\n * Threat model: \"URL leak\" — someone obtains the tunnel URL (Slack paste, QR\n * screenshot, shoulder-surfing) but does not have the shared TOTP secret.\n * Rotating 6-digit code makes the URL stale after 30 s.\n * A determined attacker who extracts the secret from the dogfood bundle can\n * still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).\n *\n * SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear\n * in any log, error message, or process output. `verifyAuth` is a black-box\n * predicate from the caller's perspective; this module only forwards pass/fail.\n */\n\nimport { createServer, type IncomingMessage, type Server } from 'node:http';\nimport { createRequire } from 'node:module';\nimport type { AddressInfo } from 'node:net';\nimport type { Duplex } from 'node:stream';\n// `ws` is a direct dependency of this package (NOT a transitive reach into\n// chii's tree — same principle as the ajv incident): the reject path below\n// needs `WebSocketServer.handleUpgrade` to complete a handshake we are about\n// to close with a named code.\nimport { type WebSocket, WebSocketServer } from 'ws';\nimport {\n RELAY_AUTH_REJECT_CLOSE_CODE,\n RELAY_AUTH_REJECT_REASON,\n} from '../shared/relay-auth-close.js';\n\nconst require = createRequire(import.meta.url);\n\n/**\n * WS keepalive ping interval (ms).\n *\n * Cloudflare proxied connections are dropped after ~100 s of no traffic.\n * 45 s comfortably fits inside that window and lets both the phone-target leg\n * and the daemon-client leg survive idle CDP sessions.\n */\nconst DEFAULT_KEEPALIVE_INTERVAL_MS = 45_000;\n\n/**\n * Minimal shape of chii's internal WebSocketServer instance.\n *\n * `chii/server/lib/WebSocketServer` holds the real `ws.Server` in `_wss`.\n * `_wss.clients` is the standard `Set<WebSocket>` tracking all live sockets.\n * We access this to ping every connected socket — no chii internals beyond\n * this single field are touched.\n */\ninterface ChiiInternalWss {\n _wss: { clients: Set<WebSocket> };\n start(server: import('node:http').Server): void;\n}\n\n/**\n * Loads chii's internal WebSocketServer class and returns it together with a\n * flag indicating whether the real class was found.\n *\n * Returns `null` if the internal path is not resolvable (future chii release\n * changes the layout) — callers skip keepalive gracefully.\n */\nfunction tryLoadChiiWssClass(): (new () => ChiiInternalWss) | null {\n try {\n const mod: unknown = require('chii/server/lib/WebSocketServer');\n if (typeof mod === 'function') {\n return mod as new () => ChiiInternalWss;\n }\n } catch {\n // Module not found or shape changed — keepalive will be skipped.\n }\n return null;\n}\n\n/**\n * Calls `chii.start()` and returns the chii `WebSocketServer` instance that\n * was constructed during the call.\n *\n * How: `chii/server/index.js`'s `start()` creates `new WebSocketServer()`\n * where `WebSocketServer` is captured from `require('./lib/WebSocketServer')`\n * at module load time. The class reference is stable, so we can temporarily\n * patch `ChiiWssClass.prototype.start` — which runs *on the instance* —\n * to record `this` before the original `start` runs.\n *\n * The patch is installed before `chii.start()` and removed (via `finally`)\n * immediately after, so concurrent `startChiiRelay` calls nest correctly: each\n * call's patch overrides the previous in the prototype chain for the duration\n * of its own `chii.start()` call, restoring the prior descriptor on exit.\n *\n * If `ChiiWssClass` is null (internal path changed in a future chii release),\n * `chii.start()` runs unpatched and the function returns null — callers skip\n * keepalive gracefully without affecting relay correctness.\n */\nasync function startChiiWithCapture(\n chii: ChiiServerModule,\n startOptions: Parameters<ChiiServerModule['start']>[0],\n ChiiWssClass: (new () => ChiiInternalWss) | null,\n): Promise<ChiiInternalWss | null> {\n if (ChiiWssClass === null) {\n await chii.start(startOptions);\n return null;\n }\n\n let captured: ChiiInternalWss | null = null;\n const proto = ChiiWssClass.prototype as ChiiInternalWss;\n const originalStart = proto.start;\n\n proto.start = function (this: ChiiInternalWss, server) {\n captured = this;\n return originalStart.call(this, server);\n };\n\n try {\n await chii.start(startOptions);\n } finally {\n // Always restore — even if chii.start() throws.\n proto.start = originalStart;\n }\n\n return captured;\n}\n\n/** `chii/server` is CommonJS and shipped without TypeScript types. */\ninterface ChiiServerModule {\n start(options: {\n port?: number;\n host?: string;\n domain?: string;\n server?: Server;\n basePath?: string;\n }): Promise<void>;\n}\n\nfunction loadChiiServer(): ChiiServerModule {\n // `chii`'s package `main` is `./server/index.js`, exposing `{ start }`.\n const mod: unknown = require('chii');\n if (\n typeof mod === 'object' &&\n mod !== null &&\n 'start' in mod &&\n typeof (mod as { start: unknown }).start === 'function'\n ) {\n return mod as ChiiServerModule;\n }\n throw new Error('chii server module did not expose start()');\n}\n\nexport interface ChiiRelay {\n port: number;\n /** Base URL for the relay HTTP/WS server, e.g. `http://127.0.0.1:54321`. */\n baseUrl: string;\n close(): Promise<void>;\n}\n\n/**\n * Secret-free metadata about a single auth rejection (issue #467).\n *\n * SECRET-HANDLING: this event carries ONLY the surface kind. It must never\n * grow fields for `req.url`, query strings, codes, or secrets — observers\n * (diagnostics counters, console hints) only need \"a rejection happened\".\n */\nexport interface RelayAuthRejectEvent {\n /** Which inbound surface was rejected. */\n kind: 'ws-upgrade' | 'http-request';\n}\n\n/**\n * Rewrites a `/at/<code>/…` path-prefixed request URL into the equivalent\n * query-based form, e.g.:\n *\n * `/at/123456/target.js` → `/target.js?at=123456`\n * `/at/123456/target/x?url=u` → `/target/x?url=u&at=123456`\n * `/at/123456/` → `/?at=123456`\n *\n * Returns `null` when the URL does not carry the prefix (including an empty\n * code segment) — callers fall back to the unmodified URL and the existing\n * query-based auth path.\n *\n * Pure string surgery — this function knows nothing about secrets or code\n * validity; verification stays inside the caller-provided `verifyAuth`\n * predicate (which parses the query). The raw path segment is appended\n * verbatim to the query: both path segments and query values are\n * percent-decoded exactly once by their consumers, so no re-encoding is\n * needed (TOTP codes are 6 digits and never percent-encoded in practice).\n */\nexport function rewriteAtPathPrefix(rawUrl: string): string | null {\n const match = /^\\/at\\/([^/?]+)(\\/[^?]*)?(\\?.*)?$/.exec(rawUrl);\n if (match === null) return null;\n const code = match[1];\n const path = match[2] === undefined || match[2] === '' ? '/' : match[2];\n const query = match[3] ?? '';\n const separator = query === '' ? '?' : '&';\n return `${path}${query}${separator}at=${code}`;\n}\n\nexport interface StartChiiRelayOptions {\n /**\n * Local port for the relay. Default 0 (OS-assigned ephemeral port).\n *\n * Using 0 means the OS picks a free port — this is the safe default because\n * a stale cloudflared child process (PPID 1, orphaned after SIGKILL) may still\n * be holding a fixed port. A fixed port causes EADDRINUSE on the next startup,\n * which makes the MCP handshake fail with -32000. With port 0 the new relay\n * always gets a fresh port, making any orphaned process harmless.\n *\n * Pass an explicit number to restore fixed-port behaviour (backwards-compatible).\n */\n port?: number;\n /** Bind host. Default 127.0.0.1 (tunnel reaches it locally). */\n host?: string;\n /**\n * Optional auth predicate for WebSocket upgrade requests.\n *\n * When provided, every inbound WebSocket upgrade is checked by calling\n * `verifyAuth(req)` before Chii processes it. Return `true` to allow the\n * upgrade; return `false` to reject with HTTP 401 and destroy the socket.\n *\n * The predicate MUST NOT log the secret or any TOTP code — it is a black-box\n * from this module's perspective.\n *\n * @param req - The raw HTTP `IncomingMessage` from the upgrade handshake.\n * Inspect `req.url` for query parameters (e.g. `at=<code>`). Path-prefixed\n * URLs (`/at/<code>/…`, the phone-target transport — issue #466) are\n * rewritten into the query form BEFORE this predicate runs, so a\n * query-only predicate covers both transports.\n * @returns `true` if the upgrade is authorised, `false` to reject.\n */\n verifyAuth?: (req: IncomingMessage) => boolean;\n /**\n * Secret-free observability callback fired on every auth rejection\n * (issue #467). Only meaningful together with `verifyAuth`.\n *\n * SECRET-HANDLING: the event carries ONLY the rejection kind — never\n * `req.url`, query strings, TOTP codes, or the secret. Implementations must\n * keep it that way (e.g. increment a counter + timestamp). Exceptions thrown\n * by the callback are swallowed so observability can never break the gate.\n */\n onAuthReject?: (event: RelayAuthRejectEvent) => void;\n /**\n * WS protocol ping interval in milliseconds (issue #483).\n *\n * The relay sends a ping frame to every connected WebSocket at this interval\n * so that Cloudflare's proxied-connection idle timer (~100 s) is reset for\n * both the phone-target leg and the daemon-client leg. The peer responds with\n * a pong automatically (browser / ws library behaviour) — no application\n * code change is needed on either end.\n *\n * Default: 45 000 ms (45 s). Set to 0 to disable keepalive entirely.\n *\n * Pass a small value in tests to avoid real-time waits — pair with fake\n * timers (`vi.useFakeTimers()`) or a short sleep.\n */\n keepaliveIntervalMs?: number;\n}\n\n/**\n * Starts the Chii relay and resolves once listening.\n *\n * Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral\n * port on every start, so a stale cloudflared orphan holding any particular\n * port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`\n * always reflect the actual bound port.\n *\n * chii.start() is called with `server` (our pre-created httpServer) BEFORE\n * httpServer.listen(). This is intentional: chii attaches its Koa handler and\n * WS upgrade listener to the server object, but the actual TCP bind is\n * performed by our httpServer.listen() call below. The `port`/`domain` values\n * passed to chii.start() are used for display/banner purposes inside chii and\n * do not affect which port the server binds. The connection path (clients\n * connecting to `relay.baseUrl`) always uses the post-listen confirmed port.\n */\nexport async function startChiiRelay(options: StartChiiRelayOptions = {}): Promise<ChiiRelay> {\n const requestedPort = options.port ?? 0;\n const host = options.host ?? '127.0.0.1';\n const { verifyAuth, onAuthReject } = options;\n const keepaliveIntervalMs =\n options.keepaliveIntervalMs !== undefined\n ? options.keepaliveIntervalMs\n : DEFAULT_KEEPALIVE_INTERVAL_MS;\n\n const httpServer = createServer();\n\n // Secret-free observability hook (issue #467). Swallow callback exceptions —\n // a broken observer must never turn into an open gate or a crashed relay.\n const notifyAuthReject = (kind: RelayAuthRejectEvent['kind']): void => {\n if (onAuthReject === undefined) return;\n try {\n onAuthReject({ kind });\n } catch {\n // Ignore — observability is best-effort.\n }\n };\n\n // Register the HTTP-request auth listener BEFORE chii.start() so it fires\n // first. Node's http.Server emits 'request' to all listeners in registration\n // order; the first to end() the response wins. Valid requests return without\n // side-effect so chii's own handler takes over normally — and because\n // listeners run synchronously in order, mutating `req.url` here (path-prefix\n // strip, issue #466) means chii's later-registered handler only ever sees\n // the stripped URL.\n //\n // We only register when verifyAuth is provided so the no-auth path is\n // zero-overhead for tests and local-only dev sessions. (The phone-side\n // `/at/<code>/` prefix only ever appears when TOTP is armed — the launcher\n // QR carries the `at` code — so the no-auth path never needs the strip.)\n if (verifyAuth) {\n // Plain HTTP requests: two cases are gated, everything else passes through.\n //\n // Case 1 — path-prefixed form (`/at/<code>/…`): the phone fetches\n // `target.js` via `https://<host>/at/<code>/target.js` (issue #466).\n // The prefix is rewritten to the query form and then verified; chii's Koa\n // static handler sees the stripped URL.\n //\n // Case 2 — `/targets` read route (issue #474): without gating this route a\n // URL-leaker (threat model: someone who obtained the tunnel URL but not\n // the secret) can read session metadata (id/url/title, including any query\n // params in the page URL) without a code. Debugger attach stays blocked by\n // the WS gate, but /targets is an HTTP read that was previously ungated.\n // The `at` code may arrive as a query param (`/targets?at=<code>`) — which\n // buildRelayVerifyAuth already handles — or via the `/at/<code>/` path\n // prefix (rewriteAtPathPrefix normalises that to the query form first).\n //\n // Static assets (target.js, chii front-end HTML/JS/CSS) and any other\n // non-prefixed, non-/targets request keep today's ungated pass-through — the\n // phone fetches some via the legacy no-prefix path and gating them would\n // break env-2/3/4.\n //\n // SECRET-HANDLING: We do NOT log req.url or any auth value in this listener.\n httpServer.on('request', (req, res) => {\n const rewritten = rewriteAtPathPrefix(req.url ?? '');\n if (rewritten !== null) {\n // Path-prefix form: normalise to query form, then verify.\n req.url = rewritten;\n if (!verifyAuth(req)) {\n // CORS header + tiny JSON body (issue #478): the script URL is\n // cross-origin from the phone page (tunnel origin ≠ relay origin), so\n // without ACAO a fetch() probe sees an opaque error and cannot tell\n // auth rejection from a network failure. The header rides ONLY on\n // this error response — no relay asset is exposed through it.\n res.statusCode = 401;\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));\n notifyAuthReject('http-request');\n }\n // Auth passed (or was rejected above): return so the path-prefix branch\n // never falls through to the /targets check below.\n return;\n }\n\n // Non-prefixed request: check if this is the /targets read route (issue\n // #474). Extract the pathname robustly — req.url is a raw path+query\n // string like `/targets?at=123456` so we split on `?`.\n const pathname = (req.url ?? '').split('?')[0];\n if (pathname === '/targets' || pathname === '/targets/') {\n // The `at` code must be present as a query param — verifyAuth reads it\n // from req.url via URLSearchParams, which already handles `?at=<code>`\n // without any URL rewrite needed.\n if (!verifyAuth(req)) {\n // Same 401 shape as the path-prefix branch (issue #478 contract).\n res.statusCode = 401;\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));\n notifyAuthReject('http-request');\n // res.end() wins — chii's Koa handler will not write.\n return;\n }\n // Auth passed: return without ending the response. Node invokes every\n // 'request' listener in registration order, so chii's Koa listener\n // (registered later by chii.start) still runs and serves the /targets\n // JSON — this return only means \"this gate listener is done\", not \"end\n // the response\".\n return;\n }\n\n // Any other non-prefixed request (static assets, chii front-end, etc.):\n // ungated pass-through to chii. Auth passed: no-op — chii's Koa\n // 'request' listener (registered below by chii.start) serves the URL.\n // (Koa skips writing when an earlier listener already ended the response,\n // so the 401 paths above are safe even though Koa still runs.)\n });\n }\n\n // WS keepalive (issue #483): capture chii's WebSocketServer instance so we\n // can read `_wss.clients` and send periodic ping frames.\n //\n // `chii/server/index.js`'s start() creates `new WebSocketServer()` but\n // doesn't expose the instance. We capture it by temporarily patching\n // `ChiiWssClass.prototype.start` — that method runs on the instance, so\n // `this` gives us the reference we need.\n //\n // The patch is installed for the duration of one `chii.start()` call and\n // removed in a `finally` block, so concurrent relays nest correctly. If the\n // internal path changes in a future chii release (tryLoadChiiWssClass returns\n // null), chii.start() runs unpatched and the keepalive loop is silently\n // skipped — relay correctness is unaffected.\n const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;\n const capturedChiiWss = await startChiiWithCapture(\n loadChiiServer(),\n { server: httpServer, domain: `${host}:${requestedPort}`, port: requestedPort },\n chiiWssClass,\n );\n\n // WS upgrade gate (issue #478, accept-then-close): take over the upgrade\n // chain AFTER chii.start() has registered chii's own upgrade listener.\n // Listener ordering alone protected chii when rejection meant\n // socket.destroy(); accept-then-close keeps the socket ALIVE, so chii's\n // listener (which always runs on every 'upgrade' emit) would complete a\n // second handshake on the rejected socket — frames after our close frame\n // would reach chii's server-side WebSocket, i.e. an auth bypass. Capturing\n // chii's listeners and re-dispatching only on auth pass closes that hole.\n if (verifyAuth) {\n const chiiUpgradeListeners = httpServer.listeners('upgrade') as Array<\n (req: IncomingMessage, socket: Duplex, head: Buffer) => void\n >;\n httpServer.removeAllListeners('upgrade');\n // noServer: handshake-only — never binds a port; used purely to send a\n // spec-compliant close frame with a code the browser can read.\n const rejectWss = new WebSocketServer({ noServer: true });\n httpServer.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer) => {\n // Phone-target transport (issue #466): normalise a `/at/<code>/…` path\n // prefix into the query form before verification, and strip it from the\n // URL chii will see. No-prefix URLs pass through untouched (daemon\n // client query transport — back-compat).\n const rewritten = rewriteAtPathPrefix(req.url ?? '');\n if (rewritten !== null) {\n req.url = rewritten;\n }\n if (!verifyAuth(req)) {\n // Reject: complete the handshake, then close with a NAMED code so the\n // browser-side observer (in-app attach.ts) can distinguish \"stale\n // TOTP code\" (4401) from \"tunnel down\" (1006). Raw-401-destroy only\n // ever produced 1006 client-side — the env-2 silence gap (#478).\n // We do NOT log req.url or any auth param here to avoid leaking codes;\n // the close reason is a fixed enum string.\n rejectWss.handleUpgrade(req, socket, head, (ws) => {\n ws.close(RELAY_AUTH_REJECT_CLOSE_CODE, RELAY_AUTH_REJECT_REASON);\n });\n notifyAuthReject('ws-upgrade');\n // Early return — chii's captured listeners are NOT called.\n return;\n }\n // Auth passed: hand the upgrade to chii's own listeners (it sees the\n // stripped URL — same observable behaviour as the pre-#478 ordering).\n for (const listener of chiiUpgradeListeners) {\n listener(req, socket, head);\n }\n });\n }\n\n const actualPort = await new Promise<number>((resolve, reject) => {\n httpServer.once('error', reject);\n httpServer.listen(requestedPort, host, () => {\n httpServer.off('error', reject);\n // httpServer.address() is non-null immediately after the listen callback.\n const addr = httpServer.address() as AddressInfo;\n resolve(addr.port);\n });\n });\n\n // WS keepalive interval (issue #483): send a ping frame to every connected\n // socket on each tick. Both the phone-target leg and the daemon-client leg\n // terminate as WebSocket connections on this relay, so pinging chii's\n // `_wss.clients` covers both.\n //\n // Per-ping log output is intentionally absent — pings happen every 45 s and\n // logging each one would flood the MCP console without adding signal.\n //\n // `ws` clients respond to ping frames with pong automatically (RFC 6455 §5.5)\n // — no application code is needed on either end.\n let keepaliveHandle: ReturnType<typeof setInterval> | null = null;\n if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {\n const chiiWss = capturedChiiWss;\n keepaliveHandle = setInterval(() => {\n for (const client of chiiWss._wss.clients) {\n // readyState 1 = OPEN (ws library constant). Only ping live sockets.\n if (client.readyState === 1) {\n client.ping();\n }\n }\n }, keepaliveIntervalMs);\n }\n\n return {\n port: actualPort,\n baseUrl: `http://${host}:${actualPort}`,\n close: () =>\n new Promise<void>((resolve) => {\n if (keepaliveHandle !== null) {\n clearInterval(keepaliveHandle);\n keepaliveHandle = null;\n }\n httpServer.close(() => resolve());\n }),\n };\n}\n","/**\n * URL of the AITC Sandbox launcher PWA.\n *\n * Declared here (not imported from `src/unplugin/tunnel.ts`) to respect the\n * mcp → unplugin layering boundary. unplugin/tunnel.ts declares its own copy\n * for the same reason — keep the two in sync when the URL changes.\n */\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Optional metadata that enriches the launcher deep-link (#498).\n *\n * These fields are added as query params so the launcher PWA can display\n * a recognizable identity (name, icon) without the user having to configure\n * anything extra.\n */\nexport interface LauncherAttachUrlOpts {\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * Absolute `https://` icon URL for the partner nav bar icon slot (`icon=`\n * param). Non-https or falsy values are not added.\n */\n icon?: string;\n /**\n * When `true`, adds `selfdebug=1` to the launcher URL so the launcher PWA\n * registers its own document as a CDP target (issue #531/#543).\n *\n * **Single-attach model**: attaching the launcher self-target causes any\n * currently-attached mini-app target to be evicted. This is intentional —\n * `selfdebug` is a \"launcher diagnostics mode\" for inspecting the launcher's\n * own DOM/console/safe-area, not simultaneous dual-attach.\n *\n * When `false` or omitted (default), the param is not added and the output\n * is byte-identical to the previous behaviour.\n */\n selfdebug?: boolean;\n}\n\n/**\n * Builds a launcher PWA deep-link for env-2 MCP-attach (issue #378).\n *\n * The launcher at {@link LAUNCHER_URL} renders tunnelUrl in a full-viewport\n * iframe. `&debug=1&relay=<wssUrl>` is forwarded onto the iframe src so the\n * framed page's in-app debug gate (Layer C) is satisfied and a Chii target.js\n * is injected. `&at=<totpCode>` is added only when a code is provided (same\n * conditional as {@link buildDeepLinkAttachUrl}).\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the\n * launcher partner bar shows the app name instead of the generic default (#498).\n * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the\n * launcher can render an icon next to the title (#498).\n *\n * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL\n * via raw string manipulation), this function uses WHATWG `encodeURIComponent`\n * because the target is a standard `https:` URL.\n *\n * SECRET-HANDLING: `totpCode` (when provided) is placed into the `at=` param\n * only — never logged or returned separately. Callers must NOT log the result\n * of this function to stdout/stderr.\n *\n * @param tunnelUrl - The `https://*.trycloudflare.com` app tunnel URL\n * (`AIT_TUNNEL_BASE_URL`). This is the URL the launcher frames.\n * @param wssUrl - The `wss://` relay URL the framed page will attach to.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is appended as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Omit when TOTP is disabled.\n * @param opts - Optional app identity hints: `name`, `icon`, and `selfdebug`\n * (#498, #543).\n * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>\n * [&at=<code>][&name=<enc>][&icon=<enc>][&selfdebug=1]` params.\n */\nexport function buildLauncherAttachUrl(\n tunnelUrl: string,\n wssUrl: string,\n totpCode?: string,\n opts?: LauncherAttachUrlOpts,\n): string {\n let url =\n `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}` +\n `&debug=1&relay=${encodeURIComponent(wssUrl)}`;\n if (totpCode !== undefined && totpCode !== '') {\n url += `&at=${encodeURIComponent(totpCode)}`;\n }\n // App identity hints (#498): add non-blank name and valid https icon.\n if (opts?.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts?.icon !== undefined) {\n let iconParsed: URL;\n try {\n iconParsed = new URL(opts.icon);\n } catch {\n iconParsed = null as unknown as URL;\n }\n if (iconParsed?.protocol === 'https:') {\n url += `&icon=${encodeURIComponent(opts.icon)}`;\n }\n }\n // Self-debug opt-in (#543): add selfdebug=1 only when explicitly requested.\n // Without this flag the output is byte-identical to the previous behaviour.\n if (opts?.selfdebug === true) {\n url += '&selfdebug=1';\n }\n return url;\n}\n\n/**\n * Build a self-attaching dog-food deep-link.\n *\n * `ait deploy --scheme-only` prints an `intoss-private://…?_deploymentId=<uuid>`\n * URL that opens a dog-food bundle on a phone. The in-app debug gate\n * (`src/in-app/gate.ts`) auto-attaches when the entry URL also carries\n * `debug=1` and `relay=<wss-url>`. This helper splices those params (plus\n * `at=<code>` when TOTP is enabled) into the scheme URL; rendering the result\n * as a QR code and scanning it with the phone camera opens the mini-app and\n * attaches it to the live Chii relay. QR is the single entry path — it needs\n * no USB cable, platform CLI, or driver, and works the same on iOS/Android.\n *\n * The Toss app propagates extra query params from the entry deep link into the\n * mini-app WebView's `location.search` (confirmed behavior), so the gate reads\n * them at attach time.\n *\n * TOTP `at=` param:\n * When a TOTP secret is active, `buildDeepLinkAttachUrl` accepts an optional\n * `totpCode` argument and splices `at=<code>` alongside `debug` and `relay`.\n * The code must be computed by the caller at call time — do NOT pre-compute\n * and cache it, because the 30-second window expires quickly. The in-app gate\n * (`src/in-app/gate.ts` Layer C) validates this code against the baked secret.\n *\n * Why not `URL`/`URLSearchParams`: `intoss-private:` is a non-special scheme.\n * The WHATWG `URL` parser treats such schemes opaquely (no host/path/query\n * decomposition you can rely on across runtimes), so query manipulation via\n * `url.searchParams` is not portable here. We splice the query string directly\n * on the raw string instead, which keeps the scheme, authority, path, and any\n * pre-existing params (notably `_deploymentId`) byte-for-byte intact.\n */\n\n/**\n * Suspicious/generic authority values that indicate a malformed or placeholder\n * scheme URL. These are host strings that will almost certainly cause the Toss\n * app to fail with \"bundle not found\" silently.\n *\n * The expected form from `ait deploy --scheme-only` is:\n * intoss-private://<appName>?_deploymentId=<uuid>\n * where `<appName>` is a non-generic string like `aitc-sdk-example`.\n */\nconst SUSPICIOUS_AUTHORITIES = new Set<string>(['', 'web', 'localhost', '127.0.0.1', 'app']);\n\n/**\n * Validates the authority (host) portion of a scheme URL.\n *\n * Returns a warning message if the authority is missing or looks like a\n * placeholder, or `null` if the authority looks valid.\n *\n * Expected form: `intoss-private://<appName>?_deploymentId=<uuid>`\n * The authority must be a non-empty, non-generic app name (e.g. `aitc-sdk-example`).\n */\nexport function validateSchemeAuthority(schemeUrl: string): string | null {\n // Extract authority from `scheme://authority[/path][?query][#hash]`.\n // We cannot use the WHATWG URL parser for non-special schemes reliably\n // (see the deeplink.ts module comment), so we parse the raw string.\n const afterScheme = schemeUrl.replace(/^[a-zA-Z][a-zA-Z0-9+\\-.]*:\\/\\//, '');\n if (afterScheme === schemeUrl) {\n // No `://` found — not a scheme URL at all.\n return (\n 'scheme_url does not look like a scheme URL (expected `intoss-private://<appName>?_deploymentId=<uuid>`). ' +\n 'Use the URL printed by `ait deploy --scheme-only`.'\n );\n }\n\n // authority ends at the first `/`, `?`, `#`, or end of string.\n const authorityEnd = afterScheme.search(/[/?#]/);\n const authority = authorityEnd === -1 ? afterScheme : afterScheme.slice(0, authorityEnd);\n\n if (SUSPICIOUS_AUTHORITIES.has(authority.toLowerCase())) {\n const displayAuthority = authority === '' ? '(empty)' : `\"${authority}\"`;\n return (\n `scheme_url authority ${displayAuthority} looks like a placeholder. ` +\n 'Expected an app name like `intoss-private://aitc-sdk-example?_deploymentId=<uuid>`. ' +\n 'Use the URL printed by `ait deploy --scheme-only` — it includes the correct app name as the host.'\n );\n }\n\n return null;\n}\n\n/** A param the helper appends. Existing occurrences are replaced, not duplicated. */\ntype AppendParam = readonly [key: string, value: string];\n\nfunction stripExisting(query: string, key: string): string {\n if (query === '') return '';\n return query\n .split('&')\n .filter((pair) => pair !== '' && pair.split('=')[0] !== key)\n .join('&');\n}\n\n/**\n * Splices `debug=1`, `relay=<wssUrl>`, and (optionally) `at=<totpCode>` into a\n * scheme URL's query string, preserving everything else (scheme, authority,\n * path, hash, and the existing `_deploymentId` param). If any of the spliced\n * params is already present it is replaced so the helper is idempotent.\n *\n * @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL printed\n * by `ait deploy --scheme-only`. Must already carry `_deploymentId` (Layer B\n * of the gate); this helper does not invent one.\n * @param wssUrl - The live relay URL (`wss://…trycloudflare.com`) from the\n * running debug MCP server's quick tunnel.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is spliced as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Pass `undefined` or omit when TOTP is disabled.\n * @returns The same URL with `debug=1&relay=<encoded wssUrl>[&at=<totpCode>]`\n * appended.\n * @throws If `wssUrl` is not a `wss:` URL (the gate rejects anything else, so\n * producing such a link would be a silent dead end).\n */\nexport function buildDeepLinkAttachUrl(\n schemeUrl: string,\n wssUrl: string,\n totpCode?: string,\n): string {\n let relay: URL;\n try {\n relay = new URL(wssUrl);\n } catch {\n throw new Error(`relay URL is not a valid URL: ${wssUrl}`);\n }\n if (relay.protocol !== 'wss:') {\n throw new Error(`relay URL must use the wss: scheme, got ${relay.protocol} (${wssUrl})`);\n }\n\n const hashIndex = schemeUrl.indexOf('#');\n const hash = hashIndex === -1 ? '' : schemeUrl.slice(hashIndex);\n const beforeHash = hashIndex === -1 ? schemeUrl : schemeUrl.slice(0, hashIndex);\n\n const queryIndex = beforeHash.indexOf('?');\n const base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);\n let query = queryIndex === -1 ? '' : beforeHash.slice(queryIndex + 1);\n\n const appended: AppendParam[] = [\n ['debug', '1'],\n ['relay', wssUrl],\n ];\n // Only splice `at=` when a code is provided (TOTP enabled). Omitting it when\n // TOTP is disabled preserves backward compatibility with gate deployments\n // that do not yet evaluate the `at` param.\n if (totpCode !== undefined && totpCode !== '') {\n appended.push(['at', totpCode]);\n }\n\n // Always strip the `at` key from the existing query so a stale code from a\n // previous run is removed even when the caller does not provide a fresh code.\n query = stripExisting(query, 'at');\n\n for (const [key] of appended) {\n query = stripExisting(query, key);\n }\n for (const [key, value] of appended) {\n const pair = `${key}=${encodeURIComponent(value)}`;\n query = query === '' ? pair : `${query}&${pair}`;\n }\n\n return `${base}?${query}${hash}`;\n}\n","/**\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","/**\n * Unified response envelope for all MCP debug tools.\n *\n * Every tool result is wrapped in a `ToolEnvelope<T>` so agents can use a\n * single parser regardless of which tool they called. Before this, tool shapes\n * diverged: raw array returns, `{exceptions}`, `{value,type}`, `{ok,value|error}` …\n *\n * ## Schema\n *\n * ```ts\n * {\n * ok: boolean,\n * data?: T, // tool payload (absent when ok:false)\n * error?: { code, message, nextRecommendedAction? },\n * meta: {\n * tool: string,\n * env: 'mock' | 'relay-dev' | 'relay-live' | 'relay-mobile',\n * attached: boolean,\n * contentType: 'json' | 'image',\n * }\n * }\n * ```\n *\n * ## Compat mode\n *\n * Set `AIT_MCP_COMPAT=chrome-devtools` to bypass envelope wrapping and return\n * the raw payload. This restores 0.1.x behaviour for consumers that already\n * parse the old shapes (e.g. chrome-devtools-mcp integrations).\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n/** Allowed values for `meta.env`. */\nexport type EnvelopeEnv = 'mock' | 'relay-dev' | 'relay-live' | 'relay-mobile';\n\n/** The unified envelope returned by every debug MCP tool (when compat mode is off). */\nexport interface ToolEnvelope<T = unknown> {\n ok: boolean;\n data?: T;\n error?: {\n code: string;\n message: string;\n nextRecommendedAction?: {\n tool: string;\n reason: string;\n };\n };\n meta: {\n tool: string;\n env: EnvelopeEnv;\n attached: boolean;\n contentType: 'json' | 'image';\n };\n}\n\n/**\n * Returns `true` when `AIT_MCP_COMPAT=chrome-devtools` is set, which bypasses\n * envelope wrapping and returns raw payloads (0.1.x back-compat).\n */\nexport function isCompatMode(): boolean {\n return process.env.AIT_MCP_COMPAT === 'chrome-devtools';\n}\n\n/**\n * Maps `McpEnvironment` to `EnvelopeEnv`. After #307 these are the same\n * union (`mock | relay-dev | relay-live`), so this is identity — kept as a\n * named export for surface stability if envelope env diverges in the future.\n */\nexport function toEnvelopeEnv(env: McpEnvironment): EnvelopeEnv {\n return env;\n}\n\n/**\n * Context passed to `wrapEnvelope` that carries the per-request metadata.\n */\nexport interface EnvelopeContext {\n tool: string;\n env: McpEnvironment;\n attached: boolean;\n contentType?: 'json' | 'image';\n}\n\n/**\n * Wraps `data` in a `ToolEnvelope<T>` **unless** compat mode is active, in\n * which case `data` is returned as-is.\n *\n * Use this at every tool call-site in `debug-server.ts` and `server.ts`.\n *\n * @example\n * ```ts\n * return jsonResult(wrapEnvelope(listPages(connection, tunnel), {\n * tool: 'list_pages',\n * env: resolveEnvironment(),\n * attached: connection.listTargets().length > 0,\n * }));\n * ```\n */\nexport function wrapEnvelope<T>(data: T, ctx: EnvelopeContext): ToolEnvelope<T> | T {\n if (isCompatMode()) return data;\n return {\n ok: true,\n data,\n meta: {\n tool: ctx.tool,\n env: toEnvelopeEnv(ctx.env),\n attached: ctx.attached,\n contentType: ctx.contentType ?? 'json',\n },\n };\n}\n","/**\n * MCP environment — derived from two orthogonal axes (issue #348).\n *\n * Before #348 the environment was a single sticky decision made once per\n * process by `getEnvironment()` via a 5-step precedence chain (env var → URL\n * pattern sniffing → caller-stated default → baked-in default). That model\n * could not express a daemon that holds two live connections at once and swaps\n * the active one without a restart — the dual-connection design (#348).\n *\n * The 4-value `McpEnvironment` is now *derived* from cheap signals rather\n * than detected:\n *\n * 1. `mock` vs `relay-*` — free from `connection.kind` (`'local'` | `'relay'`,\n * see `cdp-connection.ts`). Authoritative, known before any target\n * attaches, and swappable at runtime by pointing at a different connection.\n *\n * 2. `relay-dev` vs `relay-live` — physically underivable (dog-food and\n * production relays are byte-identical on the wire), so it is a single\n * operator-supplied bit, `liveIntent`. It is armed only by\n * `start_debug({ mode: 'relay-live' })` and is inert whenever the active\n * connection is local.\n *\n * 3. `relay-dev` vs `relay-mobile` — both are `kind: 'relay'`, !liveIntent\n * relays, so they are distinguished by the booted family's `relayOrigin`\n * discriminator (`'intoss-webview'` → relay-dev, `'external-pwa'` →\n * relay-mobile, issue #378). NOT sniffed from the relay URL.\n *\n * `McpEnvironment` survives as an OUTPUT-BOUNDARY type — `get_debug_status` and\n * the envelope `meta.env` field still surface the precise three-value string —\n * but it is reconstructed from `(connection.kind, liveIntent)` via\n * {@link deriveEnvironment}, never sniffed.\n *\n * LIVE side-effect guard (relay-live, env 4): the `call_sdk` / `evaluate` tools\n * require an explicit `confirm: true`. The guard now reads to a single line in\n * `debug-server.ts`: `connection.kind === 'relay' && liveIntent && !confirm`.\n * `relay && liveIntent` together means a stale `liveIntent` bit is inert\n * against a local target (it only fires when the active connection is relay).\n *\n * Backward compatibility:\n * - `MCP_ENV=relay-live` is a deprecated alias that seeds `liveIntent=true`\n * at boot (see `cli.ts`). `MCP_ENV=mock|relay|relay-dev` are accepted and\n * ignored for env derivation (kind is authoritative) — they only matter for\n * `relay-live`'s liveIntent seed.\n * - `isRelayEnv()` / `isLiveRelayEnv()` / `toLegacyEnv()` are unchanged.\n *\n * SECRET-HANDLING: this module never reads the TOTP secret, deploy key, or any\n * URL. It deals only in the connection kind and a single boolean.\n */\n\n/**\n * The four environments the MCP server can surface in its output (issues #307,\n * #378).\n *\n * - `mock` — local Chromium + mock SDK (env 1) — active connection is local.\n * - `relay-dev` — real-device dog-food relay (env 3) — relay connection, liveIntent off,\n * intoss-private WebView (the relay devtools started).\n * - `relay-live` — real-device live/production relay (env 4) — relay connection,\n * liveIntent on, read-only LIVE guard active.\n * - `relay-mobile` — real-device PWA over an EXTERNAL relay (env 2, issue #378) —\n * relay connection, liveIntent off, an external-PWA relay\n * (the unplugin started it; the MCP only attaches a CDP client).\n *\n * This is a derived OUTPUT string (see module docstring) — not a detected,\n * sticky decision.\n */\nexport type McpEnvironment = 'mock' | 'relay-dev' | 'relay-live' | 'relay-mobile';\n\n/** Connection kind — the authoritative `mock` vs `relay` signal (issue #348). */\nexport type ConnectionKind = 'relay' | 'local';\n\n/**\n * Origin of a relay connection — the discriminator that distinguishes two relay\n * families that are otherwise both `kind: 'relay'` (issue #378):\n *\n * - `'intoss-webview'` — the intoss-private dog-food / live relay (env 3/4),\n * booted BY the MCP server (`bootRelayFamily`). Maps to `relay-dev` /\n * `relay-live` depending on `liveIntent`.\n * - `'external-pwa'` — an external CDP relay the unplugin already brought up\n * for the env-2 PWA (`bootExternalRelayFamily`). Maps to `relay-mobile`.\n *\n * Carried on the booted family (NOT sniffed from the relay URL), so the output\n * layer can tell `relay-mobile` apart from `relay-dev`.\n */\nexport type RelayOrigin = 'intoss-webview' | 'external-pwa';\n\n/**\n * Returns `true` when the environment is any relay variant (`relay-dev`,\n * `relay-live`, or `relay-mobile`). Use this instead of `env === 'relay'` for\n * tier checks — every relay env surfaces the Tier B / relay-only tool set.\n *\n * Written as an exhaustive switch so a future `McpEnvironment` member that is\n * missing an arm is a TS compile error rather than a silent `false`.\n */\nexport function isRelayEnv(env: McpEnvironment): boolean {\n switch (env) {\n case 'relay-dev':\n case 'relay-live':\n case 'relay-mobile':\n return true;\n case 'mock':\n return false;\n }\n}\n\n/**\n * Returns `true` when the environment is the LIVE relay (`relay-live`).\n * This is the guard condition for side-effect tool protection. `relay-mobile`\n * is a dev-intent env (env 2 PWA) and is NOT live.\n */\nexport function isLiveRelayEnv(env: McpEnvironment): boolean {\n return env === 'relay-live';\n}\n\n/**\n * Maps the `McpEnvironment` union to the legacy two-value union\n * (`'mock' | 'relay'`) for backward-compatible fields in diagnostics output.\n * Every relay variant (incl. `relay-mobile`) collapses to `'relay'`.\n */\nexport function toLegacyEnv(env: McpEnvironment): 'mock' | 'relay' {\n if (env === 'mock') return 'mock';\n return 'relay';\n}\n\n/**\n * Reconstructs the four-value `McpEnvironment` output string from the\n * orthogonal signals (issues #348, #378):\n *\n * - `kind === 'local'` → `'mock'`\n * - `kind === 'relay'` && liveIntent → `'relay-live'`\n * - `kind === 'relay'` && !liveIntent && origin 'external-pwa' → `'relay-mobile'`\n * - `kind === 'relay'` && !liveIntent && origin intoss/undefined → `'relay-dev'`\n *\n * `relayOrigin` is the booted-family discriminator (NOT sniffed from the URL)\n * that distinguishes the env-2 external-PWA relay (`relay-mobile`) from the\n * intoss-private dog-food relay (`relay-dev`); both are `kind: 'relay'`.\n *\n * Pure — used at every output boundary (envelope `meta.env`, `get_debug_status`,\n * `measure_safe_area` provenance) so the surface never sniffs a URL again.\n *\n * Written switch-style so a missing arm is a TS compile error (never falls\n * through to a default).\n */\nexport function deriveEnvironment(\n kind: ConnectionKind,\n liveIntent: boolean,\n relayOrigin?: RelayOrigin,\n): McpEnvironment {\n switch (kind) {\n case 'local':\n return 'mock';\n case 'relay':\n if (liveIntent) return 'relay-live';\n return relayOrigin === 'external-pwa' ? 'relay-mobile' : 'relay-dev';\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* liveIntent — the single operator-supplied bit (relay-dev vs relay-live) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Module-level `relay-dev` vs `relay-live` intent bit (issue #348).\n *\n * Armed by `start_debug({ mode: 'relay-live' })` (and seeded at boot by the\n * deprecated `MCP_ENV=relay-live` alias). Disarming is implicit: when the\n * active connection becomes local, the LIVE guard reads\n * `connection.kind === 'relay' && liveIntent`, so a stale `true` bit is inert.\n *\n * SECRET-HANDLING: this is a boolean — never a secret. Safe to read in logs.\n */\nlet liveIntent = false;\n\n/** Returns the current `liveIntent` bit. */\nexport function getLiveIntent(): boolean {\n return liveIntent;\n}\n\n/**\n * Sets the `liveIntent` bit. Called by `start_debug` (true for `relay-live`,\n * false for every other mode) and once at boot by the `MCP_ENV=relay-live`\n * deprecated alias.\n */\nexport function setLiveIntent(value: boolean): void {\n liveIntent = value;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Test override hook (narrow) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Test/override hook — when non-null, callers that consult\n * {@link getEnvironmentOverride} return this value regardless of the live\n * connection kind. Production code never sets it; it exists so a unit test can\n * pin a precise `McpEnvironment` without constructing a real connection.\n *\n * This is intentionally NARROW: it no longer drives a precedence chain. The\n * authoritative production signal is `connection.kind` + `liveIntent`; this\n * override is a pure test affordance.\n */\nlet envOverride: McpEnvironment | null = null;\n\n/** Sets a sticky environment override. Intended for tests only. */\nexport function setEnvironmentOverride(env: McpEnvironment | null): void {\n envOverride = env;\n}\n\n/** Reads the current override (test inspection). */\nexport function getEnvironmentOverride(): McpEnvironment | null {\n return envOverride;\n}\n","/**\n * MCP tool 거부/에러 응답 메시지 헬퍼 — 4상태 차별화 + Tier 거부 통일.\n *\n * 모든 tool 거부/에러 응답을 \"원인 + 다음 행동\" 한국어 한 줄 포맷으로 일원화한다.\n * debug-server.ts · tools.ts의 거부 응답 호출부가 이 헬퍼를 통해 생성된다.\n *\n * 4가지 상태 (진단 메시지 차별화):\n * - tunnel-down : cloudflared 터널 미가동 — 서버 재시작 필요\n * - page-missing : 페이지가 attach 안 됨 — build_attach_url → QR 스캔\n * - page-crash : 페이지 crash 감지 — 앱 재실행 후 재attach\n * - sdk-absent : window.__sdkCall 미주입 — dog-food 채널로 재배포\n */\n\n/** MCP tool-result 에러 응답 형식. */\nexport interface McpErrorResult {\n content: Array<{ type: 'text'; text: string }>;\n isError: true;\n}\n\n/**\n * 한국어 한 줄 \"원인 + 다음 행동\" 포맷으로 에러 결과를 빌드한다.\n *\n * @param message - 사용자에게 보여줄 에러 본문 (원인 + 다음 행동 포함).\n */\nexport function mcpError(message: string): McpErrorResult {\n return {\n content: [{ type: 'text', text: message }],\n isError: true,\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Tier 거부 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Tier A/B 환경 불일치 거부 메시지.\n *\n * @param toolName - 거부된 tool 이름.\n * @param requiredEnv - 해당 tool이 요구하는 환경 ('mock' | 'relay').\n * @param currentEnv - 현재 세션 환경.\n * @param reason - 환경이 결정된 근거를 나타내는 파생 문자열\n * (예: `derived:kind=relay,liveIntent=true`).\n */\nexport function tierRejectionError(\n toolName: string,\n requiredEnv: string,\n currentEnv: string,\n reason: string,\n): McpErrorResult {\n const envLabel = requiredEnv === 'relay' ? 'relay (실기기 연결)' : 'mock (로컬 브라우저)';\n const currentLabel = currentEnv === 'relay' ? 'relay' : 'mock';\n const hint =\n requiredEnv === 'relay'\n ? 'relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고 build_attach_url → QR 스캔으로 실기기를 attach하세요.'\n : 'mock으로 전환하려면 MCP_ENV=mock 설정 후 서버를 재시작하세요.';\n const text =\n `${toolName}은 ${envLabel} 환경에서만 사용할 수 있습니다. ` +\n `현재 환경: ${currentLabel} (${reason}). ${hint}`;\n // 하위 호환 — 기존 테스트가 기대하는 영문 패턴도 유지\n const compat = `tool ${toolName} is available only in ${requiredEnv}. Current environment is ${currentEnv} (${reason}).`;\n return mcpError(`${text}\\n\\n${compat}`);\n}\n\n/* -------------------------------------------------------------------------- */\n/* 4상태 차별화 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * 상태 1: tunnel 미가동 — cloudflared 터널이 아직 뜨지 않았다.\n *\n * `build_attach_url` 호출 시 tunnel.up === false 인 경우.\n */\nexport function tunnelDownError(): McpErrorResult {\n return mcpError(\n 'cloudflared 터널이 안 떠 있습니다. ' +\n 'MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.',\n );\n}\n\n/**\n * 상태 2: page 미attach — 터널은 살아 있으나 아직 페이지가 연결되지 않았다.\n *\n * enableDomains()가 \"No mini-app page attached\" 에러를 던질 때.\n */\nexport function pageMissingError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}페이지가 attach 안 됨. ` +\n 'dog-food 번들 배포 후 build_attach_url을 호출해 QR을 생성하세요: ' +\n '`ait deploy --scheme-only` → `build_attach_url(scheme_url)` → QR 스캔.',\n );\n}\n\n/**\n * 상태 3: page crash — 연결됐던 페이지가 crash/destroy됐다.\n *\n * chii-connection 이 'replaced-by-new-attach' / 'targetCrashed' / 'targetDestroyed' 를\n * 던질 때 이 메시지를 사용한다.\n */\nexport function pageCrashError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}페이지가 crash됐습니다. ` +\n '토스 앱을 재실행한 뒤 build_attach_url → QR 스캔으로 재attach하세요.',\n );\n}\n\n/**\n * 상태 4: SDK 부재 — window.__sdkCall이 주입되지 않았다.\n *\n * call_sdk 호출 시 브리지가 없을 때. 같은 \"브리지 부재\"라도 다음 행동은\n * connection 종류에 따라 정반대다 (issue #360):\n * - relay(`--target` 없는 intoss / env-2): dog-food 빌드가 아니다 → dog-food\n * 채널로 재배포 후 QR 재스캔.\n * - local(`--target=local`, env 1 로컬 브라우저): 재배포가 아니라 dev 서버를\n * `pnpm dev`로 띄웠는지 + unplugin alias가 `@apps-in-toss/web-framework`를\n * devtools mock으로 resolve하는지 확인. dev 빌드면 `import.meta.env.DEV`\n * 경로로 `window.__sdkCall`이 자동 설치된다.\n *\n * `isLocal`이 생략되면 relay 안내(이전 동작)를 유지한다.\n */\nexport function sdkAbsentError(toolName?: string, isLocal = false): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n if (isLocal) {\n return mcpError(\n `${prefix}window.__sdkCall이 주입되지 않았습니다 (로컬 dev 브리지 부재). ` +\n 'sdk-example을 `pnpm dev`로 띄웠는지, 그리고 unplugin alias가 ' +\n '`@apps-in-toss/web-framework`를 devtools mock으로 resolve하는지 확인하세요. ' +\n 'dev 빌드(`import.meta.env.DEV`)면 `window.__sdkCall`이 자동 설치됩니다.',\n );\n }\n return mcpError(\n `${prefix}window.__sdkCall이 주입되지 않았습니다 (dog-food 빌드가 아닙니다). ` +\n 'dog-food 채널(intoss-private)로 재배포 후 QR을 다시 스캔하세요: ' +\n '`ait build && aitcc app deploy`.',\n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* LIVE side-effect guard 메시지 (relay-live env) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * relay-live 환경에서 side-effect 도구(`call_sdk`, `evaluate`)를 `confirm: true`\n * 없이 호출했을 때 반환하는 거부 메시지.\n *\n * 다음 행동을 두 가지로 제시한다:\n * 1. 같은 호출에 `confirm: true` 인자를 추가해 재시도.\n * 2. 읽기 전용 환경(relay-dev, mock)으로 전환.\n */\nexport function liveGuardError(toolName: string): McpErrorResult {\n const text =\n `[LIVE relay guard] ${toolName}은 현재 relay-live(실 출시 런타임) 세션에서 ` +\n 'side-effect 호출입니다. 실유저에게 영향을 줄 수 있어 명시적 동의가 필요합니다.\\n\\n' +\n '다음 중 하나를 선택하세요:\\n' +\n ` 1. \\`confirm: true\\` 인자를 추가해 재호출: ${toolName}(…, confirm: true)\\n` +\n ' 2. 읽기 전용 도구(list_pages, list_console_messages, take_screenshot 등)를 사용하세요.\\n' +\n ' 3. dog-food 빌드(relay-dev 환경)에서 먼저 검증 후 live에 적용하세요.\\n\\n' +\n 'live-guard: MCP_ENV=relay-live + confirm: true missing';\n return mcpError(text);\n}\n\n/* -------------------------------------------------------------------------- */\n/* relay 연결 끊김 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * relay WebSocket 연결이 끊겼을 때 — 크래시가 아닌 네트워크/프로세스 종료.\n */\nexport function relayDisconnectError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}relay 연결이 끊겼습니다. ` +\n 'list_pages로 상태를 확인하고, 필요하면 앱을 재실행 후 재attach하세요.',\n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* 일반 tool 에러 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * CDP/AIT 명령 중 발생한 예외를 4상태로 분류해 적절한 에러 결과를 반환한다.\n *\n * - SDK 부재 패턴 (`window.__sdkCall is not available`) → sdkAbsentError\n * - crash 패턴 (`replaced-by-new-attach`, `targetCrashed`, `targetDestroyed`) → pageCrashError\n * - 연결 끊김 패턴 (`relay에 연결되어 있지 않습니다`, `relay WebSocket`) → relayDisconnectError\n * - 그 외 (일반 에러) → 원본 메시지를 포함한 mcpError\n */\nexport function classifyToolError(err: unknown, toolName: string, isLocal = false): McpErrorResult {\n const message = err instanceof Error ? err.message : String(err);\n\n // 상태 1: tunnel 미가동 (buildAttachUrl이 던지는 패턴)\n if (message.startsWith('tunnel-down:') || message.includes('터널이 안 떠 있습니다')) {\n return tunnelDownError();\n }\n\n // 상태 4: SDK 부재. page-side probe가 던지는 메시지는 relay 가정으로 쓰여\n // 있으나, 안내는 connection 종류로 재구성한다 (issue #360) — local 세션이면\n // dog-food 재배포가 아니라 dev 서버/unplugin alias 확인이 맞다.\n if (\n message.startsWith('sdk-absent:') ||\n message.includes('__sdkCall이 주입되지 않았습니다') ||\n message.includes('window.__sdkCall is not available') ||\n (message.includes('__sdkCall') && message.includes('not available'))\n ) {\n return sdkAbsentError(toolName, isLocal);\n }\n\n // 상태 3: page crash / target destroyed / replaced-by-new-attach\n if (\n message.includes('replaced-by-new-attach') ||\n message.includes('targetCrashed') ||\n message.includes('targetDestroyed') ||\n message.includes('detachedFromTarget')\n ) {\n return pageCrashError(toolName);\n }\n\n // relay 연결 끊김 (단순 disconnect — crash 아님)\n if (message.includes('relay에 연결되어 있지 않습니다') || message.includes('relay WebSocket')) {\n return relayDisconnectError(toolName);\n }\n\n // 그 외: 원본 메시지를 포함하되 list_pages 다음 행동 안내 추가\n return mcpError(\n `${toolName} 실패: ${message}\\nlist_pages로 미니앱이 relay에 attach됐는지 확인하세요.`,\n );\n}\n","/**\n * Local-browser `CdpConnection` — attaches directly to a Chromium instance\n * started with `--remote-debugging-port=<port>`.\n *\n * Topology (local debug mode, env 1):\n * Chromium --CDP WS--> this connection <--stdio--> MCP host\n *\n * The core insight: local Chromium and the phone's Toss WebView both speak\n * Chrome DevTools Protocol. The only difference is the attach strategy — how\n * you reach the CDP endpoint. Here we hit the Chromium DevTools HTTP endpoint\n * (`GET /json`) to discover per-target websocket URLs, then connect directly.\n * The Chii relay (env 2/3) uses `GET /targets` + `/client/<id>?target=<id>`.\n * Every tool (list_console_messages, get_dom_document, take_screenshot, …)\n * reads only the `CdpConnection` interface and works unchanged on both.\n *\n * Node-only: imports `ws`. Never bundled into the browser/in-app entries.\n */\n\nimport { EventEmitter } from 'node:events';\nimport { WebSocket } from 'ws';\nimport type {\n CdpCommandMap,\n CdpCommandName,\n CdpConnection,\n CdpEventMap,\n CdpEventName,\n CdpTarget,\n} from './cdp-connection.js';\n\n/** Max events retained per domain ring buffer. */\nconst DEFAULT_BUFFER_SIZE = 500;\n\n/** A CDP message arriving over the local Chromium websocket. */\ninterface CdpInboundMessage {\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { message: string };\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction parseInbound(raw: string): CdpInboundMessage | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isObject(parsed)) return null;\n const message: CdpInboundMessage = {};\n if (typeof parsed.id === 'number') message.id = parsed.id;\n if (typeof parsed.method === 'string') message.method = parsed.method;\n if ('params' in parsed) message.params = parsed.params;\n if ('result' in parsed) message.result = parsed.result;\n if (isObject(parsed.error) && typeof parsed.error.message === 'string') {\n message.error = { message: parsed.error.message };\n }\n return message;\n}\n\nconst PHASE_1_EVENTS: readonly CdpEventName[] = [\n 'Runtime.consoleAPICalled',\n 'Network.requestWillBeSent',\n 'Network.responseReceived',\n];\n\n/**\n * A target entry from the Chromium DevTools HTTP `/json` endpoint.\n * Each page target includes a `webSocketDebuggerUrl` pointing directly at the\n * target's CDP websocket — no relay URL indirection.\n */\ninterface ChromiumJsonTarget {\n id: string;\n title: string;\n url: string;\n type: string;\n webSocketDebuggerUrl?: string;\n}\n\nexport interface LocalCdpConnectionOptions {\n /**\n * Base URL of the Chromium DevTools HTTP server, e.g. `http://127.0.0.1:9222`.\n * The connection hits `<devtoolsHttpUrl>/json` to discover targets.\n */\n devtoolsHttpUrl: string;\n /** Per-domain ring buffer size. Default 500. */\n bufferSize?: number;\n}\n\n/**\n * `CdpConnection` that attaches directly to a local Chromium over its built-in\n * CDP websocket. Mirrors `ChiiCdpConnection`'s buffering/command-routing/event\n * logic — same `parseInbound`, ring-buffer, `pending` map patterns — but the\n * attach strategy differs:\n *\n * Chii relay: `GET /targets` → open `/client/<id>?target=<id>` WS\n * Local CDP: `GET /json` → open `webSocketDebuggerUrl` per target directly\n *\n * Target selection: first `type === 'page'` target whose URL is not\n * `about:blank`, `about:newtab`, or a devtools:// URL.\n */\nexport class LocalCdpConnection implements CdpConnection {\n /** Authoritative connection kind (issue #348) — local Chromium CDP. */\n readonly kind = 'local' as const;\n\n private readonly devtoolsHttpUrl: string;\n private readonly bufferSize: number;\n private readonly emitter = new EventEmitter();\n private readonly buffers = new Map<CdpEventName, unknown[]>();\n private readonly targets = new Map<string, CdpTarget>();\n\n private ws: WebSocket | null = null;\n private nextCommandId = 1;\n /** In-flight enableDomains() promise — concurrent callers share it. */\n private enablingPromise: Promise<void> | null = null;\n /** Pending request→response commands keyed by CDP message id. */\n private readonly pending = new Map<\n number,\n { resolve: (result: unknown) => void; reject: (err: Error) => void }\n >();\n\n constructor(options: LocalCdpConnectionOptions) {\n this.devtoolsHttpUrl = options.devtoolsHttpUrl.replace(/\\/$/, '');\n this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;\n for (const event of PHASE_1_EVENTS) this.buffers.set(event, []);\n // EventEmitter caps listeners at 10 by default; the tool layer may add\n // several short-lived subscriptions, so lift the cap.\n this.emitter.setMaxListeners(0);\n }\n\n /**\n * Fetch the target list from the Chromium DevTools `/json` (or `/json/list`)\n * endpoint and pick the first non-blank page target.\n *\n * Returns the selected target's `webSocketDebuggerUrl` alongside the\n * normalized `CdpTarget` list (all page targets visible to the server).\n */\n private async fetchTargets(): Promise<{\n selected: ChromiumJsonTarget | null;\n all: CdpTarget[];\n }> {\n // Chromium exposes both /json and /json/list; /json is the canonical form.\n const res = await fetch(`${this.devtoolsHttpUrl}/json`);\n if (!res.ok) {\n throw new Error(\n `Chromium DevTools /json returned HTTP ${res.status} ${res.statusText}. ` +\n 'Is the browser running with --remote-debugging-port?',\n );\n }\n const body: unknown = await res.json();\n const list: ChromiumJsonTarget[] = Array.isArray(body) ? (body as ChromiumJsonTarget[]) : [];\n\n this.targets.clear();\n let selected: ChromiumJsonTarget | null = null;\n\n for (const item of list) {\n if (!isObject(item) || typeof item.id !== 'string') continue;\n const cdpTarget: CdpTarget = {\n id: item.id,\n title: typeof item.title === 'string' ? item.title : '',\n url: typeof item.url === 'string' ? item.url : '',\n };\n this.targets.set(item.id, cdpTarget);\n\n // Pick the first `page` target that is not a blank/devtools page.\n if (\n selected === null &&\n item.type === 'page' &&\n typeof item.webSocketDebuggerUrl === 'string' &&\n !isBlankOrDevtoolsUrl(item.url)\n ) {\n selected = item;\n }\n }\n\n return { selected, all: [...this.targets.values()] };\n }\n\n listTargets(): CdpTarget[] {\n return [...this.targets.values()];\n }\n\n /**\n * Discover the target, open a direct CDP websocket to its\n * `webSocketDebuggerUrl`, and enable Phase 1+2 domains. Resolves once the\n * socket is open and domain-enable commands are sent. Idempotent — concurrent\n * callers share the in-flight promise.\n */\n async enableDomains(): Promise<void> {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) return;\n if (this.enablingPromise) return this.enablingPromise;\n this.enablingPromise = this._doEnableDomains().finally(() => {\n this.enablingPromise = null;\n });\n return this.enablingPromise;\n }\n\n private async _doEnableDomains(): Promise<void> {\n const { selected } = await this.fetchTargets();\n if (!selected) {\n throw new Error(\n 'No suitable page target found in the local Chromium instance. ' +\n 'Ensure the browser has a non-blank page open and was started with ' +\n '--remote-debugging-port matching devtoolsHttpUrl.',\n );\n }\n\n // Local CDP gives us the per-target WS URL directly — no relay path needed.\n const wsUrl = selected.webSocketDebuggerUrl as string;\n const ws = new WebSocket(wsUrl);\n this.ws = ws;\n\n await new Promise<void>((resolve, reject) => {\n ws.once('open', () => resolve());\n ws.once('error', (err: Error) => reject(err));\n });\n\n ws.on('message', (data: WebSocket.RawData) => this.handleMessage(data.toString()));\n\n // Enable the same domain set as ChiiCdpConnection so all tools work identically.\n this.sendFireAndForget('Runtime.enable');\n this.sendFireAndForget('Network.enable');\n this.sendFireAndForget('DOM.enable');\n this.sendFireAndForget('Page.enable');\n }\n\n /** Fire-and-forget CDP message (used for `*.enable`, no result awaited). */\n private sendFireAndForget(method: string, params: Record<string, unknown> = {}): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n const id = this.nextCommandId++;\n this.ws.send(JSON.stringify({ id, method, params }));\n }\n\n /**\n * Issue a CDP command and resolve with its typed result. Rejects on a CDP\n * error frame or when no websocket is open.\n */\n send<M extends CdpCommandName>(\n method: M,\n params?: CdpCommandMap[M]['params'],\n ): Promise<CdpCommandMap[M]['result']> {\n return this.sendCommand(method, (params ?? {}) as Record<string, unknown>) as Promise<\n CdpCommandMap[M]['result']\n >;\n }\n\n /**\n * Issue an arbitrary request→response command and resolve with its raw\n * result. Both the typed CDP `send` and any AIT domain commands build on this.\n */\n sendCommand(method: string, params: Record<string, unknown> = {}): Promise<unknown> {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n return Promise.reject(\n new Error(\n 'No local Chromium page attached yet. Call enableDomains() first and ensure ' +\n 'the browser is running with --remote-debugging-port.',\n ),\n );\n }\n const id = this.nextCommandId++;\n const ws = this.ws;\n return new Promise<unknown>((resolve, reject) => {\n this.pending.set(id, { resolve, reject });\n ws.send(JSON.stringify({ id, method, params }));\n });\n }\n\n private handleMessage(raw: string): void {\n const message = parseInbound(raw);\n if (!message) return;\n\n // Command response (has an id matching a pending request).\n if (typeof message.id === 'number' && this.pending.has(message.id)) {\n const waiter = this.pending.get(message.id);\n this.pending.delete(message.id);\n if (waiter) {\n if (message.error) waiter.reject(new Error(message.error.message));\n else waiter.resolve(message.result);\n }\n return;\n }\n\n // Event (buffered for the Phase 1 stream tools).\n if (typeof message.method !== 'string') return;\n if (!this.buffers.has(message.method as CdpEventName)) return;\n const event = message.method as CdpEventName;\n const buffer = this.buffers.get(event);\n if (!buffer) return;\n buffer.push(message.params);\n if (buffer.length > this.bufferSize) buffer.shift();\n this.emitter.emit(event, message.params);\n }\n\n getBufferedEvents<E extends CdpEventName>(event: E): ReadonlyArray<CdpEventMap[E]> {\n const buffer = this.buffers.get(event);\n return (buffer ?? []) as ReadonlyArray<CdpEventMap[E]>;\n }\n\n on<E extends CdpEventName>(event: E, listener: (payload: CdpEventMap[E]) => void): () => void {\n this.emitter.on(event, listener as (payload: unknown) => void);\n return () => this.emitter.off(event, listener as (payload: unknown) => void);\n }\n\n /** Close the local CDP websocket and reject any in-flight commands. */\n close(): void {\n this.ws?.close();\n this.ws = null;\n for (const waiter of this.pending.values()) {\n waiter.reject(new Error('Local Chromium CDP connection closed.'));\n }\n this.pending.clear();\n }\n}\n\n/** True for URLs that should be skipped when selecting a page target. */\nfunction isBlankOrDevtoolsUrl(url: string): boolean {\n return (\n url === '' ||\n url === 'about:blank' ||\n url === 'about:newtab' ||\n url.startsWith('devtools://') ||\n url.startsWith('chrome://') ||\n url.startsWith('chrome-extension://')\n );\n}\n","/**\n * Chromium launcher for the local debug mode (env 1).\n *\n * Launch decision rationale:\n * - `chrome-launcher` (npm) is purpose-built and finds installed Chrome, but\n * adds a runtime dependency to the MCP bundle. The repo already has a clear\n * \"external dependency minimization\" policy; `chrome-launcher` is not worth\n * pulling in for what is essentially `spawn(chromeBin, [...flags])`.\n * - Playwright is a devDependency used for E2E only — pulling `chromium.launch`\n * into the runtime MCP path would add ~100 MB of bundled Chromium to the\n * production install and break the \"devDep = e2e only\" boundary.\n * - `child_process.spawn` with a platform-aware binary search is the lightest\n * option: zero new dependencies, portable across macOS/Linux/Windows, and\n * trivially testable by injecting a `spawnFn`.\n *\n * The launcher finds an installed Chrome/Chromium using a prioritized list of\n * well-known binary paths per platform, then spawns it with:\n * --remote-debugging-port=<port>\n * --no-first-run\n * --no-default-browser-check\n * <devUrl>\n *\n * `pnpm dev` is started by the user; the MCP only launches the browser pointing\n * at it.\n *\n * Node-only.\n */\n\nimport { type ChildProcess, spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport net from 'node:net';\nimport { platform } from 'node:os';\n\n/** A handle returned by `launchChromium`. */\nexport interface ChromiumHandle {\n /** The port Chromium is listening on for CDP (`--remote-debugging-port`). */\n port: number;\n /** Devtools HTTP base URL, e.g. `http://127.0.0.1:9222`. */\n devtoolsUrl: string;\n /** Stop the Chromium child process. */\n stop(): void;\n}\n\nexport interface LaunchChromiumOptions {\n /**\n * CDP remote debugging port. If 0 or omitted, an ephemeral free port is\n * chosen automatically.\n */\n port?: number;\n /**\n * URL to open in the browser. Defaults to `AIT_DEVTOOLS_URL` env var or\n * `http://localhost:5173`.\n */\n devUrl?: string;\n /**\n * Extra Chromium flags appended to the spawn command. Use with caution.\n */\n extraArgs?: string[];\n /**\n * Injectable `spawn` function for unit testing — defaults to Node's\n * `child_process.spawn`. Tests inject a fake to avoid launching a real browser.\n */\n spawnFn?: typeof spawn;\n}\n\n/**\n * Find an ephemeral free TCP port by briefly binding a server on port 0.\n * Resolves with the OS-assigned port number.\n */\nexport function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address();\n const port = typeof addr === 'object' && addr !== null ? addr.port : null;\n server.close(() => {\n if (port === null) {\n reject(new Error('Failed to determine free port from net.Server.'));\n } else {\n resolve(port);\n }\n });\n });\n server.once('error', reject);\n });\n}\n\n/**\n * Returns an ordered list of Chromium/Chrome binary paths to try for the\n * current platform.\n */\nexport function candidateChromePaths(): string[] {\n const os = platform();\n if (os === 'darwin') {\n return [\n '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',\n '/Applications/Chromium.app/Contents/MacOS/Chromium',\n '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',\n ];\n }\n if (os === 'linux') {\n return [\n '/usr/bin/google-chrome',\n '/usr/bin/google-chrome-stable',\n '/usr/bin/chromium',\n '/usr/bin/chromium-browser',\n '/usr/local/bin/google-chrome',\n '/usr/local/bin/chromium',\n '/snap/bin/chromium',\n ];\n }\n if (os === 'win32') {\n const programFiles = process.env.PROGRAMFILES ?? 'C:\\\\Program Files';\n const programFilesX86 = process.env['PROGRAMFILES(X86)'] ?? 'C:\\\\Program Files (x86)';\n return [\n `${programFiles}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n `${programFilesX86}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n `${programFiles}\\\\Chromium\\\\Application\\\\chrome.exe`,\n ];\n }\n return [];\n}\n\n/** Find the first Chrome/Chromium binary that exists on this machine. */\nexport function findChromeBinary(): string | null {\n for (const p of candidateChromePaths()) {\n if (existsSync(p)) return p;\n }\n return null;\n}\n\n/**\n * Launch a local Chromium instance with CDP remote debugging enabled.\n *\n * The caller is responsible for calling `handle.stop()` when done.\n *\n * @throws if no Chrome/Chromium binary is found on the system.\n */\nexport async function launchChromium(options: LaunchChromiumOptions = {}): Promise<ChromiumHandle> {\n const spawnImpl = options.spawnFn ?? spawn;\n\n // Resolve the CDP port — find a free one if not specified.\n const requestedPort = options.port ?? 0;\n const port = requestedPort === 0 ? await findFreePort() : requestedPort;\n\n const devUrl = options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n\n const binary = findChromeBinary();\n if (binary === null) {\n throw new Error(\n 'No Chrome/Chromium binary found on this system. ' +\n 'Install Google Chrome or Chromium and try again. ' +\n 'Searched: ' +\n candidateChromePaths().join(', '),\n );\n }\n\n const args = [\n `--remote-debugging-port=${port}`,\n '--no-first-run',\n '--no-default-browser-check',\n // Use a separate profile dir so the debugged instance doesn't interfere\n // with the user's regular Chrome profile.\n '--user-data-dir=/tmp/ait-devtools-chromium-profile',\n ...(options.extraArgs ?? []),\n devUrl,\n ];\n\n const child: ChildProcess = spawnImpl(binary, args, {\n // Detach stdio so the MCP server's stdio transport is not contaminated.\n stdio: 'ignore',\n detached: false,\n });\n\n // Allow the Node process to exit even if the child is still running.\n child.unref();\n\n const devtoolsUrl = `http://127.0.0.1:${port}`;\n\n process.stderr.write(\n `[ait-local-debug] Launched Chromium: ${binary}\\n` +\n `[ait-local-debug] CDP endpoint: ${devtoolsUrl}\\n` +\n `[ait-local-debug] Opening: ${devUrl}\\n`,\n );\n\n return {\n port,\n devtoolsUrl,\n stop(): void {\n try {\n child.kill();\n } catch {\n // Ignore — the child may have already exited.\n }\n },\n };\n}\n","import type { StringKey } from './ko.js';\n\n// English translations. Mirrors every key in `ko.ts`; missing keys fall back to\n// the key string at runtime (see `t()` in index.ts), but the `Record<StringKey,\n// string>` type below means a missing key will typecheck-fail.\n\nexport const en: Record<StringKey, string> = {\n // Panel chrome\n 'panel.title': 'AIT DevTools',\n 'panel.toggle.title': 'AIT DevTools',\n 'panel.close': 'Close',\n 'panel.editMode.on': 'EDIT',\n 'panel.editMode.off': 'READ-ONLY',\n 'panel.editMode.toggleTitle': 'Toggle panel edit mode',\n 'panel.tabError': 'Error rendering \"{tab}\" tab.',\n\n // Tab names\n 'panel.tab.env': 'Environment',\n 'panel.tab.presets': 'Presets',\n 'panel.tab.viewport': 'Viewport',\n 'panel.tab.permissions': 'Permissions',\n 'panel.tab.notifications': 'Notifications',\n 'panel.tab.location': 'Location',\n 'panel.tab.device': 'Device',\n 'panel.tab.iap': 'IAP',\n 'panel.tab.ads': 'Ads',\n 'panel.tab.events': 'Events',\n 'panel.tab.analytics': 'Analytics',\n 'panel.tab.storage': 'Storage',\n\n // Common\n 'common.readOnly': 'Read-only — mock responses are controlled at build time.',\n\n // Consent toast\n 'toast.consent.title': 'Send anonymous usage stats?',\n 'toast.consent.body':\n 'We collect anonymous events only, to improve the tool. You can turn this off anytime in the Environment tab.',\n 'toast.consent.learnMore': 'Learn more',\n 'toast.consent.accept': 'Yes, send',\n 'toast.consent.deny': 'No, thanks',\n\n // Environment tab\n 'env.section.platform': 'Platform',\n 'env.row.os': 'OS',\n 'env.row.appVersion': 'App Version',\n 'env.row.environment': 'Environment',\n 'env.row.locale': 'Locale',\n 'env.section.network': 'Network',\n 'env.row.networkStatus': 'Status',\n 'env.section.safeArea': 'Safe Area Insets',\n 'env.row.safeArea.top': 'Top',\n 'env.row.safeArea.bottom': 'Bottom',\n 'env.section.navigation': 'Navigation',\n 'env.row.iosSwipeGesture': 'iOS swipe-back',\n 'env.value.iosSwipeGesture.unset': 'not called',\n 'env.value.iosSwipeGesture.enabled': 'enabled',\n 'env.value.iosSwipeGesture.disabled': 'disabled',\n 'env.hint.iosSwipeGesture':\n 'Last value passed to setIosSwipeGestureEnabled. Switching Environment to toss lets a toss-gated guard toggle this.',\n\n // Environment > Telemetry section\n 'env.telemetry.section': 'Telemetry',\n // Tier 0 — opt-out anonymous signal\n 'env.telemetry.t0Row': 'Anonymous usage signal (Tier 0)',\n 'env.telemetry.t0On': 'On',\n 'env.telemetry.t0Off': 'Off',\n 'env.telemetry.t0TurnOn': 'Turn on',\n 'env.telemetry.t0TurnOff': 'Turn off',\n 'env.telemetry.t0Desc': 'Version + date only, no PII. Once per day. Helps improve the package.',\n // Tier 1 — opt-in extended telemetry\n 'env.telemetry.row': 'Extended telemetry (Tier 1)',\n 'env.telemetry.on': 'On',\n 'env.telemetry.off': 'Off',\n 'env.telemetry.turnOn': 'Turn on',\n 'env.telemetry.turnOff': 'Turn off',\n 'env.telemetry.anonIdLabel': 'anon_id: {value}',\n 'env.telemetry.anonIdNotSet': '(not yet set)',\n 'env.telemetry.anonIdCopyTitle': 'Click to copy full anon_id',\n 'env.telemetry.deleteBtn': 'Delete my data',\n 'env.telemetry.deleting': 'Deleting…',\n 'env.telemetry.deleted': 'Deleted',\n 'env.telemetry.deleteFailedRetry': 'Delete failed (please retry)',\n 'env.telemetry.deleteFailed': 'Delete failed',\n 'env.telemetry.privacyLink': 'Privacy policy →',\n\n // Environment > Language toggle (new)\n 'env.section.language': 'Language',\n 'env.language.row': 'Language',\n 'env.language.ko': '한국어',\n 'env.language.en': 'English',\n\n // Permissions tab\n 'permissions.section.device': 'Device Permissions',\n\n // Location tab\n 'location.section.current': 'Current Location',\n 'location.row.latitude': 'Latitude',\n 'location.row.longitude': 'Longitude',\n 'location.row.accuracy': 'Accuracy',\n\n // Device tab\n 'device.section.modes': 'Device API Modes',\n 'device.row.camera': 'Camera',\n 'device.row.photos': 'Photos',\n 'device.row.location': 'Location',\n 'device.row.network': 'Network',\n 'device.row.clipboard': 'Clipboard',\n 'device.section.mockImages': 'Mock Images ({count})',\n 'device.btn.add': '+ Add',\n 'device.btn.useDefaults': 'Use defaults',\n 'device.btn.clear': 'Clear',\n 'device.prompt.camera.title': 'Camera Prompt — Select an image',\n 'device.prompt.photos.title': 'Photos Prompt — Select images',\n 'device.prompt.location.title': 'Location Prompt — Enter coordinates',\n 'device.prompt.locationUpdate.title': 'Location Update — Send coordinates',\n 'device.prompt.fallbackTitle': 'Prompt: {type}',\n 'device.prompt.label.lat': 'Lat',\n 'device.prompt.label.lng': 'Lng',\n 'device.prompt.send': 'Send',\n 'device.prompt.cancel': 'Cancel',\n\n // Device tab — Haptic section\n 'device.section.haptic': 'Haptic',\n 'device.haptic.lastCall': 'Last haptic',\n 'device.haptic.noneYet': '(none yet)',\n 'device.haptic.trigger': 'Trigger haptic',\n\n // Viewport tab\n 'viewport.section.device': 'Device',\n 'viewport.row.preset': 'Preset',\n 'viewport.row.orientation': 'Orientation',\n 'viewport.row.notchSide': 'Notch side',\n 'viewport.section.custom': 'Custom size',\n 'viewport.row.width': 'Width (px)',\n 'viewport.row.height': 'Height (px)',\n 'viewport.section.appearance': 'Appearance',\n 'viewport.row.showFrame': 'Show frame',\n 'viewport.row.showAitNavBar': 'Show Apps in Toss nav bar',\n 'viewport.row.navBarType': 'Nav bar type',\n 'viewport.status.noConstraint': 'No viewport constraint — body fills the window.',\n 'viewport.status.cssPhysical': 'CSS / physical',\n 'viewport.status.safeArea': 'Safe area',\n 'viewport.status.aitNavBar': 'AIT nav bar',\n 'viewport.status.aitNavBarValue': '{height}px → SafeArea top · {type}',\n 'viewport.orientation.autoSuffix': '{orient} (auto)',\n\n // IAP tab\n 'iap.section.simulator': 'IAP Simulator',\n 'iap.row.nextResult': 'Next Purchase Result',\n 'iap.section.tossPay': 'TossPay',\n 'iap.row.tossPayResult': 'Next Payment Result',\n 'iap.section.pending': 'Pending Orders ({count})',\n 'iap.empty.pending': '(no pending orders)',\n 'iap.section.completed': 'Completed Orders ({count})',\n 'iap.empty.completed': '(no completed orders)',\n 'iap.btn.complete': 'Complete',\n 'iap.label.pending': 'PENDING',\n\n // Events tab\n 'events.section.navigation': 'Navigation Events',\n 'events.btn.triggerBack': 'Trigger Back Event',\n 'events.btn.triggerHome': 'Trigger Home Event',\n 'events.section.login': 'Login',\n 'events.row.loggedIn': 'Logged In',\n 'events.row.tossLoginIntegrated': 'Toss Login Integrated',\n\n // Analytics tab\n 'analytics.section.log': 'Analytics Log ({count})',\n 'analytics.btn.clear': 'Clear',\n 'analytics.calls.section': 'SDK Calls ({count})',\n 'analytics.calls.btn.clear': 'Clear',\n 'analytics.calls.empty': '(no SDK calls yet)',\n\n // Storage tab\n 'storage.section.title': 'Storage ({count} items)',\n 'storage.btn.clearAll': 'Clear All',\n 'storage.empty': 'No items in storage',\n\n // Presets tab\n 'presets.section.builtIn': 'Built-in scenarios',\n 'presets.section.saved': 'Saved presets ({count})',\n 'presets.section.save': 'Save',\n 'presets.save.description': 'Capture network / permissions / auth / IAP / ads / payment slices.',\n 'presets.btn.saveCurrent': 'Save current as preset',\n 'presets.btn.apply': 'Apply',\n 'presets.btn.reApply': 'Re-apply',\n 'presets.btn.delete': 'Delete',\n 'presets.empty.saved': 'No saved presets yet.',\n 'presets.empty.builtIn': 'No built-in presets.',\n 'presets.prompt.label': 'Preset label?',\n 'presets.confirm.delete': 'Delete preset \"{label}\"?',\n\n // Ads tab\n 'ads.section.state': 'Ads State',\n 'ads.row.isLoaded': 'isLoaded',\n 'ads.row.forceNoFill': 'Force \"no fill\"',\n 'ads.empty.events': 'No events yet',\n 'ads.section.googleAdMob': 'GoogleAdMob',\n 'ads.section.tossAds': 'TossAds',\n 'ads.section.fullScreenAd': 'FullScreenAd',\n 'ads.btn.load': 'Load',\n 'ads.btn.show': 'Show',\n 'ads.section.tossAdsBanner': 'TossAds Banner',\n 'ads.row.rewardUnitType': 'Reward unit type',\n 'ads.row.rewardAmount': 'Reward amount',\n 'ads.btn.render': 'Render',\n 'ads.btn.noFill': 'No-fill',\n 'ads.btn.click': 'Click',\n 'ads.btn.destroy': 'Destroy',\n\n // Notifications tab\n 'notifications.section.title': 'requestNotificationAgreement',\n 'notifications.option.newAgreement': 'newAgreement (first-time agree)',\n 'notifications.option.alreadyAgreed': 'alreadyAgreed (already opted-in)',\n 'notifications.option.agreementRejected': 'agreementRejected (user declined)',\n\n // qr-http-server — lang switcher (dashboard / attach pages)\n 'dashboard.lang.ko': '한국어',\n 'dashboard.lang.en': 'English',\n\n // qr-http-server — dashboard page (server-side, Node, per-request)\n 'dashboard.title': 'AIT Debug Dashboard',\n 'dashboard.updated': 'Last updated: {ts}',\n 'dashboard.tunnel.section': 'Tunnel status',\n 'dashboard.tunnel.up': 'Connected',\n 'dashboard.tunnel.down': 'Disconnected',\n 'dashboard.attach.section': 'Attach QR',\n 'dashboard.attach.hint': 'Call the build_attach_url MCP tool to show the QR here.',\n 'dashboard.pages.section': 'Connected Pages',\n 'dashboard.pages.empty': 'No attached pages',\n\n // qr-http-server — url-box copy button\n 'dashboard.url.copy': 'Copy',\n 'dashboard.url.copied': 'Copied',\n\n // qr-http-server — inspector open link (#503)\n 'dashboard.inspector.section': 'Inspector',\n 'dashboard.inspector.open': 'Open DevTools',\n 'dashboard.inspector.waiting': 'Attach a page to enable the \"Open DevTools\" button',\n\n // qr-http-server — /inspector stable entry (issue #530)\n 'inspector.error.noTarget': 'No page attached. Attach a device and try again.',\n 'inspector.error.relayDown': 'Relay is not active. Start a relay session first.',\n\n // qr-http-server — attach page (server-side, Node, per-request)\n // Copy branches per session mode into sandbox (env 2) / intoss (env 3·4) families (#468).\n 'attach.title': 'AIT Debug Session — QR Scan',\n 'attach.deployment': 'deployment: {label}',\n 'attach.steps.section': 'How to scan',\n 'attach.faq.section': 'Troubleshooting checklist',\n 'attach.url.section': 'URL (fallback)',\n\n // qr-http-server — attach page mode label (environment visibility, #468)\n 'attach.mode.sandbox': 'env 2 — AITC Sandbox App (PWA)',\n 'attach.mode.intossDev': 'env 3 — intoss-private relay dev',\n 'attach.mode.intossLive': 'env 4 — intoss live relay debug',\n\n // attach page — sandbox family (env 2: launcher PWA; no Toss app / _deploymentId concepts)\n 'attach.sandbox.step1':\n 'Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).',\n 'attach.sandbox.step2':\n 'Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.',\n 'attach.sandbox.step3':\n 'The mini-app opens fullscreen and the debug session attaches automatically.',\n 'attach.sandbox.faq.notInstalled':\n '<strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen',\n 'attach.sandbox.faq.cameraApp':\n '<strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner',\n 'attach.sandbox.faq.totp':\n '<strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code',\n 'attach.sandbox.faq.chii':\n '<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import',\n\n // attach page — intoss family (env 3·4: Toss app deep-link)\n 'attach.intoss.step1': 'Open the Toss app.',\n 'attach.intoss.step2': 'Scan the QR code with your phone camera app.',\n 'attach.intoss.step3': 'Tap <strong>\"Open in Toss\"</strong> when the popup appears.',\n 'attach.intoss.step4': 'The mini-app opens and the debug session attaches automatically.',\n 'attach.intoss.faq.appNotOpen':\n '<strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)',\n 'attach.intoss.faq.prepare':\n '<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter',\n 'attach.intoss.faq.chii':\n '<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import',\n 'attach.intoss.faq.totp':\n '<strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server',\n // env 4 (relay-live) only — appended to the intoss family at runtime (#468).\n 'attach.intoss.faq.liveReadOnly':\n '<strong>LIVE session is read-only</strong> — <code>call_sdk</code>/<code>evaluate</code> require an explicit <code>confirm</code>',\n\n // Launcher PWA\n 'launcher.title': 'AITC DevTools Launcher',\n 'launcher.description': 'Scan the terminal QR code or paste the tunnel URL.',\n 'launcher.installCta': 'Install launcher to your phone',\n 'launcher.urlPlaceholder': 'https://example.trycloudflare.com',\n 'launcher.openBtn': 'Open',\n 'launcher.scanBtn': 'Scan QR with camera',\n 'launcher.noCamera': 'No camera available — paste the URL instead.',\n 'launcher.cameraError': 'Could not access the camera — paste the URL instead.',\n 'launcher.invalidUrlHttps': 'Enter a valid https:// URL (the tunnel URL from your terminal).',\n 'launcher.invalidUrl': 'Enter a valid http(s):// URL.',\n 'launcher.debugAuthFailed': 'Debug connection authentication failed',\n 'launcher.debugAuthFailedHint': 'The QR code may have expired. Scan a fresh QR code.',\n 'launcher.debugAuthExpiredHint':\n 'The debug session has expired. Scan a fresh QR from the attach page on your Mac.',\n 'launcher.debugAuthRescanCta': 'Scan a new QR',\n 'launcher.diagTitle': 'Viewport diagnostics',\n 'launcher.diagYes': 'yes',\n 'launcher.diagNo': 'no',\n 'launcher.letterboxDetected':\n 'An iOS viewport constraint may clip the bottom {pt}pt — rotating to landscape and back to portrait may resolve it.',\n 'launcher.letterboxClipped':\n 'An iOS viewport bug makes the bottom {pt}pt unusable — rotating to landscape and back to portrait may recover it.',\n // #536: verdict reason labels for diag panel\n 'launcher.diagVerdictLabel': 'Verdict reason',\n 'launcher.diagSafeAreaTrace': 'top re-measure trace',\n 'launcher.diagVerdict.detected': '✓ letterbox correction',\n 'launcher.diagVerdict.notStandalone': 'not standalone',\n 'launcher.diagVerdict.landscape': 'landscape',\n 'launcher.diagVerdict.shortfallTooSmall': 'shortfall too small',\n 'launcher.diagVerdict.safeAreaTopZero': 'top=0 (env() stale?)',\n // Nav-bar emulation (#495/#510)\n 'launcher.navbar.defaultTitle': 'Mini App',\n 'launcher.navbar.back': 'Back',\n 'launcher.navbar.menu': 'Menu',\n 'launcher.navbar.close': 'Close',\n 'launcher.navbar.menuRescan': 'Rescan',\n 'launcher.navbar.menuDiag': 'Viewport diagnostics',\n 'launcher.navbar.menuLanguage': 'Language',\n};\n","// Korean string catalog (source of truth — keys are typed from this file).\n// Keys follow `<area>.<purpose>` convention. Variable interpolation uses\n// `{name}` placeholders resolved by `t(key, { name: value })`.\n//\n// Some chrome (button labels like \"Load\", \"Show\", \"Clear\", \"Apply\") is left as\n// English in both locales because the panel is an internal devtools surface\n// and these terms are universally recognised by developers in both locales.\n\nexport const ko = {\n // Panel chrome\n 'panel.title': 'AIT DevTools',\n 'panel.toggle.title': 'AIT DevTools',\n 'panel.close': 'Close',\n 'panel.editMode.on': 'EDIT',\n 'panel.editMode.off': 'READ-ONLY',\n 'panel.editMode.toggleTitle': '패널 편집 모드 전환',\n 'panel.tabError': '\"{tab}\" 탭 렌더링 중 오류가 발생했습니다.',\n\n // Tab names\n 'panel.tab.env': 'Environment',\n 'panel.tab.presets': 'Presets',\n 'panel.tab.viewport': 'Viewport',\n 'panel.tab.permissions': 'Permissions',\n 'panel.tab.notifications': 'Notifications',\n 'panel.tab.location': 'Location',\n 'panel.tab.device': 'Device',\n 'panel.tab.iap': 'IAP',\n 'panel.tab.ads': 'Ads',\n 'panel.tab.events': 'Events',\n 'panel.tab.analytics': 'Analytics',\n 'panel.tab.storage': 'Storage',\n\n // Common\n 'common.readOnly': '읽기 전용 — mock 응답은 빌드 타임에 고정됩니다.',\n\n // Consent toast\n 'toast.consent.title': '익명 사용 통계를 보낼까요?',\n 'toast.consent.body': '도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.',\n 'toast.consent.learnMore': '더 알아보기',\n 'toast.consent.accept': '네, 보낼게요',\n 'toast.consent.deny': '아니요',\n\n // Environment tab\n 'env.section.platform': 'Platform',\n 'env.row.os': 'OS',\n 'env.row.appVersion': 'App Version',\n 'env.row.environment': 'Environment',\n 'env.row.locale': 'Locale',\n 'env.section.network': 'Network',\n 'env.row.networkStatus': 'Status',\n 'env.section.safeArea': 'Safe Area Insets',\n 'env.row.safeArea.top': 'Top',\n 'env.row.safeArea.bottom': 'Bottom',\n 'env.section.navigation': 'Navigation',\n 'env.row.iosSwipeGesture': 'iOS swipe-back',\n 'env.value.iosSwipeGesture.unset': '미호출',\n 'env.value.iosSwipeGesture.enabled': 'enabled',\n 'env.value.iosSwipeGesture.disabled': 'disabled',\n 'env.hint.iosSwipeGesture':\n 'setIosSwipeGestureEnabled의 마지막 호출값. Environment를 toss로 바꾸면 toss-gated 가드가 이 값을 토글합니다.',\n\n // Environment > Telemetry section\n 'env.telemetry.section': 'Telemetry',\n // Tier 0 — opt-out anonymous signal\n 'env.telemetry.t0Row': '익명 사용 신호 (Tier 0)',\n 'env.telemetry.t0On': 'On',\n 'env.telemetry.t0Off': 'Off',\n 'env.telemetry.t0TurnOn': 'Turn on',\n 'env.telemetry.t0TurnOff': 'Turn off',\n 'env.telemetry.t0Desc': '버전·날짜만 수집, PII 없음. 하루 1회. 패키지 개선에 사용됩니다.',\n // Tier 1 — opt-in extended telemetry\n 'env.telemetry.row': '확장 텔레메트리 (Tier 1)',\n 'env.telemetry.on': 'On',\n 'env.telemetry.off': 'Off',\n 'env.telemetry.turnOn': 'Turn on',\n 'env.telemetry.turnOff': 'Turn off',\n 'env.telemetry.anonIdLabel': 'anon_id: {value}',\n 'env.telemetry.anonIdNotSet': '(not yet set)',\n 'env.telemetry.anonIdCopyTitle': '전체 anon_id 복사',\n 'env.telemetry.deleteBtn': '내 데이터 삭제',\n 'env.telemetry.deleting': '삭제 중…',\n 'env.telemetry.deleted': '삭제 완료',\n 'env.telemetry.deleteFailedRetry': '삭제 실패 (다시 시도해주세요)',\n 'env.telemetry.deleteFailed': '삭제 실패',\n 'env.telemetry.privacyLink': '개인정보 처리방침 →',\n\n // Environment > Language toggle (new)\n 'env.section.language': 'Language',\n 'env.language.row': 'Language',\n 'env.language.ko': '한국어',\n 'env.language.en': 'English',\n\n // Permissions tab\n 'permissions.section.device': 'Device Permissions',\n\n // Location tab\n 'location.section.current': 'Current Location',\n 'location.row.latitude': 'Latitude',\n 'location.row.longitude': 'Longitude',\n 'location.row.accuracy': 'Accuracy',\n\n // Device tab\n 'device.section.modes': 'Device API Modes',\n 'device.row.camera': 'Camera',\n 'device.row.photos': 'Photos',\n 'device.row.location': 'Location',\n 'device.row.network': 'Network',\n 'device.row.clipboard': 'Clipboard',\n 'device.section.mockImages': 'Mock Images ({count})',\n 'device.btn.add': '+ Add',\n 'device.btn.useDefaults': 'Use defaults',\n 'device.btn.clear': 'Clear',\n 'device.prompt.camera.title': 'Camera Prompt — 이미지를 선택하세요',\n 'device.prompt.photos.title': 'Photos Prompt — 이미지를 선택하세요',\n 'device.prompt.location.title': 'Location Prompt — 좌표 입력',\n 'device.prompt.locationUpdate.title': 'Location Update — 좌표 전송',\n 'device.prompt.fallbackTitle': 'Prompt: {type}',\n 'device.prompt.label.lat': 'Lat',\n 'device.prompt.label.lng': 'Lng',\n 'device.prompt.send': 'Send',\n 'device.prompt.cancel': 'Cancel',\n\n // Device tab — Haptic section\n 'device.section.haptic': 'Haptic',\n 'device.haptic.lastCall': '마지막 haptic',\n 'device.haptic.noneYet': '(아직 없음)',\n 'device.haptic.trigger': 'Haptic 트리거',\n\n // Viewport tab\n 'viewport.section.device': 'Device',\n 'viewport.row.preset': 'Preset',\n 'viewport.row.orientation': 'Orientation',\n 'viewport.row.notchSide': 'Notch side',\n 'viewport.section.custom': 'Custom size',\n 'viewport.row.width': 'Width (px)',\n 'viewport.row.height': 'Height (px)',\n 'viewport.section.appearance': 'Appearance',\n 'viewport.row.showFrame': 'Show frame',\n 'viewport.row.showAitNavBar': 'Apps in Toss 내비게이션 바 표시',\n 'viewport.row.navBarType': 'Nav bar type',\n 'viewport.status.noConstraint': '뷰포트 제약 없음 — body가 창을 가득 채웁니다.',\n 'viewport.status.cssPhysical': 'CSS / physical',\n 'viewport.status.safeArea': 'Safe area',\n 'viewport.status.aitNavBar': 'AIT nav bar',\n 'viewport.status.aitNavBarValue': '{height}px → SafeArea top · {type}',\n 'viewport.orientation.autoSuffix': '{orient} (auto)',\n\n // IAP tab\n 'iap.section.simulator': 'IAP Simulator',\n 'iap.row.nextResult': 'Next Purchase Result',\n 'iap.section.tossPay': 'TossPay',\n 'iap.row.tossPayResult': 'Next Payment Result',\n 'iap.section.pending': 'Pending Orders ({count})',\n 'iap.empty.pending': '(대기 중인 주문 없음)',\n 'iap.section.completed': 'Completed Orders ({count})',\n 'iap.empty.completed': '(완료된 주문 없음)',\n 'iap.btn.complete': 'Complete',\n 'iap.label.pending': 'PENDING',\n\n // Events tab\n 'events.section.navigation': 'Navigation Events',\n 'events.btn.triggerBack': 'Back 이벤트 발생',\n 'events.btn.triggerHome': 'Home 이벤트 발생',\n 'events.section.login': 'Login',\n 'events.row.loggedIn': 'Logged In',\n 'events.row.tossLoginIntegrated': 'Toss Login Integrated',\n\n // Analytics tab\n 'analytics.section.log': 'Analytics Log ({count})',\n 'analytics.btn.clear': 'Clear',\n 'analytics.calls.section': 'SDK Calls ({count})',\n 'analytics.calls.btn.clear': 'Clear',\n 'analytics.calls.empty': '(아직 SDK 호출 없음)',\n\n // Storage tab\n 'storage.section.title': 'Storage ({count} items)',\n 'storage.btn.clearAll': 'Clear All',\n 'storage.empty': '저장된 항목이 없습니다',\n\n // Presets tab\n 'presets.section.builtIn': 'Built-in scenarios',\n 'presets.section.saved': 'Saved presets ({count})',\n 'presets.section.save': 'Save',\n 'presets.save.description':\n 'network / permissions / auth / IAP / ads / payment 슬라이스를 캡처합니다.',\n 'presets.btn.saveCurrent': '현재 상태를 프리셋으로 저장',\n 'presets.btn.apply': 'Apply',\n 'presets.btn.reApply': 'Re-apply',\n 'presets.btn.delete': 'Delete',\n 'presets.empty.saved': '저장된 프리셋이 아직 없습니다.',\n 'presets.empty.builtIn': '내장 프리셋이 없습니다.',\n 'presets.prompt.label': '프리셋 라벨을 입력하세요',\n 'presets.confirm.delete': '\"{label}\" 프리셋을 삭제할까요?',\n\n // Ads tab\n 'ads.section.state': 'Ads State',\n 'ads.row.isLoaded': 'isLoaded',\n 'ads.row.forceNoFill': '강제 \"no fill\"',\n 'ads.empty.events': '아직 이벤트가 없습니다',\n 'ads.section.googleAdMob': 'GoogleAdMob',\n 'ads.section.tossAds': 'TossAds',\n 'ads.section.fullScreenAd': 'FullScreenAd',\n 'ads.btn.load': 'Load',\n 'ads.btn.show': 'Show',\n 'ads.section.tossAdsBanner': 'TossAds 배너',\n 'ads.row.rewardUnitType': '리워드 단위 타입',\n 'ads.row.rewardAmount': '리워드 수량',\n 'ads.btn.render': 'Render',\n 'ads.btn.noFill': 'No-fill',\n 'ads.btn.click': 'Click',\n 'ads.btn.destroy': 'Destroy',\n\n // Notifications tab\n 'notifications.section.title': 'requestNotificationAgreement',\n 'notifications.option.newAgreement': 'newAgreement (최초 동의)',\n 'notifications.option.alreadyAgreed': 'alreadyAgreed (이미 동의됨)',\n 'notifications.option.agreementRejected': 'agreementRejected (사용자 거절)',\n\n // qr-http-server — lang switcher (dashboard / attach pages)\n 'dashboard.lang.ko': '한국어',\n 'dashboard.lang.en': 'English',\n\n // qr-http-server — dashboard page (server-side, Node, per-request)\n 'dashboard.title': 'AIT 디버그 Dashboard',\n 'dashboard.updated': '마지막 갱신: {ts}',\n 'dashboard.tunnel.section': '터널 상태',\n 'dashboard.tunnel.up': '연결됨',\n 'dashboard.tunnel.down': '끊어짐',\n 'dashboard.attach.section': 'Attach QR',\n 'dashboard.attach.hint': 'build_attach_url MCP tool을 호출하면 QR이 여기에 표시됩니다.',\n 'dashboard.pages.section': '연결된 Pages',\n 'dashboard.pages.empty': 'attach된 페이지 없음',\n\n // qr-http-server — url-box copy button\n 'dashboard.url.copy': '복사',\n 'dashboard.url.copied': '복사됨',\n\n // qr-http-server — inspector open link (#503)\n 'dashboard.inspector.section': '인스펙터',\n 'dashboard.inspector.open': '디버그 툴 열기',\n 'dashboard.inspector.waiting': '페이지를 attach하면 \"디버그 툴 열기\" 버튼이 표시됩니다',\n\n // qr-http-server — /inspector stable entry (issue #530)\n 'inspector.error.noTarget': '연결된 페이지가 없습니다. 기기를 attach한 후 다시 시도하세요.',\n 'inspector.error.relayDown': 'relay가 활성화되지 않았습니다. start_debug로 relay를 기동하세요.',\n\n // qr-http-server — attach page (server-side, Node, per-request)\n // 카피는 세션 mode별로 sandbox(환경 2) / intoss(환경 3·4) family로 분기한다 (#468).\n 'attach.title': 'AIT 디버그 세션 — QR 스캔',\n 'attach.deployment': 'deployment: {label}',\n 'attach.steps.section': '스캔 절차',\n 'attach.faq.section': '진단 체크리스트',\n 'attach.url.section': 'URL (fallback)',\n\n // qr-http-server — attach page mode 라벨 (환경 가시화, #468)\n 'attach.mode.sandbox': '환경 2 — AITC Sandbox App (PWA)',\n 'attach.mode.intossDev': '환경 3 — intoss-private relay dev',\n 'attach.mode.intossLive': '환경 4 — intoss live relay debug',\n\n // attach page — sandbox family (환경 2: launcher PWA, 토스 앱·_deploymentId 개념 없음)\n 'attach.sandbox.step1':\n '홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).',\n 'attach.sandbox.step2':\n 'launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.',\n 'attach.sandbox.step3': '미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.',\n 'attach.sandbox.faq.notInstalled':\n '<strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요',\n 'attach.sandbox.faq.cameraApp':\n '<strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요',\n 'attach.sandbox.faq.totp':\n '<strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요',\n 'attach.sandbox.faq.chii':\n '<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인',\n\n // attach page — intoss family (환경 3·4: 토스 앱 deep-link)\n 'attach.intoss.step1': '토스 앱을 실행하세요.',\n 'attach.intoss.step2': '폰 카메라 앱으로 QR 코드를 스캔하세요.',\n 'attach.intoss.step3': '팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.',\n 'attach.intoss.step4': '미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.',\n 'attach.intoss.faq.appNotOpen':\n '<strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)',\n 'attach.intoss.faq.prepare':\n '<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인',\n 'attach.intoss.faq.chii':\n '<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인',\n 'attach.intoss.faq.totp':\n '<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인',\n // 환경 4(relay-live) 전용 — intoss family에 런타임으로 한 줄 추가된다 (#468).\n 'attach.intoss.faq.liveReadOnly':\n '<strong>LIVE 세션은 read-only입니다</strong> — <code>call_sdk</code>/<code>evaluate</code> 실행에는 명시적 <code>confirm</code>이 필요합니다',\n\n // Launcher PWA\n 'launcher.title': 'AITC DevTools Launcher',\n 'launcher.description': '터미널 QR을 스캔하거나 URL을 입력하세요.',\n 'launcher.installCta': '폰에 런처 설치하기',\n 'launcher.urlPlaceholder': 'https://example.trycloudflare.com',\n 'launcher.openBtn': 'Open',\n 'launcher.scanBtn': 'QR 카메라로 스캔',\n 'launcher.noCamera': '카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.',\n 'launcher.cameraError': '카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.',\n 'launcher.invalidUrlHttps': '올바른 https:// URL을 입력하세요 (터미널의 터널 URL).',\n 'launcher.invalidUrl': '올바른 http(s):// URL을 입력하세요.',\n 'launcher.debugAuthFailed': '디버그 연결 인증 실패',\n 'launcher.debugAuthFailedHint': 'QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.',\n 'launcher.debugAuthExpiredHint':\n '디버그 세션이 만료됐어요. Mac의 attach 페이지에서 새 QR을 스캔하세요.',\n 'launcher.debugAuthRescanCta': '새 QR 스캔하기',\n 'launcher.diagTitle': '뷰포트 진단',\n 'launcher.diagYes': '예',\n 'launcher.diagNo': '아니요',\n 'launcher.letterboxDetected':\n 'iOS 뷰포트 제약으로 화면 아래 {pt}pt가 잘릴 수 있습니다 — 기기를 가로로 돌렸다 세로로 복귀하면 해소될 수 있어요.',\n 'launcher.letterboxClipped':\n 'iOS 뷰포트 버그로 화면 아래 {pt}pt를 쓸 수 없습니다 — 기기를 가로로 돌렸다 세로로 돌리면 복구될 수 있어요.',\n // #536: verdict reason labels for diag panel\n 'launcher.diagVerdictLabel': '판정 사유',\n 'launcher.diagSafeAreaTrace': 'top 재측정 추이',\n 'launcher.diagVerdict.detected': '✓ letterbox 보정',\n 'launcher.diagVerdict.notStandalone': '홈 화면 앱 아님',\n 'launcher.diagVerdict.landscape': '가로 모드',\n 'launcher.diagVerdict.shortfallTooSmall': '높이 차이 미달',\n 'launcher.diagVerdict.safeAreaTopZero': 'top=0 (env() stale?)',\n // Nav-bar emulation (#495/#510)\n 'launcher.navbar.defaultTitle': '미니앱',\n 'launcher.navbar.back': '뒤로가기',\n 'launcher.navbar.menu': '메뉴',\n 'launcher.navbar.close': '닫기',\n 'launcher.navbar.menuRescan': '다시 스캔',\n 'launcher.navbar.menuDiag': '뷰포트 진단',\n 'launcher.navbar.menuLanguage': '언어',\n} as const;\n\nexport type StringKey = keyof typeof ko;\n","/**\n * Vanilla TS i18n for the floating DevTools panel.\n *\n * Public surface:\n * - `t(key, vars?)` — look up a UI string, with `{name}` placeholder\n * interpolation. Falls back to the key itself if a translation is missing.\n * - `getLocale()` / `setLocale(locale)` — read/persist the active locale.\n * `setLocale` dispatches `__ait:localechange` so the panel can remount.\n * - `detectLocale()` — first-run heuristic from `navigator.language`.\n *\n * `ko` is the source of truth (keys are typed from it). `en` is also a full\n * `Record<StringKey, string>` (devtools is developer-facing, en is a real\n * audience). The `Partial` lookup table preserves the runtime `?? key` safety\n * net even though we ship complete catalogs today.\n */\n\nimport { en } from './en.js';\nimport { ko, type StringKey } from './ko.js';\n\nexport type Locale = 'ko' | 'en';\n\nconst LOCALE_STORAGE_KEY = '__ait_locale';\nconst LOCALE_CHANGE_EVENT = '__ait:localechange';\n\nconst tables: Record<Locale, Partial<Record<StringKey, string>>> = { ko, en };\n\nlet currentLocale: Locale | null = null;\n\nfunction safeReadStorage(): Locale | null {\n if (typeof localStorage === 'undefined') return null;\n try {\n const raw = localStorage.getItem(LOCALE_STORAGE_KEY);\n if (raw === 'ko' || raw === 'en') return raw;\n } catch {\n /* localStorage can throw in privacy modes — fall back silently */\n }\n return null;\n}\n\nfunction safeWriteStorage(locale: Locale): void {\n if (typeof localStorage === 'undefined') return;\n try {\n localStorage.setItem(LOCALE_STORAGE_KEY, locale);\n } catch {\n /* ignore quota / privacy errors */\n }\n}\n\n/**\n * Decide a locale from a BCP-47 language tag. `ko` (and `ko-*`) → `'ko'`,\n * everything else → `'en'`. Shared by the browser (`navigator.language`) and\n * Node (`Accept-Language` header) paths so both resolve identically.\n */\nfunction localeFromLanguageTag(lang: string): Locale {\n return /^ko\\b/i.test(lang) ? 'ko' : 'en';\n}\n\n/**\n * Read `navigator.language` and decide a locale. `ko` (and `ko-*`) → `'ko'`,\n * everything else → `'en'`. Pure function; does not touch storage.\n */\nexport function detectLocale(): Locale {\n if (typeof navigator === 'undefined') return 'en';\n return localeFromLanguageTag(navigator.language ?? '');\n}\n\n/**\n * Decide a locale from an HTTP `Accept-Language` header value. The Node-served\n * surfaces (e.g. the qr-http-server dashboard) have no `navigator`, so the\n * request header is the only language signal. Reads the FIRST language tag\n * (highest priority, ignoring `q=` weights — good enough for ko/en) and feeds\n * it through the same `ko`-vs-`en` heuristic `detectLocale` uses. Returns `'ko'`\n * for an empty/missing header (ko is the primary locale).\n */\nexport function parseAcceptLanguage(header: string | undefined | null): Locale {\n if (!header) return 'ko';\n const first = header.split(',')[0]?.trim().split(';')[0]?.trim() ?? '';\n return localeFromLanguageTag(first);\n}\n\n/**\n * A locale-bound string resolver for surfaces that can't use the in-memory\n * `getLocale()` cache — notably the Node HTTP server, which resolves locale\n * per-request from `Accept-Language` rather than from a process-global. Returns\n * a `t`-compatible closure over the SAME `ko`/`en` tables (single source of\n * truth), so the dashboard/attach HTML shares the exact 169-key catalog the\n * browser surfaces use. The `key: StringKey` signature keeps compile-time key\n * safety on the Node path identical to `t()`.\n */\nexport function resolveLocaleStrings(\n locale: Locale,\n): (key: StringKey, vars?: Record<string, string | number>) => string {\n const table = tables[locale];\n return (key, vars) => {\n const raw = table[key] ?? key;\n if (!vars) return raw;\n return raw.replace(/\\{(\\w+)\\}/g, (match, name: string) => {\n const value = vars[name];\n return value === undefined ? match : String(value);\n });\n };\n}\n\n/**\n * Resolve the active locale, in order:\n * 1. previously set in-memory value (set by `setLocale`)\n * 2. localStorage `__ait_locale`\n * 3. `detectLocale()` from navigator\n */\nexport function getLocale(): Locale {\n if (currentLocale) return currentLocale;\n const stored = safeReadStorage();\n currentLocale = stored ?? detectLocale();\n return currentLocale;\n}\n\n/**\n * Persist a locale choice and notify listeners. The panel listens for\n * `__ait:localechange` and re-mounts so every string re-evaluates.\n */\nexport function setLocale(locale: Locale): void {\n currentLocale = locale;\n safeWriteStorage(locale);\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent(LOCALE_CHANGE_EVENT));\n }\n}\n\n/**\n * Look up a UI string for the current locale. Falls back to the key if missing,\n * so a forgotten key surfaces visibly rather than rendering empty.\n */\nexport function t(key: StringKey, vars?: Record<string, string | number>): string {\n const raw = tables[getLocale()][key] ?? key;\n if (!vars) return raw;\n return raw.replace(/\\{(\\w+)\\}/g, (match, name: string) => {\n const value = vars[name];\n return value === undefined ? match : String(value);\n });\n}\n\nexport type { StringKey };\nexport { LOCALE_CHANGE_EVENT, LOCALE_STORAGE_KEY };\n\n/**\n * Test-only escape hatch — resets the cached in-memory locale so subsequent\n * `getLocale()` calls re-read storage / re-detect. Production code never needs\n * this; tests use it between cases.\n */\nexport function _resetLocaleCacheForTests(): void {\n currentLocale = null;\n}\n","/**\n * dashboard.generated.ts\n *\n * AUTO-GENERATED by scripts/build-dashboard-html.ts — DO NOT EDIT BY HAND.\n * Regenerate: pnpm build:dashboard-html\n *\n * Exports precompiled HTML chrome strings for each locale. Per-request\n * dynamic values are inserted by qr-http-server.ts at runtime via simple\n * string replacement of __PLACEHOLDER__ tokens.\n *\n * Token map (dashboard chrome):\n * __TUNNEL_CLASS__ CSS class: \"status-up\" | \"status-down\"\n * __TUNNEL_STATUS__ localised tunnel status label\n * __ATTACH_SECTION__ QR img+url-box HTML, or hint text\n * __INSPECTOR_SECTION__ inspector link <a> or waiting hint <span> (#503)\n * __PAGES_SECTION__ pages <section> block, or empty string\n * __NOW__ ISO timestamp of current render\n * __LANG_SWITCHER__ ko/en toggle links (href preserves existing query params)\n *\n * Token map (attach chrome — precompiled per locale × copy family, #468):\n * __QR_DATA_URL__ base64 data URL for the QR image\n * __SAFE_LABEL__ HTML-escaped deploymentId label (intoss family only)\n * __SAFE_ATTACH_URL__ HTML-escaped attach URL\n * __MODE_LABEL__ environment badge (<p class=\"mode-label\">…</p>), or empty\n * __LIVE_FAQ__ env-4 LIVE read-only <li>, or empty (intoss family only)\n * __LANG_SWITCHER__ ko/en toggle links (href preserves existing query params)\n *\n * SECRET-HANDLING: wssUrl MUST NOT appear here. If it does, the build script's\n * assertion would have caught it — this file should be react-free and secret-free.\n */\n\nimport type { Locale } from '../i18n/index.js';\n\n/** Copy family of the attach page chrome (#468) — env 2 vs env 3/4. */\nexport type AttachChromeFamily = 'sandbox' | 'intoss';\n\n// ── locale: ko ──────────────────────────────────────────────────────\n\nexport const dashboardChromeHtmlKo =\n`<!DOCTYPE html>\n<html lang=\"ko\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><title>AIT 디버그 Dashboard</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.updated { font-size: 0.75rem; opacity: 0.4; font-family: monospace; margin: 0; }\nsection { width: 100%; max-width: 520px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\n.status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }\n.status-up { background: #238636; color: #fff; }\n.status-down { background: #6e7681; color: #fff; }\nimg.qr {\n width: min(80vw, 300px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 0.75rem; border-radius: 10px;\n display: block; margin: 0.5rem auto;\n}\n.url-row {\n display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.7rem;\n word-break: break-all; opacity: 0.45;\n background: #161b22; padding: 0.6rem 0.85rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.65; }\n.copy-btn {\n flex-shrink: 0; padding: 0.4rem 0.7rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\n.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\nul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }\nli.empty { opacity: 0.4; list-style: none; padding-left: 0; }\n.page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }\n.page-url { word-break: break-all; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT 디버그 Dashboard</h1>__LANG_SWITCHER__<p class=\"updated\" id=\"updated\">마지막 갱신: __NOW__</p><section><h2>터널 상태</h2><span class=\"status __TUNNEL_CLASS__\" id=\"tunnel-status\">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id=\"attach-section\">__ATTACH_SECTION__</div></section><hr/><section id=\"inspector-section\"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section>__PAGES_SECTION__</body></html>`;\n\nexport const attachChromeHtmlKoSandbox =\n`<!DOCTYPE html>\n<html lang=\"ko\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT 디버그 세션 — QR 스캔</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\n</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"복사\">복사</button></div></section><hr/><section id=\"inspector-section\"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section></body></html>`;\n\nexport const attachChromeHtmlKoIntoss =\n`<!DOCTYPE html>\n<html lang=\"ko\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT 디버그 세션 — QR 스캔</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\n</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<p class=\"label\">deployment: __SAFE_LABEL__</p><div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li>__LIVE_FAQ__</ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"복사\">복사</button></div></section><hr/><section id=\"inspector-section\"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section></body></html>`;\n\n// ── locale: en ──────────────────────────────────────────────────────\n\nexport const dashboardChromeHtmlEn =\n`<!DOCTYPE html>\n<html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><title>AIT Debug Dashboard</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.updated { font-size: 0.75rem; opacity: 0.4; font-family: monospace; margin: 0; }\nsection { width: 100%; max-width: 520px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\n.status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }\n.status-up { background: #238636; color: #fff; }\n.status-down { background: #6e7681; color: #fff; }\nimg.qr {\n width: min(80vw, 300px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 0.75rem; border-radius: 10px;\n display: block; margin: 0.5rem auto;\n}\n.url-row {\n display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.7rem;\n word-break: break-all; opacity: 0.45;\n background: #161b22; padding: 0.6rem 0.85rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.65; }\n.copy-btn {\n flex-shrink: 0; padding: 0.4rem 0.7rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\n.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\nul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }\nli.empty { opacity: 0.4; list-style: none; padding-left: 0; }\n.page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }\n.page-url { word-break: break-all; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT Debug Dashboard</h1>__LANG_SWITCHER__<p class=\"updated\" id=\"updated\">Last updated: __NOW__</p><section><h2>Tunnel status</h2><span class=\"status __TUNNEL_CLASS__\" id=\"tunnel-status\">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id=\"attach-section\">__ATTACH_SECTION__</div></section><hr/><section id=\"inspector-section\"><h2>Inspector</h2>__INSPECTOR_SECTION__</section>__PAGES_SECTION__</body></html>`;\n\nexport const attachChromeHtmlEnSandbox =\n`<!DOCTYPE html>\n<html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT Debug Session — QR Scan</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\n</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"Copy\">Copy</button></div></section><hr/><section id=\"inspector-section\"><h2>Inspector</h2>__INSPECTOR_SECTION__</section></body></html>`;\n\nexport const attachChromeHtmlEnIntoss =\n`<!DOCTYPE html>\n<html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT Debug Session — QR Scan</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\n</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<p class=\"label\">deployment: __SAFE_LABEL__</p><div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>\"Open in Toss\"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li>__LIVE_FAQ__</ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"Copy\">Copy</button></div></section><hr/><section id=\"inspector-section\"><h2>Inspector</h2>__INSPECTOR_SECTION__</section></body></html>`;\n\n/** Map from Locale to the precompiled dashboard chrome string. */\nexport const dashboardChromeByLocale: Record<Locale, string> = {\n ko: dashboardChromeHtmlKo,\n en: dashboardChromeHtmlEn,\n};\n\n/** Map from Locale × copy family to the precompiled attach page chrome string (#468). */\nexport const attachChromeByLocale: Record<Locale, Record<AttachChromeFamily, string>> = {\n ko: { sandbox: attachChromeHtmlKoSandbox, intoss: attachChromeHtmlKoIntoss },\n en: { sandbox: attachChromeHtmlEnSandbox, intoss: attachChromeHtmlEnIntoss },\n};\n","/**\n * 로컬 HTTP 서버 — QR 페이지를 `http://127.0.0.1:<port>` 에서 서빙한다.\n *\n * file:// origin 대신 HTTP origin을 쓰는 이유: 브라우저 보안 정책상 file://에서\n * 로드된 페이지는 외부 fetch/script가 전부 차단되며, file:// 절대 경로를 <img src>에\n * 넣으면 브라우저에 따라 빈 화면이 된다. 127.0.0.1 HTTP는 modern 브라우저가 fully trust.\n *\n * INSTALL-GRAPH INVARIANT:\n * 이 모듈은 react/react-dom을 절대 import하지 않는다. dashboard/attach HTML은\n * scripts/build-dashboard-html.ts가 빌드 타임에 precompile해 dashboard.generated.ts\n * (plain string exports)로 커밋한다. 이 모듈은 그 생성된 string만 import한다.\n * check-mcp-react-free.sh 가드가 dist/mcp/cli.js·server.js의 react 유입을 기계적으로 검증.\n *\n * HTML 조립 전략 (token-fill vs runtime builder):\n * - static chrome (head/style/섹션 레이블) → 빌드타임 precompile, dashboard.generated.ts\n * - 동적 부분 → 런타임 string 조립:\n * __NOW__ : per-request ISO timestamp\n * __TUNNEL_CLASS__ : \"status-up\" | \"status-down\"\n * __TUNNEL_STATUS__ : 로컬라이즈된 tunnel 상태 레이블\n * __ATTACH_SECTION__ : QR img+url-box, 또는 hint 텍스트\n * __PAGES_SECTION__ : pages <section> 블록, 또는 빈 문자열 (null → '')\n * - inline SSE <script> → 런타임 suffix로 append (localised string 포함)\n *\n * i18n:\n * GET / 와 GET /attach 라우트에서 req.headers['accept-language']를 읽어\n * parseAcceptLanguage()로 locale 결정. resolveLocaleStrings()로 동적 부분의\n * localised 문자열을 해결. navigator 없음, React hook 없음 (Node 표면).\n *\n * SECRET-HANDLING:\n * - 127.0.0.1 바인딩만 — 외부 노출 0.\n * - attachUrl은 HTML 본문과 /qr.png query에만 들어간다 (의도된 전달 경로).\n * - wssUrl은 dashboard HTML에 절대 들어가지 않는다. tunnel.up boolean만 사용.\n * - stdout/stderr/로그에 별도 출력하지 않는다.\n * - tmp 파일 만들지 않음 — 모든 응답을 메모리에서 생성.\n * - TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — SSE payload나 page 목록 등\n * 다른 필드에 TOTP 코드를 평문으로 싣지 않는다.\n */\n\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\nimport { createServer } from 'node:http';\nimport { parseAcceptLanguage, resolveLocaleStrings } from '../i18n/index.js';\nimport {\n type AttachChromeFamily,\n attachChromeByLocale,\n dashboardChromeByLocale,\n} from './dashboard.generated.js';\nimport type { McpEnvironment } from './environment.js';\n\n/** dashboard에 노출되는 현재 상태 스냅샷. */\nexport interface DashboardState {\n /** 현재 터널 상태 — up/down + wssUrl. SECRET: wssUrl은 로그 출력 금지. */\n tunnel: { up: boolean; wssUrl: string | null };\n /**\n * 현재 연결된 page 목록 (id/url만).\n *\n * - `Array<…>` — env 3/4(MCP): relay에 attach된 페이지를 라이브 조회한 목록.\n * 빈 배열 `[]`은 \"attach된 페이지 없음\"으로 정직하게 표시한다.\n * - `null` — env 2(unplugin 터널): 플러그인 핸들이 connected target을 노출하지\n * 않아 라이브 page 목록을 알 수 없다. 거짓 빈 목록을 보여주느니 \"연결된 Pages\"\n * 섹션 자체를 숨긴다(#411). 정적 렌더와 SSE 갱신 양쪽에서 섹션이 사라진다.\n */\n pages: Array<{ id: string; url: string }> | null;\n /** 마지막으로 생성된 attachUrl (없으면 null). TOTP at= 코드는 이 안에 캡슐화. */\n attachUrl: string | null;\n /**\n * 현재 세션의 Chii 인스펙터 URL — 살아있는 세션 기준 DevTools 진입점 (#503).\n *\n * - `string` — relay up + 페이지 attached → `buildChiiInspectorUrl`로 조립된 URL.\n * TOTP at= 코드는 이 URL 안에 캡슐화. 대시보드 HTML 내 렌더는 의도된 transport.\n * - `null` — relay up이지만 페이지 미첨부, 또는 relay down, 또는 env 1(mock).\n *\n * SECRET-HANDLING: 이 URL은 relay host + TOTP at= 코드를 담을 수 있다.\n * 대시보드 HTML 본문에 렌더되는 건 의도된 transport(attachUrl과 동일 취급)이지만,\n * stdout/stderr/로그/에러 메시지에는 절대 출력하지 않는다.\n */\n inspectorUrl?: string | null;\n /**\n * 현재 세션 환경 — /attach 스캔 절차·체크리스트 카피 분기 + 상단 환경 라벨 (#468).\n *\n * - `'relay-mobile'` → sandbox family (환경 2: launcher PWA 절차, 토스 앱·_deploymentId 없음)\n * - `'relay-dev'` → intoss family (환경 3: 토스 앱 deep-link 절차)\n * - `'relay-live'` → intoss family + LIVE read-only 한 줄 (환경 4)\n * - `'mock'` / 미지정 → intoss family, 환경 라벨 없음 (환경 1은 /attach 표면이\n * 없어 사실상 도달 불가 — legacy 카피 유지 fallback)\n *\n * 호출처는 자기 mode를 명시적으로 전달한다: debug-server는 active connection에서\n * `deriveEnvironment(...)`로 파생, unplugin tunnel 대시보드는 `'relay-mobile'` 고정.\n */\n mode?: McpEnvironment;\n}\n\n/** mode → 어느 precompiled attach chrome family를 쓰는가 (#468). */\nfunction attachFamilyForMode(mode: McpEnvironment | undefined): AttachChromeFamily {\n return mode === 'relay-mobile' ? 'sandbox' : 'intoss';\n}\n\n/**\n * mode → 페이지 상단 환경 라벨 HTML (`__MODE_LABEL__` 토큰 채움, #468).\n * 사용자가 fidelity 사다리의 어느 겹에 있는지 즉시 알게 하는 환경 가시화 배지.\n * mode 미지정/'mock'은 빈 문자열 — 알 수 없는 환경을 거짓으로 라벨링하지 않는다.\n */\nfunction buildModeLabel(\n mode: McpEnvironment | undefined,\n s: ReturnType<typeof resolveLocaleStrings>,\n): string {\n let label: string;\n switch (mode) {\n case 'relay-mobile':\n label = s('attach.mode.sandbox');\n break;\n case 'relay-dev':\n label = s('attach.mode.intossDev');\n break;\n case 'relay-live':\n label = s('attach.mode.intossLive');\n break;\n case 'mock':\n case undefined:\n return '';\n }\n return `<p class=\"mode-label\">${escapeHtml(label)}</p>`;\n}\n\nexport interface QrHttpServer {\n port: number;\n /** `http://127.0.0.1:<port>/attach?u=<encoded>` URL 생성 헬퍼. */\n buildAttachPageUrl(attachUrl: string): string;\n /**\n * 안정 인스펙터 진입점 URL — `http://127.0.0.1:<port>/inspector` (issue #530).\n * 클릭 시점에 TOTP를 mint하고 302 redirect하므로 URL 자체에 시크릿이 없다.\n * 대시보드/stdout/로그 어디든 출력 가능.\n */\n readonly inspectorStableUrl: string;\n /**\n * 상태 변경 시 호출 — SSE 구독자에게 최신 상태를 push한다.\n * `getDashboardState`가 주입돼 있지 않으면 no-op.\n */\n notifyStateChange(): void;\n close(): Promise<void>;\n}\n\n/** HTML 특수문자를 이스케이프한다. */\nfunction escapeHtml(s: string): string {\n return s.replace(/[<>&\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n}\n\n/**\n * 현재 path+query에서 lang 파라미터만 교체한 ko/en 토글 링크를 생성한다.\n *\n * SECRET-HANDLING: u= (attachUrl, TOTP at= 캡슐 포함) 등 기존 query를 보존한다.\n * lang= 만 덮어쓴다. 링크 href에 at= 코드가 들어가는 건 의도된 전달 경로.\n */\nfunction buildLangSwitcher(\n path: string,\n existingParams: URLSearchParams,\n locale: 'ko' | 'en',\n s: ReturnType<typeof resolveLocaleStrings>,\n): string {\n function switcherHref(targetLang: 'ko' | 'en'): string {\n const p = new URLSearchParams(existingParams);\n p.set('lang', targetLang);\n return `${escapeHtml(path)}?${p.toString()}`;\n }\n const koLabel = escapeHtml(s('dashboard.lang.ko'));\n const enLabel = escapeHtml(s('dashboard.lang.en'));\n const koClass = locale === 'ko' ? 'active' : '';\n const enClass = locale === 'en' ? 'active' : '';\n return `<div class=\"lang-switcher\"><a href=\"${switcherHref('ko')}\" class=\"${koClass}\">${koLabel}</a><a href=\"${switcherHref('en')}\" class=\"${enClass}\">${enLabel}</a></div>`;\n}\n\n/**\n * Dashboard HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.\n *\n * 토큰 채우기 순서:\n * 1. chrome string(locale별 precompile)을 가져온다.\n * 2. 동적 부분을 단순 replaceAll로 채운다 (토큰이 HTML context 밖에 있으므로 안전).\n * 3. inline SSE <script>를 </body> 직전에 주입한다.\n *\n * 동적 파트 분류:\n * - \"token-fill\": 단일 값 교체 (__NOW__, __TUNNEL_CLASS__, __TUNNEL_STATUS__,\n * __ATTACH_SECTION__, __INSPECTOR_SECTION__)\n * - \"runtime builder\": 가변 길이 구조 (__PAGES_SECTION__ — 조건부 렌더 + 가변 rows)\n * - \"suffix\": inline SSE <script> (빌드 파이프라인 없는 클라이언트 스크립트, locale\n * aware 문자열 포함)\n *\n * SECRET-HANDLING:\n * - attachUrl은 url-box 안에서만 노출 (TOTP at= 코드 캡슐 그대로).\n * - inspectorUrl은 anchor href 안에서만 노출 (TOTP at= 코드 캡슐 그대로).\n * relay host + TOTP 코드가 담길 수 있으나 대시보드 HTML은 의도된 transport.\n * - tunnel wssUrl은 \"터널 연결됨\" 상태 표시에서 UP/DOWN만 노출.\n * wssUrl 값 자체는 dashboard HTML에 넣지 않는다.\n */\nfunction buildDashboardHtml(\n state: DashboardState,\n qrDataUrl: string | null,\n locale: 'ko' | 'en',\n path = '/',\n params = new URLSearchParams(),\n): string {\n const s = resolveLocaleStrings(locale);\n const now = new Date().toISOString();\n\n const tunnelStatus = state.tunnel.up ? s('dashboard.tunnel.up') : s('dashboard.tunnel.down');\n const tunnelClass = state.tunnel.up ? 'status-up' : 'status-down';\n\n // attachSection: QR img + url-row(url-box + 복사 버튼), or hint.\n // dashboard 표면에서 SSE 재렌더 시에도 동일 구조를 유지해 복사 버튼이 생존한다.\n let attachSection: string;\n if (qrDataUrl && state.attachUrl) {\n const safeAttachUrl = escapeHtml(state.attachUrl);\n const copyLabel = escapeHtml(s('dashboard.url.copy'));\n attachSection =\n `<img class=\"qr\" src=\"${qrDataUrl}\" alt=\"attach QR\" />` +\n `<div class=\"url-row\">` +\n `<p class=\"url-box\" id=\"url-box\">${safeAttachUrl}</p>` +\n `<button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"${copyLabel}\">${copyLabel}</button>` +\n `</div>`;\n } else {\n attachSection = `<p class=\"hint\">${escapeHtml(s('dashboard.attach.hint'))}</p>`;\n }\n\n // inspectorSection — 인스펙터 열기 링크 (#503, gate 보정 #544).\n // 게이트: pages.length > 0 (페이지가 attach돼 있을 때만 활성).\n // #530 이후 inspectorUrl은 안정 /inspector URL이라 항상 non-null이지만,\n // 미attach 상태에서 버튼을 클릭하면 502 noTarget을 반환할 뿐이다.\n // 게이트를 pages.length > 0으로 바꿔 미attach 시 대기 힌트를 표시하고\n // /과 /attach 양쪽에서 동일한 동작을 보장한다.\n // SECRET-HANDLING: inspectorUrl에 relay host + TOTP at= 코드가 담길 수 있으나\n // 대시보드 HTML 본문 렌더는 의도된 transport — 단 stdout/로그로 출력 금지.\n const pagesAttached = Array.isArray(state.pages) && state.pages.length > 0;\n let inspectorSection: string;\n if (pagesAttached && state.inspectorUrl) {\n const safeUrl = escapeHtml(state.inspectorUrl);\n const label = escapeHtml(s('dashboard.inspector.open'));\n inspectorSection = `<a class=\"inspector-link\" id=\"inspector-link\" href=\"${safeUrl}\" target=\"_blank\" rel=\"noopener noreferrer\">${label}</a>`;\n } else {\n const hint = escapeHtml(s('dashboard.inspector.waiting'));\n inspectorSection = `<span class=\"inspector-hint\" id=\"inspector-link\">${hint}</span>`;\n }\n\n // pagesSection — \"연결된 Pages\" 섹션: env 3/4(pages: Array)에서만 렌더한다.\n // env 2(pages: null)는 라이브 page 목록을 알 수 없어 섹션 자체를 숨긴다(#411).\n // runtime builder: 조건부 블록 + 가변 row 목록이라 token-fill로는 불충분.\n const pagesSection =\n state.pages === null\n ? ''\n : `<hr /><section id=\"pages-section\"><h2>${escapeHtml(s('dashboard.pages.section'))}</h2><ul id=\"pages-list\">${\n state.pages.length > 0\n ? state.pages\n .map((p) => {\n const safeId = escapeHtml(p.id);\n const safeUrl = escapeHtml(p.url.slice(0, 120));\n return `<li><span class=\"page-id\">${safeId}</span> <span class=\"page-url\">${safeUrl}</span></li>`;\n })\n .join('\\n')\n : `<li class=\"empty\">${escapeHtml(s('dashboard.pages.empty'))}</li>`\n }</ul></section>`;\n\n // locale-aware strings for the inline SSE client script\n const sseStrings: SseScriptStrings = {\n tunnelUp: JSON.stringify(s('dashboard.tunnel.up')),\n tunnelDown: JSON.stringify(s('dashboard.tunnel.down')),\n pagesEmpty: JSON.stringify(s('dashboard.pages.empty')),\n attachHint: JSON.stringify(s('dashboard.attach.hint')),\n copyLabel: JSON.stringify(s('dashboard.url.copy')),\n copiedLabel: JSON.stringify(s('dashboard.url.copied')),\n inspectorOpenLabel: JSON.stringify(s('dashboard.inspector.open')),\n inspectorWaitingLabel: JSON.stringify(s('dashboard.inspector.waiting')),\n dashboardSurface: true,\n };\n\n const langSwitcher = buildLangSwitcher(path, params, locale, s);\n\n // Fill token placeholders in the precompiled chrome.\n // replaceAll is safe because these __TOKEN__ strings cannot appear in\n // any legitimate user-facing value (they are sentinel strings).\n const chrome = dashboardChromeByLocale[locale];\n const filled = chrome\n .replaceAll('__LANG_SWITCHER__', langSwitcher)\n .replaceAll('__NOW__', escapeHtml(now))\n .replaceAll('__TUNNEL_CLASS__', tunnelClass)\n .replaceAll('__TUNNEL_STATUS__', escapeHtml(tunnelStatus))\n .replaceAll('__ATTACH_SECTION__', attachSection)\n .replaceAll('__INSPECTOR_SECTION__', inspectorSection)\n .replaceAll('__PAGES_SECTION__', pagesSection);\n\n // Append the inline SSE <script> suffix directly before </body>.\n // This keeps the client script out of the precompiled chrome (it references\n // locale-aware strings resolved per-request) while staying self-contained.\n const sseScript = buildSseScript(sseStrings);\n return filled.replace('</body>', `${sseScript}\\n</body>`);\n}\n\ninterface SseScriptStrings {\n tunnelUp: string;\n tunnelDown: string;\n pagesEmpty: string;\n attachHint: string;\n /** 복사 버튼 기본 라벨 (JSON.stringify로 이미 escape됨). */\n copyLabel: string;\n /** 복사 완료 피드백 라벨 (JSON.stringify로 이미 escape됨). */\n copiedLabel: string;\n /** \"인스펙터 열기\" 링크 라벨 (JSON.stringify로 이미 escape됨, #503). */\n inspectorOpenLabel: string;\n /** 인스펙터 URL 대기 힌트 (JSON.stringify로 이미 escape됨, #503). */\n inspectorWaitingLabel: string;\n /**\n * true: dashboard 표면 — `#attach-section` innerHTML 전체 교체 방식 유지.\n * url-box 텍스트도 innerHTML 교체로 갱신됨.\n * false: /attach 표면 — img src만 교체, url-box는 `#url-box` textContent만 갱신.\n * 이 분기가 url-box 이중 표시 결함을 방지한다.\n */\n dashboardSurface: boolean;\n}\n\n/**\n * Inline SSE client <script> — injected into the dashboard HTML at runtime.\n *\n * Subscribes to /events and updates the DOM without a build pipeline.\n * client side: attachUrl은 DOM에 렌더링, wssUrl은 절대 렌더링하지 않는다.\n * pages === null 이면 섹션을 건드리지 않는다 (#411).\n *\n * 두 표면(dashboard / attach) 분기:\n * - dashboard (dashboardSurface=true): #attach-section innerHTML 전체 교체 방식 유지.\n * url-box도 innerHTML 재렌더 안에 포함되어 갱신됨.\n * - /attach (dashboardSurface=false): #attach-section의 img src만 교체하고,\n * url-box는 #url-box textContent만 갱신한다. (#attach-section에 url-box가 없으므로\n * innerHTML 교체 시 url-box가 새로 생겨 이중 표시되는 결함을 방지 — #458 결함 수정.)\n *\n * 복사 기능: 이벤트 위임으로 document에 단일 핸들러. innerHTML 재렌더 후에도 생존.\n * - .url-box 클릭 또는 .copy-btn 클릭 → 현재 #url-box textContent 복사.\n * - clipboard: navigator.clipboard.writeText → 실패/부재 시 textarea execCommand fallback.\n * - 피드백: 버튼 라벨이 COPIED_LABEL로 ~1.5초 전환 후 COPY_LABEL로 복귀.\n *\n * 문자열 인자는 빌드타임에 ko/en 테이블에서 가져와 JSON.stringify로 이미 escape됨.\n *\n * SECRET-HANDLING: URL 값을 console.log 등으로 출력하지 않는다.\n */\nfunction buildSseScript(strings: SseScriptStrings): string {\n const isDashboard = strings.dashboardSurface;\n return `<script>\n // SSE — /events 구독해 상태 자동 갱신. 빌드 파이프라인 없는 인라인 스크립트.\n (function () {\n var TUNNEL_UP = ${strings.tunnelUp};\n var TUNNEL_DOWN = ${strings.tunnelDown};\n var PAGES_EMPTY = ${strings.pagesEmpty};\n var ATTACH_HINT = ${strings.attachHint};\n var COPY_LABEL = ${strings.copyLabel};\n var COPIED_LABEL = ${strings.copiedLabel};\n var INSPECTOR_OPEN_LABEL = ${strings.inspectorOpenLabel};\n var INSPECTOR_WAITING_LABEL = ${strings.inspectorWaitingLabel};\n\n // ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────\n function copyText(text) {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n return navigator.clipboard.writeText(text);\n }\n // fallback: textarea + execCommand\n return new Promise(function (resolve, reject) {\n var ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.focus();\n ta.select();\n try {\n document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed'));\n } catch (err) {\n reject(err);\n } finally {\n document.body.removeChild(ta);\n }\n });\n }\n\n // ── 복사 피드백 ───────────────────────────────────────────────────────\n var copyTimer = null;\n function triggerCopy() {\n var urlBox = document.getElementById('url-box');\n if (!urlBox) return;\n var text = urlBox.textContent || '';\n if (!text) return;\n copyText(text).then(function () {\n var btn = document.getElementById('copy-btn');\n if (btn) {\n btn.textContent = COPIED_LABEL;\n if (copyTimer) clearTimeout(copyTimer);\n copyTimer = setTimeout(function () {\n btn.textContent = COPY_LABEL;\n copyTimer = null;\n }, 1500);\n }\n }).catch(function () { /* 복사 실패 시 조용히 무시 */ });\n }\n\n // ── 이벤트 위임 — document 레벨에서 단일 핸들러 (innerHTML 재렌더 후에도 생존) ──\n document.addEventListener('click', function (e) {\n var target = e.target;\n if (!target) return;\n // .copy-btn 또는 .url-box 클릭 시 복사\n if (target.closest && (target.closest('.copy-btn') || target.closest('.url-box'))) {\n triggerCopy();\n }\n });\n\n // ── SSE 구독 ──────────────────────────────────────────────────────────\n var src = new EventSource('/events');\n src.onmessage = function (e) {\n try {\n var s = JSON.parse(e.data);\n // 터널 상태 갱신\n var el = document.getElementById('tunnel-status');\n if (el) {\n el.textContent = s.tunnel && s.tunnel.up ? TUNNEL_UP : TUNNEL_DOWN;\n el.className = 'status ' + (s.tunnel && s.tunnel.up ? 'status-up' : 'status-down');\n }\n // page 목록 갱신 — pages === null(env 2)이면 섹션 자체를 숨긴 채 둔다.\n // 정적 렌더가 #pages-section을 아예 안 그렸으므로 여기서도 손대지 않아\n // SSE push 때 섹션이 되살아나지 않는다(#411). 배열일 때만 목록을 채운다.\n if (s.pages !== null && s.pages !== undefined) {\n var ul = document.getElementById('pages-list');\n if (ul) {\n if (s.pages.length === 0) {\n ul.innerHTML = '<li class=\"empty\">' + PAGES_EMPTY + '</li>';\n } else {\n ul.innerHTML = s.pages.map(function (p) {\n var sid = String(p.id || '').slice(0, 36).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n var su = String(p.url || '').slice(0, 120).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n return '<li><span class=\"page-id\">' + sid + '</span> <span class=\"page-url\">' + su + '</span></li>';\n }).join('');\n }\n }\n }\n // attachUrl QR + url-box 갱신\n // SECRET-HANDLING: URL 값을 로그로 출력하지 않는다.\n var sec = document.getElementById('attach-section');\n if (sec) {\n if (s.attachUrl) {\n var encoded = encodeURIComponent(s.attachUrl);\n var safeUrl = String(s.attachUrl).slice(0, 2000).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n ${\n isDashboard\n ? `// dashboard: #attach-section innerHTML 전체 교체 (img + url-row).\n // url-box id=\"url-box\" 를 포함해 복사 핸들러가 계속 동작함.\n sec.innerHTML =\n '<img class=\"qr\" src=\"/qr.png?u=' + encoded + '\" alt=\"attach QR\" />' +\n '<div class=\\\\\"url-row\\\\\">' +\n '<p class=\\\\\"url-box\\\\\" id=\\\\\"url-box\\\\\">' + safeUrl + '</p>' +\n '<button class=\\\\\"copy-btn\\\\\" id=\\\\\"copy-btn\\\\\" type=\\\\\"button\\\\\" aria-label=\\\\\"' + COPY_LABEL + '\\\\\">' + COPY_LABEL + '</button>' +\n '</div>';`\n : `// /attach: img src만 교체 — url-box는 별도 #url-section에서 관리해 이중 표시 방지(#458).\n // QR img src 교체: img가 있으면 src만 갱신, 없으면 img 요소 생성.\n var img = sec.querySelector('img.qr');\n if (img) {\n img.src = '/qr.png?u=' + encoded;\n } else {\n sec.innerHTML = '<img class=\\\\\"qr\\\\\" src=\\\\\"/qr.png?u=' + encoded + '\\\\\" alt=\\\\\"attach QR\\\\\" />';\n }\n // url-box textContent만 갱신 (innerHTML 교체하지 않아 복사 버튼/핸들러 생존).\n var ub = document.getElementById('url-box');\n if (ub) ub.textContent = s.attachUrl;`\n }\n } else {\n ${\n isDashboard\n ? `sec.innerHTML = '<p class=\\\\\"hint\\\\\">' + ATTACH_HINT + '</p>';`\n : `// /attach에서 hint가 필요한 경우는 없으나 방어 처리.\n sec.innerHTML = '<p class=\\\\\"hint\\\\\">' + ATTACH_HINT + '</p>';`\n }\n }\n }\n // 인스펙터 링크 갱신 — #inspector-link (#503, gate 보정 #544).\n // 게이트: pages.length > 0 (페이지 attach 여부) — inspectorUrl 존재 여부가 아님.\n // #530 이후 inspectorUrl은 항상 안정 URL이므로 null 게이트는 사실상 항상 활성이었다.\n // pages.length > 0 으로 바꿔 미attach 시 대기 힌트를 보여주도록 수정.\n // SECRET-HANDLING: inspectorUrl을 console.log 등으로 출력하지 않는다.\n var insp = document.getElementById('inspector-link');\n if (insp) {\n var pagesAttachedSse = Array.isArray(s.pages) && s.pages.length > 0;\n if (pagesAttachedSse && s.inspectorUrl) {\n var safeInspUrl = String(s.inspectorUrl).slice(0, 2000).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n insp.outerHTML = '<a class=\\\\\"inspector-link\\\\\" id=\\\\\"inspector-link\\\\\" href=\\\\\"' + safeInspUrl + '\\\\\" target=\\\\\"_blank\\\\\" rel=\\\\\"noopener noreferrer\\\\\">' + INSPECTOR_OPEN_LABEL + '</a>';\n } else {\n insp.outerHTML = '<span class=\\\\\"inspector-hint\\\\\" id=\\\\\"inspector-link\\\\\">' + INSPECTOR_WAITING_LABEL + '</span>';\n }\n }\n // 갱신 시각 (dashboard만 #updated 요소 있음)\n var upd = document.getElementById('updated');\n if (upd) upd.textContent = upd.textContent.replace(/[^ ]+$/, new Date().toISOString());\n } catch (_) { /* 파싱 오류 무시 */ }\n };\n src.onerror = function () {\n // 재연결은 EventSource가 자동 처리 (spec 기본 동작).\n };\n })();\n </script>`;\n}\n\n/**\n * Attach 페이지 HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.\n *\n * 동적 파트:\n * - __QR_DATA_URL__ : base64 data URL (QR 이미지)\n * - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)\n * - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)\n * - __MODE_LABEL__ : 환경 배지 (`<p class=\"mode-label\">…</p>` 또는 빈 문자열, #468)\n * - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)\n * - __INSPECTOR_SECTION__ : \"디버그 툴 열기\" 버튼 또는 대기 힌트 (#544)\n *\n * mode-aware 분기 (#468): mode가 `relay-mobile`이면 sandbox family chrome(launcher\n * PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다. `relay-live`는\n * intoss chrome에 LIVE read-only 라인을 추가한다.\n *\n * SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이\n * `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#inspector-link`도 SSE push로\n * pages.length > 0 게이트에 따라 활성/비활성 전환된다 (#544).\n *\n * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.\n * inspectorStableUrl은 /inspector 안정 URL (127.0.0.1, 시크릿 없음) — 노출 가능.\n */\nfunction buildAttachHtml(\n qrDataUrl: string,\n safeLabel: string,\n safeAttachUrl: string,\n locale: 'ko' | 'en',\n path = '/attach',\n params = new URLSearchParams(),\n mode?: McpEnvironment,\n pagesAttached = false,\n inspectorStableUrl: string | null = null,\n): string {\n const s = resolveLocaleStrings(locale);\n const langSwitcher = buildLangSwitcher(path, params, locale, s);\n const family = attachFamilyForMode(mode);\n // 환경 4 전용 LIVE read-only 라인 — i18n 문자열은 신뢰된 빌드타임 카피(strong/code\n // 인라인 HTML 포함)라 verbatim 주입한다 (다른 FAQ 항목과 동일한 취급).\n const liveFaq = mode === 'relay-live' ? `<li>${s('attach.intoss.faq.liveReadOnly')}</li>` : '';\n\n // inspector 섹션 — pages.length > 0 게이트 (#544).\n // inspectorStableUrl은 /inspector 안정 URL (시크릿 없음) — href 노출 가능.\n let inspectorSection: string;\n if (pagesAttached && inspectorStableUrl) {\n const safeUrl = escapeHtml(inspectorStableUrl);\n const label = escapeHtml(s('dashboard.inspector.open'));\n inspectorSection = `<a class=\"inspector-link\" id=\"inspector-link\" href=\"${safeUrl}\" target=\"_blank\" rel=\"noopener noreferrer\">${label}</a>`;\n } else {\n const hint = escapeHtml(s('dashboard.inspector.waiting'));\n inspectorSection = `<span class=\"inspector-hint\" id=\"inspector-link\">${hint}</span>`;\n }\n\n const chrome = attachChromeByLocale[locale][family];\n const filled = chrome\n .replaceAll('__LANG_SWITCHER__', langSwitcher)\n .replaceAll('__MODE_LABEL__', buildModeLabel(mode, s))\n .replaceAll('__LIVE_FAQ__', liveFaq)\n .replaceAll('__QR_DATA_URL__', qrDataUrl)\n .replaceAll('__SAFE_LABEL__', safeLabel)\n .replaceAll('__SAFE_ATTACH_URL__', safeAttachUrl)\n .replaceAll('__INSPECTOR_SECTION__', inspectorSection);\n\n // Inject SSE script so QR auto-refreshes on each /events push,\n // and #inspector-link updates via pages.length > 0 gate on state change.\n // dashboardSurface: false → /attach 표면 분기 (img src 교체, url-box textContent만 갱신).\n const sseStrings: SseScriptStrings = {\n tunnelUp: JSON.stringify(s('dashboard.tunnel.up')),\n tunnelDown: JSON.stringify(s('dashboard.tunnel.down')),\n pagesEmpty: JSON.stringify(s('dashboard.pages.empty')),\n attachHint: JSON.stringify(s('dashboard.attach.hint')),\n copyLabel: JSON.stringify(s('dashboard.url.copy')),\n copiedLabel: JSON.stringify(s('dashboard.url.copied')),\n // /attach 페이지의 #inspector-link SSE 갱신에 쓰인다 (#544).\n inspectorOpenLabel: JSON.stringify(s('dashboard.inspector.open')),\n inspectorWaitingLabel: JSON.stringify(s('dashboard.inspector.waiting')),\n // /attach 표면: img src만 교체, #url-box textContent만 갱신 → url-box 이중 표시 방지(#458).\n dashboardSurface: false,\n };\n const sseScript = buildSseScript(sseStrings);\n return filled.replace('</body>', `${sseScript}\\n</body>`);\n}\n\nexport interface QrHttpServerOptions {\n /**\n * SSE 주기 갱신 간격 (ms). 기본값 90_000 (90초).\n *\n * SSE 구독자가 있는 동안 이 간격마다 `notifyStateChange()`와 동일한 push를 수행한다.\n * `getDashboardState()`가 호출 시점에 `at=` TOTP 코드를 재발급하므로, push 자체가\n * 열린 탭의 인스펙터 링크를 신선하게 유지한다. 90s 주기 < relay gate 허용창 ~3분\n * (±6 TOTP steps)이므로 탭이 열려 있는 한 링크가 항상 유효하다 (issue #509).\n *\n * 테스트에서 짧은 값(예: 50ms)을 주입해 검증한다. `undefined`이면 기본값 90_000.\n */\n sseRefreshIntervalMs?: number;\n /**\n * GET /inspector 라우트에서 클릭 시점 직접 인스펙터 URL을 조립하는 getter.\n *\n * getDashboardState().inspectorUrl(= /inspector 자기 자신)로 redirect하면 무한 루프가\n * 발생하므로, /inspector 라우트 내부는 이 getter로 직접 chii front_end URL을 조립한다.\n * 매 요청마다 호출되므로 TOTP를 요청 시점에 mint한다.\n *\n * - 미주입 → 기존 503 응답 유지.\n * - `ok: false, reason: 'relayDown'` → 502 (relay 미활성).\n * - `ok: false, reason: 'noTarget'` → 502 (relay up이지만 페이지 미attach).\n * - `ok: false, reason: 'totpUnavailable'` → 502 (TOTP secret 미설정, fail-closed).\n * - `ok: true` → 302 Location: url (Cache-Control: no-store).\n *\n * SECRET-HANDLING: ok:true 시 url 안에 relay host + TOTP at= 코드가 담긴다.\n * Location 헤더로 전달되는 건 의도된 transport. 로그/stdout 출력 금지.\n */\n getDirectInspectorUrl?: () =>\n | { ok: true; url: string }\n | { ok: false; reason: 'relayDown' | 'noTarget' | 'totpUnavailable' };\n}\n\n/**\n * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.\n * MCP debug server 생애주기에 묶어 사용 — `runDebugServer` shutdown 시 `close()`로 정리.\n *\n * @param getDashboardState - dashboard 상태를 반환하는 클로저. 주입 시 `GET /` dashboard와\n * `GET /events` SSE 스트림이 활성화된다. 미주입 시 두 라우트는 204/서비스 없음으로 응답.\n * @param options - 서버 옵션. `sseRefreshIntervalMs`로 idle 탭 TOTP 만료 방지 주기를 조정.\n * `getDirectInspectorUrl`로 /inspector 라우트에서 직접 조립 URL을 제공해 redirect 루프를 방지.\n */\nexport async function startQrHttpServer(\n getDashboardState?: () => DashboardState,\n options?: QrHttpServerOptions,\n): Promise<QrHttpServer> {\n const { default: QRCode } = await import('qrcode');\n\n /** SSE 활성 연결 목록 — `notifyStateChange()` 시 전체 push. */\n const sseClients: ServerResponse[] = [];\n\n /** SSE 연결 하나에 상태 이벤트를 flush한다. */\n function pushStateToClient(res: ServerResponse, state: DashboardState): void {\n const payload = JSON.stringify({\n tunnel: { up: state.tunnel.up, wssUrl: state.tunnel.wssUrl },\n pages: state.pages,\n // attachUrl은 캡슐 그대로 전달 — TOTP at= 코드 분리 없음 (의도된 설계).\n attachUrl: state.attachUrl,\n // inspectorUrl: relay + 페이지 attached 시 살아있는 인스펙터 URL (#503).\n // SECRET-HANDLING: URL(relay host + TOTP at=)은 SSE payload 전달이 의도된 transport.\n // 단 stdout/로그/에러에는 절대 출력하지 않는다.\n inspectorUrl: state.inspectorUrl ?? null,\n });\n // SSE frame: \"data: <json>\\n\\n\"\n res.write(`data: ${payload}\\n\\n`);\n }\n\n const server: Server = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const rawUrl = req.url ?? '/';\n const [path, query = ''] = rawUrl.split('?', 2) as [string, string | undefined];\n const params = new URLSearchParams(query ?? '');\n\n // per-request locale — ?lang= query param이 있으면 우선 적용, 없으면 Accept-Language header에서 결정.\n const langParam = params.get('lang');\n const locale =\n langParam === 'ko' || langParam === 'en'\n ? langParam\n : parseAcceptLanguage(req.headers['accept-language']);\n\n // ── GET / — dashboard 루트 ─────────────────────────────────────────────\n if (path === '/') {\n if (!getDashboardState) {\n res.writeHead(204, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end();\n return;\n }\n const state = getDashboardState();\n let qrDataUrl: string | null = null;\n if (state.attachUrl) {\n try {\n qrDataUrl = await QRCode.toDataURL(state.attachUrl, {\n type: 'image/png',\n errorCorrectionLevel: 'M',\n });\n } catch {\n // QR 생성 실패 시 null 유지 — dashboard는 텍스트 fallback 표시\n }\n }\n const html = buildDashboardHtml(state, qrDataUrl, locale, path, params);\n res.writeHead(200, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n });\n res.end(html);\n return;\n }\n\n // ── GET /events — SSE 스트림 ──────────────────────────────────────────\n if (path === '/events') {\n if (!getDashboardState) {\n res.writeHead(204, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end();\n return;\n }\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n // 즉시 현재 상태를 한 번 push — 페이지 로드 시 최신 상태 보장.\n const initialState = getDashboardState();\n pushStateToClient(res, initialState);\n\n sseClients.push(res);\n\n // 연결 끊기면 목록에서 제거.\n req.once('close', () => {\n const idx = sseClients.indexOf(res);\n if (idx !== -1) sseClients.splice(idx, 1);\n });\n return;\n }\n\n if (path === '/attach') {\n const encodedU = params.get('u') ?? '';\n let attachUrl: string;\n try {\n attachUrl = decodeURIComponent(encodedU);\n } catch {\n res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('잘못된 u 파라미터입니다.');\n return;\n }\n\n // deploymentId 라벨 — attachUrl에서 _deploymentId 파라미터만 추출 (at= 노출 방지).\n let deploymentIdLabel = 'attach';\n try {\n const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);\n if (dpMatch?.[1]) {\n deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);\n }\n } catch {\n // best-effort\n }\n\n // 현재 세션 mode + pages 상태 — 카피 분기(#468), inspector 게이트(#544).\n // getDashboardState 미주입(legacy) 시 undefined → intoss family + 환경 라벨 없음 fallback.\n const currentState = getDashboardState?.();\n const mode = currentState?.mode;\n const pagesAttached =\n Array.isArray(currentState?.pages) && (currentState?.pages.length ?? 0) > 0;\n // inspectorStableUrl: /inspector 안정 URL (시크릿 없음) — getDirectInspectorUrl 주입 시만 활성.\n // 서버 주소는 listen 후에만 확정되므로 server.address()로 런타임에 읽는다.\n // (요청은 listen 완료 후 들어오므로 address()는 항상 non-null이다.)\n const inspectorStableUrlForAttach: string | null = (() => {\n if (!options?.getDirectInspectorUrl) return null;\n const addr = server.address();\n if (!addr || typeof addr === 'string') return null;\n return `http://127.0.0.1:${addr.port}/inspector`;\n })();\n\n // QR을 base64 data URL로 인라인 생성 — 외부 fetch 없이 self-contained HTML.\n QRCode.toDataURL(attachUrl, { type: 'image/png', errorCorrectionLevel: 'M' })\n .then((dataUrl: string) => {\n const safeLabel = escapeHtml(deploymentIdLabel);\n const safeAttachUrl = escapeHtml(attachUrl);\n const html = buildAttachHtml(\n dataUrl,\n safeLabel,\n safeAttachUrl,\n locale,\n path,\n params,\n mode,\n pagesAttached,\n inspectorStableUrlForAttach,\n );\n res.writeHead(200, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n });\n res.end(html);\n })\n .catch(() => {\n res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('QR 생성에 실패했습니다.');\n });\n return;\n }\n\n // ── GET /inspector — 안정 인스펙터 진입점 (issue #530) ───────────────────\n // 클릭 시점에 getDirectInspectorUrl()로 직접 chii front_end URL을 조립해 302 redirect.\n // getDashboardState().inspectorUrl(= /inspector 자기 자신)을 쓰면 무한 루프 → 분리됨.\n // SECRET-HANDLING: redirect Location(relay host + at=)은 HTTP 응답으로만 전달.\n // 로그에 Location 값 출력 금지.\n if (path === '/inspector') {\n const getDirectInspectorUrl = options?.getDirectInspectorUrl;\n if (!getDirectInspectorUrl) {\n res.writeHead(503, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('Inspector endpoint is not available in this server mode.');\n return;\n }\n // 매 요청마다 getter 호출 — TOTP를 요청 시점에 mint.\n const result = getDirectInspectorUrl();\n const s = resolveLocaleStrings(locale);\n if (!result.ok) {\n const msgKey =\n result.reason === 'noTarget' ? 'inspector.error.noTarget' : 'inspector.error.relayDown';\n const msg = s(msgKey);\n const body =\n `<!DOCTYPE html><html lang=\"${locale}\"><head>` +\n `<meta charset=\"utf-8\"><title>Inspector</title></head><body>` +\n `<p>${escapeHtml(msg)}</p>` +\n `<p style=\"font-size:0.9em;color:#666\">` +\n (locale === 'ko'\n ? '(<a href=\"/\">대시보드로 돌아가기</a>)'\n : '(<a href=\"/\">Back to dashboard</a>)') +\n `</p></body></html>`;\n res.writeHead(502, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n });\n res.end(body);\n return;\n }\n // ok: true — 302 redirect. Location에 relay host + TOTP at= 포함.\n // SECRET-HANDLING: Location 값은 HTTP 응답으로만 — 로그/stdout 출력 금지.\n res.writeHead(302, {\n Location: result.url,\n 'Cache-Control': 'no-store',\n });\n res.end();\n return;\n }\n\n if (path === '/qr.png') {\n const encodedU = params.get('u') ?? '';\n let attachUrl: string;\n try {\n attachUrl = decodeURIComponent(encodedU);\n } catch {\n res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('잘못된 u 파라미터입니다.');\n return;\n }\n\n QRCode.toBuffer(attachUrl, { type: 'png', errorCorrectionLevel: 'M' })\n .then((buf: Buffer) => {\n res.writeHead(200, {\n 'Content-Type': 'image/png',\n 'Cache-Control': 'no-store',\n 'Content-Length': String(buf.length),\n });\n res.end(buf);\n })\n .catch(() => {\n res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('QR PNG 생성에 실패했습니다.');\n });\n return;\n }\n\n res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('Not Found');\n });\n\n const listenPort = Number(process.env.AIT_DEBUG_HTTP_PORT ?? 0);\n\n await new Promise<void>((resolve, reject) => {\n server.listen(listenPort, '127.0.0.1', () => resolve());\n server.once('error', reject);\n });\n\n const address = server.address();\n if (!address || typeof address === 'string') {\n throw new Error('qr-http-server: server.address()가 예상하지 못한 형태입니다.');\n }\n const port = address.port;\n\n /** idle 탭 TOTP 만료 방지용 주기 SSE 갱신 interval. */\n function notifyStateChangeInternal(): void {\n if (!getDashboardState) return;\n const state = getDashboardState();\n for (const client of sseClients) {\n try {\n pushStateToClient(client, state);\n } catch {\n // 연결이 이미 끊어진 경우 — 무시 (close 핸들러가 목록에서 제거함).\n }\n }\n }\n\n // 주기 SSE 갱신 — getDashboardState() 호출 시점에 TOTP at=가 재발급되므로\n // push 자체가 열린 탭의 인스펙터 링크를 신선하게 유지한다 (issue #509).\n // .unref()로 프로세스 종료를 막지 않는다.\n const refreshIntervalMs = options?.sseRefreshIntervalMs ?? 90_000;\n const refreshHandle = setInterval(() => {\n if (sseClients.length > 0 && getDashboardState) {\n notifyStateChangeInternal();\n }\n }, refreshIntervalMs).unref();\n\n return {\n port,\n buildAttachPageUrl(_attachUrl: string): string {\n // 사용자 대면 URL을 루트 `/`로 수렴 (#595).\n // 같은 데몬이 attachUrl을 이미 server-state(getDashboardState)로 보유하므로\n // `/attach?u=<encoded>` 쿼리는 redundant하다.\n // SECRET-HANDLING: 브라우저에 열리는 URL에서 tunnel host·relay wss·TOTP at= 제거.\n // /attach?u= 라우트 자체는 back-compat으로 유지(기존 인쇄된 링크 보호).\n return `http://127.0.0.1:${port}/`;\n },\n // 안정 인스펙터 진입점 URL (issue #530) — 클릭 시 302 redirect (TOTP 클릭 시점 mint).\n // URL 자체에 시크릿 없음 → 대시보드/stdout/로그 어디든 출력 가능.\n get inspectorStableUrl(): string {\n return `http://127.0.0.1:${port}/inspector`;\n },\n notifyStateChange(): void {\n notifyStateChangeInternal();\n },\n close(): Promise<void> {\n clearInterval(refreshHandle);\n return new Promise((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n },\n };\n}\n","/**\n * Single debug session lock for the `devtools-mcp` debug server.\n *\n * At most one debug server process should run on a given machine at a time —\n * multiple concurrent instances create duplicate cloudflared tunnels, waste\n * resources, and confuse the user about which wssUrl to use.\n *\n * ## Lock file\n *\n * Location: `~/.ait-devtools/server.lock`\n *\n * Schema (JSON):\n * ```json\n * { \"pid\": 12345, \"wssUrl\": \"wss://xxx.trycloudflare.com\", \"startedAt\": \"2026-01-01T00:00:00.000Z\" }\n * ```\n *\n * ## Behaviour\n *\n * - **Acquire**: write PID + wssUrl + startedAt. Returns a `release()` handle.\n * - **Stale lock recovery**: if the stored PID is no longer alive\n * (`process.kill(pid, 0)` throws ESRCH), the lock is silently replaced.\n * - **Live conflict (option B)**: if the stored PID is alive, `acquireLock`\n * throws `ServerLockConflictError` with the existing PID and wssUrl so the\n * caller can surface a clear message to the agent.\n * - **Release**: remove the lock file. Called on graceful shutdown (SIGINT /\n * SIGTERM / SIGHUP). SIGKILL survivors leave a stale file — the next startup\n * recovers it automatically via the alive check.\n *\n * ## wssUrl update\n *\n * The lock is written before cloudflared starts, so `wssUrl` begins as `null`\n * and is updated in place once the tunnel URL is known via `updateWssUrl`.\n *\n * Node-only.\n */\n\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { isPidAlive as _isPidAlive } from '../shared/parent-watcher.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface LockData {\n pid: number;\n /** `null` until the cloudflared tunnel URL is assigned. */\n wssUrl: string | null;\n startedAt: string;\n /**\n * PID of the cloudflared child process. Written once the tunnel is up via\n * `LockHandle.updateTunnelChildPid`. Absent in lock files written by older\n * versions — those fall back to PID-only stale detection.\n *\n * FIX 3 (issue #571): `acquireLock` treats a live holder whose tunnel child\n * is known-dead as a stale lock and reclaims it.\n */\n tunnelChildPid?: number | null;\n}\n\nexport interface LockHandle {\n /** Updates the wssUrl field in the lock file once the tunnel URL is known. */\n updateWssUrl(wssUrl: string): void;\n /**\n * Updates the cloudflared child PID in the lock file once the tunnel is up.\n *\n * FIX 3 (issue #571): a second `acquireLock` caller will see this PID and\n * can detect that the holder's tunnel child is dead even though the Node\n * process itself is still alive, allowing lock reclamation.\n */\n updateTunnelChildPid(pid: number): void;\n /** Removes the lock file. Idempotent — safe to call multiple times. */\n release(): void;\n}\n\n/** Thrown when a live server process already holds the lock. */\nexport class ServerLockConflictError extends Error {\n /** PID of the existing server process. */\n readonly existingPid: number;\n /** wssUrl from the existing lock — may be `null` if the tunnel is still starting. */\n readonly existingWssUrl: string | null;\n /** ISO timestamp from the existing lock — when that session started. */\n readonly existingStartedAt: string;\n\n constructor(existingPid: number, existingWssUrl: string | null, existingStartedAt: string) {\n const urlNote =\n existingWssUrl != null\n ? ` relay URL: ${existingWssUrl}\\n`\n : ' relay URL: (tunnel still starting — retry in a moment)\\n';\n\n super(\n `A debug server is already running (PID ${existingPid}).\\n` +\n urlNote +\n 'Stop the existing session before starting a new one.\\n' +\n 'If it is already stopped but this error persists, remove the lock file:\\n' +\n ` rm \"${lockFilePath()}\"`,\n );\n this.name = 'ServerLockConflictError';\n this.existingPid = existingPid;\n this.existingWssUrl = existingWssUrl;\n this.existingStartedAt = existingStartedAt;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Paths\n// ---------------------------------------------------------------------------\n\n/** Returns `~/.ait-devtools/server.lock` (or `AIT_DEVTOOLS_LOCK_DIR` override for tests). */\nexport function lockFilePath(): string {\n const dir = process.env.AIT_DEVTOOLS_LOCK_DIR ?? join(homedir(), '.ait-devtools');\n return join(dir, 'server.lock');\n}\n\nfunction ensureLockDir(lockPath: string): void {\n const dir = join(lockPath, '..');\n mkdirSync(dir, { recursive: true });\n}\n\n// ---------------------------------------------------------------------------\n// PID alive check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when the given PID refers to a running process.\n *\n * Re-exported from `../shared/parent-watcher` so external callers that\n * import from `./server-lock` keep working without an import-path change.\n */\nexport const isPidAlive: (pid: number) => boolean = _isPidAlive;\n\n// ---------------------------------------------------------------------------\n// Read / write helpers\n// ---------------------------------------------------------------------------\n\nfunction readLock(lockPath: string): LockData | null {\n if (!existsSync(lockPath)) return null;\n try {\n const raw = readFileSync(lockPath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'pid' in parsed &&\n typeof (parsed as Record<string, unknown>).pid === 'number' &&\n 'startedAt' in parsed &&\n typeof (parsed as Record<string, unknown>).startedAt === 'string'\n ) {\n const p = parsed as Record<string, unknown>;\n // FIX 3: read optional tunnelChildPid — absent in lock files from older\n // versions; those fall back to PID-only stale detection.\n const tunnelChildPid = typeof p.tunnelChildPid === 'number' ? p.tunnelChildPid : null;\n return {\n pid: p.pid as number,\n wssUrl: typeof p.wssUrl === 'string' ? p.wssUrl : null,\n startedAt: p.startedAt as string,\n tunnelChildPid,\n };\n }\n // Unrecognised schema — treat as stale.\n return null;\n } catch {\n // Corrupt / unreadable — treat as stale.\n return null;\n }\n}\n\nfunction writeLock(lockPath: string, data: LockData): void {\n ensureLockDir(lockPath);\n writeFileSync(lockPath, JSON.stringify(data, null, 2), { encoding: 'utf8' });\n}\n\nfunction removeLock(lockPath: string): void {\n try {\n rmSync(lockPath);\n } catch {\n // Already removed — fine.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Force-takeover helper\n// ---------------------------------------------------------------------------\n\n/**\n * Sends SIGTERM to `pid` and waits up to `graceMs` (default 2 000 ms) for it\n * to exit; then falls back to SIGKILL. Synchronous — uses a busy-wait loop so\n * it is usable in the top-level startup path without async plumbing.\n *\n * Ignores errors from `process.kill` so that a race where the target exits\n * between the alive check and the kill call does not crash the caller.\n */\nfunction killAndWait(pid: number, graceMs = 2_000): void {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone — nothing to do.\n return;\n }\n\n const deadline = Date.now() + graceMs;\n // Poll every 100 ms until the process is gone or the grace period expires.\n while (isPidAlive(pid) && Date.now() < deadline) {\n // Busy-wait: this is a very short window (≤2 s) at startup.\n const end = Date.now() + 100;\n while (Date.now() < end) {\n // spin\n }\n }\n\n if (isPidAlive(pid)) {\n try {\n process.kill(pid, 'SIGKILL');\n } catch {\n // Already gone.\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Reads the current lock file without acquiring it. Returns the parsed\n * `LockData` when the file exists and is valid, otherwise `null`. Used by\n * `get_debug_status` to surface the `serverLockHolder` field without\n * interfering with the running lock owner.\n */\nexport function readServerLock(): LockData | null {\n return readLock(lockFilePath());\n}\n\n/** Options for `acquireLock`. */\nexport interface AcquireLockOptions {\n /**\n * When `true`, terminates the process holding the existing lock (SIGTERM →\n * wait up to 2 s → SIGKILL) and takes over the lock.\n *\n * Corresponds to the `--force` / `--takeover` CLI flag.\n */\n force?: boolean;\n}\n\n/**\n * Attempts to acquire the server lock.\n *\n * - If no lock exists (or the lock is stale): writes a new lock and returns a\n * `LockHandle` with `updateWssUrl` + `release`.\n * - If a live process holds the lock and `force` is `false` (default): writes\n * a clear recovery message to stderr and throws `ServerLockConflictError`.\n * - If a live process holds the lock and `force` is `true`: sends SIGTERM to\n * that process (waiting up to 2 s then SIGKILL) and takes over the lock.\n *\n * The initial `wssUrl` in the lock file is `null` — call\n * `handle.updateWssUrl(url)` once the cloudflared tunnel is ready.\n */\nexport function acquireLock(options: AcquireLockOptions = {}): LockHandle {\n const { force = false } = options;\n const lockPath = lockFilePath();\n const existing = readLock(lockPath);\n\n if (existing !== null) {\n if (isPidAlive(existing.pid)) {\n // FIX 3 (issue #571): even if the Node process is alive, check whether\n // its cloudflared child has died. A zombie daemon whose tunnel is dead\n // is effectively stale — reclaim the lock without waiting for the user\n // to manually kill the process.\n const tunnelChildPid = existing.tunnelChildPid;\n const tunnelChildDead = typeof tunnelChildPid === 'number' && !isPidAlive(tunnelChildPid);\n\n if (tunnelChildDead) {\n process.stderr.write(\n `[ait-debug] stale lock: holder PID=${existing.pid} alive but tunnel child PID=${tunnelChildPid} is dead — reclaiming lock.\\n`,\n );\n // Fall through to write a fresh lock.\n } else if (force) {\n // Force takeover: SIGTERM → 2 s grace → SIGKILL.\n process.stderr.write(\n `[ait-debug] --force: terminating existing session PID=${existing.pid} …\\n`,\n );\n killAndWait(existing.pid);\n process.stderr.write(`[ait-debug] --force: PID=${existing.pid} stopped, taking over.\\n`);\n } else {\n // Emit a user-actionable message before throwing so the MCP host can\n // surface it — the thrown message is included in the \"process exited\"\n // log, but the stderr line is more prominent and machine-parseable.\n const urlPart =\n existing.wssUrl != null ? `wssUrl=${existing.wssUrl}` : 'wssUrl=(tunnel starting)';\n process.stderr.write(\n `[ait-debug] 기존 debug-mode 세션이 이미 실행 중 — PID=${existing.pid}, started ${existing.startedAt}, ${urlPart}\\n` +\n `[ait-debug] 회복: \\`kill ${existing.pid}\\` 또는 \\`npx @ait-co/devtools devtools-mcp --force\\`\\n`,\n );\n throw new ServerLockConflictError(existing.pid, existing.wssUrl, existing.startedAt);\n }\n } else {\n // Stale lock — previous process died without cleanup.\n process.stderr.write(\n `[ait-debug] stale lock from PID ${existing.pid} recovered — starting fresh.\\n`,\n );\n }\n }\n\n const data: LockData = {\n pid: process.pid,\n wssUrl: null,\n startedAt: new Date().toISOString(),\n };\n writeLock(lockPath, data);\n\n let released = false;\n\n return {\n updateWssUrl(wssUrl: string): void {\n if (released) return;\n data.wssUrl = wssUrl;\n writeLock(lockPath, data);\n },\n updateTunnelChildPid(pid: number): void {\n if (released) return;\n data.tunnelChildPid = pid;\n writeLock(lockPath, data);\n },\n release(): void {\n if (released) return;\n released = true;\n removeLock(lockPath);\n },\n };\n}\n","/**\n * call_sdk 인자 시그니처 레지스트리\n *\n * 잘 알려진 SDK 메서드의 인자 schema를 수동으로 등록한다.\n * 목적: 잘못된 인자가 native bridge에 도달하기 전에 MCP 레이어에서 reject하여\n * 토스 앱 crash(Swift/Kotlin 측에서 `.type` 등을 undefined로 읽는 경우)를 예방.\n *\n * 등록되지 않은 메서드는 passthrough — 알 수 없는 메서드에 대해 stderr 경고 1회.\n *\n * 시그니처 출처:\n * - `src/__typecheck.ts` — Original SDK 타입 호환성 검증\n * - `src/mock/navigation/index.ts` — mock 구현의 함수 시그니처\n * - `src/mock/device/` — device mock 시그니처\n *\n * 새 메서드 추가 방법:\n * 1. `src/__typecheck.ts` 또는 mock 구현에서 시그니처 확인\n * 2. 아래 SIGNATURES 배열에 `SdkSignature` 항목 추가\n * 3. `src/__tests__/call-sdk-validation.test.ts`에 ok + bad 케이스 추가\n */\n\n/** 단일 메서드에 대한 인자 검증 결과 */\nexport type ValidationResult = { ok: true } | { ok: false; expected: string; received: string };\n\n/** 등록된 SDK 메서드 시그니처 */\nexport interface SdkSignature {\n /** SDK 메서드 이름 (예: \"setDeviceOrientation\") */\n name: string;\n /**\n * 인자 배열을 검증하는 함수.\n * `args[0]` 등 필요한 인자를 `unknown` 타입으로 받아 type guard로 검증.\n */\n validateArgs(args: unknown[]): ValidationResult;\n /**\n * 에러 메시지에 포함할 올바른 호출 예시.\n * 예: `call_sdk('setDeviceOrientation', [{ type: 'landscape' }])`\n */\n example: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/* 헬퍼 — 공통 type guard */\n/* -------------------------------------------------------------------------- */\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction describeArgs(args: unknown[]): string {\n try {\n return JSON.stringify(args);\n } catch {\n return String(args);\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* 시그니처 레지스트리 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * 등록된 메서드 목록.\n *\n * 시그니처 출처 확인:\n * - 함수가 인자를 받지 않으면 args[0] 없음 → `args.length === 0`을 체크하지 않고\n * 그냥 통과시킨다(args 무시하는 stub가 많아서 noArgs 체크가 noise).\n * - 실 SDK 시그니처는 `src/__typecheck.ts`의 `Assert<Mock, Original>` 줄로 보장.\n */\nconst SIGNATURES: SdkSignature[] = [\n // --- setDeviceOrientation ---\n // 실 시그니처: setDeviceOrientation(options: { type: 'portrait' | 'landscape' }): Promise<void>\n // 출처: src/mock/navigation/index.ts:40 / src/__typecheck.ts:55\n {\n name: 'setDeviceOrientation',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg)) {\n return {\n ok: false,\n expected: \"{ type: 'portrait' | 'landscape' }\",\n received: describeArgs(args),\n };\n }\n const type = arg.type;\n if (type !== 'portrait' && type !== 'landscape') {\n return {\n ok: false,\n expected: \"{ type: 'portrait' | 'landscape' }\",\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setDeviceOrientation', [{ type: 'landscape' }])\",\n },\n\n // --- setIosSwipeGestureEnabled ---\n // 실 시그니처: setIosSwipeGestureEnabled(options: { isEnabled: boolean }): Promise<void>\n // 출처: src/mock/navigation/index.ts:32 / src/__typecheck.ts:51\n {\n name: 'setIosSwipeGestureEnabled',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.isEnabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ isEnabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setIosSwipeGestureEnabled', [{ isEnabled: false }])\",\n },\n\n // --- setSecureScreen ---\n // 실 시그니처: setSecureScreen(options: { enabled: boolean }): Promise<{ enabled: boolean }>\n // 출처: src/mock/navigation/index.ts:66 / src/__typecheck.ts:46\n {\n name: 'setSecureScreen',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.enabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ enabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setSecureScreen', [{ enabled: true }])\",\n },\n\n // --- setScreenAwakeMode ---\n // 실 시그니처: setScreenAwakeMode(options: { enabled: boolean }): Promise<{ enabled: boolean }>\n // 출처: src/mock/navigation/index.ts:57 / src/__typecheck.ts:47\n {\n name: 'setScreenAwakeMode',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.enabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ enabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setScreenAwakeMode', [{ enabled: true }])\",\n },\n\n // --- getOperationalEnvironment ---\n // 실 시그니처: getOperationalEnvironment(): 'toss' | 'sandbox'\n // 인자 없음 — args는 무시 (SDK 자체가 인자를 무시함)\n // 출처: src/mock/navigation/index.ts:88 / src/__typecheck.ts:62\n {\n name: 'getOperationalEnvironment',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getOperationalEnvironment', [])\",\n },\n\n // --- getPlatformOS ---\n // 실 시그니처: getPlatformOS(): 'ios' | 'android'\n // 출처: src/mock/navigation/index.ts:84 / src/__typecheck.ts:61\n {\n name: 'getPlatformOS',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getPlatformOS', [])\",\n },\n\n // --- getDeviceId ---\n // 실 시그니처: getDeviceId(): string\n // 출처: src/mock/navigation/index.ts:119 / src/__typecheck.ts:74\n {\n name: 'getDeviceId',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getDeviceId', [])\",\n },\n\n // --- getLocale ---\n // 실 시그니처: getLocale(): string\n // 출처: src/mock/navigation/index.ts:115 / src/__typecheck.ts:72\n {\n name: 'getLocale',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getLocale', [])\",\n },\n\n // --- getNetworkStatus ---\n // 실 시그니처: getNetworkStatus(): Promise<NetworkStatus>\n // 출처: src/mock/navigation/index.ts:127 / src/__typecheck.ts:73\n {\n name: 'getNetworkStatus',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getNetworkStatus', [])\",\n },\n\n // --- getSchemeUri ---\n // 실 시그니처: getSchemeUri(): string\n // 출처: src/mock/navigation/index.ts:111 / src/__typecheck.ts:71\n {\n name: 'getSchemeUri',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getSchemeUri', [])\",\n },\n\n // --- requestReview ---\n // 실 시그니처: requestReview(): Promise<void>\n // 출처: src/mock/navigation/index.ts:75 / src/__typecheck.ts:76\n {\n name: 'requestReview',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('requestReview', [])\",\n },\n\n // --- closeView ---\n // 실 시그니처: closeView(): Promise<void>\n // 출처: src/mock/navigation/index.ts:10 / src/__typecheck.ts:42\n {\n name: 'closeView',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('closeView', [])\",\n },\n];\n\n/* -------------------------------------------------------------------------- */\n/* 레지스트리 공개 API */\n/* -------------------------------------------------------------------------- */\n\nconst SIGNATURE_MAP = new Map<string, SdkSignature>(SIGNATURES.map((s) => [s.name, s]));\n\n/** 세션 내 passthrough 경고를 한 번만 emit하기 위한 Set */\nconst _warnedPassthrough = new Set<string>();\n\n/**\n * 메서드 이름으로 시그니처를 조회한다.\n * 등록된 메서드이면 `SdkSignature`를 반환하고, 미등록이면 `undefined`.\n */\nexport function lookupSignature(name: string): SdkSignature | undefined {\n return SIGNATURE_MAP.get(name);\n}\n\n/**\n * 미등록 메서드에 대해 stderr에 passthrough 경고를 1회 출력한다.\n * 세션 내 동일 메서드 이름은 최초 1회만 출력.\n */\nexport function warnPassthrough(name: string): void {\n if (_warnedPassthrough.has(name)) return;\n _warnedPassthrough.add(name);\n process.stderr.write(`[ait-debug] call_sdk: \"${name}\" 시그니처가 등록되지 않음 — passthrough\\n`);\n}\n\n/**\n * 테스트에서 passthrough 경고 Set을 초기화하기 위한 헬퍼.\n * 프로덕션 코드에서는 호출하지 않는다.\n */\nexport function _resetWarnedPassthroughForTest(): void {\n _warnedPassthrough.clear();\n}\n\n/**\n * 등록된 메서드 이름 목록 — tool description 생성 등에서 사용.\n */\nexport const REGISTERED_METHOD_NAMES: ReadonlyArray<string> = SIGNATURES.map((s) => s.name);\n","/**\n * Debug-mode MCP tools (Phase 1–3 + safe-area probe).\n *\n * Read-only tools that normalize CDP / AIT data into `chrome-devtools-mcp`-\n * compatible shapes. The tools never touch a websocket or HTTP endpoint\n * directly — they read from an injected `CdpConnection` (CDP events/commands)\n * or `AitSource` (AIT.* domain), which is what makes them unit-testable with a\n * fake. No phone and no running dev server are needed in tests.\n *\n * Phase 1 (CDP events):\n * - `list_console_messages` ← Runtime.consoleAPICalled\n * - `list_network_requests` ← Network.requestWillBeSent + responseReceived\n * - `list_pages` ← Chii relay target list + tunnel status\n * Phase 2 (CDP commands):\n * - `get_dom_document` ← DOM.getDocument\n * - `take_snapshot` ← DOMSnapshot.captureSnapshot\n * - `take_screenshot` ← Page.captureScreenshot\n * - `measure_safe_area` ← Runtime.evaluate (safe-area probe)\n * Phase 3 (AIT.* domain — CDP can't cover these):\n * - `AIT.getSdkCallHistory`\n * - `AIT.getMockState`\n * - `AIT.getOperationalEnvironment`\n */\n\nimport type {\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\nimport type {\n CdpCallFrame,\n CdpConnection,\n CdpRemoteObject,\n ConsoleApiCalledEvent,\n DomGetDocumentResult,\n DomSnapshotResult,\n NetworkRequestWillBeSentEvent,\n NetworkResponseReceivedEvent,\n RuntimeExceptionThrownEvent,\n} from './cdp-connection.js';\nimport { buildDeepLinkAttachUrl, validateSchemeAuthority } from './deeplink.js';\nimport type { McpEnvironment } from './environment.js';\nimport { isLiveRelayEnv, isRelayEnv, toLegacyEnv } from './environment.js';\nimport { lookupSignature, warnPassthrough } from './sdk-signatures.js';\nimport { isPidAlive } from './server-lock.js';\nimport { generateTotp, RELAY_VERIFY_SKEW_STEPS } from './totp.js';\n\n/** Tunnel state surfaced by `list_pages`. */\nexport interface TunnelStatus {\n /** Whether the cloudflared quick tunnel is up. */\n up: boolean;\n /** Public `wss://*.trycloudflare.com` relay URL the phone attaches to. */\n wssUrl: string | null;\n /**\n * ISO timestamp when a tunnel drop was first detected by the health probe.\n * `null` means the tunnel has not dropped (or has recovered since the last\n * drop). When non-null and `up` is false, the tunnel is down and the probe\n * has exhausted all reissue attempts — the server must be restarted.\n */\n droppedAt?: string | null;\n /**\n * Number of automatic reissue attempts made after a drop was detected.\n * Resets to 0 after a successful reissue. Reaches `MAX_REISSUE_ATTEMPTS`\n * (3) before the probe gives up and enters the permanent-error state.\n */\n reissueAttempts?: number;\n}\n\n/**\n * Tier classification per RFC #277 (\"MCP tool surface fidelity\"):\n *\n * - **Tier A** (`mock` only) — mock-internal state dials with no real-device\n * equivalent. Hidden when env is `relay`.\n * - **Tier B** (`relay` only) — relay infrastructure tools that have no mock\n * equivalent (e.g. `build_attach_url` needs a cloudflared tunnel URL). Hidden\n * when env is `mock`.\n * - **Tier C** (`both`) — fidelity-parallel tools that produce semantically\n * equivalent results across mock and relay. The agent sees the same tool with\n * the same shape; only the `source` provenance field (where applicable)\n * differs.\n */\nexport type ToolAvailability = 'mock' | 'relay' | 'both';\n\n/** Static MCP tool descriptors (name + JSONSchema) for the full debug tool surface. */\nexport const DEBUG_TOOL_DEFINITIONS = [\n {\n name: 'list_console_messages',\n description:\n 'Lists recent console messages (console.log/warn/error/info) captured from the attached ' +\n 'mini-app page over CDP (Runtime.consoleAPICalled). Read-only. Returns level, text, ' +\n 'timestamp, and stringified args, oldest-first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_network_requests',\n description:\n 'Lists recent network requests (XHR/fetch) captured from the attached mini-app page over ' +\n 'CDP (Network.requestWillBeSent + Network.responseReceived). Read-only. Returns url, ' +\n 'method, status, and timing, oldest-first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_pages',\n description:\n 'Returns the single active page (at most one) the relay sees attached. ' +\n 'When a second page attaches, the previous one is evicted (last-attach wins — ' +\n 'single-attach model). The result includes `singleAttachModel: true` so the agent ' +\n 'knows the array is always 0 or 1 entries. ' +\n 'Also returns whether the cloudflared tunnel is up and the public wss relay URL. ' +\n 'The `tunnel` field includes `droppedAt` (ISO timestamp or null/undefined): when non-null ' +\n 'the tunnel has permanently dropped after 3 failed reissue attempts — restart the debug ' +\n 'server with `npx @ait-co/devtools devtools-mcp`. ' +\n 'Each page entry includes a `lastSeenAt` ISO timestamp (last inbound CDP message from ' +\n 'that target — useful to detect stale entries when the phone app backgrounded). ' +\n 'The result also includes `crashDetectedAt` (ISO timestamp or null): when non-null, ' +\n 'a page crash was detected via Inspector.targetCrashed / Target.targetDestroyed since ' +\n 'the last attach, the pages list will be empty, and `crashWarning` shows a Korean hint ' +\n 'to re-attach. ' +\n 'Call this first to confirm a page is attached before reading console/network. ' +\n 'When a page attaches or detaches the server emits notifications/tools/list_changed — ' +\n 'call tools/list again to get the full updated tool surface.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'build_attach_url',\n description:\n \"The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. \" +\n 'Builds a self-attaching deep-link for the active relay environment and returns a QR code. ' +\n 'Scan the QR with the phone camera to open the mini-app and attach it to this debug session ' +\n '(QR is the single entry path — no USB cable or platform CLI needed). ' +\n 'Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: ' +\n '`npx @ait-co/devtools devtools-mcp`.\\n\\n' +\n 'Environment-specific behaviour:\\n' +\n ' • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the ' +\n 'intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices ' +\n 'debug=1 + relay URL into the scheme URL to produce a self-attach deep-link.\\n' +\n ' • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads ' +\n 'AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) ' +\n 'and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). ' +\n 'When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. ' +\n 'Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\\n\\n' +\n 'Set wait_for_attach=true to block until a page attaches (default 60 s, adjustable via wait_timeout_seconds). ' +\n 'On timeout, call build_attach_url again to resume polling. ' +\n 'The server automatically opens the QR dashboard in the OS default browser when running on a ' +\n 'local GUI machine — headless/remote environments fall back to the text QR in the tool output.' +\n '\\n\\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl ' +\n 'automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes ' +\n '(the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). ' +\n 'The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). ' +\n 'If the phone scan happens after expiresAt, the relay will reject the code — just call ' +\n 'build_attach_url again to get a fresh URL. ' +\n 'Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.\\n\\n' +\n 'selfdebug (env 2 / relay-sandbox only): pass selfdebug=true to add &selfdebug=1 to the ' +\n 'launcher deep-link. The launcher PWA then registers its own document as the CDP target ' +\n 'instead of the framed mini-app. SINGLE-ATTACH MODEL: attaching the launcher self-target ' +\n 'evicts any currently-attached mini-app target — use this mode exclusively for diagnosing ' +\n 'the launcher document itself (DOM, safe-area, console). Not applicable in env 3/4 ' +\n '(relay-staging/relay-live) — passing selfdebug=true there returns an error.',\n inputSchema: {\n type: 'object',\n properties: {\n scheme_url: {\n type: 'string',\n description:\n 'The intoss-private:// scheme URL from `ait deploy --scheme-only` (must carry _deploymentId). ' +\n 'Required for env 3/relay-staging mode. Not used in env 2/relay-sandbox mode (use AIT_TUNNEL_BASE_URL instead). ' +\n 'The authority (host) must be the app name (e.g. intoss-private://aitc-sdk-example?_deploymentId=…). ' +\n 'Generic values like \"web\" or an empty host indicate a malformed URL.',\n },\n wait_for_attach: {\n type: 'boolean',\n description:\n 'If true, block after returning the QR until a page attaches to the relay (polls ' +\n 'listTargets ~1 s interval, default 60 s). On attach, the response includes the ' +\n 'attached page list. On timeout, call build_attach_url again to resume polling.',\n },\n wait_timeout_seconds: {\n type: 'number',\n description:\n 'Maximum seconds to wait when wait_for_attach=true (default 60, range 1–600). ' +\n 'Values outside the range or invalid inputs (0, negative, NaN) fall back to the default silently. ' +\n 'Only meaningful when wait_for_attach=true.',\n },\n projectRoot: {\n type: 'string',\n description:\n 'Absolute path to the mini-app project root (the directory containing its package.json and .ait_urls). ' +\n 'When AIT_TUNNEL_BASE_URL is unset (env 2 / relay-mobile only), the daemon reads the app tunnel URL ' +\n 'from <projectRoot>/.ait_urls written by the dev server (tunnel:{cdp:true}). ' +\n \"Pass this because the daemon's own cwd is fixed at launch. Omit when AIT_TUNNEL_BASE_URL is set explicitly.\",\n },\n selfdebug: {\n type: 'boolean',\n description:\n 'Env 2 / relay-sandbox only. When true, adds &selfdebug=1 to the launcher deep-link ' +\n 'so the launcher PWA registers its own document as the CDP target (launcher diagnostics mode). ' +\n 'SINGLE-ATTACH MODEL: self-target attach evicts any currently-attached mini-app target. ' +\n 'Use only when you need to inspect the launcher itself (DOM, safe-area, console). ' +\n 'Passing selfdebug=true in env 3/4 (relay-staging/relay-live) returns an error. ' +\n 'Default: false (omitted — output is byte-identical to previous behaviour).',\n },\n },\n // scheme_url is required only for env 3/relay-staging; env 2/relay-sandbox uses AIT_TUNNEL_BASE_URL.\n // The handler enforces the requirement at runtime based on the active environment.\n required: [],\n },\n // Tier B per RFC #277 — the URL synthesis requires a live cloudflared\n // tunnel + relay, which only exists in the `relay` environment.\n availableIn: 'relay' as ToolAvailability,\n },\n {\n name: 'get_dom_document',\n description:\n 'Returns the DOM tree of the attached mini-app page over CDP (DOM.getDocument). Read-only. ' +\n 'Use for structural/layout regression diagnosis (e.g. confirming an element exists, ' +\n 'inspecting attributes). Returns the document root node with children.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_snapshot',\n description:\n 'Captures a serialized snapshot of the attached page over CDP (DOMSnapshot.captureSnapshot). ' +\n 'Read-only. Returns the documents + interned strings table for visual-regression diagnosis ' +\n '(e.g. checking computed CSS custom properties like --sat against the live layout).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_screenshot',\n description:\n 'Captures a PNG screenshot of the attached mini-app page over CDP (Page.captureScreenshot) ' +\n 'so the agent can see the phone screen directly. Read-only. ' +\n 'Returns an image content block — this is the only debug tool that returns an image; ' +\n 'all other debug tools return text (JSON).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'measure_safe_area',\n description:\n 'Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns ' +\n 'normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. ' +\n 'Read-only — does not modify page state. ' +\n 'Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel ' +\n 'page with window.__ait state) and `relay` (real-device WebView with window.__sdk). ' +\n 'The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-live\" | \"relay-mobile\"` field so consumers can identify ' +\n 'provenance without inspecting payload values. ' +\n '(`relay-mobile` = env 2 real-device PWA over an external relay; ' +\n '`relay-dev` = env 3 dog-food WebView; `relay-live` = env 4 production WebView.) ' +\n 'Use in a relay session (phone attached) to get ground-truth values for upgrading a ' +\n 'viewport preset from extrapolated/placeholder to measured. ' +\n 'Requires a page to be attached — call list_pages first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'evaluate',\n description:\n 'Evaluates an arbitrary JavaScript expression on the attached mini-app page via ' +\n 'CDP Runtime.evaluate (returnByValue: true) and returns the result. ' +\n 'NOT read-only — the expression can have side effects (DOM mutations, SDK calls, ' +\n 'state changes). Requires the relay to be attached — call list_pages first. ' +\n 'Throws if the evaluation throws an exception on the page.\\n\\n' +\n 'SECURITY: expression and result are not redacted — never include secrets or auth ' +\n 'tokens in the expression.\\n\\n' +\n 'LIVE guard: when running against a live/production relay (relay-live env, ' +\n 'MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that ' +\n 'the expression may affect real users. Without it the call is rejected with a ' +\n 'structured error. mock and relay-dev sessions are unaffected.',\n inputSchema: {\n type: 'object',\n properties: {\n expression: {\n type: 'string',\n description: 'JavaScript expression to evaluate in the page context.',\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge ' +\n 'that this expression may have side effects on real/live users. ' +\n 'Omitting this in a relay-live session results in a structured rejection error. ' +\n 'Has no effect in mock or relay-dev sessions.',\n },\n },\n required: ['expression'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_exceptions',\n description:\n 'Lists JS-level exceptions captured via `Runtime.exceptionThrown` from the relay attached ' +\n 'page. Includes timestamp, exception text, source URL/line, and stack trace. ' +\n 'Use to root-cause SDK throws that may precede a Toss app crash (#265 / #267). ' +\n 'The buffer holds up to 50 most recent exceptions and survives target ' +\n 'replaced/crashed/destroyed events so an exception just before a crash is preserved. ' +\n 'Returns up to 50 most recent by default.',\n inputSchema: {\n type: 'object',\n properties: {\n limit: {\n type: 'number',\n description: 'Maximum number of exceptions to return (default 50, max 50).',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'call_sdk',\n description:\n 'Calls a dog-food SDK method via the window.__sdkCall bridge ' +\n '(exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). ' +\n 'NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). ' +\n 'On env 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) and ' +\n 'env 2 (PWA relay — real WebKit, mock SDK) it hits the mock SDK. ' +\n 'Requires the relay to be attached — call list_pages first. ' +\n 'Returns {ok: true, value} on success or {ok: false, error} on failure. ' +\n 'If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], ' +\n 'the result also includes `recentException` for crash triage. ' +\n 'Returns a clear error if window.__sdkCall is not available — on relay (env 3/4) ' +\n 'that means a non-dog-food bundle (redeploy via `ait build && aitcc app deploy`); ' +\n 'on local (--target=local, env 1) it means the dev bridge is not installed ' +\n '(start the dev server with `pnpm dev`).\\n\\n' +\n 'SECURITY: method name, args, and result value are not redacted — never include secrets.\\n\\n' +\n 'LIVE guard: when running against a live/production relay (relay-live env, ' +\n 'MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that ' +\n 'the SDK call may affect real users. Without it the call is rejected with a ' +\n 'structured error. mock and relay-dev sessions are unaffected.\\n\\n' +\n 'IMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\\n' +\n ' setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\\n' +\n ' setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\\n' +\n ' setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\\n' +\n ' setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\\n' +\n ' getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\\n' +\n ' getPlatformOS: call_sdk(\"getPlatformOS\", [])\\n' +\n ' getDeviceId: call_sdk(\"getDeviceId\", [])\\n' +\n ' getLocale: call_sdk(\"getLocale\", [])\\n' +\n ' getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\\n' +\n ' getSchemeUri: call_sdk(\"getSchemeUri\", [])\\n' +\n ' requestReview: call_sdk(\"requestReview\", [])\\n' +\n ' closeView: call_sdk(\"closeView\", [])',\n inputSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'SDK method name to call (e.g. \"getOperationalEnvironment\").',\n },\n args: {\n type: 'array',\n description: 'Arguments to pass to the SDK method (optional, default []).',\n items: {},\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge ' +\n 'that this SDK call may have side effects on real/live users. ' +\n 'Omitting this in a relay-live session results in a structured rejection error. ' +\n 'Has no effect in mock or relay-dev sessions.',\n },\n },\n required: ['name'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getSdkCallHistory',\n description:\n 'Returns the recent Apps In Toss SDK call trace (method, args, result/error, timestamp) that ' +\n 'raw CDP cannot observe. Read-only. Use to confirm an SDK call fired and how it resolved ' +\n '(e.g. a saveBase64Data permission regression).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getMockState',\n description:\n 'Returns the devtools mock state snapshot (window.__ait) — environment, permissions, location, ' +\n 'auth, network, IAP, and more. Read-only. In dev mode this is the live browser mock state; in ' +\n 'debug mode the in-app side reports it over the AIT domain.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getOperationalEnvironment',\n description:\n 'Returns getOperationalEnvironment() plus the resolved SDK version — metadata raw CDP cannot ' +\n 'observe. Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'start_debug',\n description:\n 'Switches the active debug environment in-place (issue #348) — no Claude Code restart and ' +\n 'no MCP re-handshake. One daemon holds both a local (env 1, mock SDK in a Chromium) and a ' +\n 'relay (env 3/4, real-device Toss WebView over the Chii relay + cloudflared tunnel) ' +\n 'connection at once; this tool flips which one every other tool reads from, lazily booting ' +\n \"the requested family's infra on first use and keeping the inactive one warm so an existing \" +\n 'attach survives the switch. After switching it emits notifications/tools/list_changed — ' +\n 'call tools/list again to see the updated tool surface for the new environment.\\n\\n' +\n 'modes:\\n' +\n ' local-browser — env 1: desktop Chromium with the mock SDK and a local CDP attach. ' +\n 'Side-effect tools (call_sdk/evaluate) run unguarded against the mock; nothing touches a ' +\n 'real device or real users. No prerequisites — the default, always-available environment ' +\n 'for state/contract and visual-layout work.\\n' +\n ' relay-sandbox — env 2: a real-device PWA (real WebKit engine, mock SDK) over an external ' +\n 'Chii relay. CDP covers real-device WebKit DOM, console, exceptions, and safe-area ' +\n 'observation; call_sdk still hits the mock (SDK fidelity needs relay-staging). liveIntent ' +\n 'off — dev-intent, LIVE guard inactive, side-effect tools run unguarded against the mock. ' +\n 'Only the dual-connection daemon can enter relay-sandbox in-place; a single-connection ' +\n 'session rejects it with \"동적 전환할 수 없습니다 … relay-sandbox 모드로 재시작하세요\" — ' +\n 'follow that hint and restart the MCP server in relay-sandbox mode rather than retrying. ' +\n 'Prerequisites: both AIT_RELAY_BASE_URL (the relay base the unplugin emits when started ' +\n 'with tunnel:{cdp:true}, used for the CDP attach) and AIT_TUNNEL_BASE_URL (the dev-server ' +\n 'tunnel host, required by build_attach_url to render the launcher QR) must be set before ' +\n 'the MCP server starts — the unplugin does not auto-forward either; set them explicitly. ' +\n 'Both carry relay/tunnel hosts (secret-class) — keep them out of logs.\\n' +\n ' relay-staging — env 3: a real-device Toss WebView dog-food build with the REAL SDK over the ' +\n 'intoss-private relay. The first environment where call_sdk exercises the genuine native ' +\n 'bridge. Side-effect tools run unguarded (dog-food, not released to real users). ' +\n 'Prerequisite: a dog-food candidate bundle built with `RELEASE_CHANNEL=dogfood ait build`, ' +\n 'then uploaded with `ait deploy` (add `--scheme-only` to print the resulting ' +\n 'intoss-private://…?_deploymentId=… deep-link); open that deep-link/QR on the device to ' +\n 'cold-load the bundle with the relay injected. Unlike env 2, env 3 is NOT a dev-server ' +\n 'tunnel — it is a deployed bundle reached via the intoss-private scheme, so `pnpm dev` ' +\n 'plays no part here.\\n' +\n ' relay-live — env 4: the REVIEW-PASSED, released production runtime with the REAL SDK over ' +\n 'the intoss relay — real end users are on the other side. Read-only debugging is the intent: ' +\n 'the LIVE guard is armed, so call_sdk/evaluate require confirm:true per call, and ENTERING ' +\n 'relay-live ALSO requires confirm:true on this call. Use it only to observe a shipped ' +\n 'regression; verify fixes in relay-staging first.\\n\\n' +\n 'Switching back to local-browser automatically disarms the LIVE guard.\\n\\n' +\n 'For a relay mode (relay-sandbox/relay-staging/relay-live), also pass projectRoot — the ' +\n 'absolute mini-app project root — so the daemon can read the relay auth secret from ' +\n '<projectRoot>/.ait_relay (read-only; the daemon never mints it). Omit it for local-browser.',\n inputSchema: {\n type: 'object',\n properties: {\n mode: {\n type: 'string',\n enum: ['local-browser', 'relay-sandbox', 'relay-staging', 'relay-live'],\n description:\n 'Target environment to switch to. mode=relay-live additionally requires confirm: true (and arms the read-only LIVE guard).',\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when mode=relay-live — set true to acknowledge entering LIVE (env 4) ' +\n 'debugging that can affect real users. Ignored for the other modes.',\n },\n projectRoot: {\n type: 'string',\n description:\n 'Absolute path to the mini-app project root (the directory containing its package.json and .ait_relay). ' +\n 'The daemon reads the relay auth secret from <projectRoot>/.ait_relay (read-only) when switching to a relay ' +\n \"environment (relay-staging/relay-live/relay-sandbox). Pass this because the daemon's own cwd is fixed at launch and may not be \" +\n 'the project being debugged. Omit for mode=local-browser (no secret needed).',\n },\n },\n required: ['mode'],\n },\n // Tier C — always callable so the agent can enter any environment from any\n // starting environment (including a fresh, unattached session).\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_debug_status',\n description:\n 'Reports the current debug session state — which environment/mode is active, whether a page ' +\n 'is attached, and a full diagnostic snapshot — in one call. Use this any time to answer ' +\n '\"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. ' +\n 'Fields: mcpVersion (MCP SDK version), ' +\n 'devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), ' +\n 'pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, ' +\n 'recentErrors (last N server-side errors, PII/secret redacted), ' +\n 'authRejects ({count, lastAt} — relay TOTP 401 rejections, secret-free; count > 0 with empty pages ' +\n 'means the phone reached the relay but its code was rejected), ' +\n 'environment (kind: mock|relay-dev|relay-live|relay-mobile, env: mock|relay backward-compat, reason, ' +\n 'liveGuardActive: true when relay-live LIVE guard is active; ' +\n 'start_debug mode→kind mapping: relay-sandbox→relay-mobile, relay-staging→relay-dev, ' +\n 'relay-live→relay-live, local-browser→mock), ' +\n 'serverLockHolder (pid + startedAt from the lock file, or null), ' +\n 'nextRecommendedAction ({tool, reason} or null — the single next tool to call; ' +\n 'in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). ' +\n 'All fields are nullable — missing data is null, not an error. ' +\n 'debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. ' +\n 'Tier C (both mock and relay).',\n inputSchema: {\n type: 'object',\n properties: {\n recent_errors_limit: {\n type: 'number',\n description:\n 'Maximum number of recent server-side errors to include (default 10, max 50).',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n] as const;\n\nexport type DebugToolName = (typeof DEBUG_TOOL_DEFINITIONS)[number]['name'];\n\nconst DEBUG_TOOL_NAMES = new Set<string>(DEBUG_TOOL_DEFINITIONS.map((t) => t.name));\n\nexport function isDebugToolName(name: string): name is DebugToolName {\n return DEBUG_TOOL_NAMES.has(name);\n}\n\n/**\n * Returns the `ToolAvailability` declared on a registered debug tool, or\n * `undefined` when the name is not a known debug tool. Used by the tool\n * registry to filter `tools/list` by current env and by the call handler to\n * reject env-mismatch invocations.\n */\nexport function getToolAvailability(name: string): ToolAvailability | undefined {\n for (const t of DEBUG_TOOL_DEFINITIONS) {\n if (t.name === name) return t.availableIn;\n }\n return undefined;\n}\n\n/**\n * Returns true when the named tool is available in the given environment.\n * Unknown tools return `false` — callers should reject them as unknown rather\n * than as env-mismatched.\n *\n * Relay variants (`relay-dev`, `relay-live`, `relay-mobile`) all satisfy the\n * `'relay'` availability tier — `isRelayEnv()` is used for the check.\n */\nexport function isToolAvailableIn(name: string, env: McpEnvironment): boolean {\n const availability = getToolAvailability(name);\n if (availability === undefined) return false;\n if (availability === 'both') return true;\n if (availability === 'relay') return isRelayEnv(env);\n return availability === env;\n}\n\n/**\n * Filters a `DEBUG_TOOL_DEFINITIONS`-shaped list to those whose `availableIn`\n * matches the given env. Pure — preserves order; both Tier C (\"both\") and the\n * matching single-env tier pass through.\n *\n * Relay variants (`relay-dev`, `relay-live`, `relay-mobile`) all satisfy the\n * `'relay'` tier.\n */\nexport function filterToolsByEnvironment<T extends { name: string; availableIn: ToolAvailability }>(\n tools: ReadonlyArray<T>,\n env: McpEnvironment,\n): T[] {\n return tools.filter(\n (t) =>\n t.availableIn === 'both' ||\n (t.availableIn === 'relay' && isRelayEnv(env)) ||\n t.availableIn === env,\n );\n}\n\n/**\n * Tool names that are available before any page attaches (bootstrap tier).\n *\n * `build_attach_url` — pure URL synthesis, no attach needed.\n * `list_pages` — reports tunnel status + empty pages even pre-attach.\n *\n * All other tools require an attached page (`enableDomains` must succeed) and\n * are only advertised in `tools/list` once a target appears.\n */\nexport const BOOTSTRAP_TOOL_NAMES: ReadonlySet<string> = new Set<string>([\n 'build_attach_url',\n 'get_debug_status',\n 'list_pages',\n // start_debug must be visible from the very first tools/list (before any\n // attach) so the agent can switch environments to bootstrap an attach.\n 'start_debug',\n]);\n\n/** Normalized console message returned by `list_console_messages`. */\nexport interface ConsoleMessage {\n level: string;\n text: string;\n timestamp: number;\n args: string[];\n}\n\n/** Normalized network request returned by `list_network_requests`. */\nexport interface NetworkRequest {\n requestId: string;\n url: string;\n method: string;\n /** HTTP status once a response was seen, else null (still in-flight). */\n status: number | null;\n statusText: string | null;\n /** Request start (CDP timestamp). */\n startTime: number;\n /** Response received (CDP timestamp), else null. */\n endTime: number | null;\n}\n\n/** Renders a CDP `RemoteObject` console arg to a stable display string. */\nfunction renderRemoteObject(arg: CdpRemoteObject): string {\n if (arg.value !== undefined) {\n if (typeof arg.value === 'string') return arg.value;\n try {\n return JSON.stringify(arg.value);\n } catch {\n return String(arg.value);\n }\n }\n if (arg.description !== undefined) return arg.description;\n if (arg.className !== undefined) return arg.className;\n return arg.subtype ?? arg.type;\n}\n\nexport function normalizeConsoleMessage(event: ConsoleApiCalledEvent): ConsoleMessage {\n const args = event.args.map(renderRemoteObject);\n return {\n level: event.type,\n text: args.join(' '),\n timestamp: event.timestamp,\n args,\n };\n}\n\nexport function listConsoleMessages(connection: CdpConnection): ConsoleMessage[] {\n return connection\n .getBufferedEvents('Runtime.consoleAPICalled')\n .map((event) => normalizeConsoleMessage(event));\n}\n\nexport function listNetworkRequests(connection: CdpConnection): NetworkRequest[] {\n const requests = connection.getBufferedEvents('Network.requestWillBeSent');\n const responses = connection.getBufferedEvents('Network.responseReceived');\n\n const responseByRequestId = new Map<string, NetworkResponseReceivedEvent>();\n for (const response of responses) {\n responseByRequestId.set(response.requestId, response);\n }\n\n return requests.map((request: NetworkRequestWillBeSentEvent) => {\n const response = responseByRequestId.get(request.requestId);\n return {\n requestId: request.requestId,\n url: request.request.url,\n method: request.request.method,\n status: response ? response.response.status : null,\n statusText: response ? response.response.statusText : null,\n startTime: request.timestamp,\n endTime: response ? response.timestamp : null,\n };\n });\n}\n\n/* -------------------------------------------------------------------------- */\n/* list_exceptions — Runtime.exceptionThrown ring buffer */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Normalized exception returned by `list_exceptions`.\n *\n * Flattens the CDP `Runtime.ExceptionDetails` shape into the most useful\n * fields. The `raw` field carries the original event for callers that need\n * the full payload.\n */\nexport interface BufferedException {\n /** Wall-clock ms since epoch (CDP `Runtime.Timestamp`). */\n timestamp: number;\n /** Short summary text from `exceptionDetails.text`. */\n text: string;\n /** Source URL where the exception was thrown, if known. */\n url?: string;\n /** 0-based line number in the source file, if known. */\n lineNumber?: number;\n /** 0-based column number in the source file, if known. */\n columnNumber?: number;\n /** `description` of the thrown `RemoteObject` (e.g. \"TypeError: …\"). */\n exceptionText?: string;\n /**\n * Formatted stack trace: `at fn (url:line:col)` lines joined by `\\n`.\n * Omitted when no `stackTrace.callFrames` are available.\n */\n stack?: string;\n /** Full original `Runtime.exceptionThrown` event payload. */\n raw: RuntimeExceptionThrownEvent;\n}\n\n/** Formats a single CDP call frame into `at fn (url:line:col)`. */\nfunction formatCallFrame(frame: CdpCallFrame): string {\n const fn = frame.functionName || '(anonymous)';\n return `at ${fn} (${frame.url}:${frame.lineNumber}:${frame.columnNumber})`;\n}\n\n/** Normalizes a raw `Runtime.exceptionThrown` event into a `BufferedException`. */\nexport function normalizeException(event: RuntimeExceptionThrownEvent): BufferedException {\n const { timestamp, exceptionDetails } = event;\n const frames = exceptionDetails.stackTrace?.callFrames;\n const stack = frames && frames.length > 0 ? frames.map(formatCallFrame).join('\\n') : undefined;\n const exceptionText = exceptionDetails.exception?.description ?? undefined;\n\n const result: BufferedException = {\n timestamp,\n text: exceptionDetails.text,\n raw: event,\n };\n if (exceptionDetails.url !== undefined) result.url = exceptionDetails.url;\n if (exceptionDetails.lineNumber !== undefined) result.lineNumber = exceptionDetails.lineNumber;\n if (exceptionDetails.columnNumber !== undefined)\n result.columnNumber = exceptionDetails.columnNumber;\n if (exceptionText !== undefined) result.exceptionText = exceptionText;\n if (stack !== undefined) result.stack = stack;\n return result;\n}\n\n/**\n * Returns the most recent buffered `Runtime.exceptionThrown` events, normalized.\n * Oldest-first; limited to `limit` entries (default 50, max 50).\n */\nexport function listExceptions(connection: CdpConnection, limit = 50): BufferedException[] {\n const cap = Math.min(Math.max(1, limit), 50);\n const events = connection.getBufferedEvents('Runtime.exceptionThrown');\n // Slice from the tail to respect the cap while preserving oldest-first order.\n const sliced = events.length > cap ? events.slice(events.length - cap) : events;\n return sliced.map((e) => normalizeException(e));\n}\n\n/** A page entry in the `list_pages` result, extended with freshness info. */\nexport interface ListPagesEntry {\n id: string;\n title: string;\n url: string;\n /** ISO timestamp of the last inbound CDP message from this target, or null. */\n lastSeenAt: string | null;\n}\n\n/** Result of `list_pages`: attach status + tunnel state + crash info. */\nexport interface ListPagesResult {\n /**\n * The single active page, or an empty array when nothing is attached.\n * Under the single-attach model this is always 0 or 1 entries.\n */\n pages: ListPagesEntry[];\n tunnel: TunnelStatus;\n /**\n * ISO timestamp of the most recent crash / targetDestroyed / detachedFromTarget\n * event detected since the last `enableDomains()`, or `null` if none.\n * When non-null, all attached pages have been removed from the relay map and\n * a new `enableDomains()` call is required to resume debugging.\n */\n crashDetectedAt: string | null;\n /** Korean warning line shown in tool output when a crash was detected. */\n crashWarning: string | null;\n /**\n * Always `true` — signals to the agent that at most one page is ever present.\n * When a second page attaches, the previous one is evicted (last-attach wins).\n */\n singleAttachModel: true;\n}\n\n/**\n * Duck-type interface for the crash-detection extras exposed by `ChiiCdpConnection`.\n * The base `CdpConnection` interface is kept minimal (fake-friendly); the extras\n * are opt-in so tests without them continue to compile.\n */\ninterface CrashAwareCdpConnection extends CdpConnection {\n getLastCrashDetectedAt(): number | null;\n getTargetLastSeenAt(targetId: string): number | null;\n}\n\nfunction isCrashAware(conn: CdpConnection): conn is CrashAwareCdpConnection {\n return (\n typeof (conn as CrashAwareCdpConnection).getLastCrashDetectedAt === 'function' &&\n typeof (conn as CrashAwareCdpConnection).getTargetLastSeenAt === 'function'\n );\n}\n\nexport function listPages(connection: CdpConnection, tunnel: TunnelStatus): ListPagesResult {\n const rawTargets = connection.listTargets();\n const pages: ListPagesEntry[] = rawTargets.map((t) => {\n const lastSeenMs = isCrashAware(connection) ? connection.getTargetLastSeenAt(t.id) : null;\n return {\n id: t.id,\n title: t.title,\n url: t.url,\n lastSeenAt: lastSeenMs !== null ? new Date(lastSeenMs).toISOString() : null,\n };\n });\n\n const crashMs = isCrashAware(connection) ? connection.getLastCrashDetectedAt() : null;\n const crashDetectedAt = crashMs !== null ? new Date(crashMs).toISOString() : null;\n const crashWarning = crashDetectedAt\n ? `[ait-debug] page crash 감지됨 — 새 attach 필요 (관측 시각: ${crashDetectedAt})`\n : null;\n\n return { pages, tunnel, crashDetectedAt, crashWarning, singleAttachModel: true };\n}\n\n/** A `build_attach_url` result: the spliced deep-link the phone should open. */\nexport interface BuildAttachUrlResult {\n /** The scheme URL with `debug=1&relay=<wss>[&at=<totp-code>]` spliced in. */\n attachUrl: string;\n /** The relay URL that was spliced in (this session's quick tunnel). */\n relayUrl: string;\n /**\n * Non-fatal warning about the scheme URL's authority being missing or\n * suspicious (e.g. \"web\", \"localhost\"). Callers should surface this to\n * help the user catch a malformed URL early.\n */\n authorityWarning?: string;\n /**\n * TOTP metadata — present when `AIT_DEBUG_TOTP_SECRET` is set.\n *\n * SECRET-HANDLING: the `at=` code value is spliced into `attachUrl` only.\n * It is never surfaced separately here to avoid inadvertent logging of the\n * one-time code outside of the URL.\n */\n totp?: {\n /** `true` when a TOTP code was spliced into `attachUrl`. */\n enabled: true;\n /** RFC 6238 step duration in seconds. */\n ttlSeconds: number;\n /** ISO timestamp when the current step expires. Rescan or call build_attach_url again after this. */\n expiresAt: string;\n };\n}\n\n/**\n * Builds a self-attaching dog-food deep-link from an `ait deploy --scheme-only`\n * URL plus this session's live relay. Throws if the tunnel is not up yet (no\n * relay URL to splice in) — the caller surfaces that as a tool error.\n *\n * When `AIT_DEBUG_TOTP_SECRET` is set, generates the current TOTP code and\n * splices it as `at=<code>` into the attach URL. The code is valid for ~3\n * minutes (the relay gate uses {@link RELAY_VERIFY_SKEW_STEPS}=6, accepting\n * past 6 steps = 180–210 s backwards from issuance). If the scan happens after\n * `totp.expiresAt`, call `build_attach_url` again to get a fresh code (#490).\n *\n * Also validates the scheme URL's authority. A suspicious authority (empty,\n * \"web\", \"localhost\", etc.) is surfaced as a non-fatal `authorityWarning` on\n * the result so the caller can show a helpful hint without blocking the link\n * generation (the warning is consistent with how other validation in\n * `buildDeepLinkAttachUrl` works — hard errors for relay, soft warning for\n * the scheme authority which is in the caller's input, not ours to own).\n *\n * SECRET-HANDLING: `totpSecret` (if provided) is used only to compute a code\n * and must never appear in any log, error message, or output outside of the\n * spliced `at=` param in `attachUrl`.\n *\n * @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL.\n * @param tunnel - Current tunnel status from the running debug server.\n * @param totpSecret - Optional hex-encoded TOTP secret (from\n * `AIT_DEBUG_TOTP_SECRET`). When provided, the current code is spliced into\n * the attach URL as `at=<code>`.\n */\nexport function buildAttachUrl(\n schemeUrl: string,\n tunnel: TunnelStatus,\n totpSecret?: string,\n): BuildAttachUrlResult {\n if (!tunnel.up || tunnel.wssUrl === null) {\n throw new Error(\n 'tunnel-down: cloudflared 터널이 안 떠 있습니다. ' +\n 'MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.',\n );\n }\n const authorityWarning = validateSchemeAuthority(schemeUrl) ?? undefined;\n\n // Generate a live TOTP code when a secret is provided.\n // SECRET-HANDLING: the code value is placed into attachUrl only — not logged.\n let totpCode: string | undefined;\n let totpMeta: BuildAttachUrlResult['totp'];\n if (totpSecret !== undefined && totpSecret !== '') {\n const now = Date.now();\n totpCode = generateTotp(totpSecret, now);\n const STEP_SECONDS = 30;\n // expiresAt reflects the relay gate's actual acceptance window (#490):\n // the gate uses RELAY_VERIFY_SKEW_STEPS=6, so past 6 steps (180 s) are\n // accepted. The code issued NOW is valid until step (currentStep + 7)\n // starts — i.e. the earliest time it can be rejected is 180 s after\n // the NEXT step boundary, which is (currentStep+1)*30 + 6*30 = now-aligned\n // ~180–210 s from issuance. We report issuanceTime + 180 s as a conservative\n // lower bound so callers know the code is safe for at least ~3 minutes.\n const expiresAtMs = now + RELAY_VERIFY_SKEW_STEPS * STEP_SECONDS * 1000;\n totpMeta = {\n enabled: true,\n ttlSeconds: RELAY_VERIFY_SKEW_STEPS * STEP_SECONDS,\n expiresAt: new Date(expiresAtMs).toISOString(),\n };\n }\n\n return {\n attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl, totpCode),\n relayUrl: tunnel.wssUrl,\n ...(authorityWarning !== undefined ? { authorityWarning } : {}),\n ...(totpMeta !== undefined ? { totp: totpMeta } : {}),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* QR PNG rendering + browser open */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Heuristic: can this process open a GUI browser?\n *\n * Returns `true` when we think a GUI is available:\n * - On macOS (`darwin`) we assume yes (MCP normally runs on the user's Mac).\n * - On Linux we check for `DISPLAY` or `WAYLAND_DISPLAY`.\n * - On Windows we assume yes.\n * - In a CI environment (`CI=true`) we assume no.\n */\nexport function canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/**\n * Result of `openQrInBrowser`.\n *\n * HTTP URL 기반으로 재구현 — tmp 파일 없음. `httpUrl`이 브라우저에 전달되는 URL이다.\n * SECRET-HANDLING: `httpUrl`은 127.0.0.1 로컬 전용이며 tunnel host·relay wss·TOTP at= 코드를\n * 담지 않는다 (#595). attachUrl은 server-state(getDashboardState)로만 보유된다.\n */\nexport interface OpenQrInBrowserResult {\n /** `true` if the browser was successfully opened. */\n opened: boolean;\n /** `http://127.0.0.1:<port>/` — 브라우저에 전달되는 루트 URL (#595). */\n httpUrl: string;\n /** `http://127.0.0.1:<port>/qr.png?u=...` — PNG fallback URL. */\n pngUrl: string;\n /** Error message if `opened` is false (browser spawn failed). */\n error?: string;\n /** Captured stderr from failed spawn attempts (at= 값은 redact됨). */\n stderrSummary?: string;\n /**\n * `true` when the first attempt failed but a retry succeeded.\n * Helps distinguish \"worked on first try\" from \"needed retry\" in diagnostics.\n */\n retried?: boolean;\n}\n\n/** platform별 browser open 명령 후보 목록 — 앞에서부터 순차 시도. */\nfunction getBrowserCandidates(httpUrl: string): Array<{ cmd: string; args: string[] }> {\n const platform = process.platform;\n if (platform === 'darwin') {\n return [\n { cmd: 'open', args: [httpUrl] },\n { cmd: 'open', args: ['-a', 'Safari', httpUrl] },\n { cmd: 'open', args: ['-a', 'Google Chrome', httpUrl] },\n { cmd: 'open', args: ['-a', 'Firefox', httpUrl] },\n ];\n }\n if (platform === 'win32') {\n return [\n { cmd: 'cmd', args: ['/c', 'start', '', httpUrl] },\n { cmd: 'rundll32', args: ['url.dll,FileProtocolHandler', httpUrl] },\n ];\n }\n // linux + fallback\n return [\n { cmd: 'xdg-open', args: [httpUrl] },\n { cmd: 'sensible-browser', args: [httpUrl] },\n { cmd: 'x-www-browser', args: [httpUrl] },\n { cmd: 'firefox', args: [httpUrl] },\n { cmd: 'google-chrome', args: [httpUrl] },\n { cmd: 'chromium', args: [httpUrl] },\n ];\n}\n\n/** stderr에서 at= TOTP 코드 값을 redact한다. */\nfunction redactSecrets(text: string): string {\n // at=<value> 패턴에서 값 부분을 redact — TOTP 코드가 노출되지 않도록.\n return text.replace(/\\bat=([^&\\s\"']+)/g, 'at=<redacted>');\n}\n\n/** spawnSync exit 0이어도 stderr에 launch 실패 시그널이 있으면 실패로 판단한다. */\nconst LAUNCH_FAILURE_PATTERNS = [\n /LSOpenURLsWithRole\\(\\) failed/,\n /kLSApplicationNotFoundErr/,\n /No application/,\n /Unable to find application/,\n /xdg-open: not found/,\n /command not found/,\n];\n\nfunction isLaunchFailureStderr(stderr: string): boolean {\n return LAUNCH_FAILURE_PATTERNS.some((p) => p.test(stderr));\n}\n\n/**\n * 로컬 HTTP 서버 루트 URL(`http://127.0.0.1:<port>/`)을 OS 기본 브라우저로 연다 (#595).\n *\n * platform별 fallback chain으로 시도하며, 모두 실패하면 1회 retry를 수행한다\n * (ephemeral process launch 타이밍 문제 대응). retry까지 실패해도 `opened: false` +\n * `httpUrl`을 반환해 사용자가 직접 브라우저에 붙여넣을 수 있게 한다.\n *\n * SECRET-HANDLING:\n * - tmp 파일을 만들지 않는다 (HTML/PNG는 HTTP 서버가 메모리에서 응답).\n * - httpUrl은 `http://127.0.0.1:<port>/`(루트, 시크릿 없음). pngUrl은 127.0.0.1 로컬 전용.\n * - stderr 캡처 결과에서 at= 코드 값을 redact한 후 stderrSummary에 포함.\n * - attachUrl, deploymentId, TOTP 코드를 stdout/stderr/로그에 직접 출력 금지.\n *\n * @param httpUrl - `http://127.0.0.1:<port>/` 루트 URL (시크릿 없음, #595).\n * @param pngUrl - `http://127.0.0.1:<port>/qr.png?u=<encoded>` PNG fallback URL.\n */\nexport async function openQrInBrowser(\n httpUrl: string,\n pngUrl: string,\n): Promise<OpenQrInBrowserResult> {\n const { spawnSync } = await import('node:child_process');\n\n /**\n * 한 번의 fallback chain 시도. 성공하면 열린 후보 cmd를 반환, 실패하면 null.\n * stderrLines에 각 후보의 stderr를 누적한다.\n */\n function tryOnce(stderrLines: string[]): boolean {\n const candidates = getBrowserCandidates(httpUrl);\n for (const { cmd, args } of candidates) {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5000 });\n\n if (result.error) {\n stderrLines.push(`${cmd}: ${result.error.message}`);\n continue;\n }\n\n const stderr = typeof result.stderr === 'string' ? result.stderr : '';\n if (stderr) {\n stderrLines.push(`${cmd}: ${redactSecrets(stderr.trim())}`);\n }\n\n if (result.status === 0 && !isLaunchFailureStderr(stderr)) {\n return true;\n }\n }\n return false;\n }\n\n const stderrLines: string[] = [];\n\n // 1차 시도\n if (tryOnce(stderrLines)) {\n return { opened: true, httpUrl, pngUrl };\n }\n\n // 1회 retry (ephemeral process launch 타이밍 문제 대응)\n if (tryOnce(stderrLines)) {\n return { opened: true, httpUrl, pngUrl, retried: true };\n }\n\n const stderrSummary = stderrLines.length > 0 ? stderrLines.join('\\n') : undefined;\n return {\n opened: false,\n httpUrl,\n pngUrl,\n error: '모든 브라우저 실행 후보가 실패했습니다.',\n stderrSummary,\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Phase 2 — DOM / snapshot / screenshot (CDP commands) */\n/* -------------------------------------------------------------------------- */\n\n/** Returns the DOM tree of the attached page (`DOM.getDocument`). */\nexport function getDomDocument(connection: CdpConnection): Promise<DomGetDocumentResult> {\n // `pierce: true` flattens shadow roots; depth -1 returns the whole subtree so\n // a single call yields the full tree for structural diagnosis.\n return connection.send('DOM.getDocument', { depth: -1, pierce: true });\n}\n\n/** Returns a serialized page snapshot (`DOMSnapshot.captureSnapshot`). */\nexport function takeSnapshot(connection: CdpConnection): Promise<DomSnapshotResult> {\n return connection.send('DOMSnapshot.captureSnapshot', {});\n}\n\n/** A `take_screenshot` result: the raw base64 PNG plus a ready-to-use data URI. */\nexport interface ScreenshotResult {\n /** Base64-encoded PNG bytes (no data-URI prefix). */\n data: string;\n /** `data:image/png;base64,…` form for clients that render a URI. */\n dataUri: string;\n mimeType: 'image/png';\n}\n\n/** Captures a PNG screenshot of the attached page (`Page.captureScreenshot`). */\nexport async function takeScreenshot(connection: CdpConnection): Promise<ScreenshotResult> {\n const { data } = await connection.send('Page.captureScreenshot', { format: 'png' });\n return { data, dataUri: `data:image/png;base64,${data}`, mimeType: 'image/png' };\n}\n\n/* -------------------------------------------------------------------------- */\n/* measure_safe_area — Runtime.evaluate probe */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The JS probe injected via `Runtime.evaluate`. It reads:\n * 1. `env(safe-area-inset-*)` via a temporary element with padding set to\n * those CSS env vars, then `getComputedStyle`.\n * 2. SDK insets via a priority chain so the SAME probe works on both relay\n * (real device) and mock (devtools panel page):\n * a. `window.__sdk.SafeAreaInsets.get()` — dog-food bundle on real device.\n * b. `window.__sdk.getSafeAreaInsets()` — dog-food bundle (deprecated).\n * c. `window.__ait.state.safeAreaInsets` — devtools mock state (mock env).\n * The probe records `sdkInsetsSource` = `'window.__sdk'` | `'window.__ait'`\n * | `null`. If all paths fail the result carries `sdkInsetsError`.\n * 3. nav bar geometry: the SDK does not expose navBar height as a standalone\n * API — `.ait-navbar` DOM height is read as a cross-check, and\n * `navBarHeightSource` records where it came from.\n * 4. `innerWidth`, `innerHeight`, `devicePixelRatio`, `navigator.userAgent`.\n *\n * Returns a plain JSON-serialisable object so `returnByValue: true` works.\n *\n * NOTE: This expression is evaluated in the page context — on the real device\n * (relay) or on the mock panel page. It does not mutate any page state — the\n * temporary element is removed after reading. No secret or auth token is read\n * or returned.\n *\n * RFC #277 Tier C parity: the SAME probe string runs in both envs. Mock fidelity\n * comes from the panel's `applyViewport` / `computeSafeAreaInsets` correctly\n * setting `window.__ait.state.safeAreaInsets` (#275). When that is correct,\n * the cssEnv + sdkInsets pair returned here matches the relay's shape.\n */\nexport const SAFE_AREA_PROBE_EXPRESSION = `\n(function() {\n var el = document.createElement('div');\n el.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;visibility:hidden;' +\n 'padding-top:env(safe-area-inset-top,0px);' +\n 'padding-right:env(safe-area-inset-right,0px);' +\n 'padding-bottom:env(safe-area-inset-bottom,0px);' +\n 'padding-left:env(safe-area-inset-left,0px)';\n document.documentElement.appendChild(el);\n var cs = window.getComputedStyle(el);\n var cssEnv = {\n top: parseFloat(cs.paddingTop) || 0,\n right: parseFloat(cs.paddingRight) || 0,\n bottom: parseFloat(cs.paddingBottom) || 0,\n left: parseFloat(cs.paddingLeft) || 0\n };\n document.documentElement.removeChild(el);\n var sdkInsets = null;\n var sdkInsetsSource = null;\n var sdkInsetsError = undefined;\n try {\n var sdk = window.__sdk;\n var ait = window.__ait;\n if (sdk && sdk.SafeAreaInsets && typeof sdk.SafeAreaInsets.get === 'function') {\n sdkInsets = sdk.SafeAreaInsets.get();\n sdkInsetsSource = 'window.__sdk';\n } else if (sdk && typeof sdk.getSafeAreaInsets === 'function') {\n sdkInsets = sdk.getSafeAreaInsets();\n sdkInsetsSource = 'window.__sdk';\n } else if (ait && ait.state && ait.state.safeAreaInsets &&\n typeof ait.state.safeAreaInsets.top === 'number') {\n var s = ait.state.safeAreaInsets;\n sdkInsets = { top: s.top, bottom: s.bottom, left: s.left, right: s.right };\n sdkInsetsSource = 'window.__ait';\n } else if (!sdk && !ait) {\n sdkInsetsError = 'neither window.__sdk (relay) nor window.__ait (mock) available';\n } else if (sdk) {\n sdkInsetsError = 'neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk';\n } else {\n sdkInsetsError = 'window.__ait.state.safeAreaInsets is missing or malformed';\n }\n } catch(e) {\n sdkInsetsError = String(e && e.message || e);\n }\n var navBarHeight = null;\n var navBarHeightSource = 'not-exposed-by-sdk';\n try {\n var nb = document.querySelector('.ait-navbar');\n if (nb) {\n navBarHeight = nb.getBoundingClientRect().height;\n navBarHeightSource = 'dom-.ait-navbar';\n }\n } catch(_) {}\n var result = {\n cssEnv: cssEnv,\n sdkInsets: sdkInsets,\n sdkInsetsSource: sdkInsetsSource,\n navBarHeight: navBarHeight,\n navBarHeightSource: navBarHeightSource,\n innerWidth: window.innerWidth,\n innerHeight: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio,\n userAgent: navigator.userAgent\n };\n if (sdkInsetsError !== undefined) result.sdkInsetsError = sdkInsetsError;\n return JSON.stringify(result);\n})()\n`.trim();\n\n/**\n * Where the SDK insets came from. `null` when the lookup failed (in which case\n * `sdkInsetsError` is populated).\n *\n * - `'window.__sdk'` — real-device dog-food bundle (relay env).\n * - `'window.__ait'` — devtools mock state (mock env).\n * - `null` — both paths absent or threw.\n */\nexport type SdkInsetsSource = 'window.__sdk' | 'window.__ait' | null;\n\n/**\n * Normalized result returned by `measure_safe_area`.\n *\n * All inset values are in CSS pixels as reported by the page context.\n * `userAgent` is included for device identification; it never contains\n * authentication secrets or session tokens.\n */\nexport interface SafeAreaMeasurement {\n /**\n * MCP environment this measurement was taken in:\n * - `'mock'` — dev browser panel\n * - `'relay-dev'` — real-device WebView, dog-food build\n * - `'relay-live'` — real-device WebView, live/production build\n * - `'relay-mobile'` — real-device PWA (env 2) over an external relay\n *\n * Set by the caller (`measureSafeArea`) from the env detection SSoT\n * (`getEnvironment`).\n */\n source: McpEnvironment;\n /**\n * `env(safe-area-inset-*)` values read via `getComputedStyle` on the page.\n * On iOS inside the Toss host WebView this is typically all-zero because the\n * WebView viewport is placed below the physical notch by the host app.\n */\n cssEnv: { top: number; right: number; bottom: number; left: number };\n /**\n * SDK insets from one of three paths (in priority order):\n * - `window.__sdk.SafeAreaInsets.get()` (relay, dog-food bundle)\n * - `window.__sdk.getSafeAreaInsets()` (relay, deprecated)\n * - `window.__ait.state.safeAreaInsets` (mock, devtools panel state)\n *\n * `null` when all paths fail — see `sdkInsetsError` for the reason.\n * In the Toss host WebView `top` is the nav bar height and `bottom` is the\n * home-indicator height.\n */\n sdkInsets: { top: number; right: number; bottom: number; left: number } | null;\n /**\n * Which path resolved `sdkInsets` — useful for diagnosis of fidelity gaps\n * between mock and relay. `null` when `sdkInsets` is `null`.\n */\n sdkInsetsSource: SdkInsetsSource;\n /**\n * Populated when the SDK inset lookup failed (all paths absent or threw).\n * `undefined` when `sdkInsets` is non-null (i.e. the lookup succeeded).\n *\n * Example values:\n * - `\"neither window.__sdk (relay) nor window.__ait (mock) available\"`\n * - `\"neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk\"`\n * - `\"window.__ait.state.safeAreaInsets is missing or malformed\"`\n * - `\"TypeError: ...\"`\n */\n sdkInsetsError?: string;\n /**\n * Height of the `.ait-navbar` element (px) if present, else `null`.\n * The SDK does not expose navBar height as a standalone API; this DOM\n * measurement is used to cross-validate `sdkInsets.top`.\n */\n navBarHeight: number | null;\n /**\n * Describes where `navBarHeight` came from:\n * - `\"dom-.ait-navbar\"` — read from the `.ait-navbar` element's bounding rect.\n * - `\"not-exposed-by-sdk\"` — the SDK has no standalone navBar height API and\n * no `.ait-navbar` element was found in the DOM.\n */\n navBarHeightSource: string;\n /** CSS viewport width (`window.innerWidth`). */\n innerWidth: number;\n /** CSS viewport height (`window.innerHeight`). */\n innerHeight: number;\n /**\n * Device pixel ratio (`window.devicePixelRatio`).\n * Note: `window.devicePixelRatio` is read-only in the browser, so devtools\n * cannot emulate DPR locally — this is the ground-truth value from the device.\n */\n devicePixelRatio: number;\n /**\n * `navigator.userAgent` string for device identification.\n * Does not contain authentication secrets.\n */\n userAgent: string;\n}\n\n/**\n * Parses a raw `Runtime.evaluate` result value into a `SafeAreaMeasurement`.\n * The probe returns a JSON string (because `returnByValue:true` with a plain\n * object works unreliably across Chii relay versions — stringifying is safer).\n *\n * `source` is supplied by the caller (`measureSafeArea`) from the env SSoT.\n *\n * Throws if the result is missing, contains an exception, or cannot be parsed.\n */\nexport function normalizeSafeAreaResult(\n rawValue: unknown,\n source: McpEnvironment,\n): SafeAreaMeasurement {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `measure_safe_area: probe returned unexpected type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n throw new Error(`measure_safe_area: probe returned non-JSON string: ${rawValue}`);\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('measure_safe_area: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n\n function requireInsets(\n key: string,\n ): { top: number; right: number; bottom: number; left: number } | null {\n const v = obj[key];\n if (v === null || v === undefined) return null;\n if (typeof v !== 'object') return null;\n const r = v as Record<string, unknown>;\n return {\n top: typeof r.top === 'number' ? r.top : 0,\n right: typeof r.right === 'number' ? r.right : 0,\n bottom: typeof r.bottom === 'number' ? r.bottom : 0,\n left: typeof r.left === 'number' ? r.left : 0,\n };\n }\n\n const cssEnv = requireInsets('cssEnv') ?? { top: 0, right: 0, bottom: 0, left: 0 };\n const sdkInsets = requireInsets('sdkInsets');\n const sdkInsetsSource: SdkInsetsSource =\n obj.sdkInsetsSource === 'window.__sdk' || obj.sdkInsetsSource === 'window.__ait'\n ? obj.sdkInsetsSource\n : null;\n const sdkInsetsError = typeof obj.sdkInsetsError === 'string' ? obj.sdkInsetsError : undefined;\n const navBarHeight = typeof obj.navBarHeight === 'number' ? obj.navBarHeight : null;\n const navBarHeightSource =\n typeof obj.navBarHeightSource === 'string' ? obj.navBarHeightSource : 'not-exposed-by-sdk';\n const innerWidth = typeof obj.innerWidth === 'number' ? obj.innerWidth : 0;\n const innerHeight = typeof obj.innerHeight === 'number' ? obj.innerHeight : 0;\n const devicePixelRatio = typeof obj.devicePixelRatio === 'number' ? obj.devicePixelRatio : 1;\n const userAgent = typeof obj.userAgent === 'string' ? obj.userAgent : '';\n\n return {\n source,\n cssEnv,\n sdkInsets,\n sdkInsetsSource,\n ...(sdkInsetsError !== undefined ? { sdkInsetsError } : {}),\n navBarHeight,\n navBarHeightSource,\n innerWidth,\n innerHeight,\n devicePixelRatio,\n userAgent,\n };\n}\n\n/**\n * Runs the safe-area probe on the attached page and returns a normalized\n * `SafeAreaMeasurement`. Read-only — does not mutate page state.\n *\n * `source` is supplied by the caller from the env detection SSoT (see\n * `src/mcp/environment.ts`). The same `Runtime.evaluate` call runs in both\n * envs — the probe expression tries `window.__sdk` first (relay) then\n * `window.__ait` (mock), so mock fidelity is enforced by the panel's\n * `applyViewport`/`computeSafeAreaInsets` keeping `__ait.state.safeAreaInsets`\n * correct (RFC #277 Tier C parity, #275 model).\n *\n * Throws on CDP error, probe exception, or result parse failure.\n */\nexport async function measureSafeArea(\n connection: CdpConnection,\n source: McpEnvironment,\n): Promise<SafeAreaMeasurement> {\n const result = await connection.send('Runtime.evaluate', {\n expression: SAFE_AREA_PROBE_EXPRESSION,\n returnByValue: true,\n awaitPromise: false,\n });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`measure_safe_area: probe threw — ${msg}`);\n }\n return normalizeSafeAreaResult(result.result.value, source);\n}\n\n/* -------------------------------------------------------------------------- */\n/* evaluate — arbitrary JS via Runtime.evaluate */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result returned by the `evaluate` tool.\n *\n * `value` holds the `returnByValue` result from CDP — it may be any\n * JSON-serialisable type. Treat it as opaque for logging purposes (it could\n * carry sensitive data from the page context).\n *\n * SECRET-HANDLING: do NOT write `value` to any log or stderr — return it to\n * the agent via the tool result only.\n */\nexport interface EvaluateResult {\n /** The evaluated result value (`returnByValue: true`). */\n value: unknown;\n /** CDP type string of the result (e.g. \"string\", \"number\", \"object\"). */\n type: string;\n}\n\n/**\n * Evaluates an arbitrary JS expression on the attached page via\n * `Runtime.evaluate`. NOT read-only — the expression may have side effects.\n *\n * Throws if the evaluation produced a CDP exception.\n *\n * SECRET-HANDLING: expression and result value are NOT written to any log.\n */\nexport async function evaluate(\n connection: CdpConnection,\n expression: string,\n): Promise<EvaluateResult> {\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: false,\n });\n if (result.exceptionDetails) {\n // Surface only the engine error string — never the expression or result value.\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`evaluate failed: ${msg}`);\n }\n return { value: result.result.value, type: result.result.type };\n}\n\n/* -------------------------------------------------------------------------- */\n/* call_sdk — window.__sdkCall bridge via Runtime.evaluate */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result returned by the `call_sdk` tool.\n * The bridge call wraps success/failure in a JSON envelope so cross-Chii\n * stringification is reliable (same approach as `measure_safe_area`).\n *\n * `recentException` is populated when a `Runtime.exceptionThrown` event was\n * observed within the heuristic triage window [callStart-50ms, callEnd+200ms].\n * This helps correlate an SDK throw with the bridge result, especially when\n * the SDK throws synchronously before the promise resolves.\n */\nexport type CallSdkResult =\n | { ok: true; value: unknown; recentException?: BufferedException }\n | { ok: false; error: string; recentException?: BufferedException };\n\n/**\n * Builds the Runtime.evaluate expression that calls `window.__sdkCall` with\n * the given method name and args, awaits the promise, and returns a JSON\n * envelope `{ok, value/error}` as a string.\n *\n * Name and args are embedded via `JSON.stringify` so they are safely escaped.\n * The expression checks for `window.__sdkCall` and returns a clear error if\n * it is absent (non-dog-food bundle).\n *\n * SECRET-HANDLING: the expression is built here and MUST NOT be written to\n * any log or stderr by the caller.\n */\nexport function buildCallSdkExpression(name: string, args: unknown[]): string {\n const safeName = JSON.stringify(name);\n const safeArgs = JSON.stringify(args);\n return (\n `(async () => {` +\n ` if (typeof window.__sdkCall !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'sdk-absent: window.__sdkCall이 주입되지 않았습니다 (dog-food 빌드가 아닙니다). dog-food 채널로 재배포하세요.'});` +\n ` }` +\n ` try {` +\n ` const r = await window.__sdkCall(${safeName}, ...${safeArgs});` +\n ` return JSON.stringify({ok:true,value:r});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Parses the JSON envelope string returned by the `call_sdk` expression.\n * Returns a typed `CallSdkResult`.\n *\n * Throws only on parse failure (not on ok:false — that is a normal result).\n */\nexport function normalizeCallSdkResult(rawValue: unknown): CallSdkResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `call_sdk: bridge returned unexpected type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue in the error message — it could contain secrets.\n throw new Error('call_sdk: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('call_sdk: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, value: obj.value };\n }\n if (obj.ok === false) {\n return { ok: false, error: typeof obj.error === 'string' ? obj.error : String(obj.error) };\n }\n throw new Error('call_sdk: bridge result missing \"ok\" field');\n}\n\n/**\n * Looks up the most recent exception from the buffer that falls within the\n * triage window [windowStart, windowEnd]. Returns `undefined` if none found.\n *\n * The heuristic window is:\n * - windowStart = callStart - 50ms (catch sync throws before bridge fires)\n * - windowEnd = callEnd + 200ms (catch async throws resolved soon after)\n *\n * Only the most recent exception within the window is returned (the one most\n * likely to be causally related to the SDK call).\n */\nfunction findRecentException(\n connection: CdpConnection,\n windowStart: number,\n windowEnd: number,\n): BufferedException | undefined {\n const events = connection.getBufferedEvents('Runtime.exceptionThrown');\n // Scan from the tail (most recent) to find the closest-in-time exception.\n for (let i = events.length - 1; i >= 0; i--) {\n const e = events[i];\n if (e.timestamp >= windowStart && e.timestamp <= windowEnd) {\n return normalizeException(e);\n }\n }\n return undefined;\n}\n\n/**\n * Calls a dog-food SDK method via `window.__sdkCall` on the attached page.\n * NOT read-only — SDK calls may have side effects.\n *\n * On env 3/4 (toss WebView relay) this hits the real SDK. On env 1 (local\n * mock) and env 2 (PWA relay — real WebKit, mock SDK) it hits the mock SDK.\n *\n * 인자 시그니처 검증: 등록된 메서드는 bridge 호출 전에 인자를 검증하고, mismatch면\n * `{ok:false, error}` MCP 오류 결과를 반환한다(bridge에 도달하지 않음).\n * 미등록 메서드는 passthrough + stderr 경고 1회.\n *\n * Throws on CDP error or result parse failure. Returns `{ok:false, error}`\n * for bridge-level errors (method not found, SDK threw, bridge absent) or\n * argument schema violations.\n *\n * If a `Runtime.exceptionThrown` event was observed within the triage window\n * [callStart-50ms, callEnd+200ms], the result includes `recentException` for\n * crash triage. This window is a heuristic — it catches the common case of an\n * SDK throw immediately before/after the bridge resolves.\n *\n * SECRET-HANDLING: name, args, and the result value are NOT written to any log.\n */\nexport async function callSdk(\n connection: CdpConnection,\n name: string,\n args: unknown[],\n): Promise<CallSdkResult> {\n // 인자 시그니처 검증 — bridge 호출 전에 reject하여 native crash를 예방한다.\n const signature = lookupSignature(name);\n if (signature !== undefined) {\n const validation = signature.validateArgs(args);\n if (!validation.ok) {\n // isError: true 형태로 반환 — bridge에 도달하지 않음.\n const errorText =\n `call_sdk(\"${name}\") 인자 시그니처 오류.\\n` +\n `받음: ${validation.received}\\n` +\n `기대: ${validation.expected}\\n` +\n `올바른 예시: ${signature.example}`;\n return { ok: false, error: errorText };\n }\n } else {\n // 미등록 메서드 — passthrough하지만 stderr에 경고 1회.\n warnPassthrough(name);\n }\n\n const callStart = Date.now();\n const expression = buildCallSdkExpression(name, args);\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\n });\n const callEnd = Date.now();\n\n if (result.exceptionDetails) {\n // Surface only the engine error string — never name, args, or result value.\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`call_sdk threw: ${msg}`);\n }\n\n const sdkResult = normalizeCallSdkResult(result.result.value);\n\n // Triage window: [callStart - 50ms, callEnd + 200ms].\n // -50ms: catches sync throws that fire just before the bridge call is sent.\n // +200ms: catches async throws resolved shortly after the bridge returns.\n const recentException = findRecentException(connection, callStart - 50, callEnd + 200);\n\n if (recentException !== undefined) {\n return { ...sdkResult, recentException };\n }\n return sdkResult;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Phase 3 — AIT.* domain (CDP can't cover these) */\n/* -------------------------------------------------------------------------- */\n\n/** Set of tool names served by the AIT source rather than the CDP connection. */\nconst AIT_TOOL_NAMES = new Set<string>([\n 'AIT.getSdkCallHistory',\n 'AIT.getMockState',\n 'AIT.getOperationalEnvironment',\n]);\n\n/** True for the Phase 3 AIT.* tools (served by an `AitSource`, not CDP). */\nexport function isAitToolName(name: string): boolean {\n return AIT_TOOL_NAMES.has(name);\n}\n\n/** Returns the recent SDK call trace (`AIT.getSdkCallHistory`). */\nexport function getSdkCallHistory(source: AitSource): Promise<AitSdkCallHistory> {\n return source.get('AIT.getSdkCallHistory');\n}\n\n/** Returns the devtools mock-state snapshot (`AIT.getMockState`). */\nexport function getMockState(source: AitSource): Promise<AitMockState> {\n return source.get('AIT.getMockState');\n}\n\n/** Returns the operational environment + SDK version (`AIT.getOperationalEnvironment`). */\nexport function getOperationalEnvironment(source: AitSource): Promise<AitOperationalEnvironment> {\n return source.get('AIT.getOperationalEnvironment');\n}\n\n/* -------------------------------------------------------------------------- */\n/* get_debug_status — single-call server status snapshot (#286) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Represents a single redacted server-side error entry in the diagnostics\n * snapshot. PII / secrets are scrubbed before this is returned.\n */\nexport interface DiagnosticsError {\n /** ISO timestamp when the error was recorded. */\n timestamp: string;\n /** Error message with PII/secrets redacted (e.g. `at=<redacted>`). */\n message: string;\n /** Optional error category for quick triage. */\n category?: string;\n}\n\n/**\n * Tunnel state in the diagnostics snapshot. Same shape as `TunnelStatus` but\n * extended with the lock-file data (pid, startedAt) when available.\n */\nexport interface DiagnosticsTunnelInfo {\n /** Whether the cloudflared quick tunnel is currently up. */\n up: boolean;\n /** Public `wss://*.trycloudflare.com` relay URL, or `null`. */\n wssUrl: string | null;\n /**\n * PID of the MCP server process that owns the tunnel (from the lock file),\n * or `null` when no lock is present.\n */\n pid: number | null;\n /**\n * ISO timestamp when the owning server process started (from the lock file),\n * or `null`.\n */\n startedAt: string | null;\n /**\n * ISO timestamp when the tunnel permanently dropped (health probe exhausted\n * all reissue attempts). `null` when the tunnel has not permanently dropped.\n * When non-null, the MCP server must be restarted to recover.\n */\n droppedAt: string | null;\n /**\n * Number of automatic reissue attempts made before the permanent drop.\n * 0 when no drop has occurred.\n */\n reissueAttempts: number;\n}\n\n/**\n * Server-lock holder info from `~/.ait-devtools/server.lock`. `null` when\n * no lock file exists (server was cleanly shut down or never started).\n */\nexport interface DiagnosticsLockHolder {\n pid: number;\n startedAt: string;\n /** wssUrl recorded in the lock file — may be `null` when tunnel is still starting. */\n wssUrl: string | null;\n}\n\n/**\n * Secret-free snapshot of relay auth rejections (issue #467).\n *\n * Counts WS-upgrade / HTTP 401 rejections from the relay's TOTP gate so an\n * empty `pages` list can be distinguished from \"the phone never reached the\n * relay\". SECRET-HANDLING: count + timestamp ONLY — never the URL, query,\n * code, or secret.\n */\nexport interface AuthRejectsSnapshot {\n /** Total auth-rejected relay requests since server start. */\n count: number;\n /** ISO timestamp of the most recent rejection, or `null` when none. */\n lastAt: string | null;\n}\n\n/**\n * The next recommended tool for the agent to call, based on the current server\n * state snapshot. `null` means the session looks healthy — no specific action needed.\n */\nexport interface NextRecommendedAction {\n /** MCP tool name to call next (e.g. `'build_attach_url'`, `'restart'`). */\n tool: string;\n /** Human-readable reason explaining why this action is recommended. */\n reason: string;\n}\n\n/**\n * Full server status snapshot returned by `get_debug_status`.\n *\n * All fields are nullable — a missing value means \"not yet known\" (e.g. tunnel\n * not up yet) rather than an error. The schema is intentionally stable across\n * versions: new optional fields may be added but existing fields are not\n * removed or renamed.\n *\n * SECRET-HANDLING: No TOTP secret, cookie, deploy key, or `at=` code value\n * appears in this snapshot. `recentErrors` entries are redacted before inclusion.\n */\nexport interface DiagnosticsResult {\n /** `@modelcontextprotocol/sdk` package version string. */\n mcpVersion: string | null;\n /** `@ait-co/devtools` package version string. */\n devtoolsVersion: string | null;\n /** Tunnel state including lock-file pid/startedAt. */\n tunnel: DiagnosticsTunnelInfo;\n /** Current list_pages result (pages + crash info + singleAttachModel). */\n pages: ListPagesResult | null;\n /** ISO timestamp of the most recent page attach, or `null`. */\n lastAttachAt: string | null;\n /** ISO timestamp of the most recent page detach, or `null`. */\n lastDetachAt: string | null;\n /**\n * Recent server-side errors (up to `recent_errors_limit`, default 10).\n * Redacted: `at=<redacted>`, cookie headers stripped, AITCC_API_KEY masked.\n * When auth rejections occurred, ONE synthetic summary entry\n * (`category: 'auth'`) is appended so an empty array can be read as\n * \"no attach attempts\" without missing silent 401s (issue #467).\n */\n recentErrors: DiagnosticsError[];\n /**\n * Relay TOTP auth-reject counter (issue #467). `count` is 0 when no\n * rejection occurred. Secret-free: count + last timestamp only.\n */\n authRejects: AuthRejectsSnapshot;\n /**\n * Resolved environment and the reason string.\n *\n * `kind` — the precise four-value environment (`mock` | `relay-dev` |\n * `relay-live` | `relay-mobile`). Use this for new code.\n * `env` — backward-compat two-value alias (`mock` | `relay`). Kept so\n * existing callers that only distinguish mock vs relay continue to work.\n */\n environment: {\n kind: McpEnvironment;\n /** @deprecated Use `kind` instead. Kept for backward compatibility. */\n env: 'mock' | 'relay';\n reason: string;\n /** `true` when the LIVE side-effect guard is active (`kind === 'relay-live'`). */\n liveGuardActive: boolean;\n };\n /**\n * Contents of `~/.ait-devtools/server.lock`, or `null` when absent.\n * Useful for diagnosing stale-lock conflicts without running the full server.\n */\n serverLockHolder: DiagnosticsLockHolder | null;\n /**\n * Basic process identity for the running MCP server daemon.\n * Useful for diagnosing orphaned daemons and stale parent associations.\n */\n process: {\n /** PID of this MCP server process. */\n pid: number;\n /** Parent PID at the time `get_debug_status` was called. */\n ppid: number;\n /** Whether the parent process is still alive at snapshot time. */\n parentAlive: boolean;\n };\n /**\n * Single next recommended action for the agent, or `null` when the session\n * looks healthy. Derived deterministically from the other snapshot fields —\n * the agent should call this tool next rather than inferring from raw fields.\n *\n * Branch rules (evaluated in priority order):\n * 0. tunnel.droppedAt non-null → restart (permanent tunnel drop)\n * 1. tunnel.up === false AND relay env → restart\n * 1b. tunnel.up === false AND mock env, no pages → wait_for_page (local target is tunnel-less)\n * 2a. authRejects.count > 0 AND pages empty → build_attach_url (TOTP 거부 — QR 재스캔 안내)\n * 2. tunnel.up, pages empty, env === relay → build_attach_url\n * 3. pages[0] exists + crashDetectedAt non-null → build_attach_url (re-attach)\n * 4. otherwise → null\n */\n nextRecommendedAction: NextRecommendedAction | null;\n}\n\n/**\n * Registry of server-side errors collected by `DiagnosticsCollector`.\n * Injected into `createDebugServer` so it is testable without a real process.\n */\nexport interface DiagnosticsCollector {\n /** Records a server-side error for later surfacing in `get_debug_status`. */\n recordError(message: string, category?: string): void;\n /** Returns the most recent `limit` errors, oldest-first. */\n getRecentErrors(limit: number): DiagnosticsError[];\n /** Records an attach event (ISO timestamp stored). */\n recordAttach(): void;\n /** Records a detach event (ISO timestamp stored). */\n recordDetach(): void;\n /** Returns the ISO timestamp of the last attach, or `null`. */\n getLastAttachAt(): string | null;\n /** Returns the ISO timestamp of the last detach, or `null`. */\n getLastDetachAt(): string | null;\n /**\n * Records one relay auth rejection (issue #467). Secret-free by contract —\n * implementations store only a counter + timestamp, never request data.\n */\n recordAuthReject(): void;\n /** Returns the auth-reject counter snapshot ({count: 0, lastAt: null} when none). */\n getAuthRejects(): AuthRejectsSnapshot;\n}\n\n/** Secret-redaction patterns applied before error messages enter the buffer. */\nconst SECRET_REDACT_PATTERNS: ReadonlyArray<[RegExp, string]> = [\n // TOTP at= code value.\n [/\\bat=([^&\\s\"']+)/g, 'at=<redacted>'],\n // Cookie / Set-Cookie header values — replace everything after the colon.\n [/((?:set-)?cookie)\\s*:\\s*.+/gi, '$1: <redacted>'],\n // AITCC_API_KEY env-var-style references.\n [/AITCC_API_KEY\\s*=\\s*\\S+/gi, 'AITCC_API_KEY=<redacted>'],\n // Authorization header (covers \"Authorization: Bearer …\" and bare \"Bearer <token>\").\n [/Authorization\\s*:\\s*.+/gi, 'Authorization: <redacted>'],\n [/\\bBearer\\s+\\S+/g, 'Bearer <redacted>'],\n];\n\n/**\n * Applies all secret-redaction patterns to an error message string.\n * Used before storing errors in the `DiagnosticsCollector` ring buffer.\n *\n * SECRET-HANDLING: this is the single bottleneck for redaction — all error\n * strings must pass through here before reaching the buffer.\n */\nexport function redactErrorMessage(message: string): string {\n let result = message;\n for (const [pattern, replacement] of SECRET_REDACT_PATTERNS) {\n result = result.replace(pattern, replacement);\n }\n return result;\n}\n\n/** Default max buffer size for the error ring buffer. */\nconst DEFAULT_ERROR_BUFFER_SIZE = 50;\n\n/**\n * In-memory implementation of `DiagnosticsCollector`. Thread-safe in the\n * single-threaded Node.js sense (synchronous mutations only).\n */\nexport class InMemoryDiagnosticsCollector implements DiagnosticsCollector {\n private readonly buffer: DiagnosticsError[] = [];\n private readonly maxSize: number;\n private lastAttachAt: string | null = null;\n private lastDetachAt: string | null = null;\n private authRejectCount = 0;\n private lastAuthRejectAt: string | null = null;\n\n constructor(maxSize = DEFAULT_ERROR_BUFFER_SIZE) {\n this.maxSize = maxSize;\n }\n\n recordError(message: string, category?: string): void {\n const entry: DiagnosticsError = {\n timestamp: new Date().toISOString(),\n message: redactErrorMessage(message),\n ...(category !== undefined ? { category } : {}),\n };\n this.buffer.push(entry);\n // Keep only the most recent `maxSize` entries.\n if (this.buffer.length > this.maxSize) {\n this.buffer.shift();\n }\n }\n\n getRecentErrors(limit: number): DiagnosticsError[] {\n const cap = Math.min(Math.max(1, limit), DEFAULT_ERROR_BUFFER_SIZE);\n const sliced =\n this.buffer.length > cap ? this.buffer.slice(this.buffer.length - cap) : [...this.buffer];\n return sliced;\n }\n\n recordAttach(): void {\n this.lastAttachAt = new Date().toISOString();\n }\n\n recordDetach(): void {\n this.lastDetachAt = new Date().toISOString();\n }\n\n getLastAttachAt(): string | null {\n return this.lastAttachAt;\n }\n\n getLastDetachAt(): string | null {\n return this.lastDetachAt;\n }\n\n recordAuthReject(): void {\n this.authRejectCount += 1;\n this.lastAuthRejectAt = new Date().toISOString();\n }\n\n getAuthRejects(): AuthRejectsSnapshot {\n return { count: this.authRejectCount, lastAt: this.lastAuthRejectAt };\n }\n}\n\n/**\n * Returns the `@modelcontextprotocol/sdk` version baked in at build time via\n * the `__MCP_SDK_VERSION__` define (see `tsdown.config.ts`). Returns `null`\n * when the define is absent (unbundled test runs) and the runtime fallback\n * below also fails — diagnostics must never throw.\n *\n * Earlier attempts resolved `@modelcontextprotocol/sdk/package.json` (not in\n * the SDK `exports` map → `ERR_PACKAGE_PATH_NOT_EXPORTED`) or the bare\n * `@modelcontextprotocol/sdk` main entry (also absent → `MODULE_NOT_FOUND`),\n * so both this fallback AND the build-time define silently produced `null` —\n * leaving `mcpVersion: null` in a real bundle (issue #361, observed live). The\n * fix resolves a subpath that IS exported (`./server/mcp.js`) and walks back to\n * the package root, in BOTH the build define and this fallback.\n *\n * Kept `async` for call-site compatibility (`Promise.all` at the caller); the\n * body is synchronous apart from the best-effort fallback.\n */\nexport async function readMcpSdkVersion(): Promise<string | null> {\n // Primary: build-time define (bare identifier, substituted by tsdown).\n if (typeof __MCP_SDK_VERSION__ === 'string' && __MCP_SDK_VERSION__.length > 0) {\n return __MCP_SDK_VERSION__;\n }\n // Fallback for unbundled runs (the define never ran): resolve an EXPORTED\n // subpath (`./server/mcp.js`) and read the sibling package.json by path —\n // bypassing the `exports` gate that blocks both the `/package.json` subpath\n // and the bare main entry.\n try {\n const { createRequire } = await import('node:module');\n const req = createRequire(import.meta.url);\n const entry = req.resolve('@modelcontextprotocol/sdk/server/mcp.js');\n const marker = '@modelcontextprotocol/sdk';\n const root = entry.slice(0, entry.indexOf(marker) + marker.length);\n const { readFileSync } = await import('node:fs');\n const raw = readFileSync(`${root}/package.json`, 'utf8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n return typeof parsed.version === 'string' ? parsed.version : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Returns the `@ait-co/devtools` package version injected at build time via\n * the `__VERSION__` define. Returns `null` when the global is absent (e.g. in\n * some test environments that skip the build step).\n */\nexport function readDevtoolsVersion(): string | null {\n // `__VERSION__` is a bare identifier replaced at build time by the tsdown\n // `define` (see `tsdown.config.ts`) — the SAME mechanism `debug-server.ts`\n // and `server.ts` use for the MCP server `version`. It must be referenced as\n // a bare identifier, not `globalThis.__VERSION__`: `define` only substitutes\n // the bare token, so the property access always read `undefined` and this\n // function always returned `null` in a real bundle (issue #361). The\n // `typeof` guard keeps it null-safe in unbundled test runs where the define\n // never ran.\n return typeof __VERSION__ === 'string' && __VERSION__.length > 0 ? __VERSION__ : null;\n}\n\n/**\n * Derives the next recommended action from a completed diagnostics snapshot.\n *\n * Branch rules (evaluated in priority order):\n * 0. tunnel.droppedAt non-null → restart (permanent tunnel drop — highest priority)\n * 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)\n * 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)\n * 2a. authRejects.count > 0 AND pages empty → build_attach_url (relay TOTP 거부 관측 — QR 재스캔\n * 또는 target-side `at` 전달 확인. 일반 rule 2보다 구체적이므로 먼저 평가 — issue #467)\n * 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)\n * 3. pages has entry + crashDetectedAt non-null → build_attach_url (re-attach after crash)\n * 4. otherwise → null (session looks healthy)\n *\n * Pure — does not throw; receives the final assembled snapshot fields.\n *\n * SECRET-HANDLING: the auth-reject reason string carries only the count and\n * timestamp from {@link AuthRejectsSnapshot} — never a URL, code, or secret.\n */\nexport function computeNextRecommendedAction(\n tunnel: DiagnosticsTunnelInfo,\n pages: ListPagesResult | null,\n env: McpEnvironment,\n authRejects: AuthRejectsSnapshot | null = null,\n): NextRecommendedAction | null {\n // Rule 0: permanent tunnel drop — highest priority, beats crash / empty-pages rules.\n // droppedAt is set by the health probe after exhausting all reissue attempts.\n if (tunnel.droppedAt != null) {\n return {\n tool: 'restart',\n reason:\n `tunnel permanently dropped at ${tunnel.droppedAt} after ${tunnel.reissueAttempts} reissue attempt(s) — ` +\n 'restart the MCP server (npx @ait-co/devtools devtools-mcp)',\n };\n }\n\n // Rule 1: tunnel is down.\n if (!tunnel.up) {\n // Rule 1b: local-target (mock env) runs without a relay tunnel by design —\n // tunnel.up === false is the expected steady state. Instead of recommending\n // a server restart, guide the agent to wait for the page to load.\n if (!isRelayEnv(env)) {\n // Only surface wait_for_page when no page is attached yet; once a page\n // attaches the session is healthy and null is the correct return value.\n if (pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) {\n return {\n tool: 'wait_for_page',\n reason:\n 'local Chromium spawn 직후 — 페이지 로드를 기다리거나 list_pages를 재호출하세요 ' +\n '(local 모드는 tunnel이 없는 게 정상입니다)',\n };\n }\n // Page already attached or crash detected — fall through to other rules.\n } else {\n // Rule 1 (relay env): tunnel must be up for relay to work — restart.\n return {\n tool: 'restart',\n reason: 'tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart',\n };\n }\n }\n\n // Rule 2a (issue #467): auth rejections observed while no page is attached —\n // the phone DID reach the relay but its TOTP verification failed. Without\n // this rule the generic rule 2 (\"call build_attach_url\") hides the rejection\n // and the diagnosis runs the wrong way (\"the phone never arrived\").\n if (authRejects !== null && authRejects.count > 0 && pages !== null && pages.pages.length === 0) {\n return {\n tool: 'build_attach_url',\n reason:\n `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? 'unknown'}) — ` +\n 'QR을 다시 스캔해 새 코드로 attach하세요(코드는 ~3분마다 만료). 반복되면 폰 페이지 URL에 ' +\n 'at 파라미터가 전달되는지(target-side TOTP 전달 경로)를 확인하세요',\n };\n }\n\n // Rule 2: tunnel up but no pages attached in relay env → start attach.\n if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) {\n return {\n tool: 'build_attach_url',\n reason: 'tunnel ready, no pages attached — call build_attach_url to generate the attach QR',\n };\n }\n\n // Rule 3: crash detected — need to re-attach.\n if (pages !== null && pages.crashDetectedAt !== null) {\n return {\n tool: 'build_attach_url',\n reason: `page crashed at ${pages.crashDetectedAt} — call build_attach_url to re-attach`,\n };\n }\n\n // Rule 4: session looks healthy.\n return null;\n}\n\n/** Input for `getDiagnostics`. */\nexport interface GetDiagnosticsInput {\n /** Current tunnel status (from the server's live `getTunnelStatus()`). */\n tunnel: TunnelStatus;\n /**\n * CDP connection used to call `list_pages` — may be absent in edge cases\n * (e.g. called from the dev-mode server which has no CDP connection).\n */\n connection?: CdpConnection;\n /**\n * Resolved MCP environment (`mock` | `relay-dev` | `relay-live` |\n * `relay-mobile`). Caller obtains via `resolveEnvironment()`.\n */\n env: McpEnvironment;\n /** Human-readable reason for the env decision. */\n envReason: string;\n /** Diagnostics collector for errors / attach events. */\n collector: DiagnosticsCollector;\n /** Lock-file reader — injected so tests can override without touching the FS. */\n readLock: () => import('./server-lock.js').LockData | null;\n /** Maximum number of recent errors to include (default 10). */\n recentErrorsLimit?: number;\n /** Optional async resolver for the MCP SDK version. */\n getMcpVersion?: () => Promise<string | null>;\n /**\n * Injectable parent-alive check for testability.\n * Defaults to `() => isPidAlive(process.ppid)` in production.\n */\n checkParentAlive?: () => boolean;\n /**\n * PID of the cloudflared child process — obtained from `QuickTunnel.childPid`\n * and written to the lock file via `LockHandle.updateTunnelChildPid`.\n *\n * FIX 2 (issue #571): when this PID is known, `getDiagnostics` performs a\n * live `isPidAlive(tunnelChildPid)` check and overrides `tunnel.up = false`\n * if the child is dead, preventing the cached `up: true` from being reported\n * as truth when the cloudflared process has already exited.\n */\n tunnelChildPid?: number | null;\n}\n\n/**\n * Builds the `get_debug_status` response. Pure — does not throw; missing data\n * fields are `null`. Async because `readMcpSdkVersion` needs `import()`.\n *\n * SECRET-HANDLING:\n * - `recentErrors` messages are already redacted by `recordError` (via\n * `redactErrorMessage`). No additional redaction needed here.\n * - `tunnel.wssUrl` is a public cloudflared hostname — not a secret.\n * - Lock file data contains only pid + startedAt + wssUrl — no secrets.\n */\nexport async function getDiagnostics(input: GetDiagnosticsInput): Promise<DiagnosticsResult> {\n const {\n tunnel,\n connection,\n env,\n envReason,\n collector,\n readLock: readLockFn,\n recentErrorsLimit = 10,\n getMcpVersion = readMcpSdkVersion,\n checkParentAlive = () => isPidAlive(process.ppid),\n tunnelChildPid,\n } = input;\n\n const [mcpVersion, devtoolsVersion] = await Promise.all([\n getMcpVersion(),\n Promise.resolve(readDevtoolsVersion()),\n ]);\n\n // Read lock file for serverLockHolder + tunnel pid/startedAt.\n const lockData = readLockFn();\n const serverLockHolder: DiagnosticsLockHolder | null = lockData\n ? { pid: lockData.pid, startedAt: lockData.startedAt, wssUrl: lockData.wssUrl }\n : null;\n\n // FIX 2 (issue #571): if the cloudflared child PID is known, perform a live\n // probe to detect child death even when the cached `tunnel.up` is still true.\n // This prevents the 2d17h zombie scenario where the process died but the cache\n // was never invalidated.\n //\n // Source priority: explicit `tunnelChildPid` arg (in-memory, always current) →\n // lock file's `tunnelChildPid` (populated by FIX 3 via onTunnelChildPid) →\n // null (no probe). The lock-file fallback ensures the check fires even when the\n // handler didn't pass the in-memory PID explicitly (issue #572 review).\n const effectiveTunnelChildPid = tunnelChildPid ?? lockData?.tunnelChildPid ?? null;\n let effectiveUp = tunnel.up;\n if (\n tunnel.up &&\n typeof effectiveTunnelChildPid === 'number' &&\n effectiveTunnelChildPid !== null &&\n !isPidAlive(effectiveTunnelChildPid)\n ) {\n effectiveUp = false;\n }\n\n const tunnelInfo: DiagnosticsTunnelInfo = {\n up: effectiveUp,\n wssUrl: tunnel.wssUrl,\n pid: lockData?.pid ?? null,\n startedAt: lockData?.startedAt ?? null,\n droppedAt: tunnel.droppedAt ?? null,\n reissueAttempts: tunnel.reissueAttempts ?? 0,\n };\n\n // list_pages — non-fatal; null on any error.\n // Refresh from relay first (#551 — stale cache causes pages:0 / wrong\n // nextRecommendedAction even when a target is attached). Same best-effort\n // pattern as the list_pages handler: errors are silently ignored so the\n // caller always gets *something* back, even when the relay is unreachable.\n let pages: ListPagesResult | null = null;\n if (connection !== undefined) {\n try {\n await connection.refreshTargets?.();\n } catch {\n // Ignore refresh errors — continue with cached state.\n }\n try {\n pages = listPages(connection, tunnel);\n } catch {\n // Ignore — pages stays null.\n }\n }\n\n const limit = Math.min(Math.max(1, recentErrorsLimit), 50);\n const recentErrors = collector.getRecentErrors(limit);\n\n // Issue #467: surface relay auth rejections. One synthetic summary entry\n // (not N entries — rejections can be frequent) so \"recentErrors: []\" can be\n // read as \"no attach attempts\" without hiding silent 401s.\n // SECRET-HANDLING: message carries only count + timestamp.\n const authRejects = collector.getAuthRejects();\n if (authRejects.count > 0) {\n recentErrors.push({\n timestamp: authRejects.lastAt ?? new Date().toISOString(),\n message: `WS upgrade auth-rejected (${authRejects.count} times, last ${authRejects.lastAt ?? 'unknown'})`,\n category: 'auth',\n });\n }\n\n const nextRecommendedAction = computeNextRecommendedAction(tunnelInfo, pages, env, authRejects);\n\n return {\n mcpVersion,\n devtoolsVersion,\n tunnel: tunnelInfo,\n pages,\n lastAttachAt: collector.getLastAttachAt(),\n lastDetachAt: collector.getLastDetachAt(),\n recentErrors,\n authRejects,\n environment: {\n kind: env,\n env: toLegacyEnv(env),\n reason: envReason,\n liveGuardActive: isLiveRelayEnv(env),\n },\n serverLockHolder,\n process: {\n pid: process.pid,\n ppid: process.ppid,\n parentAlive: checkParentAlive(),\n },\n nextRecommendedAction,\n };\n}\n","/**\n * cloudflared quick tunnel + attach banner for the debug-mode MCP server.\n *\n * On spawn, the debug server opens an accountless `*.trycloudflare.com` quick\n * tunnel to the local Chii relay so the phone can attach over a public wss URL,\n * then prints a unicode half-block QR + attach instructions. When TOTP auth is\n * enabled (`AIT_DEBUG_TOTP_SECRET` is set), the QR encodes only the base relay\n * URL — the TOTP code (`at=`) is NOT included because it rotates every 30 s\n * and would be stale by the time a human scans. The in-app deep-link builder\n * splices the live code at attach time.\n *\n * Tunnel health probe (`TunnelHealthProbe`):\n * After the tunnel is up, a periodic HTTP HEAD probe hits the tunnel's\n * `https://` URL every `probeIntervalMs` (default 60 s). Two consecutive\n * failures trigger a reissue attempt (spawn a new cloudflared quick tunnel\n * and redirect traffic). After `MAX_REISSUE_ATTEMPTS` (3) consecutive\n * reissue failures, the probe gives up and marks the tunnel permanently\n * dropped — `tunnelStatus.up` becomes false with `droppedAt` set. The caller\n * should surface this to the agent so the user knows to restart the server.\n *\n * SECRET-HANDLING: The TOTP secret and computed code values MUST NOT appear\n * in any output from this module.\n *\n * Node-only: spawns the cloudflared binary and writes to stdout/stderr.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { bin, install, Tunnel } from 'cloudflared';\nimport type { TunnelStatus } from './tools.js';\n\n/** Generates a 32-byte hex attach token shown as a pairing hint (relay-side validation is a later phase). */\nexport function generateAttachToken(): string {\n return randomBytes(32).toString('hex');\n}\n\nexport interface QuickTunnel {\n /** Public `https://*.trycloudflare.com` URL the tunnel exposes. */\n url: string;\n /** Same host as `wss://` — the relay endpoint the phone attaches to. */\n wssUrl: string;\n /**\n * PID of the cloudflared child process. Present once the tunnel is up.\n * Safe to surface in diagnostics (plain integer — not a secret).\n */\n childPid?: number;\n /**\n * Register a callback to be invoked when the cloudflared child exits\n * unexpectedly (i.e. NOT due to our own `stop()` call). The caller\n * (`startTunnelHealthProbe`) uses this to immediately trigger reissue\n * without waiting for the next probe interval.\n *\n * Only one callback can be registered at a time; calling this again\n * replaces the previous one.\n */\n onUnexpectedExit(cb: (code: number | null) => void): void;\n stop(): void;\n}\n\n/** Ensures the cloudflared binary is installed (downloads + caches on first run). */\nasync function ensureCloudflaredBin(): Promise<void> {\n const { existsSync } = await import('node:fs');\n if (!existsSync(bin)) {\n await install(bin);\n }\n}\n\n/**\n * Opens a cloudflared quick tunnel to the local relay port and resolves once\n * the public URL is assigned.\n *\n * FIX 1 (issue #571): after URL resolution the returned `QuickTunnel` object\n * watches the cloudflared child process for unexpected exits and calls any\n * registered `onUnexpectedExit` callback so the health probe can immediately\n * trigger reissue instead of waiting for the next poll interval.\n */\nexport async function startQuickTunnel(localPort: number): Promise<QuickTunnel> {\n await ensureCloudflaredBin();\n\n const tunnel = Tunnel.quick(`http://127.0.0.1:${localPort}`);\n\n const url = await new Promise<string>((resolve, reject) => {\n const onUrl = (assigned: string) => {\n cleanup();\n resolve(assigned);\n };\n const onError = (err: Error) => {\n cleanup();\n reject(err);\n };\n const onExit = (code: number | null) => {\n cleanup();\n reject(new Error(`cloudflared exited before assigning a URL (code ${code})`));\n };\n const cleanup = () => {\n tunnel.off('url', onUrl);\n tunnel.off('error', onError);\n tunnel.off('exit', onExit);\n };\n tunnel.once('url', onUrl);\n tunnel.once('error', onError);\n tunnel.once('exit', onExit);\n });\n\n // FIX 1: watch for unexpected child death AFTER URL is resolved.\n // `intentionalStop` guards against triggering a reissue when we called stop() ourselves\n // (cloudflared exits on SIGINT from tunnel.stop(), which would otherwise look like a crash).\n let intentionalStop = false;\n let unexpectedExitCb: ((code: number | null) => void) | null = null;\n\n tunnel.once('exit', (code: number | null) => {\n if (!intentionalStop && unexpectedExitCb !== null) {\n unexpectedExitCb(code);\n }\n });\n\n return {\n url,\n wssUrl: url.replace(/^https/, 'wss'),\n childPid: (tunnel.process as { pid?: number } | null)?.pid,\n onUnexpectedExit(cb: (code: number | null) => void): void {\n unexpectedExitCb = cb;\n },\n stop(): void {\n intentionalStop = true;\n tunnel.stop();\n },\n };\n}\n\nexport interface AttachBannerInput {\n wssUrl: string;\n /**\n * Whether TOTP auth is enabled on the relay (`AIT_DEBUG_TOTP_SECRET` is set).\n *\n * When `true`, the banner notes that a rotating code (`at=`) will be\n * appended to attach URLs at call time — the code is NOT printed here\n * because it rotates every 30 s and would be stale in seconds.\n */\n totpEnabled: boolean;\n}\n\n/**\n * Renders a pure unicode half-block QR string for the given text.\n *\n * Uses `qrcode` (Node full lib) to get the raw bit matrix, then encodes every\n * two vertical modules into a single half-block character:\n * - both dark → `█`\n * - top only → `▀`\n * - bottom only → `▄`\n * - both light → ` ` (space)\n *\n * The output contains **zero ANSI escape codes**, so it renders correctly in\n * every surface (terminal, VS Code, JetBrains, web) and can be scanned by a\n * phone camera when shown verbatim in an agent response.\n *\n * Shared by `renderAttachBanner` (relay wssUrl QR) and the `build_attach_url`\n * MCP tool response (attach deep-link QR).\n */\nexport async function renderQr(text: string): Promise<string> {\n // Dynamic import mirrors the cloudflared/qrcode-terminal precedent: keeps the\n // dependency out of the module graph when the function is not called.\n const { default: QRCode } = await import('qrcode');\n const qr = QRCode.create(text, { errorCorrectionLevel: 'M' });\n const size: number = qr.modules.size;\n const data: Uint8Array = qr.modules.data as Uint8Array;\n\n const isDark = (x: number, y: number): boolean => {\n if (x < 0 || y < 0 || x >= size || y >= size) return false;\n return data[y * size + x] === 1;\n };\n\n const QUIET = 1;\n const lines: string[] = [];\n for (let y = -QUIET; y < size + QUIET; y += 2) {\n let line = '';\n for (let x = -QUIET; x < size + QUIET; x++) {\n const top = isDark(x, y);\n const bot = isDark(x, y + 1);\n line += top && bot ? '█' : top ? '▀' : bot ? '▄' : ' ';\n }\n lines.push(line);\n }\n return `${lines.join('\\n')}\\n`;\n}\n\n/**\n * Renders the attach banner (relay URL + ASCII QR) as a string.\n *\n * The QR encodes the base `wssUrl` only. When `totpEnabled` is true, a note\n * is added that attach URLs generated by `build_attach_url` will include a\n * live TOTP code (`at=`) appended at call time.\n *\n * SECRET-HANDLING: no secret value, TOTP code, or intermediate value is\n * included in this output.\n */\nexport async function renderAttachBanner(input: AttachBannerInput): Promise<string> {\n // The QR encodes only the relay wssUrl — no token or code. This is safe\n // because the relay gate enforces the code at WS upgrade time anyway; the\n // QR is just for locating the relay, not for bypassing auth.\n const qr = await renderQr(input.wssUrl);\n\n const authNote = input.totpEnabled\n ? ' auth: TOTP enabled — attach URLs include a rotating code (at=).'\n : ' auth: none (set AIT_DEBUG_TOTP_SECRET to enable TOTP).';\n\n return [\n '',\n 'AIT debug — attach a mini-app to this session',\n '',\n ` relay (wss): ${input.wssUrl}`,\n authNote,\n '',\n ' Use build_attach_url to generate a deep link with the current TOTP code.',\n ' Scan the QR to locate the relay (open the dog-food URL separately with',\n ' ?debug=1&relay=<wss>&at=<code> or use the build_attach_url tool):',\n '',\n qr,\n ].join('\\n');\n}\n\n/** Prints the attach banner to stderr (stdout is the MCP stdio channel). */\nexport async function printAttachBanner(input: AttachBannerInput): Promise<void> {\n const banner = await renderAttachBanner(input);\n process.stderr.write(`${banner}\\n`);\n}\n\n/* -------------------------------------------------------------------------- */\n/* TunnelHealthProbe — periodic health check + auto-reissue */\n/* -------------------------------------------------------------------------- */\n\n/** Maximum consecutive reissue attempts before the probe gives up. */\nexport const MAX_REISSUE_ATTEMPTS = 3;\n\n/**\n * Probes `https://` URL with an HTTP HEAD request.\n * Returns `true` when the server responds (any HTTP status), `false` on\n * network error or timeout.\n *\n * We treat any HTTP response (including 4xx/5xx) as \"tunnel alive\" because\n * cloudflared itself responds to the HEAD — if the tunnel process died, the\n * request fails at the network level rather than returning a status code.\n *\n * @param httpsUrl - The `https://` tunnel URL to probe.\n * @param timeoutMs - Abort timeout in ms. Default 10 000.\n */\nexport async function probeTunnel(httpsUrl: string, timeoutMs = 10_000): Promise<boolean> {\n const { default: https } = await import('node:https');\n return new Promise<boolean>((resolve) => {\n const url = new URL(httpsUrl);\n const timer = setTimeout(() => {\n req.destroy();\n resolve(false);\n }, timeoutMs);\n\n const req = https.request(\n { hostname: url.hostname, port: 443, path: url.pathname || '/', method: 'HEAD' },\n (_res) => {\n clearTimeout(timer);\n _res.resume(); // drain response body to free socket\n resolve(true);\n },\n );\n req.on('error', () => {\n clearTimeout(timer);\n resolve(false);\n });\n req.end();\n });\n}\n\nexport interface TunnelHealthProbeOptions {\n /**\n * Interval in ms between health probes. Default 60 000 (60 s).\n * Use a smaller value in tests.\n */\n probeIntervalMs?: number;\n /**\n * How many consecutive probe failures to tolerate before triggering a\n * reissue. Default 2 (so one transient network hiccup is forgiven).\n */\n failuresBeforeReissue?: number;\n /**\n * Callback invoked after a successful reissue. The caller (debug-server)\n * uses this to update `tunnelStatus` and reprint the attach banner with the\n * new `wssUrl`.\n */\n onReissue: (newTunnel: QuickTunnel) => void;\n /**\n * Callback invoked when the probe permanently gives up (all reissue attempts\n * exhausted). The caller should mark `tunnelStatus.up = false` and surface\n * the error to the agent / user.\n */\n onPermanentDrop: (droppedAt: string) => void;\n /**\n * Optional stderr-compatible logger. Default `process.stderr.write`.\n * Injected in tests to avoid real I/O.\n */\n log?: (msg: string) => void;\n /**\n * Optional probe function override (for tests — avoids real HTTP requests).\n */\n probe?: (httpsUrl: string) => Promise<boolean>;\n /**\n * Optional tunnel spawner override (for tests — avoids real cloudflared).\n */\n spawnTunnel?: (localPort: number) => Promise<QuickTunnel>;\n}\n\n/**\n * Starts a periodic health probe for a cloudflared quick tunnel.\n *\n * Every `probeIntervalMs` the probe sends an HTTP HEAD request to the tunnel's\n * `https://` URL. When `failuresBeforeReissue` consecutive failures are\n * detected, it attempts to spawn a new tunnel (up to `MAX_REISSUE_ATTEMPTS`\n * times). On success the caller is notified via `onReissue`; on permanent\n * failure via `onPermanentDrop`.\n *\n * FIX 1 (issue #571): the probe also subscribes to each tunnel's\n * `onUnexpectedExit` callback to detect child death *immediately* instead of\n * waiting for the next probe interval (which could be 60 s away).\n *\n * @returns `stop` — call during server shutdown to clear the probe interval.\n */\nexport function startTunnelHealthProbe(\n initialTunnel: QuickTunnel,\n localPort: number,\n options: TunnelHealthProbeOptions,\n): { stop(): void } {\n const {\n probeIntervalMs = 60_000,\n failuresBeforeReissue = 2,\n onReissue,\n onPermanentDrop,\n log = (msg: string) => process.stderr.write(msg),\n probe = probeTunnel,\n spawnTunnel = startQuickTunnel,\n } = options;\n\n let currentTunnel = initialTunnel;\n let consecutiveFailures = 0;\n let reissueAttempts = 0;\n let stopped = false;\n\n // FIX 1: shared reissue-or-drop logic — called both from the periodic\n // interval (after failuresBeforeReissue consecutive probe misses) and from\n // the child-exit handler (immediately on unexpected process death).\n const doReissueOrDrop = async (): Promise<void> => {\n if (stopped) return;\n\n reissueAttempts += 1;\n if (reissueAttempts > MAX_REISSUE_ATTEMPTS) {\n // Already exhausted — do not log again.\n return;\n }\n\n log(\n `[ait-debug] tunnel drop detected — reissuing (attempt ${reissueAttempts}/${MAX_REISSUE_ATTEMPTS})\\n`,\n );\n\n try {\n const newTunnel = await spawnTunnel(localPort);\n // Stop the old tunnel process to free system resources.\n try {\n currentTunnel.stop();\n } catch {\n // Ignore stop errors — the process may already be dead.\n }\n currentTunnel = newTunnel;\n consecutiveFailures = 0;\n // FIX 1: arm child-exit watcher on the newly spawned tunnel too.\n armChildExitWatch(newTunnel);\n log(`[ait-debug] tunnel reissued — new relay: ${newTunnel.wssUrl}\\n`);\n onReissue(newTunnel);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n log(`[ait-debug] tunnel reissue attempt ${reissueAttempts} failed: ${message}\\n`);\n\n if (reissueAttempts >= MAX_REISSUE_ATTEMPTS) {\n clearInterval(handle);\n stopped = true;\n const droppedAt = new Date().toISOString();\n log(\n `[ait-debug] tunnel permanently dropped after ${MAX_REISSUE_ATTEMPTS} reissue attempts — ` +\n 'restart the debug server to continue (npx @ait-co/devtools devtools-mcp).\\n',\n );\n onPermanentDrop(droppedAt);\n }\n }\n };\n\n // FIX 1: register exit watcher on a QuickTunnel so unexpected child death\n // immediately kicks off reissue without waiting for the probe interval.\n const armChildExitWatch = (t: QuickTunnel): void => {\n t.onUnexpectedExit((code) => {\n if (stopped) return;\n log(\n `[ait-debug] cloudflared child exited unexpectedly (code=${code}) — triggering immediate reissue\\n`,\n );\n // Set failures to threshold so the next interval probe also sees a clean\n // state; the actual reissue happens immediately below.\n consecutiveFailures = failuresBeforeReissue;\n void doReissueOrDrop();\n });\n };\n\n // Arm the watcher on the initial tunnel.\n armChildExitWatch(initialTunnel);\n\n const handle = setInterval(() => {\n void (async () => {\n if (stopped) return;\n\n const httpsUrl = currentTunnel.url;\n const alive = await probe(httpsUrl);\n\n if (alive) {\n // Tunnel responded — reset failure counter.\n if (consecutiveFailures > 0) {\n log('[ait-debug] tunnel health probe: tunnel recovered\\n');\n }\n consecutiveFailures = 0;\n reissueAttempts = 0;\n return;\n }\n\n consecutiveFailures += 1;\n log(\n `[ait-debug] tunnel health probe: failure ${consecutiveFailures}/${failuresBeforeReissue} (url=${httpsUrl})\\n`,\n );\n\n if (consecutiveFailures < failuresBeforeReissue) {\n // Tolerate transient failures — wait for the next interval.\n return;\n }\n\n // Threshold reached — attempt reissue.\n await doReissueOrDrop();\n })();\n }, probeIntervalMs);\n\n return {\n stop() {\n stopped = true;\n clearInterval(handle);\n },\n };\n}\n\n/**\n * Builds a `TunnelStatus` snapshot that includes drop state.\n *\n * Convenience helper for callers (debug-server) that maintain a mutable\n * `tunnelStatus` object — keeps the shape construction in one place.\n */\nexport function makeTunnelStatus(\n up: boolean,\n wssUrl: string | null,\n droppedAt: string | null = null,\n reissueAttempts = 0,\n): TunnelStatus {\n return { up, wssUrl, droppedAt, reissueAttempts };\n}\n","/**\n * @ait-co/devtools debug-mode MCP server (stdio).\n *\n * Lets an AI coding agent attach to a running mini-app (real Toss WebView, or a\n * browser in dev mode) and read its console/network/DOM/screenshot over CDP plus\n * the AIT.* domain, without a human watching a phone. Transport is CDP-via-Chii:\n * a local Chii relay on an OS-assigned port (default 0) exposed through a\n * cloudflared quick tunnel; the phone attaches over the public wss URL.\n *\n * AI host --stdio--> this server --CDP client WS--> Chii relay :<OS-port>\n * ^-- target WS -- phone\n *\n * Port 0 (default): the OS picks a free ephemeral port on every startup.\n * This prevents EADDRINUSE when a stale cloudflared child (orphaned after\n * SIGKILL, PPID 1) still holds a fixed port — which previously caused the MCP\n * handshake to fail with -32000. With port 0 any orphaned cloudflared is\n * harmless; the new relay always gets a fresh port.\n *\n * Best-effort child cleanup: SIGINT/SIGTERM/SIGHUP handlers call shutdown() to\n * stop cloudflared and the relay. uncaughtException/unhandledRejection also\n * call shutdown() before exit. SIGKILL cannot be intercepted by Node, so\n * cloudflared orphans from SIGKILL remain (port 0 makes them harmless). Users\n * can clean up manually: `pkill -f 'cloudflared.*trycloudflare'`.\n *\n * The tool layer reads from an injectable `CdpConnection` (CDP) and `AitSource`\n * (AIT.*), so every tool is unit-testable with a fake (no phone). This module\n * wires the live pieces (relay + tunnel + production connection); the phone\n * roundtrip is fully wired and pending only on-device acceptance.\n *\n * Dynamic tool registration (issue #208):\n * The server advertises `listChanged: true` so MCP clients can subscribe to\n * `notifications/tools/list_changed`. Before any page attaches, only bootstrap\n * tools (`build_attach_url`, `list_pages`) are listed. Once a target appears,\n * the full attach-dependent tool set is added and a `list_changed` notification\n * is sent — without requiring a session restart. `runDebugServer` and\n * `runLocalDebugServer` start a polling watcher that detects the 0→N target\n * transition and calls `server.sendToolListChanged()`.\n *\n * Note: `src/mcp/server.ts` (dev mode, HTTP mock-state) is NOT subject to this\n * model — it has no attach concept and always exposes the full tool surface.\n *\n * Node-only.\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { startMaxAgeWatchdog, startParentWatcher } from '../shared/parent-watcher.js';\nimport { ChiiAitSource } from './ait-chii-source.js';\nimport type { AitSource } from './ait-source.js';\nimport type { CdpConnection } from './cdp-connection.js';\nimport { ChiiCdpConnection } from './chii-connection.js';\nimport { startChiiRelay } from './chii-relay.js';\nimport { buildDeepLinkAttachUrl, buildLauncherAttachUrl } from './deeplink.js';\nimport { AutoDevtoolsOpener, buildChiiInspectorUrl } from './devtools-opener.js';\nimport { wrapEnvelope } from './envelope.js';\nimport {\n deriveEnvironment,\n getLiveIntent,\n type McpEnvironment,\n type RelayOrigin,\n setLiveIntent,\n} from './environment.js';\nimport {\n classifyToolError,\n liveGuardError,\n mcpError,\n pageCrashError,\n pageMissingError,\n relayDisconnectError,\n sdkAbsentError,\n tierRejectionError,\n} from './errors.js';\nimport { LocalCdpConnection } from './local-connection.js';\nimport { launchChromium } from './local-launcher.js';\nimport { logError, logInfo, logWarn } from './log.js';\nimport {\n type DashboardState,\n type QrHttpServer,\n type QrHttpServerOptions,\n startQrHttpServer,\n} from './qr-http-server.js';\nimport { loadRelaySecretReadOnly } from './relay-secret-store.js';\nimport { acquireLock, readServerLock } from './server-lock.js';\nimport {\n BOOTSTRAP_TOOL_NAMES,\n buildAttachUrl,\n callSdk,\n canOpenBrowser,\n DEBUG_TOOL_DEFINITIONS,\n type DiagnosticsCollector,\n evaluate,\n filterToolsByEnvironment,\n getDiagnostics,\n getDomDocument,\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n getToolAvailability,\n InMemoryDiagnosticsCollector,\n isAitToolName,\n isDebugToolName,\n isToolAvailableIn,\n listConsoleMessages,\n listExceptions,\n listNetworkRequests,\n listPages,\n measureSafeArea,\n openQrInBrowser,\n type TunnelStatus,\n takeScreenshot,\n takeSnapshot,\n} from './tools.js';\nimport {\n assertRelayAuthConfigured,\n buildRelayVerifyAuth,\n generateTotp,\n RELAY_VERIFY_SKEW_STEPS,\n} from './totp.js';\n\nexport { startMaxAgeWatchdog, startParentWatcher } from '../shared/parent-watcher.js';\n\nimport {\n generateAttachToken,\n makeTunnelStatus,\n printAttachBanner,\n type QuickTunnel,\n renderQr,\n startQuickTunnel,\n startTunnelHealthProbe,\n} from './tunnel.js';\n\n/**\n * Parses `_deploymentId` from the query string of a scheme URL.\n *\n * Returns `null` when the param is absent or empty — callers treat that as\n * \"no deploymentId filter; match on presence only\" and fall back to the\n * original `attachedPages.length > 0` condition.\n *\n * SECRET-HANDLING: deploymentId is a public identifier and may appear in\n * debug output. Never confuse it with TOTP secrets or relay tunnel URLs.\n */\nexport function extractDeploymentId(schemeUrl: string): string | null {\n try {\n // scheme URLs like `intoss-private://host?_deploymentId=xxx` are not\n // parseable by `new URL()` in all environments, so we extract the query\n // string manually.\n const qIndex = schemeUrl.indexOf('?');\n if (qIndex === -1) return null;\n const params = new URLSearchParams(schemeUrl.slice(qIndex + 1));\n const id = params.get('_deploymentId');\n return id && id.length > 0 ? id : null;\n } catch {\n return null;\n }\n}\n\n/**\n * The result of a `start_debug` mode switch (issue #348). Reported back to the\n * agent so it knows the active mode, whether the LIVE guard is armed, and the\n * suggested next step — all without a Claude Code restart or MCP re-handshake.\n */\nexport interface ModeSwitchReport {\n /** The mode now active after the switch. */\n mode: StartDebugMode;\n /** Derived `McpEnvironment` for the now-active connection. */\n environment: McpEnvironment;\n /** Kind of the now-active connection. */\n kind: 'relay' | 'local';\n /** `true` when the relay-live LIVE side-effect guard is now armed. */\n liveGuardActive: boolean;\n /** Human-readable next-step hint for the agent. */\n nextStep: string;\n}\n\n/**\n * The four canonical `start_debug` modes (issues #382, #378, #398 — each names\n * the four-environment fidelity ladder rung it attaches to):\n *\n * - `local-browser` → env 1: desktop Chromium with the MOCK SDK + local CDP\n * attach. Side-effect tools (call_sdk/evaluate) run unguarded\n * against the mock; nothing touches a real device or real users.\n * No prerequisites — the default, always-available environment.\n *\n * - `relay-sandbox` → env 2: real-device PWA (real WebKit engine + mock SDK)\n * over an EXTERNAL CDP relay that the unplugin (`tunnel: { cdp:\n * true }`) already brought up. liveIntent off — dev-intent, never\n * LIVE. Output env `relay-mobile`. Prerequisite: `AIT_RELAY_BASE_URL`\n * set to the unplugin's relay base URL. The MCP only attaches a\n * CDP client; it does NOT start (or stop) that relay.\n *\n * - `relay-staging` → env 3: real-device Toss WebView dog-food build with the\n * REAL SDK over the intoss-private relay. liveIntent off.\n * Prerequisite: deployed dog-food bundle + device cold-loaded via\n * intoss-private deep-link/QR relay injection.\n *\n * - `relay-live` → env 4: REVIEW-PASSED production runtime with the REAL SDK\n * over the intoss relay. liveIntent on (requires `confirm: true`).\n * Read-only debugging: call_sdk/evaluate require confirm per call.\n *\n * `relay-staging` and `relay-live` share ONE physical relay connection\n * (`relay-intoss`, see {@link FamilyKey}) — wire-identical, distinguished only by\n * the `liveIntent` bit — so switching staging↔live never re-boots the tunnel.\n *\n * Normalization is handled by `normalizeStartDebugMode`.\n */\nexport type StartDebugMode = 'local-browser' | 'relay-sandbox' | 'relay-staging' | 'relay-live';\n\n/**\n * Returns `true` when the mode routes to a relay connection (`relay-sandbox`,\n * `relay-staging`, or `relay-live`). `relay-sandbox` is an external-PWA relay;\n * `relay-staging`/`relay-live` are intoss-private relays — but all three surface\n * the Tier B / relay-only tool set.\n */\nexport function isRelayMode(mode: StartDebugMode): boolean {\n return mode === 'relay-sandbox' || mode === 'relay-staging' || mode === 'relay-live';\n}\n\n/**\n * Attach URL components — stored in the run functions instead of a finished\n * URL string so that `getDashboardState` can RE-MINT a fresh TOTP code on\n * every call (Defect 1: baked codes expire → relay 401 reason:'auth').\n *\n * `kind: 'launcher'` = env 2 (launcher PWA QR, `buildLauncherAttachUrl`).\n * `kind: 'scheme'` = env 3/4 (intoss-private deep-link, `buildDeepLinkAttachUrl`).\n *\n * SECRET-HANDLING: these components contain tunnel/scheme hosts. They are\n * NEVER logged. The TOTP code is minted fresh at call time via `rebuildAttachUrl`\n * and rides inside the assembled URL's `at=` param only.\n */\nexport type AttachUrlParts =\n | { kind: 'launcher'; tunnelHttpUrl: string; wssUrl: string; appName?: string }\n | { kind: 'scheme'; schemeUrl: string; wssUrl: string };\n\n/**\n * Owns the two coexisting CDP connections (local + relay) and the `active`\n * pointer that `start_debug` flips (issue #348 — DUAL-CONNECTION-COEXIST).\n *\n * The MCP `Server` + transport are created once; the request handlers read the\n * connection through `active`, so swapping the pointer underneath is invisible\n * to the MCP host (no re-handshake, no restart). Inactive infra is left warm —\n * teardown happens only at process exit (see the unified shutdown in the run\n * functions), which is what preserves a warm attach across mode switches.\n */\nexport interface ConnectionRouter {\n /** The connection the request handlers must read this instant. */\n readonly active: CdpConnection;\n /**\n * Relay origin of the currently-active family (issue #378) — the\n * discriminator that distinguishes the env-2 external-PWA relay\n * (`'external-pwa'` → `relay-mobile`) from the intoss-private relay\n * (`'intoss-webview'` → `relay-dev`). `undefined` for a local (mock) active\n * connection, or for a single-connection router that has no family concept.\n * Threaded into `deriveEnvironment` so the output env can tell the two\n * `kind: 'relay'` families apart.\n */\n readonly activeRelayOrigin?: RelayOrigin;\n /**\n * Switches the active connection to the family for `mode`, lazily booting\n * that family's infra if needed, re-arming the attach watcher, and emitting\n * `tools/list_changed`. Sets `liveIntent` (true only for `relay-live`).\n *\n * `projectRoot` (issue #396) is the per-debug-session mini-app project root\n * supplied by `start_debug`. When switching into a relay family the router\n * loads the relay TOTP secret read-only from `<projectRoot>/.ait_relay` into\n * `process.env` (via `loadRelaySecretReadOnly`) BEFORE the relay boots, so the\n * `assertRelayAuthConfigured()` / `buildRelayVerifyAuth()` at the boot site see\n * it. The daemon never mints — it only reads. Ignored for the local family.\n *\n * Rejects (without swapping) when a swap is already in flight, or when\n * `relay-live` is requested without `confirm: true`.\n */\n switchMode(\n mode: StartDebugMode,\n confirm: boolean,\n projectRoot?: string,\n ): Promise<ModeSwitchReport>;\n}\n\n/** Live infra the connection reads tunnel status from. */\nexport interface DebugServerDeps {\n connection: CdpConnection;\n /**\n * Dual-connection router (issue #348). When provided, the request handlers\n * read the live connection through `router.active` and `start_debug` calls\n * `router.switchMode()`. When omitted (the dominant test path), a trivial\n * router pinned to `deps.connection` is synthesized and `start_debug` reports\n * that dynamic switching is unavailable — back-compat with every existing\n * single-connection test.\n */\n router?: ConnectionRouter;\n /** AIT.* domain source — forwarded over the same Chii channel in production. */\n aitSource: AitSource;\n /** Returns current tunnel status (URL changes per spawn). */\n getTunnelStatus(): TunnelStatus;\n /**\n * Maximum time in ms to wait for a page to attach when `wait_for_attach=true`.\n * Default 60 000 ms. Exposed for testing so tests can use a small value without\n * fake timers (which conflict with MCP SDK's own timeouts).\n */\n waitForAttachTimeoutMs?: number;\n /**\n * 로컬 QR HTTP 서버 — `build_attach_url` tool이 브라우저로 열 HTTP URL을 제공.\n * 없으면 text QR fallback으로만 동작 (GUI 없는 환경 호환).\n */\n qrHttpServer?: QrHttpServer;\n /**\n * Resolves the current MCP environment (`mock` | `relay-dev` | `relay-live`).\n * Used by `tools/list` to filter Tier A/B tools and by Tier C tools (e.g.\n * `measure_safe_area`) to label the `source` provenance field.\n *\n * Optional — defaults (issue #348) to deriving the env from the *active*\n * connection's `kind` + the module-level `liveIntent` bit\n * (`deriveEnvironment(router.active.kind, getLiveIntent())`). No URL sniffing\n * or precedence chain. Tests inject a fake to pin a precise env.\n */\n getEnvironment?: () => McpEnvironment;\n /** Resolves the reason for the current env decision (for logs). */\n getEnvironmentReason?: () => string;\n /**\n * Diagnostics collector — records server-side errors, attach/detach events,\n * and surfaces them via `get_debug_status`. When omitted a no-op collector is\n * used (backwards-compatible with existing tests that don't inject one).\n */\n diagnosticsCollector?: DiagnosticsCollector;\n /**\n * Hex-encoded TOTP secret for `build_attach_url` auto-splice.\n *\n * When set, `build_attach_url` generates a fresh TOTP code on every call and\n * splices it as `at=<code>` into the returned `attachUrl`. The response also\n * includes a `totp` field with `ttlSeconds` and `expiresAt` so callers know\n * when to re-invoke.\n *\n * SECRET-HANDLING: this value is captured in a closure and MUST NOT be logged\n * or included in any output other than the `at=` param inside `attachUrl`.\n *\n * Tests inject a dummy hex string or omit it. Production uses the late-bound\n * {@link getTotpSecret} variant instead (read at call time) — see below.\n */\n totpSecret?: string;\n /**\n * Late-bound variant of {@link totpSecret}: read AT `build_attach_url` CALL\n * TIME rather than captured once at server construction (issue #396).\n *\n * Why late-bound: since #396 the relay TOTP secret lives in a project-local\n * `.ait_relay` file loaded read-only into `process.env.AIT_DEBUG_TOTP_SECRET`\n * by `switchMode` BEFORE a relay family boots — which is AFTER the daemon\n * (and thus `createDebugServer`) already started. Capturing the secret at\n * construction would read an empty value on the all-lazy daemon, so\n * `build_attach_url` would emit a QR with no `at=` code and every attach would\n * be rejected by the relay gate. Reading it at call time makes the loaded\n * secret visible.\n *\n * When omitted, `createDebugServer` falls back to the captured {@link totpSecret}\n * (preserving all existing test behavior).\n *\n * SECRET-HANDLING: same as {@link totpSecret} — the returned value MUST NOT be\n * logged or included in any output other than the `at=` param inside `attachUrl`.\n *\n * Production: passed as `() => process.env.AIT_DEBUG_TOTP_SECRET` by the three\n * run functions.\n */\n getTotpSecret?: () => string | undefined;\n /**\n * `build_attach_url` 핸들러가 attach URL 컴포넌트를 확정한 직후 호출되는 콜백.\n * run 함수에서 `lastAttachParts` 갱신 + `qrHttpServer.notifyStateChange()` 트리거에 사용.\n * 테스트에서는 주입하지 않아도 되고, 미주입 시 no-op.\n *\n * 완성된 URL 문자열이 아니라 컴포넌트를 전달하는 이유: `getDashboardState`가\n * 호출될 때마다 최신 TOTP 코드를 freshly mint해 QR을 갱신하기 위함이다.\n * 정적 URL에 구워진 코드는 ~3분 후 만료(RELAY_VERIFY_SKEW_STEPS=6 기준) → relay 401 reason:'auth' (Defect 1).\n * rebuildAttachUrl()이 매 호출 시 generateTotp(secret)를 새로 계산한다.\n *\n * SECRET-HANDLING: 컴포넌트 안의 tunnel/scheme host와 wssUrl은 NEVER 로그 출력.\n * TOTP 코드는 rebuildAttachUrl() 내부에서만 mint되며 attachUrl의 at= param 안에만 노출.\n */\n onAttachUrlBuilt?: (parts: AttachUrlParts) => void;\n /**\n * Returns the cloudflared child PID of the currently active tunnel.\n * When provided, `get_debug_status` passes it to `getDiagnostics` as the\n * live in-memory source for FIX 2 (issue #571) — the PID is also picked up\n * from the lock file as a fallback, but the in-memory value is preferred as\n * it stays current across reissues.\n *\n * Production: injected by the run functions via a captured `activeTunnelChildPid`\n * variable that is updated whenever `onTunnelChildPid` fires (including reissues).\n * Tests inject a controlled value. Omitting it (old tests) falls back to the\n * lock-file path in `getDiagnostics`.\n */\n getTunnelChildPid?: () => number | null | undefined;\n /**\n * Lock-file reader — injected here so tests can control the lock data without\n * touching the filesystem. Defaults to `readServerLock` (the real file).\n *\n * This also enables handler-level tests for FIX 2 (issue #572 review) that\n * need to simulate a stale lock with a dead tunnelChildPid.\n */\n readLock?: () => import('./server-lock.js').LockData | null;\n}\n\n/**\n * Waits for the first target matching `filterFn` to attach, using the\n * event-driven `waitForFirstTarget()` when the connection supports it\n * (interface-optional member, present on `ChiiCdpConnection`), or falling\n * back to a polling loop for connections that don't implement it (test fakes,\n * `LocalCdpConnection`).\n *\n * This eliminates the polling-only race that previously caused `wait_for_attach`\n * to resolve before the relay had observed the first inbound CDP message from\n * the phone.\n *\n * @param connection - The CDP connection (production or fake).\n * @param filterFn - Resolves when this predicate is satisfied.\n * @param timeoutMs - Maximum wait time in ms.\n * @param pollIntervalMs - Fallback poll interval for connections without waitForFirstTarget.\n */\nfunction waitForAttachWithEvents(\n connection: CdpConnection,\n filterFn: (targets: ReturnType<CdpConnection['listTargets']>) => boolean,\n timeoutMs: number,\n pollIntervalMs = 1_000,\n): Promise<ReturnType<CdpConnection['listTargets']>> {\n // Use event-driven path when available (CdpConnection.waitForFirstTarget is\n // optional; ChiiCdpConnection implements it, LocalCdpConnection and test fakes do not).\n if (connection.waitForFirstTarget) {\n return connection.waitForFirstTarget(filterFn, timeoutMs, pollIntervalMs);\n }\n // Generic fallback for connections without waitForFirstTarget\n // (test fakes, LocalCdpConnection — they don't emit 'target:attached').\n return new Promise<ReturnType<CdpConnection['listTargets']>>((resolve, reject) => {\n const deadline = Date.now() + timeoutMs;\n let settled = false;\n const poll = setInterval(() => {\n const targets = connection.listTargets();\n if (filterFn(targets)) {\n settled = true;\n clearInterval(poll);\n resolve(targets);\n } else if (Date.now() >= deadline) {\n settled = true;\n clearInterval(poll);\n reject(new Error(`waitForAttachWithEvents: 타임아웃 (${timeoutMs}ms)`));\n }\n }, pollIntervalMs);\n // Also check immediately.\n const targets = connection.listTargets();\n if (!settled && filterFn(targets)) {\n settled = true;\n clearInterval(poll);\n resolve(targets);\n }\n });\n}\n\n/**\n * Builds the debug-mode MCP server around an injected CDP connection + AIT\n * source + tunnel status getter. Pure wiring — does not start a relay or\n * tunnel, which is what makes the tool surface unit-testable.\n *\n * `tools/list` is two-tiered (issue #208):\n * - bootstrap (always): `build_attach_url`, `list_pages`\n * - attach-dependent (after `connection.listTargets().length > 0`): all others\n *\n * `CallTool` is NOT tiered — hidden tools still execute (attach errors surface\n * naturally via `enableDomains`). The tier only controls visibility.\n */\nexport function createDebugServer(deps: DebugServerDeps): Server {\n const {\n connection,\n router: routerDep,\n aitSource,\n getTunnelStatus,\n waitForAttachTimeoutMs = 60_000,\n qrHttpServer,\n getEnvironment: getEnvDep,\n getEnvironmentReason: getEnvReasonDep,\n diagnosticsCollector: collectorDep,\n totpSecret,\n onAttachUrlBuilt,\n getTunnelChildPid,\n readLock: readLockDep,\n } = deps;\n\n // Late-bound TOTP secret accessor (issue #396): production injects\n // `getTotpSecret` so the secret is read from env at `build_attach_url` call\n // time (after switchMode's project-local .ait_relay load). When absent we fall\n // back to the captured `totpSecret` — preserving existing test behavior.\n // SECRET-HANDLING: the returned value is used only for the at= code, never logged.\n const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);\n\n // Lock-file reader — defaults to the real file reader; injected by tests to\n // control lock data without touching the filesystem. Also used by the\n // get_debug_status handler to forward lock data into getDiagnostics for the\n // FIX 2 lock-file fallback (issue #572 review).\n const readLockFn = readLockDep ?? readServerLock;\n\n // Dual-connection router (issue #348). Production passes a real router that\n // holds both the local + relay connections and flips `active` on\n // `start_debug`. Tests (and any single-connection caller) omit it — we\n // synthesize a trivial router pinned to `deps.connection` whose `switchMode`\n // reports that dynamic switching is unavailable. Either way the handlers read\n // the live connection through `router.active`, so per-call snapshots are\n // uniform.\n const router: ConnectionRouter = routerDep ?? makeSingleConnectionRouter(connection);\n\n // Env SSoT (issue #348) — derived, not detected: `mock` vs `relay-*` is free\n // from the ACTIVE connection's `kind`; `relay-dev` vs `relay-live` is the\n // module-level `liveIntent` bit. No URL sniffing, no precedence chain. Tests\n // inject `getEnvironment`/`getEnvironmentReason` to pin a precise env.\n const resolveEnvironment: () => McpEnvironment =\n getEnvDep ??\n (() => deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin));\n const resolveEnvironmentReason: () => string =\n getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},liveIntent=${getLiveIntent()}`);\n\n // Diagnostics collector — production uses an `InMemoryDiagnosticsCollector`;\n // tests may inject a no-op or fake. A no-op is created lazily when none\n // is supplied so existing tests that don't inject one continue to work.\n const collector: DiagnosticsCollector = collectorDep ?? new InMemoryDiagnosticsCollector();\n\n const server = new Server(\n { name: 'ait-debug', version: __VERSION__ },\n // listChanged: true — the server emits notifications/tools/list_changed when\n // a page attaches (0→N target transition), promoted attach-dependent tools.\n { capabilities: { tools: { listChanged: true } } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, () => {\n // Per-request snapshot of the active connection (issue #348). `kind` is\n // authoritative even before any target attaches, so bootstrap visibility\n // (e.g. Tier B `build_attach_url`) is correct from the first `tools/list`.\n const conn = router.active;\n const env = resolveEnvironment();\n const attached = conn.listTargets().length > 0;\n // Tier A/B filter first (env), then bootstrap tier (attach state).\n const envFiltered = filterToolsByEnvironment(DEBUG_TOOL_DEFINITIONS, env);\n const tools = attached\n ? envFiltered.map((tool) => ({ ...tool }))\n : envFiltered\n .filter((tool) => BOOTSTRAP_TOOL_NAMES.has(tool.name))\n .map((tool) => ({ ...tool }));\n return { tools };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n if (!isDebugToolName(name)) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${name}` }],\n isError: true,\n };\n }\n\n // PER-CALL SNAPSHOT (issue #348). Capture the active connection exactly\n // once at handler entry and use ONLY `conn` for the rest of this call.\n // `start_debug` may flip `router.active` mid-flight (and other concurrent\n // requests too); re-reading `router.active` after an `await` would race the\n // swap. This is the hard-constraint that keeps a switch from corrupting an\n // in-flight tool call.\n const conn = router.active;\n\n // start_debug — single entry to switch families (local ↔ relay) without a\n // Claude Code restart or MCP re-handshake. Always callable (Tier C /\n // bootstrap), so it is handled before the env-mismatch guard below.\n if (name === 'start_debug') {\n const rawMode = request.params.arguments?.mode;\n const mode = normalizeStartDebugMode(rawMode);\n if (mode === null) {\n return mcpError(\n 'start_debug: mode가 올바르지 않습니다. ' +\n \"'local-browser' | 'relay-sandbox' | 'relay-staging' | 'relay-live' 중 하나를 전달하세요.\",\n );\n }\n const confirm = request.params.arguments?.confirm === true;\n // Per-session project root (issue #396): the daemon reads the relay TOTP\n // secret read-only from <projectRoot>/.ait_relay when switching to a relay\n // family. Optional — omitted for local, or when the operator exported the\n // secret. SECRET-HANDLING: projectRoot is a path, never the secret value.\n const rawProjectRoot = request.params.arguments?.projectRoot;\n const projectRoot = typeof rawProjectRoot === 'string' ? rawProjectRoot : undefined;\n try {\n const report = await router.switchMode(mode, confirm, projectRoot);\n return jsonResult(report);\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n // PER-CALL SNAPSHOT of the derived environment (issue #348 / #354 regression\n // fix). Capture `env` + `envReason` exactly once, right after the start_debug\n // branch (so this call sees the post-switch env when it *is* a switch) and\n // before the first `await`. Every site below reuses these locals instead of\n // re-calling `resolveEnvironment()`/`resolveEnvironmentReason()` — those\n // closures re-read `router.active.kind` + `getLiveIntent()` live, so a\n // concurrent `start_debug` swap mid-await would otherwise corrupt the env\n // stamped into this call's envelope / provenance label.\n const env = resolveEnvironment();\n const envReason = resolveEnvironmentReason();\n // Tier A/B env-mismatch guard (RFC #277). Tier C tools pass through.\n // We return a tool-result error (not an MCP protocol error) so the client\n // sees a structured isError + reason text rather than a thrown exception —\n // the MCP SDK still surfaces this as an error to the agent, but with the\n // explanatory `data.reason` payload preserved as text.\n if (!isToolAvailableIn(name, env)) {\n const requiredEnv = getToolAvailability(name) ?? 'unknown';\n // Log structured (no secrets — only stable env strings + tool name).\n logWarn('tool.error', {\n tool: name,\n errorKind: 'tier-filter',\n requiredEnv,\n currentEnv: env,\n envReason,\n });\n return tierRejectionError(name, requiredEnv, env, envReason);\n }\n\n // AIT.* tools are served by the AIT source. In production it rides the same\n // Chii websocket as CDP, so the connection must be attached first; the AIT\n // source's sendCommand rejects with a clear message if no page is attached.\n if (isAitToolName(name)) {\n try {\n await conn.enableDomains();\n switch (name) {\n case 'AIT.getSdkCallHistory':\n return jsonResult(await getSdkCallHistory(aitSource));\n case 'AIT.getMockState':\n return jsonResult(await getMockState(aitSource));\n case 'AIT.getOperationalEnvironment':\n return jsonResult(await getOperationalEnvironment(aitSource));\n default:\n return unknownTool(name);\n }\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n // get_debug_status is a bootstrap tool — it works before any page attaches\n // and must not require enableDomains. It aggregates all server state into a\n // single response so the agent can diagnose session problems in one call.\n if (name === 'get_debug_status') {\n try {\n const rawLimit = request.params.arguments?.recent_errors_limit;\n const recentErrorsLimit = typeof rawLimit === 'number' && rawLimit > 0 ? rawLimit : 10;\n const result = await getDiagnostics({\n tunnel: getTunnelStatus(),\n connection: conn,\n env,\n envReason,\n collector,\n readLock: readLockFn,\n recentErrorsLimit,\n tunnelChildPid: getTunnelChildPid?.() ?? undefined,\n });\n const attached = conn.listTargets().length > 0;\n return envelopeResult(result, name, env, attached);\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n // build_attach_url is pure synthesis (relay URL → deep link).\n // It works before any page attaches, so it must not require enableDomains.\n //\n // ENV BRANCH: env 2 (relay-mobile) builds a launcher PWA QR using\n // AIT_TUNNEL_BASE_URL; env 3/4 (relay-dev/live) use the existing\n // intoss-private scheme URL path. Both branches converge below at the\n // shared QR rendering path (attachUrl + relayUrl + totp + authorityWarning).\n if (name === 'build_attach_url') {\n const waitForAttach = request.params.arguments?.wait_for_attach === true;\n // open_in_browser 옵션은 삭제됨 (#553) — 항상 대시보드 오픈을 시도한다.\n // 구버전 클라이언트가 open_in_browser 키를 보내도 에러 없이 무시됨(하위호환).\n // selfdebug: opt-in launcher self-target mode (#543). Only valid in env 2.\n const selfdebug = request.params.arguments?.selfdebug === true;\n\n // wait_timeout_seconds: per-call override of the default wait timeout.\n // Clamp to 1–600 s; invalid values (0, negative, NaN, non-number) silently fall back to default.\n const rawWaitTimeout = request.params.arguments?.wait_timeout_seconds;\n const callTimeoutMs = (() => {\n if (typeof rawWaitTimeout !== 'number' || !Number.isFinite(rawWaitTimeout)) {\n return waitForAttachTimeoutMs;\n }\n const clamped = Math.max(1, Math.min(600, rawWaitTimeout));\n // rawWaitTimeout ≤ 0 falls back to default (clamp produces 1 but intent is \"invalid\").\n if (rawWaitTimeout <= 0) return waitForAttachTimeoutMs;\n return Math.round(clamped) * 1000;\n })();\n\n // Guard: selfdebug is a launcher-only feature — reject early for env 3/4\n // so the caller gets a clear diagnostic instead of silently ignoring the flag.\n if (selfdebug && env !== 'relay-mobile') {\n return mcpError(\n 'build_attach_url: selfdebug=true는 env 2 / relay-sandbox 전용 기능입니다. ' +\n '현재 환경(env 3/4)에서는 launcher가 없어 self-target 모드를 지원하지 않습니다. ' +\n 'launcher self-target이 필요하다면 relay-sandbox 모드로 재시작하세요.',\n );\n }\n\n // ── relay-mobile branch (env 2 — launcher PWA QR) ─────────────────────\n if (env === 'relay-mobile') {\n // SECRET-HANDLING: AIT_TUNNEL_BASE_URL carries the app tunnel host —\n // NEVER echo the value in error messages or logs.\n // (#424) env wins; .ait_urls is the fallback when env is unset.\n const rawBuildProjectRoot = request.params.arguments?.projectRoot;\n const buildProjectRoot =\n typeof rawBuildProjectRoot === 'string' ? rawBuildProjectRoot : undefined;\n const envTunnelUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? '';\n let tunnelHttpUrl = envTunnelUrl;\n if (tunnelHttpUrl === '' && buildProjectRoot !== undefined) {\n const { readRelayUrls } = await import('./relay-url-store.js');\n const stored = await readRelayUrls({ projectRoot: buildProjectRoot });\n tunnelHttpUrl = stored?.tunnelBaseUrl ?? '';\n }\n if (tunnelHttpUrl === '') {\n return mcpError(\n 'build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. ' +\n 'dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. ' +\n '자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.',\n );\n }\n const tunnelStatus = getTunnelStatus();\n if (!tunnelStatus.up || tunnelStatus.wssUrl === null) {\n return mcpError(\n 'build_attach_url(mobile): relay wssUrl이 아직 설정되지 않았습니다. ' +\n 'unplugin tunnel:{cdp:true}가 relay를 완전히 기동할 때까지 잠시 후 다시 시도하세요.',\n );\n }\n\n // SECRET-HANDLING: the secret is used only to compute a code that is\n // spliced as at= in the attachUrl — never logged or returned separately.\n // Read at call time (#396) so the project-local .ait_relay secret loaded\n // by switchMode is visible.\n const secret = getTotpSecret();\n // Defense-in-depth (#452): relay mode requires TOTP auth — if the secret\n // is missing, fail-closed rather than issuing an unauthenticated attach URL.\n // assertRelayAuthConfigured() at bootRelayFamily/bootExternalRelayFamily\n // already gates relay startup, so this branch is dead code in normal\n // operation; the guard exists to prevent accidental TOTP bypass if the\n // boot-time guard is ever bypassed or removed.\n // SECRET-HANDLING: error message names the requirement only — never the\n // secret value, length, or any derived fragment.\n if (secret === undefined || secret === '') {\n return mcpError(\n 'build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. ' +\n 'relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.',\n );\n }\n let totpCode: string | undefined;\n let totpMeta: { enabled: true; ttlSeconds: number; expiresAt: string } | undefined;\n {\n const now = Date.now();\n totpCode = generateTotp(secret, now);\n const STEP_SECONDS = 30;\n // expiresAt reflects the relay gate's RELAY_VERIFY_SKEW_STEPS=6 window (#490):\n // the code is valid for ~3 min (180 s) from issuance.\n const expiresAtMs = now + RELAY_VERIFY_SKEW_STEPS * STEP_SECONDS * 1000;\n totpMeta = {\n enabled: true,\n ttlSeconds: RELAY_VERIFY_SKEW_STEPS * STEP_SECONDS,\n expiresAt: new Date(expiresAtMs).toISOString(),\n };\n }\n\n // Read the app name from projectRoot/package.json to add to the launcher\n // deep-link (#498). Failure to read is silently ignored (fail-open).\n let launcherAppName: string | undefined;\n if (buildProjectRoot !== undefined) {\n try {\n const { readFileSync } = await import('node:fs');\n const pkgRaw = readFileSync(`${buildProjectRoot}/package.json`, 'utf8');\n const pkg = JSON.parse(pkgRaw) as Record<string, unknown>;\n const rawName = typeof pkg.name === 'string' ? pkg.name : '';\n // Strip npm scope prefix (@scope/foo → foo).\n const stripped = rawName.includes('/')\n ? rawName.slice(rawName.indexOf('/') + 1)\n : rawName;\n launcherAppName = stripped.trim() || undefined;\n } catch {\n // Silently ignore — fail-open (existing behavior unchanged).\n }\n }\n\n // SECRET-HANDLING: attachUrl encodes tunnelHttpUrl and wssUrl inside\n // the QR payload only — not logged or returned as standalone fields.\n const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, {\n name: launcherAppName,\n ...(selfdebug ? { selfdebug: true } : {}),\n });\n // Notify dashboard with components (not a finished URL) so getDashboardState\n // re-mints a fresh TOTP code on every SSE push/reload (Defect 1).\n // SECRET-HANDLING: components contain tunnel host — never logged.\n onAttachUrlBuilt?.({\n kind: 'launcher',\n tunnelHttpUrl,\n wssUrl: tunnelStatus.wssUrl,\n appName: launcherAppName,\n });\n const relayUrl = tunnelStatus.wssUrl;\n const authorityWarning: string | undefined = undefined; // no scheme authority for launcher\n const totp = totpMeta;\n\n // In mobile mode, deploymentId filtering is not applicable —\n // the launcher attach is not tied to a specific bundle deployment.\n // match on presence only (any page that attaches is the target).\n const isMatchingPage = (pages: ReturnType<CdpConnection['listTargets']>): boolean =>\n pages.length > 0;\n const buildTimeoutError = (\n baseText: string,\n timeoutSec: number,\n observed: ReturnType<CdpConnection['listTargets']>,\n ): string => {\n const observedUrls = observed\n .slice(0, 3)\n .map((p) => p.url.slice(0, 80))\n .join(', ');\n const observedNote =\n observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : '';\n return (\n `${baseText}\\n\\nNo page attached within ${timeoutSec}s${observedNote} — ` +\n 'launcher QR을 폰 카메라로 스캔한 뒤 call list_pages를 다시 호출하세요.'\n );\n };\n\n // Fall through to the shared QR rendering path below.\n // (extracted into a local async IIFE so both branches can return from it)\n return await (async () => {\n const header =\n 'This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).';\n const warningPrefix = authorityWarning\n ? `⚠️ scheme_url 경고: ${authorityWarning}\\n\\n`\n : '';\n const guiAvailable = canOpenBrowser();\n\n if (!guiAvailable) {\n const headlessNote =\n 'GUI 환경이 감지되지 않았습니다 (headless/remote 환경). ' +\n '텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\\n\\n';\n const qrHeadless = await renderQr(attachUrl);\n const headlessText = `${warningPrefix}${headlessNote}${header}\\n${JSON.stringify({ attachUrl, relayUrl, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qrHeadless}`;\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: headlessText }] };\n }\n let attachedPagesHl: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPagesHl = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(headlessText, callTimeoutMs / 1000, attachedPagesHl),\n },\n ],\n isError: true,\n };\n }\n const pagesResultHl = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${headlessText}\\n\\n${JSON.stringify(pagesResultHl, null, 2)}`,\n },\n ],\n };\n }\n\n if (guiAvailable && qrHttpServer) {\n const httpUrl = qrHttpServer.buildAttachPageUrl(attachUrl);\n const pngUrl = `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`;\n const browserResult = await openQrInBrowser(httpUrl, pngUrl);\n if (browserResult.opened) {\n const retriedNote = browserResult.retried ? ' (1회 retry 후 성공)' : '';\n const openResult = {\n attempted: true,\n succeeded: true,\n ...(browserResult.retried ? { retried: true } : {}),\n };\n const shortText =\n `${warningPrefix}${header}\\n` +\n `${JSON.stringify({ relayUrl, openResult, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n` +\n `브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\\n` +\n `URL: ${browserResult.httpUrl}`;\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: shortText }] };\n }\n let attachedPages: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPages = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(shortText, callTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n const pagesResult = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${shortText}\\n\\n${JSON.stringify(pagesResult, null, 2)}`,\n },\n ],\n };\n }\n const openResult = {\n attempted: true,\n succeeded: false,\n failureReason: browserResult.error ?? '브라우저 실행 후보 모두 실패',\n pngUrl: browserResult.pngUrl,\n ...(browserResult.stderrSummary\n ? { stderrSummary: browserResult.stderrSummary }\n : {}),\n };\n const stderrNote = browserResult.stderrSummary\n ? `\\nstderr: ${browserResult.stderrSummary}`\n : '';\n const fallbackNote =\n `브라우저 자동 열기에 실패했습니다. ` +\n `다음 URL을 직접 브라우저에서 여세요:\\n${browserResult.httpUrl}\\n` +\n `또는 PNG로 받기: ${browserResult.pngUrl}` +\n stderrNote +\n '\\n\\n';\n const qr = await renderQr(attachUrl);\n const baseText = `${warningPrefix}${fallbackNote}${header}\\n${JSON.stringify({ attachUrl, relayUrl, openResult, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qr}`;\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: baseText }] };\n }\n let attachedPagesFb: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPagesFb = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, callTimeoutMs / 1000, attachedPagesFb),\n },\n ],\n isError: true,\n };\n }\n const pagesResultFb = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${baseText}\\n\\n${JSON.stringify(pagesResultFb, null, 2)}`,\n },\n ],\n };\n }\n\n const qr = await renderQr(attachUrl);\n const baseText = `${warningPrefix}${header}\\n${JSON.stringify({ attachUrl, relayUrl, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qr}`;\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: baseText }] };\n }\n let attachedPages: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPages = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, callTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n const pagesResult = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${baseText}\\n\\n${JSON.stringify(pagesResult, null, 2)}`,\n },\n ],\n };\n })();\n }\n // ── end relay-mobile branch ────────────────────────────────────────────\n\n // ── relay-dev / relay-live branch (env 3/4 — intoss-private QR) ───────\n const schemeUrl = request.params.arguments?.scheme_url;\n if (typeof schemeUrl !== 'string' || schemeUrl === '') {\n return mcpError(\n 'build_attach_url: scheme_url이 비어 있습니다. ' +\n '`ait deploy --scheme-only`가 출력하는 intoss-private:// URL을 인자로 전달하세요. ' +\n '환경 2(mobile)라면 scheme_url 대신 AIT_TUNNEL_BASE_URL을 설정하세요.',\n );\n }\n\n // Parse _deploymentId from scheme_url to filter stale attached pages.\n // null → \"no filter; match on presence only\" (original behaviour preserved).\n const deploymentId = extractDeploymentId(schemeUrl);\n if (!deploymentId) {\n logInfo('tool.call', {\n tool: 'build_attach_url',\n msg: 'no _deploymentId in scheme_url; matching on presence only',\n });\n }\n\n /** Returns true when the page list satisfies the attach condition. */\n const isMatchingPage = (pages: ReturnType<CdpConnection['listTargets']>): boolean => {\n if (pages.length === 0) return false;\n if (deploymentId === null) return true;\n return pages.some((p) => p.url.includes(deploymentId));\n };\n\n /** Builds a timeout error message with diagnostic context. */\n const buildTimeoutError = (\n baseText: string,\n timeoutSec: number,\n observed: ReturnType<CdpConnection['listTargets']>,\n ): string => {\n const observedUrls = observed\n .slice(0, 3)\n .map((p) => p.url.slice(0, 80))\n .join(', ');\n const observedNote =\n observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : '';\n const deploymentNote = deploymentId ? ` matching deploymentId=${deploymentId}` : '';\n return (\n `${baseText}\\n\\nNo page${deploymentNote} attached within ${timeoutSec}s${observedNote} — ` +\n 'call list_pages to retry.'\n );\n };\n\n // Defense-in-depth (#452): relay-dev/live mode requires TOTP auth.\n // Read secret before entering try{} so we can return mcpError (not throw).\n // assertRelayAuthConfigured() at bootRelayFamily already gates relay startup,\n // so this is dead code in normal operation — the guard closes the fail-open\n // branch in buildAttachUrl if the boot-time guard is ever bypassed.\n // SECRET-HANDLING: error message names the requirement only.\n {\n const relaySecret = getTotpSecret();\n if (relaySecret === undefined || relaySecret === '') {\n return mcpError(\n 'build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. ' +\n 'relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.',\n );\n }\n }\n try {\n // SECRET-HANDLING: the secret is passed to buildAttachUrl only; it is\n // never logged or included in output other than the at= param in attachUrl.\n // Read at call time (#396) so the project-local .ait_relay secret loaded\n // by switchMode is visible.\n // Snapshot the tunnel once so we use a consistent wssUrl in both buildAttachUrl\n // and the onAttachUrlBuilt components (avoids a torn read if tunnel reissues mid-call).\n const tunnelForBuild = getTunnelStatus();\n const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(\n schemeUrl,\n tunnelForBuild,\n getTotpSecret(),\n );\n // Notify dashboard with components (not a finished URL) so getDashboardState\n // re-mints a fresh TOTP code on every SSE push/reload (Defect 1).\n // buildAttachUrl already throws on tunnel-down before this, so wssUrl is non-null.\n // SECRET-HANDLING: components contain scheme/wss host — never logged.\n if (tunnelForBuild.wssUrl !== null) {\n onAttachUrlBuilt?.({ kind: 'scheme', schemeUrl, wssUrl: tunnelForBuild.wssUrl });\n }\n\n // Prepend a non-fatal authority warning when the scheme URL host looks wrong.\n const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\\n\\n` : '';\n\n const header =\n 'This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).';\n\n // canOpenBrowser()를 한 번만 호출하여 이 요청 안에서 일관된 값을 사용한다.\n // mockReturnValueOnce 등 테스트 대역이 여러 번 호출로 소비되지 않도록.\n const guiAvailable = canOpenBrowser();\n\n // headless 환경 감지: GUI가 없는 경우 안내 후 text QR fallback.\n if (!guiAvailable) {\n const headlessNote =\n 'GUI 환경이 감지되지 않았습니다 (headless/remote 환경). ' +\n '텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\\n\\n';\n const qrHeadless = await renderQr(attachUrl);\n const headlessText = `${warningPrefix}${headlessNote}${header}\\n${JSON.stringify({ attachUrl, relayUrl, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qrHeadless}`;\n\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: headlessText }] };\n }\n\n // wait_for_attach + headless fallback\n let attachedPagesHl: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPagesHl = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(headlessText, callTimeoutMs / 1000, attachedPagesHl),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResultHl = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${headlessText}\\n\\n${JSON.stringify(pagesResultHl, null, 2)}`,\n },\n ],\n };\n }\n\n // Try to open QR in browser: GUI is available and the HTTP server is up.\n if (guiAvailable && qrHttpServer) {\n const httpUrl = qrHttpServer.buildAttachPageUrl(attachUrl);\n const pngUrl = `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`;\n\n const browserResult = await openQrInBrowser(httpUrl, pngUrl);\n\n if (browserResult.opened) {\n // Opened successfully — HTTP URL을 사용자에게 명시.\n // SECRET-HANDLING: attachUrl은 httpUrl query string 안에 있고, tool result에는 httpUrl만 노출.\n const retriedNote = browserResult.retried ? ' (1회 retry 후 성공)' : '';\n const openResult = {\n attempted: true,\n succeeded: true,\n ...(browserResult.retried ? { retried: true } : {}),\n };\n const shortText =\n `${warningPrefix}${header}\\n` +\n `${JSON.stringify({ relayUrl, openResult, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n` +\n `브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\\n` +\n `URL: ${browserResult.httpUrl}`;\n\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: shortText }] };\n }\n\n // wait_for_attach path (browser opened) — event-driven via waitForAttachWithEvents.\n let attachedPages: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPages = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(shortText, callTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResult = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${shortText}\\n\\n${JSON.stringify(pagesResult, null, 2)}`,\n },\n ],\n };\n }\n\n // Browser open failed — openResult 포함 구조화 에러 + URL 안내 + text QR fallback.\n const openResult = {\n attempted: true,\n succeeded: false,\n failureReason: browserResult.error ?? '브라우저 실행 후보 모두 실패',\n pngUrl: browserResult.pngUrl,\n ...(browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}),\n };\n const stderrNote = browserResult.stderrSummary\n ? `\\nstderr: ${browserResult.stderrSummary}`\n : '';\n const fallbackNote =\n `브라우저 자동 열기에 실패했습니다. ` +\n `다음 URL을 직접 브라우저에서 여세요:\\n` +\n `${browserResult.httpUrl}\\n` +\n `또는 PNG로 받기: ${browserResult.pngUrl}` +\n stderrNote +\n '\\n\\n';\n const qr = await renderQr(attachUrl);\n const baseText = `${warningPrefix}${fallbackNote}${header}\\n${JSON.stringify({ attachUrl, relayUrl, openResult, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qr}`;\n\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: baseText }] };\n }\n\n // wait_for_attach + fallback path — event-driven via waitForAttachWithEvents.\n let attachedPagesFb: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPagesFb = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, callTimeoutMs / 1000, attachedPagesFb),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResultFb = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${baseText}\\n\\n${JSON.stringify(pagesResultFb, null, 2)}`,\n },\n ],\n };\n }\n\n // No GUI available or no HTTP server: text QR fallback.\n const qr = await renderQr(attachUrl);\n const baseText = `${warningPrefix}${header}\\n${JSON.stringify({ attachUrl, relayUrl, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qr}`;\n\n if (!waitForAttach) {\n return {\n content: [{ type: 'text' as const, text: baseText }],\n };\n }\n\n // wait_for_attach=true: event-driven via waitForAttachWithEvents.\n // enableDomains is NOT called here — listTargets is a buffered target list\n // read and does not require domain negotiation.\n // The deploymentId filter (parsed above) ensures we don't return a stale\n // page from a previous session — resolves only when an attached page's\n // URL contains the expected deploymentId.\n let attachedPages: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPages = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, callTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResult = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${baseText}\\n\\n${JSON.stringify(pagesResult, null, 2)}`,\n },\n ],\n };\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n try {\n // Ensure CDP domains are enabled before reading. No-op once attached;\n // throws a clear message while no page is attached yet.\n await conn.enableDomains();\n } catch (err) {\n if (name === 'list_pages') {\n // list_pages is still useful pre-attach: report tunnel + empty pages.\n // Refresh from relay first so evicted-then-reattached targets are not\n // served as stale empty (#281 — stale cache diagnosis).\n try {\n await conn.refreshTargets?.();\n } catch {\n // Ignore refresh errors — still return cached state.\n }\n const pagesData = listPages(conn, getTunnelStatus());\n const attached = conn.listTargets().length > 0;\n return envelopeResult(pagesData, name, env, attached);\n }\n // 4상태 분류: page 미attach vs crash vs relay disconnect\n return classifyEnableDomainError(err, name);\n }\n\n try {\n switch (name) {\n case 'list_console_messages':\n return jsonResult(listConsoleMessages(conn));\n case 'list_exceptions': {\n const rawLimit = request.params.arguments?.limit;\n const limit = typeof rawLimit === 'number' && rawLimit > 0 ? rawLimit : 50;\n return jsonResult({ exceptions: listExceptions(conn, limit) });\n }\n case 'list_network_requests':\n return jsonResult(listNetworkRequests(conn));\n case 'list_pages': {\n // Refresh from relay so evict→reattach transitions are not served stale.\n try {\n await conn.refreshTargets?.();\n } catch {\n // Ignore refresh errors — still return cached state.\n }\n const listPagesData = listPages(conn, getTunnelStatus());\n const listPagesAttached = conn.listTargets().length > 0;\n return envelopeResult(listPagesData, name, env, listPagesAttached);\n }\n case 'get_dom_document':\n return jsonResult(await getDomDocument(conn));\n case 'take_snapshot':\n return jsonResult(await takeSnapshot(conn));\n case 'take_screenshot': {\n const shot = await takeScreenshot(conn);\n return {\n content: [{ type: 'image' as const, data: shot.data, mimeType: shot.mimeType }],\n };\n }\n case 'measure_safe_area': {\n // Pass the SNAPSHOT env to attach `source: 'mock' | 'relay'` to the\n // result (Tier C parity per RFC #277 — the same Runtime.evaluate probe\n // runs in both envs; only the provenance label differs). The label must\n // match the `conn` the probe actually ran on, so it reads the snapshot\n // `env` (entry-time, same as `conn`) — not a freshly re-derived env that\n // a concurrent swap could have moved.\n const safeAreaData = await measureSafeArea(conn, env);\n const safeAreaAttached = conn.listTargets().length > 0;\n return envelopeResult(safeAreaData, name, env, safeAreaAttached);\n }\n case 'evaluate': {\n const expression = request.params.arguments?.expression;\n if (typeof expression !== 'string' || expression === '') {\n return mcpError(\n 'evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.',\n );\n }\n // LIVE guard (issue #348, race fix #354). Evaluated at the side-effect\n // boundary with a SNAPSHOT `conn.kind` + a FRESH `getLiveIntent()` — not\n // the stale entry-time `env`. The side effect always runs on `conn`, so\n // the guard judges by `conn.kind`; reading `liveIntent` fresh closes the\n // false→true race where a concurrent `start_debug('relay-live')` arms\n // liveIntent while this call is parked on an `await`, after the stale\n // entry-time `env` was already computed as non-live. A stale `true`\n // bit stays inert against a local target (conn.kind !== 'relay').\n if (\n conn.kind === 'relay' &&\n getLiveIntent() &&\n request.params.arguments?.confirm !== true\n ) {\n return liveGuardError('evaluate');\n }\n // SECRET-HANDLING: do not log expression or result value.\n return jsonResult(await evaluate(conn, expression));\n }\n case 'call_sdk': {\n const sdkName = request.params.arguments?.name;\n if (typeof sdkName !== 'string' || sdkName === '') {\n return mcpError(\n 'call_sdk: name 인자가 비어 있습니다. 호출할 SDK 메서드 이름을 전달하세요.',\n );\n }\n const rawArgs = request.params.arguments?.args;\n const sdkArgs: unknown[] = Array.isArray(rawArgs) ? rawArgs : [];\n // LIVE guard (issue #348, race fix #354): see `evaluate` above —\n // snapshot `conn.kind` + fresh `getLiveIntent()` so the false→true\n // race (concurrent `start_debug('relay-live')` mid-await) is rejected.\n if (\n conn.kind === 'relay' &&\n getLiveIntent() &&\n request.params.arguments?.confirm !== true\n ) {\n return liveGuardError('call_sdk');\n }\n // SECRET-HANDLING: do not log name, args, or result value.\n const sdkResult = await callSdk(conn, sdkName, sdkArgs);\n // 상태 4: SDK 부재 — ok:false + 'sdk-absent:' 패턴은 isError로 승격\n if (\n !sdkResult.ok &&\n typeof sdkResult.error === 'string' &&\n sdkResult.error.startsWith('sdk-absent:')\n ) {\n // issue #360: local(`--target=local`) 세션은 dog-food 재배포가 아니라\n // dev 서버/unplugin alias 확인이 맞는 안내다 — connection.kind로 분기.\n return sdkAbsentError('call_sdk', conn.kind === 'local');\n }\n const callSdkAttached = conn.listTargets().length > 0;\n return envelopeResult(sdkResult, name, env, callSdkAttached);\n }\n default:\n return unknownTool(name);\n }\n } catch (err) {\n // issue #360: sdk-absent 분류가 local 세션이면 dev-bridge 안내로 분기하도록\n // connection 종류를 넘긴다. 다른 에러 분류에는 영향 없음(isLocal 미사용).\n return errorResult(err, name, conn.kind === 'local');\n }\n });\n\n return server;\n}\n\n/**\n * Normalizes a raw `start_debug` `mode` argument to a `StartDebugMode`, or\n * `null` when the value is not one of the four accepted modes:\n * 'local-browser' | 'relay-sandbox' | 'relay-staging' | 'relay-live'\n *\n * Hard rename (issue #398): the older `local`/`mobile`/`staging`/`live` names\n * and their aliases are no longer accepted — pre-1.0, no back-compat.\n */\nexport function normalizeStartDebugMode(raw: unknown): StartDebugMode | null {\n if (\n raw === 'local-browser' ||\n raw === 'relay-sandbox' ||\n raw === 'relay-staging' ||\n raw === 'relay-live'\n ) {\n return raw;\n }\n return null;\n}\n\n/**\n * Builds a trivial `ConnectionRouter` pinned to a single connection (issue\n * #348). Used by `createDebugServer` when no real dual router is injected —\n * every existing single-connection test and the `local`-only / `relay`-only\n * boot path. `switchMode` here cannot lazily boot another family, so it only\n * honors a request that matches the connection's own kind (and arms/disarms\n * `liveIntent` accordingly for relay-live); any cross-family request is\n * rejected with a clear \"dynamic switch unavailable in this session\" error.\n */\nexport function makeSingleConnectionRouter(connection: CdpConnection): ConnectionRouter {\n return {\n get active() {\n return connection;\n },\n // A single-connection router has no family concept, so it carries no relay\n // origin discriminator (issue #378). Env derives as `relay-dev` for a relay\n // connection here — `relay-sandbox` (external-PWA origin) is rejected below\n // since this router cannot boot the external relay family.\n activeRelayOrigin: undefined,\n // `_projectRoot` (issue #396) is accepted for interface conformance but\n // unused here: this router never lazily boots a relay family — its single\n // connection (and thus any relay verifyAuth) was already built at startup,\n // so a per-session project-local secret cannot retroactively rewire it. The\n // dual router below performs the read-only load before a lazy relay boot.\n switchMode(\n mode: StartDebugMode,\n confirm: boolean,\n _projectRoot?: string,\n ): Promise<ModeSwitchReport> {\n // `relay-sandbox` (env 2) needs a distinct external-PWA relay family this\n // single-connection router cannot synthesize. Reject the same way a\n // cross-family switch is rejected (issue #378).\n if (mode === 'relay-sandbox') {\n return Promise.reject(\n new Error(\n 'start_debug: 이 세션은 단일 연결만 보유합니다 — ' +\n \"'relay-sandbox'(환경 2 PWA, 외부 relay)로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). \" +\n 'MCP 서버를 relay-sandbox 모드로 재시작하세요.',\n ),\n );\n }\n const wantRelay = isRelayMode(mode);\n const haveRelay = connection.kind === 'relay';\n if (wantRelay !== haveRelay) {\n return Promise.reject(\n new Error(\n `start_debug: 이 세션은 단일 ${connection.kind} 연결만 보유합니다 — ` +\n `'${mode}'로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). ` +\n 'MCP 서버를 원하는 모드로 재시작하세요.',\n ),\n );\n }\n // relay-live entry gate: confirm:true required (mirrors the per-tool gate).\n if (mode === 'relay-live' && !confirm) {\n return Promise.reject(\n new Error(\n 'start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — ' +\n '실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.',\n ),\n );\n }\n setLiveIntent(mode === 'relay-live');\n const environment = deriveEnvironment(connection.kind, getLiveIntent());\n return Promise.resolve({\n mode,\n environment,\n kind: connection.kind,\n liveGuardActive: connection.kind === 'relay' && getLiveIntent(),\n nextStep:\n connection.kind === 'relay'\n ? 'build_attach_url로 attach QR을 생성하세요.'\n : 'list_pages로 로컬 페이지 attach를 확인하세요.',\n });\n },\n };\n}\n\n/**\n * Re-builds an attach URL from stored components with a FRESHLY-minted TOTP code,\n * so the dashboard/`/attach` QR is never an expired bake-in (Defect 1).\n * SECRET-HANDLING: reads AIT_DEBUG_TOTP_SECRET at call time (mirrors tunnel.ts\n * getDashboardState). The minted code rides inside attachUrl's at= param only —\n * never logged. generateTotp() relies on its Date.now() default.\n */\nfunction rebuildAttachUrl(parts: AttachUrlParts): string {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n const code = secret ? generateTotp(secret) : undefined;\n return parts.kind === 'launcher'\n ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {\n name: parts.appName,\n })\n : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);\n}\n\nfunction jsonResult(value: unknown) {\n return { content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] };\n}\n\n/**\n * Wraps `value` in a `ToolEnvelope` (when compat mode is off) and returns it\n * as a text content block. When `AIT_MCP_COMPAT=chrome-devtools` is set the\n * envelope is skipped and the raw value is returned — identical to `jsonResult`.\n */\nfunction envelopeResult(value: unknown, tool: string, env: McpEnvironment, attached: boolean) {\n const wrapped = wrapEnvelope(value, { tool, env, attached });\n return { content: [{ type: 'text' as const, text: JSON.stringify(wrapped, null, 2) }] };\n}\n\nfunction unknownTool(name: string) {\n return mcpError(`알 수 없는 tool: ${name}`);\n}\n\n/**\n * enableDomains()가 던진 에러를 4상태로 분류해 적절한 메시지를 반환한다.\n *\n * - \"No mini-app page attached\" → page 미attach (상태 2)\n * - crash/destroy/replaced 패턴 → page crash (상태 3)\n * - relay disconnect 패턴 → relay 연결 끊김\n * - 그 외 → 원본 메시지 + list_pages 안내\n */\nfunction classifyEnableDomainError(err: unknown, toolName: string) {\n const message = err instanceof Error ? err.message : String(err);\n\n // 상태 2: page 미attach\n if (message.includes('No mini-app page attached') || message.includes('페이지가 attach 안')) {\n return pageMissingError(toolName);\n }\n\n // 상태 3: page crash / target destroyed / replaced\n if (\n message.includes('replaced-by-new-attach') ||\n message.includes('targetCrashed') ||\n message.includes('targetDestroyed') ||\n message.includes('detachedFromTarget')\n ) {\n return pageCrashError(toolName);\n }\n\n // relay 연결 끊김\n if (\n message.includes('relay에 연결되어 있지 않습니다') ||\n message.includes('relay WebSocket') ||\n message.includes('Chii relay connection closed')\n ) {\n return relayDisconnectError(toolName);\n }\n\n // 그 외\n return classifyToolError(err, toolName);\n}\n\n/**\n * CDP/AIT 명령 실행 중 catch된 에러를 4상태로 분류해 tool 결과로 반환한다.\n * debug-server 내부 try/catch 블록에서 공통으로 사용한다.\n */\nfunction errorResult(err: unknown, name: string, isLocal = false) {\n return classifyToolError(err, name, isLocal);\n}\n\n/**\n * Starts a polling watcher that detects target-set changes on\n * `connection.listTargets()` and sends a `notifications/tools/list_changed`\n * notification on the given server.\n *\n * The watcher polls every `intervalMs` (default 1 000 ms). It fires\n * `server.sendToolListChanged()` + `onAttach()` whenever the sorted target-id\n * signature changes AND the new target set is non-empty. This covers:\n * - 0→N first attach\n * - 1→1 target replacement (same count, different id — e.g. rescan)\n * - N→M any change where the result is still non-empty\n *\n * Full detach (→ empty) updates the stored signature but does NOT fire the\n * callback — `onAttach` semantics are about a live target being present.\n *\n * The interval is **never cleared automatically** — it keeps running until\n * `stop()` is called during shutdown. This ensures that a target replacement\n * after the first attach is always detected.\n *\n * `onAttach` is called on every non-empty signature change (or immediately when\n * already attached). Use this to trigger side-effects such as pushing a fresh\n * SSE state to open dashboard tabs (issue #509). The callback is optional;\n * omitting it preserves the previous behaviour exactly.\n *\n * SECRET-HANDLING: target `id`/`title`/`url` are not written to any log here.\n * Only an attach-detected stderr line is emitted (no target details).\n *\n * @returns `stop` — call this during shutdown to clear the interval.\n */\nexport function startAttachWatcher(\n connection: CdpConnection,\n server: Server,\n intervalMs = 1_000,\n onAttach?: () => void,\n): { stop(): void } {\n /** Sorted, comma-joined target-id string — '' means no targets attached. */\n function signature(): string {\n return connection\n .listTargets()\n .map((t) => t.id)\n .sort()\n .join(',');\n }\n\n let lastSignature = signature();\n // If already attached when the watcher starts, send once immediately.\n if (lastSignature !== '') {\n void server.sendToolListChanged();\n onAttach?.();\n }\n\n const handle = setInterval(() => {\n const current = signature();\n if (current !== lastSignature) {\n lastSignature = current;\n if (current !== '') {\n // Non-empty signature change — new or replaced target(s).\n void server.sendToolListChanged();\n onAttach?.();\n }\n // Empty signature (full detach): signature updated above, callback skipped.\n }\n }, intervalMs);\n\n return {\n stop() {\n clearInterval(handle);\n },\n };\n}\n\nexport interface RunDebugServerOptions {\n /**\n * Local Chii relay port. Default 0 (OS-assigned ephemeral port).\n *\n * Passing 0 lets the OS choose a free port on each startup — this prevents\n * EADDRINUSE when a stale cloudflared orphan still holds a fixed port (the\n * root cause of -32000 MCP handshake failures). Pass an explicit port number\n * only when a fixed port is specifically required (backwards-compatible).\n */\n relayPort?: number;\n /**\n * When `true`, terminates the process holding the existing server lock and\n * takes over the session. Corresponds to `--force` / `--takeover` CLI flags.\n *\n * Default `false`.\n */\n force?: boolean;\n}\n\n// `buildRelayVerifyAuth` now lives in `./totp.js` (lightweight, node:crypto\n// only) so the unplugin's env-2 relay can wire the same TOTP upgrade gate\n// without pulling the heavy MCP server module graph. Re-exported here so\n// existing importers (and tests) keep resolving it from `debug-server.js`.\nexport { buildRelayVerifyAuth };\n\n/**\n * Factory that constructs a `ChiiCdpConnection` for the given relay base URL.\n *\n * Introduced as a named seam so PR-2 (dual-connection, #348) can defer\n * construction to first-activation time by moving or replacing this call. Since\n * #396 every family (relay included) is constructed lazily on its first\n * `start_debug`, so this is always called from the lazy boot path.\n *\n * The relay base URL is only available after `startChiiRelay()` resolves, so\n * the factory is called right after that point (same as before this refactor).\n */\nfunction createRelayConnection(relayBaseUrl: string): ChiiCdpConnection {\n // Pass the SECRET (not a code) so the connection mints a fresh TOTP per\n // (re)connect. Read from env directly: both callers run\n // assertRelayAuthConfigured() first, so when a TOTP-gated relay is up this is\n // a valid hex secret; when TOTP is disabled it is undefined and no `at=` is\n // appended (backward compatible). SECRET-HANDLING: forwarded, never logged.\n return new ChiiCdpConnection({\n relayBaseUrl,\n totpSecret: process.env.AIT_DEBUG_TOTP_SECRET,\n });\n}\n\n/**\n * AIT source that always forwards over the *currently active* connection\n * (issue #348). The single-connection `ChiiAitSource` binds one sender at\n * construction; in the dual-connection daemon the AIT.* domain must follow the\n * active connection across `start_debug` swaps, so this indirection reads\n * `getActive()` on every call.\n *\n * Both `ChiiCdpConnection` and `LocalCdpConnection` expose `sendCommand`, so\n * the active connection is a valid `AitCommandSender`.\n */\nclass RoutingAitSource extends ChiiAitSource {\n constructor(\n getActive: () => {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n },\n ) {\n super({\n sendCommand: (method, params) => getActive().sendCommand(method, params),\n });\n }\n}\n\n/**\n * A booted infra family the dual router can tear down at process exit.\n *\n * Direction-neutral (issue #356): any of the three families can be the first one\n * booted. Since #396 every family is lazy-booted on its first `start_debug`. The\n * relay family additionally exposes its live tunnel status; the local family\n * leaves it `undefined` (a local browser has no relay tunnel), so the\n * router/handlers read the relay tunnel status from whichever family is the\n * relay one.\n */\nexport interface BootedFamily {\n connection: CdpConnection;\n /** Synchronous best-effort teardown (closes the connection + any infra). */\n stop(): void;\n /**\n * Live tunnel status — only the relay family provides it (the URL changes per\n * tunnel reissue). `undefined` on the local family.\n */\n getTunnelStatus?: () => TunnelStatus;\n /**\n * Relay origin discriminator (issue #378) — set by the boot fn, NOT sniffed\n * from the URL. `'intoss-webview'` for the intoss-private relay\n * (`bootRelayFamily`), `'external-pwa'` for the env-2 external relay\n * (`bootExternalRelayFamily`). `undefined` for the local family (kind is\n * `'local'`, so the origin is irrelevant). Threaded into `deriveEnvironment`\n * so `relay-mobile` can be told apart from `relay-dev`.\n */\n relayOrigin?: RelayOrigin;\n /**\n * Local HTTP base URL of the Chii relay (e.g. `http://127.0.0.1:9100` for\n * the intoss relay, or the external cloudflare URL for env-2). Used by\n * {@link AutoDevtoolsOpener} to build the Chii self-hosted inspector URL\n * (`<relayHttpUrl>/front_end/chii_app.html`). `undefined` for the local-\n * browser family (no relay, F12 is available directly).\n *\n * SECRET-HANDLING: this value contains the relay host. MUST NOT be logged.\n */\n relayHttpUrl?: string;\n /**\n * LOCAL loopback HTTP base URL of the Chii relay for env-2\n * (`http://127.0.0.1:<relay-port>`). When set, the MCP uses this instead of\n * `relayHttpUrl` (the cloudflare tunnel base) to build inspector URLs — so\n * front_end page load and the client WS leg stay on the loopback and do not\n * traverse the tunnel (issue #530).\n *\n * Only relevant for `bootExternalRelayFamily` (env-2): the intoss relay\n * (`bootRelayFamily`) already uses a loopback `relay.baseUrl`.\n *\n * Safe to log/surface: loopback address contains no tunnel host.\n */\n relayLocalHttpUrl?: string;\n}\n\n/**\n * Boots the local-browser family (issues #348, #356). Launches a Chromium with\n * `--remote-debugging-port` and returns a `LocalCdpConnection` attached to it,\n * plus a `stop()` that kills both.\n *\n * Booted lazily via the dual router's `bootLazyFor('local-browser')` callback,\n * at most once on the first `start_debug({ mode: 'local-browser' })` (all-lazy,\n * #396 — no run function boots a family at startup anymore).\n */\nexport async function bootLocalFamily(): Promise<BootedFamily> {\n const cdpPort = 0; // OS-assigned ephemeral port.\n const devUrl = process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n const chromium = await launchChromium({ port: cdpPort, devUrl });\n // Give Chromium a moment to open its CDP endpoint before first attach.\n await new Promise<void>((r) => setTimeout(r, 800));\n const connection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });\n return {\n connection,\n stop() {\n connection.close();\n chromium.stop();\n },\n };\n}\n\n/** Options for {@link bootRelayFamily}. */\nexport interface BootRelayFamilyOptions {\n /** Relay local port. Default 0 (OS-assigned ephemeral). */\n relayPort?: number;\n /**\n * TOTP `verifyAuth` predicate for the relay WS upgrade gate. Built from\n * `AIT_DEBUG_TOTP_SECRET` at the call site via {@link buildRelayVerifyAuth}.\n * `undefined` disables the gate.\n */\n verifyAuth?: (req: import('node:http').IncomingMessage) => boolean;\n /**\n * Called whenever the public tunnel URL is (re)assigned, so the caller can\n * mirror it into the server lock file (`lockHandle.updateWssUrl`). The wssUrl\n * carries the relay host — callers MUST NOT log it directly.\n */\n onWssUrl?: (wssUrl: string) => void;\n /**\n * Secret-free observability callback for relay auth rejections (issue #467) —\n * forwarded to {@link startChiiRelay}'s `onAuthReject`. Receives only the\n * rejection kind; never the URL, query, code, or secret. Boot sites wire it\n * to `DiagnosticsCollector.recordAuthReject()` so `get_debug_status` can\n * surface silent 401s.\n */\n onAuthReject?: (event: import('./chii-relay.js').RelayAuthRejectEvent) => void;\n /**\n * Called with the cloudflared child PID once the tunnel is up.\n *\n * FIX 3 (issue #571): callers wire this to\n * `lockHandle.updateTunnelChildPid(pid)` so the lock file records the child\n * PID and a subsequent `acquireLock` can detect a zombie daemon (Node\n * process alive, tunnel child dead) without requiring `--force`.\n */\n onTunnelChildPid?: (pid: number) => void;\n}\n\n/**\n * Boots the relay family (issues #348, #356): starts the Chii relay on an\n * OS-assigned port (with optional TOTP gate), opens a cloudflared quick tunnel\n * to the relay's confirmed port in the background, prints the attach banner,\n * and arms the tunnel health probe. Returns a {@link BootedFamily} whose\n * `getTunnelStatus()` reflects the live tunnel (it flips up once the background\n * tunnel resolves and follows reissues).\n *\n * Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback\n * (symmetry with {@link bootLocalFamily}), at most once on the first\n * `start_debug({ mode: 'relay-staging' | 'relay-live' })` (all-lazy, #396 — every\n * relay boot now flows through `switchMode` after the project-local secret load).\n *\n * The relay base URL is only known after `startChiiRelay()` resolves, so the\n * `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside\n * this function, after the relay port is confirmed.\n *\n * SECRET-HANDLING: the TOTP secret rides only inside `verifyAuth`; the wssUrl\n * (relay host) is never logged here directly.\n */\nexport async function bootRelayFamily(options: BootRelayFamilyOptions = {}): Promise<BootedFamily> {\n // Relay-auth baseline (issue #250): this boots a public-internet-exposed relay\n // (cloudflared quick tunnel), so a configured TOTP secret is MANDATORY — Layer\n // C is the only fail-fast layer that stops a leaked tunnel URL from attaching.\n // Fail fast before opening the relay/tunnel. Local-only sessions never call\n // this fn and so stay exempt. SECRET-HANDLING: the guard never logs the value.\n assertRelayAuthConfigured();\n\n // Default 0: OS picks a free port. Prevents EADDRINUSE from stale cloudflared\n // orphans (SIGKILL survivors) that would otherwise block a fixed port and\n // cause -32000 MCP handshake failures on reconnect.\n const relayPort = options.relayPort ?? 0;\n const totpEnabled = options.verifyAuth !== undefined;\n\n const relay = await startChiiRelay({\n port: relayPort,\n verifyAuth: options.verifyAuth,\n onAuthReject: options.onAuthReject,\n });\n // relay.port is the actual OS-assigned port (may differ from relayPort when 0).\n logInfo('server.start', { port: relay.port, totpEnabled });\n\n let tunnel: QuickTunnel | null = null;\n let tunnelStatus: TunnelStatus = makeTunnelStatus(false, null);\n let tunnelProbe: { stop(): void } | null = null;\n // generateAttachToken is kept for legacy/non-TOTP token use, but we no longer\n // print it in the banner to avoid accidental secret exposure.\n const _token = generateAttachToken();\n\n // Bring the cloudflared tunnel up in the background so the MCP stdio transport\n // can answer `initialize` immediately. cloudflared has to lazy-download a\n // ~38 MB binary on first run; awaiting it here pushes the initialize response\n // past Claude Code's MCP connection timeout. Tools that need the tunnel\n // (`build_attach_url`) already gate on `getTunnelStatus()` and return a clear\n // \"tunnel not up\" message when it isn't ready yet, so dropping the await is\n // safe — the agent retries once the banner prints.\n const tunnelReady = startQuickTunnel(relay.port).then(\n (t) => {\n tunnel = t;\n tunnelStatus = makeTunnelStatus(true, t.wssUrl);\n options.onWssUrl?.(t.wssUrl);\n // FIX 3 (issue #571): notify caller of the cloudflared child PID so it\n // can be persisted in the server lock file for zombie detection.\n // childPid is a plain integer — not a secret.\n if (t.childPid !== undefined) {\n options.onTunnelChildPid?.(t.childPid);\n }\n // SECRET-HANDLING: wssUrl contains the relay host — do not log it directly.\n logInfo('tunnel.up', { totpEnabled });\n\n // Start the health probe now that the tunnel URL is known.\n // The probe runs every 60 s and attempts up to 3 reissues on drop.\n tunnelProbe = startTunnelHealthProbe(t, relay.port, {\n onReissue: (newTunnel) => {\n tunnel = newTunnel;\n tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);\n options.onWssUrl?.(newTunnel.wssUrl);\n // FIX (issue #572 review): update the lock's tunnelChildPid so a later\n // acquireLock sees the reissued tunnel's child — not the original dead one.\n // childPid is a plain integer — not a secret.\n if (newTunnel.childPid !== undefined) {\n options.onTunnelChildPid?.(newTunnel.childPid);\n }\n // Reprint the banner so the user (and agent) see the new URL + QR.\n void printAttachBanner({ wssUrl: newTunnel.wssUrl, totpEnabled }).then(() => {\n logInfo('tunnel.up', { totpEnabled, reissued: true });\n });\n },\n onPermanentDrop: (droppedAt) => {\n tunnelStatus = makeTunnelStatus(false, null, droppedAt, 3);\n logError('tunnel.down', {\n msg: `tunnel permanently dropped (${droppedAt}). Restart: npx @ait-co/devtools devtools-mcp`,\n });\n },\n });\n\n return printAttachBanner({ wssUrl: t.wssUrl, totpEnabled });\n },\n (err) => {\n const message = err instanceof Error ? err.message : String(err);\n logError('tunnel.down', {\n msg: `Failed to open cloudflared quick tunnel: ${message}. The relay is up locally; attach over the public URL is unavailable until the tunnel starts.`,\n });\n },\n );\n // Reference the promise to placate the linter — actual completion is observed\n // via the side-effects on `tunnelStatus` from inside `.then`.\n void tunnelReady;\n\n const connection = createRelayConnection(relay.baseUrl);\n\n return {\n connection,\n // Intoss-private dog-food/live relay (env 3/4) → relay-dev / relay-live.\n relayOrigin: 'intoss-webview',\n // Local HTTP base of the Chii relay — used by AutoDevtoolsOpener to build\n // the self-hosted inspector URL. SECRET-HANDLING: not logged.\n relayHttpUrl: relay.baseUrl,\n getTunnelStatus: () => tunnelStatus,\n stop() {\n tunnelProbe?.stop();\n // tunnel.stop() is synchronous (child process kill) — safe from exit handler.\n tunnel?.stop();\n connection.close();\n // relay.close() is async — fine for signal/exit handlers.\n void relay.close();\n },\n };\n}\n\n/**\n * Boots the EXTERNAL relay family for env 2 (real-device PWA, issue #378).\n *\n * Unlike {@link bootRelayFamily}, this does NOT start a relay or a tunnel —\n * the unplugin (`tunnel: { cdp: true }`) already brought up a Chii relay for\n * the env-2 PWA and exposed its public base URL via `AIT_RELAY_BASE_URL`. Here\n * the MCP only opens a CDP client (`createRelayConnection`) against that\n * external relay. The relay's lifecycle is owned by the unplugin, so `stop()`\n * closes ONLY the CDP client — it must never tear down the relay or a tunnel\n * we did not start.\n *\n * `getTunnelStatus()` reports `up: true` with a `wssUrl` derived from\n * `relayBaseUrl` (http→ws, https→wss) so the `build_attach_url` gate\n * (`up: true && wssUrl !== null`) is satisfied even though we never opened a\n * cloudflared tunnel ourselves.\n *\n * SECRET-HANDLING: `relayBaseUrl` carries the relay host (same sensitivity as a\n * wss URL) — it is NEVER logged here. The caller validates presence and passes\n * the value straight to the CDP client.\n */\n/**\n * Attempts to read the local loopback HTTP base URL of the env-2 Chii relay\n * (issue #530). Resolution order:\n * 1. `AIT_RELAY_LOCAL_URL` env var, if set and non-empty.\n * 2. `relayLocalUrl` from the `.ait_urls` file, if `projectRoot` is given.\n * 3. `undefined` — caller falls back to the tunnel base (existing behavior).\n *\n * This is a best-effort read — never throws. The returned value is a plain\n * `http://127.0.0.1:<port>` loopback URL; no secret exposure.\n */\nexport async function readRelayLocalUrl(\n env: NodeJS.ProcessEnv = process.env,\n projectRoot?: string,\n): Promise<string | undefined> {\n const envValue = (env.AIT_RELAY_LOCAL_URL ?? '').trim();\n if (envValue !== '') return envValue;\n\n if (projectRoot !== undefined) {\n try {\n const { readRelayUrls } = await import('./relay-url-store.js');\n const stored = await readRelayUrls({ projectRoot });\n if (stored?.relayLocalUrl) return stored.relayLocalUrl;\n } catch {\n // Silent best-effort.\n }\n }\n return undefined;\n}\n\nexport async function bootExternalRelayFamily(\n relayBaseUrl: string,\n relayLocalUrl?: string,\n): Promise<BootedFamily> {\n // Relay-auth baseline (issue #250): the env-2 PWA relay is reachable over a\n // public `*.trycloudflare.com` tunnel (started by the unplugin). The Layer C\n // TOTP gate is what blocks a leaked tunnel URL, so a configured secret is\n // MANDATORY here too. The unplugin's relay reads the SAME `AIT_DEBUG_TOTP_SECRET`,\n // so this also fails fast when the operator forgot to set it. Fail before\n // opening the CDP client. SECRET-HANDLING: the guard never logs the value.\n assertRelayAuthConfigured();\n\n const connection = createRelayConnection(relayBaseUrl);\n // Derive the public wss URL from the relay base so build_attach_url's\n // `up && wssUrl !== null` gate passes. SECRET-HANDLING: not logged.\n const externalWss = relayBaseUrl.replace(/^http/, 'ws');\n const tunnelStatus = makeTunnelStatus(true, externalWss);\n return {\n connection,\n // External env-2 PWA relay → relay-mobile (distinct from relay-dev).\n relayOrigin: 'external-pwa',\n // HTTP base of the external relay — used as fallback for inspector URL.\n // For env-2 this is the cloudflare tunnel URL (https://<host>.trycloudflare.com).\n // SECRET-HANDLING: not logged.\n relayHttpUrl: relayBaseUrl,\n // LOCAL loopback base for inspector URL assembly (issue #530) — preferred\n // over relayHttpUrl when available so front_end + client WS stay local.\n // Safe to log: loopback URL contains no tunnel host.\n relayLocalHttpUrl: relayLocalUrl,\n getTunnelStatus: () => tunnelStatus,\n stop() {\n // The unplugin owns the relay + its tunnel — close ONLY our CDP client.\n connection.close();\n },\n };\n}\n\n/**\n * Identifies a booted family slot in the dual router (issue #378).\n *\n * Before #378 the router warm-kept a single \"opposite-kind\" lazy family, which\n * could not hold both an intoss relay (`relay-staging`/`relay-live`) AND an\n * external relay (`relay-sandbox`) at once — they are both `kind: 'relay'` and\n * would collide in the single slot. The three keys separate the three distinct\n * families (4 exposed modes → 3 physical slots — `relay-staging`/`relay-live`\n * share `'relay-intoss'`, see {@link familyKeyForMode}):\n *\n * - `'local-browser'` — local Chromium + mock SDK (env 1).\n * - `'relay-intoss'` — intoss-private relay (env 3/4, `bootRelayFamily`).\n * - `'relay-sandbox'` — env-2 external PWA relay (`bootExternalRelayFamily`).\n */\nexport type FamilyKey = 'local-browser' | 'relay-intoss' | 'relay-sandbox';\n\n/**\n * Maps a `StartDebugMode` to the {@link FamilyKey} that serves it (issue #378).\n * local-browser → 'local-browser'; relay-sandbox → 'relay-sandbox';\n * relay-staging/relay-live → 'relay-intoss' (the shared physical slot).\n */\nexport function familyKeyForMode(mode: StartDebugMode): FamilyKey {\n switch (mode) {\n case 'local-browser':\n return 'local-browser';\n case 'relay-sandbox':\n return 'relay-sandbox';\n case 'relay-staging':\n case 'relay-live':\n return 'relay-intoss';\n }\n}\n\n/** The error thrown / surfaced when entering `mobile` without AIT_RELAY_BASE_URL. */\nexport const MOBILE_RELAY_BASE_URL_MISSING_MESSAGE =\n 'start_debug(mobile): AIT_RELAY_BASE_URL이 설정되지 않았습니다. ' +\n 'dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. ' +\n '자동 발견이 되지 않을 경우 relay base URL을 AIT_RELAY_BASE_URL 환경변수로 직접 전달하세요. ' +\n '환경 2(실기기 PWA) 진입은 외부 relay base가 필요합니다.';\n\n/**\n * Reads the env-2 relay base URL for the `mobile` boot site (issue #378, #424).\n *\n * Resolution order (env wins — file is the fallback):\n * 1. `env.AIT_RELAY_BASE_URL` set and non-empty → return it (operator override).\n * 2. `projectRoot` given → read `<nearest package.json dir>/.ait_urls`;\n * if `relayBaseUrl` is present → return it (auto-discovered from dev server).\n * 3. Neither → throw {@link MOBILE_RELAY_BASE_URL_MISSING_MESSAGE}.\n *\n * SECRET-HANDLING: `AIT_RELAY_BASE_URL` and the file-discovered value carry the\n * relay host. On the missing path the thrown message names the env var and notes\n * that the dev server auto-publishes it — it NEVER echoes any URL value. The\n * present value is returned to the caller (the CDP client) but never logged.\n */\nexport async function readMobileRelayBaseUrl(\n env: NodeJS.ProcessEnv = process.env,\n projectRoot?: string,\n): Promise<string> {\n // 1. Env wins — operator override.\n const raw = env.AIT_RELAY_BASE_URL;\n const envValue = typeof raw === 'string' ? raw.trim() : '';\n if (envValue !== '') {\n return envValue;\n }\n\n // 2. File fallback — auto-discovered from dev server (#424).\n if (projectRoot !== undefined) {\n const { readRelayUrls } = await import('./relay-url-store.js');\n const stored = await readRelayUrls({ projectRoot });\n if (stored?.relayBaseUrl !== undefined) {\n return stored.relayBaseUrl;\n }\n }\n\n // 3. Neither source — throw the precise guidance message.\n throw new Error(MOBILE_RELAY_BASE_URL_MISSING_MESSAGE);\n}\n\n/**\n * Options the dual router needs to re-arm the attach watcher and auto-open\n * DevTools after a swap (issues #348, #356, #378, #396).\n *\n * All-lazy (#396): NO family is booted at startup — every family boots lazily on\n * its first `start_debug` via `bootLazyFor(key)`. This routes EVERY relay boot\n * through `switchMode` (which runs `loadRelaySecretReadOnly` first), closing the\n * gap where an eager startup boot bypassed the project-local secret load. The\n * router is direction-neutral (#356): any of the three families can be the first\n * one booted, so a session can hot-switch in any direction without a restart.\n */\nexport interface DualRouterDeps {\n /**\n * Lazy boot for the family identified by `key` — called at most once per key,\n * on the first `start_debug` whose family key has not yet been booted (issue\n * #378 — keyed so an intoss relay and an external relay can be warm-kept\n * simultaneously). Since #396 NO family is booted eagerly, so this boots the\n * family for ANY of the three FamilyKey values on first use.\n *\n * `projectRoot` is threaded from the per-session `start_debug` call (#424) so\n * `relay-sandbox` boot can fall back to the `.ait_urls` file discovery when\n * `AIT_RELAY_BASE_URL` is not set.\n */\n bootLazyFor: (key: FamilyKey, projectRoot?: string) => Promise<BootedFamily>;\n /** Diagnostics collector (re-armed watcher records attach there). */\n diagnosticsCollector: DiagnosticsCollector;\n /** Auto-opens Chrome DevTools on the first relay attach (env 3/4 only). */\n devtoolsOpener: AutoDevtoolsOpener;\n /** Attach-watcher poll interval (ms). Default 1 000. */\n attachWatcherIntervalMs?: number;\n /**\n * Called on every non-empty target-signature change (first attach, target\n * replacement, or re-attach after detach). Used by run functions to push a\n * dashboard SSE notification so open browser tabs receive fresh target id\n * and TOTP links (issue #509).\n */\n onPageAttach?: () => void;\n /**\n * Returns the stable `/inspector` URL from the QR HTTP server (issue #530).\n * Called by `armWatcher` to pass to `AutoDevtoolsOpener.open()` so it can\n * open the secret-free stable URL instead of building a direct TOTP URL.\n * Returns null if the QR server is not yet started.\n */\n getInspectorStableUrl?: () => string | null;\n}\n\n/**\n * Sentinel connection returned by {@link DualConnectionRouter.active} before the\n * first `start_debug` boots a family (all-lazy, issue #396). It satisfies the\n * full {@link CdpConnection} interface but holds nothing: `listTargets()` is\n * empty, every command rejects with a clear \"call start_debug first\" message,\n * and all event/teardown members are safe no-ops. Callers that read tools before\n * any switchMode therefore get an honest empty/down state instead of an NPE.\n */\nconst NULL_CDP_CONNECTION: CdpConnection = {\n kind: 'local',\n enableDomains: () => Promise.resolve(),\n listTargets: () => [],\n getBufferedEvents: () => [],\n on: () => () => {},\n send: () => Promise.reject(new Error('no family booted yet — call start_debug first')),\n close: () => {},\n};\n\n/**\n * Production `ConnectionRouter` (issues #348, #356, #378 — DUAL-CONNECTION-COEXIST).\n *\n * Holds a keyed set of lazily-booted families ({@link FamilyKey} →\n * `BootedFamily`, issue #378) with NO family active at startup (issue #396); the\n * first `start_debug` boots and activates one. Plus an `active` pointer and the\n * single attach watcher armed on the active connection. The router is\n * **direction-neutral** (#356): any family can be the first one booted, so a\n * `--target=local` session can hot-switch into relay (and vice versa) without\n * restarting the MCP server.\n *\n * Why a KEYED map and not a single lazy slot (#378): `relay-sandbox` (env-2\n * external relay) and `relay-staging`/`relay-live` (intoss relay) are BOTH\n * `kind: 'relay'`. A single \"opposite-kind\" slot could not warm-keep both at\n * once — they would collide. The three `FamilyKey`s\n * (`local-browser` / `relay-intoss` / `relay-sandbox`) give each its own warm\n * slot — `relay-staging` and `relay-live` deliberately share the one\n * `relay-intoss` slot (wire-identical, distinguished only by `liveIntent`).\n *\n * Why all-lazy (#396): the relay TOTP secret now lives in a project-local\n * `.ait_relay` file loaded read-only by `switchMode` BEFORE a relay family boots.\n * Booting any family eagerly at startup would bypass that load. With NO eager\n * boot every relay boot flows through `switchMode → loadRelaySecretReadOnly`, so\n * the secret is always populated before `assertRelayAuthConfigured()` /\n * `buildRelayVerifyAuth()` run at the boot site.\n *\n * `switchMode`:\n * 1. rejects re-entrant swaps (`swapInFlight`) and an unconfirmed `relay-live`;\n * 2. resolves the requested mode's `FamilyKey`:\n * `lazyFamilies.get(key) ?? (boot via bootLazyFor(key), store)`;\n * 3. flips `active` (the MCP `Server` never re-handshakes — it reads through\n * `active` per request);\n * 4. sets `liveIntent` (true only for `relay-live`; `relay-sandbox` is dev-intent → false);\n * 5. stops the old attach watcher and re-arms one on the new connection\n * (the watcher self-clears, so re-arm is mandatory);\n * 6. emits `tools/list_changed`.\n *\n * Inactive infra is left WARM — teardown happens only at process exit (the\n * unified shutdown in the run functions), which is what keeps a phone attach\n * alive across a local→relay→local round trip.\n */\nexport class DualConnectionRouter implements ConnectionRouter {\n private readonly deps: DualRouterDeps;\n /** Families, booted lazily and warm-kept per {@link FamilyKey} (#378, #396). */\n private readonly lazyFamilies = new Map<FamilyKey, BootedFamily>();\n /** `null` until the first `start_debug` boots a family (all-lazy, #396). */\n private activeFamily: BootedFamily | null = null;\n private server: Server | null = null;\n private attachWatcher: { stop(): void } | null = null;\n private swapInFlight = false;\n\n constructor(deps: DualRouterDeps) {\n this.deps = deps;\n }\n\n get active(): CdpConnection {\n return this.activeFamily ? this.activeFamily.connection : NULL_CDP_CONNECTION;\n }\n\n /** Relay origin of the currently-active family (issue #378). */\n get activeRelayOrigin(): RelayOrigin | undefined {\n return this.activeFamily?.relayOrigin;\n }\n\n /**\n * HTTP base URL of the Chii relay to use for inspector URL assembly (#503,\n * #530). Prefers the LOCAL loopback base (`relayLocalHttpUrl`) when available\n * so front_end page load + client WS do not traverse a cloudflare tunnel —\n * falls back to `relayHttpUrl` (the tunnel base for env-2, loopback for env-3/4)\n * when not set. Returns `undefined` when no relay family is active.\n *\n * SECRET-HANDLING: when relayLocalHttpUrl is absent this falls back to\n * relayHttpUrl which may carry the tunnel host — callers must not log it.\n */\n get activeRelayHttpUrl(): string | undefined {\n if (!this.activeFamily) return undefined;\n return this.activeFamily.relayLocalHttpUrl ?? this.activeFamily.relayHttpUrl;\n }\n\n /** Every booted family (for unified shutdown). All families are lazy (#396). */\n bootedFamilies(): BootedFamily[] {\n return [...this.lazyFamilies.values()];\n }\n\n /**\n * Live tunnel status of the active relay family (issues #356, #378). Reads\n * the ACTIVE family's tunnel when it has one (so `relay-sandbox` surfaces the\n * external relay wss and `relay-staging`/`relay-live` the intoss relay wss); otherwise\n * falls back to the first booted family that has a tunnel. Returns \"down\"\n * until any relay family is booted (any session before the first relay\n * start_debug) — the correct signal for `build_attach_url` (no tunnel yet).\n */\n relayTunnelStatus(): TunnelStatus {\n if (this.activeFamily?.getTunnelStatus) return this.activeFamily.getTunnelStatus();\n for (const family of this.bootedFamilies()) {\n if (family.getTunnelStatus) return family.getTunnelStatus();\n }\n return { up: false, wssUrl: null };\n }\n\n /**\n * Binds the MCP `Server`; the attach watcher is armed by the first\n * `start_debug` since no family is active at startup (all-lazy, #396). Called\n * once after `createDebugServer` + `connect`.\n */\n start(server: Server): void {\n this.server = server;\n this.armWatcher();\n }\n\n /** Stops the current attach watcher (for shutdown). */\n stopWatcher(): void {\n this.attachWatcher?.stop();\n this.attachWatcher = null;\n }\n\n /** Arms a fresh attach watcher on the current active connection. */\n private armWatcher(): void {\n const server = this.server;\n if (!server) return;\n // No family active yet (all-lazy, #396) — nothing to watch until the first\n // `start_debug` boots one and re-arms the watcher.\n const activeFamily = this.activeFamily;\n if (!activeFamily) return;\n this.attachWatcher = startAttachWatcher(\n activeFamily.connection,\n server,\n this.deps.attachWatcherIntervalMs ?? 1_000,\n () => {\n this.deps.diagnosticsCollector.recordAttach();\n // Notify dashboard of page attach — SSE push so the browser tab updates.\n this.deps.onPageAttach?.();\n // Auto-open Chii DevTools only for a relay attach (env 2/3/4). The\n // opener no-ops for a local (mock) connection — guard on the active\n // kind so a local session never tries to open a relay devtools.\n // AutoDevtoolsOpener._opened is a once-per-session guard, so repeat\n // fires (target replacement) do not open an extra browser window.\n if (activeFamily.connection.kind === 'relay') {\n // Take the first attached target's id — we are in the onAttach\n // callback, so listTargets() is guaranteed to be non-empty.\n const firstTarget = activeFamily.connection.listTargets()[0];\n const env = deriveEnvironment(\n activeFamily.connection.kind,\n getLiveIntent(),\n activeFamily.relayOrigin,\n );\n // Prefer the stable /inspector URL (issue #530): secret-free, no\n // expiry race. Falls back to the direct URL path when qrServer is\n // not yet available (should not happen in practice).\n const inspectorStableUrl = this.deps.getInspectorStableUrl?.() ?? null;\n this.deps.devtoolsOpener.open({\n inspectorStableUrl,\n relayHttpBaseUrl: activeFamily.relayHttpUrl,\n targetId: firstTarget?.id,\n // Mint a fresh TOTP code from the daemon's secret at open time.\n // The relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps (~3 min).\n // SECRET-HANDLING: the closure captures only the getter, never logs.\n // Only used when inspectorStableUrl is absent (legacy path).\n mintTotp: process.env.AIT_DEBUG_TOTP_SECRET\n ? () => generateTotp(process.env.AIT_DEBUG_TOTP_SECRET as string)\n : undefined,\n env,\n });\n }\n },\n );\n }\n\n /**\n * Resolves the `BootedFamily` for `key`: the warm family if already booted,\n * otherwise boots it via `bootLazyFor(key, projectRoot)` and stores it (once\n * per key). Since #396 every family is lazy, so this is the single boot path\n * for all three keys.\n *\n * `projectRoot` is forwarded to `bootLazyFor` so `relay-sandbox` boot can\n * fall back to `.ait_urls` file discovery (#424) when `AIT_RELAY_BASE_URL` is\n * not set in the environment.\n */\n private async familyFor(key: FamilyKey, projectRoot?: string): Promise<BootedFamily> {\n const warm = this.lazyFamilies.get(key);\n if (warm) return warm;\n const booted = await this.deps.bootLazyFor(key, projectRoot);\n this.lazyFamilies.set(key, booted);\n return booted;\n }\n\n async switchMode(\n mode: StartDebugMode,\n confirm: boolean,\n projectRoot?: string,\n ): Promise<ModeSwitchReport> {\n if (this.swapInFlight) {\n throw new Error('start_debug: 이전 전환이 아직 진행 중입니다 — 잠시 후 다시 호출하세요.');\n }\n if (mode === 'relay-live' && !confirm) {\n throw new Error(\n 'start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — ' +\n '실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.',\n );\n }\n\n this.swapInFlight = true;\n try {\n // (1) Project-local relay secret load (issue #396). When entering a relay\n // family, read the relay TOTP secret read-only from\n // <projectRoot>/.ait_relay into process.env BEFORE the relay boots, so the\n // lazy boot's assertRelayAuthConfigured() + buildRelayVerifyAuth() (both\n // read env at the boot site) see it. The daemon NEVER mints — a missing or\n // invalid file leaves env untouched and the boot-site assert remains the\n // single #250 fail-fast. Local switches need no secret, so skip the load.\n // SECRET-HANDLING: loadRelaySecretReadOnly never logs the value or path.\n if (isRelayMode(mode)) {\n await loadRelaySecretReadOnly({ projectRoot });\n }\n\n // (2) Resolve the family by key (#378). `bootLazyFor` may throw (e.g.\n // mobile without AIT_RELAY_BASE_URL / .ait_urls) — let it propagate\n // WITHOUT flipping active or arming liveIntent, so a failed entry leaves\n // state untouched. Pass projectRoot so relay-sandbox boot can discover\n // the relay URL from .ait_urls (#424).\n const target = await this.familyFor(familyKeyForMode(mode), projectRoot);\n\n // (3) Flip the active pointer. The MCP Server reads through `active` per\n // request, so no re-handshake / restart is needed.\n this.activeFamily = target;\n\n // (4) Arm/disarm liveIntent. true only for relay-live; any other mode\n // (including local-browser and relay-sandbox) disarms it — relay-sandbox\n // is dev-intent.\n setLiveIntent(mode === 'relay-live');\n\n // (5) Re-arm the attach watcher on the new connection (self-clearing).\n this.stopWatcher();\n this.armWatcher();\n\n // (6) Tell the MCP host the tool surface may have changed (env flip).\n void this.server?.sendToolListChanged();\n\n const wantRelay = isRelayMode(mode);\n const environment = deriveEnvironment(\n target.connection.kind,\n getLiveIntent(),\n target.relayOrigin,\n );\n return {\n mode,\n environment,\n kind: target.connection.kind,\n liveGuardActive: target.connection.kind === 'relay' && getLiveIntent(),\n nextStep: wantRelay\n ? 'build_attach_url로 attach QR을 생성하세요 (relay 세션).'\n : 'list_pages로 로컬 Chromium 페이지 attach를 확인하세요.',\n };\n } finally {\n this.swapInFlight = false;\n }\n }\n}\n\n/**\n * Boots the live debug stack and serves it over stdio:\n * 1. start the Chii relay on an OS-assigned port (with TOTP auth if\n * AIT_DEBUG_TOTP_SECRET is set),\n * 2. open a cloudflared quick tunnel to the relay's confirmed port,\n * 3. print relay URL + attach instructions,\n * 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.\n */\nexport async function runDebugServer(options: RunDebugServerOptions = {}): Promise<void> {\n // Enforce a single debug session per machine. If another server is alive,\n // ServerLockConflictError is thrown — the MCP host surfaces the message to\n // the agent without a relay or cloudflared ever starting.\n // `force: true` kills the existing process and takes over the lock.\n const lockHandle = acquireLock({ force: options.force ?? false });\n\n // Dual-connection router (issues #348, #356, #378, #396): ALL families are\n // lazy-booted on the first matching `start_debug`. Nothing boots at startup —\n // every relay boot flows through `switchMode → loadRelaySecretReadOnly` first,\n // so the project-local `.ait_relay` secret is always loaded before the relay\n // boot's assertRelayAuthConfigured() / buildRelayVerifyAuth() read the env.\n const devtoolsOpener = new AutoDevtoolsOpener();\n // Diagnostics collector — records server-side errors and attach/detach events\n // so `get_debug_status` can surface them in a single call.\n const diagnosticsCollector = new InMemoryDiagnosticsCollector();\n\n // FIX (issue #572 review): track the live cloudflared child PID in memory so\n // get_debug_status can pass it to getDiagnostics as source (a). Updated by\n // onTunnelChildPid on initial boot and on every reissue.\n let activeTunnelChildPid: number | null = null;\n\n const router = new DualConnectionRouter({\n // Lazy resolver for all three family slots (#378, #396, #424).\n // SECRET-HANDLING: readMobileRelayBaseUrl reads AIT_RELAY_BASE_URL (or .ait_urls\n // fallback) only here, at the mobile boot site, and never logs its value.\n // verifyAuth is built INSIDE the lambda (lazily, at the relay boot site) so it\n // reads the env AFTER switchMode's project-local secret load (#396) has\n // populated AIT_DEBUG_TOTP_SECRET — never captured at server startup.\n bootLazyFor: async (key, projectRoot) =>\n key === 'relay-sandbox'\n ? bootExternalRelayFamily(\n await readMobileRelayBaseUrl(process.env, projectRoot),\n await readRelayLocalUrl(process.env, projectRoot),\n )\n : key === 'local-browser'\n ? bootLocalFamily()\n : bootRelayFamily({\n relayPort: options.relayPort,\n verifyAuth: buildRelayVerifyAuth(),\n // Mirror the assigned tunnel URL into the lock file so a second\n // caller sees the correct wssUrl in the conflict error message, and\n // notify the dashboard SSE clients of the tunnel URL change.\n onWssUrl: (wssUrl) => {\n lockHandle.updateWssUrl(wssUrl);\n qrServer?.notifyStateChange();\n },\n // FIX 3 (issue #571): persist the cloudflared child PID in the\n // lock file so a subsequent acquireLock can detect zombie daemons.\n // Also update the in-memory tracker (source a for FIX 2).\n onTunnelChildPid: (pid) => {\n activeTunnelChildPid = pid;\n lockHandle.updateTunnelChildPid(pid);\n },\n // Issue #467: count relay TOTP 401s (secret-free) so\n // get_debug_status can distinguish \"phone never arrived\" from\n // \"phone arrived but was rejected\".\n onAuthReject: () => diagnosticsCollector.recordAuthReject(),\n }),\n diagnosticsCollector,\n devtoolsOpener,\n onPageAttach: () => qrServer?.notifyStateChange(),\n // Stable /inspector URL for auto-open (issue #530). qrServer is set after\n // the router is created but before armWatcher fires, so the closure safely\n // captures it by reference.\n getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,\n });\n\n // AIT.* methods ride the *active* connection's command channel (relay Chii or\n // local CDP), so the AIT source follows `start_debug` swaps.\n const aitSource = new RoutingAitSource(() => {\n const active = router.active as CdpConnection & {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n };\n return active;\n });\n\n // dashboard용 lastAttachParts 상태 — build_attach_url 호출마다 갱신.\n // 완성 URL 대신 컴포넌트를 저장해 getDashboardState 호출마다 fresh TOTP를 mint (Defect 1).\n // SECRET-HANDLING: 컴포넌트에는 tunnel/scheme host가 있으므로 로그 출력 금지.\n let lastAttachParts: AttachUrlParts | null = null;\n\n // getDashboardState 클로저 — qr-http-server dashboard에 현재 상태 전달.\n // rebuildAttachUrl()로 매 호출마다 최신 TOTP 코드를 mint한 URL을 생성한다 (Defect 1).\n // inspectorUrl은 안정 /inspector URL(issue #530) — 시크릿 없으므로 출력 가능.\n const getDashboardState = (): DashboardState => {\n const targets = router.active.listTargets();\n // inspectorUrl — /inspector 안정 진입점 (issue #530).\n // qrServer가 아직 없으면 null(초기화 직후 race). qrServer가 생기면 항상 안정 URL.\n // 클릭 시점에 TOTP를 mint하고 302 redirect하므로 stale 문제가 없다.\n // SECRET-HANDLING: /inspector URL 자체에 시크릿 없음 — 출력 가능.\n const inspectorUrl = qrServer?.inspectorStableUrl ?? null;\n return {\n tunnel: { up: router.relayTunnelStatus().up, wssUrl: router.relayTunnelStatus().wssUrl },\n pages: targets.map((t) => ({ id: t.id, url: t.url })),\n attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,\n inspectorUrl,\n // 현재 active connection에서 매 호출마다 파생한 env — /attach 카피·환경 라벨\n // 분기(#468). start_debug family swap을 따라가도록 저장하지 않고 파생한다.\n mode: deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin),\n };\n };\n\n // getDirectInspectorUrl — /inspector 라우트에서 직접 chii front_end URL을 조립.\n // getDashboardState().inspectorUrl(= /inspector 자기 자신)을 쓰면 무한 루프가 발생하므로\n // 별도 getter로 분리한다. 매 요청마다 호출되어 TOTP를 요청 시점에 mint한다.\n // SECRET-HANDLING: ok:true url에 relay host + at= 코드가 담긴다 — 로그/stdout 출력 금지.\n const getDirectInspectorUrl = (): ReturnType<\n NonNullable<QrHttpServerOptions['getDirectInspectorUrl']>\n > => {\n const relayHttpUrl = router.activeRelayHttpUrl;\n if (!relayHttpUrl) {\n return { ok: false, reason: 'relayDown' };\n }\n const targets = router.active.listTargets();\n if (targets.length === 0) {\n return { ok: false, reason: 'noTarget' };\n }\n const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;\n if (!totpSecret) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n const url = buildChiiInspectorUrl(relayHttpUrl, targets[0].id, () =>\n generateTotp(totpSecret, Date.now()),\n );\n if (url === null) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n return { ok: true, url };\n };\n\n // 로컬 QR HTTP 서버를 await로 시작 — build_attach_url 첫 호출이 qrHttpServer 확인 전에\n // 도달하는 race를 없애기 위해 cloudflared(fire-and-forget)와 달리 동기 await 사용.\n // GUI 없는 환경에서는 startQrHttpServer가 실패해도 text QR fallback으로 동작한다.\n let qrServer: QrHttpServer | undefined;\n try {\n qrServer = await startQrHttpServer(getDashboardState, { getDirectInspectorUrl });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logWarn('server.start', { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}` });\n }\n\n // TOTP 주기 갱신 타이머 — 이벤트 없이 페이지가 방치될 때 at= 코드가 stale되는 갭 수정 (#445).\n // TOTP step은 30초이므로 20초 주기로 push해 step 경계를 놓치지 않는다.\n // SECRET-HANDLING: 콜백은 단순 trigger만 — TOTP 값·at= 코드는 절대 로그/stdout에 출력 금지.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = null;\n totpRefreshHandle = setInterval(() => {\n if (lastAttachParts !== null) {\n qrServer?.notifyStateChange();\n }\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const server = createDebugServer({\n // `connection` is still required by the deps shape; the router overrides\n // which connection the handlers actually read (NULL until the first switch).\n connection: router.active,\n router,\n aitSource,\n // Tunnel status follows the active relay family once one is lazy-booted (#356).\n getTunnelStatus: () => router.relayTunnelStatus(),\n // FIX (issue #572 review): expose the live cloudflared child PID (source a)\n // so get_debug_status can feed it into getDiagnostics for the FIX 2 probe.\n getTunnelChildPid: () => activeTunnelChildPid,\n get qrHttpServer() {\n return qrServer;\n },\n diagnosticsCollector,\n // SECRET-HANDLING: the TOTP secret is read from env AT CALL TIME (inside\n // build_attach_url) so the project-local .ait_relay secret loaded by\n // switchMode (#396) is visible. It is used only to generate the at= code and\n // is never logged or surfaced in any output.\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n // dashboard 갱신 콜백 — URL 컴포넌트 저장 후 SSE push.\n // 컴포넌트를 저장해 getDashboardState가 fresh TOTP로 URL을 재빌드 (Defect 1).\n onAttachUrlBuilt: (parts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n },\n });\n\n const transport = new StdioServerTransport();\n\n // ---------------------------------------------------------------------------\n // Unified dual-family shutdown (issues #348, #356, #396): tears down every\n // family ever booted at process exit (all are lazy now — relay + tunnel +\n // health probe + every booted connection, plus a lazily-booted local\n // Chromium). Each family's `stop()` owns its own infra teardown — the relay\n // family stops its tunnel + probe, the local family kills its Chromium.\n // Inactive infra is left warm during the session and only collected here —\n // that is what preserves a warm attach across `start_debug` swaps.\n //\n // SIGKILL cannot be intercepted — cloudflared may remain orphaned (PPID 1).\n // Port 0 makes such orphans harmless: the next startup gets a fresh port.\n // Manual cleanup if needed: `pkill -f 'cloudflared.*trycloudflare'`\n // ---------------------------------------------------------------------------\n\n let closed = false;\n let parentWatcher: { stop(): void } | null = null;\n let maxAgeWatchdog: { stop(): void } | null = null;\n\n const shutdown = () => {\n // Idempotent: multiple simultaneous signals/exit/uncaught calls run only once.\n if (closed) return;\n closed = true;\n\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n // Tear down every booted family (all lazy, #396 — only those ever started).\n // family.stop() is synchronous for the infra (tunnel/Chromium kill) — safe\n // from exit handlers; the relay's relay.close() inside is async fire-and-forget.\n for (const family of router.bootedFamilies()) family.stop();\n // server.close(), qrServer.close() are async — fine for signal handlers.\n void server.close();\n void qrServer?.close();\n // Remove the lock file so the next startup can proceed immediately.\n lockHandle.release();\n };\n\n // Graceful termination signals.\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n // SIGHUP: terminal hangup / parent process exit.\n process.once('SIGHUP', shutdown);\n\n // Synchronous-only cleanup on process.exit (async calls are silently ignored\n // by Node at this stage — only family.stop() infra kills which are sync).\n process.on('exit', () => {\n if (!closed) {\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n for (const family of router.bootedFamilies()) family.stop();\n // Synchronous lock release — rmSync is safe from exit handlers.\n lockHandle.release();\n }\n });\n\n // Crash safety: shutdown before exiting so cloudflared is killed even on\n // unhandled errors. Covers cases where no signal is delivered (e.g. thrown\n // exception in async code that wasn't caught).\n process.on('uncaughtException', (err) => {\n logError('tool.error', { msg: `uncaughtException: ${String(err)}`, errorKind: 'uncaught' });\n shutdown();\n process.exit(1);\n });\n\n process.on('unhandledRejection', (reason) => {\n logError('tool.error', {\n msg: `unhandledRejection: ${String(reason)}`,\n errorKind: 'unhandled-rejection',\n });\n shutdown();\n process.exit(1);\n });\n\n await server.connect(transport);\n\n // Bind the server to the router. No family is active yet (all-lazy, #396) —\n // the attach watcher is armed by the first `start_debug` and re-armed on every\n // swap.\n router.start(server);\n\n // Self-terminate when the parent process (Claude Code or another AI host) has\n // died without sending SIGTERM/SIGHUP. Without this watcher the daemon runs\n // as a zombie, holding a stale cloudflared tunnel that silently blocks new\n // attach attempts.\n //\n // AIT_DEBUG_NO_PARENT_WATCH=1 disables the watcher — useful for:\n // - shells / process managers that legitimately re-parent the daemon\n // - manual standalone invocations where ppid churn is expected\n if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== '1') {\n parentWatcher = startParentWatcher(\n () => {\n shutdown();\n process.exit(0);\n },\n { intervalMs: 5_000 },\n );\n // Also exit when stdin closes — the MCP host closed the pipe.\n process.stdin.once('end', () => {\n shutdown();\n process.exit(0);\n });\n process.stdin.once('close', () => {\n shutdown();\n process.exit(0);\n });\n }\n\n // FIX 4 (issue #571): max-age watchdog — self-terminate after a configured\n // maximum lifetime. cloudflared quick-tunnel lifetimes are finite; a daemon\n // that outlives its tunnel will silently fail. Default 6 hours.\n //\n // AIT_DEBUG_NO_MAX_AGE=1 disables the watchdog — useful for long-running\n // manual debug sessions or process-manager environments.\n // AIT_DEBUG_MAX_AGE_MS=<ms> overrides the default 6-hour cap.\n if (process.env.AIT_DEBUG_NO_MAX_AGE !== '1') {\n const maxAgeMs = process.env.AIT_DEBUG_MAX_AGE_MS\n ? Number.parseInt(process.env.AIT_DEBUG_MAX_AGE_MS, 10) || undefined\n : undefined;\n maxAgeWatchdog = startMaxAgeWatchdog(\n () => {\n process.stderr.write(\n '[ait-debug] max-age watchdog: daemon lifetime exceeded — shutting down for a fresh start.\\n',\n );\n shutdown();\n process.exit(0);\n },\n { maxAgeMs },\n );\n }\n}\n\nexport interface RunLocalDebugServerOptions {\n /**\n * CDP remote debugging port for the local Chromium. Default 0 (OS-assigned).\n * Uses an ephemeral free port when 0, avoiding EADDRINUSE on reconnect.\n */\n cdpPort?: number;\n /**\n * URL to open in the launched browser. Defaults to `AIT_DEVTOOLS_URL` env var\n * or `http://localhost:5173`.\n */\n devUrl?: string;\n /**\n * When `true`, terminates the process holding the existing server lock and\n * takes over the session. Corresponds to `--force` / `--takeover` CLI flags.\n *\n * Default `false`.\n */\n force?: boolean;\n}\n\n/**\n * Serves the debug stack over stdio with the local browser as the default\n * target. Since #396 NOTHING boots at startup — every family (including the\n * local Chromium) is lazy-booted on its first `start_debug`:\n * 1. `start_debug({ mode: 'local-browser' })` launches a local Chromium with\n * `--remote-debugging-port=<port>` and attaches a `LocalCdpConnection`;\n * 2. the intoss/external relay families lazy-boot on the first\n * `start_debug({ mode: 'relay-staging' | 'relay-live' | 'relay-sandbox' })`;\n * 3. all of this runs through the SAME direction-neutral\n * `DualConnectionRouter` that `runDebugServer` uses (issue #356).\n *\n * Symmetry with `runDebugServer` (#356): starting with `--target=local` no\n * longer pins a single-connection router. A `--target=local` session can\n * hot-switch into relay (env 1 → env 3) without restarting the MCP server,\n * closing the asymmetry where only the default (relay-target) entry point had\n * bidirectional hot-switch. The intended fidelity-ladder flow — \"validate in\n * env 1 (local), then env 3 (intoss-private) in ONE session, no restart\" — now\n * works from either entry point.\n *\n * `build_attach_url` (relay-specific) stays effectively hidden / non-applicable\n * until the relay family is booted: before the first relay switch the env\n * derives to `mock` and `relayTunnelStatus()` reports \"down\", so the tool fails\n * with a clear \"tunnel not up\" message. After a relay switch the relay tunnel\n * is live and the tool works.\n *\n * The AIT.* tools (`AIT.getSdkCallHistory`, `AIT.getMockState`,\n * `AIT.getOperationalEnvironment`) ride the *active* connection's CDP channel\n * via `RoutingAitSource`, so they follow `start_debug` swaps.\n */\nexport async function runLocalDebugServer(options: RunLocalDebugServerOptions = {}): Promise<void> {\n // Enforce a single debug session per machine (same lock as relay mode).\n // `force: true` kills the existing process and takes over the lock.\n const lockHandle = acquireLock({ force: options.force ?? false });\n\n const cdpPort = options.cdpPort ?? 0;\n const devUrl = options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n\n // Local family boot, deferred into the lazy resolver (all-lazy, #396). Launches\n // the Chromium + attaches a LocalCdpConnection only when `start_debug({ mode:\n // 'local-browser' })` first fires — so a session that goes straight to relay never\n // spawns a Chromium it would have to clean up. Honors this entry's\n // cdpPort/devUrl options (vs the env-only `bootLocalFamily`).\n const bootLocalFamilyForEntry = async (): Promise<BootedFamily> => {\n const chromium = await launchChromium({ port: cdpPort, devUrl });\n // Give Chromium a moment to start the CDP endpoint before we connect.\n // 800 ms is enough on most machines; the connection retries if it fails.\n await new Promise<void>((r) => setTimeout(r, 800));\n const localConnection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });\n return {\n connection: localConnection,\n stop() {\n localConnection.close();\n chromium.stop();\n },\n };\n };\n\n // Dual-connection router (issues #348, #356, #378, #396): ALL families are\n // lazy-booted — the local family on the first `start_debug({ mode: 'local-browser' })`,\n // the intoss relay on `relay-staging`/`relay-live`, the env-2 external relay on `relay-sandbox`.\n const devtoolsOpener = new AutoDevtoolsOpener();\n const diagnosticsCollector = new InMemoryDiagnosticsCollector();\n\n // FIX (issue #572 review): track the live cloudflared child PID in memory so\n // get_debug_status can pass it to getDiagnostics as source (a). Updated by\n // onTunnelChildPid on initial boot and on every reissue.\n let activeTunnelChildPid: number | null = null;\n\n const router = new DualConnectionRouter({\n // Lazy resolver for all three family slots (#378, #396, #424).\n // SECRET-HANDLING: readMobileRelayBaseUrl reads AIT_RELAY_BASE_URL (or .ait_urls\n // fallback) only here, at the mobile boot site, and never logs its value.\n // verifyAuth is built INSIDE the lambda (lazily, at the relay boot site) so it\n // reads the env AFTER switchMode's project-local secret load (#396) has\n // populated AIT_DEBUG_TOTP_SECRET — never captured at server startup.\n bootLazyFor: async (key, projectRoot) =>\n key === 'relay-sandbox'\n ? bootExternalRelayFamily(\n await readMobileRelayBaseUrl(process.env, projectRoot),\n await readRelayLocalUrl(process.env, projectRoot),\n )\n : key === 'local-browser'\n ? bootLocalFamilyForEntry()\n : bootRelayFamily({\n verifyAuth: buildRelayVerifyAuth(),\n onWssUrl: (wssUrl) => {\n lockHandle.updateWssUrl(wssUrl);\n qrServer?.notifyStateChange();\n },\n // FIX 3 (issue #571): persist cloudflared child PID for zombie detection.\n // Also update the in-memory tracker (source a for FIX 2).\n onTunnelChildPid: (pid) => {\n activeTunnelChildPid = pid;\n lockHandle.updateTunnelChildPid(pid);\n },\n // Issue #467: secret-free relay TOTP 401 counter for get_debug_status.\n onAuthReject: () => diagnosticsCollector.recordAuthReject(),\n }),\n diagnosticsCollector,\n devtoolsOpener,\n onPageAttach: () => qrServer?.notifyStateChange(),\n // Stable /inspector URL for auto-open (issue #530).\n getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,\n });\n\n // AIT.* methods ride the *active* connection's command channel (local CDP or,\n // after a relay switch, relay Chii), so the AIT source follows swaps.\n const aitSource = new RoutingAitSource(() => {\n const active = router.active as CdpConnection & {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n };\n return active;\n });\n\n // dashboard용 lastAttachParts 상태 — build_attach_url 호출마다 갱신.\n // 완성 URL 대신 컴포넌트를 저장해 getDashboardState 호출마다 fresh TOTP를 mint (Defect 1).\n // SECRET-HANDLING: 컴포넌트에는 tunnel/scheme host가 있으므로 로그 출력 금지.\n let lastAttachParts: AttachUrlParts | null = null;\n\n const getDashboardState = (): DashboardState => {\n const targets = router.active.listTargets();\n // inspectorUrl — /inspector 안정 진입점 (issue #530).\n // SECRET-HANDLING: /inspector URL 자체에 시크릿 없음 — 출력 가능.\n const inspectorUrl = qrServer?.inspectorStableUrl ?? null;\n return {\n tunnel: { up: router.relayTunnelStatus().up, wssUrl: router.relayTunnelStatus().wssUrl },\n pages: targets.map((t) => ({ id: t.id, url: t.url })),\n attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,\n inspectorUrl,\n };\n };\n\n // getDirectInspectorUrl — /inspector 라우트에서 직접 chii front_end URL을 조립.\n // getDashboardState().inspectorUrl(= /inspector 자기 자신)을 쓰면 무한 루프가 발생하므로\n // 별도 getter로 분리한다. 매 요청마다 호출되어 TOTP를 요청 시점에 mint한다.\n // SECRET-HANDLING: ok:true url에 relay host + at= 코드가 담긴다 — 로그/stdout 출력 금지.\n const getDirectInspectorUrl = (): ReturnType<\n NonNullable<QrHttpServerOptions['getDirectInspectorUrl']>\n > => {\n const relayHttpUrl = router.activeRelayHttpUrl;\n if (!relayHttpUrl) {\n return { ok: false, reason: 'relayDown' };\n }\n const targets = router.active.listTargets();\n if (targets.length === 0) {\n return { ok: false, reason: 'noTarget' };\n }\n const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;\n if (!totpSecret) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n const url = buildChiiInspectorUrl(relayHttpUrl, targets[0].id, () =>\n generateTotp(totpSecret, Date.now()),\n );\n if (url === null) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n return { ok: true, url };\n };\n\n // Local QR HTTP server — awaited so the first build_attach_url call (after a\n // relay switch) doesn't race its startup. Failure falls back to text QR.\n let qrServer: QrHttpServer | undefined;\n try {\n qrServer = await startQrHttpServer(getDashboardState, { getDirectInspectorUrl });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logWarn('server.start', { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}` });\n }\n\n // TOTP 주기 갱신 타이머 — 이벤트 없이 페이지가 방치될 때 at= 코드가 stale되는 갭 수정 (#448).\n // TOTP step은 30초이므로 20초 주기로 push해 step 경계를 놓치지 않는다.\n // local-only 동안엔 lastAttachParts가 null이라 no-op — relay로 전환된 뒤 첫 build_attach_url\n // 호출 시 lastAttachParts가 세팅되면 갱신이 시작된다.\n // SECRET-HANDLING: 콜백은 단순 trigger만 — TOTP 값·at= 코드는 절대 로그/stdout 출력 금지.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = null;\n totpRefreshHandle = setInterval(() => {\n if (lastAttachParts !== null) {\n qrServer?.notifyStateChange();\n }\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const server = createDebugServer({\n connection: router.active,\n router,\n aitSource,\n // Tunnel status follows the relay family once it is lazy-booted (#356);\n // until then it reports \"down\" (no relay tunnel exists), which keeps\n // build_attach_url correctly gated.\n getTunnelStatus: () => router.relayTunnelStatus(),\n // FIX (issue #572 review): expose the live cloudflared child PID (source a)\n // so get_debug_status can feed it into getDiagnostics for the FIX 2 probe.\n getTunnelChildPid: () => activeTunnelChildPid,\n get qrHttpServer() {\n return qrServer;\n },\n diagnosticsCollector,\n // SECRET-HANDLING: the TOTP secret is read from env AT CALL TIME (inside\n // build_attach_url) so the project-local .ait_relay secret loaded by\n // switchMode (#396) is visible. It is used only to generate the at= code and\n // is never logged or surfaced in any output.\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n // dashboard 갱신 콜백 — URL 컴포넌트 저장 후 SSE push (Defect 1 fix).\n onAttachUrlBuilt: (parts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n },\n });\n\n const transport = new StdioServerTransport();\n\n // ---------------------------------------------------------------------------\n // Unified dual-family shutdown (issues #356, #396, mirrors runDebugServer):\n // tears down every family ever booted at process exit (all lazy now). Each\n // family's stop() owns its infra — the local family kills its Chromium, a\n // lazily-booted relay family stops its tunnel + probe + relay. Inactive infra\n // is left warm during the session.\n // ---------------------------------------------------------------------------\n\n let closed = false;\n let parentWatcher: { stop(): void } | null = null;\n let maxAgeWatchdog: { stop(): void } | null = null;\n\n const shutdown = () => {\n if (closed) return;\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n // Tear down every booted family (all lazy, #396 — only those ever started).\n for (const family of router.bootedFamilies()) family.stop();\n void server.close();\n void qrServer?.close();\n // Remove the lock file so the next startup can proceed immediately.\n lockHandle.release();\n };\n\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n process.once('SIGHUP', shutdown);\n\n process.on('exit', () => {\n if (!closed) {\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n for (const family of router.bootedFamilies()) family.stop();\n lockHandle.release();\n }\n });\n\n process.on('uncaughtException', (err) => {\n logError('tool.error', {\n msg: `uncaughtException: ${String(err)}`,\n errorKind: 'uncaught',\n mode: 'local-browser',\n });\n shutdown();\n process.exit(1);\n });\n\n process.on('unhandledRejection', (reason) => {\n logError('tool.error', {\n msg: `unhandledRejection: ${String(reason)}`,\n errorKind: 'unhandled-rejection',\n mode: 'local-browser',\n });\n shutdown();\n process.exit(1);\n });\n\n await server.connect(transport);\n\n // Bind the server to the router. No family is active yet (all-lazy, #396) —\n // the attach watcher is armed by the first `start_debug` and re-armed on every\n // swap.\n router.start(server);\n\n // Self-terminate when the parent process has died without sending SIGTERM/SIGHUP.\n if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== '1') {\n parentWatcher = startParentWatcher(\n () => {\n shutdown();\n process.exit(0);\n },\n { intervalMs: 5_000 },\n );\n process.stdin.once('end', () => {\n shutdown();\n process.exit(0);\n });\n process.stdin.once('close', () => {\n shutdown();\n process.exit(0);\n });\n }\n\n // FIX 4 (issue #571): max-age watchdog.\n if (process.env.AIT_DEBUG_NO_MAX_AGE !== '1') {\n const maxAgeMs = process.env.AIT_DEBUG_MAX_AGE_MS\n ? Number.parseInt(process.env.AIT_DEBUG_MAX_AGE_MS, 10) || undefined\n : undefined;\n maxAgeWatchdog = startMaxAgeWatchdog(\n () => {\n process.stderr.write(\n '[ait-debug] max-age watchdog: daemon lifetime exceeded — shutting down for a fresh start.\\n',\n );\n shutdown();\n process.exit(0);\n },\n { maxAgeMs },\n );\n }\n}\n\nexport interface RunMobileDebugServerOptions {\n /**\n * When `true`, terminates the process holding the existing server lock and\n * takes over the session. Corresponds to `--force` / `--takeover` CLI flags.\n *\n * Default `false`.\n */\n force?: boolean;\n /**\n * Project root for `.ait_urls` file-based URL discovery (#424). When supplied,\n * `readMobileRelayBaseUrl` falls back to the `.ait_urls` file written by the\n * unplugin if `AIT_RELAY_BASE_URL` is not set. Defaults to `process.cwd()`.\n */\n projectRoot?: string;\n}\n\n/**\n * Serves the env-2 (real-device PWA) debug stack over stdio with the external\n * Chii relay as the default target (issue #378). Since #396 NOTHING boots at\n * startup — the external relay family is lazy-booted on the first\n * `start_debug({ mode: 'relay-sandbox' })`.\n *\n * Unlike `runDebugServer` (which starts its own relay + cloudflared tunnel),\n * `runMobileDebugServer` attaches to a relay the unplugin ALREADY brought up\n * (`tunnel: { cdp: true }`) and exposed via `AIT_RELAY_BASE_URL`. The MCP only\n * opens a CDP client against that external relay — it never starts or tears down\n * a relay or a tunnel it did not own (see {@link bootExternalRelayFamily}).\n *\n * Symmetry with `runDebugServer` / `runLocalDebugServer` (#356, #378, #396): all\n * three families are lazy-booted — the env-2 external relay on the first\n * `start_debug({ mode: 'relay-sandbox' })`, the local family on `local-browser`,\n * the intoss relay on `relay-staging`/`relay-live` — so a `--target=mobile`\n * session can hot-switch\n * without a restart. The active env derives to `relay-mobile` (external-PWA\n * origin, liveIntent off).\n *\n * SECRET-HANDLING: `AIT_RELAY_BASE_URL` is read once here via\n * {@link readMobileRelayBaseUrl}; when unset it throws\n * {@link MOBILE_RELAY_BASE_URL_MISSING_MESSAGE} — a message that names the env\n * var and how to obtain it, never echoing any URL value. The error propagates to\n * the bin entry's fatal handler (the missing-URL path prints the guidance, not a\n * value). The present value is passed straight to the CDP client, never logged.\n */\nexport async function runMobileDebugServer(\n options: RunMobileDebugServerOptions = {},\n): Promise<void> {\n // Read the external relay base BEFORE acquiring the lock so a missing-URL\n // invocation fails fast (fatal stderr via the bin entry) without taking the\n // single-session lock or opening any connection. Kept pre-flight (NOT moved\n // into the lazy lambda) so the fail-fast still precedes the lock.\n // (#424) Falls back to .ait_urls if AIT_RELAY_BASE_URL is unset.\n // SECRET-HANDLING: relayBaseUrl is passed to the CDP client only, never logged.\n const relayBaseUrl = await readMobileRelayBaseUrl(\n process.env,\n options.projectRoot ?? process.cwd(),\n );\n\n // Enforce a single debug session per machine (same lock as the other modes).\n // `force: true` kills the existing process and takes over the lock.\n const lockHandle = acquireLock({ force: options.force ?? false });\n\n // Dual-connection router (issues #348, #356, #378, #396): ALL families are\n // lazy-booted — the env-2 external relay on the first `start_debug({ mode:\n // 'relay-sandbox' })`, the local family on `local-browser`, the intoss relay on\n // `relay-staging`/`relay-live`.\n const devtoolsOpener = new AutoDevtoolsOpener();\n const diagnosticsCollector = new InMemoryDiagnosticsCollector();\n\n // FIX (issue #572 review): track the live cloudflared child PID in memory so\n // get_debug_status can pass it to getDiagnostics as source (a). Updated by\n // onTunnelChildPid on initial boot and on every reissue.\n let activeTunnelChildPid: number | null = null;\n\n const router = new DualConnectionRouter({\n // Lazy resolver for all three family slots (#378, #396, #424). The external\n // relay boot captures the pre-flight `relayBaseUrl`. Its stop() closes ONLY\n // the CDP client — the unplugin owns the relay + its tunnel.\n // verifyAuth is built INSIDE the lambda (lazily, at the relay boot site) so it\n // reads the env AFTER switchMode's project-local secret load (#396) has\n // populated AIT_DEBUG_TOTP_SECRET — never captured at server startup.\n bootLazyFor: async (key) =>\n key === 'relay-sandbox'\n ? bootExternalRelayFamily(\n relayBaseUrl,\n await readRelayLocalUrl(process.env, options.projectRoot ?? process.cwd()),\n )\n : key === 'local-browser'\n ? bootLocalFamily()\n : bootRelayFamily({\n verifyAuth: buildRelayVerifyAuth(),\n onWssUrl: (wssUrl) => {\n lockHandle.updateWssUrl(wssUrl);\n qrServer?.notifyStateChange();\n },\n // FIX 3 (issue #571): persist cloudflared child PID for zombie detection.\n // Also update the in-memory tracker (source a for FIX 2).\n onTunnelChildPid: (pid) => {\n activeTunnelChildPid = pid;\n lockHandle.updateTunnelChildPid(pid);\n },\n // Issue #467: secret-free relay TOTP 401 counter for get_debug_status.\n onAuthReject: () => diagnosticsCollector.recordAuthReject(),\n }),\n diagnosticsCollector,\n devtoolsOpener,\n onPageAttach: () => qrServer?.notifyStateChange(),\n // Stable /inspector URL for auto-open (issue #530).\n getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,\n });\n\n // AIT.* methods ride the *active* connection's command channel (external relay\n // Chii, or local CDP / intoss Chii after a switch), so the AIT source follows\n // `start_debug` swaps.\n const aitSource = new RoutingAitSource(() => {\n const active = router.active as CdpConnection & {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n };\n return active;\n });\n\n // dashboard용 lastAttachParts 상태 — build_attach_url 호출마다 갱신.\n // 완성 URL 대신 컴포넌트를 저장해 getDashboardState 호출마다 fresh TOTP를 mint (Defect 1).\n // SECRET-HANDLING: 컴포넌트에는 tunnel/scheme host가 있으므로 로그 출력 금지.\n let lastAttachParts: AttachUrlParts | null = null;\n\n const getDashboardState = (): DashboardState => {\n const targets = router.active.listTargets();\n // inspectorUrl — /inspector 안정 진입점 (issue #530).\n // SECRET-HANDLING: /inspector URL 자체에 시크릿 없음 — 출력 가능.\n const inspectorUrl = qrServer?.inspectorStableUrl ?? null;\n return {\n tunnel: { up: router.relayTunnelStatus().up, wssUrl: router.relayTunnelStatus().wssUrl },\n pages: targets.map((t) => ({ id: t.id, url: t.url })),\n attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,\n inspectorUrl,\n };\n };\n\n // getDirectInspectorUrl — /inspector 라우트에서 직접 chii front_end URL을 조립.\n // getDashboardState().inspectorUrl(= /inspector 자기 자신)을 쓰면 무한 루프가 발생하므로\n // 별도 getter로 분리한다. 매 요청마다 호출되어 TOTP를 요청 시점에 mint한다.\n // SECRET-HANDLING: ok:true url에 relay host + at= 코드가 담긴다 — 로그/stdout 출력 금지.\n const getDirectInspectorUrl = (): ReturnType<\n NonNullable<QrHttpServerOptions['getDirectInspectorUrl']>\n > => {\n const relayHttpUrl = router.activeRelayHttpUrl;\n if (!relayHttpUrl) {\n return { ok: false, reason: 'relayDown' };\n }\n const targets = router.active.listTargets();\n if (targets.length === 0) {\n return { ok: false, reason: 'noTarget' };\n }\n const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;\n if (!totpSecret) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n const url = buildChiiInspectorUrl(relayHttpUrl, targets[0].id, () =>\n generateTotp(totpSecret, Date.now()),\n );\n if (url === null) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n return { ok: true, url };\n };\n\n // Local QR HTTP server — awaited so the first build_attach_url call doesn't\n // race its startup. Failure falls back to text QR.\n let qrServer: QrHttpServer | undefined;\n try {\n qrServer = await startQrHttpServer(getDashboardState, { getDirectInspectorUrl });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logWarn('server.start', { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}` });\n }\n\n // TOTP 주기 갱신 타이머 — 이벤트 없이 페이지가 방치될 때 at= 코드가 stale되는 갭 수정 (#448).\n // TOTP step은 30초이므로 20초 주기로 push해 step 경계를 놓치지 않는다.\n // SECRET-HANDLING: 콜백은 단순 trigger만 — TOTP 값·at= 코드는 절대 로그/stdout 출력 금지.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = null;\n totpRefreshHandle = setInterval(() => {\n if (lastAttachParts !== null) {\n qrServer?.notifyStateChange();\n }\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const server = createDebugServer({\n connection: router.active,\n router,\n aitSource,\n // Tunnel status follows the active relay family — once the env-2 external\n // relay is lazy-booted it reports up with its wss URL, so build_attach_url is\n // satisfied without us opening a cloudflared tunnel.\n getTunnelStatus: () => router.relayTunnelStatus(),\n // FIX (issue #572 review): expose the live cloudflared child PID (source a)\n // so get_debug_status can feed it into getDiagnostics for the FIX 2 probe.\n getTunnelChildPid: () => activeTunnelChildPid,\n get qrHttpServer() {\n return qrServer;\n },\n diagnosticsCollector,\n // SECRET-HANDLING: the TOTP secret is read from env AT CALL TIME (inside\n // build_attach_url) so the project-local .ait_relay secret loaded by\n // switchMode (#396) is visible. It is used only to generate the at= code and\n // is never logged or surfaced in any output.\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n // dashboard 갱신 콜백 — URL 컴포넌트 저장 후 SSE push (Defect 1 fix).\n onAttachUrlBuilt: (parts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n },\n });\n\n const transport = new StdioServerTransport();\n\n // ---------------------------------------------------------------------------\n // Unified dual-family shutdown (issues #356, #378, #396, mirrors the other run\n // functions): tears down every family ever booted at process exit (all lazy\n // now). The external relay family's stop() closes ONLY our CDP client (the\n // unplugin owns the relay + tunnel); a lazily-booted intoss relay family stops\n // its own tunnel + probe + relay; a lazily-booted local family kills its\n // Chromium.\n // ---------------------------------------------------------------------------\n\n let closed = false;\n let parentWatcher: { stop(): void } | null = null;\n let maxAgeWatchdog: { stop(): void } | null = null;\n\n const shutdown = () => {\n if (closed) return;\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n for (const family of router.bootedFamilies()) family.stop();\n void server.close();\n void qrServer?.close();\n lockHandle.release();\n };\n\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n process.once('SIGHUP', shutdown);\n\n process.on('exit', () => {\n if (!closed) {\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n for (const family of router.bootedFamilies()) family.stop();\n lockHandle.release();\n }\n });\n\n process.on('uncaughtException', (err) => {\n logError('tool.error', {\n msg: `uncaughtException: ${String(err)}`,\n errorKind: 'uncaught',\n mode: 'relay-sandbox',\n });\n shutdown();\n process.exit(1);\n });\n\n process.on('unhandledRejection', (reason) => {\n logError('tool.error', {\n msg: `unhandledRejection: ${String(reason)}`,\n errorKind: 'unhandled-rejection',\n mode: 'relay-sandbox',\n });\n shutdown();\n process.exit(1);\n });\n\n await server.connect(transport);\n\n // Bind the server to the router. No family is active yet (all-lazy, #396) —\n // the attach watcher is armed by the first `start_debug` and re-armed on every\n // swap.\n router.start(server);\n\n // Self-terminate when the parent process has died without sending SIGTERM/SIGHUP.\n if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== '1') {\n parentWatcher = startParentWatcher(\n () => {\n shutdown();\n process.exit(0);\n },\n { intervalMs: 5_000 },\n );\n process.stdin.once('end', () => {\n shutdown();\n process.exit(0);\n });\n process.stdin.once('close', () => {\n shutdown();\n process.exit(0);\n });\n }\n\n // FIX 4 (issue #571): max-age watchdog.\n if (process.env.AIT_DEBUG_NO_MAX_AGE !== '1') {\n const maxAgeMs = process.env.AIT_DEBUG_MAX_AGE_MS\n ? Number.parseInt(process.env.AIT_DEBUG_MAX_AGE_MS, 10) || undefined\n : undefined;\n maxAgeWatchdog = startMaxAgeWatchdog(\n () => {\n process.stderr.write(\n '[ait-debug] max-age watchdog: daemon lifetime exceeded — shutting down for a fresh start.\\n',\n );\n shutdown();\n process.exit(0);\n },\n { maxAgeMs },\n );\n }\n}\n","/**\n * Dev-mode `AitSource` — backed by the Vite dev server's mock-state endpoint.\n *\n * The dev server already exposes the live browser mock state at\n * `GET /api/ait-devtools/state` (registered by the unplugin with `mcp: true`).\n * Phase 3 aligns dev mode and debug mode on the same `AIT.*` tool surface, so\n * dev mode serves those tools off this one HTTP source instead of a CDP channel:\n *\n * - `AIT.getMockState` → the full state snapshot (verbatim).\n * - `AIT.getOperationalEnvironment` → derived from the snapshot's\n * `environment` + `appVersion` fields.\n * - `AIT.getSdkCallHistory` → empty (the dev endpoint does not record\n * an SDK call trace — honest, not faked).\n *\n * An AI agent thus sees the same `AIT.getMockState` tool whether attached to a\n * phone (debug) or a dev browser (dev). Tests inject a fake `fetch`.\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** Minimal `fetch` shape this source needs (injectable in tests). */\nexport type FetchLike = (url: string) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n}>;\n\nexport interface HttpAitSourceOptions {\n /** Full URL of the mock-state endpoint, e.g. `http://localhost:5173/api/ait-devtools/state`. */\n stateEndpoint: string;\n /** Injected for tests; defaults to global `fetch`. */\n fetchImpl?: FetchLike;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport class HttpAitSource implements AitSource {\n private readonly stateEndpoint: string;\n private readonly fetchImpl: FetchLike;\n\n constructor(options: HttpAitSourceOptions) {\n this.stateEndpoint = options.stateEndpoint;\n this.fetchImpl = options.fetchImpl ?? ((url) => fetch(url));\n }\n\n private async fetchState(): Promise<AitMockState> {\n const res = await this.fetchImpl(this.stateEndpoint);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch mock state from ${this.stateEndpoint}: HTTP ${res.status} ${res.statusText}. ` +\n 'Ensure the Vite dev server is running with the @ait-co/devtools unplugin option `mcp: true`.',\n );\n }\n const body = await res.json();\n return isObject(body) ? body : {};\n }\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n switch (method) {\n case 'AIT.getMockState': {\n const state = await this.fetchState();\n return state as AitMethodMap[M];\n }\n case 'AIT.getOperationalEnvironment': {\n const state = await this.fetchState();\n const environment = typeof state.environment === 'string' ? state.environment : 'unknown';\n const sdkVersion = typeof state.appVersion === 'string' ? state.appVersion : null;\n const result: AitOperationalEnvironment = { environment, sdkVersion };\n return result as AitMethodMap[M];\n }\n case 'AIT.getSdkCallHistory': {\n // sdkCallLog slice is now part of the mock state pushed by the browser panel.\n // Read it from the state snapshot rather than returning an empty stub.\n const state = await this.fetchState();\n const raw = state.sdkCallLog;\n const calls = Array.isArray(raw) ? (raw as AitSdkCallHistory['calls']) : [];\n const result: AitSdkCallHistory = { calls };\n return result as AitMethodMap[M];\n }\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * @ait-co/devtools dev-mode MCP server (stdio).\n *\n * Exposes the live browser mock state from a running Vite dev server to AI\n * coding agents via the Model Context Protocol (MCP).\n *\n * Architecture:\n * Browser (aitState) → Vite dev server endpoint (/api/ait-devtools/state)\n * ← HTTP GET ← this stdio MCP server ← AI agent\n *\n * The Vite endpoint is registered by the unplugin when `mcp: true` is set in\n * the plugin options (see `src/unplugin/index.ts`).\n *\n * Phase 3 tool-surface alignment: dev mode and debug mode now expose the same\n * `AIT.*` tools (`AIT.getMockState`, `AIT.getOperationalEnvironment`,\n * `AIT.getSdkCallHistory`). In dev mode they are backed by the HTTP mock-state\n * endpoint (see `HttpAitSource`); in debug mode by the Chii channel. So an AI\n * sees a coherent tool whether attached to a phone (debug) or a dev browser\n * (dev). `devtools_get_mock_state` (the original devtools#130 name) is kept as a\n * backward-compatible alias of `AIT.getMockState`.\n *\n * Issue #305 (M2-1) — dev/debug tool-surface unification:\n * dev-mode now also exposes `list_pages`, `get_debug_status`, `measure_safe_area`,\n * and `call_sdk` so the docs/qa/scenarios.md acceptance sequence\n * `list_pages → measure_safe_area → call_sdk` works in dev mode without\n * \"Unknown tool\" failures.\n *\n * - `list_pages` — shim: returns the Vite dev URL as a single-entry array.\n * - `get_debug_status` — dumps dev-mode server state (endpoint URL, last fetch\n * error, reachability, mode/environment metadata).\n * - `measure_safe_area`— reads safeAreaInsets from the mock state snapshot\n * (source: 'mock-vite').\n * - `call_sdk` — reads mock state and builds a mock-equivalent result\n * using window.__ait.state for supported methods; returns\n * an explicit tier-filter error for methods that require\n * a live CDP bridge.\n * - CDP-only tools (`evaluate`, `take_screenshot`, `get_dom_document`,\n * `take_snapshot`, `list_console_messages`,\n * `list_network_requests`, `list_exceptions`) — return an\n * explicit tier-filter error explaining that CDP is unavailable\n * in dev-mode and pointing to `--mode=local` or `--mode=debug`.\n *\n * This module is reached via the `devtools-mcp --mode=dev` CLI entry (see\n * `cli.ts`); the default (no flag) bin mode is the debug-mode CDP/Chii server.\n *\n * Usage (in your MCP client config, e.g. Claude Desktop):\n * {\n * \"mcpServers\": {\n * \"ait-devtools\": {\n * \"command\": \"pnpm\",\n * \"args\": [\"exec\", \"devtools-mcp\", \"--mode=dev\"],\n * \"env\": { \"AIT_DEVTOOLS_URL\": \"http://localhost:5173\" }\n * }\n * }\n * }\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { HttpAitSource } from './ait-http-source.js';\nimport type { AitSource } from './ait-source.js';\nimport { wrapEnvelope } from './envelope.js';\nimport { mcpError, tierRejectionError } from './errors.js';\nimport {\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n isAitToolName,\n type ToolAvailability,\n} from './tools.js';\n\n/** Error message prefix for CDP-dependent tools called in dev-mode. */\nconst CDP_UNAVAILABLE_IN_DEV_MODE =\n 'dev-mode에서는 CDP 연결이 없어 이 도구를 사용할 수 없습니다. ' +\n '실기기 또는 로컬 Chromium에 붙이려면 `devtools-mcp --mode=local` 또는 ' +\n '`devtools-mcp` (debug 모드 기본)로 전환하세요.';\n\n/**\n * Tool descriptors served by the dev-mode server.\n *\n * All dev-mode tools are Tier C (both envs) per RFC #277 — the dev-mode server\n * itself is the mock-side embodiment of those Tier C tools. `availableIn` is\n * declared so the surface stays consistent with the debug-mode registry.\n *\n * Issue #305: CDP-only tools are also listed with explicit descriptions so\n * agents do not get \"Unknown tool\" failures — they get a clear tier-filter\n * error message instead.\n */\nconst DEV_TOOL_DEFINITIONS = [\n /* ------------------------------------------------------------------ */\n /* AIT.* tools — HTTP mock-state backed */\n /* ------------------------------------------------------------------ */\n {\n name: 'AIT.getMockState',\n description:\n 'Returns the devtools mock state snapshot (window.__ait) from the running browser session — ' +\n 'environment, permissions, location, auth, network, IAP, and more. Read-only. ' +\n 'Requires the Vite dev server running with the @ait-co/devtools unplugin option `mcp: true`. ' +\n 'Same tool as in debug mode, where the in-app side reports it over the AIT domain.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getOperationalEnvironment',\n description:\n 'Returns the operational environment + SDK/app version derived from the dev mock state. ' +\n 'Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getSdkCallHistory',\n description:\n 'Returns the SDK call trace. In dev mode the HTTP mock-state endpoint records no trace, so ' +\n 'this returns an empty list; in debug mode it is populated over the AIT domain. Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'devtools_get_mock_state',\n description:\n 'Backward-compatible alias of AIT.getMockState (the original devtools#130 name). Returns the ' +\n 'current AIT DevTools mock state snapshot. Read-only. Prefer AIT.getMockState in new configs.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* Unified surface — dev-mode shims (issue #305) */\n /* ------------------------------------------------------------------ */\n {\n name: 'list_pages',\n description:\n 'dev-mode: returns the Vite dev server URL as a single-entry page list. ' +\n 'No CDP relay is involved — `tunnel.up` is always false and `devMode: true` marks ' +\n 'this as a shim result. Call this first to confirm the dev server is reachable. ' +\n 'In debug mode (`devtools-mcp` / `--mode=local`) this returns real attached pages.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_debug_status',\n description:\n 'dev-mode: reports the current dev session state — Vite endpoint URL, last fetch ' +\n 'timestamp/error, mock state endpoint reachability, mode (\"dev\"), and environment metadata — ' +\n 'in one call. Use this any time to confirm what the dev server is doing or when its ' +\n 'connection is suspect. In debug mode this returns tunnel/relay/attach status instead.',\n inputSchema: {\n type: 'object',\n properties: {\n recent_errors_limit: {\n type: 'number',\n description: 'Ignored in dev-mode (no error ring buffer). Present for schema parity.',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'measure_safe_area',\n description:\n 'dev-mode: reads safe-area insets from the mock state snapshot via the Vite endpoint. ' +\n 'Returns `{ source: \"mock-vite\", sdkInsets, sdkInsetsSource: \"window.__ait\", ... }`. ' +\n 'Values reflect what the DevTools panel reports at the time of the last state push. ' +\n 'In debug mode this runs a Runtime.evaluate CDP probe on the attached page.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'call_sdk',\n description:\n 'dev-mode: calls a mock SDK method via the Vite mock state endpoint. ' +\n 'Supported methods read from window.__ait mock state (e.g. getOperationalEnvironment). ' +\n 'Returns the same `{ok, value}` / `{ok, error}` envelope as debug mode. ' +\n 'In debug mode this calls the real SDK via window.__sdkCall over CDP.',\n inputSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'Mock SDK method name to call (e.g. \"getOperationalEnvironment\").',\n },\n args: {\n type: 'array',\n description: 'Arguments (ignored in dev-mode mock path; present for schema parity).',\n items: {},\n },\n },\n required: ['name'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* Tier B tool — tier-filter stub (issue #323) */\n /* */\n /* build_attach_url is relay-only (Tier B per RFC #277). Listing it */\n /* here in dev-mode ensures agents don't hit \"Unknown tool\" and get a */\n /* clear hand-off hint toward --mode=debug (station 2 → 3 seam). */\n /* ------------------------------------------------------------------ */\n {\n name: 'build_attach_url',\n description:\n 'Turns an `ait deploy --scheme-only` URL into a self-attaching deep link for a real device. ' +\n 'NOT available in dev-mode — requires a live cloudflared relay (Tier B, relay-only). ' +\n 'To use this tool: restart the MCP server with `--mode=debug` (or omit --mode) and set ' +\n 'MCP_ENV=relay, then call build_attach_url to generate the QR for phone scanning. ' +\n 'See: https://docs.aitc.dev/guides/debug-relay',\n inputSchema: {\n type: 'object',\n properties: {\n scheme_url: {\n type: 'string',\n description: 'The intoss-private:// URL from `ait deploy --scheme-only`.',\n },\n wait_for_attach: {\n type: 'boolean',\n description: 'If true, block until a page attaches (default 60 s).',\n },\n wait_timeout_seconds: {\n type: 'number',\n description:\n 'Maximum seconds to wait when wait_for_attach=true (default 60, range 1–600). ' +\n 'Invalid inputs fall back to default.',\n },\n },\n required: ['scheme_url'],\n },\n availableIn: 'relay' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* CDP-only tools — tier-filter stubs so agents see a clear error */\n /* instead of \"Unknown tool\" (issue #305) */\n /* ------------------------------------------------------------------ */\n {\n name: 'evaluate',\n description:\n 'Evaluates an arbitrary JavaScript expression via CDP Runtime.evaluate. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug` for CDP access.',\n inputSchema: {\n type: 'object',\n properties: {\n expression: { type: 'string', description: 'JavaScript expression to evaluate.' },\n },\n required: ['expression'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_screenshot',\n description:\n 'Captures a PNG screenshot via CDP Page.captureScreenshot. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_dom_document',\n description:\n 'Returns the DOM tree via CDP DOM.getDocument. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_snapshot',\n description:\n 'Captures a serialized page snapshot via CDP DOMSnapshot.captureSnapshot. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_console_messages',\n description:\n 'Lists console messages captured via CDP Runtime.consoleAPICalled. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_network_requests',\n description:\n 'Lists network requests captured via CDP Network events. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_exceptions',\n description:\n 'Lists JS exceptions captured via CDP Runtime.exceptionThrown. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: {\n type: 'object',\n properties: {\n limit: { type: 'number', description: 'Maximum exceptions to return.' },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n] as const;\n\n/** All tool names served in dev-mode (including tier-filter stubs). */\nconst DEV_TOOL_NAMES = new Set<string>(DEV_TOOL_DEFINITIONS.map((t) => t.name));\n\n/** CDP-only tools — return a tier-filter error in dev-mode. */\nconst CDP_ONLY_TOOL_NAMES = new Set<string>([\n 'evaluate',\n 'take_screenshot',\n 'get_dom_document',\n 'take_snapshot',\n 'list_console_messages',\n 'list_network_requests',\n 'list_exceptions',\n]);\n\n/**\n * Tier B tools — relay-only per RFC #277.\n * Listed in dev-mode tool surface (issue #323) so agents get a hand-off hint\n * toward `--mode=debug` instead of \"Unknown tool\".\n */\nconst TIER_B_TOOL_NAMES = new Set<string>(['build_attach_url']);\n\nexport interface CreateDevServerDeps {\n /** AIT source for the dev tools. Defaults to an HTTP source over the dev server. */\n aitSource?: AitSource;\n}\n\n/**\n * Builds the `list_pages` dev-mode shim response.\n * Returns the Vite dev URL as a single-entry page list with `devMode: true`.\n */\nfunction buildDevListPagesResult(devtoolsUrl: string) {\n return {\n pages: [\n {\n url: devtoolsUrl,\n title: 'dev fixture',\n attached: true,\n },\n ],\n tunnel: { up: false },\n devMode: true,\n singleAttachModel: true,\n };\n}\n\n/**\n * Builds the `get_debug_status` dev-mode response.\n * Probes the mock state endpoint reachability and returns server metadata.\n */\nasync function buildDevDiagnostics(\n devtoolsUrl: string,\n stateEndpoint: string,\n fetchImpl: (url: string) => Promise<Response>,\n): Promise<Record<string, unknown>> {\n let reachable = false;\n let lastFetchError: string | null = null;\n let lastFetchAt: string | null = null;\n\n try {\n const res = await fetchImpl(stateEndpoint);\n reachable = res.ok;\n lastFetchAt = new Date().toISOString();\n if (!res.ok) {\n lastFetchError = `HTTP ${res.status} ${res.statusText}`;\n }\n } catch (err) {\n lastFetchError = err instanceof Error ? err.message : String(err);\n lastFetchAt = new Date().toISOString();\n }\n\n return {\n mode: 'dev',\n devtoolsUrl,\n mcpStateEndpoint: stateEndpoint,\n mockStateEndpointReachable: reachable,\n lastFetchAt,\n lastFetchError,\n environment: {\n kind: 'mock',\n reason: 'dev-mode — Vite HTTP endpoint, no CDP connection',\n },\n nextRecommendedAction: reachable\n ? null\n : 'mock state endpoint가 응답하지 않습니다. Vite dev 서버가 `mcp: true` 옵션으로 실행 중인지 확인하고, 필요하면 dev 서버를 재시작하세요.',\n };\n}\n\n/**\n * Builds the `measure_safe_area` dev-mode response from mock state.\n * Reads `safeAreaInsets` from the AIT mock state and returns a parity-schema\n * result with `source: 'mock-vite'`.\n */\nasync function buildDevMeasureSafeArea(aitSource: AitSource): Promise<Record<string, unknown>> {\n const state = await aitSource.get('AIT.getMockState');\n const raw = state as Record<string, unknown>;\n\n // Extract safeAreaInsets from the mock state.\n const rawInsets = raw.safeAreaInsets;\n let sdkInsets: { top: number; right: number; bottom: number; left: number } | null = null;\n if (rawInsets !== null && typeof rawInsets === 'object' && !Array.isArray(rawInsets)) {\n const r = rawInsets as Record<string, unknown>;\n sdkInsets = {\n top: typeof r.top === 'number' ? r.top : 0,\n right: typeof r.right === 'number' ? r.right : 0,\n bottom: typeof r.bottom === 'number' ? r.bottom : 0,\n left: typeof r.left === 'number' ? r.left : 0,\n };\n }\n\n return {\n source: 'mock-vite',\n // CSS env() vars are not available from the server side — report zeros.\n cssEnv: { top: 0, right: 0, bottom: 0, left: 0 },\n sdkInsets,\n sdkInsetsSource: sdkInsets !== null ? 'window.__ait' : null,\n ...(sdkInsets === null\n ? { sdkInsetsError: 'window.__ait.state.safeAreaInsets not found in mock state snapshot' }\n : {}),\n // Viewport geometry is not available from server side.\n innerWidth: null,\n innerHeight: null,\n devicePixelRatio: null,\n userAgent: null,\n navBarHeight: null,\n navBarHeightSource: 'not-available-in-dev-mode',\n };\n}\n\n/**\n * Builds the `call_sdk` dev-mode response.\n *\n * Supported methods are served from the mock state snapshot. Unsupported\n * methods return `{ ok: false, error: 'dev-mode-unsupported: ...' }` so the\n * agent gets an informative message rather than a generic failure.\n */\nasync function buildDevCallSdk(\n methodName: string,\n aitSource: AitSource,\n): Promise<Record<string, unknown>> {\n switch (methodName) {\n case 'getOperationalEnvironment': {\n const env = await aitSource.get('AIT.getOperationalEnvironment');\n return {\n ok: true,\n value: env.environment,\n };\n }\n default: {\n // For methods not readable from mock state, return a structured error.\n return {\n ok: false,\n error:\n `dev-mode-unsupported: \"${methodName}\"은 dev-mode에서 직접 호출할 수 없습니다. ` +\n 'CDP bridge(window.__sdkCall)가 없으므로 실제 SDK 호출은 `--mode=local` 또는 ' +\n 'debug 모드에서만 가능합니다. ' +\n '지원 메서드: getOperationalEnvironment (mock state에서 읽음).',\n };\n }\n }\n}\n\n/** Builds the dev-mode MCP server (does not connect a transport). */\nexport function createDevServer(deps: CreateDevServerDeps = {}): Server {\n const devtoolsUrl = process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n const stateEndpoint = `${devtoolsUrl}/api/ait-devtools/state`;\n const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });\n\n const server = new Server(\n { name: 'ait-devtools', version: __VERSION__ },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, () => ({\n tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n if (!DEV_TOOL_NAMES.has(name)) {\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n\n // CDP-only tools — tier-filter error with mode-switch hint.\n if (CDP_ONLY_TOOL_NAMES.has(name)) {\n return mcpError(`${name}: ${CDP_UNAVAILABLE_IN_DEV_MODE}`);\n }\n\n // Tier B tools (relay-only) — return a tier-filter error with a hand-off\n // hint toward --mode=debug so the station 2 → 3 seam is explicit.\n // (issue #323, Option B: list Tier B in dev tools/list + reject on call)\n if (TIER_B_TOOL_NAMES.has(name)) {\n return tierRejectionError(\n name,\n 'relay',\n 'mock',\n 'dev-mode — Vite HTTP endpoint, no CDP/relay connection. ' +\n '`--mode=debug` (または `devtools-mcp` without --mode) + MCP_ENV=relay로 재시작하세요.',\n );\n }\n\n try {\n // `devtools_get_mock_state` is an alias of `AIT.getMockState`.\n const effective = name === 'devtools_get_mock_state' ? 'AIT.getMockState' : name;\n\n // AIT.* tools backed by HTTP mock-state endpoint.\n if (isAitToolName(effective)) {\n switch (effective) {\n case 'AIT.getMockState':\n return jsonResult(await getMockState(aitSource));\n case 'AIT.getOperationalEnvironment':\n return jsonResult(await getOperationalEnvironment(aitSource));\n case 'AIT.getSdkCallHistory':\n return jsonResult(await getSdkCallHistory(aitSource));\n default:\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n }\n\n // Unified-surface tools (issue #305 shims).\n // Responses are wrapped in ToolEnvelope (issue #322) so agents use the\n // same {ok, data, meta} parser regardless of dev vs debug mode.\n switch (name) {\n case 'list_pages':\n return envelopeResult('list_pages', buildDevListPagesResult(devtoolsUrl));\n\n case 'get_debug_status':\n return envelopeResult(\n 'get_debug_status',\n await buildDevDiagnostics(devtoolsUrl, stateEndpoint, (url) => fetch(url)),\n );\n\n case 'measure_safe_area':\n return envelopeResult('measure_safe_area', await buildDevMeasureSafeArea(aitSource));\n\n case 'call_sdk': {\n const sdkName = request.params.arguments?.name;\n if (typeof sdkName !== 'string' || sdkName === '') {\n return mcpError(\n 'call_sdk: name 인자가 비어 있습니다. 호출할 메서드 이름을 전달하세요.',\n );\n }\n return envelopeResult('call_sdk', await buildDevCallSdk(sdkName, aitSource));\n }\n\n default:\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return mcpError(\n `${name} 실패: ${message}\\n` +\n 'Vite dev 서버가 @ait-co/devtools unplugin `mcp: true` 옵션으로 실행 중인지 확인하세요. ' +\n 'AIT_DEVTOOLS_URL 환경변수가 올바르게 설정됐는지도 확인하세요.',\n );\n }\n });\n\n return server;\n}\n\nfunction jsonResult(value: unknown) {\n return { content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] };\n}\n\n/**\n * Wraps `value` in a `ToolEnvelope` (when compat mode is off) and returns it\n * as a text content block. In dev-mode `env` is always `'mock'` and\n * `attached` is always `true` (the Vite dev server is the single implicit\n * \"attached\" page).\n *\n * When `AIT_MCP_COMPAT=chrome-devtools` the envelope is skipped and the raw\n * value is returned — identical to `jsonResult` (0.1.x back-compat).\n */\nfunction envelopeResult(tool: string, value: unknown) {\n const wrapped = wrapEnvelope(value, { tool, env: 'mock', attached: true });\n return { content: [{ type: 'text' as const, text: JSON.stringify(wrapped, null, 2) }] };\n}\n\n/** Builds the dev-mode server and connects it over stdio. */\nexport async function runDevServer(): Promise<void> {\n const server = createDevServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n","/**\n * `devtools-mcp` bin entry.\n *\n * Single bin, two modes selected by `--mode` and one target selected by\n * `--target`:\n *\n * --mode=debug (default)\n * --target=relay (default) — CDP/Chii relay + cloudflared quick tunnel.\n * Attach a running mini-app (real Toss WebView, env 3/4) and read its\n * console + network over CDP without a human watching a phone.\n * --target=local — CDP direct-attach to a local Chromium launched by the\n * MCP server (env 1). No relay or tunnel; the browser is launched\n * pointing at AIT_DEVTOOLS_URL (default http://localhost:5173).\n * --target=mobile — CDP attach to an EXTERNAL Chii relay the unplugin\n * already brought up for the env-2 real-device PWA (`tunnel: { cdp: true }`),\n * exposed via AIT_RELAY_BASE_URL. The MCP starts no relay or tunnel; it\n * only opens a CDP client against that external relay (issue #378).\n *\n * --mode=dev — dev mode — reads the live browser mock state from a running\n * Vite dev server (the devtools#130 `devtools_get_mock_state` surface).\n *\n * Back-compat (issue #348): the legacy `--mode`/`--target` flags and `MCP_ENV`\n * still work. `--target=relay`/`local` select the initial active connection;\n * the in-session `start_debug(mode)` MCP tool can then flip between them with no\n * restart. `MCP_ENV=relay-live` seeds LIVE intent at boot (deprecated alias);\n * `MCP_ENV=mock|relay|relay-dev` are accepted and ignored for env derivation\n * (the active connection's `kind` is authoritative).\n *\n * Node-only stdio process.\n */\n\nimport { realpathSync } from 'node:fs';\nimport { argv } from 'node:process';\nimport { fileURLToPath } from 'node:url';\nimport { runDebugServer, runLocalDebugServer, runMobileDebugServer } from './debug-server.js';\nimport { setLiveIntent } from './environment.js';\nimport { runDevServer } from './server.js';\n\n/**\n * Seeds the module-level `liveIntent` bit from the deprecated `MCP_ENV` alias\n * (issue #348). `MCP_ENV=relay-live` is the only value that matters now — it\n * arms LIVE intent at boot so a session launched straight into env 4 has the\n * guard active without a `start_debug({ mode: 'relay-live' })` round-trip. All\n * other `MCP_ENV` values (`mock`, `relay`, `relay-dev`) are accepted-and-ignored\n * for env derivation — the active connection's `kind` is authoritative.\n *\n * SECRET-HANDLING: reads only the env-var string; never logs a secret.\n */\nexport function seedLiveIntentFromEnv(env: NodeJS.ProcessEnv = process.env): void {\n if (env.MCP_ENV === 'relay-live') setLiveIntent(true);\n}\n\ntype Mode = 'debug' | 'dev';\ntype Target = 'relay' | 'local' | 'mobile';\n\n/**\n * Returns `true` when `--force` or `--takeover` is present in argv.\n *\n * Both flags are accepted as aliases — `--force` is the short form listed in\n * the `--help` output; `--takeover` is a longer synonym.\n */\nexport function parseForce(argv: readonly string[]): boolean {\n return argv.includes('--force') || argv.includes('--takeover');\n}\n\n/** Parses `--mode=<value>` / `--mode <value>` from argv; default `debug`. */\nexport function parseMode(argv: readonly string[]): Mode {\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === undefined) continue;\n if (arg.startsWith('--mode=')) {\n return normalizeMode(arg.slice('--mode='.length));\n }\n if (arg === '--mode') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw new Error(\"--mode requires a value: 'debug' (default) or 'dev'.\");\n }\n return normalizeMode(next);\n }\n }\n return 'debug';\n}\n\n/**\n * Parses `--target=<value>` / `--target <value>` from argv; default `relay`.\n *\n * Only meaningful when `--mode=debug`:\n * - `relay` — phone/WebView attach over Chii relay + cloudflared tunnel (env 3/4).\n * - `local` — local Chromium CDP attach (env 1, no relay needed).\n * - `mobile` — CDP attach to an EXTERNAL relay (env 2 PWA, AIT_RELAY_BASE_URL).\n */\nexport function parseTarget(argv: readonly string[]): Target {\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === undefined) continue;\n if (arg.startsWith('--target=')) {\n return normalizeTarget(arg.slice('--target='.length));\n }\n if (arg === '--target') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw new Error(\"--target requires a value: 'relay' (default), 'local', or 'mobile'.\");\n }\n return normalizeTarget(next);\n }\n }\n return 'relay';\n}\n\nfunction normalizeMode(value: string): Mode {\n if (value === 'dev') return 'dev';\n if (value === 'debug') return 'debug';\n throw new Error(`Unknown --mode '${value}'. Expected 'debug' (default) or 'dev'.`);\n}\n\nfunction normalizeTarget(value: string): Target {\n if (value === 'relay') return 'relay';\n if (value === 'local') return 'local';\n if (value === 'mobile') return 'mobile';\n throw new Error(`Unknown --target '${value}'. Expected 'relay' (default), 'local', or 'mobile'.`);\n}\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n // Back-compat (issue #348): `MCP_ENV=relay-live` seeds LIVE intent at boot.\n seedLiveIntentFromEnv();\n const mode = parseMode(args);\n if (mode === 'dev') {\n await runDevServer();\n } else {\n const target = parseTarget(args);\n const force = parseForce(args);\n if (target === 'local') {\n await runLocalDebugServer({ force });\n } else if (target === 'mobile') {\n await runMobileDebugServer({ force });\n } else {\n await runDebugServer({ force });\n }\n }\n}\n\n/**\n * True when this file is the process entry (the bin), not an import.\n *\n * `argv[1]` is whatever path the OS used to launch node — under `npx`/npm's\n * bin shim that's the symlink in `node_modules/.bin/` (or a wrapper), whereas\n * `import.meta.url` resolves to the realpath inside the package. Comparing\n * the two raw paths gives a false negative on every install that goes through\n * a bin shim — exactly the dominant path for `npx -y @ait-co/devtools\n * devtools-mcp`. Resolve `argv[1]` to its realpath before comparing.\n */\nfunction isEntrypoint(): boolean {\n const entry = argv[1];\n if (entry === undefined) return false;\n try {\n return fileURLToPath(import.meta.url) === realpathSync(entry);\n } catch {\n return false;\n }\n}\n\nif (isEntrypoint()) {\n main().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`[devtools-mcp] fatal: ${message}\\n`);\n process.exitCode = 1;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,SAAgBA,aAAW,KAAsB;AAC/C,KAAI;AACF,UAAQ,KAAK,KAAK,EAAE;AACpB,SAAO;UACA,KAAc;AAGrB,MAAK,IAA8B,SAAS,QAAS,QAAO;AAC5D,SAAO;;;;;;;;;;;;;;;;;;;;;AA0BX,SAAgB,mBACd,YACA,MAOkB;CAClB,MAAM,EACJ,aAAa,KACb,cAAc,QAAQ,MACtB,UAAUA,cACV,gBAAgB,QAAQ,MACxB,OAAO,QAAgB,QAAQ,OAAO,MAAM,IAAI,KAC9C,QAAQ,EAAE;AAId,KAAI,eAAe,GAAG;AACpB,MAAI,2EAA2E;AAC/E,SAAO,EAAE,OAAO,IAAI;;CAGtB,IAAI,QAAQ;CAEZ,MAAM,SAAS,kBAAkB;AAC/B,MAAI,MAAO;EAEX,MAAM,cAAc,SAAS;AAG7B,MAFiB,gBAAgB,eAAe,CAAC,QAAQ,YAAY,EAEvD;AACZ,WAAQ;AACR,iBAAc,OAAO;AACrB,OACE,8CAA8C,YAAY,wBAAwB,YAAY,qBAC/F;AACD,eAAY;;IAEb,WAAW;AAEd,QAAO,EACL,OAAO;AACL,gBAAc,OAAO;IAExB;;;;;;;;;;;;;;;;;;;;AAyBH,SAAgB,oBACd,WACA,OAII,EAAE,EACY;CAClB,MAAM,EACJ,WAAW,MAAS,KAAK,KACzB,aAAa,KACb,YAAY,KAAK,KAAK,KACpB;CAEJ,MAAM,YAAY,KAAK;CACvB,IAAI,QAAQ;CAEZ,MAAM,SAAS,kBAAkB;AAC/B,MAAI,MAAO;AACX,MAAI,KAAK,GAAG,aAAa,UAAU;AACjC,WAAQ;AACR,iBAAc,OAAO;AACrB,cAAW;;IAEZ,WAAW;AAEd,QAAO,EACL,OAAO;AACL,gBAAc,OAAO;IAExB;;;;AC5HH,SAASC,WAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU;;;AAIhD,SAAS,iBAAiB,KAAiC;AACzD,KAAIA,WAAS,IAAI,IAAI,MAAM,QAAQ,IAAI,MAAM,CAC3C,QAAO,EAAE,OAAO,IAAI,OAAqC;AAE3D,QAAO,EAAE,OAAO,EAAE,EAAE;;;AAItB,SAAS,YAAY,KAA4B;AAC/C,QAAOA,WAAS,IAAI,GAAG,MAAM,EAAE;;;AAIjC,SAAS,yBAAyB,KAAyC;AAIzE,QAAO;EAAE,aAFPA,WAAS,IAAI,IAAI,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;EAErD,YADHA,WAAS,IAAI,IAAI,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;EACxD;;AAGpC,IAAa,gBAAb,MAAgD;CAC9C,YAAY,QAA2C;AAA1B,OAAA,SAAA;;CAE7B,MAAM,IAA6B,QAAqC;EACtE,MAAM,MAAM,MAAM,KAAK,OAAO,YAAY,OAAO;AAGjD,UAAQ,QAAR;GACE,KAAK,wBACH,QAAO,iBAAiB,IAAI;GAC9B,KAAK,mBACH,QAAO,YAAY,IAAI;GACzB,KAAK,gCACH,QAAO,yBAAyB,IAAI;GACtC,QACE,OAAM,IAAI,MAAM,uBAAuB,OAAO,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACvChE,MAAa,+BAA+B;;;;;;AAO5C,MAAa,2BAA2B;;;;;;;ACUxC,MAAM,eAAe,IAAI,IAAI;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AASF,MAAM,kBAA4B;CAEhC;CAEA;CAEA;CAEA;CAEA;CACD;;;;;AAMD,SAAS,cAAc,OAAwB;AAC7C,QAAO,gBAAgB,MAAM,OAAO,GAAG,KAAK,MAAM,CAAC;;;;;;;AAQrD,SAAS,YAAY,OAAyB;AAC5C,KAAI,OAAO,UAAU,YAAY,cAAc,MAAM,CACnD,QAAO;AAET,QAAO;;;;;;;;;;AAWT,SAAS,aACP,OACA,OACA,QACyB;CACzB,MAAM,MAA+B;EACnC,qBAAI,IAAI,MAAM,EAAC,aAAa;EAC5B;EACA;EACD;AAED,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,EAAE;AACjD,MAAI,CAAC,aAAa,IAAI,IAAI,CAAE;AAE5B,MAAI,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,QAAS;AACxD,MAAI,OAAO,YAAY,MAAM;;AAG/B,QAAO;;;;;;AAOT,SAAS,SAAS,OAAiB,OAAiB,SAAkC,EAAE,EAAQ;CAC9F,MAAM,UAAU,aAAa,OAAO,OAAO,OAAO;AAClD,SAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,QAAQ,CAAC,IAAI;;;AAQtD,SAAgB,QAAQ,OAAiB,SAAkC,EAAE,EAAQ;AACnF,UAAS,QAAQ,OAAO,OAAO;;;AAIjC,SAAgB,QAAQ,OAAiB,SAAkC,EAAE,EAAQ;AACnF,UAAS,QAAQ,OAAO,OAAO;;;AAIjC,SAAgB,SAAS,OAAiB,SAAkC,EAAE,EAAQ;AACpF,UAAS,SAAS,OAAO,OAAO;;;;;;;;;;;;;;;;;;;;;;;;AC7HlC,MAAMC,wBAAsB;AA4B5B,SAASC,WAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU;;AAGhD,SAASC,eAAa,KAAuC;CAC3D,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AACN,SAAO;;AAET,KAAI,CAACD,WAAS,OAAO,CAAE,QAAO;CAC9B,MAAM,UAA6B,EAAE;AACrC,KAAI,OAAO,OAAO,OAAO,SAAU,SAAQ,KAAK,OAAO;AACvD,KAAI,OAAO,OAAO,WAAW,SAAU,SAAQ,SAAS,OAAO;AAC/D,KAAI,YAAY,OAAQ,SAAQ,SAAS,OAAO;AAChD,KAAI,YAAY,OAAQ,SAAQ,SAAS,OAAO;AAChD,KAAIA,WAAS,OAAO,MAAM,IAAI,OAAO,OAAO,MAAM,YAAY,SAC5D,SAAQ,QAAQ,EAAE,SAAS,OAAO,MAAM,SAAS;AAEnD,QAAO;;AAGT,MAAME,mBAA0C;CAC9C;CACA;CACA;CACD;;;;;;;;;;;;;;;AAgBD,MAAM,wBAAwB;;AAwB9B,MAAM,6BAA6B;;;;;AAMnC,IAAa,oBAAb,MAAwD;;CAEtD,OAAgB;CAEhB;CACA;CACA;CACA;CACA,UAA2B,IAAI,cAAc;CAC7C,0BAA2B,IAAI,KAA8B;CAC7D,0BAA2B,IAAI,KAAwB;CAEvD,KAA+B;CAC/B,kBAAiE;CACjE,gBAAwB;;;;;;CAMxB,iBAAwC;;CAExC,kBAAgD;;CAEhD,0BAA2B,IAAI,KAG5B;;;;;CAMH,sBAA6C;;;;;CAM7C,mCAAoC,IAAI,KAAqB;;CAG7D,kBAAiE;;CAGjE,qBAAoF,EAAE;CAEtF,YAAY,SAAmC;AAC7C,OAAK,eAAe,QAAQ,aAAa,QAAQ,OAAO,GAAG;AAC3D,OAAK,aAAa,QAAQ,cAAcH;AACxC,OAAK,aAAa,QAAQ;EAC1B,MAAM,QAAQ,QAAQ,IAAI,6BACtB,OAAO,QAAQ,IAAI,2BAA2B,GAC9C,KAAA;AACJ,OAAK,oBACF,UAAU,KAAA,KAAa,OAAO,SAAS,MAAM,IAAI,QAAQ,IAAI,QAAQ,KAAA,MACtE,QAAQ,oBACR;AACF,OAAK,MAAM,SAASG,iBAAgB,MAAK,QAAQ,IAAI,OAAO,EAAE,CAAC;AAG/D,OAAK,QAAQ,IAAI,2BAA2B,EAAE,CAAC;AAG/C,OAAK,QAAQ,gBAAgB,EAAE;;;CAIjC,MAAM,iBAAuC;EAO3C,IAAI,aAAa,GAAG,KAAK,aAAa;AACtC,MAAI,KAAK,YAAY;GACnB,MAAM,OAAO,aAAa,KAAK,WAAW;AAC1C,iBAAc,OAAO,mBAAmB,KAAK;;EAE/C,MAAM,MAAM,MAAM,MAAM,WAAW;AACnC,MAAI,CAAC,IAAI,GACP,OAAM,IAAI,MAAM,qCAAqC,IAAI,OAAO,GAAG,IAAI,aAAa;EAEtF,MAAM,OAAgB,MAAM,IAAI,MAAM;EACtC,MAAM,OAAOF,WAAS,KAAK,IAAI,MAAM,QAAQ,KAAK,QAAQ,GAAG,KAAK,UAAU,EAAE;EAM9E,IAAI,iBAAgC;AACpC,OAAK,MAAM,QAAQ,MAAM;AACvB,OAAI,CAACA,WAAS,KAAK,IAAI,OAAO,KAAK,OAAO,SAAU;AACpD,oBAAiB,KAAK;;AAIxB,MACE,mBAAmB,QACnB,KAAK,mBAAmB,QACxB,mBAAmB,KAAK,gBACxB;GACA,MAAM,SAAS,KAAK;AAGpB,WAAQ,iBAAiB,EAAE,cAAc,QAAQ,CAAC;AAClD,QAAK,YAAY,OAAO;;AAI1B,OAAK,QAAQ,OAAO;AACpB,OAAK,MAAM,QAAQ,MAAM;AACvB,OAAI,CAACA,WAAS,KAAK,IAAI,OAAO,KAAK,OAAO,SAAU;AAEpD,OAAI,KAAK,OAAO,eAAgB;AAChC,QAAK,QAAQ,IAAI,KAAK,IAAI;IACxB,IAAI,KAAK;IACT,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;IACrD,KAAK,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;IAChD,CAAC;;AAGJ,MAAI,mBAAmB,KACrB,MAAK,iBAAiB;MAEtB,MAAK,iBAAiB;EAGxB,MAAM,SAAS,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC;AAIzC,MAAI,mBAAmB,KACrB,MAAK,QAAQ,KAAK,mBAAmB,OAAO;AAG9C,SAAO;;CAGT,cAA2B;AACzB,SAAO,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;CAwBnC,mBACE,UACA,YAAY,KACZ,iBAAiB,KACK;EAEtB,MAAM,UAAU,KAAK,aAAa;AAClC,MAAI,SAAS,QAAQ,CAAE,QAAO,QAAQ,QAAQ,QAAQ;AAEtD,SAAO,IAAI,SAAsB,SAAS,WAAW;GACnD,IAAI,UAAU;GACd,IAAI,aAAoD;GAExD,MAAM,UAAU,YAA+B;AAC7C,QAAI,QAAS;AACb,cAAU;AACV,iBAAa,cAAc;AAC3B,QAAI,eAAe,MAAM;AACvB,mBAAc,WAAW;AACzB,kBAAa;;AAEf,SAAK,QAAQ,IAAI,mBAAmB,SAAS;AAC7C,YAAQ,QAAQ;;GAGlB,MAAM,YAAY,YAA+B;AAC/C,QAAI,SAAS,QAAQ,CAAE,QAAO,QAAQ;;GAGxC,MAAM,gBAAgB,iBAAiB;AACrC,QAAI,QAAS;AACb,cAAU;AACV,QAAI,eAAe,MAAM;AACvB,mBAAc,WAAW;AACzB,kBAAa;;AAEf,SAAK,QAAQ,IAAI,mBAAmB,SAAS;AAC7C,2BACE,IAAI,MACF,6BAA6B,UAAU,iCACxC,CACF;MACA,UAAU;AAGb,QAAK,QAAQ,GAAG,mBAAmB,SAAS;AAM5C,gBAAa,kBAAkB;AAE7B,SAAK,gBAAgB,CAAC,MACnB,YAAY;AACX,SAAI,SAAS,QAAQ,CAAE,QAAO,QAAQ;aAElC,GAGP;MACA,eAAe;IAClB;;;;;;CAOJ,yBAAwC;AACtC,SAAO,KAAK;;;;;;CAOd,oBAAoB,UAAiC;AACnD,SAAO,KAAK,iBAAiB,IAAI,SAAS,IAAI;;;CAIhD,YAAY,UAA6D;AACvE,OAAK,mBAAmB,KAAK,SAAS;AACtC,eAAa;GACX,MAAM,MAAM,KAAK,mBAAmB,QAAQ,SAAS;AACrD,OAAI,QAAQ,GAAI,MAAK,mBAAmB,OAAO,KAAK,EAAE;;;;;;;CAQ1D,MAAM,gBAA+B;AACnC,MAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAAM;AAGtD,MAAI,KAAK,gBAAiB,QAAO,KAAK;AACtC,OAAK,kBAAkB,KAAK,kBAAkB,CAAC,cAAc;AAC3D,QAAK,kBAAkB;IACvB;AACF,SAAO,KAAK;;CAGd,MAAc,mBAAkC;EAE9C,MAAM,UADU,MAAM,KAAK,gBAAgB,EACpB;AACvB,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,mDAAmD;EAKrE,IAAI,YAAY,GAFD,KAAK,aAAa,QAAQ,SAAS,KAAK,CAE7B,UADT,gBAAgB,KAAK,KAAK,GACE,UAAU,mBAAmB,OAAO,GAAG;AAOpF,MAAI,KAAK,YAAY;GACnB,MAAM,OAAO,aAAa,KAAK,WAAW;AAC1C,gBAAa,OAAO,mBAAmB,KAAK;;EAE9C,MAAM,KAAK,IAAI,UAAU,UAAU;AACnC,OAAK,KAAK;AAEV,QAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,MAAG,KAAK,cAAc,SAAS,CAAC;AAChC,MAAG,KAAK,UAAU,QAAe,OAAO,IAAI,CAAC;AAO7C,MAAG,KAAK,UAAU,SAAiB;AACjC,QAAI,SAAA,KACF,wBACE,IAAI,MACF,4EACD,CACF;KAEH;IACF;AAGF,OAAK,sBAAsB;AAC3B,OAAK,iBAAiB,OAAO;AAG7B,OAAK,kBAAkB;AACvB,KAAG,GAAG,YAAY,SAA4B,KAAK,cAAc,KAAK,UAAU,CAAC,CAAC;AAKlF,KAAG,GAAG,UAAU,SACd,KAAK,iBACH,SAAA,OACI,gDACA,4BACL,CACF;AACD,KAAG,GAAG,UAAU,QAAe,KAAK,iBAAiB,uBAAuB,IAAI,UAAU,CAAC;AAE3F,OAAK,kBAAkB,iBAAiB;AACxC,OAAK,kBAAkB,iBAAiB;AAGxC,OAAK,kBAAkB,aAAa;AACpC,OAAK,kBAAkB,cAAc;AAIrC,OAAK,kBAAkB,mBAAmB;AAC1C,OAAK,kBAAkB,6BAA6B,EAAE,UAAU,MAAM,CAAC;AAGvE,OAAK,eAAe,OAAO,GAAG;;;CAIhC,kBAA0B,QAAgB,SAAkC,EAAE,EAAQ;AACpF,MAAI,CAAC,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAAM;EACvD,MAAM,KAAK,KAAK;AAChB,OAAK,GAAG,KAAK,KAAK,UAAU;GAAE;GAAI;GAAQ;GAAQ,CAAC,CAAC;;;;;;CAOtD,KACE,QACA,QACqC;AACrC,SAAO,KAAK,YAAY,QAAS,UAAU,EAAE,CAA6B;;;;;;;;;;;;;;;CAkB5E,YAAY,QAAgB,SAAkC,EAAE,EAAoB;AAElF,MAAI,KAAK,oBAAoB,eAC3B,QAAO,QAAQ,uBACb,IAAI,MACF,wBAAwB,OAAO,yDAChC,CACF;AAEH,MAAI,CAAC,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAC/C,QAAO,QAAQ,uBACb,IAAI,MAAM,+EAA+E,CAC1F;EAEH,MAAM,KAAK,KAAK;EAChB,MAAM,KAAK,KAAK;EAChB,MAAM,YAAY,KAAK;AACvB,SAAO,IAAI,SAAkB,SAAS,WAAW;GAC/C,MAAM,SAAS,iBAAiB;AAC9B,SAAK,QAAQ,OAAO,GAAG;AACvB,2BACE,IAAI,MACF,qBAAqB,OAAO,IAAI,UAAU,iFAG3C,CACF;MACA,UAAU;AACb,QAAK,QAAQ,IAAI,IAAI;IACnB,UAAU,MAAM;AACd,kBAAa,OAAO;AACpB,aAAQ,EAAE;;IAEZ,SAAS,MAAM;AACb,kBAAa,OAAO;AACpB,YAAO,EAAE;;IAEZ,CAAC;AACF,MAAG,KAAK,KAAK,UAAU;IAAE;IAAI;IAAQ;IAAQ,CAAC,CAAC;IAC/C;;;;;;;CAQJ,iBAAyB,QAAsB;AAC7C,MAAI,KAAK,oBAAoB,eAAgB;AAC7C,OAAK,kBAAkB;AACvB,OAAK,KAAK;AACV,OAAK,eAAe;EACpB,MAAM,sBAAM,IAAI,MACd,GAAG,OAAO,wDACX;AACD,OAAK,MAAM,UAAU,KAAK,QAAQ,QAAQ,CACxC,QAAO,OAAO,IAAI;AAEpB,OAAK,QAAQ,OAAO;;;;;;;;;;CAWtB,YAAoB,UAAwB;EAC1C,MAAM,8BAAa,IAAI,MAAM,EAAC,aAAa;AAC3C,OAAK,QAAQ,OAAO,SAAS;AAC7B,OAAK,iBAAiB,OAAO,SAAS;EAEtC,MAAM,sBAAM,IAAI,MACd,+EAA+E,SAAS,qCAEzF;AACD,OAAK,MAAM,UAAU,KAAK,QAAQ,QAAQ,CACxC,QAAO,OAAO,IAAI;AAEpB,OAAK,QAAQ,OAAO;EAEpB,MAAM,QAA8B;GAAE,MAAM;GAAY;GAAU;GAAY;AAC9E,OAAK,MAAM,YAAY,KAAK,mBAC1B,KAAI;AACF,YAAS,MAAM;UACT;;;;;;;;;;;CAeZ,iBAAyB,MAAoC,UAA+B;EAC1F,MAAM,8BAAa,IAAI,MAAM,EAAC,aAAa;AAC3C,OAAK,sBAAsB,KAAK,KAAK;AAGrC,MAAI,aAAa,MAAM;AACrB,QAAK,QAAQ,OAAO,SAAS;AAC7B,QAAK,iBAAiB,OAAO,SAAS;AAEtC,OAAI,KAAK,mBAAmB,SAC1B,MAAK,iBAAiB;SAEnB;AAEL,QAAK,QAAQ,OAAO;AACpB,QAAK,iBAAiB,OAAO;AAC7B,QAAK,iBAAiB;;EAUxB,MAAM,sBAAM,IAAI,MACd,eANA,SAAS,YACL,yCACA,SAAS,cACP,uCACA,4CAEe,iFAEtB;AACD,OAAK,MAAM,UAAU,KAAK,QAAQ,QAAQ,CACxC,QAAO,OAAO,IAAI;AAEpB,OAAK,QAAQ,OAAO;EAGpB,MAAM,QAA8B;GAAE;GAAM;GAAU;GAAY;AAClE,OAAK,MAAM,YAAY,KAAK,mBAC1B,KAAI;AACF,YAAS,MAAM;UACT;;;;;;;;;;;;;;;;CAoBZ,eAAuB,iBAA+B;AACpD,OAAK,eAAe;EAEpB,MAAM,QAAQ,QAAQ,IAAI,uBACtB,OAAO,QAAQ,IAAI,qBAAqB,GACxC,KAAA;AACJ,MAAI,UAAU,KAAA,KAAa,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,EAAG;EAElE,MAAM,kBAAkB;AAExB,OAAK,kBAAkB,kBAAkB;GAEvC,MAAM,YAAY,KAAK,QAAQ,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,MAAM,CAAC,GAAG,CAAC,gBAAgB;AACtF,QAAK,MAAM,YAAY,WAAW;IAEhC,MAAM,cAAc,KAAK,YAAY,oBAAoB;KACvD,YAAY;KACZ,eAAe;KACf,SAAS;KACV,CAAC;IACF,MAAM,iBAAiB,IAAI,SAAgB,GAAG,WAC5C,iBACQ,uBAAO,IAAI,MAAM,oBAAoB,CAAC,EAC5C,kBAAkB,IACnB,CACF;AACD,YAAQ,KAAK,CAAC,aAAa,eAAe,CAAC,CAAC,YAAY;AAEtD,SAAI,KAAK,QAAQ,IAAI,SAAS,CAC5B,MAAK,iBAAiB,aAAa,SAAS;MAE9C;;KAEH,MAAM;;CAGX,gBAA8B;AAC5B,MAAI,KAAK,oBAAoB,MAAM;AACjC,iBAAc,KAAK,gBAAgB;AACnC,QAAK,kBAAkB;;;CAI3B,cAAsB,KAAmB;EACvC,MAAM,UAAUC,eAAa,IAAI;AACjC,MAAI,CAAC,QAAS;AAGd,MAAI,OAAO,QAAQ,OAAO,YAAY,KAAK,QAAQ,IAAI,QAAQ,GAAG,EAAE;GAClE,MAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ,GAAG;AAC3C,QAAK,QAAQ,OAAO,QAAQ,GAAG;AAC/B,OAAI,OACF,KAAI,QAAQ,MAAO,QAAO,OAAO,IAAI,MAAM,QAAQ,MAAM,QAAQ,CAAC;OAC7D,QAAO,QAAQ,QAAQ,OAAO;AAErC;;EAQF,MAAM,MAAM,KAAK,KAAK;EACtB,IAAI,mBAAmB;AACvB,OAAK,MAAM,YAAY,KAAK,QAAQ,MAAM,EAAE;AAC1C,OAAI,CAAC,KAAK,iBAAiB,IAAI,SAAS,CACtC,oBAAmB;AAErB,QAAK,iBAAiB,IAAI,UAAU,IAAI;;AAE1C,MAAI,oBAAoB,KAAK,QAAQ,OAAO,EAC1C,MAAK,QAAQ,KAAK,mBAAmB,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC,CAAC;AAGlE,MAAI,OAAO,QAAQ,WAAW,SAAU;AAMxC,MAAI,QAAQ,WAAW,2BAA2B;AAChD,QAAK,iBAAiB,WAAW,KAAK;AACtC;;AAIF,MAAI,QAAQ,WAAW,0BAA0B;GAC/C,MAAM,WACJD,WAAS,QAAQ,OAAO,IAAI,OAAO,QAAQ,OAAO,aAAa,WAC3D,QAAQ,OAAO,WACf;AACN,QAAK,iBAAiB,aAAa,SAAS;AAC5C;;AAIF,MAAI,QAAQ,WAAW,6BAA6B;GAClD,MAAM,WACJA,WAAS,QAAQ,OAAO,IAAI,OAAO,QAAQ,OAAO,aAAa,WAC3D,QAAQ,OAAO,WACf;AACN,QAAK,iBAAiB,YAAY,SAAS;AAC3C;;AAIF,MAAI,CAAC,KAAK,QAAQ,IAAI,QAAQ,OAAuB,CAAE;EACvD,MAAM,QAAQ,QAAQ;EACtB,MAAM,SAAS,KAAK,QAAQ,IAAI,MAAM;AACtC,MAAI,CAAC,OAAQ;AACb,SAAO,KAAK,QAAQ,OAAO;EAG3B,MAAM,MAAM,UAAU,4BAA4B,wBAAwB,KAAK;AAC/E,MAAI,OAAO,SAAS,IAAK,QAAO,OAAO;AACvC,OAAK,QAAQ,KAAK,OAAO,QAAQ,OAAO;;CAG1C,kBAA0C,OAAyC;AAEjF,SADe,KAAK,QAAQ,IAAI,MAAM,IACpB,EAAE;;CAGtB,GAA2B,OAAU,UAAyD;AAC5F,OAAK,QAAQ,GAAG,OAAO,SAAuC;AAC9D,eAAa,KAAK,QAAQ,IAAI,OAAO,SAAuC;;;CAI9E,QAAc;EACZ,MAAM,KAAK,KAAK;AAChB,OAAK,eAAe;AAGpB,OAAK,iBAAiB,+BAA+B;AACrD,MAAI,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACvuBf,MAAMG,YAAU,cAAc,OAAO,KAAK,IAAI;;;;;;;;AAS9C,MAAM,gCAAgC;;;;;;;;AAsBtC,SAAS,sBAA0D;AACjE,KAAI;EACF,MAAM,MAAeA,UAAQ,kCAAkC;AAC/D,MAAI,OAAO,QAAQ,WACjB,QAAO;SAEH;AAGR,QAAO;;;;;;;;;;;;;;;;;;;;;AAsBT,eAAe,qBACb,MACA,cACA,cACiC;AACjC,KAAI,iBAAiB,MAAM;AACzB,QAAM,KAAK,MAAM,aAAa;AAC9B,SAAO;;CAGT,IAAI,WAAmC;CACvC,MAAM,QAAQ,aAAa;CAC3B,MAAM,gBAAgB,MAAM;AAE5B,OAAM,QAAQ,SAAiC,QAAQ;AACrD,aAAW;AACX,SAAO,cAAc,KAAK,MAAM,OAAO;;AAGzC,KAAI;AACF,QAAM,KAAK,MAAM,aAAa;WACtB;AAER,QAAM,QAAQ;;AAGhB,QAAO;;AAcT,SAAS,iBAAmC;CAE1C,MAAM,MAAeA,UAAQ,OAAO;AACpC,KACE,OAAO,QAAQ,YACf,QAAQ,QACR,WAAW,OACX,OAAQ,IAA2B,UAAU,WAE7C,QAAO;AAET,OAAM,IAAI,MAAM,4CAA4C;;;;;;;;;;;;;;;;;;;;;AAyC9D,SAAgB,oBAAoB,QAA+B;CACjE,MAAM,QAAQ,oCAAoC,KAAK,OAAO;AAC9D,KAAI,UAAU,KAAM,QAAO;CAC3B,MAAM,OAAO,MAAM;CACnB,MAAM,OAAO,MAAM,OAAO,KAAA,KAAa,MAAM,OAAO,KAAK,MAAM,MAAM;CACrE,MAAM,QAAQ,MAAM,MAAM;AAE1B,QAAO,GAAG,OAAO,QADC,UAAU,KAAK,MAAM,IACJ,KAAK;;;;;;;;;;;;;;;;;;AA+E1C,eAAsB,eAAe,UAAiC,EAAE,EAAsB;CAC5F,MAAM,gBAAgB,QAAQ,QAAQ;CACtC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,EAAE,YAAY,iBAAiB;CACrC,MAAM,sBACJ,QAAQ,wBAAwB,KAAA,IAC5B,QAAQ,sBACR;CAEN,MAAM,aAAa,cAAc;CAIjC,MAAM,oBAAoB,SAA6C;AACrE,MAAI,iBAAiB,KAAA,EAAW;AAChC,MAAI;AACF,gBAAa,EAAE,MAAM,CAAC;UAChB;;AAiBV,KAAI,WAuBF,YAAW,GAAG,YAAY,KAAK,QAAQ;EACrC,MAAM,YAAY,oBAAoB,IAAI,OAAO,GAAG;AACpD,MAAI,cAAc,MAAM;AAEtB,OAAI,MAAM;AACV,OAAI,CAAC,WAAW,IAAI,EAAE;AAMpB,QAAI,aAAa;AACjB,QAAI,UAAU,+BAA+B,IAAI;AACjD,QAAI,UAAU,gBAAgB,mBAAmB;AACjD,QAAI,IAAI,KAAK,UAAU,EAAE,OAAO,0BAA0B,CAAC,CAAC;AAC5D,qBAAiB,eAAe;;AAIlC;;EAMF,MAAM,YAAY,IAAI,OAAO,IAAI,MAAM,IAAI,CAAC;AAC5C,MAAI,aAAa,cAAc,aAAa,aAAa;AAIvD,OAAI,CAAC,WAAW,IAAI,EAAE;AAEpB,QAAI,aAAa;AACjB,QAAI,UAAU,+BAA+B,IAAI;AACjD,QAAI,UAAU,gBAAgB,mBAAmB;AACjD,QAAI,IAAI,KAAK,UAAU,EAAE,OAAO,0BAA0B,CAAC,CAAC;AAC5D,qBAAiB,eAAe;AAEhC;;AAOF;;GAQF;CAgBJ,MAAM,eAAe,sBAAsB,IAAI,qBAAqB,GAAG;CACvE,MAAM,kBAAkB,MAAM,qBAC5B,gBAAgB,EAChB;EAAE,QAAQ;EAAY,QAAQ,GAAG,KAAK,GAAG;EAAiB,MAAM;EAAe,EAC/E,aACD;AAUD,KAAI,YAAY;EACd,MAAM,uBAAuB,WAAW,UAAU,UAAU;AAG5D,aAAW,mBAAmB,UAAU;EAGxC,MAAM,YAAY,IAAI,gBAAgB,EAAE,UAAU,MAAM,CAAC;AACzD,aAAW,GAAG,YAAY,KAAsB,QAAgB,SAAiB;GAK/E,MAAM,YAAY,oBAAoB,IAAI,OAAO,GAAG;AACpD,OAAI,cAAc,KAChB,KAAI,MAAM;AAEZ,OAAI,CAAC,WAAW,IAAI,EAAE;AAOpB,cAAU,cAAc,KAAK,QAAQ,OAAO,OAAO;AACjD,QAAG,MAAM,8BAA8B,yBAAyB;MAChE;AACF,qBAAiB,aAAa;AAE9B;;AAIF,QAAK,MAAM,YAAY,qBACrB,UAAS,KAAK,QAAQ,KAAK;IAE7B;;CAGJ,MAAM,aAAa,MAAM,IAAI,SAAiB,SAAS,WAAW;AAChE,aAAW,KAAK,SAAS,OAAO;AAChC,aAAW,OAAO,eAAe,YAAY;AAC3C,cAAW,IAAI,SAAS,OAAO;AAG/B,WADa,WAAW,SAAS,CACpB,KAAK;IAClB;GACF;CAYF,IAAI,kBAAyD;AAC7D,KAAI,sBAAsB,KAAK,oBAAoB,MAAM;EACvD,MAAM,UAAU;AAChB,oBAAkB,kBAAkB;AAClC,QAAK,MAAM,UAAU,QAAQ,KAAK,QAEhC,KAAI,OAAO,eAAe,EACxB,QAAO,MAAM;KAGhB,oBAAoB;;AAGzB,QAAO;EACL,MAAM;EACN,SAAS,UAAU,KAAK,GAAG;EAC3B,aACE,IAAI,SAAe,YAAY;AAC7B,OAAI,oBAAoB,MAAM;AAC5B,kBAAc,gBAAgB;AAC9B,sBAAkB;;AAEpB,cAAW,YAAY,SAAS,CAAC;IACjC;EACL;;;;;;;;;;;AClhBH,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoErB,SAAgB,uBACd,WACA,QACA,UACA,MACQ;CACR,IAAI,MACF,GAAG,aAAa,OAAO,mBAAmB,UAAU,CAAA,iBAClC,mBAAmB,OAAO;AAC9C,KAAI,aAAa,KAAA,KAAa,aAAa,GACzC,QAAO,OAAO,mBAAmB,SAAS;AAG5C,KAAI,MAAM,SAAS,KAAA,KAAa,KAAK,KAAK,MAAM,KAAK,GACnD,QAAO,SAAS,mBAAmB,KAAK,KAAK,MAAM,CAAC;AAEtD,KAAI,MAAM,SAAS,KAAA,GAAW;EAC5B,IAAI;AACJ,MAAI;AACF,gBAAa,IAAI,IAAI,KAAK,KAAK;UACzB;AACN,gBAAa;;AAEf,MAAI,YAAY,aAAa,SAC3B,QAAO,SAAS,mBAAmB,KAAK,KAAK;;AAKjD,KAAI,MAAM,cAAc,KACtB,QAAO;AAET,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CT,MAAM,yBAAyB,IAAI,IAAY;CAAC;CAAI;CAAO;CAAa;CAAa;CAAM,CAAC;;;;;;;;;;AAW5F,SAAgB,wBAAwB,WAAkC;CAIxE,MAAM,cAAc,UAAU,QAAQ,kCAAkC,GAAG;AAC3E,KAAI,gBAAgB,UAElB,QACE;CAMJ,MAAM,eAAe,YAAY,OAAO,QAAQ;CAChD,MAAM,YAAY,iBAAiB,KAAK,cAAc,YAAY,MAAM,GAAG,aAAa;AAExF,KAAI,uBAAuB,IAAI,UAAU,aAAa,CAAC,CAErD,QACE,wBAFuB,cAAc,KAAK,YAAY,IAAI,UAAU,GAE3B;AAM7C,QAAO;;AAMT,SAAS,cAAc,OAAe,KAAqB;AACzD,KAAI,UAAU,GAAI,QAAO;AACzB,QAAO,MACJ,MAAM,IAAI,CACV,QAAQ,SAAS,SAAS,MAAM,KAAK,MAAM,IAAI,CAAC,OAAO,IAAI,CAC3D,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;AAsBd,SAAgB,uBACd,WACA,QACA,UACQ;CACR,IAAI;AACJ,KAAI;AACF,UAAQ,IAAI,IAAI,OAAO;SACjB;AACN,QAAM,IAAI,MAAM,iCAAiC,SAAS;;AAE5D,KAAI,MAAM,aAAa,OACrB,OAAM,IAAI,MAAM,2CAA2C,MAAM,SAAS,IAAI,OAAO,GAAG;CAG1F,MAAM,YAAY,UAAU,QAAQ,IAAI;CACxC,MAAM,OAAO,cAAc,KAAK,KAAK,UAAU,MAAM,UAAU;CAC/D,MAAM,aAAa,cAAc,KAAK,YAAY,UAAU,MAAM,GAAG,UAAU;CAE/E,MAAM,aAAa,WAAW,QAAQ,IAAI;CAC1C,MAAM,OAAO,eAAe,KAAK,aAAa,WAAW,MAAM,GAAG,WAAW;CAC7E,IAAI,QAAQ,eAAe,KAAK,KAAK,WAAW,MAAM,aAAa,EAAE;CAErE,MAAM,WAA0B,CAC9B,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,OAAO,CAClB;AAID,KAAI,aAAa,KAAA,KAAa,aAAa,GACzC,UAAS,KAAK,CAAC,MAAM,SAAS,CAAC;AAKjC,SAAQ,cAAc,OAAO,KAAK;AAElC,MAAK,MAAM,CAAC,QAAQ,SAClB,SAAQ,cAAc,OAAO,IAAI;AAEnC,MAAK,MAAM,CAAC,KAAK,UAAU,UAAU;EACnC,MAAM,OAAO,GAAG,IAAI,GAAG,mBAAmB,MAAM;AAChD,UAAQ,UAAU,KAAK,OAAO,GAAG,MAAM,GAAG;;AAG5C,QAAO,GAAG,KAAK,GAAG,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9J5B,SAAgB,sBACd,kBACA,UACA,UACA,QAAwD,WACzC;AAKf,KAAI,CAAC,SACH,QAAO;CAST,IAAI;CACJ,IAAI;AACJ,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,iBAAiB;AACxC,cAAY,OAAO;AACnB,gBAAc,OAAO,aAAa,WAAW,QAAQ;SAC/C;AAEN,cAAY,iBAAiB,QAAQ,iBAAiB,GAAG;AACzD,gBAAc,WAAW,KAAK,iBAAiB,GAAG,QAAQ;;CAK5D,MAAM,WAAW,mBAAmB,KAAK,KAAK,CAAC,SAAS,GAAG;CAO3D,MAAM,OAAO,UAAU;CACvB,MAAM,SAAS,GAAG,UAAU,UAAU,SAAS,UAAU,mBAAmB,SAAS,CAAC,MAAM,mBAAmB,KAAK;CAEpH,MAAM,SAAS,IAAI,gBAAgB;GAAG,cAAc;EAAQ;EAAO,CAAC;AACpE,QAAO,GAAG,iBAAiB,QAAQ,OAAO,GAAG,CAAC,2BAA2B,OAAO,UAAU;;;;;;;;;;;;;AAkB5F,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;;;;;;;;;;;;;;;;;;AA6ET,IAAa,qBAAb,MAAgC;;CAE9B,iCAAkC,IAAI,KAAa;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BnD,KAAK,SAAoC;AACvC,MAAI,wBAAwB,CAAE;AAC9B,MAAI,QAAQ,QAAQ,OAAQ;AAC5B,MAAI,CAAC,QAAQ,SAAU;EAGvB,MAAM,WAAW,QAAQ;AACzB,MAAI,KAAK,eAAe,IAAI,SAAS,CAAE;AAGvC,MAAI,QAAQ,oBAAoB;AAC9B,QAAK,eAAe,IAAI,SAAS;GACjC,MAAM,YAAY,QAAQ;AAC1B,WAAQ,OAAO,MACb;6BACgC,UAAU,QAAQ,cAAc,GAAG,CAAC;EAErE;AAED,OAAI,CADW,iBAAiB,UAAU,CAExC,SAAQ,OAAO,MACb,+BAA+B,UAAU,qBAC1C;AAEH;;AAIF,MAAI,CAAC,QAAQ,iBAAkB;AAE/B,OAAK,eAAe,IAAI,SAAS;EAEjC,MAAM,eAAe,sBACnB,QAAQ,kBACR,UACA,QAAQ,SACT;AAKD,MAAI,iBAAiB,MAAM;AACzB,WAAQ,OAAO,MACb,6HAED;AACD;;AAGF,UAAQ,OAAO,MACb;4BAC+B,aAAa;;EAG7C;AAGD,MAAI,CADW,iBAAiB,aAAa,CAE3C,SAAQ,OAAO,MACb,sDACD;;;;;;;CASL,IAAI,SAAkB;AACpB,SAAO,KAAK,eAAe,OAAO;;;CAIpC,IAAI,gBAAqC;AACvC,SAAO,KAAK;;;;;;;;;ACnVhB,SAAgB,eAAwB;AACtC,QAAO,QAAQ,IAAI,mBAAmB;;;;;;;AAQxC,SAAgB,cAAc,KAAkC;AAC9D,QAAO;;;;;;;;;;;;;;;;;AA4BT,SAAgB,aAAgB,MAAS,KAA2C;AAClF,KAAI,cAAc,CAAE,QAAO;AAC3B,QAAO;EACL,IAAI;EACJ;EACA,MAAM;GACJ,MAAM,IAAI;GACV,KAAK,cAAc,IAAI,IAAI;GAC3B,UAAU,IAAI;GACd,aAAa,IAAI,eAAe;GACjC;EACF;;;;;;;;;;;;ACfH,SAAgB,WAAW,KAA8B;AACvD,SAAQ,KAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK,eACH,QAAO;EACT,KAAK,OACH,QAAO;;;;;;;;AASb,SAAgB,eAAe,KAA8B;AAC3D,QAAO,QAAQ;;;;;;;AAQjB,SAAgB,YAAY,KAAuC;AACjE,KAAI,QAAQ,OAAQ,QAAO;AAC3B,QAAO;;;;;;;;;;;;;;;;;;;;;AAsBT,SAAgB,kBACd,MACA,YACA,aACgB;AAChB,SAAQ,MAAR;EACE,KAAK,QACH,QAAO;EACT,KAAK;AACH,OAAI,WAAY,QAAO;AACvB,UAAO,gBAAgB,iBAAiB,iBAAiB;;;;;;;;;;;;;AAkB/D,IAAI,aAAa;;AAGjB,SAAgB,gBAAyB;AACvC,QAAO;;;;;;;AAQT,SAAgB,cAAc,OAAsB;AAClD,cAAa;;;;;;;;;AC/Jf,SAAgB,SAAS,SAAiC;AACxD,QAAO;EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM;GAAS,CAAC;EAC1C,SAAS;EACV;;;;;;;;;;;AAgBH,SAAgB,mBACd,UACA,aACA,YACA,QACgB;AAYhB,QAAO,SAAS,GAJd,GAAG,SAAS,IAPG,gBAAgB,UAAU,mBAAmB,iBAOnC,4BANN,eAAe,UAAU,UAAU,OAO/B,IAAI,OAAO,KALlC,gBAAgB,UACZ,yFACA,+CAMkB,MADT,QAAQ,SAAS,wBAAwB,YAAY,2BAA2B,WAAW,IAAI,OAAO,MAC9E;;;;;;;AAYzC,SAAgB,kBAAkC;AAChD,QAAO,SACL,6EAED;;;;;;;AAQH,SAAgB,iBAAiB,UAAmC;AAElE,QAAO,SACL,GAFa,WAAW,GAAG,SAAS,MAAM,GAEhC,6IAGX;;;;;;;;AASH,SAAgB,eAAe,UAAmC;AAEhE,QAAO,SACL,GAFa,WAAW,GAAG,SAAS,MAAM,GAEhC,qEAEX;;;;;;;;;;;;;;;;AAiBH,SAAgB,eAAe,UAAmB,UAAU,OAAuB;CACjF,MAAM,SAAS,WAAW,GAAG,SAAS,MAAM;AAC5C,KAAI,QACF,QAAO,SACL,GAAG,OAAO,wOAIX;AAEH,QAAO,SACL,GAAG,OAAO,uIAGX;;;;;;;;;;AAeH,SAAgB,eAAe,UAAkC;AAS/D,QAAO,SAPL,sBAAsB,SAAS;;;sCAGQ,SAAS;;;wDAI7B;;;;;AAUvB,SAAgB,qBAAqB,UAAmC;AAEtE,QAAO,SACL,GAFa,WAAW,GAAG,SAAS,MAAM,GAEhC,kEAEX;;;;;;;;;;AAeH,SAAgB,kBAAkB,KAAc,UAAkB,UAAU,OAAuB;CACjG,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAGhE,KAAI,QAAQ,WAAW,eAAe,IAAI,QAAQ,SAAS,eAAe,CACxE,QAAO,iBAAiB;AAM1B,KACE,QAAQ,WAAW,cAAc,IACjC,QAAQ,SAAS,wBAAwB,IACzC,QAAQ,SAAS,oCAAoC,IACpD,QAAQ,SAAS,YAAY,IAAI,QAAQ,SAAS,gBAAgB,CAEnE,QAAO,eAAe,UAAU,QAAQ;AAI1C,KACE,QAAQ,SAAS,yBAAyB,IAC1C,QAAQ,SAAS,gBAAgB,IACjC,QAAQ,SAAS,kBAAkB,IACnC,QAAQ,SAAS,qBAAqB,CAEtC,QAAO,eAAe,SAAS;AAIjC,KAAI,QAAQ,SAAS,sBAAsB,IAAI,QAAQ,SAAS,kBAAkB,CAChF,QAAO,qBAAqB,SAAS;AAIvC,QAAO,SACL,GAAG,SAAS,OAAO,QAAQ,4CAC5B;;;;;;;;;;;;;;;;;;;;;;ACtMH,MAAM,sBAAsB;AAW5B,SAASC,WAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU;;AAGhD,SAAS,aAAa,KAAuC;CAC3D,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AACN,SAAO;;AAET,KAAI,CAACA,WAAS,OAAO,CAAE,QAAO;CAC9B,MAAM,UAA6B,EAAE;AACrC,KAAI,OAAO,OAAO,OAAO,SAAU,SAAQ,KAAK,OAAO;AACvD,KAAI,OAAO,OAAO,WAAW,SAAU,SAAQ,SAAS,OAAO;AAC/D,KAAI,YAAY,OAAQ,SAAQ,SAAS,OAAO;AAChD,KAAI,YAAY,OAAQ,SAAQ,SAAS,OAAO;AAChD,KAAIA,WAAS,OAAO,MAAM,IAAI,OAAO,OAAO,MAAM,YAAY,SAC5D,SAAQ,QAAQ,EAAE,SAAS,OAAO,MAAM,SAAS;AAEnD,QAAO;;AAGT,MAAM,iBAA0C;CAC9C;CACA;CACA;CACD;;;;;;;;;;;;;AAqCD,IAAa,qBAAb,MAAyD;;CAEvD,OAAgB;CAEhB;CACA;CACA,UAA2B,IAAI,cAAc;CAC7C,0BAA2B,IAAI,KAA8B;CAC7D,0BAA2B,IAAI,KAAwB;CAEvD,KAA+B;CAC/B,gBAAwB;;CAExB,kBAAgD;;CAEhD,0BAA2B,IAAI,KAG5B;CAEH,YAAY,SAAoC;AAC9C,OAAK,kBAAkB,QAAQ,gBAAgB,QAAQ,OAAO,GAAG;AACjE,OAAK,aAAa,QAAQ,cAAc;AACxC,OAAK,MAAM,SAAS,eAAgB,MAAK,QAAQ,IAAI,OAAO,EAAE,CAAC;AAG/D,OAAK,QAAQ,gBAAgB,EAAE;;;;;;;;;CAUjC,MAAc,eAGX;EAED,MAAM,MAAM,MAAM,MAAM,GAAG,KAAK,gBAAgB,OAAO;AACvD,MAAI,CAAC,IAAI,GACP,OAAM,IAAI,MACR,yCAAyC,IAAI,OAAO,GAAG,IAAI,WAAW,wDAEvE;EAEH,MAAM,OAAgB,MAAM,IAAI,MAAM;EACtC,MAAM,OAA6B,MAAM,QAAQ,KAAK,GAAI,OAAgC,EAAE;AAE5F,OAAK,QAAQ,OAAO;EACpB,IAAI,WAAsC;AAE1C,OAAK,MAAM,QAAQ,MAAM;AACvB,OAAI,CAACA,WAAS,KAAK,IAAI,OAAO,KAAK,OAAO,SAAU;GACpD,MAAM,YAAuB;IAC3B,IAAI,KAAK;IACT,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;IACrD,KAAK,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;IAChD;AACD,QAAK,QAAQ,IAAI,KAAK,IAAI,UAAU;AAGpC,OACE,aAAa,QACb,KAAK,SAAS,UACd,OAAO,KAAK,yBAAyB,YACrC,CAAC,qBAAqB,KAAK,IAAI,CAE/B,YAAW;;AAIf,SAAO;GAAE;GAAU,KAAK,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC;GAAE;;CAGtD,cAA2B;AACzB,SAAO,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC;;;;;;;;CASnC,MAAM,gBAA+B;AACnC,MAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAAM;AACtD,MAAI,KAAK,gBAAiB,QAAO,KAAK;AACtC,OAAK,kBAAkB,KAAK,kBAAkB,CAAC,cAAc;AAC3D,QAAK,kBAAkB;IACvB;AACF,SAAO,KAAK;;CAGd,MAAc,mBAAkC;EAC9C,MAAM,EAAE,aAAa,MAAM,KAAK,cAAc;AAC9C,MAAI,CAAC,SACH,OAAM,IAAI,MACR,oLAGD;EAIH,MAAM,QAAQ,SAAS;EACvB,MAAM,KAAK,IAAI,UAAU,MAAM;AAC/B,OAAK,KAAK;AAEV,QAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,MAAG,KAAK,cAAc,SAAS,CAAC;AAChC,MAAG,KAAK,UAAU,QAAe,OAAO,IAAI,CAAC;IAC7C;AAEF,KAAG,GAAG,YAAY,SAA4B,KAAK,cAAc,KAAK,UAAU,CAAC,CAAC;AAGlF,OAAK,kBAAkB,iBAAiB;AACxC,OAAK,kBAAkB,iBAAiB;AACxC,OAAK,kBAAkB,aAAa;AACpC,OAAK,kBAAkB,cAAc;;;CAIvC,kBAA0B,QAAgB,SAAkC,EAAE,EAAQ;AACpF,MAAI,CAAC,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAAM;EACvD,MAAM,KAAK,KAAK;AAChB,OAAK,GAAG,KAAK,KAAK,UAAU;GAAE;GAAI;GAAQ;GAAQ,CAAC,CAAC;;;;;;CAOtD,KACE,QACA,QACqC;AACrC,SAAO,KAAK,YAAY,QAAS,UAAU,EAAE,CAA6B;;;;;;CAS5E,YAAY,QAAgB,SAAkC,EAAE,EAAoB;AAClF,MAAI,CAAC,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAC/C,QAAO,QAAQ,uBACb,IAAI,MACF,kIAED,CACF;EAEH,MAAM,KAAK,KAAK;EAChB,MAAM,KAAK,KAAK;AAChB,SAAO,IAAI,SAAkB,SAAS,WAAW;AAC/C,QAAK,QAAQ,IAAI,IAAI;IAAE;IAAS;IAAQ,CAAC;AACzC,MAAG,KAAK,KAAK,UAAU;IAAE;IAAI;IAAQ;IAAQ,CAAC,CAAC;IAC/C;;CAGJ,cAAsB,KAAmB;EACvC,MAAM,UAAU,aAAa,IAAI;AACjC,MAAI,CAAC,QAAS;AAGd,MAAI,OAAO,QAAQ,OAAO,YAAY,KAAK,QAAQ,IAAI,QAAQ,GAAG,EAAE;GAClE,MAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ,GAAG;AAC3C,QAAK,QAAQ,OAAO,QAAQ,GAAG;AAC/B,OAAI,OACF,KAAI,QAAQ,MAAO,QAAO,OAAO,IAAI,MAAM,QAAQ,MAAM,QAAQ,CAAC;OAC7D,QAAO,QAAQ,QAAQ,OAAO;AAErC;;AAIF,MAAI,OAAO,QAAQ,WAAW,SAAU;AACxC,MAAI,CAAC,KAAK,QAAQ,IAAI,QAAQ,OAAuB,CAAE;EACvD,MAAM,QAAQ,QAAQ;EACtB,MAAM,SAAS,KAAK,QAAQ,IAAI,MAAM;AACtC,MAAI,CAAC,OAAQ;AACb,SAAO,KAAK,QAAQ,OAAO;AAC3B,MAAI,OAAO,SAAS,KAAK,WAAY,QAAO,OAAO;AACnD,OAAK,QAAQ,KAAK,OAAO,QAAQ,OAAO;;CAG1C,kBAA0C,OAAyC;AAEjF,SADe,KAAK,QAAQ,IAAI,MAAM,IACpB,EAAE;;CAGtB,GAA2B,OAAU,UAAyD;AAC5F,OAAK,QAAQ,GAAG,OAAO,SAAuC;AAC9D,eAAa,KAAK,QAAQ,IAAI,OAAO,SAAuC;;;CAI9E,QAAc;AACZ,OAAK,IAAI,OAAO;AAChB,OAAK,KAAK;AACV,OAAK,MAAM,UAAU,KAAK,QAAQ,QAAQ,CACxC,QAAO,uBAAO,IAAI,MAAM,wCAAwC,CAAC;AAEnE,OAAK,QAAQ,OAAO;;;;AAKxB,SAAS,qBAAqB,KAAsB;AAClD,QACE,QAAQ,MACR,QAAQ,iBACR,QAAQ,kBACR,IAAI,WAAW,cAAc,IAC7B,IAAI,WAAW,YAAY,IAC3B,IAAI,WAAW,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjQzC,SAAgB,eAAgC;AAC9C,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAS,IAAI,cAAc;AACjC,SAAO,OAAO,GAAG,mBAAmB;GAClC,MAAM,OAAO,OAAO,SAAS;GAC7B,MAAM,OAAO,OAAO,SAAS,YAAY,SAAS,OAAO,KAAK,OAAO;AACrE,UAAO,YAAY;AACjB,QAAI,SAAS,KACX,wBAAO,IAAI,MAAM,iDAAiD,CAAC;QAEnE,SAAQ,KAAK;KAEf;IACF;AACF,SAAO,KAAK,SAAS,OAAO;GAC5B;;;;;;AAOJ,SAAgB,uBAAiC;CAC/C,MAAM,KAAK,UAAU;AACrB,KAAI,OAAO,SACT,QAAO;EACL;EACA;EACA;EACA;EACD;AAEH,KAAI,OAAO,QACT,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACD;AAEH,KAAI,OAAO,SAAS;EAClB,MAAM,eAAe,QAAQ,IAAI,gBAAgB;EACjD,MAAM,kBAAkB,QAAQ,IAAI,wBAAwB;AAC5D,SAAO;GACL,GAAG,aAAa;GAChB,GAAG,gBAAgB;GACnB,GAAG,aAAa;GACjB;;AAEH,QAAO,EAAE;;;AAIX,SAAgB,mBAAkC;AAChD,MAAK,MAAM,KAAK,sBAAsB,CACpC,KAAI,WAAW,EAAE,CAAE,QAAO;AAE5B,QAAO;;;;;;;;;AAUT,eAAsB,eAAe,UAAiC,EAAE,EAA2B;CACjG,MAAM,YAAY,QAAQ,WAAW;CAGrC,MAAM,gBAAgB,QAAQ,QAAQ;CACtC,MAAM,OAAO,kBAAkB,IAAI,MAAM,cAAc,GAAG;CAE1D,MAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,oBAAoB;CAEjE,MAAM,SAAS,kBAAkB;AACjC,KAAI,WAAW,KACb,OAAM,IAAI,MACR,gHAGE,sBAAsB,CAAC,KAAK,KAAK,CACpC;CAcH,MAAM,QAAsB,UAAU,QAXzB;EACX,2BAA2B;EAC3B;EACA;EAGA;EACA,GAAI,QAAQ,aAAa,EAAE;EAC3B;EACD,EAEmD;EAElD,OAAO;EACP,UAAU;EACX,CAAC;AAGF,OAAM,OAAO;CAEb,MAAM,cAAc,oBAAoB;AAExC,SAAQ,OAAO,MACb,wCAAwC,OAAO,oCACV,YAAY,+BACjB,OAAO,IACxC;AAED,QAAO;EACL;EACA;EACA,OAAa;AACX,OAAI;AACF,UAAM,MAAM;WACN;;EAIX;;;;AC9LH,MAAa,KAAgC;CAE3C,eAAe;CACf,sBAAsB;CACtB,eAAe;CACf,qBAAqB;CACrB,sBAAsB;CACtB,8BAA8B;CAC9B,kBAAkB;CAGlB,iBAAiB;CACjB,qBAAqB;CACrB,sBAAsB;CACtB,yBAAyB;CACzB,2BAA2B;CAC3B,sBAAsB;CACtB,oBAAoB;CACpB,iBAAiB;CACjB,iBAAiB;CACjB,oBAAoB;CACpB,uBAAuB;CACvB,qBAAqB;CAGrB,mBAAmB;CAGnB,uBAAuB;CACvB,sBACE;CACF,2BAA2B;CAC3B,wBAAwB;CACxB,sBAAsB;CAGtB,wBAAwB;CACxB,cAAc;CACd,sBAAsB;CACtB,uBAAuB;CACvB,kBAAkB;CAClB,uBAAuB;CACvB,yBAAyB;CACzB,wBAAwB;CACxB,wBAAwB;CACxB,2BAA2B;CAC3B,0BAA0B;CAC1B,2BAA2B;CAC3B,mCAAmC;CACnC,qCAAqC;CACrC,sCAAsC;CACtC,4BACE;CAGF,yBAAyB;CAEzB,uBAAuB;CACvB,sBAAsB;CACtB,uBAAuB;CACvB,0BAA0B;CAC1B,2BAA2B;CAC3B,wBAAwB;CAExB,qBAAqB;CACrB,oBAAoB;CACpB,qBAAqB;CACrB,wBAAwB;CACxB,yBAAyB;CACzB,6BAA6B;CAC7B,8BAA8B;CAC9B,iCAAiC;CACjC,2BAA2B;CAC3B,0BAA0B;CAC1B,yBAAyB;CACzB,mCAAmC;CACnC,8BAA8B;CAC9B,6BAA6B;CAG7B,wBAAwB;CACxB,oBAAoB;CACpB,mBAAmB;CACnB,mBAAmB;CAGnB,8BAA8B;CAG9B,4BAA4B;CAC5B,yBAAyB;CACzB,0BAA0B;CAC1B,yBAAyB;CAGzB,wBAAwB;CACxB,qBAAqB;CACrB,qBAAqB;CACrB,uBAAuB;CACvB,sBAAsB;CACtB,wBAAwB;CACxB,6BAA6B;CAC7B,kBAAkB;CAClB,0BAA0B;CAC1B,oBAAoB;CACpB,8BAA8B;CAC9B,8BAA8B;CAC9B,gCAAgC;CAChC,sCAAsC;CACtC,+BAA+B;CAC/B,2BAA2B;CAC3B,2BAA2B;CAC3B,sBAAsB;CACtB,wBAAwB;CAGxB,yBAAyB;CACzB,0BAA0B;CAC1B,yBAAyB;CACzB,yBAAyB;CAGzB,2BAA2B;CAC3B,uBAAuB;CACvB,4BAA4B;CAC5B,0BAA0B;CAC1B,2BAA2B;CAC3B,sBAAsB;CACtB,uBAAuB;CACvB,+BAA+B;CAC/B,0BAA0B;CAC1B,8BAA8B;CAC9B,2BAA2B;CAC3B,gCAAgC;CAChC,+BAA+B;CAC/B,4BAA4B;CAC5B,6BAA6B;CAC7B,kCAAkC;CAClC,mCAAmC;CAGnC,yBAAyB;CACzB,sBAAsB;CACtB,uBAAuB;CACvB,yBAAyB;CACzB,uBAAuB;CACvB,qBAAqB;CACrB,yBAAyB;CACzB,uBAAuB;CACvB,oBAAoB;CACpB,qBAAqB;CAGrB,6BAA6B;CAC7B,0BAA0B;CAC1B,0BAA0B;CAC1B,wBAAwB;CACxB,uBAAuB;CACvB,kCAAkC;CAGlC,yBAAyB;CACzB,uBAAuB;CACvB,2BAA2B;CAC3B,6BAA6B;CAC7B,yBAAyB;CAGzB,yBAAyB;CACzB,wBAAwB;CACxB,iBAAiB;CAGjB,2BAA2B;CAC3B,yBAAyB;CACzB,wBAAwB;CACxB,4BAA4B;CAC5B,2BAA2B;CAC3B,qBAAqB;CACrB,uBAAuB;CACvB,sBAAsB;CACtB,uBAAuB;CACvB,yBAAyB;CACzB,wBAAwB;CACxB,0BAA0B;CAG1B,qBAAqB;CACrB,oBAAoB;CACpB,uBAAuB;CACvB,oBAAoB;CACpB,2BAA2B;CAC3B,uBAAuB;CACvB,4BAA4B;CAC5B,gBAAgB;CAChB,gBAAgB;CAChB,6BAA6B;CAC7B,0BAA0B;CAC1B,wBAAwB;CACxB,kBAAkB;CAClB,kBAAkB;CAClB,iBAAiB;CACjB,mBAAmB;CAGnB,+BAA+B;CAC/B,qCAAqC;CACrC,sCAAsC;CACtC,0CAA0C;CAG1C,qBAAqB;CACrB,qBAAqB;CAGrB,mBAAmB;CACnB,qBAAqB;CACrB,4BAA4B;CAC5B,uBAAuB;CACvB,yBAAyB;CACzB,4BAA4B;CAC5B,yBAAyB;CACzB,2BAA2B;CAC3B,yBAAyB;CAGzB,sBAAsB;CACtB,wBAAwB;CAGxB,+BAA+B;CAC/B,4BAA4B;CAC5B,+BAA+B;CAG/B,4BAA4B;CAC5B,6BAA6B;CAI7B,gBAAgB;CAChB,qBAAqB;CACrB,wBAAwB;CACxB,sBAAsB;CACtB,sBAAsB;CAGtB,uBAAuB;CACvB,yBAAyB;CACzB,0BAA0B;CAG1B,wBACE;CACF,wBACE;CACF,wBACE;CACF,mCACE;CACF,gCACE;CACF,2BACE;CACF,2BACE;CAGF,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,gCACE;CACF,6BACE;CACF,0BACE;CACF,0BACE;CAEF,kCACE;CAGF,kBAAkB;CAClB,wBAAwB;CACxB,uBAAuB;CACvB,2BAA2B;CAC3B,oBAAoB;CACpB,oBAAoB;CACpB,qBAAqB;CACrB,wBAAwB;CACxB,4BAA4B;CAC5B,uBAAuB;CACvB,4BAA4B;CAC5B,gCAAgC;CAChC,iCACE;CACF,+BAA+B;CAC/B,sBAAsB;CACtB,oBAAoB;CACpB,mBAAmB;CACnB,8BACE;CACF,6BACE;CAEF,6BAA6B;CAC7B,8BAA8B;CAC9B,iCAAiC;CACjC,sCAAsC;CACtC,kCAAkC;CAClC,0CAA0C;CAC1C,wCAAwC;CAExC,gCAAgC;CAChC,wBAAwB;CACxB,wBAAwB;CACxB,yBAAyB;CACzB,8BAA8B;CAC9B,4BAA4B;CAC5B,gCAAgC;CACjC;;;;;;;;;;;;;;;;;;AEjTD,MAAM,SAA6D;CAAE,IDhBnD;EAEhB,eAAe;EACf,sBAAsB;EACtB,eAAe;EACf,qBAAqB;EACrB,sBAAsB;EACtB,8BAA8B;EAC9B,kBAAkB;EAGlB,iBAAiB;EACjB,qBAAqB;EACrB,sBAAsB;EACtB,yBAAyB;EACzB,2BAA2B;EAC3B,sBAAsB;EACtB,oBAAoB;EACpB,iBAAiB;EACjB,iBAAiB;EACjB,oBAAoB;EACpB,uBAAuB;EACvB,qBAAqB;EAGrB,mBAAmB;EAGnB,uBAAuB;EACvB,sBAAsB;EACtB,2BAA2B;EAC3B,wBAAwB;EACxB,sBAAsB;EAGtB,wBAAwB;EACxB,cAAc;EACd,sBAAsB;EACtB,uBAAuB;EACvB,kBAAkB;EAClB,uBAAuB;EACvB,yBAAyB;EACzB,wBAAwB;EACxB,wBAAwB;EACxB,2BAA2B;EAC3B,0BAA0B;EAC1B,2BAA2B;EAC3B,mCAAmC;EACnC,qCAAqC;EACrC,sCAAsC;EACtC,4BACE;EAGF,yBAAyB;EAEzB,uBAAuB;EACvB,sBAAsB;EACtB,uBAAuB;EACvB,0BAA0B;EAC1B,2BAA2B;EAC3B,wBAAwB;EAExB,qBAAqB;EACrB,oBAAoB;EACpB,qBAAqB;EACrB,wBAAwB;EACxB,yBAAyB;EACzB,6BAA6B;EAC7B,8BAA8B;EAC9B,iCAAiC;EACjC,2BAA2B;EAC3B,0BAA0B;EAC1B,yBAAyB;EACzB,mCAAmC;EACnC,8BAA8B;EAC9B,6BAA6B;EAG7B,wBAAwB;EACxB,oBAAoB;EACpB,mBAAmB;EACnB,mBAAmB;EAGnB,8BAA8B;EAG9B,4BAA4B;EAC5B,yBAAyB;EACzB,0BAA0B;EAC1B,yBAAyB;EAGzB,wBAAwB;EACxB,qBAAqB;EACrB,qBAAqB;EACrB,uBAAuB;EACvB,sBAAsB;EACtB,wBAAwB;EACxB,6BAA6B;EAC7B,kBAAkB;EAClB,0BAA0B;EAC1B,oBAAoB;EACpB,8BAA8B;EAC9B,8BAA8B;EAC9B,gCAAgC;EAChC,sCAAsC;EACtC,+BAA+B;EAC/B,2BAA2B;EAC3B,2BAA2B;EAC3B,sBAAsB;EACtB,wBAAwB;EAGxB,yBAAyB;EACzB,0BAA0B;EAC1B,yBAAyB;EACzB,yBAAyB;EAGzB,2BAA2B;EAC3B,uBAAuB;EACvB,4BAA4B;EAC5B,0BAA0B;EAC1B,2BAA2B;EAC3B,sBAAsB;EACtB,uBAAuB;EACvB,+BAA+B;EAC/B,0BAA0B;EAC1B,8BAA8B;EAC9B,2BAA2B;EAC3B,gCAAgC;EAChC,+BAA+B;EAC/B,4BAA4B;EAC5B,6BAA6B;EAC7B,kCAAkC;EAClC,mCAAmC;EAGnC,yBAAyB;EACzB,sBAAsB;EACtB,uBAAuB;EACvB,yBAAyB;EACzB,uBAAuB;EACvB,qBAAqB;EACrB,yBAAyB;EACzB,uBAAuB;EACvB,oBAAoB;EACpB,qBAAqB;EAGrB,6BAA6B;EAC7B,0BAA0B;EAC1B,0BAA0B;EAC1B,wBAAwB;EACxB,uBAAuB;EACvB,kCAAkC;EAGlC,yBAAyB;EACzB,uBAAuB;EACvB,2BAA2B;EAC3B,6BAA6B;EAC7B,yBAAyB;EAGzB,yBAAyB;EACzB,wBAAwB;EACxB,iBAAiB;EAGjB,2BAA2B;EAC3B,yBAAyB;EACzB,wBAAwB;EACxB,4BACE;EACF,2BAA2B;EAC3B,qBAAqB;EACrB,uBAAuB;EACvB,sBAAsB;EACtB,uBAAuB;EACvB,yBAAyB;EACzB,wBAAwB;EACxB,0BAA0B;EAG1B,qBAAqB;EACrB,oBAAoB;EACpB,uBAAuB;EACvB,oBAAoB;EACpB,2BAA2B;EAC3B,uBAAuB;EACvB,4BAA4B;EAC5B,gBAAgB;EAChB,gBAAgB;EAChB,6BAA6B;EAC7B,0BAA0B;EAC1B,wBAAwB;EACxB,kBAAkB;EAClB,kBAAkB;EAClB,iBAAiB;EACjB,mBAAmB;EAGnB,+BAA+B;EAC/B,qCAAqC;EACrC,sCAAsC;EACtC,0CAA0C;EAG1C,qBAAqB;EACrB,qBAAqB;EAGrB,mBAAmB;EACnB,qBAAqB;EACrB,4BAA4B;EAC5B,uBAAuB;EACvB,yBAAyB;EACzB,4BAA4B;EAC5B,yBAAyB;EACzB,2BAA2B;EAC3B,yBAAyB;EAGzB,sBAAsB;EACtB,wBAAwB;EAGxB,+BAA+B;EAC/B,4BAA4B;EAC5B,+BAA+B;EAG/B,4BAA4B;EAC5B,6BAA6B;EAI7B,gBAAgB;EAChB,qBAAqB;EACrB,wBAAwB;EACxB,sBAAsB;EACtB,sBAAsB;EAGtB,uBAAuB;EACvB,yBAAyB;EACzB,0BAA0B;EAG1B,wBACE;EACF,wBACE;EACF,wBAAwB;EACxB,mCACE;EACF,gCACE;EACF,2BACE;EACF,2BACE;EAGF,uBAAuB;EACvB,uBAAuB;EACvB,uBAAuB;EACvB,uBAAuB;EACvB,gCACE;EACF,6BACE;EACF,0BACE;EACF,0BACE;EAEF,kCACE;EAGF,kBAAkB;EAClB,wBAAwB;EACxB,uBAAuB;EACvB,2BAA2B;EAC3B,oBAAoB;EACpB,oBAAoB;EACpB,qBAAqB;EACrB,wBAAwB;EACxB,4BAA4B;EAC5B,uBAAuB;EACvB,4BAA4B;EAC5B,gCAAgC;EAChC,iCACE;EACF,+BAA+B;EAC/B,sBAAsB;EACtB,oBAAoB;EACpB,mBAAmB;EACnB,8BACE;EACF,6BACE;EAEF,6BAA6B;EAC7B,8BAA8B;EAC9B,iCAAiC;EACjC,sCAAsC;EACtC,kCAAkC;EAClC,0CAA0C;EAC1C,wCAAwC;EAExC,gCAAgC;EAChC,wBAAwB;EACxB,wBAAwB;EACxB,yBAAyB;EACzB,8BAA8B;EAC9B,4BAA4B;EAC5B,gCAAgC;EACjC;CClTwE;CAAI;;;;;;AA6B7E,SAAS,sBAAsB,MAAsB;AACnD,QAAO,SAAS,KAAK,KAAK,GAAG,OAAO;;;;;;;;;;AAoBtC,SAAgB,oBAAoB,QAA2C;AAC7E,KAAI,CAAC,OAAQ,QAAO;AAEpB,QAAO,sBADO,OAAO,MAAM,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,MAAM,IAAI,GACjC;;;;;;;;;;;AAYrC,SAAgB,qBACd,QACoE;CACpE,MAAM,QAAQ,OAAO;AACrB,SAAQ,KAAK,SAAS;EACpB,MAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,IAAI,QAAQ,eAAe,OAAO,SAAiB;GACxD,MAAM,QAAQ,KAAK;AACnB,UAAO,UAAU,KAAA,IAAY,QAAQ,OAAO,MAAM;IAClD;;;;;AC7DN,MAAa,wBACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAa,4BACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,MAAa,2BACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAa,wBACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAa,4BACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,MAAa,2BACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,MAAa,0BAAkD;CAC7D,IAAI;CACJ,IAAI;CACL;;AAGD,MAAa,uBAA2E;CACtF,IAAI;EAAE,SAAS;EAA2B,QAAQ;EAA0B;CAC5E,IAAI;EAAE,SAAS;EAA2B,QAAQ;EAA0B;CAC7E;;;;AClUD,SAAS,oBAAoB,MAAsD;AACjF,QAAO,SAAS,iBAAiB,YAAY;;;;;;;AAQ/C,SAAS,eACP,MACA,GACQ;CACR,IAAI;AACJ,SAAQ,MAAR;EACE,KAAK;AACH,WAAQ,EAAE,sBAAsB;AAChC;EACF,KAAK;AACH,WAAQ,EAAE,wBAAwB;AAClC;EACF,KAAK;AACH,WAAQ,EAAE,yBAAyB;AACnC;EACF,KAAK;EACL,KAAK,KAAA,EACH,QAAO;;AAEX,QAAO,yBAAyB,WAAW,MAAM,CAAC;;;AAsBpD,SAAS,WAAW,GAAmB;AACrC,QAAO,EAAE,QAAQ,aAAa,MAAM,KAAK,EAAE,WAAW,EAAE,CAAC,GAAG;;;;;;;;AAS9D,SAAS,kBACP,MACA,gBACA,QACA,GACQ;CACR,SAAS,aAAa,YAAiC;EACrD,MAAM,IAAI,IAAI,gBAAgB,eAAe;AAC7C,IAAE,IAAI,QAAQ,WAAW;AACzB,SAAO,GAAG,WAAW,KAAK,CAAC,GAAG,EAAE,UAAU;;CAE5C,MAAM,UAAU,WAAW,EAAE,oBAAoB,CAAC;CAClD,MAAM,UAAU,WAAW,EAAE,oBAAoB,CAAC;CAClD,MAAM,UAAU,WAAW,OAAO,WAAW;CAC7C,MAAM,UAAU,WAAW,OAAO,WAAW;AAC7C,QAAO,uCAAuC,aAAa,KAAK,CAAC,WAAW,QAAQ,IAAI,QAAQ,eAAe,aAAa,KAAK,CAAC,WAAW,QAAQ,IAAI,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;AAyBnK,SAAS,mBACP,OACA,WACA,QACA,OAAO,KACP,SAAS,IAAI,iBAAiB,EACtB;CACR,MAAM,IAAI,qBAAqB,OAAO;CACtC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;CAEpC,MAAM,eAAe,MAAM,OAAO,KAAK,EAAE,sBAAsB,GAAG,EAAE,wBAAwB;CAC5F,MAAM,cAAc,MAAM,OAAO,KAAK,cAAc;CAIpD,IAAI;AACJ,KAAI,aAAa,MAAM,WAAW;EAChC,MAAM,gBAAgB,WAAW,MAAM,UAAU;EACjD,MAAM,YAAY,WAAW,EAAE,qBAAqB,CAAC;AACrD,kBACE,wBAAwB,UAAU,2EAEC,cAAc,uEACmB,UAAU,IAAI,UAAU;OAG9F,iBAAgB,mBAAmB,WAAW,EAAE,wBAAwB,CAAC,CAAC;CAW5E,MAAM,gBAAgB,MAAM,QAAQ,MAAM,MAAM,IAAI,MAAM,MAAM,SAAS;CACzE,IAAI;AACJ,KAAI,iBAAiB,MAAM,aAGzB,oBAAmB,uDAFH,WAAW,MAAM,aAAa,CAEoC,8CADpE,WAAW,EAAE,2BAA2B,CAAC,CAC+E;KAGtI,oBAAmB,oDADN,WAAW,EAAE,8BAA8B,CAAC,CACmB;CAM9E,MAAM,eACJ,MAAM,UAAU,OACZ,KACA,yCAAyC,WAAW,EAAE,0BAA0B,CAAC,CAAC,2BAChF,MAAM,MAAM,SAAS,IACjB,MAAM,MACH,KAAK,MAAM;AAGV,SAAO,6BAFQ,WAAW,EAAE,GAAG,CAEY,iCAD3B,WAAW,EAAE,IAAI,MAAM,GAAG,IAAI,CAAC,CACqC;GACpF,CACD,KAAK,KAAK,GACb,qBAAqB,WAAW,EAAE,wBAAwB,CAAC,CAAC,OACjE;CAGP,MAAM,aAA+B;EACnC,UAAU,KAAK,UAAU,EAAE,sBAAsB,CAAC;EAClD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,WAAW,KAAK,UAAU,EAAE,qBAAqB,CAAC;EAClD,aAAa,KAAK,UAAU,EAAE,uBAAuB,CAAC;EACtD,oBAAoB,KAAK,UAAU,EAAE,2BAA2B,CAAC;EACjE,uBAAuB,KAAK,UAAU,EAAE,8BAA8B,CAAC;EACvE,kBAAkB;EACnB;CAED,MAAM,eAAe,kBAAkB,MAAM,QAAQ,QAAQ,EAAE;CAM/D,MAAM,SADS,wBAAwB,QAEpC,WAAW,qBAAqB,aAAa,CAC7C,WAAW,WAAW,WAAW,IAAI,CAAC,CACtC,WAAW,oBAAoB,YAAY,CAC3C,WAAW,qBAAqB,WAAW,aAAa,CAAC,CACzD,WAAW,sBAAsB,cAAc,CAC/C,WAAW,yBAAyB,iBAAiB,CACrD,WAAW,qBAAqB,aAAa;CAKhD,MAAM,YAAY,eAAe,WAAW;AAC5C,QAAO,OAAO,QAAQ,WAAW,GAAG,UAAU,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;AAgD3D,SAAS,eAAe,SAAmC;CACzD,MAAM,cAAc,QAAQ;AAC5B,QAAO;;;wBAGe,QAAQ,SAAS;0BACf,QAAQ,WAAW;0BACnB,QAAQ,WAAW;0BACnB,QAAQ,WAAW;yBACpB,QAAQ,UAAU;2BAChB,QAAQ,YAAY;mCACZ,QAAQ,mBAAmB;sCACxB,QAAQ,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA4FpD,cACI;;;;;;;6BAQA;;;;;;;;;;qDAWL;;gBAGC,cACI,mEACA;8EAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDf,SAAS,gBACP,WACA,WACA,eACA,QACA,OAAO,WACP,SAAS,IAAI,iBAAiB,EAC9B,MACA,gBAAgB,OAChB,qBAAoC,MAC5B;CACR,MAAM,IAAI,qBAAqB,OAAO;CACtC,MAAM,eAAe,kBAAkB,MAAM,QAAQ,QAAQ,EAAE;CAC/D,MAAM,SAAS,oBAAoB,KAAK;CAGxC,MAAM,UAAU,SAAS,eAAe,OAAO,EAAE,iCAAiC,CAAC,SAAS;CAI5F,IAAI;AACJ,KAAI,iBAAiB,mBAGnB,oBAAmB,uDAFH,WAAW,mBAAmB,CAEoC,8CADpE,WAAW,EAAE,2BAA2B,CAAC,CAC+E;KAGtI,oBAAmB,oDADN,WAAW,EAAE,8BAA8B,CAAC,CACmB;CAI9E,MAAM,SADS,qBAAqB,QAAQ,QAEzC,WAAW,qBAAqB,aAAa,CAC7C,WAAW,kBAAkB,eAAe,MAAM,EAAE,CAAC,CACrD,WAAW,gBAAgB,QAAQ,CACnC,WAAW,mBAAmB,UAAU,CACxC,WAAW,kBAAkB,UAAU,CACvC,WAAW,uBAAuB,cAAc,CAChD,WAAW,yBAAyB,iBAAiB;CAkBxD,MAAM,YAAY,eAbmB;EACnC,UAAU,KAAK,UAAU,EAAE,sBAAsB,CAAC;EAClD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,WAAW,KAAK,UAAU,EAAE,qBAAqB,CAAC;EAClD,aAAa,KAAK,UAAU,EAAE,uBAAuB,CAAC;EAEtD,oBAAoB,KAAK,UAAU,EAAE,2BAA2B,CAAC;EACjE,uBAAuB,KAAK,UAAU,EAAE,8BAA8B,CAAC;EAEvE,kBAAkB;EACnB,CAC2C;AAC5C,QAAO,OAAO,QAAQ,WAAW,GAAG,UAAU,WAAW;;;;;;;;;;;AA6C3D,eAAsB,kBACpB,mBACA,SACuB;CACvB,MAAM,EAAE,SAAS,WAAW,MAAM,OAAO;;CAGzC,MAAM,aAA+B,EAAE;;CAGvC,SAAS,kBAAkB,KAAqB,OAA6B;EAC3E,MAAM,UAAU,KAAK,UAAU;GAC7B,QAAQ;IAAE,IAAI,MAAM,OAAO;IAAI,QAAQ,MAAM,OAAO;IAAQ;GAC5D,OAAO,MAAM;GAEb,WAAW,MAAM;GAIjB,cAAc,MAAM,gBAAgB;GACrC,CAAC;AAEF,MAAI,MAAM,SAAS,QAAQ,MAAM;;CAGnC,MAAM,SAAiB,aAAa,OAAO,KAAsB,QAAwB;EAEvF,MAAM,CAAC,MAAM,QAAQ,OADN,IAAI,OAAO,KACQ,MAAM,KAAK,EAAE;EAC/C,MAAM,SAAS,IAAI,gBAAgB,SAAS,GAAG;EAG/C,MAAM,YAAY,OAAO,IAAI,OAAO;EACpC,MAAM,SACJ,cAAc,QAAQ,cAAc,OAChC,YACA,oBAAoB,IAAI,QAAQ,mBAAmB;AAGzD,MAAI,SAAS,KAAK;AAChB,OAAI,CAAC,mBAAmB;AACtB,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,KAAK;AACT;;GAEF,MAAM,QAAQ,mBAAmB;GACjC,IAAI,YAA2B;AAC/B,OAAI,MAAM,UACR,KAAI;AACF,gBAAY,MAAM,OAAO,UAAU,MAAM,WAAW;KAClD,MAAM;KACN,sBAAsB;KACvB,CAAC;WACI;GAIV,MAAM,OAAO,mBAAmB,OAAO,WAAW,QAAQ,MAAM,OAAO;AACvE,OAAI,UAAU,KAAK;IACjB,gBAAgB;IAChB,iBAAiB;IAClB,CAAC;AACF,OAAI,IAAI,KAAK;AACb;;AAIF,MAAI,SAAS,WAAW;AACtB,OAAI,CAAC,mBAAmB;AACtB,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,KAAK;AACT;;AAEF,OAAI,UAAU,KAAK;IACjB,gBAAgB;IAChB,iBAAiB;IACjB,YAAY;IACZ,qBAAqB;IACtB,CAAC;AAGF,qBAAkB,KADG,mBAAmB,CACJ;AAEpC,cAAW,KAAK,IAAI;AAGpB,OAAI,KAAK,eAAe;IACtB,MAAM,MAAM,WAAW,QAAQ,IAAI;AACnC,QAAI,QAAQ,GAAI,YAAW,OAAO,KAAK,EAAE;KACzC;AACF;;AAGF,MAAI,SAAS,WAAW;GACtB,MAAM,WAAW,OAAO,IAAI,IAAI,IAAI;GACpC,IAAI;AACJ,OAAI;AACF,gBAAY,mBAAmB,SAAS;WAClC;AACN,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,iBAAiB;AACzB;;GAIF,IAAI,oBAAoB;AACxB,OAAI;IACF,MAAM,UAAU,UAAU,MAAM,4BAA4B;AAC5D,QAAI,UAAU,GACZ,qBAAoB,mBAAmB,QAAQ,GAAG,CAAC,MAAM,GAAG,GAAG;WAE3D;GAMR,MAAM,eAAe,qBAAqB;GAC1C,MAAM,OAAO,cAAc;GAC3B,MAAM,gBACJ,MAAM,QAAQ,cAAc,MAAM,KAAK,cAAc,MAAM,UAAU,KAAK;GAI5E,MAAM,qCAAoD;AACxD,QAAI,CAAC,SAAS,sBAAuB,QAAO;IAC5C,MAAM,OAAO,OAAO,SAAS;AAC7B,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,WAAO,oBAAoB,KAAK,KAAK;OACnC;AAGJ,UAAO,UAAU,WAAW;IAAE,MAAM;IAAa,sBAAsB;IAAK,CAAC,CAC1E,MAAM,YAAoB;IAGzB,MAAM,OAAO,gBACX,SAHgB,WAAW,kBAAkB,EACzB,WAAW,UAAU,EAKzC,QACA,MACA,QACA,MACA,eACA,4BACD;AACD,QAAI,UAAU,KAAK;KACjB,gBAAgB;KAChB,iBAAiB;KAClB,CAAC;AACF,QAAI,IAAI,KAAK;KACb,CACD,YAAY;AACX,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,iBAAiB;KACzB;AACJ;;AAQF,MAAI,SAAS,cAAc;GACzB,MAAM,wBAAwB,SAAS;AACvC,OAAI,CAAC,uBAAuB;AAC1B,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,2DAA2D;AACnE;;GAGF,MAAM,SAAS,uBAAuB;GACtC,MAAM,IAAI,qBAAqB,OAAO;AACtC,OAAI,CAAC,OAAO,IAAI;IAId,MAAM,OACJ,8BAA8B,OAAO,wEAE/B,WAJI,EADV,OAAO,WAAW,aAAa,6BAA6B,4BACzC,CAIE,CAAC,+CAErB,WAAW,OACR,mCACA,2CACJ;AACF,QAAI,UAAU,KAAK;KACjB,gBAAgB;KAChB,iBAAiB;KAClB,CAAC;AACF,QAAI,IAAI,KAAK;AACb;;AAIF,OAAI,UAAU,KAAK;IACjB,UAAU,OAAO;IACjB,iBAAiB;IAClB,CAAC;AACF,OAAI,KAAK;AACT;;AAGF,MAAI,SAAS,WAAW;GACtB,MAAM,WAAW,OAAO,IAAI,IAAI,IAAI;GACpC,IAAI;AACJ,OAAI;AACF,gBAAY,mBAAmB,SAAS;WAClC;AACN,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,iBAAiB;AACzB;;AAGF,UAAO,SAAS,WAAW;IAAE,MAAM;IAAO,sBAAsB;IAAK,CAAC,CACnE,MAAM,QAAgB;AACrB,QAAI,UAAU,KAAK;KACjB,gBAAgB;KAChB,iBAAiB;KACjB,kBAAkB,OAAO,IAAI,OAAO;KACrC,CAAC;AACF,QAAI,IAAI,IAAI;KACZ,CACD,YAAY;AACX,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,qBAAqB;KAC7B;AACJ;;AAGF,MAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,MAAI,IAAI,YAAY;GACpB;CAEF,MAAM,aAAa,OAAO,QAAQ,IAAI,uBAAuB,EAAE;AAE/D,OAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,SAAO,OAAO,YAAY,mBAAmB,SAAS,CAAC;AACvD,SAAO,KAAK,SAAS,OAAO;GAC5B;CAEF,MAAM,UAAU,OAAO,SAAS;AAChC,KAAI,CAAC,WAAW,OAAO,YAAY,SACjC,OAAM,IAAI,MAAM,mDAAmD;CAErE,MAAM,OAAO,QAAQ;;CAGrB,SAAS,4BAAkC;AACzC,MAAI,CAAC,kBAAmB;EACxB,MAAM,QAAQ,mBAAmB;AACjC,OAAK,MAAM,UAAU,WACnB,KAAI;AACF,qBAAkB,QAAQ,MAAM;UAC1B;;CASZ,MAAM,oBAAoB,SAAS,wBAAwB;CAC3D,MAAM,gBAAgB,kBAAkB;AACtC,MAAI,WAAW,SAAS,KAAK,kBAC3B,4BAA2B;IAE5B,kBAAkB,CAAC,OAAO;AAE7B,QAAO;EACL;EACA,mBAAmB,YAA4B;AAM7C,UAAO,oBAAoB,KAAK;;EAIlC,IAAI,qBAA6B;AAC/B,UAAO,oBAAoB,KAAK;;EAElC,oBAA0B;AACxB,8BAA2B;;EAE7B,QAAuB;AACrB,iBAAc,cAAc;AAC5B,UAAO,IAAI,SAAS,SAAS,WAAW;AACtC,WAAO,OAAO,QAAS,MAAM,OAAO,IAAI,GAAG,SAAS,CAAE;KACtD;;EAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACz0BH,IAAa,0BAAb,cAA6C,MAAM;;CAEjD;;CAEA;;CAEA;CAEA,YAAY,aAAqB,gBAA+B,mBAA2B;EACzF,MAAM,UACJ,kBAAkB,OACd,gBAAgB,eAAe,MAC/B;AAEN,QACE,0CAA0C,YAAY,QACpD,UACA;;QAES,cAAc,CAAC,GAC3B;AACD,OAAK,OAAO;AACZ,OAAK,cAAc;AACnB,OAAK,iBAAiB;AACtB,OAAK,oBAAoB;;;;AAS7B,SAAgB,eAAuB;AAErC,QAAO,KADK,QAAQ,IAAI,yBAAyB,KAAK,SAAS,EAAE,gBAAgB,EAChE,cAAc;;AAGjC,SAAS,cAAc,UAAwB;AAE7C,WADY,KAAK,UAAU,KAAK,EACjB,EAAE,WAAW,MAAM,CAAC;;;;;;;;AAarC,MAAa,aAAuCC;AAMpD,SAAS,SAAS,UAAmC;AACnD,KAAI,CAAC,WAAW,SAAS,CAAE,QAAO;AAClC,KAAI;EACF,MAAM,MAAM,aAAa,UAAU,OAAO;EAC1C,MAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,MACE,OAAO,WAAW,YAClB,WAAW,QACX,SAAS,UACT,OAAQ,OAAmC,QAAQ,YACnD,eAAe,UACf,OAAQ,OAAmC,cAAc,UACzD;GACA,MAAM,IAAI;GAGV,MAAM,iBAAiB,OAAO,EAAE,mBAAmB,WAAW,EAAE,iBAAiB;AACjF,UAAO;IACL,KAAK,EAAE;IACP,QAAQ,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;IAClD,WAAW,EAAE;IACb;IACD;;AAGH,SAAO;SACD;AAEN,SAAO;;;AAIX,SAAS,UAAU,UAAkB,MAAsB;AACzD,eAAc,SAAS;AACvB,eAAc,UAAU,KAAK,UAAU,MAAM,MAAM,EAAE,EAAE,EAAE,UAAU,QAAQ,CAAC;;AAG9E,SAAS,WAAW,UAAwB;AAC1C,KAAI;AACF,SAAO,SAAS;SACV;;;;;;;;;;AAiBV,SAAS,YAAY,KAAa,UAAU,KAAa;AACvD,KAAI;AACF,UAAQ,KAAK,KAAK,UAAU;SACtB;AAEN;;CAGF,MAAM,WAAW,KAAK,KAAK,GAAG;AAE9B,QAAO,WAAW,IAAI,IAAI,KAAK,KAAK,GAAG,UAAU;EAE/C,MAAM,MAAM,KAAK,KAAK,GAAG;AACzB,SAAO,KAAK,KAAK,GAAG;;AAKtB,KAAI,WAAW,IAAI,CACjB,KAAI;AACF,UAAQ,KAAK,KAAK,UAAU;SACtB;;;;;;;;AAgBZ,SAAgB,iBAAkC;AAChD,QAAO,SAAS,cAAc,CAAC;;;;;;;;;;;;;;;AA2BjC,SAAgB,YAAY,UAA8B,EAAE,EAAc;CACxE,MAAM,EAAE,QAAQ,UAAU;CAC1B,MAAM,WAAW,cAAc;CAC/B,MAAM,WAAW,SAAS,SAAS;AAEnC,KAAI,aAAa,KACf,KAAI,WAAW,SAAS,IAAI,EAAE;EAK5B,MAAM,iBAAiB,SAAS;AAGhC,MAFwB,OAAO,mBAAmB,YAAY,CAAC,WAAW,eAAe,CAGvF,SAAQ,OAAO,MACb,sCAAsC,SAAS,IAAI,8BAA8B,eAAe,+BACjG;WAEQ,OAAO;AAEhB,WAAQ,OAAO,MACb,yDAAyD,SAAS,IAAI,MACvE;AACD,eAAY,SAAS,IAAI;AACzB,WAAQ,OAAO,MAAM,4BAA4B,SAAS,IAAI,0BAA0B;SACnF;GAIL,MAAM,UACJ,SAAS,UAAU,OAAO,UAAU,SAAS,WAAW;AAC1D,WAAQ,OAAO,MACb,+CAA+C,SAAS,IAAI,YAAY,SAAS,UAAU,IAAI,QAAQ,2BAC3E,SAAS,IAAI,uDAC1C;AACD,SAAM,IAAI,wBAAwB,SAAS,KAAK,SAAS,QAAQ,SAAS,UAAU;;OAItF,SAAQ,OAAO,MACb,mCAAmC,SAAS,IAAI,gCACjD;CAIL,MAAM,OAAiB;EACrB,KAAK,QAAQ;EACb,QAAQ;EACR,4BAAW,IAAI,MAAM,EAAC,aAAa;EACpC;AACD,WAAU,UAAU,KAAK;CAEzB,IAAI,WAAW;AAEf,QAAO;EACL,aAAa,QAAsB;AACjC,OAAI,SAAU;AACd,QAAK,SAAS;AACd,aAAU,UAAU,KAAK;;EAE3B,qBAAqB,KAAmB;AACtC,OAAI,SAAU;AACd,QAAK,iBAAiB;AACtB,aAAU,UAAU,KAAK;;EAE3B,UAAgB;AACd,OAAI,SAAU;AACd,cAAW;AACX,cAAW,SAAS;;EAEvB;;;;AC9RH,SAASC,WAAS,GAA0C;AAC1D,QAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,EAAE;;AAGjE,SAAS,aAAa,MAAyB;AAC7C,KAAI;AACF,SAAO,KAAK,UAAU,KAAK;SACrB;AACN,SAAO,OAAO,KAAK;;;;;;;;;;;AAgBvB,MAAM,aAA6B;CAIjC;EACE,MAAM;EACN,aAAa,MAAM;GACjB,MAAM,MAAM,KAAK;AACjB,OAAI,CAACA,WAAS,IAAI,CAChB,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;GAEH,MAAM,OAAO,IAAI;AACjB,OAAI,SAAS,cAAc,SAAS,YAClC,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;AAEH,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,MAAM;GACjB,MAAM,MAAM,KAAK;AACjB,OAAI,CAACA,WAAS,IAAI,IAAI,OAAO,IAAI,cAAc,UAC7C,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;AAEH,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,MAAM;GACjB,MAAM,MAAM,KAAK;AACjB,OAAI,CAACA,WAAS,IAAI,IAAI,OAAO,IAAI,YAAY,UAC3C,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;AAEH,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,MAAM;GACjB,MAAM,MAAM,KAAK;AACjB,OAAI,CAACA,WAAS,IAAI,IAAI,OAAO,IAAI,YAAY,UAC3C,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;AAEH,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAMD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CACF;AAMD,MAAM,gBAAgB,IAAI,IAA0B,WAAW,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;;AAGvF,MAAM,qCAAqB,IAAI,KAAa;;;;;AAM5C,SAAgB,gBAAgB,MAAwC;AACtE,QAAO,cAAc,IAAI,KAAK;;;;;;AAOhC,SAAgB,gBAAgB,MAAoB;AAClD,KAAI,mBAAmB,IAAI,KAAK,CAAE;AAClC,oBAAmB,IAAI,KAAK;AAC5B,SAAQ,OAAO,MAAM,0BAA0B,KAAK,iCAAiC;;AAczB,WAAW,KAAK,MAAM,EAAE,KAAK;;;;ACnM3F,MAAa,yBAAyB;CACpC;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAiBF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAgCF,aAAa;GACX,MAAM;GACN,YAAY;IACV,YAAY;KACV,MAAM;KACN,aACE;KAIH;IACD,iBAAiB;KACf,MAAM;KACN,aACE;KAGH;IACD,sBAAsB;KACpB,MAAM;KACN,aACE;KAGH;IACD,aAAa;KACX,MAAM;KACN,aACE;KAIH;IACD,WAAW;KACT,MAAM;KACN,aACE;KAMH;IACF;GAGD,UAAU,EAAE;GACb;EAGD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAIF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAYF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAWF,aAAa;GACX,MAAM;GACN,YAAY;IACV,YAAY;KACV,MAAM;KACN,aAAa;KACd;IACD,SAAS;KACP,MAAM;KACN,aACE;KAIH;IACF;GACD,UAAU,CAAC,aAAa;GACzB;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAMF,aAAa;GACX,MAAM;GACN,YAAY,EACV,OAAO;IACL,MAAM;IACN,aAAa;IACd,EACF;GACD,UAAU,EAAE;GACb;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EA+BF,aAAa;GACX,MAAM;GACN,YAAY;IACV,MAAM;KACJ,MAAM;KACN,aAAa;KACd;IACD,MAAM;KACJ,MAAM;KACN,aAAa;KACb,OAAO,EAAE;KACV;IACD,SAAS;KACP,MAAM;KACN,aACE;KAIH;IACF;GACD,UAAU,CAAC,OAAO;GACnB;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAEF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EA0CF,aAAa;GACX,MAAM;GACN,YAAY;IACV,MAAM;KACJ,MAAM;KACN,MAAM;MAAC;MAAiB;MAAiB;MAAiB;MAAa;KACvE,aACE;KACH;IACD,SAAS;KACP,MAAM;KACN,aACE;KAEH;IACD,aAAa;KACX,MAAM;KACN,aACE;KAIH;IACF;GACD,UAAU,CAAC,OAAO;GACnB;EAGD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAmBF,aAAa;GACX,MAAM;GACN,YAAY,EACV,qBAAqB;IACnB,MAAM;IACN,aACE;IACH,EACF;GACD,UAAU,EAAE;GACb;EACD,aAAa;EACd;CACF;AAID,MAAM,mBAAmB,IAAI,IAAY,uBAAuB,KAAK,MAAM,EAAE,KAAK,CAAC;AAEnF,SAAgB,gBAAgB,MAAqC;AACnE,QAAO,iBAAiB,IAAI,KAAK;;;;;;;;AASnC,SAAgB,oBAAoB,MAA4C;AAC9E,MAAK,MAAM,KAAK,uBACd,KAAI,EAAE,SAAS,KAAM,QAAO,EAAE;;;;;;;;;;AAalC,SAAgB,kBAAkB,MAAc,KAA8B;CAC5E,MAAM,eAAe,oBAAoB,KAAK;AAC9C,KAAI,iBAAiB,KAAA,EAAW,QAAO;AACvC,KAAI,iBAAiB,OAAQ,QAAO;AACpC,KAAI,iBAAiB,QAAS,QAAO,WAAW,IAAI;AACpD,QAAO,iBAAiB;;;;;;;;;;AAW1B,SAAgB,yBACd,OACA,KACK;AACL,QAAO,MAAM,QACV,MACC,EAAE,gBAAgB,UACjB,EAAE,gBAAgB,WAAW,WAAW,IAAI,IAC7C,EAAE,gBAAgB,IACrB;;;;;;;;;;;AAYH,MAAa,uBAA4C,IAAI,IAAY;CACvE;CACA;CACA;CAGA;CACD,CAAC;;AAyBF,SAAS,mBAAmB,KAA8B;AACxD,KAAI,IAAI,UAAU,KAAA,GAAW;AAC3B,MAAI,OAAO,IAAI,UAAU,SAAU,QAAO,IAAI;AAC9C,MAAI;AACF,UAAO,KAAK,UAAU,IAAI,MAAM;UAC1B;AACN,UAAO,OAAO,IAAI,MAAM;;;AAG5B,KAAI,IAAI,gBAAgB,KAAA,EAAW,QAAO,IAAI;AAC9C,KAAI,IAAI,cAAc,KAAA,EAAW,QAAO,IAAI;AAC5C,QAAO,IAAI,WAAW,IAAI;;AAG5B,SAAgB,wBAAwB,OAA8C;CACpF,MAAM,OAAO,MAAM,KAAK,IAAI,mBAAmB;AAC/C,QAAO;EACL,OAAO,MAAM;EACb,MAAM,KAAK,KAAK,IAAI;EACpB,WAAW,MAAM;EACjB;EACD;;AAGH,SAAgB,oBAAoB,YAA6C;AAC/E,QAAO,WACJ,kBAAkB,2BAA2B,CAC7C,KAAK,UAAU,wBAAwB,MAAM,CAAC;;AAGnD,SAAgB,oBAAoB,YAA6C;CAC/E,MAAM,WAAW,WAAW,kBAAkB,4BAA4B;CAC1E,MAAM,YAAY,WAAW,kBAAkB,2BAA2B;CAE1E,MAAM,sCAAsB,IAAI,KAA2C;AAC3E,MAAK,MAAM,YAAY,UACrB,qBAAoB,IAAI,SAAS,WAAW,SAAS;AAGvD,QAAO,SAAS,KAAK,YAA2C;EAC9D,MAAM,WAAW,oBAAoB,IAAI,QAAQ,UAAU;AAC3D,SAAO;GACL,WAAW,QAAQ;GACnB,KAAK,QAAQ,QAAQ;GACrB,QAAQ,QAAQ,QAAQ;GACxB,QAAQ,WAAW,SAAS,SAAS,SAAS;GAC9C,YAAY,WAAW,SAAS,SAAS,aAAa;GACtD,WAAW,QAAQ;GACnB,SAAS,WAAW,SAAS,YAAY;GAC1C;GACD;;;AAqCJ,SAAS,gBAAgB,OAA6B;AAEpD,QAAO,MADI,MAAM,gBAAgB,cACjB,IAAI,MAAM,IAAI,GAAG,MAAM,WAAW,GAAG,MAAM,aAAa;;;AAI1E,SAAgB,mBAAmB,OAAuD;CACxF,MAAM,EAAE,WAAW,qBAAqB;CACxC,MAAM,SAAS,iBAAiB,YAAY;CAC5C,MAAM,QAAQ,UAAU,OAAO,SAAS,IAAI,OAAO,IAAI,gBAAgB,CAAC,KAAK,KAAK,GAAG,KAAA;CACrF,MAAM,gBAAgB,iBAAiB,WAAW,eAAe,KAAA;CAEjE,MAAM,SAA4B;EAChC;EACA,MAAM,iBAAiB;EACvB,KAAK;EACN;AACD,KAAI,iBAAiB,QAAQ,KAAA,EAAW,QAAO,MAAM,iBAAiB;AACtE,KAAI,iBAAiB,eAAe,KAAA,EAAW,QAAO,aAAa,iBAAiB;AACpF,KAAI,iBAAiB,iBAAiB,KAAA,EACpC,QAAO,eAAe,iBAAiB;AACzC,KAAI,kBAAkB,KAAA,EAAW,QAAO,gBAAgB;AACxD,KAAI,UAAU,KAAA,EAAW,QAAO,QAAQ;AACxC,QAAO;;;;;;AAOT,SAAgB,eAAe,YAA2B,QAAQ,IAAyB;CACzF,MAAM,MAAM,KAAK,IAAI,KAAK,IAAI,GAAG,MAAM,EAAE,GAAG;CAC5C,MAAM,SAAS,WAAW,kBAAkB,0BAA0B;AAGtE,SADe,OAAO,SAAS,MAAM,OAAO,MAAM,OAAO,SAAS,IAAI,GAAG,QAC3D,KAAK,MAAM,mBAAmB,EAAE,CAAC;;AA8CjD,SAAS,aAAa,MAAsD;AAC1E,QACE,OAAQ,KAAiC,2BAA2B,cACpE,OAAQ,KAAiC,wBAAwB;;AAIrE,SAAgB,UAAU,YAA2B,QAAuC;CAE1F,MAAM,QADa,WAAW,aAAa,CACA,KAAK,MAAM;EACpD,MAAM,aAAa,aAAa,WAAW,GAAG,WAAW,oBAAoB,EAAE,GAAG,GAAG;AACrF,SAAO;GACL,IAAI,EAAE;GACN,OAAO,EAAE;GACT,KAAK,EAAE;GACP,YAAY,eAAe,OAAO,IAAI,KAAK,WAAW,CAAC,aAAa,GAAG;GACxE;GACD;CAEF,MAAM,UAAU,aAAa,WAAW,GAAG,WAAW,wBAAwB,GAAG;CACjF,MAAM,kBAAkB,YAAY,OAAO,IAAI,KAAK,QAAQ,CAAC,aAAa,GAAG;AAK7E,QAAO;EAAE;EAAO;EAAQ;EAAiB,cAJpB,kBACjB,oDAAoD,gBAAgB,KACpE;EAEmD,mBAAmB;EAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DlF,SAAgB,eACd,WACA,QACA,YACsB;AACtB,KAAI,CAAC,OAAO,MAAM,OAAO,WAAW,KAClC,OAAM,IAAI,MACR,0FAED;CAEH,MAAM,mBAAmB,wBAAwB,UAAU,IAAI,KAAA;CAI/D,IAAI;CACJ,IAAI;AACJ,KAAI,eAAe,KAAA,KAAa,eAAe,IAAI;EACjD,MAAM,MAAM,KAAK,KAAK;AACtB,aAAW,aAAa,YAAY,IAAI;EACxC,MAAM,eAAe;EAQrB,MAAM,cAAc,MAAA,IAAgC,eAAe;AACnE,aAAW;GACT,SAAS;GACT,YAAA,IAAsC;GACtC,WAAW,IAAI,KAAK,YAAY,CAAC,aAAa;GAC/C;;AAGH,QAAO;EACL,WAAW,uBAAuB,WAAW,OAAO,QAAQ,SAAS;EACrE,UAAU,OAAO;EACjB,GAAI,qBAAqB,KAAA,IAAY,EAAE,kBAAkB,GAAG,EAAE;EAC9D,GAAI,aAAa,KAAA,IAAY,EAAE,MAAM,UAAU,GAAG,EAAE;EACrD;;;;;;;;;;;AAgBH,SAAgB,iBAA0B;AACxC,KAAI,QAAQ,IAAI,OAAO,UAAU,QAAQ,IAAI,OAAO,IAAK,QAAO;CAChE,MAAM,WAAW,QAAQ;AACzB,KAAI,aAAa,YAAY,aAAa,QAAS,QAAO;AAC1D,KAAI,aAAa,QACf,QAAO,QAAQ,QAAQ,IAAI,WAAW,QAAQ,IAAI,gBAAgB;AAEpE,QAAO;;;AA6BT,SAAS,qBAAqB,SAAyD;CACrF,MAAM,WAAW,QAAQ;AACzB,KAAI,aAAa,SACf,QAAO;EACL;GAAE,KAAK;GAAQ,MAAM,CAAC,QAAQ;GAAE;EAChC;GAAE,KAAK;GAAQ,MAAM;IAAC;IAAM;IAAU;IAAQ;GAAE;EAChD;GAAE,KAAK;GAAQ,MAAM;IAAC;IAAM;IAAiB;IAAQ;GAAE;EACvD;GAAE,KAAK;GAAQ,MAAM;IAAC;IAAM;IAAW;IAAQ;GAAE;EAClD;AAEH,KAAI,aAAa,QACf,QAAO,CACL;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAQ;EAAE,EAClD;EAAE,KAAK;EAAY,MAAM,CAAC,+BAA+B,QAAQ;EAAE,CACpE;AAGH,QAAO;EACL;GAAE,KAAK;GAAY,MAAM,CAAC,QAAQ;GAAE;EACpC;GAAE,KAAK;GAAoB,MAAM,CAAC,QAAQ;GAAE;EAC5C;GAAE,KAAK;GAAiB,MAAM,CAAC,QAAQ;GAAE;EACzC;GAAE,KAAK;GAAW,MAAM,CAAC,QAAQ;GAAE;EACnC;GAAE,KAAK;GAAiB,MAAM,CAAC,QAAQ;GAAE;EACzC;GAAE,KAAK;GAAY,MAAM,CAAC,QAAQ;GAAE;EACrC;;;AAIH,SAAS,cAAc,MAAsB;AAE3C,QAAO,KAAK,QAAQ,qBAAqB,gBAAgB;;;AAI3D,MAAM,0BAA0B;CAC9B;CACA;CACA;CACA;CACA;CACA;CACD;AAED,SAAS,sBAAsB,QAAyB;AACtD,QAAO,wBAAwB,MAAM,MAAM,EAAE,KAAK,OAAO,CAAC;;;;;;;;;;;;;;;;;;AAmB5D,eAAsB,gBACpB,SACA,QACgC;CAChC,MAAM,EAAE,cAAc,MAAM,OAAO;;;;;CAMnC,SAAS,QAAQ,aAAgC;EAC/C,MAAM,aAAa,qBAAqB,QAAQ;AAChD,OAAK,MAAM,EAAE,KAAK,UAAU,YAAY;GACtC,MAAM,SAAS,UAAU,KAAK,MAAM;IAAE,UAAU;IAAQ,SAAS;IAAM,CAAC;AAExE,OAAI,OAAO,OAAO;AAChB,gBAAY,KAAK,GAAG,IAAI,IAAI,OAAO,MAAM,UAAU;AACnD;;GAGF,MAAM,SAAS,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AACnE,OAAI,OACF,aAAY,KAAK,GAAG,IAAI,IAAI,cAAc,OAAO,MAAM,CAAC,GAAG;AAG7D,OAAI,OAAO,WAAW,KAAK,CAAC,sBAAsB,OAAO,CACvD,QAAO;;AAGX,SAAO;;CAGT,MAAM,cAAwB,EAAE;AAGhC,KAAI,QAAQ,YAAY,CACtB,QAAO;EAAE,QAAQ;EAAM;EAAS;EAAQ;AAI1C,KAAI,QAAQ,YAAY,CACtB,QAAO;EAAE,QAAQ;EAAM;EAAS;EAAQ,SAAS;EAAM;AAIzD,QAAO;EACL,QAAQ;EACR;EACA;EACA,OAAO;EACP,eANoB,YAAY,SAAS,IAAI,YAAY,KAAK,KAAK,GAAG,KAAA;EAOvE;;;AAQH,SAAgB,eAAe,YAA0D;AAGvF,QAAO,WAAW,KAAK,mBAAmB;EAAE,OAAO;EAAI,QAAQ;EAAM,CAAC;;;AAIxE,SAAgB,aAAa,YAAuD;AAClF,QAAO,WAAW,KAAK,+BAA+B,EAAE,CAAC;;;AAa3D,eAAsB,eAAe,YAAsD;CACzF,MAAM,EAAE,SAAS,MAAM,WAAW,KAAK,0BAA0B,EAAE,QAAQ,OAAO,CAAC;AACnF,QAAO;EAAE;EAAM,SAAS,yBAAyB;EAAQ,UAAU;EAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmClF,MAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmExC,MAAM;;;;;;;;;;AAuGR,SAAgB,wBACd,UACA,QACqB;AACrB,KAAI,OAAO,aAAa,SACtB,OAAM,IAAI,MACR,sDAAsD,OAAO,SAAS,0BACvE;CAEH,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,SAAS;SACvB;AACN,QAAM,IAAI,MAAM,sDAAsD,WAAW;;AAEnF,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MAAM,oDAAoD;CAEtE,MAAM,MAAM;CAEZ,SAAS,cACP,KACqE;EACrE,MAAM,IAAI,IAAI;AACd,MAAI,MAAM,QAAQ,MAAM,KAAA,EAAW,QAAO;AAC1C,MAAI,OAAO,MAAM,SAAU,QAAO;EAClC,MAAM,IAAI;AACV,SAAO;GACL,KAAK,OAAO,EAAE,QAAQ,WAAW,EAAE,MAAM;GACzC,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;GAC/C,QAAQ,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;GAClD,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;GAC7C;;CAGH,MAAM,SAAS,cAAc,SAAS,IAAI;EAAE,KAAK;EAAG,OAAO;EAAG,QAAQ;EAAG,MAAM;EAAG;CAClF,MAAM,YAAY,cAAc,YAAY;CAC5C,MAAM,kBACJ,IAAI,oBAAoB,kBAAkB,IAAI,oBAAoB,iBAC9D,IAAI,kBACJ;CACN,MAAM,iBAAiB,OAAO,IAAI,mBAAmB,WAAW,IAAI,iBAAiB,KAAA;CACrF,MAAM,eAAe,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;CAC/E,MAAM,qBACJ,OAAO,IAAI,uBAAuB,WAAW,IAAI,qBAAqB;CACxE,MAAM,aAAa,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;CACzE,MAAM,cAAc,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;CAC5E,MAAM,mBAAmB,OAAO,IAAI,qBAAqB,WAAW,IAAI,mBAAmB;CAC3F,MAAM,YAAY,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY;AAEtE,QAAO;EACL;EACA;EACA;EACA;EACA,GAAI,mBAAmB,KAAA,IAAY,EAAE,gBAAgB,GAAG,EAAE;EAC1D;EACA;EACA;EACA;EACA;EACA;EACD;;;;;;;;;;;;;;;AAgBH,eAAsB,gBACpB,YACA,QAC8B;CAC9B,MAAM,SAAS,MAAM,WAAW,KAAK,oBAAoB;EACvD,YAAY;EACZ,eAAe;EACf,cAAc;EACf,CAAC;AACF,KAAI,OAAO,kBAAkB;EAC3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,oCAAoC,MAAM;;AAE5D,QAAO,wBAAwB,OAAO,OAAO,OAAO,OAAO;;;;;;;;;;AAgC7D,eAAsB,SACpB,YACA,YACyB;CACzB,MAAM,SAAS,MAAM,WAAW,KAAK,oBAAoB;EACvD;EACA,eAAe;EACf,cAAc;EACf,CAAC;AACF,KAAI,OAAO,kBAAkB;EAE3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,oBAAoB,MAAM;;AAE5C,QAAO;EAAE,OAAO,OAAO,OAAO;EAAO,MAAM,OAAO,OAAO;EAAM;;;;;;;;;;;;;;AAiCjE,SAAgB,uBAAuB,MAAc,MAAyB;AAG5E,QACE,yOAHe,KAAK,UAAU,KAAK,CAQY,OAPhC,KAAK,UAAU,KAAK,CAO4B;;;;;;;;AAenE,SAAgB,uBAAuB,UAAkC;AACvE,KAAI,OAAO,aAAa,SACtB,OAAM,IAAI,MACR,8CAA8C,OAAO,SAAS,0BAC/D;CAEH,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,SAAS;SACvB;AAEN,QAAM,IAAI,MAAM,4CAA4C;;AAE9D,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MAAM,2CAA2C;CAE7D,MAAM,MAAM;AACZ,KAAI,IAAI,OAAO,KACb,QAAO;EAAE,IAAI;EAAM,OAAO,IAAI;EAAO;AAEvC,KAAI,IAAI,OAAO,MACb,QAAO;EAAE,IAAI;EAAO,OAAO,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ,OAAO,IAAI,MAAM;EAAE;AAE5F,OAAM,IAAI,MAAM,+CAA6C;;;;;;;;;;;;;AAc/D,SAAS,oBACP,YACA,aACA,WAC+B;CAC/B,MAAM,SAAS,WAAW,kBAAkB,0BAA0B;AAEtE,MAAK,IAAI,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;EAC3C,MAAM,IAAI,OAAO;AACjB,MAAI,EAAE,aAAa,eAAe,EAAE,aAAa,UAC/C,QAAO,mBAAmB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;AA4BlC,eAAsB,QACpB,YACA,MACA,MACwB;CAExB,MAAM,YAAY,gBAAgB,KAAK;AACvC,KAAI,cAAc,KAAA,GAAW;EAC3B,MAAM,aAAa,UAAU,aAAa,KAAK;AAC/C,MAAI,CAAC,WAAW,GAOd,QAAO;GAAE,IAAI;GAAO,OAJlB,aAAa,KAAK,sBACX,WAAW,SAAS,QACpB,WAAW,SAAS,YAChB,UAAU;GACe;OAIxC,iBAAgB,KAAK;CAGvB,MAAM,YAAY,KAAK,KAAK;CAC5B,MAAM,aAAa,uBAAuB,MAAM,KAAK;CACrD,MAAM,SAAS,MAAM,WAAW,KAAK,oBAAoB;EACvD;EACA,eAAe;EACf,cAAc;EACf,CAAC;CACF,MAAM,UAAU,KAAK,KAAK;AAE1B,KAAI,OAAO,kBAAkB;EAE3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,mBAAmB,MAAM;;CAG3C,MAAM,YAAY,uBAAuB,OAAO,OAAO,MAAM;CAK7D,MAAM,kBAAkB,oBAAoB,YAAY,YAAY,IAAI,UAAU,IAAI;AAEtF,KAAI,oBAAoB,KAAA,EACtB,QAAO;EAAE,GAAG;EAAW;EAAiB;AAE1C,QAAO;;;AAQT,MAAM,iBAAiB,IAAI,IAAY;CACrC;CACA;CACA;CACD,CAAC;;AAGF,SAAgB,cAAc,MAAuB;AACnD,QAAO,eAAe,IAAI,KAAK;;;AAIjC,SAAgB,kBAAkB,QAA+C;AAC/E,QAAO,OAAO,IAAI,wBAAwB;;;AAI5C,SAAgB,aAAa,QAA0C;AACrE,QAAO,OAAO,IAAI,mBAAmB;;;AAIvC,SAAgB,0BAA0B,QAAuD;AAC/F,QAAO,OAAO,IAAI,gCAAgC;;;AA2MpD,MAAM,yBAA0D;CAE9D,CAAC,qBAAqB,gBAAgB;CAEtC,CAAC,gCAAgC,iBAAiB;CAElD,CAAC,6BAA6B,2BAA2B;CAEzD,CAAC,4BAA4B,4BAA4B;CACzD,CAAC,mBAAmB,oBAAoB;CACzC;;;;;;;;AASD,SAAgB,mBAAmB,SAAyB;CAC1D,IAAI,SAAS;AACb,MAAK,MAAM,CAAC,SAAS,gBAAgB,uBACnC,UAAS,OAAO,QAAQ,SAAS,YAAY;AAE/C,QAAO;;;AAIT,MAAM,4BAA4B;;;;;AAMlC,IAAa,+BAAb,MAA0E;CACxE,SAA8C,EAAE;CAChD;CACA,eAAsC;CACtC,eAAsC;CACtC,kBAA0B;CAC1B,mBAA0C;CAE1C,YAAY,UAAU,2BAA2B;AAC/C,OAAK,UAAU;;CAGjB,YAAY,SAAiB,UAAyB;EACpD,MAAM,QAA0B;GAC9B,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC,SAAS,mBAAmB,QAAQ;GACpC,GAAI,aAAa,KAAA,IAAY,EAAE,UAAU,GAAG,EAAE;GAC/C;AACD,OAAK,OAAO,KAAK,MAAM;AAEvB,MAAI,KAAK,OAAO,SAAS,KAAK,QAC5B,MAAK,OAAO,OAAO;;CAIvB,gBAAgB,OAAmC;EACjD,MAAM,MAAM,KAAK,IAAI,KAAK,IAAI,GAAG,MAAM,EAAE,0BAA0B;AAGnE,SADE,KAAK,OAAO,SAAS,MAAM,KAAK,OAAO,MAAM,KAAK,OAAO,SAAS,IAAI,GAAG,CAAC,GAAG,KAAK,OAAO;;CAI7F,eAAqB;AACnB,OAAK,gCAAe,IAAI,MAAM,EAAC,aAAa;;CAG9C,eAAqB;AACnB,OAAK,gCAAe,IAAI,MAAM,EAAC,aAAa;;CAG9C,kBAAiC;AAC/B,SAAO,KAAK;;CAGd,kBAAiC;AAC/B,SAAO,KAAK;;CAGd,mBAAyB;AACvB,OAAK,mBAAmB;AACxB,OAAK,oCAAmB,IAAI,MAAM,EAAC,aAAa;;CAGlD,iBAAsC;AACpC,SAAO;GAAE,OAAO,KAAK;GAAiB,QAAQ,KAAK;GAAkB;;;;;;;;;;;;;;;;;;;;AAqBzE,eAAsB,oBAA4C;AAG9D,QAAA;;;;;;;AA0BJ,SAAgB,sBAAqC;AASnD,QAAA;;;;;;;;;;;;;;;;;;;;AAqBF,SAAgB,6BACd,QACA,OACA,KACA,cAA0C,MACZ;AAG9B,KAAI,OAAO,aAAa,KACtB,QAAO;EACL,MAAM;EACN,QACE,iCAAiC,OAAO,UAAU,SAAS,OAAO,gBAAgB;EAErF;AAIH,KAAI,CAAC,OAAO,GAIV,KAAI,CAAC,WAAW,IAAI;MAGd,UAAU,QAAQ,MAAM,MAAM,WAAW,KAAK,CAAC,MAAM,gBACvD,QAAO;GACL,MAAM;GACN,QACE;GAEH;OAKH,QAAO;EACL,MAAM;EACN,QAAQ;EACT;AAQL,KAAI,gBAAgB,QAAQ,YAAY,QAAQ,KAAK,UAAU,QAAQ,MAAM,MAAM,WAAW,EAC5F,QAAO;EACL,MAAM;EACN,QACE,qBAAqB,YAAY,MAAM,aAAa,YAAY,UAAU,UAAU;EAGvF;AAIH,KAAI,WAAW,IAAI,IAAI,UAAU,QAAQ,MAAM,MAAM,WAAW,KAAK,CAAC,MAAM,gBAC1E,QAAO;EACL,MAAM;EACN,QAAQ;EACT;AAIH,KAAI,UAAU,QAAQ,MAAM,oBAAoB,KAC9C,QAAO;EACL,MAAM;EACN,QAAQ,mBAAmB,MAAM,gBAAgB;EAClD;AAIH,QAAO;;;;;;;;;;;;AAsDT,eAAsB,eAAe,OAAwD;CAC3F,MAAM,EACJ,QACA,YACA,KACA,WACA,WACA,UAAU,YACV,oBAAoB,IACpB,gBAAgB,mBAChB,yBAAyB,WAAW,QAAQ,KAAK,EACjD,mBACE;CAEJ,MAAM,CAAC,YAAY,mBAAmB,MAAM,QAAQ,IAAI,CACtD,eAAe,EACf,QAAQ,QAAQ,qBAAqB,CAAC,CACvC,CAAC;CAGF,MAAM,WAAW,YAAY;CAC7B,MAAM,mBAAiD,WACnD;EAAE,KAAK,SAAS;EAAK,WAAW,SAAS;EAAW,QAAQ,SAAS;EAAQ,GAC7E;CAWJ,MAAM,0BAA0B,kBAAkB,UAAU,kBAAkB;CAC9E,IAAI,cAAc,OAAO;AACzB,KACE,OAAO,MACP,OAAO,4BAA4B,YACnC,4BAA4B,QAC5B,CAAC,WAAW,wBAAwB,CAEpC,eAAc;CAGhB,MAAM,aAAoC;EACxC,IAAI;EACJ,QAAQ,OAAO;EACf,KAAK,UAAU,OAAO;EACtB,WAAW,UAAU,aAAa;EAClC,WAAW,OAAO,aAAa;EAC/B,iBAAiB,OAAO,mBAAmB;EAC5C;CAOD,IAAI,QAAgC;AACpC,KAAI,eAAe,KAAA,GAAW;AAC5B,MAAI;AACF,SAAM,WAAW,kBAAkB;UAC7B;AAGR,MAAI;AACF,WAAQ,UAAU,YAAY,OAAO;UAC/B;;CAKV,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,GAAG,kBAAkB,EAAE,GAAG;CAC1D,MAAM,eAAe,UAAU,gBAAgB,MAAM;CAMrD,MAAM,cAAc,UAAU,gBAAgB;AAC9C,KAAI,YAAY,QAAQ,EACtB,cAAa,KAAK;EAChB,WAAW,YAAY,2BAAU,IAAI,MAAM,EAAC,aAAa;EACzD,SAAS,6BAA6B,YAAY,MAAM,eAAe,YAAY,UAAU,UAAU;EACvG,UAAU;EACX,CAAC;CAGJ,MAAM,wBAAwB,6BAA6B,YAAY,OAAO,KAAK,YAAY;AAE/F,QAAO;EACL;EACA;EACA,QAAQ;EACR;EACA,cAAc,UAAU,iBAAiB;EACzC,cAAc,UAAU,iBAAiB;EACzC;EACA;EACA,aAAa;GACX,MAAM;GACN,KAAK,YAAY,IAAI;GACrB,QAAQ;GACR,iBAAiB,eAAe,IAAI;GACrC;EACD;EACA,SAAS;GACP,KAAK,QAAQ;GACb,MAAM,QAAQ;GACd,aAAa,kBAAkB;GAChC;EACD;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACpsEH,SAAgB,sBAA8B;AAC5C,QAAO,YAAY,GAAG,CAAC,SAAS,MAAM;;;AA2BxC,eAAe,uBAAsC;CACnD,MAAM,EAAE,eAAe,MAAM,OAAO;AACpC,KAAI,CAAC,WAAW,IAAI,CAClB,OAAM,QAAQ,IAAI;;;;;;;;;;;AAatB,eAAsB,iBAAiB,WAAyC;AAC9E,OAAM,sBAAsB;CAE5B,MAAM,SAAS,OAAO,MAAM,oBAAoB,YAAY;CAE5D,MAAM,MAAM,MAAM,IAAI,SAAiB,SAAS,WAAW;EACzD,MAAM,SAAS,aAAqB;AAClC,YAAS;AACT,WAAQ,SAAS;;EAEnB,MAAM,WAAW,QAAe;AAC9B,YAAS;AACT,UAAO,IAAI;;EAEb,MAAM,UAAU,SAAwB;AACtC,YAAS;AACT,0BAAO,IAAI,MAAM,mDAAmD,KAAK,GAAG,CAAC;;EAE/E,MAAM,gBAAgB;AACpB,UAAO,IAAI,OAAO,MAAM;AACxB,UAAO,IAAI,SAAS,QAAQ;AAC5B,UAAO,IAAI,QAAQ,OAAO;;AAE5B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,KAAK,SAAS,QAAQ;AAC7B,SAAO,KAAK,QAAQ,OAAO;GAC3B;CAKF,IAAI,kBAAkB;CACtB,IAAI,mBAA2D;AAE/D,QAAO,KAAK,SAAS,SAAwB;AAC3C,MAAI,CAAC,mBAAmB,qBAAqB,KAC3C,kBAAiB,KAAK;GAExB;AAEF,QAAO;EACL;EACA,QAAQ,IAAI,QAAQ,UAAU,MAAM;EACpC,UAAW,OAAO,SAAqC;EACvD,iBAAiB,IAAyC;AACxD,sBAAmB;;EAErB,OAAa;AACX,qBAAkB;AAClB,UAAO,MAAM;;EAEhB;;;;;;;;;;;;;;;;;;;AAgCH,eAAsB,SAAS,MAA+B;CAG5D,MAAM,EAAE,SAAS,WAAW,MAAM,OAAO;CACzC,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE,sBAAsB,KAAK,CAAC;CAC7D,MAAM,OAAe,GAAG,QAAQ;CAChC,MAAM,OAAmB,GAAG,QAAQ;CAEpC,MAAM,UAAU,GAAW,MAAuB;AAChD,MAAI,IAAI,KAAK,IAAI,KAAK,KAAK,QAAQ,KAAK,KAAM,QAAO;AACrD,SAAO,KAAK,IAAI,OAAO,OAAO;;CAGhC,MAAM,QAAQ;CACd,MAAM,QAAkB,EAAE;AAC1B,MAAK,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,GAAG;EAC7C,IAAI,OAAO;AACX,OAAK,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK;GAC1C,MAAM,MAAM,OAAO,GAAG,EAAE;GACxB,MAAM,MAAM,OAAO,GAAG,IAAI,EAAE;AAC5B,WAAQ,OAAO,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM;;AAErD,QAAM,KAAK,KAAK;;AAElB,QAAO,GAAG,MAAM,KAAK,KAAK,CAAC;;;;;;;;;;;;AAa7B,eAAsB,mBAAmB,OAA2C;CAIlF,MAAM,KAAK,MAAM,SAAS,MAAM,OAAO;CAEvC,MAAM,WAAW,MAAM,cACnB,+EACA;AAEJ,QAAO;EACL;EACA;EACA;EACA,oBAAoB,MAAM;EAC1B;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK;;;AAId,eAAsB,kBAAkB,OAAyC;CAC/E,MAAM,SAAS,MAAM,mBAAmB,MAAM;AAC9C,SAAQ,OAAO,MAAM,GAAG,OAAO,IAAI;;;;;;;;;;;;;;AAsBrC,eAAsB,YAAY,UAAkB,YAAY,KAA0B;CACxF,MAAM,EAAE,SAAS,UAAU,MAAM,OAAO;AACxC,QAAO,IAAI,SAAkB,YAAY;EACvC,MAAM,MAAM,IAAI,IAAI,SAAS;EAC7B,MAAM,QAAQ,iBAAiB;AAC7B,OAAI,SAAS;AACb,WAAQ,MAAM;KACb,UAAU;EAEb,MAAM,MAAM,MAAM,QAChB;GAAE,UAAU,IAAI;GAAU,MAAM;GAAK,MAAM,IAAI,YAAY;GAAK,QAAQ;GAAQ,GAC/E,SAAS;AACR,gBAAa,MAAM;AACnB,QAAK,QAAQ;AACb,WAAQ,KAAK;IAEhB;AACD,MAAI,GAAG,eAAe;AACpB,gBAAa,MAAM;AACnB,WAAQ,MAAM;IACd;AACF,MAAI,KAAK;GACT;;;;;;;;;;;;;;;;;AAwDJ,SAAgB,uBACd,eACA,WACA,SACkB;CAClB,MAAM,EACJ,kBAAkB,KAClB,wBAAwB,GACxB,WACA,iBACA,OAAO,QAAgB,QAAQ,OAAO,MAAM,IAAI,EAChD,QAAQ,aACR,cAAc,qBACZ;CAEJ,IAAI,gBAAgB;CACpB,IAAI,sBAAsB;CAC1B,IAAI,kBAAkB;CACtB,IAAI,UAAU;CAKd,MAAM,kBAAkB,YAA2B;AACjD,MAAI,QAAS;AAEb,qBAAmB;AACnB,MAAI,kBAAA,EAEF;AAGF,MACE,yDAAyD,gBAAgB,OAC1E;AAED,MAAI;GACF,MAAM,YAAY,MAAM,YAAY,UAAU;AAE9C,OAAI;AACF,kBAAc,MAAM;WACd;AAGR,mBAAgB;AAChB,yBAAsB;AAEtB,qBAAkB,UAAU;AAC5B,OAAI,4CAA4C,UAAU,OAAO,IAAI;AACrE,aAAU,UAAU;WACb,KAAK;GACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,OAAI,sCAAsC,gBAAgB,WAAW,QAAQ,IAAI;AAEjF,OAAI,mBAAA,GAAyC;AAC3C,kBAAc,OAAO;AACrB,cAAU;IACV,MAAM,6BAAY,IAAI,MAAM,EAAC,aAAa;AAC1C,QACE;EAED;AACD,oBAAgB,UAAU;;;;CAOhC,MAAM,qBAAqB,MAAyB;AAClD,IAAE,kBAAkB,SAAS;AAC3B,OAAI,QAAS;AACb,OACE,2DAA2D,KAAK,oCACjE;AAGD,yBAAsB;AACjB,oBAAiB;IACtB;;AAIJ,mBAAkB,cAAc;CAEhC,MAAM,SAAS,kBAAkB;AAC/B,GAAM,YAAY;AAChB,OAAI,QAAS;GAEb,MAAM,WAAW,cAAc;AAG/B,OAFc,MAAM,MAAM,SAAS,EAExB;AAET,QAAI,sBAAsB,EACxB,KAAI,sDAAsD;AAE5D,0BAAsB;AACtB,sBAAkB;AAClB;;AAGF,0BAAuB;AACvB,OACE,4CAA4C,oBAAoB,GAAG,sBAAsB,QAAQ,SAAS,KAC3G;AAED,OAAI,sBAAsB,sBAExB;AAIF,SAAM,iBAAiB;MACrB;IACH,gBAAgB;AAEnB,QAAO,EACL,OAAO;AACL,YAAU;AACV,gBAAc,OAAO;IAExB;;;;;;;;AASH,SAAgB,iBACd,IACA,QACA,YAA2B,MAC3B,kBAAkB,GACJ;AACd,QAAO;EAAE;EAAI;EAAQ;EAAW;EAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9TnD,SAAgB,oBAAoB,WAAkC;AACpE,KAAI;EAIF,MAAM,SAAS,UAAU,QAAQ,IAAI;AACrC,MAAI,WAAW,GAAI,QAAO;EAE1B,MAAM,KADS,IAAI,gBAAgB,UAAU,MAAM,SAAS,EAAE,CAAC,CAC7C,IAAI,gBAAgB;AACtC,SAAO,MAAM,GAAG,SAAS,IAAI,KAAK;SAC5B;AACN,SAAO;;;;;;;;;AA6DX,SAAgB,YAAY,MAA+B;AACzD,QAAO,SAAS,mBAAmB,SAAS,mBAAmB,SAAS;;;;;;;;;;;;;;;;;;AAyM1E,SAAS,wBACP,YACA,UACA,WACA,iBAAiB,KACkC;AAGnD,KAAI,WAAW,mBACb,QAAO,WAAW,mBAAmB,UAAU,WAAW,eAAe;AAI3E,QAAO,IAAI,SAAmD,SAAS,WAAW;EAChF,MAAM,WAAW,KAAK,KAAK,GAAG;EAC9B,IAAI,UAAU;EACd,MAAM,OAAO,kBAAkB;GAC7B,MAAM,UAAU,WAAW,aAAa;AACxC,OAAI,SAAS,QAAQ,EAAE;AACrB,cAAU;AACV,kBAAc,KAAK;AACnB,YAAQ,QAAQ;cACP,KAAK,KAAK,IAAI,UAAU;AACjC,cAAU;AACV,kBAAc,KAAK;AACnB,2BAAO,IAAI,MAAM,kCAAkC,UAAU,KAAK,CAAC;;KAEpE,eAAe;EAElB,MAAM,UAAU,WAAW,aAAa;AACxC,MAAI,CAAC,WAAW,SAAS,QAAQ,EAAE;AACjC,aAAU;AACV,iBAAc,KAAK;AACnB,WAAQ,QAAQ;;GAElB;;;;;;;;;;;;;;AAeJ,SAAgB,kBAAkB,MAA+B;CAC/D,MAAM,EACJ,YACA,QAAQ,WACR,WACA,iBACA,yBAAyB,KACzB,cACA,gBAAgB,WAChB,sBAAsB,iBACtB,sBAAsB,cACtB,YACA,kBACA,mBACA,UAAU,gBACR;CAOJ,MAAM,gBAAgB,KAAK,wBAAwB;CAMnD,MAAM,aAAa,eAAe;CASlC,MAAM,SAA2B,aAAa,2BAA2B,WAAW;CAMpF,MAAM,qBACJ,oBACO,kBAAkB,OAAO,OAAO,MAAM,eAAe,EAAE,OAAO,kBAAkB;CACzF,MAAM,2BACJ,0BAA0B,gBAAgB,OAAO,OAAO,KAAK,cAAc,eAAe;CAK5F,MAAM,YAAkC,gBAAgB,IAAI,8BAA8B;CAE1F,MAAM,SAAS,IAAI,OACjB;EAAE,MAAM;EAAa,SAAA;EAAsB,EAG3C,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa,MAAM,EAAE,EAAE,CACnD;AAED,QAAO,kBAAkB,8BAA8B;EAIrD,MAAM,OAAO,OAAO;EACpB,MAAM,MAAM,oBAAoB;EAChC,MAAM,WAAW,KAAK,aAAa,CAAC,SAAS;EAE7C,MAAM,cAAc,yBAAyB,wBAAwB,IAAI;AAMzE,SAAO,EAAE,OALK,WACV,YAAY,KAAK,UAAU,EAAE,GAAG,MAAM,EAAE,GACxC,YACG,QAAQ,SAAS,qBAAqB,IAAI,KAAK,KAAK,CAAC,CACrD,KAAK,UAAU,EAAE,GAAG,MAAM,EAAE,EACnB;GAChB;AAEF,QAAO,kBAAkB,uBAAuB,OAAO,YAAY;EACjE,MAAM,OAAO,QAAQ,OAAO;AAC5B,MAAI,CAAC,gBAAgB,KAAK,CACxB,QAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,iBAAiB;IAAQ,CAAC;GAC1D,SAAS;GACV;EASH,MAAM,OAAO,OAAO;AAKpB,MAAI,SAAS,eAAe;GAC1B,MAAM,UAAU,QAAQ,OAAO,WAAW;GAC1C,MAAM,OAAO,wBAAwB,QAAQ;AAC7C,OAAI,SAAS,KACX,QAAO,SACL,gHAED;GAEH,MAAM,UAAU,QAAQ,OAAO,WAAW,YAAY;GAKtD,MAAM,iBAAiB,QAAQ,OAAO,WAAW;GACjD,MAAM,cAAc,OAAO,mBAAmB,WAAW,iBAAiB,KAAA;AAC1E,OAAI;AAEF,WAAOC,aADQ,MAAM,OAAO,WAAW,MAAM,SAAS,YAAY,CACzC;YAClB,KAAK;AACZ,WAAO,YAAY,KAAK,KAAK;;;EAYjC,MAAM,MAAM,oBAAoB;EAChC,MAAM,YAAY,0BAA0B;AAM5C,MAAI,CAAC,kBAAkB,MAAM,IAAI,EAAE;GACjC,MAAM,cAAc,oBAAoB,KAAK,IAAI;AAEjD,WAAQ,cAAc;IACpB,MAAM;IACN,WAAW;IACX;IACA,YAAY;IACZ;IACD,CAAC;AACF,UAAO,mBAAmB,MAAM,aAAa,KAAK,UAAU;;AAM9D,MAAI,cAAc,KAAK,CACrB,KAAI;AACF,SAAM,KAAK,eAAe;AAC1B,WAAQ,MAAR;IACE,KAAK,wBACH,QAAOA,aAAW,MAAM,kBAAkB,UAAU,CAAC;IACvD,KAAK,mBACH,QAAOA,aAAW,MAAM,aAAa,UAAU,CAAC;IAClD,KAAK,gCACH,QAAOA,aAAW,MAAM,0BAA0B,UAAU,CAAC;IAC/D,QACE,QAAO,YAAY,KAAK;;WAErB,KAAK;AACZ,UAAO,YAAY,KAAK,KAAK;;AAOjC,MAAI,SAAS,mBACX,KAAI;GACF,MAAM,WAAW,QAAQ,OAAO,WAAW;GAC3C,MAAM,oBAAoB,OAAO,aAAa,YAAY,WAAW,IAAI,WAAW;AAYpF,UAAOC,iBAXQ,MAAM,eAAe;IAClC,QAAQ,iBAAiB;IACzB,YAAY;IACZ;IACA;IACA;IACA,UAAU;IACV;IACA,gBAAgB,qBAAqB,IAAI,KAAA;IAC1C,CAAC,EAE4B,MAAM,KADnB,KAAK,aAAa,CAAC,SAAS,EACK;WAC3C,KAAK;AACZ,UAAO,YAAY,KAAK,KAAK;;AAWjC,MAAI,SAAS,oBAAoB;GAC/B,MAAM,gBAAgB,QAAQ,OAAO,WAAW,oBAAoB;GAIpE,MAAM,YAAY,QAAQ,OAAO,WAAW,cAAc;GAI1D,MAAM,iBAAiB,QAAQ,OAAO,WAAW;GACjD,MAAM,uBAAuB;AAC3B,QAAI,OAAO,mBAAmB,YAAY,CAAC,OAAO,SAAS,eAAe,CACxE,QAAO;IAET,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,eAAe,CAAC;AAE1D,QAAI,kBAAkB,EAAG,QAAO;AAChC,WAAO,KAAK,MAAM,QAAQ,GAAG;OAC3B;AAIJ,OAAI,aAAa,QAAQ,eACvB,QAAO,SACL,oLAGD;AAIH,OAAI,QAAQ,gBAAgB;IAI1B,MAAM,sBAAsB,QAAQ,OAAO,WAAW;IACtD,MAAM,mBACJ,OAAO,wBAAwB,WAAW,sBAAsB,KAAA;IAElE,IAAI,gBADiB,QAAQ,IAAI,qBAAqB,MAAM,IAAI;AAEhE,QAAI,kBAAkB,MAAM,qBAAqB,KAAA,GAAW;KAC1D,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,sBADe,MAAM,cAAc,EAAE,aAAa,kBAAkB,CAAC,GAC7C,iBAAiB;;AAE3C,QAAI,kBAAkB,GACpB,QAAO,SACL,gMAGD;IAEH,MAAM,eAAe,iBAAiB;AACtC,QAAI,CAAC,aAAa,MAAM,aAAa,WAAW,KAC9C,QAAO,SACL,uHAED;IAOH,MAAM,SAAS,eAAe;AAS9B,QAAI,WAAW,KAAA,KAAa,WAAW,GACrC,QAAO,SACL,gIAED;IAEH,IAAI;IACJ,IAAI;IACJ;KACE,MAAM,MAAM,KAAK,KAAK;AACtB,gBAAW,aAAa,QAAQ,IAAI;KACpC,MAAM,eAAe;KAGrB,MAAM,cAAc,MAAA,IAAgC,eAAe;AACnE,gBAAW;MACT,SAAS;MACT,YAAA,IAAsC;MACtC,WAAW,IAAI,KAAK,YAAY,CAAC,aAAa;MAC/C;;IAKH,IAAI;AACJ,QAAI,qBAAqB,KAAA,EACvB,KAAI;KACF,MAAM,EAAE,iBAAiB,MAAM,OAAO;KACtC,MAAM,SAAS,aAAa,GAAG,iBAAiB,gBAAgB,OAAO;KACvE,MAAM,MAAM,KAAK,MAAM,OAAO;KAC9B,MAAM,UAAU,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAK1D,wBAHiB,QAAQ,SAAS,IAAI,GAClC,QAAQ,MAAM,QAAQ,QAAQ,IAAI,GAAG,EAAE,GACvC,SACuB,MAAM,IAAI,KAAA;YAC/B;IAOV,MAAM,YAAY,uBAAuB,eAAe,aAAa,QAAQ,UAAU;KACrF,MAAM;KACN,GAAI,YAAY,EAAE,WAAW,MAAM,GAAG,EAAE;KACzC,CAAC;AAIF,uBAAmB;KACjB,MAAM;KACN;KACA,QAAQ,aAAa;KACrB,SAAS;KACV,CAAC;IACF,MAAM,WAAW,aAAa;IAE9B,MAAM,OAAO;IAKb,MAAM,kBAAkB,UACtB,MAAM,SAAS;IACjB,MAAM,qBACJ,UACA,YACA,aACW;KACX,MAAM,eAAe,SAClB,MAAM,GAAG,EAAE,CACX,KAAK,MAAM,EAAE,IAAI,MAAM,GAAG,GAAG,CAAC,CAC9B,KAAK,KAAK;AAGb,YACE,GAAG,SAAS,8BAA8B,WAAW,GAFrD,SAAS,SAAS,IAAI,kCAAkC,aAAa,KAAK,GAEL;;AAOzE,WAAO,OAAO,YAAY;KACxB,MAAM,SACJ;KACF,MAAM,gBAEF;KACJ,MAAM,eAAe,gBAAgB;AAErC,SAAI,CAAC,cAAc;MACjB,MAAM,eACJ;MAEF,MAAM,aAAa,MAAM,SAAS,UAAU;MAC5C,MAAM,eAAe,GAAG,gBAAgB,eAAe,OAAO,IAAI,KAAK,UAAU;OAAE;OAAW;OAAU,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;OAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AACpJ,UAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;OAAE,MAAM;OAAiB,MAAM;OAAc,CAAC,EAAE;MAErE,IAAI,kBAA4D,EAAE;AAClE,UAAI;AACF,yBAAkB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;cAC9E;AACN,yBAAkB,KAAK,aAAa;AACpC,cAAO;QACL,SAAS,CACP;SACE,MAAM;SACN,MAAM,kBAAkB,cAAc,gBAAgB,KAAM,gBAAgB;SAC7E,CACF;QACD,SAAS;QACV;;MAEH,MAAM,gBAAgB,UAAU,MAAM,iBAAiB,CAAC;AACxD,aAAO,EACL,SAAS,CACP;OACE,MAAM;OACN,MAAM,GAAG,aAAa,MAAM,KAAK,UAAU,eAAe,MAAM,EAAE;OACnE,CACF,EACF;;AAGH,SAAI,gBAAgB,cAAc;MAGhC,MAAM,gBAAgB,MAAM,gBAFZ,aAAa,mBAAmB,UAAU,EAC3C,oBAAoB,aAAa,KAAK,YAAY,mBAAmB,UAAU,GAClC;AAC5D,UAAI,cAAc,QAAQ;OACxB,MAAM,cAAc,cAAc,UAAU,qBAAqB;OACjE,MAAM,aAAa;QACjB,WAAW;QACX,WAAW;QACX,GAAI,cAAc,UAAU,EAAE,SAAS,MAAM,GAAG,EAAE;QACnD;OACD,MAAM,YACJ,GAAG,gBAAgB,OAAO,IACvB,KAAK,UAAU;QAAE;QAAU;QAAY,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;QAAG,EAAE,MAAM,EAAE,CAAC,sBAC7D,YAAY,wBACvB,cAAc;AACxB,WAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;QAAE,MAAM;QAAiB,MAAM;QAAW,CAAC,EAAE;OAElE,IAAI,gBAA0D,EAAE;AAChE,WAAI;AACF,wBAAgB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;eAC5E;AACN,wBAAgB,KAAK,aAAa;AAClC,eAAO;SACL,SAAS,CACP;UACE,MAAM;UACN,MAAM,kBAAkB,WAAW,gBAAgB,KAAM,cAAc;UACxE,CACF;SACD,SAAS;SACV;;OAEH,MAAM,cAAc,UAAU,MAAM,iBAAiB,CAAC;AACtD,cAAO,EACL,SAAS,CACP;QACE,MAAM;QACN,MAAM,GAAG,UAAU,MAAM,KAAK,UAAU,aAAa,MAAM,EAAE;QAC9D,CACF,EACF;;MAEH,MAAM,aAAa;OACjB,WAAW;OACX,WAAW;OACX,eAAe,cAAc,SAAS;OACtC,QAAQ,cAAc;OACtB,GAAI,cAAc,gBACd,EAAE,eAAe,cAAc,eAAe,GAC9C,EAAE;OACP;MACD,MAAM,aAAa,cAAc,gBAC7B,aAAa,cAAc,kBAC3B;MACJ,MAAM,eACJ,+CAC2B,cAAc,QAAQ,gBAClC,cAAc,WAC7B,aACA;MACF,MAAM,KAAK,MAAM,SAAS,UAAU;MACpC,MAAM,WAAW,GAAG,gBAAgB,eAAe,OAAO,IAAI,KAAK,UAAU;OAAE;OAAW;OAAU;OAAY,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;OAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AAC5J,UAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;OAAE,MAAM;OAAiB,MAAM;OAAU,CAAC,EAAE;MAEjE,IAAI,kBAA4D,EAAE;AAClE,UAAI;AACF,yBAAkB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;cAC9E;AACN,yBAAkB,KAAK,aAAa;AACpC,cAAO;QACL,SAAS,CACP;SACE,MAAM;SACN,MAAM,kBAAkB,UAAU,gBAAgB,KAAM,gBAAgB;SACzE,CACF;QACD,SAAS;QACV;;MAEH,MAAM,gBAAgB,UAAU,MAAM,iBAAiB,CAAC;AACxD,aAAO,EACL,SAAS,CACP;OACE,MAAM;OACN,MAAM,GAAG,SAAS,MAAM,KAAK,UAAU,eAAe,MAAM,EAAE;OAC/D,CACF,EACF;;KAGH,MAAM,KAAK,MAAM,SAAS,UAAU;KACpC,MAAM,WAAW,GAAG,gBAAgB,OAAO,IAAI,KAAK,UAAU;MAAE;MAAW;MAAU,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;MAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AACjI,SAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;MAAE,MAAM;MAAiB,MAAM;MAAU,CAAC,EAAE;KAEjE,IAAI,gBAA0D,EAAE;AAChE,SAAI;AACF,sBAAgB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;aAC5E;AACN,sBAAgB,KAAK,aAAa;AAClC,aAAO;OACL,SAAS,CACP;QACE,MAAM;QACN,MAAM,kBAAkB,UAAU,gBAAgB,KAAM,cAAc;QACvE,CACF;OACD,SAAS;OACV;;KAEH,MAAM,cAAc,UAAU,MAAM,iBAAiB,CAAC;AACtD,YAAO,EACL,SAAS,CACP;MACE,MAAM;MACN,MAAM,GAAG,SAAS,MAAM,KAAK,UAAU,aAAa,MAAM,EAAE;MAC7D,CACF,EACF;QACC;;GAKN,MAAM,YAAY,QAAQ,OAAO,WAAW;AAC5C,OAAI,OAAO,cAAc,YAAY,cAAc,GACjD,QAAO,SACL,qKAGD;GAKH,MAAM,eAAe,oBAAoB,UAAU;AACnD,OAAI,CAAC,aACH,SAAQ,aAAa;IACnB,MAAM;IACN,KAAK;IACN,CAAC;;GAIJ,MAAM,kBAAkB,UAA6D;AACnF,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAI,iBAAiB,KAAM,QAAO;AAClC,WAAO,MAAM,MAAM,MAAM,EAAE,IAAI,SAAS,aAAa,CAAC;;;GAIxD,MAAM,qBACJ,UACA,YACA,aACW;IACX,MAAM,eAAe,SAClB,MAAM,GAAG,EAAE,CACX,KAAK,MAAM,EAAE,IAAI,MAAM,GAAG,GAAG,CAAC,CAC9B,KAAK,KAAK;IACb,MAAM,eACJ,SAAS,SAAS,IAAI,kCAAkC,aAAa,KAAK;AAE5E,WACE,GAAG,SAAS,aAFS,eAAe,0BAA0B,iBAAiB,GAEvC,mBAAmB,WAAW,GAAG,aAAa;;GAW1F;IACE,MAAM,cAAc,eAAe;AACnC,QAAI,gBAAgB,KAAA,KAAa,gBAAgB,GAC/C,QAAO,SACL,gIAED;;AAGL,OAAI;IAOF,MAAM,iBAAiB,iBAAiB;IACxC,MAAM,EAAE,WAAW,UAAU,kBAAkB,SAAS,eACtD,WACA,gBACA,eAAe,CAChB;AAKD,QAAI,eAAe,WAAW,KAC5B,oBAAmB;KAAE,MAAM;KAAU;KAAW,QAAQ,eAAe;KAAQ,CAAC;IAIlF,MAAM,gBAAgB,mBAAmB,sBAAsB,iBAAiB,QAAQ;IAExF,MAAM,SACJ;IAIF,MAAM,eAAe,gBAAgB;AAGrC,QAAI,CAAC,cAAc;KACjB,MAAM,eACJ;KAEF,MAAM,aAAa,MAAM,SAAS,UAAU;KAC5C,MAAM,eAAe,GAAG,gBAAgB,eAAe,OAAO,IAAI,KAAK,UAAU;MAAE;MAAW;MAAU,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;MAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AAEpJ,SAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;MAAE,MAAM;MAAiB,MAAM;MAAc,CAAC,EAAE;KAIrE,IAAI,kBAA4D,EAAE;AAClE,SAAI;AACF,wBAAkB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;aAC9E;AACN,wBAAkB,KAAK,aAAa;AACpC,aAAO;OACL,SAAS,CACP;QACE,MAAM;QACN,MAAM,kBAAkB,cAAc,gBAAgB,KAAM,gBAAgB;QAC7E,CACF;OACD,SAAS;OACV;;KAGH,MAAM,gBAAgB,UAAU,MAAM,iBAAiB,CAAC;AACxD,YAAO,EACL,SAAS,CACP;MACE,MAAM;MACN,MAAM,GAAG,aAAa,MAAM,KAAK,UAAU,eAAe,MAAM,EAAE;MACnE,CACF,EACF;;AAIH,QAAI,gBAAgB,cAAc;KAIhC,MAAM,gBAAgB,MAAM,gBAHZ,aAAa,mBAAmB,UAAU,EAC3C,oBAAoB,aAAa,KAAK,YAAY,mBAAmB,UAAU,GAElC;AAE5D,SAAI,cAAc,QAAQ;MAGxB,MAAM,cAAc,cAAc,UAAU,qBAAqB;MACjE,MAAM,aAAa;OACjB,WAAW;OACX,WAAW;OACX,GAAI,cAAc,UAAU,EAAE,SAAS,MAAM,GAAG,EAAE;OACnD;MACD,MAAM,YACJ,GAAG,gBAAgB,OAAO,IACvB,KAAK,UAAU;OAAE;OAAU;OAAY,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;OAAG,EAAE,MAAM,EAAE,CAAC,sBAC7D,YAAY,wBACvB,cAAc;AAExB,UAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;OAAE,MAAM;OAAiB,MAAM;OAAW,CAAC,EAAE;MAIlE,IAAI,gBAA0D,EAAE;AAChE,UAAI;AACF,uBAAgB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;cAC5E;AACN,uBAAgB,KAAK,aAAa;AAClC,cAAO;QACL,SAAS,CACP;SACE,MAAM;SACN,MAAM,kBAAkB,WAAW,gBAAgB,KAAM,cAAc;SACxE,CACF;QACD,SAAS;QACV;;MAGH,MAAM,cAAc,UAAU,MAAM,iBAAiB,CAAC;AACtD,aAAO,EACL,SAAS,CACP;OACE,MAAM;OACN,MAAM,GAAG,UAAU,MAAM,KAAK,UAAU,aAAa,MAAM,EAAE;OAC9D,CACF,EACF;;KAIH,MAAM,aAAa;MACjB,WAAW;MACX,WAAW;MACX,eAAe,cAAc,SAAS;MACtC,QAAQ,cAAc;MACtB,GAAI,cAAc,gBAAgB,EAAE,eAAe,cAAc,eAAe,GAAG,EAAE;MACtF;KACD,MAAM,aAAa,cAAc,gBAC7B,aAAa,cAAc,kBAC3B;KACJ,MAAM,eACJ;EAEG,cAAc,QAAQ,gBACV,cAAc,WAC7B,aACA;KACF,MAAM,KAAK,MAAM,SAAS,UAAU;KACpC,MAAM,WAAW,GAAG,gBAAgB,eAAe,OAAO,IAAI,KAAK,UAAU;MAAE;MAAW;MAAU;MAAY,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;MAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AAE5J,SAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;MAAE,MAAM;MAAiB,MAAM;MAAU,CAAC,EAAE;KAIjE,IAAI,kBAA4D,EAAE;AAClE,SAAI;AACF,wBAAkB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;aAC9E;AACN,wBAAkB,KAAK,aAAa;AACpC,aAAO;OACL,SAAS,CACP;QACE,MAAM;QACN,MAAM,kBAAkB,UAAU,gBAAgB,KAAM,gBAAgB;QACzE,CACF;OACD,SAAS;OACV;;KAGH,MAAM,gBAAgB,UAAU,MAAM,iBAAiB,CAAC;AACxD,YAAO,EACL,SAAS,CACP;MACE,MAAM;MACN,MAAM,GAAG,SAAS,MAAM,KAAK,UAAU,eAAe,MAAM,EAAE;MAC/D,CACF,EACF;;IAIH,MAAM,KAAK,MAAM,SAAS,UAAU;IACpC,MAAM,WAAW,GAAG,gBAAgB,OAAO,IAAI,KAAK,UAAU;KAAE;KAAW;KAAU,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;KAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AAEjI,QAAI,CAAC,cACH,QAAO,EACL,SAAS,CAAC;KAAE,MAAM;KAAiB,MAAM;KAAU,CAAC,EACrD;IASH,IAAI,gBAA0D,EAAE;AAChE,QAAI;AACF,qBAAgB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;YAC5E;AACN,qBAAgB,KAAK,aAAa;AAClC,YAAO;MACL,SAAS,CACP;OACE,MAAM;OACN,MAAM,kBAAkB,UAAU,gBAAgB,KAAM,cAAc;OACvE,CACF;MACD,SAAS;MACV;;IAGH,MAAM,cAAc,UAAU,MAAM,iBAAiB,CAAC;AACtD,WAAO,EACL,SAAS,CACP;KACE,MAAM;KACN,MAAM,GAAG,SAAS,MAAM,KAAK,UAAU,aAAa,MAAM,EAAE;KAC7D,CACF,EACF;YACM,KAAK;AACZ,WAAO,YAAY,KAAK,KAAK;;;AAIjC,MAAI;AAGF,SAAM,KAAK,eAAe;WACnB,KAAK;AACZ,OAAI,SAAS,cAAc;AAIzB,QAAI;AACF,WAAM,KAAK,kBAAkB;YACvB;AAKR,WAAOA,iBAFW,UAAU,MAAM,iBAAiB,CAAC,EAEnB,MAAM,KADtB,KAAK,aAAa,CAAC,SAAS,EACQ;;AAGvD,UAAO,0BAA0B,KAAK,KAAK;;AAG7C,MAAI;AACF,WAAQ,MAAR;IACE,KAAK,wBACH,QAAOD,aAAW,oBAAoB,KAAK,CAAC;IAC9C,KAAK,mBAAmB;KACtB,MAAM,WAAW,QAAQ,OAAO,WAAW;AAE3C,YAAOA,aAAW,EAAE,YAAY,eAAe,MADjC,OAAO,aAAa,YAAY,WAAW,IAAI,WAAW,GACb,EAAE,CAAC;;IAEhE,KAAK,wBACH,QAAOA,aAAW,oBAAoB,KAAK,CAAC;IAC9C,KAAK;AAEH,SAAI;AACF,YAAM,KAAK,kBAAkB;aACvB;AAKR,YAAOC,iBAFe,UAAU,MAAM,iBAAiB,CAAC,EAEnB,MAAM,KADjB,KAAK,aAAa,CAAC,SAAS,EACY;IAEpE,KAAK,mBACH,QAAOD,aAAW,MAAM,eAAe,KAAK,CAAC;IAC/C,KAAK,gBACH,QAAOA,aAAW,MAAM,aAAa,KAAK,CAAC;IAC7C,KAAK,mBAAmB;KACtB,MAAM,OAAO,MAAM,eAAe,KAAK;AACvC,YAAO,EACL,SAAS,CAAC;MAAE,MAAM;MAAkB,MAAM,KAAK;MAAM,UAAU,KAAK;MAAU,CAAC,EAChF;;IAEH,KAAK,oBASH,QAAOC,iBAFc,MAAM,gBAAgB,MAAM,IAAI,EAEjB,MAAM,KADjB,KAAK,aAAa,CAAC,SAAS,EACW;IAElE,KAAK,YAAY;KACf,MAAM,aAAa,QAAQ,OAAO,WAAW;AAC7C,SAAI,OAAO,eAAe,YAAY,eAAe,GACnD,QAAO,SACL,+DACD;AAUH,SACE,KAAK,SAAS,WACd,eAAe,IACf,QAAQ,OAAO,WAAW,YAAY,KAEtC,QAAO,eAAe,WAAW;AAGnC,YAAOD,aAAW,MAAM,SAAS,MAAM,WAAW,CAAC;;IAErD,KAAK,YAAY;KACf,MAAM,UAAU,QAAQ,OAAO,WAAW;AAC1C,SAAI,OAAO,YAAY,YAAY,YAAY,GAC7C,QAAO,SACL,qDACD;KAEH,MAAM,UAAU,QAAQ,OAAO,WAAW;KAC1C,MAAM,UAAqB,MAAM,QAAQ,QAAQ,GAAG,UAAU,EAAE;AAIhE,SACE,KAAK,SAAS,WACd,eAAe,IACf,QAAQ,OAAO,WAAW,YAAY,KAEtC,QAAO,eAAe,WAAW;KAGnC,MAAM,YAAY,MAAM,QAAQ,MAAM,SAAS,QAAQ;AAEvD,SACE,CAAC,UAAU,MACX,OAAO,UAAU,UAAU,YAC3B,UAAU,MAAM,WAAW,cAAc,CAIzC,QAAO,eAAe,YAAY,KAAK,SAAS,QAAQ;AAG1D,YAAOC,iBAAe,WAAW,MAAM,KADf,KAAK,aAAa,CAAC,SAAS,EACQ;;IAE9D,QACE,QAAO,YAAY,KAAK;;WAErB,KAAK;AAGZ,UAAO,YAAY,KAAK,MAAM,KAAK,SAAS,QAAQ;;GAEtD;AAEF,QAAO;;;;;;;;;;AAWT,SAAgB,wBAAwB,KAAqC;AAC3E,KACE,QAAQ,mBACR,QAAQ,mBACR,QAAQ,mBACR,QAAQ,aAER,QAAO;AAET,QAAO;;;;;;;;;;;AAYT,SAAgB,2BAA2B,YAA6C;AACtF,QAAO;EACL,IAAI,SAAS;AACX,UAAO;;EAMT,mBAAmB,KAAA;EAMnB,WACE,MACA,SACA,cAC2B;AAI3B,OAAI,SAAS,gBACX,QAAO,QAAQ,uBACb,IAAI,MACF,qJAGD,CACF;AAIH,OAFkB,YAAY,KAAK,MACjB,WAAW,SAAS,SAEpC,QAAO,QAAQ,uBACb,IAAI,MACF,yBAAyB,WAAW,KAAK,gBACnC,KAAK,sEAEZ,CACF;AAGH,OAAI,SAAS,gBAAgB,CAAC,QAC5B,QAAO,QAAQ,uBACb,IAAI,MACF,wGAED,CACF;AAEH,iBAAc,SAAS,aAAa;GACpC,MAAM,cAAc,kBAAkB,WAAW,MAAM,eAAe,CAAC;AACvE,UAAO,QAAQ,QAAQ;IACrB;IACA;IACA,MAAM,WAAW;IACjB,iBAAiB,WAAW,SAAS,WAAW,eAAe;IAC/D,UACE,WAAW,SAAS,UAChB,wCACA;IACP,CAAC;;EAEL;;;;;;;;;AAUH,SAAS,iBAAiB,OAA+B;CACvD,MAAM,SAAS,QAAQ,IAAI;CAC3B,MAAM,OAAO,SAAS,aAAa,OAAO,GAAG,KAAA;AAC7C,QAAO,MAAM,SAAS,aAClB,uBAAuB,MAAM,eAAe,MAAM,QAAQ,MAAM,EAC9D,MAAM,MAAM,SACb,CAAC,GACF,uBAAuB,MAAM,WAAW,MAAM,QAAQ,KAAK;;AAGjE,SAASD,aAAW,OAAgB;AAClC,QAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,OAAO,MAAM,EAAE;EAAE,CAAC,EAAE;;;;;;;AAQvF,SAASC,iBAAe,OAAgB,MAAc,KAAqB,UAAmB;CAC5F,MAAM,UAAU,aAAa,OAAO;EAAE;EAAM;EAAK;EAAU,CAAC;AAC5D,QAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE;EAAE,CAAC,EAAE;;AAGzF,SAAS,YAAY,MAAc;AACjC,QAAO,SAAS,gBAAgB,OAAO;;;;;;;;;;AAWzC,SAAS,0BAA0B,KAAc,UAAkB;CACjE,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAGhE,KAAI,QAAQ,SAAS,4BAA4B,IAAI,QAAQ,SAAS,gBAAgB,CACpF,QAAO,iBAAiB,SAAS;AAInC,KACE,QAAQ,SAAS,yBAAyB,IAC1C,QAAQ,SAAS,gBAAgB,IACjC,QAAQ,SAAS,kBAAkB,IACnC,QAAQ,SAAS,qBAAqB,CAEtC,QAAO,eAAe,SAAS;AAIjC,KACE,QAAQ,SAAS,sBAAsB,IACvC,QAAQ,SAAS,kBAAkB,IACnC,QAAQ,SAAS,+BAA+B,CAEhD,QAAO,qBAAqB,SAAS;AAIvC,QAAO,kBAAkB,KAAK,SAAS;;;;;;AAOzC,SAAS,YAAY,KAAc,MAAc,UAAU,OAAO;AAChE,QAAO,kBAAkB,KAAK,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC9C,SAAgB,mBACd,YACA,QACA,aAAa,KACb,UACkB;;CAElB,SAAS,YAAoB;AAC3B,SAAO,WACJ,aAAa,CACb,KAAK,MAAM,EAAE,GAAG,CAChB,MAAM,CACN,KAAK,IAAI;;CAGd,IAAI,gBAAgB,WAAW;AAE/B,KAAI,kBAAkB,IAAI;AACnB,SAAO,qBAAqB;AACjC,cAAY;;CAGd,MAAM,SAAS,kBAAkB;EAC/B,MAAM,UAAU,WAAW;AAC3B,MAAI,YAAY,eAAe;AAC7B,mBAAgB;AAChB,OAAI,YAAY,IAAI;AAEb,WAAO,qBAAqB;AACjC,gBAAY;;;IAIf,WAAW;AAEd,QAAO,EACL,OAAO;AACL,gBAAc,OAAO;IAExB;;;;;;;;;;;;;AAuCH,SAAS,sBAAsB,cAAyC;AAMtE,QAAO,IAAI,kBAAkB;EAC3B;EACA,YAAY,QAAQ,IAAI;EACzB,CAAC;;;;;;;;;;;;AAaJ,IAAM,mBAAN,cAA+B,cAAc;CAC3C,YACE,WAGA;AACA,QAAM,EACJ,cAAc,QAAQ,WAAW,WAAW,CAAC,YAAY,QAAQ,OAAO,EACzE,CAAC;;;;;;;;;;;;AAkEN,eAAsB,kBAAyC;CAG7D,MAAM,WAAW,MAAM,eAAe;EAAE,MAFxB;EAEuC,QADxC,QAAQ,IAAI,oBAAoB;EACgB,CAAC;AAEhE,OAAM,IAAI,SAAe,MAAM,WAAW,GAAG,IAAI,CAAC;CAClD,MAAM,aAAa,IAAI,mBAAmB,EAAE,iBAAiB,SAAS,aAAa,CAAC;AACpF,QAAO;EACL;EACA,OAAO;AACL,cAAW,OAAO;AAClB,YAAS,MAAM;;EAElB;;;;;;;;;;;;;;;;;;;;;;AA0DH,eAAsB,gBAAgB,UAAkC,EAAE,EAAyB;AAMjG,4BAA2B;CAK3B,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,cAAc,QAAQ,eAAe,KAAA;CAE3C,MAAM,QAAQ,MAAM,eAAe;EACjC,MAAM;EACN,YAAY,QAAQ;EACpB,cAAc,QAAQ;EACvB,CAAC;AAEF,SAAQ,gBAAgB;EAAE,MAAM,MAAM;EAAM;EAAa,CAAC;CAE1D,IAAI,SAA6B;CACjC,IAAI,eAA6B,iBAAiB,OAAO,KAAK;CAC9D,IAAI,cAAuC;AAG5B,sBAAqB;AAShB,kBAAiB,MAAM,KAAK,CAAC,MAC9C,MAAM;AACL,WAAS;AACT,iBAAe,iBAAiB,MAAM,EAAE,OAAO;AAC/C,UAAQ,WAAW,EAAE,OAAO;AAI5B,MAAI,EAAE,aAAa,KAAA,EACjB,SAAQ,mBAAmB,EAAE,SAAS;AAGxC,UAAQ,aAAa,EAAE,aAAa,CAAC;AAIrC,gBAAc,uBAAuB,GAAG,MAAM,MAAM;GAClD,YAAY,cAAc;AACxB,aAAS;AACT,mBAAe,iBAAiB,MAAM,UAAU,QAAQ,MAAM,EAAE;AAChE,YAAQ,WAAW,UAAU,OAAO;AAIpC,QAAI,UAAU,aAAa,KAAA,EACzB,SAAQ,mBAAmB,UAAU,SAAS;AAG3C,sBAAkB;KAAE,QAAQ,UAAU;KAAQ;KAAa,CAAC,CAAC,WAAW;AAC3E,aAAQ,aAAa;MAAE;MAAa,UAAU;MAAM,CAAC;MACrD;;GAEJ,kBAAkB,cAAc;AAC9B,mBAAe,iBAAiB,OAAO,MAAM,WAAW,EAAE;AAC1D,aAAS,eAAe,EACtB,KAAK,+BAA+B,UAAU,gDAC/C,CAAC;;GAEL,CAAC;AAEF,SAAO,kBAAkB;GAAE,QAAQ,EAAE;GAAQ;GAAa,CAAC;KAE5D,QAAQ;AAEP,WAAS,eAAe,EACtB,KAAK,4CAFS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAEL,gGAC1D,CAAC;GAEL;CAKD,MAAM,aAAa,sBAAsB,MAAM,QAAQ;AAEvD,QAAO;EACL;EAEA,aAAa;EAGb,cAAc,MAAM;EACpB,uBAAuB;EACvB,OAAO;AACL,gBAAa,MAAM;AAEnB,WAAQ,MAAM;AACd,cAAW,OAAO;AAEb,SAAM,OAAO;;EAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCH,eAAsB,kBACpB,MAAyB,QAAQ,KACjC,aAC6B;CAC7B,MAAM,YAAY,IAAI,uBAAuB,IAAI,MAAM;AACvD,KAAI,aAAa,GAAI,QAAO;AAE5B,KAAI,gBAAgB,KAAA,EAClB,KAAI;EACF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,SAAS,MAAM,cAAc,EAAE,aAAa,CAAC;AACnD,MAAI,QAAQ,cAAe,QAAO,OAAO;SACnC;;AAOZ,eAAsB,wBACpB,cACA,eACuB;AAOvB,4BAA2B;CAE3B,MAAM,aAAa,sBAAsB,aAAa;CAItD,MAAM,eAAe,iBAAiB,MADlB,aAAa,QAAQ,SAAS,KAAK,CACC;AACxD,QAAO;EACL;EAEA,aAAa;EAIb,cAAc;EAId,mBAAmB;EACnB,uBAAuB;EACvB,OAAO;AAEL,cAAW,OAAO;;EAErB;;;;;;;AAwBH,SAAgB,iBAAiB,MAAiC;AAChE,SAAQ,MAAR;EACE,KAAK,gBACH,QAAO;EACT,KAAK,gBACH,QAAO;EACT,KAAK;EACL,KAAK,aACH,QAAO;;;;AAKb,MAAa,wCACX;;;;;;;;;;;;;;;AAmBF,eAAsB,uBACpB,MAAyB,QAAQ,KACjC,aACiB;CAEjB,MAAM,MAAM,IAAI;CAChB,MAAM,WAAW,OAAO,QAAQ,WAAW,IAAI,MAAM,GAAG;AACxD,KAAI,aAAa,GACf,QAAO;AAIT,KAAI,gBAAgB,KAAA,GAAW;EAC7B,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,SAAS,MAAM,cAAc,EAAE,aAAa,CAAC;AACnD,MAAI,QAAQ,iBAAiB,KAAA,EAC3B,QAAO,OAAO;;AAKlB,OAAM,IAAI,MAAM,sCAAsC;;;;;;;;;;AAyDxD,MAAM,sBAAqC;CACzC,MAAM;CACN,qBAAqB,QAAQ,SAAS;CACtC,mBAAmB,EAAE;CACrB,yBAAyB,EAAE;CAC3B,gBAAgB;CAChB,YAAY,QAAQ,uBAAO,IAAI,MAAM,gDAAgD,CAAC;CACtF,aAAa;CACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CD,IAAa,uBAAb,MAA8D;CAC5D;;CAEA,+BAAgC,IAAI,KAA8B;;CAElE,eAA4C;CAC5C,SAAgC;CAChC,gBAAiD;CACjD,eAAuB;CAEvB,YAAY,MAAsB;AAChC,OAAK,OAAO;;CAGd,IAAI,SAAwB;AAC1B,SAAO,KAAK,eAAe,KAAK,aAAa,aAAa;;;CAI5D,IAAI,oBAA6C;AAC/C,SAAO,KAAK,cAAc;;;;;;;;;;;;CAa5B,IAAI,qBAAyC;AAC3C,MAAI,CAAC,KAAK,aAAc,QAAO,KAAA;AAC/B,SAAO,KAAK,aAAa,qBAAqB,KAAK,aAAa;;;CAIlE,iBAAiC;AAC/B,SAAO,CAAC,GAAG,KAAK,aAAa,QAAQ,CAAC;;;;;;;;;;CAWxC,oBAAkC;AAChC,MAAI,KAAK,cAAc,gBAAiB,QAAO,KAAK,aAAa,iBAAiB;AAClF,OAAK,MAAM,UAAU,KAAK,gBAAgB,CACxC,KAAI,OAAO,gBAAiB,QAAO,OAAO,iBAAiB;AAE7D,SAAO;GAAE,IAAI;GAAO,QAAQ;GAAM;;;;;;;CAQpC,MAAM,QAAsB;AAC1B,OAAK,SAAS;AACd,OAAK,YAAY;;;CAInB,cAAoB;AAClB,OAAK,eAAe,MAAM;AAC1B,OAAK,gBAAgB;;;CAIvB,aAA2B;EACzB,MAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ;EAGb,MAAM,eAAe,KAAK;AAC1B,MAAI,CAAC,aAAc;AACnB,OAAK,gBAAgB,mBACnB,aAAa,YACb,QACA,KAAK,KAAK,2BAA2B,WAC/B;AACJ,QAAK,KAAK,qBAAqB,cAAc;AAE7C,QAAK,KAAK,gBAAgB;AAM1B,OAAI,aAAa,WAAW,SAAS,SAAS;IAG5C,MAAM,cAAc,aAAa,WAAW,aAAa,CAAC;IAC1D,MAAM,MAAM,kBACV,aAAa,WAAW,MACxB,eAAe,EACf,aAAa,YACd;IAID,MAAM,qBAAqB,KAAK,KAAK,yBAAyB,IAAI;AAClE,SAAK,KAAK,eAAe,KAAK;KAC5B;KACA,kBAAkB,aAAa;KAC/B,UAAU,aAAa;KAKvB,UAAU,QAAQ,IAAI,8BACZ,aAAa,QAAQ,IAAI,sBAAgC,GAC/D,KAAA;KACJ;KACD,CAAC;;IAGP;;;;;;;;;;;;CAaH,MAAc,UAAU,KAAgB,aAA6C;EACnF,MAAM,OAAO,KAAK,aAAa,IAAI,IAAI;AACvC,MAAI,KAAM,QAAO;EACjB,MAAM,SAAS,MAAM,KAAK,KAAK,YAAY,KAAK,YAAY;AAC5D,OAAK,aAAa,IAAI,KAAK,OAAO;AAClC,SAAO;;CAGT,MAAM,WACJ,MACA,SACA,aAC2B;AAC3B,MAAI,KAAK,aACP,OAAM,IAAI,MAAM,kDAAkD;AAEpE,MAAI,SAAS,gBAAgB,CAAC,QAC5B,OAAM,IAAI,MACR,wGAED;AAGH,OAAK,eAAe;AACpB,MAAI;AASF,OAAI,YAAY,KAAK,CACnB,OAAM,wBAAwB,EAAE,aAAa,CAAC;GAQhD,MAAM,SAAS,MAAM,KAAK,UAAU,iBAAiB,KAAK,EAAE,YAAY;AAIxE,QAAK,eAAe;AAKpB,iBAAc,SAAS,aAAa;AAGpC,QAAK,aAAa;AAClB,QAAK,YAAY;AAGZ,QAAK,QAAQ,qBAAqB;GAEvC,MAAM,YAAY,YAAY,KAAK;AAMnC,UAAO;IACL;IACA,aAPkB,kBAClB,OAAO,WAAW,MAClB,eAAe,EACf,OAAO,YACR;IAIC,MAAM,OAAO,WAAW;IACxB,iBAAiB,OAAO,WAAW,SAAS,WAAW,eAAe;IACtE,UAAU,YACN,mDACA;IACL;YACO;AACR,QAAK,eAAe;;;;;;;;;;;;AAa1B,eAAsB,eAAe,UAAiC,EAAE,EAAiB;CAKvF,MAAM,aAAa,YAAY,EAAE,OAAO,QAAQ,SAAS,OAAO,CAAC;CAOjE,MAAM,iBAAiB,IAAI,oBAAoB;CAG/C,MAAM,uBAAuB,IAAI,8BAA8B;CAK/D,IAAI,uBAAsC;CAE1C,MAAM,SAAS,IAAI,qBAAqB;EAOtC,aAAa,OAAO,KAAK,gBACvB,QAAQ,kBACJ,wBACE,MAAM,uBAAuB,QAAQ,KAAK,YAAY,EACtD,MAAM,kBAAkB,QAAQ,KAAK,YAAY,CAClD,GACD,QAAQ,kBACN,iBAAiB,GACjB,gBAAgB;GACd,WAAW,QAAQ;GACnB,YAAY,sBAAsB;GAIlC,WAAW,WAAW;AACpB,eAAW,aAAa,OAAO;AAC/B,cAAU,mBAAmB;;GAK/B,mBAAmB,QAAQ;AACzB,2BAAuB;AACvB,eAAW,qBAAqB,IAAI;;GAKtC,oBAAoB,qBAAqB,kBAAkB;GAC5D,CAAC;EACV;EACA;EACA,oBAAoB,UAAU,mBAAmB;EAIjD,6BAA6B,UAAU,sBAAsB;EAC9D,CAAC;CAIF,MAAM,YAAY,IAAI,uBAAuB;AAI3C,SAHe,OAAO;GAItB;CAKF,IAAI,kBAAyC;CAK7C,MAAM,0BAA0C;EAC9C,MAAM,UAAU,OAAO,OAAO,aAAa;EAK3C,MAAM,eAAe,UAAU,sBAAsB;AACrD,SAAO;GACL,QAAQ;IAAE,IAAI,OAAO,mBAAmB,CAAC;IAAI,QAAQ,OAAO,mBAAmB,CAAC;IAAQ;GACxF,OAAO,QAAQ,KAAK,OAAO;IAAE,IAAI,EAAE;IAAI,KAAK,EAAE;IAAK,EAAE;GACrD,WAAW,kBAAkB,iBAAiB,gBAAgB,GAAG;GACjE;GAGA,MAAM,kBAAkB,OAAO,OAAO,MAAM,eAAe,EAAE,OAAO,kBAAkB;GACvF;;CAOH,MAAM,8BAED;EACH,MAAM,eAAe,OAAO;AAC5B,MAAI,CAAC,aACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAa;EAE3C,MAAM,UAAU,OAAO,OAAO,aAAa;AAC3C,MAAI,QAAQ,WAAW,EACrB,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAY;EAE1C,MAAM,aAAa,QAAQ,IAAI;AAC/B,MAAI,CAAC,WACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;EAEjD,MAAM,MAAM,sBAAsB,cAAc,QAAQ,GAAG,UACzD,aAAa,YAAY,KAAK,KAAK,CAAC,CACrC;AACD,MAAI,QAAQ,KACV,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;AAEjD,SAAO;GAAE,IAAI;GAAM;GAAK;;CAM1B,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,kBAAkB,mBAAmB,EAAE,uBAAuB,CAAC;UACzE,KAAc;AAErB,UAAQ,gBAAgB,EAAE,KAAK,2CADf,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,IACqB,CAAC;;CAMxF,MAAM,2BAA2B;CACjC,IAAI,oBAA2D;AAC/D,qBAAoB,kBAAkB;AACpC,MAAI,oBAAoB,KACtB,WAAU,mBAAmB;IAE9B,yBAAyB;AAC5B,mBAAkB,OAAO;CAEzB,MAAM,SAAS,kBAAkB;EAG/B,YAAY,OAAO;EACnB;EACA;EAEA,uBAAuB,OAAO,mBAAmB;EAGjD,yBAAyB;EACzB,IAAI,eAAe;AACjB,UAAO;;EAET;EAKA,qBAAqB,QAAQ,IAAI;EAGjC,mBAAmB,UAAU;AAC3B,qBAAkB;AAClB,aAAU,mBAAmB;;EAEhC,CAAC;CAEF,MAAM,YAAY,IAAI,sBAAsB;CAgB5C,IAAI,SAAS;CACb,IAAI,gBAAyC;CAC7C,IAAI,iBAA0C;CAE9C,MAAM,iBAAiB;AAErB,MAAI,OAAQ;AACZ,WAAS;AAET,iBAAe,MAAM;AACrB,kBAAgB,MAAM;AACtB,MAAI,kBAAmB,eAAc,kBAAkB;AACvD,SAAO,aAAa;AAIpB,OAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AAEtD,SAAO,OAAO;AACd,YAAU,OAAO;AAEtB,aAAW,SAAS;;AAItB,SAAQ,KAAK,UAAU,SAAS;AAChC,SAAQ,KAAK,WAAW,SAAS;AAEjC,SAAQ,KAAK,UAAU,SAAS;AAIhC,SAAQ,GAAG,cAAc;AACvB,MAAI,CAAC,QAAQ;AACX,YAAS;AACT,kBAAe,MAAM;AACrB,mBAAgB,MAAM;AACtB,OAAI,kBAAmB,eAAc,kBAAkB;AACvD,UAAO,aAAa;AACpB,QAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AAE3D,cAAW,SAAS;;GAEtB;AAKF,SAAQ,GAAG,sBAAsB,QAAQ;AACvC,WAAS,cAAc;GAAE,KAAK,sBAAsB,OAAO,IAAI;GAAI,WAAW;GAAY,CAAC;AAC3F,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,SAAQ,GAAG,uBAAuB,WAAW;AAC3C,WAAS,cAAc;GACrB,KAAK,uBAAuB,OAAO,OAAO;GAC1C,WAAW;GACZ,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,OAAM,OAAO,QAAQ,UAAU;AAK/B,QAAO,MAAM,OAAO;AAUpB,KAAI,QAAQ,IAAI,8BAA8B,KAAK;AACjD,kBAAgB,yBACR;AACJ,aAAU;AACV,WAAQ,KAAK,EAAE;KAEjB,EAAE,YAAY,KAAO,CACtB;AAED,UAAQ,MAAM,KAAK,aAAa;AAC9B,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;AACF,UAAQ,MAAM,KAAK,eAAe;AAChC,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;;AAUJ,KAAI,QAAQ,IAAI,yBAAyB,IAIvC,kBAAiB,0BACT;AACJ,UAAQ,OAAO,MACb,8FACD;AACD,YAAU;AACV,UAAQ,KAAK,EAAE;IAEjB,EAAE,UAXa,QAAQ,IAAI,uBACzB,OAAO,SAAS,QAAQ,IAAI,sBAAsB,GAAG,IAAI,KAAA,IACzD,KAAA,GASU,CACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDL,eAAsB,oBAAoB,UAAsC,EAAE,EAAiB;CAGjG,MAAM,aAAa,YAAY,EAAE,OAAO,QAAQ,SAAS,OAAO,CAAC;CAEjE,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,oBAAoB;CAOjE,MAAM,0BAA0B,YAAmC;EACjE,MAAM,WAAW,MAAM,eAAe;GAAE,MAAM;GAAS;GAAQ,CAAC;AAGhE,QAAM,IAAI,SAAe,MAAM,WAAW,GAAG,IAAI,CAAC;EAClD,MAAM,kBAAkB,IAAI,mBAAmB,EAAE,iBAAiB,SAAS,aAAa,CAAC;AACzF,SAAO;GACL,YAAY;GACZ,OAAO;AACL,oBAAgB,OAAO;AACvB,aAAS,MAAM;;GAElB;;CAMH,MAAM,iBAAiB,IAAI,oBAAoB;CAC/C,MAAM,uBAAuB,IAAI,8BAA8B;CAK/D,IAAI,uBAAsC;CAE1C,MAAM,SAAS,IAAI,qBAAqB;EAOtC,aAAa,OAAO,KAAK,gBACvB,QAAQ,kBACJ,wBACE,MAAM,uBAAuB,QAAQ,KAAK,YAAY,EACtD,MAAM,kBAAkB,QAAQ,KAAK,YAAY,CAClD,GACD,QAAQ,kBACN,yBAAyB,GACzB,gBAAgB;GACd,YAAY,sBAAsB;GAClC,WAAW,WAAW;AACpB,eAAW,aAAa,OAAO;AAC/B,cAAU,mBAAmB;;GAI/B,mBAAmB,QAAQ;AACzB,2BAAuB;AACvB,eAAW,qBAAqB,IAAI;;GAGtC,oBAAoB,qBAAqB,kBAAkB;GAC5D,CAAC;EACV;EACA;EACA,oBAAoB,UAAU,mBAAmB;EAEjD,6BAA6B,UAAU,sBAAsB;EAC9D,CAAC;CAIF,MAAM,YAAY,IAAI,uBAAuB;AAI3C,SAHe,OAAO;GAItB;CAKF,IAAI,kBAAyC;CAE7C,MAAM,0BAA0C;EAC9C,MAAM,UAAU,OAAO,OAAO,aAAa;EAG3C,MAAM,eAAe,UAAU,sBAAsB;AACrD,SAAO;GACL,QAAQ;IAAE,IAAI,OAAO,mBAAmB,CAAC;IAAI,QAAQ,OAAO,mBAAmB,CAAC;IAAQ;GACxF,OAAO,QAAQ,KAAK,OAAO;IAAE,IAAI,EAAE;IAAI,KAAK,EAAE;IAAK,EAAE;GACrD,WAAW,kBAAkB,iBAAiB,gBAAgB,GAAG;GACjE;GACD;;CAOH,MAAM,8BAED;EACH,MAAM,eAAe,OAAO;AAC5B,MAAI,CAAC,aACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAa;EAE3C,MAAM,UAAU,OAAO,OAAO,aAAa;AAC3C,MAAI,QAAQ,WAAW,EACrB,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAY;EAE1C,MAAM,aAAa,QAAQ,IAAI;AAC/B,MAAI,CAAC,WACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;EAEjD,MAAM,MAAM,sBAAsB,cAAc,QAAQ,GAAG,UACzD,aAAa,YAAY,KAAK,KAAK,CAAC,CACrC;AACD,MAAI,QAAQ,KACV,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;AAEjD,SAAO;GAAE,IAAI;GAAM;GAAK;;CAK1B,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,kBAAkB,mBAAmB,EAAE,uBAAuB,CAAC;UACzE,KAAc;AAErB,UAAQ,gBAAgB,EAAE,KAAK,2CADf,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,IACqB,CAAC;;CAQxF,MAAM,2BAA2B;CACjC,IAAI,oBAA2D;AAC/D,qBAAoB,kBAAkB;AACpC,MAAI,oBAAoB,KACtB,WAAU,mBAAmB;IAE9B,yBAAyB;AAC5B,mBAAkB,OAAO;CAEzB,MAAM,SAAS,kBAAkB;EAC/B,YAAY,OAAO;EACnB;EACA;EAIA,uBAAuB,OAAO,mBAAmB;EAGjD,yBAAyB;EACzB,IAAI,eAAe;AACjB,UAAO;;EAET;EAKA,qBAAqB,QAAQ,IAAI;EAEjC,mBAAmB,UAAU;AAC3B,qBAAkB;AAClB,aAAU,mBAAmB;;EAEhC,CAAC;CAEF,MAAM,YAAY,IAAI,sBAAsB;CAU5C,IAAI,SAAS;CACb,IAAI,gBAAyC;CAC7C,IAAI,iBAA0C;CAE9C,MAAM,iBAAiB;AACrB,MAAI,OAAQ;AACZ,WAAS;AACT,iBAAe,MAAM;AACrB,kBAAgB,MAAM;AACtB,MAAI,kBAAmB,eAAc,kBAAkB;AACvD,SAAO,aAAa;AAEpB,OAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AACtD,SAAO,OAAO;AACd,YAAU,OAAO;AAEtB,aAAW,SAAS;;AAGtB,SAAQ,KAAK,UAAU,SAAS;AAChC,SAAQ,KAAK,WAAW,SAAS;AACjC,SAAQ,KAAK,UAAU,SAAS;AAEhC,SAAQ,GAAG,cAAc;AACvB,MAAI,CAAC,QAAQ;AACX,YAAS;AACT,kBAAe,MAAM;AACrB,mBAAgB,MAAM;AACtB,OAAI,kBAAmB,eAAc,kBAAkB;AACvD,UAAO,aAAa;AACpB,QAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AAC3D,cAAW,SAAS;;GAEtB;AAEF,SAAQ,GAAG,sBAAsB,QAAQ;AACvC,WAAS,cAAc;GACrB,KAAK,sBAAsB,OAAO,IAAI;GACtC,WAAW;GACX,MAAM;GACP,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,SAAQ,GAAG,uBAAuB,WAAW;AAC3C,WAAS,cAAc;GACrB,KAAK,uBAAuB,OAAO,OAAO;GAC1C,WAAW;GACX,MAAM;GACP,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,OAAM,OAAO,QAAQ,UAAU;AAK/B,QAAO,MAAM,OAAO;AAGpB,KAAI,QAAQ,IAAI,8BAA8B,KAAK;AACjD,kBAAgB,yBACR;AACJ,aAAU;AACV,WAAQ,KAAK,EAAE;KAEjB,EAAE,YAAY,KAAO,CACtB;AACD,UAAQ,MAAM,KAAK,aAAa;AAC9B,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;AACF,UAAQ,MAAM,KAAK,eAAe;AAChC,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;;AAIJ,KAAI,QAAQ,IAAI,yBAAyB,IAIvC,kBAAiB,0BACT;AACJ,UAAQ,OAAO,MACb,8FACD;AACD,YAAU;AACV,UAAQ,KAAK,EAAE;IAEjB,EAAE,UAXa,QAAQ,IAAI,uBACzB,OAAO,SAAS,QAAQ,IAAI,sBAAsB,GAAG,IAAI,KAAA,IACzD,KAAA,GASU,CACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CL,eAAsB,qBACpB,UAAuC,EAAE,EAC1B;CAOf,MAAM,eAAe,MAAM,uBACzB,QAAQ,KACR,QAAQ,eAAe,QAAQ,KAAK,CACrC;CAID,MAAM,aAAa,YAAY,EAAE,OAAO,QAAQ,SAAS,OAAO,CAAC;CAMjE,MAAM,iBAAiB,IAAI,oBAAoB;CAC/C,MAAM,uBAAuB,IAAI,8BAA8B;CAK/D,IAAI,uBAAsC;CAE1C,MAAM,SAAS,IAAI,qBAAqB;EAOtC,aAAa,OAAO,QAClB,QAAQ,kBACJ,wBACE,cACA,MAAM,kBAAkB,QAAQ,KAAK,QAAQ,eAAe,QAAQ,KAAK,CAAC,CAC3E,GACD,QAAQ,kBACN,iBAAiB,GACjB,gBAAgB;GACd,YAAY,sBAAsB;GAClC,WAAW,WAAW;AACpB,eAAW,aAAa,OAAO;AAC/B,cAAU,mBAAmB;;GAI/B,mBAAmB,QAAQ;AACzB,2BAAuB;AACvB,eAAW,qBAAqB,IAAI;;GAGtC,oBAAoB,qBAAqB,kBAAkB;GAC5D,CAAC;EACV;EACA;EACA,oBAAoB,UAAU,mBAAmB;EAEjD,6BAA6B,UAAU,sBAAsB;EAC9D,CAAC;CAKF,MAAM,YAAY,IAAI,uBAAuB;AAI3C,SAHe,OAAO;GAItB;CAKF,IAAI,kBAAyC;CAE7C,MAAM,0BAA0C;EAC9C,MAAM,UAAU,OAAO,OAAO,aAAa;EAG3C,MAAM,eAAe,UAAU,sBAAsB;AACrD,SAAO;GACL,QAAQ;IAAE,IAAI,OAAO,mBAAmB,CAAC;IAAI,QAAQ,OAAO,mBAAmB,CAAC;IAAQ;GACxF,OAAO,QAAQ,KAAK,OAAO;IAAE,IAAI,EAAE;IAAI,KAAK,EAAE;IAAK,EAAE;GACrD,WAAW,kBAAkB,iBAAiB,gBAAgB,GAAG;GACjE;GACD;;CAOH,MAAM,8BAED;EACH,MAAM,eAAe,OAAO;AAC5B,MAAI,CAAC,aACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAa;EAE3C,MAAM,UAAU,OAAO,OAAO,aAAa;AAC3C,MAAI,QAAQ,WAAW,EACrB,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAY;EAE1C,MAAM,aAAa,QAAQ,IAAI;AAC/B,MAAI,CAAC,WACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;EAEjD,MAAM,MAAM,sBAAsB,cAAc,QAAQ,GAAG,UACzD,aAAa,YAAY,KAAK,KAAK,CAAC,CACrC;AACD,MAAI,QAAQ,KACV,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;AAEjD,SAAO;GAAE,IAAI;GAAM;GAAK;;CAK1B,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,kBAAkB,mBAAmB,EAAE,uBAAuB,CAAC;UACzE,KAAc;AAErB,UAAQ,gBAAgB,EAAE,KAAK,2CADf,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,IACqB,CAAC;;CAMxF,MAAM,2BAA2B;CACjC,IAAI,oBAA2D;AAC/D,qBAAoB,kBAAkB;AACpC,MAAI,oBAAoB,KACtB,WAAU,mBAAmB;IAE9B,yBAAyB;AAC5B,mBAAkB,OAAO;CAEzB,MAAM,SAAS,kBAAkB;EAC/B,YAAY,OAAO;EACnB;EACA;EAIA,uBAAuB,OAAO,mBAAmB;EAGjD,yBAAyB;EACzB,IAAI,eAAe;AACjB,UAAO;;EAET;EAKA,qBAAqB,QAAQ,IAAI;EAEjC,mBAAmB,UAAU;AAC3B,qBAAkB;AAClB,aAAU,mBAAmB;;EAEhC,CAAC;CAEF,MAAM,YAAY,IAAI,sBAAsB;CAW5C,IAAI,SAAS;CACb,IAAI,gBAAyC;CAC7C,IAAI,iBAA0C;CAE9C,MAAM,iBAAiB;AACrB,MAAI,OAAQ;AACZ,WAAS;AACT,iBAAe,MAAM;AACrB,kBAAgB,MAAM;AACtB,MAAI,kBAAmB,eAAc,kBAAkB;AACvD,SAAO,aAAa;AACpB,OAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AACtD,SAAO,OAAO;AACd,YAAU,OAAO;AACtB,aAAW,SAAS;;AAGtB,SAAQ,KAAK,UAAU,SAAS;AAChC,SAAQ,KAAK,WAAW,SAAS;AACjC,SAAQ,KAAK,UAAU,SAAS;AAEhC,SAAQ,GAAG,cAAc;AACvB,MAAI,CAAC,QAAQ;AACX,YAAS;AACT,kBAAe,MAAM;AACrB,mBAAgB,MAAM;AACtB,OAAI,kBAAmB,eAAc,kBAAkB;AACvD,UAAO,aAAa;AACpB,QAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AAC3D,cAAW,SAAS;;GAEtB;AAEF,SAAQ,GAAG,sBAAsB,QAAQ;AACvC,WAAS,cAAc;GACrB,KAAK,sBAAsB,OAAO,IAAI;GACtC,WAAW;GACX,MAAM;GACP,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,SAAQ,GAAG,uBAAuB,WAAW;AAC3C,WAAS,cAAc;GACrB,KAAK,uBAAuB,OAAO,OAAO;GAC1C,WAAW;GACX,MAAM;GACP,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,OAAM,OAAO,QAAQ,UAAU;AAK/B,QAAO,MAAM,OAAO;AAGpB,KAAI,QAAQ,IAAI,8BAA8B,KAAK;AACjD,kBAAgB,yBACR;AACJ,aAAU;AACV,WAAQ,KAAK,EAAE;KAEjB,EAAE,YAAY,KAAO,CACtB;AACD,UAAQ,MAAM,KAAK,aAAa;AAC9B,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;AACF,UAAQ,MAAM,KAAK,eAAe;AAChC,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;;AAIJ,KAAI,QAAQ,IAAI,yBAAyB,IAIvC,kBAAiB,0BACT;AACJ,UAAQ,OAAO,MACb,8FACD;AACD,YAAU;AACV,UAAQ,KAAK,EAAE;IAEjB,EAAE,UAXa,QAAQ,IAAI,uBACzB,OAAO,SAAS,QAAQ,IAAI,sBAAsB,GAAG,IAAI,KAAA,IACzD,KAAA,GASU,CACb;;;;ACl1GL,SAAS,SAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU;;AAGhD,IAAa,gBAAb,MAAgD;CAC9C;CACA;CAEA,YAAY,SAA+B;AACzC,OAAK,gBAAgB,QAAQ;AAC7B,OAAK,YAAY,QAAQ,eAAe,QAAQ,MAAM,IAAI;;CAG5D,MAAc,aAAoC;EAChD,MAAM,MAAM,MAAM,KAAK,UAAU,KAAK,cAAc;AACpD,MAAI,CAAC,IAAI,GACP,OAAM,IAAI,MACR,mCAAmC,KAAK,cAAc,SAAS,IAAI,OAAO,GAAG,IAAI,WAAW,kGAE7F;EAEH,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,SAAO,SAAS,KAAK,GAAG,OAAO,EAAE;;CAGnC,MAAM,IAA6B,QAAqC;AACtE,UAAQ,QAAR;GACE,KAAK,mBAEH,QADc,MAAM,KAAK,YAAY;GAGvC,KAAK,iCAAiC;IACpC,MAAM,QAAQ,MAAM,KAAK,YAAY;AAIrC,WAD0C;KAAE,aAFxB,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc;KAEvB,YADtC,OAAO,MAAM,eAAe,WAAW,MAAM,aAAa;KACR;;GAGvE,KAAK,yBAAyB;IAI5B,MAAM,OADQ,MAAM,KAAK,YAAY,EACnB;AAGlB,WADkC,EAAE,OADtB,MAAM,QAAQ,IAAI,GAAI,MAAqC,EAAE,EAChC;;GAG7C,QACE,OAAM,IAAI,MAAM,uBAAuB,OAAO,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjBhE,MAAM,8BACJ;;;;;;;;;;;;AAeF,MAAM,uBAAuB;CAI3B;EACE,MAAM;EACN,aACE;EAIF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAEF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAEF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAEF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CAID;EACE,MAAM;EACN,aACE;EAIF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAIF,aAAa;GACX,MAAM;GACN,YAAY,EACV,qBAAqB;IACnB,MAAM;IACN,aAAa;IACd,EACF;GACD,UAAU,EAAE;GACb;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAIF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAIF,aAAa;GACX,MAAM;GACN,YAAY;IACV,MAAM;KACJ,MAAM;KACN,aAAa;KACd;IACD,MAAM;KACJ,MAAM;KACN,aAAa;KACb,OAAO,EAAE;KACV;IACF;GACD,UAAU,CAAC,OAAO;GACnB;EACD,aAAa;EACd;CAQD;EACE,MAAM;EACN,aACE;EAKF,aAAa;GACX,MAAM;GACN,YAAY;IACV,YAAY;KACV,MAAM;KACN,aAAa;KACd;IACD,iBAAiB;KACf,MAAM;KACN,aAAa;KACd;IACD,sBAAsB;KACpB,MAAM;KACN,aACE;KAEH;IACF;GACD,UAAU,CAAC,aAAa;GACzB;EACD,aAAa;EACd;CAKD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GACX,MAAM;GACN,YAAY,EACV,YAAY;IAAE,MAAM;IAAU,aAAa;IAAsC,EAClF;GACD,UAAU,CAAC,aAAa;GACzB;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GACX,MAAM;GACN,YAAY,EACV,OAAO;IAAE,MAAM;IAAU,aAAa;IAAiC,EACxE;GACD,UAAU,EAAE;GACb;EACD,aAAa;EACd;CACF;;AAGD,MAAM,iBAAiB,IAAI,IAAY,qBAAqB,KAAK,MAAM,EAAE,KAAK,CAAC;;AAG/E,MAAM,sBAAsB,IAAI,IAAY;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;AAOF,MAAM,oBAAoB,IAAI,IAAY,CAAC,mBAAmB,CAAC;;;;;AAW/D,SAAS,wBAAwB,aAAqB;AACpD,QAAO;EACL,OAAO,CACL;GACE,KAAK;GACL,OAAO;GACP,UAAU;GACX,CACF;EACD,QAAQ,EAAE,IAAI,OAAO;EACrB,SAAS;EACT,mBAAmB;EACpB;;;;;;AAOH,eAAe,oBACb,aACA,eACA,WACkC;CAClC,IAAI,YAAY;CAChB,IAAI,iBAAgC;CACpC,IAAI,cAA6B;AAEjC,KAAI;EACF,MAAM,MAAM,MAAM,UAAU,cAAc;AAC1C,cAAY,IAAI;AAChB,iCAAc,IAAI,MAAM,EAAC,aAAa;AACtC,MAAI,CAAC,IAAI,GACP,kBAAiB,QAAQ,IAAI,OAAO,GAAG,IAAI;UAEtC,KAAK;AACZ,mBAAiB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AACjE,iCAAc,IAAI,MAAM,EAAC,aAAa;;AAGxC,QAAO;EACL,MAAM;EACN;EACA,kBAAkB;EAClB,4BAA4B;EAC5B;EACA;EACA,aAAa;GACX,MAAM;GACN,QAAQ;GACT;EACD,uBAAuB,YACnB,OACA;EACL;;;;;;;AAQH,eAAe,wBAAwB,WAAwD;CAK7F,MAAM,aAJQ,MAAM,UAAU,IAAI,mBAAmB,EAI/B;CACtB,IAAI,YAAiF;AACrF,KAAI,cAAc,QAAQ,OAAO,cAAc,YAAY,CAAC,MAAM,QAAQ,UAAU,EAAE;EACpF,MAAM,IAAI;AACV,cAAY;GACV,KAAK,OAAO,EAAE,QAAQ,WAAW,EAAE,MAAM;GACzC,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;GAC/C,QAAQ,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;GAClD,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;GAC7C;;AAGH,QAAO;EACL,QAAQ;EAER,QAAQ;GAAE,KAAK;GAAG,OAAO;GAAG,QAAQ;GAAG,MAAM;GAAG;EAChD;EACA,iBAAiB,cAAc,OAAO,iBAAiB;EACvD,GAAI,cAAc,OACd,EAAE,gBAAgB,sEAAsE,GACxF,EAAE;EAEN,YAAY;EACZ,aAAa;EACb,kBAAkB;EAClB,WAAW;EACX,cAAc;EACd,oBAAoB;EACrB;;;;;;;;;AAUH,eAAe,gBACb,YACA,WACkC;AAClC,SAAQ,YAAR;EACE,KAAK,4BAEH,QAAO;GACL,IAAI;GACJ,QAHU,MAAM,UAAU,IAAI,gCAAgC,EAGnD;GACZ;EAEH,QAEE,QAAO;GACL,IAAI;GACJ,OACE,0BAA0B,WAAW;GAIxC;;;;AAMP,SAAgB,gBAAgB,OAA4B,EAAE,EAAU;CACtE,MAAM,cAAc,QAAQ,IAAI,oBAAoB;CACpD,MAAM,gBAAgB,GAAG,YAAY;CACrC,MAAM,YAAY,KAAK,aAAa,IAAI,cAAc,EAAE,eAAe,CAAC;CAExE,MAAM,SAAS,IAAI,OACjB;EAAE,MAAM;EAAgB,SAAA;EAAsB,EAC9C,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,EAAE,CAChC;AAED,QAAO,kBAAkB,+BAA+B,EACtD,OAAO,qBAAqB,KAAK,UAAU,EAAE,GAAG,MAAM,EAAE,EACzD,EAAE;AAEH,QAAO,kBAAkB,uBAAuB,OAAO,YAAY;EACjE,MAAM,OAAO,QAAQ,OAAO;AAC5B,MAAI,CAAC,eAAe,IAAI,KAAK,CAC3B,QAAO,SAAS,gBAAgB,OAAO;AAIzC,MAAI,oBAAoB,IAAI,KAAK,CAC/B,QAAO,SAAS,GAAG,KAAK,IAAI,8BAA8B;AAM5D,MAAI,kBAAkB,IAAI,KAAK,CAC7B,QAAO,mBACL,MACA,SACA,QACA,sIAED;AAGH,MAAI;GAEF,MAAM,YAAY,SAAS,4BAA4B,qBAAqB;AAG5E,OAAI,cAAc,UAAU,CAC1B,SAAQ,WAAR;IACE,KAAK,mBACH,QAAO,WAAW,MAAM,aAAa,UAAU,CAAC;IAClD,KAAK,gCACH,QAAO,WAAW,MAAM,0BAA0B,UAAU,CAAC;IAC/D,KAAK,wBACH,QAAO,WAAW,MAAM,kBAAkB,UAAU,CAAC;IACvD,QACE,QAAO,SAAS,gBAAgB,OAAO;;AAO7C,WAAQ,MAAR;IACE,KAAK,aACH,QAAO,eAAe,cAAc,wBAAwB,YAAY,CAAC;IAE3E,KAAK,mBACH,QAAO,eACL,oBACA,MAAM,oBAAoB,aAAa,gBAAgB,QAAQ,MAAM,IAAI,CAAC,CAC3E;IAEH,KAAK,oBACH,QAAO,eAAe,qBAAqB,MAAM,wBAAwB,UAAU,CAAC;IAEtF,KAAK,YAAY;KACf,MAAM,UAAU,QAAQ,OAAO,WAAW;AAC1C,SAAI,OAAO,YAAY,YAAY,YAAY,GAC7C,QAAO,SACL,iDACD;AAEH,YAAO,eAAe,YAAY,MAAM,gBAAgB,SAAS,UAAU,CAAC;;IAG9E,QACE,QAAO,SAAS,gBAAgB,OAAO;;WAEpC,KAAK;AAEZ,UAAO,SACL,GAAG,KAAK,OAFM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAEvC,qHAGxB;;GAEH;AAEF,QAAO;;AAGT,SAAS,WAAW,OAAgB;AAClC,QAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,OAAO,MAAM,EAAE;EAAE,CAAC,EAAE;;;;;;;;;;;AAYvF,SAAS,eAAe,MAAc,OAAgB;CACpD,MAAM,UAAU,aAAa,OAAO;EAAE;EAAM,KAAK;EAAQ,UAAU;EAAM,CAAC;AAC1E,QAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE;EAAE,CAAC,EAAE;;;AAIzF,eAAsB,eAA8B;CAClD,MAAM,SAAS,iBAAiB;CAChC,MAAM,YAAY,IAAI,sBAAsB;AAC5C,OAAM,OAAO,QAAQ,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjiBjC,SAAgB,sBAAsB,MAAyB,QAAQ,KAAW;AAChF,KAAI,IAAI,YAAY,aAAc,eAAc,KAAK;;;;;;;;AAYvD,SAAgB,WAAW,MAAkC;AAC3D,QAAO,KAAK,SAAS,UAAU,IAAI,KAAK,SAAS,aAAa;;;AAIhE,SAAgB,UAAU,MAA+B;AACvD,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,KAAA,EAAW;AACvB,MAAI,IAAI,WAAW,UAAU,CAC3B,QAAO,cAAc,IAAI,MAAM,EAAiB,CAAC;AAEnD,MAAI,QAAQ,UAAU;GACpB,MAAM,OAAO,KAAK,IAAI;AACtB,OAAI,SAAS,KAAA,EACX,OAAM,IAAI,MAAM,uDAAuD;AAEzE,UAAO,cAAc,KAAK;;;AAG9B,QAAO;;;;;;;;;;AAWT,SAAgB,YAAY,MAAiC;AAC3D,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,KAAA,EAAW;AACvB,MAAI,IAAI,WAAW,YAAY,CAC7B,QAAO,gBAAgB,IAAI,MAAM,EAAmB,CAAC;AAEvD,MAAI,QAAQ,YAAY;GACtB,MAAM,OAAO,KAAK,IAAI;AACtB,OAAI,SAAS,KAAA,EACX,OAAM,IAAI,MAAM,sEAAsE;AAExF,UAAO,gBAAgB,KAAK;;;AAGhC,QAAO;;AAGT,SAAS,cAAc,OAAqB;AAC1C,KAAI,UAAU,MAAO,QAAO;AAC5B,KAAI,UAAU,QAAS,QAAO;AAC9B,OAAM,IAAI,MAAM,mBAAmB,MAAM,yCAAyC;;AAGpF,SAAS,gBAAgB,OAAuB;AAC9C,KAAI,UAAU,QAAS,QAAO;AAC9B,KAAI,UAAU,QAAS,QAAO;AAC9B,KAAI,UAAU,SAAU,QAAO;AAC/B,OAAM,IAAI,MAAM,qBAAqB,MAAM,sDAAsD;;AAGnG,eAAe,OAAsB;CACnC,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAElC,wBAAuB;AAEvB,KADa,UAAU,KAAK,KACf,MACX,OAAM,cAAc;MACf;EACL,MAAM,SAAS,YAAY,KAAK;EAChC,MAAM,QAAQ,WAAW,KAAK;AAC9B,MAAI,WAAW,QACb,OAAM,oBAAoB,EAAE,OAAO,CAAC;WAC3B,WAAW,SACpB,OAAM,qBAAqB,EAAE,OAAO,CAAC;MAErC,OAAM,eAAe,EAAE,OAAO,CAAC;;;;;;;;;;;;;AAerC,SAAS,eAAwB;CAC/B,MAAM,QAAQ,KAAK;AACnB,KAAI,UAAU,KAAA,EAAW,QAAO;AAChC,KAAI;AACF,SAAO,cAAc,OAAO,KAAK,IAAI,KAAK,aAAa,MAAM;SACvD;AACN,SAAO;;;AAIX,IAAI,cAAc,CAChB,OAAM,CAAC,OAAO,QAAiB;CAC7B,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,SAAQ,OAAO,MAAM,yBAAyB,QAAQ,IAAI;AAC1D,SAAQ,WAAW;EACnB"}
1
+ {"version":3,"file":"cli.js","names":["isPidAlive","isObject","DEFAULT_BUFFER_SIZE","isObject","parseInbound","PHASE_1_EVENTS","require","isObject","_isPidAlive","isObject","jsonResult","envelopeResult"],"sources":["../../src/shared/parent-watcher.ts","../../src/mcp/ait-chii-source.ts","../../src/shared/relay-auth-close.ts","../../src/mcp/log.ts","../../src/mcp/chii-connection.ts","../../src/mcp/chii-relay.ts","../../src/mcp/deeplink.ts","../../src/mcp/devtools-opener.ts","../../src/mcp/envelope.ts","../../src/mcp/environment.ts","../../src/mcp/errors.ts","../../src/mcp/local-connection.ts","../../src/mcp/local-launcher.ts","../../src/i18n/en.ts","../../src/i18n/ko.ts","../../src/i18n/index.ts","../../src/mcp/dashboard.generated.ts","../../src/mcp/qr-http-server.ts","../../src/mcp/server-lock.ts","../../src/mcp/sdk-signatures.ts","../../src/mcp/tools.ts","../../src/mcp/tunnel.ts","../../src/mcp/debug-server.ts","../../src/mcp/ait-http-source.ts","../../src/mcp/server.ts","../../src/mcp/cli.ts"],"sourcesContent":["/**\n * Shared parent-PID watcher — used by both the MCP debug daemon and the\n * unplugin tunnel path to self-terminate when the parent process (e.g. Claude\n * Code, vite) has died or been reparented without sending SIGTERM/SIGHUP.\n *\n * Intentionally react-free and Node-stdlib-only so this module is safe to\n * import from the MCP daemon bundle (`dist/mcp/cli.js`) without violating the\n * install-graph invariant.\n */\n\n// ---------------------------------------------------------------------------\n// isPidAlive — extracted from src/mcp/server-lock.ts\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when the given PID refers to a running process.\n *\n * Uses `process.kill(pid, 0)` — a no-op signal that succeeds when the process\n * exists and we have permission to signal it; throws ESRCH when it doesn't exist.\n */\nexport function isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err: unknown) {\n // ESRCH = no such process → stale lock.\n // EPERM = process exists but we can't signal it (still alive).\n if ((err as NodeJS.ErrnoException).code === 'EPERM') return true;\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// startParentWatcher — extracted from src/mcp/debug-server.ts\n// ---------------------------------------------------------------------------\n\n/**\n * Starts a periodic watcher that detects when the parent process (e.g. Claude\n * Code) has died without sending SIGTERM/SIGHUP, and calls `onOrphaned` so the\n * daemon can self-terminate rather than running as a zombie.\n *\n * Mirrors the `startAttachWatcher` pattern: `setInterval`-based, returns\n * `{ stop(): void }`, injectable deps for testability.\n *\n * @param onOrphaned - Called once when the parent is gone.\n * @param opts.intervalMs - Poll interval in milliseconds (default 5 000).\n * @param opts.initialPpid - Parent PID to watch (default `process.ppid`).\n * @param opts.isAlive - Predicate to test if a PID is running (default `isPidAlive`).\n * @param opts.getPpid - Supplier of current ppid (default `() => process.ppid`).\n * Detects ppid changes as well as death.\n * @param opts.log - Logger (default `process.stderr.write`).\n *\n * @returns `stop` — call during shutdown to clear the interval.\n */\nexport function startParentWatcher(\n onOrphaned: () => void,\n opts?: {\n intervalMs?: number;\n initialPpid?: number;\n isAlive?: (pid: number) => boolean;\n getPpid?: () => number;\n log?: (msg: string) => void;\n },\n): { stop(): void } {\n const {\n intervalMs = 5_000,\n initialPpid = process.ppid,\n isAlive = isPidAlive,\n getPpid = () => process.ppid,\n log = (msg: string) => process.stderr.write(msg),\n } = opts ?? {};\n\n // PID 1 is init/launchd — running under a process manager or as a detached\n // daemon. There is no meaningful parent to watch; skip the watcher entirely.\n if (initialPpid <= 1) {\n log('[ait-debug] parent-pid watcher: no parent to watch (ppid<=1), skipping\\n');\n return { stop() {} };\n }\n\n let fired = false;\n\n const handle = setInterval(() => {\n if (fired) return;\n\n const currentPpid = getPpid();\n const orphaned = currentPpid !== initialPpid || !isAlive(initialPpid);\n\n if (orphaned) {\n fired = true;\n clearInterval(handle);\n log(\n `[ait-debug] parent-pid watcher: parent PID ${initialPpid} is gone (currentPpid=${currentPpid}) — shutting down\\n`,\n );\n onOrphaned();\n }\n }, intervalMs);\n\n return {\n stop() {\n clearInterval(handle);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// startMaxAgeWatchdog — FIX 4: daemon lifetime cap\n// ---------------------------------------------------------------------------\n\n/**\n * Starts a periodic watchdog that calls `onExpired` once after `maxAgeMs`\n * milliseconds have elapsed since the watchdog was created.\n *\n * Motivation (issue #571): cloudflared quick-tunnel lifetimes are finite (a\n * few hours). A daemon that has been running for days will have outlived its\n * tunnel regardless of whether the tunnel process exited cleanly. This watchdog\n * caps the daemon's maximum age and forces a fresh start so the tunnel is\n * replaced before it silently expires.\n *\n * @param onExpired - Called once when the maximum age is reached. The caller\n * should call `shutdown()` then `process.exit(0)`.\n * @param opts.maxAgeMs - Maximum daemon lifetime in ms. Default 6 h.\n * @param opts.intervalMs - Check interval in ms. Default 60 000 (1 min).\n * @param opts.now - Time source (injectable for tests). Default `Date.now`.\n *\n * @returns `stop` — call during shutdown to clear the interval.\n */\nexport function startMaxAgeWatchdog(\n onExpired: () => void,\n opts: {\n maxAgeMs?: number;\n intervalMs?: number;\n now?: () => number;\n } = {},\n): { stop(): void } {\n const {\n maxAgeMs = 6 * 60 * 60 * 1_000, // 6 hours\n intervalMs = 60_000,\n now = () => Date.now(),\n } = opts;\n\n const startedAt = now();\n let fired = false;\n\n const handle = setInterval(() => {\n if (fired) return;\n if (now() - startedAt >= maxAgeMs) {\n fired = true;\n clearInterval(handle);\n onExpired();\n }\n }, intervalMs);\n\n return {\n stop() {\n clearInterval(handle);\n },\n };\n}\n","/**\n * Debug-mode `AitSource` — forwards `AIT.*` methods over the Chii channel.\n *\n * The AIT domain (`AIT.getSdkCallHistory` / `getMockState` /\n * `getOperationalEnvironment`) is non-standard CDP: the in-app side registers a\n * handler for these methods and answers them over the same Chii websocket the\n * CDP commands use. Building the AIT source on `ChiiCdpConnection.sendCommand`\n * means both domains share one transport (spec: \"the same MCP server forwards\n * both CDP and AIT domains\").\n *\n * The in-app `AIT.*` handler lives downstream in sdk-example. Here we build\n * the MCP-server-side forwarding + the injectable seam; tests inject a fake\n * `AitSource` returning canned responses, so this forwarding layer needs no\n * phone.\n *\n * Node-only (wraps the relay websocket connection).\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** The slice of `ChiiCdpConnection` this source needs (keeps it testable). */\nexport interface AitCommandSender {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Narrows an `AIT.getSdkCallHistory` response, tolerating a missing array. */\nfunction asSdkCallHistory(raw: unknown): AitSdkCallHistory {\n if (isObject(raw) && Array.isArray(raw.calls)) {\n return { calls: raw.calls as AitSdkCallHistory['calls'] };\n }\n return { calls: [] };\n}\n\n/** Narrows an `AIT.getMockState` response to an opaque record. */\nfunction asMockState(raw: unknown): AitMockState {\n return isObject(raw) ? raw : {};\n}\n\n/** Narrows an `AIT.getOperationalEnvironment` response. */\nfunction asOperationalEnvironment(raw: unknown): AitOperationalEnvironment {\n const environment =\n isObject(raw) && typeof raw.environment === 'string' ? raw.environment : 'unknown';\n const sdkVersion = isObject(raw) && typeof raw.sdkVersion === 'string' ? raw.sdkVersion : null;\n return { environment, sdkVersion };\n}\n\nexport class ChiiAitSource implements AitSource {\n constructor(private readonly sender: AitCommandSender) {}\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n const raw = await this.sender.sendCommand(method);\n // The map's value type is resolved per-key below; the cast is the single\n // narrowing point (each branch returns the precise shape for `method`).\n switch (method) {\n case 'AIT.getSdkCallHistory':\n return asSdkCallHistory(raw) as AitMethodMap[M];\n case 'AIT.getMockState':\n return asMockState(raw) as AitMethodMap[M];\n case 'AIT.getOperationalEnvironment':\n return asOperationalEnvironment(raw) as AitMethodMap[M];\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * Shared constants for the relay's named TOTP-auth rejection (issue #478).\n *\n * Before #478 the relay rejected an unauthenticated WebSocket upgrade with a\n * raw `HTTP/1.1 401` + `socket.destroy()`. A handshake aborted that way is\n * indistinguishable from a network failure on the browser side — the\n * WebSocket only ever sees close code 1006, so the phone (env-2 launcher PWA)\n * could not tell \"stale TOTP code\" apart from \"tunnel down\" and stayed\n * silent. The fix is accept-then-close: complete the handshake, then close\n * with an application close code that NAMES the rejection.\n *\n * Three parties share this contract:\n * - `src/mcp/chii-relay.ts` (Node) sends the close frame / HTTP error body;\n * - `src/in-app/attach.ts` (browser) observes relay-bound WebSockets and\n * surfaces the code to the launcher shell;\n * - `src/mcp/chii-connection.ts` (Node daemon client) recognises the code\n * as an auth failure on its own `/client` dial (defensive — #439's fresh\n * code mint means it should not normally hit this).\n *\n * This module is intentionally dependency-free (no Node, no DOM) so it is\n * safe to import from both the browser in-app bundle and the MCP daemon\n * bundle.\n *\n * SECRET-HANDLING: these are fixed enum values. The close reason / error body\n * must never grow to carry a secret, a TOTP code, or a host.\n */\n\n/**\n * WebSocket close code sent by the relay when TOTP auth is rejected.\n *\n * 4000–4999 is the application-reserved range (RFC 6455 §7.4.2); 4401 mirrors\n * HTTP 401 so it reads as \"unauthorized\" at a glance.\n */\nexport const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;\n\n/**\n * Close reason string accompanying {@link RELAY_AUTH_REJECT_CLOSE_CODE}, and\n * the `error` value of the relay's HTTP 401 JSON body. Enum string only —\n * never interpolated with request data.\n */\nexport const RELAY_AUTH_REJECT_REASON = 'totp-rejected';\n","/**\n * Structured JSON-line server logger + allowlist-based secret redact.\n *\n * Every log line emitted by the debug-mode MCP server is a single JSON object:\n * { \"ts\": \"<ISO-8601>\", \"level\": \"info\"|\"warn\"|\"error\", \"event\": \"<category>\", ...fields }\n *\n * Allowlist approach — only the keys in ALLOWED_KEYS pass through to the output\n * object unchanged. Any value that matches a known-secret pattern is replaced\n * with \"***\" regardless of key name. This provides two complementary layers:\n * 1. Key allowlist — unknown keys (e.g. a future field accidentally containing\n * a credential) are dropped entirely.\n * 2. Value redact — pattern matching catches secrets that slip through under\n * an allowed key name (e.g. a message string that includes a TOTP code).\n *\n * SECRET-HANDLING (MUST NOT appear in stdout/stderr/logs):\n * - TOTP 6-digit codes (pattern: standalone 6-digit run)\n * - AITCC_API_KEY values (pattern: \"aitcc_\" or \"AITCC_\" prefix — Deploy Key format)\n * - cookie header values (pattern: \"cookie:\" header content)\n * - relay WSS URLs (contain the relay host which is semi-sensitive)\n * - \"at=<TOTP>\" query params\n *\n * Canonical event categories:\n * server.start — MCP server started (relay port, TOTP enabled, etc.)\n * tunnel.up — cloudflared tunnel assigned a public URL\n * tunnel.down — tunnel error / shutdown\n * page.attached — first CDP target appeared (deploymentId, env)\n * page.detached — target evicted / session replaced\n * page.crashed — target crash detected\n * tool.call — MCP tool invocation (tool name only — no args/results)\n * tool.error — MCP tool error (tool name + safe error category)\n */\n\n/** Structured log levels. */\nexport type LogLevel = 'info' | 'warn' | 'error';\n\n/** Every valid event category. */\nexport type LogEvent =\n | 'server.start'\n | 'tunnel.up'\n | 'tunnel.down'\n | 'page.attached'\n | 'page.detached'\n | 'page.crashed'\n | 'tool.call'\n | 'tool.error';\n\n/**\n * Allowed field keys that may pass through to a log line.\n * Unknown keys are dropped. Values are still redact-scanned.\n */\nconst ALLOWED_KEYS = new Set([\n 'ts',\n 'level',\n 'event',\n 'msg',\n 'port',\n 'totpEnabled',\n 'env',\n 'tool',\n 'deploymentId',\n 'errorKind',\n 'reason',\n 'prevTargetId',\n 'mode',\n]);\n\n/**\n * Patterns that match secret values.\n * Match order matters — more-specific patterns first.\n *\n * #268 redact script covers: relay=wss://…, at=<TOTP>, _deploymentId=<uuid>.\n * Here we extend to in-process value-level patterns used in server logs.\n */\nconst SECRET_PATTERNS: RegExp[] = [\n // TOTP 6-digit code as a standalone value (whole string is exactly 6 digits).\n /^\\d{6}$/,\n // Deploy Key — AITCC_API_KEY value prefix formats.\n /^(aitcc_|AITCC_)/i,\n // Cookie header value (whole string starts with a cookie-like name=value pair).\n /^[A-Za-z0-9_-]+=.{4,}/,\n // WSS relay URL value.\n /^wss:\\/\\//,\n // TOTP \"at=\" query param embedded in a string.\n /(?:^|[?&])at=[A-Z0-9]{6}/i,\n];\n\n/**\n * Returns `true` when the string value matches any known-secret pattern.\n * Only string values are tested — numbers/booleans are always safe.\n */\nfunction isSecretValue(value: string): boolean {\n return SECRET_PATTERNS.some((re) => re.test(value));\n}\n\n/**\n * Redacts a single scalar value.\n * - strings: return \"***\" if the value matches a secret pattern.\n * - other: return as-is.\n */\nfunction redactValue(value: unknown): unknown {\n if (typeof value === 'string' && isSecretValue(value)) {\n return '***';\n }\n return value;\n}\n\n/**\n * Builds a safe log payload from raw fields.\n *\n * - Only keys in `ALLOWED_KEYS` are included.\n * - String values are scanned for secret patterns and replaced with \"***\".\n * - `ts` and `level` and `event` are always included (they are injected by the\n * logger functions below, not by callers).\n */\nfunction buildPayload(\n level: LogLevel,\n event: LogEvent,\n fields: Record<string, unknown>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {\n ts: new Date().toISOString(),\n level,\n event,\n };\n\n for (const [key, value] of Object.entries(fields)) {\n if (!ALLOWED_KEYS.has(key)) continue;\n // ts/level/event are controlled above.\n if (key === 'ts' || key === 'level' || key === 'event') continue;\n out[key] = redactValue(value);\n }\n\n return out;\n}\n\n/**\n * Writes a single JSON log line to stderr.\n * MCP stdio transport uses stdout; all diagnostics go to stderr.\n */\nfunction writeLog(level: LogLevel, event: LogEvent, fields: Record<string, unknown> = {}): void {\n const payload = buildPayload(level, event, fields);\n process.stderr.write(`${JSON.stringify(payload)}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// Public logger functions — one per level.\n// ---------------------------------------------------------------------------\n\n/** Log an informational structured event. */\nexport function logInfo(event: LogEvent, fields: Record<string, unknown> = {}): void {\n writeLog('info', event, fields);\n}\n\n/** Log a warning structured event. */\nexport function logWarn(event: LogEvent, fields: Record<string, unknown> = {}): void {\n writeLog('warn', event, fields);\n}\n\n/** Log an error structured event. */\nexport function logError(event: LogEvent, fields: Record<string, unknown> = {}): void {\n writeLog('error', event, fields);\n}\n\n// ---------------------------------------------------------------------------\n// Exported redact helper for use in tests and callers that need to sanitise\n// before passing to the logger (e.g. error message strings).\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a redacted copy of `value`:\n * - string: \"***\" if it matches a secret pattern, otherwise the original.\n * - other types: returned as-is.\n *\n * Exposed for unit tests and for callers that build dynamic `msg` strings.\n */\nexport function redact(value: unknown): unknown {\n return redactValue(value);\n}\n","/**\n * Production `CdpConnection` backed by the local Chii relay.\n *\n * Topology (debug mode):\n * phone target.js --WS--> Chii relay :9100 <--WS-- this connection\n *\n * The phone connects to the relay as a `target`; this module connects as a\n * `client` (the role a CDP frontend would take) so CDP events the page emits\n * (`Runtime.consoleAPICalled`, `Network.*`) flow back here. We buffer recent\n * events in ring buffers the tool layer reads via `getBufferedEvents`.\n *\n * Node-only: imports `ws`. Never bundled into the browser/in-app entries.\n *\n * Attach reliability (#281):\n * `refreshTargets()` emits an internal 'target:attached' event whenever a\n * new target is added to the relay. `waitForFirstTarget()` awaits that event\n * (with a polling-interval fallback) so `build_attach_url wait_for_attach`\n * resolves deterministically rather than racing between polling rounds.\n */\n\nimport { EventEmitter } from 'node:events';\nimport { WebSocket } from 'ws';\nimport { RELAY_AUTH_REJECT_CLOSE_CODE } from '../shared/relay-auth-close.js';\nimport type {\n CdpCommandMap,\n CdpCommandName,\n CdpConnection,\n CdpEventMap,\n CdpEventName,\n CdpTarget,\n} from './cdp-connection.js';\nimport { logInfo } from './log.js';\nimport { generateTotp } from './totp.js';\n\n/** Max events retained per domain ring buffer. */\nconst DEFAULT_BUFFER_SIZE = 500;\n\n/** A CDP message arriving over the relay websocket. */\ninterface CdpInboundMessage {\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { message: string };\n}\n\n/**\n * Events emitted by `ChiiCdpConnection` for crash / lifecycle notifications.\n * Consumers (e.g. the tool layer) can subscribe with `.onLifecycle(cb)`.\n */\nexport interface TargetLifecycleEvent {\n /**\n * 'crashed' → Inspector.targetCrashed\n * 'destroyed' → Target.targetDestroyed\n * 'detached' → Target.detachedFromTarget\n * 'replaced' → evicted by single-attach model (last-attach wins)\n */\n kind: 'crashed' | 'destroyed' | 'detached' | 'replaced';\n targetId: string | null;\n /** ISO timestamp of detection. */\n detectedAt: string;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction parseInbound(raw: string): CdpInboundMessage | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isObject(parsed)) return null;\n const message: CdpInboundMessage = {};\n if (typeof parsed.id === 'number') message.id = parsed.id;\n if (typeof parsed.method === 'string') message.method = parsed.method;\n if ('params' in parsed) message.params = parsed.params;\n if ('result' in parsed) message.result = parsed.result;\n if (isObject(parsed.error) && typeof parsed.error.message === 'string') {\n message.error = { message: parsed.error.message };\n }\n return message;\n}\n\nconst PHASE_1_EVENTS: readonly CdpEventName[] = [\n 'Runtime.consoleAPICalled',\n 'Network.requestWillBeSent',\n 'Network.responseReceived',\n];\n\n/**\n * Ring buffer size for `Runtime.exceptionThrown`.\n *\n * Exceptions are rarer than console messages but each is heavier (stack\n * trace). 50 is generous enough to cover a crash scenario while keeping\n * memory bounded.\n *\n * **Lifecycle note**: the exception buffer intentionally survives `replaced` /\n * `crashed` / `destroyed` lifecycle events — it is NOT cleared on target\n * transitions. Rationale: an exception fired just before a crash is exactly\n * the signal we want to preserve for root-cause analysis. The buffer\n * represents \"exceptions seen in this MCP session\", not \"exceptions in the\n * current page\".\n */\nconst EXCEPTION_BUFFER_SIZE = 50;\n\nexport interface ChiiCdpConnectionOptions {\n /** Base URL of the local Chii relay HTTP/WS server, e.g. `http://127.0.0.1:9100`. */\n relayBaseUrl: string;\n /** Per-domain ring buffer size. */\n bufferSize?: number;\n /**\n * Default per-command timeout in milliseconds.\n * Override via env `AIT_CDP_COMMAND_TIMEOUT_MS`.\n * Defaults to 30 000 ms (30s).\n */\n commandTimeoutMs?: number;\n /**\n * Hex-encoded TOTP secret (the SECRET, never a code). When set, each client WS\n * (re)connect mints a fresh `at=` code so it lands inside the relay's 90s\n * acceptance window. Leave undefined when the relay has TOTP disabled.\n * SECRET-HANDLING: stored privately, never logged; the minted code rides only\n * in the WS URL query.\n */\n totpSecret?: string;\n}\n\n/** Default per-command timeout if neither option nor env var is set. */\nconst DEFAULT_COMMAND_TIMEOUT_MS = 30_000;\n\n/**\n * Production CDP connection. Polls the relay for the first attached target,\n * opens a client websocket to it, enables Phase 1 domains, and buffers events.\n */\nexport class ChiiCdpConnection implements CdpConnection {\n /** Authoritative connection kind (issue #348) — relay-backed. */\n readonly kind = 'relay' as const;\n\n private readonly relayBaseUrl: string;\n private readonly bufferSize: number;\n private readonly commandTimeoutMs: number;\n private readonly totpSecret: string | undefined;\n private readonly emitter = new EventEmitter();\n private readonly buffers = new Map<CdpEventName, unknown[]>();\n private readonly targets = new Map<string, CdpTarget>();\n\n private ws: WebSocket | null = null;\n private connectionState: 'idle' | 'connected' | 'disconnected' = 'idle';\n private nextCommandId = 1;\n /**\n * The single active target id under the single-attach model.\n * Updated by `refreshTargets()` whenever a non-null target is present.\n * Used to detect a new (different) target attach and evict the previous one.\n */\n private activeTargetId: string | null = null;\n /** In-flight enableDomains() promise — concurrent callers share it. */\n private enablingPromise: Promise<void> | null = null;\n /** Pending request→response commands keyed by CDP message id. */\n private readonly pending = new Map<\n number,\n { resolve: (result: unknown) => void; reject: (err: Error) => void }\n >();\n\n /**\n * Timestamp (ms since epoch) of the most recent crash/destroy/detach event,\n * or `null` if no crash has been detected since the last `enableDomains()`.\n */\n private lastCrashDetectedAt: number | null = null;\n\n /**\n * Per-target last-seen timestamp (ms since epoch). Updated on any inbound\n * CDP message carrying data from a target. Keyed by target id.\n */\n private readonly targetLastSeenAt = new Map<string, number>();\n\n /** Active heartbeat interval handle (only when `AIT_CDP_HEARTBEAT_MS` is set). */\n private heartbeatHandle: ReturnType<typeof setInterval> | null = null;\n\n /** Lifecycle event listeners (crash / destroyed / detached). */\n private readonly lifecycleListeners: Array<(event: TargetLifecycleEvent) => void> = [];\n\n constructor(options: ChiiCdpConnectionOptions) {\n this.relayBaseUrl = options.relayBaseUrl.replace(/\\/$/, '');\n this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;\n this.totpSecret = options.totpSecret;\n const envMs = process.env.AIT_CDP_COMMAND_TIMEOUT_MS\n ? Number(process.env.AIT_CDP_COMMAND_TIMEOUT_MS)\n : undefined;\n this.commandTimeoutMs =\n (envMs !== undefined && Number.isFinite(envMs) && envMs > 0 ? envMs : undefined) ??\n options.commandTimeoutMs ??\n DEFAULT_COMMAND_TIMEOUT_MS;\n for (const event of PHASE_1_EVENTS) this.buffers.set(event, []);\n // Exception buffer initialized separately — its per-event size cap\n // (EXCEPTION_BUFFER_SIZE=50) is enforced in handleMessage below.\n this.buffers.set('Runtime.exceptionThrown', []);\n // EventEmitter caps listeners at 10 by default; the tool layer may add\n // several short-lived subscriptions, so lift the cap.\n this.emitter.setMaxListeners(0);\n }\n\n /** Refresh the attached-target list from the relay's `GET /targets`. */\n async refreshTargets(): Promise<CdpTarget[]> {\n // When TOTP is active, append a freshly-minted code as `?at=<code>` so the\n // relay's /targets gate (issue #474) accepts this daemon poll. Mirrors the\n // existing /client WS pattern (~line 412). `generateTotp` defaults `when` to\n // Date.now() — rely on that default; never hand-compute the time.\n // SECRET-HANDLING: never log `code` or `this.totpSecret`; the code rides only\n // in the at= param.\n let targetsUrl = `${this.relayBaseUrl}/targets`;\n if (this.totpSecret) {\n const code = generateTotp(this.totpSecret);\n targetsUrl += `?at=${encodeURIComponent(code)}`;\n }\n const res = await fetch(targetsUrl);\n if (!res.ok) {\n throw new Error(`Chii relay /targets returned HTTP ${res.status} ${res.statusText}`);\n }\n const body: unknown = await res.json();\n const list = isObject(body) && Array.isArray(body.targets) ? body.targets : [];\n\n // Single-attach model: find the \"newest\" target id from the relay response.\n // The relay may return multiple targets if the previous session did not cleanly\n // detach. We keep only the last entry (last-attach wins) and evict the previous\n // active target if it differs.\n let newestTargetId: string | null = null;\n for (const item of list) {\n if (!isObject(item) || typeof item.id !== 'string') continue;\n newestTargetId = item.id; // last wins\n }\n\n // Evict previous active target when a genuinely new targetId arrives.\n if (\n newestTargetId !== null &&\n this.activeTargetId !== null &&\n newestTargetId !== this.activeTargetId\n ) {\n const prevId = this.activeTargetId;\n // SECRET-HANDLING: prevTargetId is a Chii internal ID (not a secret) but\n // keep it short — no URL or credentials logged here.\n logInfo('page.detached', { prevTargetId: prevId });\n this.evictTarget(prevId);\n }\n\n // Rebuild the targets map with at most the single newest target.\n this.targets.clear();\n for (const item of list) {\n if (!isObject(item) || typeof item.id !== 'string') continue;\n // Single-attach model: only register the newest target.\n if (item.id !== newestTargetId) continue;\n this.targets.set(item.id, {\n id: item.id,\n title: typeof item.title === 'string' ? item.title : '',\n url: typeof item.url === 'string' ? item.url : '',\n });\n }\n\n if (newestTargetId !== null) {\n this.activeTargetId = newestTargetId;\n } else {\n this.activeTargetId = null;\n }\n\n const result = [...this.targets.values()];\n\n // Emit 'target:attached' for every newly-seen target so waitForFirstTarget()\n // can race against the next refreshTargets() polling round.\n if (newestTargetId !== null) {\n this.emitter.emit('target:attached', result);\n }\n\n return result;\n }\n\n listTargets(): CdpTarget[] {\n return [...this.targets.values()];\n }\n\n /**\n * Waits until at least one target matching `filterFn` is attached, then\n * resolves with the full target list at that moment.\n *\n * Resolution happens on whichever comes first:\n * (a) a `'target:attached'` event from `refreshTargets()` (triggered by\n * the /targets poll finding a new target), OR\n * (b) a `'target:attached'` event from `handleMessage()` (triggered by\n * the first inbound CDP message from a target — confirms the relay\n * websocket has data from the phone, not just a target entry in the map).\n *\n * This dual-signal approach eliminates the polling race that previously\n * caused `wait_for_attach` to resolve before the first CDP message arrived.\n *\n * Falls back to checking `listTargets()` every `pollIntervalMs` in case the\n * EventEmitter is missed (defensive belt-and-suspenders).\n *\n * @param filterFn - Predicate that the returned targets must satisfy.\n * @param timeoutMs - Reject after this many ms (default 90 000).\n * @param pollIntervalMs - Fallback poll interval (default 500ms).\n */\n waitForFirstTarget(\n filterFn: (targets: CdpTarget[]) => boolean,\n timeoutMs = 90_000,\n pollIntervalMs = 500,\n ): Promise<CdpTarget[]> {\n // Fast path: already attached.\n const current = this.listTargets();\n if (filterFn(current)) return Promise.resolve(current);\n\n return new Promise<CdpTarget[]>((resolve, reject) => {\n let settled = false;\n let pollHandle: ReturnType<typeof setInterval> | null = null;\n\n const settle = (targets: CdpTarget[]): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutHandle);\n if (pollHandle !== null) {\n clearInterval(pollHandle);\n pollHandle = null;\n }\n this.emitter.off('target:attached', onAttach);\n resolve(targets);\n };\n\n const onAttach = (targets: CdpTarget[]): void => {\n if (filterFn(targets)) settle(targets);\n };\n\n const timeoutHandle = setTimeout(() => {\n if (settled) return;\n settled = true;\n if (pollHandle !== null) {\n clearInterval(pollHandle);\n pollHandle = null;\n }\n this.emitter.off('target:attached', onAttach);\n reject(\n new Error(\n `waitForFirstTarget: 타임아웃 (${timeoutMs}ms) — 폰이 relay에 attach되지 않았습니다.`,\n ),\n );\n }, timeoutMs);\n\n // Primary: event-driven path.\n this.emitter.on('target:attached', onAttach);\n\n // Fallback: polling path — also calls refreshTargets() to keep the in-memory\n // target map up-to-date. This ensures the polling path works even without\n // a live WebSocket (pre-enableDomains) and catches targets that appear\n // between 'target:attached' events.\n pollHandle = setInterval(() => {\n // Refresh from relay, then check. Errors are ignored — we keep polling.\n this.refreshTargets().then(\n (targets) => {\n if (filterFn(targets)) settle(targets);\n },\n () => {\n // Relay temporarily unreachable — keep polling.\n },\n );\n }, pollIntervalMs);\n });\n }\n\n /**\n * Timestamp (ms since epoch) of the most recent crash/destroy/detach event\n * detected since the last `enableDomains()` call, or `null` if none.\n */\n getLastCrashDetectedAt(): number | null {\n return this.lastCrashDetectedAt;\n }\n\n /**\n * Last-seen timestamp (ms since epoch) for a given target id, or `null` if\n * the target is unknown / no message has been received from it yet.\n */\n getTargetLastSeenAt(targetId: string): number | null {\n return this.targetLastSeenAt.get(targetId) ?? null;\n }\n\n /** Subscribe to target lifecycle events (crash / destroyed / detached). */\n onLifecycle(listener: (event: TargetLifecycleEvent) => void): () => void {\n this.lifecycleListeners.push(listener);\n return () => {\n const idx = this.lifecycleListeners.indexOf(listener);\n if (idx !== -1) this.lifecycleListeners.splice(idx, 1);\n };\n }\n\n /**\n * Connect a client websocket to the first attached target and enable Phase 1\n * domains. Resolves once the socket is open and enable commands are sent.\n */\n async enableDomains(): Promise<void> {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) return;\n // If a connect attempt is already in-flight, await it rather than racing\n // to open a second websocket that would overwrite `this.ws` and leak the first.\n if (this.enablingPromise) return this.enablingPromise;\n this.enablingPromise = this._doEnableDomains().finally(() => {\n this.enablingPromise = null;\n });\n return this.enablingPromise;\n }\n\n private async _doEnableDomains(): Promise<void> {\n const targets = await this.refreshTargets();\n const target = targets[0];\n if (!target) {\n throw new Error('No mini-app page attached to the Chii relay yet.');\n }\n\n const wsBase = this.relayBaseUrl.replace(/^http/, 'ws');\n const clientId = `devtools-mcp-${Date.now()}`;\n let clientUrl = `${wsBase}/client/${clientId}?target=${encodeURIComponent(target.id)}`;\n // Append a freshly-minted TOTP code so the relay's WS upgrade gate\n // (chii-relay.ts verifyAuth) accepts this /client upgrade. Minted per-connect\n // so reconnects stay inside the 90s acceptance window. generateTotp defaults\n // `when` to Date.now() — rely on that default; never hand-compute the time.\n // SECRET-HANDLING: never log `code` or `this.totpSecret`; the code rides only\n // in the URL query.\n if (this.totpSecret) {\n const code = generateTotp(this.totpSecret);\n clientUrl += `&at=${encodeURIComponent(code)}`;\n }\n const ws = new WebSocket(clientUrl);\n this.ws = ws;\n\n await new Promise<void>((resolve, reject) => {\n ws.once('open', () => resolve());\n ws.once('error', (err: Error) => reject(err));\n // Issue #478: the relay rejects auth with accept-then-close (4401)\n // instead of a raw 401 destroy, so a rejected dial no longer surfaces\n // as an 'error' event. 'open' always precedes the close frame, making\n // this reject a settled-promise no-op in practice — kept as a defensive\n // boundary (and for any relay that closes before open). The post-open\n // 4401 is recognised by the persistent close handler below.\n ws.once('close', (code: number) => {\n if (code === RELAY_AUTH_REJECT_CLOSE_CODE) {\n reject(\n new Error(\n 'relay 인증(TOTP)이 거부됐습니다 (close 4401). 코드가 만료됐을 수 있습니다 — 재연결 시 새 코드가 발급됩니다.',\n ),\n );\n }\n });\n });\n\n // Reset crash state when a new connection is established.\n this.lastCrashDetectedAt = null;\n this.targetLastSeenAt.clear();\n // activeTargetId is already set by refreshTargets() above; don't reset here.\n\n this.connectionState = 'connected';\n ws.on('message', (data: WebSocket.RawData) => this.handleMessage(data.toString()));\n // Issue #478: close 4401 is the relay's named TOTP rejection\n // (accept-then-close) — name it as an auth failure instead of a generic\n // drop. #439's per-connect fresh code mint means this should not happen in\n // practice; defensive alignment with the relay contract.\n ws.on('close', (code: number) =>\n this.handleDisconnect(\n code === RELAY_AUTH_REJECT_CLOSE_CODE\n ? 'relay 인증(TOTP)이 거부돼 연결이 종료됐습니다 (close 4401)'\n : 'relay WebSocket 연결이 끊겼습니다',\n ),\n );\n ws.on('error', (err: Error) => this.handleDisconnect(`relay WebSocket 오류: ${err.message}`));\n\n this.sendFireAndForget('Runtime.enable');\n this.sendFireAndForget('Network.enable');\n // DOM/Page domains back the Phase 2 command tools; Chii answers their\n // request→response commands once enabled.\n this.sendFireAndForget('DOM.enable');\n this.sendFireAndForget('Page.enable');\n // Subscribe to page-level crash and target lifecycle events.\n // Inspector.targetCrashed fires when a page OOM/JS-crash/native-bridge crash.\n // Target.setDiscoverTargets enables Target.targetDestroyed + Target.detachedFromTarget.\n this.sendFireAndForget('Inspector.enable');\n this.sendFireAndForget('Target.setDiscoverTargets', { discover: true });\n\n // Optional heartbeat: env AIT_CDP_HEARTBEAT_MS=N enables a ping loop.\n this.startHeartbeat(target.id);\n }\n\n /** Fire-and-forget CDP message (used for `*.enable`, no result awaited). */\n private sendFireAndForget(method: string, params: Record<string, unknown> = {}): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n const id = this.nextCommandId++;\n this.ws.send(JSON.stringify({ id, method, params }));\n }\n\n /**\n * Issue a CDP command and resolve with its result (Phase 2). Rejects on a CDP\n * error frame or when no websocket is open (no page attached yet).\n */\n send<M extends CdpCommandName>(\n method: M,\n params?: CdpCommandMap[M]['params'],\n ): Promise<CdpCommandMap[M]['result']> {\n return this.sendCommand(method, (params ?? {}) as Record<string, unknown>) as Promise<\n CdpCommandMap[M]['result']\n >;\n }\n\n /**\n * Issue an arbitrary request→response command over the relay and resolve with\n * its raw result. Both the typed CDP {@link send} and the AIT domain (Phase 3\n * `AIT.*` methods, forwarded over the same Chii channel) build on this.\n *\n * Rejects immediately if the connection is disconnected (fail-fast — no\n * auto-reconnect). Caller should re-run `list_pages` or `enableDomains` to\n * reattach.\n *\n * Times out after `commandTimeoutMs` (default 30s, env\n * `AIT_CDP_COMMAND_TIMEOUT_MS`). On timeout the pending entry is cleaned up\n * and the promise rejects with a descriptive Korean error.\n */\n sendCommand(method: string, params: Record<string, unknown> = {}): Promise<unknown> {\n // Fail-fast: connection already known to be dead — don't write into a dead socket.\n if (this.connectionState === 'disconnected') {\n return Promise.reject(\n new Error(\n `relay에 연결되어 있지 않습니다 (${method}). list_pages로 attach 상태를 확인하고 enableDomains()로 재연결하세요.`,\n ),\n );\n }\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n return Promise.reject(\n new Error('No mini-app page attached to the Chii relay yet. Call enableDomains() first.'),\n );\n }\n const id = this.nextCommandId++;\n const ws = this.ws;\n const timeoutMs = this.commandTimeoutMs;\n return new Promise<unknown>((resolve, reject) => {\n const handle = setTimeout(() => {\n this.pending.delete(id);\n reject(\n new Error(\n `CDP 명령이 타임아웃됐습니다 (${method}, ${timeoutMs}ms). ` +\n '폰 측 토스 앱이 백그라운드로 내려갔거나 미니앱이 unload됐을 수 있습니다. ' +\n 'list_pages로 attach 상태를 확인하세요.',\n ),\n );\n }, timeoutMs);\n this.pending.set(id, {\n resolve: (v) => {\n clearTimeout(handle);\n resolve(v);\n },\n reject: (e) => {\n clearTimeout(handle);\n reject(e);\n },\n });\n ws.send(JSON.stringify({ id, method, params }));\n });\n }\n\n /**\n * Called on WebSocket `close` or `error` after a successful connection.\n * Rejects all pending commands and marks the connection as disconnected so\n * subsequent `sendCommand` calls fail fast (no auto-reconnect).\n */\n private handleDisconnect(reason: string): void {\n if (this.connectionState === 'disconnected') return; // already handled\n this.connectionState = 'disconnected';\n this.ws = null;\n this.stopHeartbeat();\n const err = new Error(\n `${reason}. list_pages로 attach 상태를 확인하고 enableDomains()로 재연결하세요.`,\n );\n for (const waiter of this.pending.values()) {\n waiter.reject(err);\n }\n this.pending.clear();\n }\n\n /**\n * Evict a previously active target under the single-attach model.\n * Rejects pending commands with a 'replaced-by-new-attach' reason and emits\n * a 'replaced' lifecycle event. Does NOT clear all targets — only the specific\n * targetId. The caller is responsible for rebuilding the targets map afterwards.\n *\n * The error message uses 'replaced-by-new-attach' so test assertions can match it.\n */\n private evictTarget(targetId: string): void {\n const detectedAt = new Date().toISOString();\n this.targets.delete(targetId);\n this.targetLastSeenAt.delete(targetId);\n\n const err = new Error(\n `[ait-debug] replaced-by-new-attach — 이전 page 세션이 새 attach로 교체됐습니다 (targetId=${targetId}). ` +\n 'list_pages로 현재 attach 상태를 확인하세요.',\n );\n for (const waiter of this.pending.values()) {\n waiter.reject(err);\n }\n this.pending.clear();\n\n const event: TargetLifecycleEvent = { kind: 'replaced', targetId, detectedAt };\n for (const listener of this.lifecycleListeners) {\n try {\n listener(event);\n } catch {\n // Listeners must not crash the connection.\n }\n }\n }\n\n /**\n * Handle a page-level crash or target destruction event.\n * Removes the target from the in-memory map, rejects all pending commands,\n * and emits a lifecycle event.\n *\n * @param kind - Event kind: 'crashed' | 'destroyed' | 'detached'\n * @param targetId - The target ID from the event params (may be null for\n * Inspector.targetCrashed which has no targetId in the params).\n */\n private handleTargetGone(kind: TargetLifecycleEvent['kind'], targetId: string | null): void {\n const detectedAt = new Date().toISOString();\n this.lastCrashDetectedAt = Date.now();\n\n // Remove matching target(s) from the in-memory map.\n if (targetId !== null) {\n this.targets.delete(targetId);\n this.targetLastSeenAt.delete(targetId);\n // Also clear activeTargetId when the active target is gone.\n if (this.activeTargetId === targetId) {\n this.activeTargetId = null;\n }\n } else {\n // Inspector.targetCrashed carries no targetId — clear all targets.\n this.targets.clear();\n this.targetLastSeenAt.clear();\n this.activeTargetId = null;\n }\n\n // Reject pending commands with a descriptive Korean error.\n const label =\n kind === 'crashed'\n ? 'page crash (Inspector.targetCrashed)'\n : kind === 'destroyed'\n ? 'target 종료 (Target.targetDestroyed)'\n : 'target detach (Target.detachedFromTarget)';\n const err = new Error(\n `[ait-debug] ${label} 감지됨 — relay에서 제거됐습니다. ` +\n '새 attach가 필요합니다 (list_pages로 확인 → enableDomains()로 재연결).',\n );\n for (const waiter of this.pending.values()) {\n waiter.reject(err);\n }\n this.pending.clear();\n\n // Notify lifecycle listeners.\n const event: TargetLifecycleEvent = { kind, targetId, detectedAt };\n for (const listener of this.lifecycleListeners) {\n try {\n listener(event);\n } catch {\n // Listeners must not crash the connection.\n }\n }\n }\n\n /**\n * Start the optional CDP heartbeat loop.\n *\n * When `AIT_CDP_HEARTBEAT_MS` is set to a positive integer, every interval\n * we send `Runtime.evaluate({expression: '1'})` to each active target. If\n * the command times out (2 s hard deadline) or errors, we treat the target\n * as dead and call `handleTargetGone`.\n *\n * This is a zombie-detector fallback: cloudflared keeps-alive the tunnel ws\n * even when the phone app has crashed, so the ws-level disconnect (#252) won't\n * fire. The heartbeat catches this gap.\n *\n * Default: OFF. Only activates when `AIT_CDP_HEARTBEAT_MS` is set.\n */\n private startHeartbeat(initialTargetId: string): void {\n this.stopHeartbeat(); // clear any previous interval\n\n const envMs = process.env.AIT_CDP_HEARTBEAT_MS\n ? Number(process.env.AIT_CDP_HEARTBEAT_MS)\n : undefined;\n if (envMs === undefined || !Number.isFinite(envMs) || envMs <= 0) return;\n\n const PING_TIMEOUT_MS = 2_000;\n\n this.heartbeatHandle = setInterval(() => {\n // Take a snapshot of current targets to avoid mutation during iteration.\n const targetIds = this.targets.size > 0 ? [...this.targets.keys()] : [initialTargetId];\n for (const targetId of targetIds) {\n // Issue a lightweight eval with a 2 s deadline.\n const pingPromise = this.sendCommand('Runtime.evaluate', {\n expression: '1',\n returnByValue: true,\n timeout: PING_TIMEOUT_MS,\n });\n const timeoutPromise = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error('heartbeat timeout')),\n PING_TIMEOUT_MS + 500, // slightly longer than the CDP timeout\n ),\n );\n Promise.race([pingPromise, timeoutPromise]).catch(() => {\n // Ping failed: mark target as dead if it still exists in the map.\n if (this.targets.has(targetId)) {\n this.handleTargetGone('destroyed', targetId);\n }\n });\n }\n }, envMs) as unknown as ReturnType<typeof setInterval>;\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatHandle !== null) {\n clearInterval(this.heartbeatHandle);\n this.heartbeatHandle = null;\n }\n }\n\n private handleMessage(raw: string): void {\n const message = parseInbound(raw);\n if (!message) return;\n\n // Command response (has an id matching a pending request).\n if (typeof message.id === 'number' && this.pending.has(message.id)) {\n const waiter = this.pending.get(message.id);\n this.pending.delete(message.id);\n if (waiter) {\n if (message.error) waiter.reject(new Error(message.error.message));\n else waiter.resolve(message.result);\n }\n return;\n }\n\n // Any inbound message implies the connection is active — update lastSeenAt\n // for whichever target we currently know about (single-target model).\n // Also emit 'target:attached' on the first inbound message from a target\n // (targetLastSeenAt unset) so waitForFirstTarget() resolves on first CDP\n // message, not just on the next /targets poll.\n const now = Date.now();\n let firstMessageSeen = false;\n for (const targetId of this.targets.keys()) {\n if (!this.targetLastSeenAt.has(targetId)) {\n firstMessageSeen = true;\n }\n this.targetLastSeenAt.set(targetId, now);\n }\n if (firstMessageSeen && this.targets.size > 0) {\n this.emitter.emit('target:attached', [...this.targets.values()]);\n }\n\n if (typeof message.method !== 'string') return;\n\n // --- Target lifecycle events ---\n\n // Inspector.targetCrashed: page OOM / JS exception / native bridge crash.\n // Params are usually empty; no targetId field in the event.\n if (message.method === 'Inspector.targetCrashed') {\n this.handleTargetGone('crashed', null);\n return;\n }\n\n // Target.targetDestroyed: params = { targetId: string }\n if (message.method === 'Target.targetDestroyed') {\n const targetId =\n isObject(message.params) && typeof message.params.targetId === 'string'\n ? message.params.targetId\n : null;\n this.handleTargetGone('destroyed', targetId);\n return;\n }\n\n // Target.detachedFromTarget: params = { sessionId, targetId? }\n if (message.method === 'Target.detachedFromTarget') {\n const targetId =\n isObject(message.params) && typeof message.params.targetId === 'string'\n ? message.params.targetId\n : null;\n this.handleTargetGone('detached', targetId);\n return;\n }\n\n // --- Phase 1 event stream (buffered ring-buffer) ---\n if (!this.buffers.has(message.method as CdpEventName)) return;\n const event = message.method as CdpEventName;\n const buffer = this.buffers.get(event);\n if (!buffer) return;\n buffer.push(message.params);\n // Runtime.exceptionThrown uses a dedicated smaller cap (50); all other\n // Phase 1 events use the default bufferSize (500).\n const cap = event === 'Runtime.exceptionThrown' ? EXCEPTION_BUFFER_SIZE : this.bufferSize;\n if (buffer.length > cap) buffer.shift();\n this.emitter.emit(event, message.params);\n }\n\n getBufferedEvents<E extends CdpEventName>(event: E): ReadonlyArray<CdpEventMap[E]> {\n const buffer = this.buffers.get(event);\n return (buffer ?? []) as ReadonlyArray<CdpEventMap[E]>;\n }\n\n on<E extends CdpEventName>(event: E, listener: (payload: CdpEventMap[E]) => void): () => void {\n this.emitter.on(event, listener as (payload: unknown) => void);\n return () => this.emitter.off(event, listener as (payload: unknown) => void);\n }\n\n /** Close the relay client websocket and reject any in-flight commands. */\n close(): void {\n const ws = this.ws;\n this.stopHeartbeat();\n // handleDisconnect clears this.ws and pending; call it first so the 'close'\n // event from ws.close() below is a no-op (already disconnected).\n this.handleDisconnect('Chii relay connection closed');\n ws?.close();\n }\n}\n","/**\n * Boots the local Chii relay server.\n *\n * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome\n * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.\n * The relay accepts a `target` websocket from the phone's injected `target.js`\n * and `client` websockets from CDP frontends (our MCP connection).\n *\n * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app\n * entries.\n *\n * TOTP auth (relay-side, authoritative gate):\n * When `verifyAuth` is provided, this module gates both inbound surfaces:\n *\n * - HTTP 'request': a listener registered BEFORE `chii.start({server})`.\n * Node's `http.Server` calls listeners in registration order; the first\n * to call `res.end()` wins. Invalid auth → 401 + CORS header + a tiny\n * JSON body (`{\"error\":\"totp-rejected\"}`) so a cross-origin script\n * `fetch()` probe can READ the status (issue #478). Valid auth → return\n * without side-effect (chii's Koa handler serves it).\n *\n * - WS 'upgrade': after `chii.start()` has registered chii's own upgrade\n * listener, we take over the upgrade chain (remove chii's listeners,\n * re-dispatch manually). Invalid auth → accept-then-close: complete the\n * handshake via a `noServer` WebSocketServer, then immediately close\n * with code 4401 reason 'totp-rejected' (issue #478). A raw 401 +\n * `socket.destroy()` only ever surfaced as close code 1006 in the\n * browser — indistinguishable from a tunnel failure, which left the\n * env-2 phone UI silent. The explicit dispatch (not listener ordering)\n * is what keeps chii away from rejected sockets: accept-then-close\n * leaves the socket alive, so an order-based early-return would let\n * chii's later listener complete a SECOND handshake on the same socket\n * — an auth bypass. Valid auth → forward to chii's captured listeners.\n *\n * TOTP code transports (issue #466) — two equivalent ways to carry the code:\n * 1. Query param `at=<code>` — used by the daemon-side `/client` connection\n * (`chii-connection.ts` appends it; it holds the secret).\n * 2. Path prefix `/at/<code>/…` — used by the phone-side target. Chii's\n * stock `target.js` derives its WS endpoint from the script `src`\n * (`scriptEl.src.replace('target.js','')`), so the only way for the\n * phone to carry a code is to embed it in the script URL path. The\n * in-app attach injects `https://<host>/at/<code>/target.js`; both the\n * script fetch and the derived `wss://<host>/at/<code>/target/<id>` WS\n * dial then carry the prefix. The listeners below rewrite the prefix\n * into the query form (`rewriteAtPathPrefix`) and MUTATE `req.url`\n * before chii's own handlers (registered later) parse it — chii only\n * ever sees the stripped URL.\n *\n * Threat model: \"URL leak\" — someone obtains the tunnel URL (Slack paste, QR\n * screenshot, shoulder-surfing) but does not have the shared TOTP secret.\n * Rotating 6-digit code makes the URL stale after 30 s.\n * A determined attacker who extracts the secret from the dogfood bundle can\n * still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).\n *\n * SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear\n * in any log, error message, or process output. `verifyAuth` is a black-box\n * predicate from the caller's perspective; this module only forwards pass/fail.\n */\n\nimport { createServer, type IncomingMessage, type Server } from 'node:http';\nimport { createRequire } from 'node:module';\nimport type { AddressInfo } from 'node:net';\nimport type { Duplex } from 'node:stream';\n// `ws` is a direct dependency of this package (NOT a transitive reach into\n// chii's tree — same principle as the ajv incident): the reject path below\n// needs `WebSocketServer.handleUpgrade` to complete a handshake we are about\n// to close with a named code.\nimport { type WebSocket, WebSocketServer } from 'ws';\nimport {\n RELAY_AUTH_REJECT_CLOSE_CODE,\n RELAY_AUTH_REJECT_REASON,\n} from '../shared/relay-auth-close.js';\n\nconst require = createRequire(import.meta.url);\n\n/**\n * WS keepalive ping interval (ms).\n *\n * Cloudflare proxied connections are dropped after ~100 s of no traffic.\n * 45 s comfortably fits inside that window and lets both the phone-target leg\n * and the daemon-client leg survive idle CDP sessions.\n */\nconst DEFAULT_KEEPALIVE_INTERVAL_MS = 45_000;\n\n/**\n * Minimal shape of chii's internal WebSocketServer instance.\n *\n * `chii/server/lib/WebSocketServer` holds the real `ws.Server` in `_wss`.\n * `_wss.clients` is the standard `Set<WebSocket>` tracking all live sockets.\n * We access this to ping every connected socket — no chii internals beyond\n * this single field are touched.\n */\ninterface ChiiInternalWss {\n _wss: { clients: Set<WebSocket> };\n start(server: import('node:http').Server): void;\n}\n\n/**\n * Loads chii's internal WebSocketServer class and returns it together with a\n * flag indicating whether the real class was found.\n *\n * Returns `null` if the internal path is not resolvable (future chii release\n * changes the layout) — callers skip keepalive gracefully.\n */\nfunction tryLoadChiiWssClass(): (new () => ChiiInternalWss) | null {\n try {\n const mod: unknown = require('chii/server/lib/WebSocketServer');\n if (typeof mod === 'function') {\n return mod as new () => ChiiInternalWss;\n }\n } catch {\n // Module not found or shape changed — keepalive will be skipped.\n }\n return null;\n}\n\n/**\n * Calls `chii.start()` and returns the chii `WebSocketServer` instance that\n * was constructed during the call.\n *\n * How: `chii/server/index.js`'s `start()` creates `new WebSocketServer()`\n * where `WebSocketServer` is captured from `require('./lib/WebSocketServer')`\n * at module load time. The class reference is stable, so we can temporarily\n * patch `ChiiWssClass.prototype.start` — which runs *on the instance* —\n * to record `this` before the original `start` runs.\n *\n * The patch is installed before `chii.start()` and removed (via `finally`)\n * immediately after, so concurrent `startChiiRelay` calls nest correctly: each\n * call's patch overrides the previous in the prototype chain for the duration\n * of its own `chii.start()` call, restoring the prior descriptor on exit.\n *\n * If `ChiiWssClass` is null (internal path changed in a future chii release),\n * `chii.start()` runs unpatched and the function returns null — callers skip\n * keepalive gracefully without affecting relay correctness.\n */\nasync function startChiiWithCapture(\n chii: ChiiServerModule,\n startOptions: Parameters<ChiiServerModule['start']>[0],\n ChiiWssClass: (new () => ChiiInternalWss) | null,\n): Promise<ChiiInternalWss | null> {\n if (ChiiWssClass === null) {\n await chii.start(startOptions);\n return null;\n }\n\n let captured: ChiiInternalWss | null = null;\n const proto = ChiiWssClass.prototype as ChiiInternalWss;\n const originalStart = proto.start;\n\n proto.start = function (this: ChiiInternalWss, server) {\n captured = this;\n return originalStart.call(this, server);\n };\n\n try {\n await chii.start(startOptions);\n } finally {\n // Always restore — even if chii.start() throws.\n proto.start = originalStart;\n }\n\n return captured;\n}\n\n/** `chii/server` is CommonJS and shipped without TypeScript types. */\ninterface ChiiServerModule {\n start(options: {\n port?: number;\n host?: string;\n domain?: string;\n server?: Server;\n basePath?: string;\n }): Promise<void>;\n}\n\nfunction loadChiiServer(): ChiiServerModule {\n // `chii`'s package `main` is `./server/index.js`, exposing `{ start }`.\n const mod: unknown = require('chii');\n if (\n typeof mod === 'object' &&\n mod !== null &&\n 'start' in mod &&\n typeof (mod as { start: unknown }).start === 'function'\n ) {\n return mod as ChiiServerModule;\n }\n throw new Error('chii server module did not expose start()');\n}\n\nexport interface ChiiRelay {\n port: number;\n /** Base URL for the relay HTTP/WS server, e.g. `http://127.0.0.1:54321`. */\n baseUrl: string;\n close(): Promise<void>;\n}\n\n/**\n * Secret-free metadata about a single auth rejection (issue #467).\n *\n * SECRET-HANDLING: this event carries ONLY the surface kind. It must never\n * grow fields for `req.url`, query strings, codes, or secrets — observers\n * (diagnostics counters, console hints) only need \"a rejection happened\".\n */\nexport interface RelayAuthRejectEvent {\n /** Which inbound surface was rejected. */\n kind: 'ws-upgrade' | 'http-request';\n}\n\n/**\n * Rewrites a `/at/<code>/…` path-prefixed request URL into the equivalent\n * query-based form, e.g.:\n *\n * `/at/123456/target.js` → `/target.js?at=123456`\n * `/at/123456/target/x?url=u` → `/target/x?url=u&at=123456`\n * `/at/123456/` → `/?at=123456`\n *\n * Returns `null` when the URL does not carry the prefix (including an empty\n * code segment) — callers fall back to the unmodified URL and the existing\n * query-based auth path.\n *\n * Pure string surgery — this function knows nothing about secrets or code\n * validity; verification stays inside the caller-provided `verifyAuth`\n * predicate (which parses the query). The raw path segment is appended\n * verbatim to the query: both path segments and query values are\n * percent-decoded exactly once by their consumers, so no re-encoding is\n * needed (TOTP codes are 6 digits and never percent-encoded in practice).\n */\nexport function rewriteAtPathPrefix(rawUrl: string): string | null {\n const match = /^\\/at\\/([^/?]+)(\\/[^?]*)?(\\?.*)?$/.exec(rawUrl);\n if (match === null) return null;\n const code = match[1];\n const path = match[2] === undefined || match[2] === '' ? '/' : match[2];\n const query = match[3] ?? '';\n const separator = query === '' ? '?' : '&';\n return `${path}${query}${separator}at=${code}`;\n}\n\nexport interface StartChiiRelayOptions {\n /**\n * Local port for the relay. Default 0 (OS-assigned ephemeral port).\n *\n * Using 0 means the OS picks a free port — this is the safe default because\n * a stale cloudflared child process (PPID 1, orphaned after SIGKILL) may still\n * be holding a fixed port. A fixed port causes EADDRINUSE on the next startup,\n * which makes the MCP handshake fail with -32000. With port 0 the new relay\n * always gets a fresh port, making any orphaned process harmless.\n *\n * Pass an explicit number to restore fixed-port behaviour (backwards-compatible).\n */\n port?: number;\n /** Bind host. Default 127.0.0.1 (tunnel reaches it locally). */\n host?: string;\n /**\n * Optional auth predicate for WebSocket upgrade requests.\n *\n * When provided, every inbound WebSocket upgrade is checked by calling\n * `verifyAuth(req)` before Chii processes it. Return `true` to allow the\n * upgrade; return `false` to reject with HTTP 401 and destroy the socket.\n *\n * The predicate MUST NOT log the secret or any TOTP code — it is a black-box\n * from this module's perspective.\n *\n * @param req - The raw HTTP `IncomingMessage` from the upgrade handshake.\n * Inspect `req.url` for query parameters (e.g. `at=<code>`). Path-prefixed\n * URLs (`/at/<code>/…`, the phone-target transport — issue #466) are\n * rewritten into the query form BEFORE this predicate runs, so a\n * query-only predicate covers both transports.\n * @returns `true` if the upgrade is authorised, `false` to reject.\n */\n verifyAuth?: (req: IncomingMessage) => boolean;\n /**\n * Secret-free observability callback fired on every auth rejection\n * (issue #467). Only meaningful together with `verifyAuth`.\n *\n * SECRET-HANDLING: the event carries ONLY the rejection kind — never\n * `req.url`, query strings, TOTP codes, or the secret. Implementations must\n * keep it that way (e.g. increment a counter + timestamp). Exceptions thrown\n * by the callback are swallowed so observability can never break the gate.\n */\n onAuthReject?: (event: RelayAuthRejectEvent) => void;\n /**\n * WS protocol ping interval in milliseconds (issue #483).\n *\n * The relay sends a ping frame to every connected WebSocket at this interval\n * so that Cloudflare's proxied-connection idle timer (~100 s) is reset for\n * both the phone-target leg and the daemon-client leg. The peer responds with\n * a pong automatically (browser / ws library behaviour) — no application\n * code change is needed on either end.\n *\n * Default: 45 000 ms (45 s). Set to 0 to disable keepalive entirely.\n *\n * Pass a small value in tests to avoid real-time waits — pair with fake\n * timers (`vi.useFakeTimers()`) or a short sleep.\n */\n keepaliveIntervalMs?: number;\n}\n\n/**\n * Starts the Chii relay and resolves once listening.\n *\n * Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral\n * port on every start, so a stale cloudflared orphan holding any particular\n * port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`\n * always reflect the actual bound port.\n *\n * chii.start() is called with `server` (our pre-created httpServer) BEFORE\n * httpServer.listen(). This is intentional: chii attaches its Koa handler and\n * WS upgrade listener to the server object, but the actual TCP bind is\n * performed by our httpServer.listen() call below. The `port`/`domain` values\n * passed to chii.start() are used for display/banner purposes inside chii and\n * do not affect which port the server binds. The connection path (clients\n * connecting to `relay.baseUrl`) always uses the post-listen confirmed port.\n */\nexport async function startChiiRelay(options: StartChiiRelayOptions = {}): Promise<ChiiRelay> {\n const requestedPort = options.port ?? 0;\n const host = options.host ?? '127.0.0.1';\n const { verifyAuth, onAuthReject } = options;\n const keepaliveIntervalMs =\n options.keepaliveIntervalMs !== undefined\n ? options.keepaliveIntervalMs\n : DEFAULT_KEEPALIVE_INTERVAL_MS;\n\n const httpServer = createServer();\n\n // Secret-free observability hook (issue #467). Swallow callback exceptions —\n // a broken observer must never turn into an open gate or a crashed relay.\n const notifyAuthReject = (kind: RelayAuthRejectEvent['kind']): void => {\n if (onAuthReject === undefined) return;\n try {\n onAuthReject({ kind });\n } catch {\n // Ignore — observability is best-effort.\n }\n };\n\n // Register the HTTP-request auth listener BEFORE chii.start() so it fires\n // first. Node's http.Server emits 'request' to all listeners in registration\n // order; the first to end() the response wins. Valid requests return without\n // side-effect so chii's own handler takes over normally — and because\n // listeners run synchronously in order, mutating `req.url` here (path-prefix\n // strip, issue #466) means chii's later-registered handler only ever sees\n // the stripped URL.\n //\n // We only register when verifyAuth is provided so the no-auth path is\n // zero-overhead for tests and local-only dev sessions. (The phone-side\n // `/at/<code>/` prefix only ever appears when TOTP is armed — the launcher\n // QR carries the `at` code — so the no-auth path never needs the strip.)\n if (verifyAuth) {\n // Plain HTTP requests: two cases are gated, everything else passes through.\n //\n // Case 1 — path-prefixed form (`/at/<code>/…`): the phone fetches\n // `target.js` via `https://<host>/at/<code>/target.js` (issue #466).\n // The prefix is rewritten to the query form and then verified; chii's Koa\n // static handler sees the stripped URL.\n //\n // Case 2 — `/targets` read route (issue #474): without gating this route a\n // URL-leaker (threat model: someone who obtained the tunnel URL but not\n // the secret) can read session metadata (id/url/title, including any query\n // params in the page URL) without a code. Debugger attach stays blocked by\n // the WS gate, but /targets is an HTTP read that was previously ungated.\n // The `at` code may arrive as a query param (`/targets?at=<code>`) — which\n // buildRelayVerifyAuth already handles — or via the `/at/<code>/` path\n // prefix (rewriteAtPathPrefix normalises that to the query form first).\n //\n // Static assets (target.js, chii front-end HTML/JS/CSS) and any other\n // non-prefixed, non-/targets request keep today's ungated pass-through — the\n // phone fetches some via the legacy no-prefix path and gating them would\n // break env-2/3/4.\n //\n // SECRET-HANDLING: We do NOT log req.url or any auth value in this listener.\n httpServer.on('request', (req, res) => {\n const rewritten = rewriteAtPathPrefix(req.url ?? '');\n if (rewritten !== null) {\n // Path-prefix form: normalise to query form, then verify.\n req.url = rewritten;\n if (!verifyAuth(req)) {\n // CORS header + tiny JSON body (issue #478): the script URL is\n // cross-origin from the phone page (tunnel origin ≠ relay origin), so\n // without ACAO a fetch() probe sees an opaque error and cannot tell\n // auth rejection from a network failure. The header rides ONLY on\n // this error response — no relay asset is exposed through it.\n res.statusCode = 401;\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));\n notifyAuthReject('http-request');\n }\n // Auth passed (or was rejected above): return so the path-prefix branch\n // never falls through to the /targets check below.\n return;\n }\n\n // Non-prefixed request: check if this is the /targets read route (issue\n // #474). Extract the pathname robustly — req.url is a raw path+query\n // string like `/targets?at=123456` so we split on `?`.\n const pathname = (req.url ?? '').split('?')[0];\n if (pathname === '/targets' || pathname === '/targets/') {\n // The `at` code must be present as a query param — verifyAuth reads it\n // from req.url via URLSearchParams, which already handles `?at=<code>`\n // without any URL rewrite needed.\n if (!verifyAuth(req)) {\n // Same 401 shape as the path-prefix branch (issue #478 contract).\n res.statusCode = 401;\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));\n notifyAuthReject('http-request');\n // res.end() wins — chii's Koa handler will not write.\n return;\n }\n // Auth passed: return without ending the response. Node invokes every\n // 'request' listener in registration order, so chii's Koa listener\n // (registered later by chii.start) still runs and serves the /targets\n // JSON — this return only means \"this gate listener is done\", not \"end\n // the response\".\n return;\n }\n\n // Any other non-prefixed request (static assets, chii front-end, etc.):\n // ungated pass-through to chii. Auth passed: no-op — chii's Koa\n // 'request' listener (registered below by chii.start) serves the URL.\n // (Koa skips writing when an earlier listener already ended the response,\n // so the 401 paths above are safe even though Koa still runs.)\n });\n }\n\n // WS keepalive (issue #483): capture chii's WebSocketServer instance so we\n // can read `_wss.clients` and send periodic ping frames.\n //\n // `chii/server/index.js`'s start() creates `new WebSocketServer()` but\n // doesn't expose the instance. We capture it by temporarily patching\n // `ChiiWssClass.prototype.start` — that method runs on the instance, so\n // `this` gives us the reference we need.\n //\n // The patch is installed for the duration of one `chii.start()` call and\n // removed in a `finally` block, so concurrent relays nest correctly. If the\n // internal path changes in a future chii release (tryLoadChiiWssClass returns\n // null), chii.start() runs unpatched and the keepalive loop is silently\n // skipped — relay correctness is unaffected.\n const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;\n const capturedChiiWss = await startChiiWithCapture(\n loadChiiServer(),\n { server: httpServer, domain: `${host}:${requestedPort}`, port: requestedPort },\n chiiWssClass,\n );\n\n // WS upgrade gate (issue #478, accept-then-close): take over the upgrade\n // chain AFTER chii.start() has registered chii's own upgrade listener.\n // Listener ordering alone protected chii when rejection meant\n // socket.destroy(); accept-then-close keeps the socket ALIVE, so chii's\n // listener (which always runs on every 'upgrade' emit) would complete a\n // second handshake on the rejected socket — frames after our close frame\n // would reach chii's server-side WebSocket, i.e. an auth bypass. Capturing\n // chii's listeners and re-dispatching only on auth pass closes that hole.\n if (verifyAuth) {\n const chiiUpgradeListeners = httpServer.listeners('upgrade') as Array<\n (req: IncomingMessage, socket: Duplex, head: Buffer) => void\n >;\n httpServer.removeAllListeners('upgrade');\n // noServer: handshake-only — never binds a port; used purely to send a\n // spec-compliant close frame with a code the browser can read.\n const rejectWss = new WebSocketServer({ noServer: true });\n httpServer.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer) => {\n // Phone-target transport (issue #466): normalise a `/at/<code>/…` path\n // prefix into the query form before verification, and strip it from the\n // URL chii will see. No-prefix URLs pass through untouched (daemon\n // client query transport — back-compat).\n const rewritten = rewriteAtPathPrefix(req.url ?? '');\n if (rewritten !== null) {\n req.url = rewritten;\n }\n if (!verifyAuth(req)) {\n // Reject: complete the handshake, then close with a NAMED code so the\n // browser-side observer (in-app attach.ts) can distinguish \"stale\n // TOTP code\" (4401) from \"tunnel down\" (1006). Raw-401-destroy only\n // ever produced 1006 client-side — the env-2 silence gap (#478).\n // We do NOT log req.url or any auth param here to avoid leaking codes;\n // the close reason is a fixed enum string.\n rejectWss.handleUpgrade(req, socket, head, (ws) => {\n ws.close(RELAY_AUTH_REJECT_CLOSE_CODE, RELAY_AUTH_REJECT_REASON);\n });\n notifyAuthReject('ws-upgrade');\n // Early return — chii's captured listeners are NOT called.\n return;\n }\n // Auth passed: hand the upgrade to chii's own listeners (it sees the\n // stripped URL — same observable behaviour as the pre-#478 ordering).\n for (const listener of chiiUpgradeListeners) {\n listener(req, socket, head);\n }\n });\n }\n\n const actualPort = await new Promise<number>((resolve, reject) => {\n httpServer.once('error', reject);\n httpServer.listen(requestedPort, host, () => {\n httpServer.off('error', reject);\n // httpServer.address() is non-null immediately after the listen callback.\n const addr = httpServer.address() as AddressInfo;\n resolve(addr.port);\n });\n });\n\n // WS keepalive interval (issue #483): send a ping frame to every connected\n // socket on each tick. Both the phone-target leg and the daemon-client leg\n // terminate as WebSocket connections on this relay, so pinging chii's\n // `_wss.clients` covers both.\n //\n // Per-ping log output is intentionally absent — pings happen every 45 s and\n // logging each one would flood the MCP console without adding signal.\n //\n // `ws` clients respond to ping frames with pong automatically (RFC 6455 §5.5)\n // — no application code is needed on either end.\n let keepaliveHandle: ReturnType<typeof setInterval> | null = null;\n if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {\n const chiiWss = capturedChiiWss;\n keepaliveHandle = setInterval(() => {\n for (const client of chiiWss._wss.clients) {\n // readyState 1 = OPEN (ws library constant). Only ping live sockets.\n if (client.readyState === 1) {\n client.ping();\n }\n }\n }, keepaliveIntervalMs);\n }\n\n return {\n port: actualPort,\n baseUrl: `http://${host}:${actualPort}`,\n close: () =>\n new Promise<void>((resolve) => {\n if (keepaliveHandle !== null) {\n clearInterval(keepaliveHandle);\n keepaliveHandle = null;\n }\n httpServer.close(() => resolve());\n }),\n };\n}\n","/**\n * URL of the AITC Sandbox launcher PWA.\n *\n * Declared here (not imported from `src/unplugin/tunnel.ts`) to respect the\n * mcp → unplugin layering boundary. unplugin/tunnel.ts declares its own copy\n * for the same reason — keep the two in sync when the URL changes.\n */\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Optional metadata that enriches the launcher deep-link (#498).\n *\n * These fields are added as query params so the launcher PWA can display\n * a recognizable identity (name, icon) without the user having to configure\n * anything extra.\n */\nexport interface LauncherAttachUrlOpts {\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * Absolute `https://` icon URL for the partner nav bar icon slot (`icon=`\n * param). Non-https or falsy values are not added.\n */\n icon?: string;\n /**\n * When `true`, adds `selfdebug=1` to the launcher URL so the launcher PWA\n * registers its own document as a CDP target (issue #531/#543).\n *\n * **Single-attach model**: attaching the launcher self-target causes any\n * currently-attached mini-app target to be evicted. This is intentional —\n * `selfdebug` is a \"launcher diagnostics mode\" for inspecting the launcher's\n * own DOM/console/safe-area, not simultaneous dual-attach.\n *\n * When `false` or omitted (default), the param is not added and the output\n * is byte-identical to the previous behaviour.\n */\n selfdebug?: boolean;\n}\n\n/**\n * Builds a launcher PWA deep-link for env-2 MCP-attach (issue #378).\n *\n * The launcher at {@link LAUNCHER_URL} renders tunnelUrl in a full-viewport\n * iframe. `&debug=1&relay=<wssUrl>` is forwarded onto the iframe src so the\n * framed page's in-app debug gate (Layer C) is satisfied and a Chii target.js\n * is injected. `&at=<totpCode>` is added only when a code is provided (same\n * conditional as {@link buildDeepLinkAttachUrl}).\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the\n * launcher partner bar shows the app name instead of the generic default (#498).\n * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the\n * launcher can render an icon next to the title (#498).\n *\n * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL\n * via raw string manipulation), this function uses WHATWG `encodeURIComponent`\n * because the target is a standard `https:` URL.\n *\n * SECRET-HANDLING: `totpCode` (when provided) is placed into the `at=` param\n * only — never logged or returned separately. Callers must NOT log the result\n * of this function to stdout/stderr.\n *\n * @param tunnelUrl - The `https://*.trycloudflare.com` app tunnel URL\n * (`AIT_TUNNEL_BASE_URL`). This is the URL the launcher frames.\n * @param wssUrl - The `wss://` relay URL the framed page will attach to.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is appended as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Omit when TOTP is disabled.\n * @param opts - Optional app identity hints: `name`, `icon`, and `selfdebug`\n * (#498, #543).\n * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>\n * [&at=<code>][&name=<enc>][&icon=<enc>][&selfdebug=1]` params.\n */\nexport function buildLauncherAttachUrl(\n tunnelUrl: string,\n wssUrl: string,\n totpCode?: string,\n opts?: LauncherAttachUrlOpts,\n): string {\n let url =\n `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}` +\n `&debug=1&relay=${encodeURIComponent(wssUrl)}`;\n if (totpCode !== undefined && totpCode !== '') {\n url += `&at=${encodeURIComponent(totpCode)}`;\n }\n // App identity hints (#498): add non-blank name and valid https icon.\n if (opts?.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts?.icon !== undefined) {\n let iconParsed: URL;\n try {\n iconParsed = new URL(opts.icon);\n } catch {\n iconParsed = null as unknown as URL;\n }\n if (iconParsed?.protocol === 'https:') {\n url += `&icon=${encodeURIComponent(opts.icon)}`;\n }\n }\n // Self-debug opt-in (#543): add selfdebug=1 only when explicitly requested.\n // Without this flag the output is byte-identical to the previous behaviour.\n if (opts?.selfdebug === true) {\n url += '&selfdebug=1';\n }\n return url;\n}\n\n/**\n * Build a self-attaching dog-food deep-link.\n *\n * `ait deploy --scheme-only` prints an `intoss-private://…?_deploymentId=<uuid>`\n * URL that opens a dog-food bundle on a phone. The in-app debug gate\n * (`src/in-app/gate.ts`) auto-attaches when the entry URL also carries\n * `debug=1` and `relay=<wss-url>`. This helper splices those params (plus\n * `at=<code>` when TOTP is enabled) into the scheme URL; rendering the result\n * as a QR code and scanning it with the phone camera opens the mini-app and\n * attaches it to the live Chii relay. QR is the single entry path — it needs\n * no USB cable, platform CLI, or driver, and works the same on iOS/Android.\n *\n * The Toss app propagates extra query params from the entry deep link into the\n * mini-app WebView's `location.search` (confirmed behavior), so the gate reads\n * them at attach time.\n *\n * TOTP `at=` param:\n * When a TOTP secret is active, `buildDeepLinkAttachUrl` accepts an optional\n * `totpCode` argument and splices `at=<code>` alongside `debug` and `relay`.\n * The code must be computed by the caller at call time — do NOT pre-compute\n * and cache it, because the 30-second window expires quickly. The in-app gate\n * (`src/in-app/gate.ts` Layer C) validates this code against the baked secret.\n *\n * Why not `URL`/`URLSearchParams`: `intoss-private:` is a non-special scheme.\n * The WHATWG `URL` parser treats such schemes opaquely (no host/path/query\n * decomposition you can rely on across runtimes), so query manipulation via\n * `url.searchParams` is not portable here. We splice the query string directly\n * on the raw string instead, which keeps the scheme, authority, path, and any\n * pre-existing params (notably `_deploymentId`) byte-for-byte intact.\n */\n\n/**\n * Suspicious/generic authority values that indicate a malformed or placeholder\n * scheme URL. These are host strings that will almost certainly cause the Toss\n * app to fail with \"bundle not found\" silently.\n *\n * The expected form from `ait deploy --scheme-only` is:\n * intoss-private://<appName>?_deploymentId=<uuid>\n * where `<appName>` is a non-generic string like `aitc-sdk-example`.\n */\nconst SUSPICIOUS_AUTHORITIES = new Set<string>(['', 'web', 'localhost', '127.0.0.1', 'app']);\n\n/**\n * Validates the authority (host) portion of a scheme URL.\n *\n * Returns a warning message if the authority is missing or looks like a\n * placeholder, or `null` if the authority looks valid.\n *\n * Expected form: `intoss-private://<appName>?_deploymentId=<uuid>`\n * The authority must be a non-empty, non-generic app name (e.g. `aitc-sdk-example`).\n */\nexport function validateSchemeAuthority(schemeUrl: string): string | null {\n // Extract authority from `scheme://authority[/path][?query][#hash]`.\n // We cannot use the WHATWG URL parser for non-special schemes reliably\n // (see the deeplink.ts module comment), so we parse the raw string.\n const afterScheme = schemeUrl.replace(/^[a-zA-Z][a-zA-Z0-9+\\-.]*:\\/\\//, '');\n if (afterScheme === schemeUrl) {\n // No `://` found — not a scheme URL at all.\n return (\n 'scheme_url does not look like a scheme URL (expected `intoss-private://<appName>?_deploymentId=<uuid>`). ' +\n 'Use the URL printed by `ait deploy --scheme-only`.'\n );\n }\n\n // authority ends at the first `/`, `?`, `#`, or end of string.\n const authorityEnd = afterScheme.search(/[/?#]/);\n const authority = authorityEnd === -1 ? afterScheme : afterScheme.slice(0, authorityEnd);\n\n if (SUSPICIOUS_AUTHORITIES.has(authority.toLowerCase())) {\n const displayAuthority = authority === '' ? '(empty)' : `\"${authority}\"`;\n return (\n `scheme_url authority ${displayAuthority} looks like a placeholder. ` +\n 'Expected an app name like `intoss-private://aitc-sdk-example?_deploymentId=<uuid>`. ' +\n 'Use the URL printed by `ait deploy --scheme-only` — it includes the correct app name as the host.'\n );\n }\n\n return null;\n}\n\n/** A param the helper appends. Existing occurrences are replaced, not duplicated. */\ntype AppendParam = readonly [key: string, value: string];\n\nfunction stripExisting(query: string, key: string): string {\n if (query === '') return '';\n return query\n .split('&')\n .filter((pair) => pair !== '' && pair.split('=')[0] !== key)\n .join('&');\n}\n\n/**\n * Splices `debug=1`, `relay=<wssUrl>`, and (optionally) `at=<totpCode>` into a\n * scheme URL's query string, preserving everything else (scheme, authority,\n * path, hash, and the existing `_deploymentId` param). If any of the spliced\n * params is already present it is replaced so the helper is idempotent.\n *\n * @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL printed\n * by `ait deploy --scheme-only`. Must already carry `_deploymentId` (Layer B\n * of the gate); this helper does not invent one.\n * @param wssUrl - The live relay URL (`wss://…trycloudflare.com`) from the\n * running debug MCP server's quick tunnel.\n * @param totpCode - Optional current TOTP code (6 digits). When provided, it\n * is spliced as `at=<totpCode>`. Must be computed at call time — it rotates\n * every 30 s. Pass `undefined` or omit when TOTP is disabled.\n * @returns The same URL with `debug=1&relay=<encoded wssUrl>[&at=<totpCode>]`\n * appended.\n * @throws If `wssUrl` is not a `wss:` URL (the gate rejects anything else, so\n * producing such a link would be a silent dead end).\n */\nexport function buildDeepLinkAttachUrl(\n schemeUrl: string,\n wssUrl: string,\n totpCode?: string,\n): string {\n let relay: URL;\n try {\n relay = new URL(wssUrl);\n } catch {\n throw new Error(`relay URL is not a valid URL: ${wssUrl}`);\n }\n if (relay.protocol !== 'wss:') {\n throw new Error(`relay URL must use the wss: scheme, got ${relay.protocol} (${wssUrl})`);\n }\n\n const hashIndex = schemeUrl.indexOf('#');\n const hash = hashIndex === -1 ? '' : schemeUrl.slice(hashIndex);\n const beforeHash = hashIndex === -1 ? schemeUrl : schemeUrl.slice(0, hashIndex);\n\n const queryIndex = beforeHash.indexOf('?');\n const base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);\n let query = queryIndex === -1 ? '' : beforeHash.slice(queryIndex + 1);\n\n const appended: AppendParam[] = [\n ['debug', '1'],\n ['relay', wssUrl],\n ];\n // Only splice `at=` when a code is provided (TOTP enabled). Omitting it when\n // TOTP is disabled preserves backward compatibility with gate deployments\n // that do not yet evaluate the `at` param.\n if (totpCode !== undefined && totpCode !== '') {\n appended.push(['at', totpCode]);\n }\n\n // Always strip the `at` key from the existing query so a stale code from a\n // previous run is removed even when the caller does not provide a fresh code.\n query = stripExisting(query, 'at');\n\n for (const [key] of appended) {\n query = stripExisting(query, key);\n }\n for (const [key, value] of appended) {\n const pair = `${key}=${encodeURIComponent(value)}`;\n query = query === '' ? pair : `${query}&${pair}`;\n }\n\n return `${base}?${query}${hash}`;\n}\n","/**\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","/**\n * Unified response envelope for all MCP debug tools.\n *\n * Every tool result is wrapped in a `ToolEnvelope<T>` so agents can use a\n * single parser regardless of which tool they called. Before this, tool shapes\n * diverged: raw array returns, `{exceptions}`, `{value,type}`, `{ok,value|error}` …\n *\n * ## Schema\n *\n * ```ts\n * {\n * ok: boolean,\n * data?: T, // tool payload (absent when ok:false)\n * error?: { code, message, nextRecommendedAction? },\n * meta: {\n * tool: string,\n * env: 'mock' | 'relay-dev' | 'relay-live' | 'relay-mobile',\n * attached: boolean,\n * contentType: 'json' | 'image',\n * }\n * }\n * ```\n *\n * ## Compat mode\n *\n * Set `AIT_MCP_COMPAT=chrome-devtools` to bypass envelope wrapping and return\n * the raw payload. This restores 0.1.x behaviour for consumers that already\n * parse the old shapes (e.g. chrome-devtools-mcp integrations).\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n/** Allowed values for `meta.env`. */\nexport type EnvelopeEnv = 'mock' | 'relay-dev' | 'relay-live' | 'relay-mobile';\n\n/** The unified envelope returned by every debug MCP tool (when compat mode is off). */\nexport interface ToolEnvelope<T = unknown> {\n ok: boolean;\n data?: T;\n error?: {\n code: string;\n message: string;\n nextRecommendedAction?: {\n tool: string;\n reason: string;\n };\n };\n meta: {\n tool: string;\n env: EnvelopeEnv;\n attached: boolean;\n contentType: 'json' | 'image';\n };\n}\n\n/**\n * Returns `true` when `AIT_MCP_COMPAT=chrome-devtools` is set, which bypasses\n * envelope wrapping and returns raw payloads (0.1.x back-compat).\n */\nexport function isCompatMode(): boolean {\n return process.env.AIT_MCP_COMPAT === 'chrome-devtools';\n}\n\n/**\n * Maps `McpEnvironment` to `EnvelopeEnv`. After #307 these are the same\n * union (`mock | relay-dev | relay-live`), so this is identity — kept as a\n * named export for surface stability if envelope env diverges in the future.\n */\nexport function toEnvelopeEnv(env: McpEnvironment): EnvelopeEnv {\n return env;\n}\n\n/**\n * Context passed to `wrapEnvelope` that carries the per-request metadata.\n */\nexport interface EnvelopeContext {\n tool: string;\n env: McpEnvironment;\n attached: boolean;\n contentType?: 'json' | 'image';\n}\n\n/**\n * Wraps `data` in a `ToolEnvelope<T>` **unless** compat mode is active, in\n * which case `data` is returned as-is.\n *\n * Use this at every tool call-site in `debug-server.ts` and `server.ts`.\n *\n * @example\n * ```ts\n * return jsonResult(wrapEnvelope(listPages(connection, tunnel), {\n * tool: 'list_pages',\n * env: resolveEnvironment(),\n * attached: connection.listTargets().length > 0,\n * }));\n * ```\n */\nexport function wrapEnvelope<T>(data: T, ctx: EnvelopeContext): ToolEnvelope<T> | T {\n if (isCompatMode()) return data;\n return {\n ok: true,\n data,\n meta: {\n tool: ctx.tool,\n env: toEnvelopeEnv(ctx.env),\n attached: ctx.attached,\n contentType: ctx.contentType ?? 'json',\n },\n };\n}\n","/**\n * MCP environment — derived from two orthogonal axes (issue #348).\n *\n * Before #348 the environment was a single sticky decision made once per\n * process by `getEnvironment()` via a 5-step precedence chain (env var → URL\n * pattern sniffing → caller-stated default → baked-in default). That model\n * could not express a daemon that holds two live connections at once and swaps\n * the active one without a restart — the dual-connection design (#348).\n *\n * The 4-value `McpEnvironment` is now *derived* from cheap signals rather\n * than detected:\n *\n * 1. `mock` vs `relay-*` — free from `connection.kind` (`'local'` | `'relay'`,\n * see `cdp-connection.ts`). Authoritative, known before any target\n * attaches, and swappable at runtime by pointing at a different connection.\n *\n * 2. `relay-dev` vs `relay-live` — physically underivable (dog-food and\n * production relays are byte-identical on the wire), so it is a single\n * operator-supplied bit, `liveIntent`. It is armed only by\n * `start_debug({ mode: 'relay-live' })` and is inert whenever the active\n * connection is local.\n *\n * 3. `relay-dev` vs `relay-mobile` — both are `kind: 'relay'`, !liveIntent\n * relays, so they are distinguished by the booted family's `relayOrigin`\n * discriminator (`'intoss-webview'` → relay-dev, `'external-pwa'` →\n * relay-mobile, issue #378). NOT sniffed from the relay URL.\n *\n * `McpEnvironment` survives as an OUTPUT-BOUNDARY type — `get_debug_status` and\n * the envelope `meta.env` field still surface the precise three-value string —\n * but it is reconstructed from `(connection.kind, liveIntent)` via\n * {@link deriveEnvironment}, never sniffed.\n *\n * LIVE side-effect guard (relay-live, env 4): the `call_sdk` / `evaluate` tools\n * require an explicit `confirm: true`. The guard now reads to a single line in\n * `debug-server.ts`: `connection.kind === 'relay' && liveIntent && !confirm`.\n * `relay && liveIntent` together means a stale `liveIntent` bit is inert\n * against a local target (it only fires when the active connection is relay).\n *\n * Backward compatibility:\n * - `MCP_ENV=relay-live` is a deprecated alias that seeds `liveIntent=true`\n * at boot (see `cli.ts`). `MCP_ENV=mock|relay|relay-dev` are accepted and\n * ignored for env derivation (kind is authoritative) — they only matter for\n * `relay-live`'s liveIntent seed.\n * - `isRelayEnv()` / `isLiveRelayEnv()` / `toLegacyEnv()` are unchanged.\n *\n * SECRET-HANDLING: this module never reads the TOTP secret, deploy key, or any\n * URL. It deals only in the connection kind and a single boolean.\n */\n\n/**\n * The four environments the MCP server can surface in its output (issues #307,\n * #378).\n *\n * - `mock` — local Chromium + mock SDK (env 1) — active connection is local.\n * - `relay-dev` — real-device dog-food relay (env 3) — relay connection, liveIntent off,\n * intoss-private WebView (the relay devtools started).\n * - `relay-live` — real-device live/production relay (env 4) — relay connection,\n * liveIntent on, read-only LIVE guard active.\n * - `relay-mobile` — real-device PWA over an EXTERNAL relay (env 2, issue #378) —\n * relay connection, liveIntent off, an external-PWA relay\n * (the unplugin started it; the MCP only attaches a CDP client).\n *\n * This is a derived OUTPUT string (see module docstring) — not a detected,\n * sticky decision.\n */\nexport type McpEnvironment = 'mock' | 'relay-dev' | 'relay-live' | 'relay-mobile';\n\n/** Connection kind — the authoritative `mock` vs `relay` signal (issue #348). */\nexport type ConnectionKind = 'relay' | 'local';\n\n/**\n * Origin of a relay connection — the discriminator that distinguishes two relay\n * families that are otherwise both `kind: 'relay'` (issue #378):\n *\n * - `'intoss-webview'` — the intoss-private dog-food / live relay (env 3/4),\n * booted BY the MCP server (`bootRelayFamily`). Maps to `relay-dev` /\n * `relay-live` depending on `liveIntent`.\n * - `'external-pwa'` — an external CDP relay the unplugin already brought up\n * for the env-2 PWA (`bootExternalRelayFamily`). Maps to `relay-mobile`.\n *\n * Carried on the booted family (NOT sniffed from the relay URL), so the output\n * layer can tell `relay-mobile` apart from `relay-dev`.\n */\nexport type RelayOrigin = 'intoss-webview' | 'external-pwa';\n\n/**\n * Returns `true` when the environment is any relay variant (`relay-dev`,\n * `relay-live`, or `relay-mobile`). Use this instead of `env === 'relay'` for\n * tier checks — every relay env surfaces the Tier B / relay-only tool set.\n *\n * Written as an exhaustive switch so a future `McpEnvironment` member that is\n * missing an arm is a TS compile error rather than a silent `false`.\n */\nexport function isRelayEnv(env: McpEnvironment): boolean {\n switch (env) {\n case 'relay-dev':\n case 'relay-live':\n case 'relay-mobile':\n return true;\n case 'mock':\n return false;\n }\n}\n\n/**\n * Returns `true` when the environment is the LIVE relay (`relay-live`).\n * This is the guard condition for side-effect tool protection. `relay-mobile`\n * is a dev-intent env (env 2 PWA) and is NOT live.\n */\nexport function isLiveRelayEnv(env: McpEnvironment): boolean {\n return env === 'relay-live';\n}\n\n/**\n * Maps the `McpEnvironment` union to the legacy two-value union\n * (`'mock' | 'relay'`) for backward-compatible fields in diagnostics output.\n * Every relay variant (incl. `relay-mobile`) collapses to `'relay'`.\n */\nexport function toLegacyEnv(env: McpEnvironment): 'mock' | 'relay' {\n if (env === 'mock') return 'mock';\n return 'relay';\n}\n\n/**\n * Reconstructs the four-value `McpEnvironment` output string from the\n * orthogonal signals (issues #348, #378):\n *\n * - `kind === 'local'` → `'mock'`\n * - `kind === 'relay'` && liveIntent → `'relay-live'`\n * - `kind === 'relay'` && !liveIntent && origin 'external-pwa' → `'relay-mobile'`\n * - `kind === 'relay'` && !liveIntent && origin intoss/undefined → `'relay-dev'`\n *\n * `relayOrigin` is the booted-family discriminator (NOT sniffed from the URL)\n * that distinguishes the env-2 external-PWA relay (`relay-mobile`) from the\n * intoss-private dog-food relay (`relay-dev`); both are `kind: 'relay'`.\n *\n * Pure — used at every output boundary (envelope `meta.env`, `get_debug_status`,\n * `measure_safe_area` provenance) so the surface never sniffs a URL again.\n *\n * Written switch-style so a missing arm is a TS compile error (never falls\n * through to a default).\n */\nexport function deriveEnvironment(\n kind: ConnectionKind,\n liveIntent: boolean,\n relayOrigin?: RelayOrigin,\n): McpEnvironment {\n switch (kind) {\n case 'local':\n return 'mock';\n case 'relay':\n if (liveIntent) return 'relay-live';\n return relayOrigin === 'external-pwa' ? 'relay-mobile' : 'relay-dev';\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* liveIntent — the single operator-supplied bit (relay-dev vs relay-live) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Module-level `relay-dev` vs `relay-live` intent bit (issue #348).\n *\n * Armed by `start_debug({ mode: 'relay-live' })` (and seeded at boot by the\n * deprecated `MCP_ENV=relay-live` alias). Disarming is implicit: when the\n * active connection becomes local, the LIVE guard reads\n * `connection.kind === 'relay' && liveIntent`, so a stale `true` bit is inert.\n *\n * SECRET-HANDLING: this is a boolean — never a secret. Safe to read in logs.\n */\nlet liveIntent = false;\n\n/** Returns the current `liveIntent` bit. */\nexport function getLiveIntent(): boolean {\n return liveIntent;\n}\n\n/**\n * Sets the `liveIntent` bit. Called by `start_debug` (true for `relay-live`,\n * false for every other mode) and once at boot by the `MCP_ENV=relay-live`\n * deprecated alias.\n */\nexport function setLiveIntent(value: boolean): void {\n liveIntent = value;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Test override hook (narrow) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Test/override hook — when non-null, callers that consult\n * {@link getEnvironmentOverride} return this value regardless of the live\n * connection kind. Production code never sets it; it exists so a unit test can\n * pin a precise `McpEnvironment` without constructing a real connection.\n *\n * This is intentionally NARROW: it no longer drives a precedence chain. The\n * authoritative production signal is `connection.kind` + `liveIntent`; this\n * override is a pure test affordance.\n */\nlet envOverride: McpEnvironment | null = null;\n\n/** Sets a sticky environment override. Intended for tests only. */\nexport function setEnvironmentOverride(env: McpEnvironment | null): void {\n envOverride = env;\n}\n\n/** Reads the current override (test inspection). */\nexport function getEnvironmentOverride(): McpEnvironment | null {\n return envOverride;\n}\n","/**\n * MCP tool 거부/에러 응답 메시지 헬퍼 — 4상태 차별화 + Tier 거부 통일.\n *\n * 모든 tool 거부/에러 응답을 \"원인 + 다음 행동\" 한국어 한 줄 포맷으로 일원화한다.\n * debug-server.ts · tools.ts의 거부 응답 호출부가 이 헬퍼를 통해 생성된다.\n *\n * 4가지 상태 (진단 메시지 차별화):\n * - tunnel-down : cloudflared 터널 미가동 — 서버 재시작 필요\n * - page-missing : 페이지가 attach 안 됨 — build_attach_url → QR 스캔\n * - page-crash : 페이지 crash 감지 — 앱 재실행 후 재attach\n * - sdk-absent : window.__sdkCall 미주입 — dog-food 채널로 재배포\n */\n\n/** MCP tool-result 에러 응답 형식. */\nexport interface McpErrorResult {\n content: Array<{ type: 'text'; text: string }>;\n isError: true;\n}\n\n/**\n * 한국어 한 줄 \"원인 + 다음 행동\" 포맷으로 에러 결과를 빌드한다.\n *\n * @param message - 사용자에게 보여줄 에러 본문 (원인 + 다음 행동 포함).\n */\nexport function mcpError(message: string): McpErrorResult {\n return {\n content: [{ type: 'text', text: message }],\n isError: true,\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Tier 거부 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Tier A/B 환경 불일치 거부 메시지.\n *\n * @param toolName - 거부된 tool 이름.\n * @param requiredEnv - 해당 tool이 요구하는 환경 ('mock' | 'relay').\n * @param currentEnv - 현재 세션 환경.\n * @param reason - 환경이 결정된 근거를 나타내는 파생 문자열\n * (예: `derived:kind=relay,liveIntent=true`).\n */\nexport function tierRejectionError(\n toolName: string,\n requiredEnv: string,\n currentEnv: string,\n reason: string,\n): McpErrorResult {\n const envLabel = requiredEnv === 'relay' ? 'relay (실기기 연결)' : 'mock (로컬 브라우저)';\n const currentLabel = currentEnv === 'relay' ? 'relay' : 'mock';\n const hint =\n requiredEnv === 'relay'\n ? 'relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고 build_attach_url → QR 스캔으로 실기기를 attach하세요.'\n : 'mock으로 전환하려면 MCP_ENV=mock 설정 후 서버를 재시작하세요.';\n const text =\n `${toolName}은 ${envLabel} 환경에서만 사용할 수 있습니다. ` +\n `현재 환경: ${currentLabel} (${reason}). ${hint}`;\n // 하위 호환 — 기존 테스트가 기대하는 영문 패턴도 유지\n const compat = `tool ${toolName} is available only in ${requiredEnv}. Current environment is ${currentEnv} (${reason}).`;\n return mcpError(`${text}\\n\\n${compat}`);\n}\n\n/* -------------------------------------------------------------------------- */\n/* 4상태 차별화 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * 상태 1: tunnel 미가동 — cloudflared 터널이 아직 뜨지 않았다.\n *\n * `build_attach_url` 호출 시 tunnel.up === false 인 경우.\n */\nexport function tunnelDownError(): McpErrorResult {\n return mcpError(\n 'cloudflared 터널이 안 떠 있습니다. ' +\n 'MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.',\n );\n}\n\n/**\n * 상태 2: page 미attach — 터널은 살아 있으나 아직 페이지가 연결되지 않았다.\n *\n * enableDomains()가 \"No mini-app page attached\" 에러를 던질 때.\n */\nexport function pageMissingError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}페이지가 attach 안 됨. ` +\n 'dog-food 번들 배포 후 build_attach_url을 호출해 QR을 생성하세요: ' +\n '`ait deploy --scheme-only` → `build_attach_url(scheme_url)` → QR 스캔.',\n );\n}\n\n/**\n * 상태 3: page crash — 연결됐던 페이지가 crash/destroy됐다.\n *\n * chii-connection 이 'replaced-by-new-attach' / 'targetCrashed' / 'targetDestroyed' 를\n * 던질 때 이 메시지를 사용한다.\n */\nexport function pageCrashError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}페이지가 crash됐습니다. ` +\n '토스 앱을 재실행한 뒤 build_attach_url → QR 스캔으로 재attach하세요.',\n );\n}\n\n/**\n * 상태 4: SDK 부재 — window.__sdkCall이 주입되지 않았다.\n *\n * call_sdk 호출 시 브리지가 없을 때. 같은 \"브리지 부재\"라도 다음 행동은\n * connection 종류에 따라 정반대다 (issue #360):\n * - relay(`--target` 없는 intoss / env-2): dog-food 빌드가 아니다 → dog-food\n * 채널로 재배포 후 QR 재스캔.\n * - local(`--target=local`, env 1 로컬 브라우저): 재배포가 아니라 dev 서버를\n * `pnpm dev`로 띄웠는지 + unplugin alias가 `@apps-in-toss/web-framework`를\n * devtools mock으로 resolve하는지 확인. dev 빌드면 `import.meta.env.DEV`\n * 경로로 `window.__sdkCall`이 자동 설치된다.\n *\n * `isLocal`이 생략되면 relay 안내(이전 동작)를 유지한다.\n */\nexport function sdkAbsentError(toolName?: string, isLocal = false): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n if (isLocal) {\n return mcpError(\n `${prefix}window.__sdkCall이 주입되지 않았습니다 (로컬 dev 브리지 부재). ` +\n 'sdk-example을 `pnpm dev`로 띄웠는지, 그리고 unplugin alias가 ' +\n '`@apps-in-toss/web-framework`를 devtools mock으로 resolve하는지 확인하세요. ' +\n 'dev 빌드(`import.meta.env.DEV`)면 `window.__sdkCall`이 자동 설치됩니다.',\n );\n }\n return mcpError(\n `${prefix}window.__sdkCall이 주입되지 않았습니다 (dog-food 빌드가 아닙니다). ` +\n 'dog-food 채널(intoss-private)로 재배포 후 QR을 다시 스캔하세요: ' +\n '`ait build && aitcc app deploy`.',\n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* LIVE side-effect guard 메시지 (relay-live env) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * relay-live 환경에서 side-effect 도구(`call_sdk`, `evaluate`)를 `confirm: true`\n * 없이 호출했을 때 반환하는 거부 메시지.\n *\n * 다음 행동을 두 가지로 제시한다:\n * 1. 같은 호출에 `confirm: true` 인자를 추가해 재시도.\n * 2. 읽기 전용 환경(relay-dev, mock)으로 전환.\n */\nexport function liveGuardError(toolName: string): McpErrorResult {\n const text =\n `[LIVE relay guard] ${toolName}은 현재 relay-live(실 출시 런타임) 세션에서 ` +\n 'side-effect 호출입니다. 실유저에게 영향을 줄 수 있어 명시적 동의가 필요합니다.\\n\\n' +\n '다음 중 하나를 선택하세요:\\n' +\n ` 1. \\`confirm: true\\` 인자를 추가해 재호출: ${toolName}(…, confirm: true)\\n` +\n ' 2. 읽기 전용 도구(list_pages, list_console_messages, take_screenshot 등)를 사용하세요.\\n' +\n ' 3. dog-food 빌드(relay-dev 환경)에서 먼저 검증 후 live에 적용하세요.\\n\\n' +\n 'live-guard: MCP_ENV=relay-live + confirm: true missing';\n return mcpError(text);\n}\n\n/* -------------------------------------------------------------------------- */\n/* relay 연결 끊김 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * relay WebSocket 연결이 끊겼을 때 — 크래시가 아닌 네트워크/프로세스 종료.\n */\nexport function relayDisconnectError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}relay 연결이 끊겼습니다. ` +\n 'list_pages로 상태를 확인하고, 필요하면 앱을 재실행 후 재attach하세요.',\n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* 일반 tool 에러 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * CDP/AIT 명령 중 발생한 예외를 4상태로 분류해 적절한 에러 결과를 반환한다.\n *\n * - SDK 부재 패턴 (`window.__sdkCall is not available`) → sdkAbsentError\n * - crash 패턴 (`replaced-by-new-attach`, `targetCrashed`, `targetDestroyed`) → pageCrashError\n * - 연결 끊김 패턴 (`relay에 연결되어 있지 않습니다`, `relay WebSocket`) → relayDisconnectError\n * - 그 외 (일반 에러) → 원본 메시지를 포함한 mcpError\n */\nexport function classifyToolError(err: unknown, toolName: string, isLocal = false): McpErrorResult {\n const message = err instanceof Error ? err.message : String(err);\n\n // 상태 1: tunnel 미가동 (buildAttachUrl이 던지는 패턴)\n if (message.startsWith('tunnel-down:') || message.includes('터널이 안 떠 있습니다')) {\n return tunnelDownError();\n }\n\n // 상태 4: SDK 부재. page-side probe가 던지는 메시지는 relay 가정으로 쓰여\n // 있으나, 안내는 connection 종류로 재구성한다 (issue #360) — local 세션이면\n // dog-food 재배포가 아니라 dev 서버/unplugin alias 확인이 맞다.\n if (\n message.startsWith('sdk-absent:') ||\n message.includes('__sdkCall이 주입되지 않았습니다') ||\n message.includes('window.__sdkCall is not available') ||\n (message.includes('__sdkCall') && message.includes('not available'))\n ) {\n return sdkAbsentError(toolName, isLocal);\n }\n\n // 상태 3: page crash / target destroyed / replaced-by-new-attach\n if (\n message.includes('replaced-by-new-attach') ||\n message.includes('targetCrashed') ||\n message.includes('targetDestroyed') ||\n message.includes('detachedFromTarget')\n ) {\n return pageCrashError(toolName);\n }\n\n // relay 연결 끊김 (단순 disconnect — crash 아님)\n if (message.includes('relay에 연결되어 있지 않습니다') || message.includes('relay WebSocket')) {\n return relayDisconnectError(toolName);\n }\n\n // 그 외: 원본 메시지를 포함하되 list_pages 다음 행동 안내 추가\n return mcpError(\n `${toolName} 실패: ${message}\\nlist_pages로 미니앱이 relay에 attach됐는지 확인하세요.`,\n );\n}\n","/**\n * Local-browser `CdpConnection` — attaches directly to a Chromium instance\n * started with `--remote-debugging-port=<port>`.\n *\n * Topology (local debug mode, env 1):\n * Chromium --CDP WS--> this connection <--stdio--> MCP host\n *\n * The core insight: local Chromium and the phone's Toss WebView both speak\n * Chrome DevTools Protocol. The only difference is the attach strategy — how\n * you reach the CDP endpoint. Here we hit the Chromium DevTools HTTP endpoint\n * (`GET /json`) to discover per-target websocket URLs, then connect directly.\n * The Chii relay (env 2/3) uses `GET /targets` + `/client/<id>?target=<id>`.\n * Every tool (list_console_messages, get_dom_document, take_screenshot, …)\n * reads only the `CdpConnection` interface and works unchanged on both.\n *\n * Node-only: imports `ws`. Never bundled into the browser/in-app entries.\n */\n\nimport { EventEmitter } from 'node:events';\nimport { WebSocket } from 'ws';\nimport type {\n CdpCommandMap,\n CdpCommandName,\n CdpConnection,\n CdpEventMap,\n CdpEventName,\n CdpTarget,\n} from './cdp-connection.js';\n\n/** Max events retained per domain ring buffer. */\nconst DEFAULT_BUFFER_SIZE = 500;\n\n/** A CDP message arriving over the local Chromium websocket. */\ninterface CdpInboundMessage {\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { message: string };\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction parseInbound(raw: string): CdpInboundMessage | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isObject(parsed)) return null;\n const message: CdpInboundMessage = {};\n if (typeof parsed.id === 'number') message.id = parsed.id;\n if (typeof parsed.method === 'string') message.method = parsed.method;\n if ('params' in parsed) message.params = parsed.params;\n if ('result' in parsed) message.result = parsed.result;\n if (isObject(parsed.error) && typeof parsed.error.message === 'string') {\n message.error = { message: parsed.error.message };\n }\n return message;\n}\n\nconst PHASE_1_EVENTS: readonly CdpEventName[] = [\n 'Runtime.consoleAPICalled',\n 'Network.requestWillBeSent',\n 'Network.responseReceived',\n];\n\n/**\n * A target entry from the Chromium DevTools HTTP `/json` endpoint.\n * Each page target includes a `webSocketDebuggerUrl` pointing directly at the\n * target's CDP websocket — no relay URL indirection.\n */\ninterface ChromiumJsonTarget {\n id: string;\n title: string;\n url: string;\n type: string;\n webSocketDebuggerUrl?: string;\n}\n\nexport interface LocalCdpConnectionOptions {\n /**\n * Base URL of the Chromium DevTools HTTP server, e.g. `http://127.0.0.1:9222`.\n * The connection hits `<devtoolsHttpUrl>/json` to discover targets.\n */\n devtoolsHttpUrl: string;\n /** Per-domain ring buffer size. Default 500. */\n bufferSize?: number;\n}\n\n/**\n * `CdpConnection` that attaches directly to a local Chromium over its built-in\n * CDP websocket. Mirrors `ChiiCdpConnection`'s buffering/command-routing/event\n * logic — same `parseInbound`, ring-buffer, `pending` map patterns — but the\n * attach strategy differs:\n *\n * Chii relay: `GET /targets` → open `/client/<id>?target=<id>` WS\n * Local CDP: `GET /json` → open `webSocketDebuggerUrl` per target directly\n *\n * Target selection: first `type === 'page'` target whose URL is not\n * `about:blank`, `about:newtab`, or a devtools:// URL.\n */\nexport class LocalCdpConnection implements CdpConnection {\n /** Authoritative connection kind (issue #348) — local Chromium CDP. */\n readonly kind = 'local' as const;\n\n private readonly devtoolsHttpUrl: string;\n private readonly bufferSize: number;\n private readonly emitter = new EventEmitter();\n private readonly buffers = new Map<CdpEventName, unknown[]>();\n private readonly targets = new Map<string, CdpTarget>();\n\n private ws: WebSocket | null = null;\n private nextCommandId = 1;\n /** In-flight enableDomains() promise — concurrent callers share it. */\n private enablingPromise: Promise<void> | null = null;\n /** Pending request→response commands keyed by CDP message id. */\n private readonly pending = new Map<\n number,\n { resolve: (result: unknown) => void; reject: (err: Error) => void }\n >();\n\n constructor(options: LocalCdpConnectionOptions) {\n this.devtoolsHttpUrl = options.devtoolsHttpUrl.replace(/\\/$/, '');\n this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;\n for (const event of PHASE_1_EVENTS) this.buffers.set(event, []);\n // EventEmitter caps listeners at 10 by default; the tool layer may add\n // several short-lived subscriptions, so lift the cap.\n this.emitter.setMaxListeners(0);\n }\n\n /**\n * Fetch the target list from the Chromium DevTools `/json` (or `/json/list`)\n * endpoint and pick the first non-blank page target.\n *\n * Returns the selected target's `webSocketDebuggerUrl` alongside the\n * normalized `CdpTarget` list (all page targets visible to the server).\n */\n private async fetchTargets(): Promise<{\n selected: ChromiumJsonTarget | null;\n all: CdpTarget[];\n }> {\n // Chromium exposes both /json and /json/list; /json is the canonical form.\n const res = await fetch(`${this.devtoolsHttpUrl}/json`);\n if (!res.ok) {\n throw new Error(\n `Chromium DevTools /json returned HTTP ${res.status} ${res.statusText}. ` +\n 'Is the browser running with --remote-debugging-port?',\n );\n }\n const body: unknown = await res.json();\n const list: ChromiumJsonTarget[] = Array.isArray(body) ? (body as ChromiumJsonTarget[]) : [];\n\n this.targets.clear();\n let selected: ChromiumJsonTarget | null = null;\n\n for (const item of list) {\n if (!isObject(item) || typeof item.id !== 'string') continue;\n const cdpTarget: CdpTarget = {\n id: item.id,\n title: typeof item.title === 'string' ? item.title : '',\n url: typeof item.url === 'string' ? item.url : '',\n };\n this.targets.set(item.id, cdpTarget);\n\n // Pick the first `page` target that is not a blank/devtools page.\n if (\n selected === null &&\n item.type === 'page' &&\n typeof item.webSocketDebuggerUrl === 'string' &&\n !isBlankOrDevtoolsUrl(item.url)\n ) {\n selected = item;\n }\n }\n\n return { selected, all: [...this.targets.values()] };\n }\n\n listTargets(): CdpTarget[] {\n return [...this.targets.values()];\n }\n\n /**\n * Discover the target, open a direct CDP websocket to its\n * `webSocketDebuggerUrl`, and enable Phase 1+2 domains. Resolves once the\n * socket is open and domain-enable commands are sent. Idempotent — concurrent\n * callers share the in-flight promise.\n */\n async enableDomains(): Promise<void> {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) return;\n if (this.enablingPromise) return this.enablingPromise;\n this.enablingPromise = this._doEnableDomains().finally(() => {\n this.enablingPromise = null;\n });\n return this.enablingPromise;\n }\n\n private async _doEnableDomains(): Promise<void> {\n const { selected } = await this.fetchTargets();\n if (!selected) {\n throw new Error(\n 'No suitable page target found in the local Chromium instance. ' +\n 'Ensure the browser has a non-blank page open and was started with ' +\n '--remote-debugging-port matching devtoolsHttpUrl.',\n );\n }\n\n // Local CDP gives us the per-target WS URL directly — no relay path needed.\n const wsUrl = selected.webSocketDebuggerUrl as string;\n const ws = new WebSocket(wsUrl);\n this.ws = ws;\n\n await new Promise<void>((resolve, reject) => {\n ws.once('open', () => resolve());\n ws.once('error', (err: Error) => reject(err));\n });\n\n ws.on('message', (data: WebSocket.RawData) => this.handleMessage(data.toString()));\n\n // Enable the same domain set as ChiiCdpConnection so all tools work identically.\n this.sendFireAndForget('Runtime.enable');\n this.sendFireAndForget('Network.enable');\n this.sendFireAndForget('DOM.enable');\n this.sendFireAndForget('Page.enable');\n }\n\n /** Fire-and-forget CDP message (used for `*.enable`, no result awaited). */\n private sendFireAndForget(method: string, params: Record<string, unknown> = {}): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n const id = this.nextCommandId++;\n this.ws.send(JSON.stringify({ id, method, params }));\n }\n\n /**\n * Issue a CDP command and resolve with its typed result. Rejects on a CDP\n * error frame or when no websocket is open.\n */\n send<M extends CdpCommandName>(\n method: M,\n params?: CdpCommandMap[M]['params'],\n ): Promise<CdpCommandMap[M]['result']> {\n return this.sendCommand(method, (params ?? {}) as Record<string, unknown>) as Promise<\n CdpCommandMap[M]['result']\n >;\n }\n\n /**\n * Issue an arbitrary request→response command and resolve with its raw\n * result. Both the typed CDP `send` and any AIT domain commands build on this.\n */\n sendCommand(method: string, params: Record<string, unknown> = {}): Promise<unknown> {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n return Promise.reject(\n new Error(\n 'No local Chromium page attached yet. Call enableDomains() first and ensure ' +\n 'the browser is running with --remote-debugging-port.',\n ),\n );\n }\n const id = this.nextCommandId++;\n const ws = this.ws;\n return new Promise<unknown>((resolve, reject) => {\n this.pending.set(id, { resolve, reject });\n ws.send(JSON.stringify({ id, method, params }));\n });\n }\n\n private handleMessage(raw: string): void {\n const message = parseInbound(raw);\n if (!message) return;\n\n // Command response (has an id matching a pending request).\n if (typeof message.id === 'number' && this.pending.has(message.id)) {\n const waiter = this.pending.get(message.id);\n this.pending.delete(message.id);\n if (waiter) {\n if (message.error) waiter.reject(new Error(message.error.message));\n else waiter.resolve(message.result);\n }\n return;\n }\n\n // Event (buffered for the Phase 1 stream tools).\n if (typeof message.method !== 'string') return;\n if (!this.buffers.has(message.method as CdpEventName)) return;\n const event = message.method as CdpEventName;\n const buffer = this.buffers.get(event);\n if (!buffer) return;\n buffer.push(message.params);\n if (buffer.length > this.bufferSize) buffer.shift();\n this.emitter.emit(event, message.params);\n }\n\n getBufferedEvents<E extends CdpEventName>(event: E): ReadonlyArray<CdpEventMap[E]> {\n const buffer = this.buffers.get(event);\n return (buffer ?? []) as ReadonlyArray<CdpEventMap[E]>;\n }\n\n on<E extends CdpEventName>(event: E, listener: (payload: CdpEventMap[E]) => void): () => void {\n this.emitter.on(event, listener as (payload: unknown) => void);\n return () => this.emitter.off(event, listener as (payload: unknown) => void);\n }\n\n /** Close the local CDP websocket and reject any in-flight commands. */\n close(): void {\n this.ws?.close();\n this.ws = null;\n for (const waiter of this.pending.values()) {\n waiter.reject(new Error('Local Chromium CDP connection closed.'));\n }\n this.pending.clear();\n }\n}\n\n/** True for URLs that should be skipped when selecting a page target. */\nfunction isBlankOrDevtoolsUrl(url: string): boolean {\n return (\n url === '' ||\n url === 'about:blank' ||\n url === 'about:newtab' ||\n url.startsWith('devtools://') ||\n url.startsWith('chrome://') ||\n url.startsWith('chrome-extension://')\n );\n}\n","/**\n * Chromium launcher for the local debug mode (env 1).\n *\n * Launch decision rationale:\n * - `chrome-launcher` (npm) is purpose-built and finds installed Chrome, but\n * adds a runtime dependency to the MCP bundle. The repo already has a clear\n * \"external dependency minimization\" policy; `chrome-launcher` is not worth\n * pulling in for what is essentially `spawn(chromeBin, [...flags])`.\n * - Playwright is a devDependency used for E2E only — pulling `chromium.launch`\n * into the runtime MCP path would add ~100 MB of bundled Chromium to the\n * production install and break the \"devDep = e2e only\" boundary.\n * - `child_process.spawn` with a platform-aware binary search is the lightest\n * option: zero new dependencies, portable across macOS/Linux/Windows, and\n * trivially testable by injecting a `spawnFn`.\n *\n * The launcher finds an installed Chrome/Chromium using a prioritized list of\n * well-known binary paths per platform, then spawns it with:\n * --remote-debugging-port=<port>\n * --no-first-run\n * --no-default-browser-check\n * <devUrl>\n *\n * `pnpm dev` is started by the user; the MCP only launches the browser pointing\n * at it.\n *\n * Node-only.\n */\n\nimport { type ChildProcess, spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport net from 'node:net';\nimport { platform } from 'node:os';\n\n/** A handle returned by `launchChromium`. */\nexport interface ChromiumHandle {\n /** The port Chromium is listening on for CDP (`--remote-debugging-port`). */\n port: number;\n /** Devtools HTTP base URL, e.g. `http://127.0.0.1:9222`. */\n devtoolsUrl: string;\n /** Stop the Chromium child process. */\n stop(): void;\n}\n\nexport interface LaunchChromiumOptions {\n /**\n * CDP remote debugging port. If 0 or omitted, an ephemeral free port is\n * chosen automatically.\n */\n port?: number;\n /**\n * URL to open in the browser. Defaults to `AIT_DEVTOOLS_URL` env var or\n * `http://localhost:5173`.\n */\n devUrl?: string;\n /**\n * Extra Chromium flags appended to the spawn command. Use with caution.\n */\n extraArgs?: string[];\n /**\n * Injectable `spawn` function for unit testing — defaults to Node's\n * `child_process.spawn`. Tests inject a fake to avoid launching a real browser.\n */\n spawnFn?: typeof spawn;\n}\n\n/**\n * Find an ephemeral free TCP port by briefly binding a server on port 0.\n * Resolves with the OS-assigned port number.\n */\nexport function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address();\n const port = typeof addr === 'object' && addr !== null ? addr.port : null;\n server.close(() => {\n if (port === null) {\n reject(new Error('Failed to determine free port from net.Server.'));\n } else {\n resolve(port);\n }\n });\n });\n // net.Server는 EventEmitter를 상속하므로 런타임에 on/once가 있다.\n // TypeScript 6 + @types/node 없는 환경에서 타입 해석이 누락되므로 unknown cast로 우회.\n (server as unknown as { on: (ev: string, fn: (err: unknown) => void) => void }).on(\n 'error',\n reject,\n );\n });\n}\n\n/**\n * Returns an ordered list of Chromium/Chrome binary paths to try for the\n * current platform.\n */\nexport function candidateChromePaths(): string[] {\n const os = platform();\n if (os === 'darwin') {\n return [\n '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',\n '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',\n '/Applications/Chromium.app/Contents/MacOS/Chromium',\n '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',\n ];\n }\n if (os === 'linux') {\n return [\n '/usr/bin/google-chrome',\n '/usr/bin/google-chrome-stable',\n '/usr/bin/chromium',\n '/usr/bin/chromium-browser',\n '/usr/local/bin/google-chrome',\n '/usr/local/bin/chromium',\n '/snap/bin/chromium',\n ];\n }\n if (os === 'win32') {\n const programFiles = process.env.PROGRAMFILES ?? 'C:\\\\Program Files';\n const programFilesX86 = process.env['PROGRAMFILES(X86)'] ?? 'C:\\\\Program Files (x86)';\n return [\n `${programFiles}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n `${programFilesX86}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n `${programFiles}\\\\Chromium\\\\Application\\\\chrome.exe`,\n ];\n }\n return [];\n}\n\n/** Find the first Chrome/Chromium binary that exists on this machine. */\nexport function findChromeBinary(): string | null {\n for (const p of candidateChromePaths()) {\n if (existsSync(p)) return p;\n }\n return null;\n}\n\n/**\n * Launch a local Chromium instance with CDP remote debugging enabled.\n *\n * The caller is responsible for calling `handle.stop()` when done.\n *\n * @throws if no Chrome/Chromium binary is found on the system.\n */\nexport async function launchChromium(options: LaunchChromiumOptions = {}): Promise<ChromiumHandle> {\n const spawnImpl = options.spawnFn ?? spawn;\n\n // Resolve the CDP port — find a free one if not specified.\n const requestedPort = options.port ?? 0;\n const port = requestedPort === 0 ? await findFreePort() : requestedPort;\n\n const devUrl = options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n\n const binary = findChromeBinary();\n if (binary === null) {\n throw new Error(\n 'No Chrome/Chromium binary found on this system. ' +\n 'Install Google Chrome or Chromium and try again. ' +\n 'Searched: ' +\n candidateChromePaths().join(', '),\n );\n }\n\n const args = [\n `--remote-debugging-port=${port}`,\n '--no-first-run',\n '--no-default-browser-check',\n // Use a separate profile dir so the debugged instance doesn't interfere\n // with the user's regular Chrome profile.\n '--user-data-dir=/tmp/ait-devtools-chromium-profile',\n ...(options.extraArgs ?? []),\n devUrl,\n ];\n\n const child: ChildProcess = spawnImpl(binary, args, {\n // Detach stdio so the MCP server's stdio transport is not contaminated.\n stdio: 'ignore',\n detached: false,\n });\n\n // Allow the Node process to exit even if the child is still running.\n child.unref();\n\n const devtoolsUrl = `http://127.0.0.1:${port}`;\n\n process.stderr.write(\n `[ait-local-debug] Launched Chromium: ${binary}\\n` +\n `[ait-local-debug] CDP endpoint: ${devtoolsUrl}\\n` +\n `[ait-local-debug] Opening: ${devUrl}\\n`,\n );\n\n return {\n port,\n devtoolsUrl,\n stop(): void {\n try {\n child.kill();\n } catch {\n // Ignore — the child may have already exited.\n }\n },\n };\n}\n","import type { StringKey } from './ko.js';\n\n// English translations. Mirrors every key in `ko.ts`; missing keys fall back to\n// the key string at runtime (see `t()` in index.ts), but the `Record<StringKey,\n// string>` type below means a missing key will typecheck-fail.\n\nexport const en: Record<StringKey, string> = {\n // Panel chrome\n 'panel.title': 'AIT DevTools',\n 'panel.toggle.title': 'AIT DevTools',\n 'panel.close': 'Close',\n 'panel.editMode.on': 'EDIT',\n 'panel.editMode.off': 'READ-ONLY',\n 'panel.editMode.toggleTitle': 'Toggle panel edit mode',\n 'panel.tabError': 'Error rendering \"{tab}\" tab.',\n\n // Tab names\n 'panel.tab.env': 'Environment',\n 'panel.tab.presets': 'Presets',\n 'panel.tab.viewport': 'Viewport',\n 'panel.tab.permissions': 'Permissions',\n 'panel.tab.notifications': 'Notifications',\n 'panel.tab.location': 'Location',\n 'panel.tab.device': 'Device',\n 'panel.tab.iap': 'IAP',\n 'panel.tab.ads': 'Ads',\n 'panel.tab.events': 'Events',\n 'panel.tab.analytics': 'Analytics',\n 'panel.tab.storage': 'Storage',\n\n // Common\n 'common.readOnly': 'Read-only — mock responses are controlled at build time.',\n\n // Consent toast\n 'toast.consent.title': 'Send anonymous usage stats?',\n 'toast.consent.body':\n 'We collect anonymous events only, to improve the tool. You can turn this off anytime in the Environment tab.',\n 'toast.consent.learnMore': 'Learn more',\n 'toast.consent.accept': 'Yes, send',\n 'toast.consent.deny': 'No, thanks',\n\n // Environment tab\n 'env.section.platform': 'Platform',\n 'env.row.os': 'OS',\n 'env.row.appVersion': 'App Version',\n 'env.row.environment': 'Environment',\n 'env.row.locale': 'Locale',\n 'env.section.network': 'Network',\n 'env.row.networkStatus': 'Status',\n 'env.section.safeArea': 'Safe Area Insets',\n 'env.row.safeArea.top': 'Top',\n 'env.row.safeArea.bottom': 'Bottom',\n 'env.section.navigation': 'Navigation',\n 'env.row.iosSwipeGesture': 'iOS swipe-back',\n 'env.value.iosSwipeGesture.unset': 'not called',\n 'env.value.iosSwipeGesture.enabled': 'enabled',\n 'env.value.iosSwipeGesture.disabled': 'disabled',\n 'env.hint.iosSwipeGesture':\n 'Last value passed to setIosSwipeGestureEnabled. Switching Environment to toss lets a toss-gated guard toggle this.',\n\n // Environment > Telemetry section\n 'env.telemetry.section': 'Telemetry',\n // Tier 0 — opt-out anonymous signal\n 'env.telemetry.t0Row': 'Anonymous usage signal (Tier 0)',\n 'env.telemetry.t0On': 'On',\n 'env.telemetry.t0Off': 'Off',\n 'env.telemetry.t0TurnOn': 'Turn on',\n 'env.telemetry.t0TurnOff': 'Turn off',\n 'env.telemetry.t0Desc': 'Version + date only, no PII. Once per day. Helps improve the package.',\n // Tier 1 — opt-in extended telemetry\n 'env.telemetry.row': 'Extended telemetry (Tier 1)',\n 'env.telemetry.on': 'On',\n 'env.telemetry.off': 'Off',\n 'env.telemetry.turnOn': 'Turn on',\n 'env.telemetry.turnOff': 'Turn off',\n 'env.telemetry.anonIdLabel': 'anon_id: {value}',\n 'env.telemetry.anonIdNotSet': '(not yet set)',\n 'env.telemetry.anonIdCopyTitle': 'Click to copy full anon_id',\n 'env.telemetry.deleteBtn': 'Delete my data',\n 'env.telemetry.deleting': 'Deleting…',\n 'env.telemetry.deleted': 'Deleted',\n 'env.telemetry.deleteFailedRetry': 'Delete failed (please retry)',\n 'env.telemetry.deleteFailed': 'Delete failed',\n 'env.telemetry.privacyLink': 'Privacy policy →',\n\n // Environment > Language toggle (new)\n 'env.section.language': 'Language',\n 'env.language.row': 'Language',\n 'env.language.ko': '한국어',\n 'env.language.en': 'English',\n\n // Permissions tab\n 'permissions.section.device': 'Device Permissions',\n\n // Location tab\n 'location.section.current': 'Current Location',\n 'location.row.latitude': 'Latitude',\n 'location.row.longitude': 'Longitude',\n 'location.row.accuracy': 'Accuracy',\n\n // Device tab\n 'device.section.modes': 'Device API Modes',\n 'device.row.camera': 'Camera',\n 'device.row.photos': 'Photos',\n 'device.row.location': 'Location',\n 'device.row.network': 'Network',\n 'device.row.clipboard': 'Clipboard',\n 'device.section.mockImages': 'Mock Images ({count})',\n 'device.btn.add': '+ Add',\n 'device.btn.useDefaults': 'Use defaults',\n 'device.btn.clear': 'Clear',\n 'device.prompt.camera.title': 'Camera Prompt — Select an image',\n 'device.prompt.photos.title': 'Photos Prompt — Select images',\n 'device.prompt.location.title': 'Location Prompt — Enter coordinates',\n 'device.prompt.locationUpdate.title': 'Location Update — Send coordinates',\n 'device.prompt.fallbackTitle': 'Prompt: {type}',\n 'device.prompt.label.lat': 'Lat',\n 'device.prompt.label.lng': 'Lng',\n 'device.prompt.send': 'Send',\n 'device.prompt.cancel': 'Cancel',\n\n // Device tab — Haptic section\n 'device.section.haptic': 'Haptic',\n 'device.haptic.lastCall': 'Last haptic',\n 'device.haptic.noneYet': '(none yet)',\n 'device.haptic.trigger': 'Trigger haptic',\n\n // Viewport tab\n 'viewport.section.device': 'Device',\n 'viewport.row.preset': 'Preset',\n 'viewport.row.orientation': 'Orientation',\n 'viewport.row.notchSide': 'Notch side',\n 'viewport.section.custom': 'Custom size',\n 'viewport.row.width': 'Width (px)',\n 'viewport.row.height': 'Height (px)',\n 'viewport.section.appearance': 'Appearance',\n 'viewport.row.showFrame': 'Show frame',\n 'viewport.row.showAitNavBar': 'Show Apps in Toss nav bar',\n 'viewport.row.navBarType': 'Nav bar type',\n 'viewport.status.noConstraint': 'No viewport constraint — body fills the window.',\n 'viewport.status.cssPhysical': 'CSS / physical',\n 'viewport.status.safeArea': 'Safe area',\n 'viewport.status.aitNavBar': 'AIT nav bar',\n 'viewport.status.aitNavBarValue': '{height}px → SafeArea top · {type}',\n 'viewport.orientation.autoSuffix': '{orient} (auto)',\n\n // IAP tab\n 'iap.section.simulator': 'IAP Simulator',\n 'iap.row.nextResult': 'Next Purchase Result',\n 'iap.section.tossPay': 'TossPay',\n 'iap.row.tossPayResult': 'Next Payment Result',\n 'iap.section.pending': 'Pending Orders ({count})',\n 'iap.empty.pending': '(no pending orders)',\n 'iap.section.completed': 'Completed Orders ({count})',\n 'iap.empty.completed': '(no completed orders)',\n 'iap.btn.complete': 'Complete',\n 'iap.label.pending': 'PENDING',\n\n // Events tab\n 'events.section.navigation': 'Navigation Events',\n 'events.btn.triggerBack': 'Trigger Back Event',\n 'events.btn.triggerHome': 'Trigger Home Event',\n 'events.section.login': 'Login',\n 'events.row.loggedIn': 'Logged In',\n 'events.row.tossLoginIntegrated': 'Toss Login Integrated',\n\n // Analytics tab\n 'analytics.section.log': 'Analytics Log ({count})',\n 'analytics.btn.clear': 'Clear',\n 'analytics.calls.section': 'SDK Calls ({count})',\n 'analytics.calls.btn.clear': 'Clear',\n 'analytics.calls.empty': '(no SDK calls yet)',\n\n // Storage tab\n 'storage.section.title': 'Storage ({count} items)',\n 'storage.btn.clearAll': 'Clear All',\n 'storage.empty': 'No items in storage',\n\n // Presets tab\n 'presets.section.builtIn': 'Built-in scenarios',\n 'presets.section.saved': 'Saved presets ({count})',\n 'presets.section.save': 'Save',\n 'presets.save.description': 'Capture network / permissions / auth / IAP / ads / payment slices.',\n 'presets.btn.saveCurrent': 'Save current as preset',\n 'presets.btn.apply': 'Apply',\n 'presets.btn.reApply': 'Re-apply',\n 'presets.btn.delete': 'Delete',\n 'presets.empty.saved': 'No saved presets yet.',\n 'presets.empty.builtIn': 'No built-in presets.',\n 'presets.prompt.label': 'Preset label?',\n 'presets.confirm.delete': 'Delete preset \"{label}\"?',\n\n // Ads tab\n 'ads.section.state': 'Ads State',\n 'ads.row.isLoaded': 'isLoaded',\n 'ads.row.forceNoFill': 'Force \"no fill\"',\n 'ads.empty.events': 'No events yet',\n 'ads.section.googleAdMob': 'GoogleAdMob',\n 'ads.section.tossAds': 'TossAds',\n 'ads.section.fullScreenAd': 'FullScreenAd',\n 'ads.btn.load': 'Load',\n 'ads.btn.show': 'Show',\n 'ads.section.tossAdsBanner': 'TossAds Banner',\n 'ads.row.rewardUnitType': 'Reward unit type',\n 'ads.row.rewardAmount': 'Reward amount',\n 'ads.btn.render': 'Render',\n 'ads.btn.noFill': 'No-fill',\n 'ads.btn.click': 'Click',\n 'ads.btn.destroy': 'Destroy',\n\n // Notifications tab\n 'notifications.section.title': 'requestNotificationAgreement',\n 'notifications.option.newAgreement': 'newAgreement (first-time agree)',\n 'notifications.option.alreadyAgreed': 'alreadyAgreed (already opted-in)',\n 'notifications.option.agreementRejected': 'agreementRejected (user declined)',\n\n // qr-http-server — lang switcher (dashboard / attach pages)\n 'dashboard.lang.ko': '한국어',\n 'dashboard.lang.en': 'English',\n\n // qr-http-server — dashboard page (server-side, Node, per-request)\n 'dashboard.title': 'AIT Debug Dashboard',\n 'dashboard.updated': 'Last updated: {ts}',\n 'dashboard.tunnel.section': 'Tunnel status',\n 'dashboard.tunnel.up': 'Connected',\n 'dashboard.tunnel.down': 'Disconnected',\n 'dashboard.attach.section': 'Attach QR',\n 'dashboard.attach.hint': 'Call the build_attach_url MCP tool to show the QR here.',\n 'dashboard.pages.section': 'Connected Pages',\n 'dashboard.pages.empty': 'No attached pages',\n\n // qr-http-server — url-box copy button\n 'dashboard.url.copy': 'Copy',\n 'dashboard.url.copied': 'Copied',\n\n // qr-http-server — inspector open link (#503)\n 'dashboard.inspector.section': 'Inspector',\n 'dashboard.inspector.open': 'Open DevTools',\n 'dashboard.inspector.waiting': 'Attach a page to enable the \"Open DevTools\" button',\n\n // qr-http-server — /inspector stable entry (issue #530)\n 'inspector.error.noTarget': 'No page attached. Attach a device and try again.',\n 'inspector.error.relayDown': 'Relay is not active. Start a relay session first.',\n\n // qr-http-server — attach page (server-side, Node, per-request)\n // Copy branches per session mode into sandbox (env 2) / intoss (env 3·4) families (#468).\n 'attach.title': 'AIT Debug Session — QR Scan',\n 'attach.deployment': 'deployment: {label}',\n 'attach.steps.section': 'How to scan',\n 'attach.faq.section': 'Troubleshooting checklist',\n 'attach.url.section': 'URL (fallback)',\n\n // qr-http-server — attach page mode label (environment visibility, #468)\n 'attach.mode.sandbox': 'env 2 — AITC Sandbox App (PWA)',\n 'attach.mode.intossDev': 'env 3 — intoss-private relay dev',\n 'attach.mode.intossLive': 'env 4 — intoss live relay debug',\n\n // attach page — sandbox family (env 2: launcher PWA; no Toss app / _deploymentId concepts)\n 'attach.sandbox.step1':\n 'Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).',\n 'attach.sandbox.step2':\n 'Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.',\n 'attach.sandbox.step3':\n 'The mini-app opens fullscreen and the debug session attaches automatically.',\n 'attach.sandbox.faq.notInstalled':\n '<strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen',\n 'attach.sandbox.faq.cameraApp':\n '<strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner',\n 'attach.sandbox.faq.totp':\n '<strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code',\n 'attach.sandbox.faq.chii':\n '<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import',\n\n // attach page — intoss family (env 3·4: Toss app deep-link)\n 'attach.intoss.step1': 'Open the Toss app.',\n 'attach.intoss.step2': 'Scan the QR code with your phone camera app.',\n 'attach.intoss.step3': 'Tap <strong>\"Open in Toss\"</strong> when the popup appears.',\n 'attach.intoss.step4': 'The mini-app opens and the debug session attaches automatically.',\n 'attach.intoss.faq.appNotOpen':\n '<strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)',\n 'attach.intoss.faq.prepare':\n '<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter',\n 'attach.intoss.faq.chii':\n '<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import',\n 'attach.intoss.faq.totp':\n '<strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server',\n // env 4 (relay-live) only — appended to the intoss family at runtime (#468).\n 'attach.intoss.faq.liveReadOnly':\n '<strong>LIVE session is read-only</strong> — <code>call_sdk</code>/<code>evaluate</code> require an explicit <code>confirm</code>',\n\n // Launcher PWA\n 'launcher.title': 'AITC DevTools Launcher',\n 'launcher.description': 'Scan the terminal QR code or paste the tunnel URL.',\n 'launcher.installCta': 'Install launcher to your phone',\n 'launcher.urlPlaceholder': 'https://example.trycloudflare.com',\n 'launcher.openBtn': 'Open',\n 'launcher.scanBtn': 'Scan QR with camera',\n 'launcher.noCamera': 'No camera available — paste the URL instead.',\n 'launcher.cameraError': 'Could not access the camera — paste the URL instead.',\n 'launcher.invalidUrlHttps': 'Enter a valid https:// URL (the tunnel URL from your terminal).',\n 'launcher.invalidUrl': 'Enter a valid http(s):// URL.',\n 'launcher.debugAuthFailed': 'Debug connection authentication failed',\n 'launcher.debugAuthFailedHint': 'The QR code may have expired. Scan a fresh QR code.',\n 'launcher.debugAuthExpiredHint':\n 'The debug session has expired. Scan a fresh QR from the attach page on your Mac.',\n 'launcher.debugAuthRescanCta': 'Scan a new QR',\n 'launcher.diagTitle': 'Viewport diagnostics',\n 'launcher.diagYes': 'yes',\n 'launcher.diagNo': 'no',\n 'launcher.letterboxDetected':\n 'An iOS viewport constraint may clip the bottom {pt}pt — rotating to landscape and back to portrait may resolve it.',\n 'launcher.letterboxClipped':\n 'An iOS viewport bug makes the bottom {pt}pt unusable — rotating to landscape and back to portrait may recover it.',\n // #536: verdict reason labels for diag panel\n 'launcher.diagVerdictLabel': 'Verdict reason',\n 'launcher.diagSafeAreaTrace': 'top re-measure trace',\n 'launcher.diagVerdict.detected': '✓ letterbox correction',\n 'launcher.diagVerdict.notStandalone': 'not standalone',\n 'launcher.diagVerdict.landscape': 'landscape',\n 'launcher.diagVerdict.shortfallTooSmall': 'shortfall too small',\n 'launcher.diagVerdict.safeAreaTopZero': 'top=0 (env() stale?)',\n // Nav-bar emulation (#495/#510)\n 'launcher.navbar.defaultTitle': 'Mini App',\n 'launcher.navbar.back': 'Back',\n 'launcher.navbar.menu': 'Menu',\n 'launcher.navbar.close': 'Close',\n 'launcher.navbar.menuRescan': 'Rescan',\n 'launcher.navbar.menuDiag': 'Viewport diagnostics',\n 'launcher.navbar.menuLanguage': 'Language',\n};\n","// Korean string catalog (source of truth — keys are typed from this file).\n// Keys follow `<area>.<purpose>` convention. Variable interpolation uses\n// `{name}` placeholders resolved by `t(key, { name: value })`.\n//\n// Some chrome (button labels like \"Load\", \"Show\", \"Clear\", \"Apply\") is left as\n// English in both locales because the panel is an internal devtools surface\n// and these terms are universally recognised by developers in both locales.\n\nexport const ko = {\n // Panel chrome\n 'panel.title': 'AIT DevTools',\n 'panel.toggle.title': 'AIT DevTools',\n 'panel.close': 'Close',\n 'panel.editMode.on': 'EDIT',\n 'panel.editMode.off': 'READ-ONLY',\n 'panel.editMode.toggleTitle': '패널 편집 모드 전환',\n 'panel.tabError': '\"{tab}\" 탭 렌더링 중 오류가 발생했습니다.',\n\n // Tab names\n 'panel.tab.env': 'Environment',\n 'panel.tab.presets': 'Presets',\n 'panel.tab.viewport': 'Viewport',\n 'panel.tab.permissions': 'Permissions',\n 'panel.tab.notifications': 'Notifications',\n 'panel.tab.location': 'Location',\n 'panel.tab.device': 'Device',\n 'panel.tab.iap': 'IAP',\n 'panel.tab.ads': 'Ads',\n 'panel.tab.events': 'Events',\n 'panel.tab.analytics': 'Analytics',\n 'panel.tab.storage': 'Storage',\n\n // Common\n 'common.readOnly': '읽기 전용 — mock 응답은 빌드 타임에 고정됩니다.',\n\n // Consent toast\n 'toast.consent.title': '익명 사용 통계를 보낼까요?',\n 'toast.consent.body': '도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.',\n 'toast.consent.learnMore': '더 알아보기',\n 'toast.consent.accept': '네, 보낼게요',\n 'toast.consent.deny': '아니요',\n\n // Environment tab\n 'env.section.platform': 'Platform',\n 'env.row.os': 'OS',\n 'env.row.appVersion': 'App Version',\n 'env.row.environment': 'Environment',\n 'env.row.locale': 'Locale',\n 'env.section.network': 'Network',\n 'env.row.networkStatus': 'Status',\n 'env.section.safeArea': 'Safe Area Insets',\n 'env.row.safeArea.top': 'Top',\n 'env.row.safeArea.bottom': 'Bottom',\n 'env.section.navigation': 'Navigation',\n 'env.row.iosSwipeGesture': 'iOS swipe-back',\n 'env.value.iosSwipeGesture.unset': '미호출',\n 'env.value.iosSwipeGesture.enabled': 'enabled',\n 'env.value.iosSwipeGesture.disabled': 'disabled',\n 'env.hint.iosSwipeGesture':\n 'setIosSwipeGestureEnabled의 마지막 호출값. Environment를 toss로 바꾸면 toss-gated 가드가 이 값을 토글합니다.',\n\n // Environment > Telemetry section\n 'env.telemetry.section': 'Telemetry',\n // Tier 0 — opt-out anonymous signal\n 'env.telemetry.t0Row': '익명 사용 신호 (Tier 0)',\n 'env.telemetry.t0On': 'On',\n 'env.telemetry.t0Off': 'Off',\n 'env.telemetry.t0TurnOn': 'Turn on',\n 'env.telemetry.t0TurnOff': 'Turn off',\n 'env.telemetry.t0Desc': '버전·날짜만 수집, PII 없음. 하루 1회. 패키지 개선에 사용됩니다.',\n // Tier 1 — opt-in extended telemetry\n 'env.telemetry.row': '확장 텔레메트리 (Tier 1)',\n 'env.telemetry.on': 'On',\n 'env.telemetry.off': 'Off',\n 'env.telemetry.turnOn': 'Turn on',\n 'env.telemetry.turnOff': 'Turn off',\n 'env.telemetry.anonIdLabel': 'anon_id: {value}',\n 'env.telemetry.anonIdNotSet': '(not yet set)',\n 'env.telemetry.anonIdCopyTitle': '전체 anon_id 복사',\n 'env.telemetry.deleteBtn': '내 데이터 삭제',\n 'env.telemetry.deleting': '삭제 중…',\n 'env.telemetry.deleted': '삭제 완료',\n 'env.telemetry.deleteFailedRetry': '삭제 실패 (다시 시도해주세요)',\n 'env.telemetry.deleteFailed': '삭제 실패',\n 'env.telemetry.privacyLink': '개인정보 처리방침 →',\n\n // Environment > Language toggle (new)\n 'env.section.language': 'Language',\n 'env.language.row': 'Language',\n 'env.language.ko': '한국어',\n 'env.language.en': 'English',\n\n // Permissions tab\n 'permissions.section.device': 'Device Permissions',\n\n // Location tab\n 'location.section.current': 'Current Location',\n 'location.row.latitude': 'Latitude',\n 'location.row.longitude': 'Longitude',\n 'location.row.accuracy': 'Accuracy',\n\n // Device tab\n 'device.section.modes': 'Device API Modes',\n 'device.row.camera': 'Camera',\n 'device.row.photos': 'Photos',\n 'device.row.location': 'Location',\n 'device.row.network': 'Network',\n 'device.row.clipboard': 'Clipboard',\n 'device.section.mockImages': 'Mock Images ({count})',\n 'device.btn.add': '+ Add',\n 'device.btn.useDefaults': 'Use defaults',\n 'device.btn.clear': 'Clear',\n 'device.prompt.camera.title': 'Camera Prompt — 이미지를 선택하세요',\n 'device.prompt.photos.title': 'Photos Prompt — 이미지를 선택하세요',\n 'device.prompt.location.title': 'Location Prompt — 좌표 입력',\n 'device.prompt.locationUpdate.title': 'Location Update — 좌표 전송',\n 'device.prompt.fallbackTitle': 'Prompt: {type}',\n 'device.prompt.label.lat': 'Lat',\n 'device.prompt.label.lng': 'Lng',\n 'device.prompt.send': 'Send',\n 'device.prompt.cancel': 'Cancel',\n\n // Device tab — Haptic section\n 'device.section.haptic': 'Haptic',\n 'device.haptic.lastCall': '마지막 haptic',\n 'device.haptic.noneYet': '(아직 없음)',\n 'device.haptic.trigger': 'Haptic 트리거',\n\n // Viewport tab\n 'viewport.section.device': 'Device',\n 'viewport.row.preset': 'Preset',\n 'viewport.row.orientation': 'Orientation',\n 'viewport.row.notchSide': 'Notch side',\n 'viewport.section.custom': 'Custom size',\n 'viewport.row.width': 'Width (px)',\n 'viewport.row.height': 'Height (px)',\n 'viewport.section.appearance': 'Appearance',\n 'viewport.row.showFrame': 'Show frame',\n 'viewport.row.showAitNavBar': 'Apps in Toss 내비게이션 바 표시',\n 'viewport.row.navBarType': 'Nav bar type',\n 'viewport.status.noConstraint': '뷰포트 제약 없음 — body가 창을 가득 채웁니다.',\n 'viewport.status.cssPhysical': 'CSS / physical',\n 'viewport.status.safeArea': 'Safe area',\n 'viewport.status.aitNavBar': 'AIT nav bar',\n 'viewport.status.aitNavBarValue': '{height}px → SafeArea top · {type}',\n 'viewport.orientation.autoSuffix': '{orient} (auto)',\n\n // IAP tab\n 'iap.section.simulator': 'IAP Simulator',\n 'iap.row.nextResult': 'Next Purchase Result',\n 'iap.section.tossPay': 'TossPay',\n 'iap.row.tossPayResult': 'Next Payment Result',\n 'iap.section.pending': 'Pending Orders ({count})',\n 'iap.empty.pending': '(대기 중인 주문 없음)',\n 'iap.section.completed': 'Completed Orders ({count})',\n 'iap.empty.completed': '(완료된 주문 없음)',\n 'iap.btn.complete': 'Complete',\n 'iap.label.pending': 'PENDING',\n\n // Events tab\n 'events.section.navigation': 'Navigation Events',\n 'events.btn.triggerBack': 'Back 이벤트 발생',\n 'events.btn.triggerHome': 'Home 이벤트 발생',\n 'events.section.login': 'Login',\n 'events.row.loggedIn': 'Logged In',\n 'events.row.tossLoginIntegrated': 'Toss Login Integrated',\n\n // Analytics tab\n 'analytics.section.log': 'Analytics Log ({count})',\n 'analytics.btn.clear': 'Clear',\n 'analytics.calls.section': 'SDK Calls ({count})',\n 'analytics.calls.btn.clear': 'Clear',\n 'analytics.calls.empty': '(아직 SDK 호출 없음)',\n\n // Storage tab\n 'storage.section.title': 'Storage ({count} items)',\n 'storage.btn.clearAll': 'Clear All',\n 'storage.empty': '저장된 항목이 없습니다',\n\n // Presets tab\n 'presets.section.builtIn': 'Built-in scenarios',\n 'presets.section.saved': 'Saved presets ({count})',\n 'presets.section.save': 'Save',\n 'presets.save.description':\n 'network / permissions / auth / IAP / ads / payment 슬라이스를 캡처합니다.',\n 'presets.btn.saveCurrent': '현재 상태를 프리셋으로 저장',\n 'presets.btn.apply': 'Apply',\n 'presets.btn.reApply': 'Re-apply',\n 'presets.btn.delete': 'Delete',\n 'presets.empty.saved': '저장된 프리셋이 아직 없습니다.',\n 'presets.empty.builtIn': '내장 프리셋이 없습니다.',\n 'presets.prompt.label': '프리셋 라벨을 입력하세요',\n 'presets.confirm.delete': '\"{label}\" 프리셋을 삭제할까요?',\n\n // Ads tab\n 'ads.section.state': 'Ads State',\n 'ads.row.isLoaded': 'isLoaded',\n 'ads.row.forceNoFill': '강제 \"no fill\"',\n 'ads.empty.events': '아직 이벤트가 없습니다',\n 'ads.section.googleAdMob': 'GoogleAdMob',\n 'ads.section.tossAds': 'TossAds',\n 'ads.section.fullScreenAd': 'FullScreenAd',\n 'ads.btn.load': 'Load',\n 'ads.btn.show': 'Show',\n 'ads.section.tossAdsBanner': 'TossAds 배너',\n 'ads.row.rewardUnitType': '리워드 단위 타입',\n 'ads.row.rewardAmount': '리워드 수량',\n 'ads.btn.render': 'Render',\n 'ads.btn.noFill': 'No-fill',\n 'ads.btn.click': 'Click',\n 'ads.btn.destroy': 'Destroy',\n\n // Notifications tab\n 'notifications.section.title': 'requestNotificationAgreement',\n 'notifications.option.newAgreement': 'newAgreement (최초 동의)',\n 'notifications.option.alreadyAgreed': 'alreadyAgreed (이미 동의됨)',\n 'notifications.option.agreementRejected': 'agreementRejected (사용자 거절)',\n\n // qr-http-server — lang switcher (dashboard / attach pages)\n 'dashboard.lang.ko': '한국어',\n 'dashboard.lang.en': 'English',\n\n // qr-http-server — dashboard page (server-side, Node, per-request)\n 'dashboard.title': 'AIT 디버그 Dashboard',\n 'dashboard.updated': '마지막 갱신: {ts}',\n 'dashboard.tunnel.section': '터널 상태',\n 'dashboard.tunnel.up': '연결됨',\n 'dashboard.tunnel.down': '끊어짐',\n 'dashboard.attach.section': 'Attach QR',\n 'dashboard.attach.hint': 'build_attach_url MCP tool을 호출하면 QR이 여기에 표시됩니다.',\n 'dashboard.pages.section': '연결된 Pages',\n 'dashboard.pages.empty': 'attach된 페이지 없음',\n\n // qr-http-server — url-box copy button\n 'dashboard.url.copy': '복사',\n 'dashboard.url.copied': '복사됨',\n\n // qr-http-server — inspector open link (#503)\n 'dashboard.inspector.section': '인스펙터',\n 'dashboard.inspector.open': '디버그 툴 열기',\n 'dashboard.inspector.waiting': '페이지를 attach하면 \"디버그 툴 열기\" 버튼이 표시됩니다',\n\n // qr-http-server — /inspector stable entry (issue #530)\n 'inspector.error.noTarget': '연결된 페이지가 없습니다. 기기를 attach한 후 다시 시도하세요.',\n 'inspector.error.relayDown': 'relay가 활성화되지 않았습니다. start_debug로 relay를 기동하세요.',\n\n // qr-http-server — attach page (server-side, Node, per-request)\n // 카피는 세션 mode별로 sandbox(환경 2) / intoss(환경 3·4) family로 분기한다 (#468).\n 'attach.title': 'AIT 디버그 세션 — QR 스캔',\n 'attach.deployment': 'deployment: {label}',\n 'attach.steps.section': '스캔 절차',\n 'attach.faq.section': '진단 체크리스트',\n 'attach.url.section': 'URL (fallback)',\n\n // qr-http-server — attach page mode 라벨 (환경 가시화, #468)\n 'attach.mode.sandbox': '환경 2 — AITC Sandbox App (PWA)',\n 'attach.mode.intossDev': '환경 3 — intoss-private relay dev',\n 'attach.mode.intossLive': '환경 4 — intoss live relay debug',\n\n // attach page — sandbox family (환경 2: launcher PWA, 토스 앱·_deploymentId 개념 없음)\n 'attach.sandbox.step1':\n '홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).',\n 'attach.sandbox.step2':\n 'launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.',\n 'attach.sandbox.step3': '미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.',\n 'attach.sandbox.faq.notInstalled':\n '<strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요',\n 'attach.sandbox.faq.cameraApp':\n '<strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요',\n 'attach.sandbox.faq.totp':\n '<strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요',\n 'attach.sandbox.faq.chii':\n '<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인',\n\n // attach page — intoss family (환경 3·4: 토스 앱 deep-link)\n 'attach.intoss.step1': '토스 앱을 실행하세요.',\n 'attach.intoss.step2': '폰 카메라 앱으로 QR 코드를 스캔하세요.',\n 'attach.intoss.step3': '팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.',\n 'attach.intoss.step4': '미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.',\n 'attach.intoss.faq.appNotOpen':\n '<strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)',\n 'attach.intoss.faq.prepare':\n '<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인',\n 'attach.intoss.faq.chii':\n '<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인',\n 'attach.intoss.faq.totp':\n '<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인',\n // 환경 4(relay-live) 전용 — intoss family에 런타임으로 한 줄 추가된다 (#468).\n 'attach.intoss.faq.liveReadOnly':\n '<strong>LIVE 세션은 read-only입니다</strong> — <code>call_sdk</code>/<code>evaluate</code> 실행에는 명시적 <code>confirm</code>이 필요합니다',\n\n // Launcher PWA\n 'launcher.title': 'AITC DevTools Launcher',\n 'launcher.description': '터미널 QR을 스캔하거나 URL을 입력하세요.',\n 'launcher.installCta': '폰에 런처 설치하기',\n 'launcher.urlPlaceholder': 'https://example.trycloudflare.com',\n 'launcher.openBtn': 'Open',\n 'launcher.scanBtn': 'QR 카메라로 스캔',\n 'launcher.noCamera': '카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.',\n 'launcher.cameraError': '카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.',\n 'launcher.invalidUrlHttps': '올바른 https:// URL을 입력하세요 (터미널의 터널 URL).',\n 'launcher.invalidUrl': '올바른 http(s):// URL을 입력하세요.',\n 'launcher.debugAuthFailed': '디버그 연결 인증 실패',\n 'launcher.debugAuthFailedHint': 'QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.',\n 'launcher.debugAuthExpiredHint':\n '디버그 세션이 만료됐어요. Mac의 attach 페이지에서 새 QR을 스캔하세요.',\n 'launcher.debugAuthRescanCta': '새 QR 스캔하기',\n 'launcher.diagTitle': '뷰포트 진단',\n 'launcher.diagYes': '예',\n 'launcher.diagNo': '아니요',\n 'launcher.letterboxDetected':\n 'iOS 뷰포트 제약으로 화면 아래 {pt}pt가 잘릴 수 있습니다 — 기기를 가로로 돌렸다 세로로 복귀하면 해소될 수 있어요.',\n 'launcher.letterboxClipped':\n 'iOS 뷰포트 버그로 화면 아래 {pt}pt를 쓸 수 없습니다 — 기기를 가로로 돌렸다 세로로 돌리면 복구될 수 있어요.',\n // #536: verdict reason labels for diag panel\n 'launcher.diagVerdictLabel': '판정 사유',\n 'launcher.diagSafeAreaTrace': 'top 재측정 추이',\n 'launcher.diagVerdict.detected': '✓ letterbox 보정',\n 'launcher.diagVerdict.notStandalone': '홈 화면 앱 아님',\n 'launcher.diagVerdict.landscape': '가로 모드',\n 'launcher.diagVerdict.shortfallTooSmall': '높이 차이 미달',\n 'launcher.diagVerdict.safeAreaTopZero': 'top=0 (env() stale?)',\n // Nav-bar emulation (#495/#510)\n 'launcher.navbar.defaultTitle': '미니앱',\n 'launcher.navbar.back': '뒤로가기',\n 'launcher.navbar.menu': '메뉴',\n 'launcher.navbar.close': '닫기',\n 'launcher.navbar.menuRescan': '다시 스캔',\n 'launcher.navbar.menuDiag': '뷰포트 진단',\n 'launcher.navbar.menuLanguage': '언어',\n} as const;\n\nexport type StringKey = keyof typeof ko;\n","/**\n * Vanilla TS i18n for the floating DevTools panel.\n *\n * Public surface:\n * - `t(key, vars?)` — look up a UI string, with `{name}` placeholder\n * interpolation. Falls back to the key itself if a translation is missing.\n * - `getLocale()` / `setLocale(locale)` — read/persist the active locale.\n * `setLocale` dispatches `__ait:localechange` so the panel can remount.\n * - `detectLocale()` — first-run heuristic from `navigator.language`.\n *\n * `ko` is the source of truth (keys are typed from it). `en` is also a full\n * `Record<StringKey, string>` (devtools is developer-facing, en is a real\n * audience). The `Partial` lookup table preserves the runtime `?? key` safety\n * net even though we ship complete catalogs today.\n */\n\nimport { en } from './en.js';\nimport { ko, type StringKey } from './ko.js';\n\nexport type Locale = 'ko' | 'en';\n\nconst LOCALE_STORAGE_KEY = '__ait_locale';\nconst LOCALE_CHANGE_EVENT = '__ait:localechange';\n\nconst tables: Record<Locale, Partial<Record<StringKey, string>>> = { ko, en };\n\nlet currentLocale: Locale | null = null;\n\nfunction safeReadStorage(): Locale | null {\n if (typeof localStorage === 'undefined') return null;\n try {\n const raw = localStorage.getItem(LOCALE_STORAGE_KEY);\n if (raw === 'ko' || raw === 'en') return raw;\n } catch {\n /* localStorage can throw in privacy modes — fall back silently */\n }\n return null;\n}\n\nfunction safeWriteStorage(locale: Locale): void {\n if (typeof localStorage === 'undefined') return;\n try {\n localStorage.setItem(LOCALE_STORAGE_KEY, locale);\n } catch {\n /* ignore quota / privacy errors */\n }\n}\n\n/**\n * Decide a locale from a BCP-47 language tag. `ko` (and `ko-*`) → `'ko'`,\n * everything else → `'en'`. Shared by the browser (`navigator.language`) and\n * Node (`Accept-Language` header) paths so both resolve identically.\n */\nfunction localeFromLanguageTag(lang: string): Locale {\n return /^ko\\b/i.test(lang) ? 'ko' : 'en';\n}\n\n/**\n * Read `navigator.language` and decide a locale. `ko` (and `ko-*`) → `'ko'`,\n * everything else → `'en'`. Pure function; does not touch storage.\n */\nexport function detectLocale(): Locale {\n if (typeof navigator === 'undefined') return 'en';\n return localeFromLanguageTag(navigator.language ?? '');\n}\n\n/**\n * Decide a locale from an HTTP `Accept-Language` header value. The Node-served\n * surfaces (e.g. the qr-http-server dashboard) have no `navigator`, so the\n * request header is the only language signal. Reads the FIRST language tag\n * (highest priority, ignoring `q=` weights — good enough for ko/en) and feeds\n * it through the same `ko`-vs-`en` heuristic `detectLocale` uses. Returns `'ko'`\n * for an empty/missing header (ko is the primary locale).\n */\nexport function parseAcceptLanguage(header: string | undefined | null): Locale {\n if (!header) return 'ko';\n const first = header.split(',')[0]?.trim().split(';')[0]?.trim() ?? '';\n return localeFromLanguageTag(first);\n}\n\n/**\n * A locale-bound string resolver for surfaces that can't use the in-memory\n * `getLocale()` cache — notably the Node HTTP server, which resolves locale\n * per-request from `Accept-Language` rather than from a process-global. Returns\n * a `t`-compatible closure over the SAME `ko`/`en` tables (single source of\n * truth), so the dashboard/attach HTML shares the exact 169-key catalog the\n * browser surfaces use. The `key: StringKey` signature keeps compile-time key\n * safety on the Node path identical to `t()`.\n */\nexport function resolveLocaleStrings(\n locale: Locale,\n): (key: StringKey, vars?: Record<string, string | number>) => string {\n const table = tables[locale];\n return (key, vars) => {\n const raw = table[key] ?? key;\n if (!vars) return raw;\n return raw.replace(/\\{(\\w+)\\}/g, (match, name: string) => {\n const value = vars[name];\n return value === undefined ? match : String(value);\n });\n };\n}\n\n/**\n * Resolve the active locale, in order:\n * 1. previously set in-memory value (set by `setLocale`)\n * 2. localStorage `__ait_locale`\n * 3. `detectLocale()` from navigator\n */\nexport function getLocale(): Locale {\n if (currentLocale) return currentLocale;\n const stored = safeReadStorage();\n currentLocale = stored ?? detectLocale();\n return currentLocale;\n}\n\n/**\n * Persist a locale choice and notify listeners. The panel listens for\n * `__ait:localechange` and re-mounts so every string re-evaluates.\n */\nexport function setLocale(locale: Locale): void {\n currentLocale = locale;\n safeWriteStorage(locale);\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent(LOCALE_CHANGE_EVENT));\n }\n}\n\n/**\n * Look up a UI string for the current locale. Falls back to the key if missing,\n * so a forgotten key surfaces visibly rather than rendering empty.\n */\nexport function t(key: StringKey, vars?: Record<string, string | number>): string {\n const raw = tables[getLocale()][key] ?? key;\n if (!vars) return raw;\n return raw.replace(/\\{(\\w+)\\}/g, (match, name: string) => {\n const value = vars[name];\n return value === undefined ? match : String(value);\n });\n}\n\nexport type { StringKey };\nexport { LOCALE_CHANGE_EVENT, LOCALE_STORAGE_KEY };\n\n/**\n * Test-only escape hatch — resets the cached in-memory locale so subsequent\n * `getLocale()` calls re-read storage / re-detect. Production code never needs\n * this; tests use it between cases.\n */\nexport function _resetLocaleCacheForTests(): void {\n currentLocale = null;\n}\n","/**\n * dashboard.generated.ts\n *\n * AUTO-GENERATED by scripts/build-dashboard-html.ts — DO NOT EDIT BY HAND.\n * Regenerate: pnpm build:dashboard-html\n *\n * Exports precompiled HTML chrome strings for each locale. Per-request\n * dynamic values are inserted by qr-http-server.ts at runtime via simple\n * string replacement of __PLACEHOLDER__ tokens.\n *\n * Token map (dashboard chrome):\n * __TUNNEL_CLASS__ CSS class: \"status-up\" | \"status-down\"\n * __TUNNEL_STATUS__ localised tunnel status label\n * __ATTACH_SECTION__ QR img+url-box HTML, or hint text\n * __INSPECTOR_SECTION__ inspector link <a> or waiting hint <span> (#503)\n * __PAGES_SECTION__ pages <section> block, or empty string\n * __NOW__ ISO timestamp of current render\n * __LANG_SWITCHER__ ko/en toggle links (href preserves existing query params)\n *\n * Token map (attach chrome — precompiled per locale × copy family, #468):\n * __QR_DATA_URL__ base64 data URL for the QR image\n * __SAFE_LABEL__ HTML-escaped deploymentId label (intoss family only)\n * __SAFE_ATTACH_URL__ HTML-escaped attach URL\n * __MODE_LABEL__ environment badge (<p class=\"mode-label\">…</p>), or empty\n * __LIVE_FAQ__ env-4 LIVE read-only <li>, or empty (intoss family only)\n * __LANG_SWITCHER__ ko/en toggle links (href preserves existing query params)\n *\n * SECRET-HANDLING: wssUrl MUST NOT appear here. If it does, the build script's\n * assertion would have caught it — this file should be react-free and secret-free.\n */\n\nimport type { Locale } from '../i18n/index.js';\n\n/** Copy family of the attach page chrome (#468) — env 2 vs env 3/4. */\nexport type AttachChromeFamily = 'sandbox' | 'intoss';\n\n// ── locale: ko ──────────────────────────────────────────────────────\n\nexport const dashboardChromeHtmlKo =\n`<!DOCTYPE html>\n<html lang=\"ko\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><title>AIT 디버그 Dashboard</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.updated { font-size: 0.75rem; opacity: 0.4; font-family: monospace; margin: 0; }\nsection { width: 100%; max-width: 520px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\n.status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }\n.status-up { background: #238636; color: #fff; }\n.status-down { background: #6e7681; color: #fff; }\nimg.qr {\n width: min(80vw, 300px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 0.75rem; border-radius: 10px;\n display: block; margin: 0.5rem auto;\n}\n.url-row {\n display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.7rem;\n word-break: break-all; opacity: 0.45;\n background: #161b22; padding: 0.6rem 0.85rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.65; }\n.copy-btn {\n flex-shrink: 0; padding: 0.4rem 0.7rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\n.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\nul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }\nli.empty { opacity: 0.4; list-style: none; padding-left: 0; }\n.page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }\n.page-url { word-break: break-all; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT 디버그 Dashboard</h1>__LANG_SWITCHER__<p class=\"updated\" id=\"updated\">마지막 갱신: __NOW__</p><section><h2>터널 상태</h2><span class=\"status __TUNNEL_CLASS__\" id=\"tunnel-status\">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id=\"attach-section\">__ATTACH_SECTION__</div></section><hr/><section id=\"inspector-section\"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section>__PAGES_SECTION__</body></html>`;\n\nexport const attachChromeHtmlKoSandbox =\n`<!DOCTYPE html>\n<html lang=\"ko\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT 디버그 세션 — QR 스캔</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\n</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"복사\">복사</button></div></section><hr/><section id=\"inspector-section\"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section></body></html>`;\n\nexport const attachChromeHtmlKoIntoss =\n`<!DOCTYPE html>\n<html lang=\"ko\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT 디버그 세션 — QR 스캔</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\n</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<p class=\"label\">deployment: __SAFE_LABEL__</p><div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li>__LIVE_FAQ__</ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"복사\">복사</button></div></section><hr/><section id=\"inspector-section\"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section></body></html>`;\n\n// ── locale: en ──────────────────────────────────────────────────────\n\nexport const dashboardChromeHtmlEn =\n`<!DOCTYPE html>\n<html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><title>AIT Debug Dashboard</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.updated { font-size: 0.75rem; opacity: 0.4; font-family: monospace; margin: 0; }\nsection { width: 100%; max-width: 520px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\n.status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }\n.status-up { background: #238636; color: #fff; }\n.status-down { background: #6e7681; color: #fff; }\nimg.qr {\n width: min(80vw, 300px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 0.75rem; border-radius: 10px;\n display: block; margin: 0.5rem auto;\n}\n.url-row {\n display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.7rem;\n word-break: break-all; opacity: 0.45;\n background: #161b22; padding: 0.6rem 0.85rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.65; }\n.copy-btn {\n flex-shrink: 0; padding: 0.4rem 0.7rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\n.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\nul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }\nli.empty { opacity: 0.4; list-style: none; padding-left: 0; }\n.page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }\n.page-url { word-break: break-all; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT Debug Dashboard</h1>__LANG_SWITCHER__<p class=\"updated\" id=\"updated\">Last updated: __NOW__</p><section><h2>Tunnel status</h2><span class=\"status __TUNNEL_CLASS__\" id=\"tunnel-status\">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id=\"attach-section\">__ATTACH_SECTION__</div></section><hr/><section id=\"inspector-section\"><h2>Inspector</h2>__INSPECTOR_SECTION__</section>__PAGES_SECTION__</body></html>`;\n\nexport const attachChromeHtmlEnSandbox =\n`<!DOCTYPE html>\n<html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT Debug Session — QR Scan</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\n</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"Copy\">Copy</button></div></section><hr/><section id=\"inspector-section\"><h2>Inspector</h2>__INSPECTOR_SECTION__</section></body></html>`;\n\nexport const attachChromeHtmlEnIntoss =\n`<!DOCTYPE html>\n<html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT Debug Session — QR Scan</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\n</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<p class=\"label\">deployment: __SAFE_LABEL__</p><div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>\"Open in Toss\"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li>__LIVE_FAQ__</ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"Copy\">Copy</button></div></section><hr/><section id=\"inspector-section\"><h2>Inspector</h2>__INSPECTOR_SECTION__</section></body></html>`;\n\n/** Map from Locale to the precompiled dashboard chrome string. */\nexport const dashboardChromeByLocale: Record<Locale, string> = {\n ko: dashboardChromeHtmlKo,\n en: dashboardChromeHtmlEn,\n};\n\n/** Map from Locale × copy family to the precompiled attach page chrome string (#468). */\nexport const attachChromeByLocale: Record<Locale, Record<AttachChromeFamily, string>> = {\n ko: { sandbox: attachChromeHtmlKoSandbox, intoss: attachChromeHtmlKoIntoss },\n en: { sandbox: attachChromeHtmlEnSandbox, intoss: attachChromeHtmlEnIntoss },\n};\n","/**\n * 로컬 HTTP 서버 — QR 페이지를 `http://127.0.0.1:<port>` 에서 서빙한다.\n *\n * file:// origin 대신 HTTP origin을 쓰는 이유: 브라우저 보안 정책상 file://에서\n * 로드된 페이지는 외부 fetch/script가 전부 차단되며, file:// 절대 경로를 <img src>에\n * 넣으면 브라우저에 따라 빈 화면이 된다. 127.0.0.1 HTTP는 modern 브라우저가 fully trust.\n *\n * INSTALL-GRAPH INVARIANT:\n * 이 모듈은 react/react-dom을 절대 import하지 않는다. dashboard/attach HTML은\n * scripts/build-dashboard-html.ts가 빌드 타임에 precompile해 dashboard.generated.ts\n * (plain string exports)로 커밋한다. 이 모듈은 그 생성된 string만 import한다.\n * check-mcp-react-free.sh 가드가 dist/mcp/cli.js·server.js의 react 유입을 기계적으로 검증.\n *\n * HTML 조립 전략 (token-fill vs runtime builder):\n * - static chrome (head/style/섹션 레이블) → 빌드타임 precompile, dashboard.generated.ts\n * - 동적 부분 → 런타임 string 조립:\n * __NOW__ : per-request ISO timestamp\n * __TUNNEL_CLASS__ : \"status-up\" | \"status-down\"\n * __TUNNEL_STATUS__ : 로컬라이즈된 tunnel 상태 레이블\n * __ATTACH_SECTION__ : QR img+url-box, 또는 hint 텍스트\n * __PAGES_SECTION__ : pages <section> 블록, 또는 빈 문자열 (null → '')\n * - inline SSE <script> → 런타임 suffix로 append (localised string 포함)\n *\n * i18n:\n * GET / 와 GET /attach 라우트에서 req.headers['accept-language']를 읽어\n * parseAcceptLanguage()로 locale 결정. resolveLocaleStrings()로 동적 부분의\n * localised 문자열을 해결. navigator 없음, React hook 없음 (Node 표면).\n *\n * SECRET-HANDLING:\n * - 127.0.0.1 바인딩만 — 외부 노출 0.\n * - attachUrl은 HTML 본문과 /qr.png query에만 들어간다 (의도된 전달 경로).\n * - wssUrl은 dashboard HTML에 절대 들어가지 않는다. tunnel.up boolean만 사용.\n * - stdout/stderr/로그에 별도 출력하지 않는다.\n * - tmp 파일 만들지 않음 — 모든 응답을 메모리에서 생성.\n * - TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — SSE payload나 page 목록 등\n * 다른 필드에 TOTP 코드를 평문으로 싣지 않는다.\n */\n\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\nimport { createServer } from 'node:http';\nimport { parseAcceptLanguage, resolveLocaleStrings } from '../i18n/index.js';\nimport {\n type AttachChromeFamily,\n attachChromeByLocale,\n dashboardChromeByLocale,\n} from './dashboard.generated.js';\nimport type { McpEnvironment } from './environment.js';\n\n/** dashboard에 노출되는 현재 상태 스냅샷. */\nexport interface DashboardState {\n /** 현재 터널 상태 — up/down + wssUrl. SECRET: wssUrl은 로그 출력 금지. */\n tunnel: { up: boolean; wssUrl: string | null };\n /**\n * 현재 연결된 page 목록 (id/url만).\n *\n * - `Array<…>` — env 3/4(MCP): relay에 attach된 페이지를 라이브 조회한 목록.\n * 빈 배열 `[]`은 \"attach된 페이지 없음\"으로 정직하게 표시한다.\n * - `null` — env 2(unplugin 터널): 플러그인 핸들이 connected target을 노출하지\n * 않아 라이브 page 목록을 알 수 없다. 거짓 빈 목록을 보여주느니 \"연결된 Pages\"\n * 섹션 자체를 숨긴다(#411). 정적 렌더와 SSE 갱신 양쪽에서 섹션이 사라진다.\n */\n pages: Array<{ id: string; url: string }> | null;\n /** 마지막으로 생성된 attachUrl (없으면 null). TOTP at= 코드는 이 안에 캡슐화. */\n attachUrl: string | null;\n /**\n * 현재 세션의 Chii 인스펙터 URL — 살아있는 세션 기준 DevTools 진입점 (#503).\n *\n * - `string` — relay up + 페이지 attached → `buildChiiInspectorUrl`로 조립된 URL.\n * TOTP at= 코드는 이 URL 안에 캡슐화. 대시보드 HTML 내 렌더는 의도된 transport.\n * - `null` — relay up이지만 페이지 미첨부, 또는 relay down, 또는 env 1(mock).\n *\n * SECRET-HANDLING: 이 URL은 relay host + TOTP at= 코드를 담을 수 있다.\n * 대시보드 HTML 본문에 렌더되는 건 의도된 transport(attachUrl과 동일 취급)이지만,\n * stdout/stderr/로그/에러 메시지에는 절대 출력하지 않는다.\n */\n inspectorUrl?: string | null;\n /**\n * 현재 세션 환경 — /attach 스캔 절차·체크리스트 카피 분기 + 상단 환경 라벨 (#468).\n *\n * - `'relay-mobile'` → sandbox family (환경 2: launcher PWA 절차, 토스 앱·_deploymentId 없음)\n * - `'relay-dev'` → intoss family (환경 3: 토스 앱 deep-link 절차)\n * - `'relay-live'` → intoss family + LIVE read-only 한 줄 (환경 4)\n * - `'mock'` / 미지정 → intoss family, 환경 라벨 없음 (환경 1은 /attach 표면이\n * 없어 사실상 도달 불가 — legacy 카피 유지 fallback)\n *\n * 호출처는 자기 mode를 명시적으로 전달한다: debug-server는 active connection에서\n * `deriveEnvironment(...)`로 파생, unplugin tunnel 대시보드는 `'relay-mobile'` 고정.\n */\n mode?: McpEnvironment;\n}\n\n/** mode → 어느 precompiled attach chrome family를 쓰는가 (#468). */\nfunction attachFamilyForMode(mode: McpEnvironment | undefined): AttachChromeFamily {\n return mode === 'relay-mobile' ? 'sandbox' : 'intoss';\n}\n\n/**\n * mode → 페이지 상단 환경 라벨 HTML (`__MODE_LABEL__` 토큰 채움, #468).\n * 사용자가 fidelity 사다리의 어느 겹에 있는지 즉시 알게 하는 환경 가시화 배지.\n * mode 미지정/'mock'은 빈 문자열 — 알 수 없는 환경을 거짓으로 라벨링하지 않는다.\n */\nfunction buildModeLabel(\n mode: McpEnvironment | undefined,\n s: ReturnType<typeof resolveLocaleStrings>,\n): string {\n let label: string;\n switch (mode) {\n case 'relay-mobile':\n label = s('attach.mode.sandbox');\n break;\n case 'relay-dev':\n label = s('attach.mode.intossDev');\n break;\n case 'relay-live':\n label = s('attach.mode.intossLive');\n break;\n case 'mock':\n case undefined:\n return '';\n }\n return `<p class=\"mode-label\">${escapeHtml(label)}</p>`;\n}\n\nexport interface QrHttpServer {\n port: number;\n /** `http://127.0.0.1:<port>/attach?u=<encoded>` URL 생성 헬퍼. */\n buildAttachPageUrl(attachUrl: string): string;\n /**\n * 안정 인스펙터 진입점 URL — `http://127.0.0.1:<port>/inspector` (issue #530).\n * 클릭 시점에 TOTP를 mint하고 302 redirect하므로 URL 자체에 시크릿이 없다.\n * 대시보드/stdout/로그 어디든 출력 가능.\n */\n readonly inspectorStableUrl: string;\n /**\n * 상태 변경 시 호출 — SSE 구독자에게 최신 상태를 push한다.\n * `getDashboardState`가 주입돼 있지 않으면 no-op.\n */\n notifyStateChange(): void;\n close(): Promise<void>;\n}\n\n/** HTML 특수문자를 이스케이프한다. */\nfunction escapeHtml(s: string): string {\n return s.replace(/[<>&\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n}\n\n/**\n * 현재 path+query에서 lang 파라미터만 교체한 ko/en 토글 링크를 생성한다.\n *\n * SECRET-HANDLING: u= (attachUrl, TOTP at= 캡슐 포함) 등 기존 query를 보존한다.\n * lang= 만 덮어쓴다. 링크 href에 at= 코드가 들어가는 건 의도된 전달 경로.\n */\nfunction buildLangSwitcher(\n path: string,\n existingParams: URLSearchParams,\n locale: 'ko' | 'en',\n s: ReturnType<typeof resolveLocaleStrings>,\n): string {\n function switcherHref(targetLang: 'ko' | 'en'): string {\n const p = new URLSearchParams(existingParams);\n p.set('lang', targetLang);\n return `${escapeHtml(path)}?${p.toString()}`;\n }\n const koLabel = escapeHtml(s('dashboard.lang.ko'));\n const enLabel = escapeHtml(s('dashboard.lang.en'));\n const koClass = locale === 'ko' ? 'active' : '';\n const enClass = locale === 'en' ? 'active' : '';\n return `<div class=\"lang-switcher\"><a href=\"${switcherHref('ko')}\" class=\"${koClass}\">${koLabel}</a><a href=\"${switcherHref('en')}\" class=\"${enClass}\">${enLabel}</a></div>`;\n}\n\n/**\n * Dashboard HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.\n *\n * 토큰 채우기 순서:\n * 1. chrome string(locale별 precompile)을 가져온다.\n * 2. 동적 부분을 단순 replaceAll로 채운다 (토큰이 HTML context 밖에 있으므로 안전).\n * 3. inline SSE <script>를 </body> 직전에 주입한다.\n *\n * 동적 파트 분류:\n * - \"token-fill\": 단일 값 교체 (__NOW__, __TUNNEL_CLASS__, __TUNNEL_STATUS__,\n * __ATTACH_SECTION__, __INSPECTOR_SECTION__)\n * - \"runtime builder\": 가변 길이 구조 (__PAGES_SECTION__ — 조건부 렌더 + 가변 rows)\n * - \"suffix\": inline SSE <script> (빌드 파이프라인 없는 클라이언트 스크립트, locale\n * aware 문자열 포함)\n *\n * SECRET-HANDLING:\n * - attachUrl은 url-box 안에서만 노출 (TOTP at= 코드 캡슐 그대로).\n * - inspectorUrl은 anchor href 안에서만 노출 (TOTP at= 코드 캡슐 그대로).\n * relay host + TOTP 코드가 담길 수 있으나 대시보드 HTML은 의도된 transport.\n * - tunnel wssUrl은 \"터널 연결됨\" 상태 표시에서 UP/DOWN만 노출.\n * wssUrl 값 자체는 dashboard HTML에 넣지 않는다.\n */\nfunction buildDashboardHtml(\n state: DashboardState,\n qrDataUrl: string | null,\n locale: 'ko' | 'en',\n path = '/',\n params = new URLSearchParams(),\n /**\n * /devtools/ 진입로 URL (issue #248).\n *\n * 주입 시: relay 연결 active + pagesAttached 이면 이 URL로 가는 \"DevTools 열기\" 링크를 렌더한다.\n * null: getDirectInspectorUrl 미주입 (relay 세션 없는 mode) → 링크를 숨기고 hint 표시.\n *\n * /devtools/ 는 relay host + TOTP at= 가 없는 안정 경로이므로 href 노출 가능.\n * SECRET-HANDLING: 링크 클릭 후 302 응답의 Location에만 relay host·TOTP가 담긴다.\n */\n devtoolsEntryUrl: string | null = null,\n): string {\n const s = resolveLocaleStrings(locale);\n const now = new Date().toISOString();\n\n const tunnelStatus = state.tunnel.up ? s('dashboard.tunnel.up') : s('dashboard.tunnel.down');\n const tunnelClass = state.tunnel.up ? 'status-up' : 'status-down';\n\n // attachSection: QR img + url-row(url-box + 복사 버튼), or hint.\n // dashboard 표면에서 SSE 재렌더 시에도 동일 구조를 유지해 복사 버튼이 생존한다.\n let attachSection: string;\n if (qrDataUrl && state.attachUrl) {\n const safeAttachUrl = escapeHtml(state.attachUrl);\n const copyLabel = escapeHtml(s('dashboard.url.copy'));\n attachSection =\n `<img class=\"qr\" src=\"${qrDataUrl}\" alt=\"attach QR\" />` +\n `<div class=\"url-row\">` +\n `<p class=\"url-box\" id=\"url-box\">${safeAttachUrl}</p>` +\n `<button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"${copyLabel}\">${copyLabel}</button>` +\n `</div>`;\n } else {\n attachSection = `<p class=\"hint\">${escapeHtml(s('dashboard.attach.hint'))}</p>`;\n }\n\n // inspectorSection — \"DevTools 열기\" 링크 또는 대기 힌트 (#503, gate 보정 #544, #248).\n //\n // 게이트: relay active(devtoolsEntryUrl 주입됨) + pages.length > 0 양쪽 모두 true 일 때만 링크 활성.\n // - devtoolsEntryUrl null → getDirectInspectorUrl 미주입(relay 세션 없는 server mode) → hint 표시.\n // - relay active이지만 pages 미attach → 버튼을 보여봤자 502 noTarget — hint로 대기 안내.\n //\n // href는 /devtools/ 안정 경로 (issue #248) — relay host·TOTP at= 를 담지 않아 노출 가능.\n // 클릭 시 302 → Location에만 relay host·TOTP가 담긴다(의도된 transport).\n //\n // SSE push 시 inspectorUrl 필드를 기반으로 #inspector-link를 갱신하는 스크립트는 그대로 유지.\n // (SSE에서 inspectorUrl이 null → hint로, non-null + pages > 0 → /devtools/ 링크로 갱신.)\n const pagesAttached = Array.isArray(state.pages) && state.pages.length > 0;\n let inspectorSection: string;\n if (pagesAttached && devtoolsEntryUrl) {\n const safeUrl = escapeHtml(devtoolsEntryUrl);\n const label = escapeHtml(s('dashboard.inspector.open'));\n inspectorSection = `<a class=\"inspector-link\" id=\"inspector-link\" href=\"${safeUrl}\" target=\"_blank\" rel=\"noopener noreferrer\">${label}</a>`;\n } else {\n const hint = escapeHtml(s('dashboard.inspector.waiting'));\n inspectorSection = `<span class=\"inspector-hint\" id=\"inspector-link\">${hint}</span>`;\n }\n\n // pagesSection — \"연결된 Pages\" 섹션: env 3/4(pages: Array)에서만 렌더한다.\n // env 2(pages: null)는 라이브 page 목록을 알 수 없어 섹션 자체를 숨긴다(#411).\n // runtime builder: 조건부 블록 + 가변 row 목록이라 token-fill로는 불충분.\n const pagesSection =\n state.pages === null\n ? ''\n : `<hr /><section id=\"pages-section\"><h2>${escapeHtml(s('dashboard.pages.section'))}</h2><ul id=\"pages-list\">${\n state.pages.length > 0\n ? state.pages\n .map((p) => {\n const safeId = escapeHtml(p.id);\n const safeUrl = escapeHtml(p.url.slice(0, 120));\n return `<li><span class=\"page-id\">${safeId}</span> <span class=\"page-url\">${safeUrl}</span></li>`;\n })\n .join('\\n')\n : `<li class=\"empty\">${escapeHtml(s('dashboard.pages.empty'))}</li>`\n }</ul></section>`;\n\n // locale-aware strings for the inline SSE client script\n const sseStrings: SseScriptStrings = {\n tunnelUp: JSON.stringify(s('dashboard.tunnel.up')),\n tunnelDown: JSON.stringify(s('dashboard.tunnel.down')),\n pagesEmpty: JSON.stringify(s('dashboard.pages.empty')),\n attachHint: JSON.stringify(s('dashboard.attach.hint')),\n copyLabel: JSON.stringify(s('dashboard.url.copy')),\n copiedLabel: JSON.stringify(s('dashboard.url.copied')),\n inspectorOpenLabel: JSON.stringify(s('dashboard.inspector.open')),\n inspectorWaitingLabel: JSON.stringify(s('dashboard.inspector.waiting')),\n dashboardSurface: true,\n };\n\n const langSwitcher = buildLangSwitcher(path, params, locale, s);\n\n // Fill token placeholders in the precompiled chrome.\n // replaceAll is safe because these __TOKEN__ strings cannot appear in\n // any legitimate user-facing value (they are sentinel strings).\n const chrome = dashboardChromeByLocale[locale];\n const filled = chrome\n .replaceAll('__LANG_SWITCHER__', langSwitcher)\n .replaceAll('__NOW__', escapeHtml(now))\n .replaceAll('__TUNNEL_CLASS__', tunnelClass)\n .replaceAll('__TUNNEL_STATUS__', escapeHtml(tunnelStatus))\n .replaceAll('__ATTACH_SECTION__', attachSection)\n .replaceAll('__INSPECTOR_SECTION__', inspectorSection)\n .replaceAll('__PAGES_SECTION__', pagesSection);\n\n // Append the inline SSE <script> suffix directly before </body>.\n // This keeps the client script out of the precompiled chrome (it references\n // locale-aware strings resolved per-request) while staying self-contained.\n const sseScript = buildSseScript(sseStrings);\n return filled.replace('</body>', `${sseScript}\\n</body>`);\n}\n\ninterface SseScriptStrings {\n tunnelUp: string;\n tunnelDown: string;\n pagesEmpty: string;\n attachHint: string;\n /** 복사 버튼 기본 라벨 (JSON.stringify로 이미 escape됨). */\n copyLabel: string;\n /** 복사 완료 피드백 라벨 (JSON.stringify로 이미 escape됨). */\n copiedLabel: string;\n /** \"인스펙터 열기\" 링크 라벨 (JSON.stringify로 이미 escape됨, #503). */\n inspectorOpenLabel: string;\n /** 인스펙터 URL 대기 힌트 (JSON.stringify로 이미 escape됨, #503). */\n inspectorWaitingLabel: string;\n /**\n * true: dashboard 표면 — `#attach-section` innerHTML 전체 교체 방식 유지.\n * url-box 텍스트도 innerHTML 교체로 갱신됨.\n * false: /attach 표면 — img src만 교체, url-box는 `#url-box` textContent만 갱신.\n * 이 분기가 url-box 이중 표시 결함을 방지한다.\n */\n dashboardSurface: boolean;\n}\n\n/**\n * Inline SSE client <script> — injected into the dashboard HTML at runtime.\n *\n * Subscribes to /events and updates the DOM without a build pipeline.\n * client side: attachUrl은 DOM에 렌더링, wssUrl은 절대 렌더링하지 않는다.\n * pages === null 이면 섹션을 건드리지 않는다 (#411).\n *\n * 두 표면(dashboard / attach) 분기:\n * - dashboard (dashboardSurface=true): #attach-section innerHTML 전체 교체 방식 유지.\n * url-box도 innerHTML 재렌더 안에 포함되어 갱신됨.\n * - /attach (dashboardSurface=false): #attach-section의 img src만 교체하고,\n * url-box는 #url-box textContent만 갱신한다. (#attach-section에 url-box가 없으므로\n * innerHTML 교체 시 url-box가 새로 생겨 이중 표시되는 결함을 방지 — #458 결함 수정.)\n *\n * 복사 기능: 이벤트 위임으로 document에 단일 핸들러. innerHTML 재렌더 후에도 생존.\n * - .url-box 클릭 또는 .copy-btn 클릭 → 현재 #url-box textContent 복사.\n * - clipboard: navigator.clipboard.writeText → 실패/부재 시 textarea execCommand fallback.\n * - 피드백: 버튼 라벨이 COPIED_LABEL로 ~1.5초 전환 후 COPY_LABEL로 복귀.\n *\n * 문자열 인자는 빌드타임에 ko/en 테이블에서 가져와 JSON.stringify로 이미 escape됨.\n *\n * SECRET-HANDLING: URL 값을 console.log 등으로 출력하지 않는다.\n */\nfunction buildSseScript(strings: SseScriptStrings): string {\n const isDashboard = strings.dashboardSurface;\n return `<script>\n // SSE — /events 구독해 상태 자동 갱신. 빌드 파이프라인 없는 인라인 스크립트.\n (function () {\n var TUNNEL_UP = ${strings.tunnelUp};\n var TUNNEL_DOWN = ${strings.tunnelDown};\n var PAGES_EMPTY = ${strings.pagesEmpty};\n var ATTACH_HINT = ${strings.attachHint};\n var COPY_LABEL = ${strings.copyLabel};\n var COPIED_LABEL = ${strings.copiedLabel};\n var INSPECTOR_OPEN_LABEL = ${strings.inspectorOpenLabel};\n var INSPECTOR_WAITING_LABEL = ${strings.inspectorWaitingLabel};\n\n // ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────\n function copyText(text) {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n return navigator.clipboard.writeText(text);\n }\n // fallback: textarea + execCommand\n return new Promise(function (resolve, reject) {\n var ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.focus();\n ta.select();\n try {\n document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed'));\n } catch (err) {\n reject(err);\n } finally {\n document.body.removeChild(ta);\n }\n });\n }\n\n // ── 복사 피드백 ───────────────────────────────────────────────────────\n var copyTimer = null;\n function triggerCopy() {\n var urlBox = document.getElementById('url-box');\n if (!urlBox) return;\n var text = urlBox.textContent || '';\n if (!text) return;\n copyText(text).then(function () {\n var btn = document.getElementById('copy-btn');\n if (btn) {\n btn.textContent = COPIED_LABEL;\n if (copyTimer) clearTimeout(copyTimer);\n copyTimer = setTimeout(function () {\n btn.textContent = COPY_LABEL;\n copyTimer = null;\n }, 1500);\n }\n }).catch(function () { /* 복사 실패 시 조용히 무시 */ });\n }\n\n // ── 이벤트 위임 — document 레벨에서 단일 핸들러 (innerHTML 재렌더 후에도 생존) ──\n document.addEventListener('click', function (e) {\n var target = e.target;\n if (!target) return;\n // .copy-btn 또는 .url-box 클릭 시 복사\n if (target.closest && (target.closest('.copy-btn') || target.closest('.url-box'))) {\n triggerCopy();\n }\n });\n\n // ── SSE 구독 ──────────────────────────────────────────────────────────\n var src = new EventSource('/events');\n src.onmessage = function (e) {\n try {\n var s = JSON.parse(e.data);\n // 터널 상태 갱신\n var el = document.getElementById('tunnel-status');\n if (el) {\n el.textContent = s.tunnel && s.tunnel.up ? TUNNEL_UP : TUNNEL_DOWN;\n el.className = 'status ' + (s.tunnel && s.tunnel.up ? 'status-up' : 'status-down');\n }\n // page 목록 갱신 — pages === null(env 2)이면 섹션 자체를 숨긴 채 둔다.\n // 정적 렌더가 #pages-section을 아예 안 그렸으므로 여기서도 손대지 않아\n // SSE push 때 섹션이 되살아나지 않는다(#411). 배열일 때만 목록을 채운다.\n if (s.pages !== null && s.pages !== undefined) {\n var ul = document.getElementById('pages-list');\n if (ul) {\n if (s.pages.length === 0) {\n ul.innerHTML = '<li class=\"empty\">' + PAGES_EMPTY + '</li>';\n } else {\n ul.innerHTML = s.pages.map(function (p) {\n var sid = String(p.id || '').slice(0, 36).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n var su = String(p.url || '').slice(0, 120).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n return '<li><span class=\"page-id\">' + sid + '</span> <span class=\"page-url\">' + su + '</span></li>';\n }).join('');\n }\n }\n }\n // attachUrl QR + url-box 갱신\n // SECRET-HANDLING: URL 값을 로그로 출력하지 않는다.\n var sec = document.getElementById('attach-section');\n if (sec) {\n if (s.attachUrl) {\n var encoded = encodeURIComponent(s.attachUrl);\n var safeUrl = String(s.attachUrl).slice(0, 2000).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n ${\n isDashboard\n ? `// dashboard: #attach-section innerHTML 전체 교체 (img + url-row).\n // url-box id=\"url-box\" 를 포함해 복사 핸들러가 계속 동작함.\n sec.innerHTML =\n '<img class=\"qr\" src=\"/qr.png?u=' + encoded + '\" alt=\"attach QR\" />' +\n '<div class=\\\\\"url-row\\\\\">' +\n '<p class=\\\\\"url-box\\\\\" id=\\\\\"url-box\\\\\">' + safeUrl + '</p>' +\n '<button class=\\\\\"copy-btn\\\\\" id=\\\\\"copy-btn\\\\\" type=\\\\\"button\\\\\" aria-label=\\\\\"' + COPY_LABEL + '\\\\\">' + COPY_LABEL + '</button>' +\n '</div>';`\n : `// /attach: img src만 교체 — url-box는 별도 #url-section에서 관리해 이중 표시 방지(#458).\n // QR img src 교체: img가 있으면 src만 갱신, 없으면 img 요소 생성.\n var img = sec.querySelector('img.qr');\n if (img) {\n img.src = '/qr.png?u=' + encoded;\n } else {\n sec.innerHTML = '<img class=\\\\\"qr\\\\\" src=\\\\\"/qr.png?u=' + encoded + '\\\\\" alt=\\\\\"attach QR\\\\\" />';\n }\n // url-box textContent만 갱신 (innerHTML 교체하지 않아 복사 버튼/핸들러 생존).\n var ub = document.getElementById('url-box');\n if (ub) ub.textContent = s.attachUrl;`\n }\n } else {\n ${\n isDashboard\n ? `sec.innerHTML = '<p class=\\\\\"hint\\\\\">' + ATTACH_HINT + '</p>';`\n : `// /attach에서 hint가 필요한 경우는 없으나 방어 처리.\n sec.innerHTML = '<p class=\\\\\"hint\\\\\">' + ATTACH_HINT + '</p>';`\n }\n }\n }\n // 인스펙터 링크 갱신 — #inspector-link (#503, gate 보정 #544).\n // 게이트: pages.length > 0 (페이지 attach 여부) — inspectorUrl 존재 여부가 아님.\n // #530 이후 inspectorUrl은 항상 안정 URL이므로 null 게이트는 사실상 항상 활성이었다.\n // pages.length > 0 으로 바꿔 미attach 시 대기 힌트를 보여주도록 수정.\n // SECRET-HANDLING: inspectorUrl을 console.log 등으로 출력하지 않는다.\n var insp = document.getElementById('inspector-link');\n if (insp) {\n var pagesAttachedSse = Array.isArray(s.pages) && s.pages.length > 0;\n if (pagesAttachedSse && s.inspectorUrl) {\n var safeInspUrl = String(s.inspectorUrl).slice(0, 2000).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n insp.outerHTML = '<a class=\\\\\"inspector-link\\\\\" id=\\\\\"inspector-link\\\\\" href=\\\\\"' + safeInspUrl + '\\\\\" target=\\\\\"_blank\\\\\" rel=\\\\\"noopener noreferrer\\\\\">' + INSPECTOR_OPEN_LABEL + '</a>';\n } else {\n insp.outerHTML = '<span class=\\\\\"inspector-hint\\\\\" id=\\\\\"inspector-link\\\\\">' + INSPECTOR_WAITING_LABEL + '</span>';\n }\n }\n // 갱신 시각 (dashboard만 #updated 요소 있음)\n var upd = document.getElementById('updated');\n if (upd) upd.textContent = upd.textContent.replace(/[^ ]+$/, new Date().toISOString());\n } catch (_) { /* 파싱 오류 무시 */ }\n };\n src.onerror = function () {\n // 재연결은 EventSource가 자동 처리 (spec 기본 동작).\n };\n })();\n </script>`;\n}\n\n/**\n * Attach 페이지 HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.\n *\n * 동적 파트:\n * - __QR_DATA_URL__ : base64 data URL (QR 이미지)\n * - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)\n * - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)\n * - __MODE_LABEL__ : 환경 배지 (`<p class=\"mode-label\">…</p>` 또는 빈 문자열, #468)\n * - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)\n * - __INSPECTOR_SECTION__ : \"디버그 툴 열기\" 버튼 또는 대기 힌트 (#544)\n *\n * mode-aware 분기 (#468): mode가 `relay-mobile`이면 sandbox family chrome(launcher\n * PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다. `relay-live`는\n * intoss chrome에 LIVE read-only 라인을 추가한다.\n *\n * SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이\n * `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#inspector-link`도 SSE push로\n * pages.length > 0 게이트에 따라 활성/비활성 전환된다 (#544).\n *\n * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.\n * inspectorStableUrl은 /inspector 안정 URL (127.0.0.1, 시크릿 없음) — 노출 가능.\n */\nfunction buildAttachHtml(\n qrDataUrl: string,\n safeLabel: string,\n safeAttachUrl: string,\n locale: 'ko' | 'en',\n path = '/attach',\n params = new URLSearchParams(),\n mode?: McpEnvironment,\n pagesAttached = false,\n inspectorStableUrl: string | null = null,\n): string {\n const s = resolveLocaleStrings(locale);\n const langSwitcher = buildLangSwitcher(path, params, locale, s);\n const family = attachFamilyForMode(mode);\n // 환경 4 전용 LIVE read-only 라인 — i18n 문자열은 신뢰된 빌드타임 카피(strong/code\n // 인라인 HTML 포함)라 verbatim 주입한다 (다른 FAQ 항목과 동일한 취급).\n const liveFaq = mode === 'relay-live' ? `<li>${s('attach.intoss.faq.liveReadOnly')}</li>` : '';\n\n // inspector 섹션 — pages.length > 0 게이트 (#544).\n // inspectorStableUrl은 /inspector 안정 URL (시크릿 없음) — href 노출 가능.\n let inspectorSection: string;\n if (pagesAttached && inspectorStableUrl) {\n const safeUrl = escapeHtml(inspectorStableUrl);\n const label = escapeHtml(s('dashboard.inspector.open'));\n inspectorSection = `<a class=\"inspector-link\" id=\"inspector-link\" href=\"${safeUrl}\" target=\"_blank\" rel=\"noopener noreferrer\">${label}</a>`;\n } else {\n const hint = escapeHtml(s('dashboard.inspector.waiting'));\n inspectorSection = `<span class=\"inspector-hint\" id=\"inspector-link\">${hint}</span>`;\n }\n\n const chrome = attachChromeByLocale[locale][family];\n const filled = chrome\n .replaceAll('__LANG_SWITCHER__', langSwitcher)\n .replaceAll('__MODE_LABEL__', buildModeLabel(mode, s))\n .replaceAll('__LIVE_FAQ__', liveFaq)\n .replaceAll('__QR_DATA_URL__', qrDataUrl)\n .replaceAll('__SAFE_LABEL__', safeLabel)\n .replaceAll('__SAFE_ATTACH_URL__', safeAttachUrl)\n .replaceAll('__INSPECTOR_SECTION__', inspectorSection);\n\n // Inject SSE script so QR auto-refreshes on each /events push,\n // and #inspector-link updates via pages.length > 0 gate on state change.\n // dashboardSurface: false → /attach 표면 분기 (img src 교체, url-box textContent만 갱신).\n const sseStrings: SseScriptStrings = {\n tunnelUp: JSON.stringify(s('dashboard.tunnel.up')),\n tunnelDown: JSON.stringify(s('dashboard.tunnel.down')),\n pagesEmpty: JSON.stringify(s('dashboard.pages.empty')),\n attachHint: JSON.stringify(s('dashboard.attach.hint')),\n copyLabel: JSON.stringify(s('dashboard.url.copy')),\n copiedLabel: JSON.stringify(s('dashboard.url.copied')),\n // /attach 페이지의 #inspector-link SSE 갱신에 쓰인다 (#544).\n inspectorOpenLabel: JSON.stringify(s('dashboard.inspector.open')),\n inspectorWaitingLabel: JSON.stringify(s('dashboard.inspector.waiting')),\n // /attach 표면: img src만 교체, #url-box textContent만 갱신 → url-box 이중 표시 방지(#458).\n dashboardSurface: false,\n };\n const sseScript = buildSseScript(sseStrings);\n return filled.replace('</body>', `${sseScript}\\n</body>`);\n}\n\nexport interface QrHttpServerOptions {\n /**\n * SSE 주기 갱신 간격 (ms). 기본값 90_000 (90초).\n *\n * SSE 구독자가 있는 동안 이 간격마다 `notifyStateChange()`와 동일한 push를 수행한다.\n * `getDashboardState()`가 호출 시점에 `at=` TOTP 코드를 재발급하므로, push 자체가\n * 열린 탭의 인스펙터 링크를 신선하게 유지한다. 90s 주기 < relay gate 허용창 ~3분\n * (±6 TOTP steps)이므로 탭이 열려 있는 한 링크가 항상 유효하다 (issue #509).\n *\n * 테스트에서 짧은 값(예: 50ms)을 주입해 검증한다. `undefined`이면 기본값 90_000.\n */\n sseRefreshIntervalMs?: number;\n /**\n * GET /inspector 라우트에서 클릭 시점 직접 인스펙터 URL을 조립하는 getter.\n *\n * getDashboardState().inspectorUrl(= /inspector 자기 자신)로 redirect하면 무한 루프가\n * 발생하므로, /inspector 라우트 내부는 이 getter로 직접 chii front_end URL을 조립한다.\n * 매 요청마다 호출되므로 TOTP를 요청 시점에 mint한다.\n *\n * - 미주입 → 기존 503 응답 유지.\n * - `ok: false, reason: 'relayDown'` → 502 (relay 미활성).\n * - `ok: false, reason: 'noTarget'` → 502 (relay up이지만 페이지 미attach).\n * - `ok: false, reason: 'totpUnavailable'` → 502 (TOTP secret 미설정, fail-closed).\n * - `ok: true` → 302 Location: url (Cache-Control: no-store).\n *\n * SECRET-HANDLING: ok:true 시 url 안에 relay host + TOTP at= 코드가 담긴다.\n * Location 헤더로 전달되는 건 의도된 transport. 로그/stdout 출력 금지.\n */\n getDirectInspectorUrl?: () =>\n | { ok: true; url: string }\n | { ok: false; reason: 'relayDown' | 'noTarget' | 'totpUnavailable' };\n}\n\n/**\n * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.\n * MCP debug server 생애주기에 묶어 사용 — `runDebugServer` shutdown 시 `close()`로 정리.\n *\n * @param getDashboardState - dashboard 상태를 반환하는 클로저. 주입 시 `GET /` dashboard와\n * `GET /events` SSE 스트림이 활성화된다. 미주입 시 두 라우트는 204/서비스 없음으로 응답.\n * @param options - 서버 옵션. `sseRefreshIntervalMs`로 idle 탭 TOTP 만료 방지 주기를 조정.\n * `getDirectInspectorUrl`로 /inspector 라우트에서 직접 조립 URL을 제공해 redirect 루프를 방지.\n */\nexport async function startQrHttpServer(\n getDashboardState?: () => DashboardState,\n options?: QrHttpServerOptions,\n): Promise<QrHttpServer> {\n const { default: QRCode } = await import('qrcode');\n\n /** SSE 활성 연결 목록 — `notifyStateChange()` 시 전체 push. */\n const sseClients: ServerResponse[] = [];\n\n /** SSE 연결 하나에 상태 이벤트를 flush한다. */\n function pushStateToClient(res: ServerResponse, state: DashboardState): void {\n const payload = JSON.stringify({\n tunnel: { up: state.tunnel.up, wssUrl: state.tunnel.wssUrl },\n pages: state.pages,\n // attachUrl은 캡슐 그대로 전달 — TOTP at= 코드 분리 없음 (의도된 설계).\n attachUrl: state.attachUrl,\n // inspectorUrl: relay + 페이지 attached 시 살아있는 인스펙터 URL (#503).\n // SECRET-HANDLING: URL(relay host + TOTP at=)은 SSE payload 전달이 의도된 transport.\n // 단 stdout/로그/에러에는 절대 출력하지 않는다.\n inspectorUrl: state.inspectorUrl ?? null,\n });\n // SSE frame: \"data: <json>\\n\\n\"\n res.write(`data: ${payload}\\n\\n`);\n }\n\n const server: Server = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const rawUrl = req.url ?? '/';\n const [path, query = ''] = rawUrl.split('?', 2) as [string, string | undefined];\n const params = new URLSearchParams(query ?? '');\n\n // per-request locale — ?lang= query param이 있으면 우선 적용, 없으면 Accept-Language header에서 결정.\n const langParam = params.get('lang');\n const locale =\n langParam === 'ko' || langParam === 'en'\n ? langParam\n : parseAcceptLanguage(req.headers['accept-language']);\n\n // ── GET / — dashboard 루트 ─────────────────────────────────────────────\n if (path === '/') {\n if (!getDashboardState) {\n res.writeHead(204, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end();\n return;\n }\n const state = getDashboardState();\n let qrDataUrl: string | null = null;\n if (state.attachUrl) {\n try {\n qrDataUrl = await QRCode.toDataURL(state.attachUrl, {\n type: 'image/png',\n errorCorrectionLevel: 'M',\n });\n } catch {\n // QR 생성 실패 시 null 유지 — dashboard는 텍스트 fallback 표시\n }\n }\n // devtoolsEntryUrl — getDirectInspectorUrl 주입 시만 /devtools/ 링크를 활성화.\n // 미주입이면 /devtools/ → 503이므로 dashboard에서 링크를 숨긴다.\n // /devtools/ 는 안정 경로 (relay host·TOTP 없음) — stdout/로그 출력 가능.\n const devtoolsEntryUrl: string | null = (() => {\n if (!options?.getDirectInspectorUrl) return null;\n const addr = server.address();\n if (!addr || typeof addr === 'string') return null;\n return `http://127.0.0.1:${addr.port}/devtools/`;\n })();\n const html = buildDashboardHtml(state, qrDataUrl, locale, path, params, devtoolsEntryUrl);\n res.writeHead(200, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n });\n res.end(html);\n return;\n }\n\n // ── GET /events — SSE 스트림 ──────────────────────────────────────────\n if (path === '/events') {\n if (!getDashboardState) {\n res.writeHead(204, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end();\n return;\n }\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n // 즉시 현재 상태를 한 번 push — 페이지 로드 시 최신 상태 보장.\n const initialState = getDashboardState();\n pushStateToClient(res, initialState);\n\n sseClients.push(res);\n\n // 연결 끊기면 목록에서 제거.\n req.once('close', () => {\n const idx = sseClients.indexOf(res);\n if (idx !== -1) sseClients.splice(idx, 1);\n });\n return;\n }\n\n if (path === '/attach') {\n const encodedU = params.get('u') ?? '';\n let attachUrl: string;\n try {\n attachUrl = decodeURIComponent(encodedU);\n } catch {\n res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('잘못된 u 파라미터입니다.');\n return;\n }\n\n // deploymentId 라벨 — attachUrl에서 _deploymentId 파라미터만 추출 (at= 노출 방지).\n let deploymentIdLabel = 'attach';\n try {\n const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);\n if (dpMatch?.[1]) {\n deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);\n }\n } catch {\n // best-effort\n }\n\n // 현재 세션 mode + pages 상태 — 카피 분기(#468), inspector 게이트(#544).\n // getDashboardState 미주입(legacy) 시 undefined → intoss family + 환경 라벨 없음 fallback.\n const currentState = getDashboardState?.();\n const mode = currentState?.mode;\n const pagesAttached =\n Array.isArray(currentState?.pages) && (currentState?.pages.length ?? 0) > 0;\n // inspectorStableUrl: /inspector 안정 URL (시크릿 없음) — getDirectInspectorUrl 주입 시만 활성.\n // 서버 주소는 listen 후에만 확정되므로 server.address()로 런타임에 읽는다.\n // (요청은 listen 완료 후 들어오므로 address()는 항상 non-null이다.)\n const inspectorStableUrlForAttach: string | null = (() => {\n if (!options?.getDirectInspectorUrl) return null;\n const addr = server.address();\n if (!addr || typeof addr === 'string') return null;\n return `http://127.0.0.1:${addr.port}/inspector`;\n })();\n\n // QR을 base64 data URL로 인라인 생성 — 외부 fetch 없이 self-contained HTML.\n QRCode.toDataURL(attachUrl, { type: 'image/png', errorCorrectionLevel: 'M' })\n .then((dataUrl: string) => {\n const safeLabel = escapeHtml(deploymentIdLabel);\n const safeAttachUrl = escapeHtml(attachUrl);\n const html = buildAttachHtml(\n dataUrl,\n safeLabel,\n safeAttachUrl,\n locale,\n path,\n params,\n mode,\n pagesAttached,\n inspectorStableUrlForAttach,\n );\n res.writeHead(200, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n });\n res.end(html);\n })\n .catch(() => {\n res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('QR 생성에 실패했습니다.');\n });\n return;\n }\n\n // ── GET /inspector — 안정 인스펙터 진입점 (issue #530) ───────────────────\n // ── GET /devtools/ — chii DevTools UI 진입로 (issue #248 옵션 A) ──────────\n //\n // 두 라우트는 동일한 핸들러를 공유한다:\n // - relay 연결 active + attached target 있음 → chii front_end/chii_app.html?ws=… 302.\n // - relay down 또는 target 없음 → 502 (사용자에게 원인 + 다음 단계 안내).\n // - getDirectInspectorUrl 미주입 (relay 연결 없는 server mode) → 503.\n //\n // /inspector: dashboard의 \"디버그 툴 열기\" 링크가 이 URL을 가리키며, qrServer.inspectorStableUrl로 노출된다.\n // /devtools/: `/ait debug` 문서·가이드에서 직접 참조 가능한 고정 경로 (issue #248).\n // 이 경로가 존재함으로써 사용자가 dashboard를 열지 않고도 직접 DevTools UI에 접근할 수 있다.\n //\n // getDashboardState().inspectorUrl(= /inspector 자기 자신)을 쓰면 무한 루프 → getDirectInspectorUrl로 분리.\n // SECRET-HANDLING: redirect Location(relay host + at=)은 HTTP 응답으로만 전달.\n // 로그에 Location 값 출력 금지.\n if (path === '/inspector' || path === '/devtools' || path === '/devtools/') {\n const getDirectInspectorUrl = options?.getDirectInspectorUrl;\n if (!getDirectInspectorUrl) {\n // /inspector: 기존 영문 메시지를 유지한다.\n // 이 경로는 공개 안정 경로(#530)이고 외부 스크립트가 메시지를 파싱할 수 있어\n // 계약을 깨지 않도록 원문 그대로 유지한다.\n // /devtools[/]: issue #248에서 새로 추가된 경로. 한국어 안내를 반환한다.\n const body =\n path === '/inspector'\n ? 'Inspector endpoint is not available in this server mode.'\n : 'relay 연결 세션에서만 DevTools UI를 열 수 있습니다.';\n res.writeHead(503, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end(body);\n return;\n }\n // 매 요청마다 getter 호출 — TOTP를 요청 시점에 mint.\n const result = getDirectInspectorUrl();\n const s = resolveLocaleStrings(locale);\n if (!result.ok) {\n const msgKey =\n result.reason === 'noTarget' ? 'inspector.error.noTarget' : 'inspector.error.relayDown';\n const msg = s(msgKey);\n const body =\n `<!DOCTYPE html><html lang=\"${locale}\"><head>` +\n `<meta charset=\"utf-8\"><title>Inspector</title></head><body>` +\n `<p>${escapeHtml(msg)}</p>` +\n `<p style=\"font-size:0.9em;color:#666\">` +\n (locale === 'ko'\n ? '(<a href=\"/\">대시보드로 돌아가기</a>)'\n : '(<a href=\"/\">Back to dashboard</a>)') +\n `</p></body></html>`;\n res.writeHead(502, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n });\n res.end(body);\n return;\n }\n // ok: true — 302 redirect. Location에 relay host + TOTP at= 포함.\n // SECRET-HANDLING: Location 값은 HTTP 응답으로만 — 로그/stdout 출력 금지.\n res.writeHead(302, {\n Location: result.url,\n 'Cache-Control': 'no-store',\n });\n res.end();\n return;\n }\n\n if (path === '/qr.png') {\n const encodedU = params.get('u') ?? '';\n let attachUrl: string;\n try {\n attachUrl = decodeURIComponent(encodedU);\n } catch {\n res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('잘못된 u 파라미터입니다.');\n return;\n }\n\n QRCode.toBuffer(attachUrl, { type: 'png', errorCorrectionLevel: 'M' })\n .then((buf: Buffer) => {\n res.writeHead(200, {\n 'Content-Type': 'image/png',\n 'Cache-Control': 'no-store',\n 'Content-Length': String(buf.length),\n });\n res.end(buf);\n })\n .catch(() => {\n res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('QR PNG 생성에 실패했습니다.');\n });\n return;\n }\n\n res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('Not Found');\n });\n\n const listenPort = Number(process.env.AIT_DEBUG_HTTP_PORT ?? 0);\n\n await new Promise<void>((resolve, reject) => {\n server.listen(listenPort, '127.0.0.1', () => resolve());\n server.once('error', reject);\n });\n\n const address = server.address();\n if (!address || typeof address === 'string') {\n throw new Error('qr-http-server: server.address()가 예상하지 못한 형태입니다.');\n }\n const port = address.port;\n\n /** idle 탭 TOTP 만료 방지용 주기 SSE 갱신 interval. */\n function notifyStateChangeInternal(): void {\n if (!getDashboardState) return;\n const state = getDashboardState();\n for (const client of sseClients) {\n try {\n pushStateToClient(client, state);\n } catch {\n // 연결이 이미 끊어진 경우 — 무시 (close 핸들러가 목록에서 제거함).\n }\n }\n }\n\n // 주기 SSE 갱신 — getDashboardState() 호출 시점에 TOTP at=가 재발급되므로\n // push 자체가 열린 탭의 인스펙터 링크를 신선하게 유지한다 (issue #509).\n // .unref()로 프로세스 종료를 막지 않는다.\n const refreshIntervalMs = options?.sseRefreshIntervalMs ?? 90_000;\n const refreshHandle = setInterval(() => {\n if (sseClients.length > 0 && getDashboardState) {\n notifyStateChangeInternal();\n }\n }, refreshIntervalMs).unref();\n\n return {\n port,\n buildAttachPageUrl(_attachUrl: string): string {\n // 사용자 대면 URL을 루트 `/`로 수렴 (#595).\n // 같은 데몬이 attachUrl을 이미 server-state(getDashboardState)로 보유하므로\n // `/attach?u=<encoded>` 쿼리는 redundant하다.\n // SECRET-HANDLING: 브라우저에 열리는 URL에서 tunnel host·relay wss·TOTP at= 제거.\n // /attach?u= 라우트 자체는 back-compat으로 유지(기존 인쇄된 링크 보호).\n return `http://127.0.0.1:${port}/`;\n },\n // 안정 인스펙터 진입점 URL (issue #530) — 클릭 시 302 redirect (TOTP 클릭 시점 mint).\n // URL 자체에 시크릿 없음 → 대시보드/stdout/로그 어디든 출력 가능.\n get inspectorStableUrl(): string {\n return `http://127.0.0.1:${port}/inspector`;\n },\n notifyStateChange(): void {\n notifyStateChangeInternal();\n },\n close(): Promise<void> {\n clearInterval(refreshHandle);\n return new Promise((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n },\n };\n}\n","/**\n * Single debug session lock for the `devtools-mcp` debug server.\n *\n * At most one debug server process should run on a given machine at a time —\n * multiple concurrent instances create duplicate cloudflared tunnels, waste\n * resources, and confuse the user about which wssUrl to use.\n *\n * ## Lock file\n *\n * Location: `~/.ait-devtools/server.lock`\n *\n * Schema (JSON):\n * ```json\n * { \"pid\": 12345, \"wssUrl\": \"wss://xxx.trycloudflare.com\", \"startedAt\": \"2026-01-01T00:00:00.000Z\" }\n * ```\n *\n * ## Behaviour\n *\n * - **Acquire**: write PID + wssUrl + startedAt. Returns a `release()` handle.\n * - **Stale lock recovery**: if the stored PID is no longer alive\n * (`process.kill(pid, 0)` throws ESRCH), the lock is silently replaced.\n * - **Live conflict (option B)**: if the stored PID is alive, `acquireLock`\n * throws `ServerLockConflictError` with the existing PID and wssUrl so the\n * caller can surface a clear message to the agent.\n * - **Release**: remove the lock file. Called on graceful shutdown (SIGINT /\n * SIGTERM / SIGHUP). SIGKILL survivors leave a stale file — the next startup\n * recovers it automatically via the alive check.\n *\n * ## wssUrl update\n *\n * The lock is written before cloudflared starts, so `wssUrl` begins as `null`\n * and is updated in place once the tunnel URL is known via `updateWssUrl`.\n *\n * Node-only.\n */\n\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { isPidAlive as _isPidAlive } from '../shared/parent-watcher.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface LockData {\n pid: number;\n /** `null` until the cloudflared tunnel URL is assigned. */\n wssUrl: string | null;\n startedAt: string;\n /**\n * PID of the cloudflared child process. Written once the tunnel is up via\n * `LockHandle.updateTunnelChildPid`. Absent in lock files written by older\n * versions — those fall back to PID-only stale detection.\n *\n * FIX 3 (issue #571): `acquireLock` treats a live holder whose tunnel child\n * is known-dead as a stale lock and reclaims it.\n */\n tunnelChildPid?: number | null;\n}\n\nexport interface LockHandle {\n /** Updates the wssUrl field in the lock file once the tunnel URL is known. */\n updateWssUrl(wssUrl: string): void;\n /**\n * Updates the cloudflared child PID in the lock file once the tunnel is up.\n *\n * FIX 3 (issue #571): a second `acquireLock` caller will see this PID and\n * can detect that the holder's tunnel child is dead even though the Node\n * process itself is still alive, allowing lock reclamation.\n */\n updateTunnelChildPid(pid: number): void;\n /** Removes the lock file. Idempotent — safe to call multiple times. */\n release(): void;\n}\n\n/** Thrown when a live server process already holds the lock. */\nexport class ServerLockConflictError extends Error {\n /** PID of the existing server process. */\n readonly existingPid: number;\n /** wssUrl from the existing lock — may be `null` if the tunnel is still starting. */\n readonly existingWssUrl: string | null;\n /** ISO timestamp from the existing lock — when that session started. */\n readonly existingStartedAt: string;\n\n constructor(existingPid: number, existingWssUrl: string | null, existingStartedAt: string) {\n const urlNote =\n existingWssUrl != null\n ? ` relay URL: ${existingWssUrl}\\n`\n : ' relay URL: (tunnel still starting — retry in a moment)\\n';\n\n super(\n `A debug server is already running (PID ${existingPid}).\\n` +\n urlNote +\n 'Stop the existing session before starting a new one.\\n' +\n 'If it is already stopped but this error persists, remove the lock file:\\n' +\n ` rm \"${lockFilePath()}\"`,\n );\n this.name = 'ServerLockConflictError';\n this.existingPid = existingPid;\n this.existingWssUrl = existingWssUrl;\n this.existingStartedAt = existingStartedAt;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Paths\n// ---------------------------------------------------------------------------\n\n/** Returns `~/.ait-devtools/server.lock` (or `AIT_DEVTOOLS_LOCK_DIR` override for tests). */\nexport function lockFilePath(): string {\n const dir = process.env.AIT_DEVTOOLS_LOCK_DIR ?? join(homedir(), '.ait-devtools');\n return join(dir, 'server.lock');\n}\n\nfunction ensureLockDir(lockPath: string): void {\n const dir = join(lockPath, '..');\n mkdirSync(dir, { recursive: true });\n}\n\n// ---------------------------------------------------------------------------\n// PID alive check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when the given PID refers to a running process.\n *\n * Re-exported from `../shared/parent-watcher` so external callers that\n * import from `./server-lock` keep working without an import-path change.\n */\nexport const isPidAlive: (pid: number) => boolean = _isPidAlive;\n\n// ---------------------------------------------------------------------------\n// Read / write helpers\n// ---------------------------------------------------------------------------\n\nfunction readLock(lockPath: string): LockData | null {\n if (!existsSync(lockPath)) return null;\n try {\n const raw = readFileSync(lockPath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'pid' in parsed &&\n typeof (parsed as Record<string, unknown>).pid === 'number' &&\n 'startedAt' in parsed &&\n typeof (parsed as Record<string, unknown>).startedAt === 'string'\n ) {\n const p = parsed as Record<string, unknown>;\n // FIX 3: read optional tunnelChildPid — absent in lock files from older\n // versions; those fall back to PID-only stale detection.\n const tunnelChildPid = typeof p.tunnelChildPid === 'number' ? p.tunnelChildPid : null;\n return {\n pid: p.pid as number,\n wssUrl: typeof p.wssUrl === 'string' ? p.wssUrl : null,\n startedAt: p.startedAt as string,\n tunnelChildPid,\n };\n }\n // Unrecognised schema — treat as stale.\n return null;\n } catch {\n // Corrupt / unreadable — treat as stale.\n return null;\n }\n}\n\nfunction writeLock(lockPath: string, data: LockData): void {\n ensureLockDir(lockPath);\n writeFileSync(lockPath, JSON.stringify(data, null, 2), { encoding: 'utf8' });\n}\n\nfunction removeLock(lockPath: string): void {\n try {\n rmSync(lockPath);\n } catch {\n // Already removed — fine.\n }\n}\n\n// ---------------------------------------------------------------------------\n// Force-takeover helper\n// ---------------------------------------------------------------------------\n\n/**\n * Sends SIGTERM to `pid` and waits up to `graceMs` (default 2 000 ms) for it\n * to exit; then falls back to SIGKILL. Synchronous — uses a busy-wait loop so\n * it is usable in the top-level startup path without async plumbing.\n *\n * Ignores errors from `process.kill` so that a race where the target exits\n * between the alive check and the kill call does not crash the caller.\n */\nfunction killAndWait(pid: number, graceMs = 2_000): void {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Already gone — nothing to do.\n return;\n }\n\n const deadline = Date.now() + graceMs;\n // Poll every 100 ms until the process is gone or the grace period expires.\n while (isPidAlive(pid) && Date.now() < deadline) {\n // Busy-wait: this is a very short window (≤2 s) at startup.\n const end = Date.now() + 100;\n while (Date.now() < end) {\n // spin\n }\n }\n\n if (isPidAlive(pid)) {\n try {\n process.kill(pid, 'SIGKILL');\n } catch {\n // Already gone.\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Reads the current lock file without acquiring it. Returns the parsed\n * `LockData` when the file exists and is valid, otherwise `null`. Used by\n * `get_debug_status` to surface the `serverLockHolder` field without\n * interfering with the running lock owner.\n */\nexport function readServerLock(): LockData | null {\n return readLock(lockFilePath());\n}\n\n/** Options for `acquireLock`. */\nexport interface AcquireLockOptions {\n /**\n * When `true`, terminates the process holding the existing lock (SIGTERM →\n * wait up to 2 s → SIGKILL) and takes over the lock.\n *\n * Corresponds to the `--force` / `--takeover` CLI flag.\n */\n force?: boolean;\n}\n\n/**\n * Attempts to acquire the server lock.\n *\n * - If no lock exists (or the lock is stale): writes a new lock and returns a\n * `LockHandle` with `updateWssUrl` + `release`.\n * - If a live process holds the lock and `force` is `false` (default): writes\n * a clear recovery message to stderr and throws `ServerLockConflictError`.\n * - If a live process holds the lock and `force` is `true`: sends SIGTERM to\n * that process (waiting up to 2 s then SIGKILL) and takes over the lock.\n *\n * The initial `wssUrl` in the lock file is `null` — call\n * `handle.updateWssUrl(url)` once the cloudflared tunnel is ready.\n */\nexport function acquireLock(options: AcquireLockOptions = {}): LockHandle {\n const { force = false } = options;\n const lockPath = lockFilePath();\n const existing = readLock(lockPath);\n\n if (existing !== null) {\n if (isPidAlive(existing.pid)) {\n // FIX 3 (issue #571): even if the Node process is alive, check whether\n // its cloudflared child has died. A zombie daemon whose tunnel is dead\n // is effectively stale — reclaim the lock without waiting for the user\n // to manually kill the process.\n const tunnelChildPid = existing.tunnelChildPid;\n const tunnelChildDead = typeof tunnelChildPid === 'number' && !isPidAlive(tunnelChildPid);\n\n if (tunnelChildDead) {\n process.stderr.write(\n `[ait-debug] stale lock: holder PID=${existing.pid} alive but tunnel child PID=${tunnelChildPid} is dead — reclaiming lock.\\n`,\n );\n // Fall through to write a fresh lock.\n } else if (force) {\n // Force takeover: SIGTERM → 2 s grace → SIGKILL.\n process.stderr.write(\n `[ait-debug] --force: terminating existing session PID=${existing.pid} …\\n`,\n );\n killAndWait(existing.pid);\n process.stderr.write(`[ait-debug] --force: PID=${existing.pid} stopped, taking over.\\n`);\n } else {\n // Emit a user-actionable message before throwing so the MCP host can\n // surface it — the thrown message is included in the \"process exited\"\n // log, but the stderr line is more prominent and machine-parseable.\n const urlPart =\n existing.wssUrl != null ? `wssUrl=${existing.wssUrl}` : 'wssUrl=(tunnel starting)';\n process.stderr.write(\n `[ait-debug] 기존 debug-mode 세션이 이미 실행 중 — PID=${existing.pid}, started ${existing.startedAt}, ${urlPart}\\n` +\n `[ait-debug] 회복: \\`kill ${existing.pid}\\` 또는 \\`npx @ait-co/devtools devtools-mcp --force\\`\\n`,\n );\n throw new ServerLockConflictError(existing.pid, existing.wssUrl, existing.startedAt);\n }\n } else {\n // Stale lock — previous process died without cleanup.\n process.stderr.write(\n `[ait-debug] stale lock from PID ${existing.pid} recovered — starting fresh.\\n`,\n );\n }\n }\n\n const data: LockData = {\n pid: process.pid,\n wssUrl: null,\n startedAt: new Date().toISOString(),\n };\n writeLock(lockPath, data);\n\n let released = false;\n\n return {\n updateWssUrl(wssUrl: string): void {\n if (released) return;\n data.wssUrl = wssUrl;\n writeLock(lockPath, data);\n },\n updateTunnelChildPid(pid: number): void {\n if (released) return;\n data.tunnelChildPid = pid;\n writeLock(lockPath, data);\n },\n release(): void {\n if (released) return;\n released = true;\n removeLock(lockPath);\n },\n };\n}\n","/**\n * call_sdk 인자 시그니처 레지스트리\n *\n * 잘 알려진 SDK 메서드의 인자 schema를 수동으로 등록한다.\n * 목적: 잘못된 인자가 native bridge에 도달하기 전에 MCP 레이어에서 reject하여\n * 토스 앱 crash(Swift/Kotlin 측에서 `.type` 등을 undefined로 읽는 경우)를 예방.\n *\n * 등록되지 않은 메서드는 passthrough — 알 수 없는 메서드에 대해 stderr 경고 1회.\n *\n * 시그니처 출처:\n * - `src/__typecheck.ts` — Original SDK 타입 호환성 검증\n * - `src/mock/navigation/index.ts` — mock 구현의 함수 시그니처\n * - `src/mock/device/` — device mock 시그니처\n *\n * 새 메서드 추가 방법:\n * 1. `src/__typecheck.ts` 또는 mock 구현에서 시그니처 확인\n * 2. 아래 SIGNATURES 배열에 `SdkSignature` 항목 추가\n * 3. `src/__tests__/call-sdk-validation.test.ts`에 ok + bad 케이스 추가\n */\n\n/** 단일 메서드에 대한 인자 검증 결과 */\nexport type ValidationResult = { ok: true } | { ok: false; expected: string; received: string };\n\n/** 등록된 SDK 메서드 시그니처 */\nexport interface SdkSignature {\n /** SDK 메서드 이름 (예: \"setDeviceOrientation\") */\n name: string;\n /**\n * 인자 배열을 검증하는 함수.\n * `args[0]` 등 필요한 인자를 `unknown` 타입으로 받아 type guard로 검증.\n */\n validateArgs(args: unknown[]): ValidationResult;\n /**\n * 에러 메시지에 포함할 올바른 호출 예시.\n * 예: `call_sdk('setDeviceOrientation', [{ type: 'landscape' }])`\n */\n example: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/* 헬퍼 — 공통 type guard */\n/* -------------------------------------------------------------------------- */\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction describeArgs(args: unknown[]): string {\n try {\n return JSON.stringify(args);\n } catch {\n return String(args);\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* 시그니처 레지스트리 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * 등록된 메서드 목록.\n *\n * 시그니처 출처 확인:\n * - 함수가 인자를 받지 않으면 args[0] 없음 → `args.length === 0`을 체크하지 않고\n * 그냥 통과시킨다(args 무시하는 stub가 많아서 noArgs 체크가 noise).\n * - 실 SDK 시그니처는 `src/__typecheck.ts`의 `Assert<Mock, Original>` 줄로 보장.\n */\nconst SIGNATURES: SdkSignature[] = [\n // --- setDeviceOrientation ---\n // 실 시그니처: setDeviceOrientation(options: { type: 'portrait' | 'landscape' }): Promise<void>\n // 출처: src/mock/navigation/index.ts:40 / src/__typecheck.ts:55\n {\n name: 'setDeviceOrientation',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg)) {\n return {\n ok: false,\n expected: \"{ type: 'portrait' | 'landscape' }\",\n received: describeArgs(args),\n };\n }\n const type = arg.type;\n if (type !== 'portrait' && type !== 'landscape') {\n return {\n ok: false,\n expected: \"{ type: 'portrait' | 'landscape' }\",\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setDeviceOrientation', [{ type: 'landscape' }])\",\n },\n\n // --- setIosSwipeGestureEnabled ---\n // 실 시그니처: setIosSwipeGestureEnabled(options: { isEnabled: boolean }): Promise<void>\n // 출처: src/mock/navigation/index.ts:32 / src/__typecheck.ts:51\n {\n name: 'setIosSwipeGestureEnabled',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.isEnabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ isEnabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setIosSwipeGestureEnabled', [{ isEnabled: false }])\",\n },\n\n // --- setSecureScreen ---\n // 실 시그니처: setSecureScreen(options: { enabled: boolean }): Promise<{ enabled: boolean }>\n // 출처: src/mock/navigation/index.ts:66 / src/__typecheck.ts:46\n {\n name: 'setSecureScreen',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.enabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ enabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setSecureScreen', [{ enabled: true }])\",\n },\n\n // --- setScreenAwakeMode ---\n // 실 시그니처: setScreenAwakeMode(options: { enabled: boolean }): Promise<{ enabled: boolean }>\n // 출처: src/mock/navigation/index.ts:57 / src/__typecheck.ts:47\n {\n name: 'setScreenAwakeMode',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.enabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ enabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setScreenAwakeMode', [{ enabled: true }])\",\n },\n\n // --- getOperationalEnvironment ---\n // 실 시그니처: getOperationalEnvironment(): 'toss' | 'sandbox'\n // 인자 없음 — args는 무시 (SDK 자체가 인자를 무시함)\n // 출처: src/mock/navigation/index.ts:88 / src/__typecheck.ts:62\n {\n name: 'getOperationalEnvironment',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getOperationalEnvironment', [])\",\n },\n\n // --- getPlatformOS ---\n // 실 시그니처: getPlatformOS(): 'ios' | 'android'\n // 출처: src/mock/navigation/index.ts:84 / src/__typecheck.ts:61\n {\n name: 'getPlatformOS',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getPlatformOS', [])\",\n },\n\n // --- getDeviceId ---\n // 실 시그니처: getDeviceId(): string\n // 출처: src/mock/navigation/index.ts:119 / src/__typecheck.ts:74\n {\n name: 'getDeviceId',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getDeviceId', [])\",\n },\n\n // --- getLocale ---\n // 실 시그니처: getLocale(): string\n // 출처: src/mock/navigation/index.ts:115 / src/__typecheck.ts:72\n {\n name: 'getLocale',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getLocale', [])\",\n },\n\n // --- getNetworkStatus ---\n // 실 시그니처: getNetworkStatus(): Promise<NetworkStatus>\n // 출처: src/mock/navigation/index.ts:127 / src/__typecheck.ts:73\n {\n name: 'getNetworkStatus',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getNetworkStatus', [])\",\n },\n\n // --- getSchemeUri ---\n // 실 시그니처: getSchemeUri(): string\n // 출처: src/mock/navigation/index.ts:111 / src/__typecheck.ts:71\n {\n name: 'getSchemeUri',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getSchemeUri', [])\",\n },\n\n // --- requestReview ---\n // 실 시그니처: requestReview(): Promise<void>\n // 출처: src/mock/navigation/index.ts:75 / src/__typecheck.ts:76\n {\n name: 'requestReview',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('requestReview', [])\",\n },\n\n // --- closeView ---\n // 실 시그니처: closeView(): Promise<void>\n // 출처: src/mock/navigation/index.ts:10 / src/__typecheck.ts:42\n {\n name: 'closeView',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('closeView', [])\",\n },\n];\n\n/* -------------------------------------------------------------------------- */\n/* 레지스트리 공개 API */\n/* -------------------------------------------------------------------------- */\n\nconst SIGNATURE_MAP = new Map<string, SdkSignature>(SIGNATURES.map((s) => [s.name, s]));\n\n/** 세션 내 passthrough 경고를 한 번만 emit하기 위한 Set */\nconst _warnedPassthrough = new Set<string>();\n\n/**\n * 메서드 이름으로 시그니처를 조회한다.\n * 등록된 메서드이면 `SdkSignature`를 반환하고, 미등록이면 `undefined`.\n */\nexport function lookupSignature(name: string): SdkSignature | undefined {\n return SIGNATURE_MAP.get(name);\n}\n\n/**\n * 미등록 메서드에 대해 stderr에 passthrough 경고를 1회 출력한다.\n * 세션 내 동일 메서드 이름은 최초 1회만 출력.\n */\nexport function warnPassthrough(name: string): void {\n if (_warnedPassthrough.has(name)) return;\n _warnedPassthrough.add(name);\n process.stderr.write(`[ait-debug] call_sdk: \"${name}\" 시그니처가 등록되지 않음 — passthrough\\n`);\n}\n\n/**\n * 테스트에서 passthrough 경고 Set을 초기화하기 위한 헬퍼.\n * 프로덕션 코드에서는 호출하지 않는다.\n */\nexport function _resetWarnedPassthroughForTest(): void {\n _warnedPassthrough.clear();\n}\n\n/**\n * 등록된 메서드 이름 목록 — tool description 생성 등에서 사용.\n */\nexport const REGISTERED_METHOD_NAMES: ReadonlyArray<string> = SIGNATURES.map((s) => s.name);\n","/**\n * Debug-mode MCP tools (Phase 1–3 + safe-area probe).\n *\n * Read-only tools that normalize CDP / AIT data into `chrome-devtools-mcp`-\n * compatible shapes. The tools never touch a websocket or HTTP endpoint\n * directly — they read from an injected `CdpConnection` (CDP events/commands)\n * or `AitSource` (AIT.* domain), which is what makes them unit-testable with a\n * fake. No phone and no running dev server are needed in tests.\n *\n * Phase 1 (CDP events):\n * - `list_console_messages` ← Runtime.consoleAPICalled\n * - `list_network_requests` ← Network.requestWillBeSent + responseReceived\n * - `list_pages` ← Chii relay target list + tunnel status\n * Phase 2 (CDP commands):\n * - `get_dom_document` ← DOM.getDocument\n * - `take_snapshot` ← DOMSnapshot.captureSnapshot\n * - `take_screenshot` ← Page.captureScreenshot\n * - `measure_safe_area` ← Runtime.evaluate (safe-area probe)\n * Phase 3 (AIT.* domain — CDP can't cover these):\n * - `AIT.getSdkCallHistory`\n * - `AIT.getMockState`\n * - `AIT.getOperationalEnvironment`\n */\n\nimport type {\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\nimport type {\n CdpCallFrame,\n CdpConnection,\n CdpRemoteObject,\n ConsoleApiCalledEvent,\n DomGetDocumentResult,\n DomSnapshotResult,\n NetworkRequestWillBeSentEvent,\n NetworkResponseReceivedEvent,\n RuntimeExceptionThrownEvent,\n} from './cdp-connection.js';\nimport { buildDeepLinkAttachUrl, validateSchemeAuthority } from './deeplink.js';\nimport type { McpEnvironment } from './environment.js';\nimport { isLiveRelayEnv, isRelayEnv, toLegacyEnv } from './environment.js';\nimport { lookupSignature, warnPassthrough } from './sdk-signatures.js';\nimport { isPidAlive } from './server-lock.js';\nimport { generateTotp, RELAY_VERIFY_SKEW_STEPS } from './totp.js';\n\n/** Tunnel state surfaced by `list_pages`. */\nexport interface TunnelStatus {\n /** Whether the cloudflared quick tunnel is up. */\n up: boolean;\n /** Public `wss://*.trycloudflare.com` relay URL the phone attaches to. */\n wssUrl: string | null;\n /**\n * ISO timestamp when a tunnel drop was first detected by the health probe.\n * `null` means the tunnel has not dropped (or has recovered since the last\n * drop). When non-null and `up` is false, the tunnel is down and the probe\n * has exhausted all reissue attempts — the server must be restarted.\n */\n droppedAt?: string | null;\n /**\n * Number of automatic reissue attempts made after a drop was detected.\n * Resets to 0 after a successful reissue. Reaches `MAX_REISSUE_ATTEMPTS`\n * (3) before the probe gives up and enters the permanent-error state.\n */\n reissueAttempts?: number;\n}\n\n/**\n * Tier classification per RFC #277 (\"MCP tool surface fidelity\"):\n *\n * - **Tier A** (`mock` only) — mock-internal state dials with no real-device\n * equivalent. Hidden when env is `relay`.\n * - **Tier B** (`relay` only) — relay infrastructure tools that have no mock\n * equivalent (e.g. `build_attach_url` needs a cloudflared tunnel URL). Hidden\n * when env is `mock`.\n * - **Tier C** (`both`) — fidelity-parallel tools that produce semantically\n * equivalent results across mock and relay. The agent sees the same tool with\n * the same shape; only the `source` provenance field (where applicable)\n * differs.\n */\nexport type ToolAvailability = 'mock' | 'relay' | 'both';\n\n/** Static MCP tool descriptors (name + JSONSchema) for the full debug tool surface. */\nexport const DEBUG_TOOL_DEFINITIONS = [\n {\n name: 'list_console_messages',\n description:\n 'Lists recent console messages (console.log/warn/error/info) captured from the attached ' +\n 'mini-app page over CDP (Runtime.consoleAPICalled). Read-only. Returns level, text, ' +\n 'timestamp, and stringified args, oldest-first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_network_requests',\n description:\n 'Lists recent network requests (XHR/fetch) captured from the attached mini-app page over ' +\n 'CDP (Network.requestWillBeSent + Network.responseReceived). Read-only. Returns url, ' +\n 'method, status, and timing, oldest-first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_pages',\n description:\n 'Returns the single active page (at most one) the relay sees attached. ' +\n 'When a second page attaches, the previous one is evicted (last-attach wins — ' +\n 'single-attach model). The result includes `singleAttachModel: true` so the agent ' +\n 'knows the array is always 0 or 1 entries. ' +\n 'Also returns whether the cloudflared tunnel is up and the public wss relay URL. ' +\n 'The `tunnel` field includes `droppedAt` (ISO timestamp or null/undefined): when non-null ' +\n 'the tunnel has permanently dropped after 3 failed reissue attempts — restart the debug ' +\n 'server with `npx @ait-co/devtools devtools-mcp`. ' +\n 'Each page entry includes a `lastSeenAt` ISO timestamp (last inbound CDP message from ' +\n 'that target — useful to detect stale entries when the phone app backgrounded). ' +\n 'The result also includes `crashDetectedAt` (ISO timestamp or null): when non-null, ' +\n 'a page crash was detected via Inspector.targetCrashed / Target.targetDestroyed since ' +\n 'the last attach, the pages list will be empty, and `crashWarning` shows a Korean hint ' +\n 'to re-attach. ' +\n 'Call this first to confirm a page is attached before reading console/network. ' +\n 'When a page attaches or detaches the server emits notifications/tools/list_changed — ' +\n 'call tools/list again to get the full updated tool surface.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'build_attach_url',\n description:\n \"The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. \" +\n 'Builds a self-attaching deep-link for the active relay environment and returns a QR code. ' +\n 'Scan the QR with the phone camera to open the mini-app and attach it to this debug session ' +\n '(QR is the single entry path — no USB cable or platform CLI needed). ' +\n 'Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: ' +\n '`npx @ait-co/devtools devtools-mcp`.\\n\\n' +\n 'Environment-specific behaviour:\\n' +\n ' • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the ' +\n 'intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices ' +\n 'debug=1 + relay URL into the scheme URL to produce a self-attach deep-link.\\n' +\n ' • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads ' +\n 'AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) ' +\n 'and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). ' +\n 'When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. ' +\n 'Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\\n\\n' +\n 'Set wait_for_attach=true to block until a page attaches (default 60 s, adjustable via wait_timeout_seconds). ' +\n 'On timeout, call build_attach_url again to resume polling. ' +\n 'The server automatically opens the QR dashboard in the OS default browser when running on a ' +\n 'local GUI machine — headless/remote environments fall back to the text QR in the tool output.' +\n '\\n\\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl ' +\n 'automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes ' +\n '(the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). ' +\n 'The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). ' +\n 'If the phone scan happens after expiresAt, the relay will reject the code — just call ' +\n 'build_attach_url again to get a fresh URL. ' +\n 'Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.\\n\\n' +\n 'selfdebug (env 2 / relay-sandbox only): pass selfdebug=true to add &selfdebug=1 to the ' +\n 'launcher deep-link. The launcher PWA then registers its own document as the CDP target ' +\n 'instead of the framed mini-app. SINGLE-ATTACH MODEL: attaching the launcher self-target ' +\n 'evicts any currently-attached mini-app target — use this mode exclusively for diagnosing ' +\n 'the launcher document itself (DOM, safe-area, console). Not applicable in env 3/4 ' +\n '(relay-staging/relay-live) — passing selfdebug=true there returns an error.',\n inputSchema: {\n type: 'object',\n properties: {\n scheme_url: {\n type: 'string',\n description:\n 'The intoss-private:// scheme URL from `ait deploy --scheme-only` (must carry _deploymentId). ' +\n 'Required for env 3/relay-staging mode. Not used in env 2/relay-sandbox mode (use AIT_TUNNEL_BASE_URL instead). ' +\n 'The authority (host) must be the app name (e.g. intoss-private://aitc-sdk-example?_deploymentId=…). ' +\n 'Generic values like \"web\" or an empty host indicate a malformed URL.',\n },\n wait_for_attach: {\n type: 'boolean',\n description:\n 'If true, block after returning the QR until a page attaches to the relay (polls ' +\n 'listTargets ~1 s interval, default 60 s). On attach, the response includes the ' +\n 'attached page list. On timeout, call build_attach_url again to resume polling.',\n },\n wait_timeout_seconds: {\n type: 'number',\n description:\n 'Maximum seconds to wait when wait_for_attach=true (default 60, range 1–600). ' +\n 'Values outside the range or invalid inputs (0, negative, NaN) fall back to the default silently. ' +\n 'Only meaningful when wait_for_attach=true.',\n },\n projectRoot: {\n type: 'string',\n description:\n 'Absolute path to the mini-app project root (the directory containing its package.json and .ait_urls). ' +\n 'When AIT_TUNNEL_BASE_URL is unset (env 2 / relay-mobile only), the daemon reads the app tunnel URL ' +\n 'from <projectRoot>/.ait_urls written by the dev server (tunnel:{cdp:true}). ' +\n \"Pass this because the daemon's own cwd is fixed at launch. Omit when AIT_TUNNEL_BASE_URL is set explicitly.\",\n },\n selfdebug: {\n type: 'boolean',\n description:\n 'Env 2 / relay-sandbox only. When true, adds &selfdebug=1 to the launcher deep-link ' +\n 'so the launcher PWA registers its own document as the CDP target (launcher diagnostics mode). ' +\n 'SINGLE-ATTACH MODEL: self-target attach evicts any currently-attached mini-app target. ' +\n 'Use only when you need to inspect the launcher itself (DOM, safe-area, console). ' +\n 'Passing selfdebug=true in env 3/4 (relay-staging/relay-live) returns an error. ' +\n 'Default: false (omitted — output is byte-identical to previous behaviour).',\n },\n },\n // scheme_url is required only for env 3/relay-staging; env 2/relay-sandbox uses AIT_TUNNEL_BASE_URL.\n // The handler enforces the requirement at runtime based on the active environment.\n required: [],\n },\n // Tier B per RFC #277 — the URL synthesis requires a live cloudflared\n // tunnel + relay, which only exists in the `relay` environment.\n availableIn: 'relay' as ToolAvailability,\n },\n {\n name: 'get_dom_document',\n description:\n 'Returns the DOM tree of the attached mini-app page over CDP (DOM.getDocument). Read-only. ' +\n 'Use for structural/layout regression diagnosis (e.g. confirming an element exists, ' +\n 'inspecting attributes). Returns the document root node with children.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_snapshot',\n description:\n 'Captures a serialized snapshot of the attached page over CDP (DOMSnapshot.captureSnapshot). ' +\n 'Read-only. Returns the documents + interned strings table for visual-regression diagnosis ' +\n '(e.g. checking computed CSS custom properties like --sat against the live layout).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_screenshot',\n description:\n 'Captures a PNG screenshot of the attached mini-app page over CDP (Page.captureScreenshot) ' +\n 'so the agent can see the phone screen directly. Read-only. ' +\n 'Returns an image content block — this is the only debug tool that returns an image; ' +\n 'all other debug tools return text (JSON).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'measure_safe_area',\n description:\n 'Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns ' +\n 'normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. ' +\n 'Read-only — does not modify page state. ' +\n 'Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel ' +\n 'page with window.__ait state) and `relay` (real-device WebView with window.__sdk). ' +\n 'The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-live\" | \"relay-mobile\"` field so consumers can identify ' +\n 'provenance without inspecting payload values. ' +\n '(`relay-mobile` = env 2 real-device PWA over an external relay; ' +\n '`relay-dev` = env 3 dog-food WebView; `relay-live` = env 4 production WebView.) ' +\n 'Use in a relay session (phone attached) to get ground-truth values for upgrading a ' +\n 'viewport preset from extrapolated/placeholder to measured. ' +\n 'Requires a page to be attached — call list_pages first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'evaluate',\n description:\n 'Evaluates an arbitrary JavaScript expression on the attached mini-app page via ' +\n 'CDP Runtime.evaluate (returnByValue: true) and returns the result. ' +\n 'NOT read-only — the expression can have side effects (DOM mutations, SDK calls, ' +\n 'state changes). Requires the relay to be attached — call list_pages first. ' +\n 'Throws if the evaluation throws an exception on the page.\\n\\n' +\n 'SECURITY: expression and result are not redacted — never include secrets or auth ' +\n 'tokens in the expression.\\n\\n' +\n 'LIVE guard: when running against a live/production relay (relay-live env, ' +\n 'MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that ' +\n 'the expression may affect real users. Without it the call is rejected with a ' +\n 'structured error. mock and relay-dev sessions are unaffected.',\n inputSchema: {\n type: 'object',\n properties: {\n expression: {\n type: 'string',\n description: 'JavaScript expression to evaluate in the page context.',\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge ' +\n 'that this expression may have side effects on real/live users. ' +\n 'Omitting this in a relay-live session results in a structured rejection error. ' +\n 'Has no effect in mock or relay-dev sessions.',\n },\n },\n required: ['expression'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_exceptions',\n description:\n 'Lists JS-level exceptions captured via `Runtime.exceptionThrown` from the relay attached ' +\n 'page. Includes timestamp, exception text, source URL/line, and stack trace. ' +\n 'Use to root-cause SDK throws that may precede a Toss app crash (#265 / #267). ' +\n 'The buffer holds up to 50 most recent exceptions and survives target ' +\n 'replaced/crashed/destroyed events so an exception just before a crash is preserved. ' +\n 'Returns up to 50 most recent by default.',\n inputSchema: {\n type: 'object',\n properties: {\n limit: {\n type: 'number',\n description: 'Maximum number of exceptions to return (default 50, max 50).',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'call_sdk',\n description:\n 'Calls a dog-food SDK method via the window.__sdkCall bridge ' +\n '(exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). ' +\n 'NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). ' +\n 'On env 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) and ' +\n 'env 2 (PWA relay — real WebKit, mock SDK) it hits the mock SDK. ' +\n 'Requires the relay to be attached — call list_pages first. ' +\n 'Returns {ok: true, value} on success or {ok: false, error} on failure. ' +\n 'If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], ' +\n 'the result also includes `recentException` for crash triage. ' +\n 'Returns a clear error if window.__sdkCall is not available — on relay (env 3/4) ' +\n 'that means a non-dog-food bundle (redeploy via `ait build && aitcc app deploy`); ' +\n 'on local (--target=local, env 1) it means the dev bridge is not installed ' +\n '(start the dev server with `pnpm dev`).\\n\\n' +\n 'SECURITY: method name, args, and result value are not redacted — never include secrets.\\n\\n' +\n 'LIVE guard: when running against a live/production relay (relay-live env, ' +\n 'MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that ' +\n 'the SDK call may affect real users. Without it the call is rejected with a ' +\n 'structured error. mock and relay-dev sessions are unaffected.\\n\\n' +\n 'IMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\\n' +\n ' setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\\n' +\n ' setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\\n' +\n ' setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\\n' +\n ' setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\\n' +\n ' getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\\n' +\n ' getPlatformOS: call_sdk(\"getPlatformOS\", [])\\n' +\n ' getDeviceId: call_sdk(\"getDeviceId\", [])\\n' +\n ' getLocale: call_sdk(\"getLocale\", [])\\n' +\n ' getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\\n' +\n ' getSchemeUri: call_sdk(\"getSchemeUri\", [])\\n' +\n ' requestReview: call_sdk(\"requestReview\", [])\\n' +\n ' closeView: call_sdk(\"closeView\", [])',\n inputSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'SDK method name to call (e.g. \"getOperationalEnvironment\").',\n },\n args: {\n type: 'array',\n description: 'Arguments to pass to the SDK method (optional, default []).',\n items: {},\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge ' +\n 'that this SDK call may have side effects on real/live users. ' +\n 'Omitting this in a relay-live session results in a structured rejection error. ' +\n 'Has no effect in mock or relay-dev sessions.',\n },\n },\n required: ['name'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getSdkCallHistory',\n description:\n 'Returns the recent Apps In Toss SDK call trace (method, args, result/error, timestamp) that ' +\n 'raw CDP cannot observe. Read-only. Use to confirm an SDK call fired and how it resolved ' +\n '(e.g. a saveBase64Data permission regression).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getMockState',\n description:\n 'Returns the devtools mock state snapshot (window.__ait) — environment, permissions, location, ' +\n 'auth, network, IAP, and more. Read-only. In dev mode this is the live browser mock state; in ' +\n 'debug mode the in-app side reports it over the AIT domain.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getOperationalEnvironment',\n description:\n 'Returns getOperationalEnvironment() plus the resolved SDK version — metadata raw CDP cannot ' +\n 'observe. Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'start_debug',\n description:\n 'Switches the active debug environment in-place (issue #348) — no Claude Code restart and ' +\n 'no MCP re-handshake. One daemon holds both a local (env 1, mock SDK in a Chromium) and a ' +\n 'relay (env 3/4, real-device Toss WebView over the Chii relay + cloudflared tunnel) ' +\n 'connection at once; this tool flips which one every other tool reads from, lazily booting ' +\n \"the requested family's infra on first use and keeping the inactive one warm so an existing \" +\n 'attach survives the switch. After switching it emits notifications/tools/list_changed — ' +\n 'call tools/list again to see the updated tool surface for the new environment.\\n\\n' +\n 'modes:\\n' +\n ' local-browser — env 1: desktop Chromium with the mock SDK and a local CDP attach. ' +\n 'Side-effect tools (call_sdk/evaluate) run unguarded against the mock; nothing touches a ' +\n 'real device or real users. No prerequisites — the default, always-available environment ' +\n 'for state/contract and visual-layout work.\\n' +\n ' relay-sandbox — env 2: a real-device PWA (real WebKit engine, mock SDK) over an external ' +\n 'Chii relay. CDP covers real-device WebKit DOM, console, exceptions, and safe-area ' +\n 'observation; call_sdk still hits the mock (SDK fidelity needs relay-staging). liveIntent ' +\n 'off — dev-intent, LIVE guard inactive, side-effect tools run unguarded against the mock. ' +\n 'Only the dual-connection daemon can enter relay-sandbox in-place; a single-connection ' +\n 'session rejects it with \"동적 전환할 수 없습니다 … relay-sandbox 모드로 재시작하세요\" — ' +\n 'follow that hint and restart the MCP server in relay-sandbox mode rather than retrying. ' +\n 'Prerequisites: both AIT_RELAY_BASE_URL (the relay base the unplugin emits when started ' +\n 'with tunnel:{cdp:true}, used for the CDP attach) and AIT_TUNNEL_BASE_URL (the dev-server ' +\n 'tunnel host, required by build_attach_url to render the launcher QR) must be set before ' +\n 'the MCP server starts — the unplugin does not auto-forward either; set them explicitly. ' +\n 'Both carry relay/tunnel hosts (secret-class) — keep them out of logs.\\n' +\n ' relay-staging — env 3: a real-device Toss WebView dog-food build with the REAL SDK over the ' +\n 'intoss-private relay. The first environment where call_sdk exercises the genuine native ' +\n 'bridge. Side-effect tools run unguarded (dog-food, not released to real users). ' +\n 'Prerequisite: a dog-food candidate bundle built with `RELEASE_CHANNEL=dogfood ait build`, ' +\n 'then uploaded with `ait deploy` (add `--scheme-only` to print the resulting ' +\n 'intoss-private://…?_deploymentId=… deep-link); open that deep-link/QR on the device to ' +\n 'cold-load the bundle with the relay injected. Unlike env 2, env 3 is NOT a dev-server ' +\n 'tunnel — it is a deployed bundle reached via the intoss-private scheme, so `pnpm dev` ' +\n 'plays no part here.\\n' +\n ' relay-live — env 4: the REVIEW-PASSED, released production runtime with the REAL SDK over ' +\n 'the intoss relay — real end users are on the other side. Read-only debugging is the intent: ' +\n 'the LIVE guard is armed, so call_sdk/evaluate require confirm:true per call, and ENTERING ' +\n 'relay-live ALSO requires confirm:true on this call. Use it only to observe a shipped ' +\n 'regression; verify fixes in relay-staging first.\\n\\n' +\n 'Switching back to local-browser automatically disarms the LIVE guard.\\n\\n' +\n 'For a relay mode (relay-sandbox/relay-staging/relay-live), also pass projectRoot — the ' +\n 'absolute mini-app project root — so the daemon can read the relay auth secret from ' +\n '<projectRoot>/.ait_relay (read-only; the daemon never mints it). Omit it for local-browser.',\n inputSchema: {\n type: 'object',\n properties: {\n mode: {\n type: 'string',\n enum: ['local-browser', 'relay-sandbox', 'relay-staging', 'relay-live'],\n description:\n 'Target environment to switch to. mode=relay-live additionally requires confirm: true (and arms the read-only LIVE guard).',\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when mode=relay-live — set true to acknowledge entering LIVE (env 4) ' +\n 'debugging that can affect real users. Ignored for the other modes.',\n },\n projectRoot: {\n type: 'string',\n description:\n 'Absolute path to the mini-app project root (the directory containing its package.json and .ait_relay). ' +\n 'The daemon reads the relay auth secret from <projectRoot>/.ait_relay (read-only) when switching to a relay ' +\n \"environment (relay-staging/relay-live/relay-sandbox). Pass this because the daemon's own cwd is fixed at launch and may not be \" +\n 'the project being debugged. Omit for mode=local-browser (no secret needed).',\n },\n },\n required: ['mode'],\n },\n // Tier C — always callable so the agent can enter any environment from any\n // starting environment (including a fresh, unattached session).\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_debug_status',\n description:\n 'Reports the current debug session state — which environment/mode is active, whether a page ' +\n 'is attached, and a full diagnostic snapshot — in one call. Use this any time to answer ' +\n '\"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. ' +\n 'Fields: mcpVersion (MCP SDK version), ' +\n 'devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), ' +\n 'pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, ' +\n 'recentErrors (last N server-side errors, PII/secret redacted), ' +\n 'authRejects ({count, lastAt} — relay TOTP 401 rejections, secret-free; count > 0 with empty pages ' +\n 'means the phone reached the relay but its code was rejected), ' +\n 'environment (kind: mock|relay-dev|relay-live|relay-mobile, env: mock|relay backward-compat, reason, ' +\n 'liveGuardActive: true when relay-live LIVE guard is active; ' +\n 'start_debug mode→kind mapping: relay-sandbox→relay-mobile, relay-staging→relay-dev, ' +\n 'relay-live→relay-live, local-browser→mock), ' +\n 'serverLockHolder (pid + startedAt from the lock file, or null), ' +\n 'nextRecommendedAction ({tool, reason} or null — the single next tool to call; ' +\n 'in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). ' +\n 'All fields are nullable — missing data is null, not an error. ' +\n 'debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. ' +\n 'Tier C (both mock and relay).',\n inputSchema: {\n type: 'object',\n properties: {\n recent_errors_limit: {\n type: 'number',\n description:\n 'Maximum number of recent server-side errors to include (default 10, max 50).',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n] as const;\n\nexport type DebugToolName = (typeof DEBUG_TOOL_DEFINITIONS)[number]['name'];\n\nconst DEBUG_TOOL_NAMES = new Set<string>(DEBUG_TOOL_DEFINITIONS.map((t) => t.name));\n\nexport function isDebugToolName(name: string): name is DebugToolName {\n return DEBUG_TOOL_NAMES.has(name);\n}\n\n/**\n * Returns the `ToolAvailability` declared on a registered debug tool, or\n * `undefined` when the name is not a known debug tool. Used by the tool\n * registry to filter `tools/list` by current env and by the call handler to\n * reject env-mismatch invocations.\n */\nexport function getToolAvailability(name: string): ToolAvailability | undefined {\n for (const t of DEBUG_TOOL_DEFINITIONS) {\n if (t.name === name) return t.availableIn;\n }\n return undefined;\n}\n\n/**\n * Returns true when the named tool is available in the given environment.\n * Unknown tools return `false` — callers should reject them as unknown rather\n * than as env-mismatched.\n *\n * Relay variants (`relay-dev`, `relay-live`, `relay-mobile`) all satisfy the\n * `'relay'` availability tier — `isRelayEnv()` is used for the check.\n */\nexport function isToolAvailableIn(name: string, env: McpEnvironment): boolean {\n const availability = getToolAvailability(name);\n if (availability === undefined) return false;\n if (availability === 'both') return true;\n if (availability === 'relay') return isRelayEnv(env);\n return availability === env;\n}\n\n/**\n * Filters a `DEBUG_TOOL_DEFINITIONS`-shaped list to those whose `availableIn`\n * matches the given env. Pure — preserves order; both Tier C (\"both\") and the\n * matching single-env tier pass through.\n *\n * Relay variants (`relay-dev`, `relay-live`, `relay-mobile`) all satisfy the\n * `'relay'` tier.\n */\nexport function filterToolsByEnvironment<T extends { name: string; availableIn: ToolAvailability }>(\n tools: ReadonlyArray<T>,\n env: McpEnvironment,\n): T[] {\n return tools.filter(\n (t) =>\n t.availableIn === 'both' ||\n (t.availableIn === 'relay' && isRelayEnv(env)) ||\n t.availableIn === env,\n );\n}\n\n/**\n * Tool names that are available before any page attaches (bootstrap tier).\n *\n * `build_attach_url` — pure URL synthesis, no attach needed.\n * `list_pages` — reports tunnel status + empty pages even pre-attach.\n *\n * All other tools require an attached page (`enableDomains` must succeed) and\n * are only advertised in `tools/list` once a target appears.\n */\nexport const BOOTSTRAP_TOOL_NAMES: ReadonlySet<string> = new Set<string>([\n 'build_attach_url',\n 'get_debug_status',\n 'list_pages',\n // start_debug must be visible from the very first tools/list (before any\n // attach) so the agent can switch environments to bootstrap an attach.\n 'start_debug',\n]);\n\n/** Normalized console message returned by `list_console_messages`. */\nexport interface ConsoleMessage {\n level: string;\n text: string;\n timestamp: number;\n args: string[];\n}\n\n/** Normalized network request returned by `list_network_requests`. */\nexport interface NetworkRequest {\n requestId: string;\n url: string;\n method: string;\n /** HTTP status once a response was seen, else null (still in-flight). */\n status: number | null;\n statusText: string | null;\n /** Request start (CDP timestamp). */\n startTime: number;\n /** Response received (CDP timestamp), else null. */\n endTime: number | null;\n}\n\n/** Renders a CDP `RemoteObject` console arg to a stable display string. */\nfunction renderRemoteObject(arg: CdpRemoteObject): string {\n if (arg.value !== undefined) {\n if (typeof arg.value === 'string') return arg.value;\n try {\n return JSON.stringify(arg.value);\n } catch {\n return String(arg.value);\n }\n }\n if (arg.description !== undefined) return arg.description;\n if (arg.className !== undefined) return arg.className;\n return arg.subtype ?? arg.type;\n}\n\nexport function normalizeConsoleMessage(event: ConsoleApiCalledEvent): ConsoleMessage {\n const args = event.args.map(renderRemoteObject);\n return {\n level: event.type,\n text: args.join(' '),\n timestamp: event.timestamp,\n args,\n };\n}\n\nexport function listConsoleMessages(connection: CdpConnection): ConsoleMessage[] {\n return connection\n .getBufferedEvents('Runtime.consoleAPICalled')\n .map((event) => normalizeConsoleMessage(event));\n}\n\nexport function listNetworkRequests(connection: CdpConnection): NetworkRequest[] {\n const requests = connection.getBufferedEvents('Network.requestWillBeSent');\n const responses = connection.getBufferedEvents('Network.responseReceived');\n\n const responseByRequestId = new Map<string, NetworkResponseReceivedEvent>();\n for (const response of responses) {\n responseByRequestId.set(response.requestId, response);\n }\n\n return requests.map((request: NetworkRequestWillBeSentEvent) => {\n const response = responseByRequestId.get(request.requestId);\n return {\n requestId: request.requestId,\n url: request.request.url,\n method: request.request.method,\n status: response ? response.response.status : null,\n statusText: response ? response.response.statusText : null,\n startTime: request.timestamp,\n endTime: response ? response.timestamp : null,\n };\n });\n}\n\n/* -------------------------------------------------------------------------- */\n/* list_exceptions — Runtime.exceptionThrown ring buffer */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Normalized exception returned by `list_exceptions`.\n *\n * Flattens the CDP `Runtime.ExceptionDetails` shape into the most useful\n * fields. The `raw` field carries the original event for callers that need\n * the full payload.\n */\nexport interface BufferedException {\n /** Wall-clock ms since epoch (CDP `Runtime.Timestamp`). */\n timestamp: number;\n /** Short summary text from `exceptionDetails.text`. */\n text: string;\n /** Source URL where the exception was thrown, if known. */\n url?: string;\n /** 0-based line number in the source file, if known. */\n lineNumber?: number;\n /** 0-based column number in the source file, if known. */\n columnNumber?: number;\n /** `description` of the thrown `RemoteObject` (e.g. \"TypeError: …\"). */\n exceptionText?: string;\n /**\n * Formatted stack trace: `at fn (url:line:col)` lines joined by `\\n`.\n * Omitted when no `stackTrace.callFrames` are available.\n */\n stack?: string;\n /** Full original `Runtime.exceptionThrown` event payload. */\n raw: RuntimeExceptionThrownEvent;\n}\n\n/** Formats a single CDP call frame into `at fn (url:line:col)`. */\nfunction formatCallFrame(frame: CdpCallFrame): string {\n const fn = frame.functionName || '(anonymous)';\n return `at ${fn} (${frame.url}:${frame.lineNumber}:${frame.columnNumber})`;\n}\n\n/** Normalizes a raw `Runtime.exceptionThrown` event into a `BufferedException`. */\nexport function normalizeException(event: RuntimeExceptionThrownEvent): BufferedException {\n const { timestamp, exceptionDetails } = event;\n const frames = exceptionDetails.stackTrace?.callFrames;\n const stack = frames && frames.length > 0 ? frames.map(formatCallFrame).join('\\n') : undefined;\n const exceptionText = exceptionDetails.exception?.description ?? undefined;\n\n const result: BufferedException = {\n timestamp,\n text: exceptionDetails.text,\n raw: event,\n };\n if (exceptionDetails.url !== undefined) result.url = exceptionDetails.url;\n if (exceptionDetails.lineNumber !== undefined) result.lineNumber = exceptionDetails.lineNumber;\n if (exceptionDetails.columnNumber !== undefined)\n result.columnNumber = exceptionDetails.columnNumber;\n if (exceptionText !== undefined) result.exceptionText = exceptionText;\n if (stack !== undefined) result.stack = stack;\n return result;\n}\n\n/**\n * Returns the most recent buffered `Runtime.exceptionThrown` events, normalized.\n * Oldest-first; limited to `limit` entries (default 50, max 50).\n */\nexport function listExceptions(connection: CdpConnection, limit = 50): BufferedException[] {\n const cap = Math.min(Math.max(1, limit), 50);\n const events = connection.getBufferedEvents('Runtime.exceptionThrown');\n // Slice from the tail to respect the cap while preserving oldest-first order.\n const sliced = events.length > cap ? events.slice(events.length - cap) : events;\n return sliced.map((e) => normalizeException(e));\n}\n\n/** A page entry in the `list_pages` result, extended with freshness info. */\nexport interface ListPagesEntry {\n id: string;\n title: string;\n url: string;\n /** ISO timestamp of the last inbound CDP message from this target, or null. */\n lastSeenAt: string | null;\n}\n\n/** Result of `list_pages`: attach status + tunnel state + crash info. */\nexport interface ListPagesResult {\n /**\n * The single active page, or an empty array when nothing is attached.\n * Under the single-attach model this is always 0 or 1 entries.\n */\n pages: ListPagesEntry[];\n tunnel: TunnelStatus;\n /**\n * ISO timestamp of the most recent crash / targetDestroyed / detachedFromTarget\n * event detected since the last `enableDomains()`, or `null` if none.\n * When non-null, all attached pages have been removed from the relay map and\n * a new `enableDomains()` call is required to resume debugging.\n */\n crashDetectedAt: string | null;\n /** Korean warning line shown in tool output when a crash was detected. */\n crashWarning: string | null;\n /**\n * Always `true` — signals to the agent that at most one page is ever present.\n * When a second page attaches, the previous one is evicted (last-attach wins).\n */\n singleAttachModel: true;\n}\n\n/**\n * Duck-type interface for the crash-detection extras exposed by `ChiiCdpConnection`.\n * The base `CdpConnection` interface is kept minimal (fake-friendly); the extras\n * are opt-in so tests without them continue to compile.\n */\ninterface CrashAwareCdpConnection extends CdpConnection {\n getLastCrashDetectedAt(): number | null;\n getTargetLastSeenAt(targetId: string): number | null;\n}\n\nfunction isCrashAware(conn: CdpConnection): conn is CrashAwareCdpConnection {\n return (\n typeof (conn as CrashAwareCdpConnection).getLastCrashDetectedAt === 'function' &&\n typeof (conn as CrashAwareCdpConnection).getTargetLastSeenAt === 'function'\n );\n}\n\nexport function listPages(connection: CdpConnection, tunnel: TunnelStatus): ListPagesResult {\n const rawTargets = connection.listTargets();\n const pages: ListPagesEntry[] = rawTargets.map((t) => {\n const lastSeenMs = isCrashAware(connection) ? connection.getTargetLastSeenAt(t.id) : null;\n return {\n id: t.id,\n title: t.title,\n url: t.url,\n lastSeenAt: lastSeenMs !== null ? new Date(lastSeenMs).toISOString() : null,\n };\n });\n\n const crashMs = isCrashAware(connection) ? connection.getLastCrashDetectedAt() : null;\n const crashDetectedAt = crashMs !== null ? new Date(crashMs).toISOString() : null;\n const crashWarning = crashDetectedAt\n ? `[ait-debug] page crash 감지됨 — 새 attach 필요 (관측 시각: ${crashDetectedAt})`\n : null;\n\n return { pages, tunnel, crashDetectedAt, crashWarning, singleAttachModel: true };\n}\n\n/** A `build_attach_url` result: the spliced deep-link the phone should open. */\nexport interface BuildAttachUrlResult {\n /** The scheme URL with `debug=1&relay=<wss>[&at=<totp-code>]` spliced in. */\n attachUrl: string;\n /** The relay URL that was spliced in (this session's quick tunnel). */\n relayUrl: string;\n /**\n * Non-fatal warning about the scheme URL's authority being missing or\n * suspicious (e.g. \"web\", \"localhost\"). Callers should surface this to\n * help the user catch a malformed URL early.\n */\n authorityWarning?: string;\n /**\n * TOTP metadata — present when `AIT_DEBUG_TOTP_SECRET` is set.\n *\n * SECRET-HANDLING: the `at=` code value is spliced into `attachUrl` only.\n * It is never surfaced separately here to avoid inadvertent logging of the\n * one-time code outside of the URL.\n */\n totp?: {\n /** `true` when a TOTP code was spliced into `attachUrl`. */\n enabled: true;\n /** RFC 6238 step duration in seconds. */\n ttlSeconds: number;\n /** ISO timestamp when the current step expires. Rescan or call build_attach_url again after this. */\n expiresAt: string;\n };\n}\n\n/**\n * Builds a self-attaching dog-food deep-link from an `ait deploy --scheme-only`\n * URL plus this session's live relay. Throws if the tunnel is not up yet (no\n * relay URL to splice in) — the caller surfaces that as a tool error.\n *\n * When `AIT_DEBUG_TOTP_SECRET` is set, generates the current TOTP code and\n * splices it as `at=<code>` into the attach URL. The code is valid for ~3\n * minutes (the relay gate uses {@link RELAY_VERIFY_SKEW_STEPS}=6, accepting\n * past 6 steps = 180–210 s backwards from issuance). If the scan happens after\n * `totp.expiresAt`, call `build_attach_url` again to get a fresh code (#490).\n *\n * Also validates the scheme URL's authority. A suspicious authority (empty,\n * \"web\", \"localhost\", etc.) is surfaced as a non-fatal `authorityWarning` on\n * the result so the caller can show a helpful hint without blocking the link\n * generation (the warning is consistent with how other validation in\n * `buildDeepLinkAttachUrl` works — hard errors for relay, soft warning for\n * the scheme authority which is in the caller's input, not ours to own).\n *\n * SECRET-HANDLING: `totpSecret` (if provided) is used only to compute a code\n * and must never appear in any log, error message, or output outside of the\n * spliced `at=` param in `attachUrl`.\n *\n * @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL.\n * @param tunnel - Current tunnel status from the running debug server.\n * @param totpSecret - Optional hex-encoded TOTP secret (from\n * `AIT_DEBUG_TOTP_SECRET`). When provided, the current code is spliced into\n * the attach URL as `at=<code>`.\n */\nexport function buildAttachUrl(\n schemeUrl: string,\n tunnel: TunnelStatus,\n totpSecret?: string,\n): BuildAttachUrlResult {\n if (!tunnel.up || tunnel.wssUrl === null) {\n throw new Error(\n 'tunnel-down: cloudflared 터널이 안 떠 있습니다. ' +\n 'MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.',\n );\n }\n const authorityWarning = validateSchemeAuthority(schemeUrl) ?? undefined;\n\n // Generate a live TOTP code when a secret is provided.\n // SECRET-HANDLING: the code value is placed into attachUrl only — not logged.\n let totpCode: string | undefined;\n let totpMeta: BuildAttachUrlResult['totp'];\n if (totpSecret !== undefined && totpSecret !== '') {\n const now = Date.now();\n totpCode = generateTotp(totpSecret, now);\n const STEP_SECONDS = 30;\n // expiresAt reflects the relay gate's actual acceptance window (#490):\n // the gate uses RELAY_VERIFY_SKEW_STEPS=6, so past 6 steps (180 s) are\n // accepted. The code issued NOW is valid until step (currentStep + 7)\n // starts — i.e. the earliest time it can be rejected is 180 s after\n // the NEXT step boundary, which is (currentStep+1)*30 + 6*30 = now-aligned\n // ~180–210 s from issuance. We report issuanceTime + 180 s as a conservative\n // lower bound so callers know the code is safe for at least ~3 minutes.\n const expiresAtMs = now + RELAY_VERIFY_SKEW_STEPS * STEP_SECONDS * 1000;\n totpMeta = {\n enabled: true,\n ttlSeconds: RELAY_VERIFY_SKEW_STEPS * STEP_SECONDS,\n expiresAt: new Date(expiresAtMs).toISOString(),\n };\n }\n\n return {\n attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl, totpCode),\n relayUrl: tunnel.wssUrl,\n ...(authorityWarning !== undefined ? { authorityWarning } : {}),\n ...(totpMeta !== undefined ? { totp: totpMeta } : {}),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* QR PNG rendering + browser open */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Heuristic: can this process open a GUI browser?\n *\n * Returns `true` when we think a GUI is available:\n * - On macOS (`darwin`) we assume yes (MCP normally runs on the user's Mac).\n * - On Linux we check for `DISPLAY` or `WAYLAND_DISPLAY`.\n * - On Windows we assume yes.\n * - In a CI environment (`CI=true`) we assume no.\n */\nexport function canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/**\n * Result of `openQrInBrowser`.\n *\n * HTTP URL 기반으로 재구현 — tmp 파일 없음. `httpUrl`이 브라우저에 전달되는 URL이다.\n * SECRET-HANDLING: `httpUrl`은 127.0.0.1 로컬 전용이며 tunnel host·relay wss·TOTP at= 코드를\n * 담지 않는다 (#595). attachUrl은 server-state(getDashboardState)로만 보유된다.\n */\nexport interface OpenQrInBrowserResult {\n /** `true` if the browser was successfully opened. */\n opened: boolean;\n /** `http://127.0.0.1:<port>/` — 브라우저에 전달되는 루트 URL (#595). */\n httpUrl: string;\n /** `http://127.0.0.1:<port>/qr.png?u=...` — PNG fallback URL. */\n pngUrl: string;\n /** Error message if `opened` is false (browser spawn failed). */\n error?: string;\n /** Captured stderr from failed spawn attempts (at= 값은 redact됨). */\n stderrSummary?: string;\n /**\n * `true` when the first attempt failed but a retry succeeded.\n * Helps distinguish \"worked on first try\" from \"needed retry\" in diagnostics.\n */\n retried?: boolean;\n}\n\n/** platform별 browser open 명령 후보 목록 — 앞에서부터 순차 시도. */\nfunction getBrowserCandidates(httpUrl: string): Array<{ cmd: string; args: string[] }> {\n const platform = process.platform;\n if (platform === 'darwin') {\n return [\n { cmd: 'open', args: [httpUrl] },\n { cmd: 'open', args: ['-a', 'Safari', httpUrl] },\n { cmd: 'open', args: ['-a', 'Google Chrome', httpUrl] },\n { cmd: 'open', args: ['-a', 'Firefox', httpUrl] },\n ];\n }\n if (platform === 'win32') {\n return [\n { cmd: 'cmd', args: ['/c', 'start', '', httpUrl] },\n { cmd: 'rundll32', args: ['url.dll,FileProtocolHandler', httpUrl] },\n ];\n }\n // linux + fallback\n return [\n { cmd: 'xdg-open', args: [httpUrl] },\n { cmd: 'sensible-browser', args: [httpUrl] },\n { cmd: 'x-www-browser', args: [httpUrl] },\n { cmd: 'firefox', args: [httpUrl] },\n { cmd: 'google-chrome', args: [httpUrl] },\n { cmd: 'chromium', args: [httpUrl] },\n ];\n}\n\n/** stderr에서 at= TOTP 코드 값을 redact한다. */\nfunction redactSecrets(text: string): string {\n // at=<value> 패턴에서 값 부분을 redact — TOTP 코드가 노출되지 않도록.\n return text.replace(/\\bat=([^&\\s\"']+)/g, 'at=<redacted>');\n}\n\n/** spawnSync exit 0이어도 stderr에 launch 실패 시그널이 있으면 실패로 판단한다. */\nconst LAUNCH_FAILURE_PATTERNS = [\n /LSOpenURLsWithRole\\(\\) failed/,\n /kLSApplicationNotFoundErr/,\n /No application/,\n /Unable to find application/,\n /xdg-open: not found/,\n /command not found/,\n];\n\nfunction isLaunchFailureStderr(stderr: string): boolean {\n return LAUNCH_FAILURE_PATTERNS.some((p) => p.test(stderr));\n}\n\n/**\n * 로컬 HTTP 서버 루트 URL(`http://127.0.0.1:<port>/`)을 OS 기본 브라우저로 연다 (#595).\n *\n * platform별 fallback chain으로 시도하며, 모두 실패하면 1회 retry를 수행한다\n * (ephemeral process launch 타이밍 문제 대응). retry까지 실패해도 `opened: false` +\n * `httpUrl`을 반환해 사용자가 직접 브라우저에 붙여넣을 수 있게 한다.\n *\n * SECRET-HANDLING:\n * - tmp 파일을 만들지 않는다 (HTML/PNG는 HTTP 서버가 메모리에서 응답).\n * - httpUrl은 `http://127.0.0.1:<port>/`(루트, 시크릿 없음). pngUrl은 127.0.0.1 로컬 전용.\n * - stderr 캡처 결과에서 at= 코드 값을 redact한 후 stderrSummary에 포함.\n * - attachUrl, deploymentId, TOTP 코드를 stdout/stderr/로그에 직접 출력 금지.\n *\n * @param httpUrl - `http://127.0.0.1:<port>/` 루트 URL (시크릿 없음, #595).\n * @param pngUrl - `http://127.0.0.1:<port>/qr.png?u=<encoded>` PNG fallback URL.\n */\nexport async function openQrInBrowser(\n httpUrl: string,\n pngUrl: string,\n): Promise<OpenQrInBrowserResult> {\n const { spawnSync } = await import('node:child_process');\n\n /**\n * 한 번의 fallback chain 시도. 성공하면 열린 후보 cmd를 반환, 실패하면 null.\n * stderrLines에 각 후보의 stderr를 누적한다.\n */\n function tryOnce(stderrLines: string[]): boolean {\n const candidates = getBrowserCandidates(httpUrl);\n for (const { cmd, args } of candidates) {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5000 });\n\n if (result.error) {\n stderrLines.push(`${cmd}: ${result.error.message}`);\n continue;\n }\n\n const stderr = typeof result.stderr === 'string' ? result.stderr : '';\n if (stderr) {\n stderrLines.push(`${cmd}: ${redactSecrets(stderr.trim())}`);\n }\n\n if (result.status === 0 && !isLaunchFailureStderr(stderr)) {\n return true;\n }\n }\n return false;\n }\n\n const stderrLines: string[] = [];\n\n // 1차 시도\n if (tryOnce(stderrLines)) {\n return { opened: true, httpUrl, pngUrl };\n }\n\n // 1회 retry (ephemeral process launch 타이밍 문제 대응)\n if (tryOnce(stderrLines)) {\n return { opened: true, httpUrl, pngUrl, retried: true };\n }\n\n const stderrSummary = stderrLines.length > 0 ? stderrLines.join('\\n') : undefined;\n return {\n opened: false,\n httpUrl,\n pngUrl,\n error: '모든 브라우저 실행 후보가 실패했습니다.',\n stderrSummary,\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Phase 2 — DOM / snapshot / screenshot (CDP commands) */\n/* -------------------------------------------------------------------------- */\n\n/** Returns the DOM tree of the attached page (`DOM.getDocument`). */\nexport function getDomDocument(connection: CdpConnection): Promise<DomGetDocumentResult> {\n // `pierce: true` flattens shadow roots; depth -1 returns the whole subtree so\n // a single call yields the full tree for structural diagnosis.\n return connection.send('DOM.getDocument', { depth: -1, pierce: true });\n}\n\n/** Returns a serialized page snapshot (`DOMSnapshot.captureSnapshot`). */\nexport function takeSnapshot(connection: CdpConnection): Promise<DomSnapshotResult> {\n return connection.send('DOMSnapshot.captureSnapshot', {});\n}\n\n/** A `take_screenshot` result: the raw base64 PNG plus a ready-to-use data URI. */\nexport interface ScreenshotResult {\n /** Base64-encoded PNG bytes (no data-URI prefix). */\n data: string;\n /** `data:image/png;base64,…` form for clients that render a URI. */\n dataUri: string;\n mimeType: 'image/png';\n}\n\n/** Captures a PNG screenshot of the attached page (`Page.captureScreenshot`). */\nexport async function takeScreenshot(connection: CdpConnection): Promise<ScreenshotResult> {\n const { data } = await connection.send('Page.captureScreenshot', { format: 'png' });\n return { data, dataUri: `data:image/png;base64,${data}`, mimeType: 'image/png' };\n}\n\n/* -------------------------------------------------------------------------- */\n/* measure_safe_area — Runtime.evaluate probe */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The JS probe injected via `Runtime.evaluate`. It reads:\n * 1. `env(safe-area-inset-*)` via a temporary element with padding set to\n * those CSS env vars, then `getComputedStyle`.\n * 2. SDK insets via a priority chain so the SAME probe works on both relay\n * (real device) and mock (devtools panel page):\n * a. `window.__sdk.SafeAreaInsets.get()` — dog-food bundle on real device.\n * b. `window.__sdk.getSafeAreaInsets()` — dog-food bundle (deprecated).\n * c. `window.__ait.state.safeAreaInsets` — devtools mock state (mock env).\n * The probe records `sdkInsetsSource` = `'window.__sdk'` | `'window.__ait'`\n * | `null`. If all paths fail the result carries `sdkInsetsError`.\n * 3. nav bar geometry: the SDK does not expose navBar height as a standalone\n * API — `.ait-navbar` DOM height is read as a cross-check, and\n * `navBarHeightSource` records where it came from.\n * 4. `innerWidth`, `innerHeight`, `devicePixelRatio`, `navigator.userAgent`.\n *\n * Returns a plain JSON-serialisable object so `returnByValue: true` works.\n *\n * NOTE: This expression is evaluated in the page context — on the real device\n * (relay) or on the mock panel page. It does not mutate any page state — the\n * temporary element is removed after reading. No secret or auth token is read\n * or returned.\n *\n * RFC #277 Tier C parity: the SAME probe string runs in both envs. Mock fidelity\n * comes from the panel's `applyViewport` / `computeSafeAreaInsets` correctly\n * setting `window.__ait.state.safeAreaInsets` (#275). When that is correct,\n * the cssEnv + sdkInsets pair returned here matches the relay's shape.\n */\nexport const SAFE_AREA_PROBE_EXPRESSION = `\n(function() {\n var el = document.createElement('div');\n el.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;visibility:hidden;' +\n 'padding-top:env(safe-area-inset-top,0px);' +\n 'padding-right:env(safe-area-inset-right,0px);' +\n 'padding-bottom:env(safe-area-inset-bottom,0px);' +\n 'padding-left:env(safe-area-inset-left,0px)';\n document.documentElement.appendChild(el);\n var cs = window.getComputedStyle(el);\n var cssEnv = {\n top: parseFloat(cs.paddingTop) || 0,\n right: parseFloat(cs.paddingRight) || 0,\n bottom: parseFloat(cs.paddingBottom) || 0,\n left: parseFloat(cs.paddingLeft) || 0\n };\n document.documentElement.removeChild(el);\n var sdkInsets = null;\n var sdkInsetsSource = null;\n var sdkInsetsError = undefined;\n try {\n var sdk = window.__sdk;\n var ait = window.__ait;\n if (sdk && sdk.SafeAreaInsets && typeof sdk.SafeAreaInsets.get === 'function') {\n sdkInsets = sdk.SafeAreaInsets.get();\n sdkInsetsSource = 'window.__sdk';\n } else if (sdk && typeof sdk.getSafeAreaInsets === 'function') {\n sdkInsets = sdk.getSafeAreaInsets();\n sdkInsetsSource = 'window.__sdk';\n } else if (ait && ait.state && ait.state.safeAreaInsets &&\n typeof ait.state.safeAreaInsets.top === 'number') {\n var s = ait.state.safeAreaInsets;\n sdkInsets = { top: s.top, bottom: s.bottom, left: s.left, right: s.right };\n sdkInsetsSource = 'window.__ait';\n } else if (!sdk && !ait) {\n sdkInsetsError = 'neither window.__sdk (relay) nor window.__ait (mock) available';\n } else if (sdk) {\n sdkInsetsError = 'neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk';\n } else {\n sdkInsetsError = 'window.__ait.state.safeAreaInsets is missing or malformed';\n }\n } catch(e) {\n sdkInsetsError = String(e && e.message || e);\n }\n var navBarHeight = null;\n var navBarHeightSource = 'not-exposed-by-sdk';\n try {\n var nb = document.querySelector('.ait-navbar');\n if (nb) {\n navBarHeight = nb.getBoundingClientRect().height;\n navBarHeightSource = 'dom-.ait-navbar';\n }\n } catch(_) {}\n var result = {\n cssEnv: cssEnv,\n sdkInsets: sdkInsets,\n sdkInsetsSource: sdkInsetsSource,\n navBarHeight: navBarHeight,\n navBarHeightSource: navBarHeightSource,\n innerWidth: window.innerWidth,\n innerHeight: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio,\n userAgent: navigator.userAgent\n };\n if (sdkInsetsError !== undefined) result.sdkInsetsError = sdkInsetsError;\n return JSON.stringify(result);\n})()\n`.trim();\n\n/**\n * Where the SDK insets came from. `null` when the lookup failed (in which case\n * `sdkInsetsError` is populated).\n *\n * - `'window.__sdk'` — real-device dog-food bundle (relay env).\n * - `'window.__ait'` — devtools mock state (mock env).\n * - `null` — both paths absent or threw.\n */\nexport type SdkInsetsSource = 'window.__sdk' | 'window.__ait' | null;\n\n/**\n * Normalized result returned by `measure_safe_area`.\n *\n * All inset values are in CSS pixels as reported by the page context.\n * `userAgent` is included for device identification; it never contains\n * authentication secrets or session tokens.\n */\nexport interface SafeAreaMeasurement {\n /**\n * MCP environment this measurement was taken in:\n * - `'mock'` — dev browser panel\n * - `'relay-dev'` — real-device WebView, dog-food build\n * - `'relay-live'` — real-device WebView, live/production build\n * - `'relay-mobile'` — real-device PWA (env 2) over an external relay\n *\n * Set by the caller (`measureSafeArea`) from the env detection SSoT\n * (`getEnvironment`).\n */\n source: McpEnvironment;\n /**\n * `env(safe-area-inset-*)` values read via `getComputedStyle` on the page.\n * On iOS inside the Toss host WebView this is typically all-zero because the\n * WebView viewport is placed below the physical notch by the host app.\n */\n cssEnv: { top: number; right: number; bottom: number; left: number };\n /**\n * SDK insets from one of three paths (in priority order):\n * - `window.__sdk.SafeAreaInsets.get()` (relay, dog-food bundle)\n * - `window.__sdk.getSafeAreaInsets()` (relay, deprecated)\n * - `window.__ait.state.safeAreaInsets` (mock, devtools panel state)\n *\n * `null` when all paths fail — see `sdkInsetsError` for the reason.\n * In the Toss host WebView `top` is the nav bar height and `bottom` is the\n * home-indicator height.\n */\n sdkInsets: { top: number; right: number; bottom: number; left: number } | null;\n /**\n * Which path resolved `sdkInsets` — useful for diagnosis of fidelity gaps\n * between mock and relay. `null` when `sdkInsets` is `null`.\n */\n sdkInsetsSource: SdkInsetsSource;\n /**\n * Populated when the SDK inset lookup failed (all paths absent or threw).\n * `undefined` when `sdkInsets` is non-null (i.e. the lookup succeeded).\n *\n * Example values:\n * - `\"neither window.__sdk (relay) nor window.__ait (mock) available\"`\n * - `\"neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk\"`\n * - `\"window.__ait.state.safeAreaInsets is missing or malformed\"`\n * - `\"TypeError: ...\"`\n */\n sdkInsetsError?: string;\n /**\n * Height of the `.ait-navbar` element (px) if present, else `null`.\n * The SDK does not expose navBar height as a standalone API; this DOM\n * measurement is used to cross-validate `sdkInsets.top`.\n */\n navBarHeight: number | null;\n /**\n * Describes where `navBarHeight` came from:\n * - `\"dom-.ait-navbar\"` — read from the `.ait-navbar` element's bounding rect.\n * - `\"not-exposed-by-sdk\"` — the SDK has no standalone navBar height API and\n * no `.ait-navbar` element was found in the DOM.\n */\n navBarHeightSource: string;\n /** CSS viewport width (`window.innerWidth`). */\n innerWidth: number;\n /** CSS viewport height (`window.innerHeight`). */\n innerHeight: number;\n /**\n * Device pixel ratio (`window.devicePixelRatio`).\n * Note: `window.devicePixelRatio` is read-only in the browser, so devtools\n * cannot emulate DPR locally — this is the ground-truth value from the device.\n */\n devicePixelRatio: number;\n /**\n * `navigator.userAgent` string for device identification.\n * Does not contain authentication secrets.\n */\n userAgent: string;\n}\n\n/**\n * Parses a raw `Runtime.evaluate` result value into a `SafeAreaMeasurement`.\n * The probe returns a JSON string (because `returnByValue:true` with a plain\n * object works unreliably across Chii relay versions — stringifying is safer).\n *\n * `source` is supplied by the caller (`measureSafeArea`) from the env SSoT.\n *\n * Throws if the result is missing, contains an exception, or cannot be parsed.\n */\nexport function normalizeSafeAreaResult(\n rawValue: unknown,\n source: McpEnvironment,\n): SafeAreaMeasurement {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `measure_safe_area: probe returned unexpected type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n throw new Error(`measure_safe_area: probe returned non-JSON string: ${rawValue}`);\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('measure_safe_area: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n\n function requireInsets(\n key: string,\n ): { top: number; right: number; bottom: number; left: number } | null {\n const v = obj[key];\n if (v === null || v === undefined) return null;\n if (typeof v !== 'object') return null;\n const r = v as Record<string, unknown>;\n return {\n top: typeof r.top === 'number' ? r.top : 0,\n right: typeof r.right === 'number' ? r.right : 0,\n bottom: typeof r.bottom === 'number' ? r.bottom : 0,\n left: typeof r.left === 'number' ? r.left : 0,\n };\n }\n\n const cssEnv = requireInsets('cssEnv') ?? { top: 0, right: 0, bottom: 0, left: 0 };\n const sdkInsets = requireInsets('sdkInsets');\n const sdkInsetsSource: SdkInsetsSource =\n obj.sdkInsetsSource === 'window.__sdk' || obj.sdkInsetsSource === 'window.__ait'\n ? obj.sdkInsetsSource\n : null;\n const sdkInsetsError = typeof obj.sdkInsetsError === 'string' ? obj.sdkInsetsError : undefined;\n const navBarHeight = typeof obj.navBarHeight === 'number' ? obj.navBarHeight : null;\n const navBarHeightSource =\n typeof obj.navBarHeightSource === 'string' ? obj.navBarHeightSource : 'not-exposed-by-sdk';\n const innerWidth = typeof obj.innerWidth === 'number' ? obj.innerWidth : 0;\n const innerHeight = typeof obj.innerHeight === 'number' ? obj.innerHeight : 0;\n const devicePixelRatio = typeof obj.devicePixelRatio === 'number' ? obj.devicePixelRatio : 1;\n const userAgent = typeof obj.userAgent === 'string' ? obj.userAgent : '';\n\n return {\n source,\n cssEnv,\n sdkInsets,\n sdkInsetsSource,\n ...(sdkInsetsError !== undefined ? { sdkInsetsError } : {}),\n navBarHeight,\n navBarHeightSource,\n innerWidth,\n innerHeight,\n devicePixelRatio,\n userAgent,\n };\n}\n\n/**\n * Runs the safe-area probe on the attached page and returns a normalized\n * `SafeAreaMeasurement`. Read-only — does not mutate page state.\n *\n * `source` is supplied by the caller from the env detection SSoT (see\n * `src/mcp/environment.ts`). The same `Runtime.evaluate` call runs in both\n * envs — the probe expression tries `window.__sdk` first (relay) then\n * `window.__ait` (mock), so mock fidelity is enforced by the panel's\n * `applyViewport`/`computeSafeAreaInsets` keeping `__ait.state.safeAreaInsets`\n * correct (RFC #277 Tier C parity, #275 model).\n *\n * Throws on CDP error, probe exception, or result parse failure.\n */\nexport async function measureSafeArea(\n connection: CdpConnection,\n source: McpEnvironment,\n): Promise<SafeAreaMeasurement> {\n const result = await connection.send('Runtime.evaluate', {\n expression: SAFE_AREA_PROBE_EXPRESSION,\n returnByValue: true,\n awaitPromise: false,\n });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`measure_safe_area: probe threw — ${msg}`);\n }\n return normalizeSafeAreaResult(result.result.value, source);\n}\n\n/* -------------------------------------------------------------------------- */\n/* evaluate — arbitrary JS via Runtime.evaluate */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result returned by the `evaluate` tool.\n *\n * `value` holds the `returnByValue` result from CDP — it may be any\n * JSON-serialisable type. Treat it as opaque for logging purposes (it could\n * carry sensitive data from the page context).\n *\n * SECRET-HANDLING: do NOT write `value` to any log or stderr — return it to\n * the agent via the tool result only.\n */\nexport interface EvaluateResult {\n /** The evaluated result value (`returnByValue: true`). */\n value: unknown;\n /** CDP type string of the result (e.g. \"string\", \"number\", \"object\"). */\n type: string;\n}\n\n/**\n * Evaluates an arbitrary JS expression on the attached page via\n * `Runtime.evaluate`. NOT read-only — the expression may have side effects.\n *\n * Throws if the evaluation produced a CDP exception.\n *\n * SECRET-HANDLING: expression and result value are NOT written to any log.\n */\nexport async function evaluate(\n connection: CdpConnection,\n expression: string,\n): Promise<EvaluateResult> {\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: false,\n });\n if (result.exceptionDetails) {\n // Surface only the engine error string — never the expression or result value.\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`evaluate failed: ${msg}`);\n }\n return { value: result.result.value, type: result.result.type };\n}\n\n/* -------------------------------------------------------------------------- */\n/* call_sdk — window.__sdkCall bridge via Runtime.evaluate */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result returned by the `call_sdk` tool.\n * The bridge call wraps success/failure in a JSON envelope so cross-Chii\n * stringification is reliable (same approach as `measure_safe_area`).\n *\n * `recentException` is populated when a `Runtime.exceptionThrown` event was\n * observed within the heuristic triage window [callStart-50ms, callEnd+200ms].\n * This helps correlate an SDK throw with the bridge result, especially when\n * the SDK throws synchronously before the promise resolves.\n */\nexport type CallSdkResult =\n | { ok: true; value: unknown; recentException?: BufferedException }\n | { ok: false; error: string; recentException?: BufferedException };\n\n/**\n * Builds the Runtime.evaluate expression that calls `window.__sdkCall` with\n * the given method name and args, awaits the promise, and returns a JSON\n * envelope `{ok, value/error}` as a string.\n *\n * Name and args are embedded via `JSON.stringify` so they are safely escaped.\n * The expression checks for `window.__sdkCall` and returns a clear error if\n * it is absent (non-dog-food bundle).\n *\n * SECRET-HANDLING: the expression is built here and MUST NOT be written to\n * any log or stderr by the caller.\n */\nexport function buildCallSdkExpression(name: string, args: unknown[]): string {\n const safeName = JSON.stringify(name);\n const safeArgs = JSON.stringify(args);\n return (\n `(async () => {` +\n ` if (typeof window.__sdkCall !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'sdk-absent: window.__sdkCall이 주입되지 않았습니다 (dog-food 빌드가 아닙니다). dog-food 채널로 재배포하세요.'});` +\n ` }` +\n ` try {` +\n ` const r = await window.__sdkCall(${safeName}, ...${safeArgs});` +\n ` return JSON.stringify({ok:true,value:r});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Parses the JSON envelope string returned by the `call_sdk` expression.\n * Returns a typed `CallSdkResult`.\n *\n * Throws only on parse failure (not on ok:false — that is a normal result).\n */\nexport function normalizeCallSdkResult(rawValue: unknown): CallSdkResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `call_sdk: bridge returned unexpected type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue in the error message — it could contain secrets.\n throw new Error('call_sdk: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('call_sdk: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, value: obj.value };\n }\n if (obj.ok === false) {\n return { ok: false, error: typeof obj.error === 'string' ? obj.error : String(obj.error) };\n }\n throw new Error('call_sdk: bridge result missing \"ok\" field');\n}\n\n/**\n * Looks up the most recent exception from the buffer that falls within the\n * triage window [windowStart, windowEnd]. Returns `undefined` if none found.\n *\n * The heuristic window is:\n * - windowStart = callStart - 50ms (catch sync throws before bridge fires)\n * - windowEnd = callEnd + 200ms (catch async throws resolved soon after)\n *\n * Only the most recent exception within the window is returned (the one most\n * likely to be causally related to the SDK call).\n */\nfunction findRecentException(\n connection: CdpConnection,\n windowStart: number,\n windowEnd: number,\n): BufferedException | undefined {\n const events = connection.getBufferedEvents('Runtime.exceptionThrown');\n // Scan from the tail (most recent) to find the closest-in-time exception.\n for (let i = events.length - 1; i >= 0; i--) {\n const e = events[i];\n if (e.timestamp >= windowStart && e.timestamp <= windowEnd) {\n return normalizeException(e);\n }\n }\n return undefined;\n}\n\n/**\n * Calls a dog-food SDK method via `window.__sdkCall` on the attached page.\n * NOT read-only — SDK calls may have side effects.\n *\n * On env 3/4 (toss WebView relay) this hits the real SDK. On env 1 (local\n * mock) and env 2 (PWA relay — real WebKit, mock SDK) it hits the mock SDK.\n *\n * 인자 시그니처 검증: 등록된 메서드는 bridge 호출 전에 인자를 검증하고, mismatch면\n * `{ok:false, error}` MCP 오류 결과를 반환한다(bridge에 도달하지 않음).\n * 미등록 메서드는 passthrough + stderr 경고 1회.\n *\n * Throws on CDP error or result parse failure. Returns `{ok:false, error}`\n * for bridge-level errors (method not found, SDK threw, bridge absent) or\n * argument schema violations.\n *\n * If a `Runtime.exceptionThrown` event was observed within the triage window\n * [callStart-50ms, callEnd+200ms], the result includes `recentException` for\n * crash triage. This window is a heuristic — it catches the common case of an\n * SDK throw immediately before/after the bridge resolves.\n *\n * SECRET-HANDLING: name, args, and the result value are NOT written to any log.\n */\nexport async function callSdk(\n connection: CdpConnection,\n name: string,\n args: unknown[],\n): Promise<CallSdkResult> {\n // 인자 시그니처 검증 — bridge 호출 전에 reject하여 native crash를 예방한다.\n const signature = lookupSignature(name);\n if (signature !== undefined) {\n const validation = signature.validateArgs(args);\n if (!validation.ok) {\n // isError: true 형태로 반환 — bridge에 도달하지 않음.\n const errorText =\n `call_sdk(\"${name}\") 인자 시그니처 오류.\\n` +\n `받음: ${validation.received}\\n` +\n `기대: ${validation.expected}\\n` +\n `올바른 예시: ${signature.example}`;\n return { ok: false, error: errorText };\n }\n } else {\n // 미등록 메서드 — passthrough하지만 stderr에 경고 1회.\n warnPassthrough(name);\n }\n\n const callStart = Date.now();\n const expression = buildCallSdkExpression(name, args);\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\n });\n const callEnd = Date.now();\n\n if (result.exceptionDetails) {\n // Surface only the engine error string — never name, args, or result value.\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`call_sdk threw: ${msg}`);\n }\n\n const sdkResult = normalizeCallSdkResult(result.result.value);\n\n // Triage window: [callStart - 50ms, callEnd + 200ms].\n // -50ms: catches sync throws that fire just before the bridge call is sent.\n // +200ms: catches async throws resolved shortly after the bridge returns.\n const recentException = findRecentException(connection, callStart - 50, callEnd + 200);\n\n if (recentException !== undefined) {\n return { ...sdkResult, recentException };\n }\n return sdkResult;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Phase 3 — AIT.* domain (CDP can't cover these) */\n/* -------------------------------------------------------------------------- */\n\n/** Set of tool names served by the AIT source rather than the CDP connection. */\nconst AIT_TOOL_NAMES = new Set<string>([\n 'AIT.getSdkCallHistory',\n 'AIT.getMockState',\n 'AIT.getOperationalEnvironment',\n]);\n\n/** True for the Phase 3 AIT.* tools (served by an `AitSource`, not CDP). */\nexport function isAitToolName(name: string): boolean {\n return AIT_TOOL_NAMES.has(name);\n}\n\n/** Returns the recent SDK call trace (`AIT.getSdkCallHistory`). */\nexport function getSdkCallHistory(source: AitSource): Promise<AitSdkCallHistory> {\n return source.get('AIT.getSdkCallHistory');\n}\n\n/** Returns the devtools mock-state snapshot (`AIT.getMockState`). */\nexport function getMockState(source: AitSource): Promise<AitMockState> {\n return source.get('AIT.getMockState');\n}\n\n/** Returns the operational environment + SDK version (`AIT.getOperationalEnvironment`). */\nexport function getOperationalEnvironment(source: AitSource): Promise<AitOperationalEnvironment> {\n return source.get('AIT.getOperationalEnvironment');\n}\n\n/* -------------------------------------------------------------------------- */\n/* get_debug_status — single-call server status snapshot (#286) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Represents a single redacted server-side error entry in the diagnostics\n * snapshot. PII / secrets are scrubbed before this is returned.\n */\nexport interface DiagnosticsError {\n /** ISO timestamp when the error was recorded. */\n timestamp: string;\n /** Error message with PII/secrets redacted (e.g. `at=<redacted>`). */\n message: string;\n /** Optional error category for quick triage. */\n category?: string;\n}\n\n/**\n * Tunnel state in the diagnostics snapshot. Same shape as `TunnelStatus` but\n * extended with the lock-file data (pid, startedAt) when available.\n */\nexport interface DiagnosticsTunnelInfo {\n /** Whether the cloudflared quick tunnel is currently up. */\n up: boolean;\n /** Public `wss://*.trycloudflare.com` relay URL, or `null`. */\n wssUrl: string | null;\n /**\n * PID of the MCP server process that owns the tunnel (from the lock file),\n * or `null` when no lock is present.\n */\n pid: number | null;\n /**\n * ISO timestamp when the owning server process started (from the lock file),\n * or `null`.\n */\n startedAt: string | null;\n /**\n * ISO timestamp when the tunnel permanently dropped (health probe exhausted\n * all reissue attempts). `null` when the tunnel has not permanently dropped.\n * When non-null, the MCP server must be restarted to recover.\n */\n droppedAt: string | null;\n /**\n * Number of automatic reissue attempts made before the permanent drop.\n * 0 when no drop has occurred.\n */\n reissueAttempts: number;\n}\n\n/**\n * Server-lock holder info from `~/.ait-devtools/server.lock`. `null` when\n * no lock file exists (server was cleanly shut down or never started).\n */\nexport interface DiagnosticsLockHolder {\n pid: number;\n startedAt: string;\n /** wssUrl recorded in the lock file — may be `null` when tunnel is still starting. */\n wssUrl: string | null;\n}\n\n/**\n * Secret-free snapshot of relay auth rejections (issue #467).\n *\n * Counts WS-upgrade / HTTP 401 rejections from the relay's TOTP gate so an\n * empty `pages` list can be distinguished from \"the phone never reached the\n * relay\". SECRET-HANDLING: count + timestamp ONLY — never the URL, query,\n * code, or secret.\n */\nexport interface AuthRejectsSnapshot {\n /** Total auth-rejected relay requests since server start. */\n count: number;\n /** ISO timestamp of the most recent rejection, or `null` when none. */\n lastAt: string | null;\n}\n\n/**\n * The next recommended tool for the agent to call, based on the current server\n * state snapshot. `null` means the session looks healthy — no specific action needed.\n */\nexport interface NextRecommendedAction {\n /** MCP tool name to call next (e.g. `'build_attach_url'`, `'restart'`). */\n tool: string;\n /** Human-readable reason explaining why this action is recommended. */\n reason: string;\n}\n\n/**\n * Full server status snapshot returned by `get_debug_status`.\n *\n * All fields are nullable — a missing value means \"not yet known\" (e.g. tunnel\n * not up yet) rather than an error. The schema is intentionally stable across\n * versions: new optional fields may be added but existing fields are not\n * removed or renamed.\n *\n * SECRET-HANDLING: No TOTP secret, cookie, deploy key, or `at=` code value\n * appears in this snapshot. `recentErrors` entries are redacted before inclusion.\n */\nexport interface DiagnosticsResult {\n /** `@modelcontextprotocol/sdk` package version string. */\n mcpVersion: string | null;\n /** `@ait-co/devtools` package version string. */\n devtoolsVersion: string | null;\n /** Tunnel state including lock-file pid/startedAt. */\n tunnel: DiagnosticsTunnelInfo;\n /** Current list_pages result (pages + crash info + singleAttachModel). */\n pages: ListPagesResult | null;\n /** ISO timestamp of the most recent page attach, or `null`. */\n lastAttachAt: string | null;\n /** ISO timestamp of the most recent page detach, or `null`. */\n lastDetachAt: string | null;\n /**\n * Recent server-side errors (up to `recent_errors_limit`, default 10).\n * Redacted: `at=<redacted>`, cookie headers stripped, AITCC_API_KEY masked.\n * When auth rejections occurred, ONE synthetic summary entry\n * (`category: 'auth'`) is appended so an empty array can be read as\n * \"no attach attempts\" without missing silent 401s (issue #467).\n */\n recentErrors: DiagnosticsError[];\n /**\n * Relay TOTP auth-reject counter (issue #467). `count` is 0 when no\n * rejection occurred. Secret-free: count + last timestamp only.\n */\n authRejects: AuthRejectsSnapshot;\n /**\n * Resolved environment and the reason string.\n *\n * `kind` — the precise four-value environment (`mock` | `relay-dev` |\n * `relay-live` | `relay-mobile`). Use this for new code.\n * `env` — backward-compat two-value alias (`mock` | `relay`). Kept so\n * existing callers that only distinguish mock vs relay continue to work.\n */\n environment: {\n kind: McpEnvironment;\n /** @deprecated Use `kind` instead. Kept for backward compatibility. */\n env: 'mock' | 'relay';\n reason: string;\n /** `true` when the LIVE side-effect guard is active (`kind === 'relay-live'`). */\n liveGuardActive: boolean;\n };\n /**\n * Contents of `~/.ait-devtools/server.lock`, or `null` when absent.\n * Useful for diagnosing stale-lock conflicts without running the full server.\n */\n serverLockHolder: DiagnosticsLockHolder | null;\n /**\n * Basic process identity for the running MCP server daemon.\n * Useful for diagnosing orphaned daemons and stale parent associations.\n */\n process: {\n /** PID of this MCP server process. */\n pid: number;\n /** Parent PID at the time `get_debug_status` was called. */\n ppid: number;\n /** Whether the parent process is still alive at snapshot time. */\n parentAlive: boolean;\n };\n /**\n * Single next recommended action for the agent, or `null` when the session\n * looks healthy. Derived deterministically from the other snapshot fields —\n * the agent should call this tool next rather than inferring from raw fields.\n *\n * Branch rules (evaluated in priority order):\n * 0. tunnel.droppedAt non-null → restart (permanent tunnel drop)\n * 1. tunnel.up === false AND relay env → restart\n * 1b. tunnel.up === false AND mock env, no pages → wait_for_page (local target is tunnel-less)\n * 2a. authRejects.count > 0 AND pages empty → build_attach_url (TOTP 거부 — QR 재스캔 안내)\n * 2. tunnel.up, pages empty, env === relay → build_attach_url\n * 3. pages[0] exists + crashDetectedAt non-null → build_attach_url (re-attach)\n * 4. otherwise → null\n */\n nextRecommendedAction: NextRecommendedAction | null;\n}\n\n/**\n * Registry of server-side errors collected by `DiagnosticsCollector`.\n * Injected into `createDebugServer` so it is testable without a real process.\n */\nexport interface DiagnosticsCollector {\n /** Records a server-side error for later surfacing in `get_debug_status`. */\n recordError(message: string, category?: string): void;\n /** Returns the most recent `limit` errors, oldest-first. */\n getRecentErrors(limit: number): DiagnosticsError[];\n /** Records an attach event (ISO timestamp stored). */\n recordAttach(): void;\n /** Records a detach event (ISO timestamp stored). */\n recordDetach(): void;\n /** Returns the ISO timestamp of the last attach, or `null`. */\n getLastAttachAt(): string | null;\n /** Returns the ISO timestamp of the last detach, or `null`. */\n getLastDetachAt(): string | null;\n /**\n * Records one relay auth rejection (issue #467). Secret-free by contract —\n * implementations store only a counter + timestamp, never request data.\n */\n recordAuthReject(): void;\n /** Returns the auth-reject counter snapshot ({count: 0, lastAt: null} when none). */\n getAuthRejects(): AuthRejectsSnapshot;\n}\n\n/** Secret-redaction patterns applied before error messages enter the buffer. */\nconst SECRET_REDACT_PATTERNS: ReadonlyArray<[RegExp, string]> = [\n // TOTP at= code value.\n [/\\bat=([^&\\s\"']+)/g, 'at=<redacted>'],\n // Cookie / Set-Cookie header values — replace everything after the colon.\n [/((?:set-)?cookie)\\s*:\\s*.+/gi, '$1: <redacted>'],\n // AITCC_API_KEY env-var-style references.\n [/AITCC_API_KEY\\s*=\\s*\\S+/gi, 'AITCC_API_KEY=<redacted>'],\n // Authorization header (covers \"Authorization: Bearer …\" and bare \"Bearer <token>\").\n [/Authorization\\s*:\\s*.+/gi, 'Authorization: <redacted>'],\n [/\\bBearer\\s+\\S+/g, 'Bearer <redacted>'],\n];\n\n/**\n * Applies all secret-redaction patterns to an error message string.\n * Used before storing errors in the `DiagnosticsCollector` ring buffer.\n *\n * SECRET-HANDLING: this is the single bottleneck for redaction — all error\n * strings must pass through here before reaching the buffer.\n */\nexport function redactErrorMessage(message: string): string {\n let result = message;\n for (const [pattern, replacement] of SECRET_REDACT_PATTERNS) {\n result = result.replace(pattern, replacement);\n }\n return result;\n}\n\n/** Default max buffer size for the error ring buffer. */\nconst DEFAULT_ERROR_BUFFER_SIZE = 50;\n\n/**\n * In-memory implementation of `DiagnosticsCollector`. Thread-safe in the\n * single-threaded Node.js sense (synchronous mutations only).\n */\nexport class InMemoryDiagnosticsCollector implements DiagnosticsCollector {\n private readonly buffer: DiagnosticsError[] = [];\n private readonly maxSize: number;\n private lastAttachAt: string | null = null;\n private lastDetachAt: string | null = null;\n private authRejectCount = 0;\n private lastAuthRejectAt: string | null = null;\n\n constructor(maxSize = DEFAULT_ERROR_BUFFER_SIZE) {\n this.maxSize = maxSize;\n }\n\n recordError(message: string, category?: string): void {\n const entry: DiagnosticsError = {\n timestamp: new Date().toISOString(),\n message: redactErrorMessage(message),\n ...(category !== undefined ? { category } : {}),\n };\n this.buffer.push(entry);\n // Keep only the most recent `maxSize` entries.\n if (this.buffer.length > this.maxSize) {\n this.buffer.shift();\n }\n }\n\n getRecentErrors(limit: number): DiagnosticsError[] {\n const cap = Math.min(Math.max(1, limit), DEFAULT_ERROR_BUFFER_SIZE);\n const sliced =\n this.buffer.length > cap ? this.buffer.slice(this.buffer.length - cap) : [...this.buffer];\n return sliced;\n }\n\n recordAttach(): void {\n this.lastAttachAt = new Date().toISOString();\n }\n\n recordDetach(): void {\n this.lastDetachAt = new Date().toISOString();\n }\n\n getLastAttachAt(): string | null {\n return this.lastAttachAt;\n }\n\n getLastDetachAt(): string | null {\n return this.lastDetachAt;\n }\n\n recordAuthReject(): void {\n this.authRejectCount += 1;\n this.lastAuthRejectAt = new Date().toISOString();\n }\n\n getAuthRejects(): AuthRejectsSnapshot {\n return { count: this.authRejectCount, lastAt: this.lastAuthRejectAt };\n }\n}\n\n/**\n * Returns the `@modelcontextprotocol/sdk` version baked in at build time via\n * the `__MCP_SDK_VERSION__` define (see `tsdown.config.ts`). Returns `null`\n * when the define is absent (unbundled test runs) and the runtime fallback\n * below also fails — diagnostics must never throw.\n *\n * Earlier attempts resolved `@modelcontextprotocol/sdk/package.json` (not in\n * the SDK `exports` map → `ERR_PACKAGE_PATH_NOT_EXPORTED`) or the bare\n * `@modelcontextprotocol/sdk` main entry (also absent → `MODULE_NOT_FOUND`),\n * so both this fallback AND the build-time define silently produced `null` —\n * leaving `mcpVersion: null` in a real bundle (issue #361, observed live). The\n * fix resolves a subpath that IS exported (`./server/mcp.js`) and walks back to\n * the package root, in BOTH the build define and this fallback.\n *\n * Kept `async` for call-site compatibility (`Promise.all` at the caller); the\n * body is synchronous apart from the best-effort fallback.\n */\nexport async function readMcpSdkVersion(): Promise<string | null> {\n // Primary: build-time define (bare identifier, substituted by tsdown).\n if (typeof __MCP_SDK_VERSION__ === 'string' && __MCP_SDK_VERSION__.length > 0) {\n return __MCP_SDK_VERSION__;\n }\n // Fallback for unbundled runs (the define never ran): resolve an EXPORTED\n // subpath (`./server/mcp.js`) and read the sibling package.json by path —\n // bypassing the `exports` gate that blocks both the `/package.json` subpath\n // and the bare main entry.\n try {\n const { createRequire } = await import('node:module');\n const req = createRequire(import.meta.url);\n const entry = req.resolve('@modelcontextprotocol/sdk/server/mcp.js');\n const marker = '@modelcontextprotocol/sdk';\n const root = entry.slice(0, entry.indexOf(marker) + marker.length);\n const { readFileSync } = await import('node:fs');\n const raw = readFileSync(`${root}/package.json`, 'utf8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n return typeof parsed.version === 'string' ? parsed.version : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Returns the `@ait-co/devtools` package version injected at build time via\n * the `__VERSION__` define. Returns `null` when the global is absent (e.g. in\n * some test environments that skip the build step).\n */\nexport function readDevtoolsVersion(): string | null {\n // `__VERSION__` is a bare identifier replaced at build time by the tsdown\n // `define` (see `tsdown.config.ts`) — the SAME mechanism `debug-server.ts`\n // and `server.ts` use for the MCP server `version`. It must be referenced as\n // a bare identifier, not `globalThis.__VERSION__`: `define` only substitutes\n // the bare token, so the property access always read `undefined` and this\n // function always returned `null` in a real bundle (issue #361). The\n // `typeof` guard keeps it null-safe in unbundled test runs where the define\n // never ran.\n return typeof __VERSION__ === 'string' && __VERSION__.length > 0 ? __VERSION__ : null;\n}\n\n/**\n * Derives the next recommended action from a completed diagnostics snapshot.\n *\n * Branch rules (evaluated in priority order):\n * 0. tunnel.droppedAt non-null → restart (permanent tunnel drop — highest priority)\n * 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)\n * 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)\n * 2a. authRejects.count > 0 AND pages empty → build_attach_url (relay TOTP 거부 관측 — QR 재스캔\n * 또는 target-side `at` 전달 확인. 일반 rule 2보다 구체적이므로 먼저 평가 — issue #467)\n * 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)\n * 3. pages has entry + crashDetectedAt non-null → build_attach_url (re-attach after crash)\n * 4. otherwise → null (session looks healthy)\n *\n * Pure — does not throw; receives the final assembled snapshot fields.\n *\n * SECRET-HANDLING: the auth-reject reason string carries only the count and\n * timestamp from {@link AuthRejectsSnapshot} — never a URL, code, or secret.\n */\nexport function computeNextRecommendedAction(\n tunnel: DiagnosticsTunnelInfo,\n pages: ListPagesResult | null,\n env: McpEnvironment,\n authRejects: AuthRejectsSnapshot | null = null,\n): NextRecommendedAction | null {\n // Rule 0: permanent tunnel drop — highest priority, beats crash / empty-pages rules.\n // droppedAt is set by the health probe after exhausting all reissue attempts.\n if (tunnel.droppedAt != null) {\n return {\n tool: 'restart',\n reason:\n `tunnel permanently dropped at ${tunnel.droppedAt} after ${tunnel.reissueAttempts} reissue attempt(s) — ` +\n 'restart the MCP server (npx @ait-co/devtools devtools-mcp)',\n };\n }\n\n // Rule 1: tunnel is down.\n if (!tunnel.up) {\n // Rule 1b: local-target (mock env) runs without a relay tunnel by design —\n // tunnel.up === false is the expected steady state. Instead of recommending\n // a server restart, guide the agent to wait for the page to load.\n if (!isRelayEnv(env)) {\n // Only surface wait_for_page when no page is attached yet; once a page\n // attaches the session is healthy and null is the correct return value.\n if (pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) {\n return {\n tool: 'wait_for_page',\n reason:\n 'local Chromium spawn 직후 — 페이지 로드를 기다리거나 list_pages를 재호출하세요 ' +\n '(local 모드는 tunnel이 없는 게 정상입니다)',\n };\n }\n // Page already attached or crash detected — fall through to other rules.\n } else {\n // Rule 1 (relay env): tunnel must be up for relay to work — restart.\n return {\n tool: 'restart',\n reason: 'tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart',\n };\n }\n }\n\n // Rule 2a (issue #467): auth rejections observed while no page is attached —\n // the phone DID reach the relay but its TOTP verification failed. Without\n // this rule the generic rule 2 (\"call build_attach_url\") hides the rejection\n // and the diagnosis runs the wrong way (\"the phone never arrived\").\n if (authRejects !== null && authRejects.count > 0 && pages !== null && pages.pages.length === 0) {\n return {\n tool: 'build_attach_url',\n reason:\n `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? 'unknown'}) — ` +\n 'QR을 다시 스캔해 새 코드로 attach하세요(코드는 ~3분마다 만료). 반복되면 폰 페이지 URL에 ' +\n 'at 파라미터가 전달되는지(target-side TOTP 전달 경로)를 확인하세요',\n };\n }\n\n // Rule 2: tunnel up but no pages attached in relay env → start attach.\n if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) {\n return {\n tool: 'build_attach_url',\n reason: 'tunnel ready, no pages attached — call build_attach_url to generate the attach QR',\n };\n }\n\n // Rule 3: crash detected — need to re-attach.\n if (pages !== null && pages.crashDetectedAt !== null) {\n return {\n tool: 'build_attach_url',\n reason: `page crashed at ${pages.crashDetectedAt} — call build_attach_url to re-attach`,\n };\n }\n\n // Rule 4: session looks healthy.\n return null;\n}\n\n/** Input for `getDiagnostics`. */\nexport interface GetDiagnosticsInput {\n /** Current tunnel status (from the server's live `getTunnelStatus()`). */\n tunnel: TunnelStatus;\n /**\n * CDP connection used to call `list_pages` — may be absent in edge cases\n * (e.g. called from the dev-mode server which has no CDP connection).\n */\n connection?: CdpConnection;\n /**\n * Resolved MCP environment (`mock` | `relay-dev` | `relay-live` |\n * `relay-mobile`). Caller obtains via `resolveEnvironment()`.\n */\n env: McpEnvironment;\n /** Human-readable reason for the env decision. */\n envReason: string;\n /** Diagnostics collector for errors / attach events. */\n collector: DiagnosticsCollector;\n /** Lock-file reader — injected so tests can override without touching the FS. */\n readLock: () => import('./server-lock.js').LockData | null;\n /** Maximum number of recent errors to include (default 10). */\n recentErrorsLimit?: number;\n /** Optional async resolver for the MCP SDK version. */\n getMcpVersion?: () => Promise<string | null>;\n /**\n * Injectable parent-alive check for testability.\n * Defaults to `() => isPidAlive(process.ppid)` in production.\n */\n checkParentAlive?: () => boolean;\n /**\n * PID of the cloudflared child process — obtained from `QuickTunnel.childPid`\n * and written to the lock file via `LockHandle.updateTunnelChildPid`.\n *\n * FIX 2 (issue #571): when this PID is known, `getDiagnostics` performs a\n * live `isPidAlive(tunnelChildPid)` check and overrides `tunnel.up = false`\n * if the child is dead, preventing the cached `up: true` from being reported\n * as truth when the cloudflared process has already exited.\n */\n tunnelChildPid?: number | null;\n}\n\n/**\n * Builds the `get_debug_status` response. Pure — does not throw; missing data\n * fields are `null`. Async because `readMcpSdkVersion` needs `import()`.\n *\n * SECRET-HANDLING:\n * - `recentErrors` messages are already redacted by `recordError` (via\n * `redactErrorMessage`). No additional redaction needed here.\n * - `tunnel.wssUrl` is a public cloudflared hostname — not a secret.\n * - Lock file data contains only pid + startedAt + wssUrl — no secrets.\n */\nexport async function getDiagnostics(input: GetDiagnosticsInput): Promise<DiagnosticsResult> {\n const {\n tunnel,\n connection,\n env,\n envReason,\n collector,\n readLock: readLockFn,\n recentErrorsLimit = 10,\n getMcpVersion = readMcpSdkVersion,\n checkParentAlive = () => isPidAlive(process.ppid),\n tunnelChildPid,\n } = input;\n\n const [mcpVersion, devtoolsVersion] = await Promise.all([\n getMcpVersion(),\n Promise.resolve(readDevtoolsVersion()),\n ]);\n\n // Read lock file for serverLockHolder + tunnel pid/startedAt.\n const lockData = readLockFn();\n const serverLockHolder: DiagnosticsLockHolder | null = lockData\n ? { pid: lockData.pid, startedAt: lockData.startedAt, wssUrl: lockData.wssUrl }\n : null;\n\n // FIX 2 (issue #571): if the cloudflared child PID is known, perform a live\n // probe to detect child death even when the cached `tunnel.up` is still true.\n // This prevents the 2d17h zombie scenario where the process died but the cache\n // was never invalidated.\n //\n // Source priority: explicit `tunnelChildPid` arg (in-memory, always current) →\n // lock file's `tunnelChildPid` (populated by FIX 3 via onTunnelChildPid) →\n // null (no probe). The lock-file fallback ensures the check fires even when the\n // handler didn't pass the in-memory PID explicitly (issue #572 review).\n const effectiveTunnelChildPid = tunnelChildPid ?? lockData?.tunnelChildPid ?? null;\n let effectiveUp = tunnel.up;\n if (\n tunnel.up &&\n typeof effectiveTunnelChildPid === 'number' &&\n effectiveTunnelChildPid !== null &&\n !isPidAlive(effectiveTunnelChildPid)\n ) {\n effectiveUp = false;\n }\n\n const tunnelInfo: DiagnosticsTunnelInfo = {\n up: effectiveUp,\n wssUrl: tunnel.wssUrl,\n pid: lockData?.pid ?? null,\n startedAt: lockData?.startedAt ?? null,\n droppedAt: tunnel.droppedAt ?? null,\n reissueAttempts: tunnel.reissueAttempts ?? 0,\n };\n\n // list_pages — non-fatal; null on any error.\n // Refresh from relay first (#551 — stale cache causes pages:0 / wrong\n // nextRecommendedAction even when a target is attached). Same best-effort\n // pattern as the list_pages handler: errors are silently ignored so the\n // caller always gets *something* back, even when the relay is unreachable.\n let pages: ListPagesResult | null = null;\n if (connection !== undefined) {\n try {\n await connection.refreshTargets?.();\n } catch {\n // Ignore refresh errors — continue with cached state.\n }\n try {\n pages = listPages(connection, tunnel);\n } catch {\n // Ignore — pages stays null.\n }\n }\n\n const limit = Math.min(Math.max(1, recentErrorsLimit), 50);\n const recentErrors = collector.getRecentErrors(limit);\n\n // Issue #467: surface relay auth rejections. One synthetic summary entry\n // (not N entries — rejections can be frequent) so \"recentErrors: []\" can be\n // read as \"no attach attempts\" without hiding silent 401s.\n // SECRET-HANDLING: message carries only count + timestamp.\n const authRejects = collector.getAuthRejects();\n if (authRejects.count > 0) {\n recentErrors.push({\n timestamp: authRejects.lastAt ?? new Date().toISOString(),\n message: `WS upgrade auth-rejected (${authRejects.count} times, last ${authRejects.lastAt ?? 'unknown'})`,\n category: 'auth',\n });\n }\n\n const nextRecommendedAction = computeNextRecommendedAction(tunnelInfo, pages, env, authRejects);\n\n return {\n mcpVersion,\n devtoolsVersion,\n tunnel: tunnelInfo,\n pages,\n lastAttachAt: collector.getLastAttachAt(),\n lastDetachAt: collector.getLastDetachAt(),\n recentErrors,\n authRejects,\n environment: {\n kind: env,\n env: toLegacyEnv(env),\n reason: envReason,\n liveGuardActive: isLiveRelayEnv(env),\n },\n serverLockHolder,\n process: {\n pid: process.pid,\n ppid: process.ppid,\n parentAlive: checkParentAlive(),\n },\n nextRecommendedAction,\n };\n}\n","/**\n * cloudflared quick tunnel + attach banner for the debug-mode MCP server.\n *\n * On spawn, the debug server opens an accountless `*.trycloudflare.com` quick\n * tunnel to the local Chii relay so the phone can attach over a public wss URL,\n * then prints a unicode half-block QR + attach instructions. When TOTP auth is\n * enabled (`AIT_DEBUG_TOTP_SECRET` is set), the QR encodes only the base relay\n * URL — the TOTP code (`at=`) is NOT included because it rotates every 30 s\n * and would be stale by the time a human scans. The in-app deep-link builder\n * splices the live code at attach time.\n *\n * Tunnel health probe (`TunnelHealthProbe`):\n * After the tunnel is up, a periodic HTTP HEAD probe hits the tunnel's\n * `https://` URL every `probeIntervalMs` (default 60 s). Two consecutive\n * failures trigger a reissue attempt (spawn a new cloudflared quick tunnel\n * and redirect traffic). After `MAX_REISSUE_ATTEMPTS` (3) consecutive\n * reissue failures, the probe gives up and marks the tunnel permanently\n * dropped — `tunnelStatus.up` becomes false with `droppedAt` set. The caller\n * should surface this to the agent so the user knows to restart the server.\n *\n * SECRET-HANDLING: The TOTP secret and computed code values MUST NOT appear\n * in any output from this module.\n *\n * Node-only: spawns the cloudflared binary and writes to stdout/stderr.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { bin, install, Tunnel } from 'cloudflared';\nimport type { TunnelStatus } from './tools.js';\n\n/** Generates a 32-byte hex attach token shown as a pairing hint (relay-side validation is a later phase). */\nexport function generateAttachToken(): string {\n return randomBytes(32).toString('hex');\n}\n\nexport interface QuickTunnel {\n /** Public `https://*.trycloudflare.com` URL the tunnel exposes. */\n url: string;\n /** Same host as `wss://` — the relay endpoint the phone attaches to. */\n wssUrl: string;\n /**\n * PID of the cloudflared child process. Present once the tunnel is up.\n * Safe to surface in diagnostics (plain integer — not a secret).\n */\n childPid?: number;\n /**\n * Register a callback to be invoked when the cloudflared child exits\n * unexpectedly (i.e. NOT due to our own `stop()` call). The caller\n * (`startTunnelHealthProbe`) uses this to immediately trigger reissue\n * without waiting for the next probe interval.\n *\n * Only one callback can be registered at a time; calling this again\n * replaces the previous one.\n */\n onUnexpectedExit(cb: (code: number | null) => void): void;\n stop(): void;\n}\n\n/** Ensures the cloudflared binary is installed (downloads + caches on first run). */\nasync function ensureCloudflaredBin(): Promise<void> {\n const { existsSync } = await import('node:fs');\n if (!existsSync(bin)) {\n await install(bin);\n }\n}\n\n/**\n * Opens a cloudflared quick tunnel to the local relay port and resolves once\n * the public URL is assigned.\n *\n * FIX 1 (issue #571): after URL resolution the returned `QuickTunnel` object\n * watches the cloudflared child process for unexpected exits and calls any\n * registered `onUnexpectedExit` callback so the health probe can immediately\n * trigger reissue instead of waiting for the next poll interval.\n */\nexport async function startQuickTunnel(localPort: number): Promise<QuickTunnel> {\n await ensureCloudflaredBin();\n\n const tunnel = Tunnel.quick(`http://127.0.0.1:${localPort}`);\n\n const url = await new Promise<string>((resolve, reject) => {\n const onUrl = (assigned: string) => {\n cleanup();\n resolve(assigned);\n };\n const onError = (err: Error) => {\n cleanup();\n reject(err);\n };\n const onExit = (code: number | null) => {\n cleanup();\n reject(new Error(`cloudflared exited before assigning a URL (code ${code})`));\n };\n const cleanup = () => {\n tunnel.off('url', onUrl);\n tunnel.off('error', onError);\n tunnel.off('exit', onExit);\n };\n tunnel.once('url', onUrl);\n tunnel.once('error', onError);\n tunnel.once('exit', onExit);\n });\n\n // FIX 1: watch for unexpected child death AFTER URL is resolved.\n // `intentionalStop` guards against triggering a reissue when we called stop() ourselves\n // (cloudflared exits on SIGINT from tunnel.stop(), which would otherwise look like a crash).\n let intentionalStop = false;\n let unexpectedExitCb: ((code: number | null) => void) | null = null;\n\n tunnel.once('exit', (code: number | null) => {\n if (!intentionalStop && unexpectedExitCb !== null) {\n unexpectedExitCb(code);\n }\n });\n\n return {\n url,\n wssUrl: url.replace(/^https/, 'wss'),\n childPid: (tunnel.process as { pid?: number } | null)?.pid,\n onUnexpectedExit(cb: (code: number | null) => void): void {\n unexpectedExitCb = cb;\n },\n stop(): void {\n intentionalStop = true;\n tunnel.stop();\n },\n };\n}\n\nexport interface AttachBannerInput {\n wssUrl: string;\n /**\n * Whether TOTP auth is enabled on the relay (`AIT_DEBUG_TOTP_SECRET` is set).\n *\n * When `true`, the banner notes that a rotating code (`at=`) will be\n * appended to attach URLs at call time — the code is NOT printed here\n * because it rotates every 30 s and would be stale in seconds.\n */\n totpEnabled: boolean;\n}\n\n/**\n * Renders a pure unicode half-block QR string for the given text.\n *\n * Uses `qrcode` (Node full lib) to get the raw bit matrix, then encodes every\n * two vertical modules into a single half-block character:\n * - both dark → `█`\n * - top only → `▀`\n * - bottom only → `▄`\n * - both light → ` ` (space)\n *\n * The output contains **zero ANSI escape codes**, so it renders correctly in\n * every surface (terminal, VS Code, JetBrains, web) and can be scanned by a\n * phone camera when shown verbatim in an agent response.\n *\n * Shared by `renderAttachBanner` (relay wssUrl QR) and the `build_attach_url`\n * MCP tool response (attach deep-link QR).\n */\nexport async function renderQr(text: string): Promise<string> {\n // Dynamic import mirrors the cloudflared/qrcode-terminal precedent: keeps the\n // dependency out of the module graph when the function is not called.\n const { default: QRCode } = await import('qrcode');\n const qr = QRCode.create(text, { errorCorrectionLevel: 'M' });\n const size: number = qr.modules.size;\n const data: Uint8Array = qr.modules.data as Uint8Array;\n\n const isDark = (x: number, y: number): boolean => {\n if (x < 0 || y < 0 || x >= size || y >= size) return false;\n return data[y * size + x] === 1;\n };\n\n const QUIET = 1;\n const lines: string[] = [];\n for (let y = -QUIET; y < size + QUIET; y += 2) {\n let line = '';\n for (let x = -QUIET; x < size + QUIET; x++) {\n const top = isDark(x, y);\n const bot = isDark(x, y + 1);\n line += top && bot ? '█' : top ? '▀' : bot ? '▄' : ' ';\n }\n lines.push(line);\n }\n return `${lines.join('\\n')}\\n`;\n}\n\n/**\n * Renders the attach banner (relay URL + ASCII QR) as a string.\n *\n * The QR encodes the base `wssUrl` only. When `totpEnabled` is true, a note\n * is added that attach URLs generated by `build_attach_url` will include a\n * live TOTP code (`at=`) appended at call time.\n *\n * SECRET-HANDLING: no secret value, TOTP code, or intermediate value is\n * included in this output.\n */\nexport async function renderAttachBanner(input: AttachBannerInput): Promise<string> {\n // The QR encodes only the relay wssUrl — no token or code. This is safe\n // because the relay gate enforces the code at WS upgrade time anyway; the\n // QR is just for locating the relay, not for bypassing auth.\n const qr = await renderQr(input.wssUrl);\n\n const authNote = input.totpEnabled\n ? ' auth: TOTP enabled — attach URLs include a rotating code (at=).'\n : ' auth: none (set AIT_DEBUG_TOTP_SECRET to enable TOTP).';\n\n return [\n '',\n 'AIT debug — attach a mini-app to this session',\n '',\n ` relay (wss): ${input.wssUrl}`,\n authNote,\n '',\n ' Use build_attach_url to generate a deep link with the current TOTP code.',\n ' Scan the QR to locate the relay (open the dog-food URL separately with',\n ' ?debug=1&relay=<wss>&at=<code> or use the build_attach_url tool):',\n '',\n qr,\n ].join('\\n');\n}\n\n/** Prints the attach banner to stderr (stdout is the MCP stdio channel). */\nexport async function printAttachBanner(input: AttachBannerInput): Promise<void> {\n const banner = await renderAttachBanner(input);\n process.stderr.write(`${banner}\\n`);\n}\n\n/* -------------------------------------------------------------------------- */\n/* TunnelHealthProbe — periodic health check + auto-reissue */\n/* -------------------------------------------------------------------------- */\n\n/** Maximum consecutive reissue attempts before the probe gives up. */\nexport const MAX_REISSUE_ATTEMPTS = 3;\n\n/**\n * Probes `https://` URL with an HTTP HEAD request.\n * Returns `true` when the server responds (any HTTP status), `false` on\n * network error or timeout.\n *\n * We treat any HTTP response (including 4xx/5xx) as \"tunnel alive\" because\n * cloudflared itself responds to the HEAD — if the tunnel process died, the\n * request fails at the network level rather than returning a status code.\n *\n * @param httpsUrl - The `https://` tunnel URL to probe.\n * @param timeoutMs - Abort timeout in ms. Default 10 000.\n */\nexport async function probeTunnel(httpsUrl: string, timeoutMs = 10_000): Promise<boolean> {\n const { default: https } = await import('node:https');\n return new Promise<boolean>((resolve) => {\n const url = new URL(httpsUrl);\n const timer = setTimeout(() => {\n req.destroy();\n resolve(false);\n }, timeoutMs);\n\n const req = https.request(\n { hostname: url.hostname, port: 443, path: url.pathname || '/', method: 'HEAD' },\n (_res) => {\n clearTimeout(timer);\n _res.resume(); // drain response body to free socket\n resolve(true);\n },\n );\n req.on('error', () => {\n clearTimeout(timer);\n resolve(false);\n });\n req.end();\n });\n}\n\nexport interface TunnelHealthProbeOptions {\n /**\n * Interval in ms between health probes. Default 60 000 (60 s).\n * Use a smaller value in tests.\n */\n probeIntervalMs?: number;\n /**\n * How many consecutive probe failures to tolerate before triggering a\n * reissue. Default 2 (so one transient network hiccup is forgiven).\n */\n failuresBeforeReissue?: number;\n /**\n * Callback invoked after a successful reissue. The caller (debug-server)\n * uses this to update `tunnelStatus` and reprint the attach banner with the\n * new `wssUrl`.\n */\n onReissue: (newTunnel: QuickTunnel) => void;\n /**\n * Callback invoked when the probe permanently gives up (all reissue attempts\n * exhausted). The caller should mark `tunnelStatus.up = false` and surface\n * the error to the agent / user.\n */\n onPermanentDrop: (droppedAt: string) => void;\n /**\n * Optional stderr-compatible logger. Default `process.stderr.write`.\n * Injected in tests to avoid real I/O.\n */\n log?: (msg: string) => void;\n /**\n * Optional probe function override (for tests — avoids real HTTP requests).\n */\n probe?: (httpsUrl: string) => Promise<boolean>;\n /**\n * Optional tunnel spawner override (for tests — avoids real cloudflared).\n */\n spawnTunnel?: (localPort: number) => Promise<QuickTunnel>;\n}\n\n/**\n * Starts a periodic health probe for a cloudflared quick tunnel.\n *\n * Every `probeIntervalMs` the probe sends an HTTP HEAD request to the tunnel's\n * `https://` URL. When `failuresBeforeReissue` consecutive failures are\n * detected, it attempts to spawn a new tunnel (up to `MAX_REISSUE_ATTEMPTS`\n * times). On success the caller is notified via `onReissue`; on permanent\n * failure via `onPermanentDrop`.\n *\n * FIX 1 (issue #571): the probe also subscribes to each tunnel's\n * `onUnexpectedExit` callback to detect child death *immediately* instead of\n * waiting for the next probe interval (which could be 60 s away).\n *\n * @returns `stop` — call during server shutdown to clear the probe interval.\n */\nexport function startTunnelHealthProbe(\n initialTunnel: QuickTunnel,\n localPort: number,\n options: TunnelHealthProbeOptions,\n): { stop(): void } {\n const {\n probeIntervalMs = 60_000,\n failuresBeforeReissue = 2,\n onReissue,\n onPermanentDrop,\n log = (msg: string) => process.stderr.write(msg),\n probe = probeTunnel,\n spawnTunnel = startQuickTunnel,\n } = options;\n\n let currentTunnel = initialTunnel;\n let consecutiveFailures = 0;\n let reissueAttempts = 0;\n let stopped = false;\n\n // FIX 1: shared reissue-or-drop logic — called both from the periodic\n // interval (after failuresBeforeReissue consecutive probe misses) and from\n // the child-exit handler (immediately on unexpected process death).\n const doReissueOrDrop = async (): Promise<void> => {\n if (stopped) return;\n\n reissueAttempts += 1;\n if (reissueAttempts > MAX_REISSUE_ATTEMPTS) {\n // Already exhausted — do not log again.\n return;\n }\n\n log(\n `[ait-debug] tunnel drop detected — reissuing (attempt ${reissueAttempts}/${MAX_REISSUE_ATTEMPTS})\\n`,\n );\n\n try {\n const newTunnel = await spawnTunnel(localPort);\n // Stop the old tunnel process to free system resources.\n try {\n currentTunnel.stop();\n } catch {\n // Ignore stop errors — the process may already be dead.\n }\n currentTunnel = newTunnel;\n consecutiveFailures = 0;\n // FIX 1: arm child-exit watcher on the newly spawned tunnel too.\n armChildExitWatch(newTunnel);\n log(`[ait-debug] tunnel reissued — new relay: ${newTunnel.wssUrl}\\n`);\n onReissue(newTunnel);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n log(`[ait-debug] tunnel reissue attempt ${reissueAttempts} failed: ${message}\\n`);\n\n if (reissueAttempts >= MAX_REISSUE_ATTEMPTS) {\n clearInterval(handle);\n stopped = true;\n const droppedAt = new Date().toISOString();\n log(\n `[ait-debug] tunnel permanently dropped after ${MAX_REISSUE_ATTEMPTS} reissue attempts — ` +\n 'restart the debug server to continue (npx @ait-co/devtools devtools-mcp).\\n',\n );\n onPermanentDrop(droppedAt);\n }\n }\n };\n\n // FIX 1: register exit watcher on a QuickTunnel so unexpected child death\n // immediately kicks off reissue without waiting for the probe interval.\n const armChildExitWatch = (t: QuickTunnel): void => {\n t.onUnexpectedExit((code) => {\n if (stopped) return;\n log(\n `[ait-debug] cloudflared child exited unexpectedly (code=${code}) — triggering immediate reissue\\n`,\n );\n // Set failures to threshold so the next interval probe also sees a clean\n // state; the actual reissue happens immediately below.\n consecutiveFailures = failuresBeforeReissue;\n void doReissueOrDrop();\n });\n };\n\n // Arm the watcher on the initial tunnel.\n armChildExitWatch(initialTunnel);\n\n const handle = setInterval(() => {\n void (async () => {\n if (stopped) return;\n\n const httpsUrl = currentTunnel.url;\n const alive = await probe(httpsUrl);\n\n if (alive) {\n // Tunnel responded — reset failure counter.\n if (consecutiveFailures > 0) {\n log('[ait-debug] tunnel health probe: tunnel recovered\\n');\n }\n consecutiveFailures = 0;\n reissueAttempts = 0;\n return;\n }\n\n consecutiveFailures += 1;\n log(\n `[ait-debug] tunnel health probe: failure ${consecutiveFailures}/${failuresBeforeReissue} (url=${httpsUrl})\\n`,\n );\n\n if (consecutiveFailures < failuresBeforeReissue) {\n // Tolerate transient failures — wait for the next interval.\n return;\n }\n\n // Threshold reached — attempt reissue.\n await doReissueOrDrop();\n })();\n }, probeIntervalMs);\n\n return {\n stop() {\n stopped = true;\n clearInterval(handle);\n },\n };\n}\n\n/**\n * Builds a `TunnelStatus` snapshot that includes drop state.\n *\n * Convenience helper for callers (debug-server) that maintain a mutable\n * `tunnelStatus` object — keeps the shape construction in one place.\n */\nexport function makeTunnelStatus(\n up: boolean,\n wssUrl: string | null,\n droppedAt: string | null = null,\n reissueAttempts = 0,\n): TunnelStatus {\n return { up, wssUrl, droppedAt, reissueAttempts };\n}\n","/**\n * @ait-co/devtools debug-mode MCP server (stdio).\n *\n * Lets an AI coding agent attach to a running mini-app (real Toss WebView, or a\n * browser in dev mode) and read its console/network/DOM/screenshot over CDP plus\n * the AIT.* domain, without a human watching a phone. Transport is CDP-via-Chii:\n * a local Chii relay on an OS-assigned port (default 0) exposed through a\n * cloudflared quick tunnel; the phone attaches over the public wss URL.\n *\n * AI host --stdio--> this server --CDP client WS--> Chii relay :<OS-port>\n * ^-- target WS -- phone\n *\n * Port 0 (default): the OS picks a free ephemeral port on every startup.\n * This prevents EADDRINUSE when a stale cloudflared child (orphaned after\n * SIGKILL, PPID 1) still holds a fixed port — which previously caused the MCP\n * handshake to fail with -32000. With port 0 any orphaned cloudflared is\n * harmless; the new relay always gets a fresh port.\n *\n * Best-effort child cleanup: SIGINT/SIGTERM/SIGHUP handlers call shutdown() to\n * stop cloudflared and the relay. uncaughtException/unhandledRejection also\n * call shutdown() before exit. SIGKILL cannot be intercepted by Node, so\n * cloudflared orphans from SIGKILL remain (port 0 makes them harmless). Users\n * can clean up manually: `pkill -f 'cloudflared.*trycloudflare'`.\n *\n * The tool layer reads from an injectable `CdpConnection` (CDP) and `AitSource`\n * (AIT.*), so every tool is unit-testable with a fake (no phone). This module\n * wires the live pieces (relay + tunnel + production connection); the phone\n * roundtrip is fully wired and pending only on-device acceptance.\n *\n * Dynamic tool registration (issue #208):\n * The server advertises `listChanged: true` so MCP clients can subscribe to\n * `notifications/tools/list_changed`. Before any page attaches, only bootstrap\n * tools (`build_attach_url`, `list_pages`) are listed. Once a target appears,\n * the full attach-dependent tool set is added and a `list_changed` notification\n * is sent — without requiring a session restart. `runDebugServer` and\n * `runLocalDebugServer` start a polling watcher that detects the 0→N target\n * transition and calls `server.sendToolListChanged()`.\n *\n * Note: `src/mcp/server.ts` (dev mode, HTTP mock-state) is NOT subject to this\n * model — it has no attach concept and always exposes the full tool surface.\n *\n * Node-only.\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { startMaxAgeWatchdog, startParentWatcher } from '../shared/parent-watcher.js';\nimport { ChiiAitSource } from './ait-chii-source.js';\nimport type { AitSource } from './ait-source.js';\nimport type { CdpConnection } from './cdp-connection.js';\nimport { ChiiCdpConnection } from './chii-connection.js';\nimport { startChiiRelay } from './chii-relay.js';\nimport { buildDeepLinkAttachUrl, buildLauncherAttachUrl } from './deeplink.js';\nimport { AutoDevtoolsOpener, buildChiiInspectorUrl } from './devtools-opener.js';\nimport { wrapEnvelope } from './envelope.js';\nimport {\n deriveEnvironment,\n getLiveIntent,\n type McpEnvironment,\n type RelayOrigin,\n setLiveIntent,\n} from './environment.js';\nimport {\n classifyToolError,\n liveGuardError,\n mcpError,\n pageCrashError,\n pageMissingError,\n relayDisconnectError,\n sdkAbsentError,\n tierRejectionError,\n} from './errors.js';\nimport { LocalCdpConnection } from './local-connection.js';\nimport { launchChromium } from './local-launcher.js';\nimport { logError, logInfo, logWarn } from './log.js';\nimport {\n type DashboardState,\n type QrHttpServer,\n type QrHttpServerOptions,\n startQrHttpServer,\n} from './qr-http-server.js';\nimport { loadRelaySecretReadOnly } from './relay-secret-store.js';\nimport { acquireLock, readServerLock } from './server-lock.js';\nimport {\n BOOTSTRAP_TOOL_NAMES,\n buildAttachUrl,\n callSdk,\n canOpenBrowser,\n DEBUG_TOOL_DEFINITIONS,\n type DiagnosticsCollector,\n evaluate,\n filterToolsByEnvironment,\n getDiagnostics,\n getDomDocument,\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n getToolAvailability,\n InMemoryDiagnosticsCollector,\n isAitToolName,\n isDebugToolName,\n isToolAvailableIn,\n listConsoleMessages,\n listExceptions,\n listNetworkRequests,\n listPages,\n measureSafeArea,\n openQrInBrowser,\n type TunnelStatus,\n takeScreenshot,\n takeSnapshot,\n} from './tools.js';\nimport {\n assertRelayAuthConfigured,\n buildRelayVerifyAuth,\n generateTotp,\n RELAY_VERIFY_SKEW_STEPS,\n} from './totp.js';\n\nexport { startMaxAgeWatchdog, startParentWatcher } from '../shared/parent-watcher.js';\n\nimport {\n generateAttachToken,\n makeTunnelStatus,\n printAttachBanner,\n type QuickTunnel,\n renderQr,\n startQuickTunnel,\n startTunnelHealthProbe,\n} from './tunnel.js';\n\n/**\n * Maximum age (ms) of a page's `lastSeenAt` before it is treated as a ghost\n * and excluded from the `wait_for_attach` short-circuit in `build_attach_url`\n * (issue #610).\n *\n * Rationale: the env-2 relay is owned by the dev server (unplugin), so every\n * `dev:phone:cdp` restart produces a new quick-tunnel. The old relay goes\n * offline immediately, but the daemon's warm `ChiiCdpConnection` still lists\n * the last-seen target — its `lastSeenAt` freezes at the moment the old relay\n * died. A 5-minute threshold is large enough to be invisible in normal usage\n * (active CDP sessions see a message every few seconds) while being small\n * enough to catch a relay that went down before the daemon was re-entered.\n *\n * Injectable for tests via {@link DebugServerDeps.stalePageThresholdMs}.\n */\nexport const RELAY_SANDBOX_STALE_PAGE_MS = 5 * 60 * 1_000; // 5 minutes\n\n/**\n * Predicate used by `build_attach_url`'s `wait_for_attach` loop to decide\n * whether the relay-sandbox connection has a genuinely fresh page attached.\n *\n * Stale-ghost gating (issue #610): when the dev server restarts with a new\n * quick-tunnel, the warm `ChiiCdpConnection` still lists the last-seen target\n * but its `lastSeenAt` is frozen. A page whose `lastSeenAt` exceeds\n * `stalePageThresholdMs` is a ghost from the dead relay — it must NOT\n * short-circuit `wait_for_attach`.\n *\n * Rules:\n * - `pages.length === 0` → false (nothing attached).\n * - Connection has no `getLastSeenAt` (test fakes, local-browser) → falls back\n * to `pages.length > 0` (regression-safe).\n * - `seenMs === null` → treat as fresh (no CDP message received yet, first\n * message pending — the connection is alive).\n * - Otherwise: at least one page must satisfy `nowMs - seenMs <=\n * stalePageThresholdMs`.\n *\n * Exported for unit testing.\n */\nexport function isSandboxPageFresh(\n pages: ReadonlyArray<{ id: string }>,\n getLastSeenAt: ((id: string) => number | null) | null,\n nowMs: number,\n stalePageThresholdMs: number,\n): boolean {\n if (pages.length === 0) return false;\n if (getLastSeenAt === null) return true;\n return pages.some((p) => {\n const seenMs = getLastSeenAt(p.id);\n // null = no CDP message yet (fresh attach, first message pending) → fresh.\n if (seenMs === null) return true;\n return nowMs - seenMs <= stalePageThresholdMs;\n });\n}\n\n/**\n * Parses `_deploymentId` from the query string of a scheme URL.\n *\n * Returns `null` when the param is absent or empty — callers treat that as\n * \"no deploymentId filter; match on presence only\" and fall back to the\n * original `attachedPages.length > 0` condition.\n *\n * SECRET-HANDLING: deploymentId is a public identifier and may appear in\n * debug output. Never confuse it with TOTP secrets or relay tunnel URLs.\n */\nexport function extractDeploymentId(schemeUrl: string): string | null {\n try {\n // scheme URLs like `intoss-private://host?_deploymentId=xxx` are not\n // parseable by `new URL()` in all environments, so we extract the query\n // string manually.\n const qIndex = schemeUrl.indexOf('?');\n if (qIndex === -1) return null;\n const params = new URLSearchParams(schemeUrl.slice(qIndex + 1));\n const id = params.get('_deploymentId');\n return id && id.length > 0 ? id : null;\n } catch {\n return null;\n }\n}\n\n/**\n * The result of a `start_debug` mode switch (issue #348). Reported back to the\n * agent so it knows the active mode, whether the LIVE guard is armed, and the\n * suggested next step — all without a Claude Code restart or MCP re-handshake.\n */\nexport interface ModeSwitchReport {\n /** The mode now active after the switch. */\n mode: StartDebugMode;\n /** Derived `McpEnvironment` for the now-active connection. */\n environment: McpEnvironment;\n /** Kind of the now-active connection. */\n kind: 'relay' | 'local';\n /** `true` when the relay-live LIVE side-effect guard is now armed. */\n liveGuardActive: boolean;\n /** Human-readable next-step hint for the agent. */\n nextStep: string;\n}\n\n/**\n * The four canonical `start_debug` modes (issues #382, #378, #398 — each names\n * the four-environment fidelity ladder rung it attaches to):\n *\n * - `local-browser` → env 1: desktop Chromium with the MOCK SDK + local CDP\n * attach. Side-effect tools (call_sdk/evaluate) run unguarded\n * against the mock; nothing touches a real device or real users.\n * No prerequisites — the default, always-available environment.\n *\n * - `relay-sandbox` → env 2: real-device PWA (real WebKit engine + mock SDK)\n * over an EXTERNAL CDP relay that the unplugin (`tunnel: { cdp:\n * true }`) already brought up. liveIntent off — dev-intent, never\n * LIVE. Output env `relay-mobile`. Prerequisite: `AIT_RELAY_BASE_URL`\n * set to the unplugin's relay base URL. The MCP only attaches a\n * CDP client; it does NOT start (or stop) that relay.\n *\n * - `relay-staging` → env 3: real-device Toss WebView dog-food build with the\n * REAL SDK over the intoss-private relay. liveIntent off.\n * Prerequisite: deployed dog-food bundle + device cold-loaded via\n * intoss-private deep-link/QR relay injection.\n *\n * - `relay-live` → env 4: REVIEW-PASSED production runtime with the REAL SDK\n * over the intoss relay. liveIntent on (requires `confirm: true`).\n * Read-only debugging: call_sdk/evaluate require confirm per call.\n *\n * `relay-staging` and `relay-live` share ONE physical relay connection\n * (`relay-intoss`, see {@link FamilyKey}) — wire-identical, distinguished only by\n * the `liveIntent` bit — so switching staging↔live never re-boots the tunnel.\n *\n * Normalization is handled by `normalizeStartDebugMode`.\n */\nexport type StartDebugMode = 'local-browser' | 'relay-sandbox' | 'relay-staging' | 'relay-live';\n\n/**\n * Returns `true` when the mode routes to a relay connection (`relay-sandbox`,\n * `relay-staging`, or `relay-live`). `relay-sandbox` is an external-PWA relay;\n * `relay-staging`/`relay-live` are intoss-private relays — but all three surface\n * the Tier B / relay-only tool set.\n */\nexport function isRelayMode(mode: StartDebugMode): boolean {\n return mode === 'relay-sandbox' || mode === 'relay-staging' || mode === 'relay-live';\n}\n\n/**\n * Attach URL components — stored in the run functions instead of a finished\n * URL string so that `getDashboardState` can RE-MINT a fresh TOTP code on\n * every call (Defect 1: baked codes expire → relay 401 reason:'auth').\n *\n * `kind: 'launcher'` = env 2 (launcher PWA QR, `buildLauncherAttachUrl`).\n * `kind: 'scheme'` = env 3/4 (intoss-private deep-link, `buildDeepLinkAttachUrl`).\n *\n * SECRET-HANDLING: these components contain tunnel/scheme hosts. They are\n * NEVER logged. The TOTP code is minted fresh at call time via `rebuildAttachUrl`\n * and rides inside the assembled URL's `at=` param only.\n */\nexport type AttachUrlParts =\n | { kind: 'launcher'; tunnelHttpUrl: string; wssUrl: string; appName?: string }\n | { kind: 'scheme'; schemeUrl: string; wssUrl: string };\n\n/**\n * Owns the two coexisting CDP connections (local + relay) and the `active`\n * pointer that `start_debug` flips (issue #348 — DUAL-CONNECTION-COEXIST).\n *\n * The MCP `Server` + transport are created once; the request handlers read the\n * connection through `active`, so swapping the pointer underneath is invisible\n * to the MCP host (no re-handshake, no restart). Inactive infra is left warm —\n * teardown happens only at process exit (see the unified shutdown in the run\n * functions), which is what preserves a warm attach across mode switches.\n */\nexport interface ConnectionRouter {\n /** The connection the request handlers must read this instant. */\n readonly active: CdpConnection;\n /**\n * Relay origin of the currently-active family (issue #378) — the\n * discriminator that distinguishes the env-2 external-PWA relay\n * (`'external-pwa'` → `relay-mobile`) from the intoss-private relay\n * (`'intoss-webview'` → `relay-dev`). `undefined` for a local (mock) active\n * connection, or for a single-connection router that has no family concept.\n * Threaded into `deriveEnvironment` so the output env can tell the two\n * `kind: 'relay'` families apart.\n */\n readonly activeRelayOrigin?: RelayOrigin;\n /**\n * Switches the active connection to the family for `mode`, lazily booting\n * that family's infra if needed, re-arming the attach watcher, and emitting\n * `tools/list_changed`. Sets `liveIntent` (true only for `relay-live`).\n *\n * `projectRoot` (issue #396) is the per-debug-session mini-app project root\n * supplied by `start_debug`. When switching into a relay family the router\n * loads the relay TOTP secret read-only from `<projectRoot>/.ait_relay` into\n * `process.env` (via `loadRelaySecretReadOnly`) BEFORE the relay boots, so the\n * `assertRelayAuthConfigured()` / `buildRelayVerifyAuth()` at the boot site see\n * it. The daemon never mints — it only reads. Ignored for the local family.\n *\n * Rejects (without swapping) when a swap is already in flight, or when\n * `relay-live` is requested without `confirm: true`.\n */\n switchMode(\n mode: StartDebugMode,\n confirm: boolean,\n projectRoot?: string,\n ): Promise<ModeSwitchReport>;\n}\n\n/** Live infra the connection reads tunnel status from. */\nexport interface DebugServerDeps {\n connection: CdpConnection;\n /**\n * Dual-connection router (issue #348). When provided, the request handlers\n * read the live connection through `router.active` and `start_debug` calls\n * `router.switchMode()`. When omitted (the dominant test path), a trivial\n * router pinned to `deps.connection` is synthesized and `start_debug` reports\n * that dynamic switching is unavailable — back-compat with every existing\n * single-connection test.\n */\n router?: ConnectionRouter;\n /** AIT.* domain source — forwarded over the same Chii channel in production. */\n aitSource: AitSource;\n /** Returns current tunnel status (URL changes per spawn). */\n getTunnelStatus(): TunnelStatus;\n /**\n * Maximum time in ms to wait for a page to attach when `wait_for_attach=true`.\n * Default 60 000 ms. Exposed for testing so tests can use a small value without\n * fake timers (which conflict with MCP SDK's own timeouts).\n */\n waitForAttachTimeoutMs?: number;\n /**\n * 로컬 QR HTTP 서버 — `build_attach_url` tool이 브라우저로 열 HTTP URL을 제공.\n * 없으면 text QR fallback으로만 동작 (GUI 없는 환경 호환).\n */\n qrHttpServer?: QrHttpServer;\n /**\n * Resolves the current MCP environment (`mock` | `relay-dev` | `relay-live`).\n * Used by `tools/list` to filter Tier A/B tools and by Tier C tools (e.g.\n * `measure_safe_area`) to label the `source` provenance field.\n *\n * Optional — defaults (issue #348) to deriving the env from the *active*\n * connection's `kind` + the module-level `liveIntent` bit\n * (`deriveEnvironment(router.active.kind, getLiveIntent())`). No URL sniffing\n * or precedence chain. Tests inject a fake to pin a precise env.\n */\n getEnvironment?: () => McpEnvironment;\n /** Resolves the reason for the current env decision (for logs). */\n getEnvironmentReason?: () => string;\n /**\n * Diagnostics collector — records server-side errors, attach/detach events,\n * and surfaces them via `get_debug_status`. When omitted a no-op collector is\n * used (backwards-compatible with existing tests that don't inject one).\n */\n diagnosticsCollector?: DiagnosticsCollector;\n /**\n * Hex-encoded TOTP secret for `build_attach_url` auto-splice.\n *\n * When set, `build_attach_url` generates a fresh TOTP code on every call and\n * splices it as `at=<code>` into the returned `attachUrl`. The response also\n * includes a `totp` field with `ttlSeconds` and `expiresAt` so callers know\n * when to re-invoke.\n *\n * SECRET-HANDLING: this value is captured in a closure and MUST NOT be logged\n * or included in any output other than the `at=` param inside `attachUrl`.\n *\n * Tests inject a dummy hex string or omit it. Production uses the late-bound\n * {@link getTotpSecret} variant instead (read at call time) — see below.\n */\n totpSecret?: string;\n /**\n * Late-bound variant of {@link totpSecret}: read AT `build_attach_url` CALL\n * TIME rather than captured once at server construction (issue #396).\n *\n * Why late-bound: since #396 the relay TOTP secret lives in a project-local\n * `.ait_relay` file loaded read-only into `process.env.AIT_DEBUG_TOTP_SECRET`\n * by `switchMode` BEFORE a relay family boots — which is AFTER the daemon\n * (and thus `createDebugServer`) already started. Capturing the secret at\n * construction would read an empty value on the all-lazy daemon, so\n * `build_attach_url` would emit a QR with no `at=` code and every attach would\n * be rejected by the relay gate. Reading it at call time makes the loaded\n * secret visible.\n *\n * When omitted, `createDebugServer` falls back to the captured {@link totpSecret}\n * (preserving all existing test behavior).\n *\n * SECRET-HANDLING: same as {@link totpSecret} — the returned value MUST NOT be\n * logged or included in any output other than the `at=` param inside `attachUrl`.\n *\n * Production: passed as `() => process.env.AIT_DEBUG_TOTP_SECRET` by the three\n * run functions.\n */\n getTotpSecret?: () => string | undefined;\n /**\n * `build_attach_url` 핸들러가 attach URL 컴포넌트를 확정한 직후 호출되는 콜백.\n * run 함수에서 `lastAttachParts` 갱신 + `qrHttpServer.notifyStateChange()` 트리거에 사용.\n * 테스트에서는 주입하지 않아도 되고, 미주입 시 no-op.\n *\n * 완성된 URL 문자열이 아니라 컴포넌트를 전달하는 이유: `getDashboardState`가\n * 호출될 때마다 최신 TOTP 코드를 freshly mint해 QR을 갱신하기 위함이다.\n * 정적 URL에 구워진 코드는 ~3분 후 만료(RELAY_VERIFY_SKEW_STEPS=6 기준) → relay 401 reason:'auth' (Defect 1).\n * rebuildAttachUrl()이 매 호출 시 generateTotp(secret)를 새로 계산한다.\n *\n * SECRET-HANDLING: 컴포넌트 안의 tunnel/scheme host와 wssUrl은 NEVER 로그 출력.\n * TOTP 코드는 rebuildAttachUrl() 내부에서만 mint되며 attachUrl의 at= param 안에만 노출.\n */\n onAttachUrlBuilt?: (parts: AttachUrlParts) => void;\n /**\n * Returns the cloudflared child PID of the currently active tunnel.\n * When provided, `get_debug_status` passes it to `getDiagnostics` as the\n * live in-memory source for FIX 2 (issue #571) — the PID is also picked up\n * from the lock file as a fallback, but the in-memory value is preferred as\n * it stays current across reissues.\n *\n * Production: injected by the run functions via a captured `activeTunnelChildPid`\n * variable that is updated whenever `onTunnelChildPid` fires (including reissues).\n * Tests inject a controlled value. Omitting it (old tests) falls back to the\n * lock-file path in `getDiagnostics`.\n */\n getTunnelChildPid?: () => number | null | undefined;\n /**\n * Lock-file reader — injected here so tests can control the lock data without\n * touching the filesystem. Defaults to `readServerLock` (the real file).\n *\n * This also enables handler-level tests for FIX 2 (issue #572 review) that\n * need to simulate a stale lock with a dead tunnelChildPid.\n */\n readLock?: () => import('./server-lock.js').LockData | null;\n /**\n * Maximum age (ms) of a page's `lastSeenAt` before it is treated as a\n * ghost and excluded from `wait_for_attach` short-circuit logic (issue #610).\n *\n * Default: {@link RELAY_SANDBOX_STALE_PAGE_MS} (5 minutes).\n * Injectable for tests so they can use a small value without fake timers.\n */\n stalePageThresholdMs?: number;\n /**\n * Monotonic clock for stale-page checks (issue #610). Defaults to\n * `Date.now`. Injectable for tests so they can freeze time without fake\n * timers (which conflict with MCP SDK's own timeouts).\n */\n nowMs?: () => number;\n}\n\n/**\n * Waits for the first target matching `filterFn` to attach, using the\n * event-driven `waitForFirstTarget()` when the connection supports it\n * (interface-optional member, present on `ChiiCdpConnection`), or falling\n * back to a polling loop for connections that don't implement it (test fakes,\n * `LocalCdpConnection`).\n *\n * This eliminates the polling-only race that previously caused `wait_for_attach`\n * to resolve before the relay had observed the first inbound CDP message from\n * the phone.\n *\n * @param connection - The CDP connection (production or fake).\n * @param filterFn - Resolves when this predicate is satisfied.\n * @param timeoutMs - Maximum wait time in ms.\n * @param pollIntervalMs - Fallback poll interval for connections without waitForFirstTarget.\n */\nfunction waitForAttachWithEvents(\n connection: CdpConnection,\n filterFn: (targets: ReturnType<CdpConnection['listTargets']>) => boolean,\n timeoutMs: number,\n pollIntervalMs = 1_000,\n): Promise<ReturnType<CdpConnection['listTargets']>> {\n // Use event-driven path when available (CdpConnection.waitForFirstTarget is\n // optional; ChiiCdpConnection implements it, LocalCdpConnection and test fakes do not).\n if (connection.waitForFirstTarget) {\n return connection.waitForFirstTarget(filterFn, timeoutMs, pollIntervalMs);\n }\n // Generic fallback for connections without waitForFirstTarget\n // (test fakes, LocalCdpConnection — they don't emit 'target:attached').\n return new Promise<ReturnType<CdpConnection['listTargets']>>((resolve, reject) => {\n const deadline = Date.now() + timeoutMs;\n let settled = false;\n const poll = setInterval(() => {\n const targets = connection.listTargets();\n if (filterFn(targets)) {\n settled = true;\n clearInterval(poll);\n resolve(targets);\n } else if (Date.now() >= deadline) {\n settled = true;\n clearInterval(poll);\n reject(new Error(`waitForAttachWithEvents: 타임아웃 (${timeoutMs}ms)`));\n }\n }, pollIntervalMs);\n // Also check immediately.\n const targets = connection.listTargets();\n if (!settled && filterFn(targets)) {\n settled = true;\n clearInterval(poll);\n resolve(targets);\n }\n });\n}\n\n/**\n * Builds the debug-mode MCP server around an injected CDP connection + AIT\n * source + tunnel status getter. Pure wiring — does not start a relay or\n * tunnel, which is what makes the tool surface unit-testable.\n *\n * `tools/list` is two-tiered (issue #208):\n * - bootstrap (always): `build_attach_url`, `list_pages`\n * - attach-dependent (after `connection.listTargets().length > 0`): all others\n *\n * `CallTool` is NOT tiered — hidden tools still execute (attach errors surface\n * naturally via `enableDomains`). The tier only controls visibility.\n */\nexport function createDebugServer(deps: DebugServerDeps): Server {\n const {\n connection,\n router: routerDep,\n aitSource,\n getTunnelStatus,\n waitForAttachTimeoutMs = 60_000,\n qrHttpServer,\n getEnvironment: getEnvDep,\n getEnvironmentReason: getEnvReasonDep,\n diagnosticsCollector: collectorDep,\n totpSecret,\n onAttachUrlBuilt,\n getTunnelChildPid,\n readLock: readLockDep,\n stalePageThresholdMs = RELAY_SANDBOX_STALE_PAGE_MS,\n nowMs = () => Date.now(),\n } = deps;\n\n // Late-bound TOTP secret accessor (issue #396): production injects\n // `getTotpSecret` so the secret is read from env at `build_attach_url` call\n // time (after switchMode's project-local .ait_relay load). When absent we fall\n // back to the captured `totpSecret` — preserving existing test behavior.\n // SECRET-HANDLING: the returned value is used only for the at= code, never logged.\n const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);\n\n // Lock-file reader — defaults to the real file reader; injected by tests to\n // control lock data without touching the filesystem. Also used by the\n // get_debug_status handler to forward lock data into getDiagnostics for the\n // FIX 2 lock-file fallback (issue #572 review).\n const readLockFn = readLockDep ?? readServerLock;\n\n // Dual-connection router (issue #348). Production passes a real router that\n // holds both the local + relay connections and flips `active` on\n // `start_debug`. Tests (and any single-connection caller) omit it — we\n // synthesize a trivial router pinned to `deps.connection` whose `switchMode`\n // reports that dynamic switching is unavailable. Either way the handlers read\n // the live connection through `router.active`, so per-call snapshots are\n // uniform.\n const router: ConnectionRouter = routerDep ?? makeSingleConnectionRouter(connection);\n\n // Env SSoT (issue #348) — derived, not detected: `mock` vs `relay-*` is free\n // from the ACTIVE connection's `kind`; `relay-dev` vs `relay-live` is the\n // module-level `liveIntent` bit. No URL sniffing, no precedence chain. Tests\n // inject `getEnvironment`/`getEnvironmentReason` to pin a precise env.\n const resolveEnvironment: () => McpEnvironment =\n getEnvDep ??\n (() => deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin));\n const resolveEnvironmentReason: () => string =\n getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},liveIntent=${getLiveIntent()}`);\n\n // Diagnostics collector — production uses an `InMemoryDiagnosticsCollector`;\n // tests may inject a no-op or fake. A no-op is created lazily when none\n // is supplied so existing tests that don't inject one continue to work.\n const collector: DiagnosticsCollector = collectorDep ?? new InMemoryDiagnosticsCollector();\n\n const server = new Server(\n { name: 'ait-debug', version: __VERSION__ },\n // listChanged: true — the server emits notifications/tools/list_changed when\n // a page attaches (0→N target transition), promoted attach-dependent tools.\n { capabilities: { tools: { listChanged: true } } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, () => {\n // Per-request snapshot of the active connection (issue #348). `kind` is\n // authoritative even before any target attaches, so bootstrap visibility\n // (e.g. Tier B `build_attach_url`) is correct from the first `tools/list`.\n const conn = router.active;\n const env = resolveEnvironment();\n const attached = conn.listTargets().length > 0;\n // Tier A/B filter first (env), then bootstrap tier (attach state).\n const envFiltered = filterToolsByEnvironment(DEBUG_TOOL_DEFINITIONS, env);\n const tools = attached\n ? envFiltered.map((tool) => ({ ...tool }))\n : envFiltered\n .filter((tool) => BOOTSTRAP_TOOL_NAMES.has(tool.name))\n .map((tool) => ({ ...tool }));\n return { tools };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n if (!isDebugToolName(name)) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${name}` }],\n isError: true,\n };\n }\n\n // PER-CALL SNAPSHOT (issue #348). Capture the active connection exactly\n // once at handler entry and use ONLY `conn` for the rest of this call.\n // `start_debug` may flip `router.active` mid-flight (and other concurrent\n // requests too); re-reading `router.active` after an `await` would race the\n // swap. This is the hard-constraint that keeps a switch from corrupting an\n // in-flight tool call.\n const conn = router.active;\n\n // start_debug — single entry to switch families (local ↔ relay) without a\n // Claude Code restart or MCP re-handshake. Always callable (Tier C /\n // bootstrap), so it is handled before the env-mismatch guard below.\n if (name === 'start_debug') {\n const rawMode = request.params.arguments?.mode;\n const mode = normalizeStartDebugMode(rawMode);\n if (mode === null) {\n return mcpError(\n 'start_debug: mode가 올바르지 않습니다. ' +\n \"'local-browser' | 'relay-sandbox' | 'relay-staging' | 'relay-live' 중 하나를 전달하세요.\",\n );\n }\n const confirm = request.params.arguments?.confirm === true;\n // Per-session project root (issue #396): the daemon reads the relay TOTP\n // secret read-only from <projectRoot>/.ait_relay when switching to a relay\n // family. Optional — omitted for local, or when the operator exported the\n // secret. SECRET-HANDLING: projectRoot is a path, never the secret value.\n const rawProjectRoot = request.params.arguments?.projectRoot;\n const projectRoot = typeof rawProjectRoot === 'string' ? rawProjectRoot : undefined;\n try {\n const report = await router.switchMode(mode, confirm, projectRoot);\n return jsonResult(report);\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n // PER-CALL SNAPSHOT of the derived environment (issue #348 / #354 regression\n // fix). Capture `env` + `envReason` exactly once, right after the start_debug\n // branch (so this call sees the post-switch env when it *is* a switch) and\n // before the first `await`. Every site below reuses these locals instead of\n // re-calling `resolveEnvironment()`/`resolveEnvironmentReason()` — those\n // closures re-read `router.active.kind` + `getLiveIntent()` live, so a\n // concurrent `start_debug` swap mid-await would otherwise corrupt the env\n // stamped into this call's envelope / provenance label.\n const env = resolveEnvironment();\n const envReason = resolveEnvironmentReason();\n // Tier A/B env-mismatch guard (RFC #277). Tier C tools pass through.\n // We return a tool-result error (not an MCP protocol error) so the client\n // sees a structured isError + reason text rather than a thrown exception —\n // the MCP SDK still surfaces this as an error to the agent, but with the\n // explanatory `data.reason` payload preserved as text.\n if (!isToolAvailableIn(name, env)) {\n const requiredEnv = getToolAvailability(name) ?? 'unknown';\n // Log structured (no secrets — only stable env strings + tool name).\n logWarn('tool.error', {\n tool: name,\n errorKind: 'tier-filter',\n requiredEnv,\n currentEnv: env,\n envReason,\n });\n return tierRejectionError(name, requiredEnv, env, envReason);\n }\n\n // AIT.* tools are served by the AIT source. In production it rides the same\n // Chii websocket as CDP, so the connection must be attached first; the AIT\n // source's sendCommand rejects with a clear message if no page is attached.\n if (isAitToolName(name)) {\n try {\n await conn.enableDomains();\n switch (name) {\n case 'AIT.getSdkCallHistory':\n return jsonResult(await getSdkCallHistory(aitSource));\n case 'AIT.getMockState':\n return jsonResult(await getMockState(aitSource));\n case 'AIT.getOperationalEnvironment':\n return jsonResult(await getOperationalEnvironment(aitSource));\n default:\n return unknownTool(name);\n }\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n // get_debug_status is a bootstrap tool — it works before any page attaches\n // and must not require enableDomains. It aggregates all server state into a\n // single response so the agent can diagnose session problems in one call.\n if (name === 'get_debug_status') {\n try {\n const rawLimit = request.params.arguments?.recent_errors_limit;\n const recentErrorsLimit = typeof rawLimit === 'number' && rawLimit > 0 ? rawLimit : 10;\n const result = await getDiagnostics({\n tunnel: getTunnelStatus(),\n connection: conn,\n env,\n envReason,\n collector,\n readLock: readLockFn,\n recentErrorsLimit,\n tunnelChildPid: getTunnelChildPid?.() ?? undefined,\n });\n const attached = conn.listTargets().length > 0;\n return envelopeResult(result, name, env, attached);\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n // build_attach_url is pure synthesis (relay URL → deep link).\n // It works before any page attaches, so it must not require enableDomains.\n //\n // ENV BRANCH: env 2 (relay-mobile) builds a launcher PWA QR using\n // AIT_TUNNEL_BASE_URL; env 3/4 (relay-dev/live) use the existing\n // intoss-private scheme URL path. Both branches converge below at the\n // shared QR rendering path (attachUrl + relayUrl + totp + authorityWarning).\n if (name === 'build_attach_url') {\n const waitForAttach = request.params.arguments?.wait_for_attach === true;\n // open_in_browser 옵션은 삭제됨 (#553) — 항상 대시보드 오픈을 시도한다.\n // 구버전 클라이언트가 open_in_browser 키를 보내도 에러 없이 무시됨(하위호환).\n // selfdebug: opt-in launcher self-target mode (#543). Only valid in env 2.\n const selfdebug = request.params.arguments?.selfdebug === true;\n\n // wait_timeout_seconds: per-call override of the default wait timeout.\n // Clamp to 1–600 s; invalid values (0, negative, NaN, non-number) silently fall back to default.\n const rawWaitTimeout = request.params.arguments?.wait_timeout_seconds;\n const callTimeoutMs = (() => {\n if (typeof rawWaitTimeout !== 'number' || !Number.isFinite(rawWaitTimeout)) {\n return waitForAttachTimeoutMs;\n }\n const clamped = Math.max(1, Math.min(600, rawWaitTimeout));\n // rawWaitTimeout ≤ 0 falls back to default (clamp produces 1 but intent is \"invalid\").\n if (rawWaitTimeout <= 0) return waitForAttachTimeoutMs;\n return Math.round(clamped) * 1000;\n })();\n\n // Guard: selfdebug is a launcher-only feature — reject early for env 3/4\n // so the caller gets a clear diagnostic instead of silently ignoring the flag.\n if (selfdebug && env !== 'relay-mobile') {\n return mcpError(\n 'build_attach_url: selfdebug=true는 env 2 / relay-sandbox 전용 기능입니다. ' +\n '현재 환경(env 3/4)에서는 launcher가 없어 self-target 모드를 지원하지 않습니다. ' +\n 'launcher self-target이 필요하다면 relay-sandbox 모드로 재시작하세요.',\n );\n }\n\n // ── relay-mobile branch (env 2 — launcher PWA QR) ─────────────────────\n if (env === 'relay-mobile') {\n // SECRET-HANDLING: AIT_TUNNEL_BASE_URL carries the app tunnel host —\n // NEVER echo the value in error messages or logs.\n // (#424) env wins; .ait_urls is the fallback when env is unset.\n const rawBuildProjectRoot = request.params.arguments?.projectRoot;\n const buildProjectRoot =\n typeof rawBuildProjectRoot === 'string' ? rawBuildProjectRoot : undefined;\n const envTunnelUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? '';\n let tunnelHttpUrl = envTunnelUrl;\n if (tunnelHttpUrl === '' && buildProjectRoot !== undefined) {\n const { readRelayUrls } = await import('./relay-url-store.js');\n const stored = await readRelayUrls({ projectRoot: buildProjectRoot });\n tunnelHttpUrl = stored?.tunnelBaseUrl ?? '';\n }\n if (tunnelHttpUrl === '') {\n return mcpError(\n 'build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. ' +\n 'dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. ' +\n '자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.',\n );\n }\n const tunnelStatus = getTunnelStatus();\n if (!tunnelStatus.up || tunnelStatus.wssUrl === null) {\n return mcpError(\n 'build_attach_url(mobile): relay wssUrl이 아직 설정되지 않았습니다. ' +\n 'unplugin tunnel:{cdp:true}가 relay를 완전히 기동할 때까지 잠시 후 다시 시도하세요.',\n );\n }\n\n // SECRET-HANDLING: the secret is used only to compute a code that is\n // spliced as at= in the attachUrl — never logged or returned separately.\n // Read at call time (#396) so the project-local .ait_relay secret loaded\n // by switchMode is visible.\n const secret = getTotpSecret();\n // Defense-in-depth (#452): relay mode requires TOTP auth — if the secret\n // is missing, fail-closed rather than issuing an unauthenticated attach URL.\n // assertRelayAuthConfigured() at bootRelayFamily/bootExternalRelayFamily\n // already gates relay startup, so this branch is dead code in normal\n // operation; the guard exists to prevent accidental TOTP bypass if the\n // boot-time guard is ever bypassed or removed.\n // SECRET-HANDLING: error message names the requirement only — never the\n // secret value, length, or any derived fragment.\n if (secret === undefined || secret === '') {\n return mcpError(\n 'build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. ' +\n 'relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.',\n );\n }\n let totpCode: string | undefined;\n let totpMeta: { enabled: true; ttlSeconds: number; expiresAt: string } | undefined;\n {\n const now = Date.now();\n totpCode = generateTotp(secret, now);\n const STEP_SECONDS = 30;\n // expiresAt reflects the relay gate's RELAY_VERIFY_SKEW_STEPS=6 window (#490):\n // the code is valid for ~3 min (180 s) from issuance.\n const expiresAtMs = now + RELAY_VERIFY_SKEW_STEPS * STEP_SECONDS * 1000;\n totpMeta = {\n enabled: true,\n ttlSeconds: RELAY_VERIFY_SKEW_STEPS * STEP_SECONDS,\n expiresAt: new Date(expiresAtMs).toISOString(),\n };\n }\n\n // Read the app name from projectRoot/package.json to add to the launcher\n // deep-link (#498). Failure to read is silently ignored (fail-open).\n let launcherAppName: string | undefined;\n if (buildProjectRoot !== undefined) {\n try {\n const { readFileSync } = await import('node:fs');\n const pkgRaw = readFileSync(`${buildProjectRoot}/package.json`, 'utf8');\n const pkg = JSON.parse(pkgRaw) as Record<string, unknown>;\n const rawName = typeof pkg.name === 'string' ? pkg.name : '';\n // Strip npm scope prefix (@scope/foo → foo).\n const stripped = rawName.includes('/')\n ? rawName.slice(rawName.indexOf('/') + 1)\n : rawName;\n launcherAppName = stripped.trim() || undefined;\n } catch {\n // Silently ignore — fail-open (existing behavior unchanged).\n }\n }\n\n // SECRET-HANDLING: attachUrl encodes tunnelHttpUrl and wssUrl inside\n // the QR payload only — not logged or returned as standalone fields.\n const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, {\n name: launcherAppName,\n ...(selfdebug ? { selfdebug: true } : {}),\n });\n // Notify dashboard with components (not a finished URL) so getDashboardState\n // re-mints a fresh TOTP code on every SSE push/reload (Defect 1).\n // SECRET-HANDLING: components contain tunnel host — never logged.\n onAttachUrlBuilt?.({\n kind: 'launcher',\n tunnelHttpUrl,\n wssUrl: tunnelStatus.wssUrl,\n appName: launcherAppName,\n });\n const relayUrl = tunnelStatus.wssUrl;\n const authorityWarning: string | undefined = undefined; // no scheme authority for launcher\n const totp = totpMeta;\n\n // In mobile mode, deploymentId filtering is not applicable —\n // the launcher attach is not tied to a specific bundle deployment.\n // match on presence only — but with a stale-ghost guard (issue #610).\n //\n // env-2 relay is owned by the dev server (unplugin): every `dev:phone:cdp`\n // restart produces a new quick-tunnel and the old relay goes offline. The\n // warm ChiiCdpConnection still lists the last-seen target, but its\n // `lastSeenAt` is frozen. A ghost page (lastSeenAt older than\n // `stalePageThresholdMs`) must NOT short-circuit wait_for_attach — doing\n // so would return a dead WS target immediately and block all MCP tools\n // until the daemon is fully restarted. We treat such pages as \"not yet\n // attached\" so the caller polls until a genuinely fresh page appears.\n //\n // Duck-type guard: only `ChiiCdpConnection` exposes `getTargetLastSeenAt`.\n // For connections that don't (test fakes, local), the check is skipped\n // (pages.length > 0 is the full predicate) — regression-safe.\n // Duck-type: only ChiiCdpConnection exposes getTargetLastSeenAt.\n // Cast via unknown to satisfy the compiler — we guard with typeof first.\n const connAsAny = conn as unknown as {\n getTargetLastSeenAt?: (id: string) => number | null;\n };\n const getLastSeenAt =\n typeof connAsAny.getTargetLastSeenAt === 'function'\n ? (id: string) => (connAsAny.getTargetLastSeenAt as (id: string) => number | null)(id)\n : null;\n const callNow = nowMs();\n // Delegate to the exported pure function so it can be unit-tested (#610).\n const isMatchingPage = (pages: ReturnType<CdpConnection['listTargets']>): boolean =>\n isSandboxPageFresh(pages, getLastSeenAt, callNow, stalePageThresholdMs);\n const buildTimeoutError = (\n baseText: string,\n timeoutSec: number,\n observed: ReturnType<CdpConnection['listTargets']>,\n ): string => {\n const observedUrls = observed\n .slice(0, 3)\n .map((p) => p.url.slice(0, 80))\n .join(', ');\n const observedNote =\n observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : '';\n return (\n `${baseText}\\n\\nNo page attached within ${timeoutSec}s${observedNote} — ` +\n 'launcher QR을 폰 카메라로 스캔한 뒤 call list_pages를 다시 호출하세요.'\n );\n };\n\n // Fall through to the shared QR rendering path below.\n // (extracted into a local async IIFE so both branches can return from it)\n return await (async () => {\n const header =\n 'This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).';\n const warningPrefix = authorityWarning\n ? `⚠️ scheme_url 경고: ${authorityWarning}\\n\\n`\n : '';\n const guiAvailable = canOpenBrowser();\n\n if (!guiAvailable) {\n const headlessNote =\n 'GUI 환경이 감지되지 않았습니다 (headless/remote 환경). ' +\n '텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\\n\\n';\n const qrHeadless = await renderQr(attachUrl);\n const headlessText = `${warningPrefix}${headlessNote}${header}\\n${JSON.stringify({ attachUrl, relayUrl, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qrHeadless}`;\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: headlessText }] };\n }\n let attachedPagesHl: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPagesHl = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(headlessText, callTimeoutMs / 1000, attachedPagesHl),\n },\n ],\n isError: true,\n };\n }\n const pagesResultHl = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${headlessText}\\n\\n${JSON.stringify(pagesResultHl, null, 2)}`,\n },\n ],\n };\n }\n\n if (guiAvailable && qrHttpServer) {\n const httpUrl = qrHttpServer.buildAttachPageUrl(attachUrl);\n const pngUrl = `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`;\n const browserResult = await openQrInBrowser(httpUrl, pngUrl);\n if (browserResult.opened) {\n const retriedNote = browserResult.retried ? ' (1회 retry 후 성공)' : '';\n const openResult = {\n attempted: true,\n succeeded: true,\n ...(browserResult.retried ? { retried: true } : {}),\n };\n const shortText =\n `${warningPrefix}${header}\\n` +\n `${JSON.stringify({ relayUrl, openResult, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n` +\n `브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\\n` +\n `URL: ${browserResult.httpUrl}`;\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: shortText }] };\n }\n let attachedPages: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPages = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(shortText, callTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n const pagesResult = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${shortText}\\n\\n${JSON.stringify(pagesResult, null, 2)}`,\n },\n ],\n };\n }\n const openResult = {\n attempted: true,\n succeeded: false,\n failureReason: browserResult.error ?? '브라우저 실행 후보 모두 실패',\n pngUrl: browserResult.pngUrl,\n ...(browserResult.stderrSummary\n ? { stderrSummary: browserResult.stderrSummary }\n : {}),\n };\n const stderrNote = browserResult.stderrSummary\n ? `\\nstderr: ${browserResult.stderrSummary}`\n : '';\n const fallbackNote =\n `브라우저 자동 열기에 실패했습니다. ` +\n `다음 URL을 직접 브라우저에서 여세요:\\n${browserResult.httpUrl}\\n` +\n `또는 PNG로 받기: ${browserResult.pngUrl}` +\n stderrNote +\n '\\n\\n';\n const qr = await renderQr(attachUrl);\n const baseText = `${warningPrefix}${fallbackNote}${header}\\n${JSON.stringify({ attachUrl, relayUrl, openResult, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qr}`;\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: baseText }] };\n }\n let attachedPagesFb: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPagesFb = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, callTimeoutMs / 1000, attachedPagesFb),\n },\n ],\n isError: true,\n };\n }\n const pagesResultFb = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${baseText}\\n\\n${JSON.stringify(pagesResultFb, null, 2)}`,\n },\n ],\n };\n }\n\n const qr = await renderQr(attachUrl);\n const baseText = `${warningPrefix}${header}\\n${JSON.stringify({ attachUrl, relayUrl, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qr}`;\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: baseText }] };\n }\n let attachedPages: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPages = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, callTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n const pagesResult = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${baseText}\\n\\n${JSON.stringify(pagesResult, null, 2)}`,\n },\n ],\n };\n })();\n }\n // ── end relay-mobile branch ────────────────────────────────────────────\n\n // ── relay-dev / relay-live branch (env 3/4 — intoss-private QR) ───────\n const schemeUrl = request.params.arguments?.scheme_url;\n if (typeof schemeUrl !== 'string' || schemeUrl === '') {\n return mcpError(\n 'build_attach_url: scheme_url이 비어 있습니다. ' +\n '`ait deploy --scheme-only`가 출력하는 intoss-private:// URL을 인자로 전달하세요. ' +\n '환경 2(mobile)라면 scheme_url 대신 AIT_TUNNEL_BASE_URL을 설정하세요.',\n );\n }\n\n // Parse _deploymentId from scheme_url to filter stale attached pages.\n // null → \"no filter; match on presence only\" (original behaviour preserved).\n const deploymentId = extractDeploymentId(schemeUrl);\n if (!deploymentId) {\n logInfo('tool.call', {\n tool: 'build_attach_url',\n msg: 'no _deploymentId in scheme_url; matching on presence only',\n });\n }\n\n /** Returns true when the page list satisfies the attach condition. */\n const isMatchingPage = (pages: ReturnType<CdpConnection['listTargets']>): boolean => {\n if (pages.length === 0) return false;\n if (deploymentId === null) return true;\n return pages.some((p) => p.url.includes(deploymentId));\n };\n\n /** Builds a timeout error message with diagnostic context. */\n const buildTimeoutError = (\n baseText: string,\n timeoutSec: number,\n observed: ReturnType<CdpConnection['listTargets']>,\n ): string => {\n const observedUrls = observed\n .slice(0, 3)\n .map((p) => p.url.slice(0, 80))\n .join(', ');\n const observedNote =\n observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : '';\n const deploymentNote = deploymentId ? ` matching deploymentId=${deploymentId}` : '';\n return (\n `${baseText}\\n\\nNo page${deploymentNote} attached within ${timeoutSec}s${observedNote} — ` +\n 'call list_pages to retry.'\n );\n };\n\n // Defense-in-depth (#452): relay-dev/live mode requires TOTP auth.\n // Read secret before entering try{} so we can return mcpError (not throw).\n // assertRelayAuthConfigured() at bootRelayFamily already gates relay startup,\n // so this is dead code in normal operation — the guard closes the fail-open\n // branch in buildAttachUrl if the boot-time guard is ever bypassed.\n // SECRET-HANDLING: error message names the requirement only.\n {\n const relaySecret = getTotpSecret();\n if (relaySecret === undefined || relaySecret === '') {\n return mcpError(\n 'build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. ' +\n 'relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.',\n );\n }\n }\n try {\n // SECRET-HANDLING: the secret is passed to buildAttachUrl only; it is\n // never logged or included in output other than the at= param in attachUrl.\n // Read at call time (#396) so the project-local .ait_relay secret loaded\n // by switchMode is visible.\n // Snapshot the tunnel once so we use a consistent wssUrl in both buildAttachUrl\n // and the onAttachUrlBuilt components (avoids a torn read if tunnel reissues mid-call).\n const tunnelForBuild = getTunnelStatus();\n const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(\n schemeUrl,\n tunnelForBuild,\n getTotpSecret(),\n );\n // Notify dashboard with components (not a finished URL) so getDashboardState\n // re-mints a fresh TOTP code on every SSE push/reload (Defect 1).\n // buildAttachUrl already throws on tunnel-down before this, so wssUrl is non-null.\n // SECRET-HANDLING: components contain scheme/wss host — never logged.\n if (tunnelForBuild.wssUrl !== null) {\n onAttachUrlBuilt?.({ kind: 'scheme', schemeUrl, wssUrl: tunnelForBuild.wssUrl });\n }\n\n // Prepend a non-fatal authority warning when the scheme URL host looks wrong.\n const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\\n\\n` : '';\n\n const header =\n 'This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).';\n\n // canOpenBrowser()를 한 번만 호출하여 이 요청 안에서 일관된 값을 사용한다.\n // mockReturnValueOnce 등 테스트 대역이 여러 번 호출로 소비되지 않도록.\n const guiAvailable = canOpenBrowser();\n\n // headless 환경 감지: GUI가 없는 경우 안내 후 text QR fallback.\n if (!guiAvailable) {\n const headlessNote =\n 'GUI 환경이 감지되지 않았습니다 (headless/remote 환경). ' +\n '텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\\n\\n';\n const qrHeadless = await renderQr(attachUrl);\n const headlessText = `${warningPrefix}${headlessNote}${header}\\n${JSON.stringify({ attachUrl, relayUrl, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qrHeadless}`;\n\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: headlessText }] };\n }\n\n // wait_for_attach + headless fallback\n let attachedPagesHl: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPagesHl = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(headlessText, callTimeoutMs / 1000, attachedPagesHl),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResultHl = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${headlessText}\\n\\n${JSON.stringify(pagesResultHl, null, 2)}`,\n },\n ],\n };\n }\n\n // Try to open QR in browser: GUI is available and the HTTP server is up.\n if (guiAvailable && qrHttpServer) {\n const httpUrl = qrHttpServer.buildAttachPageUrl(attachUrl);\n const pngUrl = `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`;\n\n const browserResult = await openQrInBrowser(httpUrl, pngUrl);\n\n if (browserResult.opened) {\n // Opened successfully — HTTP URL을 사용자에게 명시.\n // SECRET-HANDLING: attachUrl은 httpUrl query string 안에 있고, tool result에는 httpUrl만 노출.\n const retriedNote = browserResult.retried ? ' (1회 retry 후 성공)' : '';\n const openResult = {\n attempted: true,\n succeeded: true,\n ...(browserResult.retried ? { retried: true } : {}),\n };\n const shortText =\n `${warningPrefix}${header}\\n` +\n `${JSON.stringify({ relayUrl, openResult, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n` +\n `브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\\n` +\n `URL: ${browserResult.httpUrl}`;\n\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: shortText }] };\n }\n\n // wait_for_attach path (browser opened) — event-driven via waitForAttachWithEvents.\n let attachedPages: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPages = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(shortText, callTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResult = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${shortText}\\n\\n${JSON.stringify(pagesResult, null, 2)}`,\n },\n ],\n };\n }\n\n // Browser open failed — openResult 포함 구조화 에러 + URL 안내 + text QR fallback.\n const openResult = {\n attempted: true,\n succeeded: false,\n failureReason: browserResult.error ?? '브라우저 실행 후보 모두 실패',\n pngUrl: browserResult.pngUrl,\n ...(browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}),\n };\n const stderrNote = browserResult.stderrSummary\n ? `\\nstderr: ${browserResult.stderrSummary}`\n : '';\n const fallbackNote =\n `브라우저 자동 열기에 실패했습니다. ` +\n `다음 URL을 직접 브라우저에서 여세요:\\n` +\n `${browserResult.httpUrl}\\n` +\n `또는 PNG로 받기: ${browserResult.pngUrl}` +\n stderrNote +\n '\\n\\n';\n const qr = await renderQr(attachUrl);\n const baseText = `${warningPrefix}${fallbackNote}${header}\\n${JSON.stringify({ attachUrl, relayUrl, openResult, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qr}`;\n\n if (!waitForAttach) {\n return { content: [{ type: 'text' as const, text: baseText }] };\n }\n\n // wait_for_attach + fallback path — event-driven via waitForAttachWithEvents.\n let attachedPagesFb: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPagesFb = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, callTimeoutMs / 1000, attachedPagesFb),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResultFb = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${baseText}\\n\\n${JSON.stringify(pagesResultFb, null, 2)}`,\n },\n ],\n };\n }\n\n // No GUI available or no HTTP server: text QR fallback.\n const qr = await renderQr(attachUrl);\n const baseText = `${warningPrefix}${header}\\n${JSON.stringify({ attachUrl, relayUrl, ...(totp ? { totp } : {}) }, null, 2)}\\n\\n${qr}`;\n\n if (!waitForAttach) {\n return {\n content: [{ type: 'text' as const, text: baseText }],\n };\n }\n\n // wait_for_attach=true: event-driven via waitForAttachWithEvents.\n // enableDomains is NOT called here — listTargets is a buffered target list\n // read and does not require domain negotiation.\n // The deploymentId filter (parsed above) ensures we don't return a stale\n // page from a previous session — resolves only when an attached page's\n // URL contains the expected deploymentId.\n let attachedPages: ReturnType<CdpConnection['listTargets']> = [];\n try {\n attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);\n } catch {\n attachedPages = conn.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, callTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResult = listPages(conn, getTunnelStatus());\n return {\n content: [\n {\n type: 'text' as const,\n text: `${baseText}\\n\\n${JSON.stringify(pagesResult, null, 2)}`,\n },\n ],\n };\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n try {\n // Ensure CDP domains are enabled before reading. No-op once attached;\n // throws a clear message while no page is attached yet.\n await conn.enableDomains();\n } catch (err) {\n if (name === 'list_pages') {\n // list_pages is still useful pre-attach: report tunnel + empty pages.\n // Refresh from relay first so evicted-then-reattached targets are not\n // served as stale empty (#281 — stale cache diagnosis).\n try {\n await conn.refreshTargets?.();\n } catch {\n // Ignore refresh errors — still return cached state.\n }\n const pagesData = listPages(conn, getTunnelStatus());\n const attached = conn.listTargets().length > 0;\n return envelopeResult(pagesData, name, env, attached);\n }\n // 4상태 분류: page 미attach vs crash vs relay disconnect\n return classifyEnableDomainError(err, name);\n }\n\n try {\n switch (name) {\n case 'list_console_messages':\n return jsonResult(listConsoleMessages(conn));\n case 'list_exceptions': {\n const rawLimit = request.params.arguments?.limit;\n const limit = typeof rawLimit === 'number' && rawLimit > 0 ? rawLimit : 50;\n return jsonResult({ exceptions: listExceptions(conn, limit) });\n }\n case 'list_network_requests':\n return jsonResult(listNetworkRequests(conn));\n case 'list_pages': {\n // Refresh from relay so evict→reattach transitions are not served stale.\n try {\n await conn.refreshTargets?.();\n } catch {\n // Ignore refresh errors — still return cached state.\n }\n const listPagesData = listPages(conn, getTunnelStatus());\n const listPagesAttached = conn.listTargets().length > 0;\n return envelopeResult(listPagesData, name, env, listPagesAttached);\n }\n case 'get_dom_document':\n return jsonResult(await getDomDocument(conn));\n case 'take_snapshot':\n return jsonResult(await takeSnapshot(conn));\n case 'take_screenshot': {\n const shot = await takeScreenshot(conn);\n return {\n content: [{ type: 'image' as const, data: shot.data, mimeType: shot.mimeType }],\n };\n }\n case 'measure_safe_area': {\n // Pass the SNAPSHOT env to attach `source: 'mock' | 'relay'` to the\n // result (Tier C parity per RFC #277 — the same Runtime.evaluate probe\n // runs in both envs; only the provenance label differs). The label must\n // match the `conn` the probe actually ran on, so it reads the snapshot\n // `env` (entry-time, same as `conn`) — not a freshly re-derived env that\n // a concurrent swap could have moved.\n const safeAreaData = await measureSafeArea(conn, env);\n const safeAreaAttached = conn.listTargets().length > 0;\n return envelopeResult(safeAreaData, name, env, safeAreaAttached);\n }\n case 'evaluate': {\n const expression = request.params.arguments?.expression;\n if (typeof expression !== 'string' || expression === '') {\n return mcpError(\n 'evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.',\n );\n }\n // LIVE guard (issue #348, race fix #354). Evaluated at the side-effect\n // boundary with a SNAPSHOT `conn.kind` + a FRESH `getLiveIntent()` — not\n // the stale entry-time `env`. The side effect always runs on `conn`, so\n // the guard judges by `conn.kind`; reading `liveIntent` fresh closes the\n // false→true race where a concurrent `start_debug('relay-live')` arms\n // liveIntent while this call is parked on an `await`, after the stale\n // entry-time `env` was already computed as non-live. A stale `true`\n // bit stays inert against a local target (conn.kind !== 'relay').\n if (\n conn.kind === 'relay' &&\n getLiveIntent() &&\n request.params.arguments?.confirm !== true\n ) {\n return liveGuardError('evaluate');\n }\n // SECRET-HANDLING: do not log expression or result value.\n return jsonResult(await evaluate(conn, expression));\n }\n case 'call_sdk': {\n const sdkName = request.params.arguments?.name;\n if (typeof sdkName !== 'string' || sdkName === '') {\n return mcpError(\n 'call_sdk: name 인자가 비어 있습니다. 호출할 SDK 메서드 이름을 전달하세요.',\n );\n }\n const rawArgs = request.params.arguments?.args;\n const sdkArgs: unknown[] = Array.isArray(rawArgs) ? rawArgs : [];\n // LIVE guard (issue #348, race fix #354): see `evaluate` above —\n // snapshot `conn.kind` + fresh `getLiveIntent()` so the false→true\n // race (concurrent `start_debug('relay-live')` mid-await) is rejected.\n if (\n conn.kind === 'relay' &&\n getLiveIntent() &&\n request.params.arguments?.confirm !== true\n ) {\n return liveGuardError('call_sdk');\n }\n // SECRET-HANDLING: do not log name, args, or result value.\n const sdkResult = await callSdk(conn, sdkName, sdkArgs);\n // 상태 4: SDK 부재 — ok:false + 'sdk-absent:' 패턴은 isError로 승격\n if (\n !sdkResult.ok &&\n typeof sdkResult.error === 'string' &&\n sdkResult.error.startsWith('sdk-absent:')\n ) {\n // issue #360: local(`--target=local`) 세션은 dog-food 재배포가 아니라\n // dev 서버/unplugin alias 확인이 맞는 안내다 — connection.kind로 분기.\n return sdkAbsentError('call_sdk', conn.kind === 'local');\n }\n const callSdkAttached = conn.listTargets().length > 0;\n return envelopeResult(sdkResult, name, env, callSdkAttached);\n }\n default:\n return unknownTool(name);\n }\n } catch (err) {\n // issue #360: sdk-absent 분류가 local 세션이면 dev-bridge 안내로 분기하도록\n // connection 종류를 넘긴다. 다른 에러 분류에는 영향 없음(isLocal 미사용).\n return errorResult(err, name, conn.kind === 'local');\n }\n });\n\n return server;\n}\n\n/**\n * Normalizes a raw `start_debug` `mode` argument to a `StartDebugMode`, or\n * `null` when the value is not one of the four accepted modes:\n * 'local-browser' | 'relay-sandbox' | 'relay-staging' | 'relay-live'\n *\n * Hard rename (issue #398): the older `local`/`mobile`/`staging`/`live` names\n * and their aliases are no longer accepted — pre-1.0, no back-compat.\n */\nexport function normalizeStartDebugMode(raw: unknown): StartDebugMode | null {\n if (\n raw === 'local-browser' ||\n raw === 'relay-sandbox' ||\n raw === 'relay-staging' ||\n raw === 'relay-live'\n ) {\n return raw;\n }\n return null;\n}\n\n/**\n * Builds a trivial `ConnectionRouter` pinned to a single connection (issue\n * #348). Used by `createDebugServer` when no real dual router is injected —\n * every existing single-connection test and the `local`-only / `relay`-only\n * boot path. `switchMode` here cannot lazily boot another family, so it only\n * honors a request that matches the connection's own kind (and arms/disarms\n * `liveIntent` accordingly for relay-live); any cross-family request is\n * rejected with a clear \"dynamic switch unavailable in this session\" error.\n */\nexport function makeSingleConnectionRouter(connection: CdpConnection): ConnectionRouter {\n return {\n get active() {\n return connection;\n },\n // A single-connection router has no family concept, so it carries no relay\n // origin discriminator (issue #378). Env derives as `relay-dev` for a relay\n // connection here — `relay-sandbox` (external-PWA origin) is rejected below\n // since this router cannot boot the external relay family.\n activeRelayOrigin: undefined,\n // `_projectRoot` (issue #396) is accepted for interface conformance but\n // unused here: this router never lazily boots a relay family — its single\n // connection (and thus any relay verifyAuth) was already built at startup,\n // so a per-session project-local secret cannot retroactively rewire it. The\n // dual router below performs the read-only load before a lazy relay boot.\n switchMode(\n mode: StartDebugMode,\n confirm: boolean,\n _projectRoot?: string,\n ): Promise<ModeSwitchReport> {\n // `relay-sandbox` (env 2) needs a distinct external-PWA relay family this\n // single-connection router cannot synthesize. Reject the same way a\n // cross-family switch is rejected (issue #378).\n if (mode === 'relay-sandbox') {\n return Promise.reject(\n new Error(\n 'start_debug: 이 세션은 단일 연결만 보유합니다 — ' +\n \"'relay-sandbox'(환경 2 PWA, 외부 relay)로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). \" +\n 'MCP 서버를 relay-sandbox 모드로 재시작하세요.',\n ),\n );\n }\n const wantRelay = isRelayMode(mode);\n const haveRelay = connection.kind === 'relay';\n if (wantRelay !== haveRelay) {\n return Promise.reject(\n new Error(\n `start_debug: 이 세션은 단일 ${connection.kind} 연결만 보유합니다 — ` +\n `'${mode}'로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). ` +\n 'MCP 서버를 원하는 모드로 재시작하세요.',\n ),\n );\n }\n // relay-live entry gate: confirm:true required (mirrors the per-tool gate).\n if (mode === 'relay-live' && !confirm) {\n return Promise.reject(\n new Error(\n 'start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — ' +\n '실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.',\n ),\n );\n }\n setLiveIntent(mode === 'relay-live');\n const environment = deriveEnvironment(connection.kind, getLiveIntent());\n return Promise.resolve({\n mode,\n environment,\n kind: connection.kind,\n liveGuardActive: connection.kind === 'relay' && getLiveIntent(),\n nextStep:\n connection.kind === 'relay'\n ? 'build_attach_url로 attach QR을 생성하세요.'\n : 'list_pages로 로컬 페이지 attach를 확인하세요.',\n });\n },\n };\n}\n\n/**\n * Re-builds an attach URL from stored components with a FRESHLY-minted TOTP code,\n * so the dashboard/`/attach` QR is never an expired bake-in (Defect 1).\n * SECRET-HANDLING: reads AIT_DEBUG_TOTP_SECRET at call time (mirrors tunnel.ts\n * getDashboardState). The minted code rides inside attachUrl's at= param only —\n * never logged. generateTotp() relies on its Date.now() default.\n */\nfunction rebuildAttachUrl(parts: AttachUrlParts): string {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n const code = secret ? generateTotp(secret) : undefined;\n return parts.kind === 'launcher'\n ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {\n name: parts.appName,\n })\n : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);\n}\n\nfunction jsonResult(value: unknown) {\n return { content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] };\n}\n\n/**\n * Wraps `value` in a `ToolEnvelope` (when compat mode is off) and returns it\n * as a text content block. When `AIT_MCP_COMPAT=chrome-devtools` is set the\n * envelope is skipped and the raw value is returned — identical to `jsonResult`.\n */\nfunction envelopeResult(value: unknown, tool: string, env: McpEnvironment, attached: boolean) {\n const wrapped = wrapEnvelope(value, { tool, env, attached });\n return { content: [{ type: 'text' as const, text: JSON.stringify(wrapped, null, 2) }] };\n}\n\nfunction unknownTool(name: string) {\n return mcpError(`알 수 없는 tool: ${name}`);\n}\n\n/**\n * enableDomains()가 던진 에러를 4상태로 분류해 적절한 메시지를 반환한다.\n *\n * - \"No mini-app page attached\" → page 미attach (상태 2)\n * - crash/destroy/replaced 패턴 → page crash (상태 3)\n * - relay disconnect 패턴 → relay 연결 끊김\n * - 그 외 → 원본 메시지 + list_pages 안내\n */\nfunction classifyEnableDomainError(err: unknown, toolName: string) {\n const message = err instanceof Error ? err.message : String(err);\n\n // 상태 2: page 미attach\n if (message.includes('No mini-app page attached') || message.includes('페이지가 attach 안')) {\n return pageMissingError(toolName);\n }\n\n // 상태 3: page crash / target destroyed / replaced\n if (\n message.includes('replaced-by-new-attach') ||\n message.includes('targetCrashed') ||\n message.includes('targetDestroyed') ||\n message.includes('detachedFromTarget')\n ) {\n return pageCrashError(toolName);\n }\n\n // relay 연결 끊김\n if (\n message.includes('relay에 연결되어 있지 않습니다') ||\n message.includes('relay WebSocket') ||\n message.includes('Chii relay connection closed')\n ) {\n return relayDisconnectError(toolName);\n }\n\n // 그 외\n return classifyToolError(err, toolName);\n}\n\n/**\n * CDP/AIT 명령 실행 중 catch된 에러를 4상태로 분류해 tool 결과로 반환한다.\n * debug-server 내부 try/catch 블록에서 공통으로 사용한다.\n */\nfunction errorResult(err: unknown, name: string, isLocal = false) {\n return classifyToolError(err, name, isLocal);\n}\n\n/**\n * Starts a polling watcher that detects target-set changes on\n * `connection.listTargets()` and sends a `notifications/tools/list_changed`\n * notification on the given server.\n *\n * The watcher polls every `intervalMs` (default 1 000 ms). It fires\n * `server.sendToolListChanged()` + `onAttach()` whenever the sorted target-id\n * signature changes AND the new target set is non-empty. This covers:\n * - 0→N first attach\n * - 1→1 target replacement (same count, different id — e.g. rescan)\n * - N→M any change where the result is still non-empty\n *\n * Full detach (→ empty) updates the stored signature but does NOT fire the\n * callback — `onAttach` semantics are about a live target being present.\n *\n * The interval is **never cleared automatically** — it keeps running until\n * `stop()` is called during shutdown. This ensures that a target replacement\n * after the first attach is always detected.\n *\n * `onAttach` is called on every non-empty signature change (or immediately when\n * already attached). Use this to trigger side-effects such as pushing a fresh\n * SSE state to open dashboard tabs (issue #509). The callback is optional;\n * omitting it preserves the previous behaviour exactly.\n *\n * SECRET-HANDLING: target `id`/`title`/`url` are not written to any log here.\n * Only an attach-detected stderr line is emitted (no target details).\n *\n * @returns `stop` — call this during shutdown to clear the interval.\n */\nexport function startAttachWatcher(\n connection: CdpConnection,\n server: Server,\n intervalMs = 1_000,\n onAttach?: () => void,\n): { stop(): void } {\n /** Sorted, comma-joined target-id string — '' means no targets attached. */\n function signature(): string {\n return connection\n .listTargets()\n .map((t) => t.id)\n .sort()\n .join(',');\n }\n\n let lastSignature = signature();\n // If already attached when the watcher starts, send once immediately.\n if (lastSignature !== '') {\n void server.sendToolListChanged();\n onAttach?.();\n }\n\n const handle = setInterval(() => {\n const current = signature();\n if (current !== lastSignature) {\n lastSignature = current;\n if (current !== '') {\n // Non-empty signature change — new or replaced target(s).\n void server.sendToolListChanged();\n onAttach?.();\n }\n // Empty signature (full detach): signature updated above, callback skipped.\n }\n }, intervalMs);\n\n return {\n stop() {\n clearInterval(handle);\n },\n };\n}\n\nexport interface RunDebugServerOptions {\n /**\n * Local Chii relay port. Default 0 (OS-assigned ephemeral port).\n *\n * Passing 0 lets the OS choose a free port on each startup — this prevents\n * EADDRINUSE when a stale cloudflared orphan still holds a fixed port (the\n * root cause of -32000 MCP handshake failures). Pass an explicit port number\n * only when a fixed port is specifically required (backwards-compatible).\n */\n relayPort?: number;\n /**\n * When `true`, terminates the process holding the existing server lock and\n * takes over the session. Corresponds to `--force` / `--takeover` CLI flags.\n *\n * Default `false`.\n */\n force?: boolean;\n}\n\n// `buildRelayVerifyAuth` now lives in `./totp.js` (lightweight, node:crypto\n// only) so the unplugin's env-2 relay can wire the same TOTP upgrade gate\n// without pulling the heavy MCP server module graph. Re-exported here so\n// existing importers (and tests) keep resolving it from `debug-server.js`.\nexport { buildRelayVerifyAuth };\n\n/**\n * Factory that constructs a `ChiiCdpConnection` for the given relay base URL.\n *\n * Introduced as a named seam so PR-2 (dual-connection, #348) can defer\n * construction to first-activation time by moving or replacing this call. Since\n * #396 every family (relay included) is constructed lazily on its first\n * `start_debug`, so this is always called from the lazy boot path.\n *\n * The relay base URL is only available after `startChiiRelay()` resolves, so\n * the factory is called right after that point (same as before this refactor).\n */\nfunction createRelayConnection(relayBaseUrl: string): ChiiCdpConnection {\n // Pass the SECRET (not a code) so the connection mints a fresh TOTP per\n // (re)connect. Read from env directly: both callers run\n // assertRelayAuthConfigured() first, so when a TOTP-gated relay is up this is\n // a valid hex secret; when TOTP is disabled it is undefined and no `at=` is\n // appended (backward compatible). SECRET-HANDLING: forwarded, never logged.\n return new ChiiCdpConnection({\n relayBaseUrl,\n totpSecret: process.env.AIT_DEBUG_TOTP_SECRET,\n });\n}\n\n/**\n * AIT source that always forwards over the *currently active* connection\n * (issue #348). The single-connection `ChiiAitSource` binds one sender at\n * construction; in the dual-connection daemon the AIT.* domain must follow the\n * active connection across `start_debug` swaps, so this indirection reads\n * `getActive()` on every call.\n *\n * Both `ChiiCdpConnection` and `LocalCdpConnection` expose `sendCommand`, so\n * the active connection is a valid `AitCommandSender`.\n */\nclass RoutingAitSource extends ChiiAitSource {\n constructor(\n getActive: () => {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n },\n ) {\n super({\n sendCommand: (method, params) => getActive().sendCommand(method, params),\n });\n }\n}\n\n/**\n * A booted infra family the dual router can tear down at process exit.\n *\n * Direction-neutral (issue #356): any of the three families can be the first one\n * booted. Since #396 every family is lazy-booted on its first `start_debug`. The\n * relay family additionally exposes its live tunnel status; the local family\n * leaves it `undefined` (a local browser has no relay tunnel), so the\n * router/handlers read the relay tunnel status from whichever family is the\n * relay one.\n */\nexport interface BootedFamily {\n connection: CdpConnection;\n /** Synchronous best-effort teardown (closes the connection + any infra). */\n stop(): void;\n /**\n * Live tunnel status — only the relay family provides it (the URL changes per\n * tunnel reissue). `undefined` on the local family.\n */\n getTunnelStatus?: () => TunnelStatus;\n /**\n * Relay origin discriminator (issue #378) — set by the boot fn, NOT sniffed\n * from the URL. `'intoss-webview'` for the intoss-private relay\n * (`bootRelayFamily`), `'external-pwa'` for the env-2 external relay\n * (`bootExternalRelayFamily`). `undefined` for the local family (kind is\n * `'local'`, so the origin is irrelevant). Threaded into `deriveEnvironment`\n * so `relay-mobile` can be told apart from `relay-dev`.\n */\n relayOrigin?: RelayOrigin;\n /**\n * Local HTTP base URL of the Chii relay (e.g. `http://127.0.0.1:9100` for\n * the intoss relay, or the external cloudflare URL for env-2). Used by\n * {@link AutoDevtoolsOpener} to build the Chii self-hosted inspector URL\n * (`<relayHttpUrl>/front_end/chii_app.html`). `undefined` for the local-\n * browser family (no relay, F12 is available directly).\n *\n * SECRET-HANDLING: this value contains the relay host. MUST NOT be logged.\n */\n relayHttpUrl?: string;\n /**\n * LOCAL loopback HTTP base URL of the Chii relay for env-2\n * (`http://127.0.0.1:<relay-port>`). When set, the MCP uses this instead of\n * `relayHttpUrl` (the cloudflare tunnel base) to build inspector URLs — so\n * front_end page load and the client WS leg stay on the loopback and do not\n * traverse the tunnel (issue #530).\n *\n * Only relevant for `bootExternalRelayFamily` (env-2): the intoss relay\n * (`bootRelayFamily`) already uses a loopback `relay.baseUrl`.\n *\n * Safe to log/surface: loopback address contains no tunnel host.\n */\n relayLocalHttpUrl?: string;\n}\n\n/**\n * Boots the local-browser family (issues #348, #356). Launches a Chromium with\n * `--remote-debugging-port` and returns a `LocalCdpConnection` attached to it,\n * plus a `stop()` that kills both.\n *\n * Booted lazily via the dual router's `bootLazyFor('local-browser')` callback,\n * at most once on the first `start_debug({ mode: 'local-browser' })` (all-lazy,\n * #396 — no run function boots a family at startup anymore).\n */\nexport async function bootLocalFamily(): Promise<BootedFamily> {\n const cdpPort = 0; // OS-assigned ephemeral port.\n const devUrl = process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n const chromium = await launchChromium({ port: cdpPort, devUrl });\n // Give Chromium a moment to open its CDP endpoint before first attach.\n await new Promise<void>((r) => setTimeout(r, 800));\n const connection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });\n return {\n connection,\n stop() {\n connection.close();\n chromium.stop();\n },\n };\n}\n\n/** Options for {@link bootRelayFamily}. */\nexport interface BootRelayFamilyOptions {\n /** Relay local port. Default 0 (OS-assigned ephemeral). */\n relayPort?: number;\n /**\n * TOTP `verifyAuth` predicate for the relay WS upgrade gate. Built from\n * `AIT_DEBUG_TOTP_SECRET` at the call site via {@link buildRelayVerifyAuth}.\n * `undefined` disables the gate.\n */\n verifyAuth?: (req: import('node:http').IncomingMessage) => boolean;\n /**\n * Called whenever the public tunnel URL is (re)assigned, so the caller can\n * mirror it into the server lock file (`lockHandle.updateWssUrl`). The wssUrl\n * carries the relay host — callers MUST NOT log it directly.\n */\n onWssUrl?: (wssUrl: string) => void;\n /**\n * Secret-free observability callback for relay auth rejections (issue #467) —\n * forwarded to {@link startChiiRelay}'s `onAuthReject`. Receives only the\n * rejection kind; never the URL, query, code, or secret. Boot sites wire it\n * to `DiagnosticsCollector.recordAuthReject()` so `get_debug_status` can\n * surface silent 401s.\n */\n onAuthReject?: (event: import('./chii-relay.js').RelayAuthRejectEvent) => void;\n /**\n * Called with the cloudflared child PID once the tunnel is up.\n *\n * FIX 3 (issue #571): callers wire this to\n * `lockHandle.updateTunnelChildPid(pid)` so the lock file records the child\n * PID and a subsequent `acquireLock` can detect a zombie daemon (Node\n * process alive, tunnel child dead) without requiring `--force`.\n */\n onTunnelChildPid?: (pid: number) => void;\n}\n\n/**\n * Boots the relay family (issues #348, #356): starts the Chii relay on an\n * OS-assigned port (with optional TOTP gate), opens a cloudflared quick tunnel\n * to the relay's confirmed port in the background, prints the attach banner,\n * and arms the tunnel health probe. Returns a {@link BootedFamily} whose\n * `getTunnelStatus()` reflects the live tunnel (it flips up once the background\n * tunnel resolves and follows reissues).\n *\n * Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback\n * (symmetry with {@link bootLocalFamily}), at most once on the first\n * `start_debug({ mode: 'relay-staging' | 'relay-live' })` (all-lazy, #396 — every\n * relay boot now flows through `switchMode` after the project-local secret load).\n *\n * The relay base URL is only known after `startChiiRelay()` resolves, so the\n * `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside\n * this function, after the relay port is confirmed.\n *\n * SECRET-HANDLING: the TOTP secret rides only inside `verifyAuth`; the wssUrl\n * (relay host) is never logged here directly.\n */\nexport async function bootRelayFamily(options: BootRelayFamilyOptions = {}): Promise<BootedFamily> {\n // Relay-auth baseline (issue #250): this boots a public-internet-exposed relay\n // (cloudflared quick tunnel), so a configured TOTP secret is MANDATORY — Layer\n // C is the only fail-fast layer that stops a leaked tunnel URL from attaching.\n // Fail fast before opening the relay/tunnel. Local-only sessions never call\n // this fn and so stay exempt. SECRET-HANDLING: the guard never logs the value.\n assertRelayAuthConfigured();\n\n // Default 0: OS picks a free port. Prevents EADDRINUSE from stale cloudflared\n // orphans (SIGKILL survivors) that would otherwise block a fixed port and\n // cause -32000 MCP handshake failures on reconnect.\n const relayPort = options.relayPort ?? 0;\n const totpEnabled = options.verifyAuth !== undefined;\n\n const relay = await startChiiRelay({\n port: relayPort,\n verifyAuth: options.verifyAuth,\n onAuthReject: options.onAuthReject,\n });\n // relay.port is the actual OS-assigned port (may differ from relayPort when 0).\n logInfo('server.start', { port: relay.port, totpEnabled });\n\n let tunnel: QuickTunnel | null = null;\n let tunnelStatus: TunnelStatus = makeTunnelStatus(false, null);\n let tunnelProbe: { stop(): void } | null = null;\n // generateAttachToken is kept for legacy/non-TOTP token use, but we no longer\n // print it in the banner to avoid accidental secret exposure.\n const _token = generateAttachToken();\n\n // Bring the cloudflared tunnel up in the background so the MCP stdio transport\n // can answer `initialize` immediately. cloudflared has to lazy-download a\n // ~38 MB binary on first run; awaiting it here pushes the initialize response\n // past Claude Code's MCP connection timeout. Tools that need the tunnel\n // (`build_attach_url`) already gate on `getTunnelStatus()` and return a clear\n // \"tunnel not up\" message when it isn't ready yet, so dropping the await is\n // safe — the agent retries once the banner prints.\n const tunnelReady = startQuickTunnel(relay.port).then(\n (t) => {\n tunnel = t;\n tunnelStatus = makeTunnelStatus(true, t.wssUrl);\n options.onWssUrl?.(t.wssUrl);\n // FIX 3 (issue #571): notify caller of the cloudflared child PID so it\n // can be persisted in the server lock file for zombie detection.\n // childPid is a plain integer — not a secret.\n if (t.childPid !== undefined) {\n options.onTunnelChildPid?.(t.childPid);\n }\n // SECRET-HANDLING: wssUrl contains the relay host — do not log it directly.\n logInfo('tunnel.up', { totpEnabled });\n\n // Start the health probe now that the tunnel URL is known.\n // The probe runs every 60 s and attempts up to 3 reissues on drop.\n tunnelProbe = startTunnelHealthProbe(t, relay.port, {\n onReissue: (newTunnel) => {\n tunnel = newTunnel;\n tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);\n options.onWssUrl?.(newTunnel.wssUrl);\n // FIX (issue #572 review): update the lock's tunnelChildPid so a later\n // acquireLock sees the reissued tunnel's child — not the original dead one.\n // childPid is a plain integer — not a secret.\n if (newTunnel.childPid !== undefined) {\n options.onTunnelChildPid?.(newTunnel.childPid);\n }\n // Reprint the banner so the user (and agent) see the new URL + QR.\n void printAttachBanner({ wssUrl: newTunnel.wssUrl, totpEnabled }).then(() => {\n logInfo('tunnel.up', { totpEnabled, reissued: true });\n });\n },\n onPermanentDrop: (droppedAt) => {\n tunnelStatus = makeTunnelStatus(false, null, droppedAt, 3);\n logError('tunnel.down', {\n msg: `tunnel permanently dropped (${droppedAt}). Restart: npx @ait-co/devtools devtools-mcp`,\n });\n },\n });\n\n return printAttachBanner({ wssUrl: t.wssUrl, totpEnabled });\n },\n (err) => {\n const message = err instanceof Error ? err.message : String(err);\n logError('tunnel.down', {\n msg: `Failed to open cloudflared quick tunnel: ${message}. The relay is up locally; attach over the public URL is unavailable until the tunnel starts.`,\n });\n },\n );\n // Reference the promise to placate the linter — actual completion is observed\n // via the side-effects on `tunnelStatus` from inside `.then`.\n void tunnelReady;\n\n const connection = createRelayConnection(relay.baseUrl);\n\n return {\n connection,\n // Intoss-private dog-food/live relay (env 3/4) → relay-dev / relay-live.\n relayOrigin: 'intoss-webview',\n // Local HTTP base of the Chii relay — used by AutoDevtoolsOpener to build\n // the self-hosted inspector URL. SECRET-HANDLING: not logged.\n relayHttpUrl: relay.baseUrl,\n getTunnelStatus: () => tunnelStatus,\n stop() {\n tunnelProbe?.stop();\n // tunnel.stop() is synchronous (child process kill) — safe from exit handler.\n tunnel?.stop();\n connection.close();\n // relay.close() is async — fine for signal/exit handlers.\n void relay.close();\n },\n };\n}\n\n/**\n * Boots the EXTERNAL relay family for env 2 (real-device PWA, issue #378).\n *\n * Unlike {@link bootRelayFamily}, this does NOT start a relay or a tunnel —\n * the unplugin (`tunnel: { cdp: true }`) already brought up a Chii relay for\n * the env-2 PWA and exposed its public base URL via `AIT_RELAY_BASE_URL`. Here\n * the MCP only opens a CDP client (`createRelayConnection`) against that\n * external relay. The relay's lifecycle is owned by the unplugin, so `stop()`\n * closes ONLY the CDP client — it must never tear down the relay or a tunnel\n * we did not start.\n *\n * `getTunnelStatus()` reports `up: true` with a `wssUrl` derived from\n * `relayBaseUrl` (http→ws, https→wss) so the `build_attach_url` gate\n * (`up: true && wssUrl !== null`) is satisfied even though we never opened a\n * cloudflared tunnel ourselves.\n *\n * SECRET-HANDLING: `relayBaseUrl` carries the relay host (same sensitivity as a\n * wss URL) — it is NEVER logged here. The caller validates presence and passes\n * the value straight to the CDP client.\n */\n/**\n * Attempts to read the local loopback HTTP base URL of the env-2 Chii relay\n * (issue #530). Resolution order:\n * 1. `AIT_RELAY_LOCAL_URL` env var, if set and non-empty.\n * 2. `relayLocalUrl` from the `.ait_urls` file, if `projectRoot` is given.\n * 3. `undefined` — caller falls back to the tunnel base (existing behavior).\n *\n * This is a best-effort read — never throws. The returned value is a plain\n * `http://127.0.0.1:<port>` loopback URL; no secret exposure.\n */\nexport async function readRelayLocalUrl(\n env: NodeJS.ProcessEnv = process.env,\n projectRoot?: string,\n): Promise<string | undefined> {\n const envValue = (env.AIT_RELAY_LOCAL_URL ?? '').trim();\n if (envValue !== '') return envValue;\n\n if (projectRoot !== undefined) {\n try {\n const { readRelayUrls } = await import('./relay-url-store.js');\n const stored = await readRelayUrls({ projectRoot });\n if (stored?.relayLocalUrl) return stored.relayLocalUrl;\n } catch {\n // Silent best-effort.\n }\n }\n return undefined;\n}\n\nexport async function bootExternalRelayFamily(\n relayBaseUrl: string,\n relayLocalUrl?: string,\n): Promise<BootedFamily> {\n // Relay-auth baseline (issue #250): the env-2 PWA relay is reachable over a\n // public `*.trycloudflare.com` tunnel (started by the unplugin). The Layer C\n // TOTP gate is what blocks a leaked tunnel URL, so a configured secret is\n // MANDATORY here too. The unplugin's relay reads the SAME `AIT_DEBUG_TOTP_SECRET`,\n // so this also fails fast when the operator forgot to set it. Fail before\n // opening the CDP client. SECRET-HANDLING: the guard never logs the value.\n assertRelayAuthConfigured();\n\n const connection = createRelayConnection(relayBaseUrl);\n // Derive the public wss URL from the relay base so build_attach_url's\n // `up && wssUrl !== null` gate passes. SECRET-HANDLING: not logged.\n const externalWss = relayBaseUrl.replace(/^http/, 'ws');\n const tunnelStatus = makeTunnelStatus(true, externalWss);\n return {\n connection,\n // External env-2 PWA relay → relay-mobile (distinct from relay-dev).\n relayOrigin: 'external-pwa',\n // HTTP base of the external relay — used as fallback for inspector URL.\n // For env-2 this is the cloudflare tunnel URL (https://<host>.trycloudflare.com).\n // SECRET-HANDLING: not logged.\n relayHttpUrl: relayBaseUrl,\n // LOCAL loopback base for inspector URL assembly (issue #530) — preferred\n // over relayHttpUrl when available so front_end + client WS stay local.\n // Safe to log: loopback URL contains no tunnel host.\n relayLocalHttpUrl: relayLocalUrl,\n getTunnelStatus: () => tunnelStatus,\n stop() {\n // The unplugin owns the relay + its tunnel — close ONLY our CDP client.\n connection.close();\n },\n };\n}\n\n/**\n * Identifies a booted family slot in the dual router (issue #378).\n *\n * Before #378 the router warm-kept a single \"opposite-kind\" lazy family, which\n * could not hold both an intoss relay (`relay-staging`/`relay-live`) AND an\n * external relay (`relay-sandbox`) at once — they are both `kind: 'relay'` and\n * would collide in the single slot. The three keys separate the three distinct\n * families (4 exposed modes → 3 physical slots — `relay-staging`/`relay-live`\n * share `'relay-intoss'`, see {@link familyKeyForMode}):\n *\n * - `'local-browser'` — local Chromium + mock SDK (env 1).\n * - `'relay-intoss'` — intoss-private relay (env 3/4, `bootRelayFamily`).\n * - `'relay-sandbox'` — env-2 external PWA relay (`bootExternalRelayFamily`).\n */\nexport type FamilyKey = 'local-browser' | 'relay-intoss' | 'relay-sandbox';\n\n/**\n * Maps a `StartDebugMode` to the {@link FamilyKey} that serves it (issue #378).\n * local-browser → 'local-browser'; relay-sandbox → 'relay-sandbox';\n * relay-staging/relay-live → 'relay-intoss' (the shared physical slot).\n */\nexport function familyKeyForMode(mode: StartDebugMode): FamilyKey {\n switch (mode) {\n case 'local-browser':\n return 'local-browser';\n case 'relay-sandbox':\n return 'relay-sandbox';\n case 'relay-staging':\n case 'relay-live':\n return 'relay-intoss';\n }\n}\n\n/** The error thrown / surfaced when entering `mobile` without AIT_RELAY_BASE_URL. */\nexport const MOBILE_RELAY_BASE_URL_MISSING_MESSAGE =\n 'start_debug(mobile): AIT_RELAY_BASE_URL이 설정되지 않았습니다. ' +\n 'dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. ' +\n '자동 발견이 되지 않을 경우 relay base URL을 AIT_RELAY_BASE_URL 환경변수로 직접 전달하세요. ' +\n '환경 2(실기기 PWA) 진입은 외부 relay base가 필요합니다.';\n\n/**\n * Reads the env-2 relay base URL for the `mobile` boot site (issue #378, #424).\n *\n * Resolution order (env wins — file is the fallback):\n * 1. `env.AIT_RELAY_BASE_URL` set and non-empty → return it (operator override).\n * 2. `projectRoot` given → read `<nearest package.json dir>/.ait_urls`;\n * if `relayBaseUrl` is present → return it (auto-discovered from dev server).\n * 3. Neither → throw {@link MOBILE_RELAY_BASE_URL_MISSING_MESSAGE}.\n *\n * SECRET-HANDLING: `AIT_RELAY_BASE_URL` and the file-discovered value carry the\n * relay host. On the missing path the thrown message names the env var and notes\n * that the dev server auto-publishes it — it NEVER echoes any URL value. The\n * present value is returned to the caller (the CDP client) but never logged.\n */\nexport async function readMobileRelayBaseUrl(\n env: NodeJS.ProcessEnv = process.env,\n projectRoot?: string,\n): Promise<string> {\n // 1. Env wins — operator override.\n const raw = env.AIT_RELAY_BASE_URL;\n const envValue = typeof raw === 'string' ? raw.trim() : '';\n if (envValue !== '') {\n return envValue;\n }\n\n // 2. File fallback — auto-discovered from dev server (#424).\n if (projectRoot !== undefined) {\n const { readRelayUrls } = await import('./relay-url-store.js');\n const stored = await readRelayUrls({ projectRoot });\n if (stored?.relayBaseUrl !== undefined) {\n return stored.relayBaseUrl;\n }\n }\n\n // 3. Neither source — throw the precise guidance message.\n throw new Error(MOBILE_RELAY_BASE_URL_MISSING_MESSAGE);\n}\n\n/**\n * Options the dual router needs to re-arm the attach watcher and auto-open\n * DevTools after a swap (issues #348, #356, #378, #396).\n *\n * All-lazy (#396): NO family is booted at startup — every family boots lazily on\n * its first `start_debug` via `bootLazyFor(key)`. This routes EVERY relay boot\n * through `switchMode` (which runs `loadRelaySecretReadOnly` first), closing the\n * gap where an eager startup boot bypassed the project-local secret load. The\n * router is direction-neutral (#356): any of the three families can be the first\n * one booted, so a session can hot-switch in any direction without a restart.\n */\nexport interface DualRouterDeps {\n /**\n * Lazy boot for the family identified by `key` — called at most once per key,\n * on the first `start_debug` whose family key has not yet been booted (issue\n * #378 — keyed so an intoss relay and an external relay can be warm-kept\n * simultaneously). Since #396 NO family is booted eagerly, so this boots the\n * family for ANY of the three FamilyKey values on first use.\n *\n * `projectRoot` is threaded from the per-session `start_debug` call (#424) so\n * `relay-sandbox` boot can fall back to the `.ait_urls` file discovery when\n * `AIT_RELAY_BASE_URL` is not set.\n */\n bootLazyFor: (key: FamilyKey, projectRoot?: string) => Promise<BootedFamily>;\n /**\n * Reads the current relay base URL for the `relay-sandbox` family (issue #610).\n *\n * Called on every `relay-sandbox` re-entry when a warm family is already\n * cached — the result is compared against the cached family's `relayHttpUrl`.\n * When they differ the stale family is torn down and a fresh one is booted.\n * When they match the warm family is reused (no unnecessary teardown).\n *\n * Returns `null` on any failure (missing file, missing env var) — the caller\n * keeps the warm family on null (fail-open: better a stale connection than a\n * surprise disconnect).\n *\n * SECRET-HANDLING: the returned URL carries the relay host. Callers MUST NOT\n * log it. Only boolean same/different is safe to surface.\n *\n * Production: injected by the run functions as\n * `(pr) => readMobileRelayBaseUrl(process.env, pr).catch(() => null)`.\n * Tests inject a controlled function.\n */\n readSandboxRelayUrl?: (projectRoot?: string) => Promise<string | null>;\n /** Diagnostics collector (re-armed watcher records attach there). */\n diagnosticsCollector: DiagnosticsCollector;\n /** Auto-opens Chrome DevTools on the first relay attach (env 3/4 only). */\n devtoolsOpener: AutoDevtoolsOpener;\n /** Attach-watcher poll interval (ms). Default 1 000. */\n attachWatcherIntervalMs?: number;\n /**\n * Called on every non-empty target-signature change (first attach, target\n * replacement, or re-attach after detach). Used by run functions to push a\n * dashboard SSE notification so open browser tabs receive fresh target id\n * and TOTP links (issue #509).\n */\n onPageAttach?: () => void;\n /**\n * Returns the stable `/inspector` URL from the QR HTTP server (issue #530).\n * Called by `armWatcher` to pass to `AutoDevtoolsOpener.open()` so it can\n * open the secret-free stable URL instead of building a direct TOTP URL.\n * Returns null if the QR server is not yet started.\n */\n getInspectorStableUrl?: () => string | null;\n}\n\n/**\n * Sentinel connection returned by {@link DualConnectionRouter.active} before the\n * first `start_debug` boots a family (all-lazy, issue #396). It satisfies the\n * full {@link CdpConnection} interface but holds nothing: `listTargets()` is\n * empty, every command rejects with a clear \"call start_debug first\" message,\n * and all event/teardown members are safe no-ops. Callers that read tools before\n * any switchMode therefore get an honest empty/down state instead of an NPE.\n */\nconst NULL_CDP_CONNECTION: CdpConnection = {\n kind: 'local',\n enableDomains: () => Promise.resolve(),\n listTargets: () => [],\n getBufferedEvents: () => [],\n on: () => () => {},\n send: () => Promise.reject(new Error('no family booted yet — call start_debug first')),\n close: () => {},\n};\n\n/**\n * Production `ConnectionRouter` (issues #348, #356, #378 — DUAL-CONNECTION-COEXIST).\n *\n * Holds a keyed set of lazily-booted families ({@link FamilyKey} →\n * `BootedFamily`, issue #378) with NO family active at startup (issue #396); the\n * first `start_debug` boots and activates one. Plus an `active` pointer and the\n * single attach watcher armed on the active connection. The router is\n * **direction-neutral** (#356): any family can be the first one booted, so a\n * `--target=local` session can hot-switch into relay (and vice versa) without\n * restarting the MCP server.\n *\n * Why a KEYED map and not a single lazy slot (#378): `relay-sandbox` (env-2\n * external relay) and `relay-staging`/`relay-live` (intoss relay) are BOTH\n * `kind: 'relay'`. A single \"opposite-kind\" slot could not warm-keep both at\n * once — they would collide. The three `FamilyKey`s\n * (`local-browser` / `relay-intoss` / `relay-sandbox`) give each its own warm\n * slot — `relay-staging` and `relay-live` deliberately share the one\n * `relay-intoss` slot (wire-identical, distinguished only by `liveIntent`).\n *\n * Why all-lazy (#396): the relay TOTP secret now lives in a project-local\n * `.ait_relay` file loaded read-only by `switchMode` BEFORE a relay family boots.\n * Booting any family eagerly at startup would bypass that load. With NO eager\n * boot every relay boot flows through `switchMode → loadRelaySecretReadOnly`, so\n * the secret is always populated before `assertRelayAuthConfigured()` /\n * `buildRelayVerifyAuth()` run at the boot site.\n *\n * `switchMode`:\n * 1. rejects re-entrant swaps (`swapInFlight`) and an unconfirmed `relay-live`;\n * 2. resolves the requested mode's `FamilyKey`:\n * `lazyFamilies.get(key) ?? (boot via bootLazyFor(key), store)`;\n * 3. flips `active` (the MCP `Server` never re-handshakes — it reads through\n * `active` per request);\n * 4. sets `liveIntent` (true only for `relay-live`; `relay-sandbox` is dev-intent → false);\n * 5. stops the old attach watcher and re-arms one on the new connection\n * (the watcher self-clears, so re-arm is mandatory);\n * 6. emits `tools/list_changed`.\n *\n * Inactive infra is left WARM — teardown happens only at process exit (the\n * unified shutdown in the run functions), which is what keeps a phone attach\n * alive across a local→relay→local round trip.\n */\nexport class DualConnectionRouter implements ConnectionRouter {\n private readonly deps: DualRouterDeps;\n /** Families, booted lazily and warm-kept per {@link FamilyKey} (#378, #396). */\n private readonly lazyFamilies = new Map<FamilyKey, BootedFamily>();\n /** `null` until the first `start_debug` boots a family (all-lazy, #396). */\n private activeFamily: BootedFamily | null = null;\n private server: Server | null = null;\n private attachWatcher: { stop(): void } | null = null;\n private swapInFlight = false;\n\n constructor(deps: DualRouterDeps) {\n this.deps = deps;\n }\n\n get active(): CdpConnection {\n return this.activeFamily ? this.activeFamily.connection : NULL_CDP_CONNECTION;\n }\n\n /** Relay origin of the currently-active family (issue #378). */\n get activeRelayOrigin(): RelayOrigin | undefined {\n return this.activeFamily?.relayOrigin;\n }\n\n /**\n * HTTP base URL of the Chii relay to use for inspector URL assembly (#503,\n * #530). Prefers the LOCAL loopback base (`relayLocalHttpUrl`) when available\n * so front_end page load + client WS do not traverse a cloudflare tunnel —\n * falls back to `relayHttpUrl` (the tunnel base for env-2, loopback for env-3/4)\n * when not set. Returns `undefined` when no relay family is active.\n *\n * SECRET-HANDLING: when relayLocalHttpUrl is absent this falls back to\n * relayHttpUrl which may carry the tunnel host — callers must not log it.\n */\n get activeRelayHttpUrl(): string | undefined {\n if (!this.activeFamily) return undefined;\n return this.activeFamily.relayLocalHttpUrl ?? this.activeFamily.relayHttpUrl;\n }\n\n /** Every booted family (for unified shutdown). All families are lazy (#396). */\n bootedFamilies(): BootedFamily[] {\n return [...this.lazyFamilies.values()];\n }\n\n /**\n * Live tunnel status of the active relay family (issues #356, #378). Reads\n * the ACTIVE family's tunnel when it has one (so `relay-sandbox` surfaces the\n * external relay wss and `relay-staging`/`relay-live` the intoss relay wss); otherwise\n * falls back to the first booted family that has a tunnel. Returns \"down\"\n * until any relay family is booted (any session before the first relay\n * start_debug) — the correct signal for `build_attach_url` (no tunnel yet).\n */\n relayTunnelStatus(): TunnelStatus {\n if (this.activeFamily?.getTunnelStatus) return this.activeFamily.getTunnelStatus();\n for (const family of this.bootedFamilies()) {\n if (family.getTunnelStatus) return family.getTunnelStatus();\n }\n return { up: false, wssUrl: null };\n }\n\n /**\n * Binds the MCP `Server`; the attach watcher is armed by the first\n * `start_debug` since no family is active at startup (all-lazy, #396). Called\n * once after `createDebugServer` + `connect`.\n */\n start(server: Server): void {\n this.server = server;\n this.armWatcher();\n }\n\n /** Stops the current attach watcher (for shutdown). */\n stopWatcher(): void {\n this.attachWatcher?.stop();\n this.attachWatcher = null;\n }\n\n /** Arms a fresh attach watcher on the current active connection. */\n private armWatcher(): void {\n const server = this.server;\n if (!server) return;\n // No family active yet (all-lazy, #396) — nothing to watch until the first\n // `start_debug` boots one and re-arms the watcher.\n const activeFamily = this.activeFamily;\n if (!activeFamily) return;\n this.attachWatcher = startAttachWatcher(\n activeFamily.connection,\n server,\n this.deps.attachWatcherIntervalMs ?? 1_000,\n () => {\n this.deps.diagnosticsCollector.recordAttach();\n // Notify dashboard of page attach — SSE push so the browser tab updates.\n this.deps.onPageAttach?.();\n // Auto-open Chii DevTools only for a relay attach (env 2/3/4). The\n // opener no-ops for a local (mock) connection — guard on the active\n // kind so a local session never tries to open a relay devtools.\n // AutoDevtoolsOpener._opened is a once-per-session guard, so repeat\n // fires (target replacement) do not open an extra browser window.\n if (activeFamily.connection.kind === 'relay') {\n // Take the first attached target's id — we are in the onAttach\n // callback, so listTargets() is guaranteed to be non-empty.\n const firstTarget = activeFamily.connection.listTargets()[0];\n const env = deriveEnvironment(\n activeFamily.connection.kind,\n getLiveIntent(),\n activeFamily.relayOrigin,\n );\n // Prefer the stable /inspector URL (issue #530): secret-free, no\n // expiry race. Falls back to the direct URL path when qrServer is\n // not yet available (should not happen in practice).\n const inspectorStableUrl = this.deps.getInspectorStableUrl?.() ?? null;\n this.deps.devtoolsOpener.open({\n inspectorStableUrl,\n relayHttpBaseUrl: activeFamily.relayHttpUrl,\n targetId: firstTarget?.id,\n // Mint a fresh TOTP code from the daemon's secret at open time.\n // The relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps (~3 min).\n // SECRET-HANDLING: the closure captures only the getter, never logs.\n // Only used when inspectorStableUrl is absent (legacy path).\n mintTotp: process.env.AIT_DEBUG_TOTP_SECRET\n ? () => generateTotp(process.env.AIT_DEBUG_TOTP_SECRET as string)\n : undefined,\n env,\n });\n }\n },\n );\n }\n\n /**\n * Resolves the `BootedFamily` for `key`: the warm family if already booted,\n * otherwise boots it via `bootLazyFor(key, projectRoot)` and stores it (once\n * per key). Since #396 every family is lazy, so this is the single boot path\n * for all three keys.\n *\n * `projectRoot` is forwarded to `bootLazyFor` so `relay-sandbox` boot can\n * fall back to `.ait_urls` file discovery (#424) when `AIT_RELAY_BASE_URL` is\n * not set in the environment.\n *\n * **Relay-sandbox stale-URL rebuild (issue #610):** when the `relay-sandbox`\n * family is already warm, reads the current relay URL via\n * `deps.readSandboxRelayUrl` and compares it against the cached\n * `relayHttpUrl`. If they differ (dev server was restarted → new tunnel),\n * the stale family is torn down, evicted from the map, and a fresh one is\n * booted. If they match, or if the URL cannot be read, the warm family is\n * reused (fail-open — no unnecessary teardown on transient read errors).\n *\n * SECRET-HANDLING: fresh and cached relay URLs carry the tunnel host. The\n * comparison result (same/different) is the only thing surfaced — URLs are\n * never logged.\n */\n private async familyFor(key: FamilyKey, projectRoot?: string): Promise<BootedFamily> {\n const warm = this.lazyFamilies.get(key);\n if (warm) {\n // (#610) relay-sandbox re-entry: check whether the relay host has rotated.\n // env-2 relay is owned by the dev server (unplugin), so every `dev:phone:cdp`\n // restart produces a new quick-tunnel URL. If the cached family still points\n // at the old tunnel, teardown and rebuild with the fresh URL.\n if (key === 'relay-sandbox' && this.deps.readSandboxRelayUrl !== undefined) {\n let freshUrl: string | null = null;\n try {\n freshUrl = await this.deps.readSandboxRelayUrl(projectRoot);\n } catch {\n // Treat any read error as \"URL unchanged\" — fail-open to avoid\n // dropping a working connection on a transient FS error.\n freshUrl = null;\n }\n // SECRET-HANDLING: only compare; never log the URL values.\n const changed = freshUrl !== null && freshUrl !== warm.relayHttpUrl;\n if (changed) {\n // Stale relay: close only the CDP client (the unplugin owns the relay\n // + tunnel — exactly what bootExternalRelayFamily's stop() does).\n warm.stop();\n this.lazyFamilies.delete(key);\n const booted = await this.deps.bootLazyFor(key, projectRoot);\n this.lazyFamilies.set(key, booted);\n return booted;\n }\n }\n return warm;\n }\n const booted = await this.deps.bootLazyFor(key, projectRoot);\n this.lazyFamilies.set(key, booted);\n return booted;\n }\n\n async switchMode(\n mode: StartDebugMode,\n confirm: boolean,\n projectRoot?: string,\n ): Promise<ModeSwitchReport> {\n if (this.swapInFlight) {\n throw new Error('start_debug: 이전 전환이 아직 진행 중입니다 — 잠시 후 다시 호출하세요.');\n }\n if (mode === 'relay-live' && !confirm) {\n throw new Error(\n 'start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — ' +\n '실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.',\n );\n }\n\n this.swapInFlight = true;\n try {\n // (1) Project-local relay secret load (issue #396). When entering a relay\n // family, read the relay TOTP secret read-only from\n // <projectRoot>/.ait_relay into process.env BEFORE the relay boots, so the\n // lazy boot's assertRelayAuthConfigured() + buildRelayVerifyAuth() (both\n // read env at the boot site) see it. The daemon NEVER mints — a missing or\n // invalid file leaves env untouched and the boot-site assert remains the\n // single #250 fail-fast. Local switches need no secret, so skip the load.\n // SECRET-HANDLING: loadRelaySecretReadOnly never logs the value or path.\n if (isRelayMode(mode)) {\n await loadRelaySecretReadOnly({ projectRoot });\n }\n\n // (2) Resolve the family by key (#378). `bootLazyFor` may throw (e.g.\n // mobile without AIT_RELAY_BASE_URL / .ait_urls) — let it propagate\n // WITHOUT flipping active or arming liveIntent, so a failed entry leaves\n // state untouched. Pass projectRoot so relay-sandbox boot can discover\n // the relay URL from .ait_urls (#424).\n const target = await this.familyFor(familyKeyForMode(mode), projectRoot);\n\n // (3) Flip the active pointer. The MCP Server reads through `active` per\n // request, so no re-handshake / restart is needed.\n this.activeFamily = target;\n\n // (4) Arm/disarm liveIntent. true only for relay-live; any other mode\n // (including local-browser and relay-sandbox) disarms it — relay-sandbox\n // is dev-intent.\n setLiveIntent(mode === 'relay-live');\n\n // (5) Re-arm the attach watcher on the new connection (self-clearing).\n this.stopWatcher();\n this.armWatcher();\n\n // (6) Tell the MCP host the tool surface may have changed (env flip).\n void this.server?.sendToolListChanged();\n\n const wantRelay = isRelayMode(mode);\n const environment = deriveEnvironment(\n target.connection.kind,\n getLiveIntent(),\n target.relayOrigin,\n );\n return {\n mode,\n environment,\n kind: target.connection.kind,\n liveGuardActive: target.connection.kind === 'relay' && getLiveIntent(),\n nextStep: wantRelay\n ? 'build_attach_url로 attach QR을 생성하세요 (relay 세션).'\n : 'list_pages로 로컬 Chromium 페이지 attach를 확인하세요.',\n };\n } finally {\n this.swapInFlight = false;\n }\n }\n}\n\n/**\n * Boots the live debug stack and serves it over stdio:\n * 1. start the Chii relay on an OS-assigned port (with TOTP auth if\n * AIT_DEBUG_TOTP_SECRET is set),\n * 2. open a cloudflared quick tunnel to the relay's confirmed port,\n * 3. print relay URL + attach instructions,\n * 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.\n */\nexport async function runDebugServer(options: RunDebugServerOptions = {}): Promise<void> {\n // Enforce a single debug session per machine. If another server is alive,\n // ServerLockConflictError is thrown — the MCP host surfaces the message to\n // the agent without a relay or cloudflared ever starting.\n // `force: true` kills the existing process and takes over the lock.\n const lockHandle = acquireLock({ force: options.force ?? false });\n\n // Dual-connection router (issues #348, #356, #378, #396): ALL families are\n // lazy-booted on the first matching `start_debug`. Nothing boots at startup —\n // every relay boot flows through `switchMode → loadRelaySecretReadOnly` first,\n // so the project-local `.ait_relay` secret is always loaded before the relay\n // boot's assertRelayAuthConfigured() / buildRelayVerifyAuth() read the env.\n const devtoolsOpener = new AutoDevtoolsOpener();\n // Diagnostics collector — records server-side errors and attach/detach events\n // so `get_debug_status` can surface them in a single call.\n const diagnosticsCollector = new InMemoryDiagnosticsCollector();\n\n // FIX (issue #572 review): track the live cloudflared child PID in memory so\n // get_debug_status can pass it to getDiagnostics as source (a). Updated by\n // onTunnelChildPid on initial boot and on every reissue.\n let activeTunnelChildPid: number | null = null;\n\n const router = new DualConnectionRouter({\n // Lazy resolver for all three family slots (#378, #396, #424).\n // SECRET-HANDLING: readMobileRelayBaseUrl reads AIT_RELAY_BASE_URL (or .ait_urls\n // fallback) only here, at the mobile boot site, and never logs its value.\n // verifyAuth is built INSIDE the lambda (lazily, at the relay boot site) so it\n // reads the env AFTER switchMode's project-local secret load (#396) has\n // populated AIT_DEBUG_TOTP_SECRET — never captured at server startup.\n bootLazyFor: async (key, projectRoot) =>\n key === 'relay-sandbox'\n ? bootExternalRelayFamily(\n await readMobileRelayBaseUrl(process.env, projectRoot),\n await readRelayLocalUrl(process.env, projectRoot),\n )\n : key === 'local-browser'\n ? bootLocalFamily()\n : bootRelayFamily({\n relayPort: options.relayPort,\n verifyAuth: buildRelayVerifyAuth(),\n // Mirror the assigned tunnel URL into the lock file so a second\n // caller sees the correct wssUrl in the conflict error message, and\n // notify the dashboard SSE clients of the tunnel URL change.\n onWssUrl: (wssUrl) => {\n lockHandle.updateWssUrl(wssUrl);\n qrServer?.notifyStateChange();\n },\n // FIX 3 (issue #571): persist the cloudflared child PID in the\n // lock file so a subsequent acquireLock can detect zombie daemons.\n // Also update the in-memory tracker (source a for FIX 2).\n onTunnelChildPid: (pid) => {\n activeTunnelChildPid = pid;\n lockHandle.updateTunnelChildPid(pid);\n },\n // Issue #467: count relay TOTP 401s (secret-free) so\n // get_debug_status can distinguish \"phone never arrived\" from\n // \"phone arrived but was rejected\".\n onAuthReject: () => diagnosticsCollector.recordAuthReject(),\n }),\n diagnosticsCollector,\n devtoolsOpener,\n onPageAttach: () => qrServer?.notifyStateChange(),\n // Stable /inspector URL for auto-open (issue #530). qrServer is set after\n // the router is created but before armWatcher fires, so the closure safely\n // captures it by reference.\n getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,\n // (#610) Stale relay-sandbox rebuild: re-read the relay URL on every\n // relay-sandbox re-entry so the router can detect when the dev server was\n // restarted (new quick-tunnel) and rebuild the CDP client accordingly.\n // SECRET-HANDLING: readMobileRelayBaseUrl never logs the URL value.\n readSandboxRelayUrl: (pr) => readMobileRelayBaseUrl(process.env, pr).catch(() => null),\n });\n\n // AIT.* methods ride the *active* connection's command channel (relay Chii or\n // local CDP), so the AIT source follows `start_debug` swaps.\n const aitSource = new RoutingAitSource(() => {\n const active = router.active as CdpConnection & {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n };\n return active;\n });\n\n // dashboard용 lastAttachParts 상태 — build_attach_url 호출마다 갱신.\n // 완성 URL 대신 컴포넌트를 저장해 getDashboardState 호출마다 fresh TOTP를 mint (Defect 1).\n // SECRET-HANDLING: 컴포넌트에는 tunnel/scheme host가 있으므로 로그 출력 금지.\n let lastAttachParts: AttachUrlParts | null = null;\n\n // getDashboardState 클로저 — qr-http-server dashboard에 현재 상태 전달.\n // rebuildAttachUrl()로 매 호출마다 최신 TOTP 코드를 mint한 URL을 생성한다 (Defect 1).\n // inspectorUrl은 안정 /inspector URL(issue #530) — 시크릿 없으므로 출력 가능.\n const getDashboardState = (): DashboardState => {\n const targets = router.active.listTargets();\n // inspectorUrl — /inspector 안정 진입점 (issue #530).\n // qrServer가 아직 없으면 null(초기화 직후 race). qrServer가 생기면 항상 안정 URL.\n // 클릭 시점에 TOTP를 mint하고 302 redirect하므로 stale 문제가 없다.\n // SECRET-HANDLING: /inspector URL 자체에 시크릿 없음 — 출력 가능.\n const inspectorUrl = qrServer?.inspectorStableUrl ?? null;\n return {\n tunnel: { up: router.relayTunnelStatus().up, wssUrl: router.relayTunnelStatus().wssUrl },\n pages: targets.map((t) => ({ id: t.id, url: t.url })),\n attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,\n inspectorUrl,\n // 현재 active connection에서 매 호출마다 파생한 env — /attach 카피·환경 라벨\n // 분기(#468). start_debug family swap을 따라가도록 저장하지 않고 파생한다.\n mode: deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin),\n };\n };\n\n // getDirectInspectorUrl — /inspector 라우트에서 직접 chii front_end URL을 조립.\n // getDashboardState().inspectorUrl(= /inspector 자기 자신)을 쓰면 무한 루프가 발생하므로\n // 별도 getter로 분리한다. 매 요청마다 호출되어 TOTP를 요청 시점에 mint한다.\n // SECRET-HANDLING: ok:true url에 relay host + at= 코드가 담긴다 — 로그/stdout 출력 금지.\n const getDirectInspectorUrl = (): ReturnType<\n NonNullable<QrHttpServerOptions['getDirectInspectorUrl']>\n > => {\n const relayHttpUrl = router.activeRelayHttpUrl;\n if (!relayHttpUrl) {\n return { ok: false, reason: 'relayDown' };\n }\n const targets = router.active.listTargets();\n if (targets.length === 0) {\n return { ok: false, reason: 'noTarget' };\n }\n const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;\n if (!totpSecret) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n const url = buildChiiInspectorUrl(relayHttpUrl, targets[0].id, () =>\n generateTotp(totpSecret, Date.now()),\n );\n if (url === null) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n return { ok: true, url };\n };\n\n // 로컬 QR HTTP 서버를 await로 시작 — build_attach_url 첫 호출이 qrHttpServer 확인 전에\n // 도달하는 race를 없애기 위해 cloudflared(fire-and-forget)와 달리 동기 await 사용.\n // GUI 없는 환경에서는 startQrHttpServer가 실패해도 text QR fallback으로 동작한다.\n let qrServer: QrHttpServer | undefined;\n try {\n qrServer = await startQrHttpServer(getDashboardState, { getDirectInspectorUrl });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logWarn('server.start', { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}` });\n }\n\n // TOTP 주기 갱신 타이머 — 이벤트 없이 페이지가 방치될 때 at= 코드가 stale되는 갭 수정 (#445).\n // TOTP step은 30초이므로 20초 주기로 push해 step 경계를 놓치지 않는다.\n // SECRET-HANDLING: 콜백은 단순 trigger만 — TOTP 값·at= 코드는 절대 로그/stdout에 출력 금지.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = null;\n totpRefreshHandle = setInterval(() => {\n if (lastAttachParts !== null) {\n qrServer?.notifyStateChange();\n }\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const server = createDebugServer({\n // `connection` is still required by the deps shape; the router overrides\n // which connection the handlers actually read (NULL until the first switch).\n connection: router.active,\n router,\n aitSource,\n // Tunnel status follows the active relay family once one is lazy-booted (#356).\n getTunnelStatus: () => router.relayTunnelStatus(),\n // FIX (issue #572 review): expose the live cloudflared child PID (source a)\n // so get_debug_status can feed it into getDiagnostics for the FIX 2 probe.\n getTunnelChildPid: () => activeTunnelChildPid,\n get qrHttpServer() {\n return qrServer;\n },\n diagnosticsCollector,\n // SECRET-HANDLING: the TOTP secret is read from env AT CALL TIME (inside\n // build_attach_url) so the project-local .ait_relay secret loaded by\n // switchMode (#396) is visible. It is used only to generate the at= code and\n // is never logged or surfaced in any output.\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n // dashboard 갱신 콜백 — URL 컴포넌트 저장 후 SSE push.\n // 컴포넌트를 저장해 getDashboardState가 fresh TOTP로 URL을 재빌드 (Defect 1).\n onAttachUrlBuilt: (parts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n },\n });\n\n const transport = new StdioServerTransport();\n\n // ---------------------------------------------------------------------------\n // Unified dual-family shutdown (issues #348, #356, #396): tears down every\n // family ever booted at process exit (all are lazy now — relay + tunnel +\n // health probe + every booted connection, plus a lazily-booted local\n // Chromium). Each family's `stop()` owns its own infra teardown — the relay\n // family stops its tunnel + probe, the local family kills its Chromium.\n // Inactive infra is left warm during the session and only collected here —\n // that is what preserves a warm attach across `start_debug` swaps.\n //\n // SIGKILL cannot be intercepted — cloudflared may remain orphaned (PPID 1).\n // Port 0 makes such orphans harmless: the next startup gets a fresh port.\n // Manual cleanup if needed: `pkill -f 'cloudflared.*trycloudflare'`\n // ---------------------------------------------------------------------------\n\n let closed = false;\n let parentWatcher: { stop(): void } | null = null;\n let maxAgeWatchdog: { stop(): void } | null = null;\n\n const shutdown = () => {\n // Idempotent: multiple simultaneous signals/exit/uncaught calls run only once.\n if (closed) return;\n closed = true;\n\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n // Tear down every booted family (all lazy, #396 — only those ever started).\n // family.stop() is synchronous for the infra (tunnel/Chromium kill) — safe\n // from exit handlers; the relay's relay.close() inside is async fire-and-forget.\n for (const family of router.bootedFamilies()) family.stop();\n // server.close(), qrServer.close() are async — fine for signal handlers.\n void server.close();\n void qrServer?.close();\n // Remove the lock file so the next startup can proceed immediately.\n lockHandle.release();\n };\n\n // Graceful termination signals.\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n // SIGHUP: terminal hangup / parent process exit.\n process.once('SIGHUP', shutdown);\n\n // Synchronous-only cleanup on process.exit (async calls are silently ignored\n // by Node at this stage — only family.stop() infra kills which are sync).\n process.on('exit', () => {\n if (!closed) {\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n for (const family of router.bootedFamilies()) family.stop();\n // Synchronous lock release — rmSync is safe from exit handlers.\n lockHandle.release();\n }\n });\n\n // Crash safety: shutdown before exiting so cloudflared is killed even on\n // unhandled errors. Covers cases where no signal is delivered (e.g. thrown\n // exception in async code that wasn't caught).\n process.on('uncaughtException', (err) => {\n logError('tool.error', { msg: `uncaughtException: ${String(err)}`, errorKind: 'uncaught' });\n shutdown();\n process.exit(1);\n });\n\n process.on('unhandledRejection', (reason) => {\n logError('tool.error', {\n msg: `unhandledRejection: ${String(reason)}`,\n errorKind: 'unhandled-rejection',\n });\n shutdown();\n process.exit(1);\n });\n\n await server.connect(transport);\n\n // Bind the server to the router. No family is active yet (all-lazy, #396) —\n // the attach watcher is armed by the first `start_debug` and re-armed on every\n // swap.\n router.start(server);\n\n // Self-terminate when the parent process (Claude Code or another AI host) has\n // died without sending SIGTERM/SIGHUP. Without this watcher the daemon runs\n // as a zombie, holding a stale cloudflared tunnel that silently blocks new\n // attach attempts.\n //\n // AIT_DEBUG_NO_PARENT_WATCH=1 disables the watcher — useful for:\n // - shells / process managers that legitimately re-parent the daemon\n // - manual standalone invocations where ppid churn is expected\n if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== '1') {\n parentWatcher = startParentWatcher(\n () => {\n shutdown();\n process.exit(0);\n },\n { intervalMs: 5_000 },\n );\n // Also exit when stdin closes — the MCP host closed the pipe.\n process.stdin.once('end', () => {\n shutdown();\n process.exit(0);\n });\n process.stdin.once('close', () => {\n shutdown();\n process.exit(0);\n });\n }\n\n // FIX 4 (issue #571): max-age watchdog — self-terminate after a configured\n // maximum lifetime. cloudflared quick-tunnel lifetimes are finite; a daemon\n // that outlives its tunnel will silently fail. Default 6 hours.\n //\n // AIT_DEBUG_NO_MAX_AGE=1 disables the watchdog — useful for long-running\n // manual debug sessions or process-manager environments.\n // AIT_DEBUG_MAX_AGE_MS=<ms> overrides the default 6-hour cap.\n if (process.env.AIT_DEBUG_NO_MAX_AGE !== '1') {\n const maxAgeMs = process.env.AIT_DEBUG_MAX_AGE_MS\n ? Number.parseInt(process.env.AIT_DEBUG_MAX_AGE_MS, 10) || undefined\n : undefined;\n maxAgeWatchdog = startMaxAgeWatchdog(\n () => {\n process.stderr.write(\n '[ait-debug] max-age watchdog: daemon lifetime exceeded — shutting down for a fresh start.\\n',\n );\n shutdown();\n process.exit(0);\n },\n { maxAgeMs },\n );\n }\n}\n\nexport interface RunLocalDebugServerOptions {\n /**\n * CDP remote debugging port for the local Chromium. Default 0 (OS-assigned).\n * Uses an ephemeral free port when 0, avoiding EADDRINUSE on reconnect.\n */\n cdpPort?: number;\n /**\n * URL to open in the launched browser. Defaults to `AIT_DEVTOOLS_URL` env var\n * or `http://localhost:5173`.\n */\n devUrl?: string;\n /**\n * When `true`, terminates the process holding the existing server lock and\n * takes over the session. Corresponds to `--force` / `--takeover` CLI flags.\n *\n * Default `false`.\n */\n force?: boolean;\n}\n\n/**\n * Serves the debug stack over stdio with the local browser as the default\n * target. Since #396 NOTHING boots at startup — every family (including the\n * local Chromium) is lazy-booted on its first `start_debug`:\n * 1. `start_debug({ mode: 'local-browser' })` launches a local Chromium with\n * `--remote-debugging-port=<port>` and attaches a `LocalCdpConnection`;\n * 2. the intoss/external relay families lazy-boot on the first\n * `start_debug({ mode: 'relay-staging' | 'relay-live' | 'relay-sandbox' })`;\n * 3. all of this runs through the SAME direction-neutral\n * `DualConnectionRouter` that `runDebugServer` uses (issue #356).\n *\n * Symmetry with `runDebugServer` (#356): starting with `--target=local` no\n * longer pins a single-connection router. A `--target=local` session can\n * hot-switch into relay (env 1 → env 3) without restarting the MCP server,\n * closing the asymmetry where only the default (relay-target) entry point had\n * bidirectional hot-switch. The intended fidelity-ladder flow — \"validate in\n * env 1 (local), then env 3 (intoss-private) in ONE session, no restart\" — now\n * works from either entry point.\n *\n * `build_attach_url` (relay-specific) stays effectively hidden / non-applicable\n * until the relay family is booted: before the first relay switch the env\n * derives to `mock` and `relayTunnelStatus()` reports \"down\", so the tool fails\n * with a clear \"tunnel not up\" message. After a relay switch the relay tunnel\n * is live and the tool works.\n *\n * The AIT.* tools (`AIT.getSdkCallHistory`, `AIT.getMockState`,\n * `AIT.getOperationalEnvironment`) ride the *active* connection's CDP channel\n * via `RoutingAitSource`, so they follow `start_debug` swaps.\n */\nexport async function runLocalDebugServer(options: RunLocalDebugServerOptions = {}): Promise<void> {\n // Enforce a single debug session per machine (same lock as relay mode).\n // `force: true` kills the existing process and takes over the lock.\n const lockHandle = acquireLock({ force: options.force ?? false });\n\n const cdpPort = options.cdpPort ?? 0;\n const devUrl = options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n\n // Local family boot, deferred into the lazy resolver (all-lazy, #396). Launches\n // the Chromium + attaches a LocalCdpConnection only when `start_debug({ mode:\n // 'local-browser' })` first fires — so a session that goes straight to relay never\n // spawns a Chromium it would have to clean up. Honors this entry's\n // cdpPort/devUrl options (vs the env-only `bootLocalFamily`).\n const bootLocalFamilyForEntry = async (): Promise<BootedFamily> => {\n const chromium = await launchChromium({ port: cdpPort, devUrl });\n // Give Chromium a moment to start the CDP endpoint before we connect.\n // 800 ms is enough on most machines; the connection retries if it fails.\n await new Promise<void>((r) => setTimeout(r, 800));\n const localConnection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });\n return {\n connection: localConnection,\n stop() {\n localConnection.close();\n chromium.stop();\n },\n };\n };\n\n // Dual-connection router (issues #348, #356, #378, #396): ALL families are\n // lazy-booted — the local family on the first `start_debug({ mode: 'local-browser' })`,\n // the intoss relay on `relay-staging`/`relay-live`, the env-2 external relay on `relay-sandbox`.\n const devtoolsOpener = new AutoDevtoolsOpener();\n const diagnosticsCollector = new InMemoryDiagnosticsCollector();\n\n // FIX (issue #572 review): track the live cloudflared child PID in memory so\n // get_debug_status can pass it to getDiagnostics as source (a). Updated by\n // onTunnelChildPid on initial boot and on every reissue.\n let activeTunnelChildPid: number | null = null;\n\n const router = new DualConnectionRouter({\n // Lazy resolver for all three family slots (#378, #396, #424).\n // SECRET-HANDLING: readMobileRelayBaseUrl reads AIT_RELAY_BASE_URL (or .ait_urls\n // fallback) only here, at the mobile boot site, and never logs its value.\n // verifyAuth is built INSIDE the lambda (lazily, at the relay boot site) so it\n // reads the env AFTER switchMode's project-local secret load (#396) has\n // populated AIT_DEBUG_TOTP_SECRET — never captured at server startup.\n bootLazyFor: async (key, projectRoot) =>\n key === 'relay-sandbox'\n ? bootExternalRelayFamily(\n await readMobileRelayBaseUrl(process.env, projectRoot),\n await readRelayLocalUrl(process.env, projectRoot),\n )\n : key === 'local-browser'\n ? bootLocalFamilyForEntry()\n : bootRelayFamily({\n verifyAuth: buildRelayVerifyAuth(),\n onWssUrl: (wssUrl) => {\n lockHandle.updateWssUrl(wssUrl);\n qrServer?.notifyStateChange();\n },\n // FIX 3 (issue #571): persist cloudflared child PID for zombie detection.\n // Also update the in-memory tracker (source a for FIX 2).\n onTunnelChildPid: (pid) => {\n activeTunnelChildPid = pid;\n lockHandle.updateTunnelChildPid(pid);\n },\n // Issue #467: secret-free relay TOTP 401 counter for get_debug_status.\n onAuthReject: () => diagnosticsCollector.recordAuthReject(),\n }),\n diagnosticsCollector,\n devtoolsOpener,\n onPageAttach: () => qrServer?.notifyStateChange(),\n // Stable /inspector URL for auto-open (issue #530).\n getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,\n // (#610) Stale relay-sandbox rebuild: re-read the relay URL on every\n // relay-sandbox re-entry so the router can detect when the dev server was\n // restarted (new quick-tunnel) and rebuild the CDP client accordingly.\n // SECRET-HANDLING: readMobileRelayBaseUrl never logs the URL value.\n readSandboxRelayUrl: (pr) => readMobileRelayBaseUrl(process.env, pr).catch(() => null),\n });\n\n // AIT.* methods ride the *active* connection's command channel (local CDP or,\n // after a relay switch, relay Chii), so the AIT source follows swaps.\n const aitSource = new RoutingAitSource(() => {\n const active = router.active as CdpConnection & {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n };\n return active;\n });\n\n // dashboard용 lastAttachParts 상태 — build_attach_url 호출마다 갱신.\n // 완성 URL 대신 컴포넌트를 저장해 getDashboardState 호출마다 fresh TOTP를 mint (Defect 1).\n // SECRET-HANDLING: 컴포넌트에는 tunnel/scheme host가 있으므로 로그 출력 금지.\n let lastAttachParts: AttachUrlParts | null = null;\n\n const getDashboardState = (): DashboardState => {\n const targets = router.active.listTargets();\n // inspectorUrl — /inspector 안정 진입점 (issue #530).\n // SECRET-HANDLING: /inspector URL 자체에 시크릿 없음 — 출력 가능.\n const inspectorUrl = qrServer?.inspectorStableUrl ?? null;\n return {\n tunnel: { up: router.relayTunnelStatus().up, wssUrl: router.relayTunnelStatus().wssUrl },\n pages: targets.map((t) => ({ id: t.id, url: t.url })),\n attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,\n inspectorUrl,\n };\n };\n\n // getDirectInspectorUrl — /inspector 라우트에서 직접 chii front_end URL을 조립.\n // getDashboardState().inspectorUrl(= /inspector 자기 자신)을 쓰면 무한 루프가 발생하므로\n // 별도 getter로 분리한다. 매 요청마다 호출되어 TOTP를 요청 시점에 mint한다.\n // SECRET-HANDLING: ok:true url에 relay host + at= 코드가 담긴다 — 로그/stdout 출력 금지.\n const getDirectInspectorUrl = (): ReturnType<\n NonNullable<QrHttpServerOptions['getDirectInspectorUrl']>\n > => {\n const relayHttpUrl = router.activeRelayHttpUrl;\n if (!relayHttpUrl) {\n return { ok: false, reason: 'relayDown' };\n }\n const targets = router.active.listTargets();\n if (targets.length === 0) {\n return { ok: false, reason: 'noTarget' };\n }\n const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;\n if (!totpSecret) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n const url = buildChiiInspectorUrl(relayHttpUrl, targets[0].id, () =>\n generateTotp(totpSecret, Date.now()),\n );\n if (url === null) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n return { ok: true, url };\n };\n\n // Local QR HTTP server — awaited so the first build_attach_url call (after a\n // relay switch) doesn't race its startup. Failure falls back to text QR.\n let qrServer: QrHttpServer | undefined;\n try {\n qrServer = await startQrHttpServer(getDashboardState, { getDirectInspectorUrl });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logWarn('server.start', { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}` });\n }\n\n // TOTP 주기 갱신 타이머 — 이벤트 없이 페이지가 방치될 때 at= 코드가 stale되는 갭 수정 (#448).\n // TOTP step은 30초이므로 20초 주기로 push해 step 경계를 놓치지 않는다.\n // local-only 동안엔 lastAttachParts가 null이라 no-op — relay로 전환된 뒤 첫 build_attach_url\n // 호출 시 lastAttachParts가 세팅되면 갱신이 시작된다.\n // SECRET-HANDLING: 콜백은 단순 trigger만 — TOTP 값·at= 코드는 절대 로그/stdout 출력 금지.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = null;\n totpRefreshHandle = setInterval(() => {\n if (lastAttachParts !== null) {\n qrServer?.notifyStateChange();\n }\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const server = createDebugServer({\n connection: router.active,\n router,\n aitSource,\n // Tunnel status follows the relay family once it is lazy-booted (#356);\n // until then it reports \"down\" (no relay tunnel exists), which keeps\n // build_attach_url correctly gated.\n getTunnelStatus: () => router.relayTunnelStatus(),\n // FIX (issue #572 review): expose the live cloudflared child PID (source a)\n // so get_debug_status can feed it into getDiagnostics for the FIX 2 probe.\n getTunnelChildPid: () => activeTunnelChildPid,\n get qrHttpServer() {\n return qrServer;\n },\n diagnosticsCollector,\n // SECRET-HANDLING: the TOTP secret is read from env AT CALL TIME (inside\n // build_attach_url) so the project-local .ait_relay secret loaded by\n // switchMode (#396) is visible. It is used only to generate the at= code and\n // is never logged or surfaced in any output.\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n // dashboard 갱신 콜백 — URL 컴포넌트 저장 후 SSE push (Defect 1 fix).\n onAttachUrlBuilt: (parts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n },\n });\n\n const transport = new StdioServerTransport();\n\n // ---------------------------------------------------------------------------\n // Unified dual-family shutdown (issues #356, #396, mirrors runDebugServer):\n // tears down every family ever booted at process exit (all lazy now). Each\n // family's stop() owns its infra — the local family kills its Chromium, a\n // lazily-booted relay family stops its tunnel + probe + relay. Inactive infra\n // is left warm during the session.\n // ---------------------------------------------------------------------------\n\n let closed = false;\n let parentWatcher: { stop(): void } | null = null;\n let maxAgeWatchdog: { stop(): void } | null = null;\n\n const shutdown = () => {\n if (closed) return;\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n // Tear down every booted family (all lazy, #396 — only those ever started).\n for (const family of router.bootedFamilies()) family.stop();\n void server.close();\n void qrServer?.close();\n // Remove the lock file so the next startup can proceed immediately.\n lockHandle.release();\n };\n\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n process.once('SIGHUP', shutdown);\n\n process.on('exit', () => {\n if (!closed) {\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n for (const family of router.bootedFamilies()) family.stop();\n lockHandle.release();\n }\n });\n\n process.on('uncaughtException', (err) => {\n logError('tool.error', {\n msg: `uncaughtException: ${String(err)}`,\n errorKind: 'uncaught',\n mode: 'local-browser',\n });\n shutdown();\n process.exit(1);\n });\n\n process.on('unhandledRejection', (reason) => {\n logError('tool.error', {\n msg: `unhandledRejection: ${String(reason)}`,\n errorKind: 'unhandled-rejection',\n mode: 'local-browser',\n });\n shutdown();\n process.exit(1);\n });\n\n await server.connect(transport);\n\n // Bind the server to the router. No family is active yet (all-lazy, #396) —\n // the attach watcher is armed by the first `start_debug` and re-armed on every\n // swap.\n router.start(server);\n\n // Self-terminate when the parent process has died without sending SIGTERM/SIGHUP.\n if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== '1') {\n parentWatcher = startParentWatcher(\n () => {\n shutdown();\n process.exit(0);\n },\n { intervalMs: 5_000 },\n );\n process.stdin.once('end', () => {\n shutdown();\n process.exit(0);\n });\n process.stdin.once('close', () => {\n shutdown();\n process.exit(0);\n });\n }\n\n // FIX 4 (issue #571): max-age watchdog.\n if (process.env.AIT_DEBUG_NO_MAX_AGE !== '1') {\n const maxAgeMs = process.env.AIT_DEBUG_MAX_AGE_MS\n ? Number.parseInt(process.env.AIT_DEBUG_MAX_AGE_MS, 10) || undefined\n : undefined;\n maxAgeWatchdog = startMaxAgeWatchdog(\n () => {\n process.stderr.write(\n '[ait-debug] max-age watchdog: daemon lifetime exceeded — shutting down for a fresh start.\\n',\n );\n shutdown();\n process.exit(0);\n },\n { maxAgeMs },\n );\n }\n}\n\nexport interface RunMobileDebugServerOptions {\n /**\n * When `true`, terminates the process holding the existing server lock and\n * takes over the session. Corresponds to `--force` / `--takeover` CLI flags.\n *\n * Default `false`.\n */\n force?: boolean;\n /**\n * Project root for `.ait_urls` file-based URL discovery (#424). When supplied,\n * `readMobileRelayBaseUrl` falls back to the `.ait_urls` file written by the\n * unplugin if `AIT_RELAY_BASE_URL` is not set. Defaults to `process.cwd()`.\n */\n projectRoot?: string;\n}\n\n/**\n * Serves the env-2 (real-device PWA) debug stack over stdio with the external\n * Chii relay as the default target (issue #378). Since #396 NOTHING boots at\n * startup — the external relay family is lazy-booted on the first\n * `start_debug({ mode: 'relay-sandbox' })`.\n *\n * Unlike `runDebugServer` (which starts its own relay + cloudflared tunnel),\n * `runMobileDebugServer` attaches to a relay the unplugin ALREADY brought up\n * (`tunnel: { cdp: true }`) and exposed via `AIT_RELAY_BASE_URL`. The MCP only\n * opens a CDP client against that external relay — it never starts or tears down\n * a relay or a tunnel it did not own (see {@link bootExternalRelayFamily}).\n *\n * Symmetry with `runDebugServer` / `runLocalDebugServer` (#356, #378, #396): all\n * three families are lazy-booted — the env-2 external relay on the first\n * `start_debug({ mode: 'relay-sandbox' })`, the local family on `local-browser`,\n * the intoss relay on `relay-staging`/`relay-live` — so a `--target=mobile`\n * session can hot-switch\n * without a restart. The active env derives to `relay-mobile` (external-PWA\n * origin, liveIntent off).\n *\n * SECRET-HANDLING: `AIT_RELAY_BASE_URL` is read once here via\n * {@link readMobileRelayBaseUrl}; when unset it throws\n * {@link MOBILE_RELAY_BASE_URL_MISSING_MESSAGE} — a message that names the env\n * var and how to obtain it, never echoing any URL value. The error propagates to\n * the bin entry's fatal handler (the missing-URL path prints the guidance, not a\n * value). The present value is passed straight to the CDP client, never logged.\n */\nexport async function runMobileDebugServer(\n options: RunMobileDebugServerOptions = {},\n): Promise<void> {\n // Read the external relay base BEFORE acquiring the lock so a missing-URL\n // invocation fails fast (fatal stderr via the bin entry) without taking the\n // single-session lock or opening any connection. Kept pre-flight (NOT moved\n // into the lazy lambda) so the fail-fast still precedes the lock.\n // (#424) Falls back to .ait_urls if AIT_RELAY_BASE_URL is unset.\n // SECRET-HANDLING: relayBaseUrl is passed to the CDP client only, never logged.\n const relayBaseUrl = await readMobileRelayBaseUrl(\n process.env,\n options.projectRoot ?? process.cwd(),\n );\n\n // Enforce a single debug session per machine (same lock as the other modes).\n // `force: true` kills the existing process and takes over the lock.\n const lockHandle = acquireLock({ force: options.force ?? false });\n\n // Dual-connection router (issues #348, #356, #378, #396): ALL families are\n // lazy-booted — the env-2 external relay on the first `start_debug({ mode:\n // 'relay-sandbox' })`, the local family on `local-browser`, the intoss relay on\n // `relay-staging`/`relay-live`.\n const devtoolsOpener = new AutoDevtoolsOpener();\n const diagnosticsCollector = new InMemoryDiagnosticsCollector();\n\n // FIX (issue #572 review): track the live cloudflared child PID in memory so\n // get_debug_status can pass it to getDiagnostics as source (a). Updated by\n // onTunnelChildPid on initial boot and on every reissue.\n let activeTunnelChildPid: number | null = null;\n\n const router = new DualConnectionRouter({\n // Lazy resolver for all three family slots (#378, #396, #424). The external\n // relay boot captures the pre-flight `relayBaseUrl`. Its stop() closes ONLY\n // the CDP client — the unplugin owns the relay + its tunnel.\n // verifyAuth is built INSIDE the lambda (lazily, at the relay boot site) so it\n // reads the env AFTER switchMode's project-local secret load (#396) has\n // populated AIT_DEBUG_TOTP_SECRET — never captured at server startup.\n bootLazyFor: async (key) =>\n key === 'relay-sandbox'\n ? bootExternalRelayFamily(\n relayBaseUrl,\n await readRelayLocalUrl(process.env, options.projectRoot ?? process.cwd()),\n )\n : key === 'local-browser'\n ? bootLocalFamily()\n : bootRelayFamily({\n verifyAuth: buildRelayVerifyAuth(),\n onWssUrl: (wssUrl) => {\n lockHandle.updateWssUrl(wssUrl);\n qrServer?.notifyStateChange();\n },\n // FIX 3 (issue #571): persist cloudflared child PID for zombie detection.\n // Also update the in-memory tracker (source a for FIX 2).\n onTunnelChildPid: (pid) => {\n activeTunnelChildPid = pid;\n lockHandle.updateTunnelChildPid(pid);\n },\n // Issue #467: secret-free relay TOTP 401 counter for get_debug_status.\n onAuthReject: () => diagnosticsCollector.recordAuthReject(),\n }),\n diagnosticsCollector,\n devtoolsOpener,\n onPageAttach: () => qrServer?.notifyStateChange(),\n // Stable /inspector URL for auto-open (issue #530).\n getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,\n // (#610) Stale relay-sandbox rebuild: re-read the relay URL on every\n // relay-sandbox re-entry so the router can detect when the dev server was\n // restarted (new quick-tunnel) and rebuild the CDP client accordingly.\n // SECRET-HANDLING: readMobileRelayBaseUrl never logs the URL value.\n readSandboxRelayUrl: (pr) =>\n readMobileRelayBaseUrl(process.env, pr ?? options.projectRoot ?? process.cwd()).catch(\n () => null,\n ),\n });\n\n // AIT.* methods ride the *active* connection's command channel (external relay\n // Chii, or local CDP / intoss Chii after a switch), so the AIT source follows\n // `start_debug` swaps.\n const aitSource = new RoutingAitSource(() => {\n const active = router.active as CdpConnection & {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n };\n return active;\n });\n\n // dashboard용 lastAttachParts 상태 — build_attach_url 호출마다 갱신.\n // 완성 URL 대신 컴포넌트를 저장해 getDashboardState 호출마다 fresh TOTP를 mint (Defect 1).\n // SECRET-HANDLING: 컴포넌트에는 tunnel/scheme host가 있으므로 로그 출력 금지.\n let lastAttachParts: AttachUrlParts | null = null;\n\n const getDashboardState = (): DashboardState => {\n const targets = router.active.listTargets();\n // inspectorUrl — /inspector 안정 진입점 (issue #530).\n // SECRET-HANDLING: /inspector URL 자체에 시크릿 없음 — 출력 가능.\n const inspectorUrl = qrServer?.inspectorStableUrl ?? null;\n return {\n tunnel: { up: router.relayTunnelStatus().up, wssUrl: router.relayTunnelStatus().wssUrl },\n pages: targets.map((t) => ({ id: t.id, url: t.url })),\n attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,\n inspectorUrl,\n };\n };\n\n // getDirectInspectorUrl — /inspector 라우트에서 직접 chii front_end URL을 조립.\n // getDashboardState().inspectorUrl(= /inspector 자기 자신)을 쓰면 무한 루프가 발생하므로\n // 별도 getter로 분리한다. 매 요청마다 호출되어 TOTP를 요청 시점에 mint한다.\n // SECRET-HANDLING: ok:true url에 relay host + at= 코드가 담긴다 — 로그/stdout 출력 금지.\n const getDirectInspectorUrl = (): ReturnType<\n NonNullable<QrHttpServerOptions['getDirectInspectorUrl']>\n > => {\n const relayHttpUrl = router.activeRelayHttpUrl;\n if (!relayHttpUrl) {\n return { ok: false, reason: 'relayDown' };\n }\n const targets = router.active.listTargets();\n if (targets.length === 0) {\n return { ok: false, reason: 'noTarget' };\n }\n const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;\n if (!totpSecret) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n const url = buildChiiInspectorUrl(relayHttpUrl, targets[0].id, () =>\n generateTotp(totpSecret, Date.now()),\n );\n if (url === null) {\n return { ok: false, reason: 'totpUnavailable' };\n }\n return { ok: true, url };\n };\n\n // Local QR HTTP server — awaited so the first build_attach_url call doesn't\n // race its startup. Failure falls back to text QR.\n let qrServer: QrHttpServer | undefined;\n try {\n qrServer = await startQrHttpServer(getDashboardState, { getDirectInspectorUrl });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logWarn('server.start', { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}` });\n }\n\n // TOTP 주기 갱신 타이머 — 이벤트 없이 페이지가 방치될 때 at= 코드가 stale되는 갭 수정 (#448).\n // TOTP step은 30초이므로 20초 주기로 push해 step 경계를 놓치지 않는다.\n // SECRET-HANDLING: 콜백은 단순 trigger만 — TOTP 값·at= 코드는 절대 로그/stdout 출력 금지.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = null;\n totpRefreshHandle = setInterval(() => {\n if (lastAttachParts !== null) {\n qrServer?.notifyStateChange();\n }\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const server = createDebugServer({\n connection: router.active,\n router,\n aitSource,\n // Tunnel status follows the active relay family — once the env-2 external\n // relay is lazy-booted it reports up with its wss URL, so build_attach_url is\n // satisfied without us opening a cloudflared tunnel.\n getTunnelStatus: () => router.relayTunnelStatus(),\n // FIX (issue #572 review): expose the live cloudflared child PID (source a)\n // so get_debug_status can feed it into getDiagnostics for the FIX 2 probe.\n getTunnelChildPid: () => activeTunnelChildPid,\n get qrHttpServer() {\n return qrServer;\n },\n diagnosticsCollector,\n // SECRET-HANDLING: the TOTP secret is read from env AT CALL TIME (inside\n // build_attach_url) so the project-local .ait_relay secret loaded by\n // switchMode (#396) is visible. It is used only to generate the at= code and\n // is never logged or surfaced in any output.\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n // dashboard 갱신 콜백 — URL 컴포넌트 저장 후 SSE push (Defect 1 fix).\n onAttachUrlBuilt: (parts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n },\n });\n\n const transport = new StdioServerTransport();\n\n // ---------------------------------------------------------------------------\n // Unified dual-family shutdown (issues #356, #378, #396, mirrors the other run\n // functions): tears down every family ever booted at process exit (all lazy\n // now). The external relay family's stop() closes ONLY our CDP client (the\n // unplugin owns the relay + tunnel); a lazily-booted intoss relay family stops\n // its own tunnel + probe + relay; a lazily-booted local family kills its\n // Chromium.\n // ---------------------------------------------------------------------------\n\n let closed = false;\n let parentWatcher: { stop(): void } | null = null;\n let maxAgeWatchdog: { stop(): void } | null = null;\n\n const shutdown = () => {\n if (closed) return;\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n for (const family of router.bootedFamilies()) family.stop();\n void server.close();\n void qrServer?.close();\n lockHandle.release();\n };\n\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n process.once('SIGHUP', shutdown);\n\n process.on('exit', () => {\n if (!closed) {\n closed = true;\n parentWatcher?.stop();\n maxAgeWatchdog?.stop();\n if (totpRefreshHandle) clearInterval(totpRefreshHandle);\n router.stopWatcher();\n for (const family of router.bootedFamilies()) family.stop();\n lockHandle.release();\n }\n });\n\n process.on('uncaughtException', (err) => {\n logError('tool.error', {\n msg: `uncaughtException: ${String(err)}`,\n errorKind: 'uncaught',\n mode: 'relay-sandbox',\n });\n shutdown();\n process.exit(1);\n });\n\n process.on('unhandledRejection', (reason) => {\n logError('tool.error', {\n msg: `unhandledRejection: ${String(reason)}`,\n errorKind: 'unhandled-rejection',\n mode: 'relay-sandbox',\n });\n shutdown();\n process.exit(1);\n });\n\n await server.connect(transport);\n\n // Bind the server to the router. No family is active yet (all-lazy, #396) —\n // the attach watcher is armed by the first `start_debug` and re-armed on every\n // swap.\n router.start(server);\n\n // Self-terminate when the parent process has died without sending SIGTERM/SIGHUP.\n if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== '1') {\n parentWatcher = startParentWatcher(\n () => {\n shutdown();\n process.exit(0);\n },\n { intervalMs: 5_000 },\n );\n process.stdin.once('end', () => {\n shutdown();\n process.exit(0);\n });\n process.stdin.once('close', () => {\n shutdown();\n process.exit(0);\n });\n }\n\n // FIX 4 (issue #571): max-age watchdog.\n if (process.env.AIT_DEBUG_NO_MAX_AGE !== '1') {\n const maxAgeMs = process.env.AIT_DEBUG_MAX_AGE_MS\n ? Number.parseInt(process.env.AIT_DEBUG_MAX_AGE_MS, 10) || undefined\n : undefined;\n maxAgeWatchdog = startMaxAgeWatchdog(\n () => {\n process.stderr.write(\n '[ait-debug] max-age watchdog: daemon lifetime exceeded — shutting down for a fresh start.\\n',\n );\n shutdown();\n process.exit(0);\n },\n { maxAgeMs },\n );\n }\n}\n","/**\n * Dev-mode `AitSource` — backed by the Vite dev server's mock-state endpoint.\n *\n * The dev server already exposes the live browser mock state at\n * `GET /api/ait-devtools/state` (registered by the unplugin with `mcp: true`).\n * Phase 3 aligns dev mode and debug mode on the same `AIT.*` tool surface, so\n * dev mode serves those tools off this one HTTP source instead of a CDP channel:\n *\n * - `AIT.getMockState` → the full state snapshot (verbatim).\n * - `AIT.getOperationalEnvironment` → derived from the snapshot's\n * `environment` + `appVersion` fields.\n * - `AIT.getSdkCallHistory` → empty (the dev endpoint does not record\n * an SDK call trace — honest, not faked).\n *\n * An AI agent thus sees the same `AIT.getMockState` tool whether attached to a\n * phone (debug) or a dev browser (dev). Tests inject a fake `fetch`.\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** Minimal `fetch` shape this source needs (injectable in tests). */\nexport type FetchLike = (url: string) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n}>;\n\nexport interface HttpAitSourceOptions {\n /** Full URL of the mock-state endpoint, e.g. `http://localhost:5173/api/ait-devtools/state`. */\n stateEndpoint: string;\n /** Injected for tests; defaults to global `fetch`. */\n fetchImpl?: FetchLike;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport class HttpAitSource implements AitSource {\n private readonly stateEndpoint: string;\n private readonly fetchImpl: FetchLike;\n\n constructor(options: HttpAitSourceOptions) {\n this.stateEndpoint = options.stateEndpoint;\n this.fetchImpl = options.fetchImpl ?? ((url) => fetch(url));\n }\n\n private async fetchState(): Promise<AitMockState> {\n const res = await this.fetchImpl(this.stateEndpoint);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch mock state from ${this.stateEndpoint}: HTTP ${res.status} ${res.statusText}. ` +\n 'Ensure the Vite dev server is running with the @ait-co/devtools unplugin option `mcp: true`.',\n );\n }\n const body = await res.json();\n return isObject(body) ? body : {};\n }\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n switch (method) {\n case 'AIT.getMockState': {\n const state = await this.fetchState();\n return state as AitMethodMap[M];\n }\n case 'AIT.getOperationalEnvironment': {\n const state = await this.fetchState();\n const environment = typeof state.environment === 'string' ? state.environment : 'unknown';\n const sdkVersion = typeof state.appVersion === 'string' ? state.appVersion : null;\n const result: AitOperationalEnvironment = { environment, sdkVersion };\n return result as AitMethodMap[M];\n }\n case 'AIT.getSdkCallHistory': {\n // sdkCallLog slice is now part of the mock state pushed by the browser panel.\n // Read it from the state snapshot rather than returning an empty stub.\n const state = await this.fetchState();\n const raw = state.sdkCallLog;\n const calls = Array.isArray(raw) ? (raw as AitSdkCallHistory['calls']) : [];\n const result: AitSdkCallHistory = { calls };\n return result as AitMethodMap[M];\n }\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * @ait-co/devtools dev-mode MCP server (stdio).\n *\n * Exposes the live browser mock state from a running Vite dev server to AI\n * coding agents via the Model Context Protocol (MCP).\n *\n * Architecture:\n * Browser (aitState) → Vite dev server endpoint (/api/ait-devtools/state)\n * ← HTTP GET ← this stdio MCP server ← AI agent\n *\n * The Vite endpoint is registered by the unplugin when `mcp: true` is set in\n * the plugin options (see `src/unplugin/index.ts`).\n *\n * Phase 3 tool-surface alignment: dev mode and debug mode now expose the same\n * `AIT.*` tools (`AIT.getMockState`, `AIT.getOperationalEnvironment`,\n * `AIT.getSdkCallHistory`). In dev mode they are backed by the HTTP mock-state\n * endpoint (see `HttpAitSource`); in debug mode by the Chii channel. So an AI\n * sees a coherent tool whether attached to a phone (debug) or a dev browser\n * (dev). `devtools_get_mock_state` (the original devtools#130 name) is kept as a\n * backward-compatible alias of `AIT.getMockState`.\n *\n * Issue #305 (M2-1) — dev/debug tool-surface unification:\n * dev-mode now also exposes `list_pages`, `get_debug_status`, `measure_safe_area`,\n * and `call_sdk` so the docs/qa/scenarios.md acceptance sequence\n * `list_pages → measure_safe_area → call_sdk` works in dev mode without\n * \"Unknown tool\" failures.\n *\n * - `list_pages` — shim: returns the Vite dev URL as a single-entry array.\n * - `get_debug_status` — dumps dev-mode server state (endpoint URL, last fetch\n * error, reachability, mode/environment metadata).\n * - `measure_safe_area`— reads safeAreaInsets from the mock state snapshot\n * (source: 'mock-vite').\n * - `call_sdk` — reads mock state and builds a mock-equivalent result\n * using window.__ait.state for supported methods; returns\n * an explicit tier-filter error for methods that require\n * a live CDP bridge.\n * - CDP-only tools (`evaluate`, `take_screenshot`, `get_dom_document`,\n * `take_snapshot`, `list_console_messages`,\n * `list_network_requests`, `list_exceptions`) — return an\n * explicit tier-filter error explaining that CDP is unavailable\n * in dev-mode and pointing to `--mode=local` or `--mode=debug`.\n *\n * This module is reached via the `devtools-mcp --mode=dev` CLI entry (see\n * `cli.ts`); the default (no flag) bin mode is the debug-mode CDP/Chii server.\n *\n * Usage (in your MCP client config, e.g. Claude Desktop):\n * {\n * \"mcpServers\": {\n * \"ait-devtools\": {\n * \"command\": \"pnpm\",\n * \"args\": [\"exec\", \"devtools-mcp\", \"--mode=dev\"],\n * \"env\": { \"AIT_DEVTOOLS_URL\": \"http://localhost:5173\" }\n * }\n * }\n * }\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { HttpAitSource } from './ait-http-source.js';\nimport type { AitSource } from './ait-source.js';\nimport { wrapEnvelope } from './envelope.js';\nimport { mcpError, tierRejectionError } from './errors.js';\nimport {\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n isAitToolName,\n type ToolAvailability,\n} from './tools.js';\n\n/** Error message prefix for CDP-dependent tools called in dev-mode. */\nconst CDP_UNAVAILABLE_IN_DEV_MODE =\n 'dev-mode에서는 CDP 연결이 없어 이 도구를 사용할 수 없습니다. ' +\n '실기기 또는 로컬 Chromium에 붙이려면 `devtools-mcp --mode=local` 또는 ' +\n '`devtools-mcp` (debug 모드 기본)로 전환하세요.';\n\n/**\n * Tool descriptors served by the dev-mode server.\n *\n * All dev-mode tools are Tier C (both envs) per RFC #277 — the dev-mode server\n * itself is the mock-side embodiment of those Tier C tools. `availableIn` is\n * declared so the surface stays consistent with the debug-mode registry.\n *\n * Issue #305: CDP-only tools are also listed with explicit descriptions so\n * agents do not get \"Unknown tool\" failures — they get a clear tier-filter\n * error message instead.\n */\nconst DEV_TOOL_DEFINITIONS = [\n /* ------------------------------------------------------------------ */\n /* AIT.* tools — HTTP mock-state backed */\n /* ------------------------------------------------------------------ */\n {\n name: 'AIT.getMockState',\n description:\n 'Returns the devtools mock state snapshot (window.__ait) from the running browser session — ' +\n 'environment, permissions, location, auth, network, IAP, and more. Read-only. ' +\n 'Requires the Vite dev server running with the @ait-co/devtools unplugin option `mcp: true`. ' +\n 'Same tool as in debug mode, where the in-app side reports it over the AIT domain.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getOperationalEnvironment',\n description:\n 'Returns the operational environment + SDK/app version derived from the dev mock state. ' +\n 'Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getSdkCallHistory',\n description:\n 'Returns the SDK call trace. In dev mode the HTTP mock-state endpoint records no trace, so ' +\n 'this returns an empty list; in debug mode it is populated over the AIT domain. Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'devtools_get_mock_state',\n description:\n 'Backward-compatible alias of AIT.getMockState (the original devtools#130 name). Returns the ' +\n 'current AIT DevTools mock state snapshot. Read-only. Prefer AIT.getMockState in new configs.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* Unified surface — dev-mode shims (issue #305) */\n /* ------------------------------------------------------------------ */\n {\n name: 'list_pages',\n description:\n 'dev-mode: returns the Vite dev server URL as a single-entry page list. ' +\n 'No CDP relay is involved — `tunnel.up` is always false and `devMode: true` marks ' +\n 'this as a shim result. Call this first to confirm the dev server is reachable. ' +\n 'In debug mode (`devtools-mcp` / `--mode=local`) this returns real attached pages.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_debug_status',\n description:\n 'dev-mode: reports the current dev session state — Vite endpoint URL, last fetch ' +\n 'timestamp/error, mock state endpoint reachability, mode (\"dev\"), and environment metadata — ' +\n 'in one call. Use this any time to confirm what the dev server is doing or when its ' +\n 'connection is suspect. In debug mode this returns tunnel/relay/attach status instead.',\n inputSchema: {\n type: 'object',\n properties: {\n recent_errors_limit: {\n type: 'number',\n description: 'Ignored in dev-mode (no error ring buffer). Present for schema parity.',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'measure_safe_area',\n description:\n 'dev-mode: reads safe-area insets from the mock state snapshot via the Vite endpoint. ' +\n 'Returns `{ source: \"mock-vite\", sdkInsets, sdkInsetsSource: \"window.__ait\", ... }`. ' +\n 'Values reflect what the DevTools panel reports at the time of the last state push. ' +\n 'In debug mode this runs a Runtime.evaluate CDP probe on the attached page.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'call_sdk',\n description:\n 'dev-mode: calls a mock SDK method via the Vite mock state endpoint. ' +\n 'Supported methods read from window.__ait mock state (e.g. getOperationalEnvironment). ' +\n 'Returns the same `{ok, value}` / `{ok, error}` envelope as debug mode. ' +\n 'In debug mode this calls the real SDK via window.__sdkCall over CDP.',\n inputSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'Mock SDK method name to call (e.g. \"getOperationalEnvironment\").',\n },\n args: {\n type: 'array',\n description: 'Arguments (ignored in dev-mode mock path; present for schema parity).',\n items: {},\n },\n },\n required: ['name'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* Tier B tool — tier-filter stub (issue #323) */\n /* */\n /* build_attach_url is relay-only (Tier B per RFC #277). Listing it */\n /* here in dev-mode ensures agents don't hit \"Unknown tool\" and get a */\n /* clear hand-off hint toward --mode=debug (station 2 → 3 seam). */\n /* ------------------------------------------------------------------ */\n {\n name: 'build_attach_url',\n description:\n 'Turns an `ait deploy --scheme-only` URL into a self-attaching deep link for a real device. ' +\n 'NOT available in dev-mode — requires a live cloudflared relay (Tier B, relay-only). ' +\n 'To use this tool: restart the MCP server with `--mode=debug` (or omit --mode) and set ' +\n 'MCP_ENV=relay, then call build_attach_url to generate the QR for phone scanning. ' +\n 'See: https://docs.aitc.dev/guides/debug-relay',\n inputSchema: {\n type: 'object',\n properties: {\n scheme_url: {\n type: 'string',\n description: 'The intoss-private:// URL from `ait deploy --scheme-only`.',\n },\n wait_for_attach: {\n type: 'boolean',\n description: 'If true, block until a page attaches (default 60 s).',\n },\n wait_timeout_seconds: {\n type: 'number',\n description:\n 'Maximum seconds to wait when wait_for_attach=true (default 60, range 1–600). ' +\n 'Invalid inputs fall back to default.',\n },\n },\n required: ['scheme_url'],\n },\n availableIn: 'relay' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* CDP-only tools — tier-filter stubs so agents see a clear error */\n /* instead of \"Unknown tool\" (issue #305) */\n /* ------------------------------------------------------------------ */\n {\n name: 'evaluate',\n description:\n 'Evaluates an arbitrary JavaScript expression via CDP Runtime.evaluate. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug` for CDP access.',\n inputSchema: {\n type: 'object',\n properties: {\n expression: { type: 'string', description: 'JavaScript expression to evaluate.' },\n },\n required: ['expression'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_screenshot',\n description:\n 'Captures a PNG screenshot via CDP Page.captureScreenshot. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_dom_document',\n description:\n 'Returns the DOM tree via CDP DOM.getDocument. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_snapshot',\n description:\n 'Captures a serialized page snapshot via CDP DOMSnapshot.captureSnapshot. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_console_messages',\n description:\n 'Lists console messages captured via CDP Runtime.consoleAPICalled. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_network_requests',\n description:\n 'Lists network requests captured via CDP Network events. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_exceptions',\n description:\n 'Lists JS exceptions captured via CDP Runtime.exceptionThrown. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: {\n type: 'object',\n properties: {\n limit: { type: 'number', description: 'Maximum exceptions to return.' },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n] as const;\n\n/** All tool names served in dev-mode (including tier-filter stubs). */\nconst DEV_TOOL_NAMES = new Set<string>(DEV_TOOL_DEFINITIONS.map((t) => t.name));\n\n/** CDP-only tools — return a tier-filter error in dev-mode. */\nconst CDP_ONLY_TOOL_NAMES = new Set<string>([\n 'evaluate',\n 'take_screenshot',\n 'get_dom_document',\n 'take_snapshot',\n 'list_console_messages',\n 'list_network_requests',\n 'list_exceptions',\n]);\n\n/**\n * Tier B tools — relay-only per RFC #277.\n * Listed in dev-mode tool surface (issue #323) so agents get a hand-off hint\n * toward `--mode=debug` instead of \"Unknown tool\".\n */\nconst TIER_B_TOOL_NAMES = new Set<string>(['build_attach_url']);\n\nexport interface CreateDevServerDeps {\n /** AIT source for the dev tools. Defaults to an HTTP source over the dev server. */\n aitSource?: AitSource;\n}\n\n/**\n * Builds the `list_pages` dev-mode shim response.\n * Returns the Vite dev URL as a single-entry page list with `devMode: true`.\n */\nfunction buildDevListPagesResult(devtoolsUrl: string) {\n return {\n pages: [\n {\n url: devtoolsUrl,\n title: 'dev fixture',\n attached: true,\n },\n ],\n tunnel: { up: false },\n devMode: true,\n singleAttachModel: true,\n };\n}\n\n/**\n * Builds the `get_debug_status` dev-mode response.\n * Probes the mock state endpoint reachability and returns server metadata.\n */\nasync function buildDevDiagnostics(\n devtoolsUrl: string,\n stateEndpoint: string,\n fetchImpl: (url: string) => Promise<Response>,\n): Promise<Record<string, unknown>> {\n let reachable = false;\n let lastFetchError: string | null = null;\n let lastFetchAt: string | null = null;\n\n try {\n const res = await fetchImpl(stateEndpoint);\n reachable = res.ok;\n lastFetchAt = new Date().toISOString();\n if (!res.ok) {\n lastFetchError = `HTTP ${res.status} ${res.statusText}`;\n }\n } catch (err) {\n lastFetchError = err instanceof Error ? err.message : String(err);\n lastFetchAt = new Date().toISOString();\n }\n\n return {\n mode: 'dev',\n devtoolsUrl,\n mcpStateEndpoint: stateEndpoint,\n mockStateEndpointReachable: reachable,\n lastFetchAt,\n lastFetchError,\n environment: {\n kind: 'mock',\n reason: 'dev-mode — Vite HTTP endpoint, no CDP connection',\n },\n nextRecommendedAction: reachable\n ? null\n : 'mock state endpoint가 응답하지 않습니다. Vite dev 서버가 `mcp: true` 옵션으로 실행 중인지 확인하고, 필요하면 dev 서버를 재시작하세요.',\n };\n}\n\n/**\n * Builds the `measure_safe_area` dev-mode response from mock state.\n * Reads `safeAreaInsets` from the AIT mock state and returns a parity-schema\n * result with `source: 'mock-vite'`.\n */\nasync function buildDevMeasureSafeArea(aitSource: AitSource): Promise<Record<string, unknown>> {\n const state = await aitSource.get('AIT.getMockState');\n const raw = state as Record<string, unknown>;\n\n // Extract safeAreaInsets from the mock state.\n const rawInsets = raw.safeAreaInsets;\n let sdkInsets: { top: number; right: number; bottom: number; left: number } | null = null;\n if (rawInsets !== null && typeof rawInsets === 'object' && !Array.isArray(rawInsets)) {\n const r = rawInsets as Record<string, unknown>;\n sdkInsets = {\n top: typeof r.top === 'number' ? r.top : 0,\n right: typeof r.right === 'number' ? r.right : 0,\n bottom: typeof r.bottom === 'number' ? r.bottom : 0,\n left: typeof r.left === 'number' ? r.left : 0,\n };\n }\n\n return {\n source: 'mock-vite',\n // CSS env() vars are not available from the server side — report zeros.\n cssEnv: { top: 0, right: 0, bottom: 0, left: 0 },\n sdkInsets,\n sdkInsetsSource: sdkInsets !== null ? 'window.__ait' : null,\n ...(sdkInsets === null\n ? { sdkInsetsError: 'window.__ait.state.safeAreaInsets not found in mock state snapshot' }\n : {}),\n // Viewport geometry is not available from server side.\n innerWidth: null,\n innerHeight: null,\n devicePixelRatio: null,\n userAgent: null,\n navBarHeight: null,\n navBarHeightSource: 'not-available-in-dev-mode',\n };\n}\n\n/**\n * Builds the `call_sdk` dev-mode response.\n *\n * Supported methods are served from the mock state snapshot. Unsupported\n * methods return `{ ok: false, error: 'dev-mode-unsupported: ...' }` so the\n * agent gets an informative message rather than a generic failure.\n */\nasync function buildDevCallSdk(\n methodName: string,\n aitSource: AitSource,\n): Promise<Record<string, unknown>> {\n switch (methodName) {\n case 'getOperationalEnvironment': {\n const env = await aitSource.get('AIT.getOperationalEnvironment');\n return {\n ok: true,\n value: env.environment,\n };\n }\n default: {\n // For methods not readable from mock state, return a structured error.\n return {\n ok: false,\n error:\n `dev-mode-unsupported: \"${methodName}\"은 dev-mode에서 직접 호출할 수 없습니다. ` +\n 'CDP bridge(window.__sdkCall)가 없으므로 실제 SDK 호출은 `--mode=local` 또는 ' +\n 'debug 모드에서만 가능합니다. ' +\n '지원 메서드: getOperationalEnvironment (mock state에서 읽음).',\n };\n }\n }\n}\n\n/** Builds the dev-mode MCP server (does not connect a transport). */\nexport function createDevServer(deps: CreateDevServerDeps = {}): Server {\n const devtoolsUrl = process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n const stateEndpoint = `${devtoolsUrl}/api/ait-devtools/state`;\n const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });\n\n const server = new Server(\n { name: 'ait-devtools', version: __VERSION__ },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, () => ({\n tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n if (!DEV_TOOL_NAMES.has(name)) {\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n\n // CDP-only tools — tier-filter error with mode-switch hint.\n if (CDP_ONLY_TOOL_NAMES.has(name)) {\n return mcpError(`${name}: ${CDP_UNAVAILABLE_IN_DEV_MODE}`);\n }\n\n // Tier B tools (relay-only) — return a tier-filter error with a hand-off\n // hint toward --mode=debug so the station 2 → 3 seam is explicit.\n // (issue #323, Option B: list Tier B in dev tools/list + reject on call)\n if (TIER_B_TOOL_NAMES.has(name)) {\n return tierRejectionError(\n name,\n 'relay',\n 'mock',\n 'dev-mode — Vite HTTP endpoint, no CDP/relay connection. ' +\n '`--mode=debug` (または `devtools-mcp` without --mode) + MCP_ENV=relay로 재시작하세요.',\n );\n }\n\n try {\n // `devtools_get_mock_state` is an alias of `AIT.getMockState`.\n const effective = name === 'devtools_get_mock_state' ? 'AIT.getMockState' : name;\n\n // AIT.* tools backed by HTTP mock-state endpoint.\n if (isAitToolName(effective)) {\n switch (effective) {\n case 'AIT.getMockState':\n return jsonResult(await getMockState(aitSource));\n case 'AIT.getOperationalEnvironment':\n return jsonResult(await getOperationalEnvironment(aitSource));\n case 'AIT.getSdkCallHistory':\n return jsonResult(await getSdkCallHistory(aitSource));\n default:\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n }\n\n // Unified-surface tools (issue #305 shims).\n // Responses are wrapped in ToolEnvelope (issue #322) so agents use the\n // same {ok, data, meta} parser regardless of dev vs debug mode.\n switch (name) {\n case 'list_pages':\n return envelopeResult('list_pages', buildDevListPagesResult(devtoolsUrl));\n\n case 'get_debug_status':\n return envelopeResult(\n 'get_debug_status',\n await buildDevDiagnostics(devtoolsUrl, stateEndpoint, (url) => fetch(url)),\n );\n\n case 'measure_safe_area':\n return envelopeResult('measure_safe_area', await buildDevMeasureSafeArea(aitSource));\n\n case 'call_sdk': {\n const sdkName = request.params.arguments?.name;\n if (typeof sdkName !== 'string' || sdkName === '') {\n return mcpError(\n 'call_sdk: name 인자가 비어 있습니다. 호출할 메서드 이름을 전달하세요.',\n );\n }\n return envelopeResult('call_sdk', await buildDevCallSdk(sdkName, aitSource));\n }\n\n default:\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return mcpError(\n `${name} 실패: ${message}\\n` +\n 'Vite dev 서버가 @ait-co/devtools unplugin `mcp: true` 옵션으로 실행 중인지 확인하세요. ' +\n 'AIT_DEVTOOLS_URL 환경변수가 올바르게 설정됐는지도 확인하세요.',\n );\n }\n });\n\n return server;\n}\n\nfunction jsonResult(value: unknown) {\n return { content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] };\n}\n\n/**\n * Wraps `value` in a `ToolEnvelope` (when compat mode is off) and returns it\n * as a text content block. In dev-mode `env` is always `'mock'` and\n * `attached` is always `true` (the Vite dev server is the single implicit\n * \"attached\" page).\n *\n * When `AIT_MCP_COMPAT=chrome-devtools` the envelope is skipped and the raw\n * value is returned — identical to `jsonResult` (0.1.x back-compat).\n */\nfunction envelopeResult(tool: string, value: unknown) {\n const wrapped = wrapEnvelope(value, { tool, env: 'mock', attached: true });\n return { content: [{ type: 'text' as const, text: JSON.stringify(wrapped, null, 2) }] };\n}\n\n/** Builds the dev-mode server and connects it over stdio. */\nexport async function runDevServer(): Promise<void> {\n const server = createDevServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n","/**\n * `devtools-mcp` bin entry.\n *\n * Single bin, two modes selected by `--mode` and one target selected by\n * `--target`:\n *\n * --mode=debug (default)\n * --target=relay (default) — CDP/Chii relay + cloudflared quick tunnel.\n * Attach a running mini-app (real Toss WebView, env 3/4) and read its\n * console + network over CDP without a human watching a phone.\n * --target=local — CDP direct-attach to a local Chromium launched by the\n * MCP server (env 1). No relay or tunnel; the browser is launched\n * pointing at AIT_DEVTOOLS_URL (default http://localhost:5173).\n * --target=mobile — CDP attach to an EXTERNAL Chii relay the unplugin\n * already brought up for the env-2 real-device PWA (`tunnel: { cdp: true }`),\n * exposed via AIT_RELAY_BASE_URL. The MCP starts no relay or tunnel; it\n * only opens a CDP client against that external relay (issue #378).\n *\n * --mode=dev — dev mode — reads the live browser mock state from a running\n * Vite dev server (the devtools#130 `devtools_get_mock_state` surface).\n *\n * Back-compat (issue #348): the legacy `--mode`/`--target` flags and `MCP_ENV`\n * still work. `--target=relay`/`local` select the initial active connection;\n * the in-session `start_debug(mode)` MCP tool can then flip between them with no\n * restart. `MCP_ENV=relay-live` seeds LIVE intent at boot (deprecated alias);\n * `MCP_ENV=mock|relay|relay-dev` are accepted and ignored for env derivation\n * (the active connection's `kind` is authoritative).\n *\n * Node-only stdio process.\n */\n\nimport { realpathSync } from 'node:fs';\nimport { argv } from 'node:process';\nimport { fileURLToPath } from 'node:url';\nimport { runDebugServer, runLocalDebugServer, runMobileDebugServer } from './debug-server.js';\nimport { setLiveIntent } from './environment.js';\nimport { runDevServer } from './server.js';\n\n/**\n * Seeds the module-level `liveIntent` bit from the deprecated `MCP_ENV` alias\n * (issue #348). `MCP_ENV=relay-live` is the only value that matters now — it\n * arms LIVE intent at boot so a session launched straight into env 4 has the\n * guard active without a `start_debug({ mode: 'relay-live' })` round-trip. All\n * other `MCP_ENV` values (`mock`, `relay`, `relay-dev`) are accepted-and-ignored\n * for env derivation — the active connection's `kind` is authoritative.\n *\n * SECRET-HANDLING: reads only the env-var string; never logs a secret.\n */\nexport function seedLiveIntentFromEnv(env: NodeJS.ProcessEnv = process.env): void {\n if (env.MCP_ENV === 'relay-live') setLiveIntent(true);\n}\n\ntype Mode = 'debug' | 'dev';\ntype Target = 'relay' | 'local' | 'mobile';\n\n/**\n * Returns `true` when `--force` or `--takeover` is present in argv.\n *\n * Both flags are accepted as aliases — `--force` is the short form listed in\n * the `--help` output; `--takeover` is a longer synonym.\n */\nexport function parseForce(argv: readonly string[]): boolean {\n return argv.includes('--force') || argv.includes('--takeover');\n}\n\n/** Parses `--mode=<value>` / `--mode <value>` from argv; default `debug`. */\nexport function parseMode(argv: readonly string[]): Mode {\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === undefined) continue;\n if (arg.startsWith('--mode=')) {\n return normalizeMode(arg.slice('--mode='.length));\n }\n if (arg === '--mode') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw new Error(\"--mode requires a value: 'debug' (default) or 'dev'.\");\n }\n return normalizeMode(next);\n }\n }\n return 'debug';\n}\n\n/**\n * Parses `--target=<value>` / `--target <value>` from argv; default `relay`.\n *\n * Only meaningful when `--mode=debug`:\n * - `relay` — phone/WebView attach over Chii relay + cloudflared tunnel (env 3/4).\n * - `local` — local Chromium CDP attach (env 1, no relay needed).\n * - `mobile` — CDP attach to an EXTERNAL relay (env 2 PWA, AIT_RELAY_BASE_URL).\n */\nexport function parseTarget(argv: readonly string[]): Target {\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === undefined) continue;\n if (arg.startsWith('--target=')) {\n return normalizeTarget(arg.slice('--target='.length));\n }\n if (arg === '--target') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw new Error(\"--target requires a value: 'relay' (default), 'local', or 'mobile'.\");\n }\n return normalizeTarget(next);\n }\n }\n return 'relay';\n}\n\nfunction normalizeMode(value: string): Mode {\n if (value === 'dev') return 'dev';\n if (value === 'debug') return 'debug';\n throw new Error(`Unknown --mode '${value}'. Expected 'debug' (default) or 'dev'.`);\n}\n\nfunction normalizeTarget(value: string): Target {\n if (value === 'relay') return 'relay';\n if (value === 'local') return 'local';\n if (value === 'mobile') return 'mobile';\n throw new Error(`Unknown --target '${value}'. Expected 'relay' (default), 'local', or 'mobile'.`);\n}\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n // Back-compat (issue #348): `MCP_ENV=relay-live` seeds LIVE intent at boot.\n seedLiveIntentFromEnv();\n const mode = parseMode(args);\n if (mode === 'dev') {\n await runDevServer();\n } else {\n const target = parseTarget(args);\n const force = parseForce(args);\n if (target === 'local') {\n await runLocalDebugServer({ force });\n } else if (target === 'mobile') {\n await runMobileDebugServer({ force });\n } else {\n await runDebugServer({ force });\n }\n }\n}\n\n/**\n * True when this file is the process entry (the bin), not an import.\n *\n * `argv[1]` is whatever path the OS used to launch node — under `npx`/npm's\n * bin shim that's the symlink in `node_modules/.bin/` (or a wrapper), whereas\n * `import.meta.url` resolves to the realpath inside the package. Comparing\n * the two raw paths gives a false negative on every install that goes through\n * a bin shim — exactly the dominant path for `npx -y @ait-co/devtools\n * devtools-mcp`. Resolve `argv[1]` to its realpath before comparing.\n */\nfunction isEntrypoint(): boolean {\n const entry = argv[1];\n if (entry === undefined) return false;\n try {\n return fileURLToPath(import.meta.url) === realpathSync(entry);\n } catch {\n return false;\n }\n}\n\nif (isEntrypoint()) {\n main().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`[devtools-mcp] fatal: ${message}\\n`);\n process.exitCode = 1;\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,SAAgBA,aAAW,KAAsB;AAC/C,KAAI;AACF,UAAQ,KAAK,KAAK,EAAE;AACpB,SAAO;UACA,KAAc;AAGrB,MAAK,IAA8B,SAAS,QAAS,QAAO;AAC5D,SAAO;;;;;;;;;;;;;;;;;;;;;AA0BX,SAAgB,mBACd,YACA,MAOkB;CAClB,MAAM,EACJ,aAAa,KACb,cAAc,QAAQ,MACtB,UAAUA,cACV,gBAAgB,QAAQ,MACxB,OAAO,QAAgB,QAAQ,OAAO,MAAM,IAAI,KAC9C,QAAQ,EAAE;AAId,KAAI,eAAe,GAAG;AACpB,MAAI,2EAA2E;AAC/E,SAAO,EAAE,OAAO,IAAI;;CAGtB,IAAI,QAAQ;CAEZ,MAAM,SAAS,kBAAkB;AAC/B,MAAI,MAAO;EAEX,MAAM,cAAc,SAAS;AAG7B,MAFiB,gBAAgB,eAAe,CAAC,QAAQ,YAAY,EAEvD;AACZ,WAAQ;AACR,iBAAc,OAAO;AACrB,OACE,8CAA8C,YAAY,wBAAwB,YAAY,qBAC/F;AACD,eAAY;;IAEb,WAAW;AAEd,QAAO,EACL,OAAO;AACL,gBAAc,OAAO;IAExB;;;;;;;;;;;;;;;;;;;;AAyBH,SAAgB,oBACd,WACA,OAII,EAAE,EACY;CAClB,MAAM,EACJ,WAAW,MAAS,KAAK,KACzB,aAAa,KACb,YAAY,KAAK,KAAK,KACpB;CAEJ,MAAM,YAAY,KAAK;CACvB,IAAI,QAAQ;CAEZ,MAAM,SAAS,kBAAkB;AAC/B,MAAI,MAAO;AACX,MAAI,KAAK,GAAG,aAAa,UAAU;AACjC,WAAQ;AACR,iBAAc,OAAO;AACrB,cAAW;;IAEZ,WAAW;AAEd,QAAO,EACL,OAAO;AACL,gBAAc,OAAO;IAExB;;;;AC5HH,SAASC,WAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU;;;AAIhD,SAAS,iBAAiB,KAAiC;AACzD,KAAIA,WAAS,IAAI,IAAI,MAAM,QAAQ,IAAI,MAAM,CAC3C,QAAO,EAAE,OAAO,IAAI,OAAqC;AAE3D,QAAO,EAAE,OAAO,EAAE,EAAE;;;AAItB,SAAS,YAAY,KAA4B;AAC/C,QAAOA,WAAS,IAAI,GAAG,MAAM,EAAE;;;AAIjC,SAAS,yBAAyB,KAAyC;AAIzE,QAAO;EAAE,aAFPA,WAAS,IAAI,IAAI,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;EAErD,YADHA,WAAS,IAAI,IAAI,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;EACxD;;AAGpC,IAAa,gBAAb,MAAgD;CAC9C,YAAY,QAA2C;AAA1B,OAAA,SAAA;;CAE7B,MAAM,IAA6B,QAAqC;EACtE,MAAM,MAAM,MAAM,KAAK,OAAO,YAAY,OAAO;AAGjD,UAAQ,QAAR;GACE,KAAK,wBACH,QAAO,iBAAiB,IAAI;GAC9B,KAAK,mBACH,QAAO,YAAY,IAAI;GACzB,KAAK,gCACH,QAAO,yBAAyB,IAAI;GACtC,QACE,OAAM,IAAI,MAAM,uBAAuB,OAAO,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACvChE,MAAa,+BAA+B;;;;;;AAO5C,MAAa,2BAA2B;;;;;;;ACUxC,MAAM,eAAe,IAAI,IAAI;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;AASF,MAAM,kBAA4B;CAEhC;CAEA;CAEA;CAEA;CAEA;CACD;;;;;AAMD,SAAS,cAAc,OAAwB;AAC7C,QAAO,gBAAgB,MAAM,OAAO,GAAG,KAAK,MAAM,CAAC;;;;;;;AAQrD,SAAS,YAAY,OAAyB;AAC5C,KAAI,OAAO,UAAU,YAAY,cAAc,MAAM,CACnD,QAAO;AAET,QAAO;;;;;;;;;;AAWT,SAAS,aACP,OACA,OACA,QACyB;CACzB,MAAM,MAA+B;EACnC,qBAAI,IAAI,MAAM,EAAC,aAAa;EAC5B;EACA;EACD;AAED,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,EAAE;AACjD,MAAI,CAAC,aAAa,IAAI,IAAI,CAAE;AAE5B,MAAI,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,QAAS;AACxD,MAAI,OAAO,YAAY,MAAM;;AAG/B,QAAO;;;;;;AAOT,SAAS,SAAS,OAAiB,OAAiB,SAAkC,EAAE,EAAQ;CAC9F,MAAM,UAAU,aAAa,OAAO,OAAO,OAAO;AAClD,SAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,QAAQ,CAAC,IAAI;;;AAQtD,SAAgB,QAAQ,OAAiB,SAAkC,EAAE,EAAQ;AACnF,UAAS,QAAQ,OAAO,OAAO;;;AAIjC,SAAgB,QAAQ,OAAiB,SAAkC,EAAE,EAAQ;AACnF,UAAS,QAAQ,OAAO,OAAO;;;AAIjC,SAAgB,SAAS,OAAiB,SAAkC,EAAE,EAAQ;AACpF,UAAS,SAAS,OAAO,OAAO;;;;;;;;;;;;;;;;;;;;;;;;AC7HlC,MAAMC,wBAAsB;AA4B5B,SAASC,WAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU;;AAGhD,SAASC,eAAa,KAAuC;CAC3D,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AACN,SAAO;;AAET,KAAI,CAACD,WAAS,OAAO,CAAE,QAAO;CAC9B,MAAM,UAA6B,EAAE;AACrC,KAAI,OAAO,OAAO,OAAO,SAAU,SAAQ,KAAK,OAAO;AACvD,KAAI,OAAO,OAAO,WAAW,SAAU,SAAQ,SAAS,OAAO;AAC/D,KAAI,YAAY,OAAQ,SAAQ,SAAS,OAAO;AAChD,KAAI,YAAY,OAAQ,SAAQ,SAAS,OAAO;AAChD,KAAIA,WAAS,OAAO,MAAM,IAAI,OAAO,OAAO,MAAM,YAAY,SAC5D,SAAQ,QAAQ,EAAE,SAAS,OAAO,MAAM,SAAS;AAEnD,QAAO;;AAGT,MAAME,mBAA0C;CAC9C;CACA;CACA;CACD;;;;;;;;;;;;;;;AAgBD,MAAM,wBAAwB;;AAwB9B,MAAM,6BAA6B;;;;;AAMnC,IAAa,oBAAb,MAAwD;;CAEtD,OAAgB;CAEhB;CACA;CACA;CACA;CACA,UAA2B,IAAI,cAAc;CAC7C,0BAA2B,IAAI,KAA8B;CAC7D,0BAA2B,IAAI,KAAwB;CAEvD,KAA+B;CAC/B,kBAAiE;CACjE,gBAAwB;;;;;;CAMxB,iBAAwC;;CAExC,kBAAgD;;CAEhD,0BAA2B,IAAI,KAG5B;;;;;CAMH,sBAA6C;;;;;CAM7C,mCAAoC,IAAI,KAAqB;;CAG7D,kBAAiE;;CAGjE,qBAAoF,EAAE;CAEtF,YAAY,SAAmC;AAC7C,OAAK,eAAe,QAAQ,aAAa,QAAQ,OAAO,GAAG;AAC3D,OAAK,aAAa,QAAQ,cAAcH;AACxC,OAAK,aAAa,QAAQ;EAC1B,MAAM,QAAQ,QAAQ,IAAI,6BACtB,OAAO,QAAQ,IAAI,2BAA2B,GAC9C,KAAA;AACJ,OAAK,oBACF,UAAU,KAAA,KAAa,OAAO,SAAS,MAAM,IAAI,QAAQ,IAAI,QAAQ,KAAA,MACtE,QAAQ,oBACR;AACF,OAAK,MAAM,SAASG,iBAAgB,MAAK,QAAQ,IAAI,OAAO,EAAE,CAAC;AAG/D,OAAK,QAAQ,IAAI,2BAA2B,EAAE,CAAC;AAG/C,OAAK,QAAQ,gBAAgB,EAAE;;;CAIjC,MAAM,iBAAuC;EAO3C,IAAI,aAAa,GAAG,KAAK,aAAa;AACtC,MAAI,KAAK,YAAY;GACnB,MAAM,OAAO,aAAa,KAAK,WAAW;AAC1C,iBAAc,OAAO,mBAAmB,KAAK;;EAE/C,MAAM,MAAM,MAAM,MAAM,WAAW;AACnC,MAAI,CAAC,IAAI,GACP,OAAM,IAAI,MAAM,qCAAqC,IAAI,OAAO,GAAG,IAAI,aAAa;EAEtF,MAAM,OAAgB,MAAM,IAAI,MAAM;EACtC,MAAM,OAAOF,WAAS,KAAK,IAAI,MAAM,QAAQ,KAAK,QAAQ,GAAG,KAAK,UAAU,EAAE;EAM9E,IAAI,iBAAgC;AACpC,OAAK,MAAM,QAAQ,MAAM;AACvB,OAAI,CAACA,WAAS,KAAK,IAAI,OAAO,KAAK,OAAO,SAAU;AACpD,oBAAiB,KAAK;;AAIxB,MACE,mBAAmB,QACnB,KAAK,mBAAmB,QACxB,mBAAmB,KAAK,gBACxB;GACA,MAAM,SAAS,KAAK;AAGpB,WAAQ,iBAAiB,EAAE,cAAc,QAAQ,CAAC;AAClD,QAAK,YAAY,OAAO;;AAI1B,OAAK,QAAQ,OAAO;AACpB,OAAK,MAAM,QAAQ,MAAM;AACvB,OAAI,CAACA,WAAS,KAAK,IAAI,OAAO,KAAK,OAAO,SAAU;AAEpD,OAAI,KAAK,OAAO,eAAgB;AAChC,QAAK,QAAQ,IAAI,KAAK,IAAI;IACxB,IAAI,KAAK;IACT,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;IACrD,KAAK,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;IAChD,CAAC;;AAGJ,MAAI,mBAAmB,KACrB,MAAK,iBAAiB;MAEtB,MAAK,iBAAiB;EAGxB,MAAM,SAAS,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC;AAIzC,MAAI,mBAAmB,KACrB,MAAK,QAAQ,KAAK,mBAAmB,OAAO;AAG9C,SAAO;;CAGT,cAA2B;AACzB,SAAO,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;CAwBnC,mBACE,UACA,YAAY,KACZ,iBAAiB,KACK;EAEtB,MAAM,UAAU,KAAK,aAAa;AAClC,MAAI,SAAS,QAAQ,CAAE,QAAO,QAAQ,QAAQ,QAAQ;AAEtD,SAAO,IAAI,SAAsB,SAAS,WAAW;GACnD,IAAI,UAAU;GACd,IAAI,aAAoD;GAExD,MAAM,UAAU,YAA+B;AAC7C,QAAI,QAAS;AACb,cAAU;AACV,iBAAa,cAAc;AAC3B,QAAI,eAAe,MAAM;AACvB,mBAAc,WAAW;AACzB,kBAAa;;AAEf,SAAK,QAAQ,IAAI,mBAAmB,SAAS;AAC7C,YAAQ,QAAQ;;GAGlB,MAAM,YAAY,YAA+B;AAC/C,QAAI,SAAS,QAAQ,CAAE,QAAO,QAAQ;;GAGxC,MAAM,gBAAgB,iBAAiB;AACrC,QAAI,QAAS;AACb,cAAU;AACV,QAAI,eAAe,MAAM;AACvB,mBAAc,WAAW;AACzB,kBAAa;;AAEf,SAAK,QAAQ,IAAI,mBAAmB,SAAS;AAC7C,2BACE,IAAI,MACF,6BAA6B,UAAU,iCACxC,CACF;MACA,UAAU;AAGb,QAAK,QAAQ,GAAG,mBAAmB,SAAS;AAM5C,gBAAa,kBAAkB;AAE7B,SAAK,gBAAgB,CAAC,MACnB,YAAY;AACX,SAAI,SAAS,QAAQ,CAAE,QAAO,QAAQ;aAElC,GAGP;MACA,eAAe;IAClB;;;;;;CAOJ,yBAAwC;AACtC,SAAO,KAAK;;;;;;CAOd,oBAAoB,UAAiC;AACnD,SAAO,KAAK,iBAAiB,IAAI,SAAS,IAAI;;;CAIhD,YAAY,UAA6D;AACvE,OAAK,mBAAmB,KAAK,SAAS;AACtC,eAAa;GACX,MAAM,MAAM,KAAK,mBAAmB,QAAQ,SAAS;AACrD,OAAI,QAAQ,GAAI,MAAK,mBAAmB,OAAO,KAAK,EAAE;;;;;;;CAQ1D,MAAM,gBAA+B;AACnC,MAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAAM;AAGtD,MAAI,KAAK,gBAAiB,QAAO,KAAK;AACtC,OAAK,kBAAkB,KAAK,kBAAkB,CAAC,cAAc;AAC3D,QAAK,kBAAkB;IACvB;AACF,SAAO,KAAK;;CAGd,MAAc,mBAAkC;EAE9C,MAAM,UADU,MAAM,KAAK,gBAAgB,EACpB;AACvB,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,mDAAmD;EAKrE,IAAI,YAAY,GAFD,KAAK,aAAa,QAAQ,SAAS,KAAK,CAE7B,UADT,gBAAgB,KAAK,KAAK,GACE,UAAU,mBAAmB,OAAO,GAAG;AAOpF,MAAI,KAAK,YAAY;GACnB,MAAM,OAAO,aAAa,KAAK,WAAW;AAC1C,gBAAa,OAAO,mBAAmB,KAAK;;EAE9C,MAAM,KAAK,IAAI,UAAU,UAAU;AACnC,OAAK,KAAK;AAEV,QAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,MAAG,KAAK,cAAc,SAAS,CAAC;AAChC,MAAG,KAAK,UAAU,QAAe,OAAO,IAAI,CAAC;AAO7C,MAAG,KAAK,UAAU,SAAiB;AACjC,QAAI,SAAA,KACF,wBACE,IAAI,MACF,4EACD,CACF;KAEH;IACF;AAGF,OAAK,sBAAsB;AAC3B,OAAK,iBAAiB,OAAO;AAG7B,OAAK,kBAAkB;AACvB,KAAG,GAAG,YAAY,SAA4B,KAAK,cAAc,KAAK,UAAU,CAAC,CAAC;AAKlF,KAAG,GAAG,UAAU,SACd,KAAK,iBACH,SAAA,OACI,gDACA,4BACL,CACF;AACD,KAAG,GAAG,UAAU,QAAe,KAAK,iBAAiB,uBAAuB,IAAI,UAAU,CAAC;AAE3F,OAAK,kBAAkB,iBAAiB;AACxC,OAAK,kBAAkB,iBAAiB;AAGxC,OAAK,kBAAkB,aAAa;AACpC,OAAK,kBAAkB,cAAc;AAIrC,OAAK,kBAAkB,mBAAmB;AAC1C,OAAK,kBAAkB,6BAA6B,EAAE,UAAU,MAAM,CAAC;AAGvE,OAAK,eAAe,OAAO,GAAG;;;CAIhC,kBAA0B,QAAgB,SAAkC,EAAE,EAAQ;AACpF,MAAI,CAAC,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAAM;EACvD,MAAM,KAAK,KAAK;AAChB,OAAK,GAAG,KAAK,KAAK,UAAU;GAAE;GAAI;GAAQ;GAAQ,CAAC,CAAC;;;;;;CAOtD,KACE,QACA,QACqC;AACrC,SAAO,KAAK,YAAY,QAAS,UAAU,EAAE,CAA6B;;;;;;;;;;;;;;;CAkB5E,YAAY,QAAgB,SAAkC,EAAE,EAAoB;AAElF,MAAI,KAAK,oBAAoB,eAC3B,QAAO,QAAQ,uBACb,IAAI,MACF,wBAAwB,OAAO,yDAChC,CACF;AAEH,MAAI,CAAC,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAC/C,QAAO,QAAQ,uBACb,IAAI,MAAM,+EAA+E,CAC1F;EAEH,MAAM,KAAK,KAAK;EAChB,MAAM,KAAK,KAAK;EAChB,MAAM,YAAY,KAAK;AACvB,SAAO,IAAI,SAAkB,SAAS,WAAW;GAC/C,MAAM,SAAS,iBAAiB;AAC9B,SAAK,QAAQ,OAAO,GAAG;AACvB,2BACE,IAAI,MACF,qBAAqB,OAAO,IAAI,UAAU,iFAG3C,CACF;MACA,UAAU;AACb,QAAK,QAAQ,IAAI,IAAI;IACnB,UAAU,MAAM;AACd,kBAAa,OAAO;AACpB,aAAQ,EAAE;;IAEZ,SAAS,MAAM;AACb,kBAAa,OAAO;AACpB,YAAO,EAAE;;IAEZ,CAAC;AACF,MAAG,KAAK,KAAK,UAAU;IAAE;IAAI;IAAQ;IAAQ,CAAC,CAAC;IAC/C;;;;;;;CAQJ,iBAAyB,QAAsB;AAC7C,MAAI,KAAK,oBAAoB,eAAgB;AAC7C,OAAK,kBAAkB;AACvB,OAAK,KAAK;AACV,OAAK,eAAe;EACpB,MAAM,sBAAM,IAAI,MACd,GAAG,OAAO,wDACX;AACD,OAAK,MAAM,UAAU,KAAK,QAAQ,QAAQ,CACxC,QAAO,OAAO,IAAI;AAEpB,OAAK,QAAQ,OAAO;;;;;;;;;;CAWtB,YAAoB,UAAwB;EAC1C,MAAM,8BAAa,IAAI,MAAM,EAAC,aAAa;AAC3C,OAAK,QAAQ,OAAO,SAAS;AAC7B,OAAK,iBAAiB,OAAO,SAAS;EAEtC,MAAM,sBAAM,IAAI,MACd,+EAA+E,SAAS,qCAEzF;AACD,OAAK,MAAM,UAAU,KAAK,QAAQ,QAAQ,CACxC,QAAO,OAAO,IAAI;AAEpB,OAAK,QAAQ,OAAO;EAEpB,MAAM,QAA8B;GAAE,MAAM;GAAY;GAAU;GAAY;AAC9E,OAAK,MAAM,YAAY,KAAK,mBAC1B,KAAI;AACF,YAAS,MAAM;UACT;;;;;;;;;;;CAeZ,iBAAyB,MAAoC,UAA+B;EAC1F,MAAM,8BAAa,IAAI,MAAM,EAAC,aAAa;AAC3C,OAAK,sBAAsB,KAAK,KAAK;AAGrC,MAAI,aAAa,MAAM;AACrB,QAAK,QAAQ,OAAO,SAAS;AAC7B,QAAK,iBAAiB,OAAO,SAAS;AAEtC,OAAI,KAAK,mBAAmB,SAC1B,MAAK,iBAAiB;SAEnB;AAEL,QAAK,QAAQ,OAAO;AACpB,QAAK,iBAAiB,OAAO;AAC7B,QAAK,iBAAiB;;EAUxB,MAAM,sBAAM,IAAI,MACd,eANA,SAAS,YACL,yCACA,SAAS,cACP,uCACA,4CAEe,iFAEtB;AACD,OAAK,MAAM,UAAU,KAAK,QAAQ,QAAQ,CACxC,QAAO,OAAO,IAAI;AAEpB,OAAK,QAAQ,OAAO;EAGpB,MAAM,QAA8B;GAAE;GAAM;GAAU;GAAY;AAClE,OAAK,MAAM,YAAY,KAAK,mBAC1B,KAAI;AACF,YAAS,MAAM;UACT;;;;;;;;;;;;;;;;CAoBZ,eAAuB,iBAA+B;AACpD,OAAK,eAAe;EAEpB,MAAM,QAAQ,QAAQ,IAAI,uBACtB,OAAO,QAAQ,IAAI,qBAAqB,GACxC,KAAA;AACJ,MAAI,UAAU,KAAA,KAAa,CAAC,OAAO,SAAS,MAAM,IAAI,SAAS,EAAG;EAElE,MAAM,kBAAkB;AAExB,OAAK,kBAAkB,kBAAkB;GAEvC,MAAM,YAAY,KAAK,QAAQ,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,MAAM,CAAC,GAAG,CAAC,gBAAgB;AACtF,QAAK,MAAM,YAAY,WAAW;IAEhC,MAAM,cAAc,KAAK,YAAY,oBAAoB;KACvD,YAAY;KACZ,eAAe;KACf,SAAS;KACV,CAAC;IACF,MAAM,iBAAiB,IAAI,SAAgB,GAAG,WAC5C,iBACQ,uBAAO,IAAI,MAAM,oBAAoB,CAAC,EAC5C,kBAAkB,IACnB,CACF;AACD,YAAQ,KAAK,CAAC,aAAa,eAAe,CAAC,CAAC,YAAY;AAEtD,SAAI,KAAK,QAAQ,IAAI,SAAS,CAC5B,MAAK,iBAAiB,aAAa,SAAS;MAE9C;;KAEH,MAAM;;CAGX,gBAA8B;AAC5B,MAAI,KAAK,oBAAoB,MAAM;AACjC,iBAAc,KAAK,gBAAgB;AACnC,QAAK,kBAAkB;;;CAI3B,cAAsB,KAAmB;EACvC,MAAM,UAAUC,eAAa,IAAI;AACjC,MAAI,CAAC,QAAS;AAGd,MAAI,OAAO,QAAQ,OAAO,YAAY,KAAK,QAAQ,IAAI,QAAQ,GAAG,EAAE;GAClE,MAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ,GAAG;AAC3C,QAAK,QAAQ,OAAO,QAAQ,GAAG;AAC/B,OAAI,OACF,KAAI,QAAQ,MAAO,QAAO,OAAO,IAAI,MAAM,QAAQ,MAAM,QAAQ,CAAC;OAC7D,QAAO,QAAQ,QAAQ,OAAO;AAErC;;EAQF,MAAM,MAAM,KAAK,KAAK;EACtB,IAAI,mBAAmB;AACvB,OAAK,MAAM,YAAY,KAAK,QAAQ,MAAM,EAAE;AAC1C,OAAI,CAAC,KAAK,iBAAiB,IAAI,SAAS,CACtC,oBAAmB;AAErB,QAAK,iBAAiB,IAAI,UAAU,IAAI;;AAE1C,MAAI,oBAAoB,KAAK,QAAQ,OAAO,EAC1C,MAAK,QAAQ,KAAK,mBAAmB,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC,CAAC;AAGlE,MAAI,OAAO,QAAQ,WAAW,SAAU;AAMxC,MAAI,QAAQ,WAAW,2BAA2B;AAChD,QAAK,iBAAiB,WAAW,KAAK;AACtC;;AAIF,MAAI,QAAQ,WAAW,0BAA0B;GAC/C,MAAM,WACJD,WAAS,QAAQ,OAAO,IAAI,OAAO,QAAQ,OAAO,aAAa,WAC3D,QAAQ,OAAO,WACf;AACN,QAAK,iBAAiB,aAAa,SAAS;AAC5C;;AAIF,MAAI,QAAQ,WAAW,6BAA6B;GAClD,MAAM,WACJA,WAAS,QAAQ,OAAO,IAAI,OAAO,QAAQ,OAAO,aAAa,WAC3D,QAAQ,OAAO,WACf;AACN,QAAK,iBAAiB,YAAY,SAAS;AAC3C;;AAIF,MAAI,CAAC,KAAK,QAAQ,IAAI,QAAQ,OAAuB,CAAE;EACvD,MAAM,QAAQ,QAAQ;EACtB,MAAM,SAAS,KAAK,QAAQ,IAAI,MAAM;AACtC,MAAI,CAAC,OAAQ;AACb,SAAO,KAAK,QAAQ,OAAO;EAG3B,MAAM,MAAM,UAAU,4BAA4B,wBAAwB,KAAK;AAC/E,MAAI,OAAO,SAAS,IAAK,QAAO,OAAO;AACvC,OAAK,QAAQ,KAAK,OAAO,QAAQ,OAAO;;CAG1C,kBAA0C,OAAyC;AAEjF,SADe,KAAK,QAAQ,IAAI,MAAM,IACpB,EAAE;;CAGtB,GAA2B,OAAU,UAAyD;AAC5F,OAAK,QAAQ,GAAG,OAAO,SAAuC;AAC9D,eAAa,KAAK,QAAQ,IAAI,OAAO,SAAuC;;;CAI9E,QAAc;EACZ,MAAM,KAAK,KAAK;AAChB,OAAK,eAAe;AAGpB,OAAK,iBAAiB,+BAA+B;AACrD,MAAI,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACvuBf,MAAMG,YAAU,cAAc,OAAO,KAAK,IAAI;;;;;;;;AAS9C,MAAM,gCAAgC;;;;;;;;AAsBtC,SAAS,sBAA0D;AACjE,KAAI;EACF,MAAM,MAAeA,UAAQ,kCAAkC;AAC/D,MAAI,OAAO,QAAQ,WACjB,QAAO;SAEH;AAGR,QAAO;;;;;;;;;;;;;;;;;;;;;AAsBT,eAAe,qBACb,MACA,cACA,cACiC;AACjC,KAAI,iBAAiB,MAAM;AACzB,QAAM,KAAK,MAAM,aAAa;AAC9B,SAAO;;CAGT,IAAI,WAAmC;CACvC,MAAM,QAAQ,aAAa;CAC3B,MAAM,gBAAgB,MAAM;AAE5B,OAAM,QAAQ,SAAiC,QAAQ;AACrD,aAAW;AACX,SAAO,cAAc,KAAK,MAAM,OAAO;;AAGzC,KAAI;AACF,QAAM,KAAK,MAAM,aAAa;WACtB;AAER,QAAM,QAAQ;;AAGhB,QAAO;;AAcT,SAAS,iBAAmC;CAE1C,MAAM,MAAeA,UAAQ,OAAO;AACpC,KACE,OAAO,QAAQ,YACf,QAAQ,QACR,WAAW,OACX,OAAQ,IAA2B,UAAU,WAE7C,QAAO;AAET,OAAM,IAAI,MAAM,4CAA4C;;;;;;;;;;;;;;;;;;;;;AAyC9D,SAAgB,oBAAoB,QAA+B;CACjE,MAAM,QAAQ,oCAAoC,KAAK,OAAO;AAC9D,KAAI,UAAU,KAAM,QAAO;CAC3B,MAAM,OAAO,MAAM;CACnB,MAAM,OAAO,MAAM,OAAO,KAAA,KAAa,MAAM,OAAO,KAAK,MAAM,MAAM;CACrE,MAAM,QAAQ,MAAM,MAAM;AAE1B,QAAO,GAAG,OAAO,QADC,UAAU,KAAK,MAAM,IACJ,KAAK;;;;;;;;;;;;;;;;;;AA+E1C,eAAsB,eAAe,UAAiC,EAAE,EAAsB;CAC5F,MAAM,gBAAgB,QAAQ,QAAQ;CACtC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,EAAE,YAAY,iBAAiB;CACrC,MAAM,sBACJ,QAAQ,wBAAwB,KAAA,IAC5B,QAAQ,sBACR;CAEN,MAAM,aAAa,cAAc;CAIjC,MAAM,oBAAoB,SAA6C;AACrE,MAAI,iBAAiB,KAAA,EAAW;AAChC,MAAI;AACF,gBAAa,EAAE,MAAM,CAAC;UAChB;;AAiBV,KAAI,WAuBF,YAAW,GAAG,YAAY,KAAK,QAAQ;EACrC,MAAM,YAAY,oBAAoB,IAAI,OAAO,GAAG;AACpD,MAAI,cAAc,MAAM;AAEtB,OAAI,MAAM;AACV,OAAI,CAAC,WAAW,IAAI,EAAE;AAMpB,QAAI,aAAa;AACjB,QAAI,UAAU,+BAA+B,IAAI;AACjD,QAAI,UAAU,gBAAgB,mBAAmB;AACjD,QAAI,IAAI,KAAK,UAAU,EAAE,OAAO,0BAA0B,CAAC,CAAC;AAC5D,qBAAiB,eAAe;;AAIlC;;EAMF,MAAM,YAAY,IAAI,OAAO,IAAI,MAAM,IAAI,CAAC;AAC5C,MAAI,aAAa,cAAc,aAAa,aAAa;AAIvD,OAAI,CAAC,WAAW,IAAI,EAAE;AAEpB,QAAI,aAAa;AACjB,QAAI,UAAU,+BAA+B,IAAI;AACjD,QAAI,UAAU,gBAAgB,mBAAmB;AACjD,QAAI,IAAI,KAAK,UAAU,EAAE,OAAO,0BAA0B,CAAC,CAAC;AAC5D,qBAAiB,eAAe;AAEhC;;AAOF;;GAQF;CAgBJ,MAAM,eAAe,sBAAsB,IAAI,qBAAqB,GAAG;CACvE,MAAM,kBAAkB,MAAM,qBAC5B,gBAAgB,EAChB;EAAE,QAAQ;EAAY,QAAQ,GAAG,KAAK,GAAG;EAAiB,MAAM;EAAe,EAC/E,aACD;AAUD,KAAI,YAAY;EACd,MAAM,uBAAuB,WAAW,UAAU,UAAU;AAG5D,aAAW,mBAAmB,UAAU;EAGxC,MAAM,YAAY,IAAI,gBAAgB,EAAE,UAAU,MAAM,CAAC;AACzD,aAAW,GAAG,YAAY,KAAsB,QAAgB,SAAiB;GAK/E,MAAM,YAAY,oBAAoB,IAAI,OAAO,GAAG;AACpD,OAAI,cAAc,KAChB,KAAI,MAAM;AAEZ,OAAI,CAAC,WAAW,IAAI,EAAE;AAOpB,cAAU,cAAc,KAAK,QAAQ,OAAO,OAAO;AACjD,QAAG,MAAM,8BAA8B,yBAAyB;MAChE;AACF,qBAAiB,aAAa;AAE9B;;AAIF,QAAK,MAAM,YAAY,qBACrB,UAAS,KAAK,QAAQ,KAAK;IAE7B;;CAGJ,MAAM,aAAa,MAAM,IAAI,SAAiB,SAAS,WAAW;AAChE,aAAW,KAAK,SAAS,OAAO;AAChC,aAAW,OAAO,eAAe,YAAY;AAC3C,cAAW,IAAI,SAAS,OAAO;AAG/B,WADa,WAAW,SAAS,CACpB,KAAK;IAClB;GACF;CAYF,IAAI,kBAAyD;AAC7D,KAAI,sBAAsB,KAAK,oBAAoB,MAAM;EACvD,MAAM,UAAU;AAChB,oBAAkB,kBAAkB;AAClC,QAAK,MAAM,UAAU,QAAQ,KAAK,QAEhC,KAAI,OAAO,eAAe,EACxB,QAAO,MAAM;KAGhB,oBAAoB;;AAGzB,QAAO;EACL,MAAM;EACN,SAAS,UAAU,KAAK,GAAG;EAC3B,aACE,IAAI,SAAe,YAAY;AAC7B,OAAI,oBAAoB,MAAM;AAC5B,kBAAc,gBAAgB;AAC9B,sBAAkB;;AAEpB,cAAW,YAAY,SAAS,CAAC;IACjC;EACL;;;;;;;;;;;AClhBH,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoErB,SAAgB,uBACd,WACA,QACA,UACA,MACQ;CACR,IAAI,MACF,GAAG,aAAa,OAAO,mBAAmB,UAAU,CAAA,iBAClC,mBAAmB,OAAO;AAC9C,KAAI,aAAa,KAAA,KAAa,aAAa,GACzC,QAAO,OAAO,mBAAmB,SAAS;AAG5C,KAAI,MAAM,SAAS,KAAA,KAAa,KAAK,KAAK,MAAM,KAAK,GACnD,QAAO,SAAS,mBAAmB,KAAK,KAAK,MAAM,CAAC;AAEtD,KAAI,MAAM,SAAS,KAAA,GAAW;EAC5B,IAAI;AACJ,MAAI;AACF,gBAAa,IAAI,IAAI,KAAK,KAAK;UACzB;AACN,gBAAa;;AAEf,MAAI,YAAY,aAAa,SAC3B,QAAO,SAAS,mBAAmB,KAAK,KAAK;;AAKjD,KAAI,MAAM,cAAc,KACtB,QAAO;AAET,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CT,MAAM,yBAAyB,IAAI,IAAY;CAAC;CAAI;CAAO;CAAa;CAAa;CAAM,CAAC;;;;;;;;;;AAW5F,SAAgB,wBAAwB,WAAkC;CAIxE,MAAM,cAAc,UAAU,QAAQ,kCAAkC,GAAG;AAC3E,KAAI,gBAAgB,UAElB,QACE;CAMJ,MAAM,eAAe,YAAY,OAAO,QAAQ;CAChD,MAAM,YAAY,iBAAiB,KAAK,cAAc,YAAY,MAAM,GAAG,aAAa;AAExF,KAAI,uBAAuB,IAAI,UAAU,aAAa,CAAC,CAErD,QACE,wBAFuB,cAAc,KAAK,YAAY,IAAI,UAAU,GAE3B;AAM7C,QAAO;;AAMT,SAAS,cAAc,OAAe,KAAqB;AACzD,KAAI,UAAU,GAAI,QAAO;AACzB,QAAO,MACJ,MAAM,IAAI,CACV,QAAQ,SAAS,SAAS,MAAM,KAAK,MAAM,IAAI,CAAC,OAAO,IAAI,CAC3D,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;AAsBd,SAAgB,uBACd,WACA,QACA,UACQ;CACR,IAAI;AACJ,KAAI;AACF,UAAQ,IAAI,IAAI,OAAO;SACjB;AACN,QAAM,IAAI,MAAM,iCAAiC,SAAS;;AAE5D,KAAI,MAAM,aAAa,OACrB,OAAM,IAAI,MAAM,2CAA2C,MAAM,SAAS,IAAI,OAAO,GAAG;CAG1F,MAAM,YAAY,UAAU,QAAQ,IAAI;CACxC,MAAM,OAAO,cAAc,KAAK,KAAK,UAAU,MAAM,UAAU;CAC/D,MAAM,aAAa,cAAc,KAAK,YAAY,UAAU,MAAM,GAAG,UAAU;CAE/E,MAAM,aAAa,WAAW,QAAQ,IAAI;CAC1C,MAAM,OAAO,eAAe,KAAK,aAAa,WAAW,MAAM,GAAG,WAAW;CAC7E,IAAI,QAAQ,eAAe,KAAK,KAAK,WAAW,MAAM,aAAa,EAAE;CAErE,MAAM,WAA0B,CAC9B,CAAC,SAAS,IAAI,EACd,CAAC,SAAS,OAAO,CAClB;AAID,KAAI,aAAa,KAAA,KAAa,aAAa,GACzC,UAAS,KAAK,CAAC,MAAM,SAAS,CAAC;AAKjC,SAAQ,cAAc,OAAO,KAAK;AAElC,MAAK,MAAM,CAAC,QAAQ,SAClB,SAAQ,cAAc,OAAO,IAAI;AAEnC,MAAK,MAAM,CAAC,KAAK,UAAU,UAAU;EACnC,MAAM,OAAO,GAAG,IAAI,GAAG,mBAAmB,MAAM;AAChD,UAAQ,UAAU,KAAK,OAAO,GAAG,MAAM,GAAG;;AAG5C,QAAO,GAAG,KAAK,GAAG,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9J5B,SAAgB,sBACd,kBACA,UACA,UACA,QAAwD,WACzC;AAKf,KAAI,CAAC,SACH,QAAO;CAST,IAAI;CACJ,IAAI;AACJ,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,iBAAiB;AACxC,cAAY,OAAO;AACnB,gBAAc,OAAO,aAAa,WAAW,QAAQ;SAC/C;AAEN,cAAY,iBAAiB,QAAQ,iBAAiB,GAAG;AACzD,gBAAc,WAAW,KAAK,iBAAiB,GAAG,QAAQ;;CAK5D,MAAM,WAAW,mBAAmB,KAAK,KAAK,CAAC,SAAS,GAAG;CAO3D,MAAM,OAAO,UAAU;CACvB,MAAM,SAAS,GAAG,UAAU,UAAU,SAAS,UAAU,mBAAmB,SAAS,CAAC,MAAM,mBAAmB,KAAK;CAEpH,MAAM,SAAS,IAAI,gBAAgB;GAAG,cAAc;EAAQ;EAAO,CAAC;AACpE,QAAO,GAAG,iBAAiB,QAAQ,OAAO,GAAG,CAAC,2BAA2B,OAAO,UAAU;;;;;;;;;;;;;AAkB5F,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;;;;;;;;;;;;;;;;;;AA6ET,IAAa,qBAAb,MAAgC;;CAE9B,iCAAkC,IAAI,KAAa;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BnD,KAAK,SAAoC;AACvC,MAAI,wBAAwB,CAAE;AAC9B,MAAI,QAAQ,QAAQ,OAAQ;AAC5B,MAAI,CAAC,QAAQ,SAAU;EAGvB,MAAM,WAAW,QAAQ;AACzB,MAAI,KAAK,eAAe,IAAI,SAAS,CAAE;AAGvC,MAAI,QAAQ,oBAAoB;AAC9B,QAAK,eAAe,IAAI,SAAS;GACjC,MAAM,YAAY,QAAQ;AAC1B,WAAQ,OAAO,MACb;6BACgC,UAAU,QAAQ,cAAc,GAAG,CAAC;EAErE;AAED,OAAI,CADW,iBAAiB,UAAU,CAExC,SAAQ,OAAO,MACb,+BAA+B,UAAU,qBAC1C;AAEH;;AAIF,MAAI,CAAC,QAAQ,iBAAkB;AAE/B,OAAK,eAAe,IAAI,SAAS;EAEjC,MAAM,eAAe,sBACnB,QAAQ,kBACR,UACA,QAAQ,SACT;AAKD,MAAI,iBAAiB,MAAM;AACzB,WAAQ,OAAO,MACb,6HAED;AACD;;AAGF,UAAQ,OAAO,MACb;4BAC+B,aAAa;;EAG7C;AAGD,MAAI,CADW,iBAAiB,aAAa,CAE3C,SAAQ,OAAO,MACb,sDACD;;;;;;;CASL,IAAI,SAAkB;AACpB,SAAO,KAAK,eAAe,OAAO;;;CAIpC,IAAI,gBAAqC;AACvC,SAAO,KAAK;;;;;;;;;ACnVhB,SAAgB,eAAwB;AACtC,QAAO,QAAQ,IAAI,mBAAmB;;;;;;;AAQxC,SAAgB,cAAc,KAAkC;AAC9D,QAAO;;;;;;;;;;;;;;;;;AA4BT,SAAgB,aAAgB,MAAS,KAA2C;AAClF,KAAI,cAAc,CAAE,QAAO;AAC3B,QAAO;EACL,IAAI;EACJ;EACA,MAAM;GACJ,MAAM,IAAI;GACV,KAAK,cAAc,IAAI,IAAI;GAC3B,UAAU,IAAI;GACd,aAAa,IAAI,eAAe;GACjC;EACF;;;;;;;;;;;;ACfH,SAAgB,WAAW,KAA8B;AACvD,SAAQ,KAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK,eACH,QAAO;EACT,KAAK,OACH,QAAO;;;;;;;;AASb,SAAgB,eAAe,KAA8B;AAC3D,QAAO,QAAQ;;;;;;;AAQjB,SAAgB,YAAY,KAAuC;AACjE,KAAI,QAAQ,OAAQ,QAAO;AAC3B,QAAO;;;;;;;;;;;;;;;;;;;;;AAsBT,SAAgB,kBACd,MACA,YACA,aACgB;AAChB,SAAQ,MAAR;EACE,KAAK,QACH,QAAO;EACT,KAAK;AACH,OAAI,WAAY,QAAO;AACvB,UAAO,gBAAgB,iBAAiB,iBAAiB;;;;;;;;;;;;;AAkB/D,IAAI,aAAa;;AAGjB,SAAgB,gBAAyB;AACvC,QAAO;;;;;;;AAQT,SAAgB,cAAc,OAAsB;AAClD,cAAa;;;;;;;;;AC/Jf,SAAgB,SAAS,SAAiC;AACxD,QAAO;EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM;GAAS,CAAC;EAC1C,SAAS;EACV;;;;;;;;;;;AAgBH,SAAgB,mBACd,UACA,aACA,YACA,QACgB;AAYhB,QAAO,SAAS,GAJd,GAAG,SAAS,IAPG,gBAAgB,UAAU,mBAAmB,iBAOnC,4BANN,eAAe,UAAU,UAAU,OAO/B,IAAI,OAAO,KALlC,gBAAgB,UACZ,yFACA,+CAMkB,MADT,QAAQ,SAAS,wBAAwB,YAAY,2BAA2B,WAAW,IAAI,OAAO,MAC9E;;;;;;;AAYzC,SAAgB,kBAAkC;AAChD,QAAO,SACL,6EAED;;;;;;;AAQH,SAAgB,iBAAiB,UAAmC;AAElE,QAAO,SACL,GAFa,WAAW,GAAG,SAAS,MAAM,GAEhC,6IAGX;;;;;;;;AASH,SAAgB,eAAe,UAAmC;AAEhE,QAAO,SACL,GAFa,WAAW,GAAG,SAAS,MAAM,GAEhC,qEAEX;;;;;;;;;;;;;;;;AAiBH,SAAgB,eAAe,UAAmB,UAAU,OAAuB;CACjF,MAAM,SAAS,WAAW,GAAG,SAAS,MAAM;AAC5C,KAAI,QACF,QAAO,SACL,GAAG,OAAO,wOAIX;AAEH,QAAO,SACL,GAAG,OAAO,uIAGX;;;;;;;;;;AAeH,SAAgB,eAAe,UAAkC;AAS/D,QAAO,SAPL,sBAAsB,SAAS;;;sCAGQ,SAAS;;;wDAI7B;;;;;AAUvB,SAAgB,qBAAqB,UAAmC;AAEtE,QAAO,SACL,GAFa,WAAW,GAAG,SAAS,MAAM,GAEhC,kEAEX;;;;;;;;;;AAeH,SAAgB,kBAAkB,KAAc,UAAkB,UAAU,OAAuB;CACjG,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAGhE,KAAI,QAAQ,WAAW,eAAe,IAAI,QAAQ,SAAS,eAAe,CACxE,QAAO,iBAAiB;AAM1B,KACE,QAAQ,WAAW,cAAc,IACjC,QAAQ,SAAS,wBAAwB,IACzC,QAAQ,SAAS,oCAAoC,IACpD,QAAQ,SAAS,YAAY,IAAI,QAAQ,SAAS,gBAAgB,CAEnE,QAAO,eAAe,UAAU,QAAQ;AAI1C,KACE,QAAQ,SAAS,yBAAyB,IAC1C,QAAQ,SAAS,gBAAgB,IACjC,QAAQ,SAAS,kBAAkB,IACnC,QAAQ,SAAS,qBAAqB,CAEtC,QAAO,eAAe,SAAS;AAIjC,KAAI,QAAQ,SAAS,sBAAsB,IAAI,QAAQ,SAAS,kBAAkB,CAChF,QAAO,qBAAqB,SAAS;AAIvC,QAAO,SACL,GAAG,SAAS,OAAO,QAAQ,4CAC5B;;;;;;;;;;;;;;;;;;;;;;ACtMH,MAAM,sBAAsB;AAW5B,SAASC,WAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU;;AAGhD,SAAS,aAAa,KAAuC;CAC3D,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AACN,SAAO;;AAET,KAAI,CAACA,WAAS,OAAO,CAAE,QAAO;CAC9B,MAAM,UAA6B,EAAE;AACrC,KAAI,OAAO,OAAO,OAAO,SAAU,SAAQ,KAAK,OAAO;AACvD,KAAI,OAAO,OAAO,WAAW,SAAU,SAAQ,SAAS,OAAO;AAC/D,KAAI,YAAY,OAAQ,SAAQ,SAAS,OAAO;AAChD,KAAI,YAAY,OAAQ,SAAQ,SAAS,OAAO;AAChD,KAAIA,WAAS,OAAO,MAAM,IAAI,OAAO,OAAO,MAAM,YAAY,SAC5D,SAAQ,QAAQ,EAAE,SAAS,OAAO,MAAM,SAAS;AAEnD,QAAO;;AAGT,MAAM,iBAA0C;CAC9C;CACA;CACA;CACD;;;;;;;;;;;;;AAqCD,IAAa,qBAAb,MAAyD;;CAEvD,OAAgB;CAEhB;CACA;CACA,UAA2B,IAAI,cAAc;CAC7C,0BAA2B,IAAI,KAA8B;CAC7D,0BAA2B,IAAI,KAAwB;CAEvD,KAA+B;CAC/B,gBAAwB;;CAExB,kBAAgD;;CAEhD,0BAA2B,IAAI,KAG5B;CAEH,YAAY,SAAoC;AAC9C,OAAK,kBAAkB,QAAQ,gBAAgB,QAAQ,OAAO,GAAG;AACjE,OAAK,aAAa,QAAQ,cAAc;AACxC,OAAK,MAAM,SAAS,eAAgB,MAAK,QAAQ,IAAI,OAAO,EAAE,CAAC;AAG/D,OAAK,QAAQ,gBAAgB,EAAE;;;;;;;;;CAUjC,MAAc,eAGX;EAED,MAAM,MAAM,MAAM,MAAM,GAAG,KAAK,gBAAgB,OAAO;AACvD,MAAI,CAAC,IAAI,GACP,OAAM,IAAI,MACR,yCAAyC,IAAI,OAAO,GAAG,IAAI,WAAW,wDAEvE;EAEH,MAAM,OAAgB,MAAM,IAAI,MAAM;EACtC,MAAM,OAA6B,MAAM,QAAQ,KAAK,GAAI,OAAgC,EAAE;AAE5F,OAAK,QAAQ,OAAO;EACpB,IAAI,WAAsC;AAE1C,OAAK,MAAM,QAAQ,MAAM;AACvB,OAAI,CAACA,WAAS,KAAK,IAAI,OAAO,KAAK,OAAO,SAAU;GACpD,MAAM,YAAuB;IAC3B,IAAI,KAAK;IACT,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;IACrD,KAAK,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;IAChD;AACD,QAAK,QAAQ,IAAI,KAAK,IAAI,UAAU;AAGpC,OACE,aAAa,QACb,KAAK,SAAS,UACd,OAAO,KAAK,yBAAyB,YACrC,CAAC,qBAAqB,KAAK,IAAI,CAE/B,YAAW;;AAIf,SAAO;GAAE;GAAU,KAAK,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC;GAAE;;CAGtD,cAA2B;AACzB,SAAO,CAAC,GAAG,KAAK,QAAQ,QAAQ,CAAC;;;;;;;;CASnC,MAAM,gBAA+B;AACnC,MAAI,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAAM;AACtD,MAAI,KAAK,gBAAiB,QAAO,KAAK;AACtC,OAAK,kBAAkB,KAAK,kBAAkB,CAAC,cAAc;AAC3D,QAAK,kBAAkB;IACvB;AACF,SAAO,KAAK;;CAGd,MAAc,mBAAkC;EAC9C,MAAM,EAAE,aAAa,MAAM,KAAK,cAAc;AAC9C,MAAI,CAAC,SACH,OAAM,IAAI,MACR,oLAGD;EAIH,MAAM,QAAQ,SAAS;EACvB,MAAM,KAAK,IAAI,UAAU,MAAM;AAC/B,OAAK,KAAK;AAEV,QAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,MAAG,KAAK,cAAc,SAAS,CAAC;AAChC,MAAG,KAAK,UAAU,QAAe,OAAO,IAAI,CAAC;IAC7C;AAEF,KAAG,GAAG,YAAY,SAA4B,KAAK,cAAc,KAAK,UAAU,CAAC,CAAC;AAGlF,OAAK,kBAAkB,iBAAiB;AACxC,OAAK,kBAAkB,iBAAiB;AACxC,OAAK,kBAAkB,aAAa;AACpC,OAAK,kBAAkB,cAAc;;;CAIvC,kBAA0B,QAAgB,SAAkC,EAAE,EAAQ;AACpF,MAAI,CAAC,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAAM;EACvD,MAAM,KAAK,KAAK;AAChB,OAAK,GAAG,KAAK,KAAK,UAAU;GAAE;GAAI;GAAQ;GAAQ,CAAC,CAAC;;;;;;CAOtD,KACE,QACA,QACqC;AACrC,SAAO,KAAK,YAAY,QAAS,UAAU,EAAE,CAA6B;;;;;;CAS5E,YAAY,QAAgB,SAAkC,EAAE,EAAoB;AAClF,MAAI,CAAC,KAAK,MAAM,KAAK,GAAG,eAAe,UAAU,KAC/C,QAAO,QAAQ,uBACb,IAAI,MACF,kIAED,CACF;EAEH,MAAM,KAAK,KAAK;EAChB,MAAM,KAAK,KAAK;AAChB,SAAO,IAAI,SAAkB,SAAS,WAAW;AAC/C,QAAK,QAAQ,IAAI,IAAI;IAAE;IAAS;IAAQ,CAAC;AACzC,MAAG,KAAK,KAAK,UAAU;IAAE;IAAI;IAAQ;IAAQ,CAAC,CAAC;IAC/C;;CAGJ,cAAsB,KAAmB;EACvC,MAAM,UAAU,aAAa,IAAI;AACjC,MAAI,CAAC,QAAS;AAGd,MAAI,OAAO,QAAQ,OAAO,YAAY,KAAK,QAAQ,IAAI,QAAQ,GAAG,EAAE;GAClE,MAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ,GAAG;AAC3C,QAAK,QAAQ,OAAO,QAAQ,GAAG;AAC/B,OAAI,OACF,KAAI,QAAQ,MAAO,QAAO,OAAO,IAAI,MAAM,QAAQ,MAAM,QAAQ,CAAC;OAC7D,QAAO,QAAQ,QAAQ,OAAO;AAErC;;AAIF,MAAI,OAAO,QAAQ,WAAW,SAAU;AACxC,MAAI,CAAC,KAAK,QAAQ,IAAI,QAAQ,OAAuB,CAAE;EACvD,MAAM,QAAQ,QAAQ;EACtB,MAAM,SAAS,KAAK,QAAQ,IAAI,MAAM;AACtC,MAAI,CAAC,OAAQ;AACb,SAAO,KAAK,QAAQ,OAAO;AAC3B,MAAI,OAAO,SAAS,KAAK,WAAY,QAAO,OAAO;AACnD,OAAK,QAAQ,KAAK,OAAO,QAAQ,OAAO;;CAG1C,kBAA0C,OAAyC;AAEjF,SADe,KAAK,QAAQ,IAAI,MAAM,IACpB,EAAE;;CAGtB,GAA2B,OAAU,UAAyD;AAC5F,OAAK,QAAQ,GAAG,OAAO,SAAuC;AAC9D,eAAa,KAAK,QAAQ,IAAI,OAAO,SAAuC;;;CAI9E,QAAc;AACZ,OAAK,IAAI,OAAO;AAChB,OAAK,KAAK;AACV,OAAK,MAAM,UAAU,KAAK,QAAQ,QAAQ,CACxC,QAAO,uBAAO,IAAI,MAAM,wCAAwC,CAAC;AAEnE,OAAK,QAAQ,OAAO;;;;AAKxB,SAAS,qBAAqB,KAAsB;AAClD,QACE,QAAQ,MACR,QAAQ,iBACR,QAAQ,kBACR,IAAI,WAAW,cAAc,IAC7B,IAAI,WAAW,YAAY,IAC3B,IAAI,WAAW,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjQzC,SAAgB,eAAgC;AAC9C,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAS,IAAI,cAAc;AACjC,SAAO,OAAO,GAAG,mBAAmB;GAClC,MAAM,OAAO,OAAO,SAAS;GAC7B,MAAM,OAAO,OAAO,SAAS,YAAY,SAAS,OAAO,KAAK,OAAO;AACrE,UAAO,YAAY;AACjB,QAAI,SAAS,KACX,wBAAO,IAAI,MAAM,iDAAiD,CAAC;QAEnE,SAAQ,KAAK;KAEf;IACF;AAGD,SAA+E,GAC9E,SACA,OACD;GACD;;;;;;AAOJ,SAAgB,uBAAiC;CAC/C,MAAM,KAAK,UAAU;AACrB,KAAI,OAAO,SACT,QAAO;EACL;EACA;EACA;EACA;EACD;AAEH,KAAI,OAAO,QACT,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACD;AAEH,KAAI,OAAO,SAAS;EAClB,MAAM,eAAe,QAAQ,IAAI,gBAAgB;EACjD,MAAM,kBAAkB,QAAQ,IAAI,wBAAwB;AAC5D,SAAO;GACL,GAAG,aAAa;GAChB,GAAG,gBAAgB;GACnB,GAAG,aAAa;GACjB;;AAEH,QAAO,EAAE;;;AAIX,SAAgB,mBAAkC;AAChD,MAAK,MAAM,KAAK,sBAAsB,CACpC,KAAI,WAAW,EAAE,CAAE,QAAO;AAE5B,QAAO;;;;;;;;;AAUT,eAAsB,eAAe,UAAiC,EAAE,EAA2B;CACjG,MAAM,YAAY,QAAQ,WAAW;CAGrC,MAAM,gBAAgB,QAAQ,QAAQ;CACtC,MAAM,OAAO,kBAAkB,IAAI,MAAM,cAAc,GAAG;CAE1D,MAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,oBAAoB;CAEjE,MAAM,SAAS,kBAAkB;AACjC,KAAI,WAAW,KACb,OAAM,IAAI,MACR,gHAGE,sBAAsB,CAAC,KAAK,KAAK,CACpC;CAcH,MAAM,QAAsB,UAAU,QAXzB;EACX,2BAA2B;EAC3B;EACA;EAGA;EACA,GAAI,QAAQ,aAAa,EAAE;EAC3B;EACD,EAEmD;EAElD,OAAO;EACP,UAAU;EACX,CAAC;AAGF,OAAM,OAAO;CAEb,MAAM,cAAc,oBAAoB;AAExC,SAAQ,OAAO,MACb,wCAAwC,OAAO,oCACV,YAAY,+BACjB,OAAO,IACxC;AAED,QAAO;EACL;EACA;EACA,OAAa;AACX,OAAI;AACF,UAAM,MAAM;WACN;;EAIX;;;;ACnMH,MAAa,KAAgC;CAE3C,eAAe;CACf,sBAAsB;CACtB,eAAe;CACf,qBAAqB;CACrB,sBAAsB;CACtB,8BAA8B;CAC9B,kBAAkB;CAGlB,iBAAiB;CACjB,qBAAqB;CACrB,sBAAsB;CACtB,yBAAyB;CACzB,2BAA2B;CAC3B,sBAAsB;CACtB,oBAAoB;CACpB,iBAAiB;CACjB,iBAAiB;CACjB,oBAAoB;CACpB,uBAAuB;CACvB,qBAAqB;CAGrB,mBAAmB;CAGnB,uBAAuB;CACvB,sBACE;CACF,2BAA2B;CAC3B,wBAAwB;CACxB,sBAAsB;CAGtB,wBAAwB;CACxB,cAAc;CACd,sBAAsB;CACtB,uBAAuB;CACvB,kBAAkB;CAClB,uBAAuB;CACvB,yBAAyB;CACzB,wBAAwB;CACxB,wBAAwB;CACxB,2BAA2B;CAC3B,0BAA0B;CAC1B,2BAA2B;CAC3B,mCAAmC;CACnC,qCAAqC;CACrC,sCAAsC;CACtC,4BACE;CAGF,yBAAyB;CAEzB,uBAAuB;CACvB,sBAAsB;CACtB,uBAAuB;CACvB,0BAA0B;CAC1B,2BAA2B;CAC3B,wBAAwB;CAExB,qBAAqB;CACrB,oBAAoB;CACpB,qBAAqB;CACrB,wBAAwB;CACxB,yBAAyB;CACzB,6BAA6B;CAC7B,8BAA8B;CAC9B,iCAAiC;CACjC,2BAA2B;CAC3B,0BAA0B;CAC1B,yBAAyB;CACzB,mCAAmC;CACnC,8BAA8B;CAC9B,6BAA6B;CAG7B,wBAAwB;CACxB,oBAAoB;CACpB,mBAAmB;CACnB,mBAAmB;CAGnB,8BAA8B;CAG9B,4BAA4B;CAC5B,yBAAyB;CACzB,0BAA0B;CAC1B,yBAAyB;CAGzB,wBAAwB;CACxB,qBAAqB;CACrB,qBAAqB;CACrB,uBAAuB;CACvB,sBAAsB;CACtB,wBAAwB;CACxB,6BAA6B;CAC7B,kBAAkB;CAClB,0BAA0B;CAC1B,oBAAoB;CACpB,8BAA8B;CAC9B,8BAA8B;CAC9B,gCAAgC;CAChC,sCAAsC;CACtC,+BAA+B;CAC/B,2BAA2B;CAC3B,2BAA2B;CAC3B,sBAAsB;CACtB,wBAAwB;CAGxB,yBAAyB;CACzB,0BAA0B;CAC1B,yBAAyB;CACzB,yBAAyB;CAGzB,2BAA2B;CAC3B,uBAAuB;CACvB,4BAA4B;CAC5B,0BAA0B;CAC1B,2BAA2B;CAC3B,sBAAsB;CACtB,uBAAuB;CACvB,+BAA+B;CAC/B,0BAA0B;CAC1B,8BAA8B;CAC9B,2BAA2B;CAC3B,gCAAgC;CAChC,+BAA+B;CAC/B,4BAA4B;CAC5B,6BAA6B;CAC7B,kCAAkC;CAClC,mCAAmC;CAGnC,yBAAyB;CACzB,sBAAsB;CACtB,uBAAuB;CACvB,yBAAyB;CACzB,uBAAuB;CACvB,qBAAqB;CACrB,yBAAyB;CACzB,uBAAuB;CACvB,oBAAoB;CACpB,qBAAqB;CAGrB,6BAA6B;CAC7B,0BAA0B;CAC1B,0BAA0B;CAC1B,wBAAwB;CACxB,uBAAuB;CACvB,kCAAkC;CAGlC,yBAAyB;CACzB,uBAAuB;CACvB,2BAA2B;CAC3B,6BAA6B;CAC7B,yBAAyB;CAGzB,yBAAyB;CACzB,wBAAwB;CACxB,iBAAiB;CAGjB,2BAA2B;CAC3B,yBAAyB;CACzB,wBAAwB;CACxB,4BAA4B;CAC5B,2BAA2B;CAC3B,qBAAqB;CACrB,uBAAuB;CACvB,sBAAsB;CACtB,uBAAuB;CACvB,yBAAyB;CACzB,wBAAwB;CACxB,0BAA0B;CAG1B,qBAAqB;CACrB,oBAAoB;CACpB,uBAAuB;CACvB,oBAAoB;CACpB,2BAA2B;CAC3B,uBAAuB;CACvB,4BAA4B;CAC5B,gBAAgB;CAChB,gBAAgB;CAChB,6BAA6B;CAC7B,0BAA0B;CAC1B,wBAAwB;CACxB,kBAAkB;CAClB,kBAAkB;CAClB,iBAAiB;CACjB,mBAAmB;CAGnB,+BAA+B;CAC/B,qCAAqC;CACrC,sCAAsC;CACtC,0CAA0C;CAG1C,qBAAqB;CACrB,qBAAqB;CAGrB,mBAAmB;CACnB,qBAAqB;CACrB,4BAA4B;CAC5B,uBAAuB;CACvB,yBAAyB;CACzB,4BAA4B;CAC5B,yBAAyB;CACzB,2BAA2B;CAC3B,yBAAyB;CAGzB,sBAAsB;CACtB,wBAAwB;CAGxB,+BAA+B;CAC/B,4BAA4B;CAC5B,+BAA+B;CAG/B,4BAA4B;CAC5B,6BAA6B;CAI7B,gBAAgB;CAChB,qBAAqB;CACrB,wBAAwB;CACxB,sBAAsB;CACtB,sBAAsB;CAGtB,uBAAuB;CACvB,yBAAyB;CACzB,0BAA0B;CAG1B,wBACE;CACF,wBACE;CACF,wBACE;CACF,mCACE;CACF,gCACE;CACF,2BACE;CACF,2BACE;CAGF,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,uBAAuB;CACvB,gCACE;CACF,6BACE;CACF,0BACE;CACF,0BACE;CAEF,kCACE;CAGF,kBAAkB;CAClB,wBAAwB;CACxB,uBAAuB;CACvB,2BAA2B;CAC3B,oBAAoB;CACpB,oBAAoB;CACpB,qBAAqB;CACrB,wBAAwB;CACxB,4BAA4B;CAC5B,uBAAuB;CACvB,4BAA4B;CAC5B,gCAAgC;CAChC,iCACE;CACF,+BAA+B;CAC/B,sBAAsB;CACtB,oBAAoB;CACpB,mBAAmB;CACnB,8BACE;CACF,6BACE;CAEF,6BAA6B;CAC7B,8BAA8B;CAC9B,iCAAiC;CACjC,sCAAsC;CACtC,kCAAkC;CAClC,0CAA0C;CAC1C,wCAAwC;CAExC,gCAAgC;CAChC,wBAAwB;CACxB,wBAAwB;CACxB,yBAAyB;CACzB,8BAA8B;CAC9B,4BAA4B;CAC5B,gCAAgC;CACjC;;;;;;;;;;;;;;;;;;AEjTD,MAAM,SAA6D;CAAE,IDhBnD;EAEhB,eAAe;EACf,sBAAsB;EACtB,eAAe;EACf,qBAAqB;EACrB,sBAAsB;EACtB,8BAA8B;EAC9B,kBAAkB;EAGlB,iBAAiB;EACjB,qBAAqB;EACrB,sBAAsB;EACtB,yBAAyB;EACzB,2BAA2B;EAC3B,sBAAsB;EACtB,oBAAoB;EACpB,iBAAiB;EACjB,iBAAiB;EACjB,oBAAoB;EACpB,uBAAuB;EACvB,qBAAqB;EAGrB,mBAAmB;EAGnB,uBAAuB;EACvB,sBAAsB;EACtB,2BAA2B;EAC3B,wBAAwB;EACxB,sBAAsB;EAGtB,wBAAwB;EACxB,cAAc;EACd,sBAAsB;EACtB,uBAAuB;EACvB,kBAAkB;EAClB,uBAAuB;EACvB,yBAAyB;EACzB,wBAAwB;EACxB,wBAAwB;EACxB,2BAA2B;EAC3B,0BAA0B;EAC1B,2BAA2B;EAC3B,mCAAmC;EACnC,qCAAqC;EACrC,sCAAsC;EACtC,4BACE;EAGF,yBAAyB;EAEzB,uBAAuB;EACvB,sBAAsB;EACtB,uBAAuB;EACvB,0BAA0B;EAC1B,2BAA2B;EAC3B,wBAAwB;EAExB,qBAAqB;EACrB,oBAAoB;EACpB,qBAAqB;EACrB,wBAAwB;EACxB,yBAAyB;EACzB,6BAA6B;EAC7B,8BAA8B;EAC9B,iCAAiC;EACjC,2BAA2B;EAC3B,0BAA0B;EAC1B,yBAAyB;EACzB,mCAAmC;EACnC,8BAA8B;EAC9B,6BAA6B;EAG7B,wBAAwB;EACxB,oBAAoB;EACpB,mBAAmB;EACnB,mBAAmB;EAGnB,8BAA8B;EAG9B,4BAA4B;EAC5B,yBAAyB;EACzB,0BAA0B;EAC1B,yBAAyB;EAGzB,wBAAwB;EACxB,qBAAqB;EACrB,qBAAqB;EACrB,uBAAuB;EACvB,sBAAsB;EACtB,wBAAwB;EACxB,6BAA6B;EAC7B,kBAAkB;EAClB,0BAA0B;EAC1B,oBAAoB;EACpB,8BAA8B;EAC9B,8BAA8B;EAC9B,gCAAgC;EAChC,sCAAsC;EACtC,+BAA+B;EAC/B,2BAA2B;EAC3B,2BAA2B;EAC3B,sBAAsB;EACtB,wBAAwB;EAGxB,yBAAyB;EACzB,0BAA0B;EAC1B,yBAAyB;EACzB,yBAAyB;EAGzB,2BAA2B;EAC3B,uBAAuB;EACvB,4BAA4B;EAC5B,0BAA0B;EAC1B,2BAA2B;EAC3B,sBAAsB;EACtB,uBAAuB;EACvB,+BAA+B;EAC/B,0BAA0B;EAC1B,8BAA8B;EAC9B,2BAA2B;EAC3B,gCAAgC;EAChC,+BAA+B;EAC/B,4BAA4B;EAC5B,6BAA6B;EAC7B,kCAAkC;EAClC,mCAAmC;EAGnC,yBAAyB;EACzB,sBAAsB;EACtB,uBAAuB;EACvB,yBAAyB;EACzB,uBAAuB;EACvB,qBAAqB;EACrB,yBAAyB;EACzB,uBAAuB;EACvB,oBAAoB;EACpB,qBAAqB;EAGrB,6BAA6B;EAC7B,0BAA0B;EAC1B,0BAA0B;EAC1B,wBAAwB;EACxB,uBAAuB;EACvB,kCAAkC;EAGlC,yBAAyB;EACzB,uBAAuB;EACvB,2BAA2B;EAC3B,6BAA6B;EAC7B,yBAAyB;EAGzB,yBAAyB;EACzB,wBAAwB;EACxB,iBAAiB;EAGjB,2BAA2B;EAC3B,yBAAyB;EACzB,wBAAwB;EACxB,4BACE;EACF,2BAA2B;EAC3B,qBAAqB;EACrB,uBAAuB;EACvB,sBAAsB;EACtB,uBAAuB;EACvB,yBAAyB;EACzB,wBAAwB;EACxB,0BAA0B;EAG1B,qBAAqB;EACrB,oBAAoB;EACpB,uBAAuB;EACvB,oBAAoB;EACpB,2BAA2B;EAC3B,uBAAuB;EACvB,4BAA4B;EAC5B,gBAAgB;EAChB,gBAAgB;EAChB,6BAA6B;EAC7B,0BAA0B;EAC1B,wBAAwB;EACxB,kBAAkB;EAClB,kBAAkB;EAClB,iBAAiB;EACjB,mBAAmB;EAGnB,+BAA+B;EAC/B,qCAAqC;EACrC,sCAAsC;EACtC,0CAA0C;EAG1C,qBAAqB;EACrB,qBAAqB;EAGrB,mBAAmB;EACnB,qBAAqB;EACrB,4BAA4B;EAC5B,uBAAuB;EACvB,yBAAyB;EACzB,4BAA4B;EAC5B,yBAAyB;EACzB,2BAA2B;EAC3B,yBAAyB;EAGzB,sBAAsB;EACtB,wBAAwB;EAGxB,+BAA+B;EAC/B,4BAA4B;EAC5B,+BAA+B;EAG/B,4BAA4B;EAC5B,6BAA6B;EAI7B,gBAAgB;EAChB,qBAAqB;EACrB,wBAAwB;EACxB,sBAAsB;EACtB,sBAAsB;EAGtB,uBAAuB;EACvB,yBAAyB;EACzB,0BAA0B;EAG1B,wBACE;EACF,wBACE;EACF,wBAAwB;EACxB,mCACE;EACF,gCACE;EACF,2BACE;EACF,2BACE;EAGF,uBAAuB;EACvB,uBAAuB;EACvB,uBAAuB;EACvB,uBAAuB;EACvB,gCACE;EACF,6BACE;EACF,0BACE;EACF,0BACE;EAEF,kCACE;EAGF,kBAAkB;EAClB,wBAAwB;EACxB,uBAAuB;EACvB,2BAA2B;EAC3B,oBAAoB;EACpB,oBAAoB;EACpB,qBAAqB;EACrB,wBAAwB;EACxB,4BAA4B;EAC5B,uBAAuB;EACvB,4BAA4B;EAC5B,gCAAgC;EAChC,iCACE;EACF,+BAA+B;EAC/B,sBAAsB;EACtB,oBAAoB;EACpB,mBAAmB;EACnB,8BACE;EACF,6BACE;EAEF,6BAA6B;EAC7B,8BAA8B;EAC9B,iCAAiC;EACjC,sCAAsC;EACtC,kCAAkC;EAClC,0CAA0C;EAC1C,wCAAwC;EAExC,gCAAgC;EAChC,wBAAwB;EACxB,wBAAwB;EACxB,yBAAyB;EACzB,8BAA8B;EAC9B,4BAA4B;EAC5B,gCAAgC;EACjC;CClTwE;CAAI;;;;;;AA6B7E,SAAS,sBAAsB,MAAsB;AACnD,QAAO,SAAS,KAAK,KAAK,GAAG,OAAO;;;;;;;;;;AAoBtC,SAAgB,oBAAoB,QAA2C;AAC7E,KAAI,CAAC,OAAQ,QAAO;AAEpB,QAAO,sBADO,OAAO,MAAM,IAAI,CAAC,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,MAAM,IAAI,GACjC;;;;;;;;;;;AAYrC,SAAgB,qBACd,QACoE;CACpE,MAAM,QAAQ,OAAO;AACrB,SAAQ,KAAK,SAAS;EACpB,MAAM,MAAM,MAAM,QAAQ;AAC1B,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,IAAI,QAAQ,eAAe,OAAO,SAAiB;GACxD,MAAM,QAAQ,KAAK;AACnB,UAAO,UAAU,KAAA,IAAY,QAAQ,OAAO,MAAM;IAClD;;;;;AC7DN,MAAa,wBACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAa,4BACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,MAAa,2BACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAa,wBACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAa,4BACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,MAAa,2BACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,MAAa,0BAAkD;CAC7D,IAAI;CACJ,IAAI;CACL;;AAGD,MAAa,uBAA2E;CACtF,IAAI;EAAE,SAAS;EAA2B,QAAQ;EAA0B;CAC5E,IAAI;EAAE,SAAS;EAA2B,QAAQ;EAA0B;CAC7E;;;;AClUD,SAAS,oBAAoB,MAAsD;AACjF,QAAO,SAAS,iBAAiB,YAAY;;;;;;;AAQ/C,SAAS,eACP,MACA,GACQ;CACR,IAAI;AACJ,SAAQ,MAAR;EACE,KAAK;AACH,WAAQ,EAAE,sBAAsB;AAChC;EACF,KAAK;AACH,WAAQ,EAAE,wBAAwB;AAClC;EACF,KAAK;AACH,WAAQ,EAAE,yBAAyB;AACnC;EACF,KAAK;EACL,KAAK,KAAA,EACH,QAAO;;AAEX,QAAO,yBAAyB,WAAW,MAAM,CAAC;;;AAsBpD,SAAS,WAAW,GAAmB;AACrC,QAAO,EAAE,QAAQ,aAAa,MAAM,KAAK,EAAE,WAAW,EAAE,CAAC,GAAG;;;;;;;;AAS9D,SAAS,kBACP,MACA,gBACA,QACA,GACQ;CACR,SAAS,aAAa,YAAiC;EACrD,MAAM,IAAI,IAAI,gBAAgB,eAAe;AAC7C,IAAE,IAAI,QAAQ,WAAW;AACzB,SAAO,GAAG,WAAW,KAAK,CAAC,GAAG,EAAE,UAAU;;CAE5C,MAAM,UAAU,WAAW,EAAE,oBAAoB,CAAC;CAClD,MAAM,UAAU,WAAW,EAAE,oBAAoB,CAAC;CAClD,MAAM,UAAU,WAAW,OAAO,WAAW;CAC7C,MAAM,UAAU,WAAW,OAAO,WAAW;AAC7C,QAAO,uCAAuC,aAAa,KAAK,CAAC,WAAW,QAAQ,IAAI,QAAQ,eAAe,aAAa,KAAK,CAAC,WAAW,QAAQ,IAAI,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;AAyBnK,SAAS,mBACP,OACA,WACA,QACA,OAAO,KACP,SAAS,IAAI,iBAAiB,EAU9B,mBAAkC,MAC1B;CACR,MAAM,IAAI,qBAAqB,OAAO;CACtC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;CAEpC,MAAM,eAAe,MAAM,OAAO,KAAK,EAAE,sBAAsB,GAAG,EAAE,wBAAwB;CAC5F,MAAM,cAAc,MAAM,OAAO,KAAK,cAAc;CAIpD,IAAI;AACJ,KAAI,aAAa,MAAM,WAAW;EAChC,MAAM,gBAAgB,WAAW,MAAM,UAAU;EACjD,MAAM,YAAY,WAAW,EAAE,qBAAqB,CAAC;AACrD,kBACE,wBAAwB,UAAU,2EAEC,cAAc,uEACmB,UAAU,IAAI,UAAU;OAG9F,iBAAgB,mBAAmB,WAAW,EAAE,wBAAwB,CAAC,CAAC;CAc5E,MAAM,gBAAgB,MAAM,QAAQ,MAAM,MAAM,IAAI,MAAM,MAAM,SAAS;CACzE,IAAI;AACJ,KAAI,iBAAiB,iBAGnB,oBAAmB,uDAFH,WAAW,iBAAiB,CAEsC,8CADpE,WAAW,EAAE,2BAA2B,CAAC,CAC+E;KAGtI,oBAAmB,oDADN,WAAW,EAAE,8BAA8B,CAAC,CACmB;CAM9E,MAAM,eACJ,MAAM,UAAU,OACZ,KACA,yCAAyC,WAAW,EAAE,0BAA0B,CAAC,CAAC,2BAChF,MAAM,MAAM,SAAS,IACjB,MAAM,MACH,KAAK,MAAM;AAGV,SAAO,6BAFQ,WAAW,EAAE,GAAG,CAEY,iCAD3B,WAAW,EAAE,IAAI,MAAM,GAAG,IAAI,CAAC,CACqC;GACpF,CACD,KAAK,KAAK,GACb,qBAAqB,WAAW,EAAE,wBAAwB,CAAC,CAAC,OACjE;CAGP,MAAM,aAA+B;EACnC,UAAU,KAAK,UAAU,EAAE,sBAAsB,CAAC;EAClD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,WAAW,KAAK,UAAU,EAAE,qBAAqB,CAAC;EAClD,aAAa,KAAK,UAAU,EAAE,uBAAuB,CAAC;EACtD,oBAAoB,KAAK,UAAU,EAAE,2BAA2B,CAAC;EACjE,uBAAuB,KAAK,UAAU,EAAE,8BAA8B,CAAC;EACvE,kBAAkB;EACnB;CAED,MAAM,eAAe,kBAAkB,MAAM,QAAQ,QAAQ,EAAE;CAM/D,MAAM,SADS,wBAAwB,QAEpC,WAAW,qBAAqB,aAAa,CAC7C,WAAW,WAAW,WAAW,IAAI,CAAC,CACtC,WAAW,oBAAoB,YAAY,CAC3C,WAAW,qBAAqB,WAAW,aAAa,CAAC,CACzD,WAAW,sBAAsB,cAAc,CAC/C,WAAW,yBAAyB,iBAAiB,CACrD,WAAW,qBAAqB,aAAa;CAKhD,MAAM,YAAY,eAAe,WAAW;AAC5C,QAAO,OAAO,QAAQ,WAAW,GAAG,UAAU,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;AAgD3D,SAAS,eAAe,SAAmC;CACzD,MAAM,cAAc,QAAQ;AAC5B,QAAO;;;wBAGe,QAAQ,SAAS;0BACf,QAAQ,WAAW;0BACnB,QAAQ,WAAW;0BACnB,QAAQ,WAAW;yBACpB,QAAQ,UAAU;2BAChB,QAAQ,YAAY;mCACZ,QAAQ,mBAAmB;sCACxB,QAAQ,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBA4FpD,cACI;;;;;;;6BAQA;;;;;;;;;;qDAWL;;gBAGC,cACI,mEACA;8EAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDf,SAAS,gBACP,WACA,WACA,eACA,QACA,OAAO,WACP,SAAS,IAAI,iBAAiB,EAC9B,MACA,gBAAgB,OAChB,qBAAoC,MAC5B;CACR,MAAM,IAAI,qBAAqB,OAAO;CACtC,MAAM,eAAe,kBAAkB,MAAM,QAAQ,QAAQ,EAAE;CAC/D,MAAM,SAAS,oBAAoB,KAAK;CAGxC,MAAM,UAAU,SAAS,eAAe,OAAO,EAAE,iCAAiC,CAAC,SAAS;CAI5F,IAAI;AACJ,KAAI,iBAAiB,mBAGnB,oBAAmB,uDAFH,WAAW,mBAAmB,CAEoC,8CADpE,WAAW,EAAE,2BAA2B,CAAC,CAC+E;KAGtI,oBAAmB,oDADN,WAAW,EAAE,8BAA8B,CAAC,CACmB;CAI9E,MAAM,SADS,qBAAqB,QAAQ,QAEzC,WAAW,qBAAqB,aAAa,CAC7C,WAAW,kBAAkB,eAAe,MAAM,EAAE,CAAC,CACrD,WAAW,gBAAgB,QAAQ,CACnC,WAAW,mBAAmB,UAAU,CACxC,WAAW,kBAAkB,UAAU,CACvC,WAAW,uBAAuB,cAAc,CAChD,WAAW,yBAAyB,iBAAiB;CAkBxD,MAAM,YAAY,eAbmB;EACnC,UAAU,KAAK,UAAU,EAAE,sBAAsB,CAAC;EAClD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,YAAY,KAAK,UAAU,EAAE,wBAAwB,CAAC;EACtD,WAAW,KAAK,UAAU,EAAE,qBAAqB,CAAC;EAClD,aAAa,KAAK,UAAU,EAAE,uBAAuB,CAAC;EAEtD,oBAAoB,KAAK,UAAU,EAAE,2BAA2B,CAAC;EACjE,uBAAuB,KAAK,UAAU,EAAE,8BAA8B,CAAC;EAEvE,kBAAkB;EACnB,CAC2C;AAC5C,QAAO,OAAO,QAAQ,WAAW,GAAG,UAAU,WAAW;;;;;;;;;;;AA6C3D,eAAsB,kBACpB,mBACA,SACuB;CACvB,MAAM,EAAE,SAAS,WAAW,MAAM,OAAO;;CAGzC,MAAM,aAA+B,EAAE;;CAGvC,SAAS,kBAAkB,KAAqB,OAA6B;EAC3E,MAAM,UAAU,KAAK,UAAU;GAC7B,QAAQ;IAAE,IAAI,MAAM,OAAO;IAAI,QAAQ,MAAM,OAAO;IAAQ;GAC5D,OAAO,MAAM;GAEb,WAAW,MAAM;GAIjB,cAAc,MAAM,gBAAgB;GACrC,CAAC;AAEF,MAAI,MAAM,SAAS,QAAQ,MAAM;;CAGnC,MAAM,SAAiB,aAAa,OAAO,KAAsB,QAAwB;EAEvF,MAAM,CAAC,MAAM,QAAQ,OADN,IAAI,OAAO,KACQ,MAAM,KAAK,EAAE;EAC/C,MAAM,SAAS,IAAI,gBAAgB,SAAS,GAAG;EAG/C,MAAM,YAAY,OAAO,IAAI,OAAO;EACpC,MAAM,SACJ,cAAc,QAAQ,cAAc,OAChC,YACA,oBAAoB,IAAI,QAAQ,mBAAmB;AAGzD,MAAI,SAAS,KAAK;AAChB,OAAI,CAAC,mBAAmB;AACtB,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,KAAK;AACT;;GAEF,MAAM,QAAQ,mBAAmB;GACjC,IAAI,YAA2B;AAC/B,OAAI,MAAM,UACR,KAAI;AACF,gBAAY,MAAM,OAAO,UAAU,MAAM,WAAW;KAClD,MAAM;KACN,sBAAsB;KACvB,CAAC;WACI;GAOV,MAAM,0BAAyC;AAC7C,QAAI,CAAC,SAAS,sBAAuB,QAAO;IAC5C,MAAM,OAAO,OAAO,SAAS;AAC7B,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,WAAO,oBAAoB,KAAK,KAAK;OACnC;GACJ,MAAM,OAAO,mBAAmB,OAAO,WAAW,QAAQ,MAAM,QAAQ,iBAAiB;AACzF,OAAI,UAAU,KAAK;IACjB,gBAAgB;IAChB,iBAAiB;IAClB,CAAC;AACF,OAAI,IAAI,KAAK;AACb;;AAIF,MAAI,SAAS,WAAW;AACtB,OAAI,CAAC,mBAAmB;AACtB,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,KAAK;AACT;;AAEF,OAAI,UAAU,KAAK;IACjB,gBAAgB;IAChB,iBAAiB;IACjB,YAAY;IACZ,qBAAqB;IACtB,CAAC;AAGF,qBAAkB,KADG,mBAAmB,CACJ;AAEpC,cAAW,KAAK,IAAI;AAGpB,OAAI,KAAK,eAAe;IACtB,MAAM,MAAM,WAAW,QAAQ,IAAI;AACnC,QAAI,QAAQ,GAAI,YAAW,OAAO,KAAK,EAAE;KACzC;AACF;;AAGF,MAAI,SAAS,WAAW;GACtB,MAAM,WAAW,OAAO,IAAI,IAAI,IAAI;GACpC,IAAI;AACJ,OAAI;AACF,gBAAY,mBAAmB,SAAS;WAClC;AACN,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,iBAAiB;AACzB;;GAIF,IAAI,oBAAoB;AACxB,OAAI;IACF,MAAM,UAAU,UAAU,MAAM,4BAA4B;AAC5D,QAAI,UAAU,GACZ,qBAAoB,mBAAmB,QAAQ,GAAG,CAAC,MAAM,GAAG,GAAG;WAE3D;GAMR,MAAM,eAAe,qBAAqB;GAC1C,MAAM,OAAO,cAAc;GAC3B,MAAM,gBACJ,MAAM,QAAQ,cAAc,MAAM,KAAK,cAAc,MAAM,UAAU,KAAK;GAI5E,MAAM,qCAAoD;AACxD,QAAI,CAAC,SAAS,sBAAuB,QAAO;IAC5C,MAAM,OAAO,OAAO,SAAS;AAC7B,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,WAAO,oBAAoB,KAAK,KAAK;OACnC;AAGJ,UAAO,UAAU,WAAW;IAAE,MAAM;IAAa,sBAAsB;IAAK,CAAC,CAC1E,MAAM,YAAoB;IAGzB,MAAM,OAAO,gBACX,SAHgB,WAAW,kBAAkB,EACzB,WAAW,UAAU,EAKzC,QACA,MACA,QACA,MACA,eACA,4BACD;AACD,QAAI,UAAU,KAAK;KACjB,gBAAgB;KAChB,iBAAiB;KAClB,CAAC;AACF,QAAI,IAAI,KAAK;KACb,CACD,YAAY;AACX,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,iBAAiB;KACzB;AACJ;;AAkBF,MAAI,SAAS,gBAAgB,SAAS,eAAe,SAAS,cAAc;GAC1E,MAAM,wBAAwB,SAAS;AACvC,OAAI,CAAC,uBAAuB;IAK1B,MAAM,OACJ,SAAS,eACL,6DACA;AACN,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,KAAK;AACb;;GAGF,MAAM,SAAS,uBAAuB;GACtC,MAAM,IAAI,qBAAqB,OAAO;AACtC,OAAI,CAAC,OAAO,IAAI;IAId,MAAM,OACJ,8BAA8B,OAAO,wEAE/B,WAJI,EADV,OAAO,WAAW,aAAa,6BAA6B,4BACzC,CAIE,CAAC,+CAErB,WAAW,OACR,mCACA,2CACJ;AACF,QAAI,UAAU,KAAK;KACjB,gBAAgB;KAChB,iBAAiB;KAClB,CAAC;AACF,QAAI,IAAI,KAAK;AACb;;AAIF,OAAI,UAAU,KAAK;IACjB,UAAU,OAAO;IACjB,iBAAiB;IAClB,CAAC;AACF,OAAI,KAAK;AACT;;AAGF,MAAI,SAAS,WAAW;GACtB,MAAM,WAAW,OAAO,IAAI,IAAI,IAAI;GACpC,IAAI;AACJ,OAAI;AACF,gBAAY,mBAAmB,SAAS;WAClC;AACN,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,iBAAiB;AACzB;;AAGF,UAAO,SAAS,WAAW;IAAE,MAAM;IAAO,sBAAsB;IAAK,CAAC,CACnE,MAAM,QAAgB;AACrB,QAAI,UAAU,KAAK;KACjB,gBAAgB;KAChB,iBAAiB;KACjB,kBAAkB,OAAO,IAAI,OAAO;KACrC,CAAC;AACF,QAAI,IAAI,IAAI;KACZ,CACD,YAAY;AACX,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IAAI,qBAAqB;KAC7B;AACJ;;AAGF,MAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,MAAI,IAAI,YAAY;GACpB;CAEF,MAAM,aAAa,OAAO,QAAQ,IAAI,uBAAuB,EAAE;AAE/D,OAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,SAAO,OAAO,YAAY,mBAAmB,SAAS,CAAC;AACvD,SAAO,KAAK,SAAS,OAAO;GAC5B;CAEF,MAAM,UAAU,OAAO,SAAS;AAChC,KAAI,CAAC,WAAW,OAAO,YAAY,SACjC,OAAM,IAAI,MAAM,mDAAmD;CAErE,MAAM,OAAO,QAAQ;;CAGrB,SAAS,4BAAkC;AACzC,MAAI,CAAC,kBAAmB;EACxB,MAAM,QAAQ,mBAAmB;AACjC,OAAK,MAAM,UAAU,WACnB,KAAI;AACF,qBAAkB,QAAQ,MAAM;UAC1B;;CASZ,MAAM,oBAAoB,SAAS,wBAAwB;CAC3D,MAAM,gBAAgB,kBAAkB;AACtC,MAAI,WAAW,SAAS,KAAK,kBAC3B,4BAA2B;IAE5B,kBAAkB,CAAC,OAAO;AAE7B,QAAO;EACL;EACA,mBAAmB,YAA4B;AAM7C,UAAO,oBAAoB,KAAK;;EAIlC,IAAI,qBAA6B;AAC/B,UAAO,oBAAoB,KAAK;;EAElC,oBAA0B;AACxB,8BAA2B;;EAE7B,QAAuB;AACrB,iBAAc,cAAc;AAC5B,UAAO,IAAI,SAAS,SAAS,WAAW;AACtC,WAAO,OAAO,QAAS,MAAM,OAAO,IAAI,GAAG,SAAS,CAAE;KACtD;;EAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACj3BH,IAAa,0BAAb,cAA6C,MAAM;;CAEjD;;CAEA;;CAEA;CAEA,YAAY,aAAqB,gBAA+B,mBAA2B;EACzF,MAAM,UACJ,kBAAkB,OACd,gBAAgB,eAAe,MAC/B;AAEN,QACE,0CAA0C,YAAY,QACpD,UACA;;QAES,cAAc,CAAC,GAC3B;AACD,OAAK,OAAO;AACZ,OAAK,cAAc;AACnB,OAAK,iBAAiB;AACtB,OAAK,oBAAoB;;;;AAS7B,SAAgB,eAAuB;AAErC,QAAO,KADK,QAAQ,IAAI,yBAAyB,KAAK,SAAS,EAAE,gBAAgB,EAChE,cAAc;;AAGjC,SAAS,cAAc,UAAwB;AAE7C,WADY,KAAK,UAAU,KAAK,EACjB,EAAE,WAAW,MAAM,CAAC;;;;;;;;AAarC,MAAa,aAAuCC;AAMpD,SAAS,SAAS,UAAmC;AACnD,KAAI,CAAC,WAAW,SAAS,CAAE,QAAO;AAClC,KAAI;EACF,MAAM,MAAM,aAAa,UAAU,OAAO;EAC1C,MAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,MACE,OAAO,WAAW,YAClB,WAAW,QACX,SAAS,UACT,OAAQ,OAAmC,QAAQ,YACnD,eAAe,UACf,OAAQ,OAAmC,cAAc,UACzD;GACA,MAAM,IAAI;GAGV,MAAM,iBAAiB,OAAO,EAAE,mBAAmB,WAAW,EAAE,iBAAiB;AACjF,UAAO;IACL,KAAK,EAAE;IACP,QAAQ,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;IAClD,WAAW,EAAE;IACb;IACD;;AAGH,SAAO;SACD;AAEN,SAAO;;;AAIX,SAAS,UAAU,UAAkB,MAAsB;AACzD,eAAc,SAAS;AACvB,eAAc,UAAU,KAAK,UAAU,MAAM,MAAM,EAAE,EAAE,EAAE,UAAU,QAAQ,CAAC;;AAG9E,SAAS,WAAW,UAAwB;AAC1C,KAAI;AACF,SAAO,SAAS;SACV;;;;;;;;;;AAiBV,SAAS,YAAY,KAAa,UAAU,KAAa;AACvD,KAAI;AACF,UAAQ,KAAK,KAAK,UAAU;SACtB;AAEN;;CAGF,MAAM,WAAW,KAAK,KAAK,GAAG;AAE9B,QAAO,WAAW,IAAI,IAAI,KAAK,KAAK,GAAG,UAAU;EAE/C,MAAM,MAAM,KAAK,KAAK,GAAG;AACzB,SAAO,KAAK,KAAK,GAAG;;AAKtB,KAAI,WAAW,IAAI,CACjB,KAAI;AACF,UAAQ,KAAK,KAAK,UAAU;SACtB;;;;;;;;AAgBZ,SAAgB,iBAAkC;AAChD,QAAO,SAAS,cAAc,CAAC;;;;;;;;;;;;;;;AA2BjC,SAAgB,YAAY,UAA8B,EAAE,EAAc;CACxE,MAAM,EAAE,QAAQ,UAAU;CAC1B,MAAM,WAAW,cAAc;CAC/B,MAAM,WAAW,SAAS,SAAS;AAEnC,KAAI,aAAa,KACf,KAAI,WAAW,SAAS,IAAI,EAAE;EAK5B,MAAM,iBAAiB,SAAS;AAGhC,MAFwB,OAAO,mBAAmB,YAAY,CAAC,WAAW,eAAe,CAGvF,SAAQ,OAAO,MACb,sCAAsC,SAAS,IAAI,8BAA8B,eAAe,+BACjG;WAEQ,OAAO;AAEhB,WAAQ,OAAO,MACb,yDAAyD,SAAS,IAAI,MACvE;AACD,eAAY,SAAS,IAAI;AACzB,WAAQ,OAAO,MAAM,4BAA4B,SAAS,IAAI,0BAA0B;SACnF;GAIL,MAAM,UACJ,SAAS,UAAU,OAAO,UAAU,SAAS,WAAW;AAC1D,WAAQ,OAAO,MACb,+CAA+C,SAAS,IAAI,YAAY,SAAS,UAAU,IAAI,QAAQ,2BAC3E,SAAS,IAAI,uDAC1C;AACD,SAAM,IAAI,wBAAwB,SAAS,KAAK,SAAS,QAAQ,SAAS,UAAU;;OAItF,SAAQ,OAAO,MACb,mCAAmC,SAAS,IAAI,gCACjD;CAIL,MAAM,OAAiB;EACrB,KAAK,QAAQ;EACb,QAAQ;EACR,4BAAW,IAAI,MAAM,EAAC,aAAa;EACpC;AACD,WAAU,UAAU,KAAK;CAEzB,IAAI,WAAW;AAEf,QAAO;EACL,aAAa,QAAsB;AACjC,OAAI,SAAU;AACd,QAAK,SAAS;AACd,aAAU,UAAU,KAAK;;EAE3B,qBAAqB,KAAmB;AACtC,OAAI,SAAU;AACd,QAAK,iBAAiB;AACtB,aAAU,UAAU,KAAK;;EAE3B,UAAgB;AACd,OAAI,SAAU;AACd,cAAW;AACX,cAAW,SAAS;;EAEvB;;;;AC9RH,SAASC,WAAS,GAA0C;AAC1D,QAAO,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,EAAE;;AAGjE,SAAS,aAAa,MAAyB;AAC7C,KAAI;AACF,SAAO,KAAK,UAAU,KAAK;SACrB;AACN,SAAO,OAAO,KAAK;;;;;;;;;;;AAgBvB,MAAM,aAA6B;CAIjC;EACE,MAAM;EACN,aAAa,MAAM;GACjB,MAAM,MAAM,KAAK;AACjB,OAAI,CAACA,WAAS,IAAI,CAChB,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;GAEH,MAAM,OAAO,IAAI;AACjB,OAAI,SAAS,cAAc,SAAS,YAClC,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;AAEH,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,MAAM;GACjB,MAAM,MAAM,KAAK;AACjB,OAAI,CAACA,WAAS,IAAI,IAAI,OAAO,IAAI,cAAc,UAC7C,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;AAEH,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,MAAM;GACjB,MAAM,MAAM,KAAK;AACjB,OAAI,CAACA,WAAS,IAAI,IAAI,OAAO,IAAI,YAAY,UAC3C,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;AAEH,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,MAAM;GACjB,MAAM,MAAM,KAAK;AACjB,OAAI,CAACA,WAAS,IAAI,IAAI,OAAO,IAAI,YAAY,UAC3C,QAAO;IACL,IAAI;IACJ,UAAU;IACV,UAAU,aAAa,KAAK;IAC7B;AAEH,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAMD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CAKD;EACE,MAAM;EACN,aAAa,OAAO;AAClB,UAAO,EAAE,IAAI,MAAM;;EAErB,SAAS;EACV;CACF;AAMD,MAAM,gBAAgB,IAAI,IAA0B,WAAW,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;;AAGvF,MAAM,qCAAqB,IAAI,KAAa;;;;;AAM5C,SAAgB,gBAAgB,MAAwC;AACtE,QAAO,cAAc,IAAI,KAAK;;;;;;AAOhC,SAAgB,gBAAgB,MAAoB;AAClD,KAAI,mBAAmB,IAAI,KAAK,CAAE;AAClC,oBAAmB,IAAI,KAAK;AAC5B,SAAQ,OAAO,MAAM,0BAA0B,KAAK,iCAAiC;;AAczB,WAAW,KAAK,MAAM,EAAE,KAAK;;;;ACnM3F,MAAa,yBAAyB;CACpC;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAiBF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAgCF,aAAa;GACX,MAAM;GACN,YAAY;IACV,YAAY;KACV,MAAM;KACN,aACE;KAIH;IACD,iBAAiB;KACf,MAAM;KACN,aACE;KAGH;IACD,sBAAsB;KACpB,MAAM;KACN,aACE;KAGH;IACD,aAAa;KACX,MAAM;KACN,aACE;KAIH;IACD,WAAW;KACT,MAAM;KACN,aACE;KAMH;IACF;GAGD,UAAU,EAAE;GACb;EAGD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAIF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAYF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAWF,aAAa;GACX,MAAM;GACN,YAAY;IACV,YAAY;KACV,MAAM;KACN,aAAa;KACd;IACD,SAAS;KACP,MAAM;KACN,aACE;KAIH;IACF;GACD,UAAU,CAAC,aAAa;GACzB;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAMF,aAAa;GACX,MAAM;GACN,YAAY,EACV,OAAO;IACL,MAAM;IACN,aAAa;IACd,EACF;GACD,UAAU,EAAE;GACb;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EA+BF,aAAa;GACX,MAAM;GACN,YAAY;IACV,MAAM;KACJ,MAAM;KACN,aAAa;KACd;IACD,MAAM;KACJ,MAAM;KACN,aAAa;KACb,OAAO,EAAE;KACV;IACD,SAAS;KACP,MAAM;KACN,aACE;KAIH;IACF;GACD,UAAU,CAAC,OAAO;GACnB;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAEF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EA0CF,aAAa;GACX,MAAM;GACN,YAAY;IACV,MAAM;KACJ,MAAM;KACN,MAAM;MAAC;MAAiB;MAAiB;MAAiB;MAAa;KACvE,aACE;KACH;IACD,SAAS;KACP,MAAM;KACN,aACE;KAEH;IACD,aAAa;KACX,MAAM;KACN,aACE;KAIH;IACF;GACD,UAAU,CAAC,OAAO;GACnB;EAGD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAmBF,aAAa;GACX,MAAM;GACN,YAAY,EACV,qBAAqB;IACnB,MAAM;IACN,aACE;IACH,EACF;GACD,UAAU,EAAE;GACb;EACD,aAAa;EACd;CACF;AAID,MAAM,mBAAmB,IAAI,IAAY,uBAAuB,KAAK,MAAM,EAAE,KAAK,CAAC;AAEnF,SAAgB,gBAAgB,MAAqC;AACnE,QAAO,iBAAiB,IAAI,KAAK;;;;;;;;AASnC,SAAgB,oBAAoB,MAA4C;AAC9E,MAAK,MAAM,KAAK,uBACd,KAAI,EAAE,SAAS,KAAM,QAAO,EAAE;;;;;;;;;;AAalC,SAAgB,kBAAkB,MAAc,KAA8B;CAC5E,MAAM,eAAe,oBAAoB,KAAK;AAC9C,KAAI,iBAAiB,KAAA,EAAW,QAAO;AACvC,KAAI,iBAAiB,OAAQ,QAAO;AACpC,KAAI,iBAAiB,QAAS,QAAO,WAAW,IAAI;AACpD,QAAO,iBAAiB;;;;;;;;;;AAW1B,SAAgB,yBACd,OACA,KACK;AACL,QAAO,MAAM,QACV,MACC,EAAE,gBAAgB,UACjB,EAAE,gBAAgB,WAAW,WAAW,IAAI,IAC7C,EAAE,gBAAgB,IACrB;;;;;;;;;;;AAYH,MAAa,uBAA4C,IAAI,IAAY;CACvE;CACA;CACA;CAGA;CACD,CAAC;;AAyBF,SAAS,mBAAmB,KAA8B;AACxD,KAAI,IAAI,UAAU,KAAA,GAAW;AAC3B,MAAI,OAAO,IAAI,UAAU,SAAU,QAAO,IAAI;AAC9C,MAAI;AACF,UAAO,KAAK,UAAU,IAAI,MAAM;UAC1B;AACN,UAAO,OAAO,IAAI,MAAM;;;AAG5B,KAAI,IAAI,gBAAgB,KAAA,EAAW,QAAO,IAAI;AAC9C,KAAI,IAAI,cAAc,KAAA,EAAW,QAAO,IAAI;AAC5C,QAAO,IAAI,WAAW,IAAI;;AAG5B,SAAgB,wBAAwB,OAA8C;CACpF,MAAM,OAAO,MAAM,KAAK,IAAI,mBAAmB;AAC/C,QAAO;EACL,OAAO,MAAM;EACb,MAAM,KAAK,KAAK,IAAI;EACpB,WAAW,MAAM;EACjB;EACD;;AAGH,SAAgB,oBAAoB,YAA6C;AAC/E,QAAO,WACJ,kBAAkB,2BAA2B,CAC7C,KAAK,UAAU,wBAAwB,MAAM,CAAC;;AAGnD,SAAgB,oBAAoB,YAA6C;CAC/E,MAAM,WAAW,WAAW,kBAAkB,4BAA4B;CAC1E,MAAM,YAAY,WAAW,kBAAkB,2BAA2B;CAE1E,MAAM,sCAAsB,IAAI,KAA2C;AAC3E,MAAK,MAAM,YAAY,UACrB,qBAAoB,IAAI,SAAS,WAAW,SAAS;AAGvD,QAAO,SAAS,KAAK,YAA2C;EAC9D,MAAM,WAAW,oBAAoB,IAAI,QAAQ,UAAU;AAC3D,SAAO;GACL,WAAW,QAAQ;GACnB,KAAK,QAAQ,QAAQ;GACrB,QAAQ,QAAQ,QAAQ;GACxB,QAAQ,WAAW,SAAS,SAAS,SAAS;GAC9C,YAAY,WAAW,SAAS,SAAS,aAAa;GACtD,WAAW,QAAQ;GACnB,SAAS,WAAW,SAAS,YAAY;GAC1C;GACD;;;AAqCJ,SAAS,gBAAgB,OAA6B;AAEpD,QAAO,MADI,MAAM,gBAAgB,cACjB,IAAI,MAAM,IAAI,GAAG,MAAM,WAAW,GAAG,MAAM,aAAa;;;AAI1E,SAAgB,mBAAmB,OAAuD;CACxF,MAAM,EAAE,WAAW,qBAAqB;CACxC,MAAM,SAAS,iBAAiB,YAAY;CAC5C,MAAM,QAAQ,UAAU,OAAO,SAAS,IAAI,OAAO,IAAI,gBAAgB,CAAC,KAAK,KAAK,GAAG,KAAA;CACrF,MAAM,gBAAgB,iBAAiB,WAAW,eAAe,KAAA;CAEjE,MAAM,SAA4B;EAChC;EACA,MAAM,iBAAiB;EACvB,KAAK;EACN;AACD,KAAI,iBAAiB,QAAQ,KAAA,EAAW,QAAO,MAAM,iBAAiB;AACtE,KAAI,iBAAiB,eAAe,KAAA,EAAW,QAAO,aAAa,iBAAiB;AACpF,KAAI,iBAAiB,iBAAiB,KAAA,EACpC,QAAO,eAAe,iBAAiB;AACzC,KAAI,kBAAkB,KAAA,EAAW,QAAO,gBAAgB;AACxD,KAAI,UAAU,KAAA,EAAW,QAAO,QAAQ;AACxC,QAAO;;;;;;AAOT,SAAgB,eAAe,YAA2B,QAAQ,IAAyB;CACzF,MAAM,MAAM,KAAK,IAAI,KAAK,IAAI,GAAG,MAAM,EAAE,GAAG;CAC5C,MAAM,SAAS,WAAW,kBAAkB,0BAA0B;AAGtE,SADe,OAAO,SAAS,MAAM,OAAO,MAAM,OAAO,SAAS,IAAI,GAAG,QAC3D,KAAK,MAAM,mBAAmB,EAAE,CAAC;;AA8CjD,SAAS,aAAa,MAAsD;AAC1E,QACE,OAAQ,KAAiC,2BAA2B,cACpE,OAAQ,KAAiC,wBAAwB;;AAIrE,SAAgB,UAAU,YAA2B,QAAuC;CAE1F,MAAM,QADa,WAAW,aAAa,CACA,KAAK,MAAM;EACpD,MAAM,aAAa,aAAa,WAAW,GAAG,WAAW,oBAAoB,EAAE,GAAG,GAAG;AACrF,SAAO;GACL,IAAI,EAAE;GACN,OAAO,EAAE;GACT,KAAK,EAAE;GACP,YAAY,eAAe,OAAO,IAAI,KAAK,WAAW,CAAC,aAAa,GAAG;GACxE;GACD;CAEF,MAAM,UAAU,aAAa,WAAW,GAAG,WAAW,wBAAwB,GAAG;CACjF,MAAM,kBAAkB,YAAY,OAAO,IAAI,KAAK,QAAQ,CAAC,aAAa,GAAG;AAK7E,QAAO;EAAE;EAAO;EAAQ;EAAiB,cAJpB,kBACjB,oDAAoD,gBAAgB,KACpE;EAEmD,mBAAmB;EAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DlF,SAAgB,eACd,WACA,QACA,YACsB;AACtB,KAAI,CAAC,OAAO,MAAM,OAAO,WAAW,KAClC,OAAM,IAAI,MACR,0FAED;CAEH,MAAM,mBAAmB,wBAAwB,UAAU,IAAI,KAAA;CAI/D,IAAI;CACJ,IAAI;AACJ,KAAI,eAAe,KAAA,KAAa,eAAe,IAAI;EACjD,MAAM,MAAM,KAAK,KAAK;AACtB,aAAW,aAAa,YAAY,IAAI;EACxC,MAAM,eAAe;EAQrB,MAAM,cAAc,MAAA,IAAgC,eAAe;AACnE,aAAW;GACT,SAAS;GACT,YAAA,IAAsC;GACtC,WAAW,IAAI,KAAK,YAAY,CAAC,aAAa;GAC/C;;AAGH,QAAO;EACL,WAAW,uBAAuB,WAAW,OAAO,QAAQ,SAAS;EACrE,UAAU,OAAO;EACjB,GAAI,qBAAqB,KAAA,IAAY,EAAE,kBAAkB,GAAG,EAAE;EAC9D,GAAI,aAAa,KAAA,IAAY,EAAE,MAAM,UAAU,GAAG,EAAE;EACrD;;;;;;;;;;;AAgBH,SAAgB,iBAA0B;AACxC,KAAI,QAAQ,IAAI,OAAO,UAAU,QAAQ,IAAI,OAAO,IAAK,QAAO;CAChE,MAAM,WAAW,QAAQ;AACzB,KAAI,aAAa,YAAY,aAAa,QAAS,QAAO;AAC1D,KAAI,aAAa,QACf,QAAO,QAAQ,QAAQ,IAAI,WAAW,QAAQ,IAAI,gBAAgB;AAEpE,QAAO;;;AA6BT,SAAS,qBAAqB,SAAyD;CACrF,MAAM,WAAW,QAAQ;AACzB,KAAI,aAAa,SACf,QAAO;EACL;GAAE,KAAK;GAAQ,MAAM,CAAC,QAAQ;GAAE;EAChC;GAAE,KAAK;GAAQ,MAAM;IAAC;IAAM;IAAU;IAAQ;GAAE;EAChD;GAAE,KAAK;GAAQ,MAAM;IAAC;IAAM;IAAiB;IAAQ;GAAE;EACvD;GAAE,KAAK;GAAQ,MAAM;IAAC;IAAM;IAAW;IAAQ;GAAE;EAClD;AAEH,KAAI,aAAa,QACf,QAAO,CACL;EAAE,KAAK;EAAO,MAAM;GAAC;GAAM;GAAS;GAAI;GAAQ;EAAE,EAClD;EAAE,KAAK;EAAY,MAAM,CAAC,+BAA+B,QAAQ;EAAE,CACpE;AAGH,QAAO;EACL;GAAE,KAAK;GAAY,MAAM,CAAC,QAAQ;GAAE;EACpC;GAAE,KAAK;GAAoB,MAAM,CAAC,QAAQ;GAAE;EAC5C;GAAE,KAAK;GAAiB,MAAM,CAAC,QAAQ;GAAE;EACzC;GAAE,KAAK;GAAW,MAAM,CAAC,QAAQ;GAAE;EACnC;GAAE,KAAK;GAAiB,MAAM,CAAC,QAAQ;GAAE;EACzC;GAAE,KAAK;GAAY,MAAM,CAAC,QAAQ;GAAE;EACrC;;;AAIH,SAAS,cAAc,MAAsB;AAE3C,QAAO,KAAK,QAAQ,qBAAqB,gBAAgB;;;AAI3D,MAAM,0BAA0B;CAC9B;CACA;CACA;CACA;CACA;CACA;CACD;AAED,SAAS,sBAAsB,QAAyB;AACtD,QAAO,wBAAwB,MAAM,MAAM,EAAE,KAAK,OAAO,CAAC;;;;;;;;;;;;;;;;;;AAmB5D,eAAsB,gBACpB,SACA,QACgC;CAChC,MAAM,EAAE,cAAc,MAAM,OAAO;;;;;CAMnC,SAAS,QAAQ,aAAgC;EAC/C,MAAM,aAAa,qBAAqB,QAAQ;AAChD,OAAK,MAAM,EAAE,KAAK,UAAU,YAAY;GACtC,MAAM,SAAS,UAAU,KAAK,MAAM;IAAE,UAAU;IAAQ,SAAS;IAAM,CAAC;AAExE,OAAI,OAAO,OAAO;AAChB,gBAAY,KAAK,GAAG,IAAI,IAAI,OAAO,MAAM,UAAU;AACnD;;GAGF,MAAM,SAAS,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AACnE,OAAI,OACF,aAAY,KAAK,GAAG,IAAI,IAAI,cAAc,OAAO,MAAM,CAAC,GAAG;AAG7D,OAAI,OAAO,WAAW,KAAK,CAAC,sBAAsB,OAAO,CACvD,QAAO;;AAGX,SAAO;;CAGT,MAAM,cAAwB,EAAE;AAGhC,KAAI,QAAQ,YAAY,CACtB,QAAO;EAAE,QAAQ;EAAM;EAAS;EAAQ;AAI1C,KAAI,QAAQ,YAAY,CACtB,QAAO;EAAE,QAAQ;EAAM;EAAS;EAAQ,SAAS;EAAM;AAIzD,QAAO;EACL,QAAQ;EACR;EACA;EACA,OAAO;EACP,eANoB,YAAY,SAAS,IAAI,YAAY,KAAK,KAAK,GAAG,KAAA;EAOvE;;;AAQH,SAAgB,eAAe,YAA0D;AAGvF,QAAO,WAAW,KAAK,mBAAmB;EAAE,OAAO;EAAI,QAAQ;EAAM,CAAC;;;AAIxE,SAAgB,aAAa,YAAuD;AAClF,QAAO,WAAW,KAAK,+BAA+B,EAAE,CAAC;;;AAa3D,eAAsB,eAAe,YAAsD;CACzF,MAAM,EAAE,SAAS,MAAM,WAAW,KAAK,0BAA0B,EAAE,QAAQ,OAAO,CAAC;AACnF,QAAO;EAAE;EAAM,SAAS,yBAAyB;EAAQ,UAAU;EAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmClF,MAAa,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmExC,MAAM;;;;;;;;;;AAuGR,SAAgB,wBACd,UACA,QACqB;AACrB,KAAI,OAAO,aAAa,SACtB,OAAM,IAAI,MACR,sDAAsD,OAAO,SAAS,0BACvE;CAEH,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,SAAS;SACvB;AACN,QAAM,IAAI,MAAM,sDAAsD,WAAW;;AAEnF,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MAAM,oDAAoD;CAEtE,MAAM,MAAM;CAEZ,SAAS,cACP,KACqE;EACrE,MAAM,IAAI,IAAI;AACd,MAAI,MAAM,QAAQ,MAAM,KAAA,EAAW,QAAO;AAC1C,MAAI,OAAO,MAAM,SAAU,QAAO;EAClC,MAAM,IAAI;AACV,SAAO;GACL,KAAK,OAAO,EAAE,QAAQ,WAAW,EAAE,MAAM;GACzC,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;GAC/C,QAAQ,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;GAClD,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;GAC7C;;CAGH,MAAM,SAAS,cAAc,SAAS,IAAI;EAAE,KAAK;EAAG,OAAO;EAAG,QAAQ;EAAG,MAAM;EAAG;CAClF,MAAM,YAAY,cAAc,YAAY;CAC5C,MAAM,kBACJ,IAAI,oBAAoB,kBAAkB,IAAI,oBAAoB,iBAC9D,IAAI,kBACJ;CACN,MAAM,iBAAiB,OAAO,IAAI,mBAAmB,WAAW,IAAI,iBAAiB,KAAA;CACrF,MAAM,eAAe,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;CAC/E,MAAM,qBACJ,OAAO,IAAI,uBAAuB,WAAW,IAAI,qBAAqB;CACxE,MAAM,aAAa,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;CACzE,MAAM,cAAc,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;CAC5E,MAAM,mBAAmB,OAAO,IAAI,qBAAqB,WAAW,IAAI,mBAAmB;CAC3F,MAAM,YAAY,OAAO,IAAI,cAAc,WAAW,IAAI,YAAY;AAEtE,QAAO;EACL;EACA;EACA;EACA;EACA,GAAI,mBAAmB,KAAA,IAAY,EAAE,gBAAgB,GAAG,EAAE;EAC1D;EACA;EACA;EACA;EACA;EACA;EACD;;;;;;;;;;;;;;;AAgBH,eAAsB,gBACpB,YACA,QAC8B;CAC9B,MAAM,SAAS,MAAM,WAAW,KAAK,oBAAoB;EACvD,YAAY;EACZ,eAAe;EACf,cAAc;EACf,CAAC;AACF,KAAI,OAAO,kBAAkB;EAC3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,oCAAoC,MAAM;;AAE5D,QAAO,wBAAwB,OAAO,OAAO,OAAO,OAAO;;;;;;;;;;AAgC7D,eAAsB,SACpB,YACA,YACyB;CACzB,MAAM,SAAS,MAAM,WAAW,KAAK,oBAAoB;EACvD;EACA,eAAe;EACf,cAAc;EACf,CAAC;AACF,KAAI,OAAO,kBAAkB;EAE3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,oBAAoB,MAAM;;AAE5C,QAAO;EAAE,OAAO,OAAO,OAAO;EAAO,MAAM,OAAO,OAAO;EAAM;;;;;;;;;;;;;;AAiCjE,SAAgB,uBAAuB,MAAc,MAAyB;AAG5E,QACE,yOAHe,KAAK,UAAU,KAAK,CAQY,OAPhC,KAAK,UAAU,KAAK,CAO4B;;;;;;;;AAenE,SAAgB,uBAAuB,UAAkC;AACvE,KAAI,OAAO,aAAa,SACtB,OAAM,IAAI,MACR,8CAA8C,OAAO,SAAS,0BAC/D;CAEH,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,SAAS;SACvB;AAEN,QAAM,IAAI,MAAM,4CAA4C;;AAE9D,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MAAM,2CAA2C;CAE7D,MAAM,MAAM;AACZ,KAAI,IAAI,OAAO,KACb,QAAO;EAAE,IAAI;EAAM,OAAO,IAAI;EAAO;AAEvC,KAAI,IAAI,OAAO,MACb,QAAO;EAAE,IAAI;EAAO,OAAO,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ,OAAO,IAAI,MAAM;EAAE;AAE5F,OAAM,IAAI,MAAM,+CAA6C;;;;;;;;;;;;;AAc/D,SAAS,oBACP,YACA,aACA,WAC+B;CAC/B,MAAM,SAAS,WAAW,kBAAkB,0BAA0B;AAEtE,MAAK,IAAI,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;EAC3C,MAAM,IAAI,OAAO;AACjB,MAAI,EAAE,aAAa,eAAe,EAAE,aAAa,UAC/C,QAAO,mBAAmB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;AA4BlC,eAAsB,QACpB,YACA,MACA,MACwB;CAExB,MAAM,YAAY,gBAAgB,KAAK;AACvC,KAAI,cAAc,KAAA,GAAW;EAC3B,MAAM,aAAa,UAAU,aAAa,KAAK;AAC/C,MAAI,CAAC,WAAW,GAOd,QAAO;GAAE,IAAI;GAAO,OAJlB,aAAa,KAAK,sBACX,WAAW,SAAS,QACpB,WAAW,SAAS,YAChB,UAAU;GACe;OAIxC,iBAAgB,KAAK;CAGvB,MAAM,YAAY,KAAK,KAAK;CAC5B,MAAM,aAAa,uBAAuB,MAAM,KAAK;CACrD,MAAM,SAAS,MAAM,WAAW,KAAK,oBAAoB;EACvD;EACA,eAAe;EACf,cAAc;EACf,CAAC;CACF,MAAM,UAAU,KAAK,KAAK;AAE1B,KAAI,OAAO,kBAAkB;EAE3B,MAAM,MACJ,OAAO,iBAAiB,WAAW,eACnC,OAAO,iBAAiB,QACxB;AACF,QAAM,IAAI,MAAM,mBAAmB,MAAM;;CAG3C,MAAM,YAAY,uBAAuB,OAAO,OAAO,MAAM;CAK7D,MAAM,kBAAkB,oBAAoB,YAAY,YAAY,IAAI,UAAU,IAAI;AAEtF,KAAI,oBAAoB,KAAA,EACtB,QAAO;EAAE,GAAG;EAAW;EAAiB;AAE1C,QAAO;;;AAQT,MAAM,iBAAiB,IAAI,IAAY;CACrC;CACA;CACA;CACD,CAAC;;AAGF,SAAgB,cAAc,MAAuB;AACnD,QAAO,eAAe,IAAI,KAAK;;;AAIjC,SAAgB,kBAAkB,QAA+C;AAC/E,QAAO,OAAO,IAAI,wBAAwB;;;AAI5C,SAAgB,aAAa,QAA0C;AACrE,QAAO,OAAO,IAAI,mBAAmB;;;AAIvC,SAAgB,0BAA0B,QAAuD;AAC/F,QAAO,OAAO,IAAI,gCAAgC;;;AA2MpD,MAAM,yBAA0D;CAE9D,CAAC,qBAAqB,gBAAgB;CAEtC,CAAC,gCAAgC,iBAAiB;CAElD,CAAC,6BAA6B,2BAA2B;CAEzD,CAAC,4BAA4B,4BAA4B;CACzD,CAAC,mBAAmB,oBAAoB;CACzC;;;;;;;;AASD,SAAgB,mBAAmB,SAAyB;CAC1D,IAAI,SAAS;AACb,MAAK,MAAM,CAAC,SAAS,gBAAgB,uBACnC,UAAS,OAAO,QAAQ,SAAS,YAAY;AAE/C,QAAO;;;AAIT,MAAM,4BAA4B;;;;;AAMlC,IAAa,+BAAb,MAA0E;CACxE,SAA8C,EAAE;CAChD;CACA,eAAsC;CACtC,eAAsC;CACtC,kBAA0B;CAC1B,mBAA0C;CAE1C,YAAY,UAAU,2BAA2B;AAC/C,OAAK,UAAU;;CAGjB,YAAY,SAAiB,UAAyB;EACpD,MAAM,QAA0B;GAC9B,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC,SAAS,mBAAmB,QAAQ;GACpC,GAAI,aAAa,KAAA,IAAY,EAAE,UAAU,GAAG,EAAE;GAC/C;AACD,OAAK,OAAO,KAAK,MAAM;AAEvB,MAAI,KAAK,OAAO,SAAS,KAAK,QAC5B,MAAK,OAAO,OAAO;;CAIvB,gBAAgB,OAAmC;EACjD,MAAM,MAAM,KAAK,IAAI,KAAK,IAAI,GAAG,MAAM,EAAE,0BAA0B;AAGnE,SADE,KAAK,OAAO,SAAS,MAAM,KAAK,OAAO,MAAM,KAAK,OAAO,SAAS,IAAI,GAAG,CAAC,GAAG,KAAK,OAAO;;CAI7F,eAAqB;AACnB,OAAK,gCAAe,IAAI,MAAM,EAAC,aAAa;;CAG9C,eAAqB;AACnB,OAAK,gCAAe,IAAI,MAAM,EAAC,aAAa;;CAG9C,kBAAiC;AAC/B,SAAO,KAAK;;CAGd,kBAAiC;AAC/B,SAAO,KAAK;;CAGd,mBAAyB;AACvB,OAAK,mBAAmB;AACxB,OAAK,oCAAmB,IAAI,MAAM,EAAC,aAAa;;CAGlD,iBAAsC;AACpC,SAAO;GAAE,OAAO,KAAK;GAAiB,QAAQ,KAAK;GAAkB;;;;;;;;;;;;;;;;;;;;AAqBzE,eAAsB,oBAA4C;AAG9D,QAAA;;;;;;;AA0BJ,SAAgB,sBAAqC;AASnD,QAAA;;;;;;;;;;;;;;;;;;;;AAqBF,SAAgB,6BACd,QACA,OACA,KACA,cAA0C,MACZ;AAG9B,KAAI,OAAO,aAAa,KACtB,QAAO;EACL,MAAM;EACN,QACE,iCAAiC,OAAO,UAAU,SAAS,OAAO,gBAAgB;EAErF;AAIH,KAAI,CAAC,OAAO,GAIV,KAAI,CAAC,WAAW,IAAI;MAGd,UAAU,QAAQ,MAAM,MAAM,WAAW,KAAK,CAAC,MAAM,gBACvD,QAAO;GACL,MAAM;GACN,QACE;GAEH;OAKH,QAAO;EACL,MAAM;EACN,QAAQ;EACT;AAQL,KAAI,gBAAgB,QAAQ,YAAY,QAAQ,KAAK,UAAU,QAAQ,MAAM,MAAM,WAAW,EAC5F,QAAO;EACL,MAAM;EACN,QACE,qBAAqB,YAAY,MAAM,aAAa,YAAY,UAAU,UAAU;EAGvF;AAIH,KAAI,WAAW,IAAI,IAAI,UAAU,QAAQ,MAAM,MAAM,WAAW,KAAK,CAAC,MAAM,gBAC1E,QAAO;EACL,MAAM;EACN,QAAQ;EACT;AAIH,KAAI,UAAU,QAAQ,MAAM,oBAAoB,KAC9C,QAAO;EACL,MAAM;EACN,QAAQ,mBAAmB,MAAM,gBAAgB;EAClD;AAIH,QAAO;;;;;;;;;;;;AAsDT,eAAsB,eAAe,OAAwD;CAC3F,MAAM,EACJ,QACA,YACA,KACA,WACA,WACA,UAAU,YACV,oBAAoB,IACpB,gBAAgB,mBAChB,yBAAyB,WAAW,QAAQ,KAAK,EACjD,mBACE;CAEJ,MAAM,CAAC,YAAY,mBAAmB,MAAM,QAAQ,IAAI,CACtD,eAAe,EACf,QAAQ,QAAQ,qBAAqB,CAAC,CACvC,CAAC;CAGF,MAAM,WAAW,YAAY;CAC7B,MAAM,mBAAiD,WACnD;EAAE,KAAK,SAAS;EAAK,WAAW,SAAS;EAAW,QAAQ,SAAS;EAAQ,GAC7E;CAWJ,MAAM,0BAA0B,kBAAkB,UAAU,kBAAkB;CAC9E,IAAI,cAAc,OAAO;AACzB,KACE,OAAO,MACP,OAAO,4BAA4B,YACnC,4BAA4B,QAC5B,CAAC,WAAW,wBAAwB,CAEpC,eAAc;CAGhB,MAAM,aAAoC;EACxC,IAAI;EACJ,QAAQ,OAAO;EACf,KAAK,UAAU,OAAO;EACtB,WAAW,UAAU,aAAa;EAClC,WAAW,OAAO,aAAa;EAC/B,iBAAiB,OAAO,mBAAmB;EAC5C;CAOD,IAAI,QAAgC;AACpC,KAAI,eAAe,KAAA,GAAW;AAC5B,MAAI;AACF,SAAM,WAAW,kBAAkB;UAC7B;AAGR,MAAI;AACF,WAAQ,UAAU,YAAY,OAAO;UAC/B;;CAKV,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,GAAG,kBAAkB,EAAE,GAAG;CAC1D,MAAM,eAAe,UAAU,gBAAgB,MAAM;CAMrD,MAAM,cAAc,UAAU,gBAAgB;AAC9C,KAAI,YAAY,QAAQ,EACtB,cAAa,KAAK;EAChB,WAAW,YAAY,2BAAU,IAAI,MAAM,EAAC,aAAa;EACzD,SAAS,6BAA6B,YAAY,MAAM,eAAe,YAAY,UAAU,UAAU;EACvG,UAAU;EACX,CAAC;CAGJ,MAAM,wBAAwB,6BAA6B,YAAY,OAAO,KAAK,YAAY;AAE/F,QAAO;EACL;EACA;EACA,QAAQ;EACR;EACA,cAAc,UAAU,iBAAiB;EACzC,cAAc,UAAU,iBAAiB;EACzC;EACA;EACA,aAAa;GACX,MAAM;GACN,KAAK,YAAY,IAAI;GACrB,QAAQ;GACR,iBAAiB,eAAe,IAAI;GACrC;EACD;EACA,SAAS;GACP,KAAK,QAAQ;GACb,MAAM,QAAQ;GACd,aAAa,kBAAkB;GAChC;EACD;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACpsEH,SAAgB,sBAA8B;AAC5C,QAAO,YAAY,GAAG,CAAC,SAAS,MAAM;;;AA2BxC,eAAe,uBAAsC;CACnD,MAAM,EAAE,eAAe,MAAM,OAAO;AACpC,KAAI,CAAC,WAAW,IAAI,CAClB,OAAM,QAAQ,IAAI;;;;;;;;;;;AAatB,eAAsB,iBAAiB,WAAyC;AAC9E,OAAM,sBAAsB;CAE5B,MAAM,SAAS,OAAO,MAAM,oBAAoB,YAAY;CAE5D,MAAM,MAAM,MAAM,IAAI,SAAiB,SAAS,WAAW;EACzD,MAAM,SAAS,aAAqB;AAClC,YAAS;AACT,WAAQ,SAAS;;EAEnB,MAAM,WAAW,QAAe;AAC9B,YAAS;AACT,UAAO,IAAI;;EAEb,MAAM,UAAU,SAAwB;AACtC,YAAS;AACT,0BAAO,IAAI,MAAM,mDAAmD,KAAK,GAAG,CAAC;;EAE/E,MAAM,gBAAgB;AACpB,UAAO,IAAI,OAAO,MAAM;AACxB,UAAO,IAAI,SAAS,QAAQ;AAC5B,UAAO,IAAI,QAAQ,OAAO;;AAE5B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,KAAK,SAAS,QAAQ;AAC7B,SAAO,KAAK,QAAQ,OAAO;GAC3B;CAKF,IAAI,kBAAkB;CACtB,IAAI,mBAA2D;AAE/D,QAAO,KAAK,SAAS,SAAwB;AAC3C,MAAI,CAAC,mBAAmB,qBAAqB,KAC3C,kBAAiB,KAAK;GAExB;AAEF,QAAO;EACL;EACA,QAAQ,IAAI,QAAQ,UAAU,MAAM;EACpC,UAAW,OAAO,SAAqC;EACvD,iBAAiB,IAAyC;AACxD,sBAAmB;;EAErB,OAAa;AACX,qBAAkB;AAClB,UAAO,MAAM;;EAEhB;;;;;;;;;;;;;;;;;;;AAgCH,eAAsB,SAAS,MAA+B;CAG5D,MAAM,EAAE,SAAS,WAAW,MAAM,OAAO;CACzC,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE,sBAAsB,KAAK,CAAC;CAC7D,MAAM,OAAe,GAAG,QAAQ;CAChC,MAAM,OAAmB,GAAG,QAAQ;CAEpC,MAAM,UAAU,GAAW,MAAuB;AAChD,MAAI,IAAI,KAAK,IAAI,KAAK,KAAK,QAAQ,KAAK,KAAM,QAAO;AACrD,SAAO,KAAK,IAAI,OAAO,OAAO;;CAGhC,MAAM,QAAQ;CACd,MAAM,QAAkB,EAAE;AAC1B,MAAK,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,GAAG;EAC7C,IAAI,OAAO;AACX,OAAK,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK;GAC1C,MAAM,MAAM,OAAO,GAAG,EAAE;GACxB,MAAM,MAAM,OAAO,GAAG,IAAI,EAAE;AAC5B,WAAQ,OAAO,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM;;AAErD,QAAM,KAAK,KAAK;;AAElB,QAAO,GAAG,MAAM,KAAK,KAAK,CAAC;;;;;;;;;;;;AAa7B,eAAsB,mBAAmB,OAA2C;CAIlF,MAAM,KAAK,MAAM,SAAS,MAAM,OAAO;CAEvC,MAAM,WAAW,MAAM,cACnB,+EACA;AAEJ,QAAO;EACL;EACA;EACA;EACA,oBAAoB,MAAM;EAC1B;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK;;;AAId,eAAsB,kBAAkB,OAAyC;CAC/E,MAAM,SAAS,MAAM,mBAAmB,MAAM;AAC9C,SAAQ,OAAO,MAAM,GAAG,OAAO,IAAI;;;;;;;;;;;;;;AAsBrC,eAAsB,YAAY,UAAkB,YAAY,KAA0B;CACxF,MAAM,EAAE,SAAS,UAAU,MAAM,OAAO;AACxC,QAAO,IAAI,SAAkB,YAAY;EACvC,MAAM,MAAM,IAAI,IAAI,SAAS;EAC7B,MAAM,QAAQ,iBAAiB;AAC7B,OAAI,SAAS;AACb,WAAQ,MAAM;KACb,UAAU;EAEb,MAAM,MAAM,MAAM,QAChB;GAAE,UAAU,IAAI;GAAU,MAAM;GAAK,MAAM,IAAI,YAAY;GAAK,QAAQ;GAAQ,GAC/E,SAAS;AACR,gBAAa,MAAM;AACnB,QAAK,QAAQ;AACb,WAAQ,KAAK;IAEhB;AACD,MAAI,GAAG,eAAe;AACpB,gBAAa,MAAM;AACnB,WAAQ,MAAM;IACd;AACF,MAAI,KAAK;GACT;;;;;;;;;;;;;;;;;AAwDJ,SAAgB,uBACd,eACA,WACA,SACkB;CAClB,MAAM,EACJ,kBAAkB,KAClB,wBAAwB,GACxB,WACA,iBACA,OAAO,QAAgB,QAAQ,OAAO,MAAM,IAAI,EAChD,QAAQ,aACR,cAAc,qBACZ;CAEJ,IAAI,gBAAgB;CACpB,IAAI,sBAAsB;CAC1B,IAAI,kBAAkB;CACtB,IAAI,UAAU;CAKd,MAAM,kBAAkB,YAA2B;AACjD,MAAI,QAAS;AAEb,qBAAmB;AACnB,MAAI,kBAAA,EAEF;AAGF,MACE,yDAAyD,gBAAgB,OAC1E;AAED,MAAI;GACF,MAAM,YAAY,MAAM,YAAY,UAAU;AAE9C,OAAI;AACF,kBAAc,MAAM;WACd;AAGR,mBAAgB;AAChB,yBAAsB;AAEtB,qBAAkB,UAAU;AAC5B,OAAI,4CAA4C,UAAU,OAAO,IAAI;AACrE,aAAU,UAAU;WACb,KAAK;GACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,OAAI,sCAAsC,gBAAgB,WAAW,QAAQ,IAAI;AAEjF,OAAI,mBAAA,GAAyC;AAC3C,kBAAc,OAAO;AACrB,cAAU;IACV,MAAM,6BAAY,IAAI,MAAM,EAAC,aAAa;AAC1C,QACE;EAED;AACD,oBAAgB,UAAU;;;;CAOhC,MAAM,qBAAqB,MAAyB;AAClD,IAAE,kBAAkB,SAAS;AAC3B,OAAI,QAAS;AACb,OACE,2DAA2D,KAAK,oCACjE;AAGD,yBAAsB;AACjB,oBAAiB;IACtB;;AAIJ,mBAAkB,cAAc;CAEhC,MAAM,SAAS,kBAAkB;AAC/B,GAAM,YAAY;AAChB,OAAI,QAAS;GAEb,MAAM,WAAW,cAAc;AAG/B,OAFc,MAAM,MAAM,SAAS,EAExB;AAET,QAAI,sBAAsB,EACxB,KAAI,sDAAsD;AAE5D,0BAAsB;AACtB,sBAAkB;AAClB;;AAGF,0BAAuB;AACvB,OACE,4CAA4C,oBAAoB,GAAG,sBAAsB,QAAQ,SAAS,KAC3G;AAED,OAAI,sBAAsB,sBAExB;AAIF,SAAM,iBAAiB;MACrB;IACH,gBAAgB;AAEnB,QAAO,EACL,OAAO;AACL,YAAU;AACV,gBAAc,OAAO;IAExB;;;;;;;;AASH,SAAgB,iBACd,IACA,QACA,YAA2B,MAC3B,kBAAkB,GACJ;AACd,QAAO;EAAE;EAAI;EAAQ;EAAW;EAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACzTnD,MAAa,8BAA8B,MAAS;;;;;;;;;;;;;;;;;;;;;;AAuBpD,SAAgB,mBACd,OACA,eACA,OACA,sBACS;AACT,KAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,KAAI,kBAAkB,KAAM,QAAO;AACnC,QAAO,MAAM,MAAM,MAAM;EACvB,MAAM,SAAS,cAAc,EAAE,GAAG;AAElC,MAAI,WAAW,KAAM,QAAO;AAC5B,SAAO,QAAQ,UAAU;GACzB;;;;;;;;;;;;AAaJ,SAAgB,oBAAoB,WAAkC;AACpE,KAAI;EAIF,MAAM,SAAS,UAAU,QAAQ,IAAI;AACrC,MAAI,WAAW,GAAI,QAAO;EAE1B,MAAM,KADS,IAAI,gBAAgB,UAAU,MAAM,SAAS,EAAE,CAAC,CAC7C,IAAI,gBAAgB;AACtC,SAAO,MAAM,GAAG,SAAS,IAAI,KAAK;SAC5B;AACN,SAAO;;;;;;;;;AA6DX,SAAgB,YAAY,MAA+B;AACzD,QAAO,SAAS,mBAAmB,SAAS,mBAAmB,SAAS;;;;;;;;;;;;;;;;;;AAuN1E,SAAS,wBACP,YACA,UACA,WACA,iBAAiB,KACkC;AAGnD,KAAI,WAAW,mBACb,QAAO,WAAW,mBAAmB,UAAU,WAAW,eAAe;AAI3E,QAAO,IAAI,SAAmD,SAAS,WAAW;EAChF,MAAM,WAAW,KAAK,KAAK,GAAG;EAC9B,IAAI,UAAU;EACd,MAAM,OAAO,kBAAkB;GAC7B,MAAM,UAAU,WAAW,aAAa;AACxC,OAAI,SAAS,QAAQ,EAAE;AACrB,cAAU;AACV,kBAAc,KAAK;AACnB,YAAQ,QAAQ;cACP,KAAK,KAAK,IAAI,UAAU;AACjC,cAAU;AACV,kBAAc,KAAK;AACnB,2BAAO,IAAI,MAAM,kCAAkC,UAAU,KAAK,CAAC;;KAEpE,eAAe;EAElB,MAAM,UAAU,WAAW,aAAa;AACxC,MAAI,CAAC,WAAW,SAAS,QAAQ,EAAE;AACjC,aAAU;AACV,iBAAc,KAAK;AACnB,WAAQ,QAAQ;;GAElB;;;;;;;;;;;;;;AAeJ,SAAgB,kBAAkB,MAA+B;CAC/D,MAAM,EACJ,YACA,QAAQ,WACR,WACA,iBACA,yBAAyB,KACzB,cACA,gBAAgB,WAChB,sBAAsB,iBACtB,sBAAsB,cACtB,YACA,kBACA,mBACA,UAAU,aACV,uBAAuB,6BACvB,cAAc,KAAK,KAAK,KACtB;CAOJ,MAAM,gBAAgB,KAAK,wBAAwB;CAMnD,MAAM,aAAa,eAAe;CASlC,MAAM,SAA2B,aAAa,2BAA2B,WAAW;CAMpF,MAAM,qBACJ,oBACO,kBAAkB,OAAO,OAAO,MAAM,eAAe,EAAE,OAAO,kBAAkB;CACzF,MAAM,2BACJ,0BAA0B,gBAAgB,OAAO,OAAO,KAAK,cAAc,eAAe;CAK5F,MAAM,YAAkC,gBAAgB,IAAI,8BAA8B;CAE1F,MAAM,SAAS,IAAI,OACjB;EAAE,MAAM;EAAa,SAAA;EAAsB,EAG3C,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa,MAAM,EAAE,EAAE,CACnD;AAED,QAAO,kBAAkB,8BAA8B;EAIrD,MAAM,OAAO,OAAO;EACpB,MAAM,MAAM,oBAAoB;EAChC,MAAM,WAAW,KAAK,aAAa,CAAC,SAAS;EAE7C,MAAM,cAAc,yBAAyB,wBAAwB,IAAI;AAMzE,SAAO,EAAE,OALK,WACV,YAAY,KAAK,UAAU,EAAE,GAAG,MAAM,EAAE,GACxC,YACG,QAAQ,SAAS,qBAAqB,IAAI,KAAK,KAAK,CAAC,CACrD,KAAK,UAAU,EAAE,GAAG,MAAM,EAAE,EACnB;GAChB;AAEF,QAAO,kBAAkB,uBAAuB,OAAO,YAAY;EACjE,MAAM,OAAO,QAAQ,OAAO;AAC5B,MAAI,CAAC,gBAAgB,KAAK,CACxB,QAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,iBAAiB;IAAQ,CAAC;GAC1D,SAAS;GACV;EASH,MAAM,OAAO,OAAO;AAKpB,MAAI,SAAS,eAAe;GAC1B,MAAM,UAAU,QAAQ,OAAO,WAAW;GAC1C,MAAM,OAAO,wBAAwB,QAAQ;AAC7C,OAAI,SAAS,KACX,QAAO,SACL,gHAED;GAEH,MAAM,UAAU,QAAQ,OAAO,WAAW,YAAY;GAKtD,MAAM,iBAAiB,QAAQ,OAAO,WAAW;GACjD,MAAM,cAAc,OAAO,mBAAmB,WAAW,iBAAiB,KAAA;AAC1E,OAAI;AAEF,WAAOC,aADQ,MAAM,OAAO,WAAW,MAAM,SAAS,YAAY,CACzC;YAClB,KAAK;AACZ,WAAO,YAAY,KAAK,KAAK;;;EAYjC,MAAM,MAAM,oBAAoB;EAChC,MAAM,YAAY,0BAA0B;AAM5C,MAAI,CAAC,kBAAkB,MAAM,IAAI,EAAE;GACjC,MAAM,cAAc,oBAAoB,KAAK,IAAI;AAEjD,WAAQ,cAAc;IACpB,MAAM;IACN,WAAW;IACX;IACA,YAAY;IACZ;IACD,CAAC;AACF,UAAO,mBAAmB,MAAM,aAAa,KAAK,UAAU;;AAM9D,MAAI,cAAc,KAAK,CACrB,KAAI;AACF,SAAM,KAAK,eAAe;AAC1B,WAAQ,MAAR;IACE,KAAK,wBACH,QAAOA,aAAW,MAAM,kBAAkB,UAAU,CAAC;IACvD,KAAK,mBACH,QAAOA,aAAW,MAAM,aAAa,UAAU,CAAC;IAClD,KAAK,gCACH,QAAOA,aAAW,MAAM,0BAA0B,UAAU,CAAC;IAC/D,QACE,QAAO,YAAY,KAAK;;WAErB,KAAK;AACZ,UAAO,YAAY,KAAK,KAAK;;AAOjC,MAAI,SAAS,mBACX,KAAI;GACF,MAAM,WAAW,QAAQ,OAAO,WAAW;GAC3C,MAAM,oBAAoB,OAAO,aAAa,YAAY,WAAW,IAAI,WAAW;AAYpF,UAAOC,iBAXQ,MAAM,eAAe;IAClC,QAAQ,iBAAiB;IACzB,YAAY;IACZ;IACA;IACA;IACA,UAAU;IACV;IACA,gBAAgB,qBAAqB,IAAI,KAAA;IAC1C,CAAC,EAE4B,MAAM,KADnB,KAAK,aAAa,CAAC,SAAS,EACK;WAC3C,KAAK;AACZ,UAAO,YAAY,KAAK,KAAK;;AAWjC,MAAI,SAAS,oBAAoB;GAC/B,MAAM,gBAAgB,QAAQ,OAAO,WAAW,oBAAoB;GAIpE,MAAM,YAAY,QAAQ,OAAO,WAAW,cAAc;GAI1D,MAAM,iBAAiB,QAAQ,OAAO,WAAW;GACjD,MAAM,uBAAuB;AAC3B,QAAI,OAAO,mBAAmB,YAAY,CAAC,OAAO,SAAS,eAAe,CACxE,QAAO;IAET,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,eAAe,CAAC;AAE1D,QAAI,kBAAkB,EAAG,QAAO;AAChC,WAAO,KAAK,MAAM,QAAQ,GAAG;OAC3B;AAIJ,OAAI,aAAa,QAAQ,eACvB,QAAO,SACL,oLAGD;AAIH,OAAI,QAAQ,gBAAgB;IAI1B,MAAM,sBAAsB,QAAQ,OAAO,WAAW;IACtD,MAAM,mBACJ,OAAO,wBAAwB,WAAW,sBAAsB,KAAA;IAElE,IAAI,gBADiB,QAAQ,IAAI,qBAAqB,MAAM,IAAI;AAEhE,QAAI,kBAAkB,MAAM,qBAAqB,KAAA,GAAW;KAC1D,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,sBADe,MAAM,cAAc,EAAE,aAAa,kBAAkB,CAAC,GAC7C,iBAAiB;;AAE3C,QAAI,kBAAkB,GACpB,QAAO,SACL,gMAGD;IAEH,MAAM,eAAe,iBAAiB;AACtC,QAAI,CAAC,aAAa,MAAM,aAAa,WAAW,KAC9C,QAAO,SACL,uHAED;IAOH,MAAM,SAAS,eAAe;AAS9B,QAAI,WAAW,KAAA,KAAa,WAAW,GACrC,QAAO,SACL,gIAED;IAEH,IAAI;IACJ,IAAI;IACJ;KACE,MAAM,MAAM,KAAK,KAAK;AACtB,gBAAW,aAAa,QAAQ,IAAI;KACpC,MAAM,eAAe;KAGrB,MAAM,cAAc,MAAA,IAAgC,eAAe;AACnE,gBAAW;MACT,SAAS;MACT,YAAA,IAAsC;MACtC,WAAW,IAAI,KAAK,YAAY,CAAC,aAAa;MAC/C;;IAKH,IAAI;AACJ,QAAI,qBAAqB,KAAA,EACvB,KAAI;KACF,MAAM,EAAE,iBAAiB,MAAM,OAAO;KACtC,MAAM,SAAS,aAAa,GAAG,iBAAiB,gBAAgB,OAAO;KACvE,MAAM,MAAM,KAAK,MAAM,OAAO;KAC9B,MAAM,UAAU,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAK1D,wBAHiB,QAAQ,SAAS,IAAI,GAClC,QAAQ,MAAM,QAAQ,QAAQ,IAAI,GAAG,EAAE,GACvC,SACuB,MAAM,IAAI,KAAA;YAC/B;IAOV,MAAM,YAAY,uBAAuB,eAAe,aAAa,QAAQ,UAAU;KACrF,MAAM;KACN,GAAI,YAAY,EAAE,WAAW,MAAM,GAAG,EAAE;KACzC,CAAC;AAIF,uBAAmB;KACjB,MAAM;KACN;KACA,QAAQ,aAAa;KACrB,SAAS;KACV,CAAC;IACF,MAAM,WAAW,aAAa;IAE9B,MAAM,OAAO;IAoBb,MAAM,YAAY;IAGlB,MAAM,gBACJ,OAAO,UAAU,wBAAwB,cACpC,OAAgB,UAAU,oBAAsD,GAAG,GACpF;IACN,MAAM,UAAU,OAAO;IAEvB,MAAM,kBAAkB,UACtB,mBAAmB,OAAO,eAAe,SAAS,qBAAqB;IACzE,MAAM,qBACJ,UACA,YACA,aACW;KACX,MAAM,eAAe,SAClB,MAAM,GAAG,EAAE,CACX,KAAK,MAAM,EAAE,IAAI,MAAM,GAAG,GAAG,CAAC,CAC9B,KAAK,KAAK;AAGb,YACE,GAAG,SAAS,8BAA8B,WAAW,GAFrD,SAAS,SAAS,IAAI,kCAAkC,aAAa,KAAK,GAEL;;AAOzE,WAAO,OAAO,YAAY;KACxB,MAAM,SACJ;KACF,MAAM,gBAEF;KACJ,MAAM,eAAe,gBAAgB;AAErC,SAAI,CAAC,cAAc;MACjB,MAAM,eACJ;MAEF,MAAM,aAAa,MAAM,SAAS,UAAU;MAC5C,MAAM,eAAe,GAAG,gBAAgB,eAAe,OAAO,IAAI,KAAK,UAAU;OAAE;OAAW;OAAU,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;OAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AACpJ,UAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;OAAE,MAAM;OAAiB,MAAM;OAAc,CAAC,EAAE;MAErE,IAAI,kBAA4D,EAAE;AAClE,UAAI;AACF,yBAAkB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;cAC9E;AACN,yBAAkB,KAAK,aAAa;AACpC,cAAO;QACL,SAAS,CACP;SACE,MAAM;SACN,MAAM,kBAAkB,cAAc,gBAAgB,KAAM,gBAAgB;SAC7E,CACF;QACD,SAAS;QACV;;MAEH,MAAM,gBAAgB,UAAU,MAAM,iBAAiB,CAAC;AACxD,aAAO,EACL,SAAS,CACP;OACE,MAAM;OACN,MAAM,GAAG,aAAa,MAAM,KAAK,UAAU,eAAe,MAAM,EAAE;OACnE,CACF,EACF;;AAGH,SAAI,gBAAgB,cAAc;MAGhC,MAAM,gBAAgB,MAAM,gBAFZ,aAAa,mBAAmB,UAAU,EAC3C,oBAAoB,aAAa,KAAK,YAAY,mBAAmB,UAAU,GAClC;AAC5D,UAAI,cAAc,QAAQ;OACxB,MAAM,cAAc,cAAc,UAAU,qBAAqB;OACjE,MAAM,aAAa;QACjB,WAAW;QACX,WAAW;QACX,GAAI,cAAc,UAAU,EAAE,SAAS,MAAM,GAAG,EAAE;QACnD;OACD,MAAM,YACJ,GAAG,gBAAgB,OAAO,IACvB,KAAK,UAAU;QAAE;QAAU;QAAY,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;QAAG,EAAE,MAAM,EAAE,CAAC,sBAC7D,YAAY,wBACvB,cAAc;AACxB,WAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;QAAE,MAAM;QAAiB,MAAM;QAAW,CAAC,EAAE;OAElE,IAAI,gBAA0D,EAAE;AAChE,WAAI;AACF,wBAAgB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;eAC5E;AACN,wBAAgB,KAAK,aAAa;AAClC,eAAO;SACL,SAAS,CACP;UACE,MAAM;UACN,MAAM,kBAAkB,WAAW,gBAAgB,KAAM,cAAc;UACxE,CACF;SACD,SAAS;SACV;;OAEH,MAAM,cAAc,UAAU,MAAM,iBAAiB,CAAC;AACtD,cAAO,EACL,SAAS,CACP;QACE,MAAM;QACN,MAAM,GAAG,UAAU,MAAM,KAAK,UAAU,aAAa,MAAM,EAAE;QAC9D,CACF,EACF;;MAEH,MAAM,aAAa;OACjB,WAAW;OACX,WAAW;OACX,eAAe,cAAc,SAAS;OACtC,QAAQ,cAAc;OACtB,GAAI,cAAc,gBACd,EAAE,eAAe,cAAc,eAAe,GAC9C,EAAE;OACP;MACD,MAAM,aAAa,cAAc,gBAC7B,aAAa,cAAc,kBAC3B;MACJ,MAAM,eACJ,+CAC2B,cAAc,QAAQ,gBAClC,cAAc,WAC7B,aACA;MACF,MAAM,KAAK,MAAM,SAAS,UAAU;MACpC,MAAM,WAAW,GAAG,gBAAgB,eAAe,OAAO,IAAI,KAAK,UAAU;OAAE;OAAW;OAAU;OAAY,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;OAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AAC5J,UAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;OAAE,MAAM;OAAiB,MAAM;OAAU,CAAC,EAAE;MAEjE,IAAI,kBAA4D,EAAE;AAClE,UAAI;AACF,yBAAkB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;cAC9E;AACN,yBAAkB,KAAK,aAAa;AACpC,cAAO;QACL,SAAS,CACP;SACE,MAAM;SACN,MAAM,kBAAkB,UAAU,gBAAgB,KAAM,gBAAgB;SACzE,CACF;QACD,SAAS;QACV;;MAEH,MAAM,gBAAgB,UAAU,MAAM,iBAAiB,CAAC;AACxD,aAAO,EACL,SAAS,CACP;OACE,MAAM;OACN,MAAM,GAAG,SAAS,MAAM,KAAK,UAAU,eAAe,MAAM,EAAE;OAC/D,CACF,EACF;;KAGH,MAAM,KAAK,MAAM,SAAS,UAAU;KACpC,MAAM,WAAW,GAAG,gBAAgB,OAAO,IAAI,KAAK,UAAU;MAAE;MAAW;MAAU,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;MAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AACjI,SAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;MAAE,MAAM;MAAiB,MAAM;MAAU,CAAC,EAAE;KAEjE,IAAI,gBAA0D,EAAE;AAChE,SAAI;AACF,sBAAgB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;aAC5E;AACN,sBAAgB,KAAK,aAAa;AAClC,aAAO;OACL,SAAS,CACP;QACE,MAAM;QACN,MAAM,kBAAkB,UAAU,gBAAgB,KAAM,cAAc;QACvE,CACF;OACD,SAAS;OACV;;KAEH,MAAM,cAAc,UAAU,MAAM,iBAAiB,CAAC;AACtD,YAAO,EACL,SAAS,CACP;MACE,MAAM;MACN,MAAM,GAAG,SAAS,MAAM,KAAK,UAAU,aAAa,MAAM,EAAE;MAC7D,CACF,EACF;QACC;;GAKN,MAAM,YAAY,QAAQ,OAAO,WAAW;AAC5C,OAAI,OAAO,cAAc,YAAY,cAAc,GACjD,QAAO,SACL,qKAGD;GAKH,MAAM,eAAe,oBAAoB,UAAU;AACnD,OAAI,CAAC,aACH,SAAQ,aAAa;IACnB,MAAM;IACN,KAAK;IACN,CAAC;;GAIJ,MAAM,kBAAkB,UAA6D;AACnF,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAI,iBAAiB,KAAM,QAAO;AAClC,WAAO,MAAM,MAAM,MAAM,EAAE,IAAI,SAAS,aAAa,CAAC;;;GAIxD,MAAM,qBACJ,UACA,YACA,aACW;IACX,MAAM,eAAe,SAClB,MAAM,GAAG,EAAE,CACX,KAAK,MAAM,EAAE,IAAI,MAAM,GAAG,GAAG,CAAC,CAC9B,KAAK,KAAK;IACb,MAAM,eACJ,SAAS,SAAS,IAAI,kCAAkC,aAAa,KAAK;AAE5E,WACE,GAAG,SAAS,aAFS,eAAe,0BAA0B,iBAAiB,GAEvC,mBAAmB,WAAW,GAAG,aAAa;;GAW1F;IACE,MAAM,cAAc,eAAe;AACnC,QAAI,gBAAgB,KAAA,KAAa,gBAAgB,GAC/C,QAAO,SACL,gIAED;;AAGL,OAAI;IAOF,MAAM,iBAAiB,iBAAiB;IACxC,MAAM,EAAE,WAAW,UAAU,kBAAkB,SAAS,eACtD,WACA,gBACA,eAAe,CAChB;AAKD,QAAI,eAAe,WAAW,KAC5B,oBAAmB;KAAE,MAAM;KAAU;KAAW,QAAQ,eAAe;KAAQ,CAAC;IAIlF,MAAM,gBAAgB,mBAAmB,sBAAsB,iBAAiB,QAAQ;IAExF,MAAM,SACJ;IAIF,MAAM,eAAe,gBAAgB;AAGrC,QAAI,CAAC,cAAc;KACjB,MAAM,eACJ;KAEF,MAAM,aAAa,MAAM,SAAS,UAAU;KAC5C,MAAM,eAAe,GAAG,gBAAgB,eAAe,OAAO,IAAI,KAAK,UAAU;MAAE;MAAW;MAAU,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;MAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AAEpJ,SAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;MAAE,MAAM;MAAiB,MAAM;MAAc,CAAC,EAAE;KAIrE,IAAI,kBAA4D,EAAE;AAClE,SAAI;AACF,wBAAkB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;aAC9E;AACN,wBAAkB,KAAK,aAAa;AACpC,aAAO;OACL,SAAS,CACP;QACE,MAAM;QACN,MAAM,kBAAkB,cAAc,gBAAgB,KAAM,gBAAgB;QAC7E,CACF;OACD,SAAS;OACV;;KAGH,MAAM,gBAAgB,UAAU,MAAM,iBAAiB,CAAC;AACxD,YAAO,EACL,SAAS,CACP;MACE,MAAM;MACN,MAAM,GAAG,aAAa,MAAM,KAAK,UAAU,eAAe,MAAM,EAAE;MACnE,CACF,EACF;;AAIH,QAAI,gBAAgB,cAAc;KAIhC,MAAM,gBAAgB,MAAM,gBAHZ,aAAa,mBAAmB,UAAU,EAC3C,oBAAoB,aAAa,KAAK,YAAY,mBAAmB,UAAU,GAElC;AAE5D,SAAI,cAAc,QAAQ;MAGxB,MAAM,cAAc,cAAc,UAAU,qBAAqB;MACjE,MAAM,aAAa;OACjB,WAAW;OACX,WAAW;OACX,GAAI,cAAc,UAAU,EAAE,SAAS,MAAM,GAAG,EAAE;OACnD;MACD,MAAM,YACJ,GAAG,gBAAgB,OAAO,IACvB,KAAK,UAAU;OAAE;OAAU;OAAY,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;OAAG,EAAE,MAAM,EAAE,CAAC,sBAC7D,YAAY,wBACvB,cAAc;AAExB,UAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;OAAE,MAAM;OAAiB,MAAM;OAAW,CAAC,EAAE;MAIlE,IAAI,gBAA0D,EAAE;AAChE,UAAI;AACF,uBAAgB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;cAC5E;AACN,uBAAgB,KAAK,aAAa;AAClC,cAAO;QACL,SAAS,CACP;SACE,MAAM;SACN,MAAM,kBAAkB,WAAW,gBAAgB,KAAM,cAAc;SACxE,CACF;QACD,SAAS;QACV;;MAGH,MAAM,cAAc,UAAU,MAAM,iBAAiB,CAAC;AACtD,aAAO,EACL,SAAS,CACP;OACE,MAAM;OACN,MAAM,GAAG,UAAU,MAAM,KAAK,UAAU,aAAa,MAAM,EAAE;OAC9D,CACF,EACF;;KAIH,MAAM,aAAa;MACjB,WAAW;MACX,WAAW;MACX,eAAe,cAAc,SAAS;MACtC,QAAQ,cAAc;MACtB,GAAI,cAAc,gBAAgB,EAAE,eAAe,cAAc,eAAe,GAAG,EAAE;MACtF;KACD,MAAM,aAAa,cAAc,gBAC7B,aAAa,cAAc,kBAC3B;KACJ,MAAM,eACJ;EAEG,cAAc,QAAQ,gBACV,cAAc,WAC7B,aACA;KACF,MAAM,KAAK,MAAM,SAAS,UAAU;KACpC,MAAM,WAAW,GAAG,gBAAgB,eAAe,OAAO,IAAI,KAAK,UAAU;MAAE;MAAW;MAAU;MAAY,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;MAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AAE5J,SAAI,CAAC,cACH,QAAO,EAAE,SAAS,CAAC;MAAE,MAAM;MAAiB,MAAM;MAAU,CAAC,EAAE;KAIjE,IAAI,kBAA4D,EAAE;AAClE,SAAI;AACF,wBAAkB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;aAC9E;AACN,wBAAkB,KAAK,aAAa;AACpC,aAAO;OACL,SAAS,CACP;QACE,MAAM;QACN,MAAM,kBAAkB,UAAU,gBAAgB,KAAM,gBAAgB;QACzE,CACF;OACD,SAAS;OACV;;KAGH,MAAM,gBAAgB,UAAU,MAAM,iBAAiB,CAAC;AACxD,YAAO,EACL,SAAS,CACP;MACE,MAAM;MACN,MAAM,GAAG,SAAS,MAAM,KAAK,UAAU,eAAe,MAAM,EAAE;MAC/D,CACF,EACF;;IAIH,MAAM,KAAK,MAAM,SAAS,UAAU;IACpC,MAAM,WAAW,GAAG,gBAAgB,OAAO,IAAI,KAAK,UAAU;KAAE;KAAW;KAAU,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;KAAG,EAAE,MAAM,EAAE,CAAC,MAAM;AAEjI,QAAI,CAAC,cACH,QAAO,EACL,SAAS,CAAC;KAAE,MAAM;KAAiB,MAAM;KAAU,CAAC,EACrD;IASH,IAAI,gBAA0D,EAAE;AAChE,QAAI;AACF,qBAAgB,MAAM,wBAAwB,MAAM,gBAAgB,cAAc;YAC5E;AACN,qBAAgB,KAAK,aAAa;AAClC,YAAO;MACL,SAAS,CACP;OACE,MAAM;OACN,MAAM,kBAAkB,UAAU,gBAAgB,KAAM,cAAc;OACvE,CACF;MACD,SAAS;MACV;;IAGH,MAAM,cAAc,UAAU,MAAM,iBAAiB,CAAC;AACtD,WAAO,EACL,SAAS,CACP;KACE,MAAM;KACN,MAAM,GAAG,SAAS,MAAM,KAAK,UAAU,aAAa,MAAM,EAAE;KAC7D,CACF,EACF;YACM,KAAK;AACZ,WAAO,YAAY,KAAK,KAAK;;;AAIjC,MAAI;AAGF,SAAM,KAAK,eAAe;WACnB,KAAK;AACZ,OAAI,SAAS,cAAc;AAIzB,QAAI;AACF,WAAM,KAAK,kBAAkB;YACvB;AAKR,WAAOA,iBAFW,UAAU,MAAM,iBAAiB,CAAC,EAEnB,MAAM,KADtB,KAAK,aAAa,CAAC,SAAS,EACQ;;AAGvD,UAAO,0BAA0B,KAAK,KAAK;;AAG7C,MAAI;AACF,WAAQ,MAAR;IACE,KAAK,wBACH,QAAOD,aAAW,oBAAoB,KAAK,CAAC;IAC9C,KAAK,mBAAmB;KACtB,MAAM,WAAW,QAAQ,OAAO,WAAW;AAE3C,YAAOA,aAAW,EAAE,YAAY,eAAe,MADjC,OAAO,aAAa,YAAY,WAAW,IAAI,WAAW,GACb,EAAE,CAAC;;IAEhE,KAAK,wBACH,QAAOA,aAAW,oBAAoB,KAAK,CAAC;IAC9C,KAAK;AAEH,SAAI;AACF,YAAM,KAAK,kBAAkB;aACvB;AAKR,YAAOC,iBAFe,UAAU,MAAM,iBAAiB,CAAC,EAEnB,MAAM,KADjB,KAAK,aAAa,CAAC,SAAS,EACY;IAEpE,KAAK,mBACH,QAAOD,aAAW,MAAM,eAAe,KAAK,CAAC;IAC/C,KAAK,gBACH,QAAOA,aAAW,MAAM,aAAa,KAAK,CAAC;IAC7C,KAAK,mBAAmB;KACtB,MAAM,OAAO,MAAM,eAAe,KAAK;AACvC,YAAO,EACL,SAAS,CAAC;MAAE,MAAM;MAAkB,MAAM,KAAK;MAAM,UAAU,KAAK;MAAU,CAAC,EAChF;;IAEH,KAAK,oBASH,QAAOC,iBAFc,MAAM,gBAAgB,MAAM,IAAI,EAEjB,MAAM,KADjB,KAAK,aAAa,CAAC,SAAS,EACW;IAElE,KAAK,YAAY;KACf,MAAM,aAAa,QAAQ,OAAO,WAAW;AAC7C,SAAI,OAAO,eAAe,YAAY,eAAe,GACnD,QAAO,SACL,+DACD;AAUH,SACE,KAAK,SAAS,WACd,eAAe,IACf,QAAQ,OAAO,WAAW,YAAY,KAEtC,QAAO,eAAe,WAAW;AAGnC,YAAOD,aAAW,MAAM,SAAS,MAAM,WAAW,CAAC;;IAErD,KAAK,YAAY;KACf,MAAM,UAAU,QAAQ,OAAO,WAAW;AAC1C,SAAI,OAAO,YAAY,YAAY,YAAY,GAC7C,QAAO,SACL,qDACD;KAEH,MAAM,UAAU,QAAQ,OAAO,WAAW;KAC1C,MAAM,UAAqB,MAAM,QAAQ,QAAQ,GAAG,UAAU,EAAE;AAIhE,SACE,KAAK,SAAS,WACd,eAAe,IACf,QAAQ,OAAO,WAAW,YAAY,KAEtC,QAAO,eAAe,WAAW;KAGnC,MAAM,YAAY,MAAM,QAAQ,MAAM,SAAS,QAAQ;AAEvD,SACE,CAAC,UAAU,MACX,OAAO,UAAU,UAAU,YAC3B,UAAU,MAAM,WAAW,cAAc,CAIzC,QAAO,eAAe,YAAY,KAAK,SAAS,QAAQ;AAG1D,YAAOC,iBAAe,WAAW,MAAM,KADf,KAAK,aAAa,CAAC,SAAS,EACQ;;IAE9D,QACE,QAAO,YAAY,KAAK;;WAErB,KAAK;AAGZ,UAAO,YAAY,KAAK,MAAM,KAAK,SAAS,QAAQ;;GAEtD;AAEF,QAAO;;;;;;;;;;AAWT,SAAgB,wBAAwB,KAAqC;AAC3E,KACE,QAAQ,mBACR,QAAQ,mBACR,QAAQ,mBACR,QAAQ,aAER,QAAO;AAET,QAAO;;;;;;;;;;;AAYT,SAAgB,2BAA2B,YAA6C;AACtF,QAAO;EACL,IAAI,SAAS;AACX,UAAO;;EAMT,mBAAmB,KAAA;EAMnB,WACE,MACA,SACA,cAC2B;AAI3B,OAAI,SAAS,gBACX,QAAO,QAAQ,uBACb,IAAI,MACF,qJAGD,CACF;AAIH,OAFkB,YAAY,KAAK,MACjB,WAAW,SAAS,SAEpC,QAAO,QAAQ,uBACb,IAAI,MACF,yBAAyB,WAAW,KAAK,gBACnC,KAAK,sEAEZ,CACF;AAGH,OAAI,SAAS,gBAAgB,CAAC,QAC5B,QAAO,QAAQ,uBACb,IAAI,MACF,wGAED,CACF;AAEH,iBAAc,SAAS,aAAa;GACpC,MAAM,cAAc,kBAAkB,WAAW,MAAM,eAAe,CAAC;AACvE,UAAO,QAAQ,QAAQ;IACrB;IACA;IACA,MAAM,WAAW;IACjB,iBAAiB,WAAW,SAAS,WAAW,eAAe;IAC/D,UACE,WAAW,SAAS,UAChB,wCACA;IACP,CAAC;;EAEL;;;;;;;;;AAUH,SAAS,iBAAiB,OAA+B;CACvD,MAAM,SAAS,QAAQ,IAAI;CAC3B,MAAM,OAAO,SAAS,aAAa,OAAO,GAAG,KAAA;AAC7C,QAAO,MAAM,SAAS,aAClB,uBAAuB,MAAM,eAAe,MAAM,QAAQ,MAAM,EAC9D,MAAM,MAAM,SACb,CAAC,GACF,uBAAuB,MAAM,WAAW,MAAM,QAAQ,KAAK;;AAGjE,SAASD,aAAW,OAAgB;AAClC,QAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,OAAO,MAAM,EAAE;EAAE,CAAC,EAAE;;;;;;;AAQvF,SAASC,iBAAe,OAAgB,MAAc,KAAqB,UAAmB;CAC5F,MAAM,UAAU,aAAa,OAAO;EAAE;EAAM;EAAK;EAAU,CAAC;AAC5D,QAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE;EAAE,CAAC,EAAE;;AAGzF,SAAS,YAAY,MAAc;AACjC,QAAO,SAAS,gBAAgB,OAAO;;;;;;;;;;AAWzC,SAAS,0BAA0B,KAAc,UAAkB;CACjE,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAGhE,KAAI,QAAQ,SAAS,4BAA4B,IAAI,QAAQ,SAAS,gBAAgB,CACpF,QAAO,iBAAiB,SAAS;AAInC,KACE,QAAQ,SAAS,yBAAyB,IAC1C,QAAQ,SAAS,gBAAgB,IACjC,QAAQ,SAAS,kBAAkB,IACnC,QAAQ,SAAS,qBAAqB,CAEtC,QAAO,eAAe,SAAS;AAIjC,KACE,QAAQ,SAAS,sBAAsB,IACvC,QAAQ,SAAS,kBAAkB,IACnC,QAAQ,SAAS,+BAA+B,CAEhD,QAAO,qBAAqB,SAAS;AAIvC,QAAO,kBAAkB,KAAK,SAAS;;;;;;AAOzC,SAAS,YAAY,KAAc,MAAc,UAAU,OAAO;AAChE,QAAO,kBAAkB,KAAK,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC9C,SAAgB,mBACd,YACA,QACA,aAAa,KACb,UACkB;;CAElB,SAAS,YAAoB;AAC3B,SAAO,WACJ,aAAa,CACb,KAAK,MAAM,EAAE,GAAG,CAChB,MAAM,CACN,KAAK,IAAI;;CAGd,IAAI,gBAAgB,WAAW;AAE/B,KAAI,kBAAkB,IAAI;AACnB,SAAO,qBAAqB;AACjC,cAAY;;CAGd,MAAM,SAAS,kBAAkB;EAC/B,MAAM,UAAU,WAAW;AAC3B,MAAI,YAAY,eAAe;AAC7B,mBAAgB;AAChB,OAAI,YAAY,IAAI;AAEb,WAAO,qBAAqB;AACjC,gBAAY;;;IAIf,WAAW;AAEd,QAAO,EACL,OAAO;AACL,gBAAc,OAAO;IAExB;;;;;;;;;;;;;AAuCH,SAAS,sBAAsB,cAAyC;AAMtE,QAAO,IAAI,kBAAkB;EAC3B;EACA,YAAY,QAAQ,IAAI;EACzB,CAAC;;;;;;;;;;;;AAaJ,IAAM,mBAAN,cAA+B,cAAc;CAC3C,YACE,WAGA;AACA,QAAM,EACJ,cAAc,QAAQ,WAAW,WAAW,CAAC,YAAY,QAAQ,OAAO,EACzE,CAAC;;;;;;;;;;;;AAkEN,eAAsB,kBAAyC;CAG7D,MAAM,WAAW,MAAM,eAAe;EAAE,MAFxB;EAEuC,QADxC,QAAQ,IAAI,oBAAoB;EACgB,CAAC;AAEhE,OAAM,IAAI,SAAe,MAAM,WAAW,GAAG,IAAI,CAAC;CAClD,MAAM,aAAa,IAAI,mBAAmB,EAAE,iBAAiB,SAAS,aAAa,CAAC;AACpF,QAAO;EACL;EACA,OAAO;AACL,cAAW,OAAO;AAClB,YAAS,MAAM;;EAElB;;;;;;;;;;;;;;;;;;;;;;AA0DH,eAAsB,gBAAgB,UAAkC,EAAE,EAAyB;AAMjG,4BAA2B;CAK3B,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,cAAc,QAAQ,eAAe,KAAA;CAE3C,MAAM,QAAQ,MAAM,eAAe;EACjC,MAAM;EACN,YAAY,QAAQ;EACpB,cAAc,QAAQ;EACvB,CAAC;AAEF,SAAQ,gBAAgB;EAAE,MAAM,MAAM;EAAM;EAAa,CAAC;CAE1D,IAAI,SAA6B;CACjC,IAAI,eAA6B,iBAAiB,OAAO,KAAK;CAC9D,IAAI,cAAuC;AAG5B,sBAAqB;AAShB,kBAAiB,MAAM,KAAK,CAAC,MAC9C,MAAM;AACL,WAAS;AACT,iBAAe,iBAAiB,MAAM,EAAE,OAAO;AAC/C,UAAQ,WAAW,EAAE,OAAO;AAI5B,MAAI,EAAE,aAAa,KAAA,EACjB,SAAQ,mBAAmB,EAAE,SAAS;AAGxC,UAAQ,aAAa,EAAE,aAAa,CAAC;AAIrC,gBAAc,uBAAuB,GAAG,MAAM,MAAM;GAClD,YAAY,cAAc;AACxB,aAAS;AACT,mBAAe,iBAAiB,MAAM,UAAU,QAAQ,MAAM,EAAE;AAChE,YAAQ,WAAW,UAAU,OAAO;AAIpC,QAAI,UAAU,aAAa,KAAA,EACzB,SAAQ,mBAAmB,UAAU,SAAS;AAG3C,sBAAkB;KAAE,QAAQ,UAAU;KAAQ;KAAa,CAAC,CAAC,WAAW;AAC3E,aAAQ,aAAa;MAAE;MAAa,UAAU;MAAM,CAAC;MACrD;;GAEJ,kBAAkB,cAAc;AAC9B,mBAAe,iBAAiB,OAAO,MAAM,WAAW,EAAE;AAC1D,aAAS,eAAe,EACtB,KAAK,+BAA+B,UAAU,gDAC/C,CAAC;;GAEL,CAAC;AAEF,SAAO,kBAAkB;GAAE,QAAQ,EAAE;GAAQ;GAAa,CAAC;KAE5D,QAAQ;AAEP,WAAS,eAAe,EACtB,KAAK,4CAFS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAEL,gGAC1D,CAAC;GAEL;CAKD,MAAM,aAAa,sBAAsB,MAAM,QAAQ;AAEvD,QAAO;EACL;EAEA,aAAa;EAGb,cAAc,MAAM;EACpB,uBAAuB;EACvB,OAAO;AACL,gBAAa,MAAM;AAEnB,WAAQ,MAAM;AACd,cAAW,OAAO;AAEb,SAAM,OAAO;;EAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCH,eAAsB,kBACpB,MAAyB,QAAQ,KACjC,aAC6B;CAC7B,MAAM,YAAY,IAAI,uBAAuB,IAAI,MAAM;AACvD,KAAI,aAAa,GAAI,QAAO;AAE5B,KAAI,gBAAgB,KAAA,EAClB,KAAI;EACF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,SAAS,MAAM,cAAc,EAAE,aAAa,CAAC;AACnD,MAAI,QAAQ,cAAe,QAAO,OAAO;SACnC;;AAOZ,eAAsB,wBACpB,cACA,eACuB;AAOvB,4BAA2B;CAE3B,MAAM,aAAa,sBAAsB,aAAa;CAItD,MAAM,eAAe,iBAAiB,MADlB,aAAa,QAAQ,SAAS,KAAK,CACC;AACxD,QAAO;EACL;EAEA,aAAa;EAIb,cAAc;EAId,mBAAmB;EACnB,uBAAuB;EACvB,OAAO;AAEL,cAAW,OAAO;;EAErB;;;;;;;AAwBH,SAAgB,iBAAiB,MAAiC;AAChE,SAAQ,MAAR;EACE,KAAK,gBACH,QAAO;EACT,KAAK,gBACH,QAAO;EACT,KAAK;EACL,KAAK,aACH,QAAO;;;;AAKb,MAAa,wCACX;;;;;;;;;;;;;;;AAmBF,eAAsB,uBACpB,MAAyB,QAAQ,KACjC,aACiB;CAEjB,MAAM,MAAM,IAAI;CAChB,MAAM,WAAW,OAAO,QAAQ,WAAW,IAAI,MAAM,GAAG;AACxD,KAAI,aAAa,GACf,QAAO;AAIT,KAAI,gBAAgB,KAAA,GAAW;EAC7B,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,SAAS,MAAM,cAAc,EAAE,aAAa,CAAC;AACnD,MAAI,QAAQ,iBAAiB,KAAA,EAC3B,QAAO,OAAO;;AAKlB,OAAM,IAAI,MAAM,sCAAsC;;;;;;;;;;AA6ExD,MAAM,sBAAqC;CACzC,MAAM;CACN,qBAAqB,QAAQ,SAAS;CACtC,mBAAmB,EAAE;CACrB,yBAAyB,EAAE;CAC3B,gBAAgB;CAChB,YAAY,QAAQ,uBAAO,IAAI,MAAM,gDAAgD,CAAC;CACtF,aAAa;CACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CD,IAAa,uBAAb,MAA8D;CAC5D;;CAEA,+BAAgC,IAAI,KAA8B;;CAElE,eAA4C;CAC5C,SAAgC;CAChC,gBAAiD;CACjD,eAAuB;CAEvB,YAAY,MAAsB;AAChC,OAAK,OAAO;;CAGd,IAAI,SAAwB;AAC1B,SAAO,KAAK,eAAe,KAAK,aAAa,aAAa;;;CAI5D,IAAI,oBAA6C;AAC/C,SAAO,KAAK,cAAc;;;;;;;;;;;;CAa5B,IAAI,qBAAyC;AAC3C,MAAI,CAAC,KAAK,aAAc,QAAO,KAAA;AAC/B,SAAO,KAAK,aAAa,qBAAqB,KAAK,aAAa;;;CAIlE,iBAAiC;AAC/B,SAAO,CAAC,GAAG,KAAK,aAAa,QAAQ,CAAC;;;;;;;;;;CAWxC,oBAAkC;AAChC,MAAI,KAAK,cAAc,gBAAiB,QAAO,KAAK,aAAa,iBAAiB;AAClF,OAAK,MAAM,UAAU,KAAK,gBAAgB,CACxC,KAAI,OAAO,gBAAiB,QAAO,OAAO,iBAAiB;AAE7D,SAAO;GAAE,IAAI;GAAO,QAAQ;GAAM;;;;;;;CAQpC,MAAM,QAAsB;AAC1B,OAAK,SAAS;AACd,OAAK,YAAY;;;CAInB,cAAoB;AAClB,OAAK,eAAe,MAAM;AAC1B,OAAK,gBAAgB;;;CAIvB,aAA2B;EACzB,MAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OAAQ;EAGb,MAAM,eAAe,KAAK;AAC1B,MAAI,CAAC,aAAc;AACnB,OAAK,gBAAgB,mBACnB,aAAa,YACb,QACA,KAAK,KAAK,2BAA2B,WAC/B;AACJ,QAAK,KAAK,qBAAqB,cAAc;AAE7C,QAAK,KAAK,gBAAgB;AAM1B,OAAI,aAAa,WAAW,SAAS,SAAS;IAG5C,MAAM,cAAc,aAAa,WAAW,aAAa,CAAC;IAC1D,MAAM,MAAM,kBACV,aAAa,WAAW,MACxB,eAAe,EACf,aAAa,YACd;IAID,MAAM,qBAAqB,KAAK,KAAK,yBAAyB,IAAI;AAClE,SAAK,KAAK,eAAe,KAAK;KAC5B;KACA,kBAAkB,aAAa;KAC/B,UAAU,aAAa;KAKvB,UAAU,QAAQ,IAAI,8BACZ,aAAa,QAAQ,IAAI,sBAAgC,GAC/D,KAAA;KACJ;KACD,CAAC;;IAGP;;;;;;;;;;;;;;;;;;;;;;;;CAyBH,MAAc,UAAU,KAAgB,aAA6C;EACnF,MAAM,OAAO,KAAK,aAAa,IAAI,IAAI;AACvC,MAAI,MAAM;AAKR,OAAI,QAAQ,mBAAmB,KAAK,KAAK,wBAAwB,KAAA,GAAW;IAC1E,IAAI,WAA0B;AAC9B,QAAI;AACF,gBAAW,MAAM,KAAK,KAAK,oBAAoB,YAAY;YACrD;AAGN,gBAAW;;AAIb,QADgB,aAAa,QAAQ,aAAa,KAAK,cAC1C;AAGX,UAAK,MAAM;AACX,UAAK,aAAa,OAAO,IAAI;KAC7B,MAAM,SAAS,MAAM,KAAK,KAAK,YAAY,KAAK,YAAY;AAC5D,UAAK,aAAa,IAAI,KAAK,OAAO;AAClC,YAAO;;;AAGX,UAAO;;EAET,MAAM,SAAS,MAAM,KAAK,KAAK,YAAY,KAAK,YAAY;AAC5D,OAAK,aAAa,IAAI,KAAK,OAAO;AAClC,SAAO;;CAGT,MAAM,WACJ,MACA,SACA,aAC2B;AAC3B,MAAI,KAAK,aACP,OAAM,IAAI,MAAM,kDAAkD;AAEpE,MAAI,SAAS,gBAAgB,CAAC,QAC5B,OAAM,IAAI,MACR,wGAED;AAGH,OAAK,eAAe;AACpB,MAAI;AASF,OAAI,YAAY,KAAK,CACnB,OAAM,wBAAwB,EAAE,aAAa,CAAC;GAQhD,MAAM,SAAS,MAAM,KAAK,UAAU,iBAAiB,KAAK,EAAE,YAAY;AAIxE,QAAK,eAAe;AAKpB,iBAAc,SAAS,aAAa;AAGpC,QAAK,aAAa;AAClB,QAAK,YAAY;AAGZ,QAAK,QAAQ,qBAAqB;GAEvC,MAAM,YAAY,YAAY,KAAK;AAMnC,UAAO;IACL;IACA,aAPkB,kBAClB,OAAO,WAAW,MAClB,eAAe,EACf,OAAO,YACR;IAIC,MAAM,OAAO,WAAW;IACxB,iBAAiB,OAAO,WAAW,SAAS,WAAW,eAAe;IACtE,UAAU,YACN,mDACA;IACL;YACO;AACR,QAAK,eAAe;;;;;;;;;;;;AAa1B,eAAsB,eAAe,UAAiC,EAAE,EAAiB;CAKvF,MAAM,aAAa,YAAY,EAAE,OAAO,QAAQ,SAAS,OAAO,CAAC;CAOjE,MAAM,iBAAiB,IAAI,oBAAoB;CAG/C,MAAM,uBAAuB,IAAI,8BAA8B;CAK/D,IAAI,uBAAsC;CAE1C,MAAM,SAAS,IAAI,qBAAqB;EAOtC,aAAa,OAAO,KAAK,gBACvB,QAAQ,kBACJ,wBACE,MAAM,uBAAuB,QAAQ,KAAK,YAAY,EACtD,MAAM,kBAAkB,QAAQ,KAAK,YAAY,CAClD,GACD,QAAQ,kBACN,iBAAiB,GACjB,gBAAgB;GACd,WAAW,QAAQ;GACnB,YAAY,sBAAsB;GAIlC,WAAW,WAAW;AACpB,eAAW,aAAa,OAAO;AAC/B,cAAU,mBAAmB;;GAK/B,mBAAmB,QAAQ;AACzB,2BAAuB;AACvB,eAAW,qBAAqB,IAAI;;GAKtC,oBAAoB,qBAAqB,kBAAkB;GAC5D,CAAC;EACV;EACA;EACA,oBAAoB,UAAU,mBAAmB;EAIjD,6BAA6B,UAAU,sBAAsB;EAK7D,sBAAsB,OAAO,uBAAuB,QAAQ,KAAK,GAAG,CAAC,YAAY,KAAK;EACvF,CAAC;CAIF,MAAM,YAAY,IAAI,uBAAuB;AAI3C,SAHe,OAAO;GAItB;CAKF,IAAI,kBAAyC;CAK7C,MAAM,0BAA0C;EAC9C,MAAM,UAAU,OAAO,OAAO,aAAa;EAK3C,MAAM,eAAe,UAAU,sBAAsB;AACrD,SAAO;GACL,QAAQ;IAAE,IAAI,OAAO,mBAAmB,CAAC;IAAI,QAAQ,OAAO,mBAAmB,CAAC;IAAQ;GACxF,OAAO,QAAQ,KAAK,OAAO;IAAE,IAAI,EAAE;IAAI,KAAK,EAAE;IAAK,EAAE;GACrD,WAAW,kBAAkB,iBAAiB,gBAAgB,GAAG;GACjE;GAGA,MAAM,kBAAkB,OAAO,OAAO,MAAM,eAAe,EAAE,OAAO,kBAAkB;GACvF;;CAOH,MAAM,8BAED;EACH,MAAM,eAAe,OAAO;AAC5B,MAAI,CAAC,aACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAa;EAE3C,MAAM,UAAU,OAAO,OAAO,aAAa;AAC3C,MAAI,QAAQ,WAAW,EACrB,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAY;EAE1C,MAAM,aAAa,QAAQ,IAAI;AAC/B,MAAI,CAAC,WACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;EAEjD,MAAM,MAAM,sBAAsB,cAAc,QAAQ,GAAG,UACzD,aAAa,YAAY,KAAK,KAAK,CAAC,CACrC;AACD,MAAI,QAAQ,KACV,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;AAEjD,SAAO;GAAE,IAAI;GAAM;GAAK;;CAM1B,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,kBAAkB,mBAAmB,EAAE,uBAAuB,CAAC;UACzE,KAAc;AAErB,UAAQ,gBAAgB,EAAE,KAAK,2CADf,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,IACqB,CAAC;;CAMxF,MAAM,2BAA2B;CACjC,IAAI,oBAA2D;AAC/D,qBAAoB,kBAAkB;AACpC,MAAI,oBAAoB,KACtB,WAAU,mBAAmB;IAE9B,yBAAyB;AAC5B,mBAAkB,OAAO;CAEzB,MAAM,SAAS,kBAAkB;EAG/B,YAAY,OAAO;EACnB;EACA;EAEA,uBAAuB,OAAO,mBAAmB;EAGjD,yBAAyB;EACzB,IAAI,eAAe;AACjB,UAAO;;EAET;EAKA,qBAAqB,QAAQ,IAAI;EAGjC,mBAAmB,UAAU;AAC3B,qBAAkB;AAClB,aAAU,mBAAmB;;EAEhC,CAAC;CAEF,MAAM,YAAY,IAAI,sBAAsB;CAgB5C,IAAI,SAAS;CACb,IAAI,gBAAyC;CAC7C,IAAI,iBAA0C;CAE9C,MAAM,iBAAiB;AAErB,MAAI,OAAQ;AACZ,WAAS;AAET,iBAAe,MAAM;AACrB,kBAAgB,MAAM;AACtB,MAAI,kBAAmB,eAAc,kBAAkB;AACvD,SAAO,aAAa;AAIpB,OAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AAEtD,SAAO,OAAO;AACd,YAAU,OAAO;AAEtB,aAAW,SAAS;;AAItB,SAAQ,KAAK,UAAU,SAAS;AAChC,SAAQ,KAAK,WAAW,SAAS;AAEjC,SAAQ,KAAK,UAAU,SAAS;AAIhC,SAAQ,GAAG,cAAc;AACvB,MAAI,CAAC,QAAQ;AACX,YAAS;AACT,kBAAe,MAAM;AACrB,mBAAgB,MAAM;AACtB,OAAI,kBAAmB,eAAc,kBAAkB;AACvD,UAAO,aAAa;AACpB,QAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AAE3D,cAAW,SAAS;;GAEtB;AAKF,SAAQ,GAAG,sBAAsB,QAAQ;AACvC,WAAS,cAAc;GAAE,KAAK,sBAAsB,OAAO,IAAI;GAAI,WAAW;GAAY,CAAC;AAC3F,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,SAAQ,GAAG,uBAAuB,WAAW;AAC3C,WAAS,cAAc;GACrB,KAAK,uBAAuB,OAAO,OAAO;GAC1C,WAAW;GACZ,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,OAAM,OAAO,QAAQ,UAAU;AAK/B,QAAO,MAAM,OAAO;AAUpB,KAAI,QAAQ,IAAI,8BAA8B,KAAK;AACjD,kBAAgB,yBACR;AACJ,aAAU;AACV,WAAQ,KAAK,EAAE;KAEjB,EAAE,YAAY,KAAO,CACtB;AAED,UAAQ,MAAM,KAAK,aAAa;AAC9B,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;AACF,UAAQ,MAAM,KAAK,eAAe;AAChC,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;;AAUJ,KAAI,QAAQ,IAAI,yBAAyB,IAIvC,kBAAiB,0BACT;AACJ,UAAQ,OAAO,MACb,8FACD;AACD,YAAU;AACV,UAAQ,KAAK,EAAE;IAEjB,EAAE,UAXa,QAAQ,IAAI,uBACzB,OAAO,SAAS,QAAQ,IAAI,sBAAsB,GAAG,IAAI,KAAA,IACzD,KAAA,GASU,CACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDL,eAAsB,oBAAoB,UAAsC,EAAE,EAAiB;CAGjG,MAAM,aAAa,YAAY,EAAE,OAAO,QAAQ,SAAS,OAAO,CAAC;CAEjE,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI,oBAAoB;CAOjE,MAAM,0BAA0B,YAAmC;EACjE,MAAM,WAAW,MAAM,eAAe;GAAE,MAAM;GAAS;GAAQ,CAAC;AAGhE,QAAM,IAAI,SAAe,MAAM,WAAW,GAAG,IAAI,CAAC;EAClD,MAAM,kBAAkB,IAAI,mBAAmB,EAAE,iBAAiB,SAAS,aAAa,CAAC;AACzF,SAAO;GACL,YAAY;GACZ,OAAO;AACL,oBAAgB,OAAO;AACvB,aAAS,MAAM;;GAElB;;CAMH,MAAM,iBAAiB,IAAI,oBAAoB;CAC/C,MAAM,uBAAuB,IAAI,8BAA8B;CAK/D,IAAI,uBAAsC;CAE1C,MAAM,SAAS,IAAI,qBAAqB;EAOtC,aAAa,OAAO,KAAK,gBACvB,QAAQ,kBACJ,wBACE,MAAM,uBAAuB,QAAQ,KAAK,YAAY,EACtD,MAAM,kBAAkB,QAAQ,KAAK,YAAY,CAClD,GACD,QAAQ,kBACN,yBAAyB,GACzB,gBAAgB;GACd,YAAY,sBAAsB;GAClC,WAAW,WAAW;AACpB,eAAW,aAAa,OAAO;AAC/B,cAAU,mBAAmB;;GAI/B,mBAAmB,QAAQ;AACzB,2BAAuB;AACvB,eAAW,qBAAqB,IAAI;;GAGtC,oBAAoB,qBAAqB,kBAAkB;GAC5D,CAAC;EACV;EACA;EACA,oBAAoB,UAAU,mBAAmB;EAEjD,6BAA6B,UAAU,sBAAsB;EAK7D,sBAAsB,OAAO,uBAAuB,QAAQ,KAAK,GAAG,CAAC,YAAY,KAAK;EACvF,CAAC;CAIF,MAAM,YAAY,IAAI,uBAAuB;AAI3C,SAHe,OAAO;GAItB;CAKF,IAAI,kBAAyC;CAE7C,MAAM,0BAA0C;EAC9C,MAAM,UAAU,OAAO,OAAO,aAAa;EAG3C,MAAM,eAAe,UAAU,sBAAsB;AACrD,SAAO;GACL,QAAQ;IAAE,IAAI,OAAO,mBAAmB,CAAC;IAAI,QAAQ,OAAO,mBAAmB,CAAC;IAAQ;GACxF,OAAO,QAAQ,KAAK,OAAO;IAAE,IAAI,EAAE;IAAI,KAAK,EAAE;IAAK,EAAE;GACrD,WAAW,kBAAkB,iBAAiB,gBAAgB,GAAG;GACjE;GACD;;CAOH,MAAM,8BAED;EACH,MAAM,eAAe,OAAO;AAC5B,MAAI,CAAC,aACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAa;EAE3C,MAAM,UAAU,OAAO,OAAO,aAAa;AAC3C,MAAI,QAAQ,WAAW,EACrB,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAY;EAE1C,MAAM,aAAa,QAAQ,IAAI;AAC/B,MAAI,CAAC,WACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;EAEjD,MAAM,MAAM,sBAAsB,cAAc,QAAQ,GAAG,UACzD,aAAa,YAAY,KAAK,KAAK,CAAC,CACrC;AACD,MAAI,QAAQ,KACV,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;AAEjD,SAAO;GAAE,IAAI;GAAM;GAAK;;CAK1B,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,kBAAkB,mBAAmB,EAAE,uBAAuB,CAAC;UACzE,KAAc;AAErB,UAAQ,gBAAgB,EAAE,KAAK,2CADf,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,IACqB,CAAC;;CAQxF,MAAM,2BAA2B;CACjC,IAAI,oBAA2D;AAC/D,qBAAoB,kBAAkB;AACpC,MAAI,oBAAoB,KACtB,WAAU,mBAAmB;IAE9B,yBAAyB;AAC5B,mBAAkB,OAAO;CAEzB,MAAM,SAAS,kBAAkB;EAC/B,YAAY,OAAO;EACnB;EACA;EAIA,uBAAuB,OAAO,mBAAmB;EAGjD,yBAAyB;EACzB,IAAI,eAAe;AACjB,UAAO;;EAET;EAKA,qBAAqB,QAAQ,IAAI;EAEjC,mBAAmB,UAAU;AAC3B,qBAAkB;AAClB,aAAU,mBAAmB;;EAEhC,CAAC;CAEF,MAAM,YAAY,IAAI,sBAAsB;CAU5C,IAAI,SAAS;CACb,IAAI,gBAAyC;CAC7C,IAAI,iBAA0C;CAE9C,MAAM,iBAAiB;AACrB,MAAI,OAAQ;AACZ,WAAS;AACT,iBAAe,MAAM;AACrB,kBAAgB,MAAM;AACtB,MAAI,kBAAmB,eAAc,kBAAkB;AACvD,SAAO,aAAa;AAEpB,OAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AACtD,SAAO,OAAO;AACd,YAAU,OAAO;AAEtB,aAAW,SAAS;;AAGtB,SAAQ,KAAK,UAAU,SAAS;AAChC,SAAQ,KAAK,WAAW,SAAS;AACjC,SAAQ,KAAK,UAAU,SAAS;AAEhC,SAAQ,GAAG,cAAc;AACvB,MAAI,CAAC,QAAQ;AACX,YAAS;AACT,kBAAe,MAAM;AACrB,mBAAgB,MAAM;AACtB,OAAI,kBAAmB,eAAc,kBAAkB;AACvD,UAAO,aAAa;AACpB,QAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AAC3D,cAAW,SAAS;;GAEtB;AAEF,SAAQ,GAAG,sBAAsB,QAAQ;AACvC,WAAS,cAAc;GACrB,KAAK,sBAAsB,OAAO,IAAI;GACtC,WAAW;GACX,MAAM;GACP,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,SAAQ,GAAG,uBAAuB,WAAW;AAC3C,WAAS,cAAc;GACrB,KAAK,uBAAuB,OAAO,OAAO;GAC1C,WAAW;GACX,MAAM;GACP,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,OAAM,OAAO,QAAQ,UAAU;AAK/B,QAAO,MAAM,OAAO;AAGpB,KAAI,QAAQ,IAAI,8BAA8B,KAAK;AACjD,kBAAgB,yBACR;AACJ,aAAU;AACV,WAAQ,KAAK,EAAE;KAEjB,EAAE,YAAY,KAAO,CACtB;AACD,UAAQ,MAAM,KAAK,aAAa;AAC9B,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;AACF,UAAQ,MAAM,KAAK,eAAe;AAChC,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;;AAIJ,KAAI,QAAQ,IAAI,yBAAyB,IAIvC,kBAAiB,0BACT;AACJ,UAAQ,OAAO,MACb,8FACD;AACD,YAAU;AACV,UAAQ,KAAK,EAAE;IAEjB,EAAE,UAXa,QAAQ,IAAI,uBACzB,OAAO,SAAS,QAAQ,IAAI,sBAAsB,GAAG,IAAI,KAAA,IACzD,KAAA,GASU,CACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CL,eAAsB,qBACpB,UAAuC,EAAE,EAC1B;CAOf,MAAM,eAAe,MAAM,uBACzB,QAAQ,KACR,QAAQ,eAAe,QAAQ,KAAK,CACrC;CAID,MAAM,aAAa,YAAY,EAAE,OAAO,QAAQ,SAAS,OAAO,CAAC;CAMjE,MAAM,iBAAiB,IAAI,oBAAoB;CAC/C,MAAM,uBAAuB,IAAI,8BAA8B;CAK/D,IAAI,uBAAsC;CAE1C,MAAM,SAAS,IAAI,qBAAqB;EAOtC,aAAa,OAAO,QAClB,QAAQ,kBACJ,wBACE,cACA,MAAM,kBAAkB,QAAQ,KAAK,QAAQ,eAAe,QAAQ,KAAK,CAAC,CAC3E,GACD,QAAQ,kBACN,iBAAiB,GACjB,gBAAgB;GACd,YAAY,sBAAsB;GAClC,WAAW,WAAW;AACpB,eAAW,aAAa,OAAO;AAC/B,cAAU,mBAAmB;;GAI/B,mBAAmB,QAAQ;AACzB,2BAAuB;AACvB,eAAW,qBAAqB,IAAI;;GAGtC,oBAAoB,qBAAqB,kBAAkB;GAC5D,CAAC;EACV;EACA;EACA,oBAAoB,UAAU,mBAAmB;EAEjD,6BAA6B,UAAU,sBAAsB;EAK7D,sBAAsB,OACpB,uBAAuB,QAAQ,KAAK,MAAM,QAAQ,eAAe,QAAQ,KAAK,CAAC,CAAC,YACxE,KACP;EACJ,CAAC;CAKF,MAAM,YAAY,IAAI,uBAAuB;AAI3C,SAHe,OAAO;GAItB;CAKF,IAAI,kBAAyC;CAE7C,MAAM,0BAA0C;EAC9C,MAAM,UAAU,OAAO,OAAO,aAAa;EAG3C,MAAM,eAAe,UAAU,sBAAsB;AACrD,SAAO;GACL,QAAQ;IAAE,IAAI,OAAO,mBAAmB,CAAC;IAAI,QAAQ,OAAO,mBAAmB,CAAC;IAAQ;GACxF,OAAO,QAAQ,KAAK,OAAO;IAAE,IAAI,EAAE;IAAI,KAAK,EAAE;IAAK,EAAE;GACrD,WAAW,kBAAkB,iBAAiB,gBAAgB,GAAG;GACjE;GACD;;CAOH,MAAM,8BAED;EACH,MAAM,eAAe,OAAO;AAC5B,MAAI,CAAC,aACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAa;EAE3C,MAAM,UAAU,OAAO,OAAO,aAAa;AAC3C,MAAI,QAAQ,WAAW,EACrB,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAY;EAE1C,MAAM,aAAa,QAAQ,IAAI;AAC/B,MAAI,CAAC,WACH,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;EAEjD,MAAM,MAAM,sBAAsB,cAAc,QAAQ,GAAG,UACzD,aAAa,YAAY,KAAK,KAAK,CAAC,CACrC;AACD,MAAI,QAAQ,KACV,QAAO;GAAE,IAAI;GAAO,QAAQ;GAAmB;AAEjD,SAAO;GAAE,IAAI;GAAM;GAAK;;CAK1B,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,kBAAkB,mBAAmB,EAAE,uBAAuB,CAAC;UACzE,KAAc;AAErB,UAAQ,gBAAgB,EAAE,KAAK,2CADf,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,IACqB,CAAC;;CAMxF,MAAM,2BAA2B;CACjC,IAAI,oBAA2D;AAC/D,qBAAoB,kBAAkB;AACpC,MAAI,oBAAoB,KACtB,WAAU,mBAAmB;IAE9B,yBAAyB;AAC5B,mBAAkB,OAAO;CAEzB,MAAM,SAAS,kBAAkB;EAC/B,YAAY,OAAO;EACnB;EACA;EAIA,uBAAuB,OAAO,mBAAmB;EAGjD,yBAAyB;EACzB,IAAI,eAAe;AACjB,UAAO;;EAET;EAKA,qBAAqB,QAAQ,IAAI;EAEjC,mBAAmB,UAAU;AAC3B,qBAAkB;AAClB,aAAU,mBAAmB;;EAEhC,CAAC;CAEF,MAAM,YAAY,IAAI,sBAAsB;CAW5C,IAAI,SAAS;CACb,IAAI,gBAAyC;CAC7C,IAAI,iBAA0C;CAE9C,MAAM,iBAAiB;AACrB,MAAI,OAAQ;AACZ,WAAS;AACT,iBAAe,MAAM;AACrB,kBAAgB,MAAM;AACtB,MAAI,kBAAmB,eAAc,kBAAkB;AACvD,SAAO,aAAa;AACpB,OAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AACtD,SAAO,OAAO;AACd,YAAU,OAAO;AACtB,aAAW,SAAS;;AAGtB,SAAQ,KAAK,UAAU,SAAS;AAChC,SAAQ,KAAK,WAAW,SAAS;AACjC,SAAQ,KAAK,UAAU,SAAS;AAEhC,SAAQ,GAAG,cAAc;AACvB,MAAI,CAAC,QAAQ;AACX,YAAS;AACT,kBAAe,MAAM;AACrB,mBAAgB,MAAM;AACtB,OAAI,kBAAmB,eAAc,kBAAkB;AACvD,UAAO,aAAa;AACpB,QAAK,MAAM,UAAU,OAAO,gBAAgB,CAAE,QAAO,MAAM;AAC3D,cAAW,SAAS;;GAEtB;AAEF,SAAQ,GAAG,sBAAsB,QAAQ;AACvC,WAAS,cAAc;GACrB,KAAK,sBAAsB,OAAO,IAAI;GACtC,WAAW;GACX,MAAM;GACP,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,SAAQ,GAAG,uBAAuB,WAAW;AAC3C,WAAS,cAAc;GACrB,KAAK,uBAAuB,OAAO,OAAO;GAC1C,WAAW;GACX,MAAM;GACP,CAAC;AACF,YAAU;AACV,UAAQ,KAAK,EAAE;GACf;AAEF,OAAM,OAAO,QAAQ,UAAU;AAK/B,QAAO,MAAM,OAAO;AAGpB,KAAI,QAAQ,IAAI,8BAA8B,KAAK;AACjD,kBAAgB,yBACR;AACJ,aAAU;AACV,WAAQ,KAAK,EAAE;KAEjB,EAAE,YAAY,KAAO,CACtB;AACD,UAAQ,MAAM,KAAK,aAAa;AAC9B,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;AACF,UAAQ,MAAM,KAAK,eAAe;AAChC,aAAU;AACV,WAAQ,KAAK,EAAE;IACf;;AAIJ,KAAI,QAAQ,IAAI,yBAAyB,IAIvC,kBAAiB,0BACT;AACJ,UAAQ,OAAO,MACb,8FACD;AACD,YAAU;AACV,UAAQ,KAAK,EAAE;IAEjB,EAAE,UAXa,QAAQ,IAAI,uBACzB,OAAO,SAAS,QAAQ,IAAI,sBAAsB,GAAG,IAAI,KAAA,IACzD,KAAA,GASU,CACb;;;;AC7/GL,SAAS,SAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU;;AAGhD,IAAa,gBAAb,MAAgD;CAC9C;CACA;CAEA,YAAY,SAA+B;AACzC,OAAK,gBAAgB,QAAQ;AAC7B,OAAK,YAAY,QAAQ,eAAe,QAAQ,MAAM,IAAI;;CAG5D,MAAc,aAAoC;EAChD,MAAM,MAAM,MAAM,KAAK,UAAU,KAAK,cAAc;AACpD,MAAI,CAAC,IAAI,GACP,OAAM,IAAI,MACR,mCAAmC,KAAK,cAAc,SAAS,IAAI,OAAO,GAAG,IAAI,WAAW,kGAE7F;EAEH,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,SAAO,SAAS,KAAK,GAAG,OAAO,EAAE;;CAGnC,MAAM,IAA6B,QAAqC;AACtE,UAAQ,QAAR;GACE,KAAK,mBAEH,QADc,MAAM,KAAK,YAAY;GAGvC,KAAK,iCAAiC;IACpC,MAAM,QAAQ,MAAM,KAAK,YAAY;AAIrC,WAD0C;KAAE,aAFxB,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc;KAEvB,YADtC,OAAO,MAAM,eAAe,WAAW,MAAM,aAAa;KACR;;GAGvE,KAAK,yBAAyB;IAI5B,MAAM,OADQ,MAAM,KAAK,YAAY,EACnB;AAGlB,WADkC,EAAE,OADtB,MAAM,QAAQ,IAAI,GAAI,MAAqC,EAAE,EAChC;;GAG7C,QACE,OAAM,IAAI,MAAM,uBAAuB,OAAO,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjBhE,MAAM,8BACJ;;;;;;;;;;;;AAeF,MAAM,uBAAuB;CAI3B;EACE,MAAM;EACN,aACE;EAIF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAEF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAEF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAEF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CAID;EACE,MAAM;EACN,aACE;EAIF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAIF,aAAa;GACX,MAAM;GACN,YAAY,EACV,qBAAqB;IACnB,MAAM;IACN,aAAa;IACd,EACF;GACD,UAAU,EAAE;GACb;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAIF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAIF,aAAa;GACX,MAAM;GACN,YAAY;IACV,MAAM;KACJ,MAAM;KACN,aAAa;KACd;IACD,MAAM;KACJ,MAAM;KACN,aAAa;KACb,OAAO,EAAE;KACV;IACF;GACD,UAAU,CAAC,OAAO;GACnB;EACD,aAAa;EACd;CAQD;EACE,MAAM;EACN,aACE;EAKF,aAAa;GACX,MAAM;GACN,YAAY;IACV,YAAY;KACV,MAAM;KACN,aAAa;KACd;IACD,iBAAiB;KACf,MAAM;KACN,aAAa;KACd;IACD,sBAAsB;KACpB,MAAM;KACN,aACE;KAEH;IACF;GACD,UAAU,CAAC,aAAa;GACzB;EACD,aAAa;EACd;CAKD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GACX,MAAM;GACN,YAAY,EACV,YAAY;IAAE,MAAM;IAAU,aAAa;IAAsC,EAClF;GACD,UAAU,CAAC,aAAa;GACzB;EACD,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GAAE,MAAM;GAAU,YAAY,EAAE;GAAE,UAAU,EAAE;GAAE;EAC7D,aAAa;EACd;CACD;EACE,MAAM;EACN,aACE;EAGF,aAAa;GACX,MAAM;GACN,YAAY,EACV,OAAO;IAAE,MAAM;IAAU,aAAa;IAAiC,EACxE;GACD,UAAU,EAAE;GACb;EACD,aAAa;EACd;CACF;;AAGD,MAAM,iBAAiB,IAAI,IAAY,qBAAqB,KAAK,MAAM,EAAE,KAAK,CAAC;;AAG/E,MAAM,sBAAsB,IAAI,IAAY;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;AAOF,MAAM,oBAAoB,IAAI,IAAY,CAAC,mBAAmB,CAAC;;;;;AAW/D,SAAS,wBAAwB,aAAqB;AACpD,QAAO;EACL,OAAO,CACL;GACE,KAAK;GACL,OAAO;GACP,UAAU;GACX,CACF;EACD,QAAQ,EAAE,IAAI,OAAO;EACrB,SAAS;EACT,mBAAmB;EACpB;;;;;;AAOH,eAAe,oBACb,aACA,eACA,WACkC;CAClC,IAAI,YAAY;CAChB,IAAI,iBAAgC;CACpC,IAAI,cAA6B;AAEjC,KAAI;EACF,MAAM,MAAM,MAAM,UAAU,cAAc;AAC1C,cAAY,IAAI;AAChB,iCAAc,IAAI,MAAM,EAAC,aAAa;AACtC,MAAI,CAAC,IAAI,GACP,kBAAiB,QAAQ,IAAI,OAAO,GAAG,IAAI;UAEtC,KAAK;AACZ,mBAAiB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AACjE,iCAAc,IAAI,MAAM,EAAC,aAAa;;AAGxC,QAAO;EACL,MAAM;EACN;EACA,kBAAkB;EAClB,4BAA4B;EAC5B;EACA;EACA,aAAa;GACX,MAAM;GACN,QAAQ;GACT;EACD,uBAAuB,YACnB,OACA;EACL;;;;;;;AAQH,eAAe,wBAAwB,WAAwD;CAK7F,MAAM,aAJQ,MAAM,UAAU,IAAI,mBAAmB,EAI/B;CACtB,IAAI,YAAiF;AACrF,KAAI,cAAc,QAAQ,OAAO,cAAc,YAAY,CAAC,MAAM,QAAQ,UAAU,EAAE;EACpF,MAAM,IAAI;AACV,cAAY;GACV,KAAK,OAAO,EAAE,QAAQ,WAAW,EAAE,MAAM;GACzC,OAAO,OAAO,EAAE,UAAU,WAAW,EAAE,QAAQ;GAC/C,QAAQ,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;GAClD,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;GAC7C;;AAGH,QAAO;EACL,QAAQ;EAER,QAAQ;GAAE,KAAK;GAAG,OAAO;GAAG,QAAQ;GAAG,MAAM;GAAG;EAChD;EACA,iBAAiB,cAAc,OAAO,iBAAiB;EACvD,GAAI,cAAc,OACd,EAAE,gBAAgB,sEAAsE,GACxF,EAAE;EAEN,YAAY;EACZ,aAAa;EACb,kBAAkB;EAClB,WAAW;EACX,cAAc;EACd,oBAAoB;EACrB;;;;;;;;;AAUH,eAAe,gBACb,YACA,WACkC;AAClC,SAAQ,YAAR;EACE,KAAK,4BAEH,QAAO;GACL,IAAI;GACJ,QAHU,MAAM,UAAU,IAAI,gCAAgC,EAGnD;GACZ;EAEH,QAEE,QAAO;GACL,IAAI;GACJ,OACE,0BAA0B,WAAW;GAIxC;;;;AAMP,SAAgB,gBAAgB,OAA4B,EAAE,EAAU;CACtE,MAAM,cAAc,QAAQ,IAAI,oBAAoB;CACpD,MAAM,gBAAgB,GAAG,YAAY;CACrC,MAAM,YAAY,KAAK,aAAa,IAAI,cAAc,EAAE,eAAe,CAAC;CAExE,MAAM,SAAS,IAAI,OACjB;EAAE,MAAM;EAAgB,SAAA;EAAsB,EAC9C,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,EAAE,CAChC;AAED,QAAO,kBAAkB,+BAA+B,EACtD,OAAO,qBAAqB,KAAK,UAAU,EAAE,GAAG,MAAM,EAAE,EACzD,EAAE;AAEH,QAAO,kBAAkB,uBAAuB,OAAO,YAAY;EACjE,MAAM,OAAO,QAAQ,OAAO;AAC5B,MAAI,CAAC,eAAe,IAAI,KAAK,CAC3B,QAAO,SAAS,gBAAgB,OAAO;AAIzC,MAAI,oBAAoB,IAAI,KAAK,CAC/B,QAAO,SAAS,GAAG,KAAK,IAAI,8BAA8B;AAM5D,MAAI,kBAAkB,IAAI,KAAK,CAC7B,QAAO,mBACL,MACA,SACA,QACA,sIAED;AAGH,MAAI;GAEF,MAAM,YAAY,SAAS,4BAA4B,qBAAqB;AAG5E,OAAI,cAAc,UAAU,CAC1B,SAAQ,WAAR;IACE,KAAK,mBACH,QAAO,WAAW,MAAM,aAAa,UAAU,CAAC;IAClD,KAAK,gCACH,QAAO,WAAW,MAAM,0BAA0B,UAAU,CAAC;IAC/D,KAAK,wBACH,QAAO,WAAW,MAAM,kBAAkB,UAAU,CAAC;IACvD,QACE,QAAO,SAAS,gBAAgB,OAAO;;AAO7C,WAAQ,MAAR;IACE,KAAK,aACH,QAAO,eAAe,cAAc,wBAAwB,YAAY,CAAC;IAE3E,KAAK,mBACH,QAAO,eACL,oBACA,MAAM,oBAAoB,aAAa,gBAAgB,QAAQ,MAAM,IAAI,CAAC,CAC3E;IAEH,KAAK,oBACH,QAAO,eAAe,qBAAqB,MAAM,wBAAwB,UAAU,CAAC;IAEtF,KAAK,YAAY;KACf,MAAM,UAAU,QAAQ,OAAO,WAAW;AAC1C,SAAI,OAAO,YAAY,YAAY,YAAY,GAC7C,QAAO,SACL,iDACD;AAEH,YAAO,eAAe,YAAY,MAAM,gBAAgB,SAAS,UAAU,CAAC;;IAG9E,QACE,QAAO,SAAS,gBAAgB,OAAO;;WAEpC,KAAK;AAEZ,UAAO,SACL,GAAG,KAAK,OAFM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAEvC,qHAGxB;;GAEH;AAEF,QAAO;;AAGT,SAAS,WAAW,OAAgB;AAClC,QAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,OAAO,MAAM,EAAE;EAAE,CAAC,EAAE;;;;;;;;;;;AAYvF,SAAS,eAAe,MAAc,OAAgB;CACpD,MAAM,UAAU,aAAa,OAAO;EAAE;EAAM,KAAK;EAAQ,UAAU;EAAM,CAAC;AAC1E,QAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAiB,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE;EAAE,CAAC,EAAE;;;AAIzF,eAAsB,eAA8B;CAClD,MAAM,SAAS,iBAAiB;CAChC,MAAM,YAAY,IAAI,sBAAsB;AAC5C,OAAM,OAAO,QAAQ,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjiBjC,SAAgB,sBAAsB,MAAyB,QAAQ,KAAW;AAChF,KAAI,IAAI,YAAY,aAAc,eAAc,KAAK;;;;;;;;AAYvD,SAAgB,WAAW,MAAkC;AAC3D,QAAO,KAAK,SAAS,UAAU,IAAI,KAAK,SAAS,aAAa;;;AAIhE,SAAgB,UAAU,MAA+B;AACvD,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,KAAA,EAAW;AACvB,MAAI,IAAI,WAAW,UAAU,CAC3B,QAAO,cAAc,IAAI,MAAM,EAAiB,CAAC;AAEnD,MAAI,QAAQ,UAAU;GACpB,MAAM,OAAO,KAAK,IAAI;AACtB,OAAI,SAAS,KAAA,EACX,OAAM,IAAI,MAAM,uDAAuD;AAEzE,UAAO,cAAc,KAAK;;;AAG9B,QAAO;;;;;;;;;;AAWT,SAAgB,YAAY,MAAiC;AAC3D,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,KAAA,EAAW;AACvB,MAAI,IAAI,WAAW,YAAY,CAC7B,QAAO,gBAAgB,IAAI,MAAM,EAAmB,CAAC;AAEvD,MAAI,QAAQ,YAAY;GACtB,MAAM,OAAO,KAAK,IAAI;AACtB,OAAI,SAAS,KAAA,EACX,OAAM,IAAI,MAAM,sEAAsE;AAExF,UAAO,gBAAgB,KAAK;;;AAGhC,QAAO;;AAGT,SAAS,cAAc,OAAqB;AAC1C,KAAI,UAAU,MAAO,QAAO;AAC5B,KAAI,UAAU,QAAS,QAAO;AAC9B,OAAM,IAAI,MAAM,mBAAmB,MAAM,yCAAyC;;AAGpF,SAAS,gBAAgB,OAAuB;AAC9C,KAAI,UAAU,QAAS,QAAO;AAC9B,KAAI,UAAU,QAAS,QAAO;AAC9B,KAAI,UAAU,SAAU,QAAO;AAC/B,OAAM,IAAI,MAAM,qBAAqB,MAAM,sDAAsD;;AAGnG,eAAe,OAAsB;CACnC,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE;AAElC,wBAAuB;AAEvB,KADa,UAAU,KAAK,KACf,MACX,OAAM,cAAc;MACf;EACL,MAAM,SAAS,YAAY,KAAK;EAChC,MAAM,QAAQ,WAAW,KAAK;AAC9B,MAAI,WAAW,QACb,OAAM,oBAAoB,EAAE,OAAO,CAAC;WAC3B,WAAW,SACpB,OAAM,qBAAqB,EAAE,OAAO,CAAC;MAErC,OAAM,eAAe,EAAE,OAAO,CAAC;;;;;;;;;;;;;AAerC,SAAS,eAAwB;CAC/B,MAAM,QAAQ,KAAK;AACnB,KAAI,UAAU,KAAA,EAAW,QAAO;AAChC,KAAI;AACF,SAAO,cAAc,OAAO,KAAK,IAAI,KAAK,aAAa,MAAM;SACvD;AACN,SAAO;;;AAIX,IAAI,cAAc,CAChB,OAAM,CAAC,OAAO,QAAiB;CAC7B,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,SAAQ,OAAO,MAAM,yBAAyB,QAAQ,IAAI;AAC1D,SAAQ,WAAW;EACnB"}