wave-agent-sdk 0.11.6 → 0.11.7

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 (105) hide show
  1. package/builtin/skills/init/SKILL.md +2 -0
  2. package/builtin/skills/settings/SKILLS.md +3 -2
  3. package/builtin/skills/settings/SUBAGENTS.md +1 -3
  4. package/dist/agent.d.ts +6 -0
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +18 -1
  7. package/dist/constants/tools.d.ts +1 -1
  8. package/dist/constants/tools.d.ts.map +1 -1
  9. package/dist/constants/tools.js +1 -1
  10. package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
  11. package/dist/managers/MemoryRuleManager.js +1 -9
  12. package/dist/managers/aiManager.d.ts.map +1 -1
  13. package/dist/managers/aiManager.js +22 -3
  14. package/dist/managers/messageManager.d.ts +13 -5
  15. package/dist/managers/messageManager.d.ts.map +1 -1
  16. package/dist/managers/messageManager.js +62 -34
  17. package/dist/managers/pluginManager.d.ts.map +1 -1
  18. package/dist/managers/pluginManager.js +4 -2
  19. package/dist/managers/slashCommandManager.d.ts +2 -0
  20. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  21. package/dist/managers/slashCommandManager.js +98 -4
  22. package/dist/managers/toolManager.d.ts.map +1 -1
  23. package/dist/managers/toolManager.js +8 -2
  24. package/dist/prompts/index.d.ts +2 -0
  25. package/dist/prompts/index.d.ts.map +1 -1
  26. package/dist/prompts/index.js +5 -0
  27. package/dist/services/GitService.d.ts +1 -0
  28. package/dist/services/GitService.d.ts.map +1 -1
  29. package/dist/services/GitService.js +16 -0
  30. package/dist/services/MarketplaceService.d.ts +7 -0
  31. package/dist/services/MarketplaceService.d.ts.map +1 -1
  32. package/dist/services/MarketplaceService.js +321 -252
  33. package/dist/services/aiService.d.ts +34 -0
  34. package/dist/services/aiService.d.ts.map +1 -1
  35. package/dist/services/aiService.js +124 -1
  36. package/dist/services/initializationService.d.ts.map +1 -1
  37. package/dist/services/initializationService.js +18 -0
  38. package/dist/tools/agentTool.js +3 -3
  39. package/dist/tools/bashTool.d.ts.map +1 -1
  40. package/dist/tools/bashTool.js +4 -4
  41. package/dist/tools/editTool.d.ts.map +1 -1
  42. package/dist/tools/editTool.js +2 -0
  43. package/dist/tools/globTool.d.ts.map +1 -1
  44. package/dist/tools/globTool.js +15 -3
  45. package/dist/tools/grepTool.d.ts.map +1 -1
  46. package/dist/tools/grepTool.js +38 -12
  47. package/dist/tools/readTool.d.ts.map +1 -1
  48. package/dist/tools/readTool.js +61 -0
  49. package/dist/tools/skillTool.js +2 -2
  50. package/dist/tools/types.d.ts +16 -0
  51. package/dist/tools/types.d.ts.map +1 -1
  52. package/dist/tools/webFetchTool.d.ts +3 -0
  53. package/dist/tools/webFetchTool.d.ts.map +1 -0
  54. package/dist/tools/webFetchTool.js +171 -0
  55. package/dist/tools/writeTool.d.ts.map +1 -1
  56. package/dist/tools/writeTool.js +2 -0
  57. package/dist/types/commands.d.ts +1 -1
  58. package/dist/types/commands.d.ts.map +1 -1
  59. package/dist/types/messaging.d.ts +1 -0
  60. package/dist/types/messaging.d.ts.map +1 -1
  61. package/dist/utils/bashParser.d.ts +14 -0
  62. package/dist/utils/bashParser.d.ts.map +1 -1
  63. package/dist/utils/bashParser.js +243 -142
  64. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  65. package/dist/utils/convertMessagesForAPI.js +7 -0
  66. package/dist/utils/fileUtils.d.ts +8 -0
  67. package/dist/utils/fileUtils.d.ts.map +1 -1
  68. package/dist/utils/fileUtils.js +52 -0
  69. package/dist/utils/messageOperations.d.ts +12 -3
  70. package/dist/utils/messageOperations.d.ts.map +1 -1
  71. package/dist/utils/messageOperations.js +77 -9
  72. package/package.json +4 -2
  73. package/src/agent.ts +19 -1
  74. package/src/constants/tools.ts +1 -1
  75. package/src/managers/MemoryRuleManager.ts +1 -10
  76. package/src/managers/aiManager.ts +23 -3
  77. package/src/managers/messageManager.ts +76 -38
  78. package/src/managers/pluginManager.ts +4 -2
  79. package/src/managers/slashCommandManager.ts +130 -4
  80. package/src/managers/toolManager.ts +11 -2
  81. package/src/prompts/index.ts +6 -0
  82. package/src/services/GitService.ts +20 -0
  83. package/src/services/MarketplaceService.ts +397 -324
  84. package/src/services/aiService.ts +197 -1
  85. package/src/services/initializationService.ts +38 -0
  86. package/src/tools/agentTool.ts +3 -3
  87. package/src/tools/bashTool.ts +3 -4
  88. package/src/tools/editTool.ts +3 -0
  89. package/src/tools/globTool.ts +16 -3
  90. package/src/tools/grepTool.ts +41 -13
  91. package/src/tools/readTool.ts +69 -0
  92. package/src/tools/skillTool.ts +2 -2
  93. package/src/tools/types.ts +13 -0
  94. package/src/tools/webFetchTool.ts +194 -0
  95. package/src/tools/writeTool.ts +3 -0
  96. package/src/types/commands.ts +1 -1
  97. package/src/types/messaging.ts +1 -0
  98. package/src/utils/bashParser.ts +268 -157
  99. package/src/utils/convertMessagesForAPI.ts +8 -0
  100. package/src/utils/fileUtils.ts +69 -0
  101. package/src/utils/messageOperations.ts +84 -9
  102. package/dist/tools/taskOutputTool.d.ts +0 -3
  103. package/dist/tools/taskOutputTool.d.ts.map +0 -1
  104. package/dist/tools/taskOutputTool.js +0 -198
  105. package/src/tools/taskOutputTool.ts +0 -222
