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
120 lines (119 loc) • 6.25 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/.
/**
* Single registration point for every akm agent harness (#562).
*
* `HARNESS_REGISTRY` is the ONE source of truth that replaces the three
* previously-disconnected registries (session-logs index, agent profiles,
* config/setup platform strings). All derived exports below are computed from
* it, so adding a harness is a single entry here instead of ~16 scattered edits.
*
* This module is a dependency-graph LEAF: it imports nothing from
* `core/config`. `core/config/config-types.ts` derives `VALID_HARNESS_IDS`
* from here, which keeps the import direction acyclic (config ← harnesses).
*/
import { AiderHarness } from "./aider/index.js";
import { AmazonqHarness } from "./amazonq/index.js";
import { ClaudeHarness } from "./claude/index.js";
import { CodexHarness } from "./codex/index.js";
import { CopilotHarness } from "./copilot/index.js";
import { GeminiHarness } from "./gemini/index.js";
import { HARNESS_ID_TABLE } from "./ids.js";
import { OpencodeHarness } from "./opencode/index.js";
// Import the descriptor from its LEAF module, not the per-harness barrel: the
// barrel re-exports `./sdk-runner` (which imports `core/config`, and
// `core/config/config-types` derives `VALID_HARNESS_IDS` back from this file).
// Importing the class through the barrel would form an initialization cycle
// that throws a TDZ error when the barrel is the first module loaded in a fresh
// graph (see ./opencode-sdk/harness.ts). This keeps the registry a config-leaf.
import { OpencodeSdkHarness } from "./opencode-sdk/harness.js";
import { OpenhandsHarness } from "./openhands/index.js";
import { PiHarness } from "./pi/index.js";
import { isSessionLogHarness } from "./types.js";
export { isSessionLogHarness } from "./types.js";
// Each harness descriptor (ClaudeHarness, OpencodeHarness, OpencodeSdkHarness)
// lives in its own per-harness directory alongside that harness's session-log
// reader, agent builder, config importer, and/or SDK runner (#563/#564). They
// are imported above and registered in HARNESS_REGISTRY below.
/**
* The single registration point. Add a harness here (and nothing else) to make
* every derived registry pick it up.
*
* Typed `as const` (not `AkmHarness[]`) so each entry keeps its literal `id`,
* which lets `VALID_HARNESS_IDS` stay a literal tuple — `HarnessId`,
* `z.enum(...)`, and the `platform` union all need the literal types.
*/
// Order is significant: VALID_HARNESS_IDS derives from this array and feeds the
// committed JSON-schema enum order. The original [opencode, claude,
// opencode-sdk] prefix is preserved so the pre-unification portion of the
// generated schema enum does not reorder; the seven P2 harness adapters are
// appended after it, which extends the enum additively (schemas/akm-config.json
// is regenerated in lockstep).
export const HARNESS_REGISTRY = Object.freeze([
new OpencodeHarness(),
new ClaudeHarness(),
new OpencodeSdkHarness(),
new CodexHarness(),
new CopilotHarness(),
new PiHarness(),
new GeminiHarness(),
new AiderHarness(),
new AmazonqHarness(),
new OpenhandsHarness(),
]);
// Construction-time drift guard (WI-9.8 KILL 3, D.3 edge B): ./ids.ts's
// HARNESS_ID_TABLE is config's dependency-free mirror of (id, agentDispatch)
// for every registered harness, kept out of core/config's import graph so
// config doesn't have to import this (heavier) barrel. Assert they match so
// the mirror can never silently drift — a new/reordered/recapability'd
// harness that forgets to update ./ids.ts fails loudly here instead of
// quietly breaking config's platform validation.
if (HARNESS_REGISTRY.length !== HARNESS_ID_TABLE.length) {
throw new Error(`harnesses/ids.ts HARNESS_ID_TABLE has ${HARNESS_ID_TABLE.length} entries but HARNESS_REGISTRY has ${HARNESS_REGISTRY.length} — update ids.ts to match.`);
}
HARNESS_REGISTRY.forEach((h, i) => {
const expected = HARNESS_ID_TABLE[i];
if (!expected || expected.id !== h.id || expected.agentDispatch !== h.capabilities.agentDispatch) {
throw new Error(`harnesses/ids.ts HARNESS_ID_TABLE[${i}] (${expected?.id ?? "<missing>"}, agentDispatch=${expected?.agentDispatch}) ` +
`does not match HARNESS_REGISTRY[${i}] (${h.id}, agentDispatch=${h.capabilities.agentDispatch}) — update ids.ts to match.`);
}
});
/** Lookup by the one supported harness id. */
export const HARNESS_BY_ID = new Map(HARNESS_REGISTRY.map((h) => [h.id, h]));
/**
* Canonical, ordered list of valid harness / platform ids. The Zod
* `AgentPlatformSchema` enum, the agent-engine platform union, and
* setup's `DetectedHarness` union all derive from this so they cannot drift.
*/
export const VALID_HARNESS_IDS = Object.freeze(HARNESS_REGISTRY.map((h) => h.id));
/**
* Harnesses that expose readable native session logs. Narrowed via the
* {@link isSessionLogHarness} type-predicate (rather than a plain boolean
* callback) so `sessionLogProvider` is known-present on every element — see
* that function's doc comment for why a plain predicate doesn't narrow here.
*/
export const SESSION_LOG_HARNESSES = HARNESS_REGISTRY.filter(isSessionLogHarness);
/** Harnesses that can be dispatched as an agent CLI / SDK. */
export const AGENT_DISPATCH_HARNESSES = HARNESS_REGISTRY.filter((h) => h.capabilities.agentDispatch);
/**
* Resolve an exact harness id to its descriptor, or `undefined` if unknown.
*/
export function getHarness(id) {
return HARNESS_BY_ID.get(id);
}
/**
* Default agent-profile name for a detected harness id (#566).
*
* Used by `akm setup`'s headless/recommended-config path so a newly added
* dispatch-capable harness gets a usable default profile name (its canonical
* id) instead of falling through a hardcoded if-chain with no default. Returns
* `undefined` for `"none"` or any id that is unknown / not agent-dispatch
* capable.
*/
export function defaultProfileName(detected) {
const h = HARNESS_BY_ID.get(detected);
if (!h?.capabilities.agentDispatch)
return undefined;
return h.id;
}