openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
446 lines (445 loc) • 20.8 kB
JavaScript
import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { n as requestExitAfterOneShotOutput } from "./one-shot-exit-BkFPnjbM.js";
import { n as getRuntimeConfig, s as readConfigFileSnapshot } from "./io.runtime-B9iJRs3w.js";
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { D as tryResolveLegacyCompatibilityAgentId, _ as resolveConfiguredAgentId, m as resolveAgentWorkspaceDir, y as resolveDefaultAgentId } from "./agent-scope-config-DcbEhP0R.js";
import "./session-key-BnWWjqNc.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import "./agent-scope-DbtJyKUL.js";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-B1bPgeKr.js";
import { r as replaceConfigFile } from "./mutate-ZNN4iFCn.js";
import "./config-Cs0XXL3x.js";
import { t as formatDocsLink } from "./links-ClIwBcy4.js";
import { r as theme } from "./theme-vjDs9tao.js";
import { i as canFallbackToImplicitLocalGateway } from "./gateway-rpc-C9fDQ0vT.js";
import { c as rethrowExpectedCliError, r as formatCliJsonFailure, t as ExpectedCliError } from "./failure-output-Dc5ce9_F.js";
import { n as loadGatewayStartupPluginPlanWithMetadata } from "./gateway-startup-plugin-ids-D2iVoeEm.js";
import "./channel-plugin-ids-DIyH-LxI.js";
import { t as summarizeStringEntries } from "./string-sample-BYGbtG9S.js";
import { n as decorativePrefix, t as decorativeEmoji } from "./decorative-emoji-sV_M-EcA.js";
import { n as renderTable, t as getTerminalTableWidth } from "./table-DzN9OC44.js";
import { o as resolveHookEntries } from "./config-DWWH6LIu.js";
import { t as resolveOptionFromCommand } from "./cli-utils-D1DAWB8d.js";
import { a as buildPluginDiagnosticsReport } from "./status-DornsoQ9.js";
import { t as runPluginInstallCommand } from "./plugins-install-command-BkLM_g-F.js";
import { t as loadWorkspaceHookEntries } from "./workspace-Bxltjm5Q.js";
import { t as buildWorkspaceHookStatus } from "./hooks-status-DgAi22jb.js";
import { t as runNativeHookRelayCli } from "./native-hook-relay-cli-Cd4_NWEl.js";
import { t as runPluginUpdateCommand } from "./plugins-update-command-BzrgrEZE.js";
//#region src/cli/hooks-cli.format.ts
function formatHookStatus(hook) {
if (hook.loadable) return theme.success("✓ ready");
if (!hook.enabledByConfig) return theme.warn(decorativePrefix("⏸", "disabled"));
return theme.error(`✗ ${formatHookBlockedStatusReason(hook)}`);
}
function formatHookBlockedStatusReason(hook) {
return hook.blockedReason && hook.blockedReason !== "missing requirements" ? hook.blockedReason : "missing";
}
function formatHookInfoBlockedStatusReason(hook) {
const reason = hook.blockedReason && hook.blockedReason !== "missing requirements" ? hook.blockedReason : "missing requirements";
return reason ? `${reason[0]?.toUpperCase() ?? ""}${reason.slice(1)}` : reason;
}
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"}`;
}
const HOOK_REQUIREMENT_GROUPS = [
["bins", "Binaries"],
["anyBins", "Any binary"],
["env", "Environment"],
["config", "Config"],
["os", "OS"]
];
function formatHookMissingRequirements(hook, itemLimit) {
const formatEntries = (entries) => itemLimit === void 0 ? entries.join(", ") : summarizeStringEntries({
entries,
limit: itemLimit
});
return HOOK_REQUIREMENT_GROUPS.filter(([key]) => hook.missing[key].length > 0).map(([key]) => `${key}: ${formatEntries(hook.missing[key])}`);
}
function formatHookMissingSummary(hook, itemLimit) {
const missing = formatHookMissingRequirements(hook, itemLimit);
if (hook.enabledByConfig && hook.blockedReason && hook.blockedReason !== "missing requirements") missing.unshift(hook.blockedReason);
return missing.join("; ");
}
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,
unknownEvents: h.unknownEvents,
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");
}
function formatHookInfo(hook, hookName, opts) {
if (!hook) {
if (opts.json) {
const failure = formatCliJsonFailure(`Hook "${hookName}" not found.`);
return JSON.stringify({
...failure,
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(`✗ ${formatHookInfoBlockedStatusReason(hook)}`);
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.unknownEvents.length > 0) lines.push(theme.warn(` ⚠ Event${hook.unknownEvents.length === 1 ? "" : "s"} not emitted by core (likely typo): ${hook.unknownEvents.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}`);
const requirementGroups = HOOK_REQUIREMENT_GROUPS.filter(([key]) => hook.requirements[key].length > 0);
if (requirementGroups.length > 0) {
lines.push("");
lines.push(theme.heading("Requirements:"));
const formatStatus = (value, satisfied) => satisfied ? theme.success(`✓ ${value}`) : theme.error(`✗ ${value}`);
for (const [key, label] of requirementGroups) {
const required = hook.requirements[key];
const missing = hook.missing[key];
let requirementStatus;
if (key === "anyBins" || key === "os") requirementStatus = formatStatus(`(${key === "anyBins" ? "any of: " : ""}${required.join(", ")})`, missing.length === 0);
else if (key === "config") requirementStatus = hook.configChecks.map((check) => formatStatus(check.path, check.satisfied)).join(", ");
else requirementStatus = required.map((value) => formatStatus(value, !missing.includes(value))).join(", ");
lines.push(`${theme.muted(` ${label}:`)} ${requirementStatus}`);
}
}
return lines.join("\n");
}
function formatHooksCheck(report, opts) {
const eligible = report.hooks.filter((h) => h.loadable);
const notEligible = report.hooks.filter((h) => !h.loadable);
if (opts.json) 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 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 = formatHookMissingRequirements(hook);
if (hook.blockedReason && hook.blockedReason !== "missing requirements") reasons.unshift(hook.blockedReason);
const emoji = hook.emoji ?? decorativeEmoji("🔗");
lines.push(` ${emoji ? `${emoji} ` : ""}${hook.name} - ${reasons.join("; ")}`);
}
}
return lines.join("\n");
}
//#endregion
//#region src/cli/hooks-cli.ts
const GATEWAY_HOOKS_STATUS_TIMEOUT_MS = 1500;
function resolveHooksReportTarget(config, rawAgentId) {
const requested = rawAgentId?.trim();
if (rawAgentId !== void 0 && !requested) throw new Error("--agent must not be blank");
const requestedAgentId = requested ? normalizeAgentId(requested) : void 0;
if (requestedAgentId) resolveConfiguredAgentId(config, requestedAgentId);
const agentId = requestedAgentId ?? tryResolveLegacyCompatibilityAgentId(config) ?? resolveDefaultAgentId(config, {
surface: "hooks status reporting",
hint: "Pass --agent <id> to select a configured agent."
});
return {
agentId,
workspaceDir: resolveAgentWorkspaceDir(config, agentId)
};
}
function buildHooksReport(config, target) {
const workspaceDir = target.workspaceDir;
const workspaceEntries = loadWorkspaceHookEntries(workspaceDir, { config });
const startup = loadGatewayStartupPluginPlanWithMetadata({
config,
workspaceDir,
env: process.env
});
const pluginEntries = buildPluginDiagnosticsReport({
config,
workspaceDir,
onlyPluginIds: startup.plan.pluginIds,
metadataSnapshot: startup.metadataSnapshot
}).hooks.map((hook) => hook.entry);
const entries = resolveHookEntries([...pluginEntries, ...workspaceEntries]);
return buildWorkspaceHookStatus(workspaceDir, {
config,
entries
});
}
async function loadHooksReport(agentId) {
const config = getRuntimeConfig({ skipPluginValidation: true });
const target = resolveHooksReportTarget(config, agentId);
const { callGateway } = await import("./call-Bo4Qx1ax.js");
try {
return await callGateway({
config,
method: "hooks.status",
params: { agentId: target.agentId },
timeoutMs: GATEWAY_HOOKS_STATUS_TIMEOUT_MS,
clientName: GATEWAY_CLIENT_NAMES.CLI,
mode: GATEWAY_CLIENT_MODES.CLI
});
} catch (error) {
if (!await canFallbackToImplicitLocalGateway({
config,
error,
legacyMethod: "hooks.status",
legacyAgentId: true
})) throw error;
return buildHooksReport(config, target);
}
}
function resolveHooksAgentOption(command) {
return resolveOptionFromCommand(command, "agent");
}
function resolveHookSelection(report, hookName) {
const nameMatches = report.hooks.filter((hook) => hook.name === hookName);
const matches = nameMatches.length > 0 ? nameMatches : report.hooks.filter((hook) => hook.hookKey === hookName);
if (matches.length > 1) {
const candidates = summarizeStringEntries({
entries: matches.map((hook) => `${hook.name} (${hook.hookKey})`),
limit: 5
});
throw new Error(`Hook "${hookName}" is ambiguous; matches: ${candidates}. Use a unique hook name or hook key.`);
}
return matches[0];
}
function writeHooksOutput(value, json) {
if (json) {
defaultRuntime.writeStdout(value);
return;
}
defaultRuntime.log(value);
}
async function runOneShotHooksCliAction(action, failureOwner = "command") {
const result = await action().catch((err) => {
rethrowExpectedCliError(err);
const message = formatErrorMessage(err);
const humanOutput = `${theme.error("Error:")} ${message}`;
if (failureOwner === "root") throw new ExpectedCliError({
message,
humanOutput,
machineOutput: message
});
defaultRuntime.error(humanOutput);
defaultRuntime.exit(1);
throw new Error("unreachable");
});
requestExitAfterOneShotOutput(defaultRuntime, typeof result === "number" ? result : 0);
}
async function setHookEnabled(hookName, enabled, agentId) {
const snapshot = await readConfigFileSnapshot();
const config = snapshot.sourceConfig ?? snapshot.config;
const hook = resolveHookSelection(buildHooksReport(config, resolveHooksReportTarget(config, agentId)), hookName);
if (!hook) throw new Error(`Hook "${hookName}" not found. Run \`${formatCliCommand("openclaw hooks list")}\` to see available hooks.`);
if (hook.managedByPlugin) throw new Error(`Hook "${hookName}" is managed by plugin "${hook.pluginId ?? "unknown"}" and cannot be enabled/disabled.`);
if (enabled && !hook.requirementsSatisfied) {
const missing = formatHookMissingSummary(hook, 3);
const installHint = hook.install.length ? ` Install options: ${summarizeStringEntries({
entries: hook.install.map((option) => option.label),
limit: 3
})}.` : "";
throw new Error(`Hook "${hookName}" is not eligible; missing ${missing}.${installHint} Run \`${formatCliCommand(`openclaw hooks info ${hookName}`)}\` for details.`);
}
const entries = { ...config.hooks?.internal?.entries };
entries[hook.hookKey] = {
...entries[hook.hookKey],
enabled
};
const nextConfig = {
...config,
hooks: {
...config.hooks,
internal: {
...config.hooks?.internal,
...enabled ? { enabled: true } : {},
entries
}
}
};
await replaceConfigFile({
nextConfig,
...snapshot.hash !== void 0 ? { baseHash: snapshot.hash } : {}
});
const prefix = enabled ? `${theme.success("✓")} Enabled hook:` : theme.warn(decorativePrefix("⏸", "Disabled hook:"));
const name = hook.emoji ? `${hook.emoji} ${theme.command(hook.name)}` : decorativePrefix("🔗", theme.command(hook.name));
defaultRuntime.log(`${prefix} ${name}`);
}
function registerHooksCli(program) {
const hooks = program.command("hooks").description("Manage internal agent hooks").option("--agent <id>", "Agent id to inspect").option("--json", "Output as JSON", false).addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/hooks", "docs.openclaw.ai/cli/hooks")}\n`);
const hasJsonOutput = (opts) => Boolean(opts?.json || hooks.opts().json);
hooks.hook("preAction", (_thisCommand, actionCommand) => {
const parentAgent = hooks.opts().agent;
if (parentAgent !== void 0 && !parentAgent.trim()) throw new Error("--agent must not be blank");
if (parentAgent && actionCommand !== hooks && !(/* @__PURE__ */ new Set([
"list",
"info",
"check",
"enable",
"disable"
])).has(actionCommand.name())) throw new Error(`openclaw hooks ${actionCommand.name()} does not support --agent; the option only selects an owner for read-only hook reports.`);
});
hooks.command("list").description("List all hooks").option("--agent <id>", "Agent id to inspect").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, command) => runOneShotHooksCliAction(async () => {
const report = await loadHooksReport(resolveHooksAgentOption(command));
const json = hasJsonOutput(opts);
writeHooksOutput(formatHooksList(report, {
...opts,
json
}), json);
}, "root"));
hooks.command("info <name>").description("Show detailed information about a hook").option("--agent <id>", "Agent id to inspect").option("--json", "Output as JSON", false).action(async (name, opts, command) => runOneShotHooksCliAction(async () => {
const report = await loadHooksReport(resolveHooksAgentOption(command));
const json = hasJsonOutput(opts);
const hook = resolveHookSelection(report, name);
writeHooksOutput(formatHookInfo(hook, name, {
...opts,
json
}), json);
return hook ? 0 : 1;
}, "root"));
hooks.command("check").description("Check hooks eligibility status").option("--agent <id>", "Agent id to inspect").option("--json", "Output as JSON", false).action(async (opts, command) => runOneShotHooksCliAction(async () => {
const report = await loadHooksReport(resolveHooksAgentOption(command));
const json = hasJsonOutput(opts);
writeHooksOutput(formatHooksCheck(report, {
...opts,
json
}), json);
}, "root"));
hooks.command("enable <name>").description("Enable a hook").option("--agent <id>", "Agent id whose workspace to inspect").action(async (name, _opts, command) => runOneShotHooksCliAction(async () => {
await setHookEnabled(name, true, resolveHooksAgentOption(command));
}));
hooks.command("disable <name>").description("Disable a hook").option("--agent <id>", "Agent id whose workspace to inspect").action(async (name, _opts, command) => runOneShotHooksCliAction(async () => {
await setHookEnabled(name, false, resolveHooksAgentOption(command));
}));
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("--state-db <path>", "Shared state database path").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) => runOneShotHooksCliAction(() => 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).option("--force", "Confirm non-ClawHub sources and overwrite an existing hook pack", false).option("--acknowledge-install-policy-warning", "Acknowledge security.installPolicy warnings without prompting; blocks and failures remain terminal", false).action(async (raw, opts) => {
defaultRuntime.log(theme.warn("`openclaw hooks install` is deprecated; use `openclaw plugins install`."));
await runPluginInstallCommand({
raw,
opts,
allowInstallPolicyWarningPrompt: true,
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).option("--acknowledge-install-policy-warning", "Acknowledge security.installPolicy warnings without prompting; blocks and failures remain terminal", 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 (opts, command) => runOneShotHooksCliAction(async () => {
const report = await loadHooksReport(resolveHooksAgentOption(command));
const json = hasJsonOutput(opts);
writeHooksOutput(formatHooksList(report, {
...opts,
json
}), json);
}, "root"));
}
//#endregion
export { registerHooksCli };