wave-agent-sdk 1.1.4 → 1.2.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 (89) hide show
  1. package/dist/agent.d.ts +72 -24
  2. package/dist/agent.js +123 -31
  3. package/dist/builtin/plugins.js +17 -2
  4. package/dist/builtin/skills/settings.js +6 -8
  5. package/dist/core/plugin.d.ts +4 -0
  6. package/dist/core/plugin.js +7 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +2 -1
  9. package/dist/managers/aiManager.d.ts +0 -24
  10. package/dist/managers/aiManager.js +23 -181
  11. package/dist/managers/bashModeManager.d.ts +33 -0
  12. package/dist/managers/bashModeManager.js +110 -0
  13. package/dist/managers/hookManager.d.ts +5 -0
  14. package/dist/managers/hookManager.js +7 -0
  15. package/dist/managers/liveConfigManager.js +3 -3
  16. package/dist/managers/mcpManager.d.ts +23 -0
  17. package/dist/managers/mcpManager.js +162 -14
  18. package/dist/managers/messageManager.d.ts +12 -13
  19. package/dist/managers/messageManager.js +63 -60
  20. package/dist/managers/permissionManager.d.ts +29 -0
  21. package/dist/managers/permissionManager.js +158 -70
  22. package/dist/managers/planManager.d.ts +9 -0
  23. package/dist/managers/planManager.js +19 -1
  24. package/dist/managers/skillManager.d.ts +31 -0
  25. package/dist/managers/skillManager.js +122 -12
  26. package/dist/managers/slashCommandManager.js +9 -28
  27. package/dist/managers/subagentManager.d.ts +7 -0
  28. package/dist/managers/subagentManager.js +61 -9
  29. package/dist/managers/workflowManager.js +6 -0
  30. package/dist/prompts/index.d.ts +0 -1
  31. package/dist/prompts/index.js +0 -4
  32. package/dist/services/MarketplaceService.js +36 -14
  33. package/dist/services/configurationService.d.ts +34 -2
  34. package/dist/services/configurationService.js +123 -16
  35. package/dist/services/initializationService.js +2 -2
  36. package/dist/services/jsonlHandler.d.ts +14 -0
  37. package/dist/services/jsonlHandler.js +44 -1
  38. package/dist/services/memory.d.ts +14 -0
  39. package/dist/services/memory.js +33 -0
  40. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  41. package/dist/services/officialMarketplaceMirror.js +289 -0
  42. package/dist/services/remoteSettingsService.js +4 -4
  43. package/dist/services/session.js +30 -13
  44. package/dist/services/worktreeHooks.js +6 -1
  45. package/dist/stdio/index.d.ts +10 -0
  46. package/dist/stdio/index.js +10 -0
  47. package/dist/stdio/notificationRouter.d.ts +38 -0
  48. package/dist/stdio/notificationRouter.js +96 -0
  49. package/dist/stdio/rpcClient.d.ts +18 -0
  50. package/dist/stdio/rpcClient.js +10 -0
  51. package/dist/stdio/stdioAgent.d.ts +222 -0
  52. package/dist/stdio/stdioAgent.js +341 -0
  53. package/dist/tools/bashTool.js +2 -0
  54. package/dist/tools/exitPlanMode.js +10 -2
  55. package/dist/types/agent.d.ts +8 -0
  56. package/dist/types/commands.d.ts +7 -0
  57. package/dist/types/configuration.d.ts +6 -1
  58. package/dist/types/hooks.d.ts +1 -0
  59. package/dist/types/hooks.js +19 -0
  60. package/dist/types/mcp.d.ts +3 -0
  61. package/dist/types/messaging.d.ts +1 -8
  62. package/dist/types/skills.d.ts +11 -0
  63. package/dist/utils/bashParser.d.ts +17 -0
  64. package/dist/utils/bashParser.js +72 -0
  65. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  66. package/dist/utils/bashStructure/bashLexer.js +676 -0
  67. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  68. package/dist/utils/bashStructure/bashParser.js +606 -0
  69. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  70. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  71. package/dist/utils/bashStructure/index.d.ts +26 -0
  72. package/dist/utils/bashStructure/index.js +27 -0
  73. package/dist/utils/bashStructure/types.d.ts +62 -0
  74. package/dist/utils/bashStructure/types.js +47 -0
  75. package/dist/utils/container.d.ts +6 -0
  76. package/dist/utils/container.js +9 -0
  77. package/dist/utils/containerSetup.d.ts +11 -1
  78. package/dist/utils/containerSetup.js +26 -11
  79. package/dist/utils/fileUtils.d.ts +11 -0
  80. package/dist/utils/fileUtils.js +37 -0
  81. package/dist/utils/messageOperations.d.ts +0 -18
  82. package/dist/utils/messageOperations.js +0 -62
  83. package/dist/utils/subagentParser.js +9 -2
  84. package/dist/utils/tokenCalculation.js +0 -8
  85. package/dist/utils/worktreeUtils.d.ts +2 -1
  86. package/dist/utils/worktreeUtils.js +64 -34
  87. package/package.json +6 -1
  88. package/dist/managers/bangManager.d.ts +0 -26
  89. package/dist/managers/bangManager.js +0 -78
