diff --git a/src/cli/command-secret-gateway.ts b/src/cli/command-secret-gateway.ts index b61f1af7284..5228ce0baec 100644 --- a/src/cli/command-secret-gateway.ts +++ b/src/cli/command-secret-gateway.ts @@ -393,7 +393,7 @@ function collectInactiveSurfacePathsFromDiagnostics(diagnostics: string[]): Set< } function isUnsupportedSecretsResolveError(err: unknown): boolean { - const message = formatErrorMessage(err).toLowerCase(); + const message = normalizeLowercaseStringOrEmpty(formatErrorMessage(err)); if (!message.includes("secrets.resolve")) { return false; } diff --git a/src/cli/completion-cli.ts b/src/cli/completion-cli.ts index 08875a7f327..565597b9763 100644 --- a/src/cli/completion-cli.ts +++ b/src/cli/completion-cli.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { Command, Option } from "commander"; import { resolveStateDir } from "../config/paths.js"; import { routeLogsToStderr } from "../logging/console.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; import { pathExists } from "../utils.js"; @@ -24,7 +25,7 @@ function isCompletionShell(value: string): value is CompletionShell { export function resolveShellFromEnv(env: NodeJS.ProcessEnv = process.env): CompletionShell { const shellPath = env.SHELL?.trim() ?? ""; - const shellName = shellPath ? path.basename(shellPath).toLowerCase() : ""; + const shellName = shellPath ? normalizeLowercaseStringOrEmpty(path.basename(shellPath)) : ""; if (shellName === "zsh") { return "zsh"; } diff --git a/src/cli/cron-cli/register.cron-add.ts b/src/cli/cron-cli/register.cron-add.ts index 4aed799e61a..c1bd62f454c 100644 --- a/src/cli/cron-cli/register.cron-add.ts +++ b/src/cli/cron-cli/register.cron-add.ts @@ -2,7 +2,10 @@ 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 { + normalizeLowercaseStringOrEmpty, + 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"; @@ -170,7 +173,7 @@ export function registerCronAddCommand(cron: Command) { const sessionTarget = sessionSource === "cli" ? sessionTargetRaw || "" : inferredSessionTarget; const isCustomSessionTarget = - sessionTarget.toLowerCase().startsWith("session:") && + normalizeLowercaseStringOrEmpty(sessionTarget).startsWith("session:") && sessionTarget.slice(8).trim().length > 0; const isIsolatedLikeSessionTarget = sessionTarget === "isolated" || sessionTarget === "current" || isCustomSessionTarget; diff --git a/src/cli/cron-cli/shared.ts b/src/cli/cron-cli/shared.ts index f9d0c6d5af2..d1160fde747 100644 --- a/src/cli/cron-cli/shared.ts +++ b/src/cli/cron-cli/shared.ts @@ -9,6 +9,7 @@ import { parseOffsetlessIsoDateTimeInTimeZone, } from "../../infra/format-time/parse-offsetless-zoned-datetime.js"; import { defaultRuntime, type RuntimeEnv } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { colorize, isRich, theme } from "../../terminal/theme.js"; import type { GatewayRpcOpts } from "../gateway-rpc.js"; import { callGatewayFromCli } from "../gateway-rpc.js"; @@ -67,7 +68,7 @@ export function parseDurationMs(input: string): number | null { if (!Number.isFinite(n) || n <= 0) { return null; } - const unit = (match[2] ?? "").toLowerCase(); + const unit = normalizeLowercaseStringOrEmpty(match[2] ?? ""); const factor = unit === "ms" ? 1 diff --git a/src/cli/daemon-cli/restart-health.ts b/src/cli/daemon-cli/restart-health.ts index c73d99c03e8..b326f01af38 100644 --- a/src/cli/daemon-cli/restart-health.ts +++ b/src/cli/daemon-cli/restart-health.ts @@ -8,7 +8,10 @@ import { type PortUsage, } from "../../infra/ports.js"; import { killProcessTree } from "../../process/kill-tree.js"; -import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; import { sleep } from "../../utils.js"; export const DEFAULT_RESTART_HEALTH_TIMEOUT_MS = 60_000; @@ -56,7 +59,7 @@ function looksLikeAuthClose(code: number | undefined, reason: string | undefined if (code !== 1008) { return false; } - const normalized = (reason ?? "").toLowerCase(); + const normalized = normalizeLowercaseStringOrEmpty(reason); return ( normalized.includes("auth") || normalized.includes("token") || diff --git a/src/cli/mcp-cli.ts b/src/cli/mcp-cli.ts index 2e6a4702a9d..013ec1c8935 100644 --- a/src/cli/mcp-cli.ts +++ b/src/cli/mcp-cli.ts @@ -8,6 +8,7 @@ import { } from "../config/mcp-config.js"; import { serveOpenClawChannelMcp } from "../mcp/channel-server.js"; import { defaultRuntime } from "../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; function fail(message: string): never { @@ -83,9 +84,9 @@ export function registerMcpCli(program: Command) { if (opts.password) { warnSecretCliFlag("--password"); } - const claudeChannelMode = String(opts.claudeChannelMode ?? "auto") - .trim() - .toLowerCase(); + const claudeChannelMode = normalizeLowercaseStringOrEmpty( + String(opts.claudeChannelMode ?? "auto").trim(), + ); if ( claudeChannelMode !== "auto" && claudeChannelMode !== "on" && diff --git a/src/cli/nodes-cli/register.camera.ts b/src/cli/nodes-cli/register.camera.ts index de6b0791a3c..a44d01b80d7 100644 --- a/src/cli/nodes-cli/register.camera.ts +++ b/src/cli/nodes-cli/register.camera.ts @@ -1,5 +1,6 @@ import type { Command } from "commander"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { getTerminalTableWidth, renderTable } from "../../terminal/table.js"; import { shortenHomePath } from "../../utils.js"; import { @@ -22,9 +23,7 @@ import { import type { NodesRpcOpts } from "./types.js"; const parseFacing = (value: string): CameraFacing => { - const v = String(value ?? "") - .trim() - .toLowerCase(); + const v = normalizeLowercaseStringOrEmpty(String(value ?? "").trim()); if (v === "front" || v === "back") { return v; } @@ -115,9 +114,7 @@ export function registerNodesCameraCommands(nodes: Command) { await runNodesCommand("camera snap", async () => { const node = await resolveNode(opts, String(opts.node ?? "")); const nodeId = node.nodeId; - const facingOpt = String(opts.facing ?? "both") - .trim() - .toLowerCase(); + const facingOpt = normalizeLowercaseStringOrEmpty(String(opts.facing ?? "both").trim()); const facings: CameraFacing[] = facingOpt === "both" ? ["front", "back"] diff --git a/src/cli/nodes-cli/register.canvas.ts b/src/cli/nodes-cli/register.canvas.ts index 79b6e9f7ce3..0a89b52059e 100644 --- a/src/cli/nodes-cli/register.canvas.ts +++ b/src/cli/nodes-cli/register.canvas.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import type { Command } from "commander"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { shortenHomePath } from "../../utils.js"; import { writeBase64ToFile } from "../nodes-camera.js"; import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../nodes-canvas.js"; @@ -41,9 +42,7 @@ export function registerNodesCanvasCommands(nodes: Command) { .option("--invoke-timeout ", "Node invoke timeout in ms (default 20000)", "20000") .action(async (opts: NodesRpcOpts) => { await runNodesCommand("canvas snapshot", async () => { - const formatOpt = String(opts.format ?? "jpg") - .trim() - .toLowerCase(); + const formatOpt = normalizeLowercaseStringOrEmpty(String(opts.format ?? "jpg").trim()); const formatForParams = formatOpt === "jpg" ? "jpeg" : formatOpt === "jpeg" ? "jpeg" : "png"; if (formatForParams !== "png" && formatForParams !== "jpeg") { diff --git a/src/cli/nodes-cli/register.invoke.ts b/src/cli/nodes-cli/register.invoke.ts index e663fd90075..3af69166496 100644 --- a/src/cli/nodes-cli/register.invoke.ts +++ b/src/cli/nodes-cli/register.invoke.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import { randomIdempotencyKey } from "../../gateway/call.js"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { getNodesTheme, runNodesCommand } from "./cli-utils.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; @@ -27,7 +28,7 @@ export function registerNodesInvokeCommands(nodes: Command) { defaultRuntime.exit(1); return; } - if (BLOCKED_NODE_INVOKE_COMMANDS.has(command.toLowerCase())) { + if (BLOCKED_NODE_INVOKE_COMMANDS.has(normalizeLowercaseStringOrEmpty(command))) { throw new Error( `command "${command}" is reserved for shell execution; use the exec tool with host=node instead`, ); diff --git a/src/cli/nodes-cli/register.status.ts b/src/cli/nodes-cli/register.status.ts index 5139df03a67..36030450428 100644 --- a/src/cli/nodes-cli/register.status.ts +++ b/src/cli/nodes-cli/register.status.ts @@ -3,6 +3,7 @@ import { formatErrorMessage } from "../../infra/errors.js"; import { formatTimeAgo } from "../../infra/format-time/format-relative.ts"; import { defaultRuntime } from "../../runtime.js"; import { + normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, } from "../../shared/string-coerce.js"; @@ -20,7 +21,7 @@ function formatVersionLabel(raw: string) { if (!trimmed) { return raw; } - if (trimmed.toLowerCase().startsWith("v")) { + if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("v")) { return trimmed; } return /^\d/.test(trimmed) ? `v${trimmed}` : trimmed; diff --git a/src/cli/nodes-cli/rpc.ts b/src/cli/nodes-cli/rpc.ts index a3c71145861..a8bbbe7d7c4 100644 --- a/src/cli/nodes-cli/rpc.ts +++ b/src/cli/nodes-cli/rpc.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; import type { Command } from "commander"; import { resolveNodeFromNodeList } from "../../shared/node-resolve.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { parseNodeList, parsePairingList } from "./format.js"; import type { NodeListNode, NodesRpcOpts } from "./types.js"; @@ -50,7 +51,7 @@ export function buildNodeInvokeParams(params: { } export function unauthorizedHintForMessage(message: string): string | null { - const haystack = message.toLowerCase(); + const haystack = normalizeLowercaseStringOrEmpty(message); if ( haystack.includes("unauthorizedclient") || haystack.includes("bridge client is not authorized") || diff --git a/src/cli/outbound-send-mapping.ts b/src/cli/outbound-send-mapping.ts index e6dcd8f4228..cdcd6ff7ffb 100644 --- a/src/cli/outbound-send-mapping.ts +++ b/src/cli/outbound-send-mapping.ts @@ -1,5 +1,6 @@ import { normalizeAnyChannelId } from "../channels/registry.js"; import type { OutboundSendDeps } from "../infra/outbound/deliver.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; /** * CLI-internal send function sources, keyed by channel ID. @@ -8,12 +9,13 @@ import type { OutboundSendDeps } from "../infra/outbound/deliver.js"; export type CliOutboundSendSource = { [channelId: string]: unknown }; function normalizeLegacyChannelStem(raw: string): string { - return raw - .replace(/([a-z0-9])([A-Z])/g, "$1-$2") - .replace(/_/g, "-") - .trim() - .toLowerCase() - .replace(/-/g, ""); + const normalized = normalizeLowercaseStringOrEmpty( + raw + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .replace(/_/g, "-") + .trim(), + ); + return normalized.replace(/-/g, ""); } function resolveChannelIdFromLegacySourceKey(key: string): string | undefined { diff --git a/src/cli/pairing-cli.ts b/src/cli/pairing-cli.ts index b106f45fc0c..eaf0a8f72fe 100644 --- a/src/cli/pairing-cli.ts +++ b/src/cli/pairing-cli.ts @@ -9,6 +9,7 @@ import { type PairingChannel, } from "../pairing/pairing-store.js"; import { defaultRuntime } from "../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { formatDocsLink } from "../terminal/links.js"; import { getTerminalTableWidth, renderTable } from "../terminal/table.js"; import { theme } from "../terminal/theme.js"; @@ -16,15 +17,14 @@ import { formatCliCommand } from "./command-format.js"; /** Parse channel, allowing extension channels not in core registry. */ function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel { - const value = ( - typeof raw === "string" + const value = normalizeLowercaseStringOrEmpty( + (typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" ? String(raw) : "" - ) - .trim() - .toLowerCase(); + ).trim(), + ); if (!value) { throw new Error("Channel required"); } diff --git a/src/cli/parse-bytes.ts b/src/cli/parse-bytes.ts index db993a292f7..cb48a18c324 100644 --- a/src/cli/parse-bytes.ts +++ b/src/cli/parse-bytes.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + export type BytesParseOptions = { defaultUnit?: "b" | "kb" | "mb" | "gb" | "tb"; }; @@ -15,9 +17,7 @@ const UNIT_MULTIPLIERS: Record = { }; export function parseByteSize(raw: string, opts?: BytesParseOptions): number { - const trimmed = String(raw ?? "") - .trim() - .toLowerCase(); + const trimmed = normalizeLowercaseStringOrEmpty(String(raw ?? "").trim()); if (!trimmed) { throw new Error("invalid byte size (empty)"); } @@ -32,7 +32,7 @@ export function parseByteSize(raw: string, opts?: BytesParseOptions): number { throw new Error(`invalid byte size: ${raw}`); } - const unit = (m[2] ?? opts?.defaultUnit ?? "b").toLowerCase(); + const unit = normalizeLowercaseStringOrEmpty(m[2] ?? opts?.defaultUnit ?? "b"); const multiplier = UNIT_MULTIPLIERS[unit]; if (!multiplier) { throw new Error(`invalid byte size unit: ${raw}`); diff --git a/src/cli/parse-duration.ts b/src/cli/parse-duration.ts index 4ad673fb39c..ab9cb6f5ef7 100644 --- a/src/cli/parse-duration.ts +++ b/src/cli/parse-duration.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + export type DurationMsParseOptions = { defaultUnit?: "ms" | "s" | "m" | "h" | "d"; }; @@ -11,9 +13,7 @@ const DURATION_MULTIPLIERS: Record = { }; export function parseDurationMs(raw: string, opts?: DurationMsParseOptions): number { - const trimmed = String(raw ?? "") - .trim() - .toLowerCase(); + const trimmed = normalizeLowercaseStringOrEmpty(String(raw ?? "").trim()); if (!trimmed) { throw new Error("invalid duration (empty)"); } diff --git a/src/cli/plugins-command-helpers.ts b/src/cli/plugins-command-helpers.ts index ef842c6c913..9540f52eb9c 100644 --- a/src/cli/plugins-command-helpers.ts +++ b/src/cli/plugins-command-helpers.ts @@ -6,6 +6,7 @@ import { CLAWHUB_INSTALL_ERROR_CODE } from "../plugins/clawhub.js"; import { applyExclusiveSlotSelection } from "../plugins/slots.js"; import { buildPluginDiagnosticsReport } from "../plugins/status.js"; import { defaultRuntime } from "../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { theme } from "../terminal/theme.js"; type HookInternalEntryLike = Record & { enabled?: boolean }; @@ -14,7 +15,7 @@ export function resolveFileNpmSpecToLocalPath( raw: string, ): { ok: true; path: string } | { ok: false; error: string } | null { const trimmed = raw.trim(); - if (!trimmed.toLowerCase().startsWith("file:")) { + if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("file:")) { return null; } const rest = trimmed.slice("file:".length); diff --git a/src/cli/profile-utils.ts b/src/cli/profile-utils.ts index 2e89a8243fd..2ad1ee9910e 100644 --- a/src/cli/profile-utils.ts +++ b/src/cli/profile-utils.ts @@ -1,3 +1,5 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i; export function isValidProfileName(value: string): boolean { @@ -13,7 +15,7 @@ export function normalizeProfileName(raw?: string | null): string | null { if (!profile) { return null; } - if (profile.toLowerCase() === "default") { + if (normalizeLowercaseStringOrEmpty(profile) === "default") { return null; } if (!isValidProfileName(profile)) { diff --git a/src/cli/profile.ts b/src/cli/profile.ts index 189eacf046d..9881a25c16e 100644 --- a/src/cli/profile.ts +++ b/src/cli/profile.ts @@ -2,6 +2,7 @@ import os from "node:os"; import path from "node:path"; import { FLAG_TERMINATOR } from "../infra/cli-root-options.js"; import { resolveRequiredHomeDir } from "../infra/home-dir.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { resolveCliArgvInvocation } from "./argv-invocation.js"; import { isValidProfileName } from "./profile-utils.js"; import { forwardConsumedCliRootOption } from "./root-option-forward.js"; @@ -83,7 +84,7 @@ function resolveProfileStateDir( env: Record, homedir: () => string, ): string { - const suffix = profile.toLowerCase() === "default" ? "" : `-${profile}`; + const suffix = normalizeLowercaseStringOrEmpty(profile) === "default" ? "" : `-${profile}`; return path.join(resolveRequiredHomeDir(env as NodeJS.ProcessEnv, homedir), `.openclaw${suffix}`); } diff --git a/src/cli/program/register.agent.ts b/src/cli/program/register.agent.ts index e5847f3c164..c5cfebfdb84 100644 --- a/src/cli/program/register.agent.ts +++ b/src/cli/program/register.agent.ts @@ -11,6 +11,7 @@ import { } from "../../commands/agents.js"; import { setVerbose } from "../../globals.js"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { formatDocsLink } from "../../terminal/links.js"; import { theme } from "../../terminal/theme.js"; import { runCommandWithRuntime } from "../cli-utils.js"; @@ -73,7 +74,8 @@ ${formatHelpExamples([ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.openclaw.ai/cli/agent")}`, ) .action(async (opts) => { - const verboseLevel = typeof opts.verbose === "string" ? opts.verbose.toLowerCase() : ""; + const verboseLevel = + typeof opts.verbose === "string" ? normalizeLowercaseStringOrEmpty(opts.verbose) : ""; setVerbose(verboseLevel === "on"); // Build default deps (keeps parity with other commands; future-proofing). const deps = createDefaultDeps(); diff --git a/src/cli/update-cli/progress.ts b/src/cli/update-cli/progress.ts index 676231a0e7f..1a0c13676a9 100644 --- a/src/cli/update-cli/progress.ts +++ b/src/cli/update-cli/progress.ts @@ -6,6 +6,7 @@ import type { UpdateStepProgress, } from "../../infra/update-runner.js"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { theme } from "../../terminal/theme.js"; import type { UpdateCommandOptions } from "./shared.js"; @@ -72,7 +73,7 @@ export function inferUpdateFailureHints(result: UpdateRunResult): string[] { return []; } - const stderr = (failedStep.stderrTail ?? "").toLowerCase(); + const stderr = normalizeLowercaseStringOrEmpty(failedStep.stderrTail); const hints: string[] = []; if (failedStep.name.startsWith("global update") && stderr.includes("eacces")) { diff --git a/src/cli/update-cli/shared.ts b/src/cli/update-cli/shared.ts index 3f2046b870b..8efbee06739 100644 --- a/src/cli/update-cli/shared.ts +++ b/src/cli/update-cli/shared.ts @@ -19,6 +19,7 @@ import { import type { UpdateStepProgress, UpdateStepResult } from "../../infra/update-runner.js"; import { runCommandWithTimeout } from "../../process/exec.js"; import { defaultRuntime } from "../../runtime.js"; +import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { theme } from "../../terminal/theme.js"; import { pathExists } from "../../utils.js"; @@ -129,7 +130,7 @@ function resolveDefaultGitDir(): string { } export function resolveNodeRunner(): string { - const base = path.basename(process.execPath).toLowerCase(); + const base = normalizeLowercaseStringOrEmpty(path.basename(process.execPath)); if (base === "node" || base === "node.exe") { return process.execPath; } diff --git a/src/cli/windows-argv.ts b/src/cli/windows-argv.ts index 14c1645b665..b7f9b70bb4b 100644 --- a/src/cli/windows-argv.ts +++ b/src/cli/windows-argv.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; export function normalizeWindowsArgv(argv: string[]): string[] { if (process.platform !== "win32") { @@ -28,8 +29,8 @@ export function normalizeWindowsArgv(argv: string[]): string[] { normalizeArg(value).replace(/^\\\\\\?\\/, ""); const execPath = normalizeCandidate(process.execPath); - const execPathLower = execPath.toLowerCase(); - const execBase = path.basename(execPath).toLowerCase(); + const execPathLower = normalizeLowercaseStringOrEmpty(execPath); + const execBase = normalizeLowercaseStringOrEmpty(path.basename(execPath)); const isExecPath = (value: string | undefined): boolean => { if (!value) { return false; @@ -38,7 +39,7 @@ export function normalizeWindowsArgv(argv: string[]): string[] { if (!normalized) { return false; } - const lower = normalized.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(normalized); return ( lower === execPathLower || path.basename(lower) === execBase || diff --git a/src/infra/dispatch-wrapper-resolution.ts b/src/infra/dispatch-wrapper-resolution.ts index a75f625ac6d..5a946492767 100644 --- a/src/infra/dispatch-wrapper-resolution.ts +++ b/src/infra/dispatch-wrapper-resolution.ts @@ -1,3 +1,4 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { normalizeExecutableToken } from "./exec-wrapper-tokens.js"; export const MAX_DISPATCH_WRAPPER_DEPTH = 4; @@ -128,7 +129,7 @@ function scanWrapperInvocation( idx += 1; break; } - const directive = params.onToken(token, token.toLowerCase()); + const directive = params.onToken(token, normalizeLowercaseStringOrEmpty(token)); if (directive === "stop") { break; } @@ -197,7 +198,7 @@ function envInvocationUsesModifiers(argv: string[]): boolean { if (!token.startsWith("-") || token === "-") { break; } - const lower = token.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(token); const [flag] = lower.split("=", 2); if (ENV_FLAG_OPTIONS.has(flag)) { return true; diff --git a/src/infra/restart-stale-pids.ts b/src/infra/restart-stale-pids.ts index fa06729c878..b8ce4ad9b48 100644 --- a/src/infra/restart-stale-pids.ts +++ b/src/infra/restart-stale-pids.ts @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process"; import path from "node:path"; import { resolveGatewayPort } from "../config/paths.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { isGatewayArgv } from "./gateway-process-argv.js"; import { resolveLsofCommandSync } from "./ports-lsof.js"; import { @@ -70,7 +71,11 @@ function parsePidsFromLsofOutput(stdout: string): number[] { let currentCmd: string | undefined; for (const line of stdout.split(/\r?\n/).filter(Boolean)) { if (line.startsWith("p")) { - if (currentPid != null && currentCmd && currentCmd.toLowerCase().includes("openclaw")) { + if ( + currentPid != null && + currentCmd && + normalizeLowercaseStringOrEmpty(currentCmd).includes("openclaw") + ) { pids.push(currentPid); } const parsed = Number.parseInt(line.slice(1), 10); @@ -80,7 +85,11 @@ function parsePidsFromLsofOutput(stdout: string): number[] { currentCmd = line.slice(1); } } - if (currentPid != null && currentCmd && currentCmd.toLowerCase().includes("openclaw")) { + if ( + currentPid != null && + currentCmd && + normalizeLowercaseStringOrEmpty(currentCmd).includes("openclaw") + ) { pids.push(currentPid); } // Deduplicate: dual-stack listeners (IPv4 + IPv6) cause lsof to emit the diff --git a/src/plugin-sdk/bluebubbles.ts b/src/plugin-sdk/bluebubbles.ts index bbaf2cfb783..3fd81753e5b 100644 --- a/src/plugin-sdk/bluebubbles.ts +++ b/src/plugin-sdk/bluebubbles.ts @@ -1,5 +1,6 @@ import type { ChannelStatusIssue } from "../channels/plugins/types.js"; import type { OpenClawConfig } from "../config/config.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { parseChatTargetPrefixesOrThrow, resolveServicePrefixedTarget, @@ -83,7 +84,7 @@ function stripBlueBubblesPrefix(value: string): string { if (!trimmed) { return ""; } - if (!trimmed.toLowerCase().startsWith("bluebubbles:")) { + if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("bluebubbles:")) { return trimmed; } return trimmed.slice("bluebubbles:".length).trim(); @@ -135,7 +136,7 @@ function normalizeBlueBubblesHandle(raw: string): string { if (!trimmed) { return ""; } - const lowered = trimmed.toLowerCase(); + const lowered = normalizeLowercaseStringOrEmpty(trimmed); if (lowered.startsWith("imessage:")) { return normalizeBlueBubblesHandle(trimmed.slice(9)); } @@ -146,7 +147,7 @@ function normalizeBlueBubblesHandle(raw: string): string { return normalizeBlueBubblesHandle(trimmed.slice(5)); } if (trimmed.includes("@")) { - return trimmed.toLowerCase(); + return normalizeLowercaseStringOrEmpty(trimmed); } return trimmed.replace(/\s+/g, ""); } @@ -167,7 +168,7 @@ function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { if (!trimmed) { throw new Error("BlueBubbles target is required"); } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); const servicePrefixed = resolveServicePrefixedTarget({ trimmed, diff --git a/src/plugin-sdk/channel-entry-contract.ts b/src/plugin-sdk/channel-entry-contract.ts index 907bf58d068..8e4323f2667 100644 --- a/src/plugin-sdk/channel-entry-contract.ts +++ b/src/plugin-sdk/channel-entry-contract.ts @@ -13,6 +13,7 @@ import { resolvePluginLoaderJitiConfig, } from "../plugins/sdk-alias.js"; import type { AnyAgentTool, OpenClawPluginApi, PluginCommandContext } from "../plugins/types.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; export type { AnyAgentTool, OpenClawPluginApi, PluginCommandContext }; @@ -68,7 +69,7 @@ const jitiLoaders = new Map>(); const loadedModuleExports = new Map(); function resolveSpecifierCandidates(modulePath: string): string[] { - const ext = path.extname(modulePath).toLowerCase(); + const ext = normalizeLowercaseStringOrEmpty(path.extname(modulePath)); if (ext === ".js") { return [modulePath, modulePath.slice(0, -3) + ".ts"]; } @@ -279,7 +280,7 @@ function loadBundledEntryModuleSync(importMetaUrl: string, specifier: string): u if ( process.platform === "win32" && modulePath.includes(`${path.sep}dist${path.sep}`) && - [".js", ".mjs", ".cjs"].includes(path.extname(modulePath).toLowerCase()) + [".js", ".mjs", ".cjs"].includes(normalizeLowercaseStringOrEmpty(path.extname(modulePath))) ) { try { loaded = nodeRequire(modulePath); diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 81128579541..4d9b1945b12 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -25,6 +25,7 @@ import { buildOutboundBaseSessionKey } from "../infra/outbound/base-session-key. import type { OutboundDeliveryResult } from "../infra/outbound/deliver.js"; import type { PluginRuntime } from "../plugins/runtime/types.js"; import type { OpenClawPluginApi } from "../plugins/types.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; export type { AnyAgentTool, @@ -237,8 +238,8 @@ export function getChatChannelMeta(id: ChatChannelId): ChannelMeta { export function stripChannelTargetPrefix(raw: string, ...providers: string[]): string { const trimmed = raw.trim(); for (const provider of providers) { - const prefix = `${provider.toLowerCase()}:`; - if (trimmed.toLowerCase().startsWith(prefix)) { + const prefix = `${normalizeLowercaseStringOrEmpty(provider)}:`; + if (normalizeLowercaseStringOrEmpty(trimmed).startsWith(prefix)) { return trimmed.slice(prefix.length).trim(); } } diff --git a/src/plugin-sdk/speech.ts b/src/plugin-sdk/speech.ts index 8320ab22d29..3115da936f1 100644 --- a/src/plugin-sdk/speech.ts +++ b/src/plugin-sdk/speech.ts @@ -1,4 +1,5 @@ import { rmSync } from "node:fs"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; // Public speech helpers for bundled or third-party plugins. // @@ -55,7 +56,7 @@ export function normalizeLanguageCode(code?: string): string | undefined { if (!trimmed) { return undefined; } - const normalized = trimmed.toLowerCase(); + const normalized = normalizeLowercaseStringOrEmpty(trimmed); if (!/^[a-z]{2}$/.test(normalized)) { throw new Error("languageCode must be a 2-letter ISO 639-1 code (e.g. en, de, fr)"); } @@ -67,7 +68,7 @@ export function normalizeApplyTextNormalization(mode?: string): "auto" | "on" | if (!trimmed) { return undefined; } - const normalized = trimmed.toLowerCase(); + const normalized = normalizeLowercaseStringOrEmpty(trimmed); if (normalized === "auto" || normalized === "on" || normalized === "off") { return normalized; }