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

240 lines (239 loc) • 14.4 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/. /** * 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.", DATA_DIR_NOT_WRITABLE: "Nothing was recorded. Make the akm data directory writable for this process (for an agent sandbox, add it to the writable paths), or point AKM_DATA_DIR / XDG_DATA_HOME somewhere this user can write, then retry.", 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; } }