wave-agent-sdk 0.13.4 → 0.13.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/builtin/skills/settings/HOOKS.md +25 -0
- package/builtin/skills/settings/MCP.md +22 -0
- package/builtin/skills/settings/SKILL.md +4 -1
- package/dist/agent.d.ts +21 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +102 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +5 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +19 -4
- package/dist/managers/bangManager.d.ts +1 -0
- package/dist/managers/bangManager.d.ts.map +1 -1
- package/dist/managers/bangManager.js +1 -0
- package/dist/managers/hookManager.d.ts +5 -1
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +55 -5
- package/dist/managers/lspManager.d.ts.map +1 -1
- package/dist/managers/lspManager.js +17 -2
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +20 -6
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +22 -0
- package/dist/managers/messageQueue.d.ts +20 -0
- package/dist/managers/messageQueue.d.ts.map +1 -0
- package/dist/managers/messageQueue.js +29 -0
- package/dist/managers/permissionManager.d.ts +5 -7
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +27 -22
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +5 -3
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +5 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +12 -1
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +6 -18
- package/dist/services/autoMemoryService.d.ts.map +1 -1
- package/dist/services/autoMemoryService.js +1 -0
- package/dist/services/hook.d.ts +4 -0
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +10 -0
- package/dist/services/interactionService.d.ts.map +1 -1
- package/dist/services/interactionService.js +3 -0
- package/dist/services/pluginLoader.d.ts.map +1 -1
- package/dist/services/pluginLoader.js +3 -1
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +33 -2
- package/dist/tools/types.d.ts +2 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/types/agent.d.ts +4 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +6 -1
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +4 -0
- package/dist/types/lsp.d.ts +2 -0
- package/dist/types/lsp.d.ts.map +1 -1
- package/dist/types/mcp.d.ts +2 -0
- package/dist/types/mcp.d.ts.map +1 -1
- package/dist/types/skills.d.ts +1 -0
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +4 -1
- package/package.json +1 -1
- package/src/agent.ts +122 -2
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +35 -5
- package/src/managers/bangManager.ts +2 -0
- package/src/managers/hookManager.ts +78 -19
- package/src/managers/lspManager.ts +23 -2
- package/src/managers/mcpManager.ts +29 -6
- package/src/managers/messageManager.ts +38 -0
- package/src/managers/messageQueue.ts +41 -0
- package/src/managers/permissionManager.ts +32 -26
- package/src/managers/pluginManager.ts +5 -3
- package/src/managers/skillManager.ts +9 -0
- package/src/managers/slashCommandManager.ts +16 -4
- package/src/managers/subagentManager.ts +10 -25
- package/src/services/autoMemoryService.ts +1 -0
- package/src/services/hook.ts +15 -0
- package/src/services/interactionService.ts +3 -0
- package/src/services/pluginLoader.ts +3 -1
- package/src/tools/bashTool.ts +39 -2
- package/src/tools/types.ts +2 -0
- package/src/types/agent.ts +4 -0
- package/src/types/hooks.ts +13 -2
- package/src/types/lsp.ts +2 -0
- package/src/types/mcp.ts +2 -0
- package/src/types/skills.ts +1 -0
- package/src/utils/containerSetup.ts +5 -1
|
@@ -166,28 +166,42 @@ export class HookManager {
|
|
|
166
166
|
? { timeout: hookCommand.timeout * 1000 }
|
|
167
167
|
: undefined;
|
|
168
168
|
|
|
169
|
+
// Build execution context with WAVE_PLUGIN_ROOT if this is a plugin hook
|
|
170
|
+
let command = hookCommand.command;
|
|
171
|
+
const execContext: typeof context = hookCommand.pluginRoot
|
|
172
|
+
? {
|
|
173
|
+
...context,
|
|
174
|
+
env: {
|
|
175
|
+
...("env" in context ? (context.env ?? {}) : {}),
|
|
176
|
+
WAVE_PLUGIN_ROOT: hookCommand.pluginRoot,
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
: context;
|
|
180
|
+
|
|
181
|
+
// Substitute ${WAVE_PLUGIN_ROOT} in the command string (same pattern as Claude Code)
|
|
182
|
+
if (hookCommand.pluginRoot) {
|
|
183
|
+
command = command.replace(
|
|
184
|
+
/\$\{WAVE_PLUGIN_ROOT\}/g,
|
|
185
|
+
hookCommand.pluginRoot,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
if (hookCommand.async) {
|
|
170
190
|
// Execute async command without awaiting
|
|
171
|
-
executeCommand(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
error
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
},
|
|
181
|
-
);
|
|
191
|
+
executeCommand(command, execContext, options).catch((error) => {
|
|
192
|
+
const errorMessage =
|
|
193
|
+
error instanceof Error
|
|
194
|
+
? error.message
|
|
195
|
+
: "Unknown execution error";
|
|
196
|
+
logger?.error(
|
|
197
|
+
`[HookManager] Async hook command ${commandIndex + 1} failed: ${errorMessage}`,
|
|
198
|
+
);
|
|
199
|
+
});
|
|
182
200
|
// Async hooks are not included in results to prevent blocking
|
|
183
201
|
continue;
|
|
184
202
|
}
|
|
185
203
|
|
|
186
|
-
const result = await executeCommand(
|
|
187
|
-
hookCommand.command,
|
|
188
|
-
context,
|
|
189
|
-
options,
|
|
190
|
-
);
|
|
204
|
+
const result = await executeCommand(command, execContext, options);
|
|
191
205
|
results.push(result);
|
|
192
206
|
|
|
193
207
|
// Continue with next command even if this one fails
|
|
@@ -639,7 +653,8 @@ export class HookManager {
|
|
|
639
653
|
event === "UserPromptSubmit" ||
|
|
640
654
|
event === "Stop" ||
|
|
641
655
|
event === "SubagentStop" ||
|
|
642
|
-
event === "WorktreeCreate"
|
|
656
|
+
event === "WorktreeCreate" ||
|
|
657
|
+
event === "CwdChanged"
|
|
643
658
|
) {
|
|
644
659
|
return true;
|
|
645
660
|
}
|
|
@@ -739,6 +754,7 @@ export class HookManager {
|
|
|
739
754
|
SubagentStop: 0,
|
|
740
755
|
PermissionRequest: 0,
|
|
741
756
|
WorktreeCreate: 0,
|
|
757
|
+
CwdChanged: 0,
|
|
742
758
|
},
|
|
743
759
|
};
|
|
744
760
|
}
|
|
@@ -751,6 +767,7 @@ export class HookManager {
|
|
|
751
767
|
SubagentStop: 0,
|
|
752
768
|
PermissionRequest: 0,
|
|
753
769
|
WorktreeCreate: 0,
|
|
770
|
+
CwdChanged: 0,
|
|
754
771
|
};
|
|
755
772
|
|
|
756
773
|
let totalConfigs = 0;
|
|
@@ -775,14 +792,56 @@ export class HookManager {
|
|
|
775
792
|
};
|
|
776
793
|
}
|
|
777
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Execute CwdChanged hooks.
|
|
797
|
+
*/
|
|
798
|
+
async executeCwdChangedHooks(
|
|
799
|
+
oldCwd: string,
|
|
800
|
+
newCwd: string,
|
|
801
|
+
sessionId: string,
|
|
802
|
+
transcriptPath: string,
|
|
803
|
+
env: Record<string, string>,
|
|
804
|
+
): Promise<HookExecutionResult[]> {
|
|
805
|
+
const context: ExtendedHookExecutionContext = {
|
|
806
|
+
event: "CwdChanged",
|
|
807
|
+
projectDir: this.workdir,
|
|
808
|
+
timestamp: new Date(),
|
|
809
|
+
sessionId,
|
|
810
|
+
transcriptPath,
|
|
811
|
+
cwd: newCwd,
|
|
812
|
+
oldCwd,
|
|
813
|
+
newCwd,
|
|
814
|
+
env,
|
|
815
|
+
};
|
|
816
|
+
const results = await this.executeHooks("CwdChanged", context);
|
|
817
|
+
if (results.length > 0) {
|
|
818
|
+
// For CwdChanged hooks, we don't block, just log errors
|
|
819
|
+
this.processHookResults("CwdChanged", results);
|
|
820
|
+
}
|
|
821
|
+
return results;
|
|
822
|
+
}
|
|
823
|
+
|
|
778
824
|
/**
|
|
779
825
|
* Register hooks provided by a plugin
|
|
780
826
|
*/
|
|
781
|
-
registerPluginHooks(
|
|
827
|
+
registerPluginHooks(
|
|
828
|
+
pluginRoot: string,
|
|
829
|
+
hooks: PartialHookConfiguration,
|
|
830
|
+
): void {
|
|
782
831
|
if (!this.configuration) {
|
|
783
832
|
this.configuration = {};
|
|
784
833
|
}
|
|
785
834
|
|
|
786
|
-
|
|
835
|
+
// Stamp pluginRoot on each hook command
|
|
836
|
+
const stampedHooks: PartialHookConfiguration = {};
|
|
837
|
+
for (const [event, configs] of Object.entries(hooks)) {
|
|
838
|
+
if (!isValidHookEvent(event)) continue;
|
|
839
|
+
stampedHooks[event] = configs.map((config) => ({
|
|
840
|
+
...config,
|
|
841
|
+
hooks: config.hooks.map((cmd) => ({ ...cmd, pluginRoot })),
|
|
842
|
+
}));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
this.mergeHooksConfiguration(this.configuration, stampedHooks);
|
|
787
846
|
}
|
|
788
847
|
}
|
|
@@ -90,9 +90,30 @@ export class LspManager implements ILspManager {
|
|
|
90
90
|
);
|
|
91
91
|
|
|
92
92
|
try {
|
|
93
|
-
const
|
|
93
|
+
const env = { ...process.env, ...config.env };
|
|
94
|
+
|
|
95
|
+
// For plugin servers, substitute ${WAVE_PLUGIN_ROOT} in command/args/env
|
|
96
|
+
let command = config.command;
|
|
97
|
+
let args = config.args || [];
|
|
98
|
+
if (config.pluginRoot) {
|
|
99
|
+
env.WAVE_PLUGIN_ROOT = config.pluginRoot;
|
|
100
|
+
command = command.replace(/\$\{WAVE_PLUGIN_ROOT\}/g, config.pluginRoot);
|
|
101
|
+
args = args.map((arg) =>
|
|
102
|
+
arg.replace(/\$\{WAVE_PLUGIN_ROOT\}/g, config.pluginRoot!),
|
|
103
|
+
);
|
|
104
|
+
// Also expand WAVE_PLUGIN_ROOT in user-provided env values
|
|
105
|
+
if (config.env) {
|
|
106
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
107
|
+
env[key] = value.replace(
|
|
108
|
+
/\$\{WAVE_PLUGIN_ROOT\}/g,
|
|
109
|
+
config.pluginRoot!,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const proc = spawn(command, args, {
|
|
94
115
|
cwd: config.workspaceFolder || this.workdir,
|
|
95
|
-
env
|
|
116
|
+
env,
|
|
96
117
|
stdio: ["pipe", "pipe", "pipe"],
|
|
97
118
|
});
|
|
98
119
|
|
|
@@ -348,13 +348,36 @@ export class McpManager {
|
|
|
348
348
|
logger?.info(`Connected to MCP server ${name} using SSE (fallback)`);
|
|
349
349
|
}
|
|
350
350
|
} else if (server.config.command) {
|
|
351
|
+
const env: Record<string, string> = {
|
|
352
|
+
...(process.env as Record<string, string>),
|
|
353
|
+
...(server.config.env || {}),
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// For plugin servers, substitute ${WAVE_PLUGIN_ROOT} in command/args/env
|
|
357
|
+
// (same pattern as Claude Code's substitutePluginVariables)
|
|
358
|
+
let command = server.config.command;
|
|
359
|
+
let args = server.config.args || [];
|
|
360
|
+
if (server.config.pluginRoot) {
|
|
361
|
+
env.WAVE_PLUGIN_ROOT = server.config.pluginRoot;
|
|
362
|
+
command = command.replace(
|
|
363
|
+
/\$\{WAVE_PLUGIN_ROOT\}/g,
|
|
364
|
+
server.config.pluginRoot,
|
|
365
|
+
);
|
|
366
|
+
args = args.map((arg) =>
|
|
367
|
+
arg.replace(/\$\{WAVE_PLUGIN_ROOT\}/g, server.config.pluginRoot!),
|
|
368
|
+
);
|
|
369
|
+
// Also expand WAVE_PLUGIN_ROOT in user-provided env values
|
|
370
|
+
for (const [key, value] of Object.entries(server.config.env || {})) {
|
|
371
|
+
env[key] = value.replace(
|
|
372
|
+
/\$\{WAVE_PLUGIN_ROOT\}/g,
|
|
373
|
+
server.config.pluginRoot!,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
351
377
|
transport = new StdioClientTransport({
|
|
352
|
-
command
|
|
353
|
-
args
|
|
354
|
-
env
|
|
355
|
-
...(process.env as Record<string, string>),
|
|
356
|
-
...(server.config.env || {}),
|
|
357
|
-
},
|
|
378
|
+
command,
|
|
379
|
+
args,
|
|
380
|
+
env,
|
|
358
381
|
cwd: this.workdir, // Use the agent's workdir as the process working directory
|
|
359
382
|
stderr: "pipe", // Pipe stderr to capture it
|
|
360
383
|
});
|
|
@@ -654,6 +654,25 @@ export class MessageManager {
|
|
|
654
654
|
const lastMessage = this.messages[this.messages.length - 1];
|
|
655
655
|
if (lastMessage.role !== "assistant") return;
|
|
656
656
|
|
|
657
|
+
// Finalize any streaming reasoning blocks before text content arrives
|
|
658
|
+
const reasoningIndex = lastMessage.blocks.findIndex(
|
|
659
|
+
(block) =>
|
|
660
|
+
block.type === "reasoning" &&
|
|
661
|
+
(block as { stage?: string }).stage === "streaming",
|
|
662
|
+
);
|
|
663
|
+
if (reasoningIndex >= 0) {
|
|
664
|
+
const reasoningBlock = lastMessage.blocks[reasoningIndex] as {
|
|
665
|
+
type: "reasoning";
|
|
666
|
+
content: string;
|
|
667
|
+
stage?: string;
|
|
668
|
+
};
|
|
669
|
+
lastMessage.blocks[reasoningIndex] = {
|
|
670
|
+
type: "reasoning" as const,
|
|
671
|
+
content: reasoningBlock.content,
|
|
672
|
+
stage: "end" as const,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
657
676
|
// Get the current content to calculate the chunk
|
|
658
677
|
const textBlockIndex = lastMessage.blocks.findIndex(
|
|
659
678
|
(block) => block.type === "text",
|
|
@@ -705,6 +724,25 @@ export class MessageManager {
|
|
|
705
724
|
const lastMessage = this.messages[this.messages.length - 1];
|
|
706
725
|
if (lastMessage.role !== "assistant") return;
|
|
707
726
|
|
|
727
|
+
// Finalize any streaming text blocks before reasoning content arrives
|
|
728
|
+
const textIndex = lastMessage.blocks.findIndex(
|
|
729
|
+
(block) =>
|
|
730
|
+
block.type === "text" &&
|
|
731
|
+
(block as { stage?: string }).stage === "streaming",
|
|
732
|
+
);
|
|
733
|
+
if (textIndex >= 0) {
|
|
734
|
+
const textBlock = lastMessage.blocks[textIndex] as {
|
|
735
|
+
type: "text";
|
|
736
|
+
content: string;
|
|
737
|
+
stage?: string;
|
|
738
|
+
};
|
|
739
|
+
lastMessage.blocks[textIndex] = {
|
|
740
|
+
type: "text" as const,
|
|
741
|
+
content: textBlock.content,
|
|
742
|
+
stage: "end" as const,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
708
746
|
// Get the current reasoning content to calculate the chunk
|
|
709
747
|
const reasoningBlockIndex = lastMessage.blocks.findIndex(
|
|
710
748
|
(block) => block.type === "reasoning",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface QueuedMessage {
|
|
2
|
+
type?: "message" | "bang";
|
|
3
|
+
content: string;
|
|
4
|
+
images?: Array<{ path: string; mimeType: string }>;
|
|
5
|
+
longTextMap?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class MessageQueue {
|
|
9
|
+
private queue: QueuedMessage[] = [];
|
|
10
|
+
onMessageEnqueued?: () => void;
|
|
11
|
+
|
|
12
|
+
enqueue(message: QueuedMessage): void {
|
|
13
|
+
this.queue.push(message);
|
|
14
|
+
this.onMessageEnqueued?.();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
dequeue(): QueuedMessage | null {
|
|
18
|
+
return this.queue.shift() ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
clear(): void {
|
|
22
|
+
this.queue = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
hasPending(): boolean {
|
|
26
|
+
return this.queue.length > 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getQueue(): QueuedMessage[] {
|
|
30
|
+
return [...this.queue];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
removeAt(index: number): boolean {
|
|
34
|
+
if (index < 0 || index >= this.queue.length) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
this.queue.splice(index, 1);
|
|
38
|
+
this.onMessageEnqueued?.();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -113,8 +113,6 @@ export interface PermissionManagerOptions {
|
|
|
113
113
|
additionalDirectories?: string[];
|
|
114
114
|
/** System additional directories (persistent across reloads) */
|
|
115
115
|
systemAdditionalDirectories?: string[];
|
|
116
|
-
/** The main working directory */
|
|
117
|
-
workdir?: string;
|
|
118
116
|
/** Path to the current plan file */
|
|
119
117
|
planFilePath?: string;
|
|
120
118
|
/** Optional logger */
|
|
@@ -130,10 +128,10 @@ export class PermissionManager {
|
|
|
130
128
|
private temporaryRules: string[] = [];
|
|
131
129
|
private additionalDirectories: string[] = [];
|
|
132
130
|
private systemAdditionalDirectories: string[] = [];
|
|
133
|
-
private workdir?: string;
|
|
134
131
|
private planFilePath?: string;
|
|
135
132
|
private worktreeName?: string;
|
|
136
133
|
private mainRepoRoot?: string;
|
|
134
|
+
private originalWorkdir?: string;
|
|
137
135
|
private onConfiguredPermissionModeChange?: (mode: PermissionMode) => void;
|
|
138
136
|
private _logger?: Logger;
|
|
139
137
|
|
|
@@ -146,7 +144,6 @@ export class PermissionManager {
|
|
|
146
144
|
this.deniedRules = options.deniedRules || [];
|
|
147
145
|
this.instanceAllowedRules = options.instanceAllowedRules || [];
|
|
148
146
|
this.instanceDeniedRules = options.instanceDeniedRules || [];
|
|
149
|
-
this.workdir = options.workdir;
|
|
150
147
|
this.planFilePath = options.planFilePath;
|
|
151
148
|
this._logger = options.logger;
|
|
152
149
|
this.updateAdditionalDirectories(options.additionalDirectories || []);
|
|
@@ -156,6 +153,14 @@ export class PermissionManager {
|
|
|
156
153
|
|
|
157
154
|
this.worktreeName = this.container.get<string>("WorktreeName");
|
|
158
155
|
this.mainRepoRoot = this.container.get<string>("MainRepoRoot");
|
|
156
|
+
this.originalWorkdir = this.container.get<string>("Workdir");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the working directory from the DI container
|
|
161
|
+
*/
|
|
162
|
+
private getWorkdir(): string | undefined {
|
|
163
|
+
return this.container.get<string>("Workdir");
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
/**
|
|
@@ -272,9 +277,10 @@ export class PermissionManager {
|
|
|
272
277
|
* Update the additional directories (e.g., when configuration reloads)
|
|
273
278
|
*/
|
|
274
279
|
updateAdditionalDirectories(directories: string[]): void {
|
|
280
|
+
const workdir = this.originalWorkdir;
|
|
275
281
|
this.additionalDirectories = directories.map((dir) => {
|
|
276
|
-
if (
|
|
277
|
-
return path.resolve(
|
|
282
|
+
if (workdir && !path.isAbsolute(dir)) {
|
|
283
|
+
return path.resolve(workdir, dir);
|
|
278
284
|
}
|
|
279
285
|
return path.resolve(dir);
|
|
280
286
|
});
|
|
@@ -284,9 +290,10 @@ export class PermissionManager {
|
|
|
284
290
|
* Add a system-level additional directory that is persistent across configuration reloads
|
|
285
291
|
*/
|
|
286
292
|
public addSystemAdditionalDirectory(directory: string): void {
|
|
293
|
+
const workdir = this.originalWorkdir;
|
|
287
294
|
const resolvedPath =
|
|
288
|
-
|
|
289
|
-
? path.resolve(
|
|
295
|
+
workdir && !path.isAbsolute(directory)
|
|
296
|
+
? path.resolve(workdir, directory)
|
|
290
297
|
: path.resolve(directory);
|
|
291
298
|
|
|
292
299
|
if (!this.systemAdditionalDirectories.includes(resolvedPath)) {
|
|
@@ -294,13 +301,6 @@ export class PermissionManager {
|
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
303
|
|
|
297
|
-
/**
|
|
298
|
-
* Update the working directory
|
|
299
|
-
*/
|
|
300
|
-
updateWorkdir(workdir: string): void {
|
|
301
|
-
this.workdir = workdir;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
304
|
/**
|
|
305
305
|
* Set the current plan file path
|
|
306
306
|
*/
|
|
@@ -322,7 +322,7 @@ export class PermissionManager {
|
|
|
322
322
|
targetPath: string,
|
|
323
323
|
workdir?: string,
|
|
324
324
|
): { isInside: boolean; resolvedPath: string } {
|
|
325
|
-
const effectiveWorkdir =
|
|
325
|
+
const effectiveWorkdir = this.originalWorkdir || workdir;
|
|
326
326
|
|
|
327
327
|
// Resolve the target path relative to effectiveWorkdir if it's not absolute
|
|
328
328
|
const absolutePath =
|
|
@@ -432,21 +432,25 @@ export class PermissionManager {
|
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
// 0. Check worktree safety for Write and Edit tools
|
|
435
|
+
const currentWorkdir = this.getWorkdir();
|
|
435
436
|
if (
|
|
436
437
|
this.worktreeName &&
|
|
437
438
|
this.mainRepoRoot &&
|
|
438
|
-
|
|
439
|
+
currentWorkdir &&
|
|
439
440
|
(context.toolName === WRITE_TOOL_NAME ||
|
|
440
441
|
context.toolName === EDIT_TOOL_NAME)
|
|
441
442
|
) {
|
|
442
443
|
const targetPath = context.toolInput?.file_path as string | undefined;
|
|
443
444
|
if (targetPath) {
|
|
444
|
-
const absoluteTargetPath = path.resolve(
|
|
445
|
+
const absoluteTargetPath = path.resolve(currentWorkdir, targetPath);
|
|
445
446
|
const isInsideMainRepo = isPathInside(
|
|
446
447
|
absoluteTargetPath,
|
|
447
448
|
this.mainRepoRoot,
|
|
448
449
|
);
|
|
449
|
-
const isInsideWorktree = isPathInside(
|
|
450
|
+
const isInsideWorktree = isPathInside(
|
|
451
|
+
absoluteTargetPath,
|
|
452
|
+
currentWorkdir,
|
|
453
|
+
);
|
|
450
454
|
|
|
451
455
|
// If it's inside the main repo but NOT inside the current worktree
|
|
452
456
|
if (isInsideMainRepo && !isInsideWorktree) {
|
|
@@ -455,11 +459,11 @@ export class PermissionManager {
|
|
|
455
459
|
targetPath,
|
|
456
460
|
worktreeName: this.worktreeName,
|
|
457
461
|
mainRepoRoot: this.mainRepoRoot,
|
|
458
|
-
workdir:
|
|
462
|
+
workdir: currentWorkdir,
|
|
459
463
|
});
|
|
460
464
|
return {
|
|
461
465
|
behavior: "deny",
|
|
462
|
-
message: `Access denied: You are currently in a worktree session ("${this.worktreeName}"). Modifying files in the main repository (outside the worktree) is not allowed. Please only modify files within the worktree directory: ${
|
|
466
|
+
message: `Access denied: You are currently in a worktree session ("${this.worktreeName}"). Modifying files in the main repository (outside the worktree) is not allowed. Please only modify files within the worktree directory: ${currentWorkdir}`,
|
|
463
467
|
};
|
|
464
468
|
}
|
|
465
469
|
}
|
|
@@ -808,12 +812,13 @@ export class PermissionManager {
|
|
|
808
812
|
}
|
|
809
813
|
|
|
810
814
|
// If direct match fails, try matching relative path if targetPath is absolute and pattern is relative
|
|
815
|
+
const currentWorkdir = this.getWorkdir();
|
|
811
816
|
if (
|
|
812
817
|
path.isAbsolute(targetPath) &&
|
|
813
818
|
!path.isAbsolute(pattern) &&
|
|
814
|
-
|
|
819
|
+
currentWorkdir
|
|
815
820
|
) {
|
|
816
|
-
const relativePath = path.relative(
|
|
821
|
+
const relativePath = path.relative(currentWorkdir, targetPath);
|
|
817
822
|
// Ensure the path is not outside the workdir (doesn't start with ..)
|
|
818
823
|
if (
|
|
819
824
|
!relativePath.startsWith("..") &&
|
|
@@ -1056,7 +1061,8 @@ export class PermissionManager {
|
|
|
1056
1061
|
* @param rule - The rule to add (e.g., "Bash(ls)")
|
|
1057
1062
|
*/
|
|
1058
1063
|
public async addPermissionRule(rule: string): Promise<void> {
|
|
1059
|
-
|
|
1064
|
+
const workdir = this.originalWorkdir;
|
|
1065
|
+
if (!workdir) {
|
|
1060
1066
|
throw new Error("Working directory not set in PermissionManager");
|
|
1061
1067
|
}
|
|
1062
1068
|
|
|
@@ -1065,7 +1071,7 @@ export class PermissionManager {
|
|
|
1065
1071
|
const bashMatch = rule.match(/^Bash\((.*)\)$/);
|
|
1066
1072
|
if (bashMatch) {
|
|
1067
1073
|
const command = bashMatch[1];
|
|
1068
|
-
rulesToAdd = this.expandBashRule(command,
|
|
1074
|
+
rulesToAdd = this.expandBashRule(command, workdir);
|
|
1069
1075
|
}
|
|
1070
1076
|
|
|
1071
1077
|
const configurationService = this.container.get<ConfigurationService>(
|
|
@@ -1085,7 +1091,7 @@ export class PermissionManager {
|
|
|
1085
1091
|
// 3. Persist to settings.local.json
|
|
1086
1092
|
try {
|
|
1087
1093
|
if (configurationService) {
|
|
1088
|
-
await configurationService.addAllowedRule(
|
|
1094
|
+
await configurationService.addAllowedRule(workdir, ruleToAdd);
|
|
1089
1095
|
this._logger?.debug("Persistent permission rule added", {
|
|
1090
1096
|
rule: ruleToAdd,
|
|
1091
1097
|
});
|
|
@@ -171,7 +171,8 @@ export class PluginManager {
|
|
|
171
171
|
|
|
172
172
|
if (this.lspManager && plugin.lspConfig) {
|
|
173
173
|
for (const [language, config] of Object.entries(plugin.lspConfig)) {
|
|
174
|
-
|
|
174
|
+
const configWithPluginRoot = { ...config, pluginRoot: plugin.path };
|
|
175
|
+
this.lspManager.registerServer(language, configWithPluginRoot);
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
|
|
@@ -179,12 +180,13 @@ export class PluginManager {
|
|
|
179
180
|
for (const [name, config] of Object.entries(
|
|
180
181
|
plugin.mcpConfig.mcpServers,
|
|
181
182
|
)) {
|
|
182
|
-
|
|
183
|
+
const configWithPluginRoot = { ...config, pluginRoot: plugin.path };
|
|
184
|
+
this.mcpManager.addServer(name, configWithPluginRoot);
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
if (this.hookManager && plugin.hooksConfig) {
|
|
187
|
-
this.hookManager.registerPluginHooks(plugin.hooksConfig);
|
|
189
|
+
this.hookManager.registerPluginHooks(plugin.path, plugin.hooksConfig);
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
this.plugins.set(manifest.name, plugin);
|
|
@@ -443,6 +443,14 @@ export class SkillManager extends EventEmitter {
|
|
|
443
443
|
// 2. Substitute ${WAVE_SKILL_DIR} with the skill's directory path
|
|
444
444
|
mainContent = mainContent.replace(/\$\{WAVE_SKILL_DIR\}/g, skill.skillPath);
|
|
445
445
|
|
|
446
|
+
// 3. Substitute ${WAVE_PLUGIN_ROOT} with the skill's plugin root path
|
|
447
|
+
if (skill.pluginRoot) {
|
|
448
|
+
mainContent = mainContent.replace(
|
|
449
|
+
/\$\{WAVE_PLUGIN_ROOT\}/g,
|
|
450
|
+
skill.pluginRoot,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
446
454
|
return header + description + skillPath + mainContent;
|
|
447
455
|
}
|
|
448
456
|
|
|
@@ -493,6 +501,7 @@ export class SkillManager extends EventEmitter {
|
|
|
493
501
|
disableModelInvocation: skill.disableModelInvocation,
|
|
494
502
|
userInvocable: skill.userInvocable,
|
|
495
503
|
pluginName,
|
|
504
|
+
pluginRoot: skill.pluginRoot,
|
|
496
505
|
};
|
|
497
506
|
// Update the skill object itself to have the namespaced name
|
|
498
507
|
skill.name = namespacedName;
|
|
@@ -248,6 +248,8 @@ export class SlashCommandManager {
|
|
|
248
248
|
},
|
|
249
249
|
);
|
|
250
250
|
|
|
251
|
+
// Show loading while subagent runs
|
|
252
|
+
this.aiManager.setIsLoading(true);
|
|
251
253
|
try {
|
|
252
254
|
const result = await this.subagentManager.executeAgent(
|
|
253
255
|
instance,
|
|
@@ -261,7 +263,11 @@ export class SlashCommandManager {
|
|
|
261
263
|
messageId,
|
|
262
264
|
result,
|
|
263
265
|
stage: "end",
|
|
266
|
+
success: true,
|
|
264
267
|
});
|
|
268
|
+
|
|
269
|
+
// Trigger AI to process the tool result
|
|
270
|
+
await this.aiManager.sendAIMessage();
|
|
265
271
|
} finally {
|
|
266
272
|
this.subagentManager.cleanupInstance(instance.subagentId);
|
|
267
273
|
}
|
|
@@ -271,6 +277,7 @@ export class SlashCommandManager {
|
|
|
271
277
|
id: toolBlockId,
|
|
272
278
|
messageId,
|
|
273
279
|
stage: "end",
|
|
280
|
+
success: false,
|
|
274
281
|
error: error instanceof Error ? error.message : String(error),
|
|
275
282
|
});
|
|
276
283
|
throw error; // Re-throw to be caught by outer catch for logging/error block
|
|
@@ -279,6 +286,7 @@ export class SlashCommandManager {
|
|
|
279
286
|
}
|
|
280
287
|
|
|
281
288
|
// Non-forked skill: execute and trigger AI response
|
|
289
|
+
this.aiManager.setIsLoading(true);
|
|
282
290
|
const result = await this.skillManager.executeSkill({
|
|
283
291
|
skill_name: skill.name,
|
|
284
292
|
args,
|
|
@@ -296,10 +304,9 @@ export class SlashCommandManager {
|
|
|
296
304
|
allowedRules: result.allowedTools,
|
|
297
305
|
});
|
|
298
306
|
} catch (error) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
);
|
|
307
|
+
this.aiManager.setIsLoading(false);
|
|
308
|
+
|
|
309
|
+
logger?.error(error);
|
|
303
310
|
this.messageManager.addErrorBlock(
|
|
304
311
|
`Failed to execute skill command '${skill.name}': ${
|
|
305
312
|
error instanceof Error ? error.message : String(error)
|
|
@@ -490,6 +497,9 @@ export class SlashCommandManager {
|
|
|
490
497
|
args?: string,
|
|
491
498
|
): Promise<void> {
|
|
492
499
|
try {
|
|
500
|
+
// Set loading early so UI shows feedback during bash execution
|
|
501
|
+
this.aiManager.setIsLoading(true);
|
|
502
|
+
|
|
493
503
|
// Parse bash commands from the content
|
|
494
504
|
const { commands, processedContent } = parseBashCommands(content);
|
|
495
505
|
|
|
@@ -519,6 +529,8 @@ export class SlashCommandManager {
|
|
|
519
529
|
allowedRules: config?.allowedTools,
|
|
520
530
|
});
|
|
521
531
|
} catch (error) {
|
|
532
|
+
this.aiManager.setIsLoading(false);
|
|
533
|
+
|
|
522
534
|
logger?.error(
|
|
523
535
|
`Failed to execute custom command '${commandName}':`,
|
|
524
536
|
error,
|
|
@@ -243,7 +243,6 @@ export class SubagentManager {
|
|
|
243
243
|
const parentPermissionManager =
|
|
244
244
|
this.container.get<PermissionManager>("PermissionManager");
|
|
245
245
|
const subagentPermissionManager = new PermissionManager(subagentContainer, {
|
|
246
|
-
workdir: this.workdir,
|
|
247
246
|
configuredPermissionMode:
|
|
248
247
|
parameters.permissionModeOverride ??
|
|
249
248
|
parentPermissionManager?.getConfiguredPermissionMode(),
|
|
@@ -263,6 +262,15 @@ export class SubagentManager {
|
|
|
263
262
|
});
|
|
264
263
|
subagentContainer.register("PermissionManager", subagentPermissionManager);
|
|
265
264
|
|
|
265
|
+
// Register the permission mode override in the subagent container so it
|
|
266
|
+
// shadows the inherited parent value during tool execution
|
|
267
|
+
if (parameters.permissionModeOverride) {
|
|
268
|
+
subagentContainer.register(
|
|
269
|
+
"PermissionMode",
|
|
270
|
+
parameters.permissionModeOverride,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
266
274
|
// Track this subagent's PermissionManager for rule sync
|
|
267
275
|
this.subagentPermissionManagers.set(subagentId, subagentPermissionManager);
|
|
268
276
|
|
|
@@ -399,6 +407,7 @@ export class SubagentManager {
|
|
|
399
407
|
instance.backgroundTaskId = taskId;
|
|
400
408
|
|
|
401
409
|
// Execute in background
|
|
410
|
+
// Note: notification enqueueing is handled by internalExecute when instance.backgroundTaskId is set
|
|
402
411
|
(async () => {
|
|
403
412
|
try {
|
|
404
413
|
const result = await this.internalExecute(
|
|
@@ -413,17 +422,6 @@ export class SubagentManager {
|
|
|
413
422
|
task.endTime = Date.now();
|
|
414
423
|
task.runtime = task.endTime - startTime;
|
|
415
424
|
}
|
|
416
|
-
|
|
417
|
-
// Enqueue completion notification
|
|
418
|
-
const notificationQueue = this.container.has("NotificationQueue")
|
|
419
|
-
? this.container.get<NotificationQueue>("NotificationQueue")
|
|
420
|
-
: undefined;
|
|
421
|
-
if (notificationQueue) {
|
|
422
|
-
const summary = `Agent task "${instance.description}" completed`;
|
|
423
|
-
notificationQueue.enqueue(
|
|
424
|
-
`<task-notification>\n<task-id>${taskId}</task-id>\n<task-type>agent</task-type>\n<status>completed</status>\n<summary>${summary}</summary>\n</task-notification>`,
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
425
|
} catch (error) {
|
|
428
426
|
const task = backgroundTaskManager?.getTask(taskId);
|
|
429
427
|
if (task) {
|
|
@@ -433,19 +431,6 @@ export class SubagentManager {
|
|
|
433
431
|
task.endTime = Date.now();
|
|
434
432
|
task.runtime = task.endTime - startTime;
|
|
435
433
|
}
|
|
436
|
-
|
|
437
|
-
// Enqueue error notification
|
|
438
|
-
const notificationQueue = this.container.has("NotificationQueue")
|
|
439
|
-
? this.container.get<NotificationQueue>("NotificationQueue")
|
|
440
|
-
: undefined;
|
|
441
|
-
if (notificationQueue) {
|
|
442
|
-
const errorMsg =
|
|
443
|
-
error instanceof Error ? error.message : String(error);
|
|
444
|
-
const summary = `Agent task "${instance.description}" failed: ${errorMsg}`;
|
|
445
|
-
notificationQueue.enqueue(
|
|
446
|
-
`<task-notification>\n<task-id>${taskId}</task-id>\n<task-type>agent</task-type>\n<status>failed</status>\n<summary>${summary}</summary>\n</task-notification>`,
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
434
|
}
|
|
450
435
|
})();
|
|
451
436
|
|
|
@@ -160,6 +160,7 @@ export class AutoMemoryService {
|
|
|
160
160
|
"Grep",
|
|
161
161
|
`Write(${memoryDir}/**/*)`,
|
|
162
162
|
`Edit(${memoryDir}/**/*)`,
|
|
163
|
+
`Bash(rm ${memoryDir}/**/*)`,
|
|
163
164
|
],
|
|
164
165
|
model: "fastModel", // Use fast model for background tasks to reduce latency and cost
|
|
165
166
|
permissionModeOverride: "dontAsk", // Auto-deny out-of-scope writes without prompting user
|