UNPKG

agents

Version:

A home for your AI agents

343 lines (342 loc) • 11.7 kB
import { a as isIgnoredSkillPath, c as isModelInvocable, d as err, f as ok, l as parseSkillFrontmatter, n as describeSkillResource, o as parseSkillResourcePath, r as encodeSkillResourceContent, s as SkillParseError, t as InvalidSkillResourcePathError, u as parseSkillName } from "../resource-B59OV2a2.js"; import { n as parseSkill, t as formatSkill } from "../markdown-BZ-VLtA1.js"; import { a as SkillResourceNotFoundError, i as renderSkillResource, n as formatSkillDiagnostic, o as SkillSourceError, r as loadSkillCatalog, t as UnknownSkillError } from "../catalog-BpOejOsE.js"; //#region src/skills/tree.ts /** * @internal Turn a flat list of files under a skills root into a listing. * * Each `<directory>/SKILL.md` one level below the root is a skill; every * other file in that directory is one of its resources. Workspace and R2 * sources share this so both read the same layout the same way. * * @param files - Every file under the root. * @param readSkillFile - Read a root-relative `SKILL.md` as text. * @param location - Absolute directory for a skill, when the source has one. * @throws Whatever `readSkillFile` throws; callers wrap it as a source error. */ async function indexSkillTree(files, readSkillFile, location) { const byDirectory = /* @__PURE__ */ new Map(); for (const file of files) { const slash = file.path.indexOf("/"); if (slash <= 0) continue; const directory = file.path.slice(0, slash); const entries = byDirectory.get(directory) ?? []; entries.push(file); byDirectory.set(directory, entries); } const skills = []; const invalid = []; const directories = /* @__PURE__ */ new Map(); for (const directory of [...byDirectory.keys()].sort()) { const entries = byDirectory.get(directory) ?? []; const skillPath = `${directory}/SKILL.md`; if (!entries.some((entry) => entry.path === skillPath)) continue; const markdown = await readSkillFile(skillPath); if (markdown === null) continue; const parsed = parseSkill(markdown, { directory }); if (!parsed.ok) { invalid.push({ location: skillPath, error: parsed.error }); continue; } const resources = []; for (const entry of entries) { if (entry.path === skillPath) continue; const relative = entry.path.slice(directory.length + 1); if (isIgnoredSkillPath(relative)) continue; const path = parseSkillResourcePath(relative); if (path.ok) resources.push(describeSkillResource(path.value, entry.size)); } resources.sort((a, b) => a.path.localeCompare(b.path)); skills.push({ skill: parsed.value, resources, ...location === void 0 ? {} : { location: location(directory) }, ...parsed.warnings.length === 0 ? {} : { warnings: parsed.warnings } }); if (!directories.has(parsed.value.name)) directories.set(parsed.value.name, directory); } return { listing: { skills, invalid }, directories }; } //#endregion //#region src/skills/workspace.ts /** Default skills directory inside a Workspace. */ const DEFAULT_WORKSPACE_SKILLS_ROOT = "/workspace/.agents/skills"; /** * Create a source that reads skills from a Computer Workspace. * * Skills are plain files: every `<root>/<skill-name>/SKILL.md` is a skill, * and the files beside it are its resources. The Workspace is read on every * listing, so skills the agent or a user adds, edits, or deletes take effect * the next time the catalog loads. A missing root lists no skills. * * @param target - The Workspace to read. * @param options - Skills root and source id. * @returns A source backed by the Workspace. * @throws TypeError when `root` is not a normalized absolute path. */ function workspace(target, options = {}) { const root = parseRoot(options.root ?? "/workspace/.agents/skills"); const id = options.id ?? `workspace:${root}`; const { fs } = target; let directories = /* @__PURE__ */ new Map(); async function readBytes(path) { try { return await drain(await fs.readFile(path)); } catch (error) { if (isNotFound(error)) return null; throw error; } } return { id, async list() { try { let entries; try { entries = await fs.find(root, void 0, { exclude: ["*/node_modules/**", "*/.git/**"] }); } catch (error) { if (isNotFound(error)) return ok({ skills: [], invalid: [] }); throw error; } const tree = await indexSkillTree(entries.filter((entry) => entry.type === "file").map((entry) => ({ path: entry.path.slice(root.length + 1) })), async (path) => { const bytes = await readBytes(`${root}/${path}`); return bytes === null ? null : new TextDecoder().decode(bytes); }, (directory) => `${root}/${directory}`); directories = tree.directories; return ok(tree.listing); } catch (cause) { return err(new SkillSourceError(id, "list", cause)); } }, async readResource(skill, path) { const info = describeSkillResource(path); try { const bytes = await readBytes(`${root}/${directories.get(skill) ?? skill}/${path}`); if (bytes === null) return err(new SkillResourceNotFoundError(skill, path)); return ok({ ...info, size: bytes.byteLength, content: encodeSkillResourceContent(bytes, info.encoding) }); } catch (cause) { return err(new SkillSourceError(id, "read-resource", cause)); } } }; } function parseRoot(root) { const normalized = root.replace(/\/+$/, ""); if (!normalized.startsWith("/") || normalized.includes("\0") || normalized.slice(1).split("/").some((part) => part === "" || part === "." || part === "..")) throw new TypeError(`Skills root must be a normalized absolute path: ${root}`); return normalized; } /** * Computer raises `ENOENT` errors with a `code`. Over RPC only the message * may survive, so match that too. */ function isNotFound(error) { if (!(error instanceof Error)) return false; return "code" in error && error.code === "ENOENT" || /ENOENT|no such (file|path)/i.test(error.message); } async function drain(stream) { const parts = []; let total = 0; const reader = stream.getReader(); try { while (true) { const { value, done } = await reader.read(); if (done) break; parts.push(value); total += value.byteLength; } } finally { reader.releaseLock(); } const bytes = new Uint8Array(total); let offset = 0; for (const part of parts) { bytes.set(part, offset); offset += part.byteLength; } return bytes; } //#endregion //#region src/skills/r2.ts /** * Create a read-only source backed by an R2 bucket. * * The bucket holds the Agent Skills layout under `prefix`: * `<prefix><skill-name>/SKILL.md`, plus any bundled files beside it. * * @param bucket - The R2 binding. * @param options - Prefix, id, name allowlist, and listing cache interval. * @returns A source that reads skills from the bucket. */ function r2(bucket, options = {}) { const prefix = normalizePrefix(options.prefix); const id = options.id ?? `r2:${prefix || "/"}`; const allowed = options.skills?.length ? new Set(options.skills) : null; const refreshIntervalMs = options.refreshIntervalMs ?? 6e4; let cached = null; async function list() { if (cached && Date.now() - cached.at < refreshIntervalMs) return ok(cached.listing); try { const { listing, directories } = await indexSkillTree((await listAll(bucket, prefix)).map((object) => ({ path: object.key.slice(prefix.length), size: object.size })), async (path) => { const object = await bucket.get(`${prefix}${path}`); return object ? object.text() : null; }); const filtered = allowed ? { skills: listing.skills.filter(({ skill }) => allowed.has(skill.name)), invalid: listing.invalid } : listing; cached = { listing: filtered, directories, at: Date.now() }; return ok(filtered); } catch (cause) { return err(new SkillSourceError(id, "list", cause)); } } return { id, list, async readResource(skill, path) { if (allowed && !allowed.has(skill)) return err(new SkillResourceNotFoundError(skill, path)); try { const directory = cached?.directories.get(skill) ?? skill; const object = await bucket.get(`${prefix}${directory}/${path}`); if (!object) return err(new SkillResourceNotFoundError(skill, path)); const info = describeSkillResource(path, object.size); const bytes = new Uint8Array(await object.arrayBuffer()); return ok({ ...info, content: encodeSkillResourceContent(bytes, info.encoding) }); } catch (cause) { return err(new SkillSourceError(id, "read-resource", cause)); } } }; } function normalizePrefix(prefix) { if (!prefix) return ""; return prefix.endsWith("/") ? prefix : `${prefix}/`; } async function listAll(bucket, prefix) { const objects = []; let cursor; do { const page = await bucket.list({ prefix, cursor }); objects.push(...page.objects.map((object) => ({ key: object.key, size: object.size }))); cursor = page.truncated ? page.cursor : void 0; } while (cursor); return objects; } //#endregion //#region src/skills/manifest.ts /** * Create a source from skills defined in code. * * Entries pass the same checks as `SKILL.md` files from other sources: * broken spec rules become warnings, and entries without a usable * name or description are reported as invalid. Useful for tests and for small fixed skill sets * that do not need a Workspace or bucket. * * @param definition - The skills to serve. * @returns A source that always lists the same skills. */ function manifest(definition) { const contents = /* @__PURE__ */ new Map(); const skills = []; const invalid = []; for (const entry of definition.skills) { const parsed = parseSkillFrontmatter({ name: entry.name, description: entry.description, license: entry.license, compatibility: entry.compatibility, metadata: entry.metadata, "allowed-tools": entry.allowedTools }, entry.body); if (!parsed.ok) { invalid.push({ location: `${definition.id}:${entry.name}`, error: parsed.error }); continue; } if (contents.has(parsed.value.name)) continue; const files = /* @__PURE__ */ new Map(); const resources = []; for (const resource of entry.resources ?? []) { const path = parseSkillResourcePath(resource.path); if (!path.ok) continue; const inferred = describeSkillResource(path.value); const info = { ...inferred, kind: resource.kind ?? inferred.kind, encoding: resource.encoding ?? inferred.encoding, ...resource.mimeType === void 0 ? {} : { mimeType: resource.mimeType } }; resources.push(info); files.set(path.value, { ...info, content: resource.content }); } resources.sort((a, b) => a.path.localeCompare(b.path)); contents.set(parsed.value.name, files); skills.push({ skill: parsed.value, resources, ...parsed.warnings.length === 0 ? {} : { warnings: parsed.warnings } }); } const listing = { skills, invalid }; return { id: definition.id, async list() { return ok(listing); }, async readResource(skill, path) { const resource = contents.get(skill)?.get(path); return resource ? ok(resource) : err(new SkillResourceNotFoundError(skill, path)); } }; } /** * Create a source from skills defined in code. * * @deprecated Use {@link manifest}. * @param definition - The skills to serve. * @returns A source that always lists the same skills. */ const fromManifest = manifest; //#endregion export { DEFAULT_WORKSPACE_SKILLS_ROOT, InvalidSkillResourcePathError, SkillParseError, SkillResourceNotFoundError, SkillSourceError, UnknownSkillError, formatSkill, formatSkillDiagnostic, fromManifest, isModelInvocable, loadSkillCatalog, manifest, parseSkill, parseSkillName, parseSkillResourcePath, r2, renderSkillResource, workspace }; //# sourceMappingURL=index.js.map