skills 1.7.0 → 1.7.2

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 +5 -5
  2. package/dist/cli.mjs +745 -307
  3. package/package.json +1 -1
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
  }
@@ -1918,8 +1920,8 @@ const agents = {
1918
1920
  pi: {
1919
1921
  name: "pi",
1920
1922
  displayName: "Pi",
1921
- skillsDir: ".pi/skills",
1922
- globalSkillsDir: join(home, ".pi/agent/skills"),
1923
+ skillsDir: ".agents/skills",
1924
+ globalSkillsDir: join(home, ".agents/skills"),
1923
1925
  detectInstalled: async () => {
1924
1926
  return existsSync(join(home, ".pi/agent"));
1925
1927
  }
@@ -2320,22 +2322,29 @@ async function installSkillForAgent(skill, agentType, options = {}) {
2320
2322
  success: true,
2321
2323
  path: canonicalDir,
2322
2324
  canonicalPath: canonicalDir,
2323
- mode: "symlink",
2325
+ mode: installMode,
2324
2326
  skipped: true
2325
2327
  };
2326
- await cleanAndCreateDirectory(canonicalDir);
2327
- await copyDirectory(skill.path, canonicalDir, agentType);
2328
+ if (installMode === "link") {
2329
+ if (!await createSymlink(skill.path, canonicalDir)) {
2330
+ await cleanAndCreateDirectory(canonicalDir);
2331
+ await copyDirectory(skill.path, canonicalDir, agentType);
2332
+ }
2333
+ } else {
2334
+ await cleanAndCreateDirectory(canonicalDir);
2335
+ await copyDirectory(skill.path, canonicalDir, agentType);
2336
+ }
2328
2337
  if (isGlobal && isUniversalAgent(agentType)) return {
2329
2338
  success: true,
2330
2339
  path: canonicalDir,
2331
2340
  canonicalPath: canonicalDir,
2332
- mode: "symlink"
2341
+ mode: installMode
2333
2342
  };
2334
2343
  if (shouldSkipProjectAgentSymlink(agentType, isGlobal, cwd, options.createMissingAgentRoot ?? false)) return {
2335
2344
  success: true,
2336
2345
  path: canonicalDir,
2337
2346
  canonicalPath: canonicalDir,
2338
- mode: "symlink",
2347
+ mode: installMode,
2339
2348
  skipped: true,
2340
2349
  skipReason: "missing-agent-project-directory"
2341
2350
  };
@@ -2346,7 +2355,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
2346
2355
  success: true,
2347
2356
  path: agentDir,
2348
2357
  canonicalPath: canonicalDir,
2349
- mode: "symlink",
2358
+ mode: installMode,
2350
2359
  symlinkFailed: true
2351
2360
  };
2352
2361
  }
@@ -2354,7 +2363,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
2354
2363
  success: true,
2355
2364
  path: agentDir,
2356
2365
  canonicalPath: canonicalDir,
2357
- mode: "symlink"
2366
+ mode: installMode
2358
2367
  };
2359
2368
  } catch (error) {
2360
2369
  return {
@@ -2785,8 +2794,8 @@ async function listInstalledSkills(options = {}) {
2785
2794
  } catch {}
2786
2795
  return Array.from(skillsMap.values());
2787
2796
  }
2788
- const TELEMETRY_URL = "https://add-skill.vercel.sh/t";
2789
- const AUDIT_URL = "https://add-skill.vercel.sh/audit";
2797
+ const TELEMETRY_URL = "https://www.skills.sh/tele/t";
2798
+ const AUDIT_URL = "https://www.skills.sh/tele/audit";
2790
2799
  let cliVersion = null;
2791
2800
  let detectedAgentName = null;
2792
2801
  function setDetectedAgent(agentName) {
@@ -4172,7 +4181,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
4172
4181
  tree
4173
4182
  };
4174
4183
  }
4175
- var version$1 = "1.7.0";
4184
+ var version$1 = "1.7.2";
4176
4185
  const NOTION_API_VERSION = "2026-03-11";
4177
4186
  const NTN_TIMEOUT_MS = 3e4;
4178
4187
  const NTN_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
@@ -4376,18 +4385,18 @@ async function fetchNotionSkillDirectory(pageId, options = {}) {
4376
4385
  }
4377
4386
  async function prepareNotionSkillSource(pageId, options = {}) {
4378
4387
  const download = options.download ?? downloadNotionDirectory;
4379
- const spinner$4 = spinner();
4380
- spinner$4.start("Fetching Notion skill with ntn…");
4388
+ const spinner$5 = spinner();
4389
+ spinner$5.start("Fetching Notion skill with ntn…");
4381
4390
  try {
4382
4391
  const [directory, workspace] = await Promise.all([fetchNotionSkillDirectory(pageId, { runNtn: options.runNtn }), fetchNotionWorkspaceName({ runNtn: options.runNtn })]);
4383
4392
  const downloaded = await download(directory.url);
4384
- spinner$4.stop(`Downloaded skill from Notion${inWorkspace(workspace)}`);
4393
+ spinner$5.stop(`Downloaded skill from Notion${inWorkspace(workspace)}`);
4385
4394
  return {
4386
4395
  rootDir: downloaded.rootDir,
4387
4396
  tempDir: downloaded.tempDir
4388
4397
  };
4389
4398
  } catch (error) {
4390
- spinner$4.stop(import_picocolors.default.red("Failed to prepare Notion skill"));
4399
+ spinner$5.stop(import_picocolors.default.red("Failed to prepare Notion skill"));
4391
4400
  throw error;
4392
4401
  }
4393
4402
  }
@@ -4411,17 +4420,17 @@ function printAvailablePacks(packs) {
4411
4420
  }
4412
4421
  }
