diff --git a/src/index.ts b/src/index.ts index ee210e66..aa0c6e73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,6 +109,7 @@ import { injectServerAuthIntoClient, } from "./shared"; import { filterDisabledTools } from "./shared/disabled-tools"; +import { safeCreateHook } from "./shared/safe-create-hook"; import { loadPluginConfig } from "./plugin-config"; import { createModelCacheState } from "./plugin-state"; import { createConfigHandler } from "./plugin-handlers"; @@ -135,21 +136,22 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { agent_pane_min_width: pluginConfig.tmux?.agent_pane_min_width ?? 40, } as const; const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName); + const safeHookEnabled = pluginConfig.experimental?.safe_hook_creation ?? true; const modelCacheState = createModelCacheState(); const contextWindowMonitor = isHookEnabled("context-window-monitor") - ? createContextWindowMonitorHook(ctx) + ? safeCreateHook("context-window-monitor", () => createContextWindowMonitorHook(ctx), { enabled: safeHookEnabled }) : null; const preemptiveCompaction = isHookEnabled("preemptive-compaction") && pluginConfig.experimental?.preemptive_compaction - ? createPreemptiveCompactionHook(ctx) + ? safeCreateHook("preemptive-compaction", () => createPreemptiveCompactionHook(ctx), { enabled: safeHookEnabled }) : null; const sessionRecovery = isHookEnabled("session-recovery") - ? createSessionRecoveryHook(ctx, { + ? safeCreateHook("session-recovery", () => createSessionRecoveryHook(ctx, { experimental: pluginConfig.experimental, - }) + }), { enabled: safeHookEnabled }) : null; // Check for conflicting notification plugins before creating session-notification @@ -166,17 +168,17 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { allPlugins: externalNotifier.allPlugins, }); } else { - sessionNotification = createSessionNotification(ctx); + sessionNotification = safeCreateHook("session-notification", () => createSessionNotification(ctx), { enabled: safeHookEnabled }); } } const commentChecker = isHookEnabled("comment-checker") - ? createCommentCheckerHooks(pluginConfig.comment_checker) + ? safeCreateHook("comment-checker", () => createCommentCheckerHooks(pluginConfig.comment_checker), { enabled: safeHookEnabled }) : null; const toolOutputTruncator = isHookEnabled("tool-output-truncator") - ? createToolOutputTruncatorHook(ctx, { + ? safeCreateHook("tool-output-truncator", () => createToolOutputTruncatorHook(ctx, { experimental: pluginConfig.experimental, - }) + }), { enabled: safeHookEnabled }) : null; // Check for native OpenCode AGENTS.md injection support before creating hook let directoryAgentsInjector = null; @@ -195,18 +197,18 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { }, ); } else { - directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx); + directoryAgentsInjector = safeCreateHook("directory-agents-injector", () => createDirectoryAgentsInjectorHook(ctx), { enabled: safeHookEnabled }); } } const directoryReadmeInjector = isHookEnabled("directory-readme-injector") - ? createDirectoryReadmeInjectorHook(ctx) + ? safeCreateHook("directory-readme-injector", () => createDirectoryReadmeInjectorHook(ctx), { enabled: safeHookEnabled }) : null; const emptyTaskResponseDetector = isHookEnabled( "empty-task-response-detector", ) - ? createEmptyTaskResponseDetectorHook(ctx) + ? safeCreateHook("empty-task-response-detector", () => createEmptyTaskResponseDetectorHook(ctx), { enabled: safeHookEnabled }) : null; - const thinkMode = isHookEnabled("think-mode") ? createThinkModeHook() : null; + const thinkMode = isHookEnabled("think-mode") ? safeCreateHook("think-mode", () => createThinkModeHook(), { enabled: safeHookEnabled }) : null; const claudeCodeHooks = createClaudeCodeHooksHook( ctx, { @@ -219,84 +221,84 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const anthropicContextWindowLimitRecovery = isHookEnabled( "anthropic-context-window-limit-recovery", ) - ? createAnthropicContextWindowLimitRecoveryHook(ctx, { + ? safeCreateHook("anthropic-context-window-limit-recovery", () => createAnthropicContextWindowLimitRecoveryHook(ctx, { experimental: pluginConfig.experimental, - }) + }), { enabled: safeHookEnabled }) : null; const rulesInjector = isHookEnabled("rules-injector") - ? createRulesInjectorHook(ctx) + ? safeCreateHook("rules-injector", () => createRulesInjectorHook(ctx), { enabled: safeHookEnabled }) : null; const autoUpdateChecker = isHookEnabled("auto-update-checker") - ? createAutoUpdateCheckerHook(ctx, { + ? safeCreateHook("auto-update-checker", () => createAutoUpdateCheckerHook(ctx, { showStartupToast: isHookEnabled("startup-toast"), isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true, autoUpdate: pluginConfig.auto_update ?? true, - }) + }), { enabled: safeHookEnabled }) : null; const keywordDetector = isHookEnabled("keyword-detector") - ? createKeywordDetectorHook(ctx, contextCollector) + ? safeCreateHook("keyword-detector", () => createKeywordDetectorHook(ctx, contextCollector), { enabled: safeHookEnabled }) : null; const contextInjectorMessagesTransform = createContextInjectorMessagesTransformHook(contextCollector); const agentUsageReminder = isHookEnabled("agent-usage-reminder") - ? createAgentUsageReminderHook(ctx) + ? safeCreateHook("agent-usage-reminder", () => createAgentUsageReminderHook(ctx), { enabled: safeHookEnabled }) : null; const nonInteractiveEnv = isHookEnabled("non-interactive-env") - ? createNonInteractiveEnvHook(ctx) + ? safeCreateHook("non-interactive-env", () => createNonInteractiveEnvHook(ctx), { enabled: safeHookEnabled }) : null; const interactiveBashSession = isHookEnabled("interactive-bash-session") - ? createInteractiveBashSessionHook(ctx) + ? safeCreateHook("interactive-bash-session", () => createInteractiveBashSessionHook(ctx), { enabled: safeHookEnabled }) : null; const thinkingBlockValidator = isHookEnabled("thinking-block-validator") - ? createThinkingBlockValidatorHook() + ? safeCreateHook("thinking-block-validator", () => createThinkingBlockValidatorHook(), { enabled: safeHookEnabled }) : null; let categorySkillReminder: ReturnType | null = null; const ralphLoop = isHookEnabled("ralph-loop") - ? createRalphLoopHook(ctx, { + ? safeCreateHook("ralph-loop", () => createRalphLoopHook(ctx, { config: pluginConfig.ralph_loop, checkSessionExists: async (sessionId) => sessionExists(sessionId), - }) + }), { enabled: safeHookEnabled }) : null; const editErrorRecovery = isHookEnabled("edit-error-recovery") - ? createEditErrorRecoveryHook(ctx) + ? safeCreateHook("edit-error-recovery", () => createEditErrorRecoveryHook(ctx), { enabled: safeHookEnabled }) : null; const delegateTaskRetry = isHookEnabled("delegate-task-retry") - ? createDelegateTaskRetryHook(ctx) + ? safeCreateHook("delegate-task-retry", () => createDelegateTaskRetryHook(ctx), { enabled: safeHookEnabled }) : null; const startWork = isHookEnabled("start-work") - ? createStartWorkHook(ctx) + ? safeCreateHook("start-work", () => createStartWorkHook(ctx), { enabled: safeHookEnabled }) : null; const prometheusMdOnly = isHookEnabled("prometheus-md-only") - ? createPrometheusMdOnlyHook(ctx) + ? safeCreateHook("prometheus-md-only", () => createPrometheusMdOnlyHook(ctx), { enabled: safeHookEnabled }) : null; const sisyphusJuniorNotepad = isHookEnabled("sisyphus-junior-notepad") - ? createSisyphusJuniorNotepadHook(ctx) + ? safeCreateHook("sisyphus-junior-notepad", () => createSisyphusJuniorNotepadHook(ctx), { enabled: safeHookEnabled }) : null; const tasksTodowriteDisabler = isHookEnabled("tasks-todowrite-disabler") - ? createTasksTodowriteDisablerHook({ + ? safeCreateHook("tasks-todowrite-disabler", () => createTasksTodowriteDisablerHook({ experimental: pluginConfig.experimental, - }) + }), { enabled: safeHookEnabled }) : null; const questionLabelTruncator = createQuestionLabelTruncatorHook(); const subagentQuestionBlocker = createSubagentQuestionBlockerHook(); const writeExistingFileGuard = isHookEnabled("write-existing-file-guard") - ? createWriteExistingFileGuardHook(ctx) + ? safeCreateHook("write-existing-file-guard", () => createWriteExistingFileGuardHook(ctx), { enabled: safeHookEnabled }) : null; const taskResumeInfo = createTaskResumeInfoHook(); const anthropicEffort = isHookEnabled("anthropic-effort") - ? createAnthropicEffortHook() + ? safeCreateHook("anthropic-effort", () => createAnthropicEffortHook(), { enabled: safeHookEnabled }) : null; const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig); @@ -333,28 +335,28 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { ); const atlasHook = isHookEnabled("atlas") - ? createAtlasHook(ctx, { directory: ctx.directory, backgroundManager }) + ? safeCreateHook("atlas", () => createAtlasHook(ctx, { directory: ctx.directory, backgroundManager }), { enabled: safeHookEnabled }) : null; initTaskToastManager(ctx.client); const stopContinuationGuard = isHookEnabled("stop-continuation-guard") - ? createStopContinuationGuardHook(ctx) + ? safeCreateHook("stop-continuation-guard", () => createStopContinuationGuardHook(ctx), { enabled: safeHookEnabled }) : null; const compactionContextInjector = isHookEnabled("compaction-context-injector") - ? createCompactionContextInjector() + ? safeCreateHook("compaction-context-injector", () => createCompactionContextInjector(), { enabled: safeHookEnabled }) : null; const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer") - ? createTodoContinuationEnforcer(ctx, { + ? safeCreateHook("todo-continuation-enforcer", () => createTodoContinuationEnforcer(ctx, { backgroundManager, isContinuationStopped: stopContinuationGuard?.isStopped, - }) + }), { enabled: safeHookEnabled }) : null; const unstableAgentBabysitter = isHookEnabled("unstable-agent-babysitter") - ? createUnstableAgentBabysitterHook( + ? safeCreateHook("unstable-agent-babysitter", () => createUnstableAgentBabysitterHook( { directory: ctx.directory, client: { @@ -382,7 +384,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { backgroundManager, config: pluginConfig.babysitting, }, - ) + ), { enabled: safeHookEnabled }) : null; if (sessionRecovery && todoContinuationEnforcer) { @@ -393,7 +395,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { } const backgroundNotificationHook = isHookEnabled("background-notification") - ? createBackgroundNotificationHook(backgroundManager) + ? safeCreateHook("background-notification", () => createBackgroundNotificationHook(backgroundManager), { enabled: safeHookEnabled }) : null; const backgroundTools = createBackgroundTools(backgroundManager, ctx.client); @@ -490,7 +492,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { }); categorySkillReminder = isHookEnabled("category-skill-reminder") - ? createCategorySkillReminderHook(ctx, availableSkills) + ? safeCreateHook("category-skill-reminder", () => createCategorySkillReminderHook(ctx, availableSkills), { enabled: safeHookEnabled }) : null; const skillMcpManager = new SkillMcpManager(); @@ -515,7 +517,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { }); const autoSlashCommand = isHookEnabled("auto-slash-command") - ? createAutoSlashCommandHook({ skills: mergedSkills }) + ? safeCreateHook("auto-slash-command", () => createAutoSlashCommandHook({ skills: mergedSkills }), { enabled: safeHookEnabled }) : null; const configHandler = createConfigHandler({ @@ -610,7 +612,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { await stopContinuationGuard?.["chat.message"]?.(input); await keywordDetector?.["chat.message"]?.(input, output); - await claudeCodeHooks["chat.message"]?.(input, output); + await claudeCodeHooks?.["chat.message"]?.(input, output); await autoSlashCommand?.["chat.message"]?.(input, output); await startWork?.["chat.message"]?.(input, output); @@ -700,7 +702,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { event: async (input) => { await autoUpdateChecker?.event(input); - await claudeCodeHooks.event(input); + await claudeCodeHooks?.event?.(input); await backgroundNotificationHook?.event(input); await sessionNotification?.(input); await todoContinuationEnforcer?.handler(input); @@ -800,10 +802,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { }, "tool.execute.before": async (input, output) => { - await subagentQuestionBlocker["tool.execute.before"]?.(input, output); + await subagentQuestionBlocker?.["tool.execute.before"]?.(input, output); await writeExistingFileGuard?.["tool.execute.before"]?.(input, output); - await questionLabelTruncator["tool.execute.before"]?.(input, output); - await claudeCodeHooks["tool.execute.before"](input, output); + await questionLabelTruncator?.["tool.execute.before"]?.(input, output); + await claudeCodeHooks?.["tool.execute.before"]?.(input, output); await nonInteractiveEnv?.["tool.execute.before"](input, output); await commentChecker?.["tool.execute.before"]?.(input, output); await directoryAgentsInjector?.["tool.execute.before"]?.(input, output); @@ -909,7 +911,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { } } - await claudeCodeHooks["tool.execute.after"](input, output); + await claudeCodeHooks?.["tool.execute.after"]?.(input, output); await toolOutputTruncator?.["tool.execute.after"](input, output); await preemptiveCompaction?.["tool.execute.after"](input, output); await contextWindowMonitor?.["tool.execute.after"](input, output); @@ -924,7 +926,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { await editErrorRecovery?.["tool.execute.after"](input, output); await delegateTaskRetry?.["tool.execute.after"](input, output); await atlasHook?.["tool.execute.after"]?.(input, output); - await taskResumeInfo["tool.execute.after"](input, output); + await taskResumeInfo?.["tool.execute.after"]?.(input, output); }, "experimental.session.compacting": async (