skills 1.5.6 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.mjs +397 -79
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -17,7 +17,9 @@ import * as readline from "readline";
17
17
  import { Writable } from "stream";
18
18
  import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
19
19
  import { parse } from "yaml";
20
- import { createHash } from "crypto";
20
+ import { createHash } from "node:crypto";
21
+ import { gunzipSync, inflateRawSync } from "node:zlib";
22
+ import { createHash as createHash$1 } from "crypto";
21
23
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
22
24
  function getOwnerRepo(parsed) {
23
25
  if (parsed.type === "local") return null;
@@ -1475,7 +1477,7 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
1475
1477
  if (!isPathSafe(targetDir, fullPath)) continue;
1476
1478
  const parentDir = dirname(fullPath);
1477
1479
  if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
1478
- await writeFile(fullPath, content, "utf-8");
1480
+ await writeFile(fullPath, content);
1479
1481
  }
1480
1482
  }
1481
1483
  try {
@@ -1829,6 +1831,9 @@ var ProviderRegistryImpl = class {
1829
1831
  }
1830
1832
  };
1831
1833
  new ProviderRegistryImpl();
1834
+ const DISCOVERY_SCHEMA_V2 = "https://schemas.agentskills.io/discovery/0.2.0/schema.json";
1835
+ const MAX_ARCHIVE_UNPACKED_BYTES = 50 * 1024 * 1024;
1836
+ const MAX_ARCHIVE_FILES = 1e3;
1832
1837
  var WellKnownProvider = class {
1833
1838
  id = "well-known";
1834
1839
  displayName = "Well-Known Skills";
@@ -1852,6 +1857,9 @@ var WellKnownProvider = class {
1852
1857
  }
1853
1858
  }
1854
1859
  async fetchIndex(baseUrl) {
1860
+ return (await this.fetchIndexCandidates(baseUrl))[0] ?? null;
1861
+ }
1862
+ async fetchIndexCandidates(baseUrl) {
1855
1863
  try {
1856
1864
  const parsed = new URL(baseUrl);
1857
1865
  const basePath = parsed.pathname.replace(/\/$/, "");
@@ -1868,113 +1876,383 @@ var WellKnownProvider = class {
1868
1876
  wellKnownPath
1869
1877
  });
1870
1878
  }
1879
+ const candidates = [];
1871
1880
  for (const { indexUrl, baseUrl: resolvedBase, wellKnownPath } of urlsToTry) try {
1872
1881
  const response = await fetch(indexUrl);
1873
1882
  if (!response.ok) continue;
1874
- const index = await response.json();
1875
- if (!index.skills || !Array.isArray(index.skills)) continue;
1876
- let allValid = true;
1877
- for (const entry of index.skills) if (!this.isValidSkillEntry(entry)) {
1878
- allValid = false;
1879
- break;
1880
- }
1881
- if (allValid) return {
1882
- index,
1883
+ const rawIndex = await response.json();
1884
+ const normalized = this.normalizeIndex(rawIndex, indexUrl, wellKnownPath);
1885
+ if (!normalized) continue;
1886
+ candidates.push({
1887
+ index: normalized.index,
1888
+ entries: normalized.entries,
1883
1889
  resolvedBaseUrl: resolvedBase,
1884
- resolvedWellKnownPath: wellKnownPath
1885
- };
1890
+ resolvedWellKnownPath: wellKnownPath,
1891
+ indexUrl
1892
+ });
1886
1893
  } catch {
1887
1894
  continue;
1888
1895
  }
