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
100 lines (99 loc) • 4.06 kB
JavaScript
// 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/.
/**
* Resolve the stash-authoring "standards" context (Feature B of the standards
* plan): gather the bodies of `fact` assets whose `category` frontmatter is
* `convention` or `meta` so naming / tag / frontmatter conventions are surfaced
* to the agent when it creates or edits a non-wiki asset.
*
* Selection is by **frontmatter `category`**, never by path — flat
* (`facts/x.md`) and nested (`facts/conventions/x.md`) layouts resolve
* identically. The MVP does no parsing of fenced blocks, no rule objects, and
* no warnings: it concatenates the selected facts' bodies in stable enumeration
* order, each preceded by a one-line `# <ref>` provenance header. Returns `""`
* when no matching facts exist.
*/
import fs from "node:fs";
import path from "node:path";
import { parseFrontmatter } from "../asset/frontmatter.js";
/** `category` values that mark a fact as an authoring standard. */
const STANDARD_CATEGORIES = new Set(["convention", "meta"]);
/** Directory (under the stash root) where `fact` assets live. */
const FACTS_SUBDIR = "facts";
/**
* Per-type SOFT convention facts (`facts/conventions/assets/<type>.md`, #646)
* are surfaced **type-scoped** through `resolveTypeConventions`, so they must
* NOT leak into this un-type-scoped general layer (authoring a `command` must
* not pull the `skill` convention). Excluded by relative path (POSIX form).
*/
const TYPE_CONVENTIONS_REL = "conventions/assets/";
/**
* Recursively collect `.md` files under `dir` in stable (sorted) enumeration
* order. Returns absolute paths. Missing dir → `[]`.
*/
function collectMarkdownFiles(dir) {
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
}
catch {
return [];
}
const results = [];
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...collectMarkdownFiles(full));
}
else if (entry.isFile() && entry.name.endsWith(".md")) {
results.push(full);
}
}
return results;
}
/**
* Derive a fact ref (`facts/conventions/naming`) from an absolute markdown path
* relative to the facts root. Mirrors the canonical-name derivation in
* `asset-spec.ts` (POSIX separators, `.md` stripped).
*/
function toFactRef(factsRoot, absPath) {
const rel = path.relative(factsRoot, absPath).split(path.sep).join("/");
const name = rel.endsWith(".md") ? rel.slice(0, -3) : rel;
return `${FACTS_SUBDIR}/${name}`;
}
export function resolveStashStandards(stashRoot) {
const factsRoot = path.join(stashRoot, FACTS_SUBDIR);
const sections = [];
for (const absPath of collectMarkdownFiles(factsRoot)) {
// Per-type SOFT conventions are delivered type-scoped (#646); skip them
// here so they never leak un-type-scoped into every authoring flow.
const relPosix = path.relative(factsRoot, absPath).split(path.sep).join("/");
if (relPosix.startsWith(TYPE_CONVENTIONS_REL))
continue;
let raw;
try {
raw = fs.readFileSync(absPath, "utf8");
}
catch {
continue;
}
let category = "";
let body = "";
try {
const parsed = parseFrontmatter(raw);
category = typeof parsed.data.category === "string" ? parsed.data.category.trim() : "";
body = parsed.content;
}
catch {
continue;
}
if (!STANDARD_CATEGORIES.has(category))
continue;
const trimmed = body.trim();
if (!trimmed)
continue; // skip stub facts with frontmatter but no body
sections.push(`# ${toFactRef(factsRoot, absPath)}\n${trimmed}`);
}
return sections.join("\n\n");
}