wave-agent-sdk 0.8.1 → 0.8.3

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 (55) hide show
  1. package/dist/managers/hookManager.d.ts.map +1 -1
  2. package/dist/managers/hookManager.js +0 -21
  3. package/dist/managers/liveConfigManager.d.ts.map +1 -1
  4. package/dist/managers/liveConfigManager.js +0 -36
  5. package/dist/managers/messageManager.d.ts.map +1 -1
  6. package/dist/managers/messageManager.js +2 -1
  7. package/dist/managers/permissionManager.d.ts.map +1 -1
  8. package/dist/managers/permissionManager.js +47 -29
  9. package/dist/managers/pluginManager.d.ts.map +1 -1
  10. package/dist/managers/pluginManager.js +28 -1
  11. package/dist/managers/skillManager.d.ts.map +1 -1
  12. package/dist/managers/skillManager.js +8 -2
  13. package/dist/services/aiService.d.ts.map +1 -1
  14. package/dist/services/aiService.js +2 -0
  15. package/dist/services/fileWatcher.d.ts.map +1 -1
  16. package/dist/services/fileWatcher.js +0 -4
  17. package/dist/services/initializationService.d.ts.map +1 -1
  18. package/dist/services/initializationService.js +2 -10
  19. package/dist/services/pluginLoader.d.ts.map +1 -1
  20. package/dist/services/pluginLoader.js +1 -3
  21. package/dist/services/taskManager.d.ts +2 -0
  22. package/dist/services/taskManager.d.ts.map +1 -1
  23. package/dist/services/taskManager.js +48 -0
  24. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  25. package/dist/tools/taskManagementTools.js +58 -0
  26. package/dist/tools/taskTool.d.ts.map +1 -1
  27. package/dist/tools/taskTool.js +60 -50
  28. package/dist/utils/bashParser.d.ts +4 -0
  29. package/dist/utils/bashParser.d.ts.map +1 -1
  30. package/dist/utils/bashParser.js +39 -2
  31. package/dist/utils/containerSetup.d.ts.map +1 -1
  32. package/dist/utils/containerSetup.js +3 -0
  33. package/dist/utils/messageOperations.d.ts +1 -0
  34. package/dist/utils/messageOperations.d.ts.map +1 -1
  35. package/dist/utils/messageOperations.js +6 -2
  36. package/dist/utils/openaiClient.d.ts.map +1 -1
  37. package/dist/utils/openaiClient.js +3 -1
  38. package/package.json +2 -5
  39. package/src/managers/hookManager.ts +0 -52
  40. package/src/managers/liveConfigManager.ts +0 -75
  41. package/src/managers/messageManager.ts +2 -0
  42. package/src/managers/permissionManager.ts +60 -37
  43. package/src/managers/pluginManager.ts +39 -1
  44. package/src/managers/skillManager.ts +8 -2
  45. package/src/services/aiService.ts +2 -0
  46. package/src/services/fileWatcher.ts +0 -8
  47. package/src/services/initializationService.ts +2 -19
  48. package/src/services/pluginLoader.ts +1 -3
  49. package/src/services/taskManager.ts +51 -0
  50. package/src/tools/taskManagementTools.ts +77 -0
  51. package/src/tools/taskTool.ts +70 -61
  52. package/src/utils/bashParser.ts +50 -2
  53. package/src/utils/containerSetup.ts +3 -0
  54. package/src/utils/messageOperations.ts +7 -2
  55. package/src/utils/openaiClient.ts +3 -1
@@ -121,7 +121,6 @@ export class InitializationService {
121
121
  // Initialize hooks configuration
122
122
  try {
123
123
  // Load hooks configuration using ConfigurationService
124
- logger?.debug("Loading hooks configuration...");
125
124
  const configResult =
126
125
  await configurationService.loadMergedConfiguration(workdir);
127
126
 
@@ -156,8 +155,6 @@ export class InitializationService {
156
155
  }
157
156
  }
158
157
  }
