snow-ai 0.3.6 → 0.3.8

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 (127) hide show
  1. package/dist/agents/compactAgent.js +7 -3
  2. package/dist/agents/reviewAgent.d.ts +50 -0
  3. package/dist/agents/reviewAgent.js +264 -0
  4. package/dist/agents/summaryAgent.d.ts +34 -8
  5. package/dist/agents/summaryAgent.js +167 -164
  6. package/dist/api/anthropic.d.ts +1 -0
  7. package/dist/api/anthropic.js +118 -78
  8. package/dist/api/chat.d.ts +2 -1
  9. package/dist/api/chat.js +82 -52
  10. package/dist/api/gemini.d.ts +1 -0
  11. package/dist/api/gemini.js +110 -64
  12. package/dist/api/responses.d.ts +10 -1
  13. package/dist/api/responses.js +127 -79
  14. package/dist/api/systemPrompt.d.ts +1 -1
  15. package/dist/api/systemPrompt.js +36 -7
  16. package/dist/api/types.d.ts +8 -0
  17. package/dist/app.js +15 -2
  18. package/dist/hooks/useCommandHandler.d.ts +1 -0
  19. package/dist/hooks/useCommandHandler.js +102 -1
  20. package/dist/hooks/useCommandPanel.d.ts +2 -1
  21. package/dist/hooks/useCommandPanel.js +19 -1
  22. package/dist/hooks/useConversation.d.ts +4 -1
  23. package/dist/hooks/useConversation.js +91 -29
  24. package/dist/hooks/useKeyboardInput.js +19 -0
  25. package/dist/hooks/useSnapshotState.d.ts +2 -0
  26. package/dist/hooks/useTerminalFocus.js +13 -3
  27. package/dist/mcp/aceCodeSearch.d.ts +2 -76
  28. package/dist/mcp/aceCodeSearch.js +31 -467
  29. package/dist/mcp/bash.d.ts +1 -8
  30. package/dist/mcp/bash.js +20 -40
  31. package/dist/mcp/filesystem.d.ts +131 -111
  32. package/dist/mcp/filesystem.js +212 -375
  33. package/dist/mcp/ideDiagnostics.js +2 -4
  34. package/dist/mcp/todo.d.ts +1 -17
  35. package/dist/mcp/todo.js +11 -15
  36. package/dist/mcp/types/aceCodeSearch.types.d.ts +92 -0
  37. package/dist/mcp/types/aceCodeSearch.types.js +4 -0
  38. package/dist/mcp/types/bash.types.d.ts +13 -0
  39. package/dist/mcp/types/bash.types.js +4 -0
  40. package/dist/mcp/types/filesystem.types.d.ts +135 -0
  41. package/dist/mcp/types/filesystem.types.js +4 -0
  42. package/dist/mcp/types/todo.types.d.ts +27 -0
  43. package/dist/mcp/types/todo.types.js +4 -0
  44. package/dist/mcp/types/websearch.types.d.ts +30 -0
  45. package/dist/mcp/types/websearch.types.js +4 -0
  46. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.d.ts +34 -0
  47. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.js +146 -0
  48. package/dist/mcp/utils/aceCodeSearch/language.utils.d.ts +14 -0
  49. package/dist/mcp/utils/aceCodeSearch/language.utils.js +99 -0
  50. package/dist/mcp/utils/aceCodeSearch/search.utils.d.ts +31 -0
  51. package/dist/mcp/utils/aceCodeSearch/search.utils.js +136 -0
  52. package/dist/mcp/utils/aceCodeSearch/symbol.utils.d.ts +20 -0
  53. package/dist/mcp/utils/aceCodeSearch/symbol.utils.js +141 -0
  54. package/dist/mcp/utils/bash/security.utils.d.ts +20 -0
  55. package/dist/mcp/utils/bash/security.utils.js +34 -0
  56. package/dist/mcp/utils/filesystem/batch-operations.utils.d.ts +39 -0
  57. package/dist/mcp/utils/filesystem/batch-operations.utils.js +182 -0
  58. package/dist/mcp/utils/filesystem/code-analysis.utils.d.ts +18 -0
  59. package/dist/mcp/utils/filesystem/code-analysis.utils.js +165 -0
  60. package/dist/mcp/utils/filesystem/match-finder.utils.d.ts +16 -0
  61. package/dist/mcp/utils/filesystem/match-finder.utils.js +85 -0
  62. package/dist/mcp/utils/filesystem/similarity.utils.d.ts +22 -0
  63. package/dist/mcp/utils/filesystem/similarity.utils.js +75 -0
  64. package/dist/mcp/utils/todo/date.utils.d.ts +9 -0
  65. package/dist/mcp/utils/todo/date.utils.js +14 -0
  66. package/dist/mcp/utils/websearch/browser.utils.d.ts +8 -0
  67. package/dist/mcp/utils/websearch/browser.utils.js +58 -0
  68. package/dist/mcp/utils/websearch/text.utils.d.ts +16 -0
  69. package/dist/mcp/utils/websearch/text.utils.js +39 -0
  70. package/dist/mcp/websearch.d.ts +1 -31
  71. package/dist/mcp/websearch.js +21 -97
  72. package/dist/ui/components/ChatInput.d.ts +3 -1
  73. package/dist/ui/components/ChatInput.js +12 -5
  74. package/dist/ui/components/CommandPanel.d.ts +2 -1
  75. package/dist/ui/components/CommandPanel.js +18 -3
  76. package/dist/ui/components/MarkdownRenderer.d.ts +1 -2
  77. package/dist/ui/components/MarkdownRenderer.js +25 -153
  78. package/dist/ui/components/MessageList.js +5 -5
  79. package/dist/ui/components/PendingMessages.js +1 -1
  80. package/dist/ui/components/PendingToolCalls.d.ts +11 -0
  81. package/dist/ui/components/PendingToolCalls.js +35 -0
  82. package/dist/ui/components/SessionListScreen.js +37 -17
  83. package/dist/ui/components/ToolResultPreview.d.ts +1 -1
  84. package/dist/ui/components/ToolResultPreview.js +119 -155
  85. package/dist/ui/components/UsagePanel.d.ts +2 -0
  86. package/dist/ui/components/UsagePanel.js +360 -0
  87. package/dist/ui/pages/ChatScreen.d.ts +5 -0
  88. package/dist/ui/pages/ChatScreen.js +164 -85
  89. package/dist/ui/pages/ConfigScreen.js +23 -19
  90. package/dist/ui/pages/HeadlessModeScreen.js +2 -4
  91. package/dist/ui/pages/SubAgentConfigScreen.js +17 -17
  92. package/dist/ui/pages/SystemPromptConfigScreen.js +7 -6
  93. package/dist/utils/chatExporter.d.ts +9 -0
  94. package/dist/utils/chatExporter.js +126 -0
  95. package/dist/utils/commandExecutor.d.ts +3 -3
  96. package/dist/utils/commandExecutor.js +4 -4
  97. package/dist/utils/commands/export.d.ts +2 -0
  98. package/dist/utils/commands/export.js +12 -0
  99. package/dist/utils/commands/home.d.ts +2 -0
  100. package/dist/utils/commands/home.js +12 -0
  101. package/dist/utils/commands/init.js +3 -3
  102. package/dist/utils/commands/review.d.ts +2 -0
  103. package/dist/utils/commands/review.js +81 -0
  104. package/dist/utils/commands/role.d.ts +2 -0
  105. package/dist/utils/commands/role.js +37 -0
  106. package/dist/utils/commands/usage.d.ts +2 -0
  107. package/dist/utils/commands/usage.js +12 -0
  108. package/dist/utils/contextCompressor.js +99 -367
  109. package/dist/utils/fileDialog.d.ts +9 -0
  110. package/dist/utils/fileDialog.js +74 -0
  111. package/dist/utils/incrementalSnapshot.d.ts +7 -0
  112. package/dist/utils/incrementalSnapshot.js +35 -0
  113. package/dist/utils/mcpToolsManager.js +12 -12
  114. package/dist/utils/messageFormatter.js +89 -6
  115. package/dist/utils/proxyUtils.d.ts +15 -0
  116. package/dist/utils/proxyUtils.js +50 -0
  117. package/dist/utils/retryUtils.d.ts +27 -0
  118. package/dist/utils/retryUtils.js +114 -2
  119. package/dist/utils/sessionConverter.js +11 -0
  120. package/dist/utils/sessionManager.d.ts +7 -5
  121. package/dist/utils/sessionManager.js +60 -82
  122. package/dist/utils/terminal.js +4 -3
  123. package/dist/utils/toolDisplayConfig.d.ts +16 -0
  124. package/dist/utils/toolDisplayConfig.js +42 -0
  125. package/dist/utils/usageLogger.d.ts +11 -0
  126. package/dist/utils/usageLogger.js +99 -0
  127. package/package.json +3 -7
