diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 2d66b92..a9d9387 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -31,6 +31,28 @@ ] } }, + "disabled_hooks": { + "type": "array", + "description": "List of built-in hooks to disable. Useful for selectively disabling hooks that may conflict with your workflow.", + "items": { + "type": "string", + "enum": [ + "todo-continuation-enforcer", + "context-window-monitor", + "session-recovery", + "comment-checker", + "grep-output-truncator", + "directory-agents-injector", + "directory-readme-injector", + "empty-task-response-detector", + "think-mode", + "anthropic-auto-compact", + "rules-injector", + "background-notification", + "auto-update-checker" + ] + } + }, "agents": { "type": "object", "propertyNames": { diff --git a/src/config/index.ts b/src/config/index.ts index b6e2ab9..7fcd69e 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -4,6 +4,7 @@ export { AgentOverridesSchema, McpNameSchema, AgentNameSchema, + HookNameSchema, } from "./schema" export type { @@ -12,4 +13,5 @@ export type { AgentOverrides, McpName, AgentName, + HookName, } from "./schema" diff --git a/src/config/schema.ts b/src/config/schema.ts index 7255741..3445bf5 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -24,6 +24,22 @@ export const AgentNameSchema = z.enum([ "document-writer", ]) +export const HookNameSchema = z.enum([ + "todo-continuation-enforcer", + "context-window-monitor", + "session-recovery", + "comment-checker", + "grep-output-truncator", + "directory-agents-injector", + "directory-readme-injector", + "empty-task-response-detector", + "think-mode", + "anthropic-auto-compact", + "rules-injector", + "background-notification", + "auto-update-checker", +]) + export const AgentOverrideConfigSchema = z.object({ model: z.string().optional(), temperature: z.number().min(0).max(2).optional(), @@ -62,6 +78,7 @@ export const OhMyOpenCodeConfigSchema = z.object({ $schema: z.string().optional(), disabled_mcps: z.array(McpNameSchema).optional(), disabled_agents: z.array(AgentNameSchema).optional(), + disabled_hooks: z.array(HookNameSchema).optional(), agents: AgentOverridesSchema.optional(), claude_code: ClaudeCodeConfigSchema.optional(), google_auth: z.boolean().optional(), @@ -71,5 +88,6 @@ export type OhMyOpenCodeConfig = z.infer export type AgentOverrideConfig = z.infer export type AgentOverrides = z.infer export type AgentName = z.infer +export type HookName = z.infer export { McpNameSchema, type McpName } from "../mcp/types" diff --git a/src/index.ts b/src/index.ts index b41493c..a448da7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,7 @@ import { updateTerminalTitle } from "./features/terminal"; import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools"; import { BackgroundManager } from "./features/background-agent"; import { createBuiltinMcps } from "./mcp"; -import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config"; +import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig, type HookName } from "./config"; import { log, deepMerge } from "./shared"; import * as fs from "fs"; import * as path from "path"; @@ -103,6 +103,12 @@ function mergeConfigs( ...(override.disabled_mcps ?? []), ]), ], + disabled_hooks: [ + ...new Set([ + ...(base.disabled_hooks ?? []), + ...(override.disabled_hooks ?? []), + ]), + ], claude_code: deepMerge(base.claude_code, override.claude_code), }; } @@ -135,6 +141,7 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig { agents: config.agents, disabled_agents: config.disabled_agents, disabled_mcps: config.disabled_mcps, + disabled_hooks: config.disabled_hooks, claude_code: config.claude_code, }); return config; @@ -142,34 +149,64 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig { const OhMyOpenCodePlugin: Plugin = async (ctx) => { const pluginConfig = loadPluginConfig(ctx.directory); + const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []); + const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName); - const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx); - const contextWindowMonitor = createContextWindowMonitorHook(ctx); - const sessionRecovery = createSessionRecoveryHook(ctx); + const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer") + ? createTodoContinuationEnforcer(ctx) + : null; + const contextWindowMonitor = isHookEnabled("context-window-monitor") + ? createContextWindowMonitorHook(ctx) + : null; + const sessionRecovery = isHookEnabled("session-recovery") + ? createSessionRecoveryHook(ctx) + : null; // Wire up recovery state tracking between session-recovery and todo-continuation-enforcer // This prevents the continuation enforcer from injecting prompts during active recovery - sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering); - sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete); + if (sessionRecovery && todoContinuationEnforcer) { + sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering); + sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete); + } - const commentChecker = createCommentCheckerHooks(); - const grepOutputTruncator = createGrepOutputTruncatorHook(ctx); - const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx); - const directoryReadmeInjector = createDirectoryReadmeInjectorHook(ctx); - const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx); - const thinkMode = createThinkModeHook(); + const commentChecker = isHookEnabled("comment-checker") + ? createCommentCheckerHooks() + : null; + const grepOutputTruncator = isHookEnabled("grep-output-truncator") + ? createGrepOutputTruncatorHook(ctx) + : null; + const directoryAgentsInjector = isHookEnabled("directory-agents-injector") + ? createDirectoryAgentsInjectorHook(ctx) + : null; + const directoryReadmeInjector = isHookEnabled("directory-readme-injector") + ? createDirectoryReadmeInjectorHook(ctx) + : null; + const emptyTaskResponseDetector = isHookEnabled("empty-task-response-detector") + ? createEmptyTaskResponseDetectorHook(ctx) + : null; + const thinkMode = isHookEnabled("think-mode") + ? createThinkModeHook() + : null; const claudeCodeHooks = createClaudeCodeHooksHook(ctx, { disabledHooks: (pluginConfig.claude_code?.hooks ?? true) ? undefined : true, }); - const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx); - const rulesInjector = createRulesInjectorHook(ctx); - const autoUpdateChecker = createAutoUpdateCheckerHook(ctx); + const anthropicAutoCompact = isHookEnabled("anthropic-auto-compact") + ? createAnthropicAutoCompactHook(ctx) + : null; + const rulesInjector = isHookEnabled("rules-injector") + ? createRulesInjectorHook(ctx) + : null; + const autoUpdateChecker = isHookEnabled("auto-update-checker") + ? createAutoUpdateCheckerHook(ctx) + : null; updateTerminalTitle({ sessionId: "main" }); const backgroundManager = new BackgroundManager(ctx); - const backgroundNotificationHook = createBackgroundNotificationHook(backgroundManager); + const backgroundNotificationHook = isHookEnabled("background-notification") + ? createBackgroundNotificationHook(backgroundManager) + : null; const backgroundTools = createBackgroundTools(backgroundManager, ctx.client); const callOmoAgent = createCallOmoAgent(ctx, backgroundManager); @@ -252,16 +289,16 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { }, event: async (input) => { - await autoUpdateChecker.event(input); + await autoUpdateChecker?.event(input); await claudeCodeHooks.event(input); - await backgroundNotificationHook.event(input); - await todoContinuationEnforcer.handler(input); - await contextWindowMonitor.event(input); - await directoryAgentsInjector.event(input); - await directoryReadmeInjector.event(input); - await rulesInjector.event(input); - await thinkMode.event(input); - await anthropicAutoCompact.event(input); + await backgroundNotificationHook?.event(input); + await todoContinuationEnforcer?.handler(input); + await contextWindowMonitor?.event(input); + await directoryAgentsInjector?.event(input); + await directoryReadmeInjector?.event(input); + await rulesInjector?.event(input); + await thinkMode?.event(input); + await anthropicAutoCompact?.event(input); const { event } = input; const props = event.properties as Record | undefined; @@ -313,7 +350,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const sessionID = props?.sessionID as string | undefined; const error = props?.error; - if (sessionRecovery.isRecoverableError(error)) { + if (sessionRecovery?.isRecoverableError(error)) { const messageInfo = { id: props?.messageID as string | undefined, role: "assistant" as const, @@ -359,7 +396,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { "tool.execute.before": async (input, output) => { await claudeCodeHooks["tool.execute.before"](input, output); - await commentChecker["tool.execute.before"](input, output); + await commentChecker?.["tool.execute.before"](input, output); if (input.sessionID === getMainSessionID()) { updateTerminalTitle({ @@ -374,13 +411,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { "tool.execute.after": async (input, output) => { await claudeCodeHooks["tool.execute.after"](input, output); - await grepOutputTruncator["tool.execute.after"](input, output); - await contextWindowMonitor["tool.execute.after"](input, output); - await commentChecker["tool.execute.after"](input, output); - await directoryAgentsInjector["tool.execute.after"](input, output); - await directoryReadmeInjector["tool.execute.after"](input, output); - await rulesInjector["tool.execute.after"](input, output); - await emptyTaskResponseDetector["tool.execute.after"](input, output); + await grepOutputTruncator?.["tool.execute.after"](input, output); + await contextWindowMonitor?.["tool.execute.after"](input, output); + await commentChecker?.["tool.execute.after"](input, output); + await directoryAgentsInjector?.["tool.execute.after"](input, output); + await directoryReadmeInjector?.["tool.execute.after"](input, output); + await rulesInjector?.["tool.execute.after"](input, output); + await emptyTaskResponseDetector?.["tool.execute.after"](input, output); if (input.sessionID === getMainSessionID()) { updateTerminalTitle({ @@ -402,4 +439,5 @@ export type { AgentOverrideConfig, AgentOverrides, McpName, + HookName, } from "./config";