wave-agent-sdk 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,OAAO,EACP,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACxD;AAID,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAU;IASnB,OAAO,CAAC,SAAS;IARnB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,SAAS,CAAsB;gBAG7B,SAAS,EAAE,SAAS,EAC5B,OAAO,GAAE,iBAAsB;IAKjC;;OAEG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAe,GAC3B,OAAO,CAAC,IAAI,CAAC;IAwCV,kBAAkB,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAO/C,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA0CvC,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrD,SAAS,IAAI,SAAS,GAAG,IAAI;IAI7B,aAAa,IAAI,eAAe,EAAE;IAIlC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIpD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI;IASzE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO;IAyBzD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAgB7B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA+F7C,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2BtD,oBAAoB,IAAI,OAAO,EAAE;IAW3B,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACtD,CAAC;YAsDY,uBAAuB;IA8D/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B;;OAEG;IACH,iBAAiB,IAAI,UAAU,EAAE;IA6BjC;;OAEG;IACH,iBAAiB,IAAI,0BAA0B,EAAE;IAIjD;;OAEG;IACG,wBAAwB,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IActB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAcjC"}
1
+ {"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,OAAO,EACP,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACxD;AAID,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAU;IASnB,OAAO,CAAC,SAAS;IARnB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,SAAS,CAAsB;gBAG7B,SAAS,EAAE,SAAS,EAC5B,OAAO,GAAE,iBAAsB;IAKjC;;OAEG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAe,GAC3B,OAAO,CAAC,IAAI,CAAC;IAuCV,kBAAkB,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAO/C,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA0CvC,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrD,SAAS,IAAI,SAAS,GAAG,IAAI;IAI7B,aAAa,IAAI,eAAe,EAAE;IAIlC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIpD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI;IASzE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO;IAyBzD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAgB7B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqH7C,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2BtD,oBAAoB,IAAI,OAAO,EAAE;IAW3B,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACtD,CAAC;YAsDY,uBAAuB;IA8D/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B;;OAEG;IACH,iBAAiB,IAAI,UAAU,EAAE;IA6BjC;;OAEG;IACH,iBAAiB,IAAI,0BAA0B,EAAE;IAIjD;;OAEG;IACG,wBAAwB,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IActB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAcjC"}
@@ -25,27 +25,25 @@ export class McpManager {
25
25
  // Ensure MCP configuration is loaded
26
26
  const config = await this.ensureConfigLoaded();
27
27
  if (config && config.mcpServers) {
28
- // Connect to all configured servers
29
- const connectionPromises = Object.keys(config.mcpServers).map(async (serverName) => {
30
- try {
31
- logger?.debug(`Connecting to MCP server: ${serverName}`);
32
- const success = await this.connectServer(serverName);
28
+ // Connect to all configured servers in background to avoid blocking agent initialization
29
+ Object.keys(config.mcpServers).forEach((serverName) => {
30
+ logger?.debug(`Connecting to MCP server: ${serverName}`);
31
+ this.connectServer(serverName)
32
+ .then((success) => {
33
33
  if (success) {
34
34
  logger?.debug(`Successfully connected to MCP server: ${serverName}`);
35
35
  }
36
36
  else {
37
37
  logger?.warn(`Failed to connect to MCP server: ${serverName}`);
38
38
  }
39
- }
40
- catch {
41
- logger?.error(`Error connecting to MCP server ${serverName}`);
42
- }
39
+ })
40
+ .catch((error) => {
41
+ logger?.error(`Background connection to MCP server ${serverName} failed:`, error);
42
+ });
43
43
  });
44
- // Wait for all connection attempts to complete
45
- await Promise.all(connectionPromises);
46
44
  }
47
- logger?.debug("MCP servers initialization completed");
48
- // Trigger state change callback after initialization
45
+ logger?.debug("MCP servers initialization started in background");
46
+ // Trigger state change callback after starting initialization
49
47
  this.callbacks.onServersChange?.(this.getAllServers());
50
48
  }
51
49
  }
@@ -173,7 +171,28 @@ export class McpManager {
173
171
  ...(server.config.env || {}),
174
172
  },
175
173
  cwd: this.workdir, // Use the agent's workdir as the process working directory
174
+ stderr: "pipe", // Pipe stderr to capture it
176
175
  });
