skillmds 1.0.11 → 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 +15 -0
- package/bin/skillmd-mcp.mjs +253 -232
- package/dist/cli.js +218 -74
- package/package.json +15 -4
- package/vendor/index.js +3 -3
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`.
|
package/bin/skillmd-mcp.mjs
CHANGED
|
@@ -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://
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const r = await api(`/api/
|
|
147
|
-
return text(r
|
|
148
|
-
}
|
|
149
|
-
case "
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (!raw
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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,11 +735,14 @@ 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";
|
|
703
744
|
import { join as join3 } from "path";
|
|
704
|
-
var DEFAULT_API = "https://
|
|
745
|
+
var DEFAULT_API = "https://api.skillmd.com";
|
|
705
746
|
function configPath() {
|
|
706
747
|
return join3(homedir2(), ".skillmd", "config.json");
|
|
707
748
|
}
|
|
@@ -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 =
|
|
746
|
-
if (skill.license) fm.
|
|
747
|
-
|
|
748
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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,
|
|
1034
|
+
const out = writeSkill(c.name, files, dir, { mode: "copy" });
|
|
898
1035
|
written.push({ name: c.name, dir: out });
|
|
899
|
-
lines.push(
|
|
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
|
|
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
|
|
917
|
-
import
|
|
1059
|
+
import { Command as Command8 } from "commander";
|
|
1060
|
+
import pc8 from "picocolors";
|
|
918
1061
|
function listCommand() {
|
|
919
|
-
return new
|
|
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 ?
|
|
933
|
-
console.log(`${
|
|
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
|
|
941
|
-
import { rmSync as
|
|
942
|
-
import
|
|
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
|
-
|
|
1092
|
+
rmSync3(t.path, { recursive: true, force: true });
|
|
950
1093
|
removed.push(t.path);
|
|
951
|
-
lines.push(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
1127
|
+
const next = skillMdFor(skill);
|
|
985
1128
|
if (next.trim() === s.raw.trim()) {
|
|
986
|
-
lines.push(
|
|
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(
|
|
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(
|
|
1139
|
+
lines.push(pc10.green(`\u2713 updated ${s.name}`));
|
|
997
1140
|
} catch {
|
|
998
|
-
lines.push(
|
|
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
|
|
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
|
|
1018
|
-
import
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
1197
|
+
${pc11.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
|
|
1055
1198
|
}
|
|
1056
1199
|
function publishCommand() {
|
|
1057
|
-
return new
|
|
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
|
|
1209
|
+
import { Command as Command12 } from "commander";
|
|
1067
1210
|
import * as p3 from "@clack/prompts";
|
|
1068
1211
|
function loginCommand() {
|
|
1069
|
-
return new
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 ${
|
|
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 ?
|
|
1185
|
-
return `${
|
|
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
|
-
`${
|
|
1196
|
-
`${
|
|
1197
|
-
`${
|
|
1198
|
-
`${
|
|
1199
|
-
`${
|
|
1200
|
-
`${
|
|
1201
|
-
`${
|
|
1202
|
-
`${
|
|
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(
|
|
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
|
|
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.
|
|
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": {
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://skillmd.com/contact"
|
|
29
|
+
},
|
|
28
30
|
"author": "SkillMD",
|
|
29
31
|
"license": "MIT",
|
|
30
|
-
"files": [
|
|
31
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
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"));
|