snow-ai 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/reviewAgent.d.ts +50 -0
- package/dist/agents/reviewAgent.js +264 -0
- package/dist/api/anthropic.js +104 -71
- package/dist/api/chat.d.ts +1 -1
- package/dist/api/chat.js +60 -41
- package/dist/api/gemini.js +97 -57
- package/dist/api/responses.d.ts +9 -1
- package/dist/api/responses.js +110 -70
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +36 -7
- package/dist/api/types.d.ts +8 -0
- package/dist/hooks/useCommandHandler.d.ts +1 -0
- package/dist/hooks/useCommandHandler.js +44 -1
- package/dist/hooks/useCommandPanel.js +13 -0
- package/dist/hooks/useConversation.d.ts +4 -1
- package/dist/hooks/useConversation.js +48 -6
- package/dist/hooks/useKeyboardInput.js +19 -0
- package/dist/hooks/useTerminalFocus.js +13 -3
- package/dist/mcp/aceCodeSearch.d.ts +2 -76
- package/dist/mcp/aceCodeSearch.js +31 -467
- package/dist/mcp/bash.d.ts +1 -8
- package/dist/mcp/bash.js +20 -40
- package/dist/mcp/filesystem.d.ts +3 -68
- package/dist/mcp/filesystem.js +32 -348
- package/dist/mcp/ideDiagnostics.js +2 -4
- package/dist/mcp/todo.d.ts +1 -17
- package/dist/mcp/todo.js +11 -15
- package/dist/mcp/types/aceCodeSearch.types.d.ts +92 -0
- package/dist/mcp/types/aceCodeSearch.types.js +4 -0
- package/dist/mcp/types/bash.types.d.ts +13 -0
- package/dist/mcp/types/bash.types.js +4 -0
- package/dist/mcp/types/filesystem.types.d.ts +44 -0
- package/dist/mcp/types/filesystem.types.js +4 -0
- package/dist/mcp/types/todo.types.d.ts +27 -0
- package/dist/mcp/types/todo.types.js +4 -0
- package/dist/mcp/types/websearch.types.d.ts +30 -0
- package/dist/mcp/types/websearch.types.js +4 -0
- package/dist/mcp/utils/aceCodeSearch/filesystem.utils.d.ts +34 -0
- package/dist/mcp/utils/aceCodeSearch/filesystem.utils.js +146 -0
- package/dist/mcp/utils/aceCodeSearch/language.utils.d.ts +14 -0
- package/dist/mcp/utils/aceCodeSearch/language.utils.js +99 -0
- package/dist/mcp/utils/aceCodeSearch/search.utils.d.ts +31 -0
- package/dist/mcp/utils/aceCodeSearch/search.utils.js +136 -0
- package/dist/mcp/utils/aceCodeSearch/symbol.utils.d.ts +20 -0
- package/dist/mcp/utils/aceCodeSearch/symbol.utils.js +141 -0
- package/dist/mcp/utils/bash/security.utils.d.ts +20 -0
- package/dist/mcp/utils/bash/security.utils.js +34 -0
- package/dist/mcp/utils/filesystem/code-analysis.utils.d.ts +18 -0
- package/dist/mcp/utils/filesystem/code-analysis.utils.js +165 -0
- package/dist/mcp/utils/filesystem/match-finder.utils.d.ts +16 -0
- package/dist/mcp/utils/filesystem/match-finder.utils.js +85 -0
- package/dist/mcp/utils/filesystem/similarity.utils.d.ts +22 -0
- package/dist/mcp/utils/filesystem/similarity.utils.js +75 -0
- package/dist/mcp/utils/todo/date.utils.d.ts +9 -0
- package/dist/mcp/utils/todo/date.utils.js +14 -0
- package/dist/mcp/utils/websearch/browser.utils.d.ts +8 -0
- package/dist/mcp/utils/websearch/browser.utils.js +58 -0
- package/dist/mcp/utils/websearch/text.utils.d.ts +16 -0
- package/dist/mcp/utils/websearch/text.utils.js +39 -0
- package/dist/mcp/websearch.d.ts +1 -31
- package/dist/mcp/websearch.js +21 -97
- package/dist/ui/components/ChatInput.d.ts +2 -1
- package/dist/ui/components/ChatInput.js +10 -3
- package/dist/ui/components/MarkdownRenderer.d.ts +1 -2
- package/dist/ui/components/MarkdownRenderer.js +16 -153
- package/dist/ui/components/MessageList.js +4 -4
- package/dist/ui/components/SessionListScreen.js +37 -17
- package/dist/ui/components/ToolResultPreview.js +6 -6
- package/dist/ui/components/UsagePanel.d.ts +2 -0
- package/dist/ui/components/UsagePanel.js +360 -0
- package/dist/ui/pages/ChatScreen.d.ts +4 -0
- package/dist/ui/pages/ChatScreen.js +70 -30
- package/dist/ui/pages/ConfigScreen.js +23 -19
- package/dist/ui/pages/HeadlessModeScreen.js +2 -4
- package/dist/ui/pages/SubAgentConfigScreen.js +17 -17
- package/dist/ui/pages/SystemPromptConfigScreen.js +7 -6
- package/dist/utils/commandExecutor.d.ts +3 -3
- package/dist/utils/commandExecutor.js +4 -4
- package/dist/utils/commands/home.d.ts +2 -0
- package/dist/utils/commands/home.js +12 -0
- package/dist/utils/commands/review.d.ts +2 -0
- package/dist/utils/commands/review.js +81 -0
- package/dist/utils/commands/role.d.ts +2 -0
- package/dist/utils/commands/role.js +37 -0
- package/dist/utils/commands/usage.d.ts +2 -0
- package/dist/utils/commands/usage.js +12 -0
- package/dist/utils/contextCompressor.js +99 -367
- package/dist/utils/fileUtils.js +3 -3
- package/dist/utils/mcpToolsManager.js +12 -12
- package/dist/utils/proxyUtils.d.ts +15 -0
- package/dist/utils/proxyUtils.js +50 -0
- package/dist/utils/retryUtils.d.ts +27 -0
- package/dist/utils/retryUtils.js +114 -2
- package/dist/utils/sessionManager.d.ts +2 -5
- package/dist/utils/sessionManager.js +16 -83
- package/dist/utils/terminal.js +4 -3
- package/dist/utils/usageLogger.d.ts +11 -0
- package/dist/utils/usageLogger.js +99 -0
- package/package.json +3 -7
- package/dist/agents/summaryAgent.d.ts +0 -31
- package/dist/agents/summaryAgent.js +0 -256
package/dist/mcp/filesystem.js
CHANGED
|
@@ -6,6 +6,10 @@ 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';
|
|
9
13
|
const { resolve, dirname, isAbsolute } = path;
|
|
10
14
|
const execAsync = promisify(exec);
|
|
11
15
|
/**
|
|
@@ -47,323 +51,6 @@ export class FilesystemMCPService {
|
|
|
47
51
|
});
|
|
48
52
|
this.basePath = resolve(basePath);
|
|
49
53
|
}
|
|
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
54
|
/**
|
|
368
55
|
* Get the content of a file with optional line range
|
|
369
56
|
* @param filePath - Path to the file (relative to base path or absolute) or array of file paths
|
|
@@ -693,7 +380,7 @@ export class FilesystemMCPService {
|
|
|
693
380
|
// Quick pre-filter: check first line similarity (only for multi-line searches)
|
|
694
381
|
if (usePreFilter) {
|
|
695
382
|
const firstLineCandidate = contentLines[i]?.replace(/\s+/g, ' ').trim() || '';
|
|
696
|
-
const firstLineSimilarity =
|
|
383
|
+
const firstLineSimilarity = calculateSimilarity(searchFirstLine, firstLineCandidate, preFilterThreshold);
|
|
697
384
|
// Skip only if first line is very different (< 30% match)
|
|
698
385
|
// This is safe because if first line differs this much, full match unlikely
|
|
699
386
|
if (firstLineSimilarity < preFilterThreshold) {
|
|
@@ -703,7 +390,7 @@ export class FilesystemMCPService {
|
|
|
703
390
|
// Full candidate check
|
|
704
391
|
const candidateLines = contentLines.slice(i, i + searchLines.length);
|
|
705
392
|
const candidateContent = candidateLines.join('\n');
|
|
706
|
-
const similarity =
|
|
393
|
+
const similarity = calculateSimilarity(normalizedSearch, candidateContent, threshold);
|
|
707
394
|
// Accept matches above threshold
|
|
708
395
|
if (similarity >= threshold) {
|
|
709
396
|
matches.push({
|
|
@@ -737,7 +424,7 @@ export class FilesystemMCPService {
|
|
|
737
424
|
}
|
|
738
425
|
const candidateLines = contentLines.slice(i, i + correctedSearchLines.length);
|
|
739
426
|
const candidateContent = candidateLines.join('\n');
|
|
740
|
-
const similarity =
|
|
427
|
+
const similarity = calculateSimilarity(unescapeFix.correctedString, candidateContent);
|
|
741
428
|
if (similarity >= threshold) {
|
|
742
429
|
matches.push({
|
|
743
430
|
startLine: i + 1,
|
|
@@ -760,7 +447,7 @@ export class FilesystemMCPService {
|
|
|
760
447
|
// If still no matches after unescape, provide detailed error
|
|
761
448
|
if (matches.length === 0) {
|
|
762
449
|
// Find closest matches for suggestions
|
|
763
|
-
const closestMatches = await
|
|
450
|
+
const closestMatches = await findClosestMatches(normalizedSearch, normalizedContent.split('\n'), 3);
|
|
764
451
|
let errorMessage = `❌ Search content not found in file: ${filePath}\n\n`;
|
|
765
452
|
errorMessage += `🔍 Using smart fuzzy matching (threshold: 60%)\n`;
|
|
766
453
|
if (isOverEscaped(searchContent)) {
|
|
@@ -778,23 +465,22 @@ export class FilesystemMCPService {
|
|
|
778
465
|
if (bestMatch) {
|
|
779
466
|
const bestMatchLines = lines.slice(bestMatch.startLine - 1, bestMatch.endLine);
|
|
780
467
|
const bestMatchContent = bestMatchLines.join('\n');
|
|
781
|
-
const diffMsg =
|
|
468
|
+
const diffMsg = generateDiffMessage(normalizedSearch, bestMatchContent, 5);
|
|
782
469
|
if (diffMsg) {
|
|
783
470
|
errorMessage += `📊 Difference with closest match:\n${diffMsg}\n\n`;
|
|
784
471
|
}
|
|
785
472
|
}
|
|
786
473
|
errorMessage += `💡 Suggestions:\n`;
|
|
787
|
-
errorMessage += ` • Make sure you copied content from
|
|
474
|
+
errorMessage += ` • Make sure you copied content from filesystem-read (without "123→")\n`;
|
|
788
475
|
errorMessage += ` • Whitespace differences are automatically handled\n`;
|
|
789
476
|
errorMessage += ` • Try copying a larger or smaller code block\n`;
|
|
790
|
-
errorMessage += ` • If multiple
|
|
477
|
+
errorMessage += ` • If multiple filesystem-edit_search attempts fail, use terminal-execute to edit via command line (e.g. sed, printf)\n`;
|
|
791
478
|
errorMessage += `⚠️ No similar content found in the file.\n\n`;
|
|
792
479
|
errorMessage += `📝 What you searched for (first 5 lines, formatted):\n`;
|
|
793
|
-
const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ').trim();
|
|
794
480
|
searchLines.slice(0, 5).forEach((line, idx) => {
|
|
795
481
|
errorMessage += `${idx + 1}. ${JSON.stringify(normalizeForDisplay(line))}\n`;
|
|
796
482
|
});
|
|
797
|
-
errorMessage += `\n💡 Copy exact content from
|
|
483
|
+
errorMessage += `\n💡 Copy exact content from filesystem-read (without line numbers)\n`;
|
|
798
484
|
}
|
|
799
485
|
throw new Error(errorMessage);
|
|
800
486
|
}
|
|
@@ -829,7 +515,6 @@ export class FilesystemMCPService {
|
|
|
829
515
|
const modifiedLines = [...beforeLines, ...replaceLines, ...afterLines];
|
|
830
516
|
const modifiedContent = modifiedLines.join('\n');
|
|
831
517
|
// Calculate replaced content for display (compress whitespace for readability)
|
|
832
|
-
const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ');
|
|
833
518
|
const replacedLines = lines.slice(startLine - 1, endLine);
|
|
834
519
|
const replacedContent = replacedLines
|
|
835
520
|
.map((line, idx) => {
|
|
@@ -839,7 +524,7 @@ export class FilesystemMCPService {
|
|
|
839
524
|
.join('\n');
|
|
840
525
|
// Calculate context boundaries
|
|
841
526
|
const lineDifference = replaceLines.length - (endLine - startLine + 1);
|
|
842
|
-
const smartBoundaries =
|
|
527
|
+
const smartBoundaries = findSmartContextBoundaries(lines, startLine, endLine, contextLines);
|
|
843
528
|
const contextStart = smartBoundaries.start;
|
|
844
529
|
const contextEnd = smartBoundaries.end;
|
|
845
530
|
// Extract old content for context (compress whitespace for readability)
|
|
@@ -885,7 +570,7 @@ export class FilesystemMCPService {
|
|
|
885
570
|
.join('\n');
|
|
886
571
|
// Analyze code structure
|
|
887
572
|
const editedContentLines = replaceLines;
|
|
888
|
-
const structureAnalysis =
|
|
573
|
+
const structureAnalysis = analyzeCodeStructure(finalContent, filePath, editedContentLines);
|
|
889
574
|
// Get diagnostics from IDE (VSCode or JetBrains) - non-blocking, fire-and-forget
|
|
890
575
|
let diagnostics = [];
|
|
891
576
|
try {
|
|
@@ -969,7 +654,7 @@ export class FilesystemMCPService {
|
|
|
969
654
|
}
|
|
970
655
|
}
|
|
971
656
|
if (structureAnalysis.indentationWarnings.length > 0) {
|
|
972
|
-
structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
|
|
657
|
+
structureWarnings.push(...structureAnalysis.indentationWarnings.map((w) => `Indentation: ${w}`));
|
|
973
658
|
}
|
|
974
659
|
// Note: Boundary warnings removed - partial edits are common and expected
|
|
975
660
|
if (structureWarnings.length > 0) {
|
|
@@ -991,8 +676,8 @@ export class FilesystemMCPService {
|
|
|
991
676
|
* For larger changes, make multiple parallel edits to non-overlapping sections instead of one large edit.
|
|
992
677
|
*
|
|
993
678
|
* @param filePath - Path to the file to edit
|
|
994
|
-
* @param startLine - Starting line number (1-indexed, inclusive) - get from
|
|
995
|
-
* @param endLine - Ending line number (1-indexed, inclusive) - get from
|
|
679
|
+
* @param startLine - Starting line number (1-indexed, inclusive) - get from filesystem-read output
|
|
680
|
+
* @param endLine - Ending line number (1-indexed, inclusive) - get from filesystem-read output
|
|
996
681
|
* @param newContent - New content to replace the specified lines (WITHOUT line numbers)
|
|
997
682
|
* @param contextLines - Number of context lines to return before and after the edit (default: 8)
|
|
998
683
|
* @returns Object containing success message, precise before/after comparison, and diagnostics from IDE (VSCode or JetBrains)
|
|
@@ -1026,7 +711,6 @@ export class FilesystemMCPService {
|
|
|
1026
711
|
await incrementalSnapshotManager.backupFile(fullPath);
|
|
1027
712
|
// Extract the lines that will be replaced (for comparison)
|
|
1028
713
|
// Compress whitespace for display readability
|
|
1029
|
-
const normalizeForDisplay = (line) => line.replace(/\s+/g, ' ');
|
|
1030
714
|
const replacedLines = lines.slice(startLine - 1, adjustedEndLine);
|
|
1031
715
|
const replacedContent = replacedLines
|
|
1032
716
|
.map((line, idx) => {
|
|
@@ -1035,7 +719,7 @@ export class FilesystemMCPService {
|
|
|
1035
719
|
})
|
|
1036
720
|
.join('\n');
|
|
1037
721
|
// Calculate context range using smart boundary detection
|
|
1038
|
-
const smartBoundaries =
|
|
722
|
+
const smartBoundaries = findSmartContextBoundaries(lines, startLine, adjustedEndLine, contextLines);
|
|
1039
723
|
const contextStart = smartBoundaries.start;
|
|
1040
724
|
const contextEnd = smartBoundaries.end;
|
|
1041
725
|
// Extract old content for context (compress whitespace for readability)
|
|
@@ -1100,7 +784,7 @@ export class FilesystemMCPService {
|
|
|
1100
784
|
}
|
|
1101
785
|
// Analyze code structure of the edited content (using formatted content if available)
|
|
1102
786
|
const editedContentLines = finalLines.slice(startLine - 1, startLine - 1 + newContentLines.length);
|
|
1103
|
-
const structureAnalysis =
|
|
787
|
+
const structureAnalysis = analyzeCodeStructure(finalLines.join('\n'), filePath, editedContentLines);
|
|
1104
788
|
// Try to get diagnostics from IDE (VSCode or JetBrains) after editing (non-blocking)
|
|
1105
789
|
let diagnostics = [];
|
|
1106
790
|
try {
|
|
@@ -1185,7 +869,7 @@ export class FilesystemMCPService {
|
|
|
1185
869
|
}
|
|
1186
870
|
// Check indentation
|
|
1187
871
|
if (structureAnalysis.indentationWarnings.length > 0) {
|
|
1188
|
-
structureWarnings.push(...structureAnalysis.indentationWarnings.map(w => `Indentation: ${w}`));
|
|
872
|
+
structureWarnings.push(...structureAnalysis.indentationWarnings.map((w) => `Indentation: ${w}`));
|
|
1189
873
|
}
|
|
1190
874
|
// Note: Boundary warnings removed - partial edits are common and expected
|
|
1191
875
|
// Format structure warnings
|
|
@@ -1234,8 +918,8 @@ export class FilesystemMCPService {
|
|
|
1234
918
|
export const filesystemService = new FilesystemMCPService();
|
|
1235
919
|
export const mcpTools = [
|
|
1236
920
|
{
|
|
1237
|
-
name: '
|
|
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 (
|
|
921
|
+
name: 'filesystem-read',
|
|
922
|
+
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.',
|
|
1239
923
|
inputSchema: {
|
|
1240
924
|
type: 'object',
|
|
1241
925
|
properties: {
|
|
@@ -1268,7 +952,7 @@ export const mcpTools = [
|
|
|
1268
952
|
},
|
|
1269
953
|
},
|
|
1270
954
|
{
|
|
1271
|
-
name: '
|
|
955
|
+
name: 'filesystem-create',
|
|
1272
956
|
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
957
|
inputSchema: {
|
|
1274
958
|
type: 'object',
|
|
@@ -1291,7 +975,7 @@ export const mcpTools = [
|
|
|
1291
975
|
},
|
|
1292
976
|
},
|
|
1293
977
|
{
|
|
1294
|
-
name: '
|
|
978
|
+
name: 'filesystem-delete',
|
|
1295
979
|
description: 'Delete one or multiple files. Supports both single file and batch deletion.',
|
|
1296
980
|
inputSchema: {
|
|
1297
981
|
type: 'object',
|
|
@@ -1321,7 +1005,7 @@ export const mcpTools = [
|
|
|
1321
1005
|
},
|
|
1322
1006
|
},
|
|
1323
1007
|
{
|
|
1324
|
-
name: '
|
|
1008
|
+
name: 'filesystem-list',
|
|
1325
1009
|
description: 'List files in a directory',
|
|
1326
1010
|
inputSchema: {
|
|
1327
1011
|
type: 'object',
|
|
@@ -1335,8 +1019,8 @@ export const mcpTools = [
|
|
|
1335
1019
|
},
|
|
1336
1020
|
},
|
|
1337
1021
|
{
|
|
1338
|
-
name: '
|
|
1339
|
-
description: '🎯 **RECOMMENDED** for most edits: Search-and-replace with SMART FUZZY MATCHING that automatically handles whitespace differences. **WORKFLOW**: (1) Use
|
|
1022
|
+
name: 'filesystem-edit_search',
|
|
1023
|
+
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.',
|
|
1340
1024
|
inputSchema: {
|
|
1341
1025
|
type: 'object',
|
|
1342
1026
|
properties: {
|
|
@@ -1346,7 +1030,7 @@ export const mcpTools = [
|
|
|
1346
1030
|
},
|
|
1347
1031
|
searchContent: {
|
|
1348
1032
|
type: 'string',
|
|
1349
|
-
description: 'Content to find and replace. Copy from
|
|
1033
|
+
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.',
|
|
1350
1034
|
},
|
|
1351
1035
|
replaceContent: {
|
|
1352
1036
|
type: 'string',
|
|
@@ -1367,8 +1051,8 @@ export const mcpTools = [
|
|
|
1367
1051
|
},
|
|
1368
1052
|
},
|
|
1369
1053
|
{
|
|
1370
|
-
name: '
|
|
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
|
|
1054
|
+
name: 'filesystem-edit',
|
|
1055
|
+
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.",
|
|
1372
1056
|
inputSchema: {
|
|
1373
1057
|
type: 'object',
|
|
1374
1058
|
properties: {
|
|
@@ -1378,11 +1062,11 @@ export const mcpTools = [
|
|
|
1378
1062
|
},
|
|
1379
1063
|
startLine: {
|
|
1380
1064
|
type: 'number',
|
|
1381
|
-
description: '⚠️ CRITICAL: Starting line number (1-indexed, inclusive). MUST match exact line number from
|
|
1065
|
+
description: '⚠️ CRITICAL: Starting line number (1-indexed, inclusive). MUST match exact line number from filesystem-read output. Double-check this value!',
|
|
1382
1066
|
},
|
|
1383
1067
|
endLine: {
|
|
1384
1068
|
type: 'number',
|
|
1385
|
-
description: '⚠️ CRITICAL: Ending line number (1-indexed, inclusive). MUST match exact line number from
|
|
1069
|
+
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.',
|
|
1386
1070
|
},
|
|
1387
1071
|
newContent: {
|
|
1388
1072
|
type: 'string',
|
|
@@ -44,9 +44,7 @@ export class IdeDiagnosticsMCPService {
|
|
|
44
44
|
// Add summary
|
|
45
45
|
const counts = [
|
|
46
46
|
grouped.error.length > 0 ? `${grouped.error.length} errors` : null,
|
|
47
|
-
grouped.warning.length > 0
|
|
48
|
-
? `${grouped.warning.length} warnings`
|
|
49
|
-
: null,
|
|
47
|
+
grouped.warning.length > 0 ? `${grouped.warning.length} warnings` : null,
|
|
50
48
|
grouped.info.length > 0 ? `${grouped.info.length} info` : null,
|
|
51
49
|
grouped.hint.length > 0 ? `${grouped.hint.length} hints` : null,
|
|
52
50
|
].filter(Boolean);
|
|
@@ -76,7 +74,7 @@ export const ideDiagnosticsService = new IdeDiagnosticsMCPService();
|
|
|
76
74
|
// Export MCP tool definitions
|
|
77
75
|
export const mcpTools = [
|
|
78
76
|
{
|
|
79
|
-
name: '
|
|
77
|
+
name: 'ide-get_diagnostics',
|
|
80
78
|
description: '🔍 Get diagnostics (errors, warnings, hints) for a specific file from the connected IDE. Works with both VSCode and JetBrains IDEs. Returns array of diagnostic information including severity, line number, character position, message, and source. Requires IDE plugin to be installed and running.',
|
|
81
79
|
inputSchema: {
|
|
82
80
|
type: 'object',
|
package/dist/mcp/todo.d.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import { Tool, type CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
|
|
3
|
-
id: string;
|
|
4
|
-
content: string;
|
|
5
|
-
status: 'pending' | 'completed';
|
|
6
|
-
createdAt: string;
|
|
7
|
-
updatedAt: string;
|
|
8
|
-
parentId?: string;
|
|
9
|
-
}
|
|
10
|
-
interface TodoList {
|
|
11
|
-
sessionId: string;
|
|
12
|
-
todos: TodoItem[];
|
|
13
|
-
createdAt: string;
|
|
14
|
-
updatedAt: string;
|
|
15
|
-
}
|
|
16
|
-
type GetCurrentSessionId = () => string | null;
|
|
2
|
+
import type { TodoItem, TodoList, GetCurrentSessionId } from './types/todo.types.js';
|
|
17
3
|
/**
|
|
18
4
|
* TODO 管理服务 - 支持创建、查询、更新 TODO
|
|
19
5
|
*/
|
|
@@ -23,7 +9,6 @@ export declare class TodoService {
|
|
|
23
9
|
constructor(baseDir: string, getCurrentSessionId: GetCurrentSessionId);
|
|
24
10
|
initialize(): Promise<void>;
|
|
25
11
|
private getTodoPath;
|
|
26
|
-
private formatDateForFolder;
|
|
27
12
|
private ensureTodoDir;
|
|
28
13
|
/**
|
|
29
14
|
* 创建或更新会话的 TODO List
|
|
@@ -59,4 +44,3 @@ export declare class TodoService {
|
|
|
59
44
|
*/
|
|
60
45
|
executeTool(toolName: string, args: Record<string, unknown>): Promise<CallToolResult>;
|
|
61
46
|
}
|
|
62
|
-
export {};
|