import { TOOLS } from "./copilotTools"; /** Lifetime of a bridge token. Short on purpose: it grants full control of the canvas. */ export const TOKEN_TTL_MS = 4 * 60 * 60 * 1000; export const HOSTED_BRIDGE_URL = "https://api.mrfake.name/relay"; export const DEFAULT_BRIDGE_URL = (import.meta.env.VITE_BRIDGE_URL as string | undefined) || HOSTED_BRIDGE_URL; /** Default of older builds (local relay); saved settings with it are moved to the hosted relay. */ export const LEGACY_BRIDGE_URL = "http://127.0.0.1:7433"; /** The hosted relay speaks MCP over HTTP at /mcp; a local gr-canvas-bridge.mjs speaks MCP on stdio. */ export const isHostedRelay = (url: string) => url.replace(/\/+$/, "") === DEFAULT_BRIDGE_URL || url.replace(/\/+$/, "") === HOSTED_BRIDGE_URL; export const appOrigin = () => (typeof location !== "undefined" ? location.origin : "https://gr-canvas.local"); export const docsUrl = () => `${appOrigin()}/llms.txt`; export const bridgeScriptUrl = () => `${appOrigin()}/bridge/gr-canvas-bridge.mjs`; function schemaLine(name: string, schema: Record | undefined, required: string[]): string { const props = (schema?.properties ?? {}) as Record; const keys = Object.keys(props); if (!keys.length) return ` ${name}()`; const args = keys.map((k) => { const p = props[k]; const req = required.includes(k) ? "" : "?"; const desc = p.description ? ` — ${p.description}` : ""; return ` ${k}${req}: ${p.type ?? "any"}${desc}`; }); return ` ${name}(\n${args.join("\n")}\n )`; } /** Plain-text tool reference generated from the live tool definitions, so it never drifts. */ export function toolReference(): string { return Object.values(TOOLS) .map((t) => { const f = t.spec.function; const params = f.parameters as { properties?: Record; required?: string[] } | undefined; return `${schemaLine(f.name, params as Record, params?.required ?? [])}\n [${t.kind}] ${f.description}`; }) .join("\n\n"); } /** Tool manifest sent to the relay (and exposed as /tools and MCP tools/list). */ export function toolManifest() { return Object.values(TOOLS).map((t) => ({ name: t.spec.function.name, description: t.spec.function.description, kind: t.kind, parameters: t.spec.function.parameters })); } export interface DocsOpts { bridgeUrl: string; token: string; expires: number; } /** The whole guide an external agent needs: how to connect + tool reference. */ export function agentDocs(o: DocsOpts): string { const ttl = Math.max(0, Math.round((o.expires - Date.now()) / 60000)); return `# Flow — drive the canvas from your own agent Flow is a node editor where any public Gradio Space on the Hugging Face Hub is a node. A workflow is a graph: input nodes (in:text, in:image, in:audio, in:video, in:file, in:number, in:toggle) feed values into Space endpoint nodes (space:owner/name/endpoint). Links go from an output port to an input port with a compatible type (image, audio, video, file, text, number, boolean, gallery, json; "any" matches anything). Outputs flow between Spaces as URLs. You control the canvas that is open in the user's browser tab through a relay: the tab keeps a connection open to it, and your calls are forwarded into the tab. Keep using the same token; the tab must stay open. ## Connect Relay: ${o.bridgeUrl} Token: ${o.token} (expires in ~${ttl} min; the user can rotate it in Flow → Copilot → Your own agent) Docs: ${docsUrl()} Every request carries the token: Authorization: Bearer ${o.token} GET ${o.bridgeUrl}/status → {"ok":true,"tab":true,"tools":${Object.keys(TOOLS).length}} (tab=false means the user has not connected the tab) GET ${o.bridgeUrl}/tools → JSON tool list with JSON-schema parameters POST ${o.bridgeUrl}/call body {"name":"get_workflow","arguments":{}} → {"ok":true,"result":...} or {"ok":false,"error":"..."} ${isHostedRelay(o.bridgeUrl) ? ` POST ${o.bridgeUrl}/mcp MCP (Streamable HTTP, JSON-RPC): initialize, tools/list, tools/call, ping GET ${o.bridgeUrl}/llms.txt this guide Slow tools (wait_for_run, run-related calls) can take minutes: keep the request open (up to ~10 min). ` : ""} Example: curl -s ${o.bridgeUrl}/call -H "Authorization: Bearer ${o.token}" -H "content-type: application/json" \\ -d '{"name":"search_spaces","arguments":{"query":"remove background","limit":5}}' ## How to work 1. Call get_workflow first so you know the current node ids and which Spaces are loaded. 2. To use a Space: search_spaces → pick one that is running (usable=true, most likes) → add_space to load its endpoints. Read the returned ports before adding nodes. 3. Build with add_node and connect, using the ids you got back. Set sensible widget values with set_values. Then call auto_layout so the graph reads left to right with nothing overlapping; use move_nodes for fine placement (positions and sizes come from get_workflow; x grows right, y grows down). 4. Only call run_workflow when the user asked for a run: it spends their Hugging Face quota. Then call wait_for_run and report the outcome. 5. If something fails, read get_run_status (log + per-node errors) and get_node on the failing node, and quote the actual error text. 6. Node types look like "space:owner/name/endpoint" or "in:text". Never invent types; use ones returned by tools. 7. The user watches the canvas live. Edits are applied immediately, without confirmation, so keep them deliberate. ## Tools ${toolReference()} `; } /** Short prompt to paste into any chat agent. */ export function agentPrompt(o: DocsOpts): string { return `You can control my Flow workflow canvas (a node editor for Hugging Face Gradio Spaces). Read the guide first: ${docsUrl()} Then call tools over HTTP through the relay at ${o.bridgeUrl} with header "Authorization: Bearer ${o.token}": GET /status, GET /tools, and POST /call with {"name": "", "arguments": {...}}. Start with get_workflow. Only run_workflow when I ask (it costs my quota), then wait_for_run.`; } /** Claude Code / Cursor / Claude Desktop style MCP config. */ export function mcpConfig(o: DocsOpts, scriptPath: string): string { if (isHostedRelay(o.bridgeUrl)) return JSON.stringify({ mcpServers: { flow: { type: "http", url: `${o.bridgeUrl}/mcp`, headers: { Authorization: `Bearer ${o.token}` } } } }, null, 2); return JSON.stringify( { mcpServers: { "flow": { command: "node", args: [scriptPath, "--mcp", "--port", String(portOf(o.bridgeUrl))], env: { GR_CANVAS_TOKEN: o.token }, }, }, }, null, 2, ); } /** One-line `claude mcp add` for Claude Code. */ export function claudeCodeCommand(o: DocsOpts, scriptPath: string): string { if (isHostedRelay(o.bridgeUrl)) return `claude mcp add --transport http flow ${o.bridgeUrl}/mcp --header "Authorization: Bearer ${o.token}"`; return `claude mcp add flow -e GR_CANVAS_TOKEN=${o.token} -- node ${scriptPath} --mcp --port ${portOf(o.bridgeUrl)}`; } export function curlExample(o: DocsOpts): string { return `curl -s ${o.bridgeUrl}/call -H "Authorization: Bearer ${o.token}" -H "content-type: application/json" \\\n -d '{"name":"get_workflow","arguments":{}}'`; } export function portOf(url: string): number { try { const u = new URL(url); return Number(u.port) || (u.protocol === "https:" ? 443 : 80); } catch { return 7433; } } export function newToken(): string { const b = new Uint8Array(24); crypto.getRandomValues(b); return "grc_" + btoa(String.fromCharCode(...b)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); }