← 开源
antfu

skills-npm

Install agent skills from npm

AI EngineeringGive agent toolsTypeScript
在 GitHub 打开
增长势头
+124 小时新增 Star+0.2%
533
Star
19
Fork
+4
本周
6
贡献者
创建于 2026-01-31 · 更新于 2026-10-05 · 今日第 6795 名
主要开发者
README

skills-npm

npm version npm downloads bundle JSDocs License

A CLI that installs agent skills through npm: it symlinks skills shipped inside your installed packages, and fetches the remote skills your packages declare in a skills field via the skills CLI.

Why?

Cloning skills from git repos one by one (e.g. npx skills add) has friction:

  • Version mismatch - Skills and tools update separately, causing compatibility issues
  • Manual management - Each project repeats the same add commands
  • Sharing overhead - Teams must commit cloned files or repeat setup on each machine

This project proposes two conventions that let npm do the distribution:

  • Ship skills inside npm packages under a skills/ directory. When you npm install a tool, its skills come bundled and are symlinked for your agent.
  • Curate skills in a skills field of package.json. A package can list skills hosted elsewhere; installing the package installs the list. A package that is nothing but this field is a skills pack.

Read the full proposal: PROPOSAL.md and the field spec: SPEC.md

[!NOTE] Requires Node.js 22.20 or later (the floor of the skills CLI it drives).

Usage

Install as a dev dependency and run setup once:

npm i -D skills-npm
npx skills-npm setup

skills-npm setup wires the tool into your package.json prepare script and runs the first sync. After that, skills are re-symlinked for your agent automatically whenever you install dependencies.

setup merges into any existing prepare script (it appends with && and is a no-op if already wired), resulting in:

{
  "private": true,
  "scripts": {
    "prepare": "skills-npm"
  }
}

skills-npm symlinks each skill from node_modules into your agent's skills directory under the skill's own name (the SKILL.md frontmatter name, sanitized the same way the skills CLI sanitizes install names). The symlinks are relative and point into node_modules, so you can commit them: they are restored on every fresh clone as soon as npm install runs. No .gitignore changes are made.

[!NOTE] Keep skills-npm as a devDependency. The prepare script runs on install (and before publish/pack), but never for people who install your published package, so it is safe to commit.

Remote skills and skills packs

Besides the skills/ directory, skills-npm reads a skills field from your own package.json and from the packages it scans (your direct dependencies by default). Each entry is a git-hosted source in the same syntax the skills CLI accepts, or npm: to pull in skills shipped by another npm package:

{
  "skills": [
    "vercel-labs/agent-skills@web-design-guidelines",
    "owner/repo#v1.2.0",
    { "source": "owner/repo", "skills": ["a", "b"], "ref": "v2.0.0" },
    "npm:@vueuse/skills"
  ]
}

Publish a package with only this field and you have a shareable, versioned skills pack: npm i -D @acme/frontend-skills installs the whole list for everyone on the team. The full syntax and semantics are in SPEC.md.