@@ -44,7 +44,7 @@ export const convertImageToBase64 = (imagePath) => {
44
44
  };
45
45
  export const generateMessageId = () => `msg-${randomUUID()}`;
46
46
  // Add user message
47
- export const addUserMessageToMessages = ({ messages, content, images, customCommandContent, source, id, }) => {
47
+ export const addUserMessageToMessages = ({ messages, content, images, customCommandContent, source, id, isMeta, }) => {
48
48
  const blocks = [];
49
49
  // Create text block with optional customCommandContent and source
50
50
  const textBlock = {
@@ -66,6 +66,7 @@ export const addUserMessageToMessages = ({ messages, content, images, customComm
66
66
  id: id || generateMessageId(),
67
67
  role: "user",
68
68
  blocks,
69
+ ...(isMeta !== undefined && { isMeta }),
69
70
  };
70
71
  return [...messages, userMessage];
71
72
  };
@@ -88,7 +89,11 @@ export const updateUserMessageInMessages = (messages, id, params) => {
88
89
  }
89
90
  return block;
90
91
  });
91
- return { ...msg, blocks: newBlocks };
92
+ return {
93
+ ...msg,
94
+ blocks: newBlocks,
95
+ ...(params.isMeta !== undefined && { isMeta: params.isMeta }),
96
+ };
92
97
  }
93
98
  return msg;
94
99
  });
@@ -122,12 +127,74 @@ export const addAssistantMessageToMessages = (messages, content, toolCalls, usag
122
127
  };
123
128
  return [...messages, initialAssistantMessage];
124
129
  };
