@ait-co/devtools 0.1.44 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":["isObject","DEFAULT_BUFFER_SIZE","isObject","parseInbound","PHASE_1_EVENTS","require","isObject","isObject","jsonResult"],"sources":["../../src/mcp/ait-chii-source.ts","../../src/mcp/log.ts","../../src/mcp/chii-connection.ts","../../src/mcp/chii-relay.ts","../../src/mcp/devtools-opener.ts","../../src/mcp/environment.ts","../../src/mcp/errors.ts","../../src/mcp/local-connection.ts","../../src/mcp/local-launcher.ts","../../src/mcp/qr-http-server.ts","../../src/mcp/server-lock.ts","../../src/mcp/deeplink.ts","../../src/mcp/sdk-signatures.ts","../../src/mcp/tools.ts","../../src/mcp/totp.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 * 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 * 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 type {\n CdpCommandMap,\n CdpCommandName,\n CdpConnection,\n CdpEventMap,\n CdpEventName,\n CdpTarget,\n} from './cdp-connection.js';\nimport { logInfo } from './log.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\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 private readonly relayBaseUrl: string;\n private readonly bufferSize: number;\n private readonly commandTimeoutMs: 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 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 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 const res = await fetch(`${this.relayBaseUrl}/targets`);\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 const ws = new WebSocket(\n `${wsBase}/client/${clientId}?target=${encodeURIComponent(target.id)}`,\n );\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 // 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 ws.on('close', () => this.handleDisconnect('relay WebSocket 연결이 끊겼습니다'));\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 registers an HTTP upgrade\n * listener on the server BEFORE calling `chii.start({server})`. Node's\n * `http.Server` allows multiple 'upgrade' listeners; the first to call\n * `socket.destroy()` wins. Invalid auth → 401 + destroy (chii never sees\n * the connection). Valid auth → return without side-effect (chii handles it).\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\nconst require = createRequire(import.meta.url);\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\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>`).\n * @returns `true` if the upgrade is authorised, `false` to reject.\n */\n verifyAuth?: (req: IncomingMessage) => boolean;\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 } = options;\n\n const httpServer = createServer();\n\n // Register our auth listener BEFORE chii.start() so it fires first.\n // Node's http.Server emits 'upgrade' to all listeners in registration order;\n // the first to destroy() the socket wins. Valid requests return without\n // side-effect so chii's own upgrade handler takes over normally.\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.\n if (verifyAuth) {\n httpServer.on('upgrade', (req: IncomingMessage, socket: Duplex) => {\n if (!verifyAuth(req)) {\n // Reject: send a minimal HTTP 401 response and close the socket.\n // We do NOT log req.url or any auth param here to avoid leaking codes.\n socket.write('HTTP/1.1 401 Unauthorized\\r\\nContent-Length: 0\\r\\n\\r\\n');\n socket.destroy();\n // Early return — chii's handler is NOT called for this socket.\n return;\n }\n // Auth passed: no-op. Chii's upgrade listener (registered below by\n // chii.start) will handle the rest.\n });\n }\n\n const chii = loadChiiServer();\n // Passing an existing `server` makes chii attach its Koa handler + WS upgrade\n // to our HTTP server rather than creating its own listener.\n // Note: port/domain here are display-only inside chii — the TCP bind is ours.\n await chii.start({ server: httpServer, domain: `${host}:${requestedPort}`, port: requestedPort });\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 return {\n port: actualPort,\n baseUrl: `http://${host}:${actualPort}`,\n close: () =>\n new Promise<void>((resolve) => {\n httpServer.close(() => resolve());\n }),\n };\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. The\n * Chrome DevTools frontend can connect to any such endpoint via:\n *\n * https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html\n * ?wss=<host>[/<path>]\n * &panel=console\n *\n * Where `<host>` is the public WSS relay URL without the `wss://` scheme prefix\n * (the DevTools frontend adds it). This module assembles that URL and opens it\n * in the OS default browser so the developer immediately gets a full Chrome\n * DevTools UI.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chrome DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Base URL for the Chrome DevTools inspector hosted on appspot.\n *\n * The `@` path segment is the \"latest / bleeding edge\" alias which tracks the\n * current Chrome stable CDP protocol version — compatible with the chobitsu-\n * based CDP that Chii injects. A specific commit hash may be pinned here if\n * a regression is observed.\n */\nconst DEVTOOLS_FRONTEND_BASE =\n 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html';\n\n// ---------------------------------------------------------------------------\n// URL assembly\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chrome DevTools inspector URL that connects to a Chii relay\n * WebSocket.\n *\n * The `wss=` parameter expects a host-and-path string without the `wss://`\n * scheme prefix — the DevTools frontend prepends it automatically.\n *\n * @param wssRelayUrl - Full `wss://` URL of the Chii relay (public tunnel).\n * Example: `wss://abc.trycloudflare.com`\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @example\n * buildChromeDevtoolsUrl('wss://abc.trycloudflare.com')\n * // → 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html?wss=abc.trycloudflare.com&panel=console'\n */\nexport function buildChromeDevtoolsUrl(\n wssRelayUrl: string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string {\n // Strip `wss://` prefix — the DevTools frontend expects host[/path] only.\n const wssParam = wssRelayUrl.replace(/^wss:\\/\\//i, '');\n const params = new URLSearchParams({ wss: wssParam, panel });\n return `${DEVTOOLS_FRONTEND_BASE}?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled** via the `AIT_AUTO_DEVTOOLS`\n * env var. Only the explicit `\"0\"` value disables it; anything else (including\n * absent) leaves auto-open enabled.\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS === '0';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Manages auto-opening Chrome DevTools exactly once per relay attach session.\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onFirstAttach` callback to `startAttachWatcher`.\n *\n * The open fires at most once. Subsequent `open()` calls are no-ops.\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n private _opened = false;\n\n /**\n * Attempts to auto-open Chrome DevTools.\n *\n * No-op when any of the following conditions hold:\n * 1. Already opened this session (`_opened` is true).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. Environment is `mock` (env 1 — F12 is already available).\n * 4. `wssRelayUrl` is null/undefined/empty (tunnel not yet up).\n *\n * Always writes the DevTools URL to stderr so the developer can copy it\n * if the browser open fails or the popup is blocked.\n *\n * @param wssRelayUrl - The public `wss://` relay URL (from tunnel status).\n * @param env - Current MCP environment (`mock` | `relay`).\n */\n open(wssRelayUrl: string | null | undefined, env: McpEnvironment): void {\n if (this._opened) return;\n if (isAutoDevtoolsDisabled()) return;\n if (env === 'mock') return;\n if (!wssRelayUrl) return;\n\n this._opened = true;\n\n const devtoolsUrl = buildChromeDevtoolsUrl(wssRelayUrl);\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — Chrome DevTools를 자동으로 엽니다.\\n' +\n `[ait-debug] Chrome DevTools URL: ${devtoolsUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)\\n',\n );\n\n const opened = openUrlInBrowser(devtoolsUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /** Returns `true` if `open()` has passed all guards and fired once. */\n get opened(): boolean {\n return this._opened;\n }\n}\n","/**\n * MCP environment detection — single source of truth for `mock` vs `relay`.\n *\n * RFC #277 (\"MCP tool surface fidelity\") asks us to decide *once* per process\n * whether the agent is operating against:\n * - `mock` — a local dev browser running the @ait-co/devtools mock SDK\n * (env 1 in the 4-environments fidelity ladder), or\n * - `relay` — a real device WebView attached through the Chii relay + the\n * cloudflared quick tunnel (env 2/3/4 in the ladder).\n *\n * The env decides two things:\n *\n * 1. Which tools appear on `tools/list` (Tier A → mock-only, Tier B → relay-only,\n * Tier C → both). Tier filtering happens in `tools.ts` registry and the\n * `CallTool` handler in `debug-server.ts` / `server.ts`.\n * 2. Which code path `measure_safe_area` and other Tier C tools take when they\n * need to attach a `source: 'mock' | 'relay'` provenance label to results.\n *\n * Detection precedence (highest → lowest):\n * 1. `MCP_ENV=mock|relay` — explicit env var, always wins.\n * 2. CDP target URL pattern — when a target URL matches a known\n * real-device WebView pattern (intoss-\n * private:// scheme, *.trycloudflare.com\n * host) it is `relay`.\n * 3. default — `mock` (zero external side effect).\n *\n * The env decision is intentionally *sticky* per process. Switching env should\n * be a process restart, not a runtime toggle — the RFC's reasoning is that mid-\n * session env flips silently invalidate everything an agent has learned.\n *\n * SECRET-HANDLING: this module never reads the TOTP secret, deploy key, or any\n * URL component other than the scheme/host. The pattern matching uses public\n * surface only (intoss-private://… authority, *.trycloudflare.com host suffix).\n */\n\nimport type { CdpConnection } from './cdp-connection.js';\n\n/** The two environments the MCP server can operate in. */\nexport type McpEnvironment = 'mock' | 'relay';\n\n/**\n * Why a given environment was chosen. Stable strings suitable for stderr logs\n * and the `data.reason` field on rejection errors. Does NOT include any URL,\n * secret, or other potentially-sensitive value.\n */\nexport type EnvironmentReason =\n | 'env-var-mock'\n | 'env-var-relay'\n | 'cdp-target-url-relay-pattern'\n | 'default-mock';\n\n/**\n * URL patterns that mark a CDP target as a real-device WebView relay.\n *\n * - `intoss-private://` is the Toss in-app private scheme — only ever observed\n * inside the real Toss app WebView.\n * - `*.trycloudflare.com` (host suffix) is the cloudflared quick tunnel used as\n * the relay transport. A target whose URL is on that host is, by construction,\n * reached over the relay.\n *\n * Pattern-only matches — no specific tunnel host or deploymentId is hard-coded.\n */\nconst RELAY_URL_PATTERNS: ReadonlyArray<RegExp> = [\n /^intoss-private:\\/\\//i,\n /:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(\\/|$|:|\\?)/i,\n];\n\n/**\n * Returns true when the URL string looks like a real-device WebView attached\n * over the Chii relay. Used for `getEnvironment()` precedence step 2.\n */\nexport function isRelayUrl(url: string): boolean {\n if (typeof url !== 'string' || url.length === 0) return false;\n return RELAY_URL_PATTERNS.some((p) => p.test(url));\n}\n\n/**\n * Test/override hook — when non-null, `getEnvironment()` returns this value\n * regardless of env vars or connection state. Cleared with `null`.\n */\nlet envOverride: McpEnvironment | null = null;\n\n/**\n * Sets a sticky environment override. Intended for tests; production code paths\n * should leave the override `null` and let the precedence rules decide.\n */\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/** Parses the `MCP_ENV` env var into a `McpEnvironment` if valid. */\nfunction readEnvVar(): McpEnvironment | undefined {\n const raw = process.env.MCP_ENV;\n if (raw === 'mock' || raw === 'relay') return raw;\n return undefined;\n}\n\n/**\n * Decision input for `getEnvironment` / `getEnvironmentReason`. The connection\n * is optional — when omitted, only the env var and default are consulted.\n *\n * Production callers pass the live `CdpConnection` so the URL-pattern step\n * (precedence 2) can fire. Tests can omit it to exercise pure precedence.\n */\nexport interface EnvironmentInput {\n /**\n * Live CDP connection — when its `listTargets()` includes a URL matching the\n * real-device pattern, the env resolves to `relay`. Optional.\n */\n connection?: Pick<CdpConnection, 'listTargets'>;\n}\n\n/**\n * Returns the current MCP environment, applying the precedence rules:\n * 1. test override (if set)\n * 2. `MCP_ENV` env var\n * 3. CDP target URL pattern match\n * 4. default `mock`\n */\nexport function getEnvironment(input: EnvironmentInput = {}): McpEnvironment {\n if (envOverride !== null) return envOverride;\n const fromEnv = readEnvVar();\n if (fromEnv !== undefined) return fromEnv;\n const { connection } = input;\n if (connection !== undefined) {\n const targets = connection.listTargets();\n for (const t of targets) {\n if (isRelayUrl(t.url)) return 'relay';\n }\n }\n return 'mock';\n}\n\n/**\n * Returns the `EnvironmentReason` that drove the current `getEnvironment()`\n * result. Used by stderr logs and the rejection-reason payload on Tier A/B\n * mismatch errors. SECRET-HANDLING: only stable enum strings — no URL or\n * secret value is ever returned.\n */\nexport function getEnvironmentReason(input: EnvironmentInput = {}): EnvironmentReason {\n if (envOverride !== null) return envOverride === 'mock' ? 'env-var-mock' : 'env-var-relay';\n const fromEnv = readEnvVar();\n if (fromEnv === 'mock') return 'env-var-mock';\n if (fromEnv === 'relay') return 'env-var-relay';\n const { connection } = input;\n if (connection !== undefined) {\n const targets = connection.listTargets();\n for (const t of targets) {\n if (isRelayUrl(t.url)) return 'cdp-target-url-relay-pattern';\n }\n }\n return 'default-mock';\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 미주입 — dogfood 채널로 재배포\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 - 환경이 결정된 근거 (EnvironmentReason 문자열).\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 ? 'build_attach_url → QR 스캔으로 실기기를 attach하세요.'\n : 'MCP_ENV=mock 또는 relay 환경변수를 확인하세요.';\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 'build_attach_url로 deep link를 생성하고 QR을 스캔해 미니앱을 attach하세요.',\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이 주입되지 않았다 (dogfood 빌드가 아님).\n *\n * call_sdk 호출 시 브리지가 없을 때.\n */\nexport function sdkAbsentError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}window.__sdkCall이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). ` +\n 'dogfood 채널(intoss-private)로 번들을 재배포한 뒤 재시도하세요.',\n );\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): 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 부재\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);\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 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","/**\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 * SECRET-HANDLING:\n * - 127.0.0.1 바인딩만 — 외부 노출 0.\n * - attachUrl은 HTML 본문과 /qr.png query에만 들어간다 (의도된 전달 경로).\n * - stdout/stderr/로그에 별도 출력하지 않는다.\n * - tmp 파일 만들지 않음 — 모든 응답을 메모리에서 생성.\n */\n\nimport type { Server } from 'node:http';\nimport { createServer } from 'node:http';\n\nexport interface QrHttpServer {\n port: number;\n /** `http://127.0.0.1:<port>/attach?u=<encoded>` URL 생성 헬퍼. */\n buildAttachPageUrl(attachUrl: string): string;\n close(): Promise<void>;\n}\n\n/**\n * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.\n * MCP debug server 생애주기에 묶어 사용 — `runDebugServer` shutdown 시 `close()`로 정리.\n */\nexport async function startQrHttpServer(): Promise<QrHttpServer> {\n const { default: QRCode } = await import('qrcode');\n\n const server: Server = createServer((req, res) => {\n const rawUrl = req.url ?? '/';\n const [path, query = ''] = rawUrl.split('?', 2) as [string, string | undefined];\n const params = new URLSearchParams(query ?? '');\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 // QR을 base64 data URL로 인라인 생성 — 외부 fetch 없이 self-contained HTML.\n QRCode.toDataURL(attachUrl, { type: 'image/png', errorCorrectionLevel: 'M' })\n .then((dataUrl: string) => {\n const safeLabel = deploymentIdLabel.replace(/[<>&\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n const safeAttachUrl = attachUrl.replace(/[<>&\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n const html = buildAttachHtml(dataUrl, safeLabel, safeAttachUrl);\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 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 return {\n port,\n buildAttachPageUrl(attachUrl: string): string {\n return `http://127.0.0.1:${port}/attach?u=${encodeURIComponent(attachUrl)}`;\n },\n close(): Promise<void> {\n return new Promise((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n },\n };\n}\n\n/**\n * QR 스캔 페이지 HTML 본문.\n * dark theme, inline style, 외부 fetch 없음.\n */\nfunction buildAttachHtml(qrDataUrl: string, safeLabel: string, safeAttachUrl: string): string {\n return `<!DOCTYPE html>\n<html lang=\"ko\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>AIT 디버그 세션 — QR 스캔</title>\n <style>\n *, *::before, *::after { box-sizing: border-box; }\n body {\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 }\n h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\n img.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n }\n section { width: 100%; max-width: 480px; }\n h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\n ol, ul { margin: 0; padding-left: 1.25rem; }\n li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\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 border-radius: 6px; border: 1px solid #30363d;\n }\n hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n </style>\n</head>\n<body>\n <h1>AIT 디버그 세션 — QR 스캔</h1>\n <p class=\"label\">deployment: ${safeLabel}</p>\n <img class=\"qr\" src=\"${qrDataUrl}\" alt=\"attach QR\" />\n\n <section>\n <h2>스캔 절차</h2>\n <ol>\n <li>토스 앱을 실행하세요.</li>\n <li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li>\n <li>팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.</li>\n <li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li>\n </ol>\n </section>\n\n <hr />\n\n <section>\n <h2>진단 체크리스트</h2>\n <ul>\n <li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li>\n <li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li>\n <li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li>\n <li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li>\n </ul>\n </section>\n\n <hr />\n\n <section>\n <h2>URL (fallback)</h2>\n <p class=\"url-box\">${safeAttachUrl}</p>\n </section>\n</body>\n</html>`;\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';\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\nexport interface LockHandle {\n /** Updates the wssUrl field in the lock file once the tunnel URL is known. */\n updateWssUrl(wssUrl: string): 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\n constructor(existingPid: number, existingWssUrl: string | null) {\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 }\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 * 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// 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 return {\n pid: p.pid as number,\n wssUrl: typeof p.wssUrl === 'string' ? p.wssUrl : null,\n startedAt: p.startedAt as string,\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// 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_diagnostics` 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/**\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: throws `ServerLockConflictError`.\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(): LockHandle {\n const lockPath = lockFilePath();\n const existing = readLock(lockPath);\n\n if (existing !== null) {\n if (isPidAlive(existing.pid)) {\n throw new ServerLockConflictError(existing.pid, existing.wssUrl);\n }\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 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 release(): void {\n if (released) return;\n released = true;\n removeLock(lockPath);\n },\n };\n}\n","/**\n * Build a self-attaching dogfood deep link.\n *\n * `ait deploy --scheme-only` prints an `intoss-private://…?_deploymentId=<uuid>`\n * URL that opens a dogfood 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 * 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 { lookupSignature, warnPassthrough } from './sdk-signatures.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 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 'Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a ' +\n 'self-attaching deep link by splicing in debug=1 and the live relay URL for this session. ' +\n 'Returns the deep link JSON and a unicode QR of that deep link. Scan the QR with the phone ' +\n 'camera to open the mini-app and attach it to this debug session (QR is the single entry ' +\n 'path — no USB cable or platform CLI needed). Requires the tunnel to be up — call ' +\n 'list_pages first. Set wait_for_attach=true to block until the phone scans and a page ' +\n 'attaches (polls listTargets up to 90 s), then returns the attached page info too. ' +\n 'When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default ' +\n 'browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers).',\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 '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, timeout 90 s). On attach, the response includes the ' +\n 'attached page list. On timeout, returns an error with a list_pages retry hint.',\n },\n open_in_browser: {\n type: 'boolean',\n description:\n 'If true (default), render the QR as a PNG and open it in the OS default browser. ' +\n 'Only works when the MCP server is running on a local GUI machine — headless or ' +\n 'remote container environments should set this to false to use the text QR fallback.',\n },\n },\n required: ['scheme_url'],\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. Returns an image content block.',\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\"` field so consumers can identify ' +\n 'provenance without inspecting payload values. ' +\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 inputSchema: {\n type: 'object',\n properties: {\n expression: {\n type: 'string',\n description: 'JavaScript expression to evaluate in the page context.',\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 dogfood 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 2/3 (real device relay) this hits the real SDK; on env 1 (local mock) it hits ' +\n 'the mock SDK. 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 (non-dogfood bundle).\\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 },\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: 'get_diagnostics',\n description:\n 'Returns a single-call server status snapshot so the agent can diagnose \"why is this not ' +\n 'working?\" without calling multiple tools. 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 'environment (getEnvironment() result + reason), ' +\n 'serverLockHolder (pid + startedAt from the lock file, or null). ' +\n 'All fields are nullable — missing data is null, not an error. ' +\n 'Tier C (both mock and relay). Call this first when debugging session state.',\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 */\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 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 */\nexport function filterToolsByEnvironment<T extends { name: string; availableIn: ToolAvailability }>(\n tools: ReadonlyArray<T>,\n env: McpEnvironment,\n): T[] {\n return tools.filter((t) => t.availableIn === 'both' || t.availableIn === env);\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_diagnostics',\n 'list_pages',\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>` 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\n/**\n * Builds a self-attaching dogfood 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 * 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 */\nexport function buildAttachUrl(schemeUrl: string, tunnel: TunnelStatus): 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 return {\n attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl),\n relayUrl: tunnel.wssUrl,\n ...(authorityWarning !== undefined ? { authorityWarning } : {}),\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 로컬 전용이며 at= 코드 값을 직접 담지 않는다\n * (attachUrl은 /attach?u= query로 전달되어 서버 메모리에서만 처리).\n */\nexport interface OpenQrInBrowserResult {\n /** `true` if the browser was successfully opened. */\n opened: boolean;\n /** `http://127.0.0.1:<port>/attach?u=...` — 브라우저에 전달된 URL. */\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>/attach?u=...`)을 OS 기본 브라우저로 연다.\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/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>/attach?u=<encoded>` HTTP URL.\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()` — dogfood bundle on real device.\n * b. `window.__sdk.getSafeAreaInsets()` — dogfood 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 dogfood 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 — `'mock'` for the dev\n * browser panel, `'relay'` for the real-device WebView. Set by the caller\n * (`measureSafeArea`) from the env detection SSoT (`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, dogfood 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-dogfood 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이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). dogfood 채널로 재배포하세요.'});` +\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 dogfood SDK method via `window.__sdkCall` on the attached page.\n * NOT read-only — SDK calls may have side effects.\n *\n * On env 2/3 (real device relay) this hits the real SDK; on env 1 (local\n * mock) 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_diagnostics — 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\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 * Full server status snapshot returned by `get_diagnostics`.\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 */\n recentErrors: DiagnosticsError[];\n /** Resolved environment (`mock` | `relay`) and the reason string. */\n environment: {\n env: McpEnvironment;\n reason: string;\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\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_diagnostics`. */\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\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\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\n/**\n * Reads the `@modelcontextprotocol/sdk` package version from the installed\n * package's `package.json`. Returns `null` on any error (missing file, JSON\n * parse failure, etc.) — diagnostics must never throw.\n *\n * Node-only — uses dynamic `import()` so it does not pollute the browser\n * module graph.\n */\nexport async function readMcpSdkVersion(): Promise<string | null> {\n try {\n // Resolve the package.json adjacent to the installed SDK entry point.\n const { createRequire } = await import('node:module');\n const req = createRequire(import.meta.url);\n const pkgPath = req.resolve('@modelcontextprotocol/sdk/package.json');\n const { readFileSync } = await import('node:fs');\n const raw = readFileSync(pkgPath, '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 try {\n // `__VERSION__` is injected by tsdown / vite via `define`.\n // biome-ignore lint/suspicious/noExplicitAny: intentional global check\n const v = (globalThis as any).__VERSION__;\n return typeof v === 'string' && v.length > 0 ? v : null;\n } catch {\n return null;\n }\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`). Caller obtains via\n * `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\n/**\n * Builds the `get_diagnostics` 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 } = 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 const tunnelInfo: DiagnosticsTunnelInfo = {\n up: tunnel.up,\n wssUrl: tunnel.wssUrl,\n pid: lockData?.pid ?? null,\n startedAt: lockData?.startedAt ?? null,\n };\n\n // list_pages — non-fatal; null on any error.\n let pages: ListPagesResult | null = null;\n if (connection !== undefined) {\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 return {\n mcpVersion,\n devtoolsVersion,\n tunnel: tunnelInfo,\n pages,\n lastAttachAt: collector.getLastAttachAt(),\n lastDetachAt: collector.getLastDetachAt(),\n recentErrors,\n environment: { env, reason: envReason },\n serverLockHolder,\n };\n}\n","/**\n * RFC 6238 TOTP implementation (Node.js, node:crypto only).\n *\n * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used\n * to keep the dependency surface minimal. This hand-roll is ~30 lines and\n * covers exactly what relay-side auth needs.\n *\n * Algorithm summary (RFC 6238 + RFC 4226):\n * T = floor(now / 30) — 30-second time step counter\n * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)\n * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)\n * offset = MAC[19] & 0x0f\n * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits\n *\n * Security note (keep this comment accurate):\n * The baked-in secret in a dogfood build is extractable from the bundle by a\n * determined reverse engineer. This mechanism raises the bar from\n * \"anyone with the URL\" to \"URL + bundle extraction + live TOTP calculation\".\n * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are\n * blocked; deliberate reverse engineering is not. See threat model in\n * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.\n *\n * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any\n * log, error message, or string visible outside this module. Only boolean\n * pass/fail and reason enum values are safe to surface.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Time step window in seconds (RFC 6238 default). */\nconst TIME_STEP = 30;\n\n/** Number of digits in the generated code. */\nconst DIGITS = 6;\n\n/**\n * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-\n * clock time.\n *\n * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32\n * bytes). Must be the output of `generateAttachToken()` or compatible.\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @returns A zero-padded 6-digit decimal string, e.g. `\"042193\"`.\n */\nexport function generateTotp(secret: string, when: number = Date.now()): string {\n const key = Buffer.from(secret, 'hex');\n // Clamp to 0 so negative timestamps (e.g. in ±skew checks near epoch) do not\n // produce a negative counter, which would cause writeUInt32BE to throw.\n const counter = Math.max(0, Math.floor(when / 1000 / TIME_STEP));\n\n // Encode counter as 8-byte big-endian unsigned integer.\n const counterBuf = Buffer.alloc(8);\n // JavaScript numbers are safe integers up to 2^53; counter is ~7.5×10^10 at\n // year 9999 — well within safe range so standard bitwise ops are fine.\n const hi = Math.floor(counter / 0x100000000);\n const lo = counter >>> 0;\n counterBuf.writeUInt32BE(hi, 0);\n counterBuf.writeUInt32BE(lo, 4);\n\n const mac = createHmac('sha1', key).update(counterBuf).digest();\n\n // Dynamic truncation (RFC 4226 §5.4).\n const offset = mac[19] & 0x0f;\n const binCode =\n ((mac[offset] & 0x7f) << 24) |\n ((mac[offset + 1] & 0xff) << 16) |\n ((mac[offset + 2] & 0xff) << 8) |\n (mac[offset + 3] & 0xff);\n\n const otp = binCode % 10 ** DIGITS;\n return otp.toString().padStart(DIGITS, '0');\n}\n\n/**\n * Verifies a TOTP code against the secret, accepting ±`skew` time steps to\n * tolerate clock drift between the relay host and the client device.\n *\n * Uses `timingSafeEqual` for constant-time comparison to prevent timing\n * side-channel attacks.\n *\n * @param secret - Hex-encoded shared secret.\n * @param code - The 6-digit code to verify (string or numeric).\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @param skew - Number of adjacent steps to accept on either side. Default 1\n * (accepts T-1, T, T+1 — a 90-second acceptance window).\n * @returns `true` if the code matches any accepted step, `false` otherwise.\n */\nexport function verifyTotp(\n secret: string,\n code: string,\n when: number = Date.now(),\n skew: number = 1,\n): boolean {\n const normalised = String(code).padStart(DIGITS, '0');\n if (normalised.length !== DIGITS || !/^\\d{6}$/.test(normalised)) {\n return false;\n }\n\n const candidateBuf = Buffer.from(normalised, 'utf8');\n\n for (let delta = -skew; delta <= skew; delta++) {\n const stepWhen = when + delta * TIME_STEP * 1000;\n const expected = generateTotp(secret, stepWhen);\n const expectedBuf = Buffer.from(expected, 'utf8');\n if (timingSafeEqual(expectedBuf, candidateBuf)) {\n return true;\n }\n }\n\n return false;\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 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 */\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 return {\n url,\n wssUrl: url.replace(/^https/, 'wss'),\n stop: () => {\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 dogfood 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 * @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 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 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 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 }, 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 { 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 { AutoDevtoolsOpener } from './devtools-opener.js';\nimport { getEnvironment, getEnvironmentReason, type McpEnvironment } from './environment.js';\nimport {\n classifyToolError,\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 { type QrHttpServer, startQrHttpServer } from './qr-http-server.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 { verifyTotp } from './totp.js';\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/** Live infra the connection reads tunnel status from. */\nexport interface DebugServerDeps {\n connection: CdpConnection;\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 90 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`) per RFC #277.\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 to a function that asks `getEnvironment(input)` with\n * the live connection. Tests inject a fake to pin the env without touching\n * `setEnvironmentOverride` (which is process-global).\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_diagnostics`. 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\n/**\n * Waits for the first target matching `filterFn` to attach, using the\n * event-driven `waitForFirstTarget()` on `ChiiCdpConnection` instances, or\n * falling back to a polling loop for generic `CdpConnection` fakes (tests).\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 non-ChiiCdpConnection.\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 (ChiiCdpConnection production instances).\n if (connection instanceof ChiiCdpConnection) {\n return connection.waitForFirstTarget(filterFn, timeoutMs, pollIntervalMs);\n }\n // Generic fallback for test fakes that implement CdpConnection but not\n // waitForFirstTarget (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 aitSource,\n getTunnelStatus,\n waitForAttachTimeoutMs = 90_000,\n qrHttpServer,\n getEnvironment: getEnvDep,\n getEnvironmentReason: getEnvReasonDep,\n diagnosticsCollector: collectorDep,\n } = deps;\n\n // Env SSoT — production wires the real `getEnvironment` with the connection;\n // tests inject fakes. Lazy so each request reflects the live connection.\n const resolveEnvironment: () => McpEnvironment =\n getEnvDep ?? (() => getEnvironment({ connection }));\n const resolveEnvironmentReason: () => string =\n getEnvReasonDep ?? (() => getEnvironmentReason({ connection }));\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 const env = resolveEnvironment();\n const attached = connection.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 // 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 const env = resolveEnvironment();\n if (!isToolAvailableIn(name, env)) {\n const requiredEnv = getToolAvailability(name) ?? 'unknown';\n const envReason = resolveEnvironmentReason();\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 connection.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_diagnostics 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_diagnostics') {\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,\n env: resolveEnvironment(),\n envReason: resolveEnvironmentReason(),\n collector,\n readLock: readServerLock,\n recentErrorsLimit,\n });\n return jsonResult(result);\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n // build_attach_url is pure synthesis (scheme URL + relay URL → deep link).\n // It works before any page attaches, so it must not require enableDomains.\n if (name === 'build_attach_url') {\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 );\n }\n const waitForAttach = request.params.arguments?.wait_for_attach === true;\n // open_in_browser defaults to true when not explicitly set.\n const openInBrowser = request.params.arguments?.open_in_browser !== false;\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 try {\n const { attachUrl, relayUrl, authorityWarning } = buildAttachUrl(\n schemeUrl,\n getTunnelStatus(),\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 환경 감지: open_in_browser=true인데 GUI가 없는 경우 안내 후 text QR fallback.\n if (openInBrowser && !guiAvailable) {\n const headlessNote =\n '[open_in_browser] GUI 환경이 감지되지 않았습니다 (headless/remote 환경). ' +\n 'open_in_browser=false로 자동 폴백합니다. ' +\n '텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\\n\\n';\n const qrHeadless = await renderQr(attachUrl);\n const headlessText = `${warningPrefix}${headlessNote}${header}\\n${JSON.stringify({ attachUrl, relayUrl }, 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(\n connection,\n isMatchingPage,\n waitForAttachTimeoutMs,\n );\n } catch {\n attachedPagesHl = connection.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(\n headlessText,\n waitForAttachTimeoutMs / 1000,\n attachedPagesHl,\n ),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResultHl = listPages(connection, 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 when requested, a GUI is available, and the HTTP server is up.\n if (openInBrowser && 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 }, 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(\n connection,\n isMatchingPage,\n waitForAttachTimeoutMs,\n );\n } catch {\n attachedPages = connection.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(\n shortText,\n waitForAttachTimeoutMs / 1000,\n attachedPages,\n ),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResult = listPages(connection, 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 `[open_in_browser] 브라우저 자동 열기에 실패했습니다. ` +\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 }, 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(\n connection,\n isMatchingPage,\n waitForAttachTimeoutMs,\n );\n } catch {\n attachedPagesFb = connection.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1000, attachedPagesFb),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResultFb = listPages(connection, 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 // open_in_browser=false or 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 }, 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(\n connection,\n isMatchingPage,\n waitForAttachTimeoutMs,\n );\n } catch {\n attachedPages = connection.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResult = listPages(connection, 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 connection.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 if (connection instanceof ChiiCdpConnection) {\n try {\n await connection.refreshTargets();\n } catch {\n // Ignore refresh errors — still return cached state.\n }\n }\n return jsonResult(listPages(connection, getTunnelStatus()));\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(connection));\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(connection, limit) });\n }\n case 'list_network_requests':\n return jsonResult(listNetworkRequests(connection));\n case 'list_pages':\n // Refresh from relay so evict→reattach transitions are not served stale.\n if (connection instanceof ChiiCdpConnection) {\n try {\n await connection.refreshTargets();\n } catch {\n // Ignore refresh errors — still return cached state.\n }\n }\n return jsonResult(listPages(connection, getTunnelStatus()));\n case 'get_dom_document':\n return jsonResult(await getDomDocument(connection));\n case 'take_snapshot':\n return jsonResult(await takeSnapshot(connection));\n case 'take_screenshot': {\n const shot = await takeScreenshot(connection);\n return {\n content: [{ type: 'image' as const, data: shot.data, mimeType: shot.mimeType }],\n };\n }\n case 'measure_safe_area':\n // Pass env to attach `source: 'mock' | 'relay'` to the result (Tier C\n // parity per RFC #277 — the same Runtime.evaluate probe runs in both\n // envs; only the provenance label differs).\n return jsonResult(await measureSafeArea(connection, resolveEnvironment()));\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 // SECRET-HANDLING: do not log expression or result value.\n return jsonResult(await evaluate(connection, 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 // SECRET-HANDLING: do not log name, args, or result value.\n const sdkResult = await callSdk(connection, 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 return sdkAbsentError('call_sdk');\n }\n return jsonResult(sdkResult);\n }\n default:\n return unknownTool(name);\n }\n } catch (err) {\n return errorResult(err, name);\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\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) {\n return classifyToolError(err, name);\n}\n\n/**\n * Starts a polling watcher that detects the first 0→N target transition 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()` exactly once — on the first transition — then\n * clears itself. Shutdown calls `stop()` to clear the interval.\n *\n * `onFirstAttach` is called once on the 0→N transition (or immediately when\n * already attached). Use this to trigger side-effects such as auto-opening\n * Chrome DevTools (issue #282). The callback is optional; omitting it preserves\n * 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 onFirstAttach?: () => void,\n): { stop(): void } {\n let wasAttached = connection.listTargets().length > 0;\n // If already attached when the watcher starts, send once immediately.\n if (wasAttached) {\n void server.sendToolListChanged();\n onFirstAttach?.();\n }\n\n const handle = setInterval(() => {\n const isAttached = connection.listTargets().length > 0;\n if (!wasAttached && isAttached) {\n wasAttached = true;\n // Emit once on 0→N transition so the MCP client refreshes its tool list.\n void server.sendToolListChanged();\n onFirstAttach?.();\n clearInterval(handle);\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\n/**\n * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a\n * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.\n *\n * The predicate checks the `at` query parameter against the current and\n * adjacent TOTP time steps (±1 skew) using `verifyTotp`.\n *\n * Returns `undefined` when the env var is not set — callers treat that as\n * \"auth disabled\" (no predicate registered on the relay).\n *\n * SECRET-HANDLING: The secret value read from env is captured in a closure and\n * is NEVER written to any log, error message, or process output.\n */\nexport function buildRelayVerifyAuth():\n | ((req: import('node:http').IncomingMessage) => boolean)\n | undefined {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n if (!secret) return undefined;\n\n return (req) => {\n // Parse the `at` query param from the upgrade request URL.\n // req.url is the raw request path + query, e.g. `/client/id?target=…&at=123456`\n const rawUrl = req.url ?? '';\n const qIndex = rawUrl.indexOf('?');\n const queryStr = qIndex === -1 ? '' : rawUrl.slice(qIndex + 1);\n const params = new URLSearchParams(queryStr);\n const code = params.get('at') ?? '';\n\n // Do NOT log `code`, `secret`, or any derived value here.\n return verifyTotp(secret, code);\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 const lockHandle = acquireLock();\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\n // Build the TOTP verifyAuth predicate from env at startup (runtime read).\n const verifyAuth = buildRelayVerifyAuth();\n const totpEnabled = verifyAuth !== undefined;\n\n const relay = await startChiiRelay({ port: relayPort, verifyAuth });\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 // generateAttachToken is kept for legacy/non-TOTP token use, but we no\n // longer print it in the banner to avoid accidental secret exposure.\n const _token = generateAttachToken();\n\n // Health probe handle — started once the initial tunnel is up.\n let tunnelProbe: { stop(): void } | null = null;\n\n // Bring the cloudflared tunnel up in the background so the MCP stdio\n // transport can answer `initialize` immediately. cloudflared has to lazy-\n // download a ~38 MB binary on first run; awaiting it here pushes the\n // initialize response past Claude Code's MCP connection timeout. Tools that\n // need the tunnel (`build_attach_url`) already gate on `getTunnelStatus()`\n // and return a clear \"tunnel not up\" message when it isn't ready yet, so\n // dropping the await is 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 // Update the lock file with the assigned tunnel URL so a second caller\n // can see the correct wssUrl in the conflict error message.\n lockHandle.updateWssUrl(t.wssUrl);\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 lockHandle.updateWssUrl(newTunnel.wssUrl);\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 = new ChiiCdpConnection({ relayBaseUrl: relay.baseUrl });\n // AIT.* methods ride the same Chii channel as CDP commands.\n const aitSource = new ChiiAitSource(connection);\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();\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 const devtoolsOpener = new AutoDevtoolsOpener();\n\n // Diagnostics collector — records server-side errors and attach/detach events\n // so `get_diagnostics` can surface them in a single call.\n const diagnosticsCollector = new InMemoryDiagnosticsCollector();\n\n const server = createDebugServer({\n connection,\n aitSource,\n getTunnelStatus: () => tunnelStatus,\n get qrHttpServer() {\n return qrServer;\n },\n diagnosticsCollector,\n });\n\n const transport = new StdioServerTransport();\n\n // ---------------------------------------------------------------------------\n // Shutdown: best-effort cleanup of relay + cloudflared child process.\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 attachWatcher: { 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 attachWatcher?.stop();\n tunnelProbe?.stop();\n connection.close();\n // tunnel.stop() is synchronous (child process kill) — safe from exit handler.\n tunnel?.stop();\n // relay.close(), server.close(), qrServer.close() are async — fine for signal handlers.\n void relay.close();\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 tunnel.stop() which is a sync child kill).\n process.on('exit', () => {\n if (!closed) {\n closed = true;\n attachWatcher?.stop();\n tunnelProbe?.stop();\n tunnel?.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 // Start the attach watcher after the transport is connected so\n // sendToolListChanged has a live session to notify.\n // The onFirstAttach callback auto-opens Chrome DevTools when a page attaches\n // over the relay (issue #282). It is a no-op in mock env and when\n // AIT_AUTO_DEVTOOLS=0. The tunnel wssUrl may still be null here when\n // cloudflared is still starting; devtoolsOpener.open() guards against that.\n attachWatcher = startAttachWatcher(connection, server, 1_000, () => {\n diagnosticsCollector.recordAttach();\n devtoolsOpener.open(tunnelStatus.wssUrl, getEnvironment({ connection }));\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\n/**\n * Boots the local-browser debug stack and serves it over stdio:\n * 1. launch a local Chromium with `--remote-debugging-port=<port>`,\n * 2. attach a `LocalCdpConnection` to the first non-blank page target,\n * 3. expose the debug tools backed by that connection + a `ChiiAitSource`.\n *\n * `build_attach_url` (relay-specific, generates a deep-link + QR for the phone)\n * is not applicable in local mode because there is no relay or tunnel. The tool\n * is still listed (it is part of `DEBUG_TOOL_DEFINITIONS`) but will return a\n * clear \"not applicable\" message via the tunnel-down path (wssUrl is null).\n *\n * The AIT.* tools (`AIT.getSdkCallHistory`, `AIT.getMockState`,\n * `AIT.getOperationalEnvironment`) ride the same CDP channel via\n * `ChiiAitSource` → `LocalCdpConnection.sendCommand`. They will succeed once\n * the sdk-example dev-bridge (`window.__sdkCall` install) lands in sdk-example;\n * until then they return the sdk-example \"bridge absent\" message — which is\n * expected and noted in the PR as an explicit out-of-scope follow-up.\n */\nexport async function runLocalDebugServer(options: RunLocalDebugServerOptions = {}): Promise<void> {\n // Enforce a single debug session per machine (same lock as relay mode).\n const lockHandle = acquireLock();\n\n const cdpPort = options.cdpPort ?? 0;\n const devUrl = options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n\n const chromium = await launchChromium({ port: cdpPort, devUrl });\n\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\n const connection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });\n // AIT.* methods ride the same CDP channel via LocalCdpConnection.sendCommand.\n const aitSource = new ChiiAitSource(connection);\n\n // Local mode has no relay tunnel — tunnelStatus is always \"down\" which causes\n // build_attach_url to return a clear \"tunnel not up\" error, communicating to\n // the agent that this tool is relay-only.\n const tunnelStatus: TunnelStatus = { up: false, wssUrl: null };\n\n const server = createDebugServer({\n connection,\n aitSource,\n getTunnelStatus: () => tunnelStatus,\n });\n\n const transport = new StdioServerTransport();\n\n let closed = false;\n let attachWatcher: { stop(): void } | null = null;\n\n const shutdown = () => {\n if (closed) return;\n closed = true;\n attachWatcher?.stop();\n connection.close();\n chromium.stop();\n void server.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 attachWatcher?.stop();\n chromium.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',\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',\n });\n shutdown();\n process.exit(1);\n });\n\n await server.connect(transport);\n\n // Start the attach watcher after the transport is connected so\n // sendToolListChanged has a live session to notify.\n attachWatcher = startAttachWatcher(connection, server);\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 * 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 { mcpError } from './errors.js';\nimport {\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n isAitToolName,\n type ToolAvailability,\n} from './tools.js';\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 */\nconst DEV_TOOL_DEFINITIONS = [\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] as const;\n\nconst DEV_TOOL_NAMES = new Set<string>(DEV_TOOL_DEFINITIONS.map((t) => t.name));\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/** 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 try {\n // `devtools_get_mock_state` is an alias of `AIT.getMockState`.\n const effective = name === 'devtools_get_mock_state' ? 'AIT.getMockState' : name;\n if (!isAitToolName(effective)) {\n return mcpError(`알 수 없는 tool: ${name}`);\n }\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 } 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/** 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 2/3) 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 *\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 * Node-only stdio process.\n */\n\nimport { realpathSync } from 'node:fs';\nimport { argv } from 'node:process';\nimport { fileURLToPath } from 'node:url';\nimport { runDebugServer, runLocalDebugServer } from './debug-server.js';\nimport { runDevServer } from './server.js';\n\ntype Mode = 'debug' | 'dev';\ntype Target = 'relay' | 'local';\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 2/3).\n * - `local` — local Chromium CDP attach (env 1, no relay needed).\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) or 'local'.\");\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 throw new Error(`Unknown --target '${value}'. Expected 'relay' (default) or 'local'.`);\n}\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n const mode = parseMode(args);\n if (mode === 'dev') {\n await runDevServer();\n } else {\n const target = parseTarget(args);\n if (target === 'local') {\n await runLocalDebugServer();\n } else {\n await runDebugServer();\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":"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"}
1
+ {"version":3,"file":"cli.js","names":["isObject","DEFAULT_BUFFER_SIZE","isObject","parseInbound","PHASE_1_EVENTS","require","isObject","isObject","jsonResult"],"sources":["../../src/mcp/ait-chii-source.ts","../../src/mcp/log.ts","../../src/mcp/chii-connection.ts","../../src/mcp/chii-relay.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/mcp/qr-http-server.ts","../../src/mcp/server-lock.ts","../../src/mcp/deeplink.ts","../../src/mcp/sdk-signatures.ts","../../src/mcp/totp.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 * 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 * 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 type {\n CdpCommandMap,\n CdpCommandName,\n CdpConnection,\n CdpEventMap,\n CdpEventName,\n CdpTarget,\n} from './cdp-connection.js';\nimport { logInfo } from './log.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\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 private readonly relayBaseUrl: string;\n private readonly bufferSize: number;\n private readonly commandTimeoutMs: 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 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 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 const res = await fetch(`${this.relayBaseUrl}/targets`);\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 const ws = new WebSocket(\n `${wsBase}/client/${clientId}?target=${encodeURIComponent(target.id)}`,\n );\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 // 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 ws.on('close', () => this.handleDisconnect('relay WebSocket 연결이 끊겼습니다'));\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 registers an HTTP upgrade\n * listener on the server BEFORE calling `chii.start({server})`. Node's\n * `http.Server` allows multiple 'upgrade' listeners; the first to call\n * `socket.destroy()` wins. Invalid auth → 401 + destroy (chii never sees\n * the connection). Valid auth → return without side-effect (chii handles it).\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\nconst require = createRequire(import.meta.url);\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\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>`).\n * @returns `true` if the upgrade is authorised, `false` to reject.\n */\n verifyAuth?: (req: IncomingMessage) => boolean;\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 } = options;\n\n const httpServer = createServer();\n\n // Register our auth listener BEFORE chii.start() so it fires first.\n // Node's http.Server emits 'upgrade' to all listeners in registration order;\n // the first to destroy() the socket wins. Valid requests return without\n // side-effect so chii's own upgrade handler takes over normally.\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.\n if (verifyAuth) {\n httpServer.on('upgrade', (req: IncomingMessage, socket: Duplex) => {\n if (!verifyAuth(req)) {\n // Reject: send a minimal HTTP 401 response and close the socket.\n // We do NOT log req.url or any auth param here to avoid leaking codes.\n socket.write('HTTP/1.1 401 Unauthorized\\r\\nContent-Length: 0\\r\\n\\r\\n');\n socket.destroy();\n // Early return — chii's handler is NOT called for this socket.\n return;\n }\n // Auth passed: no-op. Chii's upgrade listener (registered below by\n // chii.start) will handle the rest.\n });\n }\n\n const chii = loadChiiServer();\n // Passing an existing `server` makes chii attach its Koa handler + WS upgrade\n // to our HTTP server rather than creating its own listener.\n // Note: port/domain here are display-only inside chii — the TCP bind is ours.\n await chii.start({ server: httpServer, domain: `${host}:${requestedPort}`, port: requestedPort });\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 return {\n port: actualPort,\n baseUrl: `http://${host}:${actualPort}`,\n close: () =>\n new Promise<void>((resolve) => {\n httpServer.close(() => resolve());\n }),\n };\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. The\n * Chrome DevTools frontend can connect to any such endpoint via:\n *\n * https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html\n * ?wss=<host>[/<path>]\n * &panel=console\n *\n * Where `<host>` is the public WSS relay URL without the `wss://` scheme prefix\n * (the DevTools frontend adds it). This module assembles that URL and opens it\n * in the OS default browser so the developer immediately gets a full Chrome\n * DevTools UI.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chrome DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Base URL for the Chrome DevTools inspector hosted on appspot.\n *\n * The `@` path segment is the \"latest / bleeding edge\" alias which tracks the\n * current Chrome stable CDP protocol version — compatible with the chobitsu-\n * based CDP that Chii injects. A specific commit hash may be pinned here if\n * a regression is observed.\n */\nconst DEVTOOLS_FRONTEND_BASE =\n 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html';\n\n// ---------------------------------------------------------------------------\n// URL assembly\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chrome DevTools inspector URL that connects to a Chii relay\n * WebSocket.\n *\n * The `wss=` parameter expects a host-and-path string without the `wss://`\n * scheme prefix — the DevTools frontend prepends it automatically.\n *\n * @param wssRelayUrl - Full `wss://` URL of the Chii relay (public tunnel).\n * Example: `wss://abc.trycloudflare.com`\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @example\n * buildChromeDevtoolsUrl('wss://abc.trycloudflare.com')\n * // → 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html?wss=abc.trycloudflare.com&panel=console'\n */\nexport function buildChromeDevtoolsUrl(\n wssRelayUrl: string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string {\n // Strip `wss://` prefix — the DevTools frontend expects host[/path] only.\n const wssParam = wssRelayUrl.replace(/^wss:\\/\\//i, '');\n const params = new URLSearchParams({ wss: wssParam, panel });\n return `${DEVTOOLS_FRONTEND_BASE}?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled** via the `AIT_AUTO_DEVTOOLS`\n * env var. Only the explicit `\"0\"` value disables it; anything else (including\n * absent) leaves auto-open enabled.\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS === '0';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Manages auto-opening Chrome DevTools exactly once per relay attach session.\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onFirstAttach` callback to `startAttachWatcher`.\n *\n * The open fires at most once. Subsequent `open()` calls are no-ops.\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n private _opened = false;\n\n /**\n * Attempts to auto-open Chrome DevTools.\n *\n * No-op when any of the following conditions hold:\n * 1. Already opened this session (`_opened` is true).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. Environment is `mock` (env 1 — F12 is already available).\n * 4. `wssRelayUrl` is null/undefined/empty (tunnel not yet up).\n *\n * Always writes the DevTools URL to stderr so the developer can copy it\n * if the browser open fails or the popup is blocked.\n *\n * @param wssRelayUrl - The public `wss://` relay URL (from tunnel status).\n * @param env - Current MCP environment (`mock` | `relay`).\n */\n open(wssRelayUrl: string | null | undefined, env: McpEnvironment): void {\n if (this._opened) return;\n if (isAutoDevtoolsDisabled()) return;\n if (env === 'mock') return;\n if (!wssRelayUrl) return;\n\n this._opened = true;\n\n const devtoolsUrl = buildChromeDevtoolsUrl(wssRelayUrl);\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — Chrome DevTools를 자동으로 엽니다.\\n' +\n `[ait-debug] Chrome DevTools URL: ${devtoolsUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)\\n',\n );\n\n const opened = openUrlInBrowser(devtoolsUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /** Returns `true` if `open()` has passed all guards and fired once. */\n get opened(): boolean {\n return this._opened;\n }\n}\n","/**\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',\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';\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 detection — single source of truth for `mock` vs `relay-dev`\n * vs `relay-live`.\n *\n * RFC #277 (\"MCP tool surface fidelity\") asks us to decide *once* per process\n * whether the agent is operating against:\n * - `mock` — a local dev browser running the @ait-co/devtools mock SDK\n * (env 1 in the 4-environments fidelity ladder), or\n * - `relay-dev` — a real device WebView attached through the Chii relay +\n * cloudflared quick tunnel, dogfood bundle (env 3), or\n * - `relay-live` — a live/production WebView attached through the relay\n * (env 4 in the ladder, read-only debugging).\n *\n * The env decides two things:\n *\n * 1. Which tools appear on `tools/list` (Tier A → mock-only, Tier B → relay-only,\n * Tier C → both). Tier filtering happens in `tools.ts` registry and the\n * `CallTool` handler in `debug-server.ts` / `server.ts`.\n * 2. Which code path `measure_safe_area` and other Tier C tools take when they\n * need to attach a `source: 'mock' | 'relay-dev' | 'relay-live'` provenance\n * label to results.\n *\n * Detection precedence (highest → lowest):\n * 1. `MCP_ENV=mock|relay-dev|relay-live|relay` — explicit env var, always\n * wins. `relay` is a backward-compat alias for `relay-dev`.\n * 2. CDP target URL pattern — when a target URL matches a\n * known real-device WebView pattern (intoss-private:// scheme,\n * *.trycloudflare.com host) it is `relay-dev` (conservative — LIVE\n * requires explicit MCP_ENV=relay-live opt-in).\n * 3. caller-stated default — `defaultEnv` from the\n * input. The CLI entry point passes the mode's intent: debug-mode relay\n * target passes `'relay-dev'` so the default reflects \"user just launched\n * a relay debug session\", which is the dominant case. Local debug + dev\n * mode + tests with no input fall back to `'mock'`.\n * 4. baked-in default — `mock` (zero external\n * side effect).\n *\n * The `defaultEnv` precedence step (3) resolves the M2-5 dead-lock (issue\n * #309): without it, a fresh debug-mode session with no `MCP_ENV` and no\n * attached target resolved to `mock` and Tier B `build_attach_url` was hidden\n * from `tools/list` — leaving the agent with no way to enter env 3/4. By\n * letting the CLI pass `defaultEnv: 'relay-dev'` for the relay-target debug\n * mode, the bootstrap tool surface advertises `build_attach_url` from the\n * first `tools/list` call without forcing the user to set `MCP_ENV` explicitly.\n * LIVE-side guard still requires explicit `MCP_ENV=relay-live` opt-in.\n *\n * The env decision is intentionally *sticky* per process. Switching env should\n * be a process restart, not a runtime toggle — the RFC's reasoning is that mid-\n * session env flips silently invalidate everything an agent has learned.\n *\n * LIVE side-effect guard: when env is `relay-live`, the `call_sdk` and\n * `evaluate` tools require an explicit `confirm: true` argument. Without it,\n * the tool handler returns a structured error explaining the requirement. This\n * prevents accidental side effects on real users in the live production WebView.\n *\n * Backward compatibility:\n * - `MCP_ENV=relay` still works (resolves to `relay-dev`).\n * - Tools that accepted `McpEnvironment` of `'relay'` now work with\n * `isRelayEnv(env)` which returns true for both `relay-dev` and\n * `relay-live`.\n * - `get_diagnostics` `environment` field keeps the legacy `env` key\n * (`'mock' | 'relay'`) alongside the new `kind` key.\n *\n * SECRET-HANDLING: this module never reads the TOTP secret, deploy key, or any\n * URL component other than the scheme/host. The pattern matching uses public\n * surface only (intoss-private://… authority, *.trycloudflare.com host suffix).\n */\n\nimport type { CdpConnection } from './cdp-connection.js';\n\n/**\n * The three environments the MCP server can operate in (issue #307).\n *\n * - `mock` — local dev browser + mock SDK (env 1).\n * - `relay-dev` — real-device dogfood bundle relay (env 3).\n * - `relay-live` — real-device live/production relay, read-only guard active\n * (env 4).\n *\n * Backward-compat: the old `'relay'` value is no longer in the union type;\n * callers that need \"any relay\" should use the `isRelayEnv()` helper.\n */\nexport type McpEnvironment = 'mock' | 'relay-dev' | 'relay-live';\n\n/**\n * Legacy environment union that includes the deprecated `'relay'` alias.\n * Used only for `MCP_ENV` env-var parsing and backward-compat `env` field in\n * `get_diagnostics`. New code should use `McpEnvironment`.\n */\ntype LegacyMcpEnvVar = McpEnvironment | 'relay';\n\n/**\n * Returns `true` when the environment is any relay variant (`relay-dev` or\n * `relay-live`). Use this instead of `env === 'relay'` for tier checks.\n */\nexport function isRelayEnv(env: McpEnvironment): boolean {\n return env === 'relay-dev' || env === 'relay-live';\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.\n */\nexport function isLiveRelayEnv(env: McpEnvironment): boolean {\n return env === 'relay-live';\n}\n\n/**\n * Maps the new `McpEnvironment` union to the legacy two-value union\n * (`'mock' | 'relay'`) for backward-compatible fields in diagnostics output.\n */\nexport function toLegacyEnv(env: McpEnvironment): 'mock' | 'relay' {\n if (env === 'mock') return 'mock';\n return 'relay';\n}\n\n/**\n * Why a given environment was chosen. Stable strings suitable for stderr logs\n * and the `data.reason` field on rejection errors. Does NOT include any URL,\n * secret, or other potentially-sensitive value.\n */\nexport type EnvironmentReason =\n | 'env-var-mock'\n | 'env-var-relay-dev'\n | 'env-var-relay-live'\n | 'env-var-relay-compat'\n | 'cdp-target-url-relay-pattern'\n | 'default-mock'\n | 'default-relay-dev'\n | 'default-relay-live';\n\n/**\n * URL patterns that mark a CDP target as a real-device WebView relay.\n *\n * - `intoss-private://` is the Toss in-app private scheme — only ever observed\n * inside the real Toss app WebView.\n * - `*.trycloudflare.com` (host suffix) is the cloudflared quick tunnel used as\n * the relay transport. A target whose URL is on that host is, by construction,\n * reached over the relay.\n *\n * Pattern-only matches — no specific tunnel host or deploymentId is hard-coded.\n */\nconst RELAY_URL_PATTERNS: ReadonlyArray<RegExp> = [\n /^intoss-private:\\/\\//i,\n /:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(\\/|$|:|\\?)/i,\n];\n\n/**\n * Returns true when the URL string looks like a real-device WebView attached\n * over the Chii relay. Used for `getEnvironment()` precedence step 2.\n */\nexport function isRelayUrl(url: string): boolean {\n if (typeof url !== 'string' || url.length === 0) return false;\n return RELAY_URL_PATTERNS.some((p) => p.test(url));\n}\n\n/**\n * Test/override hook — when non-null, `getEnvironment()` returns this value\n * regardless of env vars or connection state. Cleared with `null`.\n */\nlet envOverride: McpEnvironment | null = null;\n\n/**\n * Sets a sticky environment override. Intended for tests; production code paths\n * should leave the override `null` and let the precedence rules decide.\n */\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/**\n * Parses the `MCP_ENV` env var into a `McpEnvironment` if valid.\n *\n * Accepted values:\n * - `mock` → `mock`\n * - `relay-dev` → `relay-dev`\n * - `relay-live` → `relay-live`\n * - `relay` → `relay-dev` (backward-compat alias — resolves to relay-dev)\n *\n * Any other value is ignored and falls through to the next precedence step.\n */\nfunction readEnvVar(): McpEnvironment | undefined {\n const raw = process.env.MCP_ENV as LegacyMcpEnvVar | string | undefined;\n if (raw === 'mock') return 'mock';\n if (raw === 'relay-dev') return 'relay-dev';\n if (raw === 'relay-live') return 'relay-live';\n if (raw === 'relay') return 'relay-dev'; // backward-compat alias\n return undefined;\n}\n\n/**\n * Decision input for `getEnvironment` / `getEnvironmentReason`. The connection\n * is optional — when omitted, only the env var and default are consulted.\n *\n * Production callers pass the live `CdpConnection` so the URL-pattern step\n * (precedence 2) can fire. Tests can omit it to exercise pure precedence.\n */\nexport interface EnvironmentInput {\n /**\n * Live CDP connection — when its `listTargets()` includes a URL matching the\n * real-device pattern, the env resolves to `relay`. Optional.\n */\n connection?: Pick<CdpConnection, 'listTargets'>;\n /**\n * Caller-stated default when no env var is set and no URL pattern matches.\n * The CLI entry point uses this to encode each mode's *intent* (debug-mode\n * relay target = `'relay'`, local/dev = `'mock'`) without baking the mode\n * into this module. Defaults to `'mock'` (backwards-compatible — tests and\n * legacy callers see the original behaviour).\n *\n * This is precedence step 3 (caller-stated default) — it only kicks in after\n * `MCP_ENV` and the URL pattern have been consulted, so an explicit env var\n * or a real-device URL still wins.\n */\n defaultEnv?: McpEnvironment;\n}\n\n/**\n * Returns the current MCP environment, applying the precedence rules:\n * 1. test override (if set)\n * 2. `MCP_ENV` env var\n * 3. CDP target URL pattern match → `relay-dev` (conservative — LIVE\n * requires explicit MCP_ENV=relay-live opt-in)\n * 4. caller-stated `defaultEnv` (intent hint from the CLI mode)\n * 5. baked-in default `mock`\n */\nexport function getEnvironment(input: EnvironmentInput = {}): McpEnvironment {\n if (envOverride !== null) return envOverride;\n const fromEnv = readEnvVar();\n if (fromEnv !== undefined) return fromEnv;\n const { connection, defaultEnv } = input;\n if (connection !== undefined) {\n const targets = connection.listTargets();\n for (const t of targets) {\n if (isRelayUrl(t.url)) return 'relay-dev';\n }\n }\n return defaultEnv ?? 'mock';\n}\n\n/**\n * Returns the `EnvironmentReason` that drove the current `getEnvironment()`\n * result. Used by stderr logs and the rejection-reason payload on Tier A/B\n * mismatch errors. SECRET-HANDLING: only stable enum strings — no URL or\n * secret value is ever returned.\n */\nexport function getEnvironmentReason(input: EnvironmentInput = {}): EnvironmentReason {\n if (envOverride !== null) {\n if (envOverride === 'mock') return 'env-var-mock';\n if (envOverride === 'relay-live') return 'env-var-relay-live';\n return 'env-var-relay-dev';\n }\n const rawVar = process.env.MCP_ENV;\n const fromEnv = readEnvVar();\n if (fromEnv === 'mock') return 'env-var-mock';\n if (fromEnv === 'relay-live') return 'env-var-relay-live';\n if (fromEnv === 'relay-dev') {\n // Distinguish explicit `relay-dev` from backward-compat `relay` alias.\n return rawVar === 'relay' ? 'env-var-relay-compat' : 'env-var-relay-dev';\n }\n const { connection, defaultEnv } = input;\n if (connection !== undefined) {\n const targets = connection.listTargets();\n for (const t of targets) {\n if (isRelayUrl(t.url)) return 'cdp-target-url-relay-pattern';\n }\n }\n if (defaultEnv === 'relay-live') return 'default-relay-live';\n if (defaultEnv === 'relay-dev') return 'default-relay-dev';\n return 'default-mock';\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 미주입 — dogfood 채널로 재배포\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 - 환경이 결정된 근거 (EnvironmentReason 문자열).\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 'dogfood 번들 배포 후 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이 주입되지 않았다 (dogfood 빌드가 아님).\n *\n * call_sdk 호출 시 브리지가 없을 때.\n */\nexport function sdkAbsentError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}window.__sdkCall이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). ` +\n 'dogfood 채널(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. dogfood 빌드(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): 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 부재\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);\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 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","/**\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 * SECRET-HANDLING:\n * - 127.0.0.1 바인딩만 — 외부 노출 0.\n * - attachUrl은 HTML 본문과 /qr.png query에만 들어간다 (의도된 전달 경로).\n * - stdout/stderr/로그에 별도 출력하지 않는다.\n * - tmp 파일 만들지 않음 — 모든 응답을 메모리에서 생성.\n */\n\nimport type { Server } from 'node:http';\nimport { createServer } from 'node:http';\n\nexport interface QrHttpServer {\n port: number;\n /** `http://127.0.0.1:<port>/attach?u=<encoded>` URL 생성 헬퍼. */\n buildAttachPageUrl(attachUrl: string): string;\n close(): Promise<void>;\n}\n\n/**\n * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.\n * MCP debug server 생애주기에 묶어 사용 — `runDebugServer` shutdown 시 `close()`로 정리.\n */\nexport async function startQrHttpServer(): Promise<QrHttpServer> {\n const { default: QRCode } = await import('qrcode');\n\n const server: Server = createServer((req, res) => {\n const rawUrl = req.url ?? '/';\n const [path, query = ''] = rawUrl.split('?', 2) as [string, string | undefined];\n const params = new URLSearchParams(query ?? '');\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 // QR을 base64 data URL로 인라인 생성 — 외부 fetch 없이 self-contained HTML.\n QRCode.toDataURL(attachUrl, { type: 'image/png', errorCorrectionLevel: 'M' })\n .then((dataUrl: string) => {\n const safeLabel = deploymentIdLabel.replace(/[<>&\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n const safeAttachUrl = attachUrl.replace(/[<>&\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n const html = buildAttachHtml(dataUrl, safeLabel, safeAttachUrl);\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 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 return {\n port,\n buildAttachPageUrl(attachUrl: string): string {\n return `http://127.0.0.1:${port}/attach?u=${encodeURIComponent(attachUrl)}`;\n },\n close(): Promise<void> {\n return new Promise((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n },\n };\n}\n\n/**\n * QR 스캔 페이지 HTML 본문.\n * dark theme, inline style, 외부 fetch 없음.\n */\nfunction buildAttachHtml(qrDataUrl: string, safeLabel: string, safeAttachUrl: string): string {\n return `<!DOCTYPE html>\n<html lang=\"ko\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>AIT 디버그 세션 — QR 스캔</title>\n <style>\n *, *::before, *::after { box-sizing: border-box; }\n body {\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 }\n h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\n img.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n }\n section { width: 100%; max-width: 480px; }\n h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\n ol, ul { margin: 0; padding-left: 1.25rem; }\n li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\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 border-radius: 6px; border: 1px solid #30363d;\n }\n hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n </style>\n</head>\n<body>\n <h1>AIT 디버그 세션 — QR 스캔</h1>\n <p class=\"label\">deployment: ${safeLabel}</p>\n <img class=\"qr\" src=\"${qrDataUrl}\" alt=\"attach QR\" />\n\n <section>\n <h2>스캔 절차</h2>\n <ol>\n <li>토스 앱을 실행하세요.</li>\n <li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li>\n <li>팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.</li>\n <li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li>\n </ol>\n </section>\n\n <hr />\n\n <section>\n <h2>진단 체크리스트</h2>\n <ul>\n <li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li>\n <li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li>\n <li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li>\n <li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li>\n </ul>\n </section>\n\n <hr />\n\n <section>\n <h2>URL (fallback)</h2>\n <p class=\"url-box\">${safeAttachUrl}</p>\n </section>\n</body>\n</html>`;\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';\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\nexport interface LockHandle {\n /** Updates the wssUrl field in the lock file once the tunnel URL is known. */\n updateWssUrl(wssUrl: string): 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 * 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// 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 return {\n pid: p.pid as number,\n wssUrl: typeof p.wssUrl === 'string' ? p.wssUrl : null,\n startedAt: p.startedAt as string,\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_diagnostics` 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 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 release(): void {\n if (released) return;\n released = true;\n removeLock(lockPath);\n },\n };\n}\n","/**\n * Build a self-attaching dogfood deep link.\n *\n * `ait deploy --scheme-only` prints an `intoss-private://…?_deploymentId=<uuid>`\n * URL that opens a dogfood 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 * 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 * RFC 6238 TOTP implementation (Node.js, node:crypto only).\n *\n * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used\n * to keep the dependency surface minimal. This hand-roll is ~30 lines and\n * covers exactly what relay-side auth needs.\n *\n * Algorithm summary (RFC 6238 + RFC 4226):\n * T = floor(now / 30) — 30-second time step counter\n * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)\n * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)\n * offset = MAC[19] & 0x0f\n * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits\n *\n * Security note (keep this comment accurate):\n * The baked-in secret in a dogfood build is extractable from the bundle by a\n * determined reverse engineer. This mechanism raises the bar from\n * \"anyone with the URL\" to \"URL + bundle extraction + live TOTP calculation\".\n * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are\n * blocked; deliberate reverse engineering is not. See threat model in\n * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.\n *\n * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any\n * log, error message, or string visible outside this module. Only boolean\n * pass/fail and reason enum values are safe to surface.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Time step window in seconds (RFC 6238 default). */\nconst TIME_STEP = 30;\n\n/** Number of digits in the generated code. */\nconst DIGITS = 6;\n\n/**\n * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-\n * clock time.\n *\n * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32\n * bytes). Must be the output of `generateAttachToken()` or compatible.\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @returns A zero-padded 6-digit decimal string, e.g. `\"042193\"`.\n */\nexport function generateTotp(secret: string, when: number = Date.now()): string {\n const key = Buffer.from(secret, 'hex');\n // Clamp to 0 so negative timestamps (e.g. in ±skew checks near epoch) do not\n // produce a negative counter, which would cause writeUInt32BE to throw.\n const counter = Math.max(0, Math.floor(when / 1000 / TIME_STEP));\n\n // Encode counter as 8-byte big-endian unsigned integer.\n const counterBuf = Buffer.alloc(8);\n // JavaScript numbers are safe integers up to 2^53; counter is ~7.5×10^10 at\n // year 9999 — well within safe range so standard bitwise ops are fine.\n const hi = Math.floor(counter / 0x100000000);\n const lo = counter >>> 0;\n counterBuf.writeUInt32BE(hi, 0);\n counterBuf.writeUInt32BE(lo, 4);\n\n const mac = createHmac('sha1', key).update(counterBuf).digest();\n\n // Dynamic truncation (RFC 4226 §5.4).\n const offset = mac[19] & 0x0f;\n const binCode =\n ((mac[offset] & 0x7f) << 24) |\n ((mac[offset + 1] & 0xff) << 16) |\n ((mac[offset + 2] & 0xff) << 8) |\n (mac[offset + 3] & 0xff);\n\n const otp = binCode % 10 ** DIGITS;\n return otp.toString().padStart(DIGITS, '0');\n}\n\n/**\n * Verifies a TOTP code against the secret, accepting ±`skew` time steps to\n * tolerate clock drift between the relay host and the client device.\n *\n * Uses `timingSafeEqual` for constant-time comparison to prevent timing\n * side-channel attacks.\n *\n * @param secret - Hex-encoded shared secret.\n * @param code - The 6-digit code to verify (string or numeric).\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @param skew - Number of adjacent steps to accept on either side. Default 1\n * (accepts T-1, T, T+1 — a 90-second acceptance window).\n * @returns `true` if the code matches any accepted step, `false` otherwise.\n */\nexport function verifyTotp(\n secret: string,\n code: string,\n when: number = Date.now(),\n skew: number = 1,\n): boolean {\n const normalised = String(code).padStart(DIGITS, '0');\n if (normalised.length !== DIGITS || !/^\\d{6}$/.test(normalised)) {\n return false;\n }\n\n const candidateBuf = Buffer.from(normalised, 'utf8');\n\n for (let delta = -skew; delta <= skew; delta++) {\n const stepWhen = when + delta * TIME_STEP * 1000;\n const expected = generateTotp(secret, stepWhen);\n const expectedBuf = Buffer.from(expected, 'utf8');\n if (timingSafeEqual(expectedBuf, candidateBuf)) {\n return true;\n }\n }\n\n return false;\n}\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 { generateTotp } 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 'Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a ' +\n 'self-attaching deep link by splicing in debug=1 and the live relay URL for this session. ' +\n 'Returns the deep link JSON and a unicode QR of that deep link. Scan the QR with the phone ' +\n 'camera to open the mini-app and attach it to this debug session (QR is the single entry ' +\n 'path — no USB cable or platform CLI needed). Requires the tunnel to be up — call ' +\n 'list_pages first. If the tunnel is not up, restart the MCP server: ' +\n '`npx @ait-co/devtools devtools-mcp`. ' +\n 'Set wait_for_attach=true to block until the phone scans and a page attaches ' +\n '(polls listTargets up to 30 s by default), then returns the attached page info too. ' +\n 'On timeout, call build_attach_url again to resume polling. ' +\n 'When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default ' +\n 'browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). ' +\n 'Requires MCP_ENV=relay-dev or relay-live (set automatically in debug-mode default).\\n\\n' +\n 'TOTP 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 URL is single-use for ' +\n 'that 30-second step. The response includes a `totp` field with `expiresAt` (ISO timestamp). ' +\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 one-time URL. ' +\n 'Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.',\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 '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, timeout 30 s). On attach, the response includes the ' +\n 'attached page list. On timeout, call build_attach_url again to resume polling.',\n },\n open_in_browser: {\n type: 'boolean',\n description:\n 'If true (default), render the QR as a PNG and open it in the OS default browser. ' +\n 'Only works when the MCP server is running on a local GUI machine — headless or ' +\n 'remote container environments should set this to false to use the text QR fallback.',\n },\n },\n required: ['scheme_url'],\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\"` field so consumers can identify ' +\n 'provenance without inspecting payload values. ' +\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 dogfood 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 2/3 (real device relay) this hits the real SDK; on env 1 (local mock) it hits ' +\n 'the mock SDK. 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 (non-dogfood bundle) — ' +\n 'redeploy via dogfood channel: `ait build && aitcc app deploy`.\\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: 'get_diagnostics',\n description:\n 'Returns a single-call server status snapshot so the agent can diagnose \"why is this not ' +\n 'working?\" without calling multiple tools. 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 'environment (kind: mock|relay-dev|relay-live, env: mock|relay backward-compat, reason, ' +\n 'liveGuardActive: true when relay-live LIVE guard is active), ' +\n 'serverLockHolder (pid + startedAt from the lock file, or null), ' +\n 'nextRecommendedAction ({tool, reason} or null — the single next tool to call). ' +\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). Call this first when debugging session state.',\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`) both satisfy the `'relay'`\n * 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`) both satisfy the `'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_diagnostics',\n 'list_pages',\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 dogfood 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 one\n * 30-second time step (±1 skew accepted by the relay, so the effective window\n * is up to 90 s). If the scan happens after `totp.expiresAt`, call\n * `build_attach_url` again to get a fresh code.\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 // Current step number (floor). The step expires at the start of the NEXT step.\n const currentStep = Math.floor(now / 1000 / STEP_SECONDS);\n const expiresAtMs = (currentStep + 1) * STEP_SECONDS * 1000;\n totpMeta = {\n enabled: true,\n ttlSeconds: 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 로컬 전용이며 at= 코드 값을 직접 담지 않는다\n * (attachUrl은 /attach?u= query로 전달되어 서버 메모리에서만 처리).\n */\nexport interface OpenQrInBrowserResult {\n /** `true` if the browser was successfully opened. */\n opened: boolean;\n /** `http://127.0.0.1:<port>/attach?u=...` — 브라우저에 전달된 URL. */\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>/attach?u=...`)을 OS 기본 브라우저로 연다.\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/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>/attach?u=<encoded>` HTTP URL.\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()` — dogfood bundle on real device.\n * b. `window.__sdk.getSafeAreaInsets()` — dogfood 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 dogfood 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, dogfood build\n * - `'relay-live'` — real-device WebView, live/production build\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, dogfood 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-dogfood 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이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). dogfood 채널로 재배포하세요.'});` +\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 dogfood SDK method via `window.__sdkCall` on the attached page.\n * NOT read-only — SDK calls may have side effects.\n *\n * On env 2/3 (real device relay) this hits the real SDK; on env 1 (local\n * mock) 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_diagnostics — 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\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 * 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_diagnostics`.\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 */\n recentErrors: DiagnosticsError[];\n /**\n * Resolved environment and the reason string.\n *\n * `kind` — the precise three-value environment (`mock` | `relay-dev` |\n * `relay-live`). 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 * 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 * 1. tunnel.up === false → restart\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_diagnostics`. */\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\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\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\n/**\n * Reads the `@modelcontextprotocol/sdk` package version from the installed\n * package's `package.json`. Returns `null` on any error (missing file, JSON\n * parse failure, etc.) — diagnostics must never throw.\n *\n * Node-only — uses dynamic `import()` so it does not pollute the browser\n * module graph.\n */\nexport async function readMcpSdkVersion(): Promise<string | null> {\n try {\n // Resolve the package.json adjacent to the installed SDK entry point.\n const { createRequire } = await import('node:module');\n const req = createRequire(import.meta.url);\n const pkgPath = req.resolve('@modelcontextprotocol/sdk/package.json');\n const { readFileSync } = await import('node:fs');\n const raw = readFileSync(pkgPath, '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 try {\n // `__VERSION__` is injected by tsdown / vite via `define`.\n // biome-ignore lint/suspicious/noExplicitAny: intentional global check\n const v = (globalThis as any).__VERSION__;\n return typeof v === 'string' && v.length > 0 ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Derives the next recommended action from a completed diagnostics snapshot.\n *\n * Branch rules (evaluated in priority order):\n * 1. tunnel.up === false → restart\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 */\nexport function computeNextRecommendedAction(\n tunnel: DiagnosticsTunnelInfo,\n pages: ListPagesResult | null,\n env: McpEnvironment,\n): NextRecommendedAction | null {\n // Rule 1: tunnel is down — must restart the MCP server.\n if (!tunnel.up) {\n return {\n tool: 'restart',\n reason: 'tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart',\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`). Caller\n * 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\n/**\n * Builds the `get_diagnostics` 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 } = 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 const tunnelInfo: DiagnosticsTunnelInfo = {\n up: tunnel.up,\n wssUrl: tunnel.wssUrl,\n pid: lockData?.pid ?? null,\n startedAt: lockData?.startedAt ?? null,\n };\n\n // list_pages — non-fatal; null on any error.\n let pages: ListPagesResult | null = null;\n if (connection !== undefined) {\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 const nextRecommendedAction = computeNextRecommendedAction(tunnelInfo, pages, env);\n\n return {\n mcpVersion,\n devtoolsVersion,\n tunnel: tunnelInfo,\n pages,\n lastAttachAt: collector.getLastAttachAt(),\n lastDetachAt: collector.getLastDetachAt(),\n recentErrors,\n environment: {\n kind: env,\n env: toLegacyEnv(env),\n reason: envReason,\n liveGuardActive: isLiveRelayEnv(env),\n },\n serverLockHolder,\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 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 */\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 return {\n url,\n wssUrl: url.replace(/^https/, 'wss'),\n stop: () => {\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 dogfood 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 * @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 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 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 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 }, 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 { 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 { AutoDevtoolsOpener } from './devtools-opener.js';\nimport { wrapEnvelope } from './envelope.js';\nimport {\n getEnvironment,\n getEnvironmentReason,\n isLiveRelayEnv,\n type McpEnvironment,\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 { type QrHttpServer, startQrHttpServer } from './qr-http-server.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 { verifyTotp } from './totp.js';\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/** Live infra the connection reads tunnel status from. */\nexport interface DebugServerDeps {\n connection: CdpConnection;\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 90 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`) per RFC #277.\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 to a function that asks `getEnvironment(input)` with\n * the live connection. Tests inject a fake to pin the env without touching\n * `setEnvironmentOverride` (which is process-global).\n */\n getEnvironment?: () => McpEnvironment;\n /** Resolves the reason for the current env decision (for logs). */\n getEnvironmentReason?: () => string;\n /**\n * Caller-stated default environment when no `MCP_ENV` is set and no URL\n * pattern matches. The CLI passes `'relay'` for the relay-target debug mode\n * (so bootstrap `tools/list` advertises Tier B `build_attach_url` on the\n * very first call — resolving the M2-5 dead-lock, issue #309) and `'mock'`\n * for the local-target debug mode (no relay tunnel exists). Tests omit this\n * to preserve the historical `'mock'` default.\n *\n * Ignored when `getEnvironment`/`getEnvironmentReason` are explicitly\n * provided — fake env resolvers fully control the env decision.\n */\n defaultEnv?: McpEnvironment;\n /**\n * Diagnostics collector — records server-side errors, attach/detach events,\n * and surfaces them via `get_diagnostics`. 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 * Production: passed from `process.env.AIT_DEBUG_TOTP_SECRET` by\n * `runDebugServer`. Tests inject a dummy hex string or omit it.\n */\n totpSecret?: string;\n}\n\n/**\n * Waits for the first target matching `filterFn` to attach, using the\n * event-driven `waitForFirstTarget()` on `ChiiCdpConnection` instances, or\n * falling back to a polling loop for generic `CdpConnection` fakes (tests).\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 non-ChiiCdpConnection.\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 (ChiiCdpConnection production instances).\n if (connection instanceof ChiiCdpConnection) {\n return connection.waitForFirstTarget(filterFn, timeoutMs, pollIntervalMs);\n }\n // Generic fallback for test fakes that implement CdpConnection but not\n // waitForFirstTarget (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 aitSource,\n getTunnelStatus,\n waitForAttachTimeoutMs = 90_000,\n qrHttpServer,\n getEnvironment: getEnvDep,\n getEnvironmentReason: getEnvReasonDep,\n diagnosticsCollector: collectorDep,\n defaultEnv,\n totpSecret,\n } = deps;\n\n // Env SSoT — production wires the real `getEnvironment` with the connection\n // plus the caller-stated `defaultEnv` (mode intent); tests inject fakes.\n // Lazy so each request reflects the live connection.\n const resolveEnvironment: () => McpEnvironment =\n getEnvDep ?? (() => getEnvironment({ connection, defaultEnv }));\n const resolveEnvironmentReason: () => string =\n getEnvReasonDep ?? (() => getEnvironmentReason({ connection, defaultEnv }));\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 const env = resolveEnvironment();\n const attached = connection.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 // 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 const env = resolveEnvironment();\n if (!isToolAvailableIn(name, env)) {\n const requiredEnv = getToolAvailability(name) ?? 'unknown';\n const envReason = resolveEnvironmentReason();\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 connection.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_diagnostics 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_diagnostics') {\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,\n env: resolveEnvironment(),\n envReason: resolveEnvironmentReason(),\n collector,\n readLock: readServerLock,\n recentErrorsLimit,\n });\n const attached = connection.listTargets().length > 0;\n return envelopeResult(result, name, resolveEnvironment(), attached);\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n // build_attach_url is pure synthesis (scheme URL + relay URL → deep link).\n // It works before any page attaches, so it must not require enableDomains.\n if (name === 'build_attach_url') {\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 );\n }\n const waitForAttach = request.params.arguments?.wait_for_attach === true;\n // open_in_browser defaults to true when not explicitly set.\n const openInBrowser = request.params.arguments?.open_in_browser !== false;\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 try {\n // SECRET-HANDLING: totpSecret is passed to buildAttachUrl only; it is\n // never logged or included in output other than the at= param in attachUrl.\n const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(\n schemeUrl,\n getTunnelStatus(),\n totpSecret,\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 환경 감지: open_in_browser=true인데 GUI가 없는 경우 안내 후 text QR fallback.\n if (openInBrowser && !guiAvailable) {\n const headlessNote =\n '[open_in_browser] GUI 환경이 감지되지 않았습니다 (headless/remote 환경). ' +\n 'open_in_browser=false로 자동 폴백합니다. ' +\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(\n connection,\n isMatchingPage,\n waitForAttachTimeoutMs,\n );\n } catch {\n attachedPagesHl = connection.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(\n headlessText,\n waitForAttachTimeoutMs / 1000,\n attachedPagesHl,\n ),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResultHl = listPages(connection, 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 when requested, a GUI is available, and the HTTP server is up.\n if (openInBrowser && 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(\n connection,\n isMatchingPage,\n waitForAttachTimeoutMs,\n );\n } catch {\n attachedPages = connection.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(\n shortText,\n waitForAttachTimeoutMs / 1000,\n attachedPages,\n ),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResult = listPages(connection, 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 `[open_in_browser] 브라우저 자동 열기에 실패했습니다. ` +\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(\n connection,\n isMatchingPage,\n waitForAttachTimeoutMs,\n );\n } catch {\n attachedPagesFb = connection.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1000, attachedPagesFb),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResultFb = listPages(connection, 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 // open_in_browser=false or 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(\n connection,\n isMatchingPage,\n waitForAttachTimeoutMs,\n );\n } catch {\n attachedPages = connection.listTargets();\n return {\n content: [\n {\n type: 'text' as const,\n text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1000, attachedPages),\n },\n ],\n isError: true,\n };\n }\n\n const pagesResult = listPages(connection, 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 connection.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 if (connection instanceof ChiiCdpConnection) {\n try {\n await connection.refreshTargets();\n } catch {\n // Ignore refresh errors — still return cached state.\n }\n }\n const pagesData = listPages(connection, getTunnelStatus());\n const attached = connection.listTargets().length > 0;\n return envelopeResult(pagesData, name, resolveEnvironment(), 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(connection));\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(connection, limit) });\n }\n case 'list_network_requests':\n return jsonResult(listNetworkRequests(connection));\n case 'list_pages': {\n // Refresh from relay so evict→reattach transitions are not served stale.\n if (connection instanceof ChiiCdpConnection) {\n try {\n await connection.refreshTargets();\n } catch {\n // Ignore refresh errors — still return cached state.\n }\n }\n const listPagesData = listPages(connection, getTunnelStatus());\n const listPagesAttached = connection.listTargets().length > 0;\n return envelopeResult(listPagesData, name, resolveEnvironment(), listPagesAttached);\n }\n case 'get_dom_document':\n return jsonResult(await getDomDocument(connection));\n case 'take_snapshot':\n return jsonResult(await takeSnapshot(connection));\n case 'take_screenshot': {\n const shot = await takeScreenshot(connection);\n return {\n content: [{ type: 'image' as const, data: shot.data, mimeType: shot.mimeType }],\n };\n }\n case 'measure_safe_area': {\n // Pass env to attach `source: 'mock' | 'relay'` to the result (Tier C\n // parity per RFC #277 — the same Runtime.evaluate probe runs in both\n // envs; only the provenance label differs).\n const safeAreaData = await measureSafeArea(connection, resolveEnvironment());\n const safeAreaAttached = connection.listTargets().length > 0;\n return envelopeResult(safeAreaData, name, resolveEnvironment(), 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: relay-live 환경에서 confirm: true 없이 side-effect 호출 차단.\n if (isLiveRelayEnv(env) && request.params.arguments?.confirm !== true) {\n return liveGuardError('evaluate');\n }\n // SECRET-HANDLING: do not log expression or result value.\n return jsonResult(await evaluate(connection, 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: relay-live 환경에서 confirm: true 없이 side-effect 호출 차단.\n if (isLiveRelayEnv(env) && request.params.arguments?.confirm !== true) {\n return liveGuardError('call_sdk');\n }\n // SECRET-HANDLING: do not log name, args, or result value.\n const sdkResult = await callSdk(connection, 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 return sdkAbsentError('call_sdk');\n }\n const callSdkAttached = connection.listTargets().length > 0;\n return envelopeResult(sdkResult, name, resolveEnvironment(), callSdkAttached);\n }\n default:\n return unknownTool(name);\n }\n } catch (err) {\n return errorResult(err, name);\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. 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) {\n return classifyToolError(err, name);\n}\n\n/**\n * Starts a polling watcher that detects the first 0→N target transition 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()` exactly once — on the first transition — then\n * clears itself. Shutdown calls `stop()` to clear the interval.\n *\n * `onFirstAttach` is called once on the 0→N transition (or immediately when\n * already attached). Use this to trigger side-effects such as auto-opening\n * Chrome DevTools (issue #282). The callback is optional; omitting it preserves\n * 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 onFirstAttach?: () => void,\n): { stop(): void } {\n let wasAttached = connection.listTargets().length > 0;\n // If already attached when the watcher starts, send once immediately.\n if (wasAttached) {\n void server.sendToolListChanged();\n onFirstAttach?.();\n }\n\n const handle = setInterval(() => {\n const isAttached = connection.listTargets().length > 0;\n if (!wasAttached && isAttached) {\n wasAttached = true;\n // Emit once on 0→N transition so the MCP client refreshes its tool list.\n void server.sendToolListChanged();\n onFirstAttach?.();\n clearInterval(handle);\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/**\n * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a\n * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.\n *\n * The predicate checks the `at` query parameter against the current and\n * adjacent TOTP time steps (±1 skew) using `verifyTotp`.\n *\n * Returns `undefined` when the env var is not set — callers treat that as\n * \"auth disabled\" (no predicate registered on the relay).\n *\n * SECRET-HANDLING: The secret value read from env is captured in a closure and\n * is NEVER written to any log, error message, or process output.\n */\nexport function buildRelayVerifyAuth():\n | ((req: import('node:http').IncomingMessage) => boolean)\n | undefined {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n if (!secret) return undefined;\n\n return (req) => {\n // Parse the `at` query param from the upgrade request URL.\n // req.url is the raw request path + query, e.g. `/client/id?target=…&at=123456`\n const rawUrl = req.url ?? '';\n const qIndex = rawUrl.indexOf('?');\n const queryStr = qIndex === -1 ? '' : rawUrl.slice(qIndex + 1);\n const params = new URLSearchParams(queryStr);\n const code = params.get('at') ?? '';\n\n // Do NOT log `code`, `secret`, or any derived value here.\n return verifyTotp(secret, code);\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 // 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\n // Build the TOTP verifyAuth predicate from env at startup (runtime read).\n const verifyAuth = buildRelayVerifyAuth();\n const totpEnabled = verifyAuth !== undefined;\n\n const relay = await startChiiRelay({ port: relayPort, verifyAuth });\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 // generateAttachToken is kept for legacy/non-TOTP token use, but we no\n // longer print it in the banner to avoid accidental secret exposure.\n const _token = generateAttachToken();\n\n // Health probe handle — started once the initial tunnel is up.\n let tunnelProbe: { stop(): void } | null = null;\n\n // Bring the cloudflared tunnel up in the background so the MCP stdio\n // transport can answer `initialize` immediately. cloudflared has to lazy-\n // download a ~38 MB binary on first run; awaiting it here pushes the\n // initialize response past Claude Code's MCP connection timeout. Tools that\n // need the tunnel (`build_attach_url`) already gate on `getTunnelStatus()`\n // and return a clear \"tunnel not up\" message when it isn't ready yet, so\n // dropping the await is 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 // Update the lock file with the assigned tunnel URL so a second caller\n // can see the correct wssUrl in the conflict error message.\n lockHandle.updateWssUrl(t.wssUrl);\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 lockHandle.updateWssUrl(newTunnel.wssUrl);\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 = new ChiiCdpConnection({ relayBaseUrl: relay.baseUrl });\n // AIT.* methods ride the same Chii channel as CDP commands.\n const aitSource = new ChiiAitSource(connection);\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();\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 const devtoolsOpener = new AutoDevtoolsOpener();\n\n // Diagnostics collector — records server-side errors and attach/detach events\n // so `get_diagnostics` can surface them in a single call.\n const diagnosticsCollector = new InMemoryDiagnosticsCollector();\n\n const server = createDebugServer({\n connection,\n aitSource,\n getTunnelStatus: () => tunnelStatus,\n get qrHttpServer() {\n return qrServer;\n },\n diagnosticsCollector,\n // Relay-target debug mode: the user just launched a relay debug session,\n // so the default env intent is `relay-dev`. Without this, `getEnvironment()`\n // would return `mock` until a target attaches — hiding Tier B\n // `build_attach_url` from the very first `tools/list` and leaving the\n // agent with no way to enter env 3/4 (issue #309).\n defaultEnv: 'relay-dev',\n // SECRET-HANDLING: totpSecret is read from env once at startup and passed\n // through to buildAttachUrl where it is used only to generate the at= code.\n // It is never logged or surfaced in any output.\n ...(process.env.AIT_DEBUG_TOTP_SECRET ? { totpSecret: process.env.AIT_DEBUG_TOTP_SECRET } : {}),\n });\n\n const transport = new StdioServerTransport();\n\n // ---------------------------------------------------------------------------\n // Shutdown: best-effort cleanup of relay + cloudflared child process.\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 attachWatcher: { 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 attachWatcher?.stop();\n tunnelProbe?.stop();\n connection.close();\n // tunnel.stop() is synchronous (child process kill) — safe from exit handler.\n tunnel?.stop();\n // relay.close(), server.close(), qrServer.close() are async — fine for signal handlers.\n void relay.close();\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 tunnel.stop() which is a sync child kill).\n process.on('exit', () => {\n if (!closed) {\n closed = true;\n attachWatcher?.stop();\n tunnelProbe?.stop();\n tunnel?.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 // Start the attach watcher after the transport is connected so\n // sendToolListChanged has a live session to notify.\n // The onFirstAttach callback auto-opens Chrome DevTools when a page attaches\n // over the relay (issue #282). It is a no-op in mock env and when\n // AIT_AUTO_DEVTOOLS=0. The tunnel wssUrl may still be null here when\n // cloudflared is still starting; devtoolsOpener.open() guards against that.\n attachWatcher = startAttachWatcher(connection, server, 1_000, () => {\n diagnosticsCollector.recordAttach();\n // Same defaultEnv intent as the server wiring above — keeps the env\n // resolution coherent across the surface (env 3/4 attach → relay).\n devtoolsOpener.open(\n tunnelStatus.wssUrl,\n getEnvironment({ connection, defaultEnv: 'relay-dev' }),\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 * Boots the local-browser debug stack and serves it over stdio:\n * 1. launch a local Chromium with `--remote-debugging-port=<port>`,\n * 2. attach a `LocalCdpConnection` to the first non-blank page target,\n * 3. expose the debug tools backed by that connection + a `ChiiAitSource`.\n *\n * `build_attach_url` (relay-specific, generates a deep-link + QR for the phone)\n * is not applicable in local mode because there is no relay or tunnel. The tool\n * is still listed (it is part of `DEBUG_TOOL_DEFINITIONS`) but will return a\n * clear \"not applicable\" message via the tunnel-down path (wssUrl is null).\n *\n * The AIT.* tools (`AIT.getSdkCallHistory`, `AIT.getMockState`,\n * `AIT.getOperationalEnvironment`) ride the same CDP channel via\n * `ChiiAitSource` → `LocalCdpConnection.sendCommand`. They will succeed once\n * the sdk-example dev-bridge (`window.__sdkCall` install) lands in sdk-example;\n * until then they return the sdk-example \"bridge absent\" message — which is\n * expected and noted in the PR as an explicit out-of-scope follow-up.\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 const chromium = await launchChromium({ port: cdpPort, devUrl });\n\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\n const connection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });\n // AIT.* methods ride the same CDP channel via LocalCdpConnection.sendCommand.\n const aitSource = new ChiiAitSource(connection);\n\n // Local mode has no relay tunnel — tunnelStatus is always \"down\" which causes\n // build_attach_url to return a clear \"tunnel not up\" error, communicating to\n // the agent that this tool is relay-only.\n const tunnelStatus: TunnelStatus = { up: false, wssUrl: null };\n\n const server = createDebugServer({\n connection,\n aitSource,\n getTunnelStatus: () => tunnelStatus,\n // Local-target debug mode: no relay tunnel exists by construction. The\n // env intent is `mock` (local Chromium attaches to the dev panel), so\n // `build_attach_url` (Tier B, relay-only) stays hidden — calling it would\n // fail with `tunnel-down` anyway. Explicit for parity with the relay\n // branch above.\n defaultEnv: 'mock',\n });\n\n const transport = new StdioServerTransport();\n\n let closed = false;\n let attachWatcher: { stop(): void } | null = null;\n\n const shutdown = () => {\n if (closed) return;\n closed = true;\n attachWatcher?.stop();\n connection.close();\n chromium.stop();\n void server.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 attachWatcher?.stop();\n chromium.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',\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',\n });\n shutdown();\n process.exit(1);\n });\n\n await server.connect(transport);\n\n // Start the attach watcher after the transport is connected so\n // sendToolListChanged has a live session to notify.\n attachWatcher = startAttachWatcher(connection, server);\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_diagnostics`, `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_diagnostics` — 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 { mcpError } 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_diagnostics',\n description:\n 'dev-mode: returns server diagnostics — Vite endpoint URL, last fetch timestamp/error, ' +\n 'mock state endpoint reachability, mode (\"dev\"), and environment metadata. ' +\n 'Call this when the dev server connection is suspect. ' +\n '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 /* 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\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_diagnostics` 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: {\n environment: env.environment,\n sdkVersion: env.sdkVersion,\n },\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 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 switch (name) {\n case 'list_pages':\n return jsonResult(buildDevListPagesResult(devtoolsUrl));\n\n case 'get_diagnostics':\n return jsonResult(\n await buildDevDiagnostics(devtoolsUrl, stateEndpoint, (url) => fetch(url)),\n );\n\n case 'measure_safe_area':\n return jsonResult(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 jsonResult(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/** 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 2/3) 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 *\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 * Node-only stdio process.\n */\n\nimport { realpathSync } from 'node:fs';\nimport { argv } from 'node:process';\nimport { fileURLToPath } from 'node:url';\nimport { runDebugServer, runLocalDebugServer } from './debug-server.js';\nimport { runDevServer } from './server.js';\n\ntype Mode = 'debug' | 'dev';\ntype Target = 'relay' | 'local';\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 2/3).\n * - `local` — local Chromium CDP attach (env 1, no relay needed).\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) or 'local'.\");\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 throw new Error(`Unknown --target '${value}'. Expected 'relay' (default) or 'local'.`);\n}\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\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 {\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":"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"}