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

109 lines (108 loc) • 5.42 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/. /** * Canonical asset-on-disk serialization. * * Before this module, 9+ call sites across `src/` independently reimplemented * `yamlStringify(fm).trimEnd() + "---\n…\n---\n\n${body}"` to assemble a * Markdown asset. The reimplementations drifted (different body normalization, * different separator newlines, different trailing-newline policy), which is * exactly the kind of silent format-shift the proposal-quality validators end * up chasing downstream. This file is the single point of truth for * "what does a well-formed AKM asset look like on disk". * * Two helpers are exported: * - `serializeFrontmatter(fm)` — YAML for the frontmatter block only, with * no `---` fences and no trailing newline. Single home for quoting style, * field-order policy, and trailing-whitespace rules. * - `assembleAsset(fm, body)` — frontmatter wrapped in `---` fences with a * blank line between the closing fence and the body, and exactly one * trailing `\n`. Single home for body normalization and the file-shape * contract. * * Contract (must hold for the dedup to be safe): * - Idempotent: `parseFrontmatter(assembleAsset(fm, body))` re-assembled * reproduces the same bytes. * - Field order: insertion order of `fm` is preserved (the caller controls * ordering; the helper never reorders). * - Quoting: `yaml.stringify` defaults — no custom quoting logic. * - Trailing newline: exactly one `\n` at end of output. * - Body normalization: leading newlines are stripped (`/^\n+/`). This * collapses the assorted `body.replace(/^\n+/, "")` / * `body.startsWith("\n") ? "" : "\n" + body` / bare `${body}` patterns * onto the most aggressive existing normalizer. */ import { stringify as yamlStringify } from "yaml"; /** * Serialize a frontmatter object to its on-disk YAML form, without `---` * fences and without a trailing newline. * * Two calls with the same input produce byte-identical output. Field order is * preserved from the input object's insertion order — callers control * ordering, the helper never reorders. */ export function serializeFrontmatter(frontmatter) { return yamlStringify(frontmatter).trimEnd(); } /** * Serialize a frontmatter object with every value `JSON.stringify`-quoted, * without `---` fences and without a trailing newline. * * Use this instead of {@link serializeFrontmatter} when the input is a * pre-validated LLM payload where `yaml.stringify` may emit shapes * (`|`-block scalars, anchors, unquoted multiline) that the project's * hand-rolled `parseFrontmatter` subset parser cannot read back. Every scalar * becomes a quoted JSON literal; arrays become `[<json>, <json>]` (space after * the comma). Field order is preserved from the input's insertion order. * * This is the single home for the "guaranteed-quoted scalars" serializer the * module doc above anticipates; before it, `distill` (array-aware) and * `distill/content-repair` (scalar-only) reimplemented it divergently. */ export function serializeFrontmatterQuoted(frontmatter) { return Object.entries(frontmatter) .map(([k, v]) => Array.isArray(v) ? `${k}: [${v.map((s) => JSON.stringify(s)).join(", ")}]` : `${k}: ${JSON.stringify(v)}`) .join("\n"); } /** * Assemble a complete asset file string from a frontmatter object and a body. * * Output shape: `---\n<yaml>\n---\n\n<body>\n` where: * - `<yaml>` is `serializeFrontmatter(frontmatter)`. * - `<body>` has any leading `\n` characters stripped. * - Exactly one `\n` terminates the file. * * Idempotent under round-trip through the project's `parseFrontmatter`. */ export function assembleAsset(frontmatter, body) { return assembleAssetFromString(serializeFrontmatter(frontmatter), body); } /** * Same fence/body assembly as `assembleAsset` but takes a pre-serialized * frontmatter string. Use this when a caller needs its own frontmatter * serializer (e.g. defensive single-line flattening for untrusted LLM * output, or JSON.stringify-per-value for guaranteed-quoted scalars) while * still sharing the canonical fence-and-body template. * * The `serializedFm` argument must already match `serializeFrontmatter`'s * contract: no `---` fences, no trailing newline. Trailing whitespace is * trimmed defensively. * * Output contract — identical to `assembleAsset`: * - `---\n<serializedFm>\n---\n\n<body>\n` * - body has leading `\n` characters stripped * - exactly one `\n` terminates the file * * This helper is the single point of truth for the fence-and-body template. * Two command surfaces (`distill`, `consolidate`) call it * directly because their inputs are pre-validated LLM payloads where the * full `yamlStringify` may emit shapes (`|`-block scalars, anchors) that * the project's hand-rolled `parseFrontmatter` subset parser cannot read. */ export function assembleAssetFromString(serializedFm, body) { const yaml = serializedFm.replace(/\s+$/, ""); const normalizedBody = body.replace(/^\n+/, ""); const withTrailingNewline = normalizedBody.endsWith("\n") ? normalizedBody : `${normalizedBody}\n`; return `---\n${yaml}\n---\n\n${withTrailingNewline}`; }