Spaces:
Paused
Paused
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import type { OpenClawConfig, HookConfig } from "../config/config.js"; | |
| import type { HookEligibilityContext, HookEntry } from "./types.js"; | |
| import { resolveHookKey } from "./frontmatter.js"; | |
| const DEFAULT_CONFIG_VALUES: Record<string, boolean> = { | |
| "browser.enabled": true, | |
| "browser.evaluateEnabled": true, | |
| "workspace.dir": true, | |
| }; | |
| function isTruthy(value: unknown): boolean { | |
| if (value === undefined || value === null) { | |
| return false; | |
| } | |
| if (typeof value === "boolean") { | |
| return value; | |
| } | |
| if (typeof value === "number") { | |
| return value !== 0; | |
| } | |
| if (typeof value === "string") { | |
| return value.trim().length > 0; | |
| } | |
| return true; | |
| } | |
| export function resolveConfigPath(config: OpenClawConfig | undefined, pathStr: string) { | |
| const parts = pathStr.split(".").filter(Boolean); | |
| let current: unknown = config; | |
| for (const part of parts) { | |
| if (typeof current !== "object" || current === null) { | |
| return undefined; | |
| } | |
| current = (current as Record<string, unknown>)[part]; | |
| } | |
| return current; | |
| } | |
| export function isConfigPathTruthy(config: OpenClawConfig | undefined, pathStr: string): boolean { | |
| const value = resolveConfigPath(config, pathStr); | |
| if (value === undefined && pathStr in DEFAULT_CONFIG_VALUES) { | |
| return DEFAULT_CONFIG_VALUES[pathStr]; | |
| } | |
| return isTruthy(value); | |
| } | |
| export function resolveHookConfig( | |
| config: OpenClawConfig | undefined, | |
| hookKey: string, | |
| ): HookConfig | undefined { | |
| const hooks = config?.hooks?.internal?.entries; | |
| if (!hooks || typeof hooks !== "object") { | |
| return undefined; | |
| } | |
| const entry = (hooks as Record<string, HookConfig | undefined>)[hookKey]; | |
| if (!entry || typeof entry !== "object") { | |
| return undefined; | |
| } | |
| return entry; | |
| } | |
| export function resolveRuntimePlatform(): string { | |
| return process.platform; | |
| } | |
| export function hasBinary(bin: string): boolean { | |
| const pathEnv = process.env.PATH ?? ""; | |
| const parts = pathEnv.split(path.delimiter).filter(Boolean); | |
| for (const part of parts) { | |
| const candidate = path.join(part, bin); | |
| try { | |
| fs.accessSync(candidate, fs.constants.X_OK); | |
| return true; | |
| } catch { | |
| // keep scanning | |
| } | |
| } | |
| return false; | |
| } | |
| export function shouldIncludeHook(params: { | |
| entry: HookEntry; | |
| config?: OpenClawConfig; | |
| eligibility?: HookEligibilityContext; | |
| }): boolean { | |
| const { entry, config, eligibility } = params; | |
| const hookKey = resolveHookKey(entry.hook.name, entry); | |
| const hookConfig = resolveHookConfig(config, hookKey); | |
| const pluginManaged = entry.hook.source === "openclaw-plugin"; | |
| const osList = entry.metadata?.os ?? []; | |
| const remotePlatforms = eligibility?.remote?.platforms ?? []; | |
| // Check if explicitly disabled | |
| if (!pluginManaged && hookConfig?.enabled === false) { | |
| return false; | |
| } | |
| // Check OS requirement | |
| if ( | |
| osList.length > 0 && | |
| !osList.includes(resolveRuntimePlatform()) && | |
| !remotePlatforms.some((platform) => osList.includes(platform)) | |
| ) { | |
| return false; | |
| } | |
| // If marked as 'always', bypass all other checks | |
| if (entry.metadata?.always === true) { | |
| return true; | |
| } | |
| // Check required binaries (all must be present) | |
| const requiredBins = entry.metadata?.requires?.bins ?? []; | |
| if (requiredBins.length > 0) { | |
| for (const bin of requiredBins) { | |
| if (hasBinary(bin)) { | |
| continue; | |
| } | |
| if (eligibility?.remote?.hasBin?.(bin)) { | |
| continue; | |
| } | |
| return false; | |
| } | |
| } | |
| // Check anyBins (at least one must be present) | |
| const requiredAnyBins = entry.metadata?.requires?.anyBins ?? []; | |
| if (requiredAnyBins.length > 0) { | |
| const anyFound = | |
| requiredAnyBins.some((bin) => hasBinary(bin)) || | |
| eligibility?.remote?.hasAnyBin?.(requiredAnyBins); | |
| if (!anyFound) { | |
| return false; | |
| } | |
| } | |
| // Check required environment variables | |
| const requiredEnv = entry.metadata?.requires?.env ?? []; | |
| if (requiredEnv.length > 0) { | |
| for (const envName of requiredEnv) { | |
| if (process.env[envName]) { | |
| continue; | |
| } | |
| if (hookConfig?.env?.[envName]) { | |
| continue; | |
| } | |
| return false; | |
| } | |
| } | |
| // Check required config paths | |
| const requiredConfig = entry.metadata?.requires?.config ?? []; | |
| if (requiredConfig.length > 0) { | |
| for (const configPath of requiredConfig) { | |
| if (!isConfigPathTruthy(config, configPath)) { | |
| return false; | |
| } | |
| } | |
| } | |
| return true; | |
| } | |