wave-agent-sdk 0.11.5 → 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 (110) 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/mcpManager.d.ts.map +1 -1
  15. package/dist/managers/mcpManager.js +32 -13
  16. package/dist/managers/messageManager.d.ts +13 -5
  17. package/dist/managers/messageManager.d.ts.map +1 -1
  18. package/dist/managers/messageManager.js +62 -34
  19. package/dist/managers/permissionManager.js +4 -4
  20. package/dist/managers/pluginManager.d.ts.map +1 -1
  21. package/dist/managers/pluginManager.js +4 -2
  22. package/dist/managers/slashCommandManager.d.ts +2 -0
  23. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  24. package/dist/managers/slashCommandManager.js +98 -4
  25. package/dist/managers/toolManager.d.ts.map +1 -1
  26. package/dist/managers/toolManager.js +8 -2
  27. package/dist/prompts/index.d.ts +2 -0
  28. package/dist/prompts/index.d.ts.map +1 -1
  29. package/dist/prompts/index.js +5 -0
  30. package/dist/services/GitService.d.ts +1 -0
  31. package/dist/services/GitService.d.ts.map +1 -1
  32. package/dist/services/GitService.js +16 -0
  33. package/dist/services/MarketplaceService.d.ts +7 -0
  34. package/dist/services/MarketplaceService.d.ts.map +1 -1
  35. package/dist/services/MarketplaceService.js +321 -252
  36. package/dist/services/aiService.d.ts +34 -0
  37. package/dist/services/aiService.d.ts.map +1 -1
  38. package/dist/services/aiService.js +124 -1
  39. package/dist/services/initializationService.d.ts.map +1 -1
  40. package/dist/services/initializationService.js +18 -0
  41. package/dist/tools/agentTool.js +3 -3
  42. package/dist/tools/bashTool.d.ts.map +1 -1
  43. package/dist/tools/bashTool.js +4 -4
  44. package/dist/tools/editTool.d.ts.map +1 -1
  45. package/dist/tools/editTool.js +2 -0
  46. package/dist/tools/globTool.d.ts.map +1 -1
  47. package/dist/tools/globTool.js +15 -3
  48. package/dist/tools/grepTool.d.ts.map +1 -1
  49. package/dist/tools/grepTool.js +38 -12
  50. package/dist/tools/readTool.d.ts.map +1 -1
  51. package/dist/tools/readTool.js +61 -0
  52. package/dist/tools/skillTool.js +2 -2
  53. package/dist/tools/types.d.ts +16 -0
  54. package/dist/tools/types.d.ts.map +1 -1
  55. package/dist/tools/webFetchTool.d.ts +3 -0
  56. package/dist/tools/webFetchTool.d.ts.map +1 -0
  57. package/dist/tools/webFetchTool.js +171 -0
  58. package/dist/tools/writeTool.d.ts.map +1 -1
  59. package/dist/tools/writeTool.js +2 -0
  60. package/dist/types/commands.d.ts +1 -1
  61. package/dist/types/commands.d.ts.map +1 -1
  62. package/dist/types/messaging.d.ts +1 -0
  63. package/dist/types/messaging.d.ts.map +1 -1
  64. package/dist/utils/bashParser.d.ts +20 -2
  65. package/dist/utils/bashParser.d.ts.map +1 -1
  66. package/dist/utils/bashParser.js +281 -146
  67. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  68. package/dist/utils/convertMessagesForAPI.js +7 -0
  69. package/dist/utils/fileUtils.d.ts +8 -0
  70. package/dist/utils/fileUtils.d.ts.map +1 -1
  71. package/dist/utils/fileUtils.js +52 -0
  72. package/dist/utils/messageOperations.d.ts +12 -3
  73. package/dist/utils/messageOperations.d.ts.map +1 -1
  74. package/dist/utils/messageOperations.js +77 -9
  75. package/package.json +4 -2
  76. package/src/agent.ts +19 -1
  77. package/src/constants/tools.ts +1 -1
  78. package/src/managers/MemoryRuleManager.ts +1 -10
  79. package/src/managers/aiManager.ts +23 -3
  80. package/src/managers/mcpManager.ts +37 -16
  81. package/src/managers/messageManager.ts +76 -38
  82. package/src/managers/permissionManager.ts +4 -4
  83. package/src/managers/pluginManager.ts +4 -2
  84. package/src/managers/slashCommandManager.ts +130 -4
  85. package/src/managers/toolManager.ts +11 -2
  86. package/src/prompts/index.ts +6 -0
  87. package/src/services/GitService.ts +20 -0
  88. package/src/services/MarketplaceService.ts +397 -324
  89. package/src/services/aiService.ts +197 -1
  90. package/src/services/initializationService.ts +38 -0
  91. package/src/tools/agentTool.ts +3 -3
  92. package/src/tools/bashTool.ts +3 -4
  93. package/src/tools/editTool.ts +3 -0
  94. package/src/tools/globTool.ts +16 -3
  95. package/src/tools/grepTool.ts +41 -13
  96. package/src/tools/readTool.ts +69 -0
  97. package/src/tools/skillTool.ts +2 -2
  98. package/src/tools/types.ts +13 -0
  99. package/src/tools/webFetchTool.ts +194 -0
  100. package/src/tools/writeTool.ts +3 -0
  101. package/src/types/commands.ts +1 -1
  102. package/src/types/messaging.ts +1 -0
  103. package/src/utils/bashParser.ts +316 -161
  104. package/src/utils/convertMessagesForAPI.ts +8 -0
  105. package/src/utils/fileUtils.ts +69 -0
  106. package/src/utils/messageOperations.ts +84 -9
  107. package/dist/tools/taskOutputTool.d.ts +0 -3
  108. package/dist/tools/taskOutputTool.d.ts.map +0 -1
  109. package/dist/tools/taskOutputTool.js +0 -198
  110. 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.5",
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
 
