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/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.hostname === "github.com") return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
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: "github",
198
- url: `https://github.com/${owner}/${repo}.git`,
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: "github",
208
- url: `https://github.com/${owner}/${repo}.git`,
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@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
577
- if (sshMatch) {
578
- const owner = sshMatch[1];
579
- const repo = sshMatch[2];
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@github.com:${owner}/${repo}.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.hostname !== "github.com") return null;
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@github.com:${owner}/${repo}.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.hostname === "github.com";
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
- "github.com"
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
- 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 github.com or ssh -T git@github.com`;
709
- 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 github.com or ssh -T git@github.com`;
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
- const content = await readFile(skillMdPath, "utf-8");
962
- const { data } = parseFrontmatter(content);
963
- if (!data.name || !data.description) return null;
964
- if (typeof data.name !== "string" || typeof data.description !== "string") return null;
965
- const metadata = isRecord(data.metadata) ? data.metadata : void 0;
966
- if (metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
967
- return {
968
- name: sanitizeMetadata(data.name),
969
- description: sanitizeMetadata(data.description),
970
- path: dirname(skillMdPath),
971
- rawContent: content,
972
- metadata
973
- };
974
- } catch {
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 parseSkillMd(join(searchPath, "SKILL.md"), options);
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 parseSkillMd(join(skillDir, "SKILL.md"), options);
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 parseSkillMd(join(skillDir, "SKILL.md"), options);
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$1(basePath, targetPath) {
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$1(pathA, pathB) || isPathSafe$1(pathB, pathA);
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$1(canonicalBase, canonicalDir)) return {
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$1(agentBase, agentDir)) return {
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$1(targetBase, skillDir)) return false;
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$1(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
2186
+ if (!isPathSafe$2(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
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$1(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
2193
+ if (!isPathSafe$2(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
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$1(canonicalBase, canonicalDir)) return {
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$1(agentBase, agentDir)) return {
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$1(targetDir, fullPath)) continue;
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$1(agentBase, flatSkillPath)) return {
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$1(canonicalBase, canonicalDir)) return {
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$1(agentBase, agentDir)) return {
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$1(targetDir, fullPath)) continue;
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$1(agentBase, agentSkillDir)) continue;
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$1(agentBase, candidateDir)) continue;
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 this.extractZip(bytes);
3166
+ if (this.isZipArchive(bytes, artifactUrl, contentType)) return new Map(readZipArchive(bytes, {
3167
+ maxExtractedBytes: MAX_ARCHIVE_UNPACKED_BYTES,
3168
+ maxEntries: MAX_ARCHIVE_FILES
3169
+ }));
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.19";
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.red("No skills found"));
3561
- outro(import_picocolors.default.red("No skills found at this URL. Make sure the server has a /.well-known/agent-skills/index.json or /.well-known/skills/index.json file."));
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
- return;
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
- name: skill.name,
5302
- path: skill.canonicalPath,
5303
- scope: skill.scope,
5304
- agents: skill.agents.map((a) => agents[a].displayName)
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
- console.log(`${prefix}${CYAN}${paddedName}${RESET$2} ${DIM$2}${paddedPath}${RESET$2} ${DIM$2}Agents:${RESET$2} ${agentInfo}`);
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 = lockedSkills[skill.name];
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
- if (installedSkills.length === 0) {
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 = installedSkills;
5941
+ if (options.all) selectedSkills = resolvedRequestedSkills;
5432
5942
  else if (skillNames.length > 0) {
5433
- selectedSkills = installedSkills.filter((s) => skillNames.some((name) => name.toLowerCase() === s.toLowerCase()));
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
- const lockEntry = isGlobal ? await getSkillFromLock(skillName) : null;
5517
- const effectiveSource = lockEntry?.source || "local";
5518
- const effectiveSourceType = lockEntry?.sourceType || "local";
5519
- if (isGlobal) await removeSkillFromLock(skillName);
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 = findSkillMdPaths(tree);
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") selectedSkill = selectWellKnownSkill(await wellKnownProvider.fetchAllSkills(parsed.url), selector, source);
6184
- else {
6706
+ if (parsed.type === "well-known") {
6707
+ const skills = await wellKnownProvider.fetchAllSkills(parsed.url);
6708
+ if (skills.length > 0) selectedSkill = selectWellKnownSkill(skills, selector, source);
6709
+ else {
6710
+ const downloaded = await downloadSource(parsed.url);
6711
+ cloneTempDir = downloaded.tempDir;
6712
+ const selected = selectSkill(await discoverSkills(downloaded.rootDir, void 0, {
6713
+ includeInternal,
6714
+ fullDepth: options.fullDepth
6715
+ }), selector, source);
6716
+ selectedSkill = {
6717
+ kind: "disk",
6718
+ name: selected.name,
6719
+ directoryName: selected.name,
6720
+ rawContent: selected.rawContent,
6721
+ path: selected.path
6722
+ };
6723
+ }
6724
+ } else {
6185
6725
  let skills;
6186
6726
  let blobResult = null;
6187
- if (parsed.type === "local") {
6727
+ if (parsed.type === "download") {
6728
+ const downloaded = await downloadSource(parsed.url);
6729
+ cloneTempDir = downloaded.tempDir;
6730
+ skills = await discoverSkills(downloaded.rootDir, void 0, {
6731
+ includeInternal,
6732
+ fullDepth: options.fullDepth
6733
+ });
6734
+ } else if (parsed.type === "local") {
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)));