@gguf/claw
Version:
WhatsApp gateway CLI (Baileys web) with Pi RPC agent
1,347 lines (1,334 loc) • 200 kB
JavaScript
import { a as resolveOAuthDir, n as resolveConfigPath, r as resolveDefaultConfigCandidates, s as resolveStateDir } from "./paths-B1kfl4h5.js";
import { A as normalizeAccountId, T as DEFAULT_AGENT_ID, c as resolveDefaultAgentId, j as normalizeAgentId, s as resolveAgentWorkspaceDir, w as DEFAULT_ACCOUNT_ID } from "./agent-scope-Csu2B6AM.js";
import { G as success, N as resolveUserPath, V as info, X as getChildLogger, j as resolveConfigDir, k as jidToE164, m as CHANNEL_IDS, u as defaultRuntime, y as normalizeChatChannelId } from "./exec-BMnoMcZW.js";
import { D as shouldEnableShellEnvFallback, E as shouldDeferShellEnvFallback, H as DEFAULT_CONTEXT_TOKENS, T as resolveShellEnvFallbackTimeoutMs, s as parseModelRef, w as loadShellEnvFallback } from "./model-selection-mzTqrNoj.js";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";
import JSON5 from "json5";
import fs$1 from "node:fs/promises";
import { fileURLToPath } from "node:url";
import crypto from "node:crypto";
import AjvPkg from "ajv";
import { z } from "zod";
//#region src/web/auth-store.ts
function resolveDefaultWebAuthDir() {
return path.join(resolveOAuthDir(), "whatsapp", DEFAULT_ACCOUNT_ID);
}
const WA_WEB_AUTH_DIR = resolveDefaultWebAuthDir();
function resolveWebCredsPath(authDir) {
return path.join(authDir, "creds.json");
}
function resolveWebCredsBackupPath(authDir) {
return path.join(authDir, "creds.json.bak");
}
function readCredsJsonRaw(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
const stats = fs.statSync(filePath);
if (!stats.isFile() || stats.size <= 1) return null;
return fs.readFileSync(filePath, "utf-8");
} catch {
return null;
}
}
function maybeRestoreCredsFromBackup(authDir) {
const logger = getChildLogger({ module: "web-session" });
try {
const credsPath = resolveWebCredsPath(authDir);
const backupPath = resolveWebCredsBackupPath(authDir);
const raw = readCredsJsonRaw(credsPath);
if (raw) {
JSON.parse(raw);
return;
}
const backupRaw = readCredsJsonRaw(backupPath);
if (!backupRaw) return;
JSON.parse(backupRaw);
fs.copyFileSync(backupPath, credsPath);
logger.warn({ credsPath }, "restored corrupted WhatsApp creds.json from backup");
} catch {}
}
async function webAuthExists(authDir = resolveDefaultWebAuthDir()) {
const resolvedAuthDir = resolveUserPath(authDir);
maybeRestoreCredsFromBackup(resolvedAuthDir);
const credsPath = resolveWebCredsPath(resolvedAuthDir);
try {
await fs$1.access(resolvedAuthDir);
} catch {
return false;
}
try {
const stats = await fs$1.stat(credsPath);
if (!stats.isFile() || stats.size <= 1) return false;
const raw = await fs$1.readFile(credsPath, "utf-8");
JSON.parse(raw);
return true;
} catch {
return false;
}
}
async function clearLegacyBaileysAuthState(authDir) {
const entries = await fs$1.readdir(authDir, { withFileTypes: true });
const shouldDelete = (name) => {
if (name === "oauth.json") return false;
if (name === "creds.json" || name === "creds.json.bak") return true;
if (!name.endsWith(".json")) return false;
return /^(app-state-sync|session|sender-key|pre-key)-/.test(name);
};
await Promise.all(entries.map(async (entry) => {
if (!entry.isFile()) return;
if (!shouldDelete(entry.name)) return;
await fs$1.rm(path.join(authDir, entry.name), { force: true });
}));
}
async function logoutWeb(params) {
const runtime = params.runtime ?? defaultRuntime;
const resolvedAuthDir = resolveUserPath(params.authDir ?? resolveDefaultWebAuthDir());
if (!await webAuthExists(resolvedAuthDir)) {
runtime.log(info("No WhatsApp Web session found; nothing to delete."));
return false;
}
if (params.isLegacyAuthDir) await clearLegacyBaileysAuthState(resolvedAuthDir);
else await fs$1.rm(resolvedAuthDir, {
recursive: true,
force: true
});
runtime.log(success("Cleared WhatsApp Web credentials."));
return true;
}
function readWebSelfId(authDir = resolveDefaultWebAuthDir()) {
try {
const credsPath = resolveWebCredsPath(resolveUserPath(authDir));
if (!fs.existsSync(credsPath)) return {
e164: null,
jid: null
};
const raw = fs.readFileSync(credsPath, "utf-8");
const jid = JSON.parse(raw)?.me?.id ?? null;
return {
e164: jid ? jidToE164(jid, { authDir }) : null,
jid
};
} catch {
return {
e164: null,
jid: null
};
}
}
/**
* Return the age (in milliseconds) of the cached WhatsApp web auth state, or null when missing.
* Helpful for heartbeats/observability to spot stale credentials.
*/
function getWebAuthAgeMs(authDir = resolveDefaultWebAuthDir()) {
try {
const stats = fs.statSync(resolveWebCredsPath(resolveUserPath(authDir)));
return Date.now() - stats.mtimeMs;
} catch {
return null;
}
}
function logWebSelfId(authDir = resolveDefaultWebAuthDir(), runtime = defaultRuntime, includeChannelPrefix = false) {
const { e164, jid } = readWebSelfId(authDir);
const details = e164 || jid ? `${e164 ?? "unknown"}${jid ? ` (jid ${jid})` : ""}` : "unknown";
const prefix = includeChannelPrefix ? "Web Channel: " : "";
runtime.log(info(`${prefix}${details}`));
}
//#endregion
//#region src/web/accounts.ts
function listConfiguredAccountIds(cfg) {
const accounts = cfg.channels?.whatsapp?.accounts;
if (!accounts || typeof accounts !== "object") return [];
return Object.keys(accounts).filter(Boolean);
}
function listWhatsAppAccountIds(cfg) {
const ids = listConfiguredAccountIds(cfg);
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
return ids.toSorted((a, b) => a.localeCompare(b));
}
function resolveDefaultWhatsAppAccountId(cfg) {
const ids = listWhatsAppAccountIds(cfg);
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
return ids[0] ?? DEFAULT_ACCOUNT_ID;
}
function resolveAccountConfig(cfg, accountId) {
const accounts = cfg.channels?.whatsapp?.accounts;
if (!accounts || typeof accounts !== "object") return;
return accounts[accountId];
}
function resolveDefaultAuthDir(accountId) {
return path.join(resolveOAuthDir(), "whatsapp", normalizeAccountId(accountId));
}
function resolveLegacyAuthDir() {
return resolveOAuthDir();
}
function legacyAuthExists(authDir) {
try {
return fs.existsSync(path.join(authDir, "creds.json"));
} catch {
return false;
}
}
function resolveWhatsAppAuthDir(params) {
const accountId = params.accountId.trim() || DEFAULT_ACCOUNT_ID;
const configured = resolveAccountConfig(params.cfg, accountId)?.authDir?.trim();
if (configured) return {
authDir: resolveUserPath(configured),
isLegacy: false
};
const defaultDir = resolveDefaultAuthDir(accountId);
if (accountId === DEFAULT_ACCOUNT_ID) {
const legacyDir = resolveLegacyAuthDir();
if (legacyAuthExists(legacyDir) && !legacyAuthExists(defaultDir)) return {
authDir: legacyDir,
isLegacy: true
};
}
return {
authDir: defaultDir,
isLegacy: false
};
}
function resolveWhatsAppAccount(params) {
const rootCfg = params.cfg.channels?.whatsapp;
const accountId = params.accountId?.trim() || resolveDefaultWhatsAppAccountId(params.cfg);
const accountCfg = resolveAccountConfig(params.cfg, accountId);
const enabled = accountCfg?.enabled !== false;
const { authDir, isLegacy } = resolveWhatsAppAuthDir({
cfg: params.cfg,
accountId
});
return {
accountId,
name: accountCfg?.name?.trim() || void 0,
enabled,
sendReadReceipts: accountCfg?.sendReadReceipts ?? rootCfg?.sendReadReceipts ?? true,
messagePrefix: accountCfg?.messagePrefix ?? rootCfg?.messagePrefix ?? params.cfg.messages?.messagePrefix,
authDir,
isLegacyAuthDir: isLegacy,
selfChatMode: accountCfg?.selfChatMode ?? rootCfg?.selfChatMode,
dmPolicy: accountCfg?.dmPolicy ?? rootCfg?.dmPolicy,
allowFrom: accountCfg?.allowFrom ?? rootCfg?.allowFrom,
groupAllowFrom: accountCfg?.groupAllowFrom ?? rootCfg?.groupAllowFrom,
groupPolicy: accountCfg?.groupPolicy ?? rootCfg?.groupPolicy,
textChunkLimit: accountCfg?.textChunkLimit ?? rootCfg?.textChunkLimit,
chunkMode: accountCfg?.chunkMode ?? rootCfg?.chunkMode,
mediaMaxMb: accountCfg?.mediaMaxMb ?? rootCfg?.mediaMaxMb,
blockStreaming: accountCfg?.blockStreaming ?? rootCfg?.blockStreaming,
ackReaction: accountCfg?.ackReaction ?? rootCfg?.ackReaction,
groups: accountCfg?.groups ?? rootCfg?.groups,
debounceMs: accountCfg?.debounceMs ?? rootCfg?.debounceMs
};
}
//#endregion
//#region src/version.ts
const CORE_PACKAGE_NAME = "openclaw";
const PACKAGE_JSON_CANDIDATES = [
"../package.json",
"../../package.json",
"../../../package.json",
"./package.json"
];
const BUILD_INFO_CANDIDATES = [
"../build-info.json",
"../../build-info.json",
"./build-info.json"
];
function readVersionFromJsonCandidates(moduleUrl, candidates, opts = {}) {
try {
const require = createRequire(moduleUrl);
for (const candidate of candidates) try {
const parsed = require(candidate);
const version = parsed.version?.trim();
if (!version) continue;
if (opts.requirePackageName && parsed.name !== CORE_PACKAGE_NAME) continue;
return version;
} catch {}
return null;
} catch {
return null;
}
}
function readVersionFromPackageJsonForModuleUrl(moduleUrl) {
return readVersionFromJsonCandidates(moduleUrl, PACKAGE_JSON_CANDIDATES, { requirePackageName: true });
}
function readVersionFromBuildInfoForModuleUrl(moduleUrl) {
return readVersionFromJsonCandidates(moduleUrl, BUILD_INFO_CANDIDATES);
}
function resolveVersionFromModuleUrl(moduleUrl) {
return readVersionFromPackageJsonForModuleUrl(moduleUrl) || readVersionFromBuildInfoForModuleUrl(moduleUrl);
}
const VERSION = typeof __OPENCLAW_VERSION__ === "string" && __OPENCLAW_VERSION__ || process.env.OPENCLAW_BUNDLED_VERSION || resolveVersionFromModuleUrl(import.meta.url) || "0.0.0";
//#endregion
//#region src/config/agent-dirs.ts
var DuplicateAgentDirError = class extends Error {
constructor(duplicates) {
super(formatDuplicateAgentDirError(duplicates));
this.name = "DuplicateAgentDirError";
this.duplicates = duplicates;
}
};
function canonicalizeAgentDir(agentDir) {
const resolved = path.resolve(agentDir);
if (process.platform === "darwin" || process.platform === "win32") return resolved.toLowerCase();
return resolved;
}
function collectReferencedAgentIds(cfg) {
const ids = /* @__PURE__ */ new Set();
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : [];
const defaultAgentId = agents.find((agent) => agent?.default)?.id ?? agents[0]?.id ?? DEFAULT_AGENT_ID;
ids.add(normalizeAgentId(defaultAgentId));
for (const entry of agents) if (entry?.id) ids.add(normalizeAgentId(entry.id));
const bindings = cfg.bindings;
if (Array.isArray(bindings)) for (const binding of bindings) {
const id = binding?.agentId;
if (typeof id === "string" && id.trim()) ids.add(normalizeAgentId(id));
}
return [...ids];
}
function resolveEffectiveAgentDir(cfg, agentId, deps) {
const id = normalizeAgentId(agentId);
const trimmed = (Array.isArray(cfg.agents?.list) ? cfg.agents?.list.find((agent) => normalizeAgentId(agent.id) === id)?.agentDir : void 0)?.trim();
if (trimmed) return resolveUserPath(trimmed);
const root = resolveStateDir(deps?.env ?? process.env, deps?.homedir ?? os.homedir);
return path.join(root, "agents", id, "agent");
}
function findDuplicateAgentDirs(cfg, deps) {
const byDir = /* @__PURE__ */ new Map();
for (const agentId of collectReferencedAgentIds(cfg)) {
const agentDir = resolveEffectiveAgentDir(cfg, agentId, deps);
const key = canonicalizeAgentDir(agentDir);
const entry = byDir.get(key);
if (entry) entry.agentIds.push(agentId);
else byDir.set(key, {
agentDir,
agentIds: [agentId]
});
}
return [...byDir.values()].filter((v) => v.agentIds.length > 1);
}
function formatDuplicateAgentDirError(dups) {
return [
"Duplicate agentDir detected (multi-agent config).",
"Each agent must have a unique agentDir; sharing it causes auth/session state collisions and token invalidation.",
"",
"Conflicts:",
...dups.map((d) => `- ${d.agentDir}: ${d.agentIds.map((id) => `"${id}"`).join(", ")}`),
"",
"Fix: remove the shared agents.list[].agentDir override (or give each agent its own directory).",
"If you want to share credentials, copy auth-profiles.json instead of sharing the entire agentDir."
].join("\n");
}
//#endregion
//#region src/config/agent-limits.ts
const DEFAULT_AGENT_MAX_CONCURRENT = 4;
const DEFAULT_SUBAGENT_MAX_CONCURRENT = 8;
function resolveAgentMaxConcurrent(cfg) {
const raw = cfg?.agents?.defaults?.maxConcurrent;
if (typeof raw === "number" && Number.isFinite(raw)) return Math.max(1, Math.floor(raw));
return DEFAULT_AGENT_MAX_CONCURRENT;
}
//#endregion
//#region src/config/talk.ts
function readTalkApiKeyFromProfile(deps = {}) {
const fsImpl = deps.fs ?? fs;
const osImpl = deps.os ?? os;
const pathImpl = deps.path ?? path;
const home = osImpl.homedir();
const candidates = [
".profile",
".zprofile",
".zshrc",
".bashrc"
].map((name) => pathImpl.join(home, name));
for (const candidate of candidates) {
if (!fsImpl.existsSync(candidate)) continue;
try {
const value = fsImpl.readFileSync(candidate, "utf-8").match(/(?:^|\n)\s*(?:export\s+)?ELEVENLABS_API_KEY\s*=\s*["']?([^\n"']+)["']?/)?.[1]?.trim();
if (value) return value;
} catch {}
}
return null;
}
function resolveTalkApiKey(env = process.env, deps = {}) {
const envValue = (env.ELEVENLABS_API_KEY ?? "").trim();
if (envValue) return envValue;
return readTalkApiKeyFromProfile(deps);
}
//#endregion
//#region src/config/defaults.ts
let defaultWarnState = { warned: false };
const DEFAULT_MODEL_ALIASES = {
opus: "anthropic/claude-opus-4-6",
sonnet: "anthropic/claude-sonnet-4-5",
gpt: "openai/gpt-5.2",
"gpt-mini": "openai/gpt-5-mini",
gemini: "google/gemini-3-pro-preview",
"gemini-flash": "google/gemini-3-flash-preview"
};
const DEFAULT_MODEL_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0
};
const DEFAULT_MODEL_INPUT = ["text"];
const DEFAULT_MODEL_MAX_TOKENS = 8192;
function isPositiveNumber(value) {
return typeof value === "number" && Number.isFinite(value) && value > 0;
}
function resolveModelCost(raw) {
return {
input: typeof raw?.input === "number" ? raw.input : DEFAULT_MODEL_COST.input,
output: typeof raw?.output === "number" ? raw.output : DEFAULT_MODEL_COST.output,
cacheRead: typeof raw?.cacheRead === "number" ? raw.cacheRead : DEFAULT_MODEL_COST.cacheRead,
cacheWrite: typeof raw?.cacheWrite === "number" ? raw.cacheWrite : DEFAULT_MODEL_COST.cacheWrite
};
}
function resolveAnthropicDefaultAuthMode(cfg) {
const profiles = cfg.auth?.profiles ?? {};
const anthropicProfiles = Object.entries(profiles).filter(([, profile]) => profile?.provider === "anthropic");
const order = cfg.auth?.order?.anthropic ?? [];
for (const profileId of order) {
const entry = profiles[profileId];
if (!entry || entry.provider !== "anthropic") continue;
if (entry.mode === "api_key") return "api_key";
if (entry.mode === "oauth" || entry.mode === "token") return "oauth";
}
const hasApiKey = anthropicProfiles.some(([, profile]) => profile?.mode === "api_key");
const hasOauth = anthropicProfiles.some(([, profile]) => profile?.mode === "oauth" || profile?.mode === "token");
if (hasApiKey && !hasOauth) return "api_key";
if (hasOauth && !hasApiKey) return "oauth";
if (process.env.ANTHROPIC_OAUTH_TOKEN?.trim()) return "oauth";
if (process.env.ANTHROPIC_API_KEY?.trim()) return "api_key";
return null;
}
function resolvePrimaryModelRef(raw) {
if (!raw || typeof raw !== "string") return null;
const trimmed = raw.trim();
if (!trimmed) return null;
return DEFAULT_MODEL_ALIASES[trimmed.toLowerCase()] ?? trimmed;
}
function applyMessageDefaults(cfg) {
const messages = cfg.messages;
if (messages?.ackReactionScope !== void 0) return cfg;
const nextMessages = messages ? { ...messages } : {};
nextMessages.ackReactionScope = "group-mentions";
return {
...cfg,
messages: nextMessages
};
}
function applySessionDefaults(cfg, options = {}) {
const session = cfg.session;
if (!session || session.mainKey === void 0) return cfg;
const trimmed = session.mainKey.trim();
const warn = options.warn ?? console.warn;
const warnState = options.warnState ?? defaultWarnState;
const next = {
...cfg,
session: {
...session,
mainKey: "main"
}
};
if (trimmed && trimmed !== "main" && !warnState.warned) {
warnState.warned = true;
warn("session.mainKey is ignored; main session is always \"main\".");
}
return next;
}
function applyTalkApiKey(config) {
const resolved = resolveTalkApiKey();
if (!resolved) return config;
if (config.talk?.apiKey?.trim()) return config;
return {
...config,
talk: {
...config.talk,
apiKey: resolved
}
};
}
function applyModelDefaults(cfg) {
let mutated = false;
let nextCfg = cfg;
const providerConfig = nextCfg.models?.providers;
if (providerConfig) {
const nextProviders = { ...providerConfig };
for (const [providerId, provider] of Object.entries(providerConfig)) {
const models = provider.models;
if (!Array.isArray(models) || models.length === 0) continue;
let providerMutated = false;
const nextModels = models.map((model) => {
const raw = model;
let modelMutated = false;
const reasoning = typeof raw.reasoning === "boolean" ? raw.reasoning : false;
if (raw.reasoning !== reasoning) modelMutated = true;
const input = raw.input ?? [...DEFAULT_MODEL_INPUT];
if (raw.input === void 0) modelMutated = true;
const cost = resolveModelCost(raw.cost);
if (!raw.cost || raw.cost.input !== cost.input || raw.cost.output !== cost.output || raw.cost.cacheRead !== cost.cacheRead || raw.cost.cacheWrite !== cost.cacheWrite) modelMutated = true;
const contextWindow = isPositiveNumber(raw.contextWindow) ? raw.contextWindow : DEFAULT_CONTEXT_TOKENS;
if (raw.contextWindow !== contextWindow) modelMutated = true;
const defaultMaxTokens = Math.min(DEFAULT_MODEL_MAX_TOKENS, contextWindow);
const maxTokens = isPositiveNumber(raw.maxTokens) ? raw.maxTokens : defaultMaxTokens;
if (raw.maxTokens !== maxTokens) modelMutated = true;
if (!modelMutated) return model;
providerMutated = true;
return {
...raw,
reasoning,
input,
cost,
contextWindow,
maxTokens
};
});
if (!providerMutated) continue;
nextProviders[providerId] = {
...provider,
models: nextModels
};
mutated = true;
}
if (mutated) nextCfg = {
...nextCfg,
models: {
...nextCfg.models,
providers: nextProviders
}
};
}
const existingAgent = nextCfg.agents?.defaults;
if (!existingAgent) return mutated ? nextCfg : cfg;
const existingModels = existingAgent.models ?? {};
if (Object.keys(existingModels).length === 0) return mutated ? nextCfg : cfg;
const nextModels = { ...existingModels };
for (const [alias, target] of Object.entries(DEFAULT_MODEL_ALIASES)) {
const entry = nextModels[target];
if (!entry) continue;
if (entry.alias !== void 0) continue;
nextModels[target] = {
...entry,
alias
};
mutated = true;
}
if (!mutated) return cfg;
return {
...nextCfg,
agents: {
...nextCfg.agents,
defaults: {
...existingAgent,
models: nextModels
}
}
};
}
function applyAgentDefaults(cfg) {
const agents = cfg.agents;
const defaults = agents?.defaults;
const hasMax = typeof defaults?.maxConcurrent === "number" && Number.isFinite(defaults.maxConcurrent);
const hasSubMax = typeof defaults?.subagents?.maxConcurrent === "number" && Number.isFinite(defaults.subagents.maxConcurrent);
if (hasMax && hasSubMax) return cfg;
let mutated = false;
const nextDefaults = defaults ? { ...defaults } : {};
if (!hasMax) {
nextDefaults.maxConcurrent = DEFAULT_AGENT_MAX_CONCURRENT;
mutated = true;
}
const nextSubagents = defaults?.subagents ? { ...defaults.subagents } : {};
if (!hasSubMax) {
nextSubagents.maxConcurrent = DEFAULT_SUBAGENT_MAX_CONCURRENT;
mutated = true;
}
if (!mutated) return cfg;
return {
...cfg,
agents: {
...agents,
defaults: {
...nextDefaults,
subagents: nextSubagents
}
}
};
}
function applyLoggingDefaults(cfg) {
const logging = cfg.logging;
if (!logging) return cfg;
if (logging.redactSensitive) return cfg;
return {
...cfg,
logging: {
...logging,
redactSensitive: "tools"
}
};
}
function applyContextPruningDefaults(cfg) {
const defaults = cfg.agents?.defaults;
if (!defaults) return cfg;
const authMode = resolveAnthropicDefaultAuthMode(cfg);
if (!authMode) return cfg;
let mutated = false;
const nextDefaults = { ...defaults };
const contextPruning = defaults.contextPruning ?? {};
const heartbeat = defaults.heartbeat ?? {};
if (defaults.contextPruning?.mode === void 0) {
nextDefaults.contextPruning = {
...contextPruning,
mode: "cache-ttl",
ttl: defaults.contextPruning?.ttl ?? "1h"
};
mutated = true;
}
if (defaults.heartbeat?.every === void 0) {
nextDefaults.heartbeat = {
...heartbeat,
every: authMode === "oauth" ? "1h" : "30m"
};
mutated = true;
}
if (authMode === "api_key") {
const nextModels = defaults.models ? { ...defaults.models } : {};
let modelsMutated = false;
for (const [key, entry] of Object.entries(nextModels)) {
const parsed = parseModelRef(key, "anthropic");
if (!parsed || parsed.provider !== "anthropic") continue;
const current = entry ?? {};
const params = current.params ?? {};
if (typeof params.cacheRetention === "string") continue;
nextModels[key] = {
...current,
params: {
...params,
cacheRetention: "short"
}
};
modelsMutated = true;
}
const primary = resolvePrimaryModelRef(defaults.model?.primary ?? void 0);
if (primary) {
const parsedPrimary = parseModelRef(primary, "anthropic");
if (parsedPrimary?.provider === "anthropic") {
const key = `${parsedPrimary.provider}/${parsedPrimary.model}`;
const current = nextModels[key] ?? {};
const params = current.params ?? {};
if (typeof params.cacheRetention !== "string") {
nextModels[key] = {
...current,
params: {
...params,
cacheRetention: "short"
}
};
modelsMutated = true;
}
}
}
if (modelsMutated) {
nextDefaults.models = nextModels;
mutated = true;
}
}
if (!mutated) return cfg;
return {
...cfg,
agents: {
...cfg.agents,
defaults: nextDefaults
}
};
}
function applyCompactionDefaults(cfg) {
const defaults = cfg.agents?.defaults;
if (!defaults) return cfg;
const compaction = defaults?.compaction;
if (compaction?.mode) return cfg;
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...defaults,
compaction: {
...compaction,
mode: "safeguard"
}
}
}
};
}
//#endregion
//#region src/config/env-substitution.ts
/**
* Environment variable substitution for config values.
*
* Supports `${VAR_NAME}` syntax in string values, substituted at config load time.
* - Only uppercase env vars are matched: `[A-Z_][A-Z0-9_]*`
* - Escape with `$${}` to output literal `${}`
* - Missing env vars throw `MissingEnvVarError` with context
*
* @example
* ```json5
* {
* models: {
* providers: {
* "vercel-gateway": {
* apiKey: "${VERCEL_GATEWAY_API_KEY}"
* }
* }
* }
* }
* ```
*/
const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
var MissingEnvVarError = class extends Error {
constructor(varName, configPath) {
super(`Missing env var "${varName}" referenced at config path: ${configPath}`);
this.varName = varName;
this.configPath = configPath;
this.name = "MissingEnvVarError";
}
};
function isPlainObject$4(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
}
function substituteString(value, env, configPath) {
if (!value.includes("$")) return value;
const chunks = [];
for (let i = 0; i < value.length; i += 1) {
const char = value[i];
if (char !== "$") {
chunks.push(char);
continue;
}
const next = value[i + 1];
const afterNext = value[i + 2];
if (next === "$" && afterNext === "{") {
const start = i + 3;
const end = value.indexOf("}", start);
if (end !== -1) {
const name = value.slice(start, end);
if (ENV_VAR_NAME_PATTERN.test(name)) {
chunks.push(`\${${name}}`);
i = end;
continue;
}
}
}
if (next === "{") {
const start = i + 2;
const end = value.indexOf("}", start);
if (end !== -1) {
const name = value.slice(start, end);
if (ENV_VAR_NAME_PATTERN.test(name)) {
const envValue = env[name];
if (envValue === void 0 || envValue === "") throw new MissingEnvVarError(name, configPath);
chunks.push(envValue);
i = end;
continue;
}
}
}
chunks.push(char);
}
return chunks.join("");
}
function substituteAny(value, env, path) {
if (typeof value === "string") return substituteString(value, env, path);
if (Array.isArray(value)) return value.map((item, index) => substituteAny(item, env, `${path}[${index}]`));
if (isPlainObject$4(value)) {
const result = {};
for (const [key, val] of Object.entries(value)) result[key] = substituteAny(val, env, path ? `${path}.${key}` : key);
return result;
}
return value;
}
/**
* Resolves `${VAR_NAME}` environment variable references in config values.
*
* @param obj - The parsed config object (after JSON5 parse and $include resolution)
* @param env - Environment variables to use for substitution (defaults to process.env)
* @returns The config object with env vars substituted
* @throws {MissingEnvVarError} If a referenced env var is not set or empty
*/
function resolveConfigEnvVars(obj, env = process.env) {
return substituteAny(obj, env, "");
}
//#endregion
//#region src/config/env-vars.ts
function collectConfigEnvVars(cfg) {
const envConfig = cfg?.env;
if (!envConfig) return {};
const entries = {};
if (envConfig.vars) for (const [key, value] of Object.entries(envConfig.vars)) {
if (!value) continue;
entries[key] = value;
}
for (const [key, value] of Object.entries(envConfig)) {
if (key === "shellEnv" || key === "vars") continue;
if (typeof value !== "string" || !value.trim()) continue;
entries[key] = value;
}
return entries;
}
//#endregion
//#region src/config/includes.ts
/**
* Config includes: $include directive for modular configs
*
* @example
* ```json5
* {
* "$include": "./base.json5", // single file
* "$include": ["./a.json5", "./b.json5"] // merge multiple
* }
* ```
*/
const INCLUDE_KEY = "$include";
const MAX_INCLUDE_DEPTH = 10;
var ConfigIncludeError = class extends Error {
constructor(message, includePath, cause) {
super(message);
this.includePath = includePath;
this.cause = cause;
this.name = "ConfigIncludeError";
}
};
var CircularIncludeError = class extends ConfigIncludeError {
constructor(chain) {
super(`Circular include detected: ${chain.join(" -> ")}`, chain[chain.length - 1]);
this.chain = chain;
this.name = "CircularIncludeError";
}
};
function isPlainObject$3(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
}
/** Deep merge: arrays concatenate, objects merge recursively, primitives: source wins */
function deepMerge(target, source) {
if (Array.isArray(target) && Array.isArray(source)) return [...target, ...source];
if (isPlainObject$3(target) && isPlainObject$3(source)) {
const result = { ...target };
for (const key of Object.keys(source)) result[key] = key in result ? deepMerge(result[key], source[key]) : source[key];
return result;
}
return source;
}
var IncludeProcessor = class IncludeProcessor {
constructor(basePath, resolver) {
this.basePath = basePath;
this.resolver = resolver;
this.visited = /* @__PURE__ */ new Set();
this.depth = 0;
this.visited.add(path.normalize(basePath));
}
process(obj) {
if (Array.isArray(obj)) return obj.map((item) => this.process(item));
if (!isPlainObject$3(obj)) return obj;
if (!(INCLUDE_KEY in obj)) return this.processObject(obj);
return this.processInclude(obj);
}
processObject(obj) {
const result = {};
for (const [key, value] of Object.entries(obj)) result[key] = this.process(value);
return result;
}
processInclude(obj) {
const includeValue = obj[INCLUDE_KEY];
const otherKeys = Object.keys(obj).filter((k) => k !== INCLUDE_KEY);
const included = this.resolveInclude(includeValue);
if (otherKeys.length === 0) return included;
if (!isPlainObject$3(included)) throw new ConfigIncludeError("Sibling keys require included content to be an object", typeof includeValue === "string" ? includeValue : INCLUDE_KEY);
const rest = {};
for (const key of otherKeys) rest[key] = this.process(obj[key]);
return deepMerge(included, rest);
}
resolveInclude(value) {
if (typeof value === "string") return this.loadFile(value);
if (Array.isArray(value)) return value.reduce((merged, item) => {
if (typeof item !== "string") throw new ConfigIncludeError(`Invalid $include array item: expected string, got ${typeof item}`, String(item));
return deepMerge(merged, this.loadFile(item));
}, {});
throw new ConfigIncludeError(`Invalid $include value: expected string or array of strings, got ${typeof value}`, String(value));
}
loadFile(includePath) {
const resolvedPath = this.resolvePath(includePath);
this.checkCircular(resolvedPath);
this.checkDepth(includePath);
const raw = this.readFile(includePath, resolvedPath);
const parsed = this.parseFile(includePath, resolvedPath, raw);
return this.processNested(resolvedPath, parsed);
}
resolvePath(includePath) {
const resolved = path.isAbsolute(includePath) ? includePath : path.resolve(path.dirname(this.basePath), includePath);
return path.normalize(resolved);
}
checkCircular(resolvedPath) {
if (this.visited.has(resolvedPath)) throw new CircularIncludeError([...this.visited, resolvedPath]);
}
checkDepth(includePath) {
if (this.depth >= MAX_INCLUDE_DEPTH) throw new ConfigIncludeError(`Maximum include depth (${MAX_INCLUDE_DEPTH}) exceeded at: ${includePath}`, includePath);
}
readFile(includePath, resolvedPath) {
try {
return this.resolver.readFile(resolvedPath);
} catch (err) {
throw new ConfigIncludeError(`Failed to read include file: ${includePath} (resolved: ${resolvedPath})`, includePath, err instanceof Error ? err : void 0);
}
}
parseFile(includePath, resolvedPath, raw) {
try {
return this.resolver.parseJson(raw);
} catch (err) {
throw new ConfigIncludeError(`Failed to parse include file: ${includePath} (resolved: ${resolvedPath})`, includePath, err instanceof Error ? err : void 0);
}
}
processNested(resolvedPath, parsed) {
const nested = new IncludeProcessor(resolvedPath, this.resolver);
nested.visited = new Set([...this.visited, resolvedPath]);
nested.depth = this.depth + 1;
return nested.process(parsed);
}
};
const defaultResolver = {
readFile: (p) => fs.readFileSync(p, "utf-8"),
parseJson: (raw) => JSON5.parse(raw)
};
/**
* Resolves all $include directives in a parsed config object.
*/
function resolveConfigIncludes(obj, configPath, resolver = defaultResolver) {
return new IncludeProcessor(configPath, resolver).process(obj);
}
//#endregion
//#region src/config/legacy.shared.ts
const isRecord$2 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
const getRecord = (value) => isRecord$2(value) ? value : null;
const ensureRecord = (root, key) => {
const existing = root[key];
if (isRecord$2(existing)) return existing;
const next = {};
root[key] = next;
return next;
};
const mergeMissing = (target, source) => {
for (const [key, value] of Object.entries(source)) {
if (value === void 0) continue;
const existing = target[key];
if (existing === void 0) {
target[key] = value;
continue;
}
if (isRecord$2(existing) && isRecord$2(value)) mergeMissing(existing, value);
}
};
const AUDIO_TRANSCRIPTION_CLI_ALLOWLIST = new Set(["whisper"]);
const mapLegacyAudioTranscription = (value) => {
const transcriber = getRecord(value);
const command = Array.isArray(transcriber?.command) ? transcriber?.command : null;
if (!command || command.length === 0) return null;
const rawExecutable = String(command[0] ?? "").trim();
if (!rawExecutable) return null;
const executableName = rawExecutable.split(/[\\/]/).pop() ?? rawExecutable;
if (!AUDIO_TRANSCRIPTION_CLI_ALLOWLIST.has(executableName)) return null;
const args = command.slice(1).map((part) => String(part));
const timeoutSeconds = typeof transcriber?.timeoutSeconds === "number" ? transcriber?.timeoutSeconds : void 0;
const result = {
command: rawExecutable,
type: "cli"
};
if (args.length > 0) result.args = args;
if (timeoutSeconds !== void 0) result.timeoutSeconds = timeoutSeconds;
return result;
};
const getAgentsList = (agents) => {
const list = agents?.list;
return Array.isArray(list) ? list : [];
};
const resolveDefaultAgentIdFromRaw = (raw) => {
const list = getAgentsList(getRecord(raw.agents));
const defaultEntry = list.find((entry) => isRecord$2(entry) && entry.default === true && typeof entry.id === "string" && entry.id.trim() !== "");
if (defaultEntry) return defaultEntry.id.trim();
const routing = getRecord(raw.routing);
const routingDefault = typeof routing?.defaultAgentId === "string" ? routing.defaultAgentId.trim() : "";
if (routingDefault) return routingDefault;
const firstEntry = list.find((entry) => isRecord$2(entry) && typeof entry.id === "string" && entry.id.trim() !== "");
if (firstEntry) return firstEntry.id.trim();
return "main";
};
const ensureAgentEntry = (list, id) => {
const normalized = id.trim();
const existing = list.find((entry) => isRecord$2(entry) && typeof entry.id === "string" && entry.id.trim() === normalized);
if (existing) return existing;
const created = { id: normalized };
list.push(created);
return created;
};
//#endregion
//#region src/config/legacy.migrations.part-1.ts
const LEGACY_CONFIG_MIGRATIONS_PART_1 = [
{
id: "bindings.match.provider->bindings.match.channel",
describe: "Move bindings[].match.provider to bindings[].match.channel",
apply: (raw, changes) => {
const bindings = Array.isArray(raw.bindings) ? raw.bindings : null;
if (!bindings) return;
let touched = false;
for (const entry of bindings) {
if (!isRecord$2(entry)) continue;
const match = getRecord(entry.match);
if (!match) continue;
if (typeof match.channel === "string" && match.channel.trim()) continue;
const provider = typeof match.provider === "string" ? match.provider.trim() : "";
if (!provider) continue;
match.channel = provider;
delete match.provider;
entry.match = match;
touched = true;
}
if (touched) {
raw.bindings = bindings;
changes.push("Moved bindings[].match.provider → bindings[].match.channel.");
}
}
},
{
id: "bindings.match.accountID->bindings.match.accountId",
describe: "Move bindings[].match.accountID to bindings[].match.accountId",
apply: (raw, changes) => {
const bindings = Array.isArray(raw.bindings) ? raw.bindings : null;
if (!bindings) return;
let touched = false;
for (const entry of bindings) {
if (!isRecord$2(entry)) continue;
const match = getRecord(entry.match);
if (!match) continue;
if (match.accountId !== void 0) continue;
const accountID = typeof match.accountID === "string" ? match.accountID.trim() : match.accountID;
if (!accountID) continue;
match.accountId = accountID;
delete match.accountID;
entry.match = match;
touched = true;
}
if (touched) {
raw.bindings = bindings;
changes.push("Moved bindings[].match.accountID → bindings[].match.accountId.");
}
}
},
{
id: "session.sendPolicy.rules.match.provider->match.channel",
describe: "Move session.sendPolicy.rules[].match.provider to match.channel",
apply: (raw, changes) => {
const session = getRecord(raw.session);
if (!session) return;
const sendPolicy = getRecord(session.sendPolicy);
if (!sendPolicy) return;
const rules = Array.isArray(sendPolicy.rules) ? sendPolicy.rules : null;
if (!rules) return;
let touched = false;
for (const rule of rules) {
if (!isRecord$2(rule)) continue;
const match = getRecord(rule.match);
if (!match) continue;
if (typeof match.channel === "string" && match.channel.trim()) continue;
const provider = typeof match.provider === "string" ? match.provider.trim() : "";
if (!provider) continue;
match.channel = provider;
delete match.provider;
rule.match = match;
touched = true;
}
if (touched) {
sendPolicy.rules = rules;
session.sendPolicy = sendPolicy;
raw.session = session;
changes.push("Moved session.sendPolicy.rules[].match.provider → match.channel.");
}
}
},
{
id: "messages.queue.byProvider->byChannel",
describe: "Move messages.queue.byProvider to messages.queue.byChannel",
apply: (raw, changes) => {
const messages = getRecord(raw.messages);
if (!messages) return;
const queue = getRecord(messages.queue);
if (!queue) return;
if (queue.byProvider === void 0) return;
if (queue.byChannel === void 0) {
queue.byChannel = queue.byProvider;
changes.push("Moved messages.queue.byProvider → messages.queue.byChannel.");
} else changes.push("Removed messages.queue.byProvider (messages.queue.byChannel already set).");
delete queue.byProvider;
messages.queue = queue;
raw.messages = messages;
}
},
{
id: "providers->channels",
describe: "Move provider config sections to channels.*",
apply: (raw, changes) => {
const legacyEntries = [
"whatsapp",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"msteams"
].filter((key) => isRecord$2(raw[key]));
if (legacyEntries.length === 0) return;
const channels = ensureRecord(raw, "channels");
for (const key of legacyEntries) {
const legacy = getRecord(raw[key]);
if (!legacy) continue;
const channelEntry = ensureRecord(channels, key);
const hadEntries = Object.keys(channelEntry).length > 0;
mergeMissing(channelEntry, legacy);
channels[key] = channelEntry;
delete raw[key];
changes.push(hadEntries ? `Merged ${key} → channels.${key}.` : `Moved ${key} → channels.${key}.`);
}
raw.channels = channels;
}
},
{
id: "routing.allowFrom->channels.whatsapp.allowFrom",
describe: "Move routing.allowFrom to channels.whatsapp.allowFrom",
apply: (raw, changes) => {
const routing = raw.routing;
if (!routing || typeof routing !== "object") return;
const allowFrom = routing.allowFrom;
if (allowFrom === void 0) return;
const channels = getRecord(raw.channels);
const whatsapp = channels ? getRecord(channels.whatsapp) : null;
if (!whatsapp) {
delete routing.allowFrom;
if (Object.keys(routing).length === 0) delete raw.routing;
changes.push("Removed routing.allowFrom (channels.whatsapp not configured).");
return;
}
if (whatsapp.allowFrom === void 0) {
whatsapp.allowFrom = allowFrom;
changes.push("Moved routing.allowFrom → channels.whatsapp.allowFrom.");
} else changes.push("Removed routing.allowFrom (channels.whatsapp.allowFrom already set).");
delete routing.allowFrom;
if (Object.keys(routing).length === 0) delete raw.routing;
channels.whatsapp = whatsapp;
raw.channels = channels;
}
},
{
id: "routing.groupChat.requireMention->groups.*.requireMention",
describe: "Move routing.groupChat.requireMention to channels.whatsapp/telegram/imessage groups",
apply: (raw, changes) => {
const routing = raw.routing;
if (!routing || typeof routing !== "object") return;
const groupChat = routing.groupChat && typeof routing.groupChat === "object" ? routing.groupChat : null;
if (!groupChat) return;
const requireMention = groupChat.requireMention;
if (requireMention === void 0) return;
const channels = ensureRecord(raw, "channels");
const applyTo = (key, options) => {
if (options?.requireExisting && !isRecord$2(channels[key])) return;
const section = channels[key] && typeof channels[key] === "object" ? channels[key] : {};
const groups = section.groups && typeof section.groups === "object" ? section.groups : {};
const defaultKey = "*";
const entry = groups[defaultKey] && typeof groups[defaultKey] === "object" ? groups[defaultKey] : {};
if (entry.requireMention === void 0) {
entry.requireMention = requireMention;
groups[defaultKey] = entry;
section.groups = groups;
channels[key] = section;
changes.push(`Moved routing.groupChat.requireMention → channels.${key}.groups."*".requireMention.`);
} else changes.push(`Removed routing.groupChat.requireMention (channels.${key}.groups."*" already set).`);
};
applyTo("whatsapp", { requireExisting: true });
applyTo("telegram");
applyTo("imessage");
delete groupChat.requireMention;
if (Object.keys(groupChat).length === 0) delete routing.groupChat;
if (Object.keys(routing).length === 0) delete raw.routing;
raw.channels = channels;
}
},
{
id: "gateway.token->gateway.auth.token",
describe: "Move gateway.token to gateway.auth.token",
apply: (raw, changes) => {
const gateway = raw.gateway;
if (!gateway || typeof gateway !== "object") return;
const token = gateway.token;
if (token === void 0) return;
const gatewayObj = gateway;
const auth = gatewayObj.auth && typeof gatewayObj.auth === "object" ? gatewayObj.auth : {};
if (auth.token === void 0) {
auth.token = token;
if (!auth.mode) auth.mode = "token";
changes.push("Moved gateway.token → gateway.auth.token.");
} else changes.push("Removed gateway.token (gateway.auth.token already set).");
delete gatewayObj.token;
if (Object.keys(auth).length > 0) gatewayObj.auth = auth;
raw.gateway = gatewayObj;
}
},
{
id: "telegram.requireMention->channels.telegram.groups.*.requireMention",
describe: "Move telegram.requireMention to channels.telegram.groups.*.requireMention",
apply: (raw, changes) => {
const channels = ensureRecord(raw, "channels");
const telegram = channels.telegram;
if (!telegram || typeof telegram !== "object") return;
const requireMention = telegram.requireMention;
if (requireMention === void 0) return;
const groups = telegram.groups && typeof telegram.groups === "object" ? telegram.groups : {};
const defaultKey = "*";
const entry = groups[defaultKey] && typeof groups[defaultKey] === "object" ? groups[defaultKey] : {};
if (entry.requireMention === void 0) {
entry.requireMention = requireMention;
groups[defaultKey] = entry;
telegram.groups = groups;
changes.push("Moved telegram.requireMention → channels.telegram.groups.\"*\".requireMention.");
} else changes.push("Removed telegram.requireMention (channels.telegram.groups.\"*\" already set).");
delete telegram.requireMention;
channels.telegram = telegram;
raw.channels = channels;
}
}
];
//#endregion
//#region src/config/legacy.migrations.part-2.ts
const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
{
id: "agent.model-config-v2",
describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
apply: (raw, changes) => {
const agentRoot = getRecord(raw.agent);
const defaults = getRecord(getRecord(raw.agents)?.defaults);
const agent = agentRoot ?? defaults;
if (!agent) return;
const label = agentRoot ? "agent" : "agents.defaults";
const legacyModel = typeof agent.model === "string" ? String(agent.model) : void 0;
const legacyImageModel = typeof agent.imageModel === "string" ? String(agent.imageModel) : void 0;
const legacyAllowed = Array.isArray(agent.allowedModels) ? agent.allowedModels.map(String) : [];
const legacyModelFallbacks = Array.isArray(agent.modelFallbacks) ? agent.modelFallbacks.map(String) : [];
const legacyImageModelFallbacks = Array.isArray(agent.imageModelFallbacks) ? agent.imageModelFallbacks.map(String) : [];
const legacyAliases = agent.modelAliases && typeof agent.modelAliases === "object" ? agent.modelAliases : {};
if (!(legacyModel || legacyImageModel || legacyAllowed.length > 0 || legacyModelFallbacks.length > 0 || legacyImageModelFallbacks.length > 0 || Object.keys(legacyAliases).length > 0)) return;
const models = agent.models && typeof agent.models === "object" ? agent.models : {};
const ensureModel = (rawKey) => {
if (typeof rawKey !== "string") return;
const key = rawKey.trim();
if (!key) return;
if (!models[key]) models[key] = {};
};
ensureModel(legacyModel);
ensureModel(legacyImageModel);
for (const key of legacyAllowed) ensureModel(key);
for (const key of legacyModelFallbacks) ensureModel(key);
for (const key of legacyImageModelFallbacks) ensureModel(key);
for (const target of Object.values(legacyAliases)) {
if (typeof target !== "string") continue;
ensureModel(target);
}
for (const [alias, targetRaw] of Object.entries(legacyAliases)) {
if (typeof targetRaw !== "string") continue;
const target = targetRaw.trim();
if (!target) continue;
const entry = models[target] && typeof models[target] === "object" ? models[target] : {};
if (!("alias" in entry)) {
entry.alias = alias;
models[target] = entry;
}
}
const currentModel = agent.model && typeof agent.model === "object" ? agent.model : null;
if (currentModel) {
if (!currentModel.primary && legacyModel) currentModel.primary = legacyModel;
if (legacyModelFallbacks.length > 0 && (!Array.isArray(currentModel.fallbacks) || currentModel.fallbacks.length === 0)) currentModel.fallbacks = legacyModelFallbacks;
agent.model = currentModel;
} else if (legacyModel || legacyModelFallbacks.length > 0) agent.model = {
primary: legacyModel,
fallbacks: legacyModelFallbacks.length ? legacyModelFallbacks : []
};
const currentImageModel = agent.imageModel && typeof agent.imageModel === "object" ? agent.imageModel : null;
if (currentImageModel) {
if (!currentImageModel.primary && legacyImageModel) currentImageModel.primary = legacyImageModel;
if (legacyImageModelFallbacks.length > 0 && (!Array.isArray(currentImageModel.fallbacks) || currentImageModel.fallbacks.length === 0)) currentImageModel.fallbacks = legacyImageModelFallbacks;
agent.imageModel = currentImageModel;
} else if (legacyImageModel || legacyImageModelFallbacks.length > 0) agent.imageModel = {
primary: legacyImageModel,
fallbacks: legacyImageModelFallbacks.length ? legacyImageModelFallbacks : []
};
agent.models = models;
if (legacyModel !== void 0) changes.push(`Migrated ${label}.model string → ${label}.model.primary.`);
if (legacyModelFallbacks.length > 0) changes.push(`Migrated ${label}.modelFallbacks → ${label}.model.fallbacks.`);
if (legacyImageModel !== void 0) changes.push(`Migrated ${label}.imageModel string → ${label}.imageModel.primary.`);
if (legacyImageModelFallbacks.length > 0) changes.push(`Migrated ${label}.imageModelFallbacks → ${label}.imageModel.fallbacks.`);
if (legacyAllowed.length > 0) changes.push(`Migrated ${label}.allowedModels → ${label}.models.`);
if (Object.keys(legacyAliases).length > 0) changes.push(`Migrated ${label}.modelAliases → ${label}.models.*.alias.`);
delete agent.allowedModels;
delete agent.modelAliases;
delete agent.modelFallbacks;
delete agent.imageModelFallbacks;
}
},
{
id: "routing.agents-v2",
describe: "Move routing.agents/defaultAgentId to agents.list",
apply: (raw, changes) => {
const routing = getRecord(raw.routing);
if (!routing) return;
const routingAgents = getRecord(routing.agents);
const agents = ensureRecord(raw, "agents");
const list = getAgentsList(agents);
if (routingAgents) {
for (const [rawId, entryRaw] of Object.entries(routingAgents)) {
const agentId = String(rawId ?? "").trim();
const entry = getRecord(entryRaw);
if (!agentId || !entry) continue;
const target = ensureAgentEntry(list, agentId);
const entryCopy = { ...entry };
if ("mentionPatterns" in entryCopy) {
const mentionPatterns = entryCopy.mentionPatterns;
const groupChat = ensureRecord(target, "groupChat");
if (groupChat.mentionPatterns === void 0) {
groupChat.mentionPatterns = mentionPatterns;
changes.push(`Moved routing.agents.${agentId}.mentionPatterns → agents.list (id "${agentId}").groupChat.mentionPatterns.`);
} else changes.push(`Removed routing.agents.${agentId}.mentionPatterns (agents.list groupChat mentionPatterns already set).`);
delete entryCopy.mentionPatterns;
}
const legacyGroupChat = getRecord(entryCopy.groupChat);
if (legacyGroupChat) {
mergeMissing(ensureRecord(target, "groupChat"), legacyGroupChat);
delete entryCopy.groupChat;
}
const legacySandbox = getRecord(entryCopy.sandbox);
if (legacySandbox) {
const sandboxTools = getRecord(legacySandbox.tools);
if (sandboxTools) {
mergeMissing(ensureRecord(ensureRecord(ensureRecord(target, "tools"), "sandbox"), "tools"), sandboxTools);
delete legacySandbox.tools;
changes.push(`Moved routing.agents.${agentId}.sandbox.tools → agents.list (id "${agentId}").tools.sandbox.tools.`);
}
entryCopy.sandbox = legacySandbox;
}
mergeMissing(target, entryCopy);
}
delete routing.agents;
changes.push("Moved routing.agents → agents.list.");
}
const defaultAgentId = typeof routing.defaultAgentId === "string" ? routing.defaultAgentId.trim() : "";
if (defaultAgentId) {
if (!list.some((entry) => isRecord$2(entry) && entry.default === true)) {
const entry = ensureAgentEntry(list, defaultAgentId);
entry.default = true;
changes.push(`Moved routing.defaultAgentId → agents.list (id "${defaultAgentId