diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index 262c3bde5e5..affda75716b 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -11,7 +11,11 @@ import { parseAgentSessionKey, resolveAgentIdFromSessionKey, } from "../routing/session-key.js"; -import { readStringValue, resolvePrimaryStringValue } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + readStringValue, + resolvePrimaryStringValue, +} from "../shared/string-coerce.js"; import { resolveUserPath } from "../utils.js"; import { resolveEffectiveAgentSkillFilter } from "./skills/agent-filter.js"; import { resolveDefaultAgentWorkspaceDir } from "./workspace.js"; @@ -105,11 +109,10 @@ export function resolveSessionAgentIds(params: { sessionAgentId: string; } { const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); - const explicitAgentIdRaw = - typeof params.agentId === "string" ? params.agentId.trim().toLowerCase() : ""; + const explicitAgentIdRaw = normalizeLowercaseStringOrEmpty(params.agentId); const explicitAgentId = explicitAgentIdRaw ? normalizeAgentId(explicitAgentIdRaw) : null; const sessionKey = params.sessionKey?.trim(); - const normalizedSessionKey = sessionKey ? sessionKey.toLowerCase() : undefined; + const normalizedSessionKey = sessionKey ? normalizeLowercaseStringOrEmpty(sessionKey) : undefined; const parsed = normalizedSessionKey ? parseAgentSessionKey(normalizedSessionKey) : null; const sessionAgentId = explicitAgentId ?? (parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId); diff --git a/src/agents/identity-file.ts b/src/agents/identity-file.ts index 5942589a2bf..0bbd47f3bf7 100644 --- a/src/agents/identity-file.ts +++ b/src/agents/identity-file.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { DEFAULT_IDENTITY_FILENAME } from "./workspace.js"; export type AgentIdentityFile = { @@ -26,8 +27,7 @@ function normalizeIdentityValue(value: string): string { normalized = normalized.slice(1, -1).trim(); } normalized = normalized.replace(/[\u2013\u2014]/g, "-"); - normalized = normalized.replace(/\s+/g, " ").toLowerCase(); - return normalized; + return normalizeLowercaseStringOrEmpty(normalized.replace(/\s+/g, " ")); } function isIdentityPlaceholder(value: string): boolean { @@ -44,7 +44,9 @@ export function parseIdentityMarkdown(content: string): AgentIdentityFile { if (colonIndex === -1) { continue; } - const label = cleaned.slice(0, colonIndex).replace(/[*_]/g, "").trim().toLowerCase(); + const label = normalizeLowercaseStringOrEmpty( + cleaned.slice(0, colonIndex).replace(/[*_]/g, ""), + ); const value = cleaned .slice(colonIndex + 1) .replace(/^[*_]+|[*_]+$/g, "") diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.ts b/src/agents/pi-embedded-subscribe.handlers.tools.ts index 17a7597c077..8bc47eff622 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.ts +++ b/src/agents/pi-embedded-subscribe.handlers.tools.ts @@ -20,7 +20,7 @@ import type { ExecApprovalDecision } from "../infra/exec-approvals.js"; import { splitMediaFromOutput } from "../media/parse.js"; import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; import type { PluginHookAfterToolCallEvent } from "../plugins/types.js"; -import { readStringValue } from "../shared/string-coerce.js"; +import { normalizeOptionalLowercaseString, readStringValue } from "../shared/string-coerce.js"; import type { ApplyPatchSummary } from "./apply-patch.js"; import type { ExecToolDetails } from "./bash-tools.exec-types.js"; import { parseExecApprovalResultText } from "./exec-approval-result.js"; @@ -63,7 +63,7 @@ function isCronAddAction(args: unknown): boolean { return false; } const action = (args as Record).action; - return typeof action === "string" && action.trim().toLowerCase() === "add"; + return normalizeOptionalLowercaseString(action) === "add"; } function buildToolCallSummary(toolName: string, args: unknown, meta?: string): ToolCallSummary { @@ -180,7 +180,7 @@ function buildPatchSummaryText(summary: ApplyPatchSummary): string { } function extendExecMeta(toolName: string, args: unknown, meta?: string): string | undefined { - const normalized = toolName.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(toolName); if (normalized !== "exec" && normalized !== "bash") { return meta; } diff --git a/src/agents/pi-embedded-subscribe.tools.ts b/src/agents/pi-embedded-subscribe.tools.ts index 02dbc37ba91..3306dcbe684 100644 --- a/src/agents/pi-embedded-subscribe.tools.ts +++ b/src/agents/pi-embedded-subscribe.tools.ts @@ -37,7 +37,7 @@ function normalizeToolErrorText(text: string): string | undefined { } function isErrorLikeStatus(status: string): boolean { - const normalized = status.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(status); if (!normalized) { return false; } diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index cc6d2ef594f..9e9eef1a38a 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -8,6 +8,10 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js"; import type { AgentToolsConfig } from "../config/types.tools.js"; import { normalizeAgentId } from "../routing/session-key.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "../shared/string-coerce.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js"; import type { AnyAgentTool } from "./pi-tools.types.js"; @@ -128,7 +132,7 @@ type ToolPolicyConfig = { }; function normalizeProviderKey(value: string): string { - return value.trim().toLowerCase(); + return normalizeLowercaseStringOrEmpty(value); } function resolveGroupContextFromSessionKey(sessionKey?: string | null): { @@ -167,7 +171,7 @@ function resolveGroupContextFromSessionKey(sessionKey?: string | null): { if (!groupId) { return {}; } - return { channel: channel.trim().toLowerCase(), groupId }; + return { channel: normalizeLowercaseStringOrEmpty(channel), groupId }; } function resolveProviderToolPolicy(params: { @@ -195,7 +199,7 @@ function resolveProviderToolPolicy(params: { } const normalizedProvider = normalizeProviderKey(provider); - const rawModelId = params.modelId?.trim().toLowerCase(); + const rawModelId = normalizeOptionalLowercaseString(params.modelId); const fullModelId = rawModelId && !rawModelId.includes("/") ? `${normalizedProvider}/${rawModelId}` : rawModelId; diff --git a/src/agents/subagent-capabilities.ts b/src/agents/subagent-capabilities.ts index cb9b386d97f..1da0f799222 100644 --- a/src/agents/subagent-capabilities.ts +++ b/src/agents/subagent-capabilities.ts @@ -2,6 +2,7 @@ import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js"; import type { OpenClawConfig } from "../config/config.js"; import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { isSubagentSessionKey, parseAgentSessionKey } from "../routing/session-key.js"; +import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import { getSubagentDepthFromSessionStore } from "./subagent-depth.js"; import { normalizeSubagentSessionKey } from "./subagent-session-key.js"; @@ -19,18 +20,12 @@ type SessionCapabilityEntry = { }; function normalizeSubagentRole(value: unknown): SubagentSessionRole | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim().toLowerCase(); + const trimmed = normalizeOptionalLowercaseString(value); return SUBAGENT_SESSION_ROLES.find((entry) => entry === trimmed); } function normalizeSubagentControlScope(value: unknown): SubagentControlScope | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim().toLowerCase(); + const trimmed = normalizeOptionalLowercaseString(value); return SUBAGENT_CONTROL_SCOPES.find((entry) => entry === trimmed); } diff --git a/src/agents/tool-display-common.ts b/src/agents/tool-display-common.ts index 7025317444c..fcb280ddd8d 100644 --- a/src/agents/tool-display-common.ts +++ b/src/agents/tool-display-common.ts @@ -1,4 +1,7 @@ -import { normalizeOptionalString } from "../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../shared/string-coerce.js"; import { resolveExecDetail } from "./tool-display-exec.js"; import { asRecord } from "./tool-display-record.js"; @@ -165,7 +168,7 @@ export function formatDetailKey(raw: string, overrides: Record = } const cleaned = last.replace(/_/g, " ").replace(/-/g, " "); const spaced = cleaned.replace(/([a-z0-9])([A-Z])/g, "$1 $2"); - return spaced.trim().toLowerCase() || last.toLowerCase(); + return normalizeLowercaseStringOrEmpty(spaced) || normalizeLowercaseStringOrEmpty(last); } export function resolvePathArg(args: unknown): string | undefined { diff --git a/src/agents/tool-display-exec-shell.ts b/src/agents/tool-display-exec-shell.ts index 2de6eb477b8..742107e4abc 100644 --- a/src/agents/tool-display-exec-shell.ts +++ b/src/agents/tool-display-exec-shell.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + type PreambleResult = { command: string; chdirPath?: string; @@ -82,7 +84,7 @@ export function binaryName(token: string | undefined): string | undefined { } const cleaned = stripOuterQuotes(token) ?? token; const segment = cleaned.split(/[/]/).at(-1) ?? cleaned; - return segment.trim().toLowerCase(); + return normalizeLowercaseStringOrEmpty(segment); } export function optionValue(words: string[], names: string[]): string | undefined { diff --git a/src/agents/tools/image-generate-tool.ts b/src/agents/tools/image-generate-tool.ts index ceae69bfc15..a2f04e2086a 100644 --- a/src/agents/tools/image-generate-tool.ts +++ b/src/agents/tools/image-generate-tool.ts @@ -16,6 +16,7 @@ import { getImageMetadata } from "../../media/image-ops.js"; import { saveMediaBuffer } from "../../media/store.js"; import { loadWebMedia } from "../../media/web-media.js"; import { getProviderEnvVars } from "../../secrets/provider-env-vars.js"; +import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; import { resolveUserPath } from "../../utils.js"; import { normalizeProviderId } from "../provider-id.js"; import { ToolInputError, readNumberParam, readStringParam } from "./common.js"; @@ -213,7 +214,7 @@ function resolveAction(args: Record): "generate" | "list" { if (!raw) { return "generate"; } - const normalized = raw.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(raw); if (normalized === "generate" || normalized === "list") { return normalized; } diff --git a/src/agents/tools/music-generate-tool.ts b/src/agents/tools/music-generate-tool.ts index edd66ab2207..ed68f44e034 100644 --- a/src/agents/tools/music-generate-tool.ts +++ b/src/agents/tools/music-generate-tool.ts @@ -17,6 +17,7 @@ import type { MusicGenerationSourceImage, } from "../../music-generation/types.js"; import { readSnakeCaseParamRaw } from "../../param-key.js"; +import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; import { resolveUserPath } from "../../utils.js"; import type { DeliveryContext } from "../../utils/delivery-context.js"; import { @@ -144,7 +145,7 @@ function resolveAction(args: Record): "generate" | "list" | "st if (!raw) { return "generate"; } - const normalized = raw.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(raw); if (normalized === "generate" || normalized === "list" || normalized === "status") { return normalized; } @@ -157,7 +158,7 @@ function readBooleanParam(params: Record, key: string): boolean return raw; } if (typeof raw === "string") { - const normalized = raw.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(raw); if (normalized === "true") { return true; } @@ -169,7 +170,9 @@ function readBooleanParam(params: Record, key: string): boolean } function normalizeOutputFormat(raw: string | undefined): MusicGenerationOutputFormat | undefined { - const normalized = raw?.trim().toLowerCase() as MusicGenerationOutputFormat | undefined; + const normalized = normalizeOptionalLowercaseString(raw) as + | MusicGenerationOutputFormat + | undefined; if (!normalized) { return undefined; } diff --git a/src/agents/tools/nodes-tool-commands.ts b/src/agents/tools/nodes-tool-commands.ts index cede378579d..fc604713849 100644 --- a/src/agents/tools/nodes-tool-commands.ts +++ b/src/agents/tools/nodes-tool-commands.ts @@ -1,6 +1,7 @@ import crypto from "node:crypto"; import { parseTimeoutMs } from "../../cli/parse-timeout.js"; import { formatErrorMessage } from "../../infra/errors.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { jsonResult, readStringParam } from "./common.js"; import type { GatewayCallOptions } from "./gateway.js"; import { callGatewayTool } from "./gateway.js"; @@ -53,10 +54,7 @@ export async function executeNodeCommandAction(params: { case "notifications_action": { const node = readStringParam(params.input, "node", { required: true }); const notificationKey = readStringParam(params.input, "notificationKey", { required: true }); - const notificationAction = - typeof params.input.notificationAction === "string" - ? params.input.notificationAction.trim().toLowerCase() - : ""; + const notificationAction = normalizeLowercaseStringOrEmpty(params.input.notificationAction); if ( notificationAction !== "open" && notificationAction !== "dismiss" && @@ -118,7 +116,7 @@ export async function executeNodeCommandAction(params: { const node = readStringParam(params.input, "node", { required: true }); const nodeId = await resolveNodeId(params.gatewayOpts, node); const invokeCommand = readStringParam(params.input, "invokeCommand", { required: true }); - const invokeCommandNormalized = invokeCommand.trim().toLowerCase(); + const invokeCommandNormalized = normalizeLowercaseStringOrEmpty(invokeCommand); if (BLOCKED_INVOKE_COMMANDS.has(invokeCommandNormalized)) { throw new Error( `invokeCommand "${invokeCommand}" is reserved for shell execution; use exec with host=node instead`, diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index 79407983294..4c99f51d136 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -8,7 +8,7 @@ import { } from "../../config/sessions.js"; import { callGateway } from "../../gateway/call.js"; import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; -import { readStringValue } from "../../shared/string-coerce.js"; +import { normalizeOptionalLowercaseString, readStringValue } from "../../shared/string-coerce.js"; import { describeSessionsListTool, SESSIONS_LIST_TOOL_DISPLAY_SUMMARY, @@ -75,9 +75,9 @@ export function createSessionsListTool(opts?: { sandboxed: opts?.sandboxed === true, }); - const kindsRaw = readStringArrayParam(params, "kinds")?.map((value) => - value.trim().toLowerCase(), - ); + const kindsRaw = readStringArrayParam(params, "kinds") + ?.map((value) => normalizeOptionalLowercaseString(value)) + .filter(Boolean); const allowedKindsList = (kindsRaw ?? []).filter((value) => ["main", "group", "cron", "hook", "node", "other"].includes(value), ); diff --git a/src/agents/tools/video-generate-tool.ts b/src/agents/tools/video-generate-tool.ts index 73eb7b48892..7eb211711c5 100644 --- a/src/agents/tools/video-generate-tool.ts +++ b/src/agents/tools/video-generate-tool.ts @@ -6,6 +6,7 @@ import { createSubsystemLogger } from "../../logging/subsystem.js"; import { saveMediaBuffer } from "../../media/store.js"; import { loadWebMedia } from "../../media/web-media.js"; import { readSnakeCaseParamRaw } from "../../param-key.js"; +import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; import { resolveUserPath } from "../../utils.js"; import type { DeliveryContext } from "../../utils/delivery-context.js"; import { @@ -163,7 +164,7 @@ function resolveAction(args: Record): "generate" | "list" | "st if (!raw) { return "generate"; } - const normalized = raw.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(raw); if (normalized === "generate" || normalized === "list" || normalized === "status") { return normalized; } @@ -205,7 +206,7 @@ function readBooleanParam(params: Record, key: string): boolean return raw; } if (typeof raw === "string") { - const normalized = raw.trim().toLowerCase(); + const normalized = normalizeOptionalLowercaseString(raw); if (normalized === "true") { return true; } diff --git a/src/agents/tools/web-shared.ts b/src/agents/tools/web-shared.ts index da0fbb38beb..d335df4a927 100644 --- a/src/agents/tools/web-shared.ts +++ b/src/agents/tools/web-shared.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; + export type CacheEntry = { value: T; expiresAt: number; @@ -20,7 +22,7 @@ export function resolveCacheTtlMs(value: unknown, fallbackMinutes: number): numb } export function normalizeCacheKey(value: string): string { - return value.trim().toLowerCase(); + return normalizeLowercaseStringOrEmpty(value); } export function readCache(