skillmds 1.2.1 → 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.
- package/dist/{chunk-4BFQJ3YZ.js → chunk-FVWCI37S.js} +13 -0
- package/dist/cli.js +303 -105
- package/dist/mcp.js +1 -1
- package/package.json +2 -1
|
@@ -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":
|
|
@@ -314,6 +324,9 @@ async function resolveSource(arg, opts = {}) {
|
|
|
314
324
|
if (raw.length > MAX_PACK_BYTES) throw tooBig(raw.length);
|
|
315
325
|
return [{ path: spec.display, raw, slug: `gist-${spec.id.slice(0, 8)}` }];
|
|
316
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
|
+
}
|
|
317
330
|
const gh = asGithub(spec);
|
|
318
331
|
const { dir, cleanup } = await downloadGithub(gh, "skillmd-src-", opts, Boolean(gh.subpath));
|
|
319
332
|
try {
|
package/dist/cli.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
uninstallSkill,
|
|
31
31
|
upsertEntry,
|
|
32
32
|
writeConfig
|
|
33
|
-
} from "./chunk-
|
|
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
|
|
323
|
-
s.stop(`Scanned ${
|
|
324
|
-
console.log(
|
|
325
|
-
const failed =
|
|
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 =
|
|
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
|
|
338
|
-
console.log(
|
|
339
|
-
process.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
|
|
379
|
-
console.log(
|
|
380
|
-
process.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
|
|
508
|
-
return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error:
|
|
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
|
|
567
|
-
console.log(
|
|
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
|
|
617
|
-
if (
|
|
618
|
-
process.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
|
|
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" });
|
|
@@ -693,6 +773,77 @@ function provenanceFor(arg) {
|
|
|
693
773
|
return `unknown:${arg}`;
|
|
694
774
|
}
|
|
695
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
|
+
}
|
|
696
847
|
function makeDefaultDeps(flags) {
|
|
697
848
|
const canPrompt = () => isInteractive(flags, {
|
|
698
849
|
stdinTTY: Boolean(process.stdin.isTTY),
|
|
@@ -703,6 +854,55 @@ function makeDefaultDeps(flags) {
|
|
|
703
854
|
async resolve(arg, f) {
|
|
704
855
|
const spec = parseSource(arg, { ref: f.ref });
|
|
705
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
|
+
}
|
|
706
906
|
if (spec.kind !== "slug") {
|
|
707
907
|
const rs = await resolveSource(argWithRef(arg, f.ref));
|
|
708
908
|
return rs.map((r) => ({
|
|
@@ -724,34 +924,7 @@ function makeDefaultDeps(flags) {
|
|
|
724
924
|
})();
|
|
725
925
|
const registrySlug = `${spec.owner}/${spec.name}`;
|
|
726
926
|
try {
|
|
727
|
-
|
|
728
|
-
const skillName = skill.slug.split("/")[1] || spec.name;
|
|
729
|
-
const meta = {
|
|
730
|
-
name: skillName,
|
|
731
|
-
slug: skillName,
|
|
732
|
-
registrySlug,
|
|
733
|
-
source,
|
|
734
|
-
commit_sha: skill.commit_sha ?? void 0,
|
|
735
|
-
verified: skill.verified,
|
|
736
|
-
type: skill.type,
|
|
737
|
-
securityFlags: skill.security_flags
|
|
738
|
-
};
|
|
739
|
-
if (skill.type === "pack") {
|
|
740
|
-
const bundle = await fetchBundle(apiBase, registrySlug, token);
|
|
741
|
-
if (bundle && bundle.length) {
|
|
742
|
-
const sk = bundle.find((file) => file.path === "SKILL.md");
|
|
743
|
-
const unverified = bundle.filter((file) => file.unverified).length;
|
|
744
|
-
return [{
|
|
745
|
-
...meta,
|
|
746
|
-
raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill),
|
|
747
|
-
files: bundle,
|
|
748
|
-
...unverified ? { note: unverifiedFilesNote(unverified) } : {}
|
|
749
|
-
}];
|
|
750
|
-
}
|
|
751
|
-
const pack = await resolvePackFiles(skill);
|
|
752
|
-
if (pack) return [{ ...meta, ...pack }];
|
|
753
|
-
}
|
|
754
|
-
return [{ ...meta, raw: skillMdFor(skill) }];
|
|
927
|
+
return [await registryCandidate(spec.owner, spec.name, source, { api, base: apiBase, token })];
|
|
755
928
|
} catch (e) {
|
|
756
929
|
if (e instanceof IntegrityError) throw e;
|
|
757
930
|
if (e instanceof RegistryError && e.status !== 404) registryDown = e;
|
|
@@ -785,7 +958,7 @@ function makeDefaultDeps(flags) {
|
|
|
785
958
|
};
|
|
786
959
|
}
|
|
787
960
|
async function resolveScope(flags, deps) {
|
|
788
|
-
const home = resolve3(flags.home ??
|
|
961
|
+
const home = resolve3(flags.home ?? homedir4());
|
|
789
962
|
const cwd = resolve3(flags.cwd ?? process.cwd());
|
|
790
963
|
const base = { cwd: flags.cwd, home: flags.home, env: flags.env };
|
|
791
964
|
if (flags.global) return { scope: { global: true, ...base } };
|
|
@@ -808,7 +981,7 @@ function defaultNeedsPrompt(flags) {
|
|
|
808
981
|
const env = flags.env ?? process.env;
|
|
809
982
|
if (flags.yes || flags.json || flags.global || flags.project || flags.list) return false;
|
|
810
983
|
if (detectHostAgent(env)) return false;
|
|
811
|
-
if (resolve3(flags.cwd ?? process.cwd()) === resolve3(flags.home ??
|
|
984
|
+
if (resolve3(flags.cwd ?? process.cwd()) === resolve3(flags.home ?? homedir4())) return false;
|
|
812
985
|
return !isInteractive(flags, { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env });
|
|
813
986
|
}
|
|
814
987
|
var CANCELLED_TEXT = "installation cancelled \u2014 nothing was installed";
|
|
@@ -823,7 +996,7 @@ function finish2(flags, outcome) {
|
|
|
823
996
|
...outcome.notices,
|
|
824
997
|
...outcome.records.flatMap((r) => r.kind === "installed" ? r.notices : [])
|
|
825
998
|
].map((n) => n.text);
|
|
826
|
-
const exitCode = blocked.length > 0 || installed.length === 0 ? 1 : 0;
|
|
999
|
+
const exitCode = blocked.length > 0 || installed.length === 0 && !outcome.managed ? 1 : 0;
|
|
827
1000
|
const doc = { ok: exitCode === 0, scope: outcome.scope, installed, blocked, warnings };
|
|
828
1001
|
return {
|
|
829
1002
|
written,
|
|
@@ -985,9 +1158,28 @@ async function installOne(c, ctx) {
|
|
|
985
1158
|
...out.replaced ? { replaced: out.replaced } : {}
|
|
986
1159
|
};
|
|
987
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
|
+
}
|
|
988
1179
|
async function runAdd(arg, flags, deps) {
|
|
989
1180
|
const d = deps ?? makeDefaultDeps(flags);
|
|
990
1181
|
const env = flags.env ?? process.env;
|
|
1182
|
+
const managed = await tryPluginInstall(arg, flags);
|
|
991
1183
|
let candidates;
|
|
992
1184
|
try {
|
|
993
1185
|
candidates = await resolveCandidates(arg, flags, d);
|
|
@@ -1004,14 +1196,20 @@ async function runAdd(arg, flags, deps) {
|
|
|
1004
1196
|
if (!resolved) return finish2(flags, { kind: "cancel" });
|
|
1005
1197
|
const scope = resolved.scope;
|
|
1006
1198
|
const notices = resolved.note ? [infoNotice(resolved.note)] : [];
|
|
1007
|
-
|
|
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);
|
|
1008
1206
|
if (!chosen) return finish2(flags, { kind: "cancel" });
|
|
1009
1207
|
notices.push(...chosen.notices);
|
|
1010
1208
|
const ctx = { arg, flags, deny: new Set(flags.deny ?? []), scope, targets: chosen.targets, env, deps: d };
|
|
1011
1209
|
const records = [];
|
|
1012
1210
|
for (const c of candidates) records.push(await installOne(c, ctx));
|
|
1013
|
-
const paths = { home: flags.home ??
|
|
1014
|
-
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 } : {} });
|
|
1015
1213
|
}
|
|
1016
1214
|
function addFlags(opts, parent) {
|
|
1017
1215
|
const merged = { ...parent, ...opts };
|
|
@@ -1021,12 +1219,12 @@ function addFlags(opts, parent) {
|
|
|
1021
1219
|
function addCommand() {
|
|
1022
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) => {
|
|
1023
1221
|
const merged = addFlags(opts, cmd.parent?.opts());
|
|
1024
|
-
const { run, printed } = await runAddWithUI(source, merged);
|
|
1222
|
+
const { run: run2, printed } = await runAddWithUI(source, merged);
|
|
1025
1223
|
if (!printed) {
|
|
1026
|
-
if (
|
|
1027
|
-
else console.log(
|
|
1224
|
+
if (run2.exitCode === 1 && !merged.json) console.error(run2.output);
|
|
1225
|
+
else console.log(run2.output);
|
|
1028
1226
|
}
|
|
1029
|
-
process.exitCode =
|
|
1227
|
+
process.exitCode = run2.exitCode;
|
|
1030
1228
|
});
|
|
1031
1229
|
}
|
|
1032
1230
|
var clackUI = {
|
|
@@ -1043,7 +1241,7 @@ async function runAddWithUI(source, flags, opts = {}) {
|
|
|
1043
1241
|
const ui = opts.ui ?? clackUI;
|
|
1044
1242
|
ui.intro(banner());
|
|
1045
1243
|
try {
|
|
1046
|
-
const
|
|
1244
|
+
const run2 = await runAdd(source, flags, {
|
|
1047
1245
|
// NOTE: spreading `base` reads makeDefaultDeps()'s promptScope/promptAgents
|
|
1048
1246
|
// getters right here, freezing their values for this run. That is exactly
|
|
1049
1247
|
// what we want (interactivity is decided once, up front), but it does mean
|
|
@@ -1063,13 +1261,13 @@ async function runAddWithUI(source, flags, opts = {}) {
|
|
|
1063
1261
|
}
|
|
1064
1262
|
}
|
|
1065
1263
|
});
|
|
1066
|
-
if (
|
|
1264
|
+
if (run2.cancelled) {
|
|
1067
1265
|
ui.cancel("Installation cancelled \u2014 nothing was installed.");
|
|
1068
|
-
return { run, printed: true };
|
|
1266
|
+
return { run: run2, printed: true };
|
|
1069
1267
|
}
|
|
1070
|
-
ui.message(
|
|
1071
|
-
ui.outro(
|
|
1072
|
-
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 };
|
|
1073
1271
|
} catch (e) {
|
|
1074
1272
|
ui.outro(pc8.red("Nothing was installed."));
|
|
1075
1273
|
throw e;
|
|
@@ -1078,8 +1276,8 @@ async function runAddWithUI(source, flags, opts = {}) {
|
|
|
1078
1276
|
|
|
1079
1277
|
// src/commands/list.ts
|
|
1080
1278
|
import { Command as Command8 } from "commander";
|
|
1081
|
-
import { existsSync as
|
|
1082
|
-
import { join as
|
|
1279
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
1280
|
+
import { join as join4 } from "path";
|
|
1083
1281
|
import pc9 from "picocolors";
|
|
1084
1282
|
import { parseSkillMd as parseSkillMd3 } from "@skillmds/core";
|
|
1085
1283
|
function runList(flags = {}) {
|
|
@@ -1087,9 +1285,9 @@ function runList(flags = {}) {
|
|
|
1087
1285
|
let items = [];
|
|
1088
1286
|
for (const global of scopes) {
|
|
1089
1287
|
for (const s of listInstalled({ global, cwd: flags.cwd, home: flags.home })) {
|
|
1090
|
-
const md =
|
|
1288
|
+
const md = join4(s.path, "SKILL.md");
|
|
1091
1289
|
let description = "";
|
|
1092
|
-
if (
|
|
1290
|
+
if (existsSync3(md)) {
|
|
1093
1291
|
const parsed = parseSkillMd3(readFileSync(md, "utf8"));
|
|
1094
1292
|
description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
|
|
1095
1293
|
}
|
|
@@ -1120,12 +1318,12 @@ function listCommand() {
|
|
|
1120
1318
|
// src/commands/remove.ts
|
|
1121
1319
|
import { Command as Command9 } from "commander";
|
|
1122
1320
|
import { rmSync } from "fs";
|
|
1123
|
-
import { join as
|
|
1321
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
1124
1322
|
import * as p4 from "@clack/prompts";
|
|
1125
1323
|
import pc10 from "picocolors";
|
|
1126
1324
|
var defaultDeps = (flags) => isInteractive(flags) ? {
|
|
1127
|
-
confirm: async (
|
|
1128
|
-
const a = await p4.confirm({ message:
|
|
1325
|
+
confirm: async (msg2) => {
|
|
1326
|
+
const a = await p4.confirm({ message: msg2, initialValue: false });
|
|
1129
1327
|
return !p4.isCancel(a) && a;
|
|
1130
1328
|
},
|
|
1131
1329
|
pick: async ({ names }) => {
|
|
@@ -1208,7 +1406,7 @@ async function runRemove(names, flags, deps = defaultDeps(flags)) {
|
|
|
1208
1406
|
acted = true;
|
|
1209
1407
|
continue;
|
|
1210
1408
|
}
|
|
1211
|
-
const pth =
|
|
1409
|
+
const pth = join5(dir, name);
|
|
1212
1410
|
if (isSkillDirOrLink(pth)) {
|
|
1213
1411
|
rmSync(pth, { recursive: true, force: true });
|
|
1214
1412
|
removed.push(pth);
|
|
@@ -1242,9 +1440,9 @@ async function runRemove(names, flags, deps = defaultDeps(flags)) {
|
|
|
1242
1440
|
}
|
|
1243
1441
|
function removeCommand() {
|
|
1244
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) => {
|
|
1245
|
-
const
|
|
1246
|
-
console.log(
|
|
1247
|
-
process.exitCode =
|
|
1443
|
+
const run2 = await runRemove(names, { ...cmd.parent?.opts(), ...opts });
|
|
1444
|
+
console.log(run2.output);
|
|
1445
|
+
process.exitCode = run2.exitCode;
|
|
1248
1446
|
});
|
|
1249
1447
|
}
|
|
1250
1448
|
|
|
@@ -1378,16 +1576,16 @@ function scopeOptions(cmd) {
|
|
|
1378
1576
|
}
|
|
1379
1577
|
function updateCommand() {
|
|
1380
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) => {
|
|
1381
|
-
const
|
|
1382
|
-
console.log(
|
|
1383
|
-
process.exitCode =
|
|
1579
|
+
const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts });
|
|
1580
|
+
console.log(run2.output);
|
|
1581
|
+
process.exitCode = run2.exitCode;
|
|
1384
1582
|
});
|
|
1385
1583
|
}
|
|
1386
1584
|
function checkCommand() {
|
|
1387
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) => {
|
|
1388
|
-
const
|
|
1389
|
-
console.log(
|
|
1390
|
-
process.exitCode =
|
|
1586
|
+
const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts, check: true });
|
|
1587
|
+
console.log(run2.output);
|
|
1588
|
+
process.exitCode = run2.exitCode;
|
|
1391
1589
|
});
|
|
1392
1590
|
}
|
|
1393
1591
|
|
|
@@ -1435,9 +1633,9 @@ ${pc12.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ??
|
|
|
1435
1633
|
function publishCommand() {
|
|
1436
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) => {
|
|
1437
1635
|
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
1438
|
-
const
|
|
1439
|
-
console.log(
|
|
1440
|
-
process.exitCode =
|
|
1636
|
+
const run2 = await runPublish(target, merged);
|
|
1637
|
+
console.log(run2.output);
|
|
1638
|
+
process.exitCode = run2.exitCode;
|
|
1441
1639
|
});
|
|
1442
1640
|
}
|
|
1443
1641
|
|
|
@@ -1473,8 +1671,8 @@ async function runLogin(flags, deps = defaultLoginDeps(flags)) {
|
|
|
1473
1671
|
try {
|
|
1474
1672
|
host = hostOf(resolveApi({ api: flags.api, insecureHttp: flags.insecureHttp }, { env, config: { ...cfg, api: flags.api ?? cfg.api } }));
|
|
1475
1673
|
} catch (e) {
|
|
1476
|
-
const
|
|
1477
|
-
return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error:
|
|
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) };
|
|
1478
1676
|
}
|
|
1479
1677
|
cfg.token = token;
|
|
1480
1678
|
if (flags.api) cfg.api = flags.api;
|
|
@@ -1576,9 +1774,9 @@ async function browseOneResult(item) {
|
|
|
1576
1774
|
}
|
|
1577
1775
|
p6.log.step(`Installing ${slugTxt}\u2026`);
|
|
1578
1776
|
try {
|
|
1579
|
-
const
|
|
1580
|
-
console.log(
|
|
1581
|
-
if (
|
|
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.");
|
|
1582
1780
|
else p6.log.warn("Not installed.");
|
|
1583
1781
|
} catch (err) {
|
|
1584
1782
|
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
@@ -1613,10 +1811,10 @@ async function runInteractive() {
|
|
|
1613
1811
|
const s = p6.spinner();
|
|
1614
1812
|
s.start(action === "lint" ? "Linting" : "Scanning");
|
|
1615
1813
|
try {
|
|
1616
|
-
const
|
|
1814
|
+
const run2 = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
|
|
1617
1815
|
s.stop("Done");
|
|
1618
|
-
console.log(
|
|
1619
|
-
process.exitCode =
|
|
1816
|
+
console.log(run2.output);
|
|
1817
|
+
process.exitCode = run2.exitCode;
|
|
1620
1818
|
} catch (err) {
|
|
1621
1819
|
s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
|
|
1622
1820
|
process.exitCode = 1;
|
|
@@ -1631,13 +1829,13 @@ async function runInteractive() {
|
|
|
1631
1829
|
const s = p6.spinner();
|
|
1632
1830
|
s.start("Searching");
|
|
1633
1831
|
try {
|
|
1634
|
-
const
|
|
1635
|
-
s.stop(`${
|
|
1636
|
-
if (
|
|
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) {
|
|
1637
1835
|
p6.log.info("No results.");
|
|
1638
1836
|
break;
|
|
1639
1837
|
}
|
|
1640
|
-
await browseSearchResults(
|
|
1838
|
+
await browseSearchResults(run2.items);
|
|
1641
1839
|
} catch (err) {
|
|
1642
1840
|
s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
|
|
1643
1841
|
process.exitCode = 1;
|
|
@@ -1662,9 +1860,9 @@ async function runInteractive() {
|
|
|
1662
1860
|
const src = await p6.text({ message: "Skill source", placeholder: "owner/name, https://github.com/\u2026, or ./path" });
|
|
1663
1861
|
if (bail(src)) break;
|
|
1664
1862
|
try {
|
|
1665
|
-
const
|
|
1666
|
-
console.log(
|
|
1667
|
-
process.exitCode =
|
|
1863
|
+
const run2 = await runAdd(String(src), {});
|
|
1864
|
+
console.log(run2.output);
|
|
1865
|
+
process.exitCode = run2.exitCode;
|
|
1668
1866
|
} catch (err) {
|
|
1669
1867
|
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1670
1868
|
process.exitCode = 1;
|
|
@@ -1677,9 +1875,9 @@ async function runInteractive() {
|
|
|
1677
1875
|
}
|
|
1678
1876
|
case "update": {
|
|
1679
1877
|
try {
|
|
1680
|
-
const
|
|
1681
|
-
console.log(
|
|
1682
|
-
process.exitCode =
|
|
1878
|
+
const run2 = await runUpdate([], {});
|
|
1879
|
+
console.log(run2.output);
|
|
1880
|
+
process.exitCode = run2.exitCode;
|
|
1683
1881
|
} catch (err) {
|
|
1684
1882
|
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1685
1883
|
process.exitCode = 1;
|
|
@@ -1688,9 +1886,9 @@ async function runInteractive() {
|
|
|
1688
1886
|
}
|
|
1689
1887
|
case "remove": {
|
|
1690
1888
|
try {
|
|
1691
|
-
const
|
|
1692
|
-
console.log(
|
|
1693
|
-
process.exitCode =
|
|
1889
|
+
const run2 = await runRemove([], {});
|
|
1890
|
+
console.log(run2.output);
|
|
1891
|
+
process.exitCode = run2.exitCode;
|
|
1694
1892
|
} catch (err) {
|
|
1695
1893
|
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1696
1894
|
process.exitCode = 1;
|
package/dist/mcp.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillmds",
|
|
3
|
-
"version": "1.
|
|
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
|
},
|