wave-agent-sdk 0.0.4 → 0.0.6
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/dist/agent.d.ts +63 -9
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +103 -27
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/managers/aiManager.d.ts +5 -2
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +121 -53
- package/dist/managers/backgroundBashManager.d.ts +1 -1
- package/dist/managers/backgroundBashManager.d.ts.map +1 -1
- package/dist/{hooks/manager.d.ts → managers/hookManager.d.ts} +26 -7
- package/dist/managers/hookManager.d.ts.map +1 -0
- package/dist/{hooks/manager.js → managers/hookManager.js} +108 -18
- package/dist/managers/mcpManager.d.ts +1 -1
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +5 -5
- package/dist/managers/messageManager.d.ts +29 -5
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +33 -12
- package/dist/managers/skillManager.d.ts +1 -1
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +3 -3
- package/dist/managers/slashCommandManager.d.ts +1 -1
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +1 -1
- package/dist/managers/subagentManager.d.ts +9 -12
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +43 -45
- package/dist/managers/toolManager.d.ts +1 -1
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/services/aiService.d.ts +10 -2
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +25 -4
- package/dist/services/hook.d.ts +56 -0
- package/dist/services/hook.d.ts.map +1 -0
- package/dist/services/hook.js +276 -0
- package/dist/services/memory.js +3 -3
- package/dist/services/session.d.ts +65 -16
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +85 -34
- package/dist/tools/bashTool.js +2 -2
- package/dist/tools/deleteFileTool.js +1 -1
- package/dist/tools/editTool.js +1 -1
- package/dist/tools/multiEditTool.js +2 -2
- package/dist/tools/taskTool.d.ts.map +1 -1
- package/dist/tools/taskTool.js +7 -3
- package/dist/tools/writeTool.js +1 -1
- package/dist/types/commands.d.ts +24 -0
- package/dist/types/commands.d.ts.map +1 -0
- package/dist/types/commands.js +5 -0
- package/dist/types/config.d.ts +13 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +5 -0
- package/dist/types/core.d.ts +38 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/{types.js → types/core.js} +4 -13
- package/dist/{hooks/types.d.ts → types/hooks.d.ts} +2 -1
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +21 -0
- package/dist/types/mcp.d.ts +28 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +5 -0
- package/dist/types/messaging.d.ts +80 -0
- package/dist/types/messaging.d.ts.map +1 -0
- package/dist/types/messaging.js +5 -0
- package/dist/types/processes.d.ts +17 -0
- package/dist/types/processes.d.ts.map +1 -0
- package/dist/types/processes.js +5 -0
- package/dist/types/skills.d.ts +78 -0
- package/dist/types/skills.d.ts.map +1 -0
- package/dist/types/skills.js +17 -0
- package/dist/utils/configResolver.d.ts +1 -1
- package/dist/utils/configResolver.d.ts.map +1 -1
- package/dist/utils/configResolver.js +1 -1
- package/dist/utils/configValidator.d.ts +1 -1
- package/dist/utils/configValidator.d.ts.map +1 -1
- package/dist/utils/configValidator.js +1 -1
- package/dist/utils/convertMessagesForAPI.d.ts +1 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/customCommands.d.ts +1 -1
- package/dist/utils/customCommands.d.ts.map +1 -1
- package/dist/{hooks/matcher.d.ts → utils/hookMatcher.d.ts} +1 -1
- package/dist/utils/hookMatcher.d.ts.map +1 -0
- package/dist/utils/markdownParser.d.ts +1 -1
- package/dist/utils/markdownParser.d.ts.map +1 -1
- package/dist/utils/mcpUtils.d.ts +1 -1
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/messageOperations.d.ts +7 -2
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +18 -1
- package/dist/utils/skillParser.d.ts +1 -1
- package/dist/utils/skillParser.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent.ts +150 -50
- package/src/index.ts +3 -4
- package/src/managers/aiManager.ts +282 -164
- package/src/managers/backgroundBashManager.ts +1 -1
- package/src/{hooks/manager.ts → managers/hookManager.ts} +163 -28
- package/src/managers/mcpManager.ts +6 -6
- package/src/managers/messageManager.ts +69 -10
- package/src/managers/skillManager.ts +4 -4
- package/src/managers/slashCommandManager.ts +6 -2
- package/src/managers/subagentManager.ts +58 -53
- package/src/managers/toolManager.ts +1 -1
- package/src/services/aiService.ts +37 -7
- package/src/services/hook.ts +360 -0
- package/src/services/memory.ts +3 -3
- package/src/services/session.ts +99 -33
- package/src/tools/bashTool.ts +2 -2
- package/src/tools/deleteFileTool.ts +1 -1
- package/src/tools/editTool.ts +1 -1
- package/src/tools/multiEditTool.ts +2 -2
- package/src/tools/taskTool.ts +13 -5
- package/src/tools/writeTool.ts +1 -1
- package/src/types/commands.ts +26 -0
- package/src/types/config.ts +14 -0
- package/src/types/core.ts +49 -0
- package/src/{hooks/types.ts → types/hooks.ts} +1 -0
- package/src/types/index.ts +23 -0
- package/src/{types.ts → types/index.ts.backup} +13 -0
- package/src/types/mcp.ts +31 -0
- package/src/types/messaging.ts +103 -0
- package/src/types/processes.ts +18 -0
- package/src/types/skills.ts +91 -0
- package/src/utils/configResolver.ts +1 -1
- package/src/utils/configValidator.ts +5 -1
- package/src/utils/convertMessagesForAPI.ts +1 -1
- package/src/utils/customCommands.ts +1 -1
- package/src/utils/markdownParser.ts +1 -1
- package/src/utils/mcpUtils.ts +1 -1
- package/src/utils/messageOperations.ts +22 -1
- package/src/utils/skillParser.ts +1 -1
- package/dist/hooks/executor.d.ts +0 -56
- package/dist/hooks/executor.d.ts.map +0 -1
- package/dist/hooks/executor.js +0 -312
- package/dist/hooks/index.d.ts +0 -17
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -14
- package/dist/hooks/manager.d.ts.map +0 -1
- package/dist/hooks/matcher.d.ts.map +0 -1
- package/dist/hooks/settings.d.ts +0 -46
- package/dist/hooks/settings.d.ts.map +0 -1
- package/dist/hooks/settings.js +0 -100
- package/dist/hooks/types.d.ts.map +0 -1
- package/dist/types.d.ts +0 -276
- package/dist/types.d.ts.map +0 -1
- package/src/hooks/executor.ts +0 -440
- package/src/hooks/index.ts +0 -52
- package/src/hooks/settings.ts +0 -129
- /package/dist/{hooks/types.js → types/hooks.js} +0 -0
- /package/dist/{hooks/matcher.js → utils/hookMatcher.js} +0 -0
- /package/src/{hooks/matcher.ts → utils/hookMatcher.ts} +0 -0
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import type { SubagentConfiguration } from "../utils/subagentParser.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Message,
|
|
5
|
+
Logger,
|
|
6
|
+
GatewayConfig,
|
|
7
|
+
ModelConfig,
|
|
8
|
+
Usage,
|
|
9
|
+
} from "../types/index.js";
|
|
4
10
|
import { AIManager } from "./aiManager.js";
|
|
5
11
|
import {
|
|
6
12
|
MessageManager,
|
|
@@ -15,7 +21,6 @@ export interface SubagentInstance {
|
|
|
15
21
|
messageManager: MessageManager;
|
|
16
22
|
toolManager: ToolManager;
|
|
17
23
|
status: "initializing" | "active" | "completed" | "error" | "aborted";
|
|
18
|
-
taskDescription: string;
|
|
19
24
|
messages: Message[];
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -27,6 +32,7 @@ export interface SubagentManagerOptions {
|
|
|
27
32
|
gatewayConfig: GatewayConfig;
|
|
28
33
|
modelConfig: ModelConfig;
|
|
29
34
|
tokenLimit: number;
|
|
35
|
+
onUsageAdded?: (usage: Usage) => void;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export class SubagentManager {
|
|
@@ -40,6 +46,7 @@ export class SubagentManager {
|
|
|
40
46
|
private gatewayConfig: GatewayConfig;
|
|
41
47
|
private modelConfig: ModelConfig;
|
|
42
48
|
private tokenLimit: number;
|
|
49
|
+
private onUsageAdded?: (usage: Usage) => void;
|
|
43
50
|
|
|
44
51
|
constructor(options: SubagentManagerOptions) {
|
|
45
52
|
this.workdir = options.workdir;
|
|
@@ -49,6 +56,7 @@ export class SubagentManager {
|
|
|
49
56
|
this.gatewayConfig = options.gatewayConfig;
|
|
50
57
|
this.modelConfig = options.modelConfig;
|
|
51
58
|
this.tokenLimit = options.tokenLimit;
|
|
59
|
+
this.onUsageAdded = options.onUsageAdded;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
/**
|
|
@@ -98,7 +106,11 @@ export class SubagentManager {
|
|
|
98
106
|
*/
|
|
99
107
|
async createInstance(
|
|
100
108
|
configuration: SubagentConfiguration,
|
|
101
|
-
|
|
109
|
+
parameters: {
|
|
110
|
+
description: string;
|
|
111
|
+
prompt: string;
|
|
112
|
+
subagent_type: string;
|
|
113
|
+
},
|
|
102
114
|
): Promise<SubagentInstance> {
|
|
103
115
|
if (
|
|
104
116
|
!this.parentToolManager ||
|
|
@@ -156,6 +168,9 @@ export class SubagentManager {
|
|
|
156
168
|
agentModel: modelToUse,
|
|
157
169
|
},
|
|
158
170
|
tokenLimit: this.tokenLimit,
|
|
171
|
+
callbacks: {
|
|
172
|
+
onUsageAdded: this.onUsageAdded,
|
|
173
|
+
},
|
|
159
174
|
});
|
|
160
175
|
|
|
161
176
|
const instance: SubagentInstance = {
|
|
@@ -165,7 +180,6 @@ export class SubagentManager {
|
|
|
165
180
|
messageManager,
|
|
166
181
|
toolManager,
|
|
167
182
|
status: "initializing",
|
|
168
|
-
taskDescription,
|
|
169
183
|
messages: [],
|
|
170
184
|
};
|
|
171
185
|
|
|
@@ -177,6 +191,7 @@ export class SubagentManager {
|
|
|
177
191
|
configuration.name,
|
|
178
192
|
"active",
|
|
179
193
|
[],
|
|
194
|
+
parameters,
|
|
180
195
|
);
|
|
181
196
|
|
|
182
197
|
return instance;
|
|
@@ -191,14 +206,31 @@ export class SubagentManager {
|
|
|
191
206
|
async executeTask(
|
|
192
207
|
instance: SubagentInstance,
|
|
193
208
|
prompt: string,
|
|
209
|
+
abortSignal?: AbortSignal,
|
|
194
210
|
): Promise<string> {
|
|
195
211
|
try {
|
|
212
|
+
// Check if already aborted before starting
|
|
213
|
+
if (abortSignal?.aborted) {
|
|
214
|
+
throw new Error("Task was aborted before execution started");
|
|
215
|
+
}
|
|
216
|
+
|
|
196
217
|
// Set status to active and update parent
|
|
197
218
|
this.updateInstanceStatus(instance.subagentId, "active");
|
|
198
219
|
this.parentMessageManager.updateSubagentBlock(instance.subagentId, {
|
|
199
220
|
status: "active",
|
|
200
221
|
});
|
|
201
222
|
|
|
223
|
+
// Set up abort handler
|
|
224
|
+
if (abortSignal) {
|
|
225
|
+
abortSignal.addEventListener("abort", () => {
|
|
226
|
+
this.updateInstanceStatus(instance.subagentId, "aborted");
|
|
227
|
+
this.parentMessageManager.updateSubagentBlock(instance.subagentId, {
|
|
228
|
+
status: "aborted",
|
|
229
|
+
messages: instance.messages,
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
202
234
|
// Add the user's prompt as a message
|
|
203
235
|
instance.messageManager.addUserMessage(prompt);
|
|
204
236
|
|
|
@@ -215,7 +247,9 @@ export class SubagentManager {
|
|
|
215
247
|
}
|
|
216
248
|
|
|
217
249
|
// Execute the AI request with tool restrictions
|
|
218
|
-
|
|
250
|
+
// The AIManager will handle abort signals through its own abort controllers
|
|
251
|
+
// We need to abort the AI execution if the external abort signal is triggered
|
|
252
|
+
const executeAI = instance.aiManager.sendAIMessage({
|
|
219
253
|
allowedTools,
|
|
220
254
|
model:
|
|
221
255
|
instance.configuration.model !== "inherit"
|
|
@@ -223,6 +257,25 @@ export class SubagentManager {
|
|
|
223
257
|
: undefined,
|
|
224
258
|
});
|
|
225
259
|
|
|
260
|
+
// If we have an abort signal, race against it
|
|
261
|
+
if (abortSignal) {
|
|
262
|
+
await Promise.race([
|
|
263
|
+
executeAI,
|
|
264
|
+
new Promise<never>((_, reject) => {
|
|
265
|
+
if (abortSignal.aborted) {
|
|
266
|
+
reject(new Error("Task was aborted"));
|
|
267
|
+
}
|
|
268
|
+
abortSignal.addEventListener("abort", () => {
|
|
269
|
+
// Abort the AI execution
|
|
270
|
+
instance.aiManager.abortAIMessage();
|
|
271
|
+
reject(new Error("Task was aborted"));
|
|
272
|
+
});
|
|
273
|
+
}),
|
|
274
|
+
]);
|
|
275
|
+
} else {
|
|
276
|
+
await executeAI;
|
|
277
|
+
}
|
|
278
|
+
|
|
226
279
|
// Get the latest messages to extract the response
|
|
227
280
|
const messages = instance.messageManager.getMessages();
|
|
228
281
|
const lastAssistantMessage = messages
|
|
@@ -286,52 +339,6 @@ export class SubagentManager {
|
|
|
286
339
|
}
|
|
287
340
|
}
|
|
288
341
|
|
|
289
|
-
/**
|
|
290
|
-
* Abort a running subagent instance
|
|
291
|
-
*/
|
|
292
|
-
abortInstance(subagentId: string): boolean {
|
|
293
|
-
const instance = this.instances.get(subagentId);
|
|
294
|
-
if (!instance) {
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Only abort active or initializing instances
|
|
299
|
-
if (instance.status !== "active" && instance.status !== "initializing") {
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
// Abort the AI manager operations
|
|
305
|
-
instance.aiManager.abortAIMessage();
|
|
306
|
-
|
|
307
|
-
// Update status
|
|
308
|
-
this.updateInstanceStatus(subagentId, "aborted");
|
|
309
|
-
this.parentMessageManager.updateSubagentBlock(subagentId, {
|
|
310
|
-
status: "aborted",
|
|
311
|
-
messages: instance.messages,
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
this.logger?.info(`Aborted subagent instance: ${subagentId}`);
|
|
315
|
-
return true;
|
|
316
|
-
} catch (error) {
|
|
317
|
-
this.logger?.error(
|
|
318
|
-
`Failed to abort subagent instance ${subagentId}:`,
|
|
319
|
-
error,
|
|
320
|
-
);
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Abort all active subagent instances
|
|
327
|
-
*/
|
|
328
|
-
abortAllInstances(): void {
|
|
329
|
-
const activeInstances = this.getActiveInstances();
|
|
330
|
-
for (const instance of activeInstances) {
|
|
331
|
-
this.abortInstance(instance.subagentId);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
342
|
/**
|
|
336
343
|
* Clean up completed, errored, or aborted instances
|
|
337
344
|
*/
|
|
@@ -361,8 +368,6 @@ export class SubagentManager {
|
|
|
361
368
|
* Clean up all instances (for session end)
|
|
362
369
|
*/
|
|
363
370
|
cleanup(): void {
|
|
364
|
-
// Abort all active instances before cleanup
|
|
365
|
-
this.abortAllInstances();
|
|
366
371
|
this.instances.clear();
|
|
367
372
|
}
|
|
368
373
|
}
|
|
@@ -14,7 +14,7 @@ import { createTaskTool } from "../tools/taskTool.js";
|
|
|
14
14
|
import { createSkillTool } from "../tools/skillTool.js";
|
|
15
15
|
import { McpManager } from "./mcpManager.js";
|
|
16
16
|
import { ChatCompletionFunctionTool } from "openai/resources.js";
|
|
17
|
-
import type { Logger } from "../types.js";
|
|
17
|
+
import type { Logger } from "../types/index.js";
|
|
18
18
|
import type { SubagentManager } from "./subagentManager.js";
|
|
19
19
|
import type { SkillManager } from "./skillManager.js";
|
|
20
20
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
ChatCompletionMessageParam,
|
|
6
6
|
ChatCompletionFunctionTool,
|
|
7
7
|
} from "openai/resources.js";
|
|
8
|
-
import type { GatewayConfig, ModelConfig } from "../types.js";
|
|
8
|
+
import type { GatewayConfig, ModelConfig } from "../types/index.js";
|
|
9
9
|
import * as os from "os";
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as path from "path";
|
|
@@ -116,7 +116,14 @@ export async function callAgent(
|
|
|
116
116
|
// Build system prompt content
|
|
117
117
|
let systemContent =
|
|
118
118
|
systemPrompt ||
|
|
119
|
-
`You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user
|
|
119
|
+
`You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
120
|
+
|
|
121
|
+
# Tool usage policy
|
|
122
|
+
- When doing file search, prefer to use the Task tool in order to reduce context usage.
|
|
123
|
+
- You should proactively use the Task tool with specialized agents when the task at hand matches the agent's description.
|
|
124
|
+
- You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. When making multiple bash tool calls, you MUST send a single message with multiple tools calls to run the calls in parallel. For example, if you need to run "git status" and "git diff", send a single message with two tool calls to run the calls in parallel.
|
|
125
|
+
|
|
126
|
+
`;
|
|
120
127
|
|
|
121
128
|
// Always add environment information
|
|
122
129
|
systemContent += `
|
|
@@ -214,9 +221,18 @@ export interface CompressMessagesOptions {
|
|
|
214
221
|
abortSignal?: AbortSignal;
|
|
215
222
|
}
|
|
216
223
|
|
|
224
|
+
export interface CompressMessagesResult {
|
|
225
|
+
content: string;
|
|
226
|
+
usage?: {
|
|
227
|
+
prompt_tokens: number;
|
|
228
|
+
completion_tokens: number;
|
|
229
|
+
total_tokens: number;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
217
233
|
export async function compressMessages(
|
|
218
234
|
options: CompressMessagesOptions,
|
|
219
|
-
): Promise<
|
|
235
|
+
): Promise<CompressMessagesResult> {
|
|
220
236
|
const { gatewayConfig, modelConfig, messages, abortSignal } = options;
|
|
221
237
|
|
|
222
238
|
// Create OpenAI client with injected configuration
|
|
@@ -294,15 +310,29 @@ For technical conversations, structure as:
|
|
|
294
310
|
},
|
|
295
311
|
);
|
|
296
312
|
|
|
297
|
-
|
|
313
|
+
const content =
|
|
298
314
|
response.choices[0]?.message?.content?.trim() ||
|
|
299
|
-
"Failed to compress conversation history"
|
|
300
|
-
|
|
315
|
+
"Failed to compress conversation history";
|
|
316
|
+
const usage = response.usage
|
|
317
|
+
? {
|
|
318
|
+
prompt_tokens: response.usage.prompt_tokens,
|
|
319
|
+
completion_tokens: response.usage.completion_tokens,
|
|
320
|
+
total_tokens: response.usage.total_tokens,
|
|
321
|
+
}
|
|
322
|
+
: undefined;
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
content,
|
|
326
|
+
usage,
|
|
327
|
+
};
|
|
301
328
|
} catch (error) {
|
|
302
329
|
if ((error as Error).name === "AbortError") {
|
|
303
330
|
throw new Error("Compression request was aborted");
|
|
304
331
|
}
|
|
305
332
|
// // logger.error("Failed to compress messages:", error);
|
|
306
|
-
return
|
|
333
|
+
return {
|
|
334
|
+
content: "Failed to compress conversation history",
|
|
335
|
+
usage: undefined,
|
|
336
|
+
};
|
|
307
337
|
}
|
|
308
338
|
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Services
|
|
3
|
+
*
|
|
4
|
+
* Consolidated hook services providing both execution and configuration functionality.
|
|
5
|
+
* Combines hook command execution and settings management into a single module.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
9
|
+
import { existsSync, readFileSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import {
|
|
13
|
+
type HookExecutionContext,
|
|
14
|
+
type HookExecutionResult,
|
|
15
|
+
type HookExecutionOptions,
|
|
16
|
+
type ExtendedHookExecutionContext,
|
|
17
|
+
type HookJsonInput,
|
|
18
|
+
type HookConfiguration,
|
|
19
|
+
type PartialHookConfiguration,
|
|
20
|
+
getSessionFilePath,
|
|
21
|
+
isValidHookEvent,
|
|
22
|
+
} from "../types/hooks.js";
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Hook Execution Functions
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build JSON input data for hook stdin
|
|
30
|
+
*/
|
|
31
|
+
function buildHookJsonInput(
|
|
32
|
+
context: ExtendedHookExecutionContext,
|
|
33
|
+
): HookJsonInput {
|
|
34
|
+
const jsonInput: HookJsonInput = {
|
|
35
|
+
session_id: context.sessionId || "unknown",
|
|
36
|
+
transcript_path:
|
|
37
|
+
context.transcriptPath ||
|
|
38
|
+
(context.sessionId ? getSessionFilePath(context.sessionId) : ""),
|
|
39
|
+
cwd: context.cwd || context.projectDir,
|
|
40
|
+
hook_event_name: context.event,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Add optional fields based on event type
|
|
44
|
+
if (context.event === "PreToolUse" || context.event === "PostToolUse") {
|
|
45
|
+
if (context.toolName) {
|
|
46
|
+
jsonInput.tool_name = context.toolName;
|
|
47
|
+
}
|
|
48
|
+
if (context.toolInput !== undefined) {
|
|
49
|
+
jsonInput.tool_input = context.toolInput;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (context.event === "PostToolUse" && context.toolResponse !== undefined) {
|
|
54
|
+
jsonInput.tool_response = context.toolResponse;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
context.event === "UserPromptSubmit" &&
|
|
59
|
+
context.userPrompt !== undefined
|
|
60
|
+
) {
|
|
61
|
+
jsonInput.user_prompt = context.userPrompt;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return jsonInput;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Execute a single hook command
|
|
69
|
+
*/
|
|
70
|
+
export async function executeCommand(
|
|
71
|
+
command: string,
|
|
72
|
+
context: HookExecutionContext | ExtendedHookExecutionContext,
|
|
73
|
+
options?: HookExecutionOptions,
|
|
74
|
+
): Promise<HookExecutionResult> {
|
|
75
|
+
const defaultTimeout = 10000; // 10 seconds
|
|
76
|
+
const maxTimeout = 300000; // 5 minutes
|
|
77
|
+
const skipExecution =
|
|
78
|
+
process.env.NODE_ENV === "test" &&
|
|
79
|
+
process.env.TEST_HOOK_EXECUTION !== "true";
|
|
80
|
+
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
const timeout = Math.min(options?.timeout ?? defaultTimeout, maxTimeout);
|
|
83
|
+
|
|
84
|
+
// Return mock result if execution is skipped
|
|
85
|
+
if (skipExecution) {
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
exitCode: 0,
|
|
89
|
+
stdout: "",
|
|
90
|
+
stderr: "",
|
|
91
|
+
duration: 0,
|
|
92
|
+
timedOut: false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
let stdout = "";
|
|
98
|
+
let stderr = "";
|
|
99
|
+
let timedOut = false;
|
|
100
|
+
|
|
101
|
+
// Parse command for shell execution
|
|
102
|
+
const isWindows = process.platform === "win32";
|
|
103
|
+
const shell = isWindows ? "cmd.exe" : "/bin/sh";
|
|
104
|
+
const shellFlag = isWindows ? "/c" : "-c";
|
|
105
|
+
|
|
106
|
+
const childProcess: ChildProcess = spawn(shell, [shellFlag, command], {
|
|
107
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
108
|
+
cwd: context.projectDir,
|
|
109
|
+
env: {
|
|
110
|
+
...process.env,
|
|
111
|
+
HOOK_EVENT: context.event,
|
|
112
|
+
HOOK_TOOL_NAME: context.toolName || "",
|
|
113
|
+
HOOK_PROJECT_DIR: context.projectDir,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Set up timeout
|
|
118
|
+
const timeoutHandle = setTimeout(() => {
|
|
119
|
+
timedOut = true;
|
|
120
|
+
childProcess.kill("SIGTERM");
|
|
121
|
+
|
|
122
|
+
// Force kill after additional delay
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
if (!childProcess.killed) {
|
|
125
|
+
childProcess.kill("SIGKILL");
|
|
126
|
+
}
|
|
127
|
+
}, 2000);
|
|
128
|
+
}, timeout);
|
|
129
|
+
|
|
130
|
+
// Handle stdout
|
|
131
|
+
if (childProcess.stdout) {
|
|
132
|
+
childProcess.stdout.on("data", (data: Buffer) => {
|
|
133
|
+
stdout += data.toString();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle stderr
|
|
138
|
+
if (childProcess.stderr) {
|
|
139
|
+
childProcess.stderr.on("data", (data: Buffer) => {
|
|
140
|
+
stderr += data.toString();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Send JSON input to stdin if we have extended context
|
|
145
|
+
if (childProcess.stdin && "sessionId" in context) {
|
|
146
|
+
try {
|
|
147
|
+
const jsonInput = buildHookJsonInput(context);
|
|
148
|
+
childProcess.stdin.write(JSON.stringify(jsonInput, null, 2));
|
|
149
|
+
childProcess.stdin.end();
|
|
150
|
+
} catch {
|
|
151
|
+
// Continue execution even if JSON input fails
|
|
152
|
+
}
|
|
153
|
+
} else if (childProcess.stdin) {
|
|
154
|
+
childProcess.stdin.end();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Handle process completion
|
|
158
|
+
childProcess.on("close", (code: number | null) => {
|
|
159
|
+
clearTimeout(timeoutHandle);
|
|
160
|
+
const duration = Date.now() - startTime;
|
|
161
|
+
|
|
162
|
+
resolve({
|
|
163
|
+
success: !timedOut && (code === 0 || code === null),
|
|
164
|
+
exitCode: code || 0,
|
|
165
|
+
stdout: stdout.trim(),
|
|
166
|
+
stderr: stderr.trim(),
|
|
167
|
+
duration,
|
|
168
|
+
timedOut,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Handle process errors
|
|
173
|
+
childProcess.on("error", (error: Error) => {
|
|
174
|
+
clearTimeout(timeoutHandle);
|
|
175
|
+
const duration = Date.now() - startTime;
|
|
176
|
+
|
|
177
|
+
resolve({
|
|
178
|
+
success: false,
|
|
179
|
+
exitCode: 1,
|
|
180
|
+
stdout: stdout.trim(),
|
|
181
|
+
stderr: error.message,
|
|
182
|
+
duration,
|
|
183
|
+
timedOut,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Execute multiple commands in sequence
|
|
191
|
+
*/
|
|
192
|
+
export async function executeCommands(
|
|
193
|
+
commands: string[],
|
|
194
|
+
context: HookExecutionContext | ExtendedHookExecutionContext,
|
|
195
|
+
options?: HookExecutionOptions,
|
|
196
|
+
): Promise<HookExecutionResult[]> {
|
|
197
|
+
const results: HookExecutionResult[] = [];
|
|
198
|
+
|
|
199
|
+
for (const command of commands) {
|
|
200
|
+
const result = await executeCommand(command, context, options);
|
|
201
|
+
results.push(result);
|
|
202
|
+
|
|
203
|
+
// Stop on first failure unless continueOnFailure is set
|
|
204
|
+
if (!result.success && !options?.continueOnFailure) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return results;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Validate command safety (basic checks)
|
|
214
|
+
*/
|
|
215
|
+
export function isCommandSafe(command: string): boolean {
|
|
216
|
+
const trimmed = command.trim();
|
|
217
|
+
|
|
218
|
+
// Empty commands are safe (no-op)
|
|
219
|
+
if (!trimmed) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check for obviously dangerous patterns
|
|
224
|
+
const dangerousPatterns = [
|
|
225
|
+
/rm\s+-rf\s+\//, // rm -rf /
|
|
226
|
+
/sudo\s+rm/, // sudo rm
|
|
227
|
+
/>\s*\/dev\/sd[a-z]/, // writing to disk devices
|
|
228
|
+
/dd\s+if=.*of=\/dev/, // dd to devices
|
|
229
|
+
/mkfs/, // filesystem creation
|
|
230
|
+
/fdisk/, // disk partitioning
|
|
231
|
+
/format\s+[a-z]:/, // Windows format command
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
return !dangerousPatterns.some((pattern) =>
|
|
235
|
+
pattern.test(trimmed.toLowerCase()),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// =============================================================================
|
|
240
|
+
// Hook Settings Functions
|
|
241
|
+
// =============================================================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get the user-specific hooks configuration file path
|
|
245
|
+
*/
|
|
246
|
+
export function getUserHooksConfigPath(): string {
|
|
247
|
+
return join(homedir(), ".wave", "settings.json");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get the project-specific hooks configuration file path
|
|
252
|
+
*/
|
|
253
|
+
export function getProjectHooksConfigPath(workdir: string): string {
|
|
254
|
+
return join(workdir, ".wave", "settings.json");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Load hooks configuration from a JSON file
|
|
259
|
+
*/
|
|
260
|
+
export function loadHooksConfigFromFile(
|
|
261
|
+
filePath: string,
|
|
262
|
+
): PartialHookConfiguration | null {
|
|
263
|
+
if (!existsSync(filePath)) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const content = readFileSync(filePath, "utf-8");
|
|
268
|
+
const config = JSON.parse(content) as HookConfiguration;
|
|
269
|
+
|
|
270
|
+
// Validate basic structure
|
|
271
|
+
if (!config || typeof config !== "object" || !config.hooks) {
|
|
272
|
+
throw new Error(`Invalid hooks configuration structure in ${filePath}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return config.hooks;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Load user-specific hooks configuration
|
|
280
|
+
*/
|
|
281
|
+
export function loadUserHooksConfig(): PartialHookConfiguration | null {
|
|
282
|
+
return loadHooksConfigFromFile(getUserHooksConfigPath());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Load project-specific hooks configuration
|
|
287
|
+
*/
|
|
288
|
+
export function loadProjectHooksConfig(
|
|
289
|
+
workdir: string,
|
|
290
|
+
): PartialHookConfiguration | null {
|
|
291
|
+
return loadHooksConfigFromFile(getProjectHooksConfigPath(workdir));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Load and merge hooks configuration from both user and project sources
|
|
296
|
+
*/
|
|
297
|
+
export function loadMergedHooksConfig(
|
|
298
|
+
workdir: string,
|
|
299
|
+
): PartialHookConfiguration | null {
|
|
300
|
+
const userConfig = loadUserHooksConfig();
|
|
301
|
+
const projectConfig = loadProjectHooksConfig(workdir);
|
|
302
|
+
|
|
303
|
+
// No configuration found
|
|
304
|
+
if (!userConfig && !projectConfig) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Only one configuration found
|
|
309
|
+
if (!userConfig) return projectConfig;
|
|
310
|
+
if (!projectConfig) return userConfig;
|
|
311
|
+
|
|
312
|
+
// Merge configurations (project overrides user)
|
|
313
|
+
const merged: PartialHookConfiguration = {};
|
|
314
|
+
|
|
315
|
+
// Combine all hook events
|
|
316
|
+
const allEvents = new Set([
|
|
317
|
+
...Object.keys(userConfig),
|
|
318
|
+
...Object.keys(projectConfig),
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
for (const event of allEvents) {
|
|
322
|
+
if (!isValidHookEvent(event)) continue;
|
|
323
|
+
|
|
324
|
+
const userEventConfigs = userConfig[event] || [];
|
|
325
|
+
const projectEventConfigs = projectConfig[event] || [];
|
|
326
|
+
|
|
327
|
+
// Project configurations take precedence
|
|
328
|
+
merged[event] = [...userEventConfigs, ...projectEventConfigs];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return merged;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check if hooks configuration exists (user or project)
|
|
336
|
+
*/
|
|
337
|
+
export function hasHooksConfiguration(workdir: string): boolean {
|
|
338
|
+
return (
|
|
339
|
+
existsSync(getUserHooksConfigPath()) ||
|
|
340
|
+
existsSync(getProjectHooksConfigPath(workdir))
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get hooks configuration information for debugging
|
|
346
|
+
*/
|
|
347
|
+
export function getHooksConfigurationInfo(workdir: string): {
|
|
348
|
+
hasUser: boolean;
|
|
349
|
+
hasProject: boolean;
|
|
350
|
+
paths: string[];
|
|
351
|
+
} {
|
|
352
|
+
const userPath = getUserHooksConfigPath();
|
|
353
|
+
const projectPath = getProjectHooksConfigPath(workdir);
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
hasUser: existsSync(userPath),
|
|
357
|
+
hasProject: existsSync(projectPath),
|
|
358
|
+
paths: [userPath, projectPath],
|
|
359
|
+
};
|
|
360
|
+
}
|
package/src/services/memory.ts
CHANGED
|
@@ -41,7 +41,7 @@ export const addMemory = async (
|
|
|
41
41
|
// Write file
|
|
42
42
|
await fs.writeFile(memoryFilePath, updatedContent, "utf-8");
|
|
43
43
|
|
|
44
|
-
// logger.
|
|
44
|
+
// logger.debug(`Memory added to ${memoryFilePath}:`, message);
|
|
45
45
|
} catch (error) {
|
|
46
46
|
// logger.error("Failed to add memory:", error);
|
|
47
47
|
throw new Error(`Failed to add memory: ${(error as Error).message}`);
|
|
@@ -63,7 +63,7 @@ export const ensureUserMemoryFile = async (): Promise<void> => {
|
|
|
63
63
|
const initialContent =
|
|
64
64
|
"# User Memory\n\nThis is the user-level memory file, recording important information and context across projects.\n\n";
|
|
65
65
|
await fs.writeFile(USER_MEMORY_FILE, initialContent, "utf-8");
|
|
66
|
-
// logger.
|
|
66
|
+
// logger.debug(`Created user memory file: ${USER_MEMORY_FILE}`);
|
|
67
67
|
} else {
|
|
68
68
|
throw error;
|
|
69
69
|
}
|
|
@@ -93,7 +93,7 @@ export const addUserMemory = async (message: string): Promise<void> => {
|
|
|
93
93
|
// Write file
|
|
94
94
|
await fs.writeFile(USER_MEMORY_FILE, updatedContent, "utf-8");
|
|
95
95
|
|
|
96
|
-
// logger.
|
|
96
|
+
// logger.debug(`User memory added to ${USER_MEMORY_FILE}:`, message);
|
|
97
97
|
} catch (error) {
|
|
98
98
|
// logger.error("Failed to add user memory:", error);
|
|
99
99
|
throw new Error(`Failed to add user memory: ${(error as Error).message}`);
|