skill-forge / index.html
WhySoCodius's picture
Skill Forge: static browser app + Python/MCP port for Agent Skills
4fa5831 verified
Raw
History Blame Contribute Delete
8.65 kB
<!DOCTYPE html>
<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 &amp; 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 &amp;&amp; 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
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>