skillmds 1.0.12 → 1.0.14

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/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ../cli/src/index.ts
2
- import { Command as Command12 } from "commander";
2
+ import { Command as Command13 } from "commander";
3
3
  import { readFileSync as readFileSync4 } from "fs";
4
4
 
5
5
  // ../cli/src/commands/lint.ts
@@ -13,7 +13,7 @@ import * as p from "@clack/prompts";
13
13
  // ../../packages/core/src/parse.ts
14
14
  import { parse as parseYaml } from "yaml";
15
15
  var MAX_SKILL_BYTES = 256 * 1024;
16
- var MAX_DESCRIPTION = 500;
16
+ var MAX_DESCRIPTION = 1024;
17
17
  var MAX_NAME = 120;
18
18
  var slugify = (s) => String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "skill";
19
19
  function parseSkillMd(raw, type = "single") {
@@ -30,7 +30,7 @@ function parseSkillMd(raw, type = "single") {
30
30
  const description = String(fm.description ?? "").replace(/\s+/g, " ").trim();
31
31
  if (!name) return { error: "frontmatter 'name' is required" };
32
32
  if (!description) return { error: "frontmatter 'description' is required" };
33
- if (description.length > MAX_DESCRIPTION) return { error: "description must be <= 500 chars" };
33
+ if (description.length > MAX_DESCRIPTION) return { error: `description must be <= ${MAX_DESCRIPTION} chars` };
34
34
  return {
35
35
  name: name.slice(0, MAX_NAME),
36
36
  description: description.slice(0, MAX_DESCRIPTION),
@@ -92,7 +92,7 @@ var RULES2 = [
92
92
  {
93
93
  id: "SK003",
94
94
  severity: "error",
95
- description: "description must be present and \u2264 500 chars",
95
+ description: "description must be present and \u2264 1024 chars",
96
96
  check(s, _ctx) {
97
97
  const out = [];
98
98
  if (!s.description.trim()) out.push(diag(this, "frontmatter 'description' is required"));
@@ -284,9 +284,9 @@ function toGithub(results) {
284
284
  }
285
285
 
286
286
  // ../cli/src/source.ts
287
- import { existsSync, statSync, readFileSync, readdirSync, mkdtempSync } from "fs";
287
+ import { existsSync, statSync, lstatSync, readFileSync, readdirSync, mkdtempSync, rmSync } from "fs";
288
288
  import { tmpdir } from "os";
289
- import { join, basename, dirname } from "path";
289
+ import { join, basename, dirname, relative, sep } from "path";
290
290
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
291
291
  // build/dependency dirs
292
292
  "node_modules",
@@ -357,6 +357,44 @@ async function resolveSource(arg) {
357
357
  if (files.length === 0) throw new Error(`No SKILL.md found in ${arg}`);
358
358
  return files.map(loadLocal);
359
359
  }
360
+ var MAX_PACK_FILES = 200;
361
+ var MAX_PACK_BYTES = 20 * 1024 * 1024;
362
+ function collectFiles(root, dir, limits = {}) {
363
+ const maxFiles = limits.maxFiles ?? MAX_PACK_FILES;
364
+ const maxBytes = limits.maxBytes ?? MAX_PACK_BYTES;
365
+ const out = [];
366
+ let bytes = 0;
367
+ const walk = (d, depth) => {
368
+ if (depth > MAX_DEPTH) return;
369
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
370
+ const fp = join(d, entry.name);
371
+ const st = lstatSync(fp);
372
+ if (st.isSymbolicLink()) continue;
373
+ if (st.isDirectory()) {
374
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
375
+ walk(fp, depth + 1);
376
+ } else if (st.isFile()) {
377
+ if (out.length >= maxFiles) throw new Error(`pack too large: > ${maxFiles} files`);
378
+ bytes += st.size;
379
+ if (bytes > maxBytes) throw new Error(`pack too large: > ${Math.round(maxBytes / (1024 * 1024))}MB`);
380
+ out.push({ path: relative(root, fp).split(sep).join("/"), contents: readFileSync(fp) });
381
+ }
382
+ }
383
+ };
384
+ walk(dir, 0);
385
+ return out;
386
+ }
387
+ async function resolveTree(sourceUrl, ref) {
388
+ const { downloadTemplate } = await import("giget");
389
+ const input = normalizeGiget(sourceUrl) + (ref ? `#${ref}` : "");
390
+ const dir = mkdtempSync(join(tmpdir(), "skillmd-pack-"));
391
+ try {
392
+ const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
393
+ return collectFiles(out, out);
394
+ } finally {
395
+ rmSync(dir, { recursive: true, force: true });
396
+ }
397
+ }
360
398
  function normalizeGiget(arg) {
361
399
  const m = arg.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/[^/]+\/(.+))?/);
362
400
  if (m) return `github:${m[1]}/${m[2]}${m[3] ? "/" + m[3] : ""}`;
@@ -697,6 +735,9 @@ function initCommand() {
697
735
  import { Command as Command5 } from "commander";
698
736
  import pc5 from "picocolors";
699
737
 
738
+ // ../cli/src/api.ts
739
+ import { stringify as stringifyYaml } from "yaml";
740
+
700
741
  // ../cli/src/config.ts
701
742
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
702
743
  import { homedir as homedir2 } from "os";
@@ -740,12 +781,17 @@ function createClient(flags = {}) {
740
781
  }
741
782
  return { api, base, hasToken: Boolean(token) };
742
783
  }
784
+ function skillMdFor(skill) {
785
+ if (skill.raw_md) return skill.raw_md;
786
+ return reconstructSkillMd(skill);
787
+ }
743
788
  function reconstructSkillMd(skill) {
744
789
  const name = skill.slug.split("/")[1] || skill.title;
745
- const fm = ["---", `name: ${name}`, `description: ${String(skill.description).replace(/\n/g, " ")}`];
746
- if (skill.license) fm.push(`license: ${skill.license}`);
747
- fm.push("---", "");
748
- return fm.join("\n") + (skill.body_md || "");
790
+ const fm = { name, description: String(skill.description).replace(/\n/g, " ") };
791
+ if (skill.license) fm.license = skill.license;
792
+ return `---
793
+ ${stringifyYaml(fm)}---
794
+ ${skill.body_md || ""}`;
749
795
  }
750
796
 
751
797
  // ../cli/src/commands/search.ts
@@ -775,13 +821,62 @@ function searchCommand() {
775
821
  });
776
822
  }
777
823
 
778
- // ../cli/src/commands/add.ts
824
+ // ../cli/src/commands/info.ts
779
825
  import { Command as Command6 } from "commander";
780
- import { existsSync as existsSync4 } from "fs";
781
826
  import pc6 from "picocolors";
827
+ async function runInfo(slug, flags) {
828
+ const [owner, name] = slug.split("/");
829
+ if (!owner || !name || slug.split("/").length !== 2) {
830
+ return { ok: false, exitCode: 1, output: pc6.red("Expected owner/name, e.g. anthropic/pdf") };
831
+ }
832
+ const { api } = createClient(flags);
833
+ let s;
834
+ try {
835
+ s = await api(`/api/skills/${owner}/${name}`);
836
+ } catch (err) {
837
+ const message = err instanceof Error ? err.message : String(err);
838
+ if (/^SkillMD API 404 /.test(message)) {
839
+ return { ok: false, exitCode: 1, output: pc6.red(`Skill not found on the registry: ${slug}`) };
840
+ }
841
+ return { ok: false, exitCode: 1, output: pc6.red(`Could not reach the SkillMD registry: ${message}`) };
842
+ }
843
+ if (flags.json) return { ok: true, exitCode: 0, output: JSON.stringify(s, null, 2), skill: s };
844
+ const lines = [];
845
+ lines.push(
846
+ `${pc6.bold(s.slug)} ${pc6.dim(`[${s.type ?? "single"}]`)} ${s.verified ? pc6.green("\u2713 verified") : pc6.yellow("unverified")}`
847
+ );
848
+ lines.push(` ${s.description}`);
849
+ lines.push(
850
+ pc6.dim(
851
+ ` category: ${s.category ?? "-"} \xB7 rating: ${s.avg_rating != null ? s.avg_rating.toFixed(1) : "-"} (${s.rating_count ?? 0}) \xB7 installs: ${s.install_count ?? 0}`
852
+ )
853
+ );
854
+ lines.push(
855
+ pc6.dim(` security: ${(s.security_flags ?? []).join(", ") || "unscanned"} \xB7 license: ${s.provenance?.license ?? s.license ?? "unknown"}`)
856
+ );
857
+ const sourceRepo = s.provenance?.source_repo ?? s.source_repo;
858
+ const commitSha = s.provenance?.commit_sha ?? s.commit_sha;
859
+ if (sourceRepo) lines.push(pc6.dim(` source: ${sourceRepo}${commitSha ? ` @ ${commitSha.slice(0, 8)}` : ""}`));
860
+ lines.push(pc6.dim(` page: https://skillmd.com/skills/${s.slug}`));
861
+ lines.push(pc6.dim(` install: ${s.install?.cli ?? `npx skillmds add ${s.slug}`}`));
862
+ return { ok: true, exitCode: 0, output: lines.join("\n"), skill: s };
863
+ }
864
+ function infoCommand() {
865
+ return new Command6("info").description("Show registry details for a skill: verification, security flags, provenance, rating").argument("<slug>", "owner/name, e.g. anthropic/pdf").action(async (slug, _opts, cmd) => {
866
+ const flags = { ...cmd.parent?.opts() };
867
+ const run = await runInfo(slug, flags);
868
+ if (run.output) (run.ok ? console.log : console.error)(run.output);
869
+ process.exitCode = run.exitCode;
870
+ });
871
+ }
872
+
873
+ // ../cli/src/commands/add.ts
874
+ import { Command as Command7 } from "commander";
875
+ import { existsSync as existsSync4 } from "fs";
876
+ import pc7 from "picocolors";
782
877
 
783
878
  // ../cli/src/agents.ts
784
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync, symlinkSync, existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
879
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync as rmSync2, symlinkSync, existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
785
880
  import { homedir as homedir3 } from "os";
786
881
  import { join as join4, resolve as resolve2, dirname as dirname2 } from "path";
787
882
  var AGENTS = [
@@ -804,7 +899,7 @@ function writeSkill(skillName, files, targetDir, opts = {}) {
804
899
  const dest = join4(targetDir, skillName);
805
900
  if (opts.mode === "symlink" && opts.canonicalDir) {
806
901
  mkdirSync3(dirname2(dest), { recursive: true });
807
- rmSync(dest, { recursive: true, force: true });
902
+ rmSync2(dest, { recursive: true, force: true });
808
903
  symlinkSync(resolve2(opts.canonicalDir), dest, "dir");
809
904
  return dest;
810
905
  }
@@ -812,7 +907,7 @@ function writeSkill(skillName, files, targetDir, opts = {}) {
812
907
  for (const f of files) {
813
908
  const fp = join4(dest, f.path);
814
909
  mkdirSync3(dirname2(fp), { recursive: true });
815
- writeFileSync4(fp, f.contents, "utf8");
910
+ writeFileSync4(fp, f.contents);
816
911
  }
817
912
  return dest;
818
913
  }
@@ -835,6 +930,20 @@ function installedSkills(opts = {}) {
835
930
  function isLocalish(arg) {
836
931
  return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync4(arg);
837
932
  }
933
+ async function resolvePackFiles(skill, fetchTree = resolveTree) {
934
+ if (!skill.source_repo) return null;
935
+ const attempts = [{ ref: skill.commit_sha ?? void 0, pinMiss: false }];
936
+ if (skill.commit_sha) attempts.push({ ref: void 0, pinMiss: true });
937
+ for (const attempt of attempts) {
938
+ try {
939
+ const files = await fetchTree(skill.source_repo, attempt.ref);
940
+ const sk = files.find((f) => f.path === "SKILL.md");
941
+ if (sk) return { raw: sk.contents.toString("utf8"), files, pinMiss: attempt.pinMiss };
942
+ } catch {
943
+ }
944
+ }
945
+ return null;
946
+ }
838
947
  var defaultDeps = {
839
948
  async resolve(arg, flags) {
840
949
  if (isLocalish(arg)) {
@@ -847,7 +956,19 @@ var defaultDeps = {
847
956
  try {
848
957
  const skill = await api(`/api/skills/${owner}/${name}`);
849
958
  const skillName = skill.slug.split("/")[1] || name;
850
- return [{ name: skillName, raw: reconstructSkillMd(skill), slug: skillName, registrySlug: arg, verified: skill.verified }];
959
+ const base = {
960
+ name: skillName,
961
+ slug: skillName,
962
+ registrySlug: arg,
963
+ verified: skill.verified,
964
+ type: skill.type,
965
+ securityFlags: skill.security_flags
966
+ };
967
+ if (skill.type === "pack") {
968
+ const pack = await resolvePackFiles(skill);
969
+ if (pack) return [{ ...base, ...pack }];
970
+ }
971
+ return [{ ...base, raw: skillMdFor(skill) }];
851
972
  } catch {
852
973
  }
853
974
  }
@@ -863,6 +984,16 @@ var defaultDeps = {
863
984
  body: JSON.stringify({ via: "cli" })
864
985
  }).catch(() => {
865
986
  });
987
+ },
988
+ async confirmUnverified(c, score, secFlags) {
989
+ if (!process.stdin.isTTY) return false;
990
+ const { confirm: confirm2, isCancel: isCancel4 } = await import("@clack/prompts");
991
+ const ok = await confirm2({
992
+ message: `${c.name} is not yet verified by SkillMD (lint score ${score}${secFlags.length ? `; flags: ${secFlags.join(", ")}` : ""}). Install anyway?`,
993
+ initialValue: false
994
+ });
995
+ if (isCancel4(ok)) return false;
996
+ return ok === true;
866
997
  }
867
998
  };
868
999
  async function runAdd(arg, flags, deps = defaultDeps) {
@@ -876,27 +1007,39 @@ async function runAdd(arg, flags, deps = defaultDeps) {
876
1007
  const targets = flags.agent?.length ? flags.agent : detectAgents(scope).length ? detectAgents(scope) : ["claude-code"];
877
1008
  for (const c of candidates) {
878
1009
  const result = lint(c.raw, { slug: c.slug });
879
- const deniedFlags = result.security.flags.filter((f) => deny.has(f));
1010
+ const allFlags = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
1011
+ const deniedFlags = allFlags.filter((f) => deny.has(f));
880
1012
  if (!flags.skipLint && !result.ok) {
881
1013
  blocked.push({ name: c.name, reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}` });
882
- lines.push(pc6.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
1014
+ lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
883
1015
  continue;
884
1016
  }
885
1017
  if (deniedFlags.length) {
886
1018
  blocked.push({ name: c.name, reason: `denied security flags: ${deniedFlags.join(", ")}` });
887
- lines.push(pc6.red(`\u2717 ${c.name} blocked \u2014 ${deniedFlags.join(", ")}`));
1019
+ lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 ${deniedFlags.join(", ")}`));
888
1020
  continue;
889
1021
  }
890
1022
  if (c.registrySlug && c.verified === false && !flags.allowUnverified) {
891
- blocked.push({ name: c.name, reason: "unverified registry skill (use --allow-unverified)" });
892
- lines.push(pc6.yellow(`\u2717 ${c.name} blocked \u2014 unverified (use --allow-unverified)`));
893
- continue;
1023
+ const ok = !flags.yes && deps.confirmUnverified ? await deps.confirmUnverified(c, result.score, allFlags) : false;
1024
+ if (!ok) {
1025
+ blocked.push({ name: c.name, reason: "unverified registry skill (use --allow-unverified)" });
1026
+ lines.push(pc7.yellow(`\u2717 ${c.name} blocked \u2014 not yet verified by SkillMD${allFlags.length ? ` (flags: ${allFlags.join(", ")})` : ""}`));
1027
+ lines.push(pc7.dim(` review at https://skillmd.com/skills/${c.registrySlug}, then re-run with --allow-unverified`));
1028
+ continue;
1029
+ }
894
1030
  }
1031
+ const files = c.files ?? [{ path: "SKILL.md", contents: c.raw }];
895
1032
  for (const agentId of targets) {
896
1033
  const dir = agentDir(agentId, scope);
897
- const out = writeSkill(c.name, [{ path: "SKILL.md", contents: c.raw }], dir, { mode: "copy" });
1034
+ const out = writeSkill(c.name, files, dir, { mode: "copy" });
898
1035
  written.push({ name: c.name, dir: out });
899
- lines.push(pc6.green(`\u2713 ${c.name} \u2192 ${out}`));
1036
+ lines.push(pc7.green(`\u2713 ${c.name} \u2192 ${out}`) + pc7.dim(` (score ${result.score}${allFlags.length ? `; ${allFlags.join(", ")}` : ""})`));
1037
+ }
1038
+ if (c.pinMiss) {
1039
+ lines.push(pc7.yellow(`\u26A0 ${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
1040
+ }
1041
+ if (c.type === "pack" && !c.files) {
1042
+ lines.push(pc7.yellow(`\u26A0 ${c.name} is a pack but its asset files could not be fetched \u2014 only SKILL.md was installed. It may reference missing files.`));
900
1043
  }
901
1044
  if (c.registrySlug) deps.fireInstall(c.registrySlug, flags);
902
1045
  }
@@ -904,7 +1047,7 @@ async function runAdd(arg, flags, deps = defaultDeps) {
904
1047
  return { written, blocked, exitCode, output: lines.join("\n") };
905
1048
  }
906
1049
  function addCommand() {
907
- return new Command6("add").description("Install a skill from the registry, a GitHub repo, or a local path (lints before writing)").argument("<source>", "registry slug (owner/name), GitHub source, or local path").option("-g, --global", "install to the user directory instead of the project").option("-a, --agent <agents...>", "target specific agents").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--copy", "copy files (default)", true).option("-y, --yes", "skip confirmation prompts").option("--allow-unverified", "allow installing unverified registry skills").option("--skip-lint", "install even if the skill fails lint").option("--deny <flags...>", "refuse skills with these security flags").action(async (source, opts, cmd) => {
1050
+ return new Command7("add").description("Install a skill from the registry, a GitHub repo, or a local path (lints before writing)").argument("<source>", "registry slug (owner/name), GitHub source, or local path").option("-g, --global", "install to the user directory instead of the project").option("-a, --agent <agents...>", "target specific agents").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--copy", "copy files (default)", true).option("-y, --yes", "skip confirmation prompts").option("--allow-unverified", "allow installing unverified registry skills").option("--skip-lint", "install even if the skill fails lint").option("--deny <flags...>", "refuse skills with these security flags").action(async (source, opts, cmd) => {
908
1051
  const merged = { ...cmd.parent?.opts(), ...opts };
909
1052
  const run = await runAdd(source, merged);
910
1053
  console.log(run.output);
@@ -913,10 +1056,10 @@ function addCommand() {
913
1056
  }
914
1057
 
915
1058
  // ../cli/src/commands/list.ts
916
- import { Command as Command7 } from "commander";
917
- import pc7 from "picocolors";
1059
+ import { Command as Command8 } from "commander";
1060
+ import pc8 from "picocolors";
918
1061
  function listCommand() {
919
- 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) => {
1062
+ return new Command8("list").alias("ls").description("List installed skills across detected agents").option("-g, --global", "only global skills").option("--json", "machine-readable output").action((opts) => {
920
1063
  const scope = { global: opts.global };
921
1064
  const skills = installedSkills(scope);
922
1065
  if (opts.json) {
@@ -929,44 +1072,44 @@ function listCommand() {
929
1072
  }
930
1073
  for (const s of skills) {
931
1074
  const parsed = parseSkillMd(s.raw);
932
- const desc = "error" in parsed ? pc7.red("(invalid SKILL.md)") : pc7.dim(parsed.description.slice(0, 80));
933
- console.log(`${pc7.bold(s.name)} ${pc7.dim(`[${s.agent}/${s.scope}]`)}
1075
+ const desc = "error" in parsed ? pc8.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc8.dim(parsed.description.slice(0, 80));
1076
+ console.log(`${pc8.bold(s.name)} ${pc8.dim(`[${s.agent}/${s.scope}]`)}
934
1077
  ${desc}`);
935
1078
  }
936
1079
  });
937
1080
  }
938
1081
 
939
1082
  // ../cli/src/commands/remove.ts
940
- import { Command as Command8 } from "commander";
941
- import { rmSync as rmSync2 } from "fs";
942
- import pc8 from "picocolors";
1083
+ import { Command as Command9 } from "commander";
1084
+ import { rmSync as rmSync3 } from "fs";
1085
+ import pc9 from "picocolors";
943
1086
  function runRemove(names, scope = {}) {
944
1087
  const installed = installedSkills(scope);
945
1088
  const targets = installed.filter((s) => names.includes(s.name));
946
1089
  const removed = [];
947
1090
  const lines = [];
948
1091
  for (const t of targets) {
949
- rmSync2(t.path, { recursive: true, force: true });
1092
+ rmSync3(t.path, { recursive: true, force: true });
950
1093
  removed.push(t.path);
951
- lines.push(pc8.green(`\u2713 removed ${t.name} (${t.agent})`));
1094
+ lines.push(pc9.green(`\u2713 removed ${t.name} (${t.agent})`));
952
1095
  }
953
1096
  for (const n of names) {
954
- if (!targets.some((t) => t.name === n)) lines.push(pc8.yellow(`- ${n} not installed`));
1097
+ if (!targets.some((t) => t.name === n)) lines.push(pc9.yellow(`- ${n} not installed`));
955
1098
  }
956
1099
  return { removed, output: lines.join("\n") };
957
1100
  }
958
1101
  function removeCommand() {
959
- return new Command8("remove").alias("rm").description("Remove installed skills").argument("<names...>", "skill names to remove").option("-g, --global", "only global skills").action((names, opts) => {
1102
+ return new Command9("remove").alias("rm").description("Remove installed skills").argument("<names...>", "skill names to remove").option("-g, --global", "only global skills").action((names, opts) => {
960
1103
  const run = runRemove(names, { global: opts.global });
961
1104
  console.log(run.output);
962
1105
  });
963
1106
  }
964
1107
 
965
1108
  // ../cli/src/commands/update.ts
966
- import { Command as Command9 } from "commander";
1109
+ import { Command as Command10 } from "commander";
967
1110
  import { writeFileSync as writeFileSync5 } from "fs";
968
1111
  import { join as join5 } from "path";
969
- import pc9 from "picocolors";
1112
+ import pc10 from "picocolors";
970
1113
  async function runUpdate(names, flags, scope = {}) {
971
1114
  const { api } = createClient(flags);
972
1115
  const installed = installedSkills(scope).filter((s) => names.length === 0 || names.includes(s.name));
@@ -975,27 +1118,27 @@ async function runUpdate(names, flags, scope = {}) {
975
1118
  for (const s of installed) {
976
1119
  const slug = await findRegistrySlug(api, s);
977
1120
  if (!slug) {
978
- lines.push(pc9.dim(`- ${s.name} skipped (no registry match)`));
1121
+ lines.push(pc10.dim(`- ${s.name} skipped (no registry match)`));
979
1122
  continue;
980
1123
  }
981
1124
  try {
982
1125
  const [owner, name] = slug.split("/");
983
1126
  const skill = await api(`/api/skills/${owner}/${name}`);
984
- const next = reconstructSkillMd(skill);
1127
+ const next = skillMdFor(skill);
985
1128
  if (next.trim() === s.raw.trim()) {
986
- lines.push(pc9.dim(`- ${s.name} already up to date`));
1129
+ lines.push(pc10.dim(`- ${s.name} already up to date`));
987
1130
  continue;
988
1131
  }
989
1132
  const result = lint(next, { slug: s.name });
990
1133
  if (!result.ok) {
991
- lines.push(pc9.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
1134
+ lines.push(pc10.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
992
1135
  continue;
993
1136
  }
994
1137
  writeFileSync5(join5(s.path, "SKILL.md"), next, "utf8");
995
1138
  updated.push(s.name);
996
- lines.push(pc9.green(`\u2713 updated ${s.name}`));
1139
+ lines.push(pc10.green(`\u2713 updated ${s.name}`));
997
1140
  } catch {
998
- lines.push(pc9.red(`\u2717 ${s.name} update error`));
1141
+ lines.push(pc10.red(`\u2717 ${s.name} update error`));
999
1142
  }
1000
1143
  }
1001
1144
  return { updated, output: lines.join("\n") || "Nothing to update." };
@@ -1006,7 +1149,7 @@ async function findRegistrySlug(api, s) {
1006
1149
  return hit?.slug ?? null;
1007
1150
  }
1008
1151
  function updateCommand() {
1009
- return new Command9("update").description("Update installed skills from the registry (re-lints before writing)").argument("[names...]", "specific skills to update (default: all)").option("-g, --global", "only global skills").action(async (names, opts, cmd) => {
1152
+ return new Command10("update").description("Update installed skills from the registry (re-lints before writing)").argument("[names...]", "specific skills to update (default: all)").option("-g, --global", "only global skills").action(async (names, opts, cmd) => {
1010
1153
  const merged = { ...cmd.parent?.opts(), ...opts };
1011
1154
  const run = await runUpdate(names, merged, { global: opts.global });
1012
1155
  console.log(run.output);
@@ -1014,8 +1157,8 @@ function updateCommand() {
1014
1157
  }
1015
1158
 
1016
1159
  // ../cli/src/commands/publish.ts
1017
- import { Command as Command10 } from "commander";
1018
- import pc10 from "picocolors";
1160
+ import { Command as Command11 } from "commander";
1161
+ import pc11 from "picocolors";
1019
1162
  var defaultDeps2 = {
1020
1163
  resolve: resolveSource,
1021
1164
  async submit(payload, flags) {
@@ -1038,23 +1181,23 @@ async function runPublish(target, flags, deps = defaultDeps2) {
1038
1181
  const report = toText([{ file: src.path, result }], { decorate });
1039
1182
  if (!result.ok) {
1040
1183
  return { result, published: false, exitCode: 1, output: `${report}
1041
- ${pc10.red("\u2717 Refusing to publish: fix the errors above.")}` };
1184
+ ${pc11.red("\u2717 Refusing to publish: fix the errors above.")}` };
1042
1185
  }
1043
1186
  const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
1044
1187
  if (hasWarn && !flags.force) {
1045
1188
  return { result, published: false, exitCode: 1, output: `${report}
1046
- ${pc10.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
1189
+ ${pc11.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
1047
1190
  }
1048
1191
  if (flags.dryRun) {
1049
1192
  return { result, published: false, exitCode: 0, output: `${report}
1050
- ${pc10.green("\u2713 Dry run: would publish.")}` };
1193
+ ${pc11.green("\u2713 Dry run: would publish.")}` };
1051
1194
  }
1052
1195
  const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
1053
1196
  return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
1054
- ${pc10.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
1197
+ ${pc11.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
1055
1198
  }
1056
1199
  function publishCommand() {
1057
- 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) => {
1200
+ return new Command11("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) => {
1058
1201
  const merged = { ...cmd.parent?.opts(), ...opts };
1059
1202
  const run = await runPublish(target, merged);
1060
1203
  console.log(run.output);
@@ -1063,10 +1206,10 @@ function publishCommand() {
1063
1206
  }
1064
1207
 
1065
1208
  // ../cli/src/commands/login.ts
1066
- import { Command as Command11 } from "commander";
1209
+ import { Command as Command12 } from "commander";
1067
1210
  import * as p3 from "@clack/prompts";
1068
1211
  function loginCommand() {
1069
- 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) => {
1212
+ return new Command12("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) => {
1070
1213
  let token = opts.token;
1071
1214
  if (!token) {
1072
1215
  const answer = await p3.password({ message: "Paste your SkillMD token (from your account page)" });
@@ -1089,7 +1232,7 @@ function loginCommand() {
1089
1232
  });
1090
1233
  }
1091
1234
  function logoutCommand() {
1092
- return new Command11("logout").description("Remove the stored SkillMD token").action(() => {
1235
+ return new Command12("logout").description("Remove the stored SkillMD token").action(() => {
1093
1236
  const cfg = readConfig();
1094
1237
  delete cfg.token;
1095
1238
  writeConfig(cfg);
@@ -1099,7 +1242,7 @@ function logoutCommand() {
1099
1242
 
1100
1243
  // ../cli/src/commands/interactive.ts
1101
1244
  import * as p4 from "@clack/prompts";
1102
- import pc11 from "picocolors";
1245
+ import pc12 from "picocolors";
1103
1246
  function bail(v) {
1104
1247
  if (p4.isCancel(v)) {
1105
1248
  p4.cancel("Cancelled.");
@@ -1108,105 +1251,107 @@ function bail(v) {
1108
1251
  return false;
1109
1252
  }
1110
1253
  async function runInteractive() {
1111
- p4.intro(pc11.bgCyan(pc11.black(" skillmd ")));
1112
- const action = await p4.select({
1113
- message: "What would you like to do?",
1114
- options: [
1115
- { value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
1116
- { value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
1117
- { value: "search", label: "Search the registry" },
1118
- { value: "init", label: "Create a new skill" },
1119
- { value: "list", label: "List installed skills" },
1120
- { value: "help", label: "Show all commands" }
1121
- ]
1122
- });
1123
- if (bail(action)) return;
1124
- switch (action) {
1125
- case "lint":
1126
- case "scan": {
1127
- const path = await p4.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
1128
- if (bail(path)) return;
1129
- const target = String(path || ".");
1130
- const s = p4.spinner();
1131
- s.start(action === "lint" ? "Linting" : "Scanning");
1132
- try {
1133
- const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
1134
- s.stop("Done");
1135
- console.log(run.output);
1136
- process.exitCode = run.exitCode;
1137
- } catch (err) {
1138
- s.stop(pc11.red(err instanceof Error ? err.message : String(err)));
1139
- process.exitCode = 1;
1254
+ p4.intro(pc12.bgCyan(pc12.black(" skillmd ")));
1255
+ for (; ; ) {
1256
+ const action = await p4.select({
1257
+ message: "What would you like to do?",
1258
+ options: [
1259
+ { value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
1260
+ { value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
1261
+ { value: "search", label: "Search the registry" },
1262
+ { value: "init", label: "Create a new skill" },
1263
+ { value: "list", label: "List installed skills" },
1264
+ { value: "help", label: "Show all commands" },
1265
+ { value: "quit", label: "Quit" }
1266
+ ]
1267
+ });
1268
+ if (bail(action) || action === "quit") break;
1269
+ switch (action) {
1270
+ case "lint":
1271
+ case "scan": {
1272
+ const path = await p4.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
1273
+ if (bail(path)) break;
1274
+ const target = String(path || ".");
1275
+ const s = p4.spinner();
1276
+ s.start(action === "lint" ? "Linting" : "Scanning");
1277
+ try {
1278
+ const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
1279
+ s.stop("Done");
1280
+ console.log(run.output);
1281
+ process.exitCode = run.exitCode;
1282
+ } catch (err) {
1283
+ s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
1284
+ process.exitCode = 1;
1285
+ }
1286
+ break;
1140
1287
  }
1141
- break;
1142
- }
1143
- case "search": {
1144
- const query = await p4.text({ message: "Search the registry for" });
1145
- if (bail(query)) return;
1146
- const verified = await p4.confirm({ message: "Verified skills only?", initialValue: false });
1147
- if (bail(verified)) return;
1148
- const s = p4.spinner();
1149
- s.start("Searching");
1150
- try {
1151
- const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
1152
- s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
1153
- console.log(run.output);
1154
- } catch (err) {
1155
- s.stop(pc11.red(err instanceof Error ? err.message : String(err)));
1156
- process.exitCode = 1;
1288
+ case "search": {
1289
+ const query = await p4.text({ message: "Search the registry for" });
1290
+ if (bail(query)) break;
1291
+ const verified = await p4.confirm({ message: "Verified skills only?", initialValue: false });
1292
+ if (bail(verified)) break;
1293
+ const s = p4.spinner();
1294
+ s.start("Searching");
1295
+ try {
1296
+ const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
1297
+ s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
1298
+ console.log(run.output);
1299
+ } catch (err) {
1300
+ s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
1301
+ process.exitCode = 1;
1302
+ }
1303
+ break;
1157
1304
  }
1158
- break;
1159
- }
1160
- case "init": {
1161
- const name = await p4.text({ message: "Skill name", placeholder: "my-skill" });
1162
- if (bail(name)) return;
1163
- const description = await p4.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
1164
- if (bail(description)) return;
1165
- try {
1166
- const file = initSkill(String(name), { description: String(description || "") });
1167
- p4.log.success(`Created ${pc11.bold(file)}`);
1168
- } catch (err) {
1169
- p4.log.error(err instanceof Error ? err.message : String(err));
1170
- process.exitCode = 1;
1305
+ case "init": {
1306
+ const name = await p4.text({ message: "Skill name", placeholder: "my-skill" });
1307
+ if (bail(name)) break;
1308
+ const description = await p4.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
1309
+ if (bail(description)) break;
1310
+ try {
1311
+ const file = initSkill(String(name), { description: String(description || "") });
1312
+ p4.log.success(`Created ${pc12.bold(file)}`);
1313
+ } catch (err) {
1314
+ p4.log.error(err instanceof Error ? err.message : String(err));
1315
+ process.exitCode = 1;
1316
+ }
1317
+ break;
1171
1318
  }
1172
- break;
1173
- }
1174
- case "list": {
1175
- const global = await p4.confirm({ message: "Include global skills?", initialValue: true });
1176
- if (bail(global)) return;
1177
- const skills = installedSkills({ global: Boolean(global) });
1178
- if (skills.length === 0) {
1179
- p4.log.info("No installed skills found.");
1180
- } else {
1181
- p4.log.message(
1182
- skills.map((sk) => {
1183
- const parsed = parseSkillMd(sk.raw);
1184
- const desc = "error" in parsed ? pc11.red("(invalid)") : pc11.dim(parsed.description.slice(0, 70));
1185
- return `${pc11.bold(sk.name)} ${pc11.dim(`[${sk.agent}]`)}
1319
+ case "list": {
1320
+ const skills = [...installedSkills({}), ...installedSkills({ global: true })];
1321
+ if (skills.length === 0) {
1322
+ p4.log.info("No installed skills found (checked this folder and your user directory).");
1323
+ } else {
1324
+ p4.log.message(
1325
+ skills.map((sk) => {
1326
+ const parsed = parseSkillMd(sk.raw);
1327
+ const desc = "error" in parsed ? pc12.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc12.dim(parsed.description.slice(0, 70));
1328
+ return `${pc12.bold(sk.name)} ${pc12.dim(`[${sk.agent}/${sk.scope}]`)}
1186
1329
  ${desc}`;
1187
- }).join("\n")
1330
+ }).join("\n")
1331
+ );
1332
+ }
1333
+ break;
1334
+ }
1335
+ case "help": {
1336
+ p4.note(
1337
+ [
1338
+ `${pc12.bold("lint")} [path] validate SKILL.md + quality score`,
1339
+ `${pc12.bold("scan")} [path] security scan`,
1340
+ `${pc12.bold("search")} <query> search the registry`,
1341
+ `${pc12.bold("add")} <source> install a skill (lints first)`,
1342
+ `${pc12.bold("info")} <slug> registry details for a skill`,
1343
+ `${pc12.bold("init")} [name] scaffold a new skill`,
1344
+ `${pc12.bold("list / remove / update")} manage installed skills`,
1345
+ `${pc12.bold("publish")} [path] publish (fails on lint errors)`,
1346
+ `${pc12.bold("rules / login")}`
1347
+ ].join("\n"),
1348
+ "Commands (run: skillmd <command>)"
1188
1349
  );
1350
+ break;
1189
1351
  }
1190
- break;
1191
- }
1192
- case "help": {
1193
- p4.note(
1194
- [
1195
- `${pc11.bold("lint")} [path] validate SKILL.md + quality score`,
1196
- `${pc11.bold("scan")} [path] security scan`,
1197
- `${pc11.bold("search")} <query> search the registry`,
1198
- `${pc11.bold("add")} <source> install a skill (lints first)`,
1199
- `${pc11.bold("init")} [name] scaffold a new skill`,
1200
- `${pc11.bold("list / remove / update")} manage installed skills`,
1201
- `${pc11.bold("publish")} [path] publish (fails on lint errors)`,
1202
- `${pc11.bold("rules / login")}`
1203
- ].join("\n"),
1204
- "Commands (run: skillmd <command>)"
1205
- );
1206
- break;
1207
1352
  }
1208
1353
  }
1209
- p4.outro(pc11.dim("skillmd.com"));
1354
+ p4.outro(pc12.dim("skillmd.com"));
1210
1355
  }
1211
1356
 
1212
1357
  // ../cli/src/index.ts
@@ -1215,12 +1360,13 @@ try {
1215
1360
  version = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
1216
1361
  } catch {
1217
1362
  }
1218
- var program = new Command12("skillmd").version(version).description("Lint, validate, and install Agent Skills from the SkillMD registry.").option("--json", "machine-readable output").option("--token <token>", "SkillMD personal access token").option("--api <url>", "override the registry API base URL");
1363
+ var program = new Command13("skillmd").version(version).description("Lint, validate, and install Agent Skills from the SkillMD registry.").option("--json", "machine-readable output").option("--token <token>", "SkillMD personal access token").option("--api <url>", "override the registry API base URL");
1219
1364
  program.addCommand(lintCommand());
1220
1365
  program.addCommand(scanCommand());
1221
1366
  program.addCommand(rulesCommand());
1222
1367
  program.addCommand(initCommand());
1223
1368
  program.addCommand(searchCommand());
1369
+ program.addCommand(infoCommand());
1224
1370
  program.addCommand(addCommand());
1225
1371
  program.addCommand(listCommand());
1226
1372
  program.addCommand(removeCommand());