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
@@ -0,0 +1,242 @@
1
+ /**
2
+ * ExitWorktree tool - exits a worktree session and returns to the original directory.
3
+ * Mirrors Claude Code's ExitWorktree tool behavior and prompt.
4
+ */
5
+
6
+ import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
7
+ import {
8
+ getCurrentWorktreeSession,
9
+ setCurrentWorktreeSession,
10
+ } from "../utils/worktreeSession.js";
11
+ import {
12
+ removeWorktree,
13
+ countWorktreeChanges,
14
+ } from "../utils/worktreeUtils.js";
15
+ import { EXIT_WORKTREE_TOOL_NAME } from "../constants/tools.js";
16
+ import { logger } from "../utils/globalLogger.js";
17
+
18
+ export const EXIT_WORKTREE_TOOL_PROMPT = `Exit a worktree session created by EnterWorktree and return the session to the original working directory.
19
+
20
+ ## Scope
21
+
22
+ This tool ONLY operates on worktrees created by EnterWorktree in this session. It will NOT touch:
23
+ - Worktrees you created manually with \`git worktree add\`
24
+ - Worktrees from a previous session (even if created by EnterWorktree then)
25
+ - The directory you're in if EnterWorktree was never called
26
+
27
+ If called outside an EnterWorktree session, the tool is a **no-op**: it reports that no worktree session is active and takes no action. Filesystem state is unchanged.
28
+
29
+ ## When to Use
30
+
31
+ - The user explicitly asks to "exit the worktree", "leave the worktree", "go back", or otherwise end the worktree session
32
+ - Do NOT call this proactively — only when the user asks
33
+
34
+ ## Parameters
35
+
36
+ - \`action\` (required): \`"keep"\` or \`"remove"\`
37
+ - \`"keep"\` — leave the worktree directory and branch intact on disk. Use this if the user wants to come back to the work later, or if there are changes to preserve.
38
+ - \`"remove"\` — delete the worktree directory and its branch. Use this for a clean exit when the work is done or abandoned.
39
+ - \`discard_changes\` (optional, default false): only meaningful with \`action: "remove"\`. If the worktree has uncommitted files or commits not on the original branch, the tool will REFUSE to remove it unless this is set to \`true\`. If the tool returns an error listing changes, confirm with the user before re-invoking with \`discard_changes: true\`.
40
+
41
+ ## Behavior
42
+
43
+ - Restores the session's working directory to where it was before EnterWorktree
44
+ - If action is "remove": deletes the worktree directory and branch
45
+ - Once exited, EnterWorktree can be called again to create a fresh worktree
46
+ `;
47
+
48
+ export const exitWorktreeTool: ToolPlugin = {
49
+ name: EXIT_WORKTREE_TOOL_NAME,
50
+ shouldDefer: true,
51
+ config: {
52
+ type: "function",
53
+ function: {
54
+ name: EXIT_WORKTREE_TOOL_NAME,
55
+ description: EXIT_WORKTREE_TOOL_PROMPT,
56
+ parameters: {
57
+ type: "object",
58
+ properties: {
59
+ action: {
60
+ type: "string",
61
+ enum: ["keep", "remove"],
62
+ description:
63
+ '"keep" leaves the worktree and branch on disk; "remove" deletes both.',
64
+ },
65
+ discard_changes: {
66
+ type: "boolean",
67
+ description:
68
+ 'Required true when action is "remove" and the worktree has uncommitted files or unmerged commits. The tool will refuse and list them otherwise.',
69
+ },
70
+ },
71
+ required: ["action"],
72
+ },
73
+ },
74
+ },
75
+ prompt: () => EXIT_WORKTREE_TOOL_PROMPT,
76
+
77
+ async execute(
78
+ args: Record<string, unknown>,
79
+ context: ToolContext,
80
+ ): Promise<ToolResult> {
81
+ const action = args.action as "keep" | "remove" | undefined;
82
+ const discardChanges = (args.discard_changes as boolean) ?? false;
83
+
84
+ if (!action) {
85
+ return {
86
+ success: false,
87
+ content:
88
+ 'Missing required parameter: "action" must be "keep" or "remove".',
89
+ error: 'Missing required parameter: "action"',
90
+ };
91
+ }
92
+
93
+ // Validate: must be in an active worktree session
94
+ const session = getCurrentWorktreeSession();
95
+ if (!session) {
96
+ return {
97
+ success: false,
98
+ content:
99
+ "No-op: there is no active EnterWorktree session to exit. This tool only operates on worktrees created by EnterWorktree in the current session — it will not touch worktrees created manually or in a previous session. No filesystem changes were made.",
100
+ error: "No active worktree session",
101
+ };
102
+ }
103
+
104
+ // Safety check for removal with changes
105
+ if (action === "remove" && !discardChanges) {
106
+ const summary = countWorktreeChanges(
107
+ session.worktreePath,
108
+ session.originalHeadCommit,
109
+ );
110
+ if (summary === null) {
111
+ return {
112
+ success: false,
113
+ content: `Could not verify worktree state at ${session.worktreePath}. Refusing to remove without explicit confirmation. Re-invoke with discard_changes: true to proceed — or use action: "keep" to preserve the worktree.`,
114
+ error: "Could not verify worktree state",
115
+ };
116
+ }
117
+ const { changedFiles, commits } = summary;
118
+ if (changedFiles > 0 || commits > 0) {
119
+ const parts: string[] = [];
120
+ if (changedFiles > 0) {
121
+ parts.push(
122
+ `${changedFiles} uncommitted ${changedFiles === 1 ? "file" : "files"}`,
123
+ );
124
+ }
125
+ if (commits > 0) {
126
+ parts.push(
127
+ `${commits} ${commits === 1 ? "commit" : "commits"} on ${session.worktreeBranch ?? "the worktree branch"}`,
128
+ );
129
+ }
130
+ return {
131
+ success: false,
132
+ content: `Worktree has ${parts.join(" and ")}. Removing will discard this work permanently. Confirm with the user, then re-invoke with discard_changes: true — or use action: "keep" to preserve the worktree.`,
133
+ error: "Worktree has uncommitted changes",
134
+ };
135
+ }
136
+ }
137
+
138
+ // Capture info before clearing session
139
+ const { originalCwd, worktreePath, worktreeBranch } = session;
140
+
141
+ if (action === "keep") {
142
+ // Clear session state
143
+ setCurrentWorktreeSession(null);
144
+
145
+ // Restore CWD
146
+ const aiManager = context.aiManager;
147
+ if (aiManager) {
148
+ aiManager.setWorkdir(originalCwd);
149
+ }
150
+
151
+ return {
152
+ success: true,
153
+ content: `Exited worktree. Your work is preserved at ${worktreePath}${worktreeBranch ? ` on branch ${worktreeBranch}` : ""}. Session is now back in ${originalCwd}.`,
154
+ };
155
+ }
156
+
157
+ // action === "remove"
158
+ const worktreeInfo = {
159
+ name: session.worktreeName,
160
+ path: worktreePath,
161
+ branch: worktreeBranch,
162
+ repoRoot: session.repoRoot,
163
+ isNew: session.isNew,
164
+ };
165
+
166
+ // Count changes BEFORE removing the worktree (directory will be gone after)
167
+ const summary = countWorktreeChanges(
168
+ worktreePath,
169
+ session.originalHeadCommit,
170
+ ) ?? { changedFiles: 0, commits: 0 };
171
+
172
+ removeWorktree(worktreeInfo);
173
+
174
+ // Clear session state
175
+ setCurrentWorktreeSession(null);
176
+
177
+ // Restore CWD
178
+ const aiManager = context.aiManager;
179
+ if (aiManager) {
180
+ aiManager.setWorkdir(originalCwd);
181
+ }
182
+
183
+ // Trigger WorktreeRemove hook (non-blocking)
184
+ let hookTriggered = false;
185
+ if (context.hookManager) {
186
+ try {
187
+ const hookResults = await context.hookManager.executeHooks(
188
+ "WorktreeRemove",
189
+ {
190
+ event: "WorktreeRemove",
191
+ projectDir: originalCwd,
192
+ timestamp: new Date(),
193
+ sessionId: context.sessionId ?? "",
194
+ transcriptPath: context.messageManager?.getTranscriptPath() ?? "",
195
+ cwd: originalCwd,
196
+ worktreePath,
197
+ env: Object.fromEntries(
198
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
199
+ ) as Record<string, string>,
200
+ },
201
+ );
202
+
203
+ if (context.messageManager) {
204
+ context.hookManager.processHookResults(
205
+ "WorktreeRemove",
206
+ hookResults,
207
+ context.messageManager,
208
+ );
209
+ }
210
+
211
+ hookTriggered = true;
212
+ } catch (error) {
213
+ // Non-blocking: log but don't fail the tool
214
+ logger?.warn("WorktreeRemove hooks execution failed:", error);
215
+ }
216
+ }
217
+
218
+ const discardParts: string[] = [];
219
+ if (summary.commits > 0) {
220
+ discardParts.push(
221
+ `${summary.commits} ${summary.commits === 1 ? "commit" : "commits"}`,
222
+ );
223
+ }
224
+ if (summary.changedFiles > 0) {
225
+ discardParts.push(
226
+ `${summary.changedFiles} uncommitted ${summary.changedFiles === 1 ? "file" : "files"}`,
227
+ );
228
+ }
229
+ const discardNote =
230
+ discardParts.length > 0
231
+ ? ` Discarded ${discardParts.join(" and ")}.`
232
+ : "";
233
+ const hookNote = hookTriggered
234
+ ? " WorktreeRemove hooks were executed."
235
+ : "";
236
+
237
+ return {
238
+ success: true,
239
+ content: `Exited and removed worktree at ${worktreePath}.${discardNote} Session is now back in ${originalCwd}.${hookNote}`,
240
+ };
241
+ },
242
+ };
@@ -148,19 +148,39 @@ export const skillTool: ToolPlugin = {
148
148
  // Update shortResult
149
149
  const messages = instance.messageManager.getMessages();
150
150
  const tokens = instance.messageManager.getLatestTotalTokens();
151
- const lastTools = instance.lastTools;
151
+ const usedTools = instance.usedTools;
152
152
 
153
153
  const toolCount = countToolBlocks(messages);
154
154
  const summary = formatToolTokenSummary(toolCount, tokens);
155
155
 
156
+ const getDisplayParam = (t: {
157
+ name: string;
158
+ parameters: string;
159
+ compactParams?: string;
160
+ stage?: string;
161
+ }) => {
162
+ if (
163
+ (t.stage === "end" || t.stage === "running") &&
164
+ t.compactParams
165
+ ) {
166
+ return t.compactParams;
167
+ }
168
+ const flat = t.parameters.replace(/\n/g, "\\n");
169
+ return flat.length > 30 ? `…${flat.slice(-30)}` : flat;
170
+ };
171
+
156
172
  let shortResult = "";
157
173
  if (toolCount > 2) {
158
174
  shortResult += "... ";
159
175
  }
160
- if (lastTools.length > 0) {
161
- shortResult += `${lastTools.join(", ")} `;
162
- }
163
176
  shortResult += summary;
177
+ if (usedTools.length > 0) {
178
+ shortResult +=
179
+ "\n" +
180
+ usedTools
181
+ .map((t) => `${t.name} ${getDisplayParam(t)}`)
182
+ .join("\n");
183
+ }
164
184
 
165
185
  context.onShortResultUpdate?.(shortResult);
166
186
  },
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  export const taskCreateTool: ToolPlugin = {
11
11
  name: TASK_CREATE_TOOL_NAME,
12
+ shouldDefer: true,
12
13
  config: {
13
14
  type: "function",
14
15
  function: {
@@ -143,6 +144,7 @@ NOTE that you should not use this tool if there is only one trivial task to do.
143
144
 
144
145
  export const taskGetTool: ToolPlugin = {
145
146
  name: TASK_GET_TOOL_NAME,
147
+ shouldDefer: true,
146
148
  config: {
147
149
  type: "function",
148
150
  function: {
@@ -202,6 +204,7 @@ Returns full task details:
202
204
 
203
205
  export const taskUpdateTool: ToolPlugin = {
204
206
  name: TASK_UPDATE_TOOL_NAME,
207
+ shouldDefer: true,
205
208
  config: {
206
209
  type: "function",
207
210
  function: {
@@ -556,6 +559,7 @@ Set up task dependencies:
556
559
 
557
560
  export const taskListTool: ToolPlugin = {
558
561
  name: TASK_LIST_TOOL_NAME,
562
+ shouldDefer: true,
559
563
  config: {
560
564
  type: "function",
561
565
  function: {
@@ -0,0 +1,228 @@
1
+ /**
2
+ * ToolSearchTool - Discovers deferred tool schemas on demand.
3
+ *
4
+ * When tool deferral is enabled, deferred tools are not sent to the API.
5
+ * The model must call this tool to discover a deferred tool's full schema
6
+ * before it can invoke it.
7
+ *
8
+ * Query formats:
9
+ * - "select:ToolName" — direct selection by name (comma-separated for multiple)
10
+ * - "notebook jupyter" — keyword search, up to max_results best matches
11
+ * - "+slack send" — require "slack" in the name, rank by remaining terms
12
+ */
13
+
14
+ import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
15
+ import {
16
+ isDeferredTool,
17
+ TOOL_SEARCH_TOOL_NAME,
18
+ } from "../utils/isDeferredTool.js";
19
+
20
+ function formatSchema(tool: ToolPlugin): string {
21
+ const desc = tool.config.function.description || "";
22
+ const params = JSON.stringify(tool.config.function.parameters || {}, null, 2);
23
+ return `${tool.name}: ${desc}\nParameters: ${params}`;
24
+ }
25
+
26
+ /**
27
+ * Parse tool name into searchable parts (handles CamelCase and underscores).
28
+ */
29
+ function parseToolName(name: string): string[] {
30
+ return name
31
+ .replace(/([a-z])([A-Z])/g, "$1 $2") // CamelCase to spaces
32
+ .replace(/_/g, " ")
33
+ .toLowerCase()
34
+ .split(/\s+/)
35
+ .filter(Boolean);
36
+ }
37
+
38
+ /**
39
+ * Keyword search over deferred tools by name and description.
40
+ * Matches Claude Code's scoring: required terms (+prefix) must all match,
41
+ * optional terms contribute to ranking.
42
+ */
43
+ function keywordSearch(
44
+ query: string,
45
+ deferredTools: ToolPlugin[],
46
+ maxResults: number,
47
+ ): ToolPlugin[] {
48
+ const queryLower = query.toLowerCase().trim();
49
+ const queryTerms = queryLower.split(/\s+/).filter(Boolean);
50
+
51
+ // Exact match fast path
52
+ const exact = deferredTools.find((t) => t.name.toLowerCase() === queryLower);
53
+ if (exact) return [exact];
54
+
55
+ // Partition into required (+prefixed) and optional terms
56
+ const requiredTerms: string[] = [];
57
+ const optionalTerms: string[] = [];
58
+ for (const term of queryTerms) {
59
+ if (term.startsWith("+") && term.length > 1) {
60
+ requiredTerms.push(term.slice(1));
61
+ } else {
62
+ optionalTerms.push(term);
63
+ }
64
+ }
65
+
66
+ const allScoringTerms =
67
+ requiredTerms.length > 0
68
+ ? [...requiredTerms, ...optionalTerms]
69
+ : queryTerms;
70
+
71
+ // Pre-filter to tools matching ALL required terms
72
+ let candidateTools = deferredTools;
73
+ if (requiredTerms.length > 0) {
74
+ candidateTools = deferredTools.filter((tool) => {
75
+ const parts = parseToolName(tool.name);
76
+ const desc = (tool.config.function.description || "").toLowerCase();
77
+ return requiredTerms.every(
78
+ (term) =>
79
+ parts.includes(term) ||
80
+ parts.some((p) => p.includes(term)) ||
81
+ desc.includes(term),
82
+ );
83
+ });
84
+ }
85
+
86
+ // Score each tool
87
+ const scored = candidateTools
88
+ .map((tool) => {
89
+ const parts = parseToolName(tool.name);
90
+ const desc = (tool.config.function.description || "").toLowerCase();
91
+ let score = 0;
92
+
93
+ for (const term of allScoringTerms) {
94
+ // Exact part match (high weight)
95
+ if (parts.includes(term)) {
96
+ score += tool.isMcp ? 12 : 10;
97
+ } else if (parts.some((p) => p.includes(term))) {
98
+ score += tool.isMcp ? 6 : 5;
99
+ }
100
+
101
+ // Full name fallback
102
+ if (tool.name.toLowerCase().includes(term) && score === 0) {
103
+ score += 3;
104
+ }
105
+
106
+ // Description match
107
+ if (desc.includes(term)) {
108
+ score += 2;
109
+ }
110
+ }
111
+
112
+ return { tool, score };
113
+ })
114
+ .filter((s) => s.score > 0)
115
+ .sort((a, b) => b.score - a.score)
116
+ .slice(0, maxResults)
117
+ .map((s) => s.tool);
118
+
119
+ return scored;
120
+ }
121
+
122
+ export const toolSearchTool: ToolPlugin = {
123
+ name: TOOL_SEARCH_TOOL_NAME,
124
+ config: {
125
+ type: "function",
126
+ function: {
127
+ name: TOOL_SEARCH_TOOL_NAME,
128
+ description: `Fetches full schema definitions for deferred tools so they can be called.
129
+
130
+ Deferred tools appear by name in <available-deferred-tools> messages. Until fetched, only the name is known — there is no parameter schema, so the tool cannot be invoked. This tool takes a query, matches it against the deferred tool list, and returns the matched tools' complete JSONSchema definitions inside a <functions> block. Once a tool's schema appears in that result, it is callable exactly like any tool defined at the top of the prompt.
131
+
132
+ Result format: each matched tool appears as one <function>{"description": "...", "name": "...", "parameters": {...}}`,
133
+ },
134
+ },
135
+ shouldDefer: false, // Always available
136
+ execute: async (
137
+ args: Record<string, unknown>,
138
+ context: ToolContext,
139
+ ): Promise<ToolResult> => {
140
+ const { query, max_results = 5 } = args as {
141
+ query?: string;
142
+ max_results?: number;
143
+ };
144
+
145
+ if (!query) {
146
+ return {
147
+ success: false,
148
+ content: "",
149
+ error: "Missing required 'query' parameter",
150
+ };
151
+ }
152
+
153
+ if (!context.toolManager) {
154
+ return {
155
+ success: false,
156
+ content: "",
157
+ error: "ToolManager not available in context",
158
+ };
159
+ }
160
+
161
+ const allTools = context.toolManager.list();
162
+ const deferredTools = allTools.filter(isDeferredTool);
163
+
164
+ // Handle select: prefix
165
+ const selectMatch = query.match(/^select:(.+)$/i);
166
+ if (selectMatch) {
167
+ const requested = selectMatch[1]!
168
+ .split(",")
169
+ .map((s) => s.trim())
170
+ .filter(Boolean);
171
+
172
+ const found: ToolPlugin[] = [];
173
+ const missing: string[] = [];
174
+
175
+ for (const toolName of requested) {
176
+ const tool =
177
+ deferredTools.find((t) => t.name === toolName) ??
178
+ allTools.find((t) => t.name === toolName);
179
+ if (tool) {
180
+ if (!found.some((f) => f.name === tool.name)) found.push(tool);
181
+ } else {
182
+ missing.push(toolName);
183
+ }
184
+ }
185
+
186
+ if (found.length === 0) {
187
+ return {
188
+ success: false,
189
+ content: "",
190
+ error: `No matching deferred tools found for: ${missing.join(", ")}`,
191
+ };
192
+ }
193
+
194
+ const result = found.map(formatSchema).join("\n\n---\n\n");
195
+ const shortResult = `Discovered tools: ${found.map((t) => t.name).join(", ")}`;
196
+
197
+ return {
198
+ success: true,
199
+ content: result,
200
+ shortResult,
201
+ };
202
+ }
203
+
204
+ // Keyword search
205
+ const matches = keywordSearch(query, deferredTools, max_results);
206
+
207
+ if (matches.length === 0) {
208
+ return {
209
+ success: false,
210
+ content: "",
211
+ error: `No matching deferred tools found for query: "${query}". Available deferred tools: ${getDeferredToolNamesList(deferredTools)}`,
212
+ };
213
+ }
214
+
215
+ const result = matches.map(formatSchema).join("\n\n---\n\n");
216
+ const shortResult = `Found ${matches.length} tools: ${matches.map((t) => t.name).join(", ")}`;
217
+
218
+ return {
219
+ success: true,
220
+ content: result,
221
+ shortResult,
222
+ };
223
+ },
224
+ };
225
+
226
+ function getDeferredToolNamesList(tools: ToolPlugin[]): string {
227
+ return tools.map((t) => t.name).join(", ");
228
+ }
@@ -31,6 +31,21 @@ export interface ToolPlugin {
31
31
  workdir?: string;
32
32
  isSubagent?: boolean;
33
33
  }) => string;
34
+ /**
35
+ * When true, this tool is deferred — it's not sent to the API until the model
36
+ * discovers it via ToolSearch. MCP tools are always deferred.
37
+ */
38
+ shouldDefer?: boolean;
39
+ /**
40
+ * When true, this tool is never deferred — its full schema always appears in
41
+ * the initial prompt even when tool search is enabled.
42
+ */
43
+ alwaysLoad?: boolean;
44
+ /**
45
+ * When true, this is an MCP tool (auto-set by McpManager). MCP tools are
46
+ * always deferred unless they have alwaysLoad: true.
47
+ */
48
+ isMcp?: boolean;
34
49
  }
35
50
 
36
51
  export interface ToolResult {
@@ -58,6 +73,8 @@ export interface ToolContext {
58
73
  abortSignal?: AbortSignal;
59
74
  backgroundTaskManager?: import("../managers/backgroundTaskManager.js").BackgroundTaskManager;
60
75
  workdir: string;
76
+ /** Tool manager instance for tool discovery (used by ToolSearchTool) */
77
+ toolManager?: import("../managers/toolManager.js").ToolManager;
61
78
  /** Permission mode for this tool execution */
62
79
  permissionMode?: PermissionMode;
63
80
  /** Custom permission callback */
@@ -103,4 +120,6 @@ export interface ToolContext {
103
120
  };
104
121
  /** State of files read in the current session for deduplication */
105
122
  readFileState?: Map<string, { mtime: number; hash: string }>;
123
+ /** Hook manager instance for executing hooks */
124
+ hookManager?: import("../managers/hookManager.js").HookManager;
106
125
  }
@@ -103,6 +103,7 @@ const GITHUB_URL_ERROR =
103
103
 
104
104
  export const webFetchTool: ToolPlugin = {
105
105
  name: WEB_FETCH_TOOL_NAME,
106
+ shouldDefer: true,
106
107
  config: {
107
108
  type: "function",
108
109
  function: {
@@ -8,6 +8,7 @@ import type {
8
8
  ILspManager,
9
9
  PluginConfig,
10
10
  BackgroundTask,
11
+ McpServerConfig,
11
12
  } from "./index.js";
12
13
  import type { MessageManagerCallbacks } from "../managers/messageManager.js";
13
14
  import type { BackgroundTaskManagerCallbacks } from "../managers/backgroundTaskManager.js";
@@ -78,6 +79,11 @@ export interface AgentOptions {
78
79
  * These rules follow the standard permission rule syntax: `ToolName` or `ToolName(pattern)`.
79
80
  */
80
81
  disallowedTools?: string[];
82
+ /**
83
+ * Optional MCP server configs to connect at startup.
84
+ * Overrides .mcp.json for specified server names.
85
+ */
86
+ mcpServers?: Record<string, McpServerConfig>;
81
87
  [key: string]: unknown;
82
88
  }
83
89
 
@@ -21,6 +21,7 @@ export type HookEvent =
21
21
  | "SubagentStop"
22
22
  | "PermissionRequest"
23
23
  | "WorktreeCreate"
24
+ | "WorktreeRemove"
24
25
  | "SessionStart"
25
26
  | "SessionEnd";
26
27
 
@@ -45,6 +46,8 @@ export interface HookExecutionContext {
45
46
  toolName?: string; // Present for PreToolUse/PostToolUse events
46
47
  projectDir: string; // Absolute path for $WAVE_PROJECT_DIR
47
48
  timestamp: Date;
49
+ worktreeName?: string; // Present for WorktreeCreate
50
+ worktreePath?: string; // Present for WorktreeRemove
48
51
  }
49
52
 
50
53
  // Result of hook execution
@@ -109,6 +112,7 @@ export function isValidHookEvent(event: string): event is HookEvent {
109
112
  "SubagentStop",
110
113
  "PermissionRequest",
111
114
  "WorktreeCreate",
115
+ "WorktreeRemove",
112
116
  "SessionStart",
113
117
  "SessionEnd",
114
118
  ].includes(event);
@@ -14,6 +14,7 @@ export interface Message {
14
14
  id: string; // Unique identifier for the message
15
15
  role: "user" | "assistant";
16
16
  blocks: MessageBlock[];
17
+ timestamp: string; // ISO 8601 timestamp, assigned at creation
17
18
  usage?: Usage; // Usage data for this message's AI operation (assistant messages only)
18
19
  additionalFields?: Record<string, unknown>; // Additional metadata from AI responses
19
20
  isMeta?: boolean; // Whether the message is a meta message (hidden from UI)
@@ -5,14 +5,6 @@
5
5
  * SIMPLIFIED: Removed unused interfaces to focus on core functionality
6
6
  */
7
7
 
8
- import type { Message } from "./messaging.js";
9
-
10
- // Enhanced message interface for JSONL storage (extends existing Message)
11
- export interface SessionMessage extends Message {
12
- timestamp: string; // ISO 8601 - added for JSONL format
13
- // Inherits: role: "user" | "assistant", blocks: MessageBlock[], usage?, additionalFields?
14
- }
15
-
16
8
  // Session filename structure for simple filename-based metadata
17
9
  export interface SessionFilename {
18
10
  sessionId: string;