@buddy-works/docs-schema
v0.1.3
Published
Buddy's YAML and REST API schemas, fetched, cached and resolved into documented shapes
Readme
@buddy-works/docs-schema
Fetches Buddy's YAML and REST API schemas and resolves them into shapes you can
render or search. It follows $ref pointers, flattens allOf, and keeps enums,
constraints and examples.
Installation
npm install @buddy-works/docs-schemaUsage
import { BuddySchema } from "@buddy-works/docs-schema";
const parser = await BuddySchema.load();
await parser.actions.list(); // every action a pipeline can run
await parser.actions.get("AMAZON_S3"); // one action, in full
await parser.actions.search("notify"); // the ones that send notifications
await parser.search("deploy to s3"); // across YAML and the REST API
await parser.api.endpoint("GET", "/workspaces/{workspace_domain}/projects");How it gets the schemas
BuddySchema.load() reads from a local cache and revalidates over HTTP with an
ETag once the cache is 3 hours old. A 304 keeps the cached copy and starts the
window again. A 200 replaces the document. Nothing is fetched until a call needs
it, so a process that only asks about actions never downloads the OpenAPI
document.
In a long-running process such as an MCP server, the check runs in the background and the call answers from memory. A query never waits on the network. When a 200 lands, the new document takes over and the shaped results derived from the old one are dropped. A query already in flight finishes against the graph it started with.
A response that does not parse, or that lacks the key its scope depends on, is
refused. The previous good copy stays in place rather than being overwritten by an
error page. If the server cannot be reached at all, the cached copy keeps being
served and stats() reports staleSince for that document.
It reads production unless environment says otherwise. The other names are
"stage", "master" and "dev". Each one caches separately, so a process can
hold two at once:
const live = await BuddySchema.load();
const stage = await BuddySchema.load({ environment: "stage" });Supplying the documents yourself
If you already have the schemas, hand them over instead of letting the package fetch. This covers a pinned version, a file on disk, or a schema that is not published. Nothing is fetched and nothing is cached:
import { readFileSync } from "node:fs";
const parser = BuddySchema.from({
pipelines: JSON.parse(readFileSync("yaml-schema.json", "utf8")),
});The keys are documents, not scopes, and pipelines backs two of them:
| Key | Scopes it serves |
|---|---|
| pipelines | parser.pipelines, parser.actions |
| targets | parser.targets |
| environments | parser.environments |
| sandboxes | parser.sandboxes |
| agents | parser.agents |
| domains | parser.domains |
| customActions | parser.customActions |
| distributions | parser.distributions |
| api | parser.api |
Reaching a scope whose document you did not supply throws and names what was given.
Methods
Ten scopes over those nine documents. What a scope offers depends on what its
schema declares, and the types enforce it. parser.agents.list() does not
compile, because the agents schema has no union.
Every YAML scope
| Method | What it does |
|---|---|
| names(options?) | Definition names in the document, minus the ones the schema retired. { includeDeprecated: true } lists those too.await parser.pipelines.names() → ["PipelineYaml", "TransferActionYaml", …] |
| def(name) | One definition as a document: fields, types it refers to, examples.await parser.pipelines.def("VariableYaml") |
| properties(name) | The same definition flattened into property rows, the shape a table renders. Empty for a name the document does not declare.await parser.pipelines.properties("VariableYaml") |
| search(query, options?) | Ranked hits. The query is split into words and results rank by how many they match, so a whole question works.await parser.actions.search("notify") → ROLLBAR, SMS, HONEYBADGER…await parser.search("deploy to s3") → AMAZON_S3… |
| search(query, { fuzzy: true }) | The same, tolerating typos. Off by default, because a loose match out-ranks the exact one.await parser.actions.search("varibles", { fuzzy: true }) → SET_VARIABLES… |
| search(query, { includeDeprecated: true }) | Retired members and definitions too. Off by default, like list(): searching is how a caller finds what to use.await parser.actions.search("sandbox") → the live ones only |
Scopes with a root
pipelines, targets, environments, sandboxes, agents, domains,
customActions, distributions
root() gives the document's entry type: what a YAML file at that path may
contain. await parser.pipelines.root() returns the PipelineYaml document.
Scopes with a union
actions, targets, environments
| Method | What it does |
|---|---|
| list(options?) | The discriminator values the union accepts, minus the ones the schema retired.await parser.targets.list() → ["SSH", "FTP", "MATCH", …]{ includeDeprecated: true } lists those too. get() and search() answer for a retired member either way. |
| get(value) | One member in full: name, docs slug, tags, required fields, examples, properties, and the shaped document.await parser.actions.get("BUILD") |
| commonFields() | Fields every member of the union declares.await parser.actions.commonFields() → ["type", "action", "trigger_time", …] |
api
| Method | What it does |
|---|---|
| operations() | Every operation the document declares, with its summary, operationId, tags, deprecated and security. Nothing is resolved, so listing every endpoint costs one walk rather than one per operation.await parser.api.operations() |
| endpoint(method, path, variant?) | The operation as a document: parameters, request body, responses. Pass variant to pick one branch of a union body.await parser.api.endpoint("POST", "/oauth2/register")Bodies are a list: request.contents and each response's contents hold one entry per media type, in the document's order, so a download offering a byte stream and a JSON envelope reports both. A status that declares no content has none. |
| variants(method, path) | The discriminator values you can pass as variant. Empty when the body branches by named type instead. Those shapes are on the document, under request.contents[0].body.variants. |
| type(name) | One named schema as a document, for a type name you read off a field. Saves refetching the endpoint it came from.await parser.api.type("ExecutionView") |
| typeNames(options?) | Every schema name the document declares, minus the retired ones. |
| search(query, options?) | As above, over operation summaries, paths and tags. |
On the client
| Method | What it does |
|---|---|
| search(query, options?) | One index across every scope, YAML and REST together. Narrow it with { scopes: ["actions"] }. Twenty hits unless limit says otherwise.Each hit carries kind, scope, key, name, tags, score. |
| raw(document) | The unresolved document, $ref pointers intact. For a validator compiling against them, or for reading $defs directly. |
| prefetch(documents?) | Load now rather than on the first request. Under from() this reports { refreshed: [], reason: "documents were supplied" } rather than throwing. |
| release(document) | Drop a fetched document and its resolution memo. The shaped-result cache is shared, so it is emptied whole rather than per document. Returns false for a supplied document, which cannot be reacquired. |
| stats() | Which documents are held, their ETag and fetch time, which deployment they came from, whether a write to the cache failed, and the result cache's entries, hits and misses. |
Options
await BuddySchema.load({ environment, cacheDir, ttlMs, results });
BuddySchema.from(documents, { results });environment is one of the names above. cacheDir and ttlMs fall back to the
variables below. results bounds how many shaped documents are held. Pass false
to shape every call afresh.
Errors
DocumentUnavailableError: nothing fetched and no cached copy. Names the URL and points atBuddySchema.from().MissingDocumentError: a scope was reached whose documentfrom()was not given. Lists what was.ScopeUnsupportedError: a supplied document is not the schema that scope needs. Says what was expected, what was found, and the nearest existing name.
Requirements
Node 20.19 or newer, or Bun.
The cache lives in ~/.buddy/docs-schema, one pair of files per document: the
body and its ETag. Two environment variables override the defaults:
BUDDY_DOCS_SCHEMA_CACHE: cache directory.BUDDY_DOCS_SCHEMA_TTL: milliseconds a cached copy stays fresh.0revalidates on every access.Infinitypins a run to the version it started with.
