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.
Files changed (175) hide show
  1. package/dist/agent.d.ts +58 -4
  2. package/dist/agent.js +91 -19
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/skills/settings.js +1 -12
  5. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  6. package/dist/builtin/skills/wave-daemon.js +194 -0
  7. package/dist/constants/images.d.ts +26 -0
  8. package/dist/constants/images.js +26 -0
  9. package/dist/constants/index.d.ts +16 -0
  10. package/dist/constants/index.js +16 -0
  11. package/dist/constants/memory.d.ts +26 -0
  12. package/dist/constants/memory.js +34 -0
  13. package/dist/constants/messages.d.ts +11 -0
  14. package/dist/constants/messages.js +11 -0
  15. package/dist/constants/plugins.d.ts +8 -0
  16. package/dist/constants/plugins.js +8 -0
  17. package/dist/constants/tools.d.ts +1 -0
  18. package/dist/constants/tools.js +1 -0
  19. package/dist/core/plugin.d.ts +53 -13
  20. package/dist/core/plugin.js +134 -26
  21. package/dist/core/session.d.ts +1 -1
  22. package/dist/core/session.js +1 -1
  23. package/dist/exec/catalog.d.ts +140 -0
  24. package/dist/exec/catalog.js +470 -0
  25. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  26. package/dist/exec/catalogAnnouncement.js +293 -0
  27. package/dist/exec/constants.d.ts +51 -0
  28. package/dist/exec/constants.js +51 -0
  29. package/dist/exec/execRuntime.d.ts +55 -0
  30. package/dist/exec/execRuntime.js +217 -0
  31. package/dist/exec/workerSource.d.ts +28 -0
  32. package/dist/exec/workerSource.js +299 -0
  33. package/dist/host/index.d.ts +23 -0
  34. package/dist/host/index.js +23 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +6 -0
  37. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  38. package/dist/managers/MemoryRuleManager.js +12 -0
  39. package/dist/managers/aiManager.d.ts +35 -1
  40. package/dist/managers/aiManager.js +190 -21
  41. package/dist/managers/backgroundTaskManager.js +14 -0
  42. package/dist/managers/hookManager.d.ts +13 -0
  43. package/dist/managers/hookManager.js +31 -4
  44. package/dist/managers/liveConfigManager.d.ts +33 -0
  45. package/dist/managers/liveConfigManager.js +103 -8
  46. package/dist/managers/lspManager.d.ts +9 -0
  47. package/dist/managers/lspManager.js +47 -18
  48. package/dist/managers/mcpManager.d.ts +45 -10
  49. package/dist/managers/mcpManager.js +103 -1
  50. package/dist/managers/messageManager.d.ts +48 -5
  51. package/dist/managers/messageManager.js +107 -21
  52. package/dist/managers/permissionManager.d.ts +40 -0
  53. package/dist/managers/permissionManager.js +63 -8
  54. package/dist/managers/pluginManager.d.ts +46 -2
  55. package/dist/managers/pluginManager.js +117 -11
  56. package/dist/managers/pluginScopeManager.d.ts +15 -2
  57. package/dist/managers/pluginScopeManager.js +20 -1
  58. package/dist/managers/skillManager.d.ts +19 -0
  59. package/dist/managers/skillManager.js +44 -0
  60. package/dist/managers/slashCommandManager.d.ts +10 -0
  61. package/dist/managers/slashCommandManager.js +35 -3
  62. package/dist/managers/subagentManager.d.ts +8 -0
  63. package/dist/managers/subagentManager.js +20 -0
  64. package/dist/managers/toolManager.d.ts +29 -3
  65. package/dist/managers/toolManager.js +87 -13
  66. package/dist/prompts/autoMemory.d.ts +9 -0
  67. package/dist/prompts/autoMemory.js +30 -31
  68. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  69. package/dist/prompts/autoMemoryExtraction.js +8 -111
  70. package/dist/prompts/memoryTypes.d.ts +63 -0
  71. package/dist/prompts/memoryTypes.js +191 -0
  72. package/dist/services/GitService.d.ts +7 -0
  73. package/dist/services/GitService.js +23 -0
  74. package/dist/services/MarketplaceService.d.ts +101 -17
  75. package/dist/services/MarketplaceService.js +318 -119
  76. package/dist/services/artifactContent.d.ts +84 -0
  77. package/dist/services/artifactContent.js +204 -0
  78. package/dist/services/artifactSession.d.ts +6 -0
  79. package/dist/services/artifactSession.js +17 -0
  80. package/dist/services/autoMemoryService.js +5 -13
  81. package/dist/services/configurationService.d.ts +60 -9
  82. package/dist/services/configurationService.js +129 -54
  83. package/dist/services/contentSummarizer.d.ts +15 -0
  84. package/dist/services/contentSummarizer.js +45 -0
  85. package/dist/services/execAvailability.d.ts +9 -0
  86. package/dist/services/execAvailability.js +32 -0
  87. package/dist/services/fileWatcher.js +61 -6
  88. package/dist/services/initializationService.js +19 -15
  89. package/dist/services/interactionService.d.ts +9 -1
  90. package/dist/services/interactionService.js +28 -8
  91. package/dist/services/jsonlHandler.d.ts +84 -0
  92. package/dist/services/jsonlHandler.js +209 -14
  93. package/dist/services/memory.d.ts +3 -1
  94. package/dist/services/memory.js +13 -9
  95. package/dist/services/officialMarketplaceMirror.js +3 -2
  96. package/dist/services/pluginLoader.d.ts +12 -4
  97. package/dist/services/pluginLoader.js +38 -7
  98. package/dist/services/remoteSettingsService.js +16 -2
  99. package/dist/services/session.d.ts +74 -0
  100. package/dist/services/session.js +144 -3
  101. package/dist/services/sessionEntries.d.ts +2 -0
  102. package/dist/services/sessionEntries.js +20 -0
  103. package/dist/stdio/index.d.ts +3 -1
  104. package/dist/stdio/index.js +3 -1
  105. package/dist/stdio/notificationRouter.js +1 -0
  106. package/dist/stdio/stdioAgent.d.ts +14 -7
  107. package/dist/stdio/stdioAgent.js +19 -0
  108. package/dist/tools/artifactTool.js +406 -273
  109. package/dist/tools/bashTool.js +8 -6
  110. package/dist/tools/editTool.js +6 -3
  111. package/dist/tools/execTool.d.ts +2 -0
  112. package/dist/tools/execTool.js +165 -0
  113. package/dist/tools/grepTool.js +7 -1
  114. package/dist/tools/readTool.js +30 -2
  115. package/dist/tools/types.d.ts +34 -8
  116. package/dist/tools/webFetchTool.js +15 -166
  117. package/dist/tools/workflowTool.js +40 -8
  118. package/dist/tools/writeTool.js +6 -3
  119. package/dist/types/agent.d.ts +20 -5
  120. package/dist/types/configuration.d.ts +39 -1
  121. package/dist/types/marketplace.d.ts +40 -2
  122. package/dist/types/mcp.d.ts +39 -0
  123. package/dist/types/permissions.d.ts +22 -0
  124. package/dist/types/permissions.js +17 -0
  125. package/dist/types/plugins.d.ts +26 -2
  126. package/dist/types/skills.d.ts +15 -0
  127. package/dist/utils/constants.d.ts +10 -0
  128. package/dist/utils/constants.js +10 -0
  129. package/dist/utils/containerSetup.js +43 -0
  130. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  131. package/dist/utils/convertMessagesForAPI.js +64 -14
  132. package/dist/utils/fileChangeReminder.d.ts +20 -0
  133. package/dist/utils/fileChangeReminder.js +153 -0
  134. package/dist/utils/fileSearch.js +4 -3
  135. package/dist/utils/fileUtils.d.ts +33 -0
  136. package/dist/utils/fileUtils.js +81 -0
  137. package/dist/utils/frontmatterYaml.d.ts +33 -0
  138. package/dist/utils/frontmatterYaml.js +192 -0
  139. package/dist/utils/imageBudget.d.ts +85 -0
  140. package/dist/utils/imageBudget.js +109 -0
  141. package/dist/utils/imageDimensions.d.ts +83 -0
  142. package/dist/utils/imageDimensions.js +232 -0
  143. package/dist/utils/imageProcessor.d.ts +66 -0
  144. package/dist/utils/imageProcessor.js +84 -0
  145. package/dist/utils/imageRewrite.d.ts +29 -0
  146. package/dist/utils/imageRewrite.js +251 -0
  147. package/dist/utils/markdownParser.d.ts +5 -1
  148. package/dist/utils/markdownParser.js +9 -51
  149. package/dist/utils/mcpInstructions.d.ts +61 -0
  150. package/dist/utils/mcpInstructions.js +126 -0
  151. package/dist/utils/mcpUtils.d.ts +7 -0
  152. package/dist/utils/mcpUtils.js +11 -2
  153. package/dist/utils/memoryAge.d.ts +32 -0
  154. package/dist/utils/memoryAge.js +47 -0
  155. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  156. package/dist/utils/memoryEntrypoint.js +49 -0
  157. package/dist/utils/memoryIndex.d.ts +30 -0
  158. package/dist/utils/memoryIndex.js +76 -0
  159. package/dist/utils/messageOperations.d.ts +6 -2
  160. package/dist/utils/messageOperations.js +40 -29
  161. package/dist/utils/nestedMemory.d.ts +22 -0
  162. package/dist/utils/nestedMemory.js +61 -0
  163. package/dist/utils/npmTarball.d.ts +19 -0
  164. package/dist/utils/npmTarball.js +92 -0
  165. package/dist/utils/pluginSource.d.ts +37 -0
  166. package/dist/utils/pluginSource.js +73 -0
  167. package/dist/utils/ripgrep.d.ts +18 -4
  168. package/dist/utils/ripgrep.js +56 -4
  169. package/dist/utils/runtimeDeps.d.ts +35 -0
  170. package/dist/utils/runtimeDeps.js +426 -0
  171. package/dist/utils/skillParser.js +22 -52
  172. package/dist/utils/subagentParser.js +39 -43
  173. package/dist/utils/userSettings.d.ts +90 -0
  174. package/dist/utils/userSettings.js +291 -0
  175. package/package.json +10 -7
@@ -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"];
@@ -103,6 +103,17 @@ export declare class SkillManager extends EventEmitter {
103
103
  content: string;
104
104
  skill?: undefined;
105
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;
106
117
  /**
107
118
  * Prepare skill content with arguments but without bash execution
108
119
  */
@@ -119,6 +130,14 @@ export declare class SkillManager extends EventEmitter {
119
130
  * Register skills provided by a plugin
120
131
  */
121
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;
122
141
  /**
123
142
  * Directory roots that may hold deletable copies of a skill in the given
124
143
  * discovery scope. Personal skills are scanned from three user-level dirs
@@ -422,6 +422,19 @@ export class SkillManager extends EventEmitter {
422
422
  };
423
423
  }
424
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
+ }
425
438
  /**
426
439
  * Prepare skill content with arguments but without bash execution
427
440
  */
@@ -496,6 +509,37 @@ export class SkillManager extends EventEmitter {
496
509
  }
497
510
  logger?.debug(`Registered ${skills.length} plugin skills from ${pluginName}. Total skills: ${this.skillMetadata.size}`);
498
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
+ }
499
543
  /**
500
544
  * Directory roots that may hold deletable copies of a skill in the given
501
545
  * discovery scope. Personal skills are scanned from three user-level dirs
@@ -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
  */
@@ -9,6 +9,9 @@ export class SlashCommandManager {
9
9
  this.commands = new Map();
10
10
  this.customCommands = new Map();
11
11
  this.skillCommandIds = new Set();
12
+ /** Ids registered by plugins, so they can be dropped per-plugin and are not
13
+ * mistaken for disk-sourced custom commands. */
14
+ this.pluginCommandIds = new Set();
12
15
  this.currentCommandAbortController = null;
13
16
  this.workdir = options.workdir;
14
17
  }
@@ -233,6 +236,7 @@ export class SlashCommandManager {
233
236
  const namespacedId = `${pluginName}:${command.id}`;
234
237
  const namespacedName = `${pluginName}:${command.name}`;
235
238
  this.customCommands.set(namespacedId, command);
239
+ this.pluginCommandIds.add(namespacedId);
236
240
  // Generate description: prioritize custom description, otherwise use default description
237
241
  const description = command.description ||
238
242
  `Plugin command: ${namespacedName}${hasParameterPlaceholders(command.content) ? " (supports parameters)" : ""}`;
@@ -252,15 +256,43 @@ export class SlashCommandManager {
252
256
  }
253
257
  logger?.debug(`Registered ${commands.length} commands from plugin '${pluginName}'`);
254
258
  }
259
+ /**
260
+ * Drop every slash command contributed by a plugin, addressed by its plugin
261
+ * name. Removes the registry entry and the customCommands source entry.
262
+ * Called by the in-place plugin reload path before re-registering.
263
+ * @returns the number of commands removed
264
+ */
265
+ unregisterPluginCommands(pluginName) {
266
+ const prefix = `${pluginName}:`;
267
+ let removed = 0;
268
+ for (const commandId of Array.from(this.pluginCommandIds)) {
269
+ if (!commandId.startsWith(prefix)) {
270
+ continue;
271
+ }
272
+ this.pluginCommandIds.delete(commandId);
273
+ this.customCommands.delete(commandId);
274
+ this.unregisterCommand(commandId);
275
+ removed += 1;
276
+ }
277
+ if (removed > 0) {
278
+ logger?.debug(`Unregistered ${removed} commands from plugin '${pluginName}'`);
279
+ }
280
+ return removed;
281
+ }
255
282
  /**
256
283
  * Reload custom commands (useful for development)
257
284
  */
258
285
  reloadCustomCommands() {
259
- // Clear existing custom commands
260
- for (const commandId of this.customCommands.keys()) {
286
+ // Reload only disk-sourced custom commands. Plugin commands live in the
287
+ // same map but are owned by the plugin lifecycle, so re-reading the disk
288
+ // must not drop them.
289
+ for (const [commandId] of this.customCommands) {
290
+ if (this.pluginCommandIds.has(commandId)) {
291
+ continue;
292
+ }
261
293
  this.unregisterCommand(commandId);
294
+ this.customCommands.delete(commandId);
262
295
  }
263
- this.customCommands.clear();
264
296
  // Reload
265
297
  this.loadCustomCommands();
266
298
  }
@@ -120,6 +120,14 @@ export declare class SubagentManager {
120
120
  * Names each agent as `pluginName:agentName` to avoid collisions.
121
121
  */
122
122
  registerPluginAgents(pluginName: string, agents: SubagentConfiguration[]): void;
123
+ /**
124
+ * Drop every subagent contributed by a plugin, addressed by its plugin name.
125
+ * Running instances are deliberately left alone — an in-place reload must not
126
+ * interrupt work already in flight. Called by the plugin reload path before
127
+ * re-registering.
128
+ * @returns the number of configurations removed
129
+ */
130
+ unregisterPluginAgents(pluginName: string): number;
123
131
  /**
124
132
  * Create a new subagent instance with isolated managers
125
133
  */
@@ -183,6 +183,22 @@ export class SubagentManager {
183
183
  return a.name.localeCompare(b.name);
184
184
  });
185
185
  }
186
+ /**
187
+ * Drop every subagent contributed by a plugin, addressed by its plugin name.
188
+ * Running instances are deliberately left alone — an in-place reload must not
189
+ * interrupt work already in flight. Called by the plugin reload path before
190
+ * re-registering.
191
+ * @returns the number of configurations removed
192
+ */
193
+ unregisterPluginAgents(pluginName) {
194
+ if (this.cachedConfigurations === null) {
195
+ return 0;
196
+ }
197
+ const prefix = `${pluginName}:`;
198
+ const before = this.cachedConfigurations.length;
199
+ this.cachedConfigurations = this.cachedConfigurations.filter((config) => !config.name.startsWith(prefix));
200
+ return before - this.cachedConfigurations.length;
201
+ }
186
202
  /**
187
203
  * Create a new subagent instance with isolated managers
188
204
  */
@@ -232,6 +248,10 @@ export class SubagentManager {
232
248
  const subagentPermissionManager = new PermissionManager(subagentContainer, {
233
249
  configuredPermissionMode: parameters.permissionModeOverride ??
234
250
  parentPermissionManager?.getConfiguredPermissionMode(),
251
+ // Inherit the parent session's bypass authorization: the subagent is
252
+ // created mid-session, so re-deriving it from its own mode would
253
+ // (wrongly) deny an authorized session's plan-mode subagent.
254
+ bypassAuthorization: parentPermissionManager?.getBypassAuthorization?.(),
235
255
  allowedRules: parentPermissionManager?.getAllowedRules(),
236
256
  deniedRules: parentPermissionManager?.getDeniedRules(),
237
257
  instanceAllowedRules: parentPermissionManager?.getInstanceAllowedRules?.(),
@@ -1,4 +1,5 @@
1
1
  import type { ToolContext, ToolPlugin, ToolResult } from "../tools/types.js";
2
+ import type { RenderedCatalog } from "../exec/catalog.js";
2
3
  import { PermissionManager } from "./permissionManager.js";
3
4
  import { ChatCompletionFunctionTool } from "openai/resources.js";
4
5
  import type { PermissionMode } from "../types/index.js";
@@ -56,15 +57,21 @@ declare class ToolManager {
56
57
  /**
57
58
  * Re-evaluate feature-gated built-in tools after a live configuration
58
59
  * reload. Currently gates the Artifact tool on settings.json
59
- * `enableArtifact`. Safe to call multiple times: gated tools are removed
60
- * from the registry first, then initializeBuiltInTools() re-registers them
61
- * only if still enabled (so toggling the flag off actually unregisters).
60
+ * `enableArtifact` and the Exec tool on `enableExec`. Safe to call multiple
61
+ * times: gated tools are removed from the registry first, then
62
+ * initializeBuiltInTools() re-registers them only if still enabled (so
63
+ * toggling a flag off actually unregisters).
62
64
  */
63
65
  reloadFeatureGatedTools(): void;
64
66
  /**
65
67
  * Check if a tool should be enabled based on tools configuration and permission rules
66
68
  */
67
69
  private shouldEnableTool;
70
+ /**
71
+ * The auto-memory directory for this session, or undefined when the feature
72
+ * is off. Consumers (Read) treat "unset" as "not a memory file".
73
+ */
74
+ private resolveAutoMemoryDir;
68
75
  /**
69
76
  * Execute a tool by name with the provided arguments and context
70
77
  *
@@ -80,6 +87,25 @@ declare class ToolManager {
80
87
  */
81
88
  execute(name: string, args: Record<string, unknown>, context: ToolContext): Promise<ToolResult>;
82
89
  list(): ToolPlugin[];
90
+ /**
91
+ * Whether `Exec` is declared at all: registered and not denied. Exactly when the
92
+ * catalog channel is open — an undeclared `Exec` cannot be called, so a catalog
93
+ * for it would advertise a way in that does not exist.
94
+ */
95
+ private isExecDeclared;
96
+ /** The tools the sandbox may reach — the pool the flat declarations give up. */
97
+ private execPool;
98
+ /**
99
+ * The MCP catalog as the model is meant to see it, or `undefined` when the
100
+ * catalog channel is closed.
101
+ *
102
+ * A function of the pool alone, and computed per call: with the catalog out of
103
+ * `tools[]` the declaration no longer moves when a server comes or goes, and the
104
+ * announcement channel diffs this against the history. Caching the rendering would
105
+ * mean caching pool state, which is exactly the process-side bookkeeping the
106
+ * channel is built to avoid.
107
+ */
108
+ getExecCatalog(): RenderedCatalog | undefined;
83
109
  getToolsConfig(options?: {
84
110
  availableSubagents?: SubagentConfiguration[];
85
111
  availableSkills?: SkillMetadata[];