import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; import { getHookGroups } from "../claude"; import { hookReminders, type HookModuleContext } from "../hook-context"; import { subagentIdentity } from "../subagent"; import type { HookExecutionContext, NotifyFn, SettingsFile, UserPromptSubmitResult, } from "../types"; import { addContext, collectMatchingHooks, getStringField, runHooksParallel, } from "./shared"; export async function triggerUserPromptSubmitHooks( context: HookExecutionContext, settings: SettingsFile | undefined, notify?: NotifyFn, ): Promise { const groups = getHookGroups(settings, "UserPromptSubmit"); const result: UserPromptSubmitResult = { blocked: false }; const collected = collectMatchingHooks( groups, context, "", [], () => undefined, ); const results = await runHooksParallel( collected, context, "UserPromptSubmit", ); for (const { hook, hookResult, plainStdout, jsonOutput, commonOutput, error, } of results) { if (error) { notify?.(`UserPromptSubmit execution error: ${String(error)}`, "error"); continue; } if (hookResult.exitCode === 0 && jsonOutput) { const additionalContext = getStringField( commonOutput?.hookSpecificOutput?.additionalContext, jsonOutput.additionalContext, ); result.contexts = addContext(result.contexts, hook, additionalContext); if (commonOutput?.systemMessage) { notify?.(commonOutput.systemMessage, "warning"); } if ( jsonOutput.decision !== undefined && jsonOutput.decision !== "block" ) { notify?.( `UserPromptSubmit ignoring invalid decision: ${String(jsonOutput.decision)}`, "warning", ); } if (jsonOutput.decision === "block") { result.blocked = true; result.reason = getStringField(jsonOutput.reason) ?? "Blocked by hook"; } } else if (hookResult.exitCode === 0 && plainStdout) { // Claude Code adds plain-text stdout as context for UserPromptSubmit, // UserPromptExpansion and SessionStart (hooks reference, "Exit code 0"). // Canonical hooks such as `codegraph prompt-hook` print bare text, so // treating it as context is the compatible behavior — no JSON envelope, // no wrapper command. result.contexts = addContext(result.contexts, hook, plainStdout); } if (hookResult.exitCode !== 0) { notify?.( `UserPromptSubmit failed (exit ${hookResult.exitCode}): ${hookResult.stderr}`, "error", ); } } return result; } export function registerPromptHooks( pi: ExtensionAPI, shared: HookModuleContext, ) { pi.on("input", async (event, ctx) => { const delivery = shared.captureContext(); shared.resetInjectedContext(); shared.pendingUserPromptContext = undefined; // Claude Code never fires UserPromptSubmit for a subagent's prompt. if (subagentIdentity(ctx)) return; const result = await triggerUserPromptSubmitHooks( { sessionId: shared.getSessionId(ctx), cwd: ctx.cwd, hookEventName: "UserPromptSubmit", transcriptPath: ctx.sessionManager.getSessionFile(), prompt: event.text, asyncContextSink: delivery.injectHiddenContext, }, await shared.settingsFor(ctx), (msg, type) => shared.notify(ctx, msg, type), ); if (!delivery.isActive()) return { handled: true }; if (result.blocked) { shared.notify( ctx, `UserPromptSubmit blocked: ${result.reason ?? "Blocked by hook"}`, "warning", ); return { handled: true }; } if (result.contexts) { shared.pendingUserPromptContext = result.contexts; } }); pi.on("before_agent_start", async (_event, _ctx) => { if (!shared.pendingUserPromptContext) { return; } const contexts = shared.pendingUserPromptContext; shared.pendingUserPromptContext = undefined; const details = { hookEventName: "UserPromptSubmit" } as const; return { message: { customType: "omp-hooks-plus", content: hookReminders(contexts, details), display: false, details, }, }; }); }