wave-agent-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +32 -0
  2. package/dist/agent.d.ts +96 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +286 -0
  5. package/dist/hooks/executor.d.ts +56 -0
  6. package/dist/hooks/executor.d.ts.map +1 -0
  7. package/dist/hooks/executor.js +312 -0
  8. package/dist/hooks/index.d.ts +17 -0
  9. package/dist/hooks/index.d.ts.map +1 -0
  10. package/dist/hooks/index.js +14 -0
  11. package/dist/hooks/manager.d.ts +90 -0
  12. package/dist/hooks/manager.d.ts.map +1 -0
  13. package/dist/hooks/manager.js +395 -0
  14. package/dist/hooks/matcher.d.ts +49 -0
  15. package/dist/hooks/matcher.d.ts.map +1 -0
  16. package/dist/hooks/matcher.js +147 -0
  17. package/dist/hooks/settings.d.ts +46 -0
  18. package/dist/hooks/settings.d.ts.map +1 -0
  19. package/dist/hooks/settings.js +100 -0
  20. package/dist/hooks/types.d.ts +80 -0
  21. package/dist/hooks/types.d.ts.map +1 -0
  22. package/dist/hooks/types.js +59 -0
  23. package/dist/index.d.ts +16 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +20 -0
  26. package/dist/managers/aiManager.d.ts +61 -0
  27. package/dist/managers/aiManager.d.ts.map +1 -0
  28. package/dist/managers/aiManager.js +415 -0
  29. package/dist/managers/backgroundBashManager.d.ts +27 -0
  30. package/dist/managers/backgroundBashManager.d.ts.map +1 -0
  31. package/dist/managers/backgroundBashManager.js +166 -0
  32. package/dist/managers/bashManager.d.ts +20 -0
  33. package/dist/managers/bashManager.d.ts.map +1 -0
  34. package/dist/managers/bashManager.js +66 -0
  35. package/dist/managers/mcpManager.d.ts +63 -0
  36. package/dist/managers/mcpManager.d.ts.map +1 -0
  37. package/dist/managers/mcpManager.js +378 -0
  38. package/dist/managers/messageManager.d.ts +85 -0
  39. package/dist/managers/messageManager.d.ts.map +1 -0
  40. package/dist/managers/messageManager.js +265 -0
  41. package/dist/managers/skillManager.d.ts +59 -0
  42. package/dist/managers/skillManager.d.ts.map +1 -0
  43. package/dist/managers/skillManager.js +317 -0
  44. package/dist/managers/slashCommandManager.d.ts +77 -0
  45. package/dist/managers/slashCommandManager.d.ts.map +1 -0
  46. package/dist/managers/slashCommandManager.js +208 -0
  47. package/dist/managers/toolManager.d.ts +23 -0
  48. package/dist/managers/toolManager.d.ts.map +1 -0
  49. package/dist/managers/toolManager.js +79 -0
  50. package/dist/services/aiService.d.ts +28 -0
  51. package/dist/services/aiService.d.ts.map +1 -0
  52. package/dist/services/aiService.js +180 -0
  53. package/dist/services/memory.d.ts +8 -0
  54. package/dist/services/memory.d.ts.map +1 -0
  55. package/dist/services/memory.js +128 -0
  56. package/dist/services/session.d.ts +54 -0
  57. package/dist/services/session.d.ts.map +1 -0
  58. package/dist/services/session.js +196 -0
  59. package/dist/tools/bashTool.d.ts +14 -0
  60. package/dist/tools/bashTool.d.ts.map +1 -0
  61. package/dist/tools/bashTool.js +351 -0
  62. package/dist/tools/deleteFileTool.d.ts +6 -0
  63. package/dist/tools/deleteFileTool.d.ts.map +1 -0
  64. package/dist/tools/deleteFileTool.js +67 -0
  65. package/dist/tools/editTool.d.ts +6 -0
  66. package/dist/tools/editTool.d.ts.map +1 -0
  67. package/dist/tools/editTool.js +168 -0
  68. package/dist/tools/globTool.d.ts +6 -0
  69. package/dist/tools/globTool.d.ts.map +1 -0
  70. package/dist/tools/globTool.js +113 -0
  71. package/dist/tools/grepTool.d.ts +6 -0
  72. package/dist/tools/grepTool.d.ts.map +1 -0
  73. package/dist/tools/grepTool.js +268 -0
  74. package/dist/tools/lsTool.d.ts +6 -0
  75. package/dist/tools/lsTool.d.ts.map +1 -0
  76. package/dist/tools/lsTool.js +160 -0
  77. package/dist/tools/multiEditTool.d.ts +6 -0
  78. package/dist/tools/multiEditTool.d.ts.map +1 -0
  79. package/dist/tools/multiEditTool.js +222 -0
  80. package/dist/tools/readTool.d.ts +6 -0
  81. package/dist/tools/readTool.d.ts.map +1 -0
  82. package/dist/tools/readTool.js +136 -0
  83. package/dist/tools/types.d.ts +35 -0
  84. package/dist/tools/types.d.ts.map +1 -0
  85. package/dist/tools/types.js +4 -0
  86. package/dist/tools/writeTool.d.ts +6 -0
  87. package/dist/tools/writeTool.d.ts.map +1 -0
  88. package/dist/tools/writeTool.js +138 -0
  89. package/dist/types.d.ts +212 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +13 -0
  92. package/dist/utils/bashHistory.d.ts +46 -0
  93. package/dist/utils/bashHistory.d.ts.map +1 -0
  94. package/dist/utils/bashHistory.js +236 -0
  95. package/dist/utils/commandArgumentParser.d.ts +34 -0
  96. package/dist/utils/commandArgumentParser.d.ts.map +1 -0
  97. package/dist/utils/commandArgumentParser.js +123 -0
  98. package/dist/utils/constants.d.ts +27 -0
  99. package/dist/utils/constants.d.ts.map +1 -0
  100. package/dist/utils/constants.js +28 -0
  101. package/dist/utils/convertMessagesForAPI.d.ts +9 -0
  102. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -0
  103. package/dist/utils/convertMessagesForAPI.js +189 -0
  104. package/dist/utils/customCommands.d.ts +14 -0
  105. package/dist/utils/customCommands.d.ts.map +1 -0
  106. package/dist/utils/customCommands.js +71 -0
  107. package/dist/utils/fileFilter.d.ts +26 -0
  108. package/dist/utils/fileFilter.d.ts.map +1 -0
  109. package/dist/utils/fileFilter.js +177 -0
  110. package/dist/utils/markdownParser.d.ts +27 -0
  111. package/dist/utils/markdownParser.d.ts.map +1 -0
  112. package/dist/utils/markdownParser.js +109 -0
  113. package/dist/utils/mcpUtils.d.ts +24 -0
  114. package/dist/utils/mcpUtils.d.ts.map +1 -0
  115. package/dist/utils/mcpUtils.js +51 -0
  116. package/dist/utils/messageOperations.d.ts +118 -0
  117. package/dist/utils/messageOperations.d.ts.map +1 -0
  118. package/dist/utils/messageOperations.js +334 -0
  119. package/dist/utils/path.d.ts +25 -0
  120. package/dist/utils/path.d.ts.map +1 -0
  121. package/dist/utils/path.js +109 -0
  122. package/dist/utils/skillParser.d.ts +18 -0
  123. package/dist/utils/skillParser.d.ts.map +1 -0
  124. package/dist/utils/skillParser.js +147 -0
  125. package/dist/utils/stringUtils.d.ts +13 -0
  126. package/dist/utils/stringUtils.d.ts.map +1 -0
  127. package/dist/utils/stringUtils.js +44 -0
  128. package/package.json +51 -0
  129. package/src/agent.ts +405 -0
  130. package/src/hooks/executor.ts +440 -0
  131. package/src/hooks/index.ts +52 -0
  132. package/src/hooks/manager.ts +618 -0
  133. package/src/hooks/matcher.ts +187 -0
  134. package/src/hooks/settings.ts +129 -0
  135. package/src/hooks/types.ts +169 -0
  136. package/src/index.ts +24 -0
  137. package/src/managers/aiManager.ts +573 -0
  138. package/src/managers/backgroundBashManager.ts +203 -0
  139. package/src/managers/bashManager.ts +97 -0
  140. package/src/managers/mcpManager.ts +493 -0
  141. package/src/managers/messageManager.ts +415 -0
  142. package/src/managers/skillManager.ts +404 -0
  143. package/src/managers/slashCommandManager.ts +293 -0
  144. package/src/managers/toolManager.ts +106 -0
  145. package/src/services/aiService.ts +252 -0
  146. package/src/services/memory.ts +149 -0
  147. package/src/services/session.ts +265 -0
  148. package/src/tools/bashTool.ts +402 -0
  149. package/src/tools/deleteFileTool.ts +81 -0
  150. package/src/tools/editTool.ts +192 -0
  151. package/src/tools/globTool.ts +135 -0
  152. package/src/tools/grepTool.ts +326 -0
  153. package/src/tools/lsTool.ts +187 -0
  154. package/src/tools/multiEditTool.ts +268 -0
  155. package/src/tools/readTool.ts +165 -0
  156. package/src/tools/types.ts +47 -0
  157. package/src/tools/writeTool.ts +163 -0
  158. package/src/types.ts +260 -0
  159. package/src/utils/bashHistory.ts +303 -0
  160. package/src/utils/commandArgumentParser.ts +153 -0
  161. package/src/utils/constants.ts +37 -0
  162. package/src/utils/convertMessagesForAPI.ts +236 -0
  163. package/src/utils/customCommands.ts +85 -0
  164. package/src/utils/fileFilter.ts +202 -0
  165. package/src/utils/markdownParser.ts +156 -0
  166. package/src/utils/mcpUtils.ts +81 -0
  167. package/src/utils/messageOperations.ts +506 -0
  168. package/src/utils/path.ts +118 -0
  169. package/src/utils/skillParser.ts +188 -0
  170. package/src/utils/stringUtils.ts +50 -0