1889
- return null;
1896
+ return candidates;
1890
1897
  } catch {
1891
- return null;
1898
+ return [];
1899
+ }
1900
+ }
1901
+ normalizeIndex(rawIndex, indexUrl, resolvedWellKnownPath) {
1902
+ if (!rawIndex || typeof rawIndex !== "object") return null;
1903
+ const record = rawIndex;
1904
+ if (!Array.isArray(record.skills)) return null;
1905
+ const schema = record.$schema;
1906
+ if (schema === DISCOVERY_SCHEMA_V2) {
1907
+ const entries = [];
1908
+ const v2Entries = [];
1909
+ for (const entry of record.skills) {
1910
+ if (!this.isValidSkillEntryV2(entry)) continue;
1911
+ const artifactUrl = new URL(entry.url, indexUrl).toString();
1912
+ entries.push({
1913
+ version: "0.2.0",
1914
+ name: entry.name,
1915
+ description: entry.description,
1916
+ type: entry.type,
1917
+ artifactUrl,
1918
+ digest: entry.digest,
1919
+ indexEntry: entry
1920
+ });
1921
+ v2Entries.push(entry);
1922
+ }
1923
+ if (entries.length === 0) return null;
1924
+ return {
1925
+ index: {
1926
+ $schema: DISCOVERY_SCHEMA_V2,
1927
+ skills: v2Entries
1928
+ },
1929
+ entries
1930
+ };
1931
+ }
1932
+ if (schema !== void 0) return null;
1933
+ const v1Entries = [];
1934
+ const entries = [];
1935
+ for (const entry of record.skills) {
1936
+ if (!this.isValidSkillEntryV1(entry)) return null;
1937
+ v1Entries.push(entry);
1938
+ entries.push({
1939
+ version: "0.1.0",
1940
+ name: entry.name,
1941
+ description: entry.description,
1942
+ files: entry.files,
1943
+ baseUrl: this.getLegacySkillBaseUrl(indexUrl, resolvedWellKnownPath),
1944
+ wellKnownPath: resolvedWellKnownPath,
1945
+ indexEntry: entry
1946
+ });
1892
1947
  }
1948
+ return {
1949
+ index: { skills: v1Entries },
1950
+ entries
1951
+ };
1893
1952
  }
