skillmds 1.2.0 → 1.3.0

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.
@@ -19,6 +19,12 @@ function parseSource(arg, opts = {}) {
19
19
  const exists = opts.exists ?? existsSync;
20
20
  const a = arg.trim();
21
21
  if (!a) throw new Error("a source is required: owner/name, a GitHub URL, or a local path");
22
+ if (/^(plugin|pack):/i.test(a)) {
23
+ const body2 = a.slice(a.indexOf(":") + 1);
24
+ const m2 = body2.match(/^([^/#@\s]+)\/([^/#@\s]+)$/);
25
+ if (!m2) throw new Error(`"${a}" is not a valid plugin source \u2014 expected plugin:owner/slug`);
26
+ return { kind: "plugin", owner: m2[1], slug: m2[2], display: a };
27
+ }
22
28
  if (a === "~" || a.startsWith("~/") || a.startsWith("~\\")) {
23
29
  return { kind: "local", path: join(homedir(), a.slice(1)), display: a };
24
30
  }
@@ -54,6 +60,10 @@ function sourceId(spec) {
54
60
  switch (spec.kind) {
55
61
  case "slug":
56
62
  return `registry:${spec.owner}/${spec.name}`;
63
+ // Members are recorded under their own registry slug, not this one — the
64
+ // lock file tracks what was written, and a plugin is only how they arrived.
65
+ case "plugin":
66
+ return `plugin:${spec.owner}/${spec.slug}`;
57
67
  case "github":
58
68
  return `github:${spec.owner}/${spec.repo}${spec.subpath ? `/${spec.subpath}` : ""}${spec.ref ? `#${spec.ref}` : ""}${spec.skill ? `@${spec.skill}` : ""}`;
59
69
  case "gist":
@@ -182,13 +192,14 @@ async function fetchBundle(base, slug, token, opts = {}) {
182
192
  let bytes = 0;
183
193
  for (const f of data.files) {
184
194
  let contents;
195
+ let fromUrl = false;
185
196
  if (f.content_base64 != null) {
186
197
  contents = Buffer.from(f.content_base64, "base64");
187
198
  } else if (f.source_url && isAllowedSourceUrl(f.source_url)) {
188
- if (!f.sha256) throw new IntegrityError(`"${f.path}" is delivered by URL but the registry recorded no sha256 for it \u2014 refusing to install unverifiable content`);
189
199
  const r = await doFetch(f.source_url).catch(() => null);
190
200
  if (!r || !r.ok) throw new Error(`could not fetch companion file "${f.path}" from ${hostOf(f.source_url)}`);
191
201
  contents = Buffer.from(await r.arrayBuffer());
202
+ fromUrl = true;
192
203
  } else {
193
204
  continue;
194
205
  }
@@ -197,10 +208,10 @@ async function fetchBundle(base, slug, token, opts = {}) {
197
208
  if (f.sha256) {
198
209
  const got = createHash("sha256").update(contents).digest("hex");
199
210
  if (got !== f.sha256) throw new IntegrityError(`integrity check failed for "${f.path}" (expected ${f.sha256.slice(0, 12)}\u2026, got ${got.slice(0, 12)}\u2026)`);
200
- } else if (f.path !== "SKILL.md") {
211
+ } else if (!fromUrl && f.path !== "SKILL.md") {
201
212
  throw new IntegrityError(`"${f.path}" has no sha256 in the registry bundle \u2014 refusing to install unverifiable content`);
202
213
  }
203
- out.push({ path: f.path, contents });
214
+ out.push({ path: f.path, contents, ...fromUrl && !f.sha256 ? { unverified: true } : {} });
204
215
  }
205
216
  return out;
206
217
  }
@@ -313,6 +324,9 @@ async function resolveSource(arg, opts = {}) {
313
324
  if (raw.length > MAX_PACK_BYTES) throw tooBig(raw.length);
314
325
  return [{ path: spec.display, raw, slug: `gist-${spec.id.slice(0, 8)}` }];
315
326
  }
327
+ if (spec.kind === "plugin") {
328
+ throw new Error(`"${spec.display}" is a plugin, not a single skill \u2014 install it with \`skillmd add ${spec.display}\`.`);
329
+ }
316
330
  const gh = asGithub(spec);
317
331
  const { dir, cleanup } = await downloadGithub(gh, "skillmd-src-", opts, Boolean(gh.subpath));
318
332
  try {
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  uninstallSkill,
31
31
  upsertEntry,
32
32
  writeConfig
33
- } from "./chunk-A7BKAUUO.js";
33
+ } from "./chunk-FVWCI37S.js";
34
34
 
35
35
  // src/index.ts
36
36
  import { Command as Command13 } from "commander";
@@ -319,12 +319,12 @@ function lintCommand() {
319
319
  const s = p.spinner();
320
320
  s.start(`Scanning ${target}`);
321
321
  try {
322
- const run2 = await runLint(target, opts);
323
- s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
324
- console.log(run2.output);
325
- const failed = run2.results.filter((r) => !r.result.ok).length;
322
+ const run3 = await runLint(target, opts);
323
+ s.stop(`Scanned ${run3.results.length} skill${run3.results.length === 1 ? "" : "s"}`);
324
+ console.log(run3.output);
325
+ const failed = run3.results.filter((r) => !r.result.ok).length;
326
326
  p.outro(failed === 0 ? pc2.green("All good") : pc2.red(`${failed} skill${failed === 1 ? "" : "s"} failed \u2014 details above`));
327
- process.exitCode = run2.exitCode;
327
+ process.exitCode = run3.exitCode;
328
328
  } catch (err) {
329
329
  s.stop(pc2.red("No skills found"));
330
330
  p.outro(err instanceof Error ? err.message : String(err));
@@ -334,9 +334,9 @@ function lintCommand() {
334
334
  }
335
335
  process.stderr.write(`Looking for SKILL.md under ${target} \u2026
336
336
  `);
337
- const run = await runLint(target, opts);
338
- console.log(run.output);
339
- process.exitCode = run.exitCode;
337
+ const run2 = await runLint(target, opts);
338
+ console.log(run2.output);
339
+ process.exitCode = run2.exitCode;
340
340
  });
341
341
  }
342
342
 
@@ -375,9 +375,9 @@ async function runScan(target, flags) {
375
375
  }
376
376
  function scanCommand() {
377
377
  return new Command2("scan").description("Security-scan SKILL.md file(s) for scripts, network calls and secret access").argument("[target]", "local path or GitHub source", ".").option("--deny <flags...>", "exit non-zero if any of these flags are present (e.g. executes_scripts)").option("--format <fmt>", "text | json", "text").action(async (target, opts) => {
378
- const run = await runScan(target, opts);
379
- console.log(run.output);
380
- process.exitCode = run.exitCode;
378
+ const run2 = await runScan(target, opts);
379
+ console.log(run2.output);
380
+ process.exitCode = run2.exitCode;
381
381
  });
382
382
  }
383
383
 
@@ -504,8 +504,8 @@ async function runInit(nameArg, flags, deps = defaultInitDeps(flags)) {
504
504
  const file = initSkill(name, { dir: flags.dir, description: flags.description, license: flags.license });
505
505
  return { exitCode: 0, file, output: flags.json ? JSON.stringify({ ok: true, file }, null, 2) : `Created ${file}` };
506
506
  } catch (e) {
507
- const msg = e instanceof Error ? e.message : String(e);
508
- return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg }, null, 2) : pc5.red(msg) };
507
+ const msg2 = e instanceof Error ? e.message : String(e);
508
+ return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg2 }, null, 2) : pc5.red(msg2) };
509
509
  }
