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
331 lines (330 loc) • 15.1 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 os from "node:os";
import path from "node:path";
import { resolveSecret } from "../../core/config/config.js";
import { deepMergeConfig } from "../../core/config/deep-merge.js";
import { SECRET_STORE_REFERENCE_PATTERN } from "../../core/config/schema/primitives.js";
import { ConfigError } from "../../core/errors.js";
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
import { collectSensitiveValues } from "../../core/redaction.js";
import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js";
import { getHarness } from "../harnesses/index.js";
import { DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
import { getBuiltinAgentProfile, OPENCODE_SDK_SERVER_BIN } from "./profiles.js";
const LLM_CONNECTION_FIELDS = [
"provider",
"endpoint",
"model",
"temperature",
"maxTokens",
"supportsJsonSchema",
"extraParams",
"contextLength",
"enableThinking",
"reasoningEffort",
];
function envName(reference) {
const match = /^\$(?:\{)?([A-Za-z_][A-Za-z0-9_]*)(?:\})?$/.exec(reference);
return match?.[1];
}
function expandHomePath(filePath) {
return filePath.startsWith("~") ? path.join(os.homedir(), filePath.slice(1)) : filePath;
}
/** Trim exactly one trailing newline (`\n` or `\r\n`) — never interior whitespace. */
function trimTrailingNewline(raw) {
return raw.replace(/\r?\n$/, "");
}
/** Read a file-backed credential; errors name the engine and path, never the contents. */
function readApiKeyFile(engineName, filePath) {
let raw;
try {
raw = fs.readFileSync(filePath, "utf8");
}
catch (err) {
const code = err instanceof Error ? err.code : undefined;
const reason = code === "ENOENT" ? "does not exist" : "could not be read";
throw new ConfigError(`Engine "${engineName}" apiKeyFile ${reason}: ${filePath}`, "INVALID_CONFIG_FILE");
}
const value = trimTrailingNewline(raw);
if (value.length === 0) {
throw new ConfigError(`Engine "${engineName}" apiKeyFile is empty: ${filePath}`, "INVALID_CONFIG_FILE");
}
return value;
}
/** Best-effort, non-throwing read of a file-backed credential for redaction and health. */
export function lookupApiKeyFileValue(filePath) {
try {
const value = trimTrailingNewline(fs.readFileSync(filePath, "utf8"));
return value.length > 0 ? value : undefined;
}
catch {
return undefined;
}
}
/** Best-effort, non-throwing read of a secret-store credential for redaction and health. */
export function lookupApiKeySecretRefValue(ref) {
try {
return resolveSecret(ref, resolveSecretFromStore);
}
catch {
return undefined;
}
}
function selectedEngineName(config, layers, llmOnly) {
for (let index = layers.length - 1; index >= 0; index--) {
const engine = layers[index]?.engine;
if (engine !== undefined)
return engine;
}
return llmOnly ? config.defaults?.llmEngine : config.defaults?.engine;
}
/** The configured engine of this name; an unconfigured name is an error. */
export function configuredEngine(name, config) {
const engine = config.engines && Object.hasOwn(config.engines, name) ? config.engines[name] : undefined;
if (!engine)
throw new ConfigError(`Engine "${name}" is not configured.`, "INVALID_CONFIG_FILE");
return engine;
}
function resolveCredential(name, engine, config) {
if (engine.apiKey !== undefined) {
const explicit = envName(engine.apiKey);
if (explicit)
return { names: [explicit], required: true };
// A secret-store reference is carried as `apiKeySecretRef` instead.
if (SECRET_STORE_REFERENCE_PATTERN.test(engine.apiKey))
return undefined;
throw new ConfigError(`Engine "${name}" has an invalid symbolic apiKey reference.`, "INVALID_CONFIG_FILE");
}
// An explicit apiKeyFile is its own credential source; it does not also
// fall through to the implicit AKM_ENGINE_<NAME>_API_KEY convention.
if (engine.apiKeyFile !== undefined)
return undefined;
const specific = `AKM_ENGINE_${name.toUpperCase().replaceAll("-", "_")}_API_KEY`;
return config.defaults?.llmEngine === name
? { names: [specific, "AKM_LLM_API_KEY"], required: false }
: { names: [specific], required: false };
}
/** Lookup-only credential projection used by redaction inventories. */
export function lookupCredentialFromEnv(credential, envSource = process.env) {
for (const name of credential?.names ?? []) {
const candidate = envSource[name]?.trim();
if (candidate)
return candidate;
}
return undefined;
}
/** The enforcing env-credential lookup; a missing required descriptor names its primary variable. */
export function resolveCredentialFromEnv(credential, envSource = process.env) {
const value = lookupCredentialFromEnv(credential, envSource);
if (value)
return value;
if (credential?.required) {
throw new ConfigError(`Required engine credential ${credential.names[0]} is not set.`, "INVALID_CONFIG_FILE");
}
return undefined;
}
/**
* Resolve one LLM credential value at the dispatch boundary: the env
* descriptor first, then the file-backed value, then the secret-store
* reference. Nothing before dispatch reads any of them.
*/
export function resolveLlmCredentialValue(engine, credential, apiKeyFile, apiKeySecretRef, envSource = process.env) {
const envValue = resolveCredentialFromEnv(credential, envSource);
if (envValue !== undefined)
return envValue;
if (apiKeyFile !== undefined)
return readApiKeyFile(engine, apiKeyFile);
return apiKeySecretRef !== undefined ? resolveSecret(apiKeySecretRef, resolveSecretFromStore) : undefined;
}
/**
* Non-throwing credential-presence check for `akm health` and improve's
* strategy probe. Names WHICH env var / file / secret reference is missing,
* never its value; a broken source is reported by the real dispatch.
*/
export function describeLlmCredentialAvailability(resolved, env = process.env) {
if (resolved.credential?.required) {
if (resolved.credential.names.some((name) => Boolean(env[name]?.trim())))
return { available: true };
const reference = `$${resolved.credential.names[0]}`;
return { available: false, reference, reason: `${reference} is not set in this environment` };
}
if (resolved.apiKeyFile !== undefined) {
if (lookupApiKeyFileValue(resolved.apiKeyFile) !== undefined)
return { available: true };
return {
available: false,
reference: resolved.apiKeyFile,
reason: `apiKeyFile ${resolved.apiKeyFile} is missing or empty`,
};
}
if (resolved.apiKeySecretRef !== undefined) {
if (lookupApiKeySecretRefValue(resolved.apiKeySecretRef) !== undefined)
return { available: true };
return {
available: false,
reference: resolved.apiKeySecretRef,
reason: `${resolved.apiKeySecretRef} did not resolve from the secret store`,
};
}
return { available: true };
}
export function isLlmCredentialAvailable(resolved, env = process.env) {
return describeLlmCredentialAvailability(resolved, env).available;
}
/** Collect every configured engine's current credential value for output and persistence redaction. */
export function collectEngineCredentialValues(config, envSource = process.env) {
const values = new Set();
for (const [name, engine] of Object.entries(config.engines ?? {})) {
if (engine.kind !== "llm")
continue;
for (const envVar of resolveCredential(name, engine, config)?.names ?? []) {
const value = envSource[envVar]?.trim();
if (value)
values.add(value);
}
// File and secret-store lookups are best-effort so one broken engine never
// stops redaction from collecting every other engine's value.
if (engine.apiKeyFile !== undefined) {
const value = lookupApiKeyFileValue(expandHomePath(engine.apiKeyFile));
if (value)
values.add(value);
}
if (engine.apiKey !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(engine.apiKey)) {
const value = lookupApiKeySecretRefValue(engine.apiKey);
if (value)
values.add(value);
}
}
return collectSensitiveValues(values);
}
function effectiveTimeout(engine, layers, fallback) {
for (let index = layers.length - 1; index >= 0; index--) {
const layer = layers[index];
if (layer && Object.hasOwn(layer, "timeoutMs"))
return layer.timeoutMs ?? null;
}
return Object.hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : fallback;
}
function rawLlmConnection(engine) {
const connection = {};
for (const key of LLM_CONNECTION_FIELDS) {
if (engine[key] !== undefined)
connection[key] = engine[key];
}
return connection;
}
/** Resolve one selected LLM engine and overlays without materializing credentials. */
export function resolveLlmEngineUse(config, layers) {
const name = selectedEngineName(config, layers, true);
if (!name) {
throw new ConfigError("No LLM engine is selected. Set defaults.llmEngine or specify engine.", "LLM_NOT_CONFIGURED");
}
const engine = configuredEngine(name, config);
if (engine.kind !== "llm")
throw new ConfigError(`Engine "${name}" is not an LLM engine.`, "INVALID_CONFIG_FILE");
let connection = rawLlmConnection(engine);
for (const layer of layers) {
if (layer.llm)
connection = deepMergeConfig(connection, layer.llm);
if (layer.model !== undefined)
connection.model = layer.model;
}
for (const key of Object.keys(connection)) {
if (connection[key] === undefined)
delete connection[key];
}
const apiKeySecretRef = engine.apiKey !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(engine.apiKey) ? engine.apiKey : undefined;
return {
engine: name,
connection: connection,
credential: resolveCredential(name, engine, config),
...(engine.apiKeyFile !== undefined ? { apiKeyFile: expandHomePath(engine.apiKeyFile) } : {}),
...(apiKeySecretRef !== undefined ? { apiKeySecretRef } : {}),
timeoutMs: effectiveTimeout(engine, layers, DEFAULT_LLM_TIMEOUT_MS),
};
}
/** Inject an already-resolved credential value into a connection; reads nothing itself. */
export function materializeLlmConnectionWithCredential(resolved, credentialValue) {
const extraParams = resolved.connection.extraParams;
if (extraParams !== undefined) {
const issue = validateExtraParams(extraParams)[0];
if (issue) {
throw new ConfigError(formatExtraParamsIssue(`Engine "${resolved.engine}" extraParams`, issue), "INVALID_CONFIG_FILE");
}
}
return {
...resolved.connection,
...(credentialValue ? { apiKey: credentialValue } : {}),
timeoutMs: resolved.timeoutMs,
};
}
/** Read and inject one resolved credential at the runtime boundary. */
export function materializeLlmConnection(resolved, envSource = process.env) {
return materializeLlmConnectionWithCredential(resolved, resolveLlmCredentialValue(resolved.engine, resolved.credential, resolved.apiKeyFile, resolved.apiKeySecretRef, envSource));
}
function lowerAgentEngine(name, engine, config) {
const harness = getHarness(engine.platform);
if (!harness?.capabilities.agentDispatch) {
throw new ConfigError(`Engine "${name}" names a platform that cannot dispatch agents: ${engine.platform}.`, "INVALID_CONFIG_FILE");
}
const platform = harness.id;
const sdk = platform === "opencode-sdk";
const builtin = getBuiltinAgentProfile(platform);
const profile = {
name,
platform,
personaChannel: sdk ? "native" : (harness.agentBuilder?.personaChannel ?? "prompt"),
bin: engine.bin ?? builtin?.bin ?? (sdk ? OPENCODE_SDK_SERVER_BIN : platform),
args: engine.args ?? builtin?.args ?? [],
stdio: "captured",
...(builtin?.env ? { env: builtin.env } : {}),
envPassthrough: builtin?.envPassthrough ?? [],
parseOutput: "text",
...(engine.workspace ? { workspace: path.resolve(engine.workspace) } : {}),
...(engine.model ? { model: engine.model } : {}),
...(engine.agent ? { agent: engine.agent } : {}),
};
// An engine that sets no timeoutMs leaves it unset, so a caller's own default
// (model work's 600 s) can apply; a dispatch with none runs unbounded.
const ownTimeout = Object.hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : undefined;
if (!sdk) {
return { kind: "agent", engine: name, profile, ...(ownTimeout !== undefined ? { timeoutMs: ownTimeout } : {}) };
}
// The fallback connection is the engine's own `llmEngine` and nothing else. `defaults.llmEngine`
// is the default engine for model work, not a connection every SDK engine borrows: with no
// `llmEngine`, opencode resolves provider, model and auth from its own configuration.
const fallback = engine.llmEngine ? resolveLlmEngineUse(config, [{ engine: engine.llmEngine }]) : undefined;
return {
kind: "sdk",
engine: name,
profile,
...(fallback
? {
fallbackConnection: fallback.connection,
...(fallback.credential ? { fallbackCredential: fallback.credential } : {}),
...(fallback.apiKeyFile ? { fallbackApiKeyFile: fallback.apiKeyFile } : {}),
...(fallback.apiKeySecretRef ? { fallbackApiKeySecretRef: fallback.apiKeySecretRef } : {}),
fallbackTimeoutMs: fallback.timeoutMs,
}
: {}),
...(ownTimeout !== undefined ? { timeoutMs: ownTimeout } : fallback ? { timeoutMs: fallback.timeoutMs } : {}),
};
}
/** Resolve a configured engine name to its runner: an LLM connection, a spawned agent, or the SDK. */
export function resolveEngine(name, config, engine = configuredEngine(name, config)) {
if (engine.kind !== "llm")
return lowerAgentEngine(name, engine, config);
const resolved = resolveLlmEngineUse(config, [{ engine: name }]);
return {
kind: "llm",
engine: name,
connection: resolved.connection,
...(resolved.credential ? { credential: resolved.credential } : {}),
...(resolved.apiKeyFile ? { apiKeyFile: resolved.apiKeyFile } : {}),
...(resolved.apiKeySecretRef ? { apiKeySecretRef: resolved.apiKeySecretRef } : {}),
timeoutMs: resolved.timeoutMs,
};
}