openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
414 lines (413 loc) • 19.1 kB
JavaScript
import "./src-CdRlGRnW.mjs";
import { t as expectDefined } from "./expect-lbe3Hgrh.mjs";
import { r as defaultRuntime } from "./runtime-DlO3PTAd.mjs";
import { p as shortenHomePath } from "./utils-DgZn2dLu.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs";
import { l as rethrowExpectedCliError, r as formatCliJsonFailure, t as ExpectedCliError } from "./failure-output-gtb9zaTk.mjs";
import { n as requestExitAfterOneShotOutput } from "./one-shot-exit-5AeLhgu4.mjs";
import { r as theme } from "./theme-DzaUZY4q.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import { T as tryResolveLegacyCompatibilityAgentId, d as resolveAgentWorkspaceDir, g as resolveDefaultAgentId, m as resolveConfiguredAgentId } from "./agent-scope-config-CDYpzA0r.mjs";
import "./session-key-C09ow4h0.mjs";
import "./agent-scope-CG17uXFn.mjs";
import { r as getRuntimeConfig } from "./io.runtime-BsUXSsD4.mjs";
import { a as transformConfigFile } from "./mutate-DJq1JylX.mjs";
import "./config-DLycWFvB.mjs";
import { i as canFallbackToImplicitLocalGateway } from "./gateway-rpc-0aouJxQt.mjs";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-C8RatpKO.mjs";
import { n as loadGatewayStartupPluginPlanWithMetadata } from "./gateway-startup-plugin-loader-CpygbEAY.mjs";
import "./channel-plugin-ids-CyAmLWKz.mjs";
import { r as resolveHookEntries } from "./policy-DntIYegk.mjs";
import { t as summarizeStringEntries } from "./string-sample-BYGbtG9S.mjs";
import { n as decorativePrefix, t as decorativeEmoji } from "./decorative-emoji-DbUbxlxW.mjs";
import { n as renderTable, t as getTerminalTableWidth } from "./table-CvWMfO8o.mjs";
import { t as resolveOptionFromCommand } from "./cli-utils-ClVfZRCr.mjs";
import { t as formatDocsHelp } from "./help-format-xds_bDSp.mjs";
import { s as withPluginDiagnosticsReport } from "./status-ms_HxCBN.mjs";
import { t as loadWorkspaceHookEntries } from "./workspace-CHNTN1ai.mjs";
import { t as buildWorkspaceHookStatus } from "./hooks-status-W30spsRF.mjs";
import { t as formatCliRequirements } from "./skills-hooks-cli.format-D0aZzj-h.mjs";
import { t as runNativeHookRelayCli } from "./native-hook-relay-cli-DyOpT0pq.mjs";
//#region src/cli/hooks-cli.format.ts
function formatHookStatus(hook, detailed = false) {
if (hook.loadable) return theme.success(detailed ? "✓ Ready" : "✓ ready");
if (!hook.enabledByConfig) return theme.warn(decorativePrefix("⏸", detailed ? "Disabled" : "disabled"));
const reason = hook.blockedReason && hook.blockedReason !== "missing requirements" ? hook.blockedReason : detailed ? "missing requirements" : "missing";
return theme.error(`✗ ${detailed ? `${reason.charAt(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) => ({
Status: formatHookStatus(hook),
Hook: formatHookName(hook),
Description: theme.muted(hook.description),
Source: formatHookSource(hook),
Missing: opts.verbose ? theme.warn(formatHookMissingSummary(hook)) : ""
}));
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
});
return [`${theme.heading("Hooks")} ${theme.muted(`(${eligible.length}/${hooks.length} ready)`)}`, renderTable({
width: tableWidth,
columns,
rows
}).trimEnd()].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 emoji = hook.emoji ?? decorativeEmoji("🔗");
const lines = [
`${emoji ? `${emoji} ` : ""}${theme.heading(hook.name)} ${formatHookStatus(hook, true)}`,
"",
hook.description,
"",
theme.heading("Details:"),
`${theme.muted(" Source:")} ${hook.source}${hook.managedByPlugin ? ` (${hook.pluginId ?? "unknown"})` : ""}`,
`${theme.muted(" Path:")} ${shortenHomePath(hook.filePath)}`,
`${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}`);
lines.push(...formatCliRequirements(hook, HOOK_REQUIREMENT_GROUPS, hook.configChecks));
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 = [
theme.heading("Hooks Status"),
"",
`${theme.muted("Total hooks:")} ${report.hooks.length}`,
`${theme.success("Ready:")} ${eligible.length}`,
`${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)
};
}
async function withHooksReport(config, target, consume) {
const workspaceDir = target.workspaceDir;
const workspaceEntries = loadWorkspaceHookEntries(workspaceDir, { config });
const startup = loadGatewayStartupPluginPlanWithMetadata({
config,
workspaceDir,
env: process.env
});
return withPluginDiagnosticsReport({
config,
workspaceDir,
onlyPluginIds: startup.plan.pluginIds,
metadataSnapshot: startup.metadataSnapshot
}, (pluginReport) => {
const pluginEntries = pluginReport.hooks.map((hook) => hook.entry);
const entries = resolveHookEntries([...pluginEntries, ...workspaceEntries]);
return consume(buildWorkspaceHookStatus(workspaceDir, {
config,
entries
}));
});
}
async function loadHooksReport(agentId, consume) {
const config = getRuntimeConfig({ skipPluginValidation: true });
const target = resolveHooksReportTarget(config, agentId);
const { callGateway } = await import("./call-DLG6_hH-.mjs");
let report;
try {
report = 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 withHooksReport(config, target, consume);
}
return consume(report);
}
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 committed = await transformConfigFile({ transform: (config) => withHooksReport(config, resolveHooksReportTarget(config, agentId), (report) => {
const hook = resolveHookSelection(report, 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
};
return {
nextConfig: {
...config,
hooks: {
...config.hooks,
internal: {
...config.hooks?.internal,
...enabled ? { enabled: true } : {},
entries
}
}
},
result: {
name: hook.name,
emoji: hook.emoji
}
};
}) });
const selectedHook = expectDefined(committed.result, "hook mutation result");
const prefix = enabled ? `${theme.success("✓")} Enabled hook:` : theme.warn(decorativePrefix("⏸", "Disabled hook:"));
const name = selectedHook.emoji ? `${selectedHook.emoji} ${theme.command(selectedHook.name)}` : decorativePrefix("🔗", theme.command(selectedHook.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", () => formatDocsHelp("/cli/hooks"));
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.`);
});
const reportAction = (format) => (opts, command) => runOneShotHooksCliAction(async () => {
const json = hasJsonOutput(opts);
writeHooksOutput(await loadHooksReport(resolveOptionFromCommand(command, "agent"), (report) => format(report, {
...opts,
json
})), json);
}, "root");
const runHooksList = reportAction(formatHooksList);
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(runHooksList);
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 json = hasJsonOutput(opts);
const result = await loadHooksReport(resolveOptionFromCommand(command, "agent"), (report) => {
const hook = resolveHookSelection(report, name);
return {
output: formatHookInfo(hook, name, {
...opts,
json
}),
exitCode: hook ? 0 : 1
};
});
writeHooksOutput(result.output, json);
return result.exitCode;
}, "root"));
hooks.command("check").description("Check hooks eligibility status").option("--agent <id>", "Agent id to inspect").option("--json", "Output as JSON", false).action(reportAction(formatHooksCheck));
for (const enabled of [true, false]) hooks.command(`${enabled ? "enable" : "disable"} <name>`).description(enabled ? "Enable a hook" : "Disable a hook").option("--agent <id>", "Agent id whose workspace to inspect").action(async (name, _opts, command) => runOneShotHooksCliAction(() => setHookEnabled(name, enabled, resolveOptionFromCommand(command, "agent"))));
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) => {
const { runPluginInstallCommand } = await import("./plugins-install-command-CrA5OC8h.mjs");
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) => {
const { runPluginUpdateCommand } = await import("./plugins-update-command-x-CaKiHt.mjs");
defaultRuntime.log(theme.warn("`openclaw hooks update` is deprecated; use `openclaw plugins update`."));
await runPluginUpdateCommand({
ids: id ? [id] : [],
opts
});
});
hooks.action(runHooksList);
}
//#endregion
export { registerHooksCli };