510
510
  }
511
511
  function initCommand() {
@@ -563,8 +563,8 @@ async function runSearch(query, flags) {
563
563
  function searchCommand() {
564
564
  return new Command5("search").description("Search the SkillMD registry").argument("<query>", "search terms").option("--category <slug>", "filter by category").option("--verified", "only verified skills").option("--type <type>", "single | pack").option("--min-rating <n>", "minimum average rating (0-5)").option("--limit <n>", "max results", "20").option("--json", "machine-readable output").action(async (query, opts, cmd) => {
565
565
  const merged = { ...cmd.parent?.opts(), ...opts };
566
- const run = await runSearch(query, merged);
567
- console.log(run.output);
566
+ const run2 = await runSearch(query, merged);
567
+ console.log(run2.output);
568
568
  });
569
569
  }
570
570
 
@@ -613,19 +613,99 @@ async function runInfo(slug, flags, deps = {}) {
613
613
  function infoCommand() {
614
614
  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) => {
615
615
  const flags = { ...cmd.parent?.opts() };
616
- const run = await runInfo(slug, flags);
617
- if (run.output) (run.ok ? console.log : console.error)(run.output);
618
- process.exitCode = run.exitCode;
616
+ const run2 = await runInfo(slug, flags);
617
+ if (run2.output) (run2.ok ? console.log : console.error)(run2.output);
618
+ process.exitCode = run2.exitCode;
619
619
  });
620
620
  }
621
621
 
622
622
  // src/commands/add.ts
623
623
  import { Command as Command7, Option } from "commander";
624
- import { homedir as homedir3 } from "os";
624
+ import { homedir as homedir4 } from "os";
625
625
  import * as p3 from "@clack/prompts";
626
626
  import { dirname, resolve as resolve3 } from "path";
627
627
  import pc8 from "picocolors";
628
628
  import { lint as lint2, parseSkillMd as parseSkillMd2 } from "@skillmds/core";
629
+
630
+ // src/claudePlugin.ts
631
+ import { execFile } from "child_process";
632
+ import { existsSync as existsSync2 } from "fs";
633
+ import { homedir as homedir3 } from "os";
634
+ import { delimiter, join as join2 } from "path";
635
+ import { promisify } from "util";
636
+ var run = promisify(execFile);
637
+ function resolveClaudeBin(env = process.env) {
638
+ const override = env.SKILLMD_CLAUDE_BIN;
639
+ if (override) return existsSync2(override) ? override : null;
640
+ const win = process.platform === "win32";
641
+ const exts = win ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
642
+ const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
643
+ const home = env.USERPROFILE || env.HOME || homedir3();
644
+ dirs.push(join2(home, ".local", "bin"), join2(home, ".claude", "local"));
645
+ for (const dir of dirs) {
646
+ for (const ext of exts) {
647
+ const candidate = join2(dir, `claude${ext.toLowerCase()}`);
648
+ if (existsSync2(candidate)) return candidate;
649
+ }
650
+ }
651
+ return null;
652
+ }
653
+ var needsShell = (bin) => /\.(cmd|bat)$/i.test(bin);
654
+ var MARKETPLACE_URL = "https://skillmd.com/.claude-plugin/marketplace.json";
655
+ var MARKETPLACE_NAME = "skillmd";
656
+ var TIMEOUT_MS = 12e4;
657
+ function exec(deps) {
658
+ return deps.exec ?? ((file, args) => {
659
+ const bin = file === "claude" ? resolveClaudeBin() : file;
660
+ if (!bin) return Promise.reject(new Error("claude not found on PATH"));
661
+ return run(bin, args, { timeout: TIMEOUT_MS, windowsHide: true, shell: needsShell(bin) }).then((r) => ({
662
+ stdout: String(r.stdout ?? ""),
663
+ stderr: String(r.stderr ?? "")
664
+ }));
665
+ });
666
+ }
667
+ function marketplacePluginName(owner, slug) {
668
+ const clean = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
669
+ const base = clean(slug) || "plugin";
670
+ return owner.toLowerCase() === "skillmd" ? base : `${clean(owner)}-${base}`;
671
+ }
672
+ async function claudeCliAvailable(deps = {}) {
673
+ try {
674
+ await exec(deps)("claude", ["--version"]);
675
+ return true;
676
+ } catch {
677
+ return false;
678
+ }
679
+ }
680
+ async function installClaudePlugin(owner, slug, deps = {}) {
681
+ const sh = exec(deps);
682
+ const qualified = `${marketplacePluginName(owner, slug)}@${MARKETPLACE_NAME}`;
683
+ try {
684
+ await sh("claude", ["plugin", "marketplace", "add", MARKETPLACE_URL]);
685
+ } catch (e) {
686
+ return { ok: false, reason: `could not add the SkillMD marketplace: ${msg(e)}` };
687
+ }
688
+ let out;
689
+ try {
690
+ const r = await sh("claude", ["plugin", "install", qualified]);
691
+ out = `${r.stdout}
692
+ ${r.stderr}`;
693
+ } catch (e) {
694
+ return { ok: false, reason: msg(e) };
695
+ }
696
+ if (/Successfully installed plugin/i.test(out)) return { ok: true, name: qualified, marketplace: MARKETPLACE_NAME };
697
+ const failed = /Failed to install plugin[^:]*:\s*(.+)/i.exec(out);
698
+ return { ok: false, reason: (failed?.[1] ?? out).trim().split(/\r?\n/)[0] ?? "install did not report success" };
699
+ }
700
+ function msg(e) {
701
+ if (e && typeof e === "object" && "stderr" in e) {
702
+ const s = String(e.stderr ?? "").trim();
703
+ if (s) return s.split(/\r?\n/)[0];
704
+ }
705
+ return e instanceof Error ? e.message.split(/\r?\n/)[0] : String(e);
706
+ }
707
+
708
+ // src/commands/add.ts
629
709
  var infoNotice = (text3) => ({ text: text3, style: "info" });
630
710
  var warnNotice = (text3) => ({ text: text3, style: "warn" });
631
711
  var skipNotice = (text3) => ({ text: text3, style: "skip" });
@@ -662,6 +742,10 @@ async function promptAgentsInteractive(ctx) {
662
742
  });
