UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

598 lines (597 loc) 24 kB
import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { t as formatDocsLink } from "./links-CsLBrRff.js"; import { r as theme } from "./theme-vjDs9tao.js"; import { g as shortenHomePath } from "./utils-CCC-BEJH.js"; import { n as defaultRuntime } from "./runtime-B4lgFmsS.js"; import "./agent-scope-MrLta7Pq.js"; import { c as resolveDefaultAgentId, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js"; import { i as getRuntimeConfig, u as readConfigFileSnapshot } from "./io-Gi7-pyU-.js"; import { i as replaceConfigFile } from "./config-C9RxTsn1.js"; import { o as callGateway } from "./call-B5-GYOlf.js"; import { t as setSafeTimeout } from "./timer-delay-T0mHtzex.js"; import { t as ADMIN_SCOPE } from "./operator-scopes-CS3xdS-V.js"; import "./method-scopes-BtdN3y7s.js"; import { n as decorativePrefix, t as decorativeEmoji } from "./decorative-emoji-C7-_uAV4.js"; import { n as renderTable, t as getTerminalTableWidth } from "./table-CpFtGZut.js"; import { a as isNativeHookRelayBridgeStaleRegistrationError, i as invokeNativeHookRelayBridge, s as renderNativeHookRelayUnavailableResponse } from "./native-hook-relay-Df3cOSaT.js"; import { a as resolveHookEntries } from "./config-BKOvQE9K.js"; import { a as buildPluginDiagnosticsReport } from "./status-TB29O9XH.js"; import { n as parseTimeoutMsWithFallback } from "./parse-timeout-DZMZaF9B.js"; import { t as loadWorkspaceHookEntries } from "./workspace-Cj3anEq4.js"; import { t as buildWorkspaceHookStatus } from "./hooks-status-B7g6_KWP.js"; import { n as runPluginInstallCommand } from "./plugins-install-command-BeX9FGtH.js"; import { t as runPluginUpdateCommand } from "./plugins-update-command-a7QRaTV7.js"; import "node:stream"; //#region src/cli/native-hook-relay-cli.ts const MAX_NATIVE_HOOK_STDIN_BYTES = 1024 * 1024; var NativeHookRelayDeadlineError = class extends Error { constructor(timeoutMs) { super(`native hook relay timed out after ${timeoutMs}ms`); this.name = "NativeHookRelayDeadlineError"; } }; /** Run one native hook relay invocation from stdin JSON to stdout/stderr response streams. */ async function runNativeHookRelayCli(opts, deps = {}) { const stdin = deps.stdin ?? process.stdin; const stdout = deps.stdout ?? process.stdout; const stderr = deps.stderr ?? process.stderr; const invokeBridge = deps.invokeBridge ?? invokeNativeHookRelayBridge; const callGatewayFn = deps.callGateway ?? callGateway; const provider = readRequiredOption(opts.provider, "provider"); const relayId = readRequiredOption(opts.relayId, "relay-id"); const generation = opts.generation?.trim() || void 0; const event = readRequiredOption(opts.event, "event"); let timeoutMs; try { timeoutMs = parseTimeoutMsWithFallback(opts.timeout, 5e3); } catch (error) { writeText(stderr, formatRelayCliError("invalid native hook timeout", error)); return 1; } const deadline = createNativeHookRelayDeadline(timeoutMs); try { let rawPayload; try { const rawInput = await readStreamText(stdin, MAX_NATIVE_HOOK_STDIN_BYTES, deadline); rawPayload = rawInput.trim() ? JSON.parse(rawInput) : null; } catch (error) { if (isNativeHookRelayDeadlineError(error)) return writeNativeHookRelayDeadlineResponse({ stdout, stderr, opts, provider, event, error }); writeText(stderr, formatRelayCliError("failed to read native hook input", error)); return 1; } try { const remainingMs = remainingNativeHookRelayDeadlineMs(deadline); const response = await withNativeHookRelayDeadline(deadline, invokeBridge({ provider, relayId, generation, event, rawPayload, registrationTimeoutMs: Math.min(100, remainingMs), timeoutMs: remainingMs })); writeText(stdout, response.stdout); writeText(stderr, response.stderr); return response.exitCode; } catch (error) { if (isNativeHookRelayDeadlineError(error)) return writeNativeHookRelayDeadlineResponse({ stdout, stderr, opts, provider, event, error }); if (isNativeHookRelayBridgeStaleRegistrationError(error)) { writeText(stderr, formatRelayCliError("native hook relay unavailable", error)); return writeNativeHookRelayUnavailableResponse({ stdout, stderr, opts, provider, event }); } } try { const response = await withNativeHookRelayDeadline(deadline, callGatewayFn({ method: "nativeHook.invoke", params: { provider, relayId, generation, event, rawPayload }, timeoutMs: remainingNativeHookRelayDeadlineMs(deadline), signal: deadline.signal, scopes: [ADMIN_SCOPE] })); writeText(stdout, response.stdout); writeText(stderr, response.stderr); return response.exitCode; } catch (error) { if (isNativeHookRelayDeadlineError(error)) return writeNativeHookRelayDeadlineResponse({ stdout, stderr, opts, provider, event, error }); writeText(stderr, formatRelayCliError("native hook relay unavailable", error)); return writeNativeHookRelayUnavailableResponse({ stdout, stderr, opts, provider, event }); } } finally { deadline.dispose(); } } function readRequiredOption(value, name) { if (typeof value === "string" && value.trim()) return value.trim(); throw new Error(`Missing required option --${name}`); } async function readStreamText(stream, maxBytes, deadline) { const chunks = []; let total = 0; const abortRead = () => { destroyReadableStream(stream, createNativeHookRelayDeadlineError(deadline)); }; deadline.signal.addEventListener("abort", abortRead, { once: true }); try { throwIfNativeHookRelayDeadlineExpired(deadline); for await (const chunk of stream) { throwIfNativeHookRelayDeadlineExpired(deadline); const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); total += buffer.byteLength; if (total > maxBytes) throw new Error(`native hook input exceeds ${maxBytes} bytes`); chunks.push(buffer); } throwIfNativeHookRelayDeadlineExpired(deadline); return Buffer.concat(chunks, total).toString("utf8"); } catch (error) { if (isNativeHookRelayDeadlineError(error) || deadline.signal.aborted) throw createNativeHookRelayDeadlineError(deadline); throw error; } finally { deadline.signal.removeEventListener("abort", abortRead); } } function writeText(stream, value) { if (value) stream.write(value); } function formatRelayCliError(prefix, error) { return `${prefix}: ${error instanceof Error ? error.message : String(error)}\n`; } function createNativeHookRelayDeadline(timeoutMs) { const controller = new AbortController(); const timer = setSafeTimeout(() => controller.abort(), timeoutMs); timer.unref?.(); return { expiresAtMs: Date.now() + timeoutMs, signal: controller.signal, timeoutMs, dispose: () => clearTimeout(timer) }; } function createNativeHookRelayDeadlineError(deadline) { return new NativeHookRelayDeadlineError(deadline.timeoutMs); } function isNativeHookRelayDeadlineError(error) { return error instanceof Error && error.name === "NativeHookRelayDeadlineError"; } function remainingNativeHookRelayDeadlineMs(deadline) { const remainingMs = deadline.expiresAtMs - Date.now(); if (remainingMs <= 0 || deadline.signal.aborted) throw createNativeHookRelayDeadlineError(deadline); return Math.max(1, remainingMs); } function throwIfNativeHookRelayDeadlineExpired(deadline) { remainingNativeHookRelayDeadlineMs(deadline); } function destroyReadableStream(stream, error) { const destroy = stream.destroy; if (typeof destroy === "function") { destroy.call(stream, error); return; } stream.pause(); } async function withNativeHookRelayDeadline(deadline, promise) { throwIfNativeHookRelayDeadlineExpired(deadline); return await new Promise((resolve, reject) => { const cleanup = () => deadline.signal.removeEventListener("abort", abort); const abort = () => { cleanup(); reject(createNativeHookRelayDeadlineError(deadline)); }; deadline.signal.addEventListener("abort", abort, { once: true }); promise.then((value) => { cleanup(); resolve(value); }, (error) => { cleanup(); reject(error instanceof Error ? error : new Error(String(error))); }); }); } function writeNativeHookRelayUnavailableResponse(params) { const response = renderNativeHookRelayUnavailableResponse({ provider: params.provider, event: params.event, preToolUseUnavailable: params.opts.preToolUseUnavailable, message: params.message ?? "Native hook relay unavailable" }); writeText(params.stdout, response.stdout); writeText(params.stderr, response.stderr); return response.exitCode; } function writeNativeHookRelayDeadlineResponse(params) { writeText(params.stderr, formatRelayCliError("native hook relay timed out", params.error)); return writeNativeHookRelayUnavailableResponse({ stdout: params.stdout, stderr: params.stderr, opts: params.opts, provider: params.provider, event: params.event, message: "Native hook relay timed out" }); } //#endregion //#region src/cli/hooks-cli.ts function mergeHookEntries(pluginEntries, workspaceEntries) { return resolveHookEntries([...pluginEntries, ...workspaceEntries]); } function buildHooksReport(config) { const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const workspaceEntries = loadWorkspaceHookEntries(workspaceDir, { config }); return buildWorkspaceHookStatus(workspaceDir, { config, entries: mergeHookEntries(buildPluginDiagnosticsReport({ config, workspaceDir }).hooks.map((hook) => hook.entry), workspaceEntries) }); } function resolveHookForToggle(report, hookName, opts) { const hook = report.hooks.find((h) => h.name === hookName); if (!hook) throw new Error(`Hook "${hookName}" not found`); if (hook.managedByPlugin) throw new Error(`Hook "${hookName}" is managed by plugin "${hook.pluginId ?? "unknown"}" and cannot be enabled/disabled.`); if (opts?.requireEligible && !hook.requirementsSatisfied) throw new Error(`Hook "${hookName}" is not eligible (missing requirements)`); return hook; } function buildConfigWithHookEnabled(params) { const entries = { ...params.config.hooks?.internal?.entries }; entries[params.hookName] = { ...entries[params.hookName], enabled: params.enabled }; const internal = { ...params.config.hooks?.internal, ...params.ensureHooksEnabled ? { enabled: true } : {}, entries }; return { ...params.config, hooks: { ...params.config.hooks, internal } }; } function formatHookStatus(hook) { if (hook.loadable) return theme.success("✓ ready"); if (!hook.enabledByConfig) return theme.warn(decorativePrefix("⏸", "disabled")); return theme.error("✗ missing"); } function formatHookName(hook) { const emoji = hook.emoji ?? decorativeEmoji("🔗"); const name = theme.command(hook.name); return emoji ? `${emoji} ${name}` : name; } function formatHookSource(hook) { if (!hook.managedByPlugin) return hook.source; return `plugin:${hook.pluginId ?? "unknown"}`; } function formatHookMissingSummary(hook) { const missing = []; if (hook.missing.bins.length > 0) missing.push(`bins: ${hook.missing.bins.join(", ")}`); if (hook.missing.anyBins.length > 0) missing.push(`anyBins: ${hook.missing.anyBins.join(", ")}`); if (hook.missing.env.length > 0) missing.push(`env: ${hook.missing.env.join(", ")}`); if (hook.missing.config.length > 0) missing.push(`config: ${hook.missing.config.join(", ")}`); if (hook.missing.os.length > 0) missing.push(`os: ${hook.missing.os.join(", ")}`); return missing.join("; "); } function exitHooksCliWithError(err) { defaultRuntime.error(`${theme.error("Error:")} ${formatErrorMessage(err)}`); process.exit(1); } function writeHooksOutput(value, json) { if (json) { defaultRuntime.writeStdout(value); return; } defaultRuntime.log(value); } async function runHooksCliAction(action) { try { await action(); } catch (err) { exitHooksCliWithError(err); } } /** * Format the hooks list output */ function formatHooksList(report, opts) { const hooks = opts.eligible ? report.hooks.filter((h) => h.loadable) : report.hooks; if (opts.json) { const jsonReport = { workspaceDir: report.workspaceDir, managedHooksDir: report.managedHooksDir, hooks: hooks.map((h) => ({ name: h.name, description: h.description, emoji: h.emoji, eligible: h.loadable, disabled: !h.enabledByConfig, enabledByConfig: h.enabledByConfig, requirementsSatisfied: h.requirementsSatisfied, loadable: h.loadable, blockedReason: h.blockedReason, source: h.source, pluginId: h.pluginId, events: h.events, homepage: h.homepage, missing: h.missing, managedByPlugin: h.managedByPlugin })) }; return JSON.stringify(jsonReport, null, 2); } if (hooks.length === 0) return opts.eligible ? `No eligible hooks found. Run \`${formatCliCommand("openclaw hooks list")}\` to see all hooks.` : "No hooks found."; const eligible = hooks.filter((h) => h.loadable); const tableWidth = getTerminalTableWidth(); const rows = hooks.map((hook) => { const missing = formatHookMissingSummary(hook); return { Status: formatHookStatus(hook), Hook: formatHookName(hook), Description: theme.muted(hook.description), Source: formatHookSource(hook), Missing: missing ? theme.warn(missing) : "" }; }); const columns = [ { key: "Status", header: "Status", minWidth: 10 }, { key: "Hook", header: "Hook", minWidth: 18, flex: true }, { key: "Description", header: "Description", minWidth: 24, flex: true }, { key: "Source", header: "Source", minWidth: 12, flex: true } ]; if (opts.verbose) columns.push({ key: "Missing", header: "Missing", minWidth: 18, flex: true }); const lines = []; lines.push(`${theme.heading("Hooks")} ${theme.muted(`(${eligible.length}/${hooks.length} ready)`)}`); lines.push(renderTable({ width: tableWidth, columns, rows }).trimEnd()); return lines.join("\n"); } /** * Format detailed info for a single hook */ function formatHookInfo(report, hookName, opts) { const hook = report.hooks.find((h) => h.name === hookName || h.hookKey === hookName); if (!hook) { if (opts.json) return JSON.stringify({ error: "not found", hook: hookName }, null, 2); return `Hook "${hookName}" not found. Run \`${formatCliCommand("openclaw hooks list")}\` to see available hooks.`; } if (opts.json) return JSON.stringify({ ...hook, eligible: hook.loadable, disabled: !hook.enabledByConfig }, null, 2); const lines = []; const emoji = hook.emoji ?? decorativeEmoji("🔗"); const status = hook.loadable ? theme.success("✓ Ready") : !hook.enabledByConfig ? theme.warn(decorativePrefix("⏸", "Disabled")) : theme.error("✗ Missing requirements"); lines.push(`${emoji ? `${emoji} ` : ""}${theme.heading(hook.name)} ${status}`); lines.push(""); lines.push(hook.description); lines.push(""); lines.push(theme.heading("Details:")); if (hook.managedByPlugin) lines.push(`${theme.muted(" Source:")} ${hook.source} (${hook.pluginId ?? "unknown"})`); else lines.push(`${theme.muted(" Source:")} ${hook.source}`); lines.push(`${theme.muted(" Path:")} ${shortenHomePath(hook.filePath)}`); lines.push(`${theme.muted(" Handler:")} ${shortenHomePath(hook.handlerPath)}`); if (hook.homepage) lines.push(`${theme.muted(" Homepage:")} ${hook.homepage}`); if (hook.events.length > 0) lines.push(`${theme.muted(" Events:")} ${hook.events.join(", ")}`); if (hook.managedByPlugin) lines.push(theme.muted(" Managed by plugin; enable/disable via hooks CLI not available.")); if (hook.blockedReason) lines.push(`${theme.muted(" Blocked reason:")} ${hook.blockedReason}`); if (hook.requirements.bins.length > 0 || hook.requirements.anyBins.length > 0 || hook.requirements.env.length > 0 || hook.requirements.config.length > 0 || hook.requirements.os.length > 0) { lines.push(""); lines.push(theme.heading("Requirements:")); if (hook.requirements.bins.length > 0) { const binsStatus = hook.requirements.bins.map((bin) => { return hook.missing.bins.includes(bin) ? theme.error(`✗ ${bin}`) : theme.success(`✓ ${bin}`); }); lines.push(`${theme.muted(" Binaries:")} ${binsStatus.join(", ")}`); } if (hook.requirements.anyBins.length > 0) { const anyBinsStatus = hook.missing.anyBins.length > 0 ? theme.error(`✗ (any of: ${hook.requirements.anyBins.join(", ")})`) : theme.success(`✓ (any of: ${hook.requirements.anyBins.join(", ")})`); lines.push(`${theme.muted(" Any binary:")} ${anyBinsStatus}`); } if (hook.requirements.env.length > 0) { const envStatus = hook.requirements.env.map((env) => { return hook.missing.env.includes(env) ? theme.error(`✗ ${env}`) : theme.success(`✓ ${env}`); }); lines.push(`${theme.muted(" Environment:")} ${envStatus.join(", ")}`); } if (hook.requirements.config.length > 0) { const configStatus = hook.configChecks.map((check) => { return check.satisfied ? theme.success(`✓ ${check.path}`) : theme.error(`✗ ${check.path}`); }); lines.push(`${theme.muted(" Config:")} ${configStatus.join(", ")}`); } if (hook.requirements.os.length > 0) { const osStatus = hook.missing.os.length > 0 ? theme.error(`✗ (${hook.requirements.os.join(", ")})`) : theme.success(`✓ (${hook.requirements.os.join(", ")})`); lines.push(`${theme.muted(" OS:")} ${osStatus}`); } } return lines.join("\n"); } /** * Format check output */ function formatHooksCheck(report, opts) { if (opts.json) { const eligible = report.hooks.filter((h) => h.loadable); const notEligible = report.hooks.filter((h) => !h.loadable); return JSON.stringify({ total: report.hooks.length, eligible: eligible.length, notEligible: notEligible.length, hooks: { eligible: eligible.map((h) => h.name), notEligible: notEligible.map((h) => ({ name: h.name, blockedReason: h.blockedReason, missing: h.missing })) } }, null, 2); } const eligible = report.hooks.filter((h) => h.loadable); const notEligible = report.hooks.filter((h) => !h.loadable); const lines = []; lines.push(theme.heading("Hooks Status")); lines.push(""); lines.push(`${theme.muted("Total hooks:")} ${report.hooks.length}`); lines.push(`${theme.success("Ready:")} ${eligible.length}`); lines.push(`${theme.warn("Not ready:")} ${notEligible.length}`); if (notEligible.length > 0) { lines.push(""); lines.push(theme.heading("Hooks not ready:")); for (const hook of notEligible) { const reasons = []; if (hook.blockedReason && hook.blockedReason !== "missing requirements") reasons.push(hook.blockedReason); if (hook.missing.bins.length > 0) reasons.push(`bins: ${hook.missing.bins.join(", ")}`); if (hook.missing.anyBins.length > 0) reasons.push(`anyBins: ${hook.missing.anyBins.join(", ")}`); if (hook.missing.env.length > 0) reasons.push(`env: ${hook.missing.env.join(", ")}`); if (hook.missing.config.length > 0) reasons.push(`config: ${hook.missing.config.join(", ")}`); if (hook.missing.os.length > 0) reasons.push(`os: ${hook.missing.os.join(", ")}`); const emoji = hook.emoji ?? decorativeEmoji("🔗"); lines.push(` ${emoji ? `${emoji} ` : ""}${hook.name} - ${reasons.join("; ")}`); } } return lines.join("\n"); } async function enableHook(hookName) { const snapshot = await readConfigFileSnapshot(); const config = snapshot.sourceConfig ?? snapshot.config; const hook = resolveHookForToggle(buildHooksReport(config), hookName, { requireEligible: true }); await replaceConfigFile({ nextConfig: buildConfigWithHookEnabled({ config, hookName, enabled: true, ensureHooksEnabled: true }), ...snapshot.hash !== void 0 ? { baseHash: snapshot.hash } : {} }); defaultRuntime.log(`${theme.success("✓")} Enabled hook: ${hook.emoji ? `${hook.emoji} ${theme.command(hookName)}` : decorativePrefix("🔗", theme.command(hookName))}`); } async function disableHook(hookName) { const snapshot = await readConfigFileSnapshot(); const config = snapshot.sourceConfig ?? snapshot.config; const hook = resolveHookForToggle(buildHooksReport(config), hookName); await replaceConfigFile({ nextConfig: buildConfigWithHookEnabled({ config, hookName, enabled: false }), ...snapshot.hash !== void 0 ? { baseHash: snapshot.hash } : {} }); defaultRuntime.log(`${theme.warn(decorativePrefix("⏸", "Disabled hook:"))} ${hook.emoji ? `${hook.emoji} ${theme.command(hookName)}` : decorativePrefix("🔗", theme.command(hookName))}`); } function registerHooksCli(program) { const hooks = program.command("hooks").description("Manage internal agent hooks").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/hooks", "docs.openclaw.ai/cli/hooks")}\n`); hooks.command("list").description("List all hooks").option("--eligible", "Show only eligible hooks", false).option("--json", "Output as JSON", false).option("-v, --verbose", "Show more details including missing requirements", false).action(async (opts) => runHooksCliAction(async () => { writeHooksOutput(formatHooksList(buildHooksReport(getRuntimeConfig()), opts), opts.json); })); hooks.command("info <name>").description("Show detailed information about a hook").option("--json", "Output as JSON", false).action(async (name, opts) => runHooksCliAction(async () => { writeHooksOutput(formatHookInfo(buildHooksReport(getRuntimeConfig()), name, opts), opts.json); })); hooks.command("check").description("Check hooks eligibility status").option("--json", "Output as JSON", false).action(async (opts) => runHooksCliAction(async () => { writeHooksOutput(formatHooksCheck(buildHooksReport(getRuntimeConfig()), opts), opts.json); })); hooks.command("enable <name>").description("Enable a hook").action(async (name) => runHooksCliAction(async () => { await enableHook(name); })); hooks.command("disable <name>").description("Disable a hook").action(async (name) => runHooksCliAction(async () => { await disableHook(name); })); hooks.command("relay", { hidden: true }).description("Internal native harness hook relay").requiredOption("--provider <provider>", "Native harness provider").requiredOption("--relay-id <id>", "Native hook relay id").option("--generation <generation>", "Native hook relay registration generation").requiredOption("--event <event>", "Native hook event").option("--pre-tool-use-unavailable <mode>", "PreToolUse fallback mode when the originating relay is unavailable").option("--timeout <ms>", "Gateway timeout in ms", "5000").action(async (opts) => runHooksCliAction(async () => { process.exitCode = await runNativeHookRelayCli(opts); })); hooks.command("install").description("Deprecated: install a hook pack via `openclaw plugins install`").argument("<path-or-spec>", "Path to a hook pack or npm package spec").option("-l, --link", "Link a local path instead of copying", false).option("--pin", "Record npm installs as exact resolved <name>@<version>", false).action(async (raw, opts) => { defaultRuntime.log(theme.warn("`openclaw hooks install` is deprecated; use `openclaw plugins install`.")); await runPluginInstallCommand({ raw, opts, invalidateRuntimeCache: false }); }); hooks.command("update").description("Deprecated: update hook packs via `openclaw plugins update`").argument("[id]", "Hook pack id (omit with --all)").option("--all", "Update all tracked hooks", false).option("--dry-run", "Show what would change without writing", false).action(async (id, opts) => { defaultRuntime.log(theme.warn("`openclaw hooks update` is deprecated; use `openclaw plugins update`.")); await runPluginUpdateCommand({ id, opts }); }); hooks.action(async () => runHooksCliAction(async () => { const report = buildHooksReport(getRuntimeConfig()); defaultRuntime.log(formatHooksList(report, {})); })); } //#endregion export { disableHook, enableHook, formatHookInfo, formatHooksCheck, formatHooksList, registerHooksCli };