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
@@ -0,0 +1,110 @@
1
+ import { bashTool } from "../tools/bashTool.js";
2
+ import { BASH_TOOL_NAME } from "../constants/tools.js";
3
+ /**
4
+ * Bash mode (`!command`) execution. The command is surfaced as a user-message
5
+ * tool block (name: BASH_TOOL_NAME) — the same rendering as fork-skill / `/plan`
6
+ * outputs (⎿ prefix + markdown, result enters the model context via the
7
+ * generic `<local-command-stdout>` rule) — and executed through the shared
8
+ * bash tool engine (shell resolution, CWD tracking, timeout, output handling).
9
+ */
10
+ export class BashModeManager {
11
+ constructor(container, options) {
12
+ this.container = container;
13
+ this.isCommandRunning = false;
14
+ this.abortController = null;
15
+ this.workdir = options.workdir;
16
+ }
17
+ get messageManager() {
18
+ return this.container.get("MessageManager");
19
+ }
20
+ /**
21
+ * Merged env (OS env overlaid with this session's settings snapshot) for
22
+ * bash-mode subprocesses, so settings `env` vars reach them without
23
+ * polluting other sessions in one `wave --stdio` process.
24
+ */
25
+ get sessionEnv() {
26
+ return (this.container
27
+ .get("ConfigurationService")
28
+ ?.getMergedEnv?.() ?? process.env);
29
+ }
30
+ setCommandRunning(isRunning) {
31
+ this.isCommandRunning = isRunning;
32
+ this.onCommandRunningChange?.(isRunning);
33
+ }
34
+ async executeCommand(command) {
35
+ if (this.isCommandRunning) {
36
+ throw new Error("Command already running");
37
+ }
38
+ this.setCommandRunning(true);
39
+ try {
40
+ // User message + running tool block. The user text mirrors the command
41
+ // itself (`!ls` renders as `!ls`), and incremental toolBlockUpdated
42
+ // notifications drive the live output on all three hosts.
43
+ const messageId = this.messageManager.addUserMessage({
44
+ content: command,
45
+ });
46
+ const blockId = this.messageManager.addToolBlockToMessage(messageId, {
47
+ name: BASH_TOOL_NAME,
48
+ parameters: command,
49
+ stage: "running",
50
+ });
51
+ this.abortController = new AbortController();
52
+ const taskManager = this.container.get("TaskManager");
53
+ // No permissionManager in the context: bash mode is an explicit user
54
+ // command, it must not be blocked by the permission gate.
55
+ const result = await bashTool.execute({ command }, {
56
+ workdir: this.workdir,
57
+ taskManager,
58
+ sessionEnv: this.sessionEnv,
59
+ abortSignal: this.abortController.signal,
60
+ // Stream realtime output into the running tool block — wired like
61
+ // aiManager tool execution (aiManager.ts updateToolBlock callbacks).
62
+ onShortResultUpdate: (shortResult) => {
63
+ this.messageManager.updateToolBlock({
64
+ id: blockId,
65
+ messageId,
66
+ shortResult,
67
+ stage: "running",
68
+ name: BASH_TOOL_NAME,
69
+ });
70
+ },
71
+ onResultUpdate: (resultText) => {
72
+ this.messageManager.updateToolBlock({
73
+ id: blockId,
74
+ messageId,
75
+ result: resultText,
76
+ stage: "running",
77
+ name: BASH_TOOL_NAME,
78
+ });
79
+ },
80
+ });
81
+ const exitCode = typeof result.metadata?.exitCode === "number"
82
+ ? result.metadata.exitCode
83
+ : result.success
84
+ ? 0
85
+ : 1;
86
+ const resultText = result.success
87
+ ? result.content
88
+ : `[exit code: ${exitCode}]` +
89
+ (result.content ? `\n\n${result.content}` : "");
90
+ this.messageManager.updateToolBlock({
91
+ id: blockId,
92
+ messageId,
93
+ result: resultText,
94
+ shortResult: result.shortResult,
95
+ stage: "end",
96
+ success: result.success,
97
+ });
98
+ return exitCode;
99
+ }
100
+ finally {
101
+ this.abortController = null;
102
+ this.setCommandRunning(false);
103
+ }
104
+ }
105
+ abortCommand() {
106
+ this.abortController?.abort();
107
+ this.abortController = null;
108
+ this.isCommandRunning = false;
109
+ }
110
+ }
@@ -13,6 +13,12 @@ export declare class HookManager {
13
13
  private container;
14
14
  private configuration;
15
15
  private programmaticHooks;
16
+ /**
17
+ * Plugin hooks grouped by plugin root path. A map rather than one merged
18
+ * object because mergeHooksConfiguration only appends — a flat object can
19
+ * never be reversed, so the in-place plugin reload path could not drop
20
+ * exactly one plugin's hooks (and re-registering would double them).
21
+ */
16
22
  private pluginHooks;
17
23
  private waveConfigHooks;
18
24
  private readonly matcher;
@@ -120,6 +126,18 @@ export declare class HookManager {
120
126
  * Register hooks provided by a plugin
121
127
  */
122
128
  registerPluginHooks(pluginRoot: string, hooks: PartialHookConfiguration): void;
129
+ /**
130
+ * Drop every hook contributed by a plugin, addressed by its root path.
131
+ * Used by the in-place plugin reload path (before re-registering) and when a
132
+ * plugin is disabled or uninstalled.
133
+ * @returns true if this root had hooks registered
134
+ */
135
+ unregisterPluginHooks(pluginRoot: string): boolean;
136
+ /**
137
+ * Get hooks registered by plugins (read-only view for the settings UI).
138
+ * Plugin hooks carry pluginRoot on each command and are read-only in the UI.
139
+ */
140
+ getPluginHooks(): PartialHookConfiguration;
123
141
  /**
124
142
  * Execute SessionStart hooks during initialization.
125
143
  * Collects additionalContext and initialUserMessage from hook stdout.
@@ -13,7 +13,13 @@ export class HookManager {
13
13
  constructor(container, workdir, matcher = new HookMatcher()) {
14
14
  this.container = container;
15
15
  this.programmaticHooks = {};
16
- this.pluginHooks = {};
16
+ /**
17
+ * Plugin hooks grouped by plugin root path. A map rather than one merged
18
+ * object because mergeHooksConfiguration only appends — a flat object can
19
+ * never be reversed, so the in-place plugin reload path could not drop
20
+ * exactly one plugin's hooks (and re-registering would double them).
21
+ */
22
+ this.pluginHooks = new Map();
17
23
  this.waveConfigHooks = {};
18
24
  this.workdir = workdir;
19
25
  this.matcher = matcher;
@@ -78,7 +84,9 @@ export class HookManager {
78
84
  rebuildConfiguration() {
79
85
  const rebuilt = {};
80
86
  this.mergeHooksConfiguration(rebuilt, this.programmaticHooks);
81
- this.mergeHooksConfiguration(rebuilt, this.pluginHooks);
87
+ for (const hooks of this.pluginHooks.values()) {
88
+ this.mergeHooksConfiguration(rebuilt, hooks);
89
+ }
82
90
  this.mergeHooksConfiguration(rebuilt, this.waveConfigHooks);
83
91
  this.configuration = Object.keys(rebuilt).length > 0 ? rebuilt : undefined;
84
92
  }
@@ -667,9 +675,35 @@ export class HookManager {
667
675
  hooks: config.hooks.map((cmd) => ({ ...cmd, pluginRoot })),
668
676
  }));
669
677
  }
