From cd313c7f67576c6c3cf6a63a32a647ebf0202dfc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 06:24:41 +0100 Subject: [PATCH] refactor: dedupe shared helper readers --- src/mcp/channel-shared.ts | 3 ++- src/shared/entry-metadata.ts | 4 +++- src/shared/gateway-bind-url.ts | 4 +++- src/shared/net/ip.ts | 7 ++++--- src/shared/node-match.ts | 13 ++++++++----- src/shared/string-coerce.ts | 2 +- src/shared/string-normalization.ts | 19 ++++++++++--------- 7 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/mcp/channel-shared.ts b/src/mcp/channel-shared.ts index a035d22d1a0..0eb0c6f25b4 100644 --- a/src/mcp/channel-shared.ts +++ b/src/mcp/channel-shared.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; export type ClaudeChannelMode = "off" | "on" | "auto"; @@ -125,7 +126,7 @@ export const ClaudePermissionRequestSchema = z.object({ }); export function toText(value: unknown): string | undefined { - return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; + return normalizeOptionalString(value); } export function resolveMessageId(entry: Record): string | undefined { diff --git a/src/shared/entry-metadata.ts b/src/shared/entry-metadata.ts index 692a4c83567..265d11c7dba 100644 --- a/src/shared/entry-metadata.ts +++ b/src/shared/entry-metadata.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "./string-coerce.js"; + export function resolveEmojiAndHomepage(params: { metadata?: { emoji?: string; homepage?: string } | null; frontmatter?: { @@ -13,6 +15,6 @@ export function resolveEmojiAndHomepage(params: { params.frontmatter?.homepage ?? params.frontmatter?.website ?? params.frontmatter?.url; - const homepage = homepageRaw?.trim() ? homepageRaw.trim() : undefined; + const homepage = normalizeOptionalString(homepageRaw); return { ...(emoji ? { emoji } : {}), ...(homepage ? { homepage } : {}) }; } diff --git a/src/shared/gateway-bind-url.ts b/src/shared/gateway-bind-url.ts index 9412fd8a1e1..763195b23b1 100644 --- a/src/shared/gateway-bind-url.ts +++ b/src/shared/gateway-bind-url.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "./string-coerce.js"; + export type GatewayBindUrlResult = | { url: string; @@ -18,7 +20,7 @@ export function resolveGatewayBindUrl(params: { }): GatewayBindUrlResult { const bind = params.bind ?? "loopback"; if (bind === "custom") { - const host = params.customBindHost?.trim(); + const host = normalizeOptionalString(params.customBindHost); if (host) { return { url: `${params.scheme}://${host}:${params.port}`, source: "gateway.bind=custom" }; } diff --git a/src/shared/net/ip.ts b/src/shared/net/ip.ts index b4f2a15b82b..52a07f9f470 100644 --- a/src/shared/net/ip.ts +++ b/src/shared/net/ip.ts @@ -1,4 +1,5 @@ import ipaddr from "ipaddr.js"; +import { normalizeOptionalString } from "../string-coerce.js"; export type ParsedIpAddress = ipaddr.IPv4 | ipaddr.IPv6; type Ipv4Range = ReturnType; @@ -134,7 +135,7 @@ function normalizeIpv4MappedAddress(address: ParsedIpAddress): ParsedIpAddress { } function normalizeIpParseInput(raw: string | undefined): string | undefined { - const trimmed = raw?.trim(); + const trimmed = normalizeOptionalString(raw); if (!trimmed) { return undefined; } @@ -179,7 +180,7 @@ export function normalizeIpAddress(raw: string | undefined): string | undefined } export function isCanonicalDottedDecimalIPv4(raw: string | undefined): boolean { - const trimmed = raw?.trim(); + const trimmed = normalizeOptionalString(raw); if (!trimmed) { return false; } @@ -191,7 +192,7 @@ export function isCanonicalDottedDecimalIPv4(raw: string | undefined): boolean { } export function isLegacyIpv4Literal(raw: string | undefined): boolean { - const trimmed = raw?.trim(); + const trimmed = normalizeOptionalString(raw); if (!trimmed) { return false; } diff --git a/src/shared/node-match.ts b/src/shared/node-match.ts index 731ebbf8900..e5acf07d6e5 100644 --- a/src/shared/node-match.ts +++ b/src/shared/node-match.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "./string-coerce.js"; + export type NodeMatchCandidate = { nodeId: string; displayName?: string; @@ -30,19 +32,20 @@ function listKnownNodes(nodes: NodeMatchCandidate[]): string { function formatNodeCandidateLabel(node: NodeMatchCandidate): string { const label = node.displayName || node.remoteIp || node.nodeId; const details = [`node=${node.nodeId}`]; - if (typeof node.clientId === "string" && node.clientId.trim()) { - details.push(`client=${node.clientId.trim()}`); + const clientId = normalizeOptionalString(node.clientId); + if (clientId) { + details.push(`client=${clientId}`); } return `${label} [${details.join(", ")}]`; } function isCurrentOpenClawClient(clientId: string | undefined): boolean { - const normalized = clientId?.trim().toLowerCase() ?? ""; + const normalized = normalizeOptionalString(clientId)?.toLowerCase() ?? ""; return normalized.startsWith("openclaw-"); } function isLegacyClawdbotClient(clientId: string | undefined): boolean { - const normalized = clientId?.trim().toLowerCase() ?? ""; + const normalized = normalizeOptionalString(clientId)?.toLowerCase() ?? ""; return normalized.startsWith("clawdbot-") || normalized.startsWith("moldbot-"); } @@ -95,7 +98,7 @@ function scoreNodeCandidate(node: NodeMatchCandidate, matchScore: number): numbe } function resolveScoredMatches(nodes: NodeMatchCandidate[], query: string): ScoredNodeMatch[] { - const trimmed = query.trim(); + const trimmed = normalizeOptionalString(query); if (!trimmed) { return []; } diff --git a/src/shared/string-coerce.ts b/src/shared/string-coerce.ts index 56ce6b869e2..343720c6b8e 100644 --- a/src/shared/string-coerce.ts +++ b/src/shared/string-coerce.ts @@ -15,5 +15,5 @@ export function normalizeOptionalString(value: unknown): string | undefined { } export function hasNonEmptyString(value: unknown): value is string { - return typeof value === "string" && value.trim().length > 0; + return normalizeOptionalString(value) !== undefined; } diff --git a/src/shared/string-normalization.ts b/src/shared/string-normalization.ts index 262c32c7b24..2581672105e 100644 --- a/src/shared/string-normalization.ts +++ b/src/shared/string-normalization.ts @@ -1,3 +1,5 @@ +import { normalizeOptionalString } from "./string-coerce.js"; + export function normalizeStringEntries(list?: ReadonlyArray) { return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean); } @@ -10,9 +12,10 @@ export function normalizeTrimmedStringList(value: unknown): string[] { if (!Array.isArray(value)) { return []; } - return value.flatMap((entry) => - typeof entry === "string" && entry.trim() ? [entry.trim()] : [], - ); + return value.flatMap((entry) => { + const normalized = normalizeOptionalString(entry); + return normalized ? [normalized] : []; + }); } export function normalizeOptionalTrimmedStringList(value: unknown): string[] | undefined { @@ -31,10 +34,8 @@ export function normalizeSingleOrTrimmedStringList(value: unknown): string[] { if (Array.isArray(value)) { return normalizeTrimmedStringList(value); } - if (typeof value === "string" && value.trim()) { - return [value.trim()]; - } - return []; + const normalized = normalizeOptionalString(value); + return normalized ? [normalized] : []; } export function normalizeCsvOrLooseStringList(value: unknown): string[] { @@ -51,7 +52,7 @@ export function normalizeCsvOrLooseStringList(value: unknown): string[] { } export function normalizeHyphenSlug(raw?: string | null) { - const trimmed = raw?.trim().toLowerCase() ?? ""; + const trimmed = normalizeOptionalString(raw)?.toLowerCase() ?? ""; if (!trimmed) { return ""; } @@ -61,7 +62,7 @@ export function normalizeHyphenSlug(raw?: string | null) { } export function normalizeAtHashSlug(raw?: string | null) { - const trimmed = raw?.trim().toLowerCase() ?? ""; + const trimmed = normalizeOptionalString(raw)?.toLowerCase() ?? ""; if (!trimmed) { return ""; }