skillmds 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,104 +1,113 @@
1
- # Changelog
2
-
3
- All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
4
-
5
- ## 1.0.18
6
-
7
- - **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.
8
- - **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.
9
-
10
- ## 1.0.16
11
-
12
- - **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.
13
-
14
- ## 1.0.15
15
-
16
- - Search is now **browsable in the interactive menu**: arrow through results, press Enter on one to see a card, and install it to your project or user directory — all without leaving the CLI (`← Back` at each level; Esc goes up one step, never quits).
17
- - Search results are clean two-line entries showing the source repo's **star count** (e.g. `★ 223.7k`) instead of the placeholder rating, with the description truncated to fit your terminal.
18
- - Fixes for narrow terminals: result rows are width-budgeted so they don't wrap and garble the menu; the result card prints once instead of repainting on every action.
19
-
20
- ## 1.0.14
21
-
22
- - Interactive menu (`skillmds` in a terminal) no longer exits after one action — it returns to the menu until you pick **Quit**; cancelling a sub-prompt goes back to the menu instead of quitting.
23
- - "List installed skills" now shows project **and** global skills together, tagged `[agent/scope]`, replacing the misleading "Include global skills?" prompt (which actually toggled global-only vs project-only). Invalid skills show the parse reason.
24
- - The in-menu command reference now includes `info`.
25
-
26
- ## 1.0.13
27
-
28
- Safe & easy installs release.
29
-
30
- - **Packs install their full file tree.** `skillmd add owner/pack` now downloads the pack's source directory (pinned to the registry's commit; falls back to the default branch with a warning if the pin is gone upstream), so `assets/` and `references/` files referenced by SKILL.md actually exist. Hardened: symlinks are skipped, 200-file/20MB caps, temp dirs cleaned up.
31
- - **Unverified skills prompt instead of hard-blocking.** Interactive installs show the lint score and security flags and ask before installing; non-interactive installs (and `-y`) still require the explicit `--allow-unverified`. Blocked output links the skill's page for review.
32
- - **New `skillmd info <slug>`** shows verification status, security flags, rating, provenance (source repo @ commit), and the install snippet. Unknown subcommands now print a proper error instead of silently starting the MCP server.
33
- - **MCP `skillmd_install` gained the CLI's safety gates**: refuses unverified skills unless `allow_unverified: true` (only after explicit human approval), validates before writing, reports security flags, and guards against malformed skill names.
34
- - **Installs are exact.** The registry now stores the published SKILL.md verbatim (`raw_md`) and installs deliver it byte-for-byte; reconstructed frontmatter (legacy rows) is YAML-quoted so descriptions with `:`/`#`/quotes can't corrupt the file.
35
- - Description limit aligned with Claude Code's real spec (1024 chars, was 500) — valid skills are no longer flagged "(invalid SKILL.md)" by `skillmd list`, and `list` now says *why* a file is invalid.
36
-
37
- ## 1.0.12
38
-
39
- - Registry-native `skillmd` CLI shipped inside `skillmds`: `search`, `add`, `list`, `remove`, `update`, `publish`, `login`/`logout`, plus lint/scan/rules/init. (Release notes added retroactively.)
40
-
41
- ## 1.0.11
42
-
43
- - Security flags render with full readable labels, each with its own marker: `⚑ Executes Scripts ⚑ Calls Network ⚑ Reads Secrets`.
44
-
45
- ## 1.0.10
46
-
47
- - Quality score is now a composite of spec correctness, completeness, and **security risk** — skills that read secrets, execute scripts, or make network calls score lower (a risky/stub skill is no longer a green 100).
48
- - Lint output ends with a **Failed** section listing each failure's rule, message, and full path, so problems don't scroll off in a large run.
49
- - New `--errors-only` flag to show only failing skills.
50
- - Fixed emoji spacing/overlap; rule ids are grouped by severity per skill.
51
- - The closer states how many skills failed instead of a vague "Problems found".
52
-
53
- ## 1.0.9
54
-
55
- - Compact lint output: one line per skill (status dot, score, name, rule-id chips) with a single summary line, instead of a tall multi-row block per skill. Linting a single skill still shows full diagnostic messages.
56
-
57
- ## 1.0.8
58
-
59
- - `skillmds` with no command in a terminal now opens the interactive CLI menu; it only starts the MCP server when launched over piped stdio (how MCP clients run it). Previously a bare `npx skillmds` in a terminal appeared to hang on the silent MCP server.
60
-
61
- ## 1.0.7
62
-
63
- - Nicer CLI output: colored PASS/FAIL, quality-score bars, `ERROR`/`WARN`/`INFO` badges, and a summary with average score.
64
- - Running `skillmd` (or `skillmds`) with no command on a terminal now opens a guided interactive menu (lint, scan, search, init, list). Piped/CI still prints help.
65
- - Progress spinner while scanning.
66
-
67
- ## 1.0.6
68
-
69
- - Bundle the banner image in the package and serve it via jsDelivr so it renders on the npm page (the previous absolute URL was unreachable).
70
-
71
- ## 1.0.5
72
-
73
- - The `skillmds` binary now dispatches: `skillmds <command>` (lint, scan, add, …) runs the CLI, while `skillmds` with no command starts the MCP server. This makes `npx skillmds lint .` work without the `-p` flag.
74
-
75
- ## 1.0.4
76
-
77
- - `skillmd lint`/`scan` print a progress line to stderr and warn when scanning a home directory, so a directory scan never looks like a frozen blank screen.
78
- - Directory scans skip large build/OS directories (`AppData`, `node_modules`, `target`, …) and cap recursion depth, so `lint .` in a big tree terminates quickly.
79
- - Clearer message when no `SKILL.md` is found.
80
- - Bare `skillmd` (no command) prints help.
81
-
82
- ## 1.0.3
83
-
84
- - CLI (`skillmd`) and MCP server (`skillmds`) now report their version from `package.json` instead of a hardcoded string.
85
- - READMEs rewritten; added this changelog.
86
-
87
- ## 1.0.2
88
-
89
- - `skillmd lint`: skip directories that cannot be read (e.g. `EPERM` on protected system folders) instead of aborting the scan.
90
-
91
- ## 1.0.1
92
-
93
- - Fix `skillmd_lint` in the MCP server: keep `yaml` external in the bundled engine so it loads at runtime (bundling a CommonJS module into ESM raised `Dynamic require of "process"`).
94
-
95
- ## 1.0.0
96
-
97
- - Add the `skillmd` CLI, shipped alongside the MCP server in this package. Commands: `lint`/`check`, `scan`, `rules`, `init`, `search`, `add`, `list`, `remove`, `update`, `publish`/`submit`, `login`, `logout`.
98
- - Validation engine shared with the SkillMD registry: `SKILL.md` parser, lint rules `SK001`–`SK040`, security scan, quality score, and `text`/`json`/`sarif`/`github` output.
99
- - Add the `skillmd_lint` MCP tool.
100
- - MCP server and CLI authenticate with `Authorization: Bearer` tokens.
101
-
102
- ## 0.1.1
103
-
104
- - Initial MCP server: `skillmd_search`, `skillmd_get`, `skillmd_install`, `skillmd_trending`, `skillmd_recommend`, `skillmd_list_saved`.
1
+ # Changelog
2
+
3
+ All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
4
+
5
+ ## 1.0.20
6
+
7
+ - **`skillmd add` now installs to every agent on your machine.** Detection is machine-level (an agent's config dir under your home, e.g. `~/.cursor`), so agents no longer need a pre-existing `skills/` folder to be targeted. The agent registry grows from 5 to **60+**: Gemini CLI, Antigravity, Kiro, Warp, GitHub Copilot, Cline, Roo Code, Goose, Trae, Qwen Code, Amp, OpenClaw, Kilo Code, Hermes (Nous Research), Continue, Devin, OpenHands, Replit, Mistral Vibe, Zed, Kimi Code, Droid (Factory), Crush, Augment, Junie, and more — with per-agent dirs matching each agent's own convention (`~/.codeium/windsurf/skills` for Windsurf, `~/.gemini/antigravity/skills` for Antigravity, etc.). Agents sharing the cross-agent `.agents/skills` convention are written once and labelled with every agent they serve. `-a/--agent` still overrides.
8
+ - Running from your home directory now behaves like `--global` (project scope rooted at `$HOME` *is* the global scope), so agents whose global skills live outside `~/<agent>/skills` get the right directory.
9
+
10
+ ## 1.0.19
11
+
12
+ - **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.
13
+
14
+ ## 1.0.18
15
+
16
+ - **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.
17
+ - **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.
18
+
19
+ ## 1.0.16
20
+
21
+ - **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.
22
+
23
+ ## 1.0.15
24
+
25
+ - Search is now **browsable in the interactive menu**: arrow through results, press Enter on one to see a card, and install it to your project or user directory — all without leaving the CLI (`← Back` at each level; Esc goes up one step, never quits).
26
+ - Search results are clean two-line entries showing the source repo's **star count** (e.g. `★ 223.7k`) instead of the placeholder rating, with the description truncated to fit your terminal.
27
+ - Fixes for narrow terminals: result rows are width-budgeted so they don't wrap and garble the menu; the result card prints once instead of repainting on every action.
28
+
29
+ ## 1.0.14
30
+
31
+ - Interactive menu (`skillmds` in a terminal) no longer exits after one action — it returns to the menu until you pick **Quit**; cancelling a sub-prompt goes back to the menu instead of quitting.
32
+ - "List installed skills" now shows project **and** global skills together, tagged `[agent/scope]`, replacing the misleading "Include global skills?" prompt (which actually toggled global-only vs project-only). Invalid skills show the parse reason.
33
+ - The in-menu command reference now includes `info`.
34
+
35
+ ## 1.0.13
36
+
37
+ Safe & easy installs release.
38
+
39
+ - **Packs install their full file tree.** `skillmd add owner/pack` now downloads the pack's source directory (pinned to the registry's commit; falls back to the default branch with a warning if the pin is gone upstream), so `assets/` and `references/` files referenced by SKILL.md actually exist. Hardened: symlinks are skipped, 200-file/20MB caps, temp dirs cleaned up.
40
+ - **Unverified skills prompt instead of hard-blocking.** Interactive installs show the lint score and security flags and ask before installing; non-interactive installs (and `-y`) still require the explicit `--allow-unverified`. Blocked output links the skill's page for review.
41
+ - **New `skillmd info <slug>`** shows verification status, security flags, rating, provenance (source repo @ commit), and the install snippet. Unknown subcommands now print a proper error instead of silently starting the MCP server.
42
+ - **MCP `skillmd_install` gained the CLI's safety gates**: refuses unverified skills unless `allow_unverified: true` (only after explicit human approval), validates before writing, reports security flags, and guards against malformed skill names.
43
+ - **Installs are exact.** The registry now stores the published SKILL.md verbatim (`raw_md`) and installs deliver it byte-for-byte; reconstructed frontmatter (legacy rows) is YAML-quoted so descriptions with `:`/`#`/quotes can't corrupt the file.
44
+ - Description limit aligned with Claude Code's real spec (1024 chars, was 500) — valid skills are no longer flagged "(invalid SKILL.md)" by `skillmd list`, and `list` now says *why* a file is invalid.
45
+
46
+ ## 1.0.12
47
+
48
+ - Registry-native `skillmd` CLI shipped inside `skillmds`: `search`, `add`, `list`, `remove`, `update`, `publish`, `login`/`logout`, plus lint/scan/rules/init. (Release notes added retroactively.)
49
+
50
+ ## 1.0.11
51
+
52
+ - Security flags render with full readable labels, each with its own marker: `⚑ Executes Scripts ⚑ Calls Network ⚑ Reads Secrets`.
53
+
54
+ ## 1.0.10
55
+
56
+ - Quality score is now a composite of spec correctness, completeness, and **security risk** — skills that read secrets, execute scripts, or make network calls score lower (a risky/stub skill is no longer a green 100).
57
+ - Lint output ends with a **Failed** section listing each failure's rule, message, and full path, so problems don't scroll off in a large run.
58
+ - New `--errors-only` flag to show only failing skills.
59
+ - Fixed emoji spacing/overlap; rule ids are grouped by severity per skill.
60
+ - The closer states how many skills failed instead of a vague "Problems found".
61
+
62
+ ## 1.0.9
63
+
64
+ - Compact lint output: one line per skill (status dot, score, name, rule-id chips) with a single summary line, instead of a tall multi-row block per skill. Linting a single skill still shows full diagnostic messages.
65
+
66
+ ## 1.0.8
67
+
68
+ - `skillmds` with no command in a terminal now opens the interactive CLI menu; it only starts the MCP server when launched over piped stdio (how MCP clients run it). Previously a bare `npx skillmds` in a terminal appeared to hang on the silent MCP server.
69
+
70
+ ## 1.0.7
71
+
72
+ - Nicer CLI output: colored PASS/FAIL, quality-score bars, `ERROR`/`WARN`/`INFO` badges, and a summary with average score.
73
+ - Running `skillmd` (or `skillmds`) with no command on a terminal now opens a guided interactive menu (lint, scan, search, init, list). Piped/CI still prints help.
74
+ - Progress spinner while scanning.
75
+
76
+ ## 1.0.6
77
+
78
+ - Bundle the banner image in the package and serve it via jsDelivr so it renders on the npm page (the previous absolute URL was unreachable).
79
+
80
+ ## 1.0.5
81
+
82
+ - The `skillmds` binary now dispatches: `skillmds <command>` (lint, scan, add, …) runs the CLI, while `skillmds` with no command starts the MCP server. This makes `npx skillmds lint .` work without the `-p` flag.
83
+
84
+ ## 1.0.4
85
+
86
+ - `skillmd lint`/`scan` print a progress line to stderr and warn when scanning a home directory, so a directory scan never looks like a frozen blank screen.
87
+ - Directory scans skip large build/OS directories (`AppData`, `node_modules`, `target`, …) and cap recursion depth, so `lint .` in a big tree terminates quickly.
88
+ - Clearer message when no `SKILL.md` is found.
89
+ - Bare `skillmd` (no command) prints help.
90
+
91
+ ## 1.0.3
92
+
93
+ - CLI (`skillmd`) and MCP server (`skillmds`) now report their version from `package.json` instead of a hardcoded string.
94
+ - READMEs rewritten; added this changelog.
95
+
96
+ ## 1.0.2
97
+
98
+ - `skillmd lint`: skip directories that cannot be read (e.g. `EPERM` on protected system folders) instead of aborting the scan.
99
+
100
+ ## 1.0.1
101
+
102
+ - Fix `skillmd_lint` in the MCP server: keep `yaml` external in the bundled engine so it loads at runtime (bundling a CommonJS module into ESM raised `Dynamic require of "process"`).
103
+
104
+ ## 1.0.0
105
+
106
+ - Add the `skillmd` CLI, shipped alongside the MCP server in this package. Commands: `lint`/`check`, `scan`, `rules`, `init`, `search`, `add`, `list`, `remove`, `update`, `publish`/`submit`, `login`, `logout`.
107
+ - Validation engine shared with the SkillMD registry: `SKILL.md` parser, lint rules `SK001`–`SK040`, security scan, quality score, and `text`/`json`/`sarif`/`github` output.
108
+ - Add the `skillmd_lint` MCP tool.
109
+ - MCP server and CLI authenticate with `Authorization: Bearer` tokens.
110
+
111
+ ## 0.1.1
112
+
113
+ - Initial MCP server: `skillmd_search`, `skillmd_get`, `skillmd_install`, `skillmd_trending`, `skillmd_recommend`, `skillmd_list_saved`.
@@ -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}`;
@@ -241,20 +255,37 @@ async function handleCall(name, args) {
241
255
  // bundle avoids adding an unzip dependency to this standalone binary.
242
256
  try {
243
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 = [];
244
261
  for (const f of bundle.files ?? []) {
245
262
  if (f.path.includes("..") || f.path.startsWith("/")) continue;
246
263
  let buf;
247
- if (f.content_base64 != null) buf = Buffer.from(f.content_base64, "base64");
248
- else if (f.source_url) {
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)) {
249
278
  const r = await fetch(f.source_url).catch(() => null);
250
279
  if (!r || !r.ok) continue;
251
280
  buf = Buffer.from(await r.arrayBuffer());
252
281
  } else continue;
253
- await writeSafe(f.path, f.path === "SKILL.md" ? raw : buf);
282
+ resolved.push({ path: f.path, contents: f.path === "SKILL.md" ? raw : buf });
254
283
  }
255
- written = (bundle.files ?? []).length || 1;
256
- if (!(bundle.files ?? []).some((f) => f.path === "SKILL.md")) await writeSafe("SKILL.md", raw);
257
- } catch {
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.`);
258
289
  await writeSafe("SKILL.md", raw);
