skills 1.5.25 → 1.6.0

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 +12 -1
  2. package/dist/cli.mjs +218 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,13 @@ The CLI for the open agent skills ecosystem.
6
6
  Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [75 more](#supported-agents).
7
7
  <!-- agent-list:end -->
8
8
 
9
- [![skills.sh](https://skills.sh/b/vercel-labs/skills)](https://skills.sh/vercel-labs/skills)
9
+ <p>
10
+ <a href="https://vercel.com/labs#labs-products"><img alt="Vercel Labs Product" src="https://img.shields.io/badge/LABS-PRODUCT-0a0a0a.svg?style=for-the-badge&amp;logo=Vercel&amp;labelColor=000000" height="28"></a>
11
+ <a href="https://www.npmjs.com/package/skills"><img alt="npm version: skills" src="https://img.shields.io/npm/v/skills.svg?style=for-the-badge&amp;labelColor=000000" height="28"></a>
12
+ <a href="https://github.com/vercel-labs/skills/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/github/license/vercel-labs/skills.svg?style=for-the-badge&amp;labelColor=000000" height="28"></a>
13
+ <a href="https://www.npmjs.com/package/skills"><img alt="npm downloads per month: skills" src="https://img.shields.io/npm/dm/skills.svg?style=for-the-badge&amp;labelColor=000000&amp;label=npm%20downloads" height="28"></a>
14
+ <a href="https://skills.sh/vercel-labs/skills"><img alt="skills.sh" src="https://skills.sh/b/vercel-labs/skills?style=for-the-badge" height="28"></a>
15
+ </p>
10
16
 
11
17
  ## Install a Skill
12
18
 
@@ -40,6 +46,10 @@ npx skills add https://github.com/vercel-labs/agent-skills/tree/main/skills/web-
40
46
  # GitLab URL
41
47
  npx skills add https://gitlab.com/org/repo
42
48
 
49
+ # Azure Repos (Azure DevOps Services or Server)
50
+ npx skills add https://dev.azure.com/org/project/_git/repo
51
+ npx skills add https://dev.azure.com/org/project/_git/repo?path=/skills/web-design&version=GBmain
52
+
43
53
  # Any git URL
44
54
  npx skills add git@github.com:vercel-labs/agent-skills.git
45
55
 
@@ -61,6 +71,7 @@ npx skills add ssh://git@git.example.com/acme/private-skills.git
61
71
 
62
72
  # HTTPS on any Git host (uses your configured Git credential helper)
63
73
  npx skills add https://git.example.com/acme/private-skills.git
74
+ npx skills add https://dev.azure.com/org/project/_git/private-skills
64
75
  ```
65
76
 
66
77
  For GitHub HTTPS and shorthand sources, `skills` first uses normal Git credentials. If that fails and GitHub CLI is authenticated, it tries `gh repo clone`, followed by SSH. It does not execute `gh auth token` or copy the stored GitHub CLI credential into the Node.js process.
package/dist/cli.mjs CHANGED
@@ -18,7 +18,7 @@ import { Writable } from "stream";
18
18
  import { promisify } from "util";
19
19
  import { execFile, spawn, spawnSync } from "child_process";
20
20
  import { access, chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
21
- import { parse } from "yaml";
21
+ import { parse, stringify } from "yaml";
22
22
  import { createHash } from "crypto";
23
23
  import { 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";
24
24
  import { createHash as createHash$1 } from "node:crypto";
@@ -91,6 +91,46 @@ function sanitizeSubpath(subpath) {
91
91
  for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`);
92
92
  return subpath;
93
93
  }
94
+ function azureReposVersionToRef(version) {
95
+ if (!version) return;
96
+ return /^(?:GB|GT)(.+)$/i.exec(version)?.[1] || void 0;
97
+ }
98
+ function parseAzureReposSource(input, fragmentRef, fragmentSkillFilter) {
99
+ if (!input.startsWith("http://") && !input.startsWith("https://")) return null;
100
+ let parsed;
101
+ try {
102
+ parsed = new URL(input);
103
+ } catch {
104
+ return null;
105
+ }
106
+ const segments = parsed.pathname.split("/").filter(Boolean);
107
+ const gitIndex = segments.indexOf("_git");
108
+ if (gitIndex < 0 || gitIndex === segments.length - 1) return null;
109
+ const repo = segments[gitIndex + 1].replace(/\.git$/, "");
110
+ if (!repo) return null;
111
+ const clonePath = "/" + [
112
+ ...segments.slice(0, gitIndex),
113
+ "_git",
114
+ repo
115
+ ].map((segment) => {
116
+ try {
117
+ return encodeURIComponent(decodeURIComponent(segment));
118
+ } catch {
119
+ return encodeURIComponent(segment);
120
+ }
121
+ }).join("/");
122
+ const cloneUrl = `${parsed.protocol}//${parsed.host}${clonePath}`;
123
+ const ref = azureReposVersionToRef(parsed.searchParams.get("version")) || fragmentRef;
124
+ const pathParam = parsed.searchParams.get("path");
125
+ const subpath = pathParam ? sanitizeSubpath(pathParam.replace(/^\/+/, "")) : void 0;
126
+ return {
127
+ type: "git",
128
+ url: cloneUrl,
129
+ ...ref ? { ref } : {},
130
+ ...subpath ? { subpath } : {},
131
+ ...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
132
+ };
133
+ }
94
134
  function isLocalPath(input) {
95
135
  return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input);
96
136
  }