How remote entries are handled:

  • The skills CLI does the fetching. skills-npm spawns skills add --skill … -a -y for each distinct source, so the result is exactly what npx skills add would produce: a copy under .agents/skills/, per-agent symlinks, and an entry in skills-lock.json. Commit those files as you would after npx skills add.
  • Fetch once, not on every install. An entry whose skills are already recorded in skills-lock.json from the same source (and ref) is skipped. --force refetches everything; to pull newer upstream content run npx skills update.
  • Failures fail the run. A source that cannot be cloned or an invalid entry (a local path, a non-git URL, a ref given twice) exits non-zero, so a broken pack never passes silently. Use --no-remote (or remote: false) for offline installs: network entries are left as they are, npm: entries still resolve.
  • Removal follows the field. When no honored package requests a remote skill anymore, skills-npm runs skills remove for it (unless --no-cleanup). Skills you installed by hand are never touched.
  • npm: entries are resolved from the declaring package (so pnpm's isolated layout works) and symlinked like any shipped skill. The pack must list the target package in its dependencies.

Pin refs in packs (owner/repo#v1.2.0, or "ref") so every consumer gets the same content. A commit must be a full 40-character SHA.

Lock file

Each sync writes skills-npm-lock.json in your project root: a committed manifest of the skills skills-npm manages, keyed by skill name:

{
  "version": 3,
  "skills": {
    "presenter-mode": { "package": "@slidev/cli", "version": "52.1.0", "skillPath": "skills/presenter-mode/SKILL.md" },
    "vueuse-functions": { "package": "@vueuse/skills", "version": "1.3.0", "skillPath": "skills/vueuse-functions/SKILL.md", "via": "@acme/frontend-skills" }
  },
  "remote": {
    "web-design-guidelines": { "package": "@acme/frontend-skills", "source": "vercel-labs/agent-skills", "ref": "v1.4.0" }
  }
}

It records what is installed and who asked for it, not where; agent directories are derived per machine, mirroring how the skills CLI keeps agent selection out of its committed lock. skillPath locates the skill inside the package the same way skills-lock.json does; version is the installed package version, for readers of the lock (your package manager's lock file remains the source of truth). via marks a shipped skill that is only installed because a pack requested it with npm:; remote lists the skills fetched on behalf of a skills field.

Conflicts

When the same skill name comes from more than one place, skills-npm applies this priority ladder:

  1. Explicit installs win - a name listed in the skills CLI's skills-lock.json that skills-npm did not install itself is never touched, even when the skill is missing on disk.
  2. Existing content wins - a real directory or a symlink not pointing into node_modules is never replaced.
  3. Shipped beats remote - a skill shipped in an npm package beats a remote entry of the same name.
  4. Direct beats transitive - a skill from a direct dependency beats the same name from a transitive one; identical remote requests from several packages count as one.
  5. Ties are skipped - if two direct (or only transitive) dependencies collide, all contenders are skipped with a warning; resolve with include/exclude.

Cleanup only ever removes symlinks that point into node_modules and remote skills recorded in skills-npm-lock.json, so nothing else in your agent directories is at risk.

Migrating from v3

skills-npm-lock.json moves to version 3: each entry's skillFolder becomes skillPath (the path to SKILL.md inside the package, as in skills-lock.json) and gains the installed package version. It is rewritten on the first sync. Packages are now also scanned for a root SKILL.md, dist/skills/ and .agents/skills/, so a few more skills may appear.

Migrating from v2

v3 requires Node.js 22.20+ and adds skills as a dependency. skills-npm-lock.json moves to version 2 (a remote map and an optional via per entry); it is rewritten on the first sync. Nothing changes for projects without a skills field.

Migrating from v1

v1 created npm-- links and gitignored them. On the first v2 sync, stale npm-* links are removed automatically and re-created under the new names. The old **/skills/npm-* block in .gitignore no longer matches anything; skills-npm won't edit your .gitignore, so remove it whenever convenient (a hint is printed while it remains). If you want the links shared with your team, commit them along with skills-npm-lock.json.

Configuration

You can create a skills-npm.config.ts file in your project root to configure the behavior:

// skills-npm.config.ts
import { defineConfig } from 'skills-npm'

export default defineConfig({
  // Source to discover skills from: 'node_modules' or 'package.json'
  source: 'package.json',
  // Target specific agents (defaults to all detected agents)
  agents: ['cursor', 'windsurf'],
  // Scan recursively for monorepo packages (default: false)
  recursive: false,
  // Skip confirmation prompts (default: false)
  yes: false,
  // Dry run mode (default: false)
  dryRun: false,
  // Fetch remote skills from `skills` fields (default: true); false for offline installs
  remote: true,
  // Include specific packages or skills
  include: [
    // Include all skills from a package
    '@some/package',
    // Include all skills from packages matching a wildcard pattern
    '@some/*',
    // Include specific skills from packages matching a wildcard pattern
    { package: '@some/*', skills: ['integration'] },
    // Include specific skills from a package
    { package: '@slidev/cli', skills: ['presenter-mode'] },
  ],
  // Exclude specific packages or skills
  exclude: [
    // Exclude all skills from a package
    '@some/package',
    // Exclude all skills from packages matching a wildcard pattern
    '@some/*',
    // Exclude specific skills from packages matching a wildcard pattern
    { package: '@some/*', skills: ['integration'] },
    // Exclude specific skills from a package
    { package: '@slidev/cli', skills: ['presenter-mode'] },
  ],
})

include and exclude string patterns match either a package name (@some/*) or a sanitized skill name (presenter-mode). These filters only apply to packages that were already discovered from node_modules or package.json. For remote entries they match the requesting package, and the skill name only where the entry names skills (a bare owner/repo can only be filtered by package).

Options

Option Type Default Description
cwd string Workspace root Current working directory
source 'node_modules' | 'package.json' 'package.json' Source to discover skills from
agents string | string[] All detected Target agents to install to
recursive boolean false Scan recursively for monorepo packages
yes boolean false Skip confirmation prompts
dryRun boolean false Show what would be done without making changes
include (string | { package: string, skills: string[] })[] undefined Packages or skills to include. Supports package wildcard patterns like @some/*
exclude (string | { package: string, skills: string[] })[] [] Packages or skills to exclude. Supports package wildcard patterns like @some/*
cleanup boolean true Remove stale skills-npm symlinks and remote skills nobody requests anymore
remote boolean true Fetch remote skills declared in skills fields; false skips network entries

The cwd defaults to the workspace root, which is detected by searching up for pnpm-workspace.yaml, lerna.json, or a package.json with workspaces field. Falls back to the nearest package.json.

CLI Options

skills-npm [options]          # discover and symlink skills (run by the prepare hook)
skills-npm setup [options]    # add the prepare script, then run the first sync (run once)

Options:
  --cwd              Current working directory
  -s, --source    Source to discover skills from (default: 'package.json')
  -a, --agents            Comma-separated list of agents to install to
  -r, --recursive         Scan recursively for monorepo packages
  --include     Comma-separated package names or patterns to include
  --exclude     Comma-separated package names or patterns to exclude
  -y, --yes               Skip confirmation prompts
  --dry-run               Show what would be done without making changes
  -f, --force             Force full reload, ignore cache and refetch remote skills
  --no-cleanup            Keep stale skills-npm symlinks and remote skills in agent directories
  --no-remote             Skip remote skills declared in "skills" fields (offline)
  -h, --help              Display help
  -v, --version           Display version

Agent detection

When agents is not set, skills-npm auto-detects which coding agents you use and installs to those. Detection combines two signals:

  • Config directory - an agent's home directory exists (e.g. ~/.cursor, ~/.claude).
  • Installed command - the agent's CLI is found on your PATH (e.g. claude, codex, gemini, cursor-agent). This catches agents that are installed but have not created their config directory yet.

The command check is conservative: only agents with an unambiguous CLI name are probed, so generic names and GUI-only editors are matched by the directory check alone.

In an interactive terminal, the prompt is the same one npx skills add shows: every agent is searchable, your last selection (shared with the skills CLI) or, failing that, the detected agents are pre-selected. Non-interactively (e.g. from the prepare hook), the detected set is used directly. Pass --agents (or set agents in the config) to bypass detection entirely.

Whatever you pick, .agents/skills is always linked: it is the shared directory read by every agent using the universal layout (Cursor, Codex, OpenCode, Amp, ...), so the committed links work for teammates on any agent.

When skills-npm itself runs inside a coding agent (Claude Code, Cursor, Codex, ...), it skips all prompts and targets that agent unless agents is set explicitly.

For Package Authors

Ship the skills you own in a skills/ directory:

my-tool/
├── package.json
├── dist/
└── skills/
    └── my-skill/
        └── SKILL.md

dist/skills/ and .agents/skills/ are scanned too, and a package that is a single skill can put SKILL.md at its root. These are the same locations skills experimental_sync looks in.

Curate skills you don't own in a skills field. A skills pack is just a package with that field and nothing else:

{
  "name": "@acme/frontend-skills",
  "version": "1.0.0",
  "skills": [
    "vercel-labs/agent-skills#v1.4.0@web-design-guidelines",
    "npm:@vueuse/skills"
  ],
  "dependencies": {
    "@vueuse/skills": "^1.0.0"
  }
}

See PROPOSAL.md and SPEC.md.

Showcases

Packages that ships their built-in skills:

[!NOTE] PR are welcome to add more packages that ships their built-in skills.

Sponsors

sponsors

License

MIT License © Anthony Fu