Files
openclaw/src/utils.ts
Peter Steinberger 2aa4ff0825 refactor: consolidate duplicate helper exports (#106016)
* refactor(normalization): consolidate string helpers

* refactor(normalization): consolidate agent ids

* refactor(paths): consolidate user path resolution

* refactor(gateway): preserve transcript helper facade

* fix(normalization): align helper build aliases

* fix(memory): keep helper import line neutral

* chore(plugin-sdk): align consolidated helper surface

* refactor(normalization): reuse lowercase string helper

* refactor(normalization): reuse record guard

* refactor(normalization): share surrogate boundary helper

* refactor(agents): share token estimate constant

* refactor(memory): distinguish standalone helper contracts

* refactor(normalization): share error cause formatter

* refactor(config): distinguish eligibility predicates

* refactor(plugins): share registry state symbol

* refactor(cli): reuse outbound dependency adapter

* refactor(cron): distinguish positive duration parser

* fix(gateway): keep transcript helper private

* fix(paths): preserve public helper status

* refactor(channels): reuse registry normalizer

* refactor(agents): reuse agent core runtime facade

* refactor(auth): reuse locked profile upsert

* refactor(records): distinguish trap-safe guard

* refactor(migrations): distinguish sync directory helper

* test(plugins): drop stale duration alias expectations

* fix(channels): remove stale registry import

* chore(plugin-sdk): refresh helper API baseline

* fix(memory): keep renamed helpers private

* fix(plugins): keep registry state key private

* fix(plugin-sdk): tighten wildcard surface budget

* chore(plugin-sdk): refresh rebased helper API baseline
2026-07-13 03:22:00 -07:00

158 lines
5.0 KiB
TypeScript

// Shared filesystem, path, and process helpers for the CLI.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathExists as fsSafePathExists } from "./infra/fs-safe.js";
import {
resolveEffectiveHomeDir,
resolveRequiredHomeDir,
resolveUserPath,
} from "./infra/home-dir.js";
import { isPlainObject } from "./infra/plain-object.js";
export { escapeRegExp } from "./shared/regexp.js";
export { sleep } from "./utils/sleep.js";
export { isRecord } from "@openclaw/normalization-core/record-coerce";
export { resolveUserPath };
/** Creates a directory tree if it does not already exist. */
export async function ensureDir(dir: string) {
await fs.promises.mkdir(dir, { recursive: true });
}
/** Clamps a number to an inclusive min/max range. */
export function clampNumber(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
/** Floors a number before clamping it to an inclusive min/max range. */
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;
/**
* Safely parse JSON, returning null on error instead of throwing.
*/
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- JSON parsing helper lets callers ascribe the expected payload type.
export function safeParseJson<T>(raw: string): T | null {
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
export { isPlainObject };
/** Normalizes phone-like input into the loose E.164 shape used by channel helpers. */
export function normalizeE164(number: string): string {
const withoutPrefix = number.replace(/^[a-z][a-z0-9-]*:/i, "").trim();
const digits = withoutPrefix.replace(/\D/g, "");
return digits ? `+${digits}` : "";
}
// Surrogate-safe slicing helpers live in a node-free leaf module so browser/UI
// bundles can import them without pulling in filesystem code. Re-exported here
// to preserve the historical `utils.ts` import surface.
export { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
/** Resolves the OpenClaw config directory from state/config env overrides or home. */
export function resolveConfigDir(
env: NodeJS.ProcessEnv = process.env,
homedir: () => string = os.homedir,
): string {
const override = env.OPENCLAW_STATE_DIR?.trim();
if (override) {
return resolveUserPath(override, env, homedir);
}
const configPath = env.OPENCLAW_CONFIG_PATH?.trim();
if (configPath) {
return path.dirname(resolveUserPath(configPath, env, homedir));
}
const newDir = path.join(resolveRequiredHomeDir(env, homedir), ".openclaw");
try {
const hasNew = fs.existsSync(newDir);
if (hasNew) {
return newDir;
}
} catch {
// best-effort
}
return newDir;
}
/** Resolves the effective OpenClaw home directory, if one can be determined. */
export function resolveHomeDir(): string | undefined {
return resolveEffectiveHomeDir(process.env, os.homedir);
}
function resolveHomeDisplayPrefix(): { home: string; prefix: string } | undefined {
const home = resolveHomeDir();
if (!home) {
return undefined;
}
const explicitHome = process.env.OPENCLAW_HOME?.trim();
if (explicitHome) {
return { home, prefix: "$OPENCLAW_HOME" };
}
return { home, prefix: "~" };
}
/** Replaces the leading home directory in a path with `~` or `$OPENCLAW_HOME`. */
export function shortenHomePath(input: string): string {
if (!input) {
return input;
}
const display = resolveHomeDisplayPrefix();
if (!display) {
return input;
}
const { home, prefix } = display;
if (input === home) {
return prefix;
}
if (input.startsWith(`${home}/`) || input.startsWith(`${home}\\`)) {
return `${prefix}${input.slice(home.length)}`;
}
return input;
}
/** Replaces all effective-home occurrences inside a diagnostic string. */
export function shortenHomeInString(input: string): string {
if (!input) {
return input;
}
const display = resolveHomeDisplayPrefix();
if (!display) {
return input;
}
return input.split(display.home).join(display.prefix);
}
/** Shortens a path for display without changing non-home paths. */
export function displayPath(input: string): string {
return shortenHomePath(input);
}
/** Shortens home paths embedded in arbitrary display text. */
export function displayString(input: string): string {
return shortenHomeInString(input);
}
// Gateway startup re-pins this live binding after config/state selection converges so modules
// imported during early CLI bootstrap cannot keep using the superseded configuration root.
export let CONFIG_DIR = resolveConfigDir();
export function pinConfigDir(env: NodeJS.ProcessEnv = process.env): string {
CONFIG_DIR = resolveConfigDir(env);
return CONFIG_DIR;
}
/**
* Check if a file or directory exists at the given path.
*/
export async function pathExists(targetPath: string): Promise<boolean> {
return await fsSafePathExists(targetPath);
}