diff --git a/packages/plugin-package-contract/src/index.ts b/packages/plugin-package-contract/src/index.ts index e5506684e78..10c016742a1 100644 --- a/packages/plugin-package-contract/src/index.ts +++ b/packages/plugin-package-contract/src/index.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalString } from "../../../src/shared/string-coerce.js"; import { isRecord } from "../../../src/utils.js"; export type JsonObject = Record; @@ -25,7 +26,7 @@ export const EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS = [ ] as const; function getTrimmedString(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value.trim() : undefined; + return normalizeOptionalString(value); } function readOpenClawBlock(packageJson: unknown) { diff --git a/src/acp/meta.ts b/src/acp/meta.ts index eccd865dbd5..112d9a462be 100644 --- a/src/acp/meta.ts +++ b/src/acp/meta.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "../shared/string-coerce.js"; + export function readString( meta: Record | null | undefined, keys: string[], @@ -6,9 +8,9 @@ export function readString( return undefined; } for (const key of keys) { - const value = meta[key]; - if (typeof value === "string" && value.trim()) { - return value.trim(); + const value = normalizeOptionalString(meta[key]); + if (value) { + return value; } } return undefined; diff --git a/src/agents/cli-runner/bundle-mcp.ts b/src/agents/cli-runner/bundle-mcp.ts index 3494c6a76a7..50136a77a03 100644 --- a/src/agents/cli-runner/bundle-mcp.ts +++ b/src/agents/cli-runner/bundle-mcp.ts @@ -10,6 +10,7 @@ import { loadEnabledBundleMcpConfig, type BundleMcpConfig, } from "../../plugins/bundle-mcp.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; type PreparedCliBundleMcpConfig = { backend: CliBackendConfig; @@ -34,12 +35,10 @@ function findMcpConfigPath(args?: string[]): string | undefined { for (let i = 0; i < args.length; i += 1) { const arg = args[i] ?? ""; if (arg === "--mcp-config") { - const next = args[i + 1]; - return typeof next === "string" && next.trim() ? next.trim() : undefined; + return normalizeOptionalString(args[i + 1]); } if (arg.startsWith("--mcp-config=")) { - const inline = arg.slice("--mcp-config=".length).trim(); - return inline || undefined; + return normalizeOptionalString(arg.slice("--mcp-config=".length)); } } return undefined; diff --git a/src/cli/cron-cli/register.cron-add.ts b/src/cli/cron-cli/register.cron-add.ts index 968c217a7bc..4aed799e61a 100644 --- a/src/cli/cron-cli/register.cron-add.ts +++ b/src/cli/cron-cli/register.cron-add.ts @@ -2,6 +2,7 @@ import type { Command } from "commander"; import type { CronJob } from "../../cron/types.js"; import { sanitizeAgentId } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import type { GatewayRpcOpts } from "../gateway-rpc.js"; import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js"; import { parsePositiveIntOrUndefined } from "../program/helpers.js"; @@ -144,12 +145,8 @@ export function registerCronAddCommand(cron: Command) { return { kind: "agentTurn" as const, message, - model: - typeof opts.model === "string" && opts.model.trim() ? opts.model.trim() : undefined, - thinking: - typeof opts.thinking === "string" && opts.thinking.trim() - ? opts.thinking.trim() - : undefined, + model: normalizeOptionalString(opts.model), + thinking: normalizeOptionalString(opts.thinking), timeoutSeconds: timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : undefined, lightContext: opts.lightContext === true ? true : undefined, @@ -250,7 +247,7 @@ export function registerCronAddCommand(cron: Command) { typeof opts.channel === "string" && opts.channel.trim() ? opts.channel.trim() : undefined, - to: typeof opts.to === "string" && opts.to.trim() ? opts.to.trim() : undefined, + to: normalizeOptionalString(opts.to), accountId, bestEffort: opts.bestEffortDeliver ? true : undefined, } diff --git a/src/cli/cron-cli/register.cron-edit.ts b/src/cli/cron-cli/register.cron-edit.ts index 8c0553291fe..26c45c4c6b6 100644 --- a/src/cli/cron-cli/register.cron-edit.ts +++ b/src/cli/cron-cli/register.cron-edit.ts @@ -3,6 +3,7 @@ import type { CronJob } from "../../cron/types.js"; import { danger } from "../../globals.js"; import { sanitizeAgentId } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js"; import { applyExistingCronSchedulePatch, @@ -169,12 +170,8 @@ export function registerCronEditCommand(cron: Command) { } const hasSystemEventPatch = typeof opts.systemEvent === "string"; - const model = - typeof opts.model === "string" && opts.model.trim() ? opts.model.trim() : undefined; - const thinking = - typeof opts.thinking === "string" && opts.thinking.trim() - ? opts.thinking.trim() - : undefined; + const model = normalizeOptionalString(opts.model); + const thinking = normalizeOptionalString(opts.thinking); const timeoutSeconds = opts.timeoutSeconds ? Number.parseInt(String(opts.timeoutSeconds), 10) : undefined; diff --git a/src/gateway/agent-list.ts b/src/gateway/agent-list.ts index d14cdf0c534..69362ffa08b 100644 --- a/src/gateway/agent-list.ts +++ b/src/gateway/agent-list.ts @@ -5,6 +5,7 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; import type { SessionScope } from "../config/sessions.js"; import { normalizeAgentId, normalizeMainKey } from "../routing/session-key.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; export type GatewayAgentListRow = { id: string; @@ -62,7 +63,7 @@ export function listGatewayAgentsBasic(cfg: OpenClawConfig): { continue; } configuredById.set(normalizeAgentId(entry.id), { - name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined, + name: normalizeOptionalString(entry.name), }); } const explicitIds = new Set( diff --git a/src/gateway/hooks.ts b/src/gateway/hooks.ts index e8a773a6470..8b85e2d2b1a 100644 --- a/src/gateway/hooks.ts +++ b/src/gateway/hooks.ts @@ -7,6 +7,7 @@ import type { OpenClawConfig } from "../config/config.js"; import { readJsonBodyWithLimit, requestBodyErrorToText } from "../infra/http-body.js"; import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js"; import type { HookExternalContentSource } from "../security/external-content.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import { type HookMappingResolved, resolveHookMappings } from "./hooks-mapping.js"; import { resolveAllowedAgentIds } from "./hooks-policy.js"; @@ -363,30 +364,27 @@ export function normalizeAgentPayload(payload: Record): return { ok: false, error: "message required" }; } const nameRaw = payload.name; - const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "Hook"; + const name = normalizeOptionalString(nameRaw) ?? "Hook"; const agentIdRaw = payload.agentId; - const agentId = - typeof agentIdRaw === "string" && agentIdRaw.trim() ? agentIdRaw.trim() : undefined; + const agentId = normalizeOptionalString(agentIdRaw); const idempotencyKey = resolveOptionalHookIdempotencyKey(payload.idempotencyKey); const wakeMode = payload.wakeMode === "next-heartbeat" ? "next-heartbeat" : "now"; const sessionKeyRaw = payload.sessionKey; - const sessionKey = - typeof sessionKeyRaw === "string" && sessionKeyRaw.trim() ? sessionKeyRaw.trim() : undefined; + const sessionKey = normalizeOptionalString(sessionKeyRaw); const channel = resolveHookChannel(payload.channel); if (!channel) { return { ok: false, error: getHookChannelError() }; } const toRaw = payload.to; - const to = typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined; + const to = normalizeOptionalString(toRaw); const modelRaw = payload.model; - const model = typeof modelRaw === "string" && modelRaw.trim() ? modelRaw.trim() : undefined; + const model = normalizeOptionalString(modelRaw); if (modelRaw !== undefined && !model) { return { ok: false, error: "model required" }; } const deliver = resolveHookDeliver(payload.deliver); const thinkingRaw = payload.thinking; - const thinking = - typeof thinkingRaw === "string" && thinkingRaw.trim() ? thinkingRaw.trim() : undefined; + const thinking = normalizeOptionalString(thinkingRaw); const timeoutRaw = payload.timeoutSeconds; const timeoutSeconds = typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw) && timeoutRaw > 0 diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 285002ea7bd..4fb99fbb878 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -28,6 +28,7 @@ import { classifySessionKeyShape, normalizeAgentId } from "../../routing/session import { defaultRuntime } from "../../runtime.js"; import { normalizeInputProvenance, type InputProvenance } from "../../sessions/input-provenance.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { createRunningTaskRun } from "../../tasks/task-executor.js"; import { normalizeDeliveryContext, @@ -653,25 +654,11 @@ export const agentHandlers: GatewayRequestHandlers = { const wantsDelivery = request.deliver === true; const explicitTo = - typeof request.replyTo === "string" && request.replyTo.trim() - ? request.replyTo.trim() - : typeof request.to === "string" && request.to.trim() - ? request.to.trim() - : undefined; - const explicitThreadId = - typeof request.threadId === "string" && request.threadId.trim() - ? request.threadId.trim() - : undefined; - const turnSourceChannel = - typeof request.channel === "string" && request.channel.trim() - ? request.channel.trim() - : undefined; - const turnSourceTo = - typeof request.to === "string" && request.to.trim() ? request.to.trim() : undefined; - const turnSourceAccountId = - typeof request.accountId === "string" && request.accountId.trim() - ? request.accountId.trim() - : undefined; + normalizeOptionalString(request.replyTo) ?? normalizeOptionalString(request.to); + const explicitThreadId = normalizeOptionalString(request.threadId); + const turnSourceChannel = normalizeOptionalString(request.channel); + const turnSourceTo = normalizeOptionalString(request.to); + const turnSourceAccountId = normalizeOptionalString(request.accountId); const deliveryPlan = resolveAgentDeliveryPlan({ sessionEntry, requestedChannel: request.replyChannel ?? request.channel, diff --git a/src/gateway/server-methods/agents.ts b/src/gateway/server-methods/agents.ts index 8fd00f58ccd..46239307906 100644 --- a/src/gateway/server-methods/agents.ts +++ b/src/gateway/server-methods/agents.ts @@ -37,6 +37,7 @@ import { assertNoPathAliasEscape } from "../../infra/path-alias-guards.js"; import { isNotFoundPathError } from "../../infra/path-guards.js"; import { movePathToTrash } from "../../plugin-sdk/browser-maintenance.js"; import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { resolveUserPath } from "../../utils.js"; import { ErrorCodes, @@ -396,7 +397,7 @@ function sanitizeIdentityLine(value: string): string { } function resolveOptionalStringParam(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value.trim() : undefined; + return normalizeOptionalString(value); } function respondInvalidMethodParams( diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 3d241a557ee..7ec2d0e2731 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -9,8 +9,8 @@ import { waitForEmbeddedPiRunEnd, } from "../../agents/pi-embedded-runner/runs.js"; import { compactEmbeddedPiSession } from "../../agents/pi-embedded.js"; -import { normalizeReasoningLevel, normalizeThinkLevel } from "../../auto-reply/thinking.js"; import { clearSessionQueues } from "../../auto-reply/reply/queue/cleanup.js"; +import { normalizeReasoningLevel, normalizeThinkLevel } from "../../auto-reply/thinking.js"; import { loadConfig } from "../../config/config.js"; import { loadSessionStore, @@ -33,6 +33,7 @@ import { resolveAgentIdFromSessionKey, toAgentStoreSessionKey, } from "../../routing/session-key.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { GATEWAY_CLIENT_IDS } from "../protocol/client-info.js"; import { ErrorCodes, @@ -772,18 +773,13 @@ export const sessionsHandlers: GatewayRequestHandlers = { } const p = params; const cfg = loadConfig(); - const requestedKey = typeof p.key === "string" && p.key.trim() ? p.key.trim() : undefined; + const requestedKey = normalizeOptionalString(p.key); const agentId = normalizeAgentId( - typeof p.agentId === "string" && p.agentId.trim() ? p.agentId : resolveDefaultAgentId(cfg), + normalizeOptionalString(p.agentId) ?? resolveDefaultAgentId(cfg), ); if (requestedKey) { const requestedAgentId = parseAgentSessionKey(requestedKey)?.agentId; - if ( - requestedAgentId && - requestedAgentId !== agentId && - typeof p.agentId === "string" && - p.agentId.trim() - ) { + if (requestedAgentId && requestedAgentId !== agentId && normalizeOptionalString(p.agentId)) { respond( false, undefined, @@ -795,10 +791,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { return; } } - const parentSessionKey = - typeof p.parentSessionKey === "string" && p.parentSessionKey.trim() - ? p.parentSessionKey.trim() - : undefined; + const parentSessionKey = normalizeOptionalString(p.parentSessionKey); let canonicalParentSessionKey: string | undefined; if (parentSessionKey) { const parent = loadSessionEntry(parentSessionKey); diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index d844d3a4ddd..3d97628e0b4 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -54,6 +54,7 @@ import { isWorkspaceRelativeAvatarPath, resolveAvatarMime, } from "../shared/avatar-policy.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js"; import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js"; import { @@ -666,7 +667,7 @@ export function listAgentsForGateway(cfg: OpenClawConfig): { } : undefined; configuredById.set(normalizeAgentId(entry.id), { - name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : undefined, + name: normalizeOptionalString(entry.name), identity, }); } diff --git a/src/infra/clawhub.ts b/src/infra/clawhub.ts index 46c76407bfc..9cf5e3a593e 100644 --- a/src/infra/clawhub.ts +++ b/src/infra/clawhub.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { isAtLeast, parseSemver } from "./runtime-guard.js"; import { compareComparableSemver, parseComparableSemver } from "./semver-compare.js"; import { createTempDownloadTarget } from "./temp-download.js"; @@ -207,7 +208,7 @@ function normalizeBaseUrl(baseUrl?: string): string { } function readNonEmptyString(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value.trim() : undefined; + return normalizeOptionalString(value); } function extractTokenFromClawHubConfig(value: unknown): string | undefined { diff --git a/src/infra/tailscale.ts b/src/infra/tailscale.ts index 4f6adb63e0f..0d4c865b922 100644 --- a/src/infra/tailscale.ts +++ b/src/infra/tailscale.ts @@ -4,6 +4,7 @@ import { promptYesNo } from "../cli/prompt.js"; import { danger, info, logVerbose, shouldLogVerbose, warn } from "../globals.js"; import { runExec } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { colorize, isRich, theme } from "../terminal/theme.js"; import { ensureBinary } from "./binaries.js"; @@ -435,7 +436,7 @@ export async function disableTailscaleFunnel(exec: typeof runExec = runExec) { } function getString(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value.trim() : undefined; + return normalizeOptionalString(value); } function readRecord(value: unknown): Record | null {