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

125 lines (124 loc) • 5.32 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 `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; }, };