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
174 lines (173 loc) • 9.47 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/.
/**
* Contribution command cluster (`akm agent`, `akm lint`) — the asset
* authoring / validation verbs. Extracted verbatim from src/cli.ts (WS6) so
* the God Module shrinks; the `main.subCommands.{agent,lint}` keys and every
* command's args / output shape stay byte-identical.
*
* These handlers branch on the result and set a non-zero `process.exitCode`
* conditionally (exit 1 on a failed dispatch, or on `--fail-on-flagged` lint
* findings) rather than emitting through the thrown-error path, so they keep
* the inline `runWithJsonErrors` form rather than migrating to
* `defineJsonCommand` (which is reserved for plain runWithJsonErrors+output
* handlers). `process.exitCode` (not `process.exit()`) so the process still
* exits via natural event-loop drain rather than skipping pending cleanup —
* see R-067 / F4.
*
* NOTE: the asset-authoring `propose` verb (formerly here) moved to
* `akm proposal new` (0.9 CLI overhaul, S8) — see
* src/commands/proposal/propose-cli.ts.
*/
import fs from "node:fs";
import { defineCommand } from "citty";
import { getStringArg, parsePositiveIntFlag } from "../../cli/parse-args.js";
import { EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
import { loadConfig } from "../../core/config/config.js";
import { UsageError } from "../../core/errors.js";
import { getHyphenatedBoolean } from "../../output/context.js";
import { akmLint } from "../lint/index.js";
import { akmAgentDispatch } from "./agent-dispatch.js";
const EXIT_GENERAL = EXIT_CODES.GENERAL;
export function readPromptStdin(read = () => fs.readFileSync(0, "utf8")) {
return read();
}
export const agentCommand = defineCommand({
meta: {
name: "agent",
description: "Dispatch an agent CLI (opencode, claude, …) with an optional agent persona and model defaults. Use --prompt for work; stored commands run through akm command run. Nonempty tool requests require separate operator authorization and are rejected by the current CLI.",
},
args: {
...GLOBAL_OUTPUT_ARGS,
"agent-ref": {
type: "positional",
description: "Optional agent asset ref (e.g. agents/code-reviewer). Resolves persona and model defaults; the current CLI rejects a nonempty tool request without separate operator authorization.",
required: false,
},
prompt: { type: "string", description: "Task prompt to pass to the agent" },
"prompt-stdin": {
type: "boolean",
description: "Read the task prompt from stdin (mutually exclusive with --prompt)",
default: false,
},
engine: { type: "string", description: "Agent engine to use (default: defaults.engine)" },
model: {
type: "string",
description: "Model override — accepts aliases (opus, sonnet, haiku) or exact platform model IDs. Overrides the model specified in the agent asset.",
},
"timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
cwd: {
type: "string",
description: "Working directory for the spawned agent (defaults to the current directory)",
},
},
async run({ args }) {
await runWithJsonErrors(async () => {
const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
const config = loadConfig();
const agentConfig = config;
// Preserve the selector; the common execution-source adapter resolves it.
const agentRef = getStringArg(args, "agent-ref");
const promptText = getStringArg(args, "prompt");
const promptStdin = args["prompt-stdin"] === true;
if (promptStdin && promptText !== undefined) {
throw new UsageError("--prompt-stdin cannot be combined with --prompt.", "INVALID_FLAG_VALUE");
}
const cwd = getStringArg(args, "cwd");
// The common invocation resolver loads a selected agent through its
// owning bundle adapter. This CLI never re-reads/show-projects the file.
const model = getStringArg(args, "model");
const stdinPrompt = promptStdin ? readPromptStdin() : undefined;
const result = await akmAgentDispatch({
engine: getStringArg(args, "engine"),
prompt: stdinPrompt ?? promptText,
...(agentRef === undefined ? {} : { agentRef }),
agentConfig,
...(model === undefined ? {} : { selection: { model } }),
...(cwd ? { cwd } : {}),
...(timeoutMs !== undefined && Number.isFinite(timeoutMs) ? { timeoutMs } : {}),
});
output("agent-result", result);
if (!result.ok) {
// R-067-style fix: this used to call `process.exit(EXIT_GENERAL)`
// directly, which terminates synchronously and skips any pending
// `finally`/cleanup up the call stack (including the dispatched
// agent process' own bookkeeping). `output()` has already run above,
// so there is nothing left in this handler that depends on
// terminating immediately — `process.exitCode` + `return` (this is
// the last statement anyway) lets the process exit naturally once
// the event loop drains.
process.exitCode = EXIT_GENERAL;
return;
}
});
},
});
export const lintCommand = defineCommand({
meta: {
name: "lint",
description: "Scan bundle .md files for structural issues (unquoted colons, missing updated field, orphaned stubs, placeholder stubs, missing name/type, stale paths, broken refs in body text and in refs/xrefs/supersededBy/contradictedBy frontmatter). Use --fix to auto-fix Tier 1 issues. Exits 0 on success regardless of findings; use --fail-on-flagged for CI fail-on-finding behavior.",
},
args: {
// R-051: `lint` is a raw `defineCommand` (not `defineJsonCommand`), so it
// does not get `GLOBAL_OUTPUT_ARGS` for free. `--format`/`--detail`/
// `--shape`/`--output` already parsed correctly here (this command has no
// positional for a stray value to fall into), so this is purely a
// `--help` visibility / consistency fix, not a behavior change.
...GLOBAL_OUTPUT_ARGS,
fix: {
type: "boolean",
description: "Apply auto-fixes in place",
default: false,
},
// Declared as its own arg rather than `alias: "auto-fix"`: citty's alias
// handling (node:util parseArgs `short` options) only supports
// SINGLE-character aliases. A
// multi-char alias rendered in help as `-auto-fix` (one dash) and parsed
// as a pile of junk single-char flags, so BOTH advertised spellings —
// `-auto-fix` and `--auto-fix` — silently ran a plain lint while claiming
// to fix. Two real boolean args, OR'd at the call site, work and render.
"auto-fix": {
type: "boolean",
description: "Apply auto-fixes in place (same as --fix)",
default: false,
},
dir: { type: "string", description: "Override bundle root directory (default: from config)" },
"fail-on-flagged": {
type: "boolean",
description: "Exit non-zero when summary.flagged > 0 (CI-friendly). Default: exit 0 regardless of findings.",
default: false,
},
// #884: NOT folded into `--fix`. Every other auto-fix repairs a structural
// defect in the file it edits; this one DELETES an assertion the user (or
// the improve pipeline) made about the belief graph, and it does so in
// files that are themselves perfectly well-formed. That is a data decision,
// so it stays opt-in behind its own flag even when `--fix` is present.
"prune-dangling-edges": {
type: "boolean",
description: "Opt-in repair: drop supersededBy/contradictedBy entries whose target has neither a file nor a prune tombstone. Modifies memories — review the plain `akm lint` report first.",
default: false,
},
type: {
type: "string",
description: "Only lint assets of this type (e.g. workflows, tasks, memories). akm bundles only — every other adapter validates the whole bundle and warns that the flag had no effect.",
default: undefined,
},
},
async run({ args }) {
await runWithJsonErrors(async () => {
const result = await akmLint({
fix: args.fix === true || getHyphenatedBoolean(args, "auto-fix"),
dir: getStringArg(args, "dir"),
typeFilter: getStringArg(args, "type"),
pruneDanglingEdges: getHyphenatedBoolean(args, "prune-dangling-edges"),
});
output("lint", result);
if (args["fail-on-flagged"] && result.summary.flagged > 0) {
process.exitCode = EXIT_GENERAL;
return;
}
});
},
});