skillmds 1.0.8 → 1.0.10
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 +12 -0
- package/dist/cli.js +122 -39
- package/package.json +1 -1
- package/vendor/index.js +18 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.0.10
|
|
6
|
+
|
|
7
|
+
- Quality score is now a composite of spec correctness, completeness, and **security risk** — skills that read secrets, execute scripts, or make network calls score lower (a risky/stub skill is no longer a green 100).
|
|
8
|
+
- Lint output ends with a **Failed** section listing each failure's rule, message, and full path, so problems don't scroll off in a large run.
|
|
9
|
+
- New `--errors-only` flag to show only failing skills.
|
|
10
|
+
- Fixed emoji spacing/overlap; rule ids are grouped by severity per skill.
|
|
11
|
+
- The closer states how many skills failed instead of a vague "Problems found".
|
|
12
|
+
|
|
13
|
+
## 1.0.9
|
|
14
|
+
|
|
15
|
+
- Compact lint output: one line per skill (status dot, score, name, rule-id chips) with a single summary line, instead of a tall multi-row block per skill. Linting a single skill still shows full diagnostic messages.
|
|
16
|
+
|
|
5
17
|
## 1.0.8
|
|
6
18
|
|
|
7
19
|
- `skillmds` with no command in a terminal now opens the interactive CLI menu; it only starts the MCP server when launched over piped stdio (how MCP clients run it). Previously a bare `npx skillmds` in a terminal appeared to hang on the silent MCP server.
|
package/dist/cli.js
CHANGED
|
@@ -157,12 +157,21 @@ function runRules(skill, ctx = {}) {
|
|
|
157
157
|
|
|
158
158
|
// ../../packages/core/src/quality.ts
|
|
159
159
|
var WEIGHT = {
|
|
160
|
-
error:
|
|
161
|
-
warn:
|
|
162
|
-
info:
|
|
160
|
+
error: 30,
|
|
161
|
+
warn: 10,
|
|
162
|
+
info: 3
|
|
163
163
|
};
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
var RISK = {
|
|
165
|
+
docs_only: 0,
|
|
166
|
+
reads_secrets: 25,
|
|
167
|
+
executes_scripts: 15,
|
|
168
|
+
network_calls: 8
|
|
169
|
+
};
|
|
170
|
+
function qualityScore(diagnostics, security) {
|
|
171
|
+
let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
172
|
+
if (security) {
|
|
173
|
+
for (const flag of security.flags) penalty += RISK[flag] ?? 0;
|
|
174
|
+
}
|
|
166
175
|
return Math.max(0, Math.min(100, 100 - penalty));
|
|
167
176
|
}
|
|
168
177
|
|
|
@@ -171,18 +180,19 @@ function lint(raw, opts = {}) {
|
|
|
171
180
|
const parsed = parseSkillMd(raw, opts.type ?? "single");
|
|
172
181
|
if ("error" in parsed) {
|
|
173
182
|
const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
|
|
183
|
+
const security2 = scanSecurity(raw);
|
|
174
184
|
return {
|
|
175
185
|
ok: false,
|
|
176
186
|
diagnostics: diagnostics2,
|
|
177
|
-
security:
|
|
178
|
-
score: qualityScore(diagnostics2)
|
|
187
|
+
security: security2,
|
|
188
|
+
score: qualityScore(diagnostics2, security2)
|
|
179
189
|
};
|
|
180
190
|
}
|
|
181
191
|
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
182
192
|
const diagnostics = runRules(parsed, ctx);
|
|
183
193
|
const security = scanSecurity(parsed.body);
|
|
184
194
|
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
185
|
-
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics) };
|
|
195
|
+
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security) };
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
// ../../packages/core/src/format/text.ts
|
|
@@ -370,40 +380,112 @@ var decorate = (text3, kind) => {
|
|
|
370
380
|
return pc.dim(text3);
|
|
371
381
|
}
|
|
372
382
|
};
|
|
373
|
-
var
|
|
374
|
-
error: pc.
|
|
375
|
-
warn: pc.
|
|
376
|
-
info: pc.
|
|
383
|
+
var SEV_COLOR = {
|
|
384
|
+
error: pc.red,
|
|
385
|
+
warn: pc.yellow,
|
|
386
|
+
info: pc.cyan
|
|
387
|
+
};
|
|
388
|
+
var GLYPH = {
|
|
389
|
+
error: "\u2716",
|
|
390
|
+
warn: "\u26A0",
|
|
391
|
+
info: "\u2139"
|
|
377
392
|
};
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
393
|
+
var SEC_GLYPH = "\u2691";
|
|
394
|
+
var SEV_ORDER = ["error", "warn", "info"];
|
|
395
|
+
function chips(result) {
|
|
396
|
+
const bySev = { error: [], warn: [], info: [] };
|
|
397
|
+
for (const d of result.diagnostics) bySev[d.severity].push(d.id);
|
|
398
|
+
return SEV_ORDER.filter((s) => bySev[s].length).map((s) => `${GLYPH[s]} ${SEV_COLOR[s](bySev[s].join(" "))}`).join(" ");
|
|
399
|
+
}
|
|
400
|
+
var SEC_SHORT = {
|
|
401
|
+
network_calls: "net",
|
|
402
|
+
executes_scripts: "exec",
|
|
403
|
+
reads_secrets: "secrets"
|
|
404
|
+
};
|
|
405
|
+
var scoreColor = (s) => s >= 80 ? pc.green : s >= 50 ? pc.yellow : pc.red;
|
|
406
|
+
function label(file) {
|
|
407
|
+
const parts = file.split(/[\\/]/).filter(Boolean);
|
|
408
|
+
const i = parts.lastIndexOf("SKILL.md");
|
|
409
|
+
return i > 0 ? parts[i - 1] : parts[parts.length - 1] || file;
|
|
410
|
+
}
|
|
411
|
+
function securityFlags(result) {
|
|
412
|
+
return result.security.flags.filter((f) => f !== "docs_only");
|
|
413
|
+
}
|
|
414
|
+
function secCodes(result) {
|
|
415
|
+
return securityFlags(result).map((f) => SEC_SHORT[f] ?? f);
|
|
416
|
+
}
|
|
417
|
+
function statusDot(result) {
|
|
418
|
+
if (!result.ok) return pc.red("\u25CF");
|
|
419
|
+
const attention = result.diagnostics.some((d) => d.severity === "warn") || securityFlags(result).length > 0;
|
|
420
|
+
return attention ? pc.yellow("\u25CF") : pc.green("\u25CF");
|
|
421
|
+
}
|
|
422
|
+
function failureBlock(file, result) {
|
|
423
|
+
const lines = [`${pc.red("\u2716")} ${pc.bold(label(file))}`];
|
|
424
|
+
for (const d of result.diagnostics.filter((x) => x.severity === "error")) {
|
|
425
|
+
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
426
|
+
lines.push(` ${SEV_COLOR.error(d.id)}${loc} ${d.message}`);
|
|
427
|
+
}
|
|
428
|
+
lines.push(` ${pc.dim(file)}`);
|
|
429
|
+
return lines.join("\n");
|
|
430
|
+
}
|
|
431
|
+
function renderOne(file, result) {
|
|
432
|
+
const status = result.ok ? pc.green("\u25CF PASS") : pc.red("\u25CF FAIL");
|
|
433
|
+
const out = [`${status} ${pc.bold(label(file))} ${pc.dim("quality")} ${scoreColor(result.score)(`${result.score}/100`)}`];
|
|
434
|
+
for (const d of result.diagnostics) {
|
|
435
|
+
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
436
|
+
out.push(` ${GLYPH[d.severity]} ${SEV_COLOR[d.severity](d.id)}${loc} ${d.message}`);
|
|
437
|
+
}
|
|
438
|
+
const codes = secCodes(result);
|
|
439
|
+
if (codes.length) out.push(` ${pc.magenta(`${SEC_GLYPH} ${codes.join(", ")}`)}`);
|
|
440
|
+
return out.join("\n");
|
|
383
441
|
}
|
|
384
|
-
function
|
|
442
|
+
function renderMany(results) {
|
|
443
|
+
const names = results.map((r) => label(r.file));
|
|
444
|
+
const w = Math.min(32, Math.max(4, ...names.map((n) => n.length)));
|
|
385
445
|
const out = [];
|
|
386
|
-
|
|
387
|
-
|
|
446
|
+
let anySec = false;
|
|
447
|
+
results.forEach(({ result }, i) => {
|
|
448
|
+
const score = scoreColor(result.score)(String(result.score).padStart(3));
|
|
449
|
+
const nm = names[i];
|
|
450
|
+
const name = (nm.length > w ? nm.slice(0, w - 1) + "\u2026" : nm).padEnd(w);
|
|
451
|
+
const codes = secCodes(result);
|
|
452
|
+
if (codes.length) anySec = true;
|
|
453
|
+
const sec = codes.length ? ` ${pc.magenta(`${SEC_GLYPH} ${codes.join(" ")}`)}` : "";
|
|
454
|
+
out.push(`${statusDot(result)} ${score} ${name} ${chips(result)}${sec}`.trimEnd());
|
|
455
|
+
});
|
|
456
|
+
const failed = results.filter((r) => !r.result.ok).length;
|
|
457
|
+
const warned = results.filter((r) => r.result.ok && r.result.diagnostics.some((d) => d.severity === "warn")).length;
|
|
458
|
+
const clean = results.length - failed - warned;
|
|
459
|
+
const avg = Math.round(results.reduce((s, r) => s + r.result.score, 0) / results.length);
|
|
460
|
+
out.push("");
|
|
461
|
+
const parts = [pc.green(`${clean} clean`)];
|
|
462
|
+
if (warned) parts.push(pc.yellow(`${warned} warn`));
|
|
463
|
+
if (failed) parts.push(pc.red(`${failed} fail`));
|
|
464
|
+
out.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("avg score")} ${scoreColor(avg)(`${avg}/100`)}`);
|
|
465
|
+
if (anySec) out.push(pc.dim("score = spec + completeness + security risk; magenta \u2691 = risky behaviour (net, exec, secrets)"));
|
|
466
|
+
const fails = results.filter((r) => !r.result.ok);
|
|
467
|
+
if (fails.length) {
|
|
388
468
|
out.push("");
|
|
389
|
-
out.push(
|
|
390
|
-
out.push(
|
|
391
|
-
|
|
392
|
-
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
393
|
-
out.push(` ${badge[d.severity]} ${pc.dim(d.id)}${loc} ${d.message}`);
|
|
394
|
-
}
|
|
395
|
-
const flags = result.security.flags.filter((f) => f !== "docs_only");
|
|
396
|
-
if (flags.length) out.push(` ${pc.magenta("\u2691 security")} ${pc.dim(flags.join(", "))}`);
|
|
469
|
+
out.push(pc.red(pc.bold(`Failed (${fails.length}):`)));
|
|
470
|
+
for (const f of fails.slice(0, 25)) out.push(failureBlock(f.file, f.result));
|
|
471
|
+
if (fails.length > 25) out.push(pc.dim(` \u2026and ${fails.length - 25} more \u2014 run with --errors-only to see all`));
|
|
397
472
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
473
|
+
return out.join("\n");
|
|
474
|
+
}
|
|
475
|
+
function renderFailuresView(results) {
|
|
476
|
+
const fails = results.filter((r) => !r.result.ok);
|
|
477
|
+
if (!fails.length) return pc.green(`\u2713 No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
|
|
478
|
+
const out = fails.map((f) => failureBlock(f.file, f.result));
|
|
401
479
|
out.push("");
|
|
402
|
-
out.push(pc.dim(
|
|
403
|
-
const tally = failed === 0 ? pc.green(`\u2713 ${passed} passed`) : `${pc.green(`${passed} passed`)} ${pc.red(`\u2717 ${failed} failed`)}`;
|
|
404
|
-
out.push(`${tally} ${pc.dim("avg")} ${scoreBar(avg)}`);
|
|
480
|
+
out.push(`${pc.red(`${fails.length} failed`)} ${pc.dim(`of ${results.length}`)}`);
|
|
405
481
|
return out.join("\n");
|
|
406
482
|
}
|
|
483
|
+
function renderReport(results, opts = {}) {
|
|
484
|
+
if (results.length === 0) return pc.dim("No skills found.");
|
|
485
|
+
if (opts.errorsOnly) return renderFailuresView(results);
|
|
486
|
+
if (results.length === 1) return renderOne(results[0].file, results[0].result);
|
|
487
|
+
return renderMany(results);
|
|
488
|
+
}
|
|
407
489
|
function exitCodeFor(results, policy = {}) {
|
|
408
490
|
for (const { result } of results) {
|
|
409
491
|
const hasError = result.diagnostics.some((d) => d.severity === "error");
|
|
@@ -449,12 +531,12 @@ async function runLint(target, flags) {
|
|
|
449
531
|
output = toGithub(results);
|
|
450
532
|
break;
|
|
451
533
|
default:
|
|
452
|
-
output = renderReport(results);
|
|
534
|
+
output = renderReport(results, { errorsOnly: flags.errorsOnly });
|
|
453
535
|
}
|
|
454
536
|
return { results, output, exitCode };
|
|
455
537
|
}
|
|
456
538
|
function lintCommand() {
|
|
457
|
-
return new Command("lint").alias("check").description("Validate SKILL.md file(s) against the spec and report a quality score").argument("[target]", "local path or GitHub source", ".").option("--format <fmt>", "text | json | sarif | github", "text").option("--strict", "treat warnings as errors").option("--fail-on-warning", "exit non-zero when warnings exist").option("--fix", "apply safe automatic fixes (e.g. add missing license)").action(async (target, opts) => {
|
|
539
|
+
return new Command("lint").alias("check").description("Validate SKILL.md file(s) against the spec and report a quality score").argument("[target]", "local path or GitHub source", ".").option("--format <fmt>", "text | json | sarif | github", "text").option("--strict", "treat warnings as errors").option("--fail-on-warning", "exit non-zero when warnings exist").option("--fix", "apply safe automatic fixes (e.g. add missing license)").option("--errors-only", "show only skills that failed").action(async (target, opts) => {
|
|
458
540
|
const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
|
|
459
541
|
if (resolve(target) === homedir()) {
|
|
460
542
|
process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
|
|
@@ -468,7 +550,8 @@ function lintCommand() {
|
|
|
468
550
|
const run2 = await runLint(target, opts);
|
|
469
551
|
s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
|
|
470
552
|
console.log(run2.output);
|
|
471
|
-
|
|
553
|
+
const failed = run2.results.filter((r) => !r.result.ok).length;
|
|
554
|
+
p.outro(failed === 0 ? pc2.green("All good") : pc2.red(`${failed} skill${failed === 1 ? "" : "s"} failed \u2014 details above`));
|
|
472
555
|
process.exitCode = run2.exitCode;
|
|
473
556
|
} catch (err) {
|
|
474
557
|
s.stop(pc2.red("No skills found"));
|
|
@@ -540,9 +623,9 @@ function rulesCommand() {
|
|
|
540
623
|
console.log(JSON.stringify(list.map((r) => ({ id: r.id, severity: r.severity, description: r.description })), null, 2));
|
|
541
624
|
return;
|
|
542
625
|
}
|
|
543
|
-
const
|
|
626
|
+
const badge = (sev) => sev === "error" ? pc4.bgRed(pc4.white(" error ")) : sev === "warn" ? pc4.bgYellow(pc4.black(" warn ")) : pc4.bgCyan(pc4.black(" info "));
|
|
544
627
|
console.log(pc4.bold("\nLint rules\n"));
|
|
545
|
-
for (const r of list) console.log(` ${pc4.bold(r.id)} ${
|
|
628
|
+
for (const r of list) console.log(` ${pc4.bold(r.id)} ${badge(r.severity)} ${r.description}`);
|
|
546
629
|
console.log("");
|
|
547
630
|
});
|
|
548
631
|
}
|
package/package.json
CHANGED
package/vendor/index.js
CHANGED
|
@@ -163,12 +163,21 @@ function runRules(skill, ctx = {}) {
|
|
|
163
163
|
|
|
164
164
|
// ../../packages/core/src/quality.ts
|
|
165
165
|
var WEIGHT = {
|
|
166
|
-
error:
|
|
167
|
-
warn:
|
|
168
|
-
info:
|
|
166
|
+
error: 30,
|
|
167
|
+
warn: 10,
|
|
168
|
+
info: 3
|
|
169
169
|
};
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
var RISK = {
|
|
171
|
+
docs_only: 0,
|
|
172
|
+
reads_secrets: 25,
|
|
173
|
+
executes_scripts: 15,
|
|
174
|
+
network_calls: 8
|
|
175
|
+
};
|
|
176
|
+
function qualityScore(diagnostics, security) {
|
|
177
|
+
let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
178
|
+
if (security) {
|
|
179
|
+
for (const flag of security.flags) penalty += RISK[flag] ?? 0;
|
|
180
|
+
}
|
|
172
181
|
return Math.max(0, Math.min(100, 100 - penalty));
|
|
173
182
|
}
|
|
174
183
|
|
|
@@ -177,18 +186,19 @@ function lint(raw, opts = {}) {
|
|
|
177
186
|
const parsed = parseSkillMd(raw, opts.type ?? "single");
|
|
178
187
|
if ("error" in parsed) {
|
|
179
188
|
const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
|
|
189
|
+
const security2 = scanSecurity(raw);
|
|
180
190
|
return {
|
|
181
191
|
ok: false,
|
|
182
192
|
diagnostics: diagnostics2,
|
|
183
|
-
security:
|
|
184
|
-
score: qualityScore(diagnostics2)
|
|
193
|
+
security: security2,
|
|
194
|
+
score: qualityScore(diagnostics2, security2)
|
|
185
195
|
};
|
|
186
196
|
}
|
|
187
197
|
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
188
198
|
const diagnostics = runRules(parsed, ctx);
|
|
189
199
|
const security = scanSecurity(parsed.body);
|
|
190
200
|
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
191
|
-
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics) };
|
|
201
|
+
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security) };
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
// ../../packages/core/src/format/text.ts
|