import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { resolveEffectiveHomeDir, resolveHomeRelativePath, resolveRequiredHomeDir, } from "./infra/home-dir.js"; import { isPlainObject } from "./infra/plain-object.js"; import { formatTerminalLink } from "./terminal/terminal-link.js"; export async function ensureDir(dir: string) { await fs.promises.mkdir(dir, { recursive: true }); } /** * Check if a file or directory exists at the given path. */ export async function pathExists(targetPath: string): Promise { try { await fs.promises.access(targetPath); return true; } catch { return false; } } export function clampNumber(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); } 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, "\\$&"); } /** * Safely parse JSON, returning null on error instead of throwing. */ export function safeParseJson(raw: string): T | null { try { return JSON.parse(raw) as T; } catch { return null; } } export { formatTerminalLink, isPlainObject }; /** * Type guard for Record (less strict than isPlainObject). * Accepts any non-null object that isn't an array. */ export function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function normalizeE164(number: string): string { const withoutPrefix = number.replace(/^[a-z][a-z0-9-]*:/i, "").trim(); const digits = withoutPrefix.replace(/[^\d+]/g, ""); if (digits.startsWith("+")) { return `+${digits.slice(1)}`; } return `+${digits}`; } export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function isHighSurrogate(codeUnit: number): boolean { return codeUnit >= 0xd800 && codeUnit <= 0xdbff; } function isLowSurrogate(codeUnit: number): boolean { return codeUnit >= 0xdc00 && codeUnit <= 0xdfff; } export function sliceUtf16Safe(input: string, start: number, end?: number): string { const len = input.length; let from = start < 0 ? Math.max(len + start, 0) : Math.min(start, len); let to = end === undefined ? len : end < 0 ? Math.max(len + end, 0) : Math.min(end, len); if (to < from) { const tmp = from; from = to; to = tmp; } if (from > 0 && from < len) { const codeUnit = input.charCodeAt(from); if (isLowSurrogate(codeUnit) && isHighSurrogate(input.charCodeAt(from - 1))) { from += 1; } } if (to > 0 && to < len) { const codeUnit = input.charCodeAt(to - 1); if (isHighSurrogate(codeUnit) && isLowSurrogate(input.charCodeAt(to))) { to -= 1; } } return input.slice(from, to); } export function truncateUtf16Safe(input: string, maxLen: number): string { const limit = Math.max(0, Math.floor(maxLen)); if (input.length <= limit) { return input; } return sliceUtf16Safe(input, 0, limit); } export function resolveUserPath( input: string, env: NodeJS.ProcessEnv = process.env, homedir: () => string = os.homedir, ): string { if (!input) { return ""; } return resolveHomeRelativePath(input, { env, homedir }); } 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 newDir = path.join(resolveRequiredHomeDir(env, homedir), ".openclaw"); try { const hasNew = fs.existsSync(newDir); if (hasNew) { return newDir; } } catch { // best-effort } return newDir; } 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: "~" }; } 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; } 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); } export function displayPath(input: string): string { return shortenHomePath(input); } export function displayString(input: string): string { return shortenHomeInString(input); } // Configuration root; can be overridden via OPENCLAW_STATE_DIR. export const CONFIG_DIR = resolveConfigDir();