skills 1.7.1 → 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 -3
  2. package/dist/cli.mjs +719 -298
  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
  }
@@ -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 {
@@ -4172,7 +4181,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
4172
4181
  tree
4173
4182
  };
4174
4183
  }
4175
- var version$1 = "1.7.1";
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,8 +5721,7 @@ 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
5727
  if (successful.length > 0 && installGlobally && (normalizedSource || parsed.type === "local" && !directDownload)) {
@@ -5666,12 +5767,7 @@ async function runAdd(args, options = {}) {
5666
5767
  if (computedHash === void 0) continue;
5667
5768
  const skillPathValue = skillFiles[skill.name];
5668
5769
  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,
5770
+ ...projectLockEntry(parsed, skillPathValue, computedHash),
5675
5771
  ...recordSubagents && { subagents: eveSubagents }
5676
5772
  }, cwd);
5677
5773
  } catch {}
@@ -6173,76 +6269,369 @@ ${DIM$3} 2) npx skills add <owner/repo@skill>${RESET$3}`;
6173
6269
  else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
6174
6270
  console.log();
6175
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
+ }
6176
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
+ }
6177
6326
  function shortenPath$1(fullPath, cwd) {
6178
6327
  const home = homedir();
6179
6328
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
6180
6329
  if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
6181
6330
  return fullPath;
6182
6331
  }
6183
- async function discoverNodeModuleSkills(cwd) {
6184
- const nodeModulesDir = join(cwd, "node_modules");
6185
- const skills = [];
6186
- let topNames;
6332
+ async function readPackageJson(dir) {
6187
6333
  try {
6188
- topNames = await readdir(nodeModulesDir);
6334
+ return JSON.parse(await readFile(join(dir, "package.json"), "utf-8"));
6189
6335
  } 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
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
6198
6478
  });
6199
- return;
6200
6479
  }
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
- });
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;
6220
6485
  }
6221
- } 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
6222
6502
  };
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;
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;
6230
6590
  }
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;
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)));
6246
6635
  }
6247
6636
  async function runSync(args, options = {}) {
6248
6637
  const cwd = process.cwd();
@@ -6263,46 +6652,37 @@ async function runSync(args, options = {}) {
6263
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");
6264
6653
  const spinner$2 = spinner();
6265
6654
  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."));
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."));
6270
6677
  return;
6271
6678
  }
6272
- spinner$2.stop(`Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
6273
6679
  for (const skill of discoveredSkills) {
6274
6680
  log.info(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}`);
6275
6681
  if (skill.description) log.message(import_picocolors.default.dim(` ${skill.description}`));
6276
6682
  }
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
6683
  let targetAgents;
6303
6684
  const validAgents = Object.keys(agents);
6304
6685
  const universalAgents = getUniversalAgents();
6305
- const visibleUniversalAgents = getVisibleUniversalAgents();
6306
6686
  if (options.agent?.includes("*")) {
6307
6687
  targetAgents = validAgents;
6308
6688
  log.info(`Installing to all ${targetAgents.length} agents`);
@@ -6319,54 +6699,14 @@ async function runSync(args, options = {}) {
6319
6699
  const installedAgents = await detectInstalledAgents();
6320
6700
  const totalAgents = Object.keys(agents).length;
6321
6701
  spinner$2.stop(`${totalAgents} agents`);
6322
- if (installedAgents.length === 0) if (options.yes) {
6702
+ if (installedAgents.length === 0 && options.yes) {
6323
6703
  targetAgents = universalAgents;
6324
6704
  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) {
6705
+ } else if (installedAgents.length === 1 || options.yes) {
6350
6706
  targetAgents = [...installedAgents];
6351
6707
  for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
6352
6708
  } 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
- });
6709
+ const selected = await promptForAgentChoice(getNonUniversalAgents().filter((a) => installedAgents.length === 0 || installedAgents.includes(a)), installedAgents.filter((a) => !universalAgents.includes(a)));
6370
6710
  if (isCancelled(selected)) {
6371
6711
  cancel("Sync cancelled");
6372
6712
  process.exit(0);
@@ -6374,14 +6714,27 @@ async function runSync(args, options = {}) {
6374
6714
  targetAgents = selected;
6375
6715
  }
6376
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";
6377
6725
  const summaryLines = [];
6378
6726
  for (const skill of toInstall) {
6379
- const shortCanonical = shortenPath$1(getCanonicalPath(skill.name, { global: false }), cwd);
6727
+ const canonicalPath = getCanonicalPath(skill.name, { cwd });
6380
6728
  summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${skill.packageName}`)}`);
6381
- 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)}`)}`);
6382
6730
  }
6731
+ for (const request of remoteRequests) summaryLines.push(`${import_picocolors.default.cyan(getProjectLockSource(request.parsed))} ${import_picocolors.default.dim(`← ${request.via} (remote)`)}`);
6383
6732
  console.log();
6384
6733
  note(summaryLines.join("\n"), "Sync Summary");
6734
+ if (options.dryRun) {
6735
+ outro(import_picocolors.default.dim("Dry run — nothing changed."));
6736
+ return;
6737
+ }
6385
6738
  if (!options.yes) {
6386
6739
  const confirmed = await confirm({ message: "Proceed with sync?" });
6387
6740
  if (isCancel(confirmed) || !confirmed) {
@@ -6395,7 +6748,7 @@ async function runSync(args, options = {}) {
6395
6748
  const result = await installSkillForAgent(skill, agent, {
6396
6749
  global: false,
6397
6750
  cwd,
6398
- mode: "symlink"
6751
+ mode: agent === "eve" ? "copy" : mode
6399
6752
  });
6400
6753
  results.push({
6401
6754
  skill: skill.name,
@@ -6416,9 +6769,44 @@ async function runSync(args, options = {}) {
6416
6769
  await addSkillToLocalLock(skill.name, {
6417
6770
  source: skill.packageName,
6418
6771
  sourceType: "node_modules",
6772
+ skillPath: skill.skillPath,
6773
+ ...skill.packageVersion && { version: skill.packageVersion },
6774
+ ...skill.via && { via: skill.via },
6419
6775
  computedHash
6420
6776
  }, cwd);
6421
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
+ }
6422
6810
  console.log();
6423
6811
  if (successful.length > 0) {
6424
6812
  const bySkill = /* @__PURE__ */ new Map();
@@ -6457,21 +6845,23 @@ async function runSync(args, options = {}) {
6457
6845
  }
6458
6846
  function parseSyncOptions(args) {
6459
6847
  const options = {};
6460
- 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++) {
6461
6855
  const arg = args[i];
6462
6856
  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
- }
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()];
6475
6865
  }
6476
6866
  return { options };
6477
6867
  }
@@ -6568,14 +6958,27 @@ async function runInstallFromLock(args) {
6568
6958
  }
6569
6959
  const remoteCount = skillEntries.length - nodeModuleSkills.length;
6570
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/")}`);
6571
- for (const [source, { skills }] of bySource) try {
6572
- await runAdd([source], {
6573
- skill: skills,
6574
- agent: universalAgentNames,
6575
- 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
6576
6973
  });
6577
- } catch (error) {
6578
- 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;
6579
6982
  }
