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
476 lines (475 loc) • 23.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/.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { writeFileAtomic } from "../../core/common.js";
import { ENGINE_NAME_PATTERN_SOURCE } from "../../core/config/engine-semantics.js";
import { ConfigError, UsageError } from "../../core/errors.js";
import { getConfigDir } from "../../core/paths.js";
import { warnOnce } from "../../core/warn.js";
import { cloneExecutionJsonObject } from "../../execution/json.js";
/**
* Installed and operator-owned model intent aliases (#802). `resolveExecution`
* expands a recognized alias once, into an exact model and inference defaults
* at the layer that chose it; harness builders never reinterpret it.
*/
export const MODEL_MAP_VERSION = 1;
let standaloneInstalledModelMapText;
const ENGINE_KEY_PATTERN = new RegExp(ENGINE_NAME_PATTERN_SOURCE);
const ALIAS_KEY_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
const RESERVED_MAP_KEYS = new Set(["__proto__", "constructor", "prototype", "tostring"]);
/**
* A profile's own field names (#946). When one of these appears directly
* under an alias — e.g. `"fast": { "engine": "local-fast" }` — it is not a
* per-platform column named `model`/`inference`/`engine`; it is a wildcard
* default profile applied to every platform column of that alias, exactly
* the flat shorthand the original proposal asked for. A nested per-platform
* entry in the same alias (`"fast": { "claude": { ... } }`) still overrides
* the wildcard for that one column, so both forms compose.
*/
const PROFILE_FIELD_KEYS = new Set(["model", "inference", "engine"]);
/** Internal key the wildcard default profile is stored under (never user-writable directly). */
export const WILDCARD_ENGINE_KEY = "*";
function assertSafeMapKey(key, source, jsonPath) {
if (RESERVED_MAP_KEYS.has(key.toLowerCase())) {
invalid(source, jsonPath, "reserved prototype-like key is not allowed");
}
}
function invalid(source, jsonPath, detail) {
throw new ConfigError(`Invalid ${source} at ${jsonPath}: ${detail}`, "INVALID_CONFIG_FILE", `Fix ${source}, or remove the optional user models.json to use AKM's installed defaults.`);
}
function requireRecord(value, source, jsonPath) {
if (value === null ||
typeof value !== "object" ||
Array.isArray(value) ||
Object.getPrototypeOf(value) !== Object.prototype) {
invalid(source, jsonPath, "expected a JSON object");
}
return value;
}
function assertOnlyKeys(value, allowed, source, jsonPath) {
const allowedSet = new Set(allowed);
for (const key of Object.keys(value)) {
if (!allowedSet.has(key))
invalid(source, `${jsonPath}.${key}`, "unknown field");
}
}
function requireNonemptyString(value, source, jsonPath) {
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
invalid(source, jsonPath, "expected a nonempty string without surrounding whitespace");
}
return value;
}
function freezeRecord(entries) {
const record = Object.create(null);
for (const [key, value] of entries)
record[key] = value;
return Object.freeze(record);
}
function ownValue(record, key) {
return record !== undefined && Object.hasOwn(record, key) ? record[key] : undefined;
}
function parseProfileLayer(value, source, jsonPath) {
if (typeof value === "string")
return requireNonemptyString(value, source, jsonPath);
const record = requireRecord(value, source, jsonPath);
assertOnlyKeys(record, ["model", "inference", "engine"], source, jsonPath);
if (!Object.hasOwn(record, "model") && !Object.hasOwn(record, "inference") && !Object.hasOwn(record, "engine")) {
invalid(source, jsonPath, "structured profile must contain model, inference, and/or engine");
}
if (Object.hasOwn(record, "model") && Object.hasOwn(record, "engine")) {
invalid(source, jsonPath, "model and engine cannot both be set; engine is an indirection for the model value");
}
const out = {};
if (Object.hasOwn(record, "model"))
out.model = requireNonemptyString(record.model, source, `${jsonPath}.model`);
if (Object.hasOwn(record, "engine")) {
const rawEngine = requireNonemptyString(record.engine, source, `${jsonPath}.engine`);
const engine = rawEngine.toLowerCase();
assertSafeMapKey(engine, source, `${jsonPath}.engine`);
if (!ENGINE_KEY_PATTERN.test(engine)) {
invalid(source, `${jsonPath}.engine`, "engine reference must be lowercase kebab-case");
}
out.engine = engine;
}
if (Object.hasOwn(record, "inference")) {
if (record.inference === null)
out.inference = null;
else {
try {
out.inference = cloneExecutionJsonObject(record.inference, `${source} ${jsonPath}.inference`);
}
catch (error) {
invalid(source, `${jsonPath}.inference`, error instanceof Error ? error.message : String(error));
}
}
}
return Object.freeze(out);
}
/** Parse one installed or user layer. Partial structured profiles are valid at this stage. */
export function parseModelMapLayer(text, source) {
let parsed;
try {
parsed = JSON.parse(text);
}
catch {
invalid(source, "$", "invalid JSON syntax");
}
const root = requireRecord(parsed, source, "$");
assertOnlyKeys(root, ["version", "aliases"], source, "$");
if (root.version !== MODEL_MAP_VERSION) {
invalid(source, "$.version", `expected version ${MODEL_MAP_VERSION}`);
}
if (!Object.hasOwn(root, "aliases"))
invalid(source, "$.aliases", "required field is missing");
const rawAliases = requireRecord(root.aliases, source, "$.aliases");
const aliases = [];
const aliasesSeen = new Map();
for (const [rawAlias, rawEngines] of Object.entries(rawAliases)) {
const alias = rawAlias.toLowerCase();
assertSafeMapKey(alias, source, `$.aliases.${rawAlias}`);
if (!ALIAS_KEY_PATTERN.test(alias))
invalid(source, `$.aliases.${rawAlias}`, "alias must be lowercase kebab-case");
const previous = aliasesSeen.get(alias);
if (previous !== undefined) {
invalid(source, `$.aliases.${rawAlias}`, `alias collides case-insensitively with ${previous}`);
}
aliasesSeen.set(alias, rawAlias);
const engineRecord = requireRecord(rawEngines, source, `$.aliases.${rawAlias}`);
if (Object.keys(engineRecord).length === 0) {
invalid(source, `$.aliases.${rawAlias}`, "alias must contain at least one engine mapping");
}
const engines = [];
const enginesSeen = new Map();
const wildcardFields = {};
const platformEntries = [];
for (const [rawKey, rawValue] of Object.entries(engineRecord)) {
if (PROFILE_FIELD_KEYS.has(rawKey))
wildcardFields[rawKey] = rawValue;
else
platformEntries.push([rawKey, rawValue]);
}
if (Object.keys(wildcardFields).length > 0) {
engines.push([WILDCARD_ENGINE_KEY, parseProfileLayer(wildcardFields, source, `$.aliases.${rawAlias}`)]);
}
for (const [rawEngine, rawProfile] of platformEntries) {
const engine = rawEngine.toLowerCase();
assertSafeMapKey(engine, source, `$.aliases.${rawAlias}.${rawEngine}`);
if (!ENGINE_KEY_PATTERN.test(engine)) {
invalid(source, `$.aliases.${rawAlias}.${rawEngine}`, "engine key must be lowercase kebab-case");
}
const previousEngine = enginesSeen.get(engine);
if (previousEngine !== undefined) {
invalid(source, `$.aliases.${rawAlias}.${rawEngine}`, `engine key collides case-insensitively with ${previousEngine}`);
}
enginesSeen.set(engine, rawEngine);
engines.push([engine, parseProfileLayer(rawProfile, source, `$.aliases.${rawAlias}.${rawEngine}`)]);
}
aliases.push([alias, freezeRecord(engines)]);
}
return Object.freeze({ version: MODEL_MAP_VERSION, aliases: freezeRecord(aliases) });
}
function isJsonObject(value) {
return value !== null && value !== undefined && typeof value === "object" && !Array.isArray(value);
}
function mergeJsonValue(base, overlay) {
if (!isJsonObject(base) || !isJsonObject(overlay))
return overlay;
const entries = new Map(Object.entries(base));
for (const [key, value] of Object.entries(overlay))
entries.set(key, mergeJsonValue(entries.get(key), value));
return Object.freeze(Object.fromEntries(entries));
}
function normalizeLayerEntry(entry) {
return typeof entry === "string" ? Object.freeze({ model: entry }) : entry;
}
function mergeProfiles(base, overlay) {
const next = normalizeLayerEntry(overlay);
const out = {};
if (base?.model !== undefined)
out.model = base.model;
if (base?.engine !== undefined)
out.engine = base.engine;
if (base && Object.hasOwn(base, "inference"))
out.inference = base.inference;
// A literal `model` and an `engine` indirection are mutually exclusive within
// one layer (enforced at parse time); across layers, whichever the nearer
// layer sets replaces the other so the merged profile keeps that invariant.
if (next.model !== undefined) {
out.model = next.model;
delete out.engine;
}
if (next.engine !== undefined) {
out.engine = next.engine;
delete out.model;
}
if (Object.hasOwn(next, "inference")) {
out.inference =
next.inference !== null && next.inference !== undefined && isJsonObject(base?.inference)
? mergeJsonValue(base.inference, next.inference)
: next.inference;
}
return Object.freeze(out);
}
/**
* Derive the `{ model, inference }` a model-map profile borrows from a
* configured engine (#946). `resolveExecution` derives an engine's own
* defaults with the same function, so the two cannot diverge. `model` is
* copied verbatim from the engine's own config value; it must already be
* meaningful for the model-map column's platform (akm does not translate
* between an engine's connection and an agent platform's own provider
* registry). Only `kind: "llm"` engines contribute inference defaults — an
* agent-kind engine's schema carries no temperature/thinking fields.
*/
export function engineModelAndInference(engine) {
const out = {};
if (Object.hasOwn(engine, "model") && engine.model !== undefined)
out.model = engine.model;
if (engine.kind === "llm") {
const inference = {};
if (Object.hasOwn(engine, "temperature"))
inference.temperature = engine.temperature;
if (Object.hasOwn(engine, "maxTokens"))
inference.maxTokens = engine.maxTokens;
if (Object.hasOwn(engine, "supportsJsonSchema"))
inference.supportsJsonSchema = engine.supportsJsonSchema;
if (Object.hasOwn(engine, "extraParams"))
inference.extraParams = engine.extraParams;
if (Object.hasOwn(engine, "contextLength"))
inference.contextLength = engine.contextLength;
if (Object.hasOwn(engine, "enableThinking"))
inference.enableThinking = engine.enableThinking;
if (Object.hasOwn(engine, "reasoningEffort"))
inference.reasoningEffort = engine.reasoningEffort;
if (Object.keys(inference).length > 0)
out.inference = inference;
}
return Object.freeze(out);
}
/** Overlay user fields over installed fields, per (alias, column), without resolving `engine` indirection. */
function mergeRawProfileLayers(installed, user) {
const aliases = new Map();
const apply = (layer) => {
for (const [alias, layerEngines] of Object.entries(layer.aliases)) {
const mergedEngines = aliases.get(alias) ?? new Map();
const wildcard = ownValue(layerEngines, WILDCARD_ENGINE_KEY);
if (wildcard !== undefined) {
// This layer's wildcard default (#946) overlays every platform column
// already accumulated from earlier layers, before this layer's own
// explicit per-platform entries (applied below) get a chance to
// override it for their one column.
for (const [engineKey, profile] of mergedEngines) {
if (engineKey === WILDCARD_ENGINE_KEY)
continue;
mergedEngines.set(engineKey, mergeProfiles(profile, wildcard));
}
}
for (const [engineKey, profile] of Object.entries(layerEngines)) {
mergedEngines.set(engineKey, mergeProfiles(mergedEngines.get(engineKey), profile));
}
aliases.set(alias, mergedEngines);
}
};
apply(installed);
if (user)
apply(user);
return aliases;
}
/**
* The overlaid-but-unresolved profile per (alias, column), before any
* `engine` indirection is expanded. `akm models list` (#946) uses this to
* report whether a column resolves through a literal `model` or an `engine`
* reference — information {@link mergeModelMapLayers} collapses away once it
* performs the final resolution.
*/
export function mergedModelMapProfiles(installed, user) {
const aliases = mergeRawProfileLayers(installed, user);
const out = [];
for (const [alias, engineProfiles] of aliases)
out.push([alias, freezeRecord(engineProfiles)]);
return freezeRecord(out);
}
/**
* Overlay user fields over installed fields, resolve any `engine` indirection
* against configured engines, then enforce usable merged profiles.
*/
export function mergeModelMapLayers(installed, user, engines) {
const aliases = mergeRawProfileLayers(installed, user);
const resolvedAliases = [];
for (const [alias, engineProfiles] of aliases) {
const resolvedEngines = [];
for (const [engineKey, profile] of engineProfiles) {
let model = profile.model;
let inference = Object.hasOwn(profile, "inference") ? profile.inference : undefined;
if (model === undefined && profile.engine !== undefined) {
const target = ownValue(engines, profile.engine);
if (target === undefined) {
invalid("merged models.json", `$.aliases.${alias}.${engineKey}.engine`, `references unknown engine ${JSON.stringify(profile.engine)}; a usable model is required after overlay`);
}
const expansion = engineModelAndInference(target);
if (expansion.model === undefined) {
invalid("merged models.json", `$.aliases.${alias}.${engineKey}.engine`, `engine ${JSON.stringify(profile.engine)} has no usable model; a usable model is required after overlay`);
}
model = expansion.model;
inference =
inference === undefined
? expansion.inference
: inference !== null && isJsonObject(expansion.inference)
? mergeJsonValue(expansion.inference, inference)
: inference;
}
if (model === undefined) {
invalid("merged models.json", `$.aliases.${alias}.${engineKey}.model`, "a usable model is required after overlay");
}
resolvedEngines.push([engineKey, Object.freeze(inference !== undefined ? { model, inference } : { model })]);
}
resolvedAliases.push([alias, freezeRecord(resolvedEngines)]);
}
return Object.freeze({ version: MODEL_MAP_VERSION, aliases: freezeRecord(resolvedAliases) });
}
function selectionFromProfile(input, profile) {
if (!profile.model)
throw new ConfigError(`Model alias ${JSON.stringify(input)} does not resolve to a usable model.`);
return Object.freeze({
input,
interpretation: "alias",
model: profile.model,
...(Object.hasOwn(profile, "inference") ? { inference: profile.inference } : {}),
});
}
/** Resolve one alias through the merged installed/user registry. */
export function resolveModelMapAlias(input, engine, map) {
const alias = input.toLowerCase();
const selectedEngine = engine.toLowerCase();
const tier = ownValue(map.aliases, alias);
// A specific platform column always wins; the alias-level wildcard default
// (#946) is only a fallback for a column no layer ever set explicitly.
const profile = ownValue(tier, selectedEngine) ?? ownValue(tier, WILDCARD_ENGINE_KEY);
if (profile !== undefined)
return selectionFromProfile(input, profile);
const knownAliasUnmappedForEngine = tier !== undefined;
if (knownAliasUnmappedForEngine) {
warnOnce(`model-map-alias-no-engine-mapping:${alias}:${selectedEngine}`, `[akm] Model alias ${JSON.stringify(input)} has no mapping for engine ${JSON.stringify(engine)}; using ${JSON.stringify(input)} as the literal model name. Add $.aliases.${alias}.${engine} to models.json to map it.`);
}
return Object.freeze({
input,
interpretation: "exact",
model: input,
...(knownAliasUnmappedForEngine ? { unmappedForEngine: true } : {}),
});
}
export function userModelMapPath(env = process.env) {
return path.join(getConfigDir(env), "models.json");
}
function modelMapFileError(label, filePath, action) {
return new ConfigError(`${label} could not be ${action}: ${filePath}.`, "INVALID_CONFIG_FILE", `Ensure ${filePath} is a readable regular file owned by the expected user.`);
}
/**
* Read one models.json. `optional` treats absence (including a dangling
* symlink) as "no file"; anything that is not a regular file is refused
* rather than read, so a FIFO cannot hang the CLI.
*/
function readModelMapFile(filePath, label, optional) {
let stat;
try {
stat = fs.statSync(filePath);
}
catch (error) {
if (optional && error?.code === "ENOENT")
return undefined;
throw modelMapFileError(label, filePath, optional ? "inspected" : "found");
}
if (!stat.isFile()) {
throw new ConfigError(`Unable to read ${label.toLowerCase()} because it is not a readable regular file: ${filePath}.`, "INVALID_CONFIG_FILE", "Replace it with a readable regular models.json file, or a symlink to one.");
}
try {
return fs.readFileSync(filePath, "utf8");
}
catch {
throw modelMapFileError(label, filePath, "read");
}
}
/** Resolve the one authoritative external-asset location for this runtime. */
export function installedModelMapPaths(moduleUrl = import.meta.url) {
const modulePath = fileURLToPath(moduleUrl);
const moduleDirectory = path.dirname(modulePath);
const unbundledModule = path.basename(moduleDirectory) === "agent" && path.basename(path.dirname(moduleDirectory)) === "integrations";
const assetPath = path.resolve(moduleDirectory, unbundledModule ? "../../assets/models.json" : "assets/models.json");
return Object.freeze([assetPath]);
}
/**
* Standalone bootstrap only. `scripts/akm-standalone.ts` derives these bytes
* from the authoritative JSON import that Bun embeds into the executable.
*/
export function registerStandaloneModelMapFallback(text) {
standaloneInstalledModelMapText = text;
}
/** Load installed bytes lazily so health can report a missing/bad package asset. */
export function readInstalledModelMapText(options = {}) {
if (options.installedText !== undefined)
return options.installedText;
// Registration occurs only in the compiled standalone bootstrap. Once set,
// the embedded authority must win over any mutable file beside the binary.
if (standaloneInstalledModelMapText !== undefined)
return standaloneInstalledModelMapText;
const candidates = installedModelMapPaths();
for (const candidate of candidates) {
const external = readModelMapFile(candidate, "Installed models.json", true);
if (external !== undefined)
return external;
}
throw new ConfigError(`Installed models.json is missing from this AKM installation (checked: ${candidates.join(", ")}).`, "INVALID_CONFIG_FILE", "Reinstall AKM so its dist/assets/models.json package asset is restored.");
}
/** Read and parse the installed authority plus the optional operator overlay, without resolving them. */
export function loadModelMapLayers(options = {}) {
const installed = parseModelMapLayer(readInstalledModelMapText(options), "installed models.json");
const userPath = userModelMapPath(options.env);
const userText = readModelMapFile(userPath, "User models.json", true);
const user = userText !== undefined ? parseModelMapLayer(userText, `user models.json (${userPath})`) : undefined;
return Object.freeze({ installed, ...(user ? { user } : {}), userPath, userStatus: user ? "loaded" : "absent" });
}
/** Load the installed authority plus the optional operator overlay. */
export function loadModelMap(options = {}) {
const { installed, user, userPath, userStatus } = loadModelMapLayers(options);
return Object.freeze({ map: mergeModelMapLayers(installed, user, options.engines), userPath, userStatus });
}
function targetExistsError(target, detail) {
return new UsageError(`${detail}: ${target}`, "RESOURCE_ALREADY_EXISTS", "Move the existing target aside, then retry. Use --overwrite only for a regular file you intend to replace.");
}
function copyIoError(target, action) {
return new ConfigError(`Unable to ${action} user models.json at ${target}.`, "INVALID_CONFIG_FILE", "Check the configuration directory ownership and permissions, then retry.");
}
/** `akm models copy-defaults`: copy the validated installed map into the config directory. */
export function copyDefaultModelMap(options = {}) {
const text = readInstalledModelMapText(options);
mergeModelMapLayers(parseModelMapLayer(text, "installed models.json"));
const target = userModelMapPath(options.env);
let existing;
try {
existing = fs.lstatSync(target);
}
catch (error) {
if (error?.code !== "ENOENT")
throw copyIoError(target, "inspect");
}
if (existing && !existing.isFile()) {
throw targetExistsError(target, "Refusing to replace a non-regular models.json target");
}
if (existing && options.overwrite !== true)
throw targetExistsError(target, "User models.json already exists");
try {
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
}
catch {
throw copyIoError(target, "create the configuration directory for");
}
try {
writeFileAtomic(target, text, 0o600);
}
catch {
throw copyIoError(target, "write");
}
return { path: target, copied: true, overwritten: existing !== undefined };
}