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.
- package/README.md +32 -0
- package/dist/agent.d.ts +96 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +286 -0
- package/dist/hooks/executor.d.ts +56 -0
- package/dist/hooks/executor.d.ts.map +1 -0
- package/dist/hooks/executor.js +312 -0
- package/dist/hooks/index.d.ts +17 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +14 -0
- package/dist/hooks/manager.d.ts +90 -0
- package/dist/hooks/manager.d.ts.map +1 -0
- package/dist/hooks/manager.js +395 -0
- package/dist/hooks/matcher.d.ts +49 -0
- package/dist/hooks/matcher.d.ts.map +1 -0
- package/dist/hooks/matcher.js +147 -0
- package/dist/hooks/settings.d.ts +46 -0
- package/dist/hooks/settings.d.ts.map +1 -0
- package/dist/hooks/settings.js +100 -0
- package/dist/hooks/types.d.ts +80 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +59 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/managers/aiManager.d.ts +61 -0
- package/dist/managers/aiManager.d.ts.map +1 -0
- package/dist/managers/aiManager.js +415 -0
- package/dist/managers/backgroundBashManager.d.ts +27 -0
- package/dist/managers/backgroundBashManager.d.ts.map +1 -0
- package/dist/managers/backgroundBashManager.js +166 -0
- package/dist/managers/bashManager.d.ts +20 -0
- package/dist/managers/bashManager.d.ts.map +1 -0
- package/dist/managers/bashManager.js +66 -0
- package/dist/managers/mcpManager.d.ts +63 -0
- package/dist/managers/mcpManager.d.ts.map +1 -0
- package/dist/managers/mcpManager.js +378 -0
- package/dist/managers/messageManager.d.ts +85 -0
- package/dist/managers/messageManager.d.ts.map +1 -0
- package/dist/managers/messageManager.js +265 -0
- package/dist/managers/skillManager.d.ts +59 -0
- package/dist/managers/skillManager.d.ts.map +1 -0
- package/dist/managers/skillManager.js +317 -0
- package/dist/managers/slashCommandManager.d.ts +77 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -0
- package/dist/managers/slashCommandManager.js +208 -0
- package/dist/managers/toolManager.d.ts +23 -0
- package/dist/managers/toolManager.d.ts.map +1 -0
- package/dist/managers/toolManager.js +79 -0
- package/dist/services/aiService.d.ts +28 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +180 -0
- package/dist/services/memory.d.ts +8 -0
- package/dist/services/memory.d.ts.map +1 -0
- package/dist/services/memory.js +128 -0
- package/dist/services/session.d.ts +54 -0
- package/dist/services/session.d.ts.map +1 -0
- package/dist/services/session.js +196 -0
- package/dist/tools/bashTool.d.ts +14 -0
- package/dist/tools/bashTool.d.ts.map +1 -0
- package/dist/tools/bashTool.js +351 -0
- package/dist/tools/deleteFileTool.d.ts +6 -0
- package/dist/tools/deleteFileTool.d.ts.map +1 -0
- package/dist/tools/deleteFileTool.js +67 -0
- package/dist/tools/editTool.d.ts +6 -0
- package/dist/tools/editTool.d.ts.map +1 -0
- package/dist/tools/editTool.js +168 -0
- package/dist/tools/globTool.d.ts +6 -0
- package/dist/tools/globTool.d.ts.map +1 -0
- package/dist/tools/globTool.js +113 -0
- package/dist/tools/grepTool.d.ts +6 -0
- package/dist/tools/grepTool.d.ts.map +1 -0
- package/dist/tools/grepTool.js +268 -0
- package/dist/tools/lsTool.d.ts +6 -0
- package/dist/tools/lsTool.d.ts.map +1 -0
- package/dist/tools/lsTool.js +160 -0
- package/dist/tools/multiEditTool.d.ts +6 -0
- package/dist/tools/multiEditTool.d.ts.map +1 -0
- package/dist/tools/multiEditTool.js +222 -0
- package/dist/tools/readTool.d.ts +6 -0
- package/dist/tools/readTool.d.ts.map +1 -0
- package/dist/tools/readTool.js +136 -0
- package/dist/tools/types.d.ts +35 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/writeTool.d.ts +6 -0
- package/dist/tools/writeTool.d.ts.map +1 -0
- package/dist/tools/writeTool.js +138 -0
- package/dist/types.d.ts +212 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/utils/bashHistory.d.ts +46 -0
- package/dist/utils/bashHistory.d.ts.map +1 -0
- package/dist/utils/bashHistory.js +236 -0
- package/dist/utils/commandArgumentParser.d.ts +34 -0
- package/dist/utils/commandArgumentParser.d.ts.map +1 -0
- package/dist/utils/commandArgumentParser.js +123 -0
- package/dist/utils/constants.d.ts +27 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +28 -0
- package/dist/utils/convertMessagesForAPI.d.ts +9 -0
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -0
- package/dist/utils/convertMessagesForAPI.js +189 -0
- package/dist/utils/customCommands.d.ts +14 -0
- package/dist/utils/customCommands.d.ts.map +1 -0
- package/dist/utils/customCommands.js +71 -0
- package/dist/utils/fileFilter.d.ts +26 -0
- package/dist/utils/fileFilter.d.ts.map +1 -0
- package/dist/utils/fileFilter.js +177 -0
- package/dist/utils/markdownParser.d.ts +27 -0
- package/dist/utils/markdownParser.d.ts.map +1 -0
- package/dist/utils/markdownParser.js +109 -0
- package/dist/utils/mcpUtils.d.ts +24 -0
- package/dist/utils/mcpUtils.d.ts.map +1 -0
- package/dist/utils/mcpUtils.js +51 -0
- package/dist/utils/messageOperations.d.ts +118 -0
- package/dist/utils/messageOperations.d.ts.map +1 -0
- package/dist/utils/messageOperations.js +334 -0
- package/dist/utils/path.d.ts +25 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +109 -0
- package/dist/utils/skillParser.d.ts +18 -0
- package/dist/utils/skillParser.d.ts.map +1 -0
- package/dist/utils/skillParser.js +147 -0
- package/dist/utils/stringUtils.d.ts +13 -0
- package/dist/utils/stringUtils.d.ts.map +1 -0
- package/dist/utils/stringUtils.js +44 -0
- package/package.json +51 -0
- package/src/agent.ts +405 -0
- package/src/hooks/executor.ts +440 -0
- package/src/hooks/index.ts +52 -0
- package/src/hooks/manager.ts +618 -0
- package/src/hooks/matcher.ts +187 -0
- package/src/hooks/settings.ts +129 -0
- package/src/hooks/types.ts +169 -0
- package/src/index.ts +24 -0
- package/src/managers/aiManager.ts +573 -0
- package/src/managers/backgroundBashManager.ts +203 -0
- package/src/managers/bashManager.ts +97 -0
- package/src/managers/mcpManager.ts +493 -0
- package/src/managers/messageManager.ts +415 -0
- package/src/managers/skillManager.ts +404 -0
- package/src/managers/slashCommandManager.ts +293 -0
- package/src/managers/toolManager.ts +106 -0
- package/src/services/aiService.ts +252 -0
- package/src/services/memory.ts +149 -0
- package/src/services/session.ts +265 -0
- package/src/tools/bashTool.ts +402 -0
- package/src/tools/deleteFileTool.ts +81 -0
- package/src/tools/editTool.ts +192 -0
- package/src/tools/globTool.ts +135 -0
- package/src/tools/grepTool.ts +326 -0
- package/src/tools/lsTool.ts +187 -0
- package/src/tools/multiEditTool.ts +268 -0
- package/src/tools/readTool.ts +165 -0
- package/src/tools/types.ts +47 -0
- package/src/tools/writeTool.ts +163 -0
- package/src/types.ts +260 -0
- package/src/utils/bashHistory.ts +303 -0
- package/src/utils/commandArgumentParser.ts +153 -0
- package/src/utils/constants.ts +37 -0
- package/src/utils/convertMessagesForAPI.ts +236 -0
- package/src/utils/customCommands.ts +85 -0
- package/src/utils/fileFilter.ts +202 -0
- package/src/utils/markdownParser.ts +156 -0
- package/src/utils/mcpUtils.ts +81 -0
- package/src/utils/messageOperations.ts +506 -0
- package/src/utils/path.ts +118 -0
- package/src/utils/skillParser.ts +188 -0
- 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
|
+
}
|