6580
6983
  if (nodeModuleSkills.length > 0) {
6581
6984
  log.info(`${import_picocolors.default.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? "s" : ""} from node_modules`);
@@ -6739,14 +7142,18 @@ async function runList(args) {
6739
7142
  console.log();
6740
7143
  }
6741
7144
  }
6742
- function resolveSkillsToRemove(requested, folderNames, lockKeys = []) {
7145
+ function resolveSkillsToRemove(requested, folderNames, lockKeys = [], sourceByKey = {}) {
6743
7146
  const identityBySanitized = /* @__PURE__ */ new Map();
6744
7147
  for (const folder of folderNames) identityBySanitized.set(sanitizeName(folder), folder);
6745
7148
  for (const key of lockKeys) identityBySanitized.set(sanitizeName(key), key);
6746
7149
  const matched = /* @__PURE__ */ new Set();
6747
7150
  for (const name of requested) {
6748
7151
  const hit = identityBySanitized.get(sanitizeName(name));
6749
- 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);
6750
7157
  }
6751
7158
  return Array.from(matched);
6752
7159
  }
@@ -6794,9 +7201,11 @@ async function removeCommand(skillNames, options) {
6794
7201
  }
6795
7202
  const installedSkills = Array.from(skillNamesSet).sort();
6796
7203
  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);
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]));
6798
7207
  const requestedSkills = options.all ? [...installedSkills, ...lockSkillsKeys] : skillNames;
