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
|
@@ -4,10 +4,27 @@ import * as path from "path";
|
|
|
4
4
|
import { existsSync, readdirSync } from "fs";
|
|
5
5
|
import { getBuiltinPluginsDir } from "../utils/configPaths.js";
|
|
6
6
|
import { MarketplaceService } from "../services/MarketplaceService.js";
|
|
7
|
+
import { DEFAULT_PLUGIN_VERSION } from "../constants/plugins.js";
|
|
7
8
|
export class PluginManager {
|
|
9
|
+
serialize(task) {
|
|
10
|
+
const result = this.loadChain.then(task, task);
|
|
11
|
+
// Keep the chain alive after a failure so later reloads still run.
|
|
12
|
+
this.loadChain = result.then(() => undefined, () => undefined);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
8
15
|
constructor(container, options) {
|
|
9
16
|
this.container = container;
|
|
10
17
|
this.plugins = new Map();
|
|
18
|
+
/** Explicit configs from the last load, replayed by reloadAllPlugins(). */
|
|
19
|
+
this.lastLoadConfigs = [];
|
|
20
|
+
/** Failures collected by the current loadPlugins() run. */
|
|
21
|
+
this.loadFailures = [];
|
|
22
|
+
/**
|
|
23
|
+
* Serializes plugin (re)loads. A managed-settings sync (or an early
|
|
24
|
+
* `/reload-plugins`) can ask for a reload while the startup load is still
|
|
25
|
+
* running; interleaving them would leave half-unloaded capability registries.
|
|
26
|
+
*/
|
|
27
|
+
this.loadChain = Promise.resolve();
|
|
11
28
|
this.workdir = options.workdir;
|
|
12
29
|
this.enabledPlugins = options.enabledPlugins || {};
|
|
13
30
|
}
|
|
@@ -51,12 +68,6 @@ export class PluginManager {
|
|
|
51
68
|
this.enabledPlugins = this.configurationService.getMergedEnabledPlugins(this.workdir);
|
|
52
69
|
}
|
|
53
70
|
const marketplaceService = new MarketplaceService(this.workdir, this.configurationService);
|
|
54
|
-
// Trigger auto-update for marketplaces in the background
|
|
55
|
-
if (!process.env.VITEST) {
|
|
56
|
-
marketplaceService.autoUpdateAll().catch((error) => {
|
|
57
|
-
logger?.error("Background marketplace auto-update failed:", error);
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
71
|
let installedRegistry = await marketplaceService.getInstalledPlugins();
|
|
61
72
|
const knownMarketplaces = await marketplaceService.listMarketplaces();
|
|
62
73
|
// Identify missing enabled plugins and auto-install them if marketplace is known
|
|
@@ -74,7 +85,7 @@ export class PluginManager {
|
|
|
74
85
|
const isMarketplaceKnown = knownMarketplaces.some((m) => m.name === marketplaceName);
|
|
75
86
|
if (isMarketplaceKnown) {
|
|
76
87
|
// Pre-check: verify the plugin still exists in the marketplace manifest
|
|
77
|
-
// before acquiring the lock in installPlugin (which can block ~8s during
|
|
88
|
+
// before acquiring the lock in installPlugin (which can block ~8s during a marketplace refresh)
|
|
78
89
|
const marketplace = knownMarketplaces.find((m) => m.name === marketplaceName);
|
|
79
90
|
if (!marketplace)
|
|
80
91
|
continue;
|
|
@@ -119,12 +130,18 @@ export class PluginManager {
|
|
|
119
130
|
}
|
|
120
131
|
// Refresh registry after potential auto-installs
|
|
121
132
|
installedRegistry = await marketplaceService.getInstalledPlugins();
|
|
133
|
+
// 同一插件在本机可能有多条安装记录(各作用域/各项目各一条,spec plugin
|
|
134
|
+
// A-015);按插件去重,否则同一个 cachePath 会被加载多次。
|
|
135
|
+
const loaded = new Set();
|
|
122
136
|
for (const p of installedRegistry.plugins) {
|
|
123
137
|
const pluginId = `${p.name}@${p.marketplace}`;
|
|
124
138
|
if (this.enabledPlugins[pluginId] !== true) {
|
|
125
139
|
logger?.debug(`Plugin ${pluginId} is not enabled via configuration`);
|
|
126
140
|
continue;
|
|
127
141
|
}
|
|
142
|
+
if (loaded.has(pluginId))
|
|
143
|
+
continue;
|
|
144
|
+
loaded.add(pluginId);
|
|
128
145
|
await this.loadSinglePlugin(p.cachePath);
|
|
129
146
|
}
|
|
130
147
|
}
|
|
@@ -145,12 +162,15 @@ export class PluginManager {
|
|
|
145
162
|
}
|
|
146
163
|
const plugin = {
|
|
147
164
|
...manifest,
|
|
165
|
+
// A manifest without `version` is legal (spec plugin A-022); the default
|
|
166
|
+
// matches what the install path files under `installed_plugins.json`.
|
|
167
|
+
version: manifest.version || DEFAULT_PLUGIN_VERSION,
|
|
148
168
|
path: absolutePath,
|
|
149
169
|
commands: PluginLoader.loadCommands(absolutePath),
|
|
150
170
|
skills: await PluginLoader.loadSkills(absolutePath),
|
|
151
171
|
agents: await PluginLoader.loadAgents(absolutePath),
|
|
152
172
|
lspConfig: await PluginLoader.loadLspConfig(absolutePath),
|
|
153
|
-
mcpConfig: await PluginLoader.loadMcpConfig(absolutePath),
|
|
173
|
+
mcpConfig: await PluginLoader.loadMcpConfig(absolutePath, manifest.mcpServers),
|
|
154
174
|
hooksConfig: await PluginLoader.loadHooksConfig(absolutePath),
|
|
155
175
|
};
|
|
156
176
|
// Register components with managers
|
|
@@ -168,8 +188,15 @@ export class PluginManager {
|
|
|
168
188
|
}
|
|
169
189
|
if (this.mcpManager && plugin.mcpConfig) {
|
|
170
190
|
for (const [name, config] of Object.entries(plugin.mcpConfig.mcpServers)) {
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
// One unusable server declaration must not take the rest of the plugin
|
|
192
|
+
// down with it (spec plugin A-023).
|
|
193
|
+
try {
|
|
194
|
+
const configWithPluginRoot = { ...config, pluginRoot: plugin.path };
|
|
195
|
+
this.mcpManager.addServer(name, configWithPluginRoot);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
logger?.warn(`Failed to register MCP server '${name}' from plugin ${plugin.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
199
|
+
}
|
|
173
200
|
}
|
|
174
201
|
}
|
|
175
202
|
if (this.hookManager && plugin.hooksConfig) {
|
|
@@ -179,17 +206,31 @@ export class PluginManager {
|
|
|
179
206
|
this.subagentManager.registerPluginAgents(plugin.name, plugin.agents);
|
|
180
207
|
}
|
|
181
208
|
this.plugins.set(manifest.name, plugin);
|
|
182
|
-
logger?.debug(`Loaded plugin: ${manifest.name} v${
|
|
209
|
+
logger?.debug(`Loaded plugin: ${manifest.name} v${plugin.version}`);
|
|
183
210
|
}
|
|
184
211
|
catch (error) {
|
|
212
|
+
this.loadFailures.push({
|
|
213
|
+
path: absolutePath,
|
|
214
|
+
error: error instanceof Error ? error.message : String(error),
|
|
215
|
+
});
|
|
185
216
|
logger?.error(`Failed to load plugin from ${absolutePath}`, error);
|
|
186
217
|
}
|
|
187
218
|
}
|
|
188
219
|
/**
|
|
189
220
|
* Load plugins from configuration
|
|
190
221
|
* @param configs Array of plugin configurations
|
|
222
|
+
* @returns the plugins that failed to load
|
|
191
223
|
*/
|
|
192
224
|
async loadPlugins(configs) {
|
|
225
|
+
return this.serialize(() => this.doLoadPlugins(configs));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The actual load. Reached through [loadPlugins] (which serializes concurrent
|
|
229
|
+
* requests) and directly from [reloadAllPlugins], which already holds the slot.
|
|
230
|
+
*/
|
|
231
|
+
async doLoadPlugins(configs) {
|
|
232
|
+
this.lastLoadConfigs = configs;
|
|
233
|
+
this.loadFailures = [];
|
|
193
234
|
// Load plugins from configuration (e.g. --plugin-dir) first to give them higher priority
|
|
194
235
|
for (const config of configs) {
|
|
195
236
|
if (config.type !== "local") {
|
|
@@ -205,6 +246,71 @@ export class PluginManager {
|
|
|
205
246
|
await this.loadInstalledPlugins();
|
|
206
247
|
// Load built-in plugins bundled with the SDK (lowest priority)
|
|
207
248
|
await this.loadBuiltinPlugins();
|
|
249
|
+
return [...this.loadFailures];
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Unload a single plugin: drop everything it contributed from the six
|
|
253
|
+
* capability registries, then forget it. Other plugins and the running
|
|
254
|
+
* session are untouched.
|
|
255
|
+
* @returns true if the plugin was loaded
|
|
256
|
+
*/
|
|
257
|
+
async unloadPlugin(name) {
|
|
258
|
+
const plugin = this.plugins.get(name);
|
|
259
|
+
if (!plugin) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
// Managers key their entries off the plugin name (commands/skills/agents)
|
|
263
|
+
// or the plugin root path (hooks/LSP/MCP), so each needs its own removal.
|
|
264
|
+
this.slashCommandManager?.unregisterPluginCommands(plugin.name);
|
|
265
|
+
this.skillManager?.unregisterPluginSkills(plugin.name);
|
|
266
|
+
this.subagentManager?.unregisterPluginAgents(plugin.name);
|
|
267
|
+
this.hookManager?.unregisterPluginHooks(plugin.path);
|
|
268
|
+
await this.lspManager?.unregisterServersForPlugin(plugin.path);
|
|
269
|
+
this.mcpManager?.removeServersForPlugin(plugin.path);
|
|
270
|
+
// Revoke the builtin helper-script grants this plugin added, so disabling
|
|
271
|
+
// it does not leave a standing permission behind.
|
|
272
|
+
for (const rule of PluginManager.BUILTIN_PLUGIN_ALLOW_RULES[plugin.name] ||
|
|
273
|
+
[]) {
|
|
274
|
+
this.permissionManager?.removeInstanceAllowedRule(rule);
|
|
275
|
+
}
|
|
276
|
+
this.plugins.delete(name);
|
|
277
|
+
logger?.debug(`Unloaded plugin: ${name}`);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Re-read every plugin in place — the `/reload-plugins` command.
|
|
282
|
+
*
|
|
283
|
+
* Unloads everything currently loaded, re-reads `enabledPlugins` from the
|
|
284
|
+
* configuration chain (so a settings edit is honored), then replays the
|
|
285
|
+
* original load order (explicit configs → installed → builtin). The session,
|
|
286
|
+
* transcript and sessionId are untouched; only the capability registries are
|
|
287
|
+
* swapped, which is what makes this an in-place reload instead of a rebuild.
|
|
288
|
+
*
|
|
289
|
+
* Unload must happen before any caller-side mutation of the plugin cache dir:
|
|
290
|
+
* hooks, MCP, LSP, skills and agents all bake absolute plugin paths at
|
|
291
|
+
* registration time, so an entry left pointing at a deleted directory is
|
|
292
|
+
* worse than a merely stale one.
|
|
293
|
+
*/
|
|
294
|
+
async reloadAllPlugins() {
|
|
295
|
+
return this.serialize(async () => {
|
|
296
|
+
for (const name of Array.from(this.plugins.keys())) {
|
|
297
|
+
await this.unloadPlugin(name);
|
|
298
|
+
}
|
|
299
|
+
this.enabledPlugins = this.refreshEnabledPlugins();
|
|
300
|
+
const failures = await this.doLoadPlugins(this.lastLoadConfigs);
|
|
301
|
+
return {
|
|
302
|
+
plugins: Array.from(this.plugins.keys()),
|
|
303
|
+
failures,
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Re-read enabled plugins from the merged configuration chain, falling back
|
|
309
|
+
* to the last known value when the service is unavailable.
|
|
310
|
+
*/
|
|
311
|
+
refreshEnabledPlugins() {
|
|
312
|
+
const merged = this.configurationService?.getMergedEnabledPlugins(this.workdir);
|
|
313
|
+
return merged ?? this.enabledPlugins;
|
|
208
314
|
}
|
|
209
315
|
/**
|
|
210
316
|
* Load built-in plugins bundled with the SDK (e.g. sdd).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConfigurationService } from "../services/configurationService.js";
|
|
2
2
|
import { PluginManager } from "./pluginManager.js";
|
|
3
|
-
import { Logger } from "../types/index.js";
|
|
3
|
+
import { Logger, PluginInstallLocation } from "../types/index.js";
|
|
4
4
|
import { Scope } from "../types/configuration.js";
|
|
5
5
|
export interface PluginScopeManagerOptions {
|
|
6
6
|
workdir: string;
|
|
@@ -31,9 +31,22 @@ export declare class PluginScopeManager {
|
|
|
31
31
|
* Priority: local > project > user
|
|
32
32
|
*/
|
|
33
33
|
findPluginScope(pluginId: string): Scope | null;
|
|
34
|
+
/**
|
|
35
|
+
* The install location a record for `scope` is keyed by (spec plugin A-015):
|
|
36
|
+
* 用户作用域是本机全局的(不带 projectPath),项目/本地作用域归属当前仓库。
|
|
37
|
+
*/
|
|
38
|
+
getInstallLocation(scope: Scope): PluginInstallLocation;
|
|
39
|
+
/**
|
|
40
|
+
* Remove a plugin's enabled record from a single scope, leaving every other
|
|
41
|
+
* scope (including other projects' project/local records) untouched — the
|
|
42
|
+
* uninstall semantics of spec plugin A-015. Use [removePluginFromAllScopes]
|
|
43
|
+
* only for 「更换安装作用域」.
|
|
44
|
+
*/
|
|
45
|
+
removePluginFromScope(scope: Scope, pluginId: string): Promise<void>;
|
|
34
46
|
/**
|
|
35
47
|
* Remove a plugin from all scopes (user, project, local)
|
|
36
|
-
* This is useful when
|
|
48
|
+
* This is useful when moving a plugin to another scope: the old scope must
|
|
49
|
+
* not keep the plugin (spec plugin「设置页插件市场」场景 11).
|
|
37
50
|
*/
|
|
38
51
|
removePluginFromAllScopes(pluginId: string): Promise<void>;
|
|
39
52
|
/**
|
|
@@ -56,9 +56,28 @@ export class PluginScopeManager {
|
|
|
56
56
|
}
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The install location a record for `scope` is keyed by (spec plugin A-015):
|
|
61
|
+
* 用户作用域是本机全局的(不带 projectPath),项目/本地作用域归属当前仓库。
|
|
62
|
+
*/
|
|
63
|
+
getInstallLocation(scope) {
|
|
64
|
+
return scope === "user" ? { scope } : { scope, projectPath: this.workdir };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Remove a plugin's enabled record from a single scope, leaving every other
|
|
68
|
+
* scope (including other projects' project/local records) untouched — the
|
|
69
|
+
* uninstall semantics of spec plugin A-015. Use [removePluginFromAllScopes]
|
|
70
|
+
* only for 「更换安装作用域」.
|
|
71
|
+
*/
|
|
72
|
+
async removePluginFromScope(scope, pluginId) {
|
|
73
|
+
await this.configurationService.removeEnabledPlugin(this.workdir, scope, pluginId);
|
|
74
|
+
this.logger?.info(`Removed plugin ${pluginId} from ${scope} scope`);
|
|
75
|
+
this.refreshPluginManager();
|
|
76
|
+
}
|
|
59
77
|
/**
|
|
60
78
|
* Remove a plugin from all scopes (user, project, local)
|
|
61
|
-
* This is useful when
|
|
79
|
+
* This is useful when moving a plugin to another scope: the old scope must
|
|
80
|
+
* not keep the plugin (spec plugin「设置页插件市场」场景 11).
|
|
62
81
|
*/
|
|
63
82
|
async removePluginFromAllScopes(pluginId) {
|
|
64
83
|
const scopes = ["user", "project", "local"];
|
|
@@ -16,6 +16,11 @@ export declare class SkillManager extends EventEmitter {
|
|
|
16
16
|
private pluginSkillMetadata;
|
|
17
17
|
private pluginSkillContent;
|
|
18
18
|
private initialized;
|
|
19
|
+
/** Serializes full rescans (initialize / file-watcher events / deleteSkill
|
|
20
|
+
* convergence). Overlapping scans over the shared caches must not interleave:
|
|
21
|
+
* chaining them keeps the last completed scan in sync with the newest disk
|
|
22
|
+
* state instead of letting an older snapshot win a race. */
|
|
23
|
+
private refreshChain;
|
|
19
24
|
private fileWatcher;
|
|
20
25
|
private watchEnabled;
|
|
21
26
|
constructor(container: Container, options?: SkillManagerOptions);
|
|
@@ -27,6 +32,7 @@ export declare class SkillManager extends EventEmitter {
|
|
|
27
32
|
* Refresh skills by re-discovering them
|
|
28
33
|
*/
|
|
29
34
|
private refreshSkills;
|
|
35
|
+
private performRefresh;
|
|
30
36
|
/**
|
|
31
37
|
* Setup file watcher for skill directories
|
|
32
38
|
*/
|
|
@@ -97,6 +103,17 @@ export declare class SkillManager extends EventEmitter {
|
|
|
97
103
|
content: string;
|
|
98
104
|
skill?: undefined;
|
|
99
105
|
}>;
|
|
106
|
+
/**
|
|
107
|
+
* Render a skill's body the way the model must see it: frontmatter stripped,
|
|
108
|
+
* parameters substituted, `${WAVE_SKILL_DIR}` / `${CLAUDE_SKILL_DIR}` /
|
|
109
|
+
* `${WAVE_PLUGIN_ROOT}` replaced with real paths, and the
|
|
110
|
+
* `Base directory for this skill: <path>` header prepended.
|
|
111
|
+
*
|
|
112
|
+
* Public because the post-compaction re-injection has to render the same
|
|
113
|
+
* text when it cannot reuse the content recorded at invocation time
|
|
114
|
+
* (spec core/message-compact 场景 4).
|
|
115
|
+
*/
|
|
116
|
+
renderSkillContent(skill: Skill, argsString?: string): string;
|
|
100
117
|
/**
|
|
101
118
|
* Prepare skill content with arguments but without bash execution
|
|
102
119
|
*/
|
|
@@ -113,4 +130,37 @@ export declare class SkillManager extends EventEmitter {
|
|
|
113
130
|
* Register skills provided by a plugin
|
|
114
131
|
*/
|
|
115
132
|
registerPluginSkills(pluginName: string, skills: Skill[]): void;
|
|
133
|
+
/**
|
|
134
|
+
* Drop every skill contributed by a plugin, addressed by its plugin name.
|
|
135
|
+
* Clears the plugin-specific maps too: refreshSkills() re-seeds the visible
|
|
136
|
+
* maps from them, so clearing only those would resurrect the skills on the
|
|
137
|
+
* next disk refresh. Called by the in-place plugin reload path before
|
|
138
|
+
* re-registering.
|
|
139
|
+
*/
|
|
140
|
+
unregisterPluginSkills(pluginName: string): void;
|
|
141
|
+
/**
|
|
142
|
+
* Directory roots that may hold deletable copies of a skill in the given
|
|
143
|
+
* discovery scope. Personal skills are scanned from three user-level dirs
|
|
144
|
+
* (~/.wave|~/.claude|~/.agents/skills) and project skills from the current
|
|
145
|
+
* project's three project dirs (.wave|.claude|.agents/skills). The discovery
|
|
146
|
+
* map merges same-named copies from these roots into a single name-keyed
|
|
147
|
+
* entry, so deleteSkill must sweep the whole set to remove a skill fully.
|
|
148
|
+
*/
|
|
149
|
+
private scopeSkillRoots;
|
|
150
|
+
/**
|
|
151
|
+
* Delete a user/personal or project skill by removing its directory
|
|
152
|
+
* (containing SKILL.md). Builtin and plugin skills are read-only and
|
|
153
|
+
* cannot be deleted.
|
|
154
|
+
*
|
|
155
|
+
* A same-named skill may physically live in several same-scope directories
|
|
156
|
+
* (~/.wave/skills + ~/.claude/skills + ~/.agents/skills for user skills,
|
|
157
|
+
* .wave/.claude/.agents/skills under the project for project skills) while
|
|
158
|
+
* only the highest-priority copy is listed; every copy is removed so one
|
|
159
|
+
* delete removes the skill completely instead of re-surfacing the next
|
|
160
|
+
* copy on the next scan. Caches are then re-converged with disk before
|
|
161
|
+
* returning so the caller's follow-up metadata read reflects the result.
|
|
162
|
+
* @param name - The skill name as shown in skill metadata
|
|
163
|
+
* @returns true if the skill was deleted
|
|
164
|
+
*/
|
|
165
|
+
deleteSkill(name: string): Promise<boolean>;
|
|
116
166
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, stat } from "fs/promises";
|
|
1
|
+
import { readdir, stat, rm } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { EventEmitter } from "events";
|
|
@@ -21,6 +21,11 @@ export class SkillManager extends EventEmitter {
|
|
|
21
21
|
this.pluginSkillMetadata = new Map();
|
|
22
22
|
this.pluginSkillContent = new Map();
|
|
23
23
|
this.initialized = false;
|
|
24
|
+
/** Serializes full rescans (initialize / file-watcher events / deleteSkill
|
|
25
|
+
* convergence). Overlapping scans over the shared caches must not interleave:
|
|
26
|
+
* chaining them keeps the last completed scan in sync with the newest disk
|
|
27
|
+
* state instead of letting an older snapshot win a race. */
|
|
28
|
+
this.refreshChain = Promise.resolve();
|
|
24
29
|
this.fileWatcher = null;
|
|
25
30
|
this.personalSkillsPath =
|
|
26
31
|
options.personalSkillsPath || join(homedir(), ".wave", "skills");
|
|
@@ -53,27 +58,52 @@ export class SkillManager extends EventEmitter {
|
|
|
53
58
|
/**
|
|
54
59
|
* Refresh skills by re-discovering them
|
|
55
60
|
*/
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
refreshSkills() {
|
|
62
|
+
// Serialize rescans: initialize / file-watcher events / deleteSkill all
|
|
63
|
+
// trigger full re-scans; running them concurrently makes a stale snapshot
|
|
64
|
+
// or a partial cache (see performRefresh's atomic swap) observable to
|
|
65
|
+
// metadata readers such as the settings-page skill list.
|
|
66
|
+
const run = this.refreshChain.then(() => this.performRefresh());
|
|
67
|
+
this.refreshChain = run.catch(() => { });
|
|
68
|
+
return run;
|
|
69
|
+
}
|
|
70
|
+
async performRefresh() {
|
|
71
|
+
// Route this scan's incremental skill-content writes into a scratch map
|
|
72
|
+
// (processSkillDirs writes to this.skillContent) and rebuild both caches
|
|
73
|
+
// fully before swapping them in — readers must never observe a
|
|
74
|
+
// half-refreshed (e.g. momentarily empty) skill list mid-scan.
|
|
75
|
+
const scratchContent = new Map();
|
|
76
|
+
const previousContent = this.skillContent;
|
|
77
|
+
this.skillContent = scratchContent;
|
|
78
|
+
let discovery;
|
|
79
|
+
try {
|
|
80
|
+
discovery = await this.discoverSkills();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.skillContent = previousContent;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
const nextMetadata = new Map();
|
|
87
|
+
const nextContent = new Map();
|
|
88
|
+
// Store discovered skills (builtin/personal/project)
|
|
62
89
|
discovery.builtinSkills.forEach((skill, name) => {
|
|
63
|
-
|
|
90
|
+
nextMetadata.set(name, skill);
|
|
64
91
|
});
|
|
65
92
|
discovery.personalSkills.forEach((skill, name) => {
|
|
66
|
-
|
|
93
|
+
nextMetadata.set(name, skill);
|
|
67
94
|
});
|
|
68
95
|
discovery.projectSkills.forEach((skill, name) => {
|
|
69
|
-
|
|
96
|
+
nextMetadata.set(name, skill);
|
|
97
|
+
});
|
|
98
|
+
scratchContent.forEach((skill, name) => {
|
|
99
|
+
nextContent.set(name, skill);
|
|
70
100
|
});
|
|
71
101
|
// Restore plugin skills
|
|
72
102
|
this.pluginSkillMetadata.forEach((metadata, name) => {
|
|
73
|
-
|
|
103
|
+
nextMetadata.set(name, metadata);
|
|
74
104
|
});
|
|
75
105
|
this.pluginSkillContent.forEach((skill, name) => {
|
|
76
|
-
|
|
106
|
+
nextContent.set(name, skill);
|
|
77
107
|
});
|
|
78
108
|
// Log any discovery errors
|
|
79
109
|
if (discovery.errors.length > 0) {
|
|
@@ -82,6 +112,9 @@ export class SkillManager extends EventEmitter {
|
|
|
82
112
|
logger?.warn(`Skill error in ${error.skillPath}: ${error.message}`);
|
|
83
113
|
});
|
|
84
114
|
}
|
|
115
|
+
// Atomic swap: from this point on readers see the complete new snapshot
|
|
116
|
+
this.skillMetadata = nextMetadata;
|
|
117
|
+
this.skillContent = nextContent;
|
|
85
118
|
this.emit("refreshed", Array.from(this.skillMetadata.values()));
|
|
86
119
|
}
|
|
87
120
|
/**
|
|
@@ -389,6 +422,19 @@ export class SkillManager extends EventEmitter {
|
|
|
389
422
|
};
|
|
390
423
|
}
|
|
391
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Render a skill's body the way the model must see it: frontmatter stripped,
|
|
427
|
+
* parameters substituted, `${WAVE_SKILL_DIR}` / `${CLAUDE_SKILL_DIR}` /
|
|
428
|
+
* `${WAVE_PLUGIN_ROOT}` replaced with real paths, and the
|
|
429
|
+
* `Base directory for this skill: <path>` header prepended.
|
|
430
|
+
*
|
|
431
|
+
* Public because the post-compaction re-injection has to render the same
|
|
432
|
+
* text when it cannot reuse the content recorded at invocation time
|
|
433
|
+
* (spec core/message-compact 场景 4).
|
|
434
|
+
*/
|
|
435
|
+
renderSkillContent(skill, argsString = "") {
|
|
436
|
+
return this.prepareSkillContent(skill, argsString);
|
|
437
|
+
}
|
|
392
438
|
/**
|
|
393
439
|
* Prepare skill content with arguments but without bash execution
|
|
394
440
|
*/
|
|
@@ -463,4 +509,112 @@ export class SkillManager extends EventEmitter {
|
|
|
463
509
|
}
|
|
464
510
|
logger?.debug(`Registered ${skills.length} plugin skills from ${pluginName}. Total skills: ${this.skillMetadata.size}`);
|
|
465
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Drop every skill contributed by a plugin, addressed by its plugin name.
|
|
514
|
+
* Clears the plugin-specific maps too: refreshSkills() re-seeds the visible
|
|
515
|
+
* maps from them, so clearing only those would resurrect the skills on the
|
|
516
|
+
* next disk refresh. Called by the in-place plugin reload path before
|
|
517
|
+
* re-registering.
|
|
518
|
+
*/
|
|
519
|
+
unregisterPluginSkills(pluginName) {
|
|
520
|
+
const prefix = `${pluginName}:`;
|
|
521
|
+
for (const name of Array.from(this.pluginSkillMetadata.keys())) {
|
|
522
|
+
if (name.startsWith(prefix)) {
|
|
523
|
+
this.pluginSkillMetadata.delete(name);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
for (const name of Array.from(this.pluginSkillContent.keys())) {
|
|
527
|
+
if (name.startsWith(prefix)) {
|
|
528
|
+
this.pluginSkillContent.delete(name);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
for (const name of Array.from(this.skillMetadata.keys())) {
|
|
532
|
+
if (name.startsWith(prefix)) {
|
|
533
|
+
this.skillMetadata.delete(name);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const name of Array.from(this.skillContent.keys())) {
|
|
537
|
+
if (name.startsWith(prefix)) {
|
|
538
|
+
this.skillContent.delete(name);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
logger?.debug(`Unregistered plugin skills from ${pluginName}`);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Directory roots that may hold deletable copies of a skill in the given
|
|
545
|
+
* discovery scope. Personal skills are scanned from three user-level dirs
|
|
546
|
+
* (~/.wave|~/.claude|~/.agents/skills) and project skills from the current
|
|
547
|
+
* project's three project dirs (.wave|.claude|.agents/skills). The discovery
|
|
548
|
+
* map merges same-named copies from these roots into a single name-keyed
|
|
549
|
+
* entry, so deleteSkill must sweep the whole set to remove a skill fully.
|
|
550
|
+
*/
|
|
551
|
+
scopeSkillRoots(type) {
|
|
552
|
+
if (type === "personal") {
|
|
553
|
+
return [
|
|
554
|
+
this.personalSkillsPath,
|
|
555
|
+
this.personalClaudeSkillsPath,
|
|
556
|
+
this.personalAgentsSkillsPath,
|
|
557
|
+
];
|
|
558
|
+
}
|
|
559
|
+
if (type === "project") {
|
|
560
|
+
return [
|
|
561
|
+
join(this.workdir, ".wave", "skills"),
|
|
562
|
+
join(this.workdir, ".claude", "skills"),
|
|
563
|
+
join(this.workdir, ".agents", "skills"),
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Delete a user/personal or project skill by removing its directory
|
|
570
|
+
* (containing SKILL.md). Builtin and plugin skills are read-only and
|
|
571
|
+
* cannot be deleted.
|
|
572
|
+
*
|
|
573
|
+
* A same-named skill may physically live in several same-scope directories
|
|
574
|
+
* (~/.wave/skills + ~/.claude/skills + ~/.agents/skills for user skills,
|
|
575
|
+
* .wave/.claude/.agents/skills under the project for project skills) while
|
|
576
|
+
* only the highest-priority copy is listed; every copy is removed so one
|
|
577
|
+
* delete removes the skill completely instead of re-surfacing the next
|
|
578
|
+
* copy on the next scan. Caches are then re-converged with disk before
|
|
579
|
+
* returning so the caller's follow-up metadata read reflects the result.
|
|
580
|
+
* @param name - The skill name as shown in skill metadata
|
|
581
|
+
* @returns true if the skill was deleted
|
|
582
|
+
*/
|
|
583
|
+
async deleteSkill(name) {
|
|
584
|
+
if (!this.initialized) {
|
|
585
|
+
throw new Error("SkillManager not initialized. Call initialize() first.");
|
|
586
|
+
}
|
|
587
|
+
const metadata = this.skillMetadata.get(name);
|
|
588
|
+
if (!metadata) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
// Builtin and plugin skills are read-only
|
|
592
|
+
if (metadata.type === "builtin" || metadata.pluginName) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
if (!metadata.skillPath) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
const dirsToRemove = new Set([metadata.skillPath]);
|
|
599
|
+
for (const root of this.scopeSkillRoots(metadata.type)) {
|
|
600
|
+
dirsToRemove.add(join(root, name));
|
|
601
|
+
}
|
|
602
|
+
for (const dir of dirsToRemove) {
|
|
603
|
+
await rm(dir, { recursive: true, force: true });
|
|
604
|
+
}
|
|
605
|
+
// Converge caches with disk: the serialized full refresh both drops the
|
|
606
|
+
// deleted entries and reflects any copy that survived deletion. If the
|
|
607
|
+
// refresh itself fails, fall back to dropping the entry from memory so
|
|
608
|
+
// the deleted skill does not keep being served.
|
|
609
|
+
try {
|
|
610
|
+
await this.refreshSkills();
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
logger?.warn(`Failed to refresh skills after deleting '${name}': ${error}`);
|
|
614
|
+
this.skillMetadata.delete(name);
|
|
615
|
+
this.skillContent.delete(name);
|
|
616
|
+
}
|
|
617
|
+
logger?.debug(`Deleted skill '${name}' at ${metadata.skillPath}`);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
466
620
|
}
|
|
@@ -9,6 +9,9 @@ export declare class SlashCommandManager {
|
|
|
9
9
|
private commands;
|
|
10
10
|
private customCommands;
|
|
11
11
|
private skillCommandIds;
|
|
12
|
+
/** Ids registered by plugins, so they can be dropped per-plugin and are not
|
|
13
|
+
* mistaken for disk-sourced custom commands. */
|
|
14
|
+
private pluginCommandIds;
|
|
12
15
|
private workdir;
|
|
13
16
|
private currentCommandAbortController;
|
|
14
17
|
constructor(container: Container, options: SlashCommandManagerOptions);
|
|
@@ -29,6 +32,13 @@ export declare class SlashCommandManager {
|
|
|
29
32
|
* Register commands from a plugin with namespacing
|
|
30
33
|
*/
|
|
31
34
|
registerPluginCommands(pluginName: string, commands: CustomSlashCommand[]): void;
|
|
35
|
+
/**
|
|
36
|
+
* Drop every slash command contributed by a plugin, addressed by its plugin
|
|
37
|
+
* name. Removes the registry entry and the customCommands source entry.
|
|
38
|
+
* Called by the in-place plugin reload path before re-registering.
|
|
39
|
+
* @returns the number of commands removed
|
|
40
|
+
*/
|
|
41
|
+
unregisterPluginCommands(pluginName: string): number;
|
|
32
42
|
/**
|
|
33
43
|
* Reload custom commands (useful for development)
|
|
34
44
|
*/
|