4413
4422
  async function prepareNotionPackSource(options = {}) {
4414
- const spinner$5 = spinner();
4415
- spinner$5.start("Fetching Notion packs with ntn…");
4423
+ const spinner$6 = spinner();
4424
+ spinner$6.start("Fetching Notion packs with ntn…");
4416
4425
  let packs;
4417
4426
  let workspace = null;
4418
4427
  try {
4419
4428
  [packs, workspace] = await Promise.all([fetchNotionPacks({ runNtn: options.runNtn }), fetchNotionWorkspaceName({ runNtn: options.runNtn })]);
4420
4429
  } catch (error) {
4421
- spinner$5.stop(import_picocolors.default.red("Failed to load Notion packs"));
4430
+ spinner$6.stop(import_picocolors.default.red("Failed to load Notion packs"));
4422
4431
  throw error;
4423
4432
  }
4424
- spinner$5.stop(`Found ${import_picocolors.default.green(packs.length)} Notion pack${packs.length === 1 ? "" : "s"}${inWorkspace(workspace)}`);
4433
+ spinner$6.stop(`Found ${import_picocolors.default.green(packs.length)} Notion pack${packs.length === 1 ? "" : "s"}${inWorkspace(workspace)}`);
4425
4434
  if (packs.length === 0) throw new Error("Notion returned no packs for the authenticated workspace");
4426
4435
  if (options.list) {
4427
4436
  printAvailablePacks(packs);
@@ -4460,7 +4469,7 @@ async function prepareNotionPackSource(options = {}) {
4460
4469
  const download = options.download ?? downloadNotionDirectory;
4461
4470
  const discover = options.discover ?? discoverSkills;
4462
4471
  let skillCount = 0;
4463
- spinner$5.start("Preparing selected Notion packs…");
4472
+ spinner$6.start("Preparing selected Notion packs…");
4464
4473
  try {
4465
4474
  await mkdir$1(packsDir, { recursive: true });
4466
4475
  for (const [index, pack] of selectedPacks.entries()) {
@@ -4501,7 +4510,7 @@ async function prepareNotionPackSource(options = {}) {
4501
4510
  metadata: { pluginRoot: "./packs" },
4502
4511
  plugins: manifestPlugins
4503
4512
  }, 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"}`);
4513
+ 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
4514
  return {
4506
4515
  rootDir: stagingDir,
4507
4516
  tempDir: stagingDir,
@@ -4509,7 +4518,7 @@ async function prepareNotionPackSource(options = {}) {
4509
4518
  skillCount
4510
4519
  };
4511
4520
  } catch (error) {
4512
- spinner$5.stop(import_picocolors.default.red("Failed to prepare Notion packs"));
4521
+ spinner$6.stop(import_picocolors.default.red("Failed to prepare Notion packs"));
4513
4522
  await cleanupTempDir(stagingDir).catch(() => {});
4514
4523
  throw error;
4515
4524
  }
@@ -4685,7 +4694,8 @@ async function promptForAgents(message, choices) {
4685
4694
  const defaultValues = [
4686
4695
  "claude-code",
4687
4696
  "opencode",
4688
- "codex"
4697
+ "codex",
4698
+ "grok"
4689
4699
  ].filter((a) => validAgents.includes(a));
4690
4700
  let initialValues = [];
4691
4701
  if (lastSelected && lastSelected.length > 0) initialValues = lastSelected.filter((a) => validAgents.includes(a));
@@ -5048,6 +5058,182 @@ async function handleWellKnownSkills(source, url, options, spinner) {
5048
5058
  await promptForFindSkills(options, targetAgents);
5049
5059
  return true;
5050
5060
  }
5061
+ async function resolveSkills(parsed, options, spinner) {
5062
+ const { includeInternal } = options;
5063
+ let skills;
5064
+ let blobResult = null;
5065
+ let tempDir = null;
5066
+ try {
5067
+ if (parsed.type === "local") {
5068
+ spinner.start("Discovering skills…");
5069
+ skills = await discoverSkills(parsed.localPath, parsed.subpath, {
5070
+ includeInternal,
5071
+ fullDepth: options.fullDepth
5072
+ });
5073
+ } else if (parsed.type === "well-known" || parsed.type === "download") {
5074
+ spinner.start("Downloading source...");
5075
+ const downloaded = await downloadSource(parsed.url);
5076
+ tempDir = downloaded.tempDir;
5077
+ spinner.stop(`Downloaded ${downloaded.kind === "skill-md" ? "SKILL.md file" : "archive"}`);
5078
+ spinner.start("Discovering skills...");
5079
+ skills = await discoverSkills(downloaded.rootDir, parsed.subpath, {
5080
+ includeInternal,
5081
+ fullDepth: options.fullDepth
5082
+ });
5083
+ } else if (parsed.type === "github" && !options.fullDepth) {
5084
+ let attemptedBlobInstall = false;
5085
+ const BLOB_ALLOWED_OWNERS = [
5086
+ "vercel",
5087
+ "vercel-labs",
5088
+ "heygen-com",
5089
+ "remotion-dev"
5090
+ ];
5091
+ const ownerRepo = getOwnerRepo(parsed);
5092
+ const owner = ownerRepo?.split("/")[0]?.toLowerCase();
5093
+ const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
5094
+ if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
5095
+ attemptedBlobInstall = true;
5096
+ spinner.start("Fetching skills…");
5097
+ blobResult = await tryBlobInstall(ownerRepo, {
5098
+ subpath: parsed.subpath,
5099
+ skillFilter: parsed.skillFilter,
5100
+ ref: parsed.ref,
5101
+ getToken: getGitHubToken,
5102
+ includeInternal
5103
+ });
5104
+ }
5105
+ if (blobResult) {
5106
+ skills = blobResult.skills;
5107
+ spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
5108
+ } else {
5109
+ if (attemptedBlobInstall) spinner.message("Cloning repository…");
5110
+ else spinner.start("Cloning repository…");
5111
+ tempDir = await cloneRepo(parsed.url, parsed.ref);
5112
+ spinner.stop("Repository cloned");
5113
+ spinner.start("Discovering skills…");
5114
+ skills = await discoverSkills(tempDir, parsed.subpath, {
5115
+ includeInternal,
5116
+ fullDepth: options.fullDepth
5117
+ });
5118
+ }
5119
+ } else {
5120
+ spinner.start("Cloning repository…");
5121
+ tempDir = await cloneRepo(parsed.url, parsed.ref);
5122
+ spinner.stop("Repository cloned");
5123
+ spinner.start("Discovering skills…");
5124
+ skills = await discoverSkills(tempDir, parsed.subpath, {
5125
+ includeInternal,
5126
+ fullDepth: options.fullDepth
5127
+ });
5128
+ }
5129
+ } catch (error) {
5130
+ await cleanup(tempDir);
5131
+ throw error;
5132
+ }
5133
+ return {
5134
+ skills,
5135
+ tempDir,
5136
+ blobResult
5137
+ };
5138
+ }
5139
+ async function installToTargets(resolved, skills, targets, options) {
5140
+ const results = [];
5141
+ for (const skill of skills) for (const target of targets) {
5142
+ const { agent, subagent } = target;
5143
+ const installOptions = {
5144
+ global: options.global,
5145
+ mode: options.mode,
5146
+ eveSubagent: subagent,
5147
+ createMissingAgentRoot: options.createMissingAgentRoot(agent)
5148
+ };
5149
+ let result;
5150
+ if (resolved.blobResult && "files" in skill) {
5151
+ const blobSkill = skill;
5152
+ result = await installBlobSkillForAgent({
5153
+ installName: blobSkill.name,
5154
+ files: blobSkill.files
5155
+ }, agent, installOptions);
5156
+ } else result = await installSkillForAgent(skill, agent, installOptions);
5157
+ results.push({
5158
+ skill: getSkillDisplayName(skill),
5159
+ agent: targetDisplayName(target),
5160
+ pluginName: skill.pluginName,
5161
+ ...result
5162
+ });
5163
+ }
5164
+ return results;
5165
+ }
5166
+ function getSkillRepoPaths(resolved, skills) {
5167
+ const { tempDir, blobResult } = resolved;
5168
+ const skillFiles = {};
5169
+ for (const skill of skills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
5170
+ else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
5171
+ else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
5172
+ return skillFiles;
5173
+ }
5174
+ function getProjectLockSource(parsed) {
5175
+ return getLockSource(parsed.url, getOwnerRepo(parsed)) || parsed.url;
5176
+ }
5177
+ function projectLockEntry(parsed, skillPath, computedHash) {
5178
+ const sourceUrl = getProjectLockSourceUrl(parsed.type, parsed.url);
5179
+ return {
5180
+ source: getProjectLockSource(parsed),
5181
+ ...sourceUrl && { sourceUrl },
5182
+ ref: parsed.ref,
5183
+ sourceType: parsed.type,
5184
+ ...skillPath && { skillPath },
5185
+ computedHash
5186
+ };
5187
+ }
5188
+ async function installFromSource(parsed, options) {
5189
+ const spinner$4 = spinner();
5190
+ let resolved = null;
5191
+ try {
5192
+ resolved = await resolveSkills(parsed, { includeInternal: options.skills.length > 0 }, spinner$4);
5193
+ let selected = options.skills.length > 0 ? filterSkills(resolved.skills, options.skills) : resolved.skills;
5194
+ if (selected.length === 0) {
5195
+ spinner$4.stop(import_picocolors.default.red("No matching skills found"));
5196
+ return {
5197
+ installed: [],
5198
+ failed: [],
5199
+ error: "No matching skills found"
5200
+ };
5201
+ }
5202
+ spinner$4.stop(`Found ${import_picocolors.default.green(selected.length)} skill${selected.length > 1 ? "s" : ""}`);
5203
+ if (options.select) selected = await options.select(selected);
5204
+ const results = await installToTargets(resolved, selected, options.agents.map((agent) => ({ agent })), {
5205
+ global: false,
5206
+ mode: "symlink",
5207
+ createMissingAgentRoot: () => true
5208
+ });
5209
+ const installed = new Set(results.filter((r) => r.success).map((r) => r.skill));
5210
+ const repoPaths = getSkillRepoPaths(resolved, selected);
5211
+ for (const skill of selected) {
5212
+ if (!installed.has(getSkillDisplayName(skill))) continue;
5213
+ const entry = projectLockEntry(parsed, repoPaths[skill.name], await sourceSkillHash(skill));
5214
+ await addSkillToLocalLock(skill.name, {
5215
+ ...entry,
5216
+ ...options.via && { via: options.via }
5217
+ });
5218
+ }
5219
+ return {
5220
+ installed: [...installed],
5221
+ failed: results.filter((r) => !r.success).map((r) => `${r.skill} → ${r.agent}: ${r.error}`)
5222
+ };
5223
+ } catch (error) {
5224
+ spinner$4.stop(import_picocolors.default.red("Installation failed"));
5225
+ return {
5226
+ installed: [],
5227
+ failed: [],
5228
+ error: error instanceof Error ? error.message : String(error)
5229
+ };
5230
+ } finally {
5231
+ await cleanup(resolved?.tempDir ?? null);
5232
+ }
5233
+ }
5234
+ function sourceSkillHash(skill) {
5235
+ return "snapshotHash" in skill ? Promise.resolve(skill.snapshotHash) : computeSkillFolderHash(skill.path);
5236
+ }
5051
5237
  async function runAdd(args, options = {}) {
5052
5238
  const source = args[0];
5053
5239
  let installTipShown = false;
@@ -5162,8 +5348,6 @@ async function runAdd(args, options = {}) {
5162
5348
  if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
5163
5349
  }
5164
5350
  const includeInternal = !!(options.skill && options.skill.length > 0 && !options.skill.includes("*"));
5165
- let skills;
5166
- let blobResult = null;
5167
5351
  if (parsed.type === "local") {
5168
5352
  spinner$3.start("Validating local path…");
5169
5353
  if (!existsSync(parsed.localPath)) {
@@ -5172,67 +5356,13 @@ async function runAdd(args, options = {}) {
5172
5356
  emitJsonAndExit(1, `Local path does not exist: ${parsed.localPath}`);
5173
5357
  }
5174
5358
  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
5359
  }
5360
+ const resolved = await resolveSkills(parsed, {
5361
+ includeInternal,
5362
+ fullDepth: options.fullDepth
5363
+ }, spinner$3);
5364
+ const { skills, blobResult } = resolved;
5365
+ tempDir = resolved.tempDir ?? tempDir;
5236
5366
  if (skills.length === 0) {
5237
5367
  spinner$3.stop(import_picocolors.default.red("No skills found"));
5238
5368
  outro(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
@@ -5554,47 +5684,19 @@ async function runAdd(args, options = {}) {
5554
5684
  }
5555
5685
  }
5556
5686
  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
- }
5687
+ const results = await installToTargets(resolved, selectedSkills, installTargets, {
5688
+ global: installGlobally,
5689
+ mode: installMode,
5690
+ createMissingAgentRoot: (agent) => explicitlySelectedAgents.has(agent)
5691
+ });
5585
5692
  spinner$3.stop("Installation complete");
5586
5693
  console.log();
5587
5694
  const successful = results.filter((r) => r.success);
5588
5695
  const failed = results.filter((r) => !r.success);
5589
5696
  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;
5697
+ const skillFiles = getSkillRepoPaths(resolved, selectedSkills);
5595
5698
  const normalizedSource = directDownload ? null : getOwnerRepo(parsed);
5596
5699
  const lockSource = directDownload ? null : getLockSource(parsed.url, normalizedSource);
5597
- const projectLockSourceUrl = directDownload ? void 0 : getProjectLockSourceUrl(parsed.type, parsed.url);
5598
5700
  if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
5599
5701
  if (await repoPrivacyPromise === false) track({
5600
5702
  event: "install",
@@ -5619,13 +5721,12 @@ async function runAdd(args, options = {}) {
5619
5721
  const skillDisplayName = getSkillDisplayName(skill);
5620
5722
  if (!successfulSkillNames.has(skillDisplayName)) continue;
5621
5723
  try {
5622
- const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : await computeSkillFolderHash(skill.path);
5623
- installedSkillHashes.set(skillDisplayName, computedHash);
5724
+ installedSkillHashes.set(skillDisplayName, await sourceSkillHash(skill));
5624
5725
  } catch {}
5625
5726
  }
5626
- if (successful.length > 0 && installGlobally && normalizedSource) {
5727
+ if (successful.length > 0 && installGlobally && (normalizedSource || parsed.type === "local" && !directDownload)) {
5627
5728
  let cachedTree;
5628
- if (parsed.type === "github" && !blobResult) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
5729
+ if (parsed.type === "github" && !blobResult && normalizedSource) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
5629
5730
  for (const skill of selectedSkills) {
5630
5731
  const skillDisplayName = getSkillDisplayName(skill);
5631
5732
  if (successfulSkillNames.has(skillDisplayName)) try {
@@ -5640,9 +5741,12 @@ async function runAdd(args, options = {}) {
5640
5741
  } else if (skillPathValue && tempDir) {
5641
5742
  const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue)));
5642
5743
  if (hash) skillFolderHash = hash;
5744
+ } else if (parsed.type === "local") {
5745
+ const hash = await computeSkillFolderHash(skill.path);
5746
+ if (hash) skillFolderHash = hash;
5643
5747
  }
5644
5748
  await addSkillToLock(skill.name, {
5645
- source: lockSource || normalizedSource,
5749
+ source: lockSource || normalizedSource || parsed.url,
5646
5750
  sourceType: parsed.type,
5647
5751
  sourceUrl: parsed.url,
5648
5752
  ref: parsed.ref,
@@ -5663,12 +5767,7 @@ async function runAdd(args, options = {}) {
5663
5767
  if (computedHash === void 0) continue;
5664
5768
  const skillPathValue = skillFiles[skill.name];
5665
5769
  await addSkillToLocalLock(skill.name, {
5666
- source: lockSource || parsed.url,
5667
- ...projectLockSourceUrl && { sourceUrl: projectLockSourceUrl },
5668
- ref: parsed.ref,
5669
- sourceType: parsed.type,
5670
- ...skillPathValue && { skillPath: skillPathValue },
5671
- computedHash,
5770
+ ...projectLockEntry(parsed, skillPathValue, computedHash),
5672
5771
  ...recordSubagents && { subagents: eveSubagents }
5673
5772
  }, cwd);
5674
5773
  } catch {}
@@ -6170,76 +6269,369 @@ ${DIM$3} 2) npx skills add <owner/repo@skill>${RESET$3}`;
6170
6269
  else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
6171
6270
  console.log();
6172
6271
  }
6272
+ const NPM_PREFIX = "npm:";
6273
+ const REMOTE_SOURCE_TYPES = /* @__PURE__ */ new Set([
6274
+ "github",
6275
+ "gitlab",
6276
+ "git"
6277
+ ]);
6278
+ function isSkillsFieldEntry(value) {
6279
+ if (typeof value === "string") return true;
6280
+ if (!value || typeof value !== "object" || !("source" in value)) return false;
6281
+ if (typeof value.source !== "string") return false;
6282
+ const skills = "skills" in value ? value.skills : void 0;
6283
+ const ref = "ref" in value ? value.ref : void 0;
6284
+ return (skills === void 0 || Array.isArray(skills) && skills.every((s) => typeof s === "string")) && (ref === void 0 || typeof ref === "string");
6285
+ }
6286
+ function parseSkillsField(entries, declarer) {
6287
+ const parsed = {
6288
+ npm: [],
6289
+ remote: [],
6290
+ errors: []
6291
+ };
6292
+ for (const raw of entries) {
6293
+ if (!isSkillsFieldEntry(raw)) {
6294
+ parsed.errors.push(`${declarer}: invalid "skills" entry ${JSON.stringify(raw)}`);
6295
+ continue;
6296
+ }
6297
+ const { source, skills = [], ref } = typeof raw === "string" ? { source: raw } : raw;
6298
+ if (!source.startsWith(NPM_PREFIX)) {
6299
+ const remote = parseSource(source);
6300
+ if (!REMOTE_SOURCE_TYPES.has(remote.type)) parsed.errors.push(`${declarer}: "${source}" is not a git source`);
6301
+ else if (ref !== void 0 && remote.ref !== void 0) parsed.errors.push(`${declarer}: "${source}" already has a ref; remove "ref"`);
6302
+ else parsed.remote.push({
6303
+ parsed: {
6304
+ ...remote,
6305
+ ref: ref ?? remote.ref
6306
+ },
6307
+ skills: remote.skillFilter ? [...skills, remote.skillFilter] : skills
6308
+ });
6309
+ } else if (ref !== void 0) parsed.errors.push(`${declarer}: "ref" cannot be used with "${source}"`);
6310
+ else parsed.npm.push({
6311
+ package: source.slice(4),
6312
+ skills
6313
+ });
6314
+ }
6315
+ return parsed;
6316
+ }
6173
6317
  const isCancelled = (value) => typeof value === "symbol";
6318
+ function matchesSkill(skill, patterns) {
6319
+ return patterns.some((pattern) => {
6320
+ const hash = pattern.indexOf("#");
6321
+ const packagePattern = hash === -1 ? pattern : pattern.slice(0, hash);
6322
+ const skillPattern = hash === -1 ? "**" : pattern.slice(hash + 1);
6323
+ return posix.matchesGlob(skill.packageName, packagePattern) && posix.matchesGlob(sanitizeName(skill.name), skillPattern);
6324
+ });
6325
+ }
6174
6326
  function shortenPath$1(fullPath, cwd) {
6175
6327
  const home = homedir();
6176
6328
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
6177
6329
  if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
6178
6330
  return fullPath;
6179
6331
  }
6180
- async function discoverNodeModuleSkills(cwd) {
6181
- const nodeModulesDir = join(cwd, "node_modules");
6182
- const skills = [];
6183
- let topNames;
6332
+ async function readPackageJson(dir) {
6184
6333
  try {
6185
- topNames = await readdir(nodeModulesDir);
6334
+ return JSON.parse(await readFile(join(dir, "package.json"), "utf-8"));
6186
6335
  } catch {
6187
- return skills;
6188
- }
6189
- const processPackageDir = async (pkgDir, packageName) => {
6190
- const rootSkill = await parseSkillMd(join(pkgDir, "SKILL.md"));
6191
- if (rootSkill) {
6192
- skills.push({
6193
- ...rootSkill,
6194
- packageName
6336
+ return null;
6337
+ }
6338
+ }
6339
+ async function discoverPackageSkills(pkgDir, packageName, depth) {
6340
+ const pkg = await readPackageJson(pkgDir);
6341
+ if (!pkg) return [];
6342
+ const rootSkill = await hasSkillMd(pkgDir) ? await parseSkillMd(join(pkgDir, "SKILL.md")) : null;
6343
+ if (rootSkill) return [{
6344
+ ...rootSkill,
6345
+ packageName,
6346
+ packageVersion: pkg.version,
6347
+ skillPath: "SKILL.md",
6348
+ depth
6349
+ }];
6350
+ const skills = [];
6351
+ for (const dir of ["skills", "dist/skills"]) for (const name of await readdir(join(pkgDir, dir)).catch(() => [])) {
6352
+ const skillDir = join(pkgDir, dir, name);
6353
+ if (!await hasSkillMd(skillDir)) continue;
6354
+ const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
6355
+ if (skill) skills.push({
6356
+ ...skill,
6357
+ packageName,
6358
+ packageVersion: pkg.version,
6359
+ skillPath: `${dir}/${name}/SKILL.md`,
6360
+ depth
6361
+ });
6362
+ }
6363
+ return skills;
6364
+ }
6365
+ function findInstalledPackage(from, name) {
6366
+ for (let dir = from;; dir = dirname(dir)) {
6367
+ const candidate = join(dir, "node_modules", name);
6368
+ if (existsSync(join(candidate, "package.json"))) return candidate;
6369
+ if (dirname(dir) === dir) return void 0;
6370
+ }
6371
+ }
6372
+ const DEPENDENCY_FIELDS = [
6373
+ "dependencies",
6374
+ "devDependencies",
6375
+ "optionalDependencies",
6376
+ "peerDependencies"
6377
+ ];
6378
+ async function findWorkspacePackages(cwd, pkg) {
6379
+ const patterns = [...Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages ?? []];
6380
+ try {
6381
+ const pnpm = parse(await readFile(join(cwd, "pnpm-workspace.yaml"), "utf-8"));
6382
+ if (pnpm && typeof pnpm === "object" && "packages" in pnpm && Array.isArray(pnpm.packages)) patterns.push(...pnpm.packages.filter((p) => typeof p === "string"));
6383
+ } catch {}
6384
+ const negated = patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
6385
+ const included = patterns.filter((p) => !p.startsWith("!"));
6386
+ const dirs = /* @__PURE__ */ new Set();
6387
+ for await (const file of glob(included.map((p) => posix.join(p, "package.json")), {
6388
+ cwd,
6389
+ exclude: (path) => basename(path) === "node_modules"
6390
+ })) {
6391
+ const dir = dirname(file).split(sep).join("/");
6392
+ if (dir !== "." && !negated.some((n) => posix.matchesGlob(dir, n))) dirs.add(join(cwd, dir));
6393
+ }
6394
+ return [...dirs].sort();
6395
+ }
6396
+ async function discoverNodeModuleSkills(cwd, recursive) {
6397
+ const warnings = [];
6398
+ const errors = [];
6399
+ const remote = /* @__PURE__ */ new Map();
6400
+ const pkg = await readPackageJson(cwd);
6401
+ if (!pkg) return {
6402
+ skills: [],
6403
+ remote: [],
6404
+ warnings,
6405
+ errors
6406
+ };
6407
+ const skills = [];
6408
+ const seen = /* @__PURE__ */ new Set();
6409
+ const add = async (found) => {
6410
+ for (const skill of found) {
6411
+ const key = await realpath(skill.path).catch(() => skill.path);
6412
+ if (seen.has(key)) continue;
6413
+ seen.add(key);
6414
+ skills.push(skill);
6415
+ }
6416
+ };
6417
+ const projects = [{
6418
+ name: ".",
6419
+ dir: cwd,
6420
+ pkg,
6421
+ depth: 0
6422
+ }];
6423
+ for (const dir of recursive ? await findWorkspacePackages(cwd, pkg) : []) {
6424
+ const workspacePkg = await readPackageJson(dir);
6425
+ const name = workspacePkg?.name ?? relative(cwd, dir).split(sep).join("/");
6426
+ if (workspacePkg) projects.push({
6427
+ name,
6428
+ dir,
6429
+ pkg: workspacePkg,
6430
+ depth: 1
6431
+ });
6432
+ }
6433
+ const declarers = [];
6434
+ for (const project of projects) {
6435
+ const installed = [...new Set(DEPENDENCY_FIELDS.flatMap((field) => Object.keys(project.pkg[field] ?? {})))].flatMap((name) => {
6436
+ const dir = findInstalledPackage(project.dir, name);
6437
+ return dir ? [{
6438
+ name,
6439
+ dir
6440
+ }] : [];
6441
+ });
6442
+ await add((await Promise.all(installed.map(({ name, dir }) => discoverPackageSkills(dir, name, project.depth)))).flat());
6443
+ declarers.push({
6444
+ name: project.name,
6445
+ dir: project.dir,
6446
+ depth: project.depth
6447
+ }, ...installed.map(({ name, dir }) => ({
6448
+ name,
6449
+ dir,
6450
+ depth: project.depth + 1
6451
+ })));
6452
+ }
6453
+ const visited = /* @__PURE__ */ new Set();
6454
+ for (const declarer of declarers) {
6455
+ const dir = await realpath(declarer.dir).catch(() => null);
6456
+ if (!dir || visited.has(dir)) continue;
6457
+ visited.add(dir);
6458
+ const problems = declarer.name === "." ? errors : warnings;
6459
+ const field = (await readPackageJson(dir))?.skills;
6460
+ if (field === void 0) continue;
6461
+ if (!Array.isArray(field)) {
6462
+ if (problems === errors) errors.push("package.json: \"skills\" must be an array");
6463
+ continue;
6464
+ }
6465
+ const parsed = parseSkillsField(field, declarer.name);
6466
+ problems.push(...parsed.errors);
6467
+ for (const request of parsed.remote) {
6468
+ const { url, ref, subpath } = request.parsed;
6469
+ const key = JSON.stringify([
6470
+ url,
6471
+ ref,
6472
+ subpath,
6473
+ request.skills
6474
+ ]);
6475
+ if (!remote.has(key)) remote.set(key, {
6476
+ ...request,
6477
+ via: declarer.name
6195
6478
  });
6196
- return;
6197
6479
  }
6198
- const searchDirs = [
6199
- pkgDir,
6200
- join(pkgDir, "skills"),
6201
- join(pkgDir, ".agents", "skills")
6202
- ];
6203
- for (const searchDir of searchDirs) try {
6204
- const entries = await readdir(searchDir);
6205
- for (const name of entries) {
6206
- const skillDir = join(searchDir, name);
6207
- try {
6208
- if (!(await stat(skillDir)).isDirectory()) continue;
6209
- } catch {
6210
- continue;
6211
- }
6212
- const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
6213
- if (skill) skills.push({
6214
- ...skill,
6215
- packageName
6216
- });
6480
+ for (const request of parsed.npm) {
6481
+ const target = findInstalledPackage(dir, request.package);
6482
+ if (!target) {
6483
+ problems.push(`${declarer.name}: cannot resolve "npm:${request.package}"; is it a dependency?`);
6484
+ continue;
6217
6485
  }
6218
- } catch {}
6486
+ 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) => ({
6487
+ ...skill,
6488
+ via: declarer.name
6489
+ })));
6490
+ declarers.push({
6491
+ name: request.package,
6492
+ dir: target,
6493
+ depth: declarer.depth + 1
6494
+ });
6495
+ }
6496
+ }
6497
+ return {
6498
+ skills,
6499
+ remote: [...remote.values()],
6500
+ warnings,
6501
+ errors
6219
6502
  };
6220
- await Promise.all(topNames.map(async (name) => {
6221
- if (name.startsWith(".")) return;
6222
- const fullPath = join(nodeModulesDir, name);
6223
- try {
6224
- if (!(await stat(fullPath)).isDirectory()) return;
6225
- } catch {
6226
- return;
6503
+ }
6504
+ function isUnderNodeModules(path) {
6505
+ return path.split(sep).includes("node_modules");
6506
+ }
6507
+ function installedBySync(entry) {
6508
+ return entry?.sourceType === "node_modules" || entry?.via !== void 0;
6509
+ }
6510
+ async function foreignDestination(dest, canonicalDir, lockEntry, cwd) {
6511
+ let stats;
6512
+ try {
6513
+ stats = await lstat(dest);
6514
+ } catch {
6515
+ return null;
6516
+ }
6517
+ if (stats.isSymbolicLink()) {
6518
+ const target = resolve(dirname(dest), await readlink(dest));
6519
+ if (isUnderNodeModules(target) || target === canonicalDir) return null;
6520
+ return `${shortenPath$1(dest, cwd)} is a symlink to ${shortenPath$1(target, cwd)}`;
6521
+ }
6522
+ if (installedBySync(lockEntry)) return null;
6523
+ return `${shortenPath$1(dest, cwd)} already exists and was not installed by sync`;
6524
+ }
6525
+ async function linksIntoNodeModules(path) {
6526
+ try {
6527
+ if (!(await lstat(path)).isSymbolicLink()) return false;
6528
+ return isUnderNodeModules(resolve(dirname(path), await readlink(path)));
6529
+ } catch {
6530
+ return false;
6531
+ }
6532
+ }
6533
+ async function pruneStaleSkills(cwd, keep, keepRemote, lock, dryRun) {
6534
+ const canonicalBase = getCanonicalSkillsDir(false, cwd);
6535
+ const lockKeys = /* @__PURE__ */ new Map();
6536
+ const explicit = /* @__PURE__ */ new Set();
6537
+ for (const [key, entry] of Object.entries(lock.skills)) {
6538
+ const remote = entry.sourceType !== "node_modules";
6539
+ if (installedBySync(entry) && !(remote && keepRemote(entry))) lockKeys.set(sanitizeName(key), key);
6540
+ else explicit.add(sanitizeName(key));
6541
+ }
6542
+ const candidates = new Set(lockKeys.keys());
6543
+ for (const name of await readdir(canonicalBase).catch(() => [])) if (await linksIntoNodeModules(join(canonicalBase, name))) candidates.add(name);
6544
+ const stale = [...candidates].filter((name) => !keep.has(name) && !explicit.has(name)).sort();
6545
+ if (dryRun || stale.length === 0) return stale;
6546
+ const allAgents = Object.keys(agents);
6547
+ for (const name of stale) {
6548
+ const canonicalDir = join(canonicalBase, name);
6549
+ const key = lockKeys.get(name);
6550
+ const lockEntry = key ? lock.skills[key] : void 0;
6551
+ const destinations = /* @__PURE__ */ new Set([canonicalDir, ...allAgents.map((agent) => getInstallPath(name, agent, { cwd }))]);
6552
+ for (const dest of destinations) if (!await foreignDestination(dest, canonicalDir, lockEntry, cwd)) await rm(dest, {
6553
+ recursive: true,
6554
+ force: true
6555
+ });
6556
+ if (key) delete lock.skills[key];
6557
+ }
6558
+ await writeLocalLock(lock, cwd);
6559
+ return stale;
6560
+ }
6561
+ async function blockedReason(name, targetAgents, lockEntry, cwd) {
6562
+ if (lockEntry && !installedBySync(lockEntry)) return `installed with \`skills add\` from ${lockEntry.source}`;
6563
+ const canonicalDir = getCanonicalPath(name, { cwd });
6564
+ const destinations = /* @__PURE__ */ new Set([canonicalDir, ...targetAgents.map((agent) => getInstallPath(name, agent, { cwd }))]);
6565
+ for (const dest of destinations) {
6566
+ const reason = await foreignDestination(dest, canonicalDir, lockEntry, cwd);
6567
+ if (reason) return reason;
6568
+ }
6569
+ return null;
6570
+ }
6571
+ async function resolveConflicts(skills, targetAgents, lockSkills, cwd) {
6572
+ const lockBySanitizedName = new Map(Object.entries(lockSkills).map(([name, entry]) => [sanitizeName(name), entry]));
6573
+ const byName = Map.groupBy(skills, (skill) => sanitizeName(skill.name));
6574
+ const install = [];
6575
+ const skipped = [];
6576
+ for (const [name, all] of byName) {
6577
+ const depth = Math.min(...all.map((c) => c.depth));
6578
+ const candidates = all.filter((c) => c.depth === depth);
6579
+ for (const skill of all.filter((c) => c.depth !== depth)) skipped.push({
6580
+ skill,
6581
+ reason: `${candidates[0].packageName} is closer to the project`
6582
+ });
6583
+ if (candidates.length > 1) {
6584
+ const packages = candidates.map((c) => c.packageName).join(", ");
6585
+ for (const skill of candidates) skipped.push({
6586
+ skill,
6587
+ reason: `shipped by ${packages}; drop this one with --exclude ${skill.packageName}#${name}`
6588
+ });
6589
+ continue;
6227
6590
  }
6228
- if (name.startsWith("@")) try {
6229
- const scopeNames = await readdir(fullPath);
6230
- await Promise.all(scopeNames.map(async (scopedName) => {
6231
- const scopedPath = join(fullPath, scopedName);
6232
- try {
6233
- if (!(await stat(scopedPath)).isDirectory()) return;
6234
- } catch {
6235
- return;
6236
- }
6237
- await processPackageDir(scopedPath, `${name}/${scopedName}`);
6238
- }));
6239
- } catch {}
6240
- else await processPackageDir(fullPath, name);
6241
- }));
6242
- return skills;
6591
+ const skill = candidates[0];
6592
+ const reason = await blockedReason(name, targetAgents, lockBySanitizedName.get(name), cwd);
6593
+ if (reason) skipped.push({
6594
+ skill,
6595
+ reason
6596
+ });
6597
+ else install.push(skill);
6598
+ }
6599
+ return {
6600
+ install,
6601
+ skipped
6602
+ };
6603
+ }
6604
+ async function promptForAgentChoice(choices, fallback) {
6605
+ const universalAgents = getUniversalAgents();
6606
+ const visibleUniversalAgents = getVisibleUniversalAgents();
6607
+ const last = await getLastSelectedAgents().catch(() => void 0);
6608
+ const remembered = choices.filter((a) => last?.includes(a));
6609
+ const selected = await searchMultiselect({
6610
+ message: "Which agents do you want to install to?",
6611
+ items: choices.map((a) => ({
6612
+ value: a,
6613
+ label: agents[a].displayName,
6614
+ hint: agents[a].skillsDir
6615
+ })),
6616
+ initialSelected: remembered.length > 0 ? remembered : fallback,
6617
+ lockedSection: {
6618
+ title: "Universal (.agents/skills)",
6619
+ items: visibleUniversalAgents.map((a) => ({
6620
+ value: a,
6621
+ label: agents[a].displayName
6622
+ })),
6623
+ hiddenCount: universalAgents.length - visibleUniversalAgents.length
6624
+ }
6625
+ });
6626
+ if (!isCancelled(selected)) await saveSelectedAgents(selected).catch(() => {});
6627
+ return selected;
6628
+ }
6629
+ function isFromRequest(entry, request) {
6630
+ return entry.via !== void 0 && entry.source === getProjectLockSource(request.parsed) && entry.ref === request.parsed.ref;
6631
+ }
6632
+ function isRemoteInstalled(request, lock, cwd) {
6633
+ const installed = Object.entries(lock.skills).filter(([name, entry]) => isFromRequest(entry, request) && existsSync(getCanonicalPath(name, { cwd }))).map(([name]) => sanitizeName(name));
6634
+ return request.skills.length === 0 ? installed.length > 0 : request.skills.every((name) => installed.includes(sanitizeName(name)));
6243
6635
  }
6244
6636
  async function runSync(args, options = {}) {
6245
6637
  const cwd = process.cwd();
@@ -6260,46 +6652,37 @@ async function runSync(args, options = {}) {
6260
6652
  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");
6261
6653
  const spinner$2 = spinner();
6262
6654
  spinner$2.start("Scanning node_modules for skills…");
6263
- const discoveredSkills = await discoverNodeModuleSkills(cwd);
6264
- if (discoveredSkills.length === 0) {
6265
- spinner$2.stop(import_picocolors.default.yellow("No skills found"));
6266
- outro(import_picocolors.default.dim("No SKILL.md files found in node_modules."));
6655
+ const discovery = await discoverNodeModuleSkills(cwd, options.recursive ?? false);
6656
+ if (discovery.errors.length > 0) {
6657
+ spinner$2.stop(import_picocolors.default.red("Invalid skills field"));
6658
+ for (const error of discovery.errors) log.error(error);
6659
+ outro(import_picocolors.default.red("Fix the \"skills\" field in package.json and run sync again."));
6660
+ process.exitCode = 1;
6661
+ return;
6662
+ }
6663
+ const { include, exclude } = options;
6664
+ const discoveredSkills = discovery.skills.filter((skill) => (!include?.length || matchesSkill(skill, include)) && !(exclude?.length && matchesSkill(skill, exclude)));
6665
+ 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`);
6666
+ for (const warning of discovery.warnings) log.warn(warning);
6667
+ const localLock = await readLocalLock(cwd);
6668
+ if (options.cleanup !== false) {
6669
+ const keep = new Set(discoveredSkills.map((skill) => sanitizeName(skill.name)));
6670
+ const keepRemote = (entry) => options.remote === false || discovery.remote.some((r) => isFromRequest(entry, r));
6671
+ const stale = await pruneStaleSkills(cwd, keep, keepRemote, localLock, options.dryRun ?? false);
6672
+ 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)")}`);
6673
+ }
6674
+ const remoteRequests = (options.remote === false ? [] : discovery.remote).filter((request) => !isRemoteInstalled(request, localLock, cwd));
6675
+ if (discoveredSkills.length === 0 && remoteRequests.length === 0) {
6676
+ outro(import_picocolors.default.dim("Nothing to sync."));
6267
6677
  return;
6268
6678
  }
6269
- spinner$2.stop(`Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
6270
6679
  for (const skill of discoveredSkills) {
6271
6680
  log.info(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}`);
6272
6681
  if (skill.description) log.message(import_picocolors.default.dim(` ${skill.description}`));
6273
6682
  }
6274
- const localLock = await readLocalLock(cwd);
6275
- const toInstall = [];
6276
- const upToDate = [];
6277
- if (options.force) {
6278
- toInstall.push(...discoveredSkills);
6279
- log.info(import_picocolors.default.dim("Force mode: reinstalling all skills"));
6280
- } else {
6281
- for (const skill of discoveredSkills) {
6282
- const existingEntry = localLock.skills[skill.name];
6283
- if (existingEntry) {
6284
- if (await computeSkillFolderHash(skill.path) === existingEntry.computedHash) {
6285
- upToDate.push(skill.name);
6286
- continue;
6287
- }
6288
- }
6289
- toInstall.push(skill);
6290
- }
6291
- if (upToDate.length > 0) log.info(import_picocolors.default.dim(`${upToDate.length} skill${upToDate.length !== 1 ? "s" : ""} already up to date`));
6292
- if (toInstall.length === 0) {
6293
- console.log();
6294
- outro(import_picocolors.default.green("All skills are up to date."));
6295
- return;
6296
- }
6297
- }
6298
- log.info(`${toInstall.length} skill${toInstall.length !== 1 ? "s" : ""} to install/update`);
6299
6683
  let targetAgents;
6300
6684
  const validAgents = Object.keys(agents);
6301
6685
  const universalAgents = getUniversalAgents();
6302
- const visibleUniversalAgents = getVisibleUniversalAgents();
6303
6686
  if (options.agent?.includes("*")) {
6304
6687
  targetAgents = validAgents;
6305
6688
  log.info(`Installing to all ${targetAgents.length} agents`);
@@ -6316,54 +6699,14 @@ async function runSync(args, options = {}) {
6316
6699
  const installedAgents = await detectInstalledAgents();
6317
6700
  const totalAgents = Object.keys(agents).length;
6318
6701
  spinner$2.stop(`${totalAgents} agents`);
6319
- if (installedAgents.length === 0) if (options.yes) {
6702
+ if (installedAgents.length === 0 && options.yes) {
6320
6703
  targetAgents = universalAgents;
6321
6704
  log.info("Installing to universal agents");
6322
- } else {
6323
- const selected = await searchMultiselect({
6324
- message: "Which agents do you want to install to?",
6325
- items: getNonUniversalAgents().map((a) => ({
6326
- value: a,
6327
- label: agents[a].displayName,
6328
- hint: agents[a].skillsDir
6329
- })),
6330
- initialSelected: [],
6331
- lockedSection: {
6332
- title: "Universal (.agents/skills)",
6333
- items: visibleUniversalAgents.map((a) => ({
6334
- value: a,
6335
- label: agents[a].displayName
6336
- })),
6337
- hiddenCount: universalAgents.length - visibleUniversalAgents.length
6338
- }
6339
- });
6340
- if (isCancelled(selected)) {
6341
- cancel("Sync cancelled");
6342
- process.exit(0);
6343
- }
6344
- targetAgents = selected;
6345
- }
6346
- else if (installedAgents.length === 1 || options.yes) {
6705
+ } else if (installedAgents.length === 1 || options.yes) {
6347
6706
  targetAgents = [...installedAgents];
6348
6707
  for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
6349
6708
  } else {
6350
- const selected = await searchMultiselect({
6351
- message: "Which agents do you want to install to?",
6352
- items: getNonUniversalAgents().filter((a) => installedAgents.includes(a)).map((a) => ({
6353
- value: a,
6354
- label: agents[a].displayName,
6355
- hint: agents[a].skillsDir
6356
- })),
6357
- initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
6358
- lockedSection: {
6359
- title: "Universal (.agents/skills)",
6360
- items: visibleUniversalAgents.map((a) => ({
6361
- value: a,
6362
- label: agents[a].displayName
6363
- })),
6364
- hiddenCount: universalAgents.length - visibleUniversalAgents.length
6365
- }
6366
- });
6709
+ const selected = await promptForAgentChoice(getNonUniversalAgents().filter((a) => installedAgents.length === 0 || installedAgents.includes(a)), installedAgents.filter((a) => !universalAgents.includes(a)));
6367
6710
  if (isCancelled(selected)) {
6368
6711
  cancel("Sync cancelled");
6369
6712
  process.exit(0);
@@ -6371,14 +6714,27 @@ async function runSync(args, options = {}) {
6371
6714
  targetAgents = selected;
6372
6715
  }
6373
6716
  }
6717
+ const { install: toInstall, skipped } = await resolveConflicts(discoveredSkills, targetAgents, localLock.skills, cwd);
6718
+ for (const { skill, reason } of skipped) log.warn(`Skipped ${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}: ${reason}`);
6719
+ if (toInstall.length === 0 && remoteRequests.length === 0) {
6720
+ console.log();
6721
+ outro(import_picocolors.default.yellow("Nothing to sync."));
6722
+ return;
6723
+ }
6724
+ const mode = options.copy ? "copy" : "link";
6374
6725
  const summaryLines = [];
6375
6726
  for (const skill of toInstall) {
6376
- const shortCanonical = shortenPath$1(getCanonicalPath(skill.name, { global: false }), cwd);
6727
+ const canonicalPath = getCanonicalPath(skill.name, { cwd });
6377
6728
  summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${skill.packageName}`)}`);
6378
- summaryLines.push(` ${import_picocolors.default.dim(shortCanonical)}`);
6729
+ summaryLines.push(` ${import_picocolors.default.dim(`${shortenPath$1(canonicalPath, cwd)} ${mode === "link" ? "→" : "copied from"} ${shortenPath$1(skill.path, cwd)}`)}`);
6379
6730
  }
6731
+ for (const request of remoteRequests) summaryLines.push(`${import_picocolors.default.cyan(getProjectLockSource(request.parsed))} ${import_picocolors.default.dim(`← ${request.via} (remote)`)}`);
6380
6732
  console.log();
6381
6733
  note(summaryLines.join("\n"), "Sync Summary");
6734
+ if (options.dryRun) {
6735
+ outro(import_picocolors.default.dim("Dry run — nothing changed."));
6736
+ return;
6737
+ }
6382
6738
  if (!options.yes) {
6383
6739
  const confirmed = await confirm({ message: "Proceed with sync?" });
6384
6740
  if (isCancel(confirmed) || !confirmed) {
@@ -6392,7 +6748,7 @@ async function runSync(args, options = {}) {
6392
6748
  const result = await installSkillForAgent(skill, agent, {
6393
6749
  global: false,
6394
6750
  cwd,
6395
- mode: "symlink"
6751
+ mode: agent === "eve" ? "copy" : mode
6396
6752
  });
6397
6753
  results.push({
6398
6754
  skill: skill.name,
@@ -6413,9 +6769,44 @@ async function runSync(args, options = {}) {
6413
6769
  await addSkillToLocalLock(skill.name, {
6414
6770
  source: skill.packageName,
6415
6771
  sourceType: "node_modules",
6772
+ skillPath: skill.skillPath,
6773
+ ...skill.packageVersion && { version: skill.packageVersion },
6774
+ ...skill.via && { via: skill.via },
6416
6775
  computedHash
6417
6776
  }, cwd);
6418
6777
  } catch {}
6778
+ const claimed = new Set(discoveredSkills.map((skill) => sanitizeName(skill.name)));
6779
+ const lockEntryFor = (name) => Object.entries(localLock.skills).find(([key]) => sanitizeName(key) === name)?.[1];
6780
+ for (const request of remoteRequests) {
6781
+ const label = getProjectLockSource(request.parsed);
6782
+ const result = await installFromSource(request.parsed, {
6783
+ skills: request.skills,
6784
+ agents: targetAgents,
6785
+ via: request.via,
6786
+ select: async (skills) => {
6787
+ const selected = [];
6788
+ for (const skill of skills) {
6789
+ const name = sanitizeName(skill.name);
6790
+ const reason = claimed.has(name) ? "another source in this sync provides it" : await blockedReason(name, targetAgents, lockEntryFor(name), cwd);
6791
+ if (reason) {
6792
+ log.warn(`Skipped ${import_picocolors.default.cyan(name)} from ${label}: ${reason}`);
6793
+ continue;
6794
+ }
6795
+ claimed.add(name);
6796
+ selected.push(skill);
6797
+ }
6798
+ return selected;
6799
+ }
6800
+ });
6801
+ if (result.error) {
6802
+ log.error(`Failed to install from ${import_picocolors.default.cyan(label)}: ${result.error}`);
6803
+ process.exitCode = 1;
6804
+ continue;
6805
+ }
6806
+ if (result.installed.length > 0) log.success(`Installed ${result.installed.map((name) => import_picocolors.default.cyan(name)).join(", ")} from ${label}`);
6807
+ for (const failure of result.failed) log.error(failure);
6808
+ if (result.failed.length > 0) process.exitCode = 1;
6809
+ }
6419
6810
  console.log();
6420
6811
  if (successful.length > 0) {
6421
6812
  const bySkill = /* @__PURE__ */ new Map();
@@ -6454,21 +6845,23 @@ async function runSync(args, options = {}) {
6454
6845
  }
6455
6846
  function parseSyncOptions(args) {
6456
6847
  const options = {};
6457
- for (let i = 0; i < args.length; i++) {
6848
+ let i = 0;
6849
+ const takeValues = () => {
6850
+ const values = [];
6851
+ while (args[i + 1] !== void 0 && !args[i + 1].startsWith("-")) values.push(args[++i]);
6852
+ return values;
6853
+ };
6854
+ for (; i < args.length; i++) {
6458
6855
  const arg = args[i];
6459
6856
  if (arg === "-y" || arg === "--yes") options.yes = true;
6460
- else if (arg === "-f" || arg === "--force") options.force = true;
6461
- else if (arg === "-a" || arg === "--agent") {
6462
- options.agent = options.agent || [];
6463
- i++;
6464
- let nextArg = args[i];
6465
- while (i < args.length && nextArg && !nextArg.startsWith("-")) {
6466
- options.agent.push(nextArg);
6467
- i++;
6468
- nextArg = args[i];
6469
- }
6470
- i--;
6471
- }
6857
+ else if (arg === "--copy") options.copy = true;
6858
+ else if (arg === "--dry-run") options.dryRun = true;
6859
+ else if (arg === "--no-cleanup") options.cleanup = false;
6860
+ else if (arg === "--no-remote") options.remote = false;
6861
+ else if (arg === "-r" || arg === "--recursive") options.recursive = true;
6862
+ else if (arg === "-a" || arg === "--agent") options.agent = [...options.agent ?? [], ...takeValues()];
6863
+ else if (arg === "--include") options.include = [...options.include ?? [], ...takeValues()];
6864
+ else if (arg === "--exclude") options.exclude = [...options.exclude ?? [], ...takeValues()];
6472
6865
  }
6473
6866
  return { options };
6474
6867
  }
@@ -6565,14 +6958,27 @@ async function runInstallFromLock(args) {
6565
6958
  }
6566
6959
  const remoteCount = skillEntries.length - nodeModuleSkills.length;
6567
6960
  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/")}`);
6568
- for (const [source, { skills }] of bySource) try {
6569
- await runAdd([source], {
6570
- skill: skills,
6571
- agent: universalAgentNames,
6572
- yes: true
6961
+ for (const [source, { sourceType, skills }] of bySource) {
6962
+ if (sourceType === "well-known") {
6963
+ await runAdd([source], {
6964
+ skill: skills,
6965
+ agent: universalAgentNames,
6966
+ yes: true
6967
+ });
6968
+ continue;
6969
+ }
6970
+ const result = await installFromSource(parseSource(source), {
6971
+ skills,
6972
+ agents: universalAgentNames
6573
6973
  });
6574
- } catch (error) {
6575
- log.error(`Failed to install from ${import_picocolors.default.cyan(source)}: ${error instanceof Error ? error.message : "Unknown error"}`);
6974
+ if (result.error) {
6975
+ log.error(`Failed to install from ${import_picocolors.default.cyan(source)}: ${result.error}`);
6976
+ process.exitCode = 1;
6977
+ continue;
6978
+ }
6979
+ log.success(`Restored ${result.installed.map((name) => import_picocolors.default.cyan(name)).join(", ")}`);
6980
+ for (const failure of result.failed) log.error(failure);
6981
+ if (result.failed.length > 0) process.exitCode = 1;
6576
6982
  }
6577
6983
  if (nodeModuleSkills.length > 0) {
6578
6984
  log.info(`${import_picocolors.default.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? "s" : ""} from node_modules`);
@@ -6736,14 +7142,18 @@ async function runList(args) {
6736
7142
  console.log();
6737
7143
  }
6738
7144
  }
6739
- function resolveSkillsToRemove(requested, folderNames, lockKeys = []) {
7145
+ function resolveSkillsToRemove(requested, folderNames, lockKeys = [], sourceByKey = {}) {
6740
7146
  const identityBySanitized = /* @__PURE__ */ new Map();
6741
7147
  for (const folder of folderNames) identityBySanitized.set(sanitizeName(folder), folder);
6742
7148
  for (const key of lockKeys) identityBySanitized.set(sanitizeName(key), key);
6743
7149
  const matched = /* @__PURE__ */ new Set();
6744
7150
  for (const name of requested) {
6745
7151
  const hit = identityBySanitized.get(sanitizeName(name));
6746
- if (hit) matched.add(hit);
7152
+ if (hit) {
7153
+ matched.add(hit);
7154
+ continue;
7155
+ }
7156
+ for (const key of lockKeys) if (sourceByKey[key] === name) matched.add(key);
6747
7157
  }
6748
7158
  return Array.from(matched);
6749
7159
  }
@@ -6791,9 +7201,11 @@ async function removeCommand(skillNames, options) {
6791
7201
  }
6792
7202
  const installedSkills = Array.from(skillNamesSet).sort();
6793
7203
  spinner$1.stop(`Found ${installedSkills.length} unique installed skill(s)`);
6794
- const lockSkillsKeys = isGlobal ? Object.keys((await readSkillLock()).skills) : Object.keys((await readLocalLock(cwd)).skills);
7204
+ const lockEntries = isGlobal ? (await readSkillLock()).skills : (await readLocalLock(cwd)).skills;
7205
+ const lockSkillsKeys = Object.keys(lockEntries);
7206
+ const sourceByKey = Object.fromEntries(lockSkillsKeys.map((key) => [key, lockEntries[key]?.source]));
6795
7207
  const requestedSkills = options.all ? [...installedSkills, ...lockSkillsKeys] : skillNames;
6796
- const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys) : [];
7208
+ const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys, sourceByKey) : [];
6797
7209
  if (installedSkills.length === 0 && resolvedRequestedSkills.length === 0) {
6798
7210
  outro(import_picocolors.default.yellow("No skills found to remove."));
6799
7211
  return;
@@ -6925,7 +7337,7 @@ async function removeCommand(skillNames, options) {
6925
7337
  if (successful.length > 0) {
6926
7338
  const bySource = /* @__PURE__ */ new Map();
6927
7339
  for (const r of successful) {
6928
- const source = r.source || "local";
7340
+ const source = r.sourceType === "local" ? "local" : r.source || "local";
6929
7341
  const existing = bySource.get(source) || { skills: [] };
6930
7342
  existing.skills.push(r.skill);
6931
7343
  existing.sourceType = r.sourceType;
@@ -6990,7 +7402,7 @@ function normalizeSkillName(name) {
6990
7402
  function normalizeSkillPath(path) {
6991
7403
  return path.replace(/\\/g, "/").replace(/\/+/g, "/");
6992
7404
  }
6993
- function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
7405
+ function resolveSkillLocations(lockedSkillNames, lockSkills, discovered, options = {}) {
6994
7406
  const discoveredPaths = new Set(discovered.map((skill) => normalizeSkillPath(skill.skillPath)));
6995
7407
  const pathsByName = /* @__PURE__ */ new Map();
6996
7408
  for (const skill of discovered) {
@@ -7007,11 +7419,16 @@ function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
7007
7419
  if (!lockedPath) continue;
7008
7420
  const normalizedLockedPath = normalizeSkillPath(lockedPath);
7009
7421
  const candidates = [...pathsByName.get(normalizeSkillName(name)) ?? []];
7422
+ const lockedPathStillExists = discoveredPaths.has(normalizedLockedPath);
7423
+ if (lockedPathStillExists && options.exactPathDisambiguates) {
7424
+ resolvedPaths.set(name, normalizedLockedPath);
7425
+ continue;
7426
+ }
7010
7427
  if (candidates.length > 1) {
7011
7428
  ambiguousSkills.push(name);
7012
7429
  continue;
7013
7430
  }
7014
- if (discoveredPaths.has(normalizedLockedPath)) {
7431
+ if (lockedPathStillExists) {
7015
7432
  resolvedPaths.set(name, normalizedLockedPath);
7016
7433
  continue;
7017
7434
  }
@@ -7170,8 +7587,8 @@ async function promptDeletions(source, deletedSkills, isGlobal, options) {
7170
7587
  });
7171
7588
  }
7172
7589
  }
7173
- async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discovered) {
7174
- const resolution = resolveSkillLocations(allLockedForSource, lockSkills, discovered);
7590
+ async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discovered, locationOptions = {}) {
7591
+ const resolution = resolveSkillLocations(allLockedForSource, lockSkills, discovered, locationOptions);
7175
7592
  if (resolution.ambiguousSkills.length > 0) {
7176
7593
  console.log();
7177
7594
  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:`);
@@ -7384,7 +7801,7 @@ async function updateGlobalSkills(options = {}) {
7384
7801
  name: skill.name,
7385
7802
  skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
7386
7803
  }));
7387
- 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);
7804
+ 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) });
7388
7805
  const deletedSkillSet = new Set(resolution.deletedSkills);
7389
7806
  for (const { name: skillName, entry } of itemsForSource) {
7390
7807
  if (deletedSkillSet.has(skillName)) continue;
@@ -7545,7 +7962,7 @@ async function updateProjectSkills(options = {}) {
7545
7962
  if (hasUniversal) targetParts.push("Universal");
7546
7963
  targetParts.push(...targetAgentNames);
7547
7964
  if (targetParts.length > 0) console.log(`${TEXT$1}Updating for: ${targetParts.join(", ")}${RESET$1}`);
7548
- console.log(`${TEXT$1}Refreshing ${updatable.length + wellKnownCount} skill(s)…${RESET$1}`);
7965
+ console.log(`${TEXT$1}Checking ${updatable.length + wellKnownCount} skill(s)…${RESET$1}`);
7549
7966
  console.log();
7550
7967
  const { successCount: wkSuccessCount, failCount: wkFailCount } = await processWellKnownUpdates(wellKnownGroups, false, options);
7551
7968
  successCount += wkSuccessCount;
@@ -7559,6 +7976,7 @@ async function updateProjectSkills(options = {}) {
7559
7976
  }
7560
7977
  const localLock = await readLocalLock();
7561
7978
  const cliEntry = join(__dirname$1, "..", "bin", "cli.mjs");
7979
+ let upToDateCount = 0;
7562
7980
  if (updatable.length > 0 && !existsSync(cliEntry)) {
7563
7981
  console.log(`${DIM$1}✗ CLI entrypoint not found at ${cliEntry}${RESET$1}`);
7564
7982
  return {
@@ -7576,6 +7994,7 @@ async function updateProjectSkills(options = {}) {
7576
7994
  let tempDir = null;
7577
7995
  let deletedSkills = [];
7578
7996
  let resolvedPaths = null;
7997
+ const latestHashes = /* @__PURE__ */ new Map();
7579
7998
  if (cloneSource === null) {
7580
7999
  failCount += skillsForSource.length;
7581
8000
  console.log(`${DIM$1}✗ Cannot update ${source}: skills-lock.json is missing sourceUrl for this generic Git source${RESET$1}`);
@@ -7590,9 +8009,13 @@ async function updateProjectSkills(options = {}) {
7590
8009
  name: skill.name,
7591
8010
  skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
7592
8011
  }));
7593
- const resolution = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredLocations);
8012
+ const resolution = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredLocations, { exactPathDisambiguates: !shouldUseFullDepthForUpdate(firstEntry) });
7594
8013
  deletedSkills = resolution.deletedSkills;
7595
8014
  resolvedPaths = resolution.resolvedPaths;
8015
+ for (const [name, skillPath] of resolution.resolvedPaths) try {
8016
+ const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPath)));
8017
+ if (hash) latestHashes.set(name, hash);
8018
+ } catch {}
7596
8019
  } catch (error) {
7597
8020
  console.log(`${DIM$1}✗ Failed to check for deleted skills from ${source}${RESET$1}`);
7598
8021
  } finally {
@@ -7611,6 +8034,12 @@ async function updateProjectSkills(options = {}) {
7611
8034
  ...skill.entry,
7612
8035
  skillPath: resolvedPath
7613
8036
  } : skill.entry;
8037
+ const relocated = entry.skillPath !== skill.entry.skillPath;
8038
+ const latestHash = latestHashes.get(skill.name);
8039
+ if (!relocated && latestHash && latestHash === skill.entry.computedHash) {
8040
+ upToDateCount++;
8041
+ continue;
8042
+ }
7614
8043
  console.log(`${TEXT$1}Updating ${safeName}…${RESET$1}`);
7615
8044
  const installUrl = buildLocalUpdateSource(entry);
7616
8045
  if (!installUrl) {
@@ -7647,6 +8076,8 @@ async function updateProjectSkills(options = {}) {
7647
8076
  }
7648
8077
  }
7649
8078
  }
8079
+ if (upToDateCount > 0) if (upToDateCount === updatable.length && wkSuccessCount === 0 && wkFailCount === 0) console.log(`${TEXT$1}✓ All project skills are up to date${RESET$1}`);
8080
+ else console.log(`${DIM$1}${upToDateCount} project skill(s) already up to date${RESET$1}`);
7650
8081
  printLegacyProjectSkills(legacy);
7651
8082
  return {
7652
8083
  successCount,
@@ -8210,6 +8641,13 @@ ${BOLD}Remove Options:${RESET}
8210
8641
  ${BOLD}Experimental Sync Options:${RESET}
8211
8642
  -a, --agent <agents> Specify agents to install to (use '*' for all agents)
8212
8643
  -y, --yes Skip confirmation prompts
8644
+ --copy Copy skills instead of linking them to node_modules
8645
+ --dry-run Show what would change without changing anything
8646
+ --no-cleanup Keep skills whose package no longer ships them
8647
+ --no-remote Skip git sources listed in package.json skills fields
8648
+ -r, --recursive Also sync the dependencies of each workspace package
8649
+ --include <patterns> Only sync matching packages (<pkg>) or skills (<pkg>#<skill>)
8650
+ --exclude <patterns> Skip matching packages (<pkg>) or skills (<pkg>#<skill>)
8213
8651
 
8214
8652
  ${BOLD}List Options:${RESET}
8215
8653
  -g, --global List global skills (default: project)