wave-agent-sdk 1.2.0 → 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 +58 -4
- package/dist/agent.js +91 -19
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/skills/settings.js +1 -12
- 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 +53 -13
- package/dist/core/plugin.js +134 -26
- 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 +5 -0
- package/dist/index.js +6 -0
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -1
- package/dist/managers/aiManager.js +190 -21
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/hookManager.d.ts +13 -0
- package/dist/managers/hookManager.js +31 -4
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +103 -8
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +45 -10
- package/dist/managers/mcpManager.js +103 -1
- package/dist/managers/messageManager.d.ts +48 -5
- package/dist/managers/messageManager.js +107 -21
- package/dist/managers/permissionManager.d.ts +40 -0
- package/dist/managers/permissionManager.js +63 -8
- 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 +19 -0
- package/dist/managers/skillManager.js +44 -0
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +35 -3
- package/dist/managers/subagentManager.d.ts +8 -0
- package/dist/managers/subagentManager.js +20 -0
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- 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/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 +318 -119
- 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 +60 -9
- package/dist/services/configurationService.js +129 -54
- 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 +19 -15
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +84 -0
- package/dist/services/jsonlHandler.js +209 -14
- package/dist/services/memory.d.ts +3 -1
- package/dist/services/memory.js +13 -9
- package/dist/services/officialMarketplaceMirror.js +3 -2
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +16 -2
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +144 -3
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/stdio/index.d.ts +3 -1
- package/dist/stdio/index.js +3 -1
- package/dist/stdio/notificationRouter.js +1 -0
- package/dist/stdio/stdioAgent.d.ts +14 -7
- package/dist/stdio/stdioAgent.js +19 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +8 -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/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 +20 -5
- package/dist/types/configuration.d.ts +39 -1
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +39 -0
- 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 +15 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +43 -0
- 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 +33 -0
- package/dist/utils/fileUtils.js +81 -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 -2
- package/dist/utils/messageOperations.js +40 -29
- 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 +39 -43
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/package.json +10 -7
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* - Coordination between file watchers and configuration updates
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync } from "fs";
|
|
10
|
+
import { dirname } from "path";
|
|
10
11
|
import { FileWatcherService, } from "../services/fileWatcher.js";
|
|
12
|
+
import { USER_MEMORY_FILE } from "../utils/constants.js";
|
|
11
13
|
import { isValidHookEvent } from "../types/hooks.js";
|
|
12
14
|
import { logger } from "../utils/globalLogger.js";
|
|
13
15
|
export class LiveConfigManager {
|
|
@@ -18,6 +20,13 @@ export class LiveConfigManager {
|
|
|
18
20
|
// Configuration state
|
|
19
21
|
this.currentConfiguration = null;
|
|
20
22
|
this.lastValidConfiguration = null;
|
|
23
|
+
// Turn-scoped snapshot of the settings.json-derived configuration the running
|
|
24
|
+
// turn reads from. Captured at turn start, dropped at turn end, so a live
|
|
25
|
+
// reload landing mid-turn takes effect at the next turn instead of shifting
|
|
26
|
+
// values under the running one (core/agent-config.md scenario 5). Nested turns
|
|
27
|
+
// (a subagent turn inside its parent's) share the outer snapshot.
|
|
28
|
+
this.turnSnapshot = null;
|
|
29
|
+
this.turnDepth = 0;
|
|
21
30
|
this.isWatching = false;
|
|
22
31
|
this.reloadInProgress = false;
|
|
23
32
|
this.workdir = options.workdir;
|
|
@@ -33,6 +42,36 @@ export class LiveConfigManager {
|
|
|
33
42
|
get configurationService() {
|
|
34
43
|
return this.container.get("ConfigurationService");
|
|
35
44
|
}
|
|
45
|
+
get memoryService() {
|
|
46
|
+
return this.container.get("MemoryService");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Keep the auto-memory system safe zone in sync with the live auto-memory
|
|
50
|
+
* toggle (core/agent-config.md scenario 6). Turning auto-memory off must
|
|
51
|
+
* revoke the privilege its directory got at construction time, and turning it
|
|
52
|
+
* back on must restore it — both without rebuilding the session. Idempotent:
|
|
53
|
+
* the underlying add/remove are dedup'ed by path.
|
|
54
|
+
*/
|
|
55
|
+
syncAutoMemorySafeZone() {
|
|
56
|
+
const permissionManager = this.permissionManager;
|
|
57
|
+
const memoryService = this.memoryService;
|
|
58
|
+
if (!permissionManager || !memoryService) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const directories = [
|
|
62
|
+
memoryService.getAutoMemoryDirectory(this.workdir),
|
|
63
|
+
USER_MEMORY_FILE,
|
|
64
|
+
];
|
|
65
|
+
const enabled = this.configurationService.resolveAutoMemoryEnabledNow();
|
|
66
|
+
for (const directory of directories) {
|
|
67
|
+
if (enabled) {
|
|
68
|
+
permissionManager.addSystemAdditionalDirectory(directory);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
permissionManager.removeSystemAdditionalDirectory(directory);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
36
75
|
/**
|
|
37
76
|
* Initialize configuration watching
|
|
38
77
|
* Maps to FR-004: System MUST watch settings.json files
|
|
@@ -44,18 +83,18 @@ export class LiveConfigManager {
|
|
|
44
83
|
this.projectConfigPaths = projectPaths;
|
|
45
84
|
// Load initial configuration
|
|
46
85
|
await this.reloadConfiguration();
|
|
47
|
-
//
|
|
86
|
+
// Watch every configuration path, including ones that don't exist yet:
|
|
87
|
+
// chokidar delivers `add` once a missing path appears, but only if its
|
|
88
|
+
// parent chain exists when watching starts. Skipping absent paths (the
|
|
89
|
+
// pre-2026-09-10 behavior) silently dropped the very first save of a
|
|
90
|
+
// user-level settings.json created after session start
|
|
91
|
+
// (core/agent-config.md scenario 7).
|
|
48
92
|
for (const userPath of userPaths) {
|
|
49
|
-
|
|
50
|
-
await this.fileWatcher.watchFile(userPath, (event) => this.handleFileChange(event, "user"));
|
|
51
|
-
}
|
|
93
|
+
await this.fileWatcher.watchFile(this.resolveWatchTarget(userPath), (event) => this.handleFileChange(event, "user"));
|
|
52
94
|
}
|
|
53
|
-
// Start watching local configs that exist
|
|
54
95
|
if (projectPaths) {
|
|
55
96
|
for (const projectPath of projectPaths) {
|
|
56
|
-
|
|
57
|
-
await this.fileWatcher.watchFile(projectPath, (event) => this.handleFileChange(event, "project"));
|
|
58
|
-
}
|
|
97
|
+
await this.fileWatcher.watchFile(this.resolveWatchTarget(projectPath), (event) => this.handleFileChange(event, "project"));
|
|
59
98
|
}
|
|
60
99
|
}
|
|
61
100
|
this.isWatching = true;
|
|
@@ -112,6 +151,41 @@ export class LiveConfigManager {
|
|
|
112
151
|
throw error;
|
|
113
152
|
}
|
|
114
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Turn boundary: pin the settings.json-derived configuration for the duration
|
|
156
|
+
* of a turn (called by AIManager). A live reload may land mid-turn and the
|
|
157
|
+
* turn must not see it (core/agent-config.md scenario 5) — the change is
|
|
158
|
+
* picked up by the snapshot taken when the next turn starts. Permission rules,
|
|
159
|
+
* hooks and env stay live: those are enforcement state and follow the file
|
|
160
|
+
* immediately.
|
|
161
|
+
*/
|
|
162
|
+
onTurnStart() {
|
|
163
|
+
if (this.turnDepth === 0) {
|
|
164
|
+
const configuration = this.getCurrentConfiguration();
|
|
165
|
+
this.turnSnapshot = {
|
|
166
|
+
configuration: configuration ? structuredClone(configuration) : null,
|
|
167
|
+
// `env` on the merged configuration is exactly what was published as the
|
|
168
|
+
// session env snapshot when it was loaded (both come from the same
|
|
169
|
+
// merge), so the snapshot carries both without querying the service.
|
|
170
|
+
env: { ...(configuration?.env ?? {}) },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
this.turnDepth++;
|
|
174
|
+
}
|
|
175
|
+
/** Turn boundary: drop the snapshot so the next turn reads live values. */
|
|
176
|
+
onTurnEnd() {
|
|
177
|
+
if (this.turnDepth === 0) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.turnDepth--;
|
|
181
|
+
if (this.turnDepth === 0) {
|
|
182
|
+
this.turnSnapshot = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Configuration snapshot of the running turn, or null outside a turn. */
|
|
186
|
+
getTurnSnapshot() {
|
|
187
|
+
return this.turnDepth > 0 ? this.turnSnapshot : null;
|
|
188
|
+
}
|
|
115
189
|
/**
|
|
116
190
|
* Reload configuration from files
|
|
117
191
|
* Maps to FR-008: Continue with previous valid configuration on errors
|
|
@@ -176,6 +250,7 @@ export class LiveConfigManager {
|
|
|
176
250
|
this.permissionManager.updateAllowedRules(this.currentConfiguration.permissions?.allow || []);
|
|
177
251
|
this.permissionManager.updateDeniedRules(this.currentConfiguration.permissions?.deny || []);
|
|
178
252
|
this.permissionManager.updateAdditionalDirectories(this.currentConfiguration.permissions?.additionalDirectories || []);
|
|
253
|
+
this.syncAutoMemorySafeZone();
|
|
179
254
|
}
|
|
180
255
|
// Trigger reload callback. Awaited so fire-and-forget work spawned by the
|
|
181
256
|
// callback (e.g. skill rediscovery) completes before the reload resolves —
|
|
@@ -304,6 +379,26 @@ export class LiveConfigManager {
|
|
|
304
379
|
}
|
|
305
380
|
return { added, modified, removed };
|
|
306
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Chokidar only starts watching a non-existent path when its parent directory
|
|
384
|
+
* exists at watch time (a path whose parents are all missing is never
|
|
385
|
+
* picked up, not even after they appear). Walk up to the deepest node whose
|
|
386
|
+
* parent exists and watch that instead — events for its descendants still
|
|
387
|
+
* reach this path's callback (FileWatcherService matches by path prefix).
|
|
388
|
+
*/
|
|
389
|
+
resolveWatchTarget(configPath) {
|
|
390
|
+
let candidate = configPath;
|
|
391
|
+
while (!existsSync(dirname(candidate))) {
|
|
392
|
+
const parent = dirname(candidate);
|
|
393
|
+
if (parent === candidate) {
|
|
394
|
+
// Reached the filesystem root without an existing parent: nothing to
|
|
395
|
+
// watch (unreachable for absolute paths).
|
|
396
|
+
return configPath;
|
|
397
|
+
}
|
|
398
|
+
candidate = parent;
|
|
399
|
+
}
|
|
400
|
+
return candidate;
|
|
401
|
+
}
|
|
307
402
|
/**
|
|
308
403
|
* Get configuration file paths for user and project settings
|
|
309
404
|
* Returns paths in priority order (local.json first, then .json)
|
|
@@ -21,6 +21,14 @@ export declare class LspManager implements ILspManager {
|
|
|
21
21
|
constructor(container: Container);
|
|
22
22
|
initialize(workdir: string): Promise<void>;
|
|
23
23
|
registerServer(language: string, config: LspServerConfig): void;
|
|
24
|
+
/**
|
|
25
|
+
* Drop every LSP server contributed by a plugin (matched on pluginRoot),
|
|
26
|
+
* stopping any process already started for those languages so the next
|
|
27
|
+
* request starts the reloaded command instead of reusing the stale one.
|
|
28
|
+
* Called by the in-place plugin reload path before re-registering.
|
|
29
|
+
* @returns the number of servers removed
|
|
30
|
+
*/
|
|
31
|
+
unregisterServersForPlugin(pluginRoot: string): Promise<number>;
|
|
24
32
|
private loadConfig;
|
|
25
33
|
getProcessForFile(filePath: string): Promise<LspProcess | null>;
|
|
26
34
|
private startServer;
|
|
@@ -36,6 +44,7 @@ export declare class LspManager implements ILspManager {
|
|
|
36
44
|
success: boolean;
|
|
37
45
|
content: string;
|
|
38
46
|
}>;
|
|
47
|
+
private stopProcess;
|
|
39
48
|
cleanup(): Promise<void>;
|
|
40
49
|
}
|
|
41
50
|
export {};
|
|
@@ -17,6 +17,32 @@ export class LspManager {
|
|
|
17
17
|
this.config[language] = config;
|
|
18
18
|
logger?.debug(`Registered LSP server for ${language}`);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Drop every LSP server contributed by a plugin (matched on pluginRoot),
|
|
22
|
+
* stopping any process already started for those languages so the next
|
|
23
|
+
* request starts the reloaded command instead of reusing the stale one.
|
|
24
|
+
* Called by the in-place plugin reload path before re-registering.
|
|
25
|
+
* @returns the number of servers removed
|
|
26
|
+
*/
|
|
27
|
+
async unregisterServersForPlugin(pluginRoot) {
|
|
28
|
+
let removed = 0;
|
|
29
|
+
for (const [language, config] of Object.entries(this.config)) {
|
|
30
|
+
if (config.pluginRoot !== pluginRoot) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
delete this.config[language];
|
|
34
|
+
const lspProc = this.processes.get(language);
|
|
35
|
+
if (lspProc) {
|
|
36
|
+
this.processes.delete(language);
|
|
37
|
+
await this.stopProcess(language, lspProc);
|
|
38
|
+
}
|
|
39
|
+
removed += 1;
|
|
40
|
+
}
|
|
41
|
+
if (removed > 0) {
|
|
42
|
+
logger?.debug(`Unregistered ${removed} LSP servers for plugin root ${pluginRoot}`);
|
|
43
|
+
}
|
|
44
|
+
return removed;
|
|
45
|
+
}
|
|
20
46
|
async loadConfig() {
|
|
21
47
|
const lspJsonPath = join(this.workdir, ".lsp.json");
|
|
22
48
|
try {
|
|
@@ -326,27 +352,30 @@ export class LspManager {
|
|
|
326
352
|
return { success: false, content: `LSP error: ${JSON.stringify(error)}` };
|
|
327
353
|
}
|
|
328
354
|
}
|
|
329
|
-
async
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
catch (error) {
|
|
342
|
-
logger?.debug(`Failed to gracefully shutdown LSP for ${language}: ${error}`);
|
|
355
|
+
async stopProcess(language, lspProc) {
|
|
356
|
+
try {
|
|
357
|
+
// Try graceful shutdown
|
|
358
|
+
const timeout = lspProc.config.shutdownTimeout || 2000;
|
|
359
|
+
await this.sendRequest(lspProc, "shutdown", {}, timeout);
|
|
360
|
+
await this.sendNotification(lspProc, "exit", {});
|
|
361
|
+
// Give it a moment to exit
|
|
362
|
+
if (timeout > 100) {
|
|
363
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
343
364
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
logger?.debug(`Failed to gracefully shutdown LSP for ${language}: ${error}`);
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
if (!lspProc.process.killed) {
|
|
371
|
+
lspProc.process.kill();
|
|
348
372
|
}
|
|
349
373
|
}
|
|
374
|
+
}
|
|
375
|
+
async cleanup() {
|
|
376
|
+
for (const [language, lspProc] of this.processes.entries()) {
|
|
377
|
+
await this.stopProcess(language, lspProc);
|
|
378
|
+
}
|
|
350
379
|
this.processes.clear();
|
|
351
380
|
}
|
|
352
381
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ChatCompletionFunctionTool } from "openai/resources.js";
|
|
2
2
|
import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
|
|
3
3
|
import { Container } from "../utils/container.js";
|
|
4
|
-
import type { Logger, McpServerConfig, McpConfig, McpTool, McpServerStatus } from "../types/index.js";
|
|
4
|
+
import type { Logger, McpServerConfig, McpConfig, McpTool, McpToolCallResult, McpServerStatus } from "../types/index.js";
|
|
5
5
|
export interface McpManagerCallbacks {
|
|
6
6
|
onMcpServersChange?: (servers: McpServerStatus[]) => void;
|
|
7
7
|
}
|
|
@@ -69,9 +69,37 @@ export declare class McpManager {
|
|
|
69
69
|
getConfig(): McpConfig | null;
|
|
70
70
|
getAllServers(): McpServerStatus[];
|
|
71
71
|
getServer(name: string): McpServerStatus | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* The usage notes each usable server described about itself (`initialize`'s
|
|
74
|
+
* `instructions`), for the announcement appended to the conversation.
|
|
75
|
+
*
|
|
76
|
+
* "Usable" is the same predicate `getAllConnectedTools` uses — connected, or
|
|
77
|
+
* reconnecting with its last-known snapshot retained — so a server's prose is
|
|
78
|
+
* visible exactly while its tools are. A server that dropped, failed to connect
|
|
79
|
+
* or went to `error` keeps its snapshot but stops being reported here, which is
|
|
80
|
+
* what keeps a dead server from still talking to the model.
|
|
81
|
+
*
|
|
82
|
+
* `isDenied` receives flattened tool names (`mcp__server__tool`, the shape
|
|
83
|
+
* permission rules match on); a server whose tools are *all* excluded by rules is
|
|
84
|
+
* dropped whole, so prose cannot reach the context after the user ruled the
|
|
85
|
+
* server out. A server exposing no tools at all is still reported: nothing was
|
|
86
|
+
* excluded, it is simply a server that brings context rather than tools.
|
|
87
|
+
*/
|
|
88
|
+
getServerInstructions(isDenied?: (toolName: string) => boolean): Array<{
|
|
89
|
+
name: string;
|
|
90
|
+
instructions: string;
|
|
91
|
+
}>;
|
|
72
92
|
updateServerStatus(name: string, updates: Partial<McpServerStatus>): void;
|
|
73
93
|
addServer(name: string, config: McpServerConfig): boolean;
|
|
74
94
|
removeServer(name: string): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Drop every MCP server contributed by a plugin (matched on pluginRoot),
|
|
97
|
+
* disconnecting each one. Needed instead of re-adding under the same name
|
|
98
|
+
* because addServer refuses a name that is already registered.
|
|
99
|
+
* Called by the in-place plugin reload path before re-registering.
|
|
100
|
+
* @returns the number of servers removed
|
|
101
|
+
*/
|
|
102
|
+
removeServersForPlugin(pluginRoot: string): number;
|
|
75
103
|
/**
|
|
76
104
|
* Remove a server from a persisted config file (user or project scope),
|
|
77
105
|
* then disconnect and drop it from the in-memory registry.
|
|
@@ -94,15 +122,7 @@ export declare class McpManager {
|
|
|
94
122
|
private cancelReconnect;
|
|
95
123
|
disconnectServer(name: string): Promise<boolean>;
|
|
96
124
|
getAllConnectedTools(): McpTool[];
|
|
97
|
-
executeMcpTool(toolName: string, args: Record<string, unknown>, context?: ToolContext): Promise<
|
|
98
|
-
success: boolean;
|
|
99
|
-
content: string;
|
|
100
|
-
serverName?: string;
|
|
101
|
-
images?: Array<{
|
|
102
|
-
data: string;
|
|
103
|
-
mediaType?: string;
|
|
104
|
-
}>;
|
|
105
|
-
}>;
|
|
125
|
+
executeMcpTool(toolName: string, args: Record<string, unknown>, context?: ToolContext): Promise<McpToolCallResult>;
|
|
106
126
|
private executeToolOnConnection;
|
|
107
127
|
cleanup(): Promise<void>;
|
|
108
128
|
/**
|
|
@@ -113,6 +133,21 @@ export declare class McpManager {
|
|
|
113
133
|
* Get all currently available MCP tools as OpenAI function tools
|
|
114
134
|
*/
|
|
115
135
|
getMcpToolsConfig(): ChatCompletionFunctionTool[];
|
|
136
|
+
/**
|
|
137
|
+
* Declared output schema per flattened tool name, for the Exec catalog's return
|
|
138
|
+
* types.
|
|
139
|
+
*
|
|
140
|
+
* A separate accessor because `getMcpToolsConfig()` cannot carry it: an OpenAI
|
|
141
|
+
* function declaration has room for `parameters` and nothing else, so the schema
|
|
142
|
+
* a server declared for its *output* has no field to travel in. Correlated with
|
|
143
|
+
* the same `findToolServer` lookup `getMcpToolPlugins` uses and keyed with the
|
|
144
|
+
* same `mcpToolFlatName`, so the two agree on which server owns a tool.
|
|
145
|
+
*
|
|
146
|
+
* Servers that declare no output schema are simply absent, which renders as
|
|
147
|
+
* `Promise<unknown>`: honest about the call returning *something* of unspecified
|
|
148
|
+
* shape.
|
|
149
|
+
*/
|
|
150
|
+
getMcpToolOutputSchemas(): Map<string, Record<string, unknown>>;
|
|
116
151
|
/**
|
|
117
152
|
* Execute an MCP tool by name (registry version)
|
|
118
153
|
*/
|
|
@@ -5,7 +5,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
5
5
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
6
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
7
7
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8
|
-
import { createMcpToolPlugin, findToolServer } from "../utils/mcpUtils.js";
|
|
8
|
+
import { createMcpToolPlugin, findToolServer, mcpToolFlatName, } from "../utils/mcpUtils.js";
|
|
9
|
+
import { truncateMcpInstructions } from "../utils/mcpInstructions.js";
|
|
9
10
|
import { logger } from "../utils/globalLogger.js";
|
|
10
11
|
/**
|
|
11
12
|
* Expand environment variables in a string value.
|
|
@@ -283,6 +284,38 @@ export class McpManager {
|
|
|
283
284
|
getServer(name) {
|
|
284
285
|
return this.servers.get(name);
|
|
285
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
|
+
}
|
|
286
319
|
updateServerStatus(name, updates) {
|
|
287
320
|
const server = this.servers.get(name);
|
|
288
321
|
if (server) {
|
|
@@ -353,6 +386,27 @@ export class McpManager {
|
|
|
353
386
|
}
|
|
354
387
|
return removed;
|
|
355
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
|
+
}
|
|
356
410
|
/**
|
|
357
411
|
* Remove a server from a persisted config file (user or project scope),
|
|
358
412
|
* then disconnect and drop it from the in-memory registry.
|
|
@@ -400,6 +454,9 @@ export class McpManager {
|
|
|
400
454
|
let transport;
|
|
401
455
|
let client;
|
|
402
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;
|
|
403
460
|
const createClient = () => new Client({
|
|
404
461
|
name: "wave-code",
|
|
405
462
|
version: "1.0.0",
|
|
@@ -419,12 +476,14 @@ export class McpManager {
|
|
|
419
476
|
});
|
|
420
477
|
client = createClient();
|
|
421
478
|
await client.connect(transport);
|
|
479
|
+
instructions = truncateMcpInstructions(client.getInstructions());
|
|
422
480
|
const toolsResponse = await client.listTools();
|
|
423
481
|
tools =
|
|
424
482
|
toolsResponse.tools?.map((tool) => ({
|
|
425
483
|
name: tool.name,
|
|
426
484
|
description: tool.description,
|
|
427
485
|
inputSchema: tool.inputSchema,
|
|
486
|
+
outputSchema: tool.outputSchema,
|
|
428
487
|
})) || [];
|
|
429
488
|
logger?.info(`Connected to MCP server ${name} using Streamable HTTP`);
|
|
430
489
|
}
|
|
@@ -440,12 +499,14 @@ export class McpManager {
|
|
|
440
499
|
});
|
|
441
500
|
client = createClient();
|
|
442
501
|
await client.connect(transport);
|
|
502
|
+
instructions = truncateMcpInstructions(client.getInstructions());
|
|
443
503
|
const toolsResponse = await client.listTools();
|
|
444
504
|
tools =
|
|
445
505
|
toolsResponse.tools?.map((tool) => ({
|
|
446
506
|
name: tool.name,
|
|
447
507
|
description: tool.description,
|
|
448
508
|
inputSchema: tool.inputSchema,
|
|
509
|
+
outputSchema: tool.outputSchema,
|
|
449
510
|
})) || [];
|
|
450
511
|
logger?.info(`Connected to MCP server ${name} using SSE`);
|
|
451
512
|
}
|
|
@@ -517,12 +578,14 @@ export class McpManager {
|
|
|
517
578
|
logger?.debug(`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`);
|
|
518
579
|
stderrOutput = "";
|
|
519
580
|
}
|
|
581
|
+
instructions = truncateMcpInstructions(client.getInstructions());
|
|
520
582
|
const toolsResponse = await client.listTools();
|
|
521
583
|
tools =
|
|
522
584
|
toolsResponse.tools?.map((tool) => ({
|
|
523
585
|
name: tool.name,
|
|
524
586
|
description: tool.description,
|
|
525
587
|
inputSchema: tool.inputSchema,
|
|
588
|
+
outputSchema: tool.outputSchema,
|
|
526
589
|
})) || [];
|
|
527
590
|
}
|
|
528
591
|
else if (serverType) {
|
|
@@ -568,6 +631,7 @@ export class McpManager {
|
|
|
568
631
|
status: "disconnected",
|
|
569
632
|
tools: [],
|
|
570
633
|
toolCount: 0,
|
|
634
|
+
instructions: undefined,
|
|
571
635
|
});
|
|
572
636
|
// Auto-reconnect with exponential backoff. Skipped while an explicit
|
|
573
637
|
// disconnectServer teardown is in flight — that close is expected, and
|
|
@@ -588,6 +652,7 @@ export class McpManager {
|
|
|
588
652
|
tools,
|
|
589
653
|
toolCount: tools.length,
|
|
590
654
|
capabilities: ["tools"],
|
|
655
|
+
instructions,
|
|
591
656
|
lastConnected: Date.now(),
|
|
592
657
|
error: undefined,
|
|
593
658
|
});
|
|
@@ -651,6 +716,7 @@ export class McpManager {
|
|
|
651
716
|
name: tool.name,
|
|
652
717
|
description: tool.description,
|
|
653
718
|
inputSchema: tool.inputSchema,
|
|
719
|
+
outputSchema: tool.outputSchema,
|
|
654
720
|
})) || [];
|
|
655
721
|
logger?.info(`MCP Server ${name} auto-reconnected successfully (attempt ${i + 1})`);
|
|
656
722
|
this.updateServerStatus(name, {
|
|
@@ -692,6 +758,7 @@ export class McpManager {
|
|
|
692
758
|
status: "disconnected",
|
|
693
759
|
tools: [],
|
|
694
760
|
toolCount: 0,
|
|
761
|
+
instructions: undefined,
|
|
695
762
|
error: undefined,
|
|
696
763
|
});
|
|
697
764
|
}
|
|
@@ -716,6 +783,7 @@ export class McpManager {
|
|
|
716
783
|
status: "disconnected",
|
|
717
784
|
tools: [],
|
|
718
785
|
toolCount: 0,
|
|
786
|
+
instructions: undefined,
|
|
719
787
|
error: undefined,
|
|
720
788
|
});
|
|
721
789
|
return true;
|
|
@@ -732,6 +800,7 @@ export class McpManager {
|
|
|
732
800
|
status: "disconnected",
|
|
733
801
|
tools: [],
|
|
734
802
|
toolCount: 0,
|
|
803
|
+
instructions: undefined,
|
|
735
804
|
error: error instanceof Error ? error.message : String(error),
|
|
736
805
|
});
|
|
737
806
|
return false;
|
|
@@ -823,9 +892,17 @@ export class McpManager {
|
|
|
823
892
|
: images.length > 0
|
|
824
893
|
? `Tool returned ${images.length} image(s).`
|
|
825
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);
|
|
826
902
|
return {
|
|
827
903
|
success: true,
|
|
828
904
|
content: textContentStr,
|
|
905
|
+
output,
|
|
829
906
|
images: images.length > 0 ? images : undefined,
|
|
830
907
|
serverName,
|
|
831
908
|
};
|
|
@@ -869,6 +946,31 @@ export class McpManager {
|
|
|
869
946
|
getMcpToolsConfig() {
|
|
870
947
|
return this.getMcpToolPlugins().map((tool) => tool.config);
|
|
871
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
|
+
}
|
|
872
974
|
/**
|
|
873
975
|
* Execute an MCP tool by name (registry version)
|
|
874
976
|
*/
|