@@ -113,6 +153,9 @@ function looksLikeGitSource(input) {
113
153
  const pathname = parsed.pathname;
114
154
  if (isGitHubHost(parsed.host)) return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
115
155
  if (parsed.hostname === "gitlab.com") return /^\/.+?\/[^/]+(?:\.git)?(?:\/-\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
156
+ const azureSegments = pathname.split("/").filter(Boolean);
157
+ const azureGitIndex = azureSegments.indexOf("_git");
158
+ if (azureGitIndex >= 0 && azureGitIndex < azureSegments.length - 1) return true;
116
159
  } catch {}
117
160
  if (/^https?:\/\/.+\.git(?:$|[/?])/i.test(input)) return true;
118
161
  return !input.includes(":") && !input.startsWith(".") && !input.startsWith("/") && /^([^/]+)\/([^/]+)(?:\/(.+)|@(.+))?$/.test(input);
@@ -245,6 +288,8 @@ function parseSource(input) {
245
288
  ...fragmentRef ? { ref: fragmentRef } : {}
246
289
  };
247
290
  }
291
+ const azureRepos = parseAzureReposSource(input, fragmentRef, fragmentSkillFilter);
292
+ if (azureRepos) return azureRepos;
248
293
  const githubHost = getGitHubHost();
249
294
  const shorthandSourceType = githubHost === "github.com" ? "github" : "git";
250
295
  const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
