skillsio 1.1.2 → 1.1.3

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.
Files changed (3) hide show
  1. package/README.md +204 -6
  2. package/dist/cli.mjs +164 -141
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -3,6 +3,10 @@
3
3
  A security-hardened fork of the [skills](https://github.com/vercel-labs/skills) CLI that scans agent skills for
4
4
  malicious content before installation.
5
5
 
6
+ <!-- agent-list:start -->
7
+ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [37 more](#available-agents).
8
+ <!-- agent-list:end -->
9
+
6
10
  The open agent skills ecosystem makes it trivial to install third-party instruction sets into coding agents — but that
7
11
  same ease of installation is a vector for prompt injection, data exfiltration, and credential theft.
8
12
  [Snyk's analysis](https://snyk.io/blog/) of 3,984 published skills found that **13.4% had critical security issues** and
@@ -259,18 +263,19 @@ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [35 more](#su
259
263
  <!-- supported-agents:start -->
260
264
  | Agent | `--agent` | Project Path | Global Path |
261
265
  |-------|-----------|--------------|-------------|
262
- | Amp, Kimi Code CLI, Replit | `amp`, `kimi-cli`, `replit` | `.agents/skills/` | `~/.config/agents/skills/` |
266
+ | Amp, Kimi Code CLI, Replit, Universal | `amp`, `kimi-cli`, `replit`, `universal` | `.agents/skills/` | `~/.config/agents/skills/` |
263
267
  | Antigravity | `antigravity` | `.agent/skills/` | `~/.gemini/antigravity/skills/` |
264
268
  | Augment | `augment` | `.augment/skills/` | `~/.augment/skills/` |
265
269
  | Claude Code | `claude-code` | `.claude/skills/` | `~/.claude/skills/` |
266
- | OpenClaw | `openclaw` | `skills/` | `~/.moltbot/skills/` |
270
+ | OpenClaw | `openclaw` | `skills/` | `~/.openclaw/skills/` |
267
271
  | Cline | `cline` | `.cline/skills/` | `~/.cline/skills/` |
268
272
  | CodeBuddy | `codebuddy` | `.codebuddy/skills/` | `~/.codebuddy/skills/` |
269
273
  | Codex | `codex` | `.agents/skills/` | `~/.codex/skills/` |
270
274
  | Command Code | `command-code` | `.commandcode/skills/` | `~/.commandcode/skills/` |
271
275
  | Continue | `continue` | `.continue/skills/` | `~/.continue/skills/` |
276
+ | Cortex Code | `cortex` | `.cortex/skills/` | `~/.snowflake/cortex/skills/` |
272
277
  | Crush | `crush` | `.crush/skills/` | `~/.config/crush/skills/` |
273
- | Cursor | `cursor` | `.cursor/skills/` | `~/.cursor/skills/` |
278
+ | Cursor | `cursor` | `.agents/skills/` | `~/.cursor/skills/` |
274
279
  | Droid | `droid` | `.factory/skills/` | `~/.factory/skills/` |
275
280
  | Gemini CLI | `gemini-cli` | `.agents/skills/` | `~/.gemini/skills/` |
276
281
  | GitHub Copilot | `github-copilot` | `.agents/skills/` | `~/.copilot/skills/` |
@@ -298,7 +303,154 @@ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [35 more](#su
298
303
  | AdaL | `adal` | `.adal/skills/` | `~/.adal/skills/` |
299
304
  <!-- supported-agents:end -->
300
305
 
301
- The CLI automatically detects which coding agents you have installed.
306
+ > [!NOTE]
307
+ > **Kiro CLI users:** After installing skills, manually add them to your custom agent's `resources` in
308
+ > `.kiro/agents/<agent>.json`:
309
+ >
310
+ > ```json
311
+ > {
312
+ > "resources": ["skill://.kiro/skills/**/SKILL.md"]
313
+ > }
314
+ > ```
315
+
316
+ The CLI automatically detects which coding agents you have installed. If none are detected, you'll be prompted to select
317
+ which agents to install to.
318
+
319
+ ## Creating Skills
320
+
321
+ Skills are directories containing a `SKILL.md` file with YAML frontmatter:
322
+
323
+ ```markdown
324
+ ---
325
+ name: my-skill
326
+ description: What this skill does and when to use it
327
+ ---
328
+
329
+ # My Skill
330
+
331
+ Instructions for the agent to follow when this skill is activated.
332
+
333
+ ## When to Use
334
+
335
+ Describe the scenarios where this skill should be used.
336
+
337
+ ## Steps
338
+
339
+ 1. First, do this
340
+ 2. Then, do that
341
+ ```
342
+
343
+ ### Required Fields
344
+
345
+ - `name`: Unique identifier (lowercase, hyphens allowed)
346
+ - `description`: Brief explanation of what the skill does
347
+
348
+ ### Optional Fields
349
+
350
+ - `metadata.internal`: Set to `true` to hide the skill from normal discovery. Internal skills are only visible and
351
+ installable when `INSTALL_INTERNAL_SKILLS=1` is set. Useful for work-in-progress skills or skills meant only for
352
+ internal tooling.
353
+
354
+ ```markdown
355
+ ---
356
+ name: my-internal-skill
357
+ description: An internal skill not shown by default
358
+ metadata:
359
+ internal: true
360
+ ---
361
+ ```
362
+
363
+ ### Skill Discovery
364
+
365
+ The CLI searches for skills in these locations within a repository:
366
+
367
+ <!-- skill-discovery:start -->
368
+ - Root directory (if it contains `SKILL.md`)
369
+ - `skills/`
370
+ - `skills/.curated/`
371
+ - `skills/.experimental/`
372
+ - `skills/.system/`
373
+ - `.agents/skills/`
374
+ - `.agent/skills/`
375
+ - `.augment/skills/`
376
+ - `.claude/skills/`
377
+ - `./skills/`
378
+ - `.cline/skills/`
379
+ - `.codebuddy/skills/`
380
+ - `.commandcode/skills/`
381
+ - `.continue/skills/`
382
+ - `.cortex/skills/`
383
+ - `.crush/skills/`
384
+ - `.factory/skills/`
385
+ - `.goose/skills/`
386
+ - `.junie/skills/`
387
+ - `.iflow/skills/`
388
+ - `.kilocode/skills/`
389
+ - `.kiro/skills/`
390
+ - `.kode/skills/`
391
+ - `.mcpjam/skills/`
392
+ - `.vibe/skills/`
393
+ - `.mux/skills/`
394
+ - `.openhands/skills/`
395
+ - `.pi/skills/`
396
+ - `.qoder/skills/`
397
+ - `.qwen/skills/`
398
+ - `.roo/skills/`
399
+ - `.trae/skills/`
400
+ - `.windsurf/skills/`
401
+ - `.zencoder/skills/`
402
+ - `.neovate/skills/`
403
+ - `.pochi/skills/`
404
+ - `.adal/skills/`
405
+ <!-- skill-discovery:end -->
406
+
407
+ ### Plugin Manifest Discovery
408
+
409
+ If `.claude-plugin/marketplace.json` or `.claude-plugin/plugin.json` exists, skills declared in those files are also discovered:
410
+
411
+ ```json
412
+ // .claude-plugin/marketplace.json
413
+ {
414
+ "metadata": { "pluginRoot": "./plugins" },
415
+ "plugins": [{
416
+ "name": "my-plugin",
417
+ "source": "my-plugin",
418
+ "skills": ["./skills/review", "./skills/test"]
419
+ }]
420
+ }
421
+ ```
422
+
423
+ This enables compatibility with the [Claude Code plugin marketplace](https://code.claude.com/docs/en/plugin-marketplaces) ecosystem.
424
+
425
+ If no skills are found in standard locations, a recursive search is performed.
426
+
427
+ ## Compatibility
428
+
429
+ Skills are generally compatible across agents since they follow a
430
+ shared [Agent Skills specification](https://agentskills.io). However, some features may be agent-specific:
431
+
432
+ | Feature | OpenCode | OpenHands | Claude Code | Cline | CodeBuddy | Codex | Command Code | Kiro CLI | Cursor | Antigravity | Roo Code | Github Copilot | Amp | OpenClaw | Neovate | Pi | Qoder | Zencoder |
433
+ | --------------- | -------- | --------- | ----------- | ----- | --------- | ----- | ------------ | -------- | ------ | ----------- | -------- | -------------- | --- | -------- | ------- | --- | ----- | -------- |
434
+ | Basic skills | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
435
+ | `allowed-tools` | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No |
436
+ | `context: fork` | No | No | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No |
437
+ | Hooks | No | No | Yes | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No | No |
438
+
439
+ ## Troubleshooting
440
+
441
+ ### "No skills found"
442
+
443
+ Ensure the repository contains valid `SKILL.md` files with both `name` and `description` in the frontmatter.
444
+
445
+ ### Skill not loading in agent
446
+
447
+ - Verify the skill was installed to the correct path
448
+ - Check the agent's documentation for skill loading requirements
449
+ - Ensure the `SKILL.md` frontmatter is valid YAML
450
+
451
+ ### Permission errors
452
+
453
+ Ensure you have write access to the target directory.
302
454
 
303
455
  ## Environment Variables
304
456
 
@@ -337,12 +489,27 @@ pnpm format # Format code with Prettier
337
489
 
338
490
  ## Changelog
339
491
 
492
+ ### 1.1.3
493
+
494
+ - **Synced with upstream** ([vercel-labs/skills](https://github.com/vercel-labs/skills)): universal agent support
495
+ (`.agents/skills/` as a single install target symlinked across agents), new agents (Cortex Code, and others), Kiro CLI
496
+ note, Creating Skills docs, Compatibility table, Troubleshooting section, agent-list badge
497
+ - **Replaced HTML scraping with structured API**: third-party audit now uses the `add-skill.vercel.sh/audit` JSON
498
+ endpoint instead of scraping `skills.sh` HTML — more reliable and richer data (risk levels + alert counts per auditor)
499
+ - **Stronger blocking**: critical and high risk from the API both always prompt for confirmation; `--yes` is ignored for
500
+ both (previously high was bypassed by `--yes` and critical only prompted rather than blocked)
501
+ - **Audit failure warning**: if the skills.sh API is unreachable, a yellow warning is shown rather than silently
502
+ skipping — local scan still runs regardless
503
+ - Removed duplicate audit display (previously showed both an inline scan note and a separate "Security Risk Assessments"
504
+ panel for the same data)
505
+
340
506
  ### 1.1.2
341
507
 
342
508
  - **skills.sh audit integration**: for GitHub-sourced skills, the CLI now fetches third-party audit results from
343
509
  [skills.sh](https://skills.sh) (Snyk, Socket, Gen Agent Trust Hub) and displays them alongside local scan output
344
- - A skills.sh Fail verdict from any auditor escalates severity to at least High, triggering a confirmation prompt
345
- - Lookups run in parallel with VirusTotal and fail silently on any network or parse error
510
+ - Critical or High risk from any auditor escalates the install gate critical always prompts even with `--yes`, high
511
+ blocks unless `--yes` is set; uses the structured skills.sh JSON API instead of HTML scraping
512
+ - Audit runs in parallel with VirusTotal and fails silently on any network or parse error
346
513
 
347
514
  ### 1.1.1
348
515
 
@@ -373,6 +540,37 @@ pnpm format # Format code with Prettier
373
540
  - URL transparency: all external URLs in skill files are shown before installation
374
541
  - Scanner rules informed by Snyk and ClawHavoc research
375
542
 
543
+ ## Links
544
+
545
+ - [Agent Skills Specification](https://agentskills.io)
546
+ - [Skills Directory](https://skills.sh)
547
+ - [Amp Skills Documentation](https://ampcode.com/manual#agent-skills)
548
+ - [Antigravity Skills Documentation](https://antigravity.google/docs/skills)
549
+ - [Factory AI / Droid Skills Documentation](https://docs.factory.ai/cli/configuration/skills)
550
+ - [Claude Code Skills Documentation](https://code.claude.com/docs/en/skills)
551
+ - [OpenClaw Skills Documentation](https://docs.openclaw.ai/tools/skills)
552
+ - [Cline Skills Documentation](https://docs.cline.bot/features/skills)
553
+ - [CodeBuddy Skills Documentation](https://www.codebuddy.ai/docs/ide/Features/Skills)
554
+ - [Codex Skills Documentation](https://developers.openai.com/codex/skills)
555
+ - [Command Code Skills Documentation](https://commandcode.ai/docs/skills)
556
+ - [Crush Skills Documentation](https://github.com/charmbracelet/crush?tab=readme-ov-file#agent-skills)
557
+ - [Cursor Skills Documentation](https://cursor.com/docs/context/skills)
558
+ - [Gemini CLI Skills Documentation](https://geminicli.com/docs/cli/skills/)
559
+ - [GitHub Copilot Agent Skills](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills)
560
+ - [iFlow CLI Skills Documentation](https://platform.iflow.cn/en/cli/examples/skill)
561
+ - [Kimi Code CLI Skills Documentation](https://moonshotai.github.io/kimi-cli/en/customization/skills.html)
562
+ - [Kiro CLI Skills Documentation](https://kiro.dev/docs/cli/custom-agents/configuration-reference/#skill-resources)
563
+ - [Kode Skills Documentation](https://github.com/shareAI-lab/kode/blob/main/docs/skills.md)
564
+ - [OpenCode Skills Documentation](https://opencode.ai/docs/skills)
565
+ - [Qwen Code Skills Documentation](https://qwenlm.github.io/qwen-code-docs/en/users/features/skills/)
566
+ - [OpenHands Skills Documentation](https://docs.openhands.ai/modules/usage/how-to/using-skills)
567
+ - [Pi Skills Documentation](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/skills.md)
568
+ - [Qoder Skills Documentation](https://docs.qoder.com/cli/Skills)
569
+ - [Replit Skills Documentation](https://docs.replit.com/replitai/skills)
570
+ - [Roo Code Skills Documentation](https://docs.roocode.com/features/skills)
571
+ - [Trae Skills Documentation](https://docs.trae.ai/ide/skills)
572
+ - [Vercel Agent Skills Repository](https://github.com/vercel-labs/agent-skills)
573
+
376
574
  ## Research
377
575
 
378
576
  The scanner rules are informed by the following research into malicious agent skills:
package/dist/cli.mjs CHANGED
@@ -29,14 +29,6 @@ function getOwnerRepo(parsed) {
29
29
  } catch {}
30
30
  return null;
31
31
  }
32
- function parseOwnerRepo(ownerRepo) {
33
- const match = ownerRepo.match(/^([^/]+)\/([^/]+)$/);
34
- if (match) return {
35
- owner: match[1],
36
- repo: match[2]
37
- };
38
- return null;
39
- }
40
32
  async function isRepoPrivate(owner, repo) {
41
33
  try {
42
34
  const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
@@ -58,7 +50,10 @@ function isDirectSkillUrl(input) {
58
50
  if (input.includes("gitlab.com/") && !input.includes("/-/raw/")) return false;
59
51
  return true;
60
52
  }
53
+ const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills" };
61
54
  function parseSource(input) {
55
+ const alias = SOURCE_ALIASES[input];
56
+ if (alias) input = alias;
62
57
  if (isLocalPath(input)) {
63
58
  const resolvedPath = resolve(input);
64
59
  return {
@@ -177,7 +172,8 @@ const S_STEP_CANCEL = import_picocolors.default.red("■");
177
172
  const S_STEP_SUBMIT = import_picocolors.default.green("◇");
178
173
  const S_RADIO_ACTIVE = import_picocolors.default.green("●");
179
174
  const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
180
- const S_CHECKBOX_LOCKED = import_picocolors.default.green("✓");
175
+ import_picocolors.default.green("✓");
176
+ const S_BULLET = import_picocolors.default.green("•");
181
177
  const S_BAR = import_picocolors.default.dim("│");
182
178
  const S_BAR_H = import_picocolors.default.dim("─");
183
179
  const cancelSymbol = Symbol("cancel");
@@ -220,10 +216,11 @@ async function searchMultiselect(options) {
220
216
  if (state === "active") {
221
217
  if (lockedSection && lockedSection.items.length > 0) {
222
218
  lines.push(`${S_BAR}`);
223
- lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold(lockedSection.title)} ${S_BAR_H.repeat(30)}`);
224
- for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_CHECKBOX_LOCKED} ${item.label}`);
219
+ const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
220
+ lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
221
+ for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
225
222
  lines.push(`${S_BAR}`);
226
- lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Other agents")} ${S_BAR_H.repeat(34)}`);
223
+ lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
227
224
  }
228
225
  const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
229
226
  lines.push(searchLine);
@@ -436,6 +433,7 @@ async function parseSkillMd(skillMdPath, options) {
436
433
  const content = await readFile(skillMdPath, "utf-8");
437
434
  const { data } = (0, import_gray_matter.default)(content);
438
435
  if (!data.name || !data.description) return null;
436
+ if (typeof data.name !== "string" || typeof data.description !== "string") return null;
439
437
  if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
440
438
  return {
441
439
  name: data.name,
@@ -485,7 +483,6 @@ async function discoverSkills(basePath, subpath, options) {
485
483
  join(searchPath, ".codex/skills"),
486
484
  join(searchPath, ".commandcode/skills"),
487
485
  join(searchPath, ".continue/skills"),
488
- join(searchPath, ".cursor/skills"),
489
486
  join(searchPath, ".github/skills"),
490
487
  join(searchPath, ".goose/skills"),
491
488
  join(searchPath, ".iflow/skills"),
@@ -544,6 +541,12 @@ const home = homedir();
544
541
  const configHome = xdgConfig ?? join(home, ".config");
545
542
  const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
546
543
  const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
544
+ function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
545
+ if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills");
546
+ if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills");
547
+ if (pathExists(join(homeDir, ".moltbot"))) return join(homeDir, ".moltbot/skills");
548
+ return join(homeDir, ".openclaw/skills");
549
+ }
547
550
  const agents = {
548
551
  amp: {
549
552
  name: "amp",
@@ -585,7 +588,7 @@ const agents = {
585
588
  name: "openclaw",
586
589
  displayName: "OpenClaw",
587
590
  skillsDir: "skills",
588
- globalSkillsDir: existsSync(join(home, ".openclaw")) ? join(home, ".openclaw/skills") : existsSync(join(home, ".clawdbot")) ? join(home, ".clawdbot/skills") : join(home, ".moltbot/skills"),
591
+ globalSkillsDir: getOpenClawGlobalSkillsDir(),
589
592
  detectInstalled: async () => {
590
593
  return existsSync(join(home, ".openclaw")) || existsSync(join(home, ".clawdbot")) || existsSync(join(home, ".moltbot"));
591
594
  }
@@ -635,6 +638,15 @@ const agents = {
635
638
  return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"));
636
639
  }
637
640
  },
641
+ cortex: {
642
+ name: "cortex",
643
+ displayName: "Cortex Code",
644
+ skillsDir: ".cortex/skills",
645
+ globalSkillsDir: join(home, ".snowflake/cortex/skills"),
646
+ detectInstalled: async () => {
647
+ return existsSync(join(home, ".snowflake/cortex"));
648
+ }
649
+ },
638
650
  crush: {
639
651
  name: "crush",
640
652
  displayName: "Crush",
@@ -647,7 +659,7 @@ const agents = {
647
659
  cursor: {
648
660
  name: "cursor",
649
661
  displayName: "Cursor",
650
- skillsDir: ".cursor/skills",
662
+ skillsDir: ".agents/skills",
651
663
  globalSkillsDir: join(home, ".cursor/skills"),
652
664
  detectInstalled: async () => {
653
665
  return existsSync(join(home, ".cursor"));
@@ -896,6 +908,14 @@ const agents = {
896
908
  detectInstalled: async () => {
897
909
  return existsSync(join(home, ".adal"));
898
910
  }
911
+ },
912
+ universal: {
913
+ name: "universal",
914
+ displayName: "Universal",
915
+ skillsDir: ".agents/skills",
916
+ globalSkillsDir: join(configHome, "agents/skills"),
917
+ showInUniversalList: false,
918
+ detectInstalled: async () => false
899
919
  }
900
920
  };
901
921
  async function detectInstalledAgents() {
@@ -3255,7 +3275,7 @@ function extractRemoteSkillFiles(remoteSkill) {
3255
3275
  function extractWellKnownSkillFiles(skill) {
3256
3276
  return skill.files;
3257
3277
  }
3258
- const NOT_FOUND$1 = {
3278
+ const NOT_FOUND = {
3259
3279
  found: false,
3260
3280
  verdict: "unknown",
3261
3281
  maliciousCount: 0,
@@ -3266,19 +3286,19 @@ async function lookupFileHash(sha256, apiKey) {
3266
3286
  try {
3267
3287
  response = await fetch(`https://www.virustotal.com/api/v3/files/${sha256}`, { headers: { "x-apikey": apiKey } });
3268
3288
  } catch {
3269
- return NOT_FOUND$1;
3289
+ return NOT_FOUND;
3270
3290
  }
3271
- if (response.status === 404) return NOT_FOUND$1;
3272
- if (response.status === 429) return NOT_FOUND$1;
3273
- if (!response.ok) return NOT_FOUND$1;
3291
+ if (response.status === 404) return NOT_FOUND;
3292
+ if (response.status === 429) return NOT_FOUND;
3293
+ if (!response.ok) return NOT_FOUND;
3274
3294
  let body;
3275
3295
  try {
3276
3296
  body = await response.json();
3277
3297
  } catch {
3278
- return NOT_FOUND$1;
3298
+ return NOT_FOUND;
3279
3299
  }
3280
3300
  const attrs = body.data?.attributes;
3281
- if (!attrs) return NOT_FOUND$1;
3301
+ if (!attrs) return NOT_FOUND;
3282
3302
  const stats = attrs.last_analysis_stats;
3283
3303
  const maliciousCount = stats?.malicious ?? 0;
3284
3304
  const totalEngines = stats ? Object.values(stats).reduce((sum, n) => sum + n, 0) : 0;
@@ -3304,62 +3324,22 @@ async function lookupFileHash(sha256, apiKey) {
3304
3324
  async function checkSkillOnVT(skillContent, apiKey) {
3305
3325
  return lookupFileHash(createHash("sha256").update(skillContent).digest("hex"), apiKey);
3306
3326
  }
3307
- const AUDITORS = [
3308
- {
3309
- id: "snyk",
3310
- displayName: "Snyk"
3311
- },
3312
- {
3313
- id: "socket",
3314
- displayName: "Socket"
3315
- },
3316
- {
3317
- id: "agent-trust-hub",
3318
- displayName: "Gen Agent Trust Hub"
3319
- }
3320
- ];
3321
- const NOT_FOUND = {
3322
- found: false,
3323
- permalink: "",
3324
- audits: [],
3325
- anyFail: false
3326
- };
3327
- function parseAudits(html, baseUrl) {
3328
- return AUDITORS.map(({ id, displayName }) => {
3329
- const pattern = new RegExp(`\\/security\\/${id}[^]{0,400}?\\b(Pass|Fail)\\b`, "is");
3330
- const match = html.match(pattern);
3331
- let status = "unknown";
3332
- if (match) status = match[1].toLowerCase() === "pass" ? "pass" : "fail";
3333
- return {
3334
- auditor: id,
3335
- displayName,
3336
- status,
3337
- permalink: `${baseUrl}/security/${id}`
3338
- };
3339
- });
3340
- }
3341
- async function checkSkillOnSkillsSh(source) {
3342
- const { owner, repo, skillFolder } = source;
3343
- const permalink = `https://skills.sh/${owner}/${repo}/${skillFolder}`;
3327
+ const AUDIT_URL = "https://add-skill.vercel.sh/audit";
3328
+ async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) {
3329
+ if (skillSlugs.length === 0) return null;
3344
3330
  try {
3331
+ const params = new URLSearchParams({
3332
+ source,
3333
+ skills: skillSlugs.join(",")
3334
+ });
3345
3335
  const controller = new AbortController();
3346
- const timeout = setTimeout(() => controller.abort(), 5e3);
3347
- let response;
3348
- try {
3349
- response = await fetch(permalink, { signal: controller.signal });
3350
- } finally {
3351
- clearTimeout(timeout);
3352
- }
3353
- if (!response.ok) return NOT_FOUND;
3354
- const audits = parseAudits(await response.text(), permalink);
3355
- return {
3356
- found: true,
3357
- permalink,
3358
- audits,
3359
- anyFail: audits.some((a) => a.status === "fail")
3360
- };
3336
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
3337
+ const response = await fetch(`${AUDIT_URL}?${params.toString()}`, { signal: controller.signal });
3338
+ clearTimeout(timeout);
3339
+ if (!response.ok) return null;
3340
+ return await response.json();
3361
3341
  } catch {
3362
- return NOT_FOUND;
3342
+ return null;
3363
3343
  }
3364
3344
  }
3365
3345
  const SEVERITY_LABELS = {
@@ -3376,6 +3356,14 @@ const SEVERITY_ORDER = {
3376
3356
  high: 3,
3377
3357
  critical: 4
3378
3358
  };
3359
+ const AUDIT_RISK_ORDER = {
3360
+ unknown: 0,
3361
+ safe: 0,
3362
+ low: 1,
3363
+ medium: 2,
3364
+ high: 3,
3365
+ critical: 4
3366
+ };
3379
3367
  function displayVTVerdict(verdict) {
3380
3368
  if (!verdict.found) {
3381
3369
  M.message(import_picocolors.default.dim(" VirusTotal: not found (local scan only)"));
@@ -3390,16 +3378,61 @@ function displayVTVerdict(verdict) {
3390
3378
  }
3391
3379
  if (verdict.permalink) M.message(import_picocolors.default.dim(` ${verdict.permalink}`));
3392
3380
  }
3393
- function displaySkillsShResult(result) {
3394
- if (!result.found || result.audits.length === 0) return;
3395
- const badges = result.audits.map((a) => {
3396
- const name = a.auditor === "agent-trust-hub" ? "Trust Hub" : a.displayName;
3397
- if (a.status === "pass") return `[${import_picocolors.default.green(`${name} ✓`)}]`;
3398
- if (a.status === "fail") return `[${import_picocolors.default.red(`${name} ✗`)}]`;
3399
- return `[${import_picocolors.default.dim(`${name} ~`)}]`;
3400
- }).join(" ");
3401
- M.message(` ${import_picocolors.default.cyan("◆")} skills.sh: ${result.audits.length} audits ${badges}`);
3402
- M.message(import_picocolors.default.dim(` ${result.permalink}`));
3381
+ function auditRiskBadge(displayName, audit) {
3382
+ const alerts = audit.alerts != null && audit.alerts > 0 ? ` ${audit.alerts} alert${audit.alerts !== 1 ? "s" : ""}` : "";
3383
+ switch (audit.risk) {
3384
+ case "critical": return `[${import_picocolors.default.red(import_picocolors.default.bold(`${displayName} critical${alerts}`))}]`;
3385
+ case "high": return `[${import_picocolors.default.red(`${displayName} ✗ high${alerts}`)}]`;
3386
+ case "medium": return `[${import_picocolors.default.yellow(`${displayName} ⚠ medium${alerts}`)}]`;
3387
+ case "low": return `[${import_picocolors.default.green(`${displayName} ✓ low`)}]`;
3388
+ case "safe": return `[${import_picocolors.default.green(`${displayName} ✓`)}]`;
3389
+ default: return `[${import_picocolors.default.dim(`${displayName} ~`)}]`;
3390
+ }
3391
+ }
3392
+ const AUDITORS = [
3393
+ {
3394
+ id: "ath",
3395
+ displayName: "Trust Hub"
3396
+ },
3397
+ {
3398
+ id: "socket",
3399
+ displayName: "Socket"
3400
+ },
3401
+ {
3402
+ id: "snyk",
3403
+ displayName: "Snyk"
3404
+ }
3405
+ ];
3406
+ function displayAuditResults(skillNames, auditData, source) {
3407
+ if (!skillNames.some((name) => {
3408
+ const data = auditData[name];
3409
+ return data && Object.keys(data).length > 0;
3410
+ })) return;
3411
+ for (const skillName of skillNames) {
3412
+ const data = auditData[skillName];
3413
+ if (!data || Object.keys(data).length === 0) continue;
3414
+ const badges = AUDITORS.map(({ id, displayName }) => {
3415
+ const audit = data[id];
3416
+ return audit ? auditRiskBadge(displayName, audit) : `[${import_picocolors.default.dim(`${displayName} ~`)}]`;
3417
+ }).join(" ");
3418
+ const label = skillNames.length > 1 ? `${import_picocolors.default.cyan(skillName)}: ` : "";
3419
+ M.message(` ${import_picocolors.default.cyan("◆")} ${label}${badges}`);
3420
+ }
3421
+ M.message(import_picocolors.default.dim(` https://skills.sh/${source}`));
3422
+ }
3423
+ function maxAuditRisk(skillNames, auditData) {
3424
+ let max = 0;
3425
+ for (const skillName of skillNames) {
3426
+ const data = auditData[skillName];
3427
+ if (!data) continue;
3428
+ for (const audit of Object.values(data)) {
3429
+ const level = AUDIT_RISK_ORDER[audit.risk] ?? 0;
3430
+ if (level > max) max = level;
3431
+ }
3432
+ }
3433
+ if (max >= 4) return "critical";
3434
+ if (max >= 3) return "high";
3435
+ return null;
3403
3436
  }
3404
3437
  async function presentScanResults(results, options) {
3405
3438
  const allFindings = results.flatMap((r) => r.findings.map((f) => ({
@@ -3407,10 +3440,12 @@ async function presentScanResults(results, options) {
3407
3440
  skillName: r.skillName
3408
3441
  })));
3409
3442
  const allUrls = [...new Set(results.flatMap((r) => r.urls))];
3443
+ const skillNames = results.map((r) => r.skillName);
3410
3444
  const vtVerdicts = /* @__PURE__ */ new Map();
3411
- const skillsShResults = /* @__PURE__ */ new Map();
3445
+ let auditData = null;
3446
+ let auditFailed = false;
3412
3447
  let vtEscalate = false;
3413
- let skillsShEscalate = false;
3448
+ let auditEscalate = null;
3414
3449
  await Promise.all([(async () => {
3415
3450
  if (options.vtKey && options.skillContents) for (const [skillName, content] of options.skillContents) try {
3416
3451
  const verdict = await checkSkillOnVT(content, options.vtKey);
@@ -3418,16 +3453,20 @@ async function presentScanResults(results, options) {
3418
3453
  if (verdict.found && verdict.verdict === "malicious") vtEscalate = true;
3419
3454
  } catch {}
3420
3455
  })(), (async () => {
3421
- if (options.skillsShSources) await Promise.all([...options.skillsShSources.entries()].map(async ([skillName, source]) => {
3422
- const result = await checkSkillOnSkillsSh(source);
3423
- skillsShResults.set(skillName, result);
3424
- if (result.anyFail) skillsShEscalate = true;
3425
- }));
3456
+ if (options.auditSource) {
3457
+ const data = await fetchAuditData(options.auditSource, skillNames);
3458
+ if (data) {
3459
+ auditData = data;
3460
+ auditEscalate = maxAuditRisk(skillNames, data);
3461
+ } else auditFailed = true;
3462
+ }
3426
3463
  })()]);
3427
- if (allFindings.length === 0 && !vtEscalate && !skillsShEscalate) {
3464
+ const anyEscalation = vtEscalate || auditEscalate !== null;
3465
+ if (allFindings.length === 0 && !anyEscalation) {
3428
3466
  M.success(import_picocolors.default.green("Security scan passed — no issues found"));
3429
3467
  if (vtVerdicts.size > 0) for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
3430
- for (const [, result] of skillsShResults) displaySkillsShResult(result);
3468
+ if (auditData && options.auditSource) displayAuditResults(skillNames, auditData, options.auditSource);
3469
+ if (auditFailed) M.warn(import_picocolors.default.yellow("skills.sh audit unavailable — third-party risk data could not be fetched"));
3431
3470
  if (allUrls.length > 0) return displayUrlsAndPrompt(allUrls, options);
3432
3471
  return true;
3433
3472
  }
@@ -3451,10 +3490,11 @@ async function presentScanResults(results, options) {
3451
3490
  console.log();
3452
3491
  for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
3453
3492
  }
3454
- if (skillsShResults.size > 0) {
3493
+ if (auditData && options.auditSource) {
3455
3494
  console.log();
3456
- for (const [, result] of skillsShResults) displaySkillsShResult(result);
3495
+ displayAuditResults(skillNames, auditData, options.auditSource);
3457
3496
  }
3497
+ if (auditFailed) M.warn(import_picocolors.default.yellow("skills.sh audit unavailable — third-party risk data could not be fetched"));
3458
3498
  if (allUrls.length > 0) {
3459
3499
  console.log();
3460
3500
  M.info(`External URLs found in skill files (${allUrls.length}):`);
@@ -3462,25 +3502,18 @@ async function presentScanResults(results, options) {
3462
3502
  }
3463
3503
  console.log();
3464
3504
  if (vtEscalate) overallMax = "critical";
3465
- if (skillsShEscalate && SEVERITY_ORDER[overallMax] < SEVERITY_ORDER["high"]) overallMax = "high";
3505
+ if (auditEscalate === "critical") overallMax = "critical";
3506
+ else if (auditEscalate === "high" && SEVERITY_ORDER[overallMax] < SEVERITY_ORDER["high"]) overallMax = "high";
3466
3507
  if (SEVERITY_ORDER[overallMax] <= SEVERITY_ORDER["medium"]) {
3467
3508
  M.info(import_picocolors.default.dim("Low/medium severity findings — proceeding with installation"));
3468
3509
  return true;
3469
3510
  }
3470
- if (overallMax === "critical") {
3471
- M.error(import_picocolors.default.red(import_picocolors.default.bold("Critical security issues detected. This skill may be malicious.")));
3472
- const confirmed = await ye({
3473
- message: import_picocolors.default.red("Install anyway? This is strongly discouraged."),
3474
- initialValue: false
3475
- });
3476
- if (pD(confirmed) || !confirmed) return false;
3477
- return true;
3478
- }
3479
- if (options.yes) {
3480
- M.warn(import_picocolors.default.yellow("High severity findings detected — proceeding (--yes flag set)"));
3481
- return true;
3482
- }
3483
- const confirmed = await ye({ message: import_picocolors.default.yellow("Security warnings found. Continue with installation?") });
3511
+ if (overallMax === "critical") M.error(import_picocolors.default.red(import_picocolors.default.bold("Critical security issues detected. This skill may be malicious.")));
3512
+ else M.error(import_picocolors.default.red("High severity security issues detected."));
3513
+ const confirmed = await ye({
3514
+ message: import_picocolors.default.yellow("Install anyway?"),
3515
+ initialValue: false
3516
+ });
3484
3517
  if (pD(confirmed) || !confirmed) return false;
3485
3518
  return true;
3486
3519
  }
@@ -3507,20 +3540,12 @@ function resolveExternalRules(options) {
3507
3540
  _externalRulesCache.set(key, loaded);
3508
3541
  return loaded;
3509
3542
  }
3510
- function buildSkillsShSourcesForRemote(remoteSkill, _url) {
3511
- const sourceUrl = remoteSkill.sourceUrl;
3512
- if (!sourceUrl || !sourceUrl.includes("github.com")) return void 0;
3543
+ function extractAuditSource(sourceUrl) {
3544
+ if (!sourceUrl?.includes("github.com")) return void 0;
3513
3545
  try {
3514
3546
  const parts = new URL(sourceUrl).pathname.slice(1).replace(/\.git$/, "").split("/").filter(Boolean);
3515
- const ownerRepo = parseOwnerRepo(parts.slice(0, 2).join("/"));
3516
- if (!ownerRepo) return void 0;
3517
- const skillFolder = parts.length > 2 ? parts.at(-1) : remoteSkill.installName;
3518
- const sources = /* @__PURE__ */ new Map();
3519
- sources.set(remoteSkill.installName, {
3520
- ...ownerRepo,
3521
- skillFolder
3522
- });
3523
- return sources;
3547
+ if (parts.length < 2) return void 0;
3548
+ return parts.slice(0, 2).join("/");
3524
3549
  } catch {
3525
3550
  return;
3526
3551
  }
@@ -3784,12 +3809,12 @@ async function handleRemoteSkill(source, url, options, spinner) {
3784
3809
  });
3785
3810
  const vtKey = options.vtKey || process.env.VT_API_KEY;
3786
3811
  const skillContents = vtKey ? new Map([[remoteSkill.installName, remoteSkill.content]]) : void 0;
3787
- const skillsShSources = buildSkillsShSourcesForRemote(remoteSkill, url);
3812
+ const auditSource = extractAuditSource(remoteSkill.sourceUrl);
3788
3813
  if (!await presentScanResults([scanResult], {
3789
3814
  yes: options.yes,
3790
3815
  vtKey,
3791
3816
  skillContents,
3792
- skillsShSources
3817
+ auditSource
3793
3818
  })) {
3794
3819
  xe("Installation cancelled due to security concerns");
3795
3820
  process.exit(0);
@@ -4461,6 +4486,7 @@ async function runAdd(args, options = {}) {
4461
4486
  }
4462
4487
  selectedSkills = selected;
4463
4488
  }
4489
+ const auditSource = getOwnerRepo(parsed) ?? void 0;
4464
4490
  let targetAgents;
4465
4491
  const validAgents = Object.keys(agents);
4466
4492
  if (options.agent?.includes("*")) {
@@ -4598,24 +4624,11 @@ async function runAdd(args, options = {}) {
4598
4624
  }
4599
4625
  }
4600
4626
  spinner.stop("Security scan complete");
4601
- const skillsShSources = /* @__PURE__ */ new Map();
4602
- if (parsed.type === "github") {
4603
- const ownerRepoStr = getOwnerRepo(parsed);
4604
- const ownerRepo = ownerRepoStr ? parseOwnerRepo(ownerRepoStr) : null;
4605
- if (ownerRepo) for (const skill of selectedSkills) {
4606
- const displayName = getSkillDisplayName(skill);
4607
- const skillFolder = skill.path.split("/").at(-1) ?? skill.name;
4608
- skillsShSources.set(displayName, {
4609
- ...ownerRepo,
4610
- skillFolder
4611
- });
4612
- }
4613
- }
4614
4627
  if (!await presentScanResults(scanResults, {
4615
4628
  yes: options.yes,
4616
4629
  vtKey,
4617
4630
  skillContents,
4618
- skillsShSources: skillsShSources.size > 0 ? skillsShSources : void 0
4631
+ auditSource
4619
4632
  })) {
4620
4633
  xe("Installation cancelled due to security concerns");
4621
4634
  await cleanup(tempDir);
@@ -4828,7 +4841,14 @@ const RESET$2 = "\x1B[0m";
4828
4841
  const BOLD$2 = "\x1B[1m";
4829
4842
  const DIM$2 = "\x1B[38;5;102m";
4830
4843
  const TEXT$1 = "\x1B[38;5;145m";
4844
+ const CYAN$1 = "\x1B[36m";
4831
4845
  const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh";
4846
+ function formatInstalls(count) {
4847
+ if (!count || count <= 0) return "";
4848
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
4849
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
4850
+ return `${count} install${count === 1 ? "" : "s"}`;
4851
+ }
4832
4852
  async function searchSkillsAPI(query) {
4833
4853
  try {
4834
4854
  const url = `${SEARCH_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=10`;
@@ -4878,8 +4898,10 @@ async function runSearchPrompt(initialQuery = "") {
4878
4898
  const arrow = isSelected ? `${BOLD$2}>${RESET$2}` : " ";
4879
4899
  const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$1}${skill.name}${RESET$2}`;
4880
4900
  const source = skill.source ? ` ${DIM$2}${skill.source}${RESET$2}` : "";
4901
+ const installs = formatInstalls(skill.installs);
4902
+ const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$2}` : "";
4881
4903
  const loadingIndicator = loading && i === 0 ? ` ${DIM$2}...${RESET$2}` : "";
4882
- lines.push(` ${arrow} ${name}${source}${loadingIndicator}`);
4904
+ lines.push(` ${arrow} ${name}${source}${installsBadge}${loadingIndicator}`);
4883
4905
  }
4884
4906
  }
4885
4907
  lines.push("");
@@ -4992,7 +5014,8 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4992
5014
  console.log();
4993
5015
  for (const skill of results.slice(0, 6)) {
4994
5016
  const pkg = skill.source || skill.slug;
4995
- console.log(`${TEXT$1}${pkg}@${skill.name}${RESET$2}`);
5017
+ const installs = formatInstalls(skill.installs);
5018
+ console.log(`${TEXT$1}${pkg}@${skill.name}${RESET$2}${installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""}`);
4996
5019
  console.log(`${DIM$2}└ https://skills.sh/${skill.slug}${RESET$2}`);
4997
5020
  console.log();
4998
5021
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillsio",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "The SECURE open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,6 +46,7 @@
46
46
  "codex",
47
47
  "command-code",
48
48
  "continue",
49
+ "cortex",
49
50
  "crush",
50
51
  "cursor",
51
52
  "droid",
@@ -74,7 +75,8 @@
74
75
  "zencoder",
75
76
  "neovate",
76
77
  "pochi",
77
- "adal"
78
+ "adal",
79
+ "universal"
78
80
  ],
79
81
  "repository": {
80
82
  "type": "git",