/** * Device-bound URL emission helper. * * A device-bound URL is one whose intent is "the device running the Maxy * platform needs to open this URL so its local state machine can progress" * โ€” as opposed to a generic external URL that opens wherever the operator * is viewing the chat from. * * The contract: tool results that want a URL to open on the device * embed a fenced markdown code block with language identifier * `maxy-device-url`, containing a JSON payload { url, intent, hostname? }. * The chat UI dispatches on the language identifier (alongside `mermaid`) * and renders the block as a button that navigates the device's VNC * browser via CDP. * * See `.docs/web-chat.md` ยง URL rendering contract for the full spec. * * This module is the single source of truth for the emit-side block * format. Plugin MCP tools import it so every emitter produces an * identical shape. */ export interface DeviceUrlOptions { /** The actual URL the device should navigate to. Must be http(s). */ url: string; /** Short human-readable label shown on the chat UI button (e.g. "Sign in to Cloudflare"). */ intent: string; /** Optional hostname of the device โ€” used in the fallback UI when VNC is unreachable. */ hostname?: string; } export const DEVICE_URL_LANGUAGE = "maxy-device-url"; /** * Build a fenced markdown code block that declares a device-bound URL. * * The returned string is meant to be concatenated into a tool-result * `text` content block. The chat UI parses the fence and renders a button. * When the chat UI is older than this contract (or rendering in a * fallback environment), the block degrades to a readable JSON code * block โ€” no broken link, no silent swallow. * * Throws on invalid input so emit-side bugs surface at call time rather * than producing a malformed block that the render side has to catch. */ export function deviceUrlBlock(opts: DeviceUrlOptions): string { const { url, intent, hostname } = opts; if (typeof url !== "string" || url.length === 0) { throw new Error("[device-url] url is required"); } let parsed: URL; try { parsed = new URL(url); } catch { throw new Error(`[device-url] url is not a valid URL: ${url}`); } if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { throw new Error( `[device-url] url scheme must be http or https (got ${parsed.protocol})`, ); } if (typeof intent !== "string" || intent.length === 0) { throw new Error("[device-url] intent is required and must be a non-empty string"); } if (intent.length > 100) { throw new Error(`[device-url] intent exceeds 100 chars (got ${intent.length})`); } if (hostname !== undefined && typeof hostname !== "string") { throw new Error("[device-url] hostname must be a string if provided"); } const payload: DeviceUrlOptions = hostname ? { url, intent, hostname } : { url, intent }; return "```" + DEVICE_URL_LANGUAGE + "\n" + JSON.stringify(payload) + "\n```"; }