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
@@ -16,4 +16,14 @@ export interface AgentContainerSetupOptions {
16
16
  addPermissionRule: (rule: string) => Promise<void>;
17
17
  addUsage: (usage: Usage) => void;
18
18
  }
19
- export declare function setupAgentContainer(setupOptions: AgentContainerSetupOptions): Container;
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 { BangManager } from "../managers/bangManager.js";
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 — reloadFeatureGatedSkills() clears the skill map before
238
- // rediscovering asynchronously, which would otherwise expose an empty
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
- remoteSettingsService.onSettingsUpdate(async () => {
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 bangManager = new BangManager(container, { workdir });
284
- container.register("BangManager", bangManager);
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 container;
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
  */
@@ -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
- const userWaveDir = join(process.env.HOME || "~", ".wave", "agents");
169
- const userClaudeDir = join(process.env.HOME || "~", ".claude", "agents");
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 but never block branch deletion.
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 but never block branch deletion.
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
- // Delete worktree branch
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.1.4",
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
- }