mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 18:23:57 +00:00
* refactor: extract reusable AI runtime package * refactor: complete AI provider relocation * refactor: keep llm core internal * refactor(ai): make @openclaw/ai self-contained with host policy ports Move pure transport helpers (tool projections, strict-schema normalization, prompt-cache boundary, stream guards, anthropic/openai compat, request activity) from src into packages/ai; move utf16-slice into normalization-core. Inject host policy (guarded fetch, redaction, strict-tool defaults, diagnostics logging) through AiTransportHost with inert library defaults installed by src/llm/stream.ts. Narrow the public barrel to instance-scoped createApiRegistry/createLlmRuntime; the process-default runtime moves behind internal/ and registerBuiltInApiProviders takes an explicit registry. Delete the src/llm/api-registry re-export facade. * fix(ai): teach node, jiti, and vite resolvers the @openclaw/ai and utf16-slice subpaths The workspace alias tables in root-alias.cjs, plugin-sdk-native-resolver, sdk-alias, the shared vitest config, and the Control UI vite config only knew @openclaw/llm-core; Node-side plugin loading resolved @openclaw/ai through the pnpm symlink to the unbuilt dist (checks-node-compact CI failures), and the Control UI build broke on the new normalization-core/utf16-slice subpath. * chore(ui): drop leftover service-worker debug logging * build(release): ship @openclaw/ai with its own shrinkwrap and honest dependency set packages/ai declares only its six real runtime deps (kysely, chalk, json5, tslog, zod, fs-safe, and proxyline were never imported); orphaned root deps removed. generate-npm-shrinkwrap now treats publishable packages/* like publishable plugins so the AI tarball pins its transitive tree even though workspace deps are omitted from the root shrinkwrap. knip learns the package entry points; the tsdown dts neverBundle option moves to its documented deps.dts home; the README documents the no-semver internal/* contract and host ports. * docs(ai): add minimal external-consumer example app examples/ai-chat consumes only the public @openclaw/ai surface (built dist via the workspace link): isolated runtime, built-in provider registration, one streamed completion. Supports Anthropic/OpenAI via env keys and a keyless local Ollama target; live-verified against Ollama. * docs(ai): document the @openclaw/ai package and workspace shrinkwrap boundary * chore(check): include examples/ in duplicate-scan targets * fix: emit normalization package subpaths * fix: complete AI package boundary artifacts * fix: align AI package boundary contracts * fix(ci): stabilize package release contracts * test: align documentation contract checks * test: keep cron docs guard aligned * test: align restored docs contract guards * test: follow upstream docs contracts * docs: drop superseded talk wording
179 lines
5.6 KiB
TypeScript
179 lines
5.6 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,
|
|
resolveHomeRelativePath,
|
|
resolveRequiredHomeDir,
|
|
} from "./infra/home-dir.js";
|
|
import { isPlainObject } from "./infra/plain-object.js";
|
|
export { escapeRegExp } from "./shared/regexp.js";
|
|
export { sleep } from "./utils/sleep.js";
|
|
|
|
/** 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 };
|
|
|
|
/**
|
|
* Type guard for Record<string, unknown> (less strict than isPlainObject).
|
|
* Accepts any non-null object that isn't an array.
|
|
*/
|
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
/** 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, "");
|
|
if (digits.startsWith("+")) {
|
|
return `+${digits.slice(1)}`;
|
|
}
|
|
return `+${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 `~` and OpenClaw home-relative paths with injectable env/home sources. */
|
|
export function resolveUserPath(
|
|
input: string,
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
homedir: () => string = os.homedir,
|
|
): string {
|
|
if (!input) {
|
|
return "";
|
|
}
|
|
return resolveHomeRelativePath(input, { env, homedir });
|
|
}
|
|
|
|
/** 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);
|
|
}
|