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.
- package/dist/agent.d.ts +72 -24
- package/dist/agent.js +123 -31
- package/dist/builtin/plugins.js +17 -2
- package/dist/builtin/skills/settings.js +6 -8
- package/dist/core/plugin.d.ts +4 -0
- package/dist/core/plugin.js +7 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/managers/aiManager.d.ts +0 -24
- package/dist/managers/aiManager.js +23 -181
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +5 -0
- package/dist/managers/hookManager.js +7 -0
- package/dist/managers/liveConfigManager.js +3 -3
- package/dist/managers/mcpManager.d.ts +23 -0
- package/dist/managers/mcpManager.js +162 -14
- package/dist/managers/messageManager.d.ts +12 -13
- package/dist/managers/messageManager.js +63 -60
- package/dist/managers/permissionManager.d.ts +29 -0
- package/dist/managers/permissionManager.js +158 -70
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- package/dist/managers/skillManager.d.ts +31 -0
- package/dist/managers/skillManager.js +122 -12
- package/dist/managers/slashCommandManager.js +9 -28
- package/dist/managers/subagentManager.d.ts +7 -0
- package/dist/managers/subagentManager.js +61 -9
- package/dist/managers/workflowManager.js +6 -0
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/services/MarketplaceService.js +36 -14
- package/dist/services/configurationService.d.ts +34 -2
- package/dist/services/configurationService.js +123 -16
- package/dist/services/initializationService.js +2 -2
- package/dist/services/jsonlHandler.d.ts +14 -0
- package/dist/services/jsonlHandler.js +44 -1
- package/dist/services/memory.d.ts +14 -0
- package/dist/services/memory.js +33 -0
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +289 -0
- package/dist/services/remoteSettingsService.js +4 -4
- package/dist/services/session.js +30 -13
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +10 -0
- package/dist/stdio/index.js +10 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +96 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +222 -0
- package/dist/stdio/stdioAgent.js +341 -0
- package/dist/tools/bashTool.js +2 -0
- package/dist/tools/exitPlanMode.js +10 -2
- package/dist/types/agent.d.ts +8 -0
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +6 -1
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/mcp.d.ts +3 -0
- package/dist/types/messaging.d.ts +1 -8
- package/dist/types/skills.d.ts +11 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/container.d.ts +6 -0
- package/dist/utils/container.js +9 -0
- package/dist/utils/containerSetup.d.ts +11 -1
- package/dist/utils/containerSetup.js +26 -11
- package/dist/utils/fileUtils.d.ts +11 -0
- package/dist/utils/fileUtils.js +37 -0
- package/dist/utils/messageOperations.d.ts +0 -18
- package/dist/utils/messageOperations.js +0 -62
- package/dist/utils/subagentParser.js +9 -2
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +6 -1
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
|
@@ -16,4 +16,14 @@ export interface AgentContainerSetupOptions {
|
|
|
16
16
|
addPermissionRule: (rule: string) => Promise<void>;
|
|
17
17
|
addUsage: (usage: Usage) => void;
|
|
18
18
|
}
|
|
19
|
-
export
|
|
19
|
+
export interface AgentContainerSetupResult {
|
|
20
|
+
container: Container;
|
|
21
|
+
/**
|
|
22
|
+
* Unregister module-level listeners registered during setup (remote settings
|
|
23
|
+
* hot-update, auth change). Must be called when the agent is destroyed;
|
|
24
|
+
* without it the module-level callback arrays pin the agent's object graph
|
|
25
|
+
* (the callback closures capture per-agent managers).
|
|
26
|
+
*/
|
|
27
|
+
teardown: () => void;
|
|
28
|
+
}
|
|
29
|
+
export declare function setupAgentContainer(setupOptions: AgentContainerSetupOptions): AgentContainerSetupResult;
|
|
@@ -14,7 +14,7 @@ import { HookManager } from "../managers/hookManager.js";
|
|
|
14
14
|
import { SkillManager } from "../managers/skillManager.js";
|
|
15
15
|
import { SlashCommandManager } from "../managers/slashCommandManager.js";
|
|
16
16
|
import { PluginManager } from "../managers/pluginManager.js";
|
|
17
|
-
import {
|
|
17
|
+
import { BashModeManager } from "../managers/bashModeManager.js";
|
|
18
18
|
import { CronManager } from "../managers/cronManager.js";
|
|
19
19
|
import { WorkflowManager } from "../managers/workflowManager.js";
|
|
20
20
|
import { MemoryRuleManager } from "../managers/MemoryRuleManager.js";
|
|
@@ -38,6 +38,12 @@ export function setupAgentContainer(setupOptions) {
|
|
|
38
38
|
const container = new Container();
|
|
39
39
|
container.register("AgentOptions", options);
|
|
40
40
|
container.register("Workdir", workdir);
|
|
41
|
+
// Module-level listener teardowns collected during setup and returned to the
|
|
42
|
+
// agent, which invokes them in destroy(). The remote-settings callback
|
|
43
|
+
// strongly captures the per-agent LiveConfigManager: without unsubscribing
|
|
44
|
+
// it in destroy(), the module-level callback array keeps every created
|
|
45
|
+
// agent's object graph alive even after the host drops its references.
|
|
46
|
+
const teardowns = [];
|
|
41
47
|
if (options.worktreeName) {
|
|
42
48
|
container.register("WorktreeName", options.worktreeName);
|
|
43
49
|
container.register("MainRepoRoot", getGitMainRepoRoot(workdir));
|
|
@@ -107,14 +113,14 @@ export function setupAgentContainer(setupOptions) {
|
|
|
107
113
|
});
|
|
108
114
|
container.register("McpManager", mcpManager);
|
|
109
115
|
// Wire up auth change callback to refresh/clear remote settings
|
|
110
|
-
authService.onAuthChange(async (event) => {
|
|
116
|
+
teardowns.push(authService.onAuthChange(async (event) => {
|
|
111
117
|
if (event === "login") {
|
|
112
118
|
await remoteSettingsService.refresh();
|
|
113
119
|
}
|
|
114
120
|
else if (event === "logout") {
|
|
115
121
|
remoteSettingsService.clear();
|
|
116
122
|
}
|
|
117
|
-
});
|
|
123
|
+
}));
|
|
118
124
|
const lspManager = options.lspManager || new LspManager(container);
|
|
119
125
|
container.register("LspManager", lspManager);
|
|
120
126
|
const permissionManager = new PermissionManager(container, {
|
|
@@ -234,9 +240,8 @@ export function setupAgentContainer(setupOptions) {
|
|
|
234
240
|
// Same gate for the builtin /artifact skill: refresh emits "refreshed"
|
|
235
241
|
// so slash-command registration follows enableArtifact. Awaited (via the
|
|
236
242
|
// awaited onReload) so skills are re-populated before Agent.create()
|
|
237
|
-
// returns —
|
|
238
|
-
//
|
|
239
|
-
// skill list to callers right after create().
|
|
243
|
+
// returns — otherwise callers right after create() could observe a
|
|
244
|
+
// mid-refresh skill list.
|
|
240
245
|
await skillManager.reloadFeatureGatedSkills().catch((error) => {
|
|
241
246
|
logger.error("Failed to reload feature-gated skills:", error);
|
|
242
247
|
});
|
|
@@ -245,9 +250,12 @@ export function setupAgentContainer(setupOptions) {
|
|
|
245
250
|
container.register("LiveConfigManager", liveConfigManager);
|
|
246
251
|
// Wire up remote settings hot-update: when polling detects changed settings,
|
|
247
252
|
// reload configuration so admin changes propagate to the running agent.
|
|
248
|
-
|
|
253
|
+
// The callback strongly captures the per-agent LiveConfigManager, so it MUST
|
|
254
|
+
// be unsubscribed in destroy() via teardown — otherwise the module-level
|
|
255
|
+
// callback array pins the whole agent object graph.
|
|
256
|
+
teardowns.push(remoteSettingsService.onSettingsUpdate(async () => {
|
|
249
257
|
await liveConfigManager.reload();
|
|
250
|
-
});
|
|
258
|
+
}));
|
|
251
259
|
const subagentManager = new SubagentManager(container, {
|
|
252
260
|
workdir,
|
|
253
261
|
callbacks: {
|
|
@@ -280,12 +288,19 @@ export function setupAgentContainer(setupOptions) {
|
|
|
280
288
|
slashCommandManager.initialize();
|
|
281
289
|
const pluginManager = new PluginManager(container, { workdir });
|
|
282
290
|
container.register("PluginManager", pluginManager);
|
|
283
|
-
const
|
|
284
|
-
container.register("
|
|
291
|
+
const bashModeManager = new BashModeManager(container, { workdir });
|
|
292
|
+
container.register("BashModeManager", bashModeManager);
|
|
285
293
|
const cronManager = new CronManager(container, messageManager.getSessionId());
|
|
286
294
|
container.register("CronManager", cronManager);
|
|
287
295
|
cronManager.start();
|
|
288
296
|
const workflowManager = new WorkflowManager(container);
|
|
289
297
|
container.register("WorkflowManager", workflowManager);
|
|
290
|
-
return
|
|
298
|
+
return {
|
|
299
|
+
container,
|
|
300
|
+
teardown: () => {
|
|
301
|
+
for (const unsubscribe of teardowns) {
|
|
302
|
+
unsubscribe();
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
};
|
|
291
306
|
}
|
|
@@ -26,6 +26,17 @@ export declare function readFirstNLines(filePath: string, maxLines: number): Pro
|
|
|
26
26
|
* @return {Promise<string>} - The last non-empty line of the file, or an empty string if no non-empty lines found.
|
|
27
27
|
*/
|
|
28
28
|
export declare function getLastLine(filePath: string, minLength?: number): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Read up to `maxBytes` from the end of a file and return the lines it holds,
|
|
31
|
+
* oldest first. The window may start in the middle of a line, in which case
|
|
32
|
+
* that partial first line is dropped so every returned line is complete —
|
|
33
|
+
* callers parse them as JSON.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} filePath - The path to the file.
|
|
36
|
+
* @param {number} maxBytes - Size of the tail window (default 64KB).
|
|
37
|
+
* @return {Promise<string[]>} - Trailing non-empty lines, or [] if unreadable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readTailLines(filePath: string, maxBytes?: number): Promise<string[]>;
|
|
29
40
|
/**
|
|
30
41
|
* Suggests similar paths if a file is not found.
|
|
31
42
|
*/
|
package/dist/utils/fileUtils.js
CHANGED
|
@@ -136,6 +136,43 @@ export async function getLastLine(filePath, minLength = 1) {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Read up to `maxBytes` from the end of a file and return the lines it holds,
|
|
141
|
+
* oldest first. The window may start in the middle of a line, in which case
|
|
142
|
+
* that partial first line is dropped so every returned line is complete —
|
|
143
|
+
* callers parse them as JSON.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} filePath - The path to the file.
|
|
146
|
+
* @param {number} maxBytes - Size of the tail window (default 64KB).
|
|
147
|
+
* @return {Promise<string[]>} - Trailing non-empty lines, or [] if unreadable.
|
|
148
|
+
*/
|
|
149
|
+
export async function readTailLines(filePath, maxBytes = 64 * 1024) {
|
|
150
|
+
let fileHandle;
|
|
151
|
+
try {
|
|
152
|
+
const stats = await fs.stat(filePath);
|
|
153
|
+
const fileSize = stats.size;
|
|
154
|
+
if (fileSize === 0)
|
|
155
|
+
return [];
|
|
156
|
+
const readSize = Math.min(maxBytes, fileSize);
|
|
157
|
+
const start = fileSize - readSize;
|
|
158
|
+
fileHandle = await fs.open(filePath, "r");
|
|
159
|
+
const buffer = Buffer.alloc(readSize);
|
|
160
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, start);
|
|
161
|
+
const lines = buffer.subarray(0, bytesRead).toString("utf8").split("\n");
|
|
162
|
+
if (start > 0)
|
|
163
|
+
lines.shift(); // partial line (window began mid-line)
|
|
164
|
+
return lines.map((line) => line.trim()).filter((line) => line !== "");
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Unreadable file (missing, permissions, race with deletion): no tail.
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
if (fileHandle) {
|
|
172
|
+
await fileHandle.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
139
176
|
/**
|
|
140
177
|
* Simple Levenshtein distance implementation
|
|
141
178
|
*/
|
|
@@ -54,21 +54,6 @@ export interface AddErrorBlockParams {
|
|
|
54
54
|
messages: Message[];
|
|
55
55
|
error: string;
|
|
56
56
|
}
|
|
57
|
-
export interface AddBangParams {
|
|
58
|
-
messages: Message[];
|
|
59
|
-
command: string;
|
|
60
|
-
}
|
|
61
|
-
export interface UpdateBangParams {
|
|
62
|
-
messages: Message[];
|
|
63
|
-
command: string;
|
|
64
|
-
output: string;
|
|
65
|
-
}
|
|
66
|
-
export interface CompleteBangParams {
|
|
67
|
-
messages: Message[];
|
|
68
|
-
command: string;
|
|
69
|
-
exitCode: number;
|
|
70
|
-
output?: string;
|
|
71
|
-
}
|
|
72
57
|
/**
|
|
73
58
|
* Convert image file path to base64 format
|
|
74
59
|
* @param imagePath Image file path
|
|
@@ -94,9 +79,6 @@ export declare const updateToolBlockInMessage: ({ messages, id, messageId, param
|
|
|
94
79
|
messageId?: string;
|
|
95
80
|
};
|
|
96
81
|
export declare const addErrorBlockToMessage: ({ messages, error, }: AddErrorBlockParams) => Message[];
|
|
97
|
-
export declare const addBangMessage: ({ messages, command, }: AddBangParams) => Message[];
|
|
98
|
-
export declare const updateBangInMessage: ({ messages, command, output, }: UpdateBangParams) => Message[];
|
|
99
|
-
export declare const completeBangInMessage: ({ messages, command, exitCode, output, }: CompleteBangParams) => Message[];
|
|
100
82
|
/**
|
|
101
83
|
* Helper to count tool blocks in messages
|
|
102
84
|
*/
|
|
@@ -304,64 +304,6 @@ export const addErrorBlockToMessage = ({ messages, error, }) => {
|
|
|
304
304
|
}
|
|
305
305
|
return newMessages;
|
|
306
306
|
};
|
|
307
|
-
// Add bang block to message list
|
|
308
|
-
export const addBangMessage = ({ messages, command, }) => {
|
|
309
|
-
const outputMessage = {
|
|
310
|
-
id: generateMessageId(),
|
|
311
|
-
role: "user",
|
|
312
|
-
blocks: [
|
|
313
|
-
{
|
|
314
|
-
type: "bang",
|
|
315
|
-
command,
|
|
316
|
-
output: "",
|
|
317
|
-
stage: "running",
|
|
318
|
-
exitCode: null,
|
|
319
|
-
},
|
|
320
|
-
],
|
|
321
|
-
timestamp: new Date().toISOString(),
|
|
322
|
-
};
|
|
323
|
-
return [...messages, outputMessage];
|
|
324
|
-
};
|
|
325
|
-
// Update output content of bang block
|
|
326
|
-
export const updateBangInMessage = ({ messages, command, output, }) => {
|
|
327
|
-
const newMessages = [...messages];
|
|
328
|
-
// Find the last user message with a bang block for this command
|
|
329
|
-
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
330
|
-
const msg = newMessages[i];
|
|
331
|
-
if (msg.role === "user") {
|
|
332
|
-
const commandBlock = msg.blocks.find((block) => block.type === "bang" &&
|
|
333
|
-
block.command === command &&
|
|
334
|
-
block.stage === "running");
|
|
335
|
-
if (commandBlock && commandBlock.type === "bang") {
|
|
336
|
-
commandBlock.output = output.trim();
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return newMessages;
|
|
342
|
-
};
|
|
343
|
-
// Complete bang execution, update exit status
|
|
344
|
-
export const completeBangInMessage = ({ messages, command, exitCode, output, }) => {
|
|
345
|
-
const newMessages = [...messages];
|
|
346
|
-
// Find the last user message with a bang block for this command
|
|
347
|
-
for (let i = newMessages.length - 1; i >= 0; i--) {
|
|
348
|
-
const msg = newMessages[i];
|
|
349
|
-
if (msg.role === "user") {
|
|
350
|
-
const commandBlock = msg.blocks.find((block) => block.type === "bang" &&
|
|
351
|
-
block.command === command &&
|
|
352
|
-
block.stage === "running");
|
|
353
|
-
if (commandBlock && commandBlock.type === "bang") {
|
|
354
|
-
commandBlock.stage = "end";
|
|
355
|
-
commandBlock.exitCode = exitCode;
|
|
356
|
-
if (output !== undefined) {
|
|
357
|
-
commandBlock.output = output.trim();
|
|
358
|
-
}
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return newMessages;
|
|
364
|
-
};
|
|
365
307
|
/**
|
|
366
308
|
* Helper to count tool blocks in messages
|
|
367
309
|
*/
|
|
@@ -458,10 +400,6 @@ export function getMessageContent(message) {
|
|
|
458
400
|
if (textBlock && "content" in textBlock) {
|
|
459
401
|
return textBlock.content;
|
|
460
402
|
}
|
|
461
|
-
const bangBlock = message.blocks.find((block) => block.type === "bang");
|
|
462
|
-
if (bangBlock && "command" in bangBlock) {
|
|
463
|
-
return `!${bangBlock.command}`;
|
|
464
|
-
}
|
|
465
403
|
const compactBlock = message.blocks.find((block) => block.type === "compact");
|
|
466
404
|
if (compactBlock && "content" in compactBlock) {
|
|
467
405
|
return compactBlock.content;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, statSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
2
3
|
import { join, extname, basename } from "path";
|
|
3
4
|
import { logger } from "./globalLogger.js";
|
|
4
5
|
import { getBuiltinSubagentsDir } from "./configPaths.js";
|
|
@@ -165,8 +166,14 @@ function scanSubagentDirectory(dirPath, scope) {
|
|
|
165
166
|
export async function loadSubagentConfigurations(workdir, env = process.env) {
|
|
166
167
|
const projectWaveDir = join(workdir, ".wave", "agents");
|
|
167
168
|
const projectClaudeDir = join(workdir, ".claude", "agents");
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
// Resolve user-level dirs via os.homedir() (NOT process.env.HOME): every
|
|
170
|
+
// other user-path resolver in the SDK (settings, skills, MCP, rules, ~/
|
|
171
|
+
// expansion in the Write tool) uses os.homedir(), which on Windows falls
|
|
172
|
+
// back to USERPROFILE. Windows GUI-launched processes (Electron desktop)
|
|
173
|
+
// typically have no HOME set, so process.env.HOME made user-level subagents
|
|
174
|
+
// silently invisible there (`~/.wave/agents` resolved as a relative path).
|
|
175
|
+
const userWaveDir = join(homedir(), ".wave", "agents");
|
|
176
|
+
const userClaudeDir = join(homedir(), ".claude", "agents");
|
|
170
177
|
const builtinDir = getBuiltinSubagentsDir();
|
|
171
178
|
// Load configurations from all sources
|
|
172
179
|
let builtinConfigs = scanSubagentDirectory(builtinDir, "builtin");
|
|
@@ -62,14 +62,6 @@ export function roughTokenCountForMessages(messages) {
|
|
|
62
62
|
total += estimateTokens(block.result);
|
|
63
63
|
}
|
|
64
64
|
break;
|
|
65
|
-
case "bang":
|
|
66
|
-
if (block.command) {
|
|
67
|
-
total += estimateTokens(block.command);
|
|
68
|
-
}
|
|
69
|
-
if (block.output) {
|
|
70
|
-
total += estimateTokens(block.output);
|
|
71
|
-
}
|
|
72
|
-
break;
|
|
73
65
|
case "task_notification":
|
|
74
66
|
total += estimateTokens(block.summary);
|
|
75
67
|
break;
|
|
@@ -47,7 +47,8 @@ export declare function performPostCreationSetup(worktreePath: string, repoRoot:
|
|
|
47
47
|
* is MAX_PATH-limited — deep paths (e.g. node_modules) can fail with "Filename
|
|
48
48
|
* too long", leaving an orphan directory. When git fails we fall back to
|
|
49
49
|
* fs.rmSync with an extended-length path (bypasses MAX_PATH) and prune stale
|
|
50
|
-
* metadata. Failures are logged
|
|
50
|
+
* metadata. Failures are logged; when the directory survives, its branch is
|
|
51
|
+
* deliberately kept so the leftover checkout stays reachable through git.
|
|
51
52
|
*/
|
|
52
53
|
export declare function removeWorktree(info: WorktreeInfo): void;
|
|
53
54
|
/**
|
|
@@ -517,6 +517,36 @@ function toExtendedLengthPath(worktreePath) {
|
|
|
517
517
|
}
|
|
518
518
|
return `\\\\?\\${absolute}`;
|
|
519
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Describe what is still on disk after a failed removal. Without this a log
|
|
522
|
+
* line cannot distinguish "directory already gone" from "the whole checkout is
|
|
523
|
+
* still sitting there".
|
|
524
|
+
*/
|
|
525
|
+
function probeResidue(worktreePath) {
|
|
526
|
+
try {
|
|
527
|
+
if (!fs.existsSync(worktreePath))
|
|
528
|
+
return "none";
|
|
529
|
+
const entries = fs.readdirSync(worktreePath);
|
|
530
|
+
const head = entries.slice(0, 5).join(",");
|
|
531
|
+
return `${entries.length}[${head}${entries.length > 5 ? ",…" : ""}]`;
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
return `unknown(${error.code ?? "error"})`;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/** Delete the worktree directory with fs.rmSync; returns the error, if any. */
|
|
538
|
+
function rmWorktreeDirWithFs(worktreePath) {
|
|
539
|
+
try {
|
|
540
|
+
fs.rmSync(toExtendedLengthPath(worktreePath), {
|
|
541
|
+
recursive: true,
|
|
542
|
+
force: true,
|
|
543
|
+
});
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
return error;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
520
550
|
/**
|
|
521
551
|
* Remove a git worktree and its branch.
|
|
522
552
|
*
|
|
@@ -525,28 +555,36 @@ function toExtendedLengthPath(worktreePath) {
|
|
|
525
555
|
* is MAX_PATH-limited — deep paths (e.g. node_modules) can fail with "Filename
|
|
526
556
|
* too long", leaving an orphan directory. When git fails we fall back to
|
|
527
557
|
* fs.rmSync with an extended-length path (bypasses MAX_PATH) and prune stale
|
|
528
|
-
* metadata. Failures are logged
|
|
558
|
+
* metadata. Failures are logged; when the directory survives, its branch is
|
|
559
|
+
* deliberately kept so the leftover checkout stays reachable through git.
|
|
529
560
|
*/
|
|
530
561
|
export function removeWorktree(info) {
|
|
531
562
|
const repoRoot = info.repoRoot;
|
|
532
|
-
// Get current branch in worktree before removing
|
|
533
|
-
let currentBranch;
|
|
534
|
-
try {
|
|
535
|
-
currentBranch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
536
|
-
cwd: info.path,
|
|
537
|
-
encoding: "utf8",
|
|
538
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
539
|
-
}).trim();
|
|
540
|
-
}
|
|
541
|
-
catch {
|
|
542
|
-
// Ignore errors
|
|
543
|
-
}
|
|
544
563
|
// Remove worktree
|
|
545
564
|
try {
|
|
546
565
|
execFileSync("git", ["worktree", "remove", "--force", info.path], {
|
|
547
566
|
cwd: repoRoot,
|
|
548
567
|
stdio: ["ignore", "pipe", "pipe"],
|
|
549
568
|
});
|
|
569
|
+
// git exits 0 even when it did not delete the directory: on Windows it
|
|
570
|
+
// cannot remove the directory symlinks/junctions that pnpm's node_modules
|
|
571
|
+
// is made of, so it deletes the files, leaves the skeleton behind and still
|
|
572
|
+
// reports success. Verify on disk instead of trusting the exit status.
|
|
573
|
+
if (fs.existsSync(info.path)) {
|
|
574
|
+
logger.warn("git worktree remove reported success but the directory survived, falling back to fs.rmSync:", {
|
|
575
|
+
worktreePath: info.path,
|
|
576
|
+
residue: probeResidue(info.path),
|
|
577
|
+
});
|
|
578
|
+
const rmError = rmWorktreeDirWithFs(info.path);
|
|
579
|
+
if (rmError !== null) {
|
|
580
|
+
logger.error("Failed to remove worktree or branch:", {
|
|
581
|
+
worktreePath: info.path,
|
|
582
|
+
stage: "fs(after git success)",
|
|
583
|
+
error: rmError instanceof Error ? rmError.message : String(rmError),
|
|
584
|
+
residue: probeResidue(info.path),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
550
588
|
}
|
|
551
589
|
catch (error) {
|
|
552
590
|
logger.warn("git worktree remove failed, falling back to fs.rmSync:", {
|
|
@@ -577,7 +615,19 @@ export function removeWorktree(info) {
|
|
|
577
615
|
// Ignore errors
|
|
578
616
|
}
|
|
579
617
|
}
|
|
580
|
-
//
|
|
618
|
+
// The directory, not any exit code, decides whether the removal happened. A
|
|
619
|
+
// surviving directory means its checkout (and any uncommitted work in it) is
|
|
620
|
+
// still on disk, and the branch is the only ref still leading back to it.
|
|
621
|
+
if (fs.existsSync(info.path)) {
|
|
622
|
+
logger.warn(`Worktree directory survived removal — keeping ${info.branch} so the leftover checkout stays reachable:`, {
|
|
623
|
+
worktreePath: info.path,
|
|
624
|
+
residue: probeResidue(info.path),
|
|
625
|
+
});
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
// Delete the worktree's own branch. Branches the user checked out inside the
|
|
629
|
+
// worktree are never touched — they may hold commits that are not reachable
|
|
630
|
+
// from anywhere else (aligned with Claude Code).
|
|
581
631
|
try {
|
|
582
632
|
execFileSync("git", ["branch", "-D", info.branch], {
|
|
583
633
|
cwd: repoRoot,
|
|
@@ -587,26 +637,6 @@ export function removeWorktree(info) {
|
|
|
587
637
|
catch {
|
|
588
638
|
// Ignore errors
|
|
589
639
|
}
|
|
590
|
-
// Delete current branch if different and not protected
|
|
591
|
-
if (currentBranch &&
|
|
592
|
-
currentBranch !== info.branch &&
|
|
593
|
-
currentBranch !== "HEAD") {
|
|
594
|
-
const defaultRemoteBranch = getDefaultRemoteBranch(repoRoot);
|
|
595
|
-
const defaultBranchName = defaultRemoteBranch.split("/").pop();
|
|
596
|
-
if (currentBranch !== defaultBranchName &&
|
|
597
|
-
currentBranch !== "main" &&
|
|
598
|
-
currentBranch !== "master") {
|
|
599
|
-
try {
|
|
600
|
-
execFileSync("git", ["branch", "-D", currentBranch], {
|
|
601
|
-
cwd: repoRoot,
|
|
602
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
catch {
|
|
606
|
-
// Ignore errors
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
640
|
}
|
|
611
641
|
/**
|
|
612
642
|
* Validate that a worktree path is safe to remove before running git removal.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-agent-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "SDK for building AI-powered development tools and agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"types": "./dist/constants/tools.d.ts",
|
|
27
27
|
"default": "./dist/constants/tools.js"
|
|
28
28
|
},
|
|
29
|
+
"./stdio": {
|
|
30
|
+
"types": "./dist/stdio/index.d.ts",
|
|
31
|
+
"default": "./dist/stdio/index.js"
|
|
32
|
+
},
|
|
29
33
|
"./package.json": "./package.json",
|
|
30
34
|
"./*": "./*"
|
|
31
35
|
},
|
|
@@ -50,6 +54,7 @@
|
|
|
50
54
|
"@vscode/ripgrep": "^1.18.0",
|
|
51
55
|
"chokidar": "^4.0.3",
|
|
52
56
|
"cron-parser": "^5.5.0",
|
|
57
|
+
"fflate": "^0.8.3",
|
|
53
58
|
"fuzzysort": "^3.1.0",
|
|
54
59
|
"glob": "^13.0.0",
|
|
55
60
|
"lru-cache": "^11.3.5",
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Container } from "../utils/container.js";
|
|
2
|
-
export interface BangManagerOptions {
|
|
3
|
-
workdir: string;
|
|
4
|
-
}
|
|
5
|
-
export interface CommandExecutionResult {
|
|
6
|
-
exitCode: number;
|
|
7
|
-
output: string;
|
|
8
|
-
}
|
|
9
|
-
export declare class BangManager {
|
|
10
|
-
private container;
|
|
11
|
-
private workdir;
|
|
12
|
-
isCommandRunning: boolean;
|
|
13
|
-
private currentProcess;
|
|
14
|
-
onCommandRunningChange?: (running: boolean) => void;
|
|
15
|
-
constructor(container: Container, options: BangManagerOptions);
|
|
16
|
-
private get messageManager();
|
|
17
|
-
/**
|
|
18
|
-
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
19
|
-
* bang-command subprocesses, so settings `env` vars reach them without
|
|
20
|
-
* polluting other sessions in one `wave --stdio` process.
|
|
21
|
-
*/
|
|
22
|
-
private get sessionEnv();
|
|
23
|
-
private setCommandRunning;
|
|
24
|
-
executeCommand(command: string): Promise<number>;
|
|
25
|
-
abortCommand(): void;
|
|
26
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
|
-
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
3
|
-
export class BangManager {
|
|
4
|
-
constructor(container, options) {
|
|
5
|
-
this.container = container;
|
|
6
|
-
this.isCommandRunning = false;
|
|
7
|
-
this.currentProcess = null;
|
|
8
|
-
this.workdir = options.workdir;
|
|
9
|
-
}
|
|
10
|
-
get messageManager() {
|
|
11
|
-
return this.container.get("MessageManager");
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
15
|
-
* bang-command subprocesses, so settings `env` vars reach them without
|
|
16
|
-
* polluting other sessions in one `wave --stdio` process.
|
|
17
|
-
*/
|
|
18
|
-
get sessionEnv() {
|
|
19
|
-
return (this.container
|
|
20
|
-
.get("ConfigurationService")
|
|
21
|
-
?.getMergedEnv?.() ?? process.env);
|
|
22
|
-
}
|
|
23
|
-
setCommandRunning(isRunning) {
|
|
24
|
-
this.isCommandRunning = isRunning;
|
|
25
|
-
this.onCommandRunningChange?.(isRunning);
|
|
26
|
-
}
|
|
27
|
-
async executeCommand(command) {
|
|
28
|
-
if (this.isCommandRunning) {
|
|
29
|
-
throw new Error("Command already running");
|
|
30
|
-
}
|
|
31
|
-
this.setCommandRunning(true);
|
|
32
|
-
// Add bang placeholder
|
|
33
|
-
this.messageManager.addBangMessage(command);
|
|
34
|
-
return new Promise((resolve) => {
|
|
35
|
-
const child = spawn(command, {
|
|
36
|
-
shell: resolveShellPath() ?? true,
|
|
37
|
-
stdio: "pipe",
|
|
38
|
-
cwd: this.workdir,
|
|
39
|
-
env: {
|
|
40
|
-
...process.env,
|
|
41
|
-
...this.sessionEnv,
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
this.currentProcess = child;
|
|
45
|
-
let outputBuffer = "";
|
|
46
|
-
const updateOutput = (newData) => {
|
|
47
|
-
outputBuffer += newData;
|
|
48
|
-
};
|
|
49
|
-
child.stdout?.on("data", (data) => {
|
|
50
|
-
updateOutput(data.toString());
|
|
51
|
-
});
|
|
52
|
-
child.stderr?.on("data", (data) => {
|
|
53
|
-
updateOutput(data.toString());
|
|
54
|
-
});
|
|
55
|
-
child.on("exit", (code, signal) => {
|
|
56
|
-
const exitCode = code === null && signal ? 130 : (code ?? 0);
|
|
57
|
-
this.messageManager.completeBangMessage(command, exitCode, outputBuffer);
|
|
58
|
-
this.setCommandRunning(false);
|
|
59
|
-
this.currentProcess = null;
|
|
60
|
-
resolve(exitCode);
|
|
61
|
-
});
|
|
62
|
-
child.on("error", (error) => {
|
|
63
|
-
updateOutput(`\nError: ${error.message}\n`);
|
|
64
|
-
this.messageManager.completeBangMessage(command, 1, outputBuffer);
|
|
65
|
-
this.setCommandRunning(false);
|
|
66
|
-
this.currentProcess = null;
|
|
67
|
-
resolve(1);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
abortCommand() {
|
|
72
|
-
if (this.currentProcess && this.isCommandRunning) {
|
|
73
|
-
this.currentProcess.kill("SIGKILL");
|
|
74
|
-
this.currentProcess = null;
|
|
75
|
-
this.isCommandRunning = false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|