skillsio 1.1.1 → 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 +230 -6
  2. package/dist/cli.mjs +181 -31
  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
@@ -50,6 +54,23 @@ domain patterns that regex rules can't — letting you eyeball where a skill wan
50
54
  With `--yes`, URL-only prompts are auto-continued. Skills with high/critical findings always show URLs alongside the
51
55
  findings summary.
52
56
 
57
+ ### Third-Party Audits via skills.sh
58
+
59
+ For GitHub-sourced skills, the CLI automatically checks [skills.sh](https://skills.sh) — Vercel's official skill
60
+ directory — which runs independent third-party security audits from three auditors: **Snyk**, **Socket**, and
61
+ **Gen Agent Trust Hub**. Results appear alongside local scan output:
62
+
63
+ ```
64
+ ◆ skills.sh: 3 audits [Snyk ✗] [Socket ✓] [Trust Hub ✗]
65
+ https://skills.sh/inference-sh-3/skills/agent-tools
66
+ ```
67
+
68
+ - Green ✓ = auditor passed, Red ✗ = auditor failed, Dim ~ = no result yet
69
+ - If any auditor returns a **Fail** verdict, severity is escalated to at least **High**, triggering a confirmation
70
+ prompt
71
+ - skills.sh lookup runs in parallel with VT and never blocks installation on error (graceful fallback)
72
+ - Only fires for GitHub-sourced skills that are listed on skills.sh — silent for everything else
73
+
53
74
  ### Optional: VirusTotal Integration
54
75
 
55
76
  When a [VirusTotal](https://www.virustotal.com/) API key is provided, the CLI also hashes each skill's content
@@ -242,18 +263,19 @@ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [35 more](#su
242
263
  <!-- supported-agents:start -->
243
264
  | Agent | `--agent` | Project Path | Global Path |
244
265
  |-------|-----------|--------------|-------------|
245
- | 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/` |
246
267
  | Antigravity | `antigravity` | `.agent/skills/` | `~/.gemini/antigravity/skills/` |
247
268
  | Augment | `augment` | `.augment/skills/` | `~/.augment/skills/` |
248
269
  | Claude Code | `claude-code` | `.claude/skills/` | `~/.claude/skills/` |
249
- | OpenClaw | `openclaw` | `skills/` | `~/.moltbot/skills/` |
270
+ | OpenClaw | `openclaw` | `skills/` | `~/.openclaw/skills/` |
250
271
  | Cline | `cline` | `.cline/skills/` | `~/.cline/skills/` |
251
272
  | CodeBuddy | `codebuddy` | `.codebuddy/skills/` | `~/.codebuddy/skills/` |
252
273
  | Codex | `codex` | `.agents/skills/` | `~/.codex/skills/` |
253
274
  | Command Code | `command-code` | `.commandcode/skills/` | `~/.commandcode/skills/` |
254
275
  | Continue | `continue` | `.continue/skills/` | `~/.continue/skills/` |
276
+ | Cortex Code | `cortex` | `.cortex/skills/` | `~/.snowflake/cortex/skills/` |
255
277
  | Crush | `crush` | `.crush/skills/` | `~/.config/crush/skills/` |
256
- | Cursor | `cursor` | `.cursor/skills/` | `~/.cursor/skills/` |
278
+ | Cursor | `cursor` | `.agents/skills/` | `~/.cursor/skills/` |
257
279
  | Droid | `droid` | `.factory/skills/` | `~/.factory/skills/` |
258
280
  | Gemini CLI | `gemini-cli` | `.agents/skills/` | `~/.gemini/skills/` |
259
281
  | GitHub Copilot | `github-copilot` | `.agents/skills/` | `~/.copilot/skills/` |
@@ -281,7 +303,154 @@ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [35 more](#su
281
303
  | AdaL | `adal` | `.adal/skills/` | `~/.adal/skills/` |
282
304
  <!-- supported-agents:end -->
283
305
 
284
- 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.
285
454
 
286
455
  ## Environment Variables
287
456
 
@@ -306,10 +475,12 @@ pnpm format # Format code with Prettier
306
475
  - `src/scanner.ts` — Rules engine. Defines ~81 regex rules across 8 threat categories, a correlation engine for
307
476
  multi-signal detection, and optional deep taint analysis integration. Supports loading external rules from JSON
308
477
  files via `--rules`.
309
- - `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs optional VT lookups, handles
310
- escalation logic and user confirmation prompts.
478
+ - `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs VT and skills.sh lookups in parallel,
479
+ handles escalation logic and user confirmation prompts.
311
480
  - `src/vt.ts` — VirusTotal API client. SHA-256 hashing, `GET /api/v3/files/{hash}` lookup, verdict mapping, graceful
312
481
  error handling.
482
+ - `src/skills-sh.ts` — skills.sh audit client. Fetches and HTML-parses third-party audit results (Snyk, Socket, Gen
483
+ Agent Trust Hub) for GitHub-sourced skills with a 5-second timeout; always resolves gracefully.
313
484
  - `src/deep-scan/` — Deep taint analysis engine (enabled via `--deep-scan`). Regex-based tokenizers extract sources,
314
485
  sinks, and assignments from Python/JS/TS files; a forward taint tracker propagates data flow; a cross-file analyzer
315
486
  detects multi-file attack patterns via import graph analysis. See [docs/deep-scan.md](docs/deep-scan.md).
@@ -318,6 +489,28 @@ pnpm format # Format code with Prettier
318
489
 
319
490
  ## Changelog
320
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
+
506
+ ### 1.1.2
507
+
508
+ - **skills.sh audit integration**: for GitHub-sourced skills, the CLI now fetches third-party audit results from
509
+ [skills.sh](https://skills.sh) (Snyk, Socket, Gen Agent Trust Hub) and displays them alongside local scan output
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
513
+
321
514
  ### 1.1.1
322
515
 
323
516
  - Removed anonymous usage telemetry inherited from the original Vercel `skills` CLI
@@ -347,6 +540,37 @@ pnpm format # Format code with Prettier
347
540
  - URL transparency: all external URLs in skill files are shown before installation
348
541
  - Scanner rules informed by Snyk and ClawHavoc research
349
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
+
350
574
  ## Research
351
575
 
352
576
  The scanner rules are informed by the following research into malicious agent skills:
package/dist/cli.mjs CHANGED
@@ -50,7 +50,10 @@ function isDirectSkillUrl(input) {
50
50
  if (input.includes("gitlab.com/") && !input.includes("/-/raw/")) return false;
51
51
  return true;
52
52
  }
53
+ const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills" };
53
54
  function parseSource(input) {
55
+ const alias = SOURCE_ALIASES[input];
56
+ if (alias) input = alias;
54
57
  if (isLocalPath(input)) {
55
58
  const resolvedPath = resolve(input);
56
59
  return {
@@ -169,7 +172,8 @@ const S_STEP_CANCEL = import_picocolors.default.red("■");
169
172
  const S_STEP_SUBMIT = import_picocolors.default.green("◇");
170
173
  const S_RADIO_ACTIVE = import_picocolors.default.green("●");
171
174
  const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
172
- const S_CHECKBOX_LOCKED = import_picocolors.default.green("✓");
175
+ import_picocolors.default.green("✓");
176
+ const S_BULLET = import_picocolors.default.green("•");
173
177
  const S_BAR = import_picocolors.default.dim("│");
174
178
  const S_BAR_H = import_picocolors.default.dim("─");
175
179
  const cancelSymbol = Symbol("cancel");
@@ -212,10 +216,11 @@ async function searchMultiselect(options) {
212
216
  if (state === "active") {
213
217
  if (lockedSection && lockedSection.items.length > 0) {
214
218
  lines.push(`${S_BAR}`);
215
- lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold(lockedSection.title)} ${S_BAR_H.repeat(30)}`);
216
- 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)}`);
217
222
  lines.push(`${S_BAR}`);
218
- 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)}`);
219
224
  }
