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 &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>