259
290
  note = ` NOTE: pack assets could not be fetched — only SKILL.md was written. For the full pack run: npx skillmds add ${args.slug}`;
260
291
  }
package/dist/cli.js CHANGED
@@ -41,7 +41,26 @@ function parseSkillMd(raw, type = "single") {
41
41
  }
42
42
 
43
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
+ );
44
62
  var RULES = [
63
+ ["untrusted_install", UNTRUSTED_INSTALL_RE],
45
64
  ["executes_scripts", /```(?:sh|bash|python|py|js|ts|ruby|rb)\b|\b(subprocess|os\.system|child_process|exec\()/i],
46
65
  ["network_calls", /\b(fetch|requests\.|urllib|axios|curl|wget|https?:\/\/|websocket)\b/i],
47
66
  ["reads_secrets", /\b(api[_-]?key|secret|token|password|os\.environ|process\.env|getenv)\b/i]
@@ -165,7 +184,10 @@ var RISK = {
165
184
  docs_only: 0,
166
185
  reads_secrets: 2,
167
186
  network_calls: 1,
168
- executes_scripts: 5
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
169
191
  };
170
192
  var VERIFIED_FLOOR = 85;
171
193
  function qualityScore(diagnostics, security, opts = {}) {
@@ -739,6 +761,7 @@ import { Command as Command5 } from "commander";
739
761
  import pc5 from "picocolors";
740
762
 
741
763
  // ../cli/src/api.ts
764
+ import { createHash } from "crypto";
742
765
  import { stringify as stringifyYaml } from "yaml";
743
766
 
744
767
  // ../cli/src/config.ts
@@ -769,16 +792,46 @@ function resolveApi(flags = {}) {
769
792
  }
770
793
 
771
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
+ }
772
819
  function createClient(flags = {}) {
773
820
  const base = resolveApi(flags);
774
821
  const token = resolveToken(flags);
775
822
  async function api(path, init = {}) {
776
823
  const headers = { accept: "application/json", ...init.headers };
777
824
  if (token) headers.authorization = `Bearer ${token}`;
778
- const res = await fetch(base + path, { ...init, headers });
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
+ }
779
832
  if (!res.ok) {
780
833
  const detail = await res.text().catch(() => "");
781
- throw new Error(`SkillMD API ${res.status} on ${path}: ${detail}`);
834
+ throw new RegistryError(`SkillMD API ${res.status} on ${path}: ${detail}`, res.status);
782
835
  }
783
836
  return await res.json();
784
837
  }
@@ -794,8 +847,15 @@ async function fetchBundle(base, slug, token) {
794
847
  const out = [];
795
848
  for (const f of data.files) {
796
849
  if (f.content_base64 != null) {
797
- out.push({ path: f.path, contents: Buffer.from(f.content_base64, "base64") });
798
- } else if (f.source_url) {
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)) {
799
859
  const r = await fetch(f.source_url).catch(() => null);
800
860
  if (r && r.ok) out.push({ path: f.path, contents: Buffer.from(await r.arrayBuffer()) });
801
861
  }
@@ -917,6 +977,8 @@ function infoCommand() {
917
977
  // ../cli/src/commands/add.ts
918
978
  import { Command as Command7, Option } from "commander";
919
979
  import { existsSync as existsSync4 } from "fs";
980
+ import { homedir as homedir4 } from "os";
981
+ import { resolve as resolve3 } from "path";
920
982
  import pc7 from "picocolors";
921
983
 
922
984
  // ../cli/src/agents.ts
@@ -924,20 +986,84 @@ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync as rmS
924
986
  import { homedir as homedir3 } from "os";
925
987
  import { join as join4, resolve as resolve2, dirname as dirname2, sep as sep2 } from "path";
926
988
  var AGENTS = [
927
- { id: "claude-code", segments: [".claude", "skills"] },
928
- { id: "cursor", segments: [".cursor", "skills"] },
929
- { id: "codex", segments: [".codex", "skills"] },
930
- { id: "windsurf", segments: [".windsurf", "skills"] },
931
- { id: "opencode", segments: [".opencode", "skills"] }
989
+ { id: "claude-code", project: [".claude", "skills"], global: [".claude", "skills"], detect: [[".claude"]] },
990
+ { id: "cursor", project: [".cursor", "skills"], global: [".cursor", "skills"], detect: [[".cursor"]] },
991
+ { id: "codex", project: [".codex", "skills"], global: [".codex", "skills"], detect: [[".codex"]] },
992
+ { id: "windsurf", project: [".windsurf", "skills"], global: [".codeium", "windsurf", "skills"], detect: [[".codeium", "windsurf"], [".windsurf"]] },
993
+ { id: "opencode", project: [".opencode", "skills"], global: [".config", "opencode", "skills"], detect: [[".config", "opencode"], [".opencode"]] },
994
+ { id: "gemini-cli", project: [".agents", "skills"], global: [".gemini", "skills"], detect: [[".gemini"]] },
995
+ { id: "antigravity", project: [".agents", "skills"], global: [".gemini", "antigravity", "skills"], detect: [[".gemini", "antigravity"], [".antigravity"]] },
996
+ { id: "kiro", project: [".kiro", "skills"], global: [".kiro", "skills"], detect: [[".kiro"]] },
997
+ { id: "warp", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".warp"]] },
998
+ { id: "github-copilot", project: [".agents", "skills"], global: [".copilot", "skills"], detect: [[".copilot"]] },
999
+ { id: "cline", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".cline"]] },
1000
+ { id: "roo", project: [".roo", "skills"], global: [".roo", "skills"], detect: [[".roo"]] },
1001
+ { id: "goose", project: [".goose", "skills"], global: [".config", "goose", "skills"], detect: [[".config", "goose"]] },
1002
+ { id: "trae", project: [".trae", "skills"], global: [".trae", "skills"], detect: [[".trae"]] },
1003
+ { id: "qwen-code", project: [".qwen", "skills"], global: [".qwen", "skills"], detect: [[".qwen"]] },
1004
+ { id: "amp", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".config", "amp"]] },
1005
+ // — extended registry —
1006
+ { id: "openclaw", project: ["skills"], global: [".openclaw", "skills"], detect: [[".openclaw"], [".clawdbot"], [".moltbot"]] },
1007
+ { id: "kilo", project: [".kilocode", "skills"], global: [".kilocode", "skills"], detect: [[".kilocode"]] },
1008
+ { id: "hermes-agent", project: [".hermes", "skills"], global: [".hermes", "skills"], detect: [[".hermes"]] },
1009
+ { id: "continue", project: [".continue", "skills"], global: [".continue", "skills"], detect: [[".continue"]] },
1010
+ { id: "devin", project: [".devin", "skills"], global: [".config", "devin", "skills"], detect: [[".config", "devin"]] },
1011
+ { id: "openhands", project: [".openhands", "skills"], global: [".openhands", "skills"], detect: [[".openhands"]] },
1012
+ { id: "replit", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".replit"]] },
1013
+ { id: "mistral-vibe", project: [".vibe", "skills"], global: [".vibe", "skills"], detect: [[".vibe"]] },
1014
+ { id: "zed", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".config", "zed"]] },
1015
+ { id: "adal", project: [".adal", "skills"], global: [".adal", "skills"], detect: [[".adal"]] },
1016
+ { id: "aider-desk", project: [".aider-desk", "skills"], global: [".aider-desk", "skills"], detect: [[".aider-desk"]] },
1017
+ { id: "antigravity-cli", project: [".agents", "skills"], global: [".gemini", "antigravity-cli", "skills"], detect: [[".gemini", "antigravity-cli"]] },
1018
+ { id: "augment", project: [".augment", "skills"], global: [".augment", "skills"], detect: [[".augment"]] },
1019
+ { id: "autohand-code", project: [".autohand", "skills"], global: [".autohand", "skills"], detect: [[".autohand"]] },
1020
+ { id: "bob", project: [".bob", "skills"], global: [".bob", "skills"], detect: [[".bob"]] },
1021
+ { id: "codearts-agent", project: [".codeartsdoer", "skills"], global: [".codeartsdoer", "skills"], detect: [[".codeartsdoer"]] },
1022
+ { id: "codebuddy", project: [".codebuddy", "skills"], global: [".codebuddy", "skills"], detect: [[".codebuddy"]] },
1023
+ { id: "codemaker", project: [".codemaker", "skills"], global: [".codemaker", "skills"], detect: [[".codemaker"]] },
1024
+ { id: "codestudio", project: [".codestudio", "skills"], global: [".codestudio", "skills"], detect: [[".codestudio"]] },
1025
+ { id: "command-code", project: [".commandcode", "skills"], global: [".commandcode", "skills"], detect: [[".commandcode"]] },
1026
+ { id: "cortex", project: [".cortex", "skills"], global: [".snowflake", "cortex", "skills"], detect: [[".snowflake", "cortex"]] },
1027
+ { id: "crush", project: [".crush", "skills"], global: [".config", "crush", "skills"], detect: [[".config", "crush"]] },
1028
+ { id: "deepagents", project: [".agents", "skills"], global: [".deepagents", "agent", "skills"], detect: [[".deepagents"]] },
1029
+ { id: "dexto", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".dexto"]] },
1030
+ { id: "droid", project: [".factory", "skills"], global: [".factory", "skills"], detect: [[".factory"]] },
1031
+ { id: "firebender", project: [".agents", "skills"], global: [".firebender", "skills"], detect: [[".firebender"]] },
1032
+ { id: "forgecode", project: [".forge", "skills"], global: [".forge", "skills"], detect: [[".forge"]] },
1033
+ { id: "iflow-cli", project: [".iflow", "skills"], global: [".iflow", "skills"], detect: [[".iflow"]] },
1034
+ { id: "inference-sh", project: [".inferencesh", "skills"], global: [".inferencesh", "skills"], detect: [[".inferencesh"]] },
1035
+ { id: "jazz", project: [".jazz", "skills"], global: [".jazz", "skills"], detect: [[".jazz"]] },
1036
+ { id: "junie", project: [".junie", "skills"], global: [".junie", "skills"], detect: [[".junie"]] },
1037
+ { id: "kimi-code-cli", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".kimi-code"], [".kimi"]] },
1038
+ { id: "kode", project: [".kode", "skills"], global: [".kode", "skills"], detect: [[".kode"]] },
1039
+ { id: "lingma", project: [".lingma", "skills"], global: [".lingma", "skills"], detect: [[".lingma"]] },
1040
+ { id: "loaf", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".loaf"]] },
1041
+ { id: "mcpjam", project: [".mcpjam", "skills"], global: [".mcpjam", "skills"], detect: [[".mcpjam"]] },
1042
+ { id: "moxby", project: [".moxby", "skills"], global: [".moxby", "skills"], detect: [[".moxby"]] },
1043
+ { id: "mux", project: [".mux", "skills"], global: [".mux", "skills"], detect: [[".mux"]] },
1044
+ { id: "neovate", project: [".neovate", "skills"], global: [".neovate", "skills"], detect: [[".neovate"]] },
1045
+ { id: "ona", project: [".ona", "skills"], global: [".ona", "skills"], detect: [[".ona"]] },
1046
+ { id: "pi", project: [".pi", "skills"], global: [".pi", "agent", "skills"], detect: [[".pi", "agent"]] },
1047
+ { id: "pochi", project: [".pochi", "skills"], global: [".pochi", "skills"], detect: [[".pochi"]] },
1048
+ { id: "qoder", project: [".qoder", "skills"], global: [".qoder", "skills"], detect: [[".qoder"]] },
1049
+ { id: "qoder-cn", project: [".qoder", "skills"], global: [".qoder-cn", "skills"], detect: [[".qoder-cn"]] },
1050
+ { id: "reasonix", project: [".reasonix", "skills"], global: [".reasonix", "skills"], detect: [[".reasonix"]] },
1051
+ { id: "rovodev", project: [".rovodev", "skills"], global: [".rovodev", "skills"], detect: [[".rovodev"]] },
1052
+ { id: "tabnine-cli", project: [".tabnine", "agent", "skills"], global: [".tabnine", "agent", "skills"], detect: [[".tabnine"]] },
1053
+ { id: "terramind", project: [".terramind", "skills"], global: [".terramind", "skills"], detect: [[".terramind"]] },
1054
+ { id: "tinycloud", project: [".tinycloud", "skills"], global: [".tinycloud", "skills"], detect: [[".tinycloud"]] },
1055
+ { id: "trae-cn", project: [".trae", "skills"], global: [".trae-cn", "skills"], detect: [[".trae-cn"]] },
1056
+ { id: "zcode", project: [".zcode", "skills"], global: [".zcode", "skills"], detect: [[".zcode"]] },
1057
+ { id: "zencoder", project: [".zencoder", "skills"], global: [".zencoder", "skills"], detect: [[".zencoder"]] }
932
1058
  ];
933
1059
  function agentDir(agentId, opts = {}) {
934
1060
  const agent = AGENTS.find((a) => a.id === agentId);
935
1061
  if (!agent) throw new Error(`Unknown agent: ${agentId}. Known: ${AGENTS.map((a) => a.id).join(", ")}`);
936
- const root = opts.global ? homedir3() : opts.cwd ?? process.cwd();
937
- return join4(root, ...agent.segments);
1062
+ return opts.global ? join4(opts.home ?? homedir3(), ...agent.global) : join4(opts.cwd ?? process.cwd(), ...agent.project);
938
1063
  }
939
1064
  function detectAgents(opts = {}) {
940
- return AGENTS.filter((a) => existsSync3(agentDir(a.id, opts))).map((a) => a.id);
1065
+ const home = opts.home ?? homedir3();
1066
+ return AGENTS.filter((a) => a.detect.some((d) => existsSync3(join4(home, ...d)))).map((a) => a.id);
941
1067
  }
942
1068
  function writeSkill(skillName, files, targetDir, opts = {}) {
943
1069
  if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(skillName) || skillName.includes("..")) {
@@ -965,8 +1091,11 @@ function writeSkill(skillName, files, targetDir, opts = {}) {
965
1091
  function installedSkills(opts = {}) {
966
1092
  const scope = opts.global ? "global" : "project";
967
1093
  const out = [];
1094
+ const seen = /* @__PURE__ */ new Set();
968
1095
  for (const agent of AGENTS) {
969
1096
  const dir = agentDir(agent.id, opts);
1097
+ if (seen.has(dir)) continue;
1098
+ seen.add(dir);
970
1099
  if (!existsSync3(dir)) continue;
971
1100
  for (const entry of readdirSync2(dir)) {
972
1101
  const skillMd = join4(dir, entry, "SKILL.md");
@@ -995,12 +1124,31 @@ async function resolvePackFiles(skill, fetchTree = resolveTree) {
995
1124
  }
996
1125
  return null;
997
1126
  }
1127
+ function registryFallbackNote(host) {
1128
+ return `installed from GitHub because the SkillMD registry (${host}) was unreachable \u2014 this copy is not the registry-reviewed version`;
1129
+ }
1130
+ function registryUnreachableError(arg, host, e) {
1131
+ const why = e.status ? `HTTP ${e.status}` : "network error";
1132
+ return new Error(
1133
+ `could not reach the SkillMD registry at ${host} (${why}), and "${arg}" was not found on GitHub either.
1134
+ 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.
1135
+ Registry error: ${e.message}`
1136
+ );
1137
+ }
998
1138
  var defaultDeps = {
999
1139
  async resolve(arg, flags) {
1000
1140
  if (isLocalish(arg)) {
1001
- const rs2 = await resolveSource(arg);
1002
- return rs2.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
1141
+ const rs = await resolveSource(arg);
1142
+ return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
1003
1143
  }
1144
+ let registryDown = null;
1145
+ const registryHost = (() => {
1146
+ try {
1147
+ return new URL(createClient(flags).base).host;
1148
+ } catch {
1149
+ return "api.skillmd.com";
1150
+ }
1151
+ })();
1004
1152
  if (/^[^/\s:]+\/[^/\s:]+$/.test(arg)) {
1005
1153
  const { api, base: apiBase, token } = createClient(flags);
1006
1154
  const [owner, name] = arg.split("/");
@@ -1025,11 +1173,19 @@ var defaultDeps = {
1025
1173
  if (pack) return [{ ...meta, ...pack }];
1026
1174
  }
1027
1175
  return [{ ...meta, raw: skillMdFor(skill) }];
1028
- } catch {
1176
+ } catch (e) {
1177
+ if (e instanceof IntegrityError) throw e;
1178
+ if (e instanceof RegistryError && e.status !== 404) registryDown = e;
1029
1179
  }
1030
1180
  }
1031
- const rs = await resolveSource(arg);
1032
- return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
1181
+ try {
1182
+ const rs = await resolveSource(arg);
1183
+ const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
1184
+ return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, note: note2 }));
1185
+ } catch (e) {
1186
+ if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
1187
+ throw e;
1188
+ }
1033
1189
  },
1034
1190
  fireInstall(registrySlug, flags) {
1035
1191
  const { api } = createClient(flags);
@@ -1043,14 +1199,35 @@ var defaultDeps = {
1043
1199
  }
1044
1200
  };
1045
1201
  async function runAdd(arg, flags, deps = defaultDeps) {
1046
- let candidates = await deps.resolve(arg, flags);
1202
+ let candidates;
1203
+ try {
1204
+ candidates = await deps.resolve(arg, flags);
1205
+ } catch (e) {
1206
+ if (e instanceof IntegrityError) {
1207
+ return {
1208
+ written: [],
1209
+ blocked: [{ name: arg, reason: e.message }],
1210
+ exitCode: 1,
1211
+ output: pc7.red(`\u2717 ${arg} blocked \u2014 ${e.message}`)
1212
+ };
1213
+ }
1214
+ throw e;
1215
+ }
1047
1216
  if (flags.skill?.length) candidates = candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
1048
1217
  const deny = new Set(flags.deny ?? []);
1049
1218
  const written = [];
1050
1219
  const blocked = [];
1051
1220
  const lines = [];
1052
- const scope = { global: flags.global, cwd: flags.cwd };
1053
- const targets = flags.agent?.length ? flags.agent : detectAgents(scope).length ? detectAgents(scope) : ["claude-code"];
1221
+ const home = flags.home ?? homedir4();
1222
+ const isGlobal = flags.global || resolve3(flags.cwd ?? process.cwd()) === resolve3(home);
1223
+ const scope = { global: isGlobal, cwd: flags.cwd, home: flags.home };
1224
+ const detected = detectAgents(scope);
1225
+ const targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
1226
+ const dirAgents = /* @__PURE__ */ new Map();
1227
+ for (const agentId of targets) {
1228
+ const dir = agentDir(agentId, scope);
1229
+ dirAgents.set(dir, [...dirAgents.get(dir) ?? [], agentId]);
1230
+ }
1054
1231
  for (const c of candidates) {
1055
1232
  const result = lint(c.raw, { slug: c.slug });
1056
1233
  const allFlags = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
@@ -1066,15 +1243,17 @@ async function runAdd(arg, flags, deps = defaultDeps) {
1066
1243
  continue;
1067
1244
  }
1068
1245
  const files = c.files ?? [{ path: "SKILL.md", contents: c.raw }];
1069
- for (const agentId of targets) {
1070
- const dir = agentDir(agentId, scope);
1246
+ for (const [dir, agentIds] of dirAgents) {
1071
1247
  const out = writeSkill(c.name, files, dir, { mode: "copy" });
1072
1248
  written.push({ name: c.name, dir: out });
1073
- lines.push(pc7.green(`\u2713 ${c.name} \u2192 ${out}`) + pc7.dim(` (score ${result.score}${allFlags.length ? `; ${allFlags.join(", ")}` : ""})`));
1249
+ lines.push(pc7.green(`\u2713 ${c.name} \u2192 ${out}`) + pc7.dim(` (${agentIds.join(", ")}; score ${result.score}${allFlags.length ? `; ${allFlags.join(", ")}` : ""})`));
1074
1250
  }
1075
1251
  if (c.pinMiss) {
1076
1252
  lines.push(pc7.yellow(`\u26A0 ${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
1077
1253
  }
1254
+ if (c.note) {
1255
+ lines.push(pc7.yellow(`\u26A0 ${c.name}: ${c.note}`));
1256
+ }
1078
1257
  if (c.type === "pack" && !c.files) {
1079
1258
  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.`));
1080
1259
  }
@@ -1084,7 +1263,7 @@ async function runAdd(arg, flags, deps = defaultDeps) {
1084
1263
  return { written, blocked, exitCode, output: lines.join("\n") };
1085
1264
  }
1086
1265
  function addCommand() {
1087
- return new Command7("add").description("Install a skill from the registry, a GitHub repo, or a local path (lints before writing)").argument("<source>", "registry slug (owner/name), GitHub source, or local path").option("-g, --global", "install to the user directory instead of the project").option("-a, --agent <agents...>", "target specific agents").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--copy", "copy files (default)", true).option("-y, --yes", "skip confirmation prompts").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) => {
1266
+ return new Command7("add").description("Install a skill from the registry, a GitHub repo, or a local path (lints before writing)").argument("<source>", "registry slug (owner/name), GitHub source, or local path").option("-g, --global", "install to the user directory instead of the project").option("-a, --agent <agents...>", "target specific agents (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 confirmation prompts").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) => {
1088
1267
  const merged = { ...cmd.parent?.opts(), ...opts };
1089
1268
  const run = await runAdd(source, merged);
1090
1269
  console.log(run.output);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillmds",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "SkillMD for agents: the `skillmds` MCP server + the `skillmd` CLI (lint, scan, search, install, publish Agent Skills) from skillmd.com.",
5
5
  "type": "module",
6
6
  "bin": {
package/vendor/index.js CHANGED
@@ -47,7 +47,26 @@ function categorize(text) {
47
47
  }
48
48
 
49
49
  // ../../packages/core/src/security.ts
50
+ var UNTRUSTED_INSTALL_RE = new RegExp(
51
+ [
52
+ /(?:npx|npm|yarn|pnpm|bun)\b[^\n]*--registry[= ]/.source,
53
+ // custom npm registry
54
+ /npm\s+config\s+set\s+registry/.source,
55
+ /pip3?\s+install\b[^\n]*(?:--index-url|--extra-index-url|-i\s+https?:)/.source,
56
+ // custom pip index
57
+ /(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba|z|fi)?sh\b/.source,
58
+ // curl … | sh
59
+ /(?:iwr|irm|invoke-webrequest|invoke-restmethod)\b[^\n|]*\|\s*iex\b/.source,
60
+ // PowerShell pipe-to-exec
61
+ /bash\s+<\(\s*(?:curl|wget)/.source,
62
+ // bash <(curl …)
63
+ /(?: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
64
+ // install from raw archive URL
65
+ ].join("|"),
66
+ "i"
67
+ );
50
68
  var RULES = [
69
+ ["untrusted_install", UNTRUSTED_INSTALL_RE],
51
70
  ["executes_scripts", /```(?:sh|bash|python|py|js|ts|ruby|rb)\b|\b(subprocess|os\.system|child_process|exec\()/i],
52
71
  ["network_calls", /\b(fetch|requests\.|urllib|axios|curl|wget|https?:\/\/|websocket)\b/i],
53
72
  ["reads_secrets", /\b(api[_-]?key|secret|token|password|os\.environ|process\.env|getenv)\b/i]
@@ -171,7 +190,10 @@ var RISK = {
171
190
  docs_only: 0,
172
191
  reads_secrets: 2,
173
192
  network_calls: 1,
174
- executes_scripts: 5
193
+ executes_scripts: 5,
194
+ // fetch-and-execute from a non-standard source — the supply-chain pattern the
195
+ // AI verdict floors to "warning"; the heaviest penalty here for the same reason
196
+ untrusted_install: 15
175
197
  };
176
198
  var VERIFIED_FLOOR = 85;
177
199
  function qualityScore(diagnostics, security, opts = {}) {
@@ -296,6 +318,7 @@ export {
296
318
  MAX_NAME,
297
319
  MAX_SKILL_BYTES,
298
320
  RULES2 as RULES,
321
+ UNTRUSTED_INSTALL_RE,
299
322
  categorize,
300
323
  lint,
301
324
  parseSkillMd,