@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,622 lines (1,601 loc) • 83.5 kB
JavaScript
#!/usr/bin/env node
import { spawn } from "node:child_process";
import process$1 from "node:process";
import os from "node:os";
import path from "node:path";
import fs from "node:fs";
import chalk, { Chalk } from "chalk";
import { Logger } from "tslog";
import JSON5 from "json5";
import util from "node:util";
//#region src/infra/home-dir.ts
function normalize(value) {
const trimmed = value?.trim();
return trimmed ? trimmed : void 0;
}
function resolveEffectiveHomeDir(env = process.env, homedir = os.homedir) {
const raw = resolveRawHomeDir(env, homedir);
return raw ? path.resolve(raw) : void 0;
}
function resolveRawHomeDir(env, homedir) {
const explicitHome = normalize(env.OPENCLAW_HOME);
if (explicitHome) {
if (explicitHome === "~" || explicitHome.startsWith("~/") || explicitHome.startsWith("~\\")) {
const fallbackHome = normalize(env.HOME) ?? normalize(env.USERPROFILE) ?? normalizeSafe(homedir);
if (fallbackHome) return explicitHome.replace(/^~(?=$|[\\/])/, fallbackHome);
return;
}
return explicitHome;
}
const envHome = normalize(env.HOME);
if (envHome) return envHome;
const userProfile = normalize(env.USERPROFILE);
if (userProfile) return userProfile;
return normalizeSafe(homedir);
}
function normalizeSafe(homedir) {
try {
return normalize(homedir());
} catch {
return;
}
}
function resolveRequiredHomeDir(env = process.env, homedir = os.homedir) {
return resolveEffectiveHomeDir(env, homedir) ?? path.resolve(process.cwd());
}
function expandHomePrefix(input, opts) {
if (!input.startsWith("~")) return input;
const home = normalize(opts?.home) ?? resolveEffectiveHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir);
if (!home) return input;
return input.replace(/^~(?=$|[\\/])/, home);
}
//#endregion
//#region src/cli/profile-utils.ts
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
function isValidProfileName(value) {
if (!value) return false;
return PROFILE_NAME_RE.test(value);
}
function normalizeProfileName(raw) {
const profile = raw?.trim();
if (!profile) return null;
if (profile.toLowerCase() === "default") return null;
if (!isValidProfileName(profile)) return null;
return profile;
}
//#endregion
//#region src/cli/profile.ts
function takeValue(raw, next) {
if (raw.includes("=")) {
const [, value] = raw.split("=", 2);
return {
value: (value ?? "").trim() || null,
consumedNext: false
};
}
return {
value: (next ?? "").trim() || null,
consumedNext: Boolean(next)
};
}
function parseCliProfileArgs(argv) {
if (argv.length < 2) return {
ok: true,
profile: null,
argv
};
const out = argv.slice(0, 2);
let profile = null;
let sawDev = false;
let sawCommand = false;
const args = argv.slice(2);
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === void 0) continue;
if (sawCommand) {
out.push(arg);
continue;
}
if (arg === "--dev") {
if (profile && profile !== "dev") return {
ok: false,
error: "Cannot combine --dev with --profile"
};
sawDev = true;
profile = "dev";
continue;
}
if (arg === "--profile" || arg.startsWith("--profile=")) {
if (sawDev) return {
ok: false,
error: "Cannot combine --dev with --profile"
};
const next = args[i + 1];
const { value, consumedNext } = takeValue(arg, next);
if (consumedNext) i += 1;
if (!value) return {
ok: false,
error: "--profile requires a value"
};
if (!isValidProfileName(value)) return {
ok: false,
error: "Invalid --profile (use letters, numbers, \"_\", \"-\" only)"
};
profile = value;
continue;
}
if (!arg.startsWith("-")) {
sawCommand = true;
out.push(arg);
continue;
}
out.push(arg);
}
return {
ok: true,
profile,
argv: out
};
}
function resolveProfileStateDir(profile, env, homedir) {
const suffix = profile.toLowerCase() === "default" ? "" : `-${profile}`;
return path.join(resolveRequiredHomeDir(env, homedir), `.openclaw${suffix}`);
}
function applyCliProfileEnv(params) {
const env = params.env ?? process.env;
const homedir = params.homedir ?? os.homedir;
const profile = params.profile.trim();
if (!profile) return;
env.OPENCLAW_PROFILE = profile;
const stateDir = env.OPENCLAW_STATE_DIR?.trim() || resolveProfileStateDir(profile, env, homedir);
if (!env.OPENCLAW_STATE_DIR?.trim()) env.OPENCLAW_STATE_DIR = stateDir;
if (!env.OPENCLAW_CONFIG_PATH?.trim()) env.OPENCLAW_CONFIG_PATH = path.join(stateDir, "openclaw.json");
if (profile === "dev" && !env.OPENCLAW_GATEWAY_PORT?.trim()) env.OPENCLAW_GATEWAY_PORT = "19001";
}
//#endregion
//#region src/cli/argv.ts
const HELP_FLAGS = new Set(["-h", "--help"]);
const VERSION_FLAGS = new Set(["-V", "--version"]);
const ROOT_VERSION_ALIAS_FLAG = "-v";
const ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]);
const ROOT_VALUE_FLAGS = new Set(["--profile"]);
const FLAG_TERMINATOR = "--";
function hasHelpOrVersion(argv) {
return argv.some((arg) => HELP_FLAGS.has(arg) || VERSION_FLAGS.has(arg)) || hasRootVersionAlias(argv);
}
function isValueToken(arg) {
if (!arg) return false;
if (arg === FLAG_TERMINATOR) return false;
if (!arg.startsWith("-")) return true;
return /^-\d+(?:\.\d+)?$/.test(arg);
}
function parsePositiveInt(value) {
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed) || parsed <= 0) return;
return parsed;
}
function hasFlag(argv, name) {
const args = argv.slice(2);
for (const arg of args) {
if (arg === FLAG_TERMINATOR) break;
if (arg === name) return true;
}
return false;
}
function hasRootVersionAlias(argv) {
const args = argv.slice(2);
let hasAlias = false;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg) continue;
if (arg === FLAG_TERMINATOR) break;
if (arg === ROOT_VERSION_ALIAS_FLAG) {
hasAlias = true;
continue;
}
if (ROOT_BOOLEAN_FLAGS.has(arg)) continue;
if (arg.startsWith("--profile=")) continue;
if (ROOT_VALUE_FLAGS.has(arg)) {
const next = args[i + 1];
if (isValueToken(next)) i += 1;
continue;
}
if (arg.startsWith("-")) continue;
return false;
}
return hasAlias;
}
function getFlagValue(argv, name) {
const args = argv.slice(2);
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === FLAG_TERMINATOR) break;
if (arg === name) {
const next = args[i + 1];
return isValueToken(next) ? next : null;
}
if (arg.startsWith(`${name}=`)) {
const value = arg.slice(name.length + 1);
return value ? value : null;
}
}
}
function getVerboseFlag(argv, options) {
if (hasFlag(argv, "--verbose")) return true;
if (options?.includeDebug && hasFlag(argv, "--debug")) return true;
return false;
}
function getPositiveIntFlagValue(argv, name) {
const raw = getFlagValue(argv, name);
if (raw === null || raw === void 0) return raw;
return parsePositiveInt(raw);
}
function getCommandPath(argv, depth = 2) {
const args = argv.slice(2);
const path = [];
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg) continue;
if (arg === "--") break;
if (arg.startsWith("-")) continue;
path.push(arg);
if (path.length >= depth) break;
}
return path;
}
function getPrimaryCommand(argv) {
const [primary] = getCommandPath(argv, 1);
return primary ?? null;
}
function buildParseArgv(params) {
const baseArgv = params.rawArgs && params.rawArgs.length > 0 ? params.rawArgs : params.fallbackArgv && params.fallbackArgv.length > 0 ? params.fallbackArgv : process.argv;
const programName = params.programName ?? "";
const normalizedArgv = programName && baseArgv[0] === programName ? baseArgv.slice(1) : baseArgv[0]?.endsWith("openclaw") ? baseArgv.slice(1) : baseArgv;
const executable = (normalizedArgv[0]?.split(/[/\\]/).pop() ?? "").toLowerCase();
if (normalizedArgv.length >= 2 && (isNodeExecutable(executable) || isBunExecutable(executable))) return normalizedArgv;
return [
"node",
programName || "openclaw",
...normalizedArgv
];
}
const nodeExecutablePattern = /^node-\d+(?:\.\d+)*(?:\.exe)?$/;
function isNodeExecutable(executable) {
return executable === "node" || executable === "node.exe" || executable === "nodejs" || executable === "nodejs.exe" || nodeExecutablePattern.test(executable);
}
function isBunExecutable(executable) {
return executable === "bun" || executable === "bun.exe";
}
function shouldMigrateStateFromPath(path) {
if (path.length === 0) return true;
const [primary, secondary] = path;
if (primary === "health" || primary === "status" || primary === "sessions") return false;
if (primary === "config" && (secondary === "get" || secondary === "unset")) return false;
if (primary === "models" && (secondary === "list" || secondary === "status")) return false;
if (primary === "memory" && secondary === "status") return false;
if (primary === "agent") return false;
return true;
}
//#endregion
//#region src/cli/respawn-policy.ts
function shouldSkipRespawnForArgv(argv) {
return hasHelpOrVersion(argv);
}
//#endregion
//#region src/cli/windows-argv.ts
function normalizeWindowsArgv(argv) {
if (process.platform !== "win32") return argv;
if (argv.length < 2) return argv;
const stripControlChars = (value) => {
let out = "";
for (let i = 0; i < value.length; i += 1) {
const code = value.charCodeAt(i);
if (code >= 32 && code !== 127) out += value[i];
}
return out;
};
const normalizeArg = (value) => stripControlChars(value).replace(/^['"]+|['"]+$/g, "").trim();
const normalizeCandidate = (value) => normalizeArg(value).replace(/^\\\\\\?\\/, "");
const execPath = normalizeCandidate(process.execPath);
const execPathLower = execPath.toLowerCase();
const execBase = path.basename(execPath).toLowerCase();
const isExecPath = (value) => {
if (!value) return false;
const normalized = normalizeCandidate(value);
if (!normalized) return false;
const lower = normalized.toLowerCase();
return lower === execPathLower || path.basename(lower) === execBase || lower.endsWith("\\node.exe") || lower.endsWith("/node.exe") || lower.includes("node.exe") || path.basename(lower) === "node.exe" && fs.existsSync(normalized);
};
const next = [...argv];
for (let i = 1; i <= 3 && i < next.length;) {
if (isExecPath(next[i])) {
next.splice(i, 1);
continue;
}
i += 1;
}
const filtered = next.filter((arg, index) => index === 0 || !isExecPath(arg));
if (filtered.length < 3) return filtered;
const cleaned = [...filtered];
for (let i = 2; i < cleaned.length;) {
const arg = cleaned[i];
if (!arg || arg.startsWith("-")) {
i += 1;
continue;
}
if (isExecPath(arg)) {
cleaned.splice(i, 1);
continue;
}
break;
}
return cleaned;
}
//#endregion
//#region src/hooks/internal-hooks.ts
/** Registry of hook handlers by event key */
const handlers = /* @__PURE__ */ new Map();
/**
* Register a hook handler for a specific event type or event:action combination
*
* @param eventKey - Event type (e.g., 'command') or specific action (e.g., 'command:new')
* @param handler - Function to call when the event is triggered
*
* @example
* ```ts
* // Listen to all command events
* registerInternalHook('command', async (event) => {
* console.log('Command:', event.action);
* });
*
* // Listen only to /new commands
* registerInternalHook('command:new', async (event) => {
* await saveSessionToMemory(event);
* });
* ```
*/
function registerInternalHook(eventKey, handler) {
if (!handlers.has(eventKey)) handlers.set(eventKey, []);
handlers.get(eventKey).push(handler);
}
/**
* Clear all registered hooks (useful for testing)
*/
function clearInternalHooks() {
handlers.clear();
}
/**
* Trigger a hook event
*
* Calls all handlers registered for:
* 1. The general event type (e.g., 'command')
* 2. The specific event:action combination (e.g., 'command:new')
*
* Handlers are called in registration order. Errors are caught and logged
* but don't prevent other handlers from running.
*
* @param event - The event to trigger
*/
async function triggerInternalHook(event) {
const typeHandlers = handlers.get(event.type) ?? [];
const specificHandlers = handlers.get(`${event.type}:${event.action}`) ?? [];
const allHandlers = [...typeHandlers, ...specificHandlers];
if (allHandlers.length === 0) return;
for (const handler of allHandlers) try {
await handler(event);
} catch (err) {
console.error(`Hook error [${event.type}:${event.action}]:`, err instanceof Error ? err.message : String(err));
}
}
/**
* Create a hook event with common fields filled in
*
* @param type - The event type
* @param action - The action within that type
* @param sessionKey - The session key
* @param context - Additional context
*/
function createInternalHookEvent(type, action, sessionKey, context = {}) {
return {
type,
action,
sessionKey,
context,
timestamp: /* @__PURE__ */ new Date(),
messages: []
};
}
//#endregion
//#region src/config/paths.ts
/**
* Nix mode detection: When OPENCLAW_NIX_MODE=1, the gateway is running under Nix.
* In this mode:
* - No auto-install flows should be attempted
* - Missing dependencies should produce actionable Nix-specific error messages
* - Config is managed externally (read-only from Nix perspective)
*/
function resolveIsNixMode(env = process.env) {
return env.OPENCLAW_NIX_MODE === "1";
}
const isNixMode = resolveIsNixMode();
const LEGACY_STATE_DIRNAMES = [
".clawdbot",
".moldbot",
".moltbot"
];
const NEW_STATE_DIRNAME = ".openclaw";
const CONFIG_FILENAME = "openclaw.json";
const LEGACY_CONFIG_FILENAMES = [
"clawdbot.json",
"moldbot.json",
"moltbot.json"
];
function resolveDefaultHomeDir() {
return resolveRequiredHomeDir(process.env, os.homedir);
}
/** Build a homedir thunk that respects OPENCLAW_HOME for the given env. */
function envHomedir(env) {
return () => resolveRequiredHomeDir(env, os.homedir);
}
function legacyStateDirs(homedir = resolveDefaultHomeDir) {
return LEGACY_STATE_DIRNAMES.map((dir) => path.join(homedir(), dir));
}
function newStateDir(homedir = resolveDefaultHomeDir) {
return path.join(homedir(), NEW_STATE_DIRNAME);
}
function resolveLegacyStateDirs(homedir = resolveDefaultHomeDir) {
return legacyStateDirs(homedir);
}
function resolveNewStateDir(homedir = resolveDefaultHomeDir) {
return newStateDir(homedir);
}
/**
* State directory for mutable data (sessions, logs, caches).
* Can be overridden via OPENCLAW_STATE_DIR.
* Default: ~/.openclaw
*/
function resolveStateDir(env = process.env, homedir = envHomedir(env)) {
const effectiveHomedir = () => resolveRequiredHomeDir(env, homedir);
const override = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
if (override) return resolveUserPath$1(override, env, effectiveHomedir);
const newDir = newStateDir(effectiveHomedir);
const legacyDirs = legacyStateDirs(effectiveHomedir);
if (fs.existsSync(newDir)) return newDir;
const existingLegacy = legacyDirs.find((dir) => {
try {
return fs.existsSync(dir);
} catch {
return false;
}
});
if (existingLegacy) return existingLegacy;
return newDir;
}
function resolveUserPath$1(input, env = process.env, homedir = envHomedir(env)) {
const trimmed = input.trim();
if (!trimmed) return trimmed;
if (trimmed.startsWith("~")) {
const expanded = expandHomePrefix(trimmed, {
home: resolveRequiredHomeDir(env, homedir),
env,
homedir
});
return path.resolve(expanded);
}
return path.resolve(trimmed);
}
const STATE_DIR = resolveStateDir();
/**
* Config file path (JSON5).
* Can be overridden via OPENCLAW_CONFIG_PATH.
* Default: ~/.openclaw/openclaw.json (or $OPENCLAW_STATE_DIR/openclaw.json)
*/
function resolveCanonicalConfigPath(env = process.env, stateDir = resolveStateDir(env, envHomedir(env))) {
const override = env.OPENCLAW_CONFIG_PATH?.trim() || env.CLAWDBOT_CONFIG_PATH?.trim();
if (override) return resolveUserPath$1(override, env, envHomedir(env));
return path.join(stateDir, CONFIG_FILENAME);
}
/**
* Resolve the active config path by preferring existing config candidates
* before falling back to the canonical path.
*/
function resolveConfigPathCandidate(env = process.env, homedir = envHomedir(env)) {
const existing = resolveDefaultConfigCandidates(env, homedir).find((candidate) => {
try {
return fs.existsSync(candidate);
} catch {
return false;
}
});
if (existing) return existing;
return resolveCanonicalConfigPath(env, resolveStateDir(env, homedir));
}
/**
* Active config path (prefers existing config files).
*/
function resolveConfigPath(env = process.env, stateDir = resolveStateDir(env, envHomedir(env)), homedir = envHomedir(env)) {
const override = env.OPENCLAW_CONFIG_PATH?.trim();
if (override) return resolveUserPath$1(override, env, homedir);
const stateOverride = env.OPENCLAW_STATE_DIR?.trim();
const existing = [path.join(stateDir, CONFIG_FILENAME), ...LEGACY_CONFIG_FILENAMES.map((name) => path.join(stateDir, name))].find((candidate) => {
try {
return fs.existsSync(candidate);
} catch {
return false;
}
});
if (existing) return existing;
if (stateOverride) return path.join(stateDir, CONFIG_FILENAME);
const defaultStateDir = resolveStateDir(env, homedir);
if (path.resolve(stateDir) === path.resolve(defaultStateDir)) return resolveConfigPathCandidate(env, homedir);
return path.join(stateDir, CONFIG_FILENAME);
}
const CONFIG_PATH = resolveConfigPathCandidate();
/**
* Resolve default config path candidates across default locations.
* Order: explicit config path → state-dir-derived paths → new default.
*/
function resolveDefaultConfigCandidates(env = process.env, homedir = envHomedir(env)) {
const effectiveHomedir = () => resolveRequiredHomeDir(env, homedir);
const explicit = env.OPENCLAW_CONFIG_PATH?.trim() || env.CLAWDBOT_CONFIG_PATH?.trim();
if (explicit) return [resolveUserPath$1(explicit, env, effectiveHomedir)];
const candidates = [];
const openclawStateDir = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
if (openclawStateDir) {
const resolved = resolveUserPath$1(openclawStateDir, env, effectiveHomedir);
candidates.push(path.join(resolved, CONFIG_FILENAME));
candidates.push(...LEGACY_CONFIG_FILENAMES.map((name) => path.join(resolved, name)));
}
const defaultDirs = [newStateDir(effectiveHomedir), ...legacyStateDirs(effectiveHomedir)];
for (const dir of defaultDirs) {
candidates.push(path.join(dir, CONFIG_FILENAME));
candidates.push(...LEGACY_CONFIG_FILENAMES.map((name) => path.join(dir, name)));
}
return candidates;
}
const DEFAULT_GATEWAY_PORT = 18789;
/**
* Gateway lock directory (ephemeral).
* Default: os.tmpdir()/openclaw-<uid> (uid suffix when available).
*/
function resolveGatewayLockDir(tmpdir = os.tmpdir) {
const base = tmpdir();
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
const suffix = uid != null ? `openclaw-${uid}` : "openclaw";
return path.join(base, suffix);
}
const OAUTH_FILENAME = "oauth.json";
/**
* OAuth credentials storage directory.
*
* Precedence:
* - `OPENCLAW_OAUTH_DIR` (explicit override)
* - `$*_STATE_DIR/credentials` (canonical server/default)
*/
function resolveOAuthDir(env = process.env, stateDir = resolveStateDir(env, envHomedir(env))) {
const override = env.OPENCLAW_OAUTH_DIR?.trim();
if (override) return resolveUserPath$1(override, env, envHomedir(env));
return path.join(stateDir, "credentials");
}
function resolveOAuthPath(env = process.env, stateDir = resolveStateDir(env, envHomedir(env))) {
return path.join(resolveOAuthDir(env, stateDir), OAUTH_FILENAME);
}
function resolveGatewayPort(cfg, env = process.env) {
const envRaw = env.OPENCLAW_GATEWAY_PORT?.trim() || env.CLAWDBOT_GATEWAY_PORT?.trim();
if (envRaw) {
const parsed = Number.parseInt(envRaw, 10);
if (Number.isFinite(parsed) && parsed > 0) return parsed;
}
const configPort = cfg?.gateway?.port;
if (typeof configPort === "number" && Number.isFinite(configPort)) {
if (configPort > 0) return configPort;
}
return DEFAULT_GATEWAY_PORT;
}
//#endregion
//#region src/infra/tmp-openclaw-dir.ts
const POSIX_OPENCLAW_TMP_DIR = "/tmp/openclaw";
function isNodeErrorWithCode(err, code) {
return typeof err === "object" && err !== null && "code" in err && err.code === code;
}
function resolvePreferredOpenClawTmpDir(options = {}) {
const accessSync = options.accessSync ?? fs.accessSync;
const lstatSync = options.lstatSync ?? fs.lstatSync;
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
const getuid = options.getuid ?? (() => {
try {
return typeof process.getuid === "function" ? process.getuid() : void 0;
} catch {
return;
}
});
const tmpdir = options.tmpdir ?? os.tmpdir;
const uid = getuid();
const isSecureDirForUser = (st) => {
if (uid === void 0) return true;
if (typeof st.uid === "number" && st.uid !== uid) return false;
if (typeof st.mode === "number" && (st.mode & 18) !== 0) return false;
return true;
};
const fallback = () => {
const base = tmpdir();
const suffix = uid === void 0 ? "openclaw" : `openclaw-${uid}`;
return path.join(base, suffix);
};
try {
const preferred = lstatSync(POSIX_OPENCLAW_TMP_DIR);
if (!preferred.isDirectory() || preferred.isSymbolicLink()) return fallback();
accessSync(POSIX_OPENCLAW_TMP_DIR, fs.constants.W_OK | fs.constants.X_OK);
if (!isSecureDirForUser(preferred)) return fallback();
return POSIX_OPENCLAW_TMP_DIR;
} catch (err) {
if (!isNodeErrorWithCode(err, "ENOENT")) return fallback();
}
try {
accessSync("/tmp", fs.constants.W_OK | fs.constants.X_OK);
mkdirSync(POSIX_OPENCLAW_TMP_DIR, {
recursive: true,
mode: 448
});
try {
const preferred = lstatSync(POSIX_OPENCLAW_TMP_DIR);
if (!preferred.isDirectory() || preferred.isSymbolicLink()) return fallback();
if (!isSecureDirForUser(preferred)) return fallback();
} catch {
return fallback();
}
return POSIX_OPENCLAW_TMP_DIR;
} catch {
return fallback();
}
}
//#endregion
//#region src/logging/config.ts
function readLoggingConfig() {
const configPath = resolveConfigPath();
try {
if (!fs.existsSync(configPath)) return;
const raw = fs.readFileSync(configPath, "utf-8");
const logging = JSON5.parse(raw)?.logging;
if (!logging || typeof logging !== "object" || Array.isArray(logging)) return;
return logging;
} catch {
return;
}
}
//#endregion
//#region src/logging/levels.ts
const ALLOWED_LOG_LEVELS = [
"silent",
"fatal",
"error",
"warn",
"info",
"debug",
"trace"
];
function normalizeLogLevel(level, fallback = "info") {
const candidate = (level ?? fallback).trim();
return ALLOWED_LOG_LEVELS.includes(candidate) ? candidate : fallback;
}
function levelToMinLevel(level) {
return {
fatal: 0,
error: 1,
warn: 2,
info: 3,
debug: 4,
trace: 5,
silent: Number.POSITIVE_INFINITY
}[level];
}
//#endregion
//#region src/logging/state.ts
const loggingState = {
cachedLogger: null,
cachedSettings: null,
cachedConsoleSettings: null,
overrideSettings: null,
consolePatched: false,
forceConsoleToStderr: false,
consoleTimestampPrefix: false,
consoleSubsystemFilter: null,
resolvingConsoleSettings: false,
streamErrorHandlersInstalled: false,
rawConsole: null
};
//#endregion
//#region src/logging/logger.ts
const DEFAULT_LOG_DIR = resolvePreferredOpenClawTmpDir();
const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "openclaw.log");
const LOG_PREFIX = "openclaw";
const LOG_SUFFIX = ".log";
const MAX_LOG_AGE_MS = 1440 * 60 * 1e3;
function resolveNodeRequire$1() {
const getBuiltinModule = process.getBuiltinModule;
if (typeof getBuiltinModule !== "function") return null;
try {
const moduleNamespace = getBuiltinModule("module");
return typeof moduleNamespace.createRequire === "function" ? moduleNamespace.createRequire : null;
} catch {
return null;
}
}
const requireConfig$1 = resolveNodeRequire$1()?.(import.meta.url) ?? null;
const externalTransports = /* @__PURE__ */ new Set();
function attachExternalTransport(logger, transport) {
logger.attachTransport((logObj) => {
if (!externalTransports.has(transport)) return;
try {
transport(logObj);
} catch {}
});
}
function resolveSettings() {
let cfg = loggingState.overrideSettings ?? readLoggingConfig();
if (!cfg) try {
cfg = (requireConfig$1?.("../config/config.js"))?.loadConfig?.().logging;
} catch {
cfg = void 0;
}
const defaultLevel = process.env.VITEST === "true" && process.env.OPENCLAW_TEST_FILE_LOG !== "1" ? "silent" : "info";
return {
level: normalizeLogLevel(cfg?.level, defaultLevel),
file: cfg?.file ?? defaultRollingPathForToday()
};
}
function settingsChanged(a, b) {
if (!a) return true;
return a.level !== b.level || a.file !== b.file;
}
function isFileLogLevelEnabled(level) {
const settings = loggingState.cachedSettings ?? resolveSettings();
if (!loggingState.cachedSettings) loggingState.cachedSettings = settings;
if (settings.level === "silent") return false;
return levelToMinLevel(level) <= levelToMinLevel(settings.level);
}
function buildLogger(settings) {
fs.mkdirSync(path.dirname(settings.file), { recursive: true });
if (isRollingPath(settings.file)) pruneOldRollingLogs(path.dirname(settings.file));
const logger = new Logger({
name: "openclaw",
minLevel: levelToMinLevel(settings.level),
type: "hidden"
});
logger.attachTransport((logObj) => {
try {
const time = logObj.date?.toISOString?.() ?? (/* @__PURE__ */ new Date()).toISOString();
const line = JSON.stringify({
...logObj,
time
});
fs.appendFileSync(settings.file, `${line}\n`, { encoding: "utf8" });
} catch {}
});
for (const transport of externalTransports) attachExternalTransport(logger, transport);
return logger;
}
function getLogger() {
const settings = resolveSettings();
const cachedLogger = loggingState.cachedLogger;
const cachedSettings = loggingState.cachedSettings;
if (!cachedLogger || settingsChanged(cachedSettings, settings)) {
loggingState.cachedLogger = buildLogger(settings);
loggingState.cachedSettings = settings;
}
return loggingState.cachedLogger;
}
function getChildLogger(bindings, opts) {
const base = getLogger();
const minLevel = opts?.level ? levelToMinLevel(opts.level) : void 0;
const name = bindings ? JSON.stringify(bindings) : void 0;
return base.getSubLogger({
name,
minLevel,
prefix: bindings ? [name ?? ""] : []
});
}
function toPinoLikeLogger(logger, level) {
const buildChild = (bindings) => toPinoLikeLogger(logger.getSubLogger({ name: bindings ? JSON.stringify(bindings) : void 0 }), level);
return {
level,
child: buildChild,
trace: (...args) => logger.trace(...args),
debug: (...args) => logger.debug(...args),
info: (...args) => logger.info(...args),
warn: (...args) => logger.warn(...args),
error: (...args) => logger.error(...args),
fatal: (...args) => logger.fatal(...args)
};
}
function getResolvedLoggerSettings() {
return resolveSettings();
}
function formatLocalDate(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
}
function defaultRollingPathForToday() {
const today = formatLocalDate(/* @__PURE__ */ new Date());
return path.join(DEFAULT_LOG_DIR, `${LOG_PREFIX}-${today}${LOG_SUFFIX}`);
}
function isRollingPath(file) {
const base = path.basename(file);
return base.startsWith(`${LOG_PREFIX}-`) && base.endsWith(LOG_SUFFIX) && base.length === `${LOG_PREFIX}-YYYY-MM-DD${LOG_SUFFIX}`.length;
}
function pruneOldRollingLogs(dir) {
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const cutoff = Date.now() - MAX_LOG_AGE_MS;
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.startsWith(`${LOG_PREFIX}-`) || !entry.name.endsWith(LOG_SUFFIX)) continue;
const fullPath = path.join(dir, entry.name);
try {
if (fs.statSync(fullPath).mtimeMs < cutoff) fs.rmSync(fullPath, { force: true });
} catch {}
}
} catch {}
}
//#endregion
//#region src/terminal/palette.ts
const LOBSTER_PALETTE = {
accent: "#FF5A2D",
accentBright: "#FF7A3D",
accentDim: "#D14A22",
info: "#FF8A5B",
success: "#2FBF71",
warn: "#FFB020",
error: "#E23D2D",
muted: "#8B7F77"
};
//#endregion
//#region src/terminal/theme.ts
const hasForceColor = typeof process.env.FORCE_COLOR === "string" && process.env.FORCE_COLOR.trim().length > 0 && process.env.FORCE_COLOR.trim() !== "0";
const baseChalk = process.env.NO_COLOR && !hasForceColor ? new Chalk({ level: 0 }) : chalk;
const hex = (value) => baseChalk.hex(value);
const theme = {
accent: hex(LOBSTER_PALETTE.accent),
accentBright: hex(LOBSTER_PALETTE.accentBright),
accentDim: hex(LOBSTER_PALETTE.accentDim),
info: hex(LOBSTER_PALETTE.info),
success: hex(LOBSTER_PALETTE.success),
warn: hex(LOBSTER_PALETTE.warn),
error: hex(LOBSTER_PALETTE.error),
muted: hex(LOBSTER_PALETTE.muted),
heading: baseChalk.bold.hex(LOBSTER_PALETTE.accent),
command: hex(LOBSTER_PALETTE.accentBright),
option: hex(LOBSTER_PALETTE.warn)
};
const isRich = () => Boolean(baseChalk.level > 0);
const colorize = (rich, color, value) => rich ? color(value) : value;
//#endregion
//#region src/globals.ts
let globalVerbose = false;
let globalYes = false;
function setVerbose(v) {
globalVerbose = v;
}
function isVerbose() {
return globalVerbose;
}
function shouldLogVerbose() {
return globalVerbose || isFileLogLevelEnabled("debug");
}
function logVerbose(message) {
if (!shouldLogVerbose()) return;
try {
getLogger().debug({ message }, "verbose");
} catch {}
if (!globalVerbose) return;
console.log(theme.muted(message));
}
function logVerboseConsole(message) {
if (!globalVerbose) return;
console.log(theme.muted(message));
}
function isYes() {
return globalYes;
}
const success = theme.success;
const warn = theme.warn;
const info = theme.info;
const danger = theme.error;
//#endregion
//#region src/infra/plain-object.ts
/**
* Strict plain-object guard (excludes arrays and host objects).
*/
function isPlainObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
}
//#endregion
//#region src/utils.ts
async function ensureDir(dir) {
await fs.promises.mkdir(dir, { recursive: true });
}
/**
* Check if a file or directory exists at the given path.
*/
async function pathExists(targetPath) {
try {
await fs.promises.access(targetPath);
return true;
} catch {
return false;
}
}
function clampNumber(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function clampInt(value, min, max) {
return clampNumber(Math.floor(value), min, max);
}
/** Alias for clampNumber (shorter, more common name) */
const clamp = clampNumber;
/**
* Escapes special regex characters in a string so it can be used in a RegExp constructor.
*/
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Safely parse JSON, returning null on error instead of throwing.
*/
function safeParseJson(raw) {
try {
return JSON.parse(raw);
} catch {
return null;
}
}
/**
* Type guard for Record<string, unknown> (less strict than isPlainObject).
* Accepts any non-null object that isn't an array.
*/
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeE164(number) {
const digits = number.replace(/^whatsapp:/, "").trim().replace(/[^\d+]/g, "");
if (digits.startsWith("+")) return `+${digits.slice(1)}`;
return `+${digits}`;
}
/**
* "Self-chat mode" heuristic (single phone): the gateway is logged in as the owner's own WhatsApp account,
* and `channels.whatsapp.allowFrom` includes that same number. Used to avoid side-effects that make no sense when the
* "bot" and the human are the same WhatsApp identity (e.g. auto read receipts, @mention JID triggers).
*/
function isSelfChatMode(selfE164, allowFrom) {
if (!selfE164) return false;
if (!Array.isArray(allowFrom) || allowFrom.length === 0) return false;
const normalizedSelf = normalizeE164(selfE164);
return allowFrom.some((n) => {
if (n === "*") return false;
try {
return normalizeE164(String(n)) === normalizedSelf;
} catch {
return false;
}
});
}
function toWhatsappJid(number) {
const withoutPrefix = number.replace(/^whatsapp:/, "").trim();
if (withoutPrefix.includes("@")) return withoutPrefix;
return `${normalizeE164(withoutPrefix).replace(/\D/g, "")}@s.whatsapp.net`;
}
function resolveLidMappingDirs(opts) {
const dirs = /* @__PURE__ */ new Set();
const addDir = (dir) => {
if (!dir) return;
dirs.add(resolveUserPath(dir));
};
addDir(opts?.authDir);
for (const dir of opts?.lidMappingDirs ?? []) addDir(dir);
addDir(resolveOAuthDir());
addDir(path.join(CONFIG_DIR, "credentials"));
return [...dirs];
}
function readLidReverseMapping(lid, opts) {
const mappingFilename = `lid-mapping-${lid}_reverse.json`;
const mappingDirs = resolveLidMappingDirs(opts);
for (const dir of mappingDirs) {
const mappingPath = path.join(dir, mappingFilename);
try {
const data = fs.readFileSync(mappingPath, "utf8");
const phone = JSON.parse(data);
if (phone === null || phone === void 0) continue;
return normalizeE164(String(phone));
} catch {}
}
return null;
}
function jidToE164(jid, opts) {
const match = jid.match(/^(\d+)(?::\d+)?@(s\.whatsapp\.net|hosted)$/);
if (match) return `+${match[1]}`;
const lidMatch = jid.match(/^(\d+)(?::\d+)?@(lid|hosted\.lid)$/);
if (lidMatch) {
const lid = lidMatch[1];
const phone = readLidReverseMapping(lid, opts);
if (phone) return phone;
if (opts?.logMissing ?? shouldLogVerbose()) logVerbose(`LID mapping not found for ${lid}; skipping inbound message`);
}
return null;
}
async function resolveJidToE164(jid, opts) {
if (!jid) return null;
const direct = jidToE164(jid, opts);
if (direct) return direct;
if (!/(@lid|@hosted\.lid)$/.test(jid)) return null;
if (!opts?.lidLookup?.getPNForLID) return null;
try {
const pnJid = await opts.lidLookup.getPNForLID(jid);
if (!pnJid) return null;
return jidToE164(pnJid, opts);
} catch (err) {
if (shouldLogVerbose()) logVerbose(`LID mapping lookup failed for ${jid}: ${String(err)}`);
return null;
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isHighSurrogate(codeUnit) {
return codeUnit >= 55296 && codeUnit <= 56319;
}
function isLowSurrogate(codeUnit) {
return codeUnit >= 56320 && codeUnit <= 57343;
}
function sliceUtf16Safe(input, start, end) {
const len = input.length;
let from = start < 0 ? Math.max(len + start, 0) : Math.min(start, len);
let to = end === void 0 ? 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) {
if (isLowSurrogate(input.charCodeAt(from)) && isHighSurrogate(input.charCodeAt(from - 1))) from += 1;
}
if (to > 0 && to < len) {
if (isHighSurrogate(input.charCodeAt(to - 1)) && isLowSurrogate(input.charCodeAt(to))) to -= 1;
}
return input.slice(from, to);
}
function truncateUtf16Safe(input, maxLen) {
const limit = Math.max(0, Math.floor(maxLen));
if (input.length <= limit) return input;
return sliceUtf16Safe(input, 0, limit);
}
function resolveUserPath(input) {
const trimmed = input.trim();
if (!trimmed) return trimmed;
if (trimmed.startsWith("~")) {
const expanded = expandHomePrefix(trimmed, {
home: resolveRequiredHomeDir(process.env, os.homedir),
env: process.env,
homedir: os.homedir
});
return path.resolve(expanded);
}
return path.resolve(trimmed);
}
function resolveConfigDir(env = process.env, homedir = os.homedir) {
const override = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
if (override) return resolveUserPath(override);
const newDir = path.join(resolveRequiredHomeDir(env, homedir), ".openclaw");
try {
if (fs.existsSync(newDir)) return newDir;
} catch {}
return newDir;
}
function resolveHomeDir() {
return resolveEffectiveHomeDir(process.env, os.homedir);
}
function resolveHomeDisplayPrefix() {
const home = resolveHomeDir();
if (!home) return;
if (process.env.OPENCLAW_HOME?.trim()) return {
home,
prefix: "$OPENCLAW_HOME"
};
return {
home,
prefix: "~"
};
}
function shortenHomePath(input) {
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;
}
function shortenHomeInString(input) {
if (!input) return input;
const display = resolveHomeDisplayPrefix();
if (!display) return input;
return input.split(display.home).join(display.prefix);
}
function displayPath(input) {
return shortenHomePath(input);
}
function displayString(input) {
return shortenHomeInString(input);
}
function formatTerminalLink(label, url, opts) {
const esc = "\x1B";
const safeLabel = label.replaceAll(esc, "");
const safeUrl = url.replaceAll(esc, "");
if (!(opts?.force === true ? true : opts?.force === false ? false : Boolean(process.stdout.isTTY))) return opts?.fallback ?? `${safeLabel} (${safeUrl})`;
return `\u001b]8;;${safeUrl}\u0007${safeLabel}\u001b]8;;\u0007`;
}
const CONFIG_DIR = resolveConfigDir();
//#endregion
//#region src/plugins/commands.ts
const pluginCommands = /* @__PURE__ */ new Map();
let registryLocked = false;
const MAX_ARGS_LENGTH = 4096;
/**
* Reserved command names that plugins cannot override.
* These are built-in commands from commands-registry.data.ts.
*/
const RESERVED_COMMANDS = new Set([
"help",
"commands",
"status",
"whoami",
"context",
"stop",
"restart",
"reset",
"new",
"compact",
"config",
"debug",
"allowlist",
"activation",
"skill",
"subagents",
"kill",
"steer",
"tell",
"model",
"models",
"queue",
"send",
"bash",
"exec",
"think",
"verbose",
"reasoning",
"elevated",
"usage"
]);
/**
* Validate a command name.
* Returns an error message if invalid, or null if valid.
*/
function validateCommandName(name) {
const trimmed = name.trim().toLowerCase();
if (!trimmed) return "Command name cannot be empty";
if (!/^[a-z][a-z0-9_-]*$/.test(trimmed)) return "Command name must start with a letter and contain only letters, numbers, hyphens, and underscores";
if (RESERVED_COMMANDS.has(trimmed)) return `Command name "${trimmed}" is reserved by a built-in command`;
return null;
}
/**
* Register a plugin command.
* Returns an error if the command name is invalid or reserved.
*/
function registerPluginCommand(pluginId, command) {
if (registryLocked) return {
ok: false,
error: "Cannot register commands while processing is in progress"
};
if (typeof command.handler !== "function") return {
ok: false,
error: "Command handler must be a function"
};
const validationError = validateCommandName(command.name);
if (validationError) return {
ok: false,
error: validationError
};
const key = `/${command.name.toLowerCase()}`;
if (pluginCommands.has(key)) {
const existing = pluginCommands.get(key);
return {
ok: false,
error: `Command "${command.name}" already registered by plugin "${existing.pluginId}"`
};
}
pluginCommands.set(key, {
...command,
pluginId
});
logVerbose(`Registered plugin command: ${key} (plugin: ${pluginId})`);
return { ok: true };
}
/**
* Clear all registered plugin commands.
* Called during plugin reload.
*/
function clearPluginCommands() {
pluginCommands.clear();
}
/**
* Check if a command body matches a registered plugin command.
* Returns the command definition and parsed args if matched.
*
* Note: If a command has `acceptsArgs: false` and the user provides arguments,
* the command will not match. This allows the message to fall through to
* built-in handlers or the agent. Document this behavior to plugin authors.
*/
function matchPluginCommand(commandBody) {
const trimmed = commandBody.trim();
if (!trimmed.startsWith("/")) return null;
const spaceIndex = trimmed.indexOf(" ");
const commandName = spaceIndex === -1 ? trimmed : trimmed.slice(0, spaceIndex);
const args = spaceIndex === -1 ? void 0 : trimmed.slice(spaceIndex + 1).trim();
const key = commandName.toLowerCase();
const command = pluginCommands.get(key);
if (!command) return null;
if (args && !command.acceptsArgs) return null;
return {
command,
args: args || void 0
};
}
/**
* Sanitize command arguments to prevent injection attacks.
* Removes control characters and enforces length limits.
*/
function sanitizeArgs(args) {
if (!args) return;
if (args.length > MAX_ARGS_LENGTH) return args.slice(0, MAX_ARGS_LENGTH);
let sanitized = "";
for (const char of args) {
const code = char.charCodeAt(0);
if (!(code <= 31 && code !== 9 && code !== 10 || code === 127)) sanitized += char;
}
return sanitized;
}
/**
* Execute a plugin command handler.
*
* Note: Plugin authors should still validate and sanitize ctx.args for their
* specific use case. This function provides basic defense-in-depth sanitization.
*/
async function executePluginCommand(params) {
const { command, args, senderId, channel, isAuthorizedSender, commandBody, config } = params;
if (command.requireAuth !== false && !isAuthorizedSender) {
logVerbose(`Plugin command /${command.name} blocked: unauthorized sender ${senderId || "<unknown>"}`);
return { text: "⚠️ This command requires authorization." };
}
const sanitizedArgs = sanitizeArgs(args);
const ctx = {
senderId,
channel,
channelId: params.channelId,
isAuthorizedSender,
args: sanitizedArgs,
commandBody,
config,
from: params.from,
to: params.to,
accountId: params.accountId,
messageThreadId: params.messageThreadId
};
registryLocked = true;
try {
const result = await command.handler(ctx);
logVerbose(`Plugin command /${command.name} executed successfully for ${senderId || "unknown"}`);
return result;
} catch (err) {
const error = err;
logVerbose(`Plugin command /${command.name} error: ${error.message}`);
return { text: "⚠️ Command failed. Please try again later." };
} finally {
registryLocked = false;
}
}
/**
* List all registered plugin commands.
* Used for /help and /commands output.
*/
function listPluginCommands() {
return Array.from(pluginCommands.values()).map((cmd) => ({
name: cmd.name,
description: cmd.description,
pluginId: cmd.pluginId
}));
}
/**
* Get plugin command specs for native command registration (e.g., Telegram).
*/
function getPluginCommandSpecs() {
return Array.from(pluginCommands.values()).map((cmd) => ({
name: cmd.name,
description: cmd.description
}));
}
//#endregion
//#region src/plugins/http-path.ts
function normalizePluginHttpPath(path, fallback) {
const trimmed = path?.trim();
if (!trimmed) {
const fallbackTrimmed = fallback?.trim();
if (!fallbackTrimmed) return null;
return fallbackTrimmed.startsWith("/") ? fallbackTrimmed : `/${fallbackTrimmed}`;
}
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
}
//#endregion
//#region src/plugins/registry.ts
function createEmptyPluginRegistry() {
return {
plugins: [],
tools: [],
hooks: [],
typedHooks: [],
channels: [],
providers: [],
gatewayHandlers: {},
httpHandlers: [],
httpRoutes: [],
cliRegistrars: [],
services: [],
commands: [],
diagnostics: []
};
}
function createPluginRegistry(registryParams) {
const registry = createEmptyPluginRegistry();
const coreGatewayMethods = new Set(Object.keys(registryParams.coreGatewayHandlers ?? {}));
const pushDiagnostic = (diag) => {
registry.diagnostics.push(diag);
};
const registerTool = (record, tool, opts) => {
const names = opts?.names ?? (opts?.name ? [opts.name] : []);
const optional = opts?.optional === true;
const factory = typeof tool === "function" ? tool : (_ctx) => tool;
if (typeof tool !== "function") names.push(tool.name);
const normalized = names.map((name) => name.trim()).filter(Boolean);
if (normalized.length > 0) record.toolNames.push(...normalized);
registry.tools.push({
pluginId: record.id,
factory,
names: normalized,
optional,
source: record.source
});
};
const registerHook = (record, events, handler, opts, config) => {
const normalizedEvents = (Array.isArray(events) ? events : [events]).map((event) => event.trim()).filter(Boolean);
const entry = opts?.entry ?? null;
const name = entry?.hook.name ?? opts?.name?.trim();
if (!name) {
pushDiagnostic({
level: "warn",
pluginId: record.id,
source: record.source,
message: "hook registration missing name"
});
return;
}
const description = entry?.hook.description ?? opts?.description ?? "";
const hookEntry = entry ? {
...entry,
hook: {
...entry.hook,
name,
description,
source: "openclaw-plugin",
pluginId: record.id
},
metadata: {
...entry.metadata,
events: normalizedEvents
}
} : {
hook: {
name,
description,
source: "openclaw-plugin",
pluginId: record.id,
filePath: record.source,
baseDir: path.dirname(record.source),
handlerPath: record.source
},
frontmatter: {},
metadata: { events: normalizedEvents },
invocation: { enabled: true }
};
record.hookNames.push(name);
registry.hooks.push({
pluginId: record.id,
entry: hookEntry,
events: normalizedEvents,
source: record.source
});
if (!(config?.hooks?.internal?.enabled === true) || opts?.register === false) return;
for (const event of normalizedEvents) registerInternalHook(event, handler);
};
const registerGatewayMethod = (record, method, handler) => {
const trimmed = method.trim();
if (!trimmed) return;
if (coreGatewayMethods.has(trimmed) || registry.gatewayHandlers[trimmed]) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `gateway method already registered: ${trimmed}`
});
return;
}
registry.gatewayHandlers[trimmed] = handler;
record.gatewayMethods.push(trimmed);
};
const registerHttpHandler = (record, handler) => {
record.httpHandlers += 1;
registry.httpHandlers.push({
pluginId: record.id,
handler,
source: record.source
});
};
const registerHttpRoute = (record, params) => {
const normalizedPath = normalizePluginHttpPath(params.path);
if (!normalizedPath) {
pushDiagnostic({
level: "warn",
pluginId: record.id,
source: record.source,
message: "http route registration missing path"
});
return;
}
if (registry.httpRoutes.some((entry) => entry.path === normalizedPath)) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `http route already registered: ${normalizedPath}`
});
return;
}
record.httpHandlers += 1;
registry.httpRoutes.push({
pluginId: record.id,
path: normalizedPath,
handler: params.handler,
source: record.source
});
};
const registerChannel = (record, registration) => {
const normalized = typeof registration.plugin === "object" ? registration : { plugin: registration };
const plugin = normalized.plugin;
const id = typeof plugin?.id === "string" ? plugin.id.trim() : String(plugin?.id ?? "").trim();
if (!id) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: "channel registration missing id"
});
return;
}
record.channelIds.push(id);
registry.channels.push({
pluginId: record.id,
plugin,
dock: normalized.dock,
source: record.source
});
};
const registerProvider = (record, provider) => {
const id = typeof provider?.id === "string" ? provider.id.trim() : "";
if (!id) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: "provider registration missing id"
});
return;
}
const existing = registry.providers.find((entry) => entry.provider.id === id);
if (existing) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `provider already registered: ${id} (${existing.pluginId})`
});
return;
}
record.providerIds.push(id);
registry.providers.push({
pluginId: record.id,
provider,
source: record.source
});
};
const registerCli = (record, registrar, opts) => {
const commands = (opts?.commands ?? []).map((cmd) => cmd.trim()).filter(Boolean);
record.cliCommands.push(...commands);
registry.cliRegistrars.push({
pluginId: record.id,
register: registrar,
commands,
source: record.source
});
};
const registerService = (record, service) => {
const id = service.id.trim();
if (!id) return;
record.services.push(id);
registry.services.push({
pluginId: record.id,
service,
source: record.source
});
};
const registerCommand = (record, command) => {
const name = command.name.trim();
if (!name) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: "command registration missing name"
});
return;
}
const result = registerPluginCommand(record.id, command);
if (!result.ok) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `command registration failed: ${result.error}`
});
return;
}
record.commands.push(name);
registry.commands.push({
pluginId: record.id,
command,
source: record.source
});
};
const registerTypedHook = (record, hookName, handler, opts) => {
record.hookCount += 1;
registry.typedHooks.push({
pluginId: record.id,
hookName,
handler,
priority: opts?.priority,
source: record.source
});
};
const normalizeLogger = (logger) => ({
info: logger.info,
warn: logger.warn,
error: logger.error,
debug: logger.debug
});
const createApi = (record, params) => {
return {
id: record.id,
name: record.name,
version: record.version,
description: record.description,
source: record.source,
config: params.config,
pluginConfig: params.pluginConfig,
runtime: registryParams.runtime,
logger: normalizeLogger(registryParams.logger),
registerTool