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

468 lines (467 loc) • 17.6 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/. export const NORMAL_DESCRIPTION_LIMIT = 250; const FRAGMENT_PROVENANCE_FIELDS = [ "selectedRef", "parentRef", "fragmentOrdinal", "fragmentCount", "startLine", "endLine", "previousRef", "nextRef", "fragmentChars", "fragmentEstimatedTokens", "parentChars", "parentEstimatedTokens", ]; const FRAGMENT_CONTEXT_FIELDS = ["contextMode", "contextMaxChars", "contextTruncated"]; export function shapeProposalProducerOutput(result, detail) { if (result.ok === false) { const base = { ok: false, reason: result.reason, error: result.error, ...(result.ref !== undefined ? { ref: result.ref } : {}), ...(result.type !== undefined ? { type: result.type } : {}), ...(result.name !== undefined ? { name: result.name } : {}), ...(result.engine !== undefined ? { engine: result.engine } : {}), ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}), ...(Array.isArray(result.notices) ? { notices: result.notices } : {}), }; if (detail === "full") { return { schemaVersion: result.schemaVersion, ...base, ...(result.stdout !== undefined ? { stdout: result.stdout } : {}), ...(result.stderr !== undefined ? { stderr: result.stderr } : {}), }; } return base; } const proposal = result.proposal; const base = { ok: result.ok, ref: result.ref, ...(result.engine !== undefined ? { engine: result.engine } : {}), ...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}), ...(Array.isArray(result.notices) ? { notices: result.notices } : {}), proposal: shapeProposalEntry(proposal, detail === "brief" ? "normal" : detail), }; if (detail === "full") { return { schemaVersion: result.schemaVersion, ...base }; } return base; } export function shapeProposalEntry(entry, detail) { if (detail === "brief") { // `proposal list`'s default detail level IS "brief" (no `--detail` flag // given projects here, not "normal") — `retirement` itself is not // projected until "normal", so a flagged retire proposal's risk would // otherwise be invisible by default. A minimal boolean marker, not the // full risk object, keeps brief's shape small while still letting a // reviewer see it without an extra flag. const retirement = entry.retirement; return { ...pickFields(entry, ["id", "ref", "status", "source", "createdAt"]), ...(retirement?.continuityRisk ? { continuityRisk: true } : {}), }; } if (detail === "normal") { // `confidence` and `gateDecision` (#577) explain why a proposal is pending, // so they are projected at `normal` for `akm proposal list/show` when present. // `retirement` (alpha.9) is the same kind of field for a consolidate // `retire` proposal: without it, "list/show" carries no reason a reviewer // could act on (its `payload.content` is empty by design — a retire // proposal deletes, it does not write). return pickFields(entry, [ "id", "ref", "status", "source", "sourceRun", "createdAt", "updatedAt", "confidence", "gateDecision", "feedback", "review", "reviewHistory", "retirement", ]); } // full: project everything including the payload. return pickFields(entry, [ "id", "ref", "status", "source", "sourceRun", "createdAt", "updatedAt", "confidence", "gateDecision", "feedback", "payload", "review", "reviewHistory", "retirement", "retiredArchive", "promotionSource", "promotionSourceHash", ]); } export function shapeProposalListOutput(result, detail) { const proposals = result.proposals; const shaped = proposals.map((p) => shapeProposalEntry(p, detail)); const base = { totalCount: result.totalCount, proposals: shaped, }; if (detail === "full") { return { schemaVersion: result.schemaVersion, ...base }; } return base; } export function shapeProposalShowOutput(result, detail) { const proposal = result.proposal; const validation = result.validation; const base = { proposal: shapeProposalEntry(proposal, detail === "brief" ? "normal" : detail), ...(validation ? { validation } : {}), }; if (detail === "full") { return { schemaVersion: result.schemaVersion, ...base }; } return base; } export function shapeProposalAcceptOutput(result, detail) { const proposal = result.proposal; const base = { ok: result.ok, id: result.id, ref: result.ref, assetPath: result.assetPath, proposal: shapeProposalEntry(proposal, detail === "brief" ? "normal" : detail), }; if (detail === "full") { return { schemaVersion: result.schemaVersion, ...base }; } return base; } export function shapeProposalRejectOutput(result, detail) { const proposal = result.proposal; const base = { ok: result.ok, id: result.id, ref: result.ref, ...(result.reason !== undefined ? { reason: result.reason } : {}), proposal: shapeProposalEntry(proposal, detail === "brief" ? "normal" : detail), }; if (detail === "full") { return { schemaVersion: result.schemaVersion, ...base }; } return base; } export function shapeProposalDiffOutput(result, detail) { const base = { id: result.id, ref: result.ref, isNew: result.isNew, unified: result.unified, ...(result.targetPath !== undefined ? { targetPath: result.targetPath } : {}), // A retire proposal (#997): the diff alone is a file leaving, so the pair // verdict and the accept/revert note travel with it at every detail level. ...(result.op !== undefined ? { op: result.op } : {}), ...(result.retirement !== undefined ? { retirement: result.retirement } : {}), ...(result.note !== undefined ? { note: result.note } : {}), }; if (detail === "full") { return { schemaVersion: result.schemaVersion, ...base }; } return base; } export function shapeEventsOutput(result, detail) { const events = Array.isArray(result.events) ? result.events : []; const shapedEvents = events.map((event) => shapeEventEntry(event, detail)); const base = { ...(result.ref !== undefined ? { ref: result.ref } : {}), ...(result.type !== undefined ? { type: result.type } : {}), ...(result.run !== undefined ? { run: result.run } : {}), ...(result.since !== undefined ? { since: result.since } : {}), ...(typeof result.sinceOffset === "number" ? { sinceOffset: result.sinceOffset } : {}), // D-38: echo `--limit` through the shaped envelope too — `akmEventsList` // already conditionally includes it in its raw result (see // src/commands/events.ts) only when the caller passed it, so this mirrors // that same "present only if requested" convention. ...(typeof result.limit === "number" ? { limit: result.limit } : {}), totalCount: result.totalCount ?? shapedEvents.length, events: shapedEvents, }; if (typeof result.nextOffset === "number") { base.nextOffset = result.nextOffset; } if (typeof result.reason === "string") { base.reason = result.reason; } if (detail === "full") { return { schemaVersion: result.schemaVersion ?? 1, ...base }; } return base; } export function shapeEventEntry(entry, detail) { if (detail === "brief") { return pickFields(entry, ["eventType", "ref", "ts"]); } if (detail === "normal") { return pickFields(entry, ["eventType", "ref", "ts"]); } // full: project everything the reader emits. return pickFields(entry, ["id", "schemaVersion", "eventType", "ref", "ts", "metadata"]); } export function shapeSearchOutput(result, detail, shape = "human") { const forAgent = shape === "agent"; const hits = Array.isArray(result.hits) ? result.hits : []; const registryHits = Array.isArray(result.registryHits) ? result.registryHits : []; const shapedHits = forAgent ? hits.map((hit) => shapeSearchHitForAgent(hit)) : hits.map((hit) => shapeSearchHit(hit, detail)); const shapedRegistryHits = forAgent ? registryHits.map((hit) => shapeSearchHitForAgent(hit)) : registryHits.map((hit) => shapeSearchHit(hit, detail)); const searchMode = typeof result.searchMode === "string" ? result.searchMode : undefined; const warnings = Array.isArray(result.warnings) && result.warnings.length > 0 ? result.warnings : undefined; if (forAgent) { return { hits: shapedHits, ...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}), ...(searchMode ? { searchMode } : {}), ...(warnings ? { warnings } : {}), ...(result.tip ? { tip: result.tip } : {}), }; } if (detail === "full") { return { schemaVersion: result.schemaVersion, bundleDir: result.bundleDir, source: result.source, hits: shapedHits, ...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}), ...(result.tip ? { tip: result.tip } : {}), ...(searchMode ? { searchMode } : {}), ...(warnings ? { warnings } : {}), ...(result.timing ? { timing: result.timing } : {}), }; } return { hits: shapedHits, ...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}), ...(searchMode ? { searchMode } : {}), ...(warnings ? { warnings } : {}), ...(result.tip ? { tip: result.tip } : {}), }; } export function shapeRegistrySearchOutput(result, detail) { const hits = Array.isArray(result.hits) ? result.hits : []; const assetHits = Array.isArray(result.assetHits) ? result.assetHits : []; // Shape stash hits as registry type const shapedKitHits = hits.map((hit) => shapeSearchHit({ ...hit, type: "registry" }, detail)); // Shape asset hits by detail level const shapedAssetHits = assetHits.map((hit) => shapeAssetHit(hit, detail)); const shaped = { hits: shapedKitHits, ...(shapedAssetHits.length > 0 ? { assetHits: shapedAssetHits } : {}), ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}), }; if (detail === "full") { shaped.query = result.query; } return shaped; } export function shapeAssetHit(hit, detail) { if (detail === "brief") return pickFields(hit, ["assetName", "assetType", "action", "estimatedTokens"]); if (detail === "normal") { return capDescription(pickFields(hit, ["assetName", "assetType", "description", "stash", "action", "estimatedTokens"]), NORMAL_DESCRIPTION_LIMIT); } return hit; } export function shapeSearchHit(hit, detail) { if (hit.type === "registry") { if (detail === "brief") { // RegistrySearchHit uses `title` (not `name`); always project installRef // and score so callers can use the result without --detail full (QA #28). const out = pickFields(hit, ["title", "name", "installRef", "score"]); // Normalise: if only title exists, expose it as `name` for consistency if (out.title && !out.name) out.name = out.title; return out; } if (detail === "normal") { // `curated` was removed in v1 (spec §4.2). Renderers project optional // hit-level `warnings` instead so providers can surface non-fatal issues. const out = capDescription(pickFields(hit, ["title", "name", "description", "action", "installRef", "score", "warnings"]), NORMAL_DESCRIPTION_LIMIT); if (out.title && !out.name) out.name = out.title; return out; } return hit; } // Stash hit (local or remote) // `ref` is included at `brief` so agents can run `akm show <ref>` without // needing --detail full or --shape agent (REC-03). if (detail === "brief") { return pickFields(hit, ["type", "name", "ref", "action", "estimatedTokens", "keys"]); } if (detail === "normal") { // `warnings` is projected at `normal` so non-fatal hit-level issues are // visible without forcing callers up to `--detail full`. Optional // `quality` (v1 spec §4.2) is also surfaced when present so callers // can see why a `proposed` entry showed up under `--include-proposed`. const shaped = capDescription(pickFields(hit, ["type", "name", "description", "action", "score", "estimatedTokens", "warnings", "quality"]), NORMAL_DESCRIPTION_LIMIT); if (Array.isArray(hit.keys) && hit.keys.length > 0) shaped.keys = hit.keys; return shaped; } return hit; } /** Agent-optimized search hit: only fields an LLM agent needs to decide and act */ export function shapeSearchHitForAgent(hit) { const picked = pickFields(hit, [ "name", "ref", "type", "path", "editable", "editHint", "description", "action", "score", "estimatedTokens", "keys", ]); if (picked.editable !== false) delete picked.editHint; return capDescription(picked, NORMAL_DESCRIPTION_LIMIT); } export function capDescription(hit, limit) { if (typeof hit.description !== "string") return hit; return { ...hit, description: truncateDescription(hit.description, limit) }; } export function truncateDescription(description, limit) { const normalized = description.replace(/\s+/g, " ").trim(); if (normalized.length <= limit) return normalized; const truncated = normalized.slice(0, limit - 1); const lastSpace = truncated.lastIndexOf(" "); const safe = lastSpace >= Math.floor(limit * 0.6) ? truncated.slice(0, lastSpace) : truncated; return `${safe.trimEnd()}...`; } export function shapeShowOutput(result, detail, shape = "human") { if (shape === "agent") { const shaped = pickFields(result, [ "type", "name", "ref", "path", "editable", "editHint", "description", "action", "content", "template", "prompt", "run", "setup", "cwd", "activeRun", "toolPolicy", "modelHint", "agent", "parameters", "workflowTitle", "workflowParameters", "steps", "keys", "links", ...FRAGMENT_PROVENANCE_FIELDS, ...FRAGMENT_CONTEXT_FIELDS, ]); if (shaped.editable !== false) delete shaped.editHint; return shaped; } if (shape === "summary") { return pickFields(result, [ "type", "name", // ref is present on every show shape (human/summary/agent) — it is the // canonical identity of the asset, not an agent-only convenience field // (R-020). "ref", "description", "tags", "parameters", "workflowTitle", "action", "run", "origin", "keys", "links", ...FRAGMENT_PROVENANCE_FIELDS, ...FRAGMENT_CONTEXT_FIELDS, ]); } const base = pickFields(result, [ "type", "name", // ref is always projected, same as path/editable below (R-020). "ref", "origin", "action", "description", "tags", "content", "template", "prompt", "toolPolicy", "modelHint", "agent", "parameters", "workflowTitle", "workflowParameters", "steps", "run", "setup", "cwd", "activeRun", "keys", "links", ...FRAGMENT_PROVENANCE_FIELDS, ...FRAGMENT_CONTEXT_FIELDS, // ref, path, and editable are always projected — at every --detail level, // not just --detail full — so JSON consumers can locate and edit the // asset without needing --detail full (QA #7 / D-14). "path", "editable", ]); if (detail !== "full") { return base; } return { schemaVersion: 1, ...base, ...pickFields(result, ["editHint"]), }; } export function pickFields(source, fields) { const result = {}; for (const field of fields) { if (source[field] !== undefined) { result[field] = source[field]; } } return result; }