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.
Files changed (155) hide show
  1. package/dist/agent.d.ts +63 -9
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +103 -27
  4. package/dist/index.d.ts +3 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/managers/aiManager.d.ts +5 -2
  8. package/dist/managers/aiManager.d.ts.map +1 -1
  9. package/dist/managers/aiManager.js +121 -53
  10. package/dist/managers/backgroundBashManager.d.ts +1 -1
  11. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  12. package/dist/{hooks/manager.d.ts → managers/hookManager.d.ts} +26 -7
  13. package/dist/managers/hookManager.d.ts.map +1 -0
  14. package/dist/{hooks/manager.js → managers/hookManager.js} +108 -18
  15. package/dist/managers/mcpManager.d.ts +1 -1
  16. package/dist/managers/mcpManager.d.ts.map +1 -1
  17. package/dist/managers/mcpManager.js +5 -5
  18. package/dist/managers/messageManager.d.ts +29 -5
  19. package/dist/managers/messageManager.d.ts.map +1 -1
  20. package/dist/managers/messageManager.js +33 -12
  21. package/dist/managers/skillManager.d.ts +1 -1
  22. package/dist/managers/skillManager.d.ts.map +1 -1
  23. package/dist/managers/skillManager.js +3 -3
  24. package/dist/managers/slashCommandManager.d.ts +1 -1
  25. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  26. package/dist/managers/slashCommandManager.js +1 -1
  27. package/dist/managers/subagentManager.d.ts +9 -12
  28. package/dist/managers/subagentManager.d.ts.map +1 -1
  29. package/dist/managers/subagentManager.js +43 -45
  30. package/dist/managers/toolManager.d.ts +1 -1
  31. package/dist/managers/toolManager.d.ts.map +1 -1
  32. package/dist/services/aiService.d.ts +10 -2
  33. package/dist/services/aiService.d.ts.map +1 -1
  34. package/dist/services/aiService.js +25 -4
  35. package/dist/services/hook.d.ts +56 -0
  36. package/dist/services/hook.d.ts.map +1 -0
  37. package/dist/services/hook.js +276 -0
  38. package/dist/services/memory.js +3 -3
  39. package/dist/services/session.d.ts +65 -16
  40. package/dist/services/session.d.ts.map +1 -1
  41. package/dist/services/session.js +85 -34
  42. package/dist/tools/bashTool.js +2 -2
  43. package/dist/tools/deleteFileTool.js +1 -1
  44. package/dist/tools/editTool.js +1 -1
  45. package/dist/tools/multiEditTool.js +2 -2
  46. package/dist/tools/taskTool.d.ts.map +1 -1
  47. package/dist/tools/taskTool.js +7 -3
  48. package/dist/tools/writeTool.js +1 -1
  49. package/dist/types/commands.d.ts +24 -0
  50. package/dist/types/commands.d.ts.map +1 -0
  51. package/dist/types/commands.js +5 -0
  52. package/dist/types/config.d.ts +13 -0
  53. package/dist/types/config.d.ts.map +1 -0
  54. package/dist/types/config.js +5 -0
  55. package/dist/types/core.d.ts +38 -0
  56. package/dist/types/core.d.ts.map +1 -0
  57. package/dist/{types.js → types/core.js} +4 -13
  58. package/dist/{hooks/types.d.ts → types/hooks.d.ts} +2 -1
  59. package/dist/types/hooks.d.ts.map +1 -0
  60. package/dist/types/index.d.ts +20 -0
  61. package/dist/types/index.d.ts.map +1 -0
  62. package/dist/types/index.js +21 -0
  63. package/dist/types/mcp.d.ts +28 -0
  64. package/dist/types/mcp.d.ts.map +1 -0
  65. package/dist/types/mcp.js +5 -0
  66. package/dist/types/messaging.d.ts +80 -0
  67. package/dist/types/messaging.d.ts.map +1 -0
  68. package/dist/types/messaging.js +5 -0
  69. package/dist/types/processes.d.ts +17 -0
  70. package/dist/types/processes.d.ts.map +1 -0
  71. package/dist/types/processes.js +5 -0
  72. package/dist/types/skills.d.ts +78 -0
  73. package/dist/types/skills.d.ts.map +1 -0
  74. package/dist/types/skills.js +17 -0
  75. package/dist/utils/configResolver.d.ts +1 -1
  76. package/dist/utils/configResolver.d.ts.map +1 -1
  77. package/dist/utils/configResolver.js +1 -1
  78. package/dist/utils/configValidator.d.ts +1 -1
  79. package/dist/utils/configValidator.d.ts.map +1 -1
  80. package/dist/utils/configValidator.js +1 -1
  81. package/dist/utils/convertMessagesForAPI.d.ts +1 -1
  82. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  83. package/dist/utils/customCommands.d.ts +1 -1
  84. package/dist/utils/customCommands.d.ts.map +1 -1
  85. package/dist/{hooks/matcher.d.ts → utils/hookMatcher.d.ts} +1 -1
  86. package/dist/utils/hookMatcher.d.ts.map +1 -0
  87. package/dist/utils/markdownParser.d.ts +1 -1
  88. package/dist/utils/markdownParser.d.ts.map +1 -1
  89. package/dist/utils/mcpUtils.d.ts +1 -1
  90. package/dist/utils/mcpUtils.d.ts.map +1 -1
  91. package/dist/utils/messageOperations.d.ts +7 -2
  92. package/dist/utils/messageOperations.d.ts.map +1 -1
  93. package/dist/utils/messageOperations.js +18 -1
  94. package/dist/utils/skillParser.d.ts +1 -1
  95. package/dist/utils/skillParser.d.ts.map +1 -1
  96. package/package.json +1 -1
  97. package/src/agent.ts +150 -50
  98. package/src/index.ts +3 -4
  99. package/src/managers/aiManager.ts +282 -164
  100. package/src/managers/backgroundBashManager.ts +1 -1
  101. package/src/{hooks/manager.ts → managers/hookManager.ts} +163 -28
  102. package/src/managers/mcpManager.ts +6 -6
  103. package/src/managers/messageManager.ts +69 -10
  104. package/src/managers/skillManager.ts +4 -4
  105. package/src/managers/slashCommandManager.ts +6 -2
  106. package/src/managers/subagentManager.ts +58 -53
  107. package/src/managers/toolManager.ts +1 -1
  108. package/src/services/aiService.ts +37 -7
  109. package/src/services/hook.ts +360 -0
  110. package/src/services/memory.ts +3 -3
  111. package/src/services/session.ts +99 -33
  112. package/src/tools/bashTool.ts +2 -2
  113. package/src/tools/deleteFileTool.ts +1 -1
  114. package/src/tools/editTool.ts +1 -1
  115. package/src/tools/multiEditTool.ts +2 -2
  116. package/src/tools/taskTool.ts +13 -5
  117. package/src/tools/writeTool.ts +1 -1
  118. package/src/types/commands.ts +26 -0
  119. package/src/types/config.ts +14 -0
  120. package/src/types/core.ts +49 -0
  121. package/src/{hooks/types.ts → types/hooks.ts} +1 -0
  122. package/src/types/index.ts +23 -0
  123. package/src/{types.ts → types/index.ts.backup} +13 -0
  124. package/src/types/mcp.ts +31 -0
  125. package/src/types/messaging.ts +103 -0
  126. package/src/types/processes.ts +18 -0
  127. package/src/types/skills.ts +91 -0
  128. package/src/utils/configResolver.ts +1 -1
  129. package/src/utils/configValidator.ts +5 -1
  130. package/src/utils/convertMessagesForAPI.ts +1 -1
  131. package/src/utils/customCommands.ts +1 -1
  132. package/src/utils/markdownParser.ts +1 -1
  133. package/src/utils/mcpUtils.ts +1 -1
  134. package/src/utils/messageOperations.ts +22 -1
  135. package/src/utils/skillParser.ts +1 -1
  136. package/dist/hooks/executor.d.ts +0 -56
  137. package/dist/hooks/executor.d.ts.map +0 -1
  138. package/dist/hooks/executor.js +0 -312
  139. package/dist/hooks/index.d.ts +0 -17
  140. package/dist/hooks/index.d.ts.map +0 -1
  141. package/dist/hooks/index.js +0 -14
  142. package/dist/hooks/manager.d.ts.map +0 -1
  143. package/dist/hooks/matcher.d.ts.map +0 -1
  144. package/dist/hooks/settings.d.ts +0 -46
  145. package/dist/hooks/settings.d.ts.map +0 -1
  146. package/dist/hooks/settings.js +0 -100
  147. package/dist/hooks/types.d.ts.map +0 -1
  148. package/dist/types.d.ts +0 -276
  149. package/dist/types.d.ts.map +0 -1
  150. package/src/hooks/executor.ts +0 -440
  151. package/src/hooks/index.ts +0 -52
  152. package/src/hooks/settings.ts +0 -129
  153. /package/dist/{hooks/types.js → types/hooks.js} +0 -0
  154. /package/dist/{hooks/matcher.js → utils/hookMatcher.js} +0 -0
  155. /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 { Message, Logger, GatewayConfig, ModelConfig } from "../types.js";
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
- taskDescription: string,
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
- await instance.aiManager.sendAIMessage({
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<string> {
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
- return (
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 "Failed to compress conversation history";
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
+ }
@@ -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.info(`Memory added to ${memoryFilePath}:`, message);
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.info(`Created user memory file: ${USER_MEMORY_FILE}`);
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.info(`User memory added to ${USER_MEMORY_FILE}:`, message);
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}`);