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
239 lines (238 loc) • 14.2 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/.
/**
* Default hint for each ConfigError code. Keep these short, actionable, and
* imperative. Returning undefined means "no canned hint".
*/
const CONFIG_HINTS = {
STASH_DIR_NOT_FOUND: "Run `akm setup` to create and configure your bundle, or configure a defaultBundle path.",
STASH_DIR_NOT_A_DIRECTORY: "The configured default bundle path exists but isn't a directory. Update it to point at a folder.",
STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update the default bundle path.",
DATA_DIR_UNREADABLE: "The data directory is not readable by the user running akm. Check its owner and mode, or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user owns.",
INDEX_SCHEMA_INCOMPATIBLE: "Run `akm index --full` to rebuild the derived index from the currently materialized sources.",
LLM_NOT_CONFIGURED: 'Run `akm setup` or configure an `engines` entry with `kind: "llm"`, then select it with `defaults.llmEngine`.',
TEST_ISOLATION_MISSING: "Under bun test, when AKM_BUNDLE_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
UNSAFE_STASH_DIR: "Choose a path inside your home directory (e.g. ~/akm) or another empty workspace. The bundle directory cannot be the filesystem root, your home directory itself, or a sensitive system path like /etc, /var, ~/.config, or ~/.ssh.",
UNKNOWN_IMPROVE_STRATEGY: "Pass one of the listed strategy names to `--strategy`, or define it under `improve.strategies`. Names are case-sensitive.",
EXECUTION_NOT_AUTHORIZED: "Change the selected tools or update the machine/user execution policy, then retry.",
SECRET_REFERENCE_UNRESOLVED: "Check the secret exists (`akm secret list`) and the name after `secret://` matches, or run `akm secret set <name> <value>` to store it.",
REGISTRY_URL_INVALID: "Registry URLs must be credential-free http(s) URLs. Fix the entry with `akm registry list` / `akm registry add`, or the AKM_REGISTRY_URL / AKM_NPM_REGISTRY variable that supplied it.",
};
// Code-review finding: COMPOSITION_INVALID covers several unrelated causes
// (a rejected with:, a multi-job source, a composition cycle/depth/size
// violation, an invalid child-output reference). USAGE_HINTS below carries
// only the with:-rejection text — accurate for every with:-rejection throw
// site (none of which pass an explicit constructor hint), but wrong for the
// others. Those throw sites pass their OWN explicit hint (the constructor's
// 3rd argument overrides USAGE_HINTS, per errors-usage-hints.test.ts's
// "explicit constructor hint still overrides the USAGE_HINTS default").
// Multi-job rejection is thrown from 5 separate call sites across
// src/tasks/prepare/prepare-support.ts and src/workflows/**, so its hint is
// centralized here as the one shared string all 5 import, rather than
// duplicated at each site and risking drift.
export const COMPOSITION_INVALID_MULTI_JOB_HINT = "AKM workflows support exactly one job per source, with no needs: between jobs. Split the extra job(s) into " +
"their own workflow file, and compose them with uses: workflows/<ref> instead.";
/** Default hint for each UsageError code. */
const USAGE_HINTS = {
INVALID_FLAG_VALUE: "Run `akm <command> --help` to see accepted values.",
INVALID_SOURCE_VALUE: "Pick one of: local, registry, all, or a configured source name.",
INVALID_FORMAT_VALUE: "Pick one of: json, jsonl, yaml, text, md, html.",
INVALID_DETAIL_VALUE: "Pick one of: brief, normal, full. For agent/summary projections use --shape.",
INVALID_SHAPE_VALUE: "Pick one of: human, agent, summary (summary falls back to agent, with a warning, on commands with no summary projection).",
INVALID_JSON_CONFIG_VALUE: 'Quote JSON values in your shell, for example: akm config set embedding \'{"endpoint":"http://localhost:11434/v1/embeddings","model":"nomic-embed-text"}\'.',
MISSING_OR_AMBIGUOUS_TARGET: "Use `akm bundle update --all` or pass a target like `akm bundle update npm:@scope/pkg` (not both).",
TARGET_NOT_UPDATABLE: "Run `akm bundle list` to view your sources, then retry with one of those values.",
MISSING_REQUIRED_ARGUMENT: "Refs use the form [bundle//]conceptId, e.g. `akm show knowledge/guide.md` or `akm show skills/deploy`.",
UNKNOWN_COMMAND: "Run `akm --help` to see available commands.",
UNKNOWN_FLAG: "Run the command with `--help` to see its accepted flags.",
// P2b (docs/plans/specs/p2b-input-bindings.md §1.7 A-N5, §7 F-A3): the
// "arrives in a later 0.9.x release" promise is gone now that task-call
// inputs are implemented. Names the two real rejection causes instead:
// (1) a task target that declares no inputs: at all, (2) commands/<ref> /
// scripts/<ref>, which are never binding surfaces. F-A3 authorizes this
// edit and the matching pinned-string update in
// tests/core/errors-usage-hints.test.ts in the same commit.
//
// Code-review finding: this default is reached ONLY by with:-rejection
// throw sites (the ones above, plus task.ts's noDeclaredInputsError and
// freeze.ts's resolveStep) — every other
// COMPOSITION_INVALID throw site (multi-job source, composition
// cycle/depth/size, invalid child-output reference, an env: on a
// composing step) passes its own explicit constructor hint instead of
// falling through to this text, so this stays scoped and accurate rather
// than generalized into something vaguer.
COMPOSITION_INVALID: "Remove the with: block, or target a tasks/<ref> whose source declares inputs: — commands/<ref> and scripts/<ref> steps are not binding surfaces.",
TASK_SOURCE_INVALID: "Fix the task source at the reported path and line, then re-run.",
TARGET_REF_INVALID: "Targets are canonical asset refs: `commands/review`, `scripts/build.sh`, `tasks/nightly`, `workflows/release`.",
// P4 (docs/plans/specs/p4-deletions-closeout.md §4.1, row B-53, R-R5): the
// original hint named `akm workflow validate`, a verb that was never
// implemented. Points at the two verbs that actually inspect a workflow
// source without executing it.
WORKFLOW_SOURCE_INVALID: "Run `akm lint` to see the failing source location, or `akm workflow plan <ref>` to compile it without writing.",
INPUT_BINDING_INVALID: "Check the step's with: keys against the target's declared inputs.",
TASK_TARGET_UNSUPPORTED: "Task definitions support command, script, workflow, and shell (run:) targets; akm/command is layered by callers.",
};
/** Default hint for each TransientErrorCode. */
const TRANSIENT_HINTS = {
RUN_LEASE_HELD: "Wait for the akm process driving this run (the named pid) to finish, then retry.",
STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs.",
IMPROVE_LOCK_HELD: "Another akm improve run holds the whole-run lock right now. Wait for it to finish and retry, or pass --skip-if-locked on scheduled runs.",
LOCKFILE_CONTENDED: "Another akm process is updating its bundle lockfile right now. Wait a few seconds and retry.",
ASSET_MUTATION_LEASE_HELD: "Another akm process is writing bundle content right now. Wait for it to finish and retry.",
SCHEDULER_LOCK_HELD: "Another `akm task sync`, `add`, `enable`, `disable` or `prune` is writing the scheduler right now. Wait for it to finish and retry.",
REGISTRY_UNREACHABLE: "The registry did not answer in time. Check the network and the registry URL (`akm registry list`), then retry.",
};
/** Default hint for each NotFoundError code. */
const NOT_FOUND_HINTS = {
ASSET_NOT_FOUND: "Run `akm search <query>` or `akm index` to refresh the index.",
SOURCE_NOT_FOUND: "Run `akm bundle list` to view your sources, then retry with one of those values.",
WORKFLOW_NOT_FOUND: "Run `akm workflow list --active` to see runs.",
// A proposal is addressed by id or ref, never by path — reusing
// FILE_NOT_FOUND here handed users "check the path exists and is readable"
// for a mistyped id, which points at the wrong thing entirely.
PROPOSAL_NOT_FOUND: "Run `akm proposal list` to see pending proposals and their ids.",
FILE_NOT_FOUND: "Check the path exists and is readable.",
IMPROVE_RUN_NOT_FOUND: "Run `akm improve` first, or `akm improve report --since 30d` to see recent run ids in `runIds`.",
REGISTRY_NOT_FOUND: "Check the ref's spelling and version; `akm search <query> --from registry` lists installable refs.",
};
/**
* Base class for all akm-thrown, classified errors. Carries the `kind`
* discriminant consumed by the CLI exit-code classifier. Errors that are NOT
* instances of `AkmError` are treated as genuinely unexpected (INTERNAL).
*/
export class AkmError extends Error {
}
/** Raised when configuration or environment is invalid or missing. */
export class ConfigError extends AkmError {
kind = "config";
code;
_hint;
constructor(msg, code = "INVALID_CONFIG_FILE", hint) {
super(msg);
this.name = "ConfigError";
this.code = code;
this._hint = hint;
// Fixes `instanceof` checks under ES5 transpilation targets.
Object.setPrototypeOf(this, new.target.prototype);
}
hint() {
return this._hint ?? CONFIG_HINTS[this.code];
}
}
/** Raised when the user supplies invalid arguments or input. */
export class UsageError extends AkmError {
kind = "usage";
code;
_hint;
constructor(msg, code = "INVALID_FLAG_VALUE", hint) {
super(msg);
this.name = "UsageError";
this.code = code;
this._hint = hint;
// Fixes `instanceof` checks under ES5 transpilation targets.
Object.setPrototypeOf(this, new.target.prototype);
}
hint() {
return this._hint ?? USAGE_HINTS[this.code];
}
}
/**
* Raised when a condition is ordinary, retryable contention rather than a
* bad command line or a genuine failure — another akm process holds a lock
* or is writing state.db right now. Distinct from `UsageError` (#948
* addendum, dev-team field review 2026-09-09): schedulers classify exit 2 as
* "fix the command line", so contention needs its own exit code (75,
* sysexits EX_TEMPFAIL) a cron wrapper can branch on to retry instead of
* alerting.
*/
export class TransientError extends AkmError {
kind = "transient";
code;
_hint;
constructor(msg, code, hint) {
super(msg);
this.name = "TransientError";
this.code = code;
this._hint = hint;
// Fixes `instanceof` checks under ES5 transpilation targets.
Object.setPrototypeOf(this, new.target.prototype);
}
hint() {
return this._hint ?? TRANSIENT_HINTS[this.code];
}
}
/** Raised when a requested resource (asset, entry, file) is not found. */
export class NotFoundError extends AkmError {
kind = "not-found";
code;
_hint;
constructor(msg, code = "ASSET_NOT_FOUND", hint) {
super(msg);
this.name = "NotFoundError";
this.code = code;
this._hint = hint;
// Fixes `instanceof` checks under ES5 transpilation targets.
Object.setPrototypeOf(this, new.target.prototype);
}
hint() {
return this._hint ?? NOT_FOUND_HINTS[this.code];
}
}
/**
* Test-isolation guard helper.
*
* `src/core/paths.ts` throws `ConfigError("TEST_ISOLATION_MISSING")` under
* `bun test` when `AKM_BUNDLE_DIR` is set without a paired data-dir or
* state-dir override. That throw must never be swallowed by best-effort
* catches around DB/data-dir operations — otherwise the guard's loud failure
* silently degrades into a "no result" outcome (cold cache, missing snapshot,
* etc.) and the underlying test leak goes undetected.
*
* Call `rethrowIfTestIsolationError(err)` from any catch block that returns
* a fallback value (null, [], empty result) after touching DB or data-dir
* paths. It re-throws when the caught error is the guard violation, otherwise
* does nothing so the existing benign-fallback path can proceed unchanged.
*
* Usage:
* try {
* const db = openDatabase();
* // ...
* } catch (err) {
* rethrowIfTestIsolationError(err);
* // existing benign-fallback handling
* }
*/
export function isTestIsolationError(err) {
return err instanceof ConfigError && err.code === "TEST_ISOLATION_MISSING";
}
export function rethrowIfTestIsolationError(err) {
if (isTestIsolationError(err)) {
throw err;
}
}
/**
* Unreadable-data-dir guard helper — the #791 sibling of the test-isolation
* pair above, and it exists for the same reason.
*
* `DATA_DIR_UNREADABLE` says "this path is there and I am not allowed to read
* it". It is raised by `assertIndexPathReadable` and friends precisely so a
* permission fault stops being indistinguishable from "nothing indexed yet".
* That distinction is destroyed again the moment a best-effort `catch` around
* the open collapses it into the same `null`/`[]`/`0` the absent case returns —
* which is how `akm search` came to answer "No search index available. Run
* 'akm index'" at exit 0 for a populated index sitting right there on disk.
*
* Call `rethrowIfDataDirUnreadable(err)` from any catch block that returns a
* fallback value after touching a data-dir path. Absent stays absent; a fault
* the operator has to fix keeps travelling.
*/
export function isDataDirUnreadableError(err) {
return err instanceof ConfigError && err.code === "DATA_DIR_UNREADABLE";
}
export function rethrowIfDataDirUnreadable(err) {
if (isDataDirUnreadableError(err)) {
throw err;
}
}