skills 1.5.19 → 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 +11 -2
- package/dist/_chunks/libs/@clack/prompts.mjs +1 -0
- package/dist/cli.mjs +695 -145
- package/package.json +4 -1
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,11 +20,31 @@ 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);
|
|
30
|
+
const DEFAULT_GITHUB_HOST = "github.com";
|
|
31
|
+
function getGitHubHost() {
|
|
32
|
+
const configuredHost = process.env.GH_HOST?.trim();
|
|
33
|
+
if (!configuredHost) return DEFAULT_GITHUB_HOST;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(`https://${configuredHost}`);
|
|
36
|
+
if (parsed.username || parsed.password || parsed.port || parsed.pathname !== "/" || parsed.search || parsed.hash) return DEFAULT_GITHUB_HOST;
|
|
37
|
+
return parsed.hostname;
|
|
38
|
+
} catch {
|
|
39
|
+
return DEFAULT_GITHUB_HOST;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isGitHubHost(host) {
|
|
43
|
+
const normalizedHost = host.toLowerCase();
|
|
44
|
+
return normalizedHost === DEFAULT_GITHUB_HOST || normalizedHost === getGitHubHost().toLowerCase();
|
|
45
|
+
}
|
|
24
46
|
function getOwnerRepo(parsed) {
|
|
25
|
-
if (parsed.type === "local") return null;
|
|
47
|
+
if (parsed.type === "local" || parsed.type === "download") return null;
|
|
26
48
|
const sshMatch = parsed.url.match(/^git@[^:]+:(.+)$/);
|
|
27
49
|
if (sshMatch) {
|
|
28
50
|
let path = sshMatch[1];
|
|
@@ -88,7 +110,7 @@ function looksLikeGitSource(input) {
|
|
|
88
110
|
if (input.startsWith("http://") || input.startsWith("https://")) try {
|
|
89
111
|
const parsed = new URL(input);
|
|
90
112
|
const pathname = parsed.pathname;
|
|
91
|
-
if (parsed.
|
|
113
|
+
if (isGitHubHost(parsed.host)) return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
|
|
92
114
|
if (parsed.hostname === "gitlab.com") return /^\/.+?\/[^/]+(?:\.git)?(?:\/-\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
|
|
93
115
|
} catch {}
|
|
94
116
|
if (/^https?:\/\/.+\.git(?:$|[/?])/i.test(input)) return true;
|
|
@@ -117,6 +139,18 @@ function appendFragmentRef(input, ref, skillFilter) {
|
|
|
117
139
|
if (!ref) return input;
|
|
118
140
|
return `${input}#${ref}${skillFilter ? `@${skillFilter}` : ""}`;
|
|
119
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
|
+
}
|
|
120
154
|
function parseSource(input) {
|
|
121
155
|
if (isLocalPath(input)) {
|
|
122
156
|
const resolvedPath = resolve(input);
|
|
@@ -134,6 +168,26 @@ function parseSource(input) {
|
|
|
134
168
|
if (githubPrefixMatch) return parseSource(appendFragmentRef(githubPrefixMatch[1], fragmentRef, fragmentSkillFilter));
|
|
135
169
|
const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
|
|
136
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
|
+
};
|
|
175
|
+
if (getGitHubHost() !== "github.com" && /^https?:\/\//.test(input)) try {
|
|
176
|
+
const parsedUrl = new URL(input);
|
|
177
|
+
if (isGitHubHost(parsedUrl.host) && parsedUrl.host !== "github.com") {
|
|
178
|
+
const [owner, rawRepo, marker, ref, ...subpathSegments] = parsedUrl.pathname.split("/").filter(Boolean);
|
|
179
|
+
if (owner && rawRepo) {
|
|
180
|
+
const repo = rawRepo.replace(/\.git$/, "");
|
|
181
|
+
const isTreeUrl = marker === "tree" && ref;
|
|
182
|
+
return {
|
|
183
|
+
type: "git",
|
|
184
|
+
url: `${parsedUrl.protocol}//${parsedUrl.host}/${owner}/${repo}.git`,
|
|
185
|
+
...isTreeUrl ? { ref } : fragmentRef ? { ref: fragmentRef } : {},
|
|
186
|
+
...isTreeUrl && subpathSegments.length > 0 ? { subpath: sanitizeSubpath(subpathSegments.join("/")) } : {}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch {}
|
|
137
191
|
const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
|
|
138
192
|
if (githubTreeWithPathMatch) {
|
|
139
193
|
const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
|
|
@@ -190,12 +244,14 @@ function parseSource(input) {
|
|
|
190
244
|
...fragmentRef ? { ref: fragmentRef } : {}
|
|
191
245
|
};
|
|
192
246
|
}
|
|
247
|
+
const githubHost = getGitHubHost();
|
|
248
|
+
const shorthandSourceType = githubHost === "github.com" ? "github" : "git";
|
|
193
249
|
const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
|
|
194
250
|
if (atSkillMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
|
|
195
251
|
const [, owner, repo, skillFilter] = atSkillMatch;
|
|
196
252
|
return {
|
|
197
|
-
type:
|
|
198
|
-
url: `https
|
|
253
|
+
type: shorthandSourceType,
|
|
254
|
+
url: `https://${githubHost}/${owner}/${repo}.git`,
|
|
199
255
|
...fragmentRef ? { ref: fragmentRef } : {},
|
|
200
256
|
skillFilter: fragmentSkillFilter || skillFilter
|
|
201
257
|
};
|
|
@@ -204,8 +260,8 @@ function parseSource(input) {
|
|
|
204
260
|
if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
|
|
205
261
|
const [, owner, repo, subpath] = shorthandMatch;
|
|
206
262
|
return {
|
|
207
|
-
type:
|
|
208
|
-
url: `https
|
|
263
|
+
type: shorthandSourceType,
|
|
264
|
+
url: `https://${githubHost}/${owner}/${repo}.git`,
|
|
209
265
|
...fragmentRef ? { ref: fragmentRef } : {},
|
|
210
266
|
subpath: subpath ? sanitizeSubpath(subpath) : subpath,
|
|
211
267
|
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
@@ -573,20 +629,21 @@ var GitCloneError = class extends Error {
|
|
|
573
629
|
}
|
|
574
630
|
};
|
|
575
631
|
function parseGitHubRepoUrl(url) {
|
|
576
|
-
const sshMatch = url.match(/^git@
|
|
577
|
-
if (sshMatch) {
|
|
578
|
-
const
|
|
579
|
-
const
|
|
632
|
+
const sshMatch = url.match(/^git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
633
|
+
if (sshMatch && isGitHubHost(sshMatch[1])) {
|
|
634
|
+
const host = sshMatch[1];
|
|
635
|
+
const owner = sshMatch[2];
|
|
636
|
+
const repo = sshMatch[3];
|
|
580
637
|
return {
|
|
581
638
|
owner,
|
|
582
639
|
repo,
|
|
583
640
|
slug: `${owner}/${repo}`,
|
|
584
|
-
sshUrl: `git
|
|
641
|
+
sshUrl: `git@${host}:${owner}/${repo}.git`
|
|
585
642
|
};
|
|
586
643
|
}
|
|
587
644
|
try {
|
|
588
645
|
const parsed = new URL(url);
|
|
589
|
-
if (parsed.
|
|
646
|
+
if (!isGitHubHost(parsed.host)) return null;
|
|
590
647
|
const match = parsed.pathname.match(/^\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
591
648
|
if (!match) return null;
|
|
592
649
|
const owner = match[1];
|
|
@@ -595,7 +652,7 @@ function parseGitHubRepoUrl(url) {
|
|
|
595
652
|
owner,
|
|
596
653
|
repo,
|
|
597
654
|
slug: `${owner}/${repo}`,
|
|
598
|
-
sshUrl: `git
|
|
655
|
+
sshUrl: `git@${parsed.host}:${owner}/${repo}.git`
|
|
599
656
|
};
|
|
600
657
|
} catch {
|
|
601
658
|
return null;
|
|
@@ -604,7 +661,7 @@ function parseGitHubRepoUrl(url) {
|
|
|
604
661
|
function isGitHubHttpsCloneUrl(url) {
|
|
605
662
|
try {
|
|
606
663
|
const parsed = new URL(url);
|
|
607
|
-
return parsed.protocol === "https:" && parsed.
|
|
664
|
+
return parsed.protocol === "https:" && isGitHubHost(parsed.host);
|
|
608
665
|
} catch {
|
|
609
666
|
return false;
|
|
610
667
|
}
|
|
@@ -665,12 +722,13 @@ async function resetTempDir(dir) {
|
|
|
665
722
|
}
|
|
666
723
|
async function tryGhClone(repo, tempDir, ref) {
|
|
667
724
|
let cloneTarget = repo.slug;
|
|
725
|
+
const host = repo.sshUrl.match(/^git@([^:]+):/)?.[1] || "github.com";
|
|
668
726
|
try {
|
|
669
727
|
const { stdout, stderr } = await execFileAsync("gh", [
|
|
670
728
|
"auth",
|
|
671
729
|
"status",
|
|
672
730
|
"-h",
|
|
673
|
-
|
|
731
|
+
host
|
|
674
732
|
], {
|
|
675
733
|
timeout: 5e3,
|
|
676
734
|
env: {
|
|
@@ -705,8 +763,9 @@ async function tryGhClone(repo, tempDir, ref) {
|
|
|
705
763
|
return true;
|
|
706
764
|
}
|
|
707
765
|
function buildGitHubAuthError(url, repo, message) {
|
|
708
|
-
|
|
709
|
-
if (repo) return `
|
|
766
|
+
const host = repo?.sshUrl.match(/^git@([^:]+):/)?.[1] || "github.com";
|
|
767
|
+
if (repo && isGitHubSsoAuthError(message)) return `GitHub blocked HTTPS access to ${url} because the organization enforces SAML SSO.\n skills tried your existing git credentials and available fallbacks, but none succeeded.\n - Re-authorize your GitHub credentials/app for that org's SSO policy\n - Or rerun with SSH: npx skills add ${repo.sshUrl}\n - Verify access with: gh auth status -h ${host} or ssh -T git@${host}`;
|
|
768
|
+
if (repo) return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - Retry with SSH: npx skills add ${repo.sshUrl}\n - Check access with: gh auth status -h ${host} or ssh -T git@${host}`;
|
|
710
769
|
return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`;
|
|
711
770
|
}
|
|
712
771
|
async function cloneRepo(url, ref) {
|
|
@@ -897,6 +956,13 @@ async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
|
897
956
|
lock.skills[skillName] = entry;
|
|
898
957
|
await writeLocalLock(lock, cwd);
|
|
899
958
|
}
|
|
959
|
+
async function removeSkillFromLocalLock(skillName, cwd) {
|
|
960
|
+
const lock = await readLocalLock(cwd);
|
|
961
|
+
if (!(skillName in lock.skills)) return false;
|
|
962
|
+
delete lock.skills[skillName];
|
|
963
|
+
await writeLocalLock(lock, cwd);
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
900
966
|
function createEmptyLocalLock() {
|
|
901
967
|
return {
|
|
902
968
|
version: CURRENT_VERSION$1,
|
|
@@ -920,9 +986,11 @@ const AGENT_PROJECT_SKILL_DIRS = [
|
|
|
920
986
|
".continue/skills",
|
|
921
987
|
".github/skills",
|
|
922
988
|
".goose/skills",
|
|
989
|
+
".grok/skills",
|
|
923
990
|
".iflow/skills",
|
|
924
991
|
".junie/skills",
|
|
925
992
|
".kilocode/skills",
|
|
993
|
+
".kimchi/skills",
|
|
926
994
|
".kiro/skills",
|
|
927
995
|
".mux/skills",
|
|
928
996
|
".neovate/skills",
|
|
@@ -956,24 +1024,44 @@ async function hasSkillMd(dir) {
|
|
|
956
1024
|
return false;
|
|
957
1025
|
}
|
|
958
1026
|
}
|
|
1027
|
+
function warnSkippedSkill(skillMdPath, reason) {
|
|
1028
|
+
console.warn(`⚠ Skipped ${sanitizeMetadata(skillMdPath)} — ${stripTerminalEscapes(reason)}`);
|
|
1029
|
+
}
|
|
959
1030
|
async function parseSkillMd(skillMdPath, options) {
|
|
1031
|
+
let content;
|
|
960
1032
|
try {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1033
|
+
content = await readFile(skillMdPath, "utf-8");
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
warnSkippedSkill(skillMdPath, `failed to read file: ${err.message}`);
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
let data;
|
|
1039
|
+
try {
|
|
1040
|
+
({data} = parseFrontmatter(content));
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
warnSkippedSkill(skillMdPath, `YAML parse error: ${err.message}`);
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
if (!data.name || !data.description) {
|
|
1046
|
+
const missing = [];
|
|
1047
|
+
if (!data.name) missing.push("name");
|
|
1048
|
+
if (!data.description) missing.push("description");
|
|
1049
|
+
warnSkippedSkill(skillMdPath, `missing required frontmatter field(s): ${missing.join(", ")}`);
|
|
975
1050
|
return null;
|
|
976
1051
|
}
|
|
1052
|
+
if (typeof data.name !== "string" || typeof data.description !== "string") {
|
|
1053
|
+
warnSkippedSkill(skillMdPath, `frontmatter "name" and "description" must be strings (got ${typeof data.name} and ${typeof data.description})`);
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
const metadata = isRecord(data.metadata) ? data.metadata : void 0;
|
|
1057
|
+
if (metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
|
|
1058
|
+
return {
|
|
1059
|
+
name: sanitizeMetadata(data.name),
|
|
1060
|
+
description: sanitizeMetadata(data.description),
|
|
1061
|
+
path: dirname(skillMdPath),
|
|
1062
|
+
rawContent: content,
|
|
1063
|
+
metadata
|
|
1064
|
+
};
|
|
977
1065
|
}
|
|
978
1066
|
async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
|
|
979
1067
|
if (depth > maxDepth) return [];
|
|
@@ -994,6 +1082,7 @@ function isSubpathSafe(basePath, subpath) {
|
|
|
994
1082
|
async function discoverSkills(basePath, subpath, options) {
|
|
995
1083
|
const skills = [];
|
|
996
1084
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1085
|
+
const parsedSkillPaths = /* @__PURE__ */ new Set();
|
|
997
1086
|
const localLock = await readLocalLock(basePath);
|
|
998
1087
|
const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName));
|
|
999
1088
|
if (subpath && !isSubpathSafe(basePath, subpath)) throw new Error(`Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.`);
|
|
@@ -1012,8 +1101,14 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
1012
1101
|
const directoryName = normalizeSkillName(basename(skill.path));
|
|
1013
1102
|
return lockedSkillNames.has(skillName) || lockedSkillNames.has(directoryName);
|
|
1014
1103
|
};
|
|
1104
|
+
const parseSkillAt = async (skillDir) => {
|
|
1105
|
+
const skillMdPath = resolve(skillDir, "SKILL.md");
|
|
1106
|
+
if (parsedSkillPaths.has(skillMdPath)) return null;
|
|
1107
|
+
parsedSkillPaths.add(skillMdPath);
|
|
1108
|
+
return parseSkillMd(skillMdPath, options);
|
|
1109
|
+
};
|
|
1015
1110
|
if (await hasSkillMd(searchPath)) {
|
|
1016
|
-
let skill = await
|
|
1111
|
+
let skill = await parseSkillAt(searchPath);
|
|
1017
1112
|
if (skill) {
|
|
1018
1113
|
if (!isInstalledProjectSkill(skill)) {
|
|
1019
1114
|
skill = enhanceSkill(skill);
|
|
@@ -1035,7 +1130,7 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
1035
1130
|
prioritySearchDirs.push(...await getPluginSkillPaths(searchPath));
|
|
1036
1131
|
const tryAddSkillAt = async (skillDir) => {
|
|
1037
1132
|
if (!await hasSkillMd(skillDir)) return false;
|
|
1038
|
-
let skill = await
|
|
1133
|
+
let skill = await parseSkillAt(skillDir);
|
|
1039
1134
|
if (!skill || seenNames.has(skill.name)) return true;
|
|
1040
1135
|
if (isInstalledProjectSkill(skill)) return true;
|
|
1041
1136
|
skill = enhanceSkill(skill);
|
|
@@ -1065,7 +1160,7 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
1065
1160
|
if (skills.length === 0 || options?.fullDepth) {
|
|
1066
1161
|
const allSkillDirs = await findSkillDirs(searchPath);
|
|
1067
1162
|
for (const skillDir of allSkillDirs) {
|
|
1068
|
-
let skill = await
|
|
1163
|
+
let skill = await parseSkillAt(skillDir);
|
|
1069
1164
|
if (skill && !seenNames.has(skill.name) && !isInstalledProjectSkill(skill)) {
|
|
1070
1165
|
skill = enhanceSkill(skill);
|
|
1071
1166
|
skills.push(skill);
|
|
@@ -1093,6 +1188,7 @@ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude"
|
|
|
1093
1188
|
const vibeHome = process.env.VIBE_HOME?.trim() || join(home, ".vibe");
|
|
1094
1189
|
const hermesHome = process.env.HERMES_HOME?.trim() || join(home, ".hermes");
|
|
1095
1190
|
const autohandHome = process.env.AUTOHAND_HOME?.trim() || join(home, ".autohand");
|
|
1191
|
+
const grokHome = process.env.GROK_HOME?.trim() || join(home, ".grok");
|
|
1096
1192
|
const zedAppDataHome = process.env.APPDATA?.trim();
|
|
1097
1193
|
const zedFlatpakConfigHome = process.env.FLATPAK_XDG_CONFIG_HOME?.trim();
|
|
1098
1194
|
function packageJsonHasDependency(packageJsonPath, dependencyName) {
|
|
@@ -1112,6 +1208,9 @@ function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
|
|
|
1112
1208
|
function isZCodeInstalled(homeDir = home, pathExists = existsSync) {
|
|
1113
1209
|
return pathExists(join(homeDir, ".zcode")) || pathExists("/Applications/ZCode.app");
|
|
1114
1210
|
}
|
|
1211
|
+
function isKimchiInstalled(homeDir = home, pathExists = existsSync) {
|
|
1212
|
+
return pathExists(join(homeDir, ".config", "kimchi"));
|
|
1213
|
+
}
|
|
1115
1214
|
const agents = {
|
|
1116
1215
|
"aider-desk": {
|
|
1117
1216
|
name: "aider-desk",
|
|
@@ -1395,6 +1494,15 @@ const agents = {
|
|
|
1395
1494
|
return existsSync(join(configHome, "goose"));
|
|
1396
1495
|
}
|
|
1397
1496
|
},
|
|
1497
|
+
grok: {
|
|
1498
|
+
name: "grok",
|
|
1499
|
+
displayName: "Grok Build",
|
|
1500
|
+
skillsDir: ".grok/skills",
|
|
1501
|
+
globalSkillsDir: join(grokHome, "skills"),
|
|
1502
|
+
detectInstalled: async () => {
|
|
1503
|
+
return existsSync(grokHome);
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1398
1506
|
"hermes-agent": {
|
|
1399
1507
|
name: "hermes-agent",
|
|
1400
1508
|
displayName: "Hermes Agent",
|
|
@@ -1449,6 +1557,15 @@ const agents = {
|
|
|
1449
1557
|
return existsSync(join(home, ".kilocode"));
|
|
1450
1558
|
}
|
|
1451
1559
|
},
|
|
1560
|
+
kimchi: {
|
|
1561
|
+
name: "kimchi",
|
|
1562
|
+
displayName: "Kimchi",
|
|
1563
|
+
skillsDir: ".kimchi/skills",
|
|
1564
|
+
globalSkillsDir: join(home, ".config", "kimchi", "harness", "skills"),
|
|
1565
|
+
detectInstalled: async () => {
|
|
1566
|
+
return isKimchiInstalled();
|
|
1567
|
+
}
|
|
1568
|
+
},
|
|
1452
1569
|
"kimi-code-cli": {
|
|
1453
1570
|
name: "kimi-code-cli",
|
|
1454
1571
|
displayName: "Kimi Code CLI",
|
|
@@ -1809,13 +1926,13 @@ const SKILLS_SUBDIR = "skills";
|
|
|
1809
1926
|
function sanitizeName(name) {
|
|
1810
1927
|
return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
|
|
1811
1928
|
}
|
|
1812
|
-
function isPathSafe$
|
|
1929
|
+
function isPathSafe$2(basePath, targetPath) {
|
|
1813
1930
|
const normalizedBase = normalize(resolve(basePath));
|
|
1814
1931
|
const normalizedTarget = normalize(resolve(targetPath));
|
|
1815
1932
|
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
1816
1933
|
}
|
|
1817
1934
|
function pathsOverlap(pathA, pathB) {
|
|
1818
|
-
return isPathSafe$
|
|
1935
|
+
return isPathSafe$2(pathA, pathB) || isPathSafe$2(pathB, pathA);
|
|
1819
1936
|
}
|
|
1820
1937
|
async function isDirEntryOrSymlinkToDir(entry, entryPath) {
|
|
1821
1938
|
if (entry.isDirectory()) return true;
|
|
@@ -1909,13 +2026,13 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1909
2026
|
const canonicalDir = join(canonicalBase, skillName);
|
|
1910
2027
|
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent);
|
|
1911
2028
|
const agentDir = join(agentBase, skillName);
|
|
1912
|
-
if (!isPathSafe$
|
|
2029
|
+
if (!isPathSafe$2(canonicalBase, canonicalDir)) return {
|
|
1913
2030
|
success: false,
|
|
1914
2031
|
path: agentDir,
|
|
1915
2032
|
mode: installMode,
|
|
1916
2033
|
error: "Invalid skill name: potential path traversal detected"
|
|
1917
2034
|
};
|
|
1918
|
-
if (!isPathSafe$
|
|
2035
|
+
if (!isPathSafe$2(agentBase, agentDir)) return {
|
|
1919
2036
|
success: false,
|
|
1920
2037
|
path: agentDir,
|
|
1921
2038
|
mode: installMode,
|
|
@@ -2052,7 +2169,7 @@ async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
|
2052
2169
|
if (options.global && agent.globalSkillsDir === void 0) return false;
|
|
2053
2170
|
const targetBase = options.global ? agent.globalSkillsDir : agentType === "eve" && options.eveSubagent ? getEveSubagentSkillsDir(options.eveSubagent, options.cwd) : join(options.cwd || process.cwd(), agent.skillsDir);
|
|
2054
2171
|
const skillDir = join(targetBase, sanitized);
|
|
2055
|
-
if (!isPathSafe$
|
|
2172
|
+
if (!isPathSafe$2(targetBase, skillDir)) return false;
|
|
2056
2173
|
try {
|
|
2057
2174
|
await access(skillDir);
|
|
2058
2175
|
return true;
|
|
@@ -2066,14 +2183,14 @@ function getInstallPath(skillName, agentType, options = {}) {
|
|
|
2066
2183
|
const sanitized = sanitizeName(skillName);
|
|
2067
2184
|
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd, options.eveSubagent);
|
|
2068
2185
|
const installPath = join(targetBase, sanitized);
|
|
2069
|
-
if (!isPathSafe$
|
|
2186
|
+
if (!isPathSafe$2(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
2070
2187
|
return installPath;
|
|
2071
2188
|
}
|
|
2072
2189
|
function getCanonicalPath(skillName, options = {}) {
|
|
2073
2190
|
const sanitized = sanitizeName(skillName);
|
|
2074
2191
|
const canonicalBase = options.agent === "eve" ? getAgentBaseDir("eve", options.global ?? false, options.cwd, options.eveSubagent) : getCanonicalSkillsDir(options.global ?? false, options.cwd);
|
|
2075
2192
|
const canonicalPath = join(canonicalBase, sanitized);
|
|
2076
|
-
if (!isPathSafe$
|
|
2193
|
+
if (!isPathSafe$2(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
2077
2194
|
return canonicalPath;
|
|
2078
2195
|
}
|
|
2079
2196
|
async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
@@ -2093,13 +2210,13 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
|
2093
2210
|
const canonicalDir = join(canonicalBase, skillName);
|
|
2094
2211
|
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent);
|
|
2095
2212
|
const agentDir = join(agentBase, skillName);
|
|
2096
|
-
if (!isPathSafe$
|
|
2213
|
+
if (!isPathSafe$2(canonicalBase, canonicalDir)) return {
|
|
2097
2214
|
success: false,
|
|
2098
2215
|
path: agentDir,
|
|
2099
2216
|
mode: installMode,
|
|
2100
2217
|
error: "Invalid skill name: potential path traversal detected"
|
|
2101
2218
|
};
|
|
2102
|
-
if (!isPathSafe$
|
|
2219
|
+
if (!isPathSafe$2(agentBase, agentDir)) return {
|
|
2103
2220
|
success: false,
|
|
2104
2221
|
path: agentDir,
|
|
2105
2222
|
mode: installMode,
|
|
@@ -2108,7 +2225,7 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
|
2108
2225
|
async function writeSkillFiles(targetDir) {
|
|
2109
2226
|
for (const [filePath, content] of skill.files) {
|
|
2110
2227
|
const fullPath = join(targetDir, filePath);
|
|
2111
|
-
if (!isPathSafe$
|
|
2228
|
+
if (!isPathSafe$2(targetDir, fullPath)) continue;
|
|
2112
2229
|
const parentDir = dirname(fullPath);
|
|
2113
2230
|
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
2114
2231
|
await writeFile(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" && typeof content === "string" ? stripIgnoredEveFrontmatter(content) : content);
|
|
@@ -2174,7 +2291,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2174
2291
|
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent);
|
|
2175
2292
|
if (agentType === "eve" && !isEvePackagedSkill(skill.files)) {
|
|
2176
2293
|
const flatSkillPath = join(agentBase, toEveFlatSkillFileName(skill.installName));
|
|
2177
|
-
if (!isPathSafe$
|
|
2294
|
+
if (!isPathSafe$2(agentBase, flatSkillPath)) return {
|
|
2178
2295
|
success: false,
|
|
2179
2296
|
path: flatSkillPath,
|
|
2180
2297
|
mode: installMode,
|
|
@@ -2204,13 +2321,13 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2204
2321
|
const canonicalBase = agentType === "eve" && installMode === "symlink" ? getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
|
|
2205
2322
|
const canonicalDir = join(canonicalBase, skillName);
|
|
2206
2323
|
const agentDir = join(agentBase, skillName);
|
|
2207
|
-
if (!isPathSafe$
|
|
2324
|
+
if (!isPathSafe$2(canonicalBase, canonicalDir)) return {
|
|
2208
2325
|
success: false,
|
|
2209
2326
|
path: agentDir,
|
|
2210
2327
|
mode: installMode,
|
|
2211
2328
|
error: "Invalid skill name: potential path traversal detected"
|
|
2212
2329
|
};
|
|
2213
|
-
if (!isPathSafe$
|
|
2330
|
+
if (!isPathSafe$2(agentBase, agentDir)) return {
|
|
2214
2331
|
success: false,
|
|
2215
2332
|
path: agentDir,
|
|
2216
2333
|
mode: installMode,
|
|
@@ -2219,7 +2336,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2219
2336
|
async function writeSkillFiles(targetDir) {
|
|
2220
2337
|
for (const file of skill.files) {
|
|
2221
2338
|
const fullPath = join(targetDir, file.path);
|
|
2222
|
-
if (!isPathSafe$
|
|
2339
|
+
if (!isPathSafe$2(targetDir, fullPath)) continue;
|
|
2223
2340
|
const parentDir = dirname(fullPath);
|
|
2224
2341
|
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
2225
2342
|
await writeFile(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
|
|
@@ -2244,7 +2361,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2244
2361
|
mode: "symlink"
|
|
2245
2362
|
};
|
|
2246
2363
|
if (!isGlobal && !isUniversalAgent(agentType)) {
|
|
2247
|
-
if (!existsSync(join(cwd, agents[agentType].skillsDir.split("/")[0]))) return {
|
|
2364
|
+
if (!existsSync(join(cwd, agents[agentType].skillsDir.split("/")[0])) && agentType !== "claude-code") return {
|
|
2248
2365
|
success: true,
|
|
2249
2366
|
path: canonicalDir,
|
|
2250
2367
|
canonicalPath: canonicalDir,
|
|
@@ -2368,7 +2485,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
2368
2485
|
]));
|
|
2369
2486
|
for (const possibleName of possibleNames) {
|
|
2370
2487
|
const agentSkillDir = join(agentBase, possibleName);
|
|
2371
|
-
if (!isPathSafe$
|
|
2488
|
+
if (!isPathSafe$2(agentBase, agentSkillDir)) continue;
|
|
2372
2489
|
try {
|
|
2373
2490
|
await access(agentSkillDir);
|
|
2374
2491
|
found = true;
|
|
@@ -2380,7 +2497,7 @@ async function listInstalledSkills(options = {}) {
|
|
|
2380
2497
|
for (const agentEntry of agentEntries) {
|
|
2381
2498
|
const candidateDir = join(agentBase, agentEntry.name);
|
|
2382
2499
|
if (!await isDirEntryOrSymlinkToDir(agentEntry, candidateDir)) continue;
|
|
2383
|
-
if (!isPathSafe$
|
|
2500
|
+
if (!isPathSafe$2(agentBase, candidateDir)) continue;
|
|
2384
2501
|
try {
|
|
2385
2502
|
const candidateSkillMd = join(candidateDir, "SKILL.md");
|
|
2386
2503
|
await stat(candidateSkillMd);
|
|
@@ -2520,9 +2637,220 @@ var ProviderRegistryImpl = class {
|
|
|
2520
2637
|
}
|
|
2521
2638
|
};
|
|
2522
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
|
+
}
|
|
2523
2850
|
const DISCOVERY_SCHEMA_V2 = "https://schemas.agentskills.io/discovery/0.2.0/schema.json";
|
|
2524
2851
|
const MAX_ARCHIVE_UNPACKED_BYTES = 50 * 1024 * 1024;
|
|
2525
2852
|
const MAX_ARCHIVE_FILES = 1e3;
|
|
2853
|
+
const DISCOVERY_TIMEOUT_MS = 1e4;
|
|
2526
2854
|
var WellKnownProvider = class {
|
|
2527
2855
|
id = "well-known";
|
|
2528
2856
|
displayName = "Well-Known Skills";
|
|
@@ -2552,6 +2880,7 @@ var WellKnownProvider = class {
|
|
|
2552
2880
|
try {
|
|
2553
2881
|
const parsed = new URL(baseUrl);
|
|
2554
2882
|
const basePath = parsed.pathname.replace(/\/$/, "");
|
|
2883
|
+
const signal = AbortSignal.timeout(DISCOVERY_TIMEOUT_MS);
|
|
2555
2884
|
const urlsToTry = [];
|
|
2556
2885
|
for (const wellKnownPath of this.WELL_KNOWN_PATHS) {
|
|
2557
2886
|
urlsToTry.push({
|
|
@@ -2567,7 +2896,7 @@ var WellKnownProvider = class {
|
|
|
2567
2896
|
}
|
|
2568
2897
|
const candidates = [];
|
|
2569
2898
|
for (const { indexUrl, baseUrl: resolvedBase, wellKnownPath } of urlsToTry) try {
|
|
2570
|
-
const response = await fetch(indexUrl);
|
|
2899
|
+
const response = await fetch(indexUrl, { signal });
|
|
2571
2900
|
if (!response.ok) continue;
|
|
2572
2901
|
const rawIndex = await response.json();
|
|
2573
2902
|
const normalized = this.normalizeIndex(rawIndex, indexUrl, wellKnownPath);
|
|
@@ -2834,7 +3163,10 @@ var WellKnownProvider = class {
|
|
|
2834
3163
|
return `sha256:${createHash$1("sha256").update(bytes).digest("hex")}`;
|
|
2835
3164
|
}
|
|
2836
3165
|
extractArchive(bytes, artifactUrl, contentType) {
|
|
2837
|
-
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
|
+
}));
|
|
2838
3170
|
if (this.isTarGzArchive(bytes, artifactUrl, contentType)) return this.extractTarGz(bytes);
|
|
2839
3171
|
throw new Error("Unsupported archive format");
|
|
2840
3172
|
}
|
|
@@ -2894,54 +3226,6 @@ var WellKnownProvider = class {
|
|
|
2894
3226
|
const nul = slice.indexOf(0);
|
|
2895
3227
|
return new TextDecoder().decode(nul >= 0 ? slice.subarray(0, nul) : slice);
|
|
2896
3228
|
}
|
|
2897
|
-
extractZip(bytes) {
|
|
2898
|
-
const buffer = Buffer.from(bytes);
|
|
2899
|
-
const eocdOffset = this.findZipEndOfCentralDirectory(buffer);
|
|
2900
|
-
if (eocdOffset < 0) throw new Error("Invalid zip archive");
|
|
2901
|
-
const totalEntries = buffer.readUInt16LE(eocdOffset + 10);
|
|
2902
|
-
const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + 16);
|
|
2903
|
-
const files = /* @__PURE__ */ new Map();
|
|
2904
|
-
const runningTotal = { bytes: 0 };
|
|
2905
|
-
let offset = centralDirectoryOffset;
|
|
2906
|
-
for (let i = 0; i < totalEntries; i++) {
|
|
2907
|
-
if (buffer.readUInt32LE(offset) !== 33639248) throw new Error("Invalid zip directory");
|
|
2908
|
-
const flags = buffer.readUInt16LE(offset + 8);
|
|
2909
|
-
const method = buffer.readUInt16LE(offset + 10);
|
|
2910
|
-
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
2911
|
-
const uncompressedSize = buffer.readUInt32LE(offset + 24);
|
|
2912
|
-
const fileNameLength = buffer.readUInt16LE(offset + 28);
|
|
2913
|
-
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
2914
|
-
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
2915
|
-
const externalAttributes = buffer.readUInt32LE(offset + 38);
|
|
2916
|
-
const localHeaderOffset = buffer.readUInt32LE(offset + 42);
|
|
2917
|
-
const nameStart = offset + 46;
|
|
2918
|
-
const rawName = buffer.subarray(nameStart, nameStart + fileNameLength);
|
|
2919
|
-
const fileName = new TextDecoder(flags & 2048 ? "utf-8" : void 0).decode(rawName);
|
|
2920
|
-
offset = nameStart + fileNameLength + extraLength + commentLength;
|
|
2921
|
-
if (fileName.endsWith("/")) continue;
|
|
2922
|
-
if (flags & 1) throw new Error("Encrypted zip entries are not supported");
|
|
2923
|
-
const fileType = externalAttributes >>> 16 & 61440;
|
|
2924
|
-
if (fileType === 40960 || fileType === 4096) throw new Error("Archive links are not supported");
|
|
2925
|
-
if (buffer.readUInt32LE(localHeaderOffset) !== 67324752) throw new Error("Invalid zip local header");
|
|
2926
|
-
const localFileNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
|
|
2927
|
-
const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
|
|
2928
|
-
const dataStart = localHeaderOffset + 30 + localFileNameLength + localExtraLength;
|
|
2929
|
-
const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
|
|
2930
|
-
let content;
|
|
2931
|
-
if (method === 0) content = compressed;
|
|
2932
|
-
else if (method === 8) content = inflateRawSync(compressed);
|
|
2933
|
-
else throw new Error(`Unsupported zip compression method: ${method}`);
|
|
2934
|
-
if (content.byteLength !== uncompressedSize) throw new Error("Zip entry size mismatch");
|
|
2935
|
-
this.addArchiveFile(files, fileName, new Uint8Array(content), runningTotal);
|
|
2936
|
-
}
|
|
2937
|
-
if (!files.has("SKILL.md")) throw new Error("Archive missing root SKILL.md");
|
|
2938
|
-
return files;
|
|
2939
|
-
}
|
|
2940
|
-
findZipEndOfCentralDirectory(buffer) {
|
|
2941
|
-
const minOffset = Math.max(0, buffer.length - 65535 - 22);
|
|
2942
|
-
for (let offset = buffer.length - 22; offset >= minOffset; offset--) if (buffer.readUInt32LE(offset) === 101010256) return offset;
|
|
2943
|
-
return -1;
|
|
2944
|
-
}
|
|
2945
3229
|
toRawUrl(url) {
|
|
2946
3230
|
try {
|
|
2947
3231
|
const parsed = new URL(url);
|
|
@@ -2970,6 +3254,187 @@ var WellKnownProvider = class {
|
|
|
2970
3254
|
}
|
|
2971
3255
|
};
|
|
2972
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
|
+
}
|
|
2973
3438
|
const AGENTS_DIR = ".agents";
|
|
2974
3439
|
const LOCK_FILE = ".skill-lock.json";
|
|
2975
3440
|
const CURRENT_VERSION = 3;
|
|
@@ -3165,9 +3630,11 @@ const PRIORITY_PREFIXES = [
|
|
|
3165
3630
|
".continue/skills/",
|
|
3166
3631
|
".github/skills/",
|
|
3167
3632
|
".goose/skills/",
|
|
3633
|
+
".grok/skills/",
|
|
3168
3634
|
".iflow/skills/",
|
|
3169
3635
|
".junie/skills/",
|
|
3170
3636
|
".kilocode/skills/",
|
|
3637
|
+
".kimchi/skills/",
|
|
3171
3638
|
".kiro/skills/",
|
|
3172
3639
|
".mux/skills/",
|
|
3173
3640
|
".neovate/skills/",
|
|
@@ -3334,7 +3801,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
|
|
|
3334
3801
|
tree
|
|
3335
3802
|
};
|
|
3336
3803
|
}
|
|
3337
|
-
var version$1 = "1.5.
|
|
3804
|
+
var version$1 = "1.5.21";
|
|
3338
3805
|
const isCancelled$1 = (value) => typeof value === "symbol";
|
|
3339
3806
|
const EVE_AGENT_LABEL = "eve agent";
|
|
3340
3807
|
async function isSourcePrivate(source) {
|
|
@@ -3554,12 +4021,11 @@ async function selectAgentsInteractive(options) {
|
|
|
3554
4021
|
}
|
|
3555
4022
|
setVersion(version$1);
|
|
3556
4023
|
async function handleWellKnownSkills(source, url, options, spinner) {
|
|
3557
|
-
spinner.start("Discovering skills from well-known endpoint
|
|
3558
|
-
const skills = await wellKnownProvider.fetchAllSkills(url);
|
|
4024
|
+
spinner.start("Discovering skills from well-known endpoint...");
|
|
4025
|
+
const skills = await wellKnownProvider.fetchAllSkills(url).catch(() => []);
|
|
3559
4026
|
if (skills.length === 0) {
|
|
3560
|
-
spinner.stop(import_picocolors.default.
|
|
3561
|
-
|
|
3562
|
-
process.exit(1);
|
|
4027
|
+
spinner.stop(import_picocolors.default.dim("No well-known skills found; trying direct download..."));
|
|
4028
|
+
return false;
|
|
3563
4029
|
}
|
|
3564
4030
|
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3565
4031
|
for (const skill of skills) {
|
|
@@ -3764,6 +4230,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
3764
4230
|
agents: targetAgents.join(","),
|
|
3765
4231
|
...installGlobally && { global: "1" },
|
|
3766
4232
|
skillFiles: JSON.stringify(skillFiles),
|
|
4233
|
+
installUrl: url,
|
|
3767
4234
|
metadata: options.metadata,
|
|
3768
4235
|
sourceType: "well-known"
|
|
3769
4236
|
});
|
|
@@ -3839,6 +4306,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
3839
4306
|
console.log();
|
|
3840
4307
|
outro(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
|
|
3841
4308
|
await promptForFindSkills(options, targetAgents);
|
|
4309
|
+
return true;
|
|
3842
4310
|
}
|
|
3843
4311
|
async function runAdd(args, options = {}) {
|
|
3844
4312
|
const source = args[0];
|
|
@@ -3882,17 +4350,19 @@ async function runAdd(args, options = {}) {
|
|
|
3882
4350
|
const spinner$3 = spinner();
|
|
3883
4351
|
spinner$3.start("Parsing source…");
|
|
3884
4352
|
const parsed = parseSource(source);
|
|
4353
|
+
let directDownload = parsed.type === "download";
|
|
3885
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)}` : ""}`);
|
|
3886
|
-
const ownerRepoRaw = getOwnerRepo(parsed);
|
|
4355
|
+
const ownerRepoRaw = parsed.type === "well-known" || parsed.type === "download" ? null : getOwnerRepo(parsed);
|
|
3887
4356
|
const repoPrivacyPromise = (() => {
|
|
4357
|
+
if (parsed.type !== "github") return Promise.resolve(null);
|
|
3888
4358
|
if (!ownerRepoRaw) return Promise.resolve(null);
|
|
3889
4359
|
const ownerRepo = parseOwnerRepo(ownerRepoRaw);
|
|
3890
4360
|
if (!ownerRepo) return Promise.resolve(null);
|
|
3891
4361
|
return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null);
|
|
3892
4362
|
})();
|
|
3893
4363
|
if (parsed.type === "well-known") {
|
|
3894
|
-
await handleWellKnownSkills(source, parsed.url, options, spinner$3);
|
|
3895
|
-
|
|
4364
|
+
if (await handleWellKnownSkills(source, parsed.url, options, spinner$3)) return;
|
|
4365
|
+
directDownload = true;
|
|
3896
4366
|
}
|
|
3897
4367
|
if (parsed.skillFilter) {
|
|
3898
4368
|
options.skill = options.skill || [];
|
|
@@ -3914,6 +4384,16 @@ async function runAdd(args, options = {}) {
|
|
|
3914
4384
|
includeInternal,
|
|
3915
4385
|
fullDepth: options.fullDepth
|
|
3916
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
|
+
});
|
|
3917
4397
|
} else if (parsed.type === "github" && !options.fullDepth) {
|
|
3918
4398
|
const BLOB_ALLOWED_OWNERS = [
|
|
3919
4399
|
"vercel",
|
|
@@ -4306,9 +4786,9 @@ async function runAdd(args, options = {}) {
|
|
|
4306
4786
|
else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
|
|
4307
4787
|
else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
|
|
4308
4788
|
else continue;
|
|
4309
|
-
const normalizedSource = getOwnerRepo(parsed);
|
|
4310
|
-
const lockSource = getLockSource(parsed.url, normalizedSource);
|
|
4311
|
-
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);
|
|
4312
4792
|
if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
|
|
4313
4793
|
if (await repoPrivacyPromise === false) track({
|
|
4314
4794
|
event: "install",
|
|
@@ -4359,7 +4839,7 @@ async function runAdd(args, options = {}) {
|
|
|
4359
4839
|
} catch {}
|
|
4360
4840
|
}
|
|
4361
4841
|
}
|
|
4362
|
-
if (successful.length > 0 && !installGlobally) {
|
|
4842
|
+
if (successful.length > 0 && !installGlobally && !directDownload) {
|
|
4363
4843
|
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
4364
4844
|
const eveSubagents = targetAgents.includes("eve") ? eveSubagentTargets.map((s) => s ?? "") : void 0;
|
|
4365
4845
|
const recordSubagents = eveSubagents && (eveSubagents.length > 1 || eveSubagents.some((s) => s !== ""));
|
|
@@ -5155,7 +5635,7 @@ function deriveSkillFolder(skillPath) {
|
|
|
5155
5635
|
return folder;
|
|
5156
5636
|
}
|
|
5157
5637
|
function supportsAppendedSubpath(source) {
|
|
5158
|
-
if (source.startsWith("git@")) return false;
|
|
5638
|
+
if (source.startsWith("git@") || source.startsWith("ssh://")) return false;
|
|
5159
5639
|
if (source.endsWith(".git")) return false;
|
|
5160
5640
|
if (source.startsWith("http://") || source.startsWith("https://")) try {
|
|
5161
5641
|
const host = new URL(source).hostname;
|
|
@@ -5173,6 +5653,11 @@ function getLocalSource(entry) {
|
|
|
5173
5653
|
if ((entry.sourceType === "git" || entry.sourceType === "gitlab") && isBareShorthand(entry.source)) return null;
|
|
5174
5654
|
return entry.source;
|
|
5175
5655
|
}
|
|
5656
|
+
function shouldUseFullDepthForUpdate(entry) {
|
|
5657
|
+
if (!entry.skillPath) return false;
|
|
5658
|
+
const source = entry.sourceType && entry.sourceType !== "github" ? getLocalSource(entry) : entry.source;
|
|
5659
|
+
return source !== null && !supportsAppendedSubpath(source);
|
|
5660
|
+
}
|
|
5176
5661
|
function appendFolderAndRef(source, skillPath, ref) {
|
|
5177
5662
|
if (!supportsAppendedSubpath(source)) return formatSourceInput(source, ref);
|
|
5178
5663
|
const folder = deriveSkillFolder(skillPath);
|
|
@@ -5296,18 +5781,26 @@ async function runList(args) {
|
|
|
5296
5781
|
global: scope,
|
|
5297
5782
|
agentFilter
|
|
5298
5783
|
});
|
|
5784
|
+
const cwd = process.cwd();
|
|
5785
|
+
const lockedSkills = scope ? await getAllLockedSkills() : (await readLocalLock(cwd)).skills;
|
|
5786
|
+
const lockEntriesBySanitizedName = new Map(Object.entries(lockedSkills).map(([name, entry]) => [sanitizeName(name), entry]));
|
|
5787
|
+
const getLockEntry = (skillName) => lockedSkills[skillName] ?? lockEntriesBySanitizedName.get(sanitizeName(skillName));
|
|
5299
5788
|
if (options.json) {
|
|
5300
|
-
const jsonOutput = installedSkills.map((skill) =>
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5789
|
+
const jsonOutput = installedSkills.map((skill) => {
|
|
5790
|
+
const lockEntry = getLockEntry(skill.name);
|
|
5791
|
+
return {
|
|
5792
|
+
name: skill.name,
|
|
5793
|
+
path: skill.canonicalPath,
|
|
5794
|
+
scope: skill.scope,
|
|
5795
|
+
agents: skill.agents.map((a) => agents[a].displayName),
|
|
5796
|
+
source: lockEntry?.source ?? null,
|
|
5797
|
+
sourceUrl: lockEntry?.sourceUrl ?? null,
|
|
5798
|
+
sourceType: lockEntry?.sourceType ?? null
|
|
5799
|
+
};
|
|
5800
|
+
});
|
|
5306
5801
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
5307
5802
|
return;
|
|
5308
5803
|
}
|
|
5309
|
-
const lockedSkills = await getAllLockedSkills();
|
|
5310
|
-
const cwd = process.cwd();
|
|
5311
5804
|
const scopeLabel = scope ? "Global" : "Project";
|
|
5312
5805
|
if (installedSkills.length === 0) {
|
|
5313
5806
|
if (options.json) {
|
|
@@ -5326,14 +5819,17 @@ async function runList(args) {
|
|
|
5326
5819
|
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$2}`;
|
|
5327
5820
|
const paddedName = sanitizeMetadata(skill.name).padEnd(maxNameLength);
|
|
5328
5821
|
const paddedPath = shortPath.padEnd(maxPathLength);
|
|
5329
|
-
|
|
5822
|
+
const source = getLockEntry(skill.name)?.source ?? null;
|
|
5823
|
+
const sourceLabel = source ? sanitizeMetadata(source) : "local";
|
|
5824
|
+
console.log(`${prefix}${CYAN}${paddedName}${RESET$2} ${DIM$2}${paddedPath}${RESET$2}`);
|
|
5825
|
+
console.log(`${prefix} ${DIM$2}Agents:${RESET$2} ${agentInfo} ${DIM$2}Source:${RESET$2} ${sourceLabel}`);
|
|
5330
5826
|
}
|
|
5331
5827
|
console.log(`${BOLD$2}${scopeLabel} Skills${RESET$2}`);
|
|
5332
5828
|
console.log();
|
|
5333
5829
|
const groupedSkills = {};
|
|
5334
5830
|
const ungroupedSkills = [];
|
|
5335
5831
|
for (const skill of installedSkills) {
|
|
5336
|
-
const lockEntry =
|
|
5832
|
+
const lockEntry = getLockEntry(skill.name);
|
|
5337
5833
|
if (lockEntry?.pluginName) {
|
|
5338
5834
|
const group = lockEntry.pluginName;
|
|
5339
5835
|
if (!groupedSkills[group]) groupedSkills[group] = [];
|
|
@@ -5385,6 +5881,17 @@ async function runList(args) {
|
|
|
5385
5881
|
console.log();
|
|
5386
5882
|
}
|
|
5387
5883
|
}
|
|
5884
|
+
function resolveSkillsToRemove(requested, folderNames, lockKeys = []) {
|
|
5885
|
+
const identityBySanitized = /* @__PURE__ */ new Map();
|
|
5886
|
+
for (const folder of folderNames) identityBySanitized.set(sanitizeName(folder), folder);
|
|
5887
|
+
for (const key of lockKeys) identityBySanitized.set(sanitizeName(key), key);
|
|
5888
|
+
const matched = /* @__PURE__ */ new Set();
|
|
5889
|
+
for (const name of requested) {
|
|
5890
|
+
const hit = identityBySanitized.get(sanitizeName(name));
|
|
5891
|
+
if (hit) matched.add(hit);
|
|
5892
|
+
}
|
|
5893
|
+
return Array.from(matched);
|
|
5894
|
+
}
|
|
5388
5895
|
async function removeCommand(skillNames, options) {
|
|
5389
5896
|
const agentResult = await detectAgent();
|
|
5390
5897
|
if (agentResult.isAgent) {
|
|
@@ -5414,7 +5921,10 @@ async function removeCommand(skillNames, options) {
|
|
|
5414
5921
|
}
|
|
5415
5922
|
const installedSkills = Array.from(skillNamesSet).sort();
|
|
5416
5923
|
spinner$1.stop(`Found ${installedSkills.length} unique installed skill(s)`);
|
|
5417
|
-
|
|
5924
|
+
const lockSkillsKeys = isGlobal ? Object.keys((await readSkillLock()).skills) : Object.keys((await readLocalLock(cwd)).skills);
|
|
5925
|
+
const requestedSkills = options.all ? [...installedSkills, ...lockSkillsKeys] : skillNames;
|
|
5926
|
+
const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys) : [];
|
|
5927
|
+
if (installedSkills.length === 0 && resolvedRequestedSkills.length === 0) {
|
|
5418
5928
|
outro(import_picocolors.default.yellow("No skills found to remove."));
|
|
5419
5929
|
return;
|
|
5420
5930
|
}
|
|
@@ -5428,9 +5938,9 @@ async function removeCommand(skillNames, options) {
|
|
|
5428
5938
|
}
|
|
5429
5939
|
}
|
|
5430
5940
|
let selectedSkills = [];
|
|
5431
|
-
if (options.all) selectedSkills =
|
|
5941
|
+
if (options.all) selectedSkills = resolvedRequestedSkills;
|
|
5432
5942
|
else if (skillNames.length > 0) {
|
|
5433
|
-
selectedSkills =
|
|
5943
|
+
selectedSkills = resolvedRequestedSkills;
|
|
5434
5944
|
if (selectedSkills.length === 0) {
|
|
5435
5945
|
log.error(`No matching skills found for: ${skillNames.join(", ")}`);
|
|
5436
5946
|
return;
|
|
@@ -5449,7 +5959,7 @@ async function removeCommand(skillNames, options) {
|
|
|
5449
5959
|
cancel("Removal cancelled");
|
|
5450
5960
|
process.exit(0);
|
|
5451
5961
|
}
|
|
5452
|
-
selectedSkills = selected;
|
|
5962
|
+
selectedSkills = resolveSkillsToRemove(selected, installedSkills, lockSkillsKeys);
|
|
5453
5963
|
}
|
|
5454
5964
|
let targetAgents;
|
|
5455
5965
|
if (options.agent && options.agent.length > 0) targetAgents = options.agent;
|
|
@@ -5513,10 +6023,19 @@ async function removeCommand(skillNames, options) {
|
|
|
5513
6023
|
recursive: true,
|
|
5514
6024
|
force: true
|
|
5515
6025
|
});
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
6026
|
+
let effectiveSource = "local";
|
|
6027
|
+
let effectiveSourceType = "local";
|
|
6028
|
+
if (isGlobal) {
|
|
6029
|
+
const lockEntry = await getSkillFromLock(skillName);
|
|
6030
|
+
effectiveSource = lockEntry?.source || "local";
|
|
6031
|
+
effectiveSourceType = lockEntry?.sourceType || "local";
|
|
6032
|
+
await removeSkillFromLock(skillName);
|
|
6033
|
+
} else {
|
|
6034
|
+
const lockEntry = (await readLocalLock(cwd)).skills[skillName];
|
|
6035
|
+
effectiveSource = lockEntry?.source || "local";
|
|
6036
|
+
effectiveSourceType = lockEntry?.sourceType || "local";
|
|
6037
|
+
await removeSkillFromLocalLock(skillName, cwd);
|
|
6038
|
+
}
|
|
5520
6039
|
results.push({
|
|
5521
6040
|
skill: skillName,
|
|
5522
6041
|
success: true,
|
|
@@ -5786,7 +6305,7 @@ async function updateGlobalSkills(options = {}) {
|
|
|
5786
6305
|
console.log(` ${DIM$1}✗ Failed to fetch tree for ${source}${RESET$1}`);
|
|
5787
6306
|
continue;
|
|
5788
6307
|
}
|
|
5789
|
-
const discoveredPaths =
|
|
6308
|
+
const discoveredPaths = tree.tree.filter((entry) => entry.type === "blob").map((entry) => entry.path);
|
|
5790
6309
|
const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source).map(([name, _]) => name), lock.skills, true, options, discoveredPaths);
|
|
5791
6310
|
const deletedSkillSet = new Set(deletedSkills);
|
|
5792
6311
|
for (const { name: skillName, entry } of itemsForSource) {
|
|
@@ -5801,7 +6320,7 @@ async function updateGlobalSkills(options = {}) {
|
|
|
5801
6320
|
continue;
|
|
5802
6321
|
}
|
|
5803
6322
|
tempDir = await cloneRepo(sourceUrl, firstEntry.ref);
|
|
5804
|
-
const discoveredPaths = (await discoverSkills(tempDir)).map((skill) => {
|
|
6323
|
+
const discoveredPaths = (await discoverSkills(tempDir, void 0, { fullDepth: true })).map((skill) => {
|
|
5805
6324
|
return join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/");
|
|
5806
6325
|
});
|
|
5807
6326
|
const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source).map(([name, _]) => name), lock.skills, true, options, discoveredPaths);
|
|
@@ -5866,10 +6385,12 @@ async function updateGlobalSkills(options = {}) {
|
|
|
5866
6385
|
console.log(` ${DIM$1}✗ Failed to update ${safeName}: CLI entrypoint not found at ${cliEntry}${RESET$1}`);
|
|
5867
6386
|
continue;
|
|
5868
6387
|
}
|
|
6388
|
+
const fullDepthArgs = shouldUseFullDepthForUpdate(update.entry) ? ["--full-depth"] : [];
|
|
5869
6389
|
if (spawnSync(process.execPath, [
|
|
5870
6390
|
cliEntry,
|
|
5871
6391
|
"add",
|
|
5872
6392
|
installUrl,
|
|
6393
|
+
...fullDepthArgs,
|
|
5873
6394
|
"-g",
|
|
5874
6395
|
"-y"
|
|
5875
6396
|
], {
|
|
@@ -5967,7 +6488,7 @@ async function updateProjectSkills(options = {}) {
|
|
|
5967
6488
|
}
|
|
5968
6489
|
try {
|
|
5969
6490
|
tempDir = await cloneRepo(sourceUrl, ref);
|
|
5970
|
-
const discoveredPaths = (await discoverSkills(tempDir)).map((s) => {
|
|
6491
|
+
const discoveredPaths = (await discoverSkills(tempDir, void 0, { fullDepth: true })).map((s) => {
|
|
5971
6492
|
return join(relative(tempDir, s.path), "SKILL.md").split(sep).join("/");
|
|
5972
6493
|
});
|
|
5973
6494
|
deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredPaths);
|
|
@@ -5987,6 +6508,7 @@ async function updateProjectSkills(options = {}) {
|
|
|
5987
6508
|
continue;
|
|
5988
6509
|
}
|
|
5989
6510
|
const subagentArgs = skill.entry.subagents?.length ? ["--subagent", ...skill.entry.subagents.map((s) => s === "" ? "root" : s)] : [];
|
|
6511
|
+
const fullDepthArgs = shouldUseFullDepthForUpdate(skill.entry) ? ["--full-depth"] : [];
|
|
5990
6512
|
if (spawnSync(process.execPath, [
|
|
5991
6513
|
cliEntry,
|
|
5992
6514
|
"add",
|
|
@@ -5994,6 +6516,7 @@ async function updateProjectSkills(options = {}) {
|
|
|
5994
6516
|
"--skill",
|
|
5995
6517
|
skill.name,
|
|
5996
6518
|
...subagentArgs,
|
|
6519
|
+
...fullDepthArgs,
|
|
5997
6520
|
"-y"
|
|
5998
6521
|
], {
|
|
5999
6522
|
stdio: [
|
|
@@ -6180,11 +6703,35 @@ async function runUse(sourceArgs, options = {}, parseErrors = []) {
|
|
|
6180
6703
|
const selector = resolveSelector(parsed.skillFilter, options.skill);
|
|
6181
6704
|
const includeInternal = selector !== void 0;
|
|
6182
6705
|
let selectedSkill;
|
|
6183
|
-
if (parsed.type === "well-known")
|
|
6184
|
-
|
|
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 {
|
|
6185
6725
|
let skills;
|
|
6186
6726
|
let blobResult = null;
|
|
6187
|
-
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") {
|
|
6188
6735
|
if (!existsSync(parsed.localPath)) fail(`Local path does not exist: ${parsed.localPath}`);
|
|
6189
6736
|
skills = await discoverSkills(parsed.localPath, parsed.subpath, {
|
|
6190
6737
|
includeInternal,
|
|
@@ -6674,6 +7221,11 @@ async function main() {
|
|
|
6674
7221
|
}
|
|
6675
7222
|
const command = args[0];
|
|
6676
7223
|
const restArgs = args.slice(1);
|
|
7224
|
+
if (command !== "--help" && command !== "-h" && command !== "--version" && command !== "-v" && (restArgs.includes("--help") || restArgs.includes("-h"))) {
|
|
7225
|
+
if (command === "remove" || command === "rm" || command === "r") showRemoveHelp();
|
|
7226
|
+
else showHelp();
|
|
7227
|
+
return;
|
|
7228
|
+
}
|
|
6677
7229
|
switch (command) {
|
|
6678
7230
|
case "find":
|
|
6679
7231
|
case "search":
|
|
@@ -6713,14 +7265,11 @@ async function main() {
|
|
|
6713
7265
|
}
|
|
6714
7266
|
case "remove":
|
|
6715
7267
|
case "rm":
|
|
6716
|
-
case "r":
|
|
6717
|
-
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
6718
|
-
showRemoveHelp();
|
|
6719
|
-
break;
|
|
6720
|
-
}
|
|
7268
|
+
case "r": {
|
|
6721
7269
|
const { skills, options: removeOptions } = parseRemoveOptions(restArgs);
|
|
6722
7270
|
await removeCommand(skills, removeOptions);
|
|
6723
7271
|
break;
|
|
7272
|
+
}
|
|
6724
7273
|
case "experimental_sync": {
|
|
6725
7274
|
if (!inAgent) showLogo();
|
|
6726
7275
|
const { options: syncOptions } = parseSyncOptions(restArgs);
|
|
@@ -6747,6 +7296,7 @@ async function main() {
|
|
|
6747
7296
|
default:
|
|
6748
7297
|
console.log(`Unknown command: ${command}`);
|
|
6749
7298
|
console.log(`Run ${BOLD}skills --help${RESET} for usage.`);
|
|
7299
|
+
process.exitCode = 1;
|
|
6750
7300
|
}
|
|
6751
7301
|
}
|
|
6752
7302
|
main().finally(() => flushTelemetry().then(() => process.exit(process.exitCode ?? 0)));
|