export type LegacyConfigRule = { path: string[]; message: string; match?: (value: unknown, root: Record) => boolean; // If true, only report when the legacy value is present in the original parsed // source (not only after include/env resolution). requireSourceLiteral?: boolean; }; export type LegacyConfigMigration = { id: string; describe: string; apply: (raw: Record, changes: string[]) => void; }; import { isSafeExecutableValue } from "../infra/exec-safety.js"; import { isRecord } from "../utils.js"; import { isBlockedObjectKey } from "./prototype-keys.js"; export { isRecord }; export const getRecord = (value: unknown): Record | null => isRecord(value) ? value : null; export const ensureRecord = ( root: Record, key: string, ): Record => { const existing = root[key]; if (isRecord(existing)) { return existing; } const next: Record = {}; root[key] = next; return next; }; export const mergeMissing = (target: Record, source: Record) => { for (const [key, value] of Object.entries(source)) { if (value === undefined || isBlockedObjectKey(key)) { continue; } const existing = target[key]; if (existing === undefined) { target[key] = value; continue; } if (isRecord(existing) && isRecord(value)) { mergeMissing(existing, value); } } }; export const mapLegacyAudioTranscription = (value: unknown): Record | null => { const transcriber = getRecord(value); const command = Array.isArray(transcriber?.command) ? transcriber?.command : null; if (!command || command.length === 0) { return null; } if (typeof command[0] !== "string") { return null; } if (!command.every((part) => typeof part === "string")) { return null; } const rawExecutable = command[0].trim(); if (!rawExecutable) { return null; } if (!isSafeExecutableValue(rawExecutable)) { return null; } const args = command.slice(1); const timeoutSeconds = typeof transcriber?.timeoutSeconds === "number" ? transcriber?.timeoutSeconds : undefined; const result: Record = { command: rawExecutable, type: "cli" }; if (args.length > 0) { result.args = args; } if (timeoutSeconds !== undefined) { result.timeoutSeconds = timeoutSeconds; } return result; }; export const getAgentsList = (agents: Record | null) => { const list = agents?.list; return Array.isArray(list) ? list : []; }; export const resolveDefaultAgentIdFromRaw = (raw: Record) => { const agents = getRecord(raw.agents); const list = getAgentsList(agents); const defaultEntry = list.find( (entry): entry is { id: string } => isRecord(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): entry is { id: string } => isRecord(entry) && typeof entry.id === "string" && entry.id.trim() !== "", ); if (firstEntry) { return firstEntry.id.trim(); } return "main"; }; export const ensureAgentEntry = (list: unknown[], id: string): Record => { const normalized = id.trim(); const existing = list.find( (entry): entry is Record => isRecord(entry) && typeof entry.id === "string" && entry.id.trim() === normalized, ); if (existing) { return existing; } const created: Record = { id: normalized }; list.push(created); return created; };