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,395 @@
|
|
|
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
|
+
import { HookConfigurationError, isValidHookEvent, isValidHookEventConfig, } from "./types.js";
|
|
8
|
+
import { HookMatcher } from "./matcher.js";
|
|
9
|
+
import { HookExecutor } from "./executor.js";
|
|
10
|
+
import { loadMergedHooksConfig } from "./settings.js";
|
|
11
|
+
export class HookManager {
|
|
12
|
+
constructor(workdir, matcher = new HookMatcher(), executor, logger) {
|
|
13
|
+
this.workdir = workdir;
|
|
14
|
+
this.matcher = matcher;
|
|
15
|
+
// Create executor with logger if provided, or use passed executor, or create default
|
|
16
|
+
this.executor = logger
|
|
17
|
+
? new HookExecutor(logger)
|
|
18
|
+
: executor || new HookExecutor();
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Load and merge hook configurations from user and project settings
|
|
23
|
+
* Project settings take precedence over user settings
|
|
24
|
+
*/
|
|
25
|
+
loadConfiguration(userHooks, projectHooks) {
|
|
26
|
+
const merged = {};
|
|
27
|
+
// Start with user hooks
|
|
28
|
+
if (userHooks) {
|
|
29
|
+
this.mergeHooksConfiguration(merged, userHooks);
|
|
30
|
+
}
|
|
31
|
+
// Override with project hooks (project settings take precedence)
|
|
32
|
+
if (projectHooks) {
|
|
33
|
+
this.mergeHooksConfiguration(merged, projectHooks);
|
|
34
|
+
}
|
|
35
|
+
// Validate merged configuration
|
|
36
|
+
const validation = this.validatePartialConfiguration(merged);
|
|
37
|
+
if (!validation.valid) {
|
|
38
|
+
throw new HookConfigurationError("merged configuration", validation.errors);
|
|
39
|
+
}
|
|
40
|
+
this.configuration = merged;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load configuration from filesystem settings
|
|
44
|
+
* Automatically loads and merges user and project hooks configuration
|
|
45
|
+
*/
|
|
46
|
+
loadConfigurationFromSettings() {
|
|
47
|
+
try {
|
|
48
|
+
this.logger?.debug(`[HookManager] Loading configuration...`);
|
|
49
|
+
const mergedConfig = loadMergedHooksConfig(this.workdir);
|
|
50
|
+
this.logger?.debug(`[HookManager] Merged config result:`, mergedConfig);
|
|
51
|
+
this.configuration = mergedConfig;
|
|
52
|
+
// Validate the loaded configuration
|
|
53
|
+
const validation = this.validatePartialConfiguration(mergedConfig);
|
|
54
|
+
if (!validation.valid) {
|
|
55
|
+
throw new HookConfigurationError("filesystem settings", validation.errors);
|
|
56
|
+
}
|
|
57
|
+
this.logger?.debug(`[HookManager] Configuration loaded successfully with ${Object.keys(mergedConfig).length} event types`);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// If loading fails, start with undefined configuration (no hooks)
|
|
61
|
+
this.configuration = undefined;
|
|
62
|
+
// Re-throw configuration errors, but handle file system errors gracefully
|
|
63
|
+
if (error instanceof HookConfigurationError) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.logger?.warn("Failed to load hooks configuration from settings:", error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Execute hooks for a specific event
|
|
73
|
+
*/
|
|
74
|
+
async executeHooks(event, context) {
|
|
75
|
+
// Validate execution context
|
|
76
|
+
const contextValidation = this.validateExecutionContext(event, context);
|
|
77
|
+
if (!contextValidation.valid) {
|
|
78
|
+
this.logger?.error(`[HookManager] Invalid execution context for ${event}: ${contextValidation.errors.join(", ")}`);
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
success: false,
|
|
82
|
+
stderr: `Invalid execution context: ${contextValidation.errors.join(", ")}`,
|
|
83
|
+
duration: 0,
|
|
84
|
+
timedOut: false,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
if (!this.configuration) {
|
|
89
|
+
this.logger?.debug(`[HookManager] No configuration loaded, skipping ${event} hooks`);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const eventConfigs = this.configuration[event];
|
|
93
|
+
if (!eventConfigs || eventConfigs.length === 0) {
|
|
94
|
+
this.logger?.debug(`[HookManager] No hooks configured for ${event} event`);
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
this.logger?.debug(`[HookManager] Starting ${event} hook execution with ${eventConfigs.length} configurations`);
|
|
98
|
+
const results = [];
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
for (let configIndex = 0; configIndex < eventConfigs.length; configIndex++) {
|
|
101
|
+
const config = eventConfigs[configIndex];
|
|
102
|
+
// Check if this config applies to the current context
|
|
103
|
+
if (!this.configApplies(config, event, context.toolName)) {
|
|
104
|
+
this.logger?.debug(`[HookManager] Skipping configuration ${configIndex + 1}: matcher '${config.matcher}' does not match tool '${context.toolName}'`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
this.logger?.debug(`[HookManager] Executing configuration ${configIndex + 1} with ${config.hooks.length} commands (matcher: ${config.matcher || "any"})`);
|
|
108
|
+
// Execute all commands for this configuration
|
|
109
|
+
for (let commandIndex = 0; commandIndex < config.hooks.length; commandIndex++) {
|
|
110
|
+
const hookCommand = config.hooks[commandIndex];
|
|
111
|
+
try {
|
|
112
|
+
this.logger?.debug(`[HookManager] Executing command ${commandIndex + 1}/${config.hooks.length} in configuration ${configIndex + 1}`);
|
|
113
|
+
const result = await this.executor.executeCommand(hookCommand.command, context);
|
|
114
|
+
results.push(result);
|
|
115
|
+
// Report individual command result
|
|
116
|
+
if (result.success) {
|
|
117
|
+
this.logger?.debug(`[HookManager] Command ${commandIndex + 1} completed successfully in ${result.duration}ms`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
this.logger?.warn(`[HookManager] Command ${commandIndex + 1} failed in ${result.duration}ms (exit code: ${result.exitCode}, timed out: ${result.timedOut})`);
|
|
121
|
+
}
|
|
122
|
+
// Continue with next command even if this one fails
|
|
123
|
+
// This allows for non-critical hooks to fail without stopping the workflow
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
// This should be rare as executor handles most errors
|
|
127
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown execution error";
|
|
128
|
+
this.logger?.error(`[HookManager] Unexpected error in command ${commandIndex + 1}: ${errorMessage}`);
|
|
129
|
+
results.push({
|
|
130
|
+
success: false,
|
|
131
|
+
stderr: errorMessage,
|
|
132
|
+
duration: 0,
|
|
133
|
+
timedOut: false,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Generate execution summary
|
|
139
|
+
const totalDuration = Date.now() - startTime;
|
|
140
|
+
const summary = this.generateExecutionSummary(event, results, totalDuration);
|
|
141
|
+
this.logger?.info(`[HookManager] ${event} execution summary: ${summary}`);
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check if hooks are configured for an event/tool combination
|
|
146
|
+
*/
|
|
147
|
+
hasHooks(event, toolName) {
|
|
148
|
+
if (!this.configuration)
|
|
149
|
+
return false;
|
|
150
|
+
const eventConfigs = this.configuration[event];
|
|
151
|
+
if (!eventConfigs || eventConfigs.length === 0)
|
|
152
|
+
return false;
|
|
153
|
+
return eventConfigs.some((config) => this.configApplies(config, event, toolName));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Validate hook configuration structure and content
|
|
157
|
+
*/
|
|
158
|
+
validateConfiguration(config) {
|
|
159
|
+
const errors = [];
|
|
160
|
+
if (!config || typeof config !== "object") {
|
|
161
|
+
return { valid: false, errors: ["Configuration must be an object"] };
|
|
162
|
+
}
|
|
163
|
+
if (!config.hooks || typeof config.hooks !== "object") {
|
|
164
|
+
return {
|
|
165
|
+
valid: false,
|
|
166
|
+
errors: ["Configuration must have a hooks property"],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Validate each hook event
|
|
170
|
+
for (const [eventName, eventConfigs] of Object.entries(config.hooks)) {
|
|
171
|
+
// Validate event name
|
|
172
|
+
if (!isValidHookEvent(eventName)) {
|
|
173
|
+
errors.push(`Invalid hook event: ${eventName}`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Validate event configurations
|
|
177
|
+
if (!Array.isArray(eventConfigs)) {
|
|
178
|
+
errors.push(`Hook event ${eventName} must be an array of configurations`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
eventConfigs.forEach((eventConfig, index) => {
|
|
182
|
+
const configErrors = this.validateEventConfig(eventName, eventConfig, index);
|
|
183
|
+
errors.push(...configErrors);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
valid: errors.length === 0,
|
|
188
|
+
errors,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Validate partial hook configuration structure and content
|
|
193
|
+
*/
|
|
194
|
+
validatePartialConfiguration(config) {
|
|
195
|
+
const errors = [];
|
|
196
|
+
if (!config || typeof config !== "object") {
|
|
197
|
+
return { valid: false, errors: ["Configuration must be an object"] };
|
|
198
|
+
}
|
|
199
|
+
// Validate each hook event that is present
|
|
200
|
+
for (const [eventName, eventConfigs] of Object.entries(config)) {
|
|
201
|
+
// Validate event name
|
|
202
|
+
if (!isValidHookEvent(eventName)) {
|
|
203
|
+
errors.push(`Invalid hook event: ${eventName}`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
// Validate event configurations
|
|
207
|
+
if (!Array.isArray(eventConfigs)) {
|
|
208
|
+
errors.push(`Hook event ${eventName} must be an array of configurations`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
eventConfigs.forEach((eventConfig, index) => {
|
|
212
|
+
const configErrors = this.validateEventConfig(eventName, eventConfig, index);
|
|
213
|
+
errors.push(...configErrors);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
valid: errors.length === 0,
|
|
218
|
+
errors,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get current configuration
|
|
223
|
+
*/
|
|
224
|
+
getConfiguration() {
|
|
225
|
+
if (!this.configuration)
|
|
226
|
+
return undefined;
|
|
227
|
+
// Deep clone to prevent external modification
|
|
228
|
+
return JSON.parse(JSON.stringify(this.configuration));
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Clear current configuration
|
|
232
|
+
*/
|
|
233
|
+
clearConfiguration() {
|
|
234
|
+
this.configuration = undefined;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Validate execution context for a specific event
|
|
238
|
+
*/
|
|
239
|
+
validateExecutionContext(event, context) {
|
|
240
|
+
const errors = [];
|
|
241
|
+
// Validate basic context structure
|
|
242
|
+
if (!context || typeof context !== "object") {
|
|
243
|
+
return { valid: false, errors: ["Context must be an object"] };
|
|
244
|
+
}
|
|
245
|
+
// Warn about event mismatch but don't fail validation
|
|
246
|
+
if (context.event !== event) {
|
|
247
|
+
this.logger?.warn(`[HookManager] Context event '${context.event}' does not match requested event '${event}'`);
|
|
248
|
+
}
|
|
249
|
+
// Validate project directory
|
|
250
|
+
if (!context.projectDir || typeof context.projectDir !== "string") {
|
|
251
|
+
errors.push("Context must have a valid projectDir string");
|
|
252
|
+
}
|
|
253
|
+
// Validate timestamp
|
|
254
|
+
if (!context.timestamp || !(context.timestamp instanceof Date)) {
|
|
255
|
+
errors.push("Context must have a valid timestamp Date object");
|
|
256
|
+
}
|
|
257
|
+
// Validate tool-specific requirements
|
|
258
|
+
if (event === "PreToolUse" || event === "PostToolUse") {
|
|
259
|
+
if (!context.toolName || typeof context.toolName !== "string") {
|
|
260
|
+
errors.push(`${event} event requires a valid toolName in context`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Validate non-tool events don't have unexpected tool names
|
|
264
|
+
if ((event === "UserPromptSubmit" || event === "Stop") &&
|
|
265
|
+
context.toolName !== undefined) {
|
|
266
|
+
this.logger?.warn(`[HookManager] ${event} event has unexpected toolName in context: ${context.toolName}`);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
valid: errors.length === 0,
|
|
270
|
+
errors,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Generate a summary of hook execution results
|
|
275
|
+
*/
|
|
276
|
+
generateExecutionSummary(event, results, totalDuration) {
|
|
277
|
+
if (results.length === 0) {
|
|
278
|
+
return `No hooks executed for ${event}`;
|
|
279
|
+
}
|
|
280
|
+
const successful = results.filter((r) => r.success).length;
|
|
281
|
+
const failed = results.length - successful;
|
|
282
|
+
const timedOut = results.filter((r) => r.timedOut).length;
|
|
283
|
+
const avgDuration = results.reduce((sum, r) => sum + r.duration, 0) / results.length;
|
|
284
|
+
let summary = `${successful}/${results.length} commands successful`;
|
|
285
|
+
if (failed > 0) {
|
|
286
|
+
summary += `, ${failed} failed`;
|
|
287
|
+
}
|
|
288
|
+
if (timedOut > 0) {
|
|
289
|
+
summary += `, ${timedOut} timed out`;
|
|
290
|
+
}
|
|
291
|
+
summary += ` (avg: ${Math.round(avgDuration)}ms, total: ${totalDuration}ms)`;
|
|
292
|
+
return summary;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Merge hook configurations, with the second taking precedence
|
|
296
|
+
*/
|
|
297
|
+
mergeHooksConfiguration(target, source) {
|
|
298
|
+
for (const [event, configs] of Object.entries(source)) {
|
|
299
|
+
if (isValidHookEvent(event)) {
|
|
300
|
+
// For now, completely replace event configs rather than merging
|
|
301
|
+
// This ensures project settings completely override user settings for each event
|
|
302
|
+
target[event] = [...configs];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Check if a hook configuration applies to the current context
|
|
308
|
+
*/
|
|
309
|
+
configApplies(config, event, toolName) {
|
|
310
|
+
// For events that don't use matchers, config always applies
|
|
311
|
+
if (event === "UserPromptSubmit" || event === "Stop") {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
// For tool-based events, check matcher if present
|
|
315
|
+
if (event === "PreToolUse" || event === "PostToolUse") {
|
|
316
|
+
if (!config.matcher) {
|
|
317
|
+
// No matcher means applies to all tools
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
if (!toolName) {
|
|
321
|
+
// No tool name provided, cannot match
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
return this.matcher.matches(config.matcher, toolName);
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Validate a single event configuration
|
|
330
|
+
*/
|
|
331
|
+
validateEventConfig(event, config, index) {
|
|
332
|
+
const errors = [];
|
|
333
|
+
const prefix = `Hook event ${event}[${index}]`;
|
|
334
|
+
if (!isValidHookEventConfig(config)) {
|
|
335
|
+
errors.push(`${prefix}: Invalid hook event configuration structure`);
|
|
336
|
+
return errors;
|
|
337
|
+
}
|
|
338
|
+
// Validate matcher requirements
|
|
339
|
+
if ((event === "PreToolUse" || event === "PostToolUse") && config.matcher) {
|
|
340
|
+
if (!this.matcher.isValidPattern(config.matcher)) {
|
|
341
|
+
errors.push(`${prefix}: Invalid matcher pattern: ${config.matcher}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Validate that non-tool events don't have matchers
|
|
345
|
+
if ((event === "UserPromptSubmit" || event === "Stop") && config.matcher) {
|
|
346
|
+
errors.push(`${prefix}: Event ${event} should not have a matcher`);
|
|
347
|
+
}
|
|
348
|
+
// Validate commands
|
|
349
|
+
config.hooks.forEach((hookCommand, cmdIndex) => {
|
|
350
|
+
if (!this.executor.isCommandSafe(hookCommand.command)) {
|
|
351
|
+
errors.push(`${prefix}.hooks[${cmdIndex}]: Command may be unsafe: ${hookCommand.command}`);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
return errors;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Get statistics about current configuration
|
|
358
|
+
*/
|
|
359
|
+
getConfigurationStats() {
|
|
360
|
+
if (!this.configuration) {
|
|
361
|
+
return {
|
|
362
|
+
totalEvents: 0,
|
|
363
|
+
totalConfigs: 0,
|
|
364
|
+
totalCommands: 0,
|
|
365
|
+
eventBreakdown: {
|
|
366
|
+
PreToolUse: 0,
|
|
367
|
+
PostToolUse: 0,
|
|
368
|
+
UserPromptSubmit: 0,
|
|
369
|
+
Stop: 0,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const eventBreakdown = {
|
|
374
|
+
PreToolUse: 0,
|
|
375
|
+
PostToolUse: 0,
|
|
376
|
+
UserPromptSubmit: 0,
|
|
377
|
+
Stop: 0,
|
|
378
|
+
};
|
|
379
|
+
let totalConfigs = 0;
|
|
380
|
+
let totalCommands = 0;
|
|
381
|
+
Object.entries(this.configuration).forEach(([event, configs]) => {
|
|
382
|
+
if (isValidHookEvent(event)) {
|
|
383
|
+
eventBreakdown[event] = configs.length;
|
|
384
|
+
totalConfigs += configs.length;
|
|
385
|
+
totalCommands += configs.reduce((sum, config) => sum + config.hooks.length, 0);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
totalEvents: Object.keys(this.configuration).length,
|
|
390
|
+
totalConfigs,
|
|
391
|
+
totalCommands,
|
|
392
|
+
eventBreakdown,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Pattern Matcher
|
|
3
|
+
*
|
|
4
|
+
* Provides pattern matching functionality for hook tool name matching.
|
|
5
|
+
* Supports exact matching, wildcard patterns, and pipe-separated alternatives.
|
|
6
|
+
*/
|
|
7
|
+
export interface IHookMatcher {
|
|
8
|
+
matches(pattern: string, toolName: string): boolean;
|
|
9
|
+
isValidPattern(pattern: string): boolean;
|
|
10
|
+
getPatternType(pattern: string): "exact" | "glob" | "regex" | "alternatives";
|
|
11
|
+
}
|
|
12
|
+
export declare class HookMatcher implements IHookMatcher {
|
|
13
|
+
/**
|
|
14
|
+
* Test if pattern matches tool name
|
|
15
|
+
* Supports multiple matching strategies:
|
|
16
|
+
* - Exact matching: "Edit" matches "Edit"
|
|
17
|
+
* - Pipe alternatives: "Edit|Write" matches "Edit" or "Write"
|
|
18
|
+
* - Glob patterns: "Edit*" matches "EditFile", "EditText", etc.
|
|
19
|
+
* - Case insensitive matching
|
|
20
|
+
*/
|
|
21
|
+
matches(pattern: string, toolName: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Match a single pattern against tool name
|
|
24
|
+
*/
|
|
25
|
+
private matchesSingle;
|
|
26
|
+
/**
|
|
27
|
+
* Validate pattern syntax
|
|
28
|
+
*/
|
|
29
|
+
isValidPattern(pattern: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Validate single pattern syntax
|
|
32
|
+
*/
|
|
33
|
+
private isValidSinglePattern;
|
|
34
|
+
/**
|
|
35
|
+
* Get pattern type for optimization
|
|
36
|
+
*/
|
|
37
|
+
getPatternType(pattern: string): "exact" | "glob" | "regex" | "alternatives";
|
|
38
|
+
/**
|
|
39
|
+
* Get all tool names that would match this pattern from a given list
|
|
40
|
+
* Useful for testing and validation
|
|
41
|
+
*/
|
|
42
|
+
getMatches(pattern: string, toolNames: string[]): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Optimize pattern for repeated matching
|
|
45
|
+
* Returns a compiled matcher function for performance
|
|
46
|
+
*/
|
|
47
|
+
compile(pattern: string): (toolName: string) => boolean;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../../src/hooks/matcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,YAAY;IAE3B,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAGpD,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAGzC,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,cAAc,CAAC;CAC9E;AAED,qBAAa,WAAY,YAAW,YAAY;IAC9C;;;;;;;OAOG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAYnD;;OAEG;IACH,OAAO,CAAC,aAAa;IAmBrB;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAoBxC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAmB5B;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,cAAc;IA6B5E;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IAI1D;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO;CAkCxD"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Pattern Matcher
|
|
3
|
+
*
|
|
4
|
+
* Provides pattern matching functionality for hook tool name matching.
|
|
5
|
+
* Supports exact matching, wildcard patterns, and pipe-separated alternatives.
|
|
6
|
+
*/
|
|
7
|
+
import { minimatch } from "minimatch";
|
|
8
|
+
export class HookMatcher {
|
|
9
|
+
/**
|
|
10
|
+
* Test if pattern matches tool name
|
|
11
|
+
* Supports multiple matching strategies:
|
|
12
|
+
* - Exact matching: "Edit" matches "Edit"
|
|
13
|
+
* - Pipe alternatives: "Edit|Write" matches "Edit" or "Write"
|
|
14
|
+
* - Glob patterns: "Edit*" matches "EditFile", "EditText", etc.
|
|
15
|
+
* - Case insensitive matching
|
|
16
|
+
*/
|
|
17
|
+
matches(pattern, toolName) {
|
|
18
|
+
if (!pattern || !toolName)
|
|
19
|
+
return false;
|
|
20
|
+
// Handle pipe-separated alternatives (e.g., "Edit|Write|Delete")
|
|
21
|
+
if (pattern.includes("|")) {
|
|
22
|
+
const alternatives = pattern.split("|").map((alt) => alt.trim());
|
|
23
|
+
return alternatives.some((alt) => this.matchesSingle(alt, toolName));
|
|
24
|
+
}
|
|
25
|
+
return this.matchesSingle(pattern, toolName);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Match a single pattern against tool name
|
|
29
|
+
*/
|
|
30
|
+
matchesSingle(pattern, toolName) {
|
|
31
|
+
// Exact match (case insensitive)
|
|
32
|
+
if (pattern.toLowerCase() === toolName.toLowerCase()) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Glob pattern matching using minimatch
|
|
36
|
+
try {
|
|
37
|
+
return minimatch(toolName, pattern, {
|
|
38
|
+
nocase: true, // Case insensitive
|
|
39
|
+
noglobstar: false, // Allow ** patterns
|
|
40
|
+
nonegate: true, // Disable negation for security
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Invalid pattern, fall back to exact match
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate pattern syntax
|
|
50
|
+
*/
|
|
51
|
+
isValidPattern(pattern) {
|
|
52
|
+
if (!pattern || typeof pattern !== "string")
|
|
53
|
+
return false;
|
|
54
|
+
// Empty pattern is invalid
|
|
55
|
+
if (pattern.trim().length === 0)
|
|
56
|
+
return false;
|
|
57
|
+
// Handle pipe-separated alternatives
|
|
58
|
+
if (pattern.includes("|")) {
|
|
59
|
+
const alternatives = pattern.split("|").map((alt) => alt.trim());
|
|
60
|
+
return (alternatives.length > 0 &&
|
|
61
|
+
alternatives.every((alt) => alt.length > 0 && this.isValidSinglePattern(alt)));
|
|
62
|
+
}
|
|
63
|
+
return this.isValidSinglePattern(pattern);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validate single pattern syntax
|
|
67
|
+
*/
|
|
68
|
+
isValidSinglePattern(pattern) {
|
|
69
|
+
// Basic validation - non-empty string
|
|
70
|
+
if (!pattern || pattern.trim().length === 0)
|
|
71
|
+
return false;
|
|
72
|
+
// Check for dangerous characters that could be used for command injection
|
|
73
|
+
// Note: [ ] are allowed for glob patterns
|
|
74
|
+
const dangerousChars = /[;&|`$(){}><]/;
|
|
75
|
+
if (dangerousChars.test(pattern))
|
|
76
|
+
return false;
|
|
77
|
+
// Validate glob pattern syntax using minimatch
|
|
78
|
+
try {
|
|
79
|
+
// Test with a dummy string to validate pattern
|
|
80
|
+
minimatch("test", pattern, { nocase: true });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get pattern type for optimization
|
|
89
|
+
*/
|
|
90
|
+
getPatternType(pattern) {
|
|
91
|
+
if (!pattern)
|
|
92
|
+
return "exact";
|
|
93
|
+
// Check for pipe alternatives first
|
|
94
|
+
if (pattern.includes("|")) {
|
|
95
|
+
return "alternatives";
|
|
96
|
+
}
|
|
97
|
+
// Check for regex patterns first (before glob check)
|
|
98
|
+
if (pattern.startsWith("/") &&
|
|
99
|
+
pattern.endsWith("/") &&
|
|
100
|
+
pattern.length > 2) {
|
|
101
|
+
return "regex";
|
|
102
|
+
}
|
|
103
|
+
// Check for glob patterns
|
|
104
|
+
if (pattern.includes("*") ||
|
|
105
|
+
pattern.includes("?") ||
|
|
106
|
+
pattern.includes("[")) {
|
|
107
|
+
return "glob";
|
|
108
|
+
}
|
|
109
|
+
return "exact";
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get all tool names that would match this pattern from a given list
|
|
113
|
+
* Useful for testing and validation
|
|
114
|
+
*/
|
|
115
|
+
getMatches(pattern, toolNames) {
|
|
116
|
+
return toolNames.filter((toolName) => this.matches(pattern, toolName));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Optimize pattern for repeated matching
|
|
120
|
+
* Returns a compiled matcher function for performance
|
|
121
|
+
*/
|
|
122
|
+
compile(pattern) {
|
|
123
|
+
if (!this.isValidPattern(pattern)) {
|
|
124
|
+
return () => false;
|
|
125
|
+
}
|
|
126
|
+
const patternType = this.getPatternType(pattern);
|
|
127
|
+
switch (patternType) {
|
|
128
|
+
case "exact": {
|
|
129
|
+
const lowerPattern = pattern.toLowerCase();
|
|
130
|
+
return (toolName) => toolName.toLowerCase() === lowerPattern;
|
|
131
|
+
}
|
|
132
|
+
case "alternatives": {
|
|
133
|
+
const alternatives = pattern
|
|
134
|
+
.split("|")
|
|
135
|
+
.map((alt) => alt.trim().toLowerCase());
|
|
136
|
+
return (toolName) => {
|
|
137
|
+
const lowerTool = toolName.toLowerCase();
|
|
138
|
+
return alternatives.some((alt) => lowerTool === alt || minimatch(toolName, alt, { nocase: true }));
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
case "glob":
|
|
142
|
+
return (toolName) => minimatch(toolName, pattern, { nocase: true });
|
|
143
|
+
default:
|
|
144
|
+
return (toolName) => this.matches(pattern, toolName);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Settings Management
|
|
3
|
+
*
|
|
4
|
+
* Handles loading and merging of hook configurations from:
|
|
5
|
+
* - User settings: ~/.wave/hooks.json
|
|
6
|
+
* - Project settings: ./.wave/hooks.json
|
|
7
|
+
*/
|
|
8
|
+
import type { PartialHookConfiguration } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Get the user-specific hooks configuration file path
|
|
11
|
+
*/
|
|
12
|
+
export declare function getUserHooksConfigPath(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Get the project-specific hooks configuration file path
|
|
15
|
+
*/
|
|
16
|
+
export declare function getProjectHooksConfigPath(workdir: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Load hooks configuration from a JSON file
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadHooksConfigFromFile(filePath: string): PartialHookConfiguration | null;
|
|
21
|
+
/**
|
|
22
|
+
* Load user hooks configuration
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadUserHooksConfig(): PartialHookConfiguration | null;
|
|
25
|
+
/**
|
|
26
|
+
* Load project hooks configuration
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadProjectHooksConfig(workdir: string): PartialHookConfiguration | null;
|
|
29
|
+
/**
|
|
30
|
+
* Load and merge hooks configuration with project settings taking precedence
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadMergedHooksConfig(workdir: string): PartialHookConfiguration;
|
|
33
|
+
/**
|
|
34
|
+
* Check if any hooks configuration files exist
|
|
35
|
+
*/
|
|
36
|
+
export declare function hasHooksConfiguration(workdir: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Get information about available hooks configuration files
|
|
39
|
+
*/
|
|
40
|
+
export declare function getHooksConfigurationInfo(workdir: string): {
|
|
41
|
+
userConfigExists: boolean;
|
|
42
|
+
projectConfigExists: boolean;
|
|
43
|
+
userConfigPath: string;
|
|
44
|
+
projectConfigPath: string;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/hooks/settings.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAqB,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG9E;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,GACf,wBAAwB,GAAG,IAAI,CAoBjC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,wBAAwB,GAAG,IAAI,CAErE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,GACd,wBAAwB,GAAG,IAAI,CAEjC;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,GACd,wBAAwB,CAyB1B;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAK9D;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG;IAC1D,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAOA"}
|