/** * Manifest parsing — reads SKILL.md, AGENT.md, agent.yaml, CONTRACT.md, * CONNECTOR.md, *.prompt.md, *.flow.yaml, *.bundle.yaml files and * produces CatalogEntry objects. * Used by @skaile/asset-manager for on-demand repo scanning. * * **All filesystem access here is async and bounded** (issue #454). A repo root * can resolve onto the rclone FUSE mount at `/skaile/workspace` — either * directly (a `sources[].path` pointing into the workspace) or indirectly, since * the walk follows symlinks and an off-mount tree can link into one. A * synchronous `readdirSync`/`statSync`/`readFileSync` there parks the whole * `skaile serve` event loop in an uninterruptible kernel wait, and a hang is not * an error so `try/catch` never fires. Every call goes through * {@link withFsDeadline}/{@link tryWithFsDeadline}, and the walk itself is * bounded by depth, entry count, a wall-clock budget, and an ancestor-inode set. */ import type { CatalogEntry, Dependency } from "./models.js"; /** * Result of parsing YAML frontmatter from a markdown file. * @docLink packages/core/concepts#frontmatter-result */ export interface FrontmatterResult { /** Parsed YAML data. Empty object if no frontmatter block is present. */ data: Record; /** Markdown body below the frontmatter block (trimmed). */ body: string; } /** * Parse YAML frontmatter from a markdown file (`---` delimited). * Returns `{ data: {}, body: text }` when no frontmatter block is present. * * @param text - Raw file contents * @returns Parsed frontmatter data and the remaining body text * @docLink packages/core/concepts#parse-frontmatter */ export declare function parseFrontmatter(text: string): FrontmatterResult; /** * Parse the `requires:` field from manifest frontmatter into typed `Dependency` objects. * Accepts a comma-separated string or an array of `"kind:name"` strings. Reads from either * the root or `metadata.requires` (matching `extractVersion`'s dual-location behavior) so * skill authors can follow the agentskills.io convention of placing skaile extensions under * `metadata:` while remaining backward-compatible with root-level declarations. * * @param meta - Parsed frontmatter data object * @returns Array of `Dependency` objects (empty when `requires` is absent) * @docLink packages/core/concepts#parse-requires */ export declare function parseRequires(meta: Record): Dependency[]; /** * Parse a `SKILL.md` file into a `CatalogEntry`. * The `kind` field defaults to `"skill"` but can be overridden to `"agent"` or `"prompt"` * via the `type:` frontmatter key. Falls back to the parent directory name when `name:` is absent. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the `SKILL.md` file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind, name, description, version, and requires * @docLink packages/core/concepts#from-skill-md */ export declare function fromSkillMd(filePath: string, repoName: string): Promise; /** * Parse an `AGENT.md` file into a `CatalogEntry` with `kind: "agent"`. * Falls back to the parent directory name when `name:` is absent. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the `AGENT.md` file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind `"agent"` * @docLink packages/core/concepts#from-agent-md */ export declare function fromAgentMd(filePath: string, repoName: string): Promise; /** * Parse an `agent.yaml` file into a `CatalogEntry` with `kind: "agent"`. * Collects sub-agent dependencies from `requires[]` (new format) and `dependencies[]` * (legacy format), skill abilities from `abilities[]`, and contracts from `contracts[]`. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the `agent.yaml` file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind `"agent"` and full dependency graph * @docLink packages/core/concepts#from-agent-yaml */ export declare function fromAgentYaml(filePath: string, repoName: string): Promise; /** * Parse a `*.prompt.md` file into a `CatalogEntry` with `kind: "prompt"`. * The name defaults to the filename stem (without the `.prompt.md` suffix) * when no `name:` frontmatter key is present. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the `.prompt.md` file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind `"prompt"` * @docLink packages/core/concepts#from-prompt-md */ export declare function fromPromptMd(filePath: string, repoName: string): Promise; /** * Parse a `*.flow.yaml` or `*.flow.json` file into a `CatalogEntry` with `kind: "flow"`. * The name resolves from `name:` or `id:` frontmatter, falling back to the filename stem. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the flow definition file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind `"flow"` * @docLink packages/core/concepts#from-flow-yaml */ export declare function fromFlowYaml(filePath: string, repoName: string): Promise; /** * Parse a `*.bundle.yaml` file into a `CatalogEntry` with `kind: "bundle"`. * Bundles declare a flat list of string `dependencies` (asset refs) rather than * typed `Dependency` objects; both `requires` and `dependencies` are populated. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the bundle YAML file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind `"bundle"` * @docLink packages/core/concepts#from-bundle-yaml */ export declare function fromBundleYaml(filePath: string, repoName: string): Promise; /** * Extract a bundle's raw transitive dependency ref strings from its manifest * file — the union of its `requires` and `dependencies` lists, kept verbatim * (publisher/pin qualifiers preserved, comments dropped by the parser). * * Supports `*.bundle.yaml` / `*.flow.{yaml,json}` (top-level * `requires`/`dependencies`) and the `BUNDLE.md` frontmatter layout * (`requires`/`dependencies`, incl. nested under `metadata`). Unlike * {@link parseRequires}, refs are returned as-is — the resolver needs the full * `kind:name@[#pin]` form, not a publisher-stripped {@link Dependency}. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to a `.bundle.yaml`, `.flow.{yaml,json}`, or `BUNDLE.md`. * @returns Deduped list of ref strings; `[]` on parse failure or no deps. * @docLink packages/core/concepts#bundle-dep-refs */ export declare function bundleDepRefs(filePath: string): Promise; /** * Parse a `CONTRACT.md` file into a `CatalogEntry` with `kind: "contract"`. * Falls back to the parent directory name when `name:` is absent. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the `CONTRACT.md` file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind `"contract"` * @docLink packages/core/concepts#from-contract-md */ export declare function fromContractMd(filePath: string, repoName: string): Promise; /** * Parse an `MCP.md` file into a `CatalogEntry` with `kind: "mcp-server"`. * Falls back to the parent directory name when `name:` is absent. * * Frontmatter fields (catalog identity): * - `name` — server name (default: parent dir name) * - `description` — short description * - `version` — semver * - `keywords` — array of discovery tags * * Frontmatter fields (runtime defaults, stored in `metadata`): * - `transport` — "stdio" | "sse" | "http" (default: "stdio") * - `command` — command to execute (stdio) * - `args` — command arguments (stdio) * - `env` — environment variables (stdio) * - `url` — server URL (sse/http) * - `headers` — HTTP headers (sse/http) * * When `mcp:name` appears in `skaile.yaml` `dependencies`, the runner reads * these defaults from `metadata` to build a `McpServerDeclaration` without * requiring verbose inline config. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the `MCP.md` file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind `"mcp-server"` * @docLink packages/core/concepts#from-mcp-server-md */ export declare function fromMcpServerMd(filePath: string, repoName: string): Promise; /** * Parse pre-read MCP.md content into a `CatalogEntry`. * Callers that already hold the file content (e.g. to avoid a second read) use * this directly; `fromMcpServerMd` is a thin wrapper that reads then delegates here. * * @param content - UTF-8 content of the `MCP.md` file * @param repoName - Repository name used as the `publisher` field of the entry * @param filePath - Absolute path (used as `source` and for name fallback) * @returns Populated `CatalogEntry` with kind `"mcp-server"` */ export declare function fromMcpServerMdContent(content: string, repoName: string, filePath: string): CatalogEntry; /** * Parse a `knowledge.yaml` file into a `CatalogEntry` with `kind: "knowledge"`. * Falls back to the parent directory name when `name:` is absent. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the `knowledge.yaml` file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind `"knowledge"` * @docLink packages/core/concepts#from-knowledge-yaml */ export declare function fromKnowledgeYaml(filePath: string, repoName: string): Promise; /** * Parse a `CONNECTOR.md` file into a `CatalogEntry` with `kind: "connector"`. * Falls back to the parent directory name when `name:` is absent. * * Frontmatter fields (catalog identity): * - `name` — connector name (default: parent dir name) * - `description` — short description * - `version` — semver * * Frontmatter fields (runtime defaults, stored in `metadata`): * - `entry` — adapter entry point (default: "./adapter.ts") * - `npm_deps` — npm dependencies required by the adapter * - `fields` — configuration fields the adapter expects * - `health_check` — whether to run a health check on connect * * The markdown body (below the frontmatter) is stored in `metadata.body` * so the runner can inject the connector's skill description into the * system prompt when `expose_as_skill` is enabled. * * Async since #454 — the file may live on the FUSE workspace mount. * * @param filePath - Absolute path to the `CONNECTOR.md` file * @param repoName - Repository name used as the `repository` field of the entry * @returns Populated `CatalogEntry` with kind `"connector"` * @docLink packages/core/concepts#from-connector-md */ export declare function fromConnectorMd(filePath: string, repoName: string): Promise; interface WalkEntry { path: string; name: string; } /** Default maximum directory nesting the walk descends into. */ export declare const DEFAULT_WALK_MAX_DEPTH = 32; /** Default maximum number of directory entries a single walk may examine. */ export declare const DEFAULT_WALK_MAX_ENTRIES = 50000; /** Default wall-clock budget for one whole walk / scan pass, in milliseconds. */ export declare const DEFAULT_WALK_BUDGET_MS = 60000; /** Env override for {@link DEFAULT_WALK_MAX_DEPTH}. */ export declare const WALK_MAX_DEPTH_ENV_VAR = "SKAILE_SCAN_MAX_DEPTH"; /** Env override for {@link DEFAULT_WALK_MAX_ENTRIES}. */ export declare const WALK_MAX_ENTRIES_ENV_VAR = "SKAILE_SCAN_MAX_ENTRIES"; /** Env override for {@link DEFAULT_WALK_BUDGET_MS}. */ export declare const WALK_BUDGET_MS_ENV_VAR = "SKAILE_SCAN_DEADLINE_MS"; /** * Why a walk stopped early. Every one of these means the returned entries are a * **partial** view of the tree — never present a truncated walk as complete. * * @docLink packages/core/concepts#walk-dir */ export interface WalkTruncation { reason: "depth" | "entries" | "budget" | "cycle" | "unreadable"; /** Directory (or symlink) at which the bound was hit. */ path: string; /** The bound that was hit, when the reason has a numeric limit. */ limit?: number; } /** * Bounds for {@link walkDir} / {@link scanDirectory}. * * @docLink packages/core/concepts#walk-dir */ export interface WalkOptions { /** Maximum nesting depth below the root. Default {@link DEFAULT_WALK_MAX_DEPTH}. */ maxDepth?: number; /** Maximum directory entries examined across the whole walk. Default {@link DEFAULT_WALK_MAX_ENTRIES}. */ maxEntries?: number; /** * Wall-clock budget for the whole walk. Default {@link DEFAULT_WALK_BUDGET_MS}. * The check is inclusive, so `0` yields no entries at all. */ budgetMs?: number; /** Per-`opendir`/`stat`/`readFile` deadline. Default {@link fsDeadlineMs}. */ opTimeoutMs?: number; /** * Called once per distinct truncation reason. Defaults to a `warn` on the * core logger — override to route the signal somewhere the caller can act on. */ onTruncate?: (info: WalkTruncation) => void; } /** * Recursively yield every file entry under `dir`, sorted alphabetically at each * level. Skips `.git` and `node_modules` directories and follows symlinks * (resolving to the stat target). `node_modules` is excluded because Bun's * `node_modules/.bun/...` layout contains symlink farms that form cycles when * packages reference each other transitively. * * **Async and bounded since #454.** A repo root can resolve onto the rclone FUSE * mount, where the previous synchronous `readdirSync`/`statSync` walk parked the * whole event loop. Four bounds now hold, and hitting any of them reports a * {@link WalkTruncation} — a truncated walk is never presented as a complete one: * * - **depth** ({@link WalkOptions.maxDepth}) — also the backstop for a cycle no * inode check can see, e.g. a bind mount of the root inside itself. * - **entry count** ({@link WalkOptions.maxEntries}) — total directory entries * examined, so an enormous tree cannot run unbounded. * - **wall-clock budget** ({@link WalkOptions.budgetMs}) — a wedged mount that * times out one operation at a time cannot add up to an unbounded scan. * - **ancestor inodes** — the `dev:ino` of every directory on the path from the * root is held while its subtree is walked, so `a/link -> a` terminates * instead of recursing forever. Two sibling symlinks onto one shared * directory are a diamond, not a cycle: both are walked, at the price of * visiting the shared subtree twice — bounded by the entry and time budgets. * * @param dir - Absolute path to the directory to walk * @param options - Bounds and truncation sink; see {@link WalkOptions} * @docLink packages/core/concepts#walk-dir */ export declare function walkDir(dir: string, options?: WalkOptions): AsyncGenerator; /** * Walk `root` recursively and return a `CatalogEntry` for every recognized asset manifest found. * * Recognized filenames: `SKILL.md`, `AGENT.md`, `agent.yaml`, `CONTRACT.md`, `MCP.md`, * `CONNECTOR.md`, `knowledge.yaml`, `*.prompt.md`, `*.bundle.yaml`, `*.flow.yaml`, `*.flow.json`. * * When two entries share the same `kind:name` key, the one with the alphabetically * later source path wins (consistent with catalog precedence rules). * * **Async and bounded since #454.** `root` can resolve onto the rclone FUSE mount * (a `sources[].path` under `/skaile/workspace`), and the walk follows symlinks, so * even an off-mount root can descend into one. Every `opendir`/`stat`/`readFile` * is deadline-bounded and the walk is capped by depth, entry count, a wall-clock * budget, and an ancestor-inode set — see {@link walkDir}. Hitting any bound reports * a {@link WalkTruncation} through {@link WalkOptions.onTruncate} (a `warn` by * default); the returned array is then a **partial** scan, never a complete one. * * A manifest that cannot be read within its deadline is skipped rather than * failing the whole scan — one unreachable file must not cost the whole catalog. * * @param root - Absolute path to the directory tree to scan * @param repoName - Repository name attached to every produced entry * @param options - Walk bounds and truncation sink; see {@link WalkOptions} * @returns Array of deduplicated `CatalogEntry` objects, one per manifest file * @docLink packages/core/concepts#scan-directory */ export declare function scanDirectory(root: string, repoName: string, options?: WalkOptions): Promise; export {}; //# sourceMappingURL=manifest.d.ts.map