agents
Version:
A home for your AI agents
343 lines (342 loc) • 11.7 kB
JavaScript
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