wave-agent-sdk 0.15.0 → 0.15.2

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 (144) hide show
  1. package/builtin/skills/loop/SKILL.md +29 -3
  2. package/dist/agent.d.ts +11 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +44 -11
  5. package/dist/constants/tools.d.ts +3 -0
  6. package/dist/constants/tools.d.ts.map +1 -1
  7. package/dist/constants/tools.js +3 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/managers/aiManager.d.ts +13 -1
  12. package/dist/managers/aiManager.d.ts.map +1 -1
  13. package/dist/managers/aiManager.js +69 -17
  14. package/dist/managers/hookManager.d.ts.map +1 -1
  15. package/dist/managers/hookManager.js +9 -0
  16. package/dist/managers/mcpManager.d.ts +4 -1
  17. package/dist/managers/mcpManager.d.ts.map +1 -1
  18. package/dist/managers/mcpManager.js +25 -5
  19. package/dist/managers/messageManager.d.ts.map +1 -1
  20. package/dist/managers/messageManager.js +7 -6
  21. package/dist/managers/permissionManager.d.ts +0 -2
  22. package/dist/managers/permissionManager.d.ts.map +1 -1
  23. package/dist/managers/permissionManager.js +0 -30
  24. package/dist/managers/slashCommandManager.d.ts +1 -0
  25. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  26. package/dist/managers/slashCommandManager.js +20 -4
  27. package/dist/managers/subagentManager.d.ts +6 -1
  28. package/dist/managers/subagentManager.d.ts.map +1 -1
  29. package/dist/managers/subagentManager.js +17 -18
  30. package/dist/managers/toolManager.d.ts +6 -0
  31. package/dist/managers/toolManager.d.ts.map +1 -1
  32. package/dist/managers/toolManager.js +41 -1
  33. package/dist/prompts/index.d.ts +1 -2
  34. package/dist/prompts/index.d.ts.map +1 -1
  35. package/dist/prompts/index.js +14 -6
  36. package/dist/services/initializationService.d.ts +0 -2
  37. package/dist/services/initializationService.d.ts.map +1 -1
  38. package/dist/services/initializationService.js +3 -35
  39. package/dist/services/jsonlHandler.d.ts +4 -4
  40. package/dist/services/jsonlHandler.d.ts.map +1 -1
  41. package/dist/services/jsonlHandler.js +4 -13
  42. package/dist/services/memory.d.ts +6 -0
  43. package/dist/services/memory.d.ts.map +1 -1
  44. package/dist/services/memory.js +27 -14
  45. package/dist/services/session.d.ts.map +1 -1
  46. package/dist/services/session.js +3 -12
  47. package/dist/tools/agentTool.d.ts.map +1 -1
  48. package/dist/tools/agentTool.js +16 -4
  49. package/dist/tools/bashTool.d.ts.map +1 -1
  50. package/dist/tools/bashTool.js +2 -5
  51. package/dist/tools/cronCreateTool.d.ts.map +1 -1
  52. package/dist/tools/cronCreateTool.js +71 -6
  53. package/dist/tools/cronDeleteTool.d.ts.map +1 -1
  54. package/dist/tools/cronDeleteTool.js +5 -1
  55. package/dist/tools/cronListTool.d.ts.map +1 -1
  56. package/dist/tools/cronListTool.js +5 -1
  57. package/dist/tools/enterWorktreeTool.d.ts +8 -0
  58. package/dist/tools/enterWorktreeTool.d.ts.map +1 -0
  59. package/dist/tools/enterWorktreeTool.js +144 -0
  60. package/dist/tools/exitWorktreeTool.d.ts +8 -0
  61. package/dist/tools/exitWorktreeTool.d.ts.map +1 -0
  62. package/dist/tools/exitWorktreeTool.js +184 -0
  63. package/dist/tools/skillTool.d.ts.map +1 -1
  64. package/dist/tools/skillTool.js +16 -4
  65. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  66. package/dist/tools/taskManagementTools.js +4 -0
  67. package/dist/tools/toolSearchTool.d.ts +15 -0
  68. package/dist/tools/toolSearchTool.d.ts.map +1 -0
  69. package/dist/tools/toolSearchTool.js +185 -0
  70. package/dist/tools/types.d.ts +19 -0
  71. package/dist/tools/types.d.ts.map +1 -1
  72. package/dist/tools/webFetchTool.d.ts.map +1 -1
  73. package/dist/tools/webFetchTool.js +1 -0
  74. package/dist/types/agent.d.ts +6 -1
  75. package/dist/types/agent.d.ts.map +1 -1
  76. package/dist/types/hooks.d.ts +3 -1
  77. package/dist/types/hooks.d.ts.map +1 -1
  78. package/dist/types/hooks.js +1 -0
  79. package/dist/types/messaging.d.ts +1 -0
  80. package/dist/types/messaging.d.ts.map +1 -1
  81. package/dist/types/session.d.ts +0 -4
  82. package/dist/types/session.d.ts.map +1 -1
  83. package/dist/utils/containerSetup.d.ts.map +1 -1
  84. package/dist/utils/containerSetup.js +4 -6
  85. package/dist/utils/cronToHuman.d.ts +6 -0
  86. package/dist/utils/cronToHuman.d.ts.map +1 -0
  87. package/dist/utils/cronToHuman.js +79 -0
  88. package/dist/utils/isDeferredTool.d.ts +19 -0
  89. package/dist/utils/isDeferredTool.d.ts.map +1 -0
  90. package/dist/utils/isDeferredTool.js +31 -0
  91. package/dist/utils/mcpUtils.d.ts.map +1 -1
  92. package/dist/utils/mcpUtils.js +1 -0
  93. package/dist/utils/messageOperations.d.ts.map +1 -1
  94. package/dist/utils/messageOperations.js +5 -0
  95. package/dist/utils/parseCronExpression.d.ts +6 -0
  96. package/dist/utils/parseCronExpression.d.ts.map +1 -0
  97. package/dist/utils/parseCronExpression.js +74 -0
  98. package/dist/utils/worktreeSession.d.ts +26 -0
  99. package/dist/utils/worktreeSession.d.ts.map +1 -0
  100. package/dist/utils/worktreeSession.js +14 -0
  101. package/dist/utils/worktreeUtils.d.ts +42 -0
  102. package/dist/utils/worktreeUtils.d.ts.map +1 -0
  103. package/dist/utils/worktreeUtils.js +236 -0
  104. package/package.json +1 -1
  105. package/src/agent.ts +61 -12
  106. package/src/constants/tools.ts +3 -0
  107. package/src/index.ts +1 -0
  108. package/src/managers/aiManager.ts +73 -18
  109. package/src/managers/hookManager.ts +10 -0
  110. package/src/managers/mcpManager.ts +32 -6
  111. package/src/managers/messageManager.ts +7 -8
  112. package/src/managers/permissionManager.ts +0 -42
  113. package/src/managers/slashCommandManager.ts +30 -5
  114. package/src/managers/subagentManager.ts +28 -23
  115. package/src/managers/toolManager.ts +47 -1
  116. package/src/prompts/index.ts +17 -6
  117. package/src/services/initializationService.ts +2 -41
  118. package/src/services/jsonlHandler.ts +12 -24
  119. package/src/services/memory.ts +30 -17
  120. package/src/services/session.ts +3 -14
  121. package/src/tools/agentTool.ts +24 -5
  122. package/src/tools/bashTool.ts +2 -5
  123. package/src/tools/cronCreateTool.ts +81 -8
  124. package/src/tools/cronDeleteTool.ts +7 -2
  125. package/src/tools/cronListTool.ts +7 -2
  126. package/src/tools/enterWorktreeTool.ts +183 -0
  127. package/src/tools/exitWorktreeTool.ts +242 -0
  128. package/src/tools/skillTool.ts +24 -4
  129. package/src/tools/taskManagementTools.ts +4 -0
  130. package/src/tools/toolSearchTool.ts +228 -0
  131. package/src/tools/types.ts +19 -0
  132. package/src/tools/webFetchTool.ts +1 -0
  133. package/src/types/agent.ts +6 -0
  134. package/src/types/hooks.ts +4 -0
  135. package/src/types/messaging.ts +1 -0
  136. package/src/types/session.ts +0 -8
  137. package/src/utils/containerSetup.ts +7 -8
  138. package/src/utils/cronToHuman.ts +99 -0
  139. package/src/utils/isDeferredTool.ts +36 -0
  140. package/src/utils/mcpUtils.ts +1 -0
  141. package/src/utils/messageOperations.ts +5 -0
  142. package/src/utils/parseCronExpression.ts +78 -0
  143. package/src/utils/worktreeSession.ts +36 -0
  144. package/src/utils/worktreeUtils.ts +288 -0
