skillmds 1.0.17 → 1.0.19
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 +9 -0
- package/bin/skillmd-mcp.mjs +63 -13
- package/dist/cli.js +104 -17
- package/package.json +1 -1
- package/vendor/index.js +10 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.0.19
|
|
6
|
+
|
|
7
|
+
- **Clear diagnostics when the registry is blocked.** In sandboxed or proxied environments that deny network egress to `api.skillmd.com`, `skillmd add owner/name` used to fall back to GitHub and surface a misleading "repo not found" error. The CLI now tells 404 ("not in the registry" — GitHub fallback proceeds as before) apart from unreachable (DNS failure, proxy 403, 5xx): if the GitHub fallback also fails, the error names the blocked registry and says what fixes it (allowlist `api.skillmd.com`, or install via the SkillMD MCP server); if the fallback succeeds, a warning notes the copy came from GitHub, not the registry.
|
|
8
|
+
|
|
9
|
+
## 1.0.18
|
|
10
|
+
|
|
11
|
+
- **Cleaner tool output for agents.** `skillmd_search`, `skillmd_trending`, and `skillmd_recommend` no longer echo site-oriented fields (`avg_rating`, `rating_count`, `install_count`, `save_count`, `verified`, `type`, `owner_avatar`, `category_slug`, and the internal `confident` ranker flag). Results now carry just what an agent needs to pick a skill: slug, title, description, category, agents, repo stars, and the install snippet.
|
|
12
|
+
- **Quality score no longer punishes ordinary skills.** The security flags (`reads_secrets`, `network_calls`, `executes_scripts`) are pattern matches against prose and code samples, not proof of anything unsafe — they now cost 2/1/5 points respectively (was 25/8/15), so a skill that merely documents an env var or a URL keeps its high score. Verified skills are floored at 85 (unless the content fails to parse). `skillmd_lint` passes a skill's verified status through to the score.
|
|
13
|
+
|
|
5
14
|
## 1.0.16
|
|
6
15
|
|
|
7
16
|
- **Installs are no longer gated on verification.** Any skill installs directly — verified or not — with no flag, no prompt, and no second step, from both the CLI (`skillmd add`) and the MCP `skillmd_install` tool. Security flags (network_calls, executes_scripts, reads_secrets, docs_only) are still shown on the result as information, but never block. `--deny <flag>` remains available as an opt-in for anyone who wants to exclude a flag; `--allow-unverified` is now a no-op kept for backward compatibility.
|
package/bin/skillmd-mcp.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
// SkillMD MCP server (stdio). Wraps the public SkillMD API so any MCP-capable agent
|
|
3
3
|
// can search, fetch, install, and discover Agent Skills by name or task.
|
|
4
4
|
//
|
|
@@ -13,6 +13,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
13
13
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
14
14
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
16
17
|
import { homedir } from "node:os";
|
|
17
18
|
import { join, resolve, dirname, sep } from "node:path";
|
|
18
19
|
import { stringify as stringifyYaml } from "yaml";
|
|
@@ -36,6 +37,19 @@ try {
|
|
|
36
37
|
const API = process.env.SKILLMD_API || "https://api.skillmd.com";
|
|
37
38
|
const TOKEN = process.env.SKILLMD_TOKEN || "";
|
|
38
39
|
|
|
40
|
+
// Companion files not in R2 carry a registry-supplied source_url. Ingest only
|
|
41
|
+
// pins these to GitHub raw hosts, so anything else is a hostile/compromised
|
|
42
|
+
// registry response — refuse to fetch it (client-side SSRF guard).
|
|
43
|
+
const ALLOWED_SOURCE_HOSTS = new Set(["raw.githubusercontent.com", "gist.githubusercontent.com"]);
|
|
44
|
+
function isAllowedSourceUrl(u) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = new URL(u);
|
|
47
|
+
return parsed.protocol === "https:" && ALLOWED_SOURCE_HOSTS.has(parsed.hostname.toLowerCase());
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
async function api(path, init = {}) {
|
|
40
54
|
const headers = { accept: "application/json", ...(init.headers || {}) };
|
|
41
55
|
if (TOKEN) headers.authorization = `Bearer ${TOKEN}`;
|
|
@@ -45,10 +59,27 @@ async function api(path, init = {}) {
|
|
|
45
59
|
}
|
|
46
60
|
const text = (obj) => ({ content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }] });
|
|
47
61
|
|
|
62
|
+
// The public API returns site-oriented fields (ratings, counters, avatars,
|
|
63
|
+
// ranker internals) that are noise for an agent picking a skill. Strip them
|
|
64
|
+
// from every list-returning tool so results stay compact and rankable only
|
|
65
|
+
// on what matters to the caller.
|
|
66
|
+
const OMIT_FIELDS = new Set([
|
|
67
|
+
"type",
|
|
68
|
+
"verified",
|
|
69
|
+
"avg_rating",
|
|
70
|
+
"rating_count",
|
|
71
|
+
"install_count",
|
|
72
|
+
"save_count",
|
|
73
|
+
"owner_avatar",
|
|
74
|
+
"confident",
|
|
75
|
+
"category_slug",
|
|
76
|
+
]);
|
|
77
|
+
const trimItems = (items) => (items ?? []).map((s) => Object.fromEntries(Object.entries(s).filter(([k]) => !OMIT_FIELDS.has(k))));
|
|
78
|
+
|
|
48
79
|
const TOOLS = [
|
|
49
80
|
{
|
|
50
81
|
name: "skillmd_search",
|
|
51
|
-
description: "Search SkillMD for Agent Skills by name, description, or task. Returns matching skills with
|
|
82
|
+
description: "Search SkillMD for Agent Skills by name, description, or task. Returns matching skills with their owner/name slug and install snippets.",
|
|
52
83
|
inputSchema: {
|
|
53
84
|
type: "object",
|
|
54
85
|
properties: {
|
|
@@ -138,7 +169,7 @@ async function handleCall(name, args) {
|
|
|
138
169
|
if (args.type) p.set("type", args.type);
|
|
139
170
|
if (args.min_rating != null) p.set("min_rating", String(args.min_rating));
|
|
140
171
|
const r = await api(`/api/search?${p}`);
|
|
141
|
-
return text(r.items
|
|
172
|
+
return text(trimItems(r.items));
|
|
142
173
|
}
|
|
143
174
|
case "skillmd_get": {
|
|
144
175
|
const [owner, slugName] = String(args.slug).split("/");
|
|
@@ -149,7 +180,7 @@ async function handleCall(name, args) {
|
|
|
149
180
|
const p = new URLSearchParams({ range: args.range ?? "all", limit: String(args.limit ?? 20) });
|
|
150
181
|
if (args.category) p.set("category", args.category);
|
|
151
182
|
const r = await api(`/api/leaderboard?${p}`);
|
|
152
|
-
return text(r.items
|
|
183
|
+
return text(trimItems(r.items));
|
|
153
184
|
}
|
|
154
185
|
case "skillmd_recommend": {
|
|
155
186
|
if (args.based_on) {
|
|
@@ -157,11 +188,11 @@ async function handleCall(name, args) {
|
|
|
157
188
|
const base = await api(`/api/skills/${owner}/${slugName}`).catch(() => null);
|
|
158
189
|
if (base?.category_slug) {
|
|
159
190
|
const r = await api(`/api/leaderboard?range=all&limit=${args.limit ?? 10}&category=${base.category_slug}`);
|
|
160
|
-
return text((r.items
|
|
191
|
+
return text(trimItems(r.items).filter((s) => s.slug !== args.based_on));
|
|
161
192
|
}
|
|
162
193
|
}
|
|
163
194
|
const r = await api(`/api/leaderboard?range=30d&limit=${args.limit ?? 10}`);
|
|
164
|
-
return text(r.items
|
|
195
|
+
return text(trimItems(r.items));
|
|
165
196
|
}
|
|
166
197
|
case "skillmd_list_saved": {
|
|
167
198
|
if (!TOKEN) return text("Set SKILLMD_TOKEN (a SkillMD personal access token) to list your saved skills.");
|
|
@@ -172,14 +203,16 @@ async function handleCall(name, args) {
|
|
|
172
203
|
if (!lint) return text("Validation engine unavailable — run `npm run build` in the skillmds package.");
|
|
173
204
|
let raw = args.content;
|
|
174
205
|
let slug;
|
|
206
|
+
let verified = false;
|
|
175
207
|
if (!raw && args.slug) {
|
|
176
208
|
const [owner, slugName] = String(args.slug).split("/");
|
|
177
209
|
const skill = await api(`/api/skills/${owner}/${slugName}`);
|
|
178
210
|
raw = reconstructSkillMd(skill);
|
|
179
211
|
slug = slugName;
|
|
212
|
+
verified = !!skill.verified;
|
|
180
213
|
}
|
|
181
214
|
if (!raw) return text("Provide `content` (raw SKILL.md) or `slug` (owner/name).");
|
|
182
|
-
const result = lint(raw, slug ? { slug } : {});
|
|
215
|
+
const result = lint(raw, { ...(slug ? { slug } : {}), verified });
|
|
183
216
|
return text({ ok: result.ok, score: result.score, diagnostics: result.diagnostics, security: result.security });
|
|
184
217
|
}
|
|
185
218
|
case "skillmd_install": {
|
|
@@ -222,20 +255,37 @@ async function handleCall(name, args) {
|
|
|
222
255
|
// bundle avoids adding an unzip dependency to this standalone binary.
|
|
223
256
|
try {
|
|
224
257
|
const bundle = await api(`/api/skills/${owner}/${slugName}/bundle?format=json`);
|
|
258
|
+
// Resolve + integrity-check every file BEFORE writing any, so a
|
|
259
|
+
// tampered pack refuses cleanly instead of leaving a partial install.
|
|
260
|
+
const resolved = [];
|
|
225
261
|
for (const f of bundle.files ?? []) {
|
|
226
262
|
if (f.path.includes("..") || f.path.startsWith("/")) continue;
|
|
227
263
|
let buf;
|
|
228
|
-
if (f.content_base64 != null)
|
|
229
|
-
|
|
264
|
+
if (f.content_base64 != null) {
|
|
265
|
+
buf = Buffer.from(f.content_base64, "base64");
|
|
266
|
+
// R2 files are content-addressed; verify the bytes match the
|
|
267
|
+
// registry's sha256. Files without one (the reconstructed inline
|
|
268
|
+
// SKILL.md) carry no hash and are skipped.
|
|
269
|
+
if (f.sha256) {
|
|
270
|
+
const got = createHash("sha256").update(buf).digest("hex");
|
|
271
|
+
if (got !== f.sha256) {
|
|
272
|
+
const err = new Error(`integrity check failed for "${f.path}"`);
|
|
273
|
+
err.integrity = true;
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} else if (f.source_url && isAllowedSourceUrl(f.source_url)) {
|
|
230
278
|
const r = await fetch(f.source_url).catch(() => null);
|
|
231
279
|
if (!r || !r.ok) continue;
|
|
232
280
|
buf = Buffer.from(await r.arrayBuffer());
|
|
233
281
|
} else continue;
|
|
234
|
-
|
|
282
|
+
resolved.push({ path: f.path, contents: f.path === "SKILL.md" ? raw : buf });
|
|
235
283
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
284
|
+
for (const f of resolved) await writeSafe(f.path, f.contents);
|
|
285
|
+
written = resolved.length || 1;
|
|
286
|
+
if (!resolved.some((f) => f.path === "SKILL.md")) await writeSafe("SKILL.md", raw);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
if (e && e.integrity) return text(`Refused: ${args.slug} failed integrity verification — ${e.message}. Not installed.`);
|
|
239
289
|
await writeSafe("SKILL.md", raw);
|
|
240
290
|
note = ` NOTE: pack assets could not be fetched — only SKILL.md was written. For the full pack run: npx skillmds add ${args.slug}`;
|
|
241
291
|
}
|
package/dist/cli.js
CHANGED
|
@@ -163,16 +163,19 @@ var WEIGHT = {
|
|
|
163
163
|
};
|
|
164
164
|
var RISK = {
|
|
165
165
|
docs_only: 0,
|
|
166
|
-
reads_secrets:
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
reads_secrets: 2,
|
|
167
|
+
network_calls: 1,
|
|
168
|
+
executes_scripts: 5
|
|
169
169
|
};
|
|
170
|
-
|
|
170
|
+
var VERIFIED_FLOOR = 85;
|
|
171
|
+
function qualityScore(diagnostics, security, opts = {}) {
|
|
171
172
|
let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
172
173
|
if (security) {
|
|
173
174
|
for (const flag of security.flags) penalty += RISK[flag] ?? 0;
|
|
174
175
|
}
|
|
175
|
-
|
|
176
|
+
const score = Math.max(0, Math.min(100, 100 - penalty));
|
|
177
|
+
const hasError = diagnostics.some((d) => d.severity === "error");
|
|
178
|
+
return opts.verified && !hasError ? Math.max(score, VERIFIED_FLOOR) : score;
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
// ../../packages/core/src/lint.ts
|
|
@@ -185,14 +188,14 @@ function lint(raw, opts = {}) {
|
|
|
185
188
|
ok: false,
|
|
186
189
|
diagnostics: diagnostics2,
|
|
187
190
|
security: security2,
|
|
188
|
-
score: qualityScore(diagnostics2, security2)
|
|
191
|
+
score: qualityScore(diagnostics2, security2, { verified: opts.verified })
|
|
189
192
|
};
|
|
190
193
|
}
|
|
191
194
|
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
192
195
|
const diagnostics = runRules(parsed, ctx);
|
|
193
196
|
const security = scanSecurity(parsed.body);
|
|
194
197
|
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
195
|
-
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security) };
|
|
198
|
+
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security, { verified: opts.verified }) };
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
// ../../packages/core/src/format/text.ts
|
|
@@ -736,6 +739,7 @@ import { Command as Command5 } from "commander";
|
|
|
736
739
|
import pc5 from "picocolors";
|
|
737
740
|
|
|
738
741
|
// ../cli/src/api.ts
|
|
742
|
+
import { createHash } from "crypto";
|
|
739
743
|
import { stringify as stringifyYaml } from "yaml";
|
|
740
744
|
|
|
741
745
|
// ../cli/src/config.ts
|
|
@@ -766,16 +770,46 @@ function resolveApi(flags = {}) {
|
|
|
766
770
|
}
|
|
767
771
|
|
|
768
772
|
// ../cli/src/api.ts
|
|
773
|
+
var IntegrityError = class extends Error {
|
|
774
|
+
integrity = true;
|
|
775
|
+
constructor(message) {
|
|
776
|
+
super(message);
|
|
777
|
+
this.name = "IntegrityError";
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
var RegistryError = class extends Error {
|
|
781
|
+
constructor(message, status) {
|
|
782
|
+
super(message);
|
|
783
|
+
this.status = status;
|
|
784
|
+
this.name = "RegistryError";
|
|
785
|
+
}
|
|
786
|
+
status;
|
|
787
|
+
};
|
|
788
|
+
var ALLOWED_SOURCE_HOSTS = /* @__PURE__ */ new Set(["raw.githubusercontent.com", "gist.githubusercontent.com"]);
|
|
789
|
+
function isAllowedSourceUrl(u) {
|
|
790
|
+
try {
|
|
791
|
+
const parsed = new URL(u);
|
|
792
|
+
return parsed.protocol === "https:" && ALLOWED_SOURCE_HOSTS.has(parsed.hostname.toLowerCase());
|
|
793
|
+
} catch {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
769
797
|
function createClient(flags = {}) {
|
|
770
798
|
const base = resolveApi(flags);
|
|
771
799
|
const token = resolveToken(flags);
|
|
772
800
|
async function api(path, init = {}) {
|
|
773
801
|
const headers = { accept: "application/json", ...init.headers };
|
|
774
802
|
if (token) headers.authorization = `Bearer ${token}`;
|
|
775
|
-
|
|
803
|
+
let res;
|
|
804
|
+
try {
|
|
805
|
+
res = await fetch(base + path, { ...init, headers });
|
|
806
|
+
} catch (e) {
|
|
807
|
+
const cause = e instanceof Error ? e.cause?.message ?? e.message : String(e);
|
|
808
|
+
throw new RegistryError(`could not reach ${base}: ${cause}`);
|
|
809
|
+
}
|
|
776
810
|
if (!res.ok) {
|
|
777
811
|
const detail = await res.text().catch(() => "");
|
|
778
|
-
throw new
|
|
812
|
+
throw new RegistryError(`SkillMD API ${res.status} on ${path}: ${detail}`, res.status);
|
|
779
813
|
}
|
|
780
814
|
return await res.json();
|
|
781
815
|
}
|
|
@@ -791,8 +825,15 @@ async function fetchBundle(base, slug, token) {
|
|
|
791
825
|
const out = [];
|
|
792
826
|
for (const f of data.files) {
|
|
793
827
|
if (f.content_base64 != null) {
|
|
794
|
-
|
|
795
|
-
|
|
828
|
+
const contents = Buffer.from(f.content_base64, "base64");
|
|
829
|
+
if (f.sha256) {
|
|
830
|
+
const got = createHash("sha256").update(contents).digest("hex");
|
|
831
|
+
if (got !== f.sha256) {
|
|
832
|
+
throw new IntegrityError(`integrity check failed for "${f.path}" (expected ${f.sha256.slice(0, 12)}\u2026, got ${got.slice(0, 12)}\u2026)`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
out.push({ path: f.path, contents });
|
|
836
|
+
} else if (f.source_url && isAllowedSourceUrl(f.source_url)) {
|
|
796
837
|
const r = await fetch(f.source_url).catch(() => null);
|
|
797
838
|
if (r && r.ok) out.push({ path: f.path, contents: Buffer.from(await r.arrayBuffer()) });
|
|
798
839
|
}
|
|
@@ -937,6 +978,9 @@ function detectAgents(opts = {}) {
|
|
|
937
978
|
return AGENTS.filter((a) => existsSync3(agentDir(a.id, opts))).map((a) => a.id);
|
|
938
979
|
}
|
|
939
980
|
function writeSkill(skillName, files, targetDir, opts = {}) {
|
|
981
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(skillName) || skillName.includes("..")) {
|
|
982
|
+
throw new Error(`refused unsafe skill name: "${skillName}"`);
|
|
983
|
+
}
|
|
940
984
|
const dest = join4(targetDir, skillName);
|
|
941
985
|
if (opts.mode === "symlink" && opts.canonicalDir) {
|
|
942
986
|
mkdirSync3(dirname2(dest), { recursive: true });
|
|
@@ -989,12 +1033,31 @@ async function resolvePackFiles(skill, fetchTree = resolveTree) {
|
|
|
989
1033
|
}
|
|
990
1034
|
return null;
|
|
991
1035
|
}
|
|
1036
|
+
function registryFallbackNote(host) {
|
|
1037
|
+
return `installed from GitHub because the SkillMD registry (${host}) was unreachable \u2014 this copy is not the registry-reviewed version`;
|
|
1038
|
+
}
|
|
1039
|
+
function registryUnreachableError(arg, host, e) {
|
|
1040
|
+
const why = e.status ? `HTTP ${e.status}` : "network error";
|
|
1041
|
+
return new Error(
|
|
1042
|
+
`could not reach the SkillMD registry at ${host} (${why}), and "${arg}" was not found on GitHub either.
|
|
1043
|
+
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.
|
|
1044
|
+
Registry error: ${e.message}`
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
992
1047
|
var defaultDeps = {
|
|
993
1048
|
async resolve(arg, flags) {
|
|
994
1049
|
if (isLocalish(arg)) {
|
|
995
|
-
const
|
|
996
|
-
return
|
|
1050
|
+
const rs = await resolveSource(arg);
|
|
1051
|
+
return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
|
|
997
1052
|
}
|
|
1053
|
+
let registryDown = null;
|
|
1054
|
+
const registryHost = (() => {
|
|
1055
|
+
try {
|
|
1056
|
+
return new URL(createClient(flags).base).host;
|
|
1057
|
+
} catch {
|
|
1058
|
+
return "api.skillmd.com";
|
|
1059
|
+
}
|
|
1060
|
+
})();
|
|
998
1061
|
if (/^[^/\s:]+\/[^/\s:]+$/.test(arg)) {
|
|
999
1062
|
const { api, base: apiBase, token } = createClient(flags);
|
|
1000
1063
|
const [owner, name] = arg.split("/");
|
|
@@ -1019,11 +1082,19 @@ var defaultDeps = {
|
|
|
1019
1082
|
if (pack) return [{ ...meta, ...pack }];
|
|
1020
1083
|
}
|
|
1021
1084
|
return [{ ...meta, raw: skillMdFor(skill) }];
|
|
1022
|
-
} catch {
|
|
1085
|
+
} catch (e) {
|
|
1086
|
+
if (e instanceof IntegrityError) throw e;
|
|
1087
|
+
if (e instanceof RegistryError && e.status !== 404) registryDown = e;
|
|
1023
1088
|
}
|
|
1024
1089
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1090
|
+
try {
|
|
1091
|
+
const rs = await resolveSource(arg);
|
|
1092
|
+
const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
|
|
1093
|
+
return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, note: note2 }));
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
|
|
1096
|
+
throw e;
|
|
1097
|
+
}
|
|
1027
1098
|
},
|
|
1028
1099
|
fireInstall(registrySlug, flags) {
|
|
1029
1100
|
const { api } = createClient(flags);
|
|
@@ -1037,7 +1108,20 @@ var defaultDeps = {
|
|
|
1037
1108
|
}
|
|
1038
1109
|
};
|
|
1039
1110
|
async function runAdd(arg, flags, deps = defaultDeps) {
|
|
1040
|
-
let candidates
|
|
1111
|
+
let candidates;
|
|
1112
|
+
try {
|
|
1113
|
+
candidates = await deps.resolve(arg, flags);
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
if (e instanceof IntegrityError) {
|
|
1116
|
+
return {
|
|
1117
|
+
written: [],
|
|
1118
|
+
blocked: [{ name: arg, reason: e.message }],
|
|
1119
|
+
exitCode: 1,
|
|
1120
|
+
output: pc7.red(`\u2717 ${arg} blocked \u2014 ${e.message}`)
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
throw e;
|
|
1124
|
+
}
|
|
1041
1125
|
if (flags.skill?.length) candidates = candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
|
|
1042
1126
|
const deny = new Set(flags.deny ?? []);
|
|
1043
1127
|
const written = [];
|
|
@@ -1069,6 +1153,9 @@ async function runAdd(arg, flags, deps = defaultDeps) {
|
|
|
1069
1153
|
if (c.pinMiss) {
|
|
1070
1154
|
lines.push(pc7.yellow(`\u26A0 ${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
|
|
1071
1155
|
}
|
|
1156
|
+
if (c.note) {
|
|
1157
|
+
lines.push(pc7.yellow(`\u26A0 ${c.name}: ${c.note}`));
|
|
1158
|
+
}
|
|
1072
1159
|
if (c.type === "pack" && !c.files) {
|
|
1073
1160
|
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.`));
|
|
1074
1161
|
}
|
package/package.json
CHANGED
package/vendor/index.js
CHANGED
|
@@ -169,16 +169,19 @@ var WEIGHT = {
|
|
|
169
169
|
};
|
|
170
170
|
var RISK = {
|
|
171
171
|
docs_only: 0,
|
|
172
|
-
reads_secrets:
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
reads_secrets: 2,
|
|
173
|
+
network_calls: 1,
|
|
174
|
+
executes_scripts: 5
|
|
175
175
|
};
|
|
176
|
-
|
|
176
|
+
var VERIFIED_FLOOR = 85;
|
|
177
|
+
function qualityScore(diagnostics, security, opts = {}) {
|
|
177
178
|
let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
178
179
|
if (security) {
|
|
179
180
|
for (const flag of security.flags) penalty += RISK[flag] ?? 0;
|
|
180
181
|
}
|
|
181
|
-
|
|
182
|
+
const score = Math.max(0, Math.min(100, 100 - penalty));
|
|
183
|
+
const hasError = diagnostics.some((d) => d.severity === "error");
|
|
184
|
+
return opts.verified && !hasError ? Math.max(score, VERIFIED_FLOOR) : score;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
// ../../packages/core/src/lint.ts
|
|
@@ -191,14 +194,14 @@ function lint(raw, opts = {}) {
|
|
|
191
194
|
ok: false,
|
|
192
195
|
diagnostics: diagnostics2,
|
|
193
196
|
security: security2,
|
|
194
|
-
score: qualityScore(diagnostics2, security2)
|
|
197
|
+
score: qualityScore(diagnostics2, security2, { verified: opts.verified })
|
|
195
198
|
};
|
|
196
199
|
}
|
|
197
200
|
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
198
201
|
const diagnostics = runRules(parsed, ctx);
|
|
199
202
|
const security = scanSecurity(parsed.body);
|
|
200
203
|
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
201
|
-
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security) };
|
|
204
|
+
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security, { verified: opts.verified }) };
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
// ../../packages/core/src/format/text.ts
|