wave-agent-sdk 0.10.4 → 0.11.1

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 (155) hide show
  1. package/builtin/skills/init/SKILL.md +26 -0
  2. package/builtin/skills/loop/SKILL.md +53 -0
  3. package/builtin/skills/settings/ENV.md +64 -0
  4. package/builtin/skills/settings/HOOKS.md +94 -0
  5. package/builtin/skills/settings/MCP.md +55 -0
  6. package/builtin/skills/settings/MEMORY_RULES.md +60 -0
  7. package/{dist/builtin-skills → builtin/skills}/settings/SKILL.md +23 -16
  8. package/builtin/skills/settings/SKILLS.md +63 -0
  9. package/builtin/skills/settings/SUBAGENTS.md +60 -0
  10. package/builtin/subagents/bash.md +18 -0
  11. package/builtin/subagents/explore.md +42 -0
  12. package/builtin/subagents/general-purpose.md +20 -0
  13. package/builtin/subagents/plan.md +55 -0
  14. package/dist/agent.d.ts +8 -6
  15. package/dist/agent.d.ts.map +1 -1
  16. package/dist/agent.js +12 -9
  17. package/dist/constants/tools.d.ts +3 -0
  18. package/dist/constants/tools.d.ts.map +1 -1
  19. package/dist/constants/tools.js +3 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/managers/aiManager.d.ts +0 -2
  24. package/dist/managers/aiManager.d.ts.map +1 -1
  25. package/dist/managers/aiManager.js +53 -14
  26. package/dist/managers/cronManager.d.ts +19 -0
  27. package/dist/managers/cronManager.d.ts.map +1 -0
  28. package/dist/managers/cronManager.js +124 -0
  29. package/dist/managers/hookManager.d.ts.map +1 -1
  30. package/dist/managers/hookManager.js +21 -13
  31. package/dist/managers/liveConfigManager.js +1 -1
  32. package/dist/managers/mcpManager.d.ts +1 -1
  33. package/dist/managers/mcpManager.d.ts.map +1 -1
  34. package/dist/managers/mcpManager.js +10 -2
  35. package/dist/managers/messageManager.d.ts +0 -1
  36. package/dist/managers/messageManager.d.ts.map +1 -1
  37. package/dist/managers/permissionManager.d.ts +27 -7
  38. package/dist/managers/permissionManager.d.ts.map +1 -1
  39. package/dist/managers/permissionManager.js +119 -14
  40. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  41. package/dist/managers/slashCommandManager.js +7 -12
  42. package/dist/managers/subagentManager.d.ts +3 -0
  43. package/dist/managers/subagentManager.d.ts.map +1 -1
  44. package/dist/managers/subagentManager.js +10 -17
  45. package/dist/managers/toolManager.d.ts +1 -1
  46. package/dist/managers/toolManager.d.ts.map +1 -1
  47. package/dist/managers/toolManager.js +28 -4
  48. package/dist/prompts/index.d.ts +0 -5
  49. package/dist/prompts/index.d.ts.map +1 -1
  50. package/dist/prompts/index.js +1 -136
  51. package/dist/services/configurationService.d.ts.map +1 -1
  52. package/dist/services/configurationService.js +8 -7
  53. package/dist/services/hook.d.ts.map +1 -1
  54. package/dist/services/hook.js +3 -10
  55. package/dist/services/initializationService.js +2 -2
  56. package/dist/services/jsonlHandler.d.ts.map +1 -1
  57. package/dist/services/jsonlHandler.js +3 -0
  58. package/dist/services/reversionService.d.ts +2 -2
  59. package/dist/services/reversionService.d.ts.map +1 -1
  60. package/dist/services/reversionService.js +3 -3
  61. package/dist/services/session.d.ts.map +1 -1
  62. package/dist/services/session.js +18 -11
  63. package/dist/tools/agentTool.js +1 -1
  64. package/dist/tools/bashTool.d.ts.map +1 -1
  65. package/dist/tools/bashTool.js +5 -5
  66. package/dist/tools/cronCreateTool.d.ts +3 -0
  67. package/dist/tools/cronCreateTool.d.ts.map +1 -0
  68. package/dist/tools/cronCreateTool.js +59 -0
  69. package/dist/tools/cronDeleteTool.d.ts +3 -0
  70. package/dist/tools/cronDeleteTool.d.ts.map +1 -0
  71. package/dist/tools/cronDeleteTool.js +38 -0
  72. package/dist/tools/cronListTool.d.ts +3 -0
  73. package/dist/tools/cronListTool.d.ts.map +1 -0
  74. package/dist/tools/cronListTool.js +30 -0
  75. package/dist/tools/skillTool.d.ts +0 -3
  76. package/dist/tools/skillTool.d.ts.map +1 -1
  77. package/dist/tools/skillTool.js +4 -3
  78. package/dist/tools/taskOutputTool.d.ts.map +1 -1
  79. package/dist/tools/taskOutputTool.js +15 -8
  80. package/dist/tools/types.d.ts +2 -0
  81. package/dist/tools/types.d.ts.map +1 -1
  82. package/dist/types/agent.d.ts +10 -0
  83. package/dist/types/agent.d.ts.map +1 -1
  84. package/dist/types/configuration.d.ts +1 -1
  85. package/dist/types/configuration.d.ts.map +1 -1
  86. package/dist/types/cron.d.ts +10 -0
  87. package/dist/types/cron.d.ts.map +1 -0
  88. package/dist/types/cron.js +1 -0
  89. package/dist/types/hooks.d.ts +1 -5
  90. package/dist/types/hooks.d.ts.map +1 -1
  91. package/dist/types/hooks.js +1 -1
  92. package/dist/types/index.d.ts +1 -0
  93. package/dist/types/index.d.ts.map +1 -1
  94. package/dist/types/index.js +1 -0
  95. package/dist/types/messaging.d.ts +1 -1
  96. package/dist/types/messaging.d.ts.map +1 -1
  97. package/dist/utils/configPaths.d.ts +4 -0
  98. package/dist/utils/configPaths.d.ts.map +1 -1
  99. package/dist/utils/configPaths.js +11 -9
  100. package/dist/utils/containerSetup.d.ts.map +1 -1
  101. package/dist/utils/containerSetup.js +40 -13
  102. package/dist/utils/fileSearch.d.ts.map +1 -1
  103. package/dist/utils/fileSearch.js +7 -1
  104. package/dist/utils/mcpUtils.d.ts +2 -2
  105. package/dist/utils/mcpUtils.d.ts.map +1 -1
  106. package/dist/utils/mcpUtils.js +1 -5
  107. package/dist/utils/subagentParser.d.ts.map +1 -1
  108. package/dist/utils/subagentParser.js +14 -4
  109. package/package.json +4 -2
  110. package/src/agent.ts +17 -12
  111. package/src/constants/tools.ts +3 -0
  112. package/src/index.ts +1 -0
  113. package/src/managers/aiManager.ts +72 -24
  114. package/src/managers/cronManager.ts +167 -0
  115. package/src/managers/hookManager.ts +27 -13
  116. package/src/managers/liveConfigManager.ts +2 -2
  117. package/src/managers/mcpManager.ts +23 -2
  118. package/src/managers/messageManager.ts +0 -6
  119. package/src/managers/permissionManager.ts +154 -18
  120. package/src/managers/slashCommandManager.ts +7 -14
  121. package/src/managers/subagentManager.ts +15 -19
  122. package/src/managers/toolManager.ts +37 -4
  123. package/src/prompts/index.ts +0 -144
  124. package/src/services/configurationService.ts +8 -7
  125. package/src/services/hook.ts +5 -11
  126. package/src/services/initializationService.ts +3 -3
  127. package/src/services/jsonlHandler.ts +4 -0
  128. package/src/services/reversionService.ts +9 -4
  129. package/src/services/session.ts +19 -12
  130. package/src/tools/agentTool.ts +1 -1
  131. package/src/tools/bashTool.ts +6 -5
  132. package/src/tools/cronCreateTool.ts +73 -0
  133. package/src/tools/cronDeleteTool.ts +47 -0
  134. package/src/tools/cronListTool.ts +38 -0
  135. package/src/tools/skillTool.ts +6 -4
  136. package/src/tools/taskOutputTool.ts +14 -8
  137. package/src/tools/types.ts +2 -0
  138. package/src/types/agent.ts +10 -0
  139. package/src/types/configuration.ts +1 -1
  140. package/src/types/cron.ts +9 -0
  141. package/src/types/hooks.ts +5 -9
  142. package/src/types/index.ts +1 -0
  143. package/src/types/messaging.ts +1 -1
  144. package/src/utils/configPaths.ts +12 -10
  145. package/src/utils/containerSetup.ts +50 -16
  146. package/src/utils/fileSearch.ts +7 -1
  147. package/src/utils/mcpUtils.ts +2 -5
  148. package/src/utils/subagentParser.ts +16 -6
  149. package/dist/builtin-skills/settings/HOOKS.md +0 -95
  150. package/dist/utils/builtinSubagents.d.ts +0 -7
  151. package/dist/utils/builtinSubagents.d.ts.map +0 -1
  152. package/dist/utils/builtinSubagents.js +0 -94
  153. package/src/builtin-skills/settings/HOOKS.md +0 -95
  154. package/src/builtin-skills/settings/SKILL.md +0 -86
  155. package/src/utils/builtinSubagents.ts +0 -122
