wave-agent-sdk 0.10.3 → 0.11.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 (136) hide show
  1. package/dist/agent.d.ts +8 -6
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +12 -9
  4. package/dist/builtin-skills/builtin-skills/loop/SKILL.md +53 -0
  5. package/dist/builtin-skills/builtin-skills/loop/parsing.ts +159 -0
  6. package/dist/builtin-skills/builtin-skills/settings/HOOKS.md +82 -0
  7. package/dist/builtin-skills/{settings → builtin-skills/settings}/SKILL.md +1 -1
  8. package/dist/builtin-skills/loop/parsing.d.ts +13 -0
  9. package/dist/builtin-skills/loop/parsing.d.ts.map +1 -0
  10. package/dist/builtin-skills/loop/parsing.js +125 -0
  11. package/dist/constants/tools.d.ts +3 -0
  12. package/dist/constants/tools.d.ts.map +1 -1
  13. package/dist/constants/tools.js +3 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/managers/aiManager.d.ts +0 -2
  18. package/dist/managers/aiManager.d.ts.map +1 -1
  19. package/dist/managers/aiManager.js +53 -14
  20. package/dist/managers/cronManager.d.ts +19 -0
  21. package/dist/managers/cronManager.d.ts.map +1 -0
  22. package/dist/managers/cronManager.js +124 -0
  23. package/dist/managers/hookManager.d.ts.map +1 -1
  24. package/dist/managers/hookManager.js +21 -13
  25. package/dist/managers/liveConfigManager.js +1 -1
  26. package/dist/managers/mcpManager.d.ts +1 -1
  27. package/dist/managers/mcpManager.d.ts.map +1 -1
  28. package/dist/managers/mcpManager.js +10 -2
  29. package/dist/managers/messageManager.d.ts +0 -1
  30. package/dist/managers/messageManager.d.ts.map +1 -1
  31. package/dist/managers/permissionManager.d.ts +27 -7
  32. package/dist/managers/permissionManager.d.ts.map +1 -1
  33. package/dist/managers/permissionManager.js +119 -14
  34. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  35. package/dist/managers/slashCommandManager.js +11 -0
  36. package/dist/managers/subagentManager.d.ts +3 -0
  37. package/dist/managers/subagentManager.d.ts.map +1 -1
  38. package/dist/managers/subagentManager.js +10 -17
  39. package/dist/managers/toolManager.d.ts +1 -1
  40. package/dist/managers/toolManager.d.ts.map +1 -1
  41. package/dist/managers/toolManager.js +28 -4
  42. package/dist/services/configurationService.d.ts.map +1 -1
  43. package/dist/services/configurationService.js +8 -7
  44. package/dist/services/hook.d.ts.map +1 -1
  45. package/dist/services/hook.js +3 -10
  46. package/dist/services/initializationService.js +2 -2
  47. package/dist/services/jsonlHandler.d.ts.map +1 -1
  48. package/dist/services/jsonlHandler.js +3 -0
  49. package/dist/services/reversionService.d.ts +2 -2
  50. package/dist/services/reversionService.d.ts.map +1 -1
  51. package/dist/services/reversionService.js +3 -3
  52. package/dist/services/session.d.ts.map +1 -1
  53. package/dist/services/session.js +18 -11
  54. package/dist/tools/agentTool.js +1 -1
  55. package/dist/tools/bashTool.d.ts.map +1 -1
  56. package/dist/tools/bashTool.js +33 -12
  57. package/dist/tools/cronCreateTool.d.ts +3 -0
  58. package/dist/tools/cronCreateTool.d.ts.map +1 -0
  59. package/dist/tools/cronCreateTool.js +59 -0
  60. package/dist/tools/cronDeleteTool.d.ts +3 -0
  61. package/dist/tools/cronDeleteTool.d.ts.map +1 -0
  62. package/dist/tools/cronDeleteTool.js +38 -0
  63. package/dist/tools/cronListTool.d.ts +3 -0
  64. package/dist/tools/cronListTool.d.ts.map +1 -0
  65. package/dist/tools/cronListTool.js +30 -0
  66. package/dist/tools/readTool.d.ts.map +1 -1
  67. package/dist/tools/readTool.js +8 -32
  68. package/dist/tools/skillTool.d.ts +0 -3
  69. package/dist/tools/skillTool.d.ts.map +1 -1
  70. package/dist/tools/skillTool.js +4 -3
  71. package/dist/tools/taskOutputTool.d.ts.map +1 -1
  72. package/dist/tools/taskOutputTool.js +15 -8
  73. package/dist/tools/types.d.ts +2 -0
  74. package/dist/tools/types.d.ts.map +1 -1
  75. package/dist/types/agent.d.ts +10 -0
  76. package/dist/types/agent.d.ts.map +1 -1
  77. package/dist/types/configuration.d.ts +1 -1
  78. package/dist/types/configuration.d.ts.map +1 -1
  79. package/dist/types/cron.d.ts +10 -0
  80. package/dist/types/cron.d.ts.map +1 -0
  81. package/dist/types/cron.js +1 -0
  82. package/dist/types/hooks.d.ts +1 -5
  83. package/dist/types/hooks.d.ts.map +1 -1
  84. package/dist/types/hooks.js +1 -1
  85. package/dist/types/index.d.ts +1 -0
  86. package/dist/types/index.d.ts.map +1 -1
  87. package/dist/types/index.js +1 -0
  88. package/dist/types/messaging.d.ts +1 -1
  89. package/dist/types/messaging.d.ts.map +1 -1
  90. package/dist/utils/containerSetup.d.ts.map +1 -1
  91. package/dist/utils/containerSetup.js +40 -13
  92. package/dist/utils/mcpUtils.d.ts +2 -2
  93. package/dist/utils/mcpUtils.d.ts.map +1 -1
  94. package/dist/utils/mcpUtils.js +1 -5
  95. package/package.json +2 -1
  96. package/src/agent.ts +17 -12
  97. package/src/builtin-skills/loop/SKILL.md +53 -0
  98. package/src/builtin-skills/loop/parsing.ts +159 -0
  99. package/src/builtin-skills/settings/HOOKS.md +44 -57
  100. package/src/builtin-skills/settings/SKILL.md +1 -1
  101. package/src/constants/tools.ts +3 -0
  102. package/src/index.ts +1 -0
  103. package/src/managers/aiManager.ts +72 -24
  104. package/src/managers/cronManager.ts +167 -0
  105. package/src/managers/hookManager.ts +27 -13
  106. package/src/managers/liveConfigManager.ts +2 -2
  107. package/src/managers/mcpManager.ts +23 -2
  108. package/src/managers/messageManager.ts +0 -6
  109. package/src/managers/permissionManager.ts +154 -18
  110. package/src/managers/slashCommandManager.ts +12 -0
  111. package/src/managers/subagentManager.ts +15 -19
  112. package/src/managers/toolManager.ts +37 -4
  113. package/src/services/configurationService.ts +8 -7
  114. package/src/services/hook.ts +5 -11
  115. package/src/services/initializationService.ts +3 -3
  116. package/src/services/jsonlHandler.ts +4 -0
  117. package/src/services/reversionService.ts +9 -4
  118. package/src/services/session.ts +19 -12
  119. package/src/tools/agentTool.ts +1 -1
  120. package/src/tools/bashTool.ts +43 -14
  121. package/src/tools/cronCreateTool.ts +73 -0
  122. package/src/tools/cronDeleteTool.ts +47 -0
  123. package/src/tools/cronListTool.ts +38 -0
  124. package/src/tools/readTool.ts +11 -33
  125. package/src/tools/skillTool.ts +6 -4
  126. package/src/tools/taskOutputTool.ts +14 -8
  127. package/src/tools/types.ts +2 -0
  128. package/src/types/agent.ts +10 -0
  129. package/src/types/configuration.ts +1 -1
  130. package/src/types/cron.ts +9 -0
  131. package/src/types/hooks.ts +5 -9
  132. package/src/types/index.ts +1 -0
  133. package/src/types/messaging.ts +1 -1
  134. package/src/utils/containerSetup.ts +50 -16
  135. package/src/utils/mcpUtils.ts +2 -5
  136. package/dist/builtin-skills/settings/HOOKS.md +0 -95
