skillmds 1.0.15 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/bin/skillmd-mcp.mjs +43 -17
- package/dist/cli.js +37 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.0.16
|
|
6
|
+
|
|
7
|
+
- **Installs are no longer gated on verification.** Any skill installs directly — verified or not — with no flag, no prompt, and no second step, from both the CLI (`skillmd add`) and the MCP `skillmd_install` tool. Security flags (network_calls, executes_scripts, reads_secrets, docs_only) are still shown on the result as information, but never block. `--deny <flag>` remains available as an opt-in for anyone who wants to exclude a flag; `--allow-unverified` is now a no-op kept for backward compatibility.
|
|
8
|
+
|
|
5
9
|
## 1.0.15
|
|
6
10
|
|
|
7
11
|
- Search is now **browsable in the interactive menu**: arrow through results, press Enter on one to see a card, and install it to your project or user directory — all without leaving the CLI (`← Back` at each level; Esc goes up one step, never quits).
|
package/bin/skillmd-mcp.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
// SkillMD MCP server (stdio). Wraps the public SkillMD API so any MCP-capable agent
|
|
3
3
|
// can search, fetch, install, and discover Agent Skills by name or task.
|
|
4
4
|
//
|
|
@@ -14,7 +14,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
14
14
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
|
-
import { join, resolve } from "node:path";
|
|
17
|
+
import { join, resolve, dirname, sep } from "node:path";
|
|
18
18
|
import { stringify as stringifyYaml } from "yaml";
|
|
19
19
|
|
|
20
20
|
const VERSION = (() => {
|
|
@@ -70,13 +70,12 @@ const TOOLS = [
|
|
|
70
70
|
{
|
|
71
71
|
name: "skillmd_install",
|
|
72
72
|
description:
|
|
73
|
-
"Install a skill by slug:
|
|
73
|
+
"Install a skill by slug: writes its SKILL.md into the agent's skills directory. Never executes any scripts. Any skill installs; the response reports its security flags (docs_only / network_calls / executes_scripts / reads_secrets) as information.",
|
|
74
74
|
inputSchema: {
|
|
75
75
|
type: "object",
|
|
76
76
|
properties: {
|
|
77
77
|
slug: { type: "string", description: "owner/name, e.g. anthropic/pdf" },
|
|
78
78
|
dest: { type: "string", description: "Target skills dir. Default ~/.claude/skills" },
|
|
79
|
-
allow_unverified: { type: "boolean", description: "Install even if the skill is not verified by SkillMD. Never set this on your own initiative — only after the human has explicitly approved this specific skill." },
|
|
80
79
|
},
|
|
81
80
|
required: ["slug"],
|
|
82
81
|
},
|
|
@@ -193,13 +192,8 @@ async function handleCall(name, args) {
|
|
|
193
192
|
}
|
|
194
193
|
const skill = await api(`/api/skills/${owner}/${slugName}`);
|
|
195
194
|
const flags = skill.security_flags ?? [];
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
`Refused: ${args.slug} is not yet verified by SkillMD (security flags: ${flags.join(", ") || "unscanned"}). ` +
|
|
199
|
-
`Review it at https://skillmd.com/skills/${args.slug}. Only call skillmd_install again with allow_unverified: true ` +
|
|
200
|
-
`if the human explicitly approves installing this specific unverified skill — do not decide this yourself.`,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
195
|
+
// Verification never gates installs — any skill installs; flags are
|
|
196
|
+
// reported in the response as information, not a wall.
|
|
203
197
|
const raw = reconstructSkillMd(skill); // returns raw_md verbatim when the registry has it
|
|
204
198
|
if (lint) {
|
|
205
199
|
const result = lint(raw, { slug: slugName });
|
|
@@ -211,13 +205,45 @@ async function handleCall(name, args) {
|
|
|
211
205
|
const baseDir = args.dest ? resolve(args.dest) : join(homedir(), ".claude", "skills");
|
|
212
206
|
const dir = join(baseDir, slugName);
|
|
213
207
|
await mkdir(dir, { recursive: true });
|
|
214
|
-
const
|
|
215
|
-
|
|
208
|
+
const root = resolve(dir);
|
|
209
|
+
|
|
210
|
+
// Zip-slip guard: refuse any path that resolves outside the skill's dir.
|
|
211
|
+
const writeSafe = async (relPath, contents) => {
|
|
212
|
+
const fp = resolve(dir, relPath);
|
|
213
|
+
if (fp !== root && !fp.startsWith(root + sep)) throw new Error(`unsafe path: ${relPath}`);
|
|
214
|
+
await mkdir(dirname(fp), { recursive: true });
|
|
215
|
+
await writeFile(fp, contents);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
let written = 1;
|
|
219
|
+
let note = "";
|
|
220
|
+
if (skill.type === "pack") {
|
|
221
|
+
// Pull the full pack (SKILL.md + companion files) in one call. The JSON
|
|
222
|
+
// bundle avoids adding an unzip dependency to this standalone binary.
|
|
223
|
+
try {
|
|
224
|
+
const bundle = await api(`/api/skills/${owner}/${slugName}/bundle?format=json`);
|
|
225
|
+
for (const f of bundle.files ?? []) {
|
|
226
|
+
if (f.path.includes("..") || f.path.startsWith("/")) continue;
|
|
227
|
+
let buf;
|
|
228
|
+
if (f.content_base64 != null) buf = Buffer.from(f.content_base64, "base64");
|
|
229
|
+
else if (f.source_url) {
|
|
230
|
+
const r = await fetch(f.source_url).catch(() => null);
|
|
231
|
+
if (!r || !r.ok) continue;
|
|
232
|
+
buf = Buffer.from(await r.arrayBuffer());
|
|
233
|
+
} else continue;
|
|
234
|
+
await writeSafe(f.path, f.path === "SKILL.md" ? raw : buf);
|
|
235
|
+
}
|
|
236
|
+
written = (bundle.files ?? []).length || 1;
|
|
237
|
+
if (!(bundle.files ?? []).some((f) => f.path === "SKILL.md")) await writeSafe("SKILL.md", raw);
|
|
238
|
+
} catch {
|
|
239
|
+
await writeSafe("SKILL.md", raw);
|
|
240
|
+
note = ` NOTE: pack assets could not be fetched — only SKILL.md was written. For the full pack run: npx skillmds add ${args.slug}`;
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
await writeSafe("SKILL.md", raw);
|
|
244
|
+
}
|
|
216
245
|
api(`/api/skills/${owner}/${slugName}/install`, { method: "POST", body: JSON.stringify({ via: "mcp" }) }).catch(() => {});
|
|
217
|
-
|
|
218
|
-
? ` NOTE: this is a pack and only SKILL.md was written — its assets/references are missing. For the full pack run: npx skillmds add ${args.slug}`
|
|
219
|
-
: "";
|
|
220
|
-
return text(`Installed ${args.slug} → ${file} (security: ${flags.join(", ") || "unscanned"})${note}`);
|
|
246
|
+
return text(`Installed ${args.slug} → ${dir} (${written} file${written === 1 ? "" : "s"}; security: ${flags.join(", ") || "unscanned"})${note}`);
|
|
221
247
|
}
|
|
222
248
|
default:
|
|
223
249
|
throw new Error(`Unknown tool: ${name}`);
|
package/dist/cli.js
CHANGED
|
@@ -779,7 +779,25 @@ function createClient(flags = {}) {
|
|
|
779
779
|
}
|
|
780
780
|
return await res.json();
|
|
781
781
|
}
|
|
782
|
-
return { api, base, hasToken: Boolean(token) };
|
|
782
|
+
return { api, base, token, hasToken: Boolean(token) };
|
|
783
|
+
}
|
|
784
|
+
async function fetchBundle(base, slug, token) {
|
|
785
|
+
const headers = { accept: "application/json" };
|
|
786
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
787
|
+
const res = await fetch(`${base}/api/skills/${slug}/bundle?format=json`, { headers }).catch(() => null);
|
|
788
|
+
if (!res || !res.ok) return null;
|
|
789
|
+
const data = await res.json().catch(() => null);
|
|
790
|
+
if (!data?.files) return null;
|
|
791
|
+
const out = [];
|
|
792
|
+
for (const f of data.files) {
|
|
793
|
+
if (f.content_base64 != null) {
|
|
794
|
+
out.push({ path: f.path, contents: Buffer.from(f.content_base64, "base64") });
|
|
795
|
+
} else if (f.source_url) {
|
|
796
|
+
const r = await fetch(f.source_url).catch(() => null);
|
|
797
|
+
if (r && r.ok) out.push({ path: f.path, contents: Buffer.from(await r.arrayBuffer()) });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return out;
|
|
783
801
|
}
|
|
784
802
|
function skillMdFor(skill) {
|
|
785
803
|
if (skill.raw_md) return skill.raw_md;
|
|
@@ -894,14 +912,14 @@ function infoCommand() {
|
|
|
894
912
|
}
|
|
895
913
|
|
|
896
914
|
// ../cli/src/commands/add.ts
|
|
897
|
-
import { Command as Command7 } from "commander";
|
|
915
|
+
import { Command as Command7, Option } from "commander";
|
|
898
916
|
import { existsSync as existsSync4 } from "fs";
|
|
899
917
|
import pc7 from "picocolors";
|
|
900
918
|
|
|
901
919
|
// ../cli/src/agents.ts
|
|
902
920
|
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";
|
|
903
921
|
import { homedir as homedir3 } from "os";
|
|
904
|
-
import { join as join4, resolve as resolve2, dirname as dirname2 } from "path";
|
|
922
|
+
import { join as join4, resolve as resolve2, dirname as dirname2, sep as sep2 } from "path";
|
|
905
923
|
var AGENTS = [
|
|
906
924
|
{ id: "claude-code", segments: [".claude", "skills"] },
|
|
907
925
|
{ id: "cursor", segments: [".cursor", "skills"] },
|
|
@@ -927,8 +945,12 @@ function writeSkill(skillName, files, targetDir, opts = {}) {
|
|
|
927
945
|
return dest;
|
|
928
946
|
}
|
|
929
947
|
mkdirSync3(dest, { recursive: true });
|
|
948
|
+
const root = resolve2(dest);
|
|
930
949
|
for (const f of files) {
|
|
931
|
-
const fp =
|
|
950
|
+
const fp = resolve2(dest, f.path);
|
|
951
|
+
if (fp !== root && !fp.startsWith(root + sep2)) {
|
|
952
|
+
throw new Error(`refused unsafe path in skill "${skillName}": ${f.path}`);
|
|
953
|
+
}
|
|
932
954
|
mkdirSync3(dirname2(fp), { recursive: true });
|
|
933
955
|
writeFileSync4(fp, f.contents);
|
|
934
956
|
}
|
|
@@ -974,12 +996,12 @@ var defaultDeps = {
|
|
|
974
996
|
return rs2.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
|
|
975
997
|
}
|
|
976
998
|
if (/^[^/\s:]+\/[^/\s:]+$/.test(arg)) {
|
|
977
|
-
const { api } = createClient(flags);
|
|
999
|
+
const { api, base: apiBase, token } = createClient(flags);
|
|
978
1000
|
const [owner, name] = arg.split("/");
|
|
979
1001
|
try {
|
|
980
1002
|
const skill = await api(`/api/skills/${owner}/${name}`);
|
|
981
1003
|
const skillName = skill.slug.split("/")[1] || name;
|
|
982
|
-
const
|
|
1004
|
+
const meta = {
|
|
983
1005
|
name: skillName,
|
|
984
1006
|
slug: skillName,
|
|
985
1007
|
registrySlug: arg,
|
|
@@ -988,10 +1010,15 @@ var defaultDeps = {
|
|
|
988
1010
|
securityFlags: skill.security_flags
|
|
989
1011
|
};
|
|
990
1012
|
if (skill.type === "pack") {
|
|
1013
|
+
const bundle = await fetchBundle(apiBase, arg, token);
|
|
1014
|
+
if (bundle && bundle.length) {
|
|
1015
|
+
const sk = bundle.find((f) => f.path === "SKILL.md");
|
|
1016
|
+
return [{ ...meta, raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill), files: bundle }];
|
|
1017
|
+
}
|
|
991
1018
|
const pack = await resolvePackFiles(skill);
|
|
992
|
-
if (pack) return [{ ...
|
|
1019
|
+
if (pack) return [{ ...meta, ...pack }];
|
|
993
1020
|
}
|
|
994
|
-
return [{ ...
|
|
1021
|
+
return [{ ...meta, raw: skillMdFor(skill) }];
|
|
995
1022
|
} catch {
|
|
996
1023
|
}
|
|
997
1024
|
}
|
|
@@ -1007,16 +1034,6 @@ var defaultDeps = {
|
|
|
1007
1034
|
body: JSON.stringify({ via: "cli" })
|
|
1008
1035
|
}).catch(() => {
|
|
1009
1036
|
});
|
|
1010
|
-
},
|
|
1011
|
-
async confirmUnverified(c, score, secFlags) {
|
|
1012
|
-
if (!process.stdin.isTTY) return false;
|
|
1013
|
-
const { confirm: confirm2, isCancel: isCancel4 } = await import("@clack/prompts");
|
|
1014
|
-
const ok = await confirm2({
|
|
1015
|
-
message: `${c.name} is not yet verified by SkillMD (lint score ${score}${secFlags.length ? `; flags: ${secFlags.join(", ")}` : ""}). Install anyway?`,
|
|
1016
|
-
initialValue: false
|
|
1017
|
-
});
|
|
1018
|
-
if (isCancel4(ok)) return false;
|
|
1019
|
-
return ok === true;
|
|
1020
1037
|
}
|
|
1021
1038
|
};
|
|
1022
1039
|
async function runAdd(arg, flags, deps = defaultDeps) {
|
|
@@ -1039,18 +1056,9 @@ async function runAdd(arg, flags, deps = defaultDeps) {
|
|
|
1039
1056
|
}
|
|
1040
1057
|
if (deniedFlags.length) {
|
|
1041
1058
|
blocked.push({ name: c.name, reason: `denied security flags: ${deniedFlags.join(", ")}` });
|
|
1042
|
-
lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 ${deniedFlags.join(", ")}`));
|
|
1059
|
+
lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 you passed --deny ${deniedFlags.join(", ")}`));
|
|
1043
1060
|
continue;
|
|
1044
1061
|
}
|
|
1045
|
-
if (c.registrySlug && c.verified === false && !flags.allowUnverified) {
|
|
1046
|
-
const ok = !flags.yes && deps.confirmUnverified ? await deps.confirmUnverified(c, result.score, allFlags) : false;
|
|
1047
|
-
if (!ok) {
|
|
1048
|
-
blocked.push({ name: c.name, reason: "unverified registry skill (use --allow-unverified)" });
|
|
1049
|
-
lines.push(pc7.yellow(`\u2717 ${c.name} blocked \u2014 not yet verified by SkillMD${allFlags.length ? ` (flags: ${allFlags.join(", ")})` : ""}`));
|
|
1050
|
-
lines.push(pc7.dim(` review at https://skillmd.com/skills/${c.registrySlug}, then re-run with --allow-unverified`));
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
1062
|
const files = c.files ?? [{ path: "SKILL.md", contents: c.raw }];
|
|
1055
1063
|
for (const agentId of targets) {
|
|
1056
1064
|
const dir = agentDir(agentId, scope);
|
|
@@ -1070,7 +1078,7 @@ async function runAdd(arg, flags, deps = defaultDeps) {
|
|
|
1070
1078
|
return { written, blocked, exitCode, output: lines.join("\n") };
|
|
1071
1079
|
}
|
|
1072
1080
|
function addCommand() {
|
|
1073
|
-
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").
|
|
1081
|
+
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").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) => {
|
|
1074
1082
|
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
1075
1083
|
const run = await runAdd(source, merged);
|
|
1076
1084
|
console.log(run.output);
|
package/package.json
CHANGED