@@ -6,6 +6,11 @@ import { promisify } from 'util';
6
6
  import { vscodeConnection } from '../utils/vscodeConnection.js';
7
7
  import { incrementalSnapshotManager } from '../utils/incrementalSnapshot.js';
8
8
  import { tryUnescapeFix, trimPairIfPossible, isOverEscaped, } from '../utils/escapeHandler.js';
9
+ // Utility functions
10
+ import { calculateSimilarity, normalizeForDisplay, } from './utils/filesystem/similarity.utils.js';
11
+ import { analyzeCodeStructure, findSmartContextBoundaries, } from './utils/filesystem/code-analysis.utils.js';
12
+ import { findClosestMatches, generateDiffMessage, } from './utils/filesystem/match-finder.utils.js';
13
+ import { parseEditBySearchParams, parseEditByLineParams, executeBatchOperation, } from './utils/filesystem/batch-operations.utils.js';
9
14
  const { resolve, dirname, isAbsolute } = path;
10
15
  const execAsync = promisify(exec);
11
16
  /**
@@ -47,328 +52,11 @@ export class FilesystemMCPService {
47
52
  });
48
53
  this.basePath = resolve(basePath);
49
54
  }
50
- /**
51
- * Calculate similarity between two strings using a smarter algorithm
52
- * This normalizes whitespace first to avoid false negatives from spacing differences
53
- * Returns a value between 0 (completely different) and 1 (identical)
54
- */
55
- calculateSimilarity(str1, str2, threshold = 0) {
56
- // Normalize whitespace for comparison: collapse all whitespace to single spaces
57
- const normalize = (s) => s.replace(/\s+/g, ' ').trim();
58
- const norm1 = normalize(str1);
59
- const norm2 = normalize(str2);
60
- const len1 = norm1.length;
61
- const len2 = norm2.length;
62
- if (len1 === 0)
63
- return len2 === 0 ? 1 : 0;
64
- if (len2 === 0)
65
- return 0;
66
- // Quick length check - if lengths differ too much, similarity can't be above threshold
67
- const maxLen = Math.max(len1, len2);
68
- const minLen = Math.min(len1, len2);
69
- const lengthRatio = minLen / maxLen;
70
- if (threshold > 0 && lengthRatio < threshold) {
71
- return lengthRatio; // Can't possibly meet threshold
72
- }
73
- // Use Levenshtein distance for better similarity calculation
74
- const distance = this.levenshteinDistance(norm1, norm2, Math.ceil(maxLen * (1 - threshold)));
75
- return 1 - distance / maxLen;
76
- }
77
- /**
78
- * Calculate Levenshtein distance between two strings with early termination
79
- * @param str1 First string
80
- * @param str2 Second string
81
- * @param maxDistance Maximum distance to compute (early exit if exceeded)
82
- * @returns Levenshtein distance, or maxDistance+1 if exceeded
83
- */
84
- levenshteinDistance(str1, str2, maxDistance = Infinity) {
85
- const len1 = str1.length;
86
- const len2 = str2.length;
87
- // Quick exit for identical strings
88
- if (str1 === str2)
89
- return 0;
90
- // Quick exit if length difference already exceeds maxDistance
91
- if (Math.abs(len1 - len2) > maxDistance) {
92
- return maxDistance + 1;
93
- }
94
- // Use single-row algorithm to save memory (only need previous row)
95
- let prevRow = Array.from({ length: len2 + 1 }, (_, i) => i);
96
- for (let i = 1; i <= len1; i++) {
97
- const currRow = [i];
98
- let minInRow = i; // Track minimum value in current row
99
- for (let j = 1; j <= len2; j++) {
100
- const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
101
- const val = Math.min(prevRow[j] + 1, // deletion
102
- currRow[j - 1] + 1, // insertion
103
- prevRow[j - 1] + cost // substitution
104
- );
105
- currRow[j] = val;
106
- minInRow = Math.min(minInRow, val);
107
- }
108
- // Early termination: if minimum in this row exceeds maxDistance, we can stop
109
- if (minInRow > maxDistance) {
110
- return maxDistance + 1;
111
- }
112
- prevRow = currRow;
113
- }
114
- return prevRow[len2];
115
- }
116
- /**
117
- * Find the closest matching candidates in the file content
118
- * Returns top N candidates sorted by similarity
119
- * Optimized with safe pre-filtering and early exit
120
- * ASYNC to prevent terminal freeze during search
121
- */
122
- async findClosestMatches(searchContent, fileLines, topN = 3) {
123
- const searchLines = searchContent.split('\n');
124
- const candidates = [];
125
- // Normalize whitespace for display only (makes preview more readable)
126
- const normalizeForDisplay = (line) => line.replace(/\t/g, ' ').replace(/ +/g, ' ');
127
- // Fast pre-filter: use first line as anchor (only for multi-line searches)
128
- const searchFirstLine = searchLines[0]?.replace(/\s+/g, ' ').trim() || '';
129
- const threshold = 0.5;
130
- const usePreFilter = searchLines.length >= 5; // Only for 5+ line searches
131
- const preFilterThreshold = 0.2; // Very conservative - only skip completely unrelated lines
132
- // Try to find candidates by sliding window with optimizations
133
- const maxCandidates = topN * 3; // Collect more candidates, then pick best
134
- const YIELD_INTERVAL = 100; // Yield control every 100 iterations
135
- for (let i = 0; i <= fileLines.length - searchLines.length; i++) {
136
- // Yield control periodically to prevent UI freeze
137
- if (i % YIELD_INTERVAL === 0) {
138
- await new Promise(resolve => setImmediate(resolve));
139
- }
140
- // Quick pre-filter: check first line similarity (only for multi-line)
141
- if (usePreFilter) {
142
- const firstLineCandidate = fileLines[i]?.replace(/\s+/g, ' ').trim() || '';
143
- const firstLineSimilarity = this.calculateSimilarity(searchFirstLine, firstLineCandidate, preFilterThreshold);
144
- // Skip only if first line is very different
145
- if (firstLineSimilarity < preFilterThreshold) {
146
- continue;
147
- }
148
- }
149
- // Full candidate check
150
- const candidateLines = fileLines.slice(i, i + searchLines.length);
151
- const candidateContent = candidateLines.join('\n');
152
- const similarity = this.calculateSimilarity(searchContent, candidateContent, threshold);
153
- // Only consider candidates with >50% similarity
154
- if (similarity > threshold) {
155
- candidates.push({
156
- startLine: i + 1,
157
- endLine: i + searchLines.length,
158
- similarity,
159
- preview: candidateLines
160
- .map((line, idx) => `${i + idx + 1}→${normalizeForDisplay(line)}`)
161
- .join('\n'),
162
- });
163
- // Early exit if we found a nearly perfect match
164
- if (similarity >= 0.95) {
165
- break;
166
- }
167
- // Limit candidates to avoid excessive computation
168
- if (candidates.length >= maxCandidates) {
169
- break;
170
- }
171
- }
172
- }
173
- // Sort by similarity descending and return top N
174
- return candidates
175
- .sort((a, b) => b.similarity - a.similarity)
176
- .slice(0, topN);
177
- }
178
- /**
179
- * Generate a helpful diff message showing differences between search and actual content
180
- * Note: This is ONLY for display purposes. Tabs/spaces are normalized for better readability.
181
- */
182
- generateDiffMessage(searchContent, actualContent, maxLines = 10) {
183
- const searchLines = searchContent.split('\n');
184
- const actualLines = actualContent.split('\n');
185
- const diffLines = [];
186
- const maxLen = Math.max(searchLines.length, actualLines.length);
187
- // Normalize whitespace for display only (makes diff more readable)
188
- const normalizeForDisplay = (line) => line.replace(/\t/g, ' ').replace(/ +/g, ' ');
189
- for (let i = 0; i < Math.min(maxLen, maxLines); i++) {
190
- const searchLine = searchLines[i] || '';
191
- const actualLine = actualLines[i] || '';
192
- if (searchLine !== actualLine) {
193
- diffLines.push(`Line ${i + 1}:`);
194
- diffLines.push(` Search: ${JSON.stringify(normalizeForDisplay(searchLine))}`);
195
- diffLines.push(` Actual: ${JSON.stringify(normalizeForDisplay(actualLine))}`);
196
- }
197
- }
198
- if (maxLen > maxLines) {
199
- diffLines.push(`... (${maxLen - maxLines} more lines)`);
200
- }
201
- return diffLines.join('\n');
202
- }
203
- /**
204
- * Analyze code structure for balance and completeness
205
- * Helps AI identify bracket mismatches, unclosed tags, and boundary issues
206
- */
207
- analyzeCodeStructure(_content, filePath, editedLines) {
208
- const analysis = {
209
- bracketBalance: {
210
- curly: { open: 0, close: 0, balanced: true },
211
- round: { open: 0, close: 0, balanced: true },
212
- square: { open: 0, close: 0, balanced: true },
213
- },
214
- indentationWarnings: [],
215
- };
216
- // Count brackets in the edited content
217
- const editedContent = editedLines.join('\n');
218
- // Remove string literals and comments to avoid false positives
219
- const cleanContent = editedContent
220
- .replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, '""') // Remove strings
221
- .replace(/\/\/.*$/gm, '') // Remove single-line comments
222
- .replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments
223
- // Count brackets
224
- analysis.bracketBalance.curly.open = (cleanContent.match(/\{/g) || []).length;
225
- analysis.bracketBalance.curly.close = (cleanContent.match(/\}/g) || []).length;
226
- analysis.bracketBalance.curly.balanced =
227
- analysis.bracketBalance.curly.open ===
228
- analysis.bracketBalance.curly.close;
229
- analysis.bracketBalance.round.open = (cleanContent.match(/\(/g) || []).length;
230
- analysis.bracketBalance.round.close = (cleanContent.match(/\)/g) || []).length;
231
- analysis.bracketBalance.round.balanced =
232
- analysis.bracketBalance.round.open ===
233
- analysis.bracketBalance.round.close;
234
- analysis.bracketBalance.square.open = (cleanContent.match(/\[/g) || []).length;
235
- analysis.bracketBalance.square.close = (cleanContent.match(/\]/g) || []).length;
236
- analysis.bracketBalance.square.balanced =
237
- analysis.bracketBalance.square.open ===
238
- analysis.bracketBalance.square.close;
239
- // HTML/JSX tag analysis (for .html, .jsx, .tsx, .vue files)
240
- const isMarkupFile = /\.(html|jsx|tsx|vue)$/i.test(filePath);
241
- if (isMarkupFile) {
242
- const tagPattern = /<\/?([a-zA-Z][a-zA-Z0-9-]*)[^>]*>/g;
243
- const selfClosingPattern = /<[a-zA-Z][a-zA-Z0-9-]*[^>]*\/>/g;
244
- // Remove self-closing tags
245
- const contentWithoutSelfClosing = cleanContent.replace(selfClosingPattern, '');
246
- const tags = [];
247
- const unclosedTags = [];
248
- const unopenedTags = [];
249
- let match;
250
- while ((match = tagPattern.exec(contentWithoutSelfClosing)) !== null) {
251
- const isClosing = match[0]?.startsWith('</');
252
- const tagName = match[1]?.toLowerCase();
253
- if (!tagName)
254
- continue;
255
- if (isClosing) {
256
- const lastOpenTag = tags.pop();
257
- if (!lastOpenTag || lastOpenTag !== tagName) {
258
- unopenedTags.push(tagName);
259
- if (lastOpenTag)
260
- tags.push(lastOpenTag); // Put it back
261
- }
262
- }
263
- else {
264
- tags.push(tagName);
265
- }
266
- }
267
- unclosedTags.push(...tags);
268
- analysis.htmlTags = {
269
- unclosedTags,
270
- unopenedTags,
271
- balanced: unclosedTags.length === 0 && unopenedTags.length === 0,
272
- };
273
- }
274
- // Check indentation consistency
275
- const lines = editedContent.split('\n');
276
- const indents = lines
277
- .filter(line => line.trim().length > 0)
278
- .map(line => {
279
- const match = line.match(/^(\s*)/);
280
- return match ? match[1] : '';
281
- })
282
- .filter((indent) => indent !== undefined);
283
- // Detect mixed tabs/spaces
284
- const hasTabs = indents.some(indent => indent.includes('\t'));
285
- const hasSpaces = indents.some(indent => indent.includes(' '));
286
- if (hasTabs && hasSpaces) {
287
- analysis.indentationWarnings.push('Mixed tabs and spaces detected');
288
- }
289
- // Detect inconsistent indentation levels (spaces only)
290
- if (!hasTabs && hasSpaces) {
291
- const spaceCounts = indents
292
- .filter(indent => indent.length > 0)
293
- .map(indent => indent.length);
294
- if (spaceCounts.length > 1) {
295
- const gcd = spaceCounts.reduce((a, b) => {
296
- while (b !== 0) {
297
- const temp = b;
298
- b = a % b;
299
- a = temp;
300
- }
301
- return a;
302
- });
303
- const hasInconsistent = spaceCounts.some(count => count % gcd !== 0 && gcd > 1);
304
- if (hasInconsistent) {
305
- analysis.indentationWarnings.push(`Inconsistent indentation (expected multiples of ${gcd} spaces)`);
306
- }
307
- }
308
- }
309
- // Note: Boundary checking removed - AI should be free to edit partial code blocks
310
- // The bracket balance check above is sufficient for detecting real issues
311
- return analysis;
312
- }
313
- /**
314
- * Find smart context boundaries for editing
315
- * Expands context to include complete code blocks when possible
316
- */
317
- findSmartContextBoundaries(lines, startLine, endLine, requestedContext) {
318
- const totalLines = lines.length;
319
- let contextStart = Math.max(1, startLine - requestedContext);
320
- let contextEnd = Math.min(totalLines, endLine + requestedContext);
321
- let extended = false;
322
- // Try to find the start of the enclosing block
323
- let bracketDepth = 0;
324
- for (let i = startLine - 1; i >= Math.max(0, startLine - 50); i--) {
325
- const line = lines[i];
326
- if (!line)
327
- continue;
328
- const trimmed = line.trim();
329
- // Count brackets (simple approach)
330
- const openBrackets = (line.match(/\{/g) || []).length;
331
- const closeBrackets = (line.match(/\}/g) || []).length;
332
- bracketDepth += closeBrackets - openBrackets;
333
- // If we find a function/class/block definition with balanced brackets
334
- if (bracketDepth === 0 &&
335
- (trimmed.match(/^(function|class|const|let|var|if|for|while|async|export)\s/i) ||
336
- trimmed.match(/=>\s*\{/) ||
337
- trimmed.match(/^\w+\s*\(/))) {
338
- if (i + 1 < contextStart) {
339
- contextStart = i + 1;
340
- extended = true;
341
- }
342
- break;
343
- }
344
- }
345
- // Try to find the end of the enclosing block
346
- bracketDepth = 0;
347
- for (let i = endLine - 1; i < Math.min(totalLines, endLine + 50); i++) {
348
- const line = lines[i];
349
- if (!line)
350
- continue;
351
- const trimmed = line.trim();
352
- // Count brackets
353
- const openBrackets = (line.match(/\{/g) || []).length;
354
- const closeBrackets = (line.match(/\}/g) || []).length;
355
- bracketDepth += openBrackets - closeBrackets;
356
- // If we find a closing bracket at depth 0
357
- if (bracketDepth === 0 && trimmed.startsWith('}')) {
358
- if (i + 1 > contextEnd) {
359
- contextEnd = i + 1;
360
- extended = true;
361
- }
362
- break;
363
- }
364
- }
365
- return { start: contextStart, end: contextEnd, extended };
366
- }
367
55
  /**
368
56
  * Get the content of a file with optional line range
369
- * @param filePath - Path to the file (relative to base path or absolute) or array of file paths
370
- * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1)
371
- * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to 500 or file end)
57
+ * @param filePath - Path to the file (relative to base path or absolute) or array of file paths or array of file config objects
58
+ * @param startLine - Starting line number (1-indexed, inclusive, optional - defaults to 1). Used for single file or as default for array of strings
59
+ * @param endLine - Ending line number (1-indexed, inclusive, optional - defaults to file end). Used for single file or as default for array of strings
372
60
  * @returns Object containing the requested content with line numbers and metadata
373
61
  * @throws Error if file doesn't exist or cannot be read
374
62
  */
@@ -378,8 +66,24 @@ export class FilesystemMCPService {
378
66
  if (Array.isArray(filePath)) {
379
67
  const filesData = [];
380
68
  const allContents = [];
381
- for (const file of filePath) {
69
+ for (const fileItem of filePath) {
382
70
  try {
71
+ // Support both string format and object format
72
+ let file;
73
+ let fileStartLine;
74
+ let fileEndLine;
75
+ if (typeof fileItem === 'string') {
76
+ // String format: use global startLine/endLine
77
+ file = fileItem;
78
+ fileStartLine = startLine;
79
+ fileEndLine = endLine;
80
+ }
81
+ else {
82
+ // Object format: use per-file startLine/endLine
83
+ file = fileItem.path;
84
+ fileStartLine = fileItem.startLine ?? startLine;
85
+ fileEndLine = fileItem.endLine ?? endLine;
86
+ }
383
87
  const fullPath = this.resolvePath(file);
384
88
  // For absolute paths, skip validation to allow access outside base path
385
89
  if (!isAbsolute(file)) {
@@ -402,9 +106,9 @@ export class FilesystemMCPService {
402
106
  const content = await fs.readFile(fullPath, 'utf-8');
403
107
  const lines = content.split('\n');
404
108
  const totalLines = lines.length;
405
- // Default values and logic
406
- const actualStartLine = startLine ?? 1;
407
- const actualEndLine = endLine ?? totalLines;
109
+ // Default values and logic (use file-specific values)
110
+ const actualStartLine = fileStartLine ?? 1;
111
+ const actualEndLine = fileEndLine ?? totalLines;
408
112
  // Validate and adjust line numbers
409
113
  if (actualStartLine < 1) {
410
114
  throw new Error(`Start line must be greater than 0 for ${file}`);
@@ -434,7 +138,9 @@ export class FilesystemMCPService {
434
138
  }
435
139
  catch (error) {
436
140
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
437
- allContents.push(`❌ ${file}: ${errorMsg}`);
141
+ // Extract file path for error message
142
+ const filePath = typeof fileItem === 'string' ? fileItem : fileItem.path;
143
+ allContents.push(`❌ ${filePath}: ${errorMsg}`);
438
144
  }
439
145
  }
440
146
  return {
@@ -644,18 +350,35 @@ export class FilesystemMCPService {
644
350
  }
645
351
  }
646
352
  /**
647
- * Edit a file by searching for exact content and replacing it
353
+ * Edit file(s) by searching for exact content and replacing it
648
354
  * This method uses SMART MATCHING to handle whitespace differences automatically.
649
355
  *
650
- * @param filePath - Path to the file to edit
651
- * @param searchContent - Content to search for (whitespace will be normalized automatically)
652
- * @param replaceContent - New content to replace the search content with
356
+ * @param filePath - Path to the file to edit, or array of file paths, or array of edit config objects
357
+ * @param searchContent - Content to search for (for single file or unified mode)
358
+ * @param replaceContent - New content to replace (for single file or unified mode)
653
359
  * @param occurrence - Which occurrence to replace (1-indexed, default: 1, use -1 for all)
654
360
  * @param contextLines - Number of context lines to return before and after the edit (default: 8)
655
361
  * @returns Object containing success message, before/after comparison, and diagnostics from IDE (VSCode or JetBrains)
656
362
  * @throws Error if search content is not found or multiple matches exist
657
363
  */
658
364
  async editFileBySearch(filePath, searchContent, replaceContent, occurrence = 1, contextLines = 8) {
365
+ // Handle array of files
366
+ if (Array.isArray(filePath)) {
367
+ return await executeBatchOperation(filePath, fileItem => parseEditBySearchParams(fileItem, searchContent, replaceContent, occurrence), (path, search, replace, occ) => this.editFileBySearchSingle(path, search, replace, occ, contextLines), (path, result) => {
368
+ return { path, ...result };
369
+ });
370
+ }
371
+ // Single file mode
372
+ if (!searchContent || !replaceContent) {
373
+ throw new Error('searchContent and replaceContent are required for single file mode');
374
+ }
375
+ return await this.editFileBySearchSingle(filePath, searchContent, replaceContent, occurrence, contextLines);
376
+ }
377
+ /**
378
+ * Internal method: Edit a single file by search-replace
379
+ * @private
380
+ */
381
+ async editFileBySearchSingle(filePath, searchContent, replaceContent, occurrence, contextLines) {
659
382
  try {
660
383
  const fullPath = this.resolvePath(filePath);
661
384
  // For absolute paths, skip validation to allow access outside base path
@@ -693,7 +416,7 @@ export class FilesystemMCPService {
693
416
  // Quick pre-filter: check first line similarity (only for multi-line searches)
694
417
  if (usePreFilter) {
695
418
  const firstLineCandidate = contentLines[i]?.replace(/\s+/g, ' ').trim() || '';
696
- const firstLineSimilarity = this.calculateSimilarity(searchFirstLine, firstLineCandidate, preFilterThreshold);
419
+ const firstLineSimilarity = calculateSimilarity(searchFirstLine, firstLineCandidate, preFilterThreshold);
697
420
  // Skip only if first line is very different (< 30% match)
698
421
  // This is safe because if first line differs this much, full match unlikely
699
422
  if (firstLineSimilarity < preFilterThreshold) {
@@ -703,7 +426,7 @@ export class FilesystemMCPService {
703
426
  // Full candidate check
704
427
  const candidateLines = contentLines.slice(i, i + searchLines.length);
705
428
  const candidateContent = candidateLines.join('\n');
706
- const similarity = this.calculateSimilarity(normalizedSearch, candidateContent, threshold);
429
+ const similarity = calculateSimilarity(normalizedSearch, candidateContent, threshold);
707
430
  // Accept matches above threshold
708
431
  if (similarity >= threshold) {
709
432
  matches.push({
@@ -737,7 +460,7 @@ export class FilesystemMCPService {
737
460
  }
738
461
  const candidateLines = contentLines.slice(i, i + correctedSearchLines.length);
739
462
  const candidateContent = candidateLines.join('\n');
740
- const similarity = this.calculateSimilarity(unescapeFix.correctedString, candidateContent);
463
+ const similarity = calculateSimilarity(unescapeFix.correctedString, candidateContent);
741
464
  if (similarity >= threshold) {
742
465
  matches.push({
743
466
  startLine: i + 1,
@@ -760,7 +483,7 @@ export class FilesystemMCPService {
760
483
  // If still no matches after unescape, provide detailed error
761
484
  if (matches.length === 0) {
762
485
  // Find closest matches for suggestions
763
- const closestMatches = await this.findClosestMatches(normalizedSearch, normalizedContent.split('\n'), 3);
486
+ const closestMatches = await findClosestMatches(normalizedSearch, normalizedContent.split('\n'), 3);
764
487
  let errorMessage = `❌ Search content not found in file: ${filePath}\n\n`;
765
488
  errorMessage += `🔍 Using smart fuzzy matching (threshold: 60%)\n`;
766
489
  if (isOverEscaped(searchContent)) {
@@ -778,23 +501,22 @@ export class FilesystemMCPService {
778
501
  if (bestMatch) {
779
502
  const bestMatchLines = lines.slice(bestMatch.startLine - 1, bestMatch.endLine);
780
503
  const bestMatchContent = bestMatchLines.join('\n');
781
- const diffMsg = this.generateDiffMessage(normalizedSearch, bestMatchContent, 5);
504
+ const diffMsg = generateDiffMessage(normalizedSearch, bestMatchContent, 5);
782
505
  if (diffMsg) {
783
506
  errorMessage += `📊 Difference with closest match:\n${diffMsg}\n\n`;
784
507
  }
785
508
  }
786
509
  errorMessage += `💡 Suggestions:\n`;
787
- errorMessage += ` • Make sure you copied content from filesystem_read (without "123→")\n`;
510
+ errorMessage += ` • Make sure you copied content from filesystem-read (without "123→")\n`;
788
511
  errorMessage += ` • Whitespace differences are automatically handled\n`;
789
512
  errorMessage += ` • Try copying a larger or smaller code block\n`;
790
- errorMessage += ` • If multiple filesystem_edit_search attempts fail, use terminal_execute to edit via command line (e.g. sed, printf)\n`;
513
+ errorMessage += ` • If multiple filesystem-edit_search attempts fail, use terminal-execute to edit via command line (e.g. sed, printf)\n`;
791
514
  errorMessage += `⚠️ No similar content found in the file.\n\n`;
792
515
  errorMessage += `📝 What you searched for (first 5 lines, formatted):\n`;
793
- const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ').trim();
794
516
  searchLines.slice(0, 5).forEach((line, idx) => {
795
517
  errorMessage += `${idx + 1}. ${JSON.stringify(normalizeForDisplay(line))}\n`;
796
518
  });
797
- errorMessage += `\n💡 Copy exact content from filesystem_read (without line numbers)\n`;
519
+ errorMessage += `\n💡 Copy exact content from filesystem-read (without line numbers)\n`;
798
520
  }
799
521
  throw new Error(errorMessage);
800
522
  }
@@ -829,7 +551,6 @@ export class FilesystemMCPService {
829
551
  const modifiedLines = [...beforeLines, ...replaceLines, ...afterLines];
830
552
  const modifiedContent = modifiedLines.join('\n');
831
553
  // Calculate replaced content for display (compress whitespace for readability)
832
- const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ');
833
554
  const replacedLines = lines.slice(startLine - 1, endLine);
834
555
  const replacedContent = replacedLines
835
556
  .map((line, idx) => {
@@ -839,7 +560,7 @@ export class FilesystemMCPService {
839
560
  .join('\n');
840
561
  // Calculate context boundaries
841
562
  const lineDifference = replaceLines.length - (endLine - startLine + 1);
842
- const smartBoundaries = this.findSmartContextBoundaries(lines, startLine, endLine, contextLines);
563
+ const smartBoundaries = findSmartContextBoundaries(lines, startLine, endLine, contextLines);
843
564
  const contextStart = smartBoundaries.start;
844
565
  const contextEnd = smartBoundaries.end;
845
566
  // Extract old content for context (compress whitespace for readability)
@@ -885,7 +606,7 @@ export class FilesystemMCPService {
885
606
  .join('\n');
886
607
  // Analyze code structure
887
608
  const editedContentLines = replaceLines;
888
- const structureAnalysis = this.analyzeCodeStructure(finalContent, filePath, editedContentLines);
609
+ const structureAnalysis = analyzeCodeStructure(finalContent, filePath, editedContentLines);
889
610
  // Get diagnostics from IDE (VSCode or JetBrains) - non-blocking, fire-and-forget
890
611
  let diagnostics = [];
891
612
  try {
@@ -969,7 +690,7 @@ export class FilesystemMCPService {
969
690
  }
970
691
  }
971
692
  if (structureAnalysis.indentationWarnings.length > 0) {
972
- structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
693
+ structureWarnings.push(...structureAnalysis.indentationWarnings.map((w) => `Indentation: ${w}`));
973
694
  }
974
695
  // Note: Boundary warnings removed - partial edits are common and expected
975
696
  if (structureWarnings.length > 0) {
@@ -986,19 +707,38 @@ export class FilesystemMCPService {
986
707
  }
987
708
  }
988
709
  /**
989
- * Edit a file by replacing lines within a specified range
710
+ * Edit file(s) by replacing lines within a specified range
990
711
  * BEST PRACTICE: Keep edits small and focused (≤15 lines recommended) for better accuracy.
991
712
  * For larger changes, make multiple parallel edits to non-overlapping sections instead of one large edit.
992
713
  *
993
- * @param filePath - Path to the file to edit
994
- * @param startLine - Starting line number (1-indexed, inclusive) - get from filesystem_read output
995
- * @param endLine - Ending line number (1-indexed, inclusive) - get from filesystem_read output
996
- * @param newContent - New content to replace the specified lines (WITHOUT line numbers)
714
+ * @param filePath - Path to the file to edit, or array of file paths, or array of edit config objects
715
+ * @param startLine - Starting line number (for single file or unified mode)
716
+ * @param endLine - Ending line number (for single file or unified mode)
717
+ * @param newContent - New content to replace (for single file or unified mode)
997
718
  * @param contextLines - Number of context lines to return before and after the edit (default: 8)
998
719
  * @returns Object containing success message, precise before/after comparison, and diagnostics from IDE (VSCode or JetBrains)
999
720
  * @throws Error if file editing fails
1000
721
  */
1001
722
  async editFile(filePath, startLine, endLine, newContent, contextLines = 8) {
723
+ // Handle array of files
724
+ if (Array.isArray(filePath)) {
725
+ return await executeBatchOperation(filePath, fileItem => parseEditByLineParams(fileItem, startLine, endLine, newContent), (path, start, end, content) => this.editFileSingle(path, start, end, content, contextLines), (path, result) => {
726
+ return { path, ...result };
727
+ });
728
+ }
729
+ // Single file mode
730
+ if (startLine === undefined ||
731
+ endLine === undefined ||
732
+ newContent === undefined) {
733
+ throw new Error('startLine, endLine, and newContent are required for single file mode');
734
+ }
735
+ return await this.editFileSingle(filePath, startLine, endLine, newContent, contextLines);
736
+ }
737
+ /**
738
+ * Internal method: Edit a single file by line range
739
+ * @private
740
+ */
741
+ async editFileSingle(filePath, startLine, endLine, newContent, contextLines) {
1002
742
  try {
1003
743
  const fullPath = this.resolvePath(filePath);
1004
744
  // For absolute paths, skip validation to allow access outside base path
@@ -1026,7 +766,6 @@ export class FilesystemMCPService {
1026
766
  await incrementalSnapshotManager.backupFile(fullPath);
1027
767
  // Extract the lines that will be replaced (for comparison)
1028
768
  // Compress whitespace for display readability
1029
- const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ');
1030
769
  const replacedLines = lines.slice(startLine - 1, adjustedEndLine);
1031
770
  const replacedContent = replacedLines
1032
771
  .map((line, idx) => {
@@ -1035,7 +774,7 @@ export class FilesystemMCPService {
1035
774
  })
1036
775
  .join('\n');
1037
776
  // Calculate context range using smart boundary detection
1038
- const smartBoundaries = this.findSmartContextBoundaries(lines, startLine, adjustedEndLine, contextLines);
777
+ const smartBoundaries = findSmartContextBoundaries(lines, startLine, adjustedEndLine, contextLines);
1039
778
  const contextStart = smartBoundaries.start;
1040
779
  const contextEnd = smartBoundaries.end;
1041
780
  // Extract old content for context (compress whitespace for readability)
@@ -1100,7 +839,7 @@ export class FilesystemMCPService {
1100
839
  }
1101
840
  // Analyze code structure of the edited content (using formatted content if available)
1102
841
  const editedContentLines = finalLines.slice(startLine - 1, startLine - 1 + newContentLines.length);
1103
- const structureAnalysis = this.analyzeCodeStructure(finalLines.join('\n'), filePath, editedContentLines);
842
+ const structureAnalysis = analyzeCodeStructure(finalLines.join('\n'), filePath, editedContentLines);
1104
843
  // Try to get diagnostics from IDE (VSCode or JetBrains) after editing (non-blocking)
1105
844
  let diagnostics = [];
1106
845
  try {
@@ -1185,7 +924,7 @@ export class FilesystemMCPService {
1185
924
  }
1186
925
  // Check indentation
1187
926
  if (structureAnalysis.indentationWarnings.length > 0) {
1188
- structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
927
+ structureWarnings.push(...structureAnalysis.indentationWarnings.map((w) => `Indentation: ${w}`));
1189
928
  }
1190
929
  // Note: Boundary warnings removed - partial edits are common and expected
1191
930
  // Format structure warnings
@@ -1234,8 +973,8 @@ export class FilesystemMCPService {
1234
973
  export const filesystemService = new FilesystemMCPService();
1235
974
  export const mcpTools = [
1236
975
  {
1237
- name: 'filesystem_read',
1238
- description: '📖 Read file content with line numbers. **SUPPORTS MULTIPLE FILES**: Pass either a single file path (string) or multiple file paths (array of strings) to read in one call. ⚠️ **IMPORTANT WORKFLOW**: (1) ALWAYS use ACE search tools FIRST (ace_text_search/ace_search_symbols/ace_file_outline) to locate the relevant code, (2) ONLY use filesystem_read when you know the approximate location and need precise line numbers for editing. **ANTI-PATTERN**: Reading files line-by-line from the top wastes tokens - use search instead! **USAGE**: Call without parameters to read entire file(s), or specify startLine/endLine for partial reads. Returns content with line numbers (format: "123→code") for precise editing. **MULTI-FILE EXAMPLE**: filePath=["src/component.ts", "src/utils.ts"] reads both files together.',
976
+ name: 'filesystem-read',
977
+ description: '📖 Read file content with line numbers. **SUPPORTS MULTIPLE FILES WITH FLEXIBLE LINE RANGES**: Pass either (1) a single file path (string), (2) array of file paths (strings) with unified startLine/endLine, or (3) array of file config objects with per-file line ranges. ⚠️ **IMPORTANT WORKFLOW**: (1) ALWAYS use ACE search tools FIRST (ace-text_search/ace-search_symbols/ace-file_outline) to locate the relevant code, (2) ONLY use filesystem-read when you know the approximate location and need precise line numbers for editing. **ANTI-PATTERN**: Reading files line-by-line from the top wastes tokens - use search instead! **USAGE**: Call without parameters to read entire file(s), or specify startLine/endLine for partial reads. Returns content with line numbers (format: "123→code") for precise editing. **EXAMPLES**: (A) Unified: filePath=["a.ts", "b.ts"], startLine=1, endLine=50 reads lines 1-50 from both. (B) Per-file: filePath=[{path:"a.ts", startLine:1, endLine:30}, {path:"b.ts", startLine:100, endLine:150}] reads different ranges from each file.',
1239
978
  inputSchema: {
1240
979
  type: 'object',
1241
980
  properties: {
@@ -1250,25 +989,47 @@ export const mcpTools = [
1250
989
  items: {
1251
990
  type: 'string',
1252
991
  },
1253
- description: 'Array of file paths to read in one call',
992
+ description: 'Array of file paths to read in one call (uses unified startLine/endLine from top-level parameters)',
993
+ },
994
+ {
995
+ type: 'array',
996
+ items: {
997
+ type: 'object',
998
+ properties: {
999
+ path: {
1000
+ type: 'string',
1001
+ description: 'File path',
1002
+ },
1003
+ startLine: {
1004
+ type: 'number',
1005
+ description: 'Optional: Starting line for this file (overrides top-level startLine)',
1006
+ },
1007
+ endLine: {
1008
+ type: 'number',
1009
+ description: 'Optional: Ending line for this file (overrides top-level endLine)',
1010
+ },
1011
+ },
1012
+ required: ['path'],
1013
+ },
1014
+ description: 'Array of file config objects with per-file line ranges. Each file can have its own startLine/endLine.',
1254
1015
  },
1255
1016
  ],
1256
- description: 'Path to the file(s) to read (single string or array of strings)',
1017
+ description: 'Path to the file(s) to read: string, array of strings, or array of {path, startLine?, endLine?} objects',
1257
1018
  },
1258
1019
  startLine: {
1259
1020
  type: 'number',
1260
- description: 'Optional: Starting line number (1-indexed). Omit to read from line 1. Applied to all files.',
1021
+ description: 'Optional: Default starting line number (1-indexed) for all files. Omit to read from line 1. Can be overridden by per-file startLine in object format.',
1261
1022
  },
1262
1023
  endLine: {
1263
1024
  type: 'number',
1264
- description: 'Optional: Ending line number (1-indexed). Omit to read to end of file. Applied to all files.',
1025
+ description: 'Optional: Default ending line number (1-indexed) for all files. Omit to read to end of file. Can be overridden by per-file endLine in object format.',
1265
1026
  },
1266
1027
  },
1267
1028
  required: ['filePath'],
1268
1029
  },
1269
1030
  },
1270
1031
  {
1271
- name: 'filesystem_create',
1032
+ name: 'filesystem-create',
1272
1033
  description: 'PREFERRED tool for file creation: Create a new file with specified content. More reliable than terminal commands like echo/cat with redirects. Automatically creates parent directories if needed. Terminal commands can be used as a fallback if needed.',
1273
1034
  inputSchema: {
1274
1035
  type: 'object',
@@ -1291,7 +1052,7 @@ export const mcpTools = [
1291
1052
  },
1292
1053
  },
1293
1054
  {
1294
- name: 'filesystem_delete',
1055
+ name: 'filesystem-delete',
1295
1056
  description: 'Delete one or multiple files. Supports both single file and batch deletion.',
1296
1057
  inputSchema: {
1297
1058
  type: 'object',
@@ -1321,7 +1082,7 @@ export const mcpTools = [
1321
1082
  },
1322
1083
  },
1323
1084
  {
1324
- name: 'filesystem_list',
1085
+ name: 'filesystem-list',
1325
1086
  description: 'List files in a directory',
1326
1087
  inputSchema: {
1327
1088
  type: 'object',
@@ -1335,22 +1096,60 @@ export const mcpTools = [
1335
1096
  },
1336
1097
  },
1337
1098
  {
1338
- name: 'filesystem_edit_search',
1339
- description: '🎯 **RECOMMENDED** for most edits: Search-and-replace with SMART FUZZY MATCHING that automatically handles whitespace differences. **WORKFLOW**: (1) Use ace_text_search/ace_search_symbols to locate code, (2) Use filesystem_read to view content, (3) Copy the code block you want to change (without line numbers), (4) Use THIS tool - whitespace will be normalized automatically. **WHY**: No line tracking, auto-handles spacing/tabs, finds best match. **BEST FOR**: Modifying functions, fixing bugs, updating logic.',
1099
+ name: 'filesystem-edit_search',
1100
+ description: '🎯 **RECOMMENDED** for most edits: Search-and-replace with SMART FUZZY MATCHING. **SUPPORTS BATCH EDITING**: Pass (1) single file with search/replace, (2) array of file paths with unified search/replace, or (3) array of {path, searchContent, replaceContent, occurrence?} for per-file edits. **WORKFLOW**: (1) Use ace-text_search/ace-search_symbols to locate code, (2) Use filesystem-read to view content, (3) Copy code blocks (without line numbers), (4) Use THIS tool. **WHY**: No line tracking, auto-handles spacing/tabs, finds best match. **BATCH EXAMPLE**: filePath=[{path:"a.ts", searchContent:"old1", replaceContent:"new1"}, {path:"b.ts", searchContent:"old2", replaceContent:"new2"}]',
1340
1101
  inputSchema: {
1341
1102
  type: 'object',
1342
1103
  properties: {
1343
1104
  filePath: {
1344
- type: 'string',
1345
- description: 'Path to the file to edit',
1105
+ oneOf: [
1106
+ {
1107
+ type: 'string',
1108
+ description: 'Path to a single file to edit',
1109
+ },
1110
+ {
1111
+ type: 'array',
1112
+ items: {
1113
+ type: 'string',
1114
+ },
1115
+ description: 'Array of file paths (uses unified searchContent/replaceContent from top-level)',
1116
+ },
1117
+ {
1118
+ type: 'array',
1119
+ items: {
1120
+ type: 'object',
1121
+ properties: {
1122
+ path: {
1123
+ type: 'string',
1124
+ description: 'File path',
1125
+ },
1126
+ searchContent: {
1127
+ type: 'string',
1128
+ description: 'Content to search for in this file',
1129
+ },
1130
+ replaceContent: {
1131
+ type: 'string',
1132
+ description: 'New content to replace with',
1133
+ },
1134
+ occurrence: {
1135
+ type: 'number',
1136
+ description: 'Which match to replace (1-indexed, default: 1)',
1137
+ },
1138
+ },
1139
+ required: ['path', 'searchContent', 'replaceContent'],
1140
+ },
1141
+ description: 'Array of edit config objects for per-file search-replace operations',
1142
+ },
1143
+ ],
1144
+ description: 'File path(s) to edit',
1346
1145
  },
1347
1146
  searchContent: {
1348
1147
  type: 'string',
1349
- description: 'Content to find and replace. Copy from filesystem_read output WITHOUT line numbers (e.g., "123→"). Whitespace differences are automatically handled - focus on getting the content right.',
1148
+ description: 'Content to find and replace (for single file or unified mode). Copy from filesystem-read WITHOUT line numbers.',
1350
1149
  },
1351
1150
  replaceContent: {
1352
1151
  type: 'string',
1353
- description: 'New content to replace with. Indentation will be preserved automatically.',
1152
+ description: 'New content to replace with (for single file or unified mode)',
1354
1153
  },
1355
1154
  occurrence: {
1356
1155
  type: 'number',
@@ -1363,30 +1162,68 @@ export const mcpTools = [
1363
1162
  default: 8,
1364
1163
  },
1365
1164
  },
1366
- required: ['filePath', 'searchContent', 'replaceContent'],
1165
+ required: ['filePath'],
1367
1166
  },
1368
1167
  },
1369
1168
  {
1370
- name: 'filesystem_edit',
1371
- description: "🔧 Line-based editing for precise control. **WHEN TO USE**: (1) Adding completely new code sections, (2) Deleting specific line ranges, (3) When search-replace is not suitable. **WORKFLOW**: (1) Use ace_text_search/ace_file_outline to locate relevant area, (2) Use filesystem_read to get exact line numbers, (3) Use THIS tool with precise line ranges. **RECOMMENDATION**: For modifying existing code, use filesystem_edit_search instead - it's safer. **BEST PRACTICES**: Keep edits small (≤15 lines), double-check line numbers, verify bracket closure.",
1169
+ name: 'filesystem-edit',
1170
+ description: '🔧 Line-based editing for precise control. **SUPPORTS BATCH EDITING**: Pass (1) single file with line range, (2) array of file paths with unified line range, or (3) array of {path, startLine, endLine, newContent} for per-file edits. **WHEN TO USE**: (1) Adding new code sections, (2) Deleting specific line ranges, (3) When search-replace not suitable. **WORKFLOW**: (1) Use ace-text_search/ace-file_outline to locate area, (2) Use filesystem-read to get line numbers, (3) Use THIS tool. **RECOMMENDATION**: For modifying existing code, use filesystem-edit_search - safer. **BATCH EXAMPLE**: filePath=[{path:"a.ts", startLine:10, endLine:20, newContent:"..."}, {path:"b.ts", startLine:50, endLine:60, newContent:"..."}]',
1372
1171
  inputSchema: {
1373
1172
  type: 'object',
1374
1173
  properties: {
1375
1174
  filePath: {
1376
- type: 'string',
1377
- description: 'Path to the file to edit (absolute or relative)',
1175
+ oneOf: [
1176
+ {
1177
+ type: 'string',
1178
+ description: 'Path to a single file to edit',
1179
+ },
1180
+ {
1181
+ type: 'array',
1182
+ items: {
1183
+ type: 'string',
1184
+ },
1185
+ description: 'Array of file paths (uses unified startLine/endLine/newContent from top-level)',
1186
+ },
1187
+ {
1188
+ type: 'array',
1189
+ items: {
1190
+ type: 'object',
1191
+ properties: {
1192
+ path: {
1193
+ type: 'string',
1194
+ description: 'File path',
1195
+ },
1196
+ startLine: {
1197
+ type: 'number',
1198
+ description: 'Starting line number (1-indexed, inclusive)',
1199
+ },
1200
+ endLine: {
1201
+ type: 'number',
1202
+ description: 'Ending line number (1-indexed, inclusive)',
1203
+ },
1204
+ newContent: {
1205
+ type: 'string',
1206
+ description: 'New content to replace lines (without line numbers)',
1207
+ },
1208
+ },
1209
+ required: ['path', 'startLine', 'endLine', 'newContent'],
1210
+ },
1211
+ description: 'Array of edit config objects for per-file line-based edits',
1212
+ },
1213
+ ],
1214
+ description: 'File path(s) to edit',
1378
1215
  },
1379
1216
  startLine: {
1380
1217
  type: 'number',
1381
- description: '⚠️ CRITICAL: Starting line number (1-indexed, inclusive). MUST match exact line number from filesystem_read output. Double-check this value!',
1218
+ description: '⚠️ CRITICAL: Starting line number (1-indexed, inclusive) for single file or unified mode. MUST match filesystem-read output.',
1382
1219
  },
1383
1220
  endLine: {
1384
1221
  type: 'number',
1385
- description: '⚠️ CRITICAL: Ending line number (1-indexed, inclusive). MUST match exact line number from filesystem_read output. 💡 TIP: Keep edits small (≤15 lines recommended) for better accuracy.',
1222
+ description: '⚠️ CRITICAL: Ending line number (1-indexed, inclusive) for single file or unified mode. Keep edits small (≤15 lines).',
1386
1223
  },
1387
1224
  newContent: {
1388
1225
  type: 'string',
1389
- description: 'New content to replace specified lines. ⚠️ Do NOT include line numbers. ⚠️ Ensure proper indentation and bracket closure. Keep changes MINIMAL and FOCUSED.',
1226
+ description: 'New content to replace specified lines (for single file or unified mode). ⚠️ Do NOT include line numbers. Ensure proper indentation.',
1390
1227
  },
1391
1228
  contextLines: {
1392
1229
  type: 'number',
@@ -1394,7 +1231,7 @@ export const mcpTools = [
1394
1231
  default: 8,
1395
1232
  },
1396
1233
  },
1397
- required: ['filePath', 'startLine', 'endLine', 'newContent'],
1234
+ required: ['filePath'],
1398
1235
  },
1399
1236
  },
1400
1237
  ];