6799
- const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys) : [];
7208
+ const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys, sourceByKey) : [];
6800
7209
  if (installedSkills.length === 0 && resolvedRequestedSkills.length === 0) {
6801
7210
  outro(import_picocolors.default.yellow("No skills found to remove."));
6802
7211
  return;
@@ -6993,7 +7402,7 @@ function normalizeSkillName(name) {
6993
7402
  function normalizeSkillPath(path) {
6994
7403
  return path.replace(/\\/g, "/").replace(/\/+/g, "/");
6995
7404
  }
6996
- function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
7405
+ function resolveSkillLocations(lockedSkillNames, lockSkills, discovered, options = {}) {
6997
7406
  const discoveredPaths = new Set(discovered.map((skill) => normalizeSkillPath(skill.skillPath)));
6998
7407
  const pathsByName = /* @__PURE__ */ new Map();
6999
7408
  for (const skill of discovered) {
@@ -7010,11 +7419,16 @@ function resolveSkillLocations(lockedSkillNames, lockSkills, discovered) {
7010
7419
  if (!lockedPath) continue;
7011
7420
  const normalizedLockedPath = normalizeSkillPath(lockedPath);
7012
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
+ }
7013
7427
  if (candidates.length > 1) {
7014
7428
  ambiguousSkills.push(name);
7015
7429
  continue;
7016
7430
  }
7017
- if (discoveredPaths.has(normalizedLockedPath)) {
7431
+ if (lockedPathStillExists) {
7018
7432
  resolvedPaths.set(name, normalizedLockedPath);
7019
7433
  continue;
7020
7434
  }
@@ -7173,8 +7587,8 @@ async function promptDeletions(source, deletedSkills, isGlobal, options) {
7173
7587
  });
7174
7588
  }
7175
7589
  }
7176
- async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills, isGlobal, options, discovered) {
7177
- 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);
7178
7592
  if (resolution.ambiguousSkills.length > 0) {
7179
7593
  console.log();
7180
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:`);
@@ -7387,7 +7801,7 @@ async function updateGlobalSkills(options = {}) {
7387
7801
  name: skill.name,
7388
7802
  skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
7389
7803
  }));
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);
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) });
7391
7805
  const deletedSkillSet = new Set(resolution.deletedSkills);
7392
7806
  for (const { name: skillName, entry } of itemsForSource) {
7393
7807
  if (deletedSkillSet.has(skillName)) continue;
@@ -7595,7 +8009,7 @@ async function updateProjectSkills(options = {}) {
7595
8009
  name: skill.name,
7596
8010
  skillPath: join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/")
7597
8011
  }));
7598
- 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) });
7599
8013
  deletedSkills = resolution.deletedSkills;
7600
8014
  resolvedPaths = resolution.resolvedPaths;
7601
8015
  for (const [name, skillPath] of resolution.resolvedPaths) try {
@@ -8227,6 +8641,13 @@ ${BOLD}Remove Options:${RESET}
8227
8641
  ${BOLD}Experimental Sync Options:${RESET}
8228
8642
  -a, --agent <agents> Specify agents to install to (use '*' for all agents)
8229
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>)
8230
8651
 
8231
8652
  ${BOLD}List Options:${RESET}
8232
8653
  -g, --global List global skills (default: project)