220
225
  const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
221
226
  lines.push(searchLine);
@@ -428,6 +433,7 @@ async function parseSkillMd(skillMdPath, options) {
428
433
  const content = await readFile(skillMdPath, "utf-8");
429
434
  const { data } = (0, import_gray_matter.default)(content);
430
435
  if (!data.name || !data.description) return null;
436
+ if (typeof data.name !== "string" || typeof data.description !== "string") return null;
431
437
  if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
432
438
  return {
433
439
  name: data.name,
@@ -477,7 +483,6 @@ async function discoverSkills(basePath, subpath, options) {
477
483
  join(searchPath, ".codex/skills"),
478
484
  join(searchPath, ".commandcode/skills"),
479
485
  join(searchPath, ".continue/skills"),
480
- join(searchPath, ".cursor/skills"),
481
486
  join(searchPath, ".github/skills"),
482
487
  join(searchPath, ".goose/skills"),
483
488
  join(searchPath, ".iflow/skills"),
@@ -536,6 +541,12 @@ const home = homedir();
536
541
  const configHome = xdgConfig ?? join(home, ".config");
537
542
  const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
538
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
+ }
539
550
  const agents = {
540
551
  amp: {
541
552
  name: "amp",
@@ -577,7 +588,7 @@ const agents = {
577
588
  name: "openclaw",
578
589
  displayName: "OpenClaw",
579
590
  skillsDir: "skills",
580
- globalSkillsDir: existsSync(join(home, ".openclaw")) ? join(home, ".openclaw/skills") : existsSync(join(home, ".clawdbot")) ? join(home, ".clawdbot/skills") : join(home, ".moltbot/skills"),
591
+ globalSkillsDir: getOpenClawGlobalSkillsDir(),
581
592
  detectInstalled: async () => {
582
593
  return existsSync(join(home, ".openclaw")) || existsSync(join(home, ".clawdbot")) || existsSync(join(home, ".moltbot"));
583
594
  }
@@ -627,6 +638,15 @@ const agents = {
627
638
  return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"));
628
639
  }
629
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
+ },
630
650
  crush: {
631
651
  name: "crush",
632
652
  displayName: "Crush",
@@ -639,7 +659,7 @@ const agents = {
639
659
  cursor: {
640
660
  name: "cursor",
641
661
  displayName: "Cursor",
642
- skillsDir: ".cursor/skills",
662
+ skillsDir: ".agents/skills",
643
663
  globalSkillsDir: join(home, ".cursor/skills"),
644
664
  detectInstalled: async () => {
645
665
  return existsSync(join(home, ".cursor"));
@@ -888,6 +908,14 @@ const agents = {
888
908
  detectInstalled: async () => {
889
909
  return existsSync(join(home, ".adal"));
890
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
891
919
  }
892
920
  };
893
921
  async function detectInstalledAgents() {
@@ -3296,6 +3324,24 @@ async function lookupFileHash(sha256, apiKey) {
3296
3324
  async function checkSkillOnVT(skillContent, apiKey) {
3297
3325
  return lookupFileHash(createHash("sha256").update(skillContent).digest("hex"), apiKey);
3298
3326
  }
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;
3330
+ try {
3331
+ const params = new URLSearchParams({
3332
+ source,
3333
+ skills: skillSlugs.join(",")
3334
+ });
3335
+ const controller = new AbortController();
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();
3341
+ } catch {
3342
+ return null;
3343
+ }
3344
+ }
3299
3345
  const SEVERITY_LABELS = {
3300
3346
  critical: import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" CRITICAL "))),
3301
3347
  high: import_picocolors.default.red(import_picocolors.default.bold("HIGH")),
@@ -3310,6 +3356,14 @@ const SEVERITY_ORDER = {
3310
3356
  high: 3,
3311
3357
  critical: 4
3312
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
+ };
3313
3367
  function displayVTVerdict(verdict) {
3314
3368
  if (!verdict.found) {
3315
3369
  M.message(import_picocolors.default.dim(" VirusTotal: not found (local scan only)"));
@@ -3324,22 +3378,95 @@ function displayVTVerdict(verdict) {
3324
3378
  }
3325
3379
  if (verdict.permalink) M.message(import_picocolors.default.dim(` ${verdict.permalink}`));
3326
3380
  }
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;
3436
+ }
3327
3437
  async function presentScanResults(results, options) {
3328
3438
  const allFindings = results.flatMap((r) => r.findings.map((f) => ({
3329
3439
  ...f,
3330
3440
  skillName: r.skillName
3331
3441
  })));
3332
3442
  const allUrls = [...new Set(results.flatMap((r) => r.urls))];
3443
+ const skillNames = results.map((r) => r.skillName);
3333
3444
  const vtVerdicts = /* @__PURE__ */ new Map();
3445
+ let auditData = null;
3446
+ let auditFailed = false;
3334
3447
  let vtEscalate = false;
3335
- if (options.vtKey && options.skillContents) for (const [skillName, content] of options.skillContents) try {
3336
- const verdict = await checkSkillOnVT(content, options.vtKey);
3337
- vtVerdicts.set(skillName, verdict);
3338
- if (verdict.found && verdict.verdict === "malicious") vtEscalate = true;
3339
- } catch {}
3340
- if (allFindings.length === 0 && !vtEscalate) {
3448
+ let auditEscalate = null;
3449
+ await Promise.all([(async () => {
3450
+ if (options.vtKey && options.skillContents) for (const [skillName, content] of options.skillContents) try {
3451
+ const verdict = await checkSkillOnVT(content, options.vtKey);
3452
+ vtVerdicts.set(skillName, verdict);
3453
+ if (verdict.found && verdict.verdict === "malicious") vtEscalate = true;
3454
+ } catch {}
3455
+ })(), (async () => {
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
+ }
3463
+ })()]);
3464
+ const anyEscalation = vtEscalate || auditEscalate !== null;
3465
+ if (allFindings.length === 0 && !anyEscalation) {
3341
3466
  M.success(import_picocolors.default.green("Security scan passed — no issues found"));
3342
3467
  if (vtVerdicts.size > 0) for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
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"));
3343
3470
  if (allUrls.length > 0) return displayUrlsAndPrompt(allUrls, options);
3344
3471
  return true;
3345
3472
  }
@@ -3363,6 +3490,11 @@ async function presentScanResults(results, options) {
3363
3490
  console.log();
3364
3491
  for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
3365
3492
  }
3493
+ if (auditData && options.auditSource) {
3494
+ console.log();
3495
+ displayAuditResults(skillNames, auditData, options.auditSource);
3496
+ }
3497
+ if (auditFailed) M.warn(import_picocolors.default.yellow("skills.sh audit unavailable — third-party risk data could not be fetched"));
3366
3498
  if (allUrls.length > 0) {
3367
3499
  console.log();
3368
3500
  M.info(`External URLs found in skill files (${allUrls.length}):`);
@@ -3370,24 +3502,18 @@ async function presentScanResults(results, options) {
3370
3502
  }
3371
3503
  console.log();
3372
3504
  if (vtEscalate) overallMax = "critical";
3505
+ if (auditEscalate === "critical") overallMax = "critical";
3506
+ else if (auditEscalate === "high" && SEVERITY_ORDER[overallMax] < SEVERITY_ORDER["high"]) overallMax = "high";
3373
3507
  if (SEVERITY_ORDER[overallMax] <= SEVERITY_ORDER["medium"]) {
3374
3508
  M.info(import_picocolors.default.dim("Low/medium severity findings — proceeding with installation"));
3375
3509
  return true;
3376
3510
  }
3377
- if (overallMax === "critical") {
3378
- M.error(import_picocolors.default.red(import_picocolors.default.bold("Critical security issues detected. This skill may be malicious.")));
3379
- const confirmed = await ye({
3380
- message: import_picocolors.default.red("Install anyway? This is strongly discouraged."),
3381
- initialValue: false
3382
- });
3383
- if (pD(confirmed) || !confirmed) return false;
3384
- return true;
3385
- }
3386
- if (options.yes) {
3387
- M.warn(import_picocolors.default.yellow("High severity findings detected — proceeding (--yes flag set)"));
3388
- return true;
3389
- }
3390
- 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
+ });
3391
3517
  if (pD(confirmed) || !confirmed) return false;
3392
3518
  return true;
3393
3519
  }
@@ -3414,6 +3540,16 @@ function resolveExternalRules(options) {
3414
3540
  _externalRulesCache.set(key, loaded);
3415
3541
  return loaded;
3416
3542
  }
3543
+ function extractAuditSource(sourceUrl) {
3544
+ if (!sourceUrl?.includes("github.com")) return void 0;
3545
+ try {
3546
+ const parts = new URL(sourceUrl).pathname.slice(1).replace(/\.git$/, "").split("/").filter(Boolean);
3547
+ if (parts.length < 2) return void 0;
3548
+ return parts.slice(0, 2).join("/");
3549
+ } catch {
3550
+ return;
3551
+ }
3552
+ }
3417
3553
  function shortenPath$1(fullPath, cwd) {
3418
3554
  const home = homedir();
3419
3555
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
@@ -3673,10 +3809,12 @@ async function handleRemoteSkill(source, url, options, spinner) {
3673
3809
  });
3674
3810
  const vtKey = options.vtKey || process.env.VT_API_KEY;
3675
3811
  const skillContents = vtKey ? new Map([[remoteSkill.installName, remoteSkill.content]]) : void 0;
3812
+ const auditSource = extractAuditSource(remoteSkill.sourceUrl);
3676
3813
  if (!await presentScanResults([scanResult], {
3677
3814
  yes: options.yes,
3678
3815
  vtKey,
3679
- skillContents
3816
+ skillContents,
3817
+ auditSource
3680
3818
  })) {
3681
3819
  xe("Installation cancelled due to security concerns");
3682
3820
  process.exit(0);
@@ -4348,6 +4486,7 @@ async function runAdd(args, options = {}) {
4348
4486
  }
4349
4487
  selectedSkills = selected;
4350
4488
  }
4489
+ const auditSource = getOwnerRepo(parsed) ?? void 0;
4351
4490
  let targetAgents;
4352
4491
  const validAgents = Object.keys(agents);
4353
4492
  if (options.agent?.includes("*")) {
@@ -4488,7 +4627,8 @@ async function runAdd(args, options = {}) {
4488
4627
  if (!await presentScanResults(scanResults, {
4489
4628
  yes: options.yes,
4490
4629
  vtKey,
4491
- skillContents
4630
+ skillContents,
4631
+ auditSource
4492
4632
  })) {
4493
4633
  xe("Installation cancelled due to security concerns");
4494
4634
  await cleanup(tempDir);
@@ -4701,7 +4841,14 @@ const RESET$2 = "\x1B[0m";
4701
4841
  const BOLD$2 = "\x1B[1m";
4702
4842
  const DIM$2 = "\x1B[38;5;102m";
4703
4843
  const TEXT$1 = "\x1B[38;5;145m";
4844
+ const CYAN$1 = "\x1B[36m";
4704
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
+ }
4705
4852
  async function searchSkillsAPI(query) {
4706
4853
  try {
4707
4854
  const url = `${SEARCH_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=10`;
@@ -4751,8 +4898,10 @@ async function runSearchPrompt(initialQuery = "") {
4751
4898
  const arrow = isSelected ? `${BOLD$2}>${RESET$2}` : " ";
4752
4899
  const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$1}${skill.name}${RESET$2}`;
4753
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}` : "";
4754
4903
  const loadingIndicator = loading && i === 0 ? ` ${DIM$2}...${RESET$2}` : "";
4755
- lines.push(` ${arrow} ${name}${source}${loadingIndicator}`);
4904
+ lines.push(` ${arrow} ${name}${source}${installsBadge}${loadingIndicator}`);
4756
4905
  }
4757
4906
  }
4758
4907
  lines.push("");
@@ -4865,7 +5014,8 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4865
5014
  console.log();
4866
5015
  for (const skill of results.slice(0, 6)) {
4867
5016
  const pkg = skill.source || skill.slug;
4868
- 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}` : ""}`);
4869
5019
  console.log(`${DIM$2}└ https://skills.sh/${skill.slug}${RESET$2}`);
4870
5020
  console.log();
4871
5021
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillsio",
3
- "version": "1.1.1",
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",