@ait-co/devtools 0.1.37 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/cli.js +317 -108
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +2 -2
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -326,7 +326,7 @@ function createDevServer(deps = {}) {
|
|
|
326
326
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
327
327
|
const server = new Server({
|
|
328
328
|
name: "ait-devtools",
|
|
329
|
-
version: "0.1.
|
|
329
|
+
version: "0.1.39"
|
|
330
330
|
}, { capabilities: { tools: {} } });
|
|
331
331
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
332
332
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
package/dist/mcp/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":[],"sources":["../../src/mcp/ait-http-source.ts","../../src/mcp/tools.ts","../../src/mcp/server.ts"],"sourcesContent":["/**\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 * 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 CdpConnection,\n CdpRemoteObject,\n ConsoleApiCalledEvent,\n DomGetDocumentResult,\n DomSnapshotResult,\n NetworkRequestWillBeSentEvent,\n NetworkResponseReceivedEvent,\n} from './cdp-connection.js';\nimport { buildDeepLinkAttachUrl, validateSchemeAuthority } from './deeplink.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\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 },\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 },\n {\n name: 'list_pages',\n description:\n 'Lists the mini-app page(s) the Chii relay currently sees attached, plus whether the ' +\n 'cloudflared tunnel is up and the public wss relay URL the phone uses to attach. ' +\n 'Call this first to confirm a page is attached before reading console/network.',\n inputSchema: { type: 'object', properties: {}, required: [] },\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 },\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 },\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 },\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 },\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 '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 the relay to be attached — call list_pages first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\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 },\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 'Returns a clear error if window.__sdkCall is not available (non-dogfood bundle).',\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 },\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 },\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 },\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 },\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 * 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 '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/** Result of `list_pages`: attach status + tunnel state. */\nexport interface ListPagesResult {\n pages: ReturnType<CdpConnection['listTargets']>;\n tunnel: TunnelStatus;\n}\n\nexport function listPages(connection: CdpConnection, tunnel: TunnelStatus): ListPagesResult {\n return { pages: connection.listTargets(), tunnel };\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 'No relay URL yet — the cloudflared quick tunnel is not up. ' +\n 'Call list_pages to check tunnel status.',\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 * SECRET-HANDLING: `htmlPath` and `pngPath` are local filesystem paths — they\n * do NOT contain the `at=` TOTP code value. The attach URL (which may contain\n * `at=`) is embedded inside the HTML file, but the path itself is safe.\n */\nexport interface OpenQrInBrowserResult {\n /** `true` if the browser was successfully opened. */\n opened: boolean;\n /** Absolute path to the written HTML file. */\n htmlPath: string;\n /** Absolute path to the written PNG file. */\n pngPath: string;\n /** Error message if `opened` is false (browser spawn failed). */\n error?: string;\n}\n\n/**\n * Writes the attach URL as a QR PNG + a wrapper HTML page to the OS temp\n * directory, then opens the HTML in the OS default browser.\n *\n * SECRET-HANDLING:\n * - File names are derived from a short timestamp, NOT from the attach URL or\n * any token/code value. The `at=` code is NOT in the file name.\n * - The attach URL (which may carry `at=`) is embedded inside the HTML page\n * body — that is the intended delivery channel for the QR.\n * - This function must NOT write the attach URL, deploymentId, or any\n * TOTP code to stdout, stderr, or any log.\n *\n * @param attachUrl - The deep link to encode as a QR. May contain `at=<code>`.\n * @param deploymentId - Optional human-readable label for the HTML page (e.g. UUID substring).\n * Must NOT be derived from the `at=` code value.\n * @returns `OpenQrInBrowserResult` — never throws (errors are returned in `.error`).\n */\nexport async function openQrInBrowser(\n attachUrl: string,\n deploymentId?: string,\n): Promise<OpenQrInBrowserResult> {\n const { tmpdir } = await import('node:os');\n const { writeFileSync } = await import('node:fs');\n const { join } = await import('node:path');\n const { spawnSync } = await import('node:child_process');\n const { default: QRCode } = await import('qrcode');\n\n // Use a timestamp-based name, NOT anything derived from the attach URL.\n const stamp = Date.now();\n const pngPath = join(tmpdir(), `ait-qr-${stamp}.png`);\n const htmlPath = join(tmpdir(), `ait-qr-${stamp}.html`);\n\n // Write the QR PNG.\n try {\n await QRCode.toFile(pngPath, attachUrl, { type: 'png', errorCorrectionLevel: 'M' });\n } catch (err) {\n return {\n opened: false,\n htmlPath,\n pngPath,\n error: `QR PNG write failed: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n\n // Write the HTML wrapper.\n // SECRET: attachUrl is placed in the HTML as a text node and QR image src,\n // which is the intended delivery channel. It must NOT be in the file name.\n const safeLabel = deploymentId\n ? deploymentId.replace(/[<>&\"']/g, (c) => `&#${c.charCodeAt(0)};`)\n : 'attach';\n const htmlContent = `<!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 Debug — QR</title>\n <style>\n body { font-family: monospace; background: #111; color: #eee; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; gap: 1.5rem; padding: 2rem; box-sizing: border-box; }\n img { width: min(90vw, 400px); height: auto; image-rendering: pixelated; background: #fff; padding: 1rem; border-radius: 8px; }\n .label { font-size: 0.85rem; opacity: 0.6; }\n .url { font-size: 0.75rem; word-break: break-all; max-width: 60ch; opacity: 0.5; }\n </style>\n</head>\n<body>\n <img src=\"${pngPath}\" alt=\"QR code\" />\n <p class=\"label\">deployment: ${safeLabel}</p>\n</body>\n</html>`;\n\n try {\n writeFileSync(htmlPath, htmlContent, 'utf8');\n } catch (err) {\n return {\n opened: false,\n htmlPath,\n pngPath,\n error: `HTML write failed: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n\n // Open in OS default browser.\n const platform = process.platform;\n let openCmd: string;\n let openArgs: string[];\n if (platform === 'darwin') {\n openCmd = 'open';\n openArgs = [htmlPath];\n } else if (platform === 'win32') {\n openCmd = 'cmd';\n openArgs = ['/c', 'start', '', htmlPath];\n } else {\n openCmd = 'xdg-open';\n openArgs = [htmlPath];\n }\n\n // Use spawnSync with a short timeout — we don't need to wait for the browser\n // to finish loading, just for the launcher to start.\n const spawnResult = spawnSync(openCmd, openArgs, { timeout: 5000 });\n if (spawnResult.error) {\n return {\n opened: false,\n htmlPath,\n pngPath,\n error: `Browser open failed (${openCmd}): ${spawnResult.error.message}`,\n };\n }\n\n return { opened: true, htmlPath, pngPath };\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. `SafeAreaInsets.get()` if the native SDK object is available.\n * 3. nav bar geometry (first `.ait-navbar` element height, if present).\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 * It does not mutate any page state — the temporary element is removed after\n * reading. No secret or auth token is read or returned.\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 try {\n if (typeof SafeAreaInsets !== 'undefined' && SafeAreaInsets && typeof SafeAreaInsets.get === 'function') {\n sdkInsets = SafeAreaInsets.get();\n }\n } catch(_) {}\n var navBarHeight = null;\n try {\n var nb = document.querySelector('.ait-navbar');\n if (nb) navBarHeight = nb.getBoundingClientRect().height;\n } catch(_) {}\n return JSON.stringify({\n cssEnv: cssEnv,\n sdkInsets: sdkInsets,\n navBarHeight: navBarHeight,\n innerWidth: window.innerWidth,\n innerHeight: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio,\n userAgent: navigator.userAgent\n });\n})()\n`.trim();\n\n/**\n * Normalized result returned by `measure_safe_area`.\n *\n * All inset values are in CSS pixels as reported by the real device.\n * `userAgent` is included for device identification; it never contains\n * authentication secrets or session tokens.\n */\nexport interface SafeAreaMeasurement {\n /**\n * `env(safe-area-inset-*)` values read via `getComputedStyle` on the device.\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 * `SafeAreaInsets.get()` result from the native SDK, if available.\n * In the Toss host this carries the nav bar height as `top` and the\n * home-indicator height as `bottom`. `null` when the SDK object is absent\n * (e.g. outside a Toss WebView).\n */\n sdkInsets: { top: number; right: number; bottom: number; left: number } | null;\n /**\n * Height of the `.ait-navbar` element (px) if present, else `null`.\n * Useful to cross-validate `sdkInsets.top` against the rendered nav bar.\n */\n navBarHeight: number | null;\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 * Throws if the result is missing, contains an exception, or cannot be parsed.\n */\nexport function normalizeSafeAreaResult(rawValue: unknown): 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 navBarHeight = typeof obj.navBarHeight === 'number' ? obj.navBarHeight : null;\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 { cssEnv, sdkInsets, navBarHeight, innerWidth, innerHeight, devicePixelRatio, userAgent };\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 * Throws on CDP error, probe exception, or result parse failure.\n */\nexport async function measureSafeArea(connection: CdpConnection): 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);\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 */\nexport type CallSdkResult = { ok: true; value: unknown } | { ok: false; error: string };\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:'window.__sdkCall is not available — is this a dogfood (__DEBUG_BUILD__) bundle?'});` +\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 * 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 * Throws on CDP error or result parse failure. Returns `{ok:false, error}`\n * for bridge-level errors (method not found, SDK threw, bridge absent).\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 const expression = buildCallSdkExpression(name, args);\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\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 return normalizeCallSdkResult(result.result.value);\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 * @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 {\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n isAitToolName,\n} from './tools.js';\n\n/** Tool descriptors served by the dev-mode server. */\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 },\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 },\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 },\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 },\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 { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };\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 { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };\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 { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n content: [\n {\n type: 'text',\n text:\n `${message}\\n` +\n 'Is the Vite dev server running with the @ait-co/devtools unplugin option `mcp: true`? ' +\n 'Is AIT_DEVTOOLS_URL set correctly?',\n },\n ],\n isError: true,\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../../src/mcp/ait-http-source.ts","../../src/mcp/tools.ts","../../src/mcp/server.ts"],"sourcesContent":["/**\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 * 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 CdpConnection,\n CdpRemoteObject,\n ConsoleApiCalledEvent,\n DomGetDocumentResult,\n DomSnapshotResult,\n NetworkRequestWillBeSentEvent,\n NetworkResponseReceivedEvent,\n} from './cdp-connection.js';\nimport { buildDeepLinkAttachUrl, validateSchemeAuthority } from './deeplink.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\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 },\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 },\n {\n name: 'list_pages',\n description:\n 'Lists the mini-app page(s) the Chii relay currently sees attached, plus whether the ' +\n 'cloudflared tunnel is up and the public wss relay URL the phone uses to attach. ' +\n 'Call this first to confirm a page is attached before reading console/network.',\n inputSchema: { type: 'object', properties: {}, required: [] },\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 },\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 },\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 },\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 },\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 '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 the relay to be attached — call list_pages first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\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 },\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 'Returns a clear error if window.__sdkCall is not available (non-dogfood bundle).',\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 },\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 },\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 },\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 },\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 * 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 '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/** Result of `list_pages`: attach status + tunnel state. */\nexport interface ListPagesResult {\n pages: ReturnType<CdpConnection['listTargets']>;\n tunnel: TunnelStatus;\n}\n\nexport function listPages(connection: CdpConnection, tunnel: TunnelStatus): ListPagesResult {\n return { pages: connection.listTargets(), tunnel };\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 'No relay URL yet — the cloudflared quick tunnel is not up. ' +\n 'Call list_pages to check tunnel status.',\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\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으로 시도하며, 모두 실패해도 `opened: false` + `httpUrl`을\n * 반환해 사용자가 직접 브라우저에 붙여넣을 수 있게 한다.\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 const candidates = getBrowserCandidates(httpUrl);\n const stderrLines: string[] = [];\n\n for (const { cmd, args } of candidates) {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5000 });\n\n if (result.error) {\n // 명령 자체를 실행하지 못한 경우 (ENOENT 등) — 다음 후보로.\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 // exit 0이어도 stderr에 launch 실패 패턴이 있으면 실패로 취급.\n if (result.status === 0 && !isLaunchFailureStderr(stderr)) {\n return { opened: true, httpUrl, pngUrl };\n }\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. `SafeAreaInsets.get()` if the native SDK object is available.\n * 3. nav bar geometry (first `.ait-navbar` element height, if present).\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 * It does not mutate any page state — the temporary element is removed after\n * reading. No secret or auth token is read or returned.\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 try {\n if (typeof SafeAreaInsets !== 'undefined' && SafeAreaInsets && typeof SafeAreaInsets.get === 'function') {\n sdkInsets = SafeAreaInsets.get();\n }\n } catch(_) {}\n var navBarHeight = null;\n try {\n var nb = document.querySelector('.ait-navbar');\n if (nb) navBarHeight = nb.getBoundingClientRect().height;\n } catch(_) {}\n return JSON.stringify({\n cssEnv: cssEnv,\n sdkInsets: sdkInsets,\n navBarHeight: navBarHeight,\n innerWidth: window.innerWidth,\n innerHeight: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio,\n userAgent: navigator.userAgent\n });\n})()\n`.trim();\n\n/**\n * Normalized result returned by `measure_safe_area`.\n *\n * All inset values are in CSS pixels as reported by the real device.\n * `userAgent` is included for device identification; it never contains\n * authentication secrets or session tokens.\n */\nexport interface SafeAreaMeasurement {\n /**\n * `env(safe-area-inset-*)` values read via `getComputedStyle` on the device.\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 * `SafeAreaInsets.get()` result from the native SDK, if available.\n * In the Toss host this carries the nav bar height as `top` and the\n * home-indicator height as `bottom`. `null` when the SDK object is absent\n * (e.g. outside a Toss WebView).\n */\n sdkInsets: { top: number; right: number; bottom: number; left: number } | null;\n /**\n * Height of the `.ait-navbar` element (px) if present, else `null`.\n * Useful to cross-validate `sdkInsets.top` against the rendered nav bar.\n */\n navBarHeight: number | null;\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 * Throws if the result is missing, contains an exception, or cannot be parsed.\n */\nexport function normalizeSafeAreaResult(rawValue: unknown): 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 navBarHeight = typeof obj.navBarHeight === 'number' ? obj.navBarHeight : null;\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 { cssEnv, sdkInsets, navBarHeight, innerWidth, innerHeight, devicePixelRatio, userAgent };\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 * Throws on CDP error, probe exception, or result parse failure.\n */\nexport async function measureSafeArea(connection: CdpConnection): 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);\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 */\nexport type CallSdkResult = { ok: true; value: unknown } | { ok: false; error: string };\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:'window.__sdkCall is not available — is this a dogfood (__DEBUG_BUILD__) bundle?'});` +\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 * 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 * Throws on CDP error or result parse failure. Returns `{ok:false, error}`\n * for bridge-level errors (method not found, SDK threw, bridge absent).\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 const expression = buildCallSdkExpression(name, args);\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\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 return normalizeCallSdkResult(result.result.value);\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 * @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 {\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n isAitToolName,\n} from './tools.js';\n\n/** Tool descriptors served by the dev-mode server. */\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 },\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 },\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 },\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 },\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 { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };\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 { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };\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 { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n content: [\n {\n type: 'text',\n text:\n `${message}\\n` +\n 'Is the Vite dev server running with the @ait-co/devtools unplugin option `mcp: true`? ' +\n 'Is AIT_DEVTOOLS_URL set correctly?',\n },\n ],\n isError: true,\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"],"mappings":"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"}
|
package/dist/panel/index.js
CHANGED
|
@@ -1109,7 +1109,7 @@ function readGlobalString(key) {
|
|
|
1109
1109
|
}
|
|
1110
1110
|
const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
|
|
1111
1111
|
function getVersion() {
|
|
1112
|
-
return "0.1.
|
|
1112
|
+
return "0.1.39";
|
|
1113
1113
|
}
|
|
1114
1114
|
let panelVisibleSince = null;
|
|
1115
1115
|
let accumulatedMs = 0;
|
|
@@ -4873,7 +4873,7 @@ function mount() {
|
|
|
4873
4873
|
mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
|
|
4874
4874
|
refreshPanel();
|
|
4875
4875
|
});
|
|
4876
|
-
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.
|
|
4876
|
+
const headerRight = h("span", { style: "display:flex;align-items:center;gap:6px" }, mockBadge, h("span", { style: "font-size:11px;color:#666;font-weight:400" }, `v0.1.39`), closeBtn);
|
|
4877
4877
|
const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
|
|
4878
4878
|
tabsEl = h("div", { className: "ait-panel-tabs" });
|
|
4879
4879
|
for (const tab of getTabs()) {
|
package/package.json
CHANGED