@mastra/agent-builder 0.0.0-experimental-agent-builder-20250815195917 → 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/defaults.ts CHANGED
@@ -2,9 +2,9 @@ import { spawn as nodeSpawn } from 'child_process';
2
2
  import { readFile, writeFile, mkdir, stat, readdir } from 'fs/promises';
3
3
  import { join, dirname, relative, isAbsolute, resolve } from 'path';
4
4
  import { createTool } from '@mastra/core/tools';
5
- import { MCPClient } from '@mastra/mcp';
5
+ import ignore from 'ignore';
6
6
  import { z } from 'zod';
7
- import { exec, spawnSWPM } from './utils';
7
+ import { exec, execFile, spawnSWPM, spawnWithOutput } from './utils';
8
8
 
9
9
  export class AgentBuilderDefaults {
10
10
  static DEFAULT_INSTRUCTIONS = (
@@ -394,26 +394,6 @@ export const mastra = new Mastra({
394
394
  });
395
395
  \`\`\`
396
396
 
397
- ### MCPClient
398
- \`\`\`
399
- // ./src/mcp/client.ts
400
-
401
- import { MCPClient } from '@mastra/mcp-client';
402
-
403
- // leverage existing MCP servers, or create your own
404
- export const mcpClient = new MCPClient({
405
- id: 'example-mcp-client',
406
- servers: {
407
- some-mcp-server: {
408
- command: 'npx',
409
- args: ["some-mcp-server"],
410
- },
411
- },
412
- });
413
-
414
- export const tools = await mcpClient.getTools();
415
- \`\`\`
416
-
417
397
  </examples>`;
418
398
 
419
399
  static DEFAULT_MEMORY_CONFIG = {
@@ -428,33 +408,8 @@ export const tools = await mcpClient.getTools();
428
408
  network: 'src/mastra/networks',
429
409
  };
430
410
 
431
- static DEFAULT_TOOLS = async (projectPath?: string, mode: 'template' | 'code-editor' = 'code-editor') => {
432
- const mcpClient = new MCPClient({
433
- id: 'agent-builder-mcp-client',
434
- servers: {
435
- // web: {
436
- // command: 'node',
437
- // args: ['/Users/daniellew/Documents/Mastra/web-search/build/index.js'],
438
- // },
439
- docs: {
440
- command: 'npx',
441
- args: ['-y', '@mastra/mcp-docs-server'],
442
- },
443
- },
444
- });
445
-
446
- const tools = await mcpClient.getTools();
447
- const filteredTools: Record<string, any> = {};
448
-
449
- Object.keys(tools).forEach(key => {
450
- if (!key.includes('MastraCourse')) {
451
- filteredTools[key] = tools[key];
452
- }
453
- });
454
-
411
+ static DEFAULT_TOOLS = async (projectPath: string, mode: 'template' | 'code-editor' = 'code-editor') => {
455
412
  const agentBuilderTools = {
456
- ...filteredTools,
457
-
458
413
  readFile: createTool({
459
414
  id: 'read-file',
460
415
  description: 'Read contents of a file with optional line range selection.',
@@ -648,6 +603,29 @@ export const tools = await mcpClient.getTools();
648
603
  },
649
604
  }),
650
605
 
606
+ replaceLines: createTool({
607
+ id: 'replace-lines',
608
+ description:
609
+ 'Replace specific line ranges in files with new content. Perfect for fixing multiline imports, function signatures, or other structured code.',
610
+ inputSchema: z.object({
611
+ filePath: z.string().describe('Path to the file to edit'),
612
+ startLine: z.number().describe('Starting line number to replace (1-indexed)'),
613
+ endLine: z.number().describe('Ending line number to replace (1-indexed, inclusive)'),
614
+ newContent: z.string().describe('New content to replace the lines with'),
615
+ createBackup: z.boolean().default(false).describe('Create backup file before editing'),
616
+ }),
617
+ outputSchema: z.object({
618
+ success: z.boolean(),
619
+ message: z.string(),
620
+ linesReplaced: z.number().optional(),
621
+ backup: z.string().optional(),
622
+ error: z.string().optional(),
623
+ }),
624
+ execute: async ({ context }) => {
625
+ return await AgentBuilderDefaults.replaceLines({ ...context, projectPath });
626
+ },
627
+ }),
628
+
651
629
  // Interactive Communication
652
630
  askClarification: createTool({
653
631
  id: 'ask-clarification',
@@ -730,7 +708,7 @@ export const tools = await mcpClient.getTools();
730
708
  }),
731
709
  }),
732
710
  execute: async ({ context }) => {
733
- return await AgentBuilderDefaults.performSmartSearch(context);
711
+ return await AgentBuilderDefaults.performSmartSearch(context, projectPath);
734
712
  },
735
713
  }),
