// ============================================================================= // Generated by adapters/opencode/adapter.sh — do not edit. // mbifc-hooks.js — opencode plugin that dispatches events to bash hook scripts // ============================================================================= // This file is generated at build time. It contains a vendored bash executor // (from adapters/opencode/templates/bash-executor.js) and a hook registry // synthesised from the source hooks/*.hook.yaml files. // ============================================================================= // ── bash-executor.js (vendored) ───────────────────────────────────────────── // ============================================================================= // bash-executor.js — Spawn a bash script and pipe neutral JSON context to stdin // ============================================================================= // This file is vendored verbatim into the generated mbifc-hooks.js plugin at // build time. It must not require any npm modules beyond Node.js builtins so // that opencode's embedded runtime can execute it without installation. // ============================================================================= const { spawn } = require("node:child_process"); /** * Run a bash script with a JSON payload on stdin. * * @param {string} scriptPath Absolute path to the .sh file. * @param {object} payload Neutral-schema object to pipe in as JSON. * @param {object} [env] Extra environment variables (merged with process.env). * @returns {Promise<{exitCode:number, stdout:string, stderr:string}>} */ function runBashHook(scriptPath, payload, env = {}) { return new Promise((resolve, reject) => { const child = spawn("bash", [scriptPath], { env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout.on("data", (d) => { stdout += d.toString(); }); child.stderr.on("data", (d) => { stderr += d.toString(); }); child.on("error", reject); child.on("close", (exitCode) => { resolve({ exitCode: exitCode ?? 0, stdout, stderr }); }); try { child.stdin.write(JSON.stringify(payload)); child.stdin.end(); } catch (err) { reject(err); } }); } module.exports = { runBashHook }; // ── Hook registry (generated) ─────────────────────────────────────────────── const HOOKS = [ { "name": "notify", "script": "../hooks/notify.sh", "triggers": [ { "event": "session.idle", "matchTool": [] } ] }, { "name": "protect-system-files", "script": "../hooks/protect-system-files.sh", "triggers": [ { "event": "tool.execute.before", "matchTool": [ "edit", "write" ] } ] }, { "name": "validate-frontmatter", "script": "../hooks/validate-frontmatter.sh", "triggers": [ { "event": "tool.execute.after", "matchTool": [ "write" ] } ] } ]; // ── Helpers ───────────────────────────────────────────────────────────────── /** * Build the neutral JSON payload from an opencode event context. * The opencode event shapes vary per event; we extract the common fields and * leave everything else under `args`. */ function buildPayload(eventName, input) { const tool = (input && (input.tool || input.toolName)) || ""; const args = (input && (input.args || input.toolInput || input.input)) || (input && input.title !== undefined ? { title: input.title, message: input.message } : {}) || {}; const neutralEvent = (() => { switch (eventName) { case "tool.execute.before": return "before-tool-use"; case "tool.execute.after": return "after-tool-use"; case "session.idle": return "on-notification"; case "session.created": return "on-session-start"; case "tui.prompt.append": return "on-prompt-submit"; default: return eventName; } })(); return { event: neutralEvent, tool, args, session_id: (input && input.sessionId) || "", cwd: (input && input.cwd) || process.cwd(), framework: "opencode", platform_dir: ".opencode", dispatcher_name: "AGENTS.md", }; } /** * Check whether a hook's match-tool filter (if any) matches the current tool. */ function matchesTool(hook, tool) { const m = hook.matchTool; if (!m || m.length === 0) return true; return m.includes((tool || "").toLowerCase()); } // ── Plugin entry point ────────────────────────────────────────────────────── module.exports = async function mbifcHooksPlugin({ app, client }) { // Group hooks by opencode event name const byEvent = {}; for (const h of HOOKS) { for (const trig of h.triggers) { (byEvent[trig.event] ||= []).push({ ...h, matchTool: trig.matchTool }); } } // Register one handler per opencode event; dispatch to matching hooks. const handlers = {}; for (const [event, hooks] of Object.entries(byEvent)) { handlers[event] = async (input, output) => { const payload = buildPayload(event, { ...input, ...output }); for (const h of hooks) { if (!matchesTool(h, payload.tool)) continue; const path = require("node:path"); const scriptAbs = path.isAbsolute(h.script) ? h.script : path.join(__dirname, h.script); const { exitCode, stdout, stderr } = await runBashHook(scriptAbs, payload); if (exitCode === 2) { // Documented opencode block mechanism for tool.execute.before const reason = stdout.trim() || stderr.trim() || "exit 2"; throw new Error(`[${h.name}] blocked: ${reason}`); } } }; } return handlers; };