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
125 lines (124 loc) • 5.32 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/.
/**
* The `generic-files` adapter — akm 0.9.0 format-family work item (#46).
*
* The AKM-native FALLBACK family (spec §7). Any leftover file is classified by
* extension (open-question-5, RESOLVED — maintainer 2026-07):
* - a SCRIPT_EXTENSIONS extension → `script` (conceptId KEEPS the extension);
* - markdown / plain-text → `document` (conceptId STRIPS the extension);
* - everything else → `file` (conceptId keeps the extension).
* Placement is IDENTITY: `document` appends `.md`, `script`/`file` keep their
* natural path. D-R6 reserved files (`index.md`/`log.md`) are excluded from the
* catch-all so a structural listing is never indexed as a `document`.
*
* EXPLICIT-CONFIG ONLY: `generic-files` is never in the §1.2 auto-probe order —
* a user opts in deliberately — so `looksLikeRoot` NEVER fires. It has no
* type-specific validators (`validate` runs base checks only).
*
* Conformance oracle (authored, DO NOT modify): fixture
* `tests/fixtures/bundles/generic-files/` + goldens
* `tests/fixtures/format-family-goldens/generic-files/{recognition,placement,lint,renderer}.json`.
*/
import path from "node:path";
import { parseFrontmatter } from "../../asset/frontmatter.js";
import { toPosix } from "../../common.js";
import { SCRIPT_EXTENSIONS } from "../../recognition-util.js";
import { hashContent, nonEmptyString, RESERVED_FILES, readTags, runBaseValidateChecks } from "./shared.js";
/** A generic-files bundle is single-component; its one component is `main`. */
const COMPONENT_ID = "main";
/** Markdown / plain-text extensions classified as `document`. */
const DOCUMENT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".text"]);
function isReserved(base) {
return RESERVED_FILES.has(base.toLowerCase());
}
/** Classify by extension (open-question-5 predicate). */
function classify(ext) {
if (SCRIPT_EXTENSIONS.has(ext))
return "script";
if (DOCUMENT_EXTENSIONS.has(ext))
return "document";
return "file";
}
function recognize(c, file) {
const posix = toPosix(file.relPath);
const base = posix.split("/").pop() ?? posix;
if (isReserved(base))
return null; // D-R6: never index a structural listing
const type = classify(file.ext);
// document strips its extension; script/file keep the natural path.
const conceptId = type === "document" ? posix.replace(/\.[^./]+$/, "") : posix;
const name = conceptId.split("/").pop() ?? conceptId;
const raw = file.content();
const parsed = parseFrontmatter(raw);
const body = parsed.content;
const doc = {
ref: `${c.id}//${conceptId}`,
bundle: c.id,
component: COMPONENT_ID,
conceptId,
path: file.absPath,
hash: hashContent(raw),
adapterId: "generic-files",
type,
name,
content: body,
};
if (type !== "script")
doc.ownsPresentation = true;
const description = nonEmptyString(parsed.data.description);
if (description !== undefined)
doc.description = description;
const tags = readTags(parsed.data.tags);
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;
// No type-specific validators — base checks only (spec §6 script/document/file rows).
diagnostics.push(...(await runBaseValidateChecks(toPosix(change.path), parseFrontmatter(raw), c.root, ctx)));
}
return diagnostics;
}
export const genericFilesAdapter = {
id: "generic-files",
version: "0.9.0",
// Non-exhaustive HINT: generic-files recognizes ANY file — recognize() is the
// source of truth (empty extension list would drop the extension-strip hint).
extensions: [".md", ".txt"],
recognize,
validate,
readCandidates(c, conceptId) {
const posix = toPosix(conceptId);
const extension = path.extname(posix).toLowerCase();
const documentCandidates = [...DOCUMENT_EXTENSIONS].map((candidateExtension) => ({
path: path.join(c.root, `${posix}${candidateExtension}`),
conceptId: posix,
}));
return DOCUMENT_EXTENSIONS.has(extension)
? documentCandidates
: [{ path: path.join(c.root, posix), conceptId: posix }, ...documentCandidates];
},
/**
* IDENTITY placement (open-question-5): a conceptId that carries an extension
* (`script`/`file`) places to itself; an extension-less conceptId (a
* `document`) appends `.md`.
*/
placeNew(c, conceptId) {
const posix = toPosix(conceptId);
const hasExt = path.extname(posix) !== "";
return path.join(c.root, hasExt ? posix : `${posix}.md`);
},
/** generic-files is EXPLICIT-CONFIG ONLY — never auto-selected (§1.2), so this never fires. */
looksLikeRoot() {
return false;
},
};