skillmds 1.0.5 → 1.0.7

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,16 @@
2
2
 
3
3
  All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
4
4
 
5
+ ## 1.0.7
6
+
7
+ - Nicer CLI output: colored PASS/FAIL, quality-score bars, `ERROR`/`WARN`/`INFO` badges, and a summary with average score.
8
+ - Running `skillmd` (or `skillmds`) with no command on a terminal now opens a guided interactive menu (lint, scan, search, init, list). Piped/CI still prints help.
9
+ - Progress spinner while scanning.
10
+
11
+ ## 1.0.6
12
+
13
+ - Bundle the banner image in the package and serve it via jsDelivr so it renders on the npm page (the previous absolute URL was unreachable).
14
+
5
15
  ## 1.0.5
6
16
 
7
17
  - The `skillmds` binary now dispatches: `skillmds <command>` (lint, scan, add, …) runs the CLI, while `skillmds` with no command starts the MCP server. This makes `npx skillmds lint .` work without the `-p` flag.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://skillmd.com/skillmd-banner.jpg" alt="SkillMD" width="560">
2
+ <img src="https://cdn.jsdelivr.net/npm/skillmds/assets/skillmd-banner.jpg" alt="SkillMD" width="560">
3
3
  </p>
4
4
 
5
5
  # skillmds
Binary file
package/dist/cli.js CHANGED
@@ -7,6 +7,8 @@ import { Command } from "commander";
7
7
  import { writeFileSync } from "fs";
8
8
  import { homedir } from "os";
9
9
  import { resolve } from "path";
10
+ import pc2 from "picocolors";
11
+ import * as p from "@clack/prompts";
10
12
 
11
13
  // ../../packages/core/src/parse.ts
12
14
  import { parse as parseYaml } from "yaml";
@@ -46,9 +48,9 @@ var RULES = [
46
48
  ];
