@ait-co/devtools 0.1.24 → 0.1.25
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 +9 -8
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +2 -2
- package/dist/{tunnel-DgiECOnW.cjs → tunnel-BYP0yRBN.cjs} +9 -3
- package/dist/{tunnel-DgiECOnW.cjs.map → tunnel-BYP0yRBN.cjs.map} +1 -1
- package/dist/{tunnel-CY1velpk.js → tunnel-D0_TwDNE.js} +9 -3
- package/dist/{tunnel-CY1velpk.js.map → tunnel-D0_TwDNE.js.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +8 -2
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.js +8 -2
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/cli.js
CHANGED
|
@@ -461,14 +461,14 @@ function getOperationalEnvironment(source) {
|
|
|
461
461
|
*
|
|
462
462
|
* On spawn, the debug server opens an accountless `*.trycloudflare.com` quick
|
|
463
463
|
* tunnel to the local Chii relay so the phone can attach over a public wss URL,
|
|
464
|
-
* then prints that URL +
|
|
464
|
+
* then prints that URL + an attach token + an ASCII QR to the terminal. The
|
|
465
465
|
* phone scans the QR (or pastes the URL) to attach; the in-app side passes the
|
|
466
|
-
* token back. The token is generated + displayed
|
|
467
|
-
*
|
|
466
|
+
* token back. The token is generated + displayed as a pairing hint; relay-side
|
|
467
|
+
* validation (ACL enforcement) is a later phase.
|
|
468
468
|
*
|
|
469
469
|
* Node-only: spawns the cloudflared binary and writes to stdout/stderr.
|
|
470
470
|
*/
|
|
471
|
-
/** Generates a 32-byte hex
|
|
471
|
+
/** Generates a 32-byte hex attach token shown as a pairing hint (relay-side validation is a later phase). */
|
|
472
472
|
function generateAttachToken() {
|
|
473
473
|
return randomBytes(32).toString("hex");
|
|
474
474
|
}
|
|
@@ -524,8 +524,9 @@ async function renderAttachBanner(input) {
|
|
|
524
524
|
"",
|
|
525
525
|
"AIT debug — attach a mini-app to this session",
|
|
526
526
|
"",
|
|
527
|
-
` relay (wss):
|
|
528
|
-
` token:
|
|
527
|
+
` relay (wss): ${input.wssUrl}`,
|
|
528
|
+
` attach token: ${input.token}`,
|
|
529
|
+
` (token is a pairing hint — relay-side validation lands in a later phase)`,
|
|
529
530
|
"",
|
|
530
531
|
" Open the dogfood mini-app with ?debug=1, then scan the QR",
|
|
531
532
|
" (or paste the relay URL + token in the in-app attach form):",
|
|
@@ -568,7 +569,7 @@ function createDebugServer(deps) {
|
|
|
568
569
|
const { connection, aitSource, getTunnelStatus } = deps;
|
|
569
570
|
const server = new Server({
|
|
570
571
|
name: "ait-debug",
|
|
571
|
-
version: "0.1.
|
|
572
|
+
version: "0.1.25"
|
|
572
573
|
}, { capabilities: { tools: {} } });
|
|
573
574
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEBUG_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
574
575
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -815,7 +816,7 @@ function createDevServer(deps = {}) {
|
|
|
815
816
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
816
817
|
const server = new Server({
|
|
817
818
|
name: "ait-devtools",
|
|
818
|
-
version: "0.1.
|
|
819
|
+
version: "0.1.25"
|
|
819
820
|
}, { capabilities: { tools: {} } });
|
|
820
821
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
821
822
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
package/dist/mcp/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":["isObject","isObject","jsonResult"],"sources":["../../src/mcp/ait-chii-source.ts","../../src/mcp/chii-connection.ts","../../src/mcp/chii-relay.ts","../../src/mcp/tools.ts","../../src/mcp/tunnel.ts","../../src/mcp/debug-server.ts","../../src/mcp/ait-http-source.ts","../../src/mcp/server.ts","../../src/mcp/cli.ts"],"sourcesContent":["/**\n * Debug-mode `AitSource` — forwards `AIT.*` methods over the Chii channel.\n *\n * The AIT domain (`AIT.getSdkCallHistory` / `getMockState` /\n * `getOperationalEnvironment`) is non-standard CDP: the in-app side registers a\n * handler for these methods and answers them over the same Chii websocket the\n * CDP commands use. Building the AIT source on `ChiiCdpConnection.sendCommand`\n * means both domains share one transport (spec: \"the same MCP server forwards\n * both CDP and AIT domains\").\n *\n * The in-app `AIT.*` handler lives downstream in sdk-example. Here we build\n * the MCP-server-side forwarding + the injectable seam; tests inject a fake\n * `AitSource` returning canned responses, so this forwarding layer needs no\n * phone.\n *\n * Node-only (wraps the relay websocket connection).\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** The slice of `ChiiCdpConnection` this source needs (keeps it testable). */\nexport interface AitCommandSender {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Narrows an `AIT.getSdkCallHistory` response, tolerating a missing array. */\nfunction asSdkCallHistory(raw: unknown): AitSdkCallHistory {\n if (isObject(raw) && Array.isArray(raw.calls)) {\n return { calls: raw.calls as AitSdkCallHistory['calls'] };\n }\n return { calls: [] };\n}\n\n/** Narrows an `AIT.getMockState` response to an opaque record. */\nfunction asMockState(raw: unknown): AitMockState {\n return isObject(raw) ? raw : {};\n}\n\n/** Narrows an `AIT.getOperationalEnvironment` response. */\nfunction asOperationalEnvironment(raw: unknown): AitOperationalEnvironment {\n const environment =\n isObject(raw) && typeof raw.environment === 'string' ? raw.environment : 'unknown';\n const sdkVersion = isObject(raw) && typeof raw.sdkVersion === 'string' ? raw.sdkVersion : null;\n return { environment, sdkVersion };\n}\n\nexport class ChiiAitSource implements AitSource {\n constructor(private readonly sender: AitCommandSender) {}\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n const raw = await this.sender.sendCommand(method);\n // The map's value type is resolved per-key below; the cast is the single\n // narrowing point (each branch returns the precise shape for `method`).\n switch (method) {\n case 'AIT.getSdkCallHistory':\n return asSdkCallHistory(raw) as AitMethodMap[M];\n case 'AIT.getMockState':\n return asMockState(raw) as AitMethodMap[M];\n case 'AIT.getOperationalEnvironment':\n return asOperationalEnvironment(raw) as AitMethodMap[M];\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * Production `CdpConnection` backed by the local Chii relay.\n *\n * Topology (debug mode):\n * phone target.js --WS--> Chii relay :9100 <--WS-- this connection\n *\n * The phone connects to the relay as a `target`; this module connects as a\n * `client` (the role a CDP frontend would take) so CDP events the page emits\n * (`Runtime.consoleAPICalled`, `Network.*`) flow back here. We buffer recent\n * events in ring buffers the tool layer reads via `getBufferedEvents`.\n *\n * Node-only: imports `ws`. Never bundled into the browser/in-app entries.\n */\n\nimport { EventEmitter } from 'node:events';\nimport { WebSocket } from 'ws';\nimport type {\n CdpCommandMap,\n CdpCommandName,\n CdpConnection,\n CdpEventMap,\n CdpEventName,\n CdpTarget,\n} from './cdp-connection.js';\n\n/** Max events retained per domain ring buffer. */\nconst DEFAULT_BUFFER_SIZE = 500;\n\n/** A CDP message arriving over the relay websocket. */\ninterface CdpInboundMessage {\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { message: string };\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction parseInbound(raw: string): CdpInboundMessage | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isObject(parsed)) return null;\n const message: CdpInboundMessage = {};\n if (typeof parsed.id === 'number') message.id = parsed.id;\n if (typeof parsed.method === 'string') message.method = parsed.method;\n if ('params' in parsed) message.params = parsed.params;\n if ('result' in parsed) message.result = parsed.result;\n if (isObject(parsed.error) && typeof parsed.error.message === 'string') {\n message.error = { message: parsed.error.message };\n }\n return message;\n}\n\nconst PHASE_1_EVENTS: readonly CdpEventName[] = [\n 'Runtime.consoleAPICalled',\n 'Network.requestWillBeSent',\n 'Network.responseReceived',\n];\n\nexport interface ChiiCdpConnectionOptions {\n /** Base URL of the local Chii relay HTTP/WS server, e.g. `http://127.0.0.1:9100`. */\n relayBaseUrl: string;\n /** Per-domain ring buffer size. */\n bufferSize?: number;\n}\n\n/**\n * Production CDP connection. Polls the relay for the first attached target,\n * opens a client websocket to it, enables Phase 1 domains, and buffers events.\n */\nexport class ChiiCdpConnection implements CdpConnection {\n private readonly relayBaseUrl: string;\n private readonly bufferSize: number;\n private readonly emitter = new EventEmitter();\n private readonly buffers = new Map<CdpEventName, unknown[]>();\n private readonly targets = new Map<string, CdpTarget>();\n\n private ws: WebSocket | null = null;\n private nextCommandId = 1;\n /** In-flight enableDomains() promise — concurrent callers share it. */\n private enablingPromise: Promise<void> | null = null;\n /** Pending request→response commands keyed by CDP message id. */\n private readonly pending = new Map<\n number,\n { resolve: (result: unknown) => void; reject: (err: Error) => void }\n >();\n\n constructor(options: ChiiCdpConnectionOptions) {\n this.relayBaseUrl = options.relayBaseUrl.replace(/\\/$/, '');\n this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;\n for (const event of PHASE_1_EVENTS) this.buffers.set(event, []);\n // EventEmitter caps listeners at 10 by default; the tool layer may add\n // several short-lived subscriptions, so lift the cap.\n this.emitter.setMaxListeners(0);\n }\n\n /** Refresh the attached-target list from the relay's `GET /targets`. */\n async refreshTargets(): Promise<CdpTarget[]> {\n const res = await fetch(`${this.relayBaseUrl}/targets`);\n if (!res.ok) {\n throw new Error(`Chii relay /targets returned HTTP ${res.status} ${res.statusText}`);\n }\n const body: unknown = await res.json();\n const list = isObject(body) && Array.isArray(body.targets) ? body.targets : [];\n this.targets.clear();\n for (const item of list) {\n if (!isObject(item) || typeof item.id !== 'string') continue;\n this.targets.set(item.id, {\n id: item.id,\n title: typeof item.title === 'string' ? item.title : '',\n url: typeof item.url === 'string' ? item.url : '',\n });\n }\n return [...this.targets.values()];\n }\n\n listTargets(): CdpTarget[] {\n return [...this.targets.values()];\n }\n\n /**\n * Connect a client websocket to the first attached target and enable Phase 1\n * domains. Resolves once the socket is open and enable commands are sent.\n */\n async enableDomains(): Promise<void> {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) return;\n // If a connect attempt is already in-flight, await it rather than racing\n // to open a second websocket that would overwrite `this.ws` and leak the first.\n if (this.enablingPromise) return this.enablingPromise;\n this.enablingPromise = this._doEnableDomains().finally(() => {\n this.enablingPromise = null;\n });\n return this.enablingPromise;\n }\n\n private async _doEnableDomains(): Promise<void> {\n const targets = await this.refreshTargets();\n const target = targets[0];\n if (!target) {\n throw new Error('No mini-app page attached to the Chii relay yet.');\n }\n\n const wsBase = this.relayBaseUrl.replace(/^http/, 'ws');\n const clientId = `devtools-mcp-${Date.now()}`;\n const ws = new WebSocket(\n `${wsBase}/client/${clientId}?target=${encodeURIComponent(target.id)}`,\n );\n this.ws = ws;\n\n await new Promise<void>((resolve, reject) => {\n ws.once('open', () => resolve());\n ws.once('error', (err: Error) => reject(err));\n });\n\n ws.on('message', (data: WebSocket.RawData) => this.handleMessage(data.toString()));\n\n this.sendFireAndForget('Runtime.enable');\n this.sendFireAndForget('Network.enable');\n // DOM/Page domains back the Phase 2 command tools; Chii answers their\n // request→response commands once enabled.\n this.sendFireAndForget('DOM.enable');\n this.sendFireAndForget('Page.enable');\n }\n\n /** Fire-and-forget CDP message (used for `*.enable`, no result awaited). */\n private sendFireAndForget(method: string, params: Record<string, unknown> = {}): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n const id = this.nextCommandId++;\n this.ws.send(JSON.stringify({ id, method, params }));\n }\n\n /**\n * Issue a CDP command and resolve with its result (Phase 2). Rejects on a CDP\n * error frame or when no websocket is open (no page attached yet).\n */\n send<M extends CdpCommandName>(\n method: M,\n params?: CdpCommandMap[M]['params'],\n ): Promise<CdpCommandMap[M]['result']> {\n return this.sendCommand(method, params ?? {}) as Promise<CdpCommandMap[M]['result']>;\n }\n\n /**\n * Issue an arbitrary request→response command over the relay and resolve with\n * its raw result. Both the typed CDP {@link send} and the AIT domain (Phase 3\n * `AIT.*` methods, forwarded over the same Chii channel) build on this.\n */\n sendCommand(method: string, params: Record<string, unknown> = {}): Promise<unknown> {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n return Promise.reject(\n new Error('No mini-app page attached to the Chii relay yet. Call enableDomains() first.'),\n );\n }\n const id = this.nextCommandId++;\n const ws = this.ws;\n return new Promise<unknown>((resolve, reject) => {\n this.pending.set(id, { resolve, reject });\n ws.send(JSON.stringify({ id, method, params }));\n });\n }\n\n private handleMessage(raw: string): void {\n const message = parseInbound(raw);\n if (!message) return;\n\n // Command response (has an id matching a pending request).\n if (typeof message.id === 'number' && this.pending.has(message.id)) {\n const waiter = this.pending.get(message.id);\n this.pending.delete(message.id);\n if (waiter) {\n if (message.error) waiter.reject(new Error(message.error.message));\n else waiter.resolve(message.result);\n }\n return;\n }\n\n // Event (buffered for the Phase 1 stream tools).\n if (typeof message.method !== 'string') return;\n if (!this.buffers.has(message.method as CdpEventName)) return;\n const event = message.method as CdpEventName;\n const buffer = this.buffers.get(event);\n if (!buffer) return;\n buffer.push(message.params);\n if (buffer.length > this.bufferSize) buffer.shift();\n this.emitter.emit(event, message.params);\n }\n\n getBufferedEvents<E extends CdpEventName>(event: E): ReadonlyArray<CdpEventMap[E]> {\n const buffer = this.buffers.get(event);\n return (buffer ?? []) as ReadonlyArray<CdpEventMap[E]>;\n }\n\n on<E extends CdpEventName>(event: E, listener: (payload: CdpEventMap[E]) => void): () => void {\n this.emitter.on(event, listener as (payload: unknown) => void);\n return () => this.emitter.off(event, listener as (payload: unknown) => void);\n }\n\n /** Close the relay client websocket and reject any in-flight commands. */\n close(): void {\n this.ws?.close();\n this.ws = null;\n for (const waiter of this.pending.values()) {\n waiter.reject(new Error('Chii relay connection closed.'));\n }\n this.pending.clear();\n }\n}\n","/**\n * Boots the local Chii relay server.\n *\n * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome\n * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.\n * The relay accepts a `target` websocket from the phone's injected `target.js`\n * and `client` websockets from CDP frontends (our MCP connection).\n *\n * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app\n * entries.\n */\n\nimport { createServer, type Server } from 'node:http';\nimport { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\n\n/** `chii/server` is CommonJS and shipped without TypeScript types. */\ninterface ChiiServerModule {\n start(options: {\n port?: number;\n host?: string;\n domain?: string;\n server?: Server;\n basePath?: string;\n }): Promise<void>;\n}\n\nfunction loadChiiServer(): ChiiServerModule {\n // `chii`'s package `main` is `./server/index.js`, exposing `{ start }`.\n const mod: unknown = require('chii');\n if (\n typeof mod === 'object' &&\n mod !== null &&\n 'start' in mod &&\n typeof (mod as { start: unknown }).start === 'function'\n ) {\n return mod as ChiiServerModule;\n }\n throw new Error('chii server module did not expose start()');\n}\n\nexport interface ChiiRelay {\n port: number;\n /** Base URL for the relay HTTP/WS server, e.g. `http://127.0.0.1:9100`. */\n baseUrl: string;\n close(): Promise<void>;\n}\n\nexport interface StartChiiRelayOptions {\n /** Local port for the relay. Default 9100. */\n port?: number;\n /** Bind host. Default 127.0.0.1 (tunnel reaches it locally). */\n host?: string;\n}\n\n/** Starts the Chii relay on the given port and resolves once listening. */\nexport async function startChiiRelay(options: StartChiiRelayOptions = {}): Promise<ChiiRelay> {\n const port = options.port ?? 9100;\n const host = options.host ?? '127.0.0.1';\n\n const httpServer = createServer();\n const chii = loadChiiServer();\n // Passing an existing `server` makes chii attach its Koa handler + WS upgrade\n // to our HTTP server rather than creating its own listener.\n await chii.start({ server: httpServer, domain: `${host}:${port}`, port });\n\n await new Promise<void>((resolve, reject) => {\n httpServer.once('error', reject);\n httpServer.listen(port, host, () => {\n httpServer.off('error', reject);\n resolve();\n });\n });\n\n return {\n port,\n baseUrl: `http://${host}:${port}`,\n close: () =>\n new Promise<void>((resolve) => {\n httpServer.close(() => resolve());\n }),\n };\n}\n","/**\n * Debug-mode MCP tools (Phase 1–3).\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 * 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';\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: '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: '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/** 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/* -------------------------------------------------------------------------- */\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/* 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 * cloudflared quick tunnel + attach banner for the debug-mode MCP server.\n *\n * On spawn, the debug server opens an accountless `*.trycloudflare.com` quick\n * tunnel to the local Chii relay so the phone can attach over a public wss URL,\n * then prints that URL + a secret token + an ASCII QR to the terminal. The\n * phone scans the QR (or pastes the URL) to attach; the in-app side passes the\n * token back. The token is generated + displayed for the phone to pass on\n * attach; full ACL enforcement (token validation at the relay) is a later phase.\n *\n * Node-only: spawns the cloudflared binary and writes to stdout/stderr.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { bin, install, Tunnel } from 'cloudflared';\nimport qrcode from 'qrcode-terminal';\n\n/** Generates a 32-byte hex secret token used to gate attach. */\nexport function generateAttachToken(): string {\n return randomBytes(32).toString('hex');\n}\n\nexport interface QuickTunnel {\n /** Public `https://*.trycloudflare.com` URL the tunnel exposes. */\n url: string;\n /** Same host as `wss://` — the relay endpoint the phone attaches to. */\n wssUrl: string;\n stop(): void;\n}\n\n/** Ensures the cloudflared binary is installed (downloads + caches on first run). */\nasync function ensureCloudflaredBin(): Promise<void> {\n const { existsSync } = await import('node:fs');\n if (!existsSync(bin)) {\n await install(bin);\n }\n}\n\n/**\n * Opens a cloudflared quick tunnel to the local relay port and resolves once\n * the public URL is assigned.\n */\nexport async function startQuickTunnel(localPort: number): Promise<QuickTunnel> {\n await ensureCloudflaredBin();\n\n const tunnel = Tunnel.quick(`http://127.0.0.1:${localPort}`);\n\n const url = await new Promise<string>((resolve, reject) => {\n const onUrl = (assigned: string) => {\n cleanup();\n resolve(assigned);\n };\n const onError = (err: Error) => {\n cleanup();\n reject(err);\n };\n const onExit = (code: number | null) => {\n cleanup();\n reject(new Error(`cloudflared exited before assigning a URL (code ${code})`));\n };\n const cleanup = () => {\n tunnel.off('url', onUrl);\n tunnel.off('error', onError);\n tunnel.off('exit', onExit);\n };\n tunnel.once('url', onUrl);\n tunnel.once('error', onError);\n tunnel.once('exit', onExit);\n });\n\n return {\n url,\n wssUrl: url.replace(/^https/, 'wss'),\n stop: () => {\n tunnel.stop();\n },\n };\n}\n\nexport interface AttachBannerInput {\n wssUrl: string;\n token: string;\n}\n\n/** Renders the attach banner (URL + token + ASCII QR) as a string. */\nexport async function renderAttachBanner(input: AttachBannerInput): Promise<string> {\n // Encode the attach payload as a URL so a QR scan opens directly.\n const payload = `${input.wssUrl}?token=${input.token}`;\n const qr = await new Promise<string>((resolve) => {\n qrcode.generate(payload, { small: true }, (rendered) => resolve(rendered));\n });\n return [\n '',\n 'AIT debug — attach a mini-app to this session',\n '',\n ` relay (wss): ${input.wssUrl}`,\n ` token: ${input.token}`,\n '',\n ' Open the dogfood mini-app with ?debug=1, then scan the QR',\n ' (or paste the relay URL + token in the in-app attach form):',\n '',\n qr,\n ].join('\\n');\n}\n\n/** Prints the attach banner to stderr (stdout is the MCP stdio channel). */\nexport async function printAttachBanner(input: AttachBannerInput): Promise<void> {\n const banner = await renderAttachBanner(input);\n process.stderr.write(`${banner}\\n`);\n}\n","/**\n * @ait-co/devtools debug-mode MCP server (stdio).\n *\n * Lets an AI coding agent attach to a running mini-app (real Toss WebView, or a\n * browser in dev mode) and read its console/network/DOM/screenshot over CDP plus\n * the AIT.* domain, without a human watching a phone. Transport is CDP-via-Chii:\n * a local Chii relay :9100 exposed through a cloudflared quick tunnel; the phone\n * attaches over the public wss URL.\n *\n * AI host --stdio--> this server --CDP client WS--> Chii relay :9100\n * ^-- target WS -- phone\n *\n * The tool layer reads from an injectable `CdpConnection` (CDP) and `AitSource`\n * (AIT.*), so every tool is unit-testable with a fake (no phone). This module\n * wires the live pieces (relay + tunnel + production connection); the phone\n * roundtrip is fully wired and pending only on-device acceptance.\n *\n * Node-only.\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { ChiiAitSource } from './ait-chii-source.js';\nimport type { AitSource } from './ait-source.js';\nimport type { CdpConnection } from './cdp-connection.js';\nimport { ChiiCdpConnection } from './chii-connection.js';\nimport { startChiiRelay } from './chii-relay.js';\nimport {\n DEBUG_TOOL_DEFINITIONS,\n getDomDocument,\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n isAitToolName,\n isDebugToolName,\n listConsoleMessages,\n listNetworkRequests,\n listPages,\n type TunnelStatus,\n takeScreenshot,\n takeSnapshot,\n} from './tools.js';\nimport {\n generateAttachToken,\n printAttachBanner,\n type QuickTunnel,\n startQuickTunnel,\n} from './tunnel.js';\n\n/** Live infra the connection reads tunnel status from. */\nexport interface DebugServerDeps {\n connection: CdpConnection;\n /** AIT.* domain source — forwarded over the same Chii channel in production. */\n aitSource: AitSource;\n /** Returns current tunnel status (URL changes per spawn). */\n getTunnelStatus(): TunnelStatus;\n}\n\n/**\n * Builds the debug-mode MCP server around an injected CDP connection + AIT\n * source + tunnel status getter. Pure wiring — does not start a relay or\n * tunnel, which is what makes the tool surface unit-testable.\n */\nexport function createDebugServer(deps: DebugServerDeps): Server {\n const { connection, aitSource, getTunnelStatus } = deps;\n\n const server = new Server(\n { name: 'ait-debug', version: __VERSION__ },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, () => ({\n tools: DEBUG_TOOL_DEFINITIONS.map((tool) => ({ ...tool })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n if (!isDebugToolName(name)) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${name}` }],\n isError: true,\n };\n }\n\n // AIT.* tools are served by the AIT source. In production it rides the same\n // Chii websocket as CDP, so the connection must be attached first; the AIT\n // source's sendCommand rejects with a clear message if no page is attached.\n if (isAitToolName(name)) {\n try {\n await connection.enableDomains();\n switch (name) {\n case 'AIT.getSdkCallHistory':\n return jsonResult(await getSdkCallHistory(aitSource));\n case 'AIT.getMockState':\n return jsonResult(await getMockState(aitSource));\n case 'AIT.getOperationalEnvironment':\n return jsonResult(await getOperationalEnvironment(aitSource));\n default:\n return unknownTool(name);\n }\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n try {\n // Ensure CDP domains are enabled before reading. No-op once attached;\n // throws a clear message while no page is attached yet.\n await connection.enableDomains();\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (name === 'list_pages') {\n // list_pages is still useful pre-attach: report tunnel + empty pages.\n return jsonResult(listPages(connection, getTunnelStatus()));\n }\n return {\n content: [\n {\n type: 'text',\n text: `${message}\\nCall list_pages to confirm a mini-app has attached over the relay.`,\n },\n ],\n isError: true,\n };\n }\n\n try {\n switch (name) {\n case 'list_console_messages':\n return jsonResult(listConsoleMessages(connection));\n case 'list_network_requests':\n return jsonResult(listNetworkRequests(connection));\n case 'list_pages':\n return jsonResult(listPages(connection, getTunnelStatus()));\n case 'get_dom_document':\n return jsonResult(await getDomDocument(connection));\n case 'take_snapshot':\n return jsonResult(await takeSnapshot(connection));\n case 'take_screenshot': {\n const shot = await takeScreenshot(connection);\n return {\n content: [{ type: 'image' as const, data: shot.data, mimeType: shot.mimeType }],\n };\n }\n default:\n return unknownTool(name);\n }\n } catch (err) {\n return errorResult(err, name);\n }\n });\n\n return server;\n}\n\nfunction jsonResult(value: unknown) {\n return { content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] };\n}\n\nfunction unknownTool(name: string) {\n return { content: [{ type: 'text' as const, text: `Unknown tool: ${name}` }], isError: true };\n}\n\nfunction errorResult(err: unknown, name: string) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n content: [\n {\n type: 'text' as const,\n text: `${name} failed: ${message}\\nCall list_pages to confirm a mini-app has attached over the relay.`,\n },\n ],\n isError: true,\n };\n}\n\nexport interface RunDebugServerOptions {\n /** Local Chii relay port. Default 9100. */\n relayPort?: number;\n}\n\n/**\n * Boots the live debug stack and serves it over stdio:\n * 1. start the Chii relay,\n * 2. open a cloudflared quick tunnel to it,\n * 3. print QR + secret token,\n * 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.\n */\nexport async function runDebugServer(options: RunDebugServerOptions = {}): Promise<void> {\n const relayPort = options.relayPort ?? 9100;\n\n const relay = await startChiiRelay({ port: relayPort });\n\n let tunnel: QuickTunnel | null = null;\n let tunnelStatus: TunnelStatus = { up: false, wssUrl: null };\n const token = generateAttachToken();\n\n try {\n tunnel = await startQuickTunnel(relayPort);\n tunnelStatus = { up: true, wssUrl: tunnel.wssUrl };\n await printAttachBanner({ wssUrl: tunnel.wssUrl, token });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(\n `[ait-debug] Failed to open cloudflared quick tunnel: ${message}\\n` +\n '[ait-debug] The relay is up locally; attach over the public URL is unavailable until the tunnel starts.\\n',\n );\n }\n\n const connection = new ChiiCdpConnection({ relayBaseUrl: relay.baseUrl });\n // AIT.* methods ride the same Chii channel as CDP commands.\n const aitSource = new ChiiAitSource(connection);\n const server = createDebugServer({\n connection,\n aitSource,\n getTunnelStatus: () => tunnelStatus,\n });\n\n const transport = new StdioServerTransport();\n\n const shutdown = () => {\n connection.close();\n tunnel?.stop();\n void relay.close();\n void server.close();\n };\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n\n await server.connect(transport);\n}\n","/**\n * Dev-mode `AitSource` — backed by the Vite dev server's mock-state endpoint.\n *\n * The dev server already exposes the live browser mock state at\n * `GET /api/ait-devtools/state` (registered by the unplugin with `mcp: true`).\n * Phase 3 aligns dev mode and debug mode on the same `AIT.*` tool surface, so\n * dev mode serves those tools off this one HTTP source instead of a CDP channel:\n *\n * - `AIT.getMockState` → the full state snapshot (verbatim).\n * - `AIT.getOperationalEnvironment` → derived from the snapshot's\n * `environment` + `appVersion` fields.\n * - `AIT.getSdkCallHistory` → empty (the dev endpoint does not record\n * an SDK call trace — honest, not faked).\n *\n * An AI agent thus sees the same `AIT.getMockState` tool whether attached to a\n * phone (debug) or a dev browser (dev). Tests inject a fake `fetch`.\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** Minimal `fetch` shape this source needs (injectable in tests). */\nexport type FetchLike = (url: string) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n}>;\n\nexport interface HttpAitSourceOptions {\n /** Full URL of the mock-state endpoint, e.g. `http://localhost:5173/api/ait-devtools/state`. */\n stateEndpoint: string;\n /** Injected for tests; defaults to global `fetch`. */\n fetchImpl?: FetchLike;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport class HttpAitSource implements AitSource {\n private readonly stateEndpoint: string;\n private readonly fetchImpl: FetchLike;\n\n constructor(options: HttpAitSourceOptions) {\n this.stateEndpoint = options.stateEndpoint;\n this.fetchImpl = options.fetchImpl ?? ((url) => fetch(url));\n }\n\n private async fetchState(): Promise<AitMockState> {\n const res = await this.fetchImpl(this.stateEndpoint);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch mock state from ${this.stateEndpoint}: HTTP ${res.status} ${res.statusText}. ` +\n 'Ensure the Vite dev server is running with the @ait-co/devtools unplugin option `mcp: true`.',\n );\n }\n const body = await res.json();\n return isObject(body) ? body : {};\n }\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n switch (method) {\n case 'AIT.getMockState': {\n const state = await this.fetchState();\n return state as AitMethodMap[M];\n }\n case 'AIT.getOperationalEnvironment': {\n const state = await this.fetchState();\n const environment = typeof state.environment === 'string' ? state.environment : 'unknown';\n const sdkVersion = typeof state.appVersion === 'string' ? state.appVersion : null;\n const result: AitOperationalEnvironment = { environment, sdkVersion };\n return result as AitMethodMap[M];\n }\n case 'AIT.getSdkCallHistory': {\n // Dev endpoint records no SDK call trace; return empty rather than fake.\n const result: AitSdkCallHistory = { calls: [] };\n return result as AitMethodMap[M];\n }\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * @ait-co/devtools dev-mode MCP server (stdio).\n *\n * Exposes the live browser mock state from a running Vite dev server to AI\n * coding agents via the Model Context Protocol (MCP).\n *\n * Architecture:\n * Browser (aitState) → Vite dev server endpoint (/api/ait-devtools/state)\n * ← HTTP GET ← this stdio MCP server ← AI agent\n *\n * The Vite endpoint is registered by the unplugin when `mcp: true` is set in\n * the plugin options (see `src/unplugin/index.ts`).\n *\n * Phase 3 tool-surface alignment: dev mode and debug mode now expose the same\n * `AIT.*` tools (`AIT.getMockState`, `AIT.getOperationalEnvironment`,\n * `AIT.getSdkCallHistory`). In dev mode they are backed by the HTTP mock-state\n * endpoint (see `HttpAitSource`); in debug mode by the Chii channel. So an AI\n * sees a coherent tool whether attached to a phone (debug) or a dev browser\n * (dev). `devtools_get_mock_state` (the original devtools#130 name) is kept as a\n * backward-compatible alias of `AIT.getMockState`.\n *\n * This module is reached via the `devtools-mcp --mode=dev` CLI entry (see\n * `cli.ts`); the default (no flag) bin mode is the debug-mode CDP/Chii server.\n *\n * Usage (in your MCP client config, e.g. Claude Desktop):\n * {\n * \"mcpServers\": {\n * \"ait-devtools\": {\n * \"command\": \"pnpm\",\n * \"args\": [\"exec\", \"devtools-mcp\", \"--mode=dev\"],\n * \"env\": { \"AIT_DEVTOOLS_URL\": \"http://localhost:5173\" }\n * }\n * }\n * }\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { HttpAitSource } from './ait-http-source.js';\nimport type { AitSource } from './ait-source.js';\nimport {\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","#!/usr/bin/env node\n/**\n * `devtools-mcp` bin entry.\n *\n * Single bin, two transports selected by `--mode`:\n * - (default, no flag) debug mode — CDP/Chii relay + cloudflared quick tunnel.\n * Attach a running mini-app (real Toss WebView or a browser) and read its\n * console + network over CDP without a human watching a phone.\n * - `--mode=dev` — dev mode — reads the live browser mock state from a running\n * Vite dev server (the devtools#130 `devtools_get_mock_state` surface).\n *\n * Node-only stdio process.\n */\n\nimport { argv } from 'node:process';\nimport { fileURLToPath } from 'node:url';\nimport { runDebugServer } from './debug-server.js';\nimport { runDevServer } from './server.js';\n\ntype Mode = 'debug' | 'dev';\n\n/** Parses `--mode=<value>` / `--mode <value>` from argv; default `debug`. */\nexport function parseMode(argv: readonly string[]): Mode {\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === undefined) continue;\n if (arg.startsWith('--mode=')) {\n return normalizeMode(arg.slice('--mode='.length));\n }\n if (arg === '--mode') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw new Error(\"--mode requires a value: 'debug' (default) or 'dev'.\");\n }\n return normalizeMode(next);\n }\n }\n return 'debug';\n}\n\nfunction normalizeMode(value: string): Mode {\n if (value === 'dev') return 'dev';\n if (value === 'debug') return 'debug';\n throw new Error(`Unknown --mode '${value}'. Expected 'debug' (default) or 'dev'.`);\n}\n\nasync function main(): Promise<void> {\n const mode = parseMode(process.argv.slice(2));\n if (mode === 'dev') {\n await runDevServer();\n } else {\n await runDebugServer();\n }\n}\n\n/** True when this file is the process entry (the bin), not an import. */\nfunction isEntrypoint(): boolean {\n const entry = argv[1];\n if (entry === undefined) return false;\n try {\n return fileURLToPath(import.meta.url) === entry;\n } catch {\n return false;\n }\n}\n\nif (isEntrypoint()) {\n main().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`[devtools-mcp] fatal: ${message}\\n`);\n process.exitCode = 1;\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":["isObject","isObject","jsonResult"],"sources":["../../src/mcp/ait-chii-source.ts","../../src/mcp/chii-connection.ts","../../src/mcp/chii-relay.ts","../../src/mcp/tools.ts","../../src/mcp/tunnel.ts","../../src/mcp/debug-server.ts","../../src/mcp/ait-http-source.ts","../../src/mcp/server.ts","../../src/mcp/cli.ts"],"sourcesContent":["/**\n * Debug-mode `AitSource` — forwards `AIT.*` methods over the Chii channel.\n *\n * The AIT domain (`AIT.getSdkCallHistory` / `getMockState` /\n * `getOperationalEnvironment`) is non-standard CDP: the in-app side registers a\n * handler for these methods and answers them over the same Chii websocket the\n * CDP commands use. Building the AIT source on `ChiiCdpConnection.sendCommand`\n * means both domains share one transport (spec: \"the same MCP server forwards\n * both CDP and AIT domains\").\n *\n * The in-app `AIT.*` handler lives downstream in sdk-example. Here we build\n * the MCP-server-side forwarding + the injectable seam; tests inject a fake\n * `AitSource` returning canned responses, so this forwarding layer needs no\n * phone.\n *\n * Node-only (wraps the relay websocket connection).\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** The slice of `ChiiCdpConnection` this source needs (keeps it testable). */\nexport interface AitCommandSender {\n sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Narrows an `AIT.getSdkCallHistory` response, tolerating a missing array. */\nfunction asSdkCallHistory(raw: unknown): AitSdkCallHistory {\n if (isObject(raw) && Array.isArray(raw.calls)) {\n return { calls: raw.calls as AitSdkCallHistory['calls'] };\n }\n return { calls: [] };\n}\n\n/** Narrows an `AIT.getMockState` response to an opaque record. */\nfunction asMockState(raw: unknown): AitMockState {\n return isObject(raw) ? raw : {};\n}\n\n/** Narrows an `AIT.getOperationalEnvironment` response. */\nfunction asOperationalEnvironment(raw: unknown): AitOperationalEnvironment {\n const environment =\n isObject(raw) && typeof raw.environment === 'string' ? raw.environment : 'unknown';\n const sdkVersion = isObject(raw) && typeof raw.sdkVersion === 'string' ? raw.sdkVersion : null;\n return { environment, sdkVersion };\n}\n\nexport class ChiiAitSource implements AitSource {\n constructor(private readonly sender: AitCommandSender) {}\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n const raw = await this.sender.sendCommand(method);\n // The map's value type is resolved per-key below; the cast is the single\n // narrowing point (each branch returns the precise shape for `method`).\n switch (method) {\n case 'AIT.getSdkCallHistory':\n return asSdkCallHistory(raw) as AitMethodMap[M];\n case 'AIT.getMockState':\n return asMockState(raw) as AitMethodMap[M];\n case 'AIT.getOperationalEnvironment':\n return asOperationalEnvironment(raw) as AitMethodMap[M];\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * Production `CdpConnection` backed by the local Chii relay.\n *\n * Topology (debug mode):\n * phone target.js --WS--> Chii relay :9100 <--WS-- this connection\n *\n * The phone connects to the relay as a `target`; this module connects as a\n * `client` (the role a CDP frontend would take) so CDP events the page emits\n * (`Runtime.consoleAPICalled`, `Network.*`) flow back here. We buffer recent\n * events in ring buffers the tool layer reads via `getBufferedEvents`.\n *\n * Node-only: imports `ws`. Never bundled into the browser/in-app entries.\n */\n\nimport { EventEmitter } from 'node:events';\nimport { WebSocket } from 'ws';\nimport type {\n CdpCommandMap,\n CdpCommandName,\n CdpConnection,\n CdpEventMap,\n CdpEventName,\n CdpTarget,\n} from './cdp-connection.js';\n\n/** Max events retained per domain ring buffer. */\nconst DEFAULT_BUFFER_SIZE = 500;\n\n/** A CDP message arriving over the relay websocket. */\ninterface CdpInboundMessage {\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { message: string };\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction parseInbound(raw: string): CdpInboundMessage | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isObject(parsed)) return null;\n const message: CdpInboundMessage = {};\n if (typeof parsed.id === 'number') message.id = parsed.id;\n if (typeof parsed.method === 'string') message.method = parsed.method;\n if ('params' in parsed) message.params = parsed.params;\n if ('result' in parsed) message.result = parsed.result;\n if (isObject(parsed.error) && typeof parsed.error.message === 'string') {\n message.error = { message: parsed.error.message };\n }\n return message;\n}\n\nconst PHASE_1_EVENTS: readonly CdpEventName[] = [\n 'Runtime.consoleAPICalled',\n 'Network.requestWillBeSent',\n 'Network.responseReceived',\n];\n\nexport interface ChiiCdpConnectionOptions {\n /** Base URL of the local Chii relay HTTP/WS server, e.g. `http://127.0.0.1:9100`. */\n relayBaseUrl: string;\n /** Per-domain ring buffer size. */\n bufferSize?: number;\n}\n\n/**\n * Production CDP connection. Polls the relay for the first attached target,\n * opens a client websocket to it, enables Phase 1 domains, and buffers events.\n */\nexport class ChiiCdpConnection implements CdpConnection {\n private readonly relayBaseUrl: string;\n private readonly bufferSize: number;\n private readonly emitter = new EventEmitter();\n private readonly buffers = new Map<CdpEventName, unknown[]>();\n private readonly targets = new Map<string, CdpTarget>();\n\n private ws: WebSocket | null = null;\n private nextCommandId = 1;\n /** In-flight enableDomains() promise — concurrent callers share it. */\n private enablingPromise: Promise<void> | null = null;\n /** Pending request→response commands keyed by CDP message id. */\n private readonly pending = new Map<\n number,\n { resolve: (result: unknown) => void; reject: (err: Error) => void }\n >();\n\n constructor(options: ChiiCdpConnectionOptions) {\n this.relayBaseUrl = options.relayBaseUrl.replace(/\\/$/, '');\n this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;\n for (const event of PHASE_1_EVENTS) this.buffers.set(event, []);\n // EventEmitter caps listeners at 10 by default; the tool layer may add\n // several short-lived subscriptions, so lift the cap.\n this.emitter.setMaxListeners(0);\n }\n\n /** Refresh the attached-target list from the relay's `GET /targets`. */\n async refreshTargets(): Promise<CdpTarget[]> {\n const res = await fetch(`${this.relayBaseUrl}/targets`);\n if (!res.ok) {\n throw new Error(`Chii relay /targets returned HTTP ${res.status} ${res.statusText}`);\n }\n const body: unknown = await res.json();\n const list = isObject(body) && Array.isArray(body.targets) ? body.targets : [];\n this.targets.clear();\n for (const item of list) {\n if (!isObject(item) || typeof item.id !== 'string') continue;\n this.targets.set(item.id, {\n id: item.id,\n title: typeof item.title === 'string' ? item.title : '',\n url: typeof item.url === 'string' ? item.url : '',\n });\n }\n return [...this.targets.values()];\n }\n\n listTargets(): CdpTarget[] {\n return [...this.targets.values()];\n }\n\n /**\n * Connect a client websocket to the first attached target and enable Phase 1\n * domains. Resolves once the socket is open and enable commands are sent.\n */\n async enableDomains(): Promise<void> {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) return;\n // If a connect attempt is already in-flight, await it rather than racing\n // to open a second websocket that would overwrite `this.ws` and leak the first.\n if (this.enablingPromise) return this.enablingPromise;\n this.enablingPromise = this._doEnableDomains().finally(() => {\n this.enablingPromise = null;\n });\n return this.enablingPromise;\n }\n\n private async _doEnableDomains(): Promise<void> {\n const targets = await this.refreshTargets();\n const target = targets[0];\n if (!target) {\n throw new Error('No mini-app page attached to the Chii relay yet.');\n }\n\n const wsBase = this.relayBaseUrl.replace(/^http/, 'ws');\n const clientId = `devtools-mcp-${Date.now()}`;\n const ws = new WebSocket(\n `${wsBase}/client/${clientId}?target=${encodeURIComponent(target.id)}`,\n );\n this.ws = ws;\n\n await new Promise<void>((resolve, reject) => {\n ws.once('open', () => resolve());\n ws.once('error', (err: Error) => reject(err));\n });\n\n ws.on('message', (data: WebSocket.RawData) => this.handleMessage(data.toString()));\n\n this.sendFireAndForget('Runtime.enable');\n this.sendFireAndForget('Network.enable');\n // DOM/Page domains back the Phase 2 command tools; Chii answers their\n // request→response commands once enabled.\n this.sendFireAndForget('DOM.enable');\n this.sendFireAndForget('Page.enable');\n }\n\n /** Fire-and-forget CDP message (used for `*.enable`, no result awaited). */\n private sendFireAndForget(method: string, params: Record<string, unknown> = {}): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n const id = this.nextCommandId++;\n this.ws.send(JSON.stringify({ id, method, params }));\n }\n\n /**\n * Issue a CDP command and resolve with its result (Phase 2). Rejects on a CDP\n * error frame or when no websocket is open (no page attached yet).\n */\n send<M extends CdpCommandName>(\n method: M,\n params?: CdpCommandMap[M]['params'],\n ): Promise<CdpCommandMap[M]['result']> {\n return this.sendCommand(method, params ?? {}) as Promise<CdpCommandMap[M]['result']>;\n }\n\n /**\n * Issue an arbitrary request→response command over the relay and resolve with\n * its raw result. Both the typed CDP {@link send} and the AIT domain (Phase 3\n * `AIT.*` methods, forwarded over the same Chii channel) build on this.\n */\n sendCommand(method: string, params: Record<string, unknown> = {}): Promise<unknown> {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n return Promise.reject(\n new Error('No mini-app page attached to the Chii relay yet. Call enableDomains() first.'),\n );\n }\n const id = this.nextCommandId++;\n const ws = this.ws;\n return new Promise<unknown>((resolve, reject) => {\n this.pending.set(id, { resolve, reject });\n ws.send(JSON.stringify({ id, method, params }));\n });\n }\n\n private handleMessage(raw: string): void {\n const message = parseInbound(raw);\n if (!message) return;\n\n // Command response (has an id matching a pending request).\n if (typeof message.id === 'number' && this.pending.has(message.id)) {\n const waiter = this.pending.get(message.id);\n this.pending.delete(message.id);\n if (waiter) {\n if (message.error) waiter.reject(new Error(message.error.message));\n else waiter.resolve(message.result);\n }\n return;\n }\n\n // Event (buffered for the Phase 1 stream tools).\n if (typeof message.method !== 'string') return;\n if (!this.buffers.has(message.method as CdpEventName)) return;\n const event = message.method as CdpEventName;\n const buffer = this.buffers.get(event);\n if (!buffer) return;\n buffer.push(message.params);\n if (buffer.length > this.bufferSize) buffer.shift();\n this.emitter.emit(event, message.params);\n }\n\n getBufferedEvents<E extends CdpEventName>(event: E): ReadonlyArray<CdpEventMap[E]> {\n const buffer = this.buffers.get(event);\n return (buffer ?? []) as ReadonlyArray<CdpEventMap[E]>;\n }\n\n on<E extends CdpEventName>(event: E, listener: (payload: CdpEventMap[E]) => void): () => void {\n this.emitter.on(event, listener as (payload: unknown) => void);\n return () => this.emitter.off(event, listener as (payload: unknown) => void);\n }\n\n /** Close the relay client websocket and reject any in-flight commands. */\n close(): void {\n this.ws?.close();\n this.ws = null;\n for (const waiter of this.pending.values()) {\n waiter.reject(new Error('Chii relay connection closed.'));\n }\n this.pending.clear();\n }\n}\n","/**\n * Boots the local Chii relay server.\n *\n * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome\n * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.\n * The relay accepts a `target` websocket from the phone's injected `target.js`\n * and `client` websockets from CDP frontends (our MCP connection).\n *\n * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app\n * entries.\n */\n\nimport { createServer, type Server } from 'node:http';\nimport { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\n\n/** `chii/server` is CommonJS and shipped without TypeScript types. */\ninterface ChiiServerModule {\n start(options: {\n port?: number;\n host?: string;\n domain?: string;\n server?: Server;\n basePath?: string;\n }): Promise<void>;\n}\n\nfunction loadChiiServer(): ChiiServerModule {\n // `chii`'s package `main` is `./server/index.js`, exposing `{ start }`.\n const mod: unknown = require('chii');\n if (\n typeof mod === 'object' &&\n mod !== null &&\n 'start' in mod &&\n typeof (mod as { start: unknown }).start === 'function'\n ) {\n return mod as ChiiServerModule;\n }\n throw new Error('chii server module did not expose start()');\n}\n\nexport interface ChiiRelay {\n port: number;\n /** Base URL for the relay HTTP/WS server, e.g. `http://127.0.0.1:9100`. */\n baseUrl: string;\n close(): Promise<void>;\n}\n\nexport interface StartChiiRelayOptions {\n /** Local port for the relay. Default 9100. */\n port?: number;\n /** Bind host. Default 127.0.0.1 (tunnel reaches it locally). */\n host?: string;\n}\n\n/** Starts the Chii relay on the given port and resolves once listening. */\nexport async function startChiiRelay(options: StartChiiRelayOptions = {}): Promise<ChiiRelay> {\n const port = options.port ?? 9100;\n const host = options.host ?? '127.0.0.1';\n\n const httpServer = createServer();\n const chii = loadChiiServer();\n // Passing an existing `server` makes chii attach its Koa handler + WS upgrade\n // to our HTTP server rather than creating its own listener.\n await chii.start({ server: httpServer, domain: `${host}:${port}`, port });\n\n await new Promise<void>((resolve, reject) => {\n httpServer.once('error', reject);\n httpServer.listen(port, host, () => {\n httpServer.off('error', reject);\n resolve();\n });\n });\n\n return {\n port,\n baseUrl: `http://${host}:${port}`,\n close: () =>\n new Promise<void>((resolve) => {\n httpServer.close(() => resolve());\n }),\n };\n}\n","/**\n * Debug-mode MCP tools (Phase 1–3).\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 * 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';\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: '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: '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/** 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/* -------------------------------------------------------------------------- */\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/* 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 * cloudflared quick tunnel + attach banner for the debug-mode MCP server.\n *\n * On spawn, the debug server opens an accountless `*.trycloudflare.com` quick\n * tunnel to the local Chii relay so the phone can attach over a public wss URL,\n * then prints that URL + an attach token + an ASCII QR to the terminal. The\n * phone scans the QR (or pastes the URL) to attach; the in-app side passes the\n * token back. The token is generated + displayed as a pairing hint; relay-side\n * validation (ACL enforcement) is a later phase.\n *\n * Node-only: spawns the cloudflared binary and writes to stdout/stderr.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { bin, install, Tunnel } from 'cloudflared';\nimport qrcode from 'qrcode-terminal';\n\n/** Generates a 32-byte hex attach token shown as a pairing hint (relay-side validation is a later phase). */\nexport function generateAttachToken(): string {\n return randomBytes(32).toString('hex');\n}\n\nexport interface QuickTunnel {\n /** Public `https://*.trycloudflare.com` URL the tunnel exposes. */\n url: string;\n /** Same host as `wss://` — the relay endpoint the phone attaches to. */\n wssUrl: string;\n stop(): void;\n}\n\n/** Ensures the cloudflared binary is installed (downloads + caches on first run). */\nasync function ensureCloudflaredBin(): Promise<void> {\n const { existsSync } = await import('node:fs');\n if (!existsSync(bin)) {\n await install(bin);\n }\n}\n\n/**\n * Opens a cloudflared quick tunnel to the local relay port and resolves once\n * the public URL is assigned.\n */\nexport async function startQuickTunnel(localPort: number): Promise<QuickTunnel> {\n await ensureCloudflaredBin();\n\n const tunnel = Tunnel.quick(`http://127.0.0.1:${localPort}`);\n\n const url = await new Promise<string>((resolve, reject) => {\n const onUrl = (assigned: string) => {\n cleanup();\n resolve(assigned);\n };\n const onError = (err: Error) => {\n cleanup();\n reject(err);\n };\n const onExit = (code: number | null) => {\n cleanup();\n reject(new Error(`cloudflared exited before assigning a URL (code ${code})`));\n };\n const cleanup = () => {\n tunnel.off('url', onUrl);\n tunnel.off('error', onError);\n tunnel.off('exit', onExit);\n };\n tunnel.once('url', onUrl);\n tunnel.once('error', onError);\n tunnel.once('exit', onExit);\n });\n\n return {\n url,\n wssUrl: url.replace(/^https/, 'wss'),\n stop: () => {\n tunnel.stop();\n },\n };\n}\n\nexport interface AttachBannerInput {\n wssUrl: string;\n token: string;\n}\n\n/** Renders the attach banner (URL + token + ASCII QR) as a string. */\nexport async function renderAttachBanner(input: AttachBannerInput): Promise<string> {\n // Encode the attach payload as a URL so a QR scan opens directly.\n const payload = `${input.wssUrl}?token=${input.token}`;\n const qr = await new Promise<string>((resolve) => {\n qrcode.generate(payload, { small: true }, (rendered) => resolve(rendered));\n });\n return [\n '',\n 'AIT debug — attach a mini-app to this session',\n '',\n ` relay (wss): ${input.wssUrl}`,\n ` attach token: ${input.token}`,\n ` (token is a pairing hint — relay-side validation lands in a later phase)`,\n '',\n ' Open the dogfood mini-app with ?debug=1, then scan the QR',\n ' (or paste the relay URL + token in the in-app attach form):',\n '',\n qr,\n ].join('\\n');\n}\n\n/** Prints the attach banner to stderr (stdout is the MCP stdio channel). */\nexport async function printAttachBanner(input: AttachBannerInput): Promise<void> {\n const banner = await renderAttachBanner(input);\n process.stderr.write(`${banner}\\n`);\n}\n","/**\n * @ait-co/devtools debug-mode MCP server (stdio).\n *\n * Lets an AI coding agent attach to a running mini-app (real Toss WebView, or a\n * browser in dev mode) and read its console/network/DOM/screenshot over CDP plus\n * the AIT.* domain, without a human watching a phone. Transport is CDP-via-Chii:\n * a local Chii relay :9100 exposed through a cloudflared quick tunnel; the phone\n * attaches over the public wss URL.\n *\n * AI host --stdio--> this server --CDP client WS--> Chii relay :9100\n * ^-- target WS -- phone\n *\n * The tool layer reads from an injectable `CdpConnection` (CDP) and `AitSource`\n * (AIT.*), so every tool is unit-testable with a fake (no phone). This module\n * wires the live pieces (relay + tunnel + production connection); the phone\n * roundtrip is fully wired and pending only on-device acceptance.\n *\n * Node-only.\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { ChiiAitSource } from './ait-chii-source.js';\nimport type { AitSource } from './ait-source.js';\nimport type { CdpConnection } from './cdp-connection.js';\nimport { ChiiCdpConnection } from './chii-connection.js';\nimport { startChiiRelay } from './chii-relay.js';\nimport {\n DEBUG_TOOL_DEFINITIONS,\n getDomDocument,\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n isAitToolName,\n isDebugToolName,\n listConsoleMessages,\n listNetworkRequests,\n listPages,\n type TunnelStatus,\n takeScreenshot,\n takeSnapshot,\n} from './tools.js';\nimport {\n generateAttachToken,\n printAttachBanner,\n type QuickTunnel,\n startQuickTunnel,\n} from './tunnel.js';\n\n/** Live infra the connection reads tunnel status from. */\nexport interface DebugServerDeps {\n connection: CdpConnection;\n /** AIT.* domain source — forwarded over the same Chii channel in production. */\n aitSource: AitSource;\n /** Returns current tunnel status (URL changes per spawn). */\n getTunnelStatus(): TunnelStatus;\n}\n\n/**\n * Builds the debug-mode MCP server around an injected CDP connection + AIT\n * source + tunnel status getter. Pure wiring — does not start a relay or\n * tunnel, which is what makes the tool surface unit-testable.\n */\nexport function createDebugServer(deps: DebugServerDeps): Server {\n const { connection, aitSource, getTunnelStatus } = deps;\n\n const server = new Server(\n { name: 'ait-debug', version: __VERSION__ },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, () => ({\n tools: DEBUG_TOOL_DEFINITIONS.map((tool) => ({ ...tool })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n if (!isDebugToolName(name)) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${name}` }],\n isError: true,\n };\n }\n\n // AIT.* tools are served by the AIT source. In production it rides the same\n // Chii websocket as CDP, so the connection must be attached first; the AIT\n // source's sendCommand rejects with a clear message if no page is attached.\n if (isAitToolName(name)) {\n try {\n await connection.enableDomains();\n switch (name) {\n case 'AIT.getSdkCallHistory':\n return jsonResult(await getSdkCallHistory(aitSource));\n case 'AIT.getMockState':\n return jsonResult(await getMockState(aitSource));\n case 'AIT.getOperationalEnvironment':\n return jsonResult(await getOperationalEnvironment(aitSource));\n default:\n return unknownTool(name);\n }\n } catch (err) {\n return errorResult(err, name);\n }\n }\n\n try {\n // Ensure CDP domains are enabled before reading. No-op once attached;\n // throws a clear message while no page is attached yet.\n await connection.enableDomains();\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (name === 'list_pages') {\n // list_pages is still useful pre-attach: report tunnel + empty pages.\n return jsonResult(listPages(connection, getTunnelStatus()));\n }\n return {\n content: [\n {\n type: 'text',\n text: `${message}\\nCall list_pages to confirm a mini-app has attached over the relay.`,\n },\n ],\n isError: true,\n };\n }\n\n try {\n switch (name) {\n case 'list_console_messages':\n return jsonResult(listConsoleMessages(connection));\n case 'list_network_requests':\n return jsonResult(listNetworkRequests(connection));\n case 'list_pages':\n return jsonResult(listPages(connection, getTunnelStatus()));\n case 'get_dom_document':\n return jsonResult(await getDomDocument(connection));\n case 'take_snapshot':\n return jsonResult(await takeSnapshot(connection));\n case 'take_screenshot': {\n const shot = await takeScreenshot(connection);\n return {\n content: [{ type: 'image' as const, data: shot.data, mimeType: shot.mimeType }],\n };\n }\n default:\n return unknownTool(name);\n }\n } catch (err) {\n return errorResult(err, name);\n }\n });\n\n return server;\n}\n\nfunction jsonResult(value: unknown) {\n return { content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] };\n}\n\nfunction unknownTool(name: string) {\n return { content: [{ type: 'text' as const, text: `Unknown tool: ${name}` }], isError: true };\n}\n\nfunction errorResult(err: unknown, name: string) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n content: [\n {\n type: 'text' as const,\n text: `${name} failed: ${message}\\nCall list_pages to confirm a mini-app has attached over the relay.`,\n },\n ],\n isError: true,\n };\n}\n\nexport interface RunDebugServerOptions {\n /** Local Chii relay port. Default 9100. */\n relayPort?: number;\n}\n\n/**\n * Boots the live debug stack and serves it over stdio:\n * 1. start the Chii relay,\n * 2. open a cloudflared quick tunnel to it,\n * 3. print QR + secret token,\n * 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.\n */\nexport async function runDebugServer(options: RunDebugServerOptions = {}): Promise<void> {\n const relayPort = options.relayPort ?? 9100;\n\n const relay = await startChiiRelay({ port: relayPort });\n\n let tunnel: QuickTunnel | null = null;\n let tunnelStatus: TunnelStatus = { up: false, wssUrl: null };\n const token = generateAttachToken();\n\n try {\n tunnel = await startQuickTunnel(relayPort);\n tunnelStatus = { up: true, wssUrl: tunnel.wssUrl };\n await printAttachBanner({ wssUrl: tunnel.wssUrl, token });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(\n `[ait-debug] Failed to open cloudflared quick tunnel: ${message}\\n` +\n '[ait-debug] The relay is up locally; attach over the public URL is unavailable until the tunnel starts.\\n',\n );\n }\n\n const connection = new ChiiCdpConnection({ relayBaseUrl: relay.baseUrl });\n // AIT.* methods ride the same Chii channel as CDP commands.\n const aitSource = new ChiiAitSource(connection);\n const server = createDebugServer({\n connection,\n aitSource,\n getTunnelStatus: () => tunnelStatus,\n });\n\n const transport = new StdioServerTransport();\n\n const shutdown = () => {\n connection.close();\n tunnel?.stop();\n void relay.close();\n void server.close();\n };\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n\n await server.connect(transport);\n}\n","/**\n * Dev-mode `AitSource` — backed by the Vite dev server's mock-state endpoint.\n *\n * The dev server already exposes the live browser mock state at\n * `GET /api/ait-devtools/state` (registered by the unplugin with `mcp: true`).\n * Phase 3 aligns dev mode and debug mode on the same `AIT.*` tool surface, so\n * dev mode serves those tools off this one HTTP source instead of a CDP channel:\n *\n * - `AIT.getMockState` → the full state snapshot (verbatim).\n * - `AIT.getOperationalEnvironment` → derived from the snapshot's\n * `environment` + `appVersion` fields.\n * - `AIT.getSdkCallHistory` → empty (the dev endpoint does not record\n * an SDK call trace — honest, not faked).\n *\n * An AI agent thus sees the same `AIT.getMockState` tool whether attached to a\n * phone (debug) or a dev browser (dev). Tests inject a fake `fetch`.\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** Minimal `fetch` shape this source needs (injectable in tests). */\nexport type FetchLike = (url: string) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n}>;\n\nexport interface HttpAitSourceOptions {\n /** Full URL of the mock-state endpoint, e.g. `http://localhost:5173/api/ait-devtools/state`. */\n stateEndpoint: string;\n /** Injected for tests; defaults to global `fetch`. */\n fetchImpl?: FetchLike;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport class HttpAitSource implements AitSource {\n private readonly stateEndpoint: string;\n private readonly fetchImpl: FetchLike;\n\n constructor(options: HttpAitSourceOptions) {\n this.stateEndpoint = options.stateEndpoint;\n this.fetchImpl = options.fetchImpl ?? ((url) => fetch(url));\n }\n\n private async fetchState(): Promise<AitMockState> {\n const res = await this.fetchImpl(this.stateEndpoint);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch mock state from ${this.stateEndpoint}: HTTP ${res.status} ${res.statusText}. ` +\n 'Ensure the Vite dev server is running with the @ait-co/devtools unplugin option `mcp: true`.',\n );\n }\n const body = await res.json();\n return isObject(body) ? body : {};\n }\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n switch (method) {\n case 'AIT.getMockState': {\n const state = await this.fetchState();\n return state as AitMethodMap[M];\n }\n case 'AIT.getOperationalEnvironment': {\n const state = await this.fetchState();\n const environment = typeof state.environment === 'string' ? state.environment : 'unknown';\n const sdkVersion = typeof state.appVersion === 'string' ? state.appVersion : null;\n const result: AitOperationalEnvironment = { environment, sdkVersion };\n return result as AitMethodMap[M];\n }\n case 'AIT.getSdkCallHistory': {\n // Dev endpoint records no SDK call trace; return empty rather than fake.\n const result: AitSdkCallHistory = { calls: [] };\n return result as AitMethodMap[M];\n }\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * @ait-co/devtools dev-mode MCP server (stdio).\n *\n * Exposes the live browser mock state from a running Vite dev server to AI\n * coding agents via the Model Context Protocol (MCP).\n *\n * Architecture:\n * Browser (aitState) → Vite dev server endpoint (/api/ait-devtools/state)\n * ← HTTP GET ← this stdio MCP server ← AI agent\n *\n * The Vite endpoint is registered by the unplugin when `mcp: true` is set in\n * the plugin options (see `src/unplugin/index.ts`).\n *\n * Phase 3 tool-surface alignment: dev mode and debug mode now expose the same\n * `AIT.*` tools (`AIT.getMockState`, `AIT.getOperationalEnvironment`,\n * `AIT.getSdkCallHistory`). In dev mode they are backed by the HTTP mock-state\n * endpoint (see `HttpAitSource`); in debug mode by the Chii channel. So an AI\n * sees a coherent tool whether attached to a phone (debug) or a dev browser\n * (dev). `devtools_get_mock_state` (the original devtools#130 name) is kept as a\n * backward-compatible alias of `AIT.getMockState`.\n *\n * This module is reached via the `devtools-mcp --mode=dev` CLI entry (see\n * `cli.ts`); the default (no flag) bin mode is the debug-mode CDP/Chii server.\n *\n * Usage (in your MCP client config, e.g. Claude Desktop):\n * {\n * \"mcpServers\": {\n * \"ait-devtools\": {\n * \"command\": \"pnpm\",\n * \"args\": [\"exec\", \"devtools-mcp\", \"--mode=dev\"],\n * \"env\": { \"AIT_DEVTOOLS_URL\": \"http://localhost:5173\" }\n * }\n * }\n * }\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { HttpAitSource } from './ait-http-source.js';\nimport type { AitSource } from './ait-source.js';\nimport {\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","#!/usr/bin/env node\n/**\n * `devtools-mcp` bin entry.\n *\n * Single bin, two transports selected by `--mode`:\n * - (default, no flag) debug mode — CDP/Chii relay + cloudflared quick tunnel.\n * Attach a running mini-app (real Toss WebView or a browser) and read its\n * console + network over CDP without a human watching a phone.\n * - `--mode=dev` — dev mode — reads the live browser mock state from a running\n * Vite dev server (the devtools#130 `devtools_get_mock_state` surface).\n *\n * Node-only stdio process.\n */\n\nimport { argv } from 'node:process';\nimport { fileURLToPath } from 'node:url';\nimport { runDebugServer } from './debug-server.js';\nimport { runDevServer } from './server.js';\n\ntype Mode = 'debug' | 'dev';\n\n/** Parses `--mode=<value>` / `--mode <value>` from argv; default `debug`. */\nexport function parseMode(argv: readonly string[]): Mode {\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === undefined) continue;\n if (arg.startsWith('--mode=')) {\n return normalizeMode(arg.slice('--mode='.length));\n }\n if (arg === '--mode') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw new Error(\"--mode requires a value: 'debug' (default) or 'dev'.\");\n }\n return normalizeMode(next);\n }\n }\n return 'debug';\n}\n\nfunction normalizeMode(value: string): Mode {\n if (value === 'dev') return 'dev';\n if (value === 'debug') return 'debug';\n throw new Error(`Unknown --mode '${value}'. Expected 'debug' (default) or 'dev'.`);\n}\n\nasync function main(): Promise<void> {\n const mode = parseMode(process.argv.slice(2));\n if (mode === 'dev') {\n await runDevServer();\n } else {\n await runDebugServer();\n }\n}\n\n/** True when this file is the process entry (the bin), not an import. */\nfunction isEntrypoint(): boolean {\n const entry = argv[1];\n if (entry === undefined) return false;\n try {\n return fileURLToPath(import.meta.url) === entry;\n } catch {\n return false;\n }\n}\n\nif (isEntrypoint()) {\n main().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`[devtools-mcp] fatal: ${message}\\n`);\n process.exitCode = 1;\n });\n}\n"],"mappings":"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"}
|
package/dist/mcp/server.js
CHANGED
|
@@ -222,7 +222,7 @@ function createDevServer(deps = {}) {
|
|
|
222
222
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
223
223
|
const server = new Server({
|
|
224
224
|
name: "ait-devtools",
|
|
225
|
-
version: "0.1.
|
|
225
|
+
version: "0.1.25"
|
|
226
226
|
}, { capabilities: { tools: {} } });
|
|
227
227
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
228
228
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
package/dist/panel/index.js
CHANGED
|
@@ -1050,7 +1050,7 @@ function readGlobalString(key) {
|
|
|
1050
1050
|
}
|
|
1051
1051
|
const TELEMETRY_ENDPOINT = readGlobalString("__TELEMETRY_ENDPOINT__") ?? "https://t.aitc.dev";
|
|
1052
1052
|
function getVersion() {
|
|
1053
|
-
return "0.1.
|
|
1053
|
+
return "0.1.25";
|
|
1054
1054
|
}
|
|
1055
1055
|
let panelVisibleSince = null;
|
|
1056
1056
|
let accumulatedMs = 0;
|
|
@@ -4182,7 +4182,7 @@ function mount() {
|
|
|
4182
4182
|
mockBadge.textContent = aitState.state.panelEditable ? t("panel.editMode.on") : t("panel.editMode.off");
|
|
4183
4183
|
refreshPanel();
|
|
4184
4184
|
});
|
|
4185
|
-
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.
|
|
4185
|
+
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.25`), closeBtn);
|
|
4186
4186
|
const header = h("div", { className: "ait-panel-header" }, h("span", {}, t("panel.title")), headerRight);
|
|
4187
4187
|
tabsEl = h("div", { className: "ait-panel-tabs" });
|
|
4188
4188
|
for (const tab of getTabs()) {
|
|
@@ -92,6 +92,7 @@ async function startQuickTunnel(port) {
|
|
|
92
92
|
};
|
|
93
93
|
return new Promise((resolve, reject) => {
|
|
94
94
|
const timer = setTimeout(() => {
|
|
95
|
+
cleanup();
|
|
95
96
|
stop();
|
|
96
97
|
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared did not report a tunnel URL within ${URL_TIMEOUT_MS / 1e3}s. Check your network connection, or run \`cloudflared tunnel --url http://localhost:${port}\` manually.`));
|
|
97
98
|
}, URL_TIMEOUT_MS);
|
|
@@ -99,24 +100,29 @@ async function startQuickTunnel(port) {
|
|
|
99
100
|
const found = parseTrycloudflareUrl(line);
|
|
100
101
|
if (!found) return;
|
|
101
102
|
clearTimeout(timer);
|
|
102
|
-
|
|
103
|
-
tunnel.off("stderr", onUrl);
|
|
103
|
+
cleanup();
|
|
104
104
|
resolve({
|
|
105
105
|
url: found,
|
|
106
106
|
stop
|
|
107
107
|
});
|
|
108
108
|
};
|
|
109
|
+
const cleanup = () => {
|
|
110
|
+
tunnel.off("stdout", onUrl);
|
|
111
|
+
tunnel.off("stderr", onUrl);
|
|
112
|
+
};
|
|
109
113
|
tunnel.once("url", onUrl);
|
|
110
114
|
tunnel.on("stdout", onUrl);
|
|
111
115
|
tunnel.on("stderr", onUrl);
|
|
112
116
|
tunnel.once("error", (err) => {
|
|
113
117
|
clearTimeout(timer);
|
|
118
|
+
cleanup();
|
|
114
119
|
stop();
|
|
115
120
|
reject(err);
|
|
116
121
|
});
|
|
117
122
|
tunnel.once("exit", (code) => {
|
|
118
123
|
if (stopped) return;
|
|
119
124
|
clearTimeout(timer);
|
|
125
|
+
cleanup();
|
|
120
126
|
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared exited (code ${code ?? "null"}) before reporting a tunnel URL.`));
|
|
121
127
|
});
|
|
122
128
|
});
|
|
@@ -125,4 +131,4 @@ async function startQuickTunnel(port) {
|
|
|
125
131
|
exports.printTunnelBanner = printTunnelBanner;
|
|
126
132
|
exports.startQuickTunnel = startQuickTunnel;
|
|
127
133
|
|
|
128
|
-
//# sourceMappingURL=tunnel-
|
|
134
|
+
//# sourceMappingURL=tunnel-BYP0yRBN.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-
|
|
1
|
+
{"version":3,"file":"tunnel-BYP0yRBN.cjs","names":[],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url);\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.`,\n ),\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,sBAAsB,MAA6B;CACjE,MAAM,IAAI,KAAK,MAAM,iBAAiB;AACtC,QAAO,IAAI,EAAE,KAAK;;AAUpB,MAAM,eAAe;;;;;;;;;AAUrB,SAAgB,sBAAsB,WAA2B;AAC/D,QAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;;;;;;;;AAS7D,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,IAAI;AAc3C,KAbwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA;EACA;EACA;EACA;EACD,CACS,KAAK,KAAK,CAAC;AAErB,KAAI,KAAK,OAAO,OAAO;EAGrB,MAAM,UAAU,MAAM,OAAO,oBAAoB;AACjD,QAAM,IAAI,SAAe,YAAY;AACnC,UAAO,SAAS,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AAClD,QAAI,IAAI;AACR,aAAS;KACT;IACF;;;AAWN,MAAM,iBAAiB;;;;;;;AAQvB,eAAsB,iBAAiB,MAAoC;CAEzE,MAAM,EAAE,KAAK,SAAS,WADF,MAAM,OAAO;AAGjC,KAAI,EAAA,GAAA,QAAA,YAAY,IAAI,EAAE;AACpB,SAAA,GAAA,iBAAA,QAAA,GAAA,UAAA,SAAoB,IAAI,EAAE,EAAE,WAAW,MAAM,CAAC;AAC9C,QAAM,QAAQ,IAAI;;CAGpB,MAAM,SAAS,OAAO,MAAM,oBAAoB,OAAO;CACvD,IAAI,UAAU;CACd,MAAM,aAAa;AACjB,MAAI,QAAS;AACb,YAAU;AACV,MAAI;AACF,UAAO,MAAM;UACP;;AAKV,QAAO,IAAI,SAAsB,SAAS,WAAW;EACnD,MAAM,QAAQ,iBAAiB;AAC7B,YAAS;AACT,SAAM;AACN,0BACE,IAAI,MACF,qEACE,iBAAiB,IAClB,uFAAuF,KAAK,cAC9F,CACF;KACA,eAAe;EAElB,MAAM,SAAS,SAAiB;GAC9B,MAAM,QAAQ,sBAAsB,KAAK;AACzC,OAAI,CAAC,MAAO;AACZ,gBAAa,MAAM;AAEnB,YAAS;AACT,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;EAG/B,MAAM,gBAAgB;AACpB,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;;AAK7B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,YAAS;AACT,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,YAAS;AACT,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
|
@@ -92,6 +92,7 @@ async function startQuickTunnel(port) {
|
|
|
92
92
|
};
|
|
93
93
|
return new Promise((resolve, reject) => {
|
|
94
94
|
const timer = setTimeout(() => {
|
|
95
|
+
cleanup();
|
|
95
96
|
stop();
|
|
96
97
|
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared did not report a tunnel URL within ${URL_TIMEOUT_MS / 1e3}s. Check your network connection, or run \`cloudflared tunnel --url http://localhost:${port}\` manually.`));
|
|
97
98
|
}, URL_TIMEOUT_MS);
|
|
@@ -99,24 +100,29 @@ async function startQuickTunnel(port) {
|
|
|
99
100
|
const found = parseTrycloudflareUrl(line);
|
|
100
101
|
if (!found) return;
|
|
101
102
|
clearTimeout(timer);
|
|
102
|
-
|
|
103
|
-
tunnel.off("stderr", onUrl);
|
|
103
|
+
cleanup();
|
|
104
104
|
resolve({
|
|
105
105
|
url: found,
|
|
106
106
|
stop
|
|
107
107
|
});
|
|
108
108
|
};
|
|
109
|
+
const cleanup = () => {
|
|
110
|
+
tunnel.off("stdout", onUrl);
|
|
111
|
+
tunnel.off("stderr", onUrl);
|
|
112
|
+
};
|
|
109
113
|
tunnel.once("url", onUrl);
|
|
110
114
|
tunnel.on("stdout", onUrl);
|
|
111
115
|
tunnel.on("stderr", onUrl);
|
|
112
116
|
tunnel.once("error", (err) => {
|
|
113
117
|
clearTimeout(timer);
|
|
118
|
+
cleanup();
|
|
114
119
|
stop();
|
|
115
120
|
reject(err);
|
|
116
121
|
});
|
|
117
122
|
tunnel.once("exit", (code) => {
|
|
118
123
|
if (stopped) return;
|
|
119
124
|
clearTimeout(timer);
|
|
125
|
+
cleanup();
|
|
120
126
|
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared exited (code ${code ?? "null"}) before reporting a tunnel URL.`));
|
|
121
127
|
});
|
|
122
128
|
});
|
|
@@ -124,4 +130,4 @@ async function startQuickTunnel(port) {
|
|
|
124
130
|
//#endregion
|
|
125
131
|
export { printTunnelBanner, startQuickTunnel };
|
|
126
132
|
|
|
127
|
-
//# sourceMappingURL=tunnel-
|
|
133
|
+
//# sourceMappingURL=tunnel-D0_TwDNE.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-
|
|
1
|
+
{"version":3,"file":"tunnel-D0_TwDNE.js","names":[],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url);\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.`,\n ),\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,sBAAsB,MAA6B;CACjE,MAAM,IAAI,KAAK,MAAM,iBAAiB;AACtC,QAAO,IAAI,EAAE,KAAK;;AAUpB,MAAM,eAAe;;;;;;;;;AAUrB,SAAgB,sBAAsB,WAA2B;AAC/D,QAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;;;;;;;;AAS7D,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,IAAI;AAc3C,KAbwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA;EACA;EACA;EACA;EACD,CACS,KAAK,KAAK,CAAC;AAErB,KAAI,KAAK,OAAO,OAAO;EAGrB,MAAM,UAAU,MAAM,OAAO,oBAAoB;AACjD,QAAM,IAAI,SAAe,YAAY;AACnC,UAAO,SAAS,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AAClD,QAAI,IAAI;AACR,aAAS;KACT;IACF;;;AAWN,MAAM,iBAAiB;;;;;;;AAQvB,eAAsB,iBAAiB,MAAoC;CAEzE,MAAM,EAAE,KAAK,SAAS,WADF,MAAM,OAAO;AAGjC,KAAI,CAAC,WAAW,IAAI,EAAE;AACpB,QAAM,MAAM,QAAQ,IAAI,EAAE,EAAE,WAAW,MAAM,CAAC;AAC9C,QAAM,QAAQ,IAAI;;CAGpB,MAAM,SAAS,OAAO,MAAM,oBAAoB,OAAO;CACvD,IAAI,UAAU;CACd,MAAM,aAAa;AACjB,MAAI,QAAS;AACb,YAAU;AACV,MAAI;AACF,UAAO,MAAM;UACP;;AAKV,QAAO,IAAI,SAAsB,SAAS,WAAW;EACnD,MAAM,QAAQ,iBAAiB;AAC7B,YAAS;AACT,SAAM;AACN,0BACE,IAAI,MACF,qEACE,iBAAiB,IAClB,uFAAuF,KAAK,cAC9F,CACF;KACA,eAAe;EAElB,MAAM,SAAS,SAAiB;GAC9B,MAAM,QAAQ,sBAAsB,KAAK;AACzC,OAAI,CAAC,MAAO;AACZ,gBAAa,MAAM;AAEnB,YAAS;AACT,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;EAG/B,MAAM,gBAAgB;AACpB,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;;AAK7B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,YAAS;AACT,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,YAAS;AACT,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
package/dist/unplugin/index.cjs
CHANGED
|
@@ -132,7 +132,7 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
|
|
|
132
132
|
console.warn("[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.");
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
|
-
Promise.resolve().then(() => require("../tunnel-
|
|
135
|
+
Promise.resolve().then(() => require("../tunnel-BYP0yRBN.cjs")).then(async ({ startQuickTunnel, printTunnelBanner }) => {
|
|
136
136
|
const t = await startQuickTunnel(port);
|
|
137
137
|
tunnel = t;
|
|
138
138
|
await printTunnelBanner(t.url, { qr: tunnelConfig.qr });
|
package/dist/unplugin/index.js
CHANGED
|
@@ -128,7 +128,7 @@ const aitDevtoolsPlugin = createUnplugin((options) => {
|
|
|
128
128
|
console.warn("[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.");
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
|
-
import("../tunnel-
|
|
131
|
+
import("../tunnel-D0_TwDNE.js").then(async ({ startQuickTunnel, printTunnelBanner }) => {
|
|
132
132
|
const t = await startQuickTunnel(port);
|
|
133
133
|
tunnel = t;
|
|
134
134
|
await printTunnelBanner(t.url, { qr: tunnelConfig.qr });
|
package/dist/unplugin/tunnel.cjs
CHANGED
|
@@ -93,6 +93,7 @@ async function startQuickTunnel(port) {
|
|
|
93
93
|
};
|
|
94
94
|
return new Promise((resolve, reject) => {
|
|
95
95
|
const timer = setTimeout(() => {
|
|
96
|
+
cleanup();
|
|
96
97
|
stop();
|
|
97
98
|
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared did not report a tunnel URL within ${URL_TIMEOUT_MS / 1e3}s. Check your network connection, or run \`cloudflared tunnel --url http://localhost:${port}\` manually.`));
|
|
98
99
|
}, URL_TIMEOUT_MS);
|
|
@@ -100,24 +101,29 @@ async function startQuickTunnel(port) {
|
|
|
100
101
|
const found = parseTrycloudflareUrl(line);
|
|
101
102
|
if (!found) return;
|
|
102
103
|
clearTimeout(timer);
|
|
103
|
-
|
|
104
|
-
tunnel.off("stderr", onUrl);
|
|
104
|
+
cleanup();
|
|
105
105
|
resolve({
|
|
106
106
|
url: found,
|
|
107
107
|
stop
|
|
108
108
|
});
|
|
109
109
|
};
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
tunnel.off("stdout", onUrl);
|
|
112
|
+
tunnel.off("stderr", onUrl);
|
|
113
|
+
};
|
|
110
114
|
tunnel.once("url", onUrl);
|
|
111
115
|
tunnel.on("stdout", onUrl);
|
|
112
116
|
tunnel.on("stderr", onUrl);
|
|
113
117
|
tunnel.once("error", (err) => {
|
|
114
118
|
clearTimeout(timer);
|
|
119
|
+
cleanup();
|
|
115
120
|
stop();
|
|
116
121
|
reject(err);
|
|
117
122
|
});
|
|
118
123
|
tunnel.once("exit", (code) => {
|
|
119
124
|
if (stopped) return;
|
|
120
125
|
clearTimeout(timer);
|
|
126
|
+
cleanup();
|
|
121
127
|
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared exited (code ${code ?? "null"}) before reporting a tunnel URL.`));
|
|
122
128
|
});
|
|
123
129
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.cjs","names":[],"sources":["../../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url);\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n const timer = setTimeout(() => {\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n
|
|
1
|
+
{"version":3,"file":"tunnel.cjs","names":[],"sources":["../../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url);\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.`,\n ),\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,sBAAsB,MAA6B;CACjE,MAAM,IAAI,KAAK,MAAM,iBAAiB;AACtC,QAAO,IAAI,EAAE,KAAK;;AAUpB,MAAM,eAAe;;;;;;;;;AAUrB,SAAgB,sBAAsB,WAA2B;AAC/D,QAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;;;;;;;;AAS7D,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,IAAI;AAc3C,KAbwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA;EACA;EACA;EACA;EACD,CACS,KAAK,KAAK,CAAC;AAErB,KAAI,KAAK,OAAO,OAAO;EAGrB,MAAM,UAAU,MAAM,OAAO,oBAAoB;AACjD,QAAM,IAAI,SAAe,YAAY;AACnC,UAAO,SAAS,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AAClD,QAAI,IAAI;AACR,aAAS;KACT;IACF;;;AAWN,MAAM,iBAAiB;;;;;;;AAQvB,eAAsB,iBAAiB,MAAoC;CAEzE,MAAM,EAAE,KAAK,SAAS,WADF,MAAM,OAAO;AAGjC,KAAI,EAAA,GAAA,QAAA,YAAY,IAAI,EAAE;AACpB,SAAA,GAAA,iBAAA,QAAA,GAAA,UAAA,SAAoB,IAAI,EAAE,EAAE,WAAW,MAAM,CAAC;AAC9C,QAAM,QAAQ,IAAI;;CAGpB,MAAM,SAAS,OAAO,MAAM,oBAAoB,OAAO;CACvD,IAAI,UAAU;CACd,MAAM,aAAa;AACjB,MAAI,QAAS;AACb,YAAU;AACV,MAAI;AACF,UAAO,MAAM;UACP;;AAKV,QAAO,IAAI,SAAsB,SAAS,WAAW;EACnD,MAAM,QAAQ,iBAAiB;AAC7B,YAAS;AACT,SAAM;AACN,0BACE,IAAI,MACF,qEACE,iBAAiB,IAClB,uFAAuF,KAAK,cAC9F,CACF;KACA,eAAe;EAElB,MAAM,SAAS,SAAiB;GAC9B,MAAM,QAAQ,sBAAsB,KAAK;AACzC,OAAI,CAAC,MAAO;AACZ,gBAAa,MAAM;AAEnB,YAAS;AACT,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;EAG/B,MAAM,gBAAgB;AACpB,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;;AAK7B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,YAAS;AACT,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,YAAS;AACT,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
package/dist/unplugin/tunnel.js
CHANGED
|
@@ -92,6 +92,7 @@ async function startQuickTunnel(port) {
|
|
|
92
92
|
};
|
|
93
93
|
return new Promise((resolve, reject) => {
|
|
94
94
|
const timer = setTimeout(() => {
|
|
95
|
+
cleanup();
|
|
95
96
|
stop();
|
|
96
97
|
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared did not report a tunnel URL within ${URL_TIMEOUT_MS / 1e3}s. Check your network connection, or run \`cloudflared tunnel --url http://localhost:${port}\` manually.`));
|
|
97
98
|
}, URL_TIMEOUT_MS);
|
|
@@ -99,24 +100,29 @@ async function startQuickTunnel(port) {
|
|
|
99
100
|
const found = parseTrycloudflareUrl(line);
|
|
100
101
|
if (!found) return;
|
|
101
102
|
clearTimeout(timer);
|
|
102
|
-
|
|
103
|
-
tunnel.off("stderr", onUrl);
|
|
103
|
+
cleanup();
|
|
104
104
|
resolve({
|
|
105
105
|
url: found,
|
|
106
106
|
stop
|
|
107
107
|
});
|
|
108
108
|
};
|
|
109
|
+
const cleanup = () => {
|
|
110
|
+
tunnel.off("stdout", onUrl);
|
|
111
|
+
tunnel.off("stderr", onUrl);
|
|
112
|
+
};
|
|
109
113
|
tunnel.once("url", onUrl);
|
|
110
114
|
tunnel.on("stdout", onUrl);
|
|
111
115
|
tunnel.on("stderr", onUrl);
|
|
112
116
|
tunnel.once("error", (err) => {
|
|
113
117
|
clearTimeout(timer);
|
|
118
|
+
cleanup();
|
|
114
119
|
stop();
|
|
115
120
|
reject(err);
|
|
116
121
|
});
|
|
117
122
|
tunnel.once("exit", (code) => {
|
|
118
123
|
if (stopped) return;
|
|
119
124
|
clearTimeout(timer);
|
|
125
|
+
cleanup();
|
|
120
126
|
reject(/* @__PURE__ */ new Error(`[@ait-co/devtools] cloudflared exited (code ${code ?? "null"}) before reporting a tunnel URL.`));
|
|
121
127
|
});
|
|
122
128
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.js","names":[],"sources":["../../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url);\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n const timer = setTimeout(() => {\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n
|
|
1
|
+
{"version":3,"file":"tunnel.js","names":[],"sources":["../../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n */\nexport function buildLauncherDeepLink(tunnelUrl: string): string {\n return `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url);\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.`,\n ),\n );\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAM,mBAAmB;;;;;;AAOzB,SAAgB,sBAAsB,MAA6B;CACjE,MAAM,IAAI,KAAK,MAAM,iBAAiB;AACtC,QAAO,IAAI,EAAE,KAAK;;AAUpB,MAAM,eAAe;;;;;;;;;AAUrB,SAAgB,sBAAsB,WAA2B;AAC/D,QAAO,GAAG,aAAa,OAAO,mBAAmB,UAAU;;;;;;;;AAS7D,eAAsB,kBACpB,KACA,OAAiC,EAAE,EACpB;CACf,MAAM,MAAM,KAAK,SAAS,MAAc,QAAQ,IAAI,EAAE;CACtD,MAAM,WAAW,sBAAsB,IAAI;AAc3C,KAbwB;EACtB;EACA;EACA,QAAQ;EACR;EACA,wCAAwC;EACxC;EACA;EACA;EACA;EACA;EACA;EACD,CACS,KAAK,KAAK,CAAC;AAErB,KAAI,KAAK,OAAO,OAAO;EAGrB,MAAM,UAAU,MAAM,OAAO,oBAAoB;AACjD,QAAM,IAAI,SAAe,YAAY;AACnC,UAAO,SAAS,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AAClD,QAAI,IAAI;AACR,aAAS;KACT;IACF;;;AAWN,MAAM,iBAAiB;;;;;;;AAQvB,eAAsB,iBAAiB,MAAoC;CAEzE,MAAM,EAAE,KAAK,SAAS,WADF,MAAM,OAAO;AAGjC,KAAI,CAAC,WAAW,IAAI,EAAE;AACpB,QAAM,MAAM,QAAQ,IAAI,EAAE,EAAE,WAAW,MAAM,CAAC;AAC9C,QAAM,QAAQ,IAAI;;CAGpB,MAAM,SAAS,OAAO,MAAM,oBAAoB,OAAO;CACvD,IAAI,UAAU;CACd,MAAM,aAAa;AACjB,MAAI,QAAS;AACb,YAAU;AACV,MAAI;AACF,UAAO,MAAM;UACP;;AAKV,QAAO,IAAI,SAAsB,SAAS,WAAW;EACnD,MAAM,QAAQ,iBAAiB;AAC7B,YAAS;AACT,SAAM;AACN,0BACE,IAAI,MACF,qEACE,iBAAiB,IAClB,uFAAuF,KAAK,cAC9F,CACF;KACA,eAAe;EAElB,MAAM,SAAS,SAAiB;GAC9B,MAAM,QAAQ,sBAAsB,KAAK;AACzC,OAAI,CAAC,MAAO;AACZ,gBAAa,MAAM;AAEnB,YAAS;AACT,WAAQ;IAAE,KAAK;IAAO;IAAM,CAAC;;EAG/B,MAAM,gBAAgB;AACpB,UAAO,IAAI,UAAU,MAAM;AAC3B,UAAO,IAAI,UAAU,MAAM;;AAK7B,SAAO,KAAK,OAAO,MAAM;AACzB,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO,KAAK,UAAU,QAAe;AACnC,gBAAa,MAAM;AACnB,YAAS;AACT,SAAM;AACN,UAAO,IAAI;IACX;AACF,SAAO,KAAK,SAAS,SAAwB;AAC3C,OAAI,QAAS;AACb,gBAAa,MAAM;AACnB,YAAS;AACT,0BACE,IAAI,MACF,+CAA+C,QAAQ,OAAO,kCAC/D,CACF;IACD;GACF"}
|
package/package.json
CHANGED