feat(config): add disabled_hooks option for selective hook disabling
Allow users to individually disable built-in hooks via the `disabled_hooks` configuration option in oh-my-opencode.json. This addresses issue #28 where users requested the ability to selectively disable hooks (e.g., comment-checker) that may conflict with their workflow. Available hooks: - 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 Closes #28
This commit is contained in:
@@ -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": {
|
"agents": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"propertyNames": {
|
"propertyNames": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export {
|
|||||||
AgentOverridesSchema,
|
AgentOverridesSchema,
|
||||||
McpNameSchema,
|
McpNameSchema,
|
||||||
AgentNameSchema,
|
AgentNameSchema,
|
||||||
|
HookNameSchema,
|
||||||
} from "./schema"
|
} from "./schema"
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
@@ -12,4 +13,5 @@ export type {
|
|||||||
AgentOverrides,
|
AgentOverrides,
|
||||||
McpName,
|
McpName,
|
||||||
AgentName,
|
AgentName,
|
||||||
|
HookName,
|
||||||
} from "./schema"
|
} from "./schema"
|
||||||
|
|||||||
@@ -24,6 +24,22 @@ export const AgentNameSchema = z.enum([
|
|||||||
"document-writer",
|
"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({
|
export const AgentOverrideConfigSchema = z.object({
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
temperature: z.number().min(0).max(2).optional(),
|
temperature: z.number().min(0).max(2).optional(),
|
||||||
@@ -62,6 +78,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
|||||||
$schema: z.string().optional(),
|
$schema: z.string().optional(),
|
||||||
disabled_mcps: z.array(McpNameSchema).optional(),
|
disabled_mcps: z.array(McpNameSchema).optional(),
|
||||||
disabled_agents: z.array(AgentNameSchema).optional(),
|
disabled_agents: z.array(AgentNameSchema).optional(),
|
||||||
|
disabled_hooks: z.array(HookNameSchema).optional(),
|
||||||
agents: AgentOverridesSchema.optional(),
|
agents: AgentOverridesSchema.optional(),
|
||||||
claude_code: ClaudeCodeConfigSchema.optional(),
|
claude_code: ClaudeCodeConfigSchema.optional(),
|
||||||
google_auth: z.boolean().optional(),
|
google_auth: z.boolean().optional(),
|
||||||
@@ -71,5 +88,6 @@ export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
|
|||||||
export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
|
export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
|
||||||
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
||||||
export type AgentName = z.infer<typeof AgentNameSchema>
|
export type AgentName = z.infer<typeof AgentNameSchema>
|
||||||
|
export type HookName = z.infer<typeof HookNameSchema>
|
||||||
|
|
||||||
export { McpNameSchema, type McpName } from "../mcp/types"
|
export { McpNameSchema, type McpName } from "../mcp/types"
|
||||||
|
|||||||
106
src/index.ts
106
src/index.ts
@@ -42,7 +42,7 @@ import { updateTerminalTitle } from "./features/terminal";
|
|||||||
import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools";
|
import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools";
|
||||||
import { BackgroundManager } from "./features/background-agent";
|
import { BackgroundManager } from "./features/background-agent";
|
||||||
import { createBuiltinMcps } from "./mcp";
|
import { createBuiltinMcps } from "./mcp";
|
||||||
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
|
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig, type HookName } from "./config";
|
||||||
import { log, deepMerge } from "./shared";
|
import { log, deepMerge } from "./shared";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
@@ -103,6 +103,12 @@ function mergeConfigs(
|
|||||||
...(override.disabled_mcps ?? []),
|
...(override.disabled_mcps ?? []),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
disabled_hooks: [
|
||||||
|
...new Set([
|
||||||
|
...(base.disabled_hooks ?? []),
|
||||||
|
...(override.disabled_hooks ?? []),
|
||||||
|
]),
|
||||||
|
],
|
||||||
claude_code: deepMerge(base.claude_code, override.claude_code),
|
claude_code: deepMerge(base.claude_code, override.claude_code),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -135,6 +141,7 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
|
|||||||
agents: config.agents,
|
agents: config.agents,
|
||||||
disabled_agents: config.disabled_agents,
|
disabled_agents: config.disabled_agents,
|
||||||
disabled_mcps: config.disabled_mcps,
|
disabled_mcps: config.disabled_mcps,
|
||||||
|
disabled_hooks: config.disabled_hooks,
|
||||||
claude_code: config.claude_code,
|
claude_code: config.claude_code,
|
||||||
});
|
});
|
||||||
return config;
|
return config;
|
||||||
@@ -142,34 +149,64 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
|
|||||||
|
|
||||||
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||||
const pluginConfig = loadPluginConfig(ctx.directory);
|
const pluginConfig = loadPluginConfig(ctx.directory);
|
||||||
|
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
||||||
|
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
|
||||||
|
|
||||||
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
|
const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
|
||||||
const contextWindowMonitor = createContextWindowMonitorHook(ctx);
|
? createTodoContinuationEnforcer(ctx)
|
||||||
const sessionRecovery = createSessionRecoveryHook(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
|
// Wire up recovery state tracking between session-recovery and todo-continuation-enforcer
|
||||||
// This prevents the continuation enforcer from injecting prompts during active recovery
|
// This prevents the continuation enforcer from injecting prompts during active recovery
|
||||||
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
|
if (sessionRecovery && todoContinuationEnforcer) {
|
||||||
sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
|
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
|
||||||
|
sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
|
||||||
|
}
|
||||||
|
|
||||||
const commentChecker = createCommentCheckerHooks();
|
const commentChecker = isHookEnabled("comment-checker")
|
||||||
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
|
? createCommentCheckerHooks()
|
||||||
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
|
: null;
|
||||||
const directoryReadmeInjector = createDirectoryReadmeInjectorHook(ctx);
|
const grepOutputTruncator = isHookEnabled("grep-output-truncator")
|
||||||
const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
|
? createGrepOutputTruncatorHook(ctx)
|
||||||
const thinkMode = createThinkModeHook();
|
: 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, {
|
const claudeCodeHooks = createClaudeCodeHooksHook(ctx, {
|
||||||
disabledHooks: (pluginConfig.claude_code?.hooks ?? true) ? undefined : true,
|
disabledHooks: (pluginConfig.claude_code?.hooks ?? true) ? undefined : true,
|
||||||
});
|
});
|
||||||
const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx);
|
const anthropicAutoCompact = isHookEnabled("anthropic-auto-compact")
|
||||||
const rulesInjector = createRulesInjectorHook(ctx);
|
? createAnthropicAutoCompactHook(ctx)
|
||||||
const autoUpdateChecker = createAutoUpdateCheckerHook(ctx);
|
: null;
|
||||||
|
const rulesInjector = isHookEnabled("rules-injector")
|
||||||
|
? createRulesInjectorHook(ctx)
|
||||||
|
: null;
|
||||||
|
const autoUpdateChecker = isHookEnabled("auto-update-checker")
|
||||||
|
? createAutoUpdateCheckerHook(ctx)
|
||||||
|
: null;
|
||||||
|
|
||||||
updateTerminalTitle({ sessionId: "main" });
|
updateTerminalTitle({ sessionId: "main" });
|
||||||
|
|
||||||
const backgroundManager = new BackgroundManager(ctx);
|
const backgroundManager = new BackgroundManager(ctx);
|
||||||
|
|
||||||
const backgroundNotificationHook = createBackgroundNotificationHook(backgroundManager);
|
const backgroundNotificationHook = isHookEnabled("background-notification")
|
||||||
|
? createBackgroundNotificationHook(backgroundManager)
|
||||||
|
: null;
|
||||||
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
|
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
|
||||||
|
|
||||||
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
|
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
|
||||||
@@ -252,16 +289,16 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
event: async (input) => {
|
event: async (input) => {
|
||||||
await autoUpdateChecker.event(input);
|
await autoUpdateChecker?.event(input);
|
||||||
await claudeCodeHooks.event(input);
|
await claudeCodeHooks.event(input);
|
||||||
await backgroundNotificationHook.event(input);
|
await backgroundNotificationHook?.event(input);
|
||||||
await todoContinuationEnforcer.handler(input);
|
await todoContinuationEnforcer?.handler(input);
|
||||||
await contextWindowMonitor.event(input);
|
await contextWindowMonitor?.event(input);
|
||||||
await directoryAgentsInjector.event(input);
|
await directoryAgentsInjector?.event(input);
|
||||||
await directoryReadmeInjector.event(input);
|
await directoryReadmeInjector?.event(input);
|
||||||
await rulesInjector.event(input);
|
await rulesInjector?.event(input);
|
||||||
await thinkMode.event(input);
|
await thinkMode?.event(input);
|
||||||
await anthropicAutoCompact.event(input);
|
await anthropicAutoCompact?.event(input);
|
||||||
|
|
||||||
const { event } = input;
|
const { event } = input;
|
||||||
const props = event.properties as Record<string, unknown> | undefined;
|
const props = event.properties as Record<string, unknown> | undefined;
|
||||||
@@ -313,7 +350,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const sessionID = props?.sessionID as string | undefined;
|
const sessionID = props?.sessionID as string | undefined;
|
||||||
const error = props?.error;
|
const error = props?.error;
|
||||||
|
|
||||||
if (sessionRecovery.isRecoverableError(error)) {
|
if (sessionRecovery?.isRecoverableError(error)) {
|
||||||
const messageInfo = {
|
const messageInfo = {
|
||||||
id: props?.messageID as string | undefined,
|
id: props?.messageID as string | undefined,
|
||||||
role: "assistant" as const,
|
role: "assistant" as const,
|
||||||
@@ -359,7 +396,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
|
|
||||||
"tool.execute.before": async (input, output) => {
|
"tool.execute.before": async (input, output) => {
|
||||||
await claudeCodeHooks["tool.execute.before"](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()) {
|
if (input.sessionID === getMainSessionID()) {
|
||||||
updateTerminalTitle({
|
updateTerminalTitle({
|
||||||
@@ -374,13 +411,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
|
|
||||||
"tool.execute.after": async (input, output) => {
|
"tool.execute.after": async (input, output) => {
|
||||||
await claudeCodeHooks["tool.execute.after"](input, output);
|
await claudeCodeHooks["tool.execute.after"](input, output);
|
||||||
await grepOutputTruncator["tool.execute.after"](input, output);
|
await grepOutputTruncator?.["tool.execute.after"](input, output);
|
||||||
await contextWindowMonitor["tool.execute.after"](input, output);
|
await contextWindowMonitor?.["tool.execute.after"](input, output);
|
||||||
await commentChecker["tool.execute.after"](input, output);
|
await commentChecker?.["tool.execute.after"](input, output);
|
||||||
await directoryAgentsInjector["tool.execute.after"](input, output);
|
await directoryAgentsInjector?.["tool.execute.after"](input, output);
|
||||||
await directoryReadmeInjector["tool.execute.after"](input, output);
|
await directoryReadmeInjector?.["tool.execute.after"](input, output);
|
||||||
await rulesInjector["tool.execute.after"](input, output);
|
await rulesInjector?.["tool.execute.after"](input, output);
|
||||||
await emptyTaskResponseDetector["tool.execute.after"](input, output);
|
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
||||||
|
|
||||||
if (input.sessionID === getMainSessionID()) {
|
if (input.sessionID === getMainSessionID()) {
|
||||||
updateTerminalTitle({
|
updateTerminalTitle({
|
||||||
@@ -402,4 +439,5 @@ export type {
|
|||||||
AgentOverrideConfig,
|
AgentOverrideConfig,
|
||||||
AgentOverrides,
|
AgentOverrides,
|
||||||
McpName,
|
McpName,
|
||||||
|
HookName,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
|
|||||||
Reference in New Issue
Block a user