wave-agent-sdk 1.1.5 → 1.3.0

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 (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. package/dist/managers/bangManager.js +0 -78
@@ -1,10 +1,12 @@
1
1
  import { promises as fs } from "fs";
2
2
  import { join } from "path";
3
+ import { homedir } from "os";
3
4
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
5
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
6
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
6
7
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7
- import { createMcpToolPlugin, findToolServer } from "../utils/mcpUtils.js";
8
+ import { createMcpToolPlugin, findToolServer, mcpToolFlatName, } from "../utils/mcpUtils.js";
9
+ import { truncateMcpInstructions } from "../utils/mcpInstructions.js";
8
10
  import { logger } from "../utils/globalLogger.js";
9
11
  /**
10
12
  * Expand environment variables in a string value.
@@ -73,9 +75,12 @@ export class McpManager {
73
75
  this.servers = new Map();
74
76
  this.connections = new Map();
75
77
  this.configPath = "";
78
+ this.userConfigPath = "";
76
79
  this.workdir = "";
77
80
  this.reconnectTimers = new Map();
78
81
  this.reconnectAttempts = new Map();
82
+ /** Explicit disconnectServer teardowns currently in flight */
83
+ this.disconnecting = new Set();
79
84
  this.callbacks = options.callbacks || {};
80
85
  this.mcpServers = options.mcpServers;
81
86
  }
