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
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/.
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;
}