125
- // Update Tool Block of the last assistant message
126
- export const updateToolBlockInMessage = ({ messages, id, parameters, result, success, error, stage, name, shortResult, startLineNumber, images, compactParams, parametersChunk, isManuallyBackgrounded, }) => {
130
+ /**
131
+ * Add a tool block to a specific message by ID.
132
+ */
133
+ export const addToolBlockToMessageInMessages = (messages, messageId, params) => {
134
+ const toolBlockId = randomUUID();
135
+ const newMessages = messages.map((msg) => {
136
+ if (msg.id === messageId) {
137
+ return {
138
+ ...msg,
139
+ blocks: [
140
+ ...msg.blocks,
141
+ {
142
+ type: "tool",
143
+ id: toolBlockId,
144
+ name: params.name || "unknown",
145
+ parameters: params.parameters || "",
146
+ result: params.result || "",
147
+ stage: "start",
148
+ ...params,
149
+ },
150
+ ],
151
+ };
152
+ }
153
+ return msg;
154
+ });
155
+ return { messages: newMessages, toolBlockId };
156
+ };
157
+ // Update Tool Block of the last assistant or user message
158
+ export const updateToolBlockInMessage = ({ messages, id, messageId, parameters, result, success, error, stage, name, shortResult, startLineNumber, images, compactParams, parametersChunk, isManuallyBackgrounded, }) => {
127
159
  const newMessages = [...messages];
128
- // Find the last assistant message
160
+ // If messageId is provided, target that specific message
161
+ if (messageId) {
162
+ const messageIndex = newMessages.findIndex((msg) => msg.id === messageId);
163
+ if (messageIndex !== -1) {
164
+ const toolBlockIndex = newMessages[messageIndex].blocks.findIndex((block) => block.type === "tool" && block.id === id);
165
+ if (toolBlockIndex !== -1) {
166
+ const toolBlock = newMessages[messageIndex].blocks[toolBlockIndex];
167
+ if (toolBlock.type === "tool") {
168
+ if (parameters !== undefined)
169
+ toolBlock.parameters = parameters;
170
+ if (result !== undefined)
171
+ toolBlock.result = result;
172
+ if (shortResult !== undefined)
173
+ toolBlock.shortResult = shortResult;
174
+ if (startLineNumber !== undefined)
175
+ toolBlock.startLineNumber = startLineNumber;
176
+ if (images !== undefined)
177
+ toolBlock.images = images;
178
+ if (success !== undefined)
179
+ toolBlock.success = success;
180
+ if (error !== undefined)
181
+ toolBlock.error = error;
182
+ if (stage !== undefined)
183
+ toolBlock.stage = stage;
184
+ if (compactParams !== undefined)
185
+ toolBlock.compactParams = compactParams;
186
+ if (parametersChunk !== undefined)
187
+ toolBlock.parametersChunk = parametersChunk;
188
+ if (isManuallyBackgrounded !== undefined)
189
+ toolBlock.isManuallyBackgrounded = isManuallyBackgrounded;
190
+ }
191
+ }
192
+ }
193
+ return newMessages;
194
+ }
195
+ // Find the last assistant or user message
129
196
  for (let i = newMessages.length - 1; i >= 0; i--) {
130
- if (newMessages[i].role === "assistant") {
197
+ if (newMessages[i].role === "assistant" || newMessages[i].role === "user") {
131
198
  const toolBlockIndex = newMessages[i].blocks.findIndex((block) => block.type === "tool" && block.id === id);
132
199
  if (toolBlockIndex !== -1) {
133
200
  const toolBlock = newMessages[i].blocks[toolBlockIndex];
@@ -155,9 +222,10 @@ export const updateToolBlockInMessage = ({ messages, id, parameters, result, suc
155
222
  if (isManuallyBackgrounded !== undefined)
156
223
  toolBlock.isManuallyBackgrounded = isManuallyBackgrounded;
157
224
  }
225
+ break; // Found and updated, stop searching
158
226
  }
159
- else {
160
- // If existing block not found, create new one
227
+ else if (newMessages[i].role === "assistant") {
228
+ // If existing block not found in assistant message, create new one
161
229
  // This handles cases where we're streaming tool parameters before execution
162
230
  newMessages[i].blocks.push({
163
231
  type: "tool",
@@ -175,8 +243,8 @@ export const updateToolBlockInMessage = ({ messages, id, parameters, result, suc
175
243
  parametersChunk: parametersChunk,
176
244
  isManuallyBackgrounded: isManuallyBackgrounded,
177
245
  });
246
+ break; // Created and added, stop searching
178
247
  }
179
- break;
180
248
  }
181
249
  }
182
250
  return newMessages;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.11.6",
3
+ "version": "0.11.7",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -34,9 +34,11 @@
34
34
  "fuzzysort": "^3.1.0",
35
35
  "glob": "^13.0.0",
36
36
  "minimatch": "^10.0.3",
37
- "openai": "^5.12.2"
37
+ "openai": "^5.12.2",
38
+ "turndown": "^7.2.2"
38
39
  },
39
40
  "devDependencies": {
41
+ "@types/turndown": "^5.0.6",
40
42
  "@vitest/coverage-v8": "^4.0.18",
41
43
  "rimraf": "^6.1.2",
42
44
  "tsc-alias": "^1.8.16",
package/src/agent.ts CHANGED
@@ -35,6 +35,8 @@ import { LiveConfigManager } from "./managers/liveConfigManager.js";
35
35
  import { configValidator } from "./utils/configValidator.js";
36
36
  import { SkillManager } from "./managers/skillManager.js";
37
37
  import { TaskManager } from "./services/taskManager.js";
38
+ import { btw } from "./services/aiService.js";
39
+ import { convertMessagesForAPI } from "./utils/convertMessagesForAPI.js";
38
40
  import { InitializationService } from "./services/initializationService.js";
39
41
  import { InteractionService } from "./services/interactionService.js";
40
42
  import { ConfigurationService } from "./services/configurationService.js";
@@ -196,7 +198,7 @@ export class Agent {
196
198
  }
197
199
 
198
200
  public get latestTotalTokens(): number {
199
- return this.messageManager.getlatestTotalTokens();
201
+ return this.messageManager.getLatestTotalTokens();
200
202
  }
201
203
 
202
204
  /** Get working directory */
@@ -495,6 +497,22 @@ export class Agent {
495
497
  this.messageManager.triggerShowRewind();
496
498
  }
497
499
 
500
+ /**
501
+ * Ask a side question without tool use
502
+ * @param question - The side question to ask
503
+ * @returns Promise that resolves to the AI's answer
504
+ */
505
+ public async askBtw(question: string): Promise<string> {
506
+ const messages = convertMessagesForAPI(this.messageManager.getMessages());
507
+ const result = await btw({
508
+ gatewayConfig: this.getGatewayConfig(),
509
+ modelConfig: this.getModelConfig(),
510
+ messages,
511
+ question,
512
+ });
513
+ return result.content;
514
+ }
515
+
498
516
  /**
499
517
  * Send a message to the AI agent with optional images
500
518
  *
@@ -1,6 +1,5 @@
1
1
  export const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
2
2
  export const BASH_TOOL_NAME = "Bash";
3
- export const TASK_OUTPUT_TOOL_NAME = "TaskOutput";
4
3
  export const TASK_STOP_TOOL_NAME = "TaskStop";
5
4
  export const EDIT_TOOL_NAME = "Edit";
6
5
  export const EXIT_PLAN_MODE_TOOL_NAME = "ExitPlanMode";
@@ -18,3 +17,4 @@ export const WRITE_TOOL_NAME = "Write";
18
17
  export const CRON_CREATE_TOOL_NAME = "CronCreate";
19
18
  export const CRON_DELETE_TOOL_NAME = "CronDelete";
20
19
  export const CRON_LIST_TOOL_NAME = "CronList";
20
+ export const WEB_FETCH_TOOL_NAME = "WebFetch";
@@ -55,17 +55,8 @@ export class MemoryRuleManager {
55
55
 
56
56
  this.state.rules = newRules;
57
57
  const ruleCount = Object.keys(newRules).length;
58
+ // Removed verbose logging of all discovered rules
58
59
  logger.debug(`Discovered ${ruleCount} modular memory rules`);
59
-
60
- if (ruleCount > 0) {
61
- logger.debug("Loaded memory rules:");
62
- for (const [id, rule] of Object.entries(newRules)) {
63
- const pathInfo = rule.metadata.paths
64
- ? `paths: ${JSON.stringify(rule.metadata.paths)}`
65
- : "global (no path restriction)";
66
- logger.debug(` - ${id} (${rule.source}): ${pathInfo}`);
67
- }
68
- }
69
60
  }
70
61
 
71
62
  private async scanDirectory(
@@ -339,6 +339,25 @@ export class AIManager {
339
339
  return;
340
340
  }
341
341
 
342
+ // Scan for file mentions in the last user message
343
+ if (recursionDepth === 0) {
344
+ const messages = this.messageManager.getMessages();
345
+ const lastMessage = messages[messages.length - 1];
346
+ if (lastMessage && lastMessage.role === "user") {
347
+ for (const block of lastMessage.blocks) {
348
+ if (block.type === "text") {
349
+ const content = block.content;
350
+ const fileMentionRegex = /(?:^|\s)@([\w.\-/]+)/g;
351
+ let match;
352
+ while ((match = fileMentionRegex.exec(content)) !== null) {
353
+ const filePath = match[1];
354
+ this.messageManager.touchFile(filePath);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+
342
361
  // Save session in each recursion to ensure message persistence
343
362
  await this.messageManager.saveSession();
344
363
 
@@ -798,11 +817,12 @@ export class AIManager {
798
817
  "Some tools were manually backgrounded, stopping recursion.",
799
818
  );
800
819
  } else if (!isCurrentlyAborted) {
801
- // If response was truncated and no tools were called, add a continuation message
802
- if (result.finish_reason === "length" && toolCalls.length === 0) {
820
+ // If response was truncated, add a hidden continuation message
821
+ if (result.finish_reason === "length") {
803
822
  this.messageManager.addUserMessage({
804
823
  content:
805
- "Your response was cut off because it exceeded the output token limit. Please break your work into smaller pieces. Continue from where you left off.",
824
+ "Output token limit hit. Resume directly no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.",
825
+ isMeta: true,
806
826
  });
807
827
  }
808
828
 
@@ -8,12 +8,13 @@ import {
8
8
  updateBangInMessage,
9
9
  completeBangInMessage,
10
10
  removeLastUserMessage,
11
+ addToolBlockToMessageInMessages,
11
12
  UserMessageParams,
12
13
  type AgentToolBlockUpdateParams,
13
14
  generateMessageId,
14
15
  } from "../utils/messageOperations.js";
15
16
  import type { Message, Usage } from "../types/index.js";
16
- import { join } from "path";
17
+ import { join, isAbsolute, relative } from "path";
17
18
  import {
18
19
  appendMessages,
19
20
  createSession,
@@ -137,7 +138,7 @@ export class MessageManager {
137
138
  return [...this._usages];
138
139
  }
139
140
 
140
- public getlatestTotalTokens(): number {
141
+ public getLatestTotalTokens(): number {
141
142
  return this.latestTotalTokens;
142
143
  }
143
144
 
@@ -172,12 +173,7 @@ export class MessageManager {
172
173
  const filesInContext = this.getFilesInContext();
173
174
  const activeRules = this.memoryRuleManager.getActiveRules(filesInContext);
174
175
  if (activeRules.length > 0) {
175
- logger?.debug(
176
- `Active modular rules (${activeRules.length}): ${activeRules.map((r) => r.id).join(", ")}`,
177
- );
178
- if (combined) {
179
- combined += "\n\n";
180
- }
176
+ combined += "\n\n";
181
177
  combined += activeRules.map((r) => r.content).join("\n\n");
182
178
  }
183
179
  }
@@ -221,9 +217,53 @@ export class MessageManager {
221
217
  }
222
218
  }
223
219
 
220
+ /**
221
+ * Adds a file path to the set of files in context, normalizing it if necessary.
222
+ */
223
+ public touchFile(filePath: string): void {
224
+ const normalizedPath = isAbsolute(filePath)
225
+ ? relative(this.workdir, filePath)
226
+ : filePath;
227
+ this.filesInContext.add(normalizedPath);
228
+ }
229
+
230
+ /**
231
+ * Extracts and adds file paths from a message's tool blocks.
232
+ */
233
+ private addPathsFromMessage(message: Message): void {
234
+ for (const block of message.blocks) {
235
+ if (block.type === "tool" && block.parameters) {
236
+ try {
237
+ const params = JSON.parse(block.parameters) as Record<
238
+ string,
239
+ unknown
240
+ >;
241
+ const paths = this.extractPathsFromParams(params);
242
+ for (const p of paths) {
243
+ this.touchFile(p);
244
+ }
245
+ } catch {
246
+ // Ignore parse errors
247
+ }
248
+ }
249
+ }
250
+ }
251
+
224
252
  public setMessages(messages: Message[]): void {
253
+ const oldLength = this.messages.length;
225
254
  this.messages = [...messages];
226
- this.updateFilesInContext(messages);
255
+
256
+ // Incrementally add paths from new messages
257
+ const newMessages = messages.slice(oldLength);
258
+ for (const message of newMessages) {
259
+ this.addPathsFromMessage(message);
260
+ }
261
+
262
+ // Also check if the last message was updated (common for tool blocks)
263
+ if (messages.length > 0 && messages.length === oldLength) {
264
+ this.addPathsFromMessage(messages[messages.length - 1]);
265
+ }
266
+
227
267
  this.callbacks.onMessagesChange?.([...messages]);
228
268
  }
229
269
 
@@ -296,7 +336,6 @@ export class MessageManager {
296
336
  this.rootSessionId = sessionData.rootSessionId || sessionData.id;
297
337
  this.parentSessionId = sessionData.parentSessionId;
298
338
  this.setMessages([...sessionData.messages]);
299
- this.updateFilesInContext(sessionData.messages);
300
339
  this.setlatestTotalTokens(sessionData.metadata.latestTotalTokens);
301
340
 
302
341
  // Set saved message count to the number of loaded messages since they're already saved
@@ -401,6 +440,20 @@ export class MessageManager {
401
440
  // Note: Subagent-specific callbacks are now handled by SubagentManager
402
441
  }
403
442
 
443
+ /**
444
+ * Add a tool block to a specific message by ID.
445
+ */
446
+ public addToolBlockToMessage(
447
+ messageId: string,
448
+ params: Omit<AgentToolBlockUpdateParams, "id">,
449
+ ): string {
450
+ const { messages: newMessages, toolBlockId } =
451
+ addToolBlockToMessageInMessages(this.messages, messageId, params);
452
+ this.setMessages(newMessages);
453
+ this.callbacks.onToolBlockUpdated?.({ ...params, id: toolBlockId });
454
+ return toolBlockId;
455
+ }
456
+
404
457
  public addErrorBlock(error: string): void {
405
458
  const newMessages = addErrorBlockToMessage({
406
459
  messages: this.messages,
@@ -462,6 +515,19 @@ export class MessageManager {
462
515
  // Set new message list
463
516
  this.setMessages(newMessages);
464
517
 
518
+ // Reset and re-populate filesInContext
519
+ this.filesInContext.clear();
520
+ for (const message of this.messages) {
521
+ this.addPathsFromMessage(message);
522
+ }
523
+
524
+ // Scan compressedContent for file mentions
525
+ const fileMentionRegex = /(?:^|\s)@([\w.\-/]+)/g;
526
+ let match;
527
+ while ((match = fileMentionRegex.exec(compressedContent)) !== null) {
528
+ this.touchFile(match[1]);
529
+ }
530
+
465
531
  // Trigger compression callback
466
532
  this.callbacks.onCompressBlockAdded?.(compressedContent);
467
533
  }
@@ -794,34 +860,6 @@ export class MessageManager {
794
860
  }
795
861
  }
796
862
 
797
- /**
798
- * Updates the set of files mentioned in the conversation.
799
- */
800
- private updateFilesInContext(messages: Message[]): void {
801
- this.filesInContext.clear();
802
- for (const message of messages) {
803
- for (const block of message.blocks) {
804
- if (block.type === "tool") {
805
- // Extract file paths from common tool parameters
806
- if (block.parameters) {
807
- try {
808
- const params = JSON.parse(block.parameters) as Record<
809
- string,
810
- unknown
811
- >;
812
- const paths = this.extractPathsFromParams(params);
813
- for (const p of paths) {
814
- this.filesInContext.add(p);
815
- }
816
- } catch {
817
- // Ignore parse errors
818
- }
819
- }
820
- }
821
- }
822
- }
823
- }
824
-
825
863
  private extractPathsFromParams(params: Record<string, unknown>): string[] {
826
864
  const paths: string[] = [];
827
865
  if (typeof params !== "object" || params === null) return paths;
@@ -74,9 +74,11 @@ export class PluginManager {
74
74
 
75
75
  const marketplaceService = new MarketplaceService();
76
76
 
77
- // Trigger auto-update for marketplaces
77
+ // Trigger auto-update for marketplaces in the background
78
78
  if (!process.env.VITEST) {
79
- await marketplaceService.autoUpdateAll();
79
+ marketplaceService.autoUpdateAll().catch((error) => {
80
+ logger?.error("Background marketplace auto-update failed:", error);
81
+ });
80
82
  }
81
83
 
82
84
  let installedRegistry = await marketplaceService.getInstalledPlugins();
@@ -16,8 +16,13 @@ import {
16
16
  replaceBashCommandsWithOutput,
17
17
  executeBashCommands,
18
18
  } from "../utils/markdownParser.js";
19
+ import {
20
+ countToolBlocks,
21
+ formatToolTokenSummary,
22
+ } from "../utils/messageOperations.js";
19
23
  import type { SkillManager } from "./skillManager.js";
20
24
  import type { SkillMetadata } from "../types/skills.js";
25
+ import type { SubagentManager } from "./subagentManager.js";
21
26
 
22
27
  import { logger } from "../utils/globalLogger.js";
23
28
 
@@ -30,6 +35,7 @@ export class SlashCommandManager {
30
35
  private customCommands = new Map<string, CustomSlashCommand>();
31
36
  private skillCommandIds = new Set<string>();
32
37
  private workdir: string;
38
+ private currentCommandAbortController: AbortController | null = null;
33
39
 
34
40
  constructor(
35
41
  private container: Container,
@@ -71,6 +77,10 @@ export class SlashCommandManager {
71
77
  return this.container.get<SkillManager>("SkillManager")!;
72
78
  }
73
79
 
80
+ private get subagentManager(): SubagentManager {
81
+ return this.container.get<SubagentManager>("SubagentManager")!;
82
+ }
83
+
74
84
  private initializeBuiltinCommands(): void {
75
85
  // Register built-in clear command
76
86
  this.registerCommand({
@@ -153,7 +163,7 @@ export class SlashCommandManager {
153
163
  id: commandId,
154
164
  name: skill.name,
155
165
  description: `Skill: ${skill.description}`,
156
- handler: async (args?: string) => {
166
+ handler: async (args?: string, signal?: AbortSignal) => {
157
167
  try {
158
168
  // 1. Prepare skill content immediately
159
169
  const prepared = await this.skillManager.prepareSkill({
@@ -176,6 +186,109 @@ export class SlashCommandManager {
176
186
  return;
177
187
  }
178
188
 
189
+ if (skill.context === "fork") {
190
+ // Forked skill execution
191
+ const subagentConfigs =
192
+ await this.subagentManager.loadConfigurations();
193
+ const subagentType = skill.agent || "general-purpose";
194
+ const config = subagentConfigs.find(
195
+ (c) => c.name === subagentType,
196
+ );
197
+ if (!config) {
198
+ throw new Error(
199
+ `Subagent configuration for ${subagentType} not found`,
200
+ );
201
+ }
202
+
203
+ // Add a ToolBlock to the initial command message for progress tracking
204
+ const toolBlockId = this.messageManager.addToolBlockToMessage(
205
+ messageId,
206
+ {
207
+ name: subagentType,
208
+ parameters: JSON.stringify({
209
+ description: skill.description,
210
+ prompt: prepared.content,
211
+ subagent_type: subagentType,
212
+ }),
213
+ stage: "running",
214
+ },
215
+ );
216
+
217
+ try {
218
+ const instance = await this.subagentManager.createInstance(
219
+ config,
220
+ {
221
+ description: skill.description,
222
+ prompt: prepared.content,
223
+ subagent_type: subagentType,
224
+ model: skill.model,
225
+ },
226
+ false,
227
+ () => {
228
+ // Update the tool block with progress
229
+ const subagent = this.subagentManager.getInstance(
230
+ instance.subagentId,
231
+ );
232
+ if (subagent) {
233
+ const messages = subagent.messages;
234
+ const tokens =
235
+ subagent.messageManager.getLatestTotalTokens();
236
+ const lastTools = subagent.lastTools;
237
+
238
+ const toolCount = countToolBlocks(messages);
239
+ const summary = formatToolTokenSummary(toolCount, tokens);
240
+
241
+ let shortResult = "";
242
+ if (toolCount > 2) {
243
+ shortResult += "... ";
244
+ }
245
+ if (lastTools.length > 0) {
246
+ shortResult += `${lastTools.join(", ")} `;
247
+ }
248
+
249
+ shortResult += summary;
250
+
251
+ this.messageManager.updateToolBlock({
252
+ id: toolBlockId,
253
+ messageId,
254
+ shortResult,
255
+ });
256
+ }
257
+ },
258
+ );
259
+
260
+ try {
261
+ const result = await this.subagentManager.executeAgent(
262
+ instance,
263
+ prepared.content,
264
+ signal,
265
+ );
266
+
267
+ // Update the ToolBlock with final result
268
+ this.messageManager.updateToolBlock({
269
+ id: toolBlockId,
270
+ messageId,
271
+ result,
272
+ success: true,
273
+ stage: "end",
274
+ });
275
+ } finally {
276
+ this.subagentManager.cleanupInstance(instance.subagentId);
277
+ }
278
+ } catch (error) {
279
+ // Update the ToolBlock with error
280
+ this.messageManager.updateToolBlock({
281
+ id: toolBlockId,
282
+ messageId,
283
+ success: false,
284
+ error: error instanceof Error ? error.message : String(error),
285
+ stage: "end",
286
+ });
287
+ throw error; // Re-throw to be caught by outer catch for logging/error block
288
+ }
289
+ return;
290
+ }
291
+
179
292
  // 3. Execute bash commands asynchronously
180
293
  const result = await this.skillManager.executeSkill({
181
294
  skill_name: skill.name,
@@ -228,7 +341,6 @@ export class SlashCommandManager {
228
341
  command.description ||
229
342
  `Plugin command: ${namespacedName}${hasParameterPlaceholders(command.content) ? " (supports parameters)" : ""}`;
230
343
 
231
- // Register as a regular command with a handler that executes the custom command
232
344
  this.registerCommand({
233
345
  id: namespacedId,
234
346
  name: namespacedName,
@@ -313,12 +425,22 @@ export class SlashCommandManager {
313
425
  return false;
314
426
  }
315
427
 
428
+ // Abort any previous command if it's still running
429
+ this.currentCommandAbortController?.abort();
430
+ this.currentCommandAbortController = new AbortController();
431
+
316
432
  try {
317
- await command.handler(args);
433
+ await command.handler(args, this.currentCommandAbortController.signal);
318
434
  return true;
319
435
  } catch (error) {
320
- console.error(`Failed to execute slash command ${commandId}:`, error);
436
+ if (error instanceof Error && error.name === "AbortError") {
437
+ logger?.debug(`Slash command ${commandId} was aborted`);
438
+ } else {
439
+ console.error(`Failed to execute slash command ${commandId}:`, error);
440
+ }
321
441
  return false;
442
+ } finally {
443
+ this.currentCommandAbortController = null;
322
444
  }
323
445
  }
324
446
 
@@ -429,5 +551,9 @@ export class SlashCommandManager {
429
551
  public abortCurrentCommand(): void {
430
552
  // Abort the AI manager if it's running
431
553
  this.aiManager.abortAIMessage();
554
+
555
+ // Abort the current slash command handler
556
+ this.currentCommandAbortController?.abort();
557
+ this.currentCommandAbortController = null;
432
558
  }
433
559
  }