@@ -8,7 +8,7 @@ import { dirname } from "path";
8
8
  import { getLastLine } from "../utils/fileUtils.js";
9
9
 
10
10
  import type { Message } from "../types/index.js";
11
- import type { SessionMessage, SessionFilename } from "../types/session.js";
11
+ import type { SessionFilename } from "../types/session.js";
12
12
 
13
13
  /**
14
14
  * JSONL write options
@@ -56,13 +56,7 @@ export class JsonlHandler {
56
56
  return;
57
57
  }
58
58
 
59
- // Convert to SessionMessage format with timestamps
60
- const sessionMessages: SessionMessage[] = messages.map((message) => ({
61
- ...message,
62
- timestamp: new Date().toISOString(),
63
- }));
64
-
65
- return this.append(filePath, sessionMessages);
59
+ return this.append(filePath, messages);
66
60
  }
67
61
 
68
62
  /**
@@ -70,7 +64,7 @@ export class JsonlHandler {
70
64
  */
71
65
  async append(
72
66
  filePath: string,
73
- messages: SessionMessage[],
67
+ messages: Message[],
74
68
  options?: JsonlWriteOptions,
75
69
  ): Promise<void> {
76
70
  if (messages.length === 0) {
@@ -85,16 +79,10 @@ export class JsonlHandler {
85
79
  // Ensure directory exists
86
80
  await this.ensureDirectory(dirname(filePath));
87
81
 
88
- // Convert messages to JSONL lines (always compact JSON)
82
+ // Convert messages to JSONL lines (compact JSON, timestamp first)
89
83
  const lines = messages.map((message) => {
90
- const { timestamp: existingTimestamp, ...messageWithoutTimestamp } =
91
- message;
92
- const messageWithTimestamp = {
93
- timestamp: existingTimestamp || new Date().toISOString(),
94
- ...messageWithoutTimestamp,
95
- };
96
-
97
- return JSON.stringify(messageWithTimestamp);
84
+ const { timestamp, ...rest } = message;
85
+ return JSON.stringify({ timestamp, ...rest });
98
86
  });
99
87
 
100
88
  const content = lines.join("\n") + "\n";
@@ -116,7 +104,7 @@ export class JsonlHandler {
116
104
  /**
117
105
  * Read all messages from JSONL file (simplified - no metadata handling)
118
106
  */
119
- async read(filePath: string): Promise<SessionMessage[]> {
107
+ async read(filePath: string): Promise<Message[]> {
120
108
  try {
121
109
  const content = await readFile(filePath, "utf8");
122
110
  const lines = content
@@ -128,14 +116,14 @@ export class JsonlHandler {
128
116
  return [];
129
117
  }
130
118
 
131
- const allMessages: SessionMessage[] = [];
119
+ const allMessages: Message[] = [];
132
120
 
133
121
  // Parse all messages (no metadata line to skip)
134
122
  for (let i = 0; i < lines.length; i++) {
135
123
  const line = lines[i];
136
124
 
137
125
  try {
138
- const message = JSON.parse(line) as SessionMessage;
126
+ const message = JSON.parse(line) as Message;
139
127
  if (message.timestamp) allMessages.push(message);
140
128
  } catch (error) {
141
129
  // Throw error for invalid JSON lines with line number
@@ -155,7 +143,7 @@ export class JsonlHandler {
155
143
  /**
156
144
  * Get the last message from JSONL file using efficient file reading (simplified)
157
145
  */
158
- async getLastMessage(filePath: string): Promise<SessionMessage | null> {
146
+ async getLastMessage(filePath: string): Promise<Message | null> {
159
147
  try {
160
148
  // First check if file exists
161
149
  try {
@@ -176,7 +164,7 @@ export class JsonlHandler {
176
164
 
177
165
  try {
178
166
  const parsed = JSON.parse(lastLine);
179
- return parsed as SessionMessage;
167
+ return parsed as Message;
180
168
  } catch (error) {
181
169
  throw new Error(`Invalid JSON in last line of "${filePath}": ${error}`);
182
170
  }
@@ -190,7 +178,7 @@ export class JsonlHandler {
190
178
  /**
191
179
  * Validate messages before writing
192
180
  */
193
- private validateMessages(messages: SessionMessage[]): void {
181
+ private validateMessages(messages: Message[]): void {
194
182
  for (let i = 0; i < messages.length; i++) {
195
183
  const message = messages[i];
196
184
 
@@ -8,8 +8,26 @@ import { getGitCommonDir } from "../utils/gitUtils.js";
8
8
  import { pathEncoder } from "../utils/pathEncoder.js";
9
9
 
10
10
  export class MemoryService {
11
+ private _cachedProjectMemory: string = "";
12
+ private _cachedUserMemory: string = "";
13
+ private _cachedCombinedMemory: string | null = null;
14
+
11
15
  constructor(private container: Container) {}
12
16
 
17
+ public get cachedProjectMemory(): string {
18
+ return this._cachedProjectMemory;
19
+ }
20
+
21
+ public get cachedUserMemory(): string {
22
+ return this._cachedUserMemory;
23
+ }
24
+
25
+ public clearCache(): void {
26
+ this._cachedProjectMemory = "";
27
+ this._cachedUserMemory = "";
28
+ this._cachedCombinedMemory = null;
29
+ }
30
+
13
31
  /**
14
32
  * Get the project-specific auto-memory directory.
15
33
  * Uses the git common directory to ensure worktrees share the same memory.
@@ -143,24 +161,19 @@ export class MemoryService {
143
161
  }
144
162
 
145
163
  async getCombinedMemoryContent(workdir: string): Promise<string> {
146
- // Read memory file content
147
- const memoryContent = await this.readMemoryFile(workdir);
148
-
149
- // Read user-level memory content
150
- const userMemoryContent = await this.getUserMemoryContent();
151
-
152
- // Merge project memory and user memory
153
- let combinedMemory = "";
154
- if (memoryContent.trim()) {
155
- combinedMemory += memoryContent;
164
+ if (this._cachedCombinedMemory !== null) {
165
+ return this._cachedCombinedMemory;
156
166
  }
157
- if (userMemoryContent.trim()) {
158
- if (combinedMemory) {
159
- combinedMemory += "\n\n";
160
- }
161
- combinedMemory += userMemoryContent;
167
+ this._cachedProjectMemory = await this.readMemoryFile(workdir);
168
+ this._cachedUserMemory = await this.getUserMemoryContent();
169
+
170
+ let combined = "";
171
+ if (this._cachedProjectMemory.trim()) combined += this._cachedProjectMemory;
172
+ if (this._cachedUserMemory.trim()) {
173
+ if (combined) combined += "\n\n";
174
+ combined += this._cachedUserMemory;
162
175
  }
163
-
164
- return combinedMemory;
176
+ this._cachedCombinedMemory = combined;
177
+ return combined;
165
178
  }
166
179
  }
@@ -20,7 +20,6 @@ import { join } from "path";
20
20
  import { homedir } from "os";
21
21
  import { randomUUID } from "crypto";
22
22
  import type { Message } from "../types/index.js";
23
- import type { SessionMessage } from "../types/session.js";
24
23
  import { PathEncoder } from "../utils/pathEncoder.js";
25
24
  import { JsonlHandler } from "../services/jsonlHandler.js";
26
25
  import { extractLatestTotalTokens } from "../utils/tokenCalculation.js";
@@ -228,19 +227,14 @@ export async function appendMessages(
228
227
  );
229
228
  }
230
229
 
231
- const messagesWithTimestamp: SessionMessage[] = newMessages.map((msg) => ({
232
- timestamp: new Date().toISOString(),
233
- ...msg,
234
- }));
235
-
236
- await jsonlHandler.append(filePath, messagesWithTimestamp, {
230
+ await jsonlHandler.append(filePath, newMessages, {
237
231
  atomic: false,
238
232
  });
239
233
 
240
234
  // Update index
241
235
  const encoder = new PathEncoder();
242
236
  const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
243
- const lastMessage = messagesWithTimestamp[messagesWithTimestamp.length - 1];
237
+ const lastMessage = newMessages[newMessages.length - 1];
244
238
 
245
239
  // Get first message content if it's a new session or we don't have it
246
240
  let firstMessage: string | undefined;
@@ -333,12 +327,7 @@ export async function loadSessionFromJsonl(
333
327
  id: sessionId,
334
328
  rootSessionId: rootSessionId || sessionId,
335
329
  parentSessionId,
336
- messages: messages.map((msg) => {
337
- // Remove timestamp property for backward compatibility
338
- const { timestamp: _ignored, ...messageWithoutTimestamp } = msg;
339
- void _ignored; // Use the variable to avoid eslint error
340
- return messageWithoutTimestamp;
341
- }),
330
+ messages,
342
331
  metadata: {
343
332
  workdir,
344
333
  lastActiveAt: lastMessage
@@ -159,20 +159,39 @@ When using the Agent tool, you must specify a subagent_type parameter to select
159
159
 
160
160
  const messages = instance.messageManager.getMessages();
161
161
  const tokens = instance.messageManager.getLatestTotalTokens();
162
- const lastTools = instance.lastTools;
162
+ const usedTools = instance.usedTools;
163
163
 
164
164
  const toolCount = countToolBlocks(messages);
165
165
  const summary = formatToolTokenSummary(toolCount, tokens);
166
166
 
167
+ const getDisplayParam = (t: {
168
+ name: string;
169
+ parameters: string;
170
+ compactParams?: string;
171
+ stage?: string;
172
+ }) => {
173
+ if (
174
+ (t.stage === "end" || t.stage === "running") &&
175
+ t.compactParams
176
+ ) {
177
+ return t.compactParams;
178
+ }
179
+ const flat = t.parameters.replace(/\n/g, "\\n");
180
+ return flat.length > 30 ? `…${flat.slice(-30)}` : flat;
181
+ };
182
+
167
183
  let shortResult = "";
168
184
  if (toolCount > 2) {
169
185
  shortResult += "... ";
170
186
  }
171
- if (lastTools.length > 0) {
172
- shortResult += `${lastTools.join(", ")} `;
173
- }
174
-
175
187
  shortResult += summary;
188
+ if (usedTools.length > 0) {
189
+ shortResult +=
190
+ "\n" +
191
+ usedTools
192
+ .map((t) => `${t.name} ${getDisplayParam(t)}`)
193
+ .join("\n");
194
+ }
176
195
 
177
196
  context.onShortResultUpdate?.(shortResult);
178
197
  },
@@ -80,7 +80,7 @@ export const bashTool: ToolPlugin = {
80
80
  },
81
81
  },
82
82
  prompt: () => `
83
- Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
83
+ Executes a given bash command with optional timeout, ensuring proper handling and security measures. Each invocation runs in a fresh shell process starting from the project root.
84
84
 
85
85
  IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.
86
86
 
@@ -139,10 +139,7 @@ Use the gh command via the Bash tool for GitHub-related tasks including working
139
139
  - Do not retry failing commands in a sleep loop — diagnose the root cause.
140
140
  - If waiting for a background task you started with \`run_in_background\`, you will be notified when it completes — do not poll.
141
141
  - If you must poll an external process, use a check command (e.g. \`gh run view\`) rather than sleeping first.
142
- - If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user.
143
-
144
- # CWD management
145
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it. When you use \`cd\`, the shell working directory will be reset to the original working directory after the command completes.`,
142
+ - If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user.`,
146
143
  execute: async (
147
144
  args: Record<string, unknown>,
148
145
  context: ToolContext,
@@ -1,30 +1,73 @@
1
1
  import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
2
2
  import { CRON_CREATE_TOOL_NAME } from "../constants/tools.js";
3
+ import { cronToHuman } from "../utils/cronToHuman.js";
4
+ import { parseCronExpression } from "../utils/parseCronExpression.js";
5
+
6
+ const DEFAULT_MAX_AGE_DAYS = 7;
7
+ const MAX_JOBS = 50;
8
+
9
+ const CRON_CREATE_DESCRIPTION = `Schedule a prompt to run at a future time within this Wave session — either recurring on a cron schedule, or once at a specific time.`;
10
+
11
+ const CRON_CREATE_PROMPT = `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.
12
+
13
+ Uses standard 5-field cron in the user's local timezone: minute hour day-of-month month day-of-week. "0 9 * * *" means 9am local — no timezone conversion needed.
14
+
15
+ ## One-shot tasks (recurring: false)
16
+
17
+ For "remind me at X" or "at <time>, do Y" requests — fire once then auto-delete.
18
+ Pin minute/hour/day-of-month/month to specific values:
19
+ "remind me at 2:30pm today to check the deploy" → cron: "30 14 <today_dom> <today_month> *", recurring: false
20
+ "tomorrow morning, run the smoke test" → cron: "57 8 <tomorrow_dom> <tomorrow_month> *", recurring: false
21
+
22
+ ## Recurring jobs (recurring: true, the default)
23
+
24
+ For "every N minutes" / "every hour" / "weekdays at 9am" requests:
25
+ "*/5 * * * *" (every 5 min), "0 * * * *" (hourly), "0 9 * * 1-5" (weekdays at 9am local)
26
+
27
+ ## Avoid the :00 and :30 minute marks when the task allows it
28
+
29
+ Every user who asks for "9am" gets \`0 9\`, and every user who asks for "hourly" gets \`0 *\` — which means requests from across the planet land on the API at the same instant. When the user's request is approximate, pick a minute that is NOT 0 or 30:
30
+ "every morning around 9" → "57 8 * * *" or "3 9 * * *" (not "0 9 * * *")
31
+ "hourly" → "7 * * * *" (not "0 * * * *")
32
+ "in an hour or so, remind me to..." → pick whatever minute you land on, don't round
33
+
34
+ Only use minute 0 or 30 when the user names that exact time and clearly means it ("at 9:00 sharp", "at half past", coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.
35
+
36
+ ## Session-only
37
+
38
+ Jobs live only in this Wave session — nothing is written to disk, and the job is gone when Wave exits.
39
+
40
+ ## Runtime behavior
41
+
42
+ Jobs only fire while the REPL is idle (not mid-query). The scheduler adds a small deterministic jitter on top of whatever you pick: recurring tasks fire up to 10% of their period late (max 15 min); one-shot tasks landing on :00 or :30 fire up to 90s early. Picking an off-minute is still the bigger lever.
43
+
44
+ Recurring tasks auto-expire after ${DEFAULT_MAX_AGE_DAYS} days — they fire one final time, then are deleted. This bounds session lifetime. Tell the user about the ${DEFAULT_MAX_AGE_DAYS}-day limit when scheduling recurring jobs.
45
+
46
+ Returns a job ID you can pass to CronDelete.`;
3
47
 
4
48
  export const cronCreateTool: ToolPlugin = {
5
49
  name: CRON_CREATE_TOOL_NAME,
50
+ shouldDefer: true,
6
51
  config: {
7
52
  type: "function",
8
53
  function: {
9
54
  name: CRON_CREATE_TOOL_NAME,
10
- description:
11
- "Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.",
55
+ description: CRON_CREATE_DESCRIPTION,
12
56
  parameters: {
13
57
  type: "object",
14
58
  properties: {
15
59
  cron: {
16
60
  type: "string",
17
61
  description:
18
- 'Standard 5-field cron expression in local time: "M H DoM Mon DoW"',
62
+ 'Standard 5-field cron expression in local time: "M H DoM Mon DoW" (e.g. "*/5 * * * *" = every 5 minutes, "30 14 28 2 *" = Feb 28 at 2:30pm local once).',
19
63
  },
20
64
  prompt: {
21
65
  type: "string",
22
- description: "The prompt to enqueue at each fire time",
66
+ description: "The prompt to enqueue at each fire time.",
23
67
  },
24
68
  recurring: {
25
69
  type: "boolean",
26
- description:
27
- "Default: true. true = fire on every cron match until deleted or auto-expired after 7 days. false = fire once at the next match, then auto-delete",
70
+ description: `true (default) = fire on every cron match until deleted or auto-expired after ${DEFAULT_MAX_AGE_DAYS} days. false = fire once at the next match, then auto-delete. Use false for "remind me at X" one-shot requests with pinned minute/hour/dom/month.`,
28
71
  default: true,
29
72
  },
30
73
  },
@@ -32,6 +75,7 @@ export const cronCreateTool: ToolPlugin = {
32
75
  },
33
76
  },
34
77
  },
78
+ prompt: () => CRON_CREATE_PROMPT,
35
79
  execute: async (
36
80
  args: Record<string, unknown>,
37
81
  context: ToolContext,
@@ -50,6 +94,25 @@ export const cronCreateTool: ToolPlugin = {
50
94
  };
51
95
  }
52
96
 
97
+ // Validate cron expression
98
+ if (!parseCronExpression(cron)) {
99
+ return {
100
+ success: false,
101
+ content: "",
102
+ error: `Invalid cron expression '${cron}'. Expected 5 fields: M H DoM Mon DoW.`,
103
+ };
104
+ }
105
+
106
+ // Check max jobs limit
107
+ const existingJobs = context.cronManager.listJobs();
108
+ if (existingJobs.length >= MAX_JOBS) {
109
+ return {
110
+ success: false,
111
+ content: "",
112
+ error: `Too many scheduled jobs (max ${MAX_JOBS}). Cancel one first.`,
113
+ };
114
+ }
115
+
53
116
  try {
54
117
  const job = context.cronManager.createJob({
55
118
  cron,
@@ -57,10 +120,20 @@ export const cronCreateTool: ToolPlugin = {
57
120
  recurring,
58
121
  });
59
122
 
123
+ const humanSchedule = cronToHuman(cron);
124
+ const where = "Session-only (not written to disk, dies when Wave exits)";
125
+ const resultMessage = recurring
126
+ ? `Scheduled recurring job ${job.id} (${humanSchedule}). ${where}. Auto-expires after ${DEFAULT_MAX_AGE_DAYS} days. Use CronDelete to cancel sooner.`
127
+ : `Scheduled one-shot task ${job.id} (${humanSchedule}). ${where}. It will fire once then auto-delete.`;
128
+
60
129
  return {
61
130
  success: true,
62
- content: JSON.stringify({ id: job.id }, null, 2),
63
- shortResult: `Scheduled job ${job.id}`,
131
+ content: JSON.stringify(
132
+ { id: job.id, humanSchedule, recurring },
133
+ null,
134
+ 2,
135
+ ),
136
+ shortResult: resultMessage,
64
137
  };
65
138
  } catch (error) {
66
139
  return {
@@ -1,14 +1,18 @@
1
1
  import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
2
2
  import { CRON_DELETE_TOOL_NAME } from "../constants/tools.js";
3
3
 
4
+ const CRON_DELETE_DESCRIPTION = "Cancel a scheduled cron job by ID";
5
+
6
+ const CRON_DELETE_PROMPT = `Cancel a cron job previously scheduled with CronCreate. Removes it from the in-memory session store.`;
7
+
4
8
  export const cronDeleteTool: ToolPlugin = {
5
9
  name: CRON_DELETE_TOOL_NAME,
10
+ shouldDefer: true,
6
11
  config: {
7
12
  type: "function",
8
13
  function: {
9
14
  name: CRON_DELETE_TOOL_NAME,
10
- description:
11
- "Cancel a cron job previously scheduled with CronCreate. Removes it from the in-memory session store.",
15
+ description: CRON_DELETE_DESCRIPTION,
12
16
  parameters: {
13
17
  type: "object",
14
18
  properties: {
@@ -21,6 +25,7 @@ export const cronDeleteTool: ToolPlugin = {
21
25
  },
22
26
  },
23
27
  },
28
+ prompt: () => CRON_DELETE_PROMPT,
24
29
  execute: async (
25
30
  args: Record<string, unknown>,
26
31
  context: ToolContext,
@@ -1,20 +1,25 @@
1
1
  import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
2
2
  import { CRON_LIST_TOOL_NAME } from "../constants/tools.js";
3
3
 
4
+ const CRON_LIST_DESCRIPTION = "List scheduled cron jobs";
5
+
6
+ const CRON_LIST_PROMPT = `List all cron jobs scheduled via CronCreate in this session.`;
7
+
4
8
  export const cronListTool: ToolPlugin = {
5
9
  name: CRON_LIST_TOOL_NAME,
10
+ shouldDefer: true,
6
11
  config: {
7
12
  type: "function",
8
13
  function: {
9
14
  name: CRON_LIST_TOOL_NAME,
10
- description:
11
- "List all cron jobs scheduled via CronCreate in this session.",
15
+ description: CRON_LIST_DESCRIPTION,
12
16
  parameters: {
13
17
  type: "object",
14
18
  properties: {},
15
19
  },
16
20
  },
17
21
  },
22
+ prompt: () => CRON_LIST_PROMPT,
18
23
  execute: async (
19
24
  _args: Record<string, unknown>,
20
25
  context: ToolContext,
@@ -0,0 +1,183 @@
1
+ /**
2
+ * EnterWorktree tool - creates an isolated git worktree and switches the session into it.
3
+ * Mirrors Claude Code's EnterWorktree tool behavior and prompt.
4
+ */
5
+
6
+ import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
7
+ import {
8
+ getCurrentWorktreeSession,
9
+ setCurrentWorktreeSession,
10
+ type WorktreeSession,
11
+ } from "../utils/worktreeSession.js";
12
+ import {
13
+ createWorktree,
14
+ validateWorktreeName,
15
+ generateWorktreeName,
16
+ } from "../utils/worktreeUtils.js";
17
+ import { getGitMainRepoRoot } from "../utils/gitUtils.js";
18
+ import { ENTER_WORKTREE_TOOL_NAME } from "../constants/tools.js";
19
+ import { logger } from "../utils/globalLogger.js";
20
+
21
+ export const ENTER_WORKTREE_TOOL_PROMPT = `Use this tool ONLY when the user explicitly asks to work in a worktree. This tool creates an isolated git worktree and switches the current session into it.
22
+
23
+ ## When to Use
24
+
25
+ - The user explicitly says "worktree" (e.g., "start a worktree", "work in a worktree", "create a worktree", "use a worktree")
26
+
27
+ ## When NOT to Use
28
+
29
+ - The user asks to create a branch, switch branches, or work on a different branch — use git commands instead
30
+ - The user asks to fix a bug or work on a feature — use normal git workflow unless they specifically mention worktrees
31
+ - Never use this tool unless the user explicitly mentions "worktree"
32
+
33
+ ## Requirements
34
+
35
+ - Must be in a git repository
36
+ - Must not already be in a worktree
37
+
38
+ ## Behavior
39
+
40
+ - Creates a new git worktree inside \`.wave/worktrees/\` with a new branch based on HEAD
41
+ - Switches the session's working directory to the new worktree
42
+ - Use ExitWorktree to leave the worktree mid-session (keep or remove). On session exit, if still in the worktree, the user will be prompted to keep or remove it
43
+
44
+ ## Parameters
45
+
46
+ - \`name\` (optional): A name for the worktree. Each "/"-separated segment may contain only letters, digits, dots, underscores, and dashes; max 64 chars total. A random name is generated if not provided.
47
+ `;
48
+
49
+ export const enterWorktreeTool: ToolPlugin = {
50
+ name: ENTER_WORKTREE_TOOL_NAME,
51
+ shouldDefer: true,
52
+ config: {
53
+ type: "function",
54
+ function: {
55
+ name: ENTER_WORKTREE_TOOL_NAME,
56
+ description: ENTER_WORKTREE_TOOL_PROMPT,
57
+ parameters: {
58
+ type: "object",
59
+ properties: {
60
+ name: {
61
+ type: "string",
62
+ description:
63
+ 'Optional name for the worktree. Each "/"-separated segment may contain only letters, digits, dots, underscores, and dashes; max 64 chars total. A random name is generated if not provided.',
64
+ },
65
+ },
66
+ },
67
+ },
68
+ },
69
+ prompt: () => ENTER_WORKTREE_TOOL_PROMPT,
70
+
71
+ async execute(
72
+ args: Record<string, unknown>,
73
+ context: ToolContext,
74
+ ): Promise<ToolResult> {
75
+ // Validate not already in a worktree created by this session
76
+ if (getCurrentWorktreeSession()) {
77
+ return {
78
+ success: false,
79
+ content:
80
+ "Already in a worktree session. Use ExitWorktree to leave before creating a new one.",
81
+ error: "Already in a worktree session",
82
+ };
83
+ }
84
+
85
+ const name = (args.name as string) || generateWorktreeName();
86
+
87
+ // Validate the worktree name
88
+ try {
89
+ validateWorktreeName(name);
90
+ } catch (e) {
91
+ return {
92
+ success: false,
93
+ content: `Invalid worktree name: ${(e as Error).message}`,
94
+ error: `Invalid worktree name: ${(e as Error).message}`,
95
+ };
96
+ }
97
+
98
+ // Resolve to main repo root so worktree creation works from within a subdirectory
99
+ const mainRepoRoot = getGitMainRepoRoot(context.workdir);
100
+ if (!mainRepoRoot) {
101
+ return {
102
+ success: false,
103
+ content:
104
+ "Cannot create a worktree: not in a git repository. Configure WorktreeCreate and WorktreeRemove hooks in settings.json to use worktree isolation with other VCS systems.",
105
+ error: "Not in a git repository",
106
+ };
107
+ }
108
+
109
+ // Create the worktree (captures originalHeadCommit internally)
110
+ const worktreeInfo = createWorktree(name, mainRepoRoot);
111
+
112
+ // Build session state
113
+ const session: WorktreeSession = {
114
+ originalCwd: context.workdir,
115
+ worktreePath: worktreeInfo.path,
116
+ worktreeBranch: worktreeInfo.branch,
117
+ worktreeName: worktreeInfo.name,
118
+ isNew: worktreeInfo.isNew,
119
+ repoRoot: worktreeInfo.repoRoot,
120
+ originalHeadCommit: worktreeInfo.originalHeadCommit,
121
+ };
122
+
123
+ // Set module-level session state
124
+ setCurrentWorktreeSession(session);
125
+
126
+ // Update CWD via AIManager
127
+ const aiManager = context.aiManager;
128
+ if (aiManager) {
129
+ aiManager.setWorkdir(worktreeInfo.path);
130
+ }
131
+
132
+ // Also update the container's Workdir entry
133
+ // (Container is not directly accessible from ToolContext, but AIManager.setWorkdir
134
+ // handles both its internal field and process.chdir)
135
+
136
+ // Trigger WorktreeCreate hook if worktree is new
137
+ let hookTriggered = false;
138
+ if (session.isNew && context.hookManager) {
139
+ try {
140
+ const hookResults = await context.hookManager.executeHooks(
141
+ "WorktreeCreate",
142
+ {
143
+ event: "WorktreeCreate",
144
+ projectDir: worktreeInfo.path,
145
+ timestamp: new Date(),
146
+ sessionId: context.sessionId ?? "",
147
+ transcriptPath: context.messageManager?.getTranscriptPath() ?? "",
148
+ cwd: worktreeInfo.path,
149
+ worktreeName: worktreeInfo.name,
150
+ env: Object.fromEntries(
151
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
152
+ ) as Record<string, string>,
153
+ },
154
+ );
155
+
156
+ if (context.messageManager) {
157
+ context.hookManager.processHookResults(
158
+ "WorktreeCreate",
159
+ hookResults,
160
+ context.messageManager,
161
+ );
162
+ }
163
+
164
+ hookTriggered = true;
165
+ } catch (error) {
166
+ // Non-blocking: log but don't fail the tool
167
+ logger?.warn("WorktreeCreate hooks execution failed:", error);
168
+ }
169
+ }
170
+
171
+ const branchInfo = worktreeInfo.branch
172
+ ? ` on branch ${worktreeInfo.branch}`
173
+ : "";
174
+ const hookInfo = hookTriggered
175
+ ? " WorktreeCreate hooks were executed."
176
+ : "";
177
+
178
+ return {
179
+ success: true,
180
+ content: `Created worktree at ${worktreeInfo.path}${branchInfo}. The session is now working in the worktree. Use ExitWorktree to leave mid-session, or exit the session to be prompted.${hookInfo}`,
181
+ };
182
+ },
183
+ };