openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,878 lines • 77.4 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js";
import { t as sanitizeForLog } from "./ansi-BI9w76Cm.js";
import { t as CONFIG_PATH } from "./paths-mvMm5bYV.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { c as isRecord$1 } from "./utils-CCC-BEJH.js";
import { i as LEGACY_SECRETREF_ENV_MARKER_PREFIX, p as parseLegacySecretRefEnvMarker } from "./types.secrets-_0JOMGE5.js";
import { i as normalizeProviderId$1 } from "./provider-id-Dq06Bcx6.js";
import { r as normalizeChatChannelId } from "./ids-BVNPk-iM.js";
import { n as normalizeAccountId, r as normalizeOptionalAccountId, t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js";
import { u as normalizeAgentId } from "./session-key-B_NoIfpX.js";
import { i as setPathExistingStrict } from "./path-utils-BBmSOFBL.js";
import { n as discoverConfigSecretTargets } from "./target-registry-0k0oK6Ia.js";
import { L as resolveNormalizedProviderModelMaxTokens } from "./io-Gi7-pyU-.js";
import { u as collectConfiguredModelRefs } from "./channel-env-var-names-B6Cczraa.js";
import { t as splitTrailingAuthProfile } from "./model-ref-profile-BIKs-96s.js";
import { r as getBundledChannelPlugin, s as hasBundledChannelPackageSetupFeature } from "./bundled-DcYUmgzC.js";
import { n as formatConfigIssueLines } from "./issue-format-CwCoGNbt.js";
import { r as normalizeTalkSection } from "./talk-CQIjIARC.js";
import { o as callGateway } from "./call-B5-GYOlf.js";
import { o as runPluginSetupConfigMigrations } from "./setup-registry-HspW94zx.js";
import { n as getLoadedChannelPlugin } from "./registry-MkxNn1Ue.js";
import { i as listRouteBindings } from "./bindings-CI-O7TMQ.js";
import { t as DEFAULT_GOOGLE_API_BASE_URL } from "./google-api-base-url-UBNiBOzj.js";
import { n as isCommonSingleAccountPromotionKey, t as collectSingleAccountPromotionEntries } from "./setup-promotion-keys-Oj7oZvpC.js";
import { n as formatSetExplicitDefaultInstruction, r as formatSetExplicitDefaultToConfiguredInstruction, t as formatChannelAccountsDefaultPath } from "./default-account-warnings-CK3UKJwX.js";
import { t as note } from "./note-BRSJp0UF.js";
import { t as asObjectRecord } from "./object-BsiS9JXh.js";
import { i as noteOpencodeProviderOverrides, n as noteImplicitFallbackClobberWarnings, o as stripUnknownConfigKeys } from "./doctor-config-analysis-BUgmtVDJ.js";
import { a as legacyRuntimeModelAliasRequiresRuntimePolicy, i as migrateLegacyXSearchConfig, o as listLegacyRuntimeModelProviderAliases, r as migrateLegacyWebSearchConfig, s as migrateLegacyRuntimeModelRef } from "./legacy-config-migrations-DpcSqkVJ.js";
import { c as mergeMissing, r as isLegacyModelsAddCodexMetadataModel } from "./legacy-config-migrations.runtime.models-CNjdDapx.js";
import { n as ensureRecord, r as hasOwnKey, t as cloneRecord } from "./legacy-config-record-shared-BGMSeWmL.js";
import { t as runDoctorConfigPreflight } from "./doctor-config-preflight-CBEUIQa-.js";
import { n as applyChannelDoctorCompatibilityMigrations, t as migrateLegacyConfig } from "./legacy-config-migrate-TV9KmmFR.js";
import { t as applyDoctorConfigMutation } from "./config-mutation-state-DO_xkJud.js";
import path from "node:path";
import { isDeepStrictEqual } from "node:util";
//#region src/secrets/legacy-secretref-env-marker.ts
function isLegacySecretRefEnvMarker(value) {
return typeof value === "string" && value.trim().startsWith("secretref-env:");
}
function toCandidate(target, defaults) {
if (!isLegacySecretRefEnvMarker(target.value)) return null;
return {
path: target.path,
pathSegments: target.pathSegments,
value: target.value.trim(),
ref: parseLegacySecretRefEnvMarker(target.value, defaults?.env)
};
}
/**
* Finds legacy env marker strings on registered secret targets without mutating config.
*/
function collectLegacySecretRefEnvMarkerCandidates(config) {
const defaults = config.secrets?.defaults;
return discoverConfigSecretTargets(config).map((target) => toCandidate(target, defaults)).filter((candidate) => candidate !== null);
}
/**
* Converts parseable legacy env marker strings into structured env SecretRef objects.
*/
function migrateLegacySecretRefEnvMarkers(config) {
const candidates = collectLegacySecretRefEnvMarkerCandidates(config).filter((candidate) => candidate.ref !== null);
if (candidates.length === 0) return {
config,
changes: []
};
const next = structuredClone(config);
const changes = [];
for (const candidate of candidates) {
const ref = candidate.ref;
if (!ref) continue;
if (setPathExistingStrict(next, candidate.pathSegments, ref)) changes.push(`Moved ${candidate.path} ${LEGACY_SECRETREF_ENV_MARKER_PREFIX}${ref.id} marker → structured env SecretRef.`);
}
return {
config: next,
changes
};
}
//#endregion
//#region src/channels/plugins/setup-promotion-helpers.ts
function asPromotionSurface(setup) {
return setup && typeof setup === "object" ? setup : null;
}
function getLoadedChannelSetupPromotionSurface(channelKey) {
return asPromotionSurface(getLoadedChannelPlugin(channelKey)?.setup);
}
function getBundledChannelSetupPromotionSurface(channelKey) {
if (!hasBundledChannelPackageSetupFeature(channelKey, "configPromotion")) return null;
return asPromotionSurface(getBundledChannelPlugin(channelKey)?.setup);
}
/**
* Resolves all root-level keys eligible for single-account promotion.
*/
function resolveSingleAccountKeysToMove(params) {
const { entries, hasNamedAccounts } = collectSingleAccountPromotionEntries(params.channel);
if (entries.length === 0) return [];
let loadedSetupSurface;
const resolveLoadedSetupSurface = () => {
loadedSetupSurface ??= getLoadedChannelSetupPromotionSurface(params.channelKey);
return loadedSetupSurface;
};
let bundledSetupSurface;
const resolveBundledSetupSurface = () => {
bundledSetupSurface ??= getBundledChannelSetupPromotionSurface(params.channelKey);
return bundledSetupSurface;
};
const keysToMove = entries.filter((key) => {
if (isCommonSingleAccountPromotionKey(key)) return true;
return Boolean(resolveLoadedSetupSurface()?.singleAccountKeysToMove?.includes(key) || resolveBundledSetupSurface()?.singleAccountKeysToMove?.includes(key));
});
if (!hasNamedAccounts || keysToMove.length === 0) return keysToMove;
const namedAccountPromotionKeys = resolveLoadedSetupSurface()?.namedAccountPromotionKeys ?? resolveBundledSetupSurface()?.namedAccountPromotionKeys;
if (!namedAccountPromotionKeys) return keysToMove;
return keysToMove.filter((key) => namedAccountPromotionKeys.includes(key));
}
//#endregion
//#region src/commands/doctor/shared/legacy-talk-config-normalizer.ts
function buildLegacyTalkProviderCompat(talk) {
const compat = {};
for (const key of [
"voiceId",
"voiceAliases",
"modelId",
"outputFormat",
"apiKey"
]) if (talk[key] !== void 0) compat[key] = talk[key];
return Object.keys(compat).length > 0 ? compat : void 0;
}
function buildLegacyRealtimeTalkCompat(talk, normalizedTalk) {
if (talk.realtime !== void 0) return;
const compat = {};
for (const key of [
"model",
"voice",
"mode",
"transport",
"brain"
]) if (talk[key] !== void 0) compat[key] = talk[key];
if (Object.keys(compat).length === 0) return;
if (normalizedTalk.provider !== void 0) compat.provider = normalizedTalk.provider;
if (normalizedTalk.providers !== void 0) compat.providers = normalizedTalk.providers;
return normalizeTalkSection({ realtime: compat })?.realtime;
}
function removeDerivedRealtimeSpeakerVoice(rawTalk, normalizedTalk) {
const rawRealtime = rawTalk.realtime;
const normalizedRealtime = normalizedTalk.realtime;
if (!isRecord(rawRealtime) || !normalizedRealtime || rawRealtime.speakerVoice !== void 0 || normalizedRealtime.speakerVoice === void 0 || normalizedRealtime.speakerVoice !== normalizedRealtime.voice) return;
delete normalizedRealtime.speakerVoice;
}
/** Normalize legacy Talk provider/realtime fields into current talk.providers and talk.realtime. */
function normalizeLegacyTalkConfig(cfg, changes) {
const rawTalk = cfg.talk;
if (!isRecord(rawTalk)) return cfg;
const normalizedTalk = normalizeTalkSection(rawTalk) ?? {};
const legacyProviderCompat = buildLegacyTalkProviderCompat(rawTalk);
if (legacyProviderCompat) normalizedTalk.providers = {
...normalizedTalk.providers,
elevenlabs: {
...legacyProviderCompat,
...normalizedTalk.providers?.elevenlabs
}
};
const legacyRealtimeCompat = buildLegacyRealtimeTalkCompat(rawTalk, normalizedTalk);
if (legacyRealtimeCompat) normalizedTalk.realtime = {
...legacyRealtimeCompat,
...normalizedTalk.realtime
};
removeDerivedRealtimeSpeakerVoice(rawTalk, normalizedTalk);
if (Object.keys(normalizedTalk).length === 0 || isDeepStrictEqual(normalizedTalk, rawTalk)) return cfg;
changes.push("Normalized talk.provider/providers shape (trimmed provider ids and merged missing compatibility fields).");
if (legacyRealtimeCompat) changes.push("Moved legacy realtime Talk provider/model fields into talk.realtime.");
return {
...cfg,
talk: normalizedTalk
};
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-core-normalizers.ts
/** Remove deprecated command config keys that no runtime reads anymore. */
function normalizeLegacyCommandsConfig(cfg, changes) {
const rawCommands = cfg.commands;
if (!isRecord$1(rawCommands) || !("modelsWrite" in rawCommands)) return cfg;
const commands = { ...rawCommands };
delete commands.modelsWrite;
changes.push("Removed deprecated commands.modelsWrite (/models add is deprecated).");
return {
...cfg,
commands
};
}
/** Migrate legacy browser/Chrome relay config to current browser profile settings. */
function normalizeLegacyBrowserConfig(cfg, changes) {
const rawBrowser = cfg.browser;
if (!isRecord$1(rawBrowser)) return cfg;
const browser = structuredClone(rawBrowser);
let browserChanged = false;
if ("relayBindHost" in browser) {
delete browser.relayBindHost;
browserChanged = true;
changes.push("Removed browser.relayBindHost (legacy Chrome extension relay setting; host-local Chrome now uses Chrome MCP existing-session attach).");
}
const rawProfiles = browser.profiles;
if (isRecord$1(rawProfiles)) {
const profiles = { ...rawProfiles };
let profilesChanged = false;
for (const [profileName, rawProfile] of Object.entries(rawProfiles)) {
if (!isRecord$1(rawProfile)) continue;
if ((normalizeOptionalString(rawProfile.driver) ?? "") !== "extension") continue;
profiles[profileName] = {
...rawProfile,
driver: "existing-session"
};
profilesChanged = true;
changes.push(`Moved browser.profiles.${profileName}.driver "extension" → "existing-session" (Chrome MCP attach).`);
}
if (profilesChanged) {
browser.profiles = profiles;
browserChanged = true;
}
}
const rawSsrFPolicy = browser.ssrfPolicy;
if (isRecord$1(rawSsrFPolicy) && "allowPrivateNetwork" in rawSsrFPolicy) {
const legacyAllowPrivateNetwork = rawSsrFPolicy.allowPrivateNetwork;
const currentDangerousAllowPrivateNetwork = rawSsrFPolicy.dangerouslyAllowPrivateNetwork;
let resolvedDangerousAllowPrivateNetwork = currentDangerousAllowPrivateNetwork;
if (typeof legacyAllowPrivateNetwork === "boolean" || typeof currentDangerousAllowPrivateNetwork === "boolean") resolvedDangerousAllowPrivateNetwork = legacyAllowPrivateNetwork === true || currentDangerousAllowPrivateNetwork === true;
else if (currentDangerousAllowPrivateNetwork === void 0) resolvedDangerousAllowPrivateNetwork = legacyAllowPrivateNetwork;
const nextSsrFPolicy = { ...rawSsrFPolicy };
delete nextSsrFPolicy.allowPrivateNetwork;
if (resolvedDangerousAllowPrivateNetwork !== void 0) nextSsrFPolicy.dangerouslyAllowPrivateNetwork = resolvedDangerousAllowPrivateNetwork;
browser.ssrfPolicy = nextSsrFPolicy;
browserChanged = true;
changes.push(`Moved browser.ssrfPolicy.allowPrivateNetwork → browser.ssrfPolicy.dangerouslyAllowPrivateNetwork (${String(resolvedDangerousAllowPrivateNetwork)}).`);
}
if (!browserChanged) return cfg;
return {
...cfg,
browser
};
}
/** Move single-account channel fields into accounts.default when account maps exist. */
function seedMissingDefaultAccountsFromSingleAccountBase(cfg, changes) {
const channels = cfg.channels;
if (!channels) return cfg;
let channelsChanged = false;
const nextChannels = { ...channels };
for (const [channelId, rawChannel] of Object.entries(channels)) {
if (!isRecord$1(rawChannel)) continue;
const rawAccounts = rawChannel.accounts;
if (!isRecord$1(rawAccounts)) continue;
const accountKeys = Object.keys(rawAccounts);
if (accountKeys.length === 0) continue;
if (accountKeys.some((key) => normalizeOptionalLowercaseString(key) === "default")) continue;
const keysToMove = resolveSingleAccountKeysToMove({
channelKey: channelId,
channel: rawChannel
});
if (keysToMove.length === 0) continue;
const defaultAccount = {};
for (const key of keysToMove) {
const value = rawChannel[key];
defaultAccount[key] = value && typeof value === "object" ? structuredClone(value) : value;
}
const nextChannel = { ...rawChannel };
for (const key of keysToMove) delete nextChannel[key];
nextChannel.accounts = {
...rawAccounts,
[DEFAULT_ACCOUNT_ID]: defaultAccount
};
nextChannels[channelId] = nextChannel;
channelsChanged = true;
changes.push(`Moved channels.${channelId} single-account top-level values into channels.${channelId}.accounts.default.`);
}
if (!channelsChanged) return cfg;
return {
...cfg,
channels: nextChannels
};
}
const LEGACY_CODEX_CLI_RUNTIME_ID = "codex-cli";
const CODEX_APP_SERVER_RUNTIME_ID = "codex";
function resolveLegacyWholeAgentRuntimePolicy(raw) {
if (!isRecord$1(raw)) return;
const runtime = normalizeOptionalLowercaseString(raw.id);
if (!runtime || runtime === "auto" || runtime === "openclaw") return;
const alias = listLegacyRuntimeModelProviderAliases().find((entry) => entry.cli && normalizeProviderId$1(entry.runtime) === runtime);
return alias ? {
provider: alias.provider,
runtime: alias.runtime,
requiresRuntimePolicy: alias.requiresRuntimePolicy
} : void 0;
}
function migratedRuntimeRequiresPolicy(legacyProvider) {
return legacyRuntimeModelAliasRequiresRuntimePolicy(legacyProvider);
}
function mergeModelEntry(legacyEntry, currentEntry) {
if (!isRecord$1(legacyEntry) || !isRecord$1(currentEntry)) return currentEntry ?? legacyEntry;
return {
...legacyEntry,
...currentEntry
};
}
function normalizeLegacyCodexCliAgentRuntimePolicy(raw) {
if (!isRecord$1(raw)) return {
value: raw,
changed: false
};
if (normalizeOptionalLowercaseString(raw.id) !== LEGACY_CODEX_CLI_RUNTIME_ID) return {
value: raw,
changed: false
};
return {
value: {
...raw,
id: CODEX_APP_SERVER_RUNTIME_ID
},
changed: true
};
}
function normalizeLegacyRuntimeAgentModelConfig(raw) {
if (typeof raw === "string") {
const migrated = migrateLegacyRuntimeModelRef(raw);
return migrated ? {
value: migrated.ref,
changed: true,
selectedRuntime: migrated.runtime,
selectedRuntimeRequiresPolicy: migratedRuntimeRequiresPolicy(migrated.legacyProvider),
selectedRefs: [{
ref: migrated.ref,
runtime: migrated.runtime,
requiresRuntimePolicy: migratedRuntimeRequiresPolicy(migrated.legacyProvider)
}]
} : {
value: raw,
changed: false,
selectedRuntimeRequiresPolicy: false,
selectedRefs: []
};
}
if (!isRecord$1(raw)) return {
value: raw,
changed: false,
selectedRuntimeRequiresPolicy: false,
selectedRefs: []
};
const migratedPrimary = typeof raw.primary === "string" ? migrateLegacyRuntimeModelRef(raw.primary) : null;
let changed = false;
const next = { ...raw };
const selectedRefs = [];
let selectedRuntime = migratedPrimary?.runtime;
let selectedRuntimeRequiresPolicy = migratedPrimary !== null && migratedRuntimeRequiresPolicy(migratedPrimary.legacyProvider);
if (migratedPrimary) {
next.primary = migratedPrimary.ref;
selectedRefs.push({
ref: migratedPrimary.ref,
runtime: migratedPrimary.runtime,
requiresRuntimePolicy: migratedRuntimeRequiresPolicy(migratedPrimary.legacyProvider)
});
changed = true;
}
if (Array.isArray(raw.fallbacks)) next.fallbacks = raw.fallbacks.map((fallback) => {
if (typeof fallback !== "string") return fallback;
const migratedFallback = migrateLegacyRuntimeModelRef(fallback);
if (migratedFallback && (migratedFallback.runtime === selectedRuntime || migratedFallback.legacyProvider === LEGACY_CODEX_CLI_RUNTIME_ID)) {
selectedRuntime ??= migratedFallback.runtime;
selectedRuntimeRequiresPolicy ||= migratedRuntimeRequiresPolicy(migratedFallback.legacyProvider);
selectedRefs.push({
ref: migratedFallback.ref,
runtime: migratedFallback.runtime,
requiresRuntimePolicy: migratedRuntimeRequiresPolicy(migratedFallback.legacyProvider)
});
changed = true;
return migratedFallback.ref;
}
return fallback;
});
if (!changed) return {
value: raw,
changed: false,
selectedRuntimeRequiresPolicy: false,
selectedRefs: []
};
return {
value: next,
changed: true,
selectedRuntime,
selectedRuntimeRequiresPolicy,
selectedRefs
};
}
function runtimeNeedsExplicitModelPolicy(runtime) {
return Boolean(runtime && runtime !== "codex");
}
function modelEntryWithRuntimePolicy(entry, runtime) {
const base = isRecord$1(entry) ? { ...entry } : {};
const currentRuntime = isRecord$1(base.agentRuntime) ? normalizeOptionalLowercaseString(base.agentRuntime.id) : void 0;
if (!currentRuntime || currentRuntime === "auto") base.agentRuntime = {
...isRecord$1(base.agentRuntime) ? base.agentRuntime : {},
id: runtime
};
return base;
}
function mergeModelEntryWithRuntimePolicy(legacyEntry, currentEntry, runtime, requiresRuntimePolicy = runtimeNeedsExplicitModelPolicy(runtime)) {
const merged = mergeModelEntry(legacyEntry, currentEntry);
return runtime && requiresRuntimePolicy ? modelEntryWithRuntimePolicy(merged, runtime) : merged;
}
function normalizeLegacyRuntimeAllowlistModels(rawModels, selectedRuntime, selectedRuntimeRequiresPolicy) {
if (!isRecord$1(rawModels)) return {
value: rawModels,
changed: false
};
let changed = false;
const next = {};
const legacyEntries = [];
for (const [rawKey, entry] of Object.entries(rawModels)) {
const migrated = migrateLegacyRuntimeModelRef(rawKey);
if (migrated && (migrated.runtime === selectedRuntime || migrated.legacyProvider === LEGACY_CODEX_CLI_RUNTIME_ID)) {
changed = true;
next[rawKey] = mergeModelEntry(entry, next[rawKey]);
legacyEntries.push({
migratedKey: migrated.ref,
entry,
runtime: migrated.runtime,
requiresRuntimePolicy: migratedRuntimeRequiresPolicy(migrated.legacyProvider)
});
continue;
}
next[rawKey] = mergeModelEntry(entry, next[rawKey]);
}
for (const { migratedKey, entry, runtime, requiresRuntimePolicy } of legacyEntries) next[migratedKey] = mergeModelEntryWithRuntimePolicy(entry, next[migratedKey], runtime, requiresRuntimePolicy || runtime === selectedRuntime && selectedRuntimeRequiresPolicy);
return {
value: next,
changed
};
}
function ensureSelectedModelRuntimePolicies(rawModels, selectedRefs) {
if (selectedRefs.length === 0) return {
value: rawModels,
changed: false
};
const next = isRecord$1(rawModels) ? { ...rawModels } : {};
let changed = false;
for (const { ref, runtime, requiresRuntimePolicy } of selectedRefs) {
if (!requiresRuntimePolicy) continue;
const current = next[ref];
const updated = modelEntryWithRuntimePolicy(current, runtime);
if (JSON.stringify(updated) !== JSON.stringify(current ?? {})) {
next[ref] = updated;
changed = true;
}
}
return {
value: next,
changed
};
}
function selectedCanonicalModelRefsForRuntimePolicy(rawModel, provider, runtime, requiresRuntimePolicy) {
const refs = [];
const addRef = (rawRef) => {
if (typeof rawRef !== "string") return;
const trimmed = rawRef.trim();
const slash = trimmed.indexOf("/");
if (slash <= 0 || slash >= trimmed.length - 1) return;
if (normalizeProviderId$1(trimmed.slice(0, slash)) !== normalizeProviderId$1(provider)) return;
refs.push({
ref: trimmed,
runtime,
requiresRuntimePolicy
});
};
if (typeof rawModel === "string") {
addRef(rawModel);
return refs;
}
if (!isRecord$1(rawModel)) return refs;
addRef(rawModel.primary);
if (Array.isArray(rawModel.fallbacks)) for (const fallback of rawModel.fallbacks) addRef(fallback);
return refs;
}
function normalizeLegacyCodexCliRuntimePinsInModels(rawModels, path, changes) {
if (!isRecord$1(rawModels)) return {
value: rawModels,
changed: false
};
let changed = false;
const next = { ...rawModels };
for (const [modelRef, rawEntry] of Object.entries(rawModels)) {
if (!isRecord$1(rawEntry)) continue;
const runtime = normalizeLegacyCodexCliAgentRuntimePolicy(rawEntry.agentRuntime);
if (!runtime.changed) continue;
next[modelRef] = {
...rawEntry,
agentRuntime: runtime.value
};
changed = true;
changes.push(`Moved ${path}.${sanitizeForLog(modelRef)} agentRuntime.id from codex-cli to codex.`);
}
return {
value: next,
changed
};
}
function normalizeLegacyRuntimeAgentContainer(raw, path, changes) {
let changed = false;
const next = { ...raw };
const legacyWholeAgentRuntime = resolveLegacyWholeAgentRuntimePolicy(raw.agentRuntime);
const model = normalizeLegacyRuntimeAgentModelConfig(raw.model);
if (model.changed) {
next.model = model.value;
changed = true;
const runtimeSuffix = model.selectedRuntime ? ` and selected ${model.selectedRuntime} runtime` : "";
changes.push(`Moved ${path}.model legacy runtime primary refs to canonical provider refs${runtimeSuffix}.`);
}
const models = normalizeLegacyRuntimeAllowlistModels(raw.models, model.selectedRuntime, model.selectedRuntimeRequiresPolicy);
if (models.changed) {
next.models = models.value;
changed = true;
changes.push(`Moved ${path}.models legacy runtime keys to canonical provider keys.`);
}
if (model.selectedRuntime) {
const modelRuntimes = ensureSelectedModelRuntimePolicies(next.models, model.selectedRefs);
if (modelRuntimes.changed) {
next.models = modelRuntimes.value;
changed = true;
changes.push(`Selected ${model.selectedRuntime} runtime for ${path}.models entries.`);
}
}
if (legacyWholeAgentRuntime) {
const selectedRefs = selectedCanonicalModelRefsForRuntimePolicy(next.model ?? raw.model, legacyWholeAgentRuntime.provider, legacyWholeAgentRuntime.runtime, legacyWholeAgentRuntime.requiresRuntimePolicy);
const modelRuntimes = ensureSelectedModelRuntimePolicies(next.models, selectedRefs);
if (modelRuntimes.changed) {
next.models = modelRuntimes.value;
changed = true;
changes.push(`Moved ${path}.agentRuntime.id ${legacyWholeAgentRuntime.runtime} to matching ${legacyWholeAgentRuntime.provider} model runtime policy.`);
}
}
const codexCliRuntimePins = normalizeLegacyCodexCliRuntimePinsInModels(next.models, `${path}.models`, changes);
if (codexCliRuntimePins.changed) {
next.models = codexCliRuntimePins.value;
changed = true;
}
return {
value: next,
changed
};
}
function normalizeLegacyCodexCliProviderRuntimePins(cfg, changes) {
const rawModels = cfg.models;
if (!isRecord$1(rawModels) || !isRecord$1(rawModels.providers)) return {
config: cfg,
changed: false
};
let changed = false;
const nextProviders = { ...rawModels.providers };
for (const [providerId, rawProvider] of Object.entries(rawModels.providers)) {
if (!isRecord$1(rawProvider)) continue;
let providerChanged = false;
const nextProvider = { ...rawProvider };
const providerRuntime = normalizeLegacyCodexCliAgentRuntimePolicy(rawProvider.agentRuntime);
if (providerRuntime.changed) {
nextProvider.agentRuntime = providerRuntime.value;
providerChanged = true;
changes.push(`Moved models.providers.${sanitizeForLog(providerId)} agentRuntime.id from codex-cli to codex.`);
}
if (Array.isArray(rawProvider.models)) {
const nextProviderModels = rawProvider.models.map((entry, index) => {
if (!isRecord$1(entry)) return entry;
const runtime = normalizeLegacyCodexCliAgentRuntimePolicy(entry.agentRuntime);
if (!runtime.changed) return entry;
providerChanged = true;
const modelId = normalizeOptionalString(entry.id) ?? `[${index}]`;
changes.push(`Moved models.providers.${sanitizeForLog(providerId)}.models.${sanitizeForLog(modelId)} agentRuntime.id from codex-cli to codex.`);
return Object.assign({}, entry, { agentRuntime: runtime.value });
});
if (providerChanged) nextProvider.models = nextProviderModels;
}
if (providerChanged) {
nextProviders[providerId] = nextProvider;
changed = true;
}
}
return changed ? {
config: {
...cfg,
models: {
...rawModels,
providers: nextProviders
}
},
changed: true
} : {
config: cfg,
changed: false
};
}
/** Move legacy runtime-tagged model/provider refs onto current agentRuntime policy fields. */
function normalizeLegacyRuntimeModelRefs(cfg, changes) {
const cfgWithProviders = normalizeLegacyCodexCliProviderRuntimePins(cfg, changes).config;
const rawAgents = cfgWithProviders.agents;
if (!isRecord$1(rawAgents)) return cfgWithProviders;
let changed = false;
const nextAgents = { ...rawAgents };
if (isRecord$1(rawAgents.defaults)) {
const defaults = normalizeLegacyRuntimeAgentContainer(rawAgents.defaults, "agents.defaults", changes);
if (defaults.changed) {
nextAgents.defaults = defaults.value;
changed = true;
}
}
if (Array.isArray(rawAgents.list)) {
const nextList = rawAgents.list.map((entry, index) => {
if (!isRecord$1(entry)) return entry;
const agentId = normalizeOptionalString(entry.id);
const agent = normalizeLegacyRuntimeAgentContainer(entry, agentId ? `agents.list.${sanitizeForLog(agentId)}` : `agents.list[${index}]`, changes);
if (agent.changed) {
changed = true;
return agent.value;
}
return entry;
});
if (changed) nextAgents.list = nextList;
}
return changed ? {
...cfgWithProviders,
agents: nextAgents
} : cfgWithProviders;
}
/** Add missing metadata source markers to legacy OpenAI Codex model catalog entries. */
function normalizeLegacyOpenAICodexModelsAddMetadata(cfg, changes) {
const rawModels = cfg.models;
if (!isRecord$1(rawModels) || !isRecord$1(rawModels.providers)) return cfg;
const rawProviders = rawModels.providers;
let providersChanged = false;
const nextProviders = { ...rawProviders };
for (const [providerId, rawProvider] of Object.entries(rawProviders)) {
if (normalizeProviderId$1(providerId) !== "openai-codex" || !isRecord$1(rawProvider)) continue;
const rawProviderModels = rawProvider.models;
if (!Array.isArray(rawProviderModels)) continue;
let providerChanged = false;
const nextModels = [];
for (const model of rawProviderModels) if (isRecord$1(model) && !("metadataSource" in model) && isLegacyModelsAddCodexMetadataModel({
provider: providerId,
model
})) {
providerChanged = true;
const safeProviderId = sanitizeForLog(providerId);
const safeModelId = sanitizeForLog(normalizeOptionalString(model.id) ?? "unknown");
changes.push(`Marked models.providers.${safeProviderId}.models.${safeModelId} as /models add metadata so official OpenAI Codex metadata can override it.`);
nextModels.push(Object.assign({}, model, { metadataSource: "models-add" }));
} else nextModels.push(model);
if (!providerChanged) continue;
nextProviders[providerId] = {
...rawProvider,
models: nextModels
};
providersChanged = true;
}
if (!providersChanged) return cfg;
return {
...cfg,
models: {
...rawModels,
providers: nextProviders
}
};
}
/** Rename legacy OpenAI API identifiers to the current completion/chat API ids. */
function normalizeLegacyOpenAIModelProviderApi(cfg, changes) {
const rawModels = cfg.models;
if (!isRecord$1(rawModels) || !isRecord$1(rawModels.providers)) return cfg;
const rawProviders = rawModels.providers;
let providersChanged = false;
const nextProviders = { ...rawProviders };
for (const [providerId, rawProvider] of Object.entries(rawProviders)) {
if (!isRecord$1(rawProvider)) continue;
let providerChanged = false;
const nextProvider = { ...rawProvider };
if (nextProvider.api === "openai") {
nextProvider.api = "openai-completions";
providerChanged = true;
changes.push(`Moved models.providers.${sanitizeForLog(providerId)}.api "openai" → "openai-completions".`);
}
const rawProviderModels = rawProvider.models;
if (Array.isArray(rawProviderModels)) {
let modelsChanged = false;
const nextModels = [];
rawProviderModels.forEach((model, index) => {
if (!isRecord$1(model) || model.api !== "openai") {
nextModels.push(model);
return;
}
modelsChanged = true;
changes.push(`Moved models.providers.${sanitizeForLog(providerId)}.models[${index}].api "openai" → "openai-completions".`);
nextModels.push({
...model,
api: "openai-completions"
});
});
if (modelsChanged) {
nextProvider.models = nextModels;
providerChanged = true;
}
}
if (!providerChanged) continue;
nextProviders[providerId] = nextProvider;
providersChanged = true;
}
if (!providersChanged) return cfg;
return {
...cfg,
models: {
...rawModels,
providers: nextProviders
}
};
}
/** Remove retired bundled nano-banana skill config after migrating image generation models. */
function normalizeLegacyNanoBananaSkill(cfg, changes) {
const NANO_BANANA_SKILL_KEY = "nano-banana-pro";
const NANO_BANANA_MODEL = "google/gemini-3-pro-image-preview";
const rawSkills = cfg.skills;
if (!isRecord$1(rawSkills)) return cfg;
let next = cfg;
let skillsChanged = false;
const skills = structuredClone(rawSkills);
if (Array.isArray(skills.allowBundled)) {
const allowBundled = skills.allowBundled.filter((value) => typeof value !== "string" || value.trim() !== NANO_BANANA_SKILL_KEY);
if (allowBundled.length !== skills.allowBundled.length) {
if (allowBundled.length === 0) {
delete skills.allowBundled;
changes.push(`Removed skills.allowBundled entry for ${NANO_BANANA_SKILL_KEY}.`);
} else {
skills.allowBundled = allowBundled;
changes.push(`Removed ${NANO_BANANA_SKILL_KEY} from skills.allowBundled.`);
}
skillsChanged = true;
}
}
const rawEntries = skills.entries;
if (!isRecord$1(rawEntries)) {
if (!skillsChanged) return cfg;
return {
...cfg,
skills
};
}
const rawLegacyEntry = rawEntries[NANO_BANANA_SKILL_KEY];
if (!isRecord$1(rawLegacyEntry)) {
if (!skillsChanged) return cfg;
return {
...cfg,
skills
};
}
if (next.agents?.defaults?.imageGenerationModel === void 0) {
next = {
...next,
agents: {
...next.agents,
defaults: {
...next.agents?.defaults,
imageGenerationModel: { primary: NANO_BANANA_MODEL }
}
}
};
changes.push(`Moved skills.entries.${NANO_BANANA_SKILL_KEY} → agents.defaults.imageGenerationModel.primary (${NANO_BANANA_MODEL}).`);
}
const legacyEnvApiKey = normalizeOptionalString((isRecord$1(rawLegacyEntry.env) ? rawLegacyEntry.env : void 0)?.GEMINI_API_KEY) ?? "";
const legacyApiKey = legacyEnvApiKey || (typeof rawLegacyEntry.apiKey === "string" ? normalizeOptionalString(rawLegacyEntry.apiKey) : rawLegacyEntry.apiKey && isRecord$1(rawLegacyEntry.apiKey) ? structuredClone(rawLegacyEntry.apiKey) : void 0);
const rawModels = isRecord$1(next.models) ? structuredClone(next.models) : {};
const rawProviders = isRecord$1(rawModels.providers) ? { ...rawModels.providers } : {};
const rawGoogle = isRecord$1(rawProviders.google) ? { ...rawProviders.google } : {};
if (!(rawGoogle.apiKey !== void 0) && legacyApiKey) {
rawGoogle.apiKey = legacyApiKey;
if (!rawGoogle.baseUrl) rawGoogle.baseUrl = DEFAULT_GOOGLE_API_BASE_URL;
if (!Array.isArray(rawGoogle.models)) rawGoogle.models = [];
rawProviders.google = rawGoogle;
rawModels.providers = rawProviders;
next = {
...next,
models: rawModels
};
changes.push(`Moved skills.entries.${NANO_BANANA_SKILL_KEY}.${legacyEnvApiKey ? "env.GEMINI_API_KEY" : "apiKey"} → models.providers.google.apiKey.`);
}
const entries = { ...rawEntries };
delete entries[NANO_BANANA_SKILL_KEY];
if (Object.keys(entries).length === 0) delete skills.entries;
else skills.entries = entries;
changes.push(`Removed legacy skills.entries.${NANO_BANANA_SKILL_KEY}.`);
skillsChanged = true;
if (Object.keys(skills).length === 0) {
const { skills: _ignored, ...rest } = next;
return rest;
}
if (!skillsChanged) return next;
return {
...next,
skills
};
}
/** Move legacy cross-context send boolean into explicit message crossContext policy. */
function normalizeLegacyCrossContextMessageConfig(cfg, changes) {
const rawTools = cfg.tools;
if (!isRecord$1(rawTools)) return cfg;
const rawMessage = rawTools.message;
if (!isRecord$1(rawMessage) || !("allowCrossContextSend" in rawMessage)) return cfg;
const legacyAllowCrossContextSend = rawMessage.allowCrossContextSend;
if (typeof legacyAllowCrossContextSend !== "boolean") return cfg;
const nextMessage = { ...rawMessage };
delete nextMessage.allowCrossContextSend;
if (legacyAllowCrossContextSend) {
const rawCrossContext = isRecord$1(nextMessage.crossContext) ? structuredClone(nextMessage.crossContext) : {};
rawCrossContext.allowWithinProvider = true;
rawCrossContext.allowAcrossProviders = true;
nextMessage.crossContext = rawCrossContext;
changes.push("Moved tools.message.allowCrossContextSend → tools.message.crossContext.allowWithinProvider/allowAcrossProviders (true).");
} else changes.push("Removed tools.message.allowCrossContextSend=false (default cross-context policy already matches canonical settings).");
return {
...cfg,
tools: {
...cfg.tools,
message: nextMessage
}
};
}
function mapDeepgramCompatToProviderOptions(rawCompat) {
const providerOptions = {};
if (typeof rawCompat.detectLanguage === "boolean") providerOptions.detect_language = rawCompat.detectLanguage;
if (typeof rawCompat.punctuate === "boolean") providerOptions.punctuate = rawCompat.punctuate;
if (typeof rawCompat.smartFormat === "boolean") providerOptions.smart_format = rawCompat.smartFormat;
return providerOptions;
}
function migrateLegacyDeepgramCompat(params) {
const rawCompat = isRecord$1(params.owner.deepgram) ? structuredClone(params.owner.deepgram) : null;
if (!rawCompat) return false;
const compatProviderOptions = mapDeepgramCompatToProviderOptions(rawCompat);
const currentProviderOptions = isRecord$1(params.owner.providerOptions) ? structuredClone(params.owner.providerOptions) : {};
const currentDeepgram = isRecord$1(currentProviderOptions.deepgram) ? structuredClone(currentProviderOptions.deepgram) : {};
const mergedDeepgram = {
...compatProviderOptions,
...currentDeepgram
};
delete params.owner.deepgram;
currentProviderOptions.deepgram = mergedDeepgram;
params.owner.providerOptions = currentProviderOptions;
const hadCanonicalDeepgram = Object.keys(currentDeepgram).length > 0;
params.changes.push(hadCanonicalDeepgram ? `Merged ${params.pathPrefix}.deepgram → ${params.pathPrefix}.providerOptions.deepgram (filled missing canonical fields from legacy).` : `Moved ${params.pathPrefix}.deepgram → ${params.pathPrefix}.providerOptions.deepgram.`);
return true;
}
/** Move legacy media provider option aliases into providerOptions maps. */
function normalizeLegacyMediaProviderOptions(cfg, changes) {
const rawTools = cfg.tools;
if (!isRecord$1(rawTools)) return cfg;
const rawMedia = rawTools.media;
if (!isRecord$1(rawMedia)) return cfg;
let mediaChanged = false;
const nextMedia = structuredClone(rawMedia);
const migrateModelList = (models, pathPrefix) => {
if (!Array.isArray(models)) return false;
let changedAny = false;
for (const [index, entry] of models.entries()) {
if (!isRecord$1(entry)) continue;
if (migrateLegacyDeepgramCompat({
owner: entry,
pathPrefix: `${pathPrefix}[${index}]`,
changes
})) changedAny = true;
}
return changedAny;
};
for (const capability of [
"audio",
"image",
"video"
]) {
const config = isRecord$1(nextMedia[capability]) ? structuredClone(nextMedia[capability]) : null;
if (!config) continue;
let configChanged = false;
if (migrateLegacyDeepgramCompat({
owner: config,
pathPrefix: `tools.media.${capability}`,
changes
})) configChanged = true;
if (migrateModelList(config.models, `tools.media.${capability}.models`)) configChanged = true;
if (configChanged) {
nextMedia[capability] = config;
mediaChanged = true;
}
}
if (migrateModelList(nextMedia.models, "tools.media.models")) mediaChanged = true;
if (!mediaChanged) return cfg;
return {
...cfg,
tools: {
...cfg.tools,
media: nextMedia
}
};
}
function normalizeConfiguredPositiveInteger(value) {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
return Math.floor(value);
}
function resolveConfiguredOllamaModelNumCtxBudget(params) {
const modelContextWindow = normalizeConfiguredPositiveInteger(params.model.contextWindow);
if (modelContextWindow !== void 0) return modelContextWindow;
const providerContextWindow = normalizeConfiguredPositiveInteger(params.provider.contextWindow);
if (providerContextWindow !== void 0) return params.providerNumCtxApplies ? void 0 : providerContextWindow;
const modelMaxTokens = normalizeConfiguredPositiveInteger(params.model.maxTokens);
if (modelMaxTokens !== void 0) return modelMaxTokens;
const providerMaxTokens = normalizeConfiguredPositiveInteger(params.provider.maxTokens);
if (providerMaxTokens !== void 0) return params.providerNumCtxApplies ? void 0 : providerMaxTokens;
}
function resolveConfiguredOllamaProviderNumCtxBudget(provider) {
return normalizeConfiguredPositiveInteger(provider.contextWindow) ?? normalizeConfiguredPositiveInteger(provider.maxTokens);
}
function isNativeOllamaProviderConfig(_providerId, provider) {
return normalizeOptionalLowercaseString(provider.api) === "ollama";
}
function isNativeOllamaModelConfig(params) {
const modelApi = normalizeOptionalLowercaseString(params.model.api);
if (modelApi) return modelApi === "ollama";
const providerApi = normalizeOptionalLowercaseString(params.provider.api);
if (providerApi) return providerApi === "ollama";
return false;
}
function hasConfiguredOllamaProviderNumCtx(provider) {
const rawParams = provider.params;
return isRecord$1(rawParams) && hasOwnKey(rawParams, "num_ctx");
}
function applyLegacyOllamaProviderNumCtxParams(params) {
if (!isNativeOllamaProviderConfig(params.providerId, params.provider)) return {
provider: params.provider,
changed: false
};
const rawParams = params.provider.params;
if (rawParams !== void 0 && !isRecord$1(rawParams)) return {
provider: params.provider,
changed: false
};
if (rawParams && hasOwnKey(rawParams, "num_ctx")) return {
provider: params.provider,
changed: false
};
const numCtx = resolveConfiguredOllamaProviderNumCtxBudget(params.provider);
if (numCtx === void 0) return {
provider: params.provider,
changed: false
};
params.changes.push(`Set models.providers.${sanitizeForLog(params.providerId)}.params.num_ctx to ${numCtx} for native Ollama compatibility.`);
return {
provider: {
...params.provider,
params: rawParams ? {
...rawParams,
num_ctx: numCtx
} : { num_ctx: numCtx }
},
changed: true
};
}
/** Seed native Ollama num_ctx params from legacy context-token budgets. */
function normalizeLegacyOllamaNativeNumCtxParams(cfg, changes) {
const rawProviders = cfg.models?.providers;
if (!isRecord$1(rawProviders)) return cfg;
let providersChanged = false;
const nextProviders = { ...rawProviders };
for (const [providerId, rawProvider] of Object.entries(rawProviders)) {
if (!isRecord$1(rawProvider)) continue;
const rawModels = rawProvider.models;
if (!Array.isArray(rawModels)) continue;
const providerParams = applyLegacyOllamaProviderNumCtxParams({
providerId,
provider: rawProvider,
changes
});
const providerNumCtxApplies = isNativeOllamaProviderConfig(providerId, providerParams.provider) && hasConfiguredOllamaProviderNumCtx(providerParams.provider);
if (rawModels.length === 0) {
if (!providerParams.changed) continue;
nextProviders[providerId] = providerParams.provider;
providersChanged = true;
continue;
}
let modelsChanged = false;
const nextModels = rawModels.map((model, index) => {
if (!isRecord$1(model)) return model;
if (!isNativeOllamaModelConfig({
providerId,
provider: providerParams.provider,
model
})) return model;
const rawParams = model.params;
if (rawParams !== void 0 && !isRecord$1(rawParams)) return model;
if (rawParams && hasOwnKey(rawParams, "num_ctx")) return model;
const numCtx = resolveConfiguredOllamaModelNumCtxBudget({
model,
provider: providerParams.provider,
providerNumCtxApplies
});
if (numCtx === void 0) return model;
modelsChanged = true;
changes.push(`Set models.providers.${sanitizeForLog(providerId)}.models[${index}].params.num_ctx to ${numCtx} for native Ollama compatibility.`);
return Object.assign({}, model, { params: rawParams ? {
...rawParams,
num_ctx: numCtx
} : { num_ctx: numCtx } });
});
if (!modelsChanged && !providerParams.changed) continue;
nextProviders[providerId] = {
...providerParams.provider,
models: nextModels
};
providersChanged = true;
}
if (!providersChanged) return cfg;
return {
...cfg,
models: {
...cfg.models,
providers: nextProviders
}
};
}
const MISTRAL_MODEL_CACHE_READ_COST_BY_ID = {
"codestral-latest": .03,
"devstral-medium-latest": .04,
"magistral-small": .05,
"mistral-large-latest": .05,
"mistral-medium-2508": .04,
"mistral-medium-3-5": .15,
"mistral-small-latest": .01,
"pixtral-large-latest": .2
};
function normalizeLegacyMistralModelCost(params) {
const cost = params.model.cost;
if (!isRecord$1(cost) || cost.cacheRead !== 0) return {
model: params.model,
changed: false
};
const normalizedCacheRead = MISTRAL_MODEL_CACHE_READ_COST_BY_ID[params.modelId.toLowerCase()];
if (normalizedCacheRead === void 0) return {
model: params.model,
changed: false
};
params.changes.push(`Normalized models.providers.${sanitizeForLog(params.providerId)}.models[${params.index}].cost.cacheRead (0 → ${normalizedCacheRead}) for Mistral prompt-cache billing.`);
return {
model: {
...params.model,
cost: {
...cost,
cacheRead: normalizedCacheRead
}
},
changed: true
};
}
/** Normalize stale Mistral model defaults such as prompt-cache read cost. */
function normalizeLegacyMistralModelDefaults(cfg, changes) {
const rawProviders = cfg.models?.providers;
if (!isRecord$1(rawProviders)) return cfg;
let providersChanged = false;
const nextProviders = { ...rawProviders };
for (const [providerId, rawProvider] of Object.entries(rawProviders)) {
if (normalizeProviderId$1(providerId) !== "mistral" || !isRecord$1(rawProvider)) continue;
const rawModels = rawProvider.models;
if (!Array.isArray(rawModels)) continue;
let modelsChanged = false;
const nextModels = rawModels.map((model, index) => {
if (!isRecord$1(model)) return model;
const modelId = normalizeOptionalString(model.id) ?? "";
if (!modelId) return model;
let nextModel = model;
let modelChanged = false;
const contextWindow = typeof model.contextWindow === "number" && Number.isFinite(model.contextWindow) ? model.contextWindow : null;
const maxTokens = typeof model.maxTokens === "number" && Number.isFinite(model.maxTokens) ? model.maxTokens : null;
if (contextWindow !== null && maxTokens !== null) {
const normalizedMaxTokens = resolveNormalizedProviderModelMaxTokens({
providerId,
modelId,
contextWindow,
rawMaxTokens: maxTokens
});
if (normalizedMaxTokens !== maxTokens) {
nextModel = Object.assign({}, nextModel, { maxTokens: normalizedMaxTokens });
modelChanged = true;
changes.push(`Normalized models.providers.${providerId}.models[${index}].maxTokens (${maxTokens} → ${normalizedMaxTokens}) to avoid Mistral context-window rejects.`);
}
}
const costNormalization = normalizeLegacyMistralModelCost({
providerId,
model: nextModel,
modelId,
index,
changes
});
if (costNormalization.changed) {
nextModel = costNormalization.model;
modelChanged = true;
}
if (modelChanged) modelsChanged = true;
return modelChanged ? nextModel : model;
});
if (!modelsChanged) continue;
nextProviders[providerId] = {
...rawProvider,
models: nextModels
};
providersChanged = true;
}
if (!providersChanged) return cfg;
return {
...cfg,
models: {
...cfg.models,
providers: nextProviders
}
};
}
//#endregion
//#region src/commands/doctor/shared/legacy-web-fetch-migrate.ts
const DANGEROUS_RECORD_KEYS = new Set([
"__proto__",
"prototype",
"constructor"
]);
function resolveLegacyFetchConfig(raw) {
if (!isRecord$1(raw)) return;
const tools = isRecord$1(raw.tools) ? raw.tools : void 0;
const web = isRecord$1(tools?.web) ? tools.web : void 0;
return isRecord$1(web?.fetch) ? web.fetch : void 0;
}
function copyLegacyFirecrawlFetchConfig(fetch) {
const current = fetch.firecrawl;
if (!isRecord$1(current)) return;
const next = cloneRecord(current);
delete next.enabled;
return next;
}
function hasMappedLegacyWebFetchConfig(raw) {
const fetch = resolveLegacyFetchConfig(raw);
if (!fetch) return false;
return isRecord$1(fetch.firecrawl);
}
function migratePluginWebFetchConfig(params) {
const entry = ensureRecord(ensureRecord(ensureRecord(params.root, "plugins"), "entries"), "firecrawl");
const config = ensureRecord(entry, "config");
const hadEnabled = entry.enabled !== void 0;
const existing = isRecord$1(config.webFetch) ? cloneRecord(config.webFetch) : void 0;
if (!hadEnabled) entry.enabled = true;
if (!existing) {
config.webFetch = cloneRecord(params.payload);
params.changes.push("Moved tools.web.fetch.firecrawl → plugins.entries.firecrawl.config.webFetch.");
return;
}
const merged = cloneRecord(existing);
mergeMissing(merged, params.payload);
const changed = JSON.stringify(merged) !== JSON.stringify(existing) || !hadEnabled;
config.webFetch = merged;
if (changed) {
params.changes.push("Merged tools.web.fetch.firecrawl → plugins.entries.firecrawl.config.webFetch (filled missing fields from legacy; kept explicit plugin config values).");
return;
}
params.changes.push("Removed tools.web.fetch.firecrawl (plugins.entries.firecrawl.config.webFetch already set).");
}
/** Move legacy Firecrawl web-fetch config into plugins.entries.firecrawl.config.webFetch. */
function migrateLegacyWebFetchConfig(raw) {
if (!isRecord$1(raw) || !hasMappedLegacyWebFetchConfig(raw)) return {
config: raw,
changes: []
};
return normalizeLegacyWebFetchConfigRecord(raw);
}
function normalizeLegacyWebFetchConfigRecord(raw) {
const nextRoot = structuredClone(raw);
const web = ensureRecord(ensureRecord(nextRoot, "tools"), "web");
const fetch = resolveLegacyFetchConfig(nextRoot);
if (!fetch) return {
config: raw,
changes: []
};
const nextFetch = {};
for (const [key, value] of Object.entries(fetch)) {
if (key === "firecrawl" && isRecord$1(value)) continue;
if (DANGEROUS_RECORD_KEYS.has(key)) continue;
nextFetch[key] = value;
}
web.fetch = nextFetch;
const firecrawl = copyLegacyFirecrawlFetchConfig(fetch);
const changes = [];
if (firecrawl && Object.keys(firecrawl).length > 0) migratePluginWebFetchConfig({
root: nextRoot,
payload: firecrawl,
changes
});
else if (hasOwnKey(fetch, "firecrawl")) changes.push("Removed empty tools.web.fetch.firecrawl.");
return {
config: nextRoot,
changes
};
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-compatibility-base.ts
/** Run common compatibility migrations before caller-specific setup/channel passes. */
function normalizeBaseCompatibilityConfigValues(cfg, changes, afterBrowser) {
let next = seedMissingDefaultAccountsFromSingleAccountBase(cfg, changes);
next = normalizeLegacyBrowserConfig(next, changes);
next = afterBrowser ? afterBrowser(next) : next;
for (const migrate of [
migrateLegacyWebSearchConfig,
migrateLegacyWebFetchConfig,
migrateLegacyXSearchConfig
]) {
const migrated = migrate(next);
if (migrated.changes.length === 0) continue;
next = migrated.config;
changes.push(...migrated.changes);
}
next = normalizeLegacyNanoBananaSkill(next, changes);
next = normalizeLegacyTalkConfig(next, changes);
next = normalizeLegacyOpenAIModelProviderApi(next, changes);
next = normalizeLegacyRuntimeModelRefs(next, changes);
next = normalizeLegacyCrossContextMessageConfig(next, changes);
next = normalizeLegacyMediaProviderOptions(next, changes);
next = normalizeLegacyOllamaNativeNumCtxParams(next, changes);
return normalizeLegacyMistralModelDefaults(next, changes);
}
//#endregion
//#region src/commands/doctor/shared/legacy-config-core-migrate.ts
function pruneBindingsForMissingAgents(cfg, changes) {
const agents = cfg.agents?.list;
const bindings = cfg.bindings;
if (!Array.isArray(agents) || agents.length === 0 || !Array.isArray(bindings)) return cfg;
const validAgents = agents.filter((agent) => {
return agent !== null && typeof agent === "object" && typeof agent.id === "string";
});
if (validAgents.length !== agents.length) return cfg;
const agentIds = new Set(validAgents.map((agent) => normalizeAgentId(agent.id)));
const nextBindings = bindings.filter((binding) => {
const agentId = binding && typeof binding === "object" ? binding.agentId : void 0;
return typeof agentId !== "string" || agentIds.has(normalizeAgentId(agentId));
});
const removed = bindings.length - nextBindings.length;
if (removed === 0) return cfg;
changes.push(`Removed ${removed} binding${removed === 1 ? "" : "s"} that referenced missing agents.list ids.`);
return {
...cfg,
...nextBindings.length > 0 ? { bindings: nextBindings } : { bindings: void 0 }
};
}
/** Normalize current config through core, plugin setup, channel, and secret-ref migrations. */
function normalizeCompatibilityConfigValues(cfg) {
const changes = [];
let next = normalizeBaseCompatibilityConfigValues(cfg, changes, (config) => {
const setupMigration = runPluginSetupConfigMigrations({ config });
if (setupMigration.changes.length === 0) return config;
changes.push(...setupMigration.changes);
return setupMigration.config;
});
const channelMigrations = applyChannelDoctorCompatibilityMigrations(next);
if (channelMigrations.changes.length > 0) {
next = channelMigrations.next;
changes.push(...channelMigrations.changes);
}
const secretRefMarkers = migrateLegacySecretRefEnvMarkers(next);
if (secretRefMarkers.changes.length > 0) {
next = secretRefMarkers.config;
changes.push(...secretRefMarkers.changes);
}
next = normalizeLegacyCommandsConfig(next, changes);
next = normalizeLegacyOpenAICodexModelsAddMetadata(next, changes);
next = pruneBindingsForMissingAgents(next, changes);
return {
config: next,
changes
};
}
//#endregion
//#region src/commands/doctor/emit-notes.ts
/** Strip terminal control sequences from a potentially multi-line doctor note. */
function sanitizeDoctorNote(note) {
return note.split("\n").map((line) => sanitizeForLog(line)).join("\n");
}
/** Emit grouped doctor change, info, and warning notes with sanitized content. */
function emitDoctorNotes(params) {
for (const change of params.changeNotes ?? []) params.note(sanitizeDoctorNote(change), "Doctor changes");
for (const info of params.infoNotes ?? []) params.note(sanitizeDoctorNote(info), "Doctor info");
for (const warning of params.warningNotes ?? []) params.note(sanitizeDoctorNote(warning), "Doctor warnings");
}
//#endregion
//#region src/commands/doctor/finalize-config-flow.ts
/** Decide whether doctor should write the repaired candidate config or only print hints. */
async function finalizeDoctorConfigFlow(params) {
if (!params.shouldRepair && params.pendingChanges) {
if (await params.confirm({
message: "Apply recommended config repairs now?",
initialValue: true
})) return {
cfg: params.candidate,
shouldWriteConfig: true
};
if (params.fixHints.length > 0) params.note(params.fixHints.join("\n"), "Doctor");
return {
cfg: params.cfg,
shouldWriteConfig: false
};
}
if (params.shouldRepair && params.pendingChanges) return {
cfg: params.cfg,
shouldWriteConfig: true
};
return {
cfg: params.cfg,
shouldWriteConfig: false
};
}
//#endregion
//#region src/commands/doctor-auth-profile-config.ts
/** Protects active auth profile metadata while doctor repairs broader config state. */
const AUTH_PROFILE_MODES = new Set([
"api_key",
"aws-sdk",
"oauth",
"token"
]);
function normalizeProviderId(value) {
return normalizeLowercaseStringOrEmpty(value);
}
function normalizeProfileId(value) {
return normalizeOptionalString(value) ?? null;
}
function normalizeMode(value) {
return typeof value === "string" && AUTH_PROFILE_MODES.has(value) ? value : null;
}
function extractProviderFromModelRef(value) {
const { model } = splitTrailingAuthProfile(value);
const slash = model.indexOf("/");
if (slash <= 0) return null;
return normalizeProviderId(model.slice(0, slash)) || null;
}
function extractProviderFromProfileId(profileId) {
const colon = profileId.indexOf(":");
if (colon <= 0) return null;
return normalizeProviderId(profileId.slice(0, colon)) || null;
}
function collectActiveAuthHints(config) {
const activeProviders = /* @__PURE__ */ new Set();
const explicitProfileIds = /* @__PURE__ */ new Set();
const explicitProfileProviders = /* @__PURE__ */ new Map();
const models = isRecord$1(config.models) ? config.models : {};
const providers = isRecord$1(models.providers) ? models.providers : {};
for (const providerId of Object.keys(providers)) {
const normalized = normalizeProviderId(providerId);
if (normalized) activeProviders.add(normalized);
}
for (const { value } of collectConfiguredModelRefs(config)) {
const { profile } = splitTrailingAuthProfile(value);
const provider = extractProviderFromModelRef(value);
if (profile) {
explicitProfileIds.add(profile);
if (provider) {
const providersLocal = explicitProfileProviders.get(profile) ?? /* @__PURE__ */ new Set();
providersLocal.add(provider);
explicitProfileProviders.set(profile, providersLocal);
}
}
if (provider) activeProviders.add(provider);
}
const auth = isRecord$1(config.auth) ? config.auth : {};
const order = isRecord$1(auth.order) ? auth.order : {};
for (const [providerId, profileIds] of Object.entries(order)) {
const provider = normalizeProviderId(providerId);
if (!provider || !activeProviders.has(provider) || !Array.isArray(profileIds)) continue;
for (const profileId of profileIds) {
const normalized = normalizeProfileId(profileId);
if (normalized) explicitProfileIds.add(normalized);
}
}
return {
activeProviders,
explicitProfileIds,
explicitProfileProviders
};
}
function isValidProfileMetadata(value) {
if (!isRecord$1(value)) return false;
return normalizeProviderId(value.provider) !== "" && normalizeMode(value.mode) !== null;
}
function buildProfileMetadata(params) {
const before = isRecord$1(params.before) ? params.before : {};
const after = isRecord$1(params.after) ? params.after : {};
const provider = normalizeProviderId(after.provider) || normalizeProviderId(before.provider) || extractProviderFromProfileId(params.profileId) || normalizeProviderId(params.providerHint);
if (!provider) return null;
const repaired = {
provider,
mode: normalizeMode(after.mode) ?? normalizeMode(before.mode) ?? "api_key"
};
const email = normalizeOptionalString(after.email) ?? normalizeOptionalString(before.email);
const displayName = normalizeOptionalString(after.displayName) ?? normalizeOptionalString(before.displayName);
if (email) repaired.email = email;
if (displayName) repaired.displayName = displayName;
return repaired;
}
function ensureAuthProfiles(config) {
const root = config;
const auth = isRecord$1(root.auth) ? root.auth : {};
if (root.auth !== auth) root.auth = auth;
if (!isRecord$1(auth.profiles)) auth.profiles = {};
return auth.profiles;
}
/**
* Restores valid metadata for auth profiles still referenced by active model config.
*
* Doctor can rebuild or prune auth config; this guard keeps active profiles usable when their
* provider/mode metadata can be inferred from the before/after config or profile id.
*/
function protectActiveAuthProfileConfig(params) {
const { activeProviders, explicitProfileIds, explicitProfileProviders } = collectActiveAuthHints(params.before);
const beforeAuth = isRecord$1(params.before.auth) ? params.before.auth : {};
const beforeProfiles = isRecord$1(beforeAuth.profiles) ? beforeAuth.profiles : {};
if (Object.keys(beforeProfiles).length === 0) return {
config: params.after,
repairs: [],
warnings: []
};
const config = structuredClone(params.after);
const afterAuth = isRecord$1(config.auth) ? config.auth : {};
const afterProfiles = isRecord$1(afterAuth.profiles) ? afterAuth.profiles : {};
const repairs = [];
const warnings = [];
for (const [profileId, beforeProfile] of Object.entries(beforeProfiles)) {
const afterProfile = afterProfiles[profileId];
const afterProfileRecord = isRecord$1(afterProfile) ? afterProfile : null;
const beforeProfileRecord = isRecord$1(beforeProfile) ? beforeProfile : null;
if (isValidProfileMetadata(afterProfile)) continue;
const provider = normalizeProviderId(afterProfileRecord?.provider) || normalizeProviderId(beforeProfileRecord?.provider) || extractProviderFromProfileId(profileId);
const protectsActiveProvider = provider !== null && activeProviders.has(provider);
const protectsExplicitProfile = explicitProfileIds.has(profileId);
if (!protectsActiveProvider && !protectsExplicitProfile) continue;
const repaired = buildProfileMetadata({
profileId,
before: beforeProfile,
after: afterProfile,
providerHint: explicitProfileProviders.get(profileId)?.size === 1 ? [...explicitProfileProviders.get(profileId) ?? []][0] : void 0
});
if (!repaired) {
warnings.push(`auth.profiles.${profileId}: active auth profile metadata could not be inferred; repair manually before running doctor --fix.`);
continue;
}
const profiles = ensureAuthProfiles(config);
profiles[profileId] = repaired;
repairs.push(`Repaired auth.profiles.${profileId} metadata for active ${repaired.provider} auth.`);
}
return {
config,
repairs,
warnings
};
}
//#endregion
//#region src/commands/doctor/shared/config-flow-steps.ts
/** Apply legacy config migrations and update preview/fix state for doctor config flow. */
function applyLegacyCompatibilityStep(params) {
if (params.snapshot.legacyIssues.length === 0) return {
state: params.state,
issueLines: [],
changeLines: []
};
const issueLines = formatConfigIssueLines(params.snapshot.legacyIssues, "-");
const { config: migrated, changes, partiallyValid } = migrateLegacyConfig(params.snapshot.parsed);
if (!migrated) return {
state: {
...params.state,
pendingChanges: params.state.pendingChanges || params.snapshot.legacyIssues.length > 0,
fixHints: params.shouldRepair ? params.state.fixHints : [...params.state.fixHints, `Run "${params.doctorFixCommand}" to migrate legacy config keys.`]
},
issueLines,
changeLines: changes
};
return {
state: {
cfg: migrated,
candidate: migrated,
pendingChanges: params.state.pendingChanges || params.snapshot.legacyIssues.length > 0,
fixHints: params.shouldRepair ? params.state.fixHints : [...params.state.fixHints, `Run "${params.doctorFixCommand}" to ${partiallyValid ? "finish fixing" : "migrate"} legacy config keys.`]
},
issueLines,
changeLines: changes,
partiallyValid: partiallyValid === true ? true : void 0
};
}
/** Strip unknown config keys while preserving active auth profile settings. */
function applyUnknownConfigKeyStep(params) {
const unknown = stripUnknownConfigKeys(params.state.candidate);
if (unknown.removed.length === 0) return {
state: params.state,
removed: [],
repairs: [],
warnings: []
};
const protectedAuth = protectActiveAuthProfileConfig({
before: params.state.candidate,
after: unknown.config
});
return {
state: {
cfg: params.shouldRepair ? protectedAuth.config : params.state.cfg,
candidate: protectedAuth.config,
pendingChanges: true,
fixHints: params.shouldRepair ? params.state.fixHints : [...params.state.fixHints, `Run "${params.doctorFixCommand}" to remove these keys.`]
},
removed: unknown.removed,
repairs: protectedAuth.repairs,
warnings: protectedAuth.warnings
};
}
//#endregion
//#region src/commands/doctor/shared/default-account-warnings.ts
function normalizeBindingChannelKey(raw) {
const normalized = normalizeChatChannelId(raw);
if (normalized) return normalized;
return normalizeLowercaseStringOrEmpty(raw);
}
function collectChannelsMissingDefaultAccount(cfg) {
const channels = asObjectRecord(cfg.channels);
if (!channels) return [];
const contexts = [];
for (const [channelKey, rawChannel] of Object.entries(channels)) {
const channel = asObjectRecord(rawChannel);
if (!channel) continue;
const accounts = asObjectRecord(channel.accounts);
if (!accounts) continue;
const normalizedAccountIds = Array.from(new Set(Object.keys(accounts).map((accountId) => normalizeAccountId(accountId)).filter(Boolean))).toSorted((a, b) => a.localeCompare(b));
if (normalizedAccountIds.length === 0 || normalizedAccountIds.includes("default")) continue;
contexts.push({
channelKey,
channel,
normalizedAccountIds
});
}
return contexts;
}
/** Warn when account-scoped route bindings do not cover channels without accounts.default. */
function collectMissingDefaultAccountBindingWarnings(cfg) {
const bindings = listRouteBindings(cfg);
const warnings = [];
for (const { channelKey, normalizedAccountIds } of collectChannelsMissingDefaultAccount(cfg)) {
const accountIdSet = new Set(normalizedAccountIds);
const channelPattern = normalizeBindingChannelKey(channelKey);
let hasWildcardBinding = false;
const coveredAccountIds = /* @__PURE__ */ new Set();
for (const binding of bindings) {
const bindingRecord = asObjectRecord(binding);
if (!bindingRecord) continue;
const match = asObjectRecord(bindingRecord.match);
if (!match) continue;
const matchChannel = typeof match.channel === "string" ? normalizeBindingChannelKey(match.channel) : "";
if (!matchChannel || matchChannel !== channelPattern) continue;
const rawAccountId = normalizeOptionalString(match.accountId) ?? "";
if (!rawAccountId) continue;
if (rawAccountId === "*") {
hasWildcardBinding = true;
continue;
}
const normalizedBindingAccountId = normalizeAccountId(rawAccountId);
if (accountIdSet.has(normalizedBindingAccountId)) coveredAccountIds.add(normalizedBindingAccountId);
}
if (hasWildcardBinding) continue;
const uncoveredAccountIds = normalizedAccountIds.filter((accountId) => !coveredAccountIds.has(accountId));
if (uncoveredAccountIds.length === 0) continue;
if (coveredAccountIds.size > 0) {
warnings.push(`- channels.${channelKey}: accounts.default is missing and account bindings only cover a subset of configured accounts. Uncovered accounts: ${uncoveredAccountIds.join(", ")}. Add bindings[].match.accountId for uncovered accounts (or "*"), or add ${formatChannelAccountsDefaultPath(channelKey)}.`);
continue;
}
warnings.push(`- channels.${channelKey}: accounts.default is missing and no valid account-scoped binding exists for configured accounts (${normalizedAccountIds.join(", ")}). Channel-only bindings (no accountId) match only default. Add bindings[].match.accountId for one of these accounts (or "*"), or add ${formatChannelAccountsDefaultPath(channelKey)}.`);
}
return warnings;
}
/** Warn when multi-account channels omit or misconfigure an explicit default account. */
function collectMissingExplicitDefaultAccountWarnings(cfg) {
const warnings = [];
for (const { channelKey, channel, normalizedAccountIds } of collectChannelsMissingDefaultAccount(cfg)) {
if (normalizedAccountIds.length < 2) continue;
const preferredDefault = normalizeOptionalAccountId(typeof channel.defaultAccount === "string" ? channel.defaultAccount : void 0);
if (preferredDefault) {
if (normalizedAccountIds.includes(preferredDefault)) continue;
warnings.push(`- channels.${channelKey}: defaultAccount is set to "${preferredDefault}" but does not match configured accounts (${normalizedAccountIds.join(", ")}). ${formatSetExplicitDefaultToConfiguredInstruction({ channelKey })} to avoid fallback routing.`);
continue;
}
warnings.push(`- channels.${channelKey}: multiple accounts are configured but no explicit default is set. ${formatSetExplicitDefaultInstruction(channelKey)} to avoid fallback routing.`);
}
return warnings;
}
//#endregion
//#region src/commands/doctor-config-flow.ts
/** Main doctor config flow: preflight, migrations, previews, repairs, and final write decision. */
function hasLegacyInternalHookHandlers(raw) {
const handlers = raw?.hooks?.internal?.handlers;
return Array.isArray(handlers) && handlers.length > 0;
}
function collectInvalidHookTransformsDirWarnings(cfg, configPath) {
const transformsDir = cfg.hooks?.transformsDir?.trim();
if (!transformsDir) return [];
const configDir = path.dirname(configPath);
const transformsRoot = path.join(configDir, "hooks", "transforms");
const resolved = path.isAbsolute(transformsDir) ? path.resolve(transformsDir) : path.resolve(transformsRoot, transformsDir);
const relative = path.relative(transformsRoot, resolved);
if (!(relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))) return [];
return [`- hooks.transformsDir: ${transformsDir} is outside ${transformsRoot}. Hook transform modules must live under ${transformsRoot}; move custom transforms there or remove hooks.transformsDir.`];
}
function collectUnsupportedInternalHookEntryWarnings(cfg) {
const entries = cfg.hooks?.internal?.entries;
if (!entries) return [];
const unsupportedKeysByEntry = Object.entries(entries).filter(([, entry]) => entry && typeof entry === "object" && !Array.isArray(entry)).map(([hookKey, entry]) => {
return {
hookKey,
unsupportedKeys: [
"handler",
"module",
"extraDirs",
"installs"
].filter((key) => Object.hasOwn(entry, key))
};
}).filter(({ unsupportedKeys }) => unsupportedKeys.length > 0);
if (unsupportedKeysByEntry.length === 0) return [];
return unsupportedKeysByEntry.map(({ hookKey, unsupportedKeys }) => `- hooks.internal.entries.${hookKey}: unsupported loader key${unsupportedKeys.length === 1 ? "" : "s"} ${unsupportedKeys.join(", ")} will not load hook modules. Use bootstrap-extra-files for session bootstrap content, or create a managed/workspace hook directory with HOOK.md + handler.js. Doctor cannot rewrite this automatically because per-hook entry keys are open-ended hook configuration.`);
}
function collectConfiguredChannelIds(cfg) {
const channels = cfg.channels && typeof cfg.channels === "object" && !Array.isArray(cfg.channels) ? cfg.channels : null;
if (!channels) return [];
return Object.keys(channels).filter((channelId) => channelId !== "defaults");
}
function emitDoctorChangesPanel(changeLines, shouldRepair, options = {}) {
if (changeLines.length === 0) return;
const body = changeLines.join("\n");
note(options.sanitize ? sanitizeDoctorNote(body) : body, shouldRepair ? "Doctor changes" : "Doctor changes preview");
}
async function refreshGatewayAuthStateAfterAuthProfileRepair() {
try {
await callGateway({
method: "secrets.reload",
params: {},
timeoutMs: 3e3
});
} catch {}
try {
await callGateway({
method: "models.authStatus",
params: { refresh: true },
timeoutMs: 3e3
});
} catch {}
}
/**
* Loads config, runs doctor migrations/repairs, and returns the config write plan.
*
* This is the config-side orchestration boundary for doctor; it keeps preview notes, repair
* mutations, gateway auth refreshes, and final write confirmation in one ordered flow.
*/
async function loadAndMaybeMigrateDoctorConfig(params) {
const shouldRepair = params.options.repair === true || params.options.yes === true;
const preflight = await runDoctorConfigPreflight({
repairPrefixedConfig: shouldRepair,
recoverCorruptTargetStore: shouldRepair
});
const snapshot = preflight.snapshot;
const baseCfg = preflight.baseConfig;
let cfg = baseCfg;
let candidate = structuredClone(baseCfg);
let pendingChanges = false;
let fixHints = [];
const doctorFixCommand = formatCliCommand("openclaw doctor --fix");
const sourceMeta = snapshot.sourceConfig?.meta;
const sourceLastTouchedVersion = typeof sourceMeta?.lastTouchedVersion === "string" ? sourceMeta.lastTouchedVersion : void 0;
const legacyStep = applyLegacyCompatibilityStep({
snapshot,
state: {
cfg,
candidate,
pendingChanges,
fixHints
},
shouldRepair,
doctorFixCommand
});
cfg = legacyStep.state.cfg;
candidate = legacyStep.state.candidate;
pendingChanges = pendingChanges || legacyStep.state.pendingChanges;
fixHints = legacyStep.state.fixHints;
const legacyMigrationPartiallyValid = legacyStep.partiallyValid === true;
const pluginLegacyIssues = await (async () => {
if (snapshot.parsed === snapshot.sourceConfig) return [];
const { findDoctorLegacyConfigIssues } = await import("./legacy-config-issues-WXsU36Fa.js");
return findDoctorLegacyConfigIssues(snapshot.parsed, snapshot.parsed);
})();
const seenLegacyIssues = new Set(snapshot.legacyIssues.map((issue) => `${issue.path}:${issue.message}`));
const pluginIssueLines = pluginLegacyIssues.filter((issue) => {
const key = `${issue.path}:${issue.message}`;
if (seenLegacyIssues.has(key)) return false;
seenLegacyIssues.add(key);
return true;
}).map((issue) => `- ${issue.path}: ${issue.message}`);
const legacyIssueLines = [...legacyStep.issueLines, ...pluginIssueLines];
if (pluginIssueLines.length > 0 && !shouldRepair && !fixHints.includes(`Run "${doctorFixCommand}" to migrate legacy config keys.`)) fixHints.push(`Run "${doctorFixCommand}" to migrate legacy config keys.`);
if (legacyIssueLines.length > 0) note(legacyIssueLines.join("\n"), "Legacy config keys detected");
emitDoctorChangesPanel(legacyStep.changeLines, shouldRepair);
if (hasLegacyInternalHookHandlers(snapshot.parsed)) note([
"- hooks.internal.handlers: legacy inline hook modules are no longer part of the public config surface.",
"- Migrate each entry to a managed or workspace hook directory with HOOK.md + handler.js, then enable it through hooks.internal.entries.<hookKey> as needed.",
"- openclaw doctor --fix does not rewrite this shape automatically."
].join("\n"), "Legacy config keys detected");
const hookTransformsDirWarnings = collectInvalidHookTransformsDirWarnings(cfg, snapshot.path);
if (hookTransformsDirWarnings.length > 0) note(sanitizeDoctorNote(hookTransformsDirWarnings.join("\n")), "Doctor warnings");
const unsupportedInternalHookEntryWarnings = collectUnsupportedInternalHookEntryWarnings(cfg);
if (unsupportedInternalHookEntryWarnings.length > 0) note(sanitizeDoctorNote(unsupportedInternalHookEntryWarnings.join("\n")), "Doctor warnings");
const normalized = normalizeCompatibilityConfigValues(candidate);
if (normalized.changes.length > 0) {
emitDoctorChangesPanel(normalized.changes, shouldRepair);
({cfg, candidate, pendingChanges, fixHints} = applyDoctorConfigMutation({
state: {
cfg,
candidate,
pendingChanges,
fixHints
},
mutation: normalized,
shouldRepair,
fixHint: `Run "${doctorFixCommand}" to apply these changes.`
}));
}
const { applyPluginAutoEnable } = await import("./plugin-auto-enable-Bm6cIZBX.js");
const autoEnable = applyPluginAutoEnable({
config: candidate,
env: process.env
});
if (autoEnable.changes.length > 0) {
emitDoctorChangesPanel(autoEnable.changes, shouldRepair);
({cfg, candidate, pendingChanges, fixHints} = applyDoctorConfigMutation({
state: {
cfg,
candidate,
pendingChanges,
fixHints
},
mutation: autoEnable,
shouldRepair,
fixHint: `Run "${doctorFixCommand}" to apply these changes.`
}));
}
const { collectPluginToolAllowlistWarnings } = await import("./plugin-tool-allowlist-warnings-nkVmdUWd.js");
const pluginToolAllowlistWarnings = collectPluginToolAllowlistWarnings({
cfg: candidate,
env: process.env
});
if (pluginToolAllowlistWarnings.length > 0) note(sanitizeDoctorNote(pluginToolAllowlistWarnings.join("\n")), "Doctor warnings");
const hasConfiguredChannels = collectConfiguredChannelIds(candidate).length > 0;
let collectMutableAllowlistWarnings;
if (hasConfiguredChannels) {
const channelDoctor = await import("./channel-doctor-CdbWx726.js");
collectMutableAllowlistWarnings = channelDoctor.collectChannelDoctorMutableAllowlistWarnings;
const channelDoctorSequence = await channelDoctor.runChannelDoctorConfigSequences({
cfg: candidate,
env: process.env,
shouldRepair
});
emitDoctorNotes({
note,
changeNotes: channelDoctorSequence.changeNotes,
warningNotes: channelDoctorSequence.warningNotes
});
for (const staleCleanup of await channelDoctor.collectChannelDoctorStaleConfigMutations(candidate, { env: process.env })) {
if (staleCleanup.changes.length === 0) continue;
emitDoctorChangesPanel(staleCleanup.changes, shouldRepair, { sanitize: true });
({cfg, candidate, pendingChanges, fixHints} = applyDoctorConfigMutation({
state: {
cfg,
candidate,
pendingChanges,
fixHints
},
mutation: staleCleanup,
shouldRepair,
fixHint: `Run "${doctorFixCommand}" to remove stale channel plugin references.`
}));
}
}
const missingDefaultAccountBindingWarnings = collectMissingDefaultAccountBindingWarnings(candidate);
if (missingDefaultAccountBindingWarnings.length > 0) note(missingDefaultAccountBindingWarnings.join("\n"), "Doctor warnings");
const missingExplicitDefaultWarnings = collectMissingExplicitDefaultAccountWarnings(candidate);
if (missingExplicitDefaultWarnings.length > 0) note(missingExplicitDefaultWarnings.join("\n"), "Doctor warnings");
const { repairHooksTokenReuseGatewayAuth } = await import("./hooks-token-reuse-repair-B_Pm2vQ6.js");
const hooksTokenReuseRepair = await repairHooksTokenReuseGatewayAuth(candidate, process.env);
emitDoctorChangesPanel(hooksTokenReuseRepair.changes, shouldRepair);
({cfg, candidate, pendingChanges, fixHints} = applyDoctorConfigMutation({
state: {
cfg,
candidate,
pendingChanges,
fixHints
},
mutation: hooksTokenReuseRepair,
shouldRepair,
fixHint: `Run "${doctorFixCommand}" to rotate hooks.token away from Gateway auth.`
}));
if (shouldRepair) {
const { runDoctorRepairSequence } = await import("./repair-sequencing-BMSusabO.js");
const repairSequence = await runDoctorRepairSequence({
state: {
cfg,
candidate,
pendingChanges,
fixHints
},
doctorFixCommand,
env: process.env
});
({cfg, candidate, pendingChanges, fixHints} = repairSequence.state);
if (repairSequence.authProfilesRepaired) await refreshGatewayAuthStateAfterAuthProfileRepair();
emitDoctorNotes({
note,
changeNotes: repairSequence.changeNotes,
warningNotes: repairSequence.warningNotes
});
} else {
const { collectDoctorPreviewNotes } = await import("./preview-warnings-D7-Z4v3i.js");
const previewNotes = await collectDoctorPreviewNotes({
cfg: candidate,
doctorFixCommand,
env: process.env
});
emitDoctorNotes({
note,
infoNotes: previewNotes.infoNotes,
warningNotes: previewNotes.warningNotes
});
}
const mutableAllowlistWarnings = collectMutableAllowlistWarnings ? await collectMutableAllowlistWarnings({
cfg: candidate,
env: process.env
}) : [];
if (mutableAllowlistWarnings.length > 0) note(sanitizeDoctorNote(mutableAllowlistWarnings.join("\n")), "Doctor warnings");
const unknownStep = applyUnknownConfigKeyStep({
state: {
cfg,
candidate,
pendingChanges,
fixHints
},
shouldRepair,
doctorFixCommand
});
({cfg, candidate, pendingChanges, fixHints} = unknownStep.state);
if (unknownStep.removed.length > 0 || unknownStep.repairs.length > 0) note([...unknownStep.removed.map((pathLocal) => `- ${pathLocal}`), ...unknownStep.repairs.map((change) => `- ${change}`)].join("\n"), shouldRepair ? "Doctor changes" : "Unknown config keys");
if (unknownStep.warnings.length > 0) note(unknownStep.warnings.join("\n"), "Doctor warnings");
const finalized = await finalizeDoctorConfigFlow({
cfg,
candidate,
pendingChanges,
shouldRepair,
fixHints,
confirm: params.confirm,
note
});
cfg = finalized.cfg;
noteOpencodeProviderOverrides(cfg);
noteImplicitFallbackClobberWarnings(cfg);
return {
cfg,
path: snapshot.path ?? CONFIG_PATH,
shouldWriteConfig: finalized.shouldWriteConfig,
sourceConfigValid: snapshot.valid,
preservedLegacyRootKeys: ["defaultModel"],
...sourceLastTouchedVersion ? { sourceLastTouchedVersion } : {},
...legacyMigrationPartiallyValid ? { skipPluginValidationOnWrite: true } : {}
};
}
//#endregion
export { loadAndMaybeMigrateDoctorConfig };