1894
- isValidSkillEntry(entry) {
1953
+ getLegacySkillBaseUrl(indexUrl, wellKnownPath) {
1954
+ const parsed = new URL(indexUrl);
1955
+ const marker = `/${wellKnownPath}/${this.INDEX_FILE}`;
1956
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname.slice(0, -marker.length)}`;
1957
+ }
1958
+ isValidSkillName(name) {
1959
+ if (typeof name !== "string") return false;
1960
+ if (name.length < 1 || name.length > 64) return false;
1961
+ if (!/^[a-z0-9-]+$/.test(name)) return false;
1962
+ if (name.startsWith("-") || name.endsWith("-")) return false;
1963
+ if (name.includes("--")) return false;
1964
+ return true;
1965
+ }
1966
+ isSafeLegacyFilePath(filePath) {
1967
+ if (typeof filePath !== "string" || filePath.length === 0) return false;
1968
+ if (filePath.startsWith("/") || filePath.startsWith("\\") || filePath.includes("..")) return false;
1969
+ if (filePath.includes("\0")) return false;
1970
+ return true;
1971
+ }
1972
+ isValidSkillEntryV1(entry) {
1895
1973
  if (!entry || typeof entry !== "object") return false;
1896
1974
  const e = entry;
1897
- if (typeof e.name !== "string" || !e.name) return false;
1975
+ if (!this.isValidSkillName(e.name)) return false;
1898
1976
  if (typeof e.description !== "string" || !e.description) return false;
1899
1977
  if (!Array.isArray(e.files) || e.files.length === 0) return false;
1900
- if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(e.name) && e.name.length > 1) {
1901
- if (e.name.length === 1 && !/^[a-z0-9]$/.test(e.name)) return false;
1902
- }
1903
- for (const file of e.files) {
1904
- if (typeof file !== "string") return false;
1905
- if (file.startsWith("/") || file.startsWith("\\") || file.includes("..")) return false;
1978
+ for (const file of e.files) if (!this.isSafeLegacyFilePath(file)) return false;
1979
+ return e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md");
1980
+ }
1981
+ isValidSkillEntryV2(entry) {
1982
+ if (!entry || typeof entry !== "object") return false;
1983
+ const e = entry;
1984
+ if (!this.isValidSkillName(e.name)) return false;
1985
+ if (typeof e.description !== "string" || !e.description || e.description.length > 1024) return false;
1986
+ if (e.type !== "skill-md" && e.type !== "archive") return false;
1987
+ if (typeof e.url !== "string" || !e.url) return false;
1988
+ if (typeof e.digest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(e.digest)) return false;
1989
+ try {
1990
+ new URL(e.url, "https://example.com/.well-known/agent-skills/index.json");
1991
+ } catch {
1992
+ return false;
1906
1993
  }
1907
- if (!e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md")) return false;
1908
1994
  return true;
1909
1995
  }
1910
1996
  async fetchSkill(url) {
1911
1997
  try {
1912
1998
  const parsed = new URL(url);
1913
- const result = await this.fetchIndex(url);
1914
- if (!result) return null;
1915
- const { index, resolvedBaseUrl, resolvedWellKnownPath } = result;
1916
- let skillName = null;
1917
- const pathMatch = parsed.pathname.match(/\/.well-known\/(?:agent-skills|skills)\/([^/]+)\/?$/);
1918
- if (pathMatch && pathMatch[1] && pathMatch[1] !== "index.json") skillName = pathMatch[1];
1919
- else if (index.skills.length === 1) skillName = index.skills[0].name;
1920
- if (!skillName) return null;
1921
- const skillEntry = index.skills.find((s) => s.name === skillName);
1922
- if (!skillEntry) return null;
1923
- return this.fetchSkillByEntry(resolvedBaseUrl, skillEntry, resolvedWellKnownPath);
1999
+ const candidates = await this.fetchIndexCandidates(url);
2000
+ for (const result of candidates) {
2001
+ const { entries } = result;
2002
+ let skillName = null;
2003
+ const pathMatch = parsed.pathname.match(/\/.well-known\/(?:agent-skills|skills)\/([^/]+)\/?$/);
2004
+ if (pathMatch && pathMatch[1] && pathMatch[1] !== "index.json") skillName = pathMatch[1];
2005
+ else if (entries.length === 1) skillName = entries[0].name;
2006
+ if (!skillName) continue;
2007
+ const skillEntry = entries.find((s) => s.name === skillName);
2008
+ if (!skillEntry) continue;
2009
+ const skill = await this.fetchSkillByEntry(skillEntry);
2010
+ if (skill) return skill;
2011
+ }
2012
+ return null;
1924
2013
  } catch {
1925
2014
  return null;
1926
2015
  }
1927
2016
  }
1928
- async fetchSkillByEntry(baseUrl, entry, wellKnownPath) {
2017
+ async fetchSkillByEntry(baseUrlOrEntry, legacyEntry, legacyWellKnownPath) {
2018
+ if (typeof baseUrlOrEntry === "string") {
2019
+ if (!legacyEntry) return null;
2020
+ return this.fetchLegacySkillByEntry({
2021
+ version: "0.1.0",
2022
+ name: legacyEntry.name,
2023
+ description: legacyEntry.description,
2024
+ files: legacyEntry.files,
2025
+ baseUrl: baseUrlOrEntry,
2026
+ wellKnownPath: legacyWellKnownPath ?? this.WELL_KNOWN_PATHS[0],
2027
+ indexEntry: legacyEntry
2028
+ });
2029
+ }
2030
+ if (baseUrlOrEntry.version === "0.1.0") return this.fetchLegacySkillByEntry(baseUrlOrEntry);
2031
+ return this.fetchArtifactSkillByEntry(baseUrlOrEntry);
2032
+ }
2033
+ async fetchLegacySkillByEntry(entry) {
1929
2034
  try {
1930
- const resolvedPath = wellKnownPath ?? this.WELL_KNOWN_PATHS[0];
1931
- const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${resolvedPath}/${entry.name}`;
2035
+ const skillBaseUrl = `${entry.baseUrl.replace(/\/$/, "")}/${entry.wellKnownPath}/${entry.name}`;
1932
2036
  const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
1933
2037
  const response = await fetch(skillMdUrl);
1934
2038
  if (!response.ok) return null;
1935
2039
  const content = await response.text();
1936
2040
  const { data } = parseFrontmatter(content);
1937
- if (!data.name || !data.description) return null;
2041
+ if (typeof data.name !== "string" || typeof data.description !== "string") return null;
1938
2042
  const files = /* @__PURE__ */ new Map();
1939
2043
  files.set("SKILL.md", content);
1940
2044
  const filePromises = entry.files.filter((f) => f.toLowerCase() !== "skill.md").map(async (filePath) => {
1941
2045
  try {
1942
2046
  const fileUrl = `${skillBaseUrl}/${filePath}`;
1943
2047
  const fileResponse = await fetch(fileUrl);
1944
- if (fileResponse.ok) return {
1945
- path: filePath,
1946
- content: await fileResponse.text()
1947
- };
2048
+ if (fileResponse.ok) {
2049
+ const fileContent = await fileResponse.arrayBuffer();
2050
+ return {
2051
+ path: filePath,
2052
+ content: new Uint8Array(fileContent)
2053
+ };
2054
+ }
1948
2055
  } catch {}
1949
2056
  return null;
1950
2057
  });
1951
2058
  const fileResults = await Promise.all(filePromises);
1952
2059
  for (const result of fileResults) if (result) files.set(result.path, result.content);
1953
- return {
1954
- name: sanitizeMetadata(data.name),
1955
- description: sanitizeMetadata(data.description),
2060
+ return this.createSkill({
2061
+ name: data.name,
2062
+ description: data.description,
1956
2063
  content,
1957
2064
  installName: entry.name,
1958
2065
  sourceUrl: skillMdUrl,
1959
2066
  metadata: data.metadata,
1960
2067
  files,
1961
- indexEntry: entry
1962
- };
2068
+ indexEntry: entry.indexEntry
2069
+ });
2070
+ } catch {
2071
+ return null;
2072
+ }
2073
+ }
2074
+ async fetchArtifactSkillByEntry(entry) {
2075
+ try {
2076
+ const response = await fetch(entry.artifactUrl);
2077
+ if (!response.ok) return null;
2078
+ const contentType = response.headers.get("content-type") ?? "";
2079
+ const bytes = new Uint8Array(await response.arrayBuffer());
2080
+ if (this.computeDigest(bytes) !== entry.digest) return null;
2081
+ if (entry.type === "skill-md") {
2082
+ const content = new TextDecoder().decode(bytes);
2083
+ const { data } = parseFrontmatter(content);
2084
+ if (typeof data.name !== "string" || typeof data.description !== "string") return null;
2085
+ const files = /* @__PURE__ */ new Map();
2086
+ files.set("SKILL.md", content);
2087
+ return this.createSkill({
2088
+ name: data.name,
2089
+ description: data.description,
2090
+ content,
2091
+ installName: entry.name,
2092
+ sourceUrl: entry.artifactUrl,
2093
+ metadata: data.metadata,
2094
+ files,
2095
+ indexEntry: entry.indexEntry
2096
+ });
2097
+ }
2098
+ const files = this.extractArchive(bytes, entry.artifactUrl, contentType);
2099
+ const skillMdBytes = files.get("SKILL.md");
2100
+ if (!skillMdBytes) return null;
2101
+ const content = typeof skillMdBytes === "string" ? skillMdBytes : new TextDecoder().decode(skillMdBytes);
2102
+ files.set("SKILL.md", content);
2103
+ const { data } = parseFrontmatter(content);
2104
+ if (typeof data.name !== "string" || typeof data.description !== "string") return null;
2105
+ return this.createSkill({
2106
+ name: data.name,
2107
+ description: data.description,
2108
+ content,
2109
+ installName: entry.name,
2110
+ sourceUrl: entry.artifactUrl,
2111
+ metadata: data.metadata,
2112
+ files,
2113
+ indexEntry: entry.indexEntry
2114
+ });
1963
2115
  } catch {
1964
2116
  return null;
1965
2117
  }
1966
2118
  }
