skillmds 1.0.12 → 1.0.13

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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
4
4
 
5
+ ## 1.0.13
6
+
7
+ Safe & easy installs release.
8
+
9
+ - **Packs install their full file tree.** `skillmd add owner/pack` now downloads the pack's source directory (pinned to the registry's commit; falls back to the default branch with a warning if the pin is gone upstream), so `assets/` and `references/` files referenced by SKILL.md actually exist. Hardened: symlinks are skipped, 200-file/20MB caps, temp dirs cleaned up.
10
+ - **Unverified skills prompt instead of hard-blocking.** Interactive installs show the lint score and security flags and ask before installing; non-interactive installs (and `-y`) still require the explicit `--allow-unverified`. Blocked output links the skill's page for review.
11
+ - **New `skillmd info <slug>`** shows verification status, security flags, rating, provenance (source repo @ commit), and the install snippet. Unknown subcommands now print a proper error instead of silently starting the MCP server.
12
+ - **MCP `skillmd_install` gained the CLI's safety gates**: refuses unverified skills unless `allow_unverified: true` (only after explicit human approval), validates before writing, reports security flags, and guards against malformed skill names.
13
+ - **Installs are exact.** The registry now stores the published SKILL.md verbatim (`raw_md`) and installs deliver it byte-for-byte; reconstructed frontmatter (legacy rows) is YAML-quoted so descriptions with `:`/`#`/quotes can't corrupt the file.
14
+ - Description limit aligned with Claude Code's real spec (1024 chars, was 500) — valid skills are no longer flagged "(invalid SKILL.md)" by `skillmd list`, and `list` now says *why* a file is invalid.
15
+
16
+ ## 1.0.12
17
+
18
+ - Registry-native `skillmd` CLI shipped inside `skillmds`: `search`, `add`, `list`, `remove`, `update`, `publish`, `login`/`logout`, plus lint/scan/rules/init. (Release notes added retroactively.)
19
+
5
20
  ## 1.0.11
6
21
 
7
22
  - Security flags render with full readable labels, each with its own marker: `⚑ Executes Scripts ⚑ Calls Network ⚑ Reads Secrets`.
