import JSON5 from "json5"; import type { OpenClawHookMetadata, HookEntry, HookInstallSpec, HookInvocationPolicy, ParsedHookFrontmatter, } from "./types.js"; import { LEGACY_MANIFEST_KEYS, MANIFEST_KEY } from "../compat/legacy-names.js"; import { parseFrontmatterBlock } from "../markdown/frontmatter.js"; import { parseBooleanValue } from "../utils/boolean.js"; export function parseFrontmatter(content: string): ParsedHookFrontmatter { return parseFrontmatterBlock(content); } function normalizeStringList(input: unknown): string[] { if (!input) { return []; } if (Array.isArray(input)) { return input.map((value) => String(value).trim()).filter(Boolean); } if (typeof input === "string") { return input .split(",") .map((value) => value.trim()) .filter(Boolean); } return []; } function parseInstallSpec(input: unknown): HookInstallSpec | undefined { if (!input || typeof input !== "object") { return undefined; } const raw = input as Record; const kindRaw = typeof raw.kind === "string" ? raw.kind : typeof raw.type === "string" ? raw.type : ""; const kind = kindRaw.trim().toLowerCase(); if (kind !== "bundled" && kind !== "npm" && kind !== "git") { return undefined; } const spec: HookInstallSpec = { kind: kind, }; if (typeof raw.id === "string") { spec.id = raw.id; } if (typeof raw.label === "string") { spec.label = raw.label; } const bins = normalizeStringList(raw.bins); if (bins.length > 0) { spec.bins = bins; } if (typeof raw.package === "string") { spec.package = raw.package; } if (typeof raw.repository === "string") { spec.repository = raw.repository; } return spec; } function getFrontmatterValue(frontmatter: ParsedHookFrontmatter, key: string): string | undefined { const raw = frontmatter[key]; return typeof raw === "string" ? raw : undefined; } function parseFrontmatterBool(value: string | undefined, fallback: boolean): boolean { const parsed = parseBooleanValue(value); return parsed === undefined ? fallback : parsed; } export function resolveOpenClawMetadata( frontmatter: ParsedHookFrontmatter, ): OpenClawHookMetadata | undefined { const raw = getFrontmatterValue(frontmatter, "metadata"); if (!raw) { return undefined; } try { const parsed = JSON5.parse(raw); if (!parsed || typeof parsed !== "object") { return undefined; } const metadataRawCandidates = [MANIFEST_KEY, ...LEGACY_MANIFEST_KEYS]; let metadataRaw: unknown; for (const key of metadataRawCandidates) { const candidate = parsed[key]; if (candidate && typeof candidate === "object") { metadataRaw = candidate; break; } } if (!metadataRaw || typeof metadataRaw !== "object") { return undefined; } const metadataObj = metadataRaw as Record; const requiresRaw = typeof metadataObj.requires === "object" && metadataObj.requires !== null ? (metadataObj.requires as Record) : undefined; const installRaw = Array.isArray(metadataObj.install) ? (metadataObj.install as unknown[]) : []; const install = installRaw .map((entry) => parseInstallSpec(entry)) .filter((entry): entry is HookInstallSpec => Boolean(entry)); const osRaw = normalizeStringList(metadataObj.os); const eventsRaw = normalizeStringList(metadataObj.events); return { always: typeof metadataObj.always === "boolean" ? metadataObj.always : undefined, emoji: typeof metadataObj.emoji === "string" ? metadataObj.emoji : undefined, homepage: typeof metadataObj.homepage === "string" ? metadataObj.homepage : undefined, hookKey: typeof metadataObj.hookKey === "string" ? metadataObj.hookKey : undefined, export: typeof metadataObj.export === "string" ? metadataObj.export : undefined, os: osRaw.length > 0 ? osRaw : undefined, events: eventsRaw.length > 0 ? eventsRaw : [], requires: requiresRaw ? { bins: normalizeStringList(requiresRaw.bins), anyBins: normalizeStringList(requiresRaw.anyBins), env: normalizeStringList(requiresRaw.env), config: normalizeStringList(requiresRaw.config), } : undefined, install: install.length > 0 ? install : undefined, }; } catch { return undefined; } } export function resolveHookInvocationPolicy( frontmatter: ParsedHookFrontmatter, ): HookInvocationPolicy { return { enabled: parseFrontmatterBool(getFrontmatterValue(frontmatter, "enabled"), true), }; } export function resolveHookKey(hookName: string, entry?: HookEntry): string { return entry?.metadata?.hookKey ?? hookName; }