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 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: 25,
161
- warn: 8,
162
- info: 2
160
+ error: 30,
161
+ warn: 10,
162
+ info: 3
163
163
  };
164
- function qualityScore(diagnostics) {
165
- const penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
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: scanSecurity(raw),
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 badge = {
374
- error: pc.bgRed(pc.white(" ERROR ")),
375
- warn: pc.bgYellow(pc.black(" WARN ")),
376
- info: pc.bgCyan(pc.black(" INFO "))
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
- function scoreBar(score) {
379
- const filled = Math.max(0, Math.min(10, Math.round(score / 10)));
380
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
381
- const color = score >= 80 ? pc.green : score >= 50 ? pc.yellow : pc.red;
382
- return `${color(bar)} ${pc.bold(String(score))}${pc.dim("/100")}`;
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 renderReport(results) {
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
- for (const { file, result } of results) {
387
- const status = result.ok ? pc.green("\u25CF") + pc.bold(" PASS") : pc.red("\u25CF") + pc.bold(" FAIL");
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(`${status} ${pc.bold(file)}`);
390
- out.push(` ${scoreBar(result.score)}`);
391
- for (const d of result.diagnostics) {
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
- const failed = results.filter((r) => !r.result.ok).length;
399
- const passed = results.length - failed;
400
- const avg = results.length ? Math.round(results.reduce((s, r) => s + r.result.score, 0) / results.length) : 0;
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("\u2500".repeat(44)));
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
- p.outro(run2.exitCode === 0 ? pc2.green("All good") : pc2.red("Problems found"));
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 badge2 = (sev) => sev === "error" ? pc4.bgRed(pc4.white(" error ")) : sev === "warn" ? pc4.bgYellow(pc4.black(" warn ")) : pc4.bgCyan(pc4.black(" info "));
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)} ${badge2(r.severity)} ${r.description}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillmds",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
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": {
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: 25,
167
- warn: 8,
168
- info: 2
166
+ error: 30,
167
+ warn: 10,
168
+ info: 3
169
169
  };
170
- function qualityScore(diagnostics) {
171
- const penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
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: scanSecurity(raw),
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