@@ -1,232 +1,253 @@
1
- #!/usr/bin/env node
2
- // SkillMD MCP server (stdio). Wraps the public SkillMD API so any MCP-capable agent
3
- // can search, fetch, install, and discover Agent Skills by name or task.
4
- //
5
- // claude mcp add skillmd -- npx -y skillmds
6
- //
7
- // Env:
8
- // SKILLMD_API override API base (default https://api.skillmd.com)
9
- // SKILLMD_TOKEN personal access token (enables skillmd_list_saved / personalized recommend)
10
-
11
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
- import { mkdir, writeFile } from "node:fs/promises";
15
- import { readFileSync } from "node:fs";
16
- import { homedir } from "node:os";
17
- import { join, resolve } from "node:path";
18
-
19
- const VERSION = (() => {
20
- try {
21
- return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
22
- } catch {
23
- return "0.0.0";
24
- }
25
- })();
26
- // Shared validation engine, bundled into ./vendor at publish time (npm run build).
27
- // Falls back gracefully if the vendor build is absent (e.g. running from source).
28
- let lint = null;
29
- try {
30
- ({ lint } = await import("../vendor/index.js"));
31
- } catch {
32
- /* skillmd_lint will report that validation is unavailable */
33
- }
34
-
35
- const API = process.env.SKILLMD_API || "https://api.skillmd.com";
36
- const TOKEN = process.env.SKILLMD_TOKEN || "";
37
-
38
- async function api(path, init = {}) {
39
- const headers = { accept: "application/json", ...(init.headers || {}) };
40
- if (TOKEN) headers.authorization = `Bearer ${TOKEN}`;
41
- const res = await fetch(API + path, { ...init, headers });
42
- if (!res.ok) throw new Error(`SkillMD API ${res.status} on ${path}: ${await res.text().catch(() => "")}`);
43
- return res.json();
44
- }
45
- const text = (obj) => ({ content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }] });
46
-
47
- const TOOLS = [
48
- {
49
- name: "skillmd_search",
50
- description: "Search SkillMD for Agent Skills by name, description, or task. Returns matching skills with ratings and install snippets.",
51
- inputSchema: {
52
- type: "object",
53
- properties: {
54
- query: { type: "string", description: "Natural-language query or keywords" },
55
- category: { type: "string", description: "Filter by category slug (e.g. devops, data, security)" },
56
- verified_only: { type: "boolean", description: "Only verified skills" },
57
- type: { type: "string", enum: ["single", "pack"], description: "single file or pack" },
58
- min_rating: { type: "number", description: "Minimum average rating 0-5" },
59
- limit: { type: "number", description: "Max results (default 20)" },
60
- },
61
- required: ["query"],
62
- },
63
- },
64
- {
65
- name: "skillmd_get",
66
- description: "Fetch a single skill by slug (owner/name), including its SKILL.md body, provenance, license and security flags.",
67
- inputSchema: { type: "object", properties: { slug: { type: "string", description: "owner/name, e.g. anthropic/pdf" } }, required: ["slug"] },
68
- },
69
- {
70
- name: "skillmd_install",
71
- description: "Install a skill by slug: writes its SKILL.md into the agent's skills directory. Never executes any scripts.",
72
- inputSchema: {
73
- type: "object",
74
- properties: {
75
- slug: { type: "string", description: "owner/name, e.g. anthropic/pdf" },
76
- dest: { type: "string", description: "Target skills dir. Default ~/.claude/skills" },
77
- },
78
- required: ["slug"],
79
- },
80
- },
81
- {
82
- name: "skillmd_trending",
83
- description: "Top skills on the SkillMD leaderboard. range: '30d' or 'all'.",
84
- inputSchema: {
85
- type: "object",
86
- properties: {
87
- range: { type: "string", enum: ["30d", "all"], description: "Time window (default all)" },
88
- category: { type: "string", description: "Optional category slug" },
89
- limit: { type: "number", description: "Max results (default 20)" },
90
- },
91
- },
92
- },
93
- {
94
- name: "skillmd_recommend",
95
- description: "Recommend skills. If based_on is given, suggests skills similar to it (same category); otherwise returns trending.",
96
- inputSchema: {
97
- type: "object",
98
- properties: { based_on: { type: "string", description: "A slug to base recommendations on" }, limit: { type: "number" } },
99
- },
100
- },
101
- {
102
- name: "skillmd_list_saved",
103
- description: "List the signed-in user's saved skills. Requires SKILLMD_TOKEN to be set.",
104
- inputSchema: { type: "object", properties: {} },
105
- },
106
- {
107
- name: "skillmd_lint",
108
- description:
109
- "Validate a SKILL.md against the SkillMD spec and return diagnostics, security flags, and a quality score (0-100). Pass raw SKILL.md content, or a registry slug (owner/name) to fetch and lint.",
110
- inputSchema: {
111
- type: "object",
112
- properties: {
113
- content: { type: "string", description: "Raw SKILL.md content to validate" },
114
- slug: { type: "string", description: "Registry slug owner/name to fetch and validate" },
115
- },
116
- },
117
- },
118
- ];
119
-
120
- function reconstructSkillMd(skill) {
121
- const fm = ["---", `name: ${skill.slug.split("/")[1] || skill.title}`, `description: ${String(skill.description).replace(/\n/g, " ")}`];
122
- if (skill.license) fm.push(`license: ${skill.license}`);
123
- fm.push("---", "");
124
- return fm.join("\n") + (skill.body_md || "");
125
- }
126
-
127
- async function handleCall(name, args) {
128
- switch (name) {
129
- case "skillmd_search": {
130
- const p = new URLSearchParams({ q: args.query, limit: String(args.limit ?? 20) });
131
- if (args.category) p.set("category", args.category);
132
- if (args.verified_only) p.set("verified", "true");
133
- if (args.type) p.set("type", args.type);
134
- if (args.min_rating != null) p.set("min_rating", String(args.min_rating));
135
- const r = await api(`/api/search?${p}`);
136
- return text(r.items ?? []);
137
- }
138
- case "skillmd_get": {
139
- const [owner, slugName] = String(args.slug).split("/");
140
- const r = await api(`/api/skills/${owner}/${slugName}`);
141
- return text(r);
142
- }
143
- case "skillmd_trending": {
144
- const p = new URLSearchParams({ range: args.range ?? "all", limit: String(args.limit ?? 20) });
145
- if (args.category) p.set("category", args.category);
146
- const r = await api(`/api/leaderboard?${p}`);
147
- return text(r.items ?? []);
148
- }
149
- case "skillmd_recommend": {
150
- if (args.based_on) {
151
- const [owner, slugName] = String(args.based_on).split("/");
152
- const base = await api(`/api/skills/${owner}/${slugName}`).catch(() => null);
153
- if (base?.category_slug) {
154
- const r = await api(`/api/leaderboard?range=all&limit=${args.limit ?? 10}&category=${base.category_slug}`);
155
- return text((r.items ?? []).filter((s) => s.slug !== args.based_on));
156
- }
157
- }
158
- const r = await api(`/api/leaderboard?range=30d&limit=${args.limit ?? 10}`);
159
- return text(r.items ?? []);
160
- }
161
- case "skillmd_list_saved": {
162
- if (!TOKEN) return text("Set SKILLMD_TOKEN (a SkillMD personal access token) to list your saved skills.");
163
- const r = await api(`/api/favorites`);
164
- return text(r.items ?? []);
165
- }
166
- case "skillmd_lint": {
167
- if (!lint) return text("Validation engine unavailable — run `npm run build` in the skillmds package.");
168
- let raw = args.content;
169
- let slug;
170
- if (!raw && args.slug) {
171
- const [owner, slugName] = String(args.slug).split("/");
172
- const skill = await api(`/api/skills/${owner}/${slugName}`);
173
- raw = reconstructSkillMd(skill);
174
- slug = slugName;
175
- }
176
- if (!raw) return text("Provide `content` (raw SKILL.md) or `slug` (owner/name).");
177
- const result = lint(raw, slug ? { slug } : {});
178
- return text({ ok: result.ok, score: result.score, diagnostics: result.diagnostics, security: result.security });
179
- }
180
- case "skillmd_install": {
181
- const [owner, slugName] = String(args.slug).split("/");
182
- const skill = await api(`/api/skills/${owner}/${slugName}`);
183
- const baseDir = args.dest ? resolve(args.dest) : join(homedir(), ".claude", "skills");
184
- const dir = join(baseDir, slugName);
185
- await mkdir(dir, { recursive: true });
186
- const file = join(dir, "SKILL.md");
187
- await writeFile(file, reconstructSkillMd(skill), "utf8");
188
- api(`/api/skills/${owner}/${slugName}/install`, { method: "POST", body: JSON.stringify({ via: "mcp" }) }).catch(() => {});
189
- const note = skill.type === "pack"
190
- ? " (pack assets are not bundled yet — only SKILL.md was written)"
191
- : "";
192
- return text(`Installed ${args.slug} → ${file}${note}`);
193
- }
194
- default:
195
- throw new Error(`Unknown tool: ${name}`);
196
- }
197
- }
198
-
199
- async function startMcpServer() {
200
- const server = new Server({ name: "skillmd", version: VERSION }, { capabilities: { tools: {} } });
201
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
202
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
203
- try {
204
- return await handleCall(req.params.name, req.params.arguments ?? {});
205
- } catch (e) {
206
- return { isError: true, content: [{ type: "text", text: String(e?.message || e) }] };
207
- }
208
- });
209
- const transport = new StdioServerTransport();
210
- await server.connect(transport);
211
- console.error("skillmd MCP server running (stdio)");
212
- }
213
-
214
- // This binary is invoked three ways:
215
- // - `skillmds lint .` etc. → run the CLI (so `npx skillmds <cmd>` works without -p)
216
- // - `skillmds` in a terminal → run the CLI (opens the interactive menu)
217
- // - `skillmds` over piped stdio → start the MCP server (how MCP clients launch it)
218
- // MCP clients always connect stdin as a pipe (never a TTY), so an interactive
219
- // stdin is a reliable signal that a human is present.
220
- const CLI_COMMANDS = new Set([
221
- "lint", "check", "scan", "rules", "init", "search", "add", "list", "ls",
222
- "remove", "rm", "update", "publish", "submit", "login", "logout", "help",
223
- "--help", "-h", "--version", "-V",
224
- ]);
225
- const first = process.argv[2];
226
- const isCliCommand = first && CLI_COMMANDS.has(first);
227
- const humanInTerminal = !first && process.stdin.isTTY;
228
- if (isCliCommand || humanInTerminal) {
229
- await import("../dist/cli.js"); // the CLI parses process.argv itself
230
- } else {
231
- await startMcpServer();
232
- }
1
+ #!/usr/bin/env node
2
+ // SkillMD MCP server (stdio). Wraps the public SkillMD API so any MCP-capable agent
3
+ // can search, fetch, install, and discover Agent Skills by name or task.
4
+ //
5
+ // claude mcp add skillmd -- npx -y skillmds
6
+ //
7
+ // Env:
8
+ // SKILLMD_API override API base (default https://api.skillmd.com)
9
+ // SKILLMD_TOKEN personal access token (enables skillmd_list_saved / personalized recommend)
10
+
11
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
+ import { mkdir, writeFile } from "node:fs/promises";
15
+ import { readFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { join, resolve } from "node:path";
18
+ import { stringify as stringifyYaml } from "yaml";
19
+
20
+ const VERSION = (() => {
21
+ try {
22
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
23
+ } catch {
24
+ return "0.0.0";
25
+ }
26
+ })();
27
+ // Shared validation engine, bundled into ./vendor at publish time (npm run build).
28
+ // Falls back gracefully if the vendor build is absent (e.g. running from source).
29
+ let lint = null;
30
+ try {
31
+ ({ lint } = await import("../vendor/index.js"));
32
+ } catch {
33
+ /* skillmd_lint will report that validation is unavailable */
34
+ }
35
+
36
+ const API = process.env.SKILLMD_API || "https://api.skillmd.com";
37
+ const TOKEN = process.env.SKILLMD_TOKEN || "";
38
+
39
+ async function api(path, init = {}) {
40
+ const headers = { accept: "application/json", ...(init.headers || {}) };
41
+ if (TOKEN) headers.authorization = `Bearer ${TOKEN}`;
42
+ const res = await fetch(API + path, { ...init, headers });
43
+ if (!res.ok) throw new Error(`SkillMD API ${res.status} on ${path}: ${await res.text().catch(() => "")}`);
44
+ return res.json();
45
+ }
46
+ const text = (obj) => ({ content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }] });
47
+
48
+ const TOOLS = [
49
+ {
50
+ name: "skillmd_search",
51
+ description: "Search SkillMD for Agent Skills by name, description, or task. Returns matching skills with ratings and install snippets.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ query: { type: "string", description: "Natural-language query or keywords" },
56
+ category: { type: "string", description: "Filter by category slug (e.g. devops, data, security)" },
57
+ verified_only: { type: "boolean", description: "Only verified skills" },
58
+ type: { type: "string", enum: ["single", "pack"], description: "single file or pack" },
59
+ min_rating: { type: "number", description: "Minimum average rating 0-5" },
60
+ limit: { type: "number", description: "Max results (default 20)" },
61
+ },
62
+ required: ["query"],
63
+ },
64
+ },
65
+ {
66
+ name: "skillmd_get",
67
+ description: "Fetch a single skill by slug (owner/name), including its SKILL.md body, provenance, license and security flags.",
68
+ inputSchema: { type: "object", properties: { slug: { type: "string", description: "owner/name, e.g. anthropic/pdf" } }, required: ["slug"] },
69
+ },
70
+ {
71
+ name: "skillmd_install",
72
+ description:
73
+ "Install a skill by slug: validates it, then writes its SKILL.md into the agent's skills directory. Never executes any scripts. Unverified skills are refused unless allow_unverified is true. Do not set allow_unverified on your own initiative — only after the human has explicitly approved installing this specific unverified skill.",
74
+ inputSchema: {
75
+ type: "object",
76
+ properties: {
77
+ slug: { type: "string", description: "owner/name, e.g. anthropic/pdf" },
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
+ },
81
+ required: ["slug"],
82
+ },
83
+ },
84
+ {
85
+ name: "skillmd_trending",
86
+ description: "Top skills on the SkillMD leaderboard. range: '30d' or 'all'.",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ range: { type: "string", enum: ["30d", "all"], description: "Time window (default all)" },
91
+ category: { type: "string", description: "Optional category slug" },
92
+ limit: { type: "number", description: "Max results (default 20)" },
93
+ },
94
+ },
95
+ },
96
+ {
97
+ name: "skillmd_recommend",
98
+ description: "Recommend skills. If based_on is given, suggests skills similar to it (same category); otherwise returns trending.",
99
+ inputSchema: {
100
+ type: "object",
101
+ properties: { based_on: { type: "string", description: "A slug to base recommendations on" }, limit: { type: "number" } },
102
+ },
103
+ },
104
+ {
105
+ name: "skillmd_list_saved",
106
+ description: "List the signed-in user's saved skills. Requires SKILLMD_TOKEN to be set.",
107
+ inputSchema: { type: "object", properties: {} },
108
+ },
109
+ {
110
+ name: "skillmd_lint",
111
+ description:
112
+ "Validate a SKILL.md against the SkillMD spec and return diagnostics, security flags, and a quality score (0-100). Pass raw SKILL.md content, or a registry slug (owner/name) to fetch and lint.",
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: {
116
+ content: { type: "string", description: "Raw SKILL.md content to validate" },
117
+ slug: { type: "string", description: "Registry slug owner/name to fetch and validate" },
118
+ },
119
+ },
120
+ },
121
+ ];
122
+
123
+ // KEEP IN SYNC with apps/cli/src/api.ts (reconstructSkillMd / skillMdFor) —
124
+ // duplicated because the MCP bin ships as a standalone script; no automated
125
+ // check enforces this.
126
+ function reconstructSkillMd(skill) {
127
+ if (skill.raw_md) return skill.raw_md;
128
+ const fm = { name: skill.slug.split("/")[1] || skill.title, description: String(skill.description).replace(/\n/g, " ") };
129
+ if (skill.license) fm.license = skill.license;
130
+ return `---\n${stringifyYaml(fm)}---\n${skill.body_md || ""}`;
131
+ }
132
+
133
+ async function handleCall(name, args) {
134
+ switch (name) {
135
+ case "skillmd_search": {
136
+ const p = new URLSearchParams({ q: args.query, limit: String(args.limit ?? 20) });
137
+ if (args.category) p.set("category", args.category);
138
+ if (args.verified_only) p.set("verified", "true");
139
+ if (args.type) p.set("type", args.type);
140
+ if (args.min_rating != null) p.set("min_rating", String(args.min_rating));
141
+ const r = await api(`/api/search?${p}`);
142
+ return text(r.items ?? []);
143
+ }
144
+ case "skillmd_get": {
145
+ const [owner, slugName] = String(args.slug).split("/");
146
+ const r = await api(`/api/skills/${owner}/${slugName}`);
147
+ return text(r);
148
+ }
149
+ case "skillmd_trending": {
150
+ const p = new URLSearchParams({ range: args.range ?? "all", limit: String(args.limit ?? 20) });
151
+ if (args.category) p.set("category", args.category);
152
+ const r = await api(`/api/leaderboard?${p}`);
153
+ return text(r.items ?? []);
154
+ }
155
+ case "skillmd_recommend": {
156
+ if (args.based_on) {
157
+ const [owner, slugName] = String(args.based_on).split("/");
158
+ const base = await api(`/api/skills/${owner}/${slugName}`).catch(() => null);
159
+ if (base?.category_slug) {
160
+ const r = await api(`/api/leaderboard?range=all&limit=${args.limit ?? 10}&category=${base.category_slug}`);
161
+ return text((r.items ?? []).filter((s) => s.slug !== args.based_on));
162
+ }
163
+ }
164
+ const r = await api(`/api/leaderboard?range=30d&limit=${args.limit ?? 10}`);
165
+ return text(r.items ?? []);
166
+ }
167
+ case "skillmd_list_saved": {
168
+ if (!TOKEN) return text("Set SKILLMD_TOKEN (a SkillMD personal access token) to list your saved skills.");
169
+ const r = await api(`/api/favorites`);
170
+ return text(r.items ?? []);
171
+ }
172
+ case "skillmd_lint": {
173
+ if (!lint) return text("Validation engine unavailable — run `npm run build` in the skillmds package.");
174
+ let raw = args.content;
175
+ let slug;
176
+ if (!raw && args.slug) {
177
+ const [owner, slugName] = String(args.slug).split("/");
178
+ const skill = await api(`/api/skills/${owner}/${slugName}`);
179
+ raw = reconstructSkillMd(skill);
180
+ slug = slugName;
181
+ }
182
+ if (!raw) return text("Provide `content` (raw SKILL.md) or `slug` (owner/name).");
183
+ const result = lint(raw, slug ? { slug } : {});
184
+ return text({ ok: result.ok, score: result.score, diagnostics: result.diagnostics, security: result.security });
185
+ }
186
+ case "skillmd_install": {
187
+ const [owner, slugName] = String(args.slug).split("/");
188
+ // Defense in depth: slugName becomes a directory name below — refuse
189
+ // anything outside the registry's slugify alphabet (path separators,
190
+ // "..", etc.) before touching the network or filesystem.
191
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slugName ?? "")) {
192
+ return text(`Refused: invalid skill name "${slugName}" — expected a lowercase slug.`);
193
+ }
194
+ const skill = await api(`/api/skills/${owner}/${slugName}`);
195
+ const flags = skill.security_flags ?? [];
196
+ if (skill.verified === false && !args.allow_unverified) {
197
+ return text(
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
+ }
203
+ const raw = reconstructSkillMd(skill); // returns raw_md verbatim when the registry has it
204
+ if (lint) {
205
+ const result = lint(raw, { slug: slugName });
206
+ if (!result.ok) {
207
+ const errors = result.diagnostics.filter((d) => d.severity === "error");
208
+ return text(`Refused: ${args.slug} failed validation (${errors.map((d) => `${d.id}: ${d.message}`).join("; ")}). Not installed.`);
209
+ }
210
+ }
211
+ const baseDir = args.dest ? resolve(args.dest) : join(homedir(), ".claude", "skills");
212
+ const dir = join(baseDir, slugName);
213
+ await mkdir(dir, { recursive: true });
214
+ const file = join(dir, "SKILL.md");
215
+ await writeFile(file, raw, "utf8");
216
+ api(`/api/skills/${owner}/${slugName}/install`, { method: "POST", body: JSON.stringify({ via: "mcp" }) }).catch(() => {});
217
+ const note = skill.type === "pack"
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}`);
221
+ }
222
+ default:
223
+ throw new Error(`Unknown tool: ${name}`);
224
+ }
225
+ }
226
+
227
+ async function startMcpServer() {
228
+ const server = new Server({ name: "skillmd", version: VERSION }, { capabilities: { tools: {} } });
229
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
230
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
231
+ try {
232
+ return await handleCall(req.params.name, req.params.arguments ?? {});
233
+ } catch (e) {
234
+ return { isError: true, content: [{ type: "text", text: String(e?.message || e) }] };
235
+ }
236
+ });
237
+ const transport = new StdioServerTransport();
238
+ await server.connect(transport);
239
+ console.error("skillmd MCP server running (stdio)");
240
+ }
241
+
242
+ // This binary is invoked three ways:
243
+ // - `skillmds <anything>` → run the CLI. commander owns argv, so known
244
+ // commands run and unknown commands get a proper "unknown command" error
245
+ // instead of silently starting an MCP server.
246
+ // - `skillmds` in a terminal → run the CLI (opens the interactive menu)
247
+ // - `skillmds` over piped stdio → start the MCP server (how MCP clients launch it;
248
+ // they always connect stdin as a pipe, never a TTY, and pass no args)
249
+ if (process.argv.length > 2 || process.stdin.isTTY) {
250
+ await import("../dist/cli.js"); // the CLI parses process.argv itself
251
+ } else {
252
+ await startMcpServer();
253
+ }
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ../cli/src/index.ts
2
- import { Command as Command12 } from "commander";
2
+ import { Command as Command13 } from "commander";
3
3
  import { readFileSync as readFileSync4 } from "fs";
4
4
 
5
5
  // ../cli/src/commands/lint.ts
@@ -13,7 +13,7 @@ import * as p from "@clack/prompts";
13
13
  // ../../packages/core/src/parse.ts
14
14
  import { parse as parseYaml } from "yaml";
15
15
  var MAX_SKILL_BYTES = 256 * 1024;
16
- var MAX_DESCRIPTION = 500;
16
+ var MAX_DESCRIPTION = 1024;
17
17
  var MAX_NAME = 120;
18
18
  var slugify = (s) => String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "skill";
19
19
  function parseSkillMd(raw, type = "single") {
@@ -30,7 +30,7 @@ function parseSkillMd(raw, type = "single") {
30
30
  const description = String(fm.description ?? "").replace(/\s+/g, " ").trim();
31
31
  if (!name) return { error: "frontmatter 'name' is required" };
32
32
  if (!description) return { error: "frontmatter 'description' is required" };
33
- if (description.length > MAX_DESCRIPTION) return { error: "description must be <= 500 chars" };
33
+ if (description.length > MAX_DESCRIPTION) return { error: `description must be <= ${MAX_DESCRIPTION} chars` };
34
34
  return {
35
35
  name: name.slice(0, MAX_NAME),
36
36
  description: description.slice(0, MAX_DESCRIPTION),
@@ -92,7 +92,7 @@ var RULES2 = [
92
92
  {
93
93
  id: "SK003",
94
94
  severity: "error",
95
- description: "description must be present and \u2264 500 chars",
95
+ description: "description must be present and \u2264 1024 chars",
96
96
  check(s, _ctx) {
97
97
  const out = [];
98
98
  if (!s.description.trim()) out.push(diag(this, "frontmatter 'description' is required"));
@@ -284,9 +284,9 @@ function toGithub(results) {
284
284
  }
285
285
 
286
286
  // ../cli/src/source.ts
287
- import { existsSync, statSync, readFileSync, readdirSync, mkdtempSync } from "fs";
287
+ import { existsSync, statSync, lstatSync, readFileSync, readdirSync, mkdtempSync, rmSync } from "fs";
288
288
  import { tmpdir } from "os";
289
- import { join, basename, dirname } from "path";
289
+ import { join, basename, dirname, relative, sep } from "path";
290
290
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
291
291
  // build/dependency dirs
292
292
  "node_modules",
@@ -357,6 +357,44 @@ async function resolveSource(arg) {
357
357
  if (files.length === 0) throw new Error(`No SKILL.md found in ${arg}`);
358
358
  return files.map(loadLocal);
359
359
  }
360
+ var MAX_PACK_FILES = 200;
361
+ var MAX_PACK_BYTES = 20 * 1024 * 1024;
362
+ function collectFiles(root, dir, limits = {}) {
363
+ const maxFiles = limits.maxFiles ?? MAX_PACK_FILES;
364
+ const maxBytes = limits.maxBytes ?? MAX_PACK_BYTES;
365
+ const out = [];
366
+ let bytes = 0;
367
+ const walk = (d, depth) => {
368
+ if (depth > MAX_DEPTH) return;
369
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
370
+ const fp = join(d, entry.name);
371
+ const st = lstatSync(fp);
372
+ if (st.isSymbolicLink()) continue;
373
+ if (st.isDirectory()) {
374
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
375
+ walk(fp, depth + 1);
376
+ } else if (st.isFile()) {
377
+ if (out.length >= maxFiles) throw new Error(`pack too large: > ${maxFiles} files`);
378
+ bytes += st.size;
379
+ if (bytes > maxBytes) throw new Error(`pack too large: > ${Math.round(maxBytes / (1024 * 1024))}MB`);
380
+ out.push({ path: relative(root, fp).split(sep).join("/"), contents: readFileSync(fp) });
381
+ }
382
+ }
383
+ };
384
+ walk(dir, 0);
385
+ return out;
386
+ }
387
+ async function resolveTree(sourceUrl, ref) {
388
+ const { downloadTemplate } = await import("giget");
389
+ const input = normalizeGiget(sourceUrl) + (ref ? `#${ref}` : "");
390
+ const dir = mkdtempSync(join(tmpdir(), "skillmd-pack-"));
391
+ try {
392
+ const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
393
+ return collectFiles(out, out);
394
+ } finally {
395
+ rmSync(dir, { recursive: true, force: true });
396
+ }
397
+ }
360
398
  function normalizeGiget(arg) {
361
399
  const m = arg.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/[^/]+\/(.+))?/);
