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.
- package/dist/agent.d.ts +128 -28
- package/dist/agent.js +201 -49
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/plugins.js +11 -20
- package/dist/builtin/skills/settings.js +7 -20
- package/dist/builtin/skills/wave-daemon.d.ts +1 -0
- package/dist/builtin/skills/wave-daemon.js +194 -0
- package/dist/constants/images.d.ts +26 -0
- package/dist/constants/images.js +26 -0
- package/dist/constants/index.d.ts +16 -0
- package/dist/constants/index.js +16 -0
- package/dist/constants/memory.d.ts +26 -0
- package/dist/constants/memory.js +34 -0
- package/dist/constants/messages.d.ts +11 -0
- package/dist/constants/messages.js +11 -0
- package/dist/constants/plugins.d.ts +8 -0
- package/dist/constants/plugins.js +8 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/core/plugin.d.ts +54 -10
- package/dist/core/plugin.js +137 -23
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +1 -1
- package/dist/exec/catalog.d.ts +140 -0
- package/dist/exec/catalog.js +470 -0
- package/dist/exec/catalogAnnouncement.d.ts +89 -0
- package/dist/exec/catalogAnnouncement.js +293 -0
- package/dist/exec/constants.d.ts +51 -0
- package/dist/exec/constants.js +51 -0
- package/dist/exec/execRuntime.d.ts +55 -0
- package/dist/exec/execRuntime.js +217 -0
- package/dist/exec/workerSource.d.ts +28 -0
- package/dist/exec/workerSource.js +299 -0
- package/dist/host/index.d.ts +23 -0
- package/dist/host/index.js +23 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +8 -1
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -25
- package/dist/managers/aiManager.js +204 -202
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +18 -0
- package/dist/managers/hookManager.js +37 -3
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +106 -11
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +68 -10
- package/dist/managers/mcpManager.js +265 -15
- package/dist/managers/messageManager.d.ts +60 -18
- package/dist/managers/messageManager.js +170 -81
- package/dist/managers/permissionManager.d.ts +69 -0
- package/dist/managers/permissionManager.js +221 -78
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- package/dist/managers/pluginManager.d.ts +46 -2
- package/dist/managers/pluginManager.js +117 -11
- package/dist/managers/pluginScopeManager.d.ts +15 -2
- package/dist/managers/pluginScopeManager.js +20 -1
- package/dist/managers/skillManager.d.ts +50 -0
- package/dist/managers/skillManager.js +166 -12
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +44 -31
- package/dist/managers/subagentManager.d.ts +15 -0
- package/dist/managers/subagentManager.js +81 -9
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/managers/workflowManager.js +6 -0
- package/dist/prompts/autoMemory.d.ts +9 -0
- package/dist/prompts/autoMemory.js +30 -31
- package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
- package/dist/prompts/autoMemoryExtraction.js +8 -111
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/prompts/memoryTypes.d.ts +63 -0
- package/dist/prompts/memoryTypes.js +191 -0
- package/dist/services/GitService.d.ts +7 -0
- package/dist/services/GitService.js +23 -0
- package/dist/services/MarketplaceService.d.ts +101 -17
- package/dist/services/MarketplaceService.js +323 -102
- package/dist/services/artifactContent.d.ts +84 -0
- package/dist/services/artifactContent.js +204 -0
- package/dist/services/artifactSession.d.ts +6 -0
- package/dist/services/artifactSession.js +17 -0
- package/dist/services/autoMemoryService.js +5 -13
- package/dist/services/configurationService.d.ts +92 -9
- package/dist/services/configurationService.js +246 -64
- package/dist/services/contentSummarizer.d.ts +15 -0
- package/dist/services/contentSummarizer.js +45 -0
- package/dist/services/execAvailability.d.ts +9 -0
- package/dist/services/execAvailability.js +32 -0
- package/dist/services/fileWatcher.js +61 -6
- package/dist/services/initializationService.js +21 -17
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +98 -0
- package/dist/services/jsonlHandler.js +250 -12
- package/dist/services/memory.d.ts +17 -1
- package/dist/services/memory.js +44 -7
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +290 -0
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +20 -6
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +174 -16
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +12 -0
- package/dist/stdio/index.js +12 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +97 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +229 -0
- package/dist/stdio/stdioAgent.js +360 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +10 -6
- package/dist/tools/editTool.js +6 -3
- package/dist/tools/execTool.d.ts +2 -0
- package/dist/tools/execTool.js +165 -0
- package/dist/tools/exitPlanMode.js +10 -2
- package/dist/tools/grepTool.js +7 -1
- package/dist/tools/readTool.js +30 -2
- package/dist/tools/types.d.ts +34 -8
- package/dist/tools/webFetchTool.js +15 -166
- package/dist/tools/workflowTool.js +40 -8
- package/dist/tools/writeTool.js +6 -3
- package/dist/types/agent.d.ts +24 -1
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +45 -2
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +42 -0
- package/dist/types/messaging.d.ts +1 -8
- package/dist/types/permissions.d.ts +22 -0
- package/dist/types/permissions.js +17 -0
- package/dist/types/plugins.d.ts +26 -2
- package/dist/types/skills.d.ts +26 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +48 -6
- package/dist/utils/convertMessagesForAPI.d.ts +7 -1
- package/dist/utils/convertMessagesForAPI.js +64 -14
- package/dist/utils/fileChangeReminder.d.ts +20 -0
- package/dist/utils/fileChangeReminder.js +153 -0
- package/dist/utils/fileSearch.js +4 -3
- package/dist/utils/fileUtils.d.ts +44 -0
- package/dist/utils/fileUtils.js +118 -0
- package/dist/utils/frontmatterYaml.d.ts +33 -0
- package/dist/utils/frontmatterYaml.js +192 -0
- package/dist/utils/imageBudget.d.ts +85 -0
- package/dist/utils/imageBudget.js +109 -0
- package/dist/utils/imageDimensions.d.ts +83 -0
- package/dist/utils/imageDimensions.js +232 -0
- package/dist/utils/imageProcessor.d.ts +66 -0
- package/dist/utils/imageProcessor.js +84 -0
- package/dist/utils/imageRewrite.d.ts +29 -0
- package/dist/utils/imageRewrite.js +251 -0
- package/dist/utils/markdownParser.d.ts +5 -1
- package/dist/utils/markdownParser.js +9 -51
- package/dist/utils/mcpInstructions.d.ts +61 -0
- package/dist/utils/mcpInstructions.js +126 -0
- package/dist/utils/mcpUtils.d.ts +7 -0
- package/dist/utils/mcpUtils.js +11 -2
- package/dist/utils/memoryAge.d.ts +32 -0
- package/dist/utils/memoryAge.js +47 -0
- package/dist/utils/memoryEntrypoint.d.ts +20 -0
- package/dist/utils/memoryEntrypoint.js +49 -0
- package/dist/utils/memoryIndex.d.ts +30 -0
- package/dist/utils/memoryIndex.js +76 -0
- package/dist/utils/messageOperations.d.ts +6 -20
- package/dist/utils/messageOperations.js +40 -91
- package/dist/utils/nestedMemory.d.ts +22 -0
- package/dist/utils/nestedMemory.js +61 -0
- package/dist/utils/npmTarball.d.ts +19 -0
- package/dist/utils/npmTarball.js +92 -0
- package/dist/utils/pluginSource.d.ts +37 -0
- package/dist/utils/pluginSource.js +73 -0
- package/dist/utils/ripgrep.d.ts +18 -4
- package/dist/utils/ripgrep.js +56 -4
- package/dist/utils/runtimeDeps.d.ts +35 -0
- package/dist/utils/runtimeDeps.js +426 -0
- package/dist/utils/skillParser.js +22 -52
- package/dist/utils/subagentParser.js +48 -45
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +12 -4
- package/dist/managers/bangManager.d.ts +0 -26
- 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
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
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(
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
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
|
|
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
|
|
464
|
-
|
|
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
|
-
|
|
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.
|
|
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[]
|
|
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
|
-
*
|
|
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
|
|
288
|
+
* Get recently invoked skills, sorted by timestamp (newest first).
|
|
247
289
|
* @param maxSkills - Maximum number of skills to return
|
|
248
|
-
* @returns
|
|
290
|
+
* @returns Invoked skill records sorted by recency
|
|
249
291
|
*/
|
|
250
|
-
|
|
292
|
+
getInvokedSkills(maxSkills?: number): InvokedSkillRecord[];
|
|
251
293
|
/**
|
|
252
294
|
* Clear all invoked skills (e.g., after compaction).
|
|
253
295
|
*/
|