663
743
  return p3.isCancel(picked) ? null : picked;
664
744
  }
745
+ function unverifiedFilesNote(n) {
746
+ const [f, it] = n === 1 ? ["file", "it"] : ["files", "them"];
747
+ return `${n} companion ${f} came straight from the source repo \u2014 the registry holds no checksum for ${it}, so only the connection was verified`;
748
+ }
665
749
  function registryFallbackNote(host) {
666
750
  return `installed from GitHub because the SkillMD registry (${host}) was unreachable \u2014 this copy is not the registry-reviewed version`;
667
751
  }
@@ -689,6 +773,77 @@ function provenanceFor(arg) {
689
773
  return `unknown:${arg}`;
690
774
  }
691
775
  }
776
+ async function registryCandidate(owner, name, source, client) {
777
+ const registrySlug = `${owner}/${name}`;
778
+ const skill = await client.api(`/api/skills/${owner}/${name}`);
779
+ const skillName = skill.slug.split("/")[1] || name;
780
+ const meta = {
781
+ name: skillName,
782
+ slug: skillName,
783
+ registrySlug,
784
+ source,
785
+ commit_sha: skill.commit_sha ?? void 0,
786
+ verified: skill.verified,
787
+ type: skill.type,
788
+ securityFlags: skill.security_flags
789
+ };
790
+ if (skill.type === "pack") {
791
+ const bundle = await fetchBundle(client.base, registrySlug, client.token);
792
+ if (bundle && bundle.length) {
793
+ const sk = bundle.find((file) => file.path === "SKILL.md");
794
+ const unverified = bundle.filter((file) => file.unverified).length;
795
+ return {
796
+ ...meta,
797
+ raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill),
798
+ files: bundle,
799
+ ...unverified ? { note: unverifiedFilesNote(unverified) } : {}
800
+ };
801
+ }
802
+ const pack = await resolvePackFiles(skill);
803
+ if (pack) return { ...meta, ...pack };
804
+ }
805
+ return { ...meta, raw: skillMdFor(skill) };
806
+ }
807
+ async function fetchPackMembers(api, owner, slug) {
808
+ const PAGE = 200;
809
+ const members = [];
810
+ let name = slug;
811
+ let total = Infinity;
812
+ for (let offset = 0; members.length < total; offset += PAGE) {
813
+ const page = await api(`/api/packs/${owner}/${slug}?limit=${PAGE}&offset=${offset}`);
814
+ name = page.name || name;
815
+ total = Number(page.item_total ?? 0);
816
+ const got = (page.items ?? []).map((i) => ({ slug: String(i.slug ?? ""), type: i.type, verified: Boolean(i.verified) })).filter((i) => i.slug);
817
+ if (!got.length) break;
818
+ members.push(...got);
819
+ }
820
+ return { name, members };
821
+ }
822
+ function packDirNames(slugs) {
823
+ const used = /* @__PURE__ */ new Set();
824
+ return slugs.map((slug) => {
825
+ const [owner, name] = slug.split("/");
826
+ let dir = name ?? slug;
827
+ if (used.has(dir)) dir = `${owner}-${name}`;
828
+ used.add(dir);
829
+ return dir;
830
+ });
831
+ }
832
+ async function fetchPackArchive(base, owner, slug, token, doFetch = fetch) {
833
+ const headers = { accept: "application/zip" };
834
+ if (token) headers.authorization = `Bearer ${token}`;
835
+ const res = await doFetch(`${base}/api/packs/${owner}/${slug}/download`, { headers });
836
+ if (!res.ok) throw new RegistryError(`SkillMD API ${res.status} on /api/packs/${owner}/${slug}/download`, res.status);
837
+ const bytes = new Uint8Array(await res.arrayBuffer());
838
+ const { unzipSync, strFromU8 } = await import("fflate");
839
+ const entries = unzipSync(bytes);
840
+ const out = /* @__PURE__ */ new Map();
841
+ for (const [path, data] of Object.entries(entries)) {
842
+ const m = /^(.+)\/SKILL\.md$/.exec(path);
843
+ if (m) out.set(m[1], strFromU8(data));
844
+ }
845
+ return out;
846
+ }
692
847
  function makeDefaultDeps(flags) {
693
848
  const canPrompt = () => isInteractive(flags, {
694
849
  stdinTTY: Boolean(process.stdin.isTTY),
@@ -699,6 +854,55 @@ function makeDefaultDeps(flags) {
699
854
  async resolve(arg, f) {
700
855
  const spec = parseSource(arg, { ref: f.ref });
701
856
  const source = sourceId(spec);
857
+ if (spec.kind === "plugin") {
858
+ const { api: api2, base: apiBase2, token: token2, fetch: doFetch } = createClient(f);
859
+ const { name: packName, members } = await fetchPackMembers(api2, spec.owner, spec.slug);
860
+ if (!members.length) throw new Error(`plugin ${spec.owner}/${spec.slug} has no installable skills`);
861
+ const archive = await fetchPackArchive(apiBase2, spec.owner, spec.slug, token2, doFetch);
862
+ const dirs = packDirNames(members.map((m) => m.slug));
863
+ const out = [];
864
+ const missing = [];
865
+ members.forEach((m, i) => {
866
+ const raw = archive.get(dirs[i]);
867
+ if (!raw) {
868
+ missing.push(m.slug);
869
+ return;
870
+ }
871
+ const name = dirs[i];
872
+ out.push({
873
+ name,
874
+ slug: name,
875
+ registrySlug: m.slug,
876
+ source,
877
+ raw,
878
+ verified: m.verified,
879
+ type: m.type
880
+ });
881
+ });
882
+ const multi = out.filter((c) => c.type === "pack");
883
+ if (multi.length) {
884
+ let n = 0;
885
+ await Promise.all(Array.from({ length: Math.min(4, multi.length) }, async () => {
886
+ for (let i = n++; i < multi.length; i = n++) {
887
+ const c = multi[i];
888
+ try {
889
+ const bundle = await fetchBundle(apiBase2, c.registrySlug, token2);
890
+ if (!bundle?.length) continue;
891
+ const sk = bundle.find((file) => file.path === "SKILL.md");
892
+ const unverified = bundle.filter((file) => file.unverified).length;
893
+ c.files = bundle;
894
+ if (sk) c.raw = sk.contents.toString("utf8");
895
+ if (unverified) c.note = unverifiedFilesNote(unverified);
896
+ } catch {
897
+ }
898
+ }
899
+ }));
900
+ }
901
+ if (!out.length) throw new Error(`plugin ${spec.owner}/${spec.slug} returned no readable skills`);
902
+ const first = out[0];
903
+ first.note = `${out.length} skill${out.length === 1 ? "" : "s"} from the ${packName} plugin` + (missing.length ? ` (${missing.length} member${missing.length === 1 ? "" : "s"} were not in the archive)` : "");
904
+ return out;
905
+ }
702
906
  if (spec.kind !== "slug") {
703
907
  const rs = await resolveSource(argWithRef(arg, f.ref));
704
908
  return rs.map((r) => ({
@@ -720,28 +924,7 @@ function makeDefaultDeps(flags) {
720
924
  })();
721
925
  const registrySlug = `${spec.owner}/${spec.name}`;
722
926
  try {
723
- const skill = await api(`/api/skills/${spec.owner}/${spec.name}`);
724
- const skillName = skill.slug.split("/")[1] || spec.name;
725
- const meta = {
726
- name: skillName,
727
- slug: skillName,
728
- registrySlug,
729
- source,
730
- commit_sha: skill.commit_sha ?? void 0,
731
- verified: skill.verified,
732
- type: skill.type,
733
- securityFlags: skill.security_flags
734
- };
735
- if (skill.type === "pack") {
736
- const bundle = await fetchBundle(apiBase, registrySlug, token);
737
- if (bundle && bundle.length) {
738
- const sk = bundle.find((file) => file.path === "SKILL.md");
739
- return [{ ...meta, raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill), files: bundle }];
740
- }
741
- const pack = await resolvePackFiles(skill);
742
- if (pack) return [{ ...meta, ...pack }];
743
- }
744
- return [{ ...meta, raw: skillMdFor(skill) }];
927
+ return [await registryCandidate(spec.owner, spec.name, source, { api, base: apiBase, token })];
745
928
  } catch (e) {
746
929
  if (e instanceof IntegrityError) throw e;
747
930
  if (e instanceof RegistryError && e.status !== 404) registryDown = e;
@@ -775,7 +958,7 @@ function makeDefaultDeps(flags) {
775
958
  };
776
959
  }
777
960
  async function resolveScope(flags, deps) {
778
- const home = resolve3(flags.home ?? homedir3());
961
+ const home = resolve3(flags.home ?? homedir4());
779
962
  const cwd = resolve3(flags.cwd ?? process.cwd());
780
963
  const base = { cwd: flags.cwd, home: flags.home, env: flags.env };
781
964
  if (flags.global) return { scope: { global: true, ...base } };
@@ -798,7 +981,7 @@ function defaultNeedsPrompt(flags) {
798
981
  const env = flags.env ?? process.env;
799
982
  if (flags.yes || flags.json || flags.global || flags.project || flags.list) return false;
800
983
  if (detectHostAgent(env)) return false;
801
- if (resolve3(flags.cwd ?? process.cwd()) === resolve3(flags.home ?? homedir3())) return false;
984
+ if (resolve3(flags.cwd ?? process.cwd()) === resolve3(flags.home ?? homedir4())) return false;
802
985
  return !isInteractive(flags, { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env });
803
986
  }
804
987
  var CANCELLED_TEXT = "installation cancelled \u2014 nothing was installed";
@@ -813,7 +996,7 @@ function finish2(flags, outcome) {
813
996
  ...outcome.notices,
814
997
  ...outcome.records.flatMap((r) => r.kind === "installed" ? r.notices : [])
815
998
  ].map((n) => n.text);
816
- const exitCode = blocked.length > 0 || installed.length === 0 ? 1 : 0;
999
+ const exitCode = blocked.length > 0 || installed.length === 0 && !outcome.managed ? 1 : 0;
817
1000
  const doc = { ok: exitCode === 0, scope: outcome.scope, installed, blocked, warnings };
818
1001
  return {
819
1002
  written,
@@ -975,9 +1158,28 @@ async function installOne(c, ctx) {
975
1158
  ...out.replaced ? { replaced: out.replaced } : {}
976
1159
  };
977
1160
  }
1161
+ async function tryPluginInstall(arg, flags) {
1162
+ let spec;
1163
+ try {
1164
+ spec = parseSource(arg, { ref: flags.ref });
1165
+ } catch {
1166
+ return null;
1167
+ }
1168
+ if (spec.kind !== "plugin") return null;
1169
+ if (flags.agent?.length || flags.skill?.length || flags.list || flags.mode === "copy") return null;
1170
+ if (!await claudeCliAvailable()) return null;
1171
+ const res = await installClaudePlugin(spec.owner, spec.slug);
1172
+ if (!res.ok) return null;
1173
+ return {
1174
+ agent: "claude-code",
1175
+ name: res.name,
1176
+ note: `installed ${res.name} as a Claude Code plugin \u2014 manage it with \`claude plugin\` or /plugin`
1177
+ };
1178
+ }
978
1179
  async function runAdd(arg, flags, deps) {
979
1180
  const d = deps ?? makeDefaultDeps(flags);
980
1181
  const env = flags.env ?? process.env;
1182
+ const managed = await tryPluginInstall(arg, flags);
981
1183
  let candidates;
982
1184
  try {
983
1185
  candidates = await resolveCandidates(arg, flags, d);
@@ -994,14 +1196,20 @@ async function runAdd(arg, flags, deps) {
994
1196
  if (!resolved) return finish2(flags, { kind: "cancel" });
995
1197
  const scope = resolved.scope;
996
1198
  const notices = resolved.note ? [infoNotice(resolved.note)] : [];
997
- const chosen = await chooseTargets(flags, d, scope, detectAgents(scope));
1199
+ if (managed) notices.push(infoNotice(managed.note));
1200
+ const detected = detectAgents(scope).filter((id) => id !== managed?.agent);
1201
+ if (managed && !detected.length) {
1202
+ const paths2 = { home: flags.home ?? homedir4(), cwd: flags.cwd ?? process.cwd() };
1203
+ return finish2(flags, { kind: "install", scope: scope.global ? "global" : "project", records: [], notices, paths: paths2, managed: managed.name });
1204
+ }
1205
+ const chosen = await chooseTargets(flags, d, scope, detected);
998
1206
  if (!chosen) return finish2(flags, { kind: "cancel" });
999
1207
  notices.push(...chosen.notices);
1000
1208
  const ctx = { arg, flags, deny: new Set(flags.deny ?? []), scope, targets: chosen.targets, env, deps: d };
1001
1209
  const records = [];
1002
1210
  for (const c of candidates) records.push(await installOne(c, ctx));
1003
- const paths = { home: flags.home ?? homedir3(), cwd: flags.cwd ?? process.cwd() };
1004
- return finish2(flags, { kind: "install", scope: scope.global ? "global" : "project", records, notices, paths });
1211
+ const paths = { home: flags.home ?? homedir4(), cwd: flags.cwd ?? process.cwd() };
1212
+ return finish2(flags, { kind: "install", scope: scope.global ? "global" : "project", records, notices, paths, ...managed ? { managed: managed.name } : {} });
1005
1213
  }
1006
1214
  function addFlags(opts, parent) {
1007
1215
  const merged = { ...parent, ...opts };
@@ -1011,12 +1219,12 @@ function addFlags(opts, parent) {
1011
1219
  function addCommand() {
1012
1220
  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 your user-level agent folders (available in every project)").option("-p, --project", "install into the current directory's agent folders (committed with the project)").option("-a, --agent <agents...>", "target specific agents (default: every agent detected on this machine)").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--ref <ref>", "git ref (branch, tag or commit) for GitHub sources").option("-l, --list", "list the skills a source contains without installing").addOption(new Option("--mode <mode>", "link (junction/symlink into each agent dir, default) or copy").choices(["link", "copy"]).default("link")).option("--force", "replace a skill installed from a different source, or an untracked directory").addOption(new Option("--copy", "alias of --mode copy").hideHelp()).option("-y, --yes", "skip prompts; scope is auto-detected (project if this folder looks like a project, else global)").addOption(new Option("--allow-unverified").hideHelp()).option("--skip-lint", "install even if the skill fails lint").option("--deny <flags...>", "refuse skills carrying any of these security flags (opt-in)").action(async (source, opts, cmd) => {
1013
1221
  const merged = addFlags(opts, cmd.parent?.opts());
1014
- const { run, printed } = await runAddWithUI(source, merged);
1222
+ const { run: run2, printed } = await runAddWithUI(source, merged);
1015
1223
  if (!printed) {
1016
- if (run.exitCode === 1 && !merged.json) console.error(run.output);
1017
- else console.log(run.output);
1224
+ if (run2.exitCode === 1 && !merged.json) console.error(run2.output);
1225
+ else console.log(run2.output);
1018
1226
  }
1019
- process.exitCode = run.exitCode;
1227
+ process.exitCode = run2.exitCode;
1020
1228
  });
1021
1229
  }
1022
1230
  var clackUI = {
@@ -1033,7 +1241,7 @@ async function runAddWithUI(source, flags, opts = {}) {
1033
1241
  const ui = opts.ui ?? clackUI;
1034
1242
  ui.intro(banner());
1035
1243
  try {
1036
- const run = await runAdd(source, flags, {
1244
+ const run2 = await runAdd(source, flags, {
1037
1245
  // NOTE: spreading `base` reads makeDefaultDeps()'s promptScope/promptAgents
1038
1246
  // getters right here, freezing their values for this run. That is exactly
1039
1247
  // what we want (interactivity is decided once, up front), but it does mean
@@ -1053,13 +1261,13 @@ async function runAddWithUI(source, flags, opts = {}) {
1053
1261
  }
1054
1262
  }
1055
1263
  });
1056
- if (run.cancelled) {
1264
+ if (run2.cancelled) {
1057
1265
  ui.cancel("Installation cancelled \u2014 nothing was installed.");
1058
- return { run, printed: true };
1266
+ return { run: run2, printed: true };
1059
1267
  }
1060
- ui.message(run.output);
1061
- ui.outro(run.exitCode === 0 ? `Done. Run ${pc8.cyan("skillmd list")} to see everything installed.` : pc8.red("Some skills were not installed."));
1062
- return { run, printed: true };
1268
+ ui.message(run2.output);
1269
+ ui.outro(run2.exitCode === 0 ? `Done. Run ${pc8.cyan("skillmd list")} to see everything installed.` : pc8.red("Some skills were not installed."));
1270
+ return { run: run2, printed: true };
1063
1271
  } catch (e) {
1064
1272
  ui.outro(pc8.red("Nothing was installed."));
1065
1273
  throw e;
@@ -1068,8 +1276,8 @@ async function runAddWithUI(source, flags, opts = {}) {
1068
1276
 
1069
1277
  // src/commands/list.ts
1070
1278
  import { Command as Command8 } from "commander";
1071
- import { existsSync as existsSync2, readFileSync } from "fs";
1072
- import { join as join3 } from "path";
1279
+ import { existsSync as existsSync3, readFileSync } from "fs";
1280
+ import { join as join4 } from "path";
1073
1281
  import pc9 from "picocolors";
1074
1282
  import { parseSkillMd as parseSkillMd3 } from "@skillmds/core";
1075
1283
  function runList(flags = {}) {
@@ -1077,9 +1285,9 @@ function runList(flags = {}) {
1077
1285
  let items = [];
1078
1286
  for (const global of scopes) {
1079
1287
  for (const s of listInstalled({ global, cwd: flags.cwd, home: flags.home })) {
1080
- const md = join3(s.path, "SKILL.md");
1288
+ const md = join4(s.path, "SKILL.md");
1081
1289
  let description = "";
1082
- if (existsSync2(md)) {
1290
+ if (existsSync3(md)) {
1083
1291
  const parsed = parseSkillMd3(readFileSync(md, "utf8"));
1084
1292
  description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
1085
1293
  }
@@ -1110,12 +1318,12 @@ function listCommand() {
1110
1318
  // src/commands/remove.ts
1111
1319
  import { Command as Command9 } from "commander";
1112
1320
  import { rmSync } from "fs";
1113
- import { join as join4, resolve as resolve4 } from "path";
1321
+ import { join as join5, resolve as resolve4 } from "path";
1114
1322
  import * as p4 from "@clack/prompts";
1115
1323
  import pc10 from "picocolors";
1116
1324
  var defaultDeps = (flags) => isInteractive(flags) ? {
1117
- confirm: async (msg) => {
1118
- const a = await p4.confirm({ message: msg, initialValue: false });
1325
+ confirm: async (msg2) => {
1326
+ const a = await p4.confirm({ message: msg2, initialValue: false });
1119
1327
  return !p4.isCancel(a) && a;
1120
1328
  },
1121
1329
  pick: async ({ names }) => {
@@ -1198,7 +1406,7 @@ async function runRemove(names, flags, deps = defaultDeps(flags)) {
1198
1406
  acted = true;
1199
1407
  continue;
1200
1408
  }
1201
- const pth = join4(dir, name);
1409
+ const pth = join5(dir, name);
1202
1410
  if (isSkillDirOrLink(pth)) {
1203
1411
  rmSync(pth, { recursive: true, force: true });
1204
1412
  removed.push(pth);
@@ -1232,9 +1440,9 @@ async function runRemove(names, flags, deps = defaultDeps(flags)) {
1232
1440
  }
1233
1441
  function removeCommand() {
1234
1442
  return new Command9("remove").alias("rm").description("Remove installed skills (links, canonical copy and lock entry)").argument("[names...]", "skill names (omit to pick interactively)").option("-g, --global", "only user-level skills").option("-p, --project", "only this project's skills").option("-a, --agent <agents...>", "remove only from these agents").option("--all", "remove every skill in the scope").option("-y, --yes", "skip the confirmation").option("--json", "machine-readable output").action(async (names, opts, cmd) => {
1235
- const run = await runRemove(names, { ...cmd.parent?.opts(), ...opts });
1236
- console.log(run.output);
1237
- process.exitCode = run.exitCode;
1443
+ const run2 = await runRemove(names, { ...cmd.parent?.opts(), ...opts });
1444
+ console.log(run2.output);
1445
+ process.exitCode = run2.exitCode;
1238
1446
  });
1239
1447
  }
1240
1448
 
@@ -1368,16 +1576,16 @@ function scopeOptions(cmd) {
1368
1576
  }
1369
1577
  function updateCommand() {
1370
1578
  return scopeOptions(new Command10("update").description("Update installed skills from the source recorded in the lock file (re-lints before writing)").argument("[names...]", "specific skills (default: all tracked)").option("--check", "only report which skills have updates")).action(async (names, opts, cmd) => {
1371
- const run = await runUpdate(names, { ...cmd.parent?.opts(), ...opts });
1372
- console.log(run.output);
1373
- process.exitCode = run.exitCode;
1579
+ const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts });
1580
+ console.log(run2.output);
1581
+ process.exitCode = run2.exitCode;
1374
1582
  });
1375
1583
  }
1376
1584
  function checkCommand() {
1377
1585
  return scopeOptions(new Command10("check").description("Report installed skills that have updates (alias of `update --check`)").argument("[names...]", "specific skills (default: all tracked)")).action(async (names, opts, cmd) => {
1378
- const run = await runUpdate(names, { ...cmd.parent?.opts(), ...opts, check: true });
1379
- console.log(run.output);
1380
- process.exitCode = run.exitCode;
1586
+ const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts, check: true });
1587
+ console.log(run2.output);
1588
+ process.exitCode = run2.exitCode;
1381
1589
  });
1382
1590
  }
1383
1591
 
@@ -1425,9 +1633,9 @@ ${pc12.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ??
1425
1633
  function publishCommand() {
1426
1634
  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) => {
1427
1635
  const merged = { ...cmd.parent?.opts(), ...opts };
1428
- const run = await runPublish(target, merged);
1429
- console.log(run.output);
1430
- process.exitCode = run.exitCode;
1636
+ const run2 = await runPublish(target, merged);
1637
+ console.log(run2.output);
1638
+ process.exitCode = run2.exitCode;
1431
1639
  });
1432
1640
  }
1433
1641
 
@@ -1463,8 +1671,8 @@ async function runLogin(flags, deps = defaultLoginDeps(flags)) {
1463
1671
  try {
1464
1672
  host = hostOf(resolveApi({ api: flags.api, insecureHttp: flags.insecureHttp }, { env, config: { ...cfg, api: flags.api ?? cfg.api } }));
1465
1673
  } catch (e) {
1466
- const msg = e instanceof Error ? e.message : String(e);
1467
- return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg }, null, 2) : pc13.red(msg) };
1674
+ const msg2 = e instanceof Error ? e.message : String(e);
1675
+ return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg2 }, null, 2) : pc13.red(msg2) };
1468
1676
  }
1469
1677
  cfg.token = token;
1470
1678
  if (flags.api) cfg.api = flags.api;
@@ -1566,9 +1774,9 @@ async function browseOneResult(item) {
1566
1774
  }
1567
1775
  p6.log.step(`Installing ${slugTxt}\u2026`);
1568
1776
  try {
1569
- const run = await runAdd(item.slug, action === "global" ? { global: true } : { project: true });
1570
- console.log(run.output);
1571
- if (run.exitCode === 0) p6.log.success("Installed.");
1777
+ const run2 = await runAdd(item.slug, action === "global" ? { global: true } : { project: true });
1778
+ console.log(run2.output);
1779
+ if (run2.exitCode === 0) p6.log.success("Installed.");
1572
1780
  else p6.log.warn("Not installed.");
1573
1781
  } catch (err) {
1574
1782
  p6.log.error(err instanceof Error ? err.message : String(err));
@@ -1603,10 +1811,10 @@ async function runInteractive() {
1603
1811
  const s = p6.spinner();
1604
1812
  s.start(action === "lint" ? "Linting" : "Scanning");
1605
1813
  try {
1606
- const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
1814
+ const run2 = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
1607
1815
  s.stop("Done");
1608
- console.log(run.output);
1609
- process.exitCode = run.exitCode;
1816
+ console.log(run2.output);
1817
+ process.exitCode = run2.exitCode;
1610
1818
  } catch (err) {
1611
1819
  s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
1612
1820
  process.exitCode = 1;
@@ -1621,13 +1829,13 @@ async function runInteractive() {
1621
1829
  const s = p6.spinner();
1622
1830
  s.start("Searching");
1623
1831
  try {
1624
- const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
1625
- s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
1626
- if (run.items.length === 0) {
1832
+ const run2 = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
1833
+ s.stop(`${run2.items.length} result${run2.items.length === 1 ? "" : "s"}`);
1834
+ if (run2.items.length === 0) {
1627
1835
  p6.log.info("No results.");
1628
1836
  break;
1629
1837
  }
1630
- await browseSearchResults(run.items);
1838
+ await browseSearchResults(run2.items);
1631
1839
  } catch (err) {
1632
1840
  s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
1633
1841
  process.exitCode = 1;
@@ -1652,9 +1860,9 @@ async function runInteractive() {
1652
1860
  const src = await p6.text({ message: "Skill source", placeholder: "owner/name, https://github.com/\u2026, or ./path" });
1653
1861
  if (bail(src)) break;
1654
1862
  try {
1655
- const run = await runAdd(String(src), {});
1656
- console.log(run.output);
1657
- process.exitCode = run.exitCode;
1863
+ const run2 = await runAdd(String(src), {});
1864
+ console.log(run2.output);
1865
+ process.exitCode = run2.exitCode;
1658
1866
  } catch (err) {
1659
1867
  p6.log.error(err instanceof Error ? err.message : String(err));
1660
1868
  process.exitCode = 1;
@@ -1667,9 +1875,9 @@ async function runInteractive() {
1667
1875
  }
1668
1876
  case "update": {
1669
1877
  try {
1670
- const run = await runUpdate([], {});
1671
- console.log(run.output);
1672
- process.exitCode = run.exitCode;
1878
+ const run2 = await runUpdate([], {});
1879
+ console.log(run2.output);
1880
+ process.exitCode = run2.exitCode;
1673
1881
  } catch (err) {
1674
1882
  p6.log.error(err instanceof Error ? err.message : String(err));
1675
1883
  process.exitCode = 1;
@@ -1678,9 +1886,9 @@ async function runInteractive() {
1678
1886
  }
1679
1887
  case "remove": {
1680
1888
  try {
1681
- const run = await runRemove([], {});
1682
- console.log(run.output);
1683
- process.exitCode = run.exitCode;
1889
+ const run2 = await runRemove([], {});
1890
+ console.log(run2.output);
1891
+ process.exitCode = run2.exitCode;
1684
1892
  } catch (err) {
1685
1893
  p6.log.error(err instanceof Error ? err.message : String(err));
1686
1894
  process.exitCode = 1;
package/dist/mcp.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  safeText,
13
13
  stripTerminalEscapes,
14
14
  telemetryDisabled
15
- } from "./chunk-A7BKAUUO.js";
15
+ } from "./chunk-FVWCI37S.js";
16
16
 
17
17
  // src/mcp/server.ts
18
18
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillmds",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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
  "license": "MIT",
6
6
  "author": "SkillMD",
@@ -59,6 +59,7 @@
59
59
  "@modelcontextprotocol/sdk": "^1.13.0",
60
60
  "@skillmds/core": "^1.0.0",
61
61
  "commander": "^12.1.0",
62
+ "fflate": "^0.8.3",
62
63
  "giget": "^3.3.0",
63
64
  "picocolors": "^1.1.0"
64
65
  },