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.
- package/dist/agent.d.ts +8 -6
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -9
- package/dist/builtin-skills/builtin-skills/loop/SKILL.md +53 -0
- package/dist/builtin-skills/builtin-skills/loop/parsing.ts +159 -0
- package/dist/builtin-skills/builtin-skills/settings/HOOKS.md +82 -0
- package/dist/builtin-skills/{settings → builtin-skills/settings}/SKILL.md +1 -1
- package/dist/builtin-skills/loop/parsing.d.ts +13 -0
- package/dist/builtin-skills/loop/parsing.d.ts.map +1 -0
- package/dist/builtin-skills/loop/parsing.js +125 -0
- package/dist/constants/tools.d.ts +3 -0
- package/dist/constants/tools.d.ts.map +1 -1
- package/dist/constants/tools.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +0 -2
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +53 -14
- package/dist/managers/cronManager.d.ts +19 -0
- package/dist/managers/cronManager.d.ts.map +1 -0
- package/dist/managers/cronManager.js +124 -0
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +21 -13
- package/dist/managers/liveConfigManager.js +1 -1
- package/dist/managers/mcpManager.d.ts +1 -1
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +10 -2
- package/dist/managers/messageManager.d.ts +0 -1
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.d.ts +27 -7
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +119 -14
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +11 -0
- package/dist/managers/subagentManager.d.ts +3 -0
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +10 -17
- package/dist/managers/toolManager.d.ts +1 -1
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +28 -4
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +8 -7
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +3 -10
- package/dist/services/initializationService.js +2 -2
- package/dist/services/jsonlHandler.d.ts.map +1 -1
- package/dist/services/jsonlHandler.js +3 -0
- package/dist/services/reversionService.d.ts +2 -2
- package/dist/services/reversionService.d.ts.map +1 -1
- package/dist/services/reversionService.js +3 -3
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +18 -11
- package/dist/tools/agentTool.js +1 -1
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +33 -12
- package/dist/tools/cronCreateTool.d.ts +3 -0
- package/dist/tools/cronCreateTool.d.ts.map +1 -0
- package/dist/tools/cronCreateTool.js +59 -0
- package/dist/tools/cronDeleteTool.d.ts +3 -0
- package/dist/tools/cronDeleteTool.d.ts.map +1 -0
- package/dist/tools/cronDeleteTool.js +38 -0
- package/dist/tools/cronListTool.d.ts +3 -0
- package/dist/tools/cronListTool.d.ts.map +1 -0
- package/dist/tools/cronListTool.js +30 -0
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +8 -32
- package/dist/tools/skillTool.d.ts +0 -3
- package/dist/tools/skillTool.d.ts.map +1 -1
- package/dist/tools/skillTool.js +4 -3
- package/dist/tools/taskOutputTool.d.ts.map +1 -1
- package/dist/tools/taskOutputTool.js +15 -8
- package/dist/tools/types.d.ts +2 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/types/agent.d.ts +10 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/configuration.d.ts +1 -1
- package/dist/types/configuration.d.ts.map +1 -1
- package/dist/types/cron.d.ts +10 -0
- package/dist/types/cron.d.ts.map +1 -0
- package/dist/types/cron.js +1 -0
- package/dist/types/hooks.d.ts +1 -5
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/messaging.d.ts +1 -1
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +40 -13
- package/dist/utils/mcpUtils.d.ts +2 -2
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +1 -5
- package/package.json +2 -1
- package/src/agent.ts +17 -12
- package/src/builtin-skills/loop/SKILL.md +53 -0
- package/src/builtin-skills/loop/parsing.ts +159 -0
- package/src/builtin-skills/settings/HOOKS.md +44 -57
- package/src/builtin-skills/settings/SKILL.md +1 -1
- package/src/constants/tools.ts +3 -0
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +72 -24
- package/src/managers/cronManager.ts +167 -0
- package/src/managers/hookManager.ts +27 -13
- package/src/managers/liveConfigManager.ts +2 -2
- package/src/managers/mcpManager.ts +23 -2
- package/src/managers/messageManager.ts +0 -6
- package/src/managers/permissionManager.ts +154 -18
- package/src/managers/slashCommandManager.ts +12 -0
- package/src/managers/subagentManager.ts +15 -19
- package/src/managers/toolManager.ts +37 -4
- package/src/services/configurationService.ts +8 -7
- package/src/services/hook.ts +5 -11
- package/src/services/initializationService.ts +3 -3
- package/src/services/jsonlHandler.ts +4 -0
- package/src/services/reversionService.ts +9 -4
- package/src/services/session.ts +19 -12
- package/src/tools/agentTool.ts +1 -1
- package/src/tools/bashTool.ts +43 -14
- package/src/tools/cronCreateTool.ts +73 -0
- package/src/tools/cronDeleteTool.ts +47 -0
- package/src/tools/cronListTool.ts +38 -0
- package/src/tools/readTool.ts +11 -33
- package/src/tools/skillTool.ts +6 -4
- package/src/tools/taskOutputTool.ts +14 -8
- package/src/tools/types.ts +2 -0
- package/src/types/agent.ts +10 -0
- package/src/types/configuration.ts +1 -1
- package/src/types/cron.ts +9 -0
- package/src/types/hooks.ts +5 -9
- package/src/types/index.ts +1 -0
- package/src/types/messaging.ts +1 -1
- package/src/utils/containerSetup.ts +50 -16
- package/src/utils/mcpUtils.ts +2 -5
- 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
|
|
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
|
-
"
|
|
24
|
+
"PreToolUse": [
|
|
24
25
|
{
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
"matcher": "Write",
|
|
27
|
+
"hooks": [
|
|
28
|
+
{
|
|
29
|
+
"command": "pnpm lint",
|
|
30
|
+
"description": "Run lint before writing files"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
29
33
|
}
|
|
30
34
|
],
|
|
31
|
-
"
|
|
35
|
+
"PermissionRequest": [
|
|
32
36
|
{
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
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
|
-
- `
|
|
45
|
-
- `
|
|
46
|
-
- `
|
|
47
|
-
- `
|
|
48
|
-
- `
|
|
49
|
-
- `
|
|
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
|
-
##
|
|
59
|
+
## Hook Input JSON
|
|
52
60
|
|
|
53
|
-
|
|
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
|
-
###
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
###
|
|
84
|
-
|
|
85
|
-
- `
|
|
86
|
-
- `
|
|
87
|
-
- `
|
|
88
|
-
- `
|
|
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
|
|
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`.
|
package/src/constants/tools.ts
CHANGED
|
@@ -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
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 "
|
|
342
|
-
// For
|
|
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:
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
262
|
-
this.currentConfiguration.permissions?.
|
|
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
|
-
(
|
|
483
|
-
|
|
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
|
}
|