Spaces:
Running
Running
| /* | |
| * Skill Forge — browser port of skillforge.py. | |
| * Pure functions; depends only on window.jsyaml (js-yaml UMD). | |
| * Kept deliberately in lockstep with the Python module so behaviour matches. | |
| */ | |
| (function (global) { | |
| "use strict"; | |
| const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; | |
| const NAME_MAX = 64; | |
| const DESC_MAX = 1024; | |
| const KNOWN_KEYS = new Set([ | |
| "name", "description", "license", "allowed-tools", "allowed_tools", | |
| "metadata", "version", "compatible-with", "compatible_with", | |
| ]); | |
| const TRIGGER_CUES = ["use when", "use this", "when the user", "when asked", | |
| "trigger", "for tasks", "helps with", "invoke when"]; | |
| function Report() { | |
| this.ok = true; this.errors = []; this.warnings = []; this.info = []; | |
| } | |
| Report.prototype.err = function (m) { this.errors.push(m); this.ok = false; }; | |
| Report.prototype.warn = function (m) { this.warnings.push(m); }; | |
| Report.prototype.note = function (m) { this.info.push(m); }; | |
| Report.prototype.asMarkdown = function () { | |
| const head = this.ok ? "## ✅ Valid skill" : "## ❌ Invalid skill"; | |
| const out = [head, ""]; | |
| [["Errors", this.errors, "🔴"], | |
| ["Warnings", this.warnings, "🟡"], | |
| ["Info", this.info, "🔵"]].forEach(function (t) { | |
| if (t[1].length) { | |
| out.push("**" + t[0] + "**"); | |
| t[1].forEach(function (x) { out.push("- " + t[2] + " " + x); }); | |
| out.push(""); | |
| } | |
| }); | |
| if (this.ok && !this.warnings.length) out.push("_No issues found._"); | |
| return out.join("\n").trim(); | |
| }; | |
| function splitFrontmatter(text) { | |
| text = text.replace(/^/, ""); | |
| if (!text.startsWith("---")) return [null, text]; | |
| const m = text.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?([\s\S]*)$/); | |
| if (!m) return [null, text]; | |
| return [m[1], m[2]]; | |
| } | |
| function validateSkill(skillMd) { | |
| const r = new Report(); | |
| if (!skillMd || !skillMd.trim()) { r.err("File is empty."); return r; } | |
| const parts = splitFrontmatter(skillMd); | |
| const fmRaw = parts[0], body = parts[1]; | |
| if (fmRaw === null) { | |
| r.err("No YAML frontmatter. SKILL.md must start with a '---' fenced block."); | |
| return r; | |
| } | |
| let fm; | |
| try { fm = global.jsyaml.load(fmRaw) || {}; } | |
| catch (e) { r.err("Frontmatter is not valid YAML: " + e.message); return r; } | |
| if (typeof fm !== "object" || Array.isArray(fm)) { | |
| r.err("Frontmatter must be a YAML mapping (key: value pairs)."); return r; | |
| } | |
| let name = fm.name; | |
| if (name === undefined || name === null || !String(name).trim()) { | |
| r.err("`name` is required in frontmatter."); | |
| } else { | |
| name = String(name).trim(); | |
| if (name.length > NAME_MAX) r.err("`name` is " + name.length + " chars; keep it <= " + NAME_MAX + "."); | |
| if (!NAME_RE.test(name)) r.err("`name` must be kebab-case: lowercase letters, digits, single hyphens."); | |
| } | |
| let desc = fm.description; | |
| if (desc === undefined || desc === null || !String(desc).trim()) { | |
| r.err("`description` is required — it is how an agent decides to use the skill."); | |
| } else { | |
| desc = String(desc).trim(); | |
| if (desc.length > DESC_MAX) r.err("`description` is " + desc.length + " chars; keep it <= " + DESC_MAX + "."); | |
| if (desc.length < 40) r.warn("`description` is very short; say what the skill does *and* when to use it."); | |
| const low = desc.toLowerCase(); | |
| if (!TRIGGER_CUES.some(function (c) { return low.indexOf(c) !== -1; })) | |
| r.warn("`description` has no explicit trigger ('Use when...'); agents may not fire it."); | |
| } | |
| if (!body.trim()) { | |
| r.err("Body is empty. Put the actual instructions after the frontmatter."); | |
| } else { | |
| if (!/^#{1,6}\s/m.test(body) && body.length > 400) | |
| r.warn("Long body with no Markdown headings; add structure for skimmability."); | |
| const links = body.match(/\[[^\]]*\]\(([^)]+)\)/g) || []; | |
| links.forEach(function (raw) { | |
| const link = raw.match(/\(([^)]+)\)/)[1]; | |
| if (/^(https?:\/\/|#|mailto:)/.test(link)) return; | |
| if (link.startsWith("/")) | |
| r.warn("Absolute path link '" + link + "'; use a path relative to the skill folder."); | |
| }); | |
| } | |
| const unknown = Object.keys(fm).filter(function (k) { return !KNOWN_KEYS.has(k); }).sort(); | |
| if (unknown.length) | |
| r.note("Non-standard frontmatter keys (ignored by most hosts): " + unknown.join(", ")); | |
| const at = fm["allowed-tools"] !== undefined ? fm["allowed-tools"] : fm.allowed_tools; | |
| if (at !== undefined && at !== null && !Array.isArray(at) && typeof at !== "string") | |
| r.warn("`allowed-tools` should be a list (or comma string) of tool names."); | |
| return r; | |
| } | |
| function lintDescription(description) { | |
| const r = new Report(); | |
| const d = (description || "").trim(); | |
| if (!d) { r.err("Empty description."); return r; } | |
| let score = 100; | |
| const low = d.toLowerCase(); | |
| const n = d.length; | |
| if (n < 40) { score -= 35; r.warn("Too short (" + n + " chars). Aim for ~150–500."); } | |
| else if (n < 150) { score -= 10; r.warn("A bit short (" + n + " chars); add concrete trigger cases."); } | |
| else if (n > DESC_MAX) { score -= 30; r.err("Over the " + DESC_MAX + "-char limit (" + n + ")."); } | |
| else if (n > 700) { score -= 8; r.warn("Long (" + n + " chars); tighten to the essentials."); } | |
| if (!TRIGGER_CUES.some(function (c) { return low.indexOf(c) !== -1; })) { | |
| score -= 25; | |
| r.warn("No explicit trigger phrase. Add 'Use when the user...' with real examples."); | |
| } | |
| if (/^(this skill|a skill|the skill|this is)/.test(low)) { | |
| score -= 10; | |
| r.warn("Starts with 'This skill...'. Lead with the capability or the trigger."); | |
| } | |
| if (/\b(i|you|we|my|your)\b/.test(low)) { | |
| score -= 6; | |
| r.note("Uses first/second person; third-person reads better in a registry."); | |
| } | |
| const verbs = low.match(/\b(create|build|convert|review|audit|generate|analy[sz]e|extract|summari[sz]e|refactor|debug|validate|lint|format|deploy|test|search|fetch|translate|render|plan)\w*/g); | |
| if (!verbs) { score -= 12; r.warn("No action verbs; name what the skill *does*."); } | |
| if (/(e\.g\.|for example|such as|like )/.test(low)) r.note("Has examples — good for trigger matching."); | |
| else { score -= 8; r.warn("No examples. Concrete phrases ('e.g. \"redesign my landing page\"') help matching."); } | |
| score = Math.max(0, Math.min(100, score)); | |
| const verdict = score >= 80 ? "strong" : score >= 55 ? "usable" : "weak"; | |
| r.note("Trigger score: " + score + "/100 (" + verdict + ")"); | |
| r.ok = score >= 55; | |
| r.score = score; | |
| return r; | |
| } | |
| function scaffoldSkill(name, description, whenToUse) { | |
| name = (name || "my-skill").trim().toLowerCase().replace(/ /g, "-").replace(/[^a-z0-9-]/g, "") || "my-skill"; | |
| let desc = (description || "").split(/\s+/).filter(Boolean).join(" "); | |
| whenToUse = (whenToUse || "").trim(); | |
| if (whenToUse) { | |
| if (desc.toLowerCase().indexOf("use when") === -1) { | |
| desc = desc | |
| ? desc + " Use when " + whenToUse.charAt(0).toLowerCase() + whenToUse.slice(1) | |
| : "Use when " + whenToUse; | |
| } else { | |
| desc = desc + " " + whenToUse; | |
| } | |
| } | |
| if (!desc) { | |
| desc = 'Describe what this skill does and the concrete situations that should ' + | |
| 'trigger it, e.g. "Use when the user asks to ...".'; | |
| } | |
| const title = name.replace(/-/g, " ").replace(/\b\w/g, function (c) { return c.toUpperCase(); }); | |
| return "---\n" + | |
| "name: " + name + "\n" + | |
| "description: " + desc + "\n" + | |
| "---\n\n" + | |
| "# " + title + "\n\n" + | |
| "## When to use this skill\n\n" + | |
| "- Trigger 1 — a concrete user request this handles.\n" + | |
| "- Trigger 2 — another phrasing or situation.\n" + | |
| "- Do **not** use it for: <the near-miss cases that belong elsewhere>.\n\n" + | |
| "## Instructions\n\n" + | |
| "1. First step.\n2. Second step.\n3. What to hand back to the user.\n\n" + | |
| "## Notes\n\n" + | |
| "- Edge cases, gotchas, and links to bundled files (paths relative to this folder).\n"; | |
| } | |
| global.SkillForge = { splitFrontmatter, validateSkill, lintDescription, scaffoldSkill }; | |
| })(typeof window !== "undefined" ? window : globalThis); | |