skillmds 1.0.9 → 1.0.11

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.11
6
+
7
+ - Security flags render with full readable labels, each with its own marker: `⚑ Executes Scripts ⚑ Calls Network ⚑ Reads Secrets`.
8
+
9
+ ## 1.0.10
10
+
11
+ - 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).
12
+ - 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.
13
+ - New `--errors-only` flag to show only failing skills.
14
+ - Fixed emoji spacing/overlap; rule ids are grouped by severity per skill.
15
+ - The closer states how many skills failed instead of a vague "Problems found".
16
+
5
17
  ## 1.0.9
6
18
 
7
19
  - 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,46 +380,80 @@ 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"
377
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_LABEL = {
401
+ network_calls: "Calls Network",
402
+ executes_scripts: "Executes Scripts",
403
+ reads_secrets: "Reads Secrets"
404
+ };
405
+ var titleCase = (flag) => flag.split("_").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
378
406
  var scoreColor = (s) => s >= 80 ? pc.green : s >= 50 ? pc.yellow : pc.red;
379
407
  function label(file) {
380
408
  const parts = file.split(/[\\/]/).filter(Boolean);
381
409
  const i = parts.lastIndexOf("SKILL.md");
382
410
  return i > 0 ? parts[i - 1] : parts[parts.length - 1] || file;
383
411
  }
412
+ function securityFlags(result) {
413
+ return result.security.flags.filter((f) => f !== "docs_only");
414
+ }
415
+ function secLabels(result) {
416
+ const flags = securityFlags(result);
417
+ return flags.map((f) => pc.magenta(`${SEC_GLYPH} ${SEC_LABEL[f] ?? titleCase(f)}`)).join(" ");
418
+ }
384
419
  function statusDot(result) {
385
420
  if (!result.ok) return pc.red("\u25CF");
386
- return result.diagnostics.some((d) => d.severity === "warn") ? pc.yellow("\u25CF") : pc.green("\u25CF");
421
+ const attention = result.diagnostics.some((d) => d.severity === "warn") || securityFlags(result).length > 0;
422
+ return attention ? pc.yellow("\u25CF") : pc.green("\u25CF");
387
423
  }
388
- function securityFlags(result) {
389
- return result.security.flags.filter((f) => f !== "docs_only");
424
+ function failureBlock(file, result) {
425
+ const lines = [`${pc.red("\u2716")} ${pc.bold(label(file))}`];
426
+ for (const d of result.diagnostics.filter((x) => x.severity === "error")) {
427
+ const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
428
+ lines.push(` ${SEV_COLOR.error(d.id)}${loc} ${d.message}`);
429
+ }
430
+ lines.push(` ${pc.dim(file)}`);
431
+ return lines.join("\n");
390
432
  }
391
433
  function renderOne(file, result) {
392
434
  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`)}`];
435
+ const out = [`${status} ${pc.bold(label(file))} ${pc.dim("quality")} ${scoreColor(result.score)(`${result.score}/100`)}`];
394
436
  for (const d of result.diagnostics) {
395
437
  const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
396
- out.push(` ${GLYPH[d.severity]} ${pc.dim(d.id)}${loc} ${d.message}`);
438
+ out.push(` ${GLYPH[d.severity]} ${SEV_COLOR[d.severity](d.id)}${loc} ${d.message}`);
397
439
  }
398
- const flags = securityFlags(result);
399
- if (flags.length) out.push(` ${pc.magenta(`\u2691 ${flags.join(", ")}`)}`);
440
+ const sec = secLabels(result);
441
+ if (sec) out.push(` ${sec}`);
400
442
  return out.join("\n");
401
443
  }
