UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

352 lines (351 loc) • 14.5 kB
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { t as isBlockedObjectKey } from "./prototype-keys-CuYw53fZ.mjs"; import { t as parseConfigPathArrayIndex } from "./path-array-index-CvEcUJa-.mjs"; import { n as formatConcreteConfigPath, o as toDotPath } from "./dot-path-CVD5Jolm.mjs"; import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs"; import { n as quotePowerShellArg, t as quoteCliArg } from "./quote-cli-arg-BEt71TUh.mjs"; import { t as normalizeConfigModelSelectionParent } from "./model-input-normalization-C54uHM0N.mjs"; import { t as rejectConfigNonFiniteNumbers } from "./value-tree-3oLf9xok.mjs"; import { s as formatStrictJsonParseFailure } from "./error-format-BaRMqGR7.mjs"; import JSON5 from "json5"; //#region src/cli/config-cli-path.ts function formatConfigSetPath(path, pathTokens, source) { return formatConcreteConfigPath(pathTokens ?? path, source); } /** Patch prints this path as its `--replace-path` argument, so a literal dot in a key needs brackets to re-parse. */ function refusalPathLabel(command, path) { return command === "patch" ? formatConfigSetPath(path) : toDotPath(path); } /** * A copied retry crosses a shell, and once a key holds a quote no single spelling survives both * the POSIX and the PowerShell convention. The host platform is no proxy for the interactive * shell - Git Bash on Windows needs the POSIX form - so the advice names both when they differ. */ function replacePathArgument(pathLabel) { const posix = quoteCliArg(pathLabel); const powershell = quotePowerShellArg(pathLabel); if (posix === pathLabel || posix === powershell) return `--replace-path ${posix}`; return `--replace-path ${posix} in bash and zsh, or --replace-path ${powershell} in PowerShell`; } function parseConfigSetValue(raw, strictJson) { const trimmed = raw.trim(); let parsed; try { parsed = strictJson ? JSON.parse(trimmed) : JSON5.parse(trimmed); } catch (err) { if (strictJson) throw new Error(formatStrictJsonParseFailure({ value: raw, cause: err }), { cause: err }); return raw; } rejectConfigNonFiniteNumbers(parsed); return parsed; } function validatePathSegments(path) { for (const segment of path) if (isBlockedObjectKey(segment)) throw new Error(`Invalid path segment: ${segment}`); } function getAtPath(root, path) { let current = root; for (const segment of path) { if (!current || typeof current !== "object") return { found: false }; if (Array.isArray(current)) { const index = parseConfigPathArrayIndex(segment); if (index === void 0 || index >= current.length) return { found: false }; current = current[index]; continue; } const record = current; if (!Object.hasOwn(record, segment)) return { found: false }; current = record[segment]; } return { found: true, value: current }; } function formatConfigUnsetMissingPathMessage(params) { if (params.runtimeOnly) return `Config path not found in authored config: ${params.path}. It only exists after runtime defaults are applied, so there is nothing for config unset to remove. Use ${formatCliCommand("openclaw config set <path> <value>")} to override the inherited value.`; return `Config path not found: ${params.path}. Nothing was changed. Run ${formatCliCommand("openclaw config get <path>")} first if you are unsure of the path.`; } function schemaHasType(schema, type) { return schema.type === type || Array.isArray(schema.type) && schema.type.includes(type); } function schemaAlternatives(schema, seen = /* @__PURE__ */ new Set()) { if (seen.has(schema)) return []; seen.add(schema); const alternatives = [schema]; for (const key of [ "anyOf", "oneOf", "allOf" ]) { const entries = schema[key]; if (!Array.isArray(entries)) continue; for (const entry of entries) if (isRecord(entry)) alternatives.push(...schemaAlternatives(entry, seen)); } return alternatives; } function schemaLooksArray(schema) { return schemaHasType(schema, "array") || isRecord(schema.items) || Array.isArray(schema.items); } function schemaLooksObject(schema) { return schemaHasType(schema, "object") || isRecord(schema.properties) || schema.additionalProperties === true || isRecord(schema.additionalProperties); } function propertySchema(schema, segment) { const schemas = []; for (const alternative of schemaAlternatives(schema)) { if (Object.keys(alternative).length === 0) { schemas.push(alternative); continue; } if (schemaLooksArray(alternative)) { const index = parseConfigPathArrayIndex(segment); if (index !== void 0) { const indexedItem = Array.isArray(alternative.items) ? alternative.items[index] : alternative.items; if (isRecord(indexedItem)) schemas.push(indexedItem); } continue; } const explicit = (isRecord(alternative.properties) ? alternative.properties : void 0)?.[segment]; if (isRecord(explicit)) schemas.push(explicit); else if (alternative.additionalProperties === true) schemas.push({}); else if (isRecord(alternative.additionalProperties)) schemas.push(alternative.additionalProperties); } return schemas; } function schemasAtPath(schema, path) { if (!schema) return []; let schemas = [schema]; for (const segment of path) { schemas = schemas.flatMap((candidate) => propertySchema(candidate, segment)); if (schemas.length === 0) return []; } return schemas; } function isConfigSchemaPath(schema, path) { return schemasAtPath(schema, path).length > 0; } function schemaPrefersArrayAtPath(schema, path) { const candidates = schemasAtPath(schema, path).flatMap((candidate) => schemaAlternatives(candidate)); if (candidates.length === 0) return; const hasArray = candidates.some((candidate) => schemaLooksArray(candidate)); return hasArray === candidates.some((candidate) => schemaLooksObject(candidate)) ? void 0 : hasArray; } function shouldCreateArrayForMissingPathSegment(params) { if (!params.next || params.options?.numericObjectKeys || parseConfigPathArrayIndex(params.next) === void 0) return false; if (typeof params.options?.pathTokens?.[params.segmentIndex + 1] === "number") return true; if (params.options?.quotedNumericSegments?.has(params.segmentIndex + 1)) return false; const parentPath = params.path.slice(0, params.segmentIndex + 1); return schemaPrefersArrayAtPath(params.options?.schema, parentPath) ?? true; } function setAtPath(root, path, value, options) { const last = path.at(-1); if (last === void 0) throw new Error("Config path must contain at least one segment"); let current = root; for (const [i, segment] of path.slice(0, -1).entries()) { const nextIsIndex = shouldCreateArrayForMissingPathSegment({ path, segmentIndex: i, next: path[i + 1], options }); if (Array.isArray(current)) { const index = parseConfigPathArrayIndex(segment); if (index === void 0) throw new Error(`Expected numeric index for array segment "${segment}"`); const existing = current[index]; if (!existing || typeof existing !== "object") current[index] = nextIsIndex ? [] : {}; current = current[index]; continue; } if (!current || typeof current !== "object") throw new Error(`Cannot traverse into "${segment}" (not an object)`); const record = current; const existing = Object.hasOwn(record, segment) ? record[segment] : void 0; if (!existing || typeof existing !== "object") record[segment] = normalizeConfigModelSelectionParent(existing, path, i) ?? (nextIsIndex ? [] : {}); current = record[segment]; } if (Array.isArray(current)) { const index = parseConfigPathArrayIndex(last); if (index === void 0) throw new Error(`Expected numeric index for array segment "${last}"`); current[index] = value; return; } if (!current || typeof current !== "object") throw new Error(`Cannot set "${last}" (parent is not an object)`); current[last] = value; } function modelArrayIds(value) { if (!Array.isArray(value)) return null; const ids = /* @__PURE__ */ new Set(); for (const entry of value) { if (!isRecord(entry) || typeof entry.id !== "string" || !entry.id.trim()) return null; ids.add(entry.id.trim()); } return ids; } function mergeModelArrays(existing, patch, path) { const merged = [...existing]; const suppliedPaths = []; const indexById = /* @__PURE__ */ new Map(); for (const [index, entry] of merged.entries()) if (isRecord(entry) && typeof entry.id === "string" && entry.id.trim()) indexById.set(entry.id.trim(), index); for (const entry of patch) { if (!isRecord(entry) || typeof entry.id !== "string" || !entry.id.trim()) { suppliedPaths.push([...path, String(merged.length)]); merged.push(entry); continue; } const id = entry.id.trim(); const existingIndex = indexById.get(id); if (existingIndex === void 0) { indexById.set(id, merged.length); suppliedPaths.push([...path, String(merged.length)]); merged.push(entry); continue; } const existingEntry = merged[existingIndex]; merged[existingIndex] = isRecord(existingEntry) ? { ...existingEntry, ...entry } : entry; for (const key of Object.keys(entry)) suppliedPaths.push([ ...path, String(existingIndex), key ]); } return { value: merged, suppliedPaths }; } function isProviderModelListPath(path) { return path.length === 4 && path[0] === "models" && path[1] === "providers" && path[3] === "models"; } function toMergePath(path) { let current; for (const segment of path) current = { parent: current, segment }; return current; } function mergePathSegments(path) { const segments = []; for (let current = path; current; current = current.parent) segments.push(current.segment); return segments.toReversed(); } function isProviderModelListMergePath(path) { const providers = path.parent?.parent; const models = providers?.parent; return path.segment === "models" && providers?.segment === "providers" && models?.segment === "models" && models.parent === void 0; } function mergeConfigValue(existing, patch, path, command) { if (isProviderModelListPath(path) && Array.isArray(existing) && Array.isArray(patch)) return mergeModelArrays(existing, patch, path); if (isRecord(existing) && isRecord(patch)) { const next = { ...existing }; const suppliedPaths = []; const pending = [{ target: next, patch, path: toMergePath(path) }]; while (pending.length > 0) { const frame = pending.pop(); for (const [key, value] of Object.entries(frame.patch)) { const current = frame.target[key]; const childPath = { parent: frame.path, segment: key }; if (Object.hasOwn(frame.target, key) && isProviderModelListMergePath(childPath) && Array.isArray(current) && Array.isArray(value)) { const merged = mergeModelArrays(current, value, mergePathSegments(childPath)); frame.target[key] = merged.value; suppliedPaths.push(...merged.suppliedPaths); } else if (Object.hasOwn(frame.target, key) && isRecord(current) && isRecord(value)) { const child = { ...current }; frame.target[key] = child; pending.push({ target: child, patch: value, path: childPath }); } else { frame.target[key] = value; suppliedPaths.push(mergePathSegments(childPath)); } } } return { value: next, suppliedPaths }; } const label = refusalPathLabel(command, path); throw new Error(`Cannot merge ${label}; use ${command === "patch" ? replacePathArgument(label) : "--replace"} to replace intentionally.`); } function mergeAtPath(root, path, value, options) { const existing = getAtPath(root, path); const merged = existing.found ? mergeConfigValue(existing.value, value, path, options?.command ?? "set") : { value, suppliedPaths: [path] }; setAtPath(root, path, merged.value, options); return merged.suppliedPaths; } function isProtectedMapReplacementPath(path) { const joined = path.join("."); return joined === "agents.defaults.models" || joined === "models.providers" || path.length === 3 && path[0] === "models" && path[1] === "providers" || joined === "agents.entries" || joined === "plugins.entries" || joined === "auth.profiles"; } function formatRemovedEntries(entries) { const visible = entries.slice(0, 6); const suffix = entries.length > visible.length ? `, ... ${entries.length - visible.length} more` : ""; return `${visible.join(", ")}${suffix}`; } function replacementAdvice(command, pathLabel, mergeSubject) { if (command === "patch") return `Use ${replacePathArgument(pathLabel)} to replace intentionally.`; return `Use --merge to merge ${mergeSubject} or --replace to replace intentionally.`; } function assertNonDestructiveReplacement(params) { if (params.allowReplace) return; const existing = getAtPath(params.root, params.path); if (!existing.found) return; const pathLabel = refusalPathLabel(params.command, params.path); let removed; let mergeHint; if (isProtectedMapReplacementPath(params.path) && isRecord(existing.value)) { if (!isRecord(params.value)) return; const nextKeys = new Set(Object.keys(params.value)); removed = Object.keys(existing.value).filter((key) => !nextKeys.has(key)); mergeHint = "object values"; } else if (isProviderModelListPath(params.path)) { const existingIds = modelArrayIds(existing.value); const nextIds = modelArrayIds(params.value); if (!existingIds || !nextIds) return; removed = [...existingIds].filter((id) => !nextIds.has(id)); mergeHint = "by id"; } else return; if (removed.length > 0) throw new Error(`Refusing to replace ${pathLabel}; it would remove existing entries: ${formatRemovedEntries(removed)}. ${replacementAdvice(params.command, pathLabel, mergeHint)}`); } function unsetAtPath(root, path) { const last = path.at(-1); if (last === void 0) return { removed: false }; const current = getAtPath(root, path.slice(0, -1)).value; if (Array.isArray(current)) { const index = parseConfigPathArrayIndex(last); if (index === void 0 || index >= current.length) return { removed: false }; current.splice(index, 1); return { removed: true, leafContainer: "array" }; } if (!current || typeof current !== "object") return { removed: false }; const record = current; if (!Object.hasOwn(record, last)) return { removed: false }; delete record[last]; return { removed: true, leafContainer: "object" }; } //#endregion export { isConfigSchemaPath as a, setAtPath as c, getAtPath as i, unsetAtPath as l, formatConfigSetPath as n, mergeAtPath as o, formatConfigUnsetMissingPathMessage as r, parseConfigSetValue as s, assertNonDestructiveReplacement as t, validatePathSegments as u };