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

273 lines (225 loc) • 14.3 kB
# Adapters A **`BundleAdapter`** (`src/core/adapter/bundle-adapter.ts`) is the code that decides what a bundle's files *are*, how they're indexed, how `akm lint` validates them, and where a new item would be placed. 0.9.0 ships 11 built-in adapters — one per format in [Supported Formats](../reference/supported-formats.md). This page is the write-path and internals reference; that page is the compatibility table. For read-only native tool bundles, recognition is runtime translation, not conversion: the adapter projects native files into AKM's index/runtime shapes while the source file remains authoritative. AKM does not synchronize native formats, maintain translated canonical copies, or write agent/command assets back through the Claude/OpenCode adapters. See [Agent, Command, Engine, and Model Resolution](specs/agent-command-engine-model-design.md). > **Stability note.** The adapter set, bundle-recognition rules, and the > `bundles` config shape are still evolving — see [STABILITY.md](../../STABILITY.md). --- ## Probe order Each bundle root is owned by exactly **one** adapter, decided once at install/index time: 1. If the bundle's config entry sets `components.<id>.adapter` explicitly, that adapter wins — no detection runs. 2. Otherwise, every built-in adapter's `looksLikeRoot(root)` probe runs in a **fixed order**, most-specific markers first. The first probe that returns `true` claims the root. 3. If no probe fires, the bundle falls back to the `akm` adapter. The order is pinned by `BUILTIN_ADAPTERS` (`src/core/adapter/adapters/index.ts`) and enforced by the conformance suite's ordered-owner matrix — it, not any prose spec listing, is authoritative: ```text website-snapshot -- root manifest.json {url, fetchedAt} agent-skills -- a <name>/SKILL.md package at the bundle root claude -- root CLAUDE.md + commands/|agents/|skills/ opencode -- opencode.json(c), or root AGENTS.md + a tool dir dotenv -- every top-level dir is env/ and/or secrets/ akm-workflow -- a top-level .md with explicit type: workflow akm-task -- a top-level valid task source v4 .yml (schedule optional) llm-wiki -- root schema.md + pages/ akm -- .stash marker, or 2+ native subdirs, or fallback okf -- root index.md, or any .md with frontmatter type generic-files -- never auto-selected; explicit config only ``` The rationale for the ordering: - The three loosest probes — `llm-wiki`, `akm`, and `okf` — stay **last**. Native AKM evidence wins before OKF because AKM Markdown is an OKF superset; the `akm` probe is strict enough not to claim an OKF bundle merely because it contains one familiar directory name. - `akm.looksLikeRoot` fires on any root carrying a bundle-subdir-named directory — which includes a `.claude`/`.opencode` tool dir (`commands/`, `agents/`, `skills/`) and a dotenv bundle (`env/`, `secrets/`). So `claude`/`opencode`/`dotenv` come **ahead of** `akm`; their tighter markers claim those roots first, and `akm` still wins its own workspace root (which those tighter probes reject). - `website-snapshot`, `agent-skills`, `akm-workflow`, and `akm-task` carry disjoint, specific markers that fire on none of the other roots, so they sit at the front for clarity. - `generic-files` is explicit-config-only — its `looksLikeRoot` never fires — so it's last and can never shadow anything. --- ## `BundleAdapter` interface contract Transcribed from `src/core/adapter/bundle-adapter.ts`: ```ts interface BundleAdapter { readonly id: string; readonly version: string; // feeds incrementality + fingerprints readonly extensions: readonly string[]; // longest-match stripping + collision priority // REQUIRED — single-file recognition primitive. recognize(c: BundleComponent, file: FileContext): IndexDocument | null; // OPTIONAL — full-component scan for non-per-file layouts (website // snapshots, llm-wiki multi-file semantics). Absent -> the core walk // (git-aware, symlink-safe, skip-dirs) drives recognize() per file. index?(inst: BundleInstallation, c: BundleComponent): AsyncIterable<IndexDocument>; // OPTIONAL — item-scoped incrementality. Default: identity (one file = one item). affectedItems?(c: BundleComponent, changedPaths: string[]): string[]; // REQUIRED — native validation (change-transaction pre-commit + lint --fix). // Adapters MUST NOT write and MUST NOT read the live filesystem: ctx serves // a run snapshot with pending changes overlaid, plus a read-only // resolveRef for link/xref existence. validate(c: BundleComponent, changes: FileChange[], ctx: ValidateContext): Promise<Diagnostic[]>; // OPTIONAL — the CLOSED-FORM set of existing-file placements/read aliases // that could own `conceptId`, used for first-owner arbitration without // reading authored bytes or walking the bundle. readCandidates?(c: BundleComponent, conceptId: string): Array<{ path: string; conceptId: string }>; // OPTIONAL — placement / discovery. placeNew?(c: BundleComponent, conceptId: string): string; // where a new item would live directoryList?(c: BundleComponent): string[]; // owned dirs; feeds git exact-path staging looksLikeRoot?(root: string): boolean; // install-time probe } ``` `recognize` and `validate` are required on every adapter; `index`, `affectedItems`, `readCandidates`, `placeNew`, `directoryList`, and `looksLikeRoot` are optional capability methods. An adapter overriding `index()` must keep `recognize()` coherent with it (conformance: `index()` == fold of `recognize()` over the core walk) or declare component-level incrementality. `readCandidates` is intentionally distinct from both `extensions` and `placeNew`: extensions are non-exhaustive walk hints, while placement is a write-normalization policy. Read candidates preserve canonical directory manifests (for example `skills/<name>/SKILL.md`) so lookup, index-backed show, and workflow runtime share one physical-owner decision. Each candidate carries its canonical concept identity, so a byte-denying ownership probe never has to infer identity from the query. Path-derived recognition (`toCanonicalName`/`toAssetPath`, `src/core/asset/asset-placement.ts`) is a deterministic function of a file's path, so its inverse — every physical spelling that could own one conceptId — is computable in CLOSED FORM (#857): the canonical placement under the type's own bundle subdir, the loose off-canonical placement (an authored path elsewhere in the bundle, keyed off its full path relative to the bundle root instead of the type subdir — this is what `readCandidates` returns for it), and any type-specific duality (for example `env`'s `.env`/`<name>.env` spellings for the "default" alias). `readCandidates` enumerates the complete closed-form set directly from the conceptId; no bundle walk is needed, so no scan bound applies. The shared authority (`src/indexer/lookup/adapter-concept-owner.ts`) still probes every candidate with a byte-denying `FileContext` before trusting it, so an adapter returning a candidate the file does not actually own is harmless — it is simply filtered out. The core walk — the live indexer's per-dir walk, drained by `drainDirDocuments` — is one implementation carrying the security policy (symlink-safety, skip-dirs, git-awareness); adapters never reimplement it. Cross-component ref existence is a **core** base check, not an adapter concern. --- ## `placeNew()` wiring status The interface declares `placeNew()` as an optional capability method, and 9 of the 11 built-in adapters already implement it — all but `okf` and `website-snapshot` (`claude` and `opencode` inherit theirs from the shared tool-dir factory). **Nothing in the write path calls it yet**: AKM-native writes still resolve through AKM's own flat type→directory placement table (`src/core/asset/asset-placement.ts`'s `PLACEMENT_SPECS`), not through the owning adapter's `placeNew()`. Placement for every existing bundle is already correct today — this is a deliberately sequenced routing change, not unfinished behavior. It's scoped out of 0.9.0 by [D12](../architecture/specs/0.9.0-decisions.md#d12--bundleadapterplacenew-stays-unwired-until-010) and deferred to 0.10; nothing user-visible changes in 0.10 for this alone. See [STABILITY.md](../../STABILITY.md) ("On the horizon") for the up-to-date status if this changes. --- ## Write allowlists `placeNew()` wiring is a separate question from **which bundles AKM-native write commands are allowed to touch at all** — that's decided today by a small allowlist check, `assertAkmAssetWrite` (`src/core/write-source.ts`), run before any write command touches the filesystem. It compares the target bundle's detected (or configured) adapter id against an allowlist and throws a `UsageError` on a miss. The default allowlist is `["akm"]`. Two command families widen it explicitly: | Command | Allowlist adds | | --- | --- | | `akm env create` / `env remove` / `secret set` | `dotenv` | | `akm workflow create` | `akm-workflow` | Every other adapter — including every adapter that already implements `placeNew()` (`agent-skills`, `claude`, `opencode`, `akm-task`, `generic-files`) — is rejected by every AKM-native write command in 0.9.0. `akm task add` in particular uses the *default* allowlist (`akm` only), so a standalone `akm-task` bundle is read-only through AKM-native write commands even though `akm-task`'s own `placeNew()` is implemented. Reading, searching, and `akm lint` are unaffected by this allowlist — only creating, editing, or deleting through AKM's own commands is restricted. "Read-only" in [Supported Formats](../reference/supported-formats.md) means this write-command restriction, not a filesystem permission. This allowlist boundary is also where AKM's [execution boundary](../guides/concepts.md) principle shows up on the write side: AKM retrieves and validates every supported format, but it only lets its own write commands mutate a narrow, explicitly-declared set of targets — it does not widen write access to a format just because that format's adapter happens to expose a `placeNew()` implementation. --- ## Task adapter The native and standalone `akm-task` paths delegate to the authoritative task source parser, `parseTaskSource`, which accepts only `version: 4` (task source v4). A task source v4 document declares exactly one executable selector (`uses` or `run`), but its scheduling is OPTIONAL — a document with no `schedule:` is valid and runnable manually. A `version: 3` or `version: 2` document is no longer read by `src` at all: it fails closed with `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming `akm migrate` as the path forward. See [Tasks](../reference/tasks.md) for the public source contract and the migration path. --- ## Implementation caveats A few adapter-specific behaviors are easy to assume differently from how they actually work: - **`agent-skills` recognition/validation are decoupled.** The per-change half of `validate()` only inspects changes that resolve to an actual `SKILL.md`, so an invalid skill is still indexed as `skill` (with its raw, invalid name projected) and its field violations surface only for files that exist. `missing-skill-md` is the exception and runs as a separate directory pass over `ValidateContext.list`: a **top-level** directory that holds files but no `SKILL.md` anywhere within three levels is flagged. A package's own resource dirs (`pdf-processing/reference/`) are part of the item, not candidate packages, and are never considered. - **`claude`/`opencode` share one codec** (`tool-dir-shared.ts`) and differ only in instruction filename and adapter/component ids. Both accept only the canonical plural tool directories. - **`dotenv` redaction is a hard, adapter-level contract.** No frontmatter `type:` override can bypass it: `env` entries surface only key names, `secret` entries surface only the file name, never content. A `.env` file placed under `secrets/` is a secret (name-only), not an env group — the directory gate wins over the `.env` suffix. - **Task adapters share one task source parser.** A standalone `akm-task` bundle and task files owned by the native `akm` adapter both route through `parseTaskSource`, which accepts only `version: 4` (task source v4) — a `version: 3` or `version: 2` document fails closed with `TASK_SCHEMA_VERSION_UNSUPPORTED` and is never indexed, validated, or run. Task source v4 requires exactly one `uses` or `run` executable selector, but its `schedule:` is OPTIONAL. It recognizes `akm/command`, `commands/`, `workflows/`, and `scripts/` targets, refuses the github-action `uses:` variant entirely (removed in v4 — no equivalent spelling), and refuses agent/task refs and unsupported actions. `.yaml` is a collection hint only: recognition still gates on `.yml`, so the near miss is diagnosed but never indexed or scheduled. See the [Tasks reference](../reference/tasks.md). - **`llm-wiki`, `akm`, and `okf` all reserve `index.md`/`log.md`** as structural files — never indexed as concepts, never valid write targets — at any depth. - **`okf`'s lenient wording is not a lenient gate.** `missing-type` and broken-link findings are labelled `info:`/`warning:` in the diagnostic text, but diagnostics carry no severity field: `akm lint` funnels every finding into one `flagged` list, and `--fail-on-flagged` fails on any non-empty list. - **`generic-files` is the only adapter with no auto-detection at all.** `looksLikeRoot` always returns `false`; a bundle only gets this adapter through an explicit `components.<id>.adapter: "generic-files"` override. --- ## See also - [Supported Formats](../reference/supported-formats.md) — the compatibility table this page's internals back - [Asset Types](../reference/asset-types.md) — the `akm` adapter's 14 native asset types - [Concepts](../guides/concepts.md) — the retrieval loop and execution boundary these adapters serve - [Architecture](architecture.md) — sources, refs, the search pipeline, and the write-source chokepoint