2119
+ createSkill(input) {
2120
+ return {
2121
+ name: sanitizeMetadata(input.name),
2122
+ description: sanitizeMetadata(input.description),
2123
+ content: input.content,
2124
+ installName: input.installName,
2125
+ sourceUrl: input.sourceUrl,
2126
+ metadata: input.metadata && typeof input.metadata === "object" ? input.metadata : void 0,
2127
+ files: input.files,
2128
+ indexEntry: input.indexEntry
2129
+ };
2130
+ }
1967
2131
  async fetchAllSkills(url) {
1968
2132
  try {
1969
- const result = await this.fetchIndex(url);
1970
- if (!result) return [];
1971
- const { index, resolvedBaseUrl, resolvedWellKnownPath } = result;
1972
- const skillPromises = index.skills.map((entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry, resolvedWellKnownPath));
1973
- return (await Promise.all(skillPromises)).filter((s) => s !== null);
2133
+ const candidates = await this.fetchIndexCandidates(url);
2134
+ for (const result of candidates) {
2135
+ const skillPromises = result.entries.map((entry) => this.fetchSkillByEntry(entry));
2136
+ const skills = (await Promise.all(skillPromises)).filter((s) => s !== null);
2137
+ if (skills.length > 0) return skills;
2138
+ }
2139
+ return [];
1974
2140
  } catch {
1975
2141
  return [];
1976
2142
  }
1977
2143
  }