@@ -63,12 +63,11 @@ export class McpManager {
63
63
  const config = await this.ensureConfigLoaded();
64
64
 
65
65
  if (config && config.mcpServers) {
66
- // Connect to all configured servers
67
- const connectionPromises = Object.keys(config.mcpServers).map(
68
- async (serverName) => {
69
- try {
70
- logger?.debug(`Connecting to MCP server: ${serverName}`);
71
- const success = await this.connectServer(serverName);
66
+ // Connect to all configured servers in background to avoid blocking agent initialization
67
+ Object.keys(config.mcpServers).forEach((serverName) => {
68
+ logger?.debug(`Connecting to MCP server: ${serverName}`);
69
+ this.connectServer(serverName)
70
+ .then((success) => {
72
71
  if (success) {
73
72
  logger?.debug(
74
73
  `Successfully connected to MCP server: ${serverName}`,
@@ -76,18 +75,18 @@ export class McpManager {
76
75
  } else {
77
76
  logger?.warn(`Failed to connect to MCP server: ${serverName}`);
78
77
  }
79
- } catch {
80
- logger?.error(`Error connecting to MCP server ${serverName}`);
81
- }
82
- },
83
- );
84
-
85
- // Wait for all connection attempts to complete
86
- await Promise.all(connectionPromises);
78
+ })
79
+ .catch((error) => {
80
+ logger?.error(
81
+ `Background connection to MCP server ${serverName} failed:`,
82
+ error,
83
+ );
84
+ });
85
+ });
87
86
  }
88
87
 
89
- logger?.debug("MCP servers initialization completed");
90
- // Trigger state change callback after initialization
88
+ logger?.debug("MCP servers initialization started in background");
89
+ // Trigger state change callback after starting initialization
91
90
  this.callbacks.onServersChange?.(this.getAllServers());
92
91
  }
93
92
  }
@@ -233,8 +232,30 @@ export class McpManager {
233
232
  ...(server.config.env || {}),
234
233
  },
235
234
  cwd: this.workdir, // Use the agent's workdir as the process working directory
235
+ stderr: "pipe", // Pipe stderr to capture it
236
236
  });
237
237
 
238
+ // Handle stderr output
239
+ const stderr = transport.stderr;
240
+ if (stderr) {
241
+ let buffer = "";
242
+ stderr.on("data", (chunk: Buffer) => {
243
+ buffer += chunk.toString();
244
+ const lines = buffer.split("\n");
245
+ buffer = lines.pop() || "";
246
+ for (const line of lines) {
247
+ if (line.trim()) {
248
+ logger?.error(`[MCP Server ${name}] ${line}`);
249
+ }
250
+ }
251
+ });
252
+ stderr.on("end", () => {
253
+ if (buffer.trim()) {
254
+ logger?.error(`[MCP Server ${name}] ${buffer}`);
255
+ }
256
+ });
257
+ }
258
+
238
259
  // Create client
239
260
  const client = new Client(
240
261
  {
@@ -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;
@@ -21,7 +21,7 @@ import {
21
21
  stripEnvVars,
22
22
  stripRedirections,
23
23
  hasWriteRedirections,
24
- isBashWriteRedirect,
24
+ isBashHeredocWrite,
25
25
  getSmartPrefix,
26
26
  DANGEROUS_COMMANDS,
27
27
  } from "../utils/bashParser.js";
@@ -370,10 +370,10 @@ export class PermissionManager {
370
370
  async checkPermission(
371
371
  context: ToolPermissionContext,
372
372
  ): Promise<PermissionDecision> {
373
- // 0. Intercept Bash file writing operations
373
+ // 0. Intercept Bash EOF writing operations
374
374
  if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
375
375
  const command = String(context.toolInput.command);
376
- if (isBashWriteRedirect(command)) {
376
+ if (isBashHeredocWrite(command)) {
377
377
  // Check if this specific command is explicitly allowed by a rule that includes redirection
378
378
  const isExplicitlyAllowed = [
379
379
  ...this.instanceAllowedRules,
@@ -395,7 +395,7 @@ export class PermissionManager {
395
395
  return {
396
396
  behavior: "deny",
397
397
  message:
398
- "Bash-based file writing operations (e.g., using '>', '>>', or 'cat <<EOF > file') are not allowed. Please use the dedicated 'Write' or 'Edit' tools instead for file modifications.",
398
+ "Bash-based file writing operations using heredocs (e.g., 'cat <<EOF > file') are not allowed. Please use the dedicated 'Write' or 'Edit' tools instead for file modifications.",
399
399
  };
400
400
  }
401
401
  }
@@ -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();