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.
- package/builtin/skills/loop/SKILL.md +29 -3
- package/dist/agent.d.ts +11 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +44 -11
- 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 +13 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +69 -17
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +9 -0
- package/dist/managers/mcpManager.d.ts +4 -1
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +25 -5
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +7 -6
- package/dist/managers/permissionManager.d.ts +0 -2
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +0 -30
- package/dist/managers/slashCommandManager.d.ts +1 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +20 -4
- package/dist/managers/subagentManager.d.ts +6 -1
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +17 -18
- package/dist/managers/toolManager.d.ts +6 -0
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +41 -1
- package/dist/prompts/index.d.ts +1 -2
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +14 -6
- package/dist/services/initializationService.d.ts +0 -2
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +3 -35
- package/dist/services/jsonlHandler.d.ts +4 -4
- package/dist/services/jsonlHandler.d.ts.map +1 -1
- package/dist/services/jsonlHandler.js +4 -13
- package/dist/services/memory.d.ts +6 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +27 -14
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +3 -12
- package/dist/tools/agentTool.d.ts.map +1 -1
- package/dist/tools/agentTool.js +16 -4
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +2 -5
- package/dist/tools/cronCreateTool.d.ts.map +1 -1
- package/dist/tools/cronCreateTool.js +71 -6
- package/dist/tools/cronDeleteTool.d.ts.map +1 -1
- package/dist/tools/cronDeleteTool.js +5 -1
- package/dist/tools/cronListTool.d.ts.map +1 -1
- package/dist/tools/cronListTool.js +5 -1
- package/dist/tools/enterWorktreeTool.d.ts +8 -0
- package/dist/tools/enterWorktreeTool.d.ts.map +1 -0
- package/dist/tools/enterWorktreeTool.js +144 -0
- package/dist/tools/exitWorktreeTool.d.ts +8 -0
- package/dist/tools/exitWorktreeTool.d.ts.map +1 -0
- package/dist/tools/exitWorktreeTool.js +184 -0
- package/dist/tools/skillTool.d.ts.map +1 -1
- package/dist/tools/skillTool.js +16 -4
- package/dist/tools/taskManagementTools.d.ts.map +1 -1
- package/dist/tools/taskManagementTools.js +4 -0
- package/dist/tools/toolSearchTool.d.ts +15 -0
- package/dist/tools/toolSearchTool.d.ts.map +1 -0
- package/dist/tools/toolSearchTool.js +185 -0
- package/dist/tools/types.d.ts +19 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/webFetchTool.d.ts.map +1 -1
- package/dist/tools/webFetchTool.js +1 -0
- package/dist/types/agent.d.ts +6 -1
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +3 -1
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +1 -0
- package/dist/types/messaging.d.ts +1 -0
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/session.d.ts +0 -4
- package/dist/types/session.d.ts.map +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +4 -6
- package/dist/utils/cronToHuman.d.ts +6 -0
- package/dist/utils/cronToHuman.d.ts.map +1 -0
- package/dist/utils/cronToHuman.js +79 -0
- package/dist/utils/isDeferredTool.d.ts +19 -0
- package/dist/utils/isDeferredTool.d.ts.map +1 -0
- package/dist/utils/isDeferredTool.js +31 -0
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +1 -0
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +5 -0
- package/dist/utils/parseCronExpression.d.ts +6 -0
- package/dist/utils/parseCronExpression.d.ts.map +1 -0
- package/dist/utils/parseCronExpression.js +74 -0
- package/dist/utils/worktreeSession.d.ts +26 -0
- package/dist/utils/worktreeSession.d.ts.map +1 -0
- package/dist/utils/worktreeSession.js +14 -0
- package/dist/utils/worktreeUtils.d.ts +42 -0
- package/dist/utils/worktreeUtils.d.ts.map +1 -0
- package/dist/utils/worktreeUtils.js +236 -0
- package/package.json +1 -1
- package/src/agent.ts +61 -12
- package/src/constants/tools.ts +3 -0
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +73 -18
- package/src/managers/hookManager.ts +10 -0
- package/src/managers/mcpManager.ts +32 -6
- package/src/managers/messageManager.ts +7 -8
- package/src/managers/permissionManager.ts +0 -42
- package/src/managers/slashCommandManager.ts +30 -5
- package/src/managers/subagentManager.ts +28 -23
- package/src/managers/toolManager.ts +47 -1
- package/src/prompts/index.ts +17 -6
- package/src/services/initializationService.ts +2 -41
- package/src/services/jsonlHandler.ts +12 -24
- package/src/services/memory.ts +30 -17
- package/src/services/session.ts +3 -14
- package/src/tools/agentTool.ts +24 -5
- package/src/tools/bashTool.ts +2 -5
- package/src/tools/cronCreateTool.ts +81 -8
- package/src/tools/cronDeleteTool.ts +7 -2
- package/src/tools/cronListTool.ts +7 -2
- package/src/tools/enterWorktreeTool.ts +183 -0
- package/src/tools/exitWorktreeTool.ts +242 -0
- package/src/tools/skillTool.ts +24 -4
- package/src/tools/taskManagementTools.ts +4 -0
- package/src/tools/toolSearchTool.ts +228 -0
- package/src/tools/types.ts +19 -0
- package/src/tools/webFetchTool.ts +1 -0
- package/src/types/agent.ts +6 -0
- package/src/types/hooks.ts +4 -0
- package/src/types/messaging.ts +1 -0
- package/src/types/session.ts +0 -8
- package/src/utils/containerSetup.ts +7 -8
- package/src/utils/cronToHuman.ts +99 -0
- package/src/utils/isDeferredTool.ts +36 -0
- package/src/utils/mcpUtils.ts +1 -0
- package/src/utils/messageOperations.ts +5 -0
- package/src/utils/parseCronExpression.ts +78 -0
- package/src/utils/worktreeSession.ts +36 -0
- 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 {
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
82
|
+
// Convert messages to JSONL lines (compact JSON, timestamp first)
|
|
89
83
|
const lines = messages.map((message) => {
|
|
90
|
-
const { timestamp
|
|
91
|
-
|
|
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<
|
|
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:
|
|
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
|
|
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<
|
|
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
|
|
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:
|
|
181
|
+
private validateMessages(messages: Message[]): void {
|
|
194
182
|
for (let i = 0; i < messages.length; i++) {
|
|
195
183
|
const message = messages[i];
|
|
196
184
|
|
package/src/services/memory.ts
CHANGED
|
@@ -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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
176
|
+
this._cachedCombinedMemory = combined;
|
|
177
|
+
return combined;
|
|
165
178
|
}
|
|
166
179
|
}
|
package/src/services/session.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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
|
package/src/tools/agentTool.ts
CHANGED
|
@@ -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
|
|
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
|
},
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -80,7 +80,7 @@ export const bashTool: ToolPlugin = {
|
|
|
80
80
|
},
|
|
81
81
|
},
|
|
82
82
|
prompt: () => `
|
|
83
|
-
Executes a given bash command
|
|
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(
|
|
63
|
-
|
|
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
|
+
};
|