Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313import { createFetchTools, type FetchResult,} from "@cloudflare/think/tools/fetch";import { parseSkillFrontmatter } from "agents/skills";import { createHash } from "node:crypto";import * as Effect from "effect/Effect";import { z } from "zod";import { SKILL_MAX_FILES, SKILL_MAX_FILE_BYTES, SKILL_MAX_PACKAGE_BYTES, type SkillPackageInput,} from "../shared/skills";import { SkillPackageError, validateSkillPath } from "./skill-package";
export class GitHubSkillError extends Error { constructor( readonly code: string, message: string, readonly status = 400, ) { super(message); }}
const invalidFolder = () => new GitHubSkillError( "invalid_url", "Use a public GitHub folder URL: https://github.com/owner/repo/tree/branch/skill-folder. Encode slashes in branch names as %2F.", );const invalidTree = () => new GitHubSkillError( "invalid_github_folder", "Could not read a complete Skill folder. Choose a smaller folder containing SKILL.md and regular Markdown files.", );
export function githubSkillFolder(address: string) { const url = new URL(address); if (url.hostname !== "github.com") return null; const [empty, owner, repo, type, ref, ...folder] = url.pathname .replace(/\/$/, "") .split("/"); if ( empty || url.protocol !== "https:" || url.port || url.username || url.password || type !== "tree" || !owner || !repo || !ref || !folder.length || !/^[A-Za-z0-9-]+$/.test(owner) || !/^[A-Za-z0-9._-]+$/.test(repo) ) throw invalidFolder(); try { const decoded = folder.map(decodeURIComponent); // A folder component cannot smuggle a path separator or a git tree selector. if ( decoded.some( (part) => !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(part) || part.endsWith("."), ) ) throw invalidFolder(); const branch = decodeURIComponent(ref); if ( !branch || branch.length > 240 || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(branch) || branch.split("/").some((part) => !part || part === "." || part === "..") ) throw invalidFolder(); return { owner, repo, ref: branch, path: decoded.join("/") }; } catch { throw invalidFolder(); }}type GitHubFolder = NonNullable<ReturnType<typeof githubSkillFolder>>;
function fetchTool(maxBytes: number, accept: string) { return createFetchTools({ allowlist: [ "https://api.github.com/repos/**", "https://raw.githubusercontent.com/**", ], maxBytes, maxModelChars: maxBytes, timeoutMs: 15_000, response: "text", spillToWorkspace: false, followRedirects: "none", modelHeaderAllowlist: ["user-agent"], defaultAccept: accept, }).fetch_url;}const commitFetch = fetchTool(128, "application/vnd.github.sha");const treeFetch = fetchTool(1024 * 1024, "application/vnd.github+json");const markdownFetch = fetchTool(SKILL_MAX_FILE_BYTES, "text/plain");
function verifiedMarkdown( body: string, receivedBytes: number, blob: { sha: string; size?: number },) { if (receivedBytes === blob.size) { // Native Think text decoding removes a leading UTF-8 BOM. Recover it only // when the bytes match GitHub's immutable blob, never from a size guess. for (const candidate of [body, `\uFEFF${body}`]) { const bytes = new TextEncoder().encode(candidate); if (bytes.length !== blob.size) continue; const digest = createHash("sha1") .update(`blob ${bytes.length}\0`) .update(bytes) .digest("hex"); if (digest === blob.sha) return candidate; } } throw new GitHubSkillError( "invalid_package", "A Markdown file could not be read as exact UTF-8 from the GitHub snapshot.", );}
function download( tool: typeof commitFetch, url: string, blob?: { sha: string; size?: number },) { return Effect.tryPromise({ try: (signal) => Promise.resolve( tool.execute!( { url, headers: { "User-Agent": "Flarebot-Skill-Importer" } }, { toolCallId: "github-skill-review", messages: [], context: {}, abortSignal: signal, }, ), ) as Promise<FetchResult>, catch: () => new GitHubSkillError( "download_failed", "Could not download the GitHub Skill. Try again.", 502, ), }).pipe( Effect.flatMap((result) => { if (!result.ok || result.truncated || result.body === undefined) return Effect.fail( new GitHubSkillError( "download_failed", "Could not download the complete GitHub Skill. Check the public folder and branch, or try again after GitHub's rate limit resets.", 502, ), ); return blob ? Effect.try({ try: () => verifiedMarkdown(result.body!, result.bytes, blob), catch: () => new GitHubSkillError( "invalid_package", "A Markdown file could not be read as exact UTF-8 from the GitHub snapshot.", ), }) : Effect.succeed(result.body); }), );}
const hash = z.string().regex(/^[a-f0-9]{40}$/);const treeSchema = z.object({ truncated: z.literal(false), tree: z .array( z.object({ path: z.string().min(1).max(4096), mode: z.string(), type: z.string(), sha: hash, size: z.number().int().nonnegative().optional(), }), ) .max(4096),});
function markdownEntries(body: string) { const parsed = treeSchema.parse(JSON.parse(body)); const entries = parsed.tree.filter((entry) => /\.md$/i.test(entry.path)); if ( !entries.length || entries.length > SKILL_MAX_FILES || !entries.some((entry) => entry.path === "SKILL.md") ) throw invalidTree(); const names = new Set<string>(); let bytes = 0; for (const entry of entries) { validateSkillPath(entry.path, "files"); if ( entry.path.length > 240 || entry.type !== "blob" || !["100644", "100755"].includes(entry.mode) || entry.size === undefined || entry.size > SKILL_MAX_FILE_BYTES || names.has(entry.path.toLowerCase()) ) throw invalidTree(); names.add(entry.path.toLowerCase()); bytes += entry.size; } if (bytes > SKILL_MAX_PACKAGE_BYTES) throw invalidTree(); return entries;}
function withSnapshotVersion(content: string, commit: string) { // The native frontmatter parser expects the opening delimiter at byte zero. content = content.replace(/^\uFEFF/, ""); const parsed = parseSkillFrontmatter(content); const metadata = parsed.data.metadata; if ( metadata !== undefined && (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) ) throw new SkillPackageError( "invalid_metadata", "SKILL.md.metadata", "Use a string metadata map.", ); if (metadata && Object.hasOwn(metadata, "version")) return content; // Standard Skills need no release number. Record the imported snapshot without // requiring the publisher to adopt Flarebot's package envelope or metadata. const data = { ...parsed.data, metadata: { ...metadata, version: `0.0.0+git.${commit}` }, }; return `---\n${JSON.stringify(data)}\n---\n${parsed.body}`;}
export function downloadGitHubSkill(folder: GitHubFolder) { return Effect.gen(function* () { const repository = `${folder.owner}/${folder.repo}`; const api = `https://api.github.com/repos/${repository}`; const resolved = yield* download( commitFetch, `${api}/commits/${encodeURIComponent(folder.ref)}`, ); const commit = yield* Effect.try({ try: () => hash.parse(resolved.trim()), catch: invalidTree, }); // Resolve once, then address both the tree and all file bytes by that commit. const tree = yield* download( treeFetch, `${api}/git/trees/${encodeURIComponent(`${commit}:${folder.path}`)}?recursive=1`, ); const entries = yield* Effect.try({ try: () => markdownEntries(tree), catch: invalidTree, }); const files = yield* Effect.forEach( entries, (entry) => { const path = `${folder.path}/${entry.path}` .split("/") .map(encodeURIComponent) .join("/"); return download( markdownFetch, `https://raw.githubusercontent.com/${repository}/${commit}/${path}`, entry, ).pipe( Effect.flatMap((content) => Effect.try({ try: () => { if (new TextEncoder().encode(content).byteLength !== entry.size) throw invalidTree(); return { path: entry.path, content: entry.path === "SKILL.md" ? withSnapshotVersion(content, commit) : content, }; }, catch: () => new GitHubSkillError( "invalid_package", "The GitHub Skill has invalid Markdown or frontmatter. Check SKILL.md and try again.", ), }), ), ); }, { concurrency: 4 }, ); return { value: { formatVersion: 1, files } satisfies SkillPackageInput, origin: { kind: "url" as const, url: `https://github.com/${repository}/tree/${commit}/${folder.path}`, }, }; });}