362
400
  if (m) return `github:${m[1]}/${m[2]}${m[3] ? "/" + m[3] : ""}`;
@@ -697,6 +735,9 @@ function initCommand() {
697
735
  import { Command as Command5 } from "commander";
698
736
  import pc5 from "picocolors";
699
737
 
738
+ // ../cli/src/api.ts
739
+ import { stringify as stringifyYaml } from "yaml";
740
+
700
741
  // ../cli/src/config.ts
701
742
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
702
743
  import { homedir as homedir2 } from "os";
@@ -740,12 +781,17 @@ function createClient(flags = {}) {
740
781
  }
741
782
  return { api, base, hasToken: Boolean(token) };
742
783
  }
784
+ function skillMdFor(skill) {
785
+ if (skill.raw_md) return skill.raw_md;
786
+ return reconstructSkillMd(skill);
787
+ }
743
788
  function reconstructSkillMd(skill) {
744
789
  const name = skill.slug.split("/")[1] || skill.title;
745
- const fm = ["---", `name: ${name}`, `description: ${String(skill.description).replace(/\n/g, " ")}`];
746
- if (skill.license) fm.push(`license: ${skill.license}`);
747
- fm.push("---", "");
748
- return fm.join("\n") + (skill.body_md || "");
790
+ const fm = { name, description: String(skill.description).replace(/\n/g, " ") };
791
+ if (skill.license) fm.license = skill.license;
792
+ return `---
793
+ ${stringifyYaml(fm)}---
794
+ ${skill.body_md || ""}`;
749
795
  }
