refactor: consolidate duplicate utility functions (#12439)

* refactor: consolidate duplicate utility functions

- Add escapeRegExp to src/utils.ts and remove 10 local duplicates
- Rename bash-tools clampNumber to clampWithDefault (different signature)
- Centralize formatError calls to use formatErrorMessage from infra/errors.ts
- Re-export formatErrorMessage from cli/cli-utils.ts to preserve API

* refactor: consolidate remaining escapeRegExp duplicates

* refactor: consolidate sleep, stripAnsi, and clamp duplicates
This commit is contained in:
max
2026-02-08 23:59:43 -08:00
committed by GitHub
parent 8968d9a339
commit ec910a235e
29 changed files with 67 additions and 146 deletions
+10 -5
View File
@@ -43,7 +43,7 @@ import {
buildDockerExecArgs,
buildSandboxEnv,
chunkString,
clampNumber,
clampWithDefault,
coerceEnv,
killSession,
readEnvInt,
@@ -105,13 +105,13 @@ function validateHostEnv(env: Record<string, string>): void {
}
}
}
const DEFAULT_MAX_OUTPUT = clampNumber(
const DEFAULT_MAX_OUTPUT = clampWithDefault(
readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"),
200_000,
1_000,
200_000,
);
const DEFAULT_PENDING_MAX_OUTPUT = clampNumber(
const DEFAULT_PENDING_MAX_OUTPUT = clampWithDefault(
readEnvInt("OPENCLAW_BASH_PENDING_MAX_OUTPUT_CHARS"),
200_000,
1_000,
@@ -801,7 +801,7 @@ export function createExecTool(
defaults?: ExecToolDefaults,
// oxlint-disable-next-line typescript/no-explicit-any
): AgentTool<any, ExecToolDetails> {
const defaultBackgroundMs = clampNumber(
const defaultBackgroundMs = clampWithDefault(
defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"),
10_000,
10,
@@ -860,7 +860,12 @@ export function createExecTool(
const yieldWindow = allowBackground
? backgroundRequested
? 0
: clampNumber(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000)
: clampWithDefault(
params.yieldMs ?? defaultBackgroundMs,
defaultBackgroundMs,
10,
120_000,
)
: null;
const elevatedDefaults = defaults?.elevated;
const elevatedAllowed = Boolean(elevatedDefaults?.enabled && elevatedDefaults.allowed);
+4 -1
View File
@@ -146,7 +146,10 @@ function safeCwd() {
}
}
export function clampNumber(
/**
* Clamp a number within min/max bounds, using defaultValue if undefined or NaN.
*/
export function clampWithDefault(
value: number | undefined,
defaultValue: number,
min: number,
+4 -7
View File
@@ -10,6 +10,7 @@ import type { CliBackendConfig } from "../../config/types.js";
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
import { runExec } from "../../process/exec.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
import { escapeRegExp } from "../../utils.js";
import { resolveDefaultModelForAgent } from "../model-selection.js";
import { detectRuntimeShell } from "../shell-utils.js";
import { buildSystemPromptParams } from "../system-prompt-params.js";
@@ -17,10 +18,6 @@ import { buildAgentSystemPrompt } from "../system-prompt.js";
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export async function cleanupResumeProcesses(
backend: CliBackendConfig,
sessionId: string,
@@ -43,7 +40,7 @@ export async function cleanupResumeProcesses(
const resumeTokens = resumeArgs.map((arg) => arg.replaceAll("{sessionId}", sessionId));
const pattern = [commandToken, ...resumeTokens]
.filter(Boolean)
.map((token) => escapeRegex(token))
.map((token) => escapeRegExp(token))
.join(".*");
if (!pattern) {
return;
@@ -95,9 +92,9 @@ function buildSessionMatchers(backend: CliBackendConfig): RegExp[] {
function tokenToRegex(token: string): string {
if (!token.includes("{sessionId}")) {
return escapeRegex(token);
return escapeRegExp(token);
}
const parts = token.split("{sessionId}").map((part) => escapeRegex(part));
const parts = token.split("{sessionId}").map((part) => escapeRegExp(part));
return parts.join("\\S+");
}
+2 -4
View File
@@ -1,3 +1,5 @@
import { escapeRegExp } from "../utils.js";
const ESC = "\x1b";
const CR = "\r";
const TAB = "\t";
@@ -12,10 +14,6 @@ type Modifiers = {
shift: boolean;
};
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const namedKeyMap = new Map<string, string>([
["enter", CR],
["return", CR],
+1 -4
View File
@@ -14,6 +14,7 @@ import type {
} from "./commands-registry.types.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import { escapeRegExp } from "../utils.js";
import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.data.js";
export type {
@@ -68,10 +69,6 @@ function getTextAliasMap(): Map<string, TextAliasSpec> {
return map;
}
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function buildSkillCommandDefinitions(skillCommands?: SkillCommandSpec[]): ChatCommandDefinition[] {
if (!skillCommands || skillCommands.length === 0) {
return [];
+1 -3
View File
@@ -1,6 +1,4 @@
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
import { escapeRegExp } from "../utils.js";
export function extractModelDirective(
body?: string,
+1 -2
View File
@@ -1,4 +1,5 @@
import type { NoticeLevel, ReasoningLevel } from "../thinking.js";
import { escapeRegExp } from "../../utils.js";
import {
type ElevatedLevel,
normalizeElevatedLevel,
@@ -17,8 +18,6 @@ type ExtractedLevel<T> = {
hasDirective: boolean;
};
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const matchLevelDirective = (
body: string,
names: string[],
+1 -4
View File
@@ -1,6 +1,7 @@
import type { MsgContext } from "../templating.js";
import { normalizeChatType } from "../../channels/chat-type.js";
import { listSenderLabelCandidates, resolveSenderLabel } from "../../channels/sender-label.js";
import { escapeRegExp } from "../../utils.js";
export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
const body = params.body;
@@ -51,7 +52,3 @@ function hasSenderMetaLine(body: string, ctx: MsgContext): boolean {
return pattern.test(body);
});
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+1 -4
View File
@@ -3,10 +3,7 @@ import type { MsgContext } from "../templating.js";
import { resolveAgentConfig } from "../../agents/agent-scope.js";
import { getChannelDock } from "../../channels/dock.js";
import { normalizeChannelId } from "../../channels/plugins/index.js";
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
import { escapeRegExp } from "../../utils.js";
function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) {
const patterns: string[] = [];
+2 -4
View File
@@ -1,10 +1,8 @@
import { escapeRegExp } from "../utils.js";
export const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
export const SILENT_REPLY_TOKEN = "NO_REPLY";
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function isSilentReplyText(
text: string | undefined,
token: string = SILENT_REPLY_TOKEN,
+2 -5
View File
@@ -1,5 +1,6 @@
import type { BrowserRouteContext } from "../server-context.js";
import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js";
import { escapeRegExp } from "../../utils.js";
import { registerBrowserRoutes } from "./index.js";
type BrowserDispatchRequest = {
@@ -22,10 +23,6 @@ type RouteEntry = {
handler: (req: BrowserRequest, res: BrowserResponse) => void | Promise<void>;
};
function escapeRegex(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function compileRoute(path: string): { regex: RegExp; paramNames: string[] } {
const paramNames: string[] = [];
const parts = path.split("/").map((part) => {
@@ -34,7 +31,7 @@ function compileRoute(path: string): { regex: RegExp; paramNames: string[] } {
paramNames.push(name);
return "([^/]+)";
}
return escapeRegex(part);
return escapeRegExp(part);
});
return { regex: new RegExp(`^${parts.join("/")}$`), paramNames };
}
+1 -3
View File
@@ -18,7 +18,7 @@ import { resolveSignalAccount } from "../signal/accounts.js";
import { resolveSlackAccount, resolveSlackReplyToMode } from "../slack/accounts.js";
import { buildSlackThreadingToolContext } from "../slack/threading-tool-context.js";
import { resolveTelegramAccount } from "../telegram/accounts.js";
import { normalizeE164 } from "../utils.js";
import { escapeRegExp, normalizeE164 } from "../utils.js";
import { resolveWhatsAppAccount } from "../web/accounts.js";
import { normalizeWhatsAppTarget } from "../whatsapp/normalize.js";
import {
@@ -76,8 +76,6 @@ const formatLower = (allowFrom: Array<string | number>) =>
.filter(Boolean)
.map((entry) => entry.toLowerCase());
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Channel docks: lightweight channel metadata/behavior for shared code paths.
//
// Rules:
+3 -4
View File
@@ -1,14 +1,13 @@
import type { Command } from "commander";
import { formatErrorMessage } from "../infra/errors.js";
export { formatErrorMessage };
export type ManagerLookupResult<T> = {
manager: T | null;
error?: string;
};
export function formatErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
export async function withManager<T>(params: {
getManager: () => Promise<ManagerLookupResult<T>>;
onMissing: (error?: string) => void;
+1 -4
View File
@@ -1,10 +1,7 @@
import { describe, expect, it } from "vitest";
import { stripAnsi } from "../terminal/ansi.js";
import { formatHealthCheckFailure } from "./health-format.js";
const ansiEscape = String.fromCharCode(27);
const ansiRegex = new RegExp(`${ansiEscape}\\[[0-9;]*m`, "g");
const stripAnsi = (input: string) => input.replace(ansiRegex, "");
describe("formatHealthCheckFailure", () => {
it("keeps non-rich output stable", () => {
const err = new Error("gateway closed (1006 abnormal closure): no close reason");
+3 -9
View File
@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import type { SystemPresence } from "../infra/system-presence.js";
import { formatErrorMessage } from "../infra/errors.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { GatewayClient } from "./client.js";
@@ -26,13 +27,6 @@ export type GatewayProbeResult = {
configSnapshot: unknown;
};
function formatError(err: unknown): string {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
export async function probeGateway(opts: {
url: string;
auth?: GatewayProbeAuth;
@@ -65,7 +59,7 @@ export async function probeGateway(opts: {
mode: GATEWAY_CLIENT_MODES.PROBE,
instanceId,
onConnectError: (err) => {
connectError = formatError(err);
connectError = formatErrorMessage(err);
},
onClose: (code, reason) => {
close = { code, reason };
@@ -93,7 +87,7 @@ export async function probeGateway(opts: {
settle({
ok: false,
connectLatencyMs,
error: formatError(err),
error: formatErrorMessage(err),
close,
health: null,
status: null,
+1 -4
View File
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import type { GatewayRequestHandlers } from "./types.js";
import { getResolvedLoggerSettings } from "../../logging.js";
import { clamp } from "../../utils.js";
import {
ErrorCodes,
errorShape,
@@ -15,10 +16,6 @@ const MAX_LIMIT = 5000;
const MAX_BYTES = 1_000_000;
const ROLLING_LOG_RE = /^openclaw-\d{4}-\d{2}-\d{2}\.log$/;
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
}
function isRollingLogFile(file: string): boolean {
return ROLLING_LOG_RE.test(path.basename(file));
}
+1 -5
View File
@@ -1,10 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { resolveConfigDir } from "../utils.js";
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
import { escapeRegExp, resolveConfigDir } from "../utils.js";
export function upsertSharedEnvVar(params: {
key: string;
+5 -11
View File
@@ -1,6 +1,7 @@
import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp";
import fsSync from "node:fs";
import type { OpenClawConfig } from "../config/config.js";
import { formatErrorMessage } from "../infra/errors.js";
import { resolveUserPath } from "../utils.js";
import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js";
import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js";
@@ -73,7 +74,7 @@ function canAutoSelectLocal(options: EmbeddingProviderOptions): boolean {
}
function isMissingApiKeyError(err: unknown): boolean {
const message = formatError(err);
const message = formatErrorMessage(err);
return message.includes("No API key found for provider");
}
@@ -149,7 +150,7 @@ export async function createEmbeddingProvider(
};
const formatPrimaryError = (err: unknown, provider: "openai" | "local" | "gemini" | "voyage") =>
provider === "local" ? formatLocalSetupError(err) : formatError(err);
provider === "local" ? formatLocalSetupError(err) : formatErrorMessage(err);
if (requestedProvider === "auto") {
const missingKeyErrors: string[] = [];
@@ -202,7 +203,7 @@ export async function createEmbeddingProvider(
} catch (fallbackErr) {
// oxlint-disable-next-line preserve-caught-error
throw new Error(
`${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`,
`${reason}\n\nFallback to ${fallback} failed: ${formatErrorMessage(fallbackErr)}`,
{ cause: fallbackErr },
);
}
@@ -211,13 +212,6 @@ export async function createEmbeddingProvider(
}
}
function formatError(err: unknown): string {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
function isNodeLlamaCppMissing(err: unknown): boolean {
if (!(err instanceof Error)) {
return false;
@@ -230,7 +224,7 @@ function isNodeLlamaCppMissing(err: unknown): boolean {
}
function formatLocalSetupError(err: unknown): string {
const detail = formatError(err);
const detail = formatErrorMessage(err);
const missing = isNodeLlamaCppMissing(err);
return [
"Local embeddings unavailable.",
+2 -1
View File
@@ -229,7 +229,8 @@ export {
} from "../agents/tools/common.js";
export { formatDocsLink } from "../terminal/links.js";
export type { HookEntry } from "../hooks/types.js";
export { normalizeE164 } from "../utils.js";
export { clamp, escapeRegExp, normalizeE164, sleep } from "../utils.js";
export { stripAnsi } from "../terminal/ansi.js";
export { missingTargetError } from "../infra/outbound/target-errors.js";
export { registerLogTransport } from "../logging/logger.js";
export type { LogTransport, LogTransportRecord } from "../logging/logger.js";
+10
View File
@@ -21,6 +21,16 @@ export function clampInt(value: number, min: number, max: number): number {
return clampNumber(Math.floor(value), min, max);
}
/** Alias for clampNumber (shorter, more common name) */
export const clamp = clampNumber;
/**
* Escapes special regex characters in a string so it can be used in a RegExp constructor.
*/
export function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export type WebChannel = "web";
export function assertWebChannel(input: string): asserts input is WebChannel {
+1 -2
View File
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import type { OpenClawConfig } from "../config/config.js";
import type { BackoffPolicy } from "../infra/backoff.js";
import { computeBackoff, sleepWithAbort } from "../infra/backoff.js";
import { clamp } from "../utils.js";
export type ReconnectPolicy = BackoffPolicy & {
maxAttempts: number;
@@ -16,8 +17,6 @@ export const DEFAULT_RECONNECT_POLICY: ReconnectPolicy = {
maxAttempts: 12,
};
const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val));
export function resolveHeartbeatSeconds(cfg: OpenClawConfig, overrideSeconds?: number): number {
const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds;
if (typeof candidate === "number" && candidate > 0) {