@@ -0,0 +1,618 @@
1
+ /**
2
+ * Hook Manager
3
+ *
4
+ * Central orchestrator for the hooks system. Handles configuration loading,
5
+ * validation, and hook execution across all supported events.
6
+ */
7
+
8
+ import {
9
+ type HookEvent,
10
+ type HookEventConfig,
11
+ type HookConfiguration,
12
+ type PartialHookConfiguration,
13
+ type HookExecutionContext,
14
+ type ExtendedHookExecutionContext,
15
+ type HookExecutionResult,
16
+ type ValidationResult,
17
+ HookConfigurationError,
18
+ isValidHookEvent,
19
+ isValidHookEventConfig,
20
+ } from "./types.js";
21
+ import { type IHookMatcher, HookMatcher } from "./matcher.js";
22
+ import { type IHookExecutor, HookExecutor } from "./executor.js";
23
+ import { loadMergedHooksConfig } from "./settings.js";
24
+ import type { Logger } from "../types.js";
25
+
26
+ export interface IHookManager {
27
+ // Load configuration from settings
28
+ loadConfiguration(
29
+ userHooks?: PartialHookConfiguration,
30
+ projectHooks?: PartialHookConfiguration,
31
+ ): void;
32
+
33
+ // Load configuration from filesystem settings
34
+ loadConfigurationFromSettings(): void;
35
+
36
+ // Execute hooks for specific event
37
+ executeHooks(
38
+ event: HookEvent,
39
+ context: HookExecutionContext | ExtendedHookExecutionContext,
40
+ ): Promise<HookExecutionResult[]>;
41
+
42
+ // Check if hooks are configured for event
43
+ hasHooks(event: HookEvent, toolName?: string): boolean;
44
+
45
+ // Validate hook configuration
46
+ validateConfiguration(config: HookConfiguration): ValidationResult;
47
+
48
+ // Get current configuration
49
+ getConfiguration(): PartialHookConfiguration | undefined;
50
+ }
51
+
52
+ export class HookManager implements IHookManager {
53
+ private configuration: PartialHookConfiguration | undefined;
54
+ private readonly matcher: IHookMatcher;
55
+ private readonly executor: IHookExecutor;
56
+ private readonly logger?: Logger;
57
+ private readonly workdir: string;
58
+
59
+ constructor(
60
+ workdir: string,
61
+ matcher: IHookMatcher = new HookMatcher(),
62
+ executor?: IHookExecutor,
63
+ logger?: Logger,
64
+ ) {
65
+ this.workdir = workdir;
66
+ this.matcher = matcher;
67
+ // Create executor with logger if provided, or use passed executor, or create default
68
+ this.executor = logger
69
+ ? new HookExecutor(logger)
70
+ : executor || new HookExecutor();
71
+ this.logger = logger;
72
+ }
73
+
74
+ /**
75
+ * Load and merge hook configurations from user and project settings
76
+ * Project settings take precedence over user settings
77
+ */
78
+ loadConfiguration(
79
+ userHooks?: PartialHookConfiguration,
80
+ projectHooks?: PartialHookConfiguration,
81
+ ): void {
82
+ const merged: PartialHookConfiguration = {};
83
+
84
+ // Start with user hooks
85
+ if (userHooks) {
86
+ this.mergeHooksConfiguration(merged, userHooks);
87
+ }
88
+
89
+ // Override with project hooks (project settings take precedence)
90
+ if (projectHooks) {
91
+ this.mergeHooksConfiguration(merged, projectHooks);
92
+ }
93
+
94
+ // Validate merged configuration
95
+ const validation = this.validatePartialConfiguration(merged);
96
+ if (!validation.valid) {
97
+ throw new HookConfigurationError(
98
+ "merged configuration",
99
+ validation.errors,
100
+ );
101
+ }
102
+
103
+ this.configuration = merged;
104
+ }
105
+
106
+ /**
107
+ * Load configuration from filesystem settings
108
+ * Automatically loads and merges user and project hooks configuration
109
+ */
110
+ loadConfigurationFromSettings(): void {
111
+ try {
112
+ this.logger?.debug(`[HookManager] Loading configuration...`);
113
+ const mergedConfig = loadMergedHooksConfig(this.workdir);
114
+ this.logger?.debug(`[HookManager] Merged config result:`, mergedConfig);
115
+ this.configuration = mergedConfig;
116
+
117
+ // Validate the loaded configuration
118
+ const validation = this.validatePartialConfiguration(mergedConfig);
119
+ if (!validation.valid) {
120
+ throw new HookConfigurationError(
121
+ "filesystem settings",
122
+ validation.errors,
123
+ );
124
+ }
125
+
126
+ this.logger?.debug(
127
+ `[HookManager] Configuration loaded successfully with ${Object.keys(mergedConfig).length} event types`,
128
+ );
129
+ } catch (error) {
130
+ // If loading fails, start with undefined configuration (no hooks)
131
+ this.configuration = undefined;
132
+
133
+ // Re-throw configuration errors, but handle file system errors gracefully
134
+ if (error instanceof HookConfigurationError) {
135
+ throw error;
136
+ } else {
137
+ this.logger?.warn(
138
+ "Failed to load hooks configuration from settings:",
139
+ error,
140
+ );
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Execute hooks for a specific event
147
+ */
148
+ async executeHooks(
149
+ event: HookEvent,
150
+ context: HookExecutionContext | ExtendedHookExecutionContext,
151
+ ): Promise<HookExecutionResult[]> {
152
+ // Validate execution context
153
+ const contextValidation = this.validateExecutionContext(event, context);
154
+ if (!contextValidation.valid) {
155
+ this.logger?.error(
156
+ `[HookManager] Invalid execution context for ${event}: ${contextValidation.errors.join(", ")}`,
157
+ );
158
+ return [
159
+ {
160
+ success: false,
161
+ stderr: `Invalid execution context: ${contextValidation.errors.join(", ")}`,
162
+ duration: 0,
163
+ timedOut: false,
164
+ },
165
+ ];
166
+ }
167
+
168
+ if (!this.configuration) {
169
+ this.logger?.debug(
170
+ `[HookManager] No configuration loaded, skipping ${event} hooks`,
171
+ );
172
+ return [];
173
+ }
174
+
175
+ const eventConfigs = this.configuration[event];
176
+ if (!eventConfigs || eventConfigs.length === 0) {
177
+ this.logger?.debug(`[HookManager] No hooks configured for ${event} event`);
178
+ return [];
179
+ }
180
+
181
+ this.logger?.debug(
182
+ `[HookManager] Starting ${event} hook execution with ${eventConfigs.length} configurations`,
183
+ );
184
+
185
+ const results: HookExecutionResult[] = [];
186
+ const startTime = Date.now();
187
+
188
+ for (
189
+ let configIndex = 0;
190
+ configIndex < eventConfigs.length;
191
+ configIndex++
192
+ ) {
193
+ const config = eventConfigs[configIndex];
194
+
195
+ // Check if this config applies to the current context
196
+ if (!this.configApplies(config, event, context.toolName)) {
197
+ this.logger?.debug(
198
+ `[HookManager] Skipping configuration ${configIndex + 1}: matcher '${config.matcher}' does not match tool '${context.toolName}'`,
199
+ );
200
+ continue;
201
+ }
202
+
203
+ this.logger?.debug(
204
+ `[HookManager] Executing configuration ${configIndex + 1} with ${config.hooks.length} commands (matcher: ${config.matcher || "any"})`,
205
+ );
206
+
207
+ // Execute all commands for this configuration
208
+ for (
209
+ let commandIndex = 0;
210
+ commandIndex < config.hooks.length;
211
+ commandIndex++
212
+ ) {
213
+ const hookCommand = config.hooks[commandIndex];
214
+
215
+ try {
216
+ this.logger?.debug(
217
+ `[HookManager] Executing command ${commandIndex + 1}/${config.hooks.length} in configuration ${configIndex + 1}`,
218
+ );
219
+
220
+ const result = await this.executor.executeCommand(
221
+ hookCommand.command,
222
+ context,
223
+ );
224
+ results.push(result);
225
+
226
+ // Report individual command result
227
+ if (result.success) {
228
+ this.logger?.debug(
229
+ `[HookManager] Command ${commandIndex + 1} completed successfully in ${result.duration}ms`,
230
+ );
231
+ } else {
232
+ this.logger?.warn(
233
+ `[HookManager] Command ${commandIndex + 1} failed in ${result.duration}ms (exit code: ${result.exitCode}, timed out: ${result.timedOut})`,
234
+ );
235
+ }
236
+
237
+ // Continue with next command even if this one fails
238
+ // This allows for non-critical hooks to fail without stopping the workflow
239
+ } catch (error) {
240
+ // This should be rare as executor handles most errors
241
+ const errorMessage =
242
+ error instanceof Error ? error.message : "Unknown execution error";
243
+ this.logger?.error(
244
+ `[HookManager] Unexpected error in command ${commandIndex + 1}: ${errorMessage}`,
245
+ );
246
+
247
+ results.push({
248
+ success: false,
249
+ stderr: errorMessage,
250
+ duration: 0,
251
+ timedOut: false,
252
+ });
253
+ }
254
+ }
255
+ }
256
+
257
+ // Generate execution summary
258
+ const totalDuration = Date.now() - startTime;
259
+ const summary = this.generateExecutionSummary(
260
+ event,
261
+ results,
262
+ totalDuration,
263
+ );
264
+ this.logger?.info(`[HookManager] ${event} execution summary: ${summary}`);
265
+
266
+ return results;
267
+ }
268
+
269
+ /**
270
+ * Check if hooks are configured for an event/tool combination
271
+ */
272
+ hasHooks(event: HookEvent, toolName?: string): boolean {
273
+ if (!this.configuration) return false;
274
+
275
+ const eventConfigs = this.configuration[event];
276
+ if (!eventConfigs || eventConfigs.length === 0) return false;
277
+
278
+ return eventConfigs.some((config) =>
279
+ this.configApplies(config, event, toolName),
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Validate hook configuration structure and content
285
+ */
286
+ validateConfiguration(config: HookConfiguration): ValidationResult {
287
+ const errors: string[] = [];
288
+
289
+ if (!config || typeof config !== "object") {
290
+ return { valid: false, errors: ["Configuration must be an object"] };
291
+ }
292
+
293
+ if (!config.hooks || typeof config.hooks !== "object") {
294
+ return {
295
+ valid: false,
296
+ errors: ["Configuration must have a hooks property"],
297
+ };
298
+ }
299
+
300
+ // Validate each hook event
301
+ for (const [eventName, eventConfigs] of Object.entries(config.hooks)) {
302
+ // Validate event name
303
+ if (!isValidHookEvent(eventName)) {
304
+ errors.push(`Invalid hook event: ${eventName}`);
305
+ continue;
306
+ }
307
+
308
+ // Validate event configurations
309
+ if (!Array.isArray(eventConfigs)) {
310
+ errors.push(
311
+ `Hook event ${eventName} must be an array of configurations`,
312
+ );
313
+ continue;
314
+ }
315
+
316
+ eventConfigs.forEach((eventConfig, index) => {
317
+ const configErrors = this.validateEventConfig(
318
+ eventName as HookEvent,
319
+ eventConfig,
320
+ index,
321
+ );
322
+ errors.push(...configErrors);
323
+ });
324
+ }
325
+
326
+ return {
327
+ valid: errors.length === 0,
328
+ errors,
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Validate partial hook configuration structure and content
334
+ */
335
+ private validatePartialConfiguration(
336
+ config: PartialHookConfiguration,
337
+ ): ValidationResult {
338
+ const errors: string[] = [];
339
+
340
+ if (!config || typeof config !== "object") {
341
+ return { valid: false, errors: ["Configuration must be an object"] };
342
+ }
343
+
344
+ // Validate each hook event that is present
345
+ for (const [eventName, eventConfigs] of Object.entries(config)) {
346
+ // Validate event name
347
+ if (!isValidHookEvent(eventName)) {
348
+ errors.push(`Invalid hook event: ${eventName}`);
349
+ continue;
350
+ }
351
+
352
+ // Validate event configurations
353
+ if (!Array.isArray(eventConfigs)) {
354
+ errors.push(
355
+ `Hook event ${eventName} must be an array of configurations`,
356
+ );
357
+ continue;
358
+ }
359
+
360
+ eventConfigs.forEach((eventConfig, index) => {
361
+ const configErrors = this.validateEventConfig(
362
+ eventName as HookEvent,
363
+ eventConfig,
364
+ index,
365
+ );
366
+ errors.push(...configErrors);
367
+ });
368
+ }
369
+
370
+ return {
371
+ valid: errors.length === 0,
372
+ errors,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Get current configuration
378
+ */
379
+ getConfiguration(): PartialHookConfiguration | undefined {
380
+ if (!this.configuration) return undefined;
381
+
382
+ // Deep clone to prevent external modification
383
+ return JSON.parse(JSON.stringify(this.configuration));
384
+ }
385
+
386
+ /**
387
+ * Clear current configuration
388
+ */
389
+ clearConfiguration(): void {
390
+ this.configuration = undefined;
391
+ }
392
+
393
+ /**
394
+ * Validate execution context for a specific event
395
+ */
396
+ private validateExecutionContext(
397
+ event: HookEvent,
398
+ context: HookExecutionContext,
399
+ ): ValidationResult {
400
+ const errors: string[] = [];
401
+
402
+ // Validate basic context structure
403
+ if (!context || typeof context !== "object") {
404
+ return { valid: false, errors: ["Context must be an object"] };
405
+ }
406
+
407
+ // Warn about event mismatch but don't fail validation
408
+ if (context.event !== event) {
409
+ this.logger?.warn(
410
+ `[HookManager] Context event '${context.event}' does not match requested event '${event}'`,
411
+ );
412
+ }
413
+
414
+ // Validate project directory
415
+ if (!context.projectDir || typeof context.projectDir !== "string") {
416
+ errors.push("Context must have a valid projectDir string");
417
+ }
418
+
419
+ // Validate timestamp
420
+ if (!context.timestamp || !(context.timestamp instanceof Date)) {
421
+ errors.push("Context must have a valid timestamp Date object");
422
+ }
423
+
424
+ // Validate tool-specific requirements
425
+ if (event === "PreToolUse" || event === "PostToolUse") {
426
+ if (!context.toolName || typeof context.toolName !== "string") {
427
+ errors.push(`${event} event requires a valid toolName in context`);
428
+ }
429
+ }
430
+
431
+ // Validate non-tool events don't have unexpected tool names
432
+ if (
433
+ (event === "UserPromptSubmit" || event === "Stop") &&
434
+ context.toolName !== undefined
435
+ ) {
436
+ this.logger?.warn(
437
+ `[HookManager] ${event} event has unexpected toolName in context: ${context.toolName}`,
438
+ );
439
+ }
440
+
441
+ return {
442
+ valid: errors.length === 0,
443
+ errors,
444
+ };
445
+ }
446
+
447
+ /**
448
+ * Generate a summary of hook execution results
449
+ */
450
+ private generateExecutionSummary(
451
+ event: HookEvent,
452
+ results: HookExecutionResult[],
453
+ totalDuration: number,
454
+ ): string {
455
+ if (results.length === 0) {
456
+ return `No hooks executed for ${event}`;
457
+ }
458
+
459
+ const successful = results.filter((r) => r.success).length;
460
+ const failed = results.length - successful;
461
+ const timedOut = results.filter((r) => r.timedOut).length;
462
+ const avgDuration =
463
+ results.reduce((sum, r) => sum + r.duration, 0) / results.length;
464
+
465
+ let summary = `${successful}/${results.length} commands successful`;
466
+
467
+ if (failed > 0) {
468
+ summary += `, ${failed} failed`;
469
+ }
470
+
471
+ if (timedOut > 0) {
472
+ summary += `, ${timedOut} timed out`;
473
+ }
474
+
475
+ summary += ` (avg: ${Math.round(avgDuration)}ms, total: ${totalDuration}ms)`;
476
+
477
+ return summary;
478
+ }
479
+
480
+ /**
481
+ * Merge hook configurations, with the second taking precedence
482
+ */
483
+ private mergeHooksConfiguration(
484
+ target: PartialHookConfiguration,
485
+ source: PartialHookConfiguration,
486
+ ): void {
487
+ for (const [event, configs] of Object.entries(source)) {
488
+ if (isValidHookEvent(event)) {
489
+ // For now, completely replace event configs rather than merging
490
+ // This ensures project settings completely override user settings for each event
491
+ target[event] = [...configs];
492
+ }
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Check if a hook configuration applies to the current context
498
+ */
499
+ private configApplies(
500
+ config: HookEventConfig,
501
+ event: HookEvent,
502
+ toolName?: string,
503
+ ): boolean {
504
+ // For events that don't use matchers, config always applies
505
+ if (event === "UserPromptSubmit" || event === "Stop") {
506
+ return true;
507
+ }
508
+
509
+ // For tool-based events, check matcher if present
510
+ if (event === "PreToolUse" || event === "PostToolUse") {
511
+ if (!config.matcher) {
512
+ // No matcher means applies to all tools
513
+ return true;
514
+ }
515
+
516
+ if (!toolName) {
517
+ // No tool name provided, cannot match
518
+ return false;
519
+ }
520
+
521
+ return this.matcher.matches(config.matcher, toolName);
522
+ }
523
+
524
+ return false;
525
+ }
526
+
527
+ /**
528
+ * Validate a single event configuration
529
+ */
530
+ private validateEventConfig(
531
+ event: HookEvent,
532
+ config: unknown,
533
+ index: number,
534
+ ): string[] {
535
+ const errors: string[] = [];
536
+ const prefix = `Hook event ${event}[${index}]`;
537
+
538
+ if (!isValidHookEventConfig(config)) {
539
+ errors.push(`${prefix}: Invalid hook event configuration structure`);
540
+ return errors;
541
+ }
542
+
543
+ // Validate matcher requirements
544
+ if ((event === "PreToolUse" || event === "PostToolUse") && config.matcher) {
545
+ if (!this.matcher.isValidPattern(config.matcher)) {
546
+ errors.push(`${prefix}: Invalid matcher pattern: ${config.matcher}`);
547
+ }
548
+ }
549
+
550
+ // Validate that non-tool events don't have matchers
551
+ if ((event === "UserPromptSubmit" || event === "Stop") && config.matcher) {
552
+ errors.push(`${prefix}: Event ${event} should not have a matcher`);
553
+ }
554
+
555
+ // Validate commands
556
+ config.hooks.forEach((hookCommand, cmdIndex) => {
557
+ if (!this.executor.isCommandSafe(hookCommand.command)) {
558
+ errors.push(
559
+ `${prefix}.hooks[${cmdIndex}]: Command may be unsafe: ${hookCommand.command}`,
560
+ );
561
+ }
562
+ });
563
+
564
+ return errors;
565
+ }
566
+
567
+ /**
568
+ * Get statistics about current configuration
569
+ */
570
+ getConfigurationStats(): {
571
+ totalEvents: number;
572
+ totalConfigs: number;
573
+ totalCommands: number;
574
+ eventBreakdown: Record<HookEvent, number>;
575
+ } {
576
+ if (!this.configuration) {
577
+ return {
578
+ totalEvents: 0,
579
+ totalConfigs: 0,
580
+ totalCommands: 0,
581
+ eventBreakdown: {
582
+ PreToolUse: 0,
583
+ PostToolUse: 0,
584
+ UserPromptSubmit: 0,
585
+ Stop: 0,
586
+ },
587
+ };
588
+ }
589
+
590
+ const eventBreakdown: Record<HookEvent, number> = {
591
+ PreToolUse: 0,
592
+ PostToolUse: 0,
593
+ UserPromptSubmit: 0,
594
+ Stop: 0,
595
+ };
596
+
597
+ let totalConfigs = 0;
598
+ let totalCommands = 0;
599
+
600
+ Object.entries(this.configuration).forEach(([event, configs]) => {
601
+ if (isValidHookEvent(event)) {
602
+ eventBreakdown[event] = configs.length;
603
+ totalConfigs += configs.length;
604
+ totalCommands += configs.reduce(
605
+ (sum, config) => sum + config.hooks.length,
606
+ 0,
607
+ );
608
+ }
609
+ });
610
+
611
+ return {
612
+ totalEvents: Object.keys(this.configuration).length,
613
+ totalConfigs,
614
+ totalCommands,
615
+ eventBreakdown,
616
+ };
617
+ }
618
+ }