skillmds 0.1.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/bin/skillmd-mcp.mjs +35 -1
- package/bin/skillmd.mjs +3 -0
- package/dist/cli.js +933 -0
- package/package.json +23 -5
- package/vendor/index.js +297 -0
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillmds",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "SkillMD for agents: the `skillmds` MCP server + the `skillmd` CLI (lint, scan, search, install, publish Agent Skills) from skillmd.com.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"skillmds": "bin/skillmd-mcp.mjs"
|
|
7
|
+
"skillmds": "bin/skillmd-mcp.mjs",
|
|
8
|
+
"skillmd": "bin/skillmd.mjs"
|
|
8
9
|
},
|
|
9
10
|
"keywords": [
|
|
10
11
|
"mcp",
|
|
11
12
|
"model-context-protocol",
|
|
13
|
+
"cli",
|
|
12
14
|
"agent-skills",
|
|
13
15
|
"skill.md",
|
|
14
16
|
"skillmd",
|
|
17
|
+
"lint",
|
|
18
|
+
"validator",
|
|
19
|
+
"sarif",
|
|
15
20
|
"claude",
|
|
16
21
|
"claude-code",
|
|
17
22
|
"cursor",
|
|
@@ -22,9 +27,22 @@
|
|
|
22
27
|
"bugs": { "url": "https://skillmd.com/contact" },
|
|
23
28
|
"author": "SkillMD",
|
|
24
29
|
"license": "MIT",
|
|
25
|
-
"files": ["bin", "README.md"],
|
|
30
|
+
"files": ["bin", "dist", "vendor", "README.md"],
|
|
26
31
|
"engines": { "node": ">=18" },
|
|
27
32
|
"dependencies": {
|
|
28
|
-
"@
|
|
33
|
+
"@clack/prompts": "^0.8.0",
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
35
|
+
"commander": "^12.1.0",
|
|
36
|
+
"giget": "^1.2.0",
|
|
37
|
+
"picocolors": "^1.1.0",
|
|
38
|
+
"yaml": "^2.6.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@skillmd/core": "*",
|
|
42
|
+
"tsup": "^8.3.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"prepublishOnly": "tsup"
|
|
29
47
|
}
|
|
30
48
|
}
|
package/vendor/index.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// ../../packages/core/src/parse.ts
|
|
2
|
+
import { parse as parseYaml } from "yaml";
|
|
3
|
+
var MAX_SKILL_BYTES = 256 * 1024;
|
|
4
|
+
var MAX_DESCRIPTION = 500;
|
|
5
|
+
var MAX_NAME = 120;
|
|
6
|
+
var slugify = (s) => String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "skill";
|
|
7
|
+
function parseSkillMd(raw, type = "single") {
|
|
8
|
+
if (!raw || raw.length > MAX_SKILL_BYTES) return { error: "SKILL.md missing or too large (max 256KB)" };
|
|
9
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
10
|
+
if (!m) return { error: "Missing YAML frontmatter (--- ... ---)" };
|
|
11
|
+
let fm;
|
|
12
|
+
try {
|
|
13
|
+
fm = parseYaml(m[1] ?? "") ?? {};
|
|
14
|
+
} catch {
|
|
15
|
+
return { error: "Invalid YAML frontmatter" };
|
|
16
|
+
}
|
|
17
|
+
const name = String(fm.name ?? "").trim();
|
|
18
|
+
const description = String(fm.description ?? "").replace(/\s+/g, " ").trim();
|
|
19
|
+
if (!name) return { error: "frontmatter 'name' is required" };
|
|
20
|
+
if (!description) return { error: "frontmatter 'description' is required" };
|
|
21
|
+
if (description.length > MAX_DESCRIPTION) return { error: "description must be <= 500 chars" };
|
|
22
|
+
return {
|
|
23
|
+
name: name.slice(0, MAX_NAME),
|
|
24
|
+
description: description.slice(0, MAX_DESCRIPTION),
|
|
25
|
+
license: fm.license ? String(fm.license).slice(0, 64) : null,
|
|
26
|
+
body: m[2] ?? "",
|
|
27
|
+
type
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
var CATEGORY_KEYWORDS = [
|
|
31
|
+
["devops", /docker|kubernetes|k8s|terraform|ci\/cd|deploy|helm|pipeline/i],
|
|
32
|
+
["data", /\b(csv|xlsx|excel|spreadsheet|sql|dataframe|pandas|etl|analytics)\b/i],
|
|
33
|
+
["docs", /\b(docx|word|pdf|markdown|report|documentation|slides?|pptx)\b/i],
|
|
34
|
+
["web", /\b(html|css|react|vue|svelte|frontend|browser|tailwind|a11y)\b/i],
|
|
35
|
+
["ai-ml", /\b(llm|embedding|rag|prompt|agent|fine-?tune|model|mcp)\b/i],
|
|
36
|
+
["security", /\b(security|vulnerabilit|secret|audit|owasp|exploit|xss)\b/i],
|
|
37
|
+
["design", /\b(image|figma|design|video|audio|svg|canvas)\b/i],
|
|
38
|
+
["research", /\b(research|citation|summari[sz]e|knowledge)\b/i],
|
|
39
|
+
["finance", /\b(invoice|account|financ|budget|revenue|business)\b/i],
|
|
40
|
+
["integrations", /\b(api|webhook|graphql|rest|integration|oauth|slack)\b/i],
|
|
41
|
+
["productivity", /\b(email|calendar|todo|task|notion|workflow)\b/i],
|
|
42
|
+
["coding", /\b(code|refactor|test|debug|lint|typescript|python|rust|golang)\b/i]
|
|
43
|
+
];
|
|
44
|
+
function categorize(text) {
|
|
45
|
+
for (const [slug, re] of CATEGORY_KEYWORDS) if (re.test(text)) return slug;
|
|
46
|
+
return "coding";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ../../packages/core/src/security.ts
|
|
50
|
+
var RULES = [
|
|
51
|
+
["executes_scripts", /```(?:sh|bash|python|py|js|ts|ruby|rb)\b|\b(subprocess|os\.system|child_process|exec\()/i],
|
|
52
|
+
["network_calls", /\b(fetch|requests\.|urllib|axios|curl|wget|https?:\/\/|websocket)\b/i],
|
|
53
|
+
["reads_secrets", /\b(api[_-]?key|secret|token|password|os\.environ|process\.env|getenv)\b/i]
|
|
54
|
+
];
|
|
55
|
+
function scanSecurity(body) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
body.split(/\r?\n/).forEach((text, i) => {
|
|
58
|
+
for (const [flag, re] of RULES) {
|
|
59
|
+
if (re.test(text)) findings.push({ flag, line: i + 1, snippet: text.trim().slice(0, 120) });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const flags = [...new Set(findings.map((f) => f.flag))];
|
|
63
|
+
return { flags: flags.length ? flags : ["docs_only"], findings };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ../../packages/core/src/rules.ts
|
|
67
|
+
function utf8Bytes(s) {
|
|
68
|
+
let bytes = 0;
|
|
69
|
+
for (let i = 0; i < s.length; i++) {
|
|
70
|
+
const c = s.charCodeAt(i);
|
|
71
|
+
if (c < 128) bytes += 1;
|
|
72
|
+
else if (c < 2048) bytes += 2;
|
|
73
|
+
else if (c >= 55296 && c <= 56319) {
|
|
74
|
+
bytes += 4;
|
|
75
|
+
i++;
|
|
76
|
+
} else bytes += 3;
|
|
77
|
+
}
|
|
78
|
+
return bytes;
|
|
79
|
+
}
|
|
80
|
+
var diag = (rule, message, line) => ({
|
|
81
|
+
id: rule.id,
|
|
82
|
+
severity: rule.severity,
|
|
83
|
+
message,
|
|
84
|
+
...line !== void 0 ? { line } : {}
|
|
85
|
+
});
|
|
86
|
+
var RULES2 = [
|
|
87
|
+
{
|
|
88
|
+
id: "SK002",
|
|
89
|
+
severity: "error",
|
|
90
|
+
description: "name must be present and \u2264 120 chars",
|
|
91
|
+
check(s, _ctx) {
|
|
92
|
+
const out = [];
|
|
93
|
+
if (!s.name.trim()) out.push(diag(this, "frontmatter 'name' is required"));
|
|
94
|
+
else if (s.name.length > MAX_NAME) out.push(diag(this, `name must be \u2264 ${MAX_NAME} chars`));
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "SK003",
|
|
100
|
+
severity: "error",
|
|
101
|
+
description: "description must be present and \u2264 500 chars",
|
|
102
|
+
check(s, _ctx) {
|
|
103
|
+
const out = [];
|
|
104
|
+
if (!s.description.trim()) out.push(diag(this, "frontmatter 'description' is required"));
|
|
105
|
+
else if (s.description.length > MAX_DESCRIPTION) out.push(diag(this, `description must be \u2264 ${MAX_DESCRIPTION} chars`));
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "SK010",
|
|
111
|
+
severity: "warn",
|
|
112
|
+
description: "description should be \u2265 20 chars (not too terse)",
|
|
113
|
+
check(s, _ctx) {
|
|
114
|
+
return s.description.trim().length > 0 && s.description.trim().length < 20 ? [diag(this, "description is very terse (< 20 chars); add more detail to help discovery")] : [];
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "SK011",
|
|
119
|
+
severity: "warn",
|
|
120
|
+
description: "license should be declared in frontmatter",
|
|
121
|
+
check(s, _ctx) {
|
|
122
|
+
return s.license ? [] : [diag(this, "no 'license' declared in frontmatter")];
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "SK020",
|
|
127
|
+
severity: "warn",
|
|
128
|
+
description: "body should be \u2265 200 chars (not a stub)",
|
|
129
|
+
check(s, _ctx) {
|
|
130
|
+
return s.body.trim().length < 200 ? [diag(this, "body is very short (< 200 chars); this looks like a stub")] : [];
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "SK021",
|
|
135
|
+
severity: "warn",
|
|
136
|
+
description: "body should contain at least one Markdown heading",
|
|
137
|
+
check(s, _ctx) {
|
|
138
|
+
return /^#{1,6}\s/m.test(s.body) ? [] : [diag(this, "body has no Markdown headings; add structure with '#' headings")];
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "SK030",
|
|
143
|
+
severity: "error",
|
|
144
|
+
description: "total SKILL.md size must be \u2264 256KB",
|
|
145
|
+
check(s, _ctx) {
|
|
146
|
+
return utf8Bytes(s.body) > MAX_SKILL_BYTES ? [diag(this, "SKILL.md exceeds 256KB")] : [];
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "SK040",
|
|
151
|
+
severity: "warn",
|
|
152
|
+
description: "name should match its directory/slug",
|
|
153
|
+
check(s, ctx) {
|
|
154
|
+
if (!ctx.slug) return [];
|
|
155
|
+
const expected = slugify(s.name);
|
|
156
|
+
return expected === ctx.slug ? [] : [diag(this, `name slug '${expected}' does not match directory '${ctx.slug}'`)];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
];
|
|
160
|
+
function runRules(skill, ctx = {}) {
|
|
161
|
+
return RULES2.flatMap((r) => r.check(skill, ctx));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ../../packages/core/src/quality.ts
|
|
165
|
+
var WEIGHT = {
|
|
166
|
+
error: 25,
|
|
167
|
+
warn: 8,
|
|
168
|
+
info: 2
|
|
169
|
+
};
|
|
170
|
+
function qualityScore(diagnostics) {
|
|
171
|
+
const penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
172
|
+
return Math.max(0, Math.min(100, 100 - penalty));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ../../packages/core/src/lint.ts
|
|
176
|
+
function lint(raw, opts = {}) {
|
|
177
|
+
const parsed = parseSkillMd(raw, opts.type ?? "single");
|
|
178
|
+
if ("error" in parsed) {
|
|
179
|
+
const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
diagnostics: diagnostics2,
|
|
183
|
+
security: scanSecurity(raw),
|
|
184
|
+
score: qualityScore(diagnostics2)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
188
|
+
const diagnostics = runRules(parsed, ctx);
|
|
189
|
+
const security = scanSecurity(parsed.body);
|
|
190
|
+
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
191
|
+
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ../../packages/core/src/format/text.ts
|
|
195
|
+
var BADGE = { error: "ERROR", warn: " WARN", info: " INFO" };
|
|
196
|
+
function toText(results, opts = {}) {
|
|
197
|
+
const paint = opts.decorate ?? ((t) => t);
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const { file, result } of results) {
|
|
200
|
+
const status = result.ok ? paint("PASS", "ok") : paint("FAIL", "error");
|
|
201
|
+
out.push(`${status} ${file} ${paint(`score ${result.score}/100`, "dim")}`);
|
|
202
|
+
for (const d of result.diagnostics) {
|
|
203
|
+
const where = d.line !== void 0 ? `:${d.line}` : "";
|
|
204
|
+
out.push(` ${paint(BADGE[d.severity], d.severity)} ${d.id}${where} ${d.message}`);
|
|
205
|
+
}
|
|
206
|
+
if (result.security.flags.length && result.security.flags[0] !== "docs_only") {
|
|
207
|
+
out.push(` ${paint("SEC ", "warn")} flags: ${result.security.flags.join(", ")}`);
|
|
208
|
+
}
|
|
209
|
+
out.push("");
|
|
210
|
+
}
|
|
211
|
+
const failed = results.filter((r) => !r.result.ok).length;
|
|
212
|
+
const summary = failed === 0 ? paint(`\u2713 ${results.length} skill(s) passed`, "ok") : paint(`\u2717 ${failed}/${results.length} skill(s) failed`, "error");
|
|
213
|
+
out.push(summary);
|
|
214
|
+
return out.join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ../../packages/core/src/format/json.ts
|
|
218
|
+
function toJson(results) {
|
|
219
|
+
return results.map(({ file, result }) => ({
|
|
220
|
+
file,
|
|
221
|
+
ok: result.ok,
|
|
222
|
+
score: result.score,
|
|
223
|
+
diagnostics: result.diagnostics,
|
|
224
|
+
security: result.security
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ../../packages/core/src/format/sarif.ts
|
|
229
|
+
var SARIF_LEVEL = { error: "error", warn: "warning", info: "note" };
|
|
230
|
+
function toSarif(results) {
|
|
231
|
+
const sarifResults = results.flatMap(
|
|
232
|
+
({ file, result }) => result.diagnostics.map((d) => ({
|
|
233
|
+
ruleId: d.id,
|
|
234
|
+
level: SARIF_LEVEL[d.severity] ?? "note",
|
|
235
|
+
message: { text: d.message },
|
|
236
|
+
locations: [
|
|
237
|
+
{
|
|
238
|
+
physicalLocation: {
|
|
239
|
+
artifactLocation: { uri: file },
|
|
240
|
+
region: { startLine: d.line ?? 1 }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}))
|
|
245
|
+
);
|
|
246
|
+
return {
|
|
247
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
248
|
+
version: "2.1.0",
|
|
249
|
+
runs: [
|
|
250
|
+
{
|
|
251
|
+
tool: {
|
|
252
|
+
driver: {
|
|
253
|
+
name: "skillmd",
|
|
254
|
+
informationUri: "https://skillmd.com",
|
|
255
|
+
rules: RULES2.map((r) => ({
|
|
256
|
+
id: r.id,
|
|
257
|
+
shortDescription: { text: r.description },
|
|
258
|
+
defaultConfiguration: { level: SARIF_LEVEL[r.severity] ?? "note" }
|
|
259
|
+
}))
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
results: sarifResults
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ../../packages/core/src/format/github.ts
|
|
269
|
+
var LEVEL = { error: "error", warn: "warning", info: "notice" };
|
|
270
|
+
function toGithub(results) {
|
|
271
|
+
const lines = [];
|
|
272
|
+
for (const { file, result } of results) {
|
|
273
|
+
for (const d of result.diagnostics) {
|
|
274
|
+
const level = LEVEL[d.severity] ?? "notice";
|
|
275
|
+
const loc = d.line !== void 0 ? `,line=${d.line}` : "";
|
|
276
|
+
lines.push(`::${level} file=${file}${loc},title=${d.id}::${d.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return lines.join("\n");
|
|
280
|
+
}
|
|
281
|
+
export {
|
|
282
|
+
MAX_DESCRIPTION,
|
|
283
|
+
MAX_NAME,
|
|
284
|
+
MAX_SKILL_BYTES,
|
|
285
|
+
RULES2 as RULES,
|
|
286
|
+
categorize,
|
|
287
|
+
lint,
|
|
288
|
+
parseSkillMd,
|
|
289
|
+
qualityScore,
|
|
290
|
+
runRules,
|
|
291
|
+
scanSecurity,
|
|
292
|
+
slugify,
|
|
293
|
+
toGithub,
|
|
294
|
+
toJson,
|
|
295
|
+
toSarif,
|
|
296
|
+
toText
|
|
297
|
+
};
|