750
796
 
751
797
  // ../cli/src/commands/search.ts
@@ -775,13 +821,62 @@ function searchCommand() {
775
821
  });
776
822
  }
777
823
 
778
- // ../cli/src/commands/add.ts
824
+ // ../cli/src/commands/info.ts
779
825
  import { Command as Command6 } from "commander";
780
- import { existsSync as existsSync4 } from "fs";
781
826
  import pc6 from "picocolors";
827
+ async function runInfo(slug, flags) {
828
+ const [owner, name] = slug.split("/");
829
+ if (!owner || !name || slug.split("/").length !== 2) {
830
+ return { ok: false, exitCode: 1, output: pc6.red("Expected owner/name, e.g. anthropic/pdf") };
831
+ }
832
+ const { api } = createClient(flags);
833
+ let s;
834
+ try {
835
+ s = await api(`/api/skills/${owner}/${name}`);
836
+ } catch (err) {
837
+ const message = err instanceof Error ? err.message : String(err);
838
+ if (/^SkillMD API 404 /.test(message)) {
839
+ return { ok: false, exitCode: 1, output: pc6.red(`Skill not found on the registry: ${slug}`) };
840
+ }
841
+ return { ok: false, exitCode: 1, output: pc6.red(`Could not reach the SkillMD registry: ${message}`) };
842
+ }
843
+ if (flags.json) return { ok: true, exitCode: 0, output: JSON.stringify(s, null, 2), skill: s };
844
+ const lines = [];
845
+ lines.push(
846
+ `${pc6.bold(s.slug)} ${pc6.dim(`[${s.type ?? "single"}]`)} ${s.verified ? pc6.green("\u2713 verified") : pc6.yellow("unverified")}`
847
+ );
848
+ lines.push(` ${s.description}`);
849
+ lines.push(
850
+ pc6.dim(
851
+ ` category: ${s.category ?? "-"} \xB7 rating: ${s.avg_rating != null ? s.avg_rating.toFixed(1) : "-"} (${s.rating_count ?? 0}) \xB7 installs: ${s.install_count ?? 0}`
852
+ )
853
+ );
854
+ lines.push(
855
+ pc6.dim(` security: ${(s.security_flags ?? []).join(", ") || "unscanned"} \xB7 license: ${s.provenance?.license ?? s.license ?? "unknown"}`)
856
+ );
857
+ const sourceRepo = s.provenance?.source_repo ?? s.source_repo;
858
+ const commitSha = s.provenance?.commit_sha ?? s.commit_sha;
859
+ if (sourceRepo) lines.push(pc6.dim(` source: ${sourceRepo}${commitSha ? ` @ ${commitSha.slice(0, 8)}` : ""}`));
860
+ lines.push(pc6.dim(` page: https://skillmd.com/skills/${s.slug}`));
861
+ lines.push(pc6.dim(` install: ${s.install?.cli ?? `npx skillmds add ${s.slug}`}`));
862
+ return { ok: true, exitCode: 0, output: lines.join("\n"), skill: s };
863
+ }
864
+ function infoCommand() {
865
+ return new Command6("info").description("Show registry details for a skill: verification, security flags, provenance, rating").argument("<slug>", "owner/name, e.g. anthropic/pdf").action(async (slug, _opts, cmd) => {
866
+ const flags = { ...cmd.parent?.opts() };
867
+ const run = await runInfo(slug, flags);
868
+ if (run.output) (run.ok ? console.log : console.error)(run.output);
869
+ process.exitCode = run.exitCode;
870
+ });
871
+ }
872
+
873
+ // ../cli/src/commands/add.ts
874
+ import { Command as Command7 } from "commander";
875
+ import { existsSync as existsSync4 } from "fs";
876
+ import pc7 from "picocolors";
782
877
 