@@ -94,6 +99,7 @@ export class McpManager {
94
99
  */
95
100
  async initialize(workdir, autoConnect = false) {
96
101
  this.configPath = join(workdir, ".mcp.json");
102
+ this.userConfigPath = join(homedir(), ".wave", "mcp.json");
97
103
  this.workdir = workdir;
98
104
  // Register constructor-provided servers before loading .mcp.json
99
105
  if (this.mcpServers) {
@@ -141,22 +147,40 @@ export class McpManager {
141
147
  return null;
142
148
  }
143
149
  try {
144
- const configContent = await fs.readFile(this.configPath, "utf-8");
145
- const rawConfig = JSON.parse(configContent);
146
- const workspaceConfig = resolveMcpConfig(rawConfig, this.envSnapshot);
150
+ // Load user-level (~/.wave/mcp.json) and project-level (<workdir>/.mcp.json)
151
+ // configs, then merge with project taking precedence over user-level.
152
+ const userConfig = await this.readConfigFile(this.userConfigPath);
153
+ const projectConfig = await this.readConfigFile(this.configPath);
154
+ // Neither file exists: preserve the original null contract (constructor-
155
+ // provided servers may still be present in this.config).
156
+ if (!userConfig && !projectConfig) {
157
+ return this.config;
158
+ }
159
+ const userResolved = userConfig
160
+ ? resolveMcpConfig(userConfig, this.envSnapshot)
161
+ : null;
162
+ const projectResolved = projectConfig
163
+ ? resolveMcpConfig(projectConfig, this.envSnapshot)
164
+ : null;
147
165
  // Extract original (pre-resolution) URLs for safe display
148
166
  const originalUrls = {};
149
- for (const [name, serverConfig] of Object.entries(rawConfig.mcpServers)) {
167
+ for (const [name, serverConfig] of Object.entries({
168
+ ...(userConfig?.mcpServers || {}),
169
+ ...(projectConfig?.mcpServers || {}),
170
+ })) {
150
171
  originalUrls[name] = serverConfig.url;
151
172
  }
152
- // Merge workspace config with any existing config (e.g., from plugins or constructor)
153
- // Constructor-provided servers take precedence, then workspace config, then existing config
173
+ // Merge: existing config (e.g. plugin servers) -> user -> project -> constructor
154
174
  const merged = { mcpServers: {} };
155
175
  if (this.config) {
156
176
  Object.assign(merged.mcpServers, this.config.mcpServers);
157
177
  }
158
- Object.assign(merged.mcpServers, workspaceConfig.mcpServers);
159
- // Constructor-provided servers override both for same names
178
+ if (userResolved) {
179
+ Object.assign(merged.mcpServers, userResolved.mcpServers);
180
+ }
181
+ if (projectResolved) {
182
+ Object.assign(merged.mcpServers, projectResolved.mcpServers);
183
+ }
160
184
  if (this.mcpServers) {
161
185
  Object.assign(merged.mcpServers, this.mcpServers);
162
186
  }
@@ -165,12 +189,27 @@ export class McpManager {
165
189
  if (this.config) {
166
190
  for (const [name, config] of Object.entries(this.config.mcpServers)) {
167
191
  const existingServer = this.servers.get(name);
192
+ // Determine scope: plugin servers registered with pluginRoot keep
193
+ // their plugin scope; user-level names come from ~/.wave/mcp.json.
194
+ let scope = existingServer?.scope;
195
+ if (!scope) {
196
+ if (config.pluginRoot) {
197
+ scope = "plugin";
198
+ }
199
+ else if (userConfig?.mcpServers?.[name]) {
200
+ scope = "user";
201
+ }
202
+ else {
203
+ scope = "project";
204
+ }
205
+ }
168
206
  if (existingServer) {
169
207
  // Update config but preserve status and other runtime info
170
208
  this.servers.set(name, {
171
209
  ...existingServer,
172
210
  config, // Update config in case it changed
173
211
  originalUrl: originalUrls[name] ?? existingServer.originalUrl,
212
+ scope,
174
213
  });
175
214
  }
176
215
  else {
@@ -179,6 +218,7 @@ export class McpManager {
179
218
  name,
180
219
  config,
181
220
  originalUrl: originalUrls[name],
221
+ scope,
182
222
  status: "disconnected",
183
223
  });
184
224
  }
@@ -189,11 +229,41 @@ export class McpManager {
189
229
  catch (error) {
190
230
  // Only log error if it's not a "file not found" error
191
231
  if (error.code !== "ENOENT") {
192
- logger?.error("Failed to load .mcp.json:", error);
232
+ logger?.error("Failed to load MCP config:", error);
193
233
  }
194
234
  return null;
195
235
  }
196
236
  }
237
+ /**
238
+ * Read a raw MCP config file, tolerating ENOENT and corrupt JSON.
239
+ */
240
+ async readConfigFile(configPath) {
241
+ if (!configPath)
242
+ return null;
243
+ try {
244
+ const content = await fs.readFile(configPath, "utf-8");
245
+ return JSON.parse(content);
246
+ }
247
+ catch (error) {
248
+ const code = error.code;
249
+ if (code === "ENOENT")
250
+ return null;
251
+ logger?.error("Failed to load .mcp.json:", error);
252
+ return null;
253
+ }
254
+ }
255
+ /**
256
+ * The user-level MCP config path (~/.wave/mcp.json).
257
+ */
258
+ getUserConfigPath() {
259
+ return this.userConfigPath;
260
+ }
261
+ /**
262
+ * The project-level MCP config path (<workdir>/.mcp.json).
263
+ */
264
+ getProjectConfigPath() {
265
+ return this.configPath;
266
+ }
197
267
  async saveConfig(config) {
198
268
  try {
199
269
  await fs.writeFile(this.configPath, JSON.stringify(config, null, 2));
@@ -214,6 +284,38 @@ export class McpManager {
214
284
  getServer(name) {
215
285
  return this.servers.get(name);
216
286
  }
287
+ /**
288
+ * The usage notes each usable server described about itself (`initialize`'s
289
+ * `instructions`), for the announcement appended to the conversation.
290
+ *
291
+ * "Usable" is the same predicate `getAllConnectedTools` uses — connected, or
292
+ * reconnecting with its last-known snapshot retained — so a server's prose is
293
+ * visible exactly while its tools are. A server that dropped, failed to connect
294
+ * or went to `error` keeps its snapshot but stops being reported here, which is
295
+ * what keeps a dead server from still talking to the model.
296
+ *
297
+ * `isDenied` receives flattened tool names (`mcp__server__tool`, the shape
298
+ * permission rules match on); a server whose tools are *all* excluded by rules is
299
+ * dropped whole, so prose cannot reach the context after the user ruled the
300
+ * server out. A server exposing no tools at all is still reported: nothing was
301
+ * excluded, it is simply a server that brings context rather than tools.
302
+ */
303
+ getServerInstructions(isDenied) {
304
+ const result = [];
305
+ for (const server of this.servers.values()) {
306
+ const usable = server.status === "connected" || server.status === "reconnecting";
307
+ const instructions = server.instructions?.trim();
308
+ if (!usable || !instructions)
309
+ continue;
310
+ const tools = server.tools ?? [];
311
+ if (tools.length > 0 &&
312
+ tools.every((tool) => isDenied?.(mcpToolFlatName(server.name, tool.name)))) {
313
+ continue;
314
+ }
315
+ result.push({ name: server.name, instructions });
316
+ }
317
+ return result;
318
+ }
217
319
  updateServerStatus(name, updates) {
218
320
  const server = this.servers.get(name);
219
321
  if (server) {
@@ -258,6 +360,7 @@ export class McpManager {
258
360
  name,
259
361
  config: resolvedConfig,
260
362
  originalUrl,
363
+ scope: resolvedConfig.pluginRoot ? "plugin" : undefined,
261
364
  status: "disconnected",
262
365
  };
263
366
  this.servers.set(name, newServer);
@@ -283,6 +386,55 @@ export class McpManager {
283
386
  }
284
387
  return removed;
285
388
  }
389
+ /**
390
+ * Drop every MCP server contributed by a plugin (matched on pluginRoot),
391
+ * disconnecting each one. Needed instead of re-adding under the same name
392
+ * because addServer refuses a name that is already registered.
393
+ * Called by the in-place plugin reload path before re-registering.
394
+ * @returns the number of servers removed
395
+ */
396
+ removeServersForPlugin(pluginRoot) {
397
+ let removed = 0;
398
+ for (const [name, server] of Array.from(this.servers.entries())) {
399
+ if (server.config.pluginRoot !== pluginRoot) {
400
+ continue;
401
+ }
402
+ this.removeServer(name);
403
+ removed += 1;
404
+ }
405
+ if (removed > 0) {
406
+ logger?.debug(`Removed ${removed} MCP servers for plugin root ${pluginRoot}`);
407
+ }
408
+ return removed;
409
+ }
410
+ /**
411
+ * Remove a server from a persisted config file (user or project scope),
412
+ * then disconnect and drop it from the in-memory registry.
413
+ * @param scope - "user" removes from ~/.wave/mcp.json; "project" removes from <workdir>/.mcp.json
414
+ * @param name - The server name
415
+ * @returns true if the server was found and removed
416
+ */
417
+ async removeServerFromConfig(scope, name) {
418
+ const configPath = scope === "user" ? this.userConfigPath : this.configPath;
419
+ if (!configPath)
420
+ return false;
421
+ const rawConfig = await this.readConfigFile(configPath);
422
+ if (!rawConfig ||
423
+ !rawConfig.mcpServers ||
424
+ !(name in rawConfig.mcpServers)) {
425
+ return false;
426
+ }
427
+ delete rawConfig.mcpServers[name];
428
+ if (Object.keys(rawConfig.mcpServers).length === 0) {
429
+ await fs.rm(configPath, { force: true });
430
+ }
431
+ else {
432
+ await fs.writeFile(configPath, JSON.stringify(rawConfig, null, 2));
433
+ }
434
+ // Drop from memory (disconnects if connected)
435
+ this.removeServer(name);
436
+ return true;
437
+ }
286
438
  // Real MCP connection implementation
287
439
  async connectServer(name) {
288
440
  const server = this.servers.get(name);
@@ -291,12 +443,20 @@ export class McpManager {
291
443
  // Already connected
292
444
  if (this.connections.has(name))
293
445
  return true;
446
+ // An explicit disconnectServer teardown is in flight for this server —
447
+ // don't race it (e.g. an auto-reconnect timer that just fired) by spawning
448
+ // a fresh process the user asked to stop.
449
+ if (this.disconnecting.has(name))
450
+ return false;
294
451
  this.updateServerStatus(name, { status: "connecting" });
295
452
  try {
296
453
  // Create transport - it will manage the process
297
454
  let transport;
298
455
  let client;
299
456
  let tools = [];
457
+ // Server-level usage notes from `initialize`; part of the connection's
458
+ // handshake, so it is read once here rather than re-requested later.
459
+ let instructions;
300
460
  const createClient = () => new Client({
301
461
  name: "wave-code",
302
462
  version: "1.0.0",
@@ -316,12 +476,14 @@ export class McpManager {
316
476
  });
317
477
  client = createClient();
318
478
  await client.connect(transport);
479
+ instructions = truncateMcpInstructions(client.getInstructions());
319
480
  const toolsResponse = await client.listTools();
320
481
  tools =
321
482
  toolsResponse.tools?.map((tool) => ({
322
483
  name: tool.name,
323
484
  description: tool.description,
324
485
  inputSchema: tool.inputSchema,
486
+ outputSchema: tool.outputSchema,
325
487
  })) || [];
326
488
  logger?.info(`Connected to MCP server ${name} using Streamable HTTP`);
327
489
  }
@@ -337,12 +499,14 @@ export class McpManager {
337
499
  });
338
500
  client = createClient();
339
501
  await client.connect(transport);
502
+ instructions = truncateMcpInstructions(client.getInstructions());
340
503
  const toolsResponse = await client.listTools();
341
504
  tools =
342
505
  toolsResponse.tools?.map((tool) => ({
343
506
  name: tool.name,
344
507
  description: tool.description,
345
508
  inputSchema: tool.inputSchema,
509
+ outputSchema: tool.outputSchema,
346
510
  })) || [];
347
511
  logger?.info(`Connected to MCP server ${name} using SSE`);
348
512
  }
@@ -414,12 +578,14 @@ export class McpManager {
414
578
  logger?.debug(`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`);
415
579
  stderrOutput = "";
416
580
  }
581
+ instructions = truncateMcpInstructions(client.getInstructions());
417
582
  const toolsResponse = await client.listTools();
418
583
  tools =
419
584
  toolsResponse.tools?.map((tool) => ({
420
585
  name: tool.name,
421
586
  description: tool.description,
422
587
  inputSchema: tool.inputSchema,
588
+ outputSchema: tool.outputSchema,
423
589
  })) || [];
424
590
  }
425
591
  else if (serverType) {
@@ -454,14 +620,23 @@ export class McpManager {
454
620
  };
455
621
  transport.onclose = () => {
456
622
  logger?.debug(`MCP Server ${name} transport closed`);
623
+ // Only the current generation of this server's transport may mutate
624
+ // state: a stale process 'close' arriving after an auto-reconnect must
625
+ // not tear down (or schedule a reconnect for) the live connection.
626
+ const current = this.connections.get(name);
627
+ if (!current || current.transport !== transport)
628
+ return;
457
629
  this.connections.delete(name);
458
630
  this.updateServerStatus(name, {
459
631
  status: "disconnected",
460
632
  tools: [],
461
633
  toolCount: 0,
634
+ instructions: undefined,
462
635
  });
463
- // Auto-reconnect with exponential backoff (not triggered by explicit disconnect)
464
- if (!this.reconnectTimers.has(name)) {
636
+ // Auto-reconnect with exponential backoff. Skipped while an explicit
637
+ // disconnectServer teardown is in flight — that close is expected, and
638
+ // reconnecting would silently undo a user-initiated disconnect.
639
+ if (!this.disconnecting.has(name) && !this.reconnectTimers.has(name)) {
465
640
  this.scheduleReconnect(name);
466
641
  }
467
642
  };
@@ -477,6 +652,7 @@ export class McpManager {
477
652
  tools,
478
653
  toolCount: tools.length,
479
654
  capabilities: ["tools"],
655
+ instructions,
480
656
  lastConnected: Date.now(),
481
657
  error: undefined,
482
658
  });
@@ -540,6 +716,7 @@ export class McpManager {
540
716
  name: tool.name,
541
717
  description: tool.description,
542
718
  inputSchema: tool.inputSchema,
719
+ outputSchema: tool.outputSchema,
543
720
  })) || [];
544
721
  logger?.info(`MCP Server ${name} auto-reconnected successfully (attempt ${i + 1})`);
545
722
  this.updateServerStatus(name, {
@@ -570,27 +747,67 @@ export class McpManager {
570
747
  async disconnectServer(name) {
571
748
  // Cancel any pending reconnect attempts
572
749
  this.cancelReconnect(name);
750
+ const server = this.servers.get(name);
573
751
  const connection = this.connections.get(name);
574
- if (!connection)
752
+ // Nothing live to tear down. Still reconcile a stale "connected"-style
753
+ // status (e.g. after an unobserved crash left no connection entry) so the
754
+ // UI converges instead of staying wedged on a disconnect action.
755
+ if (!connection) {
756
+ if (server && server.status !== "disconnected") {
757
+ this.updateServerStatus(name, {
758
+ status: "disconnected",
759
+ tools: [],
760
+ toolCount: 0,
761
+ instructions: undefined,
762
+ error: undefined,
763
+ });
764
+ }
765
+ return false;
766
+ }
767
+ // Idempotent: a teardown is already running for this server.
768
+ if (this.disconnecting.has(name))
575
769
  return false;
770
+ // Mark before closing so transport.onclose knows this is an expected,
771
+ // user-initiated teardown and must not schedule an auto-reconnect.
772
+ this.disconnecting.add(name);
576
773
  try {
577
774
  // Close client connection and transport
578
775
  await connection.client.close();
579
776
  await connection.transport.close();
580
- // Remove connection
581
- this.connections.delete(name);
777
+ // Remove connection (transport.onclose may already have done so)
778
+ if (this.connections.get(name) === connection) {
779
+ this.connections.delete(name);
780
+ }
582
781
  // updateServerStatus will trigger the callback
583
782
  this.updateServerStatus(name, {
584
783
  status: "disconnected",
585
784
  tools: [],
586
785
  toolCount: 0,
786
+ instructions: undefined,
787
+ error: undefined,
587
788
  });
588
789
  return true;
589
790
  }
590
791
  catch (error) {
591
792
  logger?.error(`Error disconnecting from MCP server ${name}:`, error);
793
+ // Never leave the server reporting connected after a failed teardown:
794
+ // drop the connection and push a terminal state so the caller/UI gets a
795
+ // definitive outcome instead of an endless "disconnecting" spinner.
796
+ if (this.connections.get(name) === connection) {
797
+ this.connections.delete(name);
798
+ }
799
+ this.updateServerStatus(name, {
800
+ status: "disconnected",
801
+ tools: [],
802
+ toolCount: 0,
803
+ instructions: undefined,
804
+ error: error instanceof Error ? error.message : String(error),
805
+ });
592
806
  return false;
593
807
  }
808
+ finally {
809
+ this.disconnecting.delete(name);
810
+ }
594
811
  }
595
812
  // Get all tools from connected servers.
596
813
  // "reconnecting" servers retain their last-known tool snapshot so that
@@ -675,9 +892,17 @@ export class McpManager {
675
892
  : images.length > 0
676
893
  ? `Tool returned ${images.length} image(s).`
677
894
  : "No content";
895
+ // The sandbox's value, decided from the text parts rather than from
896
+ // `textContentStr`: that one carries display placeholders ("No content"),
897
+ // and a script has to be able to tell "the tool said nothing" (`null`) from
898
+ // "the tool said 'No content'". Same rule the catalog renders return types
899
+ // from: structured, else text, else null.
900
+ const text = textContent.join("\n");
901
+ const output = result.structuredContent ?? (text === "" ? null : text);
678
902
  return {
679
903
  success: true,
680
904
  content: textContentStr,
905
+ output,
681
906
  images: images.length > 0 ? images : undefined,
682
907
  serverName,
683
908
  };
@@ -721,6 +946,31 @@ export class McpManager {
721
946
  getMcpToolsConfig() {
722
947
  return this.getMcpToolPlugins().map((tool) => tool.config);
723
948
  }
949
+ /**
950
+ * Declared output schema per flattened tool name, for the Exec catalog's return
951
+ * types.
952
+ *
953
+ * A separate accessor because `getMcpToolsConfig()` cannot carry it: an OpenAI
954
+ * function declaration has room for `parameters` and nothing else, so the schema
955
+ * a server declared for its *output* has no field to travel in. Correlated with
956
+ * the same `findToolServer` lookup `getMcpToolPlugins` uses and keyed with the
957
+ * same `mcpToolFlatName`, so the two agree on which server owns a tool.
958
+ *
959
+ * Servers that declare no output schema are simply absent, which renders as
960
+ * `Promise<unknown>`: honest about the call returning *something* of unspecified
961
+ * shape.
962
+ */
963
+ getMcpToolOutputSchemas() {
964
+ const schemas = new Map();
965
+ const servers = this.getAllServers();
966
+ for (const tool of this.getAllConnectedTools()) {
967
+ const server = findToolServer(tool.name, servers);
968
+ if (server && tool.outputSchema) {
969
+ schemas.set(mcpToolFlatName(server.name, tool.name), tool.outputSchema);
970
+ }
971
+ }
972
+ return schemas;
973
+ }
724
974
  /**
725
975
  * Execute an MCP tool by name (registry version)
726
976
  */
@@ -1,8 +1,9 @@
1
1
  import { UserMessageParams, type AgentToolBlockUpdateParams, type ToolBlockUpdateCallbackParams, type AddNotificationMessageParams } from "../utils/messageOperations.js";
2
- import type { Message, Usage } from "../types/index.js";
2
+ import type { Message, Usage, InvokedSkillRecord } from "../types/index.js";
3
3
  import { SessionData } from "../services/session.js";
4
4
  import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
5
5
  import type { MemoryRule } from "../types/memoryRule.js";
6
+ import { type NestedMemoryFile } from "../utils/nestedMemory.js";
6
7
  import { Container } from "../utils/container.js";
7
8
  export interface MessageManagerCallbacks {
8
9
  onSessionIdChange?: (sessionId: string) => void;
@@ -24,9 +25,6 @@ export interface MessageManagerCallbacks {
24
25
  onErrorBlockAdded?: (error: string) => void;
25
26
  onCompactBlockAdded?: (content: string) => void;
26
27
  onCompactionStateChange?: (isCompacting: boolean) => void;
27
- onAddBangMessage?: (command: string, messageId: string) => void;
28
- onUpdateBangMessage?: (command: string, output: string, messageId: string) => void;
29
- onCompleteBangMessage?: (command: string, exitCode: number, messageId: string, output?: string) => void;
30
28
  onInfoBlockAdded?: (content: string) => void;
31
29
  onShowRewind?: () => void;
32
30
  onFileHistoryBlockAdded?: (snapshots: import("../types/reversion.js").FileSnapshot[]) => void;
@@ -47,6 +45,13 @@ export declare class MessageManager {
47
45
  private container;
48
46
  private sessionId;
49
47
  private messages;
48
+ /** Full display message stream for the UI. Mirrors `messages` (context) on
49
+ * every normal change; the two diverge only around compaction, where
50
+ * `messages` is replaced by [compact, ...last API rounds] for the API
51
+ * context while `displayMessages` keeps the full history and appends the
52
+ * compact block (aligned with Claude Code desktop/VS Code: compaction
53
+ * folds the API context but leaves the UI transcript intact). */
54
+ private displayMessages;
50
55
  private latestTotalTokens;
51
56
  private workdir;
52
57
  private encodedWorkdir;
@@ -55,19 +60,38 @@ export declare class MessageManager {
55
60
  private savedMessageCount;
56
61
  private pendingFileReadTriggers;
57
62
  private loadedRuleIds;
63
+ private pendingNestedMemoryTriggers;
64
+ private loadedNestedMemoryPaths;
58
65
  private recentFileReads;
59
66
  private invokedSkills;
60
67
  private sessionType;
61
68
  private subagentType?;
62
69
  private _usages;
70
+ /**
71
+ * This session's user-set title, as last known to this process (set on
72
+ * rename, or recovered from the transcript on resume). It is the fallback
73
+ * `reAppendSessionMetadata` writes back at EOF once the `custom-title` entry
74
+ * has been pushed out of the listing's tail window — at which point the file
75
+ * itself can no longer answer "what was this session called".
76
+ */
77
+ private customTitle?;
63
78
  constructor(container: Container, options: MessageManagerOptions);
64
79
  private get memoryRuleManager();
65
80
  private get memoryService();
66
81
  getSessionId(): string;
67
82
  getRootSessionId(): string;
68
83
  getMessages(): Message[];
84
+ /** Full UI display message stream (includes pre-compaction history). */
85
+ getDisplayMessages(): Message[];
69
86
  getUsages(): Usage[];
70
87
  getLatestTotalTokens(): number;
88
+ /**
89
+ * Re-point this manager at another working directory (in-session session
90
+ * switch). The encoded workdir and the transcript path are both derived from
91
+ * it, so they must move together with it: otherwise the next append would
92
+ * create a second copy of the same session file under the old project dir.
93
+ */
94
+ setWorkdir(workdir: string): void;
71
95
  getWorkdir(): string;
72
96
  /**
73
97
  * Process pending file read triggers: match against conditional rules,
@@ -105,17 +129,42 @@ export declare class MessageManager {
105
129
  * conditional memory rules before the next AI call.
106
130
  */
107
131
  triggerFileRead(filePath: string): void;
132
+ /**
133
+ * Record an absolute path whose read may pull in a nested memory file
134
+ * (`AGENTS.md` in one of its ancestor directories). Separate from
135
+ * `triggerFileRead`: rule matching needs workdir-relative paths, the nested
136
+ * walk needs absolute ones.
137
+ */
138
+ triggerNestedMemory(filePath: string): void;
139
+ /**
140
+ * Resolve nested memory files for the paths read since the last call,
141
+ * outermost first. Clears the pending set; dedup is session-scoped via
142
+ * `loadedNestedMemoryPaths`.
143
+ */
144
+ collectNestedMemoryFiles(): Promise<NestedMemoryFile[]>;
108
145
  /**
109
146
  * Check if a file has been read (for read-before-edit enforcement).
110
147
  * Uses recentFileReads Map populated from Read tool execution.
111
148
  */
112
149
  hasFileBeenRead(filePath: string): boolean;
113
- setMessages(messages: Message[]): void;
150
+ setMessages(messages: Message[], opts?: {
151
+ keepDisplay?: boolean;
152
+ }): void;
114
153
  /**
115
154
  * Save current session
116
155
  */
117
156
  saveSession(): Promise<void>;
118
157
  setlatestTotalTokens(latestTotalTokens: number): void;
158
+ getCustomTitle(): string | undefined;
159
+ setCustomTitle(customTitle: string | undefined): void;
160
+ /**
161
+ * Re-append the session's metadata entries at EOF, so they stay inside the
162
+ * tail window the session list reads. Called at the sparse points where the
163
+ * transcript is about to be read back by another process (exit, compaction,
164
+ * resume, rewind); doing it per save would cost a tail read plus a line on
165
+ * every message.
166
+ */
167
+ reAppendSessionMetadata(): Promise<void>;
119
168
  /**
120
169
  * Clear messages
121
170
  */
@@ -145,15 +194,6 @@ export declare class MessageManager {
145
194
  */
146
195
  compactMessagesAndUpdateSession(compactedContent: string, usage?: Usage): Promise<void>;
147
196
  addFileHistoryBlock(snapshots: import("../types/reversion.js").FileSnapshot[]): void;
148
- addBangMessage(command: string): void;
149
- updateBangMessage(command: string, output: string): void;
150
- completeBangMessage(command: string, exitCode: number, output?: string): void;
151
- /**
152
- * Find the message ID of the most recent message containing a bang block
153
- * for the given command. Bang callbacks do not carry a block ID, so the
154
- * message is located by matching the command against bang blocks.
155
- */
156
- private findBangMessageId;
157
197
  addNotificationMessage(params: Omit<AddNotificationMessageParams, "messages">): void;
158
198
  /**
159
199
  * Rebuild usage array from messages containing usage metadata
@@ -215,7 +255,9 @@ export declare class MessageManager {
215
255
  */
216
256
  truncateHistory(index: number, reversionManager?: import("./reversionManager.js").ReversionManager): Promise<void>;
217
257
  /**
218
- * Rewrite the session file with the current messages.
258
+ * Drop everything past the first `keepMessageCount` messages from the
259
+ * session file. The rewrite is a filter over the file's own lines, so the
260
+ * metadata header, the user-set title and any other reserved entry survive.
219
261
  */
220
262
  private rewriteSessionFile;
221
263
  /**
@@ -243,11 +285,11 @@ export declare class MessageManager {
243
285
  */
244
286
  private extractSkillInvocationsFromMessage;
245
287
  /**
246
- * Get recently invoked skill names, sorted by timestamp (newest first).
288
+ * Get recently invoked skills, sorted by timestamp (newest first).
247
289
  * @param maxSkills - Maximum number of skills to return
248
- * @returns Array of skill names sorted by recency
290
+ * @returns Invoked skill records sorted by recency
249
291
  */
250
- getInvokedSkillNames(maxSkills?: number): string[];
292
+ getInvokedSkills(maxSkills?: number): InvokedSkillRecord[];
251
293
  /**
252
294
  * Clear all invoked skills (e.g., after compaction).
253
295
  */