skills 1.1.3 → 1.1.5

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 +26 -7
  2. package/dist/cli.js +713 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,6 +6,7 @@ The CLI for the open agent skills ecosystem.
6
6
  Supports **Opencode**, **Claude Code**, **Codex**, **Cursor**, and [25 more](#available-agents).
7
7
  <!-- agent-list:end -->
8
8
 
9
+ <!-- agent-list:end -->
9
10
 
10
11
  <!-- agent-list:end -->
11
12
 
@@ -225,6 +226,19 @@ Describe the scenarios where this skill should be used.
225
226
  - `name`: Unique identifier (lowercase, hyphens allowed)
226
227
  - `description`: Brief explanation of what the skill does
227
228
 
229
+ ### Optional Fields
230
+
231
+ - `metadata.internal`: Set to `true` to hide the skill from normal discovery. Internal skills are only visible and installable when `INSTALL_INTERNAL_SKILLS=1` is set. Useful for work-in-progress skills or skills meant only for internal tooling.
232
+
233
+ ```markdown
234
+ ---
235
+ name: my-internal-skill
236
+ description: An internal skill not shown by default
237
+ metadata:
238
+ internal: true
239
+ ---
240
+ ```
241
+
228
242
  ### Skill Discovery
229
243
 
230
244
  The CLI searches for skills in these locations within a repository:
@@ -295,18 +309,23 @@ Ensure the repository contains valid `SKILL.md` files with both `name` and `desc
295
309
 
296
310
  Ensure you have write access to the target directory.
297
311
 
298
- ## Telemetry
299
-
300
- This CLI collects anonymous usage data to help improve the tool. No personal information is collected.
312
+ ## Environment Variables
301
313
 
302
- To disable telemetry:
314
+ | Variable | Description |
315
+ | ------------------------- | -------------------------------------------------------------------------- |
316
+ | `INSTALL_INTERNAL_SKILLS` | Set to `1` or `true` to show and install skills marked as `internal: true` |
317
+ | `DISABLE_TELEMETRY` | Set to disable anonymous usage telemetry |
318
+ | `DO_NOT_TRACK` | Alternative way to disable telemetry |
303
319
 
304
320
  ```bash
305
- DISABLE_TELEMETRY=1 npx skills add vercel-labs/agent-skills
306
- # or
307
- DO_NOT_TRACK=1 npx skills add vercel-labs/agent-skills
321
+ # Install internal skills
322
+ INSTALL_INTERNAL_SKILLS=1 npx skills add vercel-labs/agent-skills --list
308
323
  ```
309
324
 
325
+ ## Telemetry
326
+
327
+ This CLI collects anonymous usage data to help improve the tool. No personal information is collected.
328
+
310
329
  Telemetry is automatically disabled in CI environments.
311
330
 
312
331
  ## Related Links
package/dist/cli.js CHANGED
@@ -129,11 +129,43 @@ function parseSource(input) {
129
129
  subpath
130
130
  };
131
131
  }
132
+ if (isWellKnownUrl(input)) {
133
+ return {
134
+ type: "well-known",
135
+ url: input
136
+ };
137
+ }
132
138
  return {
133
139
  type: "git",
134
140
  url: input
135
141
  };
136
142
  }
143
+ function isWellKnownUrl(input) {
144
+ if (!input.startsWith("http://") && !input.startsWith("https://")) {
145
+ return false;
146
+ }
147
+ try {
148
+ const parsed = new URL(input);
149
+ const excludedHosts = [
150
+ "github.com",
151
+ "gitlab.com",
152
+ "huggingface.co",
153
+ "raw.githubusercontent.com"
154
+ ];
155
+ if (excludedHosts.includes(parsed.hostname)) {
156
+ return false;
157
+ }
158
+ if (input.toLowerCase().endsWith("/skill.md")) {
159
+ return false;
160
+ }
161
+ if (input.endsWith(".git")) {
162
+ return false;
163
+ }
164
+ return true;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
137
169
 
138
170
  // src/git.ts
139
171
  import simpleGit from "simple-git";
@@ -202,6 +234,10 @@ import { readdir, readFile, stat } from "fs/promises";
202
234
  import { join as join2, basename, dirname } from "path";
203
235
  import matter from "gray-matter";
204
236
  var SKIP_DIRS = ["node_modules", ".git", "dist", "build", "__pycache__"];
237
+ function shouldInstallInternalSkills() {
238
+ const envValue = process.env.INSTALL_INTERNAL_SKILLS;
239
+ return envValue === "1" || envValue === "true";
240
+ }
205
241
  async function hasSkillMd(dir) {
206
242
  try {
207
243
  const skillPath = join2(dir, "SKILL.md");
@@ -218,6 +254,10 @@ async function parseSkillMd(skillMdPath) {
218
254
  if (!data.name || !data.description) {
219
255
  return null;
220
256
  }
257
+ const isInternal = data.metadata?.internal === true;
258
+ if (isInternal && !shouldInstallInternalSkills()) {
259
+ return null;
260
+ }
221
261
  return {
222
262
  name: data.name,
223
263
  description: data.description,
@@ -881,6 +921,86 @@ async function installRemoteSkillForAgent(skill, agentType, options = {}) {
881
921
  };
882
922
  }
883
923
  }
924
+ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
925
+ const agent = agents[agentType];
926
+ const isGlobal = options.global ?? false;
927
+ const cwd = options.cwd || process.cwd();
928
+ const installMode = options.mode ?? "symlink";
929
+ const skillName = sanitizeName(skill.installName);
930
+ const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
931
+ const canonicalDir = join4(canonicalBase, skillName);
932
+ const agentBase = isGlobal ? agent.globalSkillsDir : join4(cwd, agent.skillsDir);
933
+ const agentDir = join4(agentBase, skillName);
934
+ if (!isPathSafe(canonicalBase, canonicalDir)) {
935
+ return {
936
+ success: false,
937
+ path: agentDir,
938
+ mode: installMode,
939
+ error: "Invalid skill name: potential path traversal detected"
940
+ };
941
+ }
942
+ if (!isPathSafe(agentBase, agentDir)) {
943
+ return {
944
+ success: false,
945
+ path: agentDir,
946
+ mode: installMode,
947
+ error: "Invalid skill name: potential path traversal detected"
948
+ };
949
+ }
950
+ async function writeSkillFiles(targetDir) {
951
+ await mkdir(targetDir, { recursive: true });
952
+ for (const [filePath, content] of skill.files) {
953
+ const fullPath = join4(targetDir, filePath);
954
+ if (!isPathSafe(targetDir, fullPath)) {
955
+ continue;
956
+ }
957
+ const parentDir = dirname2(fullPath);
958
+ if (parentDir !== targetDir) {
959
+ await mkdir(parentDir, { recursive: true });
960
+ }
961
+ await writeFile(fullPath, content, "utf-8");
962
+ }
963
+ }
964
+ try {
965
+ if (installMode === "copy") {
966
+ await writeSkillFiles(agentDir);
967
+ return {
968
+ success: true,
969
+ path: agentDir,
970
+ mode: "copy"
971
+ };
972
+ }
973
+ await writeSkillFiles(canonicalDir);
974
+ const symlinkCreated = await createSymlink(canonicalDir, agentDir);
975
+ if (!symlinkCreated) {
976
+ try {
977
+ await rm2(agentDir, { recursive: true, force: true });
978
+ } catch {
979
+ }
980
+ await writeSkillFiles(agentDir);
981
+ return {
982
+ success: true,
983
+ path: agentDir,
984
+ canonicalPath: canonicalDir,
985
+ mode: "symlink",
986
+ symlinkFailed: true
987
+ };
988
+ }
989
+ return {
990
+ success: true,
991
+ path: agentDir,
992
+ canonicalPath: canonicalDir,
993
+ mode: "symlink"
994
+ };
995
+ } catch (error) {
996
+ return {
997
+ success: false,
998
+ path: agentDir,
999
+ mode: installMode,
1000
+ error: error instanceof Error ? error.message : "Unknown error"
1001
+ };
1002
+ }
1003
+ }
884
1004
 
885
1005
  // src/telemetry.ts
886
1006
  var TELEMETRY_URL = "https://add-skill.vercel.sh/t";
@@ -1082,12 +1202,268 @@ var HuggingFaceProvider = class {
1082
1202
  };
1083
1203
  var huggingFaceProvider = new HuggingFaceProvider();
1084
1204
 
1205
+ // src/providers/wellknown.ts
1206
+ import matter4 from "gray-matter";
1207
+ var WellKnownProvider = class {
1208
+ id = "well-known";
1209
+ displayName = "Well-Known Skills";
1210
+ WELL_KNOWN_PATH = ".well-known/skills";
1211
+ INDEX_FILE = "index.json";
1212
+ /**
1213
+ * Check if a URL could be a well-known skills endpoint.
1214
+ * This is a fallback provider - it matches any HTTP(S) URL that is not
1215
+ * a recognized pattern (GitHub, GitLab, owner/repo shorthand, etc.)
1216
+ */
1217
+ match(url) {
1218
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
1219
+ return { matches: false };
1220
+ }
1221
+ try {
1222
+ const parsed = new URL(url);
1223
+ const excludedHosts = ["github.com", "gitlab.com", "huggingface.co"];
1224
+ if (excludedHosts.includes(parsed.hostname)) {
1225
+ return { matches: false };
1226
+ }
1227
+ return {
1228
+ matches: true,
1229
+ sourceIdentifier: `wellknown/${parsed.hostname}`
1230
+ };
1231
+ } catch {
1232
+ return { matches: false };
1233
+ }
1234
+ }
1235
+ /**
1236
+ * Fetch the skills index from a well-known endpoint.
1237
+ * Tries both the path-relative .well-known and the root .well-known.
1238
+ */
1239
+ async fetchIndex(baseUrl) {
1240
+ try {
1241
+ const parsed = new URL(baseUrl);
1242
+ const basePath = parsed.pathname.replace(/\/$/, "");
1243
+ const urlsToTry = [
1244
+ // Path-relative: https://example.com/docs/.well-known/skills/index.json
1245
+ {
1246
+ indexUrl: `${parsed.protocol}//${parsed.hostname}${basePath}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`,
1247
+ baseUrl: `${parsed.protocol}//${parsed.hostname}${basePath}`
1248
+ }
1249
+ ];
1250
+ if (basePath && basePath !== "") {
1251
+ urlsToTry.push({
1252
+ indexUrl: `${parsed.protocol}//${parsed.hostname}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`,
1253
+ baseUrl: `${parsed.protocol}//${parsed.hostname}`
1254
+ });
1255
+ }
1256
+ for (const { indexUrl, baseUrl: resolvedBase } of urlsToTry) {
1257
+ try {
1258
+ const response = await fetch(indexUrl);
1259
+ if (!response.ok) {
1260
+ continue;
1261
+ }
1262
+ const index = await response.json();
1263
+ if (!index.skills || !Array.isArray(index.skills)) {
1264
+ continue;
1265
+ }
1266
+ let allValid = true;
1267
+ for (const entry of index.skills) {
1268
+ if (!this.isValidSkillEntry(entry)) {
1269
+ allValid = false;
1270
+ break;
1271
+ }
1272
+ }
1273
+ if (allValid) {
1274
+ return { index, resolvedBaseUrl: resolvedBase };
1275
+ }
1276
+ } catch {
1277
+ continue;
1278
+ }
1279
+ }
1280
+ return null;
1281
+ } catch {
1282
+ return null;
1283
+ }
1284
+ }
1285
+ /**
1286
+ * Validate a skill entry from the index.
1287
+ */
1288
+ isValidSkillEntry(entry) {
1289
+ if (!entry || typeof entry !== "object") return false;
1290
+ const e = entry;
1291
+ if (typeof e.name !== "string" || !e.name) return false;
1292
+ if (typeof e.description !== "string" || !e.description) return false;
1293
+ if (!Array.isArray(e.files) || e.files.length === 0) return false;
1294
+ const nameRegex = /^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/;
1295
+ if (!nameRegex.test(e.name) && e.name.length > 1) {
1296
+ if (e.name.length === 1 && !/^[a-z0-9]$/.test(e.name)) {
1297
+ return false;
1298
+ }
1299
+ }
1300
+ for (const file of e.files) {
1301
+ if (typeof file !== "string") return false;
1302
+ if (file.startsWith("/") || file.includes("..")) return false;
1303
+ }
1304
+ const hasSkillMd2 = e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md");
1305
+ if (!hasSkillMd2) return false;
1306
+ return true;
1307
+ }
1308
+ /**
1309
+ * Fetch a single skill and all its files from a well-known endpoint.
1310
+ */
1311
+ async fetchSkill(url) {
1312
+ try {
1313
+ const parsed = new URL(url);
1314
+ const result = await this.fetchIndex(url);
1315
+ if (!result) {
1316
+ return null;
1317
+ }
1318
+ const { index, resolvedBaseUrl } = result;
1319
+ let skillName = null;
1320
+ const pathMatch = parsed.pathname.match(/\/.well-known\/skills\/([^/]+)\/?$/);
1321
+ if (pathMatch && pathMatch[1] && pathMatch[1] !== "index.json") {
1322
+ skillName = pathMatch[1];
1323
+ } else if (index.skills.length === 1) {
1324
+ skillName = index.skills[0].name;
1325
+ }
1326
+ if (!skillName) {
1327
+ return null;
1328
+ }
1329
+ const skillEntry = index.skills.find((s) => s.name === skillName);
1330
+ if (!skillEntry) {
1331
+ return null;
1332
+ }
1333
+ return this.fetchSkillByEntry(resolvedBaseUrl, skillEntry);
1334
+ } catch {
1335
+ return null;
1336
+ }
1337
+ }
1338
+ /**
1339
+ * Fetch a skill by its index entry.
1340
+ * @param baseUrl - The base URL (e.g., https://example.com or https://example.com/docs)
1341
+ * @param entry - The skill entry from index.json
1342
+ */
1343
+ async fetchSkillByEntry(baseUrl, entry) {
1344
+ try {
1345
+ const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${this.WELL_KNOWN_PATH}/${entry.name}`;
1346
+ const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
1347
+ const response = await fetch(skillMdUrl);
1348
+ if (!response.ok) {
1349
+ return null;
1350
+ }
1351
+ const content = await response.text();
1352
+ const { data } = matter4(content);
1353
+ if (!data.name || !data.description) {
1354
+ return null;
1355
+ }
1356
+ const files = /* @__PURE__ */ new Map();
1357
+ files.set("SKILL.md", content);
1358
+ const otherFiles = entry.files.filter((f) => f.toLowerCase() !== "skill.md");
1359
+ const filePromises = otherFiles.map(async (filePath) => {
1360
+ try {
1361
+ const fileUrl = `${skillBaseUrl}/${filePath}`;
1362
+ const fileResponse = await fetch(fileUrl);
1363
+ if (fileResponse.ok) {
1364
+ const fileContent = await fileResponse.text();
1365
+ return { path: filePath, content: fileContent };
1366
+ }
1367
+ } catch {
1368
+ }
1369
+ return null;
1370
+ });
1371
+ const fileResults = await Promise.all(filePromises);
1372
+ for (const result of fileResults) {
1373
+ if (result) {
1374
+ files.set(result.path, result.content);
1375
+ }
1376
+ }
1377
+ return {
1378
+ name: data.name,
1379
+ description: data.description,
1380
+ content,
1381
+ installName: entry.name,
1382
+ sourceUrl: skillMdUrl,
1383
+ metadata: data.metadata,
1384
+ files,
1385
+ indexEntry: entry
1386
+ };
1387
+ } catch {
1388
+ return null;
1389
+ }
1390
+ }
1391
+ /**
1392
+ * Fetch all skills from a well-known endpoint.
1393
+ */
1394
+ async fetchAllSkills(url) {
1395
+ try {
1396
+ const result = await this.fetchIndex(url);
1397
+ if (!result) {
1398
+ return [];
1399
+ }
1400
+ const { index, resolvedBaseUrl } = result;
1401
+ const skillPromises = index.skills.map(
1402
+ (entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry)
1403
+ );
1404
+ const results = await Promise.all(skillPromises);
1405
+ return results.filter((s) => s !== null);
1406
+ } catch {
1407
+ return [];
1408
+ }
1409
+ }
1410
+ /**
1411
+ * Convert a user-facing URL to a skill URL.
1412
+ * For well-known, this extracts the base domain and constructs the proper path.
1413
+ */
1414
+ toRawUrl(url) {
1415
+ try {
1416
+ const parsed = new URL(url);
1417
+ if (url.toLowerCase().endsWith("/skill.md")) {
1418
+ return url;
1419
+ }
1420
+ const pathMatch = parsed.pathname.match(/\/.well-known\/skills\/([^/]+)\/?$/);
1421
+ if (pathMatch && pathMatch[1]) {
1422
+ const basePath2 = parsed.pathname.replace(/\/.well-known\/skills\/.*$/, "");
1423
+ return `${parsed.protocol}//${parsed.hostname}${basePath2}/${this.WELL_KNOWN_PATH}/${pathMatch[1]}/SKILL.md`;
1424
+ }
1425
+ const basePath = parsed.pathname.replace(/\/$/, "");
1426
+ return `${parsed.protocol}//${parsed.hostname}${basePath}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`;
1427
+ } catch {
1428
+ return url;
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Get the source identifier for telemetry/storage.
1433
+ * Returns the domain in owner/repo format: second-level-domain/top-level-domain.
1434
+ * e.g., "mintlify.com" → "mintlify/com", "lovable.dev" → "lovable/dev"
1435
+ * This matches the owner/repo pattern used by GitHub sources for consistency in the leaderboard.
1436
+ */
1437
+ getSourceIdentifier(url) {
1438
+ try {
1439
+ const parsed = new URL(url);
1440
+ const hostParts = parsed.hostname.split(".");
1441
+ if (hostParts.length >= 2) {
1442
+ const tld = hostParts[hostParts.length - 1];
1443
+ const sld = hostParts[hostParts.length - 2];
1444
+ return `${sld}/${tld}`;
1445
+ }
1446
+ return parsed.hostname.replace(".", "/");
1447
+ } catch {
1448
+ return "unknown/unknown";
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Check if a URL has a well-known skills index.
1453
+ */
1454
+ async hasSkillsIndex(url) {
1455
+ const result = await this.fetchIndex(url);
1456
+ return result !== null;
1457
+ }
1458
+ };
1459
+ var wellKnownProvider = new WellKnownProvider();
1460
+
1085
1461
  // src/providers/index.ts
1086
1462
  registerProvider(mintlifyProvider);
1087
1463
  registerProvider(huggingFaceProvider);
1088
1464
 
1089
1465
  // src/mintlify.ts
1090
- import matter4 from "gray-matter";
1466
+ import matter5 from "gray-matter";
1091
1467
  async function fetchMintlifySkill(url) {
1092
1468
  try {
1093
1469
  const response = await fetch(url);
@@ -1095,7 +1471,7 @@ async function fetchMintlifySkill(url) {
1095
1471
  return null;
1096
1472
  }
1097
1473
  const content = await response.text();
1098
- const { data } = matter4(content);
1474
+ const { data } = matter5(content);
1099
1475
  const mintlifySite = data.metadata?.["mintlify-proj"];
1100
1476
  if (!mintlifySite) {
1101
1477
  return null;
@@ -1219,7 +1595,7 @@ async function dismissPrompt(promptKey) {
1219
1595
  // package.json
1220
1596
  var package_default = {
1221
1597
  name: "skills",
1222
- version: "1.1.3",
1598
+ version: "1.1.5",
1223
1599
  description: "The open agent skills ecosystem",
1224
1600
  type: "module",
1225
1601
  bin: {
@@ -1651,6 +2027,333 @@ async function handleRemoteSkill(source, url, options, spinner2) {
1651
2027
  p.outro(chalk.green("Done!"));
1652
2028
  await promptForFindSkills();
1653
2029
  }
2030
+ async function handleWellKnownSkills(source, url, options, spinner2) {
2031
+ spinner2.start("Discovering skills from well-known endpoint...");
2032
+ const skills = await wellKnownProvider.fetchAllSkills(url);
2033
+ if (skills.length === 0) {
2034
+ spinner2.stop(chalk.red("No skills found"));
2035
+ p.outro(
2036
+ chalk.red(
2037
+ "No skills found at this URL. Make sure the server has a /.well-known/skills/index.json file."
2038
+ )
2039
+ );
2040
+ process.exit(1);
2041
+ }
2042
+ spinner2.stop(`Found ${chalk.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
2043
+ for (const skill of skills) {
2044
+ p.log.info(`Skill: ${chalk.cyan(skill.installName)}`);
2045
+ p.log.message(chalk.dim(skill.description));
2046
+ if (skill.files.size > 1) {
2047
+ p.log.message(chalk.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
2048
+ }
2049
+ }
2050
+ if (options.list) {
2051
+ console.log();
2052
+ p.log.step(chalk.bold("Available Skills"));
2053
+ for (const skill of skills) {
2054
+ p.log.message(` ${chalk.cyan(skill.installName)}`);
2055
+ p.log.message(` ${chalk.dim(skill.description)}`);
2056
+ if (skill.files.size > 1) {
2057
+ p.log.message(` ${chalk.dim(`Files: ${skill.files.size}`)}`);
2058
+ }
2059
+ }
2060
+ console.log();
2061
+ p.outro("Run without --list to install");
2062
+ process.exit(0);
2063
+ }
2064
+ let selectedSkills;
2065
+ if (options.skill && options.skill.length > 0) {
2066
+ selectedSkills = skills.filter(
2067
+ (s) => options.skill.some(
2068
+ (name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase()
2069
+ )
2070
+ );
2071
+ if (selectedSkills.length === 0) {
2072
+ p.log.error(`No matching skills found for: ${options.skill.join(", ")}`);
2073
+ p.log.info("Available skills:");
2074
+ for (const s of skills) {
2075
+ p.log.message(` - ${s.installName}`);
2076
+ }
2077
+ process.exit(1);
2078
+ }
2079
+ p.log.info(
2080
+ `Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => chalk.cyan(s.installName)).join(", ")}`
2081
+ );
2082
+ } else if (skills.length === 1) {
2083
+ selectedSkills = skills;
2084
+ const firstSkill = skills[0];
2085
+ p.log.info(`Skill: ${chalk.cyan(firstSkill.installName)}`);
2086
+ } else if (options.yes) {
2087
+ selectedSkills = skills;
2088
+ p.log.info(`Installing all ${skills.length} skills`);
2089
+ } else {
2090
+ const skillChoices = skills.map((s) => ({
2091
+ value: s,
2092
+ label: s.installName,
2093
+ hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
2094
+ }));
2095
+ const selected = await multiselect2({
2096
+ message: "Select skills to install",
2097
+ options: skillChoices,
2098
+ required: true
2099
+ });
2100
+ if (p.isCancel(selected)) {
2101
+ p.cancel("Installation cancelled");
2102
+ process.exit(0);
2103
+ }
2104
+ selectedSkills = selected;
2105
+ }
2106
+ let targetAgents;
2107
+ const validAgents = Object.keys(agents);
2108
+ if (options.agent && options.agent.length > 0) {
2109
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
2110
+ if (invalidAgents.length > 0) {
2111
+ p.log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
2112
+ p.log.info(`Valid agents: ${validAgents.join(", ")}`);
2113
+ process.exit(1);
2114
+ }
2115
+ targetAgents = options.agent;
2116
+ } else if (options.all) {
2117
+ targetAgents = validAgents;
2118
+ p.log.info(`Installing to all ${targetAgents.length} agents`);
2119
+ } else {
2120
+ spinner2.start("Detecting installed agents...");
2121
+ const installedAgents = await detectInstalledAgents();
2122
+ spinner2.stop(
2123
+ `Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? "s" : ""}`
2124
+ );
2125
+ if (installedAgents.length === 0) {
2126
+ if (options.yes) {
2127
+ targetAgents = validAgents;
2128
+ p.log.info("Installing to all agents (none detected)");
2129
+ } else {
2130
+ p.log.warn("No coding agents detected. You can still install skills.");
2131
+ const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
2132
+ value: key,
2133
+ label: config.displayName
2134
+ }));
2135
+ const selected = await multiselect2({
2136
+ message: "Select agents to install skills to",
2137
+ options: allAgentChoices,
2138
+ required: true,
2139
+ initialValues: Object.keys(agents)
2140
+ });
2141
+ if (p.isCancel(selected)) {
2142
+ p.cancel("Installation cancelled");
2143
+ process.exit(0);
2144
+ }
2145
+ targetAgents = selected;
2146
+ }
2147
+ } else if (installedAgents.length === 1 || options.yes) {
2148
+ targetAgents = installedAgents;
2149
+ if (installedAgents.length === 1) {
2150
+ const firstAgent = installedAgents[0];
2151
+ p.log.info(`Installing to: ${chalk.cyan(agents[firstAgent].displayName)}`);
2152
+ } else {
2153
+ p.log.info(
2154
+ `Installing to: ${installedAgents.map((a) => chalk.cyan(agents[a].displayName)).join(", ")}`
2155
+ );
2156
+ }
2157
+ } else {
2158
+ const selected = await selectAgentsInteractive(installedAgents, { global: options.global });
2159
+ if (p.isCancel(selected)) {
2160
+ p.cancel("Installation cancelled");
2161
+ process.exit(0);
2162
+ }
2163
+ targetAgents = selected;
2164
+ }
2165
+ }
2166
+ let installGlobally = options.global ?? false;
2167
+ if (options.global === void 0 && !options.yes) {
2168
+ const scope = await p.select({
2169
+ message: "Installation scope",
2170
+ options: [
2171
+ {
2172
+ value: false,
2173
+ label: "Project",
2174
+ hint: "Install in current directory (committed with your project)"
2175
+ },
2176
+ {
2177
+ value: true,
2178
+ label: "Global",
2179
+ hint: "Install in home directory (available across all projects)"
2180
+ }
2181
+ ]
2182
+ });
2183
+ if (p.isCancel(scope)) {
2184
+ p.cancel("Installation cancelled");
2185
+ process.exit(0);
2186
+ }
2187
+ installGlobally = scope;
2188
+ }
2189
+ let installMode = "symlink";
2190
+ if (!options.yes) {
2191
+ const modeChoice = await p.select({
2192
+ message: "Installation method",
2193
+ options: [
2194
+ {
2195
+ value: "symlink",
2196
+ label: "Symlink (Recommended)",
2197
+ hint: "Single source of truth, easy updates"
2198
+ },
2199
+ { value: "copy", label: "Copy to all agents", hint: "Independent copies for each agent" }
2200
+ ]
2201
+ });
2202
+ if (p.isCancel(modeChoice)) {
2203
+ p.cancel("Installation cancelled");
2204
+ process.exit(0);
2205
+ }
2206
+ installMode = modeChoice;
2207
+ }
2208
+ const cwd = process.cwd();
2209
+ const summaryLines = [];
2210
+ const agentNames = targetAgents.map((a) => agents[a].displayName);
2211
+ const overwriteStatus = /* @__PURE__ */ new Map();
2212
+ for (const skill of selectedSkills) {
2213
+ const agentStatus = /* @__PURE__ */ new Map();
2214
+ for (const agent of targetAgents) {
2215
+ agentStatus.set(
2216
+ agent,
2217
+ await isSkillInstalled(skill.installName, agent, { global: installGlobally })
2218
+ );
2219
+ }
2220
+ overwriteStatus.set(skill.installName, agentStatus);
2221
+ }
2222
+ for (const skill of selectedSkills) {
2223
+ if (summaryLines.length > 0) summaryLines.push("");
2224
+ if (installMode === "symlink") {
2225
+ const canonicalPath = getCanonicalPath(skill.installName, { global: installGlobally });
2226
+ const shortCanonical = shortenPath(canonicalPath, cwd);
2227
+ summaryLines.push(`${chalk.cyan(shortCanonical)}`);
2228
+ summaryLines.push(` ${chalk.dim("symlink \u2192")} ${formatList(agentNames)}`);
2229
+ if (skill.files.size > 1) {
2230
+ summaryLines.push(` ${chalk.dim("files:")} ${skill.files.size}`);
2231
+ }
2232
+ } else {
2233
+ summaryLines.push(`${chalk.cyan(skill.installName)}`);
2234
+ summaryLines.push(` ${chalk.dim("copy \u2192")} ${formatList(agentNames)}`);
2235
+ }
2236
+ const skillOverwrites = overwriteStatus.get(skill.installName);
2237
+ const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
2238
+ if (overwriteAgents.length > 0) {
2239
+ summaryLines.push(` ${chalk.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
2240
+ }
2241
+ }
2242
+ console.log();
2243
+ p.note(summaryLines.join("\n"), "Installation Summary");
2244
+ if (!options.yes) {
2245
+ const confirmed = await p.confirm({ message: "Proceed with installation?" });
2246
+ if (p.isCancel(confirmed) || !confirmed) {
2247
+ p.cancel("Installation cancelled");
2248
+ process.exit(0);
2249
+ }
2250
+ }
2251
+ spinner2.start("Installing skills...");
2252
+ const results = [];
2253
+ for (const skill of selectedSkills) {
2254
+ for (const agent of targetAgents) {
2255
+ const result = await installWellKnownSkillForAgent(skill, agent, {
2256
+ global: installGlobally,
2257
+ mode: installMode
2258
+ });
2259
+ results.push({
2260
+ skill: skill.installName,
2261
+ agent: agents[agent].displayName,
2262
+ ...result
2263
+ });
2264
+ }
2265
+ }
2266
+ spinner2.stop("Installation complete");
2267
+ console.log();
2268
+ const successful = results.filter((r) => r.success);
2269
+ const failed = results.filter((r) => !r.success);
2270
+ const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
2271
+ track({
2272
+ event: "install",
2273
+ source: sourceIdentifier,
2274
+ skills: selectedSkills.map((s) => s.installName).join(","),
2275
+ agents: targetAgents.join(","),
2276
+ ...installGlobally && { global: "1" },
2277
+ sourceType: "well-known"
2278
+ });
2279
+ if (successful.length > 0 && installGlobally) {
2280
+ const successfulSkillNames = new Set(successful.map((r) => r.skill));
2281
+ for (const skill of selectedSkills) {
2282
+ if (successfulSkillNames.has(skill.installName)) {
2283
+ try {
2284
+ await addSkillToLock(skill.installName, {
2285
+ source: sourceIdentifier,
2286
+ sourceType: "well-known",
2287
+ sourceUrl: skill.sourceUrl,
2288
+ skillFolderHash: ""
2289
+ // Well-known skills don't have a folder hash
2290
+ });
2291
+ } catch {
2292
+ }
2293
+ }
2294
+ }
2295
+ }
2296
+ if (successful.length > 0) {
2297
+ const bySkill = /* @__PURE__ */ new Map();
2298
+ for (const r of successful) {
2299
+ const skillResults = bySkill.get(r.skill) || [];
2300
+ skillResults.push(r);
2301
+ bySkill.set(r.skill, skillResults);
2302
+ }
2303
+ const skillCount = bySkill.size;
2304
+ const agentCount = new Set(successful.map((r) => r.agent)).size;
2305
+ const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
2306
+ const copiedAgents = symlinkFailures.map((r) => r.agent);
2307
+ const resultLines = [];
2308
+ for (const [skillName, skillResults] of bySkill) {
2309
+ const firstResult = skillResults[0];
2310
+ if (firstResult.mode === "copy") {
2311
+ resultLines.push(`${chalk.green("\u2713")} ${skillName} ${chalk.dim("(copied)")}`);
2312
+ for (const r of skillResults) {
2313
+ const shortPath = shortenPath(r.path, cwd);
2314
+ resultLines.push(` ${chalk.dim("\u2192")} ${shortPath}`);
2315
+ }
2316
+ } else {
2317
+ if (firstResult.canonicalPath) {
2318
+ const shortPath = shortenPath(firstResult.canonicalPath, cwd);
2319
+ resultLines.push(`${chalk.green("\u2713")} ${shortPath}`);
2320
+ } else {
2321
+ resultLines.push(`${chalk.green("\u2713")} ${skillName}`);
2322
+ }
2323
+ const symlinked = skillResults.filter((r) => !r.symlinkFailed).map((r) => r.agent);
2324
+ const copied = skillResults.filter((r) => r.symlinkFailed).map((r) => r.agent);
2325
+ if (symlinked.length > 0) {
2326
+ resultLines.push(` ${chalk.dim("symlink \u2192")} ${formatList(symlinked)}`);
2327
+ }
2328
+ if (copied.length > 0) {
2329
+ resultLines.push(` ${chalk.yellow("copied \u2192")} ${formatList(copied)}`);
2330
+ }
2331
+ }
2332
+ }
2333
+ const title = chalk.green(
2334
+ `Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""} to ${agentCount} agent${agentCount !== 1 ? "s" : ""}`
2335
+ );
2336
+ p.note(resultLines.join("\n"), title);
2337
+ if (symlinkFailures.length > 0) {
2338
+ p.log.warn(chalk.yellow(`Symlinks failed for: ${formatList(copiedAgents)}`));
2339
+ p.log.message(
2340
+ chalk.dim(
2341
+ " Files were copied instead. On Windows, enable Developer Mode for symlink support."
2342
+ )
2343
+ );
2344
+ }
2345
+ }
2346
+ if (failed.length > 0) {
2347
+ console.log();
2348
+ p.log.error(chalk.red(`Failed to install ${failed.length}`));
2349
+ for (const r of failed) {
2350
+ p.log.message(` ${chalk.red("\u2717")} ${r.skill} \u2192 ${r.agent}: ${chalk.dim(r.error)}`);
2351
+ }
2352
+ }
2353
+ console.log();
2354
+ p.outro(chalk.green("Done!"));
2355
+ await promptForFindSkills();
2356
+ }
1654
2357
  async function handleDirectUrlSkillLegacy(source, url, options, spinner2) {
1655
2358
  spinner2.start("Fetching skill.md...");
1656
2359
  const mintlifySkill = await fetchMintlifySkill(url);
@@ -1924,6 +2627,10 @@ async function runAdd(args, options = {}) {
1924
2627
  await handleRemoteSkill(source, parsed.url, options, spinner2);
1925
2628
  return;
1926
2629
  }
2630
+ if (parsed.type === "well-known") {
2631
+ await handleWellKnownSkills(source, parsed.url, options, spinner2);
2632
+ return;
2633
+ }
1927
2634
  let skillsDir;
1928
2635
  if (parsed.type === "local") {
1929
2636
  spinner2.start("Validating local path...");
@@ -1950,7 +2657,9 @@ async function runAdd(args, options = {}) {
1950
2657
  await cleanup(tempDir);
1951
2658
  process.exit(1);
1952
2659
  }
1953
- spinner2.stop(`Found ${chalk.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
2660
+ spinner2.stop(
2661
+ `Found ${chalk.green(skills.length)} skill${skills.length > 1 ? "s" : ""} ${chalk.dim("(via Well-known Agent Skill Discovery)")}`
2662
+ );
1954
2663
  if (options.list) {
1955
2664
  console.log();
1956
2665
  p.log.step(chalk.bold("Available Skills"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {