#!/usr/bin/env node /** * Render graphviz diagrams from a skill's SKILL.md to SVG files. * * Usage: * ./render-graphs.js # Render each diagram separately * ./render-graphs.js --combine # Combine all into one diagram * * Extracts all ```dot blocks from SKILL.md and renders to SVG. * Useful for helping your human partner visualize the process flows. * * Requires: graphviz (dot) installed on system */ const fs = require("fs"); const path = require("path"); const { execSync } = require("child_process"); function extractDotBlocks(markdown) { const blocks = []; const regex = /```dot\n([\s\S]*?)```/g; let match; while ((match = regex.exec(markdown)) !== null) { const content = match[1].trim(); // Extract digraph name const nameMatch = content.match(/digraph\s+(\w+)/); const name = nameMatch ? nameMatch[1] : `graph_${blocks.length + 1}`; blocks.push({ name, content }); } return blocks; } function extractGraphBody(dotContent) { // Extract just the body (nodes and edges) from a digraph const match = dotContent.match(/digraph\s+\w+\s*\{([\s\S]*)\}/); if (!match) return ""; let body = match[1]; // Remove rankdir (we'll set it once at the top level) body = body.replace(/^\s*rankdir\s*=\s*\w+\s*;?\s*$/gm, ""); return body.trim(); } function combineGraphs(blocks, skillName) { const bodies = blocks.map((block, i) => { const body = extractGraphBody(block.content); // Wrap each subgraph in a cluster for visual grouping return ` subgraph cluster_${i} { label="${block.name}"; ${body .split("\n") .map((line) => " " + line) .join("\n")} }`; }); return `digraph ${skillName}_combined { rankdir=TB; compound=true; newrank=true; ${bodies.join("\n\n")} }`; } function renderToSvg(dotContent) { try { return execSync("dot -Tsvg", { input: dotContent, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024, }); } catch (err) { console.error("Error running dot:", err.message); if (err.stderr) console.error(err.stderr.toString()); return null; } } function main() { const args = process.argv.slice(2); const combine = args.includes("--combine"); const skillDirArg = args.find((a) => !a.startsWith("--")); if (!skillDirArg) { console.error("Usage: render-graphs.js [--combine]"); console.error(""); console.error("Options:"); console.error(" --combine Combine all diagrams into one SVG"); console.error(""); console.error("Example:"); console.error(" ./render-graphs.js ../single-flow-task-execution"); console.error( " ./render-graphs.js ../single-flow-task-execution --combine", ); process.exit(1); } const skillDir = path.resolve(skillDirArg); const skillFile = path.join(skillDir, "SKILL.md"); const skillName = path.basename(skillDir).replace(/-/g, "_"); if (!fs.existsSync(skillFile)) { console.error(`Error: ${skillFile} not found`); process.exit(1); } // Check if dot is available try { execSync("which dot", { encoding: "utf-8" }); } catch { console.error("Error: graphviz (dot) not found. Install with:"); console.error(" brew install graphviz # macOS"); console.error(" apt install graphviz # Linux"); process.exit(1); } const markdown = fs.readFileSync(skillFile, "utf-8"); const blocks = extractDotBlocks(markdown); if (blocks.length === 0) { console.log("No ```dot blocks found in", skillFile); process.exit(0); } console.log( `Found ${blocks.length} diagram(s) in ${path.basename(skillDir)}/SKILL.md`, ); const outputDir = path.join(skillDir, "diagrams"); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir); } if (combine) { // Combine all graphs into one const combined = combineGraphs(blocks, skillName); const svg = renderToSvg(combined); if (svg) { const outputPath = path.join(outputDir, `${skillName}_combined.svg`); fs.writeFileSync(outputPath, svg); console.log(` Rendered: ${skillName}_combined.svg`); // Also write the dot source for debugging const dotPath = path.join(outputDir, `${skillName}_combined.dot`); fs.writeFileSync(dotPath, combined); console.log(` Source: ${skillName}_combined.dot`); } else { console.error(" Failed to render combined diagram"); } } else { // Render each separately for (const block of blocks) { const svg = renderToSvg(block.content); if (svg) { const outputPath = path.join(outputDir, `${block.name}.svg`); fs.writeFileSync(outputPath, svg); console.log(` Rendered: ${block.name}.svg`); } else { console.error(` Failed: ${block.name}`); } } } console.log(`\nOutput: ${outputDir}/`); } main();