736
714
 
@@ -1136,14 +1114,13 @@ export const tools = await mcpClient.getTools();
1136
1114
  */
1137
1115
  static async createMastraProject({ features, projectName }: { features?: string[]; projectName?: string }) {
1138
1116
  try {
1139
- const args = ['pnpx', 'create', 'mastra@latest', projectName ?? '', '-l', 'openai', '-k', 'skip'];
1140
-
1117
+ const args = ['pnpx', 'create-mastra@latest', projectName?.replace(/[;&|`$(){}\[\]]/g, '') ?? '', '-l', 'openai'];
1141
1118
  if (features && features.length > 0) {
1142
1119
  args.push('--components', features.join(','));
1143
1120
  }
1144
1121
  args.push('--example');
1145
1122
 
1146
- const { stdout, stderr } = await exec(args.join(' '));
1123
+ const { stdout, stderr } = await spawnWithOutput(args[0]!, args.slice(1), {});
1147
1124
 
1148
1125
  return {
1149
1126
  success: true,
@@ -1153,6 +1130,7 @@ export const tools = await mcpClient.getTools();
1153
1130
  error: stderr,
1154
1131
  };
1155
1132
  } catch (error) {
1133
+ console.log(error);
1156
1134
  return {
1157
1135
  success: false,
1158
1136
  message: `Failed to create project: ${error instanceof Error ? error.message : String(error)}`,
@@ -1363,10 +1341,21 @@ export const tools = await mcpClient.getTools();
1363
1341
  * Stop the Mastra server
1364
1342
  */
1365
1343
  static async stopMastraServer({ port = 4200, projectPath: _projectPath }: { port?: number; projectPath?: string }) {
1344
+ // Validate port to ensure it is a safe integer
1345
+ if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535) {
1346
+ return {
1347
+ success: false,
1348
+ status: 'error' as const,
1349
+ error: `Invalid port value: ${String(port)}`,
1350
+ };
1351
+ }
1366
1352
  try {
1367
- const { stdout } = await exec(`lsof -ti:${port} || echo "No process found"`);
1353
+ // Run lsof safely without shell interpretation
1354
+ const { stdout } = await execFile('lsof', ['-ti', String(port)]);
1355
+ // If no output, treat as "No process found"
1356
+ const effectiveStdout = stdout.trim() ? stdout : 'No process found';
1368
1357
 
1369
- if (!stdout.trim() || stdout.trim() === 'No process found') {
1358
+ if (!effectiveStdout || effectiveStdout === 'No process found') {
1370
1359
  return {
1371
1360
  success: true,
1372
1361
  status: 'stopped' as const,
@@ -1388,11 +1377,15 @@ export const tools = await mcpClient.getTools();
1388
1377
  try {
1389
1378
  process.kill(pid, 'SIGTERM');
1390
1379
  killedPids.push(pid);
1391
- } catch {
1380
+ } catch (e) {
1392
1381
  failedPids.push(pid);
1382
+ console.warn(`Failed to kill process ${pid}:`, e);
1393
1383
  }
1394
1384
  }
1395
1385
 
1386
+ // If some processes failed to be killed, still report partial success
1387
+ // but include warning about failed processes
1388
+
1396
1389
  if (killedPids.length === 0) {
1397
1390
  return {
1398
1391
  success: false,
@@ -1402,12 +1395,20 @@ export const tools = await mcpClient.getTools();
1402
1395
  };
1403
1396
  }
1404
1397
 
1398
+ // Report partial success if some processes were killed but others failed
1399
+ if (failedPids.length > 0) {
1400
+ console.warn(
1401
+ `Killed ${killedPids.length} processes but failed to kill ${failedPids.length} processes: ${failedPids.join(', ')}`,
1402
+ );
1403
+ }
1404
+
1405
1405
  // Wait a bit and check if processes are still running
1406
1406
  await new Promise(resolve => setTimeout(resolve, 2000));
1407
1407
 
1408
1408
  try {
1409
- const { stdout: checkStdout } = await exec(`lsof -ti:${port} || echo "No process found"`);
1410
- if (checkStdout.trim() && checkStdout.trim() !== 'No process found') {
1409
+ const { stdout: checkStdoutRaw } = await execFile('lsof', ['-ti', String(port)]);
1410
+ const checkStdout = checkStdoutRaw.trim() ? checkStdoutRaw : 'No process found';
1411
+ if (checkStdout && checkStdout !== 'No process found') {
1411
1412
  // Force kill remaining processes
1412
1413
  const remainingPids = checkStdout
1413
1414
  .trim()
@@ -1426,8 +1427,9 @@ export const tools = await mcpClient.getTools();
1426
1427
 
1427
1428
  // Final check
1428
1429
  await new Promise(resolve => setTimeout(resolve, 1000));
1429
- const { stdout: finalCheck } = await exec(`lsof -ti:${port} || echo "No process found"`);
1430
- if (finalCheck.trim() && finalCheck.trim() !== 'No process found') {
1430
+ const { stdout: finalCheckRaw } = await execFile('lsof', ['-ti', String(port)]);
1431
+ const finalCheck = finalCheckRaw.trim() ? finalCheckRaw : 'No process found';
1432
+ if (finalCheck && finalCheck !== 'No process found') {
1431
1433
  return {
1432
1434
  success: false,
1433
1435
  status: 'unknown' as const,
@@ -1494,8 +1496,9 @@ export const tools = await mcpClient.getTools();
1494
1496
  } catch {
1495
1497
  // Check if process exists on port
1496
1498
  try {
1497
- const { stdout } = await exec(`lsof -ti:${port} || echo "No process found"`);
1498
- const hasProcess = stdout.trim() && stdout.trim() !== 'No process found';
1499
+ const { stdout } = await execFile('lsof', ['-ti', String(port)]);
1500
+ const effectiveStdout = stdout.trim() ? stdout : 'No process found';
1501
+ const hasProcess = effectiveStdout && effectiveStdout !== 'No process found';
1499
1502
 
1500
1503
  return {
1501
1504
  success: Boolean(hasProcess),
@@ -1545,9 +1548,10 @@ export const tools = await mcpClient.getTools();
1545
1548
  // TypeScript validation
1546
1549
  if (validationType.includes('types')) {
1547
1550
  try {
1548
- const filePattern = files?.length ? files.join(' ') : '';
1549
- const tscCommand = files?.length ? `npx tsc --noEmit ${filePattern}` : 'npx tsc --noEmit';
1550
- await exec(tscCommand, execOptions);
1551
+ const fileArgs = files?.length ? files : [];
1552
+ // Use execFile for safe argument passing to avoid shell interpretation
1553
+ const args = ['tsc', '--noEmit', ...fileArgs];
1554
+ await execFile('npx', args, execOptions);
1551
1555
  validationsPassed.push('types');
1552
1556
  } catch (error: any) {
1553
1557
  let tsOutput = '';
@@ -1571,9 +1575,9 @@ export const tools = await mcpClient.getTools();
1571
1575
  // ESLint validation
1572
1576
  if (validationType.includes('lint')) {
1573
1577
  try {
1574
- const filePattern = files?.length ? files.join(' ') : '.';
1575
- const eslintCommand = `npx eslint ${filePattern} --format json`;
1576
- const { stdout } = await exec(eslintCommand, execOptions);
1578
+ const fileArgs = files?.length ? files : ['.'];
1579
+ const eslintArgs = ['eslint', ...fileArgs, '--format', 'json'];
1580
+ const { stdout } = await execFile('npx', eslintArgs, execOptions);
1577
1581
 
1578
1582
  if (stdout) {
1579
1583
  const eslintResults = JSON.parse(stdout);
@@ -1788,11 +1792,19 @@ export const tools = await mcpClient.getTools();
1788
1792
  }>;
1789
1793
  taskId?: string;
1790
1794
  }) {
1791
- // In-memory task storage (could be enhanced with persistent storage)
1795
+ // In-memory task storage with cleanup (could be enhanced with persistent storage)
1792
1796
  if (!AgentBuilderDefaults.taskStorage) {
1793
1797
  AgentBuilderDefaults.taskStorage = new Map();
1794
1798
  }
1795
1799
 
1800
+ // Cleanup old sessions to prevent memory leaks
1801
+ // Keep only the last 10 sessions
1802
+ const sessions = Array.from(AgentBuilderDefaults.taskStorage.keys());
1803
+ if (sessions.length > 10) {
1804
+ const sessionsToRemove = sessions.slice(0, sessions.length - 10);
1805
+ sessionsToRemove.forEach(session => AgentBuilderDefaults.taskStorage.delete(session));
1806
+ }
1807
+
1796
1808
  const sessionId = 'current'; // Could be enhanced with proper session management
1797
1809
  const existingTasks = AgentBuilderDefaults.taskStorage.get(sessionId) || [];
1798
1810
 
@@ -1917,7 +1929,37 @@ export const tools = await mcpClient.getTools();
1917
1929
 
1918
1930
  // Use ripgrep for fast searching
1919
1931
  // const excludePatterns = includeTests ? [] : ['*test*', '*spec*', '__tests__'];
1920
- const languagePattern = language ? `*.${language}` : '*';
1932
+
1933
+ // Only allow a list of known extensions/language types to prevent shell injection
1934
+ const ALLOWED_LANGUAGES = [
1935
+ 'js',
1936
+ 'ts',
1937
+ 'jsx',
1938
+ 'tsx',
1939
+ 'py',
1940
+ 'java',
1941
+ 'go',
1942
+ 'cpp',
1943
+ 'c',
1944
+ 'cs',
1945
+ 'rb',
1946
+ 'php',
1947
+ 'rs',
1948
+ 'kt',
1949
+ 'swift',
1950
+ 'm',
1951
+ 'scala',
1952
+ 'sh',
1953
+ 'json',
1954
+ 'yaml',
1955
+ 'yml',
1956
+ 'toml',
1957
+ 'ini',
1958
+ ];
1959
+ let languagePattern = '*';
1960
+ if (language && ALLOWED_LANGUAGES.includes(language)) {
1961
+ languagePattern = `*.${language}`;
1962
+ }
1921
1963
 
1922
1964
  switch (action) {
1923
1965
  case 'definitions':
@@ -1934,9 +1976,15 @@ export const tools = await mcpClient.getTools();
1934
1976
 
1935
1977
  for (const pattern of definitionPatterns) {
1936
1978
  try {
1937
- const { stdout } = await exec(
1938
- `rg -n "${pattern}" "${path}" --type ${languagePattern} --max-depth ${depth}`,
1939
- );
1979
+ const { stdout } = await execFile('rg', [
1980
+ '-n',
1981
+ pattern,
1982
+ path,
1983
+ '--type',
1984
+ languagePattern,
1985
+ '--max-depth',
1986
+ String(depth),
1987
+ ]);
1940
1988
  const matches = stdout.split('\n').filter(line => line.trim());
1941
1989
 
1942
1990
  matches.forEach(match => {
@@ -1993,7 +2041,7 @@ export const tools = await mcpClient.getTools();
1993
2041
 
1994
2042
  for (const pattern of depPatterns) {
1995
2043
  try {
1996
- const { stdout } = await exec(`rg -n "${pattern}" "${path}" --type ${languagePattern}`);
2044
+ const { stdout } = await execFile('rg', ['-n', pattern, path, '--type', languagePattern]);
1997
2045
  const matches = stdout.split('\n').filter(line => line.trim());
1998
2046
 
1999
2047
  matches.forEach(match => {
@@ -2025,11 +2073,13 @@ export const tools = await mcpClient.getTools();
2025
2073
  };
2026
2074
 
2027
2075
  case 'structure':
2028
- const { stdout: lsOutput } = await exec(`find "${path}" -type f -name "${languagePattern}" | head -1000`);
2029
- const files = lsOutput.split('\n').filter(line => line.trim());
2076
+ // Use execFile for find commands to avoid shell injection
2077
+ const { stdout: lsOutput } = await execFile('find', [path, '-type', 'f', '-name', languagePattern]);
2078
+ const allFiles = lsOutput.split('\n').filter(line => line.trim());
2079
+ const files = allFiles.slice(0, 1000); // Limit to 1000 files like head -1000
2030
2080
 
2031
- const { stdout: dirOutput } = await exec(`find "${path}" -type d | wc -l`);
2032
- const directories = parseInt(dirOutput.trim());
2081
+ const { stdout: dirOutput } = await execFile('find', [path, '-type', 'd']);
2082
+ const directories = dirOutput.split('\n').filter(line => line.trim()).length;
2033
2083
 
2034
2084
  // Count languages by file extension
2035
2085
  const languages: Record<string, number> = {};
@@ -2086,6 +2136,7 @@ export const tools = await mcpClient.getTools();
2086
2136
  createBackup?: boolean;
2087
2137
  projectPath?: string;
2088
2138
  }) {
2139
+ const { operations, createBackup = false, projectPath = process.cwd() } = context;
2089
2140
  const results: Array<{
2090
2141
  filePath: string;
2091
2142
  editsApplied: number;
@@ -2094,62 +2145,57 @@ export const tools = await mcpClient.getTools();
2094
2145
  }> = [];
2095
2146
 
2096
2147
  try {
2097
- const { projectPath } = context;
2098
-
2099
- for (const operation of context.operations) {
2100
- // Resolve path relative to project directory if it's not absolute
2101
- const resolvedPath = isAbsolute(operation.filePath)
2102
- ? operation.filePath
2103
- : resolve(projectPath || process.cwd(), operation.filePath);
2104
-
2105
- const result = {
2106
- filePath: resolvedPath,
2107
- editsApplied: 0,
2108
- errors: [] as string[],
2109
- backup: undefined as string | undefined,
2110
- };
2148
+ for (const operation of operations) {
2149
+ const filePath = isAbsolute(operation.filePath) ? operation.filePath : join(projectPath, operation.filePath);
2150
+ let editsApplied = 0;
2151
+ const errors: string[] = [];
2152
+ let backup: string | undefined;
2111
2153
 
2112
2154
  try {
2113
- // Read file content
2114
- const originalContent = await readFile(resolvedPath, 'utf-8');
2115
-
2116
2155
  // Create backup if requested
2117
- if (context.createBackup) {
2118
- const backupPath = `${resolvedPath}.backup.${Date.now()}`;
2119
- await writeFile(backupPath, originalContent);
2120
- result.backup = backupPath;
2156
+ if (createBackup) {
2157
+ const backupPath = `${filePath}.backup.${Date.now()}`;
2158
+ const originalContent = await readFile(filePath, 'utf-8');
2159
+ await writeFile(backupPath, originalContent, 'utf-8');
2160
+ backup = backupPath;
2121
2161
  }
2122
2162
 
2123
- let modifiedContent = originalContent;
2163
+ // Read current file content
2164
+ let content = await readFile(filePath, 'utf-8');
2124
2165
 
2125
- // Apply edits sequentially
2166
+ // Apply each edit
2126
2167
  for (const edit of operation.edits) {
2127
- if (edit.replaceAll) {
2128
- const regex = new RegExp(edit.oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
2129
- const matches = modifiedContent.match(regex);
2168
+ const { oldString, newString, replaceAll = false } = edit;
2169
+
2170
+ if (replaceAll) {
2171
+ const regex = new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
2172
+ const matches = content.match(regex);
2130
2173
  if (matches) {
2131
- modifiedContent = modifiedContent.replace(regex, edit.newString);
2132
- result.editsApplied += matches.length;
2174
+ content = content.replace(regex, newString);
2175
+ editsApplied += matches.length;
2133
2176
  }
2134
2177
  } else {
2135
- if (modifiedContent.includes(edit.oldString)) {
2136
- modifiedContent = modifiedContent.replace(edit.oldString, edit.newString);
2137
- result.editsApplied++;
2178
+ if (content.includes(oldString)) {
2179
+ content = content.replace(oldString, newString);
2180
+ editsApplied++;
2138
2181
  } else {
2139
- result.errors.push(`String not found: "${edit.oldString.substring(0, 50)}..."`);
2182
+ errors.push(`String not found: "${oldString.substring(0, 50)}${oldString.length > 50 ? '...' : ''}"`);
2140
2183
  }
2141
2184
  }
2142
2185
  }
2143
2186
 
2144
- // Write modified content
2145
- if (result.editsApplied > 0) {
2146
- await writeFile(resolvedPath, modifiedContent);
2147
- }
2187
+ // Write updated content back
2188
+ await writeFile(filePath, content, 'utf-8');
2148
2189
  } catch (error) {
2149
- result.errors.push(error instanceof Error ? error.message : String(error));
2190
+ errors.push(`File operation error: ${error instanceof Error ? error.message : String(error)}`);
2150
2191
  }
2151
2192
 
2152
- results.push(result);
2193
+ results.push({
2194
+ filePath: operation.filePath,
2195
+ editsApplied,
2196
+ errors,
2197
+ backup,
2198
+ });
2153
2199
  }
2154
2200
 
2155
2201
  const totalEdits = results.reduce((sum, r) => sum + r.editsApplied, 0);
@@ -2158,7 +2204,7 @@ export const tools = await mcpClient.getTools();
2158
2204
  return {
2159
2205
  success: totalErrors === 0,
2160
2206
  results,
2161
- message: `Applied ${totalEdits} edits across ${results.length} files${totalErrors > 0 ? ` with ${totalErrors} errors` : ''}`,
2207
+ message: `Applied ${totalEdits} edits across ${operations.length} files${totalErrors > 0 ? ` with ${totalErrors} errors` : ''}`,
2162
2208
  };
2163
2209
  } catch (error) {
2164
2210
  return {
@@ -2169,6 +2215,79 @@ export const tools = await mcpClient.getTools();
2169
2215
  }
2170
2216
  }
2171
2217
 
2218
+ /**
2219
+ * Replace specific line ranges in a file with new content
2220
+ */
2221
+ static async replaceLines(context: {
2222
+ filePath: string;
2223
+ startLine: number;
2224
+ endLine: number;
2225
+ newContent: string;
2226
+ createBackup?: boolean;
2227
+ projectPath?: string;
2228
+ }) {
2229
+ const { filePath, startLine, endLine, newContent, createBackup = false, projectPath = process.cwd() } = context;
2230
+
2231
+ try {
2232
+ const fullPath = isAbsolute(filePath) ? filePath : join(projectPath, filePath);
2233
+
2234
+ // Read current file content
2235
+ const content = await readFile(fullPath, 'utf-8');
2236
+ const lines = content.split('\n');
2237
+
2238
+ // Validate line numbers
2239
+ if (startLine < 1 || endLine < 1 || startLine > lines.length || endLine > lines.length) {
2240
+ return {
2241
+ success: false,
2242
+ message: `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines.`,
2243
+ error: 'Invalid line range',
2244
+ };
2245
+ }
2246
+
2247
+ if (startLine > endLine) {
2248
+ return {
2249
+ success: false,
2250
+ message: `Start line (${startLine}) cannot be greater than end line (${endLine}).`,
2251
+ error: 'Invalid line range',
2252
+ };
2253
+ }
2254
+
2255
+ // Create backup if requested
2256
+ let backup: string | undefined;
2257
+ if (createBackup) {
2258
+ const backupPath = `${fullPath}.backup.${Date.now()}`;
2259
+ await writeFile(backupPath, content, 'utf-8');
2260
+ backup = backupPath;
2261
+ }
2262
+
2263
+ // Replace the specified line range
2264
+ const beforeLines = lines.slice(0, startLine - 1);
2265
+ const afterLines = lines.slice(endLine);
2266
+ const newLines = newContent ? newContent.split('\n') : [];
2267
+
2268
+ const updatedLines = [...beforeLines, ...newLines, ...afterLines];
2269
+ const updatedContent = updatedLines.join('\n');
2270
+
2271
+ // Write updated content back
2272
+ await writeFile(fullPath, updatedContent, 'utf-8');
2273
+
2274
+ const linesReplaced = endLine - startLine + 1;
2275
+
2276
+ return {
2277
+ success: true,
2278
+ message: `Successfully replaced ${linesReplaced} lines (${startLine}-${endLine}) in ${filePath}`,
2279
+ linesReplaced,
2280
+ backup,
2281
+ };
2282
+ } catch (error) {
2283
+ return {
2284
+ success: false,
2285
+ message: `Failed to replace lines: ${error instanceof Error ? error.message : String(error)}`,
2286
+ error: error instanceof Error ? error.message : String(error),
2287
+ };
2288
+ }
2289
+ }
2290
+
2172
2291
  /**
2173
2292
  * Ask user for clarification
2174
2293
  */
@@ -2244,21 +2363,24 @@ export const tools = await mcpClient.getTools();
2244
2363
  /**
2245
2364
  * Perform intelligent search with context
2246
2365
  */
2247
- static async performSmartSearch(context: {
2248
- query: string;
2249
- type?: 'text' | 'regex' | 'fuzzy' | 'semantic';
2250
- scope?: {
2251
- paths?: string[];
2252
- fileTypes?: string[];
2253
- excludePaths?: string[];
2254
- maxResults?: number;
2255
- };
2256
- context?: {
2257
- beforeLines?: number;
2258
- afterLines?: number;
2259
- includeDefinitions?: boolean;
2260
- };
2261
- }) {
2366
+ static async performSmartSearch(
2367
+ context: {
2368
+ query: string;
2369
+ type?: 'text' | 'regex' | 'fuzzy' | 'semantic';
2370
+ scope?: {
2371
+ paths?: string[];
2372
+ fileTypes?: string[];
2373
+ excludePaths?: string[];
2374
+ maxResults?: number;
2375
+ };
2376
+ context?: {
2377
+ beforeLines?: number;
2378
+ afterLines?: number;
2379
+ includeDefinitions?: boolean;
2380
+ };
2381
+ },
2382
+ projectPath: string,
2383
+ ) {
2262
2384
  try {
2263
2385
  const { query, type = 'text', scope = {}, context: searchContext = {} } = context;
2264
2386
 
@@ -2266,42 +2388,50 @@ export const tools = await mcpClient.getTools();
2266
2388
 
2267
2389
  const { beforeLines = 2, afterLines = 2 } = searchContext;
2268
2390
 
2269
- let rgCommand = 'rg';
2391
+ // Build command and arguments array safely
2392
+ const rgArgs: string[] = [];
2270
2393
 
2271
2394
  // Add context lines
2272
- if (beforeLines > 0 || afterLines > 0) {
2273
- rgCommand += ` -A ${afterLines} -B ${beforeLines}`;
2395
+ if (beforeLines > 0) {
2396
+ rgArgs.push('-B', beforeLines.toString());
2397
+ }
2398
+ if (afterLines > 0) {
2399
+ rgArgs.push('-A', afterLines.toString());
2274
2400
  }
2275
2401
 
2276
2402
  // Add line numbers
2277
- rgCommand += ' -n';
2403
+ rgArgs.push('-n');
2278
2404
 
2279
2405
  // Handle search type
2280
2406
  if (type === 'regex') {
2281
- rgCommand += ' -e';
2407
+ rgArgs.push('-e');
2282
2408
  } else if (type === 'fuzzy') {
2283
- rgCommand += ' --fixed-strings';
2409
+ rgArgs.push('--fixed-strings');
2284
2410
  }
2285
2411
 
2286
2412
  // Add file type filters
2287
2413
  if (fileTypes.length > 0) {
2288
2414
  fileTypes.forEach(ft => {
2289
- rgCommand += ` --type-add 'custom:*.${ft}' -t custom`;
2415
+ rgArgs.push('--type-add', `custom:*.${ft}`, '-t', 'custom');
2290
2416
  });
2291
2417
  }
2292
2418
 
2293
2419
  // Add exclude patterns
2294
2420
  excludePaths.forEach(path => {
2295
- rgCommand += ` --glob '!${path}'`;
2421
+ rgArgs.push('--glob', `!${path}`);
2296
2422
  });
2297
2423
 
2298
2424
  // Add max count
2299
- rgCommand += ` -m ${maxResults}`;
2425
+ rgArgs.push('-m', maxResults.toString());
2300
2426
 
2301
- // Add search paths
2302
- rgCommand += ` "${query}" ${paths.join(' ')}`;
2427
+ // Add the search query and paths
2428
+ rgArgs.push(query);
2429
+ rgArgs.push(...paths);
2303
2430
 
2304
- const { stdout } = await exec(rgCommand);
2431
+ // Execute safely using execFile
2432
+ const { stdout } = await execFile('rg', rgArgs, {
2433
+ cwd: projectPath,
2434
+ });
2305
2435
  const lines = stdout.split('\n').filter(line => line.trim());
2306
2436
 
2307
2437
  const matches: Array<{
@@ -2491,6 +2621,19 @@ export const tools = await mcpClient.getTools();
2491
2621
  projectPath,
2492
2622
  } = context;
2493
2623
 
2624
+ const gitignorePath = join(projectPath || process.cwd(), '.gitignore');
2625
+ let gitignoreFilter: ignore.Ignore | undefined;
2626
+
2627
+ try {
2628
+ const gitignoreContent = await readFile(gitignorePath, 'utf-8');
2629
+ gitignoreFilter = ignore().add(gitignoreContent);
2630
+ } catch (err: any) {
2631
+ if (err.code !== 'ENOENT') {
2632
+ console.error(`Error reading .gitignore file:`, err);
2633
+ }
2634
+ // If .gitignore doesn't exist, gitignoreFilter remains undefined, meaning no files are ignored by gitignore.
2635
+ }
2636
+
2494
2637
  // Resolve path relative to project directory if it's not absolute
2495
2638
  const resolvedPath = isAbsolute(path) ? path : resolve(projectPath || process.cwd(), path);
2496
2639
 
@@ -2504,14 +2647,19 @@ export const tools = await mcpClient.getTools();
2504
2647
  }> = [];
2505
2648
 
2506
2649
  async function processDirectory(dirPath: string, currentDepth: number = 0) {
2650
+ const relativeToProject = relative(projectPath || process.cwd(), dirPath);
2651
+ if (gitignoreFilter?.ignores(relativeToProject)) return;
2507
2652
  if (currentDepth > maxDepth) return;
2508
2653
 
2509
2654
  const entries = await readdir(dirPath);
2510
2655
 
2511
2656
  for (const entry of entries) {
2657
+ const entryPath = join(dirPath, entry);
2658
+ const relativeEntryPath = relative(projectPath || process.cwd(), entryPath);
2659
+ if (gitignoreFilter?.ignores(relativeEntryPath)) continue;
2512
2660
  if (!includeHidden && entry.startsWith('.')) continue;
2513
2661
 
2514
- const fullPath = join(dirPath, entry);
2662
+ const fullPath = entryPath;
2515
2663
  const relativePath = relative(resolvedPath, fullPath);
2516
2664
 
2517
2665
  if (pattern) {