skills 1.5.20 → 1.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vercel, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -85,8 +85,13 @@ npx skills add vercel-labs/agent-skills --skill '*' -a claude-code
85
85
 
86
86
  # Install specific skills to all agents
87
87
  npx skills add vercel-labs/agent-skills --agent '*' --skill frontend-design
88
+
89
+ # Install from a direct SKILL.md or archive download URL
90
+ npx skills add https://example.com/download/my-skill
88
91
  ```
89
92
 
93
+ Direct download URLs are tried after well-known discovery. They may point to a single valid `SKILL.md` file or a `.zip`, `.tar`, `.tar.gz`, or `.tgz` archive; the URL does not need to include a file extension. Downloads are limited to 10 MiB, extracted content to 25 MiB, and archives to 1000 files by default. Override with `SKILLS_DOWNLOAD_MAX_BYTES`, `SKILLS_EXTRACT_MAX_BYTES`, and `SKILLS_EXTRACT_MAX_FILES` when you trust the source.
94
+
90
95
  ### Installation Scope
91
96
 
92
97
  | Scope | Flag | Location | Use Case |
@@ -542,4 +547,4 @@ Telemetry is automatically disabled in CI environments.
542
547
 
543
548
  ## License
544
549
 
545
- MIT
550
+ This project is licensed under the [MIT License](LICENSE).
@@ -1,6 +1,7 @@
1
1
  import { a, block, fastStringWidth, getColumns, getRows, n$1, r, require_src, settings, wrapAnsi, wrapTextWithPrefix } from "./core.mjs";
2
2
  import { styleText } from "node:util";
3
3
  import process$1 from "node:process";
4
+ import "node:path";
4
5
  var import_src = require_src();
5
6
  function isUnicodeSupported() {
6
7
  if (process$1.platform !== "win32") return process$1.env.TERM !== "linux";
package/dist/cli.mjs CHANGED
@@ -10,6 +10,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
10
10
  import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
11
11
  import { fileURLToPath } from "url";
12
12
  import { stripVTControlCharacters } from "node:util";
13
+ import { createWriteStream } from "node:fs";
14
+ import { dirname as dirname$1, join as join$1, normalize as normalize$1, resolve as resolve$1, sep as sep$1 } from "node:path";
13
15
  import { homedir, platform, tmpdir } from "os";
14
16
  import * as readline from "readline";
15
17
  import { Writable } from "stream";
@@ -18,8 +20,12 @@ import { execFile, execSync, spawn, spawnSync } from "child_process";
18
20
  import { access, chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
19
21
  import { parse } from "yaml";
20
22
  import { createHash } from "crypto";
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";
21
24
  import { createHash as createHash$1 } from "node:crypto";
22
- import { gunzipSync, inflateRawSync } from "node:zlib";
25
+ import { crc32, gunzipSync, inflateRawSync } from "node:zlib";
26
+ import { tmpdir as tmpdir$1 } from "node:os";
27
+ import { pipeline } from "node:stream/promises";
28
+ import * as tar from "tar";
23
29
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
24
30
  const DEFAULT_GITHUB_HOST = "github.com";
25
31
  function getGitHubHost() {
@@ -38,7 +44,7 @@ function isGitHubHost(host) {
38
44
  return normalizedHost === DEFAULT_GITHUB_HOST || normalizedHost === getGitHubHost().toLowerCase();
39
45
  }
40
46
  function getOwnerRepo(parsed) {
41
- if (parsed.type === "local") return null;
47
+ if (parsed.type === "local" || parsed.type === "download") return null;
42
48
  const sshMatch = parsed.url.match(/^git@[^:]+:(.+)$/);
43
49
  if (sshMatch) {
44
50
  let path = sshMatch[1];
@@ -133,6 +139,18 @@ function appendFragmentRef(input, ref, skillFilter) {
133
139
  if (!ref) return input;
134
140
  return `${input}#${ref}${skillFilter ? `@${skillFilter}` : ""}`;
135
141
  }
