@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/.turbo/turbo-build.log +12 -0
- package/CHANGELOG.md +7 -16
- package/README.md +4 -17
- package/dist/_tsup-dts-rollup.d.cts +2442 -618
- package/dist/_tsup-dts-rollup.d.ts +2442 -618
- package/dist/index.cjs +1134 -549
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1131 -553
- package/integration-tests/CHANGELOG.md +4 -15
- package/integration-tests/docker-compose.yml +3 -3
- package/integration-tests/package.json +3 -3
- package/integration-tests/src/agent-template-behavior.test.ts +1 -1
- package/integration-tests/src/fixtures/minimal-mastra-project/package.json +2 -2
- package/integration-tests/src/fixtures/minimal-mastra-project/src/mastra/tools/weather.ts +2 -1
- package/integration-tests/src/template-integration.test.ts +8 -8
- package/integration-tests/tsconfig.json +2 -6
- package/integration-tests/vitest.config.ts +1 -0
- package/package.json +7 -6
- package/src/agent/index.ts +187 -0
- package/src/agent-builder.test.ts +34 -12
- package/src/defaults.ts +292 -144
- package/src/index.ts +3 -187
- package/src/processors/tool-summary.ts +18 -9
- package/src/processors/write-file.ts +1 -1
- package/src/types.ts +188 -3
- package/src/utils.ts +289 -13
- package/src/workflows/index.ts +1 -1541
- package/src/workflows/template-builder.ts +1682 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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:
|
|
1410
|
-
|
|
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:
|
|
1430
|
-
|
|
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
|
|
1498
|
-
const
|
|
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
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
|
1575
|
-
const
|
|
1576
|
-
const { stdout } = await
|
|
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
|
-
|
|
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
|
|
1938
|
-
|
|
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
|
|
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
|
-
|
|
2029
|
-
const
|
|
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
|
|
2032
|
-
const directories =
|
|
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
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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 (
|
|
2118
|
-
const backupPath = `${
|
|
2119
|
-
await
|
|
2120
|
-
|
|
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
|
-
|
|
2163
|
+
// Read current file content
|
|
2164
|
+
let content = await readFile(filePath, 'utf-8');
|
|
2124
2165
|
|
|
2125
|
-
// Apply
|
|
2166
|
+
// Apply each edit
|
|
2126
2167
|
for (const edit of operation.edits) {
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
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
|
-
|
|
2132
|
-
|
|
2174
|
+
content = content.replace(regex, newString);
|
|
2175
|
+
editsApplied += matches.length;
|
|
2133
2176
|
}
|
|
2134
2177
|
} else {
|
|
2135
|
-
if (
|
|
2136
|
-
|
|
2137
|
-
|
|
2178
|
+
if (content.includes(oldString)) {
|
|
2179
|
+
content = content.replace(oldString, newString);
|
|
2180
|
+
editsApplied++;
|
|
2138
2181
|
} else {
|
|
2139
|
-
|
|
2182
|
+
errors.push(`String not found: "${oldString.substring(0, 50)}${oldString.length > 50 ? '...' : ''}"`);
|
|
2140
2183
|
}
|
|
2141
2184
|
}
|
|
2142
2185
|
}
|
|
2143
2186
|
|
|
2144
|
-
// Write
|
|
2145
|
-
|
|
2146
|
-
await writeFile(resolvedPath, modifiedContent);
|
|
2147
|
-
}
|
|
2187
|
+
// Write updated content back
|
|
2188
|
+
await writeFile(filePath, content, 'utf-8');
|
|
2148
2189
|
} catch (error) {
|
|
2149
|
-
|
|
2190
|
+
errors.push(`File operation error: ${error instanceof Error ? error.message : String(error)}`);
|
|
2150
2191
|
}
|
|
2151
2192
|
|
|
2152
|
-
results.push(
|
|
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 ${
|
|
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(
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
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
|
-
|
|
2391
|
+
// Build command and arguments array safely
|
|
2392
|
+
const rgArgs: string[] = [];
|
|
2270
2393
|
|
|
2271
2394
|
// Add context lines
|
|
2272
|
-
if (beforeLines > 0
|
|
2273
|
-
|
|
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
|
-
|
|
2403
|
+
rgArgs.push('-n');
|
|
2278
2404
|
|
|
2279
2405
|
// Handle search type
|
|
2280
2406
|
if (type === 'regex') {
|
|
2281
|
-
|
|
2407
|
+
rgArgs.push('-e');
|
|
2282
2408
|
} else if (type === 'fuzzy') {
|
|
2283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2421
|
+
rgArgs.push('--glob', `!${path}`);
|
|
2296
2422
|
});
|
|
2297
2423
|
|
|
2298
2424
|
// Add max count
|
|
2299
|
-
|
|
2425
|
+
rgArgs.push('-m', maxResults.toString());
|
|
2300
2426
|
|
|
2301
|
-
// Add search paths
|
|
2302
|
-
|
|
2427
|
+
// Add the search query and paths
|
|
2428
|
+
rgArgs.push(query);
|
|
2429
|
+
rgArgs.push(...paths);
|
|
2303
2430
|
|
|
2304
|
-
|
|
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 =
|
|
2662
|
+
const fullPath = entryPath;
|
|
2515
2663
|
const relativePath = relative(resolvedPath, fullPath);
|
|
2516
2664
|
|
|
2517
2665
|
if (pattern) {
|