|
| 1 | +import { z } from "zod" |
| 2 | + |
| 3 | +/** |
| 4 | + * Hook Event Types |
| 5 | + * |
| 6 | + * All supported hook event types that can trigger hook execution. |
| 7 | + * Based on Claude Code's hook system. |
| 8 | + */ |
| 9 | +export const hookEventTypes = [ |
| 10 | + "SessionStart", |
| 11 | + "UserPromptSubmit", |
| 12 | + "PreToolUse", |
| 13 | + "PermissionRequest", |
| 14 | + "PostToolUse", |
| 15 | + "PostToolUseFailure", |
| 16 | + "SubtaskStart", |
| 17 | + "SubtaskStop", |
| 18 | + "Stop", |
| 19 | + "PreCompact", |
| 20 | + "SessionEnd", |
| 21 | +] as const |
| 22 | + |
| 23 | +export const hookEventTypeSchema = z.enum(hookEventTypes) |
| 24 | +export type HookEventType = z.infer<typeof hookEventTypeSchema> |
| 25 | + |
| 26 | +/** |
| 27 | + * Tool Matchers |
| 28 | + * |
| 29 | + * Categories of tools that can be matched for tool-related hooks. |
| 30 | + * Used with PreToolUse, PostToolUse, PostToolUseFailure, and PermissionRequest events. |
| 31 | + */ |
| 32 | +export const toolMatchers = ["read", "edit", "browser", "command", "mcp", "modes"] as const |
| 33 | +export const toolMatcherSchema = z.enum(toolMatchers) |
| 34 | +export type ToolMatcher = z.infer<typeof toolMatcherSchema> |
| 35 | + |
| 36 | +/** |
| 37 | + * Session Matchers |
| 38 | + * |
| 39 | + * Session state types that can be matched for SessionStart hooks. |
| 40 | + */ |
| 41 | +export const sessionMatchers = ["startup", "resume", "clear", "compact"] as const |
| 42 | +export const sessionMatcherSchema = z.enum(sessionMatchers) |
| 43 | +export type SessionMatcher = z.infer<typeof sessionMatcherSchema> |
| 44 | + |
| 45 | +/** |
| 46 | + * Action Types |
| 47 | + * |
| 48 | + * Types of actions that hooks can execute. |
| 49 | + */ |
| 50 | +export const hookActionTypes = ["command", "slashCommand"] as const |
| 51 | +export const hookActionTypeSchema = z.enum(hookActionTypes) |
| 52 | +export type HookActionType = z.infer<typeof hookActionTypeSchema> |
| 53 | + |
| 54 | +/** |
| 55 | + * Shell Command Action |
| 56 | + * |
| 57 | + * Executes a shell command with optional working directory and timeout. |
| 58 | + */ |
| 59 | +export const shellCommandActionSchema = z.object({ |
| 60 | + type: z.literal("command"), |
| 61 | + command: z.string().min(1), |
| 62 | + cwd: z.string().optional(), |
| 63 | + timeout: z.number().min(1).max(300).default(30), |
| 64 | +}) |
| 65 | + |
| 66 | +export type ShellCommandAction = z.infer<typeof shellCommandActionSchema> |
| 67 | + |
| 68 | +/** |
| 69 | + * Slash Command Action |
| 70 | + * |
| 71 | + * Executes an existing slash command defined in the project or globally. |
| 72 | + */ |
| 73 | +export const slashCommandActionSchema = z.object({ |
| 74 | + type: z.literal("slashCommand"), |
| 75 | + command: z.string().min(1).startsWith("/"), |
| 76 | + args: z.string().optional(), |
| 77 | +}) |
| 78 | + |
| 79 | +export type SlashCommandAction = z.infer<typeof slashCommandActionSchema> |
| 80 | + |
| 81 | +/** |
| 82 | + * Hook Action |
| 83 | + * |
| 84 | + * Union of all action types that hooks can execute. |
| 85 | + */ |
| 86 | +export const hookActionSchema = z.discriminatedUnion("type", [shellCommandActionSchema, slashCommandActionSchema]) |
| 87 | + |
| 88 | +export type HookAction = z.infer<typeof hookActionSchema> |
| 89 | + |
| 90 | +/** |
| 91 | + * Tool Matchers Config |
| 92 | + * |
| 93 | + * Configuration for matching tool-related hooks. |
| 94 | + * Includes both category matchers and custom regex patterns. |
| 95 | + */ |
| 96 | +export const toolMatchersConfigSchema = z.object({ |
| 97 | + tools: z.array(toolMatcherSchema).optional(), |
| 98 | + customPattern: z.string().nullable().optional(), |
| 99 | +}) |
| 100 | + |
| 101 | +export type ToolMatchersConfig = z.infer<typeof toolMatchersConfigSchema> |
| 102 | + |
| 103 | +/** |
| 104 | + * Session Matchers Config |
| 105 | + * |
| 106 | + * Configuration for matching SessionStart hooks. |
| 107 | + */ |
| 108 | +export const sessionMatchersConfigSchema = z.object({ |
| 109 | + sessionType: z.array(sessionMatcherSchema).optional(), |
| 110 | +}) |
| 111 | + |
| 112 | +export type SessionMatchersConfig = z.infer<typeof sessionMatchersConfigSchema> |
| 113 | + |
| 114 | +/** |
| 115 | + * Hook Configuration |
| 116 | + * |
| 117 | + * Individual hook configuration including ID, name, enabled state, action, and matchers. |
| 118 | + */ |
| 119 | +export const hookConfigSchema = z.object({ |
| 120 | + id: z |
| 121 | + .string() |
| 122 | + .min(1) |
| 123 | + .regex(/^[a-z0-9-]+$/), |
| 124 | + name: z.string().min(1), |
| 125 | + enabled: z.boolean().default(true), |
| 126 | + action: hookActionSchema, |
| 127 | + matchers: z.union([toolMatchersConfigSchema, sessionMatchersConfigSchema]).optional(), |
| 128 | +}) |
| 129 | + |
| 130 | +export type HookConfig = z.infer<typeof hookConfigSchema> |
| 131 | + |
| 132 | +/** |
| 133 | + * Hooks File Structure |
| 134 | + * |
| 135 | + * Full structure of a .hooks configuration file. |
| 136 | + */ |
| 137 | +export const hooksFileSchema = z.object({ |
| 138 | + $schema: z.string().optional(), |
| 139 | + version: z.literal("1.0"), |
| 140 | + hooks: z.record(hookEventTypeSchema, z.array(hookConfigSchema)).default({}), |
| 141 | +}) |
| 142 | + |
| 143 | +export type HooksFile = z.infer<typeof hooksFileSchema> |
| 144 | + |
| 145 | +/** |
| 146 | + * Hook with Metadata |
| 147 | + * |
| 148 | + * Extended hook configuration with additional metadata for UI display. |
| 149 | + */ |
| 150 | +export interface HookWithMetadata extends HookConfig { |
| 151 | + eventType: HookEventType |
| 152 | + source: "global" | "project" |
| 153 | + filePath?: string |
| 154 | +} |
| 155 | + |
| 156 | +/** |
| 157 | + * Hook Event Descriptions |
| 158 | + * |
| 159 | + * Human-readable descriptions for each hook event type. |
| 160 | + */ |
| 161 | +export const hookEventDescriptions: Record<HookEventType, string> = { |
| 162 | + SessionStart: "Session begins or resumes", |
| 163 | + UserPromptSubmit: "User submits a prompt", |
| 164 | + PreToolUse: "Before tool execution", |
| 165 | + PermissionRequest: "When permission dialog appears", |
| 166 | + PostToolUse: "After tool succeeds", |
| 167 | + PostToolUseFailure: "After tool fails", |
| 168 | + SubtaskStart: "When spawning a subtask", |
| 169 | + SubtaskStop: "When subtask finishes", |
| 170 | + Stop: "Claude finishes responding", |
| 171 | + PreCompact: "Before context compaction", |
| 172 | + SessionEnd: "Session terminates", |
| 173 | +} |
| 174 | + |
| 175 | +/** |
| 176 | + * Tool Matcher Descriptions |
| 177 | + * |
| 178 | + * Human-readable descriptions for each tool matcher category. |
| 179 | + */ |
| 180 | +export const toolMatcherDescriptions: Record<ToolMatcher, string> = { |
| 181 | + read: "file reading", |
| 182 | + edit: "file writing", |
| 183 | + browser: "web tools", |
| 184 | + command: "shell/bash", |
| 185 | + mcp: "protocol tools", |
| 186 | + modes: "mode tools", |
| 187 | +} |
| 188 | + |
| 189 | +/** |
| 190 | + * Session Matcher Descriptions |
| 191 | + * |
| 192 | + * Human-readable descriptions for each session matcher type. |
| 193 | + */ |
| 194 | +export const sessionMatcherDescriptions: Record<SessionMatcher, string> = { |
| 195 | + startup: "new session", |
| 196 | + resume: "existing session", |
| 197 | + clear: "conversation cleared", |
| 198 | + compact: "context compacted", |
| 199 | +} |
| 200 | + |
| 201 | +/** |
| 202 | + * Helper to determine if a hook event type uses tool matchers. |
| 203 | + * |
| 204 | + * @param eventType - The hook event type to check |
| 205 | + * @returns true if the event type supports tool matchers |
| 206 | + */ |
| 207 | +export function usesToolMatchers(eventType: HookEventType): boolean { |
| 208 | + return ["PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest"].includes(eventType) |
| 209 | +} |
| 210 | + |
| 211 | +/** |
| 212 | + * Helper to determine if a hook event type uses session matchers. |
| 213 | + * |
| 214 | + * @param eventType - The hook event type to check |
| 215 | + * @returns true if the event type supports session matchers |
| 216 | + */ |
| 217 | +export function usesSessionMatchers(eventType: HookEventType): boolean { |
| 218 | + return eventType === "SessionStart" |
| 219 | +} |
| 220 | + |
| 221 | +/** |
| 222 | + * Helper to determine if a hook event type uses no matchers. |
| 223 | + * |
| 224 | + * These hooks fire unconditionally when their event occurs. |
| 225 | + * |
| 226 | + * @param eventType - The hook event type to check |
| 227 | + * @returns true if the event type has no matchers |
| 228 | + */ |
| 229 | +export function usesNoMatchers(eventType: HookEventType): boolean { |
| 230 | + return !usesToolMatchers(eventType) && !usesSessionMatchers(eventType) |
| 231 | +} |
| 232 | + |
| 233 | +/** |
| 234 | + * Tool ID to Matcher Mapping |
| 235 | + * |
| 236 | + * Maps internal tool IDs to their corresponding matcher categories. |
| 237 | + * Used to determine which hooks should fire for a given tool. |
| 238 | + */ |
| 239 | +export const TOOL_TO_MATCHER: Record<string, ToolMatcher> = { |
| 240 | + // Read |
| 241 | + read_file: "read", |
| 242 | + list_files: "read", |
| 243 | + search_files: "read", |
| 244 | + codebase_search: "read", |
| 245 | + fetch_instructions: "read", |
| 246 | + |
| 247 | + // Edit |
| 248 | + write_to_file: "edit", |
| 249 | + apply_diff: "edit", |
| 250 | + apply_patch: "edit", |
| 251 | + edit_file: "edit", |
| 252 | + search_and_replace: "edit", |
| 253 | + search_replace: "edit", |
| 254 | + |
| 255 | + // Browser |
| 256 | + browser_action: "browser", |
| 257 | + |
| 258 | + // Command |
| 259 | + execute_command: "command", |
| 260 | + |
| 261 | + // MCP |
| 262 | + use_mcp_tool: "mcp", |
| 263 | + access_mcp_resource: "mcp", |
| 264 | + |
| 265 | + // Modes |
| 266 | + switch_mode: "modes", |
| 267 | + new_task: "modes", |
| 268 | +} |
0 commit comments