@@ -45,12 +45,6 @@ export interface MessageManagerCallbacks {
45
45
  onErrorBlockAdded?: (error: string) => void;
46
46
  onCompressBlockAdded?: (content: string) => void;
47
47
  onCompressionStateChange?: (isCompressing: boolean) => void;
48
- onMemoryBlockAdded?: (
49
- content: string,
50
- success: boolean,
51
- type: "project" | "user",
52
- storagePath: string,
53
- ) => void;
54
48
  // Bang callback
55
49
  onAddBangMessage?: (command: string) => void;
56
50
  onUpdateBangMessage?: (command: string, output: string) => void;
@@ -92,12 +92,16 @@ const DEFAULT_ALLOWED_RULES = [
92
92
  import { logger } from "../utils/globalLogger.js";
93
93
 
94
94
  export interface PermissionManagerOptions {
95
- /** Configured default permission mode from settings */
96
- configuredDefaultMode?: PermissionMode;
95
+ /** Configured permission mode from settings */
96
+ configuredPermissionMode?: PermissionMode;
97
97
  /** Allowed rules from settings */
98
98
  allowedRules?: string[];
99
99
  /** Denied rules from settings */
100
100
  deniedRules?: string[];
101
+ /** Instance-specific allowed rules (from AgentOptions) */
102
+ instanceAllowedRules?: string[];
103
+ /** Instance-specific denied rules (from AgentOptions) */
104
+ instanceDeniedRules?: string[];
101
105
  /** Additional directories considered part of the Safe Zone */
102
106
  additionalDirectories?: string[];
103
107
  /** The main working directory */
@@ -109,61 +113,70 @@ export interface PermissionManagerOptions {
109
113
  }
110
114
 
111
115
  export class PermissionManager {
112
- private configuredDefaultMode?: PermissionMode;
116
+ private configuredPermissionMode?: PermissionMode;
113
117
  private allowedRules: string[] = [];
114
118
  private deniedRules: string[] = [];
119
+ private instanceAllowedRules: string[] = [];
120
+ private instanceDeniedRules: string[] = [];
115
121
  private temporaryRules: string[] = [];
116
122
  private additionalDirectories: string[] = [];
117
123
  private systemAdditionalDirectories: string[] = [];
118
124
  private workdir?: string;
119
125
  private planFilePath?: string;
120
- private onConfiguredDefaultModeChange?: (mode: PermissionMode) => void;
126
+ private worktreeName?: string;
127
+ private mainRepoRoot?: string;
128
+ private onConfiguredPermissionModeChange?: (mode: PermissionMode) => void;
121
129
  private _logger?: Logger;
122
130
 
123
131
  constructor(
124
132
  private container: Container,
125
133
  options: PermissionManagerOptions = {},
126
134
  ) {
127
- this.configuredDefaultMode = options.configuredDefaultMode;
135
+ this.configuredPermissionMode = options.configuredPermissionMode;
128
136
  this.allowedRules = options.allowedRules || [];
129
137
  this.deniedRules = options.deniedRules || [];
138
+ this.instanceAllowedRules = options.instanceAllowedRules || [];
139
+ this.instanceDeniedRules = options.instanceDeniedRules || [];
130
140
  this.workdir = options.workdir;
131
141
  this.planFilePath = options.planFilePath;
132
142
  this._logger = options.logger;
133
143
  this.updateAdditionalDirectories(options.additionalDirectories || []);
144
+
145
+ this.worktreeName = this.container.get<string>("WorktreeName");
146
+ this.mainRepoRoot = this.container.get<string>("MainRepoRoot");
134
147
  }
135
148
 
136
149
  /**
137
150
  * Set a callback to be notified when the effective permission mode changes due to configuration updates
138
151
  */
139
- public setOnConfiguredDefaultModeChange(
152
+ public setOnConfiguredPermissionModeChange(
140
153
  callback: (mode: PermissionMode) => void,
141
154
  ): void {
142
- this.onConfiguredDefaultModeChange = callback;
155
+ this.onConfiguredPermissionModeChange = callback;
143
156
  }
144
157
 
145
158
  /**
146
159
  * Update the configured default mode (e.g., when configuration reloads)
147
160
  */
148
- updateConfiguredDefaultMode(defaultMode?: PermissionMode): void {
161
+ updateConfiguredPermissionMode(permissionMode?: PermissionMode): void {
149
162
  const oldEffectiveMode = this.getCurrentEffectiveMode();
150
163
 
151
- this.configuredDefaultMode = defaultMode;
164
+ this.configuredPermissionMode = permissionMode;
152
165
 
153
166
  const newEffectiveMode = this.getCurrentEffectiveMode();
154
167
  if (
155
168
  oldEffectiveMode !== newEffectiveMode &&
156
- this.onConfiguredDefaultModeChange
169
+ this.onConfiguredPermissionModeChange
157
170
  ) {
158
- this.onConfiguredDefaultModeChange(newEffectiveMode);
171
+ this.onConfiguredPermissionModeChange(newEffectiveMode);
159
172
  }
160
173
  }
161
174
 
162
175
  /**
163
176
  * Get the configured default mode
164
177
  */
165
- public getConfiguredDefaultMode(): PermissionMode | undefined {
166
- return this.configuredDefaultMode;
178
+ public getConfiguredPermissionMode(): PermissionMode | undefined {
179
+ return this.configuredPermissionMode;
167
180
  }
168
181
 
169
182
  /**
@@ -180,6 +193,20 @@ export class PermissionManager {
180
193
  return [...this.deniedRules];
181
194
  }
182
195
 
196
+ /**
197
+ * Get all instance-specific allowed rules
198
+ */
199
+ public getInstanceAllowedRules(): string[] {
200
+ return [...this.instanceAllowedRules];
201
+ }
202
+
203
+ /**
204
+ * Get all instance-specific denied rules
205
+ */
206
+ public getInstanceDeniedRules(): string[] {
207
+ return [...this.instanceDeniedRules];
208
+ }
209
+
183
210
  /**
184
211
  * Get all additional directories
185
212
  */
@@ -325,8 +352,8 @@ export class PermissionManager {
325
352
  }
326
353
 
327
354
  // Use configured default mode if available
328
- if (this.configuredDefaultMode !== undefined) {
329
- return this.configuredDefaultMode;
355
+ if (this.configuredPermissionMode !== undefined) {
356
+ return this.configuredPermissionMode;
330
357
  }
331
358
 
332
359
  // Fall back to system default
@@ -340,6 +367,54 @@ export class PermissionManager {
340
367
  async checkPermission(
341
368
  context: ToolPermissionContext,
342
369
  ): Promise<PermissionDecision> {
370
+ // 0. Check instance-specific denied rules first - Deny always takes precedence
371
+ for (const rule of this.instanceDeniedRules) {
372
+ if (this.matchesRule(context, rule)) {
373
+ logger?.warn("Permission denied by instance rule", {
374
+ toolName: context.toolName,
375
+ rule,
376
+ });
377
+ return {
378
+ behavior: "deny",
379
+ message: `Access to tool '${context.toolName}' is explicitly denied by instance rule: ${rule}`,
380
+ };
381
+ }
382
+ }
383
+
384
+ // 0. Check worktree safety for Write and Edit tools
385
+ if (
386
+ this.worktreeName &&
387
+ this.mainRepoRoot &&
388
+ this.workdir &&
389
+ (context.toolName === WRITE_TOOL_NAME ||
390
+ context.toolName === EDIT_TOOL_NAME)
391
+ ) {
392
+ const targetPath = context.toolInput?.file_path as string | undefined;
393
+ if (targetPath) {
394
+ const absoluteTargetPath = path.resolve(this.workdir, targetPath);
395
+ const isInsideMainRepo = isPathInside(
396
+ absoluteTargetPath,
397
+ this.mainRepoRoot,
398
+ );
399
+ const isInsideWorktree = isPathInside(absoluteTargetPath, this.workdir);
400
+
401
+ // If it's inside the main repo but NOT inside the current worktree
402
+ if (isInsideMainRepo && !isInsideWorktree) {
403
+ logger?.warn("Worktree safety violation", {
404
+ toolName: context.toolName,
405
+ targetPath,
406
+ worktreeName: this.worktreeName,
407
+ mainRepoRoot: this.mainRepoRoot,
408
+ workdir: this.workdir,
409
+ });
410
+ return {
411
+ behavior: "deny",
412
+ message: `Access denied: You are currently in a worktree session ("${this.worktreeName}"). Modifying files in the main repository (outside the worktree) is not allowed. Please only modify files within the worktree directory: ${this.workdir}`,
413
+ };
414
+ }
415
+ }
416
+ }
417
+
343
418
  // 0. Check denied rules first - Deny always takes precedence
344
419
  for (const rule of this.deniedRules) {
345
420
  if (this.matchesRule(context, rule)) {
@@ -359,7 +434,7 @@ export class PermissionManager {
359
434
  return { behavior: "allow" };
360
435
  }
361
436
 
362
- // 1.1 If acceptEdits mode, allow Edit, Write
437
+ // 1.1 If acceptEdits mode, allow Edit, Write, and mkdir in safe zone
363
438
  if (context.permissionMode === "acceptEdits") {
364
439
  const autoAcceptedTools = [EDIT_TOOL_NAME, WRITE_TOOL_NAME];
365
440
  if (autoAcceptedTools.includes(context.toolName)) {
@@ -387,6 +462,40 @@ export class PermissionManager {
387
462
  }
388
463
  }
389
464
  }
465
+
466
+ // Special case for mkdir in Bash tool
467
+ if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
468
+ const command = String(context.toolInput.command).trim();
469
+ if (command.startsWith("mkdir ")) {
470
+ const parts = splitBashCommand(command);
471
+ // Check if it's a simple mkdir command (first part is mkdir)
472
+ if (parts.length === 1) {
473
+ const processedPart = stripEnvVars(parts[0]);
474
+ if (processedPart.startsWith("mkdir ")) {
475
+ const args = processedPart.slice(6).trim();
476
+ const pathArgs =
477
+ (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).filter(
478
+ (arg) => !arg.startsWith("-"),
479
+ ) || [];
480
+
481
+ if (pathArgs.length > 0) {
482
+ const allPathsSafe = pathArgs.every((pathArg) => {
483
+ const cleanPath = pathArg.replace(/^['"](.*)['"]$/, "$1");
484
+ const { isInside } = this.isInsideSafeZone(
485
+ cleanPath,
486
+ context.toolInput?.workdir as string | undefined,
487
+ );
488
+ return isInside;
489
+ });
490
+
491
+ if (allPathsSafe) {
492
+ return { behavior: "allow" };
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+ }
390
499
  }
391
500
 
392
501
  // 1.2 Check if tool call is allowed by persistent or temporary rules
@@ -475,7 +584,27 @@ export class PermissionManager {
475
584
  * Determine if a tool requires permission checks based on its name
476
585
  */
477
586
  isRestrictedTool(toolName: string): boolean {
478
- return (RESTRICTED_TOOLS as readonly string[]).includes(toolName);
587
+ return (
588
+ (RESTRICTED_TOOLS as readonly string[]).includes(toolName) ||
589
+ toolName.startsWith("mcp__")
590
+ );
591
+ }
592
+
593
+ /**
594
+ * Check if a tool is completely denied by name in instance or global rules
595
+ */
596
+ public isToolDenied(toolName: string): boolean {
597
+ // Check instance-specific denied rules
598
+ if (this.instanceDeniedRules.includes(toolName)) {
599
+ return true;
600
+ }
601
+
602
+ // Check global denied rules
603
+ if (this.deniedRules.includes(toolName)) {
604
+ return true;
605
+ }
606
+
607
+ return false;
479
608
  }
480
609
 
481
610
  /**
@@ -497,6 +626,8 @@ export class PermissionManager {
497
626
  const processedPart = stripRedirections(stripEnvVars(parts[0]));
498
627
  suggestedPrefix = getSmartPrefix(processedPart) ?? undefined;
499
628
  }
629
+ } else if (toolName.startsWith("mcp__")) {
630
+ suggestedPrefix = toolName;
500
631
  }
501
632
 
502
633
  const context: ToolPermissionContext = {
@@ -742,7 +873,12 @@ export class PermissionManager {
742
873
  return rules.some((rule) => this.matchesRule(ctx, rule));
743
874
  };
744
875
 
745
- // Check temporary rules first
876
+ // Check instance-specific allowed rules first
877
+ if (isAllowedByRuleList(context, this.instanceAllowedRules)) {
878
+ return true;
879
+ }
880
+
881
+ // Check temporary rules
746
882
  if (isAllowedByRuleList(context, this.temporaryRules)) {
747
883
  return true;
748
884
  }
@@ -90,6 +90,18 @@ export class SlashCommandManager {
90
90
  await this.aiManager.sendAIMessage();
91
91
  },
92
92
  });
93
+
94
+ // Register built-in clear command
95
+ this.registerCommand({
96
+ id: "clear",
97
+ name: "clear",
98
+ description: "Clear conversation history and reset session",
99
+ handler: async () => {
100
+ this.aiManager.abortAIMessage();
101
+ this.messageManager.clearMessages();
102
+ await this.taskManager.syncWithSession();
103
+ },
104
+ });
93
105
  }
94
106
 
95
107
  /**
@@ -80,6 +80,7 @@ export interface SubagentManagerOptions {
80
80
  workdir: string;
81
81
  callbacks?: SubagentManagerCallbacks; // Use SubagentManagerCallbacks instead of parentCallbacks
82
82
  onUsageAdded?: (usage: Usage) => void;
83
+ stream: boolean;
83
84
  }
84
85
 
85
86
  export class SubagentManager {
@@ -90,12 +91,14 @@ export class SubagentManager {
90
91
  private callbacks?: SubagentManagerCallbacks; // Use SubagentManagerCallbacks instead of parentCallbacks
91
92
  private onUsageAdded?: (usage: Usage) => void;
92
93
  private container: Container;
94
+ private stream: boolean;
93
95
 
94
96
  constructor(container: Container, options: SubagentManagerOptions) {
95
97
  this.container = container;
96
98
  this.workdir = options.workdir;
97
99
  this.callbacks = options.callbacks; // Store SubagentManagerCallbacks
98
100
  this.onUsageAdded = options.onUsageAdded;
101
+ this.stream = options.stream;
99
102
  }
100
103
 
101
104
  private get configurationService(): ConfigurationService {
@@ -155,6 +158,7 @@ export class SubagentManager {
155
158
  subagent_type: string;
156
159
  allowedTools?: string[];
157
160
  model?: string;
161
+ stream?: boolean;
158
162
  },
159
163
  runInBackground?: boolean,
160
164
  onUpdate?: () => void,
@@ -178,10 +182,16 @@ export class SubagentManager {
178
182
  this.container.get<PermissionManager>("PermissionManager");
179
183
  const subagentPermissionManager = new PermissionManager(subagentContainer, {
180
184
  workdir: this.workdir,
181
- configuredDefaultMode:
182
- parentPermissionManager?.getConfiguredDefaultMode(),
185
+ configuredPermissionMode:
186
+ parentPermissionManager?.getConfiguredPermissionMode(),
183
187
  allowedRules: parentPermissionManager?.getAllowedRules(),
184
188
  deniedRules: parentPermissionManager?.getDeniedRules(),
189
+ instanceAllowedRules:
190
+ parentPermissionManager?.getInstanceAllowedRules?.(),
191
+ instanceDeniedRules: [
192
+ ...(parentPermissionManager?.getInstanceDeniedRules?.() || []),
193
+ AGENT_TOOL_NAME, // Always deny Agent tool in subagents to prevent recursion
194
+ ],
185
195
  additionalDirectories:
186
196
  parentPermissionManager?.getAdditionalDirectories(),
187
197
  planFilePath: parentPermissionManager?.getPlanFilePath(),
@@ -222,6 +232,7 @@ export class SubagentManager {
222
232
  systemPrompt: configuration.systemPrompt,
223
233
  subagentType: parameters.subagent_type, // Pass subagent type for hook context
224
234
  modelOverride: parameters.model || configuration.model, // Pass model override
235
+ stream: parameters.stream ?? this.stream, // Pass streaming mode flag
225
236
  callbacks: {
226
237
  onUsageAdded: this.onUsageAdded,
227
238
  },
@@ -410,24 +421,9 @@ export class SubagentManager {
410
421
  // Add the user's prompt as a message
411
422
  instance.messageManager.addUserMessage({ content: prompt });
412
423
 
413
- // Create enabled tools list - always exclude Agent tool to prevent subagent recursion
414
- // Use instance.configuration.tools if provided, otherwise fallback to all tools
415
- let enabledTools = instance.configuration.tools;
416
-
417
- // Always filter out the Agent tool to prevent subagents from creating sub-subagents
418
- if (enabledTools) {
419
- enabledTools = enabledTools.filter((tool) => tool !== AGENT_TOOL_NAME);
420
- } else {
421
- // If no tools specified, get all tools except Agent
422
- const allTools = instance.toolManager.list().map((tool) => tool.name);
423
- enabledTools = allTools.filter((tool) => tool !== AGENT_TOOL_NAME);
424
- }
425
-
426
- // Execute the AI request with tool restrictions
424
+ // Execute the AI request
427
425
  // The AIManager will handle abort signals through its own abort controllers
428
- const executeAI = instance.aiManager.sendAIMessage({
429
- tools: enabledTools,
430
- });
426
+ const executeAI = instance.aiManager.sendAIMessage();
431
427
 
432
428
  // If we have an abort signal, race against it using utilities to prevent listener accumulation
433
429
  if (abortSignal && !instance.backgroundTaskId) {
@@ -6,6 +6,9 @@ import { editTool } from "../tools/editTool.js";
6
6
  import { writeTool } from "../tools/writeTool.js";
7
7
  import { exitPlanModeTool } from "../tools/exitPlanMode.js";
8
8
  import { askUserQuestionTool } from "../tools/askUserQuestion.js";
9
+ import { cronCreateTool } from "../tools/cronCreateTool.js";
10
+ import { cronDeleteTool } from "../tools/cronDeleteTool.js";
11
+ import { cronListTool } from "../tools/cronListTool.js";
9
12
  // New tools
10
13
  import { globTool } from "../tools/globTool.js";
11
14
  import { grepTool } from "../tools/grepTool.js";
@@ -114,6 +117,9 @@ class ToolManager {
114
117
  taskGetTool,
115
118
  taskUpdateTool,
116
119
  taskListTool,
120
+ cronCreateTool,
121
+ cronDeleteTool,
122
+ cronListTool,
117
123
  ];
118
124
 
119
125
  for (const tool of builtInTools) {
@@ -124,9 +130,17 @@ class ToolManager {
124
130
  }
125
131
 
126
132
  /**
127
- * Check if a tool should be enabled based on tools configuration
133
+ * Check if a tool should be enabled based on tools configuration and permission rules
128
134
  */
129
135
  private shouldEnableTool(name: string): boolean {
136
+ const permissionManager =
137
+ this.container.get<PermissionManager>("PermissionManager");
138
+
139
+ // If tool is explicitly denied by name in permission rules, filter it out
140
+ if (permissionManager?.isToolDenied(name)) {
141
+ return false;
142
+ }
143
+
130
144
  if (!this.tools) {
131
145
  return true;
132
146
  }
@@ -194,6 +208,11 @@ class ToolManager {
194
208
  skillManager: this.container.has("SkillManager")
195
209
  ? this.container.get<SkillManager>("SkillManager")
196
210
  : undefined,
211
+ cronManager: this.container.has("CronManager")
212
+ ? this.container.get<import("./cronManager.js").CronManager>(
213
+ "CronManager",
214
+ )
215
+ : undefined,
197
216
  sessionId: context.sessionId,
198
217
  toolCallId: context.toolCallId,
199
218
  };
@@ -241,8 +260,14 @@ class ToolManager {
241
260
  }
242
261
 
243
262
  list(): ToolPlugin[] {
244
- const builtInTools = Array.from(this.toolsRegistry.values());
245
- const mcpTools = this.mcpManager.getMcpToolPlugins();
263
+ const permissionManager =
264
+ this.container.get<PermissionManager>("PermissionManager");
265
+ const builtInTools = Array.from(this.toolsRegistry.values()).filter(
266
+ (tool) => !permissionManager?.isToolDenied(tool.name),
267
+ );
268
+ const mcpTools = this.mcpManager
269
+ .getMcpToolPlugins()
270
+ .filter((tool) => !permissionManager?.isToolDenied(tool.name));
246
271
  return [...builtInTools, ...mcpTools];
247
272
  }
248
273
 
@@ -251,9 +276,15 @@ class ToolManager {
251
276
  availableSkills?: SkillMetadata[];
252
277
  workdir?: string;
253
278
  }): ChatCompletionFunctionTool[] {
279
+ const permissionManager =
280
+ this.container.get<PermissionManager>("PermissionManager");
254
281
  const effectivePermissionMode = this.getPermissionMode();
255
282
  const builtInToolsConfig = Array.from(this.toolsRegistry.values())
256
283
  .filter((tool) => {
284
+ // If tool is explicitly denied by name in permission rules, filter it out
285
+ if (permissionManager?.isToolDenied(tool.name)) {
286
+ return false;
287
+ }
257
288
  if (effectivePermissionMode === "bypassPermissions") {
258
289
  if (tool.name === "ExitPlanMode" || tool.name === "AskUserQuestion") {
259
290
  return false;
@@ -278,7 +309,9 @@ class ToolManager {
278
309
  }
279
310
  return config;
280
311
  });
281
- const mcpToolsConfig = this.mcpManager.getMcpToolsConfig();
312
+ const mcpToolsConfig = this.mcpManager
313
+ .getMcpToolsConfig()
314
+ .filter((tool) => !permissionManager?.isToolDenied(tool.function.name));
282
315
  return [...builtInToolsConfig, ...mcpToolsConfig];
283
316
  }
284
317
 
@@ -248,18 +248,18 @@ export class ConfigurationService {
248
248
  }
249
249
  }
250
250
 
251
- // Validate defaultMode if present
252
- if (config.permissions.defaultMode !== undefined) {
251
+ // Validate permissionMode if present
252
+ if (config.permissions.permissionMode !== undefined) {
253
253
  const validModes: PermissionMode[] = [
254
254
  "default",
255
255
  "bypassPermissions",
256
256
  "acceptEdits",
257
257
  "plan",
258
258
  ];
259
- if (!validModes.includes(config.permissions.defaultMode)) {
259
+ if (!validModes.includes(config.permissions.permissionMode)) {
260
260
  result.isValid = false;
261
261
  result.errors.push(
262
- `Invalid defaultMode: "${config.permissions.defaultMode}". Must be one of: ${validModes.join(", ")}`,
262
+ `Invalid permissionMode: "${config.permissions.permissionMode}". Must be one of: ${validModes.join(", ")}`,
263
263
  );
264
264
  }
265
265
  }
@@ -967,9 +967,10 @@ export function loadMergedWaveConfig(
967
967
  ];
968
968
  }
969
969
 
970
- // Merge defaultMode (last one wins)
971
- if (config.permissions.defaultMode !== undefined) {
972
- mergedConfig.permissions.defaultMode = config.permissions.defaultMode;
970
+ // Merge permissionMode (last one wins)
971
+ if (config.permissions.permissionMode !== undefined) {
972
+ mergedConfig.permissions.permissionMode =
973
+ config.permissions.permissionMode;
973
974
  }
974
975
 
975
976
  // Merge additionalDirectories
@@ -48,7 +48,11 @@ async function buildHookJsonInput(
48
48
  };
49
49
 
50
50
  // Add optional fields based on event type
51
- if (context.event === "PreToolUse" || context.event === "PostToolUse") {
51
+ if (
52
+ context.event === "PreToolUse" ||
53
+ context.event === "PostToolUse" ||
54
+ context.event === "PermissionRequest"
55
+ ) {
52
56
  if (context.toolName) {
53
57
  jsonInput.tool_name = context.toolName;
54
58
  }
@@ -73,16 +77,6 @@ async function buildHookJsonInput(
73
77
  jsonInput.subagent_type = context.subagentType;
74
78
  }
75
79
 
76
- // Add notification fields for Notification events
77
- if (context.event === "Notification") {
78
- if (context.message !== undefined) {
79
- jsonInput.message = context.message;
80
- }
81
- if (context.notificationType !== undefined) {
82
- jsonInput.notification_type = context.notificationType;
83
- }
84
- }
85
-
86
80
  // Add name field for WorktreeCreate events
87
81
  if (context.event === "WorktreeCreate") {
88
82
  if (context.worktreeName !== undefined) {
@@ -146,9 +146,9 @@ export class InitializationService {
146
146
  configResult.configuration.permissions.deny,
147
147
  );
148
148
  }
149
- if (configResult.configuration.permissions.defaultMode) {
150
- permissionManager.updateConfiguredDefaultMode(
151
- configResult.configuration.permissions.defaultMode,
149
+ if (configResult.configuration.permissions.permissionMode) {
150
+ permissionManager.updateConfiguredPermissionMode(
151
+ configResult.configuration.permissions.permissionMode,
152
152
  );
153
153
  }
154
154
  if (configResult.configuration.permissions.additionalDirectories) {
@@ -194,6 +194,10 @@ export class JsonlHandler {
194
194
  for (let i = 0; i < messages.length; i++) {
195
195
  const message = messages[i];
196
196
 
197
+ if (!message.id) {
198
+ throw new Error(`Message at index ${i} is missing required field: id`);
199
+ }
200
+
197
201
  if (!message.role) {
198
202
  throw new Error(
199
203
  `Message at index ${i} is missing required field: role`,
@@ -6,11 +6,16 @@ import { FileSnapshot } from "../types/reversion.js";
6
6
 
7
7
  export class ReversionService {
8
8
  private historyBaseDir: string;
9
- private sessionId: string;
9
+ private rootSessionId: string;
10
10
 
11
- constructor(sessionId: string) {
12
- this.sessionId = sessionId;
13
- this.historyBaseDir = join(homedir(), ".wave", "file-history", sessionId);
11
+ constructor(rootSessionId: string) {
12
+ this.rootSessionId = rootSessionId;
13
+ this.historyBaseDir = join(
14
+ homedir(),
15
+ ".wave",
16
+ "file-history",
17
+ rootSessionId || "default",
18
+ );
14
19
  }
15
20
 
16
21
  private getFilePathHash(filePath: string): string {
@@ -294,14 +294,21 @@ export async function loadSessionFromJsonl(
294
294
  sessionType,
295
295
  );
296
296
 
297
- const messages = await jsonlHandler.read(filePath);
298
-
299
- if (messages.length === 0) {
300
- return null;
297
+ // Check if file exists
298
+ try {
299
+ await fs.access(filePath);
300
+ } catch (error) {
301
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
302
+ return null;
303
+ }
304
+ throw error;
301
305
  }
302
306
 
307
+ const messages = await jsonlHandler.read(filePath);
308
+
303
309
  // Extract metadata from messages
304
- const lastMessage = messages[messages.length - 1];
310
+ const lastMessage =
311
+ messages.length > 0 ? messages[messages.length - 1] : null;
305
312
 
306
313
  // Try to get rootSessionId and parentSessionId from index
307
314
  let rootSessionId: string | undefined;
@@ -333,8 +340,10 @@ export async function loadSessionFromJsonl(
333
340
  }),
334
341
  metadata: {
335
342
  workdir,
336
- lastActiveAt: lastMessage.timestamp,
337
- latestTotalTokens: lastMessage.usage
343
+ lastActiveAt: lastMessage
344
+ ? lastMessage.timestamp
345
+ : new Date().toISOString(),
346
+ latestTotalTokens: lastMessage?.usage
338
347
  ? extractLatestTotalTokens([lastMessage])
339
348
  : 0,
340
349
  },
@@ -963,15 +972,13 @@ export async function handleSessionRestoration(
963
972
  // Use only JSONL format - no legacy support
964
973
  sessionToRestore = await loadSessionFromJsonl(restoreSessionId, workdir);
965
974
  if (!sessionToRestore) {
966
- console.error(`Session not found: ${restoreSessionId}`);
967
- process.exit(1);
975
+ throw new Error(`Session not found: ${restoreSessionId}`);
968
976
  }
969
977
  } else if (continueLastSession) {
970
978
  // Use only JSONL format - no legacy support
971
979
  sessionToRestore = await getLatestSessionFromJsonl(workdir);
972
980
  if (!sessionToRestore) {
973
- console.error(`No previous session found for workdir: ${workdir}`);
974
- process.exit(1);
981
+ throw new Error(`No previous session found for workdir: ${workdir}`);
975
982
  }
976
983
  }
977
984
 
@@ -984,7 +991,7 @@ export async function handleSessionRestoration(
984
991
  }
985
992
  } catch (error) {
986
993
  console.error("Failed to restore session:", error);
987
- process.exit(1);
994
+ throw error;
988
995
  }
989
996
  }
990
997