176
+ // Handle stderr output
177
+ const stderr = transport.stderr;
178
+ if (stderr) {
179
+ let buffer = "";
180
+ stderr.on("data", (chunk) => {
181
+ buffer += chunk.toString();
182
+ const lines = buffer.split("\n");
183
+ buffer = lines.pop() || "";
184
+ for (const line of lines) {
185
+ if (line.trim()) {
186
+ logger?.error(`[MCP Server ${name}] ${line}`);
187
+ }
188
+ }
189
+ });
190
+ stderr.on("end", () => {
191
+ if (buffer.trim()) {
192
+ logger?.error(`[MCP Server ${name}] ${buffer}`);
193
+ }
194
+ });
195
+ }
177
196
  // Create client
178
197
  const client = new Client({
179
198
  name: "wave-code",
@@ -8,7 +8,7 @@
8
8
  import path from "node:path";
9
9
  import { minimatch } from "minimatch";
10
10
  import { RESTRICTED_TOOLS } from "../types/permissions.js";
11
- import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, isBashWriteRedirect, getSmartPrefix, DANGEROUS_COMMANDS, } from "../utils/bashParser.js";
11
+ import { splitBashCommand, stripEnvVars, stripRedirections, hasWriteRedirections, isBashHeredocWrite, getSmartPrefix, DANGEROUS_COMMANDS, } from "../utils/bashParser.js";
12
12
  import { isPathInside } from "../utils/pathSafety.js";
13
13
  import { BASH_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, READ_TOOL_NAME, } from "../constants/tools.js";
14
14
  const SAFE_COMMANDS = [
@@ -266,10 +266,10 @@ export class PermissionManager {
266
266
  * Called by individual tools after validation/diff, before real operation
267
267
  */
268
268
  async checkPermission(context) {
269
- // 0. Intercept Bash file writing operations
269
+ // 0. Intercept Bash EOF writing operations
270
270
  if (context.toolName === BASH_TOOL_NAME && context.toolInput?.command) {
271
271
  const command = String(context.toolInput.command);
272
- if (isBashWriteRedirect(command)) {
272
+ if (isBashHeredocWrite(command)) {
273
273
  // Check if this specific command is explicitly allowed by a rule that includes redirection
274
274
  const isExplicitlyAllowed = [
275
275
  ...this.instanceAllowedRules,
@@ -289,7 +289,7 @@ export class PermissionManager {
289
289
  if (!isExplicitlyAllowed) {
290
290
  return {
291
291
  behavior: "deny",
292
- message: "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.",
292
+ message: "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.",
293
293
  };
294
294
  }
295
295
  }
@@ -16,9 +16,13 @@ export declare function stripRedirections(command: string): string;
16
16
  */
17
17
  export declare function hasWriteRedirections(command: string): boolean;
18
18
  /**
19
- * Alias for hasWriteRedirections, used for semantic clarity in permission checks.
19
+ * Checks if a bash command contains any heredocs (<<, <<-).
20
20
  */
21
- export declare function isBashWriteRedirect(command: string): boolean;
21
+ export declare function hasHeredoc(command: string): boolean;
22
+ /**
23
+ * Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
24
+ */
25
+ export declare function isBashHeredocWrite(command: string): boolean;
22
26
  /**
23
27
  * Blacklist of dangerous commands that should not be safely prefix-matched
24
28
  * and should not have persistent permissions.
@@ -1 +1 @@
1
- {"version":3,"file":"bashParser.d.ts","sourceRoot":"","sources":["../../src/utils/bashParser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAmH1D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA2CpD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAuHzD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAkG7D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAa9B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA8L7D"}
1
+ {"version":3,"file":"bashParser.d.ts","sourceRoot":"","sources":["../../src/utils/bashParser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAoH1D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA2CpD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAuHzD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAkG7D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAsCnD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAa9B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA8L7D"}
@@ -53,7 +53,7 @@ export function splitBashCommand(command) {
53
53
  opLen = 1;
54
54
  else if (char === "|")
55
55
  opLen = 1;
56
- else if (char === "&" && nextChar !== ">")
56
+ else if (char === "&" && nextChar !== ">" && command[i - 1] !== ">")
57
57
  opLen = 1;
58
58
  if (opLen > 0) {
59
59
  // Check if preceded by an odd number of backslashes
@@ -361,10 +361,44 @@ export function hasWriteRedirections(command) {
361
361
  return false;
362
362
  }
363
363
  /**
364
- * Alias for hasWriteRedirections, used for semantic clarity in permission checks.
364
+ * Checks if a bash command contains any heredocs (<<, <<-).
365
365
  */
366
- export function isBashWriteRedirect(command) {
367
- return hasWriteRedirections(command);
366
+ export function hasHeredoc(command) {
367
+ let inSingleQuote = false;
368
+ let inDoubleQuote = false;
369
+ let escaped = false;
370
+ for (let i = 0; i < command.length; i++) {
371
+ const char = command[i];
372
+ if (escaped) {
373
+ escaped = false;
374
+ continue;
375
+ }
376
+ if (char === "\\") {
377
+ escaped = true;
378
+ continue;
379
+ }
380
+ if (char === "'" && !inDoubleQuote) {
381
+ inSingleQuote = !inSingleQuote;
382
+ continue;
383
+ }
384
+ if (char === '"' && !inSingleQuote) {
385
+ inDoubleQuote = !inDoubleQuote;
386
+ continue;
387
+ }
388
+ if (inSingleQuote || inDoubleQuote) {
389
+ continue;
390
+ }
391
+ if (char === "<" && command[i + 1] === "<") {
392
+ return true;
393
+ }
394
+ }
395
+ return false;
396
+ }
397
+ /**
398
+ * Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
399
+ */
400
+ export function isBashHeredocWrite(command) {
401
+ return hasHeredoc(command) && hasWriteRedirections(command);
368
402
  }
369
403
  /**
370
404
  * Blacklist of dangerous commands that should not be safely prefix-matched
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.6",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -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
  {
@@ -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
  }
@@ -58,7 +58,8 @@ export function splitBashCommand(command: string): string[] {
58
58
  else if (char === "|" && nextChar === "&") opLen = 2;
59
59
  else if (char === ";") opLen = 1;
60
60
  else if (char === "|") opLen = 1;
61
- else if (char === "&" && nextChar !== ">") opLen = 1;
61
+ else if (char === "&" && nextChar !== ">" && command[i - 1] !== ">")
62
+ opLen = 1;
62
63
 
63
64
  if (opLen > 0) {
64
65
  // Check if preceded by an odd number of backslashes
@@ -395,10 +396,53 @@ export function hasWriteRedirections(command: string): boolean {
395
396
  }
396
397
 
397
398
  /**
398
- * Alias for hasWriteRedirections, used for semantic clarity in permission checks.
399
+ * Checks if a bash command contains any heredocs (<<, <<-).
399
400
  */
400
- export function isBashWriteRedirect(command: string): boolean {
401
- return hasWriteRedirections(command);
401
+ export function hasHeredoc(command: string): boolean {
402
+ let inSingleQuote = false;
403
+ let inDoubleQuote = false;
404
+ let escaped = false;
405
+
406
+ for (let i = 0; i < command.length; i++) {
407
+ const char = command[i];
408
+
409
+ if (escaped) {
410
+ escaped = false;
411
+ continue;
412
+ }
413
+
414
+ if (char === "\\") {
415
+ escaped = true;
416
+ continue;
417
+ }
418
+
419
+ if (char === "'" && !inDoubleQuote) {
420
+ inSingleQuote = !inSingleQuote;
421
+ continue;
422
+ }
423
+
424
+ if (char === '"' && !inSingleQuote) {
425
+ inDoubleQuote = !inDoubleQuote;
426
+ continue;
427
+ }
428
+
429
+ if (inSingleQuote || inDoubleQuote) {
430
+ continue;
431
+ }
432
+
433
+ if (char === "<" && command[i + 1] === "<") {
434
+ return true;
435
+ }
436
+ }
437
+
438
+ return false;
439
+ }
440
+
441
+ /**
442
+ * Checks if a bash command is a heredoc write operation (e.g., cat <<EOF > file).
443
+ */
444
+ export function isBashHeredocWrite(command: string): boolean {
445
+ return hasHeredoc(command) && hasWriteRedirections(command);
402
446
  }
403
447
 
404
448
  /**