@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,037 lines (1,033 loc) β’ 37 kB
JavaScript
import { Dt as theme, U as CONFIG_DIR, _ as defaultRuntime, lt as shortenHomePath, ot as resolveUserPath } from "./entry.js";
import "./auth-profiles-DFa1zzNy.js";
import { t as formatCliCommand } from "./command-format-D3syQOZg.js";
import "./exec-CBKBIMpA.js";
import { c as resolveAgentWorkspaceDir, l as resolveDefaultAgentId } from "./agent-scope-RzK9Zcks.js";
import "./github-copilot-token-DuFIqfeC.js";
import "./model-DY9t1aT6.js";
import "./pi-model-discovery-Do3xMEtM.js";
import "./frontmatter-D-YR-Ghi.js";
import { a as MANIFEST_KEY, n as isPathInside, r as isPathInsideWithRealpath } from "./scan-paths-B22E7-Cg.js";
import "./skills-DJmGZazd.js";
import "./manifest-registry-DS2iK5AZ.js";
import { i as loadConfig, l as writeConfigFile } from "./config-B2kL1ciP.js";
import "./client-CDjZdZtI.js";
import "./call-DXhJGwEy.js";
import "./message-channel-CVHJDItx.js";
import "./pairing-token-Byh6drgn.js";
import "./subagent-registry-C7Edpn23.js";
import "./sessions-BD5dyLxb.js";
import "./tokens-D5RzuaYP.js";
import "./normalize-Db7Xtx2v.js";
import "./accounts-BpzwDfBB.js";
import "./bindings-KqaGKS1E.js";
import "./logging-CFvkxgcX.js";
import "./send-BFiRlO6V.js";
import "./plugins-RqhjLCb6.js";
import "./send-CWDtU8Gi.js";
import "./with-timeout-nVWy7PWz.js";
import "./deliver-CeVE1Jh-.js";
import "./diagnostic-774btyou.js";
import "./diagnostic-session-state-HO94DMou.js";
import "./accounts-B3D4iWUP.js";
import "./send-DCCBUsNn.js";
import "./image-ops-lDlFpoR2.js";
import "./pi-embedded-helpers-NVEtaJwl.js";
import "./sandbox-CO-R8v6J.js";
import "./common-DcSh1hZE.js";
import "./chrome-52ZF7gmE.js";
import { l as promptYesNo } from "./tailscale-BzRVNhMW.js";
import "./auth-Dq2pFnjj.js";
import "./server-context-C1f7cijA.js";
import "./routes-BJmh1Ify.js";
import "./redact-C2s6sr73.js";
import "./errors-CFEPAPWc.js";
import "./fs-safe-3YwsxSr5.js";
import "./paths-CXKNzNjU.js";
import "./ssrf-Cv2DiHsm.js";
import "./store-e6fIrP17.js";
import "./ports-59j-bA53.js";
import "./trash-DVLJ0k6q.js";
import "./dock-BZuwgj1O.js";
import "./accounts-Jg3_1Y-r.js";
import "./paths-CXpciDEv.js";
import "./thinking-CJPPUYWd.js";
import "./models-config-5zJ39mUb.js";
import "./reply-prefix-DJOqzjBt.js";
import "./memory-cli-C3uas9sI.js";
import "./manager-D3ZmOwqt.js";
import "./gemini-auth-CdgAkz2K.js";
import "./sqlite-1-TCahE6.js";
import "./retry-BcjnIuo-.js";
import "./chunk-Z2NYDchs.js";
import "./markdown-tables-GCHx-nGT.js";
import "./fetch-guard-B5BAqVyD.js";
import "./local-roots-SkDT1Wv8.js";
import "./ir-DAwVi0a7.js";
import "./render-e7fENCYH.js";
import "./commands-registry-CRmMPJQ9.js";
import "./image-QGUQCdGh.js";
import "./tool-display-Cs1uRaRV.js";
import "./runner-D81xREgc.js";
import "./model-catalog-DLoDxnxL.js";
import "./session-utils-C3Oi9cXA.js";
import "./skill-commands-D2rmj6w8.js";
import "./workspace-dirs-BXftTDSV.js";
import "./pairing-store-CruPwgBw.js";
import "./fetch-vg2oFVIH.js";
import "./exec-approvals-BZA6z4HM.js";
import "./nodes-screen-x7YKy8Ay.js";
import "./session-cost-usage-CpxG9Mup.js";
import "./pi-tools.policy-DeRwGLEO.js";
import "./control-service-DGleRjGJ.js";
import "./stagger-CArN2YVJ.js";
import "./channel-selection-DoXlsBhe.js";
import "./send-C3RIrBTr.js";
import "./outbound-attachment-CdFWGHgJ.js";
import "./delivery-queue-DKdWlR61.js";
import "./send-0Nz3O9Jc.js";
import "./resolve-route-BONxzeyg.js";
import "./channel-activity-BYGpAtHP.js";
import "./tables-BrqD0SUa.js";
import "./proxy-DL3MD6-P.js";
import { t as formatDocsLink } from "./links-CW8Bx7rK.js";
import "./cli-utils-CCaEbxAz.js";
import "./help-format-B0pWGnZs.js";
import "./progress-BAHiAaDW.js";
import "./replies-CuUAAFF2.js";
import "./onboard-helpers-CEx2tGVB.js";
import "./prompt-style-DwCXob2h.js";
import "./pairing-labels-DtxjElSq.js";
import { a as extractArchive, c as resolveArchiveKind, i as unscopedPackageName, l as resolvePackedRootDir, o as fileExists, s as readJsonFile, t as resolveSafeInstallDir } from "./install-safe-path-CnPOnUC0.js";
import { a as installPackageDir, i as withTempDir, n as installFromNpmSpecArchive, r as resolveArchiveSourcePath, t as validateRegistryNpmSpec } from "./npm-registry-spec-Dx1jSlNK.js";
import { t as renderTable } from "./table-Ca0mornk.js";
import { a as parseFrontmatter, t as loadWorkspaceHookEntries } from "./workspace-BbCKCKNZ.js";
import { t as buildWorkspaceHookStatus } from "./hooks-status-D_AyIYIF.js";
import { t as buildPluginStatusReport } from "./status-BxazXPbL.js";
import path from "node:path";
import fs from "node:fs";
import fs$1 from "node:fs/promises";
//#region src/hooks/install.ts
const defaultLogger = {};
function validateHookId(hookId) {
if (!hookId) return "invalid hook name: missing";
if (hookId === "." || hookId === "..") return "invalid hook name: reserved path segment";
if (hookId.includes("/") || hookId.includes("\\")) return "invalid hook name: path separators not allowed";
return null;
}
function resolveHookInstallDir(hookId, hooksDir) {
const hooksBase = hooksDir ? resolveUserPath(hooksDir) : path.join(CONFIG_DIR, "hooks");
const hookIdError = validateHookId(hookId);
if (hookIdError) throw new Error(hookIdError);
const targetDirResult = resolveSafeInstallDir({
baseDir: hooksBase,
id: hookId,
invalidNameMessage: "invalid hook name: path traversal detected"
});
if (!targetDirResult.ok) throw new Error(targetDirResult.error);
return targetDirResult.path;
}
async function ensureOpenClawHooks(manifest) {
const hooks = manifest[MANIFEST_KEY]?.hooks;
if (!Array.isArray(hooks)) throw new Error("package.json missing openclaw.hooks");
const list = hooks.map((e) => typeof e === "string" ? e.trim() : "").filter(Boolean);
if (list.length === 0) throw new Error("package.json openclaw.hooks is empty");
return list;
}
function resolveHookInstallModeOptions(params) {
return {
logger: params.logger ?? defaultLogger,
mode: params.mode ?? "install",
dryRun: params.dryRun ?? false
};
}
function resolveTimedHookInstallModeOptions(params) {
return {
...resolveHookInstallModeOptions(params),
timeoutMs: params.timeoutMs ?? 12e4
};
}
async function resolveInstallTargetDir(id, hooksDir) {
const baseHooksDir = hooksDir ? resolveUserPath(hooksDir) : path.join(CONFIG_DIR, "hooks");
await fs$1.mkdir(baseHooksDir, { recursive: true });
const targetDirResult = resolveSafeInstallDir({
baseDir: baseHooksDir,
id,
invalidNameMessage: "invalid hook name: path traversal detected"
});
if (!targetDirResult.ok) return {
ok: false,
error: targetDirResult.error
};
return {
ok: true,
targetDir: targetDirResult.path
};
}
async function resolveHookNameFromDir(hookDir) {
const hookMdPath = path.join(hookDir, "HOOK.md");
if (!await fileExists(hookMdPath)) throw new Error(`HOOK.md missing in ${hookDir}`);
return parseFrontmatter(await fs$1.readFile(hookMdPath, "utf-8")).name || path.basename(hookDir);
}
async function validateHookDir(hookDir) {
if (!await fileExists(path.join(hookDir, "HOOK.md"))) throw new Error(`HOOK.md missing in ${hookDir}`);
if (!await Promise.all([
"handler.ts",
"handler.js",
"index.ts",
"index.js"
].map(async (candidate) => fileExists(path.join(hookDir, candidate)))).then((results) => results.some(Boolean))) throw new Error(`handler.ts/handler.js/index.ts/index.js missing in ${hookDir}`);
}
async function installHookPackageFromDir(params) {
const { logger, timeoutMs, mode, dryRun } = resolveTimedHookInstallModeOptions(params);
const manifestPath = path.join(params.packageDir, "package.json");
if (!await fileExists(manifestPath)) return {
ok: false,
error: "package.json missing"
};
let manifest;
try {
manifest = await readJsonFile(manifestPath);
} catch (err) {
return {
ok: false,
error: `invalid package.json: ${String(err)}`
};
}
let hookEntries;
try {
hookEntries = await ensureOpenClawHooks(manifest);
} catch (err) {
return {
ok: false,
error: String(err)
};
}
const pkgName = typeof manifest.name === "string" ? manifest.name : "";
const hookPackId = pkgName ? unscopedPackageName(pkgName) : path.basename(params.packageDir);
const hookIdError = validateHookId(hookPackId);
if (hookIdError) return {
ok: false,
error: hookIdError
};
if (params.expectedHookPackId && params.expectedHookPackId !== hookPackId) return {
ok: false,
error: `hook pack id mismatch: expected ${params.expectedHookPackId}, got ${hookPackId}`
};
const targetDirResult = await resolveInstallTargetDir(hookPackId, params.hooksDir);
if (!targetDirResult.ok) return {
ok: false,
error: targetDirResult.error
};
const targetDir = targetDirResult.targetDir;
if (mode === "install" && await fileExists(targetDir)) return {
ok: false,
error: `hook pack already exists: ${targetDir} (delete it first)`
};
const resolvedHooks = [];
for (const entry of hookEntries) {
const hookDir = path.resolve(params.packageDir, entry);
if (!isPathInside(params.packageDir, hookDir)) return {
ok: false,
error: `openclaw.hooks entry escapes package directory: ${entry}`
};
await validateHookDir(hookDir);
if (!isPathInsideWithRealpath(params.packageDir, hookDir, { requireRealpath: true })) return {
ok: false,
error: `openclaw.hooks entry resolves outside package directory: ${entry}`
};
const hookName = await resolveHookNameFromDir(hookDir);
resolvedHooks.push(hookName);
}
if (dryRun) return {
ok: true,
hookPackId,
hooks: resolvedHooks,
targetDir,
version: typeof manifest.version === "string" ? manifest.version : void 0
};
const deps = manifest.dependencies ?? {};
const hasDeps = Object.keys(deps).length > 0;
const installRes = await installPackageDir({
sourceDir: params.packageDir,
targetDir,
mode,
timeoutMs,
logger,
copyErrorPrefix: "failed to copy hook pack",
hasDeps,
depsLogMessage: "Installing hook pack dependenciesβ¦"
});
if (!installRes.ok) return installRes;
return {
ok: true,
hookPackId,
hooks: resolvedHooks,
targetDir,
version: typeof manifest.version === "string" ? manifest.version : void 0
};
}
async function installHookFromDir(params) {
const { logger, mode, dryRun } = resolveHookInstallModeOptions(params);
await validateHookDir(params.hookDir);
const hookName = await resolveHookNameFromDir(params.hookDir);
const hookIdError = validateHookId(hookName);
if (hookIdError) return {
ok: false,
error: hookIdError
};
if (params.expectedHookPackId && params.expectedHookPackId !== hookName) return {
ok: false,
error: `hook id mismatch: expected ${params.expectedHookPackId}, got ${hookName}`
};
const targetDirResult = await resolveInstallTargetDir(hookName, params.hooksDir);
if (!targetDirResult.ok) return {
ok: false,
error: targetDirResult.error
};
const targetDir = targetDirResult.targetDir;
if (mode === "install" && await fileExists(targetDir)) return {
ok: false,
error: `hook already exists: ${targetDir} (delete it first)`
};
if (dryRun) return {
ok: true,
hookPackId: hookName,
hooks: [hookName],
targetDir
};
logger.info?.(`Installing to ${targetDir}β¦`);
let backupDir = null;
if (mode === "update" && await fileExists(targetDir)) {
backupDir = `${targetDir}.backup-${Date.now()}`;
await fs$1.rename(targetDir, backupDir);
}
try {
await fs$1.cp(params.hookDir, targetDir, { recursive: true });
} catch (err) {
if (backupDir) {
await fs$1.rm(targetDir, {
recursive: true,
force: true
}).catch(() => void 0);
await fs$1.rename(backupDir, targetDir).catch(() => void 0);
}
return {
ok: false,
error: `failed to copy hook: ${String(err)}`
};
}
if (backupDir) await fs$1.rm(backupDir, {
recursive: true,
force: true
}).catch(() => void 0);
return {
ok: true,
hookPackId: hookName,
hooks: [hookName],
targetDir
};
}
async function installHooksFromArchive(params) {
const logger = params.logger ?? defaultLogger;
const timeoutMs = params.timeoutMs ?? 12e4;
const archivePathResult = await resolveArchiveSourcePath(params.archivePath);
if (!archivePathResult.ok) return archivePathResult;
const archivePath = archivePathResult.path;
return await withTempDir("openclaw-hook-", async (tmpDir) => {
const extractDir = path.join(tmpDir, "extract");
await fs$1.mkdir(extractDir, { recursive: true });
logger.info?.(`Extracting ${archivePath}β¦`);
try {
await extractArchive({
archivePath,
destDir: extractDir,
timeoutMs,
logger
});
} catch (err) {
return {
ok: false,
error: `failed to extract archive: ${String(err)}`
};
}
let rootDir = "";
try {
rootDir = await resolvePackedRootDir(extractDir);
} catch (err) {
return {
ok: false,
error: String(err)
};
}
if (await fileExists(path.join(rootDir, "package.json"))) return await installHookPackageFromDir({
packageDir: rootDir,
hooksDir: params.hooksDir,
timeoutMs,
logger,
mode: params.mode,
dryRun: params.dryRun,
expectedHookPackId: params.expectedHookPackId
});
return await installHookFromDir({
hookDir: rootDir,
hooksDir: params.hooksDir,
logger,
mode: params.mode,
dryRun: params.dryRun,
expectedHookPackId: params.expectedHookPackId
});
});
}
async function installHooksFromNpmSpec(params) {
const { logger, timeoutMs, mode, dryRun } = resolveTimedHookInstallModeOptions(params);
const expectedHookPackId = params.expectedHookPackId;
const spec = params.spec.trim();
const specError = validateRegistryNpmSpec(spec);
if (specError) return {
ok: false,
error: specError
};
logger.info?.(`Downloading ${spec}β¦`);
const flowResult = await installFromNpmSpecArchive({
tempDirPrefix: "openclaw-hook-pack-",
spec,
timeoutMs,
expectedIntegrity: params.expectedIntegrity,
onIntegrityDrift: params.onIntegrityDrift,
warn: (message) => {
logger.warn?.(message);
},
installFromArchive: async ({ archivePath }) => await installHooksFromArchive({
archivePath,
hooksDir: params.hooksDir,
timeoutMs,
logger,
mode,
dryRun,
expectedHookPackId
})
});
if (!flowResult.ok) return flowResult;
if (!flowResult.installResult.ok) return flowResult.installResult;
return {
...flowResult.installResult,
npmResolution: flowResult.npmResolution,
integrityDrift: flowResult.integrityDrift
};
}
async function installHooksFromPath(params) {
const resolved = resolveUserPath(params.path);
if (!await fileExists(resolved)) return {
ok: false,
error: `path not found: ${resolved}`
};
if ((await fs$1.stat(resolved)).isDirectory()) {
if (await fileExists(path.join(resolved, "package.json"))) return await installHookPackageFromDir({
packageDir: resolved,
hooksDir: params.hooksDir,
timeoutMs: params.timeoutMs,
logger: params.logger,
mode: params.mode,
dryRun: params.dryRun,
expectedHookPackId: params.expectedHookPackId
});
return await installHookFromDir({
hookDir: resolved,
hooksDir: params.hooksDir,
logger: params.logger,
mode: params.mode,
dryRun: params.dryRun,
expectedHookPackId: params.expectedHookPackId
});
}
if (!resolveArchiveKind(resolved)) return {
ok: false,
error: `unsupported hook file: ${resolved}`
};
return await installHooksFromArchive({
archivePath: resolved,
hooksDir: params.hooksDir,
timeoutMs: params.timeoutMs,
logger: params.logger,
mode: params.mode,
dryRun: params.dryRun,
expectedHookPackId: params.expectedHookPackId
});
}
//#endregion
//#region src/hooks/installs.ts
function recordHookInstall(cfg, update) {
const { hookId, ...record } = update;
const installs = {
...cfg.hooks?.internal?.installs,
[hookId]: {
...cfg.hooks?.internal?.installs?.[hookId],
...record,
installedAt: record.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
}
};
return {
...cfg,
hooks: {
...cfg.hooks,
internal: {
...cfg.hooks?.internal,
installs: {
...installs,
[hookId]: installs[hookId]
}
}
}
};
}
//#endregion
//#region src/cli/hooks-cli.ts
function mergeHookEntries(pluginEntries, workspaceEntries) {
const merged = /* @__PURE__ */ new Map();
for (const entry of pluginEntries) merged.set(entry.hook.name, entry);
for (const entry of workspaceEntries) merged.set(entry.hook.name, entry);
return Array.from(merged.values());
}
function buildHooksReport(config) {
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
const workspaceEntries = loadWorkspaceHookEntries(workspaceDir, { config });
return buildWorkspaceHookStatus(workspaceDir, {
config,
entries: mergeHookEntries(buildPluginStatusReport({
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.eligible) 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.eligible) return theme.success("β ready");
if (hook.disabled) return theme.warn("βΈ disabled");
return theme.error("β missing");
}
function formatHookName(hook) {
return `${hook.emoji ?? "π"} ${theme.command(hook.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:")} ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
async function runHooksCliAction(action) {
try {
await action();
} catch (err) {
exitHooksCliWithError(err);
}
}
function createInstallLogger() {
return {
info: (msg) => defaultRuntime.log(msg),
warn: (msg) => defaultRuntime.log(theme.warn(msg))
};
}
function logGatewayRestartHint() {
defaultRuntime.log("Restart the gateway to load hooks.");
}
async function readInstalledPackageVersion(dir) {
try {
const raw = await fs$1.readFile(path.join(dir, "package.json"), "utf-8");
const parsed = JSON.parse(raw);
return typeof parsed.version === "string" ? parsed.version : void 0;
} catch {
return;
}
}
function enableInternalHookEntries(config, hookNames) {
const entries = { ...config.hooks?.internal?.entries };
for (const hookName of hookNames) entries[hookName] = {
...entries[hookName],
enabled: true
};
return {
...config,
hooks: {
...config.hooks,
internal: {
...config.hooks?.internal,
enabled: true,
entries
}
}
};
}
/**
* Format the hooks list output
*/
function formatHooksList(report, opts) {
const hooks = opts.eligible ? report.hooks.filter((h) => h.eligible) : 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.eligible,
disabled: h.disabled,
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.eligible);
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
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, null, 2);
const lines = [];
const emoji = hook.emoji ?? "π";
const status = hook.eligible ? theme.success("β Ready") : hook.disabled ? theme.warn("βΈ Disabled") : theme.error("β Missing requirements");
lines.push(`${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.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.eligible);
const notEligible = report.hooks.filter((h) => !h.eligible);
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,
missing: h.missing
}))
}
}, null, 2);
}
const eligible = report.hooks.filter((h) => h.eligible);
const notEligible = report.hooks.filter((h) => !h.eligible);
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.disabled) reasons.push("disabled");
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(", ")}`);
lines.push(` ${hook.emoji ?? "π"} ${hook.name} - ${reasons.join("; ")}`);
}
}
return lines.join("\n");
}
async function enableHook(hookName) {
const config = loadConfig();
const hook = resolveHookForToggle(buildHooksReport(config), hookName, { requireEligible: true });
await writeConfigFile(buildConfigWithHookEnabled({
config,
hookName,
enabled: true,
ensureHooksEnabled: true
}));
defaultRuntime.log(`${theme.success("β")} Enabled hook: ${hook.emoji ?? "π"} ${theme.command(hookName)}`);
}
async function disableHook(hookName) {
const config = loadConfig();
const hook = resolveHookForToggle(buildHooksReport(config), hookName);
await writeConfigFile(buildConfigWithHookEnabled({
config,
hookName,
enabled: false
}));
defaultRuntime.log(`${theme.warn("βΈ")} Disabled hook: ${hook.emoji ?? "π"} ${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 () => {
const report = buildHooksReport(loadConfig());
defaultRuntime.log(formatHooksList(report, opts));
}));
hooks.command("info <name>").description("Show detailed information about a hook").option("--json", "Output as JSON", false).action(async (name, opts) => runHooksCliAction(async () => {
const report = buildHooksReport(loadConfig());
defaultRuntime.log(formatHookInfo(report, name, opts));
}));
hooks.command("check").description("Check hooks eligibility status").option("--json", "Output as JSON", false).action(async (opts) => runHooksCliAction(async () => {
const report = buildHooksReport(loadConfig());
defaultRuntime.log(formatHooksCheck(report, opts));
}));
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("install").description("Install a hook pack (path, archive, or npm spec)").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) => {
const resolved = resolveUserPath(raw);
const cfg = loadConfig();
if (fs.existsSync(resolved)) {
if (opts.link) {
if (!fs.statSync(resolved).isDirectory()) {
defaultRuntime.error("Linked hook paths must be directories.");
process.exit(1);
}
const existing = cfg.hooks?.internal?.load?.extraDirs ?? [];
const merged = Array.from(new Set([...existing, resolved]));
const probe = await installHooksFromPath({
path: resolved,
dryRun: true
});
if (!probe.ok) {
defaultRuntime.error(probe.error);
process.exit(1);
}
let next = {
...cfg,
hooks: {
...cfg.hooks,
internal: {
...cfg.hooks?.internal,
enabled: true,
load: {
...cfg.hooks?.internal?.load,
extraDirs: merged
}
}
}
};
next = enableInternalHookEntries(next, probe.hooks);
next = recordHookInstall(next, {
hookId: probe.hookPackId,
source: "path",
sourcePath: resolved,
installPath: resolved,
version: probe.version,
hooks: probe.hooks
});
await writeConfigFile(next);
defaultRuntime.log(`Linked hook path: ${shortenHomePath(resolved)}`);
logGatewayRestartHint();
return;
}
const result = await installHooksFromPath({
path: resolved,
logger: createInstallLogger()
});
if (!result.ok) {
defaultRuntime.error(result.error);
process.exit(1);
}
let next = enableInternalHookEntries(cfg, result.hooks);
const source = resolveArchiveKind(resolved) ? "archive" : "path";
next = recordHookInstall(next, {
hookId: result.hookPackId,
source,
sourcePath: resolved,
installPath: result.targetDir,
version: result.version,
hooks: result.hooks
});
await writeConfigFile(next);
defaultRuntime.log(`Installed hooks: ${result.hooks.join(", ")}`);
logGatewayRestartHint();
return;
}
if (opts.link) {
defaultRuntime.error("`--link` requires a local path.");
process.exit(1);
}
if (raw.startsWith(".") || raw.startsWith("~") || path.isAbsolute(raw) || raw.endsWith(".zip") || raw.endsWith(".tgz") || raw.endsWith(".tar.gz") || raw.endsWith(".tar")) {
defaultRuntime.error(`Path not found: ${resolved}`);
process.exit(1);
}
const result = await installHooksFromNpmSpec({
spec: raw,
logger: createInstallLogger()
});
if (!result.ok) {
defaultRuntime.error(result.error);
process.exit(1);
}
let next = enableInternalHookEntries(cfg, result.hooks);
const resolvedSpec = result.npmResolution?.resolvedSpec;
const recordSpec = opts.pin && resolvedSpec ? resolvedSpec : raw;
if (opts.pin && !resolvedSpec) defaultRuntime.log(theme.warn("Could not resolve exact npm version for --pin; storing original npm spec."));
if (opts.pin && resolvedSpec) defaultRuntime.log(`Pinned npm install record to ${resolvedSpec}.`);
next = recordHookInstall(next, {
hookId: result.hookPackId,
source: "npm",
spec: recordSpec,
installPath: result.targetDir,
version: result.version,
resolvedName: result.npmResolution?.name,
resolvedVersion: result.npmResolution?.version,
resolvedSpec: result.npmResolution?.resolvedSpec,
integrity: result.npmResolution?.integrity,
shasum: result.npmResolution?.shasum,
resolvedAt: result.npmResolution?.resolvedAt,
hooks: result.hooks
});
await writeConfigFile(next);
defaultRuntime.log(`Installed hooks: ${result.hooks.join(", ")}`);
logGatewayRestartHint();
});
hooks.command("update").description("Update installed hooks (npm installs only)").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) => {
const cfg = loadConfig();
const installs = cfg.hooks?.internal?.installs ?? {};
const targets = opts.all ? Object.keys(installs) : id ? [id] : [];
if (targets.length === 0) {
defaultRuntime.error("Provide a hook id or use --all.");
process.exit(1);
}
let nextCfg = cfg;
let updatedCount = 0;
for (const hookId of targets) {
const record = installs[hookId];
if (!record) {
defaultRuntime.log(theme.warn(`No install record for "${hookId}".`));
continue;
}
if (record.source !== "npm") {
defaultRuntime.log(theme.warn(`Skipping "${hookId}" (source: ${record.source}).`));
continue;
}
if (!record.spec) {
defaultRuntime.log(theme.warn(`Skipping "${hookId}" (missing npm spec).`));
continue;
}
let installPath;
try {
installPath = record.installPath ?? resolveHookInstallDir(hookId);
} catch (err) {
defaultRuntime.log(theme.error(`Invalid install path for "${hookId}": ${String(err)}`));
continue;
}
const currentVersion = await readInstalledPackageVersion(installPath);
if (opts.dryRun) {
const probe = await installHooksFromNpmSpec({
spec: record.spec,
mode: "update",
dryRun: true,
expectedHookPackId: hookId,
expectedIntegrity: record.integrity,
onIntegrityDrift: async (drift) => {
const specLabel = drift.resolution.resolvedSpec ?? drift.spec;
defaultRuntime.log(theme.warn(`Integrity drift detected for "${hookId}" (${specLabel})\nExpected: ${drift.expectedIntegrity}\nActual: ${drift.actualIntegrity}`));
return true;
},
logger: createInstallLogger()
});
if (!probe.ok) {
defaultRuntime.log(theme.error(`Failed to check ${hookId}: ${probe.error}`));
continue;
}
const nextVersion = probe.version ?? "unknown";
const currentLabel = currentVersion ?? "unknown";
if (currentVersion && probe.version && currentVersion === probe.version) defaultRuntime.log(`${hookId} is up to date (${currentLabel}).`);
else defaultRuntime.log(`Would update ${hookId}: ${currentLabel} β ${nextVersion}.`);
continue;
}
const result = await installHooksFromNpmSpec({
spec: record.spec,
mode: "update",
expectedHookPackId: hookId,
expectedIntegrity: record.integrity,
onIntegrityDrift: async (drift) => {
const specLabel = drift.resolution.resolvedSpec ?? drift.spec;
defaultRuntime.log(theme.warn(`Integrity drift detected for "${hookId}" (${specLabel})\nExpected: ${drift.expectedIntegrity}\nActual: ${drift.actualIntegrity}`));
return await promptYesNo(`Continue updating "${hookId}" with this artifact?`);
},
logger: createInstallLogger()
});
if (!result.ok) {
defaultRuntime.log(theme.error(`Failed to update ${hookId}: ${result.error}`));
continue;
}
const nextVersion = result.version ?? await readInstalledPackageVersion(result.targetDir);
nextCfg = recordHookInstall(nextCfg, {
hookId,
source: "npm",
spec: record.spec,
installPath: result.targetDir,
version: nextVersion,
resolvedName: result.npmResolution?.name,
resolvedVersion: result.npmResolution?.version,
resolvedSpec: result.npmResolution?.resolvedSpec,
integrity: result.npmResolution?.integrity,
shasum: result.npmResolution?.shasum,
resolvedAt: result.npmResolution?.resolvedAt,
hooks: result.hooks
});
updatedCount += 1;
const currentLabel = currentVersion ?? "unknown";
const nextLabel = nextVersion ?? "unknown";
if (currentVersion && nextVersion && currentVersion === nextVersion) defaultRuntime.log(`${hookId} already at ${currentLabel}.`);
else defaultRuntime.log(`Updated ${hookId}: ${currentLabel} β ${nextLabel}.`);
}
if (updatedCount > 0) {
await writeConfigFile(nextCfg);
logGatewayRestartHint();
}
});
hooks.action(async () => runHooksCliAction(async () => {
const report = buildHooksReport(loadConfig());
defaultRuntime.log(formatHooksList(report, {}));
}));
}
//#endregion
export { registerHooksCli };