import type { LegacyConfigIssue } from "./types.js"; type LegacyConfigRule = { path: string[]; message: string; match?: (value: unknown, root: Record) => boolean; }; type LegacyConfigMigration = { id: string; describe: string; apply: (raw: Record, changes: string[]) => void; }; const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [ { path: ["routing", "allowFrom"], message: "routing.allowFrom was removed; use whatsapp.allowFrom instead (run `clawdbot doctor` to migrate).", }, { path: ["routing", "groupChat", "requireMention"], message: 'routing.groupChat.requireMention was removed; use whatsapp/telegram/imessage groups defaults (e.g. whatsapp.groups."*".requireMention) instead (run `clawdbot doctor` to migrate).', }, { path: ["telegram", "requireMention"], message: 'telegram.requireMention was removed; use telegram.groups."*".requireMention instead (run `clawdbot doctor` to migrate).', }, { path: ["agent", "model"], message: "agent.model string was replaced by agent.model.primary/fallbacks and agent.models (run `clawdbot doctor` to migrate).", match: (value) => typeof value === "string", }, { path: ["agent", "imageModel"], message: "agent.imageModel string was replaced by agent.imageModel.primary/fallbacks (run `clawdbot doctor` to migrate).", match: (value) => typeof value === "string", }, { path: ["agent", "allowedModels"], message: "agent.allowedModels was replaced by agent.models (run `clawdbot doctor` to migrate).", }, { path: ["agent", "modelAliases"], message: "agent.modelAliases was replaced by agent.models.*.alias (run `clawdbot doctor` to migrate).", }, { path: ["agent", "modelFallbacks"], message: "agent.modelFallbacks was replaced by agent.model.fallbacks (run `clawdbot doctor` to migrate).", }, { path: ["agent", "imageModelFallbacks"], message: "agent.imageModelFallbacks was replaced by agent.imageModel.fallbacks (run `clawdbot doctor` to migrate).", }, { path: ["gateway", "token"], message: "gateway.token is ignored; use gateway.auth.token instead (run `clawdbot doctor` to migrate).", }, ]; const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [ { id: "routing.allowFrom->whatsapp.allowFrom", describe: "Move routing.allowFrom to whatsapp.allowFrom", apply: (raw, changes) => { const routing = raw.routing; if (!routing || typeof routing !== "object") return; const allowFrom = (routing as Record).allowFrom; if (allowFrom === undefined) return; const whatsapp = raw.whatsapp && typeof raw.whatsapp === "object" ? (raw.whatsapp as Record) : {}; if (whatsapp.allowFrom === undefined) { whatsapp.allowFrom = allowFrom; changes.push("Moved routing.allowFrom → whatsapp.allowFrom."); } else { changes.push( "Removed routing.allowFrom (whatsapp.allowFrom already set).", ); } delete (routing as Record).allowFrom; if (Object.keys(routing as Record).length === 0) { delete raw.routing; } raw.whatsapp = whatsapp; }, }, { id: "routing.groupChat.requireMention->groups.*.requireMention", describe: "Move routing.groupChat.requireMention to whatsapp/telegram/imessage groups", apply: (raw, changes) => { const routing = raw.routing; if (!routing || typeof routing !== "object") return; const groupChat = (routing as Record).groupChat && typeof (routing as Record).groupChat === "object" ? ((routing as Record).groupChat as Record< string, unknown >) : null; if (!groupChat) return; const requireMention = groupChat.requireMention; if (requireMention === undefined) return; const applyTo = (key: "whatsapp" | "telegram" | "imessage") => { const section = raw[key] && typeof raw[key] === "object" ? (raw[key] as Record) : {}; const groups = section.groups && typeof section.groups === "object" ? (section.groups as Record) : {}; const defaultKey = "*"; const entry = groups[defaultKey] && typeof groups[defaultKey] === "object" ? (groups[defaultKey] as Record) : {}; if (entry.requireMention === undefined) { entry.requireMention = requireMention; groups[defaultKey] = entry; section.groups = groups; raw[key] = section; changes.push( `Moved routing.groupChat.requireMention → ${key}.groups."*".requireMention.`, ); } else { changes.push( `Removed routing.groupChat.requireMention (${key}.groups."*" already set).`, ); } }; applyTo("whatsapp"); applyTo("telegram"); applyTo("imessage"); delete groupChat.requireMention; if (Object.keys(groupChat).length === 0) { delete (routing as Record).groupChat; } if (Object.keys(routing as Record).length === 0) { delete raw.routing; } }, }, { 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 as Record).token; if (token === undefined) return; const gatewayObj = gateway as Record; const auth = gatewayObj.auth && typeof gatewayObj.auth === "object" ? (gatewayObj.auth as Record) : {}; if (auth.token === undefined) { 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->telegram.groups.*.requireMention", describe: "Move telegram.requireMention to telegram.groups.*.requireMention", apply: (raw, changes) => { const telegram = raw.telegram; if (!telegram || typeof telegram !== "object") return; const requireMention = (telegram as Record) .requireMention; if (requireMention === undefined) return; const groups = (telegram as Record).groups && typeof (telegram as Record).groups === "object" ? ((telegram as Record).groups as Record< string, unknown >) : {}; const defaultKey = "*"; const entry = groups[defaultKey] && typeof groups[defaultKey] === "object" ? (groups[defaultKey] as Record) : {}; if (entry.requireMention === undefined) { entry.requireMention = requireMention; groups[defaultKey] = entry; (telegram as Record).groups = groups; changes.push( 'Moved telegram.requireMention → telegram.groups."*".requireMention.', ); } else { changes.push( 'Removed telegram.requireMention (telegram.groups."*" already set).', ); } delete (telegram as Record).requireMention; if (Object.keys(telegram as Record).length === 0) { delete raw.telegram; } }, }, { id: "agent.model-config-v2", describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists", apply: (raw, changes) => { const agent = raw.agent && typeof raw.agent === "object" ? (raw.agent as Record) : null; if (!agent) return; const legacyModel = typeof agent.model === "string" ? String(agent.model) : undefined; const legacyImageModel = typeof agent.imageModel === "string" ? String(agent.imageModel) : undefined; const legacyAllowed = Array.isArray(agent.allowedModels) ? (agent.allowedModels as unknown[]).map(String) : []; const legacyModelFallbacks = Array.isArray(agent.modelFallbacks) ? (agent.modelFallbacks as unknown[]).map(String) : []; const legacyImageModelFallbacks = Array.isArray(agent.imageModelFallbacks) ? (agent.imageModelFallbacks as unknown[]).map(String) : []; const legacyAliases = agent.modelAliases && typeof agent.modelAliases === "object" ? (agent.modelAliases as Record) : {}; const hasLegacy = legacyModel || legacyImageModel || legacyAllowed.length > 0 || legacyModelFallbacks.length > 0 || legacyImageModelFallbacks.length > 0 || Object.keys(legacyAliases).length > 0; if (!hasLegacy) return; const models = agent.models && typeof agent.models === "object" ? (agent.models as Record) : {}; const ensureModel = (rawKey?: string) => { 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] as Record) : {}; if (!("alias" in entry)) { entry.alias = alias; models[target] = entry; } } const currentModel = agent.model && typeof agent.model === "object" ? (agent.model as Record) : 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 as Record) : 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 !== undefined) { changes.push("Migrated agent.model string → agent.model.primary."); } if (legacyModelFallbacks.length > 0) { changes.push("Migrated agent.modelFallbacks → agent.model.fallbacks."); } if (legacyImageModel !== undefined) { changes.push( "Migrated agent.imageModel string → agent.imageModel.primary.", ); } if (legacyImageModelFallbacks.length > 0) { changes.push( "Migrated agent.imageModelFallbacks → agent.imageModel.fallbacks.", ); } if (legacyAllowed.length > 0) { changes.push("Migrated agent.allowedModels → agent.models."); } if (Object.keys(legacyAliases).length > 0) { changes.push("Migrated agent.modelAliases → agent.models.*.alias."); } delete agent.allowedModels; delete agent.modelAliases; delete agent.modelFallbacks; delete agent.imageModelFallbacks; }, }, ]; export function findLegacyConfigIssues(raw: unknown): LegacyConfigIssue[] { if (!raw || typeof raw !== "object") return []; const root = raw as Record; const issues: LegacyConfigIssue[] = []; for (const rule of LEGACY_CONFIG_RULES) { let cursor: unknown = root; for (const key of rule.path) { if (!cursor || typeof cursor !== "object") { cursor = undefined; break; } cursor = (cursor as Record)[key]; } if (cursor !== undefined && (!rule.match || rule.match(cursor, root))) { issues.push({ path: rule.path.join("."), message: rule.message }); } } return issues; } export function applyLegacyMigrations(raw: unknown): { next: Record | null; changes: string[]; } { if (!raw || typeof raw !== "object") return { next: null, changes: [] }; const next = structuredClone(raw) as Record; const changes: string[] = []; for (const migration of LEGACY_CONFIG_MIGRATIONS) { migration.apply(next, changes); } if (changes.length === 0) return { next: null, changes: [] }; return { next, changes }; }