UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

192 lines (191 loc) • 7.67 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * The `akm-workflow` adapter — akm 0.9.0 format-family work item (#46). * * A native akm workflow bundle. AKM Markdown and the strict local GitHub-shaped * `.yml` subset are peer source formats. Both compile through the source IR; * neither source is rewritten or projected onto the other. * * ── validate (spec §6 workflow row) ── * * Reuses the akm adapter's per-type workflow checks (shared base checks + * `placeholder-stub` + `invalid-workflow-structure`). Delegates to the SAME * `perTypeValidateChecks` / `runBaseValidateChecks` the `akm` adapter uses, so * workflow validation has one home. * * Conformance oracle (authored, DO NOT modify): fixture * `tests/fixtures/bundles/akm-workflow/` + goldens * `tests/fixtures/format-family-goldens/akm-workflow/{recognition,placement,lint,renderer}.json`. */ import fs from "node:fs"; import path from "node:path"; import { looksLikeGithubWorkflowSource } from "../../../workflows/compile.js"; import { parseFrontmatter } from "../../asset/frontmatter.js"; import { toPosix } from "../../common.js"; import { perTypeValidateChecks, workflowYamlSourceDiagnostics } from "./akm-lint.js"; import { hashContent, nonEmptyString, readTags, runBaseValidateChecks } from "./shared.js"; /** A native workflow bundle is single-component; its one component is `main`. */ const COMPONENT_ID = "main"; /** The two authoritative workflow source formats. `.yaml` is deliberately not accepted. */ const WORKFLOW_EXTS = new Set([".md", ".yml"]); /** Strip the recognized workflow extension from a component-root-relative path → conceptId. */ function conceptIdOf(relPath) { return toPosix(relPath).replace(/\.(?:md|yml)$/i, ""); } /** `README.md` (case-insensitive) is documentation, never the typed asset — mirrors the akm matcher stack's D-R6 reserved-file exclusion (`TYPED_DIR_DOC_FILES`). */ function isReservedDocFile(fileName) { return fileName.toLowerCase() === "readme.md"; } /** * Recognition gate: any `.md` file in this bundle IS a workflow (this * adapter's entire domain is workflows — spec §2.5, "residence under * workflows/"), UNLESS its frontmatter declares a DIFFERENT non-empty * `type:` (an explicit opt-out, e.g. a README-shaped doc that wants to be * something else). No content sniffing. */ function isWorkflowFile(raw) { const data = parseFrontmatter(raw).data; const type = data.type; return type === undefined || type === "workflow"; } function recognize(c, file) { if (!WORKFLOW_EXTS.has(file.ext)) return null; if (isReservedDocFile(file.fileName)) return null; const raw = file.content(); if (file.ext === ".md" && !isWorkflowFile(raw)) return null; const conceptId = conceptIdOf(file.relPath); const name = conceptId.split("/").pop() ?? conceptId; const parsed = parseFrontmatter(raw); const description = file.ext === ".md" ? nonEmptyString(parsed.data.description) : undefined; const body = file.ext === ".md" ? parsed.content : raw; const tags = file.ext === ".md" ? readTags(parsed.data.tags) : undefined; const doc = { ref: `${c.id}//${conceptId}`, bundle: c.id, component: COMPONENT_ID, conceptId, path: file.absPath, hash: hashContent(raw), adapterId: "akm-workflow", type: "workflow", name, content: body, }; if (description !== undefined) doc.description = description; if (tags !== undefined) doc.tags = tags; return doc; } async function validate(c, changes, ctx) { const diagnostics = []; for (const change of changes) { if (change.op === "delete") continue; const raw = change.after ?? (await ctx.readFile(change.path)); if (typeof raw !== "string") continue; const ext = path.extname(change.path).toLowerCase(); if (!WORKFLOW_EXTS.has(ext) || isReservedDocFile(path.basename(change.path))) continue; const relPath = toPosix(change.path); if (ext === ".yml") { diagnostics.push(...workflowYamlSourceDiagnostics(relPath, raw, relPath, c.root).errors); continue; } if (!isWorkflowFile(raw)) continue; const p = parseFrontmatter(raw); const parsed = { data: p.data, content: p.content, frontmatter: p.frontmatter }; diagnostics.push(...(await runBaseValidateChecks(relPath, parsed, c.root, ctx))); diagnostics.push(...(await perTypeValidateChecks({ type: "workflow", relPath, raw, data: parsed.data, frontmatter: parsed.frontmatter, body: parsed.content, ext, ctx, }))); } return diagnostics; } /** * True when a top-level file in `root` is workflow-shaped (used by * looksLikeRoot). Unlike `isWorkflowFile` (which admits an absent `type:` — * the lenient default ONCE a source is already known to be this bundle), * this requires an EXPLICIT `type: workflow` — the install-time probe is * choosing WHICH adapter owns an unconfigured root among several candidates * (spec §1.2), and an incidental `.md` with no frontmatter type at all * (common in an OKF or llm-wiki bundle) must not misclassify that root as * akm-workflow's. */ function hasTopLevelWorkflowFile(root, entries) { for (const entry of entries) { if (!entry.isFile()) continue; const ext = path.extname(entry.name).toLowerCase(); if (!WORKFLOW_EXTS.has(ext) || isReservedDocFile(entry.name)) continue; let raw; try { raw = fs.readFileSync(path.join(root, entry.name), "utf8"); } catch { continue; } if (ext === ".yml" && looksLikeGithubWorkflowSource(raw)) return true; if (ext === ".md" && parseFrontmatter(raw).data.type === "workflow") return true; } return false; } export const akmWorkflowAdapter = { id: "akm-workflow", version: "0.9.2", extensions: [".md", ".yml"], recognize, validate, readCandidates(c, conceptId) { const posix = toPosix(conceptId); const canonical = posix.replace(/\.(?:md|yml)$/i, ""); return /\.(?:md|yml)$/i.test(posix) ? [{ path: path.join(c.root, posix), conceptId: canonical }] : [ { path: path.join(c.root, `${posix}.md`), conceptId: canonical }, { path: path.join(c.root, `${posix}.yml`), conceptId: canonical }, ]; }, /** Markdown remains the default; an explicit `.md`/`.yml` suffix is preserved. */ placeNew(c, conceptId) { const posix = toPosix(conceptId); if (/\.(?:md|yml)$/i.test(posix)) return path.join(c.root, posix); return path.join(c.root, `${posix}.md`); }, /** Workflows live anywhere under the component root. */ directoryList() { return ["."]; }, /** * Install-time probe (§1.2): a root holding an explicitly typed Markdown * workflow or a complete GitHub-shaped YAML workflow at top level. */ looksLikeRoot(root) { let entries; try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return false; } return hasTopLevelWorkflowFile(root, entries); }, };