47
49
  function scanSecurity(body) {
48
50
  const findings = [];
49
- body.split(/\r?\n/).forEach((text2, i) => {
51
+ body.split(/\r?\n/).forEach((text3, i) => {
50
52
  for (const [flag, re] of RULES) {
51
- if (re.test(text2)) findings.push({ flag, line: i + 1, snippet: text2.trim().slice(0, 120) });
53
+ if (re.test(text3)) findings.push({ flag, line: i + 1, snippet: text3.trim().slice(0, 120) });
52
54
  }
53
55
  });
54
56
  const flags = [...new Set(findings.map((f) => f.flag))];
@@ -354,20 +356,54 @@ function normalizeGiget(arg) {
354
356
 
355
357
  // ../cli/src/ui.ts
356
358
  import pc from "picocolors";
357
- var decorate = (text2, kind) => {
359
+ var decorate = (text3, kind) => {
358
360
  switch (kind) {
359
361
  case "error":
360
- return pc.red(text2);
362
+ return pc.red(text3);
361
363
  case "warn":
362
- return pc.yellow(text2);
364
+ return pc.yellow(text3);
363
365
  case "info":
364
- return pc.cyan(text2);
366
+ return pc.cyan(text3);
365
367
  case "ok":
366
- return pc.green(text2);
368
+ return pc.green(text3);
367
369
  case "dim":
368
- return pc.dim(text2);
370
+ return pc.dim(text3);
369
371
  }
370
372
  };
373
+ var badge = {
374
+ error: pc.bgRed(pc.white(" ERROR ")),
375
+ warn: pc.bgYellow(pc.black(" WARN ")),
376
+ info: pc.bgCyan(pc.black(" INFO "))
377
+ };
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")}`;
383
+ }
384
+ function renderReport(results) {
385
+ 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");
388
+ 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(", "))}`);
397
+ }
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;
401
+ 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)}`);
405
+ return out.join("\n");
406
+ }
371
407
  function exitCodeFor(results, policy = {}) {
372
408
  for (const { result } of results) {
373
409
  const hasError = result.diagnostics.some((d) => d.severity === "error");
@@ -413,14 +449,33 @@ async function runLint(target, flags) {
413
449
  output = toGithub(results);
414
450
  break;
415
451
  default:
416
- output = toText(results, { decorate });
452
+ output = renderReport(results);
417
453
  }
418
454
  return { results, output, exitCode };
419
455
  }
420
456
  function lintCommand() {
421
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) => {
458
+ const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
422
459
  if (resolve(target) === homedir()) {
423
- process.stderr.write("Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill\n");
460
+ process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
461
+ `));
462
+ }
463
+ if (ui) {
464
+ p.intro(pc2.bgCyan(pc2.black(" skillmd lint ")));
465
+ const s = p.spinner();
466
+ s.start(`Scanning ${target}`);
467
+ try {
468
+ const run2 = await runLint(target, opts);
469
+ s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
470
+ console.log(run2.output);
471
+ p.outro(run2.exitCode === 0 ? pc2.green("All good") : pc2.red("Problems found"));
472
+ process.exitCode = run2.exitCode;
473
+ } catch (err) {
474
+ s.stop(pc2.red("No skills found"));
475
+ p.outro(err instanceof Error ? err.message : String(err));
476
+ process.exitCode = 1;
477
+ }
478
+ return;
424
479
  }
425
480
  process.stderr.write(`Looking for SKILL.md under ${target} \u2026
426
481
  `);
@@ -432,7 +487,7 @@ function lintCommand() {
432
487
 
433
488
  // ../cli/src/commands/scan.ts
434
489
  import { Command as Command2 } from "commander";
435
- import pc2 from "picocolors";
490
+ import pc3 from "picocolors";
436
491
  async function runScan(target, flags) {
437
492
  const resolved = await resolveSource(target);
438
493
  const deny = new Set(flags.deny ?? []);
@@ -451,11 +506,11 @@ async function runScan(target, flags) {
451
506
  const lines = [];
452
507
  for (const e of entries) {
453
508
  const flagStr = e.security.flags.join(", ");
454
- const head = e.denied.length ? pc2.red(`DENY ${e.file}`) : pc2.green(`OK ${e.file}`);
455
- lines.push(`${head} ${pc2.dim(flagStr)}`);
509
+ const head = e.denied.length ? pc3.red(`DENY ${e.file}`) : pc3.green(`OK ${e.file}`);
510
+ lines.push(`${head} ${pc3.dim(flagStr)}`);
456
511
  for (const f of e.security.findings) {
457
- const mark = deny.has(f.flag) ? pc2.red(f.flag) : pc2.yellow(f.flag);
458
- lines.push(` ${mark} :${f.line} ${pc2.dim(f.snippet)}`);
512
+ const mark = deny.has(f.flag) ? pc3.red(f.flag) : pc3.yellow(f.flag);
513
+ lines.push(` ${mark} :${f.line} ${pc3.dim(f.snippet)}`);
459
514
  }
460
515
  }
461
516
  output = lines.join("\n");
@@ -472,7 +527,7 @@ function scanCommand() {
472
527
 
473
528
  // ../cli/src/commands/rules.ts
474
529
  import { Command as Command3 } from "commander";
475
- import pc3 from "picocolors";
530
+ import pc4 from "picocolors";
476
531
  function rulesCommand() {
477
532
  return new Command3("rules").description("List lint rules, or show detail for one rule id").argument("[id]", "a rule id, e.g. SK010").option("--json", "machine-readable output").action((id, opts) => {
478
533
  const list = id ? RULES2.filter((r) => r.id.toLowerCase() === id.toLowerCase()) : RULES2;
@@ -485,8 +540,10 @@ function rulesCommand() {
485
540
  console.log(JSON.stringify(list.map((r) => ({ id: r.id, severity: r.severity, description: r.description })), null, 2));
486
541
  return;
487
542
  }
488
- const color = (sev) => (sev === "error" ? pc3.red : sev === "warn" ? pc3.yellow : pc3.cyan)(sev.padEnd(5));
489
- for (const r of list) console.log(`${pc3.bold(r.id)} ${color(r.severity)} ${r.description}`);
543
+ const badge2 = (sev) => sev === "error" ? pc4.bgRed(pc4.white(" error ")) : sev === "warn" ? pc4.bgYellow(pc4.black(" warn ")) : pc4.bgCyan(pc4.black(" info "));
544
+ console.log(pc4.bold("\nLint rules\n"));
545
+ for (const r of list) console.log(` ${pc4.bold(r.id)} ${badge2(r.severity)} ${r.description}`);
546
+ console.log("");
490
547
  });
491
548
  }
492
549
 
@@ -494,7 +551,7 @@ function rulesCommand() {
494
551
  import { Command as Command4 } from "commander";
495
552
  import { mkdirSync, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
496
553
  import { join as join2 } from "path";
497
- import * as p from "@clack/prompts";
554
+ import * as p2 from "@clack/prompts";
498
555
  function skillTemplate(name, description, license) {
499
556
  const desc = description.trim() || `A skill that helps agents with ${name}.`;
500
557
  return `---
@@ -534,9 +591,9 @@ function initCommand() {
534
591
  return new Command4("init").description("Scaffold a new SKILL.md from a template").argument("[name]", "skill name").option("--name <name>", "skill name (non-interactive)").option("--description <text>", "skill description").option("--license <license>", "license identifier", "MIT").option("-y, --yes", "skip prompts").action(async (nameArg, opts) => {
535
592
  let name = nameArg ?? opts.name;
536
593
  if (!name && !opts.yes) {
537
- const answer = await p.text({ message: "Skill name?", placeholder: "my-skill" });
538
- if (p.isCancel(answer)) {
539
- p.cancel("Cancelled.");
594
+ const answer = await p2.text({ message: "Skill name?", placeholder: "my-skill" });
595
+ if (p2.isCancel(answer)) {
596
+ p2.cancel("Cancelled.");
540
597
  return;
541
598
  }
542
599
  name = answer;
@@ -553,7 +610,7 @@ function initCommand() {
553
610
 
554
611
  // ../cli/src/commands/search.ts
555
612
  import { Command as Command5 } from "commander";
556
- import pc4 from "picocolors";
613
+ import pc5 from "picocolors";
557
614
 
558
615
  // ../cli/src/config.ts
559
616
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
@@ -618,10 +675,10 @@ async function runSearch(query, flags) {
618
675
  const items = r.items ?? [];
619
676
  if (flags.json) return { items, output: JSON.stringify(items, null, 2) };
620
677
  const lines = items.map((s) => {
621
- const check = s.verified ? pc4.green("\u2713") : " ";
622
- const rating = s.avg_rating ? pc4.dim(`\u2605${s.avg_rating.toFixed(1)} (${s.rating_count ?? 0})`) : "";
623
- return `${check} ${pc4.bold(s.slug)} ${rating}
624
- ${pc4.dim(s.description)}`;
678
+ const check = s.verified ? pc5.green("\u2713") : " ";
679
+ const rating = s.avg_rating ? pc5.dim(`\u2605${s.avg_rating.toFixed(1)} (${s.rating_count ?? 0})`) : "";
680
+ return `${check} ${pc5.bold(s.slug)} ${rating}
681
+ ${pc5.dim(s.description)}`;
625
682
  });
626
683
  return { items, output: lines.join("\n") || "No skills found." };
627
684
  }
@@ -636,7 +693,7 @@ function searchCommand() {
636
693
  // ../cli/src/commands/add.ts
637
694
  import { Command as Command6 } from "commander";
638
695
  import { existsSync as existsSync4 } from "fs";
639
- import pc5 from "picocolors";
696
+ import pc6 from "picocolors";
640
697
 
641
698
  // ../cli/src/agents.ts
642
699
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync, symlinkSync, existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
@@ -737,24 +794,24 @@ async function runAdd(arg, flags, deps = defaultDeps) {
737
794
  const deniedFlags = result.security.flags.filter((f) => deny.has(f));
738
795
  if (!flags.skipLint && !result.ok) {
739
796
  blocked.push({ name: c.name, reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}` });
740
- lines.push(pc5.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
797
+ lines.push(pc6.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
741
798
  continue;
742
799
  }
743
800
  if (deniedFlags.length) {
744
801
  blocked.push({ name: c.name, reason: `denied security flags: ${deniedFlags.join(", ")}` });
745
- lines.push(pc5.red(`\u2717 ${c.name} blocked \u2014 ${deniedFlags.join(", ")}`));
802
+ lines.push(pc6.red(`\u2717 ${c.name} blocked \u2014 ${deniedFlags.join(", ")}`));
746
803
  continue;
747
804
  }
748
805
  if (c.registrySlug && c.verified === false && !flags.allowUnverified) {
749
806
  blocked.push({ name: c.name, reason: "unverified registry skill (use --allow-unverified)" });
750
- lines.push(pc5.yellow(`\u2717 ${c.name} blocked \u2014 unverified (use --allow-unverified)`));
807
+ lines.push(pc6.yellow(`\u2717 ${c.name} blocked \u2014 unverified (use --allow-unverified)`));
751
808
  continue;
752
809
  }
753
810
  for (const agentId of targets) {
754
811
  const dir = agentDir(agentId, scope);
755
812
  const out = writeSkill(c.name, [{ path: "SKILL.md", contents: c.raw }], dir, { mode: "copy" });
756
813
  written.push({ name: c.name, dir: out });
757
- lines.push(pc5.green(`\u2713 ${c.name} \u2192 ${out}`));
814
+ lines.push(pc6.green(`\u2713 ${c.name} \u2192 ${out}`));
758
815
  }
759
816
  if (c.registrySlug) deps.fireInstall(c.registrySlug, flags);
760
817
  }
@@ -772,7 +829,7 @@ function addCommand() {
772
829
 
773
830
  // ../cli/src/commands/list.ts
774
831
  import { Command as Command7 } from "commander";
775
- import pc6 from "picocolors";
832
+ import pc7 from "picocolors";
776
833
  function listCommand() {
777
834
  return new Command7("list").alias("ls").description("List installed skills across detected agents").option("-g, --global", "only global skills").option("--json", "machine-readable output").action((opts) => {
778
835
  const scope = { global: opts.global };
@@ -787,8 +844,8 @@ function listCommand() {
787
844
  }
788
845
  for (const s of skills) {
789
846
  const parsed = parseSkillMd(s.raw);
790
- const desc = "error" in parsed ? pc6.red("(invalid SKILL.md)") : pc6.dim(parsed.description.slice(0, 80));
791
- console.log(`${pc6.bold(s.name)} ${pc6.dim(`[${s.agent}/${s.scope}]`)}
847
+ const desc = "error" in parsed ? pc7.red("(invalid SKILL.md)") : pc7.dim(parsed.description.slice(0, 80));
848
+ console.log(`${pc7.bold(s.name)} ${pc7.dim(`[${s.agent}/${s.scope}]`)}
792
849
  ${desc}`);
793
850
  }
794
851
  });
@@ -797,7 +854,7 @@ function listCommand() {
797
854
  // ../cli/src/commands/remove.ts
798
855
  import { Command as Command8 } from "commander";
799
856
  import { rmSync as rmSync2 } from "fs";
800
- import pc7 from "picocolors";
857
+ import pc8 from "picocolors";
801
858
  function runRemove(names, scope = {}) {
802
859
  const installed = installedSkills(scope);
803
860
  const targets = installed.filter((s) => names.includes(s.name));
@@ -806,10 +863,10 @@ function runRemove(names, scope = {}) {
806
863
  for (const t of targets) {
807
864
  rmSync2(t.path, { recursive: true, force: true });
808
865
  removed.push(t.path);
809
- lines.push(pc7.green(`\u2713 removed ${t.name} (${t.agent})`));
866
+ lines.push(pc8.green(`\u2713 removed ${t.name} (${t.agent})`));
810
867
  }
811
868
  for (const n of names) {
812
- if (!targets.some((t) => t.name === n)) lines.push(pc7.yellow(`- ${n} not installed`));
869
+ if (!targets.some((t) => t.name === n)) lines.push(pc8.yellow(`- ${n} not installed`));
813
870
  }
814
871
  return { removed, output: lines.join("\n") };
815
872
  }
@@ -824,7 +881,7 @@ function removeCommand() {
824
881
  import { Command as Command9 } from "commander";
825
882
  import { writeFileSync as writeFileSync5 } from "fs";
826
883
  import { join as join5 } from "path";
827
- import pc8 from "picocolors";
884
+ import pc9 from "picocolors";
828
885
  async function runUpdate(names, flags, scope = {}) {
829
886
  const { api } = createClient(flags);
830
887
  const installed = installedSkills(scope).filter((s) => names.length === 0 || names.includes(s.name));
@@ -833,7 +890,7 @@ async function runUpdate(names, flags, scope = {}) {
833
890
  for (const s of installed) {
834
891
  const slug = await findRegistrySlug(api, s);
835
892
  if (!slug) {
836
- lines.push(pc8.dim(`- ${s.name} skipped (no registry match)`));
893
+ lines.push(pc9.dim(`- ${s.name} skipped (no registry match)`));
837
894
  continue;
838
895
  }
839
896
  try {
@@ -841,19 +898,19 @@ async function runUpdate(names, flags, scope = {}) {
841
898
  const skill = await api(`/api/skills/${owner}/${name}`);
842
899
  const next = reconstructSkillMd(skill);
843
900
  if (next.trim() === s.raw.trim()) {
844
- lines.push(pc8.dim(`- ${s.name} already up to date`));
901
+ lines.push(pc9.dim(`- ${s.name} already up to date`));
845
902
  continue;
846
903
  }
847
904
  const result = lint(next, { slug: s.name });
848
905
  if (!result.ok) {
849
- lines.push(pc8.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
906
+ lines.push(pc9.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
850
907
  continue;
851
908
  }
852
909
  writeFileSync5(join5(s.path, "SKILL.md"), next, "utf8");
853
910
  updated.push(s.name);
854
- lines.push(pc8.green(`\u2713 updated ${s.name}`));
911
+ lines.push(pc9.green(`\u2713 updated ${s.name}`));
855
912
  } catch {
856
- lines.push(pc8.red(`\u2717 ${s.name} update error`));
913
+ lines.push(pc9.red(`\u2717 ${s.name} update error`));
857
914
  }
858
915
  }
859
916
  return { updated, output: lines.join("\n") || "Nothing to update." };
@@ -873,7 +930,7 @@ function updateCommand() {
873
930
 
874
931
  // ../cli/src/commands/publish.ts
875
932
  import { Command as Command10 } from "commander";
876
- import pc9 from "picocolors";
933
+ import pc10 from "picocolors";
877
934
  var defaultDeps2 = {
878
935
  resolve: resolveSource,
879
936
  async submit(payload, flags) {
@@ -896,20 +953,20 @@ async function runPublish(target, flags, deps = defaultDeps2) {
896
953
  const report = toText([{ file: src.path, result }], { decorate });
897
954
  if (!result.ok) {
898
955
  return { result, published: false, exitCode: 1, output: `${report}
899
- ${pc9.red("\u2717 Refusing to publish: fix the errors above.")}` };
956
+ ${pc10.red("\u2717 Refusing to publish: fix the errors above.")}` };
900
957
  }
901
958
  const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
902
959
  if (hasWarn && !flags.force) {
903
960
  return { result, published: false, exitCode: 1, output: `${report}
904
- ${pc9.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
961
+ ${pc10.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
905
962
  }
906
963
  if (flags.dryRun) {
907
964
  return { result, published: false, exitCode: 0, output: `${report}
908
- ${pc9.green("\u2713 Dry run: would publish.")}` };
965
+ ${pc10.green("\u2713 Dry run: would publish.")}` };
909
966
  }
910
967
  const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
911
968
  return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
912
- ${pc9.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
969
+ ${pc10.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
913
970
  }
914
971
  function publishCommand() {
915
972
  return new Command10("publish").alias("submit").description("Publish a skill to the registry (refuses to publish on lint errors)").argument("[target]", "local path to the skill", ".").option("--type <type>", "single | pack", "single").option("--force", "publish despite warnings (never bypasses errors)").option("--dry-run", "lint only; do not publish").action(async (target, opts, cmd) => {
@@ -922,14 +979,14 @@ function publishCommand() {
922
979
 
923
980
  // ../cli/src/commands/login.ts
924
981
  import { Command as Command11 } from "commander";
925
- import * as p2 from "@clack/prompts";
982
+ import * as p3 from "@clack/prompts";
926
983
  function loginCommand() {
927
984
  return new Command11("login").description("Store a SkillMD personal access token for publishing").option("--token <token>", "token to store (otherwise prompt)").option("--api <url>", "registry API base to store").action(async (opts) => {
928
985
  let token = opts.token;
929
986
  if (!token) {
930
- const answer = await p2.password({ message: "Paste your SkillMD token (from your account page)" });
931
- if (p2.isCancel(answer)) {
932
- p2.cancel("Cancelled.");
987
+ const answer = await p3.password({ message: "Paste your SkillMD token (from your account page)" });
988
+ if (p3.isCancel(answer)) {
989
+ p3.cancel("Cancelled.");
933
990
  return;
934
991
  }
935
992
  token = answer;
@@ -955,6 +1012,118 @@ function logoutCommand() {
955
1012
  });
956
1013
  }
957
1014
 
1015
+ // ../cli/src/commands/interactive.ts
1016
+ import * as p4 from "@clack/prompts";
1017
+ import pc11 from "picocolors";
1018
+ function bail(v) {
1019
+ if (p4.isCancel(v)) {
1020
+ p4.cancel("Cancelled.");
1021
+ return true;
1022
+ }
1023
+ return false;
1024
+ }
1025
+ async function runInteractive() {
1026
+ p4.intro(pc11.bgCyan(pc11.black(" skillmd ")));
1027
+ const action = await p4.select({
1028
+ message: "What would you like to do?",
1029
+ options: [
1030
+ { value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
1031
+ { value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
1032
+ { value: "search", label: "Search the registry" },
1033
+ { value: "init", label: "Create a new skill" },
1034
+ { value: "list", label: "List installed skills" },
1035
+ { value: "help", label: "Show all commands" }
1036
+ ]
1037
+ });
1038
+ if (bail(action)) return;
1039
+ switch (action) {
1040
+ case "lint":
1041
+ case "scan": {
1042
+ const path = await p4.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
1043
+ if (bail(path)) return;
1044
+ const target = String(path || ".");
1045
+ const s = p4.spinner();
1046
+ s.start(action === "lint" ? "Linting" : "Scanning");
1047
+ try {
1048
+ const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
1049
+ s.stop("Done");
1050
+ console.log(run.output);
1051
+ process.exitCode = run.exitCode;
1052
+ } catch (err) {
1053
+ s.stop(pc11.red(err instanceof Error ? err.message : String(err)));
1054
+ process.exitCode = 1;
1055
+ }
1056
+ break;
1057
+ }
1058
+ case "search": {
1059
+ const query = await p4.text({ message: "Search the registry for" });
1060
+ if (bail(query)) return;
1061
+ const verified = await p4.confirm({ message: "Verified skills only?", initialValue: false });
1062
+ if (bail(verified)) return;
1063
+ const s = p4.spinner();
1064
+ s.start("Searching");
1065
+ try {
1066
+ const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
1067
+ s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
1068
+ console.log(run.output);
1069
+ } catch (err) {
1070
+ s.stop(pc11.red(err instanceof Error ? err.message : String(err)));
1071
+ process.exitCode = 1;
1072
+ }
1073
+ break;
1074
+ }
1075
+ case "init": {
1076
+ const name = await p4.text({ message: "Skill name", placeholder: "my-skill" });
1077
+ if (bail(name)) return;
1078
+ const description = await p4.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
1079
+ if (bail(description)) return;
1080
+ try {
1081
+ const file = initSkill(String(name), { description: String(description || "") });
1082
+ p4.log.success(`Created ${pc11.bold(file)}`);
1083
+ } catch (err) {
1084
+ p4.log.error(err instanceof Error ? err.message : String(err));
1085
+ process.exitCode = 1;
1086
+ }
1087
+ break;
1088
+ }
1089
+ case "list": {
1090
+ const global = await p4.confirm({ message: "Include global skills?", initialValue: true });
1091
+ if (bail(global)) return;
1092
+ const skills = installedSkills({ global: Boolean(global) });
1093
+ if (skills.length === 0) {
1094
+ p4.log.info("No installed skills found.");
1095
+ } else {
1096
+ p4.log.message(
1097
+ skills.map((sk) => {
1098
+ const parsed = parseSkillMd(sk.raw);
1099
+ const desc = "error" in parsed ? pc11.red("(invalid)") : pc11.dim(parsed.description.slice(0, 70));
1100
+ return `${pc11.bold(sk.name)} ${pc11.dim(`[${sk.agent}]`)}
1101
+ ${desc}`;
1102
+ }).join("\n")
1103
+ );
1104
+ }
1105
+ break;
1106
+ }
1107
+ case "help": {
1108
+ p4.note(
1109
+ [
1110
+ `${pc11.bold("lint")} [path] validate SKILL.md + quality score`,
1111
+ `${pc11.bold("scan")} [path] security scan`,
1112
+ `${pc11.bold("search")} <query> search the registry`,
1113
+ `${pc11.bold("add")} <source> install a skill (lints first)`,
1114
+ `${pc11.bold("init")} [name] scaffold a new skill`,
1115
+ `${pc11.bold("list / remove / update")} manage installed skills`,
1116
+ `${pc11.bold("publish")} [path] publish (fails on lint errors)`,
1117
+ `${pc11.bold("rules / login")}`
1118
+ ].join("\n"),
1119
+ "Commands (run: skillmd <command>)"
1120
+ );
1121
+ break;
1122
+ }
1123
+ }
1124
+ p4.outro(pc11.dim("skillmd.com"));
1125
+ }
1126
+
958
1127
  // ../cli/src/index.ts
959
1128
  var version = "0.0.0";
960
1129
  try {
@@ -975,6 +1144,10 @@ program.addCommand(publishCommand());
975
1144
  program.addCommand(loginCommand());
976
1145
  program.addCommand(logoutCommand());
977
1146
  if (process.argv.slice(2).length === 0) {
1147
+ if (process.stdout.isTTY) {
1148
+ await runInteractive();
1149
+ process.exit(process.exitCode ?? 0);
1150
+ }
978
1151
  program.outputHelp();
979
1152
  process.exit(0);
980
1153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillmds",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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": {
@@ -27,7 +27,7 @@
27
27
  "bugs": { "url": "https://skillmd.com/contact" },
28
28
  "author": "SkillMD",
29
29
  "license": "MIT",
30
- "files": ["bin", "dist", "vendor", "README.md", "CHANGELOG.md"],
30
+ "files": ["bin", "dist", "vendor", "assets", "README.md", "CHANGELOG.md"],
31
31
  "engines": { "node": ">=18" },
32
32
  "dependencies": {
33
33
  "@clack/prompts": "^0.8.0",