wave-agent-sdk 0.2.1 → 0.5.0

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 (194) hide show
  1. package/dist/agent.d.ts +66 -20
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +156 -83
  4. package/dist/constants/prompts.d.ts +7 -2
  5. package/dist/constants/prompts.d.ts.map +1 -1
  6. package/dist/constants/prompts.js +41 -5
  7. package/dist/constants/tools.d.ts +2 -2
  8. package/dist/constants/tools.js +2 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
  13. package/dist/managers/MemoryRuleManager.js +16 -2
  14. package/dist/managers/aiManager.d.ts +14 -4
  15. package/dist/managers/aiManager.d.ts.map +1 -1
  16. package/dist/managers/aiManager.js +61 -9
  17. package/dist/managers/backgroundBashManager.d.ts.map +1 -1
  18. package/dist/managers/backgroundBashManager.js +1 -0
  19. package/dist/managers/backgroundTaskManager.d.ts +35 -0
  20. package/dist/managers/backgroundTaskManager.d.ts.map +1 -0
  21. package/dist/managers/backgroundTaskManager.js +249 -0
  22. package/dist/managers/bashManager.d.ts.map +1 -1
  23. package/dist/managers/bashManager.js +0 -3
  24. package/dist/managers/foregroundTaskManager.d.ts +9 -0
  25. package/dist/managers/foregroundTaskManager.d.ts.map +1 -0
  26. package/dist/managers/foregroundTaskManager.js +20 -0
  27. package/dist/managers/liveConfigManager.d.ts +1 -1
  28. package/dist/managers/liveConfigManager.d.ts.map +1 -1
  29. package/dist/managers/lspManager.d.ts.map +1 -1
  30. package/dist/managers/lspManager.js +3 -1
  31. package/dist/managers/messageManager.d.ts +34 -4
  32. package/dist/managers/messageManager.d.ts.map +1 -1
  33. package/dist/managers/messageManager.js +104 -13
  34. package/dist/managers/permissionManager.d.ts.map +1 -1
  35. package/dist/managers/permissionManager.js +11 -13
  36. package/dist/managers/pluginManager.d.ts.map +1 -1
  37. package/dist/managers/pluginManager.js +3 -2
  38. package/dist/managers/pluginScopeManager.d.ts +13 -2
  39. package/dist/managers/pluginScopeManager.d.ts.map +1 -1
  40. package/dist/managers/pluginScopeManager.js +38 -0
  41. package/dist/managers/reversionManager.d.ts +39 -0
  42. package/dist/managers/reversionManager.d.ts.map +1 -0
  43. package/dist/managers/reversionManager.js +118 -0
  44. package/dist/managers/slashCommandManager.d.ts +4 -1
  45. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  46. package/dist/managers/slashCommandManager.js +16 -6
  47. package/dist/managers/subagentManager.d.ts +13 -2
  48. package/dist/managers/subagentManager.d.ts.map +1 -1
  49. package/dist/managers/subagentManager.js +144 -35
  50. package/dist/managers/toolManager.d.ts +11 -1
  51. package/dist/managers/toolManager.d.ts.map +1 -1
  52. package/dist/managers/toolManager.js +11 -3
  53. package/dist/services/GitService.d.ts.map +1 -1
  54. package/dist/services/GitService.js +6 -2
  55. package/dist/services/MarketplaceService.d.ts +14 -1
  56. package/dist/services/MarketplaceService.d.ts.map +1 -1
  57. package/dist/services/MarketplaceService.js +72 -4
  58. package/dist/services/MemoryRuleService.d.ts +1 -1
  59. package/dist/services/MemoryRuleService.d.ts.map +1 -1
  60. package/dist/services/MemoryRuleService.js +13 -2
  61. package/dist/services/aiService.js +1 -1
  62. package/dist/services/configurationService.d.ts +18 -2
  63. package/dist/services/configurationService.d.ts.map +1 -1
  64. package/dist/services/configurationService.js +62 -0
  65. package/dist/services/fileWatcher.d.ts +0 -5
  66. package/dist/services/fileWatcher.d.ts.map +1 -1
  67. package/dist/services/fileWatcher.js +0 -11
  68. package/dist/services/memory.js +1 -1
  69. package/dist/services/pluginLoader.d.ts.map +1 -1
  70. package/dist/services/pluginLoader.js +6 -1
  71. package/dist/services/reversionService.d.ts +24 -0
  72. package/dist/services/reversionService.d.ts.map +1 -0
  73. package/dist/services/reversionService.js +76 -0
  74. package/dist/services/session.d.ts +7 -0
  75. package/dist/services/session.d.ts.map +1 -1
  76. package/dist/services/session.js +126 -3
  77. package/dist/tools/bashTool.d.ts +0 -8
  78. package/dist/tools/bashTool.d.ts.map +1 -1
  79. package/dist/tools/bashTool.js +52 -174
  80. package/dist/tools/deleteFileTool.d.ts.map +1 -1
  81. package/dist/tools/deleteFileTool.js +9 -0
  82. package/dist/tools/editTool.d.ts.map +1 -1
  83. package/dist/tools/editTool.js +15 -4
  84. package/dist/tools/multiEditTool.d.ts.map +1 -1
  85. package/dist/tools/multiEditTool.js +16 -5
  86. package/dist/tools/taskOutputTool.d.ts +3 -0
  87. package/dist/tools/taskOutputTool.d.ts.map +1 -0
  88. package/dist/tools/taskOutputTool.js +149 -0
  89. package/dist/tools/taskStopTool.d.ts +3 -0
  90. package/dist/tools/taskStopTool.d.ts.map +1 -0
  91. package/dist/tools/taskStopTool.js +65 -0
  92. package/dist/tools/taskTool.d.ts.map +1 -1
  93. package/dist/tools/taskTool.js +105 -63
  94. package/dist/tools/types.d.ts +7 -0
  95. package/dist/tools/types.d.ts.map +1 -1
  96. package/dist/tools/writeTool.d.ts.map +1 -1
  97. package/dist/tools/writeTool.js +9 -0
  98. package/dist/types/commands.d.ts +1 -0
  99. package/dist/types/commands.d.ts.map +1 -1
  100. package/dist/types/configuration.d.ts +3 -0
  101. package/dist/types/configuration.d.ts.map +1 -1
  102. package/dist/types/environment.d.ts +2 -1
  103. package/dist/types/environment.d.ts.map +1 -1
  104. package/dist/types/environment.js +0 -6
  105. package/dist/types/history.d.ts +5 -0
  106. package/dist/types/history.d.ts.map +1 -0
  107. package/dist/types/history.js +1 -0
  108. package/dist/types/index.d.ts +1 -0
  109. package/dist/types/index.d.ts.map +1 -1
  110. package/dist/types/index.js +1 -0
  111. package/dist/types/marketplace.d.ts +4 -0
  112. package/dist/types/marketplace.d.ts.map +1 -1
  113. package/dist/types/messaging.d.ts +7 -1
  114. package/dist/types/messaging.d.ts.map +1 -1
  115. package/dist/types/processes.d.ts +24 -4
  116. package/dist/types/processes.d.ts.map +1 -1
  117. package/dist/types/reversion.d.ts +29 -0
  118. package/dist/types/reversion.d.ts.map +1 -0
  119. package/dist/types/reversion.js +1 -0
  120. package/dist/utils/builtinSubagents.d.ts.map +1 -1
  121. package/dist/utils/builtinSubagents.js +16 -0
  122. package/dist/utils/constants.d.ts +2 -2
  123. package/dist/utils/constants.d.ts.map +1 -1
  124. package/dist/utils/constants.js +2 -2
  125. package/dist/utils/editUtils.d.ts +4 -9
  126. package/dist/utils/editUtils.d.ts.map +1 -1
  127. package/dist/utils/editUtils.js +54 -55
  128. package/dist/utils/messageOperations.d.ts +3 -1
  129. package/dist/utils/messageOperations.d.ts.map +1 -1
  130. package/dist/utils/messageOperations.js +8 -1
  131. package/dist/utils/openaiClient.d.ts.map +1 -1
  132. package/dist/utils/openaiClient.js +56 -26
  133. package/dist/utils/promptHistory.d.ts +20 -0
  134. package/dist/utils/promptHistory.d.ts.map +1 -0
  135. package/dist/utils/promptHistory.js +117 -0
  136. package/package.json +5 -3
  137. package/src/agent.ts +193 -109
  138. package/src/constants/prompts.ts +45 -5
  139. package/src/constants/tools.ts +2 -2
  140. package/src/index.ts +1 -1
  141. package/src/managers/MemoryRuleManager.ts +18 -2
  142. package/src/managers/aiManager.ts +87 -18
  143. package/src/managers/backgroundBashManager.ts +1 -0
  144. package/src/managers/backgroundTaskManager.ts +306 -0
  145. package/src/managers/bashManager.ts +0 -4
  146. package/src/managers/foregroundTaskManager.ts +26 -0
  147. package/src/managers/liveConfigManager.ts +2 -1
  148. package/src/managers/lspManager.ts +3 -1
  149. package/src/managers/messageManager.ts +136 -18
  150. package/src/managers/permissionManager.ts +11 -13
  151. package/src/managers/pluginManager.ts +4 -3
  152. package/src/managers/pluginScopeManager.ts +57 -8
  153. package/src/managers/reversionManager.ts +152 -0
  154. package/src/managers/slashCommandManager.ts +30 -7
  155. package/src/managers/subagentManager.ts +176 -31
  156. package/src/managers/toolManager.ts +23 -4
  157. package/src/services/GitService.ts +6 -2
  158. package/src/services/MarketplaceService.ts +100 -4
  159. package/src/services/MemoryRuleService.ts +18 -6
  160. package/src/services/aiService.ts +1 -1
  161. package/src/services/configurationService.ts +79 -1
  162. package/src/services/fileWatcher.ts +0 -13
  163. package/src/services/memory.ts +1 -1
  164. package/src/services/pluginLoader.ts +7 -1
  165. package/src/services/reversionService.ts +94 -0
  166. package/src/services/session.ts +161 -3
  167. package/src/tools/bashTool.ts +73 -200
  168. package/src/tools/deleteFileTool.ts +15 -0
  169. package/src/tools/editTool.ts +20 -10
  170. package/src/tools/multiEditTool.ts +21 -11
  171. package/src/tools/taskOutputTool.ts +174 -0
  172. package/src/tools/taskStopTool.ts +72 -0
  173. package/src/tools/taskTool.ts +130 -74
  174. package/src/tools/types.ts +7 -0
  175. package/src/tools/writeTool.ts +14 -0
  176. package/src/types/commands.ts +3 -0
  177. package/src/types/configuration.ts +4 -0
  178. package/src/types/environment.ts +3 -1
  179. package/src/types/history.ts +4 -0
  180. package/src/types/index.ts +1 -0
  181. package/src/types/marketplace.ts +5 -0
  182. package/src/types/messaging.ts +9 -1
  183. package/src/types/processes.ts +33 -4
  184. package/src/types/reversion.ts +29 -0
  185. package/src/utils/builtinSubagents.ts +18 -0
  186. package/src/utils/constants.ts +2 -2
  187. package/src/utils/editUtils.ts +66 -58
  188. package/src/utils/messageOperations.ts +10 -0
  189. package/src/utils/openaiClient.ts +69 -35
  190. package/src/utils/promptHistory.ts +133 -0
  191. package/dist/utils/bashHistory.d.ts +0 -50
  192. package/dist/utils/bashHistory.d.ts.map +0 -1
  193. package/dist/utils/bashHistory.js +0 -256
  194. package/src/utils/bashHistory.ts +0 -320
