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),

140 lines (131 loc) • 6.37 kB
#!/usr/bin/env node /** * Every model post must state its license in a section of its own. * * node scripts/check-blog-structure.mjs * * ## Why this section and not a whole skeleton * * The model posts already share a spine — measured across the eleven of them, VRAM appeared * in 11, install in 10, troubleshooting in 10, FAQ in 10. That structure is real and emerged * without a gate, so gating it would be ceremony. * * Licensing was the exception, and not by a little: * * a heading of its own .... 1 / 11 (krea2, and only an H3) * buried in prose or FAQ .. 8 / 11 (between 1 and 8 passing mentions) * absent entirely ......... 2 / 11 (wan-transparent, video-extend-pusa) * * It is also the highest-stakes fact on the page and where both money-costing errors in this * audit were: krea2 invented a 50-seat allowance for a license with a $1M REVENUE gate, and * ltx-2.3 called the LTX-2 Community License "Apache 2.0" three times. Both errors are the * kind a reader acts on commercially. * * A fact that important should not be discoverable only by reading the whole post, and "we * forgot to mention the license" should not be silent. So: one required section, in the page * TOC, checked. * * ## Scope * * A post is model-shaped if it documents a pack. Engineering posts are exempt — they discuss * the software, not a model whose weights carry terms. */ import fs from 'node:fs'; import path from 'node:path'; const ROOT = path.resolve(import.meta.dirname, '..'); const BLOG = path.join(ROOT, 'docs', 'blog'); const PACKS = path.join(ROOT, 'packs'); const packNames = fs.existsSync(PACKS) ? new Set(fs.readdirSync(PACKS).filter((d) => fs.statSync(path.join(PACKS, d)).isDirectory())) : new Set(); /** Posts that document packs but are about the tooling, not a model's weights. */ const EXEMPT = new Set(['installer-packs-that-cant-rot.mdx']); /** * Strip fenced code, following Markdown's actual fence rules. * * A regex backreference demands the closing fence be the SAME LENGTH as the opener, so a * three-backtick block closed with four backticks was never stripped — and a fake * `## Licensing` inside it satisfied this gate. Markdown closes on a fence of the same * character, at least as long as the opener, with nothing after it but whitespace. */ function stripFences(src) { const out = []; let fence = null; // { char, len } for (const line of src.replace(/\r\n/g, '\n').split('\n')) { const m = line.match(/^\s*(`{3,}|~{3,})(.*)$/); if (fence) { if (m && m[1][0] === fence.char && m[1].length >= fence.len && m[2].trim() === '') { fence = null; } continue; } if (m) { fence = { char: m[1][0], len: m[1].length }; continue; } out.push(line); } return out.join('\n'); } let failures = 0; let checked = 0; for (const file of fs.readdirSync(BLOG).filter((f) => f.endsWith('.mdx'))) { if (file === 'index.mdx' || EXEMPT.has(file)) continue; const src = fs.readFileSync(path.join(BLOG, file), 'utf8'); const documentsAPack = // Dots are legal in pack names (ltx-2.3-txt2vid) — see check-blog-staleness for the same fix. [...src.matchAll(/packs\/([a-z0-9][a-z0-9.-]*[a-z0-9])/gi)].some((m) => packNames.has(m[1])) || [...src.matchAll(/`([a-z0-9][a-z0-9.-]*[a-z0-9])`\s+pack/gi)].some((m) => packNames.has(m[1])); if (!documentsAPack) continue; checked++; // Strip fenced code first: a `## Licensing` line inside an example block is not a section, // and matching raw text would let a post satisfy this rule with a code sample. // Comments are stripped BEFORE the heading search, not after. Stripping later found a // `## Licensing` that lived inside `<!-- ... -->` — the heading matched, the opening `<!--` // sat above it and so was never in the measured span, and the trailing `-->` plus the hidden // prose counted as content. A fully commented-out section passed while rendering nothing. // BOTH comment syntaxes. These are .mdx files, where `{/* ... */}` is the native comment and // renders as nothing — stripping only HTML comments left a commented-out `## Licensing` // plus its prose searchable, so the heading and its 30+ characters satisfied the gate while // the reader saw an empty section. const body = stripFences(src) .replace(/<!--[\s\S]*?-->/g, '') .replace(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g, ''); const m = body.match(/^##\s+Licensing\s*$/m); if (!m) { failures++; console.error(` ✗ blog/${file}: no "## Licensing" section`); continue; } // ...and the section must SAY something. An empty heading followed straight by the next // one passes a presence check while telling the reader nothing, which is the failure this // gate exists to prevent — the posts that got licensing wrong were wrong in prose, not in // their table of contents. const after = body.slice(m.index + m[0].length); // HTML comments are not content — they render as nothing. A long // `<!-- TODO: add licensing details -->` would otherwise satisfy the length check while the // reader sees an empty section, which is the exact state this rule exists to catch. const sectionText = after.split(/^##\s+/m)[0].trim(); // 30, not 80. The point is to catch an EMPTY section, and 80 rejected concise correct prose // — "MIT License. Commercial use is permitted without restriction." says everything required // in 61 characters. Length cannot establish semantics either way, so it is set where it // separates "nothing" from "something" and no higher. if (sectionText.length < 30) { failures++; console.error( ` ✗ blog/${file}: "## Licensing" is empty or near-empty (${sectionText.length} chars) — ` + `name the license and say whether commercial use is gated`, ); } } console.log(`${checked} model post(s) checked for a Licensing section`); if (failures) { console.error( `\n${failures} model post(s) do not state their license in a section of their own. Read the ` + `license off the model card — not from another post, and not from memory — then add a ` + `"## Licensing" H2 before the VRAM section. Name the license exactly, and say whether ` + `commercial use is gated and on what (revenue, not seats).`, ); process.exit(1); } console.log('blog structure OK');