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.
- package/dist/cli.mjs +397 -79
- 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
|
|
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
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
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
|
|
1896
|
+
return candidates;
|
|
1890
1897
|
} catch {
|
|
1891
|
-
return
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
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(
|
|
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
|
|
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 (
|
|
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)
|
|
1945
|
-
|
|
1946
|
-
|
|
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:
|
|
1955
|
-
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
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
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 (
|
|
2197
|
-
|
|
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
|
-
|
|
2200
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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,
|