@@ -2,18 +2,15 @@ import * as aiService from "../services/aiService.js";
2
2
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
3
3
  import { supportsVision } from "../utils/modelCapabilities.js";
4
4
  import { persistToolImages } from "../utils/toolImagePersistence.js";
5
- import { parseTaskNotificationXml, taskNotificationToXml, } from "../utils/notificationXml.js";
5
+ import { parseTaskNotificationXml } from "../utils/notificationXml.js";
6
6
  import { estimateContextTokens } from "../utils/tokenCalculation.js";
7
7
  import { estimateTokens } from "../utils/tokenEstimate.js";
8
8
  import { getTaskReminderTurnCounts, maybeInjectTaskReminder, TASK_REMINDER_CONFIG, } from "../utils/taskReminder.js";
9
- import { createWriteStream, existsSync } from "node:fs";
10
- import * as os from "node:os";
9
+ import { existsSync } from "node:fs";
11
10
  import * as path from "node:path";
12
11
  import { buildSystemPrompt, formatCompactSummary, getCompactPrompt, } from "../prompts/index.js";
13
12
  import { buildPlanModeReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, wrapInSystemReminder, } from "../prompts/planModeReminders.js";
14
13
  import { recoverTruncatedJson } from "../utils/stringUtils.js";
15
- import { AGENT_TOOL_NAME, TASK_CREATE_TOOL_NAME, TASK_GET_TOOL_NAME, TASK_LIST_TOOL_NAME, TASK_UPDATE_TOOL_NAME, } from "../constants/tools.js";
16
- import { addConsolidatedAbortListener } from "../utils/abortUtils.js";
17
14
  import { logger } from "../utils/globalLogger.js";
18
15
  import { startInteractionSpan, endInteractionSpan, startLLMRequestSpan, endLLMRequestSpan, resetTracingState, } from "../telemetry/sessionTracing.js";
19
16
  import { logOTelEvent } from "../telemetry/events.js";
@@ -729,182 +726,6 @@ export class AIManager {
729
726
  }
730
727
  return result;
731
728
  }