159
-
160
- logger?.debug("Hooks system initialized successfully");
161
158
  } catch (error) {
162
159
  logger?.error("Failed to initialize hooks system:", error);
163
160
  // Don't throw error to prevent app startup failure
@@ -206,9 +203,7 @@ export class InitializationService {
206
203
 
207
204
  // Initialize live configuration reload
208
205
  try {
209
- logger?.debug("Initializing live configuration reload...");
210
206
  await liveConfigManager.initialize();
211
- logger?.debug("Live configuration reload initialized successfully");
212
207
  } catch (error) {
213
208
  logger?.error("Failed to initialize live configuration reload:", error);
214
209
  // Don't throw error to prevent app startup failure - continue without live reload
@@ -216,8 +211,6 @@ export class InitializationService {
216
211
 
217
212
  // Load memory files during initialization
218
213
  try {
219
- logger?.debug("Loading memory files...");
220
-
221
214
  // Load project memory from AGENTS.md (bypass memory store for direct file access)
222
215
  try {
223
216
  const projectMemoryPath = path.join(workdir, "AGENTS.md");
@@ -226,13 +219,9 @@ export class InitializationService {
226
219
  "utf-8",
227
220
  );
228
221
  setProjectMemory(projectMemoryContent);
229
- logger?.debug("Project memory loaded successfully");
230
222
  } catch (error) {
223
+ logger?.warn("Failed to load project memory file:", error);
231
224
  setProjectMemory("");
232
- logger?.debug(
233
- "Project memory file not found or unreadable, using empty content:",
234
- error instanceof Error ? error.message : String(error),
235
- );
236
225
  }
237
226
 
238
227
  // Load user memory (bypass memory store for direct file access)
@@ -240,16 +229,10 @@ export class InitializationService {
240
229
  const userMemoryPath = path.join(os.homedir(), ".wave", "AGENTS.md");
241
230
  const userMemoryContent = await fs.readFile(userMemoryPath, "utf-8");
242
231
  setUserMemory(userMemoryContent);
243
- logger?.debug("User memory loaded successfully");
244
232
  } catch (error) {
233
+ logger?.warn("Failed to load user memory file:", error);
245
234
  setUserMemory("");
246
- logger?.debug(
247
- "User memory file not found or unreadable, using empty content:",
248
- error instanceof Error ? error.message : String(error),
249
- );
250
235
  }
251
-
252
- logger?.debug("Memory initialization completed");
253
236
  } catch (error) {
254
237
  // Ensure memory is always initialized even if loading fails
255
238
  setProjectMemory("");
@@ -98,10 +98,8 @@ export class PluginLoader {
98
98
  });
99
99
  if (parsed.isValid) {
100
100
  skills.push({
101
- name: parsed.skillMetadata.name,
102
- description: parsed.skillMetadata.description,
101
+ ...parsed.skillMetadata,
103
102
  type: "project", // Plugin skills are treated as project skills
104
- skillPath: parsed.skillMetadata.skillPath,
105
103
  content: parsed.content,
106
104
  frontmatter: parsed.frontmatter,
107
105
  isValid: parsed.isValid,
@@ -161,6 +161,57 @@ export class TaskManager extends EventEmitter {
161
161
  });
162
162
  }
163
163
 
164
+ async deleteTask(taskId: string): Promise<void> {
165
+ await this.withLock(async () => {
166
+ const taskPath = this.getTaskPath(taskId);
167
+ try {
168
+ await fs.unlink(taskPath);
169
+ this.emit("tasksChange", this.taskListId);
170
+ logger.debug(
171
+ `Task ${taskId} deleted from task list ${this.taskListId}`,
172
+ );
173
+ } catch (error) {
174
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
175
+ throw error;
176
+ }
177
+ }
178
+ });
179
+ }
180
+
181
+ async cleanupOldTaskLists(days: number = 30): Promise<void> {
182
+ const threshold = Date.now() - days * 24 * 60 * 60 * 1000;
183
+ try {
184
+ const dirs = await fs.readdir(this.baseDir);
185
+ for (const dir of dirs) {
186
+ if (dir === this.taskListId) continue;
187
+
188
+ const dirPath = join(this.baseDir, dir);
189
+ const stats = await fs.stat(dirPath);
190
+ if (!stats.isDirectory()) continue;
191
+
192
+ // Check mtime of the directory and its contents
193
+ let latestMtime = stats.mtimeMs;
194
+ const files = await fs.readdir(dirPath);
195
+ for (const file of files) {
196
+ const filePath = join(dirPath, file);
197
+ const fileStats = await fs.stat(filePath);
198
+ if (fileStats.mtimeMs > latestMtime) {
199
+ latestMtime = fileStats.mtimeMs;
200
+ }
201
+ }
202
+
203
+ if (latestMtime < threshold) {
204
+ logger.info(`Cleaning up old task list directory: ${dirPath}`);
205
+ await fs.rm(dirPath, { recursive: true, force: true });
206
+ }
207
+ }
208
+ } catch (error) {
209
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
210
+ logger.error("Failed to cleanup old task lists:", error);
211
+ }
212
+ }
213
+ }
214
+
164
215
  async listTasks(): Promise<Task[]> {
165
216
  const sessionDir = this.getSessionDir();
166
217
  try {
@@ -101,6 +101,15 @@ NOTE that you should not use this tool if there is only one trivial task to do.
101
101
  - Check TaskList first to avoid creating duplicate tasks`,
102
102
  execute: async (args, context: ToolContext): Promise<ToolResult> => {
103
103
  const taskManager = context.taskManager;
104
+
105
+ if (args.status === "deleted") {
106
+ return {
107
+ success: true,
108
+ content: `Task creation skipped because status was set to 'deleted'.`,
109
+ shortResult: `Skipped deleted task`,
110
+ };
111
+ }
112
+
104
113
  const task: Omit<Task, "id"> = {
105
114
  subject: args.subject as string,
106
115
  description: args.description as string,
@@ -334,6 +343,74 @@ Set up task dependencies:
334
343
  };
335
344
  }
336
345
 
346
+ if (args.status === "deleted") {
347
+ // Reciprocal Dependency Cleanup
348
+ // For each task in the deleted task's blocks list, remove the deleted task's ID from their blockedBy list.
349
+ for (const targetId of existingTask.blocks) {
350
+ const targetTask = await taskManager.getTask(targetId);
351
+ if (targetTask && targetTask.blockedBy.includes(taskId)) {
352
+ let targetSnapshotId: string | undefined;
353
+ if (context.reversionManager && context.messageId) {
354
+ const targetPath = taskManager.getTaskPath(targetId);
355
+ targetSnapshotId = await context.reversionManager.recordSnapshot(
356
+ context.messageId,
357
+ targetPath,
358
+ "modify",
359
+ );
360
+ }
361
+ await taskManager.updateTask({
362
+ ...targetTask,
363
+ blockedBy: targetTask.blockedBy.filter((id) => id !== taskId),
364
+ });
365
+ if (context.reversionManager && targetSnapshotId) {
366
+ await context.reversionManager.commitSnapshot(targetSnapshotId);
367
+ }
368
+ }
369
+ }
370
+
371
+ // For each task in the deleted task's blockedBy list, remove the deleted task's ID from their blocks list.
372
+ for (const targetId of existingTask.blockedBy) {
373
+ const targetTask = await taskManager.getTask(targetId);
374
+ if (targetTask && targetTask.blocks.includes(taskId)) {
375
+ let targetSnapshotId: string | undefined;
376
+ if (context.reversionManager && context.messageId) {
377
+ const targetPath = taskManager.getTaskPath(targetId);
378
+ targetSnapshotId = await context.reversionManager.recordSnapshot(
379
+ context.messageId,
380
+ targetPath,
381
+ "modify",
382
+ );
383
+ }
384
+ await taskManager.updateTask({
385
+ ...targetTask,
386
+ blocks: targetTask.blocks.filter((id) => id !== taskId),
387
+ });
388
+ if (context.reversionManager && targetSnapshotId) {
389
+ await context.reversionManager.commitSnapshot(targetSnapshotId);
390
+ }
391
+ }
392
+ }
393
+
394
+ // Record delete snapshot for the task itself
395
+ if (context.reversionManager && context.messageId) {
396
+ const taskPath = taskManager.getTaskPath(taskId);
397
+ const deleteSnapshotId = await context.reversionManager.recordSnapshot(
398
+ context.messageId,
399
+ taskPath,
400
+ "delete",
401
+ );
402
+ await context.reversionManager.commitSnapshot(deleteSnapshotId);
403
+ }
404
+
405
+ await taskManager.deleteTask(taskId);
406
+
407
+ return {
408
+ success: true,
409
+ content: `Task #${taskId} deleted and removed from disk.`,
410
+ shortResult: `Deleted task ${taskId}`,
411
+ };
412
+ }
413
+
337
414
  let snapshotId: string | undefined;
338
415
  if (context.reversionManager && context.messageId) {
339
416
  const taskPath = taskManager.getTaskPath(taskId);
@@ -162,68 +162,77 @@ ${subagentList || "No subagents configured"}
162
162
  },
163
163
  );
164
164
 
165
- // Register for backgrounding if not already in background
166
- if (!run_in_background && context.foregroundTaskManager) {
167
- context.foregroundTaskManager.registerForegroundTask({
168
- id: instance.subagentId,
169
- backgroundHandler: async () => {
170
- isBackgrounded = true;
171
- await subagentManager.backgroundInstance(instance.subagentId);
172
- },
173
- });
174
- }
175
-
176
- try {
177
- const result = await subagentManager.executeTask(
178
- instance,
179
- prompt,
180
- context.abortSignal,
181
- run_in_background,
182
- );
183
-
184
- if (isBackgrounded) {
185
- // If it was backgrounded during execution, the backgroundHandler already returned/will return
186
- // But wait, the backgroundHandler is async and returns a ToolResult.
187
- // In the current ToolManager/AIManager implementation, the backgroundHandler's return value
188
- // is what's used when backgrounding happens.
189
- // However, executeTask might still be running.
190
- // We should return a special value or just let it be.
191
- return {
192
- success: true,
193
- content: "Task backgrounded",
194
- shortResult: "Task backgrounded",
195
- isManuallyBackgrounded: true,
196
- };
197
- }
198
-
199
- if (run_in_background) {
200
- return {
201
- success: true,
202
- content: `Task started in background with ID: ${result}`,
203
- shortResult: `Task started in background: ${result}`,
204
- };
205
- }
206
-
207
- // Cleanup subagent instance after task completion
208
- subagentManager.cleanupInstance(instance.subagentId);
209
-
210
- const messages = instance.messageManager.getMessages();
211
- const tokens = instance.messageManager.getlatestTotalTokens();
212
- const toolCount = countToolBlocks(messages);
213
- const summary = formatToolTokenSummary(toolCount, tokens);
165
+ return new Promise<ToolResult>((resolve) => {
166
+ (async () => {
167
+ // Register for backgrounding if not already in background
168
+ if (!run_in_background && context.foregroundTaskManager) {
169
+ context.foregroundTaskManager.registerForegroundTask({
170
+ id: instance.subagentId,
171
+ backgroundHandler: async () => {
172
+ isBackgrounded = true;
173
+ await subagentManager.backgroundInstance(instance.subagentId);
174
+ resolve({
175
+ success: true,
176
+ content: "Task backgrounded",
177
+ shortResult: "Task backgrounded",
178
+ isManuallyBackgrounded: true,
179
+ });
180
+ },
181
+ });
182
+ }
214
183
 
215
- return {
216
- success: true,
217
- content: result,
218
- shortResult: `Task completed${summary ? ` ${summary}` : ""}`,
219
- };
220
- } finally {
221
- if (!run_in_background && context.foregroundTaskManager) {
222
- context.foregroundTaskManager.unregisterForegroundTask(
223
- instance.subagentId,
224
- );
225
- }
226
- }
184
+ try {
185
+ const result = await subagentManager.executeTask(
186
+ instance,
187
+ prompt,
188
+ context.abortSignal,
189
+ run_in_background,
190
+ );
191
+
192
+ if (isBackgrounded) {
193
+ return;
194
+ }
195
+
196
+ if (run_in_background) {
197
+ resolve({
198
+ success: true,
199
+ content: `Task started in background with ID: ${result}`,
200
+ shortResult: `Task started in background: ${result}`,
201
+ });
202
+ return;
203
+ }
204
+
205
+ // Cleanup subagent instance after task completion
206
+ subagentManager.cleanupInstance(instance.subagentId);
207
+
208
+ const messages = instance.messageManager.getMessages();
209
+ const tokens = instance.messageManager.getlatestTotalTokens();
210
+ const toolCount = countToolBlocks(messages);
211
+ const summary = formatToolTokenSummary(toolCount, tokens);
212
+
213
+ resolve({
214
+ success: true,
215
+ content: result,
216
+ shortResult: `Task completed${summary ? ` ${summary}` : ""}`,
217
+ });
218
+ } catch (error) {
219
+ if (!isBackgrounded) {
220
+ resolve({
221
+ success: false,
222
+ content: "",
223
+ error: `Task delegation failed: ${error instanceof Error ? error.message : String(error)}`,
224
+ shortResult: "Delegation error",
225
+ });
226
+ }
227
+ } finally {
228
+ if (!run_in_background && context.foregroundTaskManager) {
229
+ context.foregroundTaskManager.unregisterForegroundTask(
230
+ instance.subagentId,
231
+ );
232
+ }
233
+ }
234
+ })();
235
+ });
227
236
  } catch (error) {
228
237
  return {
229
238
  success: false,
@@ -100,8 +100,13 @@ export function splitBashCommand(command: string): string[] {
100
100
 
101
101
  const finalResult: string[] = [];
102
102
  for (const part of parts) {
103
- const stripped = stripRedirections(stripEnvVars(part));
104
- if (stripped.startsWith("(") && stripped.endsWith(")")) {
103
+ const envStripped = stripEnvVars(part);
104
+ const stripped = stripRedirections(envStripped);
105
+ if (
106
+ stripped.startsWith("(") &&
107
+ stripped.endsWith(")") &&
108
+ stripped === envStripped
109
+ ) {
105
110
  const inner = stripped.substring(1, stripped.length - 1).trim();
106
111
  if (inner) {
107
112
  finalResult.push(...splitBashCommand(inner));
@@ -286,6 +291,49 @@ export function stripRedirections(command: string): string {
286
291
  return result.trim();
287
292
  }
288
293
 
294
+ /**
295
+ * Checks if a bash command contains any write redirections (>, >>, &>, 2>, >|).
296
+ */
297
+ export function hasWriteRedirections(command: string): boolean {
298
+ let inSingleQuote = false;
299
+ let inDoubleQuote = false;
300
+ let escaped = false;
301
+
302
+ for (let i = 0; i < command.length; i++) {
303
+ const char = command[i];
304
+
305
+ if (escaped) {
306
+ escaped = false;
307
+ continue;
308
+ }
309
+
310
+ if (char === "\\") {
311
+ escaped = true;
312
+ continue;
313
+ }
314
+
315
+ if (char === "'" && !inDoubleQuote) {
316
+ inSingleQuote = !inSingleQuote;
317
+ continue;
318
+ }
319
+
320
+ if (char === '"' && !inSingleQuote) {
321
+ inDoubleQuote = !inDoubleQuote;
322
+ continue;
323
+ }
324
+
325
+ if (inSingleQuote || inDoubleQuote) {
326
+ continue;
327
+ }
328
+
329
+ if (char === ">") {
330
+ return true;
331
+ }
332
+ }
333
+
334
+ return false;
335
+ }
336
+
289
337
  /**
290
338
  * Blacklist of dangerous commands that should not be safely prefix-matched
291
339
  * and should not have persistent permissions.
@@ -116,6 +116,9 @@ export function setupAgentContainer(
116
116
  const tasks = await taskManager.listTasks();
117
117
  onTasksChange(tasks);
118
118
  });
119
+ taskManager.cleanupOldTaskLists(30).catch((error) => {
120
+ logger.error("Failed to cleanup old task lists:", error);
121
+ });
119
122
 
120
123
  const backgroundTaskManager = new BackgroundTaskManager(container, {
121
124
  callbacks: {
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "crypto";
1
2
  import type { Message, Usage } from "../types/index.js";
2
3
  import { MessageSource } from "../types/index.js";
3
4
  import { readFileSync } from "fs";
@@ -113,6 +114,8 @@ export const convertImageToBase64 = (imagePath: string): string => {
113
114
  }
114
115
  };
115
116
 
117
+ export const generateMessageId = (): string => `msg-${randomUUID()}`;
118
+
116
119
  // Add user message
117
120
  export const addUserMessageToMessages = ({
118
121
  messages,
@@ -142,7 +145,7 @@ export const addUserMessageToMessages = ({
142
145
  }
143
146
 
144
147
  const userMessage: Message = {
145
- id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
148
+ id: generateMessageId(),
146
149
  role: "user",
147
150
  blocks,
148
151
  };
@@ -179,7 +182,7 @@ export const addAssistantMessageToMessages = (
179
182
  }
180
183
 
181
184
  const initialAssistantMessage: Message = {
182
- id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
185
+ id: generateMessageId(),
183
186
  role: "assistant",
184
187
  blocks,
185
188
  usage, // Include usage data if provided
@@ -283,6 +286,7 @@ export const addErrorBlockToMessage = ({
283
286
  } else {
284
287
  // If the last message is not an assistant message, create a new assistant message
285
288
  newMessages.push({
289
+ id: generateMessageId(),
286
290
  role: "assistant",
287
291
  blocks: [
288
292
  {
@@ -302,6 +306,7 @@ export const addBangMessage = ({
302
306
  command,
303
307
  }: AddBangParams): Message[] => {
304
308
  const outputMessage: Message = {
309
+ id: generateMessageId(),
305
310
  role: "user",
306
311
  blocks: [
307
312
  {
@@ -52,7 +52,9 @@ export class OpenAIClient {
52
52
  >;
53
53
  // Prevent unhandled rejection if only withResponse() is used
54
54
  promise.catch((e) => {
55
- logger.error("Unhandled OpenAI promise rejection:", e);
55
+ if (!(e instanceof Error && e.name === "AbortError")) {
56
+ logger.error("Unhandled OpenAI promise rejection:", e);
57
+ }
56
58
  });
57
59
  return promise;
58
60
  },