402
444
  function renderMany(results) {
403
445
  const names = results.map((r) => label(r.file));
404
- const w = Math.min(34, Math.max(4, ...names.map((n) => n.length)));
446
+ const w = Math.min(32, Math.max(4, ...names.map((n) => n.length)));
405
447
  const out = [];
448
+ let anySec = false;
406
449
  results.forEach(({ result }, i) => {
407
450
  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());
451
+ const nm = names[i];
452
+ const name = (nm.length > w ? nm.slice(0, w - 1) + "\u2026" : nm).padEnd(w);
453
+ const secStr = secLabels(result);
454
+ if (secStr) anySec = true;
455
+ const sec = secStr ? ` ${secStr}` : "";
456
+ out.push(`${statusDot(result)} ${score} ${name} ${chips(result)}${sec}`.trimEnd());
413
457
  });
414
458
  const failed = results.filter((r) => !r.result.ok).length;
415
459
  const warned = results.filter((r) => r.result.ok && r.result.diagnostics.some((d) => d.severity === "warn")).length;
@@ -419,11 +463,28 @@ function renderMany(results) {
419
463
  const parts = [pc.green(`${clean} clean`)];
420
464
  if (warned) parts.push(pc.yellow(`${warned} warn`));
421
465
  if (failed) parts.push(pc.red(`${failed} fail`));
422
- out.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("avg")} ${scoreColor(avg)(`${avg}/100`)}`);
466
+ out.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("avg score")} ${scoreColor(avg)(`${avg}/100`)}`);
467
+ if (anySec) out.push(pc.dim(`${SEC_GLYPH} = risky behaviour, factored into the score alongside spec correctness and completeness`));
468
+ const fails = results.filter((r) => !r.result.ok);
469
+ if (fails.length) {
470
+ out.push("");
471
+ out.push(pc.red(pc.bold(`Failed (${fails.length}):`)));
472
+ for (const f of fails.slice(0, 25)) out.push(failureBlock(f.file, f.result));
473
+ if (fails.length > 25) out.push(pc.dim(` \u2026and ${fails.length - 25} more \u2014 run with --errors-only to see all`));
474
+ }
475
+ return out.join("\n");
476
+ }
477
+ function renderFailuresView(results) {
478
+ const fails = results.filter((r) => !r.result.ok);
479
+ if (!fails.length) return pc.green(`\u2713 No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
480
+ const out = fails.map((f) => failureBlock(f.file, f.result));
481
+ out.push("");
482
+ out.push(`${pc.red(`${fails.length} failed`)} ${pc.dim(`of ${results.length}`)}`);
423
483
  return out.join("\n");
424
484
  }
425
- function renderReport(results) {
485
+ function renderReport(results, opts = {}) {
426
486
  if (results.length === 0) return pc.dim("No skills found.");
487
+ if (opts.errorsOnly) return renderFailuresView(results);
427
488
  if (results.length === 1) return renderOne(results[0].file, results[0].result);
428
489
  return renderMany(results);
429
490
  }
@@ -472,12 +533,12 @@ async function runLint(target, flags) {
472
533
  output = toGithub(results);
473
534
  break;
474
535
  default:
475
- output = renderReport(results);
536
+ output = renderReport(results, { errorsOnly: flags.errorsOnly });
476
537
  }
477
538
  return { results, output, exitCode };
478
539
  }
479
540
  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) => {
541
+ 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
542
  const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
482
543
  if (resolve(target) === homedir()) {
483
544
  process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
@@ -491,7 +552,8 @@ function lintCommand() {
491
552
  const run2 = await runLint(target, opts);
492
553
  s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
493
554
  console.log(run2.output);
494
- p.outro(run2.exitCode === 0 ? pc2.green("All good") : pc2.red("Problems found"));
555
+ const failed = run2.results.filter((r) => !r.result.ok).length;
556
+ p.outro(failed === 0 ? pc2.green("All good") : pc2.red(`${failed} skill${failed === 1 ? "" : "s"} failed \u2014 details above`));
495
557
  process.exitCode = run2.exitCode;
496
558
  } catch (err) {
497
559
  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.11",
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