skills 1.4.0 → 1.4.1

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 +28 -24
  2. package/dist/cli.mjs +607 -89
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The CLI for the open agent skills ecosystem.
4
4
 
5
5
  <!-- agent-list:start -->
6
- Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [35 more](#available-agents).
6
+ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [37 more](#available-agents).
7
7
  <!-- agent-list:end -->
8
8
 
9
9
  ## Install a Skill
@@ -42,6 +42,7 @@ npx skills add ./my-local-skills
42
42
  | `-a, --agent <agents...>` | <!-- agent-names:start -->Target specific agents (e.g., `claude-code`, `codex`). See [Available Agents](#available-agents)<!-- agent-names:end --> |
43
43
  | `-s, --skill <skills...>` | Install specific skills by name (use `'*'` for all skills) |
44
44
  | `-l, --list` | List available skills without installing |
45
+ | `--copy` | Copy files instead of symlinking to agent directories |
45
46
  | `-y, --yes` | Skip all confirmation prompts |
46
47
  | `--all` | Install all skills to all agents without prompts |
47
48
 
@@ -91,14 +92,14 @@ When installing interactively, you can choose:
91
92
 
92
93
  ## Other Commands
93
94
 
94
- | Command | Description |
95
- | ---------------------------- | ------------------------------------------------------- |
96
- | `npx skills list` | List installed skills (alias: `ls`) |
97
- | `npx skills find [query]` | Search for skills interactively or by keyword |
98
- | `npx skills remove [skills]` | Remove installed skills from agents |
99
- | `npx skills check` | Check for available skill updates |
100
- | `npx skills update` | Update all installed skills to latest versions |
101
- | `npx skills init [name]` | Create a new SKILL.md template |
95
+ | Command | Description |
96
+ | ---------------------------- | ---------------------------------------------- |
97
+ | `npx skills list` | List installed skills (alias: `ls`) |
98
+ | `npx skills find [query]` | Search for skills interactively or by keyword |
99
+ | `npx skills remove [skills]` | Remove installed skills from agents |
100
+ | `npx skills check` | Check for available skill updates |
101
+ | `npx skills update` | Update all installed skills to latest versions |
102
+ | `npx skills init [name]` | Create a new SKILL.md template |
102
103
 
103
104
  ### `skills list`
104
105
 
@@ -180,13 +181,13 @@ npx skills remove my-skill --agent '*'
180
181
  npx skills rm my-skill
181
182
  ```
182
183
 
183
- | Option | Description |
184
- | ------------------- | ---------------------------------------------------- |
185
- | `-g, --global` | Remove from global scope (~/) instead of project |
186
- | `-a, --agent` | Remove from specific agents (use `'*'` for all) |
187
- | `-s, --skill` | Specify skills to remove (use `'*'` for all) |
188
- | `-y, --yes` | Skip confirmation prompts |
189
- | `--all` | Shorthand for `--skill '*' --agent '*' -y` |
184
+ | Option | Description |
185
+ | -------------- | ------------------------------------------------ |
186
+ | `-g, --global` | Remove from global scope (~/) instead of project |
187
+ | `-a, --agent` | Remove from specific agents (use `'*'` for all) |
188
+ | `-s, --skill` | Specify skills to remove (use `'*'` for all) |
189
+ | `-y, --yes` | Skip confirmation prompts |
190
+ | `--all` | Shorthand for `--skill '*' --agent '*' -y` |
190
191
 
191
192
  ## What are Agent Skills?
192
193
 
@@ -208,7 +209,7 @@ Skills can be installed to any of these agents:
208
209
  <!-- supported-agents:start -->
209
210
  | Agent | `--agent` | Project Path | Global Path |
210
211
  |-------|-----------|--------------|-------------|
211
- | Amp, Kimi Code CLI, Replit | `amp`, `kimi-cli`, `replit` | `.agents/skills/` | `~/.config/agents/skills/` |
212
+ | Amp, Kimi Code CLI, Replit, Universal | `amp`, `kimi-cli`, `replit`, `universal` | `.agents/skills/` | `~/.config/agents/skills/` |
212
213
  | Antigravity | `antigravity` | `.agent/skills/` | `~/.gemini/antigravity/skills/` |
213
214
  | Augment | `augment` | `.augment/skills/` | `~/.augment/skills/` |
214
215
  | Claude Code | `claude-code` | `.claude/skills/` | `~/.claude/skills/` |
@@ -218,8 +219,9 @@ Skills can be installed to any of these agents:
218
219
  | Codex | `codex` | `.agents/skills/` | `~/.codex/skills/` |
219
220
  | Command Code | `command-code` | `.commandcode/skills/` | `~/.commandcode/skills/` |
220
221
  | Continue | `continue` | `.continue/skills/` | `~/.continue/skills/` |
222
+ | Cortex Code | `cortex` | `.cortex/skills/` | `~/.snowflake/cortex/skills/` |
221
223
  | Crush | `crush` | `.crush/skills/` | `~/.config/crush/skills/` |
222
- | Cursor | `cursor` | `.cursor/skills/` | `~/.cursor/skills/` |
224
+ | Cursor | `cursor` | `.agents/skills/` | `~/.cursor/skills/` |
223
225
  | Droid | `droid` | `.factory/skills/` | `~/.factory/skills/` |
224
226
  | Gemini CLI | `gemini-cli` | `.agents/skills/` | `~/.gemini/skills/` |
225
227
  | GitHub Copilot | `github-copilot` | `.agents/skills/` | `~/.copilot/skills/` |
@@ -323,8 +325,8 @@ The CLI searches for skills in these locations within a repository:
323
325
  - `.codebuddy/skills/`
324
326
  - `.commandcode/skills/`
325
327
  - `.continue/skills/`
328
+ - `.cortex/skills/`
326
329
  - `.crush/skills/`
327
- - `.cursor/skills/`
328
330
  - `.factory/skills/`
329
331
  - `.goose/skills/`
330
332
  - `.junie/skills/`
@@ -356,11 +358,13 @@ If `.claude-plugin/marketplace.json` or `.claude-plugin/plugin.json` exists, ski
356
358
  // .claude-plugin/marketplace.json
357
359
  {
358
360
  "metadata": { "pluginRoot": "./plugins" },
359
- "plugins": [{
360
- "name": "my-plugin",
361
- "source": "my-plugin",
362
- "skills": ["./skills/review", "./skills/test"]
363
- }]
361
+ "plugins": [
362
+ {
363
+ "name": "my-plugin",
364
+ "source": "my-plugin",
365
+ "skills": ["./skills/review", "./skills/test"]
366
+ }
367
+ ]
364
368
  }
365
369
  ```
366
370
 
package/dist/cli.mjs CHANGED
@@ -13,7 +13,7 @@ import { execSync, spawn, spawnSync } from "child_process";
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
14
  import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
15
15
  import { homedir, platform, tmpdir } from "os";
16
- import "crypto";
16
+ import { createHash } from "crypto";
17
17
  import { fileURLToPath } from "url";
18
18
  import * as readline from "readline";
19
19
  import { Writable } from "stream";
@@ -180,7 +180,8 @@ const S_STEP_CANCEL = import_picocolors.default.red("■");
180
180
  const S_STEP_SUBMIT = import_picocolors.default.green("◇");
181
181
  const S_RADIO_ACTIVE = import_picocolors.default.green("●");
182
182
  const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
183
- const S_CHECKBOX_LOCKED = import_picocolors.default.green("✓");
183
+ import_picocolors.default.green("✓");
184
+ const S_BULLET = import_picocolors.default.green("•");
184
185
  const S_BAR = import_picocolors.default.dim("│");
185
186
  const S_BAR_H = import_picocolors.default.dim("─");
186
187
  const cancelSymbol = Symbol("cancel");
@@ -223,10 +224,11 @@ async function searchMultiselect(options) {
223
224
  if (state === "active") {
224
225
  if (lockedSection && lockedSection.items.length > 0) {
225
226
  lines.push(`${S_BAR}`);
226
- lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold(lockedSection.title)} ${S_BAR_H.repeat(30)}`);
227
- for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_CHECKBOX_LOCKED} ${item.label}`);
227
+ const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
228
+ lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
229
+ for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
228
230
  lines.push(`${S_BAR}`);
229
- lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Other agents")} ${S_BAR_H.repeat(34)}`);
231
+ lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
230
232
  }
231
233
  const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
232
234
  lines.push(searchLine);
@@ -348,7 +350,13 @@ var GitCloneError = class extends Error {
348
350
  };
349
351
  async function cloneRepo(url, ref) {
350
352
  const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
351
- const git = esm_default({ timeout: { block: CLONE_TIMEOUT_MS } });
353
+ const git = esm_default({
354
+ timeout: { block: CLONE_TIMEOUT_MS },
355
+ env: {
356
+ ...process.env,
357
+ GIT_TERMINAL_PROMPT: "0"
358
+ }
359
+ });
352
360
  const cloneOptions = ref ? [
353
361
  "--depth",
354
362
  "1",
@@ -489,7 +497,6 @@ async function discoverSkills(basePath, subpath, options) {
489
497
  join(searchPath, ".codex/skills"),
490
498
  join(searchPath, ".commandcode/skills"),
491
499
  join(searchPath, ".continue/skills"),
492
- join(searchPath, ".cursor/skills"),
493
500
  join(searchPath, ".github/skills"),
494
501
  join(searchPath, ".goose/skills"),
495
502
  join(searchPath, ".iflow/skills"),
@@ -570,7 +577,7 @@ const agents = {
570
577
  skillsDir: ".agent/skills",
571
578
  globalSkillsDir: join(home, ".gemini/antigravity/skills"),
572
579
  detectInstalled: async () => {
573
- return existsSync(join(process.cwd(), ".agent")) || existsSync(join(home, ".gemini/antigravity"));
580
+ return existsSync(join(home, ".gemini/antigravity"));
574
581
  }
575
582
  },
576
583
  augment: {
@@ -645,6 +652,15 @@ const agents = {
645
652
  return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"));
646
653
  }
647
654
  },
655
+ cortex: {
656
+ name: "cortex",
657
+ displayName: "Cortex Code",
658
+ skillsDir: ".cortex/skills",
659
+ globalSkillsDir: join(home, ".snowflake/cortex/skills"),
660
+ detectInstalled: async () => {
661
+ return existsSync(join(home, ".snowflake/cortex"));
662
+ }
663
+ },
648
664
  crush: {
649
665
  name: "crush",
650
666
  displayName: "Crush",
@@ -657,7 +673,7 @@ const agents = {
657
673
  cursor: {
658
674
  name: "cursor",
659
675
  displayName: "Cursor",
660
- skillsDir: ".cursor/skills",
676
+ skillsDir: ".agents/skills",
661
677
  globalSkillsDir: join(home, ".cursor/skills"),
662
678
  detectInstalled: async () => {
663
679
  return existsSync(join(home, ".cursor"));
@@ -687,7 +703,7 @@ const agents = {
687
703
  skillsDir: ".agents/skills",
688
704
  globalSkillsDir: join(home, ".copilot/skills"),
689
705
  detectInstalled: async () => {
690
- return existsSync(join(process.cwd(), ".github")) || existsSync(join(home, ".copilot"));
706
+ return existsSync(join(home, ".copilot"));
691
707
  }
692
708
  },
693
709
  goose: {
@@ -786,7 +802,7 @@ const agents = {
786
802
  skillsDir: ".agents/skills",
787
803
  globalSkillsDir: join(configHome, "opencode/skills"),
788
804
  detectInstalled: async () => {
789
- return existsSync(join(configHome, "opencode")) || existsSync(join(claudeHome, "skills"));
805
+ return existsSync(join(configHome, "opencode"));
790
806
  }
791
807
  },
792
808
  openhands: {
@@ -832,7 +848,7 @@ const agents = {
832
848
  globalSkillsDir: join(configHome, "agents/skills"),
833
849
  showInUniversalList: false,
834
850
  detectInstalled: async () => {
835
- return existsSync(join(process.cwd(), ".agents"));
851
+ return existsSync(join(process.cwd(), ".replit"));
836
852
  }
837
853
  },
838
854
  roo: {
@@ -906,6 +922,14 @@ const agents = {
906
922
  detectInstalled: async () => {
907
923
  return existsSync(join(home, ".adal"));
908
924
  }
925
+ },
926
+ universal: {
927
+ name: "universal",
928
+ displayName: "Universal",
929
+ skillsDir: ".agents/skills",
930
+ globalSkillsDir: join(configHome, "agents/skills"),
931
+ showInUniversalList: false,
932
+ detectInstalled: async () => false
909
933
  }
910
934
  };
911
935
  async function detectInstalledAgents() {
@@ -936,6 +960,16 @@ function isPathSafe(basePath, targetPath) {
936
960
  function getCanonicalSkillsDir(global, cwd) {
937
961
  return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$2, SKILLS_SUBDIR);
938
962
  }
963
+ function getAgentBaseDir(agentType, global, cwd) {
964
+ if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
965
+ const agent = agents[agentType];
966
+ const baseDir = global ? homedir() : cwd || process.cwd();
967
+ if (global) {
968
+ if (agent.globalSkillsDir === void 0) return join(baseDir, agent.skillsDir);
969
+ return agent.globalSkillsDir;
970
+ }
971
+ return join(baseDir, agent.skillsDir);
972
+ }
939
973
  function resolveSymlinkTarget(linkPath, linkTarget) {
940
974
  return resolve(dirname(linkPath), linkTarget);
941
975
  }
@@ -961,7 +995,9 @@ async function resolveParentSymlinks(path) {
961
995
  async function createSymlink(target, linkPath) {
962
996
  try {
963
997
  const resolvedTarget = resolve(target);
964
- if (resolvedTarget === resolve(linkPath)) return true;
998
+ const resolvedLinkPath = resolve(linkPath);
999
+ const [realTarget, realLinkPath] = await Promise.all([realpath(resolvedTarget).catch(() => resolvedTarget), realpath(resolvedLinkPath).catch(() => resolvedLinkPath)]);
1000
+ if (realTarget === realLinkPath) return true;
965
1001
  if (await resolveParentSymlinks(target) === await resolveParentSymlinks(linkPath)) return true;
966
1002
  try {
967
1003
  if ((await lstat(linkPath)).isSymbolicLink()) {
@@ -994,7 +1030,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
994
1030
  const skillName = sanitizeName(skill.name || basename(skill.path));
995
1031
  const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
996
1032
  const canonicalDir = join(canonicalBase, skillName);
997
- const agentBase = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
1033
+ const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
998
1034
  const agentDir = join(agentBase, skillName);
999
1035
  const installMode = options.mode ?? "symlink";
1000
1036
  if (!isPathSafe(canonicalBase, canonicalDir)) return {
@@ -1053,7 +1089,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
1053
1089
  };
1054
1090
  }
1055
1091
  }
1056
- const EXCLUDE_FILES = new Set(["README.md", "metadata.json"]);
1092
+ const EXCLUDE_FILES = new Set(["metadata.json"]);
1057
1093
  const EXCLUDE_DIRS = new Set([".git"]);
1058
1094
  const isExcluded = (name, isDirectory = false) => {
1059
1095
  if (EXCLUDE_FILES.has(name)) return true;
@@ -1089,10 +1125,10 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
1089
1125
  }
1090
1126
  }
1091
1127
  function getInstallPath(skillName, agentType, options = {}) {
1092
- const agent = agents[agentType];
1093
- const cwd = options.cwd || process.cwd();
1128
+ agents[agentType];
1129
+ options.cwd || process.cwd();
1094
1130
  const sanitized = sanitizeName(skillName);
1095
- const targetBase = options.global && agent.globalSkillsDir !== void 0 ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
1131
+ const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd);
1096
1132
  const installPath = join(targetBase, sanitized);
1097
1133
  if (!isPathSafe(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
1098
1134
  return installPath;
@@ -1118,7 +1154,7 @@ async function installRemoteSkillForAgent(skill, agentType, options = {}) {
1118
1154
  const skillName = sanitizeName(skill.installName);
1119
1155
  const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
1120
1156
  const canonicalDir = join(canonicalBase, skillName);
1121
- const agentBase = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
1157
+ const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
1122
1158
  const agentDir = join(agentBase, skillName);
1123
1159
  if (!isPathSafe(canonicalBase, canonicalDir)) return {
1124
1160
  success: false,
@@ -1190,7 +1226,7 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1190
1226
  const skillName = sanitizeName(skill.installName);
1191
1227
  const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
1192
1228
  const canonicalDir = join(canonicalBase, skillName);
1193
- const agentBase = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
1229
+ const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
1194
1230
  const agentDir = join(agentBase, skillName);
1195
1231
  if (!isPathSafe(canonicalBase, canonicalDir)) return {
1196
1232
  success: false,
@@ -1718,7 +1754,7 @@ async function fetchMintlifySkill(url) {
1718
1754
  }
1719
1755
  const AGENTS_DIR$1 = ".agents";
1720
1756
  const LOCK_FILE$1 = ".skill-lock.json";
1721
- const CURRENT_VERSION = 3;
1757
+ const CURRENT_VERSION$1 = 3;
1722
1758
  function getSkillLockPath$1() {
1723
1759
  return join(homedir(), AGENTS_DIR$1, LOCK_FILE$1);
1724
1760
  }
@@ -1728,7 +1764,7 @@ async function readSkillLock$1() {
1728
1764
  const content = await readFile(lockPath, "utf-8");
1729
1765
  const parsed = JSON.parse(content);
1730
1766
  if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLockFile();
1731
- if (parsed.version < CURRENT_VERSION) return createEmptyLockFile();
1767
+ if (parsed.version < CURRENT_VERSION$1) return createEmptyLockFile();
1732
1768
  return parsed;
1733
1769
  } catch (error) {
1734
1770
  return createEmptyLockFile();
@@ -1801,7 +1837,7 @@ async function getSkillFromLock(skillName) {
1801
1837
  }
1802
1838
  function createEmptyLockFile() {
1803
1839
  return {
1804
- version: CURRENT_VERSION,
1840
+ version: CURRENT_VERSION$1,
1805
1841
  skills: {},
1806
1842
  dismissed: {}
1807
1843
  };
@@ -1823,8 +1859,74 @@ async function saveSelectedAgents(agents) {
1823
1859
  lock.lastSelectedAgents = agents;
1824
1860
  await writeSkillLock(lock);
1825
1861
  }
1826
- var version$1 = "1.4.0";
1827
- const isCancelled = (value) => typeof value === "symbol";
1862
+ const LOCAL_LOCK_FILE = "skills-lock.json";
1863
+ const CURRENT_VERSION = 1;
1864
+ function getLocalLockPath(cwd) {
1865
+ return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
1866
+ }
1867
+ async function readLocalLock(cwd) {
1868
+ const lockPath = getLocalLockPath(cwd);
1869
+ try {
1870
+ const content = await readFile(lockPath, "utf-8");
1871
+ const parsed = JSON.parse(content);
1872
+ if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
1873
+ if (parsed.version < CURRENT_VERSION) return createEmptyLocalLock();
1874
+ return parsed;
1875
+ } catch {
1876
+ return createEmptyLocalLock();
1877
+ }
1878
+ }
1879
+ async function writeLocalLock(lock, cwd) {
1880
+ const lockPath = getLocalLockPath(cwd);
1881
+ const sortedSkills = {};
1882
+ for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
1883
+ const sorted = {
1884
+ version: lock.version,
1885
+ skills: sortedSkills
1886
+ };
1887
+ await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
1888
+ }
1889
+ async function computeSkillFolderHash(skillDir) {
1890
+ const files = [];
1891
+ await collectFiles(skillDir, skillDir, files);
1892
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
1893
+ const hash = createHash("sha256");
1894
+ for (const file of files) {
1895
+ hash.update(file.relativePath);
1896
+ hash.update(file.content);
1897
+ }
1898
+ return hash.digest("hex");
1899
+ }
1900
+ async function collectFiles(baseDir, currentDir, results) {
1901
+ const entries = await readdir(currentDir, { withFileTypes: true });
1902
+ await Promise.all(entries.map(async (entry) => {
1903
+ const fullPath = join(currentDir, entry.name);
1904
+ if (entry.isDirectory()) {
1905
+ if (entry.name === ".git" || entry.name === "node_modules") return;
1906
+ await collectFiles(baseDir, fullPath, results);
1907
+ } else if (entry.isFile()) {
1908
+ const content = await readFile(fullPath);
1909
+ const relativePath = relative(baseDir, fullPath).split("\\").join("/");
1910
+ results.push({
1911
+ relativePath,
1912
+ content
1913
+ });
1914
+ }
1915
+ }));
1916
+ }
1917
+ async function addSkillToLocalLock(skillName, entry, cwd) {
1918
+ const lock = await readLocalLock(cwd);
1919
+ lock.skills[skillName] = entry;
1920
+ await writeLocalLock(lock, cwd);
1921
+ }
1922
+ function createEmptyLocalLock() {
1923
+ return {
1924
+ version: CURRENT_VERSION,
1925
+ skills: {}
1926
+ };
1927
+ }
1928
+ var version$1 = "1.4.1";
1929
+ const isCancelled$1 = (value) => typeof value === "symbol";
1828
1930
  async function isSourcePrivate(source) {
1829
1931
  const ownerRepo = parseOwnerRepo(source);
1830
1932
  if (!ownerRepo) return false;
@@ -1875,7 +1977,7 @@ function buildSecurityLines(auditData, skills, source) {
1875
1977
  lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
1876
1978
  return lines;
1877
1979
  }
1878
- function shortenPath$1(fullPath, cwd) {
1980
+ function shortenPath$2(fullPath, cwd) {
1879
1981
  const home = homedir();
1880
1982
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
1881
1983
  if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
@@ -1952,7 +2054,7 @@ async function promptForAgents(message, choices) {
1952
2054
  initialSelected: initialValues,
1953
2055
  required: true
1954
2056
  });
1955
- if (!isCancelled(selected)) try {
2057
+ if (!isCancelled$1(selected)) try {
1956
2058
  await saveSelectedAgents(selected);
1957
2059
  } catch {}
1958
2060
  return selected;
@@ -1983,7 +2085,7 @@ async function selectAgentsInteractive(options) {
1983
2085
  initialSelected: lastSelected ? lastSelected.filter((a) => otherAgents.includes(a) && !universalAgents.includes(a)) : [],
1984
2086
  lockedSection: universalSection
1985
2087
  });
1986
- if (!isCancelled(selected)) try {
2088
+ if (!isCancelled$1(selected)) try {
1987
2089
  await saveSelectedAgents(selected);
1988
2090
  } catch {}
1989
2091
  return selected;
@@ -2040,7 +2142,7 @@ async function handleRemoteSkill(source, url, options, spinner) {
2040
2142
  M.info(`Valid agents: ${validAgents.join(", ")}`);
2041
2143
  process.exit(1);
2042
2144
  }
2043
- targetAgents = ensureUniversalAgents(options.agent);
2145
+ targetAgents = options.agent;
2044
2146
  } else {
2045
2147
  spinner.start("Loading agents...");
2046
2148
  const installedAgents = await detectInstalledAgents();
@@ -2092,8 +2194,8 @@ async function handleRemoteSkill(source, url, options, spinner) {
2092
2194
  }
2093
2195
  installGlobally = scope;
2094
2196
  }
2095
- let installMode = "symlink";
2096
- if (!options.yes) {
2197
+ let installMode = options.copy ? "copy" : "symlink";
2198
+ if (!options.copy && !options.yes) {
2097
2199
  const modeChoice = await ve({
2098
2200
  message: "Installation method",
2099
2201
  options: [{
@@ -2119,7 +2221,7 @@ async function handleRemoteSkill(source, url, options, spinner) {
2119
2221
  })));
2120
2222
  const overwriteStatus = new Map(overwriteChecks.map(({ agent, installed }) => [agent, installed]));
2121
2223
  const summaryLines = [];
2122
- const shortCanonical = shortenPath$1(getCanonicalPath(remoteSkill.installName, { global: installGlobally }), cwd);
2224
+ const shortCanonical = shortenPath$2(getCanonicalPath(remoteSkill.installName, { global: installGlobally }), cwd);
2123
2225
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
2124
2226
  summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
2125
2227
  const overwriteAgents = targetAgents.filter((a) => overwriteStatus.get(a)).map((a) => agents[a].displayName);
@@ -2172,18 +2274,27 @@ async function handleRemoteSkill(source, url, options, spinner) {
2172
2274
  skillFolderHash
2173
2275
  });
2174
2276
  } catch {}
2277
+ if (successful.length > 0 && !installGlobally) try {
2278
+ const firstResult = successful[0];
2279
+ const computedHash = await computeSkillFolderHash(firstResult.canonicalPath || firstResult.path);
2280
+ await addSkillToLocalLock(remoteSkill.installName, {
2281
+ source: remoteSkill.sourceIdentifier,
2282
+ sourceType: remoteSkill.providerId,
2283
+ computedHash
2284
+ }, cwd);
2285
+ } catch {}
2175
2286
  if (successful.length > 0) {
2176
2287
  const resultLines = [];
2177
2288
  const firstResult = successful[0];
2178
2289
  if (firstResult.mode === "copy") {
2179
2290
  resultLines.push(`${import_picocolors.default.green("✓")} ${remoteSkill.installName} ${import_picocolors.default.dim("(copied)")}`);
2180
2291
  for (const r of successful) {
2181
- const shortPath = shortenPath$1(r.path, cwd);
2292
+ const shortPath = shortenPath$2(r.path, cwd);
2182
2293
  resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
2183
2294
  }
2184
2295
  } else {
2185
2296
  if (firstResult.canonicalPath) {
2186
- const shortPath = shortenPath$1(firstResult.canonicalPath, cwd);
2297
+ const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
2187
2298
  resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
2188
2299
  } else resultLines.push(`${import_picocolors.default.green("✓")} ${remoteSkill.installName}`);
2189
2300
  resultLines.push(...buildResultLines(successful, targetAgents));
@@ -2302,7 +2413,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
2302
2413
  targetAgents = selected;
2303
2414
  }
2304
2415
  else if (installedAgents.length === 1 || options.yes) {
2305
- targetAgents = installedAgents;
2416
+ targetAgents = ensureUniversalAgents(installedAgents);
2306
2417
  if (installedAgents.length === 1) {
2307
2418
  const firstAgent = installedAgents[0];
2308
2419
  M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
@@ -2337,8 +2448,8 @@ async function handleWellKnownSkills(source, url, options, spinner) {
2337
2448
  }
2338
2449
  installGlobally = scope;
2339
2450
  }
2340
- let installMode = "symlink";
2341
- if (!options.yes) {
2451
+ let installMode = options.copy ? "copy" : "symlink";
2452
+ if (!options.copy && !options.yes) {
2342
2453
  const modeChoice = await ve({
2343
2454
  message: "Installation method",
2344
2455
  options: [{
@@ -2372,7 +2483,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
2372
2483
  }
2373
2484
  for (const skill of selectedSkills) {
2374
2485
  if (summaryLines.length > 0) summaryLines.push("");
2375
- const shortCanonical = shortenPath$1(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
2486
+ const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
2376
2487
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
2377
2488
  summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
2378
2489
  if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
@@ -2429,6 +2540,21 @@ async function handleWellKnownSkills(source, url, options, spinner) {
2429
2540
  });
2430
2541
  } catch {}
2431
2542
  }
2543
+ if (successful.length > 0 && !installGlobally) {
2544
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
2545
+ for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
2546
+ const matchingResult = successful.find((r) => r.skill === skill.installName);
2547
+ const installDir = matchingResult?.canonicalPath || matchingResult?.path;
2548
+ if (installDir) {
2549
+ const computedHash = await computeSkillFolderHash(installDir);
2550
+ await addSkillToLocalLock(skill.installName, {
2551
+ source: sourceIdentifier,
2552
+ sourceType: "well-known",
2553
+ computedHash
2554
+ }, cwd);
2555
+ }
2556
+ } catch {}
2557
+ }
2432
2558
  if (successful.length > 0) {
2433
2559
  const bySkill = /* @__PURE__ */ new Map();
2434
2560
  for (const r of successful) {
@@ -2445,12 +2571,12 @@ async function handleWellKnownSkills(source, url, options, spinner) {
2445
2571
  if (firstResult.mode === "copy") {
2446
2572
  resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
2447
2573
  for (const r of skillResults) {
2448
- const shortPath = shortenPath$1(r.path, cwd);
2574
+ const shortPath = shortenPath$2(r.path, cwd);
2449
2575
  resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
2450
2576
  }
2451
2577
  } else {
2452
2578
  if (firstResult.canonicalPath) {
2453
- const shortPath = shortenPath$1(firstResult.canonicalPath, cwd);
2579
+ const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
2454
2580
  resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
2455
2581
  } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
2456
2582
  resultLines.push(...buildResultLines(skillResults, targetAgents));
@@ -2536,7 +2662,7 @@ async function handleDirectUrlSkillLegacy(source, url, options, spinner) {
2536
2662
  targetAgents = selected;
2537
2663
  }
2538
2664
  else if (installedAgents.length === 1 || options.yes) {
2539
- targetAgents = installedAgents;
2665
+ targetAgents = ensureUniversalAgents(installedAgents);
2540
2666
  if (installedAgents.length === 1) {
2541
2667
  const firstAgent = installedAgents[0];
2542
2668
  M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
@@ -2580,7 +2706,7 @@ async function handleDirectUrlSkillLegacy(source, url, options, spinner) {
2580
2706
  const overwriteStatus = new Map(overwriteChecks.map(({ agent, installed }) => [agent, installed]));
2581
2707
  const summaryLines = [];
2582
2708
  targetAgents.map((a) => agents[a].displayName);
2583
- const shortCanonical = shortenPath$1(getCanonicalPath(remoteSkill.installName, { global: installGlobally }), cwd);
2709
+ const shortCanonical = shortenPath$2(getCanonicalPath(remoteSkill.installName, { global: installGlobally }), cwd);
2584
2710
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
2585
2711
  summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
2586
2712
  const overwriteAgents = targetAgents.filter((a) => overwriteStatus.get(a)).map((a) => agents[a].displayName);
@@ -2632,7 +2758,7 @@ async function handleDirectUrlSkillLegacy(source, url, options, spinner) {
2632
2758
  const resultLines = [];
2633
2759
  const firstResult = successful[0];
2634
2760
  if (firstResult.canonicalPath) {
2635
- const shortPath = shortenPath$1(firstResult.canonicalPath, cwd);
2761
+ const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
2636
2762
  resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
2637
2763
  } else resultLines.push(`${import_picocolors.default.green("✓")} ${remoteSkill.installName}`);
2638
2764
  resultLines.push(...buildResultLines(successful, targetAgents));
@@ -2818,7 +2944,7 @@ async function runAdd(args, options = {}) {
2818
2944
  targetAgents = selected;
2819
2945
  }
2820
2946
  else if (installedAgents.length === 1 || options.yes) {
2821
- targetAgents = installedAgents;
2947
+ targetAgents = ensureUniversalAgents(installedAgents);
2822
2948
  if (installedAgents.length === 1) {
2823
2949
  const firstAgent = installedAgents[0];
2824
2950
  M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
@@ -2855,8 +2981,8 @@ async function runAdd(args, options = {}) {
2855
2981
  }
2856
2982
  installGlobally = scope;
2857
2983
  }
2858
- let installMode = "symlink";
2859
- if (!options.yes) {
2984
+ let installMode = options.copy ? "copy" : "symlink";
2985
+ if (!options.copy && !options.yes) {
2860
2986
  const modeChoice = await ve({
2861
2987
  message: "Installation method",
2862
2988
  options: [{
@@ -2891,7 +3017,7 @@ async function runAdd(args, options = {}) {
2891
3017
  }
2892
3018
  for (const skill of selectedSkills) {
2893
3019
  if (summaryLines.length > 0) summaryLines.push("");
2894
- const shortCanonical = shortenPath$1(getCanonicalPath(skill.name, { global: installGlobally }), cwd);
3020
+ const shortCanonical = shortenPath$2(getCanonicalPath(skill.name, { global: installGlobally }), cwd);
2895
3021
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
2896
3022
  summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
2897
3023
  const skillOverwrites = overwriteStatus.get(skill.name);
@@ -2985,6 +3111,20 @@ async function runAdd(args, options = {}) {
2985
3111
  } catch {}
2986
3112
  }
2987
3113
  }
3114
+ if (successful.length > 0 && !installGlobally) {
3115
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
3116
+ for (const skill of selectedSkills) {
3117
+ const skillDisplayName = getSkillDisplayName(skill);
3118
+ if (successfulSkillNames.has(skillDisplayName)) try {
3119
+ const computedHash = await computeSkillFolderHash(skill.path);
3120
+ await addSkillToLocalLock(skill.name, {
3121
+ source: normalizedSource || parsed.url,
3122
+ sourceType: parsed.type,
3123
+ computedHash
3124
+ }, cwd);
3125
+ } catch {}
3126
+ }
3127
+ }
2988
3128
  if (successful.length > 0) {
2989
3129
  const bySkill = /* @__PURE__ */ new Map();
2990
3130
  for (const r of successful) {
@@ -3001,12 +3141,12 @@ async function runAdd(args, options = {}) {
3001
3141
  if (firstResult.mode === "copy") {
3002
3142
  resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
3003
3143
  for (const r of skillResults) {
3004
- const shortPath = shortenPath$1(r.path, cwd);
3144
+ const shortPath = shortenPath$2(r.path, cwd);
3005
3145
  resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
3006
3146
  }
3007
3147
  } else {
3008
3148
  if (firstResult.canonicalPath) {
3009
- const shortPath = shortenPath$1(firstResult.canonicalPath, cwd);
3149
+ const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
3010
3150
  resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
3011
3151
  } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
3012
3152
  resultLines.push(...buildResultLines(skillResults, targetAgents));
@@ -3113,6 +3253,7 @@ function parseAddOptions(args) {
3113
3253
  }
3114
3254
  i--;
3115
3255
  } else if (arg === "--full-depth") options.fullDepth = true;
3256
+ else if (arg === "--copy") options.copy = true;
3116
3257
  else if (arg && !arg.startsWith("-")) source.push(arg);
3117
3258
  }
3118
3259
  return {
@@ -3342,6 +3483,340 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
3342
3483
  else console.log(`${DIM$2}Discover more skills at${RESET$2} ${TEXT$1}https://skills.sh${RESET$2}`);
3343
3484
  console.log();
3344
3485
  }
3486
+ const isCancelled = (value) => typeof value === "symbol";
3487
+ function shortenPath$1(fullPath, cwd) {
3488
+ const home = homedir();
3489
+ if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
3490
+ if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
3491
+ return fullPath;
3492
+ }
3493
+ async function discoverNodeModuleSkills(cwd) {
3494
+ const nodeModulesDir = join(cwd, "node_modules");
3495
+ const skills = [];
3496
+ let topNames;
3497
+ try {
3498
+ topNames = await readdir(nodeModulesDir);
3499
+ } catch {
3500
+ return skills;
3501
+ }
3502
+ const processPackageDir = async (pkgDir, packageName) => {
3503
+ const rootSkill = await parseSkillMd(join(pkgDir, "SKILL.md"));
3504
+ if (rootSkill) {
3505
+ skills.push({
3506
+ ...rootSkill,
3507
+ packageName
3508
+ });
3509
+ return;
3510
+ }
3511
+ const searchDirs = [
3512
+ pkgDir,
3513
+ join(pkgDir, "skills"),
3514
+ join(pkgDir, ".agents", "skills")
3515
+ ];
3516
+ for (const searchDir of searchDirs) try {
3517
+ const entries = await readdir(searchDir);
3518
+ for (const name of entries) {
3519
+ const skillDir = join(searchDir, name);
3520
+ try {
3521
+ if (!(await stat(skillDir)).isDirectory()) continue;
3522
+ } catch {
3523
+ continue;
3524
+ }
3525
+ const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
3526
+ if (skill) skills.push({
3527
+ ...skill,
3528
+ packageName
3529
+ });
3530
+ }
3531
+ } catch {}
3532
+ };
3533
+ await Promise.all(topNames.map(async (name) => {
3534
+ if (name.startsWith(".")) return;
3535
+ const fullPath = join(nodeModulesDir, name);
3536
+ try {
3537
+ if (!(await stat(fullPath)).isDirectory()) return;
3538
+ } catch {
3539
+ return;
3540
+ }
3541
+ if (name.startsWith("@")) try {
3542
+ const scopeNames = await readdir(fullPath);
3543
+ await Promise.all(scopeNames.map(async (scopedName) => {
3544
+ const scopedPath = join(fullPath, scopedName);
3545
+ try {
3546
+ if (!(await stat(scopedPath)).isDirectory()) return;
3547
+ } catch {
3548
+ return;
3549
+ }
3550
+ await processPackageDir(scopedPath, `${name}/${scopedName}`);
3551
+ }));
3552
+ } catch {}
3553
+ else await processPackageDir(fullPath, name);
3554
+ }));
3555
+ return skills;
3556
+ }
3557
+ async function runSync(args, options = {}) {
3558
+ const cwd = process.cwd();
3559
+ console.log();
3560
+ Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills experimental_sync ")));
3561
+ const spinner = Y();
3562
+ spinner.start("Scanning node_modules for skills...");
3563
+ const discoveredSkills = await discoverNodeModuleSkills(cwd);
3564
+ if (discoveredSkills.length === 0) {
3565
+ spinner.stop(import_picocolors.default.yellow("No skills found"));
3566
+ Se(import_picocolors.default.dim("No SKILL.md files found in node_modules."));
3567
+ return;
3568
+ }
3569
+ spinner.stop(`Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
3570
+ for (const skill of discoveredSkills) {
3571
+ M.info(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}`);
3572
+ if (skill.description) M.message(import_picocolors.default.dim(` ${skill.description}`));
3573
+ }
3574
+ const localLock = await readLocalLock(cwd);
3575
+ const toInstall = [];
3576
+ const upToDate = [];
3577
+ if (options.force) {
3578
+ toInstall.push(...discoveredSkills);
3579
+ M.info(import_picocolors.default.dim("Force mode: reinstalling all skills"));
3580
+ } else {
3581
+ for (const skill of discoveredSkills) {
3582
+ const existingEntry = localLock.skills[skill.name];
3583
+ if (existingEntry) {
3584
+ if (await computeSkillFolderHash(skill.path) === existingEntry.computedHash) {
3585
+ upToDate.push(skill.name);
3586
+ continue;
3587
+ }
3588
+ }
3589
+ toInstall.push(skill);
3590
+ }
3591
+ if (upToDate.length > 0) M.info(import_picocolors.default.dim(`${upToDate.length} skill${upToDate.length !== 1 ? "s" : ""} already up to date`));
3592
+ if (toInstall.length === 0) {
3593
+ console.log();
3594
+ Se(import_picocolors.default.green("All skills are up to date."));
3595
+ return;
3596
+ }
3597
+ }
3598
+ M.info(`${toInstall.length} skill${toInstall.length !== 1 ? "s" : ""} to install/update`);
3599
+ let targetAgents;
3600
+ const validAgents = Object.keys(agents);
3601
+ const universalAgents = getUniversalAgents();
3602
+ if (options.agent?.includes("*")) {
3603
+ targetAgents = validAgents;
3604
+ M.info(`Installing to all ${targetAgents.length} agents`);
3605
+ } else if (options.agent && options.agent.length > 0) {
3606
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
3607
+ if (invalidAgents.length > 0) {
3608
+ M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
3609
+ M.info(`Valid agents: ${validAgents.join(", ")}`);
3610
+ process.exit(1);
3611
+ }
3612
+ targetAgents = options.agent;
3613
+ } else {
3614
+ spinner.start("Loading agents...");
3615
+ const installedAgents = await detectInstalledAgents();
3616
+ const totalAgents = Object.keys(agents).length;
3617
+ spinner.stop(`${totalAgents} agents`);
3618
+ if (installedAgents.length === 0) if (options.yes) {
3619
+ targetAgents = universalAgents;
3620
+ M.info("Installing to universal agents");
3621
+ } else {
3622
+ const selected = await searchMultiselect({
3623
+ message: "Which agents do you want to install to?",
3624
+ items: getNonUniversalAgents().map((a) => ({
3625
+ value: a,
3626
+ label: agents[a].displayName,
3627
+ hint: agents[a].skillsDir
3628
+ })),
3629
+ initialSelected: [],
3630
+ lockedSection: {
3631
+ title: "Universal (.agents/skills)",
3632
+ items: universalAgents.map((a) => ({
3633
+ value: a,
3634
+ label: agents[a].displayName
3635
+ }))
3636
+ }
3637
+ });
3638
+ if (isCancelled(selected)) {
3639
+ xe("Sync cancelled");
3640
+ process.exit(0);
3641
+ }
3642
+ targetAgents = selected;
3643
+ }
3644
+ else if (installedAgents.length === 1 || options.yes) {
3645
+ targetAgents = [...installedAgents];
3646
+ for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
3647
+ } else {
3648
+ const selected = await searchMultiselect({
3649
+ message: "Which agents do you want to install to?",
3650
+ items: getNonUniversalAgents().filter((a) => installedAgents.includes(a)).map((a) => ({
3651
+ value: a,
3652
+ label: agents[a].displayName,
3653
+ hint: agents[a].skillsDir
3654
+ })),
3655
+ initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
3656
+ lockedSection: {
3657
+ title: "Universal (.agents/skills)",
3658
+ items: universalAgents.map((a) => ({
3659
+ value: a,
3660
+ label: agents[a].displayName
3661
+ }))
3662
+ }
3663
+ });
3664
+ if (isCancelled(selected)) {
3665
+ xe("Sync cancelled");
3666
+ process.exit(0);
3667
+ }
3668
+ targetAgents = selected;
3669
+ }
3670
+ }
3671
+ const summaryLines = [];
3672
+ for (const skill of toInstall) {
3673
+ const shortCanonical = shortenPath$1(getCanonicalPath(skill.name, { global: false }), cwd);
3674
+ summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${skill.packageName}`)}`);
3675
+ summaryLines.push(` ${import_picocolors.default.dim(shortCanonical)}`);
3676
+ }
3677
+ console.log();
3678
+ Me(summaryLines.join("\n"), "Sync Summary");
3679
+ if (!options.yes) {
3680
+ const confirmed = await ye({ message: "Proceed with sync?" });
3681
+ if (pD(confirmed) || !confirmed) {
3682
+ xe("Sync cancelled");
3683
+ process.exit(0);
3684
+ }
3685
+ }
3686
+ spinner.start("Syncing skills...");
3687
+ const results = [];
3688
+ for (const skill of toInstall) for (const agent of targetAgents) {
3689
+ const result = await installSkillForAgent(skill, agent, {
3690
+ global: false,
3691
+ cwd,
3692
+ mode: "symlink"
3693
+ });
3694
+ results.push({
3695
+ skill: skill.name,
3696
+ packageName: skill.packageName,
3697
+ agent: agents[agent].displayName,
3698
+ success: result.success,
3699
+ path: result.path,
3700
+ canonicalPath: result.canonicalPath,
3701
+ error: result.error
3702
+ });
3703
+ }
3704
+ spinner.stop("Sync complete");
3705
+ const successful = results.filter((r) => r.success);
3706
+ const failed = results.filter((r) => !r.success);
3707
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
3708
+ for (const skill of toInstall) if (successfulSkillNames.has(skill.name)) try {
3709
+ const computedHash = await computeSkillFolderHash(skill.path);
3710
+ await addSkillToLocalLock(skill.name, {
3711
+ source: skill.packageName,
3712
+ sourceType: "node_modules",
3713
+ computedHash
3714
+ }, cwd);
3715
+ } catch {}
3716
+ console.log();
3717
+ if (successful.length > 0) {
3718
+ const bySkill = /* @__PURE__ */ new Map();
3719
+ for (const r of successful) {
3720
+ const skillResults = bySkill.get(r.skill) || [];
3721
+ skillResults.push(r);
3722
+ bySkill.set(r.skill, skillResults);
3723
+ }
3724
+ const resultLines = [];
3725
+ for (const [skillName, skillResults] of bySkill) {
3726
+ const firstResult = skillResults[0];
3727
+ const pkg = toInstall.find((s) => s.name === skillName)?.packageName;
3728
+ if (firstResult.canonicalPath) {
3729
+ const shortPath = shortenPath$1(firstResult.canonicalPath, cwd);
3730
+ resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim(`← ${pkg}`)}`);
3731
+ resultLines.push(` ${import_picocolors.default.dim(shortPath)}`);
3732
+ } else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim(`← ${pkg}`)}`);
3733
+ }
3734
+ const skillCount = bySkill.size;
3735
+ const title = import_picocolors.default.green(`Synced ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
3736
+ Me(resultLines.join("\n"), title);
3737
+ }
3738
+ if (failed.length > 0) {
3739
+ console.log();
3740
+ M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
3741
+ for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
3742
+ }
3743
+ track({
3744
+ event: "experimental_sync",
3745
+ skillCount: String(toInstall.length),
3746
+ successCount: String(successfulSkillNames.size),
3747
+ agents: targetAgents.join(",")
3748
+ });
3749
+ console.log();
3750
+ Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
3751
+ }
3752
+ function parseSyncOptions(args) {
3753
+ const options = {};
3754
+ for (let i = 0; i < args.length; i++) {
3755
+ const arg = args[i];
3756
+ if (arg === "-y" || arg === "--yes") options.yes = true;
3757
+ else if (arg === "-f" || arg === "--force") options.force = true;
3758
+ else if (arg === "-a" || arg === "--agent") {
3759
+ options.agent = options.agent || [];
3760
+ i++;
3761
+ let nextArg = args[i];
3762
+ while (i < args.length && nextArg && !nextArg.startsWith("-")) {
3763
+ options.agent.push(nextArg);
3764
+ i++;
3765
+ nextArg = args[i];
3766
+ }
3767
+ i--;
3768
+ }
3769
+ }
3770
+ return { options };
3771
+ }
3772
+ async function runInstallFromLock(args) {
3773
+ const lock = await readLocalLock(process.cwd());
3774
+ const skillEntries = Object.entries(lock.skills);
3775
+ if (skillEntries.length === 0) {
3776
+ M.warn("No project skills found in skills-lock.json");
3777
+ M.info(`Add project-level skills with ${import_picocolors.default.cyan("npx skills add <package>")} (without ${import_picocolors.default.cyan("-g")})`);
3778
+ return;
3779
+ }
3780
+ const universalAgentNames = getUniversalAgents();
3781
+ const nodeModuleSkills = [];
3782
+ const bySource = /* @__PURE__ */ new Map();
3783
+ for (const [skillName, entry] of skillEntries) {
3784
+ if (entry.sourceType === "node_modules") {
3785
+ nodeModuleSkills.push(skillName);
3786
+ continue;
3787
+ }
3788
+ const existing = bySource.get(entry.source);
3789
+ if (existing) existing.skills.push(skillName);
3790
+ else bySource.set(entry.source, {
3791
+ sourceType: entry.sourceType,
3792
+ skills: [skillName]
3793
+ });
3794
+ }
3795
+ const remoteCount = skillEntries.length - nodeModuleSkills.length;
3796
+ if (remoteCount > 0) M.info(`Restoring ${import_picocolors.default.cyan(String(remoteCount))} skill${remoteCount !== 1 ? "s" : ""} from skills-lock.json into ${import_picocolors.default.dim(".agents/skills/")}`);
3797
+ for (const [source, { skills }] of bySource) try {
3798
+ await runAdd([source], {
3799
+ skill: skills,
3800
+ agent: universalAgentNames,
3801
+ yes: true
3802
+ });
3803
+ } catch (error) {
3804
+ M.error(`Failed to install from ${import_picocolors.default.cyan(source)}: ${error instanceof Error ? error.message : "Unknown error"}`);
3805
+ }
3806
+ if (nodeModuleSkills.length > 0) {
3807
+ M.info(`${import_picocolors.default.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? "s" : ""} from node_modules`);
3808
+ try {
3809
+ const { options: syncOptions } = parseSyncOptions(args);
3810
+ await runSync(args, {
3811
+ ...syncOptions,
3812
+ yes: true,
3813
+ agent: universalAgentNames
3814
+ });
3815
+ } catch (error) {
3816
+ M.error(`Failed to sync node_modules skills: ${error instanceof Error ? error.message : "Unknown error"}`);
3817
+ }
3818
+ }
3819
+ }
3345
3820
  const RESET$1 = "\x1B[0m";
3346
3821
  const BOLD$1 = "\x1B[1m";
3347
3822
  const DIM$1 = "\x1B[38;5;102m";
@@ -3472,10 +3947,8 @@ async function removeCommand(skillNames, options) {
3472
3947
  let targetAgents;
3473
3948
  if (options.agent && options.agent.length > 0) targetAgents = options.agent;
3474
3949
  else {
3475
- spinner.start("Detecting installed agents...");
3476
- targetAgents = await detectInstalledAgents();
3477
- if (targetAgents.length === 0) targetAgents = Object.keys(agents);
3478
- spinner.stop(`Targeting ${targetAgents.length} installed agent(s)`);
3950
+ targetAgents = Object.keys(agents);
3951
+ spinner.stop(`Targeting ${targetAgents.length} potential agent(s)`);
3479
3952
  }
3480
3953
  if (!options.yes) {
3481
3954
  console.log();
@@ -3491,25 +3964,42 @@ async function removeCommand(skillNames, options) {
3491
3964
  spinner.start("Removing skills...");
3492
3965
  const results = [];
3493
3966
  for (const skillName of selectedSkills) try {
3967
+ const canonicalPath = getCanonicalPath(skillName, {
3968
+ global: isGlobal,
3969
+ cwd
3970
+ });
3494
3971
  for (const agentKey of targetAgents) {
3495
3972
  const agent = agents[agentKey];
3496
3973
  const skillPath = getInstallPath(skillName, agentKey, {
3497
3974
  global: isGlobal,
3498
3975
  cwd
3499
3976
  });
3500
- try {
3501
- if (await lstat(skillPath).catch(() => null)) await rm(skillPath, {
3502
- recursive: true,
3503
- force: true
3504
- });
3505
- } catch (err) {
3506
- M.warn(`Could not remove skill from ${agent.displayName}: ${err instanceof Error ? err.message : String(err)}`);
3977
+ const pathsToCleanup = new Set([skillPath]);
3978
+ const sanitizedName = sanitizeName(skillName);
3979
+ if (isGlobal && agent.globalSkillsDir) pathsToCleanup.add(join(agent.globalSkillsDir, sanitizedName));
3980
+ else pathsToCleanup.add(join(cwd, agent.skillsDir, sanitizedName));
3981
+ for (const pathToCleanup of pathsToCleanup) {
3982
+ if (pathToCleanup === canonicalPath) continue;
3983
+ try {
3984
+ if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
3985
+ recursive: true,
3986
+ force: true
3987
+ });
3988
+ } catch (err) {
3989
+ M.warn(`Could not remove skill from ${agent.displayName}: ${err instanceof Error ? err.message : String(err)}`);
3990
+ }
3507
3991
  }
3508
3992
  }
3509
- await rm(getCanonicalPath(skillName, {
3993
+ const remainingAgents = (await detectInstalledAgents()).filter((a) => !targetAgents.includes(a));
3994
+ let isStillUsed = false;
3995
+ for (const agentKey of remainingAgents) if (await lstat(getInstallPath(skillName, agentKey, {
3510
3996
  global: isGlobal,
3511
3997
  cwd
3512
- }), {
3998
+ })).catch(() => null)) {
3999
+ isStillUsed = true;
4000
+ break;
4001
+ }
4002
+ if (!isStillUsed) await rm(canonicalPath, {
3513
4003
  recursive: true,
3514
4004
  force: true
3515
4005
  });
@@ -3626,13 +4116,17 @@ function showBanner() {
3626
4116
  console.log();
3627
4117
  console.log(`${DIM}The open agent skills ecosystem${RESET}`);
3628
4118
  console.log();
3629
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills add ${DIM}<package>${RESET} ${DIM}Install a skill${RESET}`);
3630
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills list${RESET} ${DIM}List installed skills${RESET}`);
3631
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`);
3632
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills check${RESET} ${DIM}Check for updates${RESET}`);
3633
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills update${RESET} ${DIM}Update all skills${RESET}`);
3634
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills remove${RESET} ${DIM}Remove installed skills${RESET}`);
3635
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
4119
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills add ${DIM}<package>${RESET} ${DIM}Add a new skill${RESET}`);
4120
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills remove${RESET} ${DIM}Remove installed skills${RESET}`);
4121
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills list${RESET} ${DIM}List installed skills${RESET}`);
4122
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`);
4123
+ console.log();
4124
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills check${RESET} ${DIM}Check for updates${RESET}`);
4125
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills update${RESET} ${DIM}Update all skills${RESET}`);
4126
+ console.log();
4127
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
4128
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
4129
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
3636
4130
  console.log();
3637
4131
  console.log(`${DIM}try:${RESET} npx skills add vercel-labs/agent-skills`);
3638
4132
  console.log();
@@ -3643,16 +4137,22 @@ function showHelp() {
3643
4137
  console.log(`
3644
4138
  ${BOLD}Usage:${RESET} skills <command> [options]
3645
4139
 
3646
- ${BOLD}Commands:${RESET}
3647
- add <package> Add a skill package
3648
- e.g. vercel-labs/agent-skills
3649
- https://github.com/vercel-labs/agent-skills
3650
- remove [skills] Remove installed skills
3651
- list, ls List installed skills
3652
- find [query] Search for skills interactively
3653
- init [name] Initialize a skill (creates <name>/SKILL.md or ./SKILL.md)
3654
- check Check for available skill updates
3655
- update Update all skills to latest versions
4140
+ ${BOLD}Manage Skills:${RESET}
4141
+ add <package> Add a skill package (alias: a)
4142
+ e.g. vercel-labs/agent-skills
4143
+ https://github.com/vercel-labs/agent-skills
4144
+ remove [skills] Remove installed skills
4145
+ list, ls List installed skills
4146
+ find [query] Search for skills interactively
4147
+
4148
+ ${BOLD}Updates:${RESET}
4149
+ check Check for available skill updates
4150
+ update Update all skills to latest versions
4151
+
4152
+ ${BOLD}Project:${RESET}
4153
+ experimental_install Restore skills from skills-lock.json
4154
+ init [name] Initialize a skill (creates <name>/SKILL.md or ./SKILL.md)
4155
+ experimental_sync Sync skills from node_modules into agent directories
3656
4156
 
3657
4157
  ${BOLD}Add Options:${RESET}
3658
4158
  -g, --global Install skill globally (user-level) instead of project-level
@@ -3660,6 +4160,7 @@ ${BOLD}Add Options:${RESET}
3660
4160
  -s, --skill <skills> Specify skill names to install (use '*' for all skills)
3661
4161
  -l, --list List available skills in the repository without installing
3662
4162
  -y, --yes Skip confirmation prompts
4163
+ --copy Copy files instead of symlinking to agent directories
3663
4164
  --all Shorthand for --skill '*' --agent '*' -y
3664
4165
  --full-depth Search all subdirectories even when a root SKILL.md exists
3665
4166
 
@@ -3670,6 +4171,10 @@ ${BOLD}Remove Options:${RESET}
3670
4171
  -y, --yes Skip confirmation prompts
3671
4172
  --all Shorthand for --skill '*' --agent '*' -y
3672
4173
 
4174
+ ${BOLD}Experimental Sync Options:${RESET}
4175
+ -a, --agent <agents> Specify agents to install to (use '*' for all agents)
4176
+ -y, --yes Skip confirmation prompts
4177
+
3673
4178
  ${BOLD}List Options:${RESET}
3674
4179
  -g, --global List global skills (default: project)
3675
4180
  -a, --agent <agents> Filter by specific agents
@@ -3683,17 +4188,20 @@ ${BOLD}Examples:${RESET}
3683
4188
  ${DIM}$${RESET} skills add vercel-labs/agent-skills -g
3684
4189
  ${DIM}$${RESET} skills add vercel-labs/agent-skills --agent claude-code cursor
3685
4190
  ${DIM}$${RESET} skills add vercel-labs/agent-skills --skill pr-review commit
3686
- ${DIM}$${RESET} skills remove ${DIM}# interactive remove${RESET}
3687
- ${DIM}$${RESET} skills remove web-design ${DIM}# remove by name${RESET}
4191
+ ${DIM}$${RESET} skills remove ${DIM}# interactive remove${RESET}
4192
+ ${DIM}$${RESET} skills remove web-design ${DIM}# remove by name${RESET}
3688
4193
  ${DIM}$${RESET} skills rm --global frontend-design
3689
- ${DIM}$${RESET} skills list ${DIM}# list all installed skills${RESET}
3690
- ${DIM}$${RESET} skills ls -g ${DIM}# list global skills only${RESET}
3691
- ${DIM}$${RESET} skills ls -a claude-code ${DIM}# filter by agent${RESET}
3692
- ${DIM}$${RESET} skills find ${DIM}# interactive search${RESET}
3693
- ${DIM}$${RESET} skills find typescript ${DIM}# search by keyword${RESET}
3694
- ${DIM}$${RESET} skills init my-skill
4194
+ ${DIM}$${RESET} skills list ${DIM}# list project skills${RESET}
4195
+ ${DIM}$${RESET} skills ls -g ${DIM}# list global skills${RESET}
4196
+ ${DIM}$${RESET} skills ls -a claude-code ${DIM}# filter by agent${RESET}
4197
+ ${DIM}$${RESET} skills find ${DIM}# interactive search${RESET}
4198
+ ${DIM}$${RESET} skills find typescript ${DIM}# search by keyword${RESET}
3695
4199
  ${DIM}$${RESET} skills check
3696
4200
  ${DIM}$${RESET} skills update
4201
+ ${DIM}$${RESET} skills experimental_install ${DIM}# restore from skills-lock.json${RESET}
4202
+ ${DIM}$${RESET} skills init my-skill
4203
+ ${DIM}$${RESET} skills experimental_sync ${DIM}# sync from node_modules${RESET}
4204
+ ${DIM}$${RESET} skills experimental_sync -y ${DIM}# sync without prompts${RESET}
3697
4205
 
3698
4206
  Discover more skills at ${TEXT}https://skills.sh/${RESET}
3699
4207
  `);
@@ -3984,13 +4492,17 @@ async function main() {
3984
4492
  console.log();
3985
4493
  runInit(restArgs);
3986
4494
  break;
4495
+ case "experimental_install":
4496
+ showLogo();
4497
+ await runInstallFromLock(restArgs);
4498
+ break;
3987
4499
  case "i":
3988
4500
  case "install":
3989
4501
  case "a":
3990
4502
  case "add": {
3991
4503
  showLogo();
3992
- const { source, options } = parseAddOptions(restArgs);
3993
- await runAdd(source, options);
4504
+ const { source: addSource, options: addOpts } = parseAddOptions(restArgs);
4505
+ await runAdd(addSource, addOpts);
3994
4506
  break;
3995
4507
  }
3996
4508
  case "remove":
@@ -4003,6 +4515,12 @@ async function main() {
4003
4515
  const { skills, options: removeOptions } = parseRemoveOptions(restArgs);
4004
4516
  await removeCommand(skills, removeOptions);
4005
4517
  break;
4518
+ case "experimental_sync": {
4519
+ showLogo();
4520
+ const { options: syncOptions } = parseSyncOptions(restArgs);
4521
+ await runSync(restArgs, syncOptions);
4522
+ break;
4523
+ }
4006
4524
  case "list":
4007
4525
  case "ls":
4008
4526
  await runList(restArgs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "The 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",