@@ -4,7 +4,6 @@ import {
4
4
  addErrorBlockToMessage,
5
5
  addUserMessageToMessages,
6
6
  extractUserInputHistory,
7
- addMemoryBlockToMessage,
8
7
  addCommandOutputMessage,
9
8
  updateCommandOutputInMessage,
10
9
  completeCommandInMessage,
@@ -17,7 +16,7 @@ import {
17
16
  type AgentToolBlockUpdateParams,
18
17
  } from "../utils/messageOperations.js";
19
18
  import type { SubagentConfiguration } from "../utils/subagentParser.js";
20
- import type { Logger, Message, Usage } from "../types/index.js";
19
+ import type { Logger, Message, Usage, SlashCommand } from "../types/index.js";
21
20
  import { join } from "path";
22
21
  import {
23
22
  appendMessages,
@@ -27,6 +26,7 @@ import {
27
26
  SESSION_DIR,
28
27
  } from "../services/session.js";
29
28
  import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
29
+ import type { MemoryRuleManager } from "./MemoryRuleManager.js";
30
30
  import { pathEncoder } from "../utils/pathEncoder.js";
31
31
 
32
32
  export interface MessageManagerCallbacks {
@@ -57,6 +57,10 @@ export interface MessageManagerCallbacks {
57
57
  onAddCommandOutputMessage?: (command: string) => void;
58
58
  onUpdateCommandOutputMessage?: (command: string, output: string) => void;
59
59
  onCompleteCommandMessage?: (command: string, exitCode: number) => void;
60
+ onSlashCommandsChange?: (commands: SlashCommand[]) => void;
61
+ onInfoBlockAdded?: (content: string) => void;
62
+ // Rewind callbacks
63
+ onShowRewind?: () => void;
60
64
  // Subagent callbacks
61
65
  onSubAgentBlockAdded?: (
62
66
  subagentId: string,
@@ -78,6 +82,7 @@ export interface MessageManagerOptions {
78
82
  logger?: Logger;
79
83
  sessionType?: "main" | "subagent";
80
84
  subagentType?: string;
85
+ memoryRuleManager?: MemoryRuleManager;
81
86
  }
82
87
 
83
88
  export class MessageManager {
@@ -93,6 +98,7 @@ export class MessageManager {
93
98
  private transcriptPath: string; // Cached transcript path
94
99
  private savedMessageCount: number; // Track how many messages have been saved to prevent duplication
95
100
  private filesInContext: Set<string> = new Set(); // Track files mentioned in the conversation
101
+ private memoryRuleManager?: MemoryRuleManager;
96
102
  private sessionType: "main" | "subagent";
97
103
  private subagentType?: string;
98
104
 
@@ -108,6 +114,7 @@ export class MessageManager {
108
114
  this.savedMessageCount = 0; // Initialize saved message count tracker
109
115
  this.sessionType = options.sessionType || "main";
110
116
  this.subagentType = options.subagentType;
117
+ this.memoryRuleManager = options.memoryRuleManager;
111
118
 
112
119
  // Compute and cache the transcript path
113
120
  this.transcriptPath = this.computeTranscriptPath();
@@ -149,6 +156,30 @@ export class MessageManager {
149
156
  return this.transcriptPath;
150
157
  }
151
158
 
159
+ /**
160
+ * Get combined memory content (project memory + user memory + modular rules)
161
+ */
162
+ public async getCombinedMemory(): Promise<string> {
163
+ const memory = await import("../services/memory.js");
164
+ let combined = await memory.getCombinedMemoryContent(this.workdir);
165
+
166
+ if (this.memoryRuleManager) {
167
+ const filesInContext = this.getFilesInContext();
168
+ const activeRules = this.memoryRuleManager.getActiveRules(filesInContext);
169
+ if (activeRules.length > 0) {
170
+ this.logger?.debug(
171
+ `Active modular rules (${activeRules.length}): ${activeRules.map((r) => r.id).join(", ")}`,
172
+ );
173
+ if (combined) {
174
+ combined += "\n\n";
175
+ }
176
+ combined += activeRules.map((r) => r.content).join("\n\n");
177
+ }
178
+ }
179
+
180
+ return combined;
181
+ }
182
+
152
183
  /**
153
184
  * Compute the transcript path using cached encoded workdir
154
185
  * Called during construction and when sessionId changes
@@ -249,6 +280,20 @@ export class MessageManager {
249
280
  this.savedMessageCount = 0; // Reset saved message count
250
281
  }
251
282
 
283
+ /**
284
+ * Trigger the rewind UI callback
285
+ */
286
+ public triggerShowRewind(): void {
287
+ this.callbacks.onShowRewind?.();
288
+ }
289
+
290
+ /**
291
+ * Trigger slash commands change callback
292
+ */
293
+ public triggerSlashCommandsChange(commands: SlashCommand[]): void {
294
+ this.callbacks.onSlashCommandsChange?.(commands);
295
+ }
296
+
252
297
  // Initialize state from session data
253
298
  public initializeFromSession(sessionData: SessionData): void {
254
299
  this.setSessionId(sessionData.id);
@@ -384,6 +429,18 @@ export class MessageManager {
384
429
  this.callbacks.onErrorBlockAdded?.(error);
385
430
  }
386
431
 
432
+ public addInfoBlock(content: string): void {
433
+ const lastMessage = this.messages[this.messages.length - 1];
434
+ if (lastMessage && lastMessage.role === "assistant") {
435
+ lastMessage.blocks.push({
436
+ type: "info",
437
+ content,
438
+ } as unknown as import("../types/index.js").MessageBlock);
439
+ this.setMessages([...this.messages]);
440
+ this.callbacks.onInfoBlockAdded?.(content);
441
+ }
442
+ }
443
+
387
444
  /**
388
445
  * Compress messages and update session, delete compressed messages, only keep compressed messages and subsequent messages
389
446
  */
@@ -427,21 +484,19 @@ export class MessageManager {
427
484
  this.callbacks.onCompressBlockAdded?.(insertIndex, compressedContent);
428
485
  }
429
486
 
430
- public addMemoryBlock(
431
- content: string,
432
- success: boolean,
433
- type: "project" | "user",
434
- storagePath: string,
487
+ public addFileHistoryBlock(
488
+ snapshots: import("../types/reversion.js").FileSnapshot[],
435
489
  ): void {
436
- const newMessages = addMemoryBlockToMessage({
437
- messages: this.messages,
438
- content,
439
- isSuccess: success,
440
- memoryType: type,
441
- storagePath,
442
- });
443
- this.setMessages(newMessages);
444
- this.callbacks.onMemoryBlockAdded?.(content, success, type, storagePath);
490
+ if (snapshots.length === 0) return;
491
+
492
+ const lastMessage = this.messages[this.messages.length - 1];
493
+ if (lastMessage && lastMessage.role === "assistant") {
494
+ lastMessage.blocks.push({
495
+ type: "file_history",
496
+ snapshots,
497
+ } as unknown as import("../types/index.js").MessageBlock);
498
+ this.setMessages([...this.messages]);
499
+ }
445
500
  }
446
501
 
447
502
  // Bash command related message operations
@@ -480,12 +535,13 @@ export class MessageManager {
480
535
  subagentName: string,
481
536
  sessionId: string,
482
537
  configuration: SubagentConfiguration,
483
- status: "active" | "completed" | "error" = "active",
538
+ status: "active" | "completed" | "error" | "aborted" = "active",
484
539
  parameters: {
485
540
  description: string;
486
541
  prompt: string;
487
542
  subagent_type: string;
488
543
  },
544
+ runInBackground?: boolean,
489
545
  ): void {
490
546
  const params: AddSubagentBlockParams = {
491
547
  messages: this.messages,
@@ -494,6 +550,7 @@ export class MessageManager {
494
550
  sessionId,
495
551
  status,
496
552
  configuration,
553
+ runInBackground,
497
554
  };
498
555
  const updatedMessages = addSubagentBlockToMessage(params);
499
556
  this.setMessages(updatedMessages);
@@ -505,6 +562,7 @@ export class MessageManager {
505
562
  updates: Partial<{
506
563
  status: "active" | "completed" | "error" | "aborted";
507
564
  sessionId: string;
565
+ runInBackground: boolean;
508
566
  }>,
509
567
  ): void {
510
568
  const updatedMessages = updateSubagentBlockInMessage(
@@ -518,7 +576,9 @@ export class MessageManager {
518
576
  subagentId,
519
577
  status: updates.status || "active",
520
578
  };
521
- this.callbacks.onSubAgentBlockUpdated?.(params.subagentId, params.status);
579
+ if (updates.status) {
580
+ this.callbacks.onSubAgentBlockUpdated?.(params.subagentId, params.status);
581
+ }
522
582
  }
523
583
 
524
584
  /**
@@ -643,6 +703,64 @@ export class MessageManager {
643
703
  this.setMessages(newMessages);
644
704
  }
645
705
 
706
+ /**
707
+ * Truncate history to a specific index and revert file changes.
708
+ * @param index - The index of the user message to truncate to.
709
+ * @param reversionManager - Optional ReversionManager to handle file rollbacks.
710
+ */
711
+ public async truncateHistory(
712
+ index: number,
713
+ reversionManager?: import("./reversionManager.js").ReversionManager,
714
+ ): Promise<void> {
715
+ if (index < 0 || index >= this.messages.length) {
716
+ throw new Error(`Invalid message index: ${index}`);
717
+ }
718
+
719
+ // Identify messages to be removed
720
+ const messagesToRemove = this.messages.slice(index);
721
+ const messageIdsToRemove = messagesToRemove
722
+ .map((m) => m.id as string)
723
+ .filter((id) => !!id);
724
+
725
+ // Revert file changes if manager is provided
726
+ if (reversionManager && messageIdsToRemove.length > 0) {
727
+ await reversionManager.revertTo(messageIdsToRemove, this.messages);
728
+ }
729
+
730
+ // Truncate messages in memory
731
+ const newMessages = this.messages.slice(0, index);
732
+ this.setMessages(newMessages);
733
+
734
+ // Update persistence: rewrite the session file
735
+ await this.rewriteSessionFile(newMessages);
736
+
737
+ // Update saved message count
738
+ this.savedMessageCount = newMessages.length;
739
+ }
740
+
741
+ /**
742
+ * Rewrite the session file with the current messages.
743
+ */
744
+ private async rewriteSessionFile(messages: Message[]): Promise<void> {
745
+ try {
746
+ const { writeFile } = await import("fs/promises");
747
+
748
+ const sessionMessages: import("../types/session.js").SessionMessage[] =
749
+ messages.map((message) => ({
750
+ ...message,
751
+ timestamp: new Date().toISOString(),
752
+ }));
753
+
754
+ const content =
755
+ sessionMessages.map((m) => JSON.stringify(m)).join("\n") +
756
+ (sessionMessages.length > 0 ? "\n" : "");
757
+
758
+ await writeFile(this.transcriptPath, content, "utf8");
759
+ } catch (error) {
760
+ this.logger?.error("Failed to rewrite session file:", error);
761
+ }
762
+ }
763
+
646
764
  /**
647
765
  * Updates the set of files mentioned in the conversation.
648
766
  */
@@ -237,13 +237,7 @@ export class PermissionManager {
237
237
  * Get the current effective permission mode for tool execution context
238
238
  */
239
239
  getCurrentEffectiveMode(cliPermissionMode?: PermissionMode): PermissionMode {
240
- const mode = this.resolveEffectivePermissionMode(cliPermissionMode);
241
- this.logger?.debug("getCurrentEffectiveMode", {
242
- cliPermissionMode,
243
- configuredDefaultMode: this.configuredDefaultMode,
244
- resolvedMode: mode,
245
- });
246
- return mode;
240
+ return this.resolveEffectivePermissionMode(cliPermissionMode);
247
241
  }
248
242
 
249
243
  /**
@@ -558,7 +552,7 @@ export class PermissionManager {
558
552
  return true;
559
553
  }
560
554
 
561
- // 2. Tool with pattern match (e.g., "Bash(rm:*)", "Read(**/*.env)")
555
+ // 2. Tool with pattern match (e.g., "Bash(rm *)", "Read(**/*.env)")
562
556
  const match = rule.match(/^(\w+)\((.*)\)$/);
563
557
  if (!match) {
564
558
  return false;
@@ -573,10 +567,14 @@ export class PermissionManager {
573
567
  if (toolName === BASH_TOOL_NAME) {
574
568
  const command = String(context.toolInput?.command || "");
575
569
  const processedPart = stripRedirections(stripEnvVars(command));
576
- if (pattern.endsWith(":*")) {
577
- return processedPart.startsWith(pattern.slice(0, -2));
578
- }
579
- return processedPart === pattern;
570
+ // For Bash commands, we want '*' to match everything including slashes and spaces
571
+ // minimatch's default behavior for '*' is to not match across directory separators
572
+ // We use a regex to replace '*' with '.*' (match anything)
573
+ const regexPattern = pattern
574
+ .replace(/[.+^${}()|[\]\\?]/g, "\\$&") // Escape regex special chars including ?
575
+ .replace(/\*/g, ".*"); // Replace * with .*
576
+ const regex = new RegExp(`^${regexPattern}$`);
577
+ return regex.test(processedPart);
580
578
  }
581
579
 
582
580
  // Handle path-based rules (e.g., "Read(**/*.env)")
@@ -768,7 +766,7 @@ export class PermissionManager {
768
766
 
769
767
  const smartPrefix = getSmartPrefix(processedPart);
770
768
  if (smartPrefix) {
771
- rules.push(`Bash(${smartPrefix}:*)`);
769
+ rules.push(`Bash(${smartPrefix}*)`);
772
770
  } else {
773
771
  rules.push(`Bash(${processedPart})`);
774
772
  }
@@ -149,9 +149,7 @@ export class PluginManager {
149
149
  * @param configs Array of plugin configurations
150
150
  */
151
151
  async loadPlugins(configs: PluginConfig[]): Promise<void> {
152
- // Load installed plugins from marketplace first
153
- await this.loadInstalledPlugins();
154
-
152
+ // Load plugins from configuration (e.g. --plugin-dir) first to give them higher priority
155
153
  for (const config of configs) {
156
154
  if (config.type !== "local") {
157
155
  this.logger?.warn(`Unsupported plugin type: ${config.type}`);
@@ -164,6 +162,9 @@ export class PluginManager {
164
162
 
165
163
  await this.loadSinglePlugin(absolutePath);
166
164
  }
165
+
166
+ // Load installed plugins from marketplace
167
+ await this.loadInstalledPlugins();
167
168
  }
168
169
 
169
170
  /**
@@ -1,6 +1,7 @@
1
1
  import { ConfigurationService } from "../services/configurationService.js";
2
2
  import { PluginManager } from "./pluginManager.js";
3
3
  import { Logger } from "../types/index.js";
4
+ import { Scope } from "../types/configuration.js";
4
5
 
5
6
  export interface PluginScopeManagerOptions {
6
7
  workdir: string;
@@ -25,10 +26,7 @@ export class PluginScopeManager {
25
26
  /**
26
27
  * Enable a plugin in the specified scope
27
28
  */
28
- async enablePlugin(
29
- scope: "user" | "project" | "local",
30
- pluginId: string,
31
- ): Promise<void> {
29
+ async enablePlugin(scope: Scope, pluginId: string): Promise<void> {
32
30
  await this.configurationService.updateEnabledPlugin(
33
31
  this.workdir,
34
32
  scope,
@@ -42,10 +40,7 @@ export class PluginScopeManager {
42
40
  /**
43
41
  * Disable a plugin in the specified scope
44
42
  */
45
- async disablePlugin(
46
- scope: "user" | "project" | "local",
47
- pluginId: string,
48
- ): Promise<void> {
43
+ async disablePlugin(scope: Scope, pluginId: string): Promise<void> {
49
44
  await this.configurationService.updateEnabledPlugin(
50
45
  this.workdir,
51
46
  scope,
@@ -63,6 +58,60 @@ export class PluginScopeManager {
63
58
  return this.configurationService.getMergedEnabledPlugins(this.workdir);
64
59
  }
65
60
 
61
+ /**
62
+ * Find the scope where a plugin is currently enabled/disabled.
63
+ * Priority: local > project > user
64
+ */
65
+ findPluginScope(pluginId: string): Scope | null {
66
+ const projectPaths = this.configurationService.getConfigurationPaths(
67
+ this.workdir,
68
+ ).projectPaths; // [local, json]
69
+ const userPaths = this.configurationService.getConfigurationPaths(
70
+ this.workdir,
71
+ ).userPaths; // [local, json]
72
+
73
+ const checkPaths: { path: string; scope: Scope }[] = [
74
+ { path: projectPaths[0], scope: "local" },
75
+ { path: projectPaths[1], scope: "project" },
76
+ { path: userPaths[0], scope: "user" }, // user local is still user scope
77
+ { path: userPaths[1], scope: "user" },
78
+ ];
79
+
80
+ for (const { path, scope } of checkPaths) {
81
+ const config = this.configurationService.loadWaveConfigFromFile(path);
82
+ if (config?.enabledPlugins && pluginId in config.enabledPlugins) {
83
+ return scope;
84
+ }
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Remove a plugin from all scopes (user, project, local)
92
+ * This is useful when uninstalling a plugin to clean up all configuration
93
+ */
94
+ async removePluginFromAllScopes(pluginId: string): Promise<void> {
95
+ const scopes: Scope[] = ["user", "project", "local"];
96
+
97
+ for (const scope of scopes) {
98
+ try {
99
+ await this.configurationService.removeEnabledPlugin(
100
+ this.workdir,
101
+ scope,
102
+ pluginId,
103
+ );
104
+ } catch (error) {
105
+ // Continue removing from other scopes even if one fails
106
+ this.logger?.warn(
107
+ `Failed to remove plugin ${pluginId} from ${scope} scope: ${error instanceof Error ? error.message : String(error)}`,
108
+ );
109
+ }
110
+ }
111
+
112
+ this.refreshPluginManager();
113
+ }
114
+
66
115
  /**
67
116
  * Refresh the plugin manager with the latest configuration
68
117
  * Note: This only updates the configuration, it doesn't reload plugins.
@@ -0,0 +1,152 @@
1
+ import fs from "fs/promises";
2
+ import { FileSnapshot } from "../types/reversion.js";
3
+ import { ReversionService } from "../services/reversionService.js";
4
+
5
+ export class ReversionManager {
6
+ private buffer: Map<string, FileSnapshot> = new Map();
7
+ private reversionService: ReversionService;
8
+
9
+ constructor(reversionService: ReversionService) {
10
+ this.reversionService = reversionService;
11
+ }
12
+
13
+ /**
14
+ * Records the current state of a file into a temporary buffer.
15
+ * Returns a snapshotId.
16
+ */
17
+ async recordSnapshot(
18
+ messageId: string,
19
+ filePath: string,
20
+ operation: "create" | "modify" | "delete",
21
+ ): Promise<string> {
22
+ let content: string | null = null;
23
+ try {
24
+ content = await fs.readFile(filePath, "utf-8");
25
+ } catch {
26
+ // File doesn't exist, which is expected for 'create' operation
27
+ }
28
+
29
+ const snapshot: FileSnapshot = {
30
+ messageId,
31
+ filePath,
32
+ timestamp: Date.now(),
33
+ operation,
34
+ };
35
+
36
+ // We temporarily store the content in the buffer, it will be saved to disk on commit
37
+ const snapshotId = `${messageId}-${filePath}-${snapshot.timestamp}`;
38
+ this.buffer.set(snapshotId, { ...snapshot, content } as FileSnapshot & {
39
+ content: string | null;
40
+ });
41
+ return snapshotId;
42
+ }
43
+
44
+ /**
45
+ * Records the current state of a file into a temporary buffer.
46
+ * Returns a snapshotId.
47
+ */
48
+ async recordSnapshotWithId(
49
+ messageId: string,
50
+ filePath: string,
51
+ operation: "create" | "modify" | "delete",
52
+ ): Promise<string> {
53
+ return this.recordSnapshot(messageId, filePath, operation);
54
+ }
55
+
56
+ /**
57
+ * Moves the buffered snapshot to the permanent session log.
58
+ * Called only if the tool succeeds.
59
+ */
60
+ async commitSnapshot(snapshotId: string): Promise<void> {
61
+ const snapshotWithContent = this.buffer.get(snapshotId) as FileSnapshot & {
62
+ content: string | null;
63
+ };
64
+ if (snapshotWithContent) {
65
+ const { content, ...snapshot } = snapshotWithContent;
66
+ const snapshotPath = await this.reversionService.saveSnapshot({
67
+ ...snapshot,
68
+ content,
69
+ } as FileSnapshot);
70
+ snapshot.snapshotPath = snapshotPath;
71
+ this.buffer.delete(snapshotId);
72
+
73
+ // We need to return the snapshot so it can be added to the message block
74
+ // But the current API doesn't support it.
75
+ // Let's store committed snapshots in another buffer for the current turn.
76
+ this.committedSnapshots.push(snapshot);
77
+ }
78
+ }
79
+
80
+ private committedSnapshots: FileSnapshot[] = [];
81
+
82
+ /**
83
+ * Gets and clears committed snapshots for the current turn.
84
+ */
85
+ getAndClearCommittedSnapshots(): FileSnapshot[] {
86
+ const snapshots = [...this.committedSnapshots];
87
+ this.committedSnapshots = [];
88
+ return snapshots;
89
+ }
90
+
91
+ /**
92
+ * Discards the buffered snapshot.
93
+ * Called if the tool fails.
94
+ */
95
+ discardSnapshot(snapshotId: string): void {
96
+ this.buffer.delete(snapshotId);
97
+ }
98
+
99
+ /**
100
+ * Reverts all file changes associated with messages from the end of history
101
+ * down to (and including) the specified message index.
102
+ * This should be called by MessageManager.
103
+ */
104
+ async revertTo(
105
+ messageIds: string[],
106
+ allMessages: import("../types/index.js").Message[],
107
+ ): Promise<number> {
108
+ const messageIdSet = new Set(messageIds);
109
+ const snapshots: FileSnapshot[] = [];
110
+
111
+ for (const message of allMessages) {
112
+ if (message.id && messageIdSet.has(message.id)) {
113
+ const historyBlock = message.blocks.find(
114
+ (b) => b.type === "file_history",
115
+ ) as { type: "file_history"; snapshots: FileSnapshot[] } | undefined;
116
+ if (historyBlock && historyBlock.snapshots) {
117
+ snapshots.push(...historyBlock.snapshots);
118
+ }
119
+ }
120
+ }
121
+
122
+ // Revert in reverse chronological order (LIFO)
123
+ const sortedSnapshots = snapshots.sort((a, b) => b.timestamp - a.timestamp);
124
+
125
+ let revertedCount = 0;
126
+ for (const snapshot of sortedSnapshots) {
127
+ try {
128
+ if (!snapshot.snapshotPath) {
129
+ // File didn't exist before, so delete it
130
+ await fs.rm(snapshot.filePath, { force: true });
131
+ } else {
132
+ // Restore previous content
133
+ const content = await this.reversionService.readSnapshotContent(
134
+ snapshot.snapshotPath,
135
+ );
136
+ if (content !== null) {
137
+ await fs.writeFile(snapshot.filePath, content, "utf-8");
138
+ } else {
139
+ // If snapshotPath exists but content is null, it means the file should be deleted
140
+ // (This handles the case where snapshotPath was set but content was null in saveSnapshot)
141
+ await fs.rm(snapshot.filePath, { force: true });
142
+ }
143
+ }
144
+ revertedCount++;
145
+ } catch (error) {
146
+ console.error(`Failed to revert file ${snapshot.filePath}:`, error);
147
+ }
148
+ }
149
+
150
+ return revertedCount;
151
+ }
152
+ }
@@ -1,5 +1,6 @@
1
1
  import type { MessageManager } from "./messageManager.js";
2
2
  import type { AIManager } from "./aiManager.js";
3
+ import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
3
4
  import type {
4
5
  SlashCommand,
5
6
  CustomSlashCommand,
@@ -26,6 +27,7 @@ const execAsync = promisify(exec);
26
27
  export interface SlashCommandManagerOptions {
27
28
  messageManager: MessageManager;
28
29
  aiManager: AIManager;
30
+ backgroundTaskManager: BackgroundTaskManager;
29
31
  workdir: string;
30
32
  logger?: Logger;
31
33
  }
@@ -35,12 +37,14 @@ export class SlashCommandManager {
35
37
  private customCommands = new Map<string, CustomSlashCommand>();
36
38
  private messageManager: MessageManager;
37
39
  private aiManager: AIManager;
40
+ private backgroundTaskManager: BackgroundTaskManager;
38
41
  private workdir: string;
39
42
  private logger?: Logger;
40
43
 
41
44
  constructor(options: SlashCommandManagerOptions) {
42
45
  this.messageManager = options.messageManager;
43
46
  this.aiManager = options.aiManager;
47
+ this.backgroundTaskManager = options.backgroundTaskManager;
44
48
  this.workdir = options.workdir;
45
49
  this.logger = options.logger;
46
50
 
@@ -104,15 +108,24 @@ export class SlashCommandManager {
104
108
  handler: async (args?: string) => {
105
109
  // Substitute parameters in the command content
106
110
  let processedContent = command.content;
111
+
112
+ // Substitute $WAVE_PLUGIN_ROOT placeholder for plugin commands
113
+ if (command.pluginPath) {
114
+ processedContent = processedContent.replace(
115
+ /\$WAVE_PLUGIN_ROOT/g,
116
+ command.pluginPath,
117
+ );
118
+ }
119
+
107
120
  if (args) {
108
- if (hasParameterPlaceholders(command.content)) {
121
+ if (hasParameterPlaceholders(processedContent)) {
109
122
  processedContent = substituteCommandParameters(
110
- command.content,
123
+ processedContent,
111
124
  args,
112
125
  );
113
126
  } else {
114
127
  // If no placeholders, append arguments to the content
115
- processedContent = `${command.content.trim()} ${args}`;
128
+ processedContent = `${processedContent.trim()} ${args}`;
116
129
  }
117
130
  }
118
131
 
@@ -158,15 +171,24 @@ export class SlashCommandManager {
158
171
  handler: async (args?: string) => {
159
172
  // Substitute parameters in the command content
160
173
  let processedContent = command.content;
174
+
175
+ // Substitute $WAVE_PLUGIN_ROOT placeholder for plugin commands
176
+ if (command.pluginPath) {
177
+ processedContent = processedContent.replace(
178
+ /\$WAVE_PLUGIN_ROOT/g,
179
+ command.pluginPath,
180
+ );
181
+ }
182
+
161
183
  if (args) {
162
- if (hasParameterPlaceholders(command.content)) {
184
+ if (hasParameterPlaceholders(processedContent)) {
163
185
  processedContent = substituteCommandParameters(
164
- command.content,
186
+ processedContent,
165
187
  args,
166
188
  );
167
189
  } else {
168
190
  // If no placeholders, append arguments to the content
169
- processedContent = `${command.content.trim()} ${args}`;
191
+ processedContent = `${processedContent.trim()} ${args}`;
170
192
  }
171
193
  }
172
194
 
@@ -202,8 +224,9 @@ export class SlashCommandManager {
202
224
  /**
203
225
  * Register new command
204
226
  */
205
- private registerCommand(command: SlashCommand): void {
227
+ public registerCommand(command: SlashCommand): void {
206
228
  this.commands.set(command.id, command);
229
+ this.messageManager.triggerSlashCommandsChange(this.getCommands());
207
230
  }
208
231
 
209
232
  /**