skillmds 1.0.9 → 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,14 @@
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
+
5
13
  ## 1.0.9
6
14
 
7
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.
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,10 +380,27 @@ var decorate = (text3, kind) => {
370
380
  return pc.dim(text3);
371
381
  }
372
382
  };
383
+ var SEV_COLOR = {
384
+ error: pc.red,
385
+ warn: pc.yellow,
386
+ info: pc.cyan
387
+ };
373
388
  var GLYPH = {
374
- error: pc.red("\u2716"),
375
- warn: pc.yellow("\u26A0"),
376
- info: pc.cyan("\xB7")
389
+ error: "\u2716",
390
+ warn: "\u26A0",
391
+ info: "\u2139"
392
+ };
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"
377
404
  };
378
405
  var scoreColor = (s) => s >= 80 ? pc.green : s >= 50 ? pc.yellow : pc.red;
379
406
  function label(file) {
@@ -381,35 +408,50 @@ function label(file) {
381
408
  const i = parts.lastIndexOf("SKILL.md");
382
409
  return i > 0 ? parts[i - 1] : parts[parts.length - 1] || file;
383
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
+ }
384
417
  function statusDot(result) {
385
418
  if (!result.ok) return pc.red("\u25CF");
386
- return result.diagnostics.some((d) => d.severity === "warn") ? pc.yellow("\u25CF") : pc.green("\u25CF");
419
+ const attention = result.diagnostics.some((d) => d.severity === "warn") || securityFlags(result).length > 0;
420
+ return attention ? pc.yellow("\u25CF") : pc.green("\u25CF");
387
421
  }
388
- function securityFlags(result) {
389
- return result.security.flags.filter((f) => f !== "docs_only");
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");
390
430
  }
391
431
  function renderOne(file, result) {
392
432
  const status = result.ok ? pc.green("\u25CF PASS") : pc.red("\u25CF FAIL");
393
- const out = [`${status} ${pc.bold(label(file))} ${scoreColor(result.score)(`${result.score}/100`)}`];
433
+ const out = [`${status} ${pc.bold(label(file))} ${pc.dim("quality")} ${scoreColor(result.score)(`${result.score}/100`)}`];
394
434
  for (const d of result.diagnostics) {
395
435
  const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
396
- out.push(` ${GLYPH[d.severity]} ${pc.dim(d.id)}${loc} ${d.message}`);
436
+ out.push(` ${GLYPH[d.severity]} ${SEV_COLOR[d.severity](d.id)}${loc} ${d.message}`);
397
437
  }
398
- const flags = securityFlags(result);
399
- if (flags.length) out.push(` ${pc.magenta(`\u2691 ${flags.join(", ")}`)}`);
438
+ const codes = secCodes(result);
439
+ if (codes.length) out.push(` ${pc.magenta(`${SEC_GLYPH} ${codes.join(", ")}`)}`);
400
440
  return out.join("\n");
401
441
  }
402
442
  function renderMany(results) {
403
443
  const names = results.map((r) => label(r.file));
404
- const w = Math.min(34, Math.max(4, ...names.map((n) => n.length)));
444
+ const w = Math.min(32, Math.max(4, ...names.map((n) => n.length)));
405
445
  const out = [];
446
+ let anySec = false;
406
447
  results.forEach(({ result }, i) => {
407
448
  const score = scoreColor(result.score)(String(result.score).padStart(3));
408
- const name = names[i].length > w ? names[i].slice(0, w - 1) + "\u2026" : names[i].padEnd(w);
409
- const issues = result.diagnostics.map((d) => `${GLYPH[d.severity]}${pc.dim(d.id)}`).join(" ");
410
- const flags = securityFlags(result);
411
- const sec = flags.length ? ` ${pc.magenta(`\u2691 ${flags.join(",")}`)}` : "";
412
- out.push(`${statusDot(result)} ${score} ${name} ${issues}${sec}`.trimEnd());
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());
413
455
  });
414
456
  const failed = results.filter((r) => !r.result.ok).length;
415
457
  const warned = results.filter((r) => r.result.ok && r.result.diagnostics.some((d) => d.severity === "warn")).length;
@@ -419,11 +461,28 @@ function renderMany(results) {
419
461
  const parts = [pc.green(`${clean} clean`)];
420
462
  if (warned) parts.push(pc.yellow(`${warned} warn`));
421
463
  if (failed) parts.push(pc.red(`${failed} fail`));
422
- out.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("avg")} ${scoreColor(avg)(`${avg}/100`)}`);
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) {
468
+ out.push("");
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`));
472
+ }
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));
479
+ out.push("");
480
+ out.push(`${pc.red(`${fails.length} failed`)} ${pc.dim(`of ${results.length}`)}`);
423
481
  return out.join("\n");
424
482
  }
425
- function renderReport(results) {
483
+ function renderReport(results, opts = {}) {
426
484
  if (results.length === 0) return pc.dim("No skills found.");
485
+ if (opts.errorsOnly) return renderFailuresView(results);
427
486
  if (results.length === 1) return renderOne(results[0].file, results[0].result);
428
487
  return renderMany(results);
429
488
  }
@@ -472,12 +531,12 @@ async function runLint(target, flags) {
472
531
  output = toGithub(results);
473
532
  break;
474
533
  default:
475
- output = renderReport(results);
534
+ output = renderReport(results, { errorsOnly: flags.errorsOnly });
476
535
  }
477
536
  return { results, output, exitCode };
478
537
  }
479
538
  function lintCommand() {
480
- 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) => {
481
540
  const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
482
541
  if (resolve(target) === homedir()) {
483
542
  process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
@@ -491,7 +550,8 @@ function lintCommand() {
491
550
  const run2 = await runLint(target, opts);
492
551
  s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
493
552
  console.log(run2.output);
494
- 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`));
495
555
  process.exitCode = run2.exitCode;
496
556
  } catch (err) {
497
557
  s.stop(pc2.red("No skills found"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillmds",
3
- "version": "1.0.9",
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