142
+ function isHostedArtifactUrl(input) {
143
+ try {
144
+ const parsed = new URL(input);
145
+ const host = parsed.hostname.toLowerCase();
146
+ if (host === "raw.githubusercontent.com" || host === "codeload.github.com" || host === "objects.githubusercontent.com") return true;
147
+ if (host === "github.com") return /^\/[^/]+\/[^/]+\/(?:archive\/|raw\/|releases\/(?:download\/|latest\/download\/))/.test(parsed.pathname);
148
+ if (host === "gitlab.com") return /\/-\/(?:archive|raw)\//.test(parsed.pathname);
149
+ return false;
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
136
154
  function parseSource(input) {
137
155
  if (isLocalPath(input)) {
138
156
  const resolvedPath = resolve(input);
@@ -150,6 +168,10 @@ function parseSource(input) {
150
168
  if (githubPrefixMatch) return parseSource(appendFragmentRef(githubPrefixMatch[1], fragmentRef, fragmentSkillFilter));
151
169
  const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
152
170
  if (gitlabPrefixMatch) return parseSource(appendFragmentRef(`https://gitlab.com/${gitlabPrefixMatch[1]}`, fragmentRef, fragmentSkillFilter));
171
+ if (isHostedArtifactUrl(input)) return {
172
+ type: "download",
173
+ url: input
174
+ };
153
175
  if (getGitHubHost() !== "github.com" && /^https?:\/\//.test(input)) try {
154
176
  const parsedUrl = new URL(input);
155
177
  if (isGitHubHost(parsedUrl.host) && parsedUrl.host !== "github.com") {
@@ -1904,13 +1926,13 @@ const SKILLS_SUBDIR = "skills";
1904
1926
  function sanitizeName(name) {
1905
1927
  return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
1906
1928
  }
1907
- function isPathSafe$1(basePath, targetPath) {
1929
+ function isPathSafe$2(basePath, targetPath) {
1908
1930
  const normalizedBase = normalize(resolve(basePath));
1909
1931
  const normalizedTarget = normalize(resolve(targetPath));
1910
1932
  return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
1911
1933
  }
1912
1934
  function pathsOverlap(pathA, pathB) {
1913
- return isPathSafe$1(pathA, pathB) || isPathSafe$1(pathB, pathA);
1935
+ return isPathSafe$2(pathA, pathB) || isPathSafe$2(pathB, pathA);
1914
1936
  }
1915
1937
  async function isDirEntryOrSymlinkToDir(entry, entryPath) {
1916
1938
  if (entry.isDirectory()) return true;
@@ -2004,13 +2026,13 @@ async function installSkillForAgent(skill, agentType, options = {}) {
2004
2026
  const canonicalDir = join(canonicalBase, skillName);
2005
2027
  const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent);
2006
2028
  const agentDir = join(agentBase, skillName);
2007
- if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
2029
+ if (!isPathSafe$2(canonicalBase, canonicalDir)) return {
2008
2030
  success: false,
2009
2031
  path: agentDir,
2010
2032
  mode: installMode,
2011
2033
  error: "Invalid skill name: potential path traversal detected"
2012
2034
  };
2013
- if (!isPathSafe$1(agentBase, agentDir)) return {
2035
+ if (!isPathSafe$2(agentBase, agentDir)) return {
2014
2036
  success: false,
2015
2037
  path: agentDir,
2016
2038
  mode: installMode,
@@ -2147,7 +2169,7 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
2147
2169
  if (options.global && agent.globalSkillsDir === void 0) return false;
2148
2170
  const targetBase = options.global ? agent.globalSkillsDir : agentType === "eve" && options.eveSubagent ? getEveSubagentSkillsDir(options.eveSubagent, options.cwd) : join(options.cwd || process.cwd(), agent.skillsDir);
2149
2171
  const skillDir = join(targetBase, sanitized);
2150
- if (!isPathSafe$1(targetBase, skillDir)) return false;
2172
+ if (!isPathSafe$2(targetBase, skillDir)) return false;
2151
2173
  try {
2152
2174
  await access(skillDir);
2153
2175
  return true;
@@ -2161,14 +2183,14 @@ function getInstallPath(skillName, agentType, options = {}) {
2161
2183
  const sanitized = sanitizeName(skillName);
2162
2184
  const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd, options.eveSubagent);
2163
2185
  const installPath = join(targetBase, sanitized);
2164
- if (!isPathSafe$1(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
2186
+ if (!isPathSafe$2(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
2165
2187
  return installPath;
2166
2188
  }
2167
2189
  function getCanonicalPath(skillName, options = {}) {
2168
2190
  const sanitized = sanitizeName(skillName);
2169
2191
  const canonicalBase = options.agent === "eve" ? getAgentBaseDir("eve", options.global ?? false, options.cwd, options.eveSubagent) : getCanonicalSkillsDir(options.global ?? false, options.cwd);
2170
2192
  const canonicalPath = join(canonicalBase, sanitized);
2171
- if (!isPathSafe$1(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
2193
+ if (!isPathSafe$2(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
2172
2194
  return canonicalPath;
2173
2195
  }
2174
2196
  async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
@@ -2188,13 +2210,13 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
2188
2210
  const canonicalDir = join(canonicalBase, skillName);
2189
2211
  const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent);
2190
2212
  const agentDir = join(agentBase, skillName);
2191
- if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
2213
+ if (!isPathSafe$2(canonicalBase, canonicalDir)) return {
2192
2214
  success: false,
2193
2215
  path: agentDir,
2194
2216
  mode: installMode,
2195
2217
  error: "Invalid skill name: potential path traversal detected"
2196
2218
  };
2197
- if (!isPathSafe$1(agentBase, agentDir)) return {
2219
+ if (!isPathSafe$2(agentBase, agentDir)) return {
2198
2220
  success: false,
2199
2221
  path: agentDir,
2200
2222
  mode: installMode,
@@ -2203,7 +2225,7 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
2203
2225
  async function writeSkillFiles(targetDir) {
2204
2226
  for (const [filePath, content] of skill.files) {
2205
2227
  const fullPath = join(targetDir, filePath);
2206
- if (!isPathSafe$1(targetDir, fullPath)) continue;
2228
+ if (!isPathSafe$2(targetDir, fullPath)) continue;
2207
2229
  const parentDir = dirname(fullPath);
2208
2230
  if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
2209
2231
  await writeFile(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" && typeof content === "string" ? stripIgnoredEveFrontmatter(content) : content);
@@ -2269,7 +2291,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
2269
2291
  const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent);
2270
2292
  if (agentType === "eve" && !isEvePackagedSkill(skill.files)) {
2271
2293
  const flatSkillPath = join(agentBase, toEveFlatSkillFileName(skill.installName));
2272
- if (!isPathSafe$1(agentBase, flatSkillPath)) return {
2294
+ if (!isPathSafe$2(agentBase, flatSkillPath)) return {
2273
2295
  success: false,
2274
2296
  path: flatSkillPath,
2275
2297
  mode: installMode,
@@ -2299,13 +2321,13 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
2299
2321
  const canonicalBase = agentType === "eve" && installMode === "symlink" ? getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
2300
2322
  const canonicalDir = join(canonicalBase, skillName);
2301
2323
  const agentDir = join(agentBase, skillName);
2302
- if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
2324
+ if (!isPathSafe$2(canonicalBase, canonicalDir)) return {
2303
2325
  success: false,
2304
2326
  path: agentDir,
2305
2327
  mode: installMode,
2306
2328
  error: "Invalid skill name: potential path traversal detected"
2307
2329
  };
2308
- if (!isPathSafe$1(agentBase, agentDir)) return {
2330
+ if (!isPathSafe$2(agentBase, agentDir)) return {
2309
2331
  success: false,
2310
2332
  path: agentDir,
2311
2333
  mode: installMode,
@@ -2314,7 +2336,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
2314
2336
  async function writeSkillFiles(targetDir) {
2315
2337
  for (const file of skill.files) {
2316
2338
  const fullPath = join(targetDir, file.path);
2317
- if (!isPathSafe$1(targetDir, fullPath)) continue;
2339
+ if (!isPathSafe$2(targetDir, fullPath)) continue;
2318
2340
  const parentDir = dirname(fullPath);
2319
2341
  if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
2320
2342
  await writeFile(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
@@ -2463,7 +2485,7 @@ async function listInstalledSkills(options = {}) {
2463
2485
  ]));
2464
2486
  for (const possibleName of possibleNames) {
2465
2487
  const agentSkillDir = join(agentBase, possibleName);
2466
- if (!isPathSafe$1(agentBase, agentSkillDir)) continue;
2488
+ if (!isPathSafe$2(agentBase, agentSkillDir)) continue;
2467
2489
  try {
2468
2490
  await access(agentSkillDir);
2469
2491
  found = true;
@@ -2475,7 +2497,7 @@ async function listInstalledSkills(options = {}) {
2475
2497
  for (const agentEntry of agentEntries) {
2476
2498
  const candidateDir = join(agentBase, agentEntry.name);
2477
2499
  if (!await isDirEntryOrSymlinkToDir(agentEntry, candidateDir)) continue;
2478
- if (!isPathSafe$1(agentBase, candidateDir)) continue;
2500
+ if (!isPathSafe$2(agentBase, candidateDir)) continue;
2479
2501
  try {
2480
2502
  const candidateSkillMd = join(candidateDir, "SKILL.md");
2481
2503
  await stat(candidateSkillMd);
@@ -2615,9 +2637,220 @@ var ProviderRegistryImpl = class {
2615
2637
  }
2616
2638
  };
2617
2639
  new ProviderRegistryImpl();
2640
+ const ZIP_LOCAL_FILE_HEADER = 67324752;
2641
+ const ZIP_CENTRAL_DIRECTORY_HEADER = 33639248;
2642
+ const ZIP_END_OF_CENTRAL_DIRECTORY = 101010256;
2643
+ const ZIP64_END_OF_CENTRAL_DIRECTORY = 101075792;
2644
+ const ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR = 117853008;
2645
+ const ZIP_END_MIN_SIZE = 22;
2646
+ const ZIP_MAX_COMMENT_SIZE = 65535;
2647
+ const CP437_HIGH_BYTES = [
2648
+ "ÇüéâäàåçêëèïîìÄÅ",
2649
+ "ÉæÆôöòûùÿÖÜ¢£¥₧ƒ",
2650
+ "áíóúñѪº¿⌐¬½¼¡«»",
2651
+ "░▒▓│┤╡╢╖╕╣║╗╝╜╛┐",
2652
+ "└┴┬├─┼╞╟╚╔╩╦╠═╬╧",
2653
+ "╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀",
2654
+ "αßΓπΣσµτΦΘΩδ∞φε∩",
2655
+ "≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\xA0"
2656
+ ].join("");
2657
+ var ArchiveValidationError = class extends Error {
2658
+ constructor(message) {
2659
+ super(message);
2660
+ this.name = "ArchiveValidationError";
2661
+ }
2662
+ };
2663
+ function ensureRange(buffer, offset, length, label) {
2664
+ if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > buffer.length) throw new Error(`Invalid zip archive: ${label} is out of bounds`);
2665
+ }
2666
+ function findEndOfCentralDirectory(buffer) {
2667
+ const minOffset = Math.max(0, buffer.length - ZIP_MAX_COMMENT_SIZE - ZIP_END_MIN_SIZE);
2668
+ for (let offset = buffer.length - ZIP_END_MIN_SIZE; offset >= minOffset; offset--) {
2669
+ if (buffer.readUInt32LE(offset) !== ZIP_END_OF_CENTRAL_DIRECTORY) continue;
2670
+ const commentLength = buffer.readUInt16LE(offset + 20);
2671
+ if (offset + ZIP_END_MIN_SIZE + commentLength === buffer.length) return offset;
2672
+ }
2673
+ return -1;
2674
+ }
2675
+ function readUInt64AsNumber(buffer, offset, label) {
2676
+ ensureRange(buffer, offset, 8, label);
2677
+ const value = buffer.readBigUInt64LE(offset);
2678
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`Invalid zip archive: ${label} exceeds the safe integer range`);
2679
+ return Number(value);
2680
+ }
2681
+ function readCentralDirectory(buffer, endOffset) {
2682
+ const diskNumber = buffer.readUInt16LE(endOffset + 4);
2683
+ const centralDirectoryDisk = buffer.readUInt16LE(endOffset + 6);
2684
+ const entriesOnDisk = buffer.readUInt16LE(endOffset + 8);
2685
+ const totalEntries = buffer.readUInt16LE(endOffset + 10);
2686
+ const size = buffer.readUInt32LE(endOffset + 12);
2687
+ const offset = buffer.readUInt32LE(endOffset + 16);
2688
+ if (!(entriesOnDisk === 65535 || totalEntries === 65535 || size === 4294967295 || offset === 4294967295)) {
2689
+ if (diskNumber !== 0 || centralDirectoryDisk !== 0 || entriesOnDisk !== totalEntries) throw new Error("Multi-disk zip archives are not supported");
2690
+ return {
2691
+ entries: totalEntries,
2692
+ offset,
2693
+ size,
2694
+ trailerOffset: endOffset
2695
+ };
2696
+ }
2697
+ if (diskNumber !== 0 || centralDirectoryDisk !== 0) throw new Error("Multi-disk zip archives are not supported");
2698
+ const locatorOffset = endOffset - 20;
2699
+ ensureRange(buffer, locatorOffset, 20, "zip64 locator");
2700
+ if (buffer.readUInt32LE(locatorOffset) !== ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR) throw new Error("Invalid zip64 locator");
2701
+ if (buffer.readUInt32LE(locatorOffset + 4) !== 0 || buffer.readUInt32LE(locatorOffset + 16) !== 1) throw new Error("Multi-disk zip archives are not supported");
2702
+ const zip64EndOffset = readUInt64AsNumber(buffer, locatorOffset + 8, "zip64 end offset");
2703
+ ensureRange(buffer, zip64EndOffset, 56, "zip64 end of central directory");
2704
+ if (buffer.readUInt32LE(zip64EndOffset) !== ZIP64_END_OF_CENTRAL_DIRECTORY) throw new Error("Invalid zip64 end of central directory");
2705
+ const recordSize = readUInt64AsNumber(buffer, zip64EndOffset + 4, "zip64 end size");
2706
+ if (recordSize < 44) throw new Error("Invalid zip64 end of central directory");
2707
+ ensureRange(buffer, zip64EndOffset, recordSize + 12, "zip64 end of central directory");
2708
+ if (zip64EndOffset + recordSize + 12 !== locatorOffset) throw new Error("Invalid zip64 end of central directory");
2709
+ if (buffer.readUInt32LE(zip64EndOffset + 16) !== 0 || buffer.readUInt32LE(zip64EndOffset + 20) !== 0) throw new Error("Multi-disk zip archives are not supported");
2710
+ const zip64EntriesOnDisk = readUInt64AsNumber(buffer, zip64EndOffset + 24, "zip64 entries on disk");
2711
+ const zip64TotalEntries = readUInt64AsNumber(buffer, zip64EndOffset + 32, "zip64 total entries");
2712
+ if (zip64EntriesOnDisk !== zip64TotalEntries) throw new Error("Multi-disk zip archives are not supported");
2713
+ return {
2714
+ entries: zip64TotalEntries,
2715
+ size: readUInt64AsNumber(buffer, zip64EndOffset + 40, "zip64 central directory size"),
2716
+ offset: readUInt64AsNumber(buffer, zip64EndOffset + 48, "zip64 central directory offset"),
2717
+ trailerOffset: zip64EndOffset
2718
+ };
2719
+ }
2720
+ function normalizeArchivePath(rawPath) {
2721
+ if (!rawPath || rawPath.includes("\0")) return null;
2722
+ const path = rawPath.replace(/\\/g, "/");
2723
+ if (path.startsWith("/") || /^[A-Za-z]:/.test(path)) return null;
2724
+ const parts = path.split("/");
2725
+ if (parts.some((part) => part === "..")) return null;
2726
+ const normalized = parts.filter((part) => part && part !== ".").join("/");
2727
+ if (!normalized && !path.endsWith("/")) return null;
2728
+ return path.endsWith("/") && normalized ? `${normalized}/` : normalized;
2729
+ }
2730
+ function findExtraField(buffer, extraOffset, extraLength, targetId) {
2731
+ const extraEnd = extraOffset + extraLength;
2732
+ let offset = extraOffset;
2733
+ while (offset < extraEnd) {
2734
+ if (offset + 4 > extraEnd) throw new Error("Invalid zip extra field");
2735
+ const id = buffer.readUInt16LE(offset);
2736
+ const size = buffer.readUInt16LE(offset + 2);
2737
+ const dataOffset = offset + 4;
2738
+ ensureRange(buffer, dataOffset, size, "zip extra field");
2739
+ if (dataOffset + size > extraEnd) throw new Error("Invalid zip extra field");
2740
+ if (id === targetId) return buffer.subarray(dataOffset, dataOffset + size);
2741
+ offset = dataOffset + size;
2742
+ }
2743
+ return null;
2744
+ }
2745
+ function decodeFileName(bytes, isUtf8, unicodePathExtra) {
2746
+ if (isUtf8) return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2747
+ if (unicodePathExtra && unicodePathExtra.length >= 5 && unicodePathExtra[0] === 1 && unicodePathExtra.readUInt32LE(1) === crc32(bytes)) return new TextDecoder("utf-8", { fatal: true }).decode(unicodePathExtra.subarray(5));
2748
+ let result = "";
2749
+ for (const byte of bytes) result += byte < 128 ? String.fromCharCode(byte) : CP437_HIGH_BYTES[byte - 128];
2750
+ return result;
2751
+ }
2752
+ function readZip64EntryValues(buffer, extraOffset, extraLength, values) {
2753
+ if (!(values.uncompressedSize === 4294967295 || values.compressedSize === 4294967295 || values.localHeaderOffset === 4294967295 || values.diskStart === 65535)) {
2754
+ if (values.diskStart !== 0) throw new Error("Multi-disk zip archives are not supported");
2755
+ return values;
2756
+ }
2757
+ const zip64Extra = findExtraField(buffer, extraOffset, extraLength, 1);
2758
+ if (!zip64Extra) throw new Error("Invalid zip64 extra field");
2759
+ let valueOffset = 0;
2760
+ const readNextUInt64 = (label) => {
2761
+ if (valueOffset + 8 > zip64Extra.length) throw new Error(`Invalid zip64 extra field: missing ${label}`);
2762
+ const value = readUInt64AsNumber(zip64Extra, valueOffset, `zip64 ${label}`);
2763
+ valueOffset += 8;
2764
+ return value;
2765
+ };
2766
+ const resolved = { ...values };
2767
+ if (resolved.uncompressedSize === 4294967295) resolved.uncompressedSize = readNextUInt64("uncompressed size");
2768
+ if (resolved.compressedSize === 4294967295) resolved.compressedSize = readNextUInt64("compressed size");
2769
+ if (resolved.localHeaderOffset === 4294967295) resolved.localHeaderOffset = readNextUInt64("local header offset");
2770
+ if (resolved.diskStart === 65535) {
2771
+ if (valueOffset + 4 > zip64Extra.length) throw new Error("Invalid zip64 extra field: missing disk start");
2772
+ resolved.diskStart = zip64Extra.readUInt32LE(valueOffset);
2773
+ }
2774
+ if (resolved.diskStart !== 0) throw new Error("Multi-disk zip archives are not supported");
2775
+ return resolved;
2776
+ }
2777
+ function readZipArchive(bytes, limits) {
2778
+ const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2779
+ const endOffset = findEndOfCentralDirectory(buffer);
2780
+ if (endOffset < 0) throw new Error("Invalid zip archive");
2781
+ const centralDirectory = readCentralDirectory(buffer, endOffset);
2782
+ const totalEntries = centralDirectory.entries;
2783
+ if (totalEntries > limits.maxEntries) throw new ArchiveValidationError(`Archive contains too many files (${totalEntries}). Maximum is ${limits.maxEntries}.`);
2784
+ ensureRange(buffer, centralDirectory.offset, centralDirectory.size, "central directory");
2785
+ if (centralDirectory.offset + centralDirectory.size > centralDirectory.trailerOffset) throw new Error("Invalid zip archive: central directory overlaps archive trailer");
2786
+ const files = /* @__PURE__ */ new Map();
2787
+ let extractedBytes = 0;
2788
+ let offset = centralDirectory.offset;
2789
+ for (let index = 0; index < totalEntries; index++) {
2790
+ ensureRange(buffer, offset, 46, "central directory entry");
2791
+ if (buffer.readUInt32LE(offset) !== ZIP_CENTRAL_DIRECTORY_HEADER) throw new Error("Invalid zip central directory entry");
2792
+ const flags = buffer.readUInt16LE(offset + 8);
2793
+ const method = buffer.readUInt16LE(offset + 10);
2794
+ const expectedChecksum = buffer.readUInt32LE(offset + 16);
2795
+ let compressedSize = buffer.readUInt32LE(offset + 20);
2796
+ let uncompressedSize = buffer.readUInt32LE(offset + 24);
2797
+ const fileNameLength = buffer.readUInt16LE(offset + 28);
2798
+ const extraLength = buffer.readUInt16LE(offset + 30);
2799
+ const commentLength = buffer.readUInt16LE(offset + 32);
2800
+ let diskStart = buffer.readUInt16LE(offset + 34);
2801
+ const externalAttributes = buffer.readUInt32LE(offset + 38);
2802
+ let localHeaderOffset = buffer.readUInt32LE(offset + 42);
2803
+ const variableSize = fileNameLength + extraLength + commentLength;
2804
+ ensureRange(buffer, offset + 46, variableSize, "central directory entry data");
2805
+ const nameStart = offset + 46;
2806
+ const extraOffset = nameStart + fileNameLength;
2807
+ ({compressedSize, diskStart, localHeaderOffset, uncompressedSize} = readZip64EntryValues(buffer, extraOffset, extraLength, {
2808
+ compressedSize,
2809
+ diskStart,
2810
+ localHeaderOffset,
2811
+ uncompressedSize
2812
+ }));
2813
+ const centralFileName = buffer.subarray(nameStart, nameStart + fileNameLength);
2814
+ const rawFileName = decodeFileName(centralFileName, Boolean(flags & 2048), findExtraField(buffer, extraOffset, extraLength, 28789));
2815
+ const fileName = normalizeArchivePath(rawFileName);
2816
+ if (fileName === null) throw new ArchiveValidationError(`Archive contains unsafe path: ${rawFileName}`);
2817
+ if (flags & 1) throw new ArchiveValidationError("Encrypted zip entries are not supported");
2818
+ const fileType = externalAttributes >>> 16 & 61440;
2819
+ if (fileType !== 0 && fileType !== 32768 && fileType !== 16384) throw new ArchiveValidationError("Archive links are not supported");
2820
+ const isDirectory = rawFileName.replace(/\\/g, "/").endsWith("/") || fileType === 16384;
2821
+ offset += 46 + variableSize;
2822
+ extractedBytes += uncompressedSize;
2823
+ if (extractedBytes > limits.maxExtractedBytes) throw new ArchiveValidationError(`Archive extracts to more than ${limits.maxExtractedBytes} bytes.`);
2824
+ if (isDirectory) continue;
2825
+ ensureRange(buffer, localHeaderOffset, 30, "local file header");
2826
+ if (buffer.readUInt32LE(localHeaderOffset) !== ZIP_LOCAL_FILE_HEADER) throw new Error("Invalid zip local file header");
2827
+ const localFlags = buffer.readUInt16LE(localHeaderOffset + 6);
2828
+ const localMethod = buffer.readUInt16LE(localHeaderOffset + 8);
2829
+ const localFileNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
2830
+ const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
2831
+ const localNameOffset = localHeaderOffset + 30;
2832
+ ensureRange(buffer, localNameOffset, localFileNameLength + localExtraLength, "local file header data");
2833
+ const localFileName = buffer.subarray(localNameOffset, localNameOffset + localFileNameLength);
2834
+ if (localFlags !== flags || localMethod !== method || !localFileName.equals(centralFileName)) throw new Error("Zip local header does not match central directory");
2835
+ const dataOffset = localHeaderOffset + 30 + localFileNameLength + localExtraLength;
2836
+ ensureRange(buffer, dataOffset, compressedSize, "file data");
2837
+ if (dataOffset + compressedSize > centralDirectory.offset) throw new Error("Invalid zip archive: file data overlaps central directory");
2838
+ const compressed = buffer.subarray(dataOffset, dataOffset + compressedSize);
2839
+ let contents;
2840
+ if (method === 0) contents = compressed;
2841
+ else if (method === 8) contents = inflateRawSync(compressed, { maxOutputLength: uncompressedSize + 1 });
2842
+ else throw new Error(`Unsupported zip compression method: ${method}`);
2843
+ if (contents.byteLength !== uncompressedSize) throw new Error("Zip entry size mismatch");
2844
+ if (crc32(contents) !== expectedChecksum) throw new Error("Zip entry checksum mismatch");
2845
+ files.set(fileName, new Uint8Array(contents));
2846
+ }
2847
+ if (offset !== centralDirectory.offset + centralDirectory.size) throw new Error("Invalid zip central directory size");
2848
+ return files;
2849
+ }
2618
2850
  const DISCOVERY_SCHEMA_V2 = "https://schemas.agentskills.io/discovery/0.2.0/schema.json";
2619
2851
  const MAX_ARCHIVE_UNPACKED_BYTES = 50 * 1024 * 1024;
2620
2852
  const MAX_ARCHIVE_FILES = 1e3;
2853
+ const DISCOVERY_TIMEOUT_MS = 1e4;
2621
2854
  var WellKnownProvider = class {
2622
2855
  id = "well-known";
2623
2856
  displayName = "Well-Known Skills";
@@ -2647,6 +2880,7 @@ var WellKnownProvider = class {
2647
2880
  try {
2648
2881
  const parsed = new URL(baseUrl);
2649
2882
  const basePath = parsed.pathname.replace(/\/$/, "");
2883
+ const signal = AbortSignal.timeout(DISCOVERY_TIMEOUT_MS);
2650
2884
  const urlsToTry = [];
2651
2885
  for (const wellKnownPath of this.WELL_KNOWN_PATHS) {
2652
2886
  urlsToTry.push({
@@ -2662,7 +2896,7 @@ var WellKnownProvider = class {
2662
2896
  }
2663
2897
  const candidates = [];
2664
2898
  for (const { indexUrl, baseUrl: resolvedBase, wellKnownPath } of urlsToTry) try {
2665
- const response = await fetch(indexUrl);
2899
+ const response = await fetch(indexUrl, { signal });
2666
2900
  if (!response.ok) continue;
2667
2901
  const rawIndex = await response.json();
2668
2902
  const normalized = this.normalizeIndex(rawIndex, indexUrl, wellKnownPath);
@@ -2929,7 +3163,10 @@ var WellKnownProvider = class {
2929
3163
  return `sha256:${createHash$1("sha256").update(bytes).digest("hex")}`;
2930
3164
  }
2931
3165
  extractArchive(bytes, artifactUrl, contentType) {
2932
- if (this.isZipArchive(bytes, artifactUrl, contentType)) return this.extractZip(bytes);
3166
+ if (this.isZipArchive(bytes, artifactUrl, contentType)) return new Map(readZipArchive(bytes, {
3167
+ maxExtractedBytes: MAX_ARCHIVE_UNPACKED_BYTES,
3168
+ maxEntries: MAX_ARCHIVE_FILES
3169
+ }));
2933
3170
  if (this.isTarGzArchive(bytes, artifactUrl, contentType)) return this.extractTarGz(bytes);
2934
3171
  throw new Error("Unsupported archive format");
2935
3172
  }
@@ -2989,54 +3226,6 @@ var WellKnownProvider = class {
2989
3226
  const nul = slice.indexOf(0);
2990
3227
  return new TextDecoder().decode(nul >= 0 ? slice.subarray(0, nul) : slice);
2991
3228
  }
2992
- extractZip(bytes) {
2993
- const buffer = Buffer.from(bytes);
2994
- const eocdOffset = this.findZipEndOfCentralDirectory(buffer);
2995
- if (eocdOffset < 0) throw new Error("Invalid zip archive");
2996
- const totalEntries = buffer.readUInt16LE(eocdOffset + 10);
2997
- const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + 16);
2998
- const files = /* @__PURE__ */ new Map();
2999
- const runningTotal = { bytes: 0 };
3000
- let offset = centralDirectoryOffset;
3001
- for (let i = 0; i < totalEntries; i++) {
3002
- if (buffer.readUInt32LE(offset) !== 33639248) throw new Error("Invalid zip directory");
3003
- const flags = buffer.readUInt16LE(offset + 8);
3004
- const method = buffer.readUInt16LE(offset + 10);
3005
- const compressedSize = buffer.readUInt32LE(offset + 20);
3006
- const uncompressedSize = buffer.readUInt32LE(offset + 24);
3007
- const fileNameLength = buffer.readUInt16LE(offset + 28);
3008
- const extraLength = buffer.readUInt16LE(offset + 30);
3009
- const commentLength = buffer.readUInt16LE(offset + 32);
3010
- const externalAttributes = buffer.readUInt32LE(offset + 38);
3011
- const localHeaderOffset = buffer.readUInt32LE(offset + 42);
3012
- const nameStart = offset + 46;
3013
- const rawName = buffer.subarray(nameStart, nameStart + fileNameLength);
3014
- const fileName = new TextDecoder(flags & 2048 ? "utf-8" : void 0).decode(rawName);
3015
- offset = nameStart + fileNameLength + extraLength + commentLength;
3016
- if (fileName.endsWith("/")) continue;
3017
- if (flags & 1) throw new Error("Encrypted zip entries are not supported");
3018
- const fileType = externalAttributes >>> 16 & 61440;
3019
- if (fileType === 40960 || fileType === 4096) throw new Error("Archive links are not supported");
3020
- if (buffer.readUInt32LE(localHeaderOffset) !== 67324752) throw new Error("Invalid zip local header");
3021
- const localFileNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
3022
- const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
3023
- const dataStart = localHeaderOffset + 30 + localFileNameLength + localExtraLength;
3024
- const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
3025
- let content;
3026
- if (method === 0) content = compressed;
3027
- else if (method === 8) content = inflateRawSync(compressed);
3028
- else throw new Error(`Unsupported zip compression method: ${method}`);
3029
- if (content.byteLength !== uncompressedSize) throw new Error("Zip entry size mismatch");
3030
- this.addArchiveFile(files, fileName, new Uint8Array(content), runningTotal);
3031
- }
3032
- if (!files.has("SKILL.md")) throw new Error("Archive missing root SKILL.md");
3033
- return files;
3034
- }
3035
- findZipEndOfCentralDirectory(buffer) {
3036
- const minOffset = Math.max(0, buffer.length - 65535 - 22);
3037
- for (let offset = buffer.length - 22; offset >= minOffset; offset--) if (buffer.readUInt32LE(offset) === 101010256) return offset;
3038
- return -1;
3039
- }
3040
3229
  toRawUrl(url) {
3041
3230
  try {
3042
3231
  const parsed = new URL(url);
@@ -3065,6 +3254,187 @@ var WellKnownProvider = class {
3065
3254
  }
3066
3255
  };
3067
3256
  const wellKnownProvider = new WellKnownProvider();
3257
+ const DEFAULT_DOWNLOAD_MAX_BYTES = 10 * 1024 * 1024;
3258
+ const DEFAULT_EXTRACT_MAX_BYTES = 25 * 1024 * 1024;
3259
+ const DEFAULT_EXTRACT_MAX_FILES = 1e3;
3260
+ const FETCH_TIMEOUT_MS = 3e4;
3261
+ function getPositiveIntegerEnv(name, fallback) {
3262
+ const raw = process.env[name];
3263
+ if (!raw) return fallback;
3264
+ const parsed = Number.parseInt(raw, 10);
3265
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3266
+ }
3267
+ function getDownloadLimits() {
3268
+ return {
3269
+ downloadMaxBytes: getPositiveIntegerEnv("SKILLS_DOWNLOAD_MAX_BYTES", DEFAULT_DOWNLOAD_MAX_BYTES),
3270
+ extractMaxBytes: getPositiveIntegerEnv("SKILLS_EXTRACT_MAX_BYTES", DEFAULT_EXTRACT_MAX_BYTES),
3271
+ extractMaxFiles: getPositiveIntegerEnv("SKILLS_EXTRACT_MAX_FILES", DEFAULT_EXTRACT_MAX_FILES)
3272
+ };
3273
+ }
3274
+ function isPathSafe$1(basePath, targetPath) {
3275
+ const normalizedBase = normalize$1(resolve$1(basePath));
3276
+ const normalizedTarget = normalize$1(resolve$1(targetPath));
3277
+ return normalizedTarget.startsWith(normalizedBase + sep$1) || normalizedTarget === normalizedBase;
3278
+ }
3279
+ function validateArchivePath(path) {
3280
+ const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
3281
+ if (!normalized || normalized.endsWith("/")) return normalized;
3282
+ if (normalized.startsWith("/") || /^[a-zA-Z]:\//.test(normalized)) return null;
3283
+ if (normalized.split("/").includes("..")) return null;
3284
+ return normalized;
3285
+ }
3286
+ function incrementEntry(state, size, limits) {
3287
+ state.entries += 1;
3288
+ if (state.entries > limits.extractMaxFiles) throw new ArchiveValidationError(`Archive contains too many files (${state.entries}). Maximum is ${limits.extractMaxFiles}. Set SKILLS_EXTRACT_MAX_FILES to override.`);
3289
+ state.bytes += size;
3290
+ if (state.bytes > limits.extractMaxBytes) throw new ArchiveValidationError(`Archive extracts to more than ${limits.extractMaxBytes} bytes. Set SKILLS_EXTRACT_MAX_BYTES to override.`);
3291
+ }
3292
+ async function downloadToFile(url, targetFile, limits) {
3293
+ const response = await fetch(url, {
3294
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
3295
+ redirect: "follow"
3296
+ });
3297
+ if (!response.ok) throw new Error(`Download failed with HTTP ${response.status}`);
3298
+ const contentLength = response.headers.get("content-length");
3299
+ if (contentLength) {
3300
+ const parsed = Number.parseInt(contentLength, 10);
3301
+ if (Number.isFinite(parsed) && parsed > limits.downloadMaxBytes) throw new Error(`Download is larger than ${limits.downloadMaxBytes} bytes. Set SKILLS_DOWNLOAD_MAX_BYTES to override.`);
3302
+ }
3303
+ if (!response.body) throw new Error("Download response has no body");
3304
+ let downloaded = 0;
3305
+ const limitStream = new TransformStream({ transform(chunk, controller) {
3306
+ downloaded += chunk.byteLength;
3307
+ if (downloaded > limits.downloadMaxBytes) throw new Error(`Download is larger than ${limits.downloadMaxBytes} bytes. Set SKILLS_DOWNLOAD_MAX_BYTES to override.`);
3308
+ controller.enqueue(chunk);
3309
+ } });
3310
+ await pipeline(response.body.pipeThrough(limitStream), createWriteStream(targetFile));
3311
+ }
3312
+ async function isValidSkillMarkdown(filePath) {
3313
+ try {
3314
+ const { data } = parseFrontmatter(await readFile$1(filePath, "utf-8"));
3315
+ return typeof data.name === "string" && typeof data.description === "string";
3316
+ } catch {
3317
+ return false;
3318
+ }
3319
+ }
3320
+ async function extractZip(filePath, extractDir, limits) {
3321
+ const files = readZipArchive(await readFile$1(filePath), {
3322
+ maxExtractedBytes: limits.extractMaxBytes,
3323
+ maxEntries: limits.extractMaxFiles
3324
+ });
3325
+ for (const [path, contents] of files) {
3326
+ const targetPath = join$1(extractDir, path);
3327
+ if (!isPathSafe$1(extractDir, targetPath)) throw new ArchiveValidationError(`Archive contains unsafe path: ${path}`);
3328
+ await mkdir$1(dirname$1(targetPath), { recursive: true });
3329
+ await writeFile$1(targetPath, contents);
3330
+ }
3331
+ }
3332
+ function getTarEntryType(entry) {
3333
+ if (entry instanceof tar.ReadEntry) return entry.type;
3334
+ if (entry.isFile()) return "File";
3335
+ if (entry.isDirectory()) return "Directory";
3336
+ return "";
3337
+ }
3338
+ function isTarEntryFile(entry) {
3339
+ const type = getTarEntryType(entry);
3340
+ return type === "File" || type === "OldFile" || type === "ContiguousFile";
3341
+ }
3342
+ async function extractTar(filePath, extractDir, limits) {
3343
+ const state = {
3344
+ bytes: 0,
3345
+ entries: 0
3346
+ };
3347
+ let validationError;
3348
+ await tar.x({
3349
+ strict: true,
3350
+ filter(entryPath, entry) {
3351
+ if (validationError) return false;
3352
+ try {
3353
+ const safePath = validateArchivePath(entryPath);
3354
+ if (safePath === null) throw new ArchiveValidationError(`Archive contains unsafe path: ${entryPath}`);
3355
+ if (!isPathSafe$1(extractDir, join$1(extractDir, safePath))) throw new ArchiveValidationError(`Archive contains unsafe path: ${entryPath}`);
3356
+ incrementEntry(state, entry.size, limits);
3357
+ if (isTarEntryFile(entry)) return true;
3358
+ return getTarEntryType(entry) === "Directory";
3359
+ } catch (error) {
3360
+ if (error instanceof ArchiveValidationError) {
3361
+ validationError = error;
3362
+ return false;
3363
+ }
3364
+ throw error;
3365
+ }
3366
+ },
3367
+ cwd: extractDir,
3368
+ preservePaths: false,
3369
+ noChmod: true,
3370
+ file: filePath
3371
+ });
3372
+ if (validationError) throw validationError;
3373
+ }
3374
+ async function tryExtractArchive(filePath, extractDir, limits) {
3375
+ const header = await readFile$1(filePath).then((buffer) => buffer.subarray(0, 512));
3376
+ const isZip = header[0] === 80 && header[1] === 75;
3377
+ const isGzip = header[0] === 31 && header[1] === 139;
3378
+ try {
3379
+ if (isZip) {
3380
+ await extractZip(filePath, extractDir, limits);
3381
+ return true;
3382
+ }
3383
+ if (isGzip) {
3384
+ await extractTar(filePath, extractDir, limits);
3385
+ return true;
3386
+ }
3387
+ await extractTar(filePath, extractDir, limits);
3388
+ return true;
3389
+ } catch (error) {
3390
+ await rm$1(extractDir, {
3391
+ recursive: true,
3392
+ force: true
3393
+ }).catch(() => {});
3394
+ await mkdir$1(extractDir, { recursive: true });
3395
+ if (error instanceof ArchiveValidationError) throw error;
3396
+ return false;
3397
+ }
3398
+ }
3399
+ async function getSingleTopLevelDirectory(dir) {
3400
+ const { readdir } = await import("node:fs/promises");
3401
+ const visibleEntries = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.name !== "__MACOSX");
3402
+ if (visibleEntries.length !== 1 || !visibleEntries[0].isDirectory()) return null;
3403
+ return join$1(dir, visibleEntries[0].name);
3404
+ }
3405
+ async function downloadSource(url) {
3406
+ const limits = getDownloadLimits();
3407
+ const tempDir = await mkdtemp$1(join$1(tmpdir$1(), "skills-download-"));
3408
+ const downloadedFile = join$1(tempDir, "source.download");
3409
+ const extractDir = join$1(tempDir, "extract");
3410
+ try {
3411
+ await downloadToFile(url, downloadedFile, limits);
3412
+ if ((await stat$1(downloadedFile)).size === 0) throw new Error("Downloaded URL is empty");
3413
+ if (await isValidSkillMarkdown(downloadedFile)) {
3414
+ const skillDir = join$1(tempDir, "skill");
3415
+ await mkdir$1(skillDir, { recursive: true });
3416
+ await writeFile$1(join$1(skillDir, "SKILL.md"), await readFile$1(downloadedFile));
3417
+ return {
3418
+ rootDir: skillDir,
3419
+ tempDir,
3420
+ kind: "skill-md"
3421
+ };
3422
+ }
3423
+ await mkdir$1(extractDir, { recursive: true });
3424
+ if (await tryExtractArchive(downloadedFile, extractDir, limits)) return {
3425
+ rootDir: await getSingleTopLevelDirectory(extractDir) ?? extractDir,
3426
+ tempDir,
3427
+ kind: "archive"
3428
+ };
3429
+ throw new Error("Downloaded URL is not a valid SKILL.md file or supported archive");
3430
+ } catch (error) {
3431
+ await rm$1(tempDir, {
3432
+ recursive: true,
3433
+ force: true
3434
+ }).catch(() => {});
3435
+ throw error;
3436
+ }
3437
+ }
3068
3438
  const AGENTS_DIR = ".agents";
3069
3439
  const LOCK_FILE = ".skill-lock.json";
3070
3440
  const CURRENT_VERSION = 3;
@@ -3431,7 +3801,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
3431
3801
  tree
3432
3802
  };
3433
3803
  }
3434
- var version$1 = "1.5.20";
3804
+ var version$1 = "1.5.21";
3435
3805
  const isCancelled$1 = (value) => typeof value === "symbol";
3436
3806
  const EVE_AGENT_LABEL = "eve agent";
3437
3807
  async function isSourcePrivate(source) {
@@ -3651,12 +4021,11 @@ async function selectAgentsInteractive(options) {
3651
4021
  }
3652
4022
  setVersion(version$1);
3653
4023
  async function handleWellKnownSkills(source, url, options, spinner) {
3654
- spinner.start("Discovering skills from well-known endpoint");
3655
- const skills = await wellKnownProvider.fetchAllSkills(url);
4024
+ spinner.start("Discovering skills from well-known endpoint...");
4025
+ const skills = await wellKnownProvider.fetchAllSkills(url).catch(() => []);
3656
4026
  if (skills.length === 0) {
3657
- spinner.stop(import_picocolors.default.red("No skills found"));
3658
- outro(import_picocolors.default.red("No skills found at this URL. Make sure the server has a /.well-known/agent-skills/index.json or /.well-known/skills/index.json file."));
3659
- process.exit(1);
4027
+ spinner.stop(import_picocolors.default.dim("No well-known skills found; trying direct download..."));
4028
+ return false;
3660
4029
  }
3661
4030
  spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3662
4031
  for (const skill of skills) {
@@ -3861,6 +4230,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3861
4230
  agents: targetAgents.join(","),
3862
4231
  ...installGlobally && { global: "1" },
3863
4232
  skillFiles: JSON.stringify(skillFiles),
4233
+ installUrl: url,
3864
4234
  metadata: options.metadata,
3865
4235
  sourceType: "well-known"
3866
4236
  });
@@ -3936,6 +4306,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3936
4306
  console.log();
3937
4307
  outro(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
3938
4308
  await promptForFindSkills(options, targetAgents);
4309
+ return true;
3939
4310
  }
3940
4311
  async function runAdd(args, options = {}) {
3941
4312
  const source = args[0];
@@ -3979,8 +4350,9 @@ async function runAdd(args, options = {}) {
3979
4350
  const spinner$3 = spinner();
3980
4351
  spinner$3.start("Parsing source…");
3981
4352
  const parsed = parseSource(source);
4353
+ let directDownload = parsed.type === "download";
3982
4354
  spinner$3.stop(`Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
3983
- const ownerRepoRaw = getOwnerRepo(parsed);
4355
+ const ownerRepoRaw = parsed.type === "well-known" || parsed.type === "download" ? null : getOwnerRepo(parsed);
3984
4356
  const repoPrivacyPromise = (() => {
3985
4357
  if (parsed.type !== "github") return Promise.resolve(null);
3986
4358
  if (!ownerRepoRaw) return Promise.resolve(null);
@@ -3989,8 +4361,8 @@ async function runAdd(args, options = {}) {
3989
4361
  return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null);
3990
4362
  })();
3991
4363
  if (parsed.type === "well-known") {
3992
- await handleWellKnownSkills(source, parsed.url, options, spinner$3);
3993
- return;
4364
+ if (await handleWellKnownSkills(source, parsed.url, options, spinner$3)) return;
4365
+ directDownload = true;
3994
4366
  }
3995
4367
  if (parsed.skillFilter) {
3996
4368
  options.skill = options.skill || [];
@@ -4012,6 +4384,16 @@ async function runAdd(args, options = {}) {
4012
4384
  includeInternal,
4013
4385
  fullDepth: options.fullDepth
4014
4386
  });
4387
+ } else if (parsed.type === "well-known" || parsed.type === "download") {
4388
+ spinner$3.start("Downloading source...");
4389
+ const downloaded = await downloadSource(parsed.url);
4390
+ tempDir = downloaded.tempDir;
4391
+ spinner$3.stop(`Downloaded ${downloaded.kind === "skill-md" ? "SKILL.md file" : "archive"}`);
4392
+ spinner$3.start("Discovering skills...");
4393
+ skills = await discoverSkills(downloaded.rootDir, parsed.subpath, {
4394
+ includeInternal,
4395
+ fullDepth: options.fullDepth
4396
+ });
4015
4397
  } else if (parsed.type === "github" && !options.fullDepth) {
4016
4398
  const BLOB_ALLOWED_OWNERS = [
4017
4399
  "vercel",
@@ -4404,9 +4786,9 @@ async function runAdd(args, options = {}) {
4404
4786
  else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
4405
4787
  else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
4406
4788
  else continue;
4407
- const normalizedSource = getOwnerRepo(parsed);
4408
- const lockSource = getLockSource(parsed.url, normalizedSource);
4409
- const projectLockSourceUrl = getProjectLockSourceUrl(parsed.type, parsed.url);
4789
+ const normalizedSource = directDownload ? null : getOwnerRepo(parsed);
4790
+ const lockSource = directDownload ? null : getLockSource(parsed.url, normalizedSource);
4791
+ const projectLockSourceUrl = directDownload ? void 0 : getProjectLockSourceUrl(parsed.type, parsed.url);
4410
4792
  if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
4411
4793
  if (await repoPrivacyPromise === false) track({
4412
4794
  event: "install",
@@ -4457,7 +4839,7 @@ async function runAdd(args, options = {}) {
4457
4839
  } catch {}
4458
4840
  }
4459
4841
  }
4460
- if (successful.length > 0 && !installGlobally) {
4842
+ if (successful.length > 0 && !installGlobally && !directDownload) {
4461
4843
  const successfulSkillNames = new Set(successful.map((r) => r.skill));
4462
4844
  const eveSubagents = targetAgents.includes("eve") ? eveSubagentTargets.map((s) => s ?? "") : void 0;
4463
4845
  const recordSubagents = eveSubagents && (eveSubagents.length > 1 || eveSubagents.some((s) => s !== ""));
@@ -6321,11 +6703,35 @@ async function runUse(sourceArgs, options = {}, parseErrors = []) {
6321
6703
  const selector = resolveSelector(parsed.skillFilter, options.skill);
6322
6704
  const includeInternal = selector !== void 0;
6323
6705
  let selectedSkill;
6324
- if (parsed.type === "well-known") selectedSkill = selectWellKnownSkill(await wellKnownProvider.fetchAllSkills(parsed.url), selector, source);
6325
- else {
6706
+ if (parsed.type === "well-known") {
6707
+ const skills = await wellKnownProvider.fetchAllSkills(parsed.url);
6708
+ if (skills.length > 0) selectedSkill = selectWellKnownSkill(skills, selector, source);
6709
+ else {
6710
+ const downloaded = await downloadSource(parsed.url);
6711
+ cloneTempDir = downloaded.tempDir;
6712
+ const selected = selectSkill(await discoverSkills(downloaded.rootDir, void 0, {
6713
+ includeInternal,
6714
+ fullDepth: options.fullDepth
6715
+ }), selector, source);
6716
+ selectedSkill = {
6717
+ kind: "disk",
6718
+ name: selected.name,
6719
+ directoryName: selected.name,
6720
+ rawContent: selected.rawContent,
6721
+ path: selected.path
6722
+ };
6723
+ }
6724
+ } else {
6326
6725
  let skills;
6327
6726
  let blobResult = null;
6328
- if (parsed.type === "local") {
6727
+ if (parsed.type === "download") {
6728
+ const downloaded = await downloadSource(parsed.url);
6729
+ cloneTempDir = downloaded.tempDir;
6730
+ skills = await discoverSkills(downloaded.rootDir, void 0, {
6731
+ includeInternal,
6732
+ fullDepth: options.fullDepth
6733
+ });
6734
+ } else if (parsed.type === "local") {
6329
6735
  if (!existsSync(parsed.localPath)) fail(`Local path does not exist: ${parsed.localPath}`);
6330
6736
  skills = await discoverSkills(parsed.localPath, parsed.subpath, {
6331
6737
  includeInternal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.5.20",
3
+ "version": "1.5.21",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {
@@ -142,6 +142,7 @@
142
142
  },
143
143
  "packageManager": "pnpm@10.17.1",
144
144
  "dependencies": {
145
+ "tar": "^7.5.20",
145
146
  "yaml": "^2.8.3"
146
147
  }
147
148
  }