skillmds 1.0.21 → 1.1.0
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/CHANGELOG.md +125 -117
- package/LICENSE +21 -0
- package/README.md +25 -6
- package/bin/skillmd-mcp.mjs +308 -328
- package/bin/skillmd.mjs +0 -0
- package/dist/cli.js +42 -351
- package/package.json +33 -20
- package/vendor/index.js +0 -333
package/vendor/index.js
DELETED
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
// ../../packages/core/src/parse.ts
|
|
2
|
-
import { parse as parseYaml } from "yaml";
|
|
3
|
-
var MAX_SKILL_BYTES = 256 * 1024;
|
|
4
|
-
var MAX_DESCRIPTION = 1024;
|
|
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 <= ${MAX_DESCRIPTION} 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 UNTRUSTED_INSTALL_RE = new RegExp(
|
|
51
|
-
[
|
|
52
|
-
/(?:npx|npm|yarn|pnpm|bun)\b[^\n]*--registry[= ]/.source,
|
|
53
|
-
// custom npm registry
|
|
54
|
-
/npm\s+config\s+set\s+registry/.source,
|
|
55
|
-
/pip3?\s+install\b[^\n]*(?:--index-url|--extra-index-url|-i\s+https?:)/.source,
|
|
56
|
-
// custom pip index
|
|
57
|
-
/(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba|z|fi)?sh\b/.source,
|
|
58
|
-
// curl … | sh
|
|
59
|
-
/(?:iwr|irm|invoke-webrequest|invoke-restmethod)\b[^\n|]*\|\s*iex\b/.source,
|
|
60
|
-
// PowerShell pipe-to-exec
|
|
61
|
-
/bash\s+<\(\s*(?:curl|wget)/.source,
|
|
62
|
-
// bash <(curl …)
|
|
63
|
-
/(?:pip3?|npm|npx)\s+(?:install|--yes|-y)?\s*[^\n]*https?:\/\/(?!registry\.npmjs\.org|pypi\.org|files\.pythonhosted\.org)[^\s'"`]+\.(?:whl|tar\.gz|tgz|zip)\b/.source
|
|
64
|
-
// install from raw archive URL
|
|
65
|
-
].join("|"),
|
|
66
|
-
"i"
|
|
67
|
-
);
|
|
68
|
-
var RULES = [
|
|
69
|
-
["untrusted_install", UNTRUSTED_INSTALL_RE],
|
|
70
|
-
["executes_scripts", /```(?:sh|bash|python|py|js|ts|ruby|rb)\b|\b(subprocess|os\.system|child_process|exec\()/i],
|
|
71
|
-
["network_calls", /\b(fetch|requests\.|urllib|axios|curl|wget|https?:\/\/|websocket)\b/i],
|
|
72
|
-
["reads_secrets", /\b(api[_-]?key|secret|token|password|os\.environ|process\.env|getenv)\b/i]
|
|
73
|
-
];
|
|
74
|
-
function scanSecurity(body) {
|
|
75
|
-
const findings = [];
|
|
76
|
-
body.split(/\r?\n/).forEach((text, i) => {
|
|
77
|
-
for (const [flag, re] of RULES) {
|
|
78
|
-
if (re.test(text)) findings.push({ flag, line: i + 1, snippet: text.trim().slice(0, 120) });
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
const flags = [...new Set(findings.map((f) => f.flag))];
|
|
82
|
-
return { flags: flags.length ? flags : ["docs_only"], findings };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ../../packages/core/src/rules.ts
|
|
86
|
-
function utf8Bytes(s) {
|
|
87
|
-
let bytes = 0;
|
|
88
|
-
for (let i = 0; i < s.length; i++) {
|
|
89
|
-
const c = s.charCodeAt(i);
|
|
90
|
-
if (c < 128) bytes += 1;
|
|
91
|
-
else if (c < 2048) bytes += 2;
|
|
92
|
-
else if (c >= 55296 && c <= 56319) {
|
|
93
|
-
bytes += 4;
|
|
94
|
-
i++;
|
|
95
|
-
} else bytes += 3;
|
|
96
|
-
}
|
|
97
|
-
return bytes;
|
|
98
|
-
}
|
|
99
|
-
var diag = (rule, message, line) => ({
|
|
100
|
-
id: rule.id,
|
|
101
|
-
severity: rule.severity,
|
|
102
|
-
message,
|
|
103
|
-
...line !== void 0 ? { line } : {}
|
|
104
|
-
});
|
|
105
|
-
var RULES2 = [
|
|
106
|
-
{
|
|
107
|
-
id: "SK002",
|
|
108
|
-
severity: "error",
|
|
109
|
-
description: "name must be present and \u2264 120 chars",
|
|
110
|
-
check(s, _ctx) {
|
|
111
|
-
const out = [];
|
|
112
|
-
if (!s.name.trim()) out.push(diag(this, "frontmatter 'name' is required"));
|
|
113
|
-
else if (s.name.length > MAX_NAME) out.push(diag(this, `name must be \u2264 ${MAX_NAME} chars`));
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
id: "SK003",
|
|
119
|
-
severity: "error",
|
|
120
|
-
description: "description must be present and \u2264 1024 chars",
|
|
121
|
-
check(s, _ctx) {
|
|
122
|
-
const out = [];
|
|
123
|
-
if (!s.description.trim()) out.push(diag(this, "frontmatter 'description' is required"));
|
|
124
|
-
else if (s.description.length > MAX_DESCRIPTION) out.push(diag(this, `description must be \u2264 ${MAX_DESCRIPTION} chars`));
|
|
125
|
-
return out;
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
id: "SK010",
|
|
130
|
-
severity: "warn",
|
|
131
|
-
description: "description should be \u2265 20 chars (not too terse)",
|
|
132
|
-
check(s, _ctx) {
|
|
133
|
-
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")] : [];
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
id: "SK011",
|
|
138
|
-
severity: "warn",
|
|
139
|
-
description: "license should be declared in frontmatter",
|
|
140
|
-
check(s, _ctx) {
|
|
141
|
-
return s.license ? [] : [diag(this, "no 'license' declared in frontmatter")];
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
id: "SK020",
|
|
146
|
-
severity: "warn",
|
|
147
|
-
description: "body should be \u2265 200 chars (not a stub)",
|
|
148
|
-
check(s, _ctx) {
|
|
149
|
-
return s.body.trim().length < 200 ? [diag(this, "body is very short (< 200 chars); this looks like a stub")] : [];
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
id: "SK021",
|
|
154
|
-
severity: "warn",
|
|
155
|
-
description: "body should contain at least one Markdown heading",
|
|
156
|
-
check(s, _ctx) {
|
|
157
|
-
return /^#{1,6}\s/m.test(s.body) ? [] : [diag(this, "body has no Markdown headings; add structure with '#' headings")];
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
id: "SK030",
|
|
162
|
-
severity: "error",
|
|
163
|
-
description: "total SKILL.md size must be \u2264 256KB",
|
|
164
|
-
check(s, _ctx) {
|
|
165
|
-
return utf8Bytes(s.body) > MAX_SKILL_BYTES ? [diag(this, "SKILL.md exceeds 256KB")] : [];
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
id: "SK040",
|
|
170
|
-
severity: "warn",
|
|
171
|
-
description: "name should match its directory/slug",
|
|
172
|
-
check(s, ctx) {
|
|
173
|
-
if (!ctx.slug) return [];
|
|
174
|
-
const expected = slugify(s.name);
|
|
175
|
-
return expected === ctx.slug ? [] : [diag(this, `name slug '${expected}' does not match directory '${ctx.slug}'`)];
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
];
|
|
179
|
-
function runRules(skill, ctx = {}) {
|
|
180
|
-
return RULES2.flatMap((r) => r.check(skill, ctx));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ../../packages/core/src/quality.ts
|
|
184
|
-
var WEIGHT = {
|
|
185
|
-
error: 30,
|
|
186
|
-
warn: 10,
|
|
187
|
-
info: 3
|
|
188
|
-
};
|
|
189
|
-
var RISK = {
|
|
190
|
-
docs_only: 0,
|
|
191
|
-
reads_secrets: 2,
|
|
192
|
-
network_calls: 1,
|
|
193
|
-
executes_scripts: 5,
|
|
194
|
-
// fetch-and-execute from a non-standard source — the supply-chain pattern the
|
|
195
|
-
// AI verdict floors to "warning"; the heaviest penalty here for the same reason
|
|
196
|
-
untrusted_install: 15
|
|
197
|
-
};
|
|
198
|
-
var VERIFIED_FLOOR = 85;
|
|
199
|
-
function qualityScore(diagnostics, security, opts = {}) {
|
|
200
|
-
let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
201
|
-
if (security) {
|
|
202
|
-
for (const flag of security.flags) penalty += RISK[flag] ?? 0;
|
|
203
|
-
}
|
|
204
|
-
const score = Math.max(0, Math.min(100, 100 - penalty));
|
|
205
|
-
const hasError = diagnostics.some((d) => d.severity === "error");
|
|
206
|
-
return opts.verified && !hasError ? Math.max(score, VERIFIED_FLOOR) : score;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ../../packages/core/src/lint.ts
|
|
210
|
-
function lint(raw, opts = {}) {
|
|
211
|
-
const parsed = parseSkillMd(raw, opts.type ?? "single");
|
|
212
|
-
if ("error" in parsed) {
|
|
213
|
-
const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
|
|
214
|
-
const security2 = scanSecurity(raw);
|
|
215
|
-
return {
|
|
216
|
-
ok: false,
|
|
217
|
-
diagnostics: diagnostics2,
|
|
218
|
-
security: security2,
|
|
219
|
-
score: qualityScore(diagnostics2, security2, { verified: opts.verified })
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
223
|
-
const diagnostics = runRules(parsed, ctx);
|
|
224
|
-
const security = scanSecurity(parsed.body);
|
|
225
|
-
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
226
|
-
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security, { verified: opts.verified }) };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ../../packages/core/src/format/text.ts
|
|
230
|
-
var BADGE = { error: "ERROR", warn: " WARN", info: " INFO" };
|
|
231
|
-
function toText(results, opts = {}) {
|
|
232
|
-
const paint = opts.decorate ?? ((t) => t);
|
|
233
|
-
const out = [];
|
|
234
|
-
for (const { file, result } of results) {
|
|
235
|
-
const status = result.ok ? paint("PASS", "ok") : paint("FAIL", "error");
|
|
236
|
-
out.push(`${status} ${file} ${paint(`score ${result.score}/100`, "dim")}`);
|
|
237
|
-
for (const d of result.diagnostics) {
|
|
238
|
-
const where = d.line !== void 0 ? `:${d.line}` : "";
|
|
239
|
-
out.push(` ${paint(BADGE[d.severity], d.severity)} ${d.id}${where} ${d.message}`);
|
|
240
|
-
}
|
|
241
|
-
if (result.security.flags.length && result.security.flags[0] !== "docs_only") {
|
|
242
|
-
out.push(` ${paint("SEC ", "warn")} flags: ${result.security.flags.join(", ")}`);
|
|
243
|
-
}
|
|
244
|
-
out.push("");
|
|
245
|
-
}
|
|
246
|
-
const failed = results.filter((r) => !r.result.ok).length;
|
|
247
|
-
const summary = failed === 0 ? paint(`\u2713 ${results.length} skill(s) passed`, "ok") : paint(`\u2717 ${failed}/${results.length} skill(s) failed`, "error");
|
|
248
|
-
out.push(summary);
|
|
249
|
-
return out.join("\n");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ../../packages/core/src/format/json.ts
|
|
253
|
-
function toJson(results) {
|
|
254
|
-
return results.map(({ file, result }) => ({
|
|
255
|
-
file,
|
|
256
|
-
ok: result.ok,
|
|
257
|
-
score: result.score,
|
|
258
|
-
diagnostics: result.diagnostics,
|
|
259
|
-
security: result.security
|
|
260
|
-
}));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ../../packages/core/src/format/sarif.ts
|
|
264
|
-
var SARIF_LEVEL = { error: "error", warn: "warning", info: "note" };
|
|
265
|
-
function toSarif(results) {
|
|
266
|
-
const sarifResults = results.flatMap(
|
|
267
|
-
({ file, result }) => result.diagnostics.map((d) => ({
|
|
268
|
-
ruleId: d.id,
|
|
269
|
-
level: SARIF_LEVEL[d.severity] ?? "note",
|
|
270
|
-
message: { text: d.message },
|
|
271
|
-
locations: [
|
|
272
|
-
{
|
|
273
|
-
physicalLocation: {
|
|
274
|
-
artifactLocation: { uri: file },
|
|
275
|
-
region: { startLine: d.line ?? 1 }
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
]
|
|
279
|
-
}))
|
|
280
|
-
);
|
|
281
|
-
return {
|
|
282
|
-
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
283
|
-
version: "2.1.0",
|
|
284
|
-
runs: [
|
|
285
|
-
{
|
|
286
|
-
tool: {
|
|
287
|
-
driver: {
|
|
288
|
-
name: "skillmd",
|
|
289
|
-
informationUri: "https://skillmd.com",
|
|
290
|
-
rules: RULES2.map((r) => ({
|
|
291
|
-
id: r.id,
|
|
292
|
-
shortDescription: { text: r.description },
|
|
293
|
-
defaultConfiguration: { level: SARIF_LEVEL[r.severity] ?? "note" }
|
|
294
|
-
}))
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
|
-
results: sarifResults
|
|
298
|
-
}
|
|
299
|
-
]
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ../../packages/core/src/format/github.ts
|
|
304
|
-
var LEVEL = { error: "error", warn: "warning", info: "notice" };
|
|
305
|
-
function toGithub(results) {
|
|
306
|
-
const lines = [];
|
|
307
|
-
for (const { file, result } of results) {
|
|
308
|
-
for (const d of result.diagnostics) {
|
|
309
|
-
const level = LEVEL[d.severity] ?? "notice";
|
|
310
|
-
const loc = d.line !== void 0 ? `,line=${d.line}` : "";
|
|
311
|
-
lines.push(`::${level} file=${file}${loc},title=${d.id}::${d.message}`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return lines.join("\n");
|
|
315
|
-
}
|
|
316
|
-
export {
|
|
317
|
-
MAX_DESCRIPTION,
|
|
318
|
-
MAX_NAME,
|
|
319
|
-
MAX_SKILL_BYTES,
|
|
320
|
-
RULES2 as RULES,
|
|
321
|
-
UNTRUSTED_INSTALL_RE,
|
|
322
|
-
categorize,
|
|
323
|
-
lint,
|
|
324
|
-
parseSkillMd,
|
|
325
|
-
qualityScore,
|
|
326
|
-
runRules,
|
|
327
|
-
scanSecurity,
|
|
328
|
-
slugify,
|
|
329
|
-
toGithub,
|
|
330
|
-
toJson,
|
|
331
|
-
toSarif,
|
|
332
|
-
toText
|
|
333
|
-
};
|