get-skill 1.0.21
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 +117 -0
- package/README.md +128 -0
- package/assets/skillmd-banner.jpg +0 -0
- package/bin/skillmd-mcp.mjs +331 -0
- package/bin/skillmd.mjs +3 -0
- package/dist/cli.js +1742 -0
- package/package.json +55 -0
- package/vendor/index.js +333 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1742 @@
|
|
|
1
|
+
// ../cli/src/index.ts
|
|
2
|
+
import { Command as Command13 } from "commander";
|
|
3
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4
|
+
|
|
5
|
+
// ../cli/src/commands/lint.ts
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { writeFileSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { resolve } from "path";
|
|
10
|
+
import pc2 from "picocolors";
|
|
11
|
+
import * as p from "@clack/prompts";
|
|
12
|
+
|
|
13
|
+
// ../../packages/core/src/parse.ts
|
|
14
|
+
import { parse as parseYaml } from "yaml";
|
|
15
|
+
var MAX_SKILL_BYTES = 256 * 1024;
|
|
16
|
+
var MAX_DESCRIPTION = 1024;
|
|
17
|
+
var MAX_NAME = 120;
|
|
18
|
+
var slugify = (s) => String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "skill";
|
|
19
|
+
function parseSkillMd(raw, type = "single") {
|
|
20
|
+
if (!raw || raw.length > MAX_SKILL_BYTES) return { error: "SKILL.md missing or too large (max 256KB)" };
|
|
21
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
22
|
+
if (!m) return { error: "Missing YAML frontmatter (--- ... ---)" };
|
|
23
|
+
let fm;
|
|
24
|
+
try {
|
|
25
|
+
fm = parseYaml(m[1] ?? "") ?? {};
|
|
26
|
+
} catch {
|
|
27
|
+
return { error: "Invalid YAML frontmatter" };
|
|
28
|
+
}
|
|
29
|
+
const name = String(fm.name ?? "").trim();
|
|
30
|
+
const description = String(fm.description ?? "").replace(/\s+/g, " ").trim();
|
|
31
|
+
if (!name) return { error: "frontmatter 'name' is required" };
|
|
32
|
+
if (!description) return { error: "frontmatter 'description' is required" };
|
|
33
|
+
if (description.length > MAX_DESCRIPTION) return { error: `description must be <= ${MAX_DESCRIPTION} chars` };
|
|
34
|
+
return {
|
|
35
|
+
name: name.slice(0, MAX_NAME),
|
|
36
|
+
description: description.slice(0, MAX_DESCRIPTION),
|
|
37
|
+
license: fm.license ? String(fm.license).slice(0, 64) : null,
|
|
38
|
+
body: m[2] ?? "",
|
|
39
|
+
type
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ../../packages/core/src/security.ts
|
|
44
|
+
var UNTRUSTED_INSTALL_RE = new RegExp(
|
|
45
|
+
[
|
|
46
|
+
/(?:npx|npm|yarn|pnpm|bun)\b[^\n]*--registry[= ]/.source,
|
|
47
|
+
// custom npm registry
|
|
48
|
+
/npm\s+config\s+set\s+registry/.source,
|
|
49
|
+
/pip3?\s+install\b[^\n]*(?:--index-url|--extra-index-url|-i\s+https?:)/.source,
|
|
50
|
+
// custom pip index
|
|
51
|
+
/(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba|z|fi)?sh\b/.source,
|
|
52
|
+
// curl … | sh
|
|
53
|
+
/(?:iwr|irm|invoke-webrequest|invoke-restmethod)\b[^\n|]*\|\s*iex\b/.source,
|
|
54
|
+
// PowerShell pipe-to-exec
|
|
55
|
+
/bash\s+<\(\s*(?:curl|wget)/.source,
|
|
56
|
+
// bash <(curl …)
|
|
57
|
+
/(?:pip3?|npm|npx)\s+(?:install|--yes|-y)?\s*[^\n]*https?:\/\/(?!registry\.npmjs\.org|pypi\.org|files\.pythonhosted\.org)[^\s'"`]+\.(?:whl|tar\.gz|tgz|zip)\b/.source
|
|
58
|
+
// install from raw archive URL
|
|
59
|
+
].join("|"),
|
|
60
|
+
"i"
|
|
61
|
+
);
|
|
62
|
+
var RULES = [
|
|
63
|
+
["untrusted_install", UNTRUSTED_INSTALL_RE],
|
|
64
|
+
["executes_scripts", /```(?:sh|bash|python|py|js|ts|ruby|rb)\b|\b(subprocess|os\.system|child_process|exec\()/i],
|
|
65
|
+
["network_calls", /\b(fetch|requests\.|urllib|axios|curl|wget|https?:\/\/|websocket)\b/i],
|
|
66
|
+
["reads_secrets", /\b(api[_-]?key|secret|token|password|os\.environ|process\.env|getenv)\b/i]
|
|
67
|
+
];
|
|
68
|
+
function scanSecurity(body) {
|
|
69
|
+
const findings = [];
|
|
70
|
+
body.split(/\r?\n/).forEach((text3, i) => {
|
|
71
|
+
for (const [flag, re] of RULES) {
|
|
72
|
+
if (re.test(text3)) findings.push({ flag, line: i + 1, snippet: text3.trim().slice(0, 120) });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const flags = [...new Set(findings.map((f) => f.flag))];
|
|
76
|
+
return { flags: flags.length ? flags : ["docs_only"], findings };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ../../packages/core/src/rules.ts
|
|
80
|
+
function utf8Bytes(s) {
|
|
81
|
+
let bytes = 0;
|
|
82
|
+
for (let i = 0; i < s.length; i++) {
|
|
83
|
+
const c = s.charCodeAt(i);
|
|
84
|
+
if (c < 128) bytes += 1;
|
|
85
|
+
else if (c < 2048) bytes += 2;
|
|
86
|
+
else if (c >= 55296 && c <= 56319) {
|
|
87
|
+
bytes += 4;
|
|
88
|
+
i++;
|
|
89
|
+
} else bytes += 3;
|
|
90
|
+
}
|
|
91
|
+
return bytes;
|
|
92
|
+
}
|
|
93
|
+
var diag = (rule, message, line) => ({
|
|
94
|
+
id: rule.id,
|
|
95
|
+
severity: rule.severity,
|
|
96
|
+
message,
|
|
97
|
+
...line !== void 0 ? { line } : {}
|
|
98
|
+
});
|
|
99
|
+
var RULES2 = [
|
|
100
|
+
{
|
|
101
|
+
id: "SK002",
|
|
102
|
+
severity: "error",
|
|
103
|
+
description: "name must be present and \u2264 120 chars",
|
|
104
|
+
check(s, _ctx) {
|
|
105
|
+
const out = [];
|
|
106
|
+
if (!s.name.trim()) out.push(diag(this, "frontmatter 'name' is required"));
|
|
107
|
+
else if (s.name.length > MAX_NAME) out.push(diag(this, `name must be \u2264 ${MAX_NAME} chars`));
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "SK003",
|
|
113
|
+
severity: "error",
|
|
114
|
+
description: "description must be present and \u2264 1024 chars",
|
|
115
|
+
check(s, _ctx) {
|
|
116
|
+
const out = [];
|
|
117
|
+
if (!s.description.trim()) out.push(diag(this, "frontmatter 'description' is required"));
|
|
118
|
+
else if (s.description.length > MAX_DESCRIPTION) out.push(diag(this, `description must be \u2264 ${MAX_DESCRIPTION} chars`));
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "SK010",
|
|
124
|
+
severity: "warn",
|
|
125
|
+
description: "description should be \u2265 20 chars (not too terse)",
|
|
126
|
+
check(s, _ctx) {
|
|
127
|
+
return s.description.trim().length > 0 && s.description.trim().length < 20 ? [diag(this, "description is very terse (< 20 chars); add more detail to help discovery")] : [];
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "SK011",
|
|
132
|
+
severity: "warn",
|
|
133
|
+
description: "license should be declared in frontmatter",
|
|
134
|
+
check(s, _ctx) {
|
|
135
|
+
return s.license ? [] : [diag(this, "no 'license' declared in frontmatter")];
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "SK020",
|
|
140
|
+
severity: "warn",
|
|
141
|
+
description: "body should be \u2265 200 chars (not a stub)",
|
|
142
|
+
check(s, _ctx) {
|
|
143
|
+
return s.body.trim().length < 200 ? [diag(this, "body is very short (< 200 chars); this looks like a stub")] : [];
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "SK021",
|
|
148
|
+
severity: "warn",
|
|
149
|
+
description: "body should contain at least one Markdown heading",
|
|
150
|
+
check(s, _ctx) {
|
|
151
|
+
return /^#{1,6}\s/m.test(s.body) ? [] : [diag(this, "body has no Markdown headings; add structure with '#' headings")];
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "SK030",
|
|
156
|
+
severity: "error",
|
|
157
|
+
description: "total SKILL.md size must be \u2264 256KB",
|
|
158
|
+
check(s, _ctx) {
|
|
159
|
+
return utf8Bytes(s.body) > MAX_SKILL_BYTES ? [diag(this, "SKILL.md exceeds 256KB")] : [];
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "SK040",
|
|
164
|
+
severity: "warn",
|
|
165
|
+
description: "name should match its directory/slug",
|
|
166
|
+
check(s, ctx) {
|
|
167
|
+
if (!ctx.slug) return [];
|
|
168
|
+
const expected = slugify(s.name);
|
|
169
|
+
return expected === ctx.slug ? [] : [diag(this, `name slug '${expected}' does not match directory '${ctx.slug}'`)];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
];
|
|
173
|
+
function runRules(skill, ctx = {}) {
|
|
174
|
+
return RULES2.flatMap((r) => r.check(skill, ctx));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ../../packages/core/src/quality.ts
|
|
178
|
+
var WEIGHT = {
|
|
179
|
+
error: 30,
|
|
180
|
+
warn: 10,
|
|
181
|
+
info: 3
|
|
182
|
+
};
|
|
183
|
+
var RISK = {
|
|
184
|
+
docs_only: 0,
|
|
185
|
+
reads_secrets: 2,
|
|
186
|
+
network_calls: 1,
|
|
187
|
+
executes_scripts: 5,
|
|
188
|
+
// fetch-and-execute from a non-standard source — the supply-chain pattern the
|
|
189
|
+
// AI verdict floors to "warning"; the heaviest penalty here for the same reason
|
|
190
|
+
untrusted_install: 15
|
|
191
|
+
};
|
|
192
|
+
var VERIFIED_FLOOR = 85;
|
|
193
|
+
function qualityScore(diagnostics, security, opts = {}) {
|
|
194
|
+
let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
195
|
+
if (security) {
|
|
196
|
+
for (const flag of security.flags) penalty += RISK[flag] ?? 0;
|
|
197
|
+
}
|
|
198
|
+
const score = Math.max(0, Math.min(100, 100 - penalty));
|
|
199
|
+
const hasError = diagnostics.some((d) => d.severity === "error");
|
|
200
|
+
return opts.verified && !hasError ? Math.max(score, VERIFIED_FLOOR) : score;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ../../packages/core/src/lint.ts
|
|
204
|
+
function lint(raw, opts = {}) {
|
|
205
|
+
const parsed = parseSkillMd(raw, opts.type ?? "single");
|
|
206
|
+
if ("error" in parsed) {
|
|
207
|
+
const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
|
|
208
|
+
const security2 = scanSecurity(raw);
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
diagnostics: diagnostics2,
|
|
212
|
+
security: security2,
|
|
213
|
+
score: qualityScore(diagnostics2, security2, { verified: opts.verified })
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
217
|
+
const diagnostics = runRules(parsed, ctx);
|
|
218
|
+
const security = scanSecurity(parsed.body);
|
|
219
|
+
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
220
|
+
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security, { verified: opts.verified }) };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ../../packages/core/src/format/text.ts
|
|
224
|
+
var BADGE = { error: "ERROR", warn: " WARN", info: " INFO" };
|
|
225
|
+
function toText(results, opts = {}) {
|
|
226
|
+
const paint = opts.decorate ?? ((t) => t);
|
|
227
|
+
const out = [];
|
|
228
|
+
for (const { file, result } of results) {
|
|
229
|
+
const status = result.ok ? paint("PASS", "ok") : paint("FAIL", "error");
|
|
230
|
+
out.push(`${status} ${file} ${paint(`score ${result.score}/100`, "dim")}`);
|
|
231
|
+
for (const d of result.diagnostics) {
|
|
232
|
+
const where = d.line !== void 0 ? `:${d.line}` : "";
|
|
233
|
+
out.push(` ${paint(BADGE[d.severity], d.severity)} ${d.id}${where} ${d.message}`);
|
|
234
|
+
}
|
|
235
|
+
if (result.security.flags.length && result.security.flags[0] !== "docs_only") {
|
|
236
|
+
out.push(` ${paint("SEC ", "warn")} flags: ${result.security.flags.join(", ")}`);
|
|
237
|
+
}
|
|
238
|
+
out.push("");
|
|
239
|
+
}
|
|
240
|
+
const failed = results.filter((r) => !r.result.ok).length;
|
|
241
|
+
const summary = failed === 0 ? paint(`\u2713 ${results.length} skill(s) passed`, "ok") : paint(`\u2717 ${failed}/${results.length} skill(s) failed`, "error");
|
|
242
|
+
out.push(summary);
|
|
243
|
+
return out.join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ../../packages/core/src/format/json.ts
|
|
247
|
+
function toJson(results) {
|
|
248
|
+
return results.map(({ file, result }) => ({
|
|
249
|
+
file,
|
|
250
|
+
ok: result.ok,
|
|
251
|
+
score: result.score,
|
|
252
|
+
diagnostics: result.diagnostics,
|
|
253
|
+
security: result.security
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ../../packages/core/src/format/sarif.ts
|
|
258
|
+
var SARIF_LEVEL = { error: "error", warn: "warning", info: "note" };
|
|
259
|
+
function toSarif(results) {
|
|
260
|
+
const sarifResults = results.flatMap(
|
|
261
|
+
({ file, result }) => result.diagnostics.map((d) => ({
|
|
262
|
+
ruleId: d.id,
|
|
263
|
+
level: SARIF_LEVEL[d.severity] ?? "note",
|
|
264
|
+
message: { text: d.message },
|
|
265
|
+
locations: [
|
|
266
|
+
{
|
|
267
|
+
physicalLocation: {
|
|
268
|
+
artifactLocation: { uri: file },
|
|
269
|
+
region: { startLine: d.line ?? 1 }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
}))
|
|
274
|
+
);
|
|
275
|
+
return {
|
|
276
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
277
|
+
version: "2.1.0",
|
|
278
|
+
runs: [
|
|
279
|
+
{
|
|
280
|
+
tool: {
|
|
281
|
+
driver: {
|
|
282
|
+
name: "skillmd",
|
|
283
|
+
informationUri: "https://skillmd.com",
|
|
284
|
+
rules: RULES2.map((r) => ({
|
|
285
|
+
id: r.id,
|
|
286
|
+
shortDescription: { text: r.description },
|
|
287
|
+
defaultConfiguration: { level: SARIF_LEVEL[r.severity] ?? "note" }
|
|
288
|
+
}))
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
results: sarifResults
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ../../packages/core/src/format/github.ts
|
|
298
|
+
var LEVEL = { error: "error", warn: "warning", info: "notice" };
|
|
299
|
+
function toGithub(results) {
|
|
300
|
+
const lines = [];
|
|
301
|
+
for (const { file, result } of results) {
|
|
302
|
+
for (const d of result.diagnostics) {
|
|
303
|
+
const level = LEVEL[d.severity] ?? "notice";
|
|
304
|
+
const loc = d.line !== void 0 ? `,line=${d.line}` : "";
|
|
305
|
+
lines.push(`::${level} file=${file}${loc},title=${d.id}::${d.message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return lines.join("\n");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ../cli/src/source.ts
|
|
312
|
+
import { existsSync, statSync, lstatSync, readFileSync, readdirSync, mkdtempSync, rmSync } from "fs";
|
|
313
|
+
import { tmpdir } from "os";
|
|
314
|
+
import { join, basename, dirname, relative, sep } from "path";
|
|
315
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
316
|
+
// build/dependency dirs
|
|
317
|
+
"node_modules",
|
|
318
|
+
".git",
|
|
319
|
+
"dist",
|
|
320
|
+
"build",
|
|
321
|
+
"out",
|
|
322
|
+
".next",
|
|
323
|
+
".wrangler",
|
|
324
|
+
".cache",
|
|
325
|
+
".npm",
|
|
326
|
+
".pnpm-store",
|
|
327
|
+
".venv",
|
|
328
|
+
"venv",
|
|
329
|
+
"__pycache__",
|
|
330
|
+
"target",
|
|
331
|
+
"vendor",
|
|
332
|
+
// large OS / user dirs (mostly relevant when run from a home directory)
|
|
333
|
+
"AppData",
|
|
334
|
+
"Library",
|
|
335
|
+
"$Recycle.Bin",
|
|
336
|
+
"System Volume Information",
|
|
337
|
+
"Windows",
|
|
338
|
+
"Program Files"
|
|
339
|
+
]);
|
|
340
|
+
var MAX_DEPTH = 12;
|
|
341
|
+
function findSkillFiles(dir, depth = 0) {
|
|
342
|
+
const out = [];
|
|
343
|
+
if (depth > MAX_DEPTH) return out;
|
|
344
|
+
let entries;
|
|
345
|
+
try {
|
|
346
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
347
|
+
} catch {
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
if (entry.isDirectory()) {
|
|
352
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
353
|
+
out.push(...findSkillFiles(join(dir, entry.name), depth + 1));
|
|
354
|
+
} else if (entry.name === "SKILL.md") {
|
|
355
|
+
out.push(join(dir, entry.name));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
function loadLocal(file) {
|
|
361
|
+
return { path: file, raw: readFileSync(file, "utf8"), slug: basename(dirname(file)), file };
|
|
362
|
+
}
|
|
363
|
+
function isLikelyLocal(arg) {
|
|
364
|
+
return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync(arg);
|
|
365
|
+
}
|
|
366
|
+
async function resolveSource(arg) {
|
|
367
|
+
if (isLikelyLocal(arg)) {
|
|
368
|
+
if (!existsSync(arg)) throw new Error(`Path not found: ${arg}`);
|
|
369
|
+
const st = statSync(arg);
|
|
370
|
+
if (st.isFile()) return [loadLocal(arg)];
|
|
371
|
+
const files2 = findSkillFiles(arg);
|
|
372
|
+
if (files2.length === 0) {
|
|
373
|
+
throw new Error(`No SKILL.md found under ${arg}. Point at a skill directory, e.g. "skillmd lint ./my-skill", or run "skillmd init" to create one.`);
|
|
374
|
+
}
|
|
375
|
+
return files2.map(loadLocal);
|
|
376
|
+
}
|
|
377
|
+
const { downloadTemplate } = await import("giget");
|
|
378
|
+
const input = normalizeGiget(arg);
|
|
379
|
+
const dir = mkdtempSync(join(tmpdir(), "skillmd-src-"));
|
|
380
|
+
const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
|
|
381
|
+
const files = findSkillFiles(out);
|
|
382
|
+
if (files.length === 0) throw new Error(`No SKILL.md found in ${arg}`);
|
|
383
|
+
return files.map(loadLocal);
|
|
384
|
+
}
|
|
385
|
+
var MAX_PACK_FILES = 200;
|
|
386
|
+
var MAX_PACK_BYTES = 20 * 1024 * 1024;
|
|
387
|
+
function collectFiles(root, dir, limits = {}) {
|
|
388
|
+
const maxFiles = limits.maxFiles ?? MAX_PACK_FILES;
|
|
389
|
+
const maxBytes = limits.maxBytes ?? MAX_PACK_BYTES;
|
|
390
|
+
const out = [];
|
|
391
|
+
let bytes = 0;
|
|
392
|
+
const walk = (d, depth) => {
|
|
393
|
+
if (depth > MAX_DEPTH) return;
|
|
394
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
395
|
+
const fp = join(d, entry.name);
|
|
396
|
+
const st = lstatSync(fp);
|
|
397
|
+
if (st.isSymbolicLink()) continue;
|
|
398
|
+
if (st.isDirectory()) {
|
|
399
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
400
|
+
walk(fp, depth + 1);
|
|
401
|
+
} else if (st.isFile()) {
|
|
402
|
+
if (out.length >= maxFiles) throw new Error(`pack too large: > ${maxFiles} files`);
|
|
403
|
+
bytes += st.size;
|
|
404
|
+
if (bytes > maxBytes) throw new Error(`pack too large: > ${Math.round(maxBytes / (1024 * 1024))}MB`);
|
|
405
|
+
out.push({ path: relative(root, fp).split(sep).join("/"), contents: readFileSync(fp) });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
walk(dir, 0);
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
async function resolveTree(sourceUrl, ref) {
|
|
413
|
+
const { downloadTemplate } = await import("giget");
|
|
414
|
+
const input = normalizeGiget(sourceUrl) + (ref ? `#${ref}` : "");
|
|
415
|
+
const dir = mkdtempSync(join(tmpdir(), "skillmd-pack-"));
|
|
416
|
+
try {
|
|
417
|
+
const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
|
|
418
|
+
return collectFiles(out, out);
|
|
419
|
+
} finally {
|
|
420
|
+
rmSync(dir, { recursive: true, force: true });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function normalizeGiget(arg) {
|
|
424
|
+
const m = arg.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/[^/]+\/(.+))?/);
|
|
425
|
+
if (m) return `github:${m[1]}/${m[2]}${m[3] ? "/" + m[3] : ""}`;
|
|
426
|
+
if (arg.startsWith("github:") || arg.startsWith("gh:")) return arg;
|
|
427
|
+
return `github:${arg}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ../cli/src/ui.ts
|
|
431
|
+
import pc from "picocolors";
|
|
432
|
+
var decorate = (text3, kind) => {
|
|
433
|
+
switch (kind) {
|
|
434
|
+
case "error":
|
|
435
|
+
return pc.red(text3);
|
|
436
|
+
case "warn":
|
|
437
|
+
return pc.yellow(text3);
|
|
438
|
+
case "info":
|
|
439
|
+
return pc.cyan(text3);
|
|
440
|
+
case "ok":
|
|
441
|
+
return pc.green(text3);
|
|
442
|
+
case "dim":
|
|
443
|
+
return pc.dim(text3);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var SEV_COLOR = {
|
|
447
|
+
error: pc.red,
|
|
448
|
+
warn: pc.yellow,
|
|
449
|
+
info: pc.cyan
|
|
450
|
+
};
|
|
451
|
+
var GLYPH = {
|
|
452
|
+
error: "\u2716",
|
|
453
|
+
warn: "\u26A0",
|
|
454
|
+
info: "\u2139"
|
|
455
|
+
};
|
|
456
|
+
var SEC_GLYPH = "\u2691";
|
|
457
|
+
var SEV_ORDER = ["error", "warn", "info"];
|
|
458
|
+
function chips(result) {
|
|
459
|
+
const bySev = { error: [], warn: [], info: [] };
|
|
460
|
+
for (const d of result.diagnostics) bySev[d.severity].push(d.id);
|
|
461
|
+
return SEV_ORDER.filter((s) => bySev[s].length).map((s) => `${GLYPH[s]} ${SEV_COLOR[s](bySev[s].join(" "))}`).join(" ");
|
|
462
|
+
}
|
|
463
|
+
var SEC_LABEL = {
|
|
464
|
+
network_calls: "Calls Network",
|
|
465
|
+
executes_scripts: "Executes Scripts",
|
|
466
|
+
reads_secrets: "Reads Secrets"
|
|
467
|
+
};
|
|
468
|
+
var titleCase = (flag) => flag.split("_").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
469
|
+
var scoreColor = (s) => s >= 80 ? pc.green : s >= 50 ? pc.yellow : pc.red;
|
|
470
|
+
function label(file) {
|
|
471
|
+
const parts = file.split(/[\\/]/).filter(Boolean);
|
|
472
|
+
const i = parts.lastIndexOf("SKILL.md");
|
|
473
|
+
return i > 0 ? parts[i - 1] : parts[parts.length - 1] || file;
|
|
474
|
+
}
|
|
475
|
+
function securityFlags(result) {
|
|
476
|
+
return result.security.flags.filter((f) => f !== "docs_only");
|
|
477
|
+
}
|
|
478
|
+
function secLabels(result) {
|
|
479
|
+
const flags = securityFlags(result);
|
|
480
|
+
return flags.map((f) => pc.magenta(`${SEC_GLYPH} ${SEC_LABEL[f] ?? titleCase(f)}`)).join(" ");
|
|
481
|
+
}
|
|
482
|
+
function statusDot(result) {
|
|
483
|
+
if (!result.ok) return pc.red("\u25CF");
|
|
484
|
+
const attention = result.diagnostics.some((d) => d.severity === "warn") || securityFlags(result).length > 0;
|
|
485
|
+
return attention ? pc.yellow("\u25CF") : pc.green("\u25CF");
|
|
486
|
+
}
|
|
487
|
+
function failureBlock(file, result) {
|
|
488
|
+
const lines = [`${pc.red("\u2716")} ${pc.bold(label(file))}`];
|
|
489
|
+
for (const d of result.diagnostics.filter((x) => x.severity === "error")) {
|
|
490
|
+
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
491
|
+
lines.push(` ${SEV_COLOR.error(d.id)}${loc} ${d.message}`);
|
|
492
|
+
}
|
|
493
|
+
lines.push(` ${pc.dim(file)}`);
|
|
494
|
+
return lines.join("\n");
|
|
495
|
+
}
|
|
496
|
+
function renderOne(file, result) {
|
|
497
|
+
const status = result.ok ? pc.green("\u25CF PASS") : pc.red("\u25CF FAIL");
|
|
498
|
+
const out = [`${status} ${pc.bold(label(file))} ${pc.dim("quality")} ${scoreColor(result.score)(`${result.score}/100`)}`];
|
|
499
|
+
for (const d of result.diagnostics) {
|
|
500
|
+
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
501
|
+
out.push(` ${GLYPH[d.severity]} ${SEV_COLOR[d.severity](d.id)}${loc} ${d.message}`);
|
|
502
|
+
}
|
|
503
|
+
const sec = secLabels(result);
|
|
504
|
+
if (sec) out.push(` ${sec}`);
|
|
505
|
+
return out.join("\n");
|
|
506
|
+
}
|
|
507
|
+
function renderMany(results) {
|
|
508
|
+
const names = results.map((r) => label(r.file));
|
|
509
|
+
const w = Math.min(32, Math.max(4, ...names.map((n) => n.length)));
|
|
510
|
+
const out = [];
|
|
511
|
+
let anySec = false;
|
|
512
|
+
results.forEach(({ result }, i) => {
|
|
513
|
+
const score = scoreColor(result.score)(String(result.score).padStart(3));
|
|
514
|
+
const nm = names[i];
|
|
515
|
+
const name = (nm.length > w ? nm.slice(0, w - 1) + "\u2026" : nm).padEnd(w);
|
|
516
|
+
const secStr = secLabels(result);
|
|
517
|
+
if (secStr) anySec = true;
|
|
518
|
+
const sec = secStr ? ` ${secStr}` : "";
|
|
519
|
+
out.push(`${statusDot(result)} ${score} ${name} ${chips(result)}${sec}`.trimEnd());
|
|
520
|
+
});
|
|
521
|
+
const failed = results.filter((r) => !r.result.ok).length;
|
|
522
|
+
const warned = results.filter((r) => r.result.ok && r.result.diagnostics.some((d) => d.severity === "warn")).length;
|
|
523
|
+
const clean = results.length - failed - warned;
|
|
524
|
+
const avg = Math.round(results.reduce((s, r) => s + r.result.score, 0) / results.length);
|
|
525
|
+
out.push("");
|
|
526
|
+
const parts = [pc.green(`${clean} clean`)];
|
|
527
|
+
if (warned) parts.push(pc.yellow(`${warned} warn`));
|
|
528
|
+
if (failed) parts.push(pc.red(`${failed} fail`));
|
|
529
|
+
out.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("avg score")} ${scoreColor(avg)(`${avg}/100`)}`);
|
|
530
|
+
if (anySec) out.push(pc.dim(`${SEC_GLYPH} = risky behaviour, factored into the score alongside spec correctness and completeness`));
|
|
531
|
+
const fails = results.filter((r) => !r.result.ok);
|
|
532
|
+
if (fails.length) {
|
|
533
|
+
out.push("");
|
|
534
|
+
out.push(pc.red(pc.bold(`Failed (${fails.length}):`)));
|
|
535
|
+
for (const f of fails.slice(0, 25)) out.push(failureBlock(f.file, f.result));
|
|
536
|
+
if (fails.length > 25) out.push(pc.dim(` \u2026and ${fails.length - 25} more \u2014 run with --errors-only to see all`));
|
|
537
|
+
}
|
|
538
|
+
return out.join("\n");
|
|
539
|
+
}
|
|
540
|
+
function renderFailuresView(results) {
|
|
541
|
+
const fails = results.filter((r) => !r.result.ok);
|
|
542
|
+
if (!fails.length) return pc.green(`\u2713 No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
|
|
543
|
+
const out = fails.map((f) => failureBlock(f.file, f.result));
|
|
544
|
+
out.push("");
|
|
545
|
+
out.push(`${pc.red(`${fails.length} failed`)} ${pc.dim(`of ${results.length}`)}`);
|
|
546
|
+
return out.join("\n");
|
|
547
|
+
}
|
|
548
|
+
function renderReport(results, opts = {}) {
|
|
549
|
+
if (results.length === 0) return pc.dim("No skills found.");
|
|
550
|
+
if (opts.errorsOnly) return renderFailuresView(results);
|
|
551
|
+
if (results.length === 1) return renderOne(results[0].file, results[0].result);
|
|
552
|
+
return renderMany(results);
|
|
553
|
+
}
|
|
554
|
+
function exitCodeFor(results, policy = {}) {
|
|
555
|
+
for (const { result } of results) {
|
|
556
|
+
const hasError = result.diagnostics.some((d) => d.severity === "error");
|
|
557
|
+
const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
|
|
558
|
+
if (hasError) return 1;
|
|
559
|
+
if ((policy.strict || policy.failOnWarning) && hasWarn) return 1;
|
|
560
|
+
}
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ../cli/src/commands/lint.ts
|
|
565
|
+
function applyLicenseFix(raw) {
|
|
566
|
+
if (/^---[\s\S]*?\blicense:\s*\S+[\s\S]*?^---/m.test(raw)) return null;
|
|
567
|
+
const m = raw.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
|
|
568
|
+
if (!m) return null;
|
|
569
|
+
return `${m[1]}${m[2]}
|
|
570
|
+
license: UNKNOWN${m[3]}${raw.slice(m[0].length)}`;
|
|
571
|
+
}
|
|
572
|
+
async function runLint(target, flags) {
|
|
573
|
+
let resolved = await resolveSource(target);
|
|
574
|
+
if (flags.fix) {
|
|
575
|
+
resolved = resolved.map((r) => {
|
|
576
|
+
if (!r.file) return r;
|
|
577
|
+
const fixed = applyLicenseFix(r.raw);
|
|
578
|
+
if (fixed) {
|
|
579
|
+
writeFileSync(r.file, fixed, "utf8");
|
|
580
|
+
return { ...r, raw: fixed };
|
|
581
|
+
}
|
|
582
|
+
return r;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
const results = resolved.map((r) => ({ file: r.path, result: lint(r.raw, { slug: r.slug }), resolved: r }));
|
|
586
|
+
const exitCode = exitCodeFor(results, flags);
|
|
587
|
+
let output;
|
|
588
|
+
switch (flags.format) {
|
|
589
|
+
case "json":
|
|
590
|
+
output = JSON.stringify(toJson(results), null, 2);
|
|
591
|
+
break;
|
|
592
|
+
case "sarif":
|
|
593
|
+
output = JSON.stringify(toSarif(results), null, 2);
|
|
594
|
+
break;
|
|
595
|
+
case "github":
|
|
596
|
+
output = toGithub(results);
|
|
597
|
+
break;
|
|
598
|
+
default:
|
|
599
|
+
output = renderReport(results, { errorsOnly: flags.errorsOnly });
|
|
600
|
+
}
|
|
601
|
+
return { results, output, exitCode };
|
|
602
|
+
}
|
|
603
|
+
function lintCommand() {
|
|
604
|
+
return new Command("lint").alias("check").description("Validate SKILL.md file(s) against the spec and report a quality score").argument("[target]", "local path or GitHub source", ".").option("--format <fmt>", "text | json | sarif | github", "text").option("--strict", "treat warnings as errors").option("--fail-on-warning", "exit non-zero when warnings exist").option("--fix", "apply safe automatic fixes (e.g. add missing license)").option("--errors-only", "show only skills that failed").action(async (target, opts) => {
|
|
605
|
+
const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
|
|
606
|
+
if (resolve(target) === homedir()) {
|
|
607
|
+
process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
|
|
608
|
+
`));
|
|
609
|
+
}
|
|
610
|
+
if (ui) {
|
|
611
|
+
p.intro(pc2.bgCyan(pc2.black(" skillmd lint ")));
|
|
612
|
+
const s = p.spinner();
|
|
613
|
+
s.start(`Scanning ${target}`);
|
|
614
|
+
try {
|
|
615
|
+
const run2 = await runLint(target, opts);
|
|
616
|
+
s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
|
|
617
|
+
console.log(run2.output);
|
|
618
|
+
const failed = run2.results.filter((r) => !r.result.ok).length;
|
|
619
|
+
p.outro(failed === 0 ? pc2.green("All good") : pc2.red(`${failed} skill${failed === 1 ? "" : "s"} failed \u2014 details above`));
|
|
620
|
+
process.exitCode = run2.exitCode;
|
|
621
|
+
} catch (err) {
|
|
622
|
+
s.stop(pc2.red("No skills found"));
|
|
623
|
+
p.outro(err instanceof Error ? err.message : String(err));
|
|
624
|
+
process.exitCode = 1;
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
process.stderr.write(`Looking for SKILL.md under ${target} \u2026
|
|
629
|
+
`);
|
|
630
|
+
const run = await runLint(target, opts);
|
|
631
|
+
console.log(run.output);
|
|
632
|
+
process.exitCode = run.exitCode;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ../cli/src/commands/scan.ts
|
|
637
|
+
import { Command as Command2 } from "commander";
|
|
638
|
+
import pc3 from "picocolors";
|
|
639
|
+
async function runScan(target, flags) {
|
|
640
|
+
const resolved = await resolveSource(target);
|
|
641
|
+
const deny = new Set(flags.deny ?? []);
|
|
642
|
+
const entries = resolved.map((r) => {
|
|
643
|
+
const parsed = parseSkillMd(r.raw, "single");
|
|
644
|
+
const body = "error" in parsed ? r.raw : parsed.body;
|
|
645
|
+
const security = scanSecurity(body);
|
|
646
|
+
const denied = security.flags.filter((f) => deny.has(f));
|
|
647
|
+
return { file: r.path, security, denied };
|
|
648
|
+
});
|
|
649
|
+
const exitCode = entries.some((e) => e.denied.length > 0) ? 1 : 0;
|
|
650
|
+
let output;
|
|
651
|
+
if (flags.format === "json") {
|
|
652
|
+
output = JSON.stringify(entries, null, 2);
|
|
653
|
+
} else {
|
|
654
|
+
const lines = [];
|
|
655
|
+
for (const e of entries) {
|
|
656
|
+
const flagStr = e.security.flags.join(", ");
|
|
657
|
+
const head = e.denied.length ? pc3.red(`DENY ${e.file}`) : pc3.green(`OK ${e.file}`);
|
|
658
|
+
lines.push(`${head} ${pc3.dim(flagStr)}`);
|
|
659
|
+
for (const f of e.security.findings) {
|
|
660
|
+
const mark = deny.has(f.flag) ? pc3.red(f.flag) : pc3.yellow(f.flag);
|
|
661
|
+
lines.push(` ${mark} :${f.line} ${pc3.dim(f.snippet)}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
output = lines.join("\n");
|
|
665
|
+
}
|
|
666
|
+
return { entries, exitCode, output };
|
|
667
|
+
}
|
|
668
|
+
function scanCommand() {
|
|
669
|
+
return new Command2("scan").description("Security-scan SKILL.md file(s) for scripts, network calls and secret access").argument("[target]", "local path or GitHub source", ".").option("--deny <flags...>", "exit non-zero if any of these flags are present (e.g. executes_scripts)").option("--format <fmt>", "text | json", "text").action(async (target, opts) => {
|
|
670
|
+
const run = await runScan(target, opts);
|
|
671
|
+
console.log(run.output);
|
|
672
|
+
process.exitCode = run.exitCode;
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ../cli/src/commands/rules.ts
|
|
677
|
+
import { Command as Command3 } from "commander";
|
|
678
|
+
import pc4 from "picocolors";
|
|
679
|
+
function rulesCommand() {
|
|
680
|
+
return new Command3("rules").description("List lint rules, or show detail for one rule id").argument("[id]", "a rule id, e.g. SK010").option("--json", "machine-readable output").action((id, opts) => {
|
|
681
|
+
const list = id ? RULES2.filter((r) => r.id.toLowerCase() === id.toLowerCase()) : RULES2;
|
|
682
|
+
if (id && list.length === 0) {
|
|
683
|
+
console.error(`Unknown rule: ${id}`);
|
|
684
|
+
process.exitCode = 1;
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (opts.json) {
|
|
688
|
+
console.log(JSON.stringify(list.map((r) => ({ id: r.id, severity: r.severity, description: r.description })), null, 2));
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const badge = (sev) => sev === "error" ? pc4.bgRed(pc4.white(" error ")) : sev === "warn" ? pc4.bgYellow(pc4.black(" warn ")) : pc4.bgCyan(pc4.black(" info "));
|
|
692
|
+
console.log(pc4.bold("\nLint rules\n"));
|
|
693
|
+
for (const r of list) console.log(` ${pc4.bold(r.id)} ${badge(r.severity)} ${r.description}`);
|
|
694
|
+
console.log("");
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ../cli/src/commands/init.ts
|
|
699
|
+
import { Command as Command4 } from "commander";
|
|
700
|
+
import { mkdirSync, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
701
|
+
import { join as join2 } from "path";
|
|
702
|
+
import * as p2 from "@clack/prompts";
|
|
703
|
+
function skillTemplate(name, description, license) {
|
|
704
|
+
const desc = description.trim() || `A skill that helps agents with ${name}.`;
|
|
705
|
+
return `---
|
|
706
|
+
name: ${name}
|
|
707
|
+
description: ${desc}
|
|
708
|
+
license: ${license}
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
# ${name}
|
|
712
|
+
|
|
713
|
+
## When to use this skill
|
|
714
|
+
|
|
715
|
+
Describe the situations where an agent should reach for this skill. Be specific
|
|
716
|
+
about the task, the inputs it expects, and the outcome it produces.
|
|
717
|
+
|
|
718
|
+
## Instructions
|
|
719
|
+
|
|
720
|
+
1. First step the agent should take.
|
|
721
|
+
2. Second step, with any commands or file paths it needs.
|
|
722
|
+
3. How to verify the result.
|
|
723
|
+
|
|
724
|
+
## Notes
|
|
725
|
+
|
|
726
|
+
Add any constraints, edge cases, or references the agent should keep in mind.
|
|
727
|
+
`;
|
|
728
|
+
}
|
|
729
|
+
function initSkill(name, opts = {}) {
|
|
730
|
+
const slug = slugify(name);
|
|
731
|
+
const root = join2(opts.dir ?? process.cwd(), slug);
|
|
732
|
+
if (existsSync2(join2(root, "SKILL.md"))) throw new Error(`SKILL.md already exists at ${root}`);
|
|
733
|
+
mkdirSync(root, { recursive: true });
|
|
734
|
+
const file = join2(root, "SKILL.md");
|
|
735
|
+
writeFileSync2(file, skillTemplate(name, opts.description ?? "", opts.license ?? "MIT"), "utf8");
|
|
736
|
+
return file;
|
|
737
|
+
}
|
|
738
|
+
function initCommand() {
|
|
739
|
+
return new Command4("init").description("Scaffold a new SKILL.md from a template").argument("[name]", "skill name").option("--name <name>", "skill name (non-interactive)").option("--description <text>", "skill description").option("--license <license>", "license identifier", "MIT").option("-y, --yes", "skip prompts").action(async (nameArg, opts) => {
|
|
740
|
+
let name = nameArg ?? opts.name;
|
|
741
|
+
if (!name && !opts.yes) {
|
|
742
|
+
const answer = await p2.text({ message: "Skill name?", placeholder: "my-skill" });
|
|
743
|
+
if (p2.isCancel(answer)) {
|
|
744
|
+
p2.cancel("Cancelled.");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
name = answer;
|
|
748
|
+
}
|
|
749
|
+
if (!name) {
|
|
750
|
+
console.error("A skill name is required (pass a name or --name).");
|
|
751
|
+
process.exitCode = 1;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const file = initSkill(name, { description: opts.description, license: opts.license });
|
|
755
|
+
console.log(`Created ${file}`);
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ../cli/src/commands/search.ts
|
|
760
|
+
import { Command as Command5 } from "commander";
|
|
761
|
+
import pc5 from "picocolors";
|
|
762
|
+
|
|
763
|
+
// ../cli/src/api.ts
|
|
764
|
+
import { createHash } from "crypto";
|
|
765
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
766
|
+
|
|
767
|
+
// ../cli/src/config.ts
|
|
768
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
769
|
+
import { homedir as homedir2 } from "os";
|
|
770
|
+
import { join as join3 } from "path";
|
|
771
|
+
var DEFAULT_API = "https://api.skillmd.com";
|
|
772
|
+
function configPath() {
|
|
773
|
+
return join3(homedir2(), ".skillmd", "config.json");
|
|
774
|
+
}
|
|
775
|
+
function readConfig() {
|
|
776
|
+
try {
|
|
777
|
+
return JSON.parse(readFileSync2(configPath(), "utf8"));
|
|
778
|
+
} catch {
|
|
779
|
+
return {};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function writeConfig(cfg) {
|
|
783
|
+
const dir = join3(homedir2(), ".skillmd");
|
|
784
|
+
mkdirSync2(dir, { recursive: true });
|
|
785
|
+
writeFileSync3(configPath(), JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
|
|
786
|
+
}
|
|
787
|
+
function resolveToken(flags = {}) {
|
|
788
|
+
return flags.token || process.env.SKILLMD_TOKEN || readConfig().token || void 0;
|
|
789
|
+
}
|
|
790
|
+
function resolveApi(flags = {}) {
|
|
791
|
+
return flags.api || process.env.SKILLMD_API || readConfig().api || DEFAULT_API;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ../cli/src/api.ts
|
|
795
|
+
var IntegrityError = class extends Error {
|
|
796
|
+
integrity = true;
|
|
797
|
+
constructor(message) {
|
|
798
|
+
super(message);
|
|
799
|
+
this.name = "IntegrityError";
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
var RegistryError = class extends Error {
|
|
803
|
+
constructor(message, status) {
|
|
804
|
+
super(message);
|
|
805
|
+
this.status = status;
|
|
806
|
+
this.name = "RegistryError";
|
|
807
|
+
}
|
|
808
|
+
status;
|
|
809
|
+
};
|
|
810
|
+
var ALLOWED_SOURCE_HOSTS = /* @__PURE__ */ new Set(["raw.githubusercontent.com", "gist.githubusercontent.com"]);
|
|
811
|
+
function isAllowedSourceUrl(u) {
|
|
812
|
+
try {
|
|
813
|
+
const parsed = new URL(u);
|
|
814
|
+
return parsed.protocol === "https:" && ALLOWED_SOURCE_HOSTS.has(parsed.hostname.toLowerCase());
|
|
815
|
+
} catch {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function createClient(flags = {}) {
|
|
820
|
+
const base = resolveApi(flags);
|
|
821
|
+
const token = resolveToken(flags);
|
|
822
|
+
async function api(path, init = {}) {
|
|
823
|
+
const headers = { accept: "application/json", ...init.headers };
|
|
824
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
825
|
+
let res;
|
|
826
|
+
try {
|
|
827
|
+
res = await fetch(base + path, { ...init, headers });
|
|
828
|
+
} catch (e) {
|
|
829
|
+
const cause = e instanceof Error ? e.cause?.message ?? e.message : String(e);
|
|
830
|
+
throw new RegistryError(`could not reach ${base}: ${cause}`);
|
|
831
|
+
}
|
|
832
|
+
if (!res.ok) {
|
|
833
|
+
const detail = await res.text().catch(() => "");
|
|
834
|
+
throw new RegistryError(`SkillMD API ${res.status} on ${path}: ${detail}`, res.status);
|
|
835
|
+
}
|
|
836
|
+
return await res.json();
|
|
837
|
+
}
|
|
838
|
+
return { api, base, token, hasToken: Boolean(token) };
|
|
839
|
+
}
|
|
840
|
+
async function fetchBundle(base, slug, token) {
|
|
841
|
+
const headers = { accept: "application/json" };
|
|
842
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
843
|
+
const res = await fetch(`${base}/api/skills/${slug}/bundle?format=json`, { headers }).catch(() => null);
|
|
844
|
+
if (!res || !res.ok) return null;
|
|
845
|
+
const data = await res.json().catch(() => null);
|
|
846
|
+
if (!data?.files) return null;
|
|
847
|
+
const out = [];
|
|
848
|
+
for (const f of data.files) {
|
|
849
|
+
if (f.content_base64 != null) {
|
|
850
|
+
const contents = Buffer.from(f.content_base64, "base64");
|
|
851
|
+
if (f.sha256) {
|
|
852
|
+
const got = createHash("sha256").update(contents).digest("hex");
|
|
853
|
+
if (got !== f.sha256) {
|
|
854
|
+
throw new IntegrityError(`integrity check failed for "${f.path}" (expected ${f.sha256.slice(0, 12)}\u2026, got ${got.slice(0, 12)}\u2026)`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
out.push({ path: f.path, contents });
|
|
858
|
+
} else if (f.source_url && isAllowedSourceUrl(f.source_url)) {
|
|
859
|
+
const r = await fetch(f.source_url).catch(() => null);
|
|
860
|
+
if (r && r.ok) out.push({ path: f.path, contents: Buffer.from(await r.arrayBuffer()) });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return out;
|
|
864
|
+
}
|
|
865
|
+
function skillMdFor(skill) {
|
|
866
|
+
if (skill.raw_md) {
|
|
867
|
+
return hasFrontmatter(skill.raw_md) ? skill.raw_md : reconstructSkillMd({ ...skill, body_md: skill.raw_md });
|
|
868
|
+
}
|
|
869
|
+
return reconstructSkillMd(skill);
|
|
870
|
+
}
|
|
871
|
+
function hasFrontmatter(md) {
|
|
872
|
+
return /^\uFEFF?\s*---\r?\n/.test(md);
|
|
873
|
+
}
|
|
874
|
+
function reconstructSkillMd(skill) {
|
|
875
|
+
const name = skill.slug.split("/")[1] || skill.title;
|
|
876
|
+
const fm = { name, description: String(skill.description).replace(/\n/g, " ") };
|
|
877
|
+
if (skill.license) fm.license = skill.license;
|
|
878
|
+
return `---
|
|
879
|
+
${stringifyYaml(fm)}---
|
|
880
|
+
${skill.body_md || ""}`;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ../cli/src/commands/search.ts
|
|
884
|
+
var MAX_LINE_WIDTH = 100;
|
|
885
|
+
function formatStars(n) {
|
|
886
|
+
if (n >= 1e6) {
|
|
887
|
+
const m = Math.round(n / 1e6 * 10) / 10;
|
|
888
|
+
return `${m % 1 === 0 ? m.toFixed(0) : m.toFixed(1)}M`;
|
|
889
|
+
}
|
|
890
|
+
if (n >= 1e3) {
|
|
891
|
+
const k = Math.round(n / 1e3 * 10) / 10;
|
|
892
|
+
return `${k % 1 === 0 ? k.toFixed(0) : k.toFixed(1)}k`;
|
|
893
|
+
}
|
|
894
|
+
return String(n);
|
|
895
|
+
}
|
|
896
|
+
function truncate(text3, width) {
|
|
897
|
+
if (text3.length <= width) return text3;
|
|
898
|
+
if (width <= 1) return "\u2026";
|
|
899
|
+
return `${text3.slice(0, width - 1).trimEnd()}\u2026`;
|
|
900
|
+
}
|
|
901
|
+
function formatSearchResult(item, width = process.stdout.columns ?? 80) {
|
|
902
|
+
const stars = item.repo_stars != null ? pc5.dim(`\u2605 ${formatStars(item.repo_stars)}`) : "";
|
|
903
|
+
const check = item.verified ? pc5.green("\u2713") : "";
|
|
904
|
+
const pack = item.type === "pack" ? pc5.dim("[pack]") : "";
|
|
905
|
+
const head = [pc5.bold(item.slug), stars, check, pack].filter(Boolean).join(" ");
|
|
906
|
+
const indent = " ";
|
|
907
|
+
const lineWidth = Math.max(10, Math.min(width, MAX_LINE_WIDTH) - indent.length);
|
|
908
|
+
const desc = `${indent}${pc5.dim(truncate(item.description ?? "", lineWidth))}`;
|
|
909
|
+
return `${head}
|
|
910
|
+
${desc}`;
|
|
911
|
+
}
|
|
912
|
+
async function runSearch(query, flags) {
|
|
913
|
+
const { api } = createClient(flags);
|
|
914
|
+
const params = new URLSearchParams({ q: query, limit: String(flags.limit ?? 20) });
|
|
915
|
+
if (flags.category) params.set("category", flags.category);
|
|
916
|
+
if (flags.verified) params.set("verified", "true");
|
|
917
|
+
if (flags.type) params.set("type", flags.type);
|
|
918
|
+
if (flags.minRating != null) params.set("min_rating", String(flags.minRating));
|
|
919
|
+
const r = await api(`/api/search?${params}`);
|
|
920
|
+
const items = r.items ?? [];
|
|
921
|
+
if (flags.json) return { items, output: JSON.stringify(items, null, 2) };
|
|
922
|
+
const lines = items.map((s) => formatSearchResult(s));
|
|
923
|
+
return { items, output: lines.join("\n\n") || "No skills found." };
|
|
924
|
+
}
|
|
925
|
+
function searchCommand() {
|
|
926
|
+
return new Command5("search").description("Search the SkillMD registry").argument("<query>", "search terms").option("--category <slug>", "filter by category").option("--verified", "only verified skills").option("--type <type>", "single | pack").option("--min-rating <n>", "minimum average rating (0-5)").option("--limit <n>", "max results", "20").option("--json", "machine-readable output").action(async (query, opts, cmd) => {
|
|
927
|
+
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
928
|
+
const run = await runSearch(query, merged);
|
|
929
|
+
console.log(run.output);
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ../cli/src/commands/info.ts
|
|
934
|
+
import { Command as Command6 } from "commander";
|
|
935
|
+
import pc6 from "picocolors";
|
|
936
|
+
async function runInfo(slug, flags) {
|
|
937
|
+
const [owner, name] = slug.split("/");
|
|
938
|
+
if (!owner || !name || slug.split("/").length !== 2) {
|
|
939
|
+
return { ok: false, exitCode: 1, output: pc6.red("Expected owner/name, e.g. anthropic/pdf") };
|
|
940
|
+
}
|
|
941
|
+
const { api } = createClient(flags);
|
|
942
|
+
let s;
|
|
943
|
+
try {
|
|
944
|
+
s = await api(`/api/skills/${owner}/${name}`);
|
|
945
|
+
} catch (err) {
|
|
946
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
947
|
+
if (/^SkillMD API 404 /.test(message)) {
|
|
948
|
+
return { ok: false, exitCode: 1, output: pc6.red(`Skill not found on the registry: ${slug}`) };
|
|
949
|
+
}
|
|
950
|
+
return { ok: false, exitCode: 1, output: pc6.red(`Could not reach the SkillMD registry: ${message}`) };
|
|
951
|
+
}
|
|
952
|
+
if (flags.json) return { ok: true, exitCode: 0, output: JSON.stringify(s, null, 2), skill: s };
|
|
953
|
+
const lines = [];
|
|
954
|
+
lines.push(
|
|
955
|
+
`${pc6.bold(s.slug)} ${pc6.dim(`[${s.type ?? "single"}]`)} ${s.verified ? pc6.green("\u2713 verified") : pc6.yellow("unverified")}`
|
|
956
|
+
);
|
|
957
|
+
lines.push(` ${s.description}`);
|
|
958
|
+
lines.push(
|
|
959
|
+
pc6.dim(
|
|
960
|
+
` category: ${s.category ?? "-"} \xB7 rating: ${s.avg_rating != null ? s.avg_rating.toFixed(1) : "-"} (${s.rating_count ?? 0}) \xB7 installs: ${s.install_count ?? 0}`
|
|
961
|
+
)
|
|
962
|
+
);
|
|
963
|
+
lines.push(
|
|
964
|
+
pc6.dim(` security: ${(s.security_flags ?? []).join(", ") || "unscanned"} \xB7 license: ${s.provenance?.license ?? s.license ?? "unknown"}`)
|
|
965
|
+
);
|
|
966
|
+
const sourceRepo = s.provenance?.source_repo ?? s.source_repo;
|
|
967
|
+
const commitSha = s.provenance?.commit_sha ?? s.commit_sha;
|
|
968
|
+
if (sourceRepo) lines.push(pc6.dim(` source: ${sourceRepo}${commitSha ? ` @ ${commitSha.slice(0, 8)}` : ""}`));
|
|
969
|
+
lines.push(pc6.dim(` page: https://skillmd.com/skills/${s.slug}`));
|
|
970
|
+
lines.push(pc6.dim(` install: ${s.install?.cli ?? `npx skillmds add ${s.slug}`}`));
|
|
971
|
+
return { ok: true, exitCode: 0, output: lines.join("\n"), skill: s };
|
|
972
|
+
}
|
|
973
|
+
function infoCommand() {
|
|
974
|
+
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) => {
|
|
975
|
+
const flags = { ...cmd.parent?.opts() };
|
|
976
|
+
const run = await runInfo(slug, flags);
|
|
977
|
+
if (run.output) (run.ok ? console.log : console.error)(run.output);
|
|
978
|
+
process.exitCode = run.exitCode;
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ../cli/src/commands/add.ts
|
|
983
|
+
import { Command as Command7, Option } from "commander";
|
|
984
|
+
import { existsSync as existsSync4 } from "fs";
|
|
985
|
+
import { homedir as homedir4 } from "os";
|
|
986
|
+
import * as p3 from "@clack/prompts";
|
|
987
|
+
import { join as join5, relative as relative2, resolve as resolve3 } from "path";
|
|
988
|
+
import pc7 from "picocolors";
|
|
989
|
+
|
|
990
|
+
// ../cli/src/agents.ts
|
|
991
|
+
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";
|
|
992
|
+
import { homedir as homedir3 } from "os";
|
|
993
|
+
import { join as join4, resolve as resolve2, dirname as dirname2, sep as sep2 } from "path";
|
|
994
|
+
var AGENTS = [
|
|
995
|
+
{ id: "claude-code", project: [".claude", "skills"], global: [".claude", "skills"], detect: [[".claude"]] },
|
|
996
|
+
{ id: "cursor", project: [".cursor", "skills"], global: [".cursor", "skills"], detect: [[".cursor"]] },
|
|
997
|
+
{ id: "codex", project: [".codex", "skills"], global: [".codex", "skills"], detect: [[".codex"]] },
|
|
998
|
+
{ id: "windsurf", project: [".windsurf", "skills"], global: [".codeium", "windsurf", "skills"], detect: [[".codeium", "windsurf"], [".windsurf"]] },
|
|
999
|
+
{ id: "opencode", project: [".opencode", "skills"], global: [".config", "opencode", "skills"], detect: [[".config", "opencode"], [".opencode"]] },
|
|
1000
|
+
{ id: "gemini-cli", project: [".agents", "skills"], global: [".gemini", "skills"], detect: [[".gemini"]] },
|
|
1001
|
+
{ id: "antigravity", project: [".agents", "skills"], global: [".gemini", "antigravity", "skills"], detect: [[".gemini", "antigravity"], [".antigravity"]] },
|
|
1002
|
+
{ id: "kiro", project: [".kiro", "skills"], global: [".kiro", "skills"], detect: [[".kiro"]] },
|
|
1003
|
+
{ id: "warp", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".warp"]] },
|
|
1004
|
+
{ id: "github-copilot", project: [".agents", "skills"], global: [".copilot", "skills"], detect: [[".copilot"]] },
|
|
1005
|
+
{ id: "cline", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".cline"]] },
|
|
1006
|
+
{ id: "roo", project: [".roo", "skills"], global: [".roo", "skills"], detect: [[".roo"]] },
|
|
1007
|
+
{ id: "goose", project: [".goose", "skills"], global: [".config", "goose", "skills"], detect: [[".config", "goose"]] },
|
|
1008
|
+
{ id: "trae", project: [".trae", "skills"], global: [".trae", "skills"], detect: [[".trae"]] },
|
|
1009
|
+
{ id: "qwen-code", project: [".qwen", "skills"], global: [".qwen", "skills"], detect: [[".qwen"]] },
|
|
1010
|
+
{ id: "amp", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".config", "amp"]] },
|
|
1011
|
+
// — extended registry —
|
|
1012
|
+
{ id: "openclaw", project: ["skills"], global: [".openclaw", "skills"], detect: [[".openclaw"], [".clawdbot"], [".moltbot"]] },
|
|
1013
|
+
{ id: "kilo", project: [".kilocode", "skills"], global: [".kilocode", "skills"], detect: [[".kilocode"]] },
|
|
1014
|
+
{ id: "hermes-agent", project: [".hermes", "skills"], global: [".hermes", "skills"], detect: [[".hermes"]] },
|
|
1015
|
+
{ id: "continue", project: [".continue", "skills"], global: [".continue", "skills"], detect: [[".continue"]] },
|
|
1016
|
+
{ id: "devin", project: [".devin", "skills"], global: [".config", "devin", "skills"], detect: [[".config", "devin"]] },
|
|
1017
|
+
{ id: "openhands", project: [".openhands", "skills"], global: [".openhands", "skills"], detect: [[".openhands"]] },
|
|
1018
|
+
{ id: "replit", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".replit"]] },
|
|
1019
|
+
{ id: "mistral-vibe", project: [".vibe", "skills"], global: [".vibe", "skills"], detect: [[".vibe"]] },
|
|
1020
|
+
{ id: "zed", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".config", "zed"]] },
|
|
1021
|
+
{ id: "adal", project: [".adal", "skills"], global: [".adal", "skills"], detect: [[".adal"]] },
|
|
1022
|
+
{ id: "aider-desk", project: [".aider-desk", "skills"], global: [".aider-desk", "skills"], detect: [[".aider-desk"]] },
|
|
1023
|
+
{ id: "antigravity-cli", project: [".agents", "skills"], global: [".gemini", "antigravity-cli", "skills"], detect: [[".gemini", "antigravity-cli"]] },
|
|
1024
|
+
{ id: "augment", project: [".augment", "skills"], global: [".augment", "skills"], detect: [[".augment"]] },
|
|
1025
|
+
{ id: "autohand-code", project: [".autohand", "skills"], global: [".autohand", "skills"], detect: [[".autohand"]] },
|
|
1026
|
+
{ id: "bob", project: [".bob", "skills"], global: [".bob", "skills"], detect: [[".bob"]] },
|
|
1027
|
+
{ id: "codearts-agent", project: [".codeartsdoer", "skills"], global: [".codeartsdoer", "skills"], detect: [[".codeartsdoer"]] },
|
|
1028
|
+
{ id: "codebuddy", project: [".codebuddy", "skills"], global: [".codebuddy", "skills"], detect: [[".codebuddy"]] },
|
|
1029
|
+
{ id: "codemaker", project: [".codemaker", "skills"], global: [".codemaker", "skills"], detect: [[".codemaker"]] },
|
|
1030
|
+
{ id: "codestudio", project: [".codestudio", "skills"], global: [".codestudio", "skills"], detect: [[".codestudio"]] },
|
|
1031
|
+
{ id: "command-code", project: [".commandcode", "skills"], global: [".commandcode", "skills"], detect: [[".commandcode"]] },
|
|
1032
|
+
{ id: "cortex", project: [".cortex", "skills"], global: [".snowflake", "cortex", "skills"], detect: [[".snowflake", "cortex"]] },
|
|
1033
|
+
{ id: "crush", project: [".crush", "skills"], global: [".config", "crush", "skills"], detect: [[".config", "crush"]] },
|
|
1034
|
+
{ id: "deepagents", project: [".agents", "skills"], global: [".deepagents", "agent", "skills"], detect: [[".deepagents"]] },
|
|
1035
|
+
{ id: "dexto", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".dexto"]] },
|
|
1036
|
+
{ id: "droid", project: [".factory", "skills"], global: [".factory", "skills"], detect: [[".factory"]] },
|
|
1037
|
+
{ id: "firebender", project: [".agents", "skills"], global: [".firebender", "skills"], detect: [[".firebender"]] },
|
|
1038
|
+
{ id: "forgecode", project: [".forge", "skills"], global: [".forge", "skills"], detect: [[".forge"]] },
|
|
1039
|
+
{ id: "iflow-cli", project: [".iflow", "skills"], global: [".iflow", "skills"], detect: [[".iflow"]] },
|
|
1040
|
+
{ id: "inference-sh", project: [".inferencesh", "skills"], global: [".inferencesh", "skills"], detect: [[".inferencesh"]] },
|
|
1041
|
+
{ id: "jazz", project: [".jazz", "skills"], global: [".jazz", "skills"], detect: [[".jazz"]] },
|
|
1042
|
+
{ id: "junie", project: [".junie", "skills"], global: [".junie", "skills"], detect: [[".junie"]] },
|
|
1043
|
+
{ id: "kimi-code-cli", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".kimi-code"], [".kimi"]] },
|
|
1044
|
+
{ id: "kode", project: [".kode", "skills"], global: [".kode", "skills"], detect: [[".kode"]] },
|
|
1045
|
+
{ id: "lingma", project: [".lingma", "skills"], global: [".lingma", "skills"], detect: [[".lingma"]] },
|
|
1046
|
+
{ id: "loaf", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".loaf"]] },
|
|
1047
|
+
{ id: "mcpjam", project: [".mcpjam", "skills"], global: [".mcpjam", "skills"], detect: [[".mcpjam"]] },
|
|
1048
|
+
{ id: "moxby", project: [".moxby", "skills"], global: [".moxby", "skills"], detect: [[".moxby"]] },
|
|
1049
|
+
{ id: "mux", project: [".mux", "skills"], global: [".mux", "skills"], detect: [[".mux"]] },
|
|
1050
|
+
{ id: "neovate", project: [".neovate", "skills"], global: [".neovate", "skills"], detect: [[".neovate"]] },
|
|
1051
|
+
{ id: "ona", project: [".ona", "skills"], global: [".ona", "skills"], detect: [[".ona"]] },
|
|
1052
|
+
{ id: "pi", project: [".pi", "skills"], global: [".pi", "agent", "skills"], detect: [[".pi", "agent"]] },
|
|
1053
|
+
{ id: "pochi", project: [".pochi", "skills"], global: [".pochi", "skills"], detect: [[".pochi"]] },
|
|
1054
|
+
{ id: "qoder", project: [".qoder", "skills"], global: [".qoder", "skills"], detect: [[".qoder"]] },
|
|
1055
|
+
{ id: "qoder-cn", project: [".qoder", "skills"], global: [".qoder-cn", "skills"], detect: [[".qoder-cn"]] },
|
|
1056
|
+
{ id: "reasonix", project: [".reasonix", "skills"], global: [".reasonix", "skills"], detect: [[".reasonix"]] },
|
|
1057
|
+
{ id: "rovodev", project: [".rovodev", "skills"], global: [".rovodev", "skills"], detect: [[".rovodev"]] },
|
|
1058
|
+
{ id: "tabnine-cli", project: [".tabnine", "agent", "skills"], global: [".tabnine", "agent", "skills"], detect: [[".tabnine"]] },
|
|
1059
|
+
{ id: "terramind", project: [".terramind", "skills"], global: [".terramind", "skills"], detect: [[".terramind"]] },
|
|
1060
|
+
{ id: "tinycloud", project: [".tinycloud", "skills"], global: [".tinycloud", "skills"], detect: [[".tinycloud"]] },
|
|
1061
|
+
{ id: "trae-cn", project: [".trae", "skills"], global: [".trae-cn", "skills"], detect: [[".trae-cn"]] },
|
|
1062
|
+
{ id: "zcode", project: [".zcode", "skills"], global: [".zcode", "skills"], detect: [[".zcode"]] },
|
|
1063
|
+
{ id: "zencoder", project: [".zencoder", "skills"], global: [".zencoder", "skills"], detect: [[".zencoder"]] }
|
|
1064
|
+
];
|
|
1065
|
+
function agentDir(agentId, opts = {}) {
|
|
1066
|
+
const agent = AGENTS.find((a) => a.id === agentId);
|
|
1067
|
+
if (!agent) throw new Error(`Unknown agent: ${agentId}. Known: ${AGENTS.map((a) => a.id).join(", ")}`);
|
|
1068
|
+
return opts.global ? join4(opts.home ?? homedir3(), ...agent.global) : join4(opts.cwd ?? process.cwd(), ...agent.project);
|
|
1069
|
+
}
|
|
1070
|
+
function detectAgents(opts = {}) {
|
|
1071
|
+
const home = opts.home ?? homedir3();
|
|
1072
|
+
return AGENTS.filter((a) => a.detect.some((d) => existsSync3(join4(home, ...d)))).map((a) => a.id);
|
|
1073
|
+
}
|
|
1074
|
+
function writeSkill(skillName, files, targetDir, opts = {}) {
|
|
1075
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(skillName) || skillName.includes("..")) {
|
|
1076
|
+
throw new Error(`refused unsafe skill name: "${skillName}"`);
|
|
1077
|
+
}
|
|
1078
|
+
const dest = join4(targetDir, skillName);
|
|
1079
|
+
if (opts.mode === "symlink" && opts.canonicalDir) {
|
|
1080
|
+
mkdirSync3(dirname2(dest), { recursive: true });
|
|
1081
|
+
rmSync2(dest, { recursive: true, force: true });
|
|
1082
|
+
symlinkSync(resolve2(opts.canonicalDir), dest, "dir");
|
|
1083
|
+
return dest;
|
|
1084
|
+
}
|
|
1085
|
+
mkdirSync3(dest, { recursive: true });
|
|
1086
|
+
const root = resolve2(dest);
|
|
1087
|
+
for (const f of files) {
|
|
1088
|
+
const fp = resolve2(dest, f.path);
|
|
1089
|
+
if (fp !== root && !fp.startsWith(root + sep2)) {
|
|
1090
|
+
throw new Error(`refused unsafe path in skill "${skillName}": ${f.path}`);
|
|
1091
|
+
}
|
|
1092
|
+
mkdirSync3(dirname2(fp), { recursive: true });
|
|
1093
|
+
writeFileSync4(fp, f.contents);
|
|
1094
|
+
}
|
|
1095
|
+
return dest;
|
|
1096
|
+
}
|
|
1097
|
+
function installedSkills(opts = {}) {
|
|
1098
|
+
const scope = opts.global ? "global" : "project";
|
|
1099
|
+
const out = [];
|
|
1100
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1101
|
+
for (const agent of AGENTS) {
|
|
1102
|
+
const dir = agentDir(agent.id, opts);
|
|
1103
|
+
if (seen.has(dir)) continue;
|
|
1104
|
+
seen.add(dir);
|
|
1105
|
+
if (!existsSync3(dir)) continue;
|
|
1106
|
+
for (const entry of readdirSync2(dir)) {
|
|
1107
|
+
const skillMd = join4(dir, entry, "SKILL.md");
|
|
1108
|
+
if (!existsSync3(skillMd) || !statSync2(skillMd).isFile()) continue;
|
|
1109
|
+
out.push({ name: entry, agent: agent.id, scope, path: join4(dir, entry), raw: readFileSync3(skillMd, "utf8") });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return out;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// ../cli/src/commands/add.ts
|
|
1116
|
+
var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "AGENTS.md", "CLAUDE.md", "skills-lock.json"];
|
|
1117
|
+
function looksLikeProject(dir) {
|
|
1118
|
+
if (PROJECT_MARKERS.some((m) => existsSync4(join5(dir, m)))) return true;
|
|
1119
|
+
const agentRoots = new Set(AGENTS.map((a) => a.project[0]).filter((seg) => seg.startsWith(".")));
|
|
1120
|
+
return [...agentRoots].some((seg) => existsSync4(join5(dir, seg)));
|
|
1121
|
+
}
|
|
1122
|
+
var interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1123
|
+
async function promptScopeInteractive(ctx) {
|
|
1124
|
+
const choice = await p3.select({
|
|
1125
|
+
message: "Installation scope",
|
|
1126
|
+
initialValue: ctx.suggestGlobal,
|
|
1127
|
+
options: [
|
|
1128
|
+
{ value: false, label: "Project", hint: `install in ${ctx.cwd} (committed with your project)` },
|
|
1129
|
+
{ value: true, label: "Global", hint: "install in your home directory (available across all projects)" }
|
|
1130
|
+
]
|
|
1131
|
+
});
|
|
1132
|
+
return p3.isCancel(choice) ? null : choice;
|
|
1133
|
+
}
|
|
1134
|
+
async function promptAgentsInteractive(ctx) {
|
|
1135
|
+
if (ctx.detected.length < 2) return ctx.detected;
|
|
1136
|
+
const picked = await p3.multiselect({
|
|
1137
|
+
message: "Which agents do you want to install to?",
|
|
1138
|
+
options: ctx.detected.map((id) => ({ value: id, label: id, hint: ctx.dirFor(id) })),
|
|
1139
|
+
initialValues: ctx.detected,
|
|
1140
|
+
required: true
|
|
1141
|
+
});
|
|
1142
|
+
return p3.isCancel(picked) ? null : picked;
|
|
1143
|
+
}
|
|
1144
|
+
function isLocalish(arg) {
|
|
1145
|
+
return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync4(arg);
|
|
1146
|
+
}
|
|
1147
|
+
async function resolvePackFiles(skill, fetchTree = resolveTree) {
|
|
1148
|
+
if (!skill.source_repo) return null;
|
|
1149
|
+
const attempts = [{ ref: skill.commit_sha ?? void 0, pinMiss: false }];
|
|
1150
|
+
if (skill.commit_sha) attempts.push({ ref: void 0, pinMiss: true });
|
|
1151
|
+
for (const attempt of attempts) {
|
|
1152
|
+
try {
|
|
1153
|
+
const files = await fetchTree(skill.source_repo, attempt.ref);
|
|
1154
|
+
const sk = files.find((f) => f.path === "SKILL.md");
|
|
1155
|
+
if (sk) return { raw: sk.contents.toString("utf8"), files, pinMiss: attempt.pinMiss };
|
|
1156
|
+
} catch {
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
function registryFallbackNote(host) {
|
|
1162
|
+
return `installed from GitHub because the SkillMD registry (${host}) was unreachable \u2014 this copy is not the registry-reviewed version`;
|
|
1163
|
+
}
|
|
1164
|
+
function registryUnreachableError(arg, host, e) {
|
|
1165
|
+
const why = e.status ? `HTTP ${e.status}` : "network error";
|
|
1166
|
+
return new Error(
|
|
1167
|
+
`could not reach the SkillMD registry at ${host} (${why}), and "${arg}" was not found on GitHub either.
|
|
1168
|
+
If this environment restricts network access (sandbox, proxy, CI), ask an administrator to allow ${host} \u2014 or install through the SkillMD MCP server, which many sandboxed agents can already reach.
|
|
1169
|
+
Registry error: ${e.message}`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
var defaultDeps = {
|
|
1173
|
+
async resolve(arg, flags) {
|
|
1174
|
+
if (isLocalish(arg)) {
|
|
1175
|
+
const rs = await resolveSource(arg);
|
|
1176
|
+
return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
|
|
1177
|
+
}
|
|
1178
|
+
let registryDown = null;
|
|
1179
|
+
const registryHost = (() => {
|
|
1180
|
+
try {
|
|
1181
|
+
return new URL(createClient(flags).base).host;
|
|
1182
|
+
} catch {
|
|
1183
|
+
return "api.skillmd.com";
|
|
1184
|
+
}
|
|
1185
|
+
})();
|
|
1186
|
+
if (/^[^/\s:]+\/[^/\s:]+$/.test(arg)) {
|
|
1187
|
+
const { api, base: apiBase, token } = createClient(flags);
|
|
1188
|
+
const [owner, name] = arg.split("/");
|
|
1189
|
+
try {
|
|
1190
|
+
const skill = await api(`/api/skills/${owner}/${name}`);
|
|
1191
|
+
const skillName = skill.slug.split("/")[1] || name;
|
|
1192
|
+
const meta = {
|
|
1193
|
+
name: skillName,
|
|
1194
|
+
slug: skillName,
|
|
1195
|
+
registrySlug: arg,
|
|
1196
|
+
verified: skill.verified,
|
|
1197
|
+
type: skill.type,
|
|
1198
|
+
securityFlags: skill.security_flags
|
|
1199
|
+
};
|
|
1200
|
+
if (skill.type === "pack") {
|
|
1201
|
+
const bundle = await fetchBundle(apiBase, arg, token);
|
|
1202
|
+
if (bundle && bundle.length) {
|
|
1203
|
+
const sk = bundle.find((f) => f.path === "SKILL.md");
|
|
1204
|
+
return [{ ...meta, raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill), files: bundle }];
|
|
1205
|
+
}
|
|
1206
|
+
const pack = await resolvePackFiles(skill);
|
|
1207
|
+
if (pack) return [{ ...meta, ...pack }];
|
|
1208
|
+
}
|
|
1209
|
+
return [{ ...meta, raw: skillMdFor(skill) }];
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
if (e instanceof IntegrityError) throw e;
|
|
1212
|
+
if (e instanceof RegistryError && e.status !== 404) registryDown = e;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
const rs = await resolveSource(arg);
|
|
1217
|
+
const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
|
|
1218
|
+
return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, note: note2 }));
|
|
1219
|
+
} catch (e) {
|
|
1220
|
+
if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
|
|
1221
|
+
throw e;
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
get promptScope() {
|
|
1225
|
+
return interactive() ? promptScopeInteractive : void 0;
|
|
1226
|
+
},
|
|
1227
|
+
get promptAgents() {
|
|
1228
|
+
return interactive() ? promptAgentsInteractive : void 0;
|
|
1229
|
+
},
|
|
1230
|
+
fireInstall(registrySlug, flags) {
|
|
1231
|
+
const { api } = createClient(flags);
|
|
1232
|
+
const [owner, name] = registrySlug.split("/");
|
|
1233
|
+
void api(`/api/skills/${owner}/${name}/install`, {
|
|
1234
|
+
method: "POST",
|
|
1235
|
+
headers: { "content-type": "application/json" },
|
|
1236
|
+
body: JSON.stringify({ via: "cli" })
|
|
1237
|
+
}).catch(() => {
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
async function resolveScope(flags, deps) {
|
|
1242
|
+
const home = resolve3(flags.home ?? homedir4());
|
|
1243
|
+
const cwd = resolve3(flags.cwd ?? process.cwd());
|
|
1244
|
+
const base = { cwd: flags.cwd, home: flags.home };
|
|
1245
|
+
if (flags.global) return { scope: { global: true, ...base } };
|
|
1246
|
+
if (flags.project) return { scope: { global: false, ...base } };
|
|
1247
|
+
if (cwd === home) return { scope: { global: true, ...base } };
|
|
1248
|
+
const isProject = looksLikeProject(cwd);
|
|
1249
|
+
if (deps.promptScope && !flags.yes && !flags.json) {
|
|
1250
|
+
const global = await deps.promptScope({ cwd, suggestGlobal: !isProject });
|
|
1251
|
+
if (global === null) return null;
|
|
1252
|
+
return { scope: { global, ...base } };
|
|
1253
|
+
}
|
|
1254
|
+
if (isProject) return { scope: { global: false, ...base } };
|
|
1255
|
+
return {
|
|
1256
|
+
scope: { global: true, ...base },
|
|
1257
|
+
note: `no project detected in ${cwd} \u2014 installing to your user-level agent folders (pass --project to install here)`
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
var CANCELLED = { written: [], blocked: [], exitCode: 0, cancelled: true, output: pc7.dim("Installation cancelled \u2014 nothing was installed.") };
|
|
1261
|
+
async function runAdd(arg, flags, deps = defaultDeps) {
|
|
1262
|
+
let candidates;
|
|
1263
|
+
try {
|
|
1264
|
+
candidates = await deps.resolve(arg, flags);
|
|
1265
|
+
} catch (e) {
|
|
1266
|
+
if (e instanceof IntegrityError) {
|
|
1267
|
+
return {
|
|
1268
|
+
written: [],
|
|
1269
|
+
blocked: [{ name: arg, reason: e.message }],
|
|
1270
|
+
exitCode: 1,
|
|
1271
|
+
output: pc7.red(`\u2717 ${arg} blocked \u2014 ${e.message}`)
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
throw e;
|
|
1275
|
+
}
|
|
1276
|
+
if (flags.skill?.length) candidates = candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
|
|
1277
|
+
const deny = new Set(flags.deny ?? []);
|
|
1278
|
+
const written = [];
|
|
1279
|
+
const blocked = [];
|
|
1280
|
+
const lines = [];
|
|
1281
|
+
const resolved = await resolveScope(flags, deps);
|
|
1282
|
+
if (!resolved) return CANCELLED;
|
|
1283
|
+
const { scope } = resolved;
|
|
1284
|
+
const isGlobal = scope.global;
|
|
1285
|
+
const home = flags.home ?? homedir4();
|
|
1286
|
+
if (resolved.note) lines.push(pc7.dim(`\u2139 ${resolved.note}`));
|
|
1287
|
+
const detected = detectAgents(scope);
|
|
1288
|
+
let targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
|
|
1289
|
+
if (!flags.agent?.length && deps.promptAgents && !flags.yes && !flags.json) {
|
|
1290
|
+
const picked = await deps.promptAgents({ detected: targets, dirFor: (id) => agentDir(id, scope) });
|
|
1291
|
+
if (picked === null) return CANCELLED;
|
|
1292
|
+
targets = picked;
|
|
1293
|
+
}
|
|
1294
|
+
const dirAgents = /* @__PURE__ */ new Map();
|
|
1295
|
+
for (const agentId of targets) {
|
|
1296
|
+
const dir = agentDir(agentId, scope);
|
|
1297
|
+
dirAgents.set(dir, [...dirAgents.get(dir) ?? [], agentId]);
|
|
1298
|
+
}
|
|
1299
|
+
for (const c of candidates) {
|
|
1300
|
+
const result = lint(c.raw, { slug: c.slug });
|
|
1301
|
+
const allFlags = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
|
|
1302
|
+
const deniedFlags = allFlags.filter((f) => deny.has(f));
|
|
1303
|
+
if (!flags.skipLint && !result.ok) {
|
|
1304
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
1305
|
+
blocked.push({ name: c.name, reason: `lint errors: ${errors.map((d) => d.id).join(", ")}` });
|
|
1306
|
+
lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
|
|
1307
|
+
for (const d of errors) lines.push(pc7.dim(` ${d.id} ${d.message}`));
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (deniedFlags.length) {
|
|
1311
|
+
blocked.push({ name: c.name, reason: `denied security flags: ${deniedFlags.join(", ")}` });
|
|
1312
|
+
lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 you passed --deny ${deniedFlags.join(", ")}`));
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const files = c.files ?? [{ path: "SKILL.md", contents: c.raw }];
|
|
1316
|
+
const base = isGlobal ? home : resolve3(flags.cwd ?? process.cwd());
|
|
1317
|
+
lines.push(pc7.green(`\u2713 ${c.name}`) + pc7.dim(` score ${result.score}${allFlags.length ? ` ${allFlags.join(", ")}` : ""}`));
|
|
1318
|
+
for (const [dir, agentIds] of dirAgents) {
|
|
1319
|
+
const out = writeSkill(c.name, files, dir, { mode: "copy" });
|
|
1320
|
+
written.push({ name: c.name, dir: out });
|
|
1321
|
+
const rel = relative2(base, out).split("\\").join("/") || out;
|
|
1322
|
+
lines.push(pc7.dim(` ${rel.padEnd(28)} ${agentIds.join(", ")}`));
|
|
1323
|
+
}
|
|
1324
|
+
if (c.pinMiss) {
|
|
1325
|
+
lines.push(pc7.yellow(`\u26A0 ${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
|
|
1326
|
+
}
|
|
1327
|
+
if (c.note) {
|
|
1328
|
+
lines.push(pc7.yellow(`\u26A0 ${c.name}: ${c.note}`));
|
|
1329
|
+
}
|
|
1330
|
+
if (c.type === "pack" && !c.files) {
|
|
1331
|
+
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.`));
|
|
1332
|
+
}
|
|
1333
|
+
if (c.registrySlug) deps.fireInstall(c.registrySlug, flags);
|
|
1334
|
+
}
|
|
1335
|
+
const exitCode = written.length === 0 ? 1 : 0;
|
|
1336
|
+
return { written, blocked, exitCode, output: lines.join("\n") };
|
|
1337
|
+
}
|
|
1338
|
+
function addCommand() {
|
|
1339
|
+
return new Command7("add").description("Install a skill from the registry, a GitHub repo, or a local path (lints before writing)").argument("<source>", "registry slug (owner/name), GitHub source, or local path").option("-g, --global", "install to your user-level agent folders (available in every project)").option("-p, --project", "install into the current directory's agent folders (committed with the project)").option("-a, --agent <agents...>", "target specific agents (default: every agent detected on this machine)").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--copy", "copy files (default)", true).option("-y, --yes", "skip prompts; scope is auto-detected (project if this folder looks like a project, else global)").addOption(new Option("--allow-unverified").hideHelp()).option("--skip-lint", "install even if the skill fails lint").option("--deny <flags...>", "refuse skills carrying any of these security flags (opt-in)").action(async (source, opts, cmd) => {
|
|
1340
|
+
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
1341
|
+
const run = await runAdd(source, merged);
|
|
1342
|
+
console.log(run.output);
|
|
1343
|
+
process.exitCode = run.exitCode;
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// ../cli/src/commands/list.ts
|
|
1348
|
+
import { Command as Command8 } from "commander";
|
|
1349
|
+
import pc8 from "picocolors";
|
|
1350
|
+
function listCommand() {
|
|
1351
|
+
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) => {
|
|
1352
|
+
const scope = { global: opts.global };
|
|
1353
|
+
const skills = installedSkills(scope);
|
|
1354
|
+
if (opts.json) {
|
|
1355
|
+
console.log(JSON.stringify(skills.map((s) => ({ name: s.name, agent: s.agent, scope: s.scope, path: s.path })), null, 2));
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (skills.length === 0) {
|
|
1359
|
+
console.log("No installed skills found.");
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
for (const s of skills) {
|
|
1363
|
+
const parsed = parseSkillMd(s.raw);
|
|
1364
|
+
const desc = "error" in parsed ? pc8.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc8.dim(parsed.description.slice(0, 80));
|
|
1365
|
+
console.log(`${pc8.bold(s.name)} ${pc8.dim(`[${s.agent}/${s.scope}]`)}
|
|
1366
|
+
${desc}`);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// ../cli/src/commands/remove.ts
|
|
1372
|
+
import { Command as Command9 } from "commander";
|
|
1373
|
+
import { rmSync as rmSync3 } from "fs";
|
|
1374
|
+
import pc9 from "picocolors";
|
|
1375
|
+
function runRemove(names, scope = {}) {
|
|
1376
|
+
const installed = installedSkills(scope);
|
|
1377
|
+
const targets = installed.filter((s) => names.includes(s.name));
|
|
1378
|
+
const removed = [];
|
|
1379
|
+
const lines = [];
|
|
1380
|
+
for (const t of targets) {
|
|
1381
|
+
rmSync3(t.path, { recursive: true, force: true });
|
|
1382
|
+
removed.push(t.path);
|
|
1383
|
+
lines.push(pc9.green(`\u2713 removed ${t.name} (${t.agent})`));
|
|
1384
|
+
}
|
|
1385
|
+
for (const n of names) {
|
|
1386
|
+
if (!targets.some((t) => t.name === n)) lines.push(pc9.yellow(`- ${n} not installed`));
|
|
1387
|
+
}
|
|
1388
|
+
return { removed, output: lines.join("\n") };
|
|
1389
|
+
}
|
|
1390
|
+
function removeCommand() {
|
|
1391
|
+
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) => {
|
|
1392
|
+
const run = runRemove(names, { global: opts.global });
|
|
1393
|
+
console.log(run.output);
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// ../cli/src/commands/update.ts
|
|
1398
|
+
import { Command as Command10 } from "commander";
|
|
1399
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
1400
|
+
import { join as join6 } from "path";
|
|
1401
|
+
import pc10 from "picocolors";
|
|
1402
|
+
async function runUpdate(names, flags, scope = {}) {
|
|
1403
|
+
const { api } = createClient(flags);
|
|
1404
|
+
const installed = installedSkills(scope).filter((s) => names.length === 0 || names.includes(s.name));
|
|
1405
|
+
const updated = [];
|
|
1406
|
+
const lines = [];
|
|
1407
|
+
for (const s of installed) {
|
|
1408
|
+
const slug = await findRegistrySlug(api, s);
|
|
1409
|
+
if (!slug) {
|
|
1410
|
+
lines.push(pc10.dim(`- ${s.name} skipped (no registry match)`));
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
try {
|
|
1414
|
+
const [owner, name] = slug.split("/");
|
|
1415
|
+
const skill = await api(`/api/skills/${owner}/${name}`);
|
|
1416
|
+
const next = skillMdFor(skill);
|
|
1417
|
+
if (next.trim() === s.raw.trim()) {
|
|
1418
|
+
lines.push(pc10.dim(`- ${s.name} already up to date`));
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
const result = lint(next, { slug: s.name });
|
|
1422
|
+
if (!result.ok) {
|
|
1423
|
+
lines.push(pc10.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
writeFileSync5(join6(s.path, "SKILL.md"), next, "utf8");
|
|
1427
|
+
updated.push(s.name);
|
|
1428
|
+
lines.push(pc10.green(`\u2713 updated ${s.name}`));
|
|
1429
|
+
} catch {
|
|
1430
|
+
lines.push(pc10.red(`\u2717 ${s.name} update error`));
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return { updated, output: lines.join("\n") || "Nothing to update." };
|
|
1434
|
+
}
|
|
1435
|
+
async function findRegistrySlug(api, s) {
|
|
1436
|
+
const r = await api(`/api/search?q=${encodeURIComponent(s.name)}&limit=5`).catch(() => null);
|
|
1437
|
+
const hit = r?.items?.find((it) => it.slug.split("/")[1] === s.name);
|
|
1438
|
+
return hit?.slug ?? null;
|
|
1439
|
+
}
|
|
1440
|
+
function updateCommand() {
|
|
1441
|
+
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) => {
|
|
1442
|
+
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
1443
|
+
const run = await runUpdate(names, merged, { global: opts.global });
|
|
1444
|
+
console.log(run.output);
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// ../cli/src/commands/publish.ts
|
|
1449
|
+
import { Command as Command11 } from "commander";
|
|
1450
|
+
import pc11 from "picocolors";
|
|
1451
|
+
var defaultDeps2 = {
|
|
1452
|
+
resolve: resolveSource,
|
|
1453
|
+
async submit(payload, flags) {
|
|
1454
|
+
const { api, hasToken } = createClient(flags);
|
|
1455
|
+
if (!hasToken) throw new Error("No token. Run `skillmd login` or pass --token.");
|
|
1456
|
+
return api(`/api/skills`, {
|
|
1457
|
+
method: "POST",
|
|
1458
|
+
headers: { "content-type": "application/json" },
|
|
1459
|
+
body: JSON.stringify(payload)
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
async function runPublish(target, flags, deps = defaultDeps2) {
|
|
1464
|
+
const sources = await deps.resolve(target);
|
|
1465
|
+
if (sources.length !== 1) {
|
|
1466
|
+
return { result: lint(""), published: false, exitCode: 1, output: `Expected exactly one SKILL.md, found ${sources.length}.` };
|
|
1467
|
+
}
|
|
1468
|
+
const src = sources[0];
|
|
1469
|
+
const result = lint(src.raw, { slug: src.slug });
|
|
1470
|
+
const report = toText([{ file: src.path, result }], { decorate });
|
|
1471
|
+
if (!result.ok) {
|
|
1472
|
+
return { result, published: false, exitCode: 1, output: `${report}
|
|
1473
|
+
${pc11.red("\u2717 Refusing to publish: fix the errors above.")}` };
|
|
1474
|
+
}
|
|
1475
|
+
const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
|
|
1476
|
+
if (hasWarn && !flags.force) {
|
|
1477
|
+
return { result, published: false, exitCode: 1, output: `${report}
|
|
1478
|
+
${pc11.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
|
|
1479
|
+
}
|
|
1480
|
+
if (flags.dryRun) {
|
|
1481
|
+
return { result, published: false, exitCode: 0, output: `${report}
|
|
1482
|
+
${pc11.green("\u2713 Dry run: would publish.")}` };
|
|
1483
|
+
}
|
|
1484
|
+
const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
|
|
1485
|
+
return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
|
|
1486
|
+
${pc11.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
|
|
1487
|
+
}
|
|
1488
|
+
function publishCommand() {
|
|
1489
|
+
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) => {
|
|
1490
|
+
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
1491
|
+
const run = await runPublish(target, merged);
|
|
1492
|
+
console.log(run.output);
|
|
1493
|
+
process.exitCode = run.exitCode;
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// ../cli/src/commands/login.ts
|
|
1498
|
+
import { Command as Command12 } from "commander";
|
|
1499
|
+
import * as p4 from "@clack/prompts";
|
|
1500
|
+
function loginCommand() {
|
|
1501
|
+
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) => {
|
|
1502
|
+
let token = opts.token;
|
|
1503
|
+
if (!token) {
|
|
1504
|
+
const answer = await p4.password({ message: "Paste your SkillMD token (from your account page)" });
|
|
1505
|
+
if (p4.isCancel(answer)) {
|
|
1506
|
+
p4.cancel("Cancelled.");
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
token = answer;
|
|
1510
|
+
}
|
|
1511
|
+
if (!token) {
|
|
1512
|
+
console.error("A token is required.");
|
|
1513
|
+
process.exitCode = 1;
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const cfg = readConfig();
|
|
1517
|
+
cfg.token = token;
|
|
1518
|
+
if (opts.api) cfg.api = opts.api;
|
|
1519
|
+
writeConfig(cfg);
|
|
1520
|
+
console.log(`Saved token to ${configPath()}`);
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
function logoutCommand() {
|
|
1524
|
+
return new Command12("logout").description("Remove the stored SkillMD token").action(() => {
|
|
1525
|
+
const cfg = readConfig();
|
|
1526
|
+
delete cfg.token;
|
|
1527
|
+
writeConfig(cfg);
|
|
1528
|
+
console.log("Removed stored token.");
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// ../cli/src/commands/interactive.ts
|
|
1533
|
+
import * as p5 from "@clack/prompts";
|
|
1534
|
+
import pc12 from "picocolors";
|
|
1535
|
+
function bail(v) {
|
|
1536
|
+
if (p5.isCancel(v)) {
|
|
1537
|
+
p5.cancel("Cancelled.");
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
async function browseSearchResults(items) {
|
|
1543
|
+
const cols = process.stdout.columns ?? 80;
|
|
1544
|
+
for (; ; ) {
|
|
1545
|
+
const picked = await p5.select({
|
|
1546
|
+
message: "Search results",
|
|
1547
|
+
options: [
|
|
1548
|
+
...items.map((item2) => {
|
|
1549
|
+
const starsTxt = item2.repo_stars != null ? ` \u2605${formatStars(item2.repo_stars)}` : "";
|
|
1550
|
+
const checkTxt = item2.verified ? " \u2713" : "";
|
|
1551
|
+
const plainLabel = `${item2.slug}${starsTxt}${checkTxt}`;
|
|
1552
|
+
const label2 = `${pc12.bold(item2.slug)}${starsTxt ? pc12.dim(starsTxt) : ""}${checkTxt ? pc12.green(checkTxt) : ""}`;
|
|
1553
|
+
const hintWidth = Math.max(10, cols - plainLabel.length - 6);
|
|
1554
|
+
return {
|
|
1555
|
+
value: item2.slug,
|
|
1556
|
+
label: label2,
|
|
1557
|
+
hint: truncate(item2.description ?? "", hintWidth)
|
|
1558
|
+
};
|
|
1559
|
+
}),
|
|
1560
|
+
{ value: "__back", label: "\u2190 Back to menu" }
|
|
1561
|
+
]
|
|
1562
|
+
});
|
|
1563
|
+
if (p5.isCancel(picked) || picked === "__back") return;
|
|
1564
|
+
const item = items.find((i) => i.slug === picked);
|
|
1565
|
+
if (!item) continue;
|
|
1566
|
+
await browseOneResult(item);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
async function browseOneResult(item) {
|
|
1570
|
+
const starsTxt = item.repo_stars != null ? `\u2605 ${formatStars(item.repo_stars)} stars` : "";
|
|
1571
|
+
const meta = [
|
|
1572
|
+
starsTxt,
|
|
1573
|
+
item.verified ? pc12.green("\u2713 verified") : "unverified",
|
|
1574
|
+
item.type === "pack" ? "pack" : ""
|
|
1575
|
+
].filter(Boolean).join(" \xB7 ");
|
|
1576
|
+
p5.note([item.description ?? "", pc12.dim(meta)].join("\n"), item.slug);
|
|
1577
|
+
for (; ; ) {
|
|
1578
|
+
const action = await p5.select({
|
|
1579
|
+
message: "What next?",
|
|
1580
|
+
options: [
|
|
1581
|
+
{ value: "project", label: "Install to this project" },
|
|
1582
|
+
{ value: "global", label: "Install globally (user dir)" },
|
|
1583
|
+
{ value: "url", label: "Show page URL" },
|
|
1584
|
+
{ value: "back", label: "\u2190 Back to results" }
|
|
1585
|
+
]
|
|
1586
|
+
});
|
|
1587
|
+
if (p5.isCancel(action) || action === "back") return;
|
|
1588
|
+
if (action === "url") {
|
|
1589
|
+
p5.log.message(pc12.dim(`https://skillmd.com/skills/${item.slug}`));
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
p5.log.step(`Installing ${item.slug}\u2026`);
|
|
1593
|
+
try {
|
|
1594
|
+
const run = await runAdd(item.slug, action === "global" ? { global: true } : { project: true });
|
|
1595
|
+
console.log(run.output);
|
|
1596
|
+
if (run.exitCode === 0) p5.log.success("Installed.");
|
|
1597
|
+
else p5.log.warn("Not installed.");
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
p5.log.error(err instanceof Error ? err.message : String(err));
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
async function runInteractive() {
|
|
1604
|
+
p5.intro(pc12.bgCyan(pc12.black(" skillmd ")));
|
|
1605
|
+
for (; ; ) {
|
|
1606
|
+
const action = await p5.select({
|
|
1607
|
+
message: "What would you like to do?",
|
|
1608
|
+
options: [
|
|
1609
|
+
{ value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
|
|
1610
|
+
{ value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
|
|
1611
|
+
{ value: "search", label: "Search the registry" },
|
|
1612
|
+
{ value: "init", label: "Create a new skill" },
|
|
1613
|
+
{ value: "list", label: "List installed skills" },
|
|
1614
|
+
{ value: "help", label: "Show all commands" },
|
|
1615
|
+
{ value: "quit", label: "Quit" }
|
|
1616
|
+
]
|
|
1617
|
+
});
|
|
1618
|
+
if (bail(action) || action === "quit") break;
|
|
1619
|
+
switch (action) {
|
|
1620
|
+
case "lint":
|
|
1621
|
+
case "scan": {
|
|
1622
|
+
const path = await p5.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
|
|
1623
|
+
if (bail(path)) break;
|
|
1624
|
+
const target = String(path || ".");
|
|
1625
|
+
const s = p5.spinner();
|
|
1626
|
+
s.start(action === "lint" ? "Linting" : "Scanning");
|
|
1627
|
+
try {
|
|
1628
|
+
const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
|
|
1629
|
+
s.stop("Done");
|
|
1630
|
+
console.log(run.output);
|
|
1631
|
+
process.exitCode = run.exitCode;
|
|
1632
|
+
} catch (err) {
|
|
1633
|
+
s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
|
|
1634
|
+
process.exitCode = 1;
|
|
1635
|
+
}
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1638
|
+
case "search": {
|
|
1639
|
+
const query = await p5.text({ message: "Search the registry for" });
|
|
1640
|
+
if (bail(query)) break;
|
|
1641
|
+
const verified = await p5.confirm({ message: "Verified skills only?", initialValue: false });
|
|
1642
|
+
if (bail(verified)) break;
|
|
1643
|
+
const s = p5.spinner();
|
|
1644
|
+
s.start("Searching");
|
|
1645
|
+
try {
|
|
1646
|
+
const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
|
|
1647
|
+
s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
|
|
1648
|
+
if (run.items.length === 0) {
|
|
1649
|
+
p5.log.info("No results.");
|
|
1650
|
+
break;
|
|
1651
|
+
}
|
|
1652
|
+
await browseSearchResults(run.items);
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
|
|
1655
|
+
process.exitCode = 1;
|
|
1656
|
+
}
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1659
|
+
case "init": {
|
|
1660
|
+
const name = await p5.text({ message: "Skill name", placeholder: "my-skill" });
|
|
1661
|
+
if (bail(name)) break;
|
|
1662
|
+
const description = await p5.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
|
|
1663
|
+
if (bail(description)) break;
|
|
1664
|
+
try {
|
|
1665
|
+
const file = initSkill(String(name), { description: String(description || "") });
|
|
1666
|
+
p5.log.success(`Created ${pc12.bold(file)}`);
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
p5.log.error(err instanceof Error ? err.message : String(err));
|
|
1669
|
+
process.exitCode = 1;
|
|
1670
|
+
}
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
case "list": {
|
|
1674
|
+
const skills = [...installedSkills({}), ...installedSkills({ global: true })];
|
|
1675
|
+
if (skills.length === 0) {
|
|
1676
|
+
p5.log.info("No installed skills found (checked this folder and your user directory).");
|
|
1677
|
+
} else {
|
|
1678
|
+
p5.log.message(
|
|
1679
|
+
skills.map((sk) => {
|
|
1680
|
+
const parsed = parseSkillMd(sk.raw);
|
|
1681
|
+
const desc = "error" in parsed ? pc12.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc12.dim(parsed.description.slice(0, 70));
|
|
1682
|
+
return `${pc12.bold(sk.name)} ${pc12.dim(`[${sk.agent}/${sk.scope}]`)}
|
|
1683
|
+
${desc}`;
|
|
1684
|
+
}).join("\n")
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
break;
|
|
1688
|
+
}
|
|
1689
|
+
case "help": {
|
|
1690
|
+
p5.note(
|
|
1691
|
+
[
|
|
1692
|
+
`${pc12.bold("lint")} [path] validate SKILL.md + quality score`,
|
|
1693
|
+
`${pc12.bold("scan")} [path] security scan`,
|
|
1694
|
+
`${pc12.bold("search")} <query> search the registry`,
|
|
1695
|
+
`${pc12.bold("add")} <source> install a skill (lints first)`,
|
|
1696
|
+
`${pc12.bold("info")} <slug> registry details for a skill`,
|
|
1697
|
+
`${pc12.bold("init")} [name] scaffold a new skill`,
|
|
1698
|
+
`${pc12.bold("list / remove / update")} manage installed skills`,
|
|
1699
|
+
`${pc12.bold("publish")} [path] publish (fails on lint errors)`,
|
|
1700
|
+
`${pc12.bold("rules / login")}`
|
|
1701
|
+
].join("\n"),
|
|
1702
|
+
"Commands (run: skillmd <command>)"
|
|
1703
|
+
);
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
p5.outro(pc12.dim("skillmd.com"));
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// ../cli/src/index.ts
|
|
1712
|
+
var version = "0.0.0";
|
|
1713
|
+
try {
|
|
1714
|
+
version = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
1715
|
+
} catch {
|
|
1716
|
+
}
|
|
1717
|
+
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");
|
|
1718
|
+
program.addCommand(lintCommand());
|
|
1719
|
+
program.addCommand(scanCommand());
|
|
1720
|
+
program.addCommand(rulesCommand());
|
|
1721
|
+
program.addCommand(initCommand());
|
|
1722
|
+
program.addCommand(searchCommand());
|
|
1723
|
+
program.addCommand(infoCommand());
|
|
1724
|
+
program.addCommand(addCommand());
|
|
1725
|
+
program.addCommand(listCommand());
|
|
1726
|
+
program.addCommand(removeCommand());
|
|
1727
|
+
program.addCommand(updateCommand());
|
|
1728
|
+
program.addCommand(publishCommand());
|
|
1729
|
+
program.addCommand(loginCommand());
|
|
1730
|
+
program.addCommand(logoutCommand());
|
|
1731
|
+
if (process.argv.slice(2).length === 0) {
|
|
1732
|
+
if (process.stdout.isTTY) {
|
|
1733
|
+
await runInteractive();
|
|
1734
|
+
process.exit(process.exitCode ?? 0);
|
|
1735
|
+
}
|
|
1736
|
+
program.outputHelp();
|
|
1737
|
+
process.exit(0);
|
|
1738
|
+
}
|
|
1739
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1740
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1741
|
+
process.exitCode = 1;
|
|
1742
|
+
});
|