skillmds 1.0.16 → 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.
@@ -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 = (() => {
@@ -205,13 +205,45 @@ async function handleCall(name, args) {
205
205
  const baseDir = args.dest ? resolve(args.dest) : join(homedir(), ".claude", "skills");
206
206
  const dir = join(baseDir, slugName);
207
207
  await mkdir(dir, { recursive: true });
208
- const file = join(dir, "SKILL.md");
209
- await writeFile(file, raw, "utf8");
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
+ }
210
245
  api(`/api/skills/${owner}/${slugName}/install`, { method: "POST", body: JSON.stringify({ via: "mcp" }) }).catch(() => {});
211
- const note = skill.type === "pack"
212
- ? ` 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}`
213
- : "";
214
- 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}`);
215
247
  }
216
248
  default:
217
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;
@@ -901,7 +919,7 @@ import pc7 from "picocolors";
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 = join4(dest, f.path);
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 base = {
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 [{ ...base, ...pack }];
1019
+ if (pack) return [{ ...meta, ...pack }];
993
1020
  }
994
- return [{ ...base, raw: skillMdFor(skill) }];
1021
+ return [{ ...meta, raw: skillMdFor(skill) }];
995
1022
  } catch {
996
1023
  }
997
1024
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillmds",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "SkillMD for agents: the `skillmds` MCP server + the `skillmd` CLI (lint, scan, search, install, publish Agent Skills) from skillmd.com.",
5
5
  "type": "module",
6
6
  "bin": {