670
- this.mergeHooksConfiguration(this.pluginHooks, stampedHooks);
678
+ // Replace (not append) the previous registration for this root, so
679
+ // re-registering the same plugin never doubles its hooks.
680
+ this.pluginHooks.set(pluginRoot, stampedHooks);
671
681
  this.rebuildConfiguration();
672
682
  }
683
+ /**
684
+ * Drop every hook contributed by a plugin, addressed by its root path.
685
+ * Used by the in-place plugin reload path (before re-registering) and when a
686
+ * plugin is disabled or uninstalled.
687
+ * @returns true if this root had hooks registered
688
+ */
689
+ unregisterPluginHooks(pluginRoot) {
690
+ const removed = this.pluginHooks.delete(pluginRoot);
691
+ if (removed) {
692
+ this.rebuildConfiguration();
693
+ }
694
+ return removed;
695
+ }
696
+ /**
697
+ * Get hooks registered by plugins (read-only view for the settings UI).
698
+ * Plugin hooks carry pluginRoot on each command and are read-only in the UI.
699
+ */
700
+ getPluginHooks() {
701
+ const merged = {};
702
+ for (const hooks of this.pluginHooks.values()) {
703
+ this.mergeHooksConfiguration(merged, hooks);
704
+ }
705
+ return merged;
706
+ }
673
707
  /**
674
708
  * Execute SessionStart hooks during initialization.
675
709
  * Collects additionalContext and initialUserMessage from hook stdout.
@@ -6,6 +6,7 @@
6
6
  * - Configuration file watching for settings.json files
7
7
  * - Coordination between file watchers and configuration updates
8
8
  */
