fiemwl / src /lib /agentSkills /openapiParser.ts
automindy's picture
Upload 1980 files
6111b2b verified
Raw
History Blame Contribute Delete
6.71 kB
/**
* openapiParser.ts β€” parses docs/reference/openapi.yaml to extract endpoint info
* grouped by SkillArea. Used by the catalog and the generator.
*
* Reads the OpenAPI YAML synchronously at runtime (same pattern as
* src/app/api/openapi/spec/route.ts). Does NOT fetch via HTTP to remain
* usable as a standalone script/CI tool (D15).
*/
import fs from "node:fs";
import path from "node:path";
import yaml from "js-yaml";
import type { SkillArea } from "./types";
// ── Types ────────────────────────────────────────────────────────────────────
export interface OpenapiPath {
/** HTTP method (uppercase): "GET", "POST", etc. */
method: string;
/** OpenAPI path template, e.g. "/api/providers/{id}" */
path: string;
/** Summary from the operation object */
summary: string;
/** Description from the operation object (may be absent) */
description?: string;
/** OpenAPI tags */
tags: string[];
}
export interface ParsedOpenapi {
/** All endpoints keyed by "<METHOD> <path>" */
paths: Map<string, OpenapiPath>;
/** Endpoints grouped by SkillArea (only API-mapped areas) */
areas: Map<SkillArea, OpenapiPath[]>;
}
// ── Mapping: path prefix β†’ SkillArea ────────────────────────────────────────
/**
* Maps an API path prefix to the corresponding SkillArea.
* Order matters: more specific prefixes must come before generic ones.
*/
const PATH_AREA_MAP: Array<[string, SkillArea]> = [
// Auth
["/api/auth", "auth"],
["/api/session", "auth"],
// Providers
["/api/providers", "providers"],
["/api/provider-nodes", "providers"],
["/api/provider-models", "providers"],
// Models
["/api/v1/models", "models"],
["/api/models", "models"],
// Combos / routing
["/api/combos", "combos-routing"],
["/api/fallback", "combos-routing"],
// API Keys
["/api/keys", "api-keys"],
// Usage logs
["/api/usage", "usage-logs"],
// Budget / rate limit
["/api/rate-limit", "budget"],
["/api/budget", "budget"],
// Settings
["/api/settings", "settings"],
["/api/tags", "settings"],
// Proxies
["/api/settings/proxy", "proxies"],
// Cache
["/api/cache", "cache"],
// Compression / RTK
["/api/settings/compression", "compression"],
["/api/compression", "compression"],
["/api/context/rtk", "context-rtk"],
// Resilience
["/api/monitoring", "resilience"],
["/api/provider-metrics", "resilience"],
["/api/circuit-breakers", "resilience"],
// CLI tools
["/api/cli-tools", "cli-tools"],
// Tunnels
["/api/tunnel", "tunnels"],
// Sync / cloud
["/api/cloud", "sync-cloud"],
["/api/sync", "sync-cloud"],
// DB backups
["/api/system", "db-backups"],
["/api/backup", "db-backups"],
// Webhooks
["/api/webhooks", "webhooks"],
// MCP
["/api/mcp", "mcp"],
// A2A
["/a2a", "agents-a2a"],
// Version manager
["/api/services", "version-manager"],
["/api/version", "version-manager"],
// Inference (catch-all for /api/v1/* proxy endpoints)
["/api/v1", "inference"],
];
// ── HTTP methods recognised as operations ────────────────────────────────────
const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const;
// ── Parser ───────────────────────────────────────────────────────────────────
function resolveArea(urlPath: string): SkillArea | null {
for (const [prefix, area] of PATH_AREA_MAP) {
if (urlPath === prefix || urlPath.startsWith(prefix + "/") || urlPath.startsWith(prefix + "{")) {
return area;
}
}
return null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extractOperations(pathsObj: Record<string, any>): OpenapiPath[] {
const ops: OpenapiPath[] = [];
for (const [urlPath, pathItem] of Object.entries(pathsObj)) {
if (!pathItem || typeof pathItem !== "object") continue;
for (const method of HTTP_METHODS) {
const operation = pathItem[method];
if (!operation || typeof operation !== "object") continue;
ops.push({
method: method.toUpperCase(),
path: urlPath,
summary: String(operation.summary ?? ""),
description: operation.description ? String(operation.description) : undefined,
tags: Array.isArray(operation.tags) ? operation.tags.map(String) : [],
});
}
}
return ops;
}
/**
* Parses `docs/reference/openapi.yaml` and returns:
* - `paths`: all operations keyed by `"METHOD /path"`
* - `areas`: operations grouped by SkillArea (api skills only)
*
* Reads the file synchronously so it can be called from both server context
* and standalone scripts without async machinery.
*/
export function parseOpenapi(): ParsedOpenapi {
const yamlPath = path.resolve(process.cwd(), "docs", "reference", "openapi.yaml");
let rawContent: string;
try {
rawContent = fs.readFileSync(yamlPath, "utf-8");
} catch (err) {
throw new Error(
`openapiParser: could not read ${yamlPath}. ` +
`Run from project root. Underlying error: ${err instanceof Error ? err.message : String(err)}`,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const doc = yaml.load(rawContent) as Record<string, any>;
if (!doc || typeof doc !== "object") {
throw new Error("openapiParser: parsed YAML is not an object");
}
const pathsObj = doc.paths ?? {};
const operations = extractOperations(pathsObj);
const paths = new Map<string, OpenapiPath>();
const areas = new Map<SkillArea, OpenapiPath[]>();
for (const op of operations) {
const key = `${op.method} ${op.path}`;
paths.set(key, op);
const area = resolveArea(op.path);
if (area) {
if (!areas.has(area)) {
areas.set(area, []);
}
areas.get(area)!.push(op);
}
}
return { paths, areas };
}
/**
* Returns endpoint strings for a given SkillArea, suitable for `AgentSkill.endpoints`.
* Format: `"GET /api/providers/{id}"`.
*/
export function getEndpointsForArea(area: SkillArea): string[] {
const { areas } = parseOpenapi();
const ops = areas.get(area) ?? [];
return ops.map((op) => `${op.method} ${op.path}`);
}