@@ -1 +1 @@
1
- {"version":3,"file":"subagentParser.d.ts","sourceRoot":"","sources":["../../src/utils/subagentParser.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;CAClB;AA+KD;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAsBlC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAGvC"}
1
+ {"version":3,"file":"subagentParser.d.ts","sourceRoot":"","sources":["../../src/utils/subagentParser.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAwLD;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAsBlC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAGvC"}
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, readdirSync, statSync } from "fs";
2
- import { join, extname } from "path";
2
+ import { join, extname, basename } from "path";
3
3
  import { logger } from "./globalLogger.js";
4
+ import { getBuiltinSubagentsDir } from "./configPaths.js";
4
5
  /**
5
6
  * Parse YAML frontmatter from markdown file content
6
7
  */
@@ -85,10 +86,19 @@ function parseSubagentFile(filePath, scope) {
85
86
  try {
86
87
  const content = readFileSync(filePath, "utf-8");
87
88
  const { frontmatter, body } = parseFrontmatter(content);
89
+ // Use filename as default name if not specified in frontmatter
90
+ if (!frontmatter.name) {
91
+ frontmatter.name = basename(filePath, extname(filePath));
92
+ }
88
93
  validateConfiguration(frontmatter, filePath);
89
94
  if (!body.trim()) {
90
95
  throw new Error(`Empty system prompt in ${filePath}`);
91
96
  }
97
+ let priority = 1;
98
+ if (scope === "user")
99
+ priority = 2;
100
+ if (scope === "builtin")
101
+ priority = 3;
92
102
  return {
93
103
  name: frontmatter.name,
94
104
  description: frontmatter.description,
@@ -97,7 +107,7 @@ function parseSubagentFile(filePath, scope) {
97
107
  systemPrompt: body,
98
108
  filePath,
99
109
  scope,
100
- priority: scope === "project" ? 1 : 2,
110
+ priority,
101
111
  };
102
112
  }
103
113
  catch (error) {
@@ -137,9 +147,9 @@ function scanSubagentDirectory(dirPath, scope) {
137
147
  export async function loadSubagentConfigurations(workdir) {
138
148
  const projectDir = join(workdir, ".wave", "agents");
139
149
  const userDir = join(process.env.HOME || "~", ".wave", "agents");
150
+ const builtinDir = getBuiltinSubagentsDir();
140
151
  // Load configurations from all sources
141
- const { getBuiltinSubagents } = await import("./builtinSubagents.js");
142
- const builtinConfigs = getBuiltinSubagents();
152
+ const builtinConfigs = scanSubagentDirectory(builtinDir, "builtin");
143
153
  const projectConfigs = scanSubagentDirectory(projectDir, "project");
144
154
  const userConfigs = scanSubagentDirectory(userDir, "user");
145
155
  // Merge configurations, with project configs taking highest precedence
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.10.4",
3
+ "version": "0.11.1",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -24,11 +24,13 @@
24
24
  "bin",
25
25
  "vendor",
26
26
  "scripts",
27
+ "builtin",
27
28
  "README.md"
28
29
  ],
29
30
  "dependencies": {
30
31
  "@modelcontextprotocol/sdk": "^1.18.2",
31
32
  "chokidar": "^5.0.0",
33
+ "cron-parser": "^5.5.0",
32
34
  "fuzzysort": "^3.1.0",
33
35
  "glob": "^13.0.0",
34
36
  "minimatch": "^10.0.3",
@@ -47,7 +49,7 @@
47
49
  "license": "MIT",
48
50
  "scripts": {
49
51
  "postinstall": "node scripts/postinstall.js",
50
- "build": "rimraf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && cp -r src/builtin-skills dist/builtin-skills",
52
+ "build": "rimraf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
51
53
  "type-check": "tsc --noEmit --incremental",
52
54
  "watch": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json --watch",
53
55
  "test": "vitest run --reporter=dot",
package/src/agent.ts CHANGED
@@ -6,6 +6,7 @@ import { SubagentManager } from "./managers/subagentManager.js";
6
6
  import { McpManager } from "./managers/mcpManager.js";
7
7
  import { LspManager } from "./managers/lspManager.js";
8
8
  import { BangManager } from "./managers/bangManager.js";
9
+ import { CronManager } from "./managers/cronManager.js";
9
10
  import { BackgroundTaskManager } from "./managers/backgroundTaskManager.js";
10
11
  import { SlashCommandManager } from "./managers/slashCommandManager.js";
11
12
  import { PluginManager } from "./managers/pluginManager.js";
@@ -56,6 +57,7 @@ export class Agent {
56
57
  private slashCommandManager: SlashCommandManager; // Add slash command manager instance
57
58
  private pluginManager: PluginManager; // Add plugin manager instance
58
59
  private skillManager: SkillManager; // Add skill manager instance
60
+ private cronManager: CronManager; // Add cron manager instance
59
61
  private hookManager: HookManager; // Add hooks manager instance
60
62
  private reversionManager: ReversionManager;
61
63
  private memoryRuleManager: MemoryRuleManager; // Add memory rule manager instance
@@ -168,6 +170,7 @@ export class Agent {
168
170
  this.slashCommandManager = this.container.get("SlashCommandManager")!;
169
171
  this.pluginManager = this.container.get("PluginManager")!;
170
172
  this.bangManager = this.container.get("BangManager")!;
173
+ this.cronManager = this.container.get("CronManager")!;
171
174
 
172
175
  // Set initial permission mode if provided
173
176
  if (options.permissionMode) {
@@ -411,8 +414,8 @@ export class Agent {
411
414
  await this.bangManager?.executeCommand(command);
412
415
  }
413
416
 
414
- public clearMessages(): void {
415
- this.messageManager.clearMessages();
417
+ public async clearMessages(): Promise<void> {
418
+ await this.slashCommandManager.executeCommand("clear");
416
419
  }
417
420
 
418
421
  /** Unified interrupt method, interrupts both AI messages and command execution */
@@ -460,6 +463,7 @@ export class Agent {
460
463
  this.abortAIMessage(); // This will abort tools including Agent tool (subagents)
461
464
  this.abortBashCommand();
462
465
  this.abortSlashCommand();
466
+ this.cronManager.stop();
463
467
  // Cleanup background task manager
464
468
  this.backgroundTaskManager.cleanup();
465
469
  // Cleanup MCP connections
@@ -484,16 +488,6 @@ export class Agent {
484
488
  // Cleanup memory store
485
489
  }
486
490
 
487
- /**
488
- * Get a subagent instance by its ID
489
- * @param subagentId - The ID of the subagent instance
490
- */
491
- public getSubagentInstance(
492
- subagentId: string,
493
- ): import("./managers/subagentManager.js").SubagentInstance | null {
494
- return this.subagentManager.getInstance(subagentId);
495
- }
496
-
497
491
  /**
498
492
  * Trigger the rewind UI callback
499
493
  */
@@ -688,6 +682,17 @@ export class Agent {
688
682
  await this.permissionManager.addPermissionRule(rule);
689
683
  }
690
684
 
685
+ /**
686
+ * Get subagent instance by ID
687
+ * @param subagentId - The ID of the subagent instance
688
+ * @returns The subagent instance or null if not found
689
+ */
690
+ public getSubagentInstance(
691
+ subagentId: string,
692
+ ): import("./managers/subagentManager.js").SubagentInstance | null {
693
+ return this.subagentManager.getInstance(subagentId);
694
+ }
695
+
691
696
  /**
692
697
  * Get the current task list ID
693
698
  */
@@ -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 || [],