2144
+ computeDigest(bytes) {
2145
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
2146
+ }
2147
+ extractArchive(bytes, artifactUrl, contentType) {
2148
+ if (this.isZipArchive(bytes, artifactUrl, contentType)) return this.extractZip(bytes);
2149
+ if (this.isTarGzArchive(bytes, artifactUrl, contentType)) return this.extractTarGz(bytes);
2150
+ throw new Error("Unsupported archive format");
2151
+ }
2152
+ isZipArchive(bytes, artifactUrl, contentType) {
2153
+ return contentType.includes("application/zip") || artifactUrl.toLowerCase().endsWith(".zip") || bytes[0] === 80 && bytes[1] === 75;
2154
+ }
2155
+ isTarGzArchive(bytes, artifactUrl, contentType) {
2156
+ const lower = artifactUrl.toLowerCase();
2157
+ return contentType.includes("application/gzip") || contentType.includes("application/x-gzip") || lower.endsWith(".tar.gz") || lower.endsWith(".tgz") || bytes[0] === 31 && bytes[1] === 139;
2158
+ }
2159
+ normalizeArchivePath(rawPath) {
2160
+ if (!rawPath || rawPath.includes("\0")) return null;
2161
+ if (rawPath.startsWith("/") || rawPath.startsWith("\\")) return null;
2162
+ if (/^[A-Za-z]:/.test(rawPath)) return null;
2163
+ if (rawPath.includes("\\")) return null;
2164
+ const parts = rawPath.split("/").filter(Boolean);
2165
+ if (parts.length === 0) return null;
2166
+ if (parts.some((part) => part === "." || part === "..")) return null;
2167
+ return parts.join("/");
2168
+ }
2169
+ addArchiveFile(files, path, content, runningTotal) {
2170
+ const normalizedPath = this.normalizeArchivePath(path);
2171
+ if (!normalizedPath) throw new Error(`Unsafe archive path: ${path}`);
2172
+ runningTotal.bytes += content.byteLength;
2173
+ if (runningTotal.bytes > MAX_ARCHIVE_UNPACKED_BYTES) throw new Error("Archive exceeds maximum unpacked size");
2174
+ if (files.size >= MAX_ARCHIVE_FILES) throw new Error("Archive contains too many files");
2175
+ files.set(normalizedPath, content);
2176
+ }
2177
+ extractTarGz(bytes) {
2178
+ const tar = gunzipSync(Buffer.from(bytes));
2179
+ const files = /* @__PURE__ */ new Map();
2180
+ const runningTotal = { bytes: 0 };
2181
+ let offset = 0;
2182
+ while (offset + 512 <= tar.length) {
2183
+ const header = tar.subarray(offset, offset + 512);
2184
+ if (header.every((byte) => byte === 0)) break;
2185
+ const name = this.readTarString(header, 0, 100);
2186
+ const sizeText = this.readTarString(header, 124, 12).trim();
2187
+ const typeFlag = header[156];
2188
+ const prefix = this.readTarString(header, 345, 155);
2189
+ const path = prefix ? `${prefix}/${name}` : name;
2190
+ const size = Number.parseInt(sizeText || "0", 8);
2191
+ if (!Number.isFinite(size) || size < 0) throw new Error("Invalid tar entry size");
2192
+ offset += 512;
2193
+ if (typeFlag === 50 || typeFlag === 49) throw new Error("Archive links are not supported");
2194
+ if (typeFlag === 0 || typeFlag === 48) {
2195
+ const content = tar.subarray(offset, offset + size);
2196
+ this.addArchiveFile(files, path, new Uint8Array(content), runningTotal);
2197
+ }
2198
+ offset += Math.ceil(size / 512) * 512;
2199
+ }
2200
+ if (!files.has("SKILL.md")) throw new Error("Archive missing root SKILL.md");
2201
+ return files;
2202
+ }
2203
+ readTarString(buffer, offset, length) {
2204
+ const slice = buffer.subarray(offset, offset + length);
2205
+ const nul = slice.indexOf(0);
2206
+ return new TextDecoder().decode(nul >= 0 ? slice.subarray(0, nul) : slice);
2207
+ }
2208
+ extractZip(bytes) {
2209
+ const buffer = Buffer.from(bytes);
2210
+ const eocdOffset = this.findZipEndOfCentralDirectory(buffer);
2211
+ if (eocdOffset < 0) throw new Error("Invalid zip archive");
2212
+ const totalEntries = buffer.readUInt16LE(eocdOffset + 10);
2213
+ const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + 16);
2214
+ const files = /* @__PURE__ */ new Map();
2215
+ const runningTotal = { bytes: 0 };
2216
+ let offset = centralDirectoryOffset;
2217
+ for (let i = 0; i < totalEntries; i++) {
2218
+ if (buffer.readUInt32LE(offset) !== 33639248) throw new Error("Invalid zip directory");
2219
+ const flags = buffer.readUInt16LE(offset + 8);
2220
+ const method = buffer.readUInt16LE(offset + 10);
2221
+ const compressedSize = buffer.readUInt32LE(offset + 20);
2222
+ const uncompressedSize = buffer.readUInt32LE(offset + 24);
2223
+ const fileNameLength = buffer.readUInt16LE(offset + 28);
2224
+ const extraLength = buffer.readUInt16LE(offset + 30);
2225
+ const commentLength = buffer.readUInt16LE(offset + 32);
2226
+ const externalAttributes = buffer.readUInt32LE(offset + 38);
2227
+ const localHeaderOffset = buffer.readUInt32LE(offset + 42);
2228
+ const nameStart = offset + 46;
2229
+ const rawName = buffer.subarray(nameStart, nameStart + fileNameLength);
2230
+ const fileName = new TextDecoder(flags & 2048 ? "utf-8" : void 0).decode(rawName);
2231
+ offset = nameStart + fileNameLength + extraLength + commentLength;
2232
+ if (fileName.endsWith("/")) continue;
2233
+ if (flags & 1) throw new Error("Encrypted zip entries are not supported");
2234
+ const fileType = externalAttributes >>> 16 & 61440;
2235
+ if (fileType === 40960 || fileType === 4096) throw new Error("Archive links are not supported");
2236
+ if (buffer.readUInt32LE(localHeaderOffset) !== 67324752) throw new Error("Invalid zip local header");
2237
+ const localFileNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
2238
+ const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
2239
+ const dataStart = localHeaderOffset + 30 + localFileNameLength + localExtraLength;
2240
+ const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
2241
+ let content;
2242
+ if (method === 0) content = compressed;
2243
+ else if (method === 8) content = inflateRawSync(compressed);
2244
+ else throw new Error(`Unsupported zip compression method: ${method}`);
2245
+ if (content.byteLength !== uncompressedSize) throw new Error("Zip entry size mismatch");
2246
+ this.addArchiveFile(files, fileName, new Uint8Array(content), runningTotal);
2247
+ }
2248
+ if (!files.has("SKILL.md")) throw new Error("Archive missing root SKILL.md");
2249
+ return files;
2250
+ }
2251
+ findZipEndOfCentralDirectory(buffer) {
2252
+ const minOffset = Math.max(0, buffer.length - 65535 - 22);
2253
+ for (let offset = buffer.length - 22; offset >= minOffset; offset--) if (buffer.readUInt32LE(offset) === 101010256) return offset;
2254
+ return -1;
2255
+ }
1978
2256
  toRawUrl(url) {
1979
2257
  try {
1980
2258
  const parsed = new URL(url);
@@ -2028,9 +2306,14 @@ async function writeSkillLock(lock) {
2028
2306
  await mkdir(dirname(lockPath), { recursive: true });
2029
2307
  await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
2030
2308
  }
2309
+ let _ghWarningShown = false;
2031
2310
  function getGitHubToken() {
2032
2311
  if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
2033
2312
  if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
2313
+ if (!_ghWarningShown) {
2314
+ process.stderr.write("warn: GitHub API rate limit reached; reading a token via `gh auth token`.\n Set GITHUB_TOKEN in your environment to skip this fallback.\n");
2315
+ _ghWarningShown = true;
2316
+ }
2034
2317
  try {
2035
2318
  const token = execSync("gh auth token", {
2036
2319
  encoding: "utf-8",
@@ -2044,9 +2327,9 @@ function getGitHubToken() {
2044
2327
  } catch {}
2045
2328
  return null;
2046
2329
  }
2047
- async function fetchSkillFolderHash(ownerRepo, skillPath, token, ref) {
2330
+ async function fetchSkillFolderHash(ownerRepo, skillPath, getToken, ref) {
2048
2331
  const { fetchRepoTree, getSkillFolderHashFromTree } = await Promise.resolve().then(() => blob_exports);
2049
- const tree = await fetchRepoTree(ownerRepo, ref, token);
2332
+ const tree = await fetchRepoTree(ownerRepo, ref, getToken ?? void 0);
2050
2333
  if (!tree) return null;
2051
2334
  return getSkillFolderHashFromTree(tree, skillPath);
2052
2335
  }
@@ -2129,7 +2412,7 @@ async function computeSkillFolderHash(skillDir) {
2129
2412
  const files = [];
2130
2413
  await collectFiles(skillDir, skillDir, files);
2131
2414
  files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
2132
- const hash = createHash("sha256");
2415
+ const hash = createHash$1("sha256");
2133
2416
  for (const file of files) {
2134
2417
  hash.update(file.relativePath);
2135
2418
  hash.update(file.content);
@@ -2176,13 +2459,9 @@ const FETCH_TIMEOUT = 1e4;
2176
2459
  function toSkillSlug(name) {
2177
2460
  return name.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
2178
2461
  }
2179
- async function fetchRepoTree(ownerRepo, ref, token) {
2180
- const branches = ref ? [ref] : [
2181
- "HEAD",
2182
- "main",
2183
- "master"
2184
- ];
2185
- for (const branch of branches) try {
2462
+ let _rateLimitedThisSession = false;
2463
+ async function fetchTreeBranch(ownerRepo, branch, token) {
2464
+ try {
2186
2465
  const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${encodeURIComponent(branch)}?recursive=1`;
2187
2466
  const headers = {
2188
2467
  Accept: "application/vnd.github.v3+json",
@@ -2193,15 +2472,59 @@ async function fetchRepoTree(ownerRepo, ref, token) {
2193
2472
  headers,
2194
2473
  signal: AbortSignal.timeout(FETCH_TIMEOUT)
2195
2474
  });
2196
- if (!response.ok) continue;
2197
- const data = await response.json();
2475
+ if (response.ok) {
2476
+ const data = await response.json();
2477
+ return {
2478
+ tree: {
2479
+ sha: data.sha,
2480
+ branch,
2481
+ tree: data.tree
2482
+ },
2483
+ rateLimited: false
2484
+ };
2485
+ }
2198
2486
  return {
2199
- sha: data.sha,
2200
- branch,
2201
- tree: data.tree
2487
+ tree: null,
2488
+ rateLimited: response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0"
2202
2489
  };
2203
2490
  } catch {
2204
- continue;
2491
+ return {
2492
+ tree: null,
2493
+ rateLimited: false
2494
+ };
2495
+ }
2496
+ }
2497
+ async function fetchRepoTree(ownerRepo, ref, getToken) {
2498
+ const branches = ref ? [ref] : [
2499
+ "HEAD",
2500
+ "main",
2501
+ "master"
2502
+ ];
2503
+ if (_rateLimitedThisSession && getToken) {
2504
+ const token = getToken();
2505
+ if (!token) return null;
2506
+ for (const branch of branches) {
2507
+ const result = await fetchTreeBranch(ownerRepo, branch, token);
2508
+ if (result.tree) return result.tree;
2509
+ }
2510
+ return null;
2511
+ }
2512
+ let rateLimited = false;
2513
+ for (const branch of branches) {
2514
+ const result = await fetchTreeBranch(ownerRepo, branch, null);
2515
+ if (result.tree) return result.tree;
2516
+ if (result.rateLimited) {
2517
+ rateLimited = true;
2518
+ break;
2519
+ }
2520
+ }
2521
+ if (!rateLimited || !getToken) return null;
2522
+ _rateLimitedThisSession = true;
2523
+ const token = getToken();
2524
+ if (!token) return null;
2525
+ for (const branch of branches) {
2526
+ const result = await fetchTreeBranch(ownerRepo, branch, token);
2527
+ if (result.tree) return result.tree;
2205
2528
  }
2206
2529
  return null;
2207
2530
  }
@@ -2298,7 +2621,7 @@ async function fetchSkillDownload(source, slug) {
2298
2621
  }
2299
2622
  }
2300
2623
  async function tryBlobInstall(ownerRepo, options = {}) {
2301
- const tree = await fetchRepoTree(ownerRepo, options.ref, options.token);
2624
+ const tree = await fetchRepoTree(ownerRepo, options.ref, options.getToken);
2302
2625
  if (!tree) return null;
2303
2626
  let skillMdPaths = findSkillMdPaths(tree, options.subpath);
2304
2627
  if (skillMdPaths.length === 0) return null;
@@ -2370,7 +2693,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
2370
2693
  tree
2371
2694
  };
2372
2695
  }
2373
- var version$1 = "1.5.6";
2696
+ var version$1 = "1.5.7";
2374
2697
  const isCancelled$1 = (value) => typeof value === "symbol";
2375
2698
  async function isSourcePrivate(source) {
2376
2699
  const ownerRepo = parseOwnerRepo(source);
@@ -2912,12 +3235,11 @@ async function runAdd(args, options = {}) {
2912
3235
  const owner = ownerRepo?.split("/")[0]?.toLowerCase();
2913
3236
  if (ownerRepo && owner && BLOB_ALLOWED_OWNERS.includes(owner)) {
2914
3237
  spinner.start("Fetching skills...");
2915
- const token = getGitHubToken();
2916
3238
  blobResult = await tryBlobInstall(ownerRepo, {
2917
3239
  subpath: parsed.subpath,
2918
3240
  skillFilter: parsed.skillFilter,
2919
3241
  ref: parsed.ref,
2920
- token,
3242
+ getToken: getGitHubToken,
2921
3243
  includeInternal
2922
3244
  });
2923
3245
  if (!blobResult) spinner.stop(import_picocolors.default.dim("Falling back to clone..."));
@@ -3265,10 +3587,7 @@ async function runAdd(args, options = {}) {
3265
3587
  if (successful.length > 0 && installGlobally && normalizedSource) {
3266
3588
  const successfulSkillNames = new Set(successful.map((r) => r.skill));
3267
3589
  let cachedTree;
3268
- if (parsed.type === "github" && !blobResult) {
3269
- const token = getGitHubToken();
3270
- cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, token);
3271
- }
3590
+ if (parsed.type === "github" && !blobResult) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
3272
3591
  for (const skill of selectedSkills) {
3273
3592
  const skillDisplayName = getSkillDisplayName(skill);
3274
3593
  if (successfulSkillNames.has(skillDisplayName)) try {
@@ -4751,7 +5070,6 @@ async function updateGlobalSkills(skillFilter) {
4751
5070
  checkedCount: 0
4752
5071
  };
4753
5072
  }
4754
- const token = getGitHubToken();
4755
5073
  const updates = [];
4756
5074
  const skipped = [];
4757
5075
  const checkable = [];
@@ -4778,7 +5096,7 @@ async function updateGlobalSkills(skillFilter) {
4778
5096
  const { name: skillName, entry } = checkable[i];
4779
5097
  process.stdout.write(`\r${DIM}Checking global skill ${i + 1}/${checkable.length}: ${sanitizeMetadata(skillName)}${RESET}\x1b[K`);
4780
5098
  try {
4781
- const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token, entry.ref);
5099
+ const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, getGitHubToken, entry.ref);
4782
5100
  if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
4783
5101
  name: skillName,
4784
5102
  source: entry.source,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {