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
@@ -1,17 +1,18 @@
1
1
  # Wave Hooks Configuration
2
2
 
3
- Hooks allow you to automate tasks when certain events occur in Wave. This document provides detailed guidance on how to configure complex hooks in `settings.json`.
3
+ Hooks allow you to automate tasks when certain events occur in Wave. This document provides detailed guidance on how to configure hooks in `settings.json`.
4
4
 
5
5
  ## Hook Events
6
6
 
7
7
  Wave supports the following hook events:
8
8
 
9
+ - `PreToolUse`: Triggered before a tool is executed.
10
+ - `PostToolUse`: Triggered after a tool has finished executing.
11
+ - `UserPromptSubmit`: Triggered when a user submits a prompt.
12
+ - `PermissionRequest`: Triggered when Wave requests permission to use a tool.
13
+ - `Stop`: Triggered when Wave finishes its response cycle (no more tool calls).
14
+ - `SubagentStop`: Triggered when a subagent finishes its response cycle.
9
15
  - `WorktreeCreate`: Triggered when a new worktree is created.
10
- - `TaskStart`: Triggered when a task starts.
11
- - `TaskComplete`: Triggered when a task is completed.
12
- - `TaskError`: Triggered when a task fails.
13
- - `SessionStart`: Triggered when a new session starts.
14
- - `SessionEnd`: Triggered when a session ends.
15
16
 
16
17
  ## Hook Configuration Structure
17
18
 
@@ -20,19 +21,26 @@ Hooks are configured in the `hooks` field of `settings.json`. Each event can hav
20
21
  ```json
21
22
  {
22
23
  "hooks": {
23
- "WorktreeCreate": [
24
+ "PreToolUse": [
24
25
  {
25
- "command": "pnpm install",
26
- "description": "Install dependencies in new worktree",
27
- "blocking": true,
28
- "timeout": 300000
26
+ "matcher": "Write",
27
+ "hooks": [
28
+ {
29
+ "command": "pnpm lint",
30
+ "description": "Run lint before writing files"
31
+ }
32
+ ]
29
33
  }
30
34
  ],
31
- "TaskComplete": [
35
+ "PermissionRequest": [
32
36
  {
33
- "command": "pnpm test",
34
- "description": "Run tests after task completion",
35
- "blocking": false
37
+ "matcher": "Bash",
38
+ "hooks": [
39
+ {
40
+ "command": "echo \"Permission requested for Bash tool\" >> hooks.log",
41
+ "description": "Log permission requests for Bash"
42
+ }
43
+ ]
36
44
  }
37
45
  ]
38
46
  }
@@ -41,55 +49,34 @@ Hooks are configured in the `hooks` field of `settings.json`. Each event can hav
41
49
 
42
50
  ## Hook Configuration Fields
43
51
 
44
- - `command`: The shell command to execute.
45
- - `description`: A brief description of the hook's purpose.
46
- - `blocking`: (Optional) Whether the hook should block the main agent's execution (default: `false`).
47
- - `timeout`: (Optional) Maximum execution time in milliseconds (default: `60000`).
48
- - `env`: (Optional) Environment variables specific to this hook.
49
- - `cwd`: (Optional) Working directory for the hook command.
52
+ - `matcher`: (Optional) A pattern to match against the tool name (e.g., "Write", "Read*", "/^Edit/"). Only applicable for `PreToolUse`, `PostToolUse`, and `PermissionRequest`.
53
+ - `hooks`: An array of hook commands to execute.
54
+ - `command`: The shell command to execute.
55
+ - `description`: A brief description of the hook's purpose.
56
+ - `async`: (Optional) Whether the hook should run in the background without blocking (default: `false`).
57
+ - `timeout`: (Optional) Maximum execution time in seconds (default: `600`).
50
58
 
51
- ## Advanced Hook Examples
59
+ ## Hook Input JSON
52
60
 
53
- ### 1. Conditional Hooks
54
- You can use shell logic within the `command` field to create conditional hooks.
55
- ```json
56
- {
57
- "hooks": {
58
- "TaskComplete": [
59
- {
60
- "command": "if [ \"$WAVE_TASK_STATUS\" = \"completed\" ]; then pnpm lint; fi",
61
- "description": "Run linting only on successful task completion"
62
- }
63
- ]
64
- }
65
- }
66
- ```
61
+ Wave provides detailed context to hook processes via `stdin` as a JSON object. This allows hooks to make informed decisions based on the current state.
67
62
 
68
- ### 2. Hook Chaining
69
- You can chain multiple commands in a single hook or define multiple hooks for the same event.
70
- ```json
71
- {
72
- "hooks": {
73
- "WorktreeCreate": [
74
- {
75
- "command": "pnpm install && pnpm build",
76
- "description": "Install and build in new worktree"
77
- }
78
- ]
79
- }
80
- }
81
- ```
63
+ ### Common Fields
64
+ - `session_id`: The current session ID.
65
+ - `transcript_path`: Path to the session transcript file (JSON).
66
+ - `cwd`: The current working directory.
67
+ - `hook_event_name`: The name of the triggering event.
82
68
 
83
- ### 3. Using Environment Variables
84
- Wave provides several environment variables to hooks:
85
- - `WAVE_PROJECT_DIR`: The root directory of the project.
86
- - `WAVE_SESSION_ID`: The current session ID.
87
- - `WAVE_TASK_ID`: The current task ID (if applicable).
88
- - `WAVE_TASK_STATUS`: The status of the task (for `TaskComplete` and `TaskError`).
69
+ ### Event-Specific Fields
70
+ - `tool_name`: (PreToolUse, PostToolUse, PermissionRequest) The name of the tool.
71
+ - `tool_input`: (PreToolUse, PostToolUse, PermissionRequest) The input parameters passed to the tool.
72
+ - `tool_response`: (PostToolUse) The result of the tool execution.
73
+ - `user_prompt`: (UserPromptSubmit) The text submitted by the user.
74
+ - `subagent_type`: (If executed by a subagent) The type of the subagent.
75
+ - `name`: (WorktreeCreate) The name of the new worktree.
89
76
 
90
77
  ## Best Practices
91
78
 
92
- - **Keep hooks fast**: Long-running hooks can slow down your workflow, especially if they are `blocking`.
79
+ - **Keep hooks fast**: Long-running hooks can slow down your workflow unless they are `async`.
93
80
  - **Use descriptive names**: Help yourself and others understand what each hook does.
94
81
  - **Test your hooks**: Run the commands manually first to ensure they work as expected.
95
82
  - **Use local overrides**: For machine-specific hooks, use `.wave/settings.local.json`.
@@ -41,7 +41,7 @@ Manage tool permissions and define the "Safe Zone".
41
41
  "permissions": {
42
42
  "allow": ["Bash", "Read"],
43
43
  "deny": ["Write"],
44
- "defaultMode": "interactive",
44
+ "permissionMode": "default",
45
45
  "additionalDirectories": ["/tmp/wave-exports"]
46
46
  }
47
47
  }
@@ -15,3 +15,6 @@ export const TASK_GET_TOOL_NAME = "TaskGet";
15
15
  export const TASK_UPDATE_TOOL_NAME = "TaskUpdate";
16
16
  export const TASK_LIST_TOOL_NAME = "TaskList";
17
17
  export const WRITE_TOOL_NAME = "Write";
18
+ export const CRON_CREATE_TOOL_NAME = "CronCreate";
19
+ export const CRON_DELETE_TOOL_NAME = "CronDelete";
20
+ export const CRON_LIST_TOOL_NAME = "CronList";
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./constants/tools.js";
7
7
  // Export main agent
8
8
  export * from "./agent.js";
9
9
  export * from "./core/plugin.js";
10
+ export * from "./managers/cronManager.js";
10
11
 
11
12
  // Export all utilities
12
13
  export * from "./utils/bashParser.js";
@@ -8,6 +8,7 @@ import type {
8
8
  ModelConfig,
9
9
  Usage,
10
10
  PermissionMode,
11
+ Message,
11
12
  } from "../types/index.js";
12
13
  import type { ToolManager } from "./toolManager.js";
13
14
  import type { ToolContext, ToolResult } from "../tools/types.js";
@@ -160,26 +161,18 @@ export class AIManager {
160
161
  /**
161
162
  * Get filtered tool configuration based on tools list
162
163
  */
163
- private getFilteredToolsConfig(tools?: string[]) {
164
+ private getFilteredToolsConfig() {
164
165
  // Get available subagents and skills for dynamic prompts
165
166
  const availableSubagents = this.subagentManager?.getConfigurations();
166
167
  const availableSkills = this.skillManager
167
168
  ?.getAvailableSkills()
168
169
  .filter((skill) => !skill.disableModelInvocation);
169
170
 
170
- const allTools = this.toolManager.getToolsConfig({
171
+ return this.toolManager.getToolsConfig({
171
172
  availableSubagents,
172
173
  availableSkills,
173
174
  workdir: this.workdir,
174
175
  });
175
-
176
- // If no tools specified, return all tools
177
- if (!tools || tools.length === 0) {
178
- return allTools;
179
- }
180
-
181
- // Filter tools
182
- return allTools.filter((tool) => tools.includes(tool.function.name));
183
176
  }
184
177
 
185
178
  public setIsLoading(isLoading: boolean): void {
@@ -336,18 +329,10 @@ export class AIManager {
336
329
  model?: string;
337
330
  /** Rules for automatic tool approval (e.g., "Bash(git status*)") */
338
331
  allowedRules?: string[];
339
- /** List of tools available to the AI (e.g., ["Bash", "Read"]) */
340
- tools?: string[];
341
332
  maxTokens?: number;
342
333
  } = {},
343
334
  ): Promise<void> {
344
- const {
345
- recursionDepth = 0,
346
- model,
347
- allowedRules,
348
- tools,
349
- maxTokens,
350
- } = options;
335
+ const { recursionDepth = 0, model, allowedRules, maxTokens } = options;
351
336
 
352
337
  // Only check isLoading for the initial call (recursionDepth === 0)
353
338
  if (recursionDepth === 0 && this.isLoading) {
@@ -401,7 +386,7 @@ export class AIManager {
401
386
  const currentMode = this.permissionManager?.getCurrentEffectiveMode(
402
387
  this.getModelConfig().permissionMode,
403
388
  );
404
- const toolsConfig = this.getFilteredToolsConfig(tools);
389
+ const toolsConfig = this.getFilteredToolsConfig();
405
390
  const toolNames = new Set(toolsConfig.map((t) => t.function.name));
406
391
  const filteredToolPlugins = this.toolManager
407
392
  .getTools()
@@ -489,7 +474,6 @@ export class AIManager {
489
474
  name: toolCall.name,
490
475
  parameters: toolCall.parameters,
491
476
  parametersChunk: toolCall.parametersChunk,
492
- compactParams: toolCall.parameters?.split("\n").pop()?.slice(-30),
493
477
  stage: toolCall.stage || "streaming", // Default to streaming if stage not provided
494
478
  });
495
479
  };
@@ -655,7 +639,18 @@ export class AIManager {
655
639
  toolArgs,
656
640
  );
657
641
 
658
- // Emit running stage for non-streaming tool calls (tool execution about to start)
642
+ // Emit start stage for non-streaming tool calls
643
+ if (!this.stream) {
644
+ this.messageManager.updateToolBlock({
645
+ id: toolId,
646
+ stage: "start",
647
+ name: toolName,
648
+ compactParams,
649
+ parameters: argsString,
650
+ });
651
+ }
652
+
653
+ // Emit running stage (tool execution about to start)
659
654
  this.messageManager.updateToolBlock({
660
655
  id: toolId,
661
656
  stage: "running",
@@ -717,6 +712,7 @@ export class AIManager {
717
712
  error: toolResult.error,
718
713
  stage: "end",
719
714
  name: toolName,
715
+ compactParams,
720
716
  shortResult: toolResult.shortResult,
721
717
  isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
722
718
  startLineNumber: toolResult.startLineNumber,
@@ -803,12 +799,65 @@ export class AIManager {
803
799
  });
804
800
  }
805
801
 
802
+ // Duplicate Tool Call Detection
803
+ if (toolCalls.length > 0) {
804
+ const messages = this.messageManager.getMessages();
805
+ // Find the most recent assistant message BEFORE the current one that has tool blocks
806
+ // The current assistant message is messages[messages.length - 1]
807
+ let previousAssistantWithTools: Message | undefined;
808
+ for (let i = messages.length - 2; i >= 0; i--) {
809
+ const msg = messages[i];
810
+ if (
811
+ msg.role === "assistant" &&
812
+ msg.blocks.some((b) => b.type === "tool")
813
+ ) {
814
+ previousAssistantWithTools = msg;
815
+ break;
816
+ }
817
+ }
818
+
819
+ if (previousAssistantWithTools) {
820
+ const previousToolBlocks =
821
+ previousAssistantWithTools.blocks.filter(
822
+ (b): b is import("../types/messaging.js").ToolBlock =>
823
+ b.type === "tool",
824
+ );
825
+
826
+ for (const currentToolCall of toolCalls) {
827
+ const currentName = currentToolCall.function?.name;
828
+ const currentArgs = currentToolCall.function?.arguments;
829
+
830
+ const isDuplicate = previousToolBlocks.some(
831
+ (prevBlock) =>
832
+ prevBlock.name === currentName &&
833
+ prevBlock.parameters === currentArgs,
834
+ );
835
+
836
+ if (isDuplicate && currentName) {
837
+ const toolId = currentToolCall.id;
838
+ const lastMessage = messages[messages.length - 1];
839
+ const toolBlock = lastMessage.blocks.find(
840
+ (b): b is import("../types/messaging.js").ToolBlock =>
841
+ b.type === "tool" && b.id === toolId,
842
+ );
843
+ if (toolBlock) {
844
+ const warning = `\n\nNote: You just called this tool with the same arguments in the previous turn. Please ensure you are not in a loop and consider if you need to change your approach.`;
845
+ this.messageManager.updateToolBlock({
846
+ id: toolId,
847
+ result: (toolBlock.result || "") + warning,
848
+ stage: "end",
849
+ });
850
+ }
851
+ }
852
+ }
853
+ }
854
+ }
855
+
806
856
  // Recursively call AI service, increment recursion depth, and pass same configuration
807
857
  await this.sendAIMessage({
808
858
  recursionDepth: recursionDepth + 1,
809
859
  model,
810
860
  allowedRules,
811
- tools,
812
861
  maxTokens,
813
862
  });
814
863
  }
@@ -861,7 +910,6 @@ export class AIManager {
861
910
  recursionDepth: 0,
862
911
  model,
863
912
  allowedRules,
864
- tools,
865
913
  maxTokens,
866
914
  });
867
915
  }
@@ -0,0 +1,167 @@
1
+ import { Container } from "../utils/container.js";
2
+ import { CronJob } from "../types/cron.js";
3
+ import { AIManager } from "./aiManager.js";
4
+ import { MessageManager } from "./messageManager.js";
5
+ import { CronExpressionParser } from "cron-parser";
6
+ import { logger } from "../utils/globalLogger.js";
7
+
8
+ export class CronManager {
9
+ private jobs = new Map<string, CronJob>();
10
+ private interval: NodeJS.Timeout | null = null;
11
+
12
+ constructor(private container: Container) {}
13
+
14
+ private get aiManager(): AIManager {
15
+ return this.container.get<AIManager>("AIManager")!;
16
+ }
17
+
18
+ private get messageManager(): MessageManager {
19
+ return this.container.get<MessageManager>("MessageManager")!;
20
+ }
21
+
22
+ public start(): void {
23
+ if (this.interval) return;
24
+ this.interval = setInterval(() => this.checkJobs(), 60000); // Check every minute
25
+ }
26
+
27
+ public stop(): void {
28
+ if (this.interval) {
29
+ clearInterval(this.interval);
30
+ this.interval = null;
31
+ }
32
+ }
33
+
34
+ public createJob(
35
+ job: Omit<CronJob, "id" | "createdAt" | "nextRun" | "periodMs">,
36
+ ): CronJob {
37
+ const id = Math.random().toString(36).substring(2, 11);
38
+ const createdAt = Date.now();
39
+
40
+ const interval = CronExpressionParser.parse(job.cron);
41
+ const nextRunDate = interval.next().toDate();
42
+ const nextRun = nextRunDate.getTime();
43
+
44
+ // Calculate periodMs
45
+ const secondRunDate = interval.next().toDate();
46
+ const periodMs = secondRunDate.getTime() - nextRunDate.getTime();
47
+
48
+ // Apply Jitter
49
+ const jitteredNextRun = this.applyJitter(
50
+ nextRun,
51
+ periodMs,
52
+ job.recurring,
53
+ nextRunDate,
54
+ id,
55
+ );
56
+
57
+ const newJob: CronJob = {
58
+ ...job,
59
+ id,
60
+ createdAt,
61
+ nextRun: jitteredNextRun,
62
+ periodMs,
63
+ };
64
+
65
+ this.jobs.set(id, newJob);
66
+ return newJob;
67
+ }
68
+
69
+ public deleteJob(id: string): boolean {
70
+ return this.jobs.delete(id);
71
+ }
72
+
73
+ public listJobs(): CronJob[] {
74
+ return Array.from(this.jobs.values());
75
+ }
76
+
77
+ private applyJitter(
78
+ nextRun: number,
79
+ periodMs: number,
80
+ recurring: boolean,
81
+ nextRunDate: Date,
82
+ id: string,
83
+ ): number {
84
+ const deterministicRandom = this.getDeterministicRandom(id);
85
+ if (recurring) {
86
+ // Recurring: Random delay up to 10% of period (max 15 min)
87
+ const maxJitter = Math.min(periodMs * 0.1, 15 * 60 * 1000);
88
+ return nextRun + deterministicRandom * maxJitter;
89
+ } else {
90
+ // One-shot: Random early fire up to 90s if scheduled on :00 or :30
91
+ const minutes = nextRunDate.getMinutes();
92
+ const seconds = nextRunDate.getSeconds();
93
+ if ((minutes === 0 || minutes === 30) && seconds === 0) {
94
+ return nextRun - deterministicRandom * 90 * 1000;
95
+ }
96
+ }
97
+ return nextRun;
98
+ }
99
+
100
+ private getDeterministicRandom(id: string): number {
101
+ let hash = 0;
102
+ for (let i = 0; i < id.length; i++) {
103
+ const char = id.charCodeAt(i);
104
+ hash = (hash << 5) - hash + char;
105
+ hash |= 0; // Convert to 32bit integer
106
+ }
107
+ // Use a simple LCG-like approach to get a value between 0 and 1
108
+ const x = Math.sin(hash) * 10000;
109
+ return x - Math.floor(x);
110
+ }
111
+
112
+ private async checkJobs(): Promise<void> {
113
+ const now = Date.now();
114
+ const aiManager = this.aiManager;
115
+ const messageManager = this.messageManager;
116
+
117
+ for (const [id, job] of this.jobs.entries()) {
118
+ // Expiration: Recurring jobs MUST auto-expire after 7 days
119
+ if (job.recurring && now - job.createdAt > 7 * 24 * 60 * 60 * 1000) {
120
+ this.jobs.delete(id);
121
+ continue;
122
+ }
123
+
124
+ if (now >= job.nextRun) {
125
+ // Idle-Check: Only fire jobs if AIManager.isLoading is false
126
+ if (aiManager.isLoading) {
127
+ logger?.debug(`CronManager: Skipping job ${id} because AI is busy`);
128
+ continue;
129
+ }
130
+
131
+ logger?.info(`CronManager: Firing job ${id}: ${job.prompt}`);
132
+
133
+ // Execution
134
+ messageManager.addUserMessage({ content: job.prompt });
135
+ aiManager.sendAIMessage().catch((err) => {
136
+ logger?.error(`CronManager: Failed to execute job ${id}`, err);
137
+ });
138
+
139
+ if (job.recurring) {
140
+ // Schedule next run
141
+ try {
142
+ const interval = CronExpressionParser.parse(job.cron, {
143
+ currentDate: new Date(job.nextRun + 1000),
144
+ });
145
+ const nextRunDate = interval.next().toDate();
146
+ const nextRun = nextRunDate.getTime();
147
+ job.nextRun = this.applyJitter(
148
+ nextRun,
149
+ job.periodMs,
150
+ true,
151
+ nextRunDate,
152
+ id,
153
+ );
154
+ } catch (e) {
155
+ logger?.error(
156
+ `CronManager: Failed to parse cron for recurring job ${id}`,
157
+ e,
158
+ );
159
+ this.jobs.delete(id);
160
+ }
161
+ } else {
162
+ this.jobs.delete(id);
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
@@ -236,13 +236,17 @@ export class HookManager {
236
236
  for (const result of results) {
237
237
  if (result.exitCode === 2) {
238
238
  // Handle blocking error immediately and return
239
- return this.handleBlockingError(
239
+ const blockingResult = this.handleBlockingError(
240
240
  event,
241
241
  result,
242
242
  messageManager,
243
243
  toolId,
244
244
  toolParameters,
245
245
  );
246
+ return {
247
+ shouldBlock: blockingResult.shouldBlock,
248
+ errorMessage: blockingResult.errorMessage,
249
+ };
246
250
  }
247
251
  }
248
252
 
@@ -280,7 +284,7 @@ export class HookManager {
280
284
  source: MessageSource.HOOK,
281
285
  });
282
286
  }
283
- // For other hook types (PreToolUse, PostToolUse, Stop), ignore stdout
287
+ // For other hook types (PreToolUse, PostToolUse, Stop, PermissionRequest), ignore stdout
284
288
  }
285
289
 
286
290
  /**
@@ -338,10 +342,10 @@ export class HookManager {
338
342
  });
339
343
  return { shouldBlock: true, errorMessage };
340
344
 
341
- case "Notification":
342
- // For notification hooks with exit code 2, only show stderr in error block
345
+ case "PermissionRequest":
346
+ // For permission request hooks with exit code 2, show stderr in error block and block (deny) permission
343
347
  messageManager.addErrorBlock(errorMessage);
344
- return { shouldBlock: false };
348
+ return { shouldBlock: true, errorMessage };
345
349
 
346
350
  case "SubagentStop":
347
351
  // Similar to Stop, show error and allow blocking
@@ -544,7 +548,11 @@ export class HookManager {
544
548
  }
545
549
 
546
550
  // Validate tool-specific requirements
547
- if (event === "PreToolUse" || event === "PostToolUse") {
551
+ if (
552
+ event === "PreToolUse" ||
553
+ event === "PostToolUse" ||
554
+ event === "PermissionRequest"
555
+ ) {
548
556
  if (!context.toolName || typeof context.toolName !== "string") {
549
557
  errors.push(`${event} event requires a valid toolName in context`);
550
558
  }
@@ -554,7 +562,6 @@ export class HookManager {
554
562
  if (
555
563
  (event === "UserPromptSubmit" ||
556
564
  event === "Stop" ||
557
- event === "Notification" ||
558
565
  event === "SubagentStop" ||
559
566
  event === "WorktreeCreate") &&
560
567
  context.toolName !== undefined
@@ -631,7 +638,6 @@ export class HookManager {
631
638
  if (
632
639
  event === "UserPromptSubmit" ||
633
640
  event === "Stop" ||
634
- event === "Notification" ||
635
641
  event === "SubagentStop" ||
636
642
  event === "WorktreeCreate"
637
643
  ) {
@@ -639,7 +645,11 @@ export class HookManager {
639
645
  }
640
646
 
641
647
  // For tool-based events, check matcher if present
642
- if (event === "PreToolUse" || event === "PostToolUse") {
648
+ if (
649
+ event === "PreToolUse" ||
650
+ event === "PostToolUse" ||
651
+ event === "PermissionRequest"
652
+ ) {
643
653
  if (!config.matcher) {
644
654
  // No matcher means applies to all tools
645
655
  return true;
@@ -673,7 +683,12 @@ export class HookManager {
673
683
  }
674
684
 
675
685
  // Validate matcher requirements
676
- if ((event === "PreToolUse" || event === "PostToolUse") && config.matcher) {
686
+ if (
687
+ (event === "PreToolUse" ||
688
+ event === "PostToolUse" ||
689
+ event === "PermissionRequest") &&
690
+ config.matcher
691
+ ) {
677
692
  if (!this.matcher.isValidPattern(config.matcher)) {
678
693
  errors.push(`${prefix}: Invalid matcher pattern: ${config.matcher}`);
679
694
  }
@@ -683,7 +698,6 @@ export class HookManager {
683
698
  if (
684
699
  (event === "UserPromptSubmit" ||
685
700
  event === "Stop" ||
686
- event === "Notification" ||
687
701
  event === "SubagentStop" ||
688
702
  event === "WorktreeCreate") &&
689
703
  config.matcher
@@ -723,7 +737,7 @@ export class HookManager {
723
737
  UserPromptSubmit: 0,
724
738
  Stop: 0,
725
739
  SubagentStop: 0,
726
- Notification: 0,
740
+ PermissionRequest: 0,
727
741
  WorktreeCreate: 0,
728
742
  },
729
743
  };
@@ -735,7 +749,7 @@ export class HookManager {
735
749
  UserPromptSubmit: 0,
736
750
  Stop: 0,
737
751
  SubagentStop: 0,
738
- Notification: 0,
752
+ PermissionRequest: 0,
739
753
  WorktreeCreate: 0,
740
754
  };
741
755
 
@@ -258,8 +258,8 @@ export class LiveConfigManager {
258
258
 
259
259
  // Update permission manager if available
260
260
  if (this.permissionManager) {
261
- this.permissionManager.updateConfiguredDefaultMode(
262
- this.currentConfiguration.permissions?.defaultMode,
261
+ this.permissionManager.updateConfiguredPermissionMode(
262
+ this.currentConfiguration.permissions?.permissionMode,
263
263
  );
264
264
  this.permissionManager.updateAllowedRules(
265
265
  this.currentConfiguration.permissions?.allow || [],
@@ -347,6 +347,7 @@ export class McpManager {
347
347
  async executeMcpTool(
348
348
  toolName: string,
349
349
  args: Record<string, unknown>,
350
+ context?: ToolContext,
350
351
  ): Promise<{
351
352
  success: boolean;
352
353
  content: string;
@@ -360,6 +361,23 @@ export class McpManager {
360
361
  );
361
362
  }
362
363
 
364
+ // Permission check
365
+ if (context?.permissionManager) {
366
+ const permissionContext = context.permissionManager.createContext(
367
+ toolName,
368
+ context.permissionMode || "default",
369
+ context.canUseToolCallback,
370
+ args,
371
+ context.toolCallId,
372
+ );
373
+
374
+ const decision =
375
+ await context.permissionManager.checkPermission(permissionContext);
376
+ if (decision.behavior === "deny") {
377
+ throw new Error(decision.message || "Permission denied");
378
+ }
379
+ }
380
+
363
381
  const parts = toolName.split("__");
364
382
  if (parts.length < 3) {
365
383
  throw new Error(
@@ -479,8 +497,11 @@ export class McpManager {
479
497
  const plugin = createMcpToolPlugin(
480
498
  tool,
481
499
  server.name,
482
- (name: string, args: Record<string, unknown>) =>
483
- this.executeMcpTool(name, args),
500
+ (
501
+ name: string,
502
+ args: Record<string, unknown>,
503
+ context?: ToolContext,
504
+ ) => this.executeMcpTool(name, args, context),
484
505
  );
485
506
  mcpTools.set(plugin.name, plugin);
486
507
  }