diff --git a/src/infra/approval-native-target-key.ts b/src/infra/approval-native-target-key.ts index 9013787ba08..46f9804b663 100644 --- a/src/infra/approval-native-target-key.ts +++ b/src/infra/approval-native-target-key.ts @@ -1,7 +1,8 @@ import type { ChannelApprovalNativeTarget } from "../channels/plugins/types.adapters.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export function buildChannelApprovalNativeTargetKey(target: ChannelApprovalNativeTarget): string { - return `${target.to.trim()}\u0000${ - target.threadId == null ? "" : String(target.threadId).trim() + return `${normalizeOptionalString(target.to) ?? ""}\u0000${ + target.threadId == null ? "" : (normalizeOptionalString(String(target.threadId)) ?? "") }`; } diff --git a/src/infra/bonjour-errors.ts b/src/infra/bonjour-errors.ts index 1703f2696bf..5db4d89451e 100644 --- a/src/infra/bonjour-errors.ts +++ b/src/infra/bonjour-errors.ts @@ -1,7 +1,9 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + export function formatBonjourError(err: unknown): string { if (err instanceof Error) { const trimmedMessage = err.message.trim(); - const msg = trimmedMessage || err.name || String(err).trim(); + const msg = trimmedMessage || err.name || (normalizeOptionalString(String(err)) ?? ""); if (err.name && err.name !== "Error") { return msg === err.name ? err.name : `${err.name}: ${msg}`; } diff --git a/src/infra/gateway-discovery-targets.ts b/src/infra/gateway-discovery-targets.ts index 169eed62a07..dbc374e765a 100644 --- a/src/infra/gateway-discovery-targets.ts +++ b/src/infra/gateway-discovery-targets.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveGatewayDiscoveryEndpoint, type GatewayBonjourBeacon, @@ -25,13 +26,14 @@ export function buildGatewayDiscoveryTarget( ): GatewayDiscoveryTarget { const endpoint = resolveGatewayDiscoveryEndpoint(beacon); const sshPort = pickSshPort(beacon); - const sshUser = opts?.sshUser?.trim() ?? ""; + const sshUser = normalizeOptionalString(opts?.sshUser) ?? ""; const baseSshTarget = endpoint ? (sshUser ? `${sshUser}@${endpoint.host}` : endpoint.host) : null; const sshTarget = baseSshTarget && sshPort && sshPort !== 22 ? `${baseSshTarget}:${sshPort}` : baseSshTarget; return { - title: (beacon.displayName || beacon.instanceName || "Gateway").trim(), - domain: (beacon.domain || "local.").trim(), + title: + normalizeOptionalString(beacon.displayName || beacon.instanceName || "Gateway") ?? "Gateway", + domain: normalizeOptionalString(beacon.domain || "local.") ?? "local.", endpoint, wsUrl: endpoint?.wsUrl ?? null, sshPort, diff --git a/src/infra/heartbeat-reason.ts b/src/infra/heartbeat-reason.ts index 447ca733e53..b6d4bde67c4 100644 --- a/src/infra/heartbeat-reason.ts +++ b/src/infra/heartbeat-reason.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + export type HeartbeatReasonKind = | "retry" | "interval" @@ -9,7 +11,7 @@ export type HeartbeatReasonKind = | "other"; function trimReason(reason?: string): string { - return typeof reason === "string" ? reason.trim() : ""; + return normalizeOptionalString(reason) ?? ""; } export function normalizeHeartbeatWakeReason(reason?: string): string { diff --git a/src/infra/heartbeat-summary.ts b/src/infra/heartbeat-summary.ts index 89650de44a6..e26bafb73ce 100644 --- a/src/infra/heartbeat-summary.ts +++ b/src/infra/heartbeat-summary.ts @@ -8,6 +8,7 @@ import { parseDurationMs } from "../cli/parse-duration.js"; import type { OpenClawConfig } from "../config/config.js"; import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js"; import { normalizeAgentId } from "../routing/session-key.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; type HeartbeatConfig = AgentDefaultsConfig["heartbeat"]; @@ -53,7 +54,7 @@ export function resolveHeartbeatIntervalMs( if (!raw) { return null; } - const trimmed = String(raw).trim(); + const trimmed = normalizeOptionalString(String(raw)) ?? ""; if (!trimmed) { return null; } diff --git a/src/infra/heartbeat-wake.ts b/src/infra/heartbeat-wake.ts index 3aaaca5ed96..08affbd4947 100644 --- a/src/infra/heartbeat-wake.ts +++ b/src/infra/heartbeat-wake.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { isHeartbeatActionWakeReason, normalizeHeartbeatWakeReason, @@ -71,7 +72,7 @@ function normalizeWakeReason(reason?: string): string { } function normalizeWakeTarget(value?: string): string | undefined { - const trimmed = typeof value === "string" ? value.trim() : ""; + const trimmed = normalizeOptionalString(value) ?? ""; return trimmed || undefined; } diff --git a/src/infra/machine-name.ts b/src/infra/machine-name.ts index 51b4a66e8bd..4c3a50b9ac0 100644 --- a/src/infra/machine-name.ts +++ b/src/infra/machine-name.ts @@ -1,6 +1,7 @@ import { execFile } from "node:child_process"; import os from "node:os"; import { promisify } from "node:util"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; const execFileAsync = promisify(execFile); @@ -12,7 +13,7 @@ async function tryScutil(key: "ComputerName" | "LocalHostName") { timeout: 1000, windowsHide: true, }); - const value = String(stdout ?? "").trim(); + const value = normalizeOptionalString(String(stdout ?? "")) ?? ""; return value.length > 0 ? value : null; } catch { return null; @@ -20,7 +21,7 @@ async function tryScutil(key: "ComputerName" | "LocalHostName") { } function fallbackHostName() { - const trimmed = os.hostname().trim(); + const trimmed = normalizeOptionalString(os.hostname()) ?? ""; return trimmed.replace(/\.local$/i, "") || "openclaw"; } diff --git a/src/infra/outbound/agent-delivery.ts b/src/infra/outbound/agent-delivery.ts index 1eedcb69568..ee79b27afe3 100644 --- a/src/infra/outbound/agent-delivery.ts +++ b/src/infra/outbound/agent-delivery.ts @@ -1,6 +1,7 @@ import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { normalizeAccountId } from "../../utils/account-id.js"; import { INTERNAL_MESSAGE_CHANNEL, @@ -47,15 +48,11 @@ export function resolveAgentDeliveryPlan(params: { /** Turn-source `threadId` — paired with `turnSourceChannel`. */ turnSourceThreadId?: string | number; }): AgentDeliveryPlan { - const requestedRaw = - typeof params.requestedChannel === "string" ? params.requestedChannel.trim() : ""; + const requestedRaw = normalizeOptionalString(params.requestedChannel) ?? ""; const normalizedRequested = requestedRaw ? normalizeMessageChannel(requestedRaw) : undefined; const requestedChannel = normalizedRequested || "last"; - const explicitTo = - typeof params.explicitTo === "string" && params.explicitTo.trim() - ? params.explicitTo.trim() - : undefined; + const explicitTo = normalizeOptionalString(params.explicitTo) ?? undefined; // Resolve turn-source channel for cross-channel safety. const normalizedTurnSource = params.turnSourceChannel @@ -65,10 +62,7 @@ export function resolveAgentDeliveryPlan(params: { normalizedTurnSource && isDeliverableMessageChannel(normalizedTurnSource) ? normalizedTurnSource : undefined; - const turnSourceTo = - typeof params.turnSourceTo === "string" && params.turnSourceTo.trim() - ? params.turnSourceTo.trim() - : undefined; + const turnSourceTo = normalizeOptionalString(params.turnSourceTo) ?? undefined; const turnSourceAccountId = normalizeAccountId(params.turnSourceAccountId); const turnSourceThreadId = params.turnSourceThreadId != null && params.turnSourceThreadId !== "" diff --git a/src/infra/outbound/channel-target.ts b/src/infra/outbound/channel-target.ts index f87f2b8f1b0..9de5deeb2ad 100644 --- a/src/infra/outbound/channel-target.ts +++ b/src/infra/outbound/channel-target.ts @@ -1,4 +1,7 @@ -import { hasNonEmptyString as sharedHasNonEmptyString } from "../../shared/string-coerce.js"; +import { + hasNonEmptyString as sharedHasNonEmptyString, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import { MESSAGE_ACTION_TARGET_MODE } from "./message-action-spec.js"; export const hasNonEmptyString = sharedHasNonEmptyString; @@ -13,7 +16,7 @@ export function applyTargetToParams(params: { action: string; args: Record; }): void { - const target = typeof params.args.target === "string" ? params.args.target.trim() : ""; + const target = normalizeOptionalString(params.args.target) ?? ""; const hasLegacyTo = hasNonEmptyString(params.args.to); const hasLegacyChannelId = hasNonEmptyString(params.args.channelId); const mode = diff --git a/src/infra/outbound/message-action-normalization.ts b/src/infra/outbound/message-action-normalization.ts index ff40ca45e6a..8e031336bef 100644 --- a/src/infra/outbound/message-action-normalization.ts +++ b/src/infra/outbound/message-action-normalization.ts @@ -2,6 +2,7 @@ import type { ChannelMessageActionName, ChannelThreadingToolContext, } from "../../channels/plugins/types.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { isDeliverableMessageChannel, normalizeMessageChannel, @@ -16,18 +17,16 @@ export function normalizeMessageActionInput(params: { }): Record { const normalizedArgs = { ...params.args }; const { action, toolContext } = params; - const explicitChannel = - typeof normalizedArgs.channel === "string" ? normalizedArgs.channel.trim() : ""; + const explicitChannel = normalizeOptionalString(normalizedArgs.channel) ?? ""; const inferredChannel = explicitChannel || normalizeMessageChannel(toolContext?.currentChannelProvider) || ""; - const explicitTarget = - typeof normalizedArgs.target === "string" ? normalizedArgs.target.trim() : ""; + const explicitTarget = normalizeOptionalString(normalizedArgs.target) ?? ""; const hasLegacyTargetFields = typeof normalizedArgs.to === "string" || typeof normalizedArgs.channelId === "string"; const hasLegacyTarget = - (typeof normalizedArgs.to === "string" && normalizedArgs.to.trim().length > 0) || - (typeof normalizedArgs.channelId === "string" && normalizedArgs.channelId.trim().length > 0); + (normalizeOptionalString(normalizedArgs.to) ?? "").length > 0 || + (normalizeOptionalString(normalizedArgs.channelId) ?? "").length > 0; if (explicitTarget && hasLegacyTargetFields) { delete normalizedArgs.to; @@ -40,16 +39,15 @@ export function normalizeMessageActionInput(params: { actionRequiresTarget(action) && !actionHasTarget(action, normalizedArgs, { channel: inferredChannel }) ) { - const inferredTarget = toolContext?.currentChannelId?.trim(); + const inferredTarget = normalizeOptionalString(toolContext?.currentChannelId); if (inferredTarget) { normalizedArgs.target = inferredTarget; } } if (!explicitTarget && actionRequiresTarget(action) && hasLegacyTarget) { - const legacyTo = typeof normalizedArgs.to === "string" ? normalizedArgs.to.trim() : ""; - const legacyChannelId = - typeof normalizedArgs.channelId === "string" ? normalizedArgs.channelId.trim() : ""; + const legacyTo = normalizeOptionalString(normalizedArgs.to) ?? ""; + const legacyChannelId = normalizeOptionalString(normalizedArgs.channelId) ?? ""; const legacyTarget = legacyTo || legacyChannelId; if (legacyTarget) { normalizedArgs.target = legacyTarget; diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 5a75268b7a6..0f462a92bd3 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -236,7 +236,7 @@ async function resolveActionTarget(params: { accountId?: string | null; }): Promise { let resolvedTarget: ResolvedMessagingTarget | undefined; - const toRaw = typeof params.args.to === "string" ? params.args.to.trim() : ""; + const toRaw = normalizeOptionalString(params.args.to) ?? ""; if (toRaw) { const resolved = await resolveResolvedTargetOrThrow({ cfg: params.cfg, @@ -247,8 +247,7 @@ async function resolveActionTarget(params: { params.args.to = resolved.to; resolvedTarget = resolved; } - const channelIdRaw = - typeof params.args.channelId === "string" ? params.args.channelId.trim() : ""; + const channelIdRaw = normalizeOptionalString(params.args.channelId) ?? ""; if (channelIdRaw) { const resolved = await resolveResolvedTargetOrThrow({ cfg: params.cfg, diff --git a/src/infra/outbound/message-action-spec.ts b/src/infra/outbound/message-action-spec.ts index 4d320dad020..c6b7d5a0f6a 100644 --- a/src/infra/outbound/message-action-spec.ts +++ b/src/infra/outbound/message-action-spec.ts @@ -1,6 +1,9 @@ import { getBootstrapChannelPlugin } from "../../channels/plugins/bootstrap-registry.js"; import type { ChannelMessageActionName } from "../../channels/plugins/types.js"; -import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; export type MessageActionTargetMode = "to" | "channelId" | "none"; @@ -109,11 +112,11 @@ export function actionHasTarget( params: Record, options?: { channel?: string }, ): boolean { - const to = typeof params.to === "string" ? params.to.trim() : ""; + const to = normalizeOptionalString(params.to) ?? ""; if (to) { return true; } - const channelId = typeof params.channelId === "string" ? params.channelId.trim() : ""; + const channelId = normalizeOptionalString(params.channelId) ?? ""; if (channelId) { return true; } @@ -125,7 +128,7 @@ export function actionHasTarget( spec.aliases.some((alias) => { const value = params[alias]; if (typeof value === "string") { - return value.trim().length > 0; + return Boolean(normalizeOptionalString(value)); } if (typeof value === "number") { return Number.isFinite(value); diff --git a/src/infra/plugin-install-path-warnings.ts b/src/infra/plugin-install-path-warnings.ts index 43cae1f36e4..0a6db81501f 100644 --- a/src/infra/plugin-install-path-warnings.ts +++ b/src/infra/plugin-install-path-warnings.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { PluginInstallRecord } from "../config/types.plugins.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export type PluginInstallPathIssue = { kind: "custom-path" | "missing-path"; @@ -16,7 +17,7 @@ function resolvePluginInstallCandidatePaths( } return [install.sourcePath, install.installPath] - .map((value) => (typeof value === "string" ? value.trim() : "")) + .map((value) => normalizeOptionalString(value) ?? "") .filter(Boolean); } diff --git a/src/infra/skills-remote.ts b/src/infra/skills-remote.ts index 8b0c95bcd13..26f12402f67 100644 --- a/src/infra/skills-remote.ts +++ b/src/infra/skills-remote.ts @@ -5,7 +5,10 @@ import { listAgentWorkspaceDirs } from "../agents/workspace-dirs.js"; import type { OpenClawConfig } from "../config/config.js"; import type { NodeRegistry } from "../gateway/node-registry.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { listNodePairing, updatePairedNodeMetadata } from "./node-pairing.js"; type RemoteNodeRecord = { @@ -214,12 +217,12 @@ function parseBinProbePayload(payloadJSON: string | null | undefined, payload?: ? (JSON.parse(payloadJSON) as { stdout?: unknown; bins?: unknown }) : (payload as { stdout?: unknown; bins?: unknown }); if (Array.isArray(parsed.bins)) { - return parsed.bins.map((bin) => String(bin).trim()).filter(Boolean); + return parsed.bins.map((bin) => normalizeOptionalString(String(bin)) ?? "").filter(Boolean); } if (typeof parsed.stdout === "string") { return parsed.stdout .split(/\r?\n/) - .map((line) => line.trim()) + .map((line) => normalizeOptionalString(line) ?? "") .filter(Boolean); } } catch { diff --git a/src/infra/system-events.ts b/src/infra/system-events.ts index 28411e42f1e..6bb55cb1f43 100644 --- a/src/infra/system-events.ts +++ b/src/infra/system-events.ts @@ -3,7 +3,10 @@ // events ephemeral. Events are session-scoped and require an explicit key. import { resolveGlobalMap } from "../shared/global-singleton.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { mergeDeliveryContext, normalizeDeliveryContext, @@ -38,7 +41,7 @@ type SystemEventOptions = { }; function requireSessionKey(key?: string | null): string { - const trimmed = typeof key === "string" ? key.trim() : ""; + const trimmed = normalizeOptionalString(key) ?? ""; if (!trimmed) { throw new Error("system events require a sessionKey"); } diff --git a/src/infra/system-presence.ts b/src/infra/system-presence.ts index 7cd7d40de94..70642c33fea 100644 --- a/src/infra/system-presence.ts +++ b/src/infra/system-presence.ts @@ -3,6 +3,7 @@ import os from "node:os"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, + normalizeOptionalString, } from "../shared/string-coerce.js"; import { resolveRuntimeServiceVersion } from "../version.js"; import { pickBestEffortPrimaryLanIPv4 } from "./network-discovery-display.js"; @@ -55,7 +56,7 @@ function initSelfPresence() { const res = spawnSync("sysctl", ["-n", "hw.model"], { encoding: "utf-8", }); - const out = typeof res.stdout === "string" ? res.stdout.trim() : ""; + const out = normalizeOptionalString(res.stdout) ?? ""; return out.length > 0 ? out : undefined; } return os.arch(); @@ -64,7 +65,7 @@ function initSelfPresence() { const res = spawnSync("sw_vers", ["-productVersion"], { encoding: "utf-8", }); - const out = typeof res.stdout === "string" ? res.stdout.trim() : ""; + const out = normalizeOptionalString(res.stdout) ?? ""; return out.length > 0 ? out : os.release(); }; const platform = (() => { @@ -178,7 +179,7 @@ function mergeStringList(...values: Array): string[] | und continue; } for (const item of list) { - const trimmed = String(item).trim(); + const trimmed = normalizeOptionalString(String(item)) ?? ""; if (trimmed) { out.add(trimmed); } diff --git a/src/infra/voicewake.ts b/src/infra/voicewake.ts index ee73c8e40a4..1362417789f 100644 --- a/src/infra/voicewake.ts +++ b/src/infra/voicewake.ts @@ -1,5 +1,6 @@ import path from "node:path"; import { resolveStateDir } from "../config/paths.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { createAsyncLock, readJsonFile, writeJsonAtomic } from "./json-files.js"; export type VoiceWakeConfig = { @@ -16,7 +17,7 @@ function resolvePath(baseDir?: string) { function sanitizeTriggers(triggers: string[] | undefined | null): string[] { const cleaned = (triggers ?? []) - .map((w) => (typeof w === "string" ? w.trim() : "")) + .map((w) => normalizeOptionalString(w) ?? "") .filter((w) => w.length > 0); return cleaned.length > 0 ? cleaned : DEFAULT_TRIGGERS; }