783
878
  // ../cli/src/agents.ts
784
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync, symlinkSync, existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
879
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync as rmSync2, symlinkSync, existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
785
880
  import { homedir as homedir3 } from "os";
786
881
  import { join as join4, resolve as resolve2, dirname as dirname2 } from "path";
787
882
  var AGENTS = [
@@ -804,7 +899,7 @@ function writeSkill(skillName, files, targetDir, opts = {}) {
804
899
  const dest = join4(targetDir, skillName);
805
900
  if (opts.mode === "symlink" && opts.canonicalDir) {
806
901
  mkdirSync3(dirname2(dest), { recursive: true });
807
- rmSync(dest, { recursive: true, force: true });
902
+ rmSync2(dest, { recursive: true, force: true });
808
903
  symlinkSync(resolve2(opts.canonicalDir), dest, "dir");
809
904
  return dest;
810
905
  }
@@ -812,7 +907,7 @@ function writeSkill(skillName, files, targetDir, opts = {}) {
812
907
  for (const f of files) {
813
908
  const fp = join4(dest, f.path);
814
909
  mkdirSync3(dirname2(fp), { recursive: true });
815
- writeFileSync4(fp, f.contents, "utf8");
910
+ writeFileSync4(fp, f.contents);
816
911
  }
817
912
  return dest;
818
913
  }
@@ -835,6 +930,20 @@ function installedSkills(opts = {}) {
835
930
  function isLocalish(arg) {
836
931
  return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync4(arg);
837
932
  }
933
+ async function resolvePackFiles(skill, fetchTree = resolveTree) {
934
+ if (!skill.source_repo) return null;
935
+ const attempts = [{ ref: skill.commit_sha ?? void 0, pinMiss: false }];
936
+ if (skill.commit_sha) attempts.push({ ref: void 0, pinMiss: true });
937
+ for (const attempt of attempts) {
938
+ try {
939
+ const files = await fetchTree(skill.source_repo, attempt.ref);
940
+ const sk = files.find((f) => f.path === "SKILL.md");
941
+ if (sk) return { raw: sk.contents.toString("utf8"), files, pinMiss: attempt.pinMiss };
942
+ } catch {
943
+ }
944
+ }
945
+ return null;
946
+ }
838
947
  var defaultDeps = {
839
948
  async resolve(arg, flags) {
840
949
  if (isLocalish(arg)) {
@@ -847,7 +956,19 @@ var defaultDeps = {
847
956
  try {
848
957
  const skill = await api(`/api/skills/${owner}/${name}`);
849
958
  const skillName = skill.slug.split("/")[1] || name;
850
- return [{ name: skillName, raw: reconstructSkillMd(skill), slug: skillName, registrySlug: arg, verified: skill.verified }];
959
+ const base = {
960
+ name: skillName,
961
+ slug: skillName,
962
+ registrySlug: arg,
963
+ verified: skill.verified,
964
+ type: skill.type,
965
+ securityFlags: skill.security_flags
966
+ };
967
+ if (skill.type === "pack") {
968
+ const pack = await resolvePackFiles(skill);
969
+ if (pack) return [{ ...base, ...pack }];
970
+ }
971
+ return [{ ...base, raw: skillMdFor(skill) }];
851
972
  } catch {
852
973
  }
853
974
  }
@@ -863,6 +984,16 @@ var defaultDeps = {
863
984
  body: JSON.stringify({ via: "cli" })
864
985
  }).catch(() => {
865
986
  });
987
+ },
988
+ async confirmUnverified(c, score, secFlags) {
989
+ if (!process.stdin.isTTY) return false;
990
+ const { confirm: confirm2, isCancel: isCancel4 } = await import("@clack/prompts");
991
+ const ok = await confirm2({
992
+ message: `${c.name} is not yet verified by SkillMD (lint score ${score}${secFlags.length ? `; flags: ${secFlags.join(", ")}` : ""}). Install anyway?`,
993
+ initialValue: false
994
+ });
995
+ if (isCancel4(ok)) return false;
996
+ return ok === true;
866
997
  }
867
998
  };
868
999
  async function runAdd(arg, flags, deps = defaultDeps) {
@@ -876,27 +1007,39 @@ async function runAdd(arg, flags, deps = defaultDeps) {
876
1007
  const targets = flags.agent?.length ? flags.agent : detectAgents(scope).length ? detectAgents(scope) : ["claude-code"];
877
1008
  for (const c of candidates) {
878
1009
  const result = lint(c.raw, { slug: c.slug });
879
- const deniedFlags = result.security.flags.filter((f) => deny.has(f));
1010
+ const allFlags = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
1011
+ const deniedFlags = allFlags.filter((f) => deny.has(f));
880
1012
  if (!flags.skipLint && !result.ok) {
881
1013
  blocked.push({ name: c.name, reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}` });
882
- lines.push(pc6.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
1014
+ lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
883
1015
  continue;
884
1016
  }
885
1017
  if (deniedFlags.length) {
886
1018
  blocked.push({ name: c.name, reason: `denied security flags: ${deniedFlags.join(", ")}` });
887
- lines.push(pc6.red(`\u2717 ${c.name} blocked \u2014 ${deniedFlags.join(", ")}`));
1019
+ lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 ${deniedFlags.join(", ")}`));
888
1020
  continue;
889
1021
  }
890
1022
  if (c.registrySlug && c.verified === false && !flags.allowUnverified) {
891
- blocked.push({ name: c.name, reason: "unverified registry skill (use --allow-unverified)" });
892
- lines.push(pc6.yellow(`\u2717 ${c.name} blocked \u2014 unverified (use --allow-unverified)`));
893
- continue;
1023
+ const ok = !flags.yes && deps.confirmUnverified ? await deps.confirmUnverified(c, result.score, allFlags) : false;
1024
+ if (!ok) {
1025
+ blocked.push({ name: c.name, reason: "unverified registry skill (use --allow-unverified)" });
1026
+ lines.push(pc7.yellow(`\u2717 ${c.name} blocked \u2014 not yet verified by SkillMD${allFlags.length ? ` (flags: ${allFlags.join(", ")})` : ""}`));
1027
+ lines.push(pc7.dim(` review at https://skillmd.com/skills/${c.registrySlug}, then re-run with --allow-unverified`));
1028
+ continue;
1029
+ }
894
1030
  }
1031
+ const files = c.files ?? [{ path: "SKILL.md", contents: c.raw }];
895
1032
  for (const agentId of targets) {
896
1033
  const dir = agentDir(agentId, scope);
897
- const out = writeSkill(c.name, [{ path: "SKILL.md", contents: c.raw }], dir, { mode: "copy" });
1034
+ const out = writeSkill(c.name, files, dir, { mode: "copy" });
898
1035
  written.push({ name: c.name, dir: out });
899
- lines.push(pc6.green(`\u2713 ${c.name} \u2192 ${out}`));
1036
+ lines.push(pc7.green(`\u2713 ${c.name} \u2192 ${out}`) + pc7.dim(` (score ${result.score}${allFlags.length ? `; ${allFlags.join(", ")}` : ""})`));
1037
+ }
1038
+ if (c.pinMiss) {
1039
+ lines.push(pc7.yellow(`\u26A0 ${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
1040
+ }
1041
+ if (c.type === "pack" && !c.files) {
1042
+ lines.push(pc7.yellow(`\u26A0 ${c.name} is a pack but its asset files could not be fetched \u2014 only SKILL.md was installed. It may reference missing files.`));
900
1043
  }
901
1044
  if (c.registrySlug) deps.fireInstall(c.registrySlug, flags);
902
1045
  }
@@ -904,7 +1047,7 @@ async function runAdd(arg, flags, deps = defaultDeps) {
904
1047
  return { written, blocked, exitCode, output: lines.join("\n") };
905
1048
  }
906
1049
  function addCommand() {
907
- return new Command6("add").description("Install a skill from the registry, a GitHub repo, or a local path (lints before writing)").argument("<source>", "registry slug (owner/name), GitHub source, or local path").option("-g, --global", "install to the user directory instead of the project").option("-a, --agent <agents...>", "target specific agents").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--copy", "copy files (default)", true).option("-y, --yes", "skip confirmation prompts").option("--allow-unverified", "allow installing unverified registry skills").option("--skip-lint", "install even if the skill fails lint").option("--deny <flags...>", "refuse skills with these security flags").action(async (source, opts, cmd) => {
1050
+ return new Command7("add").description("Install a skill from the registry, a GitHub repo, or a local path (lints before writing)").argument("<source>", "registry slug (owner/name), GitHub source, or local path").option("-g, --global", "install to the user directory instead of the project").option("-a, --agent <agents...>", "target specific agents").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--copy", "copy files (default)", true).option("-y, --yes", "skip confirmation prompts").option("--allow-unverified", "allow installing unverified registry skills").option("--skip-lint", "install even if the skill fails lint").option("--deny <flags...>", "refuse skills with these security flags").action(async (source, opts, cmd) => {
908
1051
  const merged = { ...cmd.parent?.opts(), ...opts };
909
1052
  const run = await runAdd(source, merged);
910
1053
  console.log(run.output);
@@ -913,10 +1056,10 @@ function addCommand() {
913
1056
  }
914
1057
 
915
1058
  // ../cli/src/commands/list.ts
916
- import { Command as Command7 } from "commander";
917
- import pc7 from "picocolors";
1059
+ import { Command as Command8 } from "commander";
1060
+ import pc8 from "picocolors";
918
1061
  function listCommand() {
919
- return new Command7("list").alias("ls").description("List installed skills across detected agents").option("-g, --global", "only global skills").option("--json", "machine-readable output").action((opts) => {
1062
+ return new Command8("list").alias("ls").description("List installed skills across detected agents").option("-g, --global", "only global skills").option("--json", "machine-readable output").action((opts) => {
920
1063
  const scope = { global: opts.global };
921
1064
  const skills = installedSkills(scope);
922
1065
  if (opts.json) {
@@ -929,44 +1072,44 @@ function listCommand() {
929
1072
  }
930
1073
  for (const s of skills) {
931
1074
  const parsed = parseSkillMd(s.raw);
932
- const desc = "error" in parsed ? pc7.red("(invalid SKILL.md)") : pc7.dim(parsed.description.slice(0, 80));
933
- console.log(`${pc7.bold(s.name)} ${pc7.dim(`[${s.agent}/${s.scope}]`)}
1075
+ const desc = "error" in parsed ? pc8.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc8.dim(parsed.description.slice(0, 80));
1076
+ console.log(`${pc8.bold(s.name)} ${pc8.dim(`[${s.agent}/${s.scope}]`)}
934
1077
  ${desc}`);
935
1078
  }
936
1079
  });
937
1080
  }
938
1081
 
939
1082
  // ../cli/src/commands/remove.ts
940
- import { Command as Command8 } from "commander";
941
- import { rmSync as rmSync2 } from "fs";
942
- import pc8 from "picocolors";
1083
+ import { Command as Command9 } from "commander";
1084
+ import { rmSync as rmSync3 } from "fs";
1085
+ import pc9 from "picocolors";
943
1086
  function runRemove(names, scope = {}) {
944
1087
  const installed = installedSkills(scope);
945
1088
  const targets = installed.filter((s) => names.includes(s.name));
946
1089
  const removed = [];
947
1090
  const lines = [];
948
1091
  for (const t of targets) {
949
- rmSync2(t.path, { recursive: true, force: true });
1092
+ rmSync3(t.path, { recursive: true, force: true });
950
1093
  removed.push(t.path);
951
- lines.push(pc8.green(`\u2713 removed ${t.name} (${t.agent})`));
1094
+ lines.push(pc9.green(`\u2713 removed ${t.name} (${t.agent})`));
952
1095
  }
953
1096
  for (const n of names) {
954
- if (!targets.some((t) => t.name === n)) lines.push(pc8.yellow(`- ${n} not installed`));
1097
+ if (!targets.some((t) => t.name === n)) lines.push(pc9.yellow(`- ${n} not installed`));
955
1098
  }
956
1099
  return { removed, output: lines.join("\n") };
957
1100
  }
958
1101
  function removeCommand() {
959
- return new Command8("remove").alias("rm").description("Remove installed skills").argument("<names...>", "skill names to remove").option("-g, --global", "only global skills").action((names, opts) => {
1102
+ return new Command9("remove").alias("rm").description("Remove installed skills").argument("<names...>", "skill names to remove").option("-g, --global", "only global skills").action((names, opts) => {
960
1103
  const run = runRemove(names, { global: opts.global });
961
1104
  console.log(run.output);
962
1105
  });
963
1106
  }
964
1107
 
965
1108
  // ../cli/src/commands/update.ts
966
- import { Command as Command9 } from "commander";
1109
+ import { Command as Command10 } from "commander";
967
1110
  import { writeFileSync as writeFileSync5 } from "fs";
968
1111
  import { join as join5 } from "path";
969
- import pc9 from "picocolors";
1112
+ import pc10 from "picocolors";
970
1113
  async function runUpdate(names, flags, scope = {}) {
971
1114
  const { api } = createClient(flags);
972
1115
  const installed = installedSkills(scope).filter((s) => names.length === 0 || names.includes(s.name));
@@ -975,27 +1118,27 @@ async function runUpdate(names, flags, scope = {}) {
975
1118
  for (const s of installed) {
976
1119
  const slug = await findRegistrySlug(api, s);
977
1120
  if (!slug) {
978
- lines.push(pc9.dim(`- ${s.name} skipped (no registry match)`));
1121
+ lines.push(pc10.dim(`- ${s.name} skipped (no registry match)`));
979
1122
  continue;
980
1123
  }
981
1124
  try {
982
1125
  const [owner, name] = slug.split("/");
983
1126
  const skill = await api(`/api/skills/${owner}/${name}`);
984
- const next = reconstructSkillMd(skill);
1127
+ const next = skillMdFor(skill);
985
1128
  if (next.trim() === s.raw.trim()) {
986
- lines.push(pc9.dim(`- ${s.name} already up to date`));
1129
+ lines.push(pc10.dim(`- ${s.name} already up to date`));
987
1130
  continue;
988
1131
  }
989
1132
  const result = lint(next, { slug: s.name });
990
1133
  if (!result.ok) {
991
- lines.push(pc9.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
1134
+ lines.push(pc10.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
992
1135
  continue;
993
1136
  }
994
1137
  writeFileSync5(join5(s.path, "SKILL.md"), next, "utf8");
995
1138
  updated.push(s.name);
996
- lines.push(pc9.green(`\u2713 updated ${s.name}`));
1139
+ lines.push(pc10.green(`\u2713 updated ${s.name}`));
997
1140
  } catch {
998
- lines.push(pc9.red(`\u2717 ${s.name} update error`));
1141
+ lines.push(pc10.red(`\u2717 ${s.name} update error`));
999
1142
  }
1000
1143
  }
1001
1144
  return { updated, output: lines.join("\n") || "Nothing to update." };
@@ -1006,7 +1149,7 @@ async function findRegistrySlug(api, s) {
1006
1149
  return hit?.slug ?? null;
1007
1150
  }
1008
1151
  function updateCommand() {
1009
- return new Command9("update").description("Update installed skills from the registry (re-lints before writing)").argument("[names...]", "specific skills to update (default: all)").option("-g, --global", "only global skills").action(async (names, opts, cmd) => {
1152
+ return new Command10("update").description("Update installed skills from the registry (re-lints before writing)").argument("[names...]", "specific skills to update (default: all)").option("-g, --global", "only global skills").action(async (names, opts, cmd) => {
1010
1153
  const merged = { ...cmd.parent?.opts(), ...opts };
1011
1154
  const run = await runUpdate(names, merged, { global: opts.global });
1012
1155
  console.log(run.output);
@@ -1014,8 +1157,8 @@ function updateCommand() {
1014
1157
  }
1015
1158
 
1016
1159
  // ../cli/src/commands/publish.ts
1017
- import { Command as Command10 } from "commander";
1018
- import pc10 from "picocolors";
1160
+ import { Command as Command11 } from "commander";
1161
+ import pc11 from "picocolors";
1019
1162
  var defaultDeps2 = {
1020
1163
  resolve: resolveSource,
1021
1164
  async submit(payload, flags) {
@@ -1038,23 +1181,23 @@ async function runPublish(target, flags, deps = defaultDeps2) {
1038
1181
  const report = toText([{ file: src.path, result }], { decorate });
1039
1182
  if (!result.ok) {
1040
1183
  return { result, published: false, exitCode: 1, output: `${report}
1041
- ${pc10.red("\u2717 Refusing to publish: fix the errors above.")}` };
1184
+ ${pc11.red("\u2717 Refusing to publish: fix the errors above.")}` };
1042
1185
  }
1043
1186
  const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
1044
1187
  if (hasWarn && !flags.force) {
1045
1188
  return { result, published: false, exitCode: 1, output: `${report}
1046
- ${pc10.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
1189
+ ${pc11.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
1047
1190
  }
1048
1191
  if (flags.dryRun) {
1049
1192
  return { result, published: false, exitCode: 0, output: `${report}
1050
- ${pc10.green("\u2713 Dry run: would publish.")}` };
1193
+ ${pc11.green("\u2713 Dry run: would publish.")}` };
1051
1194
  }
1052
1195
  const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
1053
1196
  return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
1054
- ${pc10.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
1197
+ ${pc11.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
1055
1198
  }
1056
1199
  function publishCommand() {
1057
- return new Command10("publish").alias("submit").description("Publish a skill to the registry (refuses to publish on lint errors)").argument("[target]", "local path to the skill", ".").option("--type <type>", "single | pack", "single").option("--force", "publish despite warnings (never bypasses errors)").option("--dry-run", "lint only; do not publish").action(async (target, opts, cmd) => {
1200
+ return new Command11("publish").alias("submit").description("Publish a skill to the registry (refuses to publish on lint errors)").argument("[target]", "local path to the skill", ".").option("--type <type>", "single | pack", "single").option("--force", "publish despite warnings (never bypasses errors)").option("--dry-run", "lint only; do not publish").action(async (target, opts, cmd) => {
1058
1201
  const merged = { ...cmd.parent?.opts(), ...opts };
1059
1202
  const run = await runPublish(target, merged);
1060
1203
  console.log(run.output);
@@ -1063,10 +1206,10 @@ function publishCommand() {
1063
1206
  }
1064
1207
 
1065
1208
  // ../cli/src/commands/login.ts
1066
- import { Command as Command11 } from "commander";
1209
+ import { Command as Command12 } from "commander";
1067
1210
  import * as p3 from "@clack/prompts";
1068
1211
  function loginCommand() {
1069
- return new Command11("login").description("Store a SkillMD personal access token for publishing").option("--token <token>", "token to store (otherwise prompt)").option("--api <url>", "registry API base to store").action(async (opts) => {
1212
+ return new Command12("login").description("Store a SkillMD personal access token for publishing").option("--token <token>", "token to store (otherwise prompt)").option("--api <url>", "registry API base to store").action(async (opts) => {
1070
1213
  let token = opts.token;
1071
1214
  if (!token) {
1072
1215
  const answer = await p3.password({ message: "Paste your SkillMD token (from your account page)" });
@@ -1089,7 +1232,7 @@ function loginCommand() {
1089
1232
  });
1090
1233
  }
1091
1234
  function logoutCommand() {
1092
- return new Command11("logout").description("Remove the stored SkillMD token").action(() => {
1235
+ return new Command12("logout").description("Remove the stored SkillMD token").action(() => {
1093
1236
  const cfg = readConfig();
1094
1237
  delete cfg.token;
1095
1238
  writeConfig(cfg);
@@ -1099,7 +1242,7 @@ function logoutCommand() {
1099
1242
 
1100
1243
  // ../cli/src/commands/interactive.ts
1101
1244
  import * as p4 from "@clack/prompts";
1102
- import pc11 from "picocolors";
1245
+ import pc12 from "picocolors";
1103
1246
  function bail(v) {
1104
1247
  if (p4.isCancel(v)) {
1105
1248
  p4.cancel("Cancelled.");
@@ -1108,7 +1251,7 @@ function bail(v) {
1108
1251
  return false;
1109
1252
  }
1110
1253
  async function runInteractive() {
1111
- p4.intro(pc11.bgCyan(pc11.black(" skillmd ")));
1254
+ p4.intro(pc12.bgCyan(pc12.black(" skillmd ")));
1112
1255
  const action = await p4.select({
1113
1256
  message: "What would you like to do?",
1114
1257
  options: [
@@ -1135,7 +1278,7 @@ async function runInteractive() {
1135
1278
  console.log(run.output);
1136
1279
  process.exitCode = run.exitCode;
1137
1280
  } catch (err) {
1138
- s.stop(pc11.red(err instanceof Error ? err.message : String(err)));
1281
+ s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
1139
1282
  process.exitCode = 1;
1140
1283
  }
1141
1284
  break;
@@ -1152,7 +1295,7 @@ async function runInteractive() {
1152
1295
  s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
1153
1296
  console.log(run.output);
1154
1297
  } catch (err) {
1155
- s.stop(pc11.red(err instanceof Error ? err.message : String(err)));
1298
+ s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
1156
1299
  process.exitCode = 1;
1157
1300
  }
1158
1301
  break;
@@ -1164,7 +1307,7 @@ async function runInteractive() {
1164
1307
  if (bail(description)) return;
1165
1308
  try {
1166
1309
  const file = initSkill(String(name), { description: String(description || "") });
1167
- p4.log.success(`Created ${pc11.bold(file)}`);
1310
+ p4.log.success(`Created ${pc12.bold(file)}`);
1168
1311
  } catch (err) {
1169
1312
  p4.log.error(err instanceof Error ? err.message : String(err));
1170
1313
  process.exitCode = 1;
@@ -1181,8 +1324,8 @@ async function runInteractive() {
1181
1324
  p4.log.message(
1182
1325
  skills.map((sk) => {
1183
1326
  const parsed = parseSkillMd(sk.raw);
1184
- const desc = "error" in parsed ? pc11.red("(invalid)") : pc11.dim(parsed.description.slice(0, 70));
1185
- return `${pc11.bold(sk.name)} ${pc11.dim(`[${sk.agent}]`)}
1327
+ const desc = "error" in parsed ? pc12.red("(invalid)") : pc12.dim(parsed.description.slice(0, 70));
1328
+ return `${pc12.bold(sk.name)} ${pc12.dim(`[${sk.agent}]`)}
1186
1329
  ${desc}`;
1187
1330
  }).join("\n")
1188
1331
  );
@@ -1192,21 +1335,21 @@ async function runInteractive() {
1192
1335
  case "help": {
1193
1336
  p4.note(
1194
1337
  [
1195
- `${pc11.bold("lint")} [path] validate SKILL.md + quality score`,
1196
- `${pc11.bold("scan")} [path] security scan`,
1197
- `${pc11.bold("search")} <query> search the registry`,
1198
- `${pc11.bold("add")} <source> install a skill (lints first)`,
1199
- `${pc11.bold("init")} [name] scaffold a new skill`,
1200
- `${pc11.bold("list / remove / update")} manage installed skills`,
1201
- `${pc11.bold("publish")} [path] publish (fails on lint errors)`,
1202
- `${pc11.bold("rules / login")}`
1338
+ `${pc12.bold("lint")} [path] validate SKILL.md + quality score`,
1339
+ `${pc12.bold("scan")} [path] security scan`,
1340
+ `${pc12.bold("search")} <query> search the registry`,
1341
+ `${pc12.bold("add")} <source> install a skill (lints first)`,
1342
+ `${pc12.bold("init")} [name] scaffold a new skill`,
1343
+ `${pc12.bold("list / remove / update")} manage installed skills`,
1344
+ `${pc12.bold("publish")} [path] publish (fails on lint errors)`,
1345
+ `${pc12.bold("rules / login")}`
1203
1346
  ].join("\n"),
1204
1347
  "Commands (run: skillmd <command>)"
1205
1348
  );
1206
1349
  break;
1207
1350
  }
1208
1351
  }
1209
- p4.outro(pc11.dim("skillmd.com"));
1352
+ p4.outro(pc12.dim("skillmd.com"));
1210
1353
  }
1211
1354
 
1212
1355
  // ../cli/src/index.ts
@@ -1215,12 +1358,13 @@ try {
1215
1358
  version = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
1216
1359
  } catch {
1217
1360
  }
1218
- var program = new Command12("skillmd").version(version).description("Lint, validate, and install Agent Skills from the SkillMD registry.").option("--json", "machine-readable output").option("--token <token>", "SkillMD personal access token").option("--api <url>", "override the registry API base URL");
1361
+ var program = new Command13("skillmd").version(version).description("Lint, validate, and install Agent Skills from the SkillMD registry.").option("--json", "machine-readable output").option("--token <token>", "SkillMD personal access token").option("--api <url>", "override the registry API base URL");
1219
1362
  program.addCommand(lintCommand());
1220
1363
  program.addCommand(scanCommand());
1221
1364
  program.addCommand(rulesCommand());
1222
1365
  program.addCommand(initCommand());
1223
1366
  program.addCommand(searchCommand());
1367
+ program.addCommand(infoCommand());
1224
1368
  program.addCommand(addCommand());
1225
1369
  program.addCommand(listCommand());
1226
1370
  program.addCommand(removeCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillmds",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
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": {
@@ -24,11 +24,22 @@
24
24
  "llm"
25
25
  ],
26
26
  "homepage": "https://skillmd.com",
27
- "bugs": { "url": "https://skillmd.com/contact" },
27
+ "bugs": {
28
+ "url": "https://skillmd.com/contact"
29
+ },
28
30
  "author": "SkillMD",
29
31
  "license": "MIT",
30
- "files": ["bin", "dist", "vendor", "assets", "README.md", "CHANGELOG.md"],
31
- "engines": { "node": ">=18" },
32
+ "files": [
33
+ "bin",
34
+ "dist",
35
+ "vendor",
36
+ "assets",
37
+ "README.md",
38
+ "CHANGELOG.md"
39
+ ],
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
32
43
  "dependencies": {
33
44
  "@clack/prompts": "^0.8.0",
34
45
  "@modelcontextprotocol/sdk": "^1.13.0",
package/vendor/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // ../../packages/core/src/parse.ts
2
2
  import { parse as parseYaml } from "yaml";
3
3
  var MAX_SKILL_BYTES = 256 * 1024;
4
- var MAX_DESCRIPTION = 500;
4
+ var MAX_DESCRIPTION = 1024;
5
5
  var MAX_NAME = 120;
6
6
  var slugify = (s) => String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "skill";
7
7
  function parseSkillMd(raw, type = "single") {
@@ -18,7 +18,7 @@ function parseSkillMd(raw, type = "single") {
18
18
  const description = String(fm.description ?? "").replace(/\s+/g, " ").trim();
19
19
  if (!name) return { error: "frontmatter 'name' is required" };
20
20
  if (!description) return { error: "frontmatter 'description' is required" };
21
- if (description.length > MAX_DESCRIPTION) return { error: "description must be <= 500 chars" };
21
+ if (description.length > MAX_DESCRIPTION) return { error: `description must be <= ${MAX_DESCRIPTION} chars` };
22
22
  return {
23
23
  name: name.slice(0, MAX_NAME),
24
24
  description: description.slice(0, MAX_DESCRIPTION),
@@ -98,7 +98,7 @@ var RULES2 = [
98
98
  {
99
99
  id: "SK003",
100
100
  severity: "error",
101
- description: "description must be present and \u2264 500 chars",
101
+ description: "description must be present and \u2264 1024 chars",
102
102
  check(s, _ctx) {
103
103
  const out = [];
104
104
  if (!s.description.trim()) out.push(diag(this, "frontmatter 'description' is required"));