@@ -1464,6 +1509,7 @@ const agents = {
1464
1509
  displayName: "Claude Code",
1465
1510
  skillsDir: ".claude/skills",
1466
1511
  globalSkillsDir: join(claudeHome, "skills"),
1512
+ createProjectSkillsDirByDefault: true,
1467
1513
  detectInstalled: async () => {
1468
1514
  return existsSync(claudeHome);
1469
1515
  }
@@ -2145,6 +2191,11 @@ function isPathSafe$2(basePath, targetPath) {
2145
2191
  function pathsOverlap(pathA, pathB) {
2146
2192
  return isPathSafe$2(pathA, pathB) || isPathSafe$2(pathB, pathA);
2147
2193
  }
2194
+ function shouldSkipProjectAgentSymlink(agentType, isGlobal, cwd, createMissingAgentRoot) {
2195
+ if (isGlobal || isUniversalAgent(agentType) || createMissingAgentRoot || agents[agentType].createProjectSkillsDirByDefault) return false;
2196
+ const agentRoot = agents[agentType].skillsDir.split("/")[0];
2197
+ return !existsSync(join(cwd, agentRoot));
2198
+ }
2148
2199
  async function isDirEntryOrSymlinkToDir(entry, entryPath) {
2149
2200
  if (entry.isDirectory()) return true;
2150
2201
  if (!entry.isSymbolicLink()) return false;
@@ -2280,15 +2331,14 @@ async function installSkillForAgent(skill, agentType, options = {}) {
2280
2331
  canonicalPath: canonicalDir,
2281
2332
  mode: "symlink"
2282
2333
  };
2283
- if (!isGlobal && !isUniversalAgent(agentType)) {
2284
- if (!existsSync(join(cwd, agents[agentType].skillsDir.split("/")[0])) && agentType !== "claude-code") return {
2285
- success: true,
2286
- path: canonicalDir,
2287
- canonicalPath: canonicalDir,
2288
- mode: "symlink",
2289
- skipped: true
2290
- };
2291
- }
2334
+ if (shouldSkipProjectAgentSymlink(agentType, isGlobal, cwd, options.createMissingAgentRoot ?? false)) return {
2335
+ success: true,
2336
+ path: canonicalDir,
2337
+ canonicalPath: canonicalDir,
2338
+ mode: "symlink",
2339
+ skipped: true,
2340
+ skipReason: "missing-agent-project-directory"
2341
+ };
2292
2342
  if (!await createSymlink(canonicalDir, agentDir)) {
2293
2343
  await cleanAndCreateDirectory(agentDir);
2294
2344
  await copyDirectory(skill.path, agentDir, agentType);
@@ -2329,15 +2379,14 @@ const isExcluded$1 = (name, isDirectory = false) => {
2329
2379
  function stripIgnoredEveFrontmatter(raw) {
2330
2380
  const { data, content } = parseFrontmatter(raw);
2331
2381
  const eveData = {};
2382
+ if (typeof data.name === "string") eveData.name = data.name;
2332
2383
  if (typeof data.description === "string") eveData.description = data.description;
2333
2384
  if (typeof data.license === "string") eveData.license = data.license;
2334
- if (data.metadata && typeof data.metadata === "object" && !Array.isArray(data.metadata)) {
2335
- const metadata = Object.fromEntries(Object.entries(data.metadata).filter((entry) => typeof entry[1] === "string"));
2336
- if (Object.keys(metadata).length > 0) eveData.metadata = metadata;
2337
- }
2338
- const keys = Object.keys(eveData);
2339
- if (keys.length === 0) return content.replace(/^\r?\n/u, "");
2340
- return `---\n${keys.map((key) => `${key}: ${JSON.stringify(eveData[key])}`).join("\n")}\n---\n${content.replace(/^\r?\n/u, "")}`;
2385
+ if (data.compatibility !== void 0) eveData.compatibility = data.compatibility;
2386
+ if (typeof data.version === "string" || typeof data.version === "number") eveData.version = data.version;
2387
+ if (data.metadata && typeof data.metadata === "object" && !Array.isArray(data.metadata)) eveData.metadata = data.metadata;
2388
+ if (Object.keys(eveData).length === 0) return content.replace(/^\r?\n/u, "");
2389
+ return `---\n${stringify(eveData).trimEnd()}\n---\n${content.replace(/^\r?\n/u, "")}`;
2341
2390
  }
2342
2391
  async function copyDirectory(src, dest, agentType) {
2343
2392
  await mkdir(dest, { recursive: true });
@@ -2571,15 +2620,14 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
2571
2620
  canonicalPath: canonicalDir,
2572
2621
  mode: "symlink"
2573
2622
  };
2574
- if (!isGlobal && !isUniversalAgent(agentType)) {
2575
- if (!existsSync(join(cwd, agents[agentType].skillsDir.split("/")[0])) && agentType !== "claude-code") return {
2576
- success: true,
2577
- path: canonicalDir,
2578
- canonicalPath: canonicalDir,
2579
- mode: "symlink",
2580
- skipped: true
2581
- };
2582
- }
2623
+ if (shouldSkipProjectAgentSymlink(agentType, isGlobal, cwd, options.createMissingAgentRoot ?? false)) return {
2624
+ success: true,
2625
+ path: canonicalDir,
2626
+ canonicalPath: canonicalDir,
2627
+ mode: "symlink",
2628
+ skipped: true,
2629
+ skipReason: "missing-agent-project-directory"
2630
+ };
2583
2631
  if (!await createSymlink(canonicalDir, agentDir)) {
2584
2632
  await cleanAndCreateDirectory(agentDir);
2585
2633
  await writeSkillFiles(agentDir);
@@ -4121,7 +4169,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
4121
4169
  tree
4122
4170
  };
4123
4171
  }
4124
- var version$1 = "1.5.25";
4172
+ var version$1 = "1.6.0";
4125
4173
  const isCancelled$1 = (value) => typeof value === "symbol";
4126
4174
  const EVE_AGENT_LABEL = "eve agent";
4127
4175
  async function isSourcePrivate(source) {
@@ -4269,9 +4317,11 @@ function buildResultLines(results, targetAgents) {
4269
4317
  const { universal, symlinked: symlinkAgents } = splitAgentsByType(targetAgents);
4270
4318
  const successfulSymlinks = results.filter((r) => !r.symlinkFailed && !r.skipped && !universal.includes(r.agent)).map((r) => r.agent);
4271
4319
  const failedSymlinks = results.filter((r) => r.symlinkFailed && !r.skipped).map((r) => r.agent);
4320
+ const skippedSymlinks = results.filter((r) => r.skipped && r.skipReason === "missing-agent-project-directory" && symlinkAgents.includes(r.agent)).map((r) => r.agent);
4272
4321
  if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
4273
4322
  if (successfulSymlinks.length > 0) lines.push(` ${import_picocolors.default.dim("symlinked:")} ${formatList$1(successfulSymlinks)}`);
4274
4323
  if (failedSymlinks.length > 0) lines.push(` ${import_picocolors.default.yellow("copied:")} ${formatList$1(failedSymlinks)}`);
4324
+ if (skippedSymlinks.length > 0) lines.push(` ${import_picocolors.default.yellow("skipped:")} ${formatList$1(skippedSymlinks)} ${import_picocolors.default.dim("(project directory not found)")}`);
4275
4325
  return lines;
4276
4326
  }
4277
4327
  function exitInstallationCancelled() {
@@ -4341,6 +4391,17 @@ async function selectAgentsInteractive(options) {
4341
4391
  return selected;
4342
4392
  }
4343
4393
  setVersion(version$1);
4394
+ function buildJsonSecurity(auditData, skillName, source) {
4395
+ const data = auditData?.[skillName];
4396
+ if (!data || Object.keys(data).length === 0) return null;
4397
+ const socketAlerts = data.socket?.alerts ?? 0;
4398
+ return {
4399
+ ...data.ath && { gen: data.ath.risk },
4400
+ ...data.socket && { socket: `${socketAlerts} alert${socketAlerts !== 1 ? "s" : ""}` },
4401
+ ...data.snyk && { snyk: data.snyk.risk },
4402
+ ...source && { details: `https://skills.sh/${source}` }
4403
+ };
4404
+ }
4344
4405
  function isSkillsShPackUrl(url) {
4345
4406
  try {
4346
4407
  const parsed = new URL(url);
@@ -4544,6 +4605,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
4544
4605
  console.log();
4545
4606
  const successful = results.filter((r) => r.success);
4546
4607
  const failed = results.filter((r) => !r.success);
4608
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
4547
4609
  const skillFiles = {};
4548
4610
  for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
4549
4611
  if (await wellKnownPrivacyPromise !== true) track({
@@ -4558,7 +4620,6 @@ async function handleWellKnownSkills(source, url, options, spinner) {
4558
4620
  sourceType: "well-known"
4559
4621
  });
4560
4622
  if (successful.length > 0 && installGlobally) {
4561
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
4562
4623
  for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
4563
4624
  await addSkillToLock(skill.installName, {
4564
4625
  source: sourceIdentifier,
@@ -4571,7 +4632,6 @@ async function handleWellKnownSkills(source, url, options, spinner) {
4571
4632
  } catch {}
4572
4633
  }
4573
4634
  if (successful.length > 0 && !installGlobally) {
4574
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
4575
4635
  for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
4576
4636
  const matchingResult = successful.find((r) => r.skill === skill.installName);
4577
4637
  const installDir = matchingResult?.canonicalPath || matchingResult?.path;
@@ -4638,6 +4698,40 @@ async function handleWellKnownSkills(source, url, options, spinner) {
4638
4698
  async function runAdd(args, options = {}) {
4639
4699
  const source = args[0];
4640
4700
  let installTipShown = false;
4701
+ const jsonMode = options.json === true;
4702
+ const jsonResults = [];
4703
+ const originalStdoutWrite = process.stdout.write;
4704
+ let stdoutSuppressed = false;
4705
+ if (jsonMode) {
4706
+ process.stdout.write = process.stderr.write.bind(process.stderr);
4707
+ stdoutSuppressed = true;
4708
+ }
4709
+ const restoreStdout = () => {
4710
+ if (stdoutSuppressed) {
4711
+ process.stdout.write = originalStdoutWrite;
4712
+ stdoutSuppressed = false;
4713
+ }
4714
+ };
4715
+ let jsonEmitted = false;
4716
+ const emitJson = () => {
4717
+ if (!jsonMode || jsonEmitted) return;
4718
+ jsonEmitted = true;
4719
+ restoreStdout();
4720
+ console.log(JSON.stringify(jsonResults, null, 2));
4721
+ };
4722
+ const emitJsonAndExit = (code, errorMessage) => {
4723
+ if (jsonMode) {
4724
+ if (errorMessage !== void 0) console.error(errorMessage);
4725
+ if (code !== 0 && jsonResults.length === 0) jsonResults.push({
4726
+ status: "failed",
4727
+ error: errorMessage ?? "Installation failed"
4728
+ });
4729
+ emitJson();
4730
+ }
4731
+ process.exit(code);
4732
+ };
4733
+ const emitJsonOnExit = () => emitJson();
4734
+ if (jsonMode) process.once("exit", emitJsonOnExit);
4641
4735
  const showInstallTip = () => {
4642
4736
  if (installTipShown) return;
4643
4737
  log.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts."));
@@ -4653,8 +4747,9 @@ async function runAdd(args, options = {}) {
4653
4747
  console.log(import_picocolors.default.dim(" Example:"));
4654
4748
  console.log(` ${import_picocolors.default.cyan("npx skills add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`);
4655
4749
  console.log();
4656
- process.exit(1);
4750
+ emitJsonAndExit(1, "Missing required argument: source");
4657
4751
  }
4752
+ const explicitlySelectedAgents = new Set(options.agent?.includes("*") ? [] : options.agent ?? []);
4658
4753
  if (options.all) {
4659
4754
  options.skill = ["*"];
4660
4755
  options.agent = ["*"];
@@ -4668,13 +4763,19 @@ async function runAdd(args, options = {}) {
4668
4763
  if (mappedAgent) options.agent = ensureUniversalAgents([mappedAgent]);
4669
4764
  }
4670
4765
  }
4766
+ if (jsonMode && !options.yes) emitJsonAndExit(1, "The --json flag requires --yes (or --all) to run non-interactively.");
4767
+ if (jsonMode && options.list) emitJsonAndExit(1, "The --json flag cannot be combined with --list.");
4671
4768
  console.log();
4672
4769
  if (!agentResult.isAgent) intro(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
4673
4770
  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");
4674
4771
  else if (!process.stdin.isTTY) showInstallTip();
4675
4772
  let tempDir = null;
4676
4773
  try {
4677
- const spinner$3 = spinner();
4774
+ const spinner$3 = jsonMode ? {
4775
+ start: () => {},
4776
+ stop: () => {},
4777
+ message: () => {}
4778
+ } : spinner();
4678
4779
  spinner$3.start("Parsing source…");
4679
4780
  const parsed = parseSource(source);
4680
4781
  let directDownload = parsed.type === "download";
@@ -4688,6 +4789,7 @@ async function runAdd(args, options = {}) {
4688
4789
  return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null);
4689
4790
  })();
4690
4791
  if (parsed.type === "well-known") {
4792
+ if (jsonMode) emitJsonAndExit(1, "--json is not yet supported for well-known skill sources.");
4691
4793
  if (await handleWellKnownSkills(source, parsed.url, options, spinner$3)) return;
4692
4794
  directDownload = true;
4693
4795
  }
@@ -4703,7 +4805,7 @@ async function runAdd(args, options = {}) {
4703
4805
  if (!existsSync(parsed.localPath)) {
4704
4806
  spinner$3.stop(import_picocolors.default.red("Path not found"));
4705
4807
  outro(import_picocolors.default.red(`Local path does not exist: ${parsed.localPath}`));
4706
- process.exit(1);
4808
+ emitJsonAndExit(1, `Local path does not exist: ${parsed.localPath}`);
4707
4809
  }
4708
4810
  spinner$3.stop("Local path validated");
4709
4811
  spinner$3.start("Discovering skills…");
@@ -4722,15 +4824,18 @@ async function runAdd(args, options = {}) {
4722
4824
  fullDepth: options.fullDepth
4723
4825
  });
4724
4826
  } else if (parsed.type === "github" && !options.fullDepth) {
4827
+ let attemptedBlobInstall = false;
4725
4828
  const BLOB_ALLOWED_OWNERS = [
4726
4829
  "vercel",
4727
4830
  "vercel-labs",
4728
- "heygen-com"
4831
+ "heygen-com",
4832
+ "remotion-dev"
4729
4833
  ];
4730
4834
  const ownerRepo = getOwnerRepo(parsed);
4731
4835
  const owner = ownerRepo?.split("/")[0]?.toLowerCase();
4732
4836
  const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
4733
4837
  if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
4838
+ attemptedBlobInstall = true;
4734
4839
  spinner$3.start("Fetching skills…");
4735
4840
  blobResult = await tryBlobInstall(ownerRepo, {
4736
4841
  subpath: parsed.subpath,
@@ -4739,13 +4844,13 @@ async function runAdd(args, options = {}) {
4739
4844
  getToken: getGitHubToken,
4740
4845
  includeInternal
4741
4846
  });
4742
- if (!blobResult) spinner$3.stop(import_picocolors.default.dim("Falling back to clone…"));
4743
4847
  }
4744
4848
  if (blobResult) {
4745
4849
  skills = blobResult.skills;
4746
4850
  spinner$3.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
4747
4851
  } else {
4748
- spinner$3.start("Cloning repository…");
4852
+ if (attemptedBlobInstall) spinner$3.message("Cloning repository…");
4853
+ else spinner$3.start("Cloning repository…");
4749
4854
  tempDir = await cloneRepo(parsed.url, parsed.ref);
4750
4855
  spinner$3.stop("Repository cloned");
4751
4856
  spinner$3.start("Discovering skills…");
@@ -4768,7 +4873,7 @@ async function runAdd(args, options = {}) {
4768
4873
  spinner$3.stop(import_picocolors.default.red("No skills found"));
4769
4874
  outro(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
4770
4875
  await cleanup(tempDir);
4771
- process.exit(1);
4876
+ emitJsonAndExit(1, "No valid skills found. Skills require a SKILL.md with name and description.");
4772
4877
  }
4773
4878
  if (!blobResult) spinner$3.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
4774
4879
  if (options.list) {
@@ -4801,7 +4906,7 @@ async function runAdd(args, options = {}) {
4801
4906
  console.log();
4802
4907
  outro("Use --skill <name> to install specific skills");
4803
4908
  await cleanup(tempDir);
4804
- process.exit(0);
4909
+ emitJsonAndExit(0);
4805
4910
  }
4806
4911
  let selectedSkills;
4807
4912
  if (options.skill?.includes("*")) {
@@ -4809,12 +4914,19 @@ async function runAdd(args, options = {}) {
4809
4914
  log.info(`Installing all ${skills.length} skills`);
4810
4915
  } else if (options.skill && options.skill.length > 0) {
4811
4916
  selectedSkills = filterSkills(skills, options.skill);
4917
+ if (jsonMode) {
4918
+ for (const requested of options.skill) if (filterSkills(skills, [requested]).length === 0) jsonResults.push({
4919
+ name: requested,
4920
+ status: "skipped",
4921
+ reason: "No matching skill found in source"
4922
+ });
4923
+ }
4812
4924
  if (selectedSkills.length === 0) {
4813
4925
  log.error(`No matching skills found for: ${options.skill.join(", ")}`);
4814
4926
  log.info("Available skills:");
4815
4927
  for (const s of skills) log.message(` - ${getSkillDisplayName(s)}`);
4816
4928
  await cleanup(tempDir);
4817
- process.exit(1);
4929
+ emitJsonAndExit(1, `No matching skills found for: ${options.skill.join(", ")}`);
4818
4930
  }
4819
4931
  log.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => import_picocolors.default.cyan(getSkillDisplayName(s))).join(", ")}`);
4820
4932
  } else if (skills.length === 1) {
@@ -4870,7 +4982,7 @@ async function runAdd(args, options = {}) {
4870
4982
  log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
4871
4983
  log.info(`Valid agents: ${validAgents.join(", ")}`);
4872
4984
  await cleanup(tempDir);
4873
- process.exit(1);
4985
+ emitJsonAndExit(1, `Invalid agents: ${invalidAgents.join(", ")}`);
4874
4986
  }
4875
4987
  targetAgents = options.agent;
4876
4988
  } else {
@@ -4889,6 +5001,7 @@ async function runAdd(args, options = {}) {
4889
5001
  }
4890
5002
  if (useEve) {
4891
5003
  targetAgents = ["eve"];
5004
+ if (!options.yes) explicitlySelectedAgents.add("eve");
4892
5005
  log.info(`Installing to: ${import_picocolors.default.cyan(EVE_AGENT_LABEL)}`);
4893
5006
  } else {
4894
5007
  const selected = await selectAgentsInteractive({ global: options.global });
@@ -4897,6 +5010,7 @@ async function runAdd(args, options = {}) {
4897
5010
  exitInstallationCancelled();
4898
5011
  }
4899
5012
  targetAgents = selected;
5013
+ for (const agent of targetAgents) explicitlySelectedAgents.add(agent);
4900
5014
  }
4901
5015
  } else if (installedAgents.length === 0) if (options.yes) {
4902
5016
  targetAgents = validAgents;
@@ -4912,6 +5026,7 @@ async function runAdd(args, options = {}) {
4912
5026
  exitInstallationCancelled();
4913
5027
  }
4914
5028
  targetAgents = selected;
5029
+ for (const agent of targetAgents) explicitlySelectedAgents.add(agent);
4915
5030
  }
4916
5031
  else if (installedAgents.length === 1 || options.yes) {
4917
5032
  targetAgents = ensureUniversalAgents(installedAgents);
@@ -4926,9 +5041,13 @@ async function runAdd(args, options = {}) {
4926
5041
  exitInstallationCancelled();
4927
5042
  }
4928
5043
  targetAgents = selected;
5044
+ for (const agent of targetAgents) explicitlySelectedAgents.add(agent);
4929
5045
  }
4930
5046
  }
4931
- if (options.subagent && options.subagent.length > 0 && !targetAgents.includes("eve")) targetAgents = [...targetAgents, "eve"];
5047
+ if (options.subagent && options.subagent.length > 0) {
5048
+ explicitlySelectedAgents.add("eve");
5049
+ if (!targetAgents.includes("eve")) targetAgents = [...targetAgents, "eve"];
5050
+ }
4932
5051
  let eveSubagentTargets = [void 0];
4933
5052
  if (targetAgents.includes("eve")) {
4934
5053
  const availableSubagents = getEveSubagents(process.cwd());
@@ -5052,8 +5171,10 @@ async function runAdd(args, options = {}) {
5052
5171
  }
5053
5172
  console.log();
5054
5173
  note(summaryLines.join("\n"), "Installation Summary");
5174
+ let auditDataForJson = null;
5055
5175
  try {
5056
5176
  const auditData = await auditPromise;
5177
+ auditDataForJson = auditData;
5057
5178
  if (auditData && ownerRepoForAudit) {
5058
5179
  const securityLines = buildSecurityLines(auditData, selectedSkills.map((s) => ({
5059
5180
  slug: getSkillDisplayName(s),
@@ -5082,12 +5203,14 @@ async function runAdd(args, options = {}) {
5082
5203
  }, agent, {
5083
5204
  global: installGlobally,
5084
5205
  mode: installMode,
5085
- eveSubagent: subagent
5206
+ eveSubagent: subagent,
5207
+ createMissingAgentRoot: explicitlySelectedAgents.has(agent)
5086
5208
  });
5087
5209
  } else result = await installSkillForAgent(skill, agent, {
5088
5210
  global: installGlobally,
5089
5211
  mode: installMode,
5090
- eveSubagent: subagent
5212
+ eveSubagent: subagent,
5213
+ createMissingAgentRoot: explicitlySelectedAgents.has(agent)
5091
5214
  });
5092
5215
  results.push({
5093
5216
  skill: getSkillDisplayName(skill),
@@ -5100,6 +5223,7 @@ async function runAdd(args, options = {}) {
5100
5223
  console.log();
5101
5224
  const successful = results.filter((r) => r.success);
5102
5225
  const failed = results.filter((r) => !r.success);
5226
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
5103
5227
  const skillFiles = {};
5104
5228
  for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
5105
5229
  else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
@@ -5127,8 +5251,16 @@ async function runAdd(args, options = {}) {
5127
5251
  skillFiles: JSON.stringify(skillFiles),
5128
5252
  metadata: options.metadata
5129
5253
  });
5254
+ const installedSkillHashes = /* @__PURE__ */ new Map();
5255
+ if (successful.length > 0 && (jsonMode || !installGlobally)) for (const skill of selectedSkills) {
5256
+ const skillDisplayName = getSkillDisplayName(skill);
5257
+ if (!successfulSkillNames.has(skillDisplayName)) continue;
5258
+ try {
5259
+ const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : await computeSkillFolderHash(skill.path);
5260
+ installedSkillHashes.set(skillDisplayName, computedHash);
5261
+ } catch {}
5262
+ }
5130
5263
  if (successful.length > 0 && installGlobally && normalizedSource) {
5131
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
5132
5264
  let cachedTree;
5133
5265
  if (parsed.type === "github" && !blobResult) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
5134
5266
  for (const skill of selectedSkills) {
@@ -5159,13 +5291,13 @@ async function runAdd(args, options = {}) {
5159
5291
  }
5160
5292
  }
5161
5293
  if (successful.length > 0 && !installGlobally && !directDownload) {
5162
- const successfulSkillNames = new Set(successful.map((r) => r.skill));
5163
5294
  const eveSubagents = targetAgents.includes("eve") ? eveSubagentTargets.map((s) => s ?? "") : void 0;
5164
5295
  const recordSubagents = eveSubagents && (eveSubagents.length > 1 || eveSubagents.some((s) => s !== ""));
5165
5296
  for (const skill of selectedSkills) {
5166
5297
  const skillDisplayName = getSkillDisplayName(skill);
5167
5298
  if (successfulSkillNames.has(skillDisplayName)) try {
5168
- const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : await computeSkillFolderHash(skill.path);
5299
+ const computedHash = installedSkillHashes.get(skillDisplayName);
5300
+ if (computedHash === void 0) continue;
5169
5301
  const skillPathValue = skillFiles[skill.name];
5170
5302
  await addSkillToLocalLock(skill.name, {
5171
5303
  source: lockSource || parsed.url,
@@ -5179,6 +5311,37 @@ async function runAdd(args, options = {}) {
5179
5311
  } catch {}
5180
5312
  }
5181
5313
  }
5314
+ if (jsonMode) {
5315
+ const jsonSource = normalizedSource ?? (parsed.type === "local" ? parsed.localPath : parsed.url);
5316
+ for (const skill of selectedSkills) {
5317
+ const name = getSkillDisplayName(skill);
5318
+ const skillResults = results.filter((r) => r.skill === name);
5319
+ const failures = skillResults.filter((r) => !r.success);
5320
+ if (failures.length > 0) {
5321
+ jsonResults.push({
5322
+ name,
5323
+ status: "failed",
5324
+ error: failures[0].error ?? "Installation failed"
5325
+ });
5326
+ continue;
5327
+ }
5328
+ jsonResults.push({
5329
+ name,
5330
+ status: "installed",
5331
+ source: jsonSource,
5332
+ ref: parsed.ref ?? null,
5333
+ hash: installedSkillHashes.get(name) ?? null,
5334
+ path: skillResults[0]?.canonicalPath ?? skillResults[0]?.path,
5335
+ scope: installGlobally ? "global" : "project",
5336
+ agents: skillResults.filter((r) => !r.skipped).map((r) => r.agent),
5337
+ mode: skillResults[0]?.mode ?? installMode,
5338
+ security: buildJsonSecurity(auditDataForJson, name, ownerRepoForAudit)
5339
+ });
5340
+ }
5341
+ emitJson();
5342
+ if (failed.length > 0 || jsonResults.some((result) => result.status === "skipped")) process.exitCode = 1;
5343
+ return;
5344
+ }
5182
5345
  if (successful.length > 0) {
5183
5346
  const bySkill = /* @__PURE__ */ new Map();
5184
5347
  const groupedResults = {};
@@ -5256,8 +5419,10 @@ async function runAdd(args, options = {}) {
5256
5419
  } else log.error(error instanceof Error ? error.message : "Unknown error occurred");
5257
5420
  showInstallTip();
5258
5421
  outro(import_picocolors.default.red("Installation failed"));
5259
- process.exit(1);
5422
+ emitJsonAndExit(1, error instanceof GitCloneError ? `Failed to clone repository\n${error.message}` : error instanceof Error ? error.message : "Unknown error occurred");
5260
5423
  } finally {
5424
+ if (jsonMode) process.removeListener("exit", emitJsonOnExit);
5425
+ restoreStdout();
5261
5426
  await cleanup(tempDir);
5262
5427
  }
5263
5428
  }
@@ -5345,6 +5510,7 @@ function parseAddOptions(args) {
5345
5510
  errors.push("--metadata must be valid JSON");
5346
5511
  }
5347
5512
  } else if (arg === "--full-depth") options.fullDepth = true;
5513
+ else if (arg === "--json") options.json = true;
5348
5514
  else if (arg === "--copy") options.copy = true;
5349
5515
  else if (arg === "--subagent") {
5350
5516
  options.subagent = options.subagent || [];
@@ -7179,7 +7345,8 @@ async function runUpdate(args = []) {
7179
7345
  const BLOB_ALLOWED_OWNERS = [
7180
7346
  "vercel",
7181
7347
  "vercel-labs",
7182
- "heygen-com"
7348
+ "heygen-com",
7349
+ "remotion-dev"
7183
7350
  ];
7184
7351
  const EXCLUDE_FILES = /* @__PURE__ */ new Set(["metadata.json"]);
7185
7352
  const EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -7661,6 +7828,7 @@ ${BOLD}Add Options:${RESET}
7661
7828
  --subagent <names> Install to Eve subagents (use 'root' for the root agent)
7662
7829
  --all Shorthand for --skill '*' --agent '*' -y
7663
7830
  --full-depth Search all subdirectories even when a root SKILL.md exists
7831
+ --json Output results as JSON (machine-readable, no ANSI codes)
7664
7832
 
7665
7833
  ${BOLD}Use Options:${RESET}
7666
7834
  -s, --skill <skill> Specify the skill to use
@@ -7694,6 +7862,7 @@ ${BOLD}Examples:${RESET}
7694
7862
  ${DIM}$${RESET} skills add vercel-labs/agent-skills -g
7695
7863
  ${DIM}$${RESET} skills add vercel-labs/agent-skills --agent claude-code cursor
7696
7864
  ${DIM}$${RESET} skills add vercel-labs/agent-skills --skill pr-review commit
7865
+ ${DIM}$${RESET} skills add vercel-labs/agent-skills --json -y ${DIM}# JSON output${RESET}
7697
7866
  ${DIM}$${RESET} skills remove ${DIM}# interactive remove${RESET}
7698
7867
  ${DIM}$${RESET} skills remove web-design ${DIM}# remove by name${RESET}
7699
7868
  ${DIM}$${RESET} skills rm --global frontend-design
@@ -7828,10 +7997,11 @@ async function main() {
7828
7997
  case "install":
7829
7998
  case "a":
7830
7999
  case "add": {
7831
- if (!inAgent) showLogo();
7832
8000
  const { source: addSource, options: addOpts, errors } = parseAddOptions(restArgs);
8001
+ if (!inAgent && !addOpts.json) showLogo();
7833
8002
  if (errors.length > 0) {
7834
8003
  for (const error of errors) console.error(`Error: ${error}`);
8004
+ if (addOpts.json) console.log("[]");
7835
8005
  process.exitCode = 1;
7836
8006
  break;
7837
8007
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.5.25",
3
+ "version": "1.6.0",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {