diff --git a/src/plugins/bundle-manifest.ts b/src/plugins/bundle-manifest.ts index 6563f069280..bc61d25bbdd 100644 --- a/src/plugins/bundle-manifest.ts +++ b/src/plugins/bundle-manifest.ts @@ -43,7 +43,9 @@ function normalizePathList(value: unknown): string[] { if (!Array.isArray(value)) { return []; } - return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); + return value + .map((entry) => normalizeOptionalString(entry)) + .filter((entry): entry is string => Boolean(entry)); } export function normalizeBundlePathList(value: unknown): string[] { diff --git a/src/plugins/clawhub.ts b/src/plugins/clawhub.ts index fba6efbbe51..4427416b60c 100644 --- a/src/plugins/clawhub.ts +++ b/src/plugins/clawhub.ts @@ -25,6 +25,7 @@ import { type ClawHubPackageVersion, } from "../infra/clawhub.js"; import { formatErrorMessage } from "../infra/errors.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveCompatibilityHostVersion } from "../version.js"; import type { InstallSafetyOverrides } from "./install-security-scan.js"; import { installPluginFromArchive, type InstallPluginResult } from "./install.js"; @@ -164,7 +165,7 @@ function resolveRequestedVersion(params: { } function readTrimmedString(value: unknown): string | null { - return typeof value === "string" ? value.trim() : null; + return normalizeOptionalString(value) ?? null; } function normalizeClawHubRelativePath(value: unknown): string | null { diff --git a/src/plugins/conversation-binding.ts b/src/plugins/conversation-binding.ts index 808a3d4a4e6..aa686496511 100644 --- a/src/plugins/conversation-binding.ts +++ b/src/plugins/conversation-binding.ts @@ -349,8 +349,7 @@ function loadApprovalsFromDisk(): PluginBindingApprovalsFile { pluginId: typeof entry.pluginId === "string" ? entry.pluginId : "", pluginName: typeof entry.pluginName === "string" ? entry.pluginName : undefined, channel: typeof entry.channel === "string" ? normalizeChannel(entry.channel) : "", - accountId: - typeof entry.accountId === "string" ? entry.accountId.trim() || "default" : "default", + accountId: normalizeOptionalString(entry.accountId) ?? "default", approvedAt: typeof entry.approvedAt === "number" && Number.isFinite(entry.approvedAt) ? Math.floor(entry.approvedAt) diff --git a/src/plugins/install.ts b/src/plugins/install.ts index 227905705a8..7627f0aaeba 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -8,6 +8,7 @@ import { unscopedPackageName, } from "../infra/install-safe-path.js"; import { type NpmIntegrityDrift, type NpmSpecResolution } from "../infra/install-source-utils.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { CONFIG_DIR, resolveUserPath } from "../utils.js"; import type { InstallSecurityScanResult } from "./install-security-scan.js"; import type { InstallSafetyOverrides } from "./install-security-scan.js"; @@ -624,7 +625,7 @@ async function installPluginFromPackageDir( } const extensions = extensionsResult.entries; - const pkgName = typeof manifest.name === "string" ? manifest.name.trim() : ""; + const pkgName = normalizeOptionalString(manifest.name) ?? ""; const npmPluginId = pkgName || "plugin"; // Prefer the canonical `id` from openclaw.plugin.json over the npm package name. diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index be487ea7b0d..0622f1ccead 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -9,6 +9,7 @@ import type { PluginInstallRecord } from "../config/types.plugins.js"; import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; import { openBoundaryFileSync } from "../infra/boundary-file-read.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveUserPath } from "../utils.js"; import { buildPluginApi } from "./api-builder.js"; import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js"; @@ -855,8 +856,8 @@ function buildProvenanceIndex(params: { matcher: createPathMatcher(), }; const trackedPaths = [install.installPath, install.sourcePath] - .map((entry) => (typeof entry === "string" ? entry.trim() : "")) - .filter(Boolean); + .map((entry) => normalizeOptionalString(entry)) + .filter((entry): entry is string => Boolean(entry)); if (trackedPaths.length === 0) { rule.trackedWithoutPaths = true; } else { diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 7bedcf87b08..292e3b1c5f2 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -266,12 +266,9 @@ function mergePackageChannelMetaIntoChannelConfigs(params: { } const existing = params.channelConfigs[channelId]; - const label = - existing.label ?? - (typeof params.packageChannel?.label === "string" ? params.packageChannel.label.trim() : ""); + const label = existing.label ?? normalizeOptionalString(params.packageChannel?.label) ?? ""; const description = - existing.description ?? - (typeof params.packageChannel?.blurb === "string" ? params.packageChannel.blurb.trim() : ""); + existing.description ?? normalizeOptionalString(params.packageChannel?.blurb) ?? ""; const preferOver = existing.preferOver ?? normalizePreferredPluginIds(params.packageChannel?.preferOver); diff --git a/src/plugins/provider-auth-input.ts b/src/plugins/provider-auth-input.ts index 63894318323..dd15600bb58 100644 --- a/src/plugins/provider-auth-input.ts +++ b/src/plugins/provider-auth-input.ts @@ -1,7 +1,10 @@ import { resolveEnvApiKey } from "../agents/model-auth-env.js"; import type { OpenClawConfig } from "../config/types.js"; import type { SecretInput } from "../config/types.secrets.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeStringifiedOptionalString, +} from "../shared/string-coerce.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { resolveSecretInputModeForEnvSelection, @@ -29,7 +32,7 @@ export { const DEFAULT_KEY_PREVIEW = { head: 4, tail: 4 }; export function normalizeApiKeyInput(raw: string): string { - const trimmed = String(raw ?? "").trim(); + const trimmed = normalizeStringifiedOptionalString(raw) ?? ""; if (!trimmed) { return ""; } diff --git a/src/plugins/provider-auth-ref.ts b/src/plugins/provider-auth-ref.ts index 167b7d4a42c..f91a2a1e7bc 100644 --- a/src/plugins/provider-auth-ref.ts +++ b/src/plugins/provider-auth-ref.ts @@ -9,7 +9,10 @@ import { isValidFileSecretRefId, resolveDefaultSecretProviderAlias, } from "../secrets/ref-contract.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "../shared/string-coerce.js"; import type { WizardPrompter } from "../wizard/prompts.js"; let secretResolvePromise: Promise | undefined; @@ -109,7 +112,7 @@ async function promptEnvSecretRefForSetup(params: { return undefined; }, }); - const envCandidate = String(envVarRaw ?? "").trim(); + const envCandidate = normalizeStringifiedOptionalString(envVarRaw) ?? ""; const envVar = envCandidate && isValidEnvSecretRefId(envCandidate) ? envCandidate : params.defaultEnvVar; if (!envVar) { @@ -218,7 +221,7 @@ async function promptProviderSecretRefForSetup(params: { return undefined; }, }); - const id = String(idRaw ?? "").trim() || idDefault; + const id = normalizeStringifiedOptionalString(idRaw) || idDefault; const ref: SecretRef = { source: providerEntry.source, provider: selectedProvider, diff --git a/src/plugins/provider-catalog.ts b/src/plugins/provider-catalog.ts index d1153601b9c..66e5f9daab7 100644 --- a/src/plugins/provider-catalog.ts +++ b/src/plugins/provider-catalog.ts @@ -1,6 +1,9 @@ import { normalizeProviderId } from "../agents/provider-id.js"; import type { ModelProviderConfig } from "../config/types.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import type { ProviderCatalogContext, ProviderCatalogResult } from "./types.js"; export function findCatalogTemplate(params: { @@ -37,8 +40,7 @@ export async function buildSingleProviderApiKeyCatalog(params: { ([configuredProviderId]) => normalizeProviderId(configuredProviderId) === providerId, )?.[1] : undefined; - const explicitBaseUrl = - typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : ""; + const explicitBaseUrl = normalizeOptionalString(explicitProvider?.baseUrl) ?? ""; return { provider: { diff --git a/src/plugins/provider-self-hosted-setup.ts b/src/plugins/provider-self-hosted-setup.ts index 04e303fbe76..42e92d51459 100644 --- a/src/plugins/provider-self-hosted-setup.ts +++ b/src/plugins/provider-self-hosted-setup.ts @@ -8,6 +8,10 @@ import { import type { OpenClawConfig } from "../config/config.js"; import type { ModelDefinitionConfig } from "../config/types.models.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "../shared/string-coerce.js"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { applyAuthProfileConfig } from "./provider-auth-helpers.js"; @@ -53,7 +57,7 @@ export async function discoverOpenAICompatibleLocalModels(params: { const url = `${trimmedBaseUrl}/models`; try { - const trimmedApiKey = params.apiKey?.trim(); + const trimmedApiKey = normalizeOptionalString(params.apiKey); const response = await fetch(url, { headers: trimmedApiKey ? { Authorization: `Bearer ${trimmedApiKey}` } : undefined, signal: AbortSignal.timeout(5000), @@ -70,7 +74,7 @@ export async function discoverOpenAICompatibleLocalModels(params: { } return models - .map((model) => ({ id: typeof model.id === "string" ? model.id.trim() : "" })) + .map((model) => ({ id: normalizeOptionalString(model.id) ?? "" })) .filter((model) => Boolean(model.id)) .map((model) => { const modelId = model.id; @@ -218,8 +222,8 @@ export async function promptAndConfigureOpenAICompatibleSelfHostedProvider( const baseUrl = String(baseUrlRaw ?? "") .trim() .replace(/\/+$/, ""); - const apiKey = String(apiKeyRaw ?? "").trim(); - const modelId = String(modelIdRaw ?? "").trim(); + const apiKey = normalizeStringifiedOptionalString(apiKeyRaw) ?? ""; + const modelId = normalizeStringifiedOptionalString(modelIdRaw) ?? ""; const credential: AuthProfileCredential = { type: "api_key", provider: params.providerId, diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index efc0c4f33ad..187b5a936bb 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -15,7 +15,10 @@ import { NODE_SYSTEM_RUN_COMMANDS, } from "../infra/node-commands.js"; import { normalizePluginGatewayMethodScope } from "../shared/gateway-method-policy.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "../shared/string-coerce.js"; import { resolveUserPath } from "../utils.js"; import { buildPluginApi } from "./api-builder.js"; import { registerPluginCommand, validatePluginCommandDefinition } from "./command-registration.js"; @@ -611,7 +614,8 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { ? (registration as OpenClawPluginChannelRegistration) : { plugin: registration as ChannelPlugin }; const plugin = normalized.plugin; - const id = typeof plugin?.id === "string" ? plugin.id.trim() : String(plugin?.id ?? "").trim(); + const id = + normalizeOptionalString(plugin?.id) ?? normalizeStringifiedOptionalString(plugin?.id) ?? ""; if (!id) { pushDiagnostic({ level: "error",