wave-agent-sdk 1.1.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/dist/agent.d.ts +128 -28
  2. package/dist/agent.js +201 -49
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/plugins.js +11 -20
  5. package/dist/builtin/skills/settings.js +7 -20
  6. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  7. package/dist/builtin/skills/wave-daemon.js +194 -0
  8. package/dist/constants/images.d.ts +26 -0
  9. package/dist/constants/images.js +26 -0
  10. package/dist/constants/index.d.ts +16 -0
  11. package/dist/constants/index.js +16 -0
  12. package/dist/constants/memory.d.ts +26 -0
  13. package/dist/constants/memory.js +34 -0
  14. package/dist/constants/messages.d.ts +11 -0
  15. package/dist/constants/messages.js +11 -0
  16. package/dist/constants/plugins.d.ts +8 -0
  17. package/dist/constants/plugins.js +8 -0
  18. package/dist/constants/tools.d.ts +1 -0
  19. package/dist/constants/tools.js +1 -0
  20. package/dist/core/plugin.d.ts +54 -10
  21. package/dist/core/plugin.js +137 -23
  22. package/dist/core/session.d.ts +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/exec/catalog.d.ts +140 -0
  25. package/dist/exec/catalog.js +470 -0
  26. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  27. package/dist/exec/catalogAnnouncement.js +293 -0
  28. package/dist/exec/constants.d.ts +51 -0
  29. package/dist/exec/constants.js +51 -0
  30. package/dist/exec/execRuntime.d.ts +55 -0
  31. package/dist/exec/execRuntime.js +217 -0
  32. package/dist/exec/workerSource.d.ts +28 -0
  33. package/dist/exec/workerSource.js +299 -0
  34. package/dist/host/index.d.ts +23 -0
  35. package/dist/host/index.js +23 -0
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.js +8 -1
  38. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  39. package/dist/managers/MemoryRuleManager.js +12 -0
  40. package/dist/managers/aiManager.d.ts +35 -25
  41. package/dist/managers/aiManager.js +204 -202
  42. package/dist/managers/backgroundTaskManager.js +14 -0
  43. package/dist/managers/bashModeManager.d.ts +33 -0
  44. package/dist/managers/bashModeManager.js +110 -0
  45. package/dist/managers/hookManager.d.ts +18 -0
  46. package/dist/managers/hookManager.js +37 -3
  47. package/dist/managers/liveConfigManager.d.ts +33 -0
  48. package/dist/managers/liveConfigManager.js +106 -11
  49. package/dist/managers/lspManager.d.ts +9 -0
  50. package/dist/managers/lspManager.js +47 -18
  51. package/dist/managers/mcpManager.d.ts +68 -10
  52. package/dist/managers/mcpManager.js +265 -15
  53. package/dist/managers/messageManager.d.ts +60 -18
  54. package/dist/managers/messageManager.js +170 -81
  55. package/dist/managers/permissionManager.d.ts +69 -0
  56. package/dist/managers/permissionManager.js +221 -78
  57. package/dist/managers/planManager.d.ts +9 -0
  58. package/dist/managers/planManager.js +19 -1
  59. package/dist/managers/pluginManager.d.ts +46 -2
  60. package/dist/managers/pluginManager.js +117 -11
  61. package/dist/managers/pluginScopeManager.d.ts +15 -2
  62. package/dist/managers/pluginScopeManager.js +20 -1
  63. package/dist/managers/skillManager.d.ts +50 -0
  64. package/dist/managers/skillManager.js +166 -12
  65. package/dist/managers/slashCommandManager.d.ts +10 -0
  66. package/dist/managers/slashCommandManager.js +44 -31
  67. package/dist/managers/subagentManager.d.ts +15 -0
  68. package/dist/managers/subagentManager.js +81 -9
  69. package/dist/managers/toolManager.d.ts +29 -3
  70. package/dist/managers/toolManager.js +87 -13
  71. package/dist/managers/workflowManager.js +6 -0
  72. package/dist/prompts/autoMemory.d.ts +9 -0
  73. package/dist/prompts/autoMemory.js +30 -31
  74. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  75. package/dist/prompts/autoMemoryExtraction.js +8 -111
  76. package/dist/prompts/index.d.ts +0 -1
  77. package/dist/prompts/index.js +0 -4
  78. package/dist/prompts/memoryTypes.d.ts +63 -0
  79. package/dist/prompts/memoryTypes.js +191 -0
  80. package/dist/services/GitService.d.ts +7 -0
  81. package/dist/services/GitService.js +23 -0
  82. package/dist/services/MarketplaceService.d.ts +101 -17
  83. package/dist/services/MarketplaceService.js +323 -102
  84. package/dist/services/artifactContent.d.ts +84 -0
  85. package/dist/services/artifactContent.js +204 -0
  86. package/dist/services/artifactSession.d.ts +6 -0
  87. package/dist/services/artifactSession.js +17 -0
  88. package/dist/services/autoMemoryService.js +5 -13
  89. package/dist/services/configurationService.d.ts +92 -9
  90. package/dist/services/configurationService.js +246 -64
  91. package/dist/services/contentSummarizer.d.ts +15 -0
  92. package/dist/services/contentSummarizer.js +45 -0
  93. package/dist/services/execAvailability.d.ts +9 -0
  94. package/dist/services/execAvailability.js +32 -0
  95. package/dist/services/fileWatcher.js +61 -6
  96. package/dist/services/initializationService.js +21 -17
  97. package/dist/services/interactionService.d.ts +9 -1
  98. package/dist/services/interactionService.js +28 -8
  99. package/dist/services/jsonlHandler.d.ts +98 -0
  100. package/dist/services/jsonlHandler.js +250 -12
  101. package/dist/services/memory.d.ts +17 -1
  102. package/dist/services/memory.js +44 -7
  103. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  104. package/dist/services/officialMarketplaceMirror.js +290 -0
  105. package/dist/services/pluginLoader.d.ts +12 -4
  106. package/dist/services/pluginLoader.js +38 -7
  107. package/dist/services/remoteSettingsService.js +20 -6
  108. package/dist/services/session.d.ts +74 -0
  109. package/dist/services/session.js +174 -16
  110. package/dist/services/sessionEntries.d.ts +2 -0
  111. package/dist/services/sessionEntries.js +20 -0
  112. package/dist/services/worktreeHooks.js +6 -1
  113. package/dist/stdio/index.d.ts +12 -0
  114. package/dist/stdio/index.js +12 -0
  115. package/dist/stdio/notificationRouter.d.ts +38 -0
  116. package/dist/stdio/notificationRouter.js +97 -0
  117. package/dist/stdio/rpcClient.d.ts +18 -0
  118. package/dist/stdio/rpcClient.js +10 -0
  119. package/dist/stdio/stdioAgent.d.ts +229 -0
  120. package/dist/stdio/stdioAgent.js +360 -0
  121. package/dist/tools/artifactTool.js +406 -273
  122. package/dist/tools/bashTool.js +10 -6
  123. package/dist/tools/editTool.js +6 -3
  124. package/dist/tools/execTool.d.ts +2 -0
  125. package/dist/tools/execTool.js +165 -0
  126. package/dist/tools/exitPlanMode.js +10 -2
  127. package/dist/tools/grepTool.js +7 -1
  128. package/dist/tools/readTool.js +30 -2
  129. package/dist/tools/types.d.ts +34 -8
  130. package/dist/tools/webFetchTool.js +15 -166
  131. package/dist/tools/workflowTool.js +40 -8
  132. package/dist/tools/writeTool.js +6 -3
  133. package/dist/types/agent.d.ts +24 -1
  134. package/dist/types/commands.d.ts +7 -0
  135. package/dist/types/configuration.d.ts +45 -2
  136. package/dist/types/hooks.d.ts +1 -0
  137. package/dist/types/hooks.js +19 -0
  138. package/dist/types/marketplace.d.ts +40 -2
  139. package/dist/types/mcp.d.ts +42 -0
  140. package/dist/types/messaging.d.ts +1 -8
  141. package/dist/types/permissions.d.ts +22 -0
  142. package/dist/types/permissions.js +17 -0
  143. package/dist/types/plugins.d.ts +26 -2
  144. package/dist/types/skills.d.ts +26 -0
  145. package/dist/utils/bashParser.d.ts +17 -0
  146. package/dist/utils/bashParser.js +72 -0
  147. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  148. package/dist/utils/bashStructure/bashLexer.js +676 -0
  149. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  150. package/dist/utils/bashStructure/bashParser.js +606 -0
  151. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  152. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  153. package/dist/utils/bashStructure/index.d.ts +26 -0
  154. package/dist/utils/bashStructure/index.js +27 -0
  155. package/dist/utils/bashStructure/types.d.ts +62 -0
  156. package/dist/utils/bashStructure/types.js +47 -0
  157. package/dist/utils/constants.d.ts +10 -0
  158. package/dist/utils/constants.js +10 -0
  159. package/dist/utils/containerSetup.js +48 -6
  160. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  161. package/dist/utils/convertMessagesForAPI.js +64 -14
  162. package/dist/utils/fileChangeReminder.d.ts +20 -0
  163. package/dist/utils/fileChangeReminder.js +153 -0
  164. package/dist/utils/fileSearch.js +4 -3
  165. package/dist/utils/fileUtils.d.ts +44 -0
  166. package/dist/utils/fileUtils.js +118 -0
  167. package/dist/utils/frontmatterYaml.d.ts +33 -0
  168. package/dist/utils/frontmatterYaml.js +192 -0
  169. package/dist/utils/imageBudget.d.ts +85 -0
  170. package/dist/utils/imageBudget.js +109 -0
  171. package/dist/utils/imageDimensions.d.ts +83 -0
  172. package/dist/utils/imageDimensions.js +232 -0
  173. package/dist/utils/imageProcessor.d.ts +66 -0
  174. package/dist/utils/imageProcessor.js +84 -0
  175. package/dist/utils/imageRewrite.d.ts +29 -0
  176. package/dist/utils/imageRewrite.js +251 -0
  177. package/dist/utils/markdownParser.d.ts +5 -1
  178. package/dist/utils/markdownParser.js +9 -51
  179. package/dist/utils/mcpInstructions.d.ts +61 -0
  180. package/dist/utils/mcpInstructions.js +126 -0
  181. package/dist/utils/mcpUtils.d.ts +7 -0
  182. package/dist/utils/mcpUtils.js +11 -2
  183. package/dist/utils/memoryAge.d.ts +32 -0
  184. package/dist/utils/memoryAge.js +47 -0
  185. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  186. package/dist/utils/memoryEntrypoint.js +49 -0
  187. package/dist/utils/memoryIndex.d.ts +30 -0
  188. package/dist/utils/memoryIndex.js +76 -0
  189. package/dist/utils/messageOperations.d.ts +6 -20
  190. package/dist/utils/messageOperations.js +40 -91
  191. package/dist/utils/nestedMemory.d.ts +22 -0
  192. package/dist/utils/nestedMemory.js +61 -0
  193. package/dist/utils/npmTarball.d.ts +19 -0
  194. package/dist/utils/npmTarball.js +92 -0
  195. package/dist/utils/pluginSource.d.ts +37 -0
  196. package/dist/utils/pluginSource.js +73 -0
  197. package/dist/utils/ripgrep.d.ts +18 -4
  198. package/dist/utils/ripgrep.js +56 -4
  199. package/dist/utils/runtimeDeps.d.ts +35 -0
  200. package/dist/utils/runtimeDeps.js +426 -0
  201. package/dist/utils/skillParser.js +22 -52
  202. package/dist/utils/subagentParser.js +48 -45
  203. package/dist/utils/tokenCalculation.js +0 -8
  204. package/dist/utils/userSettings.d.ts +90 -0
  205. package/dist/utils/userSettings.js +291 -0
  206. package/dist/utils/worktreeUtils.d.ts +2 -1
  207. package/dist/utils/worktreeUtils.js +64 -34
  208. package/package.json +12 -4
  209. package/dist/managers/bangManager.d.ts +0 -26
  210. package/dist/managers/bangManager.js +0 -78
@@ -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 autoUpdate)
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
- const configWithPluginRoot = { ...config, pluginRoot: plugin.path };
172
- this.mcpManager.addServer(name, configWithPluginRoot);
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${manifest.version}`);
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 uninstalling a plugin to clean up all configuration
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 uninstalling a plugin to clean up all configuration
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
- async refreshSkills() {
57
- // Clear only discovered skills (builtin/personal/project), preserve plugin skills
58
- this.skillMetadata.clear();
59
- this.skillContent.clear();
60
- const discovery = await this.discoverSkills();
61
- // Store discovered skill metadata
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
- this.skillMetadata.set(name, skill);
90
+ nextMetadata.set(name, skill);
64
91
  });
65
92
  discovery.personalSkills.forEach((skill, name) => {
66
- this.skillMetadata.set(name, skill);
93
+ nextMetadata.set(name, skill);
67
94
  });
68
95
  discovery.projectSkills.forEach((skill, name) => {
69
- this.skillMetadata.set(name, skill);
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
- this.skillMetadata.set(name, metadata);
103
+ nextMetadata.set(name, metadata);
74
104
  });
75
105
  this.pluginSkillContent.forEach((skill, name) => {
76
- this.skillContent.set(name, skill);
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
  */