UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

95 lines • 3.63 kB
import { ValidationError } from "../utils/errors.js"; /** * A compact, human/LLM-readable DSL that round-trips losslessly to ComfyUI * API-format JSON. * * Grammar (v0): * <id>: <class_type>[ "title"] # node header (column 0) * <key> = <JSON value> # literal input (string/number/bool/array/object) * <key> <- <srcId>.<outputIndex> # connection input -> [srcId, idx] * # line comment ; blank lines ignored * * Connections use `<-` so they're unambiguous vs JSON-array literals. * v0 preserves node ids, class_type, input order, and _meta.title only * (other _meta keys are not represented). */ function isConnection(value, workflow) { return (Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "number" && Number.isInteger(value[1]) && Object.prototype.hasOwnProperty.call(workflow, value[0])); } function sortIds(ids) { const allNumeric = ids.every((id) => /^\d+$/.test(id)); return [...ids].sort((a, b) => allNumeric ? Number(a) - Number(b) : a.localeCompare(b)); } export function workflowToDsl(workflow) { const lines = []; for (const id of sortIds(Object.keys(workflow))) { const node = workflow[id]; const title = node._meta?.title; lines.push(`${id}: ${node.class_type}${title ? ` ${JSON.stringify(title)}` : ""}`); for (const [key, value] of Object.entries(node.inputs ?? {})) { if (isConnection(value, workflow)) { lines.push(` ${key} <- ${value[0]}.${value[1]}`); } else { lines.push(` ${key} = ${JSON.stringify(value)}`); } } lines.push(""); } return lines.join("\n").trimEnd() + "\n"; } const HEADER_RE = /^(\S+):\s*(\S+)(?:\s+(".*"))?\s*$/; const CONN_RE = /^\s+(\S+)\s*<-\s*(\S+)\.(\d+)\s*$/; const LITERAL_RE = /^\s+(\S+)\s*=\s*(.+)$/; export function dslToWorkflow(dsl) { const workflow = {}; let current; const rawLines = dsl.split("\n"); for (let i = 0; i < rawLines.length; i++) { const line = rawLines[i]; if (line.trim() === "" || line.trim().startsWith("#")) continue; const isIndented = /^\s/.test(line); if (!isIndented) { const m = HEADER_RE.exec(line); if (!m) throw new ValidationError(`Invalid node header on line ${i + 1}: "${line}"`); const [, id, classType, titleJson] = m; const node = { class_type: classType, inputs: {} }; if (titleJson) { node._meta = { title: JSON.parse(titleJson) }; } workflow[id] = node; current = node; continue; } if (!current) { throw new ValidationError(`Input on line ${i + 1} has no preceding node header: "${line}"`); } const conn = CONN_RE.exec(line); if (conn) { const [, key, srcId, idx] = conn; current.inputs[key] = [srcId, Number(idx)]; continue; } const lit = LITERAL_RE.exec(line); if (lit) { const [, key, valueStr] = lit; try { current.inputs[key] = JSON.parse(valueStr); } catch { throw new ValidationError(`Invalid JSON value for "${key}" on line ${i + 1}: ${valueStr}`); } continue; } throw new ValidationError(`Could not parse input on line ${i + 1}: "${line}"`); } return workflow; } //# sourceMappingURL=workflow-dsl.js.map