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

106 lines (105 loc) • 4.81 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 fs from "node:fs"; import path from "node:path"; import { adapterForId, getAdapters } from "./registry.js"; /** * Tool-dir-shaped adapters (#908): their install-time probe recognizes only a * BOUNDED slice of the root — `claude`/`opencode`'s own `commands`/`agents`/ * `skills` tool dirs, or `agent-skills`' own `<name>/SKILL.md` packages. A * bundle that ALSO carries ordinary akm content (`knowledge/`, `workflows/`, * a stray `content/` folder of Markdown, …) alongside one of these layouts had * that content SILENTLY dropped: the narrow adapter won the ordered probe and * indexed only its own three-dir slice, with no warning that anything else * was there (issue #908 — 73 documents disappeared from one real bundle). * * `okf` / `llm-wiki` / `dotenv` / `website-snapshot` / `akm-workflow` / * `akm-task` are deliberately NOT in this set: each carries its own tight, * disjoint marker (a root index document, `schema.md`+`pages/`, an env/secrets-only * layout, `manifest.json`, a workflow/task-shaped top-level file) that is not * at risk of firing merely because a FEW directory names happen to overlap * with akm's own stash subdirs — narrowing the fix to the three families the * issue is actually about keeps this from touching adapters it was never * about. */ const SHADOWABLE_ADAPTER_IDS = new Set(["agent-skills", "claude", "opencode"]); /** * True when `root` — already claimed by `winnerId` (one of * {@link SHADOWABLE_ADAPTER_IDS}) — ALSO carries a top-level directory the * `akm` adapter's own probe recognizes as its workspace shape (spec §1.2) and * that `winnerId` does not own (its `directoryList()`, or — for `agent-skills`, * which owns no fixed directory names — a root-level `<name>/SKILL.md` * package) but which holds at least one real file. Cheap by design: only * shallow `readdirSync` calls, one level into each candidate top-level dir — * no recursive walk, no file content read, no git spawn — so this never adds * meaningful cost to the install-time probe it augments. */ function hasExtraAkmContent(root, winnerId) { const akm = adapterForId("akm"); if (akm?.looksLikeRoot?.(root) !== true) return false; const winner = adapterForId(winnerId); const stubComponent = { id: "detect", adapter: winnerId, root, writable: false }; const ownedDirs = new Set(winner?.directoryList?.(stubComponent) ?? []); let entries; try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return false; } for (const entry of entries) { if (!entry.isDirectory() || entry.name.startsWith(".")) continue; if (ownedDirs.has(entry.name)) continue; if (winnerId === "agent-skills") { // A root-level `<name>/SKILL.md` package IS agent-skills' own recognized // surface even though it has no fixed directoryList — not "extra" content. try { if (fs.statSync(path.join(root, entry.name, "SKILL.md")).isFile()) continue; } catch { // Not a skill package — fall through to the candidate-file check below. } } let children; try { children = fs.readdirSync(path.join(root, entry.name), { withFileTypes: true }); } catch { continue; } if (children.some((child) => child.isFile() && !child.name.startsWith("."))) return true; } return false; } /** * Select the first built-in adapter whose ordered root probe claims `root`. * * A mixed-layout bundle (#908) that both a tool-dir-shaped adapter AND the * `akm` adapter would claim detects as `akm` — the superset, which still * indexes the narrower layout's own files correctly (`agent-skills`' * `<name>/SKILL.md` packages, `claude`/`opencode`'s `commands`/`agents`/ * `skills`) while also picking up whatever else is in the bundle. A bundle * that carries ONLY the narrow layout (no extra content) is unaffected. */ export function detectAdapterId(root, fallback = "akm") { for (const adapter of getAdapters()) { try { if (adapter.looksLikeRoot?.(root) === true) { if (SHADOWABLE_ADAPTER_IDS.has(adapter.id) && hasExtraAkmContent(root, adapter.id)) { return "akm"; } return adapter.id; } } catch { // An unreadable or racing probe does not claim the bundle. } } return fallback; }