Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Skill Forge</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script> | |
| <style> | |
| :root { | |
| --bg: #fbfbfd; --fg: #1a1a22; --muted: #6b7280; --card: #fff; | |
| --border: #e5e7eb; --accent: #5b4bd6; --code: #f4f4f7; | |
| --err: #b91c1c; --warn: #b45309; --info: #1d4ed8; --ok: #047857; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --bg: #0f0f14; --fg: #e6e6ec; --muted: #9ca3af; --card: #17171f; | |
| --border: #2a2a35; --accent: #a99bff; --code: #1e1e28; | |
| --err: #f87171; --warn: #fbbf24; --info: #93c5fd; --ok: #6ee7b7; | |
| } | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; background: var(--bg); color: var(--fg); | |
| font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| } | |
| .wrap { max-width: 900px; margin: 0 auto; padding: 32px 20px 64px; } | |
| h1 { font-size: 26px; margin: 0 0 4px; } | |
| .sub { color: var(--muted); margin: 0 0 24px; } | |
| .sub code { background: var(--code); padding: 1px 6px; border-radius: 5px; } | |
| .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 20px; flex-wrap: wrap; } | |
| .tabs button { | |
| background: none; border: 0; padding: 10px 14px; font: inherit; color: var(--muted); | |
| cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; | |
| } | |
| .tabs button.active { color: var(--fg); border-bottom-color: var(--accent); font-weight: 600; } | |
| .panel { display: none; } | |
| .panel.active { display: block; } | |
| label { display: block; font-weight: 600; margin: 14px 0 6px; font-size: 13px; } | |
| textarea, input[type=text] { | |
| width: 100%; background: var(--card); color: var(--fg); border: 1px solid var(--border); | |
| border-radius: 8px; padding: 10px 12px; font: inherit; resize: vertical; | |
| } | |
| textarea.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; } | |
| button.go { | |
| margin-top: 14px; background: var(--accent); color: #fff; border: 0; border-radius: 8px; | |
| padding: 10px 18px; font: inherit; font-weight: 600; cursor: pointer; | |
| } | |
| button.go.ghost { background: transparent; color: var(--accent); border: 1px solid var(--accent); margin-left: 8px; } | |
| .out { | |
| margin-top: 18px; background: var(--card); border: 1px solid var(--border); | |
| border-radius: 10px; padding: 4px 18px; min-height: 40px; | |
| } | |
| .out:empty { display: none; } | |
| .out h2 { font-size: 17px; } | |
| .out ul { padding-left: 20px; } | |
| .out li { margin: 3px 0; } | |
| .out code, pre code { background: var(--code); padding: 1px 5px; border-radius: 4px; font-size: 13px; } | |
| pre { background: var(--code); padding: 14px; border-radius: 8px; overflow-x: auto; } | |
| .foot { margin-top: 40px; color: var(--muted); font-size: 13px; border-top: 1px solid var(--border); padding-top: 16px; } | |
| .foot a { color: var(--accent); } | |
| .badge { display:inline-block; font-size:12px; padding:2px 8px; border-radius:999px; border:1px solid var(--border); color:var(--muted); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>🛠️ Skill Forge</h1> | |
| <p class="sub"> | |
| Validate, lint & scaffold <strong>Agent Skills</strong> (<code>SKILL.md</code>). | |
| Runs entirely in your browser. <span class="badge">static · no upload</span> | |
| </p> | |
| <div class="tabs"> | |
| <button data-tab="validate" class="active">Validate</button> | |
| <button data-tab="lint">Lint description</button> | |
| <button data-tab="scaffold">Scaffold</button> | |
| </div> | |
| <section id="validate" class="panel active"> | |
| <label for="md">SKILL.md</label> | |
| <textarea id="md" class="mono" rows="18"></textarea> | |
| <button class="go" id="v-go">Validate</button> | |
| <button class="go ghost" id="v-sample">Load a broken sample</button> | |
| <div class="out" id="v-out"></div> | |
| </section> | |
| <section id="lint" class="panel"> | |
| <label for="desc">Frontmatter <code>description</code></label> | |
| <textarea id="desc" rows="4">Drafts a release changelog from merged PRs. Use when the user asks to "write the changelog" or prep release notes for a tag.</textarea> | |
| <button class="go" id="l-go">Score it</button> | |
| <div class="out" id="l-out"></div> | |
| </section> | |
| <section id="scaffold" class="panel"> | |
| <label for="s-name">name (kebab-case)</label> | |
| <input type="text" id="s-name" value="terraform-plan-reviewer" /> | |
| <label for="s-desc">description</label> | |
| <textarea id="s-desc" rows="3">Reviews a Terraform plan and summarizes drift and risk.</textarea> | |
| <label for="s-when">when to use (optional)</label> | |
| <input type="text" id="s-when" value='the user runs "terraform plan" and wants a risk summary' /> | |
| <button class="go" id="s-go">Generate SKILL.md</button> | |
| <div class="out" id="s-out"></div> | |
| </section> | |
| <p class="foot"> | |
| Checks the stable, host-agnostic structure: frontmatter parses as YAML · | |
| <code>name</code> kebab-case ≤64 · <code>description</code> present ≤1024 with a | |
| trigger cue · non-empty structured body · relative bundled-file links. Unknown | |
| frontmatter keys are info, not errors. | |
| <br />Need the MCP server / Python API? <code>pip install -r requirements.txt && python app.py</code> | |
| — see <a href="./app.py">app.py</a>. MIT licensed · all original code. | |
| </p> | |
| </div> | |
| <script src="./skillforge.js"></script> | |
| <script> | |
| (function () { | |
| var GOOD = "---\nname: changelog-writer\ndescription: Drafts a release changelog from merged PRs. Use when the user asks to \"write the changelog\", \"summarize what shipped\", or prep release notes for a tag.\n---\n\n# Changelog Writer\n\n## When to use this skill\n- The user is cutting a release and wants notes grouped by type.\n\n## Instructions\n1. Collect merged PRs since the last tag.\n2. Group as Features / Fixes / Internal and write one line each.\n"; | |
| var BROKEN = "---\nname: Changelog_Writer!!\ndescription: makes changelogs\n---\n"; | |
| document.getElementById("md").value = GOOD; | |
| // tiny markdown renderer: headings (##), bold (**x**), inline `code`, - lists | |
| function mdToHtml(md) { | |
| var lines = md.split("\n"), html = [], inList = false; | |
| lines.forEach(function (ln) { | |
| var h = ln.match(/^(#{1,6})\s+(.*)/); | |
| if (h) { if (inList) { html.push("</ul>"); inList = false; } | |
| html.push("<h" + h[1].length + ">" + inline(h[2]) + "</h" + h[1].length + ">"); return; } | |
| var li = ln.match(/^-\s+(.*)/); | |
| if (li) { if (!inList) { html.push("<ul>"); inList = true; } | |
| html.push("<li>" + inline(li[1]) + "</li>"); return; } | |
| if (inList) { html.push("</ul>"); inList = false; } | |
| if (ln.trim() === "") return; | |
| html.push("<p>" + inline(ln) + "</p>"); | |
| }); | |
| if (inList) html.push("</ul>"); | |
| return html.join("\n"); | |
| } | |
| function esc(s) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); } | |
| function inline(s) { | |
| return esc(s).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") | |
| .replace(/`([^`]+)`/g, "<code>$1</code>"); | |
| } | |
| document.querySelectorAll(".tabs button").forEach(function (b) { | |
| b.addEventListener("click", function () { | |
| document.querySelectorAll(".tabs button").forEach(function (x) { x.classList.remove("active"); }); | |
| document.querySelectorAll(".panel").forEach(function (x) { x.classList.remove("active"); }); | |
| b.classList.add("active"); | |
| document.getElementById(b.dataset.tab).classList.add("active"); | |
| }); | |
| }); | |
| document.getElementById("v-go").addEventListener("click", function () { | |
| document.getElementById("v-out").innerHTML = | |
| mdToHtml(SkillForge.validateSkill(document.getElementById("md").value).asMarkdown()); | |
| }); | |
| document.getElementById("v-sample").addEventListener("click", function () { | |
| document.getElementById("md").value = BROKEN; | |
| document.getElementById("v-go").click(); | |
| }); | |
| document.getElementById("l-go").addEventListener("click", function () { | |
| document.getElementById("l-out").innerHTML = | |
| mdToHtml(SkillForge.lintDescription(document.getElementById("desc").value).asMarkdown()); | |
| }); | |
| document.getElementById("s-go").addEventListener("click", function () { | |
| var md = SkillForge.scaffoldSkill( | |
| document.getElementById("s-name").value, | |
| document.getElementById("s-desc").value, | |
| document.getElementById("s-when").value); | |
| var rep = SkillForge.validateSkill(md); | |
| document.getElementById("s-out").innerHTML = | |
| "<pre><code>" + esc(md) + "</code></pre>" + mdToHtml(rep.asMarkdown()); | |
| }); | |
| document.getElementById("v-go").click(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |