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

88 lines (87 loc) • 4.16 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/. import { isDeepStrictEqual } from "node:util"; import { parse as parseYaml } from "yaml"; import { localDateStamp } from "../common.js"; import { UsageError } from "../errors.js"; import { serializeFrontmatter } from "./asset-serialize.js"; import { parseFrontmatterBlock, replaceFrontmatterLine, spliceFrontmatterLine } from "./frontmatter.js"; /** * Ensure an AKM-authored Markdown concept is also a conformant OKF concept. * * Stamps BOTH `type` and `updated`, because both are required of a conformant * document and this is the one chokepoint every `.md` write passes through * (`core/write-source.ts`). Without the `updated` stamp, every asset akm * created for you — `akm remember`, `akm import`, accepted proposals, * authored workflows — was immediately flagged `missing-updated` by akm's own * `akm lint`, so the tool disagreed with itself about its own output. * * An existing `updated` is left alone: this fills a gap, it does not * re-stamp on every write (which would churn timestamps and manufacture * needless diffs in git-backed bundles). * * Source preservation: whenever the frontmatter block parses, it is edited as * text and never round-tripped through the YAML serializer, which rewraps long * values, reorders keys and drops comments — changes nobody made, shown to the * reviewer of what may be a one-line correction. A missing `type` and a * missing `updated` are each added as one line before the closing `---`, in * that order; a wrong `type` is replaced on its own line. Every other byte — * comments, wrapping, quoting, key order, line endings, the body — is kept as * written. The edited text is parsed back to confirm it holds exactly the * intended mapping; when it does not (a `type` value that spans several lines, * a flow-style `{…}` block), the document is re-serialized instead, as it * always used to be. A document with no frontmatter block gets a new one; * malformed YAML throws. */ export function ensureAkmMarkdownType(content, type, now = new Date()) { const block = parseFrontmatterBlock(content); if (!block) { return `---\n${serializeFrontmatter({ type, updated: localDateStamp(now) })}\n---\n${content}`; } let parsed; try { parsed = block.frontmatter.trim() ? parseYaml(block.frontmatter) : {}; } catch { throw new UsageError("AKM Markdown has malformed YAML frontmatter.", "INVALID_FLAG_VALUE"); } if (parsed === null) parsed = {}; if (typeof parsed !== "object" || Array.isArray(parsed)) { throw new UsageError("AKM Markdown frontmatter must be a YAML mapping.", "INVALID_FLAG_VALUE"); } const data = parsed; const updated = localDateStamp(now); const needsUpdated = !("updated" in data); if (data.type === type && !needsUpdated) return content; let edited = content; if (data.type !== type) { edited = "type" in data ? replaceFrontmatterLine(edited, "type", `type: ${type}`) : spliceFrontmatterLine(edited, `type: ${type}`); } if (edited !== null && needsUpdated) edited = spliceFrontmatterLine(edited, `updated: ${updated}`); const intended = needsUpdated ? { ...data, type, updated } : { ...data, type }; if (edited !== null && parsesTo(edited, intended)) return edited; // The text edit could not be done safely, but a re-serialized document // beats a non-conformant one. const { type: _priorType, ...rest } = data; const next = { type, ...rest }; if (needsUpdated) next.updated = updated; return `---\n${serializeFrontmatter(next)}\n---\n${block.content}`; } /** True when the frontmatter of `text` parses to exactly `intended`. */ function parsesTo(text, intended) { try { return isDeepStrictEqual(parseYaml(parseFrontmatterBlock(text)?.frontmatter ?? ""), intended); } catch { return false; } }