732
- /**
733
- * Fork subagent ("/subtask"): spawn a background agent that inherits the
734
- * parent's full conversation context — the same system prompt, tools, model,
735
- * and message prefix as the main loop — so the forked request prefix matches
736
- * exactly and the prompt cache is reused. The fork runs to completion in the
737
- * background via the shared BackgroundTaskManager (visible in /tasks, log at
738
- * `os.tmpdir()/wave-subagent-<taskId>.log`); its final response is enqueued
739
- * as a task notification carrying the `<result>` tag, so the main agent
740
- * picks it up on the next turn and can act on it.
741
- *
742
- * Recursion is prevented by denying the Agent tool and the Task management
743
- * tools (the fork cannot spawn further subagents or mutate the shared task
744
- * list), and by honoring the parent's denied rules for everything else.
745
- * Gate-approved tools execute locally in the stripped fork context (no
746
- * permission prompts), mirroring the other fork paths.
747
- *
748
- * Returns the background task ID (empty string when no BackgroundTaskManager
749
- * is registered, in which case the fork still runs but without task
750
- * bookkeeping or notification).
751
- */
752
- async runForkSubagent(prompt, options, abortSignal) {
753
- const modelConfig = this.getModelConfig();
754
- // Snapshot the conversation synchronously: the /subtask handler records
755
- // the command echo in the transcript after this call, so the fork never
756
- // sees its own trigger message duplicated.
757
- const historyMessages = convertMessagesForAPI(this.messageManager.getMessages(), { supportsVision: supportsVision(modelConfig.capabilities) });
758
- const backgroundTaskManager = this.backgroundTaskManager;
759
- const permissionManager = this.permissionManager;
760
- const messageQueue = this.container.has("MessageQueue")
761
- ? this.container.get("MessageQueue")
762
- : undefined;
763
- // Combine the caller's signal with an internal one so a task kill
764
- // (BackgroundTaskManager.stopTask / cleanup) aborts the in-flight fork.
765
- const abortController = new AbortController();
766
- let abortCleanup;
767
- if (abortSignal) {
768
- abortCleanup = addConsolidatedAbortListener(abortSignal, [
769
- () => abortController.abort(),
770
- ]);
771
- }
772
- // Recursion prevention: deny the Agent tool and Task tools, then honor the
773
- // parent's denied rules for everything else.
774
- const deniedTools = new Set([
775
- AGENT_TOOL_NAME,
776
- TASK_CREATE_TOOL_NAME,
777
- TASK_GET_TOOL_NAME,
778
- TASK_UPDATE_TOOL_NAME,
779
- TASK_LIST_TOOL_NAME,
780
- ]);
781
- const canUseTool = (name) => {
782
- if (deniedTools.has(name))
783
- return false;
784
- if (permissionManager?.isToolDenied(name))
785
- return false;
786
- return true;
787
- };
788
- const deniedToolMessage = "The Agent tool and Task tools are not available in a fork subagent.";
789
- // Background task bookkeeping mirrors subagentManager.executeAgent.
790
- let taskId = "";
791
- let logStream;
792
- if (backgroundTaskManager) {
793
- taskId = backgroundTaskManager.generateId();
794
- const startTime = Date.now();
795
- const logPath = path.join(os.tmpdir(), `wave-subagent-${taskId}.log`);
796
- logStream = createWriteStream(logPath, { flags: "a" });
797
- backgroundTaskManager.addTask({
798
- id: taskId,
799
- type: "subagent",
800
- status: "running",
801
- startTime,
802
- description: truncateWithMarker(options.description, 1000),
803
- stdout: "",
804
- stderr: "",
805
- outputPath: logPath,
806
- onStop: () => {
807
- logStream?.destroy();
808
- logStream = undefined;
809
- abortController.abort();
810
- },
811
- });
812
- logStream.write(`[${new Date().toISOString()}] Fork subagent started: ${options.description}\n`);
813
- }
814
- const finish = async (status, content) => {
815
- if (!backgroundTaskManager || !taskId)
816
- return;
817
- const task = backgroundTaskManager.getTask(taskId);
818
- if (!task)
819
- return;
820
- const wasAlreadyKilled = task.status === "killed";
821
- const endTime = Date.now();
822
- if (status === "completed") {
823
- task.status = "completed";
824
- task.stdout = content;
825
- logStream?.write(`[${new Date().toISOString()}] Final response:\n${content}\n`);
826
- }
827
- else {
828
- task.status = "failed";
829
- task.stderr = content;
830
- logStream?.write(`[${new Date().toISOString()}] Fork subagent failed: ${content}\n`);
831
- }
832
- task.endTime = endTime;
833
- if (task.startTime)
834
- task.runtime = endTime - task.startTime;
835
- logStream?.end();
836
- logStream = undefined;
837
- // Skip the notification when the task was already stopped (e.g. by main
838
- // agent shutdown) — mirrors subagentManager's wasAlreadyKilled guard.
839
- if (!wasAlreadyKilled && messageQueue) {
840
- const description = truncateWithMarker(options.description, 1000);
841
- const summary = status === "completed"
842
- ? `Subtask "${description}" completed`
843
- : `Subtask "${description}" failed: ${content}`;
844
- messageQueue.enqueueNotification(taskNotificationToXml({
845
- type: "task_notification",
846
- taskId,
847
- taskType: "agent",
848
- status,
849
- summary,
850
- ...(status === "completed" && { result: content }),
851
- }));
852
- }
853
- };
854
- // Fire-and-forget: the fork runs to completion in the background; the
855
- // caller gets the task ID immediately. Tracked in the async work registry
856
- // so destroy() drains it before returning (abort via the task's onStop
857
- // makes the loop settle).
858
- const forkPromise = (async () => {
859
- try {
860
- const result = await this.runForkLoop(historyMessages, prompt, {
861
- // /subtask aligns with Claude Code's fork subagent
862
- // (forkSubagent.ts:65): a loose upper bound that practically
863
- // never binds.
864
- maxTurns: options.maxTurns ?? 200,
865
- canUseTool,
866
- deniedToolMessage,
867
- }, abortController.signal);
868
- if (result.usage && this.callbacks?.onUsageAdded) {
869
- this.callbacks.onUsageAdded({
870
- ...result.usage,
871
- model: modelConfig.model,
872
- operation_type: "agent",
873
- });
874
- }
875
- if (result.content?.trim()) {
876
- await finish("completed", result.content);
877
- }
878
- else {
879
- await finish("failed", "Fork subagent produced no text response");
880
- }
881
- }
882
- catch (error) {
883
- // Abort is the expected exit for a killed task; stopTask has already
884
- // marked it "killed", so no notification is sent.
885
- if (abortController.signal.aborted) {
886
- const task = backgroundTaskManager?.getTask(taskId);
887
- if (task && task.status === "running") {
888
- task.status = "killed";
889
- task.endTime = Date.now();
890
- if (task.startTime)
891
- task.runtime = task.endTime - task.startTime;
892
- }
893
- logStream?.end();
894
- logStream = undefined;
895
- }
896
- else {
897
- logger?.error("Fork subagent execution failed:", error);
898
- await finish("failed", error instanceof Error ? error.message : String(error));
899
- }
900
- }
901
- finally {
902
- abortCleanup?.();
903
- }
904
- })();
905
- this.asyncWorkRegistry?.track(forkPromise);
906
- return taskId;
907
- }
908
729
  /**
909
730
  * Parse a fork tool call's JSON arguments, recovering truncated JSON the
910
731
  * same way the main loop does. Unparseable arguments fall back to `{}` and
@@ -1271,6 +1092,18 @@ ${question}`;
1271
1092
  this.abortController = abortController;
1272
1093
  toolAbortController = new AbortController();
1273
1094
  this.toolAbortController = toolAbortController;
1095
+ // Every tool call in a turn wires its own abort listener onto the
1096
+ // shared tool signal (see toolAbortController.signal in executeTool),
1097
+ // so a fan-out of >=11 parallel calls trips Node's default 10-listener
1098
+ // cap and spams MaxListenersExceededWarning. The signal dies with the
1099
+ // turn (toolAbortController is cleared in the tool loop), so there is
1100
+ // no real leak — raise the cap to silence the noise. Optional chaining
1101
+ // keeps this a no-op on Node >=26, which removed the EventTarget
1102
+ // warning mechanism (and the setMaxListeners method) entirely.
1103
+ const toolSignal = toolAbortController.signal;
1104
+ const turnSignal = abortController.signal;
1105
+ toolSignal.setMaxListeners?.(0);
1106
+ turnSignal.setMaxListeners?.(0);
1274
1107
  }
1275
1108
  else {
1276
1109
  // Reuse existing controllers
@@ -1353,6 +1186,9 @@ ${question}`;
1353
1186
  // Add streaming callbacks only if streaming is enabled
1354
1187
  if (this.stream) {
1355
1188
  callAgentOptions.onContentUpdate = (content) => {
1189
+ // Agent may have been destroyed mid-stream; ignore in-flight updates.
1190
+ if (!this.messageManager)
1191
+ return;
1356
1192
  // Create assistant message on first chunk if not already created
1357
1193
  if (!assistantMessageCreated) {
1358
1194
  this.messageManager.addAssistantMessage();
@@ -1361,6 +1197,9 @@ ${question}`;
1361
1197
  this.messageManager.updateCurrentMessageContent(content);
1362
1198
  };
1363
1199
  callAgentOptions.onToolUpdate = (toolCall) => {
1200
+ // Agent may have been destroyed mid-stream; ignore in-flight updates.
1201
+ if (!this.messageManager)
1202
+ return;
1364
1203
  // Create assistant message on first tool update if not already created
1365
1204
  if (!assistantMessageCreated) {
1366
1205
  this.messageManager.addAssistantMessage();
@@ -1383,6 +1222,9 @@ ${question}`;
1383
1222
  });
1384
1223
  };
1385
1224
  callAgentOptions.onReasoningUpdate = (reasoning) => {
1225
+ // Agent may have been destroyed mid-stream; ignore in-flight updates.
1226
+ if (!this.messageManager)
1227
+ return;
1386
1228
  // Create assistant message on first reasoning update if not already created
1387
1229
  if (!assistantMessageCreated) {
1388
1230
  this.messageManager.addAssistantMessage();
@@ -0,0 +1,33 @@
1
+ import { Container } from "../utils/container.js";
2
+ export interface BashModeManagerOptions {
3
+ workdir: string;
4
+ }
5
+ export interface CommandExecutionResult {
6
+ exitCode: number;
7
+ output: string;
8
+ }
9
+ /**
10
+ * Bash mode (`!command`) execution. The command is surfaced as a user-message
11
+ * tool block (name: BASH_TOOL_NAME) — the same rendering as fork-skill / `/plan`
12
+ * outputs (⎿ prefix + markdown, result enters the model context via the
13
+ * generic `<local-command-stdout>` rule) — and executed through the shared
14
+ * bash tool engine (shell resolution, CWD tracking, timeout, output handling).
15
+ */
16
+ export declare class BashModeManager {
17
+ private container;
18
+ private workdir;
19
+ isCommandRunning: boolean;
20
+ private abortController;
21
+ onCommandRunningChange?: (running: boolean) => void;
22
+ constructor(container: Container, options: BashModeManagerOptions);
23
+ private get messageManager();
24
+ /**
25
+ * Merged env (OS env overlaid with this session's settings snapshot) for
26
+ * bash-mode subprocesses, so settings `env` vars reach them without
27
+ * polluting other sessions in one `wave --stdio` process.
28
+ */
29
+ private get sessionEnv();
30
+ private setCommandRunning;
31
+ executeCommand(command: string): Promise<number>;
32
+ abortCommand(): void;
33
+ }
@@ -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
+ }
@@ -120,6 +120,11 @@ export declare class HookManager {
120
120
  * Register hooks provided by a plugin
121
121
  */
122
122
  registerPluginHooks(pluginRoot: string, hooks: PartialHookConfiguration): void;
123
+ /**
124
+ * Get hooks registered by plugins (read-only view for the settings UI).
125
+ * Plugin hooks carry pluginRoot on each command and are read-only in the UI.
126
+ */
127
+ getPluginHooks(): PartialHookConfiguration;
123
128
  /**
124
129
  * Execute SessionStart hooks during initialization.
125
130
  * Collects additionalContext and initialUserMessage from hook stdout.
@@ -670,6 +670,13 @@ export class HookManager {
670
670
  this.mergeHooksConfiguration(this.pluginHooks, stampedHooks);
671
671
  this.rebuildConfiguration();
672
672
  }
673
+ /**
674
+ * Get hooks registered by plugins (read-only view for the settings UI).
675
+ * Plugin hooks carry pluginRoot on each command and are read-only in the UI.
676
+ */
677
+ getPluginHooks() {
678
+ return this.pluginHooks;
679
+ }
673
680
  /**
674
681
  * Execute SessionStart hooks during initialization.
675
682
  * Collects additionalContext and initialUserMessage from hook stdout.
@@ -172,15 +172,15 @@ export class LiveConfigManager {
172
172
  }
173
173
  // Update permission manager if available
174
174
  if (this.permissionManager) {
175
- this.permissionManager.updateConfiguredPermissionMode(this.currentConfiguration.permissions?.permissionMode);
175
+ this.permissionManager.updateConfiguredPermissionMode(this.currentConfiguration.permissions?.defaultMode);
176
176
  this.permissionManager.updateAllowedRules(this.currentConfiguration.permissions?.allow || []);
177
177
  this.permissionManager.updateDeniedRules(this.currentConfiguration.permissions?.deny || []);
178
178
  this.permissionManager.updateAdditionalDirectories(this.currentConfiguration.permissions?.additionalDirectories || []);
179
179
  }
180
180
  // Trigger reload callback. Awaited so fire-and-forget work spawned by the
181
181
  // 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).
182
+ // otherwise Agent.create() can return while a skill rescan is still
183
+ // running (refreshSkills swaps caches atomically only after the scan).
184
184
  await this.options.onReload?.(this.currentConfiguration);
185
185
  return this.currentConfiguration;
186
186
  }
@@ -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,6 +53,18 @@ 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[];
@@ -57,6 +72,14 @@ export declare class McpManager {
57
72
  updateServerStatus(name: string, updates: Partial<McpServerStatus>): void;
58
73
  addServer(name: string, config: McpServerConfig): boolean;
59
74
  removeServer(name: string): boolean;
75
+ /**
76
+ * Remove a server from a persisted config file (user or project scope),
77
+ * then disconnect and drop it from the in-memory registry.
78
+ * @param scope - "user" removes from ~/.wave/mcp.json; "project" removes from <workdir>/.mcp.json
79
+ * @param name - The server name
80
+ * @returns true if the server was found and removed
81
+ */
82
+ removeServerFromConfig(scope: "user" | "project", name: string): Promise<boolean>;
60
83
  connectServer(name: string): Promise<boolean>;
61
84
  /**
62
85
  * Schedule auto-reconnect with exponential backoff.