9
+ import type { TurnConfigurationSnapshot } from "../services/configurationService.js";
9
10
  import { Container } from "../utils/container.js";
10
11
  import type { WaveConfiguration } from "../types/configuration.js";
11
12
  export interface LiveConfigManagerOptions {
@@ -19,6 +20,8 @@ export declare class LiveConfigManager {
19
20
  private isInitialized;
20
21
  private currentConfiguration;
21
22
  private lastValidConfiguration;
23
+ private turnSnapshot;
24
+ private turnDepth;
22
25
  private fileWatcher;
23
26
  private userConfigPaths?;
24
27
  private projectConfigPaths?;
@@ -28,6 +31,15 @@ export declare class LiveConfigManager {
28
31
  private get hookManager();
29
32
  private get permissionManager();
30
33
  private get configurationService();
34
+ private get memoryService();
35
+ /**
36
+ * Keep the auto-memory system safe zone in sync with the live auto-memory
37
+ * toggle (core/agent-config.md scenario 6). Turning auto-memory off must
38
+ * revoke the privilege its directory got at construction time, and turning it
39
+ * back on must restore it — both without rebuilding the session. Idempotent:
40
+ * the underlying add/remove are dedup'ed by path.
41
+ */
42
+ private syncAutoMemorySafeZone;
31
43
  /**
32
44
  * Initialize configuration watching
33
45
  * Maps to FR-004: System MUST watch settings.json files
@@ -46,6 +58,19 @@ export declare class LiveConfigManager {
46
58
  * Shutdown configuration management and cleanup resources
47
59
  */
48
60
  shutdown(): Promise<void>;
61
+ /**
62
+ * Turn boundary: pin the settings.json-derived configuration for the duration
63
+ * of a turn (called by AIManager). A live reload may land mid-turn and the
64
+ * turn must not see it (core/agent-config.md scenario 5) — the change is
65
+ * picked up by the snapshot taken when the next turn starts. Permission rules,
66
+ * hooks and env stay live: those are enforcement state and follow the file
67
+ * immediately.
68
+ */
69
+ onTurnStart(): void;
70
+ /** Turn boundary: drop the snapshot so the next turn reads live values. */
71
+ onTurnEnd(): void;
72
+ /** Configuration snapshot of the running turn, or null outside a turn. */
73
+ getTurnSnapshot(): TurnConfigurationSnapshot | null;
49
74
  /**
50
75
  * Reload configuration from files
51
76
  * Maps to FR-008: Continue with previous valid configuration on errors
@@ -77,6 +102,14 @@ export declare class LiveConfigManager {
77
102
  private setupFileWatcherEvents;
78
103
  private handleFileChange;
79
104
  private detectChanges;
105
+ /**
106
+ * Chokidar only starts watching a non-existent path when its parent directory
107
+ * exists at watch time (a path whose parents are all missing is never
108
+ * picked up, not even after they appear). Walk up to the deepest node whose
109
+ * parent exists and watch that instead — events for its descendants still
110
+ * reach this path's callback (FileWatcherService matches by path prefix).
111
+ */
112
+ private resolveWatchTarget;
80
113
  /**
81
114
  * Get configuration file paths for user and project settings
82
115
  * Returns paths in priority order (local.json first, then .json)
@@ -7,7 +7,9 @@
7
7
  * - Coordination between file watchers and configuration updates
8
8
  */
9
9
  import { existsSync } from "fs";
10
+ import { dirname } from "path";
10
11
  import { FileWatcherService, } from "../services/fileWatcher.js";
12
+ import { USER_MEMORY_FILE } from "../utils/constants.js";
11
13
  import { isValidHookEvent } from "../types/hooks.js";
12
14
  import { logger } from "../utils/globalLogger.js";
13
15
  export class LiveConfigManager {
@@ -18,6 +20,13 @@ export class LiveConfigManager {
18
20
  // Configuration state
19
21
  this.currentConfiguration = null;
20
22
  this.lastValidConfiguration = null;
23
+ // Turn-scoped snapshot of the settings.json-derived configuration the running
24
+ // turn reads from. Captured at turn start, dropped at turn end, so a live
25
+ // reload landing mid-turn takes effect at the next turn instead of shifting
26
+ // values under the running one (core/agent-config.md scenario 5). Nested turns
27
+ // (a subagent turn inside its parent's) share the outer snapshot.
28
+ this.turnSnapshot = null;
29
+ this.turnDepth = 0;
21
30
  this.isWatching = false;
22
31
  this.reloadInProgress = false;
23
32
  this.workdir = options.workdir;
@@ -33,6 +42,36 @@ export class LiveConfigManager {
33
42
  get configurationService() {
34
43
  return this.container.get("ConfigurationService");
35
44
  }
45
+ get memoryService() {
46
+ return this.container.get("MemoryService");
47
+ }
48
+ /**
49
+ * Keep the auto-memory system safe zone in sync with the live auto-memory
50
+ * toggle (core/agent-config.md scenario 6). Turning auto-memory off must
51
+ * revoke the privilege its directory got at construction time, and turning it
52
+ * back on must restore it — both without rebuilding the session. Idempotent:
53
+ * the underlying add/remove are dedup'ed by path.
54
+ */
55
+ syncAutoMemorySafeZone() {
56
+ const permissionManager = this.permissionManager;
57
+ const memoryService = this.memoryService;
58
+ if (!permissionManager || !memoryService) {
59
+ return;
60
+ }
61
+ const directories = [
62
+ memoryService.getAutoMemoryDirectory(this.workdir),
63
+ USER_MEMORY_FILE,
64
+ ];
65
+ const enabled = this.configurationService.resolveAutoMemoryEnabledNow();
66
+ for (const directory of directories) {
67
+ if (enabled) {
68
+ permissionManager.addSystemAdditionalDirectory(directory);
69
+ }
70
+ else {
71
+ permissionManager.removeSystemAdditionalDirectory(directory);
72
+ }
73
+ }
74
+ }
36
75
  /**
37
76
  * Initialize configuration watching
38
77
  * Maps to FR-004: System MUST watch settings.json files
@@ -44,18 +83,18 @@ export class LiveConfigManager {
44
83
  this.projectConfigPaths = projectPaths;
45
84
  // Load initial configuration
46
85
  await this.reloadConfiguration();
47
- // Start watching user configs that exist
86
+ // Watch every configuration path, including ones that don't exist yet:
87
+ // chokidar delivers `add` once a missing path appears, but only if its
88
+ // parent chain exists when watching starts. Skipping absent paths (the
89
+ // pre-2026-09-10 behavior) silently dropped the very first save of a
90
+ // user-level settings.json created after session start
91
+ // (core/agent-config.md scenario 7).
48
92
  for (const userPath of userPaths) {
49
- if (existsSync(userPath)) {
50
- await this.fileWatcher.watchFile(userPath, (event) => this.handleFileChange(event, "user"));
51
- }
93
+ await this.fileWatcher.watchFile(this.resolveWatchTarget(userPath), (event) => this.handleFileChange(event, "user"));
52
94
  }
53
- // Start watching local configs that exist
54
95
  if (projectPaths) {
55
96
  for (const projectPath of projectPaths) {
56
- if (existsSync(projectPath)) {
57
- await this.fileWatcher.watchFile(projectPath, (event) => this.handleFileChange(event, "project"));
58
- }
97
+ await this.fileWatcher.watchFile(this.resolveWatchTarget(projectPath), (event) => this.handleFileChange(event, "project"));
59
98
  }
60
99
  }
61
100
  this.isWatching = true;
@@ -112,6 +151,41 @@ export class LiveConfigManager {
112
151
  throw error;
113
152
  }
114
153
  }
154
+ /**
155
+ * Turn boundary: pin the settings.json-derived configuration for the duration
156
+ * of a turn (called by AIManager). A live reload may land mid-turn and the
157
+ * turn must not see it (core/agent-config.md scenario 5) — the change is
158
+ * picked up by the snapshot taken when the next turn starts. Permission rules,
159
+ * hooks and env stay live: those are enforcement state and follow the file
160
+ * immediately.
161
+ */
162
+ onTurnStart() {
163
+ if (this.turnDepth === 0) {
164
+ const configuration = this.getCurrentConfiguration();
165
+ this.turnSnapshot = {
166
+ configuration: configuration ? structuredClone(configuration) : null,
167
+ // `env` on the merged configuration is exactly what was published as the
168
+ // session env snapshot when it was loaded (both come from the same
169
+ // merge), so the snapshot carries both without querying the service.
170
+ env: { ...(configuration?.env ?? {}) },
171
+ };
172
+ }
173
+ this.turnDepth++;
174
+ }
175
+ /** Turn boundary: drop the snapshot so the next turn reads live values. */
176
+ onTurnEnd() {
177
+ if (this.turnDepth === 0) {
178
+ return;
179
+ }
180
+ this.turnDepth--;
181
+ if (this.turnDepth === 0) {
182
+ this.turnSnapshot = null;
183
+ }
184
+ }
185
+ /** Configuration snapshot of the running turn, or null outside a turn. */
186
+ getTurnSnapshot() {
187
+ return this.turnDepth > 0 ? this.turnSnapshot : null;
188
+ }
115
189
  /**
116
190
  * Reload configuration from files
117
191
  * Maps to FR-008: Continue with previous valid configuration on errors
@@ -172,15 +246,16 @@ export class LiveConfigManager {
172
246
  }
173
247
  // Update permission manager if available
174
248
  if (this.permissionManager) {
175
- this.permissionManager.updateConfiguredPermissionMode(this.currentConfiguration.permissions?.permissionMode);
249
+ this.permissionManager.updateConfiguredPermissionMode(this.currentConfiguration.permissions?.defaultMode);
176
250
  this.permissionManager.updateAllowedRules(this.currentConfiguration.permissions?.allow || []);
177
251
  this.permissionManager.updateDeniedRules(this.currentConfiguration.permissions?.deny || []);
178
252
  this.permissionManager.updateAdditionalDirectories(this.currentConfiguration.permissions?.additionalDirectories || []);
253
+ this.syncAutoMemorySafeZone();
179
254
  }
180
255
  // Trigger reload callback. Awaited so fire-and-forget work spawned by the
181
256
  // callback (e.g. skill rediscovery) completes before the reload resolves —
182
- // otherwise Agent.create() can return with a cleared skill map (see race
183
- // where refreshSkills() clears skillMetadata before async discoverSkills).
257
+ // otherwise Agent.create() can return while a skill rescan is still
258
+ // running (refreshSkills swaps caches atomically only after the scan).
184
259
  await this.options.onReload?.(this.currentConfiguration);
185
260
  return this.currentConfiguration;
186
261
  }
@@ -304,6 +379,26 @@ export class LiveConfigManager {
304
379
  }
305
380
  return { added, modified, removed };
306
381
  }
382
+ /**
383
+ * Chokidar only starts watching a non-existent path when its parent directory
384
+ * exists at watch time (a path whose parents are all missing is never
385
+ * picked up, not even after they appear). Walk up to the deepest node whose
386
+ * parent exists and watch that instead — events for its descendants still
387
+ * reach this path's callback (FileWatcherService matches by path prefix).
388
+ */
389
+ resolveWatchTarget(configPath) {
390
+ let candidate = configPath;
391
+ while (!existsSync(dirname(candidate))) {
392
+ const parent = dirname(candidate);
393
+ if (parent === candidate) {
394
+ // Reached the filesystem root without an existing parent: nothing to
395
+ // watch (unreachable for absolute paths).
396
+ return configPath;
397
+ }
398
+ candidate = parent;
399
+ }
400
+ return candidate;
401
+ }
307
402
  /**
308
403
  * Get configuration file paths for user and project settings
309
404
  * Returns paths in priority order (local.json first, then .json)
@@ -21,6 +21,14 @@ export declare class LspManager implements ILspManager {
21
21
  constructor(container: Container);
22
22
  initialize(workdir: string): Promise<void>;
23
23
  registerServer(language: string, config: LspServerConfig): void;
24
+ /**
25
+ * Drop every LSP server contributed by a plugin (matched on pluginRoot),
26
+ * stopping any process already started for those languages so the next
27
+ * request starts the reloaded command instead of reusing the stale one.
28
+ * Called by the in-place plugin reload path before re-registering.
29
+ * @returns the number of servers removed
30
+ */
31
+ unregisterServersForPlugin(pluginRoot: string): Promise<number>;
24
32
  private loadConfig;
25
33
  getProcessForFile(filePath: string): Promise<LspProcess | null>;
26
34
  private startServer;
@@ -36,6 +44,7 @@ export declare class LspManager implements ILspManager {
36
44
  success: boolean;
37
45
  content: string;
38
46
  }>;
47
+ private stopProcess;
39
48
  cleanup(): Promise<void>;
40
49
  }
41
50
  export {};
@@ -17,6 +17,32 @@ export class LspManager {
17
17
  this.config[language] = config;
18
18
  logger?.debug(`Registered LSP server for ${language}`);
19
19
  }
20
+ /**
21
+ * Drop every LSP server contributed by a plugin (matched on pluginRoot),
22
+ * stopping any process already started for those languages so the next
23
+ * request starts the reloaded command instead of reusing the stale one.
24
+ * Called by the in-place plugin reload path before re-registering.
25
+ * @returns the number of servers removed
26
+ */
27
+ async unregisterServersForPlugin(pluginRoot) {
28
+ let removed = 0;
29
+ for (const [language, config] of Object.entries(this.config)) {
30
+ if (config.pluginRoot !== pluginRoot) {
31
+ continue;
32
+ }
33
+ delete this.config[language];
34
+ const lspProc = this.processes.get(language);
35
+ if (lspProc) {
36
+ this.processes.delete(language);
37
+ await this.stopProcess(language, lspProc);
38
+ }
39
+ removed += 1;
40
+ }
41
+ if (removed > 0) {
42
+ logger?.debug(`Unregistered ${removed} LSP servers for plugin root ${pluginRoot}`);
43
+ }
44
+ return removed;
45
+ }
20
46
  async loadConfig() {
21
47
  const lspJsonPath = join(this.workdir, ".lsp.json");
22
48
  try {
@@ -326,27 +352,30 @@ export class LspManager {
326
352
  return { success: false, content: `LSP error: ${JSON.stringify(error)}` };
327
353
  }
328
354
  }
329
- async cleanup() {
330
- for (const [language, lspProc] of this.processes.entries()) {
331
- try {
332
- // Try graceful shutdown
333
- const timeout = lspProc.config.shutdownTimeout || 2000;
334
- await this.sendRequest(lspProc, "shutdown", {}, timeout);
335
- await this.sendNotification(lspProc, "exit", {});
336
- // Give it a moment to exit
337
- if (timeout > 100) {
338
- await new Promise((resolve) => setTimeout(resolve, 100));
339
- }
340
- }
341
- catch (error) {
342
- logger?.debug(`Failed to gracefully shutdown LSP for ${language}: ${error}`);
355
+ async stopProcess(language, lspProc) {
356
+ try {
357
+ // Try graceful shutdown
358
+ const timeout = lspProc.config.shutdownTimeout || 2000;
359
+ await this.sendRequest(lspProc, "shutdown", {}, timeout);
360
+ await this.sendNotification(lspProc, "exit", {});
361
+ // Give it a moment to exit
362
+ if (timeout > 100) {
363
+ await new Promise((resolve) => setTimeout(resolve, 100));
343
364
  }
344
- finally {
345
- if (!lspProc.process.killed) {
346
- lspProc.process.kill();
347
- }
365
+ }
366
+ catch (error) {
367
+ logger?.debug(`Failed to gracefully shutdown LSP for ${language}: ${error}`);
368
+ }
369
+ finally {
370
+ if (!lspProc.process.killed) {
371
+ lspProc.process.kill();
348
372
  }
349
373
  }
374
+ }
375
+ async cleanup() {
376
+ for (const [language, lspProc] of this.processes.entries()) {
377
+ await this.stopProcess(language, lspProc);
378
+ }
350
379
  this.processes.clear();
351
380
  }
352
381
  }
@@ -1,7 +1,7 @@
1
1
  import { ChatCompletionFunctionTool } from "openai/resources.js";
2
2
  import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
3
3
  import { Container } from "../utils/container.js";
4
- import type { Logger, McpServerConfig, McpConfig, McpTool, McpServerStatus } from "../types/index.js";
4
+ import type { Logger, McpServerConfig, McpConfig, McpTool, McpToolCallResult, McpServerStatus } from "../types/index.js";
5
5
  export interface McpManagerCallbacks {
6
6
  onMcpServersChange?: (servers: McpServerStatus[]) => void;
7
7
  }
@@ -32,11 +32,14 @@ export declare class McpManager {
32
32
  private servers;
33
33
  private connections;
34
34
  private configPath;
35
+ private userConfigPath;
35
36
  private workdir;
36
37
  private callbacks;
37
38
  private mcpServers;
38
39
  private reconnectTimers;
39
40
  private reconnectAttempts;
41
+ /** Explicit disconnectServer teardowns currently in flight */
42
+ private disconnecting;
40
43
  constructor(container: Container, options?: McpManagerOptions);
41
44
  /**
42
45
  * Merged env for this session: OS env overlaid with the per-session settings
@@ -50,13 +53,61 @@ export declare class McpManager {
50
53
  initialize(workdir: string, autoConnect?: boolean): Promise<void>;
51
54
  ensureConfigLoaded(): Promise<McpConfig | null>;
52
55
  loadConfig(): Promise<McpConfig | null>;
56
+ /**
57
+ * Read a raw MCP config file, tolerating ENOENT and corrupt JSON.
58
+ */
59
+ private readConfigFile;
60
+ /**
61
+ * The user-level MCP config path (~/.wave/mcp.json).
62
+ */
63
+ getUserConfigPath(): string;
64
+ /**
65
+ * The project-level MCP config path (<workdir>/.mcp.json).
66
+ */
67
+ getProjectConfigPath(): string;
53
68
  saveConfig(config: McpConfig): Promise<boolean>;
54
69
  getConfig(): McpConfig | null;
55
70
  getAllServers(): McpServerStatus[];
56
71
  getServer(name: string): McpServerStatus | undefined;
72
+ /**
73
+ * The usage notes each usable server described about itself (`initialize`'s
74
+ * `instructions`), for the announcement appended to the conversation.
75
+ *
76
+ * "Usable" is the same predicate `getAllConnectedTools` uses — connected, or
77
+ * reconnecting with its last-known snapshot retained — so a server's prose is
78
+ * visible exactly while its tools are. A server that dropped, failed to connect
79
+ * or went to `error` keeps its snapshot but stops being reported here, which is
80
+ * what keeps a dead server from still talking to the model.
81
+ *
82
+ * `isDenied` receives flattened tool names (`mcp__server__tool`, the shape
83
+ * permission rules match on); a server whose tools are *all* excluded by rules is
84
+ * dropped whole, so prose cannot reach the context after the user ruled the
85
+ * server out. A server exposing no tools at all is still reported: nothing was
86
+ * excluded, it is simply a server that brings context rather than tools.
87
+ */
88
+ getServerInstructions(isDenied?: (toolName: string) => boolean): Array<{
89
+ name: string;
90
+ instructions: string;
91
+ }>;
57
92
  updateServerStatus(name: string, updates: Partial<McpServerStatus>): void;
58
93
  addServer(name: string, config: McpServerConfig): boolean;
59
94
  removeServer(name: string): boolean;
95
+ /**
96
+ * Drop every MCP server contributed by a plugin (matched on pluginRoot),
97
+ * disconnecting each one. Needed instead of re-adding under the same name
98
+ * because addServer refuses a name that is already registered.
99
+ * Called by the in-place plugin reload path before re-registering.
100
+ * @returns the number of servers removed
101
+ */
102
+ removeServersForPlugin(pluginRoot: string): number;
103
+ /**
104
+ * Remove a server from a persisted config file (user or project scope),
105
+ * then disconnect and drop it from the in-memory registry.
106
+ * @param scope - "user" removes from ~/.wave/mcp.json; "project" removes from <workdir>/.mcp.json
107
+ * @param name - The server name
108
+ * @returns true if the server was found and removed
109
+ */
110
+ removeServerFromConfig(scope: "user" | "project", name: string): Promise<boolean>;
60
111
  connectServer(name: string): Promise<boolean>;
61
112
  /**
62
113
  * Schedule auto-reconnect with exponential backoff.
@@ -71,15 +122,7 @@ export declare class McpManager {
71
122
  private cancelReconnect;
72
123
  disconnectServer(name: string): Promise<boolean>;
73
124
  getAllConnectedTools(): McpTool[];
74
- executeMcpTool(toolName: string, args: Record<string, unknown>, context?: ToolContext): Promise<{
75
- success: boolean;
76
- content: string;
77
- serverName?: string;
78
- images?: Array<{
79
- data: string;
80
- mediaType?: string;
81
- }>;
82
- }>;
125
+ executeMcpTool(toolName: string, args: Record<string, unknown>, context?: ToolContext): Promise<McpToolCallResult>;
83
126
  private executeToolOnConnection;
84
127
  cleanup(): Promise<void>;
85
128
  /**
@@ -90,6 +133,21 @@ export declare class McpManager {
90
133
  * Get all currently available MCP tools as OpenAI function tools
91
134
  */
92
135
  getMcpToolsConfig(): ChatCompletionFunctionTool[];
136
+ /**
137
+ * Declared output schema per flattened tool name, for the Exec catalog's return
138
+ * types.
139
+ *
140
+ * A separate accessor because `getMcpToolsConfig()` cannot carry it: an OpenAI
141
+ * function declaration has room for `parameters` and nothing else, so the schema
142
+ * a server declared for its *output* has no field to travel in. Correlated with
143
+ * the same `findToolServer` lookup `getMcpToolPlugins` uses and keyed with the
144
+ * same `mcpToolFlatName`, so the two agree on which server owns a tool.
145
+ *
146
+ * Servers that declare no output schema are simply absent, which renders as
147
+ * `Promise<unknown>`: honest about the call returning *something* of unspecified
148
+ * shape.
149
+ */
150
+ getMcpToolOutputSchemas(): Map<string, Record<string, unknown>>;
93
151
  /**
94
152
  * Execute an MCP tool by name (registry version)
95
153
  */