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

135 lines (134 loc) • 6.75 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/. /** * Pure shaping functions that select and trim fields from command result * objects according to the active detail level / agent mode. * * Every function in this module is side-effect free and operates on plain * `Record<string, unknown>` shapes, which makes them trivial to unit test. * * Output shapes are assembled EXPLICITLY here: each per-command module under * `src/output/shapes/` EXPORTS a pure `OutputShapeEntry[]` (no top-level * side effect), and this barrel imports those exports and registers them in a * single deterministic, order-independent pass (`BUILT_IN_OUTPUT_SHAPES`). * Dropping a module from the assembly array is a COMPILE error, not a silent * runtime gap. The central `shapeForCommand` dispatcher looks up the registry * and throws for unknown commands (v1 spec §9 — exhaustive registry, no silent * fallback). */ import { warnOnce } from "../core/warn.js"; import { curateShapes } from "./shapes/curate.js"; import { envListShapes } from "./shapes/env-list.js"; import { eventsShapes } from "./shapes/events.js"; import { migrateShapes } from "./shapes/migrate.js"; import { modelsListShapes } from "./shapes/models-list.js"; import { passthroughShapes } from "./shapes/passthrough.js"; import { proposalAcceptShapes } from "./shapes/proposal/accept.js"; import { proposalDiffShapes } from "./shapes/proposal/diff.js"; import { proposalListShapes } from "./shapes/proposal/list.js"; import { proposalProducerShapes } from "./shapes/proposal/producer.js"; import { proposalRejectShapes } from "./shapes/proposal/reject.js"; import { proposalReopenShapes } from "./shapes/proposal/reopen.js"; import { proposalShowShapes } from "./shapes/proposal/show.js"; import { getOutputShapeHandler, registerOutputShapes } from "./shapes/registry.js"; import { registrySearchShapes } from "./shapes/registry-search.js"; import { searchShapes } from "./shapes/search.js"; import { secretListShapes } from "./shapes/secret-list.js"; import { showShapes } from "./shapes/show.js"; // ── Explicit built-in shape assembly ────────────────────────────────────────── // Each entry below is a pure exported `OutputShapeEntry[]` from a per-command // module. The set is registered ONCE, deterministically, with no reliance on // import order. Removing a module from this list removes its registration — // and because each name is referenced statically, a deleted export fails to // compile instead of silently disappearing at runtime. const BUILT_IN_OUTPUT_SHAPES = [ ...searchShapes, ...curateShapes, ...registrySearchShapes, ...showShapes, ...eventsShapes, ...proposalListShapes, ...proposalShowShapes, ...proposalAcceptShapes, ...proposalRejectShapes, ...proposalReopenShapes, ...proposalDiffShapes, ...proposalProducerShapes, ...envListShapes, ...secretListShapes, ...migrateShapes, ...modelsListShapes, // Passthrough commands are registered last so an explicit dedicated handler // above always wins over the identity-stamp fallback for the same name. ...passthroughShapes, ]; registerOutputShapes(BUILT_IN_OUTPUT_SHAPES); /** * Commands whose shape handler implements the `summary` projection. For every * other command, `--shape summary` is a usage error (v1 §5 — honest rejection * for a soon-frozen contract, not a silent fallback to `human`). */ const SHAPE_SUMMARY_COMMANDS = new Set(["show"]); // ── `results` collection alias ────────────────────────────────────────────── // // Every list-returning command names its collection differently (`hits`, // `items`, `proposals`, `sources`, ...) — a caller cannot write one accessor // across commands without a per-command lookup table, and the wrong guess // (`d.get("hits")` against a `curate` response) reads as "no results" rather // than "wrong key", with nothing in the envelope to correct it. // // This maps each list-returning command to the field already holding its // collection, and `withResultsAlias` below adds a `results` key pointing at // that SAME array (not a copy) to the shaped output — in every `--shape` / // `--detail` combination, `human` included, so `--shape agent` needs no // separate handling to "guarantee" it. A new list-returning command MUST add // an entry here; there is no way to detect a missed one automatically, so the // survey deliberately lives in this one place rather than scattered per // handler. const LIST_RESULT_COLLECTION_KEYS = { search: "hits", curate: "items", "registry-search": "hits", "proposal-list": "proposals", list: "sources", // `akm bundle list` "env-list": "envs", "secret-list": "secrets", "registry-list": "registries", "workflow-list": "runs", "task-history": "rows", "log-list": "events", // `akm log list` "config-diff": "rows", // `akm config diff` "models-list": "rows", // `akm models list` }; function withResultsAlias(command, shaped) { const key = LIST_RESULT_COLLECTION_KEYS[command]; if (!key) return shaped; if (shaped === null || typeof shaped !== "object" || Array.isArray(shaped)) return shaped; const obj = shaped; if ("results" in obj) return shaped; const collection = obj[key]; if (!Array.isArray(collection)) return shaped; // Same array reference as `obj[key]`, never a copy, so `results` cannot // silently drift out of sync with the semantic key it aliases. return { ...obj, results: collection }; } export function shapeForCommand(command, result, detail, shape = "human") { let effectiveShape = shape; if (shape === "summary" && !SHAPE_SUMMARY_COMMANDS.has(command)) { warnOnce(`shape-summary-unsupported:${command}`, `[output] '--shape summary' is not supported for 'akm ${command}' (only 'akm show' has a summary projection); falling back to 'agent'.`); effectiveShape = "agent"; } const handler = getOutputShapeHandler(command); if (handler) { return withResultsAlias(command, handler(result, detail, effectiveShape)); } // v1 spec §9 (output-shape registry exhaustive): no silent JSON.stringify // fallback. A missing case here is a registration bug — fail loudly so // the caller (or its tests) sees the missing command name. throw new Error(`output shape not registered for command: ${command}`); }