UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,301 lines (1,289 loc) 177 kB
import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js"; import { B as resolveConfigPath, F as CONFIG_PATH, G as resolveIsNixMode, H as resolveDefaultConfigCandidates, I as DEFAULT_GATEWAY_PORT, J as resolveOAuthDir, K as resolveLegacyStateDirs, L as STATE_DIR, R as isNixMode, U as resolveGatewayLockDir, V as resolveConfigPathCandidate, W as resolveGatewayPort, X as resolveStateDir, Y as resolveOAuthPath, Z as CHANNEL_IDS, ot as normalizeChatChannelId, q as resolveNewStateDir, z as resolveCanonicalConfigPath } from "./entry.js"; import { A as parseModelRef, ct as loadShellEnvFallback, dt as shouldEnableShellEnvFallback, ht as DEFAULT_CONTEXT_TOKENS, lt as resolveShellEnvFallbackTimeoutMs, ut as shouldDeferShellEnvFallback } from "./auth-profiles-CYBuGiBb.js"; import { c as normalizeAgentId, n as DEFAULT_AGENT_ID } from "./session-key-CZkcvAtx.js"; import { m as resolveUserPath } from "./utils-DX85MiPR.js"; import { c as resolveDefaultAgentId, s as resolveAgentWorkspaceDir } from "./agent-scope-C9VjJXEK.js"; import { c as resolveEnableState, l as resolveMemorySlotDecision, s as normalizePluginsConfig, t as loadPluginManifestRegistry } from "./manifest-registry-C69Z-I4v.js"; import { createRequire } from "node:module"; import path from "node:path"; import os from "node:os"; import fs from "node:fs"; import JSON5 from "json5"; import crypto from "node:crypto"; import AjvPkg from "ajv"; import { z } from "zod"; //#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; } function resolveSubagentMaxConcurrent(cfg) { const raw = cfg?.agents?.defaults?.subagents?.maxConcurrent; if (typeof raw === "number" && Number.isFinite(raw)) return Math.max(1, Math.floor(raw)); return DEFAULT_SUBAGENT_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$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value)); const getRecord = (value) => isRecord$1(value) ? value : null; const ensureRecord = (root, key) => { const existing = root[key]; if (isRecord$1(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$1(existing) && isRecord$1(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$1(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$1(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$1(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$1(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$1(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$1(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$1(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$1(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$1(entry) && entry.default === true)) { const entry = ensureAgentEntry(list, defaultAgentId); entry.default = true; changes.push(`Moved routing.defaultAgentId → agents.list (id "${defaultAgentId}").default.`); } else changes.push("Removed routing.defaultAgentId (agents.list default already set)."); delete routing.defaultAgentId; } if (list.length > 0) agents.list = list; if (Object.keys(routing).length === 0) delete raw.routing; } }, { id: "routing.config-v2", describe: "Move routing bindings/groupChat/queue/agentToAgent/transcribeAudio", apply: (raw, changes) => { const routing = getRecord(raw.routing); if (!routing) return; if (routing.bindings !== void 0) { if (raw.bindings === void 0) { raw.bindings = routing.bindings; changes.push("Moved routing.bindings → bindings."); } else changes.push("Removed routing.bindings (bindings already set)."); delete routing.bindings; } if (routing.agentToAgent !== void 0) { const tools = ensureRecord(raw, "tools"); if (tools.agentToAgent === void 0) { tools.agentToAgent = routing.agentToAgent; changes.push("Moved routing.agentToAgent → tools.agentToAgent."); } else changes.push("Removed routing.agentToAgent (tools.agentToAgent already set)."); delete routing.agentToAgent; } if (routing.queue !== void 0) { const messages = ensureRecord(raw, "messages"); if (messages.queue === void 0) { messages.queue = routing.queue; changes.push("Moved routing.queue → messages.queue."); } else changes.push("Removed routing.queue (messages.queue already set)."); delete routing.queue; } const groupChat = getRecord(routing.groupChat); if (groupChat) { const historyLimit = groupChat.historyLimit; if (historyLimit !== void 0) { const messagesGroup = ensureRecord(ensureRecord(raw, "messages"), "groupChat"); if (messagesGroup.historyLimit === void 0) { messagesGroup.historyLimit = historyLimit; changes.push("Moved routing.groupChat.historyLimit → messages.groupChat.historyLimit."); } else changes.push("Removed routing.groupChat.historyLimit (messages.groupChat.historyLimit already set)."); delete groupChat.historyLimit; } const mentionPatterns = groupChat.mentionPatterns; if (mentionPatterns !== void 0) { const messagesGroup = ensureRecord(ensureRecord(raw, "messages"), "groupChat"); if (messagesGroup.mentionPatterns === void 0) { messagesGroup.mentionPatterns = mentionPatterns; changes.push("Moved routing.groupChat.mentionPatterns → messages.groupChat.mentionPatterns."); } else changes.push("Removed routing.groupChat.mentionPatterns (messages.groupChat.mentionPatterns already set)."); delete groupChat.mentionPatterns; } if (Object.keys(groupChat).length === 0) delete routing.groupChat; else routing.groupChat = groupChat; } if (routing.transcribeAudio !== void 0) { const mapped = mapLegacyAudioTranscription(routing.transcribeAudio); if (mapped) { const mediaAudio = ensureRecord(ensureRecord(ensureRecord(raw, "tools"), "media"), "audio"); if ((Array.isArray(mediaAudio.models) ? mediaAudio.models : []).length === 0) { mediaAudio.enabled = true; mediaAudio.models = [mapped]; changes.push("Moved routing.transcribeAudio → tools.media.audio.models."); } else changes.push("Removed routing.transcribeAudio (tools.media.audio.models already set)."); } else changes.push("Removed routing.transcribeAudio (unsupported transcription CLI)."); delete routing.transcribeAudio; } const audio = getRecord(raw.audio); if (audio?.transcription !== void 0) { const mapped = mapLegacyAudioTranscription(audio.transcription); if (mapped) { const mediaAudio = ensureRecord(ensureRecord(ensureRecord(raw, "tools"), "media"), "audio"); if ((Array.isArray(mediaAudio.models) ? mediaAudio.models : []).length === 0) { mediaAudio.enabled = true; mediaAudio.models = [mapped]; changes.push("Moved audio.transcription → tools.media.audio.models."); } else changes.push("Removed audio.transcription (tools.media.audio.models already set)."); delete audio.transcription; if (Object.keys(audio).length === 0) delete raw.audio; else raw.audio = audio; } else { delete audio.transcription; changes.push("Removed audio.transcription (unsupported transcription CLI)."); if (Object.keys(audio).length === 0) delete raw.audio; else raw.audio = audio; } } if (Object.keys(routing).length === 0) delete raw.routing; } } ]; //#endregion //#region src/config/legacy.migrations.part-3.ts const LEGACY_CONFIG_MIGRATIONS_PART_3 = [ { id: "auth.anthropic-claude-cli-mode-oauth", describe: "Switch anthropic:claude-cli auth profile mode to oauth", apply: (raw, changes) => { const profiles = getRecord(getRecord(raw.auth)?.profiles); if (!profiles) return; const claudeCli = getRecord(profiles["anthropic:claude-cli"]); if (!claudeCli) return; if (claudeCli.mode !== "token") return; claudeCli.mode = "oauth"; changes.push("Updated auth.profiles[\"anthropic:claude-cli\"].mode → \"oauth\"."); } }, { id: "tools.bash->tools.exec", describe: "Move tools.bash to tools.exec", apply: (raw, changes) => { const tools = ensureRecord(raw, "tools"); const bash = getRecord(tools.bash); if (!bash) return; if (tools.exec === void 0) { tools.exec = bash; changes.push("Moved tools.bash → tools.exec."); } else changes.push("Removed tools.bash (tools.exec already set)."); delete tools.bash; } }, { id: "messages.tts.enabled->auto", describe: "Move messages.tts.enabled to messages.tts.auto", apply: (raw, changes) => { const tts = getRecord(getRecord(raw.messages)?.tts); if (!tts) return; if (tts.auto !== void 0) { if ("enabled" in tts) { delete tts.enabled; changes.push("Removed messages.tts.enabled (messages.tts.auto already set)."); } return; } if (typeof tts.enabled !== "boolean") return; tts.auto = tts.enabled ? "always" : "off"; delete tts.enabled; changes.push(`Moved messages.tts.enabled → messages.tts.auto (${String(tts.auto)}).`); } }, { id: "agent.defaults-v2", describe: "Move agent config to agents.defaults and tools", apply: (raw, changes) => { const agent = getRecord(raw.agent); if (!agent) return; const agents = ensureRecord(raw, "agents"); const defaults = getRecord(agents.defaults) ?? {}; const tools = ensureRecord(raw, "tools"); const agentTools = getRecord(agent.tools); if (agentTools) { if (tools.allow === void 0 && agentTools.allow !== void 0) { tools.allow = agentTools.allow; changes.push("Moved agent.tools.allow → tools.allow."); } if (tools.deny === void 0 && age