Spaces:
Running
Running
File size: 8,645 Bytes
4fa5831 51e0a42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | <!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 & 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>
|