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
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree creation and removal utilities for the SDK.
|
|
3
|
+
* Used by EnterWorktree and ExitWorktree tools.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import { getGitMainRepoRoot, getDefaultRemoteBranch } from "./gitUtils.js";
|
|
9
|
+
import { logger } from "./globalLogger.js";
|
|
10
|
+
/**
|
|
11
|
+
* Validate a worktree name to prevent path traversal and invalid characters.
|
|
12
|
+
*/
|
|
13
|
+
export function validateWorktreeName(name) {
|
|
14
|
+
const MAX_LENGTH = 64;
|
|
15
|
+
if (name.length > MAX_LENGTH) {
|
|
16
|
+
throw new Error(`Invalid worktree name: must be ${MAX_LENGTH} characters or fewer (got ${name.length})`);
|
|
17
|
+
}
|
|
18
|
+
for (const segment of name.split("/")) {
|
|
19
|
+
if (segment === "." || segment === "..") {
|
|
20
|
+
throw new Error(`Invalid worktree name "${name}": must not contain "." or ".." path segments`);
|
|
21
|
+
}
|
|
22
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(segment)) {
|
|
23
|
+
throw new Error(`Invalid worktree name "${name}": each "/"-separated segment must be non-empty and contain only letters, digits, dots, underscores, and dashes`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate a random worktree name.
|
|
29
|
+
*/
|
|
30
|
+
export function generateWorktreeName() {
|
|
31
|
+
const adjectives = [
|
|
32
|
+
"swift",
|
|
33
|
+
"calm",
|
|
34
|
+
"bold",
|
|
35
|
+
"keen",
|
|
36
|
+
"bright",
|
|
37
|
+
"cool",
|
|
38
|
+
"deep",
|
|
39
|
+
"fair",
|
|
40
|
+
"gentle",
|
|
41
|
+
"grand",
|
|
42
|
+
];
|
|
43
|
+
const nouns = [
|
|
44
|
+
"fox",
|
|
45
|
+
"owl",
|
|
46
|
+
"hawk",
|
|
47
|
+
"wolf",
|
|
48
|
+
"bear",
|
|
49
|
+
"lynx",
|
|
50
|
+
"pike",
|
|
51
|
+
"kite",
|
|
52
|
+
"dove",
|
|
53
|
+
"stag",
|
|
54
|
+
];
|
|
55
|
+
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
|
56
|
+
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
|
57
|
+
const num = Math.floor(Math.random() * 900) + 100;
|
|
58
|
+
return `${adj}-${noun}-${num}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get the current HEAD commit SHA.
|
|
62
|
+
*/
|
|
63
|
+
export function getHeadCommit(cwd) {
|
|
64
|
+
return execSync(`git -C "${cwd}" rev-parse HEAD`, {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
67
|
+
}).trim();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a git worktree for use during a session.
|
|
71
|
+
*/
|
|
72
|
+
export function createWorktree(name, cwd) {
|
|
73
|
+
const repoRoot = getGitMainRepoRoot(cwd);
|
|
74
|
+
if (!repoRoot) {
|
|
75
|
+
throw new Error("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.");
|
|
76
|
+
}
|
|
77
|
+
// Capture HEAD commit before creating worktree (for dirty-check on exit)
|
|
78
|
+
const originalHeadCommit = getHeadCommit(cwd);
|
|
79
|
+
const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
|
|
80
|
+
const branchName = `worktree-${name}`;
|
|
81
|
+
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
82
|
+
// Ensure parent directory exists
|
|
83
|
+
const parentDir = path.dirname(worktreePath);
|
|
84
|
+
if (!fs.existsSync(parentDir)) {
|
|
85
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
// Check if worktree already exists
|
|
88
|
+
if (fs.existsSync(worktreePath)) {
|
|
89
|
+
return {
|
|
90
|
+
name,
|
|
91
|
+
path: worktreePath,
|
|
92
|
+
branch: branchName,
|
|
93
|
+
repoRoot,
|
|
94
|
+
isNew: false,
|
|
95
|
+
originalHeadCommit,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
// Create worktree and branch
|
|
100
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`, {
|
|
101
|
+
cwd: repoRoot,
|
|
102
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
106
|
+
GIT_ASKPASS: "",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
name,
|
|
111
|
+
path: worktreePath,
|
|
112
|
+
branch: branchName,
|
|
113
|
+
repoRoot,
|
|
114
|
+
isNew: true,
|
|
115
|
+
originalHeadCommit,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const stderr = error.stderr?.toString() || "";
|
|
120
|
+
if (stderr.includes("already exists")) {
|
|
121
|
+
// Branch exists but worktree doesn't — attach to existing branch
|
|
122
|
+
try {
|
|
123
|
+
execSync(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
124
|
+
cwd: repoRoot,
|
|
125
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
126
|
+
env: {
|
|
127
|
+
...process.env,
|
|
128
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
129
|
+
GIT_ASKPASS: "",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
name,
|
|
134
|
+
path: worktreePath,
|
|
135
|
+
branch: branchName,
|
|
136
|
+
repoRoot,
|
|
137
|
+
isNew: true,
|
|
138
|
+
originalHeadCommit,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (innerError) {
|
|
142
|
+
throw new Error(`Failed to add worktree: ${innerError.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`Failed to create worktree: ${error.message}\n${stderr}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Remove a git worktree and its branch.
|
|
150
|
+
*/
|
|
151
|
+
export function removeWorktree(info) {
|
|
152
|
+
const repoRoot = info.repoRoot;
|
|
153
|
+
try {
|
|
154
|
+
// Get current branch in worktree before removing
|
|
155
|
+
let currentBranch;
|
|
156
|
+
try {
|
|
157
|
+
currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`, {
|
|
158
|
+
cwd: info.path,
|
|
159
|
+
encoding: "utf8",
|
|
160
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
161
|
+
}).trim();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Ignore errors
|
|
165
|
+
}
|
|
166
|
+
// Remove worktree
|
|
167
|
+
execSync(`git worktree remove --force "${info.path}"`, {
|
|
168
|
+
cwd: repoRoot,
|
|
169
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
170
|
+
});
|
|
171
|
+
// Delete worktree branch
|
|
172
|
+
try {
|
|
173
|
+
execSync(`git branch -D ${info.branch}`, {
|
|
174
|
+
cwd: repoRoot,
|
|
175
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Ignore errors
|
|
180
|
+
}
|
|
181
|
+
// Delete current branch if different and not protected
|
|
182
|
+
if (currentBranch &&
|
|
183
|
+
currentBranch !== info.branch &&
|
|
184
|
+
currentBranch !== "HEAD") {
|
|
185
|
+
const defaultRemoteBranch = getDefaultRemoteBranch(repoRoot);
|
|
186
|
+
const defaultBranchName = defaultRemoteBranch.split("/").pop();
|
|
187
|
+
if (currentBranch !== defaultBranchName &&
|
|
188
|
+
currentBranch !== "main" &&
|
|
189
|
+
currentBranch !== "master") {
|
|
190
|
+
try {
|
|
191
|
+
execSync(`git branch -D ${currentBranch}`, {
|
|
192
|
+
cwd: repoRoot,
|
|
193
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Ignore errors
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
logger.error("Failed to remove worktree or branch:", {
|
|
204
|
+
error: error instanceof Error ? error.message : String(error),
|
|
205
|
+
worktreePath: info.path,
|
|
206
|
+
});
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Count uncommitted files and new commits in a worktree.
|
|
212
|
+
* Returns null if git commands fail (fail-closed).
|
|
213
|
+
*/
|
|
214
|
+
export function countWorktreeChanges(worktreePath, originalHeadCommit) {
|
|
215
|
+
try {
|
|
216
|
+
const statusOutput = execSync(`git -C "${worktreePath}" status --porcelain`, {
|
|
217
|
+
encoding: "utf8",
|
|
218
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
219
|
+
});
|
|
220
|
+
const changedFiles = statusOutput
|
|
221
|
+
.split("\n")
|
|
222
|
+
.filter((l) => l.trim() !== "").length;
|
|
223
|
+
if (!originalHeadCommit) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const revListOutput = execSync(`git -C "${worktreePath}" rev-list --count ${originalHeadCommit}..HEAD`, {
|
|
227
|
+
encoding: "utf8",
|
|
228
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
229
|
+
});
|
|
230
|
+
const commits = parseInt(revListOutput.trim(), 10) || 0;
|
|
231
|
+
return { changedFiles, commits };
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -83,10 +83,6 @@ export class Agent {
|
|
|
83
83
|
// Configuration options storage for dynamic resolution
|
|
84
84
|
private options: AgentOptions;
|
|
85
85
|
|
|
86
|
-
// Memory content storage
|
|
87
|
-
private _projectMemoryContent: string = "";
|
|
88
|
-
private _userMemoryContent: string = "";
|
|
89
|
-
|
|
90
86
|
// Dynamic configuration getter methods
|
|
91
87
|
public getGatewayConfig(): GatewayConfig {
|
|
92
88
|
return this.configurationService.resolveGatewayConfig();
|
|
@@ -282,12 +278,20 @@ export class Agent {
|
|
|
282
278
|
|
|
283
279
|
/** Get project memory content */
|
|
284
280
|
public get projectMemory(): string {
|
|
285
|
-
|
|
281
|
+
const memoryService =
|
|
282
|
+
this.container.get<import("./services/memory.js").MemoryService>(
|
|
283
|
+
"MemoryService",
|
|
284
|
+
);
|
|
285
|
+
return memoryService?.cachedProjectMemory ?? "";
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
/** Get user memory content */
|
|
289
289
|
public get userMemory(): string {
|
|
290
|
-
|
|
290
|
+
const memoryService =
|
|
291
|
+
this.container.get<import("./services/memory.js").MemoryService>(
|
|
292
|
+
"MemoryService",
|
|
293
|
+
);
|
|
294
|
+
return memoryService?.cachedUserMemory ?? "";
|
|
291
295
|
}
|
|
292
296
|
|
|
293
297
|
/** Get combined memory content (project + user + modular rules) */
|
|
@@ -414,6 +418,7 @@ export class Agent {
|
|
|
414
418
|
* @param options.messages - Optional initial messages for testing convenience
|
|
415
419
|
* @param options.workdir - Working directory (defaults to process.cwd())
|
|
416
420
|
* @param options.systemPrompt - Optional custom system prompt
|
|
421
|
+
* @param options.mcpServers - Optional MCP server configs to connect at startup
|
|
417
422
|
* @returns Promise that resolves to initialized Agent instance
|
|
418
423
|
*
|
|
419
424
|
* @example
|
|
@@ -479,12 +484,6 @@ export class Agent {
|
|
|
479
484
|
memoryRuleManager: this.memoryRuleManager,
|
|
480
485
|
liveConfigManager: this.liveConfigManager,
|
|
481
486
|
taskManager: this.taskManager,
|
|
482
|
-
setProjectMemory: (content) => {
|
|
483
|
-
this._projectMemoryContent = content;
|
|
484
|
-
},
|
|
485
|
-
setUserMemory: (content) => {
|
|
486
|
-
this._userMemoryContent = content;
|
|
487
|
-
},
|
|
488
487
|
resolveAndValidateConfig: () => this.resolveAndValidateConfig(),
|
|
489
488
|
},
|
|
490
489
|
options,
|
|
@@ -603,6 +602,44 @@ export class Agent {
|
|
|
603
602
|
this.options.callbacks?.onBackgroundCurrentTask?.();
|
|
604
603
|
}
|
|
605
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Trigger WorktreeRemove hook before agent destruction.
|
|
607
|
+
* Called from CLI exit dialog when user chooses to remove the worktree.
|
|
608
|
+
* Non-blocking: errors logged but don't prevent removal.
|
|
609
|
+
*/
|
|
610
|
+
public async triggerWorktreeRemoveHook(worktreePath: string): Promise<void> {
|
|
611
|
+
if (!this.hookManager.hasHooks("WorktreeRemove")) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const sessionId = this.messageManager.getSessionId();
|
|
616
|
+
const transcriptPath = this.messageManager.getTranscriptPath();
|
|
617
|
+
const hookResults = await this.hookManager.executeHooks(
|
|
618
|
+
"WorktreeRemove",
|
|
619
|
+
{
|
|
620
|
+
event: "WorktreeRemove",
|
|
621
|
+
projectDir: this.workdir,
|
|
622
|
+
timestamp: new Date(),
|
|
623
|
+
sessionId,
|
|
624
|
+
transcriptPath,
|
|
625
|
+
cwd: this.workdir,
|
|
626
|
+
worktreePath,
|
|
627
|
+
env: Object.fromEntries(
|
|
628
|
+
Object.entries(process.env).filter((e) => e[1] !== undefined),
|
|
629
|
+
) as Record<string, string>,
|
|
630
|
+
},
|
|
631
|
+
);
|
|
632
|
+
// Process results via messageManager (may not be visible during shutdown)
|
|
633
|
+
this.hookManager.processHookResults(
|
|
634
|
+
"WorktreeRemove",
|
|
635
|
+
hookResults,
|
|
636
|
+
this.messageManager,
|
|
637
|
+
);
|
|
638
|
+
} catch (error) {
|
|
639
|
+
this.logger?.warn("WorktreeRemove hooks execution failed:", error);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
606
643
|
/** Destroy managers, clean up resources */
|
|
607
644
|
public async destroy(): Promise<void> {
|
|
608
645
|
// Clear notification callback first to prevent any late triggers from
|
|
@@ -894,4 +931,16 @@ export class Agent {
|
|
|
894
931
|
public get taskListId(): string {
|
|
895
932
|
return this.taskManager.getTaskListId();
|
|
896
933
|
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Check if there are any running background tasks or active subagents
|
|
937
|
+
*/
|
|
938
|
+
public get hasRunningBackgroundWork(): boolean {
|
|
939
|
+
const runningTasks = this.backgroundTaskManager
|
|
940
|
+
.getAllTasks()
|
|
941
|
+
.some((t) => t.status === "running");
|
|
942
|
+
const activeSubagents =
|
|
943
|
+
this.subagentManager.getActiveInstances().length > 0;
|
|
944
|
+
return runningTasks || activeSubagents;
|
|
945
|
+
}
|
|
897
946
|
}
|
package/src/constants/tools.ts
CHANGED
|
@@ -19,3 +19,6 @@ export const CRON_CREATE_TOOL_NAME = "CronCreate";
|
|
|
19
19
|
export const CRON_DELETE_TOOL_NAME = "CronDelete";
|
|
20
20
|
export const CRON_LIST_TOOL_NAME = "CronList";
|
|
21
21
|
export const WEB_FETCH_TOOL_NAME = "WebFetch";
|
|
22
|
+
export const ENTER_WORKTREE_TOOL_NAME = "EnterWorktree";
|
|
23
|
+
export const EXIT_WORKTREE_TOOL_NAME = "ExitWorktree";
|
|
24
|
+
export const TOOL_SEARCH_TOOL_NAME = "ToolSearch";
|
package/src/index.ts
CHANGED
|
@@ -53,20 +53,20 @@ export class AIManager {
|
|
|
53
53
|
private abortController: AbortController | null = null;
|
|
54
54
|
onLoadingChange?: (loading: boolean) => void;
|
|
55
55
|
private toolAbortController: AbortController | null = null;
|
|
56
|
-
private workdir: string;
|
|
57
56
|
private systemPrompt?: string;
|
|
58
57
|
private subagentType?: string; // Store subagent type for hook context
|
|
59
58
|
private stream: boolean; // Streaming mode flag
|
|
60
59
|
private modelOverride?: string;
|
|
61
60
|
private consecutiveCompactionFailures: number = 0;
|
|
62
61
|
private readonly maxTurns?: number;
|
|
62
|
+
/** Tracks which deferred tools have been discovered via ToolSearch */
|
|
63
|
+
private discoveredTools = new Set<string>();
|
|
63
64
|
|
|
64
65
|
// Service overrides
|
|
65
66
|
constructor(
|
|
66
67
|
private container: Container,
|
|
67
68
|
options: AIManagerOptions,
|
|
68
69
|
) {
|
|
69
|
-
this.workdir = options.workdir;
|
|
70
70
|
this.systemPrompt = options.systemPrompt;
|
|
71
71
|
this.subagentType = options.subagentType; // Store subagent type
|
|
72
72
|
this.stream = options.stream ?? true; // Default to true if not specified
|
|
@@ -166,7 +166,16 @@ export class AIManager {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
public getWorkdir(): string {
|
|
169
|
-
return this.
|
|
169
|
+
return this.container.get<string>("Workdir") ?? process.cwd();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Update the working directory mid-session (e.g., when entering/exiting a worktree).
|
|
174
|
+
* Also updates process.chdir() so bash commands use the new directory.
|
|
175
|
+
*/
|
|
176
|
+
public setWorkdir(newWorkdir: string): void {
|
|
177
|
+
this.container.register("Workdir", newWorkdir);
|
|
178
|
+
process.chdir(newWorkdir);
|
|
170
179
|
}
|
|
171
180
|
|
|
172
181
|
private isCompacting: boolean = false;
|
|
@@ -185,8 +194,9 @@ export class AIManager {
|
|
|
185
194
|
return this.toolManager.getToolsConfig({
|
|
186
195
|
availableSubagents,
|
|
187
196
|
availableSkills,
|
|
188
|
-
workdir: this.
|
|
197
|
+
workdir: this.getWorkdir(),
|
|
189
198
|
isSubagent: !!this.subagentType,
|
|
199
|
+
discoveredTools: this.discoveredTools,
|
|
190
200
|
});
|
|
191
201
|
}
|
|
192
202
|
|
|
@@ -233,7 +243,7 @@ export class AIManager {
|
|
|
233
243
|
.find((plugin) => plugin.name === toolName);
|
|
234
244
|
if (toolPlugin?.formatCompactParams) {
|
|
235
245
|
const context: ToolContext = {
|
|
236
|
-
workdir: this.
|
|
246
|
+
workdir: this.getWorkdir(),
|
|
237
247
|
taskManager: this.taskManager,
|
|
238
248
|
};
|
|
239
249
|
return toolPlugin.formatCompactParams(toolArgs, context);
|
|
@@ -332,7 +342,7 @@ export class AIManager {
|
|
|
332
342
|
|
|
333
343
|
// 2. Working directory
|
|
334
344
|
contextParts.push(
|
|
335
|
-
`\n\n[Working Directory]\nCurrent working directory: ${this.
|
|
345
|
+
`\n\n[Working Directory]\nCurrent working directory: ${this.getWorkdir()}`,
|
|
336
346
|
);
|
|
337
347
|
|
|
338
348
|
// 3. Plan mode context
|
|
@@ -620,10 +630,10 @@ export class AIManager {
|
|
|
620
630
|
|
|
621
631
|
if (this.getAutoMemoryEnabled()) {
|
|
622
632
|
const directory = this.memoryService.getAutoMemoryDirectory(
|
|
623
|
-
this.
|
|
633
|
+
this.getWorkdir(),
|
|
624
634
|
);
|
|
625
635
|
const content = await this.memoryService.getAutoMemoryContent(
|
|
626
|
-
this.
|
|
636
|
+
this.getWorkdir(),
|
|
627
637
|
);
|
|
628
638
|
autoMemoryOptions = { directory, content };
|
|
629
639
|
}
|
|
@@ -635,14 +645,14 @@ export class AIManager {
|
|
|
635
645
|
messages: recentMessages,
|
|
636
646
|
sessionId: this.messageManager.getSessionId(),
|
|
637
647
|
abortSignal: abortController.signal,
|
|
638
|
-
workdir: this.
|
|
648
|
+
workdir: this.getWorkdir(), // Pass working directory
|
|
639
649
|
tools: toolsConfig, // Pass filtered tool configuration
|
|
640
650
|
model: model, // Use passed model
|
|
641
651
|
systemPrompt: buildSystemPrompt(
|
|
642
652
|
this.systemPrompt,
|
|
643
653
|
filteredToolPlugins,
|
|
644
654
|
{
|
|
645
|
-
workdir: this.
|
|
655
|
+
workdir: this.getWorkdir(),
|
|
646
656
|
memory: combinedMemory,
|
|
647
657
|
language: this.getLanguage(),
|
|
648
658
|
isSubagent: !!this.subagentType,
|
|
@@ -892,7 +902,7 @@ export class AIManager {
|
|
|
892
902
|
const context: ToolContext = {
|
|
893
903
|
abortSignal: toolAbortController.signal,
|
|
894
904
|
backgroundTaskManager: this.backgroundTaskManager,
|
|
895
|
-
workdir: this.
|
|
905
|
+
workdir: this.getWorkdir(),
|
|
896
906
|
messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
|
|
897
907
|
sessionId: this.messageManager.getSessionId(),
|
|
898
908
|
toolCallId: toolId,
|
|
@@ -952,6 +962,11 @@ export class AIManager {
|
|
|
952
962
|
toolArgs,
|
|
953
963
|
toolResult,
|
|
954
964
|
);
|
|
965
|
+
|
|
966
|
+
// Track discovered tools from ToolSearch results
|
|
967
|
+
if (toolName === "ToolSearch" && toolResult.success) {
|
|
968
|
+
this.trackDiscoveredTools(toolResult.content);
|
|
969
|
+
}
|
|
955
970
|
} catch (toolError) {
|
|
956
971
|
const errorMessage =
|
|
957
972
|
toolError instanceof Error
|
|
@@ -1196,11 +1211,11 @@ export class AIManager {
|
|
|
1196
1211
|
|
|
1197
1212
|
const context: ExtendedHookExecutionContext = {
|
|
1198
1213
|
event: hookName,
|
|
1199
|
-
projectDir: this.
|
|
1214
|
+
projectDir: this.getWorkdir(),
|
|
1200
1215
|
timestamp: new Date(),
|
|
1201
1216
|
sessionId: this.messageManager.getSessionId(),
|
|
1202
1217
|
transcriptPath: this.messageManager.getTranscriptPath(),
|
|
1203
|
-
cwd: this.
|
|
1218
|
+
cwd: this.getWorkdir(),
|
|
1204
1219
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
1205
1220
|
// Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
|
|
1206
1221
|
env: Object.fromEntries(
|
|
@@ -1252,7 +1267,7 @@ export class AIManager {
|
|
|
1252
1267
|
if (autoMemoryService) {
|
|
1253
1268
|
// Trigger extraction, but don't block the return.
|
|
1254
1269
|
// onTurnEnd itself returns quickly after forking.
|
|
1255
|
-
autoMemoryService.onTurnEnd(this.
|
|
1270
|
+
autoMemoryService.onTurnEnd(this.getWorkdir()).catch((err) => {
|
|
1256
1271
|
logger?.error("Auto-memory extraction trigger failed:", err);
|
|
1257
1272
|
});
|
|
1258
1273
|
}
|
|
@@ -1283,12 +1298,12 @@ export class AIManager {
|
|
|
1283
1298
|
try {
|
|
1284
1299
|
const context: ExtendedHookExecutionContext = {
|
|
1285
1300
|
event: "PreToolUse",
|
|
1286
|
-
projectDir: this.
|
|
1301
|
+
projectDir: this.getWorkdir(),
|
|
1287
1302
|
timestamp: new Date(),
|
|
1288
1303
|
toolName,
|
|
1289
1304
|
sessionId: this.messageManager.getSessionId(),
|
|
1290
1305
|
transcriptPath: this.messageManager.getTranscriptPath(),
|
|
1291
|
-
cwd: this.
|
|
1306
|
+
cwd: this.getWorkdir(),
|
|
1292
1307
|
toolInput,
|
|
1293
1308
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
1294
1309
|
env: Object.fromEntries(
|
|
@@ -1350,12 +1365,12 @@ export class AIManager {
|
|
|
1350
1365
|
try {
|
|
1351
1366
|
const context: ExtendedHookExecutionContext = {
|
|
1352
1367
|
event: "PostToolUse",
|
|
1353
|
-
projectDir: this.
|
|
1368
|
+
projectDir: this.getWorkdir(),
|
|
1354
1369
|
timestamp: new Date(),
|
|
1355
1370
|
toolName,
|
|
1356
1371
|
sessionId: this.messageManager.getSessionId(),
|
|
1357
1372
|
transcriptPath: this.messageManager.getTranscriptPath(),
|
|
1358
|
-
cwd: this.
|
|
1373
|
+
cwd: this.getWorkdir(),
|
|
1359
1374
|
toolInput,
|
|
1360
1375
|
toolResponse,
|
|
1361
1376
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
@@ -1397,4 +1412,44 @@ export class AIManager {
|
|
|
1397
1412
|
logger?.error("PostToolUse hook execution failed:", error);
|
|
1398
1413
|
}
|
|
1399
1414
|
}
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* Parse ToolSearch result content to extract discovered tool names.
|
|
1418
|
+
* ToolSearch returns content like "ToolName: description\nParameters: {...}"
|
|
1419
|
+
* or shortResult like "Discovered tools: ToolA, ToolB".
|
|
1420
|
+
*/
|
|
1421
|
+
private trackDiscoveredTools(content: string): void {
|
|
1422
|
+
// Try to extract tool names from shortResult-style content
|
|
1423
|
+
const discoveredMatch = content.match(/Discovered tools?: ([\w-, ]+)/);
|
|
1424
|
+
if (discoveredMatch) {
|
|
1425
|
+
const names = discoveredMatch[1]!
|
|
1426
|
+
.split(",")
|
|
1427
|
+
.map((n) => n.trim())
|
|
1428
|
+
.filter(Boolean);
|
|
1429
|
+
for (const name of names) {
|
|
1430
|
+
this.discoveredTools.add(name);
|
|
1431
|
+
}
|
|
1432
|
+
logger?.debug("Discovered tools:", names);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Fallback: extract tool names from "ToolName: description" pattern
|
|
1437
|
+
const lines = content.split("\n");
|
|
1438
|
+
const nonToolKeywords = new Set([
|
|
1439
|
+
"parameters",
|
|
1440
|
+
"description",
|
|
1441
|
+
"result",
|
|
1442
|
+
"error",
|
|
1443
|
+
"content",
|
|
1444
|
+
"type",
|
|
1445
|
+
"properties",
|
|
1446
|
+
"required",
|
|
1447
|
+
]);
|
|
1448
|
+
for (const line of lines) {
|
|
1449
|
+
const toolMatch = line.match(/^([\w-]+):/);
|
|
1450
|
+
if (toolMatch && !nonToolKeywords.has(toolMatch[1]!.toLowerCase())) {
|
|
1451
|
+
this.discoveredTools.add(toolMatch[1]!);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1400
1455
|
}
|
|
@@ -377,6 +377,11 @@ export class HookManager {
|
|
|
377
377
|
messageManager.addErrorBlock(errorMessage);
|
|
378
378
|
return { shouldBlock: false };
|
|
379
379
|
|
|
380
|
+
case "WorktreeRemove":
|
|
381
|
+
// Non-blocking for cleanup, log error but don't block
|
|
382
|
+
messageManager.addErrorBlock(errorMessage);
|
|
383
|
+
return { shouldBlock: false };
|
|
384
|
+
|
|
380
385
|
case "SessionStart":
|
|
381
386
|
// Non-blocking for startup, show error in error block
|
|
382
387
|
messageManager.addErrorBlock(errorMessage);
|
|
@@ -591,6 +596,7 @@ export class HookManager {
|
|
|
591
596
|
event === "Stop" ||
|
|
592
597
|
event === "SubagentStop" ||
|
|
593
598
|
event === "WorktreeCreate" ||
|
|
599
|
+
event === "WorktreeRemove" ||
|
|
594
600
|
event === "SessionStart" ||
|
|
595
601
|
event === "SessionEnd") &&
|
|
596
602
|
context.toolName !== undefined
|
|
@@ -669,6 +675,7 @@ export class HookManager {
|
|
|
669
675
|
event === "Stop" ||
|
|
670
676
|
event === "SubagentStop" ||
|
|
671
677
|
event === "WorktreeCreate" ||
|
|
678
|
+
event === "WorktreeRemove" ||
|
|
672
679
|
event === "SessionStart" ||
|
|
673
680
|
event === "SessionEnd"
|
|
674
681
|
) {
|
|
@@ -731,6 +738,7 @@ export class HookManager {
|
|
|
731
738
|
event === "Stop" ||
|
|
732
739
|
event === "SubagentStop" ||
|
|
733
740
|
event === "WorktreeCreate" ||
|
|
741
|
+
event === "WorktreeRemove" ||
|
|
734
742
|
event === "SessionStart" ||
|
|
735
743
|
event === "SessionEnd") &&
|
|
736
744
|
config.matcher
|
|
@@ -772,6 +780,7 @@ export class HookManager {
|
|
|
772
780
|
SubagentStop: 0,
|
|
773
781
|
PermissionRequest: 0,
|
|
774
782
|
WorktreeCreate: 0,
|
|
783
|
+
WorktreeRemove: 0,
|
|
775
784
|
SessionStart: 0,
|
|
776
785
|
SessionEnd: 0,
|
|
777
786
|
},
|
|
@@ -786,6 +795,7 @@ export class HookManager {
|
|
|
786
795
|
SubagentStop: 0,
|
|
787
796
|
PermissionRequest: 0,
|
|
788
797
|
WorktreeCreate: 0,
|
|
798
|
+
WorktreeRemove: 0,
|
|
789
799
|
SessionStart: 0,
|
|
790
800
|
SessionEnd: 0,
|
|
791
801
|
};
|