|
| 1 | +#!/usr/bin/env node |
| 2 | +// Builds the "gitbook" skill for publishing into public-docs: |
| 3 | +// - dist/skill.md a short router (<500 lines) |
| 4 | +// - dist/skill/<name>.md one page per skill, mirrored verbatim from |
| 5 | +// skills/<name>/SKILL.md |
| 6 | +// - dist/skill-manifest.json {dir, title}[] used by update-summary.js |
| 7 | +// |
| 8 | +// Everything the router links to lives on the same origin (gitbook.com) |
| 9 | +// as the router itself — no dependency on an agent being willing or able |
| 10 | +// to fetch a second domain (github.com) to get full instructions. |
| 11 | +// |
| 12 | +// Per the Agent Skills spec (https://agentskills.io/specification): |
| 13 | +// - frontmatter `description` must be <= 1024 characters |
| 14 | +// - SKILL.md should stay under ~500 lines / ~5000 tokens; detailed |
| 15 | +// instructions belong in files loaded on demand, not inlined |
| 16 | +// Each skills/<name>/SKILL.md is already independently under that limit, |
| 17 | +// so mirroring them verbatim as their own pages keeps every page compliant |
| 18 | +// on its own. |
| 19 | +// |
| 20 | +// Deep skills/*/references/** material (~25k lines total) stays linked |
| 21 | +// back to GitHub — the one tier where asking for a domain hop is fine, |
| 22 | +// since it's optional depth most agents won't need. |
| 23 | + |
| 24 | +const fs = require("fs"); |
| 25 | +const path = require("path"); |
| 26 | + |
| 27 | +const ROOT = path.resolve(__dirname, ".."); |
| 28 | +const SKILLS_DIR = path.join(ROOT, "skills"); |
| 29 | +const DIST_DIR = path.join(ROOT, "dist"); |
| 30 | +const SKILLS_OUT_DIR = path.join(DIST_DIR, "skill"); |
| 31 | +const REPO_URL = "https://github.com/GitbookIO/gitbook-skills"; |
| 32 | +const DOCS_BASE_URL = "https://gitbook.com/docs"; |
| 33 | +const MAX_DESCRIPTION_LENGTH = 1024; |
| 34 | +const RECOMMENDED_MAX_LINES = 500; |
| 35 | + |
| 36 | +// Order matches the README's "Available Skills" table. |
| 37 | +const SKILLS = [ |
| 38 | + { dir: "write-docs", title: "Write & Edit Docs" }, |
| 39 | + { dir: "configure-site", title: "Configure a Site" }, |
| 40 | + { dir: "write-openapi", title: "Write OpenAPI Reference Docs" }, |
| 41 | + { dir: "cr-create", title: "Create & Manage Change Requests" }, |
| 42 | + { dir: "cr-review", title: "Review Change Requests" }, |
| 43 | + { dir: "build-integration", title: "Build an Integration" }, |
| 44 | +]; |
| 45 | + |
| 46 | +const UMBRELLA_DESCRIPTION = |
| 47 | + "Work with GitBook end-to-end: author and format GitBook-flavored Markdown pages and blocks " + |
| 48 | + "(hints, tabs, steppers); design, scaffold, and configure documentation sites via the GitBook " + |
| 49 | + "REST API and Git Sync; generate and troubleshoot OpenAPI/Swagger API reference docs; create, " + |
| 50 | + "push content to, and manage change requests and reviews over the REST API; and build GitBook " + |
| 51 | + "integrations (custom blocks, ContentKit UI, events, OAuth). Use whenever a task involves " + |
| 52 | + "GitBook: writing or editing docs, SUMMARY.md/.gitbook.yaml, site structure, OpenAPI " + |
| 53 | + "references, change-request review flows, or building a GitBook app/integration."; |
| 54 | + |
| 55 | +function parseTopLevelYaml(text) { |
| 56 | + const lines = text.split("\n"); |
| 57 | + const result = {}; |
| 58 | + for (let i = 0; i < lines.length; i++) { |
| 59 | + const line = lines[i]; |
| 60 | + if (!/^\S/.test(line)) continue; // only top-level (unindented) keys |
| 61 | + const m = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/); |
| 62 | + if (!m) continue; |
| 63 | + const key = m[1]; |
| 64 | + let rest = m[2].trim(); |
| 65 | + if (rest === ">-" || rest === ">" || rest === "|-" || rest === "|") { |
| 66 | + const collected = []; |
| 67 | + let j = i + 1; |
| 68 | + while (j < lines.length && (lines[j] === "" || /^\s+/.test(lines[j]))) { |
| 69 | + if (lines[j].trim() !== "") collected.push(lines[j].trim()); |
| 70 | + j++; |
| 71 | + } |
| 72 | + result[key] = collected.join(" ").trim(); |
| 73 | + i = j - 1; |
| 74 | + } else { |
| 75 | + if ( |
| 76 | + (rest.startsWith('"') && rest.endsWith('"')) || |
| 77 | + (rest.startsWith("'") && rest.endsWith("'")) |
| 78 | + ) { |
| 79 | + rest = rest.slice(1, -1); |
| 80 | + } |
| 81 | + result[key] = rest; |
| 82 | + } |
| 83 | + } |
| 84 | + return result; |
| 85 | +} |
| 86 | + |
| 87 | +function wrapFolded(text, indent = " ", width = 100) { |
| 88 | + const words = text.split(/\s+/); |
| 89 | + const lines = []; |
| 90 | + let current = ""; |
| 91 | + for (const word of words) { |
| 92 | + if ((current + " " + word).trim().length > width) { |
| 93 | + lines.push(current.trim()); |
| 94 | + current = word; |
| 95 | + } else { |
| 96 | + current = (current + " " + word).trim(); |
| 97 | + } |
| 98 | + } |
| 99 | + if (current) lines.push(current.trim()); |
| 100 | + return lines.map((l) => indent + l).join("\n"); |
| 101 | +} |
| 102 | + |
| 103 | +function parseSkillFile(dir) { |
| 104 | + const filePath = path.join(SKILLS_DIR, dir, "SKILL.md"); |
| 105 | + const raw = fs.readFileSync(filePath, "utf8"); |
| 106 | + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); |
| 107 | + if (!match) throw new Error(`No frontmatter found in ${filePath}`); |
| 108 | + const frontmatter = parseTopLevelYaml(match[1]); |
| 109 | + return { |
| 110 | + name: frontmatter.name || dir, |
| 111 | + description: frontmatter.description || "", |
| 112 | + raw, |
| 113 | + }; |
| 114 | +} |
| 115 | + |
| 116 | +function main() { |
| 117 | + if (UMBRELLA_DESCRIPTION.length > MAX_DESCRIPTION_LENGTH) { |
| 118 | + throw new Error( |
| 119 | + `Combined description is ${UMBRELLA_DESCRIPTION.length} chars, over the ${MAX_DESCRIPTION_LENGTH}-char spec limit` |
| 120 | + ); |
| 121 | + } |
| 122 | + |
| 123 | + const parsed = SKILLS.map((s) => ({ ...s, ...parseSkillFile(s.dir) })); |
| 124 | + |
| 125 | + fs.mkdirSync(SKILLS_OUT_DIR, { recursive: true }); |
| 126 | + for (const s of parsed) { |
| 127 | + const outPath = path.join(SKILLS_OUT_DIR, `${s.dir}.md`); |
| 128 | + fs.writeFileSync(outPath, s.raw); |
| 129 | + const lines = s.raw.split("\n").length; |
| 130 | + if (lines > RECOMMENDED_MAX_LINES) { |
| 131 | + console.warn( |
| 132 | + `Warning: skills/${s.dir}/SKILL.md is ${lines} lines, over the spec's recommended ${RECOMMENDED_MAX_LINES}-line ceiling` |
| 133 | + ); |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + const entries = parsed |
| 138 | + .map((s) => { |
| 139 | + const pageUrl = `${DOCS_BASE_URL}/skill/${s.dir}.md`; |
| 140 | + return `### ${s.title}\n\n${s.description}\n\nFull instructions: [${pageUrl}](${pageUrl})`; |
| 141 | + }) |
| 142 | + .join("\n\n"); |
| 143 | + |
| 144 | + const routerOut = `--- |
| 145 | +name: gitbook |
| 146 | +description: >- |
| 147 | +${wrapFolded(UMBRELLA_DESCRIPTION)} |
| 148 | +--- |
| 149 | +
|
| 150 | +{% hint style="info" %} |
| 151 | +This page is generated automatically from [GitbookIO/gitbook-skills](${REPO_URL}). Don't edit it directly — edit the source skills there instead. |
| 152 | +{% endhint %} |
| 153 | +
|
| 154 | +# GitBook |
| 155 | +
|
| 156 | +GitBook's skill for AI coding agents, covering six areas of GitBook work. Each section below is one of the six skills in [gitbook-skills](${REPO_URL}) — read its description to see if it matches your task, then fetch its linked page for full instructions before acting. Each skill's instructions link out to further reference material (full block syntax, API payloads, troubleshooting) in [gitbook-skills](${REPO_URL}) under \`skills/<name>/references/\` when you need more depth than the top-level instructions. |
| 157 | +
|
| 158 | +${entries} |
| 159 | +`; |
| 160 | + |
| 161 | + const routerLines = routerOut.split("\n").length; |
| 162 | + if (routerLines > RECOMMENDED_MAX_LINES) { |
| 163 | + console.warn( |
| 164 | + `Warning: generated skill.md is ${routerLines} lines, over the spec's recommended ${RECOMMENDED_MAX_LINES}-line ceiling` |
| 165 | + ); |
| 166 | + } |
| 167 | + |
| 168 | + fs.writeFileSync(path.join(DIST_DIR, "skill.md"), routerOut); |
| 169 | + fs.writeFileSync( |
| 170 | + path.join(DIST_DIR, "skill-manifest.json"), |
| 171 | + JSON.stringify( |
| 172 | + parsed.map(({ dir, title }) => ({ dir, title })), |
| 173 | + null, |
| 174 | + 2 |
| 175 | + ) |
| 176 | + ); |
| 177 | + |
| 178 | + console.log(`Wrote dist/skill.md (${routerOut.length} bytes, ${routerLines} lines)`); |
| 179 | + console.log(`Wrote ${parsed.length} skill pages to dist/skill/`); |
| 180 | +} |
| 181 | + |
| 182 | +main(); |
0 commit comments