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 +21 -0
- package/README.md +6 -1
- package/dist/_chunks/libs/@clack/prompts.mjs +1 -0
- package/dist/cli.mjs +490 -84
- package/package.json +2 -1
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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.
|
|
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.
|
|
3658
|
-
|
|
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
|
-
|
|
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")
|
|
6325
|
-
|
|
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 === "
|
|
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.
|
|
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
|
}
|