skills 1.7.1 → 1.7.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.
package/dist/cli.mjs CHANGED
@@ -7,7 +7,7 @@ import { esm_default } from "./_chunks/libs/simple-git.mjs";
7
7
  import { xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
8
8
  import { require_dist } from "./_chunks/libs/@vercel/detect-agent.mjs";
9
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
10
- import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
10
+ import { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep } from "path";
11
11
  import { fileURLToPath } from "url";
12
12
  import { stripVTControlCharacters } from "node:util";
13
13
  import { createWriteStream } from "node:fs";
@@ -17,7 +17,7 @@ import * as readline from "readline";
17
17
  import { Writable } from "stream";
18
18
  import { promisify } from "util";
19
19
  import { execFile, spawn, spawnSync } from "child_process";
20
- import { access, chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
20
+ import { access, chmod, cp, glob, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
21
21
  import { parse, stringify } from "yaml";
22
22
  import { createHash } from "crypto";
23
23
  import { cp as cp$1, mkdir as mkdir$1, mkdtemp as mkdtemp$1, readFile as readFile$1, rm as rm$1, stat as stat$1, writeFile as writeFile$1 } from "node:fs/promises";
@@ -1015,7 +1015,7 @@ async function cleanupTempDir(dir) {
1015
1015
  });
1016
1016
  }
1017
1017
  function parseFrontmatter(raw) {
1018
- const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
1018
+ const match = raw.match(/^\uFEFF?---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
1019
1019
  if (!match) return {
1020
1020
  data: {},
1021
1021
  content: raw
@@ -1112,14 +1112,13 @@ async function readLocalLock(cwd) {
1112
1112
  async function writeLocalLock(lock, cwd) {
1113
1113
  const lockDir = cwd || process.cwd();
1114
1114
  const lockPath = getLocalLockPath(lockDir);
1115
- const sortedSkills = {};
1116
- for (const key of Object.keys(lock.skills).sort()) {
1115
+ const sortedSkills = Object.fromEntries(Object.keys(lock.skills).sort().map((key) => {
1117
1116
  const entry = lock.skills[key];
1118
- sortedSkills[key] = entry.sourceType === "local" ? {
1117
+ return [key, entry.sourceType === "local" ? {
1119
1118
  ...entry,
1120
1119
  source: getPortableLocalSource(entry.source, lockDir)
1121
- } : entry;
1122
- }
1120
+ } : entry];
1121
+ }));
1123
1122
  const sorted = {
1124
1123
  version: lock.version,
1125
1124
  skills: sortedSkills
@@ -1165,12 +1164,15 @@ async function collectFiles(baseDir, currentDir, results) {
1165
1164
  }
1166
1165
  async function addSkillToLocalLock(skillName, entry, cwd) {
1167
1166
  const lock = await readLocalLock(cwd);
1168
- lock.skills[skillName] = entry;
1167
+ lock.skills = {
1168
+ ...lock.skills,
1169
+ [skillName]: entry
1170
+ };
1169
1171
  await writeLocalLock(lock, cwd);
1170
1172
  }
1171
1173
  async function removeSkillFromLocalLock(skillName, cwd) {
1172
1174
  const lock = await readLocalLock(cwd);
1173
- if (!(skillName in lock.skills)) return false;
1175
+ if (!Object.hasOwn(lock.skills, skillName)) return false;
1174
1176
  delete lock.skills[skillName];
1175
1177
  await writeLocalLock(lock, cwd);
1176
1178
  return true;
@@ -1572,7 +1574,7 @@ const agents = {
1572
1574
  name: "codex",
1573
1575
  displayName: "Codex",
1574
1576
  skillsDir: ".agents/skills",
1575
- globalSkillsDir: join(codexHome, "skills"),
1577
+ globalSkillsDir: join(home, ".agents", "skills"),
1576
1578
  detectInstalled: async () => {
1577
1579
  return existsSync(codexHome) || existsSync("/etc/codex");
1578
1580
  }
@@ -1692,8 +1694,8 @@ const agents = {
1692
1694
  fx: {
1693
1695
  name: "fx",
1694
1696
  displayName: "fx",
1695
- skillsDir: ".fx/skills",
1696
- globalSkillsDir: join(home, ".fx/skills"),
1697
+ skillsDir: ".agents/skills",
1698
+ globalSkillsDir: join(home, ".agents/skills"),
1697
1699
  detectInstalled: async () => {
1698
1700
  return existsSync(join(home, ".fx"));
1699
1701
  }
@@ -2316,26 +2318,34 @@ async function installSkillForAgent(skill, agentType, options = {}) {
2316
2318
  mode: "copy"
2317
2319
  };
2318
2320
  }
2319
- if (pathsOverlap(skill.path, canonicalDir)) return {
2321
+ const sourceIsCanonical = resolve(skill.path) === resolve(canonicalDir);
2322
+ if (!sourceIsCanonical && pathsOverlap(skill.path, canonicalDir)) return {
2320
2323
  success: true,
2321
2324
  path: canonicalDir,
2322
2325
  canonicalPath: canonicalDir,
2323
- mode: "symlink",
2326
+ mode: installMode,
2324
2327
  skipped: true
2325
2328
  };
2326
- await cleanAndCreateDirectory(canonicalDir);
2327
- await copyDirectory(skill.path, canonicalDir, agentType);
2329
+ if (!sourceIsCanonical) if (installMode === "link") {
2330
+ if (!await createSymlink(skill.path, canonicalDir)) {
2331
+ await cleanAndCreateDirectory(canonicalDir);
2332
+ await copyDirectory(skill.path, canonicalDir, agentType);
2333
+ }
2334
+ } else {
2335
+ await cleanAndCreateDirectory(canonicalDir);
2336
+ await copyDirectory(skill.path, canonicalDir, agentType);
2337
+ }
2328
2338
  if (isGlobal && isUniversalAgent(agentType)) return {
2329
2339
  success: true,
2330
2340
  path: canonicalDir,
2331
2341
  canonicalPath: canonicalDir,
2332
- mode: "symlink"
2342
+ mode: installMode
2333
2343
  };
2334
2344
  if (shouldSkipProjectAgentSymlink(agentType, isGlobal, cwd, options.createMissingAgentRoot ?? false)) return {
2335
2345
  success: true,
2336
2346
  path: canonicalDir,
2337
2347
  canonicalPath: canonicalDir,
2338
- mode: "symlink",
2348
+ mode: installMode,
2339
2349
  skipped: true,
2340
2350
  skipReason: "missing-agent-project-directory"
2341
2351
  };
@@ -2346,7 +2356,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
2346
2356
  success: true,
2347
2357
  path: agentDir,
2348
2358
  canonicalPath: canonicalDir,
2349
- mode: "symlink",
2359
+ mode: installMode,
2350
2360
  symlinkFailed: true
2351
2361
  };
2352
2362
  }
@@ -2354,7 +2364,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
2354
2364
  success: true,
2355
2365
  path: agentDir,
2356
2366
  canonicalPath: canonicalDir,
2357
- mode: "symlink"
2367
+ mode: installMode
2358
2368
  };
2359
2369
  } catch (error) {
2360
2370
  return {
@@ -4172,7 +4182,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
4172
4182
  tree
4173
4183
  };
4174
4184
  }
4175
- var version$1 = "1.7.1";
4185
+ var version$1 = "1.7.3";
4176
4186
  const NOTION_API_VERSION = "2026-03-11";
4177
4187
  const NTN_TIMEOUT_MS = 3e4;
4178
4188
  const NTN_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
@@ -4376,18 +4386,18 @@ async function fetchNotionSkillDirectory(pageId, options = {}) {
4376
4386
  }
4377
4387
  async function prepareNotionSkillSource(pageId, options = {}) {
4378
4388
  const download = options.download ?? downloadNotionDirectory;
4379
- const spinner$4 = spinner();
4380
- spinner$4.start("Fetching Notion skill with ntn…");
4389
+ const spinner$5 = spinner();
4390
+ spinner$5.start("Fetching Notion skill with ntn…");
4381
4391
  try {
4382
4392
  const [directory, workspace] = await Promise.all([fetchNotionSkillDirectory(pageId, { runNtn: options.runNtn }), fetchNotionWorkspaceName({ runNtn: options.runNtn })]);
4383
4393
  const downloaded = await download(directory.url);
4384
- spinner$4.stop(`Downloaded skill from Notion${inWorkspace(workspace)}`);
4394
+ spinner$5.stop(`Downloaded skill from Notion${inWorkspace(workspace)}`);
4385
4395
  return {
4386
4396
  rootDir: downloaded.rootDir,
4387
4397
  tempDir: downloaded.tempDir
4388
4398
  };
4389
4399
  } catch (error) {
4390
- spinner$4.stop(import_picocolors.default.red("Failed to prepare Notion skill"));
4400
+ spinner$5.stop(import_picocolors.default.red("Failed to prepare Notion skill"));
4391
4401
  throw error;
4392
4402
  }
4393
4403
  }
@@ -4411,17 +4421,17 @@ function printAvailablePacks(packs) {
4411
4421
  }
4412
4422
  }
4413
4423
  async function prepareNotionPackSource(options = {}) {
4414
- const spinner$5 = spinner();
4415
- spinner$5.start("Fetching Notion packs with ntn…");
4424
+ const spinner$6 = spinner();
4425
+ spinner$6.start("Fetching Notion packs with ntn…");
4416
4426
  let packs;
4417
4427
  let workspace = null;
4418
4428
  try {
4419
4429
  [packs, workspace] = await Promise.all([fetchNotionPacks({ runNtn: options.runNtn }), fetchNotionWorkspaceName({ runNtn: options.runNtn })]);
4420
4430
  } catch (error) {
4421
- spinner$5.stop(import_picocolors.default.red("Failed to load Notion packs"));
4431
+ spinner$6.stop(import_picocolors.default.red("Failed to load Notion packs"));
4422
4432
  throw error;
4423
4433
  }
4424
- spinner$5.stop(`Found ${import_picocolors.default.green(packs.length)} Notion pack${packs.length === 1 ? "" : "s"}${inWorkspace(workspace)}`);
4434
+ spinner$6.stop(`Found ${import_picocolors.default.green(packs.length)} Notion pack${packs.length === 1 ? "" : "s"}${inWorkspace(workspace)}`);
4425
4435
  if (packs.length === 0) throw new Error("Notion returned no packs for the authenticated workspace");
4426
4436
  if (options.list) {
4427
4437
  printAvailablePacks(packs);
@@ -4460,7 +4470,7 @@ async function prepareNotionPackSource(options = {}) {
4460
4470
  const download = options.download ?? downloadNotionDirectory;
4461
4471
  const discover = options.discover ?? discoverSkills;
4462
4472
  let skillCount = 0;
4463
- spinner$5.start("Preparing selected Notion packs…");
4473
+ spinner$6.start("Preparing selected Notion packs…");
4464
4474
  try {
4465
4475
  await mkdir$1(packsDir, { recursive: true });
4466
4476
  for (const [index, pack] of selectedPacks.entries()) {
@@ -4501,7 +4511,7 @@ async function prepareNotionPackSource(options = {}) {
4501
4511
  metadata: { pluginRoot: "./packs" },
4502
4512
  plugins: manifestPlugins
4503
4513
  }, null, 2)}\n`, "utf8");
4504
- spinner$5.stop(`Prepared ${import_picocolors.default.green(selectedPacks.length)} Notion pack${selectedPacks.length === 1 ? "" : "s"} with ${import_picocolors.default.green(skillCount)} skill${skillCount === 1 ? "" : "s"}`);
4514
+ spinner$6.stop(`Prepared ${import_picocolors.default.green(selectedPacks.length)} Notion pack${selectedPacks.length === 1 ? "" : "s"} with ${import_picocolors.default.green(skillCount)} skill${skillCount === 1 ? "" : "s"}`);
4505
4515
  return {
4506
4516
  rootDir: stagingDir,
4507
4517
  tempDir: stagingDir,
@@ -4509,7 +4519,7 @@ async function prepareNotionPackSource(options = {}) {
4509
4519
  skillCount
4510
4520
  };
4511
4521
  } catch (error) {
4512
- spinner$5.stop(import_picocolors.default.red("Failed to prepare Notion packs"));
4522
+ spinner$6.stop(import_picocolors.default.red("Failed to prepare Notion packs"));
4513
4523
  await cleanupTempDir(stagingDir).catch(() => {});
4514
4524
  throw error;
4515
4525
  }
@@ -4685,7 +4695,8 @@ async function promptForAgents(message, choices) {
4685
4695
  const defaultValues = [
4686
4696
  "claude-code",
4687
4697
  "opencode",
4688
- "codex"
4698
+ "codex",
4699
+ "grok"
4689
4700
  ].filter((a) => validAgents.includes(a));
4690
4701
  let initialValues = [];
4691
4702
  if (lastSelected && lastSelected.length > 0) initialValues = lastSelected.filter((a) => validAgents.includes(a));
@@ -5048,6 +5059,182 @@ async function handleWellKnownSkills(source, url, options, spinner) {
5048
5059
  await promptForFindSkills(options, targetAgents);
5049
5060
  return true;
5050
5061
  }
5062
+ async function resolveSkills(parsed, options, spinner) {
5063
+ const { includeInternal } = options;
5064
+ let skills;
5065
+ let blobResult = null;
5066
+ let tempDir = null;
5067
+ try {
5068
+ if (parsed.type === "local") {
5069
+ spinner.start("Discovering skills…");
5070
+ skills = await discoverSkills(parsed.localPath, parsed.subpath, {
5071
+ includeInternal,
5072
+ fullDepth: options.fullDepth
5073
+ });
5074
+ } else if (parsed.type === "well-known" || parsed.type === "download") {
5075
+ spinner.start("Downloading source...");
5076
+ const downloaded = await downloadSource(parsed.url);
5077
+ tempDir = downloaded.tempDir;
5078
+ spinner.stop(`Downloaded ${downloaded.kind === "skill-md" ? "SKILL.md file" : "archive"}`);
5079
+ spinner.start("Discovering skills...");
5080
+ skills = await discoverSkills(downloaded.rootDir, parsed.subpath, {
5081
+ includeInternal,
5082
+ fullDepth: options.fullDepth
5083
+ });
5084
+ } else if (parsed.type === "github" && !options.fullDepth) {
5085
+ let attemptedBlobInstall = false;
5086
+ const BLOB_ALLOWED_OWNERS = [
5087
+ "vercel",
5088
+ "vercel-labs",
5089
+ "heygen-com",
5090
+ "remotion-dev"
5091
+ ];
5092
+ const ownerRepo = getOwnerRepo(parsed);
5093
+ const owner = ownerRepo?.split("/")[0]?.toLowerCase();
5094
+ const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
5095
+ if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
5096
+ attemptedBlobInstall = true;
5097
+ spinner.start("Fetching skills…");
5098
+ blobResult = await tryBlobInstall(ownerRepo, {
5099
+ subpath: parsed.subpath,
5100
+ skillFilter: parsed.skillFilter,
5101
+ ref: parsed.ref,
5102
+ getToken: getGitHubToken,
5103
+ includeInternal
5104
+ });
5105
+ }
5106
+ if (blobResult) {
5107
+ skills = blobResult.skills;
5108
+ spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
5109
+ } else {
5110
+ if (attemptedBlobInstall) spinner.message("Cloning repository…");
5111
+ else spinner.start("Cloning repository…");
5112
+ tempDir = await cloneRepo(parsed.url, parsed.ref);
5113
+ spinner.stop("Repository cloned");
5114
+ spinner.start("Discovering skills…");
5115
+ skills = await discoverSkills(tempDir, parsed.subpath, {
5116
+ includeInternal,
5117
+ fullDepth: options.fullDepth
5118
+ });
5119
+ }
5120
+ } else {
5121
+ spinner.start("Cloning repository…");
5122
+ tempDir = await cloneRepo(parsed.url, parsed.ref);
5123
+ spinner.stop("Repository cloned");
5124
+ spinner.start("Discovering skills…");
5125
+ skills = await discoverSkills(tempDir, parsed.subpath, {
5126
+ includeInternal,
5127
+ fullDepth: options.fullDepth
5128
+ });
5129
+ }
5130
+ } catch (error) {
5131
+ await cleanup(tempDir);
5132
+ throw error;
5133
+ }
5134
+ return {
5135
+ skills,
5136
+ tempDir,
5137
+ blobResult
5138
+ };
5139
+ }
5140
+ async function installToTargets(resolved, skills, targets, options) {
5141
+ const results = [];
5142
+ for (const skill of skills) for (const target of targets) {
5143
+ const { agent, subagent } = target;
5144
+ const installOptions = {
5145
+ global: options.global,
5146
+ mode: options.mode,
5147
+ eveSubagent: subagent,
5148
+ createMissingAgentRoot: options.createMissingAgentRoot(agent)
5149
+ };
5150
+ let result;
5151
+ if (resolved.blobResult && "files" in skill) {
5152
+ const blobSkill = skill;
5153
+ result = await installBlobSkillForAgent({
5154
+ installName: blobSkill.name,
5155
+ files: blobSkill.files
5156
+ }, agent, installOptions);
5157
+ } else result = await installSkillForAgent(skill, agent, installOptions);
5158
+ results.push({
5159
+ skill: getSkillDisplayName(skill),
5160
+ agent: targetDisplayName(target),
5161
+ pluginName: skill.pluginName,
5162
+ ...result
5163
+ });
5164
+ }
5165
+ return results;
5166
+ }
5167
+ function getSkillRepoPaths(resolved, skills) {
5168
+ const { tempDir, blobResult } = resolved;
5169
+ const skillFiles = {};
5170
+ for (const skill of skills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
5171
+ else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
5172
+ else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
5173
+ return skillFiles;
5174
+ }
5175
+ function getProjectLockSource(parsed) {
5176
+ return getLockSource(parsed.url, getOwnerRepo(parsed)) || parsed.url;
5177
+ }
5178
+ function projectLockEntry(parsed, skillPath, computedHash) {
5179
+ const sourceUrl = getProjectLockSourceUrl(parsed.type, parsed.url);
5180
+ return {
5181
+ source: getProjectLockSource(parsed),
5182
+ ...sourceUrl && { sourceUrl },
5183
+ ref: parsed.ref,
5184
+ sourceType: parsed.type,
5185
+ ...skillPath && { skillPath },
5186
+ computedHash
5187
+ };
5188
+ }
5189
+ async function installFromSource(parsed, options) {
5190
+ const spinner$4 = spinner();
5191
+ let resolved = null;
5192
+ try {
5193
+ resolved = await resolveSkills(parsed, { includeInternal: options.skills.length > 0 }, spinner$4);
5194
+ let selected = options.skills.length > 0 ? filterSkills(resolved.skills, options.skills) : resolved.skills;
5195
+ if (selected.length === 0) {
5196
+ spinner$4.stop(import_picocolors.default.red("No matching skills found"));
5197
+ return {
5198
+ installed: [],
5199
+ failed: [],
5200
+ error: "No matching skills found"
5201
+ };
5202
+ }
5203
+ spinner$4.stop(`Found ${import_picocolors.default.green(selected.length)} skill${selected.length > 1 ? "s" : ""}`);
5204
+ if (options.select) selected = await options.select(selected);
5205
+ const results = await installToTargets(resolved, selected, options.agents.map((agent) => ({ agent })), {
5206
+ global: false,
5207
+ mode: "symlink",
5208
+ createMissingAgentRoot: () => true
5209
+ });
5210
+ const installed = new Set(results.filter((r) => r.success).map((r) => r.skill));
5211
+ const repoPaths = getSkillRepoPaths(resolved, selected);
5212
+ for (const skill of selected) {
5213
+ if (!installed.has(getSkillDisplayName(skill))) continue;
5214
+ const entry = projectLockEntry(parsed, repoPaths[skill.name], await sourceSkillHash(skill));
5215
+ await addSkillToLocalLock(skill.name, {
5216
+ ...entry,
5217
+ ...options.via && { via: options.via }
5218
+ });
5219
+ }
5220
+ return {
5221
+ installed: [...installed],
5222
+ failed: results.filter((r) => !r.success).map((r) => `${r.skill} → ${r.agent}: ${r.error}`)
5223
+ };
5224
+ } catch (error) {
5225
+ spinner$4.stop(import_picocolors.default.red("Installation failed"));
5226
+ return {
5227
+ installed: [],
5228
+ failed: [],
5229
+ error: error instanceof Error ? error.message : String(error)
5230
+ };
5231
+ } finally {
5232
+ await cleanup(resolved?.tempDir ?? null);
5233
+ }
5234
+ }
5235
+ function sourceSkillHash(skill) {
5236
+ return "snapshotHash" in skill ? Promise.resolve(skill.snapshotHash) : computeSkillFolderHash(skill.path);
5237
+ }
5051
5238
  async function runAdd(args, options = {}) {
5052
5239
  const source = args[0];
5053
5240
  let installTipShown = false;
@@ -5162,8 +5349,6 @@ async function runAdd(args, options = {}) {
5162
5349
  if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
5163
5350
  }
5164
5351
  const includeInternal = !!(options.skill && options.skill.length > 0 && !options.skill.includes("*"));
5165
- let skills;
5166
- let blobResult = null;
5167
5352
  if (parsed.type === "local") {
5168
5353
  spinner$3.start("Validating local path…");
5169
5354
  if (!existsSync(parsed.localPath)) {
@@ -5172,67 +5357,13 @@ async function runAdd(args, options = {}) {
5172
5357
  emitJsonAndExit(1, `Local path does not exist: ${parsed.localPath}`);
5173
5358
  }
5174
5359
  spinner$3.stop("Local path validated");
5175
- spinner$3.start("Discovering skills…");
5176
- skills = await discoverSkills(parsed.localPath, parsed.subpath, {
5177
- includeInternal,
5178
- fullDepth: options.fullDepth
5179
- });
5180
- } else if (parsed.type === "well-known" || parsed.type === "download") {
5181
- spinner$3.start("Downloading source...");
5182
- const downloaded = await downloadSource(parsed.url);
5183
- tempDir = downloaded.tempDir;
5184
- spinner$3.stop(`Downloaded ${downloaded.kind === "skill-md" ? "SKILL.md file" : "archive"}`);
5185
- spinner$3.start("Discovering skills...");
5186
- skills = await discoverSkills(downloaded.rootDir, parsed.subpath, {
5187
- includeInternal,
5188
- fullDepth: options.fullDepth
5189
- });
5190
- } else if (parsed.type === "github" && !options.fullDepth) {
5191
- let attemptedBlobInstall = false;
5192
- const BLOB_ALLOWED_OWNERS = [
5193
- "vercel",
5194
- "vercel-labs",
5195
- "heygen-com",
5196
- "remotion-dev"
5197
- ];
5198
- const ownerRepo = getOwnerRepo(parsed);
5199
- const owner = ownerRepo?.split("/")[0]?.toLowerCase();
5200
- const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
5201
- if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
5202
- attemptedBlobInstall = true;
5203
- spinner$3.start("Fetching skills…");
5204
- blobResult = await tryBlobInstall(ownerRepo, {
5205
- subpath: parsed.subpath,
5206
- skillFilter: parsed.skillFilter,
5207
- ref: parsed.ref,
5208
- getToken: getGitHubToken,
5209
- includeInternal
5210
- });
5211
- }
5212
- if (blobResult) {
5213
- skills = blobResult.skills;
5214
- spinner$3.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
5215
- } else {
5216
- if (attemptedBlobInstall) spinner$3.message("Cloning repository…");
5217
- else spinner$3.start("Cloning repository…");
5218
- tempDir = await cloneRepo(parsed.url, parsed.ref);
5219
- spinner$3.stop("Repository cloned");
5220
- spinner$3.start("Discovering skills…");
5221
- skills = await discoverSkills(tempDir, parsed.subpath, {
5222
- includeInternal,
5223
- fullDepth: options.fullDepth
5224
- });
5225
- }
5226
- } else {
5227
- spinner$3.start("Cloning repository…");
5228
- tempDir = await cloneRepo(parsed.url, parsed.ref);
5229
- spinner$3.stop("Repository cloned");
5230
- spinner$3.start("Discovering skills…");
5231
- skills = await discoverSkills(tempDir, parsed.subpath, {
5232
- includeInternal,
5233
- fullDepth: options.fullDepth
5234
- });
5235
5360
  }
5361
+ const resolved = await resolveSkills(parsed, {
5362
+ includeInternal,
5363
+ fullDepth: options.fullDepth
5364
+ }, spinner$3);
5365
+ const { skills, blobResult } = resolved;
5366
+ tempDir = resolved.tempDir ?? tempDir;
5236
5367
  if (skills.length === 0) {
5237
5368
  spinner$3.stop(import_picocolors.default.red("No skills found"));
5238
5369
  outro(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
@@ -5554,47 +5685,19 @@ async function runAdd(args, options = {}) {
5554
5685
  }
5555
5686
  }
5556
5687
  spinner$3.start("Installing skills…");
5557
- const results = [];
5558
- for (const skill of selectedSkills) for (const target of installTargets) {
5559
- const { agent, subagent } = target;
5560
- let result;
5561
- if (blobResult && "files" in skill) {
5562
- const blobSkill = skill;
5563
- result = await installBlobSkillForAgent({
5564
- installName: blobSkill.name,
5565
- files: blobSkill.files
5566
- }, agent, {
5567
- global: installGlobally,
5568
- mode: installMode,
5569
- eveSubagent: subagent,
5570
- createMissingAgentRoot: explicitlySelectedAgents.has(agent)
5571
- });
5572
- } else result = await installSkillForAgent(skill, agent, {
5573
- global: installGlobally,
5574
- mode: installMode,
5575
- eveSubagent: subagent,
5576
- createMissingAgentRoot: explicitlySelectedAgents.has(agent)
5577
- });
5578
- results.push({
5579
- skill: getSkillDisplayName(skill),
5580
- agent: targetDisplayName(target),
5581
- pluginName: skill.pluginName,
5582
- ...result
5583
- });
5584
- }
5688
+ const results = await installToTargets(resolved, selectedSkills, installTargets, {
5689
+ global: installGlobally,
5690
+ mode: installMode,
5691
+ createMissingAgentRoot: (agent) => explicitlySelectedAgents.has(agent)
5692
+ });
5585
5693
  spinner$3.stop("Installation complete");
5586
5694
  console.log();
5587
5695
  const successful = results.filter((r) => r.success);
5588
5696
  const failed = results.filter((r) => !r.success);
5589
5697
  const successfulSkillNames = new Set(successful.map((r) => r.skill));
5590
- const skillFiles = {};
5591
- for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
5592
- else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
5593
- else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
5594
- else continue;
5698
+ const skillFiles = getSkillRepoPaths(resolved, selectedSkills);
5595
5699
  const normalizedSource = directDownload ? null : getOwnerRepo(parsed);
5596
5700
  const lockSource = directDownload ? null : getLockSource(parsed.url, normalizedSource);
5597
- const projectLockSourceUrl = directDownload ? void 0 : getProjectLockSourceUrl(parsed.type, parsed.url);
5598
5701
  if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
5599
5702
  if (await repoPrivacyPromise === false) track({
5600
5703
  event: "install",
@@ -5619,8 +5722,7 @@ async function runAdd(args, options = {}) {
5619
5722
  const skillDisplayName = getSkillDisplayName(skill);
5620
5723
  if (!successfulSkillNames.has(skillDisplayName)) continue;
5621
5724
  try {
5622
- const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : await computeSkillFolderHash(skill.path);
5623
- installedSkillHashes.set(skillDisplayName, computedHash);
5725
+ installedSkillHashes.set(skillDisplayName, await sourceSkillHash(skill));
5624
5726
  } catch {}
5625
5727
  }
5626
5728
  if (successful.length > 0 && installGlobally && (normalizedSource || parsed.type === "local" && !directDownload)) {
@@ -5666,12 +5768,7 @@ async function runAdd(args, options = {}) {
5666
5768
  if (computedHash === void 0) continue;
5667
5769
  const skillPathValue = skillFiles[skill.name];
5668
5770
  await addSkillToLocalLock(skill.name, {
5669
- source: lockSource || parsed.url,
5670
- ...projectLockSourceUrl && { sourceUrl: projectLockSourceUrl },
5671
- ref: parsed.ref,
5672
- sourceType: parsed.type,
5673
- ...skillPathValue && { skillPath: skillPathValue },
5674
- computedHash,
5771
+ ...projectLockEntry(parsed, skillPathValue, computedHash),
5675
5772
  ...recordSubagents && { subagents: eveSubagents }
5676
5773
  }, cwd);
5677
5774
  } catch {}
@@ -6173,76 +6270,402 @@ ${DIM$3} 2) npx skills add <owner/repo@skill>${RESET$3}`;
6173
6270
  else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
6174
6271
  console.log();
6175
6272
  }
6273
+ const NPM_PREFIX = "npm:";
6274
+ const REMOTE_SOURCE_TYPES = /* @__PURE__ */ new Set([
6275
+ "github",
6276
+ "gitlab",
6277
+ "git"
6278
+ ]);
6279
+ function isSkillsFieldEntry(value) {
6280
+ if (typeof value === "string") return true;
6281
+ if (!value || typeof value !== "object" || !("source" in value)) return false;
6282
+ if (typeof value.source !== "string") return false;
6283
+ const skills = "skills" in value ? value.skills : void 0;
6284
+ const ref = "ref" in value ? value.ref : void 0;
6285
+ return (skills === void 0 || Array.isArray(skills) && skills.every((s) => typeof s === "string")) && (ref === void 0 || typeof ref === "string");
6286
+ }
6287
+ function parseSkillsField(entries, declarer) {
6288
+ const parsed = {
6289
+ npm: [],
6290
+ remote: [],
6291
+ errors: []
6292
+ };
6293
+ for (const raw of entries) {
6294
+ if (!isSkillsFieldEntry(raw)) {
6295
+ parsed.errors.push(`${declarer}: invalid "skills" entry ${JSON.stringify(raw)}`);
6296
+ continue;
6297
+ }
6298
+ const { source, skills = [], ref } = typeof raw === "string" ? { source: raw } : raw;
6299
+ if (!source.startsWith(NPM_PREFIX)) {
6300
+ const remote = parseSource(source);
6301
+ if (!REMOTE_SOURCE_TYPES.has(remote.type)) parsed.errors.push(`${declarer}: "${source}" is not a git source`);
6302
+ else if (ref !== void 0 && remote.ref !== void 0) parsed.errors.push(`${declarer}: "${source}" already has a ref; remove "ref"`);
6303
+ else parsed.remote.push({
6304
+ parsed: {
6305
+ ...remote,
6306
+ ref: ref ?? remote.ref
6307
+ },
6308
+ skills: remote.skillFilter ? [...skills, remote.skillFilter] : skills
6309
+ });
6310
+ } else if (ref !== void 0) parsed.errors.push(`${declarer}: "ref" cannot be used with "${source}"`);
6311
+ else parsed.npm.push({
6312
+ package: source.slice(4),
6313
+ skills
6314
+ });
6315
+ }
6316
+ return parsed;
6317
+ }
6176
6318
  const isCancelled = (value) => typeof value === "symbol";
6319
+ function matchesSkill(skill, patterns) {
6320
+ return patterns.some((pattern) => {
6321
+ const hash = pattern.indexOf("#");
6322
+ const packagePattern = hash === -1 ? pattern : pattern.slice(0, hash);
6323
+ const skillPattern = hash === -1 ? "**" : pattern.slice(hash + 1);
6324
+ return posix.matchesGlob(skill.packageName, packagePattern) && posix.matchesGlob(sanitizeName(skill.name), skillPattern);
6325
+ });
6326
+ }
6327
+ function allowedByFilters(skill, { include, exclude }) {
6328
+ return (!include?.length || matchesSkill(skill, include)) && !(exclude?.length && matchesSkill(skill, exclude));
6329
+ }
6177
6330
  function shortenPath$1(fullPath, cwd) {
6178
6331
  const home = homedir();
6179
6332
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
6180
6333
  if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
6181
6334
  return fullPath;
6182
6335
  }
6183
- async function discoverNodeModuleSkills(cwd) {
6184
- const nodeModulesDir = join(cwd, "node_modules");
6185
- const skills = [];
6186
- let topNames;
6336
+ async function readPackageJson(dir) {
6187
6337
  try {
6188
- topNames = await readdir(nodeModulesDir);
6338
+ return JSON.parse(await readFile(join(dir, "package.json"), "utf-8"));
6189
6339
  } catch {
6190
- return skills;
6191
- }
6192
- const processPackageDir = async (pkgDir, packageName) => {
6193
- const rootSkill = await parseSkillMd(join(pkgDir, "SKILL.md"));
6194
- if (rootSkill) {
6195
- skills.push({
6196
- ...rootSkill,
6197
- packageName
6340
+ return null;
6341
+ }
6342
+ }
6343
+ async function discoverPackageSkills(pkgDir, packageName, depth) {
6344
+ const pkg = await readPackageJson(pkgDir);
6345
+ if (!pkg) return [];
6346
+ const rootSkill = await hasSkillMd(pkgDir) ? await parseSkillMd(join(pkgDir, "SKILL.md")) : null;
6347
+ if (rootSkill) return [{
6348
+ ...rootSkill,
6349
+ packageName,
6350
+ packageVersion: pkg.version,
6351
+ skillPath: "SKILL.md",
6352
+ depth
6353
+ }];
6354
+ const skills = [];
6355
+ for (const dir of ["skills", "dist/skills"]) for (const name of await readdir(join(pkgDir, dir)).catch(() => [])) {
6356
+ const skillDir = join(pkgDir, dir, name);
6357
+ if (!await hasSkillMd(skillDir)) continue;
6358
+ const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
6359
+ if (skill) skills.push({
6360
+ ...skill,
6361
+ packageName,
6362
+ packageVersion: pkg.version,
6363
+ skillPath: `${dir}/${name}/SKILL.md`,
6364
+ depth
6365
+ });
6366
+ }
6367
+ return skills;
6368
+ }
6369
+ function findInstalledPackage(from, name) {
6370
+ for (let dir = from;; dir = dirname(dir)) {
6371
+ const candidate = join(dir, "node_modules", name);
6372
+ if (existsSync(join(candidate, "package.json"))) return candidate;
6373
+ if (dirname(dir) === dir) return void 0;
6374
+ }
6375
+ }
6376
+ function remoteAllowsSkill(request, name, options) {
6377
+ return request.declarers.some((packageName) => allowedByFilters({
6378
+ name,
6379
+ packageName
6380
+ }, options));
6381
+ }
6382
+ function filterRemoteRequests(requests, options) {
6383
+ return requests.flatMap((request) => {
6384
+ const declarers = request.declarers.filter((name) => (!options.include?.length || options.include.some((p) => posix.matchesGlob(name, p.split("#")[0]))) && !options.exclude?.some((p) => !p.includes("#") && posix.matchesGlob(name, p)));
6385
+ if (declarers.length === 0) return [];
6386
+ const filtered = {
6387
+ ...request,
6388
+ via: declarers[0],
6389
+ declarers
6390
+ };
6391
+ if (request.skills.length === 0) return [filtered];
6392
+ const skills = request.skills.filter((name) => remoteAllowsSkill(filtered, name, options));
6393
+ return skills.length > 0 ? [{
6394
+ ...filtered,
6395
+ skills
6396
+ }] : [];
6397
+ });
6398
+ }
6399
+ function requestsLockedSkill(request, name, entry) {
6400
+ return isFromRequest(entry, request) && (request.skills.length === 0 || request.skills.some((selected) => sanitizeName(selected) === sanitizeName(name) || selected === posix.basename(posix.dirname(entry.skillPath ?? ""))));
6401
+ }
6402
+ const DEPENDENCY_FIELDS = [
6403
+ "dependencies",
6404
+ "devDependencies",
6405
+ "optionalDependencies",
6406
+ "peerDependencies"
6407
+ ];
6408
+ async function findWorkspacePackages(cwd, pkg) {
6409
+ const patterns = [...Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages ?? []];
6410
+ try {
6411
+ const pnpm = parse(await readFile(join(cwd, "pnpm-workspace.yaml"), "utf-8"));
6412
+ if (pnpm && typeof pnpm === "object" && "packages" in pnpm && Array.isArray(pnpm.packages)) patterns.push(...pnpm.packages.filter((p) => typeof p === "string"));
6413
+ } catch {}
6414
+ const negated = patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
6415
+ const included = patterns.filter((p) => !p.startsWith("!"));
6416
+ const dirs = /* @__PURE__ */ new Set();
6417
+ for await (const file of glob(included.map((p) => posix.join(p, "package.json")), {
6418
+ cwd,
6419
+ exclude: (path) => basename(path) === "node_modules"
6420
+ })) {
6421
+ const dir = dirname(file).split(sep).join("/");
6422
+ if (dir !== "." && !negated.some((n) => posix.matchesGlob(dir, n))) dirs.add(join(cwd, dir));
6423
+ }
6424
+ return [...dirs].sort();
6425
+ }
6426
+ async function discoverNodeModuleSkills(cwd, recursive) {
6427
+ const warnings = [];
6428
+ const errors = [];
6429
+ const remote = /* @__PURE__ */ new Map();
6430
+ const pkg = await readPackageJson(cwd);
6431
+ if (!pkg) return {
6432
+ skills: [],
6433
+ remote: [],
6434
+ warnings,
6435
+ errors
6436
+ };
6437
+ const skills = [];
6438
+ const seen = /* @__PURE__ */ new Set();
6439
+ const add = async (found) => {
6440
+ for (const skill of found) {
6441
+ const key = await realpath(skill.path).catch(() => skill.path);
6442
+ if (seen.has(key)) continue;
6443
+ seen.add(key);
6444
+ skills.push(skill);
6445
+ }
6446
+ };
6447
+ const projects = [{
6448
+ name: ".",
6449
+ dir: cwd,
6450
+ pkg,
6451
+ depth: 0
6452
+ }];
6453
+ for (const dir of recursive ? await findWorkspacePackages(cwd, pkg) : []) {
6454
+ const workspacePkg = await readPackageJson(dir);
6455
+ const name = workspacePkg?.name ?? relative(cwd, dir).split(sep).join("/");
6456
+ if (workspacePkg) projects.push({
6457
+ name,
6458
+ dir,
6459
+ pkg: workspacePkg,
6460
+ depth: 1
6461
+ });
6462
+ }
6463
+ const declarers = [];
6464
+ for (const project of projects) {
6465
+ const installed = [...new Set(DEPENDENCY_FIELDS.flatMap((field) => Object.keys(project.pkg[field] ?? {})))].flatMap((name) => {
6466
+ const dir = findInstalledPackage(project.dir, name);
6467
+ return dir ? [{
6468
+ name,
6469
+ dir
6470
+ }] : [];
6471
+ });
6472
+ await add((await Promise.all(installed.map(({ name, dir }) => discoverPackageSkills(dir, name, project.depth)))).flat());
6473
+ declarers.push({
6474
+ name: project.name,
6475
+ dir: project.dir,
6476
+ depth: project.depth
6477
+ }, ...installed.map(({ name, dir }) => ({
6478
+ name,
6479
+ dir,
6480
+ depth: project.depth + 1
6481
+ })));
6482
+ }
6483
+ const visited = /* @__PURE__ */ new Set();
6484
+ for (const declarer of declarers) {
6485
+ const dir = await realpath(declarer.dir).catch(() => null);
6486
+ if (!dir || visited.has(dir)) continue;
6487
+ visited.add(dir);
6488
+ const problems = declarer.name === "." ? errors : warnings;
6489
+ const field = (await readPackageJson(dir))?.skills;
6490
+ if (field === void 0) continue;
6491
+ if (!Array.isArray(field)) {
6492
+ if (problems === errors) errors.push("package.json: \"skills\" must be an array");
6493
+ continue;
6494
+ }
6495
+ const parsed = parseSkillsField(field, declarer.name);
6496
+ problems.push(...parsed.errors);
6497
+ for (const request of parsed.remote) {
6498
+ const { url, ref, subpath } = request.parsed;
6499
+ const key = JSON.stringify([
6500
+ url,
6501
+ ref,
6502
+ subpath,
6503
+ request.skills
6504
+ ]);
6505
+ const existing = remote.get(key);
6506
+ if (existing) {
6507
+ if (!existing.declarers.includes(declarer.name)) existing.declarers.push(declarer.name);
6508
+ } else remote.set(key, {
6509
+ ...request,
6510
+ via: declarer.name,
6511
+ declarers: [declarer.name]
6198
6512
  });
6199
- return;
6200
6513
  }
6201
- const searchDirs = [
6202
- pkgDir,
6203
- join(pkgDir, "skills"),
6204
- join(pkgDir, ".agents", "skills")
6205
- ];
6206
- for (const searchDir of searchDirs) try {
6207
- const entries = await readdir(searchDir);
6208
- for (const name of entries) {
6209
- const skillDir = join(searchDir, name);
6210
- try {
6211
- if (!(await stat(skillDir)).isDirectory()) continue;
6212
- } catch {
6213
- continue;
6214
- }
6215
- const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
6216
- if (skill) skills.push({
6217
- ...skill,
6218
- packageName
6219
- });
6514
+ for (const request of parsed.npm) {
6515
+ const target = findInstalledPackage(dir, request.package);
6516
+ if (!target) {
6517
+ problems.push(`${declarer.name}: cannot resolve "npm:${request.package}"; is it a dependency?`);
6518
+ continue;
6220
6519
  }
6221
- } catch {}
6520
+ await add((await discoverPackageSkills(target, request.package, declarer.depth)).filter((skill) => request.skills.length === 0 || request.skills.includes(basename(skill.path)) || request.skills.includes(sanitizeName(skill.name))).map((skill) => ({
6521
+ ...skill,
6522
+ via: declarer.name
6523
+ })));
6524
+ declarers.push({
6525
+ name: request.package,
6526
+ dir: target,
6527
+ depth: declarer.depth + 1
6528
+ });
6529
+ }
6530
+ }
6531
+ return {
6532
+ skills,
6533
+ remote: [...remote.values()],
6534
+ warnings,
6535
+ errors
6222
6536
  };
6223
- await Promise.all(topNames.map(async (name) => {
6224
- if (name.startsWith(".")) return;
6225
- const fullPath = join(nodeModulesDir, name);
6226
- try {
6227
- if (!(await stat(fullPath)).isDirectory()) return;
6228
- } catch {
6229
- return;
6537
+ }
6538
+ function isUnderNodeModules(path) {
6539
+ return path.split(sep).includes("node_modules");
6540
+ }
6541
+ function installedBySync(entry) {
6542
+ return entry?.sourceType === "node_modules" || entry?.via !== void 0;
6543
+ }
6544
+ async function foreignDestination(dest, canonicalDir, lockEntry, cwd) {
6545
+ let stats;
6546
+ try {
6547
+ stats = await lstat(dest);
6548
+ } catch {
6549
+ return null;
6550
+ }
6551
+ if (stats.isSymbolicLink()) {
6552
+ const target = resolve(dirname(dest), await readlink(dest));
6553
+ if (isUnderNodeModules(target) || target === canonicalDir) return null;
6554
+ return `${shortenPath$1(dest, cwd)} is a symlink to ${shortenPath$1(target, cwd)}`;
6555
+ }
6556
+ if (installedBySync(lockEntry)) return null;
6557
+ return `${shortenPath$1(dest, cwd)} already exists and was not installed by sync`;
6558
+ }
6559
+ async function linksIntoNodeModules(path) {
6560
+ try {
6561
+ if (!(await lstat(path)).isSymbolicLink()) return false;
6562
+ return isUnderNodeModules(resolve(dirname(path), await readlink(path)));
6563
+ } catch {
6564
+ return false;
6565
+ }
6566
+ }
6567
+ async function pruneStaleSkills(cwd, keep, keepRemote, lock, dryRun) {
6568
+ const canonicalBase = getCanonicalSkillsDir(false, cwd);
6569
+ const lockKeys = /* @__PURE__ */ new Map();
6570
+ const explicit = /* @__PURE__ */ new Set();
6571
+ for (const [key, entry] of Object.entries(lock.skills)) {
6572
+ const remote = entry.sourceType !== "node_modules";
6573
+ if (installedBySync(entry) && !(remote && keepRemote(entry, key))) lockKeys.set(sanitizeName(key), key);
6574
+ else explicit.add(sanitizeName(key));
6575
+ }
6576
+ const candidates = new Set(lockKeys.keys());
6577
+ for (const name of await readdir(canonicalBase).catch(() => [])) if (await linksIntoNodeModules(join(canonicalBase, name))) candidates.add(name);
6578
+ const stale = [...candidates].filter((name) => !keep.has(name) && !explicit.has(name)).sort();
6579
+ if (dryRun || stale.length === 0) return stale;
6580
+ const allAgents = Object.keys(agents);
6581
+ for (const name of stale) {
6582
+ const canonicalDir = join(canonicalBase, name);
6583
+ const key = lockKeys.get(name);
6584
+ const lockEntry = key ? lock.skills[key] : void 0;
6585
+ const destinations = /* @__PURE__ */ new Set([canonicalDir, ...allAgents.map((agent) => getInstallPath(name, agent, { cwd }))]);
6586
+ for (const dest of destinations) if (!await foreignDestination(dest, canonicalDir, lockEntry, cwd)) await rm(dest, {
6587
+ recursive: true,
6588
+ force: true
6589
+ });
6590
+ if (key) delete lock.skills[key];
6591
+ }
6592
+ await writeLocalLock(lock, cwd);
6593
+ return stale;
6594
+ }
6595
+ async function blockedReason(name, targetAgents, lockEntry, cwd) {
6596
+ if (lockEntry && !installedBySync(lockEntry)) return `installed with \`skills add\` from ${lockEntry.source}`;
6597
+ const canonicalDir = getCanonicalPath(name, { cwd });
6598
+ const destinations = /* @__PURE__ */ new Set([canonicalDir, ...targetAgents.map((agent) => getInstallPath(name, agent, { cwd }))]);
6599
+ for (const dest of destinations) {
6600
+ const reason = await foreignDestination(dest, canonicalDir, lockEntry, cwd);
6601
+ if (reason) return reason;
6602
+ }
6603
+ return null;
6604
+ }
6605
+ async function resolveConflicts(skills, targetAgents, lockSkills, cwd) {
6606
+ const lockBySanitizedName = new Map(Object.entries(lockSkills).map(([name, entry]) => [sanitizeName(name), entry]));
6607
+ const byName = Map.groupBy(skills, (skill) => sanitizeName(skill.name));
6608
+ const install = [];
6609
+ const skipped = [];
6610
+ for (const [name, all] of byName) {
6611
+ const depth = Math.min(...all.map((c) => c.depth));
6612
+ const candidates = all.filter((c) => c.depth === depth);
6613
+ for (const skill of all.filter((c) => c.depth !== depth)) skipped.push({
6614
+ skill,
6615
+ reason: `${candidates[0].packageName} is closer to the project`
6616
+ });
6617
+ if (candidates.length > 1) {
6618
+ const packages = candidates.map((c) => c.packageName).join(", ");
6619
+ for (const skill of candidates) skipped.push({
6620
+ skill,
6621
+ reason: `shipped by ${packages}; drop this one with --exclude ${skill.packageName}#${name}`
6622
+ });
6623
+ continue;
6230
6624
  }
6231
- if (name.startsWith("@")) try {
6232
- const scopeNames = await readdir(fullPath);
6233
- await Promise.all(scopeNames.map(async (scopedName) => {
6234
- const scopedPath = join(fullPath, scopedName);
6235
- try {
6236
- if (!(await stat(scopedPath)).isDirectory()) return;
6237
- } catch {
6238
- return;
6239
- }
6240
- await processPackageDir(scopedPath, `${name}/${scopedName}`);
6241
- }));
6242
- } catch {}
6243
- else await processPackageDir(fullPath, name);
6244
- }));
6245
- return skills;
6625
+ const skill = candidates[0];
6626
+ const reason = await blockedReason(name, targetAgents, lockBySanitizedName.get(name), cwd);
6627
+ if (reason) skipped.push({
6628
+ skill,
6629
+ reason
6630
+ });
6631
+ else install.push(skill);
6632
+ }
6633
+ return {
6634
+ install,
6635
+ skipped
6636
+ };
6637
+ }
6638
+ async function promptForAgentChoice(choices, fallback) {
6639
+ const universalAgents = getUniversalAgents();
6640
+ const visibleUniversalAgents = getVisibleUniversalAgents();
6641
+ const last = await getLastSelectedAgents().catch(() => void 0);
6642
+ const remembered = choices.filter((a) => last?.includes(a));
6643
+ const selected = await searchMultiselect({
6644
+ message: "Which agents do you want to install to?",
6645
+ items: choices.map((a) => ({
6646
+ value: a,
6647
+ label: agents[a].displayName,
6648
+ hint: agents[a].skillsDir
6649
+ })),
6650
+ initialSelected: remembered.length > 0 ? remembered : fallback,
6651
+ lockedSection: {
6652
+ title: "Universal (.agents/skills)",
6653
+ items: visibleUniversalAgents.map((a) => ({
6654
+ value: a,
6655
+ label: agents[a].displayName
6656
+ })),
6657
+ hiddenCount: universalAgents.length - visibleUniversalAgents.length
6658
+ }
6659
+ });
6660
+ if (!isCancelled(selected)) await saveSelectedAgents(selected).catch(() => {});
6661
+ return selected;
6662
+ }
6663
+ function isFromRequest(entry, request) {
6664
+ return entry.via !== void 0 && entry.source === getProjectLockSource(request.parsed) && entry.ref === request.parsed.ref;
6665
+ }
6666
+ function isRemoteInstalled(request, lock, cwd) {
6667
+ const installed = Object.entries(lock.skills).filter(([name, entry]) => isFromRequest(entry, request) && existsSync(getCanonicalPath(name, { cwd }))).map(([name]) => sanitizeName(name));
6668
+ return request.skills.length === 0 ? installed.length > 0 : request.skills.every((name) => installed.includes(sanitizeName(name)));
6246
6669
  }
6247
6670
  async function runSync(args, options = {}) {
6248
6671
  const cwd = process.cwd();
@@ -6259,50 +6682,54 @@ async function runSync(args, options = {}) {
6259
6682
  }
6260
6683
  }
6261
6684
  console.log();
6262
- if (!agentResult.isAgent) intro(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills experimental_sync ")));
6685
+ if (!agentResult.isAgent) intro(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills sync ")));
6263
6686
  if (agentResult.isAgent) log.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — installing non-interactively");
6264
6687
  const spinner$2 = spinner();
6265
6688
  spinner$2.start("Scanning node_modules for skills…");
6266
- const discoveredSkills = await discoverNodeModuleSkills(cwd);
6267
- if (discoveredSkills.length === 0) {
6268
- spinner$2.stop(import_picocolors.default.yellow("No skills found"));
6269
- outro(import_picocolors.default.dim("No SKILL.md files found in node_modules."));
6689
+ const discovery = await discoverNodeModuleSkills(cwd, options.recursive ?? false);
6690
+ if (discovery.errors.length > 0) {
6691
+ spinner$2.stop(import_picocolors.default.red("Invalid skills field"));
6692
+ for (const error of discovery.errors) log.error(error);
6693
+ outro(import_picocolors.default.red("Fix the \"skills\" field in package.json and run sync again."));
6694
+ process.exitCode = 1;
6695
+ return;
6696
+ }
6697
+ const discoveredSkills = discovery.skills.filter((skill) => allowedByFilters(skill, options));
6698
+ const remote = filterRemoteRequests(discovery.remote, options);
6699
+ spinner$2.stop(discoveredSkills.length === 0 ? import_picocolors.default.yellow("No skills found") : `Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
6700
+ for (const warning of discovery.warnings) log.warn(warning);
6701
+ const localLock = await readLocalLock(cwd);
6702
+ if (options.cleanup !== false) {
6703
+ const keep = new Set(discoveredSkills.map((skill) => sanitizeName(skill.name)));
6704
+ const keepRemote = (entry, name) => options.remote === false || remote.some((r) => requestsLockedSkill(r, name, entry) && remoteAllowsSkill(r, name, options));
6705
+ const stale = await pruneStaleSkills(cwd, keep, keepRemote, localLock, options.dryRun ?? false);
6706
+ for (const name of stale) log.info(`${options.dryRun ? "Would remove" : "Removed"} ${import_picocolors.default.cyan(name)} ${import_picocolors.default.dim("(no longer provided by a dependency)")}`);
6707
+ }
6708
+ const remoteRequests = (options.remote === false ? [] : remote).filter((request) => !isRemoteInstalled(request, localLock, cwd));
6709
+ const cachedRemoteSkills = [];
6710
+ if (options.remote !== false) {
6711
+ const shippedNames = new Set(discoveredSkills.map((skill) => sanitizeName(skill.name)));
6712
+ for (const [name, entry] of Object.entries(localLock.skills)) {
6713
+ if (shippedNames.has(sanitizeName(name))) continue;
6714
+ if (!remote.some((request) => !remoteRequests.includes(request) && requestsLockedSkill(request, name, entry) && remoteAllowsSkill(request, name, options))) continue;
6715
+ const skill = await parseSkillMd(join(getCanonicalPath(name, { cwd }), "SKILL.md"));
6716
+ if (skill) cachedRemoteSkills.push({
6717
+ skill,
6718
+ source: entry.source
6719
+ });
6720
+ }
6721
+ }
6722
+ if (discoveredSkills.length === 0 && remoteRequests.length === 0 && cachedRemoteSkills.length === 0) {
6723
+ outro(import_picocolors.default.dim("Nothing to sync."));
6270
6724
  return;
6271
6725
  }
6272
- spinner$2.stop(`Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
6273
6726
  for (const skill of discoveredSkills) {
6274
6727
  log.info(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}`);
6275
6728
  if (skill.description) log.message(import_picocolors.default.dim(` ${skill.description}`));
6276
6729
  }
6277
- const localLock = await readLocalLock(cwd);
6278
- const toInstall = [];
6279
- const upToDate = [];
6280
- if (options.force) {
6281
- toInstall.push(...discoveredSkills);
6282
- log.info(import_picocolors.default.dim("Force mode: reinstalling all skills"));
6283
- } else {
6284
- for (const skill of discoveredSkills) {
6285
- const existingEntry = localLock.skills[skill.name];
6286
- if (existingEntry) {
6287
- if (await computeSkillFolderHash(skill.path) === existingEntry.computedHash) {
6288
- upToDate.push(skill.name);
6289
- continue;
6290
- }
6291
- }
6292
- toInstall.push(skill);
6293
- }
6294
- if (upToDate.length > 0) log.info(import_picocolors.default.dim(`${upToDate.length} skill${upToDate.length !== 1 ? "s" : ""} already up to date`));
6295
- if (toInstall.length === 0) {
6296
- console.log();
6297
- outro(import_picocolors.default.green("All skills are up to date."));
6298
- return;
6299
- }
6300
- }
6301
- log.info(`${toInstall.length} skill${toInstall.length !== 1 ? "s" : ""} to install/update`);
6302
6730
  let targetAgents;
6303
6731
  const validAgents = Object.keys(agents);
6304
6732
  const universalAgents = getUniversalAgents();
6305
- const visibleUniversalAgents = getVisibleUniversalAgents();
6306
6733
  if (options.agent?.includes("*")) {
6307
6734
  targetAgents = validAgents;
6308
6735
  log.info(`Installing to all ${targetAgents.length} agents`);
@@ -6319,54 +6746,14 @@ async function runSync(args, options = {}) {
6319
6746
  const installedAgents = await detectInstalledAgents();
6320
6747
  const totalAgents = Object.keys(agents).length;
6321
6748
  spinner$2.stop(`${totalAgents} agents`);
6322
- if (installedAgents.length === 0) if (options.yes) {
6749
+ if (installedAgents.length === 0 && options.yes) {
6323
6750
  targetAgents = universalAgents;
6324
6751
  log.info("Installing to universal agents");
6325
- } else {
6326
- const selected = await searchMultiselect({
6327
- message: "Which agents do you want to install to?",
6328
- items: getNonUniversalAgents().map((a) => ({
6329
- value: a,
6330
- label: agents[a].displayName,
6331
- hint: agents[a].skillsDir
6332
- })),
6333
- initialSelected: [],
6334
- lockedSection: {
6335
- title: "Universal (.agents/skills)",
6336
- items: visibleUniversalAgents.map((a) => ({
6337
- value: a,
6338
- label: agents[a].displayName
6339
- })),
6340
- hiddenCount: universalAgents.length - visibleUniversalAgents.length
6341
- }
6342
- });
6343
- if (isCancelled(selected)) {
6344
- cancel("Sync cancelled");
6345
- process.exit(0);
6346
- }
6347
- targetAgents = selected;
6348
- }
6349
- else if (installedAgents.length === 1 || options.yes) {
6752
+ } else if (installedAgents.length === 1 || options.yes) {
6350
6753
  targetAgents = [...installedAgents];
6351
6754
  for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
6352
6755
  } else {
6353
- const selected = await searchMultiselect({
6354
- message: "Which agents do you want to install to?",
6355
- items: getNonUniversalAgents().filter((a) => installedAgents.includes(a)).map((a) => ({
6356
- value: a,
6357
- label: agents[a].displayName,
6358
- hint: agents[a].skillsDir
6359
- })),
6360
- initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
6361
- lockedSection: {
6362
- title: "Universal (.agents/skills)",
6363
- items: visibleUniversalAgents.map((a) => ({
6364
- value: a,
6365
- label: agents[a].displayName
6366
- })),
6367
- hiddenCount: universalAgents.length - visibleUniversalAgents.length
6368
- }
6369
- });
6756
+ const selected = await promptForAgentChoice(getNonUniversalAgents().filter((a) => installedAgents.length === 0 || installedAgents.includes(a)), installedAgents.filter((a) => !universalAgents.includes(a)));
6370
6757
  if (isCancelled(selected)) {
6371
6758
  cancel("Sync cancelled");
6372
6759
  process.exit(0);
@@ -6374,14 +6761,40 @@ async function runSync(args, options = {}) {
6374
6761
  targetAgents = selected;
6375
6762
  }
6376
6763
  }
6764
+ const { install: toInstall, skipped } = await resolveConflicts(discoveredSkills, targetAgents, localLock.skills, cwd);
6765
+ for (const { skill, reason } of skipped) log.warn(`Skipped ${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}: ${reason}`);
6766
+ const cachedToInstall = [];
6767
+ for (const cached of cachedRemoteSkills) {
6768
+ const { skill } = cached;
6769
+ const missing = targetAgents.filter((agent) => !existsSync(getInstallPath(skill.name, agent, { cwd })));
6770
+ if (missing.length === 0) continue;
6771
+ const reason = await blockedReason(skill.name, missing, localLock.skills[skill.name], cwd);
6772
+ if (reason) log.warn(`Skipped ${import_picocolors.default.cyan(skill.name)}: ${reason}`);
6773
+ else cachedToInstall.push({
6774
+ ...cached,
6775
+ agents: missing
6776
+ });
6777
+ }
6778
+ if (toInstall.length === 0 && remoteRequests.length === 0 && cachedToInstall.length === 0) {
6779
+ console.log();
6780
+ outro(import_picocolors.default.yellow("Nothing to sync."));
6781
+ return;
6782
+ }
6783
+ const mode = options.copy ? "copy" : "link";
6377
6784
  const summaryLines = [];
6378
6785
  for (const skill of toInstall) {
6379
- const shortCanonical = shortenPath$1(getCanonicalPath(skill.name, { global: false }), cwd);
6786
+ const canonicalPath = getCanonicalPath(skill.name, { cwd });
6380
6787
  summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${skill.packageName}`)}`);
6381
- summaryLines.push(` ${import_picocolors.default.dim(shortCanonical)}`);
6788
+ summaryLines.push(` ${import_picocolors.default.dim(`${shortenPath$1(canonicalPath, cwd)} ${mode === "link" ? "→" : "copied from"} ${shortenPath$1(skill.path, cwd)}`)}`);
6382
6789
  }
6790
+ for (const request of remoteRequests) summaryLines.push(`${import_picocolors.default.cyan(getProjectLockSource(request.parsed))} ${import_picocolors.default.dim(`← ${request.via} (remote)`)}`);
6791
+ for (const { skill, source, agents: missing } of cachedToInstall) summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${source} (cached), link to ${missing.join(", ")}`)}`);
6383
6792
  console.log();
6384
6793
  note(summaryLines.join("\n"), "Sync Summary");
6794
+ if (options.dryRun) {
6795
+ outro(import_picocolors.default.dim("Dry run — nothing changed."));
6796
+ return;
6797
+ }
6385
6798
  if (!options.yes) {
6386
6799
  const confirmed = await confirm({ message: "Proceed with sync?" });
6387
6800
  if (isCancel(confirmed) || !confirmed) {
@@ -6391,11 +6804,27 @@ async function runSync(args, options = {}) {
6391
6804
  }
6392
6805
  spinner$2.start("Syncing skills…");
6393
6806
  const results = [];
6807
+ for (const { skill, source, agents: missing } of cachedToInstall) for (const agent of missing) {
6808
+ const result = await installSkillForAgent(skill, agent, {
6809
+ cwd,
6810
+ mode: "symlink",
6811
+ createMissingAgentRoot: true
6812
+ });
6813
+ results.push({
6814
+ skill: skill.name,
6815
+ packageName: source,
6816
+ agent: agents[agent].displayName,
6817
+ success: result.success,
6818
+ path: result.path,
6819
+ canonicalPath: result.canonicalPath,
6820
+ error: result.error
6821
+ });
6822
+ }
6394
6823
  for (const skill of toInstall) for (const agent of targetAgents) {
6395
6824
  const result = await installSkillForAgent(skill, agent, {
6396
6825
  global: false,
6397
6826
  cwd,
6398
- mode: "symlink"
6827
+ mode: agent === "eve" ? "copy" : mode
6399
6828
  });
6400
6829
  results.push({
6401
6830
  skill: skill.name,
@@ -6416,9 +6845,45 @@ async function runSync(args, options = {}) {
6416
6845
  await addSkillToLocalLock(skill.name, {
6417
6846
  source: skill.packageName,
6418
6847
  sourceType: "node_modules",
6848
+ skillPath: skill.skillPath,
6849
+ ...skill.packageVersion && { version: skill.packageVersion },
6850
+ ...skill.via && { via: skill.via },
6419
6851
  computedHash
6420
6852
  }, cwd);
6421
6853
  } catch {}
6854
+ const claimed = new Set(discoveredSkills.map((skill) => sanitizeName(skill.name)));
6855
+ const lockEntryFor = (name) => Object.entries(localLock.skills).find(([key]) => sanitizeName(key) === name)?.[1];
6856
+ for (const request of remoteRequests) {
6857
+ const label = getProjectLockSource(request.parsed);
6858
+ const result = await installFromSource(request.parsed, {
6859
+ skills: request.skills,
6860
+ agents: targetAgents,
6861
+ via: request.via,
6862
+ select: async (skills) => {
6863
+ const selected = [];
6864
+ for (const skill of skills) {
6865
+ const name = sanitizeName(skill.name);
6866
+ if (!remoteAllowsSkill(request, name, options)) continue;
6867
+ const reason = claimed.has(name) ? "another source in this sync provides it" : await blockedReason(name, targetAgents, lockEntryFor(name), cwd);
6868
+ if (reason) {
6869
+ log.warn(`Skipped ${import_picocolors.default.cyan(name)} from ${label}: ${reason}`);
6870
+ continue;
6871
+ }
6872
+ claimed.add(name);
6873
+ selected.push(skill);
6874
+ }
6875
+ return selected;
6876
+ }
6877
+ });
6878
+ if (result.error) {
6879
+ log.error(`Failed to install from ${import_picocolors.default.cyan(label)}: ${result.error}`);
6880
+ process.exitCode = 1;
6881
+ continue;
6882
+ }
6883
+ if (result.installed.length > 0) log.success(`Installed ${result.installed.map((name) => import_picocolors.default.cyan(name)).join(", ")} from ${label}`);
6884
+ for (const failure of result.failed) log.error(failure);
6885
+ if (result.failed.length > 0) process.exitCode = 1;
6886
+ }
6422
6887
  console.log();
6423
6888
  if (successful.length > 0) {
6424
6889
  const bySkill = /* @__PURE__ */ new Map();
@@ -6430,7 +6895,7 @@ async function runSync(args, options = {}) {
6430
6895
  const resultLines = [];
6431
6896
  for (const [skillName, skillResults] of bySkill) {
6432
6897
  const firstResult = skillResults[0];
6433
- const pkg = toInstall.find((s) => s.name === skillName)?.packageName;
6898
+ const pkg = firstResult.packageName;
6434
6899
  if (firstResult.canonicalPath) {
6435
6900
  const shortPath = shortenPath$1(firstResult.canonicalPath, cwd);
6436
6901
  resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim(`← ${pkg}`)}`);
@@ -6442,13 +6907,14 @@ async function runSync(args, options = {}) {
6442
6907
  note(resultLines.join("\n"), title);
6443
6908
  }
6444
6909
  if (failed.length > 0) {
6910
+ process.exitCode = 1;
6445
6911
  console.log();
6446
6912
  log.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
6447
6913
  for (const r of failed) log.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
6448
6914
  }
6449
6915
  track({
6450
6916
  event: "experimental_sync",
6451
- skillCount: String(toInstall.length),
6917
+ skillCount: String(toInstall.length + cachedToInstall.length),
6452
6918
  successCount: String(successfulSkillNames.size),
6453
6919
  agents: targetAgents.join(",")
6454
6920
  });
@@ -6457,21 +6923,23 @@ async function runSync(args, options = {}) {
6457
6923
  }
6458
6924
  function parseSyncOptions(args) {
6459
6925
  const options = {};
6460
- for (let i = 0; i < args.length; i++) {
6926
+ let i = 0;
6927
+ const takeValues = () => {
6928
+ const values = [];
6929
+ while (args[i + 1] !== void 0 && !args[i + 1].startsWith("-")) values.push(args[++i]);
6930
+ return values;
6931
+ };
6932
+ for (; i < args.length; i++) {
6461
6933
  const arg = args[i];
6462
6934
  if (arg === "-y" || arg === "--yes") options.yes = true;
6463
- else if (arg === "-f" || arg === "--force") options.force = true;
6464
- else if (arg === "-a" || arg === "--agent") {
6465
- options.agent = options.agent || [];
6466
- i++;
6467
- let nextArg = args[i];
6468
- while (i < args.length && nextArg && !nextArg.startsWith("-")) {
6469
- options.agent.push(nextArg);
6470
- i++;
6471
- nextArg = args[i];
6472
- }
6473
- i--;
6474
- }
6935
+ else if (arg === "--copy") options.copy = true;
6936
+ else if (arg === "--dry-run") options.dryRun = true;
6937
+ else if (arg === "--no-cleanup") options.cleanup = false;
6938
+ else if (arg === "--no-remote") options.remote = false;
6939
+ else if (arg === "-r" || arg === "--recursive") options.recursive = true;
6940
+ else if (arg === "-a" || arg === "--agent") options.agent = [...options.agent ?? [], ...takeValues()];
6941
+ else if (arg === "--include") options.include = [...options.include ?? [], ...takeValues()];
6942
+ else if (arg === "--exclude") options.exclude = [...options.exclude ?? [], ...takeValues()];
6475
6943
  }
6476
6944
  return { options };
6477
6945
  }
@@ -6568,14 +7036,27 @@ async function runInstallFromLock(args) {
6568
7036
  }
6569
7037
  const remoteCount = skillEntries.length - nodeModuleSkills.length;
6570
7038
  if (remoteCount > 0) log.info(`Restoring ${import_picocolors.default.cyan(String(remoteCount))} skill${remoteCount !== 1 ? "s" : ""} from skills-lock.json into ${import_picocolors.default.dim(".agents/skills/")}`);
6571
- for (const [source, { skills }] of bySource) try {
6572
- await runAdd([source], {
6573
- skill: skills,
6574
- agent: universalAgentNames,
6575
- yes: true
7039
+ for (const [source, { sourceType, skills }] of bySource) {
7040
+ if (sourceType === "well-known") {
7041
+ await runAdd([source], {
7042
+ skill: skills,
7043
+ agent: universalAgentNames,
7044
+ yes: true
7045
+ });
7046
+ continue;
7047
+ }
7048
+ const result = await installFromSource(parseSource(source), {
7049
+ skills,
7050
+ agents: universalAgentNames
6576
7051
  });
6577
- } catch (error) {
6578
- log.error(`Failed to install from ${import_picocolors.default.cyan(source)}: ${error instanceof Error ? error.message : "Unknown error"}`);
7052
+ if (result.error) {
7053
+ log.error(`Failed to install from ${import_picocolors.default.cyan(source)}: ${result.error}`);
7054
+ process.exitCode = 1;
7055
+ continue;
7056
+ }
7057
+ log.success(`Restored ${result.installed.map((name) => import_picocolors.default.cyan(name)).join(", ")}`);
7058
+ for (const failure of result.failed) log.error(failure);
7059
+ if (result.failed.length > 0) process.exitCode = 1;
6579
7060
  }
6580
7061
  if (nodeModuleSkills.length > 0) {
6581
7062
  log.info(`${import_picocolors.default.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? "s" : ""} from node_modules`);
@@ -6739,14 +7220,18 @@ async function runList(args) {
6739
7220
  console.log();
6740
7221
  }
6741
7222
  }
6742
- function resolveSkillsToRemove(requested, folderNames, lockKeys = []) {
7223
+ function resolveSkillsToRemove(requested, folderNames, lockKeys = [], sourceByKey = {}) {
6743
7224
  const identityBySanitized = /* @__PURE__ */ new Map();
6744
7225
  for (const folder of folderNames) identityBySanitized.set(sanitizeName(folder), folder);
6745
7226
  for (const key of lockKeys) identityBySanitized.set(sanitizeName(key), key);
6746
7227
  const matched = /* @__PURE__ */ new Set();
6747
7228
  for (const name of requested) {
6748
7229
  const hit = identityBySanitized.get(sanitizeName(name));
6749
- if (hit) matched.add(hit);
7230
+ if (hit) {
7231
+ matched.add(hit);
7232
+ continue;
7233
+ }
7234
+ for (const key of lockKeys) if (sourceByKey[key] === name) matched.add(key);
6750
7235
  }
6751
7236
  return Array.from(matched);
6752
7237
  }
@@ -6794,9 +7279,11 @@ async function removeCommand(skillNames, options) {
6794
7279
  }
6795
7280
  const installedSkills = Array.from(skillNamesSet).sort();
6796
7281
  spinner$1.stop(`Found ${installedSkills.length} unique installed skill(s)`);
6797
- const lockSkillsKeys = isGlobal ? Object.keys((await readSkillLock()).skills) : Object.keys((await readLocalLock(cwd)).skills);
7282
+ const lockEntries = isGlobal ? (await readSkillLock()).skills : (await readLocalLock(cwd)).skills;
7283
+ const lockSkillsKeys = Object.keys(lockEntries);
7284
+ const sourceByKey = Object.fromEntries(lockSkillsKeys.map((key) => [key, lockEntries[key]?.source]));
6798
7285
  const requestedSkills = options.all ? [...installedSkills, ...lockSkillsKeys] : skillNames;
6799
- const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys) : [];
7286
+ const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys, sourceByKey) : [];
6800
7287
  if (installedSkills.length === 0 && resolvedRequestedSkills.length === 0) {
6801
7288
  outro(import_picocolors.default.yellow("No skills found to remove."));
6802
7289
  return;
@@ -6993,7 +7480,7 @@ function normalizeSkillName(name) {
6993
7480
  function normalizeSkillPath(path) {
6994
7481
  return path.replace(/\\/g, "/").replace(/\/+/g, "/");
6995
7482
  }
6996
- function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
7483
+ function resolveSkillLocations(lockedSkillNames, lockSkills, discovered, options = {}) {
6997
7484
  const discoveredPaths = new Set(discovered.map((skill) => normalizeSkillPath(skill.skillPath)));
6998
7485
  const pathsByName = /* @__PURE__ */ new Map();
6999
7486
  for (const skill of discovered) {
@@ -7010,11 +7497,16 @@ function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
7010
7497
  if (!lockedPath) continue;
7011
7498
  const normalizedLockedPath = normalizeSkillPath(lockedPath);
7012
7499
  const candidates = [...pathsByName.get(normalizeSkillName(name)) ?? []];
7500
+ const lockedPathStillExists = discoveredPaths.has(normalizedLockedPath);
7501
+ if (lockedPathStillExists && options.exactPathDisambiguates) {
7502
+ resolvedPaths.set(name, normalizedLockedPath);
7503
+ continue;
7504
+ }
7013
7505
  if (candidates.length > 1) {
7014
7506
  ambiguousSkills.push(name);
7015
7507
  continue;
7016
7508
  }
7017
- if (discoveredPaths.has(normalizedLockedPath)) {
7509
+ if (lockedPathStillExists) {
7018
7510
  resolvedPaths.set(name, normalizedLockedPath);
7019
7511
  continue;
7020
7512
  }
@@ -7173,8 +7665,8 @@ async function promptDeletions(source, deletedSkills, isGlobal, options) {
7173
7665
  });
7174
7666
  }
7175
7667
  }
7176
- async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discovered) {
7177
- const resolution = resolveSkillLocations(allLockedForSource, lockSkills, discovered);
7668
+ async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discovered, locationOptions = {}) {
7669
+ const resolution = resolveSkillLocations(allLockedForSource, lockSkills, discovered, locationOptions);
7178
7670
  if (resolution.ambiguousSkills.length > 0) {
7179
7671
  console.log();
7180
7672
  console.log(`${DIM$1}Warning:${RESET$1} Multiple current paths match these skills from ${DIM$1}${source}${RESET$1}; skipping them rather than deleting or migrating the wrong skill:`);
@@ -7387,7 +7879,7 @@ async function updateGlobalSkills(options = {}) {
7387
7879
  name: skill.name,
7388
7880
  skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
7389
7881
  }));
7390
- const resolution = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source && entry.ref === firstEntry.ref).map(([name, _]) => name), lock.skills, true, options, discoveredLocations);
7882
+ const resolution = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source && entry.ref === firstEntry.ref).map(([name, _]) => name), lock.skills, true, options, discoveredLocations, { exactPathDisambiguates: !shouldUseFullDepthForUpdate(firstEntry) });
7391
7883
  const deletedSkillSet = new Set(resolution.deletedSkills);
7392
7884
  for (const { name: skillName, entry } of itemsForSource) {
7393
7885
  if (deletedSkillSet.has(skillName)) continue;
@@ -7595,7 +8087,7 @@ async function updateProjectSkills(options = {}) {
7595
8087
  name: skill.name,
7596
8088
  skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
7597
8089
  }));
7598
- const resolution = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredLocations);
8090
+ const resolution = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredLocations, { exactPathDisambiguates: !shouldUseFullDepthForUpdate(firstEntry) });
7599
8091
  deletedSkills = resolution.deletedSkills;
7600
8092
  resolvedPaths = resolution.resolvedPaths;
7601
8093
  for (const [name, skillPath] of resolution.resolvedPaths) try {
@@ -8160,7 +8652,7 @@ function showBanner() {
8160
8652
  console.log();
8161
8653
  console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
8162
8654
  console.log(` ${DIM}$${RESET} ${TEXT}npx skills init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
8163
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
8655
+ console.log(` ${DIM}$${RESET} ${TEXT}npx skills sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
8164
8656
  console.log();
8165
8657
  console.log(`${DIM}try:${RESET} npx skills add vercel-labs/agent-skills`);
8166
8658
  console.log();
@@ -8168,6 +8660,37 @@ function showBanner() {
8168
8660
  console.log();
8169
8661
  }
8170
8662
  function showHelp() {
8663
+ const examples = [
8664
+ ["skills add vercel-labs/agent-skills"],
8665
+ ["skills use vercel-labs/agent-skills@vercel-optimize | claude"],
8666
+ ["skills use vercel-labs/agent-skills --skill vercel-optimize --agent claude-code"],
8667
+ ["skills add vercel-labs/agent-skills -g"],
8668
+ ["skills add vercel-labs/agent-skills --agent claude-code cursor"],
8669
+ ["skills add vercel-labs/agent-skills --skill pr-review commit"],
8670
+ ["skills add vercel-labs/agent-skills --json -y", "JSON output"],
8671
+ ["skills remove", "interactive remove"],
8672
+ ["skills remove web-design", "remove by name"],
8673
+ ["skills rm --global frontend-design"],
8674
+ ["skills list", "list project skills"],
8675
+ ["skills ls -g", "list global skills"],
8676
+ ["skills ls -a claude-code", "filter by agent"],
8677
+ ["skills ls --json", "JSON output"],
8678
+ ["skills find", "interactive search"],
8679
+ ["skills find typescript", "search by keyword"],
8680
+ ["skills find react --owner vercel", "search within an owner"],
8681
+ ["skills update"],
8682
+ ["skills update my-skill", "update a single skill"],
8683
+ ["skills update -g", "update global skills only"],
8684
+ ["skills experimental_install", "restore from skills-lock.json"],
8685
+ ["skills init my-skill"],
8686
+ ["skills sync", "sync from node_modules"],
8687
+ ["skills sync -y", "sync without prompts"]
8688
+ ];
8689
+ const exampleWidth = Math.max(...examples.filter(([, description]) => description).map(([command]) => command.length));
8690
+ const formattedExamples = examples.map(([command, description]) => {
8691
+ const comment = description ? `${" ".repeat(exampleWidth - command.length + 2)}${DIM}# ${description}${RESET}` : "";
8692
+ return ` ${DIM}$${RESET} ${command}${comment}`;
8693
+ }).join("\n");
8171
8694
  console.log(`
8172
8695
  ${BOLD}Usage:${RESET} skills <command> [options]
8173
8696
 
@@ -8197,7 +8720,7 @@ ${BOLD}Update Options:${RESET}
8197
8720
  ${BOLD}Project:${RESET}
8198
8721
  experimental_install Restore skills from skills-lock.json
8199
8722
  init [name] Initialize a skill (creates <name>/SKILL.md or ./SKILL.md)
8200
- experimental_sync Sync skills from node_modules into agent directories
8723
+ sync Sync skills from node_modules into agent directories (alias: experimental_sync)
8201
8724
 
8202
8725
  ${BOLD}Add Options:${RESET}
8203
8726
  -g, --global Install skill globally (user-level) instead of project-level
@@ -8224,9 +8747,16 @@ ${BOLD}Remove Options:${RESET}
8224
8747
  -y, --yes Skip confirmation prompts
8225
8748
  --all Remove every installed skill (-y implied). Do not combine with named skills.
8226
8749
 
8227
- ${BOLD}Experimental Sync Options:${RESET}
8750
+ ${BOLD}Sync Options:${RESET}
8228
8751
  -a, --agent <agents> Specify agents to install to (use '*' for all agents)
8229
8752
  -y, --yes Skip confirmation prompts
8753
+ --copy Copy skills instead of linking them to node_modules
8754
+ --dry-run Show what would change without changing anything
8755
+ --no-cleanup Keep skills whose package no longer ships them
8756
+ --no-remote Skip git sources listed in package.json skills fields
8757
+ -r, --recursive Also sync the dependencies of each workspace package
8758
+ --include <patterns> Only sync matching packages (<pkg>) or skills (<pkg>#<skill>)
8759
+ --exclude <patterns> Skip matching packages (<pkg>) or skills (<pkg>#<skill>)
8230
8760
 
8231
8761
  ${BOLD}List Options:${RESET}
8232
8762
  -g, --global List global skills (default: project)
@@ -8238,30 +8768,7 @@ ${BOLD}Options:${RESET}
8238
8768
  --version, -v Show version number
8239
8769
 
8240
8770
  ${BOLD}Examples:${RESET}
8241
- ${DIM}$${RESET} skills add vercel-labs/agent-skills
8242
- ${DIM}$${RESET} skills use vercel-labs/agent-skills@vercel-optimize | claude
8243
- ${DIM}$${RESET} skills use vercel-labs/agent-skills --skill vercel-optimize --agent claude-code
8244
- ${DIM}$${RESET} skills add vercel-labs/agent-skills -g
8245
- ${DIM}$${RESET} skills add vercel-labs/agent-skills --agent claude-code cursor
8246
- ${DIM}$${RESET} skills add vercel-labs/agent-skills --skill pr-review commit
8247
- ${DIM}$${RESET} skills add vercel-labs/agent-skills --json -y ${DIM}# JSON output${RESET}
8248
- ${DIM}$${RESET} skills remove ${DIM}# interactive remove${RESET}
8249
- ${DIM}$${RESET} skills remove web-design ${DIM}# remove by name${RESET}
8250
- ${DIM}$${RESET} skills rm --global frontend-design
8251
- ${DIM}$${RESET} skills list ${DIM}# list project skills${RESET}
8252
- ${DIM}$${RESET} skills ls -g ${DIM}# list global skills${RESET}
8253
- ${DIM}$${RESET} skills ls -a claude-code ${DIM}# filter by agent${RESET}
8254
- ${DIM}$${RESET} skills ls --json ${DIM}# JSON output${RESET}
8255
- ${DIM}$${RESET} skills find ${DIM}# interactive search${RESET}
8256
- ${DIM}$${RESET} skills find typescript ${DIM}# search by keyword${RESET}
8257
- ${DIM}$${RESET} skills find react --owner vercel ${DIM}# search within an owner${RESET}
8258
- ${DIM}$${RESET} skills update
8259
- ${DIM}$${RESET} skills update my-skill ${DIM}# update a single skill${RESET}
8260
- ${DIM}$${RESET} skills update -g ${DIM}# update global skills only${RESET}
8261
- ${DIM}$${RESET} skills experimental_install ${DIM}# restore from skills-lock.json${RESET}
8262
- ${DIM}$${RESET} skills init my-skill
8263
- ${DIM}$${RESET} skills experimental_sync ${DIM}# sync from node_modules${RESET}
8264
- ${DIM}$${RESET} skills experimental_sync -y ${DIM}# sync without prompts${RESET}
8771
+ ${formattedExamples}
8265
8772
 
8266
8773
  Discover more skills at ${TEXT}https://skills.sh/${RESET}
8267
8774
  `);
@@ -8402,6 +8909,7 @@ async function main() {
8402
8909
  await removeCommand(skills, removeOptions);
8403
8910
  break;
8404
8911
  }
8912
+ case "sync":
8405
8913
  case "experimental_sync": {
8406
8914
  if (!inAgent) showLogo();
